batchCooking/services/tech-step-intent-service
Nicolas 690125bec3 fix(recipes): corrige les matches dupliques et le timeout de warm-up des tests CI
Deux bugs reels trouves par la premiere execution CI de la migration
node-nlp -> tech-step-intent-service :

1. PhraseMatcher retourne tous les matches y compris chevauchants — un
   synonyme comme "fondre" litteralement contenu dans "faire fondre" (tous
   deux synonymes de `melt`) produisait deux candidats separes pour la meme
   technique, dupliquant son techStepId dans le resultat final. Fixe avec
   spacy.util.filter_spans (garde le plus long match par position) dans
   LocalePipeline.process. Test de non-regression ajoute.

2. La suite Mocha construit `app` directement via createApp(), sans jamais
   passer par server.ts — le warm-up (POST /v1/train fr+en sur le corpus
   complet) se declenchait donc paresseusement dans le premier test qui
   appelait le classifieur, depassant le timeout Mocha de 10s par test.
   Fixe par un root hook plugin Mocha (test-support/mocha-root-hooks.ts,
   .mocharc.json) qui reset la DB et warm up le classifieur une seule fois
   avant toute suite, avec son propre timeout de 60s.

Verifie : 27/27 tests pytest du service (dont le nouveau test de
non-regression), lint + build complets du monorepo. La suite Mocha
elle-meme n'a toujours pas pu etre executee dans cet environnement (pas de
Postgres disponible ici) — a confirmer via la CI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 20:23:35 +02:00
..
intent_service fix(recipes): corrige les matches dupliques et le timeout de warm-up des tests CI 2026-08-25 20:23:35 +02:00
tests fix(recipes): corrige les matches dupliques et le timeout de warm-up des tests CI 2026-08-25 20:23:35 +02:00
.env.example feat(recipes): migre la detection des tech steps de node-nlp vers un microservice Python spaCy 2026-08-25 20:11:30 +02:00
.gitignore feat(recipes): migre la detection des tech steps de node-nlp vers un microservice Python spaCy 2026-08-25 20:11:30 +02:00
Dockerfile feat(recipes): migre la detection des tech steps de node-nlp vers un microservice Python spaCy 2026-08-25 20:11:30 +02:00
pyproject.toml feat(recipes): migre la detection des tech steps de node-nlp vers un microservice Python spaCy 2026-08-25 20:11:30 +02:00
README.md feat(recipes): migre la detection des tech steps de node-nlp vers un microservice Python spaCy 2026-08-25 20:11:30 +02:00
uv.lock feat(recipes): migre la detection des tech steps de node-nlp vers un microservice Python spaCy 2026-08-25 20:11:30 +02:00

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) :

  1. NER par phrases (spacy.matcher.PhraseMatcher) — trouve les mentions candidates d'une technique dans un texte, à partir des synonyms de chaque technique.
  2. Classification d'intention (textcat spaCy, bag-of-words) — verdict de la technique qu'une clause de texte signifie, entraîné sur les utterances de 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êmeapps/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, 200 une fois les modèles spaCy de base chargés (pas de lazy-load, voir intent_service/main.py).
  • POST /v1/train{ locale, entries: [{ uid, synonyms, utterances }] } → reconstruit le pipeline de locale à 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.

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-workerpas 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)

  • Textcat bag-of-words (spacy.TextCatBOW.v3) — suffisant/rapide pour le corpus actuel, mais n'exploite pas les vecteurs de mots des modèles md chargés. Migrable vers une architecture tok2vec/similarité sans changer le contrat HTTP, si le F1 mesuré par apps/api/src/scripts/calibrate-tech-step-threshold.ts le justifie un jour.
  • Reconstruit tout le pipeline à chaque /v1/train (pas de fusion incrémentale) — un choix délibéré (voir LocalePipeline.train), pas une limitation à lever : TECH_STEP_TRAINING_DATA doit toujours rester l'unique source de vérité, jamais un état local qui dérive.