feat(r42): improve KB grounding accuracy and add clear-history
R42 was missing knowledge-graph information (e.g. pension questions) because retrieval and context-building dropped relevant facts: - retrieval: exact-token TF-IDF could not match Dutch compound words, so a "pensioen" query scored 0 against "pensioenregeling" / "partnerpensioen" and never retrieved them. Add a compound-word fallback (shared >=6-char stem or containment, 0.4x weight) alongside exact matching. - rag: deep article content was only injected for verbatim-mentioned topics; retrieved topics contributed just a 200-char description. Inject ~1000 chars of content for up to 5 topics (mentions first, then top-ranked retrieved) and widen the description snippet to 320. - prompts: add a NAUWKEURIGHEID block (use all relevant facts, call lookup_topic before giving up) and relax the 4-sentence cap for detail/list answers so complete facts aren't summarised away. Also add a clear-history control: a trash button in the chat header (confirm dialog) wipes chat🧵{userId} and reseeds the greeting via clearThread() in useChat. Tests: compound-word matching + rag deep-content injection. Spec updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,13 @@ import * as db from '../../lib/db';
|
||||
import { buildIndex, retrieveTopK } from '../../lib/retrieval';
|
||||
|
||||
const TOP_K = 10;
|
||||
// How many topics get their full article body injected (not just the short
|
||||
// description). Verbatim-mentioned topics come first, then the highest-ranked
|
||||
// retrieved ones, so a query that never names a topic exactly still gets rich
|
||||
// content for what it matched.
|
||||
const DEEP_CONTENT_LIMIT = 5;
|
||||
const DEEP_SNIPPET_CHARS = 1000;
|
||||
const DESC_SNIPPET_CHARS = 320;
|
||||
|
||||
async function sha256Hex(input) {
|
||||
const enc = new TextEncoder().encode(input);
|
||||
@@ -71,7 +78,7 @@ export async function buildKbContext(userMessage = '') {
|
||||
const included = [...includedById.values()];
|
||||
|
||||
const topicLines = included.map(t => {
|
||||
const desc = (t.description || '').replace(/\s+/g, ' ').trim().slice(0, 200);
|
||||
const desc = (t.description || '').replace(/\s+/g, ' ').trim().slice(0, DESC_SNIPPET_CHARS);
|
||||
return `- ${t.id} (${t.type || 'concept'}) "${t.label}": ${desc}`;
|
||||
});
|
||||
|
||||
@@ -85,19 +92,30 @@ export async function buildKbContext(userMessage = '') {
|
||||
}
|
||||
}
|
||||
|
||||
const mentionedDeepContent = [];
|
||||
for (const id of mentionedIds) {
|
||||
const t = includedById.get(id);
|
||||
if (!t) continue;
|
||||
const content = await db.getContent(t.id).catch(() => null);
|
||||
if (!content) continue;
|
||||
let raw;
|
||||
if (typeof content === 'string') raw = content;
|
||||
else if (content.article) raw = typeof content.article === 'string' ? content.article : JSON.stringify(content.article);
|
||||
else raw = JSON.stringify(content);
|
||||
const snippet = raw.replace(/\s+/g, ' ').trim().slice(0, 1200);
|
||||
mentionedDeepContent.push(`### ${t.label}\n${snippet}`);
|
||||
// Pick which topics get their full article body: verbatim mentions first,
|
||||
// then the highest-ranked retrieved topics, capped at DEEP_CONTENT_LIMIT.
|
||||
const deepIds = [];
|
||||
for (const id of mentionedIds) deepIds.push(id);
|
||||
for (const t of retrieved) {
|
||||
if (deepIds.length >= DEEP_CONTENT_LIMIT) break;
|
||||
if (!mentionedIds.has(t.id)) deepIds.push(t.id);
|
||||
}
|
||||
const deepBlocks = await Promise.all(
|
||||
deepIds.slice(0, DEEP_CONTENT_LIMIT).map(async (id) => {
|
||||
const t = includedById.get(id);
|
||||
if (!t) return null;
|
||||
const content = await db.getContent(id).catch(() => null);
|
||||
if (!content) return null;
|
||||
let raw;
|
||||
if (typeof content === 'string') raw = content;
|
||||
else if (content.article) raw = typeof content.article === 'string' ? content.article : JSON.stringify(content.article);
|
||||
else raw = JSON.stringify(content);
|
||||
const snippet = raw.replace(/\s+/g, ' ').trim().slice(0, DEEP_SNIPPET_CHARS);
|
||||
if (!snippet) return null;
|
||||
return `### ${t.label}\n${snippet}`;
|
||||
}),
|
||||
);
|
||||
const mentionedDeepContent = deepBlocks.filter(Boolean);
|
||||
|
||||
const context = [
|
||||
`KENNISGRAAF — RELEVANTE TOPICS (top ${included.length} van ${allTopics.length}):`,
|
||||
@@ -106,7 +124,7 @@ export async function buildKbContext(userMessage = '') {
|
||||
`KENNISGRAAF — RELATIES (binnen deze selectie):`,
|
||||
relLines.length ? relLines.join('\n') : '(geen relaties binnen deze selectie)',
|
||||
mentionedDeepContent.length
|
||||
? `\n\nDIEPERE INHOUD (voor genoemde topics):\n${mentionedDeepContent.join('\n\n')}`
|
||||
? `\n\nDIEPERE INHOUD (volledige leerinhoud van de meest relevante topics — gebruik álle feiten hieruit die de vraag beantwoorden):\n${mentionedDeepContent.join('\n\n')}`
|
||||
: '',
|
||||
``,
|
||||
`Als de relevante context hierboven te beperkt is, gebruik dan de tool "lookup_topic" om de volledige beschrijving en eventuele leerinhoud van een specifiek topic op te halen.`,
|
||||
|
||||
Reference in New Issue
Block a user