Spaces:
Running
É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_detectorse charge de l'enregistrement, du tri par priorité, et de l'alimentation dearbiter.DEFAULT_TYPE_ORDER.
1. Déclarer le type de fait
Dans facts.py, ajoutez une valeur à FactType :
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 :
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'éditerarbiter.py) ; - vérifie qu'aucun autre détecteur n'est déjà enregistré sur le même
FactType(sinonValueError) ; - 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.
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 :
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) :
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) :
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) :
- Créez
templates/de.yamlen copiant la structure defr.yamlet en traduisant chaque entrée. - Ajoutez
de.jsondanspicarones/reports/html/i18n/pour les libellés d'interface. - Ajoutez
de.yamldanspicarones/reports/html/glossary/pour le glossaire. - Le code détecte automatiquement la langue via
load_glossary("de"),get_labels("de"), et_load_templates("de")— aucun code à modifier.
Tester votre changement
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 :
- 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). - Que l'importance est suffisante (>
MEDIUM) pour passer le filtre par défaut de l'arbitre. - 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 :
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 :
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_COLLAPSEetERROR_PROFILE_OUTLIERen tête pour mettre l'accent sur la lecture diagnostique des erreurs. - Comité technique : promouvoir
CONFIDENCE_WARNINGen tête pour forcer la discussion sur la fiabilité avant les classements. - Évaluation budgétaire : promouvoir
COST_OUTLIERetPARETO_ALTERNATIVEen 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_leveldans le payload desFactde typeCONFIDENCE_WARNING. - L'unité du coût (
/1000 pages) est propagée viacost_unit_pagesdansPARETO_ALTERNATIVEetCOST_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.