ares_tools/tools/
calculator.rs1use crate::registry::Tool;
2use ares_types::Result;
3use async_trait::async_trait;
4use cordis::Service;
5use serde::{Deserialize, Serialize};
6use serde_json::{json, Value};
7
8pub struct Calculator;
10
11#[async_trait]
12impl Tool for Calculator {
13 fn name(&self) -> &str {
14 "calculator"
15 }
16
17 fn description(&self) -> &str {
18 "Perform basic arithmetic operations"
19 }
20
21 fn parameters_schema(&self) -> Value {
22 json!({
23 "type": "object",
24 "properties": {
25 "operation": {
26 "type": "string",
27 "enum": ["add", "subtract", "multiply", "divide"]
28 },
29 "a": { "type": "number" },
30 "b": { "type": "number" }
31 },
32 "required": ["operation", "a", "b"]
33 })
34 }
35
36 async fn execute(&self, args: Value) -> Result<Value> {
37 let op = args["operation"].as_str().unwrap_or("add");
38 let a = args["a"].as_f64().unwrap_or(0.0);
39 let b = args["b"].as_f64().unwrap_or(0.0);
40
41 let result = match op {
42 "add" => a + b,
43 "subtract" => a - b,
44 "multiply" => a * b,
45 "divide" => a / b,
46 _ => 0.0,
47 };
48
49 Ok(json!({ "result": result }))
50 }
51}
52
53#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
58pub struct CalculatorConfig;
59
60pub struct CalculatorService;
65
66impl CalculatorService {
67 pub fn new() -> Self {
69 Self
70 }
71
72 pub fn with_config(_config: CalculatorConfig) -> Self {
75 Self
76 }
77}
78
79impl Default for CalculatorService {
80 fn default() -> Self {
81 Self::new()
82 }
83}
84
85impl Service for CalculatorService {
86 fn name(&self) -> &'static str {
87 "calculator"
88 }
89
90 fn init(&self, _ctx: &std::sync::Arc<cordis::Context>) -> cordis::ServiceInitFuture<'_> {
91 Box::pin(async move { Ok(None) })
92 }
93}
94
95#[async_trait]
96impl Tool for CalculatorService {
97 fn name(&self) -> &str {
98 "calculator"
99 }
100
101 fn description(&self) -> &str {
102 "Perform basic arithmetic operations"
103 }
104
105 fn parameters_schema(&self) -> Value {
106 json!({
107 "type": "object",
108 "properties": {
109 "operation": {
110 "type": "string",
111 "enum": ["add", "subtract", "multiply", "divide"]
112 },
113 "a": { "type": "number" },
114 "b": { "type": "number" }
115 },
116 "required": ["operation", "a", "b"]
117 })
118 }
119
120 async fn execute(&self, args: Value) -> Result<Value> {
121 let op = args["operation"].as_str().unwrap_or("add");
122 let a = args["a"].as_f64().unwrap_or(0.0);
123 let b = args["b"].as_f64().unwrap_or(0.0);
124
125 let result = match op {
126 "add" => a + b,
127 "subtract" => a - b,
128 "multiply" => a * b,
129 "divide" => a / b,
130 _ => 0.0,
131 };
132
133 Ok(json!({ "result": result }))
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140 use serde_json::json;
141
142 #[test]
143 fn test_name_and_description() {
144 let tool = Calculator;
145 assert_eq!(tool.name(), "calculator");
146 assert_eq!(tool.description(), "Perform basic arithmetic operations");
147 }
148
149 #[test]
150 fn test_parameters_schema() {
151 let tool = Calculator;
152 let schema = tool.parameters_schema();
153 assert_eq!(schema["type"], "object");
154 assert_eq!(
155 schema["properties"]["operation"]["enum"],
156 json!(["add", "subtract", "multiply", "divide"])
157 );
158 assert_eq!(schema["required"], json!(["operation", "a", "b"]));
159 }
160
161 #[tokio::test]
162 async fn test_add() {
163 let tool = Calculator;
164 let out = tool
165 .execute(json!({"operation": "add", "a": 10.0, "b": 3.5}))
166 .await
167 .unwrap();
168 assert_eq!(out["result"], json!(13.5));
169 }
170
171 #[tokio::test]
172 async fn test_subtract() {
173 let tool = Calculator;
174 let out = tool
175 .execute(json!({"operation": "subtract", "a": 10.0, "b": 3.0}))
176 .await
177 .unwrap();
178 assert_eq!(out["result"], json!(7.0));
179 }
180
181 #[tokio::test]
182 async fn test_multiply() {
183 let tool = Calculator;
184 let out = tool
185 .execute(json!({"operation": "multiply", "a": 4.0, "b": 2.5}))
186 .await
187 .unwrap();
188 assert_eq!(out["result"], json!(10.0));
189 }
190
191 #[tokio::test]
192 async fn test_divide() {
193 let tool = Calculator;
194 let out = tool
195 .execute(json!({"operation": "divide", "a": 10.0, "b": 4.0}))
196 .await
197 .unwrap();
198 assert_eq!(out["result"], json!(2.5));
199 }
200
201 #[tokio::test]
202 async fn test_unknown_operation_returns_zero() {
203 let tool = Calculator;
204 let out = tool
205 .execute(json!({"operation": "modulo", "a": 10.0, "b": 3.0}))
206 .await
207 .unwrap();
208 assert_eq!(out["result"], json!(0.0));
209 }
210
211 #[tokio::test]
212 async fn test_missing_operation_defaults_to_add() {
213 let tool = Calculator;
214 let out = tool.execute(json!({"a": 2.0, "b": 3.0})).await.unwrap();
215 assert_eq!(out["result"], json!(5.0));
216 }
217
218 #[tokio::test]
219 async fn test_missing_operands_default_to_zero() {
220 let tool = Calculator;
221 let out = tool.execute(json!({"operation": "add"})).await.unwrap();
222 assert_eq!(out["result"], json!(0.0));
223 }
224
225 #[tokio::test]
226 async fn test_divide_by_zero_serializes_as_null() {
227 let tool = Calculator;
228 let out = tool
229 .execute(json!({"operation": "divide", "a": 1.0, "b": 0.0}))
230 .await
231 .unwrap();
232 assert!(out["result"].is_null());
234 }
235
236 #[test]
239 fn test_calculator_service_via_context() {
240 use crate::registry::ToolRegistry;
241 use crate::Tools;
242 use cordis::Context;
243 use std::sync::Arc;
244
245 let mut registry = ToolRegistry::new();
246 registry.register(Arc::new(CalculatorService));
247 let tools = Arc::new(Tools::new(Arc::new(registry)));
248 let ctx = Context::new_root();
249 ctx.provide_arc(Arc::clone(&tools));
250
251 let resolved = ctx.get::<Tools>().expect("Tools should be in context");
252 let tool = resolved
253 .resolve(&ctx, "calculator")
254 .expect("calculator should resolve");
255 assert_eq!(tool.name(), "calculator");
256
257 let list = resolved.list(&ctx);
258 assert!(list.iter().any(|d| d.name == "calculator"));
259 assert!(resolved.resolve(&ctx, "unknown").is_none());
260
261 let isolated = ctx.isolate::<Tools>("tenant:acme");
262 assert!(resolved.resolve(&isolated, "calculator").is_some());
263 assert!(resolved
264 .list(&isolated)
265 .iter()
266 .any(|d| d.name == "calculator"));
267 }
268
269 #[test]
270 fn test_calculator_via_tools_list_resolve() {
271 use crate::registry::ToolRegistry;
272 use crate::Tools;
273 use cordis::Context;
274 use std::sync::Arc;
275
276 let mut registry = ToolRegistry::new();
277 registry.register(Arc::new(CalculatorService));
278 let svc = Tools::new(Arc::new(registry));
279 let ctx = Context::new_root().isolate::<Tools>("tenant:acme");
280 let tool = svc.resolve(&ctx, "calculator").unwrap();
281 assert_eq!(tool.name(), "calculator");
282 assert_eq!(tool.description(), "Perform basic arithmetic operations");
283 let defs = svc.list(&ctx);
284 assert_eq!(defs.len(), 1);
285 assert_eq!(defs[0].name, "calculator");
286 }
287}