Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/publish-website.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Publish website

# Déploie le site Quarto (docs/) sur GitHub Pages à chaque commit sur website ou main,
# via la méthode artifact officielle (pas de branche gh-pages).
on:
push:
branches:
- website
- main
workflow_dispatch:

# Permissions requises par la méthode artifact (OIDC + déploiement Pages).
permissions:
contents: read
pages: write
id-token: write

# Un seul déploiement à la fois ; on ne coupe pas un déploiement en cours.
concurrency:
group: pages
cancel-in-progress: false

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Quarto
uses: quarto-dev/quarto-actions/setup@v2

# Pas de setup Python/R : les pages docs/ ne contiennent aucun chunk exécutable.
- name: Render site
run: quarto render docs

- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: docs/_site

deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
85 changes: 85 additions & 0 deletions docs/00-generalites.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
---
title: "Le processus — Généralités"
subtitle: "Vue d'ensemble du pipeline de codification COICOP"
---

> 🚧 **Page en construction (squelette).** Compléter les sections ci-dessous.

## Principe

Le pipeline `codif-coicop-bdf` rattache chaque libellé de produit à un poste de la
nomenclature COICOP en combinant plusieurs classifieurs complémentaires, puis en les
arbitrant via un LLM (*LLM-as-judge*). Il est orchestré par **Argo Workflows**
(`argo/codif-pipeline.yaml`).

## Le DAG du pipeline

