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,345 @@
package agenttools
import (
"fmt"
"strings"
"github.com/LoveLosita/smartflow/backend/services/agent/tools/schedule"
scheduleread "github.com/LoveLosita/smartflow/backend/services/agent/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
}