package coordinator import ( "context" "encoding/json" "fmt" "os" "path/filepath" "strings" "chat-agent-system/internal/bizdev" "chat-agent-system/internal/intent" "chat-agent-system/internal/squad" "chat-agent-system/internal/stream" ) // CoordinationStatus 协调裁决状态 type CoordinationStatus string const ( StatusAccepted CoordinationStatus = "ACCEPTED" StatusRejected CoordinationStatus = "REJECTED" StatusClarificationRequired CoordinationStatus = "CLARIFICATION_REQUIRED" ) // CoordinationResult 协调层给对话层的回执 type CoordinationResult struct { Status CoordinationStatus `json:"status"` Intent intent.IntentType `json:"intent"` Branch intent.ResolutionBranch `json:"branch,omitempty"` RejectReason string `json:"reject_reason,omitempty"` ClarificationPrompt string `json:"clarification_prompt,omitempty"` NaturalReply string `json:"natural_reply"` AdvisorBrief *squad.AdvisorBriefing `json:"advisor_brief,omitempty"` ArtifactPath string `json:"artifact_path,omitempty"` UpdatedContext *UserProjectContext `json:"updated_context"` NextExpectation *intent.PendingExpectation `json:"next_expectation,omitempty"` ClearExpectation bool `json:"clear_expectation,omitempty"` } // CoordinatorEngine L2 协调引擎 type CoordinatorEngine struct { bizdevSvc bizdev.BizdevService assembler *ContextAssembler } func NewCoordinatorEngine(bizdevSvc bizdev.BizdevService) *CoordinatorEngine { return &CoordinatorEngine{ bizdevSvc: bizdevSvc, assembler: NewContextAssembler(), } } // ProcessIntent 校验候选意图合法性并驱动调度与响应 func (e *CoordinatorEngine) ProcessIntent( ctx context.Context, recRes *intent.IntentRecognitionResult, userCtx *UserProjectContext, ) (*CoordinationResult, error) { if userCtx == nil { userCtx = &UserProjectContext{ User: UserInfo{ID: "guest", Name: "用户", Role: "engineer", ProjectRole: "composer"}, BlackboardFacts: make(map[string]string), } } top := recRes.TopIntent if top == nil { top = &intent.CandidateIntent{ Intent: intent.IntentCasualChat, Confidence: 1.0, } } streamer := stream.FromContext(ctx) if streamer != nil { streamer.EmitStatus(string(top.Intent), string(recRes.Branch), string(userCtx.Project.CurrentStage), fmt.Sprintf("意图判定: %s (置信度: %.0f%%)", top.Intent, top.Confidence*100), top.FilledSlots) } // 1. 若为分支 3 (模糊/分忧引导),触发参谋主动分析与建议 if recRes.Branch == intent.BranchAmbiguousFallback { return e.handleAmbiguousConsult(top, userCtx) } // 2. 若缺少核心槽位且需澄清 if recRes.RequiresClarifying && top.ClarificationQuestion != "" { return &CoordinationResult{ Status: StatusClarificationRequired, Intent: top.Intent, Branch: recRes.Branch, ClarificationPrompt: top.ClarificationQuestion, NaturalReply: top.ClarificationQuestion, UpdatedContext: userCtx, }, nil } // 3. 根据候选意图执行分支 switch top.Intent { case intent.IntentDeclineExpectation: return e.handleDeclineExpectation(userCtx) case intent.IntentQueryProgress: return e.handleQueryProgress(top, userCtx) case intent.IntentSupplementInfo: return e.handleSupplementInfo(top, userCtx) case intent.IntentAdvanceStage: return e.handleAdvanceStage(ctx, top, userCtx) case intent.IntentTriggerTask: return e.handleTriggerTask(ctx, top, userCtx) case intent.IntentCasualChat: return e.handleCasualChat(userCtx) default: nextExp := BuildNextExpectation(userCtx.Project.CurrentStage, userCtx.Project.Name) return &CoordinationResult{ Status: StatusAccepted, Intent: top.Intent, Branch: recRes.Branch, NaturalReply: "我已收到您的指示。针对当前项目,请问需要我为您查询进展、补充技术事实,还是推进方案论证?随时吩咐我。", UpdatedContext: userCtx, NextExpectation: nextExp, }, nil } } // handleDeclineExpectation 处理用户暂缓或取消上轮建议 (分支 1 放弃) func (e *CoordinatorEngine) handleDeclineExpectation(userCtx *UserProjectContext) (*CoordinationResult, error) { projName := userCtx.Project.Name if projName == "" { projName = "当前项目" } userCtx.PendingExpectation = nil reply := fmt.Sprintf("收到,听您的!那我们先不急着推进。\n针对「%s」,您手头还有哪些细节、技术顾虑或商务考量想先聊聊?我随时待命为您解答。", projName) return &CoordinationResult{ Status: StatusAccepted, Intent: intent.IntentDeclineExpectation, Branch: intent.BranchExpectationHit, NaturalReply: reply, UpdatedContext: userCtx, ClearExpectation: true, }, nil } // handleAmbiguousConsult 处理用户犹豫或征询建议 (分支 3 模糊分忧) func (e *CoordinatorEngine) handleAmbiguousConsult(cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) { currentStage := userCtx.Project.CurrentStage projName := userCtx.Project.Name if projName == "" { projName = "当前项目" } var sb strings.Builder sb.WriteString(fmt.Sprintf("我非常理解您的审慎考量!针对「%s」目前的决策节点,我为您做个客观分析:\n\n", projName)) if currentStage == bizdev.StageV01_Triage { sb.WriteString("• 如果我们现在推进 V0.5(方案成型与法规对齐):\n") sb.WriteString(" 小队会为您把初步架构和注册分类路径摸透,并预估型检成本,您能拿出一份非常扎实的方案框架去与客户深入沟通;\n") sb.WriteString("• 如果选择先放一放 / 多做调研:\n") sb.WriteString(" 我们可以先就客户的预算范围、目标市场或使用场景再多挖掘一些信息,避免后续方案返工。\n\n") sb.WriteString("💡 贴身参谋建议:如果您手头暂时没有其他颠覆性变化,建议不妨让我先帮您跑一版 V0.5 的设计方案与法规路径供您审阅。无论如何,节奏完全以您的把握为准。\n\n") sb.WriteString("您接下来更倾向于:\n") sb.WriteString("[1] 让我先做 V0.5 方案框架(您可以直接回复“好”或“1”)\n") sb.WriteString("[2] 告诉我您现在重点考虑的具体细节,我帮您做针对性分析") } else { sb.WriteString("当前各项工作进展平稳。建议您可以随时把手头的零散想法告诉我,我来为您沉淀事实;或者等您时机成熟时,随时吩咐我推进下一阶段。") } nextExp := BuildNextExpectation(currentStage, projName) userCtx.PendingExpectation = nextExp return &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: intent.BranchAmbiguousFallback, NaturalReply: sb.String(), AdvisorBrief: userCtx.LatestAdvisorBrief, UpdatedContext: userCtx, NextExpectation: nextExp, }, nil } func (e *CoordinatorEngine) handleQueryProgress(cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) { brief := userCtx.LatestAdvisorBrief topic := cand.FilledSlots[intent.SlotQueryTopic] nextExp := BuildNextExpectation(userCtx.Project.CurrentStage, userCtx.Project.Name) userCtx.PendingExpectation = nextExp // 1. 若为特定技术/架构/功耗方案咨询,直接调取参谋专业知识库解答,不强依赖阶段简报 if topic == "technical_design" { var sb strings.Builder sb.WriteString(fmt.Sprintf("关于「%s」的技术与系统架构考量,我为您梳理以下核心要点:\n", userCtx.Project.Name)) sb.WriteString("• 供电与功耗设计:建议选用高能效前端模拟芯片与智能休眠调度,兼顾便携连续工作时间与温升控制;\n") sb.WriteString("• 电气安全与标准:设备需符合 GB 9706.1-2020 通用安全与医用电气专项要求,重点评估外壳防护等级与绝缘耐压;\n") sb.WriteString("• 供应链选型:关键芯片与传感器优先考虑国内高成熟度元器件,缩短备料周期与型检验证周期。\n\n") sb.WriteString("这项考量已为您收录,后续进入方案成型(V0.5)时,小队将作为核心约束直接带入论证。") return &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: sb.String(), AdvisorBrief: brief, UpdatedContext: userCtx, NextExpectation: nextExp, }, nil } if brief == nil { reply := fmt.Sprintf("您好,项目「%s」当前处于初始准备期。我建议先行为您启动【V0.1 闪电摸底】,快速沉淀第一版立项画像并为您准备沟通话术。您希望现在开始吗?", userCtx.Project.Name) return &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: reply, UpdatedContext: userCtx, NextExpectation: nextExp, }, nil } var sb strings.Builder switch topic { case "risk": sb.WriteString(fmt.Sprintf("关于「%s」的潜在风险,我为您提前预警以下关键卡点:\n", userCtx.Project.Name)) for _, r := range brief.PreemptiveRisks { sb.WriteString(fmt.Sprintf("• ⚠️ %s\n", r)) } sb.WriteString("\n建议您在后续与客户沟通时预先留出安全边际。") case "talk_script": sb.WriteString("这是为您草拟的对外商务沟通话术,您可以直接使用或微调:\n\n") sb.WriteString(brief.CommunicationKit) case "progress": sb.WriteString(fmt.Sprintf("【进展汇报】 项目当前成熟度等级为 %s(评分 %d/100)。\n", brief.MaturityLevel, brief.MaturityScore)) sb.WriteString(fmt.Sprintf("诊断结论:%s\n\n", brief.MaturityHeadline)) if len(brief.NextBestActions) > 0 { sb.WriteString("建议下一步选项:\n") for idx, a := range brief.NextBestActions { sb.WriteString(fmt.Sprintf("[%d] %s\n", idx+1, a)) } } default: // all sb.WriteString(fmt.Sprintf("您好!项目「%s」当前处于【%s】阶段(成熟度 %d/100)。\n", userCtx.Project.Name, brief.MaturityLevel, brief.MaturityScore)) sb.WriteString(fmt.Sprintf("核心研判:%s\n\n", brief.MaturityHeadline)) if len(brief.PreemptiveRisks) > 0 { sb.WriteString("⚠️ 关键风险预警:\n") for _, r := range brief.PreemptiveRisks { sb.WriteString(fmt.Sprintf(" • %s\n", r)) } sb.WriteString("\n") } if len(brief.NextBestActions) > 0 { sb.WriteString("您可以选择下一步动作:\n") for idx, a := range brief.NextBestActions { sb.WriteString(fmt.Sprintf(" [%d] %s\n", idx+1, a)) } } } return &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: sb.String(), AdvisorBrief: brief, UpdatedContext: userCtx, NextExpectation: nextExp, }, nil } func (e *CoordinatorEngine) handleSupplementInfo(cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) { factKey := cand.FilledSlots[intent.SlotFactKey] factVal := cand.FilledSlots[intent.SlotFactValue] rawDoc := cand.FilledSlots[intent.SlotSupplementDoc] if userCtx.BlackboardFacts == nil { userCtx.BlackboardFacts = make(map[string]string) } summaryKey := "" if factKey != "" && factVal != "" { userCtx.BlackboardFacts[factKey] = factVal summaryKey = fmt.Sprintf("「%s」已更新为「%s」", factKey, factVal) if factKey == "target_market" { userCtx.Project.TargetMarket = factVal } } else { summaryKey = "已补充新的背景信息" } streamer := stream.FromContext(context.Background()) if streamer != nil { streamer.EmitStatus(string(cand.Intent), string(cand.Branch), string(userCtx.Project.CurrentStage), fmt.Sprintf("已吸收最新事实设定:%s", summaryKey), cand.FilledSlots) } // 持久化到 blackboard.json if userCtx.WorkDir != "" { bbPath := filepath.Join(userCtx.WorkDir, "blackboard.json") var bb squad.BlackboardState if data, err := os.ReadFile(bbPath); err == nil { _ = json.Unmarshal(data, &bb) } if bb.Fields == nil { bb.Fields = make(map[string]string) } for k, v := range userCtx.BlackboardFacts { bb.Fields[k] = v } if b, err := json.MarshalIndent(bb, "", " "); err == nil { _ = os.WriteFile(bbPath, b, 0o644) } } nextExp := BuildNextExpectation(userCtx.Project.CurrentStage, userCtx.Project.Name) userCtx.PendingExpectation = nextExp var sb strings.Builder sb.WriteString(fmt.Sprintf("已收到并为您记录最新信息:%s。\n", summaryKey)) sb.WriteString("这些事实已沉淀至项目黑板,后续论证与核价将直接吸收这些设定。\n\n") sb.WriteString(fmt.Sprintf("针对「%s」,我为您梳理了接下来的推进选项:\n", userCtx.Project.Name)) for idx, opt := range nextExp.Options { recMark := "" if idx == 0 { recMark = "(推荐,可直接回复“好”或“1”)" } sb.WriteString(fmt.Sprintf("[%s] %s %s\n", opt.OptionID, opt.Title, recMark)) } if rawDoc != "" && summaryKey == "已补充新的背景信息" { sb.Reset() sb.WriteString("已为您收录补充材料,并已关联至项目事实黑板。\n在后续的文档修订和阶段演进中,智能体小队将自动引用这部分事实。") } return &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: sb.String(), AdvisorBrief: userCtx.LatestAdvisorBrief, UpdatedContext: userCtx, NextExpectation: nextExp, }, nil } func (e *CoordinatorEngine) handleAdvanceStage(ctx context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) { targetStageStr := cand.FilledSlots[intent.SlotTargetStage] targetStage := bizdev.MaturityStage(targetStageStr) if targetStage == "" { targetStage = bizdev.StageV05_Feasibility } streamer := stream.FromContext(ctx) if streamer != nil { streamer.EmitPhase("stage_transition", fmt.Sprintf("准备推进项目成熟度阶段至【%s】...", targetStage), "coordinator", 0, 0, 0) } // 1. 若携带补丁槽位 (例如 "好,但目标市场改成欧洲CE"),先行持久化更新 patchSummary := "" if len(cand.PatchSlots) > 0 { var patches []string for k, v := range cand.PatchSlots { userCtx.BlackboardFacts[k] = v patches = append(patches, fmt.Sprintf("%s=%s", k, v)) if k == "target_market" { userCtx.Project.TargetMarket = v } } patchSummary = strings.Join(patches, ", ") if userCtx.WorkDir != "" { bbPath := filepath.Join(userCtx.WorkDir, "blackboard.json") var bb squad.BlackboardState if data, err := os.ReadFile(bbPath); err == nil { _ = json.Unmarshal(data, &bb) } if bb.Fields == nil { bb.Fields = make(map[string]string) } for k, v := range cand.PatchSlots { bb.Fields[k] = v } if b, err := json.MarshalIndent(bb, "", " "); err == nil { _ = os.WriteFile(bbPath, b, 0o644) } } } // 2. 状态机合法性检查 if userCtx.Project.CurrentStage == targetStage { return &CoordinationResult{ Status: StatusRejected, Intent: cand.Intent, Branch: cand.Branch, RejectReason: "ALREADY_IN_STAGE", NaturalReply: fmt.Sprintf("项目目前已经在【%s】阶段中,无需重复推进。您可以吩咐我进行详细论证或查询当前进展。", targetStage), UpdatedContext: userCtx, }, nil } if e.bizdevSvc == nil { userCtx.Project.CurrentStage = targetStage userCtx.AvailableActions = ComputeAvailableActions(targetStage, userCtx.User.ProjectRole) nextExp := BuildNextExpectation(targetStage, userCtx.Project.Name) userCtx.PendingExpectation = nextExp reply := fmt.Sprintf("已为您模拟推进到阶段【%s】。", targetStage) if patchSummary != "" { reply = fmt.Sprintf("已为您吸收最新设定(%s),并模拟推进到阶段【%s】。", patchSummary, targetStage) } return &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: reply, UpdatedContext: userCtx, NextExpectation: nextExp, }, nil } // 3. 调用底层 L3 调度器执行阶段跃迁 stageRes, err := e.bizdevSvc.ExecuteStage(ctx, bizdev.StageRunRequest{ Stage: targetStage, ProjectName: userCtx.Project.Name, CustomerName: userCtx.Project.CustomerName, DeviceType: userCtx.Project.DeviceType, IntendedUse: userCtx.Project.IntendedUse, TargetMarket: userCtx.Project.TargetMarket, Fields: userCtx.BlackboardFacts, WorkDir: userCtx.WorkDir, }) if err != nil { return &CoordinationResult{ Status: StatusRejected, Intent: cand.Intent, Branch: cand.Branch, RejectReason: err.Error(), NaturalReply: fmt.Sprintf("阶段推进失败: %v", err), UpdatedContext: userCtx, }, err } userCtx.Project.CurrentStage = targetStage userCtx.LatestAdvisorBrief = stageRes.AdvisorBrief userCtx.AvailableActions = ComputeAvailableActions(targetStage, userCtx.User.ProjectRole) if stageRes.WorkDir != "" { userCtx.WorkDir = stageRes.WorkDir } artifactPath := "" if stageRes.PrimaryArtifact != nil { artifactPath = filepath.Join(stageRes.PrimaryArtifact.WorkDir, "output.md") } nextExp := BuildNextExpectation(targetStage, userCtx.Project.Name) userCtx.PendingExpectation = nextExp var sb strings.Builder if patchSummary != "" { sb.WriteString(fmt.Sprintf("已为您吸收最新设定(%s),并成功推进并完成了【%s】!\n", patchSummary, stageRes.StageTitle)) } else { sb.WriteString(fmt.Sprintf("已为您成功推进并完成了【%s】!\n", stageRes.StageTitle)) } sb.WriteString("智能体小队已协同完成相应论证与文档沉淀。\n\n") sb.WriteString(fmt.Sprintf("💡 最新进展诊断:%s\n", stageRes.AdvisorBrief.MaturityHeadline)) sb.WriteString(fmt.Sprintf("建议您可以审阅正式交付文档(%s),或按最新参谋建议开展后续商务洽谈。\n\n", artifactPath)) sb.WriteString("后续推进选项(供您掌舵决策):\n") for idx, opt := range nextExp.Options { recMark := "" if idx == 0 { recMark = "(推荐,可直接回复“好”或“1”)" } sb.WriteString(fmt.Sprintf("[%s] %s %s\n", opt.OptionID, opt.Title, recMark)) } return &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: sb.String(), AdvisorBrief: stageRes.AdvisorBrief, ArtifactPath: artifactPath, UpdatedContext: userCtx, NextExpectation: nextExp, }, nil } func (e *CoordinatorEngine) handleTriggerTask(ctx context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) { taskKey := cand.FilledSlots[intent.SlotTargetTask] if taskKey == "" { taskKey = "intake" } nextExp := BuildNextExpectation(userCtx.Project.CurrentStage, userCtx.Project.Name) userCtx.PendingExpectation = nextExp if e.bizdevSvc == nil { return &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: fmt.Sprintf("已收到生成【%s】的指令。", taskKey), UpdatedContext: userCtx, NextExpectation: nextExp, }, nil } taskRes, err := e.bizdevSvc.ExecuteTask(ctx, bizdev.TaskRunRequest{ TaskKey: taskKey, ProjectName: userCtx.Project.Name, CustomerName: userCtx.Project.CustomerName, DeviceType: userCtx.Project.DeviceType, IntendedUse: userCtx.Project.IntendedUse, TargetMarket: userCtx.Project.TargetMarket, Fields: userCtx.BlackboardFacts, }) if err != nil { return &CoordinationResult{ Status: StatusRejected, Intent: cand.Intent, Branch: cand.Branch, RejectReason: err.Error(), NaturalReply: fmt.Sprintf("任务执行失败: %v", err), UpdatedContext: userCtx, }, err } userCtx.LatestAdvisorBrief = taskRes.AdvisorBrief outPath := filepath.Join(taskRes.WorkDir, "output.md") var sb strings.Builder sb.WriteString(fmt.Sprintf("已为您自适应规划并完成生成【%s】!\n交付物正文已保存至:%s\n耗时: %v\n", taskRes.ArtifactTitle, outPath, taskRes.Duration)) if taskRes.AdvisorBrief != nil && len(taskRes.AdvisorBrief.BaselineAssumptions) > 0 { sb.WriteString("\n📌 智能体小队预设的基准假设 (已融入方案,您可随时修正):\n") for _, a := range taskRes.AdvisorBrief.BaselineAssumptions { sb.WriteString(fmt.Sprintf(" • %s\n", a)) } } reply := sb.String() return &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: reply, AdvisorBrief: taskRes.AdvisorBrief, ArtifactPath: outPath, UpdatedContext: userCtx, NextExpectation: nextExp, }, nil } func (e *CoordinatorEngine) handleCasualChat(userCtx *UserProjectContext) (*CoordinationResult, error) { projName := userCtx.Project.Name if projName == "" { projName = "当前医疗器械研发项目" } nextExp := BuildNextExpectation(userCtx.Project.CurrentStage, userCtx.Project.Name) userCtx.PendingExpectation = nextExp reply := fmt.Sprintf("您好!我是您的医疗器械立项与商务对接贴身参谋。\n\n针对「%s」,我可以协助您:\n• 随时查询项目成熟度、进展与预见性风险;\n• 补充或调整客户沟通事实与技术指标;\n• 自动起草面向客户负责人的微信/邮件沟通话术;\n• 驱动小队推进方案论证与最终 CDO 报价核算。\n\n请问您现在想从哪里开始?", projName) return &CoordinationResult{ Status: StatusAccepted, Intent: intent.IntentCasualChat, Branch: intent.BranchNewIntentPreempt, NaturalReply: reply, AdvisorBrief: userCtx.LatestAdvisorBrief, UpdatedContext: userCtx, NextExpectation: nextExp, }, nil }