Schéma Excalidraw du [pipeline](https://link.excalidraw.com/l/9vLymO4fSxN/3JjfZov3UgX).

```text
┌──→ create-vector-db ──────────────┐ (skippable)
preprocessing ──┐ ┌──→ prune ────┤ ├──→ run-rag(-annotations) ─┐
└──→┤ └──→ create-vector-db-annotations ───┘ │
└──→ codif-regex ─┬──→ codif-lcs ──────────────────────────────────────────────┼──→ decide-coicop ──→ final-output ──→ report
(→ prune) └──→ run-ttc ───────────────────────────────────────────────┘
```

## Les classifieurs

Quatre classifieurs tournent en parallèle, puis sont conciliés par `decide-coicop` :

| Classifieur | Approche | Page |
|---|---|---|
| Regex | Règles | [La codification par regex](02-codif-regex.qmd) |
| LCS | Similarité de chaînes | [LCS](03-codif-lcs.qmd) |
| TTC | Classifieur neuronal | [TTC](05-run-ttc.qmd) |
| RAG COICOP | RAG sur notices | [RAG sur COICOP](04-coicop-rag.qmd) |
| RAG annotations | RAG sur exemples codifiés | [RAG sur annotation](09-rag-annotation.qmd) |

## Modes prod / éval


<!-- ::: {.callout-important}
## Mode prédiction
Ce fichier contient des produits **à coder**, sans vérité terrain. Le pipeline tourne donc en
**mode prédiction** : il n'y a pas de colonne `code` de référence. Les observations préparées
sont écrites dans `observations.parquet` (l'étape `preprocessing` produit par ailleurs toujours
les annotations train/test). Cela change la dernière
étape (`report`), qui bascule sur `prediction_report.qmd` (suivi des prédictions) au lieu du
rapport d'évaluation d'exactitude. Le livrable final est le fichier produit par
[`final-output`](07-final-output.qmd).
::: -->

*TODO* — `input_file` non vide = **production** (code un fichier externe) ; vide =
**évaluation** (code le split test des annotations). Décrire le pilotage de bout en bout.

## Échantillonnage

*TODO* — sampling centralisé à `codif-regex` (`sample-observations` en prod,
`sample-annotations` en éval) hérité par tous les classifieurs.


<!-- ## Le fichier d'entrée et son mappage

L'exemple utilisé dans toute cette documentation est
`BDF_data_tickets_appli_20260320_a_codif.csv` (séparateur `;`, valeurs entre guillemets) :

| `ID_TICK` | `NAT_DEP` (produit) | `MONT_DEP` | `MAG_DEP` (enseigne) | `COM_DEP` |
|---|---|---|---|---|
| `10203_91_01` | Gasoil | 30.25 | Station Netto | Saint-Vite |
| `10203_91_06` | Ticket CB | 40.24 | Station Netto | Saint-Vite |
| `11622_91_08` | Bagu. Tradition U Blé Bretagne | 1.00 | U | Liffré |
| `10521_91_03` | Moyen frite Sce Pomme Frite | 4.10 | Mc Donald's | Civrieux-d'Azergues |
| `11165_91_01` | Illisible | 0.00 | — | — |

Le pipeline n'impose pas de noms de colonnes : l'étape `preprocessing` reçoit un
**mappage configurable** (paramètres Argo). Valeurs par défaut pour ce fichier :

| Paramètre Argo | Valeur | Colonne pipeline produite |
|---|---|---|
| `input_file` | `s3://…/workflow_inputs/BDF_data_tickets_appli_20260320_a_codif.csv` | — |
| `text_column` | `NAT_DEP` | `raw_product` (libellé à coder) |
| `shop_column` | `MAG_DEP` | `shop` (enseigne) |
| `budget_column` | `MONT_DEP` | `budget` (montant €) |
| `annee_column` | *(vide)* | `annee` |
| `source_column` | *(vide)* | `source` | -->
107 changes: 87 additions & 20 deletions docs/06-decide-coicop.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,32 @@ title: "6. Decide — arbitrage LLM-as-judge"
## Rôle de l'étape

`decide-coicop` est l'**arbitre final**. Pour chaque produit non codé par regex, elle réunit
les trois prédictions candidates — [LCS](03-codif-lcs.qmd), [RAG](04-coicop-rag.qmd),
[TTC](05-run-ttc.qmd) — et choisit le code COICOP final, soit automatiquement (consensus),
soit en interrogeant un LLM qui pèse les indices.
les prédictions candidates — [LCS](03-codif-lcs.qmd), [RAG](04-coicop-rag.qmd),
[TTC](05-run-ttc.qmd) et, optionnellement, [RAG-Annotations](09-rag-annotation.qmd) — puis
choisit le code COICOP final, soit automatiquement (consensus), soit en interrogeant un LLM
qui pèse les indices.

- Code : [`coicop-bdf-classifier/src/decide_coicop.py`](../coicop-bdf-classifier/src/decide_coicop.py)
- Commande : sous-commande `decide-coicop` de `main.py`

## Entrées

| Source | Chemin S3 |
|---|---|
| LCS | `…/{run}/codif-lcs/raw_test_LCS.parquet` |
| RAG | `…/{run}/run-rag/predictions.parquet` |
| TTC | `…/{run}/run-ttc/predictions.parquet` |
| Nomenclature | fichier des codes COICOP valides |
| Source | Chemin S3 | Argument |
|---|---|---|
| LCS | `…/{run}/codif-lcs/raw_test_LCS.parquet` | `--lcs-file` |
| RAG | `…/{run}/run-rag/predictions.parquet` | `--rag-file` |
| TTC | `…/{run}/run-ttc/predictions.parquet` | `--ttc-file` |
| RAG-Annotations *(optionnel)* | `…/{run}/run-rag-annotations/predictions.parquet` | `--rag-annotations-file` |
| Nomenclature | fichier CSV des codes COICOP valides (`Code`, `Libelle`) | `--nomenclature` |

Les prédictions sont fusionnées sur la colonne `id` (`load_all_observations()`). Le bloc
RAG-Annotations (colonnes `ragann_code`, `ragann_confidence`, `ragann_codable`) n'est joint que
si `--rag-annotations-file` est fourni : le 4ᵉ classifieur est donc **optionnel au niveau du
module/CLI** (`default=None`), et tout le traitement aval s'adapte au nombre de modèles
réellement disponibles. En revanche, **le pipeline Argo le passe systématiquement** (la step
`decide-coicop` dépend de `run-rag-annotations` et fournit toujours `--rag-annotations-file`) :
les 4 classifieurs sont donc la norme en production, l'optionalité servant surtout aux lancements
manuels ou au débogage.

## Traitement

Expand All @@ -29,38 +40,48 @@ Pour économiser temps et tokens sur les cas faciles, `try_consensus_decision()`
**sans appeler le LLM** quand :

- la confiance TTC top-1 est suffisante (`ttc_conf_1 ≥ 0,90`), **et**
- tous les codes disponibles (`lcs_code`, `rag_code`) sont **égaux** à `ttc_code_1`.
- tous les autres codes **disponibles** (`lcs_code`, `rag_code`, `ragann_code`) sont **égaux**
à `ttc_code_1`.

```python
if float(ttc_conf) < threshold: # threshold = 0.90
return None
...
# codes non nuls parmi lcs_code, rag_code, ragann_code
if not all(c == ttc_code for c in other_codes):
return None
# → décision par consensus, confiance = 5, llm_model = "consensus"
```

La décision porte alors la mention *« Consensus automatique : les N modèles s'accordent… »*.
Seuls les codes réellement présents (non `NA`) participent au vote : si RAG-Annotations n'a pas
été fourni, le consensus se joue sur les modèles restants. La décision porte alors la mention
*« Consensus automatique : les N modèles s'accordent… »* (où `N` est le nombre de codes
concordants).

### 2. Arbitrage par LLM

