1use async_trait::async_trait;
2use chrono::{DateTime, Duration, NaiveDateTime, TimeZone, Utc};
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7use crate::generate_schema;
8use ai_agents_core::{Tool, ToolResult};
9
10pub struct DateTimeTool;
11
12impl DateTimeTool {
13 pub fn new() -> Self {
14 Self
15 }
16}
17
18impl Default for DateTimeTool {
19 fn default() -> Self {
20 Self::new()
21 }
22}
23
24#[derive(Debug, Deserialize, JsonSchema)]
25struct DateTimeInput {
26 operation: String,
28 #[serde(default)]
30 value: Option<String>,
31 #[serde(default)]
33 value2: Option<String>,
34 #[serde(default)]
36 format: Option<String>,
37 #[serde(default)]
39 amount: Option<i64>,
40 #[serde(default)]
42 unit: Option<String>,
43}
44
45#[derive(Debug, Serialize, Deserialize)]
46struct NowOutput {
47 iso: String,
48 unix_timestamp: i64,
49 formatted: String,
50}
51
52#[derive(Debug, Serialize, Deserialize)]
53struct FormatOutput {
54 formatted: String,
55 original: String,
56}
57
58#[derive(Debug, Serialize, Deserialize)]
59struct ParseOutput {
60 iso: String,
61 unix_timestamp: i64,
62}
63
64#[derive(Debug, Serialize, Deserialize)]
65struct AddOutput {
66 result: String,
67 unix_timestamp: i64,
68 original: String,
69 added: String,
70}
71
72#[derive(Debug, Serialize, Deserialize)]
73struct DiffOutput {
74 seconds: i64,
75 minutes: i64,
76 hours: i64,
77 days: i64,
78 value1: String,
79 value2: String,
80}
81
82#[async_trait]
83impl Tool for DateTimeTool {
84 fn id(&self) -> &str {
85 "datetime"
86 }
87
88 fn name(&self) -> &str {
89 "DateTime"
90 }
91
92 fn description(&self) -> &str {
93 "Get current time, format dates, parse date strings, add/subtract time, and calculate differences. All times are in UTC."
94 }
95
96 fn input_schema(&self) -> Value {
97 generate_schema::<DateTimeInput>()
98 }
99
100 async fn execute(&self, args: Value) -> ToolResult {
101 let input: DateTimeInput = match serde_json::from_value(args) {
102 Ok(input) => input,
103 Err(e) => return ToolResult::error(format!("Invalid input: {}", e)),
104 };
105
106 match input.operation.to_lowercase().as_str() {
107 "now" => self.handle_now(&input),
108 "format" => self.handle_format(&input),
109 "parse" => self.handle_parse(&input),
110 "add" => self.handle_add(&input),
111 "diff" => self.handle_diff(&input),
112 _ => ToolResult::error(format!(
113 "Unknown operation: {}. Valid operations: now, format, parse, add, diff",
114 input.operation
115 )),
116 }
117 }
118}
119
120impl DateTimeTool {
121 fn handle_now(&self, input: &DateTimeInput) -> ToolResult {
122 let now = Utc::now();
123 let format = input.format.as_deref().unwrap_or("%Y-%m-%d %H:%M:%S UTC");
124
125 let output = NowOutput {
126 iso: now.to_rfc3339(),
127 unix_timestamp: now.timestamp(),
128 formatted: now.format(format).to_string(),
129 };
130
131 match serde_json::to_string(&output) {
132 Ok(json) => ToolResult::ok(json),
133 Err(e) => ToolResult::error(format!("Serialization error: {}", e)),
134 }
135 }
136
137 fn handle_format(&self, input: &DateTimeInput) -> ToolResult {
138 let value = match &input.value {
139 Some(v) => v,
140 None => return ToolResult::error("'value' is required for format operation"),
141 };
142
143 let format = input.format.as_deref().unwrap_or("%Y-%m-%d");
144
145 let dt = match self.parse_datetime(value) {
146 Ok(dt) => dt,
147 Err(e) => return ToolResult::error(e),
148 };
149
150 let output = FormatOutput {
151 formatted: dt.format(format).to_string(),
152 original: value.clone(),
153 };
154
155 match serde_json::to_string(&output) {
156 Ok(json) => ToolResult::ok(json),
157 Err(e) => ToolResult::error(format!("Serialization error: {}", e)),
158 }
159 }
160
161 fn handle_parse(&self, input: &DateTimeInput) -> ToolResult {
162 let value = match &input.value {
163 Some(v) => v,
164 None => return ToolResult::error("'value' is required for parse operation"),
165 };
166
167 let dt = match self.parse_datetime(value) {
168 Ok(dt) => dt,
169 Err(e) => return ToolResult::error(e),
170 };
171
172 let output = ParseOutput {
173 iso: dt.to_rfc3339(),
174 unix_timestamp: dt.timestamp(),
175 };
176
177 match serde_json::to_string(&output) {
178 Ok(json) => ToolResult::ok(json),
179 Err(e) => ToolResult::error(format!("Serialization error: {}", e)),
180 }
181 }
182
183 fn handle_add(&self, input: &DateTimeInput) -> ToolResult {
184 let value = match &input.value {
185 Some(v) => v,
186 None => return ToolResult::error("'value' is required for add operation"),
187 };
188
189 let amount = match input.amount {
190 Some(a) => a,
191 None => return ToolResult::error("'amount' is required for add operation"),
192 };
193
194 let unit = input.unit.as_deref().unwrap_or("days");
195
196 let dt = match self.parse_datetime(value) {
197 Ok(dt) => dt,
198 Err(e) => return ToolResult::error(e),
199 };
200
201 let duration = match unit.to_lowercase().as_str() {
202 "seconds" | "second" | "s" => Duration::seconds(amount),
203 "minutes" | "minute" | "m" => Duration::minutes(amount),
204 "hours" | "hour" | "h" => Duration::hours(amount),
205 "days" | "day" | "d" => Duration::days(amount),
206 "weeks" | "week" | "w" => Duration::weeks(amount),
207 _ => {
208 return ToolResult::error(format!(
209 "Unknown unit: {}. Valid units: seconds, minutes, hours, days, weeks",
210 unit
211 ));
212 }
213 };
214
215 let result = dt + duration;
216
217 let output = AddOutput {
218 result: result.to_rfc3339(),
219 unix_timestamp: result.timestamp(),
220 original: value.clone(),
221 added: format!("{} {}", amount, unit),
222 };
223
224 match serde_json::to_string(&output) {
225 Ok(json) => ToolResult::ok(json),
226 Err(e) => ToolResult::error(format!("Serialization error: {}", e)),
227 }
228 }
229
230 fn handle_diff(&self, input: &DateTimeInput) -> ToolResult {
231 let value1 = match &input.value {
232 Some(v) => v,
233 None => return ToolResult::error("'value' is required for diff operation"),
234 };
235
236 let value2 = match &input.value2 {
237 Some(v) => v,
238 None => return ToolResult::error("'value2' is required for diff operation"),
239 };
240
241 let dt1 = match self.parse_datetime(value1) {
242 Ok(dt) => dt,
243 Err(e) => return ToolResult::error(format!("Error parsing value: {}", e)),
244 };
245
246 let dt2 = match self.parse_datetime(value2) {
247 Ok(dt) => dt,
248 Err(e) => return ToolResult::error(format!("Error parsing value2: {}", e)),
249 };
250
251 let diff = dt2.signed_duration_since(dt1);
252 let total_seconds = diff.num_seconds();
253
254 let output = DiffOutput {
255 seconds: total_seconds,
256 minutes: total_seconds / 60,
257 hours: total_seconds / 3600,
258 days: total_seconds / 86400,
259 value1: dt1.to_rfc3339(),
260 value2: dt2.to_rfc3339(),
261 };
262
263 match serde_json::to_string(&output) {
264 Ok(json) => ToolResult::ok(json),
265 Err(e) => ToolResult::error(format!("Serialization error: {}", e)),
266 }
267 }
268
269 fn parse_datetime(&self, value: &str) -> Result<DateTime<Utc>, String> {
270 if let Ok(ts) = value.parse::<i64>() {
271 return Utc
272 .timestamp_opt(ts, 0)
273 .single()
274 .ok_or_else(|| "Invalid unix timestamp".to_string());
275 }
276
277 if let Ok(dt) = DateTime::parse_from_rfc3339(value) {
278 return Ok(dt.with_timezone(&Utc));
279 }
280
281 if let Ok(dt) = DateTime::parse_from_rfc2822(value) {
282 return Ok(dt.with_timezone(&Utc));
283 }
284
285 let formats = [
286 "%Y-%m-%d %H:%M:%S",
287 "%Y-%m-%d %H:%M",
288 "%Y-%m-%d",
289 "%Y/%m/%d %H:%M:%S",
290 "%Y/%m/%d %H:%M",
291 "%Y/%m/%d",
292 "%d-%m-%Y %H:%M:%S",
293 "%d-%m-%Y",
294 "%d/%m/%Y %H:%M:%S",
295 "%d/%m/%Y",
296 ];
297
298 for fmt in formats {
299 if let Ok(ndt) = NaiveDateTime::parse_from_str(value, fmt) {
300 return Ok(Utc.from_utc_datetime(&ndt));
301 }
302 if let Ok(nd) = chrono::NaiveDate::parse_from_str(value, fmt) {
303 let ndt = nd.and_hms_opt(0, 0, 0).unwrap();
304 return Ok(Utc.from_utc_datetime(&ndt));
305 }
306 }
307
308 Err(format!(
309 "Unable to parse date/time: '{}'. Supported formats: ISO 8601, RFC 3339, RFC 2822, YYYY-MM-DD, unix timestamp",
310 value
311 ))
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318
319 #[tokio::test]
320 async fn test_now() {
321 let tool = DateTimeTool::new();
322 let result = tool.execute(serde_json::json!({"operation": "now"})).await;
323 assert!(result.success);
324
325 let output: NowOutput = serde_json::from_str(&result.output).unwrap();
326 assert!(!output.iso.is_empty());
327 assert!(output.unix_timestamp > 0);
328 }
329
330 #[tokio::test]
331 async fn test_now_with_format() {
332 let tool = DateTimeTool::new();
333 let result = tool
334 .execute(serde_json::json!({
335 "operation": "now",
336 "format": "%Y-%m-%d"
337 }))
338 .await;
339 assert!(result.success);
340 }
341
342 #[tokio::test]
343 async fn test_format() {
344 let tool = DateTimeTool::new();
345 let result = tool
346 .execute(serde_json::json!({
347 "operation": "format",
348 "value": "2024-12-25T10:30:00Z",
349 "format": "%B %d, %Y"
350 }))
351 .await;
352 assert!(result.success);
353
354 let output: FormatOutput = serde_json::from_str(&result.output).unwrap();
355 assert_eq!(output.formatted, "December 25, 2024");
356 }
357
358 #[tokio::test]
359 async fn test_parse_iso() {
360 let tool = DateTimeTool::new();
361 let result = tool
362 .execute(serde_json::json!({
363 "operation": "parse",
364 "value": "2024-01-15T12:00:00Z"
365 }))
366 .await;
367 assert!(result.success);
368
369 let output: ParseOutput = serde_json::from_str(&result.output).unwrap();
370 assert!(output.unix_timestamp > 0);
371 }
372
373 #[tokio::test]
374 async fn test_parse_simple_date() {
375 let tool = DateTimeTool::new();
376 let result = tool
377 .execute(serde_json::json!({
378 "operation": "parse",
379 "value": "2024-01-15"
380 }))
381 .await;
382 assert!(result.success);
383 }
384
385 #[tokio::test]
386 async fn test_add_days() {
387 let tool = DateTimeTool::new();
388 let result = tool
389 .execute(serde_json::json!({
390 "operation": "add",
391 "value": "2024-01-15T00:00:00Z",
392 "amount": 10,
393 "unit": "days"
394 }))
395 .await;
396 assert!(result.success);
397
398 let output: AddOutput = serde_json::from_str(&result.output).unwrap();
399 assert!(output.result.contains("2024-01-25"));
400 }
401
402 #[tokio::test]
403 async fn test_add_negative() {
404 let tool = DateTimeTool::new();
405 let result = tool
406 .execute(serde_json::json!({
407 "operation": "add",
408 "value": "2024-01-15T00:00:00Z",
409 "amount": -5,
410 "unit": "days"
411 }))
412 .await;
413 assert!(result.success);
414
415 let output: AddOutput = serde_json::from_str(&result.output).unwrap();
416 assert!(output.result.contains("2024-01-10"));
417 }
418
419 #[tokio::test]
420 async fn test_diff() {
421 let tool = DateTimeTool::new();
422 let result = tool
423 .execute(serde_json::json!({
424 "operation": "diff",
425 "value": "2024-01-01T00:00:00Z",
426 "value2": "2024-01-02T00:00:00Z"
427 }))
428 .await;
429 assert!(result.success);
430
431 let output: DiffOutput = serde_json::from_str(&result.output).unwrap();
432 assert_eq!(output.days, 1);
433 assert_eq!(output.hours, 24);
434 assert_eq!(output.seconds, 86400);
435 }
436
437 #[tokio::test]
438 async fn test_invalid_operation() {
439 let tool = DateTimeTool::new();
440 let result = tool
441 .execute(serde_json::json!({"operation": "invalid"}))
442 .await;
443 assert!(!result.success);
444 }
445
446 #[tokio::test]
447 async fn test_missing_value() {
448 let tool = DateTimeTool::new();
449 let result = tool
450 .execute(serde_json::json!({"operation": "format"}))
451 .await;
452 assert!(!result.success);
453 }
454
455 #[tokio::test]
456 async fn test_parse_unix_timestamp() {
457 let tool = DateTimeTool::new();
458 let result = tool
459 .execute(serde_json::json!({
460 "operation": "parse",
461 "value": "1704067200"
462 }))
463 .await;
464 assert!(result.success);
465 }
466}