Skip to main content

minion_engine/steps/
template_step.rs

1use std::path::PathBuf;
2
3use async_trait::async_trait;
4
5use crate::config::StepConfig;
6use crate::engine::context::Context;
7use crate::error::StepError;
8use crate::workflow::schema::StepDef;
9
10use super::{AgentOutput, StepExecutor, StepOutput};
11
12pub struct TemplateStepExecutor {
13    prompts_dir: String,
14}
15
16impl TemplateStepExecutor {
17    pub fn new(prompts_dir: Option<&str>) -> Self {
18        Self {
19            prompts_dir: prompts_dir.unwrap_or("prompts").to_string(),
20        }
21    }
22}
23
24#[async_trait]
25impl StepExecutor for TemplateStepExecutor {
26    async fn execute(
27        &self,
28        step: &StepDef,
29        _config: &StepConfig,
30        ctx: &Context,
31    ) -> Result<StepOutput, StepError> {
32        let file_path = PathBuf::from(&self.prompts_dir)
33            .join(format!("{}.md.tera", step.name));
34
35        let template_content = tokio::fs::read_to_string(&file_path)
36            .await
37            .map_err(|e| {
38                StepError::Fail(format!(
39                    "Template file not found: '{}': {}",
40                    file_path.display(),
41                    e
42                ))
43            })?;
44
45        let rendered = ctx.render_template(&template_content)?;
46
47        Ok(StepOutput::Agent(AgentOutput {
48            response: rendered,
49            session_id: None,
50            stats: super::AgentStats::default(),
51        }))
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58    use std::collections::HashMap;
59    use crate::workflow::schema::StepType;
60    use tokio::fs;
61
62    fn make_step(name: &str) -> StepDef {
63        StepDef {
64            name: name.to_string(),
65            step_type: StepType::Template,
66            run: None,
67            prompt: None,
68            condition: None,
69            on_pass: None,
70            on_fail: None,
71            message: None,
72            scope: None,
73            max_iterations: None,
74            initial_value: None,
75            items: None,
76            parallel: None,
77            steps: None,
78            config: HashMap::new(),
79            outputs: None,
80            output_type: None,
81            async_exec: None,
82        }
83    }
84
85    #[tokio::test]
86    async fn template_renders_with_context() {
87        let tmp = tempfile::tempdir().expect("temp dir");
88        let prompts_dir = tmp.path().to_str().unwrap().to_string();
89
90        // Write a .md.tera file
91        let template_path = tmp.path().join("greet.md.tera");
92        fs::write(&template_path, "Hello {{ target }}!").await.unwrap();
93
94        let step = make_step("greet");
95        let executor = TemplateStepExecutor::new(Some(&prompts_dir));
96        let config = StepConfig::default();
97        let ctx = Context::new("world".to_string(), HashMap::new());
98
99        let result = executor.execute(&step, &config, &ctx).await.unwrap();
100        assert_eq!(result.text(), "Hello world!");
101    }
102
103    #[tokio::test]
104    async fn template_file_not_found_descriptive_error() {
105        let step = make_step("nonexistent");
106        let executor = TemplateStepExecutor::new(Some("/nonexistent/dir"));
107        let config = StepConfig::default();
108        let ctx = Context::new(String::new(), HashMap::new());
109
110        let result = executor.execute(&step, &config, &ctx).await;
111        assert!(result.is_err());
112        let err = result.unwrap_err().to_string();
113        assert!(
114            err.contains("Template file not found") || err.contains("nonexistent"),
115            "Error should describe the missing file: {}", err
116        );
117    }
118}