// Package tools —— 网络搜索工具:桥接 Agent-Native 的 TavilyProvider // (密钥池轮换由其实现)到统一注册表。配置了 tools.tavily.apiKeys 才注册 // (对齐 agent-native 语义:未配置不静默降级为空结果)。 package tools import ( "context" "encoding/json" "fmt" "strings" agenttools "git-ssh.u1680832.nyat.app/weichen/Agent-Native.git/runtime/tools" core "git-ssh.u1680832.nyat.app/weichen/Agent-System-Core.git" ) // 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(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(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.") } resp, err := provider.Search(context.Background(), 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() }