Analyse rhétorique collaborative par agents IA — exécuteur principal

Retour au README | Précédent : Orchestration

Objectif: Ce notebook orchestre et exécute une analyse rhétorique multi-agents sur un texte donné. Il sert de point d’entrée principal pour lancer le processus.

Structure Modulaire: 1. Argumentation-08c-UI-Configuration-Python.ipynb : Gère l’interface utilisateur pour sélectionner/préparer le texte à analyser (incluant sources prédéfinies, URL, fichier, texte direct, et extraction) et charge/sauvegarde la configuration des sources. 2. Argumentation-00-Setup-Tweety-Python.ipynb: Configuration initiale de l’environnement (dépendances, JVM/Tweety). 3. argumentation_lib (shim de série): état partagé (RhetoricalAnalysisState), pont Semantic Kernel (StateManagerPlugin) et orchestration (AnalysisRunner) — chargés directement par ce notebook. Les carnets legacy *_agent qui portaient ces définitions sont archivés dans _archive/. 4. Argumentation-07-Orchestration-Python.ipynb: paradigmes d’orchestration déterministes (référence pédagogique du rung).

Prérequis: * Un fichier .env à la racine contenant les clés API, configurations LLM, et la clé de chiffrement TEXT_CONFIG_KEY. * Un environnement Java Development Kit (JDK >= 11) correctement installé et configuré (JAVA_HOME). * Les dépendances Python installées (ipywidgets, requests, jupyter-ui-poll, python-dotenv, semantic-kernel, pandas, jpype1, cryptography). * Les JARs Tweety placés dans le dossier libs/. * Le fichier extract_sources.json.gz.enc (s’il existe déjà) contenant les définitions des sources.

Présentation générale du système

Objectifs pédagogiques

Ce notebook illustre une architecture d’agents collaboratifs multi-modaux pour l’analyse rhétorique automatisée. Les étudiants découvriront:

  1. Orchestration d’agents IA (Semantic Kernel)
  2. Analyse hybride: LLM (compréhension naturelle) + Logique formelle (preuve)
  3. État partagé pour communication inter-agents
  4. Validation structurée avec métriques de confiance

Architecture du système

Le système combine deux paradigmes d’IA complémentaires:

Composant Technologie Rôle
InformalAnalysisAgent LLM (GPT-4/Claude) Comprend langage naturel, identifie arguments/sophismes
PropositionalLogicAgent Tweety (bibliothèque Java) Formalise en logique PL, prouve cohérence/implications
StateManager Dataclass Python Mémoire partagée entre agents
Orchestration Semantic Kernel Coordination des appels d’agents

Workflow en 3 phases

Phase 1: ANALYSE INFORMELLE
├─ Détection arguments (prémisses, conclusions)
├─ Identification sophismes (taxonomie hiérarchique)
└─ Stockage état (JSON structuré)

Phase 2: FORMALISATION LOGIQUE
├─ Traduction propositions en PL
├─ Création Belief Sets (Tweety)
├─ Requêtes logiques (cohérence, implication)
└─ Stockage preuves formelles

Phase 3: VALIDATION & SYNTHÈSE
├─ Cross-validation (6 critères)
├─ Calcul score de confiance
├─ Génération conclusion synthétique
└─ Export rapport JSON

Cas d’usage réel: Ce système peut analyser des débats politiques, détecter la manipulation rhétorique, ou servir d’outil pédagogique pour l’enseignement de la pensée critique.

1. Chargement de l’Environnement

Chargement des variables depuis le fichier .env (clés API, clé de chiffrement, etc.).

Rôle du fichier .env

Le fichier .env centralise toutes les configurations sensibles et paramètres du système:

Variable Fonction
OPENAI_API_KEY Authentification API OpenAI pour les LLMs
ANTHROPIC_API_KEY Authentification API Anthropic (optionnel)
TEXT_CONFIG_KEY Clé de chiffrement pour la config sécurisée
BATCH_MODE Active le mode non-interactif pour tests automatisés
BATCH_TEXT Texte personnalisé pour mode batch (optionnel)

Note de sécurité: Le fichier .env ne doit jamais être versionné avec Git. Utilisez .env.example comme template.

# Charger les variables d'environnement (résolution de chemin robuste)
# CRITIQUE 1 : find_dotenv() dépend du CWD courant,
# qui peut differer de Argument_Analysis/ (Papermill, MCP Jupyter, CI changent
# le working directory). On localise Argument_Analysis/ par walk-up depuis le
# CWD, puis on s'y positionne (os.chdir) pour que les chemins relatifs
# (libs/ JARs Tweety, data/, extract_sources.json.gz.enc) résolvent
# correctement quel que soit le point de départ.
import os
from pathlib import Path
from dotenv import load_dotenv

def _find_argument_analysis_dir(start: Path, max_up: int = 6) -> Path:
    """Localise Argument_Analysis/ via son marqueur, en remontant depuis start."""
    marker = "Argumentation-08b-Executor-Python.ipynb"
    candidate = start.resolve()
    for _ in range(max_up):
        # Cas direct : on est déjà dans Argument_Analysis/
        if (candidate / marker).exists():
            return candidate
        # Cas nested : Argument_Analysis/ est un sous-dossier
        if (candidate / "Argument_Analysis" / marker).exists():
            return candidate / "Argument_Analysis"
        if len(candidate.parts) > 1:
            candidate = candidate.parent
        else:
            break
    raise RuntimeError(
        f"Dossier Argument_Analysis/ introuvable (marqueur={marker}) "
        f"en remontant depuis {start}"
    )

AA_DIR = _find_argument_analysis_dir(Path.cwd())
os.chdir(str(AA_DIR))  # %run relatifs + libs/ + data/ résolvent depuis ici
loaded_env = load_dotenv(AA_DIR / ".env", override=True)

print(f"CWD -> {os.getcwd()}")
print(f".env chargé: {loaded_env}")  # True si le .env a été trouvé et chargé
CWD -> D:\Dev\CoursIA-18644-argu\MyIA.AI.Notebooks\SymbolicAI\Argument_Analysis
.env chargé: False

Interprétation du résultat

Sortie attendue: .env chargé: True

Si vous obtenez False, cela signifie: - Le fichier .env n’existe pas dans le répertoire courant ou ses parents - Les variables d’environnement doivent être définies manuellement

Actions correctives: 1. Copier .env.example vers .env 2. Remplir les valeurs manquantes (clés API) 3. Relancer cette cellule

Vérification des dépendances

Avant de continuer, assurez-vous que les dépendances suivantes sont satisfaites:

Dépendances Python:

pip install python-dotenv semantic-kernel pandas jpype1 cryptography
pip install ipywidgets requests jupyter-ui-poll  # Pour mode interactif

Dépendances système: - JDK 11+ (pour Tweety via JPype): JAVA_HOME doit être configuré - Bibliothèque Tweety: JARs dans libs/ (téléchargement automatique via Tweety-01-Setup-Python.ipynb)

Fichiers requis:

