batchCooking/services/tech-step-intent-service/intent_service/pipeline_registry.py
Nicolas 74a0052431 feat(recipes): journalise chaque input/output du pipeline NLP
Ajoute un logging JSON structure (meme convention que LoggerService cote
apps/api) a services/tech-step-intent-service : chaque appel
POST /v1/process journalise locale/texte en entree et
entites/intent/score en sortie, chaque POST /v1/train journalise les uid
entraines et les compteurs resultants. Chatter interne de spaCy mis a
WARNING pour ne pas noyer ces lignes.

Bug trouve et corrige en verifiant les octets bruts d'un log reel (pas
juste son affichage terminal) : l'encodage par defaut de sys.stdout sur
Windows produisait de vrais octets UTF-8 invalides pour tout texte
accentue journalise (le francais des etapes de recette) — corrige par
sys.stdout.reconfigure(encoding="utf-8") au demarrage.

LOG_LEVEL configurable (INFO par defaut), documente dans le README du
service et .env.example.

Verifie : 30/30 pytest (3 nouveaux tests sur le formateur JSON), smoke
test HTTP reel confirmant au niveau des octets que les caracteres
accentues sont preserves, lint complet du monorepo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 21:34:40 +02:00

54 lines
2.5 KiB
Python

"""Détient un `LocalePipeline` par locale supportée — le seul état mutable
partagé du process (une instance vit pour toute la durée de vie d'`uvicorn`,
montée sur `app.state`, voir `main.py`).
Volontairement une classe "registre" séparée de `LocalePipeline` lui-même :
`LocalePipeline` ne connaît qu'une seule locale, ce module route
`train`/`process` vers la bonne instance selon le `locale` reçu dans la
requête — même séparation de responsabilité que `TechStepClassifierService`
(une seule instance, un seul `NlpManager` multi-langues) avait implicitement
via node-nlp, explicitée ici puisque spaCy charge un modèle par langue.
"""
import logging
from .locale_pipeline import SUPPORTED_LOCALES, LocalePipeline, ProcessResult, TrainEntry, UnsupportedLocaleError
logger = logging.getLogger(__name__)
class PipelineRegistry:
def __init__(self) -> None:
self._pipelines: dict[str, LocalePipeline] = {
locale: LocalePipeline(locale) for locale in SUPPORTED_LOCALES
}
def preload_all(self) -> None:
"""Charge le modèle spaCy de base de chaque locale connue — appelé
une fois au démarrage du process (`main.py`), pas paresseusement au
premier appel, pour que `GET /health` ne réponde `200` qu'une fois
ce coût payé (voir `LocalePipeline.preload`)."""
logger.info("tech-step NLP preloading base pipelines", extra={"locales": list(self._pipelines)})
for pipeline in self._pipelines.values():
pipeline.preload()
logger.info("tech-step NLP base pipelines ready", extra={"locales": list(self._pipelines)})
def train(self, locale: str, entries: list[TrainEntry]) -> tuple[int, int, int]:
pipeline = self._pipelines.get(locale)
if pipeline is None:
raise UnsupportedLocaleError(f"Unsupported locale: {locale!r}")
return pipeline.train(entries)
def process(self, locale: str, text: str) -> ProcessResult:
pipeline = self._pipelines.get(locale)
if pipeline is None:
# Une locale que ce service ne sait structurellement pas
# charger (pas de modèle spaCy connu) se comporte comme une
# locale "jamais entraînée" côté `process` — reproduit le test
# `apps/api` existant ("returns an empty sequence for a locale
# nothing was trained on"), qui ne distingue pas les deux cas.
return ProcessResult(entities=[], intent=None, score=0.0)
return pipeline.process(text)
registry = PipelineRegistry()