diff --git a/internal/chat/session.go b/internal/chat/session.go index 869de488..4c2c82cb 100644 --- a/internal/chat/session.go +++ b/internal/chat/session.go @@ -12,20 +12,23 @@ import ( // Message 历史消息结构 type Message struct { - ID string `json:"id"` - Role string `json:"role"` // "user" | "assistant" - Content string `json:"content"` - Intent intent.IntentType `json:"intent,omitempty"` - AdvisorBrief *squad.AdvisorBriefing `json:"advisor_brief,omitempty"` - Timestamp time.Time `json:"timestamp"` + ID string `json:"id"` + Role string `json:"role"` // "user" | "assistant" + Content string `json:"content"` + Intent intent.IntentType `json:"intent,omitempty"` + Branch intent.ResolutionBranch `json:"branch,omitempty"` + AdvisorBrief *squad.AdvisorBriefing `json:"advisor_brief,omitempty"` + Timestamp time.Time `json:"timestamp"` } // ChatResponse 会话单轮响应 type ChatResponse struct { ReplyText string `json:"reply_text"` + Branch intent.ResolutionBranch `json:"branch"` IntentResult *intent.IntentRecognitionResult `json:"intent_result"` CoordinationResult *coordinator.CoordinationResult `json:"coordination_result"` AdvisorBrief *squad.AdvisorBriefing `json:"advisor_brief,omitempty"` + PendingExpectation *intent.PendingExpectation `json:"pending_expectation,omitempty"` } // Session 多轮对话与协同会话管理器 @@ -46,6 +49,11 @@ func NewSession(id string, userCtx *coordinator.UserProjectContext, rec intent.R if id == "" { id = fmt.Sprintf("sess_%d", time.Now().Unix()) } + // 若上下文尚未挂起预期且当前已具有阶段,则初始化默认选项草稿 + if userCtx != nil && userCtx.PendingExpectation == nil && userCtx.Project.CurrentStage != "" { + userCtx.PendingExpectation = coordinator.BuildNextExpectation(userCtx.Project.CurrentStage, userCtx.Project.Name) + } + return &Session{ ID: id, UserCtx: userCtx, @@ -66,7 +74,7 @@ func (s *Session) SendMessage(ctx context.Context, userInput string) (*ChatRespo } s.Messages = append(s.Messages, userMsg) - // 1. 意图识别 (L1 对话层) + // 1. 意图识别 (L1 对话层,带非阻塞桌角备忘快照) var snap *intent.ContextSnapshot if s.UserCtx != nil { snap = s.UserCtx.ToSnapshot() @@ -82,16 +90,26 @@ func (s *Session) SendMessage(ctx context.Context, userInput string) (*ChatRespo return nil, fmt.Errorf("coordination execution failed: %w", err) } - // 3. 更新上下文与历史 + // 3. 更新上下文与挂起预期生命周期 if coordRes.UpdatedContext != nil { s.UserCtx = coordRes.UpdatedContext } + if coordRes.ClearExpectation { + s.UserCtx.PendingExpectation = nil + } else if coordRes.NextExpectation != nil { + s.UserCtx.PendingExpectation = coordRes.NextExpectation + } else if recRes.Branch == intent.BranchExpectationHit { + s.UserCtx.PendingExpectation = nil + } + // 若为 BranchNewIntentPreempt 且未产生新预期,原挂起预期在焦点底静默保留,不阻拦用户 + asstMsg := Message{ ID: fmt.Sprintf("msg_%d_a", time.Now().UnixNano()), Role: "assistant", Content: coordRes.NaturalReply, Intent: coordRes.Intent, + Branch: recRes.Branch, AdvisorBrief: coordRes.AdvisorBrief, Timestamp: time.Now(), } @@ -99,8 +117,10 @@ func (s *Session) SendMessage(ctx context.Context, userInput string) (*ChatRespo return &ChatResponse{ ReplyText: coordRes.NaturalReply, + Branch: recRes.Branch, IntentResult: recRes, CoordinationResult: coordRes, AdvisorBrief: coordRes.AdvisorBrief, + PendingExpectation: s.UserCtx.PendingExpectation, }, nil } diff --git a/internal/chat/session_test.go b/internal/chat/session_test.go index edcf711e..4331d802 100644 --- a/internal/chat/session_test.go +++ b/internal/chat/session_test.go @@ -5,6 +5,7 @@ import ( "strings" "testing" + "chat-agent-system/internal/bizdev" "chat-agent-system/internal/chat" "chat-agent-system/internal/coordinator" "chat-agent-system/internal/intent" @@ -24,7 +25,8 @@ func TestChatSession_EndToEndDialog(t *testing.T) { ProjectRole: "composer", }, Project: coordinator.ProjectInfo{ - Name: "掌上无线超声系统", + Name: "掌上无线超声系统", + CurrentStage: bizdev.StageV01_Triage, }, LatestAdvisorBrief: &squad.AdvisorBriefing{ MaturityScore: 75, @@ -38,7 +40,7 @@ func TestChatSession_EndToEndDialog(t *testing.T) { session := chat.NewSession("sess_001", userCtx, rec, engine) - // Turn 1: 问候 + // Turn 1: 问候 (分支 2: 新指令/对话) resp1, err := session.SendMessage(context.Background(), "你好,请问你是谁?") if err != nil { t.Fatalf("turn 1 failed: %v", err) @@ -47,7 +49,7 @@ func TestChatSession_EndToEndDialog(t *testing.T) { t.Errorf("turn 1 expected CASUAL_CHAT, got %s", resp1.IntentResult.TopIntent.Intent) } - // Turn 2: 查进展与风险 + // Turn 2: 查进展与风险 (分支 2: 抢占查询) resp2, err := session.SendMessage(context.Background(), "现在这个项目进展怎么样?有啥风险没?") if err != nil { t.Fatalf("turn 2 failed: %v", err) @@ -59,7 +61,7 @@ func TestChatSession_EndToEndDialog(t *testing.T) { t.Errorf("turn 2 reply should mention project name: %s", resp2.ReplyText) } - // Turn 3: 补充信息 + // Turn 3: 补充信息 (分支 2: 补充事实) resp3, err := session.SendMessage(context.Background(), "客户补充说目标市场是国内及欧洲CE") if err != nil { t.Fatalf("turn 3 failed: %v", err) @@ -71,16 +73,169 @@ func TestChatSession_EndToEndDialog(t *testing.T) { t.Errorf("target_market not updated: %v", session.UserCtx.BlackboardFacts) } - // Turn 4: 推进阶段 - resp4, err := session.SendMessage(context.Background(), "好的,进入方案论证阶段吧") + // Turn 4: 单字快速肯定兑现上轮推荐 (分支 1: 极速兑现) + resp4, err := session.SendMessage(context.Background(), "好") if err != nil { t.Fatalf("turn 4 failed: %v", err) } + if resp4.Branch != intent.BranchExpectationHit { + t.Errorf("turn 4 expected branch EXPECTATION_HIT, got %s", resp4.Branch) + } if resp4.IntentResult.TopIntent.Intent != intent.IntentAdvanceStage { t.Errorf("turn 4 expected ADVANCE_STAGE, got %s", resp4.IntentResult.TopIntent.Intent) } + if session.UserCtx.Project.CurrentStage != bizdev.StageV05_Feasibility { + t.Errorf("expected project advanced to V0.5, got %s", session.UserCtx.Project.CurrentStage) + } if len(session.Messages) != 8 { t.Errorf("expected 8 messages (4 user + 4 asst), got %d", len(session.Messages)) } } + +// 用例 1: 分支 1 极速兑现 ("好" / "1") +func TestChatSession_Branch1_FastAffirmative(t *testing.T) { + rec := intent.NewRecognizer() + engine := coordinator.NewCoordinatorEngine(nil) + userCtx := &coordinator.UserProjectContext{ + Project: coordinator.ProjectInfo{ + Name: "心电贴片系统", + CurrentStage: bizdev.StageV01_Triage, + }, + } + + session := chat.NewSession("sess_fast", userCtx, rec, engine) + resp, err := session.SendMessage(context.Background(), "好") + if err != nil { + t.Fatalf("send message failed: %v", err) + } + + if resp.Branch != intent.BranchExpectationHit { + t.Errorf("expected EXPECTATION_HIT, got %s", resp.Branch) + } + if resp.IntentResult.TopIntent.Intent != intent.IntentAdvanceStage { + t.Errorf("expected ADVANCE_STAGE, got %s", resp.IntentResult.TopIntent.Intent) + } + if session.UserCtx.Project.CurrentStage != bizdev.StageV05_Feasibility { + t.Errorf("expected stage V0.5, got %s", session.UserCtx.Project.CurrentStage) + } +} + +// 用例 2: 分支 1 复合打补丁 ("好,但目标市场改成欧洲CE") +func TestChatSession_Branch1_AffirmativeWithPatch(t *testing.T) { + rec := intent.NewRecognizer() + engine := coordinator.NewCoordinatorEngine(nil) + userCtx := &coordinator.UserProjectContext{ + Project: coordinator.ProjectInfo{ + Name: "便携超声设备", + CurrentStage: bizdev.StageV01_Triage, + }, + BlackboardFacts: make(map[string]string), + } + + session := chat.NewSession("sess_patch", userCtx, rec, engine) + resp, err := session.SendMessage(context.Background(), "好,但目标市场改成欧洲CE") + if err != nil { + t.Fatalf("send message failed: %v", err) + } + + if resp.Branch != intent.BranchExpectationHit { + t.Errorf("expected EXPECTATION_HIT, got %s", resp.Branch) + } + if resp.IntentResult.TopIntent.Intent != intent.IntentAdvanceStage { + t.Errorf("expected ADVANCE_STAGE, got %s", resp.IntentResult.TopIntent.Intent) + } + if session.UserCtx.BlackboardFacts["target_market"] != "欧洲CE" { + t.Errorf("expected blackboard target_market '欧洲CE', got '%s'", session.UserCtx.BlackboardFacts["target_market"]) + } + if session.UserCtx.Project.TargetMarket != "欧洲CE" { + t.Errorf("expected project TargetMarket '欧洲CE', got '%s'", session.UserCtx.Project.TargetMarket) + } +} + +// 用例 3: 分支 1 否定撤销 ("先别推,等等") +func TestChatSession_Branch1_Decline(t *testing.T) { + rec := intent.NewRecognizer() + engine := coordinator.NewCoordinatorEngine(nil) + userCtx := &coordinator.UserProjectContext{ + Project: coordinator.ProjectInfo{ + Name: "骨科手术机器人", + CurrentStage: bizdev.StageV01_Triage, + }, + } + + session := chat.NewSession("sess_decline", userCtx, rec, engine) + resp, err := session.SendMessage(context.Background(), "先别推,等等") + if err != nil { + t.Fatalf("send message failed: %v", err) + } + + if resp.Branch != intent.BranchExpectationHit { + t.Errorf("expected EXPECTATION_HIT, got %s", resp.Branch) + } + if resp.IntentResult.TopIntent.Intent != intent.IntentDeclineExpectation { + t.Errorf("expected DECLINE_EXPECTATION, got %s", resp.IntentResult.TopIntent.Intent) + } + if session.UserCtx.PendingExpectation != nil { + t.Errorf("expected PendingExpectation to be cleared after decline") + } + if !strings.Contains(resp.ReplyText, "听您的") { + t.Errorf("expected polite advisor reply: %s", resp.ReplyText) + } +} + +// 用例 4: 分支 2 突发话题抢占 ("查一下探头功耗要怎么设计") +func TestChatSession_Branch2_TopicPreemption(t *testing.T) { + rec := intent.NewRecognizer() + engine := coordinator.NewCoordinatorEngine(nil) + userCtx := &coordinator.UserProjectContext{ + Project: coordinator.ProjectInfo{ + Name: "无线超声系统", + CurrentStage: bizdev.StageV01_Triage, + }, + } + + session := chat.NewSession("sess_preempt", userCtx, rec, engine) + resp, err := session.SendMessage(context.Background(), "查一下探头功耗要怎么设计") + if err != nil { + t.Fatalf("send message failed: %v", err) + } + + if resp.Branch != intent.BranchNewIntentPreempt { + t.Errorf("expected NEW_INTENT_PREEMPT, got %s", resp.Branch) + } + if resp.IntentResult.TopIntent.Intent != intent.IntentQueryProgress { + t.Errorf("expected QUERY_PROGRESS, got %s", resp.IntentResult.TopIntent.Intent) + } + if !strings.Contains(resp.ReplyText, "功耗") && !strings.Contains(resp.ReplyText, "架构") { + t.Errorf("expected technical answer in reply: %s", resp.ReplyText) + } +} + +// 用例 5: 分支 3 模糊分忧 ("这个先放放,你觉得呢") +func TestChatSession_Branch3_AmbiguousConsult(t *testing.T) { + rec := intent.NewRecognizer() + engine := coordinator.NewCoordinatorEngine(nil) + userCtx := &coordinator.UserProjectContext{ + Project: coordinator.ProjectInfo{ + Name: "智能输注泵项目", + CurrentStage: bizdev.StageV01_Triage, + }, + } + + session := chat.NewSession("sess_consult", userCtx, rec, engine) + resp, err := session.SendMessage(context.Background(), "这个先放放,你觉得呢") + if err != nil { + t.Fatalf("send message failed: %v", err) + } + + if resp.Branch != intent.BranchAmbiguousFallback { + t.Errorf("expected AMBIGUOUS_FALLBACK, got %s", resp.Branch) + } + if !strings.Contains(resp.ReplyText, "审慎考量") && !strings.Contains(resp.ReplyText, "参谋建议") { + t.Errorf("expected thoughtful advisor consult in reply: %s", resp.ReplyText) + } + if session.UserCtx.PendingExpectation == nil { + t.Errorf("expected NextExpectation to be populated for user choice") + } +}