Why: Account disable could be followed by late Backend business writes, API key creation could implicitly create users, account bootstrap locks could invert, and directory cursors were process-local or leaked out-of-scope changes. Evidence: Local go test ./... and isolated PostgreSQL package suites passed on the staged candidate; exact committed-head SELF replay and stage 6 human review remain pending. Impact: Adds final account transactions and external-effect checks across HTTP, Host, Bus and run manager; tightens API key issuance and persists versioned cursor signing. Delegated RDM rename still needs the separately proposed real-operator contract, so A1 and stage 6 are not complete. Refs: Documents U3 r1, approved A1-A4 amendment fb3a821, pending delegated-projection amendment 5783940, Dev feature/iam-u3-account-directory-recovery.
116 lines
4.6 KiB
Go
116 lines
4.6 KiB
Go
// Package tools —— 网络搜索工具:桥接 Agent-Native 的 TavilyProvider
|
||
// (密钥池轮换由其实现)到统一注册表。配置了 tools.tavily.apiKeys 才注册
|
||
// (对齐 agent-native 语义:未配置不静默降级为空结果)。
|
||
package tools
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"strings"
|
||
|
||
agenttools "agent-native/runtime/tools"
|
||
core "agent-system-core"
|
||
)
|
||
|
||
// TavilySettings 装配期读取的搜索配置。
|
||
type TavilySettings struct {
|
||
Endpoint string
|
||
APIKeys []string
|
||
}
|
||
|
||
// RegisterSearchTools 注册 tavily_search(完整参数面)与 web_search(别名)。
|
||
// 工具面与 agent-native runtime/tools/search.go 逐字段对齐(名称/参数/输出/失败降级文案)。
|
||
func RegisterSearchTools(r *Registry, provider agenttools.SearchProvider) error {
|
||
return registerSearch(r, func(ToolExecCtx) agenttools.SearchProvider { return provider })
|
||
}
|
||
|
||
// RegisterLazySearchTools 注册按调用时取密钥的搜索工具:provider 在每次调用时
|
||
// 重建(设置页/svc 播种改了 tavily 密钥即刻生效,不再需要重启进程——
|
||
// 实测播种后 tavily 工具要重启才注册)。configure 返回 nil 时不静默降级,
|
||
// 返回明确的未配置提示。
|
||
func RegisterLazySearchTools(r *Registry, configure func() agenttools.SearchProvider) error {
|
||
return registerSearch(r, func(ToolExecCtx) agenttools.SearchProvider { return configure() })
|
||
}
|
||
|
||
func registerSearch(r *Registry, providerFor func(ToolExecCtx) agenttools.SearchProvider) error {
|
||
tavily := func(rc ToolExecCtx, args json.RawMessage) (string, error) {
|
||
provider := providerFor(rc)
|
||
if provider == nil {
|
||
return "", fmt.Errorf("搜索未配置:settings.tools.tavily.apiKeys(或对应 secret)为空;配置后立即生效,无需重启")
|
||
}
|
||
return execSearch(rc, provider, args)
|
||
}
|
||
if err := r.Register(core.ToolDesc{
|
||
Name: "tavily_search",
|
||
Description: "Search the live internet for real-time information, competitor intelligence, regulatory news, product specs, or market data via Tavily (with automatic API key pool rotation).",
|
||
Source: core.SourceBuiltin,
|
||
Parameters: json.RawMessage(`{
|
||
"type": "object",
|
||
"properties": {
|
||
"query": {"type": "string", "description": "The search query keywords in Chinese or English"},
|
||
"search_depth": {"type": "string", "enum": ["basic", "advanced"], "description": "Search depth: 'basic' (fast summary) or 'advanced' (deep analysis, default 'basic')"},
|
||
"max_results": {"type": "number", "description": "Maximum number of search results to retrieve (1 to 10, default 5)"}
|
||
},
|
||
"required": ["query"]
|
||
}`),
|
||
}, tavily); err != nil {
|
||
return err
|
||
}
|
||
return r.Register(core.ToolDesc{
|
||
Name: "web_search",
|
||
Description: "Alias for tavily_search. Searches the live internet for information.",
|
||
Source: core.SourceBuiltin,
|
||
Parameters: json.RawMessage(`{
|
||
"type": "object",
|
||
"properties": {"query": {"type": "string", "description": "The search query keywords"}},
|
||
"required": ["query"]
|
||
}`),
|
||
}, tavily)
|
||
}
|
||
|
||
func execSearch(rc ToolExecCtx, provider agenttools.SearchProvider, args json.RawMessage) (string, error) {
|
||
var params struct {
|
||
Query string `json:"query"`
|
||
SearchDepth string `json:"search_depth"`
|
||
MaxResults float64 `json:"max_results"`
|
||
}
|
||
if err := json.Unmarshal(args, ¶ms); err != nil {
|
||
return "", err
|
||
}
|
||
query := strings.TrimSpace(params.Query)
|
||
if query == "" {
|
||
return "", fmt.Errorf("Error: query cannot be empty.")
|
||
}
|
||
if err := checkToolExternalEffect(rc); err != nil {
|
||
return "", err
|
||
}
|
||
resp, err := provider.Search(execParentCtx(rc), agenttools.SearchRequest{
|
||
Query: query,
|
||
Depth: params.SearchDepth,
|
||
MaxResults: int(params.MaxResults),
|
||
})
|
||
if err != nil {
|
||
// 失败对模型可见并指引回退(与 agent-native 扩展降级文案一致);
|
||
// 注册表把 err.Error() 作为输出回灌,故降级文案承载在 error 内。
|
||
return "", fmt.Errorf("[Tavily Search Failed]: %v\n(Agent should rely on workspace materials or local knowledge base as fallback.)", err)
|
||
}
|
||
return formatSearchResults(resp, query), nil
|
||
}
|
||
|
||
// formatSearchResults 输出 Markdown(对齐 agent-native 的呈现形态:答案 + 编号结果)。
|
||
func formatSearchResults(resp agenttools.SearchResponse, query string) string {
|
||
var sb strings.Builder
|
||
fmt.Fprintf(&sb, "## 🌐 Tavily 实时网络搜索结果: %q\n\n", query)
|
||
if strings.TrimSpace(resp.Answer) != "" {
|
||
fmt.Fprintf(&sb, "**概要**:%s\n\n", resp.Answer)
|
||
}
|
||
if len(resp.Results) == 0 {
|
||
sb.WriteString("(无命中结果)\n")
|
||
return sb.String()
|
||
}
|
||
for i, r := range resp.Results {
|
||
fmt.Fprintf(&sb, "%d. %s\n %s\n\n", i+1, r.Title, strings.TrimSpace(r.Content))
|
||
}
|
||
return sb.String()
|
||
}
|