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>
78 lines
3.1 KiB
Python
78 lines
3.1 KiB
Python
"""Couvre `LocalePipeline`'s second `PhraseMatcher` (ustensiles,
|
|
`utensil_vocabulary.py`) — même style que `test_locale_pipeline_entities.py`
|
|
(offsets exacts, insensibilité accents/casse), mais contre le vocabulaire
|
|
*réel* (`UTENSIL_VOCABULARY`, statique, construit par `preload()` — pas
|
|
besoin d'un jeu de test dédié comme pour les techniques, voir
|
|
`LocalePipeline.preload`'s own comment)."""
|
|
|
|
from intent_service.locale_pipeline import LocalePipeline, TrainEntry
|
|
|
|
# Un `train()` minimal suffit — le `PhraseMatcher` d'ustensiles est
|
|
# construit par `preload()` (appelé par `train()`), indépendamment du
|
|
# `TrainEntry` de techniques passé ici (voir `preload()`'s own comment sur
|
|
# pourquoi les deux ne sont pas couplés).
|
|
_MINIMAL_ENTRIES = [
|
|
TrainEntry(uid="melt", synonyms=["fondre"], utterances=["faire fondre le beurre"]),
|
|
TrainEntry(uid="simmer", synonyms=["mijoter"], utterances=["faire mijoter à feu doux"]),
|
|
]
|
|
|
|
|
|
def _fr_pipeline() -> LocalePipeline:
|
|
pipeline = LocalePipeline("fr")
|
|
pipeline.train(_MINIMAL_ENTRIES)
|
|
return pipeline
|
|
|
|
|
|
def test_matches_a_real_utensil_with_exact_span():
|
|
pipeline = _fr_pipeline()
|
|
text = "Dans une poêle chaude, faire fondre le beurre"
|
|
result = pipeline.process(text)
|
|
|
|
pan_entities = [e for e in result.entities if e.uid == "pan"]
|
|
assert len(pan_entities) == 1
|
|
entity = pan_entities[0]
|
|
assert entity.kind == "utensil"
|
|
assert text[entity.start : entity.end] == "poêle"
|
|
|
|
|
|
def test_is_case_and_accent_insensitive():
|
|
pipeline = _fr_pipeline()
|
|
result = pipeline.process("Verser dans la POÊLE")
|
|
utensil_uids = [e.uid for e in result.entities if e.kind == "utensil"]
|
|
assert utensil_uids == ["pan"]
|
|
|
|
|
|
def test_matches_a_multi_word_synonym():
|
|
pipeline = _fr_pipeline()
|
|
text = "Découper les légumes sur la planche à découper"
|
|
result = pipeline.process(text)
|
|
board_entities = [e for e in result.entities if e.uid == "cuttingBoard"]
|
|
assert len(board_entities) == 1
|
|
assert text[board_entities[0].start : board_entities[0].end] == "planche à découper"
|
|
|
|
|
|
def test_technique_and_utensil_are_both_returned_without_interfering():
|
|
pipeline = _fr_pipeline()
|
|
text = "Dans une casserole, faire mijoter à feu doux"
|
|
result = pipeline.process(text)
|
|
|
|
kinds_by_uid = {e.uid: e.kind for e in result.entities}
|
|
assert kinds_by_uid.get("simmer") == "technique"
|
|
assert kinds_by_uid.get("saucepan") == "utensil"
|
|
|
|
|
|
def test_returns_no_utensil_entities_when_none_are_mentioned():
|
|
pipeline = _fr_pipeline()
|
|
result = pipeline.process("Laisser reposer la pâte une heure")
|
|
assert [e for e in result.entities if e.kind == "utensil"] == []
|
|
|
|
|
|
def test_matches_english_utensils_too():
|
|
pipeline = LocalePipeline("en")
|
|
pipeline.train([TrainEntry(uid="chop", synonyms=["chop"], utterances=["chop the onions finely"])])
|
|
text = "Heat the pan before adding the onions"
|
|
result = pipeline.process(text)
|
|
pan_entities = [e for e in result.entities if e.uid == "pan"]
|
|
assert len(pan_entities) == 1
|
|
assert pan_entities[0].kind == "utensil"
|
|
assert text[pan_entities[0].start : pan_entities[0].end] == "pan"
|