mojentic/llm/tools/
simple_date_tool.rs1use 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#[derive(Clone)]
26pub struct SimpleDateTool;
27
28impl SimpleDateTool {
29 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 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 if let Some(days) = self.extract_days_offset(&lower, "from now") {
54 return Ok(today + chrono::Duration::days(days));
55 }
56
57 if let Some(days) = self.extract_days_offset(&lower, "ago") {
59 return Ok(today - chrono::Duration::days(days));
60 }
61
62 if lower.contains("next week") {
64 return Ok(today + chrono::Duration::weeks(1));
65 }
66
67 if lower.contains("last week") {
69 return Ok(today - chrono::Duration::weeks(1));
70 }
71
72 Ok(today)
74 }
75
76 fn extract_days_offset(&self, text: &str, suffix: &str) -> Option<i64> {
78 if !text.contains(suffix) {
79 return None;
80 }
81
82 let words: Vec<&str> = text.split_whitespace().collect();
84 for (i, word) in words.iter().enumerate() {
85 if word.contains("day") && i > 0 {
86 if let Ok(num) = words[i - 1].parse::<i64>() {
88 return Some(num);
89 }
90 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}