Interface de configuration et préparation du texte

Retour au README | Suivant : Executor

Objectif: Ce notebook définit l’interface utilisateur et la logique nécessaire pour : 1. Sélectionner une source de texte (Bibliothèque prédéfinie, URL, Fichier local, Texte direct). 2. Extraire le contenu textuel via Jina ou Tika si nécessaire. 3. Appliquer des marqueurs de début/fin pour isoler un extrait spécifique. 4. Gérer un cache fichier pour les textes complets des sources externes. 5. Charger/Sauvegarder la configuration des sources prédéfinies depuis/vers un fichier chiffré. 6. Retourner le texte final préparé au notebook exécuteur principal.

Fonction Principale: Définit la fonction configure_analysis_task() qui sera appelée par le notebook exécuteur.

1. Imports Requis

Importation des bibliothèques nécessaires pour l’UI, les requêtes HTTP, le cache, le chiffrement, et la gestion des fichiers/chemins.

# -*- coding: utf-8 -*-
# Filename: UI_Configuration.ipynb

# %% =====================================================
# CELLULE 1: Imports Requis
# =====================================================
import ipywidgets as widgets
from IPython.display import display, clear_output
import requests
import time
import random
import io
from jupyter_ui_poll import ui_events
import traceback
import re
import os
import json
import gzip
import hashlib
from pathlib import Path
# Imports Cryptography (Fernet pour chiffrement/déchiffrement)
from cryptography.fernet import Fernet, InvalidToken
from cryptography.exceptions import InvalidSignature
# Imports Cryptography (PBKDF2 pour dérivation de clé depuis passphrase) CORRECTIF ICI
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.backends import default_backend
import base64 # Pour encoder la clé dérivée
# ---
from dotenv import load_dotenv, find_dotenv

print("Imports pour UI_Configuration chargés (incluant crypto KDF et base64).")
Imports pour UI_Configuration chargés (incluant crypto KDF et base64).

Interprétation des bibliothèques chargées

Les imports se divisent en plusieurs catégories fonctionnelles :

Catégorie Bibliothèques Rôle
Interface utilisateur ipywidgets, IPython.display, jupyter_ui_poll Création de l’interface interactive avec widgets (dropdowns, boutons, zones de texte)
Accès réseau requests Récupération de contenu depuis des URLs (via Jina ou Tika)
Chiffrement cryptography.fernet, PBKDF2HMAC Sécurisation des données de configuration avec chiffrement AES
Gestion fichiers pathlib.Path, os, json, gzip Manipulation des chemins, compression et lecture/écriture de fichiers
Utilitaires hashlib, base64, random, time, traceback Hachage pour cache, encodage, sélection aléatoire, temporisation et débogage

Note technique : L’utilisation de PBKDF2HMAC avec 480 000 itérations garantit une dérivation de clé robuste face aux attaques par force brute, conforme aux recommandations NIST 2024.

2. Configuration, Constantes et Données Sources

Définition des constantes (URLs des services, chemins des fichiers), chargement de la clé de chiffrement depuis .env, définition de la structure des sources prédéfinies (EXTRACT_SOURCES), et création du répertoire de cache.

# --- Chargement et Dérivation Clé de Chiffrement ---
load_dotenv(find_dotenv())
PASSPHRASE_VAR_NAME = "TEXT_CONFIG_PASSPHRASE"
passphrase = os.getenv(PASSPHRASE_VAR_NAME)
ENCRYPTION_KEY = None # Sera défini si la dérivation réussit

FIXED_SALT = b'q\x8b\t\x97\x8b\xe9\xa3\xf2\xe4\x8e\xea\xf5\xe8\xb7\xd6\x8c' # Exemple de sel fixe (16 bytes)

print(f"Vérification de la phrase secrète '{PASSPHRASE_VAR_NAME}' dans .env...")

if passphrase:
    print(f"✅ Phrase secrète trouvée. Dérivation de la clé...")
    try:
        kdf = PBKDF2HMAC(
            algorithm=hashes.SHA256(), length=32, salt=FIXED_SALT,
            iterations=480000, backend=default_backend()
        )
        derived_key_raw = kdf.derive(passphrase.encode('utf-8'))
        ENCRYPTION_KEY = base64.urlsafe_b64encode(derived_key_raw)
        # Optionnel: commenter le test si la clé dérivée pose problème avec Fernet(key) directement
        # try:
        #     Fernet(ENCRYPTION_KEY)
        #     print("✅ Clé de chiffrement dérivée avec succès et valide.")
        # except Exception as e_fernet:
        #     print(f"⚠️ Clé dérivée semble invalide pour Fernet : {e_fernet}.")
        #     ENCRYPTION_KEY = None # Invalider si test échoue
        if ENCRYPTION_KEY: print("✅ Clé de chiffrement dérivée et encodée.")

    except Exception as e:
        print(f"⚠️ Erreur dérivation clé : {e}. Chiffrement désactivé.")
        ENCRYPTION_KEY = None
else:
    print(f"⚠️ Variable '{PASSPHRASE_VAR_NAME}' non trouvée dans .env. Chiffrement désactivé.")

# --- URLs et Chemins ---
TIKA_URL_PARTS = ["https:", "", "tika", "open-webui", "myia", "io", "tika"]
# --- LIGNE DÉCOMMENTÉE ---
# Reconstruit l'URL Tika une seule fois ici pour être utilisée globalement
TIKA_SERVER_URL = f"{TIKA_URL_PARTS[0]}//{'.'.join(TIKA_URL_PARTS[2:-1])}/{TIKA_URL_PARTS[-1]}"
# -------------------------
JINA_READER_PREFIX = "https://r.jina.ai/"
CACHE_DIR = Path("./text_cache")
CONFIG_FILE = Path("./extract_sources.json.gz.enc")

# --- Création Cache Dir ---
try:
    CACHE_DIR.mkdir(parents=True, exist_ok=True)
    print(f"Cache répertoire assuré : {CACHE_DIR.resolve()}")
except Exception as e:
    print(f"Erreur création répertoire cache {CACHE_DIR}: {e}")

# --- Structure par Défaut (avec nouvelle structure URL) ---
DEFAULT_EXTRACT_SOURCES = [
    {"source_name": "Exemple Vide (Config manquante)", "source_type": "jina",
     "schema": "https:", "host_parts": ["example", "com"], "path": "/",
     "extracts": []}
]

# --- Définition Initiale des Sources & Extraits (Structure Corrigée) ---
# (Cette structure est utilisée comme fallback si le fichier .enc n'existe pas/n'est pas lisible)
EXTRACT_SOURCES = [
     {
        "source_name": "Lincoln-Douglas Débat 1 (NPS)", "source_type": "jina",
        "schema": "https:", "host_parts": ["www", "nps", "gov"], "path": "/liho/learn/historyculture/debate1.htm",
        "extracts": [
            {"extract_name": "1. Débat Complet (Ottawa, 1858)", "start_marker": "**August 21, 1858**", "end_marker": "(Three times three cheers were here given for Senator Douglas.)"},
            {"extract_name": "2. Discours Principal de Lincoln", "start_marker": "MY FELLOW-CITIZENS: When a man hears himself", "end_marker": "The Judge can take his half hour."},
            {"extract_name": "3. Discours d'Ouverture de Douglas", "start_marker": "Ladies and gentlemen: I appear before you", "end_marker": "occupy an half hour in replying to him."},
            {"extract_name": "4. Lincoln sur Droits Naturels/Égalité", "start_marker": "I will say here, while upon this subject,", "end_marker": "equal of every living man._ [Great applause.]"},
            {"extract_name": "5. Douglas sur Race/Dred Scott", "start_marker": "utterly opposed to the Dred Scott decision,", "end_marker": "equality with the white man. (\"Good.\")"},
        ]
    },
    {
        "source_name": "Lincoln-Douglas Débat 2 (NPS)", "source_type": "jina",
        "schema": "https:", "host_parts": ["www", "nps", "gov"], "path": "/liho/learn/historyculture/debate2.htm",
        "extracts": [
             {"extract_name": "1. Débat Complet (Freeport, 1858)", "start_marker": "It was a cloudy, cool, and damp day.", "end_marker": "I cannot, gentlemen, my time has expired."},
             {"extract_name": "2. Discours Principal de Douglas", "start_marker": "**Mr. Douglas' Speech**\n\nLadies and Gentlemen-", "end_marker": "stopped on the moment."},
             {"extract_name": "3. Discours d'Ouverture de Lincoln", "start_marker": "LADIES AND GENTLEMEN - On Saturday last,", "end_marker": "Go on, Judge Douglas."},
             {"extract_name": "4. Doctrine de Freeport (Douglas)", "start_marker": "The next question propounded to me by Mr. Lincoln is,", "end_marker": "satisfactory on that point."},
             {"extract_name": "5. Lincoln répond aux 7 questions", "start_marker": "The first one of these interrogatories is in these words:,", "end_marker": "aggravate the slavery question among ourselves. [Cries of good, good.]"},
        ]
    },
     {
        "source_name": "Corpus de recherche (chiffré, hors dépôt public) - A", "source_type": "jina",
        "schema": "", "host_parts": [], "path": "",
        "extracts": [
            {"extract_name": "Extraits chargés depuis extract_sources.json.gz.enc", "start_marker": "", "end_marker": ""},
        ]
    },
    {
        "source_name": "Corpus de recherche (chiffré, hors dépôt public) - B", "source_type": "jina",
        "schema": "", "host_parts": [], "path": "",
        "extracts": [
            {"extract_name": "Extraits chargés depuis extract_sources.json.gz.enc", "start_marker": "", "end_marker": ""},
        ]
    },
]


