feat: phase 5 of AI pipeline hardening — R42 retrieval & telemetry
- Add dependency-free TF-IDF retrieval (src/lib/retrieval.js) with NL+EN
stopwords and a WeakMap-cached index.
- Rewrite buildKbContext to ship the top-K relevant topics + verbatim-
mentioned ids only, filter relations to the included set, and append a
[kb_hash: <8 hex>] suffix so the ephemeral prompt cache busts when the
graph changes. Returns { context, retrievedTopics, allTopics }.
- Add LOOKUP_TOPIC_TOOL and drive useChat through callLLM directly with a
multi-hop tool_result loop capped at 3 hops; preserve Anthropic-provided
tool_use ids through callLLM so the loop can echo correct tool_use_id.
- Truncate R42 history to the last 12 turns and prepend a single
"(earlier conversation truncated)" assistant message.
- Set R42 chat defaults: temperature 0.3, maxTokens 2048.
- Add pb_migrations/1780500002_created_llm_calls.js (the best-effort
logger in callLLM was already wired) and a new Admin → Diagnostics
view showing the last 100 calls with token usage, cache-hit rate, and
USD cost from a local Anthropic price table.
- Finalize AI_PIPELINE_HARDENING_PLAN.md: mark Phases 1–5 shipped and
Phase 6 (eval harness) explicitly out of scope.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
58
src/lib/__tests__/retrieval.test.js
Normal file
58
src/lib/__tests__/retrieval.test.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildIndex, retrieveTopK, tokenize } from '../retrieval';
|
||||
|
||||
const sampleTopics = [
|
||||
{ id: 'software-engineer', label: 'Software Engineer', description: 'Bouwt en onderhoudt applicaties; werkt in agile teams.' },
|
||||
{ id: 'onboarding-buddy', label: 'Onboarding Buddy', description: 'Begeleidt nieuwe medewerkers in hun eerste weken.' },
|
||||
{ id: 'kennisbeheer', label: 'Kennisbeheer', description: 'Het proces van het vastleggen en ontsluiten van organisatiekennis.' },
|
||||
{ id: 'wekelijkse-sessie', label: 'Wekelijkse Leersessie', description: 'Microlearning sessie waarin medewerkers wekelijks leren via AI-gegenereerde quizzen.' },
|
||||
];
|
||||
|
||||
describe('tokenize', () => {
|
||||
it('lowercases and splits on non-alphanumeric', () => {
|
||||
expect(tokenize('Hello, World!')).toEqual(['hello', 'world']);
|
||||
});
|
||||
|
||||
it('drops stopwords and short tokens', () => {
|
||||
expect(tokenize('de software engineer is hier')).toEqual(['software', 'engineer', 'hier']);
|
||||
});
|
||||
|
||||
it('keeps hyphenated identifiers', () => {
|
||||
expect(tokenize('software-engineer onboarding-buddy')).toEqual(['software-engineer', 'onboarding-buddy']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildIndex / retrieveTopK', () => {
|
||||
it('returns empty for empty topics', () => {
|
||||
const idx = buildIndex([]);
|
||||
expect(retrieveTopK(idx, 'anything')).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns empty for empty query', () => {
|
||||
const idx = buildIndex(sampleTopics);
|
||||
expect(retrieveTopK(idx, '')).toEqual([]);
|
||||
});
|
||||
|
||||
it('ranks the most relevant topic first', () => {
|
||||
const idx = buildIndex(sampleTopics);
|
||||
const hits = retrieveTopK(idx, 'wat doet een onboarding buddy?', 2);
|
||||
expect(hits[0].id).toBe('onboarding-buddy');
|
||||
});
|
||||
|
||||
it('matches on description when label does not contain query terms', () => {
|
||||
const idx = buildIndex(sampleTopics);
|
||||
const hits = retrieveTopK(idx, 'microlearning quizzen', 3);
|
||||
expect(hits.map(h => h.id)).toContain('wekelijkse-sessie');
|
||||
});
|
||||
|
||||
it('returns no hits when no terms match', () => {
|
||||
const idx = buildIndex(sampleTopics);
|
||||
expect(retrieveTopK(idx, 'kwantumfysica raketten')).toEqual([]);
|
||||
});
|
||||
|
||||
it('caches the index per topics array reference', () => {
|
||||
const idx1 = buildIndex(sampleTopics);
|
||||
const idx2 = buildIndex(sampleTopics);
|
||||
expect(idx1).toBe(idx2);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user