Why: Agent-Native ships a Tavily integration (key-pool rotation, markdown output, fallback copy) that the backend never registered, so the catalog's WebSearch prompt had no carrier; wiring the tool gives it one and gives the agent live-web reach. Evidence: 4 new tool tests (registration, markdown formatting, empty query, failure fallback carried inside the error to match registry replay semantics); go test ./... 16 packages ok; live boot check - no keys means not registered, two keys log registration with both tools in /api/tools and 46 library modules in /api/prompts. Impact: tools.tavily.apiKeys/endpoint settings (restart effect; empty pool means the tool is absent, no silent degradation); tool.tavily_search and tool.web_search prompt modules inject citation discipline (cite sources, keep inference separate, fall back to workspace materials on failure); library floor raised to 46. Refs: feature/tavily-search-tool task card; guide disposition table in Documents.
98 lines
3.7 KiB
Go
98 lines
3.7 KiB
Go
// Package tools —— 网络搜索工具:桥接 Agent-Native 的 TavilyProvider
|
||
// (密钥池轮换由其实现)到统一注册表。配置了 tools.tavily.apiKeys 才注册
|
||
// (对齐 agent-native 语义:未配置不静默降级为空结果)。
|
||
package tools
|
||
|
||
import (
|
||
"context"
|
||
"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 {
|
||
tavily := func(rc ToolExecCtx, args json.RawMessage) (string, error) {
|
||
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()
|
||
}
|