Files
memind/chat-intent-router.test.mjs
T
john 14a00774d9 feat(h5): web 联网能力、实时查询路由与 session Finish 对齐
- 新增 web 能力并挂载 platform/web(web_search/fetch_url)
- 实时查询强制 web skill,router fallback 与 await session Finish
- Session Broker 覆盖率/指标、stream replay 与相关单测/E2E 脚本

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 16:06:26 +08:00

954 lines
30 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from 'node:assert/strict';
import test from 'node:test';
import {
applyAgentOrchestrationToUserMessage,
buildRouterContext,
buildAgentOrchestrationAgentText,
buildRouterNormalizedDecision,
CHAT_INTENT_ROUTE,
classifyWithRules,
createChatIntentRouter,
createManagedChatIntentRouter,
isNormalizedRouterDecisionEnabled,
isNormalizedRouterDecisionShadow,
resolveGatewayAgentSessionId,
resolveLegacyRouteFromClassification,
resolveNormalizedRouterDecisionMode,
logRouterDecisionShadow,
ROUTER_DECISION_ROUTE,
ROUTER_DECISION_MODE,
ROUTER_DECISION_SESSION_HINT,
isRealtimeInfoQuestion,
coerceRealtimeWebSkill,
resolveChatIntentRouterPolicy,
} from './chat-intent-router.mjs';
test('classifyWithRules routes greetings to direct chat', () => {
const result = classifyWithRules({
text: '你好',
userMessage: {
role: 'user',
content: [{ type: 'text', text: '你好' }],
metadata: { displayText: '你好' },
},
});
assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
assert.equal(result.source, 'rule');
});
test('classifyWithRules routes page generation to agent orchestration', () => {
const result = classifyWithRules({
text: '帮我做一个秋夜诗的 H5 页面',
userMessage: {
role: 'user',
content: [{ type: 'text', text: '帮我做一个秋夜诗的 H5 页面' }],
metadata: { displayText: '帮我做一个秋夜诗的 H5 页面' },
},
});
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
assert.equal(result.suggestedSkill, 'static-page-publish');
});
test('classifyWithRules routes themed article requests to agent orchestration', () => {
const result = classifyWithRules({
text: '帮我写个主题文章',
userMessage: {
role: 'user',
content: [{ type: 'text', text: '帮我写个主题文章' }],
metadata: { displayText: '帮我写个主题文章' },
},
});
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
assert.equal(result.suggestedSkill, 'static-page-publish');
});
test('classifyWithRules honors forceDeepReasoning bypass', () => {
const result = classifyWithRules({
text: '你好',
forceDeepReasoning: true,
userMessage: {
role: 'user',
content: [{ type: 'text', text: '你好' }],
},
});
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
assert.match(result.reason, /深度推理/);
});
test('classifyWithRules skips session continuation when llm router is enabled', () => {
const continued = classifyWithRules({
text: '我想去日本玩',
sessionId: '20260704_10',
sessionMessageCount: 2,
llmRouterEnabled: true,
});
assert.equal(continued, null);
});
test('classifyWithRules keeps empty pre-created sessions eligible for llm routing', () => {
const continued = classifyWithRules({
text: '继续聊聊',
sessionId: '20260704_2',
sessionMessageCount: 2,
});
assert.equal(continued.route, CHAT_INTENT_ROUTE.AGENT);
assert.equal(continued.reason, '延续已有 Agent 会话');
const fresh = classifyWithRules({
text: '我是 John,你对我的记忆知道多少?',
sessionId: '20260704_3',
sessionMessageCount: 0,
});
assert.equal(fresh.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
assert.match(fresh.reason, /记忆/);
});
test('classifyWithRules routes memory recall to direct chat even in agent session', () => {
const result = classifyWithRules({
text: '你记得我说想去哪儿吗',
sessionId: '20260704_9',
sessionMessageCount: 4,
userMessage: {
role: 'user',
content: [{ type: 'text', text: '你记得我说想去哪儿吗' }],
metadata: { displayText: '你记得我说想去哪儿吗' },
},
});
assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
assert.match(result.reason, /记忆/);
});
test('classifyWithRules bypasses llm router when user selected summarize skill', async () => {
const llmCalls = [];
const router = createChatIntentRouter({
enabled: true,
llmProviderService: {
async createChatCompletion() {
llmCalls.push(1);
return { ok: true, reply: '{"route":"direct_chat","confidence":0.9,"reason":"x"}' };
},
},
memoryV2: {
async resolve() {
assert.fail('memory resolve should not run for explicit skill selection');
},
},
});
const prompt = '请总结以下内容,提炼核心结论、重点信息和可执行建议(条理清晰、中文输出):';
const result = await router.classify({
userMessage: {
role: 'user',
content: [{ type: 'text', text: `${prompt}这是待总结的正文` }],
metadata: {
displayText: `${prompt}这是待总结的正文`,
memindRun: { selectedChatSkill: 'summarize' },
},
},
});
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
assert.equal(result.source, 'rule');
assert.equal(result.reason, '用户已选择 skill');
assert.equal(llmCalls.length, 0);
});
test('classifyWithRules bypasses llm router for explicit web skill prompt', async () => {
const llmCalls = [];
const router = createChatIntentRouter({
enabled: true,
llmProviderService: {
async createChatCompletion() {
llmCalls.push(1);
return { ok: true, reply: '{"route":"direct_chat","confidence":0.9,"reason":"x"}' };
},
},
});
const result = await router.classify({
userMessage: {
role: 'user',
content: [{
type: 'text',
text: '请使用 web 技能:帮我搜索并查阅相关资料(优先官方文档),并给出中文摘要与来源链接。我的问题是:今天 AI 新闻',
}],
metadata: { displayText: '今天 AI 新闻' },
},
grantedSkills: ['web'],
});
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
assert.equal(result.suggestedSkill, 'web');
assert.equal(llmCalls.length, 0);
});
test('createChatIntentRouter classifies continued sessions through llm when enabled', async () => {
const llmCalls = [];
const router = createChatIntentRouter({
enabled: true,
modelProviderKeyId: 'key-router',
model: 'deepseek-chat',
llmProviderService: {
async createChatCompletion(payload) {
llmCalls.push(payload);
return {
ok: true,
reply: JSON.stringify({
route: 'agent_orchestration',
confidence: 0.88,
reason: '用户要规划日本旅行,需要搜索和生成内容',
suggested_skill: 'static-page-publish',
agent_brief: '整理日本旅行攻略',
}),
};
},
},
memoryV2: {
async resolve() {
return { source: 'legacy-conversation-memory', memories: [{ label: 'goal', text: '用户想去日本旅游' }] };
},
},
});
const result = await router.classify({
userId: 'user-john',
sessionId: '20260704_10',
sessionMessageCount: 2,
userMessage: {
role: 'user',
content: [{ type: 'text', text: '我想去日本玩' }],
metadata: { displayText: '我想去日本玩' },
},
});
assert.equal(llmCalls.length, 1);
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
assert.equal(result.source, 'llm');
assert.match(result.reason, /日本旅行/);
assert.equal(result.memory?.itemsUsed ?? 0, 0);
});
test('createChatIntentRouter skips memory resolve for general conversational analysis', async () => {
const llmCalls = [];
const resolveCalls = [];
const router = createChatIntentRouter({
enabled: true,
modelProviderKeyId: 'key-router',
model: 'deepseek-chat',
llmProviderService: {
async createChatCompletion({ messages, providerKeyId, model, temperature }) {
llmCalls.push({ messages, providerKeyId, model, temperature });
return {
ok: true,
providerKeyId,
model,
reply: JSON.stringify({
route: 'direct_chat',
confidence: 0.91,
reason: '用户在咨询概念,不需要执行工具',
suggested_skill: null,
agent_brief: '',
}),
};
},
},
memoryV2: {
async resolve(input) {
resolveCalls.push(input);
return {
source: 'pgvector',
memories: [{ label: 'preference', text: '用户偏好简洁回答,不喜欢冗长铺垫' }],
activeGoals: ['完善春季促销活动页'],
};
},
},
});
const result = await router.classify({
userId: 'user-1',
userMessage: {
role: 'user',
content: [{ type: 'text', text: 'React 19 有哪些新特性?' }],
metadata: { displayText: 'React 19 有哪些新特性?' },
},
grantedSkills: ['web'],
});
assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
assert.equal(result.source, 'llm');
assert.equal(result.providerKeyId, 'key-router');
assert.equal(result.model, 'deepseek-chat');
assert.equal(result.memory?.itemsUsed ?? 0, 0);
assert.equal(resolveCalls.length, 0);
assert.equal(llmCalls.length, 1);
assert.equal(llmCalls[0].providerKeyId, 'key-router');
assert.equal(llmCalls[0].model, 'deepseek-chat');
assert.equal(llmCalls[0].temperature, 0);
assert.doesNotMatch(llmCalls[0].messages[1].content, /相关记忆/);
});
test('createChatIntentRouter resolves light memory for recall questions', async () => {
const resolveCalls = [];
const router = createChatIntentRouter({
enabled: true,
modelProviderKeyId: 'key-router',
model: 'deepseek-chat',
llmProviderService: {
async createChatCompletion() {
return {
ok: true,
reply: JSON.stringify({
route: 'direct_chat',
confidence: 0.95,
reason: '用户在询问个人记忆',
suggested_skill: null,
agent_brief: '',
}),
};
},
},
memoryV2: {
async resolve(input) {
resolveCalls.push(input);
return {
source: 'pgvector',
memories: [{ label: 'goal', text: '用户想去日本旅游' }],
};
},
},
});
const result = await router.classify({
userId: 'user-john',
userMessage: {
role: 'user',
content: [{ type: 'text', text: '你记得我说想去哪儿吗?' }],
metadata: { displayText: '你记得我说想去哪儿吗?' },
},
});
assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
assert.equal(resolveCalls.length, 1);
assert.equal(resolveCalls[0].limit, 3);
assert.equal(result.memory?.itemsUsed, 1);
});
test('buildRouterContext trims rich Memory V2 resolve payload for routing', () => {
const context = buildRouterContext({
source: 'letta',
memories: [
{ label: 'preference', text: '用户偏好简洁回答,不喜欢冗长铺垫,尤其在工程排障时希望先给结论。' },
{ label: 'habit', text: '用户经常要求生成 H5 活动页并发布到 MindSpace。' },
],
semanticMemories: ['上次正在修改活动页标题样式'],
behaviorSummary: '用户倾向先本地验证,再决定是否发布生产。额外句子不应进入轻量摘要。',
activeGoals: ['完善春季促销活动页'],
profile: { ignored: true },
});
assert.equal(context.source, 'letta');
assert.equal(context.itemsUsed, 5);
assert.match(context.content, /相关记忆/);
assert.match(context.content, /行为摘要/);
assert.match(context.content, /进行中目标/);
assert.doesNotMatch(context.content, /ignored/);
});
test('createChatIntentRouter fails open when Memory V2 resolve fails', async () => {
const router = createChatIntentRouter({
enabled: true,
llmProviderService: {
async createChatCompletion({ messages }) {
assert.match(messages[1].content, /\[Router Context\]\n无/);
return {
ok: true,
reply: JSON.stringify({
route: 'agent_orchestration',
confidence: 0.82,
reason: '需要工具执行',
suggested_skill: 'web',
agent_brief: '搜索并整理来源',
}),
};
},
},
memoryV2: {
async resolve() {
throw new Error('memory down');
},
},
logger: { warn() {} },
});
const result = await router.classify({
userId: 'user-1',
userMessage: {
role: 'user',
content: [{ type: 'text', text: '你记得我之前说过什么吗?' }],
metadata: { displayText: '你记得我之前说过什么吗?' },
},
grantedSkills: ['web'],
});
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
assert.equal(result.memory.degraded, true);
});
test('createManagedChatIntentRouter hot-loads admin config and stays closed by default', async () => {
const states = [
{
fingerprint: 'off',
overrides: {
MEMIND_CHAT_LLM_ROUTER_ENABLED: '0',
},
},
{
fingerprint: 'on',
overrides: {
MEMIND_CHAT_LLM_ROUTER_ENABLED: '1',
MEMIND_CHAT_ROUTER_MODEL_PROVIDER_KEY_ID: 'key-router',
MEMIND_CHAT_ROUTER_MODEL: 'deepseek-chat',
},
},
];
const llmCalls = [];
const router = createManagedChatIntentRouter({
configService: {
async getRuntimeState() {
return states[0];
},
},
llmProviderService: {
async createChatCompletion(input) {
llmCalls.push(input);
return {
ok: true,
providerKeyId: input.providerKeyId,
model: input.model,
reply: JSON.stringify({
route: 'direct_chat',
confidence: 0.9,
reason: '纯文字问答',
suggested_skill: null,
agent_brief: '',
}),
};
},
},
});
assert.equal(await router.isEnabled(), false);
states.shift();
assert.equal(await router.isEnabled(), true);
const result = await router.classify({
userMessage: {
role: 'user',
content: [{ type: 'text', text: '帮我概括一下这段材料的要点' }],
metadata: { displayText: '帮我概括一下这段材料的要点' },
},
});
assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
assert.equal(llmCalls[0].providerKeyId, 'key-router');
});
test('createChatIntentRouter escalates low-confidence direct chat to agent', async () => {
const router = createChatIntentRouter({
enabled: true,
minConfidence: 0.8,
llmProviderService: {
async createChatCompletion() {
return {
ok: true,
reply: JSON.stringify({
route: 'direct_chat',
confidence: 0.42,
reason: '可能是简单问答',
suggested_skill: null,
agent_brief: '',
}),
};
},
},
});
const result = await router.classify({
userMessage: {
role: 'user',
content: [{ type: 'text', text: '帮我整理一下这段材料' }],
metadata: { displayText: '帮我整理一下这段材料' },
},
});
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
assert.equal(result.source, 'threshold');
});
test('createChatIntentRouter falls back to agent when LLM fails', async () => {
const router = createChatIntentRouter({
enabled: true,
llmProviderService: {
async createChatCompletion() {
return { ok: false, message: 'router model unavailable' };
},
},
});
const result = await router.classify({
userMessage: {
role: 'user',
content: [{ type: 'text', text: '帮我概括一下这段材料的要点' }],
metadata: { displayText: '帮我概括一下这段材料的要点' },
},
grantedSkills: ['web'],
});
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
assert.equal(result.source, 'fallback');
});
test('createChatIntentRouter fast-paths news lookup to agent without LLM', async () => {
let llmCalls = 0;
const router = createChatIntentRouter({
enabled: true,
llmProviderService: {
async createChatCompletion() {
llmCalls += 1;
return { ok: false, message: 'router model unavailable' };
},
},
});
const result = await router.classify({
userMessage: {
role: 'user',
content: [{ type: 'text', text: '帮我查一下今天的 AI 新闻' }],
metadata: { displayText: '帮我查一下今天的 AI 新闻' },
},
grantedSkills: ['web'],
});
assert.equal(llmCalls, 0);
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
assert.equal(result.source, 'rule');
});
test('applyAgentOrchestrationToUserMessage preserves displayText and adds task envelope', () => {
const enriched = applyAgentOrchestrationToUserMessage(
{
role: 'user',
content: [{ type: 'text', text: '帮我做一个页面' }],
metadata: { displayText: '帮我做一个页面', userVisible: true },
},
{
route: CHAT_INTENT_ROUTE.AGENT,
confidence: 0.9,
reason: '需要生成页面',
suggestedSkill: 'static-page-publish',
agentBrief: '生成 HTML 并发布',
source: 'llm',
},
{ grantedSkills: ['static-page-publish'] },
);
assert.equal(enriched.metadata.displayText, '帮我做一个页面');
assert.match(enriched.content[0].text, /Memind 任务编排/);
assert.match(enriched.content[0].text, /static-page-publish/);
assert.match(enriched.content[0].text, /帮我做一个页面/);
});
test('buildAgentOrchestrationAgentText includes execution brief', () => {
const text = buildAgentOrchestrationAgentText({
displayText: '帮我搜索今天的新闻',
classification: {
reason: '需要实时搜索',
suggestedSkill: 'web',
agentBrief: '使用 web skill 搜索并整理来源',
},
skillPrompt: '请使用 web 技能:',
});
assert.match(text, /需要实时搜索/);
assert.match(text, /帮我搜索今天的新闻/);
assert.match(text, /请使用 web 技能/);
});
test('classifyWithRules attaches normalized decision on every rule result', () => {
const result = classifyWithRules({
text: '你好',
userMessage: {
role: 'user',
content: [{ type: 'text', text: '你好' }],
metadata: { displayText: '你好' },
},
});
assert.ok(result.decision);
assert.equal(result.decision.route, ROUTER_DECISION_ROUTE.CHAT);
assert.equal(result.decision.mode, ROUTER_DECISION_MODE.SSE);
assert.equal(result.decision.session_hint, ROUTER_DECISION_SESSION_HINT.NEW);
});
test('buildRouterNormalizedDecision maps legacy routes and session hints', () => {
const continued = buildRouterNormalizedDecision(
{ route: CHAT_INTENT_ROUTE.AGENT, suggestedSkill: 'web' },
{
text: '继续',
sessionId: '20260704_10',
sessionMessageCount: 2,
toolMode: 'chat',
},
);
assert.equal(continued.route, ROUTER_DECISION_ROUTE.AGENT);
assert.equal(continued.session_hint, ROUTER_DECISION_SESSION_HINT.REUSE);
assert.ok(continued.flags.includes('selected_skill'));
const freshAgentSession = buildRouterNormalizedDecision(
{ route: CHAT_INTENT_ROUTE.AGENT },
{
text: '你好',
sessionId: '20260704_11',
sessionMessageCount: 0,
},
);
assert.equal(freshAgentSession.session_hint, ROUTER_DECISION_SESSION_HINT.NEW);
const memoryRecall = buildRouterNormalizedDecision(
{ route: CHAT_INTENT_ROUTE.DIRECT_CHAT },
{ text: '你记得我说过什么吗' },
);
assert.equal(memoryRecall.route, ROUTER_DECISION_ROUTE.CHAT);
assert.ok(memoryRecall.flags.includes('memory_recall'));
});
test('resolveLegacyRouteFromClassification prefers decision when flag enabled', () => {
const classification = {
route: CHAT_INTENT_ROUTE.DIRECT_CHAT,
decision: { route: ROUTER_DECISION_ROUTE.AGENT },
};
assert.equal(
resolveLegacyRouteFromClassification(classification),
CHAT_INTENT_ROUTE.DIRECT_CHAT,
);
const previous = process.env.MEMIND_ROUTER_NORMALIZED_DECISION;
process.env.MEMIND_ROUTER_NORMALIZED_DECISION = '1';
try {
assert.equal(
resolveLegacyRouteFromClassification(classification),
CHAT_INTENT_ROUTE.AGENT,
);
assert.equal(isNormalizedRouterDecisionEnabled(), true);
} finally {
if (previous == null) delete process.env.MEMIND_ROUTER_NORMALIZED_DECISION;
else process.env.MEMIND_ROUTER_NORMALIZED_DECISION = previous;
}
});
test('resolveGatewayAgentSessionId honors session_hint without breaking hard rules', () => {
const previous = process.env.MEMIND_ROUTER_NORMALIZED_DECISION;
process.env.MEMIND_ROUTER_NORMALIZED_DECISION = '1';
try {
assert.equal(
resolveGatewayAgentSessionId({
agentSessionId: 'sess-1',
classification: {
decision: { session_hint: ROUTER_DECISION_SESSION_HINT.REUSE },
},
}),
'sess-1',
);
assert.equal(
resolveGatewayAgentSessionId({
agentSessionId: 'sess-1',
classification: {
decision: { session_hint: ROUTER_DECISION_SESSION_HINT.NEW },
},
}),
null,
);
assert.equal(
resolveGatewayAgentSessionId({
agentSessionId: 'h5direct_abc',
classification: {
decision: { session_hint: ROUTER_DECISION_SESSION_HINT.NEW },
},
}),
'h5direct_abc',
);
assert.equal(
resolveGatewayAgentSessionId({
agentSessionId: 'sess-1',
forceDeepReasoning: true,
classification: {
decision: { session_hint: ROUTER_DECISION_SESSION_HINT.NEW },
},
}),
'sess-1',
);
} finally {
if (previous == null) delete process.env.MEMIND_ROUTER_NORMALIZED_DECISION;
else process.env.MEMIND_ROUTER_NORMALIZED_DECISION = previous;
}
});
test('resolveNormalizedRouterDecisionMode supports off, shadow, and on', () => {
const previous = process.env.MEMIND_ROUTER_NORMALIZED_DECISION;
try {
delete process.env.MEMIND_ROUTER_NORMALIZED_DECISION;
assert.equal(resolveNormalizedRouterDecisionMode(), 'off');
process.env.MEMIND_ROUTER_NORMALIZED_DECISION = 'shadow';
assert.equal(resolveNormalizedRouterDecisionMode(), 'shadow');
assert.equal(isNormalizedRouterDecisionShadow(), true);
assert.equal(isNormalizedRouterDecisionEnabled(), false);
process.env.MEMIND_ROUTER_NORMALIZED_DECISION = '1';
assert.equal(resolveNormalizedRouterDecisionMode(), 'on');
assert.equal(isNormalizedRouterDecisionEnabled(), true);
} finally {
if (previous == null) delete process.env.MEMIND_ROUTER_NORMALIZED_DECISION;
else process.env.MEMIND_ROUTER_NORMALIZED_DECISION = previous;
}
});
test('shadow mode keeps legacy route for gateway decisions', () => {
const classification = {
route: CHAT_INTENT_ROUTE.DIRECT_CHAT,
decision: { route: ROUTER_DECISION_ROUTE.AGENT },
};
const previous = process.env.MEMIND_ROUTER_NORMALIZED_DECISION;
process.env.MEMIND_ROUTER_NORMALIZED_DECISION = 'shadow';
try {
assert.equal(resolveLegacyRouteFromClassification(classification), CHAT_INTENT_ROUTE.DIRECT_CHAT);
assert.equal(isNormalizedRouterDecisionEnabled(), false);
} finally {
if (previous == null) delete process.env.MEMIND_ROUTER_NORMALIZED_DECISION;
else process.env.MEMIND_ROUTER_NORMALIZED_DECISION = previous;
}
});
test('classifyWithRules fast-paths bedtime story when llm router is enabled', () => {
const result = classifyWithRules({
text: '讲一个睡前故事吧',
llmRouterEnabled: true,
sessionId: '20260706_2',
sessionMessageCount: 3,
userMessage: {
role: 'user',
content: [{ type: 'text', text: '讲一个睡前故事吧' }],
metadata: { displayText: '讲一个睡前故事吧' },
},
});
assert.equal(result?.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
assert.equal(result?.source, 'rule');
assert.equal(result?.decision?.route, ROUTER_DECISION_ROUTE.CHAT);
});
test('classifyWithRules fast-paths world cup standings to agent orchestration', () => {
const text = '世界杯现在赛况如何';
assert.equal(isRealtimeInfoQuestion(text), true);
const result = classifyWithRules({
text,
llmRouterEnabled: true,
userMessage: {
role: 'user',
content: [{ type: 'text', text }],
metadata: { displayText: text },
},
});
assert.equal(result?.route, CHAT_INTENT_ROUTE.AGENT);
assert.equal(result?.source, 'rule');
assert.equal(result?.suggestedSkill, 'web');
assert.match(result?.reason, /实时/);
});
test('createChatIntentRouter fast-paths world cup without calling router LLM', async () => {
let llmCalls = 0;
const router = createChatIntentRouter({
enabled: true,
llmProviderService: {
async createChatCompletion() {
llmCalls += 1;
throw new Error('router should not be called');
},
},
});
const result = await router.classify({
userMessage: {
role: 'user',
content: [{ type: 'text', text: '世界杯现在赛况如何' }],
metadata: { displayText: '世界杯现在赛况如何' },
},
grantedSkills: ['web'],
});
assert.equal(llmCalls, 0);
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
assert.equal(result.source, 'rule');
});
test('resolveChatIntentRouterPolicy coerces direct_chat fallback to agent', () => {
const policy = resolveChatIntentRouterPolicy({
env: {
MEMIND_CHAT_ROUTER_FALLBACK_ROUTE: 'direct_chat',
},
});
assert.equal(policy.fallbackRoute, CHAT_INTENT_ROUTE.AGENT);
});
test('coerceRealtimeWebSkill replaces search with web for world cup query', () => {
const coerced = coerceRealtimeWebSkill(
{
route: CHAT_INTENT_ROUTE.AGENT,
confidence: 0.9,
reason: '用户询问实时赛况,需要搜索最新信息',
suggestedSkill: 'search',
source: 'llm',
},
'世界杯现在赛况如何',
{ grantedSkills: ['web', 'search'] },
);
assert.equal(coerced.suggestedSkill, 'web');
assert.match(coerced.agentBrief, /web_search/);
});
test('applyAgentOrchestrationToUserMessage uses web news prompt for realtime query', () => {
const enriched = applyAgentOrchestrationToUserMessage(
{
role: 'user',
content: [{ type: 'text', text: '世界杯现在赛况如何' }],
metadata: { displayText: '世界杯现在赛况如何', userVisible: true },
},
coerceRealtimeWebSkill(
{
route: CHAT_INTENT_ROUTE.AGENT,
confidence: 0.9,
reason: '需要搜索实时资料',
suggestedSkill: 'web',
source: 'rule',
},
'世界杯现在赛况如何',
{ grantedSkills: ['web'] },
),
{ grantedSkills: ['web'] },
);
assert.match(enriched.content[0].text, /请使用 web 技能:先搜索今天\/最新相关的新闻与热点/);
assert.doesNotMatch(enriched.content[0].text, /search 技能:帮我在工作区中查找代码或文件/);
assert.match(enriched.content[0].text, /web_search/);
});
test('createChatIntentRouter coerces LLM search route to web for realtime query', async () => {
const router = createChatIntentRouter({
enabled: true,
llmProviderService: {
async createChatCompletion() {
return {
ok: true,
reply: JSON.stringify({
route: 'agent_orchestration',
confidence: 0.92,
reason: '用户询问实时赛况,需要搜索最新信息',
suggested_skill: 'search',
agent_brief: '搜索赛况',
}),
};
},
},
});
const result = await router.classify({
userMessage: {
role: 'user',
content: [{ type: 'text', text: '世界杯现在赛况如何' }],
metadata: { displayText: '世界杯现在赛况如何' },
},
grantedSkills: ['web', 'search'],
});
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
assert.equal(result.suggestedSkill, 'web');
assert.match(result.agentBrief, /web_search/);
});
test('createChatIntentRouter timeout fallback prefers agent even when policy requests direct_chat', async () => {
const router = createChatIntentRouter({
enabled: true,
timeoutMs: 5,
fallbackRoute: CHAT_INTENT_ROUTE.DIRECT_CHAT,
llmProviderService: {
async createChatCompletion() {
await new Promise((resolve) => setTimeout(resolve, 20));
return { ok: true, reply: '{"route":"direct_chat","confidence":0.9,"reason":"x","suggested_skill":null,"agent_brief":""}' };
},
},
});
const result = await router.classify({
userMessage: {
role: 'user',
content: [{ type: 'text', text: '帮我整理一下这段材料' }],
metadata: { displayText: '帮我整理一下这段材料' },
},
});
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
assert.equal(result.source, 'fallback');
});
test('logRouterDecisionShadow emits payload only in shadow mode', () => {
const previous = process.env.MEMIND_ROUTER_NORMALIZED_DECISION;
const lines = [];
const logger = { log: (line) => lines.push(line) };
const classification = {
route: CHAT_INTENT_ROUTE.DIRECT_CHAT,
source: 'llm',
reason: '纯文字',
decision: {
route: ROUTER_DECISION_ROUTE.AGENT,
mode: ROUTER_DECISION_MODE.SSE,
session_hint: ROUTER_DECISION_SESSION_HINT.REUSE,
flags: [],
},
};
try {
process.env.MEMIND_ROUTER_NORMALIZED_DECISION = '0';
logRouterDecisionShadow(classification, { logger, requestId: 'req-1' });
assert.equal(lines.length, 0);
process.env.MEMIND_ROUTER_NORMALIZED_DECISION = 'shadow';
logRouterDecisionShadow(classification, { logger, requestId: 'req-1' });
assert.equal(lines.length, 1);
assert.match(lines[0], /\[router-shadow\]/);
assert.match(lines[0], /wouldChangeRoute":true/);
} finally {
if (previous == null) delete process.env.MEMIND_ROUTER_NORMALIZED_DECISION;
else process.env.MEMIND_ROUTER_NORMALIZED_DECISION = previous;
}
});
test('createChatIntentRouter accepts chat and agent route synonyms from llm', async () => {
const router = createChatIntentRouter({
enabled: true,
llmProviderService: {
async createChatCompletion() {
return {
ok: true,
reply: JSON.stringify({
route: 'chat',
confidence: 0.92,
reason: '纯问答',
suggested_skill: null,
agent_brief: '',
}),
};
},
},
});
const result = await router.classify({
userMessage: {
role: 'user',
content: [{ type: 'text', text: '什么是 SSE' }],
metadata: { displayText: '什么是 SSE' },
},
});
assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
assert.equal(result.decision.route, ROUTER_DECISION_ROUTE.CHAT);
});