Sinon, le LLM est sollicité :

- **Filtrage de la nomenclature** (`filter_nomenclature()`) : seules les sections COICOP
pertinentes (préfixes L2 des prédictions) sont envoyées dans le prompt — réduction ×4 à ×10
du nombre de tokens.
- **Filtrage de la nomenclature** (`filter_nomenclature()`) : au lieu d'envoyer les ~700 codes
COICOP, on ne soumet au LLM que les codes « autour » des prédictions des modèles (voir le
détail [ci-dessous](#comment-est-construite-la-liste-de-codes-soumise-au-llm)) — réduction
typique de ~700 à ~50-150 lignes (gain ×4 à ×10 sur les tokens). L'option
`--full-nomenclature` désactive ce filtrage et envoie la nomenclature complète.
- **Construction du prompt** (`build_prompt()`) : un prompt structuré présentant le produit,
les trois prédictions, la nomenclature filtrée et les consignes :
les prédictions des modèles, la nomenclature filtrée et les consignes :

```text
═══ PRODUIT ═══
Libellé brut : Max garden flowers balle surprise
Libellé normalisé : max garden flowers balle surprise
Enseigne : Action
Type de magasin : …
Montant (€) : 3.99
Code de référence : …
═══ PRÉDICTIONS DES MODÈLES ═══
[LCS] Code prédit : … (proportion …, distance …)
[RAG] Code prédit : … (confiance …)
[LCS] Code prédit : … (sous-chaîne …, proportion …, distance …)
[RAG] Code prédit : … (confiance …, codable …)
[RAG-Annotations] Code prédit : … (confiance …, codable …) ← seulement si fourni
[TTC] Top 1 : … (confiance …) / Top 2 : … / Top 3 : …
═══ NOMENCLATURE COICOP (codes valides) ═══
Expand All @@ -69,6 +90,46 @@ Sinon, le LLM est sollicité :
4. Attribue un score de confiance de 1 (très faible) à 5 (très élevé).
```

Le bloc `[RAG-Annotations]` n'apparaît dans le prompt que si une prédiction `ragann_code`
non nulle existe pour l'observation.

#### Comment est construite la liste de codes soumise au LLM ?

`filter_nomenclature()` part de **tous** les codes prédits pour l'observation (`lcs_code`,
`rag_code`, `ragann_code`, `ttc_code_1`, `ttc_code_2`, `ttc_code_3`, après suppression des
`NA`) et en extrait deux ensembles de préfixes :

- **L1** = la *section*, soit le segment avant le premier point — `"01.1.2.3"` → `"01"` ;
- **L2** = la *sous-section*, soit les deux premiers segments — `"01.1.2.3"` → `"01.1"`.

Puis, pour **chaque code de la nomenclature complète**, la décision de le garder dépend de
*son propre* niveau de profondeur :

| Code de la nomenclature | Exemple | Conservé si… |
|---|---|---|
| 1 segment — en-tête de section | `01` | sa section appartient à **L1** |
| 2 segments — sous-section | `01.1`, `01.2` | sa section appartient à **L1** (filtré sur L1, pas sur L2) |
| 3 segments et plus | `01.1.2`, `01.1.2.3` | il **commence par** l'un des préfixes **L2** |

L'asymétrie est volontaire :

- **Aux niveaux 1 et 2**, le filtre est *large* : on garde l'en-tête de section **et toutes ses
sous-sections** (`01.1`, `01.2`, `01.3`…), même celles dont aucun code fin ne sera listé. Cela
donne au LLM une **vue d'ensemble de la section** pour réorienter si les modèles se sont
trompés de sous-section.
- **À partir du niveau 3**, le filtre est *étroit* : on ne garde que la descendance des L2
réellement prédits, pour ne pas noyer le prompt sous les feuilles non pertinentes.

**Exemple** — si les modèles prédisent `09.3.1` et `05.1.1`, alors `L1 = {09, 05}` et
`L2 = {09.3, 05.1}`. La liste soumise contient : les en-têtes `05` et `09` ; **toutes** les
sous-sections de `05` et `09` (`05.1`, `05.2`, …, `09.1`, …, `09.3`, …) ; mais, au niveau fin,
**uniquement** les codes sous `05.1.*` et `09.3.*`. Le LLM reste donc libre de choisir un code
précis dans ces deux sous-sections, de basculer vers une autre sous-section des sections 05/09,
ou de remonter à un niveau plus agrégé.

Cas limites : si **aucune** prédiction n'est disponible (tous les codes `NA`), ou si
`full=True` (`--full-nomenclature`), la nomenclature **complète** est renvoyée.

- **Sortie structurée** : le LLM répond en JSON validé par le modèle Pydantic
`DecisionCoicop` :

Expand Down Expand Up @@ -104,7 +165,7 @@ Le message *user* est produit par `build_prompt()` (valeurs des modèles illustr
```text
Tu es un expert en comptabilité nationale et en statistiques de consommation.
Ta mission est de déterminer le code COICOP le plus approprié pour un produit acheté,
en t'appuyant sur le contexte d'achat et les prédictions de trois modèles automatiques.
en t'appuyant sur le contexte d'achat et les prédictions de plusieurs modèles automatiques.

═══════════════════════════════════════
PRODUIT
Expand All @@ -130,6 +191,11 @@ PRÉDICTIONS DES MODÈLES
Confiance : 60%
Codable : True

[RAG-Annotations — RAG sur exemples déjà codifiés] ← présent seulement si --rag-annotations-file fourni
Code prédit : 09.3.1
Confiance : 72%
Codable : True

[TTC — Classificateur par apprentissage profond]
Top 1 : 09.3.1 (confiance 41%)
Top 2 : 05.1.1 (confiance 22%)
Expand All @@ -149,7 +215,7 @@ INSTRUCTIONS
═══════════════════════════════════════
1. Choisis UN code COICOP parmi ceux listés dans la nomenclature ci-dessus.
2. Le code doit correspondre au niveau le plus précis qui te semble justifié.
3. Explique en 1 phrase maximum comment tu as pesé les prédictions des trois modèles
3. Explique en 1 phrase maximum comment tu as pesé les prédictions des différents modèles
et tout autre indice (enseigne, type de magasin, montant).
4. Attribue un score de confiance de 1 (très faible) à 5 (très élevé).
```
Expand Down Expand Up @@ -185,7 +251,7 @@ On peut reproduire ce prompt pour n'importe quelle ligne avec le mode mono-obser

| Produit | Consensus ? | Décision |
|---|---|---|
| `Bagu. Tradition U Blé Bretagne` | si LCS = RAG = TTC et `ttc_conf_1 ≥ 0,90` → **oui** | code pain repris sans LLM, `confiance = 5`, `llm_model = "consensus"` |
| `Bagu. Tradition U Blé Bretagne` | si tous les codes disponibles = `ttc_code_1` et `ttc_conf_1 ≥ 0,90` → **oui** | code pain repris sans LLM, `confiance = 5`, `llm_model = "consensus"` |
| `Max garden flowers balle surprise` | les modèles divergent (jouet vs jardin) → **non** | le LLM arbitre à partir de l'enseigne (*Action*), du prix et des candidats |

## Que peut renvoyer la décision ?
Expand All @@ -208,6 +274,7 @@ possibles :
| Connexion impossible | `APIConnectionError: …` |
| Réponse coupée (limite de tokens) | `Response truncated before JSON was complete…` |
| JSON illisible | `JSON parsing failed: …` |
| Toute autre erreur (auth, etc.) | `<TypeErreur>: …` (catch-all) |

::: {.callout-warning}
## Le code n'est pas validé contre la nomenclature
Expand Down
37 changes: 37 additions & 0 deletions docs/09-rag-annotation.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
title: "RAG sur annotation"
subtitle: "Codification par RAG sur exemples déjà codifiés (few-shot)"
---

> 🚧 **Page en construction (squelette).** Compléter les sections ci-dessous.

## Principe

Le module `coicop-rag-annotations/` code chaque produit par **RAG sur exemples annotés** :
on récupère dans une base vectorielle les produits déjà codifiés les plus similaires, puis on
construit un prompt *few-shot* pour demander le code COICOP à un LLM.

## Étapes

1. **Construction de la vector DB** (`0_build_annotation_vector_db.py`) — indexe la KB
d'annotations prunée (+ suggester) dans Qdrant.
2. **Codification** (`1_run_rag.py`) — embeddings de l'input, recherche des k plus proches
voisins, prompt few-shot, génération, parsing JSON, export des prédictions.

<!-- TODO: détailler la représentation textuelle (produit + lieu d'achat), la taille de
récupération (retrieval.size), le modèle d'embedding et de génération. -->

## KB indexée et sources exclues

*TODO* — `exclude_sources` (éval) vs `exclude_sources_prod` (prod), inclusion du suggester,
sources exclues (ex. `copain`).

## Collection Qdrant de test

*TODO* — quand `sample-annotations` est non vide, une collection `_test` (suffixe sur
`collection_name`) est utilisée pour ne pas écraser la vector DB de production.

## Sortie et intégration

*TODO* — colonnes `ragann_*` consommées par [l'étape de réconciliation finale](06-decide-coicop.qmd)
(`decide-coicop`).
Loading
Loading