Aller au contenu

Masquer ou laisser en clair

Votre détecteur lit le nom de votre entreprise comme une personne et vous voulez qu'il reste en clair. Vos noms de code internes ne sont jamais détectés et vous voulez qu'ils soient remplacés à chaque fois. Ces deux décisions portent sur le jeu de détections plutôt que sur le détecteur. DetectionOverride est l'étape qui les impose, avec deux détecteurs. La liste à masquer (deny_list) porte ce qui est toujours masqué, et ce que son détecteur trouve est forcé dans le jeu. La liste à laisser en clair (allow_list) porte ce qui reste toujours en clair, et ce que son détecteur trouve est retiré du jeu.

L'étape s'exécute juste après la détection, avant la résolution des chevauchements et la liaison. Ses deux listes l'emportent donc sur la lecture du détecteur, et aussi sur un jeu corrigé qui revient d'une relecture humaine. Voir Architecture pour l'ordre complet des étapes.

1. Laisser une valeur en clair avec une liste à laisser en clair

Pointez un détecteur sur la valeur, passez-le à DetectionOverride comme liste à laisser en clair, puis passez l'override au pipeline. Ce que la liste à laisser en clair trouve quitte le jeu de détections, donc la valeur arrive en clair au modèle.

import asyncio

from piighost.components.detector import ExactMatchDetector
from piighost.components.override import DetectionOverride
from piighost.pipeline import AnonymizationPipeline

detector = ExactMatchDetector({"Emma": "PERSON", "Acme": "ORG"})
allow_list = ExactMatchDetector({"Acme": "ORG"})
override = DetectionOverride(allow_list=allow_list)
pipeline = AnonymizationPipeline(detector, override=override)


async def main() -> None:
    result = await pipeline.anonymize("Emma works at Acme.")
    print(result.text)


asyncio.run(main())

La sortie doit être :

<<PERSON:1>> works at Acme.

allow_list_strategy décide quelles détections une valeur trouvée par la liste à laisser en clair emporte.

  • Gardez AllowListStrategy.VALUE, le défaut, quand la valeur ne doit jamais être dé-identifiée, quel que soit le label que le détecteur lui donne. Il écarte toute détection qui porte le même texte, sans tenir compte de la casse, de la position ni du label. Le label que vous écrivez à côté de la valeur n'a donc jamais à correspondre à celui que le détecteur primaire émet.
  • Utilisez AllowListStrategy.EXACT quand c'est le label qui compte, et que les deux détecteurs lisent la valeur de la même façon. Il n'écarte une détection que si son span et son label correspondent tous les deux à la valeur trouvée.
  • Utilisez AllowListStrategy.OVERLAP quand une détection plus longue contenant la valeur doit tomber aussi. Il écarte toute détection dont le span touche un span de la liste à laisser en clair, labels ignorés.

Les trois modes sur un même texte, avec un détecteur qui étiquette Acme comme une personne et lit Globex Ltd comme une seule organisation.

import asyncio

from piighost.components.detector import ExactMatchDetector
from piighost.components.override import AllowListStrategy, DetectionOverride
from piighost.pipeline import AnonymizationPipeline


def build_pipeline(strategy: AllowListStrategy) -> AnonymizationPipeline:
    detector = ExactMatchDetector(
        {"Emma": "PERSON", "Acme": "PERSON", "Globex Ltd": "ORG"}
    )
    allow_list = ExactMatchDetector({"Acme": "ORG", "Globex": "ORG"})
    override = DetectionOverride(allow_list=allow_list, allow_list_strategy=strategy)
    return AnonymizationPipeline(detector, override=override)


async def main() -> None:
    text = "Emma works at Acme, formerly Globex Ltd."
    for strategy in AllowListStrategy:
        pipeline = build_pipeline(strategy)
        result = await pipeline.anonymize(text)
        print(strategy.value, "->", result.text)


asyncio.run(main())

La sortie doit être :

exact -> <<PERSON:1>> works at <<PERSON:2>>, formerly <<ORG:1>>.
value -> <<PERSON:1>> works at Acme, formerly <<ORG:1>>.
overlap -> <<PERSON:1>> works at Acme, formerly Globex Ltd.

