mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
feat(r5.1): socle apps/ai — ingestion anonymisée + recherche sémantique
ADR-004 : embeddings locaux sur CPU (fastembed ONNX, paraphrase-multilingual-MiniLM-L12-v2, 384 dims — les textes du client ne quittent jamais le serveur), pgvector dans le Postgres existant (RagChunk possédé par Prisma, migration r5_ia + état de corpus sur Document), génération opt-in (mode extractif par défaut : la recette passe sans clé API), service siop2-ai jamais exposé — joint par l'API NestJS seule (X-Service-Token). apps/ai (FastAPI + uv) : pipeline PDF MinIO → texte paginé (pypdf) → anonymisation D4 (e-mails, téléphones marocains, noms connus de la base, insensible casse/accents — fonction pure testée) → découpage avec chevauchement (testé) → embeddings → RagChunk localisé (« p. 42 », « bilan du 17/07 »). Bilans codés clôturés ingérés. Exclusion de corpus (D3) appliquée à l'ingestion ET à la lecture. 14 pytest + ruff, embeddeur déterministe en CI (aucun téléchargement), job CI ai (uv), deploy en dépend. Vérifié en réel avec le vrai modèle : corpus seedé réindexé en 7 s (PDF réel → 31 extraits paginés + 3 bilans), recherche sémantique concluante, e-mails → ⟨contact⟩, 0 identité dans les chunks (contrôle SQL). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
33
apps/ai/tests/test_anonymisation.py
Normal file
33
apps/ai/tests/test_anonymisation.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""D4 (loi 09-08) : rien d'identifiant ne doit survivre à l'ingestion."""
|
||||
|
||||
from siop_ai.anonymisation import JETON_CONTACT, JETON_PERSONNE, anonymiser
|
||||
|
||||
|
||||
def test_emails_et_telephones_marocains():
|
||||
texte = "Appeler M. Alami au 06 12 34 56 78 ou +212 5 22 34 56 78, sinon gardien@residence.ma"
|
||||
resultat = anonymiser(texte, ["M. Alami"])
|
||||
assert "06 12" not in resultat
|
||||
assert "+212" not in resultat
|
||||
assert "gardien@residence.ma" not in resultat
|
||||
assert resultat.count(JETON_CONTACT) == 3
|
||||
|
||||
|
||||
def test_noms_connus_meme_sans_accents_ni_casse():
|
||||
texte = "Intervention validée par salma idrissi puis contrôlée par AHMED BENALI."
|
||||
resultat = anonymiser(texte, ["Salma Idrissi", "Ahmed Benali"])
|
||||
assert "idrissi" not in resultat.lower()
|
||||
assert "benali" not in resultat.lower()
|
||||
assert resultat.count(JETON_PERSONNE) == 2
|
||||
|
||||
|
||||
def test_prenom_seul_est_attrape_mais_pas_les_mots_courts():
|
||||
resultat = anonymiser("Vu avec Ahmed sur site.", ["Ahmed Benali"])
|
||||
assert "Ahmed" not in resultat
|
||||
# « NC-31 » ou « vis » ne doivent jamais être mutilés par un nom court
|
||||
resultat2 = anonymiser("Contact NC-31 réglé, vis serrées.", ["N. C."])
|
||||
assert "NC-31" in resultat2
|
||||
|
||||
|
||||
def test_le_texte_technique_reste_intact():
|
||||
texte = "Serrer les coulisseaux au couple de 25 N·m ; jeu latéral 0,5 mm."
|
||||
assert anonymiser(texte, ["Salma Idrissi"]) == texte
|
||||
Reference in New Issue
Block a user