minion_engine/steps/
template_step.rs1use 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 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}