- 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>
144 lines
5.7 KiB
JavaScript
144 lines
5.7 KiB
JavaScript
/**
|
||
* R42 — system prompts, greeting, and the propose_graph_delta tool spec.
|
||
* All user-facing strings live here so they can be localised in one place.
|
||
*/
|
||
|
||
export const BOT_NAME = 'R42';
|
||
|
||
export const STRINGS = {
|
||
greeting: (name) =>
|
||
`Hoi ${name}, ik ben R42 — vraag iets over je weekonderwerp, of laat me uitleggen wat je in de quiz tegenkwam.`,
|
||
placeholder: `Vraag R42 iets…`,
|
||
send: 'Stuur',
|
||
status: 'online · antwoordt direct',
|
||
errorGeneric: 'Sorry, ik kon dat niet beantwoorden. Probeer het nog eens.',
|
||
errorNoKey: 'Er is geen API-sleutel ingesteld. Vraag een beheerder om hem toe te voegen in Admin → Settings.',
|
||
suggestionTitle: 'Ik hoorde iets nieuws — voeg toe aan de kennisgraaf?',
|
||
suggestionAccept: 'Ja, voeg toe',
|
||
suggestionReject: 'Nee, sla over',
|
||
suggestionAppliedAdmin: 'Toegevoegd aan de graaf.',
|
||
suggestionQueuedUser: 'Genoteerd — ik geef het door aan een beheerder.',
|
||
suggestionDismissed: 'Oké, niets gedaan.',
|
||
closeAria: 'Sluit chatvenster',
|
||
openAria: 'Open R42 chatbot',
|
||
};
|
||
|
||
const STABLE_PREAMBLE = [
|
||
`Je bent R42, de chatbot-avatar van Respellion — een leerplatform voor microlearning, quizzen en kennisontwikkeling.`,
|
||
`Antwoord altijd in het Nederlands, kort en zakelijk-vriendelijk. Spreek de gebruiker aan met hun voornaam wanneer dat natuurlijk voelt.`,
|
||
``,
|
||
`JE TAKEN:`,
|
||
`1. Leg onderwerpen uit die in de kennisbasis staan.`,
|
||
`2. Help de gebruiker quizvragen begrijpen ná afloop (niet tijdens een actieve quiz).`,
|
||
`3. Verwijs bij twijfel terug naar het bronmateriaal of zeg eerlijk dat je het niet weet.`,
|
||
``,
|
||
`JE KENNIS:`,
|
||
`Je kennis is beperkt tot de Respellion-kennisgraaf die hieronder volgt. Als een vraag duidelijk buiten dit bereik valt, zeg dat dan eerlijk en stel voor dat de gebruiker de bron toevoegt via Admin → Sources.`,
|
||
``,
|
||
`KENNISGRAAF VERFIJNEN:`,
|
||
`Wanneer de gebruiker iets noemt dat duidelijk een nieuw topic, nieuwe relatie, proces of rol is — en dat nog niet in de kennisgraaf staat — gebruik dan de tool "propose_graph_delta" om een voorstel te maken. Verzin niets: stel alleen iets voor als de gebruiker het concreet noemt. Stel maximaal 3 topics en 5 relaties per beurt voor.`,
|
||
``,
|
||
`STIJL:`,
|
||
`- Houd antwoorden onder de 4 zinnen tenzij de gebruiker om uitleg vraagt.`,
|
||
`- Geen markdown-headers; gewone Nederlandse tekst.`,
|
||
`- Bij onzekerheid: "Ik weet het niet zeker — controleer dit in het handboek."`,
|
||
].join('\n');
|
||
|
||
/**
|
||
* Build the R42 system prompt as three cacheable blocks:
|
||
* 1. stable preamble (role, tasks, style) — cached
|
||
* 2. KB context (current topics + relations) — cached (hash-bust comes in Phase 5)
|
||
* 3. per-turn tail (user name + admin status) — NOT cached
|
||
*
|
||
* Returning an array lets `callLLM` pass it through unchanged so the
|
||
* Anthropic API caches each block with the 5-minute ephemeral TTL.
|
||
*/
|
||
export function buildSystemPrompt({ userName, isAdmin, kbContext }) {
|
||
const tail = [
|
||
`De gebruiker heet ${userName}.`,
|
||
isAdmin
|
||
? `De gebruiker is beheerder; voorstellen die de tool genereert worden direct toegepast.`
|
||
: `De gebruiker is geen beheerder; voorstellen worden in een goedkeuringswachtrij gezet.`,
|
||
].join('\n');
|
||
|
||
return [
|
||
{ type: 'text', text: STABLE_PREAMBLE, cache_control: { type: 'ephemeral' } },
|
||
{ type: 'text', text: kbContext, cache_control: { type: 'ephemeral' } },
|
||
{ type: 'text', text: tail },
|
||
];
|
||
}
|
||
|
||
export const LOOKUP_TOPIC_TOOL = {
|
||
name: 'lookup_topic',
|
||
description:
|
||
'Haal de volledige beschrijving en eventuele leerinhoud van één topic op uit de kennisgraaf. Gebruik dit wanneer de samenvattende KENNISGRAAF in de systeemprompt niet genoeg informatie bevat om de vraag te beantwoorden.',
|
||
input_schema: {
|
||
type: 'object',
|
||
properties: {
|
||
id: {
|
||
type: 'string',
|
||
description: 'Het exacte topic id (kebab-case) zoals het in de kennisgraaf staat.',
|
||
},
|
||
},
|
||
required: ['id'],
|
||
},
|
||
};
|
||
|
||
export const PROPOSE_GRAPH_DELTA_TOOL = {
|
||
name: 'propose_graph_delta',
|
||
description:
|
||
'Stel toevoegingen aan de Respellion-kennisgraaf voor wanneer de gebruiker een topic, proces, rol of relatie noemt die nog niet bestaat. Gebruik dit alleen wanneer je iets nieuws en concreets hoort — niet voor algemene speculatie.',
|
||
input_schema: {
|
||
type: 'object',
|
||
properties: {
|
||
reason: {
|
||
type: 'string',
|
||
description: 'Korte uitleg (1 zin, NL) waarom dit voorstel relevant is.',
|
||
},
|
||
topics: {
|
||
type: 'array',
|
||
description: 'Maximaal 3 nieuwe topics.',
|
||
items: {
|
||
type: 'object',
|
||
properties: {
|
||
id: {
|
||
type: 'string',
|
||
description: 'kebab-case identifier, bv. "onboarding-buddy".',
|
||
},
|
||
label: {
|
||
type: 'string',
|
||
description: 'Mens-leesbare naam.',
|
||
},
|
||
type: {
|
||
type: 'string',
|
||
enum: ['concept', 'role', 'process'],
|
||
},
|
||
description: {
|
||
type: 'string',
|
||
description: '1–2 zinnen Nederlandstalige beschrijving.',
|
||
},
|
||
},
|
||
required: ['id', 'label', 'type', 'description'],
|
||
},
|
||
},
|
||
relations: {
|
||
type: 'array',
|
||
description: 'Maximaal 5 nieuwe relaties.',
|
||
items: {
|
||
type: 'object',
|
||
properties: {
|
||
source: { type: 'string', description: 'topic id' },
|
||
target: { type: 'string', description: 'topic id' },
|
||
type: {
|
||
type: 'string',
|
||
enum: ['related_to', 'depends_on', 'part_of', 'executed_by'],
|
||
},
|
||
},
|
||
required: ['source', 'target', 'type'],
|
||
},
|
||
},
|
||
},
|
||
required: ['reason'],
|
||
},
|
||
};
|