| E2.S1 | Prompt engineering | System prompt + user prompt templates, evidence-based mapping
| E2.S2 | Generate endpoint | `POST /api/behandelplan/generate` retourneert JSON, < 8 sec response | E2.S3 | AI event logging | Calls loggen naar `ai_events` tabel new file: app/api/behandelplan/generate/route.ts modified: docs/specs/behandelplan/bouwplan-behandelplan-v1.md modified: docs/templates/bouwplan_template.md new file: lib/ai/behandelplan-prompt.ts new file: lib/ai/intervention-mapping.ts modified: lib/supabase/database.types.ts
This commit is contained in:
327
app/api/behandelplan/generate/route.ts
Normal file
327
app/api/behandelplan/generate/route.ts
Normal file
@@ -0,0 +1,327 @@
|
||||
/**
|
||||
* Behandelplan Generate API
|
||||
*
|
||||
* POST /api/behandelplan/generate
|
||||
* Genereert een behandelplan met Claude AI
|
||||
*/
|
||||
|
||||
import { createClient } from '@/lib/auth/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
BEHANDELPLAN_SYSTEM_PROMPT,
|
||||
buildUserPrompt,
|
||||
validatePlanContext,
|
||||
type PlanContext,
|
||||
} from '@/lib/ai/behandelplan-prompt';
|
||||
import { type Severity } from '@/lib/ai/intervention-mapping';
|
||||
import {
|
||||
GeneratedPlanSchema,
|
||||
type GeneratedPlan,
|
||||
} from '@/lib/types/behandelplan';
|
||||
import { createDefaultLifeDomainScores, type LifeDomainScore } from '@/lib/types/leefgebieden';
|
||||
|
||||
// Input validation schema
|
||||
const GenerateInputSchema = z.object({
|
||||
patientId: z.string().uuid('Patient ID moet een geldige UUID zijn'),
|
||||
intakeId: z.string().uuid('Intake ID moet een geldige UUID zijn'),
|
||||
conditionId: z.string().uuid().optional(),
|
||||
extraInstructions: z.string().max(500).optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Map severity_code to Severity type
|
||||
*/
|
||||
function mapSeverity(severityCode: string | null | undefined): Severity {
|
||||
if (!severityCode) return 'middel';
|
||||
|
||||
const code = severityCode.toLowerCase();
|
||||
if (code.includes('mild') || code.includes('laag') || code.includes('light')) {
|
||||
return 'laag';
|
||||
}
|
||||
if (code.includes('severe') || code.includes('hoog') || code.includes('ernstig')) {
|
||||
return 'hoog';
|
||||
}
|
||||
return 'middel';
|
||||
}
|
||||
|
||||
/**
|
||||
* Load context from database
|
||||
*/
|
||||
async function loadPlanContext(
|
||||
supabase: Awaited<ReturnType<typeof createClient>>,
|
||||
patientId: string,
|
||||
intakeId: string,
|
||||
conditionId?: string
|
||||
): Promise<PlanContext> {
|
||||
// 1. Load intake
|
||||
const { data: intake, error: intakeError } = await supabase
|
||||
.from('intakes')
|
||||
.select('*')
|
||||
.eq('id', intakeId)
|
||||
.eq('patient_id', patientId)
|
||||
.single();
|
||||
|
||||
if (intakeError || !intake) {
|
||||
throw new Error(`Intake niet gevonden: ${intakeError?.message || 'niet gevonden'}`);
|
||||
}
|
||||
|
||||
// 2. Load condition (latest for patient, or specific one)
|
||||
let conditionQuery = supabase
|
||||
.from('conditions')
|
||||
.select('*')
|
||||
.eq('patient_id', patientId);
|
||||
|
||||
if (conditionId) {
|
||||
conditionQuery = conditionQuery.eq('id', conditionId);
|
||||
} else {
|
||||
conditionQuery = conditionQuery.order('recorded_date', { ascending: false }).limit(1);
|
||||
}
|
||||
|
||||
const { data: conditions } = await conditionQuery;
|
||||
const condition = conditions?.[0];
|
||||
|
||||
// 3. Build intake notes from available data
|
||||
const intakeNotes = buildIntakeNotes(intake);
|
||||
|
||||
// 4. Get life domains (from intake or default)
|
||||
const lifeDomains: LifeDomainScore[] =
|
||||
(intake.life_domains as LifeDomainScore[] | null) ||
|
||||
createDefaultLifeDomainScores();
|
||||
|
||||
// 5. Build context
|
||||
return {
|
||||
patientId,
|
||||
intakeNotes,
|
||||
dsmCategory: condition?.category || condition?.code_display || 'overig',
|
||||
severity: mapSeverity(condition?.severity_code),
|
||||
lifeDomains,
|
||||
extraInstructions: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build intake notes from intake data
|
||||
*/
|
||||
function buildIntakeNotes(intake: Record<string, unknown>): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
if (intake.notes) {
|
||||
parts.push(String(intake.notes));
|
||||
}
|
||||
|
||||
if (intake.treatment_advice) {
|
||||
const advice = intake.treatment_advice as Record<string, unknown>;
|
||||
if (advice.content) {
|
||||
parts.push(`Behandeladvies: ${String(advice.content)}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (intake.kindcheck_data) {
|
||||
const kindcheck = intake.kindcheck_data as Record<string, unknown>;
|
||||
if (kindcheck.observations) {
|
||||
parts.push(`Observaties: ${String(kindcheck.observations)}`);
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join('\n\n') || 'Geen intake notities beschikbaar.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Call Claude API
|
||||
*/
|
||||
async function callClaudeAPI(context: PlanContext): Promise<GeneratedPlan> {
|
||||
const apiKey = process.env.ANTHROPIC_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error('ANTHROPIC_API_KEY ontbreekt in environment');
|
||||
}
|
||||
|
||||
const userPrompt = buildUserPrompt(context);
|
||||
|
||||
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': apiKey,
|
||||
'anthropic-version': '2023-06-01',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'claude-sonnet-4-20250514',
|
||||
max_tokens: 4096,
|
||||
temperature: 0.3,
|
||||
system: BEHANDELPLAN_SYSTEM_PROMPT,
|
||||
messages: [{ role: 'user', content: userPrompt }],
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.text();
|
||||
console.error('Claude API error:', errorBody);
|
||||
throw new Error(`Claude API fout: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const rawText = data?.content?.[0]?.text;
|
||||
|
||||
if (!rawText) {
|
||||
throw new Error('Geen response van Claude API');
|
||||
}
|
||||
|
||||
// Parse JSON from response (handle potential markdown code blocks)
|
||||
let jsonText = rawText.trim();
|
||||
if (jsonText.startsWith('```json')) {
|
||||
jsonText = jsonText.slice(7);
|
||||
}
|
||||
if (jsonText.startsWith('```')) {
|
||||
jsonText = jsonText.slice(3);
|
||||
}
|
||||
if (jsonText.endsWith('```')) {
|
||||
jsonText = jsonText.slice(0, -3);
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(jsonText.trim());
|
||||
|
||||
// Validate with Zod schema
|
||||
const validated = GeneratedPlanSchema.parse(parsed);
|
||||
|
||||
return validated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log AI event to database
|
||||
*/
|
||||
async function logAIEvent(
|
||||
supabase: Awaited<ReturnType<typeof createClient>>,
|
||||
kind: string,
|
||||
patientId: string,
|
||||
input: Record<string, unknown>,
|
||||
output: Record<string, unknown>,
|
||||
durationMs: number
|
||||
) {
|
||||
try {
|
||||
await supabase.from('ai_events').insert({
|
||||
kind,
|
||||
patient_id: patientId,
|
||||
input_data: input,
|
||||
output_data: output,
|
||||
duration_ms: durationMs,
|
||||
});
|
||||
} catch (error) {
|
||||
// Log but don't fail the request
|
||||
console.error('Failed to log AI event:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/behandelplan/generate
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
// Validate input
|
||||
const result = GenerateInputSchema.safeParse(body);
|
||||
if (!result.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Validatiefout',
|
||||
details: result.error.issues.map((e) => ({
|
||||
field: e.path.join('.'),
|
||||
message: e.message,
|
||||
})),
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { patientId, intakeId, conditionId, extraInstructions } = result.data;
|
||||
const supabase = await createClient();
|
||||
|
||||
// Load context from database
|
||||
const context = await loadPlanContext(supabase, patientId, intakeId, conditionId);
|
||||
|
||||
// Add extra instructions if provided
|
||||
if (extraInstructions) {
|
||||
context.extraInstructions = extraInstructions;
|
||||
}
|
||||
|
||||
// Validate context
|
||||
const validation = validatePlanContext(context);
|
||||
if (!validation.valid) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Onvoldoende context voor behandelplan generatie',
|
||||
details: validation.errors,
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Call Claude API
|
||||
const plan = await callClaudeAPI(context);
|
||||
|
||||
const durationMs = Date.now() - startTime;
|
||||
|
||||
// Log AI event
|
||||
await logAIEvent(
|
||||
supabase,
|
||||
'behandelplan_generate',
|
||||
patientId,
|
||||
{ intakeId, conditionId, extraInstructions },
|
||||
{ goalCount: plan.doelen.length, interventionCount: plan.interventies.length },
|
||||
durationMs
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
plan,
|
||||
meta: {
|
||||
generatedAt: new Date().toISOString(),
|
||||
durationMs,
|
||||
context: {
|
||||
dsmCategory: context.dsmCategory,
|
||||
severity: context.severity,
|
||||
highPriorityDomains: context.lifeDomains
|
||||
.filter((d) => d.priority === 'hoog')
|
||||
.map((d) => d.domain),
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error generating behandelplan:', error);
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : 'Onbekende fout';
|
||||
|
||||
// Check for specific error types
|
||||
if (errorMessage.includes('Intake niet gevonden')) {
|
||||
return NextResponse.json(
|
||||
{ error: errorMessage },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
if (errorMessage.includes('ANTHROPIC_API_KEY')) {
|
||||
return NextResponse.json(
|
||||
{ error: 'AI service niet geconfigureerd' },
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
|
||||
if (errorMessage.includes('Claude API')) {
|
||||
return NextResponse.json(
|
||||
{ error: 'AI service tijdelijk niet beschikbaar', details: errorMessage },
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Fout bij genereren behandelplan',
|
||||
details: process.env.NODE_ENV === 'development' ? errorMessage : undefined,
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -107,9 +107,9 @@ Deze module is onderdeel van de AI Speedrun LinkedIn Serie. Het prototype demons
|
||||
|
||||
| Epic ID | Titel | Doel | Status | Stories | Geschat |
|
||||
|---------|-------|------|--------|---------|---------|
|
||||
| E0 | Foundation | Types, database schema | ⏳ To Do | 3 | 2-3 uur |
|
||||
| E1 | Leefgebieden | Intake formulier + score weergave | ⏳ To Do | 3 | 3-4 uur |
|
||||
| E2 | AI Generatie | Claude API endpoint + prompts | ⏳ To Do | 3 | 3-4 uur |
|
||||
| E0 | Foundation | Types, database schema | ✅ Done | 3 | 2-3 uur |
|
||||
| E1 | Leefgebieden | Intake formulier + score weergave | ✅ Done | 3 | 3-4 uur |
|
||||
| E2 | AI Generatie | Claude API endpoint + prompts | ✅ Done | 3 | 3-4 uur |
|
||||
| E3 | Behandelplan UI | Pagina + componenten | ⏳ To Do | 5 | 6-8 uur |
|
||||
| E4 | Stretch | Micro-regeneratie, radar chart | ⏳ Optioneel | 3 | 3-5 uur |
|
||||
|
||||
@@ -126,9 +126,9 @@ Deze module is onderdeel van de AI Speedrun LinkedIn Serie. Het prototype demons
|
||||
|
||||
| Story ID | Beschrijving | Acceptatiecriteria | Status | Afhankelijkheden | SP |
|
||||
|----------|--------------|---------------------|--------|------------------|----|
|
||||
| E0.S1 | Leefgebieden types maken | `lib/types/leefgebieden.ts` met LifeDomain, LifeDomainScore types | ⏳ | — | 1 |
|
||||
| E0.S2 | Behandelplan types maken | `lib/types/behandelplan.ts` met SmartGoal, Intervention, GeneratedPlan types + Zod schemas | ⏳ | E0.S1 | 2 |
|
||||
| E0.S3 | Database migratie | `care_plans` uitgebreid met version, behandelstructuur, evaluatiemomenten; `intakes` met life_domains | ⏳ | E0.S2 | 2 |
|
||||
| E0.S1 | Leefgebieden types maken | `lib/types/leefgebieden.ts` met LifeDomain, LifeDomainScore types | ✅ | — | 1 |
|
||||
| E0.S2 | Behandelplan types maken | `lib/types/behandelplan.ts` met SmartGoal, Intervention, GeneratedPlan types + Zod schemas | ✅ | E0.S1 | 2 |
|
||||
| E0.S3 | Database migratie | `care_plans` uitgebreid met version, behandelstructuur, evaluatiemomenten; `intakes` met life_domains | ✅ | E0.S2 | 2 |
|
||||
|
||||
**Technical Notes:**
|
||||
```typescript
|
||||
@@ -146,10 +146,10 @@ export interface LifeDomainScore {
|
||||
```
|
||||
|
||||
**Deliverables:**
|
||||
- [ ] `lib/types/leefgebieden.ts`
|
||||
- [ ] `lib/types/behandelplan.ts`
|
||||
- [ ] Database migratie via Supabase MCP (cloud-only, geen lokale instantie)
|
||||
- [ ] Gegenereerde database types via `mcp__supabase__generate_typescript_types`
|
||||
- [x] `lib/types/leefgebieden.ts`
|
||||
- [x] `lib/types/behandelplan.ts`
|
||||
- [x] Database migratie via Supabase MCP (cloud-only, geen lokale instantie)
|
||||
- [x] Gegenereerde database types via `mcp__supabase__generate_typescript_types`
|
||||
|
||||
---
|
||||
|
||||
@@ -159,9 +159,9 @@ export interface LifeDomainScore {
|
||||
|
||||
| Story ID | Beschrijving | Acceptatiecriteria | Status | Afhankelijkheden | SP |
|
||||
|----------|--------------|---------------------|--------|------------------|----|
|
||||
| E1.S1 | Leefgebieden formulier | 7 sliders (1-5), toelichting velden, prioriteit dropdowns | ⏳ | E0.S3 | 3 |
|
||||
| E1.S2 | Leefgebieden scores weergave | 7 progress bars met kleuren, baseline vs current indicator | ⏳ | E0.S1 | 2 |
|
||||
| E1.S3 | Leefgebieden badge component | Gekleurde tag per domein met emoji | ⏳ | E0.S1 | 1 |
|
||||
| E1.S1 | Leefgebieden formulier | 7 sliders (1-5), toelichting velden, prioriteit dropdowns | ✅ | E0.S3 | 3 |
|
||||
| E1.S2 | Leefgebieden scores weergave | 7 progress bars met kleuren, baseline vs current indicator | ✅ | E0.S1 | 2 |
|
||||
| E1.S3 | Leefgebieden badge component | Gekleurde tag per domein met emoji | ✅ | E0.S1 | 1 |
|
||||
|
||||
**Technical Notes:**
|
||||
```typescript
|
||||
@@ -178,9 +178,10 @@ const DOMAIN_COLORS = {
|
||||
```
|
||||
|
||||
**Deliverables:**
|
||||
- [ ] `components/behandelplan/leefgebieden-form.tsx`
|
||||
- [ ] `components/behandelplan/leefgebieden-scores.tsx`
|
||||
- [ ] `components/behandelplan/leefgebieden-badge.tsx`
|
||||
- [x] `components/behandelplan/leefgebieden-form.tsx`
|
||||
- [x] `components/behandelplan/leefgebieden-scores.tsx`
|
||||
- [x] `components/behandelplan/leefgebieden-badge.tsx`
|
||||
- [x] `components/behandelplan/index.ts` (exports)
|
||||
|
||||
---
|
||||
|
||||
@@ -190,9 +191,9 @@ const DOMAIN_COLORS = {
|
||||
|
||||
| Story ID | Beschrijving | Acceptatiecriteria | Status | Afhankelijkheden | SP |
|
||||
|----------|--------------|---------------------|--------|------------------|----|
|
||||
| E2.S1 | Prompt engineering | System prompt + user prompt templates, evidence-based mapping | ⏳ | E0.S2 | 2 |
|
||||
| E2.S2 | Generate endpoint | `POST /api/behandelplan/generate` retourneert JSON, < 8 sec response | ⏳ | E2.S1 | 3 |
|
||||
| E2.S3 | AI event logging | Calls loggen naar `ai_events` tabel | ⏳ | E2.S2 | 1 |
|
||||
| E2.S1 | Prompt engineering | System prompt + user prompt templates, evidence-based mapping | ✅ | E0.S2 | 2 |
|
||||
| E2.S2 | Generate endpoint | `POST /api/behandelplan/generate` retourneert JSON, < 8 sec response | ✅ | E2.S1 | 3 |
|
||||
| E2.S3 | AI event logging | Calls loggen naar `ai_events` tabel | ✅ | E2.S2 | 1 |
|
||||
|
||||
**Technical Notes:**
|
||||
```typescript
|
||||
@@ -211,9 +212,9 @@ const settings = {
|
||||
```
|
||||
|
||||
**Deliverables:**
|
||||
- [ ] `lib/ai/behandelplan-prompt.ts`
|
||||
- [ ] `lib/ai/intervention-mapping.ts`
|
||||
- [ ] `app/api/behandelplan/generate/route.ts`
|
||||
- [x] `lib/ai/behandelplan-prompt.ts`
|
||||
- [x] `lib/ai/intervention-mapping.ts`
|
||||
- [x] `app/api/behandelplan/generate/route.ts`
|
||||
|
||||
---
|
||||
|
||||
@@ -436,3 +437,4 @@ Optioneel (indien tijd):
|
||||
| Versie | Datum | Auteur | Wijziging |
|
||||
|--------|-------|--------|-----------|
|
||||
| v1.0 | 03-12-2024 | Colin Lit | Initiële versie |
|
||||
| v1.1 | 04-12-2024 | Colin Lit | Epic 0, 1, 2 afgerond - status bijgewerkt |
|
||||
|
||||
3
docs/templates/bouwplan_template.md
vendored
3
docs/templates/bouwplan_template.md
vendored
@@ -156,6 +156,7 @@ export const loadData = async () => {
|
||||
| E4 | Testing & Deploy | QA, demo prep en deployment | ⏳ To Do | 3 | |
|
||||
|
||||
**Belangrijk:** Voer niet in 1x het volledige plan uit. Bouw per epic en per story.
|
||||
**Belangrijk:** Het installeren van dependencies en libraries, of het uitvoeren van migraties van tabellen, moet altijd eerst aan Colin worden gemeld of gevraagd. Colin kan deze zelf eventueel uitvoeren.
|
||||
|
||||
---
|
||||
|
||||
@@ -174,6 +175,7 @@ export const loadData = async () => {
|
||||
| E0.S4 | Environment variables | `.env.local` + Vercel vars geconfigureerd | ⏳ | E0.S3 | 1 |
|
||||
|
||||
**Technical Notes:**
|
||||
- ⚠️ **Belangrijk:** Dependencies installeren moet eerst aan Colin worden gemeld of gevraagd. Colin kan deze zelf eventueel uitvoeren.
|
||||
- Gebruik `pnpm` voor snellere installs
|
||||
- `.env.example` committen voor team onboarding
|
||||
|
||||
@@ -189,6 +191,7 @@ export const loadData = async () => {
|
||||
| E1.S3 | Demo-data seeden | 3+ testcliënten met complete intake data | ⏳ | E1.S1 | 2 |
|
||||
|
||||
**Technical Notes:**
|
||||
- ⚠️ **Belangrijk:** Migraties van tabellen moeten eerst aan Colin worden gemeld of gevraagd. Colin kan deze zelf eventueel uitvoeren.
|
||||
- Collections: `clients`, `intakes`, `plans`, `ai_events`
|
||||
- Demo user heeft `all access` voor development
|
||||
- Seed script: `npm run seed`
|
||||
|
||||
221
lib/ai/behandelplan-prompt.ts
Normal file
221
lib/ai/behandelplan-prompt.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* Behandelplan AI Prompts
|
||||
*
|
||||
* System prompt en user prompt templates voor AI-generatie van behandelplannen
|
||||
*/
|
||||
|
||||
import { type LifeDomainScore, LIFE_DOMAIN_META } from '@/lib/types/leefgebieden';
|
||||
import { type Severity, getInterventionsForCategory, getRecommendedSessionCount, getRecommendedFrequency, getRecommendedFormat } from './intervention-mapping';
|
||||
|
||||
/**
|
||||
* Context voor behandelplan generatie
|
||||
*/
|
||||
export interface PlanContext {
|
||||
patientId: string;
|
||||
intakeNotes: string;
|
||||
dsmCategory: string;
|
||||
severity: Severity;
|
||||
lifeDomains: LifeDomainScore[];
|
||||
extraInstructions?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* System prompt voor Claude
|
||||
* Instructies voor het genereren van een behandelplan
|
||||
*/
|
||||
export const BEHANDELPLAN_SYSTEM_PROMPT = `Je bent een ervaren GGZ-behandelaar die behandelplannen opstelt voor cliënten in de ambulante GGZ.
|
||||
Je maakt SMART doelen die recovery-gericht en evidence-based zijn.
|
||||
|
||||
INSTRUCTIES:
|
||||
1. Genereer 2-4 SMART doelen gebaseerd op de intake en diagnose
|
||||
2. Focus op leefgebieden met prioriteit "hoog"
|
||||
3. Verdeel doelen over minimaal 2 verschillende leefgebieden
|
||||
4. Maak concrete, meetbare doelen (geen vage termen zoals "beter voelen")
|
||||
5. Genereer voor elk doel een B1-taal versie (cliënt-vriendelijk, simpele woorden)
|
||||
6. Kies evidence-based interventies passend bij de DSM-categorie
|
||||
7. Plan sessies gebaseerd op severity niveau
|
||||
8. Voeg een veiligheidsplan toe ALLEEN bij severity "hoog"
|
||||
|
||||
SMART CRITERIA:
|
||||
- Specifiek: Wat precies wil de cliënt bereiken?
|
||||
- Meetbaar: Hoe meten we vooruitgang? (bijv. "3x per week", "score van 3 naar 4")
|
||||
- Acceptabel: Past bij wensen en mogelijkheden cliënt
|
||||
- Realistisch: Haalbaar binnen de behandelperiode
|
||||
- Tijdgebonden: Binnen hoeveel weken?
|
||||
|
||||
B1-TAAL RICHTLIJNEN (cliënt-versie):
|
||||
- Korte zinnen (max 15 woorden)
|
||||
- Alledaagse woorden (geen jargon)
|
||||
- Actieve zinnen ("Ik ga..." niet "Er zal worden...")
|
||||
- Directe aanspreking ("jij" of "je")
|
||||
|
||||
OUTPUT FORMAT:
|
||||
Retourneer ALLEEN valide JSON volgens dit exacte schema (geen extra tekst):
|
||||
{
|
||||
"behandelstructuur": {
|
||||
"duur": "string (bijv. '8 weken')",
|
||||
"frequentie": "string (bijv. 'Wekelijks')",
|
||||
"aantalSessies": number,
|
||||
"vorm": "string (bijv. 'Individueel')"
|
||||
},
|
||||
"doelen": [
|
||||
{
|
||||
"id": "string (uuid)",
|
||||
"title": "string (max 200 tekens, korte beschrijving)",
|
||||
"description": "string (SMART uitwerking, 2-3 zinnen)",
|
||||
"clientVersion": "string (B1-taal versie voor cliënt)",
|
||||
"lifeDomain": "dlv|wonen|werk|sociaal|vrijetijd|financien|gezondheid",
|
||||
"priority": "hoog|middel|laag",
|
||||
"measurability": "string (hoe meten we vooruitgang?)",
|
||||
"timelineWeeks": number,
|
||||
"status": "niet_gestart",
|
||||
"progress": 0
|
||||
}
|
||||
],
|
||||
"interventies": [
|
||||
{
|
||||
"id": "string (uuid)",
|
||||
"name": "string (bijv. 'CGT', 'EMDR')",
|
||||
"description": "string (uitleg interventie)",
|
||||
"rationale": "string (waarom past dit bij deze cliënt?)",
|
||||
"linkedGoalIds": ["string (id van gekoppeld doel)"]
|
||||
}
|
||||
],
|
||||
"sessiePlanning": [
|
||||
{
|
||||
"id": "string (uuid)",
|
||||
"nummer": number,
|
||||
"focus": "string (waar gaat de sessie over?)",
|
||||
"status": "gepland",
|
||||
"gekoppeldeDoelIds": ["string"]
|
||||
}
|
||||
],
|
||||
"evaluatiemomenten": [
|
||||
{
|
||||
"id": "string (uuid)",
|
||||
"type": "tussentijds|eind",
|
||||
"weekNumber": number,
|
||||
"plannedDate": "string (ISO date, mag leeg)",
|
||||
"status": "gepland"
|
||||
}
|
||||
],
|
||||
"veiligheidsplan": null of {
|
||||
"waarschuwingssignalen": ["string (3-5 items)"],
|
||||
"copingStrategieen": ["string (3-5 items)"],
|
||||
"contacten": [
|
||||
{
|
||||
"naam": "string",
|
||||
"rol": "string",
|
||||
"telefoon": "string"
|
||||
}
|
||||
],
|
||||
"restricties": ["string (optioneel)"]
|
||||
}
|
||||
}`;
|
||||
|
||||
/**
|
||||
* Bouw user prompt met context
|
||||
*/
|
||||
export function buildUserPrompt(context: PlanContext): string {
|
||||
// Get high priority domains
|
||||
const highPriorityDomains = context.lifeDomains.filter(d => d.priority === 'hoog');
|
||||
const lowScoreDomains = context.lifeDomains.filter(d => d.baseline <= 2);
|
||||
|
||||
// Get intervention suggestions
|
||||
const suggestedInterventions = getInterventionsForCategory(context.dsmCategory, context.severity);
|
||||
const recommendedSessions = getRecommendedSessionCount(context.dsmCategory, context.severity);
|
||||
const recommendedFrequency = getRecommendedFrequency(context.severity);
|
||||
const recommendedFormat = getRecommendedFormat(context.severity);
|
||||
|
||||
// Build life domains section
|
||||
const lifeDomainLines = context.lifeDomains.map(d => {
|
||||
const meta = LIFE_DOMAIN_META[d.domain];
|
||||
const priorityMarker = d.priority === 'hoog' ? ' [PRIORITEIT]' : '';
|
||||
const lowScoreMarker = d.baseline <= 2 ? ' [LAAG]' : '';
|
||||
return `- ${meta.label} (${d.domain}): ${d.baseline}/5 → doel ${d.target}/5${priorityMarker}${lowScoreMarker}${d.notes ? ` (${d.notes})` : ''}`;
|
||||
}).join('\n');
|
||||
|
||||
// Build intervention suggestions
|
||||
const interventionLines = suggestedInterventions.slice(0, 3).map(i =>
|
||||
`- ${i.name}: ${i.description} (~${i.recommendedSessions} sessies)`
|
||||
).join('\n');
|
||||
|
||||
return `CLIËNT CONTEXT:
|
||||
=================
|
||||
Intake notities:
|
||||
${context.intakeNotes}
|
||||
|
||||
DSM-categorie: ${context.dsmCategory}
|
||||
Severity: ${context.severity}
|
||||
|
||||
LEEFGEBIEDEN ASSESSMENT:
|
||||
========================
|
||||
${lifeDomainLines}
|
||||
|
||||
${highPriorityDomains.length > 0 ? `
|
||||
FOCUS GEBIEDEN (hoge prioriteit):
|
||||
${highPriorityDomains.map(d => `- ${LIFE_DOMAIN_META[d.domain].label}`).join('\n')}
|
||||
` : ''}
|
||||
${lowScoreDomains.length > 0 ? `
|
||||
AANDACHTSPUNTEN (lage scores):
|
||||
${lowScoreDomains.map(d => `- ${LIFE_DOMAIN_META[d.domain].label} (score: ${d.baseline}/5)`).join('\n')}
|
||||
` : ''}
|
||||
|
||||
AANBEVOLEN INTERVENTIES (evidence-based):
|
||||
=========================================
|
||||
${interventionLines}
|
||||
|
||||
AANBEVOLEN BEHANDELSTRUCTUUR:
|
||||
=============================
|
||||
- Aantal sessies: ~${recommendedSessions}
|
||||
- Frequentie: ${recommendedFrequency}
|
||||
- Vorm: ${recommendedFormat}
|
||||
|
||||
${context.extraInstructions ? `
|
||||
EXTRA INSTRUCTIES VAN BEHANDELAAR:
|
||||
==================================
|
||||
${context.extraInstructions}
|
||||
` : ''}
|
||||
|
||||
Genereer nu een compleet behandelplan in JSON formaat.
|
||||
Zorg dat elk doel:
|
||||
1. Gekoppeld is aan een leefgebied met hoge prioriteit of lage score
|
||||
2. Een meetbare indicator heeft
|
||||
3. Een B1-taal versie heeft voor de cliënt
|
||||
4. Realistisch is voor de behandelperiode
|
||||
|
||||
${context.severity === 'hoog' ? 'BELANGRIJK: Voeg ook een veiligheidsplan toe met waarschuwingssignalen, copingstrategieën en contactpersonen.' : ''}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Valideer of de context voldoende informatie bevat
|
||||
*/
|
||||
export function validatePlanContext(context: PlanContext): { valid: boolean; errors: string[] } {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!context.intakeNotes || context.intakeNotes.length < 50) {
|
||||
errors.push('Intake notities zijn te kort (minimaal 50 tekens)');
|
||||
}
|
||||
|
||||
if (!context.dsmCategory) {
|
||||
errors.push('DSM-categorie ontbreekt');
|
||||
}
|
||||
|
||||
if (!context.severity) {
|
||||
errors.push('Severity niveau ontbreekt');
|
||||
}
|
||||
|
||||
if (!context.lifeDomains || context.lifeDomains.length !== 7) {
|
||||
errors.push('Leefgebieden assessment is incompleet (7 domeinen vereist)');
|
||||
}
|
||||
|
||||
const hasHighPriority = context.lifeDomains?.some(d => d.priority === 'hoog');
|
||||
if (!hasHighPriority) {
|
||||
errors.push('Minimaal 1 leefgebied moet prioriteit "hoog" hebben');
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
232
lib/ai/intervention-mapping.ts
Normal file
232
lib/ai/intervention-mapping.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
/**
|
||||
* Evidence-Based Intervention Mapping
|
||||
*
|
||||
* Mapping van DSM-categorieën naar evidence-based interventies
|
||||
* met sessie-aantallen per severity level
|
||||
*/
|
||||
|
||||
export interface InterventionSuggestion {
|
||||
name: string;
|
||||
description: string;
|
||||
sessions: {
|
||||
laag: number;
|
||||
middel: number;
|
||||
hoog: number;
|
||||
};
|
||||
}
|
||||
|
||||
export type Severity = 'laag' | 'middel' | 'hoog';
|
||||
|
||||
/**
|
||||
* DSM categorieën naar interventie mapping
|
||||
* Gebaseerd op richtlijnen GGZ Standaarden
|
||||
*/
|
||||
export const INTERVENTION_MAPPING: Record<string, InterventionSuggestion[]> = {
|
||||
angststoornissen: [
|
||||
{
|
||||
name: 'CGT',
|
||||
description: 'Cognitieve Gedragstherapie gericht op angstreductie door blootstelling en cognitieve herstructurering',
|
||||
sessions: { laag: 8, middel: 10, hoog: 14 },
|
||||
},
|
||||
{
|
||||
name: 'Exposure therapie',
|
||||
description: 'Systematische blootstelling aan angstopwekkende situaties met responspreventie',
|
||||
sessions: { laag: 6, middel: 8, hoog: 12 },
|
||||
},
|
||||
{
|
||||
name: 'ACT',
|
||||
description: 'Acceptance and Commitment Therapy voor psychologische flexibiliteit',
|
||||
sessions: { laag: 8, middel: 10, hoog: 12 },
|
||||
},
|
||||
],
|
||||
stemmingsklachten: [
|
||||
{
|
||||
name: 'CGT',
|
||||
description: 'Cognitieve Gedragstherapie gericht op negatieve denkpatronen en gedragsactivatie',
|
||||
sessions: { laag: 8, middel: 10, hoog: 14 },
|
||||
},
|
||||
{
|
||||
name: 'IPT',
|
||||
description: 'Interpersoonlijke Therapie gericht op relationele patronen en sociale steun',
|
||||
sessions: { laag: 8, middel: 12, hoog: 16 },
|
||||
},
|
||||
{
|
||||
name: 'Gedragsactivatie',
|
||||
description: 'Gestructureerde toename van plezierige en betekenisvolle activiteiten',
|
||||
sessions: { laag: 6, middel: 8, hoog: 10 },
|
||||
},
|
||||
],
|
||||
trauma_ptss: [
|
||||
{
|
||||
name: 'EMDR',
|
||||
description: 'Eye Movement Desensitization and Reprocessing voor traumaverwerking',
|
||||
sessions: { laag: 6, middel: 10, hoog: 16 },
|
||||
},
|
||||
{
|
||||
name: 'Narratieve therapie',
|
||||
description: 'Verwerking door het opbouwen van een coherent traumaverhaal',
|
||||
sessions: { laag: 8, middel: 12, hoog: 16 },
|
||||
},
|
||||
{
|
||||
name: 'CGT trauma-focus',
|
||||
description: 'Trauma-gefocuste CGT met exposure en cognitieve verwerking',
|
||||
sessions: { laag: 8, middel: 12, hoog: 16 },
|
||||
},
|
||||
],
|
||||
persoonlijkheid: [
|
||||
{
|
||||
name: 'Schematherapie',
|
||||
description: 'Langdurige therapie gericht op disfunctionele schemas en copingstijlen',
|
||||
sessions: { laag: 16, middel: 24, hoog: 40 },
|
||||
},
|
||||
{
|
||||
name: 'MBT',
|
||||
description: 'Mentalization-Based Treatment voor emotieregulatie en relaties',
|
||||
sessions: { laag: 16, middel: 24, hoog: 40 },
|
||||
},
|
||||
{
|
||||
name: 'DGT',
|
||||
description: 'Dialectische Gedragstherapie voor emotieregulatie en crisisvaardigheden',
|
||||
sessions: { laag: 16, middel: 24, hoog: 40 },
|
||||
},
|
||||
],
|
||||
verslaving: [
|
||||
{
|
||||
name: 'Motiverende gespreksvoering',
|
||||
description: 'Versterken van intrinsieke motivatie voor gedragsverandering',
|
||||
sessions: { laag: 4, middel: 6, hoog: 8 },
|
||||
},
|
||||
{
|
||||
name: 'CGT verslaving',
|
||||
description: 'Cognitieve Gedragstherapie gericht op craving en terugvalpreventie',
|
||||
sessions: { laag: 8, middel: 12, hoog: 16 },
|
||||
},
|
||||
{
|
||||
name: 'Terugvalpreventie',
|
||||
description: 'Identificeren en managen van risicosituaties en triggers',
|
||||
sessions: { laag: 6, middel: 8, hoog: 12 },
|
||||
},
|
||||
],
|
||||
adhd: [
|
||||
{
|
||||
name: 'Psycho-educatie',
|
||||
description: 'Educatie over ADHD en praktische copingstrategieën',
|
||||
sessions: { laag: 4, middel: 6, hoog: 8 },
|
||||
},
|
||||
{
|
||||
name: 'Coaching/planning',
|
||||
description: 'Structuur en planningsvaardigheden ontwikkelen',
|
||||
sessions: { laag: 6, middel: 8, hoog: 12 },
|
||||
},
|
||||
{
|
||||
name: 'CGT ADHD',
|
||||
description: 'Gedragstherapie gericht op impulscontrole en executieve functies',
|
||||
sessions: { laag: 8, middel: 10, hoog: 14 },
|
||||
},
|
||||
],
|
||||
autisme: [
|
||||
{
|
||||
name: 'Psycho-educatie',
|
||||
description: 'Educatie over autisme en zelfacceptatie',
|
||||
sessions: { laag: 4, middel: 6, hoog: 8 },
|
||||
},
|
||||
{
|
||||
name: 'Sociale vaardigheden',
|
||||
description: 'Training in sociale interactie en communicatie',
|
||||
sessions: { laag: 8, middel: 12, hoog: 16 },
|
||||
},
|
||||
{
|
||||
name: 'Stressmanagement',
|
||||
description: 'Omgaan met prikkels en sensorische overbelasting',
|
||||
sessions: { laag: 6, middel: 8, hoog: 12 },
|
||||
},
|
||||
],
|
||||
overig: [
|
||||
{
|
||||
name: 'Ondersteunende gesprekken',
|
||||
description: 'Steunende begeleiding en reflectie',
|
||||
sessions: { laag: 4, middel: 6, hoog: 8 },
|
||||
},
|
||||
{
|
||||
name: 'Psycho-educatie',
|
||||
description: 'Educatie over klachten en copingstrategieën',
|
||||
sessions: { laag: 4, middel: 6, hoog: 8 },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Haal interventies op voor een DSM categorie
|
||||
*/
|
||||
export function getInterventionsForCategory(
|
||||
dsmCategory: string,
|
||||
severity: Severity
|
||||
): { name: string; description: string; recommendedSessions: number }[] {
|
||||
// Normalize category
|
||||
const normalizedCategory = dsmCategory
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z]/g, '_')
|
||||
.replace(/_+/g, '_');
|
||||
|
||||
// Find matching category
|
||||
const interventions =
|
||||
INTERVENTION_MAPPING[normalizedCategory] ||
|
||||
INTERVENTION_MAPPING['overig'];
|
||||
|
||||
return interventions.map((intervention) => ({
|
||||
name: intervention.name,
|
||||
description: intervention.description,
|
||||
recommendedSessions: intervention.sessions[severity],
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Bereken aanbevolen aantal sessies
|
||||
*/
|
||||
export function getRecommendedSessionCount(
|
||||
dsmCategory: string,
|
||||
severity: Severity
|
||||
): number {
|
||||
const interventions = getInterventionsForCategory(dsmCategory, severity);
|
||||
if (interventions.length === 0) return 8;
|
||||
|
||||
// Neem gemiddelde van eerste 2 interventies
|
||||
const topInterventions = interventions.slice(0, 2);
|
||||
const avg =
|
||||
topInterventions.reduce((sum, i) => sum + i.recommendedSessions, 0) /
|
||||
topInterventions.length;
|
||||
|
||||
return Math.round(avg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bepaal behandelvorm op basis van severity
|
||||
*/
|
||||
export function getRecommendedFormat(severity: Severity): string {
|
||||
switch (severity) {
|
||||
case 'laag':
|
||||
return 'Individueel';
|
||||
case 'middel':
|
||||
return 'Individueel';
|
||||
case 'hoog':
|
||||
return 'Individueel + groep (optioneel)';
|
||||
default:
|
||||
return 'Individueel';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bepaal behandelfrequentie op basis van severity
|
||||
*/
|
||||
export function getRecommendedFrequency(severity: Severity): string {
|
||||
switch (severity) {
|
||||
case 'laag':
|
||||
return 'Wekelijks tot tweewekelijks';
|
||||
case 'middel':
|
||||
return 'Wekelijks';
|
||||
case 'hoog':
|
||||
return 'Wekelijks tot 2x per week';
|
||||
default:
|
||||
return 'Wekelijks';
|
||||
}
|
||||
}
|
||||
@@ -655,6 +655,7 @@ export type Database = {
|
||||
end_date: string | null
|
||||
id: string
|
||||
kindcheck_data: Json | null
|
||||
life_domains: Json | null
|
||||
notes: string | null
|
||||
patient_id: string
|
||||
psychologist_id: string | null
|
||||
@@ -670,6 +671,7 @@ export type Database = {
|
||||
end_date?: string | null
|
||||
id?: string
|
||||
kindcheck_data?: Json | null
|
||||
life_domains?: Json | null
|
||||
notes?: string | null
|
||||
patient_id: string
|
||||
psychologist_id?: string | null
|
||||
@@ -685,6 +687,7 @@ export type Database = {
|
||||
end_date?: string | null
|
||||
id?: string
|
||||
kindcheck_data?: Json | null
|
||||
life_domains?: Json | null
|
||||
notes?: string | null
|
||||
patient_id?: string
|
||||
psychologist_id?: string | null
|
||||
|
||||
Reference in New Issue
Block a user