Skip to content

DataScientest/jardin-classify

Repository files navigation

jardin-classify

Dépôt compagnon du module Méthodologie de projet (parcours MLOps / LLMOps).

Un jardin partagé municipal reçoit des messages d'adhérents (fuite d'eau, outil cassé, réservation de parcelle…). Ce projet propose automatiquement une catégorie (modèle ML mocké) et une priorité (appel LLM mocké) pour chaque message, avec un refus propre (needs_review) quand le message est vide ou hors périmètre.

Tout est déterministe et hors ligne : aucune clé d'API réelle, aucun réseau, aucun entraînement. Le but est de montrer la méthode, pas le modèle.

Naviguer dans les phases

Le dépôt évolue par itérations additives : chaque phase ajoute des briques, aucune ne réécrit les précédentes. Un tag Git marque la fin de chaque phase :

Tag Phase Semaine Contenu ajouté
p1 Phase 1 — Make it work S8 le plus petit slice propre (CLI + mocks + secrets hors code)
p2 Phase 2 — Make it explicit S12 contrats Pydantic, tests, artefact modèle (train), Docker par cycle de vie
p3 Phase 3 — Make it reliable S16 API FastAPI, timeout + retry, évaluation batch, métrique
p4 Phase 4 — Make it clean & operable S22 ADR, runbook, détection de drift (job), déploiement + rollback
git checkout p1   # voir le dépôt tel qu'il était en fin de Phase 1
git checkout p2   # ... et ainsi de suite : p3, p4
git checkout main # revenir à la version finale

Lancer le slice

Prérequis : uv (et Docker à partir de la Phase 2).

# 1. Configurer les variables d'environnement (secrets hors du code)
cp .env.example .env

# 2. Installer les dépendances (aucune en Phase 1)
uv sync   # crée .venv et installe les dépendances depuis uv.lock

# 3. Classifier un message
uv run python -m src.cli "fuite d'eau sous le robinet"
# {"category": "plomberie", "priority": "haute", "status": "ok"}

uv run python -m src.cli "bonjour, où est la clé de la cabane ?"
# {"category": "autre", "priority": "basse", "status": "ok"}

uv run python -m src.cli "???"
# {"category": null, "priority": null, "status": "needs_review"}

Tester, entraîner et relancer (Phase 2)

Le dépôt suit la règle « un service par cycle de vie, pas par fonction » : train tourne parfois puis s'arrête (il produit l'artefact du modèle) ; l'application, elle, se relance à la demande. Les deux ne communiquent que par l'artefact models/category_model_v1.json.

# Les tests passent sans aucune configuration
uv run pytest

# « Entraîner » le modèle : produit models/category_model_v1.json puis s'arrête
docker compose run train

# Relance reproductible par un tiers
docker compose up --build app

Servir, surveiller et évaluer (Phase 3)

Le service app devient api : troisième cycle de vie visible.

docker compose up --build -d api    # tourne TOUJOURS
docker compose run evaluate         # PLANIFIÉ : trace un run puis s'arrête
docker compose ps -a                # Up / Exited(0) : les cycles de vie sous vos yeux
docker compose down                 # nettoyage quand vous avez fini

curl -s -X POST http://localhost:8000/classify \
  -H "Content-Type: application/json" \
  -d '{"message": "fuite d'"'"'eau sous le robinet"}'
# {"request_id": "…", "category": "plomberie", "priority": "haute", "status": "ok"}

curl -s http://localhost:8000/health
# {"status": "ok"}

curl -s http://localhost:8000/metrics
# {"requests": 1, "latency_p95_ms": …, "threshold_ms": 500}

Seuils documentés : latence p95 de /classify < 500 ms (voir /metrics) ; accuracy d'évaluation ≥ 0,8 (sinon evaluate sort en code 1 et la version ne doit pas être promue). Le request_id présent dans chaque réponse et chaque log permet de retrouver la requête fautive.

La panne du provider est simulée (pas subie) dans tests/test_reliability.py : timeout → 503 + request_id, retry borné à 1 essai + 2 tentatives, entrée invalide → 422.

