1use crate::model::router::ModelRouter;
7use crate::types::Workflow;
8use crate::workflow::parser;
9use anyhow::{Context, Result};
10use std::sync::Arc;
11
12pub struct NLWorkflowGenerator {
14 router: Arc<ModelRouter>,
15}
16
17#[derive(Debug, Clone)]
19pub struct GeneratedWorkflow {
20 pub yaml: String,
22 pub workflow: Option<Workflow>,
24 pub explanation: String,
26}
27
28impl NLWorkflowGenerator {
29 pub fn new(router: Arc<ModelRouter>) -> Self {
30 Self { router }
31 }
32
33 pub async fn generate(&self, description: &str) -> Result<GeneratedWorkflow> {
35 let prompt = format!(
36 r#"Generate a MUR Commander workflow YAML from this description:
37
38"{}"
39
40The YAML format is:
41```yaml
42id: <kebab-case-id>
43name: <human-readable name>
44description: <what the workflow does>
45variables:
46 <key>: "<default value>"
47steps:
48 - name: <step-name>
49 step_type: <analyze|plan|debug|code|refactor|fix|search|classify|summarize|security_check|execute>
50 action: "<shell command or AI prompt>"
51 on_failure: <abort|skip|retry>
52 on_failure_max: <number> # only if retry
53 breakpoint: <true|false>
54 breakpoint_message: "<optional message>"
55```
56
57Rules:
58- Use `{{{{variable}}}}` syntax for variable interpolation
59- Set breakpoints before destructive actions (deploy, delete, restart)
60- Use appropriate step_types (analyze for AI analysis, execute for shell commands)
61- Keep steps focused and atomic
62
63Respond with:
641. The YAML block (in ```yaml fences)
652. A brief explanation of each step
66
67YAML only, no extra prose before the yaml block."#,
68 description
69 );
70
71 let response = self
72 .router
73 .complete_for_step(&crate::types::StepType::Plan, &prompt)
74 .await
75 .context("Generating workflow from NL")?;
76
77 let content = &response.content;
78
79 let yaml = extract_yaml_block(content).unwrap_or_else(|| content.clone());
81
82 let explanation = content
84 .split("```")
85 .nth(2)
86 .unwrap_or("")
87 .trim()
88 .to_string();
89
90 let workflow = parser::parse_workflow_str(&yaml).ok();
92
93 Ok(GeneratedWorkflow {
94 yaml,
95 workflow,
96 explanation,
97 })
98 }
99
100 pub async fn save(&self, generated: &GeneratedWorkflow) -> Result<std::path::PathBuf> {
102 let workflow = generated
103 .workflow
104 .as_ref()
105 .context("Cannot save invalid workflow YAML")?;
106
107 let dir = parser::workflows_dir();
108 tokio::fs::create_dir_all(&dir).await?;
109
110 let path = dir.join(format!("{}.yaml", workflow.id));
111 tokio::fs::write(&path, &generated.yaml).await?;
112
113 tracing::info!("Saved workflow to {:?}", path);
114 Ok(path)
115 }
116}
117
118fn extract_yaml_block(content: &str) -> Option<String> {
120 let start = content.find("```yaml").or_else(|| content.find("```yml"))?;
121 let after_fence = &content[start..];
122 let content_start = after_fence.find('\n')? + 1;
123 let end = after_fence[content_start..].find("```")?;
124 Some(after_fence[content_start..content_start + end].trim().to_string())
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[test]
132 fn test_extract_yaml_block() {
133 let input = r#"Here's the workflow:
134
135```yaml
136id: test
137name: Test
138steps: []
139```
140
141This workflow does XYZ."#;
142
143 let yaml = extract_yaml_block(input).unwrap();
144 assert!(yaml.contains("id: test"));
145 assert!(!yaml.contains("```"));
146 }
147
148 #[test]
149 fn test_extract_yaml_no_block() {
150 let input = "Just plain text";
151 assert!(extract_yaml_block(input).is_none());
152 }
153}