Files
smartmate/backend/agent/scheduleplan/state.go
Losita d3cec2a5b9 Version: 0.7.0.dev.260319
 feat(agent): 新增智能排程 Agent 全链路 + ReAct 精排引擎

  🏗️ 智能排程 Graph 编排(阶段 1 基础链路)
  - 新增 scheduleplan 包:state / tool / prompt / nodes / runner / graph 六件套
  - 实现 plan → preview → materialize → apply → reflect → finalize 完整图编排
  - 通过函数注入解耦 agent 层与 service 层,避免循环依赖
  - 路由层新增 schedule_plan 动作,复用现有 SSE + 持久化链路

  🧠 ReAct 精排引擎(阶段 1.5 语义化微调)
  - 粗排后构建"混合日程"(既有课程 + 建议任务),统一为 HybridScheduleEntry
  - LLM 开启深度思考,通过 Swap / Move / TimeAvailable / GetAvailableSlots 四个 Tool 在内存中优化任务时间
  - reasoning_content 实时流式推送前端,用户可见 AI 思考过程
  - 精排结果仅预览不落库,向后兼容(未注入依赖时走原有 materialize 路径)

  📝 文档
  - 新增 ReAct 精排引擎决策记录

  ⚠️ 已知问题:深度思考模式耗时较长,超时策略待优化
2026-03-19 23:18:56 +08:00

140 lines
4.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package scheduleplan
import (
"time"
"github.com/LoveLosita/smartflow/backend/model"
)
const (
// schedulePlanTimezoneName 是排程链路默认业务时区。
// 与随口记保持一致,固定东八区,避免容器运行在 UTC 导致"明天/今晚"偏移。
schedulePlanTimezoneName = "Asia/Shanghai"
// schedulePlanDatetimeLayout 是排程链路内部统一的分钟级时间格式。
schedulePlanDatetimeLayout = "2006-01-02 15:04"
)
// SchedulePlanState 是"智能排程"链路在 graph 节点间传递的统一状态容器。
//
// 设计目标:
// 1) 收拢排程请求全生命周期的上下文降低节点间参数散<E695B0><E695A3><EFBFBD>
// 2) 支持"粗排 -> 校验 -> 修补重试 -> 落库"的完整链路追踪;
// 3) 支持连续对话微调:保留上版方案 + 本次约束变更,便于增量重排。
type SchedulePlanState struct {
// ── 基础上下文 ──
TraceID string
UserID int
ConversationID string
RequestNow time.Time
RequestNowText string
// ── plan 节点输出 ──
// UserIntent 是模型对用户排程意图的结构化摘要(如"帮我安排高数复习计划")。
UserIntent string
// Constraints 是用户提出的硬约束列表(如 ["早八不排", "周末休息"])。
Constraints []string
// TaskClassID 是目标任务类 ID由 Extra 字段或模型抽取获得。
TaskClassID int
// Strategy 是排程策略steady/rapid默认 steady。
Strategy string
// ── preview 节点输出 ──
// CandidatePlans 是粗排算法生成的候选方案(展示型结构,供 SSE 推送给前端预览)。
CandidatePlans []model.UserWeekSchedule
// AllocatedItems 是粗排算法已分配的任务项EmbeddedTime 已回填),供 materialize 直接转换。
AllocatedItems []model.TaskClassItem
// ── ReAct 精排阶段 ──
// HybridEntries 是混合日程条目列表包含既有日程existing和粗排建议suggested
// ReAct 工具直接在此切片上操作(内存修改,不涉及 DB
HybridEntries []model.HybridScheduleEntry
// ReactRound 当前 ReAct 循环轮次。
ReactRound int
// ReactMaxRound 最大循环轮次(建议 3
ReactMaxRound int
// ReactSummary LLM 输出的优化摘要。
ReactSummary string
// ReactDone 标记 ReAct 是否已完成。
ReactDone bool
// ── materialize 节点输出 ──
// ApplyRequest 是转换后的落库请求体。
ApplyRequest *model.UserInsertTaskClassItemToScheduleRequestBatch
// ── apply 节点输出 ──
// Applied 标记是否落库成功。
Applied bool
// ApplyError 记录落库失败的错误信息,供 reflect 节点分析。
ApplyError string
// ── reflect 节点状态 ──
// RetryCount 记录当前重试次数。
RetryCount int
// MaxRetry 是最大重试次数(建议 = 2
MaxRetry int
// ReflectAction 记录模型给出的修补动作retry_with_patch / partial_apply / give_up
ReflectAction string
// ── 连续对话微调 ──
// PreviousPlanJSON 是上一版已落库方案的 JSON 序列化,用于增量微调。
// 从对话历史中提取,不做持久化。
PreviousPlanJSON string
// IsAdjustment 标记本次是否为微调请求(而非全新排程)。
IsAdjustment bool
// ── 最终输出 ──
// FinalSummary 是 graph 最终给用户的回复文案。
FinalSummary string
// Completed 标记整个排程链路是否成功完成。
Completed bool
}
// NewSchedulePlanState 创建排程状态对象并初始化默认值。
func NewSchedulePlanState(traceID string, userID int, conversationID string) *SchedulePlanState {
now := schedulePlanNowToMinute()
return &SchedulePlanState{
TraceID: traceID,
UserID: userID,
ConversationID: conversationID,
RequestNow: now,
RequestNowText: now.In(schedulePlanLocation()).Format(schedulePlanDatetimeLayout),
MaxRetry: 2,
Strategy: "steady",
ReactMaxRound: 3,
}
}
// CanRetry 判断当前是否还能继续重试落库。
func (s *SchedulePlanState) CanRetry() bool {
return s.RetryCount < s.MaxRetry
}
// RecordApplyError 记录一次落库失败。
func (s *SchedulePlanState) RecordApplyError(errMsg string) {
s.RetryCount++
s.ApplyError = errMsg
}
// schedulePlanLocation 返回排程链路使用的业务时区。
func schedulePlanLocation() *time.Location {
loc, err := time.LoadLocation(schedulePlanTimezoneName)
if err != nil {
return time.Local
}
return loc
}
// schedulePlanNowToMinute 返回当前时间并截断到分钟级。
func schedulePlanNowToMinute() time.Time {
return time.Now().In(schedulePlanLocation()).Truncate(time.Minute)
}