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> |
||
|---|---|---|
| .. | ||
| 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).
Pourquoi ce service ne possède aucune donnée d'entraînement
Contrairement à un service NLP habituel, ce service ne connaît aucune
technique par lui-même — apps/api reste l'unique source de vérité du
corpus (TECH_STEP_TRAINING_DATA,
apps/api/src/lib/recipe-matching/tech-step-training-data.ts, revu par PR
comme le reste du code). Il pousse l'intégralité du corpus ici via
POST /v1/train à chaque warm-up serveur (TechStepClassifierService._train)
— ce service (re)construit alors son pipeline en mémoire, sans jamais rien
persister sur disque. Le workflow mainteneur existant
(apps/api/src/scripts/retrain-tech-steps.ts, édition manuelle du corpus)
n'a pas changé.
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._train), 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 les modèles spaCy de base chargés (pas de lazy-load, voirintent_service/main.py).POST /v1/train—{ locale, entries: [{ uid, synonyms, utterances }] }→ reconstruit le pipeline delocaleà neuf.POST /v1/process—{ locale, text }→{ entities: [{ uid, start, end }], intent, score }.
/v1/train et /v1/process exigent le header X-Intent-Service-Secret
(voir intent_service/security.py), qui doit matcher INTENT_SERVICE_SECRET
côté apps/api.
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 et routes/train.py journalisent
chaque appel avec son input et son output complets :
{"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.
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).
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 (première version)
/v1/trainprend de l'ordre de la minute par locale (~110s mesuré en CI avec_TRAINING_ITERATIONS/_TRAINING_BATCH_SIZEactuels, voirlocale_pipeline.py) —apps/apil'appelle deux fois au warm-up (fr/en), donc un redémarrage prend quelques minutes avant qu'une recette puisse voir ses techniques détectées. Contrairement à node-nlp (entraînement quasi instantané), c'est un vrai compromis assumé : moins d'itérations entraînait plus vite mais laissait des verdicts corrects sousCONFIDENCE_THRESHOLD(voir le commentaire de cette constante,apps/api/src/lib/recipe-matching/tech-step-matcher.ts).- 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
/v1/train(pas de fusion incrémentale) — un choix délibéré (voirLocalePipeline.train), pas une limitation à lever :TECH_STEP_TRAINING_DATAdoit toujours rester l'unique source de vérité, jamais un état local qui dérive.