Files
memind/chat-intent-router.test.mjs
2026-07-25 21:55:00 +08:00

1408 lines
46 KiB
JavaScript
Raw Permalink 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,
coercePageDataSkill,
coercePageGenerationSkill,
createChatIntentRouter,
createManagedChatIntentRouter,
isNormalizedRouterDecisionEnabled,
isNormalizedRouterDecisionShadow,
resolveGatewayAgentSessionId,
resolveLegacyRouteFromClassification,
resolveNormalizedRouterDecisionMode,
logRouterDecisionShadow,
ROUTER_DECISION_ROUTE,
ROUTER_DECISION_MODE,
ROUTER_DECISION_SESSION_HINT,
isRealtimeInfoQuestion,
isRichChannelPageIntent,
isRichPublishableContentIntent,
IMAGE_GENERATION_MODE,
resolveImageGenerationDecision,
coerceRealtimeWebSkill,
resolveChatIntentRouterPolicy,
} from './chat-intent-router.mjs';
test('image generation intent requires a real image for explicit background requests', () => {
const decision = resolveImageGenerationDecision({
text: '帮我生成一首唐诗页面,背景是大唐盛景图片,页面要精美',
});
assert.equal(decision.mode, IMAGE_GENERATION_MODE.REQUIRED);
assert.equal(decision.source, 'intent');
});
test('image generation user override can force or disable image generation', () => {
assert.equal(resolveImageGenerationDecision({
text: '做一个纯文字页面',
requestedMode: 'required',
}).mode, IMAGE_GENERATION_MODE.REQUIRED);
assert.equal(resolveImageGenerationDecision({
text: '生成一个带背景图的页面',
requestedMode: 'disabled',
}).mode, IMAGE_GENERATION_MODE.DISABLED);
});
test('visual page hints keep image generation best-effort unless the user explicitly requests an image', () => {
const decision = resolveImageGenerationDecision({
text: '帮我写一首诗词,做个页面吧',
requestedMode: 'auto',
});
assert.equal(decision.mode, IMAGE_GENERATION_MODE.AUTO);
assert.equal(decision.source, 'intent');
});
test('router injects a fail-closed generate_image contract for required visual pages', async () => {
const router = createChatIntentRouter();
const text = '帮我生成一个精美唐诗页面,背景是大唐盛景图片';
const userMessage = {
role: 'user',
content: [{ type: 'text', text }],
metadata: {
displayText: text,
memindRun: { imageGenerationMode: 'auto' },
},
};
const classification = await router.classify({
userMessage,
grantedSkills: ['static-page-publish'],
});
assert.equal(classification.imageGeneration.mode, IMAGE_GENERATION_MODE.REQUIRED);
const enriched = router.applyAgentOrchestration(userMessage, classification, {
grantedSkills: ['static-page-publish', 'image-generation'],
});
assert.match(enriched.content[0].text, /load_skill → image-generation/);
assert.match(enriched.content[0].text, /不要反问用户工具参数/);
assert.match(enriched.content[0].text, /sandbox-fs__generate_image/);
assert.match(enriched.content[0].text, /asset\.htmlSrc/);
assert.match(enriched.content[0].text, /workspaceRelativePath 只用于工作区文件操作/);
assert.match(enriched.content[0].text, /当前这一次执行中发起新的 generate_image/);
assert.match(enriched.content[0].text, /历史对话中的成功或失败都不能作为本轮结果/);
assert.match(enriched.content[0].text, /hero 成功后必须复用该资产/);
assert.match(enriched.content[0].text, /禁止再调用 card_cover 或 feed_cover/);
assert.match(enriched.content[0].text, /不得用 SVG/);
});
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 page data collection to page-data-collect', () => {
const text = '帮我在页面增加调查问卷,密码 888 后台查看每个用户提交记录,要用 sqlite';
const result = classifyWithRules({
text,
userMessage: {
role: 'user',
content: [{ type: 'text', text }],
metadata: { displayText: text },
},
});
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
assert.equal(result.suggestedSkill, 'page-data-collect');
});
test('classifyWithRules routes storefront ordering with product admin to page-data-collect', () => {
const text = '我要做一个页面,里面可以下单,后台可以上架产品';
const result = classifyWithRules({
text,
userMessage: {
role: 'user',
content: [{ type: 'text', text }],
metadata: { displayText: text },
},
});
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
assert.equal(result.suggestedSkill, 'page-data-collect');
});
test('classifyWithRules routes ordinary household ledger language to page-data-collect', () => {
const text = '帮我做一个家庭记账页面,我每天可以记录日期、收入、支出、分类和备注,日期默认当天。再做一个管理页面,可以查看所有记录和收支汇总。';
const result = classifyWithRules({
text,
userMessage: {
role: 'user',
content: [{ type: 'text', text }],
metadata: { displayText: text },
},
});
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
assert.equal(result.suggestedSkill, 'page-data-collect');
});
test('classifyWithRules routes implicit sticky-note timeline apps to page-data-collect', () => {
const text = '帮我设计一个便签提醒,可以写便签提交,时间轴来显示';
const result = classifyWithRules({
text,
userMessage: {
role: 'user',
content: [{ type: 'text', text }],
metadata: { displayText: text },
},
});
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
assert.equal(result.suggestedSkill, 'page-data-collect');
});
test('coercePageDataSkill overrides static-page-publish when data intent is present', () => {
const text = '帮我在这个页面增加调查问卷,密码 888 查看提交记录';
const coerced = coercePageDataSkill(
{
route: CHAT_INTENT_ROUTE.AGENT,
suggestedSkill: 'static-page-publish',
reason: '明确需要生成或发布页面/文件',
agentBrief: '生成页面',
},
text,
{ grantedSkills: ['page-data-collect', 'static-page-publish'] },
);
assert.equal(coerced.suggestedSkill, 'page-data-collect');
assert.match(coerced.reason, /页面数据交互意图/);
});
test('coercePageGenerationSkill skips when page data intent is present', () => {
const text = '帮我在页面增加调查问卷和后台入口';
const coerced = coercePageGenerationSkill(
{
route: CHAT_INTENT_ROUTE.AGENT,
suggestedSkill: null,
reason: '任务编排',
agentBrief: '',
},
text,
{ grantedSkills: ['static-page-publish', 'page-data-collect'] },
);
assert.notEqual(coerced.suggestedSkill, 'static-page-publish');
});
test('classifyWithRules routes implicit travel guide page requests to static-page-publish', () => {
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');
assert.match(result.agentBrief, /不要停在追问/);
});
test('coercePageGenerationSkill replaces product-campaign-page for content pages', () => {
const coerced = coercePageGenerationSkill(
{
route: CHAT_INTENT_ROUTE.AGENT,
suggestedSkill: 'product-campaign-page',
reason: '模型路由判定',
agentBrief: '',
confidence: 0.9,
source: 'llm',
},
'苏州攻略页面',
{ grantedSkills: ['static-page-publish', 'product-campaign-page'] },
);
assert.equal(coerced.suggestedSkill, 'static-page-publish');
assert.match(coerced.reason, /static-page-publish/);
});
test('coercePageGenerationSkill preserves realtime research while making the page the deliverable', () => {
const coerced = coercePageGenerationSkill(
{
route: CHAT_INTENT_ROUTE.AGENT,
suggestedSkill: 'web',
reason: '需要搜索实时资料(实时查询强制 web)',
agentBrief: '先搜索并核对今天的新闻来源。',
confidence: 1,
source: 'rule',
},
'帮我整理今天的新闻做成页面',
{ grantedSkills: ['web', 'static-page-publish'] },
);
assert.equal(coerced.suggestedSkill, 'static-page-publish');
assert.match(coerced.agentBrief, /先搜索并核对/);
assert.match(coerced.agentBrief, /公开内容页面/);
assert.match(coerced.reason, /实时资料整理后交付/);
});
test('createChatIntentRouter keeps static-page-publish for forced realtime page tasks', async () => {
const router = createChatIntentRouter();
const result = await router.classify({
userMessage: {
role: 'user',
content: [{ type: 'text', text: '帮我整理今天的新闻做成页面' }],
metadata: { displayText: '帮我整理今天的新闻做成页面' },
},
forceDeepReasoning: true,
grantedSkills: ['web', 'static-page-publish'],
});
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
assert.equal(result.suggestedSkill, 'static-page-publish');
assert.match(result.agentBrief, /tkmind_search/);
assert.match(result.agentBrief, /公开内容页面/);
});
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 routes rich service account content to static-page-publish', () => {
const text = `以下是今天(2026年7月7日,星期二)国际上的主要热点新闻:
🌍 地缘政治与安全
- 霍尔木兹海峡油轮遇袭。
- 伊朗最高领袖哈梅内伊葬礼。
- 中国核潜艇在南太平洋试射远程导弹。
- 俄罗斯空袭基辅。
🇺🇸 美国
- Charlie Kirk 遇刺案预审听证。
- 特朗普财务披露。
- 最高法院允许德州实施应用商店年龄验证法。
⚽ 体育
- 2026世界杯,比利时4-1淘汰美国,西班牙1-0绝杀葡萄牙。
🎤 娱乐
- Taylor Swift 与 Travis Kelce 在麦迪逊广场花园举行婚礼。
📉 财经
- 道指首次突破53,000点,AI 芯片热潮推动股市新高。
- Samsung Q2 利润飙升1,800%。
🌡️ 其他
- 新泽西高温致至少25人死亡。
- 古巴全国大停电。
请整理给服务号用户阅读。`;
const result = classifyWithRules({
text,
userMessage: {
role: 'user',
content: [{ type: 'text', text }],
metadata: { displayText: text },
},
});
assert.equal(isRichChannelPageIntent(text), true);
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
assert.equal(result.suggestedSkill, 'static-page-publish');
assert.match(result.reason, /结构化长内容/);
});
test('isRichChannelPageIntent does not route short service account copy to page', () => {
assert.equal(isRichChannelPageIntent('请整理给服务号用户阅读:今天 AI 新闻有哪些?'), false);
});
test('classifyWithRules prefers pages for rich structured content meant for readers', () => {
const text = `下面是活动资料,请整理成适合用户阅读和转发的内容:
活动概览
- 名称:城市夜跑季
- 时间:7月12日到7月20日
- 地点:滨江步道、中央公园、城市广场
亮点介绍
- 三条不同难度路线,适合亲子、入门和进阶跑者
- 每晚有补给站、音乐打卡点和城市灯光秀
- 完赛可领取电子证书和联名纪念徽章
报名信息
- 早鸟票 39 元
- 标准票 59 元
- 团队票 5 人起享 8 折
注意事项
- 需提前 30 分钟签到
- 建议穿反光装备
- 遇强降雨活动顺延
传播重点
- 强调轻松参与、城市夜景和朋友一起完成
- 内容需要清晰、有层次,适合用户直接阅读。`;
const result = classifyWithRules({
text,
userMessage: {
role: 'user',
content: [{ type: 'text', text }],
metadata: { displayText: text },
},
});
assert.equal(isRichPublishableContentIntent(text), true);
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
assert.equal(result.suggestedSkill, 'static-page-publish');
});
test('isRichPublishableContentIntent keeps plain long summaries out of page fast path', () => {
const text = `请总结以下会议纪要:
产品进展
- 首页完成第一版视觉调整
- 登录链路还需要补充错误提示
- 分享页需要统一按钮文案
技术进展
- 后端接口完成基础联调
- 前端状态管理仍有重复请求
- 缓存策略需要继续评估
风险
- 设计稿还没有最终确认
- 部分接口缺少压测数据
- 发布窗口需要和运营确认
下一步
- 明天上午确认视觉改动
- 下午完成联调复测
- 周五前整理发布清单`;
assert.equal(isRichPublishableContentIntent(text), false);
});
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 keeps agent session continuation even when llm router flag is true', () => {
const continued = classifyWithRules({
text: '可以',
sessionId: '20260708_64',
sessionMessageCount: 4,
llmRouterEnabled: true,
});
assert.equal(continued.route, CHAT_INTENT_ROUTE.AGENT);
assert.match(continued.reason, /Agent 会话确认|延续已有 Agent 会话/);
});
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 keeps memory recall on agent when session already active', () => {
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.AGENT);
assert.match(result.reason, /延续已有 Agent 会话/);
});
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('chat intent router classifies ambiguous WeChat session actions semantically', async () => {
const router = createChatIntentRouter({
enabled: true,
timeoutMs: 500,
llmProviderService: {
async createChatCompletion({ messages }) {
assert.match(messages[0].content, /微信聊天会话动作分类器/);
return {
ok: true,
reply: '{"action":"reset","confidence":0.91,"reason":"用户要求换一种对话上下文"}',
};
},
},
});
const result = await router.classifySessionAction({ text: '我们换个思路聊' });
assert.equal(result.action, 'reset');
assert.equal(result.confidence, 0.91);
});
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 keeps continued sessions on agent without llm router', async () => {
const llmCalls = [];
const router = createChatIntentRouter({
enabled: true,
modelProviderKeyId: 'key-router',
model: 'deepseek-chat',
llmProviderService: {
async createChatCompletion(payload) {
llmCalls.push(payload);
return { ok: false, message: 'router disabled' };
},
},
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, 0);
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
assert.equal(result.source, 'rule');
assert.match(result.reason, /延续已有 Agent 会话/);
});
test('createChatIntentRouter uses agent fallback for general conversational analysis (skill-only)', 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: false, message: 'router disabled' };
},
},
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.AGENT);
assert.equal(result.source, 'fallback');
assert.equal(resolveCalls.length, 0);
assert.equal(llmCalls.length, 0);
});
test('createChatIntentRouter routes memory recall through rules on fresh session', async () => {
const resolveCalls = [];
const router = createChatIntentRouter({
enabled: true,
llmProviderService: {
async createChatCompletion() {
return { ok: false, message: 'router disabled' };
},
},
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(result.source, 'rule');
assert.equal(resolveCalls.length, 0);
});
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 routes memory recall through rules when memory resolve fails', async () => {
const router = createChatIntentRouter({
enabled: true,
llmProviderService: {
async createChatCompletion() {
return { ok: false, message: 'router disabled' };
},
},
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.DIRECT_CHAT);
assert.equal(result.source, 'rule');
});
test('createManagedChatIntentRouter hot-loads admin config and stays skill-only', 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(), true);
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.AGENT);
assert.equal(result.source, 'fallback');
assert.equal(llmCalls.length, 0);
});
test('createChatIntentRouter uses agent fallback when rules miss (skill-only)', async () => {
const llmCalls = [];
const router = createChatIntentRouter({
enabled: true,
minConfidence: 0.8,
llmProviderService: {
async createChatCompletion() {
llmCalls.push(1);
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, 'fallback');
assert.equal(llmCalls.length, 0);
});
test('createChatIntentRouter falls back to agent when rules miss (skill-only)', async () => {
const llmCalls = [];
const router = createChatIntentRouter({
enabled: true,
llmProviderService: {
async createChatCompletion() {
llmCalls.push(1);
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');
assert.equal(llmCalls.length, 0);
});
test('classifyWithRules routes memory recall to direct chat on fresh session', () => {
const result = classifyWithRules({
text: '你记得我说想去哪儿吗',
sessionId: '20260704_3',
sessionMessageCount: 0,
});
assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
assert.match(result.reason, /记忆/);
const lastConversation = classifyWithRules({
text: '上次我们聊了什么?',
sessionId: '20260704_4',
sessionMessageCount: 0,
});
assert.equal(lastConversation.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
});
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 on fresh session', () => {
const result = classifyWithRules({
text: '讲一个睡前故事吧',
llmRouterEnabled: true,
sessionId: '20260706_2',
sessionMessageCount: 0,
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('agent memory shadow resolve is independently gated and never marked for injection', async () => {
const calls = [];
const router = createChatIntentRouter({
env: {
MEMORY_AGENT_RESOLVE_ENABLED: '1',
MEMORY_AGENT_INJECTION_MODE: 'shadow',
MEMORY_AGENT_RESOLVE_LIMIT: '3',
MEMORY_AGENT_RESOLVE_TIMEOUT_MS: '100',
},
memoryV2: {
async resolve(input) {
calls.push(input);
return { source: 'legacy-conversation-memory', memories: [{ label: 'preference', text: '喜欢完整方案' }] };
},
},
});
const result = await router.resolveAgentMemoryContext({
userId: 'u1',
sessionId: 's1',
text: '帮我设计一个商城',
});
assert.equal(result.mode, 'shadow');
assert.equal(result.injectionEnabled, false);
assert.equal(result.skipped, false);
assert.equal(result.memories.length, 1);
assert.deepEqual(calls[0], { userId: 'u1', sessionId: 's1', query: '帮我设计一个商城', limit: 3 });
});
test('agent memory canary injects only for configured user ids', async () => {
const router = createChatIntentRouter({
env: {
MEMORY_AGENT_RESOLVE_ENABLED: '1',
MEMORY_AGENT_INJECTION_MODE: 'canary',
MEMORY_AGENT_CANARY_USER_IDS: 'user-canary, user-other',
},
memoryV2: {
async resolve() {
return { memories: [{ label: 'goal', text: '当前项目是 TKMind' }] };
},
},
});
const canary = await router.resolveAgentMemoryContext({ userId: 'user-canary', text: '继续项目' });
const control = await router.resolveAgentMemoryContext({ userId: 'user-control', text: '继续项目' });
assert.equal(canary.injectionEnabled, true);
assert.equal(control.injectionEnabled, false);
assert.equal(canary.memories.length, 1);
assert.equal(control.memories.length, 1);
});
test('agent memory recall prioritizes historical session evidence over personal memory', async () => {
const episodicCalls = [];
const router = createChatIntentRouter({
env: {
MEMORY_AGENT_RESOLVE_ENABLED: '1',
MEMORY_AGENT_INJECTION_MODE: 'active',
MEMORY_AGENT_RESOLVE_LIMIT: '3',
MEMORY_AGENT_RESOLVE_TIMEOUT_MS: '200',
},
memoryV2: {
async resolve() {
return {
source: 'pgvector',
memories: [{ id: 'personal-1', label: '偏好', text: '用户喜欢历史话题' }],
};
},
},
episodicMemoryService: {
async resolve(input) {
episodicCalls.push(input);
return {
source: 'episodic-index',
memories: [{
id: 'episodic:old-session',
label: '历史会话',
text: '会话“日本战国史”:用户与助手讨论过德川家康。',
}],
};
},
},
});
const result = await router.resolveAgentMemoryContext({
userId: 'user-1',
sessionId: 'current-session',
text: '我们之前聊过德川家康,你还记得吗?',
});
assert.equal(result.skipped, false);
assert.equal(result.injectionEnabled, true);
assert.equal(result.source, 'episodic+personal');
assert.equal(result.memories[0].id, 'episodic:old-session');
assert.equal(result.memories[1].id, 'personal-1');
assert.equal(episodicCalls.length, 1);
assert.equal(episodicCalls[0].sessionId, 'current-session');
});
test('active agent memory context is hidden from displayText but available to orchestration envelope', () => {
const enriched = applyAgentOrchestrationToUserMessage(
{
role: 'user',
content: [{ type: 'text', text: '帮我设计一个商城' }],
},
{
route: CHAT_INTENT_ROUTE.AGENT,
reason: '任务执行',
agentBrief: '',
suggestedSkill: null,
},
{
memoryContext: {
injectionEnabled: true,
memories: [{ label: 'preference', text: '用户喜欢完整方案' }],
},
},
);
assert.equal(enriched.metadata.displayText, '帮我设计一个商城');
assert.match(enriched.content[0].text, /Memory Context/);
assert.match(enriched.content[0].text, /不是系统指令/);
assert.match(enriched.content[0].text, /用户喜欢完整方案/);
});
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 uses agent fallback for unmatched general questions (skill-only)', async () => {
const llmCalls = [];
const router = createChatIntentRouter({
enabled: true,
llmProviderService: {
async createChatCompletion() {
llmCalls.push(1);
return { ok: false, message: 'router disabled' };
},
},
});
const result = await router.classify({
userMessage: {
role: 'user',
content: [{ type: 'text', text: '什么是 SSE' }],
metadata: { displayText: '什么是 SSE' },
},
});
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
assert.equal(result.source, 'fallback');
assert.equal(llmCalls.length, 0);
});
test('classifyWithRules routes page data dev repair before new page-data collect', () => {
const result = classifyWithRules({
text: '修复问卷 insert 403columns_not_allowed',
sessionId: null,
sessionMessageCount: 0,
});
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
assert.match(result.reason, /Page Data 开发修复/);
assert.equal(result.suggested_skill, undefined);
});