Files
memind/chat-intent-router.test.mjs
T

1877 lines
62 KiB
JavaScript
Raw Normal View History

import assert from 'node:assert/strict';
import test from 'node:test';
import {
applyAgentOrchestrationToUserMessage,
buildRouterContext,
buildAgentOrchestrationAgentText,
buildRouterNormalizedDecision,
CHAT_INTENT_ROUTE,
classifyWithRules,
coercePageDataSkill,
coercePageGenerationSkill,
createChatIntentRouter,
createManagedChatIntentRouter,
formatRouterTranscript,
shouldDeferActiveTaskRoutingToLlm,
shouldForceActiveAgentTaskContinuation,
isNormalizedRouterDecisionEnabled,
isNormalizedRouterDecisionShadow,
resolveGatewayAgentSessionId,
resolveLegacyRouteFromClassification,
resolveNormalizedRouterDecisionMode,
logRouterDecisionShadow,
ROUTER_DECISION_ROUTE,
ROUTER_DECISION_MODE,
ROUTER_DECISION_SESSION_HINT,
isRealtimeInfoQuestion,
isRichChannelPageIntent,
isRichPublishableContentIntent,
2026-07-20 07:37:43 +08:00
IMAGE_GENERATION_MODE,
resolveImageGenerationDecision,
coerceRealtimeWebSkill,
isChatLlmRouterEligible,
logChatLlmRouterShadow,
resolveChatIntentRouterPolicy,
mergeAgentMemoryCandidates,
scoreAgentMemoryCandidate,
shouldSkipAmbiguousMemoryQuery,
} from './chat-intent-router.mjs';
/** Ambiguous user text that should miss FAQ/rules and exercise LLM router paths in tests. */
const AMBIGUOUS_LLM_ROUTER_QUERY = '周末想放松一下,有什么活动建议?';
2026-07-20 07:37:43 +08:00
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');
});
2026-07-20 07:37:43 +08:00
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'],
2026-07-20 07:37:43 +08:00
});
assert.match(enriched.content[0].text, /load_skill → image-generation/);
assert.match(enriched.content[0].text, /不要反问用户工具参数/);
2026-07-20 07:37:43 +08:00
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 只用于工作区文件操作/);
2026-07-20 07:37:43 +08:00
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 explicit text-only Q&A to direct chat', () => {
for (const text of [
'用三句话介绍苏州园林,不要生成页面',
'简要介绍一下 Rust 和 Go 的区别',
'推荐一个最适合第一次去的,一句话说明理由',
]) {
const result = classifyWithRules({
text,
userMessage: {
role: 'user',
content: [{ type: 'text', text }],
metadata: { displayText: text },
},
});
assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT, text);
assert.equal(result.source, 'rule', text);
}
});
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');
});
2026-07-24 23:53:32 +08:00
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('classifyWithRules routes travel guide and route-map requests without 页面 to agent', () => {
for (const text of [
'你好,我要做一个旅游攻略',
'我要旅游线路图',
'帮我做一个嵊泗 3 天 2 晚的游玩攻略吧',
]) {
const result = classifyWithRules({
text,
userMessage: {
role: 'user',
content: [{ type: 'text', text }],
metadata: { displayText: text },
},
});
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT, text);
assert.equal(result.suggestedSkill, 'static-page-publish', text);
}
});
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/);
});
2026-07-25 21:55:00 +08:00
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 agent session confirmation on agent path', () => {
const continued = classifyWithRules({
text: '继续聊聊',
sessionId: '20260704_2',
sessionMessageCount: 2,
});
assert.equal(continued.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
assert.match(continued.reason, /轻量聊天/);
const confirmed = classifyWithRules({
text: '可以',
sessionId: '20260704_2',
sessionMessageCount: 2,
});
assert.equal(confirmed.route, CHAT_INTENT_ROUTE.AGENT);
assert.match(confirmed.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 continues agent task after failed run in same session', () => {
const activeTaskContext = {
hasRecentAgentTask: true,
lastRunFailed: true,
lastSuggestedSkill: 'page-data-collect',
lastRunStatus: 'failed',
lastRoute: 'agent_orchestration',
};
const result = classifyWithRules({
text: '还是没有按照我的逻辑更正',
sessionId: '20260827_3',
sessionMessageCount: 50,
activeTaskContext,
grantedSkills: ['page-data-collect'],
});
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
assert.equal(result.suggestedSkill, 'page-data-collect');
assert.match(result.reason, /上一轮任务未完成/);
});
test('classifyWithRules routes correction follow-up to agent when recent agent task exists', () => {
const activeTaskContext = {
hasRecentAgentTask: true,
lastRunFailed: false,
lastSuggestedSkill: 'page-data-collect',
lastRunStatus: 'succeeded',
lastRoute: 'agent_orchestration',
};
const result = classifyWithRules({
text: '不听指令了吗',
sessionId: '20260827_3',
sessionMessageCount: 51,
activeTaskContext,
grantedSkills: ['page-data-collect'],
});
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
assert.equal(result.suggestedSkill, 'page-data-collect');
assert.match(result.reason, /纠正或补充/);
});
test('classifyWithRules defers ambiguous active-task follow-up to LLM router', () => {
const activeTaskContext = {
hasRecentAgentTask: true,
lastRunFailed: false,
lastSuggestedSkill: 'page-data-collect',
lastRunStatus: 'succeeded',
lastRoute: 'agent_orchestration',
};
const result = classifyWithRules({
text: '招商管理端可以展开招商漏斗,明确准入门槛',
sessionId: '20260827_3',
sessionMessageCount: 52,
activeTaskContext,
grantedSkills: ['page-data-collect'],
});
assert.equal(result, null);
});
test('formatRouterTranscript keeps recent turns and strips skill prefixes from user text', () => {
const transcript = formatRouterTranscript([
{
role: 'user',
metadata: { displayText: '帮我做商户沟通台账' },
content: [{ type: 'text', text: '请使用 page-data-collect 技能:帮我做商户沟通台账' }],
},
{
role: 'assistant',
content: [{ type: 'text', text: '好的,我先创建问卷页面。' }],
},
], {
excludeLatestUserText: '招商管理端展开漏斗',
});
assert.match(transcript, /用户:帮我做商户沟通台账/);
assert.match(transcript, /助手:好的,我先创建问卷页面/);
assert.doesNotMatch(transcript, /page-data-collect 技能/);
});
test('createChatIntentRouter uses active-task fallback when LLM router is unavailable', async () => {
const router = createChatIntentRouter({
llmProviderService: {
async createChatCompletion() {
return null;
},
},
env: {
...process.env,
MEMIND_CHAT_LLM_ROUTER_ENABLED: '1',
MEMIND_CHAT_LLM_ROUTER_SHADOW: '0',
MEMIND_CHAT_ROUTER_CANARY_USER_IDS: '',
},
});
const result = await router.classify({
userId: 'user-1',
sessionId: '20260827_3',
sessionMessageCount: 40,
grantedSkills: ['page-data-collect'],
activeTaskContext: {
hasRecentAgentTask: true,
lastRunFailed: true,
lastSuggestedSkill: 'page-data-collect',
lastRunStatus: 'failed',
lastRoute: 'agent_orchestration',
},
userMessage: {
role: 'user',
content: [{ type: 'text', text: '还是没有按照我的逻辑更正' }],
metadata: { displayText: '还是没有按照我的逻辑更正' },
},
});
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
assert.equal(result.suggestedSkill, 'page-data-collect');
});
test('classifyWithRules keeps memory recall on direct chat even with failed agent task context', () => {
const activeTaskContext = {
hasRecentAgentTask: true,
lastRunFailed: true,
lastSuggestedSkill: 'page-data-collect',
lastRunStatus: 'failed',
};
const result = classifyWithRules({
text: '你记得我说想去哪儿吗',
sessionId: '20260827_3',
sessionMessageCount: 50,
activeTaskContext,
userMessage: {
role: 'user',
content: [{ type: 'text', text: '你记得我说想去哪儿吗' }],
metadata: { displayText: '你记得我说想去哪儿吗' },
},
});
assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
assert.match(result.reason, /记忆|历史对话/);
});
test('classifyWithRules keeps memory recall on direct chat even 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.DIRECT_CHAT);
assert.match(result.reason, /记忆|历史对话/);
});
test('classifyWithRules keeps casual FAQ chat on direct chat even when session already active', () => {
const result = classifyWithRules({
text: '你觉得人都是为了什么而活',
sessionId: '20260826_39',
sessionMessageCount: 4,
userMessage: {
role: 'user',
content: [{ type: 'text', text: '你觉得人都是为了什么而活' }],
metadata: { displayText: '你觉得人都是为了什么而活' },
},
});
assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
assert.match(result.reason, /闲聊|FAQ/);
});
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);
});
2026-07-23 17:18:25 +08:00
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 direct chat for general conversation', 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.DIRECT_CHAT);
assert.equal(result.source, 'rule');
assert.match(result.reason, /轻量聊天/);
});
test('createChatIntentRouter uses direct chat fallback for general conversational analysis when llm router disabled', async () => {
const llmCalls = [];
const resolveCalls = [];
const router = createChatIntentRouter({
enabled: false,
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.DIRECT_CHAT);
assert.equal(result.source, 'rule');
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 activates llm routing', 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: AMBIGUOUS_LLM_ROUTER_QUERY }],
metadata: { displayText: AMBIGUOUS_LLM_ROUTER_QUERY },
},
});
assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
assert.equal(result.source, 'rule');
assert.equal(llmCalls.length, 0);
});
test('createChatIntentRouter rejects low-confidence llm direct_chat and keeps rule default', 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: AMBIGUOUS_LLM_ROUTER_QUERY }],
metadata: { displayText: AMBIGUOUS_LLM_ROUTER_QUERY },
},
});
assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
assert.equal(result.source, 'rule');
assert.equal(llmCalls.length, 0);
});
test('createChatIntentRouter keeps direct chat when llm router fails', 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: AMBIGUOUS_LLM_ROUTER_QUERY }],
metadata: { displayText: AMBIGUOUS_LLM_ROUTER_QUERY },
},
grantedSkills: ['web'],
});
assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
assert.equal(result.source, 'rule');
assert.equal(llmCalls.length, 0);
});
test('createChatIntentRouter uses direct chat when llm router disabled', async () => {
const llmCalls = [];
const router = createChatIntentRouter({
enabled: false,
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: AMBIGUOUS_LLM_ROUTER_QUERY }],
metadata: { displayText: AMBIGUOUS_LLM_ROUTER_QUERY },
},
});
assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
assert.equal(result.source, 'rule');
assert.equal(llmCalls.length, 0);
});
test('classifyWithRules fast-paths FAQ concept questions to direct chat without LLM', () => {
const result = classifyWithRules({
text: '什么是 SSE?',
sessionId: null,
sessionMessageCount: 0,
});
assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
assert.equal(result.source, 'rule');
assert.match(result.reason, /FAQ/);
});
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, /记忆/);
2026-07-22 12:36:13 +08:00
const lastConversation = classifyWithRules({
text: '上次我们聊了什么?',
sessionId: '20260704_4',
sessionMessageCount: 0,
});
assert.equal(lastConversation.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
});
test('classifyWithRules defers ambiguous chat text to llm when chat session defer is enabled', () => {
const result = classifyWithRules({
text: '帮我比较一下英得尔和美的车载冰箱',
sessionId: 'h5direct_abc',
sessionMessageCount: 3,
chatSessionDeferEnabled: true,
chatSessionDeferMinTextLength: 12,
});
assert.equal(result, null);
});
test('classifyWithRules keeps memory recall on direct chat when defer is enabled', () => {
const result = classifyWithRules({
text: '你记得我说想去哪儿吗',
sessionId: 'h5direct_abc',
sessionMessageCount: 3,
chatSessionDeferEnabled: true,
});
assert.equal(result.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.REUSE);
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'));
const goalRun = buildRouterNormalizedDecision(
{ route: CHAT_INTENT_ROUTE.AGENT },
{ text: '帮我分阶段完成下季度产品规划' },
);
assert.ok(goalRun.flags.includes('goal_run'));
});
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 },
},
}),
'sess-1',
);
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 defaults fallback route to direct chat', () => {
const policy = resolveChatIntentRouterPolicy({
env: {
MEMIND_CHAT_ROUTER_FALLBACK_ROUTE: 'direct_chat',
},
});
assert.equal(policy.fallbackRoute, CHAT_INTENT_ROUTE.DIRECT_CHAT);
});
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 keeps direct chat default', async () => {
const router = createChatIntentRouter({
enabled: true,
timeoutMs: 40,
fallbackRoute: CHAT_INTENT_ROUTE.DIRECT_CHAT,
llmProviderService: {
async createChatCompletion() {
await new Promise((resolve) => setTimeout(resolve, 250));
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.DIRECT_CHAT);
assert.equal(result.source, 'rule');
});
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);
});
2026-07-22 12:36:13 +08:00
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('agent memory merge prefers relevant personal memories over weak episodic matches', () => {
const merged = mergeAgentMemoryCandidates({
query: '你记得我们聊过德川家康吗',
limit: 2,
episodicMemories: [{
id: 'episodic:news',
label: '历史会话',
text: '会话“每日新闻页面”:用户要求整理今天国际国内热点新闻并做成页面。',
}],
personalMemories: [{
id: 'personal:tokugawa',
label: 'interest',
text: '用户对日本战国人物德川家康感兴趣,并希望继续深入讨论',
}],
});
assert.equal(merged[0].id, 'personal:tokugawa');
assert.equal(merged[1].id, 'episodic:news');
});
test('scoreAgentMemoryCandidate boosts episodic matches when topic overlap exists', () => {
const episodicScore = scoreAgentMemoryCandidate({
id: 'episodic:old-session',
label: '历史会话',
sessionId: 'old-session',
text: '会话“日本战国史”:用户与助手讨论过德川家康。',
}, '我们之前聊过德川家康,你还记得吗?');
const personalScore = scoreAgentMemoryCandidate({
id: 'personal-1',
label: '偏好',
text: '用户喜欢历史话题',
}, '我们之前聊过德川家康,你还记得吗?');
assert.ok(episodicScore > personalScore);
});
test('shouldSkipAmbiguousMemoryQuery skips filler without topic anchor', () => {
assert.equal(shouldSkipAmbiguousMemoryQuery('可以'), true);
assert.equal(shouldSkipAmbiguousMemoryQuery('我们继续聊聊德川家康'), false);
assert.equal(shouldSkipAmbiguousMemoryQuery('9岁小孩教育'), false);
assert.equal(shouldSkipAmbiguousMemoryQuery('你记得我们聊过什么吗'), false);
});
test('agent memory skips resolve for ambiguous short filler', async () => {
const calls = [];
const router = createChatIntentRouter({
env: {
MEMORY_AGENT_RESOLVE_ENABLED: '1',
MEMORY_AGENT_INJECTION_MODE: 'active',
},
memoryV2: {
async resolve(input) {
calls.push(input);
return { memories: [{ label: 'preference', text: '每日新闻页面偏好' }] };
},
},
});
const result = await router.resolveAgentMemoryContext({
userId: 'user-tang',
text: '可以',
});
assert.equal(result.skipped, true);
assert.equal(result.reason, 'ambiguous_query_skip');
assert.equal(result.memories.length, 0);
assert.equal(calls.length, 0);
});
test('agent memory merge demotes page workflow memories when query is general chat', () => {
const merged = mergeAgentMemoryCandidates({
query: '9岁小孩教育,孩子玩手机超时怎么办',
limit: 2,
personalMemories: [
{ id: 'personal:news', label: 'preference', text: '用户希望整理每日新闻并做成页面发布' },
{ id: 'personal:kid', label: 'experience', text: '用户关心9岁孩子教育与手机使用边界' },
],
});
assert.equal(merged[0].id, 'personal:kid');
});
test('scoreAgentMemoryCandidate penalizes page workflow memories on general chat', () => {
const pageScore = scoreAgentMemoryCandidate({
id: 'personal:news',
label: 'preference',
text: '用户希望整理每日新闻并做成页面发布',
}, '可以,我们继续聊孩子教育');
const topicScore = scoreAgentMemoryCandidate({
id: 'personal:kid',
label: 'experience',
text: '用户关心9岁孩子教育与手机使用边界',
}, '可以,我们继续聊孩子教育');
assert.ok(topicScore > pageScore);
});
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('classifyWithRules routes page data dev repair before new page-data collect', () => {
const result = classifyWithRules({
text: '修复问卷 insert 403,columns_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);
});
test('isChatLlmRouterEligible respects enabled flag and canary user ids', () => {
assert.equal(isChatLlmRouterEligible({ enabled: false, canaryUserIds: ['john'], userId: 'john' }), false);
assert.equal(isChatLlmRouterEligible({ enabled: true, canaryUserIds: [], userId: 'john' }), true);
assert.equal(isChatLlmRouterEligible({ enabled: true, canaryUserIds: ['john'], userId: 'john' }), true);
assert.equal(isChatLlmRouterEligible({ enabled: true, canaryUserIds: ['john'], userId: 'other' }), false);
assert.equal(isChatLlmRouterEligible({ enabled: true, canaryUserIds: ['john'], userId: null }), false);
});
test('createChatIntentRouter limits llm routing to canary users', async () => {
const llmCalls = [];
const router = createChatIntentRouter({
enabled: true,
canaryUserIds: ['user-canary'],
llmProviderService: {
async createChatCompletion() {
llmCalls.push(1);
return {
ok: true,
reply: JSON.stringify({
route: 'direct_chat',
confidence: 0.91,
reason: '纯文字问答',
suggested_skill: null,
agent_brief: '',
}),
};
},
},
});
const control = await router.classify({
userId: 'user-control',
userMessage: {
role: 'user',
content: [{ type: 'text', text: AMBIGUOUS_LLM_ROUTER_QUERY }],
metadata: { displayText: AMBIGUOUS_LLM_ROUTER_QUERY },
},
});
const canary = await router.classify({
userId: 'user-canary',
userMessage: {
role: 'user',
content: [{ type: 'text', text: AMBIGUOUS_LLM_ROUTER_QUERY }],
metadata: { displayText: AMBIGUOUS_LLM_ROUTER_QUERY },
},
});
assert.equal(control.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
assert.equal(control.source, 'rule');
assert.equal(canary.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
assert.equal(canary.source, 'rule');
assert.equal(llmCalls.length, 0);
});
test('createChatIntentRouter shadow mode skips llm shadow when rule default matches', async () => {
const lines = [];
const logger = { warn(line) { lines.push(line); }, info(line) { lines.push(line); } };
const router = createChatIntentRouter({
enabled: true,
shadowMode: true,
llmProviderService: {
async createChatCompletion() {
return {
ok: true,
reply: JSON.stringify({
route: 'direct_chat',
confidence: 0.95,
reason: '纯文字问答',
suggested_skill: null,
agent_brief: '',
}),
};
},
},
logger,
});
const result = await router.classify({
userId: 'user-john',
userMessage: {
role: 'user',
content: [{ type: 'text', text: AMBIGUOUS_LLM_ROUTER_QUERY }],
metadata: { displayText: AMBIGUOUS_LLM_ROUTER_QUERY },
},
});
assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
assert.equal(result.source, 'rule');
assert.equal(result.llmShadow, undefined);
assert.equal(lines.length, 0);
});
test('logChatLlmRouterShadow emits structured payload', () => {
const lines = [];
logChatLlmRouterShadow({
baseline: { route: CHAT_INTENT_ROUTE.AGENT, source: 'fallback' },
llmResult: { route: CHAT_INTENT_ROUTE.DIRECT_CHAT, confidence: 0.9, reason: '闲聊' },
userId: 'user-1',
sessionId: 'sess-1',
}, { logger: { info(line) { lines.push(line); } } });
assert.equal(lines.length, 1);
assert.match(lines[0], /baselineRoute":"agent_orchestration"/);
assert.match(lines[0], /llmRoute":"direct_chat"/);
});
test('resolveChatIntentRouterPolicy parses llm router shadow and canary env', () => {
const policy = resolveChatIntentRouterPolicy({
env: {
MEMIND_CHAT_LLM_ROUTER_ENABLED: '1',
MEMIND_CHAT_LLM_ROUTER_SHADOW: '1',
MEMIND_CHAT_ROUTER_CANARY_USER_IDS: 'john, testuser1',
},
});
assert.equal(policy.enabled, true);
assert.equal(policy.shadowMode, true);
assert.deepEqual(policy.canaryUserIds, ['john', 'testuser1']);
});
test('createChatIntentRouter getStatus reflects llm router modes', () => {
const active = createChatIntentRouter({ enabled: true });
const shadow = createChatIntentRouter({ enabled: true, shadowMode: true });
const off = createChatIntentRouter({ enabled: false });
assert.equal(active.getStatus().llmRoutingEnabled, true);
assert.equal(active.getStatus().llmRoutingShadow, false);
assert.equal(shadow.getStatus().llmRoutingEnabled, false);
assert.equal(shadow.getStatus().llmRoutingShadow, true);
assert.equal(off.getStatus().configuredLlmEnabled, false);
});