mojentic/llm/tools/
simple_date_tool.rs

1use crate::error::Result;
2use crate::llm::tools::{FunctionDescriptor, LlmTool, ToolDescriptor};
3use chrono::{Local, NaiveDate};
4use serde_json::{json, Value};
5use std::collections::HashMap;
6
7/// Simple date resolution tool that resolves relative date expressions to absolute dates
8///
9/// This tool takes text like "three days from now" or "next Tuesday" and resolves it
10/// to an absolute date. It uses a simple heuristic approach for common patterns.
11///
12/// # Examples
13///
14/// ```ignore
15/// use mojentic::llm::tools::simple_date_tool::SimpleDateTool;
16///
17/// let tool = SimpleDateTool;
18/// let args = HashMap::from([
19///     ("relative_date".to_string(), json!("tomorrow"))
20/// ]);
21///
22/// let result = tool.run(&args)?;
23/// // result contains absolute date for tomorrow
24/// ```
25#[derive(Clone)]
26pub struct SimpleDateTool;
27
28impl SimpleDateTool {
29    /// Parse relative date expressions
30    ///
31    /// Supports common patterns like:
32    /// - "today", "tomorrow", "yesterday"
33    /// - "X days from now", "X days ago"
34    /// - "next week", "last week"
35    fn parse_relative_date(&self, relative_date: &str) -> Result<NaiveDate> {
36        let today = Local::now().date_naive();
37        let lower = relative_date.to_lowercase();
38
39        // Simple pattern matching for common cases
40        if lower.contains("today") {
41            return Ok(today);
42        }
43
44        if lower.contains("tomorrow") {
45            return Ok(today + chrono::Duration::days(1));
46        }
47
48        if lower.contains("yesterday") {
49            return Ok(today - chrono::Duration::days(1));
50        }
51
52        // Parse "X days from now"
53        if let Some(days) = self.extract_days_offset(&lower, "from now") {
54            return Ok(today + chrono::Duration::days(days));
55        }
56
57        // Parse "X days ago"
58        if let Some(days) = self.extract_days_offset(&lower, "ago") {
59            return Ok(today - chrono::Duration::days(days));
60        }
61
62        // Parse "next week"
63        if lower.contains("next week") {
64            return Ok(today + chrono::Duration::weeks(1));
65        }
66
67        // Parse "last week"
68        if lower.contains("last week") {
69            return Ok(today - chrono::Duration::weeks(1));
70        }
71
72        // Default to today if we can't parse
73        Ok(today)
74    }
75
76    /// Extract numeric offset from phrases like "3 days from now"
77    fn extract_days_offset(&self, text: &str, suffix: &str) -> Option<i64> {
78        if !text.contains(suffix) {
79            return None;
80        }
81
82        // Extract number before "days"
83        let words: Vec<&str> = text.split_whitespace().collect();
84        for (i, word) in words.iter().enumerate() {
85            if word.contains("day") && i > 0 {
86                // Try to parse the previous word as a number
87                if let Ok(num) = words[i - 1].parse::<i64>() {
88                    return Some(num);
89                }
90                // Try named numbers
91                return Some(match words[i - 1] {
92                    "one" | "a" => 1,
93                    "two" => 2,
94                    "three" => 3,
95                    "four" => 4,
96                    "five" => 5,
97                    "six" => 6,
98                    "seven" => 7,
99                    _ => 1,
100                });
101            }
102        }
103
104        None
105    }
106}
107
108impl LlmTool for SimpleDateTool {
109    fn run(&self, args: &HashMap<String, Value>) -> Result<Value> {
110        let relative_date =
111            args.get("relative_date").and_then(|v| v.as_str()).ok_or_else(|| {
112                crate::error::MojenticError::ToolError(
113                    "Missing required argument: relative_date".to_string(),
114                )
115            })?;
116
117        let resolved_date = self.parse_relative_date(relative_date)?;
118        let formatted_date = resolved_date.format("%Y-%m-%d").to_string();
119
120        Ok(json!({
121            "relative_date": relative_date,
122            "resolved_date": formatted_date,
123            "summary": format!("The date '{}' is {}", relative_date, formatted_date)
124        }))
125    }
126
127    fn descriptor(&self) -> ToolDescriptor {
128        ToolDescriptor {
129            r#type: "function".to_string(),
130            function: FunctionDescriptor {
131                name: "resolve_date".to_string(),
132                description: "Resolves relative date expressions to absolute dates. \
133                             Takes text like 'tomorrow', 'three days from now', or 'next week' \
134                             and returns the absolute date in YYYY-MM-DD format."
135                    .to_string(),
136                parameters: json!({
137                    "type": "object",
138                    "properties": {
139                        "relative_date": {
140                            "type": "string",
141                            "description": "The relative date expression to resolve (e.g., 'tomorrow', '3 days from now', 'next week')"
142                        }
143                    },
144                    "required": ["relative_date"]
145                }),
146            },
147        }
148    }
149
150    fn clone_box(&self) -> Box<dyn LlmTool> {
151        Box::new(self.clone())
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn test_descriptor() {
161        let tool = SimpleDateTool;
162        let desc = tool.descriptor();
163
164        assert_eq!(desc.r#type, "function");
165        assert_eq!(desc.function.name, "resolve_date");
166        assert!(desc.function.description.contains("relative date"));
167    }
168
169    #[test]
170    fn test_resolve_today() {
171        let tool = SimpleDateTool;
172        let args = HashMap::from([("relative_date".to_string(), json!("today"))]);
173
174        let result = tool.run(&args).unwrap();
175        let today = Local::now().date_naive().format("%Y-%m-%d").to_string();
176
177        assert_eq!(result["relative_date"], "today");
178        assert_eq!(result["resolved_date"], today);
179    }
180
181    #[test]
182    fn test_resolve_tomorrow() {
183        let tool = SimpleDateTool;
184        let args = HashMap::from([("relative_date".to_string(), json!("tomorrow"))]);
185
186        let result = tool.run(&args).unwrap();
187        let tomorrow = (Local::now().date_naive() + chrono::Duration::days(1))
188            .format("%Y-%m-%d")
189            .to_string();
190
191        assert_eq!(result["relative_date"], "tomorrow");
192        assert_eq!(result["resolved_date"], tomorrow);
193    }
194
195    #[test]
196    fn test_resolve_days_from_now() {
197        let tool = SimpleDateTool;
198        let args = HashMap::from([("relative_date".to_string(), json!("3 days from now"))]);
199
200        let result = tool.run(&args).unwrap();
201        let expected = (Local::now().date_naive() + chrono::Duration::days(3))
202            .format("%Y-%m-%d")
203            .to_string();
204
205        assert_eq!(result["resolved_date"], expected);
206    }
207
208    #[test]
209    fn test_resolve_days_ago() {
210        let tool = SimpleDateTool;
211        let args = HashMap::from([("relative_date".to_string(), json!("2 days ago"))]);
212
213        let result = tool.run(&args).unwrap();
214        let expected = (Local::now().date_naive() - chrono::Duration::days(2))
215            .format("%Y-%m-%d")
216            .to_string();
217
218        assert_eq!(result["resolved_date"], expected);
219    }
220
221    #[test]
222    fn test_missing_argument() {
223        let tool = SimpleDateTool;
224        let args = HashMap::new();
225
226        let result = tool.run(&args);
227        assert!(result.is_err());
228    }
229
230    #[test]
231    fn test_extract_days_offset() {
232        let tool = SimpleDateTool;
233
234        assert_eq!(tool.extract_days_offset("3 days from now", "from now"), Some(3));
235        assert_eq!(tool.extract_days_offset("five days from now", "from now"), Some(5));
236        assert_eq!(tool.extract_days_offset("2 days ago", "ago"), Some(2));
237        assert_eq!(tool.extract_days_offset("tomorrow", "from now"), None);
238    }
239}