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>
137 lines
6.6 KiB
Markdown
137 lines
6.6 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`.
|
|
|
|
## 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 :
|
|
|
|
```json
|
|
{"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`](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)
|
|
|
|
- **`/v1/train` prend de l'ordre de la minute par locale** (~110s mesuré en
|
|
CI avec `_TRAINING_ITERATIONS`/`_TRAINING_BATCH_SIZE` actuels, voir
|
|
`locale_pipeline.py`) — `apps/api` l'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
|
|
sous `CONFIDENCE_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è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.
|