Skip to main content

lc_tools/
calculator.rs

1// lc-tools/src/calculator.rs
2//! Calculator tool
3//!
4//! A math expression calculator backed by the crate-internal
5//! [`crate::expr_eval`] evaluator (a dependency-free port of the `meval`
6//! expression language).
7
8use async_trait::async_trait;
9use lc_core::tools::{BaseTool, Tool, ToolError};
10use schemars::JsonSchema;
11use serde::{Deserialize, Serialize};
12
13/// Calculator input
14#[derive(Debug, Deserialize, JsonSchema)]
15pub struct CalculatorInput {
16    /// Math expression (e.g., "2 + 3", "sqrt(16)", "3.14 * 10", "2 + 3 * 4")
17    pub expression: String,
18}
19
20/// Calculator output
21#[derive(Debug, Serialize)]
22pub struct CalculatorOutput {
23    /// Calculation result
24    pub result: f64,
25
26    /// Original expression
27    pub expression: String,
28}
29
30/// Calculator tool
31///
32/// Evaluates math expressions. Supports: basic arithmetic (+, -, *, /, %),
33/// power (^), functions (sin, cos, tan, sqrt, log, exp, abs, …), and
34/// constants (pi, e).
35pub struct Calculator;
36
37impl Calculator {
38    pub fn new() -> Self {
39        Self
40    }
41}
42
43impl Default for Calculator {
44    fn default() -> Self {
45        Self::new()
46    }
47}
48
49/// Implement Tool trait (type-safe version)
50#[async_trait]
51impl Tool for Calculator {
52    type Input = CalculatorInput;
53    type Output = CalculatorOutput;
54
55    async fn invoke(&self, input: Self::Input) -> Result<Self::Output, ToolError> {
56        let result = Self::evaluate_expression(&input.expression)?;
57
58        Ok(CalculatorOutput {
59            result,
60            expression: input.expression,
61        })
62    }
63}
64
65/// Implement BaseTool trait (string version, for Agent)
66#[async_trait]
67impl BaseTool for Calculator {
68    fn name(&self) -> &str {
69        "calculator"
70    }
71
72    fn description(&self) -> &str {
73        "Calculate math expressions. Supports basic arithmetic, power, trig functions, sqrt, log, exp, abs, and constants (pi, e).
74
75Examples:
76- '2 + 3' -> 5
77- '2 + 3 * 4' -> 14
78- 'sqrt(16)' -> 4
79- '3.14 * 10' -> 31.4
80- 'sin(pi/2)' -> 1
81- '2^10' -> 1024
82- 'log(e)' -> 1
83
84Input format: JSON object with 'expression' field
85Example: {\"expression\": \"2 + 3\"}"
86    }
87
88    async fn run(&self, input: String) -> Result<String, ToolError> {
89        // Parse input
90        let parsed: CalculatorInput = serde_json::from_str(&input)
91            .map_err(|e| ToolError::InvalidInput(format!("JSON parse error: {}", e)))?;
92
93        // Execute calculation
94        let output = self.invoke(parsed).await?;
95
96        // Return result string
97        Ok(format!("{} = {}", output.expression, output.result))
98    }
99
100    fn args_schema(&self) -> Option<serde_json::Value> {
101        use schemars::schema_for;
102        serde_json::to_value(schema_for!(CalculatorInput)).ok()
103    }
104}
105
106impl Calculator {
107    /// Evaluate a math expression
108    fn evaluate_expression(expr: &str) -> Result<f64, ToolError> {
109        crate::expr_eval::eval(expr).map_err(|e| {
110            ToolError::ExecutionFailed(format!("Failed to evaluate expression '{}': {}", expr, e))
111        })
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn test_basic_addition() {
121        assert_eq!(Calculator::evaluate_expression("2 + 3").unwrap(), 5.0);
122    }
123
124    #[test]
125    fn test_operator_precedence() {
126        // 2 + 3 * 4 = 14 (not 20)
127        assert_eq!(Calculator::evaluate_expression("2 + 3 * 4").unwrap(), 14.0);
128    }
129
130    #[test]
131    fn test_chained_addition() {
132        assert_eq!(Calculator::evaluate_expression("1 + 2 + 3").unwrap(), 6.0);
133    }
134
135    #[test]
136    fn test_subtraction() {
137        assert_eq!(Calculator::evaluate_expression("10 - 3").unwrap(), 7.0);
138    }
139
140    #[test]
141    fn test_multiplication() {
142        let result = Calculator::evaluate_expression("3.14 * 10").unwrap();
143        assert!((result - 31.4).abs() < 1e-10);
144    }
145
146    #[test]
147    fn test_division() {
148        let result = Calculator::evaluate_expression("10 / 3").unwrap();
149        assert!((result - 3.3333333333333335).abs() < 1e-10);
150    }
151
152    #[test]
153    fn test_power() {
154        assert_eq!(Calculator::evaluate_expression("2^10").unwrap(), 1024.0);
155    }
156
157    #[test]
158    fn test_sqrt() {
159        assert_eq!(Calculator::evaluate_expression("sqrt(16)").unwrap(), 4.0);
160    }
161
162    #[test]
163    fn test_sin_pi() {
164        let result = Calculator::evaluate_expression("sin(pi/2)").unwrap();
165        assert!((result - 1.0).abs() < 1e-10);
166    }
167
168    #[test]
169    fn test_plain_number() {
170        assert_eq!(Calculator::evaluate_expression("42").unwrap(), 42.0);
171    }
172
173    #[test]
174    fn test_invalid_expression() {
175        assert!(Calculator::evaluate_expression("hello").is_err());
176    }
177
178    #[tokio::test]
179    async fn test_tool_run() {
180        let tool = Calculator::new();
181        let result = tool
182            .run(r#"{"expression": "2 + 3"}"#.to_string())
183            .await
184            .unwrap();
185        assert!(result.contains("5"));
186    }
187}