Files
Agent-System-Backend/internal/tools/registry.go
T
weichen 0d74ec94dd Implement the backend: store/bus/runmgr core
Details (original message):
Implement the backend: store/bus/runmgr core, mock and LLM runners (agent-native client), HITL three-branch routing, subagent runs, builtin and declarative HTTP tools, ingest parsers (md/txt/docx/pdf-text), knowledge fulltext, layered settings, profiles, plugin host, REST+SSE API, single-binary entrypoint; e2e and unit tests.
2026-09-29 17:53:24 +00:00

169 lines
5.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package tools 实现统一工具注册表(4.8):内置、声明式 HTTP、插件工具同一形态;
// 来源标记(builtin/plugin/user)、启用开关、确认策略由画像三清单决定(runmgr 执行)。
package tools
import (
"encoding/json"
"fmt"
"sort"
"strings"
"sync"
"time"
core "agent-system-core"
)
// Executor 是无状态执行器:入参已校验,返回输出文本或错误。
type Executor func(rc ToolExecCtx, args json.RawMessage) (string, error)
// ToolExecCtx 供工具访问受控环境(当前项目/检索/工件写入)。
type ToolExecCtx interface {
ProjectID() string
ConversationID() string
Retrieve(q core.RetrievalQuery) ([]core.RetrievalHit, error)
WriteArtifact(name, kind, mime string, content []byte) (core.Artifact, error)
}
type tool struct {
desc core.ToolDesc
exec Executor
}
type Registry struct {
mu sync.RWMutex
tools map[string]tool
}
func NewRegistry() *Registry { return &Registry{tools: map[string]tool{}} }
// Register 幂等注册(后注册覆盖同名;来源标记随之更新)。
// 声明式 HTTP 工具经 HTTPExecutor(spec, lookup) 绑定执行器。
func (r *Registry) Register(desc core.ToolDesc, exec Executor) error {
if desc.Name == "" {
return fmt.Errorf("tools: empty name")
}
if exec == nil {
return fmt.Errorf("tools: %s has no executor", desc.Name)
}
r.mu.Lock()
r.tools[desc.Name] = tool{desc: desc, exec: exec}
r.mu.Unlock()
return nil
}
func (r *Registry) UnregisterBySource(source core.SourceKind, pluginID string) {
r.mu.Lock()
for name, t := range r.tools {
if t.desc.Source == source && (pluginID == "" || t.desc.PluginID == pluginID) {
delete(r.tools, name)
}
}
r.mu.Unlock()
}
func (r *Registry) List() []core.ToolDesc {
r.mu.RLock()
defer r.mu.RUnlock()
out := make([]core.ToolDesc, 0, len(r.tools))
for _, t := range r.tools {
out = append(out, t.desc)
}
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
return out
}
func (r *Registry) Get(name string) (core.ToolDesc, Executor, bool) {
r.mu.RLock()
t, ok := r.tools[name]
r.mu.RUnlock()
return t.desc, t.exec, ok
}
// Execute 校验存在性并调用执行器;策略(allow/deny/confirm)与截断由 runmgr 负责。
func (r *Registry) Execute(rc ToolExecCtx, call core.ToolCallRequest) core.ToolCallResult {
desc, exec, ok := r.Get(call.Name)
if !ok {
return core.ToolCallResult{CallID: call.CallID, Status: "error", Output: fmt.Sprintf("tool %q not found (registry miss)", call.Name)}
}
out, err := exec(rc, call.Arguments)
if err != nil {
return core.ToolCallResult{CallID: call.CallID, Status: "error", Output: err.Error()}
}
_ = desc
return core.ToolCallResult{CallID: call.CallID, Status: "ok", Output: out, Truncated: false, Artifact: nil, }
}
// ---- 内置最小工具集(一切可禁用:画像三清单控制)----
// RegisterBuiltin 注册骨架期内置工具:时间、会话历史检索、知识检索、读文件、写工件。
func (r *Registry) RegisterBuiltin() error {
if err := r.Register(core.ToolDesc{
Name: "time_now", Description: "获取当前时间(RFC3339)与时区", Source: core.SourceBuiltin,
Parameters: json.RawMessage(`{"type":"object","properties":{}}`),
}, func(rc ToolExecCtx, args json.RawMessage) (string, error) {
return time.Now().Format(time.RFC3339), nil
}); err != nil {
return err
}
if err := r.Register(core.ToolDesc{
Name: "knowledge_search", Description: "在项目知识与插件知识库中做全文检索,返回命中片段", Source: core.SourceBuiltin,
Parameters: json.RawMessage(`{"type":"object","properties":{"query":{"type":"string","description":"检索词"}},"required":["query"]}`),
}, func(rc ToolExecCtx, args json.RawMessage) (string, error) {
var a struct{ Query string `json:"query"`; TopK int `json:"topK"` }
if len(args) > 0 {
if err := json.Unmarshal(args, &a); err != nil {
return "", fmt.Errorf("knowledge_search: bad args: %w", err)
}
}
if a.Query == "" {
return "", fmt.Errorf("knowledge_search: query is required")
}
hits, err := rc.Retrieve(core.RetrievalQuery{ProjectID: rc.ProjectID(), Text: a.Query, TopK: a.TopK})
if err != nil {
return "", err
}
if len(hits) == 0 {
return "(无命中)", nil
}
var sb strings.Builder
for i, h := range hits {
fmt.Fprintf(&sb, "[%d] %s(来源 %s,得分 %.2f)\n%s\n\n", i+1, h.Doc.Title, h.Doc.Source, h.Score, h.Doc.Chunk)
}
return sb.String(), nil
}); err != nil {
return err
}
if err := r.Register(core.ToolDesc{
Name: "write_artifact", Description: "把生成内容写入工件(文档/数据),返回工件 ID 与名称", Source: core.SourceBuiltin,
Parameters: json.RawMessage(`{"type":"object","properties":{"name":{"type":"string"},"kind":{"type":"string","enum":["document","data"]},"content":{"type":"string"}},"required":["name","content"]}`),
}, func(rc ToolExecCtx, args json.RawMessage) (string, error) {
var a struct {
Name string `json:"name"`
Kind string `json:"kind"`
Content string `json:"content"`
}
if err := json.Unmarshal(args, &a); err != nil {
return "", err
}
if a.Name == "" {
return "", fmt.Errorf("write_artifact: name required")
}
kind := a.Kind
if kind == "" {
kind = "document"
}
mime := "text/markdown"
if kind == "data" {
mime = "application/json"
}
art, err := rc.WriteArtifact(a.Name, kind, mime, []byte(a.Content))
if err != nil {
return "", err
}
return fmt.Sprintf("已写入工件 %s(%s,%d 字节)", art.Name, art.ID, art.Size), nil
}); err != nil {
return err
}
return nil
}