Changement d'architecture demande par l'utilisateur : le dataset d'entrainement (TECH_STEP_TRAINING_DATA) quitte apps/api pour vivre entierement dans services/tech-step-intent-service (intent_service/training_data.py). Ce service est desormais autonome : il s'entraine lui-meme une seule fois, a son propre demarrage (PipelineRegistry.initialize, dans le lifespan FastAPI), sans plus dependre d'un POST /v1/train pousse par apps/api (route supprimee). apps/api ne connait plus aucune technique/synonyme, uniquement le resultat de POST /v1/process. Corpus enrichi avec les 48 techniques du lexique fourni (Arroser, Appertiser, Braiser, Caraméliser, Confire, Julienne/Brunoise/Mirepoix/ Paysanne, Cuire à blanc/au bain-marie/à l'étouffée, Déglacer variantes, Emulsionner, Glacer, Pocher, Réduire, Suer, Zester, etc.), soit 74 techniques au total (26 + 48). Integration complete bout en bout : - reference-seed-data.ts : 48 nouvelles entrees TECH_STEPS - apps/web/locales/fr/translation.json : libelles francais correspondants - "Mitonner" fondu comme synonyme de simmer (pas une technique distincte, sa propre definition le dit) - "Blanchir un oeuf" (whiskPale) distingue de "Blanchir un legume" (blanch, existant) via des synonymes en phrase complete plutot qu'au mot nu — filter_spans (deja en place) resout la collision par specificite Impact performance mesure : le corpus elargi (74 classes vs 26) rend l'entrainement bien plus lent a nombre d'iterations egal (150 iterations depassait 17 minutes par run de test) — reduit a 40 iterations apres mesures repetees en local (~200s/locale, ~400s pour fr+en combines). docker-compose.yml (healthcheck start_period 600s), CI (timeout curl 600s) et le README du service documentent ce nouveau temps de demarrage. CONFIDENCE_THRESHOLD recalibre a 0.2 par verification manuelle (0.75 puis 0.45 ne tenaient plus compte tenu du nombre de classes) — marque explicitement comme placeholder en attendant une vraie repasse de calibrate-tech-step-threshold.ts (necessite Postgres, indisponible dans cet environnement). Verifie : 28/28 tests pytest du service (suite complete re-ecrite pour s'entrainer une seule fois par session sur le vrai corpus, fixture partagee dans conftest.py), lint + build complets du monorepo. La suite Mocha d'apps/api reste a confirmer via CI (le root hook mocha n'attend plus l'entrainement, seulement CI's propre attente sur /health). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
77 lines
3.8 KiB
Python
77 lines
3.8 KiB
Python
"""Logging structuré — même convention que `LoggerService` côté `apps/api`
|
|
(`apps/api/src/lib/logger.service.ts`) : une ligne JSON par évènement
|
|
(`timestamp`, `level`, `message`, + le reste des champs fournis fusionné),
|
|
jamais du texte libre, pour rester grep/parse-able par `docker logs`/
|
|
Portainer ou un agrégateur de logs — cohérent avec le reste du repo plutôt
|
|
qu'un format propre à ce seul service.
|
|
|
|
Configuré une fois au démarrage (`main.py`) plutôt que par un `print()` ad
|
|
hoc dans chaque route — `routes/process.py`/`pipeline_registry.py` appellent
|
|
`logging.getLogger(__name__)` normalement, ce module ne fait que brancher le
|
|
formateur JSON sur la racine du logging Python.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import sys
|
|
from datetime import UTC, datetime
|
|
from typing import Any
|
|
|
|
|
|
class _JsonFormatter(logging.Formatter):
|
|
"""Sérialise chaque `LogRecord` en une ligne JSON. Les champs
|
|
supplémentaires passés via `logger.info(msg, extra={...})` sont fusionnés
|
|
tels quels dans l'objet — c'est ce que `routes/process.py` utilise pour
|
|
joindre `locale`/`text`/`entities`/`intent`/`score` à la ligne."""
|
|
|
|
# Attributs standards de `LogRecord` — tout le reste posé sur le record
|
|
# (via `extra=`) est un champ métier ajouté par l'appelant, à fusionner
|
|
# dans la sortie JSON.
|
|
_STANDARD_ATTRS = frozenset(logging.LogRecord("", 0, "", 0, "", None, None).__dict__.keys())
|
|
|
|
def format(self, record: logging.LogRecord) -> str:
|
|
payload: dict[str, Any] = {
|
|
"timestamp": datetime.fromtimestamp(record.created, tz=UTC).isoformat(),
|
|
"level": record.levelname.lower(),
|
|
"message": record.getMessage(),
|
|
}
|
|
extra_fields = {
|
|
key: value for key, value in record.__dict__.items() if key not in self._STANDARD_ATTRS
|
|
}
|
|
payload.update(extra_fields)
|
|
if record.exc_info:
|
|
payload["error"] = self.formatException(record.exc_info)
|
|
return json.dumps(payload, ensure_ascii=False, default=str)
|
|
|
|
|
|
def configure_logging(level: str) -> None:
|
|
"""Branche le formateur JSON sur la racine du logging Python — appelé
|
|
une fois au démarrage (`main.py`), avant que `routes/*` ne journalisent
|
|
quoi que ce soit."""
|
|
# L'encodage par défaut de `sys.stdout` suit la locale de l'OS/console,
|
|
# pas forcément UTF-8 — sur Windows en particulier, garder ce défaut
|
|
# produit de vrais octets invalides (pas juste un affichage terminal
|
|
# trompeur) pour tout texte accentué journalisé par `routes/process.py`
|
|
# (le texte réel des étapes de recette, en français) — trouvé en
|
|
# vérifiant les octets bruts d'un log réel, pas juste son affichage.
|
|
# `reconfigure` existe sur `sys.stdout` dans toute exécution Python
|
|
# normale (pas dans certains contextes embarqués/redirigés exotiques) —
|
|
# protégé par `hasattr` pour ne jamais faire planter le démarrage pour un
|
|
# souci de confort d'affichage.
|
|
if hasattr(sys.stdout, "reconfigure"):
|
|
sys.stdout.reconfigure(encoding="utf-8")
|
|
handler = logging.StreamHandler(sys.stdout)
|
|
handler.setFormatter(_JsonFormatter())
|
|
root = logging.getLogger()
|
|
root.handlers = [handler]
|
|
root.setLevel(level)
|
|
|
|
# spaCy/thinc journalisent leur propre chatter interne ("Created
|
|
# vocabulary", "Finished initializing nlp object"...) sur le logger
|
|
# `"spacy"`, qui propage jusqu'à la racine et se retrouverait donc
|
|
# mélangé aux lignes input/output de `routes/process.py`/l'entraînement
|
|
# journalisé par `pipeline_registry.py`
|
|
# — ce sont ces dernières que ce service existe pour rendre visibles, pas
|
|
# le détail interne de spaCy. `WARNING` laisse quand même remonter un
|
|
# vrai problème (dépréciation, échec partiel) sans le bruit `INFO`.
|
|
logging.getLogger("spacy").setLevel(logging.WARNING)
|