Highlights
Healthcare is where VAIA systems face their highest-stakes deployment: a wrong output can harm a patient, a data leak triggers federal penalties, and every decision needs to be explainable to a clinician. This lesson cuts through the compliance fog and builds two production-grade agents — a Medical Coding Assistant and a Clinical Summary Agent — inside a firewalled, HIPAA-conscious deployment.
What we build:
A
MedicalCodingAgentthat maps clinical text → ICD-10 and CPT codes using deterministic rule gates + LLM rationale generationA
PHIRedactorpipeline that strips Protected Health Information before any text reaches an external modelA
HIPAAComplianceLayerthat wraps every tool call with consent checks, minimum-necessary enforcement, and append-only audit loggingA
ClinicalNERExtractorusing spaCy medical models to detect diagnoses, medications, and procedures in unstructured notesA firewalled React dashboard for administrative staff with role-based views
Connection to L84 (Financial Agents):
L84 gave us VerticalAgentBase, ComplianceAuditStore, TracingMiddleware, ToolCallSanitizer, and RegulatoryReportRenderer. Healthcare inherits all of these and extends them: ComplianceAuditStore gains HIPAA-specific hash-chaining fields; ToolCallSanitizer gets a PHI-aware pre-pass; RegulatoryReportRenderer gains CMS and ONC output templates. The pattern — deterministic compliance logic, LLM for rationale only — is unchanged, deepened.
Enables L86 (Legal Agents):
The PHIRedactor architecture becomes PIIRedactor for legal documents; HIPAAComplianceLayer is refactored into a general RegulatoryComplianceLayer that L86 parameterizes with legal ethics rules; ClinicalNERExtractor transforms into LegalEntityExtractor for contract clause detection.
Architecture Context
Place in the 90-Lesson VAIA Path
Lessons 76–90 are vertical case studies — real industries, real regulatory bodies, real failure modes. L84 tackled financial crime (AML, FinCEN); L85 enters healthcare (HIPAA, CMS, ONC CURES Act); L86 will cover legal (bar ethics rules, attorney-client privilege). The pattern sharpens with each vertical: compliance logic becomes more domain-specific, the “hybrid determinism” principle becomes non-negotiable, and deployment topology shifts toward air-gapped or network-isolated architectures.
Integration with L84 Components
L84 VerticalAgentBase
└─ extended by MedicalCodingAgent
├─ inherits: tool_call_trace(), compliance_check(), audit_log()
└─ adds: phi_redact_pre_hook(), consent_gate(), icd_validate()
L84 ComplianceAuditStore (SQLite, hash-chained)
└─ extended by SecureAuditStore (HIPAA)
├─ adds: phi_access_reason column
├─ adds: minimum_necessary_flag column
└─ enforces: 6-year HIPAA retention via retention_policy trigger
L84 ToolCallSanitizer
└─ extended by PrivacyGuardMiddleware
└─ adds: PHIRedactor pre-pass on all tool inputs/outputsModule Objectives Alignment
Module 9 (L76–L90) builds the skill of reading a regulatory framework and translating it into agentic architecture decisions. HIPAA teaches:
Minimum Necessary Standard → agents must request only the data fields required for the task
Audit Controls → every PHI access must be logged with reason and accessor identity
Transmission Security → no PHI crosses a network boundary unencrypted or unredacted
Business Associate Agreements → any external API (including LLM providers) is a Business Associate; avoid sending raw PHI to cloud LLMs
Core Concepts
1. The HIPAA Constraint That Reshapes Everything
The single most important architectural decision in healthcare AI is: PHI must never reach an external LLM in raw form. This isn’t paranoia — it’s HIPAA’s Transmission Security rule (45 CFR §164.312(e)). The moment you POST a patient note to a cloud API, you need a signed BAA with that provider. Even with a BAA, the risk surface is significant.
The production pattern that resolves this tension is PHI-stripped reasoning:
Raw clinical note → PHIRedactor → De-identified note → LLM (Gemini)
↓
Code lookup ← Structured output ← ICD suggestion
↓
Validation gate (deterministic)
↓
Final codes (no PHI, auditable)The LLM never sees the patient name, MRN, DOB, or any of HIPAA’s 18 identifiers. It sees: “65-year-old with chest pain, ST elevation in V2-V4, troponin elevated.” That’s enough to suggest ICD-10 I21.09 (STEMI). The deterministic validator confirms the code exists, checks it against the diagnosis context, and only then persists the result.
2. Hybrid Determinism in Medical Coding
Medical coding (assigning ICD-10 diagnosis codes and CPT procedure codes) is high-stakes: wrong codes cause claim denials, fraud investigations, and patient harm. The hybrid pattern from L84 applies perfectly:
The LLM proposes; deterministic validators accept or reject. A coder reviews the output; the agent never auto-submits to a payer. This is the “human-in-the-loop for submission” pattern required for CMS compliance.
3. Minimum Necessary Enforcement
HIPAA’s Minimum Necessary Standard requires that agents access only the PHI fields needed for the specific task. A coding agent needs: chief complaint, diagnoses, procedures. It does not need: social security number, insurance details, emergency contact. The HIPAAComplianceLayer enforces this at the tool call level:
# Tool call arrives requesting full patient record
tool_call = {"tool": "get_patient_record", "patient_id": "P-12345"}
# HIPAAComplianceLayer intercepts
allowed_fields = TASK_FIELD_MAP["medical_coding"] # ["chief_complaint", "diagnoses", "procedures"]
filtered_result = {k: v for k, v in record.items() if k in allowed_fields}
# SSN, insurance_id, emergency_contact → stripped before returning to agentThis isn’t optional — it’s the difference between a compliant system and one that’s over-collecting PHI.
4. Firewalled Deployment Topology
Healthcare enterprises (hospitals, payers, provider groups) typically cannot use public cloud LLM APIs for anything touching patient data without extensive compliance review. The deployment pattern that works:
[Internal Clinical Systems (EHR)]
│ (HL7 FHIR R4 over TLS, internal network only)
▼
[PHI Processing Zone — firewalled]
PHIRedactor → ClinicalNERExtractor
│ (de-identified text only crosses this boundary)
▼
[DMZ / API Gateway]
HIPAAComplianceLayer → MedicalCodingAgent → Gemini API (no PHI)
│
▼
[Audit Zone]
SecureAuditStore (append-only, hash-chained, 6-year retention)The firewall boundary between the PHI Processing Zone and the DMZ is the critical enforcement point. Nothing with PHI crosses it.
5. Clinical NER: Why spaCy Over Pure LLM
A tempting shortcut is asking the LLM to extract medical entities directly from the note. The problem: you’d be sending PHI to the model to do the extraction. ClinicalNERExtractor uses spaCy’s en_core_med7_lg model (runs locally, no network call) to extract entity types and de-identify them before any LLM interaction. Performance on clinical notes: ~0.89 F1 on MIMIC-III benchmark for diagnosis extraction. Good enough for a coding assist workflow; not good enough for autonomous coding without human review.
Implementation
GitHub Link
https://github.com/sysdr/vertical-ai-agent-p/tree/main/lesson85/vaia-l85-healthcare
Component Architecture
The system has four logical zones:
PHI Ingestion Zone: FHIR-compliant intake, PHIRedactor, ClinicalNERExtractor
Agent Execution Zone: MedicalCodingAgent, ClinicalSummaryAgent, HIPAAComplianceLayer
Validation Zone: ICD10Validator, CPTValidator, ConsentGate
Persistence Zone: SecureAuditStore, de-identified result cache
Workflow and Dataflow
A clinical note enters → PHIRedactor strips 18 HIPAA identifiers → spaCy NER extracts medical entities → de-identified structured context flows to MedicalCodingAgent → Gemini suggests codes with rationale → ICD10Validator/CPTValidator confirm → HIPAAComplianceLayer logs the decision → result returned to UI with confidence scores.
State Machine
MedicalCodingAgent states: IDLE → PHI_INGESTED → REDACTING → NER_EXTRACTION → LLM_CODING → VALIDATING → AWAITING_REVIEW → APPROVED → AUDIT_LOGGED → COMPLETE. Error states: REDACTION_FAILED → IDLE; VALIDATION_FAILED → AWAITING_REVIEW (escalate to human coder); CONSENT_DENIED → AUDIT_LOGGED.
Coding Highlights
PHI Redaction Pipeline
class PHIRedactor:
"""
Detects and replaces HIPAA's 18 PHI identifiers.
Uses regex + spaCy NER. Runs entirely locally — no network.
"""
PHI_PATTERNS = {
"NAME": r"\b[A-Z][a-z]+ [A-Z][a-z]+\b",
"MRN": r"\bMRN[-:\s]?\d{5,10}\b",
"DOB": r"\b\d{1,2}[/-]\d{1,2}[/-]\d{2,4}\b",
"PHONE": r"\b\d{3}[-.]?\d{3}[-.]?\d{4}\b",
"ZIP": r"\b\d{5}(-\d{4})?\b",
"EMAIL": r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b",
"SSN": r"\b\d{3}-\d{2}-\d{4}\b",
"NPI": r"\bNPI[-:\s]?\d{10}\b",
}
def redact(self, text: str) -> tuple[str, dict]:
redacted = text
replacements = {}
for phi_type, pattern in self.PHI_PATTERNS.items():
matches = re.findall(pattern, redacted)
for match in matches:
placeholder = f"[{phi_type}_{len(replacements)}]"
replacements[placeholder] = match # kept in local memory only
redacted = redacted.replace(match, placeholder, 1)
return redacted, replacementsKey insight: replacements dict lives only in the PHI Processing Zone. It never serializes to disk or crosses the zone boundary. De-identified text goes to the LLM; the mapping is used only to re-hydrate results for the clinician’s local view.
HIPAA Compliance Layer (Minimum Necessary)
class HIPAAComplianceLayer:
TASK_FIELD_MAP = {
"medical_coding": {"chief_complaint", "diagnoses", "procedures", "encounter_type"},
"clinical_summary": {"chief_complaint", "diagnoses", "medications", "allergies", "vitals"},
"scheduling": {"appointment_type", "provider_npi", "preferred_dates"},
}
async def enforce_minimum_necessary(
self, task_type: str, raw_data: dict, accessor_id: str, reason: str
) -> dict:
allowed = self.TASK_FIELD_MAP.get(task_type, set())
filtered = {k: v for k, v in raw_data.items() if k in allowed}
# Log every PHI access — HIPAA §164.312(b)
await self.audit_store.append({
"event": "phi_access",
"accessor_id": accessor_id,
"task_type": task_type,
"fields_accessed": list(filtered.keys()),
"fields_suppressed": list(set(raw_data.keys()) - allowed),
"reason": reason,
"timestamp_utc": datetime.utcnow().isoformat(),
})
return filteredICD-10 Validation Gate (Deterministic)
class ICD10Validator:
"""Deterministic gate — LLM suggestions must pass this before logging."""
async def validate(self, suggested_code: str, diagnosis_context: str) -> ValidationResult:
async with aiosqlite.connect(self.icd10_db_path) as db:
row = await db.execute(
"SELECT description, category, valid_dx FROM icd10_codes WHERE code = ?",
(suggested_code,)
)
record = await row.fetchone()
if not record:
return ValidationResult(valid=False, reason=f"Code {suggested_code} not in ICD-10-CM 2025")
if not record[2]: # valid_dx flag
return ValidationResult(valid=False, reason=f"Code {suggested_code} is a category header, not a billable code")
return ValidationResult(valid=True, code=suggested_code, description=record[0])Why this matters: LLMs hallucinate ICD-10 codes. A code like I21.0 looks plausible but may not exist or may be a non-billable header. The validator catches this before it reaches a claim.
Gemini Integration (PHI-Free)
async def suggest_icd10_codes(self, de_identified_context: str) -> list[CodeSuggestion]:
"""
Gemini receives ONLY de-identified text. Never raw PHI.
"""
prompt = f"""You are a medical coding assistant. Analyze this de-identified clinical context
and suggest the most appropriate ICD-10-CM codes. Return JSON only.
Clinical Context (de-identified):
{de_identified_context}
Return format:
{{"suggestions": [{{"code": "X00.0", "description": "...", "confidence": 0.92, "rationale": "..."}}]}}"""
response = await self.model.generate_content_async(prompt)
return self._parse_code_suggestions(response.text)Validation
Success Criteria
Verification Methods
# Run the test suite
./test.sh
# Tests cover:
# - PHI detection on 18 identifier types
# - Minimum necessary enforcement
# - ICD-10 validation rejects non-existent codes
# - Audit log hash-chain integrity
# - Consent gate blocks unauthorized access
# - End-to-end coding workflow with de-identified notesAssignment
Extend the ClinicalSummaryAgent with a medication reconciliation capability:
Add a
MedicationReconcilertool that compares medications in the clinical note against a patient’s known medication listFlag potential drug-drug interactions using a deterministic DDI database (RxNorm subset — provided in setup.sh)
Ensure the LLM generates only the explanation of flagged interactions; the actual interaction lookup is deterministic
Add the reconciliation results to the clinical summary output
Log every medication access with reason code per HIPAA minimum necessary
Solution Hints:
The DDI lookup should be a simple SQLite JOIN on RxNorm concept IDs — no LLM needed for the detection
Use spaCy’s
en_core_med7_lgto extract medication names from unstructured text before lookupThe interaction severity comes from the database; the LLM writes the plain-language explanation for the clinician
Extend
TASK_FIELD_MAP["clinical_summary"]to include"medications"and update audit logging accordingly
Looking Ahead: L86 — Legal Agents
The architecture built here transfers almost entirely to legal AI. The conceptual mapping:
L86 will build a ContractClauseExtractor using Agentic RAG against a vector store of regulatory guidance, with the HIPAAComplianceLayer refactored into a general RegulatoryComplianceLayer. The PHI-stripped reasoning pattern becomes the blueprint for privilege-stripped reasoning — the same architecture decision, different regulatory justification.
Youtube demo link:
Module 9 Progress: L85/90 — 5 lessons remain. The compliance architecture is now battle-tested across two highly regulated verticals. One more (legal) before the capstone.






