Version: 0.9.15.dev.260412
后端: 1. 排程工具从 tools/ 根目录拆分为 tools/schedule 独立子包 - 12 个排程工具文件等价迁入 tools/schedule/,tools/ 根目录仅保留 registry.go 作为统一注册入口 - 所有依赖方(conv / model / node / prompt / service)import 统一切到 schedule 子包 2. Web 搜索工具链落地(tools/web 子包) - 新增 web_search(结构化检索)与 web_fetch(正文抓取)两个读工具,支持博查 API / mock 降级 - 启动流程按配置选择 provider,未识别类型自动降级为 mock,不阻断主流程 - 执行提示补齐 web 工具使用约束与返回值示例 - config.example.yaml 补齐 websearch 配置段 前端:无 仓库:无
This commit is contained in:
227
backend/newAgent/tools/web/tools.go
Normal file
227
backend/newAgent/tools/web/tools.go
Normal file
@@ -0,0 +1,227 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SearchToolHandler web_search 工具 handler。
|
||||
//
|
||||
// 职责:
|
||||
// 1. 解析 args(query / top_k / domain_allow / recency_days);
|
||||
// 2. 调用 SearchProvider 执行检索;
|
||||
// 3. 组装结构化 JSON observation 返回给模型。
|
||||
//
|
||||
// 不负责:
|
||||
// 1. 不负责 provider 生命周期管理(由注册层注入);
|
||||
// 2. 不负责重试(provider 内部处理)。
|
||||
type SearchToolHandler struct {
|
||||
provider SearchProvider
|
||||
}
|
||||
|
||||
// NewSearchToolHandler 创建 web_search 工具 handler。
|
||||
//
|
||||
// 1. provider 为 nil 时,Handle 返回"搜索暂未启用"的 observation;
|
||||
// 2. 这样做的好处是:即使未配置 provider,也不会阻断主流程。
|
||||
func NewSearchToolHandler(provider SearchProvider) *SearchToolHandler {
|
||||
return &SearchToolHandler{provider: provider}
|
||||
}
|
||||
|
||||
// searchToolArgs web_search 工具的参数定义。
|
||||
type searchToolArgs struct {
|
||||
Query string `json:"query"`
|
||||
TopK int `json:"top_k"`
|
||||
DomainAllow []string `json:"domain_allow"`
|
||||
RecencyDays int `json:"recency_days"`
|
||||
}
|
||||
|
||||
// searchToolResult web_search 工具的输出结构。
|
||||
type searchToolResult struct {
|
||||
Tool string `json:"tool"`
|
||||
Query string `json:"query"`
|
||||
Count int `json:"count"`
|
||||
Items []searchItem `json:"items"`
|
||||
}
|
||||
|
||||
// searchItem 输出给模型的单条搜索结果。
|
||||
type searchItem struct {
|
||||
Title string `json:"title"`
|
||||
URL string `json:"url"`
|
||||
Snippet string `json:"snippet"`
|
||||
Domain string `json:"domain"`
|
||||
PublishedAt string `json:"published_at,omitempty"`
|
||||
}
|
||||
|
||||
// Handle 执行 web_search 工具。
|
||||
//
|
||||
// 1. 解析参数,query 为必填,缺失时直接返回错误 observation;
|
||||
// 2. 调用 provider.Search,超时上限 10 秒;
|
||||
// 3. 失败时返回可恢复 observation(包含错误原因),不 panic、不阻断主流程。
|
||||
func (h *SearchToolHandler) Handle(args map[string]any) string {
|
||||
// 1. provider 为 nil 说明未启用,直接返回提示。
|
||||
if h.provider == nil {
|
||||
return `{"tool":"web_search","error":"搜索暂未启用,请跳过 web_search 继续执行其他操作。"}`
|
||||
}
|
||||
|
||||
// 2. 提取必填参数 query。
|
||||
query, _ := args["query"].(string)
|
||||
query = strings.TrimSpace(query)
|
||||
if query == "" {
|
||||
return `{"tool":"web_search","error":"参数错误:缺少必填参数 query。"}`
|
||||
}
|
||||
|
||||
// 3. 提取可选参数。
|
||||
topK, _ := args["top_k"].(float64)
|
||||
var domainAllow []string
|
||||
if raw, ok := args["domain_allow"].([]any); ok {
|
||||
for _, v := range raw {
|
||||
if s, ok := v.(string); ok {
|
||||
domainAllow = append(domainAllow, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
recencyDays, _ := args["recency_days"].(float64)
|
||||
|
||||
// 4. 构建带超时的 context,防止搜索请求卡死。
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 5. 调用 provider。
|
||||
start := time.Now()
|
||||
resp, err := h.provider.Search(ctx, query, SearchOptions{
|
||||
TopK: int(topK),
|
||||
DomainAllow: domainAllow,
|
||||
RecencyDays: int(recencyDays),
|
||||
})
|
||||
elapsed := time.Since(start)
|
||||
|
||||
// 6. 记录日志,方便排查搜索耗时、结果数、失败原因。
|
||||
log.Printf("[web_search] provider=%s query=%q topK=%d elapsed=%s results=%d err=%v",
|
||||
h.provider.Name(), query, int(topK), elapsed, len(resp.Items), err)
|
||||
|
||||
if err != nil {
|
||||
// 7. 失败时返回可恢复 observation:模型看到后可选择换 query 或跳过。
|
||||
return fmt.Sprintf(`{"tool":"web_search","error":"搜索失败:%s","query":%q}`, err.Error(), query)
|
||||
}
|
||||
|
||||
// 8. 组装输出 JSON。
|
||||
items := make([]searchItem, 0, len(resp.Items))
|
||||
for _, item := range resp.Items {
|
||||
si := searchItem{
|
||||
Title: item.Title,
|
||||
URL: item.URL,
|
||||
Snippet: item.Snippet,
|
||||
Domain: item.Domain,
|
||||
}
|
||||
if !item.PublishedAt.IsZero() {
|
||||
si.PublishedAt = item.PublishedAt.Format("2006-01-02")
|
||||
}
|
||||
items = append(items, si)
|
||||
}
|
||||
|
||||
result := searchToolResult{
|
||||
Tool: "web_search",
|
||||
Query: query,
|
||||
Count: len(items),
|
||||
Items: items,
|
||||
}
|
||||
|
||||
out, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
return fmt.Sprintf(`{"tool":"web_search","error":"序列化结果失败:%s"}`, err.Error())
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
// FetchToolHandler web_fetch 工具 handler。
|
||||
//
|
||||
// 职责:
|
||||
// 1. 解析 args(url / max_chars);
|
||||
// 2. 调用 Fetcher 抓取并清洗正文;
|
||||
// 3. 组装 JSON observation 返回给模型。
|
||||
type FetchToolHandler struct {
|
||||
fetcher *Fetcher
|
||||
}
|
||||
|
||||
// NewFetchToolHandler 创建 web_fetch 工具 handler。
|
||||
func NewFetchToolHandler(fetcher *Fetcher) *FetchToolHandler {
|
||||
return &FetchToolHandler{fetcher: fetcher}
|
||||
}
|
||||
|
||||
// fetchToolResult web_fetch 工具的输出结构。
|
||||
type fetchToolResult struct {
|
||||
Tool string `json:"tool"`
|
||||
URL string `json:"url"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Truncated bool `json:"truncated"`
|
||||
}
|
||||
|
||||
// Handle 执行 web_fetch 工具。
|
||||
//
|
||||
// 1. 解析参数,url 为必填;
|
||||
// 2. max_chars 可选,为 0 时使用 Fetcher 默认值(4000);
|
||||
// 3. 所有失败场景返回结构化错误 observation,不 panic。
|
||||
func (h *FetchToolHandler) Handle(args map[string]any) string {
|
||||
// 1. fetcher 为 nil 说明未初始化。
|
||||
if h.fetcher == nil {
|
||||
return `{"tool":"web_fetch","error":"抓取服务暂未初始化,请跳过 web_fetch 继续执行。"}`
|
||||
}
|
||||
|
||||
// 2. 提取必填参数 url。
|
||||
url, _ := args["url"].(string)
|
||||
url = strings.TrimSpace(url)
|
||||
if url == "" {
|
||||
return `{"tool":"web_fetch","error":"参数错误:缺少必填参数 url。"}`
|
||||
}
|
||||
|
||||
// 3. 提取可选参数 max_chars,覆盖 Fetcher 默认值。
|
||||
maxChars := 0
|
||||
if v, ok := args["max_chars"].(float64); ok {
|
||||
maxChars = int(v)
|
||||
}
|
||||
|
||||
// 4. 若调用方指定 max_chars,临时覆盖 Fetcher 配置。
|
||||
savedMaxChars := h.fetcher.MaxChars
|
||||
if maxChars > 0 {
|
||||
h.fetcher.MaxChars = maxChars
|
||||
}
|
||||
defer func() {
|
||||
h.fetcher.MaxChars = savedMaxChars
|
||||
}()
|
||||
|
||||
// 5. 构建带超时的 context。
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 6. 调用 Fetcher。
|
||||
start := time.Now()
|
||||
result, err := h.fetcher.Fetch(ctx, url)
|
||||
elapsed := time.Since(start)
|
||||
|
||||
log.Printf("[web_fetch] url=%q elapsed=%s truncated=%v err=%v", url, elapsed, result != nil && result.Truncated, err)
|
||||
|
||||
if err != nil {
|
||||
// 7. 失败时返回可恢复 observation。
|
||||
return fmt.Sprintf(`{"tool":"web_fetch","error":"抓取失败:%s","url":%q}`, err.Error(), url)
|
||||
}
|
||||
|
||||
// 8. 组装输出 JSON。
|
||||
out := fetchToolResult{
|
||||
Tool: "web_fetch",
|
||||
URL: url,
|
||||
Title: result.Title,
|
||||
Content: result.Content,
|
||||
Truncated: result.Truncated,
|
||||
}
|
||||
|
||||
raw, err := json.Marshal(out)
|
||||
if err != nil {
|
||||
return fmt.Sprintf(`{"tool":"web_fetch","error":"序列化结果失败:%s"}`, err.Error())
|
||||
}
|
||||
return string(raw)
|
||||
}
|
||||
Reference in New Issue
Block a user