Files
Agent-System/internal/coordinator/engine.go
T

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
}