Files
Agent-System-Backend/internal/tools/search.go
weichen adcd8fefd2 Fence U3 account writes and stabilize directory cursors
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.
2026-10-14 07:33:54 +00:00

116 lines
4.6 KiB
Go
Raw Permalink 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 —— 网络搜索工具:桥接 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, &params); 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()
}