1#[macro_export]
26macro_rules! quick_tool {
27 (
28 name: $name:ident,
29 description: $desc:expr,
30 params: ($($param_name:ident: $param_type:tt),* $(,)?),
31 execute: |$($param_var:ident),*| $body:expr
32 ) => {
33 {
34 let param_str = vec![
35 $(
36 format!(
37 "{}:{}:{}",
38 stringify!($param_name),
39 stringify!($param_type),
40 stringify!($param_name).replace('_', " ")
41 )
42 ),*
43 ].join(", ");
44
45 $crate::ToolBuilder::from_fn(
46 stringify!($name),
47 $desc,
48 param_str,
49 |args| {
50 $(
51 let $param_var = $crate::quick_tool!(@extract args, stringify!($param_name), $param_type)?;
52 )*
53 let result = $body;
54 Ok($crate::ToolResult::success(result))
55 }
56 ).build()
57 }
58 };
59
60 (@extract $args:ident, $name:expr, i32) => {
62 $args.get($name)
63 .and_then(|v| v.as_i64())
64 .map(|v| v as i32)
65 .ok_or_else(|| $crate::error::HeliosError::ToolError(
66 format!("Missing or invalid required parameter '{}'", $name)
67 ))
68 };
69 (@extract $args:ident, $name:expr, i64) => {
70 $args.get($name)
71 .and_then(|v| v.as_i64())
72 .ok_or_else(|| $crate::error::HeliosError::ToolError(
73 format!("Missing or invalid required parameter '{}'", $name)
74 ))
75 };
76 (@extract $args:ident, $name:expr, u32) => {
77 $args.get($name)
78 .and_then(|v| v.as_u64())
79 .map(|v| v as u32)
80 .ok_or_else(|| $crate::error::HeliosError::ToolError(
81 format!("Missing or invalid required parameter '{}'", $name)
82 ))
83 };
84 (@extract $args:ident, $name:expr, u64) => {
85 $args.get($name)
86 .and_then(|v| v.as_u64())
87 .ok_or_else(|| $crate::error::HeliosError::ToolError(
88 format!("Missing or invalid required parameter '{}'", $name)
89 ))
90 };
91 (@extract $args:ident, $name:expr, f32) => {
92 $args.get($name)
93 .and_then(|v| v.as_f64())
94 .map(|v| v as f32)
95 .ok_or_else(|| $crate::error::HeliosError::ToolError(
96 format!("Missing or invalid required parameter '{}'", $name)
97 ))
98 };
99 (@extract $args:ident, $name:expr, f64) => {
100 $args.get($name)
101 .and_then(|v| v.as_f64())
102 .ok_or_else(|| $crate::error::HeliosError::ToolError(
103 format!("Missing or invalid required parameter '{}'", $name)
104 ))
105 };
106 (@extract $args:ident, $name:expr, bool) => {
107 $args.get($name)
108 .and_then(|v| v.as_bool())
109 .ok_or_else(|| $crate::error::HeliosError::ToolError(
110 format!("Missing or invalid required parameter '{}'", $name)
111 ))
112 };
113 (@extract $args:ident, $name:expr, String) => {
114 $args.get($name)
115 .and_then(|v| v.as_str())
116 .map(|s| s.to_string())
117 .ok_or_else(|| $crate::error::HeliosError::ToolError(
118 format!("Missing or invalid required parameter '{}'", $name)
119 ))
120 };
121}
122
123#[cfg(test)]
124mod tests {
125 use crate::quick_tool;
126 use serde_json::json;
127
128 #[test]
129 fn test_quick_tool_with_valid_parameters() {
130 let tool = quick_tool! {
131 name: test_add,
132 description: "Add two numbers",
133 params: (x: i32, y: i32),
134 execute: |x, y| {
135 format!("Result: {}", x + y)
136 }
137 };
138
139 assert_eq!(tool.name(), "test_add");
140 assert_eq!(tool.description(), "Add two numbers");
141 }
142
143 #[tokio::test]
144 async fn test_quick_tool_execution_with_valid_args() {
145 let tool = quick_tool! {
146 name: test_multiply,
147 description: "Multiply two numbers",
148 params: (a: i32, b: i32),
149 execute: |a, b| {
150 format!("{}", a * b)
151 }
152 };
153
154 let args = json!({"a": 5, "b": 3});
155 let result = tool.execute(args).await.unwrap();
156 assert!(result.success);
157 assert_eq!(result.output, "15");
158 }
159
160 #[tokio::test]
161 async fn test_quick_tool_missing_parameter_returns_error() {
162 let tool = quick_tool! {
163 name: test_divide,
164 description: "Divide two numbers",
165 params: (numerator: f64, denominator: f64),
166 execute: |num, den| {
167 format!("{}", num / den)
168 }
169 };
170
171 let args = json!({"numerator": 10.0});
173 let result = tool.execute(args).await;
174
175 assert!(result.is_err());
176 let error_msg = format!("{}", result.unwrap_err());
177 assert!(error_msg.contains("Missing or invalid required parameter"));
178 }
179
180 #[tokio::test]
181 async fn test_quick_tool_invalid_type_returns_error() {
182 let tool = quick_tool! {
183 name: test_type_check,
184 description: "Test type checking",
185 params: (value: i32),
186 execute: |v| {
187 format!("{}", v)
188 }
189 };
190
191 let args = json!({"value": "not a number"});
193 let result = tool.execute(args).await;
194
195 assert!(result.is_err());
196 let error_msg = format!("{}", result.unwrap_err());
197 assert!(error_msg.contains("Missing or invalid required parameter"));
198 }
199
200 #[tokio::test]
201 async fn test_quick_tool_with_string_and_bool() {
202 let tool = quick_tool! {
203 name: test_greet,
204 description: "Greet someone",
205 params: (name: String, formal: bool),
206 execute: |name, formal| {
207 if formal {
208 format!("Good day, {}", name)
209 } else {
210 format!("Hey {}", name)
211 }
212 }
213 };
214
215 let args = json!({"name": "Alice", "formal": true});
216 let result = tool.execute(args).await.unwrap();
217 assert!(result.success);
218 assert_eq!(result.output, "Good day, Alice");
219
220 let args = json!({"name": "Bob", "formal": false});
221 let result = tool.execute(args).await.unwrap();
222 assert!(result.success);
223 assert_eq!(result.output, "Hey Bob");
224 }
225
226 #[tokio::test]
227 async fn test_quick_tool_prevents_division_by_zero_default() {
228 let tool = quick_tool! {
231 name: safe_divide,
232 description: "Safely divide two numbers",
233 params: (x: f64, y: f64),
234 execute: |x, y| {
235 format!("{}", x / y)
236 }
237 };
238
239 let args = json!({"x": 10.0});
241 let result = tool.execute(args).await;
242
243 assert!(
244 result.is_err(),
245 "Should error on missing divisor, not default to 0"
246 );
247 }
248}