feat(server): durcissement sanitizer PII (chevauchements + GXD5 + workflow_dict)
- Résolution des chevauchements par priorité de détecteur + longueur : corrige le FN où, sur 'Dossier/Patient NOM (NAISSANCE) Prénom', le nom de naissance fuyait. (Qwen) - RE_GXD5_DIAG : tokenise le numéro de dossier ([DOSSIER_n]) ET le nom ([NOM_n]) dans 'GXD5 Diagnostics - <num> - NOM PRENOM' — 3 patients fuyaient en prod clinique, 0 FP. (Qwen) - sanitize_workflow_dict : assainit les champs texte d'un workflow appris (by_text, noms) avant import en DB VWB (canal apprentissage). Utilisé par R1. (Claude) 14 tests verts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -172,3 +172,65 @@ def test_sanitize_event_titre_imbrique_vision_info():
|
||||
assert "[IPP_1]" in wc
|
||||
# cohérence : même titre dans window et vision_info -> même token
|
||||
assert out["window"]["title"] == wc
|
||||
|
||||
|
||||
def test_sanitize_workflow_dict_tokenise_by_text_garde_ui():
|
||||
"""R1/PII : un workflow appris ne doit pas porter de PII brute dans ses cibles
|
||||
(by_text) ni ses noms avant import en DB VWB ; l'interface est préservée."""
|
||||
import json
|
||||
from agent_v0.server_v1.pii_sanitizer import sanitize_workflow_dict
|
||||
|
||||
wf = {
|
||||
"name": "Dossier patient",
|
||||
"nodes": [{"node_id": "n1", "name": "VIOLA (VIOLA) Liliane 90 ans"}],
|
||||
"edges": [{
|
||||
"edge_id": "e1",
|
||||
"action": {
|
||||
"type": "mouse_click",
|
||||
"target": {"by_text": "Valider", "by_role": "ocr"},
|
||||
},
|
||||
}],
|
||||
}
|
||||
out = sanitize_workflow_dict(wf)
|
||||
s = json.dumps(out, ensure_ascii=False)
|
||||
assert "VIOLA" not in s # nom clinique tokenisé (dans un node name)
|
||||
assert "[NOM_1]" in s
|
||||
assert "90 ans" not in s # âge tokenisé
|
||||
assert "Valider" in s # cible UI préservée (by_text)
|
||||
assert "VIOLA" in json.dumps(wf, ensure_ascii=False) # original non muté
|
||||
|
||||
|
||||
def test_chevauchement_prefix_capitalise():
|
||||
"""FN bloquant (Claude R1) : mot capitalisé avant NOM (NAISSANCE) Prénom
|
||||
-> RE_PRENOM_NOM captait « Dossier VIOLA » et bloquait RE_NOM_NAISSANCE
|
||||
« VIOLA (VIOLA) Liliane ». Fix : résolution par priorité détecteur + longueur."""
|
||||
from agent_v0.server_v1.pii_sanitizer import anonymize_text
|
||||
|
||||
m: dict = {}
|
||||
for titre, leak in [("Dossier VIOLA (VIOLA) Liliane", "VIOLA"),
|
||||
("Patient ROSSIGNOL (SOUBIE) Pierrette", "ROSSIGNOL"),
|
||||
("Fenetre LAVAL (BARTHELEMY) Nicole", "LAVAL")]:
|
||||
out, _ = anonymize_text(titre, mapping=m)
|
||||
assert leak not in out, f"FN: {leak} still visible in '{out}'"
|
||||
|
||||
# contrôle : sans préfixe, toujours OK
|
||||
out, _ = anonymize_text("VIOLA (VIOLA) Liliane", mapping=m)
|
||||
assert "VIOLA" not in out
|
||||
|
||||
|
||||
def test_gxd5_diagnostics_numero_et_nom():
|
||||
"""GXD5 Diagnostics — numéro de dossier + nom tout-majuscules (3 patients prod)."""
|
||||
from agent_v0.server_v1.pii_sanitizer import anonymize_text
|
||||
|
||||
m: dict = {}
|
||||
for titre, num_leak, nom_leak in [
|
||||
("GXD5 Diagnostics - 128008 - BENVENISTE MARIE-LAURENCE", "128008", "BENVENISTE"),
|
||||
("GXD5 Diagnostics - 272223 - LEMOINE ERIC", "272223", "LEMOINE"),
|
||||
("GXD5 Diagnostics - 153442 - ROSELIER MATHEO", "153442", "ROSELIER"),
|
||||
]:
|
||||
out, ents = anonymize_text(titre, mapping=m)
|
||||
assert num_leak not in out, f"FN: numéro {num_leak} visible dans '{out}'"
|
||||
assert nom_leak not in out, f"FN: nom {nom_leak} visible dans '{out}'"
|
||||
types = {e["type"] for e in ents}
|
||||
assert "DOSSIER" in types, f"Pas de token DOSSIER dans {ents}"
|
||||
assert "NOM" in types, f"Pas de token NOM dans {ents}"
|
||||
|
||||
Reference in New Issue
Block a user