fix(bridge): 商品链接兜底与强制检索
This commit is contained in:
@@ -61,3 +61,122 @@ test('agent: output guard blocks fabricated order claim before send', async () =
|
||||
assert.match(sent, /无法在聊天中直接下单/);
|
||||
assert.doesNotMatch(sent, /已为您创建正式订单草稿/);
|
||||
});
|
||||
|
||||
test('agent: bare Chinese product word forces search_products before replying', async () => {
|
||||
const queries = [];
|
||||
const tools = [{
|
||||
name: 'search_products',
|
||||
description: 'fake',
|
||||
parameters: { type: 'object', properties: {} },
|
||||
handler: async (args) => {
|
||||
queries.push(args.query);
|
||||
return { products: [{ name: 'Triple Rings Ring', price: '$2.88', stock: 26, url: 'https://www.yehwang.com/triple-rings-ring-gold-color-17590.html' }], count: 1 };
|
||||
},
|
||||
}];
|
||||
tools.leadStore = { get: async () => null, save: async () => null };
|
||||
let sent = '';
|
||||
const agent = createAgent({ sites: [], defaultSite: 'www' }, {
|
||||
llm: {
|
||||
enabled: true,
|
||||
model: 'fake',
|
||||
chat: async () => ({ content: '为您找到 Triple Rings Ring,$2.88,库存 26 件。', toolCalls: [], usage: null }),
|
||||
},
|
||||
client: {
|
||||
getMessages: async () => [{ id: 1, text_content: '戒指', sender_type: 'contact', private: false }],
|
||||
sendMessage: async (_uuid, content) => { sent = content; },
|
||||
},
|
||||
tools,
|
||||
verification: { getVerifiedEmail: async () => null, sendCode: async () => ({}), verifyCode: async () => ({}) },
|
||||
metrics: { record: async () => {}, enabled: false },
|
||||
contextManager: { compressHistory: async (_c, h) => h },
|
||||
});
|
||||
const conv = { uuid: 'conv-ring-1', inbox_id: '1', channel: 'livechat', contact: { type: 'visitor' } };
|
||||
await agent.run(conv, { uuid: 'msg-ring-1', text_content: '戒指', content: '戒指' });
|
||||
assert.deepEqual(queries, ['戒指']);
|
||||
assert.match(sent, /Triple Rings Ring/);
|
||||
});
|
||||
|
||||
test('agent: backfills real product links when the LLM omits them', async () => {
|
||||
const tools = [{
|
||||
name: 'search_products',
|
||||
description: 'fake',
|
||||
parameters: { type: 'object', properties: {} },
|
||||
handler: async () => ({
|
||||
products: [{ name: 'Triple Rings Ring', price: '$2.88', stock: 26, url: 'https://www.yehwang.com/triple-rings-ring-gold-color-17590.html' }],
|
||||
count: 1,
|
||||
}),
|
||||
}];
|
||||
tools.leadStore = { get: async () => null, save: async () => null };
|
||||
let sent = '';
|
||||
const agent = createAgent({ sites: [], defaultSite: 'www' }, {
|
||||
llm: {
|
||||
enabled: true,
|
||||
model: 'fake',
|
||||
chat: async () => ({ content: '为您找到 Triple Rings Ring,$2.88,库存 26 件。', toolCalls: [], usage: null }),
|
||||
},
|
||||
client: {
|
||||
getMessages: async () => [{ id: 1, text_content: '戒指', sender_type: 'contact', private: false }],
|
||||
sendMessage: async (_uuid, content) => { sent = content; },
|
||||
sendPrivateNote: async () => {},
|
||||
},
|
||||
tools,
|
||||
verification: { getVerifiedEmail: async () => null, sendCode: async () => ({}), verifyCode: async () => ({}) },
|
||||
metrics: { record: async () => {}, enabled: false },
|
||||
contextManager: { compressHistory: async (_c, h) => h },
|
||||
});
|
||||
const conv = { uuid: 'conv-backfill-1', inbox_id: '1', channel: 'livechat', contact: { type: 'visitor' } };
|
||||
await agent.run(conv, { uuid: 'msg-backfill-1', text_content: '戒指', content: '戒指' });
|
||||
assert.match(sent, /https:\/\/www\.yehwang\.com\/triple-rings-ring-gold-color-17590\.html/);
|
||||
assert.match(sent, /可直接查看/);
|
||||
});
|
||||
|
||||
test('agent: forces search_products when the LLM answers without tools', async () => {
|
||||
const queries = [];
|
||||
let calls = 0;
|
||||
const tools = [{
|
||||
name: 'search_products',
|
||||
description: 'fake',
|
||||
parameters: { type: 'object', properties: {} },
|
||||
handler: async (args) => {
|
||||
calls += 1;
|
||||
queries.push(args.query);
|
||||
if (calls === 1) throw new Error('rpc_timeout'); // 预检索失败 → 走事后强制注入
|
||||
return {
|
||||
products: [{ name: 'Triple Rings Ring', price: '$2.88', stock: 26, url: 'https://www.yehwang.com/triple-rings-ring-gold-color-17590.html' }],
|
||||
count: 1,
|
||||
};
|
||||
},
|
||||
}];
|
||||
tools.leadStore = { get: async () => null, save: async () => null };
|
||||
let llmCalls = 0;
|
||||
let sent = '';
|
||||
const agent = createAgent({ sites: [], defaultSite: 'www' }, {
|
||||
llm: {
|
||||
enabled: true,
|
||||
model: 'fake',
|
||||
chat: async () => {
|
||||
llmCalls += 1;
|
||||
if (llmCalls === 1) {
|
||||
return { content: 'Stainless Steel Ring Set — $9.99 → 查看详情:https://www.yehwang.com/fake-ring', toolCalls: [], usage: null };
|
||||
}
|
||||
return { content: '为您找到 Triple Rings Ring,$2.88,库存 26 件。', toolCalls: [], usage: null };
|
||||
},
|
||||
},
|
||||
client: {
|
||||
getMessages: async () => [{ id: 1, text_content: '戒指', sender_type: 'contact', private: false }],
|
||||
sendMessage: async (_uuid, content) => { sent = content; },
|
||||
sendPrivateNote: async () => {},
|
||||
},
|
||||
tools,
|
||||
verification: { getVerifiedEmail: async () => null, sendCode: async () => ({}), verifyCode: async () => ({}) },
|
||||
metrics: { record: async () => {}, enabled: false },
|
||||
contextManager: { compressHistory: async (_c, h) => h },
|
||||
});
|
||||
const conv = { uuid: 'conv-force-1', inbox_id: '1', channel: 'livechat', contact: { type: 'visitor' } };
|
||||
await agent.run(conv, { uuid: 'msg-force-1', text_content: '戒指', content: '戒指' });
|
||||
assert.equal(calls, 2, 'pre-tool fails once, forced injection retries');
|
||||
assert.deepEqual(queries, ['戒指', '戒指']);
|
||||
assert.doesNotMatch(sent, /Stainless Steel Ring Set/);
|
||||
assert.doesNotMatch(sent, /fake-ring/);
|
||||
assert.match(sent, /https:\/\/www\.yehwang\.com\/triple-rings-ring-gold-color-17590\.html/);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { quickClassify, INTENTS } = require('../src/intent');
|
||||
|
||||
test('intent: bare product category word is a product search', () => {
|
||||
for (const q of ['戒指', '项链', '耳环', '手链', '珠宝', 'rings', 'necklace', 'earrings', 'oorbellen', 'halskette']) {
|
||||
assert.equal(quickClassify(q), INTENTS.PRODUCT_SEARCH, q);
|
||||
}
|
||||
});
|
||||
|
||||
test('intent: product noun inside a sentence is a product search', () => {
|
||||
assert.equal(quickClassify('有没有不锈钢戒指'), INTENTS.PRODUCT_SEARCH);
|
||||
assert.equal(quickClassify('do you sell stainless steel rings'), INTENTS.PRODUCT_SEARCH);
|
||||
});
|
||||
|
||||
test('intent: other intents keep priority', () => {
|
||||
assert.equal(quickClassify('我的订单到哪了'), INTENTS.ORDER_TRACKING);
|
||||
assert.equal(quickClassify('我要转人工'), INTENTS.ESCALATE);
|
||||
assert.equal(quickClassify('怎么退货'), INTENTS.POLICY_FAQ);
|
||||
assert.equal(quickClassify('你好'), INTENTS.GENERAL);
|
||||
});
|
||||
@@ -140,3 +140,43 @@ test('guard: rewrites unsupported price but keeps product name', () => {
|
||||
assert.doesNotMatch(r.content, /fake-a/);
|
||||
assert.match(r.content, /价格以商品页为准/);
|
||||
});
|
||||
|
||||
test('guard: drops dangling CTA text when a hallucinated product link is removed', () => {
|
||||
const r = guardReply({
|
||||
content: '<p>🔹 <strong>Stainless Steel Ring Set</strong> — $2.88<br>✅ 库存:充足<br>→ 查看详情:https://www.yehwang.com/stainless-steel-ring-set-12-pcs</p>',
|
||||
toolResults: [],
|
||||
toolCalls: [],
|
||||
lang: 'zh',
|
||||
});
|
||||
assert.equal(r.blocked, true);
|
||||
assert.ok(r.reasons.includes('stock_price_without_tool'));
|
||||
assert.deepEqual(r.removed_urls, ['https://www.yehwang.com/stainless-steel-ring-set-12-pcs']);
|
||||
assert.doesNotMatch(r.content, /查看详情/);
|
||||
assert.doesNotMatch(r.content, /stainless-steel-ring-set-12-pcs/);
|
||||
assert.match(r.content, /价格以商品页为准/);
|
||||
});
|
||||
|
||||
test('guard: generic CTA label is removed together with the unverified URL', () => {
|
||||
const r = guardReply({
|
||||
content: '商品A — [查看详情](https://www.yehwang.com/fake-a)',
|
||||
toolResults: [{ products: [{ name: '商品A' }] }],
|
||||
toolCalls: [{ name: 'search_products', ok: true }],
|
||||
lang: 'zh',
|
||||
});
|
||||
assert.equal(r.blocked, true);
|
||||
assert.deepEqual(r.removed_urls, ['https://www.yehwang.com/fake-a']);
|
||||
assert.doesNotMatch(r.content, /查看详情/);
|
||||
assert.match(r.content, /商品A/);
|
||||
});
|
||||
|
||||
test('guard: keeps meaningful anchor text when the URL is removed', () => {
|
||||
const r = guardReply({
|
||||
content: 'You can read [the product care guide](https://www.yehwang.com/fake-guide) for details.',
|
||||
toolResults: [],
|
||||
toolCalls: [],
|
||||
lang: 'en',
|
||||
});
|
||||
assert.equal(r.blocked, true);
|
||||
assert.doesNotMatch(r.content, /fake-guide/);
|
||||
assert.match(r.content, /the product care guide/);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { appendMissingProductLinks, collectProductLinks } = require('../src/reply-links');
|
||||
|
||||
const TOOL_RESULTS = [{
|
||||
products: [
|
||||
{ name: 'Triple Rings Ring', url: 'https://www.yehwang.com/triple-rings-ring-gold-color-17590.html', price: '$2.88', stock: 26 },
|
||||
{ name: 'Aurora Rings Ring', url: 'https://www.yehwang.com/aurora-rings-ring-silver-color-color-17549.html', price: '$3.17', stock: 63 },
|
||||
],
|
||||
category_links: [{ name: 'Rings', url: 'https://www.yehwang.com/jewelry/rings' }],
|
||||
}];
|
||||
|
||||
test('reply-links: appends real product/category links as markdown', () => {
|
||||
const r = appendMissingProductLinks({ content: 'Here are some rings.', toolResults: TOOL_RESULTS, lang: 'English' });
|
||||
assert.equal(r.appended, 3);
|
||||
assert.match(r.content, /\[Triple Rings Ring\]\(https:\/\/www\.yehwang\.com\/triple-rings-ring-gold-color-17590\.html\)/);
|
||||
assert.match(r.content, /\[Rings\]\(https:\/\/www\.yehwang\.com\/jewelry\/rings\)/);
|
||||
assert.match(r.content, /Click to view:/);
|
||||
assert.match(r.content, /26 pcs/);
|
||||
});
|
||||
|
||||
test('reply-links: skips links already written by the model', () => {
|
||||
const content = 'See https://www.yehwang.com/triple-rings-ring-gold-color-17590.html for details.';
|
||||
const r = appendMissingProductLinks({ content, toolResults: TOOL_RESULTS, lang: 'English' });
|
||||
assert.equal(r.appended, 2);
|
||||
assert.equal((r.content.match(/triple-rings-ring-gold-color-17590/g) || []).length, 1);
|
||||
});
|
||||
|
||||
test('reply-links: respects the limit', () => {
|
||||
const r = appendMissingProductLinks({ content: 'Rings:', toolResults: TOOL_RESULTS, lang: 'English', limit: 1 });
|
||||
assert.equal(r.appended, 1);
|
||||
assert.doesNotMatch(r.content, /Aurora Rings Ring/);
|
||||
});
|
||||
|
||||
test('reply-links: html mode emits clickable anchors', () => {
|
||||
const r = appendMissingProductLinks({ content: '<p>Rings:</p>', toolResults: TOOL_RESULTS, lang: 'Chinese', html: true });
|
||||
assert.equal(r.appended, 3);
|
||||
assert.match(r.content, /<a href="https:\/\/www\.yehwang\.com\/triple-rings-ring-gold-color-17590\.html"/);
|
||||
assert.match(r.content, /可直接查看:/);
|
||||
assert.doesNotMatch(r.content, /\[Triple Rings Ring\]\(/);
|
||||
});
|
||||
|
||||
test('reply-links: does not backfill when the reply already says there is no match', () => {
|
||||
const r = appendMissingProductLinks({ content: 'No exact matches found.', toolResults: TOOL_RESULTS, lang: 'English' });
|
||||
assert.equal(r.appended, 0);
|
||||
assert.equal(r.content, 'No exact matches found.');
|
||||
});
|
||||
|
||||
test('reply-links: collects only http(s) urls and dedupes', () => {
|
||||
const links = collectProductLinks([
|
||||
{ products: [{ name: 'A', url: 'https://a.example/1' }, { name: 'B', url: 'https://a.example/1' }] },
|
||||
{ category_links: [{ name: 'C', url: 'https://c.example/2' }, { name: 'bad', url: '/relative/3' }] },
|
||||
]);
|
||||
assert.deepEqual(links.map((l) => l.url), ['https://a.example/1', 'https://c.example/2']);
|
||||
});
|
||||
|
||||
test('reply-links: prefers in-stock products over out-of-stock ones', () => {
|
||||
const toolResults = [{ products: [
|
||||
{ name: 'Out', url: 'https://a.example/out', price: '$1.00', stock: 0 },
|
||||
{ name: 'In', url: 'https://a.example/in', price: '$2.00', stock: 26 },
|
||||
{ name: 'Backorder', url: 'https://a.example/bo', price: '$3.00', stock: 0 },
|
||||
] }];
|
||||
const r = appendMissingProductLinks({ content: 'Rings:', toolResults, lang: 'English', limit: 2 });
|
||||
assert.equal(r.appended, 2);
|
||||
assert.match(r.content, /\[In\]\(https:\/\/a\.example\/in\)/);
|
||||
assert.ok(r.content.indexOf('a.example/in') < r.content.indexOf('a.example/out'), 'in-stock link comes first');
|
||||
});
|
||||
Reference in New Issue
Block a user