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

262 lines
10 KiB
Go

package coordinator
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
"chat-agent-system/internal/agent"
"chat-agent-system/internal/bizdev"
"chat-agent-system/internal/intent"
)
type scriptedDialogueBackend struct {
lastPrompt string
lastInstructions string
reply string
fail bool
}
func (s *scriptedDialogueBackend) Name() string { return "scripted-dialogue" }
func (s *scriptedDialogueBackend) Execute(ctx context.Context, req agent.Request) (*agent.Result, error) {
s.lastPrompt = req.Prompt
s.lastInstructions = req.Instructions
if s.fail {
return &agent.Result{Success: false, ErrorReason: "forced failure"}, nil
}
_ = os.MkdirAll(req.WorkDir, 0o755)
out := s.reply
if out == "" {
out = "LLM 参谋测试答复:成都今日多云,华西学术会议可带样机展示空化吸引能力。"
}
path := filepath.Join(req.WorkDir, "reply.md")
_ = os.WriteFile(path, []byte(out), 0o644)
return &agent.Result{
Success: true,
Patches: map[string][]byte{"reply.md": []byte(out)},
}, nil
}
func TestDialogueLLM_CasualChatUsesBackend(t *testing.T) {
backend := &scriptedDialogueBackend{}
tmp := t.TempDir()
engine := NewCoordinatorEngineWithDialogue(nil, backend, tmp)
userCtx := &UserProjectContext{
Project: ProjectInfo{
ID: "p-dialogue-1",
Name: "CUSA-Dialogue-Test",
CurrentStage: bizdev.StageV01_Triage,
},
BlackboardFacts: map[string]string{},
RecentDialogue: []DialogueTurn{
{Role: "user", Content: "你好"},
{Role: "assistant", Content: "您好,我是参谋。"},
},
}
rec := &intent.IntentRecognitionResult{
RawInput: "参谋早上好!今天成都的天气如何?听说下周华西医院有学术会议。",
Branch: intent.BranchNewIntentPreempt,
TopIntent: &intent.CandidateIntent{
Intent: intent.IntentCasualChat,
Confidence: 0.85,
Branch: intent.BranchNewIntentPreempt,
},
}
res, err := engine.ProcessIntent(context.Background(), rec, userCtx)
if err != nil {
t.Fatalf("ProcessIntent: %v", err)
}
if res.ReplySource != ReplySourcePiAgent {
t.Fatalf("expected reply_source=pi-agent, got %q", res.ReplySource)
}
if !strings.Contains(res.NaturalReply, "成都") && !strings.Contains(res.NaturalReply, "华西") {
t.Fatalf("expected LLM reply content, got: %s", res.NaturalReply)
}
if !strings.Contains(backend.lastPrompt, "近期对话") {
t.Fatalf("expected history in prompt, got: %s", backend.lastPrompt)
}
}
func TestDialogueLLM_AmbiguousFallbackUsesBackend(t *testing.T) {
backend := &scriptedDialogueBackend{
reply: "逆压电效应是指对压电陶瓷施加交变电场后产生机械振动;变幅杆通过截面变化实现阻抗匹配与振幅放大。",
}
engine := NewCoordinatorEngineWithDialogue(nil, backend, t.TempDir())
userCtx := &UserProjectContext{
Project: ProjectInfo{Name: "CUSA", CurrentStage: bizdev.StageV01_Triage},
}
rec := &intent.IntentRecognitionResult{
RawInput: "请详细解释压电换能器的逆压电效应与变幅杆阻抗匹配。",
Branch: intent.BranchAmbiguousFallback,
TopIntent: &intent.CandidateIntent{
Intent: intent.IntentCasualChat,
Confidence: 0.2,
Branch: intent.BranchAmbiguousFallback,
},
}
res, err := engine.ProcessIntent(context.Background(), rec, userCtx)
if err != nil {
t.Fatalf("ProcessIntent: %v", err)
}
if res.ReplySource != ReplySourcePiAgent {
t.Fatalf("expected pi-agent, got %q reply=%s", res.ReplySource, res.NaturalReply)
}
if strings.Contains(res.NaturalReply, "V0.5") && strings.Contains(res.NaturalReply, "方案框架") {
t.Fatalf("should not fall back to stage-sales template: %s", res.NaturalReply)
}
if !strings.Contains(res.NaturalReply, "压电") && !strings.Contains(res.NaturalReply, "变幅杆") {
t.Fatalf("expected technical answer, got: %s", res.NaturalReply)
}
}
func TestDialogueLLM_FallbackToTemplateOnBackendFailure(t *testing.T) {
backend := &scriptedDialogueBackend{fail: true}
engine := NewCoordinatorEngineWithDialogue(nil, backend, t.TempDir())
userCtx := &UserProjectContext{
Project: ProjectInfo{Name: "CUSA", CurrentStage: bizdev.StageV01_Triage},
}
rec := &intent.IntentRecognitionResult{
RawInput: "你好",
Branch: intent.BranchNewIntentPreempt,
TopIntent: &intent.CandidateIntent{
Intent: intent.IntentCasualChat,
Confidence: 0.9,
Branch: intent.BranchNewIntentPreempt,
},
}
res, err := engine.ProcessIntent(context.Background(), rec, userCtx)
if err != nil {
t.Fatalf("ProcessIntent: %v", err)
}
if res.ReplySource != ReplySourceTemplate {
t.Fatalf("expected template fallback, got %q", res.ReplySource)
}
if !strings.Contains(res.NaturalReply, "项目助手") {
t.Fatalf("expected template greeting, got: %s", res.NaturalReply)
}
}
func TestDialogueLLM_PersonalizedGuidanceAndWarmPersona(t *testing.T) {
backend := &scriptedDialogueBackend{
reply: "好的,我帮您看一下。核心结论是双频方案可行;如果您接下来要准备对华西的汇报,我可以先整理一页卖点提纲。",
}
engine := NewCoordinatorEngineWithDialogue(nil, backend, t.TempDir())
userCtx := &UserProjectContext{
User: UserInfo{Name: "张工", Role: "engineer", ProjectRole: "composer"},
Project: ProjectInfo{
Name: "CUSA-Warm",
CurrentStage: bizdev.StageV01_Triage,
},
PersonalizedGuidance: "【用户个性化工作习惯与偏好指导 (Memory Injection)】\n• 沟通风格:该用户偏好【结构精炼、结论先行】\n• 重点关注:该用户较关心【法规标准合规与审评审批风险】\n",
Pending: PendingItems{
MyDraftsInProgress: []string{"用户初始需求收集表"},
},
}
rec := &intent.IntentRecognitionResult{
RawInput: "这个技术方案你怎么看?",
Branch: intent.BranchNewIntentPreempt,
TopIntent: &intent.CandidateIntent{
Intent: intent.IntentCasualChat,
Confidence: 0.8,
Branch: intent.BranchNewIntentPreempt,
},
}
res, err := engine.ProcessIntent(context.Background(), rec, userCtx)
if err != nil {
t.Fatalf("ProcessIntent: %v", err)
}
if res.ReplySource != ReplySourcePiAgent {
t.Fatalf("expected pi-agent, got %q", res.ReplySource)
}
if !strings.Contains(backend.lastPrompt, "张工") {
t.Fatalf("expected user name in prompt, got: %s", backend.lastPrompt)
}
if !strings.Contains(backend.lastPrompt, "结构精炼、结论先行") {
t.Fatalf("expected personalized guidance in prompt, got: %s", backend.lastPrompt)
}
if !strings.Contains(backend.lastPrompt, "用户初始需求收集表") {
t.Fatalf("expected pending draft in prompt, got: %s", backend.lastPrompt)
}
if !strings.Contains(backend.lastInstructions, "替用户着想") && !strings.Contains(backend.lastInstructions, "站在用户视角") {
t.Fatalf("expected warm user-centric instructions, got: %s", backend.lastInstructions)
}
}
// TestDialogueLLM_QueryProgressNoBriefUsesBackend 无简报的风险/进展查询优先真实 LLM 作答
// (Phase A 机制延伸),修复新会话询问风险时返回 V0.1 推销模板的答非所问问题。
func TestDialogueLLM_QueryProgressNoBriefUsesBackend(t *testing.T) {
backend := &scriptedDialogueBackend{
reply: "关于该项目的风险研判:当前最大的卡点是器械分类界定尚不明确,若宣称含治疗作用可能升档为三类,注册周期将增加 6-12 个月;其次是预算与交付节点尚缺客户书面口径,直接对外报价存在争议风险。",
}
engine := NewCoordinatorEngineWithDialogue(nil, backend, t.TempDir())
userCtx := &UserProjectContext{
User: UserInfo{Name: "张工", ProjectRole: "composer"},
Project: ProjectInfo{Name: "医疗器械研发项目", CurrentStage: bizdev.StageV01_Triage},
}
rec := &intent.IntentRecognitionResult{
RawInput: "简单分析一下这个项目有什么风险",
Branch: intent.BranchNewIntentPreempt,
TopIntent: &intent.CandidateIntent{
Intent: intent.IntentQueryProgress,
Confidence: 0.9,
Branch: intent.BranchNewIntentPreempt,
FilledSlots: map[string]string{intent.SlotQueryTopic: "risk"},
},
}
res, err := engine.ProcessIntent(context.Background(), rec, userCtx)
if err != nil {
t.Fatalf("ProcessIntent: %v", err)
}
if res.ReplySource != ReplySourcePiAgent {
t.Fatalf("expected reply_source=pi-agent, got %q reply=%s", res.ReplySource, res.NaturalReply)
}
if !strings.Contains(res.NaturalReply, "风险研判") {
t.Fatalf("expected LLM reply content, got: %s", res.NaturalReply)
}
if strings.Contains(res.NaturalReply, "当前处于初始准备期") {
t.Fatalf("should not regress to canned V0.1 sales pitch: %s", res.NaturalReply)
}
}
// TestDialogueLLM_QueryProgressNoBriefFallsBackToSynthesizedTemplate LLM 失败时回落到
// 合成基准简报模板:先给出实质风险研判,再附轻量摸底引导。
func TestDialogueLLM_QueryProgressNoBriefFallsBackToSynthesizedTemplate(t *testing.T) {
backend := &scriptedDialogueBackend{fail: true}
engine := NewCoordinatorEngineWithDialogue(nil, backend, t.TempDir())
userCtx := &UserProjectContext{
Project: ProjectInfo{Name: "医疗器械研发项目", CurrentStage: bizdev.StageV01_Triage},
}
rec := &intent.IntentRecognitionResult{
RawInput: "简单分析一下这个项目有什么风险",
Branch: intent.BranchNewIntentPreempt,
TopIntent: &intent.CandidateIntent{
Intent: intent.IntentQueryProgress,
Confidence: 0.9,
Branch: intent.BranchNewIntentPreempt,
FilledSlots: map[string]string{intent.SlotQueryTopic: "risk"},
},
}
res, err := engine.ProcessIntent(context.Background(), rec, userCtx)
if err != nil {
t.Fatalf("ProcessIntent: %v", err)
}
if res.ReplySource != ReplySourceTemplate {
t.Fatalf("expected template fallback, got %q", res.ReplySource)
}
if !strings.Contains(res.NaturalReply, "潜在风险") {
t.Fatalf("expected substantive risk analysis fallback, got: %s", res.NaturalReply)
}
if !strings.Contains(res.NaturalReply, "摸底") {
t.Fatalf("expected light triage guidance appended, got: %s", res.NaturalReply)
}
if res.AdvisorBrief == nil {
t.Fatalf("expected synthesized baseline brief in fallback result")
}
}