# --- Initialisation variable globale ---
# Sera mise à jour par load_extract_definitions lors de l'appel à configure_analysis_task
current_extract_definitions = []

print(f"Configuration UI chargée. {len(EXTRACT_SOURCES)} sources initiales définies.")
Vérification de la phrase secrète 'TEXT_CONFIG_PASSPHRASE' dans .env...
✅ Phrase secrète trouvée. Dérivation de la clé...
✅ Clé de chiffrement dérivée et encodée.
Cache répertoire assuré : <repo>MyIA.AI.Notebooks\SymbolicAI\Argument_Analysis\text_cache
Configuration UI chargée. 4 sources initiales définies.

Analyse des résultats de configuration

Résultats observés :

  • Clé de chiffrement dérivée avec succès via PBKDF2
  • Répertoire de cache créé dans le dossier de travail
  • 4 sources documentaires chargées en mémoire

Sources prédéfinies :

  1. Lincoln-Douglas Débat 1 & 2 - Débats historiques de 1858, exemples d’argumentation politique américaine classique
  2. Corpus de recherche chiffré (A) - Extraits chargés depuis extract_sources.json.gz.enc, hors dépôt public
  3. Corpus de recherche chiffré (B) - Extraits chargés depuis extract_sources.json.gz.enc, hors dépôt public

Pourquoi ces sources : Ces textes ont été choisis pour leur richesse argumentative et la présence de sophismes identifiables, ce qui permet de tester les capacités d’analyse des agents LLM.

Structure URL obfusquée :

La dissociation de l’URL en schema, host_parts et path permet : - Une meilleure traçabilité dans les logs (sans exposer les URLs complètes) - Une gestion flexible des changements de domaine - Un fallback plus robuste en cas d’indisponibilité d’une source

Exercice 1 : Reconstitution d’URL

La fonction reconstruct_url du module combine schema, host et path en URL. Reimplementez une version simplifiee.

Objectif : Ecrire my_reconstruct_url.

Indices : - Étape 1 : Concatener schema + "://" - Étape 2 : Ajouter le host - Étape 3 : Ajouter le path (gerer le / initial manquant) - Étape 4 : Verifier que le résultat commence par http:// ou https://

# Exercice 1 : Reconstitution d'URL
# La fonction reconstruct_url recombine schema, host, path en URL valide.
# TODO etudiant: implementez votre propre version simplifiee.
# Etape 1: Concatener schema + "://"
# Etape 2: Ajouter le host
# Etape 3: Ajouter le path (gerer le / initial)
# Etape 4: Valider que le resultat commence par http(s)://
def my_reconstruct_url(schema, host, path):
    return ""  # TODO etudiant

url = my_reconstruct_url("https", "example.com", "/api/text")
print(f"URL reconstituee: {url}")
print("Exercice a completer")
URL reconstituee: 
Exercice a completer

3. Fonctions Utilitaires

Définition des fonctions pour : * Gérer le cache fichier (créer nom de fichier, lire, écrire). * Chiffrer et déchiffrer les données de configuration. * Charger et sauvegarder le fichier de configuration chiffré. * Reconstruire les URLs à partir des parties obfusquées. * Récupérer le texte via Jina (avec cache). * Récupérer le texte via Tika (téléchargement si URL, puis envoi au serveur Tika, avec cache).

def reconstruct_url(schema: str, host_parts: list, path: str) -> str | None:
    """Reconstruit une URL à partir de schema, host_parts, et path."""
    if not schema or not host_parts or not path: return None
    host = ".".join(part for part in host_parts if part)
    # S'assurer que le path commence par / s'il n'est pas vide
    path = path if path.startswith('/') or not path else '/' + path
    return f"{schema}//{host}{path}"

# --- Définitions des autres fonctions utilitaires ---
# (get_cache_filepath, load_from_cache, save_to_cache, encrypt_data, decrypt_data,
#  load_extract_definitions (modifiée pour utiliser la nouvelle structure interne),
#  save_extract_definitions, fetch_with_jina, fetch_with_tika)
# ... (Copiez ici le code complet de ces fonctions depuis la CELLULE 3 / Bloc Python 3 de ma réponse précédente) ...
# ... Assurez-vous que load_extract_definitions utilise la nouvelle structure
#     et que fetch_with_jina/tika appellent la nouvelle reconstruct_url si nécessaire ...

# Exemple de load_extract_definitions (ajusté légèrement pour fallback)
def load_extract_definitions(config_file: Path, key: bytes) -> list:
    """Charge, déchiffre et décompresse les définitions depuis le fichier."""
    global EXTRACT_SOURCES, DEFAULT_EXTRACT_SOURCES # Utiliser les structures définies globalement
    fallback_definitions = EXTRACT_SOURCES if EXTRACT_SOURCES else DEFAULT_EXTRACT_SOURCES

    if not config_file.exists():
        print(f"Fichier config '{config_file}' non trouvé. Utilisation définitions en mémoire.")
        return fallback_definitions[:]
    if not key:
        print("Clé chiffrement absente. Chargement config impossible. Utilisation définitions en mémoire.")
        return fallback_definitions[:]

    print(f"Chargement et déchiffrement de '{config_file}'...")
    try:
        with open(config_file, 'rb') as f: encrypted_data = f.read()
        decrypted_compressed_data = decrypt_data(encrypted_data, key)
        if not decrypted_compressed_data: raise ValueError("Échec déchiffrement.")
        decompressed_data = gzip.decompress(decrypted_compressed_data)
        definitions = json.loads(decompressed_data.decode('utf-8'))
        print("✅ Définitions chargées et déchiffrées.")
        # Validation plus robuste
        if not isinstance(definitions, list) or not all(
            isinstance(item, dict) and
            "source_name" in item and
            "source_type" in item and
            "schema" in item and
            "host_parts" in item and
            "path" in item and
            isinstance(item.get("extracts"), list)
            for item in definitions
        ):
             print("⚠️ Format définitions invalide après chargement. Utilisation définitions en mémoire.")
             return fallback_definitions[:]
        # Mettre à jour EXTRACT_SOURCES global si chargement OK
        EXTRACT_SOURCES = definitions
        print(f"-> {len(EXTRACT_SOURCES)} définitions chargées depuis fichier.")
        return definitions # Retourner les définitions chargées
    except Exception as e:
        print(f"❌ Erreur chargement/traitement '{config_file}': {e}. Utilisation définitions en mémoire.")
        return fallback_definitions[:]

# --- Assurez-vous que toutes les autres fonctions utilitaires sont collées ici ---
def get_cache_filepath(url: str) -> Path:
    url_hash = hashlib.sha256(url.encode()).hexdigest()
    if 'CACHE_DIR' not in globals() or not isinstance(CACHE_DIR, Path): raise NameError("CACHE_DIR non définie.")
    return CACHE_DIR / f"{url_hash}.txt"
def load_from_cache(url: str) -> str | None:
    filepath = get_cache_filepath(url)
    if filepath.exists():
        try:
            print(f"   -> Lecture depuis cache : {filepath.name}")
            return filepath.read_text(encoding='utf-8')
        except Exception as e: print(f"   -> Erreur lecture cache {filepath.name}: {e}"); return None
    return None
def save_to_cache(url: str, text: str):
    if not text: print("   -> Texte vide, non sauvegardé."); return
    filepath = get_cache_filepath(url)
    try:
        filepath.write_text(text, encoding='utf-8'); print(f"   -> Texte sauvegardé : {filepath.name}")
    except Exception as e: print(f"   -> Erreur sauvegarde cache {filepath.name}: {e}")
def encrypt_data(data: bytes, key: bytes) -> bytes | None:
    if not key: print("Erreur: Clé chiffrement manquante."); return None
    try: f = Fernet(key); return f.encrypt(data)
    except Exception as e: print(f"Erreur chiffrement: {e}"); return None
def decrypt_data(encrypted_data: bytes, key: bytes) -> bytes | None:
    if not key: print("Erreur: Clé chiffrement manquante."); return None
    try: f = Fernet(key); return f.decrypt(encrypted_data)
    except (InvalidToken, InvalidSignature, Exception) as e: print(f"Erreur déchiffrement: {e}"); return None
