API Django de classification automatique des frais annexes de Bons de Commande (BDC) automobile en France.
Le moteur utilise un pipeline à 3 niveaux : règles regex déterministes → modèle SetFit fine-tuné → revue humaine (A_CLASSIFIER). Toute la configuration se fait via Django Admin — aucune modification de code nécessaire.
Cette section décrit comment constituer un nouveau jeu de catégories de A à Z.
À partir des Bons de Commande réels, extraire le tableau des frais annexes (libellés + montants). Analyser la fréquence et la sémantique des libellés pour identifier les regroupements naturels.
Questions à se poser :
Pour chaque catégorie identifiée, créer une entrée dans Django Admin (/admin/classifier/category/) avec :
carte_grise, garantie_extension)6354, 607, 6226)TVA 20%, Hors champ TVA, Mixte)Minimum recommandé : 5 catégories. Maximum pratique sur un seul modèle : ~20.
Pour chaque catégorie, collecter 8 à 30 libellés réels et variés issus des BDC. La variété compte plus que la quantité : Carte grise, CG, Certificat immatriculation, Carte Grise + malus sont 4 exemples qui valent mieux que 20 fois Carte grise.
Le jeu d'entraînement peut être fourni de deux manières :
src/classifier/fixtures/training_data.json (format {"categorie": ["libelle1", "libelle2"]})/admin/classifier/trainingexample/# Import complet depuis les fichiers JSON (supprime l'existant) docker compose exec category_web_app python manage.py import_training_data --clear # Import incrémental (préserve l'existant, ignore les doublons) docker compose exec category_web_app python manage.py import_training_data # Preview sans modifier la base docker compose exec category_web_app python manage.py import_training_data --dry-run # Exemples uniquement (skip les règles regex) docker compose exec category_web_app python manage.py import_training_data --skip-rules
Les règles regex capturent les cas déterministes sans passer par le modèle ML (confiance fixe 0.95, <1ms). Elles prennent effet immédiatement après sauvegarde dans l'Admin, sans redémarrage.
Dans Django Admin (/admin/classifier/categoryrule/) :
(?i)\bcarte\s*grise\b)Règles de priorité importantes :
carte_grise) reçoivent une priorité basse (1-3) pour être évaluées en premierfrais_administratif) reçoivent une priorité haute (11-14), évaluées en dernierA_CLASSIFIER (multi-match)# Entraînement (~90s sur CPU) docker compose exec category_web_app python manage.py train_setfit # Avec évaluation de précision sur un holdout de 20% docker compose exec category_web_app python manage.py train_setfit --evaluate # OBLIGATOIRE après l'entraînement — le serveur web tourne dans un processus séparé docker compose restart category_web_app
# Obtenir un token JWT
TOKEN=$(curl -s -X POST https://categoryclassification.aiichaa.com/api/token/ \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"YOUR_PASSWORD"}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['access'])")
# Classifier des frais annexes
curl -s https://categoryclassification.aiichaa.com/api/v1/classify/ \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"frais_annexes": [{"name": "Carte Grise", "prix": "238"}, {"name": "Waxoyl", "prix": "219"}]}'
Analyser les résultats :
method: rules ou method: setfit → classifiés automatiquementmethod: rules_multi_match → libellés composites, revue humaine requiseA_CLASSIFIER → envoyer via /api/v1/feedback/ avec la bonne catégorieA_CLASSIFIER et envoie des corrections à POST /api/v1/feedback/correct_category et le statut d'approbationdocker compose exec category_web_app python manage.py apply_feedback --retrain docker compose restart category_web_app
POST /api/v1/classify/ (JWT requis)
│
▼
┌─────────────────────────────────────────┐
│ Niveau 1 : RÈGLES REGEX (depuis la DB) │
│ Déterministe, <1ms, confiance 0.95 │
│ Match unique → catégorie │
│ Multi-match → A_CLASSIFIER │
└────────────────┬────────────────────────┘
│ aucun match
▼
┌─────────────────────────────────────────┐
│ Niveau 2 : MODÈLE SETFIT │
│ Fine-tuné sur vos données, ~50ms/item │
│ conf >= seuil → catégorie │
│ conf < seuil → A_CLASSIFIER │
└────────────────┬────────────────────────┘
│ faible confiance
▼
┌─────────────────────────────────────────┐
│ Niveau 3 : A_CLASSIFIER (revue humaine) │
└─────────────────────────────────────────┘
L'entraînement est toujours de zéro (non incrémental). Voici exactement ce que fait train_setfit :
src/classifier/fixtures/setfit_model/)docker compose restart category_web_app est obligatoire après chaque entraînement.
Points importants :
apply_feedback --retrain ajoute les feedbacks approuvés au corpus puis réentraîneapply_feedback supprime aussi le libellé de la mauvaise catégorie s'il y était┌────── API classifie les frais ◄──────────────────┐ │ │ ▼ │ Items A_CLASSIFIER → client envoie POST /feedback/ │ │ │ ▼ │ Ticket Mantis créé automatiquement │ │ │ ▼ │ Responsable révise dans Django Admin │ (approuve / refuse / corrige la catégorie) │ │ │ ▼ │ manage.py apply_feedback --retrain │ │ │ └───────────────────────────────────────────────────┘
# 1. Configurer l'environnement
cp .env.example .env
# Éditer .env avec vos valeurs (voir section Configuration)
# 2. Build et démarrage
docker compose up --build -d
# 3. Migrations
docker compose exec category_web_app python manage.py makemigrations classifier
docker compose exec category_web_app python manage.py migrate
# 4. Importer catégories, règles regex et exemples d'entraînement
docker compose exec category_web_app python manage.py import_training_data --clear
# 5. Entraîner le modèle SetFit (~90s sur CPU)
docker compose exec category_web_app python manage.py train_setfit
# 6. Redémarrer pour charger le modèle en mémoire
docker compose restart category_web_app
# 7. Obtenir un token JWT
curl -X POST http://localhost:8042/api/token/ \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "adminPass"}'
| Variable | Défaut | Description |
|---|---|---|
SECRET_KEY | — | Clé secrète Django (obligatoire en prod) |
DEBUG | 1 | Mode debug (0 en production) |
ALLOWED_HOSTS | * | Hôtes autorisés, séparés par virgules |
POSTGRES_DB | category_db | Nom de la base de données |
POSTGRES_USER | appuser | Utilisateur PostgreSQL |
POSTGRES_PASSWORD | — | Mot de passe PostgreSQL |
DATABASE_URL | — | URL complète (prioritaire sur les vars individuelles) |
WEB_PORT | 8042 | Port externe Django |
DB_PORT | 5434 | Port externe PostgreSQL |
MANTIS_ENABLED | 0 | Activer l'intégration Mantis (1/0) |
MANTIS_URL | — | URL de l'instance Mantis (ex: https://mantis.example.com) |
MANTIS_TOKEN | — | Token API Mantis |
MANTIS_PROJECT_NAME | — | Nom exact du projet dans Mantis |
S3_ENDPOINT_URL | (vide) | Endpoint MinIO/S3 (ex: https://s3.aiichaa.com). Vide = stockage modèle désactivé. |
S3_ACCESS_KEY_ID | — | Clé d'accès du service account scopé au bucket |
S3_SECRET_ACCESS_KEY | — | Clé secrète associée |
S3_BUCKET_NAME | — | Nom du bucket (ex: catclass-models) |
S3_REGION | auto | Région (auto convient pour MinIO) |
MODEL_S3_PREFIX | setfit/ | Préfixe sous lequel les artefacts modèle sont stockés |
AUTO_PULL_MODEL | 1 | Si 1, tente pull_model_from_s3 au démarrage du conteneur |
SETFIT_MODEL_DIR | (fixture) | Chemin où le modèle est stocké localement (overridé en staging vers /data/model/setfit_model) |
TRAINING_FROZEN | 0 | Si 1, refuse apply_feedback --retrain, train_setfit, et push_model_to_s3 (fenêtre de gel). |
.env, utiliser docker compose up -d (pas restart) pour que les nouvelles variables soient injectées dans le conteneur.
| Service | Port interne (conteneur) | Port externe (hôte) |
|---|---|---|
| Django | 8000 | 8042 (configurable via WEB_PORT) |
| PostgreSQL | 5432 | 5434 (configurable via DB_PORT) |
Tous les endpoints sauf /api/v1/status/ nécessitent un token Bearer JWT.
# Obtenir un token
curl -X POST http://localhost:8042/api/token/ \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "adminPass"}'
# Utiliser le token
curl http://localhost:8042/api/v1/categories/ \
-H "Authorization: Bearer eyJ..."
# Rafraîchir un token expiré (expire après 5 minutes)
curl -X POST http://localhost:8042/api/token/refresh/ \
-H "Content-Type: application/json" \
-d '{"refresh": "eyJ..."}'
Throttling : 100 req/min pour les accès anonymes, 1000 req/min pour les accès authentifiés.
| Endpoint | Méthode | Auth | Description |
|---|---|---|---|
/ | GET | Non | Homepage (ce document) |
/api/token/ | POST | Non | Obtenir un JWT |
/api/token/refresh/ | POST | Non | Rafraîchir un JWT |
/api/v1/status/ | GET | Non | Statut du moteur (règles, modèle, stats) |
/api/v1/categories/ | GET | Bearer | Liste des catégories actives |
/api/v1/classify/ | POST | Bearer | Classifier des frais annexes |
/api/v1/feedback/ | POST | Bearer | Soumettre une correction |
/api/v1/admin/reload-model/ | POST | Bearer (staff) | Recharger le modèle en mémoire — ?pull=1 pour aussi tirer depuis MinIO |
/api/v1/admin/stats/ | GET | Bearer (staff) | Compteurs : classifications (24h/7j/30j), feedback, modèle, dernier training |
/api/v1/admin/logs/ | GET | Bearer (staff) | Audit log paginé (filtres since, category, method, confidence_lt, libelle_contains, …) |
/api/v1/admin/feedback/ | GET | Bearer (staff) | File de feedback paginée (filtres status, applied, correct_category, …) |
POST /api/v1/classify/){
"frais_annexes": [
{"name": "Carte Grise (hors malus éventuel)", "prix": "238,00"},
{"name": "Waxoyl", "prix": "219,00"},
{"name": "immat + transport", "prix": "450,00"}
],
"confidence_threshold": 0.55
}
Réponse :
{
"frais_annexes": {
"carte_grise": [
{"name": "carte_grise_1", "code": "Carte Grise (hors malus éventuel)",
"prix": "238,00", "confidence": 0.95, "method": "rules"}
],
"traitement_carrosserie": [
{"name": "traitement_carrosserie_1", "code": "Waxoyl",
"prix": "219,00", "confidence": 0.95, "method": "rules"}
],
"A_CLASSIFIER": [
{"name": "A_CLASSIFIER_1", "code": "immat + transport",
"prix": "450,00", "confidence": 0.0, "method": "rules_multi_match",
"matched_categories": ["frais_administratif", "transport_livraison"],
"needs_review": true}
]
},
"meta": {
"total_items": 3, "classified": 2, "needs_review": 1,
"processing_time_ms": 14.2,
"methods": {"rules": 2, "rules_multi_match": 1, "setfit": 0}
}
}
POST /api/v1/feedback/){
"original_libelle": "Frais Aménagement DANGEL",
"predicted_category": "A_CLASSIFIER",
"correct_category": "options_vehicule",
"notes": "DANGEL = kit aménagement 4x4"
}
| Catégorie | Description | Fréquence |
|---|---|---|
carte_grise | Certificat d'immatriculation, taxes, redevances | ~45% |
carburant | Plein essence/diesel, charge électrique, câble | ~11% |
accessoires | Tapis, plaques luxe, Fix&Go, attelage | ~8% |
frais_administratif | Démarches, courtage, immatriculation hors CG | ~6.5% |
garantie_extension | Roole Confort, Coyote, Forfait Sérénité | ~5.5% |
gravage | Nomblot, Eurodatacar, marquage antivol | ~5.5% |
traitement_carrosserie | Waxoyl, PowerShine, céramique, lustrage | ~5.5% |
aides_gouvernementales | Malus CO2, bonus écologique, prime CEE | ~4% |
pack_securite | Gilet + triangle, extincteur, Crit'Air | ~3.5% |
mise_a_la_route | Préparation véhicule avant livraison | ~2.3% |
autres_prestations | Nettoyage, installation, services divers | ~1% |
transport_livraison | Transport, convoyage, mise à disposition | ~0.3% |
options_vehicule | Peinture métallisée, toit ouvrant, packs constructeur | ~0.2% |
remise_commerciale | Péréquation, remise, renfort LOA | ~0.1% |
contrat_entretien | Maintenance Plus, Allure Care, FlexCare | <0.1% |
A_CLASSIFIER | Revue humaine obligatoire | ~2-5% |
Chaque catégorie porte des métadonnées comptables gérées dans l'Admin :
6354, 607)TVA 20%, Hors champ TVA, Mixte : Assurance exonérée / Services TVA 20%)import_training_data — Import des données initialesImporte catégories, exemples d'entraînement et règles regex depuis les fichiers JSON de src/classifier/fixtures/.
docker compose exec category_web_app python manage.py import_training_data --clear # clean install docker compose exec category_web_app python manage.py import_training_data # incremental docker compose exec category_web_app python manage.py import_training_data --dry-run # preview docker compose exec category_web_app python manage.py import_training_data --skip-rules
train_setfit — Entraîner le modèle SetFitdocker compose exec category_web_app python manage.py train_setfit # entraînement docker compose exec category_web_app python manage.py train_setfit --evaluate # + évaluation docker compose exec category_web_app python manage.py train_setfit --dry-run # preview données
Après entraînement, pas besoin de redémarrer : chaque worker gunicorn détecte la nouvelle mtime de MODEL_DIR et recharge le modèle automatiquement à la prochaine requête.
apply_feedback — Appliquer les corrections humainesTraite uniquement les feedbacks approuvé (depuis Django Admin) : ajoute les libellés comme exemples d'entraînement dans la bonne catégorie, les retire de la mauvaise, et signale les conflits avec les règles regex. Les feedbacks en attente et refusé sont ignorés.
docker compose exec category_web_app python manage.py apply_feedback --dry-run # preview docker compose exec category_web_app python manage.py apply_feedback --retrain # appliquer + réentraîner
Pour partager un modèle et les feedbacks entre dev et staging :
# Modèle — depuis l'env qui entraîne, vers l'env de service python manage.py push_model_to_s3 --note "fb round 2026-05-12" python manage.py pull_model_from_s3 # latest python manage.py pull_model_from_s3 --tag=2026-05-12T11-01-33Z # version pinnée python manage.py pull_model_from_s3 --force # re-télécharge même si .version local correspond # Feedback — typiquement staging → dev pour rapatrier les corrections clients python manage.py dump_feedback --upload-to-s3 staging # vers feedback/staging/<ts>.json python manage.py load_feedback --from-s3-latest staging # idempotent (UUID-based) python manage.py load_feedback --from-s3-latest staging --dry-run # preview
Pour recharger le modèle dans tous les workers d'un environnement sans redémarrer le conteneur :
curl -X POST "https://catclass-api.example.com/api/v1/admin/reload-model/?pull=1" \ -H "Authorization: Bearer $STAFF_JWT"
Trois endpoints en lecture seule pour observer ce qui se passe en production :
# Compteurs : volume 24h/7j/30j, breakdown méthodes/catégories, feedback queue, modèle, dernier training curl -H "Authorization: Bearer $STAFF_JWT" \ "https://catclass-api.example.com/api/v1/admin/stats/" | jq # Logs filtrés : low-confidence des dernières 24h curl -H "Authorization: Bearer $STAFF_JWT" \ "https://catclass-api.example.com/api/v1/admin/logs/?confidence_lt=0.5&limit=20" | jq # File de feedback pending curl -H "Authorization: Bearer $STAFF_JWT" \ "https://catclass-api.example.com/api/v1/admin/feedback/?status=pending&limit=50" | jq
Filtres disponibles :
/admin/logs/ : since, until (ISO 8601 ou YYYY-MM-DD), category, method, confidence_lt, confidence_gte, libelle_contains/admin/feedback/ : status (pending/approved/denied), applied (true/false), predicted_category, correct_category, since, until, libelle_containslimit (défaut 50, max 500), offset| Modèle | Rôle |
|---|---|
Category | Catégories de classification (clé, nom, TVA, code PCG) |
CategoryRule | Règles regex par catégorie (pattern, priorité, description) |
TrainingExample | Libellés d'entraînement par catégorie (source : import / feedback / manuel) |
ClassificationLog | Journal de chaque appel API de classification (traçabilité) |
ClassificationFeedback | Corrections humaines — statut : en attente / approuvé / refusé |
TrainingRun | Historique des entraînements (précision, durée, nombre d'exemples) |
| Composant | Taille |
|---|---|
| Image Docker (Python 3.12 + PyTorch CPU + transformers + SetFit) | ~2.5 GB |
Modèle de base HuggingFace (paraphrase-multilingual-MiniLM-L12-v2) | ~500 MB |
Modèle fine-tuné (setfit_model/) | ~500 MB |
| Image PostgreSQL 16 | ~400 MB |
| Marge (logs, cache pip, rebuilds) | ~2 GB |
| Total recommandé | ~6-8 GB |
RAM : ~1.5 GB en inférence, ~2-3 GB pendant l'entraînement.
PyTorch est installé en variante CPU uniquement (index https://download.pytorch.org/whl/cpu) pour éviter de tirer la version CUDA de 8 GB. Cette installation se fait dans une couche Docker séparée, avant la copie de requirements.txt, ce qui évite de re-télécharger PyTorch à chaque changement de dépendances.
src/
├── config/
│ ├── settings.py # JWT, throttling, DB, Mantis
│ ├── urls.py # Routes : homepage, admin, JWT, api/v1/
│ └── views.py # Homepage — rendu de ce README en HTML
└── classifier/
├── engine/
│ ├── categories.py # Cache des catégories depuis la DB
│ ├── rules.py # Niveau 1 : regex + détection multi-match
│ ├── embeddings.py # Niveau 2 : SetFit (chargement, inférence, entraînement)
│ ├── classifier.py # Orchestrateur du pipeline (règles → SetFit → fallback)
│ ├── model_storage.py # Upload/download du modèle vers MinIO/S3
│ └── feedback_sync.py # Sérialisation des feedbacks pour sync cross-env
├── fixtures/
│ ├── training_data.json # Snapshot des exemples d'entraînement
│ ├── regex_rules.json # Snapshot des règles regex
│ └── setfit_model/ # Modèle fine-tuné (généré par train_setfit, gitignored)
├── management/commands/
│ ├── import_training_data.py # JSON fixtures → DB
│ ├── train_setfit.py # Entraînement SetFit depuis la DB
│ ├── apply_feedback.py # Feedbacks approuvés → exemples d'entraînement
│ ├── push_model_to_s3.py # Upload du modèle local vers MinIO
│ ├── pull_model_from_s3.py # Download du modèle depuis MinIO (atomic swap)
│ ├── dump_feedback.py # Export feedbacks → fichier ou MinIO
│ └── load_feedback.py # Import feedbacks (idempotent sur UUID)
├── models.py # Category, CategoryRule, TrainingExample, logs, feedbacks
├── serializers.py
├── views.py # Endpoints API (JWT-protected)
├── admin.py # Interface d'administration Django
└── services.py # Intégration Mantis Bug Tracker
docker compose exec category_web_app python manage.py test classifier -v2
Un jeu de test complet de 86 items couvrant toutes les catégories et les cas limites est disponible dans postman/test_classify_body.json.