feat(chat): integrate non-blocking pending expectation and topic preemption in session

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
WeiCHEN
2026-09-07 22:11:51 +08:00
co-authored by Cursor
parent 017b53d634
commit a0a9b03e28
2 changed files with 189 additions and 14 deletions
+28 -8
View File
@@ -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
}
+161 -6
View File
@@ -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")
}
}