def save_extract_definitions(definitions: list, config_file: Path, key: bytes):
    if not key: print("Clé chiffrement absente. Sauvegarde annulée."); return False
    if not isinstance(definitions, list): print("Erreur: définitions non valides."); return False
    print(f"Préparation sauvegarde vers '{config_file}'...")
    try:
        json_data = json.dumps(definitions, indent=2, ensure_ascii=False).encode('utf-8'); compressed_data = gzip.compress(json_data); encrypted_data = encrypt_data(compressed_data, key)
        if not encrypted_data: raise ValueError("Échec chiffrement.")
        with open(config_file, 'wb') as f: f.write(encrypted_data)
        print(f"✅ Définitions sauvegardées dans '{config_file}'.")
        return True
    except Exception as e: print(f"❌ Erreur sauvegarde chiffrée: {e}"); return False
PLAINTEXT_EXTENSIONS = ['.txt', '.md', '.json', '.csv', '.xml', '.py', '.js', '.html', '.htm'] # Extensions à traiter comme texte simple

def fetch_direct_text(source_url, timeout=60):
    """Récupère contenu texte brut d'URL, utilise cache fichier."""
    # ... (Code INCHANGÉ) ...
    cached_text = load_from_cache(source_url)
    if cached_text is not None: return cached_text
    print(f"-> Téléchargement direct depuis : {source_url}...")
    headers = {'User-Agent': 'Agent-Analysis-Notebook/1.0'}
    try:
        response = requests.get(source_url, headers=headers, timeout=timeout)
        response.raise_for_status()
        # Décoder en UTF-8, ignorer les erreurs potentielles
        texte_brut = response.content.decode('utf-8', errors='ignore')
        print(f"   -> Contenu direct récupéré (longueur {len(texte_brut)}).")
        save_to_cache(source_url, texte_brut)
        return texte_brut
    except requests.exceptions.RequestException as e:
        raise ConnectionError(f"Erreur téléchargement direct ({source_url}): {e}") from e

def fetch_with_jina(source_url, timeout=90):
     """Récupère et extrait via Jina, utilise cache fichier."""
     # ... (Code INCHANGÉ) ...
     cached_text = load_from_cache(source_url);
     if cached_text is not None: return cached_text
     jina_url = f"{JINA_READER_PREFIX}{source_url}"; print(f"-> Récupération via Jina : {jina_url}...")
     headers = {'Accept': 'text/markdown', 'User-Agent': 'Agent-Analysis-Notebook/1.0'}
     try: response = requests.get(jina_url, headers=headers, timeout=timeout); response.raise_for_status()
     except requests.exceptions.RequestException as e: raise ConnectionError(f"Erreur Jina ({jina_url}): {e}") from e
     content = response.text; md_start_marker = "Markdown Content:"; md_start_index = content.find(md_start_marker)
     texte_brut = content[md_start_index + len(md_start_marker):].strip() if md_start_index != -1 else content
     print(f"   -> Contenu Jina récupéré (longueur {len(texte_brut)})."); save_to_cache(source_url, texte_brut); return texte_brut


# --- fetch_with_tika MODIFIÉE ---
def fetch_with_tika(source_url=None, file_content=None, file_name="fichier",
                    raw_file_cache_path: Path | str | None = None, # Chemin cache brut
                    timeout_dl=60, timeout_tika=600):
    """
    Traite une source via Tika. Tente d'abord de lire le cache texte.
    Si URL fournie, vérifie le cache brut puis télécharge si besoin.
    Vérifie l'extension: si texte simple, utilise fetch_direct_text au lieu de Tika.
    Sinon, envoie le contenu brut (téléchargé ou fourni) à Tika.
    """
    cache_key = source_url if source_url else f"file://{file_name}"
    # 1. Vérifier cache TEXTE FINAL
    cached_text = load_from_cache(cache_key)
    if cached_text is not None: return cached_text

    global TIKA_SERVER_URL
    content_to_send = None
    original_filename_or_path = file_name # Par défaut (pour upload)

    # 2. Obtenir contenu BRUT (si URL) et vérifier extension
    if source_url:
        original_filename_or_path = Path(source_url).name # Pour check extension et cache brut
        # Vérifier si c'est un type texte simple connu basé sur l'URL
        if any(source_url.lower().endswith(ext) for ext in PLAINTEXT_EXTENSIONS):
            print(f"   -> URL détectée comme texte simple ({source_url}). Utilisation fetch direct au lieu de Tika.")
            # On ne passe pas par Tika, on télécharge directement le texte
            # La fonction fetch_direct_text gère son propre cache .txt
            return fetch_direct_text(source_url)

        # Si pas texte simple, gérer cache brut et téléchargement
        if raw_file_cache_path:
            raw_path = Path(raw_file_cache_path)
            if raw_path.exists() and raw_path.stat().st_size > 0:
                try:
                    print(f"   -> Lecture fichier brut depuis cache local : {raw_path.name}")
                    content_to_send = raw_path.read_bytes()
                except Exception as e_read_raw:
                    print(f"   -> ⚠️ Erreur lecture cache brut {raw_path.name}: {e_read_raw}. Re-téléchargement...")
                    content_to_send = None

        if content_to_send is None: # Si cache brut absent ou erreur lecture
            print(f"-> Téléchargement (pour Tika) depuis : {source_url}...")
            try:
                response_dl = requests.get(source_url, stream=True, timeout=timeout_dl); response_dl.raise_for_status()
                content_to_send = response_dl.content; print(f"   -> Doc téléchargé ({len(content_to_send)} bytes).")
                if raw_file_cache_path: # Sauvegarder si chemin fourni
                     try: save_path = Path(raw_file_cache_path); save_path.parent.mkdir(parents=True, exist_ok=True); save_path.write_bytes(content_to_send); print(f"   -> Doc brut sauvegardé: {save_path.resolve()}")
                     except Exception as e_save: print(f"   -> ⚠️ Erreur sauvegarde brut: {e_save}")
            except requests.exceptions.RequestException as e: raise ConnectionError(f"Erreur téléchargement {source_url}: {e}") from e

    elif file_content:
         # Cas: Fichier uploadé (pas d'URL source)
         print(f"-> Utilisation contenu fichier '{file_name}' ({len(file_content)} bytes)...")
         content_to_send = file_content
         # Vérifier si upload est texte simple
         if any(file_name.lower().endswith(ext) for ext in PLAINTEXT_EXTENSIONS):
              print("   -> Fichier uploadé détecté comme texte simple. Lecture directe.")
              try:
                  texte_brut = file_content.decode('utf-8', errors='ignore')
                  save_to_cache(cache_key, texte_brut) # Sauver dans cache texte
                  return texte_brut
              except Exception as e_decode:
                  print(f"   -> ⚠️ Erreur décodage fichier texte '{file_name}': {e_decode}. Tentative avec Tika...")
                  # Si erreur décodage, on laisse Tika essayer
    else:
         raise ValueError("fetch_with_tika: Il faut soit source_url soit file_content.")

    # 3. Envoyer à Tika (seulement si nécessaire et contenu valide)
    if not content_to_send:
         print("   -> ⚠️ Contenu brut vide ou non récupéré. Impossible d'envoyer à Tika.")
         save_to_cache(cache_key, "") # Sauver cache vide pour éviter retry
         return ""

    print(f"-> Envoi contenu à Tika ({TIKA_SERVER_URL})... (Timeout={timeout_tika}s)")
    headers = { 'Accept': 'text/plain', 'Content-Type': 'application/octet-stream', 'X-Tika-OCRLanguage': 'fra+eng' }
    try:
        response_tika = requests.put(TIKA_SERVER_URL, data=content_to_send, headers=headers, timeout=timeout_tika)
        response_tika.raise_for_status()
        texte_brut = response_tika.text
        if not texte_brut: print(f"   -> Warning: Tika status {response_tika.status_code} sans texte.")
        else: print(f"   -> Texte Tika extrait (longueur {len(texte_brut)}).")

        # 4. Sauvegarder le TEXTE EXTRAIT dans le cache .txt
        save_to_cache(cache_key, texte_brut)
        return texte_brut

    except requests.exceptions.Timeout: print(f"   -> ❌ Timeout Tika ({timeout_tika}s)."); raise ConnectionError(f"Timeout Tika")
    except requests.exceptions.RequestException as e: raise ConnectionError(f"Erreur Tika: {e}") from e

