* feat(recipes): associe ingredients, quantites et ustensiles aux techniques detectees
Etend le pipeline de detection de techniques (tech-step-matcher.ts) pour
resoudre, par clause, les metadonnees qui accompagnent une technique
detectee :
- Ingredients : nouvelle fonction findIngredientMentions (ingredient-matcher.ts)
qui scanne le texte d'une clause contre le catalogue Ingredient existant
(reutilise INGREDIENT_LABELS_FR/EN deja utilise par matchIngredientName),
avec extraction best-effort de la quantite+unite immediatement avant la
mention.
- Ustensiles : nouveau catalogue Utensil (Prisma) + second PhraseMatcher
cote service Python (intent_service/utensil_vocabulary.py), independant
du textcat des techniques (pas d'interpretation necessaire pour un
ustensile). POST /v1/process distingue desormais chaque entite via un
champ kind (technique|utensil).
- Persistance : deux nouvelles tables StepTechStepIngredient/
StepTechStepUtensil, liees a StepTechStep par sa cle composite
(stepId, order), peuplees au moment du matching (recipe.service.ts) et
exposees via StepTechStepView (packages/shared).
Aucune analyse syntaxique ajoutee (le parser spaCy reste exclu du
pipeline) : l'association se fait par appartenance a la clause deja
calculee par splitIntoClauses.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(recipes): corrige les tests casses par les nouveaux champs ingredients/utensils
recipe-tech-step-correction.test.ts asserte StepTechStepView en dur sans
les nouveaux champs ingredients/utensils (toujours [] pour une correction
manuelle, qui ne repasse jamais par le scan de metadonnees).
Retire aussi le nouveau cas de tech-step-matcher.test.ts qui inventait une
phrase jamais vue par le corpus reel : verifie en CI que le textcat la
classe avec confiance comme caramelize plutot que melt, un artefact du
petit corpus BOW plutot qu'un bug du code de matching. L'extraction
quantite+unite reste couverte integralement et de facon deterministe par
ingredient-matcher.test.ts.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* feat(recipes): equilibre le corpus d'entrainement du textcat a 20 phrases par technique
Chaque technique n'avait que 3 a 7 utterances par locale (moyenne ~3.8),
un desequilibre reel entre classes qui contribue directement a des
classifications confiantes mais fausses sur une formulation jamais vue
(constate concretement dans la PR precedente : une phrase inedite pour
melt classee comme caramelize avec une confiance elevee).
Porte chaque technique a exactement 20 utterances par locale (fr et en) :
- Les utterances existantes sont conservees telles quelles, jamais
reecrites.
- Le complement vient d'augment_utterances.py (nouveau script maintainer,
reutilisable pour une future technique sous-alimentee) : enveloppe
chaque utterance deja a l'imperatif/infinitif dans une tournure modale
grammaticalement valide (il faut/veillez a/make sure to...) plutot que
de dupliquer ou d'inventer du texte generique - vraie diversite de
surface, vocabulaire distinctif de la technique intact.
- tests/test_training_data_balance.py fait respecter l'invariant en CI
(20 minimum, meme nombre fr/en) pour toute future modification.
_TRAINING_ITERATIONS recalibre de 25 a 10 (locale_pipeline.py) pour
compenser les ~2.6x d'exemples par epoque : temps d'entrainement mesure
quasi identique a avant (~687s fr+en combines contre ~670s), confiance
egale ou meilleure sur les cas deja suivis (simmer 0.31 -> 0.48).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(recipes): remonte _TRAINING_ITERATIONS a 20, la gate F1 de CI etait sous 0.8 a 10
Le premier passage CI de l'equilibrage du corpus (20 utterances/technique)
a fait chuter le F1 agrege (tech-step-eval.test.ts) a 0.7999... avec
_TRAINING_ITERATIONS=10 : le pari qu'un corpus plus large convergerait en
moins d'epoques relatives etait faux a ce niveau de reduction. Remonte a
20 (mesure : ~699s pour la seule locale fr, previsiblement ~1360s pour
fr+en combines) - confiance nettement retablie sur les techniques
auparavant en echec au spot-check manuel (sweat ~0.99).
Consequence directe : le temps de demarrage du service passe d'environ
11 a environ 23 minutes. start_period (docker-compose.yml) et le timeout
d'attente /health (ci.yml) releves de 900s a 1800s en consequence.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(recipes): reequilibre le corpus via substitution de synonyme plutot que du remplissage generique
Deux tentatives precedentes de porter chaque technique a 20 utterances
ont mesurablement degrade le F1 agrege (tech-step-eval.test.ts, 0.80 ->
0.79/0.791) au lieu de l'ameliorer : le generateur reposait surtout sur
des tournures modales generiques ("il faut ...", "make sure to ..."),
partagees identiquement par les 74 classes - un textcat bag-of-words lit
ca comme une separabilite reduite entre classes, pas un padding neutre.
augment_utterances.py revu : priorite a la substitution de synonyme
(l'un des synonyms propres a la technique en tete d'une utterance
existante, remplace par un autre - vocabulaire genuinement distinctif),
les tournures modales ne servant plus qu'de complement limite (5 par
locale, pas 12). Resultat : 13 a 20 utterances par technique/locale
(moyenne ~19.7), contre un forcage uniforme a 20 qui necessitait un
remplissage generique disproportionne pour les techniques au vocabulaire
propre pauvre (julienne, sweat, bainMarie - precisement celles qui
echouaient). Confiance mesuree nettement retablie sur ces techniques
(sweat ~0.99, bainMarie ~0.98, julienne ~0.88).
tests/test_training_data_balance.py : plancher abaisse a 12 (vise 20,
garanti seulement si le vocabulaire propre de la technique le permet
sans repasser par le piege ci-dessus) ; suppression de l'exigence
fr/en egaux, plus vraie avec cette strategie (le potentiel de
substitution differe naturellement entre les deux langues).
Suite complete locale : 35/35 verts (22m26s).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* revert(recipes): annule le reequilibrage du corpus d'entrainement du textcat
Trois strategies de generation differentes (tournures modales generiques,
tournures reduites + substitution de synonyme, substitution de synonyme
en priorite) ont ete tentees pour porter chaque technique a 20 utterances
par locale. Les trois degradent mesurablement le F1 agrege contre
TECH_STEP_EVAL_DATASET (tech-step-eval.test.ts) en dessous du seuil 0.8 :
0.7999 -> 0.791 -> 0.744 (chaque tentative pire que la precedente).
tech-step-eval-runner.ts documente explicitement ce seuil comme calibre
avec une marge deja tres etroite (0.8 pour un score mesure a 0.815) et
previent contre le fait de l'assouplir pour accommoder un classifieur
plus faible plutot que de corriger le probleme de fond - assouplir le
seuil ou le jeu d'evaluation pour faire passer cette PR irait a l'encontre
de cette convention documentee du projet.
Revient a l'etat d'avant tout reequilibrage (corpus a 3-7 utterances/
technique, _TRAINING_ITERATIONS=25, timeouts a 900s) - le dernier etat
confirme vert en CI sur cette branche. Ameliorer reellement l'equilibre
du corpus necessite du contenu redige a la main et verifie technique par
technique contre ce meme F1, pas une generation programmatique en bloc.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(recipes): reequilibre le corpus via substitution de synonyme plutot que du remplissage generique
Trois tentatives precedentes d'egaliser chaque technique a 20 utterances
ont toutes degrade le F1 agrege sous 0.8 (voir le commit revert
precedent). Nouvelle strategie, beaucoup plus conservatrice : egalise
chaque technique vers le maximum DEJA present dans le corpus (7 en fr,
5 en en, portes par cook/preheat), pas vers un nombre choisi dans
l'absolu - +3-4 utterances en moyenne par technique au lieu de +13-17.
augment_utterances.py (nouveau, reutilisable) genere le complement en
priorite par substitution de synonyme (un des synonyms propres a la
technique, en tete d'une utterance existante, remplace par un autre) -
avec un garde-fou supplementaire par rapport aux tentatives precedentes :
le synonyme de remplacement doit lui aussi etre a l'imperatif/infinitif,
pas juste le synonyme d'origine, pour eviter de substituer un groupe
nominal/adjectif ("a petit feu", "gros bouillons") a la place d'un
verbe et produire une phrase grammaticalement cassee. Tournures modales
uniquement en dernier recours pour les techniques dont le vocabulaire
n'apparait qu'en milieu de phrase (julienne, brunoise...).
Resultat : chaque technique a exactement 7 utterances en fr et 5 en en,
sans exception (tests/test_training_data_balance.py fait respecter cet
invariant). _TRAINING_ITERATIONS reste a 25 (inchange). start_period/
timeout d'attente /health releves de 900s a 1200s (temps d'entrainement
mesure ~930s contre ~670s avant, la marge de securite existante etait
devenue trop juste).
Suite complete locale : 35/35 verts (14m41s).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* chore: retrigger CI (aucun run genere pour c7116d4, probable incident GitHub Actions)
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
198 lines
10 KiB
Markdown
198 lines
10 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).
|
|
3. **NER par phrases, ustensiles** (`spacy.matcher.PhraseMatcher`, second
|
|
matcher indépendant) — trouve les mentions d'un ustensile de cuisine
|
|
(`intent_service/utensil_vocabulary.py`, `UTENSIL_VOCABULARY`), sans
|
|
`textcat` associé : contrairement à une technique, un ustensile mentionné
|
|
n'a pas besoin d'être interprété selon le contexte. Renvoyé dans la même
|
|
liste `entities` que les techniques, discriminé par `kind`.
|
|
|
|
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 :
|
|
|
|
1. Éditer `intent_service/training_data.py` à la main (informé par le
|
|
rapport de `apps/api/src/scripts/list-pending-training-suggestions.ts`)
|
|
pour une technique, ou `intent_service/utensil_vocabulary.py` pour un
|
|
ustensile (pas de rapport équivalent pour ce dernier — pas de mécanisme
|
|
de correction utilisateur sur les ustensiles aujourd'hui). Chaque
|
|
technique doit garder le même nombre d'`utterances` que les autres, par
|
|
locale (voir `training_data.py`'s own doc comment) — une technique
|
|
ajoutée avec moins que le max courant, exécuter `augment_utterances.py`
|
|
(racine de ce service) pour rééquilibrer, puis **impérativement**
|
|
relancer l'étape 3 ci-dessous avant de committer : chaque tentative
|
|
passée d'élargir ce corpus (voir l'historique Git de
|
|
`training_data.py`) a dû être ajustée ou annulée après coup faute
|
|
d'avoir vérifié le F1 avant de pousser.
|
|
2. **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 via `POST /v1/train`.
|
|
3. Depuis `apps/api`, lancer `pnpm --filter api exec tsx
|
|
src/scripts/retrain-tech-steps.ts` — vérifie le F1 contre
|
|
`TECH_STEP_EVAL_DATASET` avant 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, `200` une 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, voir `intent_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, kind }], intent, score }`,
|
|
`kind` valant `"technique"` ou `"utensil"` selon le `PhraseMatcher` qui a
|
|
trouvé la mention (voir point 3 ci-dessus). `apps/api`'s `tech-step-matcher.ts`
|
|
filtre par `kind` pour savoir laquelle des deux résoudre (`TechStep`/`Utensil`).
|
|
|
|
`/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, chaque technique entraînée sur ses `synonyms`
|
|
en plus de ses `utterances` — voir `locale_pipeline.py`) prend de l'ordre
|
|
de 540 secondes pour `fr` / 390 secondes pour `en` (mesuré localement,
|
|
sans GPU), donc environ 930 secondes (~15-16 minutes) pour `fr`+`en`
|
|
combinés à chaque démarrage du process — chaque technique a désormais le
|
|
même nombre d'`utterances` par locale (voir `training_data.py`'s own doc
|
|
comment), légèrement plus qu'avant ce rééquilibrage. `docker-compose.yml`
|
|
et `.github/workflows/ci.yml` ont un `start_period`/timeout d'attente
|
|
généreux pour ça (`1200s`) — 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 :
|
|
|
|
```json
|
|
{"timestamp": "...", "level": "info", "message": "tech-step NLP process", "locale": "fr", "text": "faire fondre le beurre", "entities": [{"uid": "melt", "start": 6, "end": 13, "kind": "technique"}], "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 — voir "Temps
|
|
de démarrage" ci-dessus pour combien de temps ça prend en pratique.
|
|
|
|
## 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).
|
|
`tests/test_utensil_matching.py` couvre le second `PhraseMatcher`
|
|
(ustensiles) de la même façon, contre le vocabulaire réel (statique, pas
|
|
besoin d'un jeu de test dédié comme pour les techniques).
|
|
`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** (~15-16 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_THRESHOLD` côté `apps/api` est un placeholder** depuis
|
|
l'élargissement du corpus à ~74 techniques (calibré à la main, pas via
|
|
une vraie repasse de `calibrate-tech-step-threshold.ts` contre
|
|
`TECH_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è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 démarrage** (pas de persistance,
|
|
pas de fusion incrémentale) — un choix délibéré (voir
|
|
`LocalePipeline.train`), pas une limitation à lever : `training_data.py`
|
|
doit toujours rester l'unique source de vérité, jamais un état sur disque
|
|
qui pourrait dériver.
|