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 |
.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 |
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 docker compose restart category_web_app # OBLIGATOIRE après chaque entraînement
apply_feedback — Appliquer les corrections humainesTraite les feedbacks approuvés : 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.
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 docker compose restart category_web_app
| 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)
├── 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
├── 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.