262 lines
10 KiB
Go
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")
|
|
}
|
|
}
|