dental-soap / scripts /mass_audit.py
DrZayed's picture
Single-model ZeroGPU: local Qwen3-4B inference, named-patient demographics, Egyptian Arabic red-flag triggers
e4298fb
Raw
History Blame Contribute Delete
4.24 kB
"""Mass safety audit: every eval vignette under ~20 hostile/lay permutations.
Scales scripts/eval_safety.py from 52 stories to 1,000+ by mutating each
vignette the way real patients (and real paste buffers) mutate text:
- chatty prefixes/suffixes ("hi doctor. ...", "... what should I do?")
- case changes (lower/upper)
- smart apostrophes (can't -> can’t)
- doubled whitespace and newlines
- zero-width characters injected between words (web copy-paste artifact)
- neutral filler sentences before/after the clinical content
Every high-risk permutation must still fire its expected rule (recall), and
every benign permutation must still fire nothing (specificity).
No model, no GPU, no network:
python scripts/mass_audit.py
"""
from __future__ import annotations
import sys
from pathlib import Path
from typing import Callable
sys.path.insert(0, str(Path(__file__).parent.parent))
from safety_rules import evaluate_red_flags # noqa: E402
from schema import StructuredIntake # noqa: E402
from eval_safety import VIGNETTES, Vignette # noqa: E402
ZWSP = "​"
def _smart_quotes(s: str) -> str:
return s.replace("'", "’")
def _zero_width_between_words(s: str) -> str:
# Replace every 3rd space with a zero-width space, fusing words the way
# web copy-paste sometimes does ("neck​is swollen").
parts = s.split(" ")
out = parts[0]
for i, word in enumerate(parts[1:], start=1):
out += (ZWSP if i % 3 == 0 else " ") + word
return out
# Fillers are deliberately free of negation words and contrastive conjunctions,
# so they cannot legitimately change what the rule engine should conclude.
MUTATIONS: list[tuple[str, Callable[[str], str]]] = [
("identity", lambda s: s),
("lowercase", str.lower),
("uppercase", str.upper),
("prefix_chatty", lambda s: "hi doctor. " + s),
("prefix_question", lambda s: "Quick question. " + s),
("prefix_background", lambda s: "Some background first. " + s),
("suffix_what_to_do", lambda s: s + " What should I do?"),
("suffix_worried", lambda s: s + " I am worried."),
("suffix_started", lambda s: s + " It started recently."),
("smart_apostrophes", _smart_quotes),
("double_spaces", lambda s: s.replace(" ", " ")),
("newlines", lambda s: s.replace(". ", ".\n")),
("zero_width", _zero_width_between_words),
("filler_before", lambda s: "I drink coffee every morning. " + s),
("filler_after", lambda s: s + " I floss most days."),
("prefix_and_suffix", lambda s: "hi doctor. " + s + " What should I do?"),
("lower_smart", lambda s: _smart_quotes(s.lower())),
("lower_prefix", lambda s: "ok so " + s.lower()),
("zero_width_lower", lambda s: _zero_width_between_words(s.lower())),
("trailing_ws", lambda s: " " + s + " "),
]
def run() -> int:
recall_total = recall_hits = 0
benign_total = benign_clean = 0
failures: list[str] = []
for v in VIGNETTES:
for name, mutate in MUTATIONS:
story = mutate(v.story)
intake = StructuredIntake(chief_concern="audit", **v.intake_kwargs)
findings = evaluate_red_flags(story, intake, age=v.age, meds=v.meds, allergies=v.allergies)
fired = {f.rule_id for f in findings}
if v.expected_rule is None:
benign_total += 1
if not fired:
benign_clean += 1
else:
failures.append(f"FALSE POSITIVE [{name}] {sorted(fired)} <- {story!r}")
else:
recall_total += 1
if v.expected_rule in fired:
recall_hits += 1
else:
failures.append(f"MISS [{name}] {v.expected_rule} <- {story!r}")
total = recall_total + benign_total
print(f"Stories audited: {total} ({len(VIGNETTES)} vignettes x {len(MUTATIONS)} mutations)")
print(f"Red-flag recall: {recall_hits}/{recall_total}")
print(f"Benign specificity: {benign_clean}/{benign_total}")
for line in failures[:25]:
print(" " + line)
if len(failures) > 25:
print(f" ... and {len(failures) - 25} more")
return 0 if not failures else 1
if __name__ == "__main__":
sys.exit(run())