385 lines
8.7 KiB
Go
385 lines
8.7 KiB
Go
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",
|
||
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.",
|
||
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' }
|
||
|