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.