Spaces:
Sleeping
Sleeping
File size: 11,353 Bytes
1766da1 2b782d0 1766da1 5bb0965 92de89a 1766da1 92de89a 2b782d0 92de89a 1766da1 92de89a 1766da1 bb9f9b6 1766da1 2b782d0 1766da1 2b782d0 1766da1 c6da3d3 1766da1 e1b8452 5bb0965 e1b8452 5bb0965 e1b8452 2b782d0 e1b8452 5bb0965 e1b8452 5bb0965 e1b8452 bb9f9b6 e1b8452 bb9f9b6 e1b8452 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 | # Étendre le moteur narratif
Ce guide explique comment ajouter un nouveau type de **fait détecté** à
la synthèse factuelle en tête du rapport.
## Architecture
```
picarones/domain/narrative/
├── __init__.py # API publique + pipeline build_synthesis
├── facts.py # Modèle Fact, FactType, FactImportance, DetectorRegistry
├── detectors.py # 12 détecteurs (un par FactType)
├── arbiter.py # Tri par importance, non-redondance, anti-contradiction
├── renderer.py # Rendu str.format_map sur templates YAML
└── templates/
├── fr.yaml # Templates français (1 par FactType)
└── en.yaml # Templates anglais
```
## Ajouter un détecteur
> Un nouveau détecteur ne demande que **deux** fichiers à toucher.
> Le décorateur `@register_detector` se charge de l'enregistrement,
> du tri par
> priorité, et de l'alimentation de `arbiter.DEFAULT_TYPE_ORDER`.
### 1. Déclarer le type de fait
Dans `facts.py`, ajoutez une valeur à `FactType` :
```python
class FactType(str, Enum):
...
NEW_THING = "new_thing"
```
### 2. Implémenter et enregistrer le détecteur
Dans `detectors.py`, écrivez une fonction pure qui prend le dict
`benchmark_data` et retourne une liste de `Fact`, puis décorez-la avec
`@register_detector` :
```python
from picarones.domain.narrative.facts import Fact, FactImportance, FactType
from picarones.domain.narrative.registry import register_detector
@register_detector(
FactType.NEW_THING,
priority=55, # entre STRATUM_COLLAPSE (50) et ERROR_PROFILE_OUTLIER (60)
importance=FactImportance.HIGH,
)
def detect_new_thing(benchmark_data: dict) -> list[Fact]:
...
```
Le décorateur :
- enregistre la fonction dans le registre central trié par `priority` ;
- alimente automatiquement `arbiter.DEFAULT_TYPE_ORDER` (plus besoin
d'éditer `arbiter.py`) ;
- vérifie qu'aucun autre détecteur n'est déjà enregistré sur le même
`FactType` (sinon `ValueError`) ;
- laisse la fonction utilisable telle quelle (pour les tests unitaires
qui l'appellent directement).
### Conventions de priorité
Plus la valeur est petite, plus le fait remonte tôt en synthèse à
importance égale. Les détecteurs builtin utilisent un pas de **10**
pour laisser de la place :
| Priority | Type | Question éditoriale |
|---:|---|---|
| 10 | `GLOBAL_LEADER_CER` | Qui gagne globalement ? |
| 20 | `STATISTICAL_TIE` | Y a-t-il un ex-aequo ? |
| 30 | `SIGNIFICANT_GAP` | À quel point l'écart est solide ? |
| 40 | `STRATUM_WINNER` | Qui domine sur quel sous-corpus ? |
| 50 | `STRATUM_COLLAPSE` | Qui s'effondre sur quoi ? |
| 60 | `ERROR_PROFILE_OUTLIER` | Qui se trompe différemment ? |
| 70 | `LLM_HALLUCINATION_FLAG` | Hallucinations VLM ? |
| 80 | `ROBUSTNESS_FRAGILE` | Sensibilité aux dégradations ? |
| 90 | `PARETO_ALTERNATIVE` | Y a-t-il un compromis coût/qualité ? |
| 100 | `SPEED_WINNER` | Vitesse ? |
| 110 | `COST_OUTLIER` | Coût aberrant ? |
| 120 | `CONFIDENCE_WARNING` | Mise en garde sur la fiabilité. |
### Détails techniques
Le détecteur ne doit **jamais lever d'exception** — le
`DetectorRegistry` capte les erreurs en `logger.warning` mais c'est
une protection, pas une excuse.
```python
def detect_new_thing(benchmark_data: dict) -> list[Fact]:
"""Doc explicite : qu'est-ce qui déclenche ce fait ?"""
# Exemple : flag les moteurs où une métrique X dépasse un seuil
facts: list[Fact] = []
for engine in benchmark_data.get("engines") or []:
if (engine.get("some_metric") or 0) > 0.5:
facts.append(Fact(
type=FactType.NEW_THING,
importance=FactImportance.HIGH,
payload={
"engine": engine["name"],
"value": round(engine["some_metric"], 4),
"value_pct": round(engine["some_metric"] * 100, 1),
},
engines_involved=(engine["name"],),
))
return facts
```
**Règle d'or anti-hallucination** : chaque champ que vous mettez dans
`payload` doit être **calculé à partir de** valeurs présentes dans
`benchmark_data`. Pas de constante ni de calcul invraisemblable.
### 3. Enregistrer dans la table
Toujours dans `detectors.py`, ajoutez au dict `DETECTORS_BY_TYPE` :
```python
DETECTORS_BY_TYPE = {
...
FactType.NEW_THING: detect_new_thing,
}
```
`register_default_detectors(registry)` parcourt ce dict et l'enregistre
automatiquement. Aucune action supplémentaire requise.
### 4. Ajouter les templates FR/EN
Dans `templates/fr.yaml` et `templates/en.yaml`, ajoutez une entrée par
type, avec le nom de la valeur enum (ici `new_thing`) :
```yaml
new_thing: >-
Le moteur {engine} dépasse le seuil de la métrique X
({value_pct} %).
```
Les placeholders `{engine}`, `{value_pct}` etc. doivent **exactement**
correspondre aux clés du `payload` du détecteur. Si vous oubliez un
champ, le rendu utilisera `?` (et logguera un warning) plutôt que de
crasher — mais les tests doivent attraper ça.
### 5. Ajuster l'arbitre si besoin
Dans `arbiter.py`, deux choses à considérer :
- **Ordre canonique** : ajoutez votre type dans `_TYPE_ORDER` à la
position appropriée. Cet ordre départage les ex-aequo à importance
égale et garantit le déterminisme.
- **Paires complémentaires** : par défaut, l'arbitre supprime les
doublons sur le même moteur. Si votre nouveau type est complémentaire
d'un autre type pour le même moteur (ex. leader + speed), ajoutez la
paire dans `_COMPLEMENTARY_PAIRS`.
- **Règles anti-contradiction** : si votre fait peut contredire un autre
(ex. Nemenyi vs Wilcoxon), implémentez la règle dans
`_remove_contradictions`.
### 6. Tests
Ajoutez au minimum :
- Un test unitaire dans `tests/test_narrative_engine.py` (ou
un nouveau fichier) :
```python
class TestNewThingDetector:
def test_emits_when_threshold_crossed(self):
data = _minimal_data(engines=[
{"name": "X", "some_metric": 0.7},
])
facts = detect_new_thing(data)
assert len(facts) == 1
assert facts[0].payload["engine"] == "X"
def test_empty_when_under_threshold(self):
data = _minimal_data(engines=[
{"name": "X", "some_metric": 0.3},
])
assert detect_new_thing(data) == []
```
- Le test global de traçabilité
(`test_every_number_in_synthesis_is_traceable`) couvrira automatiquement
votre détecteur dès que vous l'ajoutez à la synthèse.
## Ajouter une langue
Pour ajouter une nouvelle langue (ex. allemand) :
1. Créez `templates/de.yaml` en copiant la structure de `fr.yaml` et en
traduisant chaque entrée.
2. Ajoutez `de.json` dans `picarones/reports/html/i18n/` pour les libellés
d'interface.
3. Ajoutez `de.yaml` dans `picarones/reports/html/glossary/` pour le glossaire.
4. Le code détecte automatiquement la langue via `load_glossary("de")`,
`get_labels("de")`, et `_load_templates("de")` — aucun code à modifier.
## Tester votre changement
```bash
pytest tests/ -q --tb=short
picarones demo --output /tmp/demo.html --docs 8
# Ouvrir /tmp/demo.html et vérifier que la synthèse contient votre fait
```
Si la synthèse ne contient pas votre fait, vérifiez :
1. Que votre détecteur retourne bien quelque chose sur les données de
démo (`grep -A 20 "def generate_sample_benchmark" picarones/evaluation/synthetic.py`).
2. Que l'importance est suffisante (> `MEDIUM`) pour passer le filtre
par défaut de l'arbitre.
3. Que votre type n'est pas en collision avec un autre déjà retenu pour
le même moteur (cf. `_is_redundant`).
---
## Politique éditoriale
L'arbitre départage les faits d'**égale importance** par un ordre canonique
des types : c'est un choix éditorial qui répond à la question *« quand A et
B sont aussi importants l'un que l'autre, lequel parle en premier ? »*.
L'ordre par défaut est défini dans `arbiter.py` sous le nom
`DEFAULT_TYPE_ORDER` :
```python
DEFAULT_TYPE_ORDER = (
FactType.GLOBAL_LEADER_CER, # 1. Qui gagne globalement
FactType.STATISTICAL_TIE, # 2. Y a-t-il un ex-aequo
FactType.SIGNIFICANT_GAP, # 3. À quel point l'écart est solide
FactType.STRATUM_WINNER, # 4. Qui domine sur quel sous-corpus
FactType.STRATUM_COLLAPSE, # 5. Qui s'effondre sur quoi
FactType.ERROR_PROFILE_OUTLIER, # 6. Qui se trompe différemment
FactType.LLM_HALLUCINATION_FLAG, # 7. Hallucinations VLM
FactType.ROBUSTNESS_FRAGILE, # 8. Sensibilité aux dégradations
FactType.PARETO_ALTERNATIVE, # 9. Y a-t-il un compromis coût/qualité
FactType.SPEED_WINNER, # 10. Vitesse
FactType.COST_OUTLIER, # 11. Coût aberrant
FactType.CONFIDENCE_WARNING, # 12. Mise en garde sur la fiabilité
)
```
**Hypothèse implicite** : un lecteur d'institution patrimoniale veut
d'abord savoir *qui gagne* puis *à quel point cette victoire est solide*,
avant de découvrir des considérations de coût ou de vitesse. Une équipe
DevOps cherchant à industrialiser une chaîne aurait probablement l'ordre
inverse — vitesse et coût d'abord, qualité ensuite.
### Surcharger l'ordre sans patcher le code
`select_facts` accepte un argument optionnel
`type_order` :
```python
from picarones.domain.narrative import build_synthesis
from picarones.domain.narrative.arbiter import select_facts, DEFAULT_TYPE_ORDER
from picarones.domain.narrative.facts import FactType
# Réordonnancement : on remonte vitesse et coût avant qualité.
custom = (
FactType.SPEED_WINNER,
FactType.COST_OUTLIER,
FactType.PARETO_ALTERNATIVE,
FactType.GLOBAL_LEADER_CER,
# ... compléter avec les autres types ; ceux qui manquent sont
# relégués à la fin sans crash.
)
facts = detect_all(benchmark_data)
selected = select_facts(facts, max_facts=5, type_order=custom)
```
Cas d'usage typiques :
- **Atelier MOOC** : promouvoir `STRATUM_COLLAPSE` et
`ERROR_PROFILE_OUTLIER` en tête pour mettre l'accent sur la lecture
diagnostique des erreurs.
- **Comité technique** : promouvoir `CONFIDENCE_WARNING` en tête pour
forcer la discussion sur la fiabilité avant les classements.
- **Évaluation budgétaire** : promouvoir `COST_OUTLIER` et
`PARETO_ALTERNATIVE` en tête.
### Règle anti-hallucination renforcée
Le test de traçabilité des nombres tolérait initialement deux
littéraux non-traçables au payload (`95` pour le seuil de l'IC, `100`
comme tolérance numérique). Cette whitelist est désormais vide :
- Le seuil de confiance est propagé via `confidence_level` dans le
payload des `Fact` de type `CONFIDENCE_WARNING`.
- L'unité du coût (`/1000 pages`) est propagée via `cost_unit_pages`
dans `PARETO_ALTERNATIVE` et `COST_OUTLIER`.
**Si vous ajoutez un détecteur dont le template référence un nombre
constant** (ex. *« seuil α = 0,05 »*), vous devez **systématiquement**
le mettre dans le `payload`. Le test
`test_narrative_engine.py::test_every_number_in_synthesis_is_traceable`
plus le test
`test_anti_hallucination.py::TestTemplatesNoHardcodedLiterals`
échoueront sinon.
|