feat(coordinator): 实现L2统一上下文组装器与意图校验调度引擎

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
WeiCHEN
2026-09-07 21:34:43 +08:00
co-authored by Cursor
parent d0ca8bed54
commit 70c96f2f3d
4 changed files with 763 additions and 0 deletions
+168
View File
@@ -0,0 +1,168 @@
package coordinator
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
"chat-agent-system/internal/bizdev"
"chat-agent-system/internal/intent"
"chat-agent-system/internal/squad"
)
// UserInfo 用户在系统与项目内的身份
type UserInfo struct {
ID string `json:"id"`
Name string `json:"name"`
Role string `json:"role"` // 系统角色 (如 engineer, pm, director)
ProjectRole string `json:"project_role"` // 项目内角色 (如 编制人 composer, 审核人 reviewer, 批准人 approver)
}
// ProjectInfo 项目与工作流当前基准
type ProjectInfo struct {
ID string `json:"id"`
Name string `json:"name"`
CustomerName string `json:"customer_name"`
DeviceType string `json:"device_type"`
IntendedUse string `json:"intended_use"`
TargetMarket string `json:"target_market"`
CurrentWorkflow string `json:"current_workflow"`
CurrentStage bizdev.MaturityStage `json:"current_stage"`
StageRules []string `json:"stage_rules,omitempty"`
}
// UserDocsInfo 用户相关文档清单与认领状态
type UserDocsInfo struct {
Composed []string `json:"composed"` // 负责编制的文档
Reviewed []string `json:"reviewed"` // 负责审核的文档
Approved []string `json:"approved"` // 负责批准的文档
Claimed []string `json:"claimed"` // 已认领/已打开查阅的文档
}
// PendingItems 待用户处理或决策的待办项
type PendingItems struct {
ConfirmModifications []string `json:"confirm_modifications,omitempty"`
PendingReviews []string `json:"pending_reviews,omitempty"`
PendingApprovals []string `json:"pending_approvals,omitempty"`
MyDraftsInProgress []string `json:"my_drafts_in_progress,omitempty"`
UnreadSupplements []string `json:"unread_supplements,omitempty"`
}
// UserProjectContext 用户项目级全量统一上下文 (对话系统与执行系统共同消费)
type UserProjectContext struct {
SnapshotID string `json:"snapshot_id"`
AssembledAt time.Time `json:"assembled_at"`
User UserInfo `json:"user"`
Project ProjectInfo `json:"project"`
UserDocs UserDocsInfo `json:"user_docs"`
Pending PendingItems `json:"pending"`
BlackboardFacts map[string]string `json:"blackboard_facts"`
LatestAdvisorBrief *squad.AdvisorBriefing `json:"latest_advisor_brief,omitempty"`
AvailableActions []string `json:"available_actions"`
RecentEvents []string `json:"recent_events,omitempty"`
WorkDir string `json:"work_dir,omitempty"`
}
// ContextAssembler 上下文组装服务
type ContextAssembler struct{}
func NewContextAssembler() *ContextAssembler {
return &ContextAssembler{}
}
// ComputeAvailableActions 根据当前成熟度阶段与角色计算允许执行的操作集合
func ComputeAvailableActions(stage bizdev.MaturityStage, pRole string) []string {
actions := []string{"QUERY_PROGRESS", "SUPPLEMENT_INFO", "VIEW_DOC"}
switch stage {
case bizdev.StageV01_Triage:
actions = append(actions, "TRIGGER_TASK:intake", "ADVANCE_STAGE:V0.5_feasibility")
case bizdev.StageV05_Feasibility:
actions = append(actions,
"TRIGGER_TASK:research",
"TRIGGER_TASK:feasibility_reg",
"TRIGGER_TASK:prelim_design",
"ADVANCE_STAGE:V1.0_quoting",
)
case bizdev.StageV10_Quoting:
actions = append(actions, "TRIGGER_TASK:cdo_quote", "LOCK_COMMERCIAL")
default:
actions = append(actions, "ADVANCE_STAGE:V0.1_triage")
}
return actions
}
// ToSnapshot 转换为 L1 意图识别器所需的轻量上下文快照
func (ctx *UserProjectContext) ToSnapshot() *intent.ContextSnapshot {
if ctx == nil {
return nil
}
return &intent.ContextSnapshot{
SnapshotID: ctx.SnapshotID,
ProjectName: ctx.Project.Name,
CurrentStage: string(ctx.Project.CurrentStage),
AvailableActions: ctx.AvailableActions,
}
}
// AssembleFromWorkspace 基于现有工作区快速组装 UserProjectContext
func (a *ContextAssembler) AssembleFromWorkspace(workDir string, user UserInfo) (*UserProjectContext, error) {
snapID := fmt.Sprintf("ctx_%d", time.Now().UnixNano())
ctx := &UserProjectContext{
SnapshotID: snapID,
AssembledAt: time.Now(),
User: user,
WorkDir: workDir,
BlackboardFacts: make(map[string]string),
AvailableActions: []string{"QUERY_PROGRESS", "SUPPLEMENT_INFO", "VIEW_DOC"},
}
// 1. 尝试读取 workspace.json
wsMetaPath := filepath.Join(workDir, "workspace.json")
if data, err := os.ReadFile(wsMetaPath); err == nil {
var wsMeta squad.WorkspaceMetadata
if json.Unmarshal(data, &wsMeta) == nil {
ctx.Project.Name = wsMeta.ProjectName
ctx.Project.CustomerName = wsMeta.CustomerName
ctx.Project.DeviceType = wsMeta.DeviceType
ctx.Project.IntendedUse = wsMeta.IntendedUse
ctx.Project.TargetMarket = wsMeta.TargetMarket
ctx.Project.CurrentWorkflow = wsMeta.TaskKey
}
}
// 2. 尝试读取 blackboard.json
bbPath := filepath.Join(workDir, "blackboard.json")
if data, err := os.ReadFile(bbPath); err == nil {
var bb squad.BlackboardState
if json.Unmarshal(data, &bb) == nil {
for k, v := range bb.Fields {
ctx.BlackboardFacts[k] = v
}
for _, g := range bb.Gaps {
if g.Blocking && !g.Resolved {
ctx.Pending.ConfirmModifications = append(ctx.Pending.ConfirmModifications, g.Item)
}
}
}
}
// 3. 尝试提取阶段与文档
if ctx.Project.CurrentStage == "" {
ctx.Project.CurrentStage = bizdev.StageV01_Triage
}
ctx.AvailableActions = ComputeAvailableActions(ctx.Project.CurrentStage, user.ProjectRole)
// 4. 若有 output.md,尝试提取最新的参谋简报
outPath := filepath.Join(workDir, "output.md")
if outBytes, err := os.ReadFile(outPath); err == nil && len(outBytes) > 0 {
ctx.LatestAdvisorBrief = squad.ExtractAdvisorBriefing(ctx.Project.CurrentWorkflow, string(outBytes), ctx.BlackboardFacts)
ctx.UserDocs.Composed = append(ctx.UserDocs.Composed, ctx.Project.CurrentWorkflow)
}
return ctx, nil
}
+105
View File
@@ -0,0 +1,105 @@
package coordinator_test
import (
"os"
"path/filepath"
"testing"
"chat-agent-system/internal/bizdev"
"chat-agent-system/internal/coordinator"
"chat-agent-system/internal/intent"
)
func TestComputeAvailableActions(t *testing.T) {
actionsV01 := coordinator.ComputeAvailableActions(bizdev.StageV01_Triage, "composer")
foundAdvance := false
for _, a := range actionsV01 {
if a == "ADVANCE_STAGE:V0.5_feasibility" {
foundAdvance = true
}
}
if !foundAdvance {
t.Errorf("expected V0.1 actions to contain advance to V0.5: %v", actionsV01)
}
actionsV05 := coordinator.ComputeAvailableActions(bizdev.StageV05_Feasibility, "composer")
foundAdvanceQuote := false
for _, a := range actionsV05 {
if a == "ADVANCE_STAGE:V1.0_quoting" {
foundAdvanceQuote = true
}
}
if !foundAdvanceQuote {
t.Errorf("expected V0.5 actions to contain advance to V1.0: %v", actionsV05)
}
}
func TestAssembleFromWorkspace(t *testing.T) {
tmpDir := t.TempDir()
wsJSON := `{
"project_name": "智能听诊器系统",
"customer_name": "水木清研",
"device_type": "电子听诊器",
"task_key": "intake"
}`
_ = os.WriteFile(filepath.Join(tmpDir, "workspace.json"), []byte(wsJSON), 0o644)
bbJSON := `{
"fields": {
"project_name": "智能听诊器系统",
"target_market": "国内"
},
"gaps": [
{"item": "缺少蓝牙传输规格", "blocking": true, "resolved": false}
]
}`
_ = os.WriteFile(filepath.Join(tmpDir, "blackboard.json"), []byte(bbJSON), 0o644)
outputMD := "# 用户初始需求收集表\n\n## 结论\n初步整理完成。\n\n## 待确认清单\n- 蓝牙传输距离与功耗要求\n"
_ = os.WriteFile(filepath.Join(tmpDir, "output.md"), []byte(outputMD), 0o644)
user := coordinator.UserInfo{
ID: "u_1001",
Name: "张工",
Role: "engineer",
ProjectRole: "composer",
}
assembler := coordinator.NewContextAssembler()
ctx, err := assembler.AssembleFromWorkspace(tmpDir, user)
if err != nil {
t.Fatalf("assemble context failed: %v", err)
}
if ctx.Project.Name != "智能听诊器系统" {
t.Errorf("expected project name '智能听诊器系统', got '%s'", ctx.Project.Name)
}
if ctx.BlackboardFacts["target_market"] != "国内" {
t.Errorf("expected target_market '国内', got '%s'", ctx.BlackboardFacts["target_market"])
}
if len(ctx.Pending.ConfirmModifications) == 0 {
t.Errorf("expected blocking gap in pending confirm modifications")
}
if ctx.LatestAdvisorBrief == nil {
t.Fatalf("expected latest advisor brief to be assembled")
}
if ctx.LatestAdvisorBrief.MaturityLevel != "V0.1" {
t.Errorf("expected V0.1 maturity, got %s", ctx.LatestAdvisorBrief.MaturityLevel)
}
}
func TestIntentContractTypes(t *testing.T) {
cand := intent.CandidateIntent{
Intent: intent.IntentQueryProgress,
Confidence: 0.95,
FilledSlots: map[string]string{
intent.SlotQueryTopic: "progress",
},
Reasoning: "用户明确询问当前立项走到哪一步了",
}
if cand.Intent != intent.IntentQueryProgress || cand.Confidence < 0.9 {
t.Errorf("candidate intent fields mismatch: %+v", cand)
}
}
+373
View File
@@ -0,0 +1,373 @@
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
}
+117
View File
@@ -0,0 +1,117 @@
package coordinator_test
import (
"context"
"testing"
"chat-agent-system/internal/bizdev"
"chat-agent-system/internal/coordinator"
"chat-agent-system/internal/intent"
"chat-agent-system/internal/squad"
)
func TestCoordinatorEngine_QueryProgress(t *testing.T) {
engine := coordinator.NewCoordinatorEngine(nil)
userCtx := &coordinator.UserProjectContext{
Project: coordinator.ProjectInfo{
Name: "便携式掌上超声项目",
},
LatestAdvisorBrief: &squad.AdvisorBriefing{
MaturityScore: 70,
MaturityLevel: "V0.1",
MaturityHeadline: "已具备基本产品定义,亟待补充探头频率与目标市场",
PreemptiveRisks: []string{"欧盟CE MDR认证周期较长"},
NextBestActions: []string{"推进进入V0.5可行性论证"},
CommunicationKit: "【给客户的建议沟通要点】...",
},
}
recRes := &intent.IntentRecognitionResult{
TopIntent: &intent.CandidateIntent{
Intent: intent.IntentQueryProgress,
FilledSlots: map[string]string{
intent.SlotQueryTopic: "risk",
},
},
}
res, err := engine.ProcessIntent(context.Background(), recRes, userCtx)
if err != nil {
t.Fatalf("process intent failed: %v", err)
}
if res.Status != coordinator.StatusAccepted {
t.Errorf("expected status ACCEPTED, got %s", res.Status)
}
if res.AdvisorBrief == nil {
t.Errorf("expected advisor brief in result")
}
}
func TestCoordinatorEngine_SupplementInfo(t *testing.T) {
engine := coordinator.NewCoordinatorEngine(nil)
userCtx := &coordinator.UserProjectContext{
Project: coordinator.ProjectInfo{
Name: "便携式掌上超声项目",
},
BlackboardFacts: make(map[string]string),
}
recRes := &intent.IntentRecognitionResult{
TopIntent: &intent.CandidateIntent{
Intent: intent.IntentSupplementInfo,
FilledSlots: map[string]string{
intent.SlotFactKey: "target_market",
intent.SlotFactValue: "国内及欧盟CE",
},
},
}
res, err := engine.ProcessIntent(context.Background(), recRes, userCtx)
if err != nil {
t.Fatalf("process intent failed: %v", err)
}
if res.Status != coordinator.StatusAccepted {
t.Errorf("expected status ACCEPTED, got %s", res.Status)
}
if userCtx.BlackboardFacts["target_market"] != "国内及欧盟CE" {
t.Errorf("expected blackboard target_market updated, got %s", userCtx.BlackboardFacts["target_market"])
}
}
func TestCoordinatorEngine_AdvanceStage(t *testing.T) {
engine := coordinator.NewCoordinatorEngine(nil)
userCtx := &coordinator.UserProjectContext{
Project: coordinator.ProjectInfo{
Name: "超声测试项目",
CurrentStage: bizdev.StageV01_Triage,
},
User: coordinator.UserInfo{
ProjectRole: "composer",
},
}
recRes := &intent.IntentRecognitionResult{
TopIntent: &intent.CandidateIntent{
Intent: intent.IntentAdvanceStage,
FilledSlots: map[string]string{
intent.SlotTargetStage: string(bizdev.StageV05_Feasibility),
},
},
}
res, err := engine.ProcessIntent(context.Background(), recRes, userCtx)
if err != nil {
t.Fatalf("process intent failed: %v", err)
}
if res.Status != coordinator.StatusAccepted {
t.Errorf("expected status ACCEPTED, got %s", res.Status)
}
if userCtx.Project.CurrentStage != bizdev.StageV05_Feasibility {
t.Errorf("expected stage V0.5_feasibility, got %s", userCtx.Project.CurrentStage)
}
}