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:
345
backend/newAgent/tools/schedule_read_handlers.go
Normal file
345
backend/newAgent/tools/schedule_read_handlers.go
Normal file
@@ -0,0 +1,345 @@
|
||||
package newagenttools
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
|
||||
scheduleread "github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule_read"
|
||||
)
|
||||
|
||||
type scheduleReadObserveFunc func(state *schedule.ScheduleState, args map[string]any) (string, *ToolExecutionResult)
|
||||
type scheduleReadViewBuilder func(input scheduleReadAdapterInput) scheduleread.ReadResultView
|
||||
|
||||
// scheduleReadAdapterInput 是父包传给 schedule_read 子包前的最小上下文。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只携带展示构造需要的 state、args、observation 与已本地化参数字段;
|
||||
// 2. 不把 ToolExecutionResult / ToolArgumentView 传入子包,避免反向依赖父包;
|
||||
// 3. ObservationText 必须原样来自底层 schedule 工具,不在 adapter 层改写。
|
||||
type scheduleReadAdapterInput struct {
|
||||
ToolName string
|
||||
Args map[string]any
|
||||
State *schedule.ScheduleState
|
||||
ObservationText string
|
||||
ArgFields []scheduleread.KVField
|
||||
}
|
||||
|
||||
// NewQueryAvailableSlotsToolHandler 为 query_available_slots 生成结构化读结果。
|
||||
func NewQueryAvailableSlotsToolHandler() ToolHandler {
|
||||
return newScheduleReadToolHandler(
|
||||
"query_available_slots",
|
||||
func(state *schedule.ScheduleState, args map[string]any) (string, *ToolExecutionResult) {
|
||||
return schedule.QueryAvailableSlots(state, args), nil
|
||||
},
|
||||
func(input scheduleReadAdapterInput) scheduleread.ReadResultView {
|
||||
return scheduleread.BuildAvailableSlotsView(scheduleread.AvailableSlotsViewInput{
|
||||
State: input.State,
|
||||
Observation: input.ObservationText,
|
||||
ArgFields: input.ArgFields,
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// NewQueryRangeToolHandler 为 query_range 生成结构化读结果。
|
||||
func NewQueryRangeToolHandler() ToolHandler {
|
||||
return newScheduleReadToolHandler(
|
||||
"query_range",
|
||||
func(state *schedule.ScheduleState, args map[string]any) (string, *ToolExecutionResult) {
|
||||
day, ok := schedule.ArgsInt(args, "day")
|
||||
if !ok {
|
||||
result := buildScheduleReadFailureResult("query_range", args, state, "查询失败:缺少必填参数 day。")
|
||||
return "", &result
|
||||
}
|
||||
if state == nil {
|
||||
result := buildScheduleReadFailureResult("query_range", args, nil, "查询失败:日程状态为空,无法读取时间范围。")
|
||||
return "", &result
|
||||
}
|
||||
return schedule.QueryRange(state, day, schedule.ArgsIntPtr(args, "slot_start"), schedule.ArgsIntPtr(args, "slot_end")), nil
|
||||
},
|
||||
func(input scheduleReadAdapterInput) scheduleread.ReadResultView {
|
||||
day, _ := schedule.ArgsInt(input.Args, "day")
|
||||
return scheduleread.BuildRangeView(scheduleread.RangeViewInput{
|
||||
State: input.State,
|
||||
Observation: input.ObservationText,
|
||||
Day: day,
|
||||
SlotStart: schedule.ArgsIntPtr(input.Args, "slot_start"),
|
||||
SlotEnd: schedule.ArgsIntPtr(input.Args, "slot_end"),
|
||||
ArgFields: input.ArgFields,
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// NewQueryTargetTasksToolHandler 为 query_target_tasks 生成结构化读结果。
|
||||
func NewQueryTargetTasksToolHandler() ToolHandler {
|
||||
return newScheduleReadToolHandler(
|
||||
"query_target_tasks",
|
||||
func(state *schedule.ScheduleState, args map[string]any) (string, *ToolExecutionResult) {
|
||||
return schedule.QueryTargetTasks(state, args), nil
|
||||
},
|
||||
func(input scheduleReadAdapterInput) scheduleread.ReadResultView {
|
||||
return scheduleread.BuildTargetTasksView(scheduleread.TargetTasksViewInput{
|
||||
State: input.State,
|
||||
Observation: input.ObservationText,
|
||||
ArgFields: input.ArgFields,
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// NewGetTaskInfoToolHandler 为 get_task_info 生成结构化读结果。
|
||||
func NewGetTaskInfoToolHandler() ToolHandler {
|
||||
return newScheduleReadToolHandler(
|
||||
"get_task_info",
|
||||
func(state *schedule.ScheduleState, args map[string]any) (string, *ToolExecutionResult) {
|
||||
taskID, ok := schedule.ArgsInt(args, "task_id")
|
||||
if !ok {
|
||||
result := buildScheduleReadFailureResult("get_task_info", args, state, "查询失败:缺少必填参数 task_id。")
|
||||
return "", &result
|
||||
}
|
||||
if state == nil {
|
||||
result := buildScheduleReadFailureResult("get_task_info", args, nil, "查询失败:日程状态为空,无法读取任务详情。")
|
||||
return "", &result
|
||||
}
|
||||
return schedule.GetTaskInfo(state, taskID), nil
|
||||
},
|
||||
func(input scheduleReadAdapterInput) scheduleread.ReadResultView {
|
||||
taskID, _ := schedule.ArgsInt(input.Args, "task_id")
|
||||
return scheduleread.BuildTaskInfoView(scheduleread.TaskInfoViewInput{
|
||||
State: input.State,
|
||||
Observation: input.ObservationText,
|
||||
TaskID: taskID,
|
||||
ArgFields: input.ArgFields,
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// NewGetOverviewToolHandler 为 get_overview 生成结构化读结果。
|
||||
func NewGetOverviewToolHandler() ToolHandler {
|
||||
return newScheduleReadToolHandler(
|
||||
"get_overview",
|
||||
func(state *schedule.ScheduleState, args map[string]any) (string, *ToolExecutionResult) {
|
||||
if state == nil {
|
||||
result := buildScheduleReadFailureResult("get_overview", args, nil, "查看总览失败:日程状态为空,无法读取总览。")
|
||||
return "", &result
|
||||
}
|
||||
return schedule.GetOverview(state), nil
|
||||
},
|
||||
func(input scheduleReadAdapterInput) scheduleread.ReadResultView {
|
||||
return scheduleread.BuildOverviewView(scheduleread.OverviewViewInput{
|
||||
State: input.State,
|
||||
Observation: input.ObservationText,
|
||||
ArgFields: input.ArgFields,
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// NewQueueStatusToolHandler 为 queue_status 生成结构化读结果。
|
||||
func NewQueueStatusToolHandler() ToolHandler {
|
||||
return newScheduleReadToolHandler(
|
||||
"queue_status",
|
||||
func(state *schedule.ScheduleState, args map[string]any) (string, *ToolExecutionResult) {
|
||||
observation := schedule.QueueStatus(state, args)
|
||||
if state == nil {
|
||||
result := buildScheduleReadFailureResult("queue_status", args, nil, observation)
|
||||
return "", &result
|
||||
}
|
||||
return observation, nil
|
||||
},
|
||||
func(input scheduleReadAdapterInput) scheduleread.ReadResultView {
|
||||
return scheduleread.BuildQueueStatusView(scheduleread.QueueStatusViewInput{
|
||||
State: input.State,
|
||||
Observation: input.ObservationText,
|
||||
ArgFields: input.ArgFields,
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// newScheduleReadToolHandler 统一构造父包 read adapter。
|
||||
//
|
||||
// 步骤化说明:
|
||||
// 1. 先执行底层 schedule 工具,拿到原始 observation,保证 LLM 观察文本不变;
|
||||
// 2. 再用 LegacyResultWithState 复用父包状态判断、参数中文展示与默认字段;
|
||||
// 3. 最后调用 schedule_read 子包生成纯展示视图,并包回 ToolExecutionResult。
|
||||
func newScheduleReadToolHandler(
|
||||
toolName string,
|
||||
observe scheduleReadObserveFunc,
|
||||
buildView scheduleReadViewBuilder,
|
||||
) ToolHandler {
|
||||
return func(state *schedule.ScheduleState, args map[string]any) ToolExecutionResult {
|
||||
observation, earlyResult := observe(state, args)
|
||||
if earlyResult != nil {
|
||||
return EnsureToolResultDefaults(*earlyResult, args)
|
||||
}
|
||||
|
||||
legacy := LegacyResultWithState(toolName, args, state, observation)
|
||||
input := scheduleReadAdapterInput{
|
||||
ToolName: toolName,
|
||||
Args: cloneAnyMap(args),
|
||||
State: state,
|
||||
ObservationText: observation,
|
||||
ArgFields: extractScheduleReadArgumentFields(legacy.ArgumentView),
|
||||
}
|
||||
|
||||
view := buildView(input)
|
||||
if normalizeToolStatus(legacy.Status) != ToolStatusDone {
|
||||
view = scheduleread.BuildFailureView(scheduleread.BuildFailureViewInput{
|
||||
ToolName: toolName,
|
||||
Status: legacy.Status,
|
||||
Observation: observation,
|
||||
ArgFields: input.ArgFields,
|
||||
})
|
||||
}
|
||||
return buildScheduleReadExecutionResult(legacy, args, view)
|
||||
}
|
||||
}
|
||||
|
||||
// buildScheduleReadFailureResult 用于底层工具执行前即可确定失败的参数/状态场景。
|
||||
func buildScheduleReadFailureResult(
|
||||
toolName string,
|
||||
args map[string]any,
|
||||
state *schedule.ScheduleState,
|
||||
observation string,
|
||||
) ToolExecutionResult {
|
||||
legacy := LegacyResultWithState(toolName, args, state, observation)
|
||||
view := scheduleread.BuildFailureView(scheduleread.BuildFailureViewInput{
|
||||
ToolName: toolName,
|
||||
Status: ToolStatusFailed,
|
||||
Observation: observation,
|
||||
ArgFields: extractScheduleReadArgumentFields(legacy.ArgumentView),
|
||||
})
|
||||
return buildScheduleReadExecutionResult(legacy, args, view)
|
||||
}
|
||||
|
||||
// buildScheduleReadExecutionResult 负责把子包纯展示视图包回父包统一协议。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只做 ReadResultView -> ToolDisplayView 的协议桥接;
|
||||
// 2. 不改写 ObservationText,确保 execute / SSE / timeline 仍使用同一份 observation;
|
||||
// 3. 错误码与错误文案继续复用父包既有 JSON / 文本解析逻辑。
|
||||
func buildScheduleReadExecutionResult(
|
||||
legacy ToolExecutionResult,
|
||||
args map[string]any,
|
||||
view scheduleread.ReadResultView,
|
||||
) 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"] = strings.TrimSpace(result.ObservationText)
|
||||
}
|
||||
|
||||
viewType := strings.TrimSpace(view.ViewType)
|
||||
if viewType == "" {
|
||||
viewType = scheduleread.ViewTypeReadResult
|
||||
}
|
||||
version := view.Version
|
||||
if version <= 0 {
|
||||
version = scheduleread.ViewVersionReadResult
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// extractScheduleReadArgumentFields 把父包 ToolArgumentView 投影成子包可消费的 KVField。
|
||||
func extractScheduleReadArgumentFields(view *ToolArgumentView) []scheduleread.KVField {
|
||||
if view == nil || view.Expanded == nil {
|
||||
return make([]scheduleread.KVField, 0)
|
||||
}
|
||||
rawFields, exists := view.Expanded["fields"]
|
||||
if !exists {
|
||||
return make([]scheduleread.KVField, 0)
|
||||
}
|
||||
|
||||
fields := make([]scheduleread.KVField, 0)
|
||||
appendField := func(row map[string]any) {
|
||||
label, _ := row["label"].(string)
|
||||
display, _ := row["display"].(string)
|
||||
label = strings.TrimSpace(label)
|
||||
display = strings.TrimSpace(display)
|
||||
if label == "" || display == "" {
|
||||
return
|
||||
}
|
||||
fields = append(fields, scheduleread.BuildKVField(label, display))
|
||||
}
|
||||
|
||||
switch typed := rawFields.(type) {
|
||||
case []map[string]any:
|
||||
for _, row := range typed {
|
||||
appendField(row)
|
||||
}
|
||||
case []any:
|
||||
for _, item := range typed {
|
||||
row, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
appendField(row)
|
||||
}
|
||||
}
|
||||
if len(fields) == 0 {
|
||||
return make([]scheduleread.KVField, 0)
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
func readStringAnyMap(payload map[string]any, key string) (string, bool) {
|
||||
if len(payload) == 0 {
|
||||
return "", false
|
||||
}
|
||||
raw, exists := payload[key]
|
||||
if !exists || raw == nil {
|
||||
return "", false
|
||||
}
|
||||
text := strings.TrimSpace(fmt.Sprintf("%v", raw))
|
||||
if text == "" || text == "<nil>" {
|
||||
return "", false
|
||||
}
|
||||
return text, true
|
||||
}
|
||||
Reference in New Issue
Block a user