Files
triqura-ecd/docs/architecture/intent-system-architecture.md
2026-07-09 23:16:28 +02:00

62 KiB
Raw Blame History

Intent-Driven Architecture for Healthcare Applications

Type: Architecture Reference Document Version: 1.0 Date: 2026-02-14 Audience: LLM agents, developers, architects building on this system Language: English (code and architecture), Dutch (domain examples)


How to Read This Document

This document describes an intent-driven architecture for healthcare applications. It is written to be consumed by both humans and LLMs. Each section follows a consistent pattern: what the component does, why it exists, how it works, and what trade-offs were made. Code examples illustrate concepts — they are not implementation specifications.

The architecture is domain-agnostic in its core (any application that translates natural language into structured actions) but makes specific decisions for healthcare contexts where safety, auditability, and clinical relevance are non-negotiable.


1. Problem Statement

1.1 The Core Tension

Healthcare workers think and communicate in unstructured language: "Jan refused his medication this morning." Healthcare systems require structured input: selecting the right form, the right category, the right patient, at the right time.

The gap between these two modes is where productivity is lost. Every form-fill, every menu navigation, every context switch from clinical thinking to system operation is friction that takes time away from patient care.

1.2 What This Architecture Solves

This architecture provides a pattern for building a command interface that sits between natural language input and structured system actions. It is not a chatbot (conversation is not the goal), not a search engine (retrieval is one action among many), and not an autonomous agent (the human always decides).

It does three things:

  1. Understand — Classify what the user wants (intent) and extract the relevant parameters (entities)
  2. Prepare — Open the right form with the right data pre-filled, ready for human confirmation
  3. Suggest — After an action completes, propose clinically relevant follow-ups based on protocols

1.3 Design Constraints from Healthcare

These constraints are non-negotiable and shape every architectural decision:

Constraint Implication
No autonomous writes The system never writes to the patient record without explicit human confirmation
Auditability Every classification, every action, every suggestion must be traceable
Graceful degradation If AI is unavailable, the system must still function (reduced capability, not failure)
Speed for routine tasks 70%+ of interactions are routine and must resolve in milliseconds, not seconds
Domain vocabulary is stable Medical terms, shift patterns, report categories change slowly — this favors rule-based approaches
Privacy by design Patient data in AI prompts must be minimized; logs must be sanitized

2. Architecture Overview

2.1 The Five Building Blocks

The system consists of five components, each with a single responsibility. They are described in the order data flows through them.

User Input (text or speech)
        │
        ▼
┌─────────────────────────┐
│   1. INTENT REGISTRY    │  Declarative definitions — the single source of truth
│      (static config)    │  for all intents, patterns, entities, and metadata
└────────────┬────────────┘
             │ provides definitions to ↓
┌─────────────────────────┐
│   2. CLASSIFICATION     │  Two-phase pipeline: fast local matching (Reflex)
│      PIPELINE           │  with LLM fallback (Orchestrator) for complex input
└────────────┬────────────┘
             │ produces classified intent + raw entities ↓
┌─────────────────────────┐
│   3. ENTITY RESOLUTION  │  Grounds raw entities against real data:
│                         │  "Jan" → Patient #427, "morgen" → 2026-02-15
└────────────┬────────────┘
             │ produces resolved intent ready for action ↓
┌─────────────────────────┐
│   4. ACTION SYSTEM      │  Maps intent → UI artifact (form, view, navigation)
│                         │  Human confirms → system executes → result
└────────────┬────────────┘
             │ after successful action ↓
┌─────────────────────────┐
│   5. PROTOCOL ENGINE    │  Evaluates clinical rules against completed action
│      (Nudge)            │  Suggests follow-ups with protocol references
└─────────────────────────┘

2.2 Why This Order Matters

The pipeline is strictly sequential for a reason. Each step produces a more refined representation:

  • Raw input → (Classification) → Intent + raw entities → (Resolution) → Grounded intent → (Action) → Prepared UI → (Human confirms) → Executed action → (Protocol) → Suggested follow-up

No step can be skipped. Classification without resolution produces ungrounded entities ("Jan" without knowing which Jan). Resolution without classification has nothing to resolve. Actions without human confirmation violate healthcare safety requirements. Protocol evaluation without completed actions has no trigger.

2.3 What Is NOT in This Architecture

Decisions about what to exclude are as important as what to include:

Excluded Reason
Custom ML model for classification Regex + LLM fallback outperforms custom models for <50 intents in a stable domain vocabulary. No training data maintenance overhead.
Event bus / pub-sub Overkill for single-tenant applications. Direct function calls and store subscriptions are simpler and equally effective.
Autonomous agent execution Healthcare requires human-in-the-loop for all record modifications. The system prepares; the human decides.
Ambient capture Passive listening during patient conversations is a fundamentally different input model. This architecture handles explicit commands (typed or spoken). Ambient capture is a future extension, not a foundation change.
FHIR server A FHIR-inspired data model is sufficient. Building a full FHIR-compliant server is a separate project that doesn't affect the intent architecture.
Knowledge graph for entity resolution Simple database lookups (fuzzy name search, date parsing) are sufficient for <1000 patients. Knowledge graphs add value at scale but premature complexity at MVP.

3. Building Block 1: Intent Registry

3.1 Purpose

The Intent Registry is the single source of truth for all intent definitions. Every other component in the system reads from the registry — nothing defines intent behavior independently.

3.2 Why This Matters

Without a registry, intent definitions drift apart. In the current prototype, an intent is defined in four separate locations: the TypeScript type union, the Reflex pattern map, the Orchestrator system prompt, and the Chat API system prompt. Adding a new intent requires synchronized changes to all four. This is the primary source of architectural debt.

The registry eliminates this by centralizing the definition and generating derived artifacts (pattern maps, prompts, validation schemas) from a single declaration.

3.3 Anatomy of an Intent Definition

interface IntentDefinition {
  // === Identity ===
  id: string;                    // Unique identifier, e.g. "dagnotitie"
  label: string;                 // Human-readable name (Dutch), e.g. "Dagnotitie"
  description: string;           // What this intent does (used in AI prompts)
  priority: 'P1' | 'P2' | 'P3'; // Classification priority tier

