支持工具调用:三个AI角色配置、工具注册表与流式工具轮
This commit is contained in:
11 files changed
+491
-28
No files matched your search
@@ -0,0 +1,61 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
"github.com/expr-lang/expr"
|
||||
)
|
||||
|
||||
type CalculatorTool struct{}
|
||||
|
||||
func (CalculatorTool) Name() string { return "calculate" }
|
||||
func (CalculatorTool) Description() string {
|
||||
return "计算数学表达式,如 \"(12 + 3) * 4\"、\"2^10\"、\"sqrt(9)\""
|
||||
}
|
||||
func (CalculatorTool) Parameters() map[string]any {
|
||||
return map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"expression": map[string]any{"type": "string", "description": "要计算的数学表达式"},
|
||||
},
|
||||
"required": []string{"expression"},
|
||||
}
|
||||
}
|
||||
|
||||
func (CalculatorTool) Execute(args json.RawMessage) (string, error) {
|
||||
var p struct {
|
||||
Expression string `json:"expression"`
|
||||
}
|
||||
if err := json.Unmarshal(args, &p); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if p.Expression == "" {
|
||||
return "", fmt.Errorf("expression 不能为空")
|
||||
}
|
||||
out, err := expr.Eval(p.Expression, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
switch v := out.(type) {
|
||||
case float64:
|
||||
return strconv.FormatFloat(round(v), 'f', -1, 64), nil
|
||||
case int:
|
||||
return strconv.Itoa(v), nil
|
||||
case bool:
|
||||
return strconv.FormatBool(v), nil
|
||||
case string:
|
||||
return v, nil
|
||||
default:
|
||||
return fmt.Sprintf("%v", v), nil
|
||||
}
|
||||
}
|
||||
|
||||
func round(v float64) float64 {
|
||||
if math.IsInf(v, 0) || math.IsNaN(v) {
|
||||
return v
|
||||
}
|
||||
return math.Round(v*1e8) / 1e8
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand/v2"
|
||||
)
|
||||
|
||||
type RandomTool struct{}
|
||||
|
||||
func (RandomTool) Name() string { return "random_number" }
|
||||
func (RandomTool) Description() string {
|
||||
return "生成指定范围内的随机整数,默认 0 到 100(含端点)"
|
||||
}
|
||||
func (RandomTool) Parameters() map[string]any {
|
||||
return map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"min": map[string]any{"type": "integer", "description": "最小值,默认 0"},
|
||||
"max": map[string]any{"type": "integer", "description": "最大值,默认 100"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (RandomTool) Execute(args json.RawMessage) (string, error) {
|
||||
var p struct {
|
||||
Min *int `json:"min"`
|
||||
Max *int `json:"max"`
|
||||
}
|
||||
_ = json.Unmarshal(args, &p)
|
||||
min, max := 0, 100
|
||||
if p.Min != nil {
|
||||
min = *p.Min
|
||||
}
|
||||
if p.Max != nil {
|
||||
max = *p.Max
|
||||
}
|
||||
if max < min {
|
||||
return "", fmt.Errorf("max (%d) 不能小于 min (%d)", max, min)
|
||||
}
|
||||
return fmt.Sprintf("%d", rand.IntN(max-min+1)+min), nil
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TimeTool struct{}
|
||||
|
||||
func (TimeTool) Name() string { return "get_current_time" }
|
||||
func (TimeTool) Description() string {
|
||||
return "获取当前日期和时间,可选指定时区(如 Asia/Shanghai,默认本地时区)"
|
||||
}
|
||||
func (TimeTool) Parameters() map[string]any {
|
||||
return map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"timezone": map[string]any{"type": "string", "description": "IANA 时区名,如 Asia/Shanghai"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (TimeTool) Execute(args json.RawMessage) (string, error) {
|
||||
var p struct {
|
||||
Timezone string `json:"timezone"`
|
||||
}
|
||||
_ = json.Unmarshal(args, &p)
|
||||
loc := time.Local
|
||||
if p.Timezone != "" {
|
||||
l, err := time.LoadLocation(p.Timezone)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
loc = l
|
||||
}
|
||||
now := time.Now().In(loc)
|
||||
return now.Format("2006-01-02 15:04:05 Monday MST"), nil
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/openai/openai-go"
|
||||
"github.com/openai/openai-go/packages/param"
|
||||
"github.com/openai/openai-go/shared"
|
||||
)
|
||||
|
||||
type Tool interface {
|
||||
Name() string
|
||||
Description() string
|
||||
Parameters() map[string]any
|
||||
Execute(args json.RawMessage) (string, error)
|
||||
}
|
||||
|
||||
type Registry struct {
|
||||
tools map[string]Tool
|
||||
}
|
||||
|
||||
func NewRegistry(t ...Tool) *Registry {
|
||||
r := &Registry{tools: make(map[string]Tool)}
|
||||
for _, tool := range t {
|
||||
r.tools[tool.Name()] = tool
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Registry) Get(name string) (Tool, bool) {
|
||||
t, ok := r.tools[name]
|
||||
return t, ok
|
||||
}
|
||||
|
||||
func (r *Registry) ParamList() []openai.ChatCompletionToolParam {
|
||||
out := make([]openai.ChatCompletionToolParam, 0, len(r.tools))
|
||||
for _, t := range r.tools {
|
||||
out = append(out, openai.ChatCompletionToolParam{
|
||||
Function: shared.FunctionDefinitionParam{
|
||||
Name: t.Name(),
|
||||
Description: param.NewOpt(t.Description()),
|
||||
Parameters: shared.FunctionParameters(t.Parameters()),
|
||||
},
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *Registry) Execute(name string, args json.RawMessage) (string, error) {
|
||||
t, ok := r.tools[name]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("未知工具: %s", name)
|
||||
}
|
||||
return t.Execute(args)
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCalculator(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
expr string
|
||||
want string
|
||||
}{
|
||||
{"四则运算", "(12 + 3) * 4", "60"},
|
||||
{"幂运算", "2^10", "1024"},
|
||||
{"浮点", "7 / 2", "3.5"},
|
||||
{"小数精度", "1 / 3", "0.33333333"},
|
||||
{"布尔", "2 > 1", "true"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
args, _ := json.Marshal(map[string]string{"expression": c.expr})
|
||||
got, err := CalculatorTool{}.Execute(args)
|
||||
if err != nil {
|
||||
t.Fatalf("Execute 出错: %v", err)
|
||||
}
|
||||
if got != c.want {
|
||||
t.Errorf("Execute(%q) = %q, want %q", c.expr, got, c.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculatorInvalid(t *testing.T) {
|
||||
args, _ := json.Marshal(map[string]string{"expression": "1 +"})
|
||||
if _, err := (CalculatorTool{}).Execute(args); err == nil {
|
||||
t.Error("非法表达式应返回错误")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRandom(t *testing.T) {
|
||||
args, _ := json.Marshal(map[string]any{"min": 1, "max": 10})
|
||||
for i := 0; i < 100; i++ {
|
||||
out, err := RandomTool{}.Execute(args)
|
||||
if err != nil {
|
||||
t.Fatalf("Execute 出错: %v", err)
|
||||
}
|
||||
var v int
|
||||
if err := json.Unmarshal([]byte(out), &v); err != nil {
|
||||
t.Fatalf("结果 %q 解析失败: %v", out, err)
|
||||
}
|
||||
if v < 1 || v > 10 {
|
||||
t.Fatalf("结果 %q 不在 [1,10] 内", out)
|
||||
}
|
||||
}
|
||||
bad, _ := json.Marshal(map[string]any{"min": 10, "max": 1})
|
||||
if _, err := (RandomTool{}).Execute(bad); err == nil {
|
||||
t.Error("min>max 应返回错误")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimeTool(t *testing.T) {
|
||||
out, err := TimeTool{}.Execute(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Execute 出错: %v", err)
|
||||
}
|
||||
if !strings.Contains(out, "20") {
|
||||
t.Errorf("时间输出异常: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistry(t *testing.T) {
|
||||
r := NewRegistry(TimeTool{}, CalculatorTool{}, RandomTool{})
|
||||
if _, ok := r.Get("get_current_time"); !ok {
|
||||
t.Error("get_current_time 未注册")
|
||||
}
|
||||
if _, ok := r.Get("nonexistent"); ok {
|
||||
t.Error("未知工具不应存在")
|
||||
}
|
||||
if len(r.ParamList()) != 3 {
|
||||
t.Errorf("ParamList 数量 = %d, want 3", len(r.ParamList()))
|
||||
}
|
||||
if _, err := r.Execute("nonexistent", nil); err == nil {
|
||||
t.Error("执行未知工具应返回错误")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user