Spaces:
Sleeping
Sleeping
File size: 9,280 Bytes
1766da1 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 | # É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/core/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
### 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 le détecteur
Dans `detectors.py`, ajoutez une fonction pure qui prend le dict
`benchmark_data` (le JSON de résultats du rapport) et retourne une
liste de `Fact`. 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_sprint19_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/report/i18n/` pour les libellés
d'interface.
3. Ajoutez `de.yaml` dans `picarones/report/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/fixtures.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 (Sprint 23)
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
Depuis le Sprint 23, `select_facts` accepte un argument optionnel
`type_order` :
```python
from picarones.core.narrative import build_synthesis
from picarones.core.narrative.arbiter import select_facts, DEFAULT_TYPE_ORDER
from picarones.core.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 (Sprint 23)
Avant le Sprint 23, le test de traçabilité des nombres tolérait 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_sprint19_narrative_engine.py::test_every_number_in_synthesis_is_traceable`
plus le test
`test_sprint23_anti_hallucination.py::TestTemplatesNoHardcodedLiterals`
échoueront sinon.
|