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' }

64
server/tools/datetime.go Normal file
View File

@@ -0,0 +1,64 @@
package tools
import (
"context"
"encoding/json"
"fmt"
"time"
)
func init() {
Register(&DateTimeTool{})
}
// ═══════════════════════════════════════════
// datetime
// ═══════════════════════════════════════════
type DateTimeTool struct{}
func (t *DateTimeTool) Definition() ToolDef {
return ToolDef{
Name: "datetime",
Description: "Get the current date, time, timezone, and day of week. Use this whenever you need to know the current date or time, calculate days between dates, or answer questions about what day it is. Do NOT guess dates — always call this tool.",
Parameters: JSONSchema(map[string]interface{}{
"timezone": Prop("string", "IANA timezone name, e.g. 'America/New_York', 'UTC', 'Europe/London'. Defaults to UTC."),
}, nil),
}
}
func (t *DateTimeTool) Execute(ctx context.Context, execCtx ExecutionContext, argsJSON string) (string, error) {
var args struct {
Timezone string `json:"timezone"`
}
if argsJSON != "" {
_ = json.Unmarshal([]byte(argsJSON), &args)
}
loc := time.UTC
if args.Timezone != "" {
parsed, err := time.LoadLocation(args.Timezone)
if err != nil {
return "", fmt.Errorf("invalid timezone %q: %w", args.Timezone, err)
}
loc = parsed
}
now := time.Now().In(loc)
year, week := now.ISOWeek()
result := map[string]interface{}{
"datetime": now.Format(time.RFC3339),
"date": now.Format("2006-01-02"),
"time": now.Format("15:04:05"),
"day": now.Weekday().String(),
"timezone": loc.String(),
"unix": now.Unix(),
"iso_week": fmt.Sprintf("%d-W%02d", year, week),
"day_of_year": now.YearDay(),
}
b, _ := json.Marshal(result)
return string(b), nil
}