feat(coordinator): 实现L2统一上下文组装器与意图校验调度引擎
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user