1use std::env;
10use std::time::Duration;
11
12use serde_json::{json, Value};
13use thiserror::Error;
14use tracing::warn;
15
16use apcore::module::ModuleAnnotations;
17
18use crate::types::ScannedModule;
19
20const DEFAULT_ENDPOINT: &str = "http://localhost:11434/v1";
21const DEFAULT_MODEL: &str = "qwen:0.6b";
22const DEFAULT_THRESHOLD: f64 = 0.7;
23const DEFAULT_BATCH_SIZE: usize = 5;
24const DEFAULT_TIMEOUT: u64 = 30;
25
26#[derive(Debug, Error)]
28pub enum AIEnhancerError {
29 #[error("invalid config: {0}")]
31 Config(String),
32 #[error("connection failed: {0}")]
34 Connection(String),
35 #[error("bad response: {0}")]
37 Response(String),
38}
39
40pub trait Enhancer {
42 fn enhance(&self, modules: Vec<ScannedModule>) -> Vec<ScannedModule>;
44}
45
46#[derive(Debug)]
56pub struct AIEnhancer {
57 pub endpoint: String,
58 pub model: String,
59 pub threshold: f64,
60 pub batch_size: usize,
61 pub timeout: u64,
62}
63
64impl AIEnhancer {
65 pub fn new(
69 endpoint: Option<String>,
70 model: Option<String>,
71 threshold: Option<f64>,
72 batch_size: Option<usize>,
73 timeout: Option<u64>,
74 ) -> Result<Self, AIEnhancerError> {
75 let endpoint = endpoint.unwrap_or_else(|| {
76 env::var("APCORE_AI_ENDPOINT").unwrap_or_else(|_| DEFAULT_ENDPOINT.into())
77 });
78 let model = model.unwrap_or_else(|| {
79 env::var("APCORE_AI_MODEL").unwrap_or_else(|_| DEFAULT_MODEL.into())
80 });
81 let threshold =
82 threshold.unwrap_or_else(|| parse_float_env("APCORE_AI_THRESHOLD", DEFAULT_THRESHOLD));
83 let batch_size = batch_size
84 .unwrap_or_else(|| parse_usize_env("APCORE_AI_BATCH_SIZE", DEFAULT_BATCH_SIZE));
85 let timeout =
86 timeout.unwrap_or_else(|| parse_u64_env("APCORE_AI_TIMEOUT", DEFAULT_TIMEOUT));
87
88 if !(0.0..=1.0).contains(&threshold) {
89 return Err(AIEnhancerError::Config(
90 "APCORE_AI_THRESHOLD must be between 0.0 and 1.0".into(),
91 ));
92 }
93 if batch_size == 0 {
94 return Err(AIEnhancerError::Config(
95 "APCORE_AI_BATCH_SIZE must be a positive integer".into(),
96 ));
97 }
98 if timeout == 0 {
99 return Err(AIEnhancerError::Config(
100 "APCORE_AI_TIMEOUT must be a positive integer".into(),
101 ));
102 }
103
104 Ok(Self {
105 endpoint,
106 model,
107 threshold,
108 batch_size,
109 timeout,
110 })
111 }
112
113 pub fn is_enabled() -> bool {
115 env::var("APCORE_AI_ENABLED")
116 .map(|v| matches!(v.to_lowercase().as_str(), "true" | "1" | "yes"))
117 .unwrap_or(false)
118 }
119
120 fn identify_gaps(&self, module: &ScannedModule) -> Vec<String> {
122 let mut gaps: Vec<String> = Vec::new();
123
124 if module.description.is_empty() || module.description == module.module_id {
125 gaps.push("description".into());
126 }
127 if module.documentation.is_none() {
128 gaps.push("documentation".into());
129 }
130 if module.annotations.is_none()
131 || module
132 .annotations
133 .as_ref()
134 .is_some_and(is_default_annotations)
135 {
136 gaps.push("annotations".into());
137 }
138 if module
139 .input_schema
140 .get("properties")
141 .and_then(|p| p.as_object())
142 .map(|o| o.is_empty())
143 .unwrap_or(true)
144 {
145 gaps.push("input_schema".into());
146 }
147
148 gaps
149 }
150
151 fn build_prompt(&self, module: &ScannedModule, gaps: &[String]) -> String {
153 let mut parts = vec![
154 "You are analyzing a function to generate metadata for an AI-perceivable module system.".into(),
155 String::new(),
156 format!("Module ID: {}", module.module_id),
157 format!("Target: {}", module.target),
158 ];
159
160 if !module.description.is_empty() {
161 parts.push(format!("Current description: {}", module.description));
162 }
163
164 parts.push(String::new());
165 parts.push("Please provide the following missing metadata as JSON:".into());
166 parts.push("{".into());
167
168 for gap in gaps {
169 match gap.as_str() {
170 "description" => {
171 parts.push(
172 r#" "description": "<≤200 chars, what this function does>","#.into(),
173 );
174 }
175 "documentation" => {
176 parts.push(r#" "documentation": "<detailed Markdown explanation>","#.into());
177 }
178 "annotations" => {
179 parts.push(r#" "annotations": {"#.into());
180 parts.push(r#" "readonly": <true if no side effects>,"#.into());
181 parts.push(r#" "destructive": <true if deletes/overwrites data>,"#.into());
182 parts.push(r#" "idempotent": <true if safe to retry>,"#.into());
183 parts.push(r#" "requires_approval": <true if dangerous operation>,"#.into());
184 parts.push(r#" "open_world": <true if calls external systems>,"#.into());
185 parts
186 .push(r#" "streaming": <true if yields results incrementally>,"#.into());
187 parts.push(r#" "cacheable": <true if results can be cached>,"#.into());
188 parts.push(r#" "cache_ttl": <seconds, 0 for no expiry>,"#.into());
189 parts.push(r#" "cache_key_fields": <list of input field names for cache key, or null for all>,"#.into());
190 parts.push(r#" "paginated": <true if supports pagination>,"#.into());
191 parts
192 .push(r#" "pagination_style": <"cursor" or "offset" or "page">"#.into());
193 parts.push(" },".into());
194 }
195 "input_schema" => {
196 parts.push(
197 r#" "input_schema": <JSON Schema object for function parameters>,"#.into(),
198 );
199 }
200 _ => {}
201 }
202 }
203
204 parts.push(r#" "confidence": {"#.into());
205 parts.push(r#" "description": 0.0, "documentation": 0.0"#.into());
206 parts.push(" }".into());
207 parts.push("}".into());
208 parts.push(String::new());
209 parts.push("Respond with ONLY valid JSON, no markdown fences or explanation.".into());
210
211 parts.join("\n")
212 }
213
214 fn call_llm(&self, prompt: &str) -> Result<String, AIEnhancerError> {
216 let url = format!("{}/chat/completions", self.endpoint.trim_end_matches('/'));
217 let payload = json!({
218 "model": self.model,
219 "messages": [{"role": "user", "content": prompt}],
220 "temperature": 0.1,
221 });
222
223 let agent = ureq::Agent::config_builder()
224 .timeout_global(Some(Duration::from_secs(self.timeout)))
225 .build()
226 .new_agent();
227
228 let body: Value = agent
229 .post(&url)
230 .header("Content-Type", "application/json")
231 .send_json(&payload)
232 .map_err(|e| AIEnhancerError::Connection(format!("Failed to reach SLM at {url}: {e}")))?
233 .body_mut()
234 .read_json()
235 .map_err(|e| AIEnhancerError::Response(format!("Failed to parse SLM response: {e}")))?;
236
237 body["choices"][0]["message"]["content"]
238 .as_str()
239 .map(|s| s.to_string())
240 .ok_or_else(|| AIEnhancerError::Response("Unexpected API response structure".into()))
241 }
242
243 fn parse_response(response: &str) -> Result<Value, AIEnhancerError> {
245 let mut text = response.trim().to_string();
246
247 if text.starts_with("```") {
249 let lines: Vec<&str> = text.split('\n').collect();
250 let start = if lines[0].starts_with("```") { 1 } else { 0 };
251 let end = if lines.last().map(|l| l.trim()) == Some("```") {
252 lines.len() - 1
253 } else {
254 lines.len()
255 };
256 text = lines[start..end].join("\n");
257 }
258
259 serde_json::from_str(&text)
260 .map_err(|e| AIEnhancerError::Response(format!("SLM returned invalid JSON: {e}")))
261 }
262
263 fn enhance_module(
265 &self,
266 module: &ScannedModule,
267 gaps: &[String],
268 ) -> Result<ScannedModule, AIEnhancerError> {
269 let prompt = self.build_prompt(module, gaps);
270 let response = self.call_llm(&prompt)?;
271 let parsed = Self::parse_response(&response)?;
272
273 let mut result = module.clone();
274 let mut confidence: serde_json::Map<String, Value> = serde_json::Map::new();
275
276 if gaps.iter().any(|g| g == "description") {
278 if let Some(desc) = parsed.get("description").and_then(|v| v.as_str()) {
279 let conf = parsed
280 .get("confidence")
281 .and_then(|c| c.get("description"))
282 .and_then(|v| v.as_f64())
283 .unwrap_or(0.0);
284 confidence.insert("description".into(), json!(conf));
285 if conf >= self.threshold {
286 result.description = desc.to_string();
287 } else {
288 result.warnings.push(format!(
289 "Low confidence ({conf:.2}) for description — skipped. Review manually."
290 ));
291 }
292 }
293 }
294
295 if gaps.iter().any(|g| g == "documentation") {
297 if let Some(doc) = parsed.get("documentation").and_then(|v| v.as_str()) {
298 let conf = parsed
299 .get("confidence")
300 .and_then(|c| c.get("documentation"))
301 .and_then(|v| v.as_f64())
302 .unwrap_or(0.0);
303 confidence.insert("documentation".into(), json!(conf));
304 if conf >= self.threshold {
305 result.documentation = Some(doc.to_string());
306 } else {
307 result.warnings.push(format!(
308 "Low confidence ({conf:.2}) for documentation — skipped. Review manually."
309 ));
310 }
311 }
312 }
313
314 if gaps.iter().any(|g| g == "annotations") {
316 if let Some(ann_data) = parsed.get("annotations").and_then(|v| v.as_object()) {
317 let ann_conf = parsed
318 .get("confidence")
319 .and_then(|v| v.as_object())
320 .cloned()
321 .unwrap_or_default();
322 let mut base = module.annotations.clone().unwrap_or_default();
323 let mut any_accepted = false;
324
325 let bool_fields = [
333 "readonly",
334 "destructive",
335 "idempotent",
336 "requires_approval",
337 "open_world",
338 "streaming",
339 "cacheable",
340 "paginated",
341 ];
342 for field in &bool_fields {
343 if let Some(val) = ann_data.get(*field).and_then(|v| v.as_bool()) {
344 let field_conf = get_annotation_confidence(&ann_conf, field);
345 confidence.insert(format!("annotations.{field}"), json!(field_conf));
346 if field_conf >= self.threshold {
347 set_bool_annotation(&mut base, field, val);
348 any_accepted = true;
349 } else {
350 result.warnings.push(format!(
351 "Low confidence ({field_conf:.2}) for annotations.{field} — skipped. Review manually."
352 ));
353 }
354 }
355 }
356
357 if let Some(val) = ann_data.get("cache_ttl").and_then(|v| v.as_u64()) {
359 let field_conf = get_annotation_confidence(&ann_conf, "cache_ttl");
360 confidence.insert("annotations.cache_ttl".into(), json!(field_conf));
361 if field_conf >= self.threshold {
362 base.cache_ttl = val;
363 any_accepted = true;
364 } else {
365 result.warnings.push(format!(
366 "Low confidence ({field_conf:.2}) for annotations.cache_ttl — skipped. Review manually."
367 ));
368 }
369 }
370
371 if let Some(val) = ann_data.get("pagination_style").and_then(|v| v.as_str()) {
373 let field_conf = get_annotation_confidence(&ann_conf, "pagination_style");
374 confidence.insert("annotations.pagination_style".into(), json!(field_conf));
375 if field_conf >= self.threshold {
376 base.pagination_style = val.to_string();
377 any_accepted = true;
378 } else {
379 result.warnings.push(format!(
380 "Low confidence ({field_conf:.2}) for annotations.pagination_style — skipped. Review manually."
381 ));
382 }
383 }
384
385 if let Some(arr) = ann_data.get("cache_key_fields").and_then(|v| v.as_array()) {
387 let field_conf = get_annotation_confidence(&ann_conf, "cache_key_fields");
388 confidence.insert("annotations.cache_key_fields".into(), json!(field_conf));
389 if field_conf >= self.threshold {
390 let keys: Vec<String> = arr
391 .iter()
392 .filter_map(|v| v.as_str().map(|s| s.to_string()))
393 .collect();
394 base.cache_key_fields = Some(keys);
395 any_accepted = true;
396 } else {
397 result.warnings.push(format!(
398 "Low confidence ({field_conf:.2}) for annotations.cache_key_fields — skipped. Review manually."
399 ));
400 }
401 }
402
403 if any_accepted {
404 result.annotations = Some(base);
405 }
406 }
407 }
408
409 if gaps.iter().any(|g| g == "input_schema") {
411 if let Some(schema) = parsed.get("input_schema") {
412 let conf = parsed
413 .get("confidence")
414 .and_then(|c| c.get("input_schema"))
415 .and_then(|v| v.as_f64())
416 .unwrap_or(0.0);
417 confidence.insert("input_schema".into(), json!(conf));
418 if conf >= self.threshold {
419 result.input_schema = schema.clone();
420 } else {
421 result.warnings.push(format!(
422 "Low confidence ({conf:.2}) for input_schema — skipped. Review manually."
423 ));
424 }
425 }
426 }
427
428 if !confidence.is_empty() {
430 result
431 .metadata
432 .insert("x-generated-by".into(), Value::String("slm".into()));
433 result
434 .metadata
435 .insert("x-ai-confidence".into(), Value::Object(confidence));
436 }
437
438 Ok(result)
439 }
440}
441
442impl Enhancer for AIEnhancer {
443 fn enhance(&self, modules: Vec<ScannedModule>) -> Vec<ScannedModule> {
444 let mut results: Vec<ScannedModule> = Vec::with_capacity(modules.len());
445
446 let mut pending: Vec<(usize, Vec<String>)> = Vec::new();
447 for (idx, module) in modules.iter().enumerate() {
448 let gaps = self.identify_gaps(module);
449 results.push(module.clone());
450 if !gaps.is_empty() {
451 pending.push((idx, gaps));
452 }
453 }
454
455 for batch in pending.chunks(self.batch_size) {
456 for (idx, gaps) in batch {
457 match self.enhance_module(&modules[*idx], gaps) {
458 Ok(enhanced) => results[*idx] = enhanced,
459 Err(e) => {
460 warn!("AI enhancement failed for {}: {e}", modules[*idx].module_id);
461 }
462 }
463 }
464 }
465
466 results
467 }
468}
469
470fn is_default_annotations(ann: &ModuleAnnotations) -> bool {
477 match (
478 serde_json::to_value(ann),
479 serde_json::to_value(ModuleAnnotations::default()),
480 ) {
481 (Ok(a), Ok(b)) => a == b,
482 _ => false,
483 }
484}
485
486fn get_annotation_confidence(conf: &serde_json::Map<String, Value>, field: &str) -> f64 {
488 conf.get(&format!("annotations.{field}"))
489 .or_else(|| conf.get(field))
490 .and_then(|v| v.as_f64())
491 .unwrap_or(0.0)
492}
493
494fn set_bool_annotation(ann: &mut ModuleAnnotations, field: &str, value: bool) {
496 match field {
497 "readonly" => ann.readonly = value,
498 "destructive" => ann.destructive = value,
499 "idempotent" => ann.idempotent = value,
500 "requires_approval" => ann.requires_approval = value,
501 "open_world" => ann.open_world = value,
502 "streaming" => ann.streaming = value,
503 "cacheable" => ann.cacheable = value,
504 "paginated" => ann.paginated = value,
505 _ => {}
506 }
507}
508
509fn parse_float_env(name: &str, default: f64) -> f64 {
510 env::var(name)
511 .ok()
512 .and_then(|v| v.parse().ok())
513 .unwrap_or(default)
514}
515
516fn parse_usize_env(name: &str, default: usize) -> usize {
517 env::var(name)
518 .ok()
519 .and_then(|v| v.parse().ok())
520 .unwrap_or(default)
521}
522
523fn parse_u64_env(name: &str, default: u64) -> u64 {
524 env::var(name)
525 .ok()
526 .and_then(|v| v.parse().ok())
527 .unwrap_or(default)
528}
529
530#[cfg(test)]
531mod tests {
532 use super::*;
533 use apcore::module::ModuleAnnotations;
534 use serde_json::json;
535
536 #[test]
537 fn test_ai_enhancer_new_defaults() {
538 let enhancer = AIEnhancer::new(None, None, None, None, None).unwrap();
539 assert_eq!(enhancer.endpoint, DEFAULT_ENDPOINT);
540 assert_eq!(enhancer.model, DEFAULT_MODEL);
541 assert!((enhancer.threshold - DEFAULT_THRESHOLD).abs() < f64::EPSILON);
542 assert_eq!(enhancer.batch_size, DEFAULT_BATCH_SIZE);
543 assert_eq!(enhancer.timeout, DEFAULT_TIMEOUT);
544 }
545
546 #[test]
547 fn test_ai_enhancer_new_with_overrides() {
548 let enhancer = AIEnhancer::new(
549 Some("http://custom:8080".into()),
550 Some("llama3".into()),
551 Some(0.5),
552 Some(10),
553 Some(60),
554 )
555 .unwrap();
556 assert_eq!(enhancer.endpoint, "http://custom:8080");
557 assert_eq!(enhancer.model, "llama3");
558 assert!((enhancer.threshold - 0.5).abs() < f64::EPSILON);
559 }
560
561 #[test]
562 fn test_ai_enhancer_threshold_validation() {
563 let result = AIEnhancer::new(None, None, Some(1.5), None, None);
564 assert!(result.is_err());
565 }
566
567 #[test]
568 fn test_ai_enhancer_batch_size_validation() {
569 let result = AIEnhancer::new(None, None, None, Some(0), None);
570 assert!(result.is_err());
571 }
572
573 #[test]
574 fn test_identify_gaps_complete_module() {
575 let enhancer = AIEnhancer::new(None, None, None, None, None).unwrap();
576 let mut module = ScannedModule::new(
577 "test".into(),
578 "A real description".into(),
579 json!({"type": "object", "properties": {"x": {"type": "string"}}}),
580 json!({}),
581 vec![],
582 "app:func".into(),
583 );
584 module.documentation = Some("Full docs".into());
585 module.annotations = Some(ModuleAnnotations {
586 readonly: true,
587 ..Default::default()
588 });
589 let gaps = enhancer.identify_gaps(&module);
590 assert!(gaps.is_empty());
591 }
592
593 #[test]
594 fn test_identify_gaps_missing_fields() {
595 let enhancer = AIEnhancer::new(None, None, None, None, None).unwrap();
596 let module = ScannedModule::new(
597 "test".into(),
598 String::new(),
599 json!({"type": "object"}),
600 json!({}),
601 vec![],
602 "app:func".into(),
603 );
604 let gaps = enhancer.identify_gaps(&module);
605 assert!(gaps.iter().any(|g| g == "description"));
606 assert!(gaps.iter().any(|g| g == "documentation"));
607 assert!(gaps.iter().any(|g| g == "annotations"));
608 assert!(gaps.iter().any(|g| g == "input_schema"));
609 }
610
611 #[test]
612 fn test_parse_response_valid_json() {
613 let response = r#"{"description": "hello", "confidence": {"description": 0.9}}"#;
614 let result = AIEnhancer::parse_response(response).unwrap();
615 assert_eq!(result["description"], "hello");
616 }
617
618 #[test]
619 fn test_parse_response_with_fences() {
620 let response = "```json\n{\"key\": \"value\"}\n```";
621 let result = AIEnhancer::parse_response(response).unwrap();
622 assert_eq!(result["key"], "value");
623 }
624
625 #[test]
626 fn test_parse_response_invalid() {
627 let result = AIEnhancer::parse_response("not json");
628 assert!(result.is_err());
629 }
630
631 #[test]
632 fn test_is_enabled_default() {
633 env::remove_var("APCORE_AI_ENABLED");
635 assert!(!AIEnhancer::is_enabled());
636 }
637
638 #[test]
639 fn test_build_prompt_contains_module_info() {
640 let enhancer = AIEnhancer::new(None, None, None, None, None).unwrap();
641 let module = ScannedModule::new(
642 "users.get".into(),
643 "Get user".into(),
644 json!({}),
645 json!({}),
646 vec![],
647 "app:get_user".into(),
648 );
649 let prompt = enhancer.build_prompt(&module, &["description".into()]);
650 assert!(prompt.contains("users.get"));
651 assert!(prompt.contains("app:get_user"));
652 assert!(prompt.contains("description"));
653 }
654
655 #[test]
656 fn test_identify_gaps_description_equals_module_id() {
657 let enhancer = AIEnhancer::new(None, None, None, None, None).unwrap();
658 let module = ScannedModule::new(
659 "my_module".into(),
660 "my_module".into(), json!({"type": "object", "properties": {"x": {"type": "string"}}}),
662 json!({}),
663 vec![],
664 "app:func".into(),
665 );
666 let gaps = enhancer.identify_gaps(&module);
667 assert!(
668 gaps.iter().any(|g| g == "description"),
669 "description matching module_id should be identified as a gap"
670 );
671 }
672
673 #[test]
674 fn test_ai_enhancer_timeout_validation() {
675 let result = AIEnhancer::new(None, None, None, None, Some(0));
676 assert!(result.is_err());
677 let err = result.unwrap_err();
678 assert!(err
679 .to_string()
680 .contains("APCORE_AI_TIMEOUT must be a positive integer"));
681 }
682
683 #[test]
686 fn test_is_enabled_variants() {
687 use std::sync::Mutex;
688 static ENV_LOCK: Mutex<()> = Mutex::new(());
689 let _guard = ENV_LOCK.lock().unwrap();
690
691 unsafe { env::remove_var("APCORE_AI_ENABLED") };
693 assert!(!AIEnhancer::is_enabled(), "should be disabled by default");
694
695 unsafe { env::set_var("APCORE_AI_ENABLED", "true") };
697 assert!(AIEnhancer::is_enabled(), "\"true\" should enable");
698
699 unsafe { env::set_var("APCORE_AI_ENABLED", "yes") };
701 assert!(AIEnhancer::is_enabled(), "\"yes\" should enable");
702
703 unsafe { env::set_var("APCORE_AI_ENABLED", "1") };
705 assert!(AIEnhancer::is_enabled(), "\"1\" should enable");
706
707 unsafe { env::set_var("APCORE_AI_ENABLED", "false") };
709 assert!(!AIEnhancer::is_enabled(), "\"false\" should disable");
710
711 unsafe { env::remove_var("APCORE_AI_ENABLED") };
713 }
714
715 #[test]
716 fn test_parse_response_strips_json_fence() {
717 let response = "```json\n{\"description\": \"hello world\"}\n```";
718 let result = AIEnhancer::parse_response(response).unwrap();
719 assert_eq!(result["description"], "hello world");
720 }
721
722 #[test]
723 fn test_build_prompt_requests_annotations() {
724 let enhancer = AIEnhancer::new(None, None, None, None, None).unwrap();
725 let module = ScannedModule::new(
726 "test".into(),
727 "desc".into(),
728 json!({}),
729 json!({}),
730 vec![],
731 "app:func".into(),
732 );
733 let prompt = enhancer.build_prompt(&module, &["annotations".into()]);
734 assert!(
735 prompt.contains("readonly"),
736 "prompt should mention annotations fields"
737 );
738 assert!(prompt.contains("destructive"));
739 assert!(prompt.contains("idempotent"));
740 }
741
742 #[test]
743 fn test_build_prompt_requests_input_schema() {
744 let enhancer = AIEnhancer::new(None, None, None, None, None).unwrap();
745 let module = ScannedModule::new(
746 "test".into(),
747 "desc".into(),
748 json!({}),
749 json!({}),
750 vec![],
751 "app:func".into(),
752 );
753 let prompt = enhancer.build_prompt(&module, &["input_schema".into()]);
754 assert!(
755 prompt.contains("input_schema"),
756 "prompt should mention input_schema"
757 );
758 assert!(prompt.contains("JSON Schema"));
759 }
760
761 #[test]
762 fn test_build_prompt_requests_documentation() {
763 let enhancer = AIEnhancer::new(None, None, None, None, None).unwrap();
764 let module = ScannedModule::new(
765 "test".into(),
766 "desc".into(),
767 json!({}),
768 json!({}),
769 vec![],
770 "app:func".into(),
771 );
772 let prompt = enhancer.build_prompt(&module, &["documentation".into()]);
773 assert!(
774 prompt.contains("documentation"),
775 "prompt should mention documentation"
776 );
777 assert!(prompt.contains("Markdown"));
778 }
779}