1use async_trait::async_trait;
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use crate::generate_schema;
7use ai_agents_core::{Tool, ToolResult, ToolSafetyMetadata};
8
9pub struct MathTool;
10
11impl MathTool {
12 pub fn new() -> Self {
13 Self
14 }
15}
16
17impl Default for MathTool {
18 fn default() -> Self {
19 Self::new()
20 }
21}
22
23#[derive(Debug, Deserialize, JsonSchema)]
24struct MathInput {
25 operation: String,
27 #[serde(default)]
29 values: Option<Vec<f64>>,
30 #[serde(default)]
32 value: Option<f64>,
33 #[serde(default)]
35 decimals: Option<i32>,
36 #[serde(default)]
38 min: Option<f64>,
39 #[serde(default)]
41 max: Option<f64>,
42 #[serde(default)]
44 base: Option<f64>,
45 #[serde(default)]
47 exponent: Option<f64>,
48 #[serde(default)]
50 total: Option<f64>,
51 #[serde(default)]
53 step: Option<f64>,
54}
55
56#[derive(Debug, Serialize, Deserialize)]
57struct StatOutput {
58 result: f64,
59 count: usize,
60}
61
62#[derive(Debug, Serialize, Deserialize)]
63struct StdevOutput {
64 stdev: f64,
65 variance: f64,
66 mean: f64,
67 count: usize,
68}
69
70#[derive(Debug, Serialize, Deserialize)]
71struct ModeOutput {
72 mode: Vec<f64>,
73 frequency: usize,
74}
75
76#[derive(Debug, Serialize, Deserialize)]
77struct SingleOutput {
78 result: f64,
79}
80
81#[derive(Debug, Serialize, Deserialize)]
82struct ClampOutput {
83 result: f64,
84 clamped: bool,
85}
86
87#[derive(Debug, Serialize, Deserialize)]
88struct RangeOutput {
89 range: Vec<f64>,
90 count: usize,
91}
92
93#[derive(Debug, Serialize, Deserialize)]
94struct MinMaxOutput {
95 min: f64,
96 max: f64,
97 range: f64,
98}
99
100#[async_trait]
101impl Tool for MathTool {
102 fn id(&self) -> &str {
103 "math"
104 }
105
106 fn name(&self) -> &str {
107 "Advanced Math"
108 }
109
110 fn description(&self) -> &str {
111 "Advanced math operations: mean (average), median, mode, stdev (standard deviation), variance, sum, min, max, minmax (both), abs, round, floor, ceil, clamp, percentage, sqrt, pow, log, log10, range, count."
112 }
113
114 fn input_schema(&self) -> Value {
115 generate_schema::<MathInput>()
116 }
117
118 fn safety_metadata(&self) -> ToolSafetyMetadata {
119 ToolSafetyMetadata::compute()
120 }
121
122 async fn execute(&self, args: Value, _ctx: ai_agents_core::ToolExecutionContext) -> ToolResult {
123 let input: MathInput = match serde_json::from_value(args) {
124 Ok(input) => input,
125 Err(e) => return ToolResult::error(format!("Invalid input: {}", e)),
126 };
127
128 match input.operation.to_lowercase().as_str() {
129 "mean" | "average" | "avg" => self.handle_mean(&input),
130 "median" => self.handle_median(&input),
131 "mode" => self.handle_mode(&input),
132 "stdev" | "std" => self.handle_stdev(&input),
133 "variance" | "var" => self.handle_variance(&input),
134 "sum" => self.handle_sum(&input),
135 "min" => self.handle_min(&input),
136 "max" => self.handle_max(&input),
137 "minmax" => self.handle_minmax(&input),
138 "abs" => self.handle_abs(&input),
139 "round" => self.handle_round(&input),
140 "floor" => self.handle_floor(&input),
141 "ceil" => self.handle_ceil(&input),
142 "clamp" => self.handle_clamp(&input),
143 "percentage" | "percent" => self.handle_percentage(&input),
144 "sqrt" => self.handle_sqrt(&input),
145 "pow" | "power" => self.handle_pow(&input),
146 "log" => self.handle_log(&input),
147 "log10" => self.handle_log10(&input),
148 "range" => self.handle_range(&input),
149 "count" => self.handle_count(&input),
150 _ => ToolResult::error(format!(
151 "Unknown operation: {}. Valid: mean, median, mode, stdev, variance, sum, min, max, minmax, abs, round, floor, ceil, clamp, percentage, sqrt, pow, log, log10, range, count",
152 input.operation
153 )),
154 }
155 }
156}
157
158impl MathTool {
159 fn get_values(&self, input: &MathInput) -> Result<Vec<f64>, ToolResult> {
160 input
161 .values
162 .clone()
163 .ok_or_else(|| ToolResult::error("'values' array is required"))
164 }
165
166 fn handle_mean(&self, input: &MathInput) -> ToolResult {
167 let values = match self.get_values(input) {
168 Ok(v) => v,
169 Err(e) => return e,
170 };
171 if values.is_empty() {
172 return ToolResult::error("values array cannot be empty");
173 }
174 let mean = values.iter().sum::<f64>() / values.len() as f64;
175 let output = StatOutput {
176 result: mean,
177 count: values.len(),
178 };
179 self.to_result(&output)
180 }
181
182 fn handle_median(&self, input: &MathInput) -> ToolResult {
183 let values = match self.get_values(input) {
184 Ok(v) => v,
185 Err(e) => return e,
186 };
187 if values.is_empty() {
188 return ToolResult::error("values array cannot be empty");
189 }
190
191 let mut sorted = values.clone();
192 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
193
194 let mid = sorted.len() / 2;
195 let median = if sorted.len() % 2 == 0 {
196 (sorted[mid - 1] + sorted[mid]) / 2.0
197 } else {
198 sorted[mid]
199 };
200
201 let output = StatOutput {
202 result: median,
203 count: values.len(),
204 };
205 self.to_result(&output)
206 }
207
208 fn handle_mode(&self, input: &MathInput) -> ToolResult {
209 let values = match self.get_values(input) {
210 Ok(v) => v,
211 Err(e) => return e,
212 };
213 if values.is_empty() {
214 return ToolResult::error("values array cannot be empty");
215 }
216
217 use std::collections::HashMap;
218 let mut counts: HashMap<String, usize> = HashMap::new();
219
220 for v in &values {
221 let key = format!("{:.10}", v);
222 *counts.entry(key).or_insert(0) += 1;
223 }
224
225 let max_count = *counts.values().max().unwrap_or(&0);
226 let modes: Vec<f64> = counts
227 .iter()
228 .filter(|&(_, &c)| c == max_count)
229 .filter_map(|(k, _)| k.parse().ok())
230 .collect();
231
232 let output = ModeOutput {
233 mode: modes,
234 frequency: max_count,
235 };
236 self.to_result(&output)
237 }
238
239 fn handle_stdev(&self, input: &MathInput) -> ToolResult {
240 let values = match self.get_values(input) {
241 Ok(v) => v,
242 Err(e) => return e,
243 };
244 if values.len() < 2 {
245 return ToolResult::error("stdev requires at least 2 values");
246 }
247
248 let mean = values.iter().sum::<f64>() / values.len() as f64;
249 let variance =
250 values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / (values.len() - 1) as f64;
251 let stdev = variance.sqrt();
252
253 let output = StdevOutput {
254 stdev,
255 variance,
256 mean,
257 count: values.len(),
258 };
259 self.to_result(&output)
260 }
261
262 fn handle_variance(&self, input: &MathInput) -> ToolResult {
263 let values = match self.get_values(input) {
264 Ok(v) => v,
265 Err(e) => return e,
266 };
267 if values.len() < 2 {
268 return ToolResult::error("variance requires at least 2 values");
269 }
270
271 let mean = values.iter().sum::<f64>() / values.len() as f64;
272 let variance =
273 values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / (values.len() - 1) as f64;
274
275 let output = StatOutput {
276 result: variance,
277 count: values.len(),
278 };
279 self.to_result(&output)
280 }
281
282 fn handle_sum(&self, input: &MathInput) -> ToolResult {
283 let values = match self.get_values(input) {
284 Ok(v) => v,
285 Err(e) => return e,
286 };
287
288 let sum: f64 = values.iter().sum();
289 let output = StatOutput {
290 result: sum,
291 count: values.len(),
292 };
293 self.to_result(&output)
294 }
295
296 fn handle_min(&self, input: &MathInput) -> ToolResult {
297 let values = match self.get_values(input) {
298 Ok(v) => v,
299 Err(e) => return e,
300 };
301 if values.is_empty() {
302 return ToolResult::error("values array cannot be empty");
303 }
304
305 let min = values.iter().cloned().fold(f64::INFINITY, f64::min);
306 let output = SingleOutput { result: min };
307 self.to_result(&output)
308 }
309
310 fn handle_max(&self, input: &MathInput) -> ToolResult {
311 let values = match self.get_values(input) {
312 Ok(v) => v,
313 Err(e) => return e,
314 };
315 if values.is_empty() {
316 return ToolResult::error("values array cannot be empty");
317 }
318
319 let max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
320 let output = SingleOutput { result: max };
321 self.to_result(&output)
322 }
323
324 fn handle_minmax(&self, input: &MathInput) -> ToolResult {
325 let values = match self.get_values(input) {
326 Ok(v) => v,
327 Err(e) => return e,
328 };
329 if values.is_empty() {
330 return ToolResult::error("values array cannot be empty");
331 }
332
333 let min = values.iter().cloned().fold(f64::INFINITY, f64::min);
334 let max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
335 let output = MinMaxOutput {
336 min,
337 max,
338 range: max - min,
339 };
340 self.to_result(&output)
341 }
342
343 fn handle_abs(&self, input: &MathInput) -> ToolResult {
344 let value = match input.value {
345 Some(v) => v,
346 None => return ToolResult::error("'value' is required for abs operation"),
347 };
348 let output = SingleOutput {
349 result: value.abs(),
350 };
351 self.to_result(&output)
352 }
353
354 fn handle_round(&self, input: &MathInput) -> ToolResult {
355 let value = match input.value {
356 Some(v) => v,
357 None => return ToolResult::error("'value' is required for round operation"),
358 };
359 let decimals = input.decimals.unwrap_or(0);
360 let multiplier = 10_f64.powi(decimals);
361 let rounded = (value * multiplier).round() / multiplier;
362 let output = SingleOutput { result: rounded };
363 self.to_result(&output)
364 }
365
366 fn handle_floor(&self, input: &MathInput) -> ToolResult {
367 let value = match input.value {
368 Some(v) => v,
369 None => return ToolResult::error("'value' is required for floor operation"),
370 };
371 let output = SingleOutput {
372 result: value.floor(),
373 };
374 self.to_result(&output)
375 }
376
377 fn handle_ceil(&self, input: &MathInput) -> ToolResult {
378 let value = match input.value {
379 Some(v) => v,
380 None => return ToolResult::error("'value' is required for ceil operation"),
381 };
382 let output = SingleOutput {
383 result: value.ceil(),
384 };
385 self.to_result(&output)
386 }
387
388 fn handle_clamp(&self, input: &MathInput) -> ToolResult {
389 let value = match input.value {
390 Some(v) => v,
391 None => return ToolResult::error("'value' is required for clamp operation"),
392 };
393 let min = match input.min {
394 Some(m) => m,
395 None => return ToolResult::error("'min' is required for clamp operation"),
396 };
397 let max = match input.max {
398 Some(m) => m,
399 None => return ToolResult::error("'max' is required for clamp operation"),
400 };
401
402 let clamped_value = value.max(min).min(max);
403 let output = ClampOutput {
404 result: clamped_value,
405 clamped: value != clamped_value,
406 };
407 self.to_result(&output)
408 }
409
410 fn handle_percentage(&self, input: &MathInput) -> ToolResult {
411 let value = match input.value {
412 Some(v) => v,
413 None => return ToolResult::error("'value' is required for percentage operation"),
414 };
415 let total = match input.total {
416 Some(t) => t,
417 None => return ToolResult::error("'total' is required for percentage operation"),
418 };
419 if total == 0.0 {
420 return ToolResult::error("total cannot be zero");
421 }
422
423 let percentage = (value / total) * 100.0;
424 let output = SingleOutput { result: percentage };
425 self.to_result(&output)
426 }
427
428 fn handle_sqrt(&self, input: &MathInput) -> ToolResult {
429 let value = match input.value {
430 Some(v) => v,
431 None => return ToolResult::error("'value' is required for sqrt operation"),
432 };
433 if value < 0.0 {
434 return ToolResult::error("cannot calculate sqrt of negative number");
435 }
436 let output = SingleOutput {
437 result: value.sqrt(),
438 };
439 self.to_result(&output)
440 }
441
442 fn handle_pow(&self, input: &MathInput) -> ToolResult {
443 let base = input.value.or(input.base);
444 let base = match base {
445 Some(b) => b,
446 None => return ToolResult::error("'value' or 'base' is required for pow operation"),
447 };
448 let exponent = match input.exponent {
449 Some(e) => e,
450 None => return ToolResult::error("'exponent' is required for pow operation"),
451 };
452 let output = SingleOutput {
453 result: base.powf(exponent),
454 };
455 self.to_result(&output)
456 }
457
458 fn handle_log(&self, input: &MathInput) -> ToolResult {
459 let value = match input.value {
460 Some(v) => v,
461 None => return ToolResult::error("'value' is required for log operation"),
462 };
463 if value <= 0.0 {
464 return ToolResult::error("cannot calculate log of non-positive number");
465 }
466 let result = match input.base {
467 Some(b) if b > 0.0 && b != 1.0 => value.log(b),
468 Some(_) => return ToolResult::error("log base must be positive and not equal to 1"),
469 None => value.ln(),
470 };
471 let output = SingleOutput { result };
472 self.to_result(&output)
473 }
474
475 fn handle_log10(&self, input: &MathInput) -> ToolResult {
476 let value = match input.value {
477 Some(v) => v,
478 None => return ToolResult::error("'value' is required for log10 operation"),
479 };
480 if value <= 0.0 {
481 return ToolResult::error("cannot calculate log of non-positive number");
482 }
483 let output = SingleOutput {
484 result: value.log10(),
485 };
486 self.to_result(&output)
487 }
488
489 fn handle_range(&self, input: &MathInput) -> ToolResult {
490 let min = input.min.unwrap_or(0.0);
491 let max = match input.max {
492 Some(m) => m,
493 None => return ToolResult::error("'max' is required for range operation"),
494 };
495 let step = input.step.unwrap_or(1.0);
496
497 if step == 0.0 {
498 return ToolResult::error("step cannot be zero");
499 }
500 if (max > min && step < 0.0) || (max < min && step > 0.0) {
501 return ToolResult::error("step direction doesn't match min/max range");
502 }
503
504 let mut values = Vec::new();
505 let mut current = min;
506
507 if step > 0.0 {
508 while current < max {
509 values.push(current);
510 current += step;
511 }
512 } else {
513 while current > max {
514 values.push(current);
515 current += step;
516 }
517 }
518
519 let output = RangeOutput {
520 count: values.len(),
521 range: values,
522 };
523 self.to_result(&output)
524 }
525
526 fn handle_count(&self, input: &MathInput) -> ToolResult {
527 let values = match self.get_values(input) {
528 Ok(v) => v,
529 Err(e) => return e,
530 };
531 let output = StatOutput {
532 result: values.len() as f64,
533 count: values.len(),
534 };
535 self.to_result(&output)
536 }
537
538 fn to_result<T: Serialize>(&self, output: &T) -> ToolResult {
539 match serde_json::to_string(output) {
540 Ok(json) => ToolResult::ok(json),
541 Err(e) => ToolResult::error(format!("Serialization error: {}", e)),
542 }
543 }
544}
545
546#[cfg(test)]
547mod tests {
548 use super::*;
549
550 #[tokio::test]
551 async fn test_mean() {
552 let tool = MathTool::new();
553 let result = tool
554 .execute(
555 serde_json::json!({
556 "operation": "mean",
557 "values": [1, 2, 3, 4, 5]
558 }),
559 ai_agents_core::ToolExecutionContext::test("test"),
560 )
561 .await;
562 assert!(result.success);
563 let output: StatOutput = serde_json::from_str(&result.output).unwrap();
564 assert!((output.result - 3.0).abs() < f64::EPSILON);
565 }
566
567 #[tokio::test]
568 async fn test_median_odd() {
569 let tool = MathTool::new();
570 let result = tool
571 .execute(
572 serde_json::json!({
573 "operation": "median",
574 "values": [1, 3, 2, 5, 4]
575 }),
576 ai_agents_core::ToolExecutionContext::test("test"),
577 )
578 .await;
579 assert!(result.success);
580 let output: StatOutput = serde_json::from_str(&result.output).unwrap();
581 assert!((output.result - 3.0).abs() < f64::EPSILON);
582 }
583
584 #[tokio::test]
585 async fn test_median_even() {
586 let tool = MathTool::new();
587 let result = tool
588 .execute(
589 serde_json::json!({
590 "operation": "median",
591 "values": [1, 2, 3, 4]
592 }),
593 ai_agents_core::ToolExecutionContext::test("test"),
594 )
595 .await;
596 assert!(result.success);
597 let output: StatOutput = serde_json::from_str(&result.output).unwrap();
598 assert!((output.result - 2.5).abs() < f64::EPSILON);
599 }
600
601 #[tokio::test]
602 async fn test_stdev() {
603 let tool = MathTool::new();
604 let result = tool
605 .execute(
606 serde_json::json!({
607 "operation": "stdev",
608 "values": [2, 4, 4, 4, 5, 5, 7, 9]
609 }),
610 ai_agents_core::ToolExecutionContext::test("test"),
611 )
612 .await;
613 assert!(result.success);
614 let output: StdevOutput = serde_json::from_str(&result.output).unwrap();
615 assert!((output.stdev - 2.138).abs() < 0.01);
616 }
617
618 #[tokio::test]
619 async fn test_sum() {
620 let tool = MathTool::new();
621 let result = tool
622 .execute(
623 serde_json::json!({
624 "operation": "sum",
625 "values": [1, 2, 3, 4, 5]
626 }),
627 ai_agents_core::ToolExecutionContext::test("test"),
628 )
629 .await;
630 assert!(result.success);
631 let output: StatOutput = serde_json::from_str(&result.output).unwrap();
632 assert!((output.result - 15.0).abs() < f64::EPSILON);
633 }
634
635 #[tokio::test]
636 async fn test_minmax() {
637 let tool = MathTool::new();
638 let result = tool
639 .execute(
640 serde_json::json!({
641 "operation": "minmax",
642 "values": [3, 1, 4, 1, 5, 9, 2, 6]
643 }),
644 ai_agents_core::ToolExecutionContext::test("test"),
645 )
646 .await;
647 assert!(result.success);
648 let output: MinMaxOutput = serde_json::from_str(&result.output).unwrap();
649 assert!((output.min - 1.0).abs() < f64::EPSILON);
650 assert!((output.max - 9.0).abs() < f64::EPSILON);
651 assert!((output.range - 8.0).abs() < f64::EPSILON);
652 }
653
654 #[tokio::test]
655 async fn test_round() {
656 let tool = MathTool::new();
657 let result = tool
658 .execute(
659 serde_json::json!({
660 "operation": "round",
661 "value": 3.14159,
662 "decimals": 2
663 }),
664 ai_agents_core::ToolExecutionContext::test("test"),
665 )
666 .await;
667 assert!(result.success);
668 let output: SingleOutput = serde_json::from_str(&result.output).unwrap();
669 assert!((output.result - 3.14).abs() < f64::EPSILON);
670 }
671
672 #[tokio::test]
673 async fn test_clamp() {
674 let tool = MathTool::new();
675 let result = tool
676 .execute(
677 serde_json::json!({
678 "operation": "clamp",
679 "value": 15,
680 "min": 0,
681 "max": 10
682 }),
683 ai_agents_core::ToolExecutionContext::test("test"),
684 )
685 .await;
686 assert!(result.success);
687 let output: ClampOutput = serde_json::from_str(&result.output).unwrap();
688 assert!((output.result - 10.0).abs() < f64::EPSILON);
689 assert!(output.clamped);
690 }
691
692 #[tokio::test]
693 async fn test_percentage() {
694 let tool = MathTool::new();
695 let result = tool
696 .execute(
697 serde_json::json!({
698 "operation": "percentage",
699 "value": 25,
700 "total": 100
701 }),
702 ai_agents_core::ToolExecutionContext::test("test"),
703 )
704 .await;
705 assert!(result.success);
706 let output: SingleOutput = serde_json::from_str(&result.output).unwrap();
707 assert!((output.result - 25.0).abs() < f64::EPSILON);
708 }
709
710 #[tokio::test]
711 async fn test_sqrt() {
712 let tool = MathTool::new();
713 let result = tool
714 .execute(
715 serde_json::json!({
716 "operation": "sqrt",
717 "value": 16
718 }),
719 ai_agents_core::ToolExecutionContext::test("test"),
720 )
721 .await;
722 assert!(result.success);
723 let output: SingleOutput = serde_json::from_str(&result.output).unwrap();
724 assert!((output.result - 4.0).abs() < f64::EPSILON);
725 }
726
727 #[tokio::test]
728 async fn test_pow() {
729 let tool = MathTool::new();
730 let result = tool
731 .execute(
732 serde_json::json!({
733 "operation": "pow",
734 "value": 2,
735 "exponent": 10
736 }),
737 ai_agents_core::ToolExecutionContext::test("test"),
738 )
739 .await;
740 assert!(result.success);
741 let output: SingleOutput = serde_json::from_str(&result.output).unwrap();
742 assert!((output.result - 1024.0).abs() < f64::EPSILON);
743 }
744
745 #[tokio::test]
746 async fn test_range() {
747 let tool = MathTool::new();
748 let result = tool
749 .execute(
750 serde_json::json!({
751 "operation": "range",
752 "min": 0,
753 "max": 5,
754 "step": 1
755 }),
756 ai_agents_core::ToolExecutionContext::test("test"),
757 )
758 .await;
759 assert!(result.success);
760 let output: RangeOutput = serde_json::from_str(&result.output).unwrap();
761 assert_eq!(output.range, vec![0.0, 1.0, 2.0, 3.0, 4.0]);
762 }
763
764 #[tokio::test]
765 async fn test_invalid_operation() {
766 let tool = MathTool::new();
767 let result = tool
768 .execute(
769 serde_json::json!({
770 "operation": "invalid"
771 }),
772 ai_agents_core::ToolExecutionContext::test("test"),
773 )
774 .await;
775 assert!(!result.success);
776 }
777
778 #[tokio::test]
779 async fn test_empty_values() {
780 let tool = MathTool::new();
781 let result = tool
782 .execute(
783 serde_json::json!({
784 "operation": "mean",
785 "values": []
786 }),
787 ai_agents_core::ToolExecutionContext::test("test"),
788 )
789 .await;
790 assert!(!result.success);
791 }
792}