Every structured-output call now uses an Anthropic tool instead of
parsing JSON out of free-form prose, and stable system prompts are
sent as cacheable blocks. Behaviour-equivalent to phase 1 from the
caller's point of view; the savings show up in token usage and in the
absence of "AI returned non-JSON response" failure modes.
* src/lib/llmTools.js — single source of truth for tool definitions:
emit_knowledge_graph, emit_handbook_delta, emit_learning_article /
_slides / _infographic / _all, emit_custom_topic, emit_quiz_questions,
emit_graph_actions, plus five article-patch tools (set_intro,
set_section, add_section, remove_section, replace_takeaways).
* src/lib/articlePatches.js — pure applyArticlePatches +
applyAndValidate; rebuilds the article from a sequence of patch tool
calls and re-validates against learningArticleSchema. set_section
falls back to appending when no matching heading exists so the
model's intent is preserved rather than silently dropped.
* src/lib/llmSchemas.js — Zod schemas for the five patch ops,
registered in toolSchemaRegistry so callLLM validates them
automatically.
* src/lib/llm.js — simulation mode now returns a tool_use stub matching
toolChoice.name, so the UI keeps working with Simulation Mode on
after the structured-output migration.
* src/lib/extractionPipeline.js — processSourceText and
analyzeHandbookDelta migrated to callLLM + tool use. System prompts
sent as { cache_control: ephemeral } blocks. Handbook results pass
through normalizeHandbookResult to collapse legacy "executes"
relations into executed_by with swapped source/target.
* src/lib/learningService.js — generateLearningContent picks the right
tool per selectedType; generateCustomTopic uses emit_custom_topic;
refineLearningContent now drives the five patch tools with
toolChoice 'any' and rejects the whole turn if the patched article
fails validation. Article-only refinement is intentional for phase 2;
refining a topic without an article surfaces a clear error.
* src/lib/testService.js — quiz generation via emit_quiz_questions.
* src/components/admin/KnowledgeGraph.jsx — analyzeGraph routed through
the reasoning tier (Opus) since graph-wide consolidation benefits
from a stronger reasoner.
* src/components/chat/prompts.js — buildSystemPrompt now returns three
text blocks: stable preamble (cached), KB context (cached, hash-bust
deferred to phase 5), per-turn user/admin tail (uncached).
* src/lib/__tests__/ — 13 new tests covering each patch op, multi-op
sequencing, post-patch validation failure, and tool/registry shape.
Acceptance: lint and 45/45 tests green; build succeeds; no
`match(/\{[\s\S]*\}/)` JSON extraction left in src/. Live verification
of cache hits on a second extraction within 5 minutes is deferred to
manual smoke testing — needs real `/api/anthropic` traffic.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
128 lines
5.2 KiB
JavaScript
128 lines
5.2 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 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'],
|
||
},
|
||
};
|