Add page data delivery and publication guards
This commit is contained in:
+107
-99
@@ -7,6 +7,7 @@ import {
|
||||
buildRouterNormalizedDecision,
|
||||
CHAT_INTENT_ROUTE,
|
||||
classifyWithRules,
|
||||
coercePageDataSkill,
|
||||
coercePageGenerationSkill,
|
||||
createChatIntentRouter,
|
||||
createManagedChatIntentRouter,
|
||||
@@ -52,6 +53,51 @@ test('classifyWithRules routes page generation to agent orchestration', () => {
|
||||
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('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: '苏州攻略页面',
|
||||
@@ -220,14 +266,15 @@ test('classifyWithRules honors forceDeepReasoning bypass', () => {
|
||||
assert.match(result.reason, /深度推理/);
|
||||
});
|
||||
|
||||
test('classifyWithRules skips session continuation when llm router is enabled', () => {
|
||||
test('classifyWithRules keeps agent session continuation even when llm router flag is true', () => {
|
||||
const continued = classifyWithRules({
|
||||
text: '我想去日本玩',
|
||||
sessionId: '20260704_10',
|
||||
sessionMessageCount: 2,
|
||||
text: '可以',
|
||||
sessionId: '20260708_64',
|
||||
sessionMessageCount: 4,
|
||||
llmRouterEnabled: true,
|
||||
});
|
||||
assert.equal(continued, null);
|
||||
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', () => {
|
||||
@@ -248,7 +295,7 @@ test('classifyWithRules keeps empty pre-created sessions eligible for llm routin
|
||||
assert.match(fresh.reason, /记忆/);
|
||||
});
|
||||
|
||||
test('classifyWithRules routes memory recall to direct chat even in agent session', () => {
|
||||
test('classifyWithRules keeps memory recall on agent when session already active', () => {
|
||||
const result = classifyWithRules({
|
||||
text: '你记得我说想去哪儿吗',
|
||||
sessionId: '20260704_9',
|
||||
@@ -259,8 +306,8 @@ test('classifyWithRules routes memory recall to direct chat even in agent sessio
|
||||
metadata: { displayText: '你记得我说想去哪儿吗' },
|
||||
},
|
||||
});
|
||||
assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
|
||||
assert.match(result.reason, /记忆/);
|
||||
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
|
||||
assert.match(result.reason, /延续已有 Agent 会话/);
|
||||
});
|
||||
|
||||
test('classifyWithRules bypasses llm router when user selected summarize skill', async () => {
|
||||
@@ -327,7 +374,7 @@ test('classifyWithRules bypasses llm router for explicit web skill prompt', asyn
|
||||
assert.equal(llmCalls.length, 0);
|
||||
});
|
||||
|
||||
test('createChatIntentRouter classifies continued sessions through llm when enabled', async () => {
|
||||
test('createChatIntentRouter keeps continued sessions on agent without llm router', async () => {
|
||||
const llmCalls = [];
|
||||
const router = createChatIntentRouter({
|
||||
enabled: true,
|
||||
@@ -336,16 +383,7 @@ test('createChatIntentRouter classifies continued sessions through llm when enab
|
||||
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: '整理日本旅行攻略',
|
||||
}),
|
||||
};
|
||||
return { ok: false, message: 'router disabled' };
|
||||
},
|
||||
},
|
||||
memoryV2: {
|
||||
@@ -366,14 +404,13 @@ test('createChatIntentRouter classifies continued sessions through llm when enab
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(llmCalls.length, 1);
|
||||
assert.equal(llmCalls.length, 0);
|
||||
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
|
||||
assert.equal(result.source, 'llm');
|
||||
assert.match(result.reason, /日本旅行/);
|
||||
assert.equal(result.memory?.itemsUsed ?? 0, 0);
|
||||
assert.equal(result.source, 'rule');
|
||||
assert.match(result.reason, /延续已有 Agent 会话/);
|
||||
});
|
||||
|
||||
test('createChatIntentRouter skips memory resolve for general conversational analysis', async () => {
|
||||
test('createChatIntentRouter uses agent fallback for general conversational analysis (skill-only)', async () => {
|
||||
const llmCalls = [];
|
||||
const resolveCalls = [];
|
||||
const router = createChatIntentRouter({
|
||||
@@ -383,18 +420,7 @@ test('createChatIntentRouter skips memory resolve for general conversational ana
|
||||
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: '',
|
||||
}),
|
||||
};
|
||||
return { ok: false, message: 'router disabled' };
|
||||
},
|
||||
},
|
||||
memoryV2: {
|
||||
@@ -419,37 +445,19 @@ test('createChatIntentRouter skips memory resolve for general conversational ana
|
||||
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(result.route, CHAT_INTENT_ROUTE.AGENT);
|
||||
assert.equal(result.source, 'fallback');
|
||||
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, /相关记忆/);
|
||||
assert.equal(llmCalls.length, 0);
|
||||
});
|
||||
|
||||
test('createChatIntentRouter resolves light memory for recall questions', async () => {
|
||||
test('createChatIntentRouter routes memory recall through rules on fresh session', 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: '',
|
||||
}),
|
||||
};
|
||||
return { ok: false, message: 'router disabled' };
|
||||
},
|
||||
},
|
||||
memoryV2: {
|
||||
@@ -473,9 +481,8 @@ test('createChatIntentRouter resolves light memory for recall questions', async
|
||||
});
|
||||
|
||||
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);
|
||||
assert.equal(result.source, 'rule');
|
||||
assert.equal(resolveCalls.length, 0);
|
||||
});
|
||||
|
||||
test('buildRouterContext trims rich Memory V2 resolve payload for routing', () => {
|
||||
@@ -499,22 +506,12 @@ test('buildRouterContext trims rich Memory V2 resolve payload for routing', () =
|
||||
assert.doesNotMatch(context.content, /ignored/);
|
||||
});
|
||||
|
||||
test('createChatIntentRouter fails open when Memory V2 resolve fails', async () => {
|
||||
test('createChatIntentRouter routes memory recall through rules when memory 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: '搜索并整理来源',
|
||||
}),
|
||||
};
|
||||
async createChatCompletion() {
|
||||
return { ok: false, message: 'router disabled' };
|
||||
},
|
||||
},
|
||||
memoryV2: {
|
||||
@@ -535,11 +532,11 @@ test('createChatIntentRouter fails open when Memory V2 resolve fails', async ()
|
||||
grantedSkills: ['web'],
|
||||
});
|
||||
|
||||
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
|
||||
assert.equal(result.memory.degraded, true);
|
||||
assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
|
||||
assert.equal(result.source, 'rule');
|
||||
});
|
||||
|
||||
test('createManagedChatIntentRouter hot-loads admin config and stays closed by default', async () => {
|
||||
test('createManagedChatIntentRouter hot-loads admin config and stays skill-only', async () => {
|
||||
const states = [
|
||||
{
|
||||
fingerprint: 'off',
|
||||
@@ -584,7 +581,7 @@ test('createManagedChatIntentRouter hot-loads admin config and stays closed by d
|
||||
|
||||
assert.equal(await router.isEnabled(), false);
|
||||
states.shift();
|
||||
assert.equal(await router.isEnabled(), true);
|
||||
assert.equal(await router.isEnabled(), false);
|
||||
const result = await router.classify({
|
||||
userMessage: {
|
||||
role: 'user',
|
||||
@@ -592,16 +589,19 @@ test('createManagedChatIntentRouter hot-loads admin config and stays closed by d
|
||||
metadata: { displayText: '帮我概括一下这段材料的要点' },
|
||||
},
|
||||
});
|
||||
assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
|
||||
assert.equal(llmCalls[0].providerKeyId, 'key-router');
|
||||
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
|
||||
assert.equal(result.source, 'fallback');
|
||||
assert.equal(llmCalls.length, 0);
|
||||
});
|
||||
|
||||
test('createChatIntentRouter escalates low-confidence direct chat to agent', async () => {
|
||||
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({
|
||||
@@ -625,14 +625,17 @@ test('createChatIntentRouter escalates low-confidence direct chat to agent', asy
|
||||
});
|
||||
|
||||
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
|
||||
assert.equal(result.source, 'threshold');
|
||||
assert.equal(result.source, 'fallback');
|
||||
assert.equal(llmCalls.length, 0);
|
||||
});
|
||||
|
||||
test('createChatIntentRouter falls back to agent when LLM fails', async () => {
|
||||
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' };
|
||||
},
|
||||
},
|
||||
@@ -649,6 +652,17 @@ test('createChatIntentRouter falls back to agent when LLM fails', async () => {
|
||||
|
||||
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, /记忆/);
|
||||
});
|
||||
|
||||
test('createChatIntentRouter fast-paths news lookup to agent without LLM', async () => {
|
||||
@@ -867,12 +881,12 @@ test('shadow mode keeps legacy route for gateway decisions', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('classifyWithRules fast-paths bedtime story when llm router is enabled', () => {
|
||||
test('classifyWithRules fast-paths bedtime story on fresh session', () => {
|
||||
const result = classifyWithRules({
|
||||
text: '讲一个睡前故事吧',
|
||||
llmRouterEnabled: true,
|
||||
sessionId: '20260706_2',
|
||||
sessionMessageCount: 3,
|
||||
sessionMessageCount: 0,
|
||||
userMessage: {
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: '讲一个睡前故事吧' }],
|
||||
@@ -1066,21 +1080,14 @@ test('logRouterDecisionShadow emits payload only in shadow mode', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('createChatIntentRouter accepts chat and agent route synonyms from llm', async () => {
|
||||
test('createChatIntentRouter uses agent fallback for unmatched general questions (skill-only)', async () => {
|
||||
const llmCalls = [];
|
||||
const router = createChatIntentRouter({
|
||||
enabled: true,
|
||||
llmProviderService: {
|
||||
async createChatCompletion() {
|
||||
return {
|
||||
ok: true,
|
||||
reply: JSON.stringify({
|
||||
route: 'chat',
|
||||
confidence: 0.92,
|
||||
reason: '纯问答',
|
||||
suggested_skill: null,
|
||||
agent_brief: '',
|
||||
}),
|
||||
};
|
||||
llmCalls.push(1);
|
||||
return { ok: false, message: 'router disabled' };
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1093,6 +1100,7 @@ test('createChatIntentRouter accepts chat and agent route synonyms from llm', as
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT);
|
||||
assert.equal(result.decision.route, ROUTER_DECISION_ROUTE.CHAT);
|
||||
assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT);
|
||||
assert.equal(result.source, 'fallback');
|
||||
assert.equal(llmCalls.length, 0);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user