内置工具接入独立配置:enabled 开关与 prompt 提示词覆盖描述
This commit is contained in:
+1
-1
@@ -41,7 +41,7 @@ func New(cfg *config.Config) (*Bot, error) {
|
||||
clients: make(map[string]*openai.Client),
|
||||
cfg: cfg,
|
||||
systemPrompt: cfg.SystemPrompt,
|
||||
toolRegistry: tools.NewRegistry(builtin.TimeTool{}, builtin.CalculatorTool{}, builtin.RandomTool{}),
|
||||
toolRegistry: tools.NewRegistry(builtin.NewTimeTool(), builtin.NewCalculatorTool(), builtin.NewRandomTool()),
|
||||
}
|
||||
b.provider = config.FindProvider(cfg.DefaultProvider)
|
||||
b.model = cfg.DefaultModel
|
||||
|
||||
@@ -9,13 +9,37 @@ import (
|
||||
"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)\""
|
||||
type calculatorTool struct {
|
||||
enabled bool
|
||||
prompt string
|
||||
}
|
||||
func (CalculatorTool) Parameters() map[string]any {
|
||||
|
||||
func NewCalculatorTool() *calculatorTool {
|
||||
return &calculatorTool{
|
||||
enabled: true,
|
||||
prompt: "计算数学表达式,如 \"(12 + 3) * 4\"、\"2^10\"、\"sqrt(9)\"",
|
||||
}
|
||||
}
|
||||
|
||||
func (t *calculatorTool) Name() string { return "calculate" }
|
||||
func (t *calculatorTool) Description() string { return t.prompt }
|
||||
func (t *calculatorTool) Enabled() bool { return t.enabled }
|
||||
func (t *calculatorTool) DefaultConfig() map[string]any {
|
||||
return map[string]any{"enabled": true, "prompt": t.prompt}
|
||||
}
|
||||
|
||||
func (t *calculatorTool) Configure(cfg map[string]any) error {
|
||||
var err error
|
||||
if t.enabled, err = parseEnabled(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
if p, ok := cfg["prompt"].(string); ok && p != "" {
|
||||
t.prompt = p
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *calculatorTool) Parameters() map[string]any {
|
||||
return map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
@@ -25,7 +49,7 @@ func (CalculatorTool) Parameters() map[string]any {
|
||||
}
|
||||
}
|
||||
|
||||
func (CalculatorTool) Execute(args json.RawMessage) (string, error) {
|
||||
func (t *calculatorTool) Execute(args json.RawMessage) (string, error) {
|
||||
var p struct {
|
||||
Expression string `json:"expression"`
|
||||
}
|
||||
|
||||
@@ -6,13 +6,37 @@ import (
|
||||
"math/rand/v2"
|
||||
)
|
||||
|
||||
type RandomTool struct{}
|
||||
|
||||
func (RandomTool) Name() string { return "random_number" }
|
||||
func (RandomTool) Description() string {
|
||||
return "生成指定范围内的随机整数,默认 0 到 100(含端点)"
|
||||
type randomTool struct {
|
||||
enabled bool
|
||||
prompt string
|
||||
}
|
||||
func (RandomTool) Parameters() map[string]any {
|
||||
|
||||
func NewRandomTool() *randomTool {
|
||||
return &randomTool{
|
||||
enabled: true,
|
||||
prompt: "生成指定范围内的随机整数,默认 0 到 100(含端点)",
|
||||
}
|
||||
}
|
||||
|
||||
func (t *randomTool) Name() string { return "random_number" }
|
||||
func (t *randomTool) Description() string { return t.prompt }
|
||||
func (t *randomTool) Enabled() bool { return t.enabled }
|
||||
func (t *randomTool) DefaultConfig() map[string]any {
|
||||
return map[string]any{"enabled": true, "prompt": t.prompt}
|
||||
}
|
||||
|
||||
func (t *randomTool) Configure(cfg map[string]any) error {
|
||||
var err error
|
||||
if t.enabled, err = parseEnabled(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
if p, ok := cfg["prompt"].(string); ok && p != "" {
|
||||
t.prompt = p
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *randomTool) Parameters() map[string]any {
|
||||
return map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
@@ -22,7 +46,7 @@ func (RandomTool) Parameters() map[string]any {
|
||||
}
|
||||
}
|
||||
|
||||
func (RandomTool) Execute(args json.RawMessage) (string, error) {
|
||||
func (t *randomTool) Execute(args json.RawMessage) (string, error) {
|
||||
var p struct {
|
||||
Min *int `json:"min"`
|
||||
Max *int `json:"max"`
|
||||
|
||||
@@ -2,16 +2,41 @@ package builtin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TimeTool struct{}
|
||||
|
||||
func (TimeTool) Name() string { return "get_current_time" }
|
||||
func (TimeTool) Description() string {
|
||||
return "获取当前日期和时间,可选指定时区(如 Asia/Shanghai,默认本地时区)"
|
||||
type timeTool struct {
|
||||
enabled bool
|
||||
prompt string
|
||||
}
|
||||
func (TimeTool) Parameters() map[string]any {
|
||||
|
||||
func NewTimeTool() *timeTool {
|
||||
return &timeTool{
|
||||
enabled: true,
|
||||
prompt: "获取当前日期和时间,可选指定时区(如 Asia/Shanghai,默认本地时区)",
|
||||
}
|
||||
}
|
||||
|
||||
func (t *timeTool) Name() string { return "get_current_time" }
|
||||
func (t *timeTool) Description() string { return t.prompt }
|
||||
func (t *timeTool) Enabled() bool { return t.enabled }
|
||||
func (t *timeTool) DefaultConfig() map[string]any {
|
||||
return map[string]any{"enabled": true, "prompt": t.prompt}
|
||||
}
|
||||
|
||||
func (t *timeTool) Configure(cfg map[string]any) error {
|
||||
var err error
|
||||
if t.enabled, err = parseEnabled(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
if p, ok := cfg["prompt"].(string); ok && p != "" {
|
||||
t.prompt = p
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *timeTool) Parameters() map[string]any {
|
||||
return map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
@@ -20,7 +45,7 @@ func (TimeTool) Parameters() map[string]any {
|
||||
}
|
||||
}
|
||||
|
||||
func (TimeTool) Execute(args json.RawMessage) (string, error) {
|
||||
func (t *timeTool) Execute(args json.RawMessage) (string, error) {
|
||||
var p struct {
|
||||
Timezone string `json:"timezone"`
|
||||
}
|
||||
@@ -36,3 +61,15 @@ func (TimeTool) Execute(args json.RawMessage) (string, error) {
|
||||
now := time.Now().In(loc)
|
||||
return now.Format("2006-01-02 15:04:05 Monday MST"), nil
|
||||
}
|
||||
|
||||
func parseEnabled(cfg map[string]any) (bool, error) {
|
||||
v, ok := cfg["enabled"]
|
||||
if !ok {
|
||||
return true, nil
|
||||
}
|
||||
b, ok := v.(bool)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("enabled 必须是布尔值,实际为 %T", v)
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
@@ -23,7 +23,7 @@ func TestCalculator(t *testing.T) {
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
args, _ := json.Marshal(map[string]string{"expression": c.expr})
|
||||
got, err := builtin.CalculatorTool{}.Execute(args)
|
||||
got, err := builtin.NewCalculatorTool().Execute(args)
|
||||
if err != nil {
|
||||
t.Fatalf("Execute 出错: %v", err)
|
||||
}
|
||||
@@ -36,7 +36,7 @@ func TestCalculator(t *testing.T) {
|
||||
|
||||
func TestCalculatorInvalid(t *testing.T) {
|
||||
args, _ := json.Marshal(map[string]string{"expression": "1 +"})
|
||||
if _, err := (builtin.CalculatorTool{}).Execute(args); err == nil {
|
||||
if _, err := builtin.NewCalculatorTool().Execute(args); err == nil {
|
||||
t.Error("非法表达式应返回错误")
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@ func TestCalculatorInvalid(t *testing.T) {
|
||||
func TestRandom(t *testing.T) {
|
||||
args, _ := json.Marshal(map[string]any{"min": 1, "max": 10})
|
||||
for i := 0; i < 100; i++ {
|
||||
out, err := builtin.RandomTool{}.Execute(args)
|
||||
out, err := builtin.NewRandomTool().Execute(args)
|
||||
if err != nil {
|
||||
t.Fatalf("Execute 出错: %v", err)
|
||||
}
|
||||
@@ -57,13 +57,13 @@ func TestRandom(t *testing.T) {
|
||||
}
|
||||
}
|
||||
bad, _ := json.Marshal(map[string]any{"min": 10, "max": 1})
|
||||
if _, err := (builtin.RandomTool{}).Execute(bad); err == nil {
|
||||
if _, err := builtin.NewRandomTool().Execute(bad); err == nil {
|
||||
t.Error("min>max 应返回错误")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimeTool(t *testing.T) {
|
||||
out, err := builtin.TimeTool{}.Execute(nil)
|
||||
out, err := builtin.NewTimeTool().Execute(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Execute 出错: %v", err)
|
||||
}
|
||||
@@ -71,3 +71,45 @@ func TestTimeTool(t *testing.T) {
|
||||
t.Errorf("时间输出异常: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigurePrompt(t *testing.T) {
|
||||
tool := builtin.NewTimeTool()
|
||||
err := tool.Configure(map[string]any{"enabled": true, "prompt": "自定义提示词"})
|
||||
if err != nil {
|
||||
t.Fatalf("Configure 出错: %v", err)
|
||||
}
|
||||
if tool.Description() != "自定义提示词" {
|
||||
t.Errorf("Description = %q, want 自定义提示词", tool.Description())
|
||||
}
|
||||
if !tool.Enabled() {
|
||||
t.Error("enabled 应为 true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigureDisable(t *testing.T) {
|
||||
tool := builtin.NewCalculatorTool()
|
||||
if err := tool.Configure(map[string]any{"enabled": false}); err != nil {
|
||||
t.Fatalf("Configure 出错: %v", err)
|
||||
}
|
||||
if tool.Enabled() {
|
||||
t.Error("enabled 应为 false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigureInvalid(t *testing.T) {
|
||||
tool := builtin.NewRandomTool()
|
||||
if err := tool.Configure(map[string]any{"enabled": "yes"}); err == nil {
|
||||
t.Error("enabled 非布尔值应报错")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultConfig(t *testing.T) {
|
||||
tool := builtin.NewTimeTool()
|
||||
cfg := tool.DefaultConfig()
|
||||
if cfg["enabled"] != true {
|
||||
t.Errorf("默认 enabled 应为 true, got %v", cfg["enabled"])
|
||||
}
|
||||
if p, ok := cfg["prompt"].(string); !ok || p == "" {
|
||||
t.Errorf("默认 prompt 缺失: %v", cfg["prompt"])
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
type stubConfigurable struct {
|
||||
configured map[string]any
|
||||
fail bool
|
||||
enabled bool
|
||||
}
|
||||
|
||||
func (s *stubConfigurable) Name() string { return "db" }
|
||||
@@ -22,13 +23,17 @@ func (s *stubConfigurable) Parameters() map[string]any {
|
||||
func (s *stubConfigurable) Execute(args json.RawMessage) (string, error) {
|
||||
return "ok", nil
|
||||
}
|
||||
func (s *stubConfigurable) Enabled() bool { return s.enabled }
|
||||
func (s *stubConfigurable) DefaultConfig() map[string]any {
|
||||
return map[string]any{"host": "127.0.0.1", "password": "请填写"}
|
||||
return map[string]any{"enabled": true, "password": "请填写"}
|
||||
}
|
||||
func (s *stubConfigurable) Configure(cfg map[string]any) error {
|
||||
if s.fail {
|
||||
return fmt.Errorf("密码为空")
|
||||
}
|
||||
if v, ok := cfg["enabled"].(bool); ok {
|
||||
s.enabled = v
|
||||
}
|
||||
s.configured = cfg
|
||||
return nil
|
||||
}
|
||||
@@ -61,7 +66,7 @@ func TestInitConfigsOK(t *testing.T) {
|
||||
if err := os.WriteFile(path, []byte("host: localhost\npassword: secret\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stub := &stubConfigurable{}
|
||||
stub := &stubConfigurable{enabled: true}
|
||||
if err := NewRegistry(stub).InitConfigs(); err != nil {
|
||||
t.Fatalf("InitConfigs 出错: %v", err)
|
||||
}
|
||||
@@ -87,3 +92,25 @@ func TestInitConfigsSkipsPlain(t *testing.T) {
|
||||
t.Fatalf("普通工具不应报错: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitConfigsDisables(t *testing.T) {
|
||||
t.Chdir(t.TempDir())
|
||||
if err := os.MkdirAll(filepath.Join("data", "tools"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
path := filepath.Join("data", "tools", "db.yaml")
|
||||
if err := os.WriteFile(path, []byte("enabled: false\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stub := &stubConfigurable{enabled: true}
|
||||
r := NewRegistry(stub)
|
||||
if err := r.InitConfigs(); err != nil {
|
||||
t.Fatalf("InitConfigs 出错: %v", err)
|
||||
}
|
||||
if stub.Enabled() {
|
||||
t.Fatal("工具应被禁用")
|
||||
}
|
||||
if _, ok := r.Get("db"); ok {
|
||||
t.Error("禁用的工具应被移出注册表")
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,11 @@ type Configurable interface {
|
||||
Configure(cfg map[string]any) error
|
||||
}
|
||||
|
||||
// Enabler 是可开关工具的接口:Configure 后返回 false 的工具会被移出注册表。
|
||||
type Enabler interface {
|
||||
Enabled() bool
|
||||
}
|
||||
|
||||
type Registry struct {
|
||||
tools map[string]Tool
|
||||
}
|
||||
@@ -102,6 +107,9 @@ func (r *Registry) InitConfigs() error {
|
||||
if err := c.Configure(cfg); err != nil {
|
||||
return fmt.Errorf("工具 %s 配置无效: %w", name, err)
|
||||
}
|
||||
if e, ok := t.(Enabler); ok && !e.Enabled() {
|
||||
delete(r.tools, name)
|
||||
}
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
return fmt.Errorf("%s", strings.Join(missing, "\n"))
|
||||
|
||||
Reference in New Issue
Block a user