# Cortex Architectuuranalyse — Van Prototype naar Productie > **Type**: Architectuurrapport > **Scope**: Cortex drie-laags AI-systeem + EPD-integratie > **Status**: Analyse & aanbevelingen > **Datum**: 2026-02-14 > **Doelgroep**: Technisch team, AI-systemen voor vervolganalyse --- ## 1. Inleiding ### 1.1 Doel van dit rapport Dit rapport beschrijft het huidige Cortex-systeem (as-is) en doet concrete voorstellen voor een productiewaardige architectuur (to-be). Het is bedoeld als referentiedocument voor verdere analyse en iteratieve verbetering. ### 1.2 Scope - Cortex drie-laags architectuur (Reflex, Orchestrator, Nudge) - Intent-classificatiesysteem - Integratie met EPD-modules (API's, state management, UI-flow) - Beknopte enterprise-eisen (NEN 7510, audit, RBAC) **Buiten scope**: Volledig datamodel, autorisatiemodel, FHIR-integratie. Deze worden als afhankelijkheden benoemd maar niet uitgewerkt. ### 1.3 Ontwerpprincipes De aanbevelingen in dit rapport volgen deze principes: 1. **Single Responsibility** — Elk onderdeel heeft precies één doel 2. **Single Source of Truth** — Geen dubbele definities; één canonieke bron 3. **Graceful Degradation** — Het systeem blijft functioneel als onderdelen falen 4. **Minimale complexiteit** — Geen abstracties voor hypothetische toekomstige eisen 5. **Observability by design** — Elk onderdeel is meetbaar en debugbaar --- ## 2. Concept ### 2.1 Probleemstelling Zorgverleners in de GGZ besteden een significant deel van hun werktijd aan administratie: rapportages schrijven, afspraken beheren, overdrachten voorbereiden, en door dossiers navigeren. Deze handelingen onderbreken het primaire proces — directe patiëntenzorg — en vereisen constante contextwisselingen tussen klinisch denken en systeembediening. Het kernprobleem is niet dat deze taken bestaan, maar dat ze **gestructureerde input vereisen** (formulieren, knoppen, menustructuren) terwijl de zorgverlener in **ongestructureerde taal** denkt en communiceert. De kloof tussen "Jan heeft zijn medicatie geweigerd" en het invullen van het juiste formulier in de juiste categorie voor de juiste patiënt is waar productiviteit verloren gaat. ### 2.2 Visie Cortex is de **intelligente tussenschakel** tussen natuurlijke taal en gestructureerde EPD-acties. Het stelt zorgverleners in staat om in hun eigen woorden te communiceren — gesproken of getypt — en vertaalt dit naar de juiste actie in het juiste systeem, met de juiste context. De kernbelofte: > **Zeg wat je wilt doen, niet hoe het systeem het verwacht.** Cortex is geen chatbot en geen zoekmachine. Het is een **command interface** dat drie dingen doet: 1. **Begrijpen** — Wat bedoelt de gebruiker? (intent + entities) 2. **Uitvoeren** — De juiste actie starten met de juiste voorvulling 3. **Voorstellen** — Proactief vervolgacties suggereren op basis van klinische protocollen ### 2.3 Ontwerpfilosofie #### Snelheid boven volledigheid 70% van de commando's in een EPD is routine: "notitie jan medicatie", "agenda vandaag", "overdracht". Deze moeten in milliseconden verwerkt worden, niet in seconden. AI wordt alleen ingezet wanneer lokale verwerking niet volstaat. Dit is een bewuste keuze: **optimaliseer voor de veelvoorkomende gevallen, investeer AI-capaciteit in de uitzonderingen.** #### Vertrouwen door transparantie Zorgverleners moeten het systeem kunnen vertrouwen. Dat betekent: - Geen automatische opslag zonder bevestiging - Altijd tonen wat het systeem begrepen heeft (prefilled formulier, niet een weggeschreven record) - Bij twijfel: vragen, niet raden - Bevestiging vereisen bij destructieve acties (annuleren, verwijderen) #### Klinisch relevante intelligentie De AI-component is geen generieke assistent. Cortex kent het domein: diensten (nacht/ochtend/middag/avond), rapportagecategorieën (medicatie/ADL/gedrag/incident/observatie), klinische protocollen (wondzorg, medicatie-evaluatie). Deze domeinkennis maakt het verschil tussen een nuttige suggestie en ruis. ### 2.4 Gebruiksmodel ``` ┌──────────────────────────────────────────────────────────────────┐ │ Werkdag van een verpleegkundige │ │ │ │ 07:00 Start dienst │ │ └─ "Wat moet ik weten?" → Overdracht met AI-samenvatting │ │ │ │ 08:30 Medicatieronde │ │ └─ "Jan medicatie gegeven" → Dagnotitie, categorie auto │ │ └─ "Marie weigert medicatie" → Dagnotitie + incident? │ │ │ │ 10:00 Intake nieuw patiënt │ │ └─ "Wat moet ik nog doen?" → Intake checklist │ │ └─ "Ga naar risico" → Navigatie naar risicotaxatie │ │ │ │ 12:00 Afsprakenbeheer │ │ └─ "Afspraken vanmiddag" → Agendaoverzicht │ │ └─ "Verzet Jan naar 15:00" → Afspraak wijzigen │ │ │ │ 14:30 Wondverzorging │ │ └─ "Notitie jan wondverzorging uitgevoerd" → Dagnotitie │ │ └─ 🔔 Nudge: "Wondcontrole inplannen over 3 dagen?" │ │ │ │ 16:45 Einde dienst │ │ └─ "Overdracht" → Overdracht genereren │ └──────────────────────────────────────────────────────────────────┘ ``` --- ## 3. Architectuur — Conceptueel ### 3.1 Het drie-laags model De architectuur is geïnspireerd op hoe het menselijk zenuwstelsel informatie verwerkt: | Laag | Analogie | Functie | Wanneer | |------|----------|---------|---------| | **Layer 1: Reflex** | Ruggenmerg | Directe, snelle respons op herkenbare patronen | Altijd (eerste poging) | | **Layer 2: Orchestrator** | Hersenen | Bewuste analyse van complexe, ambigue input | Bij twijfel of complexiteit | | **Layer 3: Nudge** | Geweten | Proactieve suggestie op basis van professionele kennis | Na afgeronde actie | Dit model heeft twee architecturele voordelen: **1. Kostenefficiëntie**: AI API-calls zijn duur en traag. Door 70%+ lokaal af te handelen worden kosten en latency gedrukt. De AI wordt alleen aangesproken wanneer de waarde evident is. **2. Resilience**: Als de AI-service onbeschikbaar is, blijft Layer 1 volledig functioneel. Het systeem degradeert graceful van "intelligent" naar "snel en betrouwbaar" in plaats van volledig te falen. ### 3.2 De intent als architecturaal primitief Het centrale concept in Cortex is de **intent**: een gestructureerde representatie van wat de gebruiker wilt bereiken. ``` Gebruikersinput: "Notitie jan medicatie gegeven" │ ▼ ┌─────────────────┐ │ Intent │ ├─────────────────┤ │ type: dagnotitie│ │ confidence: 0.95│ │ entities: │ │ patient: jan │ │ category: med │ │ content: ... │ └─────────────────┘ │ ▼ ┌─────────────────┐ │ Actie │ │ Open formulier │ │ met prefill │ └─────────────────┘ ``` Een intent bestaat uit drie delen: 1. **Type** — Wát de gebruiker wil (dagnotitie, zoeken, overdracht, etc.) 2. **Confidence** — Hoe zeker het systeem is van deze interpretatie (0.0–1.0) 3. **Entities** — De relevante parameters (patiëntnaam, categorie, datum, etc.) De confidence bepaalt het systeemgedrag: | Confidence | Gedrag | |------------|--------| | ≥ 0.9 | Direct uitvoeren (formulier openen met prefill) | | 0.7–0.9 | Uitvoeren met bevestigingsvraag | | 0.5–0.7 | Verduidelijkingsvraag stellen | | < 0.5 | "Ik begrijp het niet, kun je het anders zeggen?" | ### 3.3 IntentChain — Meervoudige acties Gebruikers combineren regelmatig acties in één zin: "Annuleer de afspraak van Jan en maak een notitie." Het systeem modelleert dit als een **IntentChain**: een geordende reeks intents die sequentieel worden uitgevoerd. ``` Input: "Annuleer jan en maak notitie griep" │ ▼ ┌─────────────────┐ │ IntentChain │ ├─────────────────┤ │ Action 1: │ │ cancel_appt │ ──▶ Uitvoeren (met bevestiging) │ {patient: jan}│ │ │ │ Action 2: │ │ dagnotitie │ ──▶ Wacht op Action 1, dan uitvoeren │ {patient: jan,│ │ content: ..} │ └─────────────────┘ ``` Multi-intent is architectureel ondersteund maar beperkt tot sequentiële uitvoering. Parallelle uitvoering is bewust niet geïmplementeerd: de gebruiker verwacht een logische volgorde. ### 3.4 Nudge — Protocol-gedreven suggesties De Nudge-laag is conceptueel anders dan Layer 1 en 2. Waar die lagen **reactief** zijn (gebruiker vraagt, systeem handelt), is Layer 3 **proactief**: het systeem stelt iets voor dat de gebruiker niet gevraagd heeft maar dat klinisch relevant is. ``` ┌───────────────────────────────┐ │ Afgeronde actie │ │ dagnotitie: wondverzorging │ └───────────────┬───────────────┘ │ ▼ ┌───────────────────────────────┐ │ Protocol Rule Engine │ │ │ │ Regel: "wondzorg-controle" │ │ Trigger: dagnotitie + │ │ content ~ "wond" │ │ ────────────────────────── │ │ Match! → Suggestie genereren│ └───────────────┬───────────────┘ │ ▼ ┌───────────────────────────────┐ │ Nudge Suggestie │ │ │ │ "Wondcontrole inplannen │ │ over 3 dagen?" │ │ │ │ Protocol: V&VN Wondzorg §4.2│ │ [Accepteren] [Verwerpen] │ └───────────────────────────────┘ ``` De klinische onderbouwing (protocolnaam, paragraafverwijzing, rationale) is essentieel: het maakt de suggestie controleerbaar en vertrouwenswaardig. De zorgverlener beslist altijd zelf. ### 3.5 Anatomie van een Intent — De bouwsteen Een intent is niet alleen een classificatieresultaat. Het is een **end-to-end bouwsteen** die door 8 lagen van het systeem snijdt. Elk nieuw intent raakt al deze lagen; de kwaliteit van het systeem hangt af van de consistentie ertussen. ``` ┌─────────────────────────────────────────────────────────────────┐ │ ANATOMIE VAN EEN INTENT │ │ │ │ ┌─ 1. TYPE DEFINITIE ─────────────────────────────────────┐ │ │ │ CortexIntent union type + ExtractedEntities │ │ │ │ Bron: lib/cortex/types.ts │ │ │ └──────────────────────────────────────────────────────────┘ │ │ ↓ │ │ ┌─ 2. REFLEX PATRONEN ────────────────────────────────────┐ │ │ │ Regex-patronen met gewichten (1.0 = exact, 0.7 = zwak) │ │ │ │ Bron: lib/cortex/reflex-classifier.ts │ │ │ └──────────────────────────────────────────────────────────┘ │ │ ↓ │ │ ┌─ 3. AI PROMPTBESCHRIJVING ──────────────────────────────┐ │ │ │ Intent beschrijving + triggers + entity definitie │ │ │ │ + voorbeelddialoog met verwacht JSON-resultaat │ │ │ │ Bron: app/api/cortex/chat/route.ts (system prompt) │ │ │ └──────────────────────────────────────────────────────────┘ │ │ ↓ │ │ ┌─ 4. VALIDATIESCHEMA ───────────────────────────────────┐ │ │ │ Zod-schema voor intent + entities in AI response │ │ │ │ Bron: lib/cortex/action-parser.ts │ │ │ └──────────────────────────────────────────────────────────┘ │ │ ↓ │ │ ┌─ 5. ACTIE-ROUTING ─────────────────────────────────────┐ │ │ │ Intent → artifact mapping + entity requirements check │ │ │ │ Bepaalt of er een artifact opent of navigatie plaatsvindt│ │ │ │ Bron: lib/cortex/action-parser.ts │ │ │ └──────────────────────────────────────────────────────────┘ │ │ ↓ │ │ ┌─ 6. API ROUTE (optioneel) ─────────────────────────────┐ │ │ │ Server-side data ophalen voor het block │ │ │ │ Bron: app/api/cortex/[domain]/route.ts │ │ │ └──────────────────────────────────────────────────────────┘ │ │ ↓ │ │ ┌─ 7. BLOCK COMPONENT ──────────────────────────────────┐ │ │ │ React component die het intent visualiseert │ │ │ │ Ontvangt prefill data uit entities │ │ │ │ Bron: components/cortex/blocks/[intent]-block.tsx │ │ │ └──────────────────────────────────────────────────────────┘ │ │ ↓ │ │ ┌─ 8. ARTIFACT CONTAINER ────────────────────────────────┐ │ │ │ Registratie in de artifact renderer (import, case, titel)│ │ │ │ Bron: components/cortex/artifacts/artifact-container.tsx │ │ │ └──────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ ``` #### Drie intenttypen Niet elk intent heeft alle 8 lagen nodig. Er zijn drie typen: | Type | Beschrijving | Lagen nodig | Voorbeeld | |------|-------------|-------------|-----------| | **Query Block** | Toont data (read-only) | Alle 8 | `risico_query`, `diagnose_query` | | **Action Block** | Formulier voor data-invoer | Alle 8 | `dagnotitie`, `create_appointment` | | **Navigatie Intent** | Navigeert naar een pagina | 1–5 (geen block/artifact) | `intake_navigeer` | #### Entity-schema per intent Elk intent definieert welke entities het verwacht: | Veld | Beschrijving | Voorbeeld | |------|-------------|-----------| | **required** | Moet aanwezig zijn voor uitvoering | `patientName` bij dagnotitie | | **optional** | Verrijkt het resultaat als aanwezig | `category` bij dagnotitie | | **prefill** | Wordt doorgegeven aan het block als voorvulling | Alle entities | Wanneer required entities ontbreken en confidence < 0.7, stelt het systeem een verduidelijkingsvraag ("Met welke patiënt?"). Wanneer ze aanwezig zijn en confidence >= 0.9, opent het direct het artifact met prefill. #### Block lifecycle Een block doorloopt vier states: ``` ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ Empty │────▶│ Loading │────▶│ Data │────▶│ Error │ │ (no ctx) │ │ (fetch) │ │ (render) │ │ (retry?) │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ ``` Elke state heeft een standaard UI-component (`BlockEmpty`, `BlockLoading`, `BlockError`) zodat blocks consistent gedrag vertonen. ### 3.6 Relatie met het EPD Cortex is geen losstaand systeem maar een **laag bovenop het EPD** die bestaande modules aanstuurt: ``` ┌─────────────────────────────────────────────────────────┐ │ CORTEX LAAG │ │ Natuurlijke taal → Intent → Actie → Nudge │ └──────────┬──────────┬──────────┬──────────┬─────────────┘ │ │ │ │ ▼ ▼ ▼ ▼ ┌────────────┐ ┌──────────┐ ┌────────┐ ┌─────────┐ │ Rapportage │ │ Agenda │ │Dossier │ │Overdracht│ │ module │ │ module │ │ module │ │ module │ └────────────┘ └──────────┘ └────────┘ └─────────┘ │ │ │ │ ▼ ▼ ▼ ▼ ┌─────────────────────────────────────────────────────────┐ │ DATA LAAG (Supabase) │ │ reports │ encounters │ patients │ conditions │ ... │ └─────────────────────────────────────────────────────────┘ ``` Cortex kent de EPD-modules niet direct. Het produceert **intents met entities**, en de UI-laag vertaalt deze naar de juiste module-interactie (formulier openen, navigeren, data ophalen). Dit houdt Cortex ontkoppeld van de specifieke EPD-implementatie. ### 3.7 Referentiekaders — Positionering in het bredere landschap Cortex gebruikt concepten die herkenbaar zijn in bestaande NLU- en AI-architecturen, maar combineert ze op een domeinspecifieke manier. Deze sectie positioneert Cortex ten opzichte van gangbare patronen, zodat een architect de vertaling kan maken naar bekende referentiekaders. #### 3.7.1 NLU Pipelines (Rasa, Dialogflow, LUIS) Klassieke NLU-systemen volgen een vaste pipeline: **tokenize → featurize → classify → extract entities**. Cortex wijkt hier op twee punten af: | Aspect | Klassieke NLU | Cortex | |--------|--------------|--------| | **Classificatie** | Eén model (ML of rule-based) | Twee lagen: Reflex (regex) + Orchestrator (LLM) | | **Entity-extractie** | Geïntegreerd in pipeline (NER) | Gescheiden: Reflex classificeert alleen, Orchestrator extraheert | | **Training** | Vereist gelabelde dataset + trainingsloop | Geen training: regex-patronen + few-shot prompting | | **Deployment** | Model artifact deployen | Geen model artifact; patronen in code, LLM via API | **Mapping voor een architect**: - Cortex **Reflex Arc** ≈ Rule-based intent classifier (vergelijkbaar met Rasa's `RegexFeaturizer` + `FallbackClassifier`) - Cortex **Orchestrator** ≈ LLM-as-NLU (vergelijkbaar met Dialogflow CX's generative fallback, maar als primaire escalatielaag) - Cortex **IntentChain** ≈ Multi-intent parsing (vergelijkbaar met Rasa's `OR`-intents of Dialogflow's composite entities, maar sequentieel uitgevoerd) **Bewuste afwijking**: Cortex gebruikt geen getraind ML-model. Dit is een bewuste keuze: in een GGZ-context is de vocabulary beperkt en stabiel (medische termen, vaste werkprocessen), waardoor regex-patronen hoge accuracy bereiken zonder de overhead van modeltraining en -onderhoud. Het LLM fungeert als vangnet voor de uitzonderingen. #### 3.7.2 Agentic Architectures (Tool Use, Function Calling) Moderne LLM-architecturen gebruiken "tool use" of "function calling": het model beslist zelf welke tools het aanroept op basis van de gebruikersvraag. Cortex lijkt hierop maar verschilt fundamenteel: | Aspect | Agentic (tool use) | Cortex | |--------|-------------------|--------| | **Beslisser** | LLM beslist autonoom welke tool | Reflex beslist lokaal; LLM alleen bij escalatie | | **Uitvoering** | LLM roept tools aan en verwerkt resultaat | UI voert acties uit; LLM classificeert alleen | | **Autonomie** | Hoog (model plant en voert uit) | Laag (mens bevestigt altijd) | | **Kosten per interactie** | Elke interactie = API call | 70%+ zonder API call | **Mapping voor een architect**: - Cortex **intent** ≈ een function/tool definitie (naam, parameters, beschrijving) - Cortex **IntentChain** ≈ een tool-use plan (sequence of function calls) - Cortex **entities** ≈ function parameters (geëxtraheerd uit natuurlijke taal) - Cortex **artifact/block** ≈ tool execution UI (maar met menselijke bevestiging ertussen) **Bewuste afwijking**: Cortex is expliciet **niet** agentic. In een zorgcontext is autonome uitvoering onacceptabel — de zorgverlener moet altijd de laatste beslissing nemen. Cortex bereidt acties voor en vult formulieren in, maar schrijft nooit zelfstandig naar het dossier. Dit is een harde eis, geen beperking. #### 3.7.3 Clinical Decision Support (CDS) De Nudge-laag (Layer 3) raakt aan Clinical Decision Support — systemen die zorgverleners proactief attenderen op klinisch relevante acties. | Aspect | CDS (HL7 CDS Hooks) | Cortex Nudge | |--------|---------------------|--------------| | **Trigger** | Workflow event (order-sign, patient-view) | Intent completion (dagnotitie opgeslagen) | | **Regelformat** | CQL (Clinical Quality Language) of FHIR PlanDefinition | TypeScript conditie-objecten (equals, contains, matches) | | **Output** | CDS Card (suggestion, info, warning) | NudgeSuggestion (suggestie + protocol referentie) | | **Standaard** | HL7 CDS Hooks specificatie | Eigen formaat | **Mapping voor een architect**: - Cortex **NudgeSuggestion** ≈ CDS Hooks `Card` (suggestion type) - Cortex **ProtocolMetadata** ≈ CDS `source` (clinical evidence reference) - Cortex **PROTOCOL_RULES** ≈ CDS service rules (maar in code i.p.v. FHIR PlanDefinition) **Pad naar standaardisatie**: Voor enterprise-gebruik kan de Nudge-engine evolueren naar CDS Hooks-compatibiliteit. Dit vereist: 1. Regels uitdrukken als FHIR `PlanDefinition` resources (i.p.v. TypeScript objecten) 2. Trigger-events mappen naar CDS Hooks trigger types 3. Output formatteren als CDS Cards Dit is **niet** nodig voor MVP maar bepaalt of de Nudge-laag kan integreren met externe CDS-systemen (bijv. ziekenhuis-CDS die al HL7-compatibel is). #### 3.7.4 FHIR en SMART on FHIR Het EPD gebruikt een FHIR-geïnspireerd datamodel (patients, observations, conditions, encounters), maar is geen FHIR-server. Cortex raakt FHIR op twee punten: **1. Entities → FHIR Resources** Geëxtraheerde entities moeten uiteindelijk mappen naar FHIR-resources: | Cortex Entity | FHIR Resource | Voorbeeld | |---------------|---------------|-----------| | `patientName` / `patientId` | `Patient` | Patient/123 | | `category` + `content` | `Observation` of `DocumentReference` | Rapportage opslaan | | `datetime` + `appointmentType` | `Appointment` | Afspraak aanmaken | | `navigationTarget` (diagnose) | `Condition` | Diagnoselijst bekijken | **2. SMART on FHIR als launch-context** In een enterprise-omgeving wordt Cortex mogelijk gestart vanuit een bestaand EPD via SMART on FHIR launch. De launch-context (welke patiënt, welke practitioner, welke encounter) vervangt dan de huidige `CortexContext`: | Cortex Context | SMART on FHIR equivalent | |----------------|--------------------------| | `activePatient` | Launch context `patient` | | `currentView` | Launch context `encounter` | | `shift` | Geen equivalent (domeinspecifiek) | | `agendaToday` | Geen equivalent (custom query) | **Implicatie voor architectuur**: De `CortexContext` interface moet zo ontworpen zijn dat hij zowel intern (uit de eigen UI) als extern (uit een SMART launch) gevuld kan worden. Het huidige ontwerp sluit dit niet uit, maar de context-opbouw zit verweven in de store i.p.v. in een aparte context-provider. #### 3.7.5 Samenvattende positionering ``` Autonomie ▲ │ Agentic │ (tool use) │ ● │ │ │ Cortex │ Orchestrator │ ● │ │ Cortex │ Reflex NLU │ ● pipeline ● │ │ ─────────────┼──────────────────▶ Domeinkennis Generiek │ Zorgspecifiek │ │ ● Cortex Nudge │ (≈ CDS) │ ``` Cortex zit in het kwadrant **laag-autonoom + hoog-domeinspecifiek**. Het gebruikt NLU-concepten (intent, entity, confidence) maar past ze toe in een context waar snelheid (Reflex), veiligheid (geen autonome uitvoering) en klinische relevantie (protocol-gedreven nudges) zwaarder wegen dan modelflexibiliteit. --- ## 4. Huidige Implementatie (As-Is) ### 4.1 Overzicht De conceptuele architectuur uit hoofdstuk 3 is in het prototype als volgt geïmplementeerd: ``` Gebruikersinput (tekst/spraak) │ ▼ ┌─────────────────────┐ │ Layer 1: Reflex │ <20ms, regex, lokaal │ (reflex-classifier)│ └────────┬────────────┘ │ escalatie bij lage confidence/ambiguïteit/complexiteit ▼ ┌─────────────────────┐ │ Layer 2: Orchestr. │ Claude Haiku, AI-classificatie │ (orchestrator) │ └────────┬────────────┘ │ na succesvolle actie ▼ ┌─────────────────────┐ │ Layer 3: Nudge │ Protocolregels, suggesties │ (nudge) │ └─────────────────────┘ ``` ### 4.2 Layer 1: Reflex Arc **Bestand**: `lib/cortex/reflex-classifier.ts` **Doel**: Snelle, lokale intentclassificatie via gewogen regex-patronen. **Werking**: 1. Detecteert eerst escalatietriggers (multi-intent, voornaamwoorden, relatieve tijd) 2. Bij escalatietrigger → direct doorsturen naar Layer 2 met best-guess intent 3. Zonder trigger → patroonmatching met gewichten (1.0=exact, 0.8=sterk, 0.6=partieel) 4. Houdt top-2 matches bij voor ambiguïteitdetectie 5. Escaleert als confidence <0.7 of als top-2 delta <0.1 **Huidige intents** (11 totaal): | Intent | Categorie | Patronen | |--------|-----------|----------| | `dagnotitie` | P1 (kritiek) | 12 patronen | | `zoeken` | P1 | 10 patronen | | `overdracht` | P1 | 10 patronen | | `agenda_query` | P2 | 10 patronen | | `create_appointment` | P2 | 8 patronen | | `cancel_appointment` | P2 | 6 patronen | | `reschedule_appointment` | P2 | 7 patronen | | `intake_status` | P3 (intake) | 7 patronen | | `intake_navigeer` | P3 | 10 patronen | | `risico_query` | P3 | 6 patronen | | `diagnose_query` | P3 | 6 patronen | **Beperking**: Reflex doet geen entity-extractie. Het resultaat bevat altijd `entities: {}`. Entities worden pas door Layer 2 of de Chat API geëxtraheerd. ### 4.3 Layer 2: Orchestrator **Bestand**: `lib/cortex/orchestrator.ts` **Doel**: AI-gestuurde classificatie voor complexe input via Claude 3.5 Haiku. **Werking**: 1. Ontvangt input + volledige applicatiecontext (actieve patiënt, agenda, recente acties) 2. Formatteert context naar leesbare prompt 3. Claude analyseert en retourneert JSON met: actions[], reasoning, needsClarification 4. Zod-validatie op AI-response met fallback bij parse-fouten 5. Bouwt `IntentChain` met één of meer sequentiële acties 6. Bij falen: fallback naar Reflex-resultaat met confidence gecapped op 0.6 **Capabilities**: - Multi-intent splitting ("annuleer afspraak en maak notitie" → 2 acties) - Voornaamwoordresolutie ("hij" → actieve patiënt) - Relatieve tijdparsing ("morgen" → concrete datum) - Bevestigingsvlaggen voor destructieve acties **Timeout**: 5 seconden via `Promise.race` ### 4.4 Layer 3: Nudge **Bestand**: `lib/cortex/nudge.ts` **Doel**: Proactieve suggesties na afgeronde acties op basis van klinische protocollen. **Werking**: 1. Na voltooiing van een actie wordt `evaluateNudge()` aangeroepen 2. Controleert alle protocolregels: intent-match + conditiechecks (AND-logica) 3. Condities: `equals`, `contains`, `matches` (regex), `exists` 4. Genereert `NudgeSuggestion` met voorgeinvulde entities 5. Sorteert op prioriteit (high → medium → low) 6. Suggesties verlopen na 5 minuten (configureerbaar per regel) **Huidige regels** (2 MVP): | Regel | Trigger | Suggestie | |-------|---------|-----------| | Wondzorg-controle | dagnotitie + content bevat "wond" | Wondcontrole inplannen over 3 dagen | | Medicatie-controle | dagnotitie + content bevat "medicatie" + "gewijzigd" | Medicatie evaluatie over 1 week | ### 4.5 API-integratie #### Classify API (`/api/cortex/classify`) Hybride endpoint dat Layer 1 en 2 combineert: 1. Authenticatie via Supabase 2. Zod-validatie op request (input, context, options) 3. Feature flag check (`CORTEX_V2_ENABLED`) 4. Reflex → eventueel Orchestrator → eventueel Fallback 5. Retourneert `IntentChain` + debug-informatie #### Chat API (`/api/cortex/chat`) Streaming conversatie-endpoint (SSE) met Claude Sonnet: - Eigen system prompt met volledige intentbeschrijvingen - Conversatiegeschiedenis (max 20 berichten) - Genereert JSON action objects in de tekststroom - In-memory rate limiting (20 req/min per gebruiker) - Eigen entity-extractie via prompt engineering #### Overige Cortex APIs - `/api/cortex/context` — Gebruikerscontext ophalen - `/api/cortex/patients/search` — Patiëntzoeken - `/api/cortex/agenda/*` — CRUD voor afspraken - `/api/cortex/intake/*` — Intakequeries ### 4.6 State Management **Bestand**: `stores/cortex-store.ts` (Zustand) Eén monolithische store voor alle Cortex-state: - **Context**: activePatient, shift, recentPatients - **Chat**: chatMessages, isStreaming, pendingAction - **Artifacts**: openArtifacts (max 3), activeArtifactId - **V2 Chain**: activeChain, chainHistory - **Nudge**: suggestions (add/accept/dismiss) - **Clarification**: pendingClarification - **UI**: inputValue, isVoiceActive, patientSidebarOpen ### 4.7 Feature Flags **Bestand**: `lib/config/feature-flags.ts` Vier flags via environment variables: - `CORTEX_V2_ENABLED` — Drie-laags architectuur aan/uit - `CORTEX_MULTI_INTENT` — Multi-intent detectie - `CORTEX_NUDGE` — Proactieve suggesties - `CORTEX_LOGGING` — Debug logging In development staan alle flags standaard aan (tenzij expliciet `false`). --- ## 5. Analyse: Sterke punten ### 5.1 Architecturele beslissingen | Beslissing | Toelichting | |------------|-------------| | **Drie-laags fallback** | Graceful degradation: als AI faalt, valt het systeem terug op lokale matching | | **Gewogen patroonmatching** | Nuanced confidence via weights i.p.v. binaire matches | | **Ambiguïteitdetectie** | Top-2 tracking voorkomt foutieve classificatie bij twijfelgevallen | | **Zod-validatie op AI output** | Beschermt tegen onverwachte LLM-responses met gestructureerde fallbacks | | **IntentChain-model** | Multi-intent support is architectureel correct opgezet | | **Protocol-based nudges** | Klinische onderbouwing via metadata (naam, referentie, rationale) | ### 5.2 Implementatiekwaliteit - TypeScript-types zijn doordacht en consistent - Escalatielogica is helder: detectie → reden → result - Fallback-chain is compleet: AI fout → Reflex result met confidence cap - Zod-schemas met `.catch()` en `.passthrough()` voor robuuste parsing --- ## 6. Analyse: Knelpunten ### 6.1 K1 — Twee divergente classificatiepaden **Probleem**: De Classify API (`/api/cortex/classify`) en de Chat API (`/api/cortex/chat`) zijn volledig onafhankelijke implementaties van intentherkenning. | Aspect | Classify API | Chat API | |--------|-------------|----------| | Classificatie | Reflex → Orchestrator | Eigen system prompt | | Model | Claude Haiku | Claude Sonnet | | Entity-extractie | AI (Orchestrator) | Prompt engineering | | Output | IntentChain (gestructureerd) | JSON-in-tekststroom | | Intents beschreven | `orchestrator.ts` prompt | `chat/route.ts` prompt | **Impact**: Intentbeschrijvingen worden op twee plekken bijgehouden. Wijzigingen in het ene pad worden niet automatisch doorgevoerd in het andere. **Ernst**: Hoog — Dit is de belangrijkste architecturale schuld. ### 6.2 K2 — Intent-definities op drie plekken Een intent wordt momenteel op drie locaties gedefinieerd: 1. **types.ts** — TypeScript type (`CortexIntent` union type) 2. **reflex-classifier.ts** — Regex-patronen en gewichten 3. **orchestrator.ts** — Systemprompt met intentbeschrijvingen en voorbeelden Een nieuw intent toevoegen vereist wijzigingen op alle drie plekken, plus optioneel de Chat API prompt (vierde plek). **Impact**: Hoog risico op inconsistentie bij wijzigingen. ### 6.3 K3 — Reflex doet geen entity-extractie Layer 1 retourneert altijd `entities: {}`. Dit betekent dat zelfs bij een hoge-confidence match ("notitie jan medicatie") geen entities worden doorgegeven aan de UI. De actie wordt geopend zonder prefill-data. **Impact**: Gemiste kans op snellere interactie. In 70%+ van de gevallen (Reflex handled) gaat entity-informatie verloren. ### 6.4 K4 — Nudge-systeem is niet geïntegreerd De `evaluateNudge()` functie bestaat maar er is geen duidelijk aanroeppunt in de applicatiecode na actievoltooiing. De integratie tussen actiecompletie (store) en nudge-evaluatie ontbreekt. **Impact**: Layer 3 is effectief inactief zonder expliciete aanroep. ### 6.5 K5 — Monolithische store De Zustand store bevat ~580 regels en beheert 7 verschillende domeinen (context, chat, artifacts, chains, nudges, clarification, UI). Dit maakt de store moeilijk te testen en te refactoren. ### 6.6 K6 — Operationele blinde vlekken - **Logging**: Alleen `console.log` met feature flag; geen gestructureerde logging - **Metrics**: Geen tracking van classificatiesnelheid, hit rates, of fallback-frequentie in productie - **Rate limiting**: In-memory (verloren bij restart), niet geschikt voor multi-instance deployment ### 6.7 K7 — Orchestrator timeout is niet doorverbonden De `classifyWithTimeout` wrapper maakt een `AbortController` aan, maar verbindt het signal niet met de daadwerkelijke `fetch`-call in `classifyWithOrchestrator`. Bij timeout wordt het API-verzoek naar Claude niet afgebroken — het blijft lopen. --- ## 7. Doelarchitectuur (To-Be) ### 7.1 Architectuuroverzicht ``` ┌───────────────────────────────────────────────────────────────────┐ │ CORTEX COMMAND CENTER │ │ │ │ ┌─────────┐ ┌──────────────────────────────────────────────┐ │ │ │ Input │──▶│ Classification Pipeline │ │ │ │ (tekst/ │ │ │ │ │ │ spraak) │ │ ┌──────────┐ ┌──────────┐ ┌────────┐ │ │ │ └─────────┘ │ │ Reflex │──▶│ Orchestr.│──▶│ Entity │ │ │ │ │ │ (Layer 1)│ │ (Layer 2)│ │ Enrich │ │ │ │ │ └──────────┘ └──────────┘ └────────┘ │ │ │ │ ↑ │ │ │ │ Intent Registry (single source) │ │ │ └──────────────────┬───────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────┐ │ │ │ Action Executor │ │ │ │ Chain uitvoering → API calls → Result │ │ │ └──────────────────┬───────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────┐ │ │ │ Nudge Evaluator (Layer 3) │ │ │ │ Protocol rules → Suggesties │ │ │ └──────────────────────────────────────────────┘ │ │ │ │ ┌───────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ │ Chat │ │ Artifact │ │ Nudge │ │ Context │ │ │ │ Store │ │ Store │ │ Store │ │ Store │ │ │ └───────────┘ └────────────┘ └────────────┘ └────────────┘ │ └───────────────────────────────────────────────────────────────────┘ ``` **Kernwijziging**: Eén classificatiepipeline als backbone. Chat en Classify gebruiken dezelfde pipeline. De Chat API voegt conversatie-context toe, maar delegeert classificatie. ### 7.2 Intent Registry — Single Source of Truth **Probleem opgelost**: K1, K2 Het centrale concept is een `IntentRegistry`: één plek waar elk intent volledig is gedefinieerd. ```typescript // lib/cortex/intent-registry.ts interface IntentDefinition { /** Uniek ID */ id: CortexIntent; /** Weergavenaam (Nederlands) */ label: string; /** Beschrijving voor AI-prompt (Nederlands) */ description: string; /** Prioriteit: P1 (kritiek), P2 (belangrijk), P3 (overig) */ priority: 'P1' | 'P2' | 'P3'; /** Regex-patronen voor Reflex Arc met gewichten */ reflexPatterns: Array<{ pattern: RegExp; weight: number }>; /** Entity-schema voor dit intent */ entitySchema: { required: string[]; optional: string[]; extraction: EntityExtractionRule[]; }; /** Of dit intent bevestiging vereist */ requiresConfirmation: boolean; /** Voorbeelden voor AI-prompt (input → verwacht resultaat) */ examples: Array<{ input: string; entities: Record }>; /** Gekoppeld artifact type */ artifactType: BlockType | null; /** Feature flag (optioneel) */ featureFlag?: FeatureFlagKey; } ``` **Voordelen**: - Eén plek om een intent toe te voegen of te wijzigen - Reflex-patronen en AI-promptbeschrijvingen komen uit dezelfde bron - Entity-schema's zijn herbruikbaar voor validatie - Voorbeelden dienen zowel de AI-prompt als test-cases - Feature flags per intent voor granulaire rollout **Afgeleide functies**: - `getReflexPatterns()` → genereert patroonmap voor Layer 1 - `buildOrchestratorPrompt()` → genereert systemprompt voor Layer 2 vanuit registry - `buildChatPrompt()` → genereert systemprompt voor Chat API vanuit registry - `getEntitySchema(intent)` → retourneert Zod-schema voor entity-validatie ### 7.3 Layer 1: Reflex Arc (verbeterd) **Probleem opgelost**: K3 ``` Input │ ├─ 1. Escalatiedetectie (ongewijzigd) │ ├─ 2. Patroonmatching via IntentRegistry │ └─ 3. [NIEUW] Basis entity-extractie │ └─ EntityExtractionRule's uit registry Voorbeeld: patientName via /^notitie\s+(\w+)/ ``` **Wijzigingen t.o.v. huidige implementatie**: 1. **Patronen laden uit IntentRegistry** i.p.v. hardcoded `INTENT_PATTERNS` object 2. **Basis entity-extractie toevoegen** via capture groups in regex-patronen 3. **Patroonmatching optimalisatie**: patronen per intent gesorteerd op gewicht (hoogste eerst), met early-exit bij weight 1.0 **Entity-extractie voorbeeld**: ```typescript interface EntityExtractionRule { /** Regex met named capture groups */ pattern: RegExp; /** Mapping: capture group naam → entity veld */ mapping: Record; } // Voorbeeld voor dagnotitie: { pattern: /^notitie\s+(?\w+)\s+(?medicatie|adl|gedrag)/i, mapping: { patient: 'patientName', cat: 'category' } } ``` **Wat NIET wijzigt**: De escalatielogica, confidence thresholds, en ambiguïteitdetectie blijven ongewijzigd. Deze werken goed. ### 7.4 Layer 2: Orchestrator (verbeterd) **Probleem opgelost**: K1, K7 **Wijzigingen**: #### 7.4.1 Promptgeneratie uit IntentRegistry De systemprompt wordt niet meer handmatig bijgehouden maar gegenereerd: ```typescript function buildOrchestratorPrompt(registry: IntentDefinition[]): string { const intentSection = registry .map(intent => ` **${intent.id}** — ${intent.description} Entities: ${intent.entitySchema.required.join(', ')} (verplicht), ${intent.entitySchema.optional.join(', ')} (optioneel) Voorbeelden: ${intent.examples.map(e => `"${e.input}"`).join(', ')} `).join('\n'); return `${ORCHESTRATOR_BASE_PROMPT}\n\n## Intent Types\n${intentSection}`; } ``` De basisprompt (instructies over multi-intent, voornaamwoordresolutie, etc.) blijft statisch. Alleen de intentlijst wordt dynamisch gegenereerd. #### 7.4.2 AbortController doorverbinden ```typescript // Fix: signal doorverbinden naar fetch const response = await fetch('https://api.anthropic.com/v1/messages', { signal, // AbortController.signal // ... rest }); ``` #### 7.4.3 Circuit breaker (simpel) In plaats van alleen een timeout, een eenvoudig circuit breaker-patroon: ```typescript interface CircuitState { failures: number; lastFailure: number; isOpen: boolean; } // Na 3 opeenvolgende fouten: circuit open voor 30 seconden // Gedurende open circuit: directe fallback naar Reflex zonder API-call ``` Dit voorkomt dat bij een Claude API-storing elk request 5 seconden wacht op timeout. ### 7.5 Unified Classification Pipeline **Probleem opgelost**: K1 De kern van de verbetering: één pipeline die zowel de Classify als Chat API gebruiken. ``` ┌──────────────────────────────────────────────────────────┐ │ Classification Pipeline │ │ │ │ classifyInput(input, context) → ClassificationResult │ │ │ │ 1. Reflex Arc → LocalResult │ │ 2. if (shouldEscalate) → Orchestrator → AIResult │ │ 3. if (AIResult fails) → Fallback │ │ 4. Entity enrichment → Final result │ │ │ │ Output: { intent, confidence, entities, chain, source } │ └──────────────────────────────────────────────────────────┘ │ │ ▼ ▼ ┌──────────────────┐ ┌──────────────────────┐ │ Classify API │ │ Chat API │ │ Retourneert │ │ Gebruikt result als │ │ pipeline result │ │ context voor LLM │ │ direct │ │ conversatie │ └──────────────────┘ └──────────────────────┘ ``` **Chat API wijziging**: In plaats van intentherkenning te dupliceren in de systemprompt, stuurt de Chat API eerst de input door de pipeline. Het resultaat wordt meegegeven aan de LLM als context: ``` Systemprompt (kort): Je bent Cortex Assistent. [tone of voice, regels] Context: { classificationResult, activePatient, shift } Opdracht: Formuleer een natuurlijk antwoord en bevestig de actie. ``` De LLM hoeft niet meer te classificeren — alleen te converseren. ### 7.6 Layer 3: Nudge (verbeterd) **Probleem opgelost**: K4 #### 7.6.1 Integratiepatroon De integratie loopt via de Action Executor: ```typescript // In de action executor (na succesvolle actie): async function executeAction(action: IntentAction): Promise { // ... actie uitvoeren ... // Na succes: nudge evalueren const nudges = evaluateNudge({ intent: action.intent, actionId: action.id, entities: action.entities, content: originalInput, }); // Nudges toevoegen aan store for (const nudge of nudges) { nudgeStore.addSuggestion(nudge); } } ``` #### 7.6.2 Regelopslag scheiden van engine ``` ┌─────────────────────────────────────────────┐ │ Nudge Engine │ │ │ │ evaluateNudge(input, rules) → suggestions │ │ │ │ - Puur functioneel: input → output │ │ - Geen kennis van waar regels vandaan komen │ └──────────────────────┬──────────────────────┘ │ Regels laden uit: │ ┌─────────────┴─────────────┐ │ │ ┌────▼────┐ ┌───────▼──────┐ │ Hardcod │ │ Database/ │ │ (MVP) │ │ Config (v2) │ └─────────┘ └──────────────┘ ``` De `evaluateNudge` functie accepteert regels als parameter i.p.v. het importeren van `PROTOCOL_RULES`. Dit maakt de engine testbaar en de regelbron vervangbaar. #### 7.6.3 Feedback loop (post-MVP) Track per regel: - Aantal keer getoond - Aantal keer geaccepteerd - Aantal keer verworpen - Gemiddelde reactietijd Dit is bewust post-MVP maar het datamodel moet er nu al rekening mee houden door `accept/dismiss` events op te slaan. ### 7.7 State Management (opsplitsing) **Probleem opgelost**: K5 Splits de monolithische store in domein-specifieke stores: ``` stores/ ├── cortex-context-store.ts # activePatient, shift, recentPatients ├── cortex-chat-store.ts # chatMessages, isStreaming, pendingAction ├── cortex-artifact-store.ts # openArtifacts, activeArtifactId ├── cortex-action-store.ts # activeChain, chainHistory └── cortex-nudge-store.ts # suggestions, accept/dismiss ``` **Koppeling tussen stores**: Via Zustand's `subscribe` mechanisme. Voorbeeld: ```typescript // Wanneer een chain completeert, evalueer nudges cortexActionStore.subscribe( state => state.activeChain, (chain, previousChain) => { if (previousChain && !chain) { // Chain is zojuist voltooid const completedActions = previousChain.actions.filter(a => a.status === 'success'); // → evaluateNudge voor elke voltooide actie } } ); ``` Elke store blijft klein (<150 regels), testbaar, en verantwoordelijk voor één domein. ### 7.8 Observability **Probleem opgelost**: K6 #### 7.8.1 Gestructureerde logging Vervang `console.log` door gestructureerde log-events: ```typescript interface CortexLogEvent { timestamp: string; layer: 'reflex' | 'orchestrator' | 'nudge' | 'pipeline'; event: string; data: Record; durationMs?: number; userId?: string; } // Voorbeeld: log({ layer: 'reflex', event: 'classification_complete', data: { input: input.slice(0, 50), intent: result.intent, confidence: result.confidence, escalated: result.shouldEscalateToAI, escalationReason: result.escalationReason, }, durationMs: result.processingTimeMs, }); ``` #### 7.8.2 Metrics Minimale set metrics voor productie: | Metric | Type | Beschrijving | |--------|------|-------------| | `cortex.classify.duration_ms` | histogram | Totale classificatietijd | | `cortex.reflex.hit_rate` | counter | % dat Reflex zelfstandig afhandelt | | `cortex.orchestrator.fallback_rate` | counter | % AI-fouten met fallback | | `cortex.orchestrator.timeout_rate` | counter | % timeouts | | `cortex.nudge.shown` | counter | Nudges getoond per regel-ID | | `cortex.nudge.accepted` | counter | Nudges geaccepteerd per regel-ID | | `cortex.intent.distribution` | counter | Frequentie per intent type | In eerste instantie te loggen naar de bestaande `ai_events` tabel. Externe monitoring (Datadog, Grafana) als post-MVP. --- ## 8. Integratiearchitectuur ### 8.1 API-structuur (herzien) ``` /api/cortex/ ├── classify/ # POST — Classification Pipeline (bestaand, herzien) ├── chat/ # POST — Streaming chat (herzien: gebruikt pipeline) ├── context/ # GET — Gebruikerscontext (ongewijzigd) ├── patients/search/ # GET — Patiëntzoeken (ongewijzigd) ├── agenda/ # GET — Dagagenda (ongewijzigd) ├── agenda/create/ # POST — Afspraak maken (ongewijzigd) ├── agenda/cancel/ # POST — Afspraak annuleren (ongewijzigd) ├── agenda/reschedule/ # POST — Afspraak verzetten (ongewijzigd) └── intake/ # GET — Intakequeries (ongewijzigd) ``` De primaire wijziging is dat `/api/cortex/chat` intern de classification pipeline aanroept in plaats van classificatie te dupliceren. ### 8.2 Dataflow: Input tot Actie ``` 1. Gebruiker typt/spreekt: "Notitie jan medicatie gegeven" │ 2. Client stuurt naar: POST /api/cortex/chat │ 3. Chat API: │ a. classifyInput(input, context) → Reflex: intent=dagnotitie, confidence=1.0, entities={patientName:"jan", category:"medicatie"} → Geen escalatie nodig │ b. LLM call met: - Systemprompt (tone of voice, regels) - Classification result als context - Conversatiegeschiedenis │ c. LLM genereert: "Komt voor elkaar. Jan, medicatie." + JSON action block │ 4. Client ontvangt SSE stream │ 5. Action parsing → pendingAction in store │ 6. CommandCenter opent DagnotitieBlock met prefill: { patientName: "jan", category: "medicatie", content: "medicatie gegeven" } │ 7. Gebruiker bevestigt/past aan → POST /api/reports │ 8. Na succes: evaluateNudge() → Geen match (content bevat geen "wond" of "medicatie gewijzigd") → Geen nudge ``` ### 8.3 Afhankelijkheden buiten scope | Afhankelijkheid | Relatie met Cortex | Status | |-----------------|-------------------|--------| | **Autorisatiemodel** | Bepaalt welke intents een gebruiker mag uitvoeren | Niet geïmplementeerd | | **Datamodel (FHIR)** | Entities moeten mappen naar FHIR-resources | Gedeeltelijk | | **Audit trail** | Elke Cortex-actie moet gelogd worden | Basis via `ai_events` tabel | | **Notificatiesysteem** | Nudges zouden ook via push/email kunnen | Niet aanwezig | --- ## 9. Enterprise-eisen ### 9.1 Niet-Functionele Eisen (NFR's) #### 9.1.1 Latency | Scenario | Target (P95) | Target (P99) | Huidige status | |----------|-------------|-------------|----------------| | Reflex-only classificatie | <20ms | <50ms | Voldoet (regex, lokaal) | | Reflex + Orchestrator (AI) | <3s | <5s | ~2-4s (afhankelijk van Claude API) | | End-to-end: input → artifact open | <500ms (Reflex) / <4s (AI) | <1s / <6s | Niet gemeten | | Chat response (first token) | <1s | <2s | ~1-2s (SSE streaming) | | Nudge evaluatie | <10ms | <20ms | Voldoet (lokale regelengine) | **Latency budget uitsplitsing** (worst case, AI-pad): ``` Gebruikersinput 0ms ├─ Reflex classificatie 20ms ├─ Escalatiebeslissing 1ms ├─ Orchestrator API call 3000ms ← dominante factor ├─ Response parsing + validatie 5ms ├─ Entity enrichment 10ms ├─ Store update + UI render 50ms └─ Artifact open (data fetch) 200ms Totaal: ~3.3s ``` **Implicatie**: De Orchestrator API call bepaalt de E2E latency. Optimalisatie moet zich richten op (1) Reflex hit-rate verhogen zodat minder naar AI geëscaleerd wordt, en (2) circuit breaker zodat bij API-traagheid snel gefallbacked wordt. #### 9.1.2 Beschikbaarheid | Component | Beschikbaarheidseis | Failover-strategie | |-----------|--------------------|--------------------| | **Reflex Arc** | 99.9% (systeembeschikbaarheid) | Geen externe dependency; draait in-process | | **Orchestrator** | Best-effort (afhankelijk van AI-provider) | Fallback naar Reflex met confidence cap 0.6 | | **Nudge Engine** | 99.9% (lokale regelengine) | Geen externe dependency | | **Chat API** | Best-effort | Fallback: classificatie werkt nog, conversatie niet | | **Cortex als geheel** | 99.9% voor basisclassificatie | Drie degradatieniveaus (zie hieronder) | **Degradatieniveaus**: ``` Niveau 0: Volledig operationeel Reflex + Orchestrator + Nudge + Chat Latency: P95 <3s (AI-pad), <500ms (Reflex-pad) Niveau 1: AI-provider onbeschikbaar Reflex + Nudge + Chat (zonder classificatie) Impact: Complexe input wordt minder goed begrepen Latency: P95 <500ms (alles lokaal) Automatische trigger: Circuit breaker na 3 fouten Niveau 2: Database onbeschikbaar Alleen Reflex classificatie (geen data-acties) Impact: Intents worden herkend maar niet uitgevoerd Gebruikersfeedback: "Classificatie succesvol, maar de actie kan momenteel niet worden uitgevoerd" Niveau 3: Volledig onbeschikbaar Cortex command center niet bereikbaar Impact: Gebruiker valt terug op traditionele EPD-navigatie EPD-modules blijven direct bereikbaar ``` #### 9.1.3 AI-provider abstractie Het systeem is momenteel hard gekoppeld aan Anthropic Claude (Haiku voor classificatie, Sonnet voor chat). Dit is een vendor lock-in risico. **Huidige koppelingen**: | Koppeling | Locatie | Impact bij providerwisseling | |-----------|---------|------------------------------| | Model ID (`claude-3-5-haiku`) | `orchestrator.ts` | Aanpassen | | Model ID (`claude-sonnet-4-5-20250929`) | `chat/route.ts` | Aanpassen | | Anthropic SDK (`@anthropic-ai/sdk`) | Beide API routes | Vervangen | | Prompt-format (system/user rollen) | Orchestrator + Chat | Herschrijven (model-afhankelijk) | | Response-parsing (JSON uit LLM) | `orchestrator.ts` | Herijken (output varieert per model) | **Aanbevolen abstractie** (minimaal): ```typescript interface LLMProvider { classify(input: string, systemPrompt: string, context: string): Promise; stream(messages: Message[], systemPrompt: string): AsyncIterable; } ``` Eén interface, twee implementaties (Anthropic, OpenAI). Geen framework (LangChain, Vercel AI SDK) nodig — de interface is klein genoeg om direct te implementeren. **Prompt-portabiliteit**: Prompts zijn niet 1-op-1 overdraagbaar tussen modellen. De systeemprompt (met JSON-outputformat, intentbeschrijvingen, en voorbeelden) is geoptimaliseerd voor Claude. Bij een providerwisseling moeten prompts geherijkt worden. Dit is inherent aan LLM-gebruik en niet volledig te abstraheren. #### 9.1.4 Schaalbaarheid | Dimensie | Huidig ontwerp | Enterprise target | Bottleneck | |----------|---------------|-------------------|------------| | Concurrent users | Onbeperkt (stateless API) | 100-500 per organisatie | Rate limiting (nu in-memory) | | Intents per seconde | Niet gemeten | 50-100 req/s | AI-provider rate limits | | Aantal intents | 11 | 30-50 | Reflex patroonmatching O(n×p) | | Conversatie-lengte | 20 berichten (hardcoded) | 20-50 berichten | Context window + kosten | | Nudge-regels | 2 (hardcoded) | 20-50 | Evaluatietijd O(r×c) | **Reflex schaalbaarheid bij meer intents**: Bij 11 intents × ~8 patronen = ~88 regex evaluaties per classificatie. Bij 50 intents × 8 patronen = ~400 evaluaties. Dit blijft <50ms maar kan geoptimaliseerd worden met: 1. **Keyword pre-filter**: Eerste woord matchen tegen index → alleen relevante intents evalueren 2. **Prioriteit-gebaseerde volgorde**: P1-intents eerst; bij hoge-confidence match, stop 3. **Categorie-groepering**: Intents per domein (klinisch, administratief, navigatie) → per categorie evalueren Deze optimalisaties zijn beschreven in `docs/intent/design/architecture-intent-scalability.md`. **Rate limiting** (enterprise): In-memory rate limiting (huidige implementatie) verliest state bij restart en werkt niet bij meerdere server-instances. Enterprise-alternatief: | Optie | Complexiteit | Geschikt voor | |-------|-------------|---------------| | Supabase rate limit tabel | Laag | Single-instance, eenvoudig | | Redis (Upstash) | Medium | Multi-instance, serverless | | API Gateway rate limiting | Laag | Als er al een API gateway is | #### 9.1.5 Data-eigenaarschap en retentie | Datatype | Eigenaar | Retentie | Locatie | |----------|---------|----------|---------| | **Classificatieresultaten** | Organisatie | 90 dagen (analytics) | `ai_events` tabel | | **Chatberichten** | Organisatie | Sessie-duur (niet persistent) | In-memory (client) | | **AI API calls (input/output)** | Organisatie + AI-provider | Per provider-DPA | Anthropic servers (niet persistent bij API) | | **Nudge-interacties** | Organisatie | 1 jaar (kwaliteitsverbetering) | Database | | **Audit events** | Organisatie | 5 jaar (NEN 7510) | Audit-tabel (append-only) | **Aandachtspunten**: - Chatberichten worden niet naar de database geschreven. Bij een enterprise-omgeving met auditplicht moet overwogen worden of chatgeschiedenis persistent moet zijn. - AI API calls bevatten potentieel patiëntgegevens in de prompt. Anthropic's data processing agreement garandeert dat API-data niet gebruikt wordt voor training, maar dit moet contractueel geborgd zijn. - PII in logs: Alle log-events moeten door een sanitisatiestap gaan die patiëntnamen, BSN, en andere PII verwijdert of hasht voordat ze worden opgeslagen. ### 9.2 NEN 7510 (Informatiebeveiliging in de zorg) | Eis | Huidige status | Actie nodig | |-----|---------------|-------------| | Toegangsbeveiliging | Supabase Auth + RLS | Rol-gebaseerde intentfiltering toevoegen | | Logging van toegang | Gedeeltelijk (`ai_events`) | Uitbreiden naar alle Cortex-acties | | Geen PII in logs | Niet gegarandeerd | Inputsanitisatie in log-events | | Encryptie in transit | HTTPS (Vercel default) | Voldoende | | Encryptie at rest | Supabase managed | Voldoende | | Integriteitscontrole | Niet aanwezig | Checksums op audit-records (append-only tabel) | | Incidentregistratie | Niet aanwezig | Cortex-fouten loggen als beveiligingsincident bij herhaaldelijk falen | ### 9.3 Audit Logging Elke Cortex-interactie moet gelogd worden: ```typescript interface AuditEvent { userId: string; action: 'classify' | 'chat' | 'nudge_shown' | 'nudge_accepted' | 'action_executed'; intent?: CortexIntent; confidence?: number; source?: 'reflex' | 'orchestrator' | 'fallback'; patientId?: string; input?: string; // Gesanitiseerd (PII verwijderd) result?: string; // Samenvatting, geen volledige response timestamp: string; sessionId?: string; // Voor correlatie binnen één gebruikerssessie durationMs?: number; // Verwerkingstijd metadata?: Record; } ``` **Opslagvereisten**: - Append-only tabel (geen UPDATE of DELETE) - Retentie: minimaal 5 jaar (NEN 7510) - Indexen op: `userId`, `patientId`, `timestamp`, `action` ### 9.4 Role-Based Access (voorbereiding) Het intent-systeem moet voorbereid zijn op rolgebaseerde filtering: ```typescript interface IntentDefinition { // ... bestaande velden ... /** Welke rollen mogen dit intent gebruiken */ allowedRoles?: string[]; // Leeg = iedereen } ``` Voorbeeldtoepassing: | Rol | Toegestane intents | Geblokkeerde intents | |-----|-------------------|---------------------| | Verpleegkundige | dagnotitie, overdracht, zoeken, agenda_* | — | | Behandelaar | Alle | — | | Stagiair | zoeken, agenda_query | cancel_appointment, reschedule_appointment | | Admin | Alle + systeem-intents | — | Dit hoeft nu niet geïmplementeerd te worden, maar het veld moet gereserveerd zijn in de IntentRegistry. De filtering kan als middleware in de Classification Pipeline worden toegevoegd: ``` Input → [RBAC filter] → Reflex → Orchestrator → Result ↓ Als intent niet toegestaan: → "Je hebt geen toegang tot deze functie" ``` --- ## 10. Migratiepad ### 10.1 Fase 1 — Intent Registry + Reflex entity-extractie **Doel**: Single Source of Truth voor intents, betere Reflex output. 1. Maak `lib/cortex/intent-registry.ts` met alle 11 intents 2. Refactor `reflex-classifier.ts` om patronen uit registry te laden 3. Voeg basis entity-extractie toe aan Reflex (capture groups) 4. Genereer Orchestrator-prompt uit registry 5. Tests: verifieer dat bestaande classificatie niet breekt **Risico**: Laag — Refactoring zonder gedragswijziging (behalve entity-extractie toevoeging). ### 10.2 Fase 2 — Unified Pipeline + Chat integratie **Doel**: Eén classificatiepad, Chat API als conversatielaag. 1. Extract `classifyInput()` als standalone pipeline-functie 2. Pas Chat API aan om pipeline te gebruiken 3. Vereenvoudig Chat systemprompt (geen intentherkenning meer) 4. Fix AbortController doorverbinding 5. Voeg circuit breaker toe **Risico**: Medium — Chat-gedrag kan subtiel veranderen. Uitgebreid testen met bestaande scenarios. ### 10.3 Fase 3 — Nudge integratie + Store opsplitsing **Doel**: Layer 3 daadwerkelijk actief, state management opgeschoond. 1. Splits Zustand store in 5 domein-stores 2. Integreer `evaluateNudge()` via store subscription na chain completion 3. Voeg 3-5 klinische regels toe aan nudge systeem 4. Implementeer nudge tracking (getoond/geaccepteerd/verworpen) **Risico**: Laag — Nieuwe functionaliteit, geen breaking changes. ### 10.4 Fase 4 — Observability + Enterprise **Doel**: Productieklare logging en monitoring. 1. Gestructureerde logging implementeren 2. Metrics toevoegen aan classification pipeline 3. Audit events loggen naar database 4. Rate limiting naar database-backed (Redis of Supabase) 5. Input sanitisatie voor logs (geen PII) **Risico**: Laag — Cross-cutting concerns, geen functionaliteitswijziging. --- ## 11. Samenvatting ### Kernboodschap Het huidige prototype heeft een architectureel solide basis (drie-laags model, fallback-strategie, Zod-validatie). De belangrijkste verbeterslag is **unificatie**: één Intent Registry als bron van waarheid, één classificatiepipeline die zowel directe classificatie als chat bedient, en gesplitste state management voor onderhoudbaarheid. ### Prioriteiten | # | Wijziging | Impact | Inspanning | |---|-----------|--------|------------| | 1 | Intent Registry (Single Source of Truth) | Hoog | Medium | | 2 | Unified Classification Pipeline | Hoog | Medium | | 3 | Reflex entity-extractie | Medium | Laag | | 4 | Nudge integratie | Medium | Laag | | 5 | Store opsplitsing | Medium | Medium | | 6 | Observability | Medium | Laag | | 7 | Circuit breaker + AbortController fix | Laag | Laag | | 8 | Enterprise (audit, RBAC-voorbereiding) | Laag (nu) | Laag | ### Wat bewust NIET wordt voorgesteld - **Event bus / pub-sub**: Overkill voor single-tenant applicatie; Zustand subscriptions volstaan - **Microservices**: Cortex als apart systeem deployen voegt complexiteit toe zonder voordeel bij deze schaal - **ML-model voor classificatie**: Regex + LLM fallback is effectiever en beter debugbaar dan een custom model - **Nudge learning via ML**: Accept/dismiss tracking is voldoende; automatische regeloptimalisatie is premature complexiteit - **Multi-tenancy**: Buiten scope; de architectuur sluit het niet uit maar optimaliseert er niet voor --- ## 12. Appendix — Documentatie-index Dit rapport is de architecturale toegangspoort tot het Cortex-systeem. Onderstaande index verwijst naar alle gerelateerde documentatie in de codebase, gegroepeerd per fase van het ontwerpproces. ### 12.1 Strategie & Product | Document | Pad | Beschrijving | |----------|-----|-------------| | PRD Cortex V2 | `docs/intent/prd-cortex-v2.md` | Product Requirements: transformatie van reactief "spraakgestuurd toetsenbord" naar AI Collega met context-awareness en multi-intent | | MVP User Stories | `docs/intent/mvp-userstories-intent-system.md` | Scope-definitie voor de publieke Cortex-prototype met agency en multi-intent demonstratie | | Haalbaarheidsanalyse | `docs/intent/haalbaarheidsanalyse-cortex-v2.md` | Conclusie: Cortex V2 haalbaar met 70% bestaande infrastructuur; gefaseerde aanpak | | Demo Script | `docs/intent/demo-script-cortex-v2.md` | 5-minuten demonstratiescript met environment setup en feature flag configuratie | ### 12.2 Functioneel & Technisch Ontwerp | Document | Pad | Beschrijving | |----------|-----|-------------| | Functioneel Ontwerp V2 | `docs/intent/fo-cortex-intent-system-v2.md` | FO gericht op contextbegrip, meerdere intenties en proactieve ondersteuning | | Technisch Ontwerp V2 | `docs/intent/to-cortex-v2.md` | Vertaling naar concrete architectuur, techstack, datamodellen en implementatiedetails | | Bouwplan Cortex V2 | `docs/intent/bouwplan-cortex-v2.md` | Implementatieplan met epic-structuur voor drie-laag architectuur | | Review Rapport | `docs/intent/review-cortex-v2.md` | Architectuurreview met goedkeuring en aanbevelingen | ### 12.3 Architectuur & Design | Document | Pad | Beschrijving | |----------|-----|-------------| | Architectuur Cortex V2 | `docs/intent/design/architecture-cortex-v2.md` | Uitgebreid architectuurplan: drie-laag model, datamodels, APIs, frontend componenten | | Intent Schaalbaarheid | `docs/intent/design/architecture-intent-scalability.md` | Schaalbaarheidsstrategieën van 7 naar 35+ intents met performance-optimalisatie | | Architectuurvoorstel | `docs/intent/intent-architecture-v2-proposal.md` | Consensus PO/UX/Dev voor hybrid leader/follower architectuur | | Analyse Intents & Nudges | `docs/intent/design/analyse-intents-nudges.md` | Analyse van geïmplementeerde intents met aanbevelingen voor 7 nieuwe nudges en 4 query intents | | GGZ Zinnen Dataset | `docs/intent/design/dataset-ggz-zinnen.md` | Dataset van GGZ-specifieke zinnen voor training en testing van classificatiesysteem | | Mini-EPD Architectuur | `docs/swift/architectuur.md` | Overkoepelende architectuur met kernwaarden (70% <20ms, GDPR-compliance) | | Architectuuroverzicht | `docs/architectuur/architectuur-overzicht.md` | Mini-EPD als AI-gestuurd EPD voor GGZ met spraakinterface | ### 12.4 Intent Implementatie | Document | Pad | Beschrijving | |----------|-----|-------------| | Intent Overzicht | `docs/architectuur/intent-overzicht.md` | Wat intents zijn en hoe gebruikersinvoer wordt geclassificeerd naar acties | | Implementatieplan Nieuwe Intents | `docs/architectuur/implementatieplan-nieuwe-intents.md` | Stappenplan: 8 aanraakpunten per intent met tijdsindicatie per type | ### 12.5 UX Research | Document | Pad | Beschrijving | |----------|-----|-------------| | UX Evaluatie Schaalbaarheid | `docs/intent/ux-evaluation-intent-scalability.md` | Dialoog PO/UX/klant over problemen van strikte hiërarchie | | UX Simulatie Next Level | `docs/intent/ux-simulation-intent-next-level.md` | Brainstormsessie: Command-Response → Proactive Assistant met input van zorgprofessionals | | UX Analyse Patiëntselectie | `docs/intent/patient-search/ux-analyse-patient-selectie.md` | Probleemdefinitie en oplossingen voor inline patiëntselectie-ervaring | | Bouwplan Patiëntselectie | `docs/intent/patient-search/bouwplan-patient-selectie-v1.md` | @mentions en persistent patient sidebar (19/19 user stories compleet) | ### 12.6 Intake-proces | Document | Pad | Beschrijving | |----------|-----|-------------| | Intake Process Intents | `docs/intent/intake-intent-proces/intake-process-intents.md` | Intake-proces met bijbehorende Cortex intents en UI-componenten | | Gap Analyse Intake-Cortex | `docs/intent/intake-intent-proces/gap-analyse-intake-cortex.md` | Backend 90% compleet; 26 intake intents en API routes ontbreken volledig | | Block Template Pattern | `docs/intent/intake-intent-proces/block-template-pattern.md` | Herbruikbaar patroon voor Cortex blocks met loading/success/error states | | Shared Components FO/TO | `docs/intent/intake-intent-proces/fo-to-cortex-shared-components.md` | Herbruikbare shared components en hooks voor alle Cortex Intake Blocks | | Bouwplan Shared Components | `docs/intent/intake-intent-proces/bouwplan-cortex-shared-components.md` | Foundation components die door alle 26 intake-intents worden hergebruikt | | Intake Blocks MVP FO/TO | `docs/intent/intake-intent-proces/fo-to-bouwplan-intake-blocks-mvp.md` | FO/TO/Bouwplan voor 4 meest waardevolle intake blocks | | Testplan Intake Blocks | `docs/intent/intake-intent-proces/testplan-intake-blocks-mvp.md` | Testplan voor intake_status, risico_query, diagnose_query en intake_navigeer | ### 12.7 Leeswijzer Voor een architect die het systeem voor het eerst benadert, is de aanbevolen leesvolgorde: ``` 1. Dit rapport (cortex-architectuur-analyse.md) └─ Geeft het volledige beeld: concept, architectuur, analyse, doelarchitectuur 2. PRD + Functioneel Ontwerp └─ prd-cortex-v2.md → fo-cortex-intent-system-v2.md └─ Geeft de product-context en gebruikersverwachtingen 3. Technisch Ontwerp + Architectuur Design └─ to-cortex-v2.md → design/architecture-cortex-v2.md └─ Geeft de technische verdieping per laag 4. Implementatieplan Nieuwe Intents └─ architectuur/implementatieplan-nieuwe-intents.md └─ Geeft het hands-on beeld van hoe een intent gebouwd wordt 5. Schaalbaarheid + Analyse └─ design/architecture-intent-scalability.md → design/analyse-intents-nudges.md └─ Geeft de toekomstvisie voor groei van 11 naar 50+ intents ```