batchCooking/services/tech-step-intent-service/README.md
Nicolas 18abae7b6a feat(recipes): migre la detection des tech steps de node-nlp vers un microservice Python spaCy
Remplace TechStepClassifierService's node-nlp (NlpManager) par
services/tech-step-intent-service, un microservice FastAPI/spaCy dedie
(PhraseMatcher pour le NER par synonymes, textcat pour la classification
d'intention). Corpus (TECH_STEP_TRAINING_DATA) toujours possede par
apps/api, pousse au service via POST /v1/train a chaque warm-up ; le
service ne touche jamais Postgres (meme posture que
services/tech-step-llm-worker).

Cote apps/api :
- intent-service-client.ts : client HTTP vers le nouveau service
- tech-step-matcher.ts : delegue NER + intent classification au client,
  logique pure (splitIntoClauses, seuil/fallback) inchangee
- env.ts : INTENT_SERVICE_BASE_URL/INTENT_SERVICE_SECRET (secret requis,
  service coeur non optionnel)
- server.ts : warm-up avec retry/backoff (service Python demarre a part)
- scripts/calibrate-tech-step-threshold.ts : recalibration empirique de
  CONFIDENCE_THRESHOLD contre le jeu d'eval existant
- node-nlp retire (package.json, node-nlp.d.ts, model.nlp du .gitignore)

docker-compose.yml : nouveau service tech-step-intent-service (pas de
port expose, healthcheck, app en depend). CI : job intent-service-test
(pytest) + le job test demarre le service en arriere-plan avant la suite
Mocha (jamais de mock d'un service interne, cf specs/dev-conventions.md).

Verifie : 26/26 tests pytest du service (dont les offsets caracteres
exacts de tech-step-matcher.test.ts), lint + build complets du monorepo,
smoke test HTTP reel bout en bout. La suite Mocha et docker compose
build/up n'ont pas pu etre executes dans cet environnement (pas de
Postgres/Docker disponibles ici) — a confirmer via la CI et en local.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 20:11:30 +02:00

112 lines
5.2 KiB
Markdown

# 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ê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, `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`](https://docs.astral.sh/uv/) pour ses dépendances
(`uv.lock` committé, `uv sync --frozen` partout — Dockerfile, CI, dev).
```bash
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
```bash
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.