This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/tools/calculator.go
2026-02-25 23:56:27 +00:00

387 lines
8.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package tools
import (
"context"
"encoding/json"
"fmt"
"math"
"strconv"
"strings"
)
func init() {
Register(&CalculatorTool{})
}
// ═══════════════════════════════════════════
// calculator
// ═══════════════════════════════════════════
type CalculatorTool struct{}
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' }