Spaces:
Sleeping
Sleeping
| # É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. | |