3e type de metadonnee de clause, a cote des ingredients et des ustensiles : temperature en °C (« 180°C », « 200 degres », °F converti), numero de thermostat (« th. 6 ») et intensite qualitative (« feu doux/moyen/vif » -> low/medium/high). services/tech-step-intent-service : - `temperature_extraction.py` : `extract_temperatures(text, locale)` pur, a base de regex (independant du modele spaCy et de l'entrainement). - `ProcessResult` / `ProcessResponse` gagnent `temperatures` (ajout additif au contrat ; cle `gas_mark` en snake_case sur le fil). `process()` les extrait meme pour une locale pas encore entrainee. - Tests `test_temperature_extraction.py` (10) ; `test_routes_process.py` : 2 assertions d'egalite stricte gagnent `"temperatures": []`, +1 cas. apps/api : - `IntentServiceTemperature` (mappe `gas_mark` -> `gasMark` a la frontiere). - `TechStepMatch.temperatures` : filtrees par appartenance de span a la clause, meme regle que les ustensiles. - `model StepTechStepTemperature` (aucune FK — la valeur structuree EST la donnee) + migration manuelle `20260829120000_step_tech_step_temperature` (cascade via le TRUNCATE de `step_tech_step`, rien a ajouter a reset-db.ts). Persistance + lecture dans `recipe.service.ts` (`recipeInclude`, `toStepTechStepViews`) et l'include de sequence fraiche du service de correction ; `sources.service.ts` (apercu d'import) les fait transiter. packages/shared : `StepTechStepTemperatureView` + `temperatures` sur `StepTechStepView`. apps/web : - `splitDescriptionSegments` enrobe `splitDescriptionByTechSteps` et redecoupe les segments non-keyword autour des spans de temperature (la logique technique intriquee reste intacte). `?? []` tolere une payload d'avant `temperatures`. - `StepDescription` : surlignage `.step-temperature` + Tooltip via `temperatureLabel` ; i18n `recipes.temperature.*`. - Tests composants +4 (22 verts) ; pas d'UI de correction (v1, comme les ustensiles). Verifie : biome + tsc + `pnpm -r build` ; web 102/103 e2e (l'echec est le flake pre-existant recipe-form.feature, sans rapport) + 49/49 composants ; pytest temperature + routes 15/15. `pnpm --filter api test` (Postgres + intent-service requis) non lance ici. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
74 lines
2.8 KiB
Python
74 lines
2.8 KiB
Python
"""Contrat JSON de `POST /v1/process` — voir `schemas.py`/`routes/process.py`.
|
|
|
|
Le service s'entraîne désormais lui-même au démarrage sur le vrai corpus
|
|
(`training_data.TECH_STEP_TRAINING_DATA`, voir `conftest.py`'s fixture
|
|
`client` partagée) — ces tests vérifient donc le contrat HTTP contre des
|
|
phrases réelles du corpus, plus besoin d'un `POST /v1/train` préalable avec
|
|
des données jouets.
|
|
"""
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
from intent_service.config import settings
|
|
|
|
_HEADERS = {"X-Intent-Service-Secret": settings.intent_service_secret}
|
|
|
|
|
|
def test_process_against_an_unsupported_locale_returns_empty_result(client: TestClient):
|
|
# "de" n'a aucun modèle spaCy connu (`SUPPORTED_LOCALES`) — se comporte
|
|
# comme "jamais entraîné" côté `/v1/process`, jamais une erreur (voir
|
|
# `PipelineRegistry.process`).
|
|
response = client.post(
|
|
"/v1/process", headers=_HEADERS, json={"locale": "de", "text": "faire mijoter à feu doux"}
|
|
)
|
|
assert response.status_code == 200
|
|
assert response.json() == {"entities": [], "intent": None, "score": 0.0, "temperatures": []}
|
|
|
|
|
|
def test_process_returns_entities_and_intent_for_a_real_corpus_sentence(client: TestClient):
|
|
response = client.post(
|
|
"/v1/process", headers=_HEADERS, json={"locale": "fr", "text": "Faire mijoter à feu doux"}
|
|
)
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
assert body["intent"] == "simmer"
|
|
assert body["score"] > 0
|
|
assert [entity["uid"] for entity in body["entities"]] == ["simmer"]
|
|
|
|
|
|
def test_process_matches_english_text_against_the_english_trained_vocabulary(client: TestClient):
|
|
response = client.post(
|
|
"/v1/process", headers=_HEADERS, json={"locale": "en", "text": "Chop the onions finely"}
|
|
)
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
assert [entity["uid"] for entity in body["entities"]] == ["chop"]
|
|
|
|
|
|
def test_process_with_blank_text_returns_empty_result(client: TestClient):
|
|
response = client.post("/v1/process", headers=_HEADERS, json={"locale": "fr", "text": " "})
|
|
assert response.status_code == 200
|
|
assert response.json() == {"entities": [], "intent": None, "score": 0.0, "temperatures": []}
|
|
|
|
|
|
def test_process_extracts_temperatures_alongside_entities(client: TestClient):
|
|
response = client.post(
|
|
"/v1/process",
|
|
headers=_HEADERS,
|
|
json={"locale": "fr", "text": "Enfourner à 180°C puis finir à feu doux"},
|
|
)
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
temps = body["temperatures"]
|
|
assert {
|
|
"start": 12,
|
|
"end": 17,
|
|
"celsius": 180.0,
|
|
"gas_mark": None,
|
|
"qualitative": None,
|
|
"raw": "180°C",
|
|
} in temps
|
|
assert any(
|
|
temp["qualitative"] == "low" and temp["celsius"] is None and temp["raw"] == "feu doux"
|
|
for temp in temps
|
|
)
|