Ajouter ou remplacer un composant du pipeline
En bref
piighostest une suite d'étapes interchangeables. On remplace un détecteur ou une règle sans toucher aux autres étapes.- Chaque étape suit un contrat écrit une fois. Toute pièce qui respecte ce contrat peut prendre sa place.
- Le fichier de configuration fabrique ces pièces. Les pièces, elles, ignorent tout du fichier de configuration.
- Les briques lourdes (modèles d'IA, bases de données) ne s'installent que si vous les demandez.
- Le type du jeton choisi est vérifié avant l'exécution, donc une combinaison incompatible est refusée tôt.
Cette page s'adresse surtout aux développeurs. Pour le déroulé d'un message, lisez Protéger un message avant l'envoi au modèle. Les termes sont définis dans le glossaire.
Comment le code est découpé
Chaque étape vit dans un paquet de src/piighost/components/. Son base.py déclare le port, c'est-à-dire un Protocol marqué runtime_checkable, nommé Any*. Le pipeline dépend du port, jamais d'une classe concrète. Un objet satisfait le port dès qu'il a la bonne méthode, sans héritage.
Quand plusieurs adaptateurs partagent un squelette, ce squelette vit dans un gabarit Base* (patron Template Method). L'adaptateur ne fournit alors que l'étape qui varie, par exemple _key pour un linker ou _reduce pour un résolveur de chevauchements.
Quels ports ont un gabarit
| Port | Gabarit Base* | Adaptateurs fournis |
|---|---|---|
AnyDetector | aucun (sauf BaseNERDetector pour les modèles NER) | RegexDetector, ExactMatchDetector, CompositeDetector, ChunkedDetector, LLMDetector, détecteurs NER |
AnyDetectionOverride | aucun | DetectionOverride |
AnyOverlapResolver | BaseOverlapResolver | ConfidenceOverlapResolver, MergeOverlapResolver |
AnyDetectionExpander | BaseDetectionExpander | WordBoundaryExpander |
AnyEntityLinker | BaseEntityLinker | ExactEntityLinker |
AnyEntityResolver | BaseEntityResolver | MergeEntityResolver, FuzzyEntityResolver, SeparateEntityResolver |
AnyAnonymizer | BaseAnonymizer | Anonymizer |
AnyPlaceholderFactory | BaseDelimitedPlaceholderFactory, BaseCounterPlaceholderFactory | fabriques de components/placeholder/ |
AnyGuardRail | aucun | DetectorGuardRail, Gliner2GuardRail, LLMGuardRail, ModerationGuardRail |
AnyConversationMemory | aucun | mémoire en processus, Redis, SQLAlchemy |
AnyHasher / AnyCipher | BaseHasher / aucun | SHA-256, Argon2id / AES-GCM |
Les ports sans gabarit expliquent cette absence dans leur docstring. Leurs implémentations diffèrent par tout leur mécanisme, et non par une seule étape.
Le détecteur NER partagé
BaseNERDetector (components/detector/ner/base.py) porte la passe commune aux modèles, c'est-à-dire la correspondance des étiquettes, le seuil de confiance appliqué quel que soit le modèle et le découpage d'un texte trop long en morceaux qui se chevauchent. Un texte plus long que max_chars est découpé si auto_chunk est actif (par défaut). Sinon, le détecteur lève TextTooLongError.
Couplage à sens unique entre configuration et cœur
config/ importe le cœur et le construit. Aucun module du cœur n'importe piighost.config à l'exécution. Chaque modèle de configuration hérite de _ComponentConfig, qui interdit toute clé non déclarée. Chaque modèle expose aussi un build() qui importe son adaptateur au dernier moment. Il n'existe ni registre de constructeurs ni méthode from_config.
Le type d'un composant se choisit par la clé type. Par exemple, DetectorConfig est une union discriminée sur type, c'est-à-dire que la valeur de type désigne le modèle de configuration à utiliser.
Dépendances optionnelles chargées à la demande
Le cœur ne dépend que de typing-extensions. Tout le reste est un extra de pyproject.toml. Un module qui a besoin d'un extra vérifie sa présence avec importlib.util.find_spec et lève une ImportError qui nomme l'extra à installer. Les paquets exposent ces noms paresseusement par un __getattr__ qui lit un dictionnaire nom vers module. from piighost import AnonymizationPipeline ne charge donc ni torch ni langchain.
Jetons typés
Les fabriques de jetons portent une étiquette de préservation (components/placeholder/tags.py). Ces étiquettes sont des sous-classes de str qui n'existent que pour le vérificateur de types. Elles disent si le jeton garde le type de valeur, l'identité, la forme, et s'il peut être retrouvé dans un texte. Le middleware exige PreservesRecognizableIdentity, c'est-à-dire un jeton qui identifie une seule valeur et qu'on peut retrouver. Passer une fabrique de masques au middleware devient donc une erreur de typage.
Ajouter un détecteur
- Copiez l'adaptateur le plus proche. Pour un modèle NER, partez de
components/detector/ner/spacy.pyet héritez deBaseNERDetector. Sinon, partez decomponents/detector/regex.pyet implémentezasync def detect(self, text: str) -> list[Detection]. - Si le module importe une dépendance lourde, gardez l'import dans le module, derrière un test
find_spec, et exposez la classe par le__getattr__du paquet. - Ajoutez l'extra dans
[project.optional-dependencies]depyproject.toml, puis dans l'extraall. - Écrivez le modèle de configuration à côté de ses voisins dans
config/models/detector_model.py. Il porte untype: Literal["..."], des champs validés et unbuild()qui importe l'adaptateur localement. - Ajoutez ce modèle à l'union
DetectorConfigdeconfig/models/detector.py. - Ajoutez un constructeur à la liste
DETECTORSdetests/components/detector/test_contract.py. - Si le module vérifie la présence de sa dépendance (étape 2), ajoutez la ligne
(module, dépendance, extra)àOPTIONAL_DEPENDENCY_GUARDSdanstests/regression/test_imports.py.
Vérifier
uv run pytest tests/components/detector/test_contract.py tests/regression/test_imports.py tests/config
make lintPour votre détecteur, le test de contrat doit rapporter la même chose que pour les autres détecteurs, c'est-à-dire la même position (span) comptée en points de code, le même texte relu dans la source et la même étiquette externe. Un point de code est un caractère Unicode compté une fois, comme le fait Python. L'emoji 😀 compte donc pour un, et non pour deux comme en JavaScript.
Pièges
- Un détecteur peut renvoyer des détections qui se chevauchent. Le port l'autorise. C'est le résolveur de chevauchements qui les arbitre, et il est toujours actif.
- Un import lourd au niveau du paquet casse l'installation minimale.
test_missing_optional_dependency_names_its_extrale repère pour les modules listés dansOPTIONAL_DEPENDENCY_GUARDS.test_every_module_imports_cleanlyne le voit pas si l'environnement de test a déjà tous les extras. - Une clé de configuration mal orthographiée est refusée, pas ignorée, grâce à
extra="forbid". C'est voulu. - Le seuil d'un détecteur NER s'applique même si le modèle l'ignore. Ne comptez pas sur le modèle pour filtrer.
Tests
| Test | Ce qu'il garantit |
|---|---|
tests/components/detector/test_contract.py | Tous les détecteurs rapportent la même valeur de la même façon (span, texte, étiquette, confiance entre 0 et 1). |
tests/regression/test_imports.py | L'API publique s'importe, chaque module s'importe sans extra, chaque garde nomme son extra. |
tests/config/ | Chaque modèle de configuration se valide et se construit. |
Aucun test n'impose le couplage à sens unique. La règle « le cœur n'importe jamais piighost.config » tient par revue de code. Pour la contrôler, grep -rn "piighost.config" src/piighost --include=*.py | grep -v "^src/piighost/config\|^src/piighost/cli" doit être vide.
Pour lancer les tests, voir Lancer et écrire les tests. Pour la configuration, voir Configurer un pipeline.