From e31cece76ec8a31bad890f0e120e800b31b562a4 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 02:22:12 +0000 Subject: [PATCH] feat(tools): add calculator tool for math expressions - Adds a new tool in `pkg/tools/calculator.go` using `github.com/Knetic/govaluate` to securely evaluate mathematical expressions. - Updates `pkg/config/tools.go` to include the tool configuration under `PICOCLAW_TOOLS_CALCULATOR_`. - Registers the new tool conditionally in `pkg/agent/instance.go`. - Adds unit tests in `pkg/tools/calculator_test.go` to verify correct expression evaluation, error handling, and float resolution. Co-authored-by: hobbyistlabs-coder <267281733+hobbyistlabs-coder@users.noreply.github.com> --- go.mod | 1 + pkg/agent/instance.go | 4 +++ pkg/config/tools.go | 3 ++ pkg/tools/calculator.go | 58 ++++++++++++++++++++++++++++++ pkg/tools/calculator_test.go | 70 ++++++++++++++++++++++++++++++++++++ 5 files changed, 136 insertions(+) create mode 100644 pkg/tools/calculator.go create mode 100644 pkg/tools/calculator_test.go diff --git a/go.mod b/go.mod index 055bc6948..bab564939 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module jane go 1.25.7 require ( + github.com/Knetic/govaluate v3.0.0+incompatible github.com/adhocore/gronx v1.19.6 github.com/alpacahq/alpaca-trade-api-go/v3 v3.9.1 github.com/anthropics/anthropic-sdk-go v1.22.1 diff --git a/pkg/agent/instance.go b/pkg/agent/instance.go index 590b18d10..0bfc1a241 100644 --- a/pkg/agent/instance.go +++ b/pkg/agent/instance.go @@ -105,6 +105,10 @@ func NewAgentInstance( } } + if cfg.Tools.IsToolEnabled("calculator") { + toolsRegistry.Register(tools.NewCalculatorTool()) + } + sessionsDir := filepath.Join(workspace, "sessions") sessions := initSessionStore(sessionsDir) diff --git a/pkg/config/tools.go b/pkg/config/tools.go index 1e746b1d3..7283f0cf6 100644 --- a/pkg/config/tools.go +++ b/pkg/config/tools.go @@ -134,6 +134,7 @@ type ToolsConfig struct { WriteFile ToolConfig `json:"write_file" envPrefix:"PICOCLAW_TOOLS_WRITE_FILE_"` BrowserAction ToolConfig `json:"browser_action" envPrefix:"PICOCLAW_TOOLS_BROWSER_ACTION_"` GoEval ToolConfig `json:"go_eval" envPrefix:"PICOCLAW_TOOLS_GO_EVAL_"` + Calculator ToolConfig `json:"calculator" envPrefix:"PICOCLAW_TOOLS_CALCULATOR_"` } type SearchCacheConfig struct { @@ -235,6 +236,8 @@ func (t *ToolsConfig) IsToolEnabled(name string) bool { return t.MCP.Enabled case "mcp2cli": return t.MCP2Cli.Enabled + case "calculator": + return t.Calculator.Enabled default: return true } diff --git a/pkg/tools/calculator.go b/pkg/tools/calculator.go new file mode 100644 index 000000000..1ccf880d8 --- /dev/null +++ b/pkg/tools/calculator.go @@ -0,0 +1,58 @@ +package tools + +import ( + "context" + "fmt" + + "github.com/Knetic/govaluate" +) + +// CalculatorTool evaluates mathematical expressions. +type CalculatorTool struct{} + +// NewCalculatorTool creates a new CalculatorTool. +func NewCalculatorTool() *CalculatorTool { + return &CalculatorTool{} +} + +func (t *CalculatorTool) Name() string { + return "calculator" +} + +func (t *CalculatorTool) Description() string { + return "Evaluates mathematical expressions. Input should be a mathematical expression as a string." +} + +func (t *CalculatorTool) Parameters() map[string]any { + return map[string]any{ + "type": "object", + "properties": map[string]any{ + "expression": map[string]any{ + "type": "string", + "description": "The mathematical expression to evaluate (e.g., '2 + 2', '10 * (5 - 3)').", + }, + }, + "required": []string{"expression"}, + } +} + +func (t *CalculatorTool) Execute(ctx context.Context, args map[string]any) *ToolResult { + exprStr, ok := args["expression"].(string) + if !ok || exprStr == "" { + return ErrorResult("expression argument is required and must be a string") + } + + expression, err := govaluate.NewEvaluableExpression(exprStr) + if err != nil { + return ErrorResult(fmt.Sprintf("invalid expression: %v", err)) + } + + result, err := expression.Evaluate(nil) + if err != nil { + return ErrorResult(fmt.Sprintf("evaluation failed: %v", err)) + } + + // Format result to handle floats properly if needed + resStr := fmt.Sprintf("%v", result) + return UserResult(resStr) +} diff --git a/pkg/tools/calculator_test.go b/pkg/tools/calculator_test.go new file mode 100644 index 000000000..571d89503 --- /dev/null +++ b/pkg/tools/calculator_test.go @@ -0,0 +1,70 @@ +package tools + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCalculatorTool_Execute(t *testing.T) { + calc := NewCalculatorTool() + + tests := []struct { + name string + expression string + want string + wantError bool + }{ + { + name: "simple addition", + expression: "2 + 2", + want: "4", + }, + { + name: "complex math", + expression: "10 * (5 - 3)", + want: "20", + }, + { + name: "floating point result", + expression: "10 / 4", + want: "2.5", + }, + { + name: "missing expression", + expression: "", + wantError: true, + }, + { + name: "invalid expression", + expression: "2 + * 2", + wantError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + args := map[string]any{} + if tt.expression != "" { + args["expression"] = tt.expression + } + + result := calc.Execute(context.Background(), args) + + if tt.wantError { + assert.True(t, result.IsError) + } else { + assert.False(t, result.IsError) + assert.Equal(t, tt.want, result.ForLLM) + } + }) + } +} + +func TestCalculatorTool_Info(t *testing.T) { + calc := NewCalculatorTool() + assert.Equal(t, "calculator", calc.Name()) + assert.NotEmpty(t, calc.Description()) + assert.NotNil(t, calc.Parameters()) +}