solo project
Governed Clinical Copilot
Role: Sole engineerPython · PyTorch · sqlglot · LangGraph
A self-hostable natural-language→SQL analytics agent over de-identified clinical (MIMIC-IV) data — built governance-first, so it refuses what it shouldn't answer instead of guessing.
PythonPyTorchsqlglot (AST)LangGraphTransformersQLoRA / ORPO
Context
Clinicians and analysts want to ask questions of EHR data in plain English. But an LLM that writes and runs SQL over health data is genuinely dangerous: it can issue destructive statements, read protected-health-information (PHI) columns, return re-identifiable small cells, or — worst — answer confidently when it should say "I can't."
Problem
The hard part wasn't SQL accuracy — it was trustworthiness under adversarial input. The system had to enforce read-only access, block PHI, suppress small-cell results, and know when to abstain, all before a query ever touches the database.
Approach
- A 5-layer guardrail stack. Three layers are AST-based (parsed with sqlglot), which is the real security boundary: (1) read-only / single-statement enforcement that walks the tree for hidden DDL/DML even inside sub-queries and CTEs; (2) a table+column allowlist that resolves CTE names and aliases before checking; (3) a hard PHI-column block. On top: (4) post-execution k-anonymity small-cell suppression (k=11, NHS standard), and (5) a heuristic prompt-injection screen on the NL input.
- Fine-tuning the SQL model. I ran a full SFT → DPO → ORPO → GRPO arc on a 7B code model with QLoRA, learning to abstain on unanswerable questions. When GRPO collapsed (zero within-group reward variance — SQL generation is bimodal), I diagnosed it and pivoted to ORPO. Separately I fine-tuned OmniSQL-7B (SFT + ORPO).
- An evaluation harness I could trust. A ~1,200-line harness mirroring the official EHRSQL-2024 scorer (execution accuracy + a reliability score that rewards correct abstention and penalizes hallucinated SQL), with execution-guided repair and self-consistency voting.
Results & what I learned
- On my own evaluation, the OmniSQL-7B (SFT+ORPO) model reached RS(10) = 0.873 on the official 1,167-question test split — matching/exceeding the public leaderboard winner in my eval.
- A 68-case adversarial suite for the guardrails (DDL-inside-a-CTE, PHI-in-a-subquery, injection strings, and more).
- The most valuable moment was catching a bug in my own scoring code — it was counting two different failing queries as a match, inflating accuracy. I fixed it and added a locking regression test. Distrusting your own green metrics is the real skill.
Honest scope: the 0.873 is my own evaluation, not independently verified against the gated leaderboard; and the served inference path uses Hugging Face + bitsandbytes (a vLLM/AWQ path was planned, not shipped). The guardrail rigor, the diagnosed GRPO failure, and the self-caught scoring bug are the parts I stand behind fully.