374 lines
13 KiB
Go
374 lines
13 KiB
Go
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"
|
|
)
|
|
|
|
// 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"`
|
|
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"`
|
|
}
|
|
|
|
// 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,
|
|
}
|
|
}
|
|
|
|
// 1. 若置信度极低或显式缺少核心槽位,转为用户澄清
|
|
if recRes.RequiresClarifying && top.ClarificationQuestion != "" {
|
|
return &CoordinationResult{
|
|
Status: StatusClarificationRequired,
|
|
Intent: top.Intent,
|
|
ClarificationPrompt: top.ClarificationQuestion,
|
|
NaturalReply: top.ClarificationQuestion,
|
|
UpdatedContext: userCtx,
|
|
}, nil
|
|
}
|
|
|
|
// 2. 根据候选意图执行分支
|
|
switch top.Intent {
|
|
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:
|
|
return &CoordinationResult{
|
|
Status: StatusAccepted,
|
|
Intent: top.Intent,
|
|
NaturalReply: "我已收到您的信息。请问需要我为您查询项目进展,还是协助您推进到下一阶段?",
|
|
UpdatedContext: userCtx,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
func (e *CoordinatorEngine) handleQueryProgress(cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) {
|
|
brief := userCtx.LatestAdvisorBrief
|
|
topic := cand.FilledSlots[intent.SlotQueryTopic]
|
|
|
|
if brief == nil {
|
|
reply := fmt.Sprintf("您好,项目「%s」当前处于初始准备期。我建议先行为您启动【V0.1 闪电摸底】,快速沉淀第一版立项画像并为您准备沟通话术。您希望现在开始吗?",
|
|
userCtx.Project.Name)
|
|
return &CoordinationResult{
|
|
Status: StatusAccepted,
|
|
Intent: cand.Intent,
|
|
NaturalReply: reply,
|
|
UpdatedContext: userCtx,
|
|
}, 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,
|
|
NaturalReply: sb.String(),
|
|
AdvisorBrief: brief,
|
|
UpdatedContext: userCtx,
|
|
}, 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)
|
|
// 同步到工作区 project 字段
|
|
if factKey == "target_market" {
|
|
userCtx.Project.TargetMarket = factVal
|
|
}
|
|
} else {
|
|
summaryKey = "已补充新的背景信息"
|
|
}
|
|
|
|
// 若存在真实工作区,写回 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)
|
|
}
|
|
}
|
|
|
|
reply := fmt.Sprintf("已收到并为您记录最新信息:%s。\n这些事实已沉淀至项目黑板,后续论证与核价将直接吸收这些设定。您接下来想:\n[1] 查阅当前最新的项目进展\n[2] 推进进入下一阶段方案论证",
|
|
summaryKey)
|
|
if rawDoc != "" && summaryKey == "已补充新的背景信息" {
|
|
reply = fmt.Sprintf("已为您收录补充材料,并已关联至项目事实黑板。\n在后续的文档修订和阶段演进中,智能体小队将自动引用这部分事实。")
|
|
}
|
|
|
|
return &CoordinationResult{
|
|
Status: StatusAccepted,
|
|
Intent: cand.Intent,
|
|
NaturalReply: reply,
|
|
AdvisorBrief: userCtx.LatestAdvisorBrief,
|
|
UpdatedContext: userCtx,
|
|
}, 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
|
|
}
|
|
|
|
// 权限与状态机合法性检查
|
|
if userCtx.Project.CurrentStage == targetStage {
|
|
return &CoordinationResult{
|
|
Status: StatusRejected,
|
|
Intent: cand.Intent,
|
|
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)
|
|
return &CoordinationResult{
|
|
Status: StatusAccepted,
|
|
Intent: cand.Intent,
|
|
NaturalReply: fmt.Sprintf("已为您模拟推进到阶段【%s】。", targetStage),
|
|
UpdatedContext: userCtx,
|
|
}, nil
|
|
}
|
|
|
|
// 调用底层 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,
|
|
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")
|
|
}
|
|
|
|
reply := fmt.Sprintf("已为您成功推进并完成了【%s】!\n智能体小队已协同完成相应论证与文档沉淀。\n\n💡 最新进展诊断:%s\n建议您可以审阅正式交付文档(%s),或按最新参谋建议开展后续商务洽谈。",
|
|
stageRes.StageTitle,
|
|
stageRes.AdvisorBrief.MaturityHeadline,
|
|
artifactPath,
|
|
)
|
|
|
|
return &CoordinationResult{
|
|
Status: StatusAccepted,
|
|
Intent: cand.Intent,
|
|
NaturalReply: reply,
|
|
AdvisorBrief: stageRes.AdvisorBrief,
|
|
ArtifactPath: artifactPath,
|
|
UpdatedContext: userCtx,
|
|
}, nil
|
|
}
|
|
|
|
func (e *CoordinatorEngine) handleTriggerTask(ctx context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) {
|
|
taskKey := cand.FilledSlots[intent.SlotTargetTask]
|
|
if taskKey == "" {
|
|
taskKey = "intake"
|
|
}
|
|
|
|
if e.bizdevSvc == nil {
|
|
return &CoordinationResult{
|
|
Status: StatusAccepted,
|
|
Intent: cand.Intent,
|
|
NaturalReply: fmt.Sprintf("已收到生成【%s】的指令。", taskKey),
|
|
UpdatedContext: userCtx,
|
|
}, 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,
|
|
RejectReason: err.Error(),
|
|
NaturalReply: fmt.Sprintf("任务执行失败: %v", err),
|
|
UpdatedContext: userCtx,
|
|
}, err
|
|
}
|
|
|
|
userCtx.LatestAdvisorBrief = taskRes.AdvisorBrief
|
|
outPath := filepath.Join(taskRes.WorkDir, "output.md")
|
|
|
|
reply := fmt.Sprintf("已为您生成【%s】!\n交付物正文已保存至:%s\n耗时: %v",
|
|
taskRes.ArtifactTitle, outPath, taskRes.Duration)
|
|
|
|
return &CoordinationResult{
|
|
Status: StatusAccepted,
|
|
Intent: cand.Intent,
|
|
NaturalReply: reply,
|
|
AdvisorBrief: taskRes.AdvisorBrief,
|
|
ArtifactPath: outPath,
|
|
UpdatedContext: userCtx,
|
|
}, nil
|
|
}
|
|
|
|
func (e *CoordinatorEngine) handleCasualChat(userCtx *UserProjectContext) (*CoordinationResult, error) {
|
|
projName := userCtx.Project.Name
|
|
if projName == "" {
|
|
projName = "当前医疗器械研发项目"
|
|
}
|
|
|
|
reply := fmt.Sprintf("您好!我是您的医疗器械立项与商务对接贴身参谋。\n\n针对「%s」,我可以协助您:\n• 随时查询项目成熟度、进展与预见性风险;\n• 补充或调整客户沟通事实与技术指标;\n• 自动起草面向客户负责人的微信/邮件沟通话术;\n• 驱动小队推进方案论证与最终 CDO 报价核算。\n\n请问您现在想从哪里开始?",
|
|
projName)
|
|
|
|
return &CoordinationResult{
|
|
Status: StatusAccepted,
|
|
Intent: intent.IntentCasualChat,
|
|
NaturalReply: reply,
|
|
AdvisorBrief: userCtx.LatestAdvisorBrief,
|
|
UpdatedContext: userCtx,
|
|
}, nil
|
|
}
|