vex_llm/tools/
datetime.rs

1//! DateTime tool for getting current date and time
2//!
3//! Uses the `chrono` crate for time operations.
4//!
5//! # Security
6//!
7//! - Only reads the system clock (no I/O)
8//! - Does not modify any state
9//! - Pure computation: safe for any sandbox
10
11use async_trait::async_trait;
12use chrono::{Local, Utc};
13use serde_json::Value;
14
15use crate::tool::{Capability, Tool, ToolDefinition};
16use crate::tool_error::ToolError;
17
18/// DateTime tool for retrieving current date and time.
19///
20/// # Example
21///
22/// ```ignore
23/// use vex_llm::DateTimeTool;
24/// use vex_llm::Tool;
25///
26/// let dt = DateTimeTool::new();
27/// let result = dt.execute(json!({"timezone": "utc"})).await?;
28/// println!("{}", result["datetime"]);
29/// ```
30pub struct DateTimeTool {
31    definition: ToolDefinition,
32}
33
34impl DateTimeTool {
35    /// Create a new datetime tool
36    pub fn new() -> Self {
37        Self {
38            definition: ToolDefinition::new(
39                "datetime",
40                "Get the current date and time in various formats and timezones.",
41                r#"{
42                    "type": "object",
43                    "properties": {
44                        "timezone": {
45                            "type": "string",
46                            "enum": ["utc", "local"],
47                            "default": "utc",
48                            "description": "Timezone: 'utc' for UTC or 'local' for system local time"
49                        },
50                        "format": {
51                            "type": "string",
52                            "description": "strftime format string. Default: '%Y-%m-%d %H:%M:%S'. Common formats: '%Y-%m-%d' (date only), '%H:%M:%S' (time only), '%Y-%m-%dT%H:%M:%SZ' (ISO 8601)"
53                        }
54                    }
55                }"#,
56            ),
57        }
58    }
59}
60
61impl Default for DateTimeTool {
62    fn default() -> Self {
63        Self::new()
64    }
65}
66
67#[async_trait]
68impl Tool for DateTimeTool {
69    fn definition(&self) -> &ToolDefinition {
70        &self.definition
71    }
72
73    fn capabilities(&self) -> Vec<Capability> {
74        vec![Capability::PureComputation] // Just reads system clock
75    }
76
77    fn validate(&self, args: &Value) -> Result<(), ToolError> {
78        // Validate timezone if provided
79        if let Some(tz) = args.get("timezone").and_then(|v| v.as_str()) {
80            if tz != "utc" && tz != "local" {
81                return Err(ToolError::invalid_args(
82                    "datetime",
83                    format!("Invalid timezone '{}'. Must be 'utc' or 'local'", tz),
84                ));
85            }
86        }
87
88        // Validate format string length
89        if let Some(fmt) = args.get("format").and_then(|v| v.as_str()) {
90            if fmt.len() > 100 {
91                return Err(ToolError::invalid_args(
92                    "datetime",
93                    "Format string too long (max 100 characters)",
94                ));
95            }
96        }
97
98        Ok(())
99    }
100
101    async fn execute(&self, args: Value) -> Result<Value, ToolError> {
102        let tz = args
103            .get("timezone")
104            .and_then(|v| v.as_str())
105            .unwrap_or("utc");
106
107        let fmt = args
108            .get("format")
109            .and_then(|v| v.as_str())
110            .unwrap_or("%Y-%m-%d %H:%M:%S");
111
112        let (formatted, timezone, unix_timestamp) = match tz {
113            "local" => {
114                let now = Local::now();
115                (now.format(fmt).to_string(), "local", now.timestamp())
116            }
117            _ => {
118                let now = Utc::now();
119                (now.format(fmt).to_string(), "utc", now.timestamp())
120            }
121        };
122
123        Ok(serde_json::json!({
124            "datetime": formatted,
125            "timezone": timezone,
126            "format": fmt,
127            "unix_timestamp": unix_timestamp
128        }))
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[tokio::test]
137    async fn test_utc_datetime() {
138        let dt = DateTimeTool::new();
139        let result = dt
140            .execute(serde_json::json!({"timezone": "utc"}))
141            .await
142            .unwrap();
143
144        assert_eq!(result["timezone"], "utc");
145        assert!(result["datetime"].is_string());
146        assert!(result["unix_timestamp"].is_i64());
147    }
148
149    #[tokio::test]
150    async fn test_local_datetime() {
151        let dt = DateTimeTool::new();
152        let result = dt
153            .execute(serde_json::json!({"timezone": "local"}))
154            .await
155            .unwrap();
156
157        assert_eq!(result["timezone"], "local");
158    }
159
160    #[tokio::test]
161    async fn test_custom_format() {
162        let dt = DateTimeTool::new();
163        let result = dt
164            .execute(serde_json::json!({"format": "%Y-%m-%d"}))
165            .await
166            .unwrap();
167
168        // Should be date only (YYYY-MM-DD format)
169        let datetime = result["datetime"].as_str().unwrap();
170        assert_eq!(datetime.len(), 10); // "2025-12-18" = 10 chars
171    }
172
173    #[tokio::test]
174    async fn test_default_values() {
175        let dt = DateTimeTool::new();
176        let result = dt.execute(serde_json::json!({})).await.unwrap();
177
178        assert_eq!(result["timezone"], "utc");
179        assert_eq!(result["format"], "%Y-%m-%d %H:%M:%S");
180    }
181
182    #[tokio::test]
183    async fn test_invalid_timezone() {
184        let dt = DateTimeTool::new();
185        let result = dt.validate(&serde_json::json!({"timezone": "invalid"}));
186
187        assert!(matches!(result, Err(ToolError::InvalidArguments { .. })));
188    }
189
190    #[tokio::test]
191    async fn test_format_too_long() {
192        let dt = DateTimeTool::new();
193        let long_format = "a".repeat(150);
194        let result = dt.validate(&serde_json::json!({"format": long_format}));
195
196        assert!(matches!(result, Err(ToolError::InvalidArguments { .. })));
197    }
198}