EXACT n'a rien trouvé à écarter, pour deux raisons. La liste à laisser en clair dit que Acme est une organisation, là où le détecteur dit une personne. Et le span du détecteur couvre Globex Ltd, là où la liste à laisser en clair ne couvre que Globex. VALUE compare des valeurs entières, donc il a écarté Acme et laissé Globex Ltd, dont le texte n'est pas celui de la liste à laisser en clair. OVERLAP a écarté les deux, parce que le span de la liste à laisser en clair est à l'intérieur de la détection plus longue.

2. Forcer une valeur ratée avec une liste à masquer

Pointez un détecteur sur le motif que le détecteur principal rate, ici un nom de code qu'une regex décrit exactement, et passez-le comme liste à masquer. Ce qu'il trouve entre dans le jeu de détections, quoi qu'ait vu le détecteur principal.

import asyncio

from piighost.components.detector import RegexDetector
from piighost.components.override import DetectionOverride
from piighost.pipeline import AnonymizationPipeline

detector = RegexDetector.from_catalog("catalog:piighost/generic")
deny_list = RegexDetector({"CODENAME": r"ACME-[A-Z]+"})
override = DetectionOverride(deny_list=deny_list)
pipeline = AnonymizationPipeline(detector, override=override)


async def main() -> None:
    result = await pipeline.anonymize("Ship ACME-FALCON to alice@example.com.")
    print(result.text)


asyncio.run(main())

La sortie doit être :

Ship <<CODENAME:1>> to <<EMAIL:1>>.

Une détection forcée remplace aussi toute détection qu'elle chevauche, donc le label de la liste à masquer l'emporte sur la lecture principale. Servez-vous-en pour corriger un label, pas seulement pour ajouter une détection.

from piighost.components.detector import ExactMatchDetector

detector = ExactMatchDetector({"Emma": "PERSON", "Acme": "PERSON"})
deny_list = ExactMatchDetector({"Acme": "ORG"})
override = DetectionOverride(deny_list=deny_list)
pipeline = AnonymizationPipeline(detector, override=override)


async def main() -> None:
    result = await pipeline.anonymize("Acme hired Emma.")
    print(result.text)


asyncio.run(main())

La sortie doit être :

<<ORG:1>> hired <<PERSON:1>>.

Une valeur forcée passe par la liaison et l'attribution de jeton comme n'importe quelle détection, donc le pipeline conversationnel la stocke en mémoire et deanonymize la restaure.

3. Dé-identifier une valeur introduite par l'assistant

Dans une conversation, une valeur que l'assistant a écrite le premier reste en clair même si la liste à masquer la trouve. Le modèle a produit cette valeur parce qu'elle était utile dans le contexte, et il ne sait pas qu'elle est confidentielle. La remplacer lui retirerait sa connaissance du monde, et signalerait que cette valeur précise est sensible. deny_list_strategy décide qui l'emporte.

  • Gardez DenyListStrategy.RESPECT_PROVENANCE, le défaut, pour laisser en clair une valeur introduite par l'assistant. La liste à masquer garantit toujours que la valeur est détectée, et la même valeur introduite par l'utilisateur est bien dé-identifiée.
  • Utilisez DenyListStrategy.FORCE pour dé-identifier une valeur de la liste à masquer quel que soit celui qui l'a écrite le premier.
import asyncio

from piighost.components.detector import ExactMatchDetector
from piighost.components.override import DenyListStrategy, DetectionOverride
from piighost.conversation_memory import MessageRole
from piighost.pipeline import ThreadAnonymizationPipeline


def build_pipeline(strategy: DenyListStrategy) -> ThreadAnonymizationPipeline:
    detector = ExactMatchDetector({})
    deny_list = ExactMatchDetector({"Acme": "ORG"})
    override = DetectionOverride(deny_list=deny_list, deny_list_strategy=strategy)
    return ThreadAnonymizationPipeline(detector, override=override)


