feat: Round 5 - Memory Service, Tool Engine, Call Records, Thinking Logs
- Fix: Session history flash (race condition + WS guard) - Fix: Chat background overlay + sidebar transparency - Fix: IoT device control (Chinese action names, status field) - Feat: Independent memory-service (port 8091, 13 endpoints) - Feat: Independent tool-engine service (port 8092, 13 tools) - Feat: Tool call logs with paginated DevTools panel - Feat: Thinking log records with DevTools panel - Feat: Future development roadmap document - Chore: Updated .gitignore, go.work, DevTools config - Chore: 5-service health check, project review docs
This commit is contained in:
@@ -0,0 +1,342 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/yourname/cyrene-ai/tool-engine/internal/model"
|
||||
)
|
||||
|
||||
// CalculatorTool performs safe mathematical expression evaluation.
|
||||
type CalculatorTool struct{}
|
||||
|
||||
// NewCalculatorTool creates a calculator tool.
|
||||
func NewCalculatorTool() *CalculatorTool {
|
||||
return &CalculatorTool{}
|
||||
}
|
||||
|
||||
// Definition returns the tool definition for LLM function calling.
|
||||
func (t *CalculatorTool) Definition() model.ToolDefinition {
|
||||
return model.ToolDefinition{
|
||||
Name: "calculator",
|
||||
Description: "执行数学计算。用于精确计算数学表达式,支持四则运算、三角函数、对数、幂运算等。适用于LLM不擅长的复杂计算场景。",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"expression": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "数学表达式,如 \"2 + 3 * 4\"、\"sqrt(16) + sin(pi/2)\"。支持运算符: + - * / % ^。支持函数: sqrt, sin, cos, tan, abs, floor, ceil, round, log, ln, pow。支持常量: pi, e。",
|
||||
},
|
||||
},
|
||||
"required": []string{"expression"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Execute evaluates a mathematical expression.
|
||||
func (t *CalculatorTool) Execute(ctx context.Context, arguments map[string]interface{}) (*model.ToolResult, error) {
|
||||
expression, ok := arguments["expression"].(string)
|
||||
if !ok || strings.TrimSpace(expression) == "" {
|
||||
return &model.ToolResult{
|
||||
ID: "",
|
||||
Error: "缺少 expression 参数",
|
||||
}, nil
|
||||
}
|
||||
|
||||
result, err := evaluate(expression)
|
||||
if err != nil {
|
||||
return &model.ToolResult{
|
||||
ID: "",
|
||||
Error: fmt.Sprintf("计算错误: %v", err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &model.ToolResult{
|
||||
ID: "",
|
||||
Output: fmt.Sprintf("表达式: %s\n结果: %s", expression, formatResult(result)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func formatResult(v float64) string {
|
||||
if v == math.Trunc(v) && math.Abs(v) < 1e15 {
|
||||
return strconv.FormatInt(int64(v), 10)
|
||||
}
|
||||
return strconv.FormatFloat(v, 'g', -1, 64)
|
||||
}
|
||||
|
||||
type tokenKind int
|
||||
|
||||
const (
|
||||
tokNumber tokenKind = iota
|
||||
tokIdent
|
||||
tokOp
|
||||
tokLParen
|
||||
tokRParen
|
||||
tokComma
|
||||
tokEOF
|
||||
)
|
||||
|
||||
type token struct {
|
||||
kind tokenKind
|
||||
value string
|
||||
}
|
||||
|
||||
type lexer struct {
|
||||
input []rune
|
||||
pos int
|
||||
}
|
||||
|
||||
func newLexer(s string) *lexer {
|
||||
return &lexer{input: []rune(s), pos: 0}
|
||||
}
|
||||
|
||||
func (l *lexer) next() token {
|
||||
l.skipWhitespace()
|
||||
if l.pos >= len(l.input) {
|
||||
return token{kind: tokEOF}
|
||||
}
|
||||
|
||||
ch := l.input[l.pos]
|
||||
|
||||
if unicode.IsDigit(ch) || ch == '.' {
|
||||
start := l.pos
|
||||
hasDot := ch == '.'
|
||||
l.pos++
|
||||
for l.pos < len(l.input) && (unicode.IsDigit(l.input[l.pos]) || l.input[l.pos] == '.') {
|
||||
if l.input[l.pos] == '.' {
|
||||
if hasDot {
|
||||
break
|
||||
}
|
||||
hasDot = true
|
||||
}
|
||||
l.pos++
|
||||
}
|
||||
return token{kind: tokNumber, value: string(l.input[start:l.pos])}
|
||||
}
|
||||
|
||||
if unicode.IsLetter(ch) || ch == '_' {
|
||||
start := l.pos
|
||||
l.pos++
|
||||
for l.pos < len(l.input) && (unicode.IsLetter(l.input[l.pos]) || unicode.IsDigit(l.input[l.pos]) || l.input[l.pos] == '_') {
|
||||
l.pos++
|
||||
}
|
||||
return token{kind: tokIdent, value: string(l.input[start:l.pos])}
|
||||
}
|
||||
|
||||
switch ch {
|
||||
case '+', '-', '*', '/', '%', '^':
|
||||
l.pos++
|
||||
return token{kind: tokOp, value: string(ch)}
|
||||
case '(':
|
||||
l.pos++
|
||||
return token{kind: tokLParen}
|
||||
case ')':
|
||||
l.pos++
|
||||
return token{kind: tokRParen}
|
||||
case ',':
|
||||
l.pos++
|
||||
return token{kind: tokComma}
|
||||
}
|
||||
|
||||
return token{kind: tokEOF}
|
||||
}
|
||||
|
||||
func (l *lexer) skipWhitespace() {
|
||||
for l.pos < len(l.input) && unicode.IsSpace(l.input[l.pos]) {
|
||||
l.pos++
|
||||
}
|
||||
}
|
||||
|
||||
type parser struct {
|
||||
lex *lexer
|
||||
cur token
|
||||
peek token
|
||||
}
|
||||
|
||||
func newParser(lex *lexer) *parser {
|
||||
p := &parser{lex: lex}
|
||||
p.cur = lex.next()
|
||||
p.peek = lex.next()
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *parser) advance() {
|
||||
p.cur = p.peek
|
||||
p.peek = p.lex.next()
|
||||
}
|
||||
|
||||
func evaluate(expr string) (float64, error) {
|
||||
lex := newLexer(expr)
|
||||
par := newParser(lex)
|
||||
result, err := par.parseExpression()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if par.cur.kind != tokEOF {
|
||||
return 0, fmt.Errorf("表达式末尾存在意外字符")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (p *parser) parseExpression() (float64, error) {
|
||||
left, err := p.parseTerm()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
for p.cur.kind == tokOp && (p.cur.value == "+" || p.cur.value == "-") {
|
||||
op := p.cur.value
|
||||
p.advance()
|
||||
right, err := p.parseTerm()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if op == "+" {
|
||||
left += right
|
||||
} else {
|
||||
left -= right
|
||||
}
|
||||
}
|
||||
return left, nil
|
||||
}
|
||||
|
||||
func (p *parser) parseTerm() (float64, error) {
|
||||
left, err := p.parseUnary()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
for p.cur.kind == tokOp && (p.cur.value == "*" || p.cur.value == "/" || p.cur.value == "%" || p.cur.value == "^") {
|
||||
op := p.cur.value
|
||||
p.advance()
|
||||
right, err := p.parseUnary()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
switch op {
|
||||
case "*":
|
||||
left *= right
|
||||
case "/":
|
||||
if right == 0 {
|
||||
return 0, fmt.Errorf("除数不能为零")
|
||||
}
|
||||
left /= right
|
||||
case "%":
|
||||
left = math.Mod(left, right)
|
||||
case "^":
|
||||
left = math.Pow(left, right)
|
||||
}
|
||||
}
|
||||
return left, nil
|
||||
}
|
||||
|
||||
func (p *parser) parseUnary() (float64, error) {
|
||||
if p.cur.kind == tokOp && p.cur.value == "-" {
|
||||
p.advance()
|
||||
val, err := p.parseUnary()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return -val, nil
|
||||
}
|
||||
if p.cur.kind == tokOp && p.cur.value == "+" {
|
||||
p.advance()
|
||||
return p.parseUnary()
|
||||
}
|
||||
return p.parseAtom()
|
||||
}
|
||||
|
||||
func (p *parser) parseAtom() (float64, error) {
|
||||
switch p.cur.kind {
|
||||
case tokNumber:
|
||||
val, err := strconv.ParseFloat(p.cur.value, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("无效数字: %s", p.cur.value)
|
||||
}
|
||||
p.advance()
|
||||
return val, nil
|
||||
|
||||
case tokIdent:
|
||||
name := strings.ToLower(p.cur.value)
|
||||
p.advance()
|
||||
|
||||
switch name {
|
||||
case "pi":
|
||||
return math.Pi, nil
|
||||
case "e":
|
||||
return math.E, nil
|
||||
}
|
||||
|
||||
if p.cur.kind != tokLParen {
|
||||
return 0, fmt.Errorf("未知标识符: %s (如果是函数需要加括号)", name)
|
||||
}
|
||||
p.advance()
|
||||
|
||||
arg, err := p.parseExpression()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if p.cur.kind != tokRParen {
|
||||
return 0, fmt.Errorf("函数 %s 缺少右括号", name)
|
||||
}
|
||||
p.advance()
|
||||
|
||||
return applyFunc(name, arg)
|
||||
|
||||
case tokLParen:
|
||||
p.advance()
|
||||
val, err := p.parseExpression()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if p.cur.kind != tokRParen {
|
||||
return 0, fmt.Errorf("缺少右括号")
|
||||
}
|
||||
p.advance()
|
||||
return val, nil
|
||||
|
||||
default:
|
||||
return 0, fmt.Errorf("意外的 token: %v", p.cur.value)
|
||||
}
|
||||
}
|
||||
|
||||
func applyFunc(name string, arg float64) (float64, error) {
|
||||
switch name {
|
||||
case "sqrt":
|
||||
if arg < 0 {
|
||||
return 0, fmt.Errorf("sqrt 参数不能为负数")
|
||||
}
|
||||
return math.Sqrt(arg), nil
|
||||
case "sin":
|
||||
return math.Sin(arg), nil
|
||||
case "cos":
|
||||
return math.Cos(arg), nil
|
||||
case "tan":
|
||||
return math.Tan(arg), nil
|
||||
case "abs":
|
||||
return math.Abs(arg), nil
|
||||
case "floor":
|
||||
return math.Floor(arg), nil
|
||||
case "ceil":
|
||||
return math.Ceil(arg), nil
|
||||
case "round":
|
||||
return math.Round(arg), nil
|
||||
case "log":
|
||||
if arg <= 0 {
|
||||
return 0, fmt.Errorf("log 参数必须大于0")
|
||||
}
|
||||
return math.Log10(arg), nil
|
||||
case "ln":
|
||||
if arg <= 0 {
|
||||
return 0, fmt.Errorf("ln 参数必须大于0")
|
||||
}
|
||||
return math.Log(arg), nil
|
||||
case "pow":
|
||||
return 0, fmt.Errorf("pow 需要两个参数,请使用 ^ 运算符代替")
|
||||
default:
|
||||
return 0, fmt.Errorf("未知函数: %s", name)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user