  // === Local Classification (Reflex) ===
  reflexPatterns: Array<{
    pattern: RegExp;
    weight: number;              // 0.5 (weak signal) to 1.0 (exact match)
  }>;

  // === Entity Schema ===
  entities: {
    required: string[];          // Must be present for action execution
    optional: string[];          // Enriches the action if present
    extractionRules: Array<{     // Regex with named capture groups
      pattern: RegExp;
      mapping: Record<string, string>; // capture group → entity field
    }>;
  };

  // === AI Classification ===
  aiExamples: Array<{
    input: string;               // Example user input
    expectedEntities: Record<string, string>;
  }>;

  // === Action Binding ===
  artifactType: string | null;   // Which UI artifact to open (null = navigation only)
  requiresConfirmation: boolean; // Must user confirm before execution?

  // === Access Control (prepared, not enforced in MVP) ===
  allowedRoles?: string[];       // Empty = everyone

  // === Feature Management ===
  featureFlag?: string;          // Optional flag to enable/disable
  enabled: boolean;              // Master switch
}

3.4 What the Registry Generates

Other components do not access the registry directly for their core logic. Instead, the registry exposes generator functions:

Function Consumer What it produces
getReflexPatterns() Classification Pipeline (Reflex phase) Map of intent → pattern/weight pairs
getExtractionRules() Entity Resolution Map of intent → regex extraction rules
buildClassificationPrompt() Classification Pipeline (Orchestrator phase) System prompt section listing all intents with descriptions and examples
buildChatPrompt() Chat API System prompt section for conversational context
getEntitySchema(intentId) Action System Zod validation schema for entity validation
getArtifactMapping() Action System Map of intent → artifact type

3.5 Trade-Off: Declarative vs. Distributed

Alternative considered: Each intent as a self-contained module (file per intent, co-located patterns + prompt + component). This is the "feature folder" pattern.

Why we chose centralized registry: In a healthcare context, the relationships between intents matter. Ambiguity detection requires comparing confidence scores across intents. RBAC filtering requires iterating all intents. Prompt generation needs the full catalog. A centralized registry makes these cross-cutting concerns natural. The cost is that adding an intent touches one large file instead of creating a new file — but that one file is the only file you need to touch.

When to reconsider: If the system grows beyond ~50 intents, the registry file becomes unwieldy. At that point, split into category-based sub-registries (clinical, administrative, navigation) that merge into a single runtime registry.


4. Building Block 2: Classification Pipeline

4.1 Purpose

The Classification Pipeline takes raw user input and produces a classified intent with confidence score and raw entities. It is the single entry point for all classification — both direct API calls and chat interactions use the same pipeline.

4.2 Two-Phase Design

Input + Context
      │
      ▼
┌──────────────────────────────────────────────────────┐
│  Phase 1: REFLEX (local, <20ms)                      │
│                                                      │
│  1. Check escalation triggers                        │
│     - Multi-intent signals ("en", "daarna", "dan")   │
│     - Pronouns ("hij", "zij", "hem", "haar")         │
│     - Relative time ("morgen", "volgende week")      │
│     → If triggered: skip to Phase 2 with best-guess  │
│                                                      │
│  2. Pattern matching against registry patterns       │
│     - Score each intent by highest matching weight   │
│     - Track top-2 for ambiguity detection            │
│     → If confidence ≥ 0.7 AND gap > 0.1: return     │
│     → Otherwise: escalate to Phase 2                 │
│                                                      │
│  3. Entity extraction via registry extraction rules  │
│     → Attach raw entities to result                  │
└──────────────────────────────────────────────────────┘
      │
      │ (only if escalation needed)
      ▼
┌──────────────────────────────────────────────────────┐
│  Phase 2: ORCHESTRATOR (AI, ~250ms-3s)               │
│                                                      │
│  1. Build prompt from registry (intent descriptions  │
│     + examples + entity schemas)                     │
│  2. Include application context:                     │
│     - Active patient (name, recent notes)            │
│     - Current shift (nacht/ochtend/middag/avond)     │
│     - Today's appointments                           │
│     - Recent intents (for pronoun resolution)        │
│  3. Send to LLM with structured output format        │
│  4. Validate response with Zod schema                │
│  5. Build IntentChain (single or multi-action)       │
│                                                      │
│  On failure: fall back to Reflex result,             │
│  cap confidence at 0.6                               │
└──────────────────────────────────────────────────────┘
      │
      ▼
ClassificationResult {
  intent, confidence, rawEntities,
  chain (if multi-action),
  source ('reflex' | 'orchestrator' | 'fallback'),
  processingTimeMs
}

4.3 Why Two Phases Instead of One

Cost: An LLM API call costs money and time. At 100 classifications per hour across an organization, pure-LLM classification becomes expensive. Reflex handles 70%+ locally at zero marginal cost.

Latency: Reflex responds in <20ms. The LLM takes 250ms-3s. For routine commands ("notitie jan medicatie"), the user should not wait for AI.

Resilience: If the LLM provider is down, Reflex still works. The system degrades from "intelligent" to "fast and reliable" instead of failing completely.

Debuggability: Regex patterns are deterministic and inspectable. When a classification is wrong, you can trace exactly which pattern matched and why. LLM reasoning is probabilistic and harder to debug.

4.4 Escalation Triggers

The Reflex phase checks for escalation triggers before doing pattern matching. This is a deliberate choice: if the input contains signals that require AI reasoning (multiple actions, pronouns, relative time), there is no point in building a local hypothesis that will be incomplete.

Trigger Detection Why AI is needed
Multi-intent Conjunctions: "en", "daarna", "dan", "ook" Reflex cannot split compound sentences
Pronouns "hij", "zij", "hem", "haar", "die", "deze" Requires context (active patient, recent actions) to resolve
Relative time "morgen", "overmorgen", "volgende week", weekday names Requires current date + calendar awareness

4.5 The Unified Pipeline Principle

