Suite a une revue de code sur locale_pipeline.py, trois ameliorations implementees et verifiees contre le vrai corpus (74 techniques) : - spacy.util.fix_random_seed(_TRAINING_SEED) avant nlp.initialize() — random.Random() ne graine que l'ordre de melange des exemples, pas l'init des poids/dropout internes de thinc. - _DiacriticsNormalizer deplace au-dessus de sa factory @Language.factory — plus d'annotation de type en chaine. - Log explicite (logger.warning) quand train() recoit moins de 2 labels et saute la creation du textcat, plus une clarification de la docstring de process() sur les deux cas menant a intent=None. - Early stopping avec suivi de la perte par epoque, _TRAINING_ITERATIONS restant le plafond. Mesure sur le vrai corpus : ne se declenche jamais dans le budget actuel de 40 iterations (la perte continue de baisser significativement jusqu'au bout) — documente honnetement comme filet de securite pour un futur relevement du plafond, pas un gain de temps aujourd'hui. Deux suggestions de la revue examinees et non retenues, avec justification en commentaire : le risque de desalignement pattern/texte via normalize_text (normalize_text opere par token deja tokenise, jamais sur la chaine brute — pas de risque de segmentation differente) ; passer a attr="LOWER" aurait au contraire regresse l'insensibilite aux accents que attr="NORM" fournit deliberement. Verifie : 28/28 pytest (dont le vrai corpus complet via la fixture partagee), lint du monorepo. Temps d'entrainement mesure stable (~200-230s/locale, dans la marge de bruit deja documentee). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| intent_service | ||
| tests | ||
| .env.example | ||
| .gitignore | ||
| Dockerfile | ||
| pyproject.toml | ||
| README.md | ||
| uv.lock | ||
tech-step-intent-service
Microservice de détection d'intention (technique de cuisine) — remplace le
pipeline node-nlp qui vivait dans apps/api
(TechStepClassifierService, apps/api/src/lib/recipe-matching/tech-step-matcher.ts) :
- NER par phrases (
spacy.matcher.PhraseMatcher) — trouve les mentions candidates d'une technique dans un texte, à partir dessynonymsde chaque technique. - Classification d'intention (
textcatspaCy, bag-of-words) — verdict de la technique qu'une clause de texte signifie, entraîné sur lesutterancesde chaque technique (y compris des paraphrases n'utilisant jamais le mot-clé lui-même).
Basé sur spaCy (fr_core_news_md/en_core_web_md) plutôt que node-nlp —
écosystème NLP plus robuste/maintenu, avec l'ambition à terme (hors scope de
ce service en l'état) de pouvoir aussi absorber ce que fait aujourd'hui
services/tech-step-llm-worker une fois ce pipeline assez riche pour s'en
passer (les modèles md, avec vecteurs de mots, sont conservés dans ce but,
même si rien ici ne s'en sert encore).
Ce service est entièrement autonome
Contrairement à sa toute première version, ce service possède désormais
son propre corpus — intent_service/training_data.py
(TECH_STEP_TRAINING_DATA), revu par PR comme le reste du code. Il
s'entraîne lui-même une seule fois, à son propre démarrage
(PipelineRegistry.initialize(), appelé par main.py's lifespan), et ne
persiste jamais rien sur disque — un redémarrage du process réentraîne
toujours from scratch depuis ce fichier. apps/api ne connaît plus aucune
technique ni aucun synonyme : il n'appelle plus que POST /v1/process (plus
de POST /v1/train, supprimé).
Workflow mainteneur pour changer le corpus :
- Éditer
intent_service/training_data.pyà la main (informé par le rapport deapps/api/src/scripts/list-pending-training-suggestions.ts). - Redémarrer ce service (
docker compose restart tech-step-intent-service, ou simplement redéployer) — le nouveau corpus n'a d'effet qu'une fois réentraîné au démarrage, contrairement à l'ancienne version qui pouvait être réentraînée à chaud viaPOST /v1/train. - Depuis
apps/api, lancerpnpm --filter api exec tsx src/scripts/retrain-tech-steps.ts— vérifie le F1 contreTECH_STEP_EVAL_DATASETavant de backfiller les recettes existantes.
Pourquoi ce service vit hors du workspace pnpm
Même raisonnement que services/tech-step-llm-worker : un service Python
n'a rien à faire dans pnpm-workspace.yaml (qui ne couvre que
apps/*/packages/*), et ses dépendances (spaCy, ses modèles) ne doivent
jamais se retrouver dans l'image apps/api. Aucun accès direct à
Postgres non plus — la résolution TechStep.key -> id reste entièrement
côté apps/api (TechStepClassifierService), ce service ne manipule que
des uid (chaînes opaques) tout du long.
Contrat HTTP
Voir intent_service/schemas.py pour le détail exact. En résumé :
GET /health— sans authentification,200une fois ce service entièrement prêt : modèles spaCy de base chargés et les deux locales entraînées (pas de lazy-load, voirintent_service/main.py) — voir "Temps de démarrage" plus bas pour ce que ça implique en pratique.POST /v1/process—{ locale, text }→{ entities: [{ uid, start, end }], intent, score }.
/v1/process exige le header X-Intent-Service-Secret (voir
intent_service/security.py), qui doit matcher INTENT_SERVICE_SECRET
côté apps/api.
Temps de démarrage
Ce service met plusieurs minutes à devenir healthy — contrairement à
node-nlp (entraînement quasi instantané), entraîner le textcat sur le
corpus réel (~74 techniques) prend de l'ordre de 200 secondes par locale
(mesuré localement, sans GPU), donc environ 400 secondes (~7 minutes) pour
fr+en combinés à chaque démarrage du process. docker-compose.yml et
.github/workflows/ci.yml ont un start_period/timeout d'attente
généreux pour ça — voir leurs propres commentaires. C'est un compromis
assumé, pas un défaut de configuration à corriger : moins d'itérations
entraîne plus vite mais laisse des verdicts corrects sous
CONFIDENCE_THRESHOLD (voir le commentaire de cette constante,
apps/api/src/lib/recipe-matching/tech-step-matcher.ts, et celui de
_TRAINING_ITERATIONS/_TRAINING_BATCH_SIZE dans locale_pipeline.py
pour le détail du compromis).
Logs
intent_service/logging_config.py branche un format JSON structuré (une
ligne par évènement — timestamp/level/message + champs métier fusionnés
— même convention que LoggerService côté apps/api) sur toute la
journalisation de ce service, niveau LOG_LEVEL (INFO par défaut, voir
.env.example). routes/process.py journalise chaque appel avec son input
et son output complets, pipeline_registry.py journalise le déroulement de
l'entraînement au démarrage :
{"timestamp": "...", "level": "info", "message": "tech-step NLP process", "locale": "fr", "text": "faire fondre le beurre", "entities": [{"uid": "melt", "start": 6, "end": 13}], "intent": "melt", "score": 0.93}
Le chatter interne de spaCy ("spacy" logger — chargement de vocabulaire,
etc.) est explicitement mis à WARNING pour ne pas noyer ces lignes.
Setup
Ce service utilise uv pour ses dépendances
(uv.lock committé, uv sync --frozen partout — Dockerfile, CI, dev).
cd services/tech-step-intent-service
uv sync
cp .env.example .env
# édite .env : génère un INTENT_SERVICE_SECRET, identique à celui d'apps/api
uv run uvicorn intent_service.main:app --reload --port 8000
apps/api (natif, pnpm dev:api, ou sa suite Mocha) doit pointer
INTENT_SERVICE_BASE_URL=http://localhost:8000 et le même
INTENT_SERVICE_SECRET (voir apps/api/.env.example).
Running via Docker Compose
docker-compose.yml (racine) définit un service tech-step-intent-service
aux côtés de postgres/app/tech-step-llm-worker — pas optionnel,
contrairement au worker LLM : sans lui, apps/api ne peut plus détecter
aucune technique de cuisine. app attend qu'il soit healthy
(depends_on: condition: service_healthy) avant de démarrer — voir "Temps
de démarrage" ci-dessus pour combien de temps ça prend en pratique.
Testing
uv run pytest
tests/test_locale_pipeline_entities.py rejoue les cas d'offsets caractère
exacts et d'insensibilité accents/casse de
apps/api/test/recipe-matching/tech-step-matcher.test.ts — le point de
fidélité le plus critique de ce service (voir le plan de migration).
tests/conftest.py's fixture client (scope "session") ne s'entraîne
qu'une seule fois pour toute la suite — c'est le vrai corpus complet,
pas un jeu jouet, donc la première utilisation de cette fixture prend le
même temps qu'un vrai démarrage (voir "Temps de démarrage" ci-dessus).
Aucun test ici ne dépend d'une vraie base Postgres ni d'apps/api en
service — à l'inverse, la suite Mocha d'apps/api
(tech-step-matcher.test.ts/recipe-translation.test.ts) exige elle une
vraie instance de ce service tournant (voir apps/api/.env.test), conforme
à la convention du repo de ne jamais mocker un service interne.
Limitations connues
- Démarrage lent (~7 minutes) — voir "Temps de démarrage" ci-dessus.
Une optimisation possible non explorée : parallélisation de
l'entraînement
fr/en(actuellement séquentiel,PipelineRegistry.initialize). CONFIDENCE_THRESHOLDcôtéapps/apiest un placeholder depuis l'élargissement du corpus à ~74 techniques (calibré à la main, pas via une vraie repasse decalibrate-tech-step-threshold.tscontreTECH_STEP_EVAL_DATASET— voir le commentaire de cette constante).- Textcat bag-of-words (
spacy.TextCatBOW.v3) — suffisant pour le corpus actuel une fois correctement entraîné, mais n'exploite pas les vecteurs de mots des modèlesmdchargés. Migrable vers une architecture tok2vec/similarité sans changer le contrat HTTP, si le F1 mesuré parapps/api/src/scripts/calibrate-tech-step-threshold.tsle justifie un jour. - Reconstruit tout le pipeline à chaque démarrage (pas de persistance,
pas de fusion incrémentale) — un choix délibéré (voir
LocalePipeline.train), pas une limitation à lever :training_data.pydoit toujours rester l'unique source de vérité, jamais un état sur disque qui pourrait dériver.