内置工具接入独立配置:enabled 开关与 prompt 提示词覆盖描述

This commit is contained in:
2026-08-14 17:35:54 +08:00
parent 965ec275de
commit 21be6e63af
7 changed files with 191 additions and 29 deletions
+1 -1
View File
@@ -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
+31 -7
View File
@@ -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"`
}
+31 -7
View File
@@ -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"`
+44 -7
View File
@@ -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
}
+47 -5
View File
@@ -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"])
}
}
+29 -2
View File
@@ -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("禁用的工具应被移出注册表")
}
}
+8
View File
@@ -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"))