package agentmodel import ( "sort" "strings" "time" agentshared "github.com/LoveLosita/smartflow/backend/agent/shared" "github.com/LoveLosita/smartflow/backend/model" ) const ( datetimeLayout = agentshared.MinuteLayout ScheduleRefineDefaultPlanMax = 2 ScheduleRefineDefaultExecuteMax = 24 ScheduleRefineDefaultPerTaskBudget = 4 ScheduleRefineDefaultReplanMax = 2 ScheduleRefineDefaultCompositeRetry = 2 ScheduleRefineDefaultRepairReserve = 1 ) const ( defaultPlanMax = ScheduleRefineDefaultPlanMax defaultExecuteMax = ScheduleRefineDefaultExecuteMax defaultPerTaskBudget = ScheduleRefineDefaultPerTaskBudget defaultReplanMax = ScheduleRefineDefaultReplanMax defaultCompositeRetry = ScheduleRefineDefaultCompositeRetry defaultRepairReserve = ScheduleRefineDefaultRepairReserve ) // RefineContract 表示本轮微调意图契约。 type RefineContract struct { Intent string `json:"intent"` Strategy string `json:"strategy"` HardRequirements []string `json:"hard_requirements"` HardAssertions []RefineAssertion `json:"hard_assertions,omitempty"` KeepRelativeOrder bool `json:"keep_relative_order"` OrderScope string `json:"order_scope"` } // RefineAssertion 表示可由后端直接判定的结构化硬断言。 // // 字段说明: // 1. Metric:断言指标名,例如 source_move_ratio_percent; // 2. Operator:比较操作符,支持 == / <= / >= / between; // 3. Value/Min/Max:阈值; // 4. Week/TargetWeek:可选周次上下文。 type RefineAssertion struct { Metric string `json:"metric"` Operator string `json:"operator"` Value int `json:"value,omitempty"` Min int `json:"min,omitempty"` Max int `json:"max,omitempty"` Week int `json:"week,omitempty"` TargetWeek int `json:"target_week,omitempty"` } // HardCheckReport 表示终审硬校验结果。 type HardCheckReport struct { PhysicsPassed bool `json:"physics_passed"` PhysicsIssues []string `json:"physics_issues,omitempty"` IntentPassed bool `json:"intent_passed"` IntentReason string `json:"intent_reason,omitempty"` IntentUnmet []string `json:"intent_unmet,omitempty"` OrderPassed bool `json:"order_passed"` OrderIssues []string `json:"order_issues,omitempty"` RepairTried bool `json:"repair_tried"` } // ReactRoundObservation 记录每轮 ReAct 的关键观察。 type ReactRoundObservation struct { Round int `json:"round"` GoalCheck string `json:"goal_check,omitempty"` Decision string `json:"decision,omitempty"` ToolName string `json:"tool_name,omitempty"` ToolParams map[string]any `json:"tool_params,omitempty"` ToolSuccess bool `json:"tool_success"` ToolErrorCode string `json:"tool_error_code,omitempty"` ToolResult string `json:"tool_result,omitempty"` Reflect string `json:"reflect,omitempty"` } // PlannerPlan 表示 Planner 生成的阶段执行计划。 type PlannerPlan struct { Summary string `json:"summary"` Steps []string `json:"steps,omitempty"` } // RefineSlicePlan 表示切片节点输出。 type RefineSlicePlan struct { WeekFilter []int `json:"week_filter,omitempty"` SourceDays []int `json:"source_days,omitempty"` TargetDays []int `json:"target_days,omitempty"` ExcludeSections []int `json:"exclude_sections,omitempty"` Reason string `json:"reason,omitempty"` } // RefineObjective 表示"可执行且可校验"的目标约束。 // // 设计说明: // 1. 由 contract/slice 从自然语言编译得到; // 2. 执行阶段(done 收口)与终审阶段(hard_check)共用同一份约束; // 3. 避免"执行逻辑与终审逻辑各说各话"。 type RefineObjective struct { Mode string `json:"mode,omitempty"` // none | move_all | move_ratio SourceWeeks []int `json:"source_weeks,omitempty"` TargetWeeks []int `json:"target_weeks,omitempty"` SourceDays []int `json:"source_days,omitempty"` TargetDays []int `json:"target_days,omitempty"` ExcludeSections []int `json:"exclude_sections,omitempty"` BaselineSourceTaskCount int `json:"baseline_source_task_count,omitempty"` RequiredMoveMin int `json:"required_move_min,omitempty"` RequiredMoveMax int `json:"required_move_max,omitempty"` Reason string `json:"reason,omitempty"` } // ScheduleRefineState 是连续微调图的统一状态。 type ScheduleRefineState struct { // 1) 请求上下文 TraceID string UserID int ConversationID string UserMessage string RequestNow time.Time RequestNowText string // 2) 继承自预览快照的数据 TaskClassIDs []int Constraints []string // InitialHybridEntries 保存本轮微调开始前的基线,用于终审做"前后对比"。 // 说明: // 1. 只读语义,不参与执行期改写; // 2. 终审只基于它判断"来源任务是否真正迁移到目标区域"。 InitialHybridEntries []model.HybridScheduleEntry HybridEntries []model.HybridScheduleEntry AllocatedItems []model.TaskClassItem CandidatePlans []model.UserWeekSchedule // 3) 本轮执行状态 UserIntent string Contract RefineContract PlanMax int PerTaskBudget int ExecuteMax int ReplanMax int // CompositeRetryMax 表示复合路由失败后的最大重试次数(不含首次尝试)。 CompositeRetryMax int PlanUsed int ReplanUsed int MaxRounds int RepairReserve int RoundUsed int ActionLogs []string ConsecutiveFailures int ThinkingBoostArmed bool ObservationHistory []ReactRoundObservation CurrentPlan PlannerPlan BatchMoveAllowed bool // DisableCompositeTools=true 表示已进入 ReAct 兜底,禁止再调用复合工具。 DisableCompositeTools bool // CompositeRouteTried 标记是否尝试过"复合批处理路由"。 CompositeRouteTried bool // CompositeRouteSucceeded 标记复合批处理路由是否已完成"复合分支出站"。 // // 说明: // 1. true 表示当前链路可以跳过 ReAct 兜底,直接进入 hard_check; // 2. 它不等价于"终审已通过",终审是否通过仍以后续 HardCheck 结果为准; // 3. 这样区分是为了避免"复合工具已成功执行,但业务目标要等终审裁决"时被误判为失败。 CompositeRouteSucceeded bool TaskActionUsed map[int]int EntriesVersion int SeenSlotQueries map[string]struct{} // RequiredCompositeTool 表示本轮策略要求"必须至少成功一次"的复合工具。 // 取值约定:"" | "SpreadEven" | "MinContextSwitch"。 RequiredCompositeTool string // CompositeToolCalled 记录复合工具是否至少调用过一次(不区分成功失败)。 CompositeToolCalled map[string]bool // CompositeToolSuccess 记录复合工具是否至少成功过一次。 CompositeToolSuccess map[string]bool SlicePlan RefineSlicePlan Objective RefineObjective WorksetTaskIDs []int WorksetCursor int CurrentTaskID int CurrentTaskAttempt int LastFailedCallSignature string OriginOrderMap map[int]int // 4) 终审状态 HardCheck HardCheckReport // 5) 最终输出 FinalSummary string Completed bool } // NewScheduleRefineState 基于上一版预览快照初始化状态。 // // 职责边界: // 1. 负责初始化预算、上下文字段与可变状态容器; // 2. 负责拷贝 preview 数据,避免跨请求引用污染; // 3. 不负责做任何调度动作。 func NewScheduleRefineState(traceID string, userID int, conversationID string, userMessage string, preview *model.SchedulePlanPreviewCache) *ScheduleRefineState { now := nowToMinute() st := &ScheduleRefineState{ TraceID: strings.TrimSpace(traceID), UserID: userID, ConversationID: strings.TrimSpace(conversationID), UserMessage: strings.TrimSpace(userMessage), RequestNow: now, RequestNowText: now.In(loadLocation()).Format(datetimeLayout), PlanMax: defaultPlanMax, PerTaskBudget: defaultPerTaskBudget, ExecuteMax: defaultExecuteMax, ReplanMax: defaultReplanMax, CompositeRetryMax: defaultCompositeRetry, RepairReserve: defaultRepairReserve, MaxRounds: defaultExecuteMax + defaultRepairReserve, ActionLogs: make([]string, 0, 32), ObservationHistory: make([]ReactRoundObservation, 0, 24), TaskActionUsed: make(map[int]int), SeenSlotQueries: make(map[string]struct{}), OriginOrderMap: make(map[int]int), CompositeToolCalled: map[string]bool{ "SpreadEven": false, "MinContextSwitch": false, }, CompositeToolSuccess: map[string]bool{ "SpreadEven": false, "MinContextSwitch": false, }, CurrentPlan: PlannerPlan{ Summary: "initialized, waiting for planner output", }, SlicePlan: RefineSlicePlan{ Reason: "尚未切片", }, } if preview == nil { return st } st.TaskClassIDs = append([]int(nil), preview.TaskClassIDs...) st.InitialHybridEntries = cloneHybridEntries(preview.HybridEntries) st.HybridEntries = cloneHybridEntries(preview.HybridEntries) st.AllocatedItems = cloneTaskClassItems(preview.AllocatedItems) st.CandidatePlans = cloneWeekSchedules(preview.CandidatePlans) st.OriginOrderMap = buildOriginOrderMap(st.HybridEntries) return st } func loadLocation() *time.Location { return agentshared.ShanghaiLocation() } func nowToMinute() time.Time { return agentshared.NowToMinute() } func cloneHybridEntries(src []model.HybridScheduleEntry) []model.HybridScheduleEntry { return agentshared.CloneHybridEntries(src) } func cloneTaskClassItems(src []model.TaskClassItem) []model.TaskClassItem { return agentshared.CloneTaskClassItems(src) } func cloneWeekSchedules(src []model.UserWeekSchedule) []model.UserWeekSchedule { return agentshared.CloneWeekSchedules(src) } // buildOriginOrderMap 构建 suggested 任务的初始顺序基线(task_item_id -> rank)。 func buildOriginOrderMap(entries []model.HybridScheduleEntry) map[int]int { orderMap := make(map[int]int) if len(entries) == 0 { return orderMap } suggested := make([]model.HybridScheduleEntry, 0, len(entries)) for _, entry := range entries { if isMovableSuggestedTask(entry) { suggested = append(suggested, entry) } } sort.SliceStable(suggested, func(i, j int) bool { left := suggested[i] right := suggested[j] if left.Week != right.Week { return left.Week < right.Week } if left.DayOfWeek != right.DayOfWeek { return left.DayOfWeek < right.DayOfWeek } if left.SectionFrom != right.SectionFrom { return left.SectionFrom < right.SectionFrom } if left.SectionTo != right.SectionTo { return left.SectionTo < right.SectionTo } return left.TaskItemID < right.TaskItemID }) for i, entry := range suggested { orderMap[entry.TaskItemID] = i + 1 } return orderMap } // FinalHardCheckPassed 判断"最终终审"是否整体通过。 // // 职责边界: // 1. 负责聚合 physics/order/intent 三类硬校验结果,给服务层与总结阶段统一复用; // 2. 不负责触发终审,也不负责推送修复动作; // 3. nil state 视为未通过,避免上层把缺失结果误判为成功。 func FinalHardCheckPassed(st *ScheduleRefineState) bool { if st == nil { return false } return st.HardCheck.PhysicsPassed && st.HardCheck.OrderPassed && st.HardCheck.IntentPassed } func isMovableSuggestedTask(entry model.HybridScheduleEntry) bool { if strings.TrimSpace(entry.Status) != "suggested" || entry.TaskItemID <= 0 { return false } if strings.EqualFold(strings.TrimSpace(entry.Type), "course") { return false } return true }