Files
Agent-System-Backend/internal/tools/search.go
T
weichen 0d55ebb948 Wire Tavily web search through the prompt-backed tool face
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.
2026-09-30 11:00:11 +00:00

98 lines
3.7 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 —— 网络搜索工具:桥接 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, &params); 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()
}