Download phi.py from htgudi/radionAI: direct link, hf CLI and curl.
- Browser
- Download file 5.81 kB
-
https://huggingface.co/spaces/htgudi/radionAI/resolve/main/phi.py
- Command line
-
hf download hf://spaces/htgudi/radionAI/phi.py
-
curl -L -o phi.py https://huggingface.co/spaces/htgudi/radionAI/resolve/main/phi.py
5.81 kB
| """ | |
| Patient identifiers — YashTech Ltd. AI Labs. | |
| Three rules, enforced here rather than trusted to the UI: | |
| 1. **Identifiers never reach the model.** They label the report; they are not | |
| clinical context. MedGemma runs on shared multi-tenant hardware, so a name | |
| in a prompt is a name on someone else's GPU. `scrub()` strips them from | |
| free text on the way in. | |
| 2. **Pseudonymised by default.** The report header shows a stable token such | |
| as `PT-K4X9M2QF`, derived by HMAC from the identifier and a secret salt. | |
| Same patient, same token, across reports and sessions — which is what makes | |
| a pseudonym useful — without ever writing the identifier down. | |
| 3. **No mapping table.** Re-identification is by recomputing the token from an | |
| identifier you already hold, not by looking it up. Nothing to leak, nothing | |
| to subject-access-request, nothing to delete. | |
| Showing a real identifier on a report is a deliberate act by someone holding the | |
| `show_identifiers` permission, and the report says so in its header. | |
| `PSEUDONYM_SALT` must be set as a secret. Without it a random salt is generated | |
| at boot: tokens stay unguessable but stop being stable across restarts, and the | |
| admin console says so rather than quietly producing a different token for the | |
| same patient tomorrow. | |
| """ | |
| from __future__ import annotations | |
| import base64 | |
| import dataclasses | |
| import hashlib | |
| import hmac | |
| import os | |
| import re | |
| import secrets | |
| PSEUDONYMISE = "pseudonymise" | |
| OMIT = "omit" | |
| FULL = "full" | |
| MODES = ( | |
| (PSEUDONYMISE, "Pseudonymised token (recommended)"), | |
| (OMIT, "No identifiers at all"), | |
| (FULL, "Show as entered (requires permission)"), | |
| ) | |
| MODE_CHOICES = [(label, value) for value, label in MODES] | |
| DEFAULT_MODE = PSEUDONYMISE | |
| _EPHEMERAL_SALT = secrets.token_hex(32) | |
| def salt() -> tuple[bytes, bool]: | |
| """(salt, is_stable). An unset salt is usable but not stable across restarts.""" | |
| configured = os.getenv("PSEUDONYM_SALT", "").strip() | |
| if configured: | |
| return configured.encode("utf-8"), True | |
| return _EPHEMERAL_SALT.encode("utf-8"), False | |
| def salt_status() -> str: | |
| _key, stable = salt() | |
| return ( | |
| "configured — tokens are stable across restarts" | |
| if stable | |
| else "NOT SET — tokens change on every restart; set PSEUDONYM_SALT as a secret" | |
| ) | |
| def _normalise(value: str) -> str: | |
| """So `NHS 123 456 7890`, `nhs1234567890` and `123-456-7890` agree.""" | |
| return re.sub(r"[^a-z0-9]", "", (value or "").strip().lower()) | |
| def pseudonym(identifier: str, prefix: str = "PT") -> str: | |
| key, _stable = salt() | |
| norm = _normalise(identifier) | |
| if not norm: | |
| return "" | |
| digest = hmac.new(key, norm.encode("utf-8"), hashlib.sha256).digest() | |
| token = base64.b32encode(digest).decode("ascii").rstrip("=")[:8] | |
| return f"{prefix}-{token}" | |
| class Patient: | |
| patient_id: str = "" | |
| name: str = "" | |
| mode: str = DEFAULT_MODE | |
| def supplied(self) -> bool: | |
| return bool(self.patient_id.strip() or self.name.strip()) | |
| def token(self) -> str: | |
| """Stable token from the ID if there is one, otherwise from the name.""" | |
| return pseudonym(self.patient_id) or pseudonym(self.name, prefix="PN") | |
| def terms(self) -> list[str]: | |
| """Strings that must never appear in a prompt.""" | |
| out = [] | |
| for value in (self.patient_id, self.name): | |
| value = (value or "").strip() | |
| if len(value) >= 3: | |
| out.append(value) | |
| out.extend(p for p in re.split(r"[\s,]+", value) if len(p) >= 3) | |
| return sorted(set(out), key=len, reverse=True) | |
| def resolve(patient: Patient, may_show_identifiers: bool) -> tuple[Patient, str | None]: | |
| """Downgrade `full` to pseudonymised when the user lacks the permission.""" | |
| if patient.mode == FULL and not may_show_identifiers: | |
| return ( | |
| dataclasses.replace(patient, mode=PSEUDONYMISE), | |
| "Your role cannot show identifiers on a report — exported pseudonymised instead.", | |
| ) | |
| if patient.mode not in {m for m, _ in MODES}: | |
| return dataclasses.replace(patient, mode=DEFAULT_MODE), None | |
| return patient, None | |
| def header_lines(patient: Patient) -> list[str]: | |
| """Markdown bullets for the report header. Never raises on bad input.""" | |
| if not patient.supplied or patient.mode == OMIT: | |
| return ["- **Patient:** not recorded (identifiers omitted)"] | |
| if patient.mode == FULL: | |
| lines = ["- **Patient identifiers: SHOWN IN FULL — handle as clinical record**"] | |
| if patient.patient_id.strip(): | |
| lines.append(f"- **Patient ID:** {patient.patient_id.strip()}") | |
| if patient.name.strip(): | |
| lines.append(f"- **Patient name:** {patient.name.strip()}") | |
| lines.append(f"- **Pseudonym:** {patient.token}") | |
| return lines | |
| lines = [f"- **Patient:** {patient.token} (pseudonymised)"] | |
| _key, stable = salt() | |
| if not stable: | |
| lines.append("- **Note:** pseudonym salt not configured — this token is not reproducible") | |
| return lines | |
| def scrub(text: str, patient: Patient) -> str: | |
| """Remove identifiers from text heading for the model. | |
| Deliberately blunt. This is a backstop for a name typed into the clinical | |
| indication box, not a de-identification engine: it cannot catch an | |
| identifier nobody entered into the patient fields. | |
| """ | |
| if not text or not patient.supplied: | |
| return text | |
| cleaned = text | |
| for term in patient.terms(): | |
| cleaned = re.sub(re.escape(term), "[redacted]", cleaned, flags=re.IGNORECASE) | |
| return cleaned | |
| def prompt_context(patient: Patient) -> str: | |
| """What the model is allowed to know: nothing identifying.""" | |
| return "" | |