Changeset 0.11.0 (#62)

This commit is contained in:
2026-02-25 13:29:15 +00:00
parent d2ec55b16d
commit c9d8e9457e
56 changed files with 5664 additions and 91 deletions

384
server/tools/calculator.go Normal file
View File

@@ -0,0 +1,384 @@
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' }