Exploiter (Phase 4)

# Détection de drift : distribution récente vs jeu de référence
docker compose run --rm drift
# DRIFT ALERT si la part d'une catégorie s'écarte de plus de 25 points

# Déployer une version taguée (build + smoke test + bascule de l'api)
./scripts/deploy.sh deploy v1.1

# Rollback en moins de 5 minutes (image précédente, pas de rebuild)
./scripts/deploy.sh rollback

Le code de src/ n'a pas été réorganisé en domain/ports/adapters : aucune friction ne le justifie encore. La décision, le raisonnement et les symptômes qui la feraient réviser sont documentés dans docs/adr_001_structuration.md. Les procédures d'exploitation (alertes, diagnostic, rollback) sont dans docs/runbook.md.

Et avec les vrais outils ?

Ce dépôt reste volontairement léger : runs.jsonl au lieu de MLflow, un JSON au lieu d'un model registry, un script stdlib au lieu d'Evidently.

Pourquoi ? Pour que vous ressentiez la friction avant de choisir l'outil. Quand votre projet dépassera ~10 runs, comparer des lignes de JSONL deviendra pénible — vous saurez alors POURQUOI vous installez MLflow, pas juste parce qu'il est dans le planning. Même logique pour DVC (données trop lourdes pour Git), Langfuse (traces d'appels LLM), Airflow (batch multi-étapes) et Prometheus/Grafana (tendances multi-jours).

Ce dépôt n'est donc pas un standard à reproduire à l'identique : c'est le point de départ minimal qui rend chaque outil justifiable le jour venu.

Structure

jardin-classify/
├── .env.example        # template des variables attendues (le .env reste local)
├── .gitignore          # exclut .env, caches Python
├── README.md
├── pyproject.toml      # dépendances (uv), ajoutées phase par phase
├── uv.lock             # versions exactes, reproductibles partout
├── Dockerfile          # (p2) image commune aux services
├── docker-compose.yml  # (p2) un service par cycle de vie — (p3) api + train + evaluate
├── .dockerignore       # (p2) jamais de .env dans l'image
├── prompts/
│   └── classify_v1.txt # (p2) prompt versionné
├── data/
│   ├── test_set_v1.jsonl   # (p2) jeu de test versionné (9 cas dont vide + ambigu)
│   └── production_sample.jsonl  # (p4) messages récents pour le drift
├── experiments/
│   └── runs.jsonl      # (p2) historique léger des runs (params + métrique)
├── monitoring/
│   └── drift_check.py  # (p4) compare la distribution récente à la référence
├── docs/
│   ├── adr_001_structuration.md  # (p4) pourquoi on ne restructure PAS src/
│   └── runbook.md      # (p4) alertes, diagnostic, rollback
├── scripts/
│   └── deploy.sh       # (p4) déploiement + rollback < 5 min
├── models/
│   └── category_model_v1.json  # (p2) ARTEFACT produit par src/train.py
├── src/
│   ├── __init__.py
│   ├── cli.py          # point d'entrée CLI
│   ├── classify.py     # orchestre ML + LLM + refus
│   ├── train.py        # (p2) job d'entraînement mocké -> artefact
│   ├── category_model.py     # MLCategoryModel : charge l'artefact (modifié p2)
│   ├── llm_client.py   # LLMClient mocké — (p3) + timeout, retry borné
│   ├── config.py       # lecture des variables d'environnement
│   ├── contracts.py    # (p2) contrats Pydantic entrée/sortie
│   ├── api.py          # (p3) FastAPI : /classify, /health, /metrics
│   └── evaluate.py     # (p3) évaluation batch -> run dans runs.jsonl
└── tests/
    ├── test_characterization.py  # (p2) protège le contrat observable
    ├── test_train.py             # (p2) l'artefact produit est chargeable
    └── test_reliability.py       # (p3) timeout simulé, retry borné, 422

About

Dépôt compagnon du module Méthodologie de projet (MLOps/LLMOps) — évolue par phases p1..p4

Resources

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors