Skip to main content

ai_agents_tools/builtin/
datetime.rs

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 to perform: now, format, parse, add, diff
27    operation: String,
28    /// Date/time value (ISO 8601 format for most operations)
29    #[serde(default)]
30    value: Option<String>,
31    /// Second date/time value (for diff operation)
32    #[serde(default)]
33    value2: Option<String>,
34    /// Format string using strftime syntax (e.g., '%Y-%m-%d %H:%M:%S')
35    #[serde(default)]
36    format: Option<String>,
37    /// Amount to add (for add operation)
38    #[serde(default)]
39    amount: Option<i64>,
40    /// Unit for add operation: seconds, minutes, hours, days, weeks
41    #[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}