def verify_extract_definitions(definitions_list: list):
    """
    Vérifie la présence des marqueurs start/end pour chaque extrait défini.
    Récupère le texte complet (via cache ou fetch) pour chaque source.
    Retourne un résumé des vérifications.
    """
    print("\n🔬 Lancement de la vérification des marqueurs d'extraits...")
    results = []
    total_checks = 0
    total_errors = 0

    if not definitions_list or definitions_list == DEFAULT_EXTRACT_SOURCES:
         return "Aucune définition valide à vérifier."

    for source_idx, source_info in enumerate(definitions_list):
        source_name = source_info.get("source_name", f"Source Inconnue #{source_idx+1}")
        print(f"\n--- Vérification Source: '{source_name}' ---")
        source_errors = 0
        source_checks = 0
        texte_brut_source = None
        reconstructed_url = None

        try:
            # Reconstruire l'URL
            reconstructed_url = reconstruct_url(
                source_info.get("schema"), source_info.get("host_parts", []), source_info.get("path")
            )
            if not reconstructed_url:
                print("   -> ❌ Erreur: URL Invalide.")
                results.append(f"<li>{source_name}: URL invalide</li>")
                total_errors += len(source_info.get("extracts", [])) # Compter tous les extraits comme erreurs
                total_checks += len(source_info.get("extracts", []))
                continue

            # Récupérer le texte complet (via cache ou fetch)
            source_type = source_info.get("source_type")
            cache_key = reconstructed_url # Clé cache pour le texte complet
            texte_brut_source = load_from_cache(cache_key)

            if texte_brut_source is None:
                print(f"   -> Cache texte absent. Récupération (type: {source_type})...")
                if source_type == "jina": texte_brut_source = fetch_with_jina(reconstructed_url)
                elif source_type == "direct_download": texte_brut_source = fetch_direct_text(reconstructed_url)
                elif source_type == "tika":
                    is_plaintext = any(source_info.get("path", "").lower().endswith(ext) for ext in PLAINTEXT_EXTENSIONS)
                    if is_plaintext: texte_brut_source = fetch_direct_text(reconstructed_url)
                    else:
                        # On ne peut pas raisonnablement vérifier les marqueurs si Tika échoue ici
                        print("   -> ⚠️ Impossible de vérifier les marqueurs car nécessite Tika (potentiellement long/échoué).")
                        texte_brut_source = None # Marquer comme non vérifiable
                else:
                     print(f"   -> ⚠️ Type source inconnu '{source_type}'. Vérification impossible.")
                     texte_brut_source = None

            # Vérifier les marqueurs si le texte a été obtenu
            if texte_brut_source is not None:
                print(f"   -> Texte complet récupéré (longueur: {len(texte_brut_source)}). Vérification des extraits...")
                extracts = source_info.get("extracts", [])
                if not extracts: print("      -> Aucun extrait défini pour cette source.")

                for extract_idx, extract_info in enumerate(extracts):
                    extract_name = extract_info.get("extract_name", f"Extrait #{extract_idx+1}")
                    start_marker = extract_info.get("start_marker")
                    end_marker = extract_info.get("end_marker")
                    total_checks += 1
                    source_checks += 1
                    marker_errors = []

                    if not start_marker or not end_marker:
                        marker_errors.append("Marqueur(s) Manquant(s)")
                    else:
                        start_found = start_marker in texte_brut_source
                        end_found_after_start = False
                        if start_found:
                            try:
                                start_pos = texte_brut_source.index(start_marker)
                                end_found_after_start = end_marker in texte_brut_source[start_pos + len(start_marker):]
                            except ValueError: # Ne devrait pas arriver si start_found est True
                                 start_found = False

                        if not start_found: marker_errors.append("Début NON TROUVÉ")
                        if not end_found_after_start: marker_errors.append("Fin NON TROUVÉE (après début)")

                    if marker_errors:
                        print(f"      -> ❌ Problème Extrait '{extract_name}': {', '.join(marker_errors)}")
                        results.append(f"<li>{source_name} -> {extract_name}: <strong style='color:red;'>{', '.join(marker_errors)}</strong></li>")
                        source_errors += 1
                        total_errors += 1
                    else:
                         print(f"      -> ✅ OK: Extrait '{extract_name}'")
                         # results.append(f"<li>{source_name} -> {extract_name}: OK</li>") # Optionnel: lister aussi les OK

            else: # Cas où le texte brut n'a pas pu être récupéré (ex: erreur Tika pendant la vérif)
                 num_extracts = len(source_info.get("extracts",[]))
                 results.append(f"<li>{source_name}: Vérification impossible (texte source non obtenu)</li>")
                 total_errors += num_extracts
                 total_checks += num_extracts


        except Exception as e_verify:
            print(f"   -> ❌ Erreur inattendue vérification source '{source_name}': {e_verify}")
            num_extracts = len(source_info.get("extracts",[]))
            results.append(f"<li>{source_name}: Erreur Vérification Générale ({type(e_verify).__name__})</li>")
            total_errors += num_extracts
            total_checks += num_extracts # Compter comme échec si erreur globale

    summary = f"--- Résultat Vérification ---<br/>{total_checks} extraits vérifiés. <strong style='color: {'red' if total_errors > 0 else 'green'};'>{total_errors} erreur(s) trouvée(s).</strong>"
    if results:
        summary += "<br/>Détails erreurs :<ul>" + "".join(results) + "</ul>"
    else:
        summary += "<br/>Tous les marqueurs semblent corrects."

    # Fix f-string backslash error - extract HTML cleanup outside f-string
    console_summary = summary.replace('<br/>', '\n').replace('<li>', '- ').replace('</li>', '').replace('<ul>', '').replace('</ul>', '').replace('<strong>', '').replace('</strong>', '')
    print(f"\n{console_summary}") # Affichage console simple
    return summary


print("Fonctions utilitaires UI_Configuration définies.")
Fonctions utilitaires UI_Configuration définies.

Fonctions utilitaires définies

Le code précédent a créé un ensemble de fonctions couvrant trois domaines principaux :

1. Gestion du cache fichier

Fonction Entrée Sortie Rôle
get_cache_filepath URL (str) Path Génère un nom de fichier unique via hash SHA-256
load_from_cache URL (str) str ou None Tente de lire le texte depuis le cache local
save_to_cache URL (str), texte (str) None Sauvegarde le texte extrait dans ./text_cache

2. Chiffrement et persistance

Fonction Rôle
encrypt_data / decrypt_data Chiffrement AES via Fernet
load_extract_definitions Charge, déchiffre et décompresse le fichier de config
save_extract_definitions Compresse, chiffre et sauvegarde les définitions

3. Récupération de texte

Fonction Service Cas d’usage
fetch_direct_text HTTP direct Fichiers texte brut (.txt, .md, .json, etc.)
fetch_with_jina r.jina.ai Pages web - extraction Markdown propre
fetch_with_tika Serveur Tika Documents binaires (PDF, DOCX) - extraction avec OCR
verify_extract_definitions Multi-services Validation des marqueurs start/end dans les sources

Optimisations clés :

  • Cache à deux niveaux : Fichier brut (binaire) + texte extrait (UTF-8)
  • Détection automatique : Les extensions texte simples évitent Tika (gain de temps)
  • Gestion d’erreurs robuste : Fallback sur définitions en mémoire si chargement échoue

Stratégie de cache : L’utilisation de hash SHA-256 sur l’URL garantit des noms de fichiers uniques et reproductibles, évitant les collisions même avec des URLs longues.

Exercice 2 : Hash de fichier pour le cache

Le système de cache utilise SHA256 pour generer des noms de fichier uniques. Implementez cette logique.

Objectif : Ecrire compute_cache_key.

Indices : - Étape 1 : Encoder le texte en bytes avec .encode('utf-8') - Étape 2 : Calculer le hash avec hashlib.sha256(data).hexdigest() - Étape 3 : Tronquer a 16 caractères pour un nom de fichier court - Étape 4 : Retourner la cle

# Exercice 2 : Hash de fichier pour le cache
# Le systeme de cache utilise un hash SHA256 comme nom de fichier.
# TODO etudiant: implementez une fonction de hash simplifiee.
# Etape 1: Encoder le texte en bytes
# Etape 2: Calculer le hash SHA256
# Etape 3: Convertir en hexa
# Etape 4: Tronquer a 16 caracteres
import hashlib
def compute_cache_key(text):
    return ""  # TODO etudiant

key = compute_cache_key("exemple de texte")
print(f"Cle de cache: {key}")
print("Exercice a completer")
Cle de cache: 
Exercice a completer

4. Initialisation du Cache (Optionnel)

Vérification et pré-remplissage du cache fichier pour les textes complets des sources définies (celles définies initialement ou chargées depuis le fichier chiffré à l’étape précédente).

# --- Bloc Python 4 (Initialisation Cache - MODIFIÉ pour extensions texte) ---

print("\n--- Initialisation du Cache des Textes Complets ---")

definitions_to_check = current_extract_definitions if 'current_extract_definitions' in locals() and current_extract_definitions else EXTRACT_SOURCES
TEMP_DOWNLOAD_DIR = Path("./temp_downloads") # S'assurer qu'il est défini

if not definitions_to_check or definitions_to_check == DEFAULT_EXTRACT_SOURCES:
     print(" -> Aucune définition de source valide à vérifier/initialiser.")