async def main() -> None:
    for strategy in DenyListStrategy:
        pipeline = build_pipeline(strategy)
        assistant = await pipeline.anonymize(
            "Acme rocks", thread_id="t1", role=MessageRole.ASSISTANT
        )
        user = await pipeline.anonymize("I love Acme", thread_id="t1")
        print(strategy.value, "->", assistant.text, "|", user.text)


asyncio.run(main())

La sortie doit être :

respect_provenance -> Acme rocks | I love Acme
force -> <<ORG:1>> rocks | I love <<ORG:1>>

4. Décider qui l'emporte quand les deux listes se contredisent

Une valeur que les deux listes trouvent est une contradiction, et conflict_strategy nomme le gagnant.

  • Gardez OverrideConflictStrategy.DENY_LIST_WINS, le défaut, pour dé-identifier la valeur contredite. La liste à laisser en clair s'applique d'abord aux détections principales, puis la liste à masquer est forcée en dernier.
  • Utilisez OverrideConflictStrategy.ALLOW_LIST_WINS pour la garder en clair. La liste à masquer est forcée d'abord, puis la liste à laisser en clair écarte le résultat, détections forcées comprises.
  • Utilisez OverrideConflictStrategy.RAISE pour refuser la contradiction. Un span de la liste à masquer qui chevauche un span de la liste à laisser en clair lève ConflictingOverrideError avant l'application de l'une ou l'autre liste.
import asyncio

from piighost.components.detector import ExactMatchDetector
from piighost.components.override import DetectionOverride, OverrideConflictStrategy
from piighost.exceptions import ConflictingOverrideError
from piighost.pipeline import AnonymizationPipeline


def build_pipeline(strategy: OverrideConflictStrategy) -> AnonymizationPipeline:
    detector = ExactMatchDetector({"Emma": "PERSON"})
    deny_list = ExactMatchDetector({"Acme": "ORG"})
    allow_list = ExactMatchDetector({"Acme": "ORG"})
    override = DetectionOverride(
        deny_list=deny_list,
        allow_list=allow_list,
        conflict_strategy=strategy,
    )
    return AnonymizationPipeline(detector, override=override)


async def main() -> None:
    for strategy in OverrideConflictStrategy:
        pipeline = build_pipeline(strategy)
        try:
            result = await pipeline.anonymize("Emma works at Acme.")
        except ConflictingOverrideError as error:
            print(strategy.value, "->", type(error).__name__, error)
        else:
            print(strategy.value, "->", result.text)


asyncio.run(main())

La sortie doit être :

deny_list_wins -> <<PERSON:1>> works at <<ORG:1>>.
allow_list_wins -> <<PERSON:1>> works at Acme.
raise -> ConflictingOverrideError Overrides contradict each other on 'Acme': a span on the deny list overlaps one on the allow list.

ALLOW_LIST_WINS écarte une détection forcée selon la stratégie de la liste à laisser en clair. Avec le défaut VALUE, une valeur forcée est donc écartée quel que soit le label que la liste à masquer lui a attaché. Sous EXACT, les deux listes doivent s'accorder sur le label pour que la liste à laisser en clair l'emporte.

5. Piloter les deux listes depuis un fichier de configuration

Les deux listes sont des configs de détecteur, [override.deny_list] et [override.allow_list], et les trois stratégies sont des clés de [override]. Le fichier ci-dessous force le nom de code et garde en clair une boîte mail publique.

[detector]
type = "regex"
catalogs = ["catalog:piighost/generic"]

[override]
allow_list_strategy = "value"

[override.deny_list]
type = "regex"
patterns = { CODENAME = 'ACME-[A-Z]+' }

[override.allow_list]
type = "exact"
values = { "public@corp.com" = "EMAIL" }

load_pipeline lit le fichier et construit le pipeline, les deux listes comprises.

import asyncio

from piighost.config import load_pipeline

pipeline = load_pipeline("pipeline.toml")


async def main() -> None:
    result = await pipeline.anonymize(
        "Mail public@corp.com or alice@example.com about ACME-FALCON."
    )
    print(result.text)


asyncio.run(main())

La sortie doit être :

Mail public@corp.com or <<EMAIL:1>> about <<CODENAME:1>>.

Pour chaque clé et chaque valeur acceptée, voir la configuration TOML.

Voir aussi