import assert from 'node:assert/strict'; import test from 'node:test'; import { applyAgentOrchestrationToUserMessage, buildRouterContext, buildAgentOrchestrationAgentText, CHAT_INTENT_ROUTE, classifyWithRules, createChatIntentRouter, createManagedChatIntentRouter, } 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: '你好' }] }, }); 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: '帮我查一下今天的 AI 新闻' }], metadata: { displayText: '帮我查一下今天的 AI 新闻' }, }, grantedSkills: ['web'], }); assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); assert.equal(result.source, 'fallback'); }); 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 技能/); });