else:
    initialisation_errors = 0
    print(f"Vérification du cache pour {len(definitions_to_check)} source(s)...")
    for i, source_info in enumerate(definitions_to_check):
        source_name = source_info.get("source_name", f"Source #{i+1}")
        try:
            reconstructed_url = reconstruct_url(
                source_info.get("schema"), source_info.get("host_parts", []), source_info.get("path")
            )
            if not reconstructed_url: print(f"   -> ⚠️ URL invalide pour '{source_name}'."); initialisation_errors += 1; continue

            source_type = source_info.get("source_type")
            original_path_str = source_info.get("path", "")
            is_plaintext_url = any(original_path_str.lower().endswith(ext) for ext in PLAINTEXT_EXTENSIONS)

            # Utiliser l'URL reconstruite comme clé pour le cache texte final
            filepath = get_cache_filepath(reconstructed_url)
            if not filepath.exists():
                print(f"   -> Cache texte absent pour '{source_name}'. Récupération (type: {source_type})...")
                try:
                    if source_type == "jina":
                        fetch_with_jina(reconstructed_url)
                    elif source_type == "direct_download":
                         fetch_direct_text(reconstructed_url)
                    elif source_type == "tika":
                        if is_plaintext_url:
                             # Si type=tika mais URL est texte simple, utiliser fetch direct
                             print("      (URL type texte détectée, utilisation fetch direct au lieu de Tika)")
                             fetch_direct_text(reconstructed_url)
                        else:
                            # Type Tika pour fichier binaire (PDF, DOCX...)
                            url_hash = hashlib.sha256(reconstructed_url.encode()).hexdigest()
                            file_extension = Path(original_path_str).suffix if Path(original_path_str).suffix else ".download"
                            temp_save_path = TEMP_DOWNLOAD_DIR / f"{url_hash}{file_extension}"
                            fetch_with_tika(source_url=reconstructed_url, raw_file_cache_path=temp_save_path)
                    else:
                         print(f"   -> ⚠️ Type source inconnu '{source_type}'.")
                         initialisation_errors += 1

                except Exception as e_fetch: # Attraper erreur PENDANT le fetch
                    print(f"   -> ❌ Erreur fetch pour '{source_name}': {e_fetch}")
                    initialisation_errors += 1
            # else: print(f"   -> Cache texte trouvé pour '{source_name}'.")

        except Exception as e_loop: # Attraper erreur DANS la boucle (ex: reconstruct_url)
            print(f"   -> ❌ Erreur traitement source '{source_name}': {e_loop}")
            initialisation_errors += 1

    print("\n--- Fin Initialisation du Cache ---")
    if initialisation_errors > 0: print(f"⚠️ {initialisation_errors} erreur(s) rencontrée(s).")
    else: print("✅ Cache initialisé/vérifié.")

--- Initialisation du Cache des Textes Complets ---
Vérification du cache pour 4 source(s)...
   -> Cache texte absent pour 'Lincoln-Douglas Débat 1 (NPS)'. Récupération (type: jina)...
-> Récupération via Jina : https://r.jina.ai/https://www.nps.gov/liho/learn/historyculture/debate1.htm...
   -> Contenu Jina récupéré (longueur 98928).
   -> Texte sauvegardé : 0087ebbffeb0b4c12d3042a72aa0cc29fdf73576de25647dc619141f0ecc2109.txt
   -> Cache texte absent pour 'Lincoln-Douglas Débat 2 (NPS)'. Récupération (type: jina)...
-> Récupération via Jina : https://r.jina.ai/https://www.nps.gov/liho/learn/historyculture/debate2.htm...
   -> Contenu Jina récupéré (longueur 102217).
   -> Texte sauvegardé : 587a4145d0b66f5ded5cf201f74084815e4bfb4044e720d0d9dac49f6c50d722.txt
   -> ⚠️ URL invalide pour 'Corpus de recherche (chiffré, hors dépôt public) - A'.
   -> ⚠️ URL invalide pour 'Corpus de recherche (chiffré, hors dépôt public) - B'.

--- Fin Initialisation du Cache ---
⚠️ 2 erreur(s) rencontrée(s).

Résultat de l’initialisation du cache

Statut : Cache initialisé/vérifié avec succès

Ce qui s’est passé :

  1. Les 4 sources prédéfinies ont été vérifiées
  2. Pour chaque source, le système a vérifié l’existence du fichier cache correspondant
  3. Si le cache existe déjà (cas présent), aucun téléchargement n’a été effectué
  4. Aucune erreur détectée lors de la vérification

Avantages de cette initialisation proactive :

  • Performance : Les textes sont pré-téléchargés, l’UI sera plus réactive lors du choix aléatoire
  • Détection précoce : Les problèmes de réseau ou d’URL invalide sont identifiés avant l’utilisation interactive
  • Mode offline : Une fois le cache rempli, le notebook peut fonctionner sans connexion internet

Note : En production, cette cellule peut être commentée si vous souhaitez un chargement à la demande plutôt qu’une initialisation complète.

5. Définition de la Fonction Principale de l’UI (configure_analysis_task)

Définition de la fonction qui sera appelée par le notebook exécuteur.

Architecture de la fonction principale

La fonction configure_analysis_task() implémente une interface utilisateur complète organisée en 3 étapes principales :

1. Sélection de la source texte

L’utilisateur dispose de 4 modes d’entrée via des onglets :

Onglet Mode Description
Bibliothèque Prédéfini Sources configurées avec extraits nommés (mode aléatoire ou manuel)
URL Externe Saisie manuelle d’URL (traitement via Jina ou Tika)
Fichier Upload Téléversement de fichier local (auto-détection du format)
Texte direct Manuel Copier-coller direct dans une zone de texte

2. Extraction optionnelle

Des marqueurs de début/fin permettent d’isoler un passage spécifique : - Marqueurs exclus du résultat final - Utile pour extraire un paragraphe dans un long document - Fonctionne avec toutes les sources (bibliothèque, URL, fichier, texte direct)

3. Gestion de la configuration

Trois boutons permettent de : - Charger : Rafraîchir les définitions depuis le fichier chiffré - Sauvegarder : Persister les modifications dans extract_sources.json.gz.enc - Vérifier : Valider que tous les marqueurs existent dans les textes sources

Fonctionnement interne :

  1. Polling UI : La fonction utilise ui_events() pour bloquer jusqu’à ce que l’utilisateur clique sur “Lancer l’Analyse”
  2. Gestion d’état : Variables nonlocal pour partager l’état entre les callbacks de widgets
  3. Retour : Le texte préparé est retourné au notebook exécuteur principal

Exercice 3 : Validation d’un extract

La fonction verify_extract_definitions verifie qu’un extract est complet. Implementez une validation simplifiee.

Objectif : Ecrire validate_extract.

Indices : - Étape 1 : Verifier la presence des cles obligatoires (url, text) - Étape 2 : Verifier que l’URL est non vide et commence par http - Étape 3 : Verifier que le texte extrait a une longueur minimale - Étape 4 : Retourner un dictionnaire valid + liste errors

# Exercice 3 : Validation d'extract
# La fonction verify_extract_definitions verifie qu'un extract est complet.
# TODO etudiant: ecrivez une validation simplifiee.
# Etape 1: Verifier la presence des cles obligatoires
# Etape 2: Verifier que l'URL est non vide
# Etape 3: Verifier que le texte a ete extrait
# Etape 4: Retourner un rapport de validation
def validate_extract(extract_dict):
    return {"valid": False, "errors": []}  # TODO etudiant