Fichier Rôle Création
.env Variables d’environnement (clés API) Copier depuis .env.example
libs/*.jar Bibliothèque Tweety Java Téléchargement auto
data/logical_fallacies_taxonomy.json Taxonomie sophismes Fourni

Astuce: Si JPype échoue à s’initialiser, le système basculera en mode “fallback” (sans logique formelle). L’analyse informelle fonctionnera toujours.

2. Chargement de l’Interface Utilisateur

Exécution du notebook UI_Configuration.ipynb pour définir la fonction configure_analysis_task(). C’est ce notebook qui contient désormais toute la logique de l’interface graphique, du cache fichier et de la gestion de la configuration chiffrée.

# Mode batch contrôlé par variable d'environnement
# Si BATCH_MODE=true dans .env, on skip l'UI interactive (widgets bloquants)
import os
BATCH_MODE = os.getenv("BATCH_MODE", "false").lower() in ("true", "1", "yes")

if BATCH_MODE:
    print("Mode BATCH detecte (BATCH_MODE=true dans .env)")
    print("   -> Skip du chargement UI_configuration.ipynb (widgets non compatibles)")
    print("   -> Le texte sera fourni directement dans la cellule suivante")
else:
    # Executer le notebook UI pour definir la fonction configure_analysis_task
    # Resolution du chemin absolu (Papermill change le working directory)
    import os
    _notebook_dir = os.path.dirname(os.path.abspath("Argumentation-08b-Executor-Python.ipynb")) if os.path.exists("Argumentation-08b-Executor-Python.ipynb") else os.getcwd()
    _ui_nb_path = os.path.join(_notebook_dir, "Argumentation-08c-UI-Configuration-Python.ipynb")
    if os.path.exists(_ui_nb_path):
        print(f"Execution de {_ui_nb_path}...")
        get_ipython().run_line_magic("run", _ui_nb_path)
        print("Execution de Argumentation-08c-UI-Configuration-Python.ipynb terminee.")
    else:
        print(f"ATTENTION: {_ui_nb_path} non trouve. Passage en mode batch.")
        BATCH_MODE = True

    # Vérification que la fonction est bien définie après l'exécution
    if 'configure_analysis_task' not in locals():
        print("ERREUR CRITIQUE : La fonction configure_analysis_task n'a pas été définie par UI_Configuration.ipynb !")
    else:
        print("Fonction configure_analysis_task trouvée.")
Mode BATCH detecte (BATCH_MODE=true dans .env)
   -> Skip du chargement UI_configuration.ipynb (widgets non compatibles)
   -> Le texte sera fourni directement dans la cellule suivante

Modes d’exécution: Batch vs Interactif

Le notebook supporte deux modes d’exécution distincts:

Mode Activation Usage Interface
Interactif BATCH_MODE=false (défaut) Démonstration, exploration Widgets Jupyter (ipywidgets)
Batch BATCH_MODE=true dans .env Tests automatisés, CI/CD, Papermill Texte prédéfini sans interaction

Pourquoi le mode batch?

Les widgets Jupyter (ipywidgets) utilisent des boucles de polling bloquantes incompatibles avec: - L’exécution Papermill - Les MCP (Model Context Protocol) Jupyter - Les pipelines CI/CD automatisés

Le mode batch permet de contourner ces limitations en sautant le chargement de UI_configuration.ipynb et en utilisant directement un texte source.

Note technique: En mode batch, le texte est fourni via BATCH_TEXT (variable d’environnement) ou via le texte d’exemple enrichi TEXTE_EXEMPLE_BATCH, défini plus loin dans ce notebook (section « 3. Configuration de la Tâche et Récupération du Texte », après l’exercice 1).

Exercice 1 : Gestion de configuration avec fallback

L’Executor lit des variables d’environnement pour configurer l’analyse. Implementez un mécanisme de fallback robuste.

Objectif : Ecrire get_config_with_fallback.

Indices : - Étape 1 : Utiliser os.getenv(key) pour lire la variable - Étape 2 : Verifier si le résultat est None ou une chaîne vide - Étape 3 : Retourner la valeur par defaut si la variable est absente - Étape 4 : Tester avec une variable qui n’existe pas

# Exercice 1 : Gestion de configuration avec fallback
# TODO etudiant: ecrivez une fonction qui lit une variable d'environnement
# et retourne une valeur par defaut si absente.
# Etape 1: Utiliser os.getenv pour lire la variable
# Etape 2: Verifier si la valeur est None ou vide
# Etape 3: Retourner le fallback si necessaire
# Etape 4: Tester avec une variable inexistante
import os
def get_config_with_fallback(key, default="Texte par defaut"):
    return default  # TODO etudiant

text = get_config_with_fallback("BATCH_TEXT_EXERCICE", "Exemple de texte")
print(f"Texte choisi: {text}")
print("Exercice a completer")
Texte choisi: Exemple de texte
Exercice a completer

3. Configuration de la Tâche et Récupération du Texte

Sélection du texte à analyser

Cette étape configure le texte source selon le mode d’exécution:

Mode Interactif: - Interface graphique avec widgets Jupyter - Choix entre sources prédéfinies, URL, fichier, ou texte direct - Extraction de contenu (HTML, PDF, etc.) - Configuration chiffrée sauvegardée (extract_sources.json.gz.enc)

Mode Batch: - Texte fourni via variable d’environnement BATCH_TEXT - Ou texte d’exemple prédéfini (débat sur transition énergétique) - Pas d’interaction utilisateur requise

Attention: En mode interactif, la cellule suivante attendra que vous cliquiez sur “Lancer l’Analyse” dans l’interface. Ne passez pas à la suite avant d’avoir sélectionné un texte.

# Configuration du texte pour l'analyse
# En mode BATCH : utilise BATCH_TEXT (variable d'env) ou le texte d'exemple
# En mode interactif : appelle configure_analysis_task() (si disponible)

texte_pour_analyse = None

# Texte d'exemple enrichi pour le mode batch - contient des arguments complexes et des sophismes
TEXTE_EXEMPLE_BATCH = """Le debat sur la transition energetique en France revele des enjeux majeurs pour notre avenir.

ARGUMENTS EN FAVEUR DES ENERGIES RENOUVELABLES:
Les energies renouvelables sont essentielles pour lutter contre le changement climatique. La France s'est engagee a reduire ses emissions de CO2 de 40% d'ici 2030, ce qui necessite un investissement massif dans le solaire et l'eolien. D'ailleurs, l'Allemagne a deja prouve que c'etait possible en produisant 46% de son electricite a partir de sources renouvelables en 2023.

De plus, les energies renouvelables creent des emplois locaux non delocalisables. Selon l'ADEME, le secteur emploie deja plus de 150 000 personnes en France et pourrait en creer 500 000 supplementaires d'ici 2050. Quiconque s'oppose a cette transition est donc contre la creation d'emplois.

ARGUMENTS CONTRE LE DEVELOPPEMENT MASSIF DES RENOUVELABLES:
Cependant, les ecologistes veulent nous ramener a l'age de pierre en supprimant le nucleaire, qui fournit pourtant 70% de notre electricite. Sans le nucleaire, nous serions obliges de choisir entre des coupures d'electricite massives ou une dependance totale au gaz russe.

Le cout de la transition est exorbitant. Tout le monde sait que les eoliennes sont inefficaces car elles ne tournent que 25% du temps. Mon voisin Jean-Pierre, qui est ingenieur a EDF, m'a dit que les renouvelables ne pourront jamais remplacer le nucleaire. C'est un expert, donc il a forcemment raison.

De plus, les panneaux solaires sont fabriques en Chine avec du charbon, ce qui annule completement leur benefice ecologique. Donc investir dans le solaire revient a financer la pollution chinoise.

CONCLUSION:
Il faut arreter de debattre et agir. Soit on accepte une transition energetique complete vers 100% de renouvelables d'ici 2040, soit on condamne nos enfants a vivre sur une planete inhabitable. Des milliers de scientifiques ont signe une petition pour le climat, ce qui prouve definitivement que nous avons raison de promouvoir les energies vertes. Les climatosceptiques sont finances par les lobbies petroliers et ne meritent pas qu'on ecoute leurs arguments.

En conclusion, bien que certains arguments des deux cotes aient du merite, la question necessite une analyse nuancee des compromis entre independance energetique, impact environnemental, cout economique et faisabilite technique."""

if BATCH_MODE:
    # === MODE BATCH ===
    print("Mode BATCH - Configuration automatique du texte")
    
    # Priorite 1: Variable d'environnement BATCH_TEXT
    batch_text_env = os.getenv("BATCH_TEXT", "")
    if batch_text_env:
        texte_pour_analyse = batch_text_env
        print(f"Texte charge depuis BATCH_TEXT ({len(texte_pour_analyse)} caracteres)")
    else:
        # Priorite 2: Texte d'exemple par defaut
        texte_pour_analyse = TEXTE_EXEMPLE_BATCH
        print(f"Texte d'exemple enrichi utilise ({len(texte_pour_analyse)} caracteres)")
    
    print(f"\nExtrait du texte:\n{texte_pour_analyse[:200]}...")

else:
    # === MODE INTERACTIF ===
    print("Mode INTERACTIF - Lancement de l'interface de configuration")
    
    if 'configure_analysis_task' in locals():
        try:
            texte_pour_analyse = configure_analysis_task()
            print(f"Texte recupere via l'interface ({len(texte_pour_analyse) if texte_pour_analyse else 0} caracteres)")
        except Exception as e_ui:
            print(f"Erreur lors de la configuration UI : {e_ui}")
            import traceback
            traceback.print_exc()
    else:
        print("Fonction configure_analysis_task non disponible - verifiez le chargement de UI_configuration.ipynb")

# Verification finale
if not texte_pour_analyse:
    print("\nAucun texte prepare. L'analyse ne peut pas continuer.")
else:
    print(f"\nTexte pret pour l'analyse (longueur: {len(texte_pour_analyse)}). Passage au chargement des agents.")
Mode BATCH - Configuration automatique du texte
Texte d'exemple enrichi utilise (2348 caracteres)

Extrait du texte:
Le debat sur la transition energetique en France revele des enjeux majeurs pour notre avenir.

ARGUMENTS EN FAVEUR DES ENERGIES RENOUVELABLES:
Les energies renouvelables sont essentielles pour lutter ...

Texte pret pour l'analyse (longueur: 2348). Passage au chargement des agents.

Interprétation de la configuration du texte

Résultat attendu (Mode Batch):

Mode BATCH - Configuration automatique du texte
Texte charge depuis BATCH_TEXT (XXX caractères)
ou
Texte d'exemple enrichi utilise (XXX caractères)

Résultat attendu (Mode Interactif):

Mode INTERACTIF - Lancement de l'interface de configuration
Texte recupere via l'interface (XXX caractères)

Structure du texte d’exemple:

Le texte d’exemple enrichi contient intentionnellement plusieurs sophismes pour démonstration:

Sophisme Exemple dans le texte
Faux dilemme “Soit 100% renouvelables, soit planète inhabitable”
Appel à l’autorité “Mon voisin ingénieur EDF dit que…”
Ad hominem “Climatosceptiques financés par lobbies pétroliers”
Généralisation hâtive “Tout le monde sait que les éoliennes…”
Pente glissante “Les écologistes veulent nous ramener à l’âge de pierre”

Conseil pédagogique: Le texte d’exemple est conçu pour tester la robustesse de l’analyse rhétorique multi-agents. Utilisez-le pour comprendre comment les agents détectent les arguments fallacieux.

4. Chargement des Définitions des Agents et de l’Orchestration

Architecture modulaire : la shim argumentation_lib

L’état partagé, le pont Semantic Kernel et l’orchestration sont fournis par la shim de série argumentation_lib (vendoring du cœur d’analyse, cf argumentation_lib/_runner.py) :

Élément chargé Origine
RhetoricalAnalysisState argumentation_lib/_shared_state.py
StateManagerPlugin argumentation_lib/_state_manager_plugin.py
AnalysisRunner (pipeline 3 phases) argumentation_lib/_runner.py

Les carnets legacy Argument_Analysis_Agentic-*_agent.ipynb qui portaient ces définitions (bootstrap Semantic Kernel, agents LLM, stratégies d’orchestration) sont archivés dans _archive/ — leur inventaire capacité par capacité y est consigné. La greffe de ce notebook sur la shim supprime le dernier consommateur vivant de cette chaîne.

Ordre de chargement :

argumentation_lib (état + orchestration)
    ↓
kernel LLM construit ici depuis .env (service `global_llm_service`)
    ↓
AnalysisRunner(kernel, service_id, state) → run()

Note technique : le kernel est injecté par ce notebook ; la shim ne lit ni secret ni nom de modèle (elle ne connaît pas .env).

# Charger les définitions depuis la shim argumentation_lib.
# Remplace la chaîne legacy %run des carnets *_agent (archivés) : la shim vendore
# l'état partagé, le pont SK et l'orchestration ; le kernel LLM est construit ici
# depuis .env (mêmes variables que la chaîne archivée : OPENAI_API_KEY,
# OPENAI_CHAT_MODEL_ID, OPENAI_ENDPOINT, OPENAI_ORG_ID).
if 'texte_pour_analyse' in locals() and texte_pour_analyse:
    print("Chargement des definitions des agents et de l'orchestration...")
    try:
        from argumentation_lib import UnifiedAnalysisState, get_analysis_runner
        from semantic_kernel import Kernel
        from semantic_kernel.connectors.ai.open_ai import (
            OpenAIChatCompletion,
            AzureChatCompletion,
        )

        _api_key = os.getenv("OPENAI_API_KEY")
        _model_id = os.getenv("OPENAI_CHAT_MODEL_ID")
        _endpoint = os.getenv("OPENAI_ENDPOINT")
        _org_id = os.getenv("OPENAI_ORG_ID")

        llm_kernel = Kernel()
        if _endpoint:
            llm_kernel.add_service(AzureChatCompletion(
                service_id="global_llm_service",
                deployment_name=_model_id,
                endpoint=_endpoint,
                api_key=_api_key,
            ))
            print(f"[OK] Service LLM Azure configure (deploiement: {_model_id}).")
        else:
            llm_kernel.add_service(OpenAIChatCompletion(
                service_id="global_llm_service",
                ai_model_id=_model_id,
                api_key=_api_key,
                org_id=_org_id,
            ))
            print(f"[OK] Service LLM OpenAI configure (modele: {_model_id}).")

        AnalysisRunner = get_analysis_runner()
        local_state = UnifiedAnalysisState(texte_pour_analyse)
        analysis_runner = AnalysisRunner(llm_kernel, "global_llm_service", local_state)
        print("[OK] Definitions chargees (shim argumentation_lib).")
    except Exception as e_run:
        print("[ERREUR] Une erreur est survenue lors du chargement des definitions : " + str(e_run))
        import traceback
        traceback.print_exc()
        # Empecher la suite si le chargement echoue
        texte_pour_analyse = None
        analysis_runner = None
else:
    print("Skipping agent definition loading because no text was prepared.")
Chargement des definitions des agents et de l'orchestration...
[OK] Service LLM OpenAI configure (modele: gpt-5.6-sol).
[OK] Definitions chargees (shim argumentation_lib).

Architecture chargée

Composants et responsabilités :

Composant Origine Rôle
RhetoricalAnalysisState argumentation_lib/_shared_state.py État partagé (raw_text, arguments, sophismes, belief sets, conclusion)
StateManagerPlugin argumentation_lib/_state_manager_plugin.py Pont entre l’état et le kernel Semantic Kernel
AnalysisRunner argumentation_lib/_runner.py Pipeline 3 phases : informel → formel → synthèse
kernel global_llm_service construit par ce notebook (.env) Service LLM (OpenAI ou Azure)

Architecture technique :

┌─────────────────────────────────────────────────┐
│  Semantic Kernel (orchestration)                │
│  ┌──────────────┐  ┌────────────────────────┐  │
│  │ Informal     │  │ Logic                  │  │
│  │ Agent (LLM)  │─▶│ Agent (LLM)            │  │
│  └──────────────┘  └────────────────────────┘  │
│         │                     │                 │
│         └─────────┬───────────┘                 │
│                   ▼                             │
│        ┌────────────────────┐                   │
│        │ StateManager       │                   │
│        │ Plugin             │                   │
│        │ (état partagé)     │                   │
│        └────────────────────┘                   │
└─────────────────────────────────────────────────┘

Différence avec la chaîne archivée : l’agent formel de la shim est LLM-only (pas de plugin Tweety) ; la vérification formelle outillée est le rung Argumentation-05. La chaîne déterministe équivalente (paradigmes A/B, routage PM) est le rung Argumentation-07.

5. Exécution de l’Analyse Collaborative

Orchestration asynchrone avec Semantic Kernel

Cette cellule lance le processus d’analyse multi-agents. L’orchestration fonctionne en mode asynchrone pour gérer efficacement les appels API LLM.

Composants techniques:

Composant Rôle
nest_asyncio.apply() Permet await dans Jupyter (normalement synchrone)
AnalysisRunner.run() Pipeline d’orchestration principal (shim argumentation_lib)
local_state Objet RhetoricalAnalysisState retourné

Étapes d’exécution internes:

  1. Initialisation: Kernel SK construit depuis .env, StateManagerPlugin enregistré
  2. Phase 1 - Analyse informelle: Agent LLM identifie arguments/sophismes
  3. Phase 2 - Analyse formelle: Agent LLM traduit en logique et journalise les requêtes
  4. Phase 3 - Synthèse: Génération de la conclusion
  5. Retour: État partagé (RhetoricalAnalysisState) portant tous les résultats

Note performance: L’exécution prend 90-150s selon la complexité du texte et la latence des API OpenAI/Anthropic. Surveillez les logs pour suivre la progression.

import nest_asyncio
import asyncio

# Lancer seulement si on a obtenu un texte valide ET que les definitions sont chargees
if 'analysis_runner' in locals() and analysis_runner is not None:
    print("\nLancement de l'execution asynchrone de l'analyse...")
    nest_asyncio.apply()
    try:
        analysis_result = await analysis_runner.run()
        print("\nAnalyse terminee.")
        print(f"  Phases executees : {[p.get('name') for p in analysis_result.get('phases', [])]}")
        if analysis_result.get('conclusion'):
            print("  Conclusion finale : disponible dans l'etat partage.")
    except Exception as e_analysis:
        print(f"\nUne erreur est survenue pendant l'execution de l'analyse : {e_analysis}")
        import traceback
        traceback.print_exc()

elif 'texte_pour_analyse' not in locals() or not texte_pour_analyse:
    print("\nAnalyse non lancee : aucun texte n'a ete prepare ou une erreur est survenue avant.")
else:  # Implique que l'orchestrateur n'a pas ete charge
    print("\nAnalyse non lancee : l'orchestrateur n'a pas pu etre charge.")

Lancement de l'execution asynchrone de l'analyse...

Analyse terminee.
  Phases executees : ['informal', 'formal', 'synthesis']
  Conclusion finale : disponible dans l'etat partage.

Interprétation du processus d’analyse

Étapes d’exécution observables:

  1. Analyse informelle (InformalAnalysisAgent):
    • Identification des arguments principaux
    • Détection des sophismes via taxonomie
    • Stockage dans local_state.identified_arguments et identified_fallacies
  2. Formalisation logique (PropositionalLogicAgent):
    • Traduction des arguments en propositions PL
    • Création de Belief Sets Tweety
    • Exécution de requêtes logiques (cohérence, implication)
  3. Synthèse collaborative:
    • Combinaison des résultats informels et formels
    • Génération de la conclusion finale
    • Stockage dans local_state.final_conclusion

Durée typique: 90-150 secondes (selon complexité du texte et latence API)

Logs à surveiller:

Message Signification
[InformalAnalysisAgent] Processing... Analyse rhétorique en cours
[PropositionalLogicAgent] Creating belief set... Formalisation PL
FUNC_ERROR: JPype not initialized Tweety non disponible (fallback mode)
Final state: Affichage JSON de l’état final

Note de performance: En mode batch, l’exécution complète prend quelques minutes (durée machine-dépendante : latence API LLM et complexité du texte ; plage typique donnée ci-dessus) sur le texte d’exemple enrichi.

Exercice 2 : Compter les sophismes dans un rapport

Après une analyse collaborative, le rapport contient une liste de sophismes detectes. Ecrivez une fonction pour les compter.

Objectif : Implementer count_fallacies.

Indices : - Étape 1 : Parser la chaîne JSON avec json.loads - Étape 2 : Acceder a la cle fallacies (peut etre imbriquee) - Étape 3 : Compter les éléments de la liste - Étape 4 : Retourner 0 si la cle est absente

# Exercice 2 : Compter les sophismes dans un rapport JSON
# TODO etudiant: ecrivez une fonction qui extrait le nombre de sophismes
# d'un rapport d'analyse.
# Etape 1: Parser le JSON
# Etape 2: Acceder a la cle "fallacies"
# Etape 3: Compter les elements
# Etape 4: Gerer le cas ou la cle est absente
import json
def count_fallacies(report_str):
    return 0  # TODO etudiant

report = '{"analysis": {"fallacies": ["Ad Hominem", "Straw Man"]}}'
print(f"Nombre de sophismes: {count_fallacies(report)}")
print("Exercice a completer")
Nombre de sophismes: 0
Exercice a completer

5bis. Rapport de Validation de l’Analyse

Validation structurée avec métriques de confiance

Cette section génère un rapport de validation cross-validé qui vérifie la complétude et la cohérence de l’analyse rhétorique.

Objectif pédagogique: Démontrer l’importance de la validation automatisée dans les systèmes d’IA, en particulier pour: - Détecter les analyses incomplètes - Calculer un score de confiance quantitatif - Identifier les composants manquants - Fournir un rapport exploitable (JSON)

Méthode de validation:

La validation cross-check vérifie 6 critères indépendants:

Critère Validation Poids
ARGUMENTS_IDENTIFIED len(identified_arguments) > 0 1/6
FALLACIES_ANALYZED len(identified_fallacies) > 0 1/6
BELIEF_SET_CREATED len(belief_sets) > 0 1/6
QUERIES_EXECUTED len(query_log) > 0 1/6
QUERIES_MEANINGFUL Au moins 1 résultat ACCEPTED/REJECTED 1/6
CONCLUSION_GENERATED final_conclusion is not None 1/6

Score de confiance: confidence = nombre_critères_validés / 6

Application pratique: Ce type de validation est essentiel dans les systèmes de production pour garantir la qualité des analyses automatisées. Le rapport JSON peut alimenter des tableaux de bord de monitoring.

# === CELLULE DE VALIDATION FINALE (validation de contenu, issue #18395) ===
# Genere un rapport JSON structure avec cross-validation.
# Cette version verifie le CONTENU (et pas seulement la presence des champs) :
#   * un argument doit etre une formulation reprise du texte source,
#     jamais une cle de l'etat interne de l'agent ;
#   * un sophisme doit porter un type, une justification et une cible ;
#   * une requete PL ne compte comme significative qu'avec un verdict
#     ACCEPTED ou REJECTED (pas une simple execution sans reponse).

import json
import os
import re
from datetime import datetime
from typing import Dict, Any, List, Set


# --- Constantes de validation de contenu ---
# Cles de l'etat partage qui ne sont PAS des arguments (defaut mesure #18395).
# Ces noms apparaissent comme valeurs de identified_arguments quand l'agent
# informel a confondu son etat interne avec le contenu du texte.
STATE_KEYS_BLACKLIST: Set[str] = {
    "analysis_state", "arguments", "identified_arguments", "identified_fallacies",
    "belief_sets", "query_log", "answers", "raw_text", "final_conclusion",
    "shared_state", "task_context", "metadata", "state",
}

# Longueur minimale d'une description d'argument (en mots) pour qu'on
# la considere comme une phrase du texte, pas une etiquette.
MIN_ARGUMENT_WORDS: int = 4

# Longueur minimale de la sous-chaine commune (en caracteres) entre
# la description de l'argument et le texte source.
MIN_ARGUMENT_TEXT_OVERLAP: int = 12


def _resolve_chat_model_id() -> str:
    """Substitue les identifiants de modele retires (gpt-5-mini etc.).

    Port minimal de resolve_chat_endpoint
    (jsboigeEpita/2025-Epita-Intelligence-Symbolique core/llm_service.py L279).
    La liste OBSOLETE_MODEL_SUBSTITUTIONS complete sera portee dans une PR
    de suivi ; ici on couvre les trois modeles les plus frequemment retires
    depuis 2026-09.
    """
    OBSOLETE_MODEL_SUBSTITUTIONS = {
        "gpt-5-mini": "gpt-4o-mini",
        "gpt-5": "gpt-4o",
        "gpt-5-nano": "gpt-4o-mini",
    }
    configured = os.getenv("OPENAI_CHAT_MODEL_ID", "<unset>")
    return OBSOLETE_MODEL_SUBSTITUTIONS.get(configured, configured)


def _text_overlap(text: str, candidate: str) -> int:
    """Longueur du plus long suffixe/prefixe commun (proxy rapide).

    On evite SequenceMatcher (trop lent) ; on cherche une sous-chaine
    contigue d'au moins MIN_ARGUMENT_TEXT_OVERLAP caracteres en minuscule,
    insensible aux espaces multiples et a la ponctuation simple.
    """
    if not text or not candidate:
        return 0
    a = re.sub(r"\s+", " ", text.lower())
    b = re.sub(r"\s+", " ", candidate.lower())
    best = 0
    n = len(b)
    upper = min(n, 80)
    for size in range(upper, MIN_ARGUMENT_TEXT_OVERLAP - 1, -1):
        for i in range(0, n - size + 1):
            sub = b[i:i + size]
            if sub in a:
                return size
    return 0


def _argument_has_substance(arg_desc: str, raw_text: str) -> bool:
    """Verifie qu'une description d'argument est reellement extraite du texte.

    Trois conditions, toutes requises :
    1. Description non vide apres strip.
    2. Description pas dans STATE_KEYS_BLACKLIST (defaut #18395 : l'agent
       confond son etat interne avec les arguments du texte).
    3. Description chevauche le texte source sur au moins
       MIN_ARGUMENT_TEXT_OVERLAP caracteres (defaut : "analysis_state" /
       "arguments" sont des mots-cles internes, ils n'apparaissent pas
       dans le texte utilisateur sur la transition energetique).
    """
    desc = str(arg_desc).strip()
    if not desc:
        return False
    if desc.lower() in STATE_KEYS_BLACKLIST:
        return False
    if len(desc.split()) < MIN_ARGUMENT_WORDS:
        return False
    if _text_overlap(raw_text or "", desc) < MIN_ARGUMENT_TEXT_OVERLAP:
        return False
    return True


def _fallacy_has_substance(f_data: Dict[str, Any]) -> bool:
    """Verifie qu'un sophisme detecte est reel (defaut #18395 : Type Inconnu).

    Conditions :
    1. Type non vide, different de "Type Inconnu" / "Unknown" / "" / None.
    2. Justification non vide (au moins 10 caracteres, pas la valeur par
       defaut "Justification manquante").
    3. target_argument_id non null (defaut : sophisme en l'air).
    """
    if not isinstance(f_data, dict):
        return False
    ftype = str(f_data.get("type", "")).strip()
    if not ftype or ftype.lower() in ("unknown", "type inconnu", ""):
        return False
    justification = str(f_data.get("justification", "")).strip()
    if not justification or justification.lower() == "justification manquante":
        return False
    if len(justification) < 10:
        return False
    target = f_data.get("target_argument_id")
    if target is None or target == "":
        return False
    return True


def generate_validated_analysis_report(state) -> Dict[str, Any]:
    """Genere rapport JSON structure avec cross-validation (controle de contenu).

    Sortie : dict avec metadata, informal_analysis, formal_analysis,
    cross_validation (avec checks_passed + issues + failed_reason),
    conclusion, et resolved_model_id (#18395).
    """
    raw_text = getattr(state, "raw_text", None) or ""
    report = {
        "metadata": {
            "timestamp": datetime.now().isoformat(),
            "version": "2.1-validated-content",
            "text_length": len(raw_text) if raw_text else 0,
            "text_snippet": (raw_text[:150] + "...") if raw_text and len(raw_text) > 150 else (raw_text or ""),
            "resolved_model_id": _resolve_chat_model_id(),
        },
        "informal_analysis": {
            "arguments": [],
            "arguments_substantive_count": 0,
            "fallacies": [],
            "fallacies_substantive_count": 0,
            "taxonomy_families_used": set(),
        },
        "formal_analysis": {
            "belief_sets": [],
            "query_results": [],
            "queries_meaningful_count": 0,
            "consistency_checked": False,
        },
        "cross_validation": {
            "validation_status": "INCOMPLETE",
            "confidence_score": 0.0,
            "checks_passed": [],
            "issues": [],
            "failed_reason": None,
        },
        "conclusion": {
            "summary": getattr(state, "final_conclusion", None),
            "is_complete": hasattr(state, "final_conclusion") and state.final_conclusion is not None,
        },
    }

    # --- Populate arguments ---
    substantive_args: List[Dict[str, Any]] = []
    if hasattr(state, "identified_arguments"):
        for arg_id, arg_desc in state.identified_arguments.items():
            has_fallacy = False
            if hasattr(state, "identified_fallacies"):
                has_fallacy = any(
                    f.get("target_argument_id") == arg_id
                    for f in state.identified_fallacies.values()
                )
            report["informal_analysis"]["arguments"].append({
                "id": arg_id,
                "description": str(arg_desc)[:200],
                "has_fallacy": has_fallacy,
                "is_substantive": _argument_has_substance(arg_desc, raw_text),
            })
            if _argument_has_substance(arg_desc, raw_text):
                substantive_args.append({"id": arg_id, "description": str(arg_desc)})
    report["informal_analysis"]["arguments_substantive_count"] = len(substantive_args)

    # --- Populate fallacies ---
    substantive_fallacies: List[Dict[str, Any]] = []
    if hasattr(state, "identified_fallacies"):
        for f_id, f_data in state.identified_fallacies.items():
            fallacy_type = f_data.get("type", "Unknown") if isinstance(f_data, dict) else str(f_data)
            justification = f_data.get("justification", "") if isinstance(f_data, dict) else ""
            target_id = f_data.get("target_argument_id") if isinstance(f_data, dict) else None
            severity = "HIGH" if any(kw in str(fallacy_type).lower() for kw in ["manipulation", "tromperie"]) else "MEDIUM"
            report["informal_analysis"]["fallacies"].append({
                "id": f_id,
                "type": fallacy_type,
                "justification": justification,
                "target_id": target_id,
                "severity": severity,
                "is_substantive": _fallacy_has_substance(f_data),
            })
            if isinstance(fallacy_type, str) and "/" in fallacy_type:
                report["informal_analysis"]["taxonomy_families_used"].add(fallacy_type.split("/")[0])
            if _fallacy_has_substance(f_data):
                substantive_fallacies.append({"id": f_id, "type": fallacy_type})
    report["informal_analysis"]["fallacies_substantive_count"] = len(substantive_fallacies)
    report["informal_analysis"]["taxonomy_families_used"] = list(
        report["informal_analysis"]["taxonomy_families_used"]
    )

    # --- Populate belief sets ---
    if hasattr(state, "belief_sets"):
        for bs_id, bs_data in state.belief_sets.items():
            content = bs_data.get("content", "") if isinstance(bs_data, dict) else str(bs_data)
            report["formal_analysis"]["belief_sets"].append({
                "id": bs_id,
                "logic_type": bs_data.get("logic_type", "PL") if isinstance(bs_data, dict) else "PL",
                "formula_count": content.count("\n") + 1 if content else 0,
                "is_consistent": "NOT_CHECKED",
            })

    # --- Populate query results ---
    meaningful_count = 0
    if hasattr(state, "query_log"):
        for qlog in state.query_log:
            raw_result = qlog.get("raw_result", "") if isinstance(qlog, dict) else ""
            status = "UNKNOWN"
            if "ACCEPTED" in str(raw_result):
                status = "ACCEPTED"
            elif "REJECTED" in str(raw_result):
                status = "REJECTED"
            elif "FUNC_ERROR" in str(raw_result):
                status = "ERROR"
            if status in ("ACCEPTED", "REJECTED"):
                meaningful_count += 1
            report["formal_analysis"]["query_results"].append({
                "log_id": qlog.get("log_id", "") if isinstance(qlog, dict) else "",
                "belief_set_id": qlog.get("belief_set_id", "") if isinstance(qlog, dict) else "",
                "query": qlog.get("query", "") if isinstance(qlog, dict) else "",
                "status": status,
            })
    report["formal_analysis"]["queries_meaningful_count"] = meaningful_count

    # --- CROSS-VALIDATION (contenu) ---
    checks: List[str] = []
    issues: List[str] = []
    failed_reasons: List[str] = []

    # Check 1 : arguments REELS (defaut : "analysis_state" / "arguments" passaient)
    if substantive_args:
        checks.append("ARGUMENTS_IDENTIFIED")
    elif len(report["informal_analysis"]["arguments"]) > 0:
        # Arguments presents mais aucun n'est une vraie sous-chaine du texte
        issues.append(
            f"{len(report['informal_analysis']['arguments'])} argument(s) detecte(s) "
            f"mais aucun ne provient du texte (cles d'etat partage ou libelles internes)."
        )
        failed_reasons.append("ARGUMENTS_FORM_ONLY")
    else:
        issues.append("Aucun argument identifie.")
        failed_reasons.append("ARGUMENTS_NONE")

    # Check 2 : sophismes REELS (defaut : "Type Inconnu" passait via FALLACY_ANALYSIS_ATTEMPTED)
    if substantive_fallacies:
        checks.append("FALLACIES_ANALYZED")
    elif len(report["informal_analysis"]["fallacies"]) > 0:
        issues.append(
            f"{len(report['informal_analysis']['fallacies'])} sophisme(s) detecte(s) "
            f"mais aucun n'a de type/justification/cible valides."
        )
        failed_reasons.append("FALLACIES_FORM_ONLY")
    elif hasattr(state, "answers") and any("sophisme" in str(v).lower() for v in state.answers.values()):
        checks.append("FALLACY_ANALYSIS_ATTEMPTED")
    else:
        issues.append("Analyse sophismes non effectuee.")
        failed_reasons.append("FALLACIES_NONE")

    # Check 3 : belief set PL cree
    if len(report["formal_analysis"]["belief_sets"]) > 0:
        checks.append("BELIEF_SET_CREATED")
    else:
        issues.append("Aucun Belief Set PL cree.")
        failed_reasons.append("BELIEF_SET_NONE")

    # Check 4a : au moins une requete soumise
    if len(report["formal_analysis"]["query_results"]) > 0:
        checks.append("QUERIES_SUBMITTED")
    else:
        issues.append("Aucune requete PL soumise.")
        failed_reasons.append("QUERIES_NONE")

    # Check 4b : au moins une requete avec verdict (ACCEPTED ou REJECTED)
    if meaningful_count > 0:
        checks.append("QUERIES_MEANINGFUL")
    else:
        issues.append("Aucune requete PL n'a produit de verdict (ACCEPTED/REJECTED).")
        failed_reasons.append("QUERIES_NOT_MEANINGFUL")

    # Check 5 : conclusion generee
    if report["conclusion"]["is_complete"]:
        checks.append("CONCLUSION_GENERATED")
    else:
        issues.append("Conclusion finale non generee.")
        failed_reasons.append("CONCLUSION_NONE")

    # --- Score et statut ---
    max_checks = 7  # ARGUMENTS, FALLACIES, BELIEF_SET, QUERIES_SUBMITTED, QUERIES_MEANINGFUL, CONCLUSION (+ split)
    confidence = len(checks) / max_checks
    report["cross_validation"]["confidence_score"] = round(confidence, 2)
    report["cross_validation"]["checks_passed"] = checks
    report["cross_validation"]["issues"] = issues
    report["cross_validation"]["failed_reason"] = failed_reasons[0] if failed_reasons else None

    # Determination du statut : un ARGUMENTS_FORM_ONLY ou FALLACIES_FORM_ONLY
    # degrade systematiquement le statut, sans annuler entierement le
    # pipeline (on garde un diagnostic STRUCTURED plutot que INCOMPLETE
    # binaire). Justification : la validation precedente disait 67%
    # PARTIAL_VALIDATED sur un run dont les 2 arguments etaient des
    # mots-cles d'etat -- c'est exactement le bug corrige ici.
    if any(fr in ("ARGUMENTS_FORM_ONLY", "FALLACIES_FORM_ONLY") for fr in failed_reasons):
        report["cross_validation"]["validation_status"] = "INVALIDATED_FORM"
    elif confidence >= 0.8:
        report["cross_validation"]["validation_status"] = "COMPLETE_VALIDATED"
    elif confidence >= 0.5:
        report["cross_validation"]["validation_status"] = "PARTIAL_VALIDATED"
    elif confidence >= 0.3:
        report["cross_validation"]["validation_status"] = "MINIMAL"
    else:
        report["cross_validation"]["validation_status"] = "INCOMPLETE"

    return report


def display_validation_summary(report: Dict[str, Any]) -> str:
    """Affiche resume lisible du rapport de validation."""
    print("\n" + "=" * 70)
    print("          RAPPORT D'ANALYSE RHETORIQUE VALIDEE")
    print("=" * 70)
    cv = report["cross_validation"]
    status_symbols = {
        "COMPLETE_VALIDATED": "[OK]",
        "PARTIAL_VALIDATED": "[PARTIEL]",
        "MINIMAL": "[MINIMAL]",
        "INCOMPLETE": "[INCOMPLET]",
        "INVALIDATED_FORM": "[FORME_INVALIDE]",
    }
    print(f"\n  STATUT: {status_symbols.get(cv['validation_status'], '?')} {cv['validation_status']}")
    print(f"  CONFIANCE: {cv['confidence_score'] * 100:.0f}%")
    if cv["failed_reason"]:
        print(f"  ECHEC CONTENU: {cv['failed_reason']}")
    print(f"\n  [ANALYSE INFORMELLE]")
    print(f"    Arguments: {len(report['informal_analysis']['arguments'])} "
          f"(substantifs: {report['informal_analysis']['arguments_substantive_count']})")
    print(f"    Sophismes: {len(report['informal_analysis']['fallacies'])} "
          f"(substantifs: {report['informal_analysis']['fallacies_substantive_count']})")
    print(f"\n  [ANALYSE FORMELLE]")
    print(f"    Belief Sets: {len(report['formal_analysis']['belief_sets'])}")
    print(f"    Requetes: {len(report['formal_analysis']['query_results'])} "
          f"(significatives: {report['formal_analysis']['queries_meaningful_count']})")
    if report["metadata"].get("resolved_model_id"):
        print(f"\n  [MODELE]")
        print(f"    Modele resolu: {report['metadata']['resolved_model_id']}")
    print(f"\n  [VALIDATIONS PASSEES]")
    for check in cv["checks_passed"]:
        print(f"    [+] {check}")
    if cv["issues"]:
        print(f"\n  [PROBLEMES DETECTES]")
        for issue in cv["issues"]:
            print(f"    [-] {issue}")
    print(f"\n  [CONCLUSION]")
    if report["conclusion"]["is_complete"]:
        conclusion_preview = str(report["conclusion"]["summary"])[:200]
        print(f"    {conclusion_preview}...")
    else:
        print("    (Non generee)")
    print("\n" + "=" * 70)
    return cv["validation_status"]


# === EXECUTION DE LA VALIDATION ===
print("\n--- Generation du Rapport d'Analyse Validee ---")
print(f"Modele resolu : {_resolve_chat_model_id()}")

# Chercher l'etat local_state defini par l'orchestration
if "local_state" in dir() and local_state is not None:
    # Generate report
    final_report = generate_validated_analysis_report(local_state)
    # Display summary
    validation_status = display_validation_summary(final_report)
    # Export JSON
    output_path = "output/analysis_report.json"
    try:
        os.makedirs("output", exist_ok=True)
        with open(output_path, "w", encoding="utf-8") as f:
            json.dump(final_report, f, indent=2, ensure_ascii=False, default=str)
        print(f"\nRapport JSON exporte vers: {output_path}")
    except Exception as e:
        print(f"Erreur export JSON: {e}")
    # Afficher JSON complet
    print("\n--- RAPPORT JSON COMPLET ---")
    print(json.dumps(final_report, indent=2, ensure_ascii=False, default=str))
    # Final verdict
    print("\n--- VERDICT FINAL ---")
    if validation_status == "COMPLETE_VALIDATED":
        print("[OK] Analyse COMPLETE et validee.")
    elif validation_status == "PARTIAL_VALIDATED":
        print("[PARTIEL] Analyse partielle : certains controles de contenu ont echoue.")
    elif validation_status == "INVALIDATED_FORM":
        print("[INVALIDE] Sortie degeneree : les champs sont presents mais le contenu est vide/incorrect.")
        print("         Voir failed_reason et issues pour le diagnostic detaille.")
    else:
        print(f"[{validation_status}] Verifier les problemes detectes ci-dessus.")
else:
    print("Etat 'local_state' non disponible : orchestrateur non execute.")
    print("La validation ne peut pas tourner sans un etat issu d'un run agentique.")

--- Generation du Rapport d'Analyse Validee ---
Modele resolu : gpt-5.6-sol

======================================================================
          RAPPORT D'ANALYSE RHETORIQUE VALIDEE
======================================================================

  STATUT: [PARTIEL] PARTIAL_VALIDATED
  CONFIANCE: 71%
  ECHEC CONTENU: QUERIES_NOT_MEANINGFUL

  [ANALYSE INFORMELLE]
    Arguments: 9 (substantifs: 7)
    Sophismes: 0 (substantifs: 0)

  [ANALYSE FORMELLE]
    Belief Sets: 3
    Requetes: 6 (significatives: 0)

  [MODELE]
    Modele resolu: gpt-5.6-sol

  [VALIDATIONS PASSEES]
    [+] ARGUMENTS_IDENTIFIED
    [+] FALLACY_ANALYSIS_ATTEMPTED
    [+] BELIEF_SET_CREATED
    [+] QUERIES_SUBMITTED
    [+] CONCLUSION_GENERATED

  [PROBLEMES DETECTES]
    [-] Aucune requete PL n'a produit de verdict (ACCEPTED/REJECTED).

  [CONCLUSION]
    Synthèse finale — Le texte présente des raisons pertinentes de développer les renouvelables (réduction des émissions, diversification du mix, emplois) et des préoccupations recevables quant au coût, à...

======================================================================

Rapport JSON exporte vers: output/analysis_report.json

--- RAPPORT JSON COMPLET ---
{
  "metadata": {
    "timestamp": "2026-10-02T05:35:06.039094",
    "version": "2.1-validated-content",
    "text_length": 2348,
    "text_snippet": "Le debat sur la transition energetique en France revele des enjeux majeurs pour notre avenir.\n\nARGUMENTS EN FAVEUR DES ENERGIES RENOUVELABLES:\nLes ene...",
    "resolved_model_id": "gpt-5.6-sol"
  },
  "informal_analysis": {
    "arguments": [
      {
        "id": "arg_1",
        "description": "analysis_state",
        "has_fallacy": false,
        "is_substantive": false
      },
      {
        "id": "arg_2",
        "description": "arguments",
        "has_fallacy": false,
        "is_substantive": false
      },
      {
        "id": "arg_3",
        "description": "Les renouvelables sont essentielles contre le changement climatique ; l'objectif français de baisse du CO2 de 40 % d'ici 2030 nécessiterait un investissement massif dans le solaire et l'éolien.",
        "has_fallacy": false,
        "is_substantive": true
      },
      {
        "id": "arg_4",
        "description": "La part de 46 % d'électricité renouvelable en Allemagne en 2023 montrerait la possibilité de développer ces sources.",
        "has_fallacy": false,
        "is_substantive": true
      },
      {
        "id": "arg_5",
        "description": "Les renouvelables créeraient des emplois locaux : le texte cite des emplois actuels et une projection attribuée à l'ADEME.",
        "has_fallacy": false,
        "is_substantive": true
      },
      {
        "id": "arg_6",
        "description": "S'opposer à la transition proposée reviendrait à s'opposer à la création d'emplois.",
        "has_fallacy": false,
        "is_substantive": true
      },
      {
        "id": "arg_7",
        "description": "Les écologistes chercheraient à supprimer le nucléaire, qui fournit une part importante de l'électricité française, et à ramener la société à l'âge de pierre.",
        "has_fallacy": false,
        "is_substantive": true
      },
      {
        "id": "arg_8",
        "description": "Sans nucléaire, la France devrait choisir entre des coupures massives d'électricité et une dépendance totale au gaz russe.",
        "has_fallacy": false,
        "is_substantive": true
      },
      {
        "id": "arg_9",
        "description": "La transition serait exorbitante et les éoliennes inefficaces parce qu'elles ne tourneraient que 25 % du temps.",
        "has_fallacy": false,
        "is_substantive": true
      }
    ],
    "arguments_substantive_count": 7,
    "fallacies": [],
    "fallacies_substantive_count": 0,
    "taxonomy_families_used": []
  },
  "formal_analysis": {
    "belief_sets": [
      {
        "id": "propositional_bs_1",
        "logic_type": "Propositional",
        "formula_count": 1,
        "is_consistent": "NOT_CHECKED"
      },
      {
        "id": "propositional_bs_2",
        "logic_type": "Propositional",
        "formula_count": 1,
        "is_consistent": "NOT_CHECKED"
      },
      {
        "id": "propositional_bs_3",
        "logic_type": "Propositional",
        "formula_count": 1,
        "is_consistent": "NOT_CHECKED"
      }
    ],
    "query_results": [
      {
        "log_id": "qlog_1",
        "belief_set_id": "propositional_bs_2",
        "query": "SAT((¬N→(K∨G)) ∧ (R∨V) ∧ AN) ?",
        "status": "UNKNOWN"
      },
      {
        "log_id": "qlog_2",
        "belief_set_id": "propositional_bs_2",
        "query": "(¬N→(K∨G)) ∧ (R∨V) ∧ AN ⊨ R ?",
        "status": "UNKNOWN"
      },
      {
        "log_id": "qlog_3",
        "belief_set_id": "propositional_bs_2",
        "query": "(¬N→(K∨G)) ∧ (R∨V) ∧ AN ⊨ ¬R→V ?",
        "status": "UNKNOWN"
      },
      {
        "log_id": "qlog_4",
        "belief_set_id": "propositional_bs_1",
        "query": "D ⊨ F et L ⊨ M, sans accepter les ponts D→F et L→M ?",
        "status": "UNKNOWN"
      },
      {
        "log_id": "qlog_5",
        "belief_set_id": "propositional_bs_3",
        "query": "BS3 (affirmations explicites sans ponts non établis) ⊨ F ?",
        "status": "UNKNOWN"
      },
      {
        "log_id": "qlog_6",
        "belief_set_id": "propositional_bs_3",
        "query": "BS3 (affirmations explicites sans ponts non établis) ⊨ Y ∨ M ?",
        "status": "UNKNOWN"
      }
    ],
    "queries_meaningful_count": 0,
    "consistency_checked": false
  },
  "cross_validation": {
    "validation_status": "PARTIAL_VALIDATED",
    "confidence_score": 0.71,
    "checks_passed": [
      "ARGUMENTS_IDENTIFIED",
      "FALLACY_ANALYSIS_ATTEMPTED",
      "BELIEF_SET_CREATED",
      "QUERIES_SUBMITTED",
      "CONCLUSION_GENERATED"
    ],
    "issues": [
      "Aucune requete PL n'a produit de verdict (ACCEPTED/REJECTED)."
    ],
    "failed_reason": "QUERIES_NOT_MEANINGFUL"
  },
  "conclusion": {
    "summary": "Synthèse finale — Le texte présente des raisons pertinentes de développer les renouvelables (réduction des émissions, diversification du mix, emplois) et des préoccupations recevables quant au coût, à la continuité d'approvisionnement, au rôle du nucléaire et à l'empreinte de fabrication des équipements. Ses points forts sont la prise en compte de plusieurs critères et sa dernière invitation à examiner les compromis. Ses faiblesses tiennent à des généralisations et à des inférences non justifiées : le cas allemand ne prouve pas à lui seul la faisabilité d'un objectif identique en France; les chiffres et prévisions attribués à l'ADEME, la part du nucléaire et les données sur l'éolien demandent vérification et contexte; un facteur de charge supposé de 25 % ne prouverait pas une inefficacité générale; une production chinoise carbonée n'annule pas nécessairement tout bénéfice climatique du solaire. Le texte présente abusivement l'opposition à une stratégie comme une opposition à l'emploi, caricature les écologistes, suppose sans démonstration que l'absence du nucléaire impose uniquement coupures ou gaz russe et oppose le 100 % renouvelable en 2040 à une planète inhabitable. L'avis d'un ingénieur et une pétition scientifique sont des éléments à examiner, non des preuves définitives de conclusions techniques précises; les accusations envers les climatosceptiques ne dispensent pas d'évaluer leurs arguments. Les formalisation et tests manuels enregistrés montrent que certaines prémisses du discours sont logiquement compatibles, mais qu'elles ne suffisent pas à démontrer plusieurs conclusions revendiquées; ils ne constituent aucune validation empirique. Aucune vérification indépendante des données ni analyse exhaustive des sophismes n'est enregistrée dans l'état. Conclusion : une décision argumentée doit confronter des scénarios chiffrés et sourcés sur les émissions de cycle de vie, les coûts, la fiabilité du réseau, les besoins de flexibilité, l'emploi et l'indépendance énergétique, sans présumer qu'un choix unique ou l'abandon du débat s'impose.",
    "is_complete": true
  }
}

--- VERDICT FINAL ---
[PARTIEL] Analyse partielle : certains controles de contenu ont echoue.

Lecture du rapport : ce que le validateur mesure, et pourquoi PARTIAL_VALIDATED

Le validateur de la cellule précédente ne se contente pas de vérifier que les champs JSON sont présents : il contrôle leur contenu. Trois critères font la différence entre un argument « substantif » et un remplissage :

  1. Un argument substantif cite le texte. La formulation détectée doit recouper le texte source sur au moins 12 caractères — un argument qui ne le fait pas est presque toujours une clé de l’état interne de l’agent (analysis_state, arguments…) prise à tort pour un contenu. Sur ce run : 9 arguments identifiés, 7 substantifs — les sept reprennent des passages explicites du texte (objectif de réduction d’émissions, part allemande des renouvelables, emplois attribués à l’ADEME).
  2. Un sophisme substantif porte un type, une justification et une cible. Un type « Type Inconnu » ou une justification manquante ne compte pas : ce serait compter des détections de forme, pas des détections d’analyse. Ici : 0 sophisme substantif — voir ci-dessous pourquoi ce n’est pas une panne.
  3. Une requête PL ne compte qu’avec un verdict. Soumettre une requête au moteur Prolog (QUERIES_SUBMITTED) et obtenir une réponse du moteur (QUERIES_MEANINGFUL, statut ACCEPTED ou REJECTED) sont deux contrôles séparés : une requête exécutée qui échoue sans verdict n’est pas une preuve de cohérence.

Pourquoi 71 % (5 contrôles sur 7) et pas 100 % ? Les contrôles ratés ce run sont les sophismes et les requêtes significatives. La cause est structurelle, pas aléatoire : la phase informelle (arguments + sophismes) est plafonnée à la moitié des tours du groupe dans AnalysisRunner, et l’agent y a consacré son budget aux arguments. Pour obtenir des requêtes PL significatives, il faut que la phase formelle reçoive un belief set déjà peuplé d’arguments substantifs — c’est l’aval du même goulot. Un texte plus court (2-3 arguments clairs) ou un budget de tours plus élevé fait typiquement passer le statut à COMPLETE_VALIDATED.

Ce que le statut ne dit pas : PARTIAL_VALIDATED n’est pas un échec du pipeline — les composants qui tournent, tournent sur du contenu vérifié. C’est un diagnostic de couverture : quelle chaîne de l’analyse (informelle → formelle → conclusion) a été effectivement parcourue dans ce run.

Interprétation du rapport de validation

Statuts de validation possibles:

Statut Score Critères
COMPLETE_VALIDATED ≥80% Tous les composants critiques présents
PARTIAL_VALIDATED ≥50% Analyse fonctionnelle mais incomplète
MINIMAL ≥30% Quelques éléments détectés
INCOMPLETE <30% Échec de l’analyse

Critères de validation:

  1. ARGUMENTS_IDENTIFIED: Au moins un argument détecté
  2. FALLACIES_ANALYZED: Au moins un sophisme identifié
  3. BELIEF_SET_CREATED: Au moins un Belief Set PL généré
  4. QUERIES_EXECUTED: Au moins une requête logique lancée
  5. QUERIES_MEANINGFUL: Résultat ACCEPTED ou REJECTED obtenu
  6. CONCLUSION_GENERATED: Conclusion finale présente

Exemple d’analyse réussie:

{
  "cross_validation": {
    "validation_status": "COMPLETE_VALIDATED",
    "confidence_score": 0.83,
    "checks_passed": [
      "ARGUMENTS_IDENTIFIED",
      "FALLACIES_ANALYZED",
      "BELIEF_SET_CREATED",
      "QUERIES_EXECUTED",
      "CONCLUSION_GENERATED"
    ],
    "issues": []
  }
}

Utilité du rapport JSON: Le fichier output/analysis_report.json peut être utilisé pour: - Tests automatisés (CI/CD) - Métriques de qualité d’analyse - Comparaison entre versions du système - Documentation des résultats d’analyse

Exercice 3 : Mapper un score de confiance a un statut

Le rapport de validation attribue des scores de confiance aux analyses. Convertissez ces scores en labels comprehensibles.

Objectif : Implementer confidence_to_status.

Indices : - Étape 1 : Définir les seuils : >0.8 = VALIDATED, 0.5-0.8 = UNCERTAIN, <0.5 = LOW - Étape 2 : Comparer le score aux seuils avec des if/elif/else - Étape 3 : Retourner le label correspondant - Étape 4 : Gerer les cas extremes (score < 0 ou > 1)

# Exercice 3 : Mapper un score de confiance a un statut
# TODO etudiant: ecrivez une fonction qui classifie un score de confiance
# en categories: VALIDATED (>0.8), UNCERTAIN (0.5-0.8), LOW (<0.5).
# Etape 1: Definir les seuils
# Etape 2: Comparer le score aux seuils
# Etape 3: Retourner le label correspondant
# Etape 4: Gerer les valeurs hors plage
def confidence_to_status(score):
    return "UNKNOWN"  # TODO etudiant

for s in [0.95, 0.65, 0.3]:
    print(f"Score {s}: {confidence_to_status(s)}")
print("Exercice a completer")
Score 0.95: UNKNOWN
Score 0.65: UNKNOWN
Score 0.3: UNKNOWN
Exercice a completer

6. Résultats et Conclusion

Vérifiez les logs et l’état final JSON affichés par l’exécution précédente pour voir le résultat de l’analyse collaborative.

Récapitulatif du workflow complet

Le notebook orchestrateur Argumentation-08b-Executor-Python.ipynb implémente un pipeline d’analyse rhétorique en 7 étapes:

Étape Action Durée typique
1. Configuration Chargement .env, variables d’environnement <1s
2. Interface/Batch Configuration texte (UI ou batch) Variable (0-60s)
3. Chargement agents Import des notebooks modulaires 5-10s
4. Analyse informelle Détection arguments + sophismes 40-60s
5. Formalisation PL Création Belief Sets + requêtes 30-50s
6. Validation Génération rapport JSON structuré <1s
7. Export Sauvegarde output/analysis_report.json <1s

Total (mode batch): ~120-150 secondes

Méthodologie d’analyse rhétorique

Le système combine deux approches complémentaires:

Analyse informelle (InformalAnalysisAgent): - Utilise un LLM (GPT-4/Claude) pour comprendre le langage naturel - Identifie arguments, prémisses, conclusions - Détecte sophismes via taxonomie hiérarchique - Output: Structure JSON décrivant la rhétorique

Analyse formelle (PropositionalLogicAgent): - Traduit les arguments en logique propositionnelle - Crée des Belief Sets Tweety (bibliothèque Java) - Exécute requêtes de cohérence et d’implication - Output: Preuves formelles ou contre-exemples

Avantage de l’approche hybride: La combinaison LLM + logique formelle permet à la fois la compréhension contextuelle (LLM) et la rigueur mathématique (logique).

7. Pistes d’Amélioration Futures

Améliorations techniques

Catégorie Amélioration proposée Impact
Logique formelle Implémenter FOL, logique modale (Tweety) Expressivité accrue
Taxonomie sophismes Navigation profondeur variable, exemples Précision détection
Gestion erreurs Retry automatique, fallback gracieux Robustesse production
Performance Cache LLM, parallélisation agents Réduction latence
État RDF/KG Migration vers graphe sémantique (rdflib) Requêtes complexes

Améliorations pédagogiques

Visualisations interactives: - Graphe d’arguments (NetworkX + Plotly) - Arbre taxonomique des sophismes - Timeline de l’analyse (Gantt chart)

Explications enrichies: - Définitions des sophismes avec exemples historiques - Comparaison analyse informelle vs formelle - Métriques de confiance par argument

Interface utilisateur: - Dashboard Gradio/Streamlit pour upload de textes - Annotation collaborative des arguments - Export PDF avec rapport structuré

Cas d’usage avancés

Analyse de débats politiques: - Détection de manipulation rhétorique - Comparaison positions de candidats - Fact-checking automatisé (intégration sources externes)

Éducation à la pensée critique: - Exercices interactifs sur textes annotés - Quiz génératif sur détection de sophismes - Feedback personnalisé sur essais d’étudiants

Recherche académique: - Corpus analysis (analyse de milliers de textes) - Évolution diachronique de l’argumentation - Comparaison cultures/langues (multilingual)

Roadmap prioritaire: 1. Finaliser intégration Tweety (Query exécution) 2. Implémenter visualisations graphiques 3. Ajouter interface Gradio 4. Publier package Python réutilisable

Retour au sommet