Version: 0.9.52.dev.260428
后端: 1. 工具结果结构化切流继续推进:schedule 读工具改为“父包 adapter + 子包 view builder”,`queue_pop_head` / `queue_skip_head` 脱离 legacy wrapper,`analyze_health` / `analyze_rhythm` 补齐 `schedule.analysis_result` 诊断卡片。 2. 非 schedule 工具补齐专属结果协议:`web_search` / `web_fetch`、`upsert_task_class`、`context_tools_add` / `context_tools_remove` 全部接入结构化 `ResultView`,注册表继续去 legacy wrapper,同时保持原始 `ObservationText` 供模型链路复用。 3. 工具展示细节继续收口:参数本地化补齐 `domain` / `packs` / `mode` / `all`,deliver 阶段补发段落分隔,避免 execute 与总结正文黏连。 前端: 4. `ToolCardRenderer` 升级为多协议通用渲染器,补齐 read / analysis / web / taskclass / context 卡片渲染、参数折叠区、未知协议兜底与操作明细展示。 5. `AssistantPanel` 修正 `tool_result` 结果回填与卡片布局宽度问题,并新增结构化卡片 fixture / mock 调试入口,便于整体验收。 仓库: 6. 更新工具结果结构化交接文档,补记第四批切流范围、当前切流点与后续收尾建议。
This commit is contained in:
191
backend/newAgent/tools/web_result_handlers.go
Normal file
191
backend/newAgent/tools/web_result_handlers.go
Normal file
@@ -0,0 +1,191 @@
|
||||
package newagenttools
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
|
||||
"github.com/LoveLosita/smartflow/backend/newAgent/tools/web"
|
||||
webresult "github.com/LoveLosita/smartflow/backend/newAgent/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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user