vex_llm/tools/
datetime.rs1use 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
18pub struct DateTimeTool {
31 definition: ToolDefinition,
32}
33
34impl DateTimeTool {
35 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] }
76
77 fn validate(&self, args: &Value) -> Result<(), ToolError> {
78 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 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 let datetime = result["datetime"].as_str().unwrap();
170 assert_eq!(datetime.len(), 10); }
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}