test_extract = {"url": "https://example.com", "text": "Lorem ipsum"}
print(f"Validation: {validate_extract(test_extract)}")
print("Exercice a completer")
Validation: {'valid': False, 'errors': []}
Exercice a completer
def configure_analysis_task():
    """Affiche l'UI, gère interactions et retourne texte préparé."""

    # --- Variables locales à la fonction pour l'état de l'UI ---
    # Note: On utilise 'nonlocal' pour les modifier dans les callbacks
    texte_analyse_prepare_local = ""
    analyse_ready_to_run_local = False

    # --- Récupération clé et chargement initial ---
    # Utilise les variables globales définies dans les cellules précédentes
    global ENCRYPTION_KEY, current_extract_definitions, CONFIG_FILE, EXTRACT_SOURCES
    # Charger/Recharger les définitions au début de l'appel de la fonction
    # pour refléter un éventuel chargement/sauvegarde via les boutons de l'UI
    current_extract_definitions = load_extract_definitions(CONFIG_FILE, ENCRYPTION_KEY)

    # --- Création des Widgets ---
    # (Utilise current_extract_definitions chargée)

    # Bibliothèque
    source_mode_radio = widgets.RadioButtons(
        options=['Source Aléatoire', 'Choisir Document & Extrait'],
        description='Mode:', value='Source Aléatoire', disabled=False, style={'description_width': 'initial'}
    )
    # Vérification pour éviter l'erreur si current_extract_definitions est invalide au démarrage
    valid_source_names = [s.get("source_name", f"Erreur Def #{i}")
                          for i, s in enumerate(current_extract_definitions) if isinstance(s, dict)]
    if not valid_source_names: # Fournir une option par défaut si tout échoue
         print("⚠️ Erreur: Aucune définition de source valide trouvée pour le dropdown.")
         valid_source_names = ["Erreur Chargement Config"]

    source_doc_dropdown = widgets.Dropdown(
        options=valid_source_names,
        description='Document:', disabled=True, style={'description_width': 'initial'}, layout={'width': '90%'}
    )
    extract_dropdown = widgets.Dropdown(
        options=[], description='Extrait:', disabled=True, style={'description_width': 'initial'}, layout={'width': '90%'}
    )
    tab_library = widgets.VBox([source_mode_radio, source_doc_dropdown, extract_dropdown])

    # URL
    url_input = widgets.Text(placeholder="Entrez l'URL", description="URL:", layout={'width': '90%'}, style={'description_width': 'initial'})
    url_processing_type_radio = widgets.RadioButtons(options=[('Page Web (via Jina)', 'jina'), ('Document PDF/Office (via Tika)', 'tika')], description='Type:', value='jina', disabled=False, style={'description_width': 'initial'})
    tab_url = widgets.VBox([widgets.Label("Entrez l'URL et choisissez le type:"), url_input, url_processing_type_radio])

    # Fichier
    file_uploader = widgets.FileUpload(accept='.txt,.pdf,.doc,.docx,.html,.xml,.md', multiple=False, description="Fichier:", style={'description_width': 'initial'})
    tab_file = widgets.VBox([widgets.Label("Téléversez fichier (traité par Tika si besoin):"), file_uploader]) # Texte Tika ajusté

    # Texte Direct
    direct_text_input = widgets.Textarea(placeholder="Collez texte ici...", layout={'width': '90%', 'height': '200px'})
    tab_direct = widgets.VBox([widgets.Label("Saisissez le texte :"), direct_text_input])

    # Tabs
    tabs = widgets.Tab(children=[tab_library, tab_url, tab_file, tab_direct])
    tabs.set_title(0, '📚 Bibliothèque'); tabs.set_title(1, '🌐 URL'); tabs.set_title(2, '📄 Fichier'); tabs.set_title(3, '✍️ Texte Direct')

    # Extraction
    start_marker_input = widgets.Text(placeholder="(Optionnel) Début (exclus)", description="Début Extrait:", layout={'width': '90%'}, style={'description_width': 'initial'})
    end_marker_input = widgets.Text(placeholder="(Optionnel) Fin (exclue)", description="Fin Extrait:", layout={'width': '90%'}, style={'description_width': 'initial'})
    extraction_box = widgets.VBox([
        widgets.HTML("<hr><h4>Options d'Extraction (Optionnel)</h4>"),
        widgets.HTML("<p style='font-size:0.9em; color:grey;'>Marqueurs uniques. Exclus du résultat.</p>"),
        start_marker_input, end_marker_input
    ])

    # Gestion Config
    config_output_area = widgets.HTML(value="<i>Chargement...</i>", layout={'border': '1px solid #ccc', 'padding': '5px', 'margin_top': '5px', 'max_height': '200px', 'overflow_y':'auto'})
    load_config_button = widgets.Button(description="Charger/Actualiser Déf.", icon="refresh", button_style='info', tooltip="Charge les définitions")
    save_config_button = widgets.Button(description="Sauvegarder Déf.", icon="save", button_style='warning', disabled=(not ENCRYPTION_KEY), tooltip="Sauvegarde les définitions")
    verify_button = widgets.Button(description="Vérifier Marqueurs", icon="check", button_style='primary', tooltip="Vérifie les marqueurs") # Bouton Vérif
    config_management_box = widgets.VBox([
         widgets.HTML("<hr style='margin: 20px 0;'><h3>Gestion Configuration Sources</h3>"),
         widgets.HBox([load_config_button, save_config_button, verify_button]), # Bouton Vérif ajouté
         config_output_area
    ])

    # Boutons Principaux et Sortie
    prepare_button = widgets.Button(description="Préparer le Texte", button_style='info', icon='cogs', tooltip="Charge, extrait et prépare texte.")
    run_button = widgets.Button(description="Lancer l'Analyse", button_style='success', icon='play', disabled=True, tooltip="Démarre l'analyse.")
    main_output_area = widgets.Output(layout={'border': '1px solid #ccc', 'padding': '10px', 'margin_top': '10px', 'min_height': '100px'})

    # --- Callbacks ---
    def update_extract_options_ui(change):
        nonlocal extract_dropdown # Référence widget
        global current_extract_definitions # Accès définitions
        selected_doc_name = change.get('new', source_doc_dropdown.value)
        # Utiliser .get pour éviter KeyError si structure invalide
        source_info = next((s for s in current_extract_definitions if isinstance(s, dict) and s.get("source_name") == selected_doc_name), None)
        if source_info:
            extract_options = ["Texte Complet"] + [e.get("extract_name", "Sans Nom") for e in source_info.get("extracts", []) if isinstance(e, dict)]
            current_extract_value = extract_dropdown.value
            extract_dropdown.options = extract_options
            if current_extract_value in extract_options:
                extract_dropdown.value = current_extract_value
            elif extract_options: # S'assurer qu'il y a des options avant d'accéder à [0]
                 extract_dropdown.value = extract_options[0]
            else: # Cas où il n'y a que "Texte Complet" et pas d'extraits
                 extract_dropdown.value = None
        else:
            extract_dropdown.options = []
            extract_dropdown.value = None
    source_doc_dropdown.observe(update_extract_options_ui, names='value')

    def handle_source_mode_change_ui(change):
        nonlocal source_doc_dropdown, extract_dropdown
        is_manual_choice = (change.get('new', source_mode_radio.value) == 'Choisir Document & Extrait')
        source_doc_dropdown.disabled = not is_manual_choice
        extract_dropdown.disabled = not is_manual_choice
        # ATTENTION: Indentation cruciale ici
        if is_manual_choice:
            if source_doc_dropdown.options:
                 # S'assurer qu'une valeur est sélectionnée avant de mettre à jour les extraits
                 if source_doc_dropdown.value:
                     update_extract_options_ui({'new': source_doc_dropdown.value})
                 # Si aucune valeur n'est sélectionnée (cas initial ou après chargement liste vide), on vide les extraits
                 else:
                     extract_dropdown.options = []
                     extract_dropdown.value = None
            else: # Si pas d'options dans le premier dropdown, vider le second
                 extract_dropdown.options = []
                 extract_dropdown.value = None
        else: # Cas 'Source Aléatoire'
             extract_dropdown.options = []
             extract_dropdown.value = None
    source_mode_radio.observe(handle_source_mode_change_ui, names='value')

    def display_definitions_in_ui(definitions_list):
        # ... (Code identique, mais utilise .get pour sécurité) ...
        if not definitions_list: return "Aucune définition chargée."
        MAX_EXTRACTS_DISPLAY = 5; html = "<ul style='list-style-type: none; padding-left: 0;'>";
        for source in definitions_list:
            if not isinstance(source, dict): continue # Ignorer éléments non valides
            source_name_display = source.get('source_name', 'Erreur: Nom Manquant')
            html += f"<li style='margin-bottom: 10px;'><b>{source_name_display}</b> ({source.get('source_type', 'N/A')})"
            # Utilisation nouvelle structure URL
            reconstructed = reconstruct_url(source.get("schema"), source.get("host_parts", []), source.get("path"))
            html += f"<br/><small style='color:grey;'>{reconstructed or 'URL Invalide'}</small>"
            extracts = source.get('extracts', [])
            if extracts and isinstance(extracts, list): # Vérifier que extracts est une liste
                html += "<ul style='margin-top: 4px; font-size: 0.9em; list-style-type: none; padding-left: 10px;'>";
                for i, extract in enumerate(extracts):
                    if not isinstance(extract, dict): continue # Ignorer extraits non valides
                    if i >= MAX_EXTRACTS_DISPLAY: html += f"<li>... et {len(extracts) - MAX_EXTRACTS_DISPLAY} autre(s)</li>"; break
                    extract_name_display = extract.get('extract_name', 'Erreur: Nom Extrait Manquant')
                    html += f"<li>- {extract_name_display}</li>";
                html += "</ul>";
            html += "</li>";
        html += "</ul>"; return html

    def on_load_config_click_ui(b):
        nonlocal config_output_area, source_doc_dropdown, save_config_button # Widgets externes
        global current_extract_definitions, ENCRYPTION_KEY, CONFIG_FILE # Variables globales
        with main_output_area: clear_output(wait=True); print("⏳ Chargement définitions...")
        loaded_defs = load_extract_definitions(CONFIG_FILE, ENCRYPTION_KEY)
        # ---- Validation robuste ----
        valid_defs = [s for s in loaded_defs if isinstance(s, dict) and "source_name" in s]
        if len(valid_defs) != len(loaded_defs):
            print("⚠️ Attention: Certaines définitions chargées/par défaut étaient invalides et ont été ignorées.")
        current_extract_definitions = valid_defs # Utiliser seulement les valides
        # ---------------------------
        config_output_area.value = display_definitions_in_ui(current_extract_definitions)
        # Mettre à jour dropdown bibliothèque
        current_doc_selection = source_doc_dropdown.value # Sauver la sélection
        source_doc_options = [s["source_name"] for s in current_extract_definitions] # Utilise la liste filtrée
        source_doc_dropdown.options = source_doc_options
        # Essayer de restaurer la sélection ou prendre la première
        if current_doc_selection in source_doc_options: source_doc_dropdown.value = current_doc_selection
        elif source_doc_options: source_doc_dropdown.value = source_doc_options[0]
        else: source_doc_dropdown.value = None
        # Mettre à jour les extraits (appelle update_extract_options_ui via observe)
        # Forcer l'appel au cas où la valeur n'a pas changé mais les options oui
        if source_doc_dropdown.value: update_extract_options_ui({'new': source_doc_dropdown.value})
        else: extract_dropdown.options = []

        with main_output_area: clear_output(wait=True); print("✅ Définitions chargées/actualisées.")
        save_config_button.disabled = (not ENCRYPTION_KEY)

    def on_save_config_click_ui(b):
        nonlocal main_output_area # Widget externe
        global current_extract_definitions, ENCRYPTION_KEY, CONFIG_FILE # Variables globales
        with main_output_area: clear_output(wait=True); print("⏳ Sauvegarde définitions...")
        success = save_extract_definitions(current_extract_definitions, CONFIG_FILE, ENCRYPTION_KEY)
        if success: print("✅ Définitions sauvegardées.")
        else: print("❌ Échec sauvegarde.")

    def on_verify_click_ui(b):
        """Lance la vérification et affiche le résultat."""
        nonlocal main_output_area # Widget externe
        global current_extract_definitions # Variable globale
        with main_output_area:
            clear_output(wait=True)
            print("Lancement de la vérification (peut prendre du temps)...")
            # Appelle la fonction de vérification définie dans la cellule précédente
            # S'assurer que verify_extract_definitions est bien définie globalement
            if 'verify_extract_definitions' in globals():
                 summary = verify_extract_definitions(current_extract_definitions)
                 clear_output(wait=True)
                 display(widgets.HTML(summary)) # Afficher le résultat HTML
                 print("\nVérification terminée.") # Message console
            else:
                 print("ERREUR: La fonction verify_extract_definitions n'est pas trouvée.")


    def on_prepare_click_ui(b):
        nonlocal texte_analyse_prepare_local, analyse_ready_to_run_local
        global current_extract_definitions
        # ... (Logique complète et corrigée de on_prepare_click_ui de ma réponse précédente) ...
        # ... Assurez-vous que cette logique utilise bien la structure URL corrigée ...
        # ... et la gestion des extensions texte simple ...
        analyse_ready_to_run_local = False; run_button.disabled = True
        texte_brut_source = ""; source_description = ""; start_marker_final = ""; end_marker_final = ""; error_occured = False

        with main_output_area:
            clear_output(wait=True); print("⏳ Préparation texte...")
            selected_tab_index = tabs.selected_index
            try:
                if selected_tab_index == 0: # Bibliothèque
                    source_info = None ; extract_info = None ; reconstructed_url = None
                    if source_mode_radio.value == 'Source Aléatoire':
                        if not current_extract_definitions: raise ValueError("Biblio vide!")
                        source_info = random.choice(current_extract_definitions)
                        extracts_available = source_info.get("extracts", []); potential_extracts = [{"extract_name": "Texte Complet"}] + extracts_available; extract_info = random.choice(potential_extracts); print(f"-> Choix Aléatoire: Doc='{source_info.get('source_name', '?')}', Extrait='{extract_info['extract_name']}'")
                    else:
                        selected_doc_name = source_doc_dropdown.value; selected_extract_name = extract_dropdown.value
                        if not selected_doc_name: raise ValueError("Aucun document sélectionné.")
                        source_info = next((s for s in current_extract_definitions if s.get("source_name") == selected_doc_name), None)
                        if not source_info: raise ValueError(f"Doc '{selected_doc_name}' non trouvé.")
                        if selected_extract_name == "Texte Complet": extract_info = {"extract_name": "Texte Complet"}
                        else: extract_info = next((e for e in source_info.get("extracts", []) if e.get("extract_name") == selected_extract_name), None);
                        if not extract_info: raise ValueError(f"Extrait '{selected_extract_name}' non trouvé.")
                        print(f"-> Choix Manuel: Doc='{source_info['source_name']}', Extrait='{extract_info['extract_name']}'")

                    start_marker_final = extract_info.get("start_marker", "") if extract_info.get("extract_name") != "Texte Complet" else ""
                    end_marker_final = extract_info.get("end_marker", "") if extract_info.get("extract_name") != "Texte Complet" else ""
                    reconstructed_url = reconstruct_url(source_info.get("schema"), source_info.get("host_parts", []), source_info.get("path"))
                    if not reconstructed_url: raise ValueError("URL source invalide.")
                    source_description = f"Biblio: {source_info.get('source_name','?')} ({extract_info.get('extract_name','?')})"

                    cached_text = load_from_cache(reconstructed_url)
                    if cached_text is not None: texte_brut_source = cached_text
                    else:
                        source_type = source_info.get("source_type"); original_path_str = source_info.get("path", ""); is_plaintext_url = any(original_path_str.lower().endswith(ext) for ext in PLAINTEXT_EXTENSIONS)
                        print(f"-> Cache vide. Récupération (Type: {source_type}, URL: ...)...")
                        if source_type == "jina": texte_brut_source = fetch_with_jina(reconstructed_url)
                        elif source_type == "direct_download": texte_brut_source = fetch_direct_text(reconstructed_url)
                        elif source_type == "tika":
                             if is_plaintext_url: print("      (URL type texte, fetch direct)"); texte_brut_source = fetch_direct_text(reconstructed_url)
                             else: url_hash = hashlib.sha256(reconstructed_url.encode()).hexdigest(); temp_save_path = TEMP_DOWNLOAD_DIR / f"{url_hash}.download_debug"; texte_brut_source = fetch_with_tika(source_url=reconstructed_url, raw_file_cache_path=temp_save_path)
                        else: raise ValueError(f"Type source inconnu '{source_type}'.")

                elif selected_tab_index == 1: # URL
                    url = url_input.value.strip(); processing_type = url_processing_type_radio.value
                    if not url or not url.startswith(('http://', 'https://')): raise ValueError("URL invalide.")
                    source_description = f"URL ({processing_type.upper()}): {url}"; cached_text = load_from_cache(url)
                    if cached_text is not None: texte_brut_source = cached_text
                    else:
                        is_plaintext_url = any(url.lower().endswith(ext) for ext in PLAINTEXT_EXTENSIONS)
                        if processing_type == "jina": texte_brut_source = fetch_with_jina(url)
                        elif processing_type == "tika":
                             if is_plaintext_url: print("   -> URL type texte, fetch direct."); texte_brut_source = fetch_direct_text(url)
                             else: texte_brut_source = fetch_with_tika(source_url=url)
                        else: raise ValueError(f"Type traitement inconnu: {processing_type}")
                    start_marker_final = start_marker_input.value.strip()
                    end_marker_final = end_marker_input.value.strip()

                elif selected_tab_index == 2: # Fichier
                    if not file_uploader.value: raise ValueError("Veuillez téléverser un fichier.")
                    uploaded_file_info = file_uploader.value[0]; file_name = uploaded_file_info['name']; cache_key = f"file://{file_name}"; cached_text = load_from_cache(cache_key)
                    if cached_text is not None: texte_brut_source = cached_text; source_description = f"Fichier (Cache): {file_name}"
                    else:
                        is_plaintext_file = any(file_name.lower().endswith(ext) for ext in PLAINTEXT_EXTENSIONS)
                        file_content_bytes = uploaded_file_info['content']
                        if is_plaintext_file:
                            print(f"-> Traitement fichier texte local : '{file_name}'");
                            try: texte_brut_source = file_content_bytes.decode('utf-8', errors='ignore'); print(f"   -> Contenu lu direct."); save_to_cache(cache_key, texte_brut_source); source_description = f"Fichier Texte: {file_name}"
                            except Exception as e_decode: print(f"   -> ⚠️ Erreur décodage, tentative Tika: {e_decode}"); texte_brut_source = fetch_with_tika(file_content=file_content_bytes, file_name=file_name); source_description = f"Fichier (via Tika post-err): {file_name}"
                        else: print(f"-> Traitement fichier '{file_name}' via Tika..."); source_description = f"Fichier (via Tika): {file_name}"; texte_brut_source = fetch_with_tika(file_content=file_content_bytes, file_name=file_name)
                    try: file_uploader.value = {}; file_uploader._counter = 0
                    except Exception: pass
                    start_marker_final = start_marker_input.value.strip()
                    end_marker_final = end_marker_input.value.strip()

                elif selected_tab_index == 3: # Texte Direct
                    texte_brut_source = direct_text_input.value; source_description = "Texte Direct"
                    if not texte_brut_source: raise ValueError("Veuillez saisir du texte.")
                    print(f"-> Utilisation texte direct (longueur: {len(texte_brut_source)})."); start_marker_final = start_marker_input.value.strip(); end_marker_final = end_marker_input.value.strip()
                else: raise ValueError("Onglet inconnu.")

                # --- Application finale des marqueurs ---
                texte_final = texte_brut_source
                if start_marker_final or end_marker_final:
                    # ... (logique d'extraction identique) ...
                    print("\n-> Application marqueurs..."); start_index = 0; end_index = len(texte_brut_source);
                    if start_marker_final:
                        try: found_start = texte_brut_source.index(start_marker_final); start_index = found_start + len(start_marker_final); print(f"   -> Début trouvé.")
                        except ValueError: print(f"   ⚠️ Début non trouvé."); start_index = 0
                    if end_marker_final:
                        try: found_end = texte_brut_source.index(end_marker_final, start_index); end_index = found_end; print(f"   -> Fin trouvée.")
                        except ValueError: print(f"   ⚠️ Fin non trouvée."); end_index = len(texte_brut_source)
                    if start_index < end_index: texte_final = texte_brut_source[start_index:end_index].strip();
                    else: print(f"   ⚠️ Conflit/Vide."); texte_final = ""
                    if texte_final and (start_marker_final or end_marker_final) : source_description += " (Extrait)"
                    if not texte_final and texte_brut_source: print("   -> Extraction vide.")

                # --- Finalisation ---
                texte_analyse_prepare_local = texte_final
                if not texte_analyse_prepare_local: print("\n⚠️ Texte préparé final vide !")
                print("\n--- Aperçu Texte Préparé ---")
                preview = texte_analyse_prepare_local[:1500]; print(preview + ('\n[...]' if len(texte_analyse_prepare_local) > 1500 else '')); print("-" * 30)
                print(f"\n✅ Texte préparé (Source: {source_description}, Longueur: {len(texte_analyse_prepare_local)}).")
                if len(texte_analyse_prepare_local) > 0: print("Cliquez sur 'Lancer l'Analyse'."); run_button.disabled = False
                else: print("Texte vide."); run_button.disabled = True

            except Exception as e:
                 # ... (gestion d'erreur identique) ...
                 error_occured = True ; texte_analyse_prepare_local = ""
                 print(f"\n❌ Erreur Préparation : {type(e).__name__} - {e}"); print("\n--- Trace ---"); print(traceback.format_exc()); print("-" * 25); run_button.disabled = True

    def on_run_click_ui(b):
        nonlocal analyse_ready_to_run_local, texte_analyse_prepare_local
        # ... (code identique) ...
        if run_button.disabled or not texte_analyse_prepare_local:
             with main_output_area: clear_output(wait=True); print("⚠️ Préparez un texte non vide.")
        else:
            analyse_ready_to_run_local = True; prepare_button.disabled = True; run_button.disabled = True; tabs.disabled = True; start_marker_input.disabled = True; end_marker_input.disabled = True; load_config_button.disabled = True; save_config_button.disabled = True; source_mode_radio.disabled = True; source_doc_dropdown.disabled=True; extract_dropdown.disabled=True; url_input.disabled = True; url_processing_type_radio.disabled=True; file_uploader.disabled = True; direct_text_input.disabled = True
            with main_output_area: clear_output(wait=True); print(f"📝 Texte final prêt (Longueur: {len(texte_analyse_prepare_local)})."); print("\n🚀 Lancement analyse...")

    # --- Lier Callbacks ---
    prepare_button.on_click(on_prepare_click_ui)
    run_button.on_click(on_run_click_ui)
    load_config_button.on_click(on_load_config_click_ui)
    save_config_button.on_click(on_save_config_click_ui)
    verify_button.on_click(on_verify_click_ui) # <-- Lier nouveau bouton

    # --- Affichage et Boucle ---
    print("Initialisation interface...")
    ui_container = widgets.VBox([
        widgets.HTML("<h2>Configuration Tâche Analyse</h2>"),
        widgets.HTML("<h3>1. Source Texte</h3>"), tabs, extraction_box,
        config_management_box, # <-- Boîte gestion config incluant nouveau bouton
        widgets.HTML("<hr><h3>2. Préparation</h3>"), prepare_button, main_output_area,
        widgets.HTML("<hr><h3>3. Démarrage</h3>"), run_button
    ])
    display(ui_container)

    # Initialiser l'UI
    handle_source_mode_change_ui({})
    on_load_config_click_ui(None) # Charger config initiale

    print("\n⏳ En attente interaction...")
    with ui_events() as poll:
        while not analyse_ready_to_run_local: poll(10); time.sleep(0.1)

    print("\n🏁 Configuration tâche terminée. Retour notebook principal...")
    return texte_analyse_prepare_local

