Skip to main content

lc_tools/
datetime.rs

1// lc-tools/src/datetime.rs
2//! Date and time tool for agents.
3//!
4//! Provides date/time query and calculation functionality.
5
6use async_trait::async_trait;
7use chrono::{DateTime, Datelike, Duration, Local, NaiveDate, NaiveDateTime, TimeZone, Weekday};
8use schemars::JsonSchema;
9use serde::{Deserialize, Serialize};
10
11use lc_core::tools::{BaseTool, Tool, ToolError};
12
13/// DateTime tool input parameters.
14#[derive(Debug, Deserialize, JsonSchema)]
15pub struct DateTimeInput {
16    /// Operation type: "now", "format", "add", "subtract", "weekday", "diff".
17    pub operation: String,
18
19    /// Date/time string (optional, format: YYYY-MM-DD or YYYY-MM-DD HH:MM:SS).
20    pub datetime: Option<String>,
21
22    /// Time unit: "days", "hours", "minutes", "weeks", "months", "years".
23    pub unit: Option<String>,
24
25    /// Value for add/subtract operations.
26    pub value: Option<i64>,
27
28    /// Target date/time for diff operation.
29    pub target: Option<String>,
30}
31
32/// DateTime tool output result.
33#[derive(Debug, Serialize)]
34pub struct DateTimeOutput {
35    /// Operation result.
36    pub result: String,
37
38    /// Operation type.
39    pub operation: String,
40
41    /// Additional details.
42    pub details: Option<String>,
43}
44
45/// DateTime tool for querying and manipulating dates and times.
46pub struct DateTimeTool;
47
48impl DateTimeTool {
49    /// Creates a new DateTimeTool instance.
50    pub fn new() -> Self {
51        Self
52    }
53
54    fn parse_datetime(&self, dt_str: &str) -> Result<DateTime<Local>, ToolError> {
55        if let Ok(dt) = NaiveDateTime::parse_from_str(dt_str, "%Y-%m-%d %H:%M:%S") {
56            return Local
57                .from_local_datetime(&dt)
58                .single()
59                .ok_or_else(|| ToolError::ExecutionFailed("invalid datetime".to_string()));
60        }
61
62        if let Ok(date) = NaiveDate::parse_from_str(dt_str, "%Y-%m-%d") {
63            let dt = date
64                .and_hms_opt(0, 0, 0)
65                .ok_or_else(|| ToolError::ExecutionFailed("invalid date".to_string()))?;
66            return Local
67                .from_local_datetime(&dt)
68                .single()
69                .ok_or_else(|| ToolError::ExecutionFailed("invalid datetime".to_string()));
70        }
71
72        Err(ToolError::ExecutionFailed(format!(
73            "failed to parse datetime: {}, use format YYYY-MM-DD or YYYY-MM-DD HH:MM:SS",
74            dt_str
75        )))
76    }
77
78    fn get_now(&self) -> DateTimeOutput {
79        let now = Local::now();
80        DateTimeOutput {
81            result: now.format("%Y-%m-%d %H:%M:%S").to_string(),
82            operation: "now".to_string(),
83            details: Some(format!(
84                "星期{},第{}周",
85                match now.weekday() {
86                    Weekday::Mon => "一",
87                    Weekday::Tue => "二",
88                    Weekday::Wed => "三",
89                    Weekday::Thu => "四",
90                    Weekday::Fri => "五",
91                    Weekday::Sat => "六",
92                    Weekday::Sun => "日",
93                },
94                now.iso_week().week()
95            )),
96        }
97    }
98
99    fn format_datetime(&self, dt_str: &str) -> Result<DateTimeOutput, ToolError> {
100        let dt = self.parse_datetime(dt_str)?;
101
102        Ok(DateTimeOutput {
103            result: dt.format("%Y年%m月%d日 %H时%M分%S秒").to_string(),
104            operation: "format".to_string(),
105            details: Some(format!(
106                "星期{},{}年{}周",
107                match dt.weekday() {
108                    Weekday::Mon => "一",
109                    Weekday::Tue => "二",
110                    Weekday::Wed => "三",
111                    Weekday::Thu => "四",
112                    Weekday::Fri => "五",
113                    Weekday::Sat => "六",
114                    Weekday::Sun => "日",
115                },
116                dt.year(),
117                dt.iso_week().week()
118            )),
119        })
120    }
121
122    fn add_time(&self, dt_str: &str, value: i64, unit: &str) -> Result<DateTimeOutput, ToolError> {
123        let dt = self.parse_datetime(dt_str)?;
124
125        let new_dt = match unit {
126            "seconds" => dt + Duration::seconds(value),
127            "minutes" => dt + Duration::minutes(value),
128            "hours" => dt + Duration::hours(value),
129            "days" => dt + Duration::days(value),
130            "weeks" => dt + Duration::weeks(value),
131            "months" => {
132                let months = value as i32;
133                let new_month = dt.month() as i32 + months;
134                let year = dt.year() + (new_month - 1) / 12;
135                let month = ((new_month - 1) % 12 + 1) as u32;
136
137                dt.with_year(year)
138                    .and_then(|d| d.with_month(month))
139                    .ok_or_else(|| ToolError::ExecutionFailed("month calculation failed".to_string()))?
140            }
141            "years" => dt
142                .with_year(dt.year() + value as i32)
143                .ok_or_else(|| ToolError::ExecutionFailed("year calculation failed".to_string()))?,
144            _ => {
145                return Err(ToolError::ExecutionFailed(format!(
146                "unsupported time unit: {}, use: seconds, minutes, hours, days, weeks, months, years",
147                unit
148            )))
149            }
150        };
151
152        Ok(DateTimeOutput {
153            result: new_dt.format("%Y-%m-%d %H:%M:%S").to_string(),
154            operation: "add".to_string(),
155            details: Some(format!("{} {} 后", value, self.unit_to_chinese(unit))),
156        })
157    }
158
159    fn subtract_time(
160        &self,
161        dt_str: &str,
162        value: i64,
163        unit: &str,
164    ) -> Result<DateTimeOutput, ToolError> {
165        self.add_time(dt_str, -value, unit)
166    }
167
168    fn get_weekday(&self, dt_str: &str) -> Result<DateTimeOutput, ToolError> {
169        let dt = self.parse_datetime(dt_str)?;
170
171        let weekday = match dt.weekday() {
172            Weekday::Mon => "星期一",
173            Weekday::Tue => "星期二",
174            Weekday::Wed => "星期三",
175            Weekday::Thu => "星期四",
176            Weekday::Fri => "星期五",
177            Weekday::Sat => "星期六",
178            Weekday::Sun => "星期日",
179        };
180
181        Ok(DateTimeOutput {
182            result: weekday.to_string(),
183            operation: "weekday".to_string(),
184            details: Some(format!("{} 是 {}", dt.format("%Y-%m-%d"), weekday)),
185        })
186    }
187
188    fn diff_time(&self, dt1_str: &str, dt2_str: &str) -> Result<DateTimeOutput, ToolError> {
189        let dt1 = self.parse_datetime(dt1_str)?;
190        let dt2 = self.parse_datetime(dt2_str)?;
191
192        let diff = dt2.signed_duration_since(dt1);
193
194        let days = diff.num_days();
195        let hours = diff.num_hours() % 24;
196        let minutes = diff.num_minutes() % 60;
197
198        Ok(DateTimeOutput {
199            result: format!("{}天 {}小时 {}分钟", days.abs(), hours.abs(), minutes.abs()),
200            operation: "diff".to_string(),
201            details: Some(if diff.num_seconds() >= 0 {
202                format!(
203                    "从 {} 到 {} 相隔 {}天 {}小时 {}分钟",
204                    dt1.format("%Y-%m-%d"),
205                    dt2.format("%Y-%m-%d"),
206                    days,
207                    hours,
208                    minutes
209                )
210            } else {
211                format!(
212                    "从 {} 到 {} 相隔 {}天 {}小时 {}分钟",
213                    dt2.format("%Y-%m-%d"),
214                    dt1.format("%Y-%m-%d"),
215                    days.abs(),
216                    hours.abs(),
217                    minutes.abs()
218                )
219            }),
220        })
221    }
222
223    fn unit_to_chinese(&self, unit: &str) -> String {
224        match unit {
225            "seconds" => "秒",
226            "minutes" => "分钟",
227            "hours" => "小时",
228            "days" => "天",
229            "weeks" => "周",
230            "months" => "月",
231            "years" => "年",
232            _ => unit,
233        }
234        .to_string()
235    }
236}
237
238impl Default for DateTimeTool {
239    fn default() -> Self {
240        Self::new()
241    }
242}
243
244#[async_trait]
245impl Tool for DateTimeTool {
246    type Input = DateTimeInput;
247    type Output = DateTimeOutput;
248
249    async fn invoke(&self, input: Self::Input) -> Result<Self::Output, ToolError> {
250        match input.operation.as_str() {
251            "now" => Ok(self.get_now()),
252            "format" => {
253                let dt = input.datetime.ok_or_else(|| {
254                    ToolError::InvalidInput(
255                        "format operation requires a datetime parameter".to_string(),
256                    )
257                })?;
258                self.format_datetime(&dt)
259            }
260            "add" => {
261                let dt = input.datetime.ok_or_else(|| {
262                    ToolError::InvalidInput(
263                        "add operation requires a datetime parameter".to_string(),
264                    )
265                })?;
266                let value = input.value.ok_or_else(|| {
267                    ToolError::InvalidInput("add operation requires a value parameter".to_string())
268                })?;
269                let unit = input.unit.ok_or_else(|| {
270                    ToolError::InvalidInput("add operation requires a unit parameter".to_string())
271                })?;
272                self.add_time(&dt, value, &unit)
273            }
274            "subtract" => {
275                let dt = input.datetime.ok_or_else(|| {
276                    ToolError::InvalidInput(
277                        "subtract operation requires a datetime parameter".to_string(),
278                    )
279                })?;
280                let value = input.value.ok_or_else(|| {
281                    ToolError::InvalidInput(
282                        "subtract operation requires a value parameter".to_string(),
283                    )
284                })?;
285                let unit = input.unit.ok_or_else(|| {
286                    ToolError::InvalidInput(
287                        "subtract operation requires a unit parameter".to_string(),
288                    )
289                })?;
290                self.subtract_time(&dt, value, &unit)
291            }
292            "weekday" => {
293                let dt = input.datetime.ok_or_else(|| {
294                    ToolError::InvalidInput(
295                        "weekday operation requires a datetime parameter".to_string(),
296                    )
297                })?;
298                self.get_weekday(&dt)
299            }
300            "diff" => {
301                let dt1 = input.datetime.ok_or_else(|| {
302                    ToolError::InvalidInput(
303                        "diff operation requires a datetime parameter".to_string(),
304                    )
305                })?;
306                let dt2 = input.target.ok_or_else(|| {
307                    ToolError::InvalidInput(
308                        "diff operation requires a target parameter".to_string(),
309                    )
310                })?;
311                self.diff_time(&dt1, &dt2)
312            }
313            _ => Err(ToolError::InvalidInput(format!(
314                "unsupported operation: {}, use: now, format, add, subtract, weekday, diff",
315                input.operation
316            ))),
317        }
318    }
319}
320
321#[async_trait]
322impl BaseTool for DateTimeTool {
323    fn name(&self) -> &str {
324        "datetime"
325    }
326
327    fn description(&self) -> &str {
328        "日期时间工具。支持多种操作:
329
330操作类型:
331- now: 获取当前时间
332- format: 格式化日期时间
333- add: 添加时间
334- subtract: 减去时间
335- weekday: 获取星期几
336- diff: 计算时间差
337
338示例:
339- 获取当前时间: {\"operation\": \"now\"}
340- 格式化日期: {\"operation\": \"format\", \"datetime\": \"2024-01-15\"}
341- 添加3天: {\"operation\": \"add\", \"datetime\": \"2024-01-15\", \"value\": 3, \"unit\": \"days\"}
342- 计算差值: {\"operation\": \"diff\", \"datetime\": \"2024-01-01\", \"target\": \"2024-01-15\"}"
343    }
344
345    async fn run(&self, input: String) -> Result<String, ToolError> {
346        let parsed: DateTimeInput = serde_json::from_str(&input)
347            .map_err(|e| ToolError::InvalidInput(format!("JSON parse failed: {}", e)))?;
348
349        let output = self.invoke(parsed).await?;
350
351        Ok(format!(
352            "{}\n详细信息: {}",
353            output.result,
354            output.details.unwrap_or_default()
355        ))
356    }
357
358    fn args_schema(&self) -> Option<serde_json::Value> {
359        use schemars::schema_for;
360        serde_json::to_value(schema_for!(DateTimeInput)).ok()
361    }
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367
368    #[tokio::test]
369    async fn test_datetime_now() {
370        let tool = DateTimeTool::new();
371
372        let input = DateTimeInput {
373            operation: "now".to_string(),
374            datetime: None,
375            unit: None,
376            value: None,
377            target: None,
378        };
379
380        let result = tool.invoke(input).await.unwrap();
381        assert!(!result.result.is_empty());
382        assert!(result.details.is_some());
383    }
384
385    #[tokio::test]
386    async fn test_datetime_format() {
387        let tool = DateTimeTool::new();
388
389        let input = DateTimeInput {
390            operation: "format".to_string(),
391            datetime: Some("2024-01-15".to_string()),
392            unit: None,
393            value: None,
394            target: None,
395        };
396
397        let result = tool.invoke(input).await.unwrap();
398        assert!(result.result.contains("2024年"));
399        assert!(result.result.contains("01月"));
400        assert!(result.result.contains("15日"));
401    }
402
403    #[tokio::test]
404    async fn test_datetime_add_days() {
405        let tool = DateTimeTool::new();
406
407        let input = DateTimeInput {
408            operation: "add".to_string(),
409            datetime: Some("2024-01-15".to_string()),
410            unit: Some("days".to_string()),
411            value: Some(3),
412            target: None,
413        };
414
415        let result = tool.invoke(input).await.unwrap();
416        assert!(result.result.contains("2024-01-18"));
417    }
418
419    #[tokio::test]
420    async fn test_datetime_weekday() {
421        let tool = DateTimeTool::new();
422
423        // 2024-01-15 is a Monday
424        let input = DateTimeInput {
425            operation: "weekday".to_string(),
426            datetime: Some("2024-01-15".to_string()),
427            unit: None,
428            value: None,
429            target: None,
430        };
431
432        let result = tool.invoke(input).await.unwrap();
433        assert_eq!(result.result, "星期一");
434    }
435
436    #[tokio::test]
437    async fn test_datetime_diff() {
438        let tool = DateTimeTool::new();
439
440        let input = DateTimeInput {
441            operation: "diff".to_string(),
442            datetime: Some("2024-01-01".to_string()),
443            unit: None,
444            value: None,
445            target: Some("2024-01-15".to_string()),
446        };
447
448        let result = tool.invoke(input).await.unwrap();
449        assert!(result.result.contains("14天"));
450    }
451}