后端: 1.彻底删除原agent文件夹,并将现agent2文件夹全量重命名为agent(包括全部涉及到的文件以及文档、注释),迁移工作完美结束 2.修复了重试消息的相关逻辑问题 前端: 1.改善了一些交互体验,修复了一些bug,现在只剩少的功能了,现存的bug基本都修复完毕 全仓库: 1.更新了决策记录和README文档
163 lines
7.1 KiB
Go
163 lines
7.1 KiB
Go
package agentsvc
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"log"
|
||
"strings"
|
||
|
||
agentgraph "github.com/LoveLosita/smartflow/backend/agent/graph"
|
||
agentmodel "github.com/LoveLosita/smartflow/backend/agent/model"
|
||
agentnode "github.com/LoveLosita/smartflow/backend/agent/node"
|
||
"github.com/LoveLosita/smartflow/backend/conv"
|
||
"github.com/LoveLosita/smartflow/backend/model"
|
||
"github.com/LoveLosita/smartflow/backend/pkg"
|
||
"github.com/cloudwego/eino-ext/components/model/ark"
|
||
"github.com/cloudwego/eino/schema"
|
||
"github.com/spf13/viper"
|
||
)
|
||
|
||
// runSchedulePlanFlow 执行“智能排程”分支。
|
||
//
|
||
// 职责边界:
|
||
// 1. 负责把本次请求接入 scheduleplan graph,并注入运行依赖。
|
||
// 2. 负责读取对话历史(优先 Redis,未命中再回源 DB)用于连续对话微调。
|
||
// 3. 负责把排程预览快照写入 Redis(供查询接口拉取 JSON)。
|
||
// 4. 负责返回给上层“可直接发给用户的最终文本回复”。
|
||
// 5. 不负责聊天持久化(由 AgentChat 主链路统一处理)。
|
||
func (s *AgentService) runSchedulePlanFlow(
|
||
ctx context.Context,
|
||
selectedModel *ark.ChatModel,
|
||
userMessage string,
|
||
userID int,
|
||
chatID string,
|
||
traceID string,
|
||
extra map[string]any,
|
||
emitStage func(stage, detail string),
|
||
outChan chan<- string,
|
||
modelName string,
|
||
) (string, error) {
|
||
// 1. 依赖预检:缺硬依赖时直接失败,避免进入 graph 后才出现空指针或半途失败。
|
||
// 1.1 SmartPlanningMultiRaw / HybridScheduleWithPlanMulti / ResolvePlanningWindow 任一缺失都无法继续。
|
||
// 1.2 selectedModel 为空时无法执行 LLM 节点,直接返回错误由上层处理。
|
||
if s.SmartPlanningMultiRawFunc == nil || s.HybridScheduleWithPlanMultiFunc == nil || s.ResolvePlanningWindowFunc == nil {
|
||
return "", errors.New("schedule plan service dependencies are not ready")
|
||
}
|
||
if selectedModel == nil {
|
||
return "", errors.New("schedule plan model is nil")
|
||
}
|
||
|
||
// 2. 连续对话微调前置处理:先尝试读取“上一版预览快照”,再清理旧 key。
|
||
// 2.1 先读后删的原因:
|
||
// 2.1.1 若先删再读,会丢失“连续微调起点”;
|
||
// 2.1.2 先读可让本轮在内存中复用上轮 HybridEntries。
|
||
// 2.2 清理旧 key 仍然保留,避免前端在本轮进行中误读到旧结果。
|
||
var previousPreview *model.SchedulePlanPreviewCache
|
||
if s.cacheDAO != nil {
|
||
preview, getErr := s.cacheDAO.GetSchedulePlanPreviewFromCache(ctx, userID, chatID)
|
||
if getErr != nil {
|
||
log.Printf("读取上一版排程预览失败 chat_id=%s: %v", chatID, getErr)
|
||
} else {
|
||
previousPreview = preview
|
||
}
|
||
if delErr := s.cacheDAO.DeleteSchedulePlanPreviewFromCache(ctx, userID, chatID); delErr != nil {
|
||
log.Printf("清理旧排程预览失败 chat_id=%s: %v", chatID, delErr)
|
||
}
|
||
}
|
||
// 2.3 Redis miss 时回落 MySQL 快照:
|
||
// 2.3.1 目的:即使 Redis TTL 过期,也能延续同会话微调语境;
|
||
// 2.3.2 回填:命中 DB 后尝试回填 Redis,提高后续读取命中率;
|
||
// 2.3.3 失败策略:DB 读取异常只打日志,链路继续按“无历史快照”执行。
|
||
if previousPreview == nil && s.repo != nil {
|
||
snapshot, snapshotErr := s.repo.GetScheduleStateSnapshot(ctx, userID, chatID)
|
||
if snapshotErr != nil {
|
||
log.Printf("从 MySQL 读取排程快照失败 chat_id=%s: %v", chatID, snapshotErr)
|
||
} else if snapshot != nil {
|
||
previousPreview = snapshotToSchedulePlanPreviewCache(snapshot)
|
||
if s.cacheDAO != nil && previousPreview != nil {
|
||
if setErr := s.cacheDAO.SetSchedulePlanPreviewToCache(ctx, userID, chatID, previousPreview); setErr != nil {
|
||
log.Printf("回填排程预览缓存失败 chat_id=%s: %v", chatID, setErr)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 3. 读取对话历史:先快后稳。
|
||
// 3.1 先查 Redis,命中则避免回源 DB,降低请求时延。
|
||
// 3.2 Redis 异常仅记录日志,不中断主流程(回源 DB 兜底)。
|
||
var chatHistory []*schema.Message
|
||
if s.agentCache != nil {
|
||
history, err := s.agentCache.GetHistory(ctx, chatID)
|
||
if err != nil {
|
||
log.Printf("获取排程对话历史失败 chat_id=%s: %v", chatID, err)
|
||
} else if history != nil {
|
||
chatHistory = history
|
||
}
|
||
}
|
||
|
||
// 3.3 Redis 未命中时回源 DB,保证链路在缓存波动时仍可用。
|
||
// 3.4 DB 回源失败同样只记日志并继续,让 graph 按“无历史”降级运行。
|
||
if chatHistory == nil && s.repo != nil {
|
||
histories, hisErr := s.repo.GetUserChatHistories(ctx, userID, pkg.HistoryFetchLimitByModel("worker"), chatID)
|
||
if hisErr != nil {
|
||
log.Printf("回源 DB 获取排程对话历史失败 chat_id=%s: %v", chatID, hisErr)
|
||
} else {
|
||
chatHistory = conv.ToEinoMessages(histories)
|
||
}
|
||
}
|
||
|
||
// 4. 执行 graph 主流程。
|
||
// 4.1 这里只负责参数拼装与调用,不在 service 层重复实现 graph 节点逻辑。
|
||
// 4.2 并发度/预算从配置注入,避免把调优参数写死在代码中。
|
||
state := agentmodel.NewSchedulePlanState(traceID, userID, chatID)
|
||
// 4.3 连续对话微调注入:
|
||
// 4.3.1 若命中上轮预览,则把任务类/混合条目/分配结果注入 state;
|
||
// 4.3.2 这样 rough_build 可按需复用旧底板,避免每轮都重新粗排。
|
||
if previousPreview != nil {
|
||
state.HasPreviousPreview = true
|
||
state.PreviousTaskClassIDs = append([]int(nil), previousPreview.TaskClassIDs...)
|
||
state.PreviousHybridEntries = cloneHybridEntries(previousPreview.HybridEntries)
|
||
state.PreviousAllocatedItems = cloneTaskClassItems(previousPreview.AllocatedItems)
|
||
state.PreviousCandidatePlans = cloneWeekSchedules(previousPreview.CandidatePlans)
|
||
}
|
||
finalState, runErr := agentgraph.RunSchedulePlanGraph(ctx, agentnode.SchedulePlanGraphRunInput{
|
||
Model: selectedModel,
|
||
State: state,
|
||
Deps: agentnode.SchedulePlanToolDeps{
|
||
SmartPlanningMultiRaw: s.SmartPlanningMultiRawFunc,
|
||
HybridScheduleWithPlanMulti: s.HybridScheduleWithPlanMultiFunc,
|
||
ResolvePlanningWindow: s.ResolvePlanningWindowFunc,
|
||
},
|
||
UserMessage: userMessage,
|
||
Extra: extra,
|
||
ChatHistory: chatHistory,
|
||
EmitStage: emitStage,
|
||
OutChan: outChan,
|
||
ModelName: modelName,
|
||
DailyRefineConcurrency: viper.GetInt("agent.dailyRefineConcurrency"),
|
||
WeeklyAdjustBudget: viper.GetInt("agent.weeklyAdjustBudget"),
|
||
})
|
||
if runErr != nil {
|
||
// 4.3 graph 失败直接上抛,由上层决定回落或报错。
|
||
return "", runErr
|
||
}
|
||
|
||
// 5. 组装最终回复文本。
|
||
// 5.1 明确移除“把排程结果序列化成 JSON 文本直接回传”的抽象,
|
||
// 避免在 SSE 聊天链路里吐出原始 JSON,影响前端展示与用户体验。
|
||
// 5.2 当 finalState 为空或 summary 为空时,返回统一兜底文案,保证接口有稳定输出。
|
||
if finalState == nil {
|
||
return "排程流程异常,请稍后重试。", nil
|
||
}
|
||
reply := strings.TrimSpace(finalState.FinalSummary)
|
||
if reply == "" {
|
||
reply = "排程流程已完成,但未生成结果摘要。"
|
||
}
|
||
|
||
// 6. 旁路写入排程预览缓存(结构化 JSON),给查询接口拉取。
|
||
// 6.1 失败只记日志,不影响本次对话回复;
|
||
// 6.2 成功后前端可通过 conversation_id 获取 candidate_plans。
|
||
s.saveSchedulePlanPreview(ctx, userID, chatID, finalState)
|
||
return reply, nil
|
||
}
|