Files

954 lines
30 KiB
JavaScript
Raw Permalink Normal View History

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);
});