Files
learning-platform/src/components/chat/prompts.js
RaymondVerhoef f838755991
All checks were successful
On Pull Request to Main / test (pull_request) Successful in 31s
On Pull Request to Main / publish (pull_request) Successful in 1m1s
On Pull Request to Main / deploy-dev (pull_request) Successful in 1m31s
feat: phase 2 of AI pipeline hardening — tool-based structured outputs + prompt caching
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>
2026-05-20 15:47:20 +02:00

128 lines
5.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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: '12 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'],
},
};