5c807d76a0
Extracted from Cyrene main repo (backend/pkg/plugins + backend/plugin-manager). Contains SDK interfaces (Plugin/Tool/HostAPI), 13 built-in plugins, ToolRegistry with call log ring buffer, and Plugin Manager REST API service. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
280 lines
5.9 KiB
Go
280 lines
5.9 KiB
Go
package calculator
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene-Plugins/sdk"
|
|
)
|
|
|
|
type CalculatorPlugin struct {
|
|
sdk.BasePlugin
|
|
}
|
|
|
|
func (p *CalculatorPlugin) Metadata() sdk.PluginMetadata {
|
|
return sdk.PluginMetadata{
|
|
Name: "calculator", DisplayName: "Calculator", Version: "1.0.0",
|
|
Description: "Safe mathematical expression evaluation with custom parser",
|
|
Category: "utility", Author: sdk.PluginAuthor{Name: "Cyrene Team"},
|
|
}
|
|
}
|
|
|
|
func (p *CalculatorPlugin) Tools() []sdk.Tool {
|
|
return []sdk.Tool{&CalculatorTool{}}
|
|
}
|
|
|
|
type CalculatorTool struct {
|
|
sdk.BaseTool
|
|
}
|
|
|
|
func (t *CalculatorTool) Definition() sdk.ToolDefinition {
|
|
return sdk.ToolDefinition{
|
|
ID: "calculator", Name: "calculator", DisplayName: "Calculator",
|
|
Description: "Execute mathematical calculations. Supports arithmetic, trig, logs, powers.",
|
|
Category: "utility", Complexity: sdk.ComplexitySimple,
|
|
Parameters: map[string]interface{}{
|
|
"type": "object", "properties": map[string]interface{}{"expression": map[string]interface{}{"type": "string"}},
|
|
"required": []string{"expression"},
|
|
},
|
|
}
|
|
}
|
|
|
|
func (t *CalculatorTool) Validate(args map[string]interface{}) error {
|
|
if _, ok := args["expression"]; !ok {
|
|
return fmt.Errorf("missing required parameter: expression")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (t *CalculatorTool) Execute(_ context.Context, args map[string]interface{}) (*sdk.ToolResult, error) {
|
|
expr, _ := args["expression"].(string)
|
|
result, err := evalExpression(expr)
|
|
if err != nil {
|
|
return &sdk.ToolResult{ToolName: "calculator", Success: false, Error: err.Error()}, nil
|
|
}
|
|
return &sdk.ToolResult{ToolName: "calculator", Success: true, Output: fmt.Sprintf("%v", result)}, nil
|
|
}
|
|
|
|
// Expression parser supporting +, -, *, /, %, ^, functions, constants.
|
|
type exprParser struct {
|
|
s string
|
|
pos int
|
|
}
|
|
|
|
func evalExpression(s string) (float64, error) {
|
|
p := &exprParser{s: strings.TrimSpace(s)}
|
|
result, err := p.parseAddSub()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if p.pos < len(p.s) {
|
|
return 0, fmt.Errorf("unexpected character at position %d: %c", p.pos, p.s[p.pos])
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (p *exprParser) peek() byte {
|
|
if p.pos < len(p.s) {
|
|
return p.s[p.pos]
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (p *exprParser) skipSpaces() {
|
|
for p.pos < len(p.s) && p.s[p.pos] == ' ' {
|
|
p.pos++
|
|
}
|
|
}
|
|
|
|
func (p *exprParser) parseAddSub() (float64, error) {
|
|
left, err := p.parseMulDiv()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
for {
|
|
p.skipSpaces()
|
|
op := p.peek()
|
|
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 *exprParser) parseMulDiv() (float64, error) {
|
|
left, err := p.parsePower()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
for {
|
|
p.skipSpaces()
|
|
op := p.peek()
|
|
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 '%':
|
|
left = math.Mod(left, right)
|
|
}
|
|
}
|
|
return left, nil
|
|
}
|
|
|
|
func (p *exprParser) parsePower() (float64, error) {
|
|
left, err := p.parseUnary()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
p.skipSpaces()
|
|
if p.peek() == '^' {
|
|
p.pos++
|
|
right, err := p.parseUnary()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return math.Pow(left, right), nil
|
|
}
|
|
return left, nil
|
|
}
|
|
|
|
func (p *exprParser) parseUnary() (float64, error) {
|
|
p.skipSpaces()
|
|
if p.peek() == '-' {
|
|
p.pos++
|
|
val, err := p.parseAtom()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return -val, nil
|
|
}
|
|
if p.peek() == '+' {
|
|
p.pos++
|
|
}
|
|
return p.parseAtom()
|
|
}
|
|
|
|
func (p *exprParser) parseAtom() (float64, error) {
|
|
p.skipSpaces()
|
|
if p.peek() == '(' {
|
|
p.pos++
|
|
result, err := p.parseAddSub()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
p.skipSpaces()
|
|
if p.peek() != ')' {
|
|
return 0, fmt.Errorf("missing closing parenthesis")
|
|
}
|
|
p.pos++
|
|
return result, nil
|
|
}
|
|
if p.peek() == 0 {
|
|
return 0, fmt.Errorf("unexpected end of expression")
|
|
}
|
|
if unicode.IsDigit(rune(p.peek())) || p.peek() == '.' {
|
|
return p.parseNumber()
|
|
}
|
|
return p.parseFuncOrConst()
|
|
}
|
|
|
|
func (p *exprParser) parseNumber() (float64, error) {
|
|
start := p.pos
|
|
for p.pos < len(p.s) && (unicode.IsDigit(rune(p.s[p.pos])) || p.s[p.pos] == '.') {
|
|
p.pos++
|
|
}
|
|
return strconv.ParseFloat(p.s[start:p.pos], 64)
|
|
}
|
|
|
|
func (p *exprParser) parseFuncOrConst() (float64, error) {
|
|
start := p.pos
|
|
for p.pos < len(p.s) && (unicode.IsLetter(rune(p.s[p.pos])) || p.s[p.pos] == '_') {
|
|
p.pos++
|
|
}
|
|
name := p.s[start:p.pos]
|
|
p.skipSpaces()
|
|
|
|
switch name {
|
|
case "pi":
|
|
return math.Pi, nil
|
|
case "e":
|
|
return math.E, nil
|
|
case "sqrt", "sin", "cos", "tan", "abs", "floor", "ceil", "round", "log", "ln":
|
|
if p.peek() != '(' {
|
|
return 0, fmt.Errorf("expected '(' after function %s", name)
|
|
}
|
|
p.pos++
|
|
arg, err := p.parseAddSub()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if p.peek() != ')' {
|
|
return 0, fmt.Errorf("missing ')' after function argument")
|
|
}
|
|
p.pos++
|
|
return applyFunc(name, arg)
|
|
default:
|
|
return 0, fmt.Errorf("unknown function or constant: %s", name)
|
|
}
|
|
}
|
|
|
|
func applyFunc(name string, x float64) (float64, error) {
|
|
switch name {
|
|
case "sqrt":
|
|
if x < 0 {
|
|
return 0, fmt.Errorf("square root of negative number")
|
|
}
|
|
return math.Sqrt(x), nil
|
|
case "sin":
|
|
return math.Sin(x), nil
|
|
case "cos":
|
|
return math.Cos(x), nil
|
|
case "tan":
|
|
return math.Tan(x), nil
|
|
case "abs":
|
|
return math.Abs(x), nil
|
|
case "floor":
|
|
return math.Floor(x), nil
|
|
case "ceil":
|
|
return math.Ceil(x), nil
|
|
case "round":
|
|
return math.Round(x), nil
|
|
case "log":
|
|
if x <= 0 {
|
|
return 0, fmt.Errorf("log of non-positive number")
|
|
}
|
|
return math.Log10(x), nil
|
|
case "ln":
|
|
if x <= 0 {
|
|
return 0, fmt.Errorf("ln of non-positive number")
|
|
}
|
|
return math.Log(x), nil
|
|
}
|
|
return 0, fmt.Errorf("unknown function: %s", name)
|
|
}
|