"""Anonymisation à l'ingestion (D4, loi 09-08) : identités et coordonnées ne partent JAMAIS dans les index vectoriels ni dans les prompts. Fonction pure, testée : e-mails, téléphones (formats marocains et internationaux), et les noms de personnes CONNUS de la base (utilisateurs, gardiens, contacts tiers) fournis par l'appelant. """ import re import unicodedata JETON_PERSONNE = "⟨personne⟩" JETON_CONTACT = "⟨contact⟩" _EMAIL = re.compile(r"[\w.+-]+@[\w-]+\.[\w.-]+") # 06 12 34 56 78 · 0612345678 · +212 6 12 34 56 78 · 05 22-34-56-78… _TELEPHONE = re.compile(r"(?:\+?\d{1,3}[\s.-]?)?(?:0|\(0\))?\d(?:[\s.-]?\d{2}){4}") def _sans_accents(texte: str) -> str: return "".join( c for c in unicodedata.normalize("NFD", texte) if unicodedata.category(c) != "Mn" ) def anonymiser(texte: str, noms_connus: list[str] | None = None) -> str: """Remplace coordonnées et noms connus par des jetons neutres. Les noms sont remplacés insensiblement à la casse ET aux accents (« Idrissi » attrape « idrissi »), prénom seul compris quand il est assez long pour ne pas mutiler le texte technique. """ resultat = _EMAIL.sub(JETON_CONTACT, texte) resultat = _TELEPHONE.sub(JETON_CONTACT, resultat) for nom in sorted(noms_connus or [], key=len, reverse=True): nom = nom.strip() if len(nom) < 3: continue morceaux = [nom] + [m for m in nom.split() if len(m) >= 4] for morceau in morceaux: motif = re.compile( r"\b" + re.escape(_sans_accents(morceau)) + r"\b", re.IGNORECASE ) # on cherche sur une copie sans accents mais on remplace l'original copie = _sans_accents(resultat) sortie: list[str] = [] position = 0 for correspondance in motif.finditer(copie): sortie.append(resultat[position : correspondance.start()]) sortie.append(JETON_PERSONNE) position = correspondance.end() sortie.append(resultat[position:]) resultat = "".join(sortie) # jetons collés en double (« prénom nom » remplacés séparément) resultat = re.sub( rf"{re.escape(JETON_PERSONNE)}(\s+{re.escape(JETON_PERSONNE)})+", JETON_PERSONNE, resultat, ) return resultat