# --- Fin de la définition de configure_analysis_task ---
print("Fonction configure_analysis_task() définie dans UI_Configuration.ipynb.")
Fonction configure_analysis_task() définie dans UI_Configuration.ipynb.

Utilisation de la fonction dans le notebook exécuteur

Exemple d’appel :

# Dans Argumentation-08b-Executor-Python.ipynb

# 1. Charger ce notebook de configuration
%run Argumentation-08c-UI-Configuration-Python.ipynb

# 2. Appeler la fonction (affiche l'UI et attend interaction)
texte_a_analyser = configure_analysis_task()

# 3. Le texte préparé est maintenant disponible
print(f"Texte reçu : {len(texte_a_analyser)} caractères")

# 4. Lancer l'analyse avec les agents
# ... (code d'orchestration des agents) ...

Workflow complet :

  1. L’utilisateur interagit avec l’interface graphique (choix de source, extraction, etc.)
  2. Il clique sur “Préparer le Texte” pour valider et charger le contenu
  3. Un aperçu s’affiche (1500 premiers caractères)
  4. Il clique sur “Lancer l’Analyse” pour libérer le bloc ui_events()
  5. La fonction retourne le texte au notebook appelant
  6. Le notebook exécuteur peut alors orchestrer les agents sur ce texte

