1use async_trait::async_trait;
4use chrono::{DateTime, Duration, Local, NaiveDate, NaiveDateTime, TimeZone, Utc};
5use cortexai_core::{errors::ToolError, ExecutionContext, Tool, ToolSchema};
6use serde_json::json;
7
8pub struct CurrentTimeTool;
10
11impl Default for CurrentTimeTool {
12 fn default() -> Self {
13 Self::new()
14 }
15}
16
17impl CurrentTimeTool {
18 pub fn new() -> Self {
19 Self
20 }
21}
22
23#[async_trait]
24impl Tool for CurrentTimeTool {
25 fn schema(&self) -> ToolSchema {
26 ToolSchema::new("current_time", "Get the current date and time").with_parameters(json!({
27 "type": "object",
28 "properties": {
29 "timezone": {
30 "type": "string",
31 "description": "Timezone (e.g., 'UTC', 'local'). Default: 'local'"
32 },
33 "format": {
34 "type": "string",
35 "description": "Output format. Options: 'iso', 'rfc2822', 'unix', 'custom'. Default: 'iso'"
36 },
37 "custom_format": {
38 "type": "string",
39 "description": "Custom strftime format string (only used when format='custom')"
40 }
41 }
42 }))
43 }
44
45 async fn execute(
46 &self,
47 _context: &ExecutionContext,
48 arguments: serde_json::Value,
49 ) -> Result<serde_json::Value, ToolError> {
50 let timezone = arguments["timezone"].as_str().unwrap_or("local");
51 let format = arguments["format"].as_str().unwrap_or("iso");
52
53 let now_utc = Utc::now();
54 let now_local = Local::now();
55
56 let (datetime_str, timestamp) = match timezone.to_lowercase().as_str() {
57 "utc" => {
58 let formatted =
59 format_datetime(&now_utc, format, arguments["custom_format"].as_str())?;
60 (formatted, now_utc.timestamp())
61 }
62 "local" | _ => {
63 let formatted =
64 format_datetime(&now_local, format, arguments["custom_format"].as_str())?;
65 (formatted, now_local.timestamp())
66 }
67 };
68
69 Ok(json!({
70 "datetime": datetime_str,
71 "timestamp": timestamp,
72 "timezone": timezone,
73 "utc": now_utc.to_rfc3339(),
74 "local": now_local.to_rfc3339(),
75 "components": {
76 "year": now_local.format("%Y").to_string(),
77 "month": now_local.format("%m").to_string(),
78 "day": now_local.format("%d").to_string(),
79 "hour": now_local.format("%H").to_string(),
80 "minute": now_local.format("%M").to_string(),
81 "second": now_local.format("%S").to_string(),
82 "weekday": now_local.format("%A").to_string(),
83 "week_number": now_local.format("%W").to_string()
84 }
85 }))
86 }
87}
88
89fn format_datetime<Tz: TimeZone>(
90 dt: &DateTime<Tz>,
91 format: &str,
92 custom_format: Option<&str>,
93) -> Result<String, ToolError>
94where
95 Tz::Offset: std::fmt::Display,
96{
97 match format {
98 "iso" => Ok(dt.to_rfc3339()),
99 "rfc2822" => Ok(dt.to_rfc2822()),
100 "unix" => Ok(dt.timestamp().to_string()),
101 "custom" => {
102 let fmt = custom_format.ok_or_else(|| {
103 ToolError::InvalidArguments(
104 "custom_format required when format='custom'".to_string(),
105 )
106 })?;
107 Ok(dt.format(fmt).to_string())
108 }
109 _ => Ok(dt.to_rfc3339()),
110 }
111}
112
113pub struct DateParserTool;
115
116impl Default for DateParserTool {
117 fn default() -> Self {
118 Self::new()
119 }
120}
121
122impl DateParserTool {
123 pub fn new() -> Self {
124 Self
125 }
126}
127
128#[async_trait]
129impl Tool for DateParserTool {
130 fn schema(&self) -> ToolSchema {
131 ToolSchema::new("parse_date", "Parse a date string into components")
132 .with_parameters(json!({
133 "type": "object",
134 "properties": {
135 "date": {
136 "type": "string",
137 "description": "Date string to parse (supports ISO 8601, RFC 2822, common formats)"
138 },
139 "output_format": {
140 "type": "string",
141 "description": "Desired output format (strftime format string)"
142 }
143 },
144 "required": ["date"]
145 }))
146 }
147
148 async fn execute(
149 &self,
150 _context: &ExecutionContext,
151 arguments: serde_json::Value,
152 ) -> Result<serde_json::Value, ToolError> {
153 let date_str = arguments["date"]
154 .as_str()
155 .ok_or_else(|| ToolError::InvalidArguments("Missing 'date' field".to_string()))?;
156
157 let parsed = parse_flexible_date(date_str)?;
159
160 let output_format = arguments["output_format"].as_str();
161 let formatted = if let Some(fmt) = output_format {
162 parsed.format(fmt).to_string()
163 } else {
164 parsed.to_rfc3339()
165 };
166
167 Ok(json!({
168 "input": date_str,
169 "parsed": parsed.to_rfc3339(),
170 "formatted": formatted,
171 "timestamp": parsed.timestamp(),
172 "components": {
173 "year": parsed.format("%Y").to_string().parse::<i32>().unwrap_or(0),
174 "month": parsed.format("%m").to_string().parse::<u32>().unwrap_or(0),
175 "day": parsed.format("%d").to_string().parse::<u32>().unwrap_or(0),
176 "hour": parsed.format("%H").to_string().parse::<u32>().unwrap_or(0),
177 "minute": parsed.format("%M").to_string().parse::<u32>().unwrap_or(0),
178 "second": parsed.format("%S").to_string().parse::<u32>().unwrap_or(0),
179 "weekday": parsed.format("%A").to_string(),
180 "day_of_year": parsed.format("%j").to_string().parse::<u32>().unwrap_or(0)
181 }
182 }))
183 }
184}
185
186fn parse_flexible_date(s: &str) -> Result<DateTime<Utc>, ToolError> {
187 if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
189 return Ok(dt.with_timezone(&Utc));
190 }
191
192 if let Ok(dt) = DateTime::parse_from_rfc2822(s) {
194 return Ok(dt.with_timezone(&Utc));
195 }
196
197 let formats = [
199 "%Y-%m-%d %H:%M:%S",
200 "%Y-%m-%d %H:%M",
201 "%Y-%m-%d",
202 "%d/%m/%Y %H:%M:%S",
203 "%d/%m/%Y %H:%M",
204 "%d/%m/%Y",
205 "%m/%d/%Y %H:%M:%S",
206 "%m/%d/%Y %H:%M",
207 "%m/%d/%Y",
208 "%Y%m%d",
209 "%d-%m-%Y",
210 "%B %d, %Y",
211 "%b %d, %Y",
212 ];
213
214 for fmt in &formats {
215 if let Ok(naive) = NaiveDateTime::parse_from_str(s, fmt) {
216 return Ok(Utc.from_utc_datetime(&naive));
217 }
218 if let Ok(naive) = NaiveDate::parse_from_str(s, fmt) {
219 return Ok(Utc.from_utc_datetime(&naive.and_hms_opt(0, 0, 0).unwrap()));
220 }
221 }
222
223 if let Ok(ts) = s.parse::<i64>() {
225 if let Some(dt) = DateTime::from_timestamp(ts, 0) {
226 return Ok(dt);
227 }
228 }
229
230 Err(ToolError::InvalidArguments(format!(
231 "Could not parse date: '{}'",
232 s
233 )))
234}
235
236pub struct DateCalculatorTool;
238
239impl Default for DateCalculatorTool {
240 fn default() -> Self {
241 Self::new()
242 }
243}
244
245impl DateCalculatorTool {
246 pub fn new() -> Self {
247 Self
248 }
249}
250
251#[async_trait]
252impl Tool for DateCalculatorTool {
253 fn schema(&self) -> ToolSchema {
254 ToolSchema::new(
255 "date_calculator",
256 "Calculate date differences or add/subtract time",
257 )
258 .with_parameters(json!({
259 "type": "object",
260 "properties": {
261 "operation": {
262 "type": "string",
263 "enum": ["diff", "add", "subtract"],
264 "description": "Operation to perform"
265 },
266 "date1": {
267 "type": "string",
268 "description": "First date (or base date for add/subtract)"
269 },
270 "date2": {
271 "type": "string",
272 "description": "Second date (for diff operation)"
273 },
274 "amount": {
275 "type": "integer",
276 "description": "Amount to add/subtract"
277 },
278 "unit": {
279 "type": "string",
280 "enum": ["days", "hours", "minutes", "seconds", "weeks"],
281 "description": "Unit for add/subtract operation"
282 }
283 },
284 "required": ["operation", "date1"]
285 }))
286 }
287
288 async fn execute(
289 &self,
290 _context: &ExecutionContext,
291 arguments: serde_json::Value,
292 ) -> Result<serde_json::Value, ToolError> {
293 let operation = arguments["operation"]
294 .as_str()
295 .ok_or_else(|| ToolError::InvalidArguments("Missing 'operation'".to_string()))?;
296 let date1_str = arguments["date1"]
297 .as_str()
298 .ok_or_else(|| ToolError::InvalidArguments("Missing 'date1'".to_string()))?;
299
300 let date1 = parse_flexible_date(date1_str)?;
301
302 match operation {
303 "diff" => {
304 let date2_str = arguments["date2"].as_str().ok_or_else(|| {
305 ToolError::InvalidArguments("Missing 'date2' for diff".to_string())
306 })?;
307 let date2 = parse_flexible_date(date2_str)?;
308
309 let diff = date2.signed_duration_since(date1);
310
311 Ok(json!({
312 "date1": date1.to_rfc3339(),
313 "date2": date2.to_rfc3339(),
314 "difference": {
315 "total_seconds": diff.num_seconds(),
316 "total_minutes": diff.num_minutes(),
317 "total_hours": diff.num_hours(),
318 "total_days": diff.num_days(),
319 "total_weeks": diff.num_weeks(),
320 "human_readable": format_duration(diff)
321 }
322 }))
323 }
324 "add" | "subtract" => {
325 let amount = arguments["amount"]
326 .as_i64()
327 .ok_or_else(|| ToolError::InvalidArguments("Missing 'amount'".to_string()))?;
328 let unit = arguments["unit"]
329 .as_str()
330 .ok_or_else(|| ToolError::InvalidArguments("Missing 'unit'".to_string()))?;
331
332 let duration = match unit {
333 "seconds" => Duration::seconds(amount),
334 "minutes" => Duration::minutes(amount),
335 "hours" => Duration::hours(amount),
336 "days" => Duration::days(amount),
337 "weeks" => Duration::weeks(amount),
338 _ => {
339 return Err(ToolError::InvalidArguments(format!(
340 "Unknown unit: {}",
341 unit
342 )))
343 }
344 };
345
346 let result = if operation == "add" {
347 date1 + duration
348 } else {
349 date1 - duration
350 };
351
352 Ok(json!({
353 "original": date1.to_rfc3339(),
354 "operation": operation,
355 "amount": amount,
356 "unit": unit,
357 "result": result.to_rfc3339(),
358 "timestamp": result.timestamp()
359 }))
360 }
361 _ => Err(ToolError::InvalidArguments(format!(
362 "Unknown operation: {}",
363 operation
364 ))),
365 }
366 }
367}
368
369fn format_duration(dur: Duration) -> String {
370 let total_seconds = dur.num_seconds().abs();
371 let days = total_seconds / 86400;
372 let hours = (total_seconds % 86400) / 3600;
373 let minutes = (total_seconds % 3600) / 60;
374 let seconds = total_seconds % 60;
375
376 let sign = if dur.num_seconds() < 0 { "-" } else { "" };
377
378 if days > 0 {
379 format!("{}{}d {}h {}m {}s", sign, days, hours, minutes, seconds)
380 } else if hours > 0 {
381 format!("{}{}h {}m {}s", sign, hours, minutes, seconds)
382 } else if minutes > 0 {
383 format!("{}{}m {}s", sign, minutes, seconds)
384 } else {
385 format!("{}{}s", sign, seconds)
386 }
387}
388
389#[cfg(test)]
390mod tests {
391 use super::*;
392 use cortexai_core::types::AgentId;
393
394 fn test_ctx() -> ExecutionContext {
395 ExecutionContext::new(AgentId::new("test-agent"))
396 }
397
398 #[tokio::test]
399 async fn test_current_time() {
400 let tool = CurrentTimeTool::new();
401 let ctx = test_ctx();
402
403 let result = tool.execute(&ctx, json!({})).await.unwrap();
404 assert!(result["datetime"].as_str().is_some());
405 assert!(result["timestamp"].as_i64().is_some());
406 }
407
408 #[tokio::test]
409 async fn test_parse_date_iso() {
410 let tool = DateParserTool::new();
411 let ctx = test_ctx();
412
413 let result = tool
414 .execute(&ctx, json!({"date": "2024-01-15T10:30:00Z"}))
415 .await
416 .unwrap();
417
418 assert_eq!(result["components"]["year"], 2024);
419 assert_eq!(result["components"]["month"], 1);
420 assert_eq!(result["components"]["day"], 15);
421 }
422
423 #[tokio::test]
424 async fn test_parse_date_formats() {
425 let tool = DateParserTool::new();
426 let ctx = test_ctx();
427
428 let result = tool
430 .execute(&ctx, json!({"date": "2024-06-20"}))
431 .await
432 .unwrap();
433 assert_eq!(result["components"]["year"], 2024);
434 assert_eq!(result["components"]["month"], 6);
435
436 let result = tool
438 .execute(&ctx, json!({"date": "1700000000"}))
439 .await
440 .unwrap();
441 assert!(result["components"]["year"].as_i64().unwrap() >= 2023);
442 }
443
444 #[tokio::test]
445 async fn test_date_calculator_diff() {
446 let tool = DateCalculatorTool::new();
447 let ctx = test_ctx();
448
449 let result = tool
450 .execute(
451 &ctx,
452 json!({
453 "operation": "diff",
454 "date1": "2024-01-01",
455 "date2": "2024-01-08"
456 }),
457 )
458 .await
459 .unwrap();
460
461 assert_eq!(result["difference"]["total_days"], 7);
462 assert_eq!(result["difference"]["total_weeks"], 1);
463 }
464
465 #[tokio::test]
466 async fn test_date_calculator_add() {
467 let tool = DateCalculatorTool::new();
468 let ctx = test_ctx();
469
470 let result = tool
471 .execute(
472 &ctx,
473 json!({
474 "operation": "add",
475 "date1": "2024-01-01T00:00:00Z",
476 "amount": 7,
477 "unit": "days"
478 }),
479 )
480 .await
481 .unwrap();
482
483 assert!(result["result"].as_str().unwrap().contains("2024-01-08"));
484 }
485}