// Package calc is an example skill giving any agent exact arithmetic: a // calculate tool evaluating +, -, *, /, %, ^, parentheses, and unary minus // over floats — because models guess at arithmetic and tools don't. package calc import ( "context" "encoding/json" "fmt" "math" "strconv" "strings" "gitea.stevedudenhoeffer.com/steve/majordomo/llm" "gitea.stevedudenhoeffer.com/steve/majordomo/skill" ) // New builds the calc skill. func New() *skill.Skill { tool := llm.Tool{ Name: "calculate", Description: "Evaluate an arithmetic expression exactly. Supports + - * / % ^ (power), parentheses, and unary minus. Use for ANY arithmetic instead of computing in your head.", Parameters: json.RawMessage(`{ "type": "object", "properties": { "expression": {"type": "string", "description": "e.g. (2 + 3) * 4 / 1.5"} }, "required": ["expression"] }`), Handler: func(_ context.Context, args json.RawMessage) (any, error) { var p struct { Expression string `json:"expression"` } if err := json.Unmarshal(args, &p); err != nil { return nil, fmt.Errorf("bad arguments: %w", err) } v, err := Eval(p.Expression) if err != nil { return nil, err } return map[string]any{"expression": p.Expression, "result": v}, nil }, } return skill.New("calc", skill.WithInstructions("You have a calculate tool. Use it for any arithmetic instead of estimating."), skill.WithTools(tool), ) } // Eval evaluates an arithmetic expression with a small recursive-descent // parser (no dependencies, no reflection, no eval). // // Grammar: // // expr := term (("+"|"-") term)* // term := unary (("*"|"/"|"%") unary)* // unary := "-" unary | power // power := atom ("^" unary)? # right-associative // atom := number | "(" expr ")" func Eval(input string) (float64, error) { p := &parser{src: input} v, err := p.expr() if err != nil { return 0, err } p.skipSpace() if p.pos < len(p.src) { return 0, fmt.Errorf("unexpected %q at position %d", p.src[p.pos], p.pos) } if math.IsInf(v, 0) || math.IsNaN(v) { return 0, fmt.Errorf("result of %q is not a finite number", input) } return v, nil } type parser struct { src string pos int } func (p *parser) skipSpace() { for p.pos < len(p.src) && (p.src[p.pos] == ' ' || p.src[p.pos] == '\t') { p.pos++ } } func (p *parser) peek() byte { p.skipSpace() if p.pos >= len(p.src) { return 0 } return p.src[p.pos] } func (p *parser) expr() (float64, error) { v, err := p.term() if err != nil { return 0, err } for { switch p.peek() { case '+': p.pos++ r, err := p.term() if err != nil { return 0, err } v += r case '-': p.pos++ r, err := p.term() if err != nil { return 0, err } v -= r default: return v, nil } } } func (p *parser) term() (float64, error) { v, err := p.unary() if err != nil { return 0, err } for { switch p.peek() { case '*': p.pos++ r, err := p.unary() if err != nil { return 0, err } v *= r case '/': p.pos++ r, err := p.unary() if err != nil { return 0, err } if r == 0 { return 0, fmt.Errorf("division by zero") } v /= r case '%': p.pos++ r, err := p.unary() if err != nil { return 0, err } if r == 0 { return 0, fmt.Errorf("modulo by zero") } v = math.Mod(v, r) default: return v, nil } } } func (p *parser) unary() (float64, error) { if p.peek() == '-' { p.pos++ v, err := p.unary() if err != nil { return 0, err } return -v, nil } return p.power() } func (p *parser) power() (float64, error) { base, err := p.atom() if err != nil { return 0, err } if p.peek() == '^' { p.pos++ exp, err := p.unary() // right-associative: 2^3^2 = 2^(3^2) if err != nil { return 0, err } return math.Pow(base, exp), nil } return base, nil } func (p *parser) atom() (float64, error) { switch c := p.peek(); { case c == '(': p.pos++ v, err := p.expr() if err != nil { return 0, err } if p.peek() != ')' { return 0, fmt.Errorf("missing closing parenthesis at position %d", p.pos) } p.pos++ return v, nil case c >= '0' && c <= '9' || c == '.': start := p.pos for p.pos < len(p.src) && (p.src[p.pos] >= '0' && p.src[p.pos] <= '9' || p.src[p.pos] == '.' || p.src[p.pos] == 'e' || p.src[p.pos] == 'E' || ((p.src[p.pos] == '+' || p.src[p.pos] == '-') && p.pos > start && (p.src[p.pos-1] == 'e' || p.src[p.pos-1] == 'E'))) { p.pos++ } v, err := strconv.ParseFloat(strings.TrimSpace(p.src[start:p.pos]), 64) if err != nil { return 0, fmt.Errorf("bad number %q at position %d", p.src[start:p.pos], start) } return v, nil case c == 0: return 0, fmt.Errorf("unexpected end of expression") default: return 0, fmt.Errorf("unexpected %q at position %d", c, p.pos) } }