Both the Classify API (direct classification) and the Chat API (conversational interaction) use the same pipeline. The difference:

  • Classify API: Returns the pipeline result directly as structured data
  • Chat API: Feeds the pipeline result as context to a conversational LLM, which generates a natural language response alongside the structured action

This means the Chat LLM does not need to classify — it receives the classification as input and only needs to formulate a response. This eliminates the problem of maintaining two separate classification systems with divergent intent definitions.

                    ┌─────────────────────┐
                    │ Classification      │
                    │ Pipeline            │
                    │ (single impl.)      │
                    └──────┬──────────────┘
                           │
              ┌────────────┴────────────┐
              ▼                         ▼
    ┌──────────────────┐     ┌──────────────────┐
    │ Classify API     │     │ Chat API         │
    │ Returns result   │     │ Passes result as │
    │ as JSON          │     │ context to LLM   │
    └──────────────────┘     │ for conversation  │
                             └──────────────────┘

4.6 Circuit Breaker for AI Resilience

The Orchestrator wraps AI calls with a simple circuit breaker:

State Machine:
  CLOSED (normal) ──[3 consecutive failures]──▶ OPEN (bypass AI)
  OPEN ──[30 seconds elapsed]──▶ HALF-OPEN (try one request)
  HALF-OPEN ──[success]──▶ CLOSED
  HALF-OPEN ──[failure]──▶ OPEN

When the circuit is open, all classifications are handled by Reflex alone (with confidence capped at 0.6). This prevents cascade failures when the AI provider is experiencing issues — instead of every request waiting 5 seconds for a timeout, the system instantly falls back.

4.7 Trade-Off: Regex vs. Embeddings for Phase 1

Current choice: Regex patterns with weights.

Alternative: Sentence embeddings (e.g., via a local model or API) that match input against intent descriptions by semantic similarity.

Why regex for now: The domain vocabulary in healthcare (especially GGZ — mental health) is stable and limited. "Notitie", "overdracht", "afspraak" are not ambiguous words. Regex captures these patterns with near-perfect accuracy and zero latency. Embeddings add infrastructure complexity (model hosting or API calls) without proportional accuracy improvement at <50 intents.

When to reconsider: If the Reflex hit rate drops below 60% — meaning more than 40% of inputs require AI escalation — the patterns are no longer covering the vocabulary. At that point, an embedding-based first pass (selecting top-10 candidate intents, then LLM for final classification) becomes worthwhile. This is the architecture Voiceflow benchmarked successfully in production.


5. Building Block 3: Entity Resolution

5.1 Purpose

