* 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>
10 KiB
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). - 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), sanstextcatassocié : contrairement à une technique, un ustensile mentionné n'a pas besoin d'être interprété selon le contexte. Renvoyé dans la même listeentitiesque les techniques, discriminé parkind.
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 :
- Éditer
intent_service/training_data.pyà la main (informé par le rapport deapps/api/src/scripts/list-pending-training-suggestions.ts) pour une technique, ouintent_service/utensil_vocabulary.pypour 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'utterancesque les autres, par locale (voirtraining_data.py's own doc comment) — une technique ajoutée avec moins que le max courant, exécuteraugment_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 detraining_data.py) a dû être ajustée ou annulée après coup faute d'avoir vérifié le F1 avant de pousser. - 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 viaPOST /v1/train. - Depuis
apps/api, lancerpnpm --filter api exec tsx src/scripts/retrain-tech-steps.ts— vérifie le F1 contreTECH_STEP_EVAL_DATASETavant 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,200une 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, voirintent_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 },kindvalant"technique"ou"utensil"selon lePhraseMatcherqui a trouvé la mention (voir point 3 ci-dessus).apps/api'stech-step-matcher.tsfiltre parkindpour 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 :
{"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 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 — voir "Temps
de démarrage" ci-dessus pour combien de temps ça prend en pratique.
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).
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_THRESHOLDcôtéapps/apiest un placeholder depuis l'élargissement du corpus à ~74 techniques (calibré à la main, pas via une vraie repasse decalibrate-tech-step-threshold.tscontreTECH_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è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 démarrage (pas de persistance,
pas de fusion incrémentale) — un choix délibéré (voir
LocalePipeline.train), pas une limitation à lever :training_data.pydoit toujours rester l'unique source de vérité, jamais un état sur disque qui pourrait dériver.