350 lines
8.0 KiB
Go
350 lines
8.0 KiB
Go
package calculator
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"math"
|
||
"os"
|
||
"path/filepath"
|
||
"strconv"
|
||
"strings"
|
||
"unicode"
|
||
|
||
agents "aichat/agenttool"
|
||
|
||
"github.com/volcengine/volcengine-go-sdk/service/arkruntime/model"
|
||
"gopkg.in/yaml.v3"
|
||
)
|
||
|
||
const (
|
||
ToolName = "calculator"
|
||
ActivationPrompt = "执行简单、确定性的数学四则运算。当用户询问加减乘除、括号表达式、小数运算或需要准确计算表达式结果时,应直接调用此工具;不用于代数推导、方程求解、统计分析或复杂数学证明。"
|
||
)
|
||
|
||
type Config struct {
|
||
Enabled bool `yaml:"enabled" json:"enabled"`
|
||
ActivationPrompt string `yaml:"activation_prompt" json:"activation_prompt"`
|
||
}
|
||
|
||
type ToolArgs struct {
|
||
Expression string `json:"expression"`
|
||
Reason string `json:"reason"`
|
||
}
|
||
|
||
type LoadedTool struct {
|
||
cfg *Config
|
||
}
|
||
|
||
func NewLoadedTool(cfg *Config) *LoadedTool {
|
||
if cfg == nil {
|
||
defaultCfg := defaultConfig()
|
||
cfg = &defaultCfg
|
||
}
|
||
return &LoadedTool{cfg: cfg}
|
||
}
|
||
|
||
func init() {
|
||
agents.Register(agents.Descriptor{Name: ToolName, Load: func(path string, options agents.LoadOptions) (agents.LoadedTool, error) {
|
||
cfg, err := LoadConfig(path)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return NewLoadedTool(cfg), nil
|
||
}})
|
||
}
|
||
|
||
func defaultConfig() Config {
|
||
return Config{Enabled: true, ActivationPrompt: ActivationPrompt}
|
||
}
|
||
|
||
func LoadConfig(path string) (*Config, error) {
|
||
if _, err := os.Stat(path); err != nil {
|
||
if !os.IsNotExist(err) {
|
||
return nil, fmt.Errorf("检查计算器工具配置失败: %w", err)
|
||
}
|
||
cfg := defaultConfig()
|
||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||
return nil, fmt.Errorf("创建计算器工具配置目录失败: %w", err)
|
||
}
|
||
data, err := yaml.Marshal(&cfg)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("生成计算器工具配置失败: %w", err)
|
||
}
|
||
if err := os.WriteFile(path, data, 0644); err != nil {
|
||
return nil, fmt.Errorf("写入计算器工具配置失败: %w", err)
|
||
}
|
||
}
|
||
|
||
data, err := os.ReadFile(path)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("读取计算器工具配置失败: %w", err)
|
||
}
|
||
var cfg Config
|
||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||
return nil, fmt.Errorf("解析计算器工具配置失败: %w", err)
|
||
}
|
||
if strings.TrimSpace(cfg.ActivationPrompt) == "" {
|
||
cfg.ActivationPrompt = ActivationPrompt
|
||
}
|
||
return &cfg, nil
|
||
}
|
||
|
||
func (t *LoadedTool) Name() string { return ToolName }
|
||
|
||
func (t *LoadedTool) Enabled() bool { return t != nil && t.cfg != nil && t.cfg.Enabled }
|
||
|
||
func (t *LoadedTool) ToolDefinition(description string) *model.Tool {
|
||
if strings.TrimSpace(description) == "" && t != nil && t.cfg != nil {
|
||
description = t.cfg.ActivationPrompt
|
||
}
|
||
return ToolDefinition(description)
|
||
}
|
||
|
||
func (t *LoadedTool) Execute(ctx context.Context, args string, runtime agents.Runtime) (string, error) {
|
||
result, err := ExecuteTool(args)
|
||
if err == nil && runtime.Emit != nil {
|
||
runtime.Emit(agents.Frame{Type: "trace", Tool: ToolName, Stage: "calculate", Status: "success", Message: "四则运算完成"})
|
||
}
|
||
return result, err
|
||
}
|
||
|
||
func (t *LoadedTool) RawState() any { return nil }
|
||
|
||
func ToolDefinition(description string) *model.Tool {
|
||
description = strings.TrimSpace(description)
|
||
if description == "" {
|
||
description = ActivationPrompt
|
||
}
|
||
return &model.Tool{
|
||
Type: model.ToolTypeFunction,
|
||
Function: &model.FunctionDefinition{
|
||
Name: ToolName,
|
||
Description: description,
|
||
Parameters: map[string]any{
|
||
"type": "object",
|
||
"properties": map[string]any{
|
||
"expression": map[string]any{
|
||
"type": "string",
|
||
"description": "要计算的四则运算表达式,例如 12.5*(3+4)/2。仅支持数字、+、-、*、/ 和括号。",
|
||
},
|
||
"reason": map[string]any{
|
||
"type": "string",
|
||
"description": "调用计算器工具的原因。",
|
||
},
|
||
},
|
||
"required": []string{"expression"},
|
||
},
|
||
},
|
||
}
|
||
}
|
||
|
||
func ExecuteTool(args string) (string, error) {
|
||
var parsed ToolArgs
|
||
if err := json.Unmarshal([]byte(strings.TrimSpace(args)), &parsed); err != nil {
|
||
return "", fmt.Errorf("解析计算器工具参数失败: %w", err)
|
||
}
|
||
expression := strings.TrimSpace(parsed.Expression)
|
||
if expression == "" {
|
||
return "", errors.New("计算表达式不能为空")
|
||
}
|
||
result, err := Evaluate(expression)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
return BuildResultContext(expression, result, parsed.Reason), nil
|
||
}
|
||
|
||
func BuildResultContext(expression string, result float64, routeReason string) string {
|
||
var b strings.Builder
|
||
b.WriteString("计算器工具结果。请优先使用这里的精确计算结果回答用户,不要重新心算。\n")
|
||
fmt.Fprintf(&b, "表达式: %s\n", strings.TrimSpace(expression))
|
||
fmt.Fprintf(&b, "结果: %s\n", FormatNumber(result))
|
||
if strings.TrimSpace(routeReason) != "" {
|
||
b.WriteString("调用原因: " + strings.TrimSpace(routeReason) + "\n")
|
||
}
|
||
return b.String()
|
||
}
|
||
|
||
func FormatNumber(value float64) string {
|
||
if math.IsInf(value, 0) || math.IsNaN(value) {
|
||
return fmt.Sprintf("%v", value)
|
||
}
|
||
return strconv.FormatFloat(value, 'f', -1, 64)
|
||
}
|
||
|
||
func Evaluate(expression string) (float64, error) {
|
||
parser := expressionParser{input: normalizeExpression(expression)}
|
||
value, err := parser.parseExpression()
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
parser.skipSpaces()
|
||
if !parser.atEnd() {
|
||
return 0, fmt.Errorf("表达式包含不支持的字符: %q", parser.peek())
|
||
}
|
||
if math.IsInf(value, 0) || math.IsNaN(value) {
|
||
return 0, errors.New("计算结果无效")
|
||
}
|
||
return value, nil
|
||
}
|
||
|
||
func normalizeExpression(expression string) string {
|
||
replacer := strings.NewReplacer(
|
||
"×", "*",
|
||
"*", "*",
|
||
"÷", "/",
|
||
"/", "/",
|
||
"(", "(",
|
||
")", ")",
|
||
"+", "+",
|
||
"-", "-",
|
||
",", ".",
|
||
)
|
||
return replacer.Replace(expression)
|
||
}
|
||
|
||
type expressionParser struct {
|
||
input string
|
||
pos int
|
||
}
|
||
|
||
func (p *expressionParser) parseExpression() (float64, error) {
|
||
value, err := p.parseTerm()
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
for {
|
||
p.skipSpaces()
|
||
switch p.peek() {
|
||
case '+':
|
||
p.pos++
|
||
rhs, err := p.parseTerm()
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
value += rhs
|
||
case '-':
|
||
p.pos++
|
||
rhs, err := p.parseTerm()
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
value -= rhs
|
||
default:
|
||
return value, nil
|
||
}
|
||
}
|
||
}
|
||
|
||
func (p *expressionParser) parseTerm() (float64, error) {
|
||
value, err := p.parseFactor()
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
for {
|
||
p.skipSpaces()
|
||
switch p.peek() {
|
||
case '*':
|
||
p.pos++
|
||
rhs, err := p.parseFactor()
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
value *= rhs
|
||
case '/':
|
||
p.pos++
|
||
rhs, err := p.parseFactor()
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
if rhs == 0 {
|
||
return 0, errors.New("除数不能为 0")
|
||
}
|
||
value /= rhs
|
||
default:
|
||
return value, nil
|
||
}
|
||
}
|
||
}
|
||
|
||
func (p *expressionParser) parseFactor() (float64, error) {
|
||
p.skipSpaces()
|
||
switch p.peek() {
|
||
case '+':
|
||
p.pos++
|
||
return p.parseFactor()
|
||
case '-':
|
||
p.pos++
|
||
value, err := p.parseFactor()
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
return -value, nil
|
||
case '(':
|
||
p.pos++
|
||
value, err := p.parseExpression()
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
p.skipSpaces()
|
||
if p.peek() != ')' {
|
||
return 0, errors.New("缺少右括号")
|
||
}
|
||
p.pos++
|
||
return value, nil
|
||
default:
|
||
return p.parseNumber()
|
||
}
|
||
}
|
||
|
||
func (p *expressionParser) parseNumber() (float64, error) {
|
||
p.skipSpaces()
|
||
start := p.pos
|
||
dotSeen := false
|
||
for !p.atEnd() {
|
||
r := p.peek()
|
||
if r == '.' {
|
||
if dotSeen {
|
||
break
|
||
}
|
||
dotSeen = true
|
||
p.pos++
|
||
continue
|
||
}
|
||
if !unicode.IsDigit(rune(r)) {
|
||
break
|
||
}
|
||
p.pos++
|
||
}
|
||
if start == p.pos {
|
||
if p.atEnd() {
|
||
return 0, errors.New("表达式不完整")
|
||
}
|
||
return 0, fmt.Errorf("期望数字,遇到 %q", p.peek())
|
||
}
|
||
value, err := strconv.ParseFloat(p.input[start:p.pos], 64)
|
||
if err != nil {
|
||
return 0, fmt.Errorf("解析数字失败: %w", err)
|
||
}
|
||
return value, nil
|
||
}
|
||
|
||
func (p *expressionParser) skipSpaces() {
|
||
for !p.atEnd() && unicode.IsSpace(rune(p.peek())) {
|
||
p.pos++
|
||
}
|
||
}
|
||
|
||
func (p *expressionParser) peek() byte {
|
||
if p.atEnd() {
|
||
return 0
|
||
}
|
||
return p.input[p.pos]
|
||
}
|
||
|
||
func (p *expressionParser) atEnd() bool {
|
||
return p.pos >= len(p.input)
|
||
}
|