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:
RaymondVerhoef
2026-07-13 14:25:08 +02:00
parent e73700763d
commit 85452f66a7
9 changed files with 239 additions and 30 deletions

View File

@@ -50,6 +50,31 @@ describe('buildIndex / retrieveTopK', () => {
expect(retrieveTopK(idx, 'kwantumfysica raketten')).toEqual([]);
});
it('matches Dutch compound words on a shared stem', () => {
const pensionTopics = [
{ id: 'pensioenregeling', label: 'Pensioenregeling', description: 'De beschikbare premieregeling bij a.s.r. Doen Pensioen.' },
{ id: 'partnerpensioen', label: 'Partnerpensioen', description: 'Uitkering aan de partner bij overlijden.' },
{ id: 'reiskostenvergoeding', label: 'Reiskostenvergoeding', description: 'EUR 0,23 per kilometer voor woon-werkverkeer.' },
];
const idx = buildIndex(pensionTopics);
// "pensioen" never appears as a standalone token in a label, yet the stem is
// a prefix of "pensioenregeling" and an infix of "partnerpensioen".
const hits = retrieveTopK(idx, 'wat dekt mijn pensioen?', 3).map(h => h.id);
expect(hits).toContain('pensioenregeling');
expect(hits).toContain('partnerpensioen');
expect(hits).not.toContain('reiskostenvergoeding');
});
it('does not partial-match on short shared prefixes', () => {
const topics = [
{ id: 'onderhoud', label: 'Onderhoud', description: 'Technisch beheer van systemen.' },
];
const idx = buildIndex(topics);
// "onderneming" shares only "onder" (5) with "onderhoud" — below the overlap
// needed for a query token this size to count.
expect(retrieveTopK(idx, 'onderneming')).toEqual([]);
});
it('caches the index per topics array reference', () => {
const idx1 = buildIndex(sampleTopics);
const idx2 = buildIndex(sampleTopics);