Version: 0.9.75.dev.260505

后端:
1.收口阶段 6 agent 结构迁移,将 newAgent 内核与 agentsvc 编排层迁入 services/agent
- 切换 Agent 启动装配与 HTTP handler 直连 agent sv,移除旧 service agent bridge
- 补齐 Agent 对 memory、task、task-class、schedule 的 RPC 适配与契约字段
- 扩展 schedule、task、task-class RPC/contract 支撑 Agent 查询、写入与 provider 切流
- 更新迁移文档、README 与相关注释,明确 agent 当前切流点和剩余 memory 迁移面
This commit is contained in:
Losita
2026-05-05 16:00:57 +08:00
parent e1819c5653
commit d7184b776b
174 changed files with 2189 additions and 1236 deletions

View File

@@ -0,0 +1,191 @@
package agenttools
import (
"strings"
"github.com/LoveLosita/smartflow/backend/services/agent/tools/schedule"
"github.com/LoveLosita/smartflow/backend/services/agent/tools/web"
webresult "github.com/LoveLosita/smartflow/backend/services/agent/tools/web_result"
)
// NewWebSearchToolHandler 返回 web_search 的结构化结果 handler。
//
// 职责边界:
// 1. 负责执行底层 web_search 工具,并保留原始 ObservationText 给模型。
// 2. 负责把工具参数投影成 web_result 子包需要的最小输入。
// 3. 不负责注册接线registry.go 由主代理统一切流。
func NewWebSearchToolHandler(provider web.SearchProvider) ToolHandler {
searchHandler := web.NewSearchToolHandler(provider)
return func(state *schedule.ScheduleState, args map[string]any) ToolExecutionResult {
observation := searchHandler.Handle(args)
legacy := LegacyResultWithState("web_search", args, state, observation)
view := webresult.BuildSearchView(webresult.SearchViewInput{
Observation: observation,
Query: readStringArg(args, "query"),
TopK: readIntArg(args, "top_k"),
DomainAllow: readStringSliceArg(args, "domain_allow"),
RecencyDays: readIntArg(args, "recency_days"),
})
return buildWebExecutionResult(legacy, args, view)
}
}
// NewWebFetchToolHandler 返回 web_fetch 的结构化结果 handler。
//
// 职责边界:
// 1. 负责执行底层 web_fetch 工具,并保留原始 ObservationText 给模型。
// 2. 负责把抓取参数投影成 web_result 子包需要的最小输入。
// 3. 不负责注册接线registry.go 由主代理统一切流。
func NewWebFetchToolHandler(fetcher *web.Fetcher) ToolHandler {
fetchHandler := web.NewFetchToolHandler(fetcher)
return func(state *schedule.ScheduleState, args map[string]any) ToolExecutionResult {
observation := fetchHandler.Handle(args)
legacy := LegacyResultWithState("web_fetch", args, state, observation)
view := webresult.BuildFetchView(webresult.FetchViewInput{
Observation: observation,
URL: readStringArg(args, "url"),
MaxChars: readIntArg(args, "max_chars"),
})
return buildWebExecutionResult(legacy, args, view)
}
}
// buildWebExecutionResult 负责把子包纯展示视图包回父包统一协议。
//
// 步骤化说明:
// 1. 先以 legacy 结果为基础,复用父包现有的参数预览、错误抽取与兜底字段。
// 2. 再用子包 collapsed.status 覆盖最终状态,支持“未启用 provider -> blocked”的卡片语义。
// 3. 最后补齐 raw_text / status_label保证 execute、SSE、timeline 都消费同一份 observation。
func buildWebExecutionResult(
legacy ToolExecutionResult,
args map[string]any,
view webresult.ResultView,
) ToolExecutionResult {
result := legacy
status := normalizeToolStatus(result.Status)
if status == "" {
status = ToolStatusDone
}
if collapsedStatus, ok := readStringAnyMap(view.Collapsed, "status"); ok {
if normalized := normalizeToolStatus(collapsedStatus); normalized != "" {
status = normalized
}
}
collapsed := cloneAnyMap(view.Collapsed)
if collapsed == nil {
collapsed = make(map[string]any)
}
expanded := cloneAnyMap(view.Expanded)
if expanded == nil {
expanded = make(map[string]any)
}
collapsed["status"] = status
if _, exists := collapsed["status_label"]; !exists {
collapsed["status_label"] = resolveToolStatusLabelCN(status)
}
if _, exists := expanded["raw_text"]; !exists {
expanded["raw_text"] = result.ObservationText
}
if _, exists := expanded["machine_payload"]; !exists {
expanded["machine_payload"] = map[string]any{}
}
viewType := strings.TrimSpace(view.ViewType)
if viewType == "" {
viewType = webresult.ViewTypeSearchResult
}
version := view.Version
if version <= 0 {
version = webresult.ViewVersionResult
}
result.Status = status
result.Success = status == ToolStatusDone
result.ResultView = &ToolDisplayView{
ViewType: viewType,
Version: version,
Collapsed: collapsed,
Expanded: expanded,
}
if title, ok := readStringAnyMap(collapsed, "title"); ok {
result.Summary = title
}
if !result.Success {
errorCode, errorMessage := extractToolErrorInfo(result.ObservationText, status)
if strings.TrimSpace(result.ErrorCode) == "" {
result.ErrorCode = strings.TrimSpace(errorCode)
}
if strings.TrimSpace(result.ErrorMessage) == "" {
result.ErrorMessage = strings.TrimSpace(errorMessage)
}
}
return EnsureToolResultDefaults(result, args)
}
func readStringArg(args map[string]any, key string) string {
if len(args) == 0 {
return ""
}
raw, exists := args[strings.TrimSpace(key)]
if !exists || raw == nil {
return ""
}
text, ok := raw.(string)
if !ok {
return ""
}
return strings.TrimSpace(text)
}
func readIntArg(args map[string]any, key string) int {
if len(args) == 0 {
return 0
}
value, ok := toInt(args[strings.TrimSpace(key)])
if !ok {
return 0
}
return value
}
func readStringSliceArg(args map[string]any, key string) []string {
if len(args) == 0 {
return nil
}
raw, exists := args[strings.TrimSpace(key)]
if !exists || raw == nil {
return nil
}
switch typed := raw.(type) {
case []string:
out := make([]string, 0, len(typed))
for _, item := range typed {
item = strings.TrimSpace(item)
if item == "" {
continue
}
out = append(out, item)
}
return out
case []any:
out := make([]string, 0, len(typed))
for _, item := range typed {
text, ok := item.(string)
if !ok {
continue
}
text = strings.TrimSpace(text)
if text == "" {
continue
}
out = append(out, text)
}
return out
default:
return nil
}
}