Entity Resolution takes the raw entities from classification (strings like "jan", "morgen", "medicatie") and grounds them against real system data (Patient #427, 2026-02-15, category "medicatie").

5.2 Why This Is a Separate Step

In the current prototype, entity extraction and resolution are conflated — some happens in the Reflex, some in the Orchestrator, some in the UI components. By making it an explicit pipeline step, we gain:

  1. Testability: Resolution logic can be unit-tested with mock data
  2. Reusability: The same resolution logic serves both Classify and Chat paths
  3. Extensibility: Swapping a name-lookup for a FHIR Patient search requires changing only this layer
  4. Separation of concerns: Classification decides what the user wants; resolution grounds who/when/what they mean

5.3 Resolution Types

Entity Type Raw Value Resolution Method Resolved Value
Patient name "jan" Fuzzy search against patient database { id: "427", name: "Jan de Vries" }
Date/time "morgen 14:00" Date parsing with Dutch locale + current date { date: "2026-02-15", time: "14:00" }
Category "medicatie", "med", "medicijn" Alias mapping to canonical values "medicatie"
Navigation target "risico", "diagnose" Enum matching "risicotaxatie"
Shift "ochtend" Direct mapping "ochtend" (07:00-12:00)

5.4 Disambiguation

When resolution is ambiguous (multiple patients named "Jan"), the system produces a ClarificationRequest:

interface ClarificationRequest {
  type: 'patient_disambiguation' | 'appointment_disambiguation' | 'time_disambiguation';
  question: string;       // "Welke Jan bedoel je?"
  options: Array<{
    label: string;        // "Jan de Vries (kamer 12)"
    value: string;        // patient ID
  }>;
  originalIntent: string; // Preserved for retry after disambiguation
  originalEntities: Record<string, unknown>; // Preserved raw entities
}

The UI presents the options. After the user selects, the pipeline resumes with the resolved entity. The original intent and entities are preserved so the user doesn't need to repeat their command.

5.5 Context-Aware Resolution

Some entities resolve differently based on application context:

  • "Hij"/"zij" → resolves to activePatient from the context store
  • "De afspraak" → resolves to the most recent or next appointment in context
  • "Vandaag" → resolves to current date, but reports before 07:00 belong to previous day's shift

This context-awareness is what makes the Orchestrator (AI) necessary for some inputs — the Reflex phase cannot resolve context-dependent references.

5.6 Trade-Off: Inline vs. Explicit Resolution

Alternative considered: Letting each UI component resolve entities on mount (current approach — components fetch patient data when they receive a name string).

Why explicit resolution: In a multi-action chain ("notitie jan en verzet zijn afspraak"), the patient resolved in action 1 must be the same patient used in action 2. Inline resolution per component can produce inconsistencies. Explicit resolution at the pipeline level ensures a single resolution pass for the entire chain.


6. Building Block 4: Action System

6.1 Purpose

The Action System maps resolved intents to UI artifacts (forms, views, navigation targets) and manages the execution lifecycle. It is the bridge between "the system understands what you want" and "the system shows you the right form."

6.2 The Intent-Action Separation

An intent is what the user means. An action is what the system does. This separation matters because:

  • One intent can map to different actions depending on context (e.g., "overdracht" opens a summary view during shift start but generates a new summary at shift end)
  • One user utterance can produce multiple actions (IntentChain)
  • Actions have lifecycle states that intents do not (pending → confirming → executing → success/failed)

6.3 Action Types

There are three types of actions, each with different UI behavior:

Type What it does UI Behavior Example
Artifact Action Opens a form or data view Opens artifact panel with pre-filled data dagnotitie, create_appointment
Query Action Fetches and displays data Opens read-only artifact risico_query, diagnose_query, agenda_query
Navigation Action Redirects to a page Client-side navigation, no artifact intake_navigeer

6.4 The Confirmation Principle

No action that modifies patient data executes without human confirmation. This is the foundational safety principle.

In practice, this means:

  • High confidence (≥0.9), non-destructive: Open form with pre-filled data. User reviews and submits.
  • Medium confidence (0.7-0.9): Open form with pre-filled data + explicit confirmation banner: "Ik begrijp: dagnotitie voor Jan, categorie medicatie. Klopt dit?"
  • Low confidence (0.5-0.7): Ask clarification question before opening anything.
  • Very low confidence (<0.5): "Ik begrijp het niet. Kun je het anders zeggen?"
  • Destructive actions (cancel, delete): Always require explicit confirmation, regardless of confidence.

The system pre-fills but never auto-submits. The human sees what the system understood and decides whether it's correct.

6.5 IntentChain: Multi-Action Execution

When the Orchestrator detects multiple intents in one utterance ("Annuleer de afspraak van Jan en maak een notitie over zijn medicatie"), it produces an IntentChain — an ordered sequence of actions.

interface IntentChain {
  id: string;
  originalInput: string;
  actions: IntentAction[];       // Ordered sequence
  status: 'pending' | 'executing' | 'completed' | 'partial' | 'failed';
  meta: {
    source: 'reflex' | 'orchestrator' | 'fallback';
    processingTimeMs: number;
    aiReasoning?: string;        // Orchestrator's explanation (for debugging)
  };
}

Execution is strictly sequential: action 2 starts only after action 1 completes (or the user confirms action 1). Parallel execution is intentionally not supported — the user expects a logical order, and in healthcare, action order can matter clinically (e.g., cancel before reschedule).

6.6 Trade-Off: Pre-fill vs. Auto-Execute

Alternative considered: For very high confidence classifications (≥0.95), skip the form and execute directly (write the note, create the appointment).

Why we chose pre-fill only: In healthcare, the cost of a wrong write is high (incorrect patient record) while the cost of an extra confirmation click is low (fraction of a second). The risk-reward ratio strongly favors requiring confirmation. Additionally, displaying the pre-filled form serves as a transparency mechanism — the user sees exactly what the system understood, building trust over time.

Exception to consider for the future: Read-only queries (agenda_query, risico_query) could arguably execute without confirmation since they don't modify data. This is a reasonable optimization but not a priority.


7. Building Block 5: Protocol Engine (Nudge)

7.1 Purpose

The Protocol Engine evaluates clinical rules after a successful action and suggests relevant follow-up actions. It is the system's proactive intelligence — suggesting things the user didn't ask for but should consider based on clinical protocols.

7.2 Why This Is the Differentiator

Any chatbot can classify intents. Any form system can pre-fill data. What makes a healthcare command interface genuinely useful is domain intelligence: knowing that after wound care documentation, a follow-up wound check should be scheduled in 3 days per V&VN guidelines. This is where the system transitions from "faster data entry" to "clinical decision support."

7.3 Rule Structure

interface ProtocolRule {
  id: string;
  name: string;                    // Human-readable rule name

  // When to trigger
  trigger: {
    intent: string;                // Which completed intent triggers this rule
    conditions: Condition[];       // ALL must match (AND logic)
  };

  // What to suggest
  suggestion: {
    intent: string;                // The suggested follow-up intent
    message: string;               // User-facing suggestion text (Dutch)
    prefillEntities: (completedAction: Action) => Record<string, unknown>;
  };

  // Clinical grounding
  protocol?: {
    name: string;                  // "V&VN Richtlijn Wondzorg"
    reference: string;             // "§4.2 Controlebeleid"
    rationale: string;             // Why this suggestion matters
  };

  priority: 'low' | 'medium' | 'high';
  expiresAfterMs: number;         // Suggestion disappears after this time
  enabled: boolean;
}

interface Condition {
  field: string;                   // Entity field to check
  operator: 'equals' | 'contains' | 'matches' | 'exists';
  value?: string;                  // Expected value (not needed for 'exists')
}

7.4 Engine Design: Pure Function

The engine is a pure function: (completedAction, rules) → suggestions[]. It has no side effects, no state, and no knowledge of where rules come from. This makes it:

  • Testable: Pass mock actions and rules, assert on suggestions
  • Portable: Rules can come from hardcoded TypeScript objects (MVP), a database table (v2), or FHIR PlanDefinitions (enterprise)
  • Predictable: Same input always produces same output

7.5 Integration Point

The Protocol Engine runs after action completion, triggered by the Action System:

Action completes successfully
        │
        ▼
evaluateNudge(completedAction, protocolRules)
        │
        ▼
NudgeSuggestion[] (sorted by priority)
        │
        ▼
UI displays suggestion toast with:
  - Message ("Wondcontrole inplannen over 3 dagen?")
  - Protocol reference ("V&VN Wondzorg §4.2")
  - Accept button → opens new artifact with pre-filled entities
  - Dismiss button → logs dismissal for analytics

7.6 Example Rules (Healthcare / GGZ)

Rule Trigger Suggestion Protocol
Wound care follow-up dagnotitie + content contains "wond" Schedule wound check in 3 days V&VN Wondzorg §4.2
Medication change review dagnotitie + content contains "medicatie" + "gewijzigd" Schedule medication evaluation in 1 week FMS Polyfarmacie §3.1
Incident follow-up dagnotitie + category = "incident" Create incident report IGJ Meldcode §2
Risk reassessment risico_query viewed Schedule risk reassessment if >30 days old GGZ Standaarden §5.4
Intake completion intake_status viewed + missing sections Navigate to next incomplete section Intake protocol

7.7 Trade-Off: Rules Engine vs. AI-Generated Suggestions

Alternative considered: Instead of rule-based suggestions, let the LLM generate contextual suggestions based on the completed action and patient history.

Why we chose rules: Clinical suggestions must be traceable to specific protocols. When a nurse sees "Schedule wound check in 3 days," she needs to know why — and the answer must be a protocol reference, not "the AI thought it was a good idea." Rule-based suggestions are deterministic, auditable, and clinically grounded. AI-generated suggestions are creative but unverifiable.

Hybrid future: The rule engine could be augmented with an AI layer that proposes new rules based on patterns in action logs. These proposed rules would then be reviewed by clinical staff and added to the rule set if valid. The engine itself remains rule-based; AI contributes to rule discovery, not rule execution.

7.8 Path Toward Standards: CDS Hooks

The Protocol Engine is conceptually aligned with the HL7 CDS Hooks specification — a standard for Clinical Decision Support in healthcare. The mapping:

Cortex Concept CDS Hooks Equivalent
ProtocolRule CDS Service
Trigger (intent + conditions) CDS Hook (workflow trigger)
NudgeSuggestion CDS Card (suggestion type)
Protocol metadata Card.source

Migrating to CDS Hooks compatibility is not needed for MVP but is architecturally possible: express rules as FHIR PlanDefinitions, map triggers to CDS hook types, format output as CDS Cards. This enables integration with hospital CDS systems that already speak HL7.


8. Cross-Cutting Concerns

8.1 State Management

The system requires state management for five distinct domains. These should be separate stores (not one monolithic store) to maintain testability and single responsibility:

Store Responsibility Key State
Context Store Application context Active patient, shift, recent patients
Chat Store Conversational state Messages, streaming status, pending action
Artifact Store UI artifact management Open artifacts (max 3), active artifact
Action Store Intent chain execution Active chain, chain history
Nudge Store Protocol suggestions Active suggestions, accept/dismiss tracking

Stores communicate via subscriptions, not direct imports. Example: when the Action Store completes a chain, the Nudge Store's subscription triggers protocol evaluation.

8.2 Observability

Every classification produces a structured log event:

interface ClassificationEvent {
  timestamp: string;
  userId: string;
  input: string;              // Sanitized (PII removed or hashed)
  intent: string;
  confidence: number;
  source: 'reflex' | 'orchestrator' | 'fallback';
  escalationReason?: string;
  processingTimeMs: number;
  entities: Record<string, unknown>;  // Sanitized
}

Key metrics to track:

Metric Why It Matters
Reflex hit rate If <60%, patterns need expansion or embeddings should be considered
Orchestrator fallback rate High rate = AI is unreliable, circuit breaker effectiveness
Classification latency (P95) User experience indicator
Intent distribution Reveals which features are actually used
Nudge acceptance rate per rule Indicates whether suggestions are clinically useful

8.3 Audit Trail

In healthcare, every system interaction with patient context must be auditable:

interface AuditEvent {
  userId: string;
  action: 'classify' | 'resolve' | 'execute' | 'nudge_shown' | 'nudge_accepted' | 'nudge_dismissed';
  intent?: string;
  patientId?: string;         // Which patient was involved
  input?: string;             // Sanitized
  result?: string;            // Summary, not full response
  timestamp: string;
  sessionId: string;          // For correlating events within a session
  durationMs?: number;
}

Storage requirements: append-only table, no UPDATE or DELETE, minimum 5 years retention (NEN 7510 compliance for Dutch healthcare).

8.4 Privacy: PII in AI Prompts

When the Orchestrator sends context to the LLM, it includes patient names and potentially clinical information. Mitigations:

  1. Minimize context: Send only what's needed for classification (patient name, not full medical history)
  2. Data processing agreement: Ensure the AI provider's DPA guarantees no training on API data
  3. Log sanitization: All log events must strip or hash patient names and identifiers before storage
  4. Pseudonymization option: For high-security environments, replace patient names with tokens before sending to AI, resolve tokens after classification

8.5 Role-Based Access (Prepared, Not Enforced)

The Intent Registry includes an allowedRoles field per intent. This enables future RBAC filtering without architectural changes:

Input → Classification Pipeline → [RBAC Filter] → Action System
                                        │
                                        └─ If intent not allowed for user's role:
                                           → "Je hebt geen toegang tot deze functie"

The filter sits between classification and action: the system classifies the intent (so it knows what was attempted) but blocks execution if the user's role doesn't permit it. The blocked attempt is logged for audit purposes.


9. Scaling Considerations

9.1 Scaling Intents: From 11 to 50+

The current system has 11 intents. The architecture supports growth to 50+ with these considerations:

Dimension At 11 intents At 50 intents Mitigation
Reflex matching ~88 regex evaluations (<20ms) ~400 evaluations (~50ms) Keyword pre-filter: match first word against index, evaluate only candidate intents
AI prompt size ~2000 tokens ~8000 tokens Category-based prompt sections; only include relevant categories based on Reflex pre-classification
Registry file size ~300 lines ~1500 lines Split into category sub-registries that merge at runtime
Ambiguity Low (intents are distinct) Higher (more overlapping patterns) Increase pattern specificity; add disambiguation rules

9.2 Scaling Users

Component Bottleneck Mitigation
Reflex None (stateless, in-process) Scales with application instances
Orchestrator AI provider rate limits Circuit breaker prevents cascade; consider provider load balancing
Rate limiting Currently in-memory (lost on restart) Move to database-backed (Supabase table or Redis)
Audit logging Write volume Batch writes; async logging pipeline

9.3 AI Provider Independence

The system is currently coupled to Anthropic Claude. To reduce vendor lock-in:

interface LLMProvider {
  classify(input: string, systemPrompt: string, context: string): Promise<string>;
  stream(messages: Message[], systemPrompt: string): AsyncIterable<string>;
}

Two implementations (Anthropic, OpenAI) behind a single interface. Note that prompts are NOT fully portable between providers — the system prompt is optimized for Claude's behavior. Provider switching requires prompt re-tuning. This is inherent to LLM usage and cannot be fully abstracted.


10. Relationship to Healthcare Standards

10.1 FHIR Resource Mapping

Extracted entities ultimately map to FHIR-inspired resources:

Entity FHIR Resource Usage
patientName / patientId Patient Patient identification
category + content Observation or DocumentReference Clinical notes
datetime + appointmentType Encounter / Appointment Scheduling
diagnosis / risk Condition Clinical data queries

The system does not need to be a FHIR server. It needs a clear mapping layer so that when FHIR compliance becomes required, entity resolution can produce FHIR-compliant references.

10.2 NEN 7510 (Dutch Healthcare Information Security)

Requirement How This Architecture Addresses It
Access control RBAC prepared in Intent Registry; Supabase Auth + RLS on data
Audit logging Append-only audit events for all interactions
No PII in logs Sanitization step in logging pipeline
Encryption in transit HTTPS (standard for modern web apps)
Encryption at rest Database-level (Supabase managed)
Incident logging Classification failures logged as potential security events

11. Migration Path from Current Prototype

Phase 1: Foundation (Low Risk)

Goal: Intent Registry as single source of truth + entity extraction in Reflex

  1. Create intent-registry.ts with all 11 current intents as declarative objects
  2. Refactor reflex-classifier.ts to load patterns from registry instead of hardcoded map
  3. Add entity extraction rules to registry; implement extraction in Reflex phase
  4. Generate Orchestrator prompt from registry (replace hardcoded prompt)
  5. Verify: all existing classifications produce the same results (regression test)

Risk: Low — refactoring without behavior change (except added entity extraction).

Phase 2: Unification (Medium Risk)

Goal: Single classification pipeline serving both Classify and Chat APIs

  1. Extract classifyInput() as standalone pipeline function
  2. Modify Chat API to use pipeline for classification (remove duplicate intent logic from chat prompt)
  3. Simplify Chat system prompt to conversational role only (no classification responsibility)
  4. Fix AbortController signal propagation (currently disconnected from actual fetch)
  5. Add circuit breaker to Orchestrator

Risk: Medium — Chat behavior may subtly change. Requires thorough testing with existing scenarios.

Phase 3: Activation (Low Risk)

Goal: Nudge system actually running; state management cleaned up

  1. Split monolithic Zustand store into 5 domain stores
  2. Wire evaluateNudge() to fire after action completion (via store subscription)
  3. Add 3-5 clinical protocol rules
  4. Implement nudge tracking (shown/accepted/dismissed)

Risk: Low — new functionality, no breaking changes to existing features.

Phase 4: Production Readiness (Low Risk)

Goal: Observable, auditable, resilient

  1. Structured logging (replace console.log)
  2. Classification metrics tracking
  3. Audit events to database
  4. Database-backed rate limiting
  5. PII sanitization in logs

Risk: Low — cross-cutting infrastructure, no functionality changes.


12. Decision Log

This section records key architectural decisions with their rationale, for future reference.

D1: Three-Layer Model (Reflex → Orchestrator → Nudge)

Decision: Use a tiered classification model with local fast path and AI fallback, plus protocol-driven post-action suggestions.

Rationale: Optimizes for the common case (70%+ routine commands handled locally in <20ms) while preserving AI capability for complex input. Nudge layer adds domain value beyond classification. This pattern is validated by industry benchmarks (Voiceflow 2025) showing hybrid NLU+LLM outperforms pure approaches on both cost and accuracy.

Alternatives rejected: Pure LLM (too slow and expensive for routine commands), pure rule-based (cannot handle multi-intent, pronouns, relative time), custom ML model (training data overhead without proportional accuracy gain in stable domain).

D2: Human-in-the-Loop for All Write Operations

Decision: The system never writes to patient records without explicit human confirmation. Pre-fill forms, never auto-submit.

Rationale: Healthcare requires accountability. The cost of a wrong write (incorrect patient record) far exceeds the cost of an extra confirmation click. This also builds user trust — the system is transparent about what it understood.

Alternatives rejected: Auto-execution for high-confidence classifications (>0.95). Rejected because even at 99% accuracy, 1 wrong write per 100 interactions is unacceptable in a clinical context.

D3: Centralized Intent Registry over Feature Folders

Decision: Single registry file over one-file-per-intent module structure.

Rationale: Cross-cutting concerns (ambiguity detection, RBAC filtering, prompt generation) need the full intent catalog. A centralized registry makes these natural. The cost (one large file) is manageable at <50 intents and can be split into sub-registries later.

D4: Regex over Embeddings for Phase 1 Classification

Decision: Use regex patterns with weights for the Reflex phase.

Rationale: Domain vocabulary is stable and limited (medical terms, fixed workflows). Regex achieves near-perfect accuracy for known patterns at zero latency. Embeddings add infrastructure complexity without proportional accuracy improvement at current scale.

Reconsideration trigger: Reflex hit rate drops below 60%.

D5: Rule-Based Protocol Engine over AI-Generated Suggestions

Decision: Nudge suggestions are deterministic, based on explicitly defined protocol rules.

Rationale: Clinical suggestions must be traceable to specific protocols. Rule-based suggestions are auditable and verifiable. AI-generated suggestions are creative but clinically unverifiable.

Future hybrid: AI proposes candidate rules from action log patterns; clinical staff reviews and approves; engine remains rule-based.

D6: Sequential Multi-Intent Execution over Parallel

Decision: Actions in an IntentChain execute sequentially, never in parallel.

Rationale: Users expect logical ordering ("cancel appointment AND make note" implies cancel first). In healthcare, action order can matter clinically. Sequential execution is predictable and debuggable.


13. Platform Model: From Intent System to Healthcare Application

13.1 The Four Layers

The complete system consists of four layers. The intent system (this document) is layer 1. Three additional layers build on top to form a healthcare application.

┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│   LAYER 4: EPD APPLICATION                                      │
│   The application the healthcare worker sees and uses           │
│                                                                 │
│   Reporting │ Patient Record │ Calendar │ Handover │ Intake     │
│   UI modules, block components, API routes, data model          │
│                                                                 │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   LAYER 3: CARE DOMAIN CONFIGURATION                            │
│   The knowledge of the specific care type                       │
│                                                                 │
│   Intent definitions (dagnotitie, overdracht, agenda, ...)     │
│   Entity schemas (patient, category, shift, ...)               │
│   Domain vocabulary (GGZ terms, report categories)             │
│   Protocol rules (nudges specific to this care type)           │
│                                                                 │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   LAYER 2: KNOWLEDGE LAYER                                      │
│   External data sources that feed the Protocol Engine          │
│                                                                 │
│   ┌───────────┐ ┌──────────────┐ ┌────────────┐ ┌───────────┐ │
│   │ Laws &    │ │ Treatment    │ │ Quality    │ │ EHR Data  │ │
│   │ Regula-   │ │ Protocols    │ │ Documents  │ │ (own      │ │
│   │ tions     │ │              │ │            │ │  patient  │ │
│   │           │ │              │ │            │ │  data)    │ │
│   └───────────┘ └──────────────┘ └────────────┘ └───────────┘ │
│                                                                 │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   LAYER 1: INTENT SYSTEM (this document)                        │
│   The generic engine                                            │
│                                                                 │
│   Intent Registry │ Classification Pipeline │ Entity Resolution │
│   Action System │ Protocol Engine (Nudge)                       │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

13.2 Why the Knowledge Layer Matters

The Protocol Engine (Building Block 5) generates nudges based on rules. But those rules don't come from nowhere — they are derived from external knowledge sources. The quality of nudges is directly dependent on the quality and accessibility of these sources.

Without the Knowledge Layer, the Protocol Engine is an empty engine: the mechanics work, but there's no fuel. The Knowledge Layer is that fuel.

13.3 The Four Knowledge Sources

13.3.1 Laws and Regulations

Laws and standards that regulate healthcare practices. These determine what must happen.

Source Example Nudge type it produces
Wlz (Long-term Care Act) Care plan requirements, evaluation deadlines "Care plan evaluation expires in 2 weeks"
Wvggz (Compulsory Mental Healthcare Act) Reporting obligations, coercive measure deadlines "Coercive measure expires in 3 days — renewal needed?"
Wkkgz (Quality, Complaints and Disputes in Healthcare Act) Incident reporting obligations "Incident registered — IGJ report required within 3 business days"
NEN 7510 / GDPR Record retention periods, access logging "Audit log: 3 staff members viewed this record"

Characteristics: Changes slowly (legislative amendments), high reliability, publicly available, legally binding.

Technical implication: These rules are stable enough to be hardcoded protocol rules in the engine. They change perhaps 1-2 times per year.

13.3.2 Treatment Protocols

Clinical guidelines and standards describing how care should be delivered.

Source Example Nudge type it produces
GGZ Standards Care Standard Depression, Psychosis, etc. "Patient with depression — ROM measurement scheduled in 6 weeks?"
V&VN Guidelines Wound care, medication management, fall prevention "Schedule wound check in 3 days (V&VN §4.2)"
FMS Guidelines Polypharmacy, pain management "Medication evaluation in 1 week (FMS Polypharmacy §3.1)"
Multidisciplinary guidelines MDO frequency, treatment plan discussions "MDO scheduled in 2 weeks — start preparation?"

Characteristics: Changes regularly (new guideline versions), high reliability, publicly available, requires interpretation by clinical staff.

Technical implication: These sources are too extensive to manually encode as rules. This is where a RAG component (Retrieval Augmented Generation) comes in: protocol texts are indexed, and for relevant actions the LLM is asked to formulate specific nudge suggestions based on protocol text. These suggestions are then validated by clinical staff and added as rules.

13.3.3 Quality Documents

Internal documents of the healthcare organization describing how this specific organization delivers care.

Source Example Nudge type it produces
Institution protocols Medication distribution protocol "Double check medication required (house protocol §2.3)"
Work agreements Handover moments, reporting frequency "Handover report not completed — shift ends in 1 hour"
Quality handbook Audit moments, improvement plans "Internal audit next month — update action plan"
Accreditation requirements HKZ, PREZO, Planetree "Client satisfaction survey — 3 patients not yet surveyed"

Characteristics: Organization-specific (not public), changes with policy decisions, requires internal coordination.

Technical implication: Most challenging source because it differs per institution. Options: manual rule creation, semi-automatic via RAG + validation, or hybrid (standard ruleset + organization-specific additions).

13.3.4 EHR Data (Own Patient Data)

Data already in the system — patient records, reports, appointments, treatment plans.

Data source Example Nudge type it produces
Report history No report in 48 hours "No daily note for Jan de Vries in 2 days"
Treatment plan Evaluation date expired "Treatment plan evaluation Jan — 1 week past deadline"
Medication overview Drug interactions, changes "New medication prescribed — interaction check needed"
Risk assessment Expired risk assessment "Risk assessment Marie — last evaluation 45 days ago"
Calendar Missed appointments, no-shows "Jan 2x no-show this month — follow up?"
Lab/measurements ROM scores, vital signs "PHQ-9 score increased from 12 to 18 — adjust treatment plan?"

Characteristics: Real-time, patient-specific, always available (it's in your own database), privacy-sensitive.

Technical implication: The only source requiring no external integration — it's already in Supabase. The Protocol Engine can run direct queries against existing tables. This is the simplest and most impactful source to start with.

13.4 How the Knowledge Layer Feeds the Protocol Engine

┌─────────────────────────────────────────────────────────────────┐
│                     KNOWLEDGE LAYER                              │
│                                                                  │
│  ┌─────────────────┐     ┌─────────────────────────────────┐   │
│  │ Static rules    │     │ Dynamic rules                    │   │
│  │                 │     │                                  │   │
│  │ Laws &          │     │ EHR data queries                │   │
│  │ regulations     │     │ (report gaps, expired            │   │
│  │ (hardcoded,     │     │  assessments, no-shows)          │   │
│  │  rarely changes)│     │                                  │   │
│  │                 │     │ Treatment protocols via RAG     │   │
│  │ National        │     │ (indexed guideline texts         │   │
│  │ guidelines      │     │  → AI-generated candidate        │   │
│  │ (V&VN, GGZ Std) │     │  rules → human validation)      │   │
│  │                 │     │                                  │   │
│  │ Institution     │     │ Quality documents               │   │
│  │ protocols       │     │ (uploaded per institution,       │   │
│  │ (configured at  │     │  indexed, AI-suggested)          │   │
│  │  onboarding)    │     │                                  │   │
│  └────────┬────────┘     └────────────┬─────────────────────┘   │
│           │                            │                         │
│           ▼                            ▼                         │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │              PROTOCOL RULES REGISTRY                      │   │
│  │                                                          │   │
│  │  All rules — regardless of source — in the same format: │   │
│  │  trigger + conditions + suggestion + protocol metadata   │   │
│  │                                                          │   │
│  │  Each rule contains:                                     │   │
│  │  - source: 'legislation' | 'guideline' | 'institution' │   │
│  │           | 'ehr_data' | 'ai_suggested'                 │   │
│  │  - validatedBy: userId (who approved the rule)          │   │
│  │  - validatedAt: timestamp                                │   │
│  │  - version: rule version (for audit trail)              │   │
│  └──────────────────────────┬───────────────────────────────┘   │
│                              │                                   │
└──────────────────────────────┼───────────────────────────────────┘
                               │
                               ▼
                    ┌──────────────────────┐
                    │   PROTOCOL ENGINE    │
                    │   (Building Block 5) │
                    │                      │
                    │   evaluateNudge()    │
                    └──────────────────────┘

13.5 Implementation Order for the Knowledge Layer

Step 1: EHR data (own database) — Start here. No external integration needed. Supabase queries on your own tables. Direct value, low complexity.

Step 2: Laws & national guidelines — Hardcoded rules. Stable sources, publicly available. 20-30 rules that apply to every GGZ institution. One-time work reusable across all clients.

Step 3: Institution protocols — Configurable per client. At onboarding: upload their protocols, translate to rules (manually or semi-automatically).

Step 4: RAG on treatment protocols — AI-assisted. Index full text of GGZ Standards, V&VN guidelines, etc. Use RAG to retrieve protocol texts and generate candidate nudges. Clinical staff validates.

13.6 Trade-Off: Rules vs. RAG

Aspect Hardcoded rules RAG-generated suggestions
Accuracy 100% (human validated) ~80-90% (AI-generated, requires validation)
Scalability Low (manual per rule) High (automatic from documents)
Traceability Perfect (rule → protocol §) Good (RAG source + AI reasoning)
Startup cost Low per rule, high for completeness High for setup, low per rule after
Maintenance Manual on guideline changes Semi-automatic (reindex + revalidate)

Recommendation: Start with hardcoded rules (steps 1-3). Build RAG when the ruleset grows beyond what's manually manageable (~50+ rules per institution). The rule format is the same — the source changes, the engine does not.

13.7 The Knowledge Layer as Product Differentiator

This is where it becomes commercially interesting. The Knowledge Layer differentiates between:

  • Generic intent system: "I understand your command and open the right form" — anyone can build this.
  • Care-specific platform: "I know that after wound care, a check is needed in 3 days per V&VN §4.2, and that the Wlz requires care plan evaluation within 6 weeks" — this requires domain knowledge.
  • Institution-specific system: "I know that your house protocol requires dual-person medication verification, and that your next internal audit is in 3 weeks" — this requires organizational knowledge.

Each layer adds value. The intent system (layer 1) is the engine anyone can build. The Knowledge Layer (layer 2) is the flywheel that's hard to copy.

13.8 Layer Separation and Reusability

The four layers are deliberately decoupled:

If you replace... Then changes... And stays unchanged...
Layer 4 (EPD application) UI, API routes, data model Intent system, Knowledge Layer, Domain config
Layer 3 (Care domain) Intents, entities, vocabulary Intent system, Knowledge Layer, EPD application
Layer 2 (Knowledge Layer) Protocol rules, data sources Intent system, Domain config, EPD application
Layer 1 (Intent system) Classification, resolution, actions Knowledge Layer, Domain config, EPD application

Concrete example: switching from GGZ to home care?

  • Layer 1 stays identical (same engine)
  • Layer 2 gets different sources (home care protocols instead of GGZ Standards)
  • Layer 3 gets different intents (care_moment_registration instead of dagnotitie, route_planning instead of agenda)
  • Layer 4 gets a different UI (home care app instead of EPD dashboard)

Appendix A: Glossary

Term Definition
Intent A classified user intention: what the user wants to do (e.g., dagnotitie, create_appointment)
Entity A parameter extracted from user input (e.g., patient name, date, category)
Confidence A score (0.01.0) indicating how certain the classification is
Reflex The fast, local classification phase using regex patterns
Orchestrator The AI-powered classification phase using an LLM
IntentChain An ordered sequence of actions derived from a single user utterance
Artifact A UI panel (form, data view) opened in response to an intent
Block A React component that renders a specific artifact type
Nudge A proactive suggestion generated by the Protocol Engine after action completion
Protocol Rule A declarative rule that triggers a nudge based on completed action + conditions
Entity Resolution The process of grounding raw entity strings against real system data
Escalation The decision to send input from Reflex to Orchestrator for AI classification
Circuit Breaker A resilience pattern that bypasses AI calls after consecutive failures

Appendix B: Current Intent Catalog

Intent Priority Category Artifact Confirmation
dagnotitie P1 Clinical Form (note creation) No (pre-fill only)
zoeken P1 Navigation Search results view No
overdracht P1 Clinical Handover summary No
agenda_query P2 Administrative Calendar view No
create_appointment P2 Administrative Appointment form No (pre-fill)
cancel_appointment P2 Administrative Confirmation dialog Yes (destructive)
reschedule_appointment P2 Administrative Appointment form Yes (modifying)
intake_status P3 Clinical Intake progress view No
intake_navigeer P3 Navigation Page navigation No (no artifact)
risico_query P3 Clinical Risk assessment view No
diagnose_query P3 Clinical Diagnosis list view No

Appendix C: References