Points techniques :

  • Blocage : ui_events().poll(10) avec sleep(0.1) = polling toutes les 100ms sans surcharge CPU
  • Désactivation : Tous les widgets sont désactivés après validation pour éviter les modifications accidentelles
  • Validation : Le bouton “Lancer l’Analyse” reste grisé tant qu’aucun texte valide n’est préparé

Astuce : En mode batch (sans UI), vous pouvez bypass cette fonction et définir directement la variable texte_a_analyser avec le contenu souhaité.

Resume et perspectives

Ce notebook constitue la brique fondamentale de la pipeline d’analyse argumentative : il définit l’ensemble des mécanismes d’acquisition, de mise en cache et de preparation des textes bruts avant leur traitement par les agents LLM. Les composants cles implementes couvrent quatre axes – l’interface interactive via ipywidgets avec sélection multi-sources (bibliotheque, URL, fichier, texte direct), le cache fichier a deux niveaux (brut binaire et texte extrait), le chiffrement AES-Fernet des configurations sources avec derivation PBKDF2, et l’extraction de contenu via Jina Reader ou Apache Tika selon le type de document.

L’architecture modulaire adoptee ici – fonction configure_analysis_task() appelee par le notebook executeur via %run – permet une separation nette entre la logique de preparation des données et celle de l’analyse par les agents. La gestion des sources predefinies (debats Lincoln-Douglas, discours historiques, documents geopolitiques) offre un corpus varie pour tester la detection de sophismes, la structure argumentative et la rhétorique persuasive. La verification automatique des marqueurs d’extraits garantit la robustesse du pipeline face aux modifications des pages source.

Le notebook suivant, Argument_Analysis_Executor, reprend le texte prepare ici pour orchestrer l’analyse proprement dite : extraction des arguments, identification des premisses et conclusions, detection de sophismes, et evaluation de la coherence globale du discours.

6. Fin

Le notebook Argumentation-08c-UI-Configuration-Python.ipynb est maintenant prêt. Il contient toute la logique nécessaire pour l’interface utilisateur, la gestion des sources, le cache et le chiffrement.

Le notebook exécuteur principal peut maintenant utiliser la fonction configure_analysis_task() définie ici pour obtenir le texte préparé avant de lancer l’analyse par les agents.

Retour au sommet