Skip to main content

lc_tools/
math.rs

1// lc-tools/src/math.rs
2//! Advanced math tool for agents.
3//!
4//! Provides complex mathematical operations including exponent, logarithm, trigonometry, etc.
5
6use async_trait::async_trait;
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9
10use lc_core::tools::{BaseTool, Tool, ToolError};
11
12/// Math tool input parameters.
13#[derive(Debug, Deserialize, JsonSchema)]
14pub struct MathInput {
15    /// Operation type: "power", "sqrt", "log", "ln", "sin", "cos", "tan", "abs", "factorial", "mod", "gcd", "lcm".
16    pub operation: String,
17
18    /// First value for calculation.
19    pub value: Option<f64>,
20
21    /// Second value for operations requiring two parameters (power, mod, gcd, lcm).
22    pub value2: Option<f64>,
23
24    /// Logarithm base for log operation (default: 10).
25    pub base: Option<f64>,
26}
27
28/// Math tool output result.
29#[derive(Debug, Serialize)]
30pub struct MathOutput {
31    /// Calculation result.
32    pub result: f64,
33
34    /// Operation type.
35    pub operation: String,
36
37    /// Additional details.
38    pub details: Option<String>,
39}
40
41/// Advanced math tool for agents.
42pub struct SimpleMathTool;
43
44impl SimpleMathTool {
45    /// Creates a new SimpleMathTool instance.
46    pub fn new() -> Self {
47        Self
48    }
49
50    fn power(&self, base: f64, exponent: f64) -> Result<MathOutput, ToolError> {
51        let result = base.powf(exponent);
52        Ok(MathOutput {
53            result,
54            operation: "power".to_string(),
55            details: Some(format!("{}^{} = {}", base, exponent, result)),
56        })
57    }
58
59    fn sqrt(&self, value: f64) -> Result<MathOutput, ToolError> {
60        if value < 0.0 {
61            return Err(ToolError::InvalidInput(
62                "square root requires a non-negative number".to_string(),
63            ));
64        }
65        let result = value.sqrt();
66        Ok(MathOutput {
67            result,
68            operation: "sqrt".to_string(),
69            details: Some(format!("√{} = {}", value, result)),
70        })
71    }
72
73    fn log(&self, value: f64, base: f64) -> Result<MathOutput, ToolError> {
74        if value <= 0.0 || base <= 0.0 || base == 1.0 {
75            return Err(ToolError::InvalidInput(
76                "logarithm requires positive values and a base other than 1".to_string(),
77            ));
78        }
79        let result = value.log(base);
80        Ok(MathOutput {
81            result,
82            operation: "log".to_string(),
83            details: Some(format!("log_{}({}) = {}", base, value, result)),
84        })
85    }
86
87    fn ln(&self, value: f64) -> Result<MathOutput, ToolError> {
88        if value <= 0.0 {
89            return Err(ToolError::InvalidInput(
90                "natural logarithm requires a positive number".to_string(),
91            ));
92        }
93        let result = value.ln();
94        Ok(MathOutput {
95            result,
96            operation: "ln".to_string(),
97            details: Some(format!("ln({}) = {}", value, result)),
98        })
99    }
100
101    fn sin(&self, value: f64) -> Result<MathOutput, ToolError> {
102        let result = value.sin();
103        Ok(MathOutput {
104            result,
105            operation: "sin".to_string(),
106            details: Some(format!("sin({}弧度) = {}", value, result)),
107        })
108    }
109
110    fn cos(&self, value: f64) -> Result<MathOutput, ToolError> {
111        let result = value.cos();
112        Ok(MathOutput {
113            result,
114            operation: "cos".to_string(),
115            details: Some(format!("cos({}弧度) = {}", value, result)),
116        })
117    }
118
119    fn tan(&self, value: f64) -> Result<MathOutput, ToolError> {
120        let result = value.tan();
121        Ok(MathOutput {
122            result,
123            operation: "tan".to_string(),
124            details: Some(format!("tan({}弧度) = {}", value, result)),
125        })
126    }
127
128    fn abs(&self, value: f64) -> Result<MathOutput, ToolError> {
129        let result = value.abs();
130        Ok(MathOutput {
131            result,
132            operation: "abs".to_string(),
133            details: Some(format!("|{}| = {}", value, result)),
134        })
135    }
136
137    fn factorial(&self, value: f64) -> Result<MathOutput, ToolError> {
138        if value < 0.0 {
139            return Err(ToolError::InvalidInput(
140                "factorial requires a non-negative integer".to_string(),
141            ));
142        }
143        if value.is_nan() {
144            return Err(ToolError::InvalidInput(
145                "factorial requires a valid number, NaN is not allowed".to_string(),
146            ));
147        }
148        if value != value.floor() {
149            return Err(ToolError::InvalidInput(
150                "factorial requires an integer, not a decimal".to_string(),
151            ));
152        }
153        let n = value as u64;
154        if n > 20 {
155            return Err(ToolError::InvalidInput(
156                "factorial value too large, maximum supported is 20".to_string(),
157            ));
158        }
159        let result = Self::compute_factorial(n);
160        Ok(MathOutput {
161            result: result as f64,
162            operation: "factorial".to_string(),
163            details: Some(format!("{}! = {}", n, result)),
164        })
165    }
166
167    // 无实例状态:关联函数而非方法(否则 stable clippy::only_used_in_recursion
168    // 会报 `&self` 只在递归调用中出现;1.85 的 clippy 没有此 lint)。
169    fn compute_factorial(n: u64) -> u64 {
170        if n == 0 || n == 1 {
171            1
172        } else {
173            n * Self::compute_factorial(n - 1)
174        }
175    }
176
177    fn mod_op(&self, a: f64, b: f64) -> Result<MathOutput, ToolError> {
178        if b == 0.0 {
179            return Err(ToolError::InvalidInput(
180                "modulo divisor must not be zero".to_string(),
181            ));
182        }
183        let result = a % b;
184        Ok(MathOutput {
185            result,
186            operation: "mod".to_string(),
187            details: Some(format!("{} mod {} = {}", a, b, result)),
188        })
189    }
190
191    fn gcd(&self, a: f64, b: f64) -> Result<MathOutput, ToolError> {
192        let a_int = a as i64;
193        let b_int = b as i64;
194
195        if a_int < 0 || b_int < 0 {
196            return Err(ToolError::InvalidInput(
197                "GCD operation requires positive integers".to_string(),
198            ));
199        }
200
201        let result = self.compute_gcd(a_int.abs(), b_int.abs());
202        Ok(MathOutput {
203            result: result as f64,
204            operation: "gcd".to_string(),
205            details: Some(format!("gcd({}, {}) = {}", a_int, b_int, result)),
206        })
207    }
208
209    // clippy::only_used_in_recursion 误报(stable 1.86+ 报,1.85 不报):
210    // 基准分支 `b == 0 => return a` 实际使用了参数 `a`,并非"只在递归中用到"。
211    #[allow(clippy::only_used_in_recursion)]
212    fn compute_gcd(&self, a: i64, b: i64) -> i64 {
213        if b == 0 {
214            a
215        } else {
216            self.compute_gcd(b, a % b)
217        }
218    }
219
220    fn lcm(&self, a: f64, b: f64) -> Result<MathOutput, ToolError> {
221        let a_int = a as i64;
222        let b_int = b as i64;
223
224        if a_int <= 0 || b_int <= 0 {
225            return Err(ToolError::InvalidInput(
226                "LCM operation requires positive integers".to_string(),
227            ));
228        }
229
230        let gcd = self.compute_gcd(a_int, b_int);
231        let result = (a_int / gcd)
232            .checked_mul(b_int)
233            .ok_or_else(|| ToolError::InvalidInput("LCM result overflows i64 range".to_string()))?;
234        Ok(MathOutput {
235            result: result as f64,
236            operation: "lcm".to_string(),
237            details: Some(format!("lcm({}, {}) = {}", a_int, b_int, result)),
238        })
239    }
240
241    fn pi(&self) -> MathOutput {
242        MathOutput {
243            result: std::f64::consts::PI,
244            operation: "pi".to_string(),
245            details: Some("π ≈ 3.141592653589793".to_string()),
246        }
247    }
248
249    fn e(&self) -> MathOutput {
250        MathOutput {
251            result: std::f64::consts::E,
252            operation: "e".to_string(),
253            details: Some("e ≈ 2.718281828459045".to_string()),
254        }
255    }
256}
257
258impl Default for SimpleMathTool {
259    fn default() -> Self {
260        Self::new()
261    }
262}
263
264#[async_trait]
265impl Tool for SimpleMathTool {
266    type Input = MathInput;
267    type Output = MathOutput;
268
269    async fn invoke(&self, input: Self::Input) -> Result<Self::Output, ToolError> {
270        match input.operation.as_str() {
271            "power" => {
272                let base = input.value.ok_or_else(||
273                    ToolError::InvalidInput("power operation requires a value parameter as the base".to_string()))?;
274                let exp = input.value2.ok_or_else(||
275                    ToolError::InvalidInput("power operation requires a value2 parameter as the exponent".to_string()))?;
276                self.power(base, exp)
277            }
278            "sqrt" => {
279                let value = input.value.ok_or_else(||
280                    ToolError::InvalidInput("sqrt operation requires a value parameter".to_string()))?;
281                self.sqrt(value)
282            }
283            "log" => {
284                let value = input.value.ok_or_else(||
285                    ToolError::InvalidInput("log operation requires a value parameter".to_string()))?;
286                let base = input.base.unwrap_or(10.0);
287                self.log(value, base)
288            }
289            "ln" => {
290                let value = input.value.ok_or_else(||
291                    ToolError::InvalidInput("ln operation requires a value parameter".to_string()))?;
292                self.ln(value)
293            }
294            "sin" => {
295                let value = input.value.ok_or_else(||
296                    ToolError::InvalidInput("sin operation requires a value parameter (radians)".to_string()))?;
297                self.sin(value)
298            }
299            "cos" => {
300                let value = input.value.ok_or_else(||
301                    ToolError::InvalidInput("cos operation requires a value parameter (radians)".to_string()))?;
302                self.cos(value)
303            }
304            "tan" => {
305                let value = input.value.ok_or_else(||
306                    ToolError::InvalidInput("tan operation requires a value parameter (radians)".to_string()))?;
307                self.tan(value)
308            }
309            "abs" => {
310                let value = input.value.ok_or_else(||
311                    ToolError::InvalidInput("abs operation requires a value parameter".to_string()))?;
312                self.abs(value)
313            }
314            "factorial" => {
315                let value = input.value.ok_or_else(||
316                    ToolError::InvalidInput("factorial operation requires a value parameter".to_string()))?;
317                self.factorial(value)
318            }
319            "mod" => {
320                let a = input.value.ok_or_else(||
321                    ToolError::InvalidInput("mod operation requires a value parameter".to_string()))?;
322                let b = input.value2.ok_or_else(||
323                    ToolError::InvalidInput("mod operation requires a value2 parameter".to_string()))?;
324                self.mod_op(a, b)
325            }
326            "gcd" => {
327                let a = input.value.ok_or_else(||
328                    ToolError::InvalidInput("gcd operation requires a value parameter".to_string()))?;
329                let b = input.value2.ok_or_else(||
330                    ToolError::InvalidInput("gcd operation requires a value2 parameter".to_string()))?;
331                self.gcd(a, b)
332            }
333            "lcm" => {
334                let a = input.value.ok_or_else(||
335                    ToolError::InvalidInput("lcm operation requires a value parameter".to_string()))?;
336                let b = input.value2.ok_or_else(||
337                    ToolError::InvalidInput("lcm operation requires a value2 parameter".to_string()))?;
338                self.lcm(a, b)
339            }
340            "pi" => Ok(self.pi()),
341            "e" => Ok(self.e()),
342            _ => Err(ToolError::InvalidInput(
343                format!("unsupported operation: {}, use: power, sqrt, log, ln, sin, cos, tan, abs, factorial, mod, gcd, lcm, pi, e", input.operation)
344            )),
345        }
346    }
347}
348
349#[async_trait]
350impl BaseTool for SimpleMathTool {
351    fn name(&self) -> &str {
352        "math"
353    }
354
355    fn description(&self) -> &str {
356        "高级数学工具。支持多种数学运算:
357
358操作类型:
359- power: 幂运算 (value^value2)
360- sqrt: 平方根
361- log: 对数(可指定底数,默认为10)
362- ln: 自然对数
363- sin, cos, tan: 三角函数(参数为弧度)
364- abs: 绝对值
365- factorial: 阶乘(最大支持20)
366- mod: 取模运算
367- gcd: 最大公约数
368- lcm: 最小公倍数
369- pi: 圆周率
370- e: 自然常数
371
372示例:
373- 幂运算: {\"operation\": \"power\", \"value\": 2, \"value2\": 10}
374- 平方根: {\"operation\": \"sqrt\", \"value\": 16}
375- 对数: {\"operation\": \"log\", \"value\": 100, \"base\": 10}
376- 三角函数: {\"operation\": \"sin\", \"value\": 1.5708}
377- GCD: {\"operation\": \"gcd\", \"value\": 12, \"value2\": 18}"
378    }
379
380    async fn run(&self, input: String) -> Result<String, ToolError> {
381        let parsed: MathInput = serde_json::from_str(&input)
382            .map_err(|e| ToolError::InvalidInput(format!("JSON parse failed: {}", e)))?;
383
384        let output = self.invoke(parsed).await?;
385
386        Ok(format!(
387            "结果: {}\n详细信息: {}",
388            output.result,
389            output.details.unwrap_or_default()
390        ))
391    }
392
393    fn args_schema(&self) -> Option<serde_json::Value> {
394        use schemars::schema_for;
395        serde_json::to_value(schema_for!(MathInput)).ok()
396    }
397}
398
399#[cfg(test)]
400mod tests {
401    use super::*;
402
403    #[tokio::test]
404    async fn test_math_power() {
405        let tool = SimpleMathTool::new();
406
407        let input = MathInput {
408            operation: "power".to_string(),
409            value: Some(2.0),
410            value2: Some(10.0),
411            base: None,
412        };
413
414        let result = tool.invoke(input).await.unwrap();
415        assert_eq!(result.result, 1024.0);
416    }
417
418    #[tokio::test]
419    async fn test_math_sqrt() {
420        let tool = SimpleMathTool::new();
421
422        let input = MathInput {
423            operation: "sqrt".to_string(),
424            value: Some(16.0),
425            value2: None,
426            base: None,
427        };
428
429        let result = tool.invoke(input).await.unwrap();
430        assert_eq!(result.result, 4.0);
431    }
432
433    #[tokio::test]
434    async fn test_math_log() {
435        let tool = SimpleMathTool::new();
436
437        let input = MathInput {
438            operation: "log".to_string(),
439            value: Some(100.0),
440            value2: None,
441            base: Some(10.0),
442        };
443
444        let result = tool.invoke(input).await.unwrap();
445        assert_eq!(result.result, 2.0);
446    }
447
448    #[tokio::test]
449    async fn test_math_ln() {
450        let tool = SimpleMathTool::new();
451
452        let input = MathInput {
453            operation: "ln".to_string(),
454            value: Some(std::f64::consts::E),
455            value2: None,
456            base: None,
457        };
458
459        let result = tool.invoke(input).await.unwrap();
460        assert!((result.result - 1.0).abs() < 0.0001);
461    }
462
463    #[tokio::test]
464    async fn test_math_sin() {
465        let tool = SimpleMathTool::new();
466
467        let input = MathInput {
468            operation: "sin".to_string(),
469            value: Some(std::f64::consts::PI / 2.0),
470            value2: None,
471            base: None,
472        };
473
474        let result = tool.invoke(input).await.unwrap();
475        assert!((result.result - 1.0).abs() < 0.0001);
476    }
477
478    #[tokio::test]
479    async fn test_math_factorial() {
480        let tool = SimpleMathTool::new();
481
482        let input = MathInput {
483            operation: "factorial".to_string(),
484            value: Some(5.0),
485            value2: None,
486            base: None,
487        };
488
489        let result = tool.invoke(input).await.unwrap();
490        assert_eq!(result.result, 120.0);
491    }
492
493    #[tokio::test]
494    async fn test_math_gcd() {
495        let tool = SimpleMathTool::new();
496
497        let input = MathInput {
498            operation: "gcd".to_string(),
499            value: Some(12.0),
500            value2: Some(18.0),
501            base: None,
502        };
503
504        let result = tool.invoke(input).await.unwrap();
505        assert_eq!(result.result, 6.0);
506    }
507
508    #[tokio::test]
509    async fn test_math_lcm() {
510        let tool = SimpleMathTool::new();
511
512        let input = MathInput {
513            operation: "lcm".to_string(),
514            value: Some(4.0),
515            value2: Some(6.0),
516            base: None,
517        };
518
519        let result = tool.invoke(input).await.unwrap();
520        assert_eq!(result.result, 12.0);
521    }
522
523    #[tokio::test]
524    async fn test_math_pi() {
525        let tool = SimpleMathTool::new();
526
527        let input = MathInput {
528            operation: "pi".to_string(),
529            value: None,
530            value2: None,
531            base: None,
532        };
533
534        let result = tool.invoke(input).await.unwrap();
535        assert!((result.result - std::f64::consts::PI).abs() < 0.0001);
536    }
537
538    #[tokio::test]
539    async fn test_math_abs() {
540        let tool = SimpleMathTool::new();
541
542        let input = MathInput {
543            operation: "abs".to_string(),
544            value: Some(-5.0),
545            value2: None,
546            base: None,
547        };
548
549        let result = tool.invoke(input).await.unwrap();
550        assert_eq!(result.result, 5.0);
551    }
552
553    #[tokio::test]
554    async fn test_math_sqrt_negative_error() {
555        let tool = SimpleMathTool::new();
556
557        let input = MathInput {
558            operation: "sqrt".to_string(),
559            value: Some(-4.0),
560            value2: None,
561            base: None,
562        };
563
564        let result = tool.invoke(input).await;
565        assert!(result.is_err());
566    }
567
568    #[tokio::test]
569    async fn test_math_factorial_overflow_error() {
570        let tool = SimpleMathTool::new();
571
572        let input = MathInput {
573            operation: "factorial".to_string(),
574            value: Some(25.0),
575            value2: None,
576            base: None,
577        };
578
579        let result = tool.invoke(input).await;
580        assert!(result.is_err());
581    }
582
583    #[tokio::test]
584    async fn test_math_base_tool_run() {
585        let tool = SimpleMathTool::new();
586
587        let input = "{\"operation\": \"power\", \"value\": 3, \"value2\": 4}".to_string();
588        let result = tool.run(input).await.unwrap();
589
590        assert!(result.contains("81"));
591        assert!(result.contains("3^4"));
592    }
593}