package tools import ( "context" "encoding/json" "fmt" "math" "strconv" "strings" ) func init() { Register(&CalculatorTool{}) } // ═══════════════════════════════════════════ // calculator // ═══════════════════════════════════════════ type CalculatorTool struct{ BaseTool } func (t *CalculatorTool) Definition() ToolDef { return ToolDef{ Name: "calculator", DisplayName: "Calculator", Description: "Evaluate mathematical expressions with precision. Supports +, -, *, /, ^ (power), % (modulo), parentheses, and functions: sqrt, abs, ceil, floor, round, log, log2, log10, sin, cos, tan, pi, e. Use this for any arithmetic, unit conversions, percentages, or calculations instead of doing mental math.", Category: "utilities", Parameters: JSONSchema(map[string]interface{}{ "expression": Prop("string", "Mathematical expression to evaluate, e.g. '(100 * 1.08) ^ 5' or 'sqrt(144) + log10(1000)'"), }, []string{"expression"}), } } func (t *CalculatorTool) Execute(ctx context.Context, execCtx ExecutionContext, argsJSON string) (string, error) { var args struct { Expression string `json:"expression"` } if err := json.Unmarshal([]byte(argsJSON), &args); err != nil { return "", fmt.Errorf("invalid arguments: %w", err) } if args.Expression == "" { return "", fmt.Errorf("expression is required") } // Normalize: replace ^ with ** for power, then rewrite for Go parser expr := normalizeExpr(args.Expression) result, err := evalExpr(expr) if err != nil { return "", fmt.Errorf("evaluation error: %w", err) } // Format result nicely var display string if result == math.Trunc(result) && !math.IsInf(result, 0) && !math.IsNaN(result) { display = strconv.FormatFloat(result, 'f', 0, 64) } else { display = strconv.FormatFloat(result, 'g', 15, 64) } resp := map[string]interface{}{ "expression": args.Expression, "result": result, "display": display, } b, _ := json.Marshal(resp) return string(b), nil } // ── Expression Normalization ──────────────── func normalizeExpr(s string) string { // Replace common aliases s = strings.ReplaceAll(s, "×", "*") s = strings.ReplaceAll(s, "÷", "/") s = strings.ReplaceAll(s, "**", "^") return s } // ── Recursive Descent Evaluator ───────────── // Supports: +, -, *, /, ^, %, unary -, parentheses, function calls, constants type calcParser struct { input string pos int } func evalExpr(input string) (float64, error) { p := &calcParser{input: strings.TrimSpace(input)} result, err := p.parseExpr() if err != nil { return 0, err } p.skipSpaces() if p.pos < len(p.input) { return 0, fmt.Errorf("unexpected character at position %d: %q", p.pos, string(p.input[p.pos])) } return result, nil } func (p *calcParser) parseExpr() (float64, error) { return p.parseAddSub() } func (p *calcParser) parseAddSub() (float64, error) { left, err := p.parseMulDiv() if err != nil { return 0, err } for { p.skipSpaces() if p.pos >= len(p.input) { break } op := p.input[p.pos] if op != '+' && op != '-' { break } p.pos++ right, err := p.parseMulDiv() if err != nil { return 0, err } if op == '+' { left += right } else { left -= right } } return left, nil } func (p *calcParser) parseMulDiv() (float64, error) { left, err := p.parsePower() if err != nil { return 0, err } for { p.skipSpaces() if p.pos >= len(p.input) { break } op := p.input[p.pos] if op != '*' && op != '/' && op != '%' { break } p.pos++ right, err := p.parsePower() if err != nil { return 0, err } switch op { case '*': left *= right case '/': if right == 0 { return 0, fmt.Errorf("division by zero") } left /= right case '%': if right == 0 { return 0, fmt.Errorf("modulo by zero") } left = math.Mod(left, right) } } return left, nil } func (p *calcParser) parsePower() (float64, error) { base, err := p.parseUnary() if err != nil { return 0, err } p.skipSpaces() if p.pos < len(p.input) && p.input[p.pos] == '^' { p.pos++ // Right-associative: 2^3^2 = 2^(3^2) exp, err := p.parsePower() if err != nil { return 0, err } return math.Pow(base, exp), nil } return base, nil } func (p *calcParser) parseUnary() (float64, error) { p.skipSpaces() if p.pos < len(p.input) && p.input[p.pos] == '-' { p.pos++ val, err := p.parseUnary() if err != nil { return 0, err } return -val, nil } if p.pos < len(p.input) && p.input[p.pos] == '+' { p.pos++ return p.parseUnary() } return p.parsePrimary() } func (p *calcParser) parsePrimary() (float64, error) { p.skipSpaces() if p.pos >= len(p.input) { return 0, fmt.Errorf("unexpected end of expression") } // Parenthesized expression if p.input[p.pos] == '(' { p.pos++ val, err := p.parseExpr() if err != nil { return 0, err } p.skipSpaces() if p.pos >= len(p.input) || p.input[p.pos] != ')' { return 0, fmt.Errorf("missing closing parenthesis") } p.pos++ return val, nil } // Identifier (function or constant) if isAlpha(p.input[p.pos]) { name := p.parseIdent() return p.evalIdentifier(name) } // Number return p.parseNumber() } func (p *calcParser) parseIdent() string { start := p.pos for p.pos < len(p.input) && (isAlpha(p.input[p.pos]) || isDigit(p.input[p.pos])) { p.pos++ } return strings.ToLower(p.input[start:p.pos]) } func (p *calcParser) evalIdentifier(name string) (float64, error) { // Constants switch name { case "pi": return math.Pi, nil case "e": return math.E, nil case "inf": return math.Inf(1), nil } // Functions (require parenthesized argument) p.skipSpaces() if p.pos >= len(p.input) || p.input[p.pos] != '(' { return 0, fmt.Errorf("unknown constant %q (did you mean %s(x)?)", name, name) } p.pos++ // consume '(' arg, err := p.parseExpr() if err != nil { return 0, err } // Check for second argument (e.g. pow(2, 3)) p.skipSpaces() var arg2 float64 hasArg2 := false if p.pos < len(p.input) && p.input[p.pos] == ',' { p.pos++ arg2, err = p.parseExpr() if err != nil { return 0, err } hasArg2 = true } p.skipSpaces() if p.pos >= len(p.input) || p.input[p.pos] != ')' { return 0, fmt.Errorf("missing closing parenthesis for %s()", name) } p.pos++ switch name { case "sqrt": return math.Sqrt(arg), nil case "abs": return math.Abs(arg), nil case "ceil": return math.Ceil(arg), nil case "floor": return math.Floor(arg), nil case "round": return math.Round(arg), nil case "log", "ln": return math.Log(arg), nil case "log2": return math.Log2(arg), nil case "log10": return math.Log10(arg), nil case "sin": return math.Sin(arg), nil case "cos": return math.Cos(arg), nil case "tan": return math.Tan(arg), nil case "asin": return math.Asin(arg), nil case "acos": return math.Acos(arg), nil case "atan": return math.Atan(arg), nil case "exp": return math.Exp(arg), nil case "pow": if !hasArg2 { return 0, fmt.Errorf("pow() requires two arguments: pow(base, exponent)") } return math.Pow(arg, arg2), nil case "max": if !hasArg2 { return 0, fmt.Errorf("max() requires two arguments") } return math.Max(arg, arg2), nil case "min": if !hasArg2 { return 0, fmt.Errorf("min() requires two arguments") } return math.Min(arg, arg2), nil default: return 0, fmt.Errorf("unknown function %q", name) } } func (p *calcParser) parseNumber() (float64, error) { start := p.pos if p.pos < len(p.input) && p.input[p.pos] == '.' { p.pos++ } if p.pos >= len(p.input) || !isDigit(p.input[p.pos]) { if p.pos > start { // lone dot return 0, fmt.Errorf("invalid number at position %d", start) } return 0, fmt.Errorf("expected number at position %d, got %q", p.pos, string(p.input[p.pos])) } for p.pos < len(p.input) && isDigit(p.input[p.pos]) { p.pos++ } if p.pos < len(p.input) && p.input[p.pos] == '.' { p.pos++ for p.pos < len(p.input) && isDigit(p.input[p.pos]) { p.pos++ } } // Scientific notation if p.pos < len(p.input) && (p.input[p.pos] == 'e' || p.input[p.pos] == 'E') { p.pos++ if p.pos < len(p.input) && (p.input[p.pos] == '+' || p.input[p.pos] == '-') { p.pos++ } for p.pos < len(p.input) && isDigit(p.input[p.pos]) { p.pos++ } } val, err := strconv.ParseFloat(p.input[start:p.pos], 64) if err != nil { return 0, fmt.Errorf("invalid number %q", p.input[start:p.pos]) } return val, nil } func (p *calcParser) skipSpaces() { for p.pos < len(p.input) && p.input[p.pos] == ' ' { p.pos++ } } func isAlpha(b byte) bool { return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == '_' } func isDigit(b byte) bool { return b >= '0' && b <= '9' }