Changeset 0.11.0 (#62)
This commit is contained in:
384
server/tools/calculator.go
Normal file
384
server/tools/calculator.go
Normal 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
64
server/tools/datetime.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user