mojentic/llm/tools/
current_datetime_tool.rs

1use crate::error::Result;
2use crate::llm::tools::{FunctionDescriptor, LlmTool, ToolDescriptor};
3use chrono::Local;
4use serde_json::{json, Value};
5use std::collections::HashMap;
6
7/// Tool for getting the current date and time
8///
9/// This tool returns the current datetime with optional formatting.
10/// It's useful when the LLM needs to know the current time or date.
11///
12/// # Examples
13///
14/// ```ignore
15/// use mojentic::llm::tools::current_datetime_tool::CurrentDatetimeTool;
16///
17/// let tool = CurrentDatetimeTool;
18/// let args = HashMap::new();
19///
20/// let result = tool.run(&args)?;
21/// // result contains current_datetime, timestamp, and timezone
22/// ```
23#[derive(Clone)]
24pub struct CurrentDatetimeTool;
25
26impl CurrentDatetimeTool {
27    /// Creates a new CurrentDatetimeTool instance
28    pub fn new() -> Self {
29        Self
30    }
31}
32
33impl Default for CurrentDatetimeTool {
34    fn default() -> Self {
35        Self::new()
36    }
37}
38
39impl LlmTool for CurrentDatetimeTool {
40    fn run(&self, args: &HashMap<String, Value>) -> Result<Value> {
41        let format_string = args
42            .get("format_string")
43            .and_then(|v| v.as_str())
44            .unwrap_or("%Y-%m-%d %H:%M:%S");
45
46        let now = Local::now();
47        let formatted_time = now.format(format_string).to_string();
48        let timestamp = now.timestamp();
49        let timezone = format!("{}", now.offset());
50
51        Ok(json!({
52            "current_datetime": formatted_time,
53            "timestamp": timestamp,
54            "timezone": timezone
55        }))
56    }
57
58    fn descriptor(&self) -> ToolDescriptor {
59        ToolDescriptor {
60            r#type: "function".to_string(),
61            function: FunctionDescriptor {
62                name: "get_current_datetime".to_string(),
63                description: "Get the current date and time. Useful when you need to know the current time or date.".to_string(),
64                parameters: json!({
65                    "type": "object",
66                    "properties": {
67                        "format_string": {
68                            "type": "string",
69                            "description": "Format string for the datetime (e.g., '%Y-%m-%d %H:%M:%S', '%A, %B %d, %Y'). Default is ISO format."
70                        }
71                    },
72                    "required": []
73                }),
74            },
75        }
76    }
77
78    fn clone_box(&self) -> Box<dyn LlmTool> {
79        Box::new(self.clone())
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn test_descriptor() {
89        let tool = CurrentDatetimeTool::new();
90        let descriptor = tool.descriptor();
91
92        assert_eq!(descriptor.r#type, "function");
93        assert_eq!(descriptor.function.name, "get_current_datetime");
94        assert!(descriptor.function.description.contains("current date and time"));
95    }
96
97    #[test]
98    fn test_run_with_default_format() {
99        let tool = CurrentDatetimeTool::new();
100        let args = HashMap::new();
101
102        let result = tool.run(&args).unwrap();
103
104        assert!(result.is_object());
105        assert!(result.get("current_datetime").is_some());
106        assert!(result.get("timestamp").is_some());
107        assert!(result.get("timezone").is_some());
108
109        // Check format matches default "%Y-%m-%d %H:%M:%S"
110        let datetime_str = result.get("current_datetime").unwrap().as_str().unwrap();
111        let re = regex::Regex::new(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$").unwrap();
112        assert!(re.is_match(datetime_str));
113    }
114
115    #[test]
116    fn test_run_with_custom_format() {
117        let tool = CurrentDatetimeTool::new();
118        let mut args = HashMap::new();
119        args.insert("format_string".to_string(), json!("%Y-%m-%d"));
120
121        let result = tool.run(&args).unwrap();
122
123        let datetime_str = result.get("current_datetime").unwrap().as_str().unwrap();
124        let re = regex::Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap();
125        assert!(re.is_match(datetime_str));
126    }
127
128    #[test]
129    fn test_timestamp_is_reasonable() {
130        let tool = CurrentDatetimeTool::new();
131        let args = HashMap::new();
132
133        let result = tool.run(&args).unwrap();
134        let timestamp = result.get("timestamp").unwrap().as_i64().unwrap();
135
136        // Timestamp should be reasonable (after 2020, before 2030)
137        assert!(timestamp > 1_577_836_800);
138        assert!(timestamp < 1_893_456_000);
139    }
140
141    #[test]
142    fn test_timezone_is_present() {
143        let tool = CurrentDatetimeTool::new();
144        let args = HashMap::new();
145
146        let result = tool.run(&args).unwrap();
147        let timezone = result.get("timezone").unwrap().as_str().unwrap();
148
149        assert!(!timezone.is_empty());
150    }
151
152    #[test]
153    fn test_tool_matches() {
154        let tool = CurrentDatetimeTool::new();
155        assert!(tool.matches("get_current_datetime"));
156        assert!(!tool.matches("other_tool"));
157    }
158
159    #[test]
160    fn test_format_with_day_name() {
161        let tool = CurrentDatetimeTool::new();
162        let mut args = HashMap::new();
163        args.insert("format_string".to_string(), json!("%A"));
164
165        let result = tool.run(&args).unwrap();
166        let day_name = result.get("current_datetime").unwrap().as_str().unwrap();
167
168        let valid_days = [
169            "Monday",
170            "Tuesday",
171            "Wednesday",
172            "Thursday",
173            "Friday",
174            "Saturday",
175            "Sunday",
176        ];
177        assert!(valid_days.contains(&day_name));
178    }
179
180    #[test]
181    fn test_format_with_month_name() {
182        let tool = CurrentDatetimeTool::new();
183        let mut args = HashMap::new();
184        args.insert("format_string".to_string(), json!("%B"));
185
186        let result = tool.run(&args).unwrap();
187        let month_name = result.get("current_datetime").unwrap().as_str().unwrap();
188
189        let valid_months = [
190            "January",
191            "February",
192            "March",
193            "April",
194            "May",
195            "June",
196            "July",
197            "August",
198            "September",
199            "October",
200            "November",
201            "December",
202        ];
203        assert!(valid_months.contains(&month_name));
204    }
205}