4781bbc156
Route themed page/article prompts to agent orchestration, reject them from portal direct chat, and finalize regular sessions to idle when a direct chat turn already completed. Co-authored-by: Cursor <cursoragent@cursor.com>
530 lines
16 KiB
JavaScript
530 lines
16 KiB
JavaScript
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 技能/);
|
|
});
|