E5.S1 compleet: AI filtert op behandelrelevantie voor psychiaters. Voortgang: 56 SP / 72 SP (78%) - Epic 5 in progress! Implementatie: - Twee-staps filtering: Server (include_in_handover=true) → AI (psychiater: behandelrelevant) - Role toggle UI: Verpleegkundige/Psychiater knoppen in OverdrachtBlock - buildSystemPrompt(role) functie met psychiater-specifieke filtering regels - filterForRole parameter toegevoegd aan GenerateOverdrachtSchema - API route verwerkt role parameter en stuurt door naar Claude AI Psychiater filtering regels: ✅ WEL RELEVANT: - Medicatie-issues: weigering, bijwerkingen, dosisaanpassingen - Stemming/gedrag: veranderingen, afwijkend gedrag, agitatie - Risico-signalen: suïcidaliteit, automutilatie, agressie - Psychotische symptomen: wanen, hallucinaties - Crisis/dwang: separatie, fixatie, dwangmedicatie - Afwijkende vitals: HH, LL met behandelimpact ❌ FILTER UIT: - Routine medicatie ("volgens schema", "zonder problemen") - ADL activiteiten (douchen, eten) tenzij significant afwijkend - Sociale activiteiten ("groepstherapie", "gesprek gehad") - Standaard observaties ("rustige dag", "geen bijzonderheden") - Routine vitals (normaal bereik, N interpretatie) - Dagstructuur ("dagprogramma gevolgd") UI/UX verbeteringen: - Role toggle met 2 knoppen (verpleegkundige/psychiater) - Visual hints: "Alle gemarkeerde items" vs "Behandelrelevante items" - Dynamic description tekst onder toggle - filterRole state + useCallback dependency update Files Changed: - lib/types/overdracht.ts: +1 line (filterForRole in schema) - lib/ai/overdracht-prompt.ts: +52 lines (buildSystemPrompt functie) - app/api/overdracht/generate/route.ts: +8/-8 lines (role parameter) - components/swift/blocks/overdracht-block.tsx: +43 lines (UI + logic) Technische details: - filterForRole default: 'verpleegkundige' - Psychiater max: 3 aandachtspunten, 2 actiepunten - Verpleegkundige max: 5 aandachtspunten, 3 actiepunten - Role parameter propageert van UI → API → Claude prompt Testing: - Build succesvol (pnpm build) - Geen nieuwe type errors - UI toggle functioneel Voortgang: 56 SP / 72 SP (78%) - E5.S1 compleet (5 SP) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
283 lines
9.0 KiB
TypeScript
283 lines
9.0 KiB
TypeScript
/**
|
|
* Overdracht AI Prompt Templates
|
|
*
|
|
* System and user prompts for generating nursing handover summaries
|
|
*/
|
|
|
|
import type {
|
|
VitalSign,
|
|
Report,
|
|
RiskAssessment,
|
|
Condition,
|
|
} from '@/lib/types/overdracht';
|
|
import { getVerpleegkundigCategory, CATEGORY_CONFIG } from '@/lib/types/report';
|
|
|
|
export interface OverdrachtContext {
|
|
patientId: string;
|
|
patientName: string;
|
|
age: number;
|
|
gender: string;
|
|
conditions: Condition[];
|
|
vitals: VitalSign[];
|
|
reports: Report[];
|
|
risks: RiskAssessment[];
|
|
}
|
|
|
|
export const OVERDRACHT_SYSTEM_PROMPT = `Je bent een ervaren verpleegkundige die overdrachten maakt in een GGZ-instelling.
|
|
|
|
Je taak: Maak een beknopte, relevante overdracht voor de opvolgende dienst.
|
|
|
|
## Outputformaat (JSON)
|
|
{
|
|
"samenvatting": "1-2 zinnen over de patiënt en wat er speelt",
|
|
"aandachtspunten": [
|
|
{
|
|
"tekst": "Beschrijving van het aandachtspunt",
|
|
"urgent": true/false,
|
|
"bron": {
|
|
"type": "observatie|rapportage|verpleegkundig|risico",
|
|
"id": "source-id",
|
|
"datum": "DD-MM-YYYY HH:mm",
|
|
"label": "Korte beschrijving bron"
|
|
}
|
|
}
|
|
],
|
|
"actiepunten": [
|
|
"Concrete actie voor opvolgende dienst"
|
|
]
|
|
}
|
|
|
|
## Regels
|
|
1. Taal: Nederlands, zakelijk, beknopt
|
|
2. Focus op: veranderingen, afwijkingen, aandachtspunten
|
|
3. Elke aandachtspunt MOET een bronverwijzing hebben
|
|
4. Maximum 5 aandachtspunten, 3 actiepunten
|
|
5. Markeer urgent=true voor:
|
|
- Medicatie-weigering
|
|
- Incidenten
|
|
- Sterk afwijkende vitals (HH, LL, H, L interpretatie)
|
|
- Hoog-risico situaties (risico_level: hoog of zeer_hoog)
|
|
6. Als er weinig data is, geef een korte samenvatting en meld dat er weinig bijzonderheden zijn
|
|
7. Gebruik ALLEEN informatie uit de aangeleverde bronnen, verzin NIETS
|
|
|
|
## Bronverwijzing formaat
|
|
- type "observatie" voor vitale functies (observations tabel)
|
|
- type "rapportage" voor rapporten (reports met type != verpleegkundig)
|
|
- type "verpleegkundig" voor verpleegkundige notities (reports met type = verpleegkundig)
|
|
- type "risico" voor risico assessments (risk_assessments tabel)
|
|
|
|
Geef je antwoord als PURE JSON, zonder markdown code blocks.`;
|
|
|
|
/**
|
|
* Build system prompt based on target role
|
|
* For psychiater: adds strict filtering on treatment relevance
|
|
* For verpleegkundige: uses base prompt (all marked items)
|
|
*/
|
|
export function buildSystemPrompt(role: 'psychiater' | 'verpleegkundige' = 'verpleegkundige'): string {
|
|
if (role === 'psychiater') {
|
|
return `${OVERDRACHT_SYSTEM_PROMPT}
|
|
|
|
## PSYCHIATER FILTERING
|
|
Je maakt overdracht voor een PSYCHIATER. Filter STRIKT op behandelrelevantie.
|
|
|
|
De verpleegkundige heeft al items geselecteerd (include_in_handover=true), maar jij moet VERDER FILTEREN.
|
|
|
|
✅ WEL RELEVANT (include in samenvatting):
|
|
- Medicatie-issues: weigering, bijwerkingen, dosisaanpassingen, therapietrouw
|
|
- Stemming/gedrag: veranderingen van baseline, afwijkend gedrag, agitatie
|
|
- Risico-signalen: suïcidale uitingen, automutilatie, agressie
|
|
- Psychotische symptomen: wanen, hallucinaties, desorganisatie, paranoia
|
|
- Crisis/dwang: separatie, fixatie, dwangmedicatie, vrijheidsbeperkende maatregelen
|
|
- Afwijkende vitals: HH (kritiek hoog), LL (kritiek laag) met behandelimpact
|
|
|
|
❌ FILTER UIT (niet in samenvatting):
|
|
- Routine medicatie: "medicatie volgens schema", "zonder problemen", "ingenomen conform afspraak"
|
|
- ADL activiteiten: douchen, aankleden, eten, drinken (tenzij significant afwijkend/weigering)
|
|
- Sociale activiteiten: "deelgenomen aan groepstherapie", "gesprek gehad", "koffie gedronken"
|
|
- Standaard observaties: "rustige dag", "geen bijzonderheden", "normaal functioneren"
|
|
- Routine vitals: bloeddruk/pols binnen normaalwaarden (N interpretatie)
|
|
- Dagstructuur: "dagprogramma gevolgd", "aanwezig bij activiteit"
|
|
|
|
VUISTREGEL: Include ALLEEN als een psychiater op basis van deze info een BEHANDELBESLISSING kan nemen.
|
|
|
|
Beperkingen:
|
|
- Maximum 3 aandachtspunten (ALLEEN behandelrelevant, geen routine items)
|
|
- Maximum 2 actiepunten (ALLEEN actionable voor psychiater)
|
|
- Bij twijfel of iets relevant is → FILTER UIT
|
|
|
|
Voorbeelden:
|
|
✅ INCLUDE: "Jan weigerde haloperidol, zegt dat medicatie hem controleert" → Medicatie-compliance issue
|
|
✅ INCLUDE: "Marie uitte suïcidale gedachten tijdens gesprek" → Risicosignaal, urgent
|
|
✅ INCLUDE: "Piet verbaal en fysiek agressief, separatie 30 min" → Crisis, gedragsverandering
|
|
❌ FILTER: "Jan heeft goed gegeten, ontbijt en lunch zonder problemen" → Routine ADL
|
|
❌ FILTER: "Marie deelgenomen aan groepstherapie" → Sociale activiteit, geen issues
|
|
❌ FILTER: "Piet heeft medicatie ingenomen volgens schema" → Routine medicatie
|
|
|
|
Geef je antwoord als PURE JSON, zonder markdown code blocks.`;
|
|
}
|
|
|
|
// Verpleegkundige uses base prompt (no filtering)
|
|
return OVERDRACHT_SYSTEM_PROMPT;
|
|
}
|
|
|
|
/**
|
|
* Format timestamp to Dutch date format
|
|
*/
|
|
function formatDateTime(timestamp: string): string {
|
|
const date = new Date(timestamp);
|
|
return date.toLocaleString('nl-NL', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get interpretation label for vital signs
|
|
*/
|
|
function getInterpretationLabel(code: string | null | undefined): string {
|
|
if (!code) return 'normaal';
|
|
switch (code) {
|
|
case 'HH':
|
|
return 'kritiek hoog';
|
|
case 'H':
|
|
return 'hoog';
|
|
case 'LL':
|
|
return 'kritiek laag';
|
|
case 'L':
|
|
return 'laag';
|
|
case 'N':
|
|
return 'normaal';
|
|
default:
|
|
return code;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Truncate text to max length
|
|
*/
|
|
function truncate(text: string, maxLength: number): string {
|
|
if (text.length <= maxLength) return text;
|
|
return text.slice(0, maxLength - 3) + '...';
|
|
}
|
|
|
|
/**
|
|
* Build the user prompt with patient context
|
|
*/
|
|
export function buildOverdrachtUserPrompt(context: OverdrachtContext): string {
|
|
const lines: string[] = [
|
|
`PATIENT: ${context.patientName}, ${context.age} jaar, ${context.gender}`,
|
|
'',
|
|
];
|
|
|
|
// Diagnoses
|
|
lines.push('DIAGNOSES:');
|
|
if (context.conditions.length > 0) {
|
|
for (const c of context.conditions) {
|
|
lines.push(`- ${c.code_display} (source: conditions/${c.id})`);
|
|
}
|
|
} else {
|
|
lines.push('- Geen actieve diagnoses geregistreerd');
|
|
}
|
|
lines.push('');
|
|
|
|
// Vitals
|
|
lines.push('VITALE FUNCTIES (vandaag):');
|
|
if (context.vitals.length > 0) {
|
|
for (const v of context.vitals) {
|
|
const value = v.value_quantity_value ?? 'onbekend';
|
|
const unit = v.value_quantity_unit ?? '';
|
|
const interpretation = getInterpretationLabel(v.interpretation_code);
|
|
lines.push(
|
|
`- ${v.code_display}: ${value} ${unit} [${interpretation}] ` +
|
|
`(source: observations/${v.id}, ${formatDateTime(v.effective_datetime)})`
|
|
);
|
|
}
|
|
} else {
|
|
lines.push('- Geen metingen vandaag');
|
|
}
|
|
lines.push('');
|
|
|
|
// Split reports into regular rapportages and verpleegkundige notities
|
|
const regularReports = context.reports.filter((r) => r.type !== 'verpleegkundig');
|
|
const verpleegkundigReports = context.reports.filter((r) => r.type === 'verpleegkundig');
|
|
|
|
// Regular reports
|
|
lines.push('RAPPORTAGES (laatste 24u):');
|
|
if (regularReports.length > 0) {
|
|
for (const r of regularReports) {
|
|
lines.push(
|
|
`- [${formatDateTime(r.created_at)}] ${r.type}: "${truncate(r.content, 200)}" ` +
|
|
`(source: reports/${r.id})`
|
|
);
|
|
}
|
|
} else {
|
|
lines.push('- Geen rapportages in de laatste 24 uur');
|
|
}
|
|
lines.push('');
|
|
|
|
// Verpleegkundige notities (was nursing logs)
|
|
lines.push('VERPLEEGKUNDIGE NOTITIES (vandaag):');
|
|
if (verpleegkundigReports.length > 0) {
|
|
for (const r of verpleegkundigReports) {
|
|
const category = getVerpleegkundigCategory(r.structured_data);
|
|
const categoryLabel = category ? CATEGORY_CONFIG[category]?.label : 'Notitie';
|
|
const handoverMark = r.include_in_handover ? '[OVERDRACHT]' : '';
|
|
lines.push(
|
|
`- [${formatDateTime(r.created_at)}] [${categoryLabel.toUpperCase()}] ${handoverMark} ${r.content} ` +
|
|
`(source: reports/${r.id})`
|
|
);
|
|
}
|
|
} else {
|
|
lines.push('- Geen verpleegkundige notities vandaag');
|
|
}
|
|
lines.push('');
|
|
|
|
// Risks
|
|
lines.push('RISICOS:');
|
|
if (context.risks.length > 0) {
|
|
for (const r of context.risks) {
|
|
const rationale = r.rationale ? `: "${truncate(r.rationale, 150)}"` : '';
|
|
lines.push(
|
|
`- [${r.risk_level.toUpperCase()}] ${r.risk_type}${rationale} ` +
|
|
`(source: risk_assessments/${r.id})`
|
|
);
|
|
}
|
|
} else {
|
|
lines.push('- Geen risico assessments geregistreerd');
|
|
}
|
|
|
|
return lines.join('\n');
|
|
}
|
|
|
|
/**
|
|
* Calculate age from birth date
|
|
*/
|
|
export function calculateAge(birthDate: string): number {
|
|
const birth = new Date(birthDate);
|
|
const today = new Date();
|
|
let age = today.getFullYear() - birth.getFullYear();
|
|
const monthDiff = today.getMonth() - birth.getMonth();
|
|
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
|
|
age--;
|
|
}
|
|
return age;
|
|
}
|
|
|
|
/**
|
|
* Format patient name
|
|
*/
|
|
export function formatPatientName(
|
|
nameGiven: string[],
|
|
nameFamily: string,
|
|
namePrefix?: string
|
|
): string {
|
|
const given = nameGiven.join(' ');
|
|
if (namePrefix) {
|
|
return `${given} ${namePrefix} ${nameFamily}`;
|
|
}
|
|
return `${given} ${nameFamily}`;
|
|
}
|