Skip to content

05 — EHR Builder: Real-Time Record Assembly

The EHR Builder assembles a complete, structured patient health record from heterogeneous, asynchronous data sources. It operates in real-time — every new data point triggers an event that updates the record.

Data Source Taxonomy

Source Channel Data Types Arrival Pattern
Chat messages POST /chat (Intake Agent) Demographics, symptoms, preferences, history Interactive, conversational
Document uploads R2 presigned + QStash OCR Lab values, imaging reports, diagnoses, meds Async, potentially delayed
Clinical Context Agent Auto-triggered on document_parsed FHIR resources (Condition, Procedure, Observation) Async, seconds after parse
Patient profile edits PUT /api/v1/patients/{id} Contact, insurance, travel preferences Direct REST, immediate
Consent events POST /api/v1/consent Processing permissions, scope changes Event-driven, immediate

Real-Time Assembly Pattern

Data Source (chat / document / agent / API)
Typed Event Emitted (e.g., clinical_entities_extracted, chat_data_collected)
EHR Builder Service subscribes and processes
    ├──▶ Validate incoming data against FHIR R4 schema
    ├──▶ Conflict detection (compare new vs existing resources)
    ├──▶ Apply merge rules (see below)
    ├──▶ Store/update FHIR resources in PostgreSQL
    ├──▶ Update Neo4j graph (add/update nodes and relationships)
    ├──▶ Index in Qdrant (update patient clinical embedding)
    ├──▶ Recalculate intake_progress
SSE notification to frontend (EHR summary view refreshes)

EHR Builder Service Interface

class EHRBuilderService:
    async def process_event(self, event: Event) -> EHRUpdateResult:
        """Main entry point. Dispatches to appropriate handler based on event_type."""

    async def merge_clinical_entities(
        self, patient_id: str, tenant_id: str,
        entities: list[ClinicalEntity], source: str, confidence: float
    ) -> list[FHIRResource]:
        """Merge extracted entities into patient EHR with conflict resolution."""

    async def update_from_chat(
        self, patient_id: str, tenant_id: str,
        extracted_data: dict, source_message_id: str
    ) -> EHRUpdateResult:
        """Process structured data extracted from chat messages."""

    async def calculate_intake_progress(
        self, patient_id: str, tenant_id: str,
        procedure_code: str | None = None
    ) -> float:
        """Calculate intake completion percentage based on procedure requirements."""

    async def get_ehr_summary(
        self, patient_id: str, tenant_id: str
    ) -> EHRSummary:
        """Return complete EHR summary for display in left panel."""

    async def detect_conflicts(
        self, patient_id: str, existing: list[FHIRResource],
        incoming: list[FHIRResource]
    ) -> list[ConflictRecord]:
        """Detect conflicts between existing and incoming data."""

Data Merging Rules

Rule 1: Same source, newer data

Update with audit trail. Previous value preserved in event history. Example: patient corrects their weight in chat — old value kept in events, new value becomes canonical.

Rule 2: Different sources, same field

Prefer structured data (FHIR from Clinical Context Agent) over conversational data (Intake Agent chat extraction). Both stored. Higher-confidence source is canonical.

Priority order: 1. Clinical Context Agent FHIR output (highest — validated against schema) 2. Direct document extraction (structured, from source material) 3. Chat extraction by Intake Agent (lower — conversational, may be imprecise) 4. Patient self-reported (lowest — unvalidated)

Rule 3: Conflicting clinical data

Store both with confidence scores. Flag for clinical review if confidence delta exceeds threshold (configurable, default 0.3). Example: Document A says "hypertension" (confidence 0.92), chat says "no hypertension" (confidence 0.7) → flag conflict, keep both, prefer document.

Rule 4: Missing required fields

Tracked in intake_progress. Intake Agent proactively asks for missing information in next conversation turn. Required fields determined by procedure-specific checklist (see Document Checklist).

Intake Progress Calculation

def calculate_progress(patient_id: str, procedure_code: str) -> float:
    """
    Calculate how complete the patient's intake is.
    Returns 0.0 to 1.0.

    Categories and weights:
    - Demographics (0.15): name, DOB, gender, nationality, contact
    - Medical history (0.25): conditions, surgeries, medications, allergies
    - Procedure-specific (0.25): required tests, imaging, specialist referrals
    - Preferences (0.10): language, dietary, gender preference, budget
    - Documents (0.15): required docs uploaded and parsed
    - Consent (0.10): all required consent purposes granted

    Each category: (fields_filled / fields_required) * weight
    Total: sum of all category scores
    """

Document Checklist & Anomaly Detection

Procedure-specific checklist for each patient case.

TKR Checklist (Aisha)

Document Category Status Logic
Knee X-ray (AP + lateral) Required Block matching without it
Complete blood panel Required Block matching without it
ECG/EKG Required Block matching without it
HbA1c Conditional (diabetes) Required because E11 comorbidity
Fasting glucose Conditional (diabetes) Required because E11 comorbidity
Cardiac clearance Conditional (age >70) Not required for Aisha
MRI knee Recommended Enhances match quality
Previous ortho consultation Recommended Enhances clinical context

Anomaly Detection Flags

Anomaly Detection Logic Action
Expired document upload_date + validity_days < today Flag, suggest re-upload
Lab value out of range Compare against procedure-specific reference ranges Flag, add to clinical context
Missing required document Not in document_references for this procedure Block matching, prompt upload
Conflicting diagnoses Different ICD codes from different documents Flag for review, keep both
Incomplete extraction Clinical Context Agent confidence < 0.5 Queue for re-extraction with Sonnet

Frontend: EHR Summary View (Left Panel)

The left panel shows a real-time EHR summary organized by category:

┌─────────────────────────┐
│ Patient: Aisha          │
│ Progress: ████████░░ 78%│
├─────────────────────────┤
│ ▼ Conditions            │
│   • M17.11 OA right knee│
│     Source: X-ray report │
│     Confidence: 0.95     │
│   • E11 Type 2 diabetes │
│     Source: Chat         │
│     Confidence: 0.88     │
│   • I10 Hypertension    │
│     Source: Chat         │
│     Confidence: 0.85     │
├─────────────────────────┤
│ ▼ Medications           │
│   • Metformin 1000mg BID│
│   • Losartan 50mg QD    │
├─────────────────────────┤
│ ▼ Documents       3/5   │
│   ✅ Knee X-ray         │
│   ✅ Blood panel        │
│   ✅ HbA1c              │
│   ⬜ ECG (required)     │
│   ⬜ MRI (recommended)  │
├─────────────────────────┤
│ ▼ Allergies             │
│   • Latex (moderate)    │
└─────────────────────────┘

Updates in real-time via SSE ehr_update events.