Version: 0.7.9.dev.260326
后端: 1.把最后一块拼图:schedule_refine也搬迁到了agent2,此时agent已经完全解耦。但是它没融入新架构,Codex只尝试把它调整了一部分,回退了一些错误的更改,保持着现在的可运行状态。下次继续改。 2.agent目录先保留,直到refine彻底融入新架构。 3.改善Codex主导的新史山结构:node文件夹里面大量文件,转而改成了module.go+module_tool.go的双文件格局,极大提升架构整洁度和代码可读性。 前端: 1.新开了日历界面,正在保持往前推进。做了很多更改,感觉越来越好了。
This commit is contained in:
@@ -7,21 +7,20 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/LoveLosita/smartflow/backend/agent/scheduleplan"
|
||||
agentmodel "github.com/LoveLosita/smartflow/backend/agent2/model"
|
||||
agentshared "github.com/LoveLosita/smartflow/backend/agent2/shared"
|
||||
"github.com/LoveLosita/smartflow/backend/model"
|
||||
"github.com/LoveLosita/smartflow/backend/respond"
|
||||
)
|
||||
|
||||
// saveSchedulePlanPreview 把排程结果以结构化 JSON 快照写入 Redis。
|
||||
// saveSchedulePlanPreview 鎶婃帓绋嬬粨鏋滀互缁撴瀯鍖?JSON 蹇収鍐欏叆 Redis銆?
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 负责把 finalState 中的 summary + candidate_plans 收敛为缓存 DTO;
|
||||
// 2. 负责以“失败不阻断聊天主链路”的策略执行写入;
|
||||
// 3. 不负责 SSE 返回协议,不负责数据库落库。
|
||||
func (s *AgentService) saveSchedulePlanPreview(ctx context.Context, userID int, chatID string, finalState *scheduleplan.SchedulePlanState) {
|
||||
// 1. 基础前置校验:state 为空时直接返回,避免写入半成品快照。
|
||||
// 鑱岃矗杈圭晫锛?
|
||||
// 1. 璐熻矗鎶?finalState 涓殑 summary + candidate_plans 鏀舵暃涓虹紦瀛?DTO锛?
|
||||
// 2. 璐熻矗浠モ€滃け璐ヤ笉闃绘柇鑱婂ぉ涓婚摼璺€濈殑绛栫暐鎵ц鍐欏叆锛?
|
||||
// 3. 涓嶈礋璐?SSE 杩斿洖鍗忚锛屼笉璐熻矗鏁版嵁搴撹惤搴撱€?
|
||||
func (s *AgentService) saveSchedulePlanPreview(ctx context.Context, userID int, chatID string, finalState *agentmodel.SchedulePlanState) {
|
||||
// 1. 鍩虹鍓嶇疆鏍¢獙锛歴tate 涓虹┖鏃剁洿鎺ヨ繑鍥烇紝閬垮厤鍐欏叆鍗婃垚鍝佸揩鐓с€?
|
||||
if s == nil || finalState == nil {
|
||||
return
|
||||
}
|
||||
@@ -30,10 +29,10 @@ func (s *AgentService) saveSchedulePlanPreview(ctx context.Context, userID int,
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 组装缓存快照:
|
||||
// 2.1 summary 优先取 final summary,空值时使用统一兜底文案;
|
||||
// 2.2 candidate_plans 做切片拷贝,避免后续引用共享导致意外覆盖;
|
||||
// 2.3 generated_at 用于前端判断“当前预览的新鲜度”。
|
||||
// 2. 缁勮缂撳瓨蹇収锛?
|
||||
// 2.1 summary 浼樺厛鍙?final summary锛岀┖鍊兼椂浣跨敤缁熶竴鍏滃簳鏂囨锛?
|
||||
// 2.2 candidate_plans 鍋氬垏鐗囨嫹璐濓紝閬垮厤鍚庣画寮曠敤鍏变韩瀵艰嚧鎰忓瑕嗙洊锛?
|
||||
// 2.3 generated_at 鐢ㄤ簬鍓嶇鍒ゆ柇鈥滃綋鍓嶉瑙堢殑鏂伴矞搴︹€濄€?
|
||||
summary := strings.TrimSpace(finalState.FinalSummary)
|
||||
if summary == "" {
|
||||
summary = "排程流程已完成,但未生成结果摘要。"
|
||||
@@ -50,35 +49,35 @@ func (s *AgentService) saveSchedulePlanPreview(ctx context.Context, userID int,
|
||||
GeneratedAt: time.Now(),
|
||||
}
|
||||
|
||||
// 3. 调用目的:先写 Redis 预览,保证前端查询接口能快速读取结构化结果。
|
||||
// 3.1 Redis 是“快路径”;失败只记录日志,不中断主链路;
|
||||
// 3.2 失败兜底由后续 MySQL 快照承接。
|
||||
// 3. 璋冪敤鐩殑锛氬厛鍐?Redis 棰勮锛屼繚璇佸墠绔煡璇㈡帴鍙h兘蹇€熻鍙栫粨鏋勫寲缁撴灉銆?
|
||||
// 3.1 Redis 鏄€滃揩璺緞鈥濓紱澶辫触鍙褰曟棩蹇楋紝涓嶄腑鏂富閾捐矾锛?
|
||||
// 3.2 澶辫触鍏滃簳鐢卞悗缁?MySQL 蹇収鎵挎帴銆?
|
||||
if s.cacheDAO != nil {
|
||||
if err := s.cacheDAO.SetSchedulePlanPreviewToCache(ctx, userID, normalizedChatID, preview); err != nil {
|
||||
log.Printf("写入排程预览缓存失败 chat_id=%s: %v", normalizedChatID, err)
|
||||
log.Printf("鍐欏叆鎺掔▼棰勮缂撳瓨澶辫触 chat_id=%s: %v", normalizedChatID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 调用目的:同步写 MySQL 状态快照,保证 Redis 失效后仍可连续微调。
|
||||
// 4.1 这里采用“同步写库”而不是 outbox:因为下一轮微调要强实时读取;
|
||||
// 4.2 快照写入失败只打日志,不阻断本轮用户回复,避免体验抖动;
|
||||
// 4.3 revision 自增由 DAO 的 upsert 冲突更新负责。
|
||||
// 4. 璋冪敤鐩殑锛氬悓姝ュ啓 MySQL 鐘舵€佸揩鐓э紝淇濊瘉 Redis 澶辨晥鍚庝粛鍙繛缁井璋冦€?
|
||||
// 4.1 杩欓噷閲囩敤鈥滃悓姝ュ啓搴撯€濊€屼笉鏄?outbox锛氬洜涓轰笅涓€杞井璋冭寮哄疄鏃惰鍙栵紱
|
||||
// 4.2 蹇収鍐欏叆澶辫触鍙墦鏃ュ織锛屼笉闃绘柇鏈疆鐢ㄦ埛鍥炲锛岄伩鍏嶄綋楠屾姈鍔紱
|
||||
// 4.3 revision 鑷鐢?DAO 鐨?upsert 鍐茬獊鏇存柊璐熻矗銆?
|
||||
if s.repo != nil {
|
||||
snapshot := buildSchedulePlanSnapshotFromState(userID, normalizedChatID, finalState)
|
||||
if err := s.repo.UpsertScheduleStateSnapshot(ctx, snapshot); err != nil {
|
||||
log.Printf("写入排程状态快照失败 chat_id=%s: %v", normalizedChatID, err)
|
||||
log.Printf("鍐欏叆鎺掔▼鐘舵€佸揩鐓уけ璐?chat_id=%s: %v", normalizedChatID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// saveSchedulePlanPreviewAgent2 把 agent2 的 schedule_plan 结果写入 Redis 预览与 MySQL 快照。
|
||||
// saveSchedulePlanPreviewAgent2 鎶?agent2 鐨?schedule_plan 缁撴灉鍐欏叆 Redis 棰勮涓?MySQL 蹇収銆?
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 负责承接“新 agent2 首次排程链路”的最终状态;
|
||||
// 2. 负责沿用现有预览缓存/状态快照协议,保证查询接口与 refine 读取逻辑不需要跟着重写;
|
||||
// 3. 不负责 refine 状态转换,refine 仍继续走旧链路的 saveSchedulePlanPreview。
|
||||
// 鑱岃矗杈圭晫锛?
|
||||
// 1. 璐熻矗鎵挎帴鈥滄柊 agent2 棣栨鎺掔▼閾捐矾鈥濈殑鏈€缁堢姸鎬侊紱
|
||||
// 2. 璐熻矗娌跨敤鐜版湁棰勮缂撳瓨/鐘舵€佸揩鐓у崗璁紝淇濊瘉鏌ヨ鎺ュ彛涓?refine 璇诲彇閫昏緫涓嶉渶瑕佽窡鐫€閲嶅啓锛?
|
||||
// 3. 涓嶈礋璐?refine 鐘舵€佽浆鎹紝refine 浠嶇户缁蛋鏃ч摼璺殑 saveSchedulePlanPreview銆?
|
||||
func (s *AgentService) saveSchedulePlanPreviewAgent2(ctx context.Context, userID int, chatID string, finalState *agentmodel.SchedulePlanState) {
|
||||
// 1. 基础前置校验:state 为空时直接返回,避免写入半成品快照。
|
||||
// 1. 鍩虹鍓嶇疆鏍¢獙锛歴tate 涓虹┖鏃剁洿鎺ヨ繑鍥烇紝閬垮厤鍐欏叆鍗婃垚鍝佸揩鐓с€?
|
||||
if s == nil || finalState == nil {
|
||||
return
|
||||
}
|
||||
@@ -87,10 +86,10 @@ func (s *AgentService) saveSchedulePlanPreviewAgent2(ctx context.Context, userID
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 组装缓存快照。
|
||||
// 2.1 summary 优先取 final summary,空值时使用统一兜底文案;
|
||||
// 2.2 candidate_plans / hybrid_entries / allocated_items 统一深拷贝,避免缓存与 graph state 共用底层切片;
|
||||
// 2.3 generated_at 用于前端判断“当前预览是否为最新方案”。
|
||||
// 2. 缁勮缂撳瓨蹇収銆?
|
||||
// 2.1 summary 浼樺厛鍙?final summary锛岀┖鍊兼椂浣跨敤缁熶竴鍏滃簳鏂囨锛?
|
||||
// 2.2 candidate_plans / hybrid_entries / allocated_items 缁熶竴娣辨嫹璐濓紝閬垮厤缂撳瓨涓?graph state 鍏辩敤搴曞眰鍒囩墖锛?
|
||||
// 2.3 generated_at 鐢ㄤ簬鍓嶇鍒ゆ柇鈥滃綋鍓嶉瑙堟槸鍚︿负鏈€鏂版柟妗堚€濄€?
|
||||
summary := strings.TrimSpace(finalState.FinalSummary)
|
||||
if summary == "" {
|
||||
summary = "排程流程已完成,但未生成结果摘要。"
|
||||
@@ -107,34 +106,34 @@ func (s *AgentService) saveSchedulePlanPreviewAgent2(ctx context.Context, userID
|
||||
GeneratedAt: time.Now(),
|
||||
}
|
||||
|
||||
// 3. 先写 Redis 预览,保证前端查询接口能立即读取结构化结果。
|
||||
// 3.1 Redis 是“快路径”;
|
||||
// 3.2 失败只记录日志,不中断聊天主链路。
|
||||
// 3. 鍏堝啓 Redis 棰勮锛屼繚璇佸墠绔煡璇㈡帴鍙h兘绔嬪嵆璇诲彇缁撴瀯鍖栫粨鏋溿€?
|
||||
// 3.1 Redis 鏄€滃揩璺緞鈥濓紱
|
||||
// 3.2 澶辫触鍙褰曟棩蹇楋紝涓嶄腑鏂亰澶╀富閾捐矾銆?
|
||||
if s.cacheDAO != nil {
|
||||
if err := s.cacheDAO.SetSchedulePlanPreviewToCache(ctx, userID, normalizedChatID, preview); err != nil {
|
||||
log.Printf("写入排程预览缓存失败 chat_id=%s: %v", normalizedChatID, err)
|
||||
log.Printf("鍐欏叆鎺掔▼棰勮缂撳瓨澶辫触 chat_id=%s: %v", normalizedChatID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 同步写 MySQL 快照,保证 Redis 失效后仍能恢复预览与连续微调上下文。
|
||||
// 4.1 这里继续保持“同步写库”策略,因为下一轮微调对快照读取是强实时依赖;
|
||||
// 4.2 写库失败只打日志,不阻断本轮给用户的文本回复。
|
||||
// 4. 鍚屾鍐?MySQL 蹇収锛屼繚璇?Redis 澶辨晥鍚庝粛鑳芥仮澶嶉瑙堜笌杩炵画寰皟涓婁笅鏂囥€?
|
||||
// 4.1 杩欓噷缁х画淇濇寔鈥滃悓姝ュ啓搴撯€濈瓥鐣ワ紝鍥犱负涓嬩竴杞井璋冨蹇収璇诲彇鏄己瀹炴椂渚濊禆锛?
|
||||
// 4.2 鍐欏簱澶辫触鍙墦鏃ュ織锛屼笉闃绘柇鏈疆缁欑敤鎴风殑鏂囨湰鍥炲銆?
|
||||
if s.repo != nil {
|
||||
snapshot := buildSchedulePlanSnapshotFromAgent2State(userID, normalizedChatID, finalState)
|
||||
if err := s.repo.UpsertScheduleStateSnapshot(ctx, snapshot); err != nil {
|
||||
log.Printf("写入排程状态快照失败 chat_id=%s: %v", normalizedChatID, err)
|
||||
log.Printf("鍐欏叆鎺掔▼鐘舵€佸揩鐓уけ璐?chat_id=%s: %v", normalizedChatID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetSchedulePlanPreview 按 conversation_id 读取结构化排程预览。
|
||||
// GetSchedulePlanPreview 鎸?conversation_id 璇诲彇缁撴瀯鍖栨帓绋嬮瑙堛€?
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 负责参数归一化、缓存读取与会话归属校验;
|
||||
// 2. 负责把缓存 DTO 转成 API 响应 DTO;
|
||||
// 3. 不负责触发排程,不负责补算缓存。
|
||||
// 鑱岃矗杈圭晫锛?
|
||||
// 1. 璐熻矗鍙傛暟褰掍竴鍖栥€佺紦瀛樿鍙栦笌浼氳瘽褰掑睘鏍¢獙锛?
|
||||
// 2. 璐熻矗鎶婄紦瀛?DTO 杞垚 API 鍝嶅簲 DTO锛?
|
||||
// 3. 涓嶈礋璐hЕ鍙戞帓绋嬶紝涓嶈礋璐hˉ绠楃紦瀛樸€?
|
||||
func (s *AgentService) GetSchedulePlanPreview(ctx context.Context, userID int, chatID string) (*model.GetSchedulePlanPreviewResponse, error) {
|
||||
// 1. 参数校验:conversation_id 为空直接返回参数错误,避免无效 Redis 请求。
|
||||
// 1. 鍙傛暟鏍¢獙锛歝onversation_id 涓虹┖鐩存帴杩斿洖鍙傛暟閿欒锛岄伩鍏嶆棤鏁?Redis 璇锋眰銆?
|
||||
normalizedChatID := strings.TrimSpace(chatID)
|
||||
if normalizedChatID == "" {
|
||||
return nil, respond.MissingParam
|
||||
@@ -143,10 +142,10 @@ func (s *AgentService) GetSchedulePlanPreview(ctx context.Context, userID int, c
|
||||
return nil, errors.New("agent service is not initialized")
|
||||
}
|
||||
|
||||
// 2. 查询缓存并校验归属:
|
||||
// 2.1 缓存未命中:统一返回“预览不存在/已过期”;
|
||||
// 2.2 命中但 user_id 不一致:按未命中处理,避免泄露他人会话信息;
|
||||
// 2.3 失败兜底:缓存读异常直接上抛,由 API 层统一错误处理。
|
||||
// 2. 鏌ヨ缂撳瓨骞舵牎楠屽綊灞烇細
|
||||
// 2.1 缂撳瓨鏈懡涓細缁熶竴杩斿洖鈥滈瑙堜笉瀛樺湪/宸茶繃鏈熲€濓紱
|
||||
// 2.2 鍛戒腑浣?user_id 涓嶄竴鑷达細鎸夋湭鍛戒腑澶勭悊锛岄伩鍏嶆硠闇蹭粬浜轰細璇濅俊鎭紱
|
||||
// 2.3 澶辫触鍏滃簳锛氱紦瀛樿寮傚父鐩存帴涓婃姏锛岀敱 API 灞傜粺涓€閿欒澶勭悊銆?
|
||||
if s.cacheDAO != nil {
|
||||
preview, err := s.cacheDAO.GetSchedulePlanPreviewFromCache(ctx, userID, normalizedChatID)
|
||||
if err != nil {
|
||||
@@ -170,10 +169,10 @@ func (s *AgentService) GetSchedulePlanPreview(ctx context.Context, userID int, c
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Redis 未命中时回落 MySQL 快照:
|
||||
// 3.1 读取成功后直接返回,避免用户看到“预览不存在”的假阴性;
|
||||
// 3.2 若本次命中 DB 且缓存可用,则顺手回填 Redis,提升后续命中率;
|
||||
// 3.3 DB 也未命中时再返回 not found。
|
||||
// 3. Redis 鏈懡涓椂鍥炶惤 MySQL 蹇収锛?
|
||||
// 3.1 璇诲彇鎴愬姛鍚庣洿鎺ヨ繑鍥烇紝閬垮厤鐢ㄦ埛鐪嬪埌鈥滈瑙堜笉瀛樺湪鈥濈殑鍋囬槾鎬э紱
|
||||
// 3.2 鑻ユ湰娆″懡涓?DB 涓旂紦瀛樺彲鐢紝鍒欓『鎵嬪洖濉?Redis锛屾彁鍗囧悗缁懡涓巼锛?
|
||||
// 3.3 DB 涔熸湭鍛戒腑鏃跺啀杩斿洖 not found銆?
|
||||
if s.repo != nil {
|
||||
snapshot, err := s.repo.GetScheduleStateSnapshot(ctx, userID, normalizedChatID)
|
||||
if err != nil {
|
||||
@@ -184,7 +183,7 @@ func (s *AgentService) GetSchedulePlanPreview(ctx context.Context, userID int, c
|
||||
if s.cacheDAO != nil {
|
||||
cachePreview := snapshotToSchedulePlanPreviewCache(snapshot)
|
||||
if setErr := s.cacheDAO.SetSchedulePlanPreviewToCache(ctx, userID, normalizedChatID, cachePreview); setErr != nil {
|
||||
log.Printf("回填排程预览缓存失败 chat_id=%s: %v", normalizedChatID, setErr)
|
||||
log.Printf("鍥炲~鎺掔▼棰勮缂撳瓨澶辫触 chat_id=%s: %v", normalizedChatID, setErr)
|
||||
}
|
||||
}
|
||||
return response, nil
|
||||
@@ -193,28 +192,28 @@ func (s *AgentService) GetSchedulePlanPreview(ctx context.Context, userID int, c
|
||||
return nil, respond.SchedulePlanPreviewNotFound
|
||||
}
|
||||
|
||||
// cloneWeekSchedules 对周视图排程结果做深拷贝,避免切片引用共享。
|
||||
// cloneWeekSchedules 瀵瑰懆瑙嗗浘鎺掔▼缁撴灉鍋氭繁鎷疯礉锛岄伩鍏嶅垏鐗囧紩鐢ㄥ叡浜€?
|
||||
func cloneWeekSchedules(src []model.UserWeekSchedule) []model.UserWeekSchedule {
|
||||
return agentshared.CloneWeekSchedules(src)
|
||||
}
|
||||
|
||||
// cloneHybridEntries 深拷贝混合条目切片,避免缓存/状态之间相互污染。
|
||||
// cloneHybridEntries 娣辨嫹璐濇贩鍚堟潯鐩垏鐗囷紝閬垮厤缂撳瓨/鐘舵€佷箣闂寸浉浜掓薄鏌撱€?
|
||||
func cloneHybridEntries(src []model.HybridScheduleEntry) []model.HybridScheduleEntry {
|
||||
return agentshared.CloneHybridEntries(src)
|
||||
}
|
||||
|
||||
// cloneTaskClassItems 深拷贝任务块切片(包含指针字段),避免跨请求引用共享。
|
||||
// cloneTaskClassItems 娣辨嫹璐濅换鍔″潡鍒囩墖锛堝寘鍚寚閽堝瓧娈碉級锛岄伩鍏嶈法璇锋眰寮曠敤鍏变韩銆?
|
||||
func cloneTaskClassItems(src []model.TaskClassItem) []model.TaskClassItem {
|
||||
return agentshared.CloneTaskClassItems(src)
|
||||
}
|
||||
|
||||
// buildSchedulePlanSnapshotFromState 把 graph 运行结果映射成可持久化快照 DTO。
|
||||
// buildSchedulePlanSnapshotFromState 鎶?graph 杩愯缁撴灉鏄犲皠鎴愬彲鎸佷箙鍖栧揩鐓?DTO銆?
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 负责字段映射与深拷贝,避免跨层共享可变切片;
|
||||
// 2. 负责补齐 state_version 默认值;
|
||||
// 3. 不负责数据库写入(写入由 DAO 承担)。
|
||||
func buildSchedulePlanSnapshotFromState(userID int, conversationID string, st *scheduleplan.SchedulePlanState) *model.SchedulePlanStateSnapshot {
|
||||
// 鑱岃矗杈圭晫锛?
|
||||
// 1. 璐熻矗瀛楁鏄犲皠涓庢繁鎷疯礉锛岄伩鍏嶈法灞傚叡浜彲鍙樺垏鐗囷紱
|
||||
// 2. 璐熻矗琛ラ綈 state_version 榛樿鍊硷紱
|
||||
// 3. 涓嶈礋璐f暟鎹簱鍐欏叆锛堝啓鍏ョ敱 DAO 鎵挎媴锛夈€?
|
||||
func buildSchedulePlanSnapshotFromState(userID int, conversationID string, st *agentmodel.SchedulePlanState) *model.SchedulePlanStateSnapshot {
|
||||
if st == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -237,12 +236,12 @@ func buildSchedulePlanSnapshotFromState(userID int, conversationID string, st *s
|
||||
}
|
||||
}
|
||||
|
||||
// buildSchedulePlanSnapshotFromAgent2State 把 agent2 的排程状态映射成可持久化快照 DTO。
|
||||
// buildSchedulePlanSnapshotFromAgent2State 鎶?agent2 鐨勬帓绋嬬姸鎬佹槧灏勬垚鍙寔涔呭寲蹇収 DTO銆?
|
||||
//
|
||||
// 调用目的:
|
||||
// 1. 这轮只迁移 schedule_plan,不动 refine;
|
||||
// 2. 因此 preview/快照协议继续复用老结构,但要补一个“agent2 state -> snapshot DTO”的映射层;
|
||||
// 3. 这样可以做到:计划创建链路切到 agent2,而 refine / 预览查询链路暂时无需大改。
|
||||
// 璋冪敤鐩殑锛?
|
||||
// 1. 杩欒疆鍙縼绉?schedule_plan锛屼笉鍔?refine锛?
|
||||
// 2. 鍥犳 preview/蹇収鍗忚缁х画澶嶇敤鑰佺粨鏋勶紝浣嗚琛ヤ竴涓€渁gent2 state -> snapshot DTO鈥濈殑鏄犲皠灞傦紱
|
||||
// 3. 杩欐牱鍙互鍋氬埌锛氳鍒掑垱寤洪摼璺垏鍒?agent2锛岃€?refine / 棰勮鏌ヨ閾捐矾鏆傛椂鏃犻渶澶ф敼銆?
|
||||
func buildSchedulePlanSnapshotFromAgent2State(userID int, conversationID string, st *agentmodel.SchedulePlanState) *model.SchedulePlanStateSnapshot {
|
||||
if st == nil {
|
||||
return nil
|
||||
@@ -266,7 +265,7 @@ func buildSchedulePlanSnapshotFromAgent2State(userID int, conversationID string,
|
||||
}
|
||||
}
|
||||
|
||||
// snapshotToSchedulePlanPreviewCache 把 MySQL 快照转换为 Redis 预览缓存结构。
|
||||
// snapshotToSchedulePlanPreviewCache 鎶?MySQL 蹇収杞崲涓?Redis 棰勮缂撳瓨缁撴瀯銆?
|
||||
func snapshotToSchedulePlanPreviewCache(snapshot *model.SchedulePlanStateSnapshot) *model.SchedulePlanPreviewCache {
|
||||
if snapshot == nil {
|
||||
return nil
|
||||
@@ -292,7 +291,7 @@ func snapshotToSchedulePlanPreviewCache(snapshot *model.SchedulePlanStateSnapsho
|
||||
}
|
||||
}
|
||||
|
||||
// snapshotToSchedulePlanPreviewResponse 把 MySQL 快照转换为查询接口响应。
|
||||
// snapshotToSchedulePlanPreviewResponse 鎶?MySQL 蹇収杞崲涓烘煡璇㈡帴鍙e搷搴斻€?
|
||||
func snapshotToSchedulePlanPreviewResponse(snapshot *model.SchedulePlanStateSnapshot) *model.GetSchedulePlanPreviewResponse {
|
||||
if snapshot == nil {
|
||||
return nil
|
||||
|
||||
@@ -6,20 +6,21 @@ import (
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/LoveLosita/smartflow/backend/agent/scheduleplan"
|
||||
"github.com/LoveLosita/smartflow/backend/agent/schedulerefine"
|
||||
agentgraph "github.com/LoveLosita/smartflow/backend/agent2/graph"
|
||||
agentmodel "github.com/LoveLosita/smartflow/backend/agent2/model"
|
||||
agentnode "github.com/LoveLosita/smartflow/backend/agent2/node"
|
||||
"github.com/LoveLosita/smartflow/backend/model"
|
||||
"github.com/LoveLosita/smartflow/backend/respond"
|
||||
"github.com/cloudwego/eino-ext/components/model/ark"
|
||||
)
|
||||
|
||||
// runScheduleRefineFlow 执行“连续对话微调排程”分支。
|
||||
// runScheduleRefineFlow 鎵ц鈥滆繛缁璇濆井璋冩帓绋嬧€濆垎鏀€?
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 负责读取“上一版排程预览快照”(优先 Redis,缺失再回源 MySQL);
|
||||
// 2. 负责调用独立 schedulerefine 图链路完成本轮微调;
|
||||
// 3. 负责把微调结果回写预览缓存与状态快照,供后续继续微调;
|
||||
// 4. 不负责聊天消息持久化(消息持久化由 AgentChat 主链路统一处理)。
|
||||
// 鑱岃矗杈圭晫锛?
|
||||
// 1. 璐熻矗璇诲彇鈥滀笂涓€鐗堟帓绋嬮瑙堝揩鐓р€濓紙浼樺厛 Redis锛岀己澶卞啀鍥炴簮 MySQL锛夛紱
|
||||
// 2. 璐熻矗璋冪敤鐙珛 schedulerefine 鍥鹃摼璺畬鎴愭湰杞井璋冿紱
|
||||
// 3. 璐熻矗鎶婂井璋冪粨鏋滃洖鍐欓瑙堢紦瀛樹笌鐘舵€佸揩鐓э紝渚涘悗缁户缁井璋冿紱
|
||||
// 4. 涓嶈礋璐h亰澶╂秷鎭寔涔呭寲锛堟秷鎭寔涔呭寲鐢?AgentChat 涓婚摼璺粺涓€澶勭悊锛夈€?
|
||||
func (s *AgentService) runScheduleRefineFlow(
|
||||
ctx context.Context,
|
||||
selectedModel *ark.ChatModel,
|
||||
@@ -34,24 +35,24 @@ func (s *AgentService) runScheduleRefineFlow(
|
||||
_ = outChan
|
||||
_ = modelName
|
||||
|
||||
// 1. 依赖预检:模型为空时无法执行任何节点,直接失败避免空指针。
|
||||
// 1. 渚濊禆棰勬锛氭ā鍨嬩负绌烘椂鏃犳硶鎵ц浠讳綍鑺傜偣锛岀洿鎺ュけ璐ラ伩鍏嶇┖鎸囬拡銆?
|
||||
if selectedModel == nil {
|
||||
return "", errors.New("schedule refine model is nil")
|
||||
}
|
||||
|
||||
emitStage("schedule_refine.context.loading", "正在加载上一版排程上下文。")
|
||||
|
||||
// 2. 先查 Redis 预览快照,保证热路径低延迟。
|
||||
// 2.1 如果 Redis 未命中,再回源 MySQL 快照兜底;
|
||||
// 2.2 如果两者都没有,说明当前会话没有可微调基础,直接返回业务错误。
|
||||
// 2. 鍏堟煡 Redis 棰勮蹇収锛屼繚璇佺儹璺緞浣庡欢杩熴€?
|
||||
// 2.1 濡傛灉 Redis 鏈懡涓紝鍐嶅洖婧?MySQL 蹇収鍏滃簳锛?
|
||||
// 2.2 濡傛灉涓よ€呴兘娌℃湁锛岃鏄庡綋鍓嶄細璇濇病鏈夊彲寰皟鍩虹锛岀洿鎺ヨ繑鍥炰笟鍔¢敊璇€?
|
||||
preview := s.loadSchedulePreviewContext(ctx, userID, chatID)
|
||||
if preview == nil {
|
||||
return "", respond.SchedulePlanPreviewNotFound
|
||||
}
|
||||
|
||||
// 3. 初始化微调状态并运行独立图。
|
||||
state := schedulerefine.NewScheduleRefineState(traceID, userID, chatID, userMessage, preview)
|
||||
finalState, runErr := schedulerefine.RunScheduleRefineGraph(ctx, schedulerefine.ScheduleRefineGraphRunInput{
|
||||
// 3. 鍒濆鍖栧井璋冪姸鎬佸苟杩愯鐙珛鍥俱€?
|
||||
state := agentnode.NewScheduleRefineState(traceID, userID, chatID, userMessage, preview)
|
||||
finalState, runErr := agentgraph.RunScheduleRefineGraph(ctx, agentnode.ScheduleRefineGraphRunInput{
|
||||
Model: selectedModel,
|
||||
State: state,
|
||||
EmitStage: emitStage,
|
||||
@@ -63,12 +64,12 @@ func (s *AgentService) runScheduleRefineFlow(
|
||||
return "", errors.New("schedule refine graph returned nil state")
|
||||
}
|
||||
|
||||
// 4. 调用目的:
|
||||
// 4.1 saveSchedulePlanPreview 目前是“预览缓存 + MySQL 快照”的统一写入口;
|
||||
// 4.2 这里把 refine state 映射为 scheduleplan state,复用已有落盘链路;
|
||||
// 4.3 但若是“独立复合分支已出站、终审仍失败”,则不覆盖上一版预览,避免外部误以为新方案已验证通过。
|
||||
// 4. 璋冪敤鐩殑锛?
|
||||
// 4.1 saveSchedulePlanPreview 鐩墠鏄€滈瑙堢紦瀛?+ MySQL 蹇収鈥濈殑缁熶竴鍐欏叆鍙o紱
|
||||
// 4.2 杩欓噷鎶?refine state 鏄犲皠涓?scheduleplan state锛屽鐢ㄥ凡鏈夎惤鐩橀摼璺紱
|
||||
// 4.3 浣嗚嫢鏄€滅嫭绔嬪鍚堝垎鏀凡鍑虹珯銆佺粓瀹′粛澶辫触鈥濓紝鍒欎笉瑕嗙洊涓婁竴鐗堥瑙堬紝閬垮厤澶栭儴璇互涓烘柊鏂规宸查獙璇侀€氳繃銆?
|
||||
if shouldPersistScheduleRefinePreview(finalState) {
|
||||
s.saveSchedulePlanPreview(ctx, userID, chatID, convertRefineStateToPlanState(finalState))
|
||||
s.saveSchedulePlanPreviewAgent2(ctx, userID, chatID, convertRefineStateToPlanState(finalState))
|
||||
} else {
|
||||
emitStage("schedule_refine.preview.skipped", "复合分支终审未通过,本轮结果不覆盖上一版预览。")
|
||||
}
|
||||
@@ -80,13 +81,13 @@ func (s *AgentService) runScheduleRefineFlow(
|
||||
return reply, nil
|
||||
}
|
||||
|
||||
// loadSchedulePreviewContext 读取“可用于连续微调”的排程上下文快照。
|
||||
// loadSchedulePreviewContext 璇诲彇鈥滃彲鐢ㄤ簬杩炵画寰皟鈥濈殑鎺掔▼涓婁笅鏂囧揩鐓с€?
|
||||
//
|
||||
// 步骤化说明:
|
||||
// 1. 先查 Redis:命中则直接返回,时延最小;
|
||||
// 2. Redis miss 再查 MySQL:保证缓存过期后仍可继续微调;
|
||||
// 3. 若 MySQL 命中且 Redis 可用,顺便回填 Redis,提升后续命中率;
|
||||
// 4. 任一步失败仅打日志,不 panic,由上层根据返回 nil 做统一处理。
|
||||
// 姝ラ鍖栬鏄庯細
|
||||
// 1. 鍏堟煡 Redis锛氬懡涓垯鐩存帴杩斿洖锛屾椂寤舵渶灏忥紱
|
||||
// 2. Redis miss 鍐嶆煡 MySQL锛氫繚璇佺紦瀛樿繃鏈熷悗浠嶅彲缁х画寰皟锛?
|
||||
// 3. 鑻?MySQL 鍛戒腑涓?Redis 鍙敤锛岄『渚垮洖濉?Redis锛屾彁鍗囧悗缁懡涓巼锛?
|
||||
// 4. 浠讳竴姝ュけ璐ヤ粎鎵撴棩蹇楋紝涓?panic锛岀敱涓婂眰鏍规嵁杩斿洖 nil 鍋氱粺涓€澶勭悊銆?
|
||||
func (s *AgentService) loadSchedulePreviewContext(ctx context.Context, userID int, chatID string) *model.SchedulePlanPreviewCache {
|
||||
normalizedChatID := strings.TrimSpace(chatID)
|
||||
if normalizedChatID == "" || userID <= 0 {
|
||||
@@ -96,7 +97,7 @@ func (s *AgentService) loadSchedulePreviewContext(ctx context.Context, userID in
|
||||
if s.cacheDAO != nil {
|
||||
preview, err := s.cacheDAO.GetSchedulePlanPreviewFromCache(ctx, userID, normalizedChatID)
|
||||
if err != nil {
|
||||
log.Printf("读取排程预览缓存失败 chat_id=%s: %v", normalizedChatID, err)
|
||||
log.Printf("璇诲彇鎺掔▼棰勮缂撳瓨澶辫触 chat_id=%s: %v", normalizedChatID, err)
|
||||
} else if preview != nil {
|
||||
return preview
|
||||
}
|
||||
@@ -107,7 +108,7 @@ func (s *AgentService) loadSchedulePreviewContext(ctx context.Context, userID in
|
||||
}
|
||||
snapshot, err := s.repo.GetScheduleStateSnapshot(ctx, userID, normalizedChatID)
|
||||
if err != nil {
|
||||
log.Printf("读取排程状态快照失败 chat_id=%s: %v", normalizedChatID, err)
|
||||
log.Printf("璇诲彇鎺掔▼鐘舵€佸揩鐓уけ璐?chat_id=%s: %v", normalizedChatID, err)
|
||||
return nil
|
||||
}
|
||||
if snapshot == nil {
|
||||
@@ -117,19 +118,19 @@ func (s *AgentService) loadSchedulePreviewContext(ctx context.Context, userID in
|
||||
preview := snapshotToSchedulePlanPreviewCache(snapshot)
|
||||
if preview != nil && s.cacheDAO != nil {
|
||||
if setErr := s.cacheDAO.SetSchedulePlanPreviewToCache(ctx, userID, normalizedChatID, preview); setErr != nil {
|
||||
log.Printf("回填排程预览缓存失败 chat_id=%s: %v", normalizedChatID, setErr)
|
||||
log.Printf("鍥炲~鎺掔▼棰勮缂撳瓨澶辫触 chat_id=%s: %v", normalizedChatID, setErr)
|
||||
}
|
||||
}
|
||||
return preview
|
||||
}
|
||||
|
||||
// convertRefineStateToPlanState 把 schedulerefine 状态映射为 scheduleplan 状态。
|
||||
// convertRefineStateToPlanState 鎶?schedulerefine 鐘舵€佹槧灏勪负 scheduleplan 鐘舵€併€?
|
||||
//
|
||||
// 设计意图:
|
||||
// 1. 复用现有 saveSchedulePlanPreview 写入链路,减少重复落盘代码;
|
||||
// 2. 仅映射“预览持久化必须字段”,避免把 refine 运行期临时字段带入存储层;
|
||||
// 3. 后续如要扩展 refine 专属快照字段,可在该映射处集中演进。
|
||||
func convertRefineStateToPlanState(st *schedulerefine.ScheduleRefineState) *scheduleplan.SchedulePlanState {
|
||||
// 璁捐鎰忓浘锛?
|
||||
// 1. 澶嶇敤鐜版湁 saveSchedulePlanPreview 鍐欏叆閾捐矾锛屽噺灏戦噸澶嶈惤鐩樹唬鐮侊紱
|
||||
// 2. 浠呮槧灏勨€滈瑙堟寔涔呭寲蹇呴』瀛楁鈥濓紝閬垮厤鎶?refine 杩愯鏈熶复鏃跺瓧娈靛甫鍏ュ瓨鍌ㄥ眰锛?
|
||||
// 3. 鍚庣画濡傝鎵╁睍 refine 涓撳睘蹇収瀛楁锛屽彲鍦ㄨ鏄犲皠澶勯泦涓紨杩涖€?
|
||||
func convertRefineStateToPlanState(st *agentnode.ScheduleRefineState) *agentmodel.SchedulePlanState {
|
||||
if st == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -137,7 +138,7 @@ func convertRefineStateToPlanState(st *schedulerefine.ScheduleRefineState) *sche
|
||||
if st.Contract.Strategy == "keep" {
|
||||
adjustmentScope = "small"
|
||||
}
|
||||
return &scheduleplan.SchedulePlanState{
|
||||
return &agentmodel.SchedulePlanState{
|
||||
TraceID: strings.TrimSpace(st.TraceID),
|
||||
UserID: st.UserID,
|
||||
ConversationID: strings.TrimSpace(st.ConversationID),
|
||||
@@ -157,17 +158,17 @@ func convertRefineStateToPlanState(st *schedulerefine.ScheduleRefineState) *sche
|
||||
}
|
||||
}
|
||||
|
||||
// shouldPersistScheduleRefinePreview 判断“本轮微调结果是否应覆盖上一版预览”。
|
||||
// shouldPersistScheduleRefinePreview 鍒ゆ柇鈥滄湰杞井璋冪粨鏋滄槸鍚﹀簲瑕嗙洊涓婁竴鐗堥瑙堚€濄€?
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 默认沿用原有 refine 持久化策略,保证普通 ReAct 微调链路不受影响;
|
||||
// 2. 仅当“独立复合分支已直接出站,但终审未通过”时,拒绝覆盖上一版预览;
|
||||
// 3. 这样可以避免外层把未经验证的复合结果当成新的基线继续滚动微调。
|
||||
func shouldPersistScheduleRefinePreview(st *schedulerefine.ScheduleRefineState) bool {
|
||||
// 鑱岃矗杈圭晫锛?
|
||||
// 1. 榛樿娌跨敤鍘熸湁 refine 鎸佷箙鍖栫瓥鐣ワ紝淇濊瘉鏅€?ReAct 寰皟閾捐矾涓嶅彈褰卞搷锛?
|
||||
// 2. 浠呭綋鈥滅嫭绔嬪鍚堝垎鏀凡鐩存帴鍑虹珯锛屼絾缁堝鏈€氳繃鈥濇椂锛屾嫆缁濊鐩栦笂涓€鐗堥瑙堬紱
|
||||
// 3. 杩欐牱鍙互閬垮厤澶栧眰鎶婃湭缁忛獙璇佺殑澶嶅悎缁撴灉褰撴垚鏂扮殑鍩虹嚎缁х画婊氬姩寰皟銆?
|
||||
func shouldPersistScheduleRefinePreview(st *agentnode.ScheduleRefineState) bool {
|
||||
if st == nil {
|
||||
return false
|
||||
}
|
||||
if st.CompositeRouteSucceeded && !schedulerefine.FinalHardCheckPassed(st) {
|
||||
if st.CompositeRouteSucceeded && !agentnode.FinalHardCheckPassed(st) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
||||
Reference in New Issue
Block a user