| """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 |
| from schema import StructuredIntake |
| from eval_safety import VIGNETTES, Vignette |
|
|
| ZWSP = "" |
|
|
|
|
| def _smart_quotes(s: str) -> str: |
| return s.replace("'", "’") |
|
|
|
|
| def _zero_width_between_words(s: str) -> str: |
| |
| |
| 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 |
|
|
|
|
| |
| |
| 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()) |
|
|