Skip to main content

zeroclaw_tools/
report_template_tool.rs

1//! Report template tool — standalone access to template engine.
2//!
3//! Exposes the report template engine directly so agents can render
4//! templates with custom variable maps without going through ProjectIntelTool.
5
6use super::report_templates;
7use async_trait::async_trait;
8use serde_json::json;
9use std::collections::HashMap;
10use zeroclaw_api::tool::{Tool, ToolResult};
11
12/// Standalone report template tool.
13///
14/// Provides direct access to the template engine for rendering
15/// weekly_status, sprint_review, risk_register, and milestone_report
16/// templates in en/de/fr/it.
17pub struct ReportTemplateTool;
18
19impl ReportTemplateTool {
20    pub fn new() -> Self {
21        Self
22    }
23}
24
25impl Default for ReportTemplateTool {
26    fn default() -> Self {
27        Self::new()
28    }
29}
30
31#[async_trait]
32impl Tool for ReportTemplateTool {
33    fn name(&self) -> &str {
34        "report_template"
35    }
36
37    fn description(&self) -> &str {
38        "Render a report template with custom variables. Supports weekly_status, sprint_review, risk_register, milestone_report in en/de/fr/it."
39    }
40
41    fn parameters_schema(&self) -> serde_json::Value {
42        json!({
43            "type": "object",
44            "properties": {
45                "template": {
46                    "type": "string",
47                    "enum": ["weekly_status", "sprint_review", "risk_register", "milestone_report"],
48                    "description": "Template name"
49                },
50                "language": {
51                    "type": "string",
52                    "enum": ["en", "de", "fr", "it"],
53                    "default": "en",
54                    "description": "Language code"
55                },
56                "variables": {
57                    "type": "object",
58                    "description": "Map of placeholder names to values (e.g., {\"project_name\": \"Acme\"})"
59                }
60            },
61            "required": ["template", "variables"]
62        })
63    }
64
65    async fn execute(&self, params: serde_json::Value) -> anyhow::Result<ToolResult> {
66        let template = params
67            .get("template")
68            .and_then(|v| v.as_str())
69            .ok_or_else(|| {
70                ::zeroclaw_log::record!(
71                    WARN,
72                    ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
73                        .with_outcome(::zeroclaw_log::EventOutcome::Failure)
74                        .with_attrs(::serde_json::json!({"param": "template"})),
75                    "report_template_tool: missing template parameter"
76                );
77                anyhow::Error::msg("missing template")
78            })?;
79
80        let language = params
81            .get("language")
82            .and_then(|v| v.as_str())
83            .unwrap_or("en");
84
85        let variables = params
86            .get("variables")
87            .and_then(|v| v.as_object())
88            .ok_or_else(|| {
89                ::zeroclaw_log::record!(
90                    WARN,
91                    ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
92                        .with_outcome(::zeroclaw_log::EventOutcome::Failure)
93                        .with_attrs(::serde_json::json!({"param": "variables"})),
94                    "report_template_tool: variables must be an object"
95                );
96                anyhow::Error::msg("variables must be object")
97            })?;
98
99        // Convert JSON object to HashMap<String, String>
100        // Non-string values are coerced to strings
101        let var_map: HashMap<String, String> = variables
102            .iter()
103            .map(|(k, v)| {
104                let value_str = match v {
105                    serde_json::Value::String(s) => s.clone(),
106                    serde_json::Value::Number(n) => n.to_string(),
107                    serde_json::Value::Bool(b) => b.to_string(),
108                    serde_json::Value::Null
109                    | serde_json::Value::Array(_)
110                    | serde_json::Value::Object(_) => String::new(),
111                };
112                (k.clone(), value_str)
113            })
114            .collect();
115
116        let rendered = report_templates::render_template(template, language, &var_map)?;
117
118        Ok(ToolResult {
119            success: true,
120            output: rendered,
121            error: None,
122        })
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[tokio::test]
131    async fn tool_name_is_report_template() {
132        let tool = ReportTemplateTool::new();
133        assert_eq!(tool.name(), "report_template");
134    }
135
136    #[tokio::test]
137    async fn tool_has_description() {
138        let tool = ReportTemplateTool::new();
139        assert!(!tool.description().is_empty());
140    }
141
142    #[tokio::test]
143    async fn tool_has_parameters_schema() {
144        let tool = ReportTemplateTool::new();
145        let schema = tool.parameters_schema();
146        assert!(schema.is_object());
147        assert!(schema["properties"].is_object());
148        assert!(schema["required"].is_array());
149    }
150
151    #[tokio::test]
152    async fn execute_renders_weekly_status() {
153        let tool = ReportTemplateTool::new();
154        let params = json!({
155            "template": "weekly_status",
156            "language": "en",
157            "variables": {
158                "project_name": "Test",
159                "period": "W1",
160                "completed": "Done",
161                "in_progress": "WIP",
162                "blocked": "None",
163                "next_steps": "Next"
164            }
165        });
166
167        let result = tool.execute(params).await.unwrap();
168        assert!(result.success);
169        assert!(result.output.contains("Project: Test"));
170    }
171
172    #[tokio::test]
173    async fn execute_defaults_to_english() {
174        let tool = ReportTemplateTool::new();
175        let params = json!({
176            "template": "weekly_status",
177            "variables": {
178                "project_name": "Test"
179            }
180        });
181
182        let result = tool.execute(params).await.unwrap();
183        assert!(result.success);
184        assert!(result.output.contains("## Summary"));
185    }
186
187    #[tokio::test]
188    async fn execute_fails_on_missing_template() {
189        let tool = ReportTemplateTool::new();
190        let params = json!({
191            "variables": {
192                "project_name": "Test"
193            }
194        });
195
196        let result = tool.execute(params).await;
197        assert!(result.is_err());
198    }
199
200    #[tokio::test]
201    async fn execute_fails_on_missing_variables() {
202        let tool = ReportTemplateTool::new();
203        let params = json!({
204            "template": "weekly_status"
205        });
206
207        let result = tool.execute(params).await;
208        assert!(result.is_err());
209    }
210
211    #[tokio::test]
212    async fn execute_fails_on_invalid_template() {
213        let tool = ReportTemplateTool::new();
214        let params = json!({
215            "template": "unknown",
216            "variables": {}
217        });
218
219        let result = tool.execute(params).await;
220        assert!(result.is_err());
221    }
222}