Spaces:
Running
Architecture
Overview
NoteGuard is the trust layer for clinical AI β a LangGraph ReAct agent (Gemini +
Tavily) where the language model structurally cannot see patient identifiers
because assert_clean() raises before any PHI reaches it.
The system has four layers:
- De-identification core (
src/deid.py) β dependency-free, runnable standalone. NHS-aware recognisers, vault-from-CSV, consistent surrogates, DOB date-shift,assert_clean()hard guarantee. - Agent (
agent/graph.py) β LangGraphStateGraph. Gemini drafts the answer; Tavily grounds it in NICE/NHS public guidance. Neither sees PHI. - REST API (
app/api.py) β FastAPI backend exposingGET /(web UI),GET /health,POST /process,POST /summarise,GET /samples, andGET /sample/{id}. Also serves the static clinician UI. - UI β
app/static/index.html(clinician web UI, vanilla JS, no build step, served by the FastAPIGET /handler).
Package layout
src/
βββ __init__.py # exports NoteGuard, DeidResult, load_known_from_csv
βββ deid.py # de-id core (standard library only)
βββ fetch_dataset.py # downloads synthetic_clinical_notes to data/
agent/
βββ graph.py # LangGraph StateGraph exposed as `noteguard` for langgraph dev
app/
βββ api.py # FastAPI β GET /, GET /health, GET /samples, GET /sample/{id},
β # POST /process, POST /summarise
βββ static/
βββ index.html # Clinician web UI (single-file, vanilla JS, no build step)
streamlit_app.py # Interactive de-id demo β no API keys needed
eval/
βββ run_eval.py # LangSmith evals: zero_phi_to_model + faithfulness
tests/ # pytest suite (24 tests, no external deps)
data/ # synthetic CSV files (git-ignored; produced by src/fetch_dataset.py)
docs/ # architecture, user guide, RAP compliance, tool card, ATRS report
Graph pipeline
For every query the graph runs:
deidentify_in β agent β reidentify_out β compute_trust
| Node | Function | Description |
|---|---|---|
deidentify_in |
NoteGuard.deidentify() + assert_clean() |
Strips PHI; raises if any identifier survives. |
agent |
create_react_agent (Gemini + Tavily) |
Drafts answer; sees only de-identified text. |
reidentify_out |
NoteGuard.reidentify() |
Restores surrogates β real names for the clinician only. |
compute_trust |
NoteGuard.scan_pii() |
De-id audit β residual PII the model saw + orphaned surrogate tokens (reversibility) for the trust panel. |
State fields
In addition to messages, the graph state carries:
| Field | Type | Description |
|---|---|---|
deid_text |
str |
De-identified version of the input note. |
forward |
dict |
Original-identifier β surrogate mapping. |
identifiers_removed |
int |
Count of identifiers replaced in this turn. |
residual_count |
int |
Known identifiers that survived (target: 0). |
leaked_tokens |
list[str] |
Orphaned/unresolved surrogate tokens in the output (reversibility). |
clinician_answer |
str |
Re-identified, clinician-facing answer. |
residual_pii |
list[dict] |
{type, text} findings of suspected un-redacted PII in deid_text. |
REST API
app/api.py exposes six endpoints:
| Endpoint | Method | Description |
|---|---|---|
/ |
GET | Serves app/static/index.html β the clinician web UI. |
/health |
GET | Liveness probe; no API keys required. Returns notes_loaded count. |
/samples |
GET | Paginated list of synthetic notes; supports q, note_type, limit, offset. |
/sample/random |
GET | One random synthetic note. |
/sample/{clinical_note_id} |
GET | Full note by ID (used by the note-picker modal). |
/process |
POST | Full UI payload: clinician_note, ai_note, identifiers, discharge_summary, metrics. |
/summarise |
POST | Compact payload: clinician_answer, identifiers_removed, residual_risk, deidentified_excerpt, ok. |
POST /process request shape:
{
"note": "Pt Margaret Okafor (NHS 485 777 3456) admitted post-fall.",
"question": "Draft a discharge summary.",
"person_id": "pt-001"
}
POST /process response shape:
{
"clinician_note": "verbatim input",
"ai_note": "de-identified text the model saw ([PERSON_1], [NHS_1], β¦)",
"identifiers": ["Margaret Okafor", "485 777 3456", "β¦"],
"discharge_summary": "re-identified Gemini compact eDischarge card for the clinician",
"metrics": {
"deid_ok": true,
"identifiers_removed": 5,
"residual_pii": [],
"residual_pii_count": 0,
"reversible": true,
"leaked_tokens": []
}
}
422 is returned when assert_clean() detects surviving PHI β the model sees nothing.
Every metric reports whether reversible pseudonymisation was done correctly.
deid_ok is true only when residual_pii is empty (nothing un-redacted reached the
model) and reversible is true (every surrogate restores to a real value). The
residual_pii audit (NoteGuard.scan_pii) is vault-independent β it catches free-text
names the vault/NER passes missed, so a pasted note with no ground truth is still graded.
Clinician web UI
app/static/index.html is a self-contained single-page application (vanilla JS, no build step):
- Note picker modal β browse and filter synthetic notes by keyword and note type; clicking a row loads the note into the textarea (the patient is never named in the output).
- Segmented toggle β switches between two views without re-calling the API:
- Clinician view: original note with each redacted identifier wrapped in a red
<mark>. - What the AI sees: de-identified note with
[TYPE_N]surrogate tokens displayed as blue monospace chips.
- Clinician view: original note with each redacted identifier wrapped in a red
- Generate β POSTs to
/process, populates the discharge summary pane and trust panel. - Trust panel β metric cards, all reporting de-id correctness: De-identification (PASS/FAIL), Identifiers replaced, Residual PII Β· model input (count + the offending snippets when > 0), Reversible (β/β, with unresolved tokens when β).
External services
| Concern | Service | Notes |
|---|---|---|
| Reasoning | Google Gemini | google_genai:gemini-2.5-flash (configurable via NOTEGUARD_MODEL). |
| Grounding | Tavily | Public NICE/NHS guidance only β patient text never sent. |
| Observability | LangSmith | Auto-traces when LANGSMITH_TRACING=true. |
All credentials are read from environment variables; nothing is hard-coded.
Deployment
Local (development)
pip install -e ".[dev]"
uvicorn app.api:app --reload --port 8000
Hugging Face Spaces (production)
Dockerfile builds a lean Docker image installing only the runtime dependencies
declared in pyproject.toml, served by uvicorn on port 7860.
Required secrets: GOOGLE_API_KEY, TAVILY_API_KEY, LANGSMITH_API_KEY.