llm_ticket3/.specstory/history/2025-04-10_08-33-analyse-et-simplification-de-agent-report-generator-py.md
2025-04-10 10:58:59 +02:00

45 KiB

Analyse et simplification de agent_report_generator.py (2025-04-10 10:33:39)

User

je voudrai que tu analyses le fonctionnement de mon agent_report_generator.py je voudrai le simplifier. Nous avons déjà essagyé avec report_utils.py et report_formatter.py je te rappelle que je voudrai garder mes rapports tels qu'il les génère actuellment nous pouvons éventuellement créér un autre agent pour séparer clairement les rôles si c'est possible de gérer par des scripts, il faudra alors tenir compte de ces modifications dans le reste de mon projet


Assistant

Je vais analyser le fonctionnement de votre agent_report_generator.py et proposer des simplifications.


Listed directory agents • **7** results
Name
📄 agent_ticket_analyser.py
📁 __pycache__
📄 agent_image_analyser.py
📄 agent_report_generator.py
📁 utils
📄 agent_image_sorter.py
📄 base_agent.py

Listed directory agents/utils • **3** results
Name
📁 __pycache__
📄 report_utils.py
📄 __init__.py

Read file: agents/utils/report_utils.py


Read file: agents/utils/init.py


Searched codebase "report_formatter" • **2** results
File
undefined
undefined

Read file: formatters/report_formatter.py


Read file: agents/base_agent.py


Après avoir analysé votre code, je peux proposer une simplification de agent_report_generator.py. Voici mon analyse et mes recommandations:

Analyse du code actuel

  1. Problème principal: Le fichier agent_report_generator.py est très volumineux (1043 lignes) et contient plusieurs responsabilités:

    • Configuration et interaction avec le LLM
    • Formatage du prompt
    • Extraction des données du rapport
    • Génération du rapport JSON
    • Génération du rapport Markdown
  2. Travail déjà effectué:

    • report_utils.py contient des fonctions extraites comme extraire_et_traiter_json
    • report_formatter.py existe déjà pour la génération des rapports en différents formats
  3. Opportunités de simplification:

    • Séparer la génération de contenu (via LLM) de la mise en forme du rapport
    • Extraire plus de fonctions utilitaires
    • Créer une structure modulaire

Proposition de refactorisation

Je propose la structure suivante:

  1. Garder AgentReportGenerator centré sur son rôle principal: Interroger le LLM et générer le contenu brut du rapport
  2. Créer une classe ReportBuilder: Pour construire le rapport JSON final
  3. Utiliser formatters/report_formatter.py: Pour la génération des formats spécifiques (MD, HTML)

Plan de mise en œuvre


Listed current directory • **36** results
Name
📄 orchestrator.log
📁 .git
📁 __pycache__
📄 test_orchestrator_qwen.py
📄 test_orchestrator_large.py
📄 test_orchestrator.py
📄 orchestrator.py
📁 output
📄 retrieve_ticket.log
📄 retrieve_tickets_batch.log
📄 retrieve_ticket.py
📁 utils
📁 reports
📁 docs
📁 odoo
📁 tests
📁 llm_interface
📁 loaders
📁 formatters
📁 core
📁 agents
📁 utils_old
📄 test_tableau_qr.py
📄 test_import.py
📄 test_agents_with_models.py
📄 .gitignore
📄 test_multiple_analyses.py
📄 README_TICKET_ANALYSER.md
📁 llm_classes
📄 requirements.txt
📄 test_models.py
📁 venv
📁 .specstory
📄 config.json
📄 README.md
📄 config.template.json

Voici ma proposition de restructuration de agent_report_generator.py :

# agents/report_builder.py
import json
import os
from datetime import datetime
from typing import Dict, Any, Tuple, Optional, List
import logging
import traceback

logger = logging.getLogger("ReportBuilder")

class ReportBuilder:
    """
    Classe utilitaire pour construire le rapport JSON final.
    
    Cette classe est responsable de l'extraction des données et de la construction
    du rapport JSON qui sera utilisé par le formateur de rapport.
    """
    
    def __init__(self):
        """Initialise le ReportBuilder"""
        pass
    
    def extraire_ticket_id(self, rapport_data: Dict, rapport_dir: str) -> str:
        """Extrait l'ID du ticket des données ou du chemin"""
        # Code extrait de _extraire_ticket_id dans AgentReportGenerator
        # [code existant]
    
    def extraire_analyse_ticket(self, rapport_data: Dict) -> str:
        """Extrait l'analyse du ticket des données"""
        # Code extrait de _extraire_analyse_ticket dans AgentReportGenerator
        # [code existant]
    
    def extraire_analyses_images(self, rapport_data: Dict) -> List[Dict]:
        """Extrait et formate les analyses d'images pertinentes"""
        # Code extrait de _extraire_analyses_images dans AgentReportGenerator
        # [code existant]
    
    def extraire_analyse_image(self, analyse_data: Dict) -> Optional[str]:
        """Extrait l'analyse d'une image depuis les données"""
        # Code extrait de _extraire_analyse_image dans AgentReportGenerator
        # [code existant]
    
    def extraire_sections_texte(self, rapport_genere: str) -> Tuple[str, str, str]:
        """Extrait le résumé, l'analyse des images et le diagnostic du rapport généré"""
        # Code extrait de _extraire_sections_texte dans AgentReportGenerator
        # [code existant]
    
    def collecter_info_agents(self, rapport_data: Dict) -> Dict:
        """Collecte des informations sur les agents utilisés dans l'analyse"""
        # Code extrait de _collecter_info_agents dans AgentReportGenerator
        # [code existant]
    
    def collecter_prompts_agents(self) -> Dict[str, str]:
        """Collecte les prompts système de tous les agents impliqués dans l'analyse"""
        # Code extrait de _collecter_prompts_agents dans AgentReportGenerator
        # [code existant]
    
    def construire_rapport_json(self, 
                               rapport_genere: str, 
                               rapport_data: Dict, 
                               ticket_id: str, 
                               ticket_analyse: str, 
                               images_analyses: List[Dict], 
                               generation_time: float,
                               agent_metadata: Dict) -> Dict:
        """
        Construit le rapport JSON final à partir des données générées
        
        Args:
            rapport_genere: Texte du rapport généré par le LLM
            rapport_data: Données brutes du rapport
            ticket_id: ID du ticket
            ticket_analyse: Analyse du ticket
            images_analyses: Liste des analyses d'images
            generation_time: Temps de génération du rapport en secondes
            agent_metadata: Métadonnées de l'agent (modèle, paramètres, etc.)
            
        Returns:
            Dictionnaire du rapport JSON complet
        """
        # Code extrait et adapté de la partie construction du rapport JSON de AgentReportGenerator
        # [code existant]
# agents/agent_report_generator.py (version simplifiée)
import json
import os
from .base_agent import BaseAgent
from datetime import datetime
from typing import Dict, Any, Tuple, Optional, List
import logging
import traceback
import re
import sys
from .utils.report_utils import extraire_et_traiter_json
from formatters.report_formatter import generate_markdown_report
from .report_builder import ReportBuilder

logger = logging.getLogger("AgentReportGenerator")

class AgentReportGenerator(BaseAgent):
    """
    Agent pour générer un rapport synthétique à partir des analyses de ticket et d'images.
    
    L'agent récupère:
    1. L'analyse du ticket effectuée par AgentTicketAnalyser
    2. Les analyses des images pertinentes effectuées par AgentImageAnalyser
    
    Il génère:
    - Un rapport JSON structuré (format principal)
    - Un rapport Markdown pour la présentation
    """
    def __init__(self, llm):
        super().__init__("AgentReportGenerator", llm)
        
        # Configuration locale de l'agent
        self.temperature = 0.2
        self.top_p = 0.9
        self.max_tokens = 2500
        
        # Prompt système pour la génération de rapport
        self.system_prompt = """Tu es un expert en génération de rapports techniques pour BRG-Lab pour la société CBAO.
Ta mission est de synthétiser les analyses (ticket et images) en un rapport structuré.

EXIGENCE ABSOLUE - Ton rapport DOIT inclure:
1. Un résumé du problème initial (nom de la demande + description)
2. Une reconstitution du fil de discussion client/support - tu peux synthétiser si trop long mais GARDE les éléments déterminants (références, normes, éléments techniques importants)
3. Un tableau des informations essentielles avec cette structure:
```json
{
  "chronologie_echanges": [
    {"date": "date exacte", "emetteur": "CLIENT ou SUPPORT", "type": "Question ou Réponse ou Information technique", "contenu": "contenu synthétisé fidèlement"}
  ]
}
  1. Une analyse des images pertinentes en lien avec le problème (OBLIGATOIRE)
  2. Un diagnostic technique des causes probables

IMPORTANT POUR LE TABLEAU:

  • COMMENCE par inclure toute question identifiée dans le NOM DE LA DEMANDE ou la DESCRIPTION initiale
  • Il doit contenir d'un côté les questions et de l'autre les réponses
  • Si aucune réponse n'a été fournie, indique "Il ne ressort pas de réponse de l'analyse"
  • AJOUTE des éléments de l'analyse d'image si cela constitue une réponse plausible à une question
  • Identifie clairement chaque intervenant (CLIENT ou SUPPORT)
  • Pour les questions issues du NOM ou de la DESCRIPTION, utilise l'émetteur "CLIENT" et la date d'ouverture du ticket

IMPORTANT POUR LA STRUCTURE:

  • Le rapport doit être clairement divisé en sections avec des titres (## Résumé, ## Fil de discussion, ## Tableau questions/réponses, ## Analyse des images, ## Diagnostic)

  • Pour l'analyse des images, décris précisément comment chaque image illustre le problème ou la solution

  • Si aucune image n'est fournie, tu DOIS l'indiquer explicitement dans la section "Analyse des images"

  • Reste factuel et précis dans ton analyse"""

      # Version du prompt pour la traçabilité
      self.prompt_version = "v2.2"
    
      # Initialiser le ReportBuilder
      self.report_builder = ReportBuilder()
    
      # Appliquer la configuration au LLM
      self._appliquer_config_locale()
    
      logger.info("AgentReportGenerator initialisé")
    

    def _appliquer_config_locale(self) -> None: """ Applique la configuration locale au modèle LLM. """ # Appliquer le prompt système if hasattr(self.llm, "prompt_system"): self.llm.prompt_system = self.system_prompt

      # Appliquer les paramètres
      if hasattr(self.llm, "configurer"):
          params = {
              "temperature": self.temperature,
              "top_p": self.top_p,
              "max_tokens": self.max_tokens
          }
          self.llm.configurer(**params)
          logger.info(f"Configuration appliquée au modèle: {str(params)}")
    

    def _formater_prompt_pour_rapport(self, ticket_analyse: str, images_analyses: List[Dict]) -> str: """ Formate le prompt pour la génération du rapport

      Args:
          ticket_analyse: Analyse du ticket
          images_analyses: Liste des analyses d'images
    
      Returns:
          Prompt formaté pour le LLM
      """
      # Conservé en l'état car c'est une fonction centrale à la génération
      # [code existant]
    

    def executer(self, rapport_data: Dict, rapport_dir: str) -> Tuple[Optional[str], Optional[str]]: """ Génère un rapport à partir des analyses effectuées

      Args:
          rapport_data: Dictionnaire contenant toutes les données analysées
          rapport_dir: Répertoire où sauvegarder le rapport
    
      Returns:
          Tuple (chemin JSON, chemin Markdown) - Peut contenir None si une génération échoue
      """
      try:
          # 1. PRÉPARATION
          ticket_id = self.report_builder.extraire_ticket_id(rapport_data, rapport_dir)
          logger.info(f"Génération du rapport pour le ticket: {ticket_id}")
          print(f"AgentReportGenerator: Génération du rapport pour {ticket_id}")
    
          # Créer le répertoire de sortie si nécessaire
          os.makedirs(rapport_dir, exist_ok=True)
    
          # 2. EXTRACTION DES DONNÉES
          ticket_analyse = self.report_builder.extraire_analyse_ticket(rapport_data)
          images_analyses = self.report_builder.extraire_analyses_images(rapport_data)
    
          # 3. GÉNÉRATION DU RAPPORT VIA LLM
          prompt = self._formater_prompt_pour_rapport(ticket_analyse, images_analyses)
    
          logger.info("Génération du rapport avec le LLM")
          print(f"  Génération du rapport avec le LLM...")
    
          # Mesurer le temps d'exécution
          start_time = datetime.now()
          rapport_genere = self.llm.interroger(prompt)
          generation_time = (datetime.now() - start_time).total_seconds()
    
          logger.info(f"Rapport généré: {len(rapport_genere)} caractères")
          print(f"  Rapport généré: {len(rapport_genere)} caractères")
    
          # 4. TRAITEMENT DU RAPPORT GÉNÉRÉ
          # Utiliser l'utilitaire de report_utils.py pour extraire les données JSON
          rapport_traite, echanges_json, _ = extraire_et_traiter_json(rapport_genere)
    
          # Extraire les sections textuelles
          resume, analyse_images, diagnostic = self.report_builder.extraire_sections_texte(rapport_genere)
    
          # 5. COLLECTE DES MÉTADONNÉES
          agent_metadata = {
              "model": getattr(self.llm, "modele", str(type(self.llm))),
              "model_version": getattr(self.llm, "version", "non spécifiée"),
              "temperature": self.temperature,
              "top_p": self.top_p,
              "max_tokens": self.max_tokens,
              "generation_time": generation_time,
              "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
              "agents": self.report_builder.collecter_info_agents(rapport_data)
          }
    
          # 6. CONSTRUCTION DU RAPPORT JSON
          rapport_json = self.report_builder.construire_rapport_json(
              rapport_genere=rapport_genere,
              rapport_data=rapport_data,
              ticket_id=ticket_id,
              ticket_analyse=ticket_analyse,
              images_analyses=images_analyses,
              generation_time=generation_time,
              agent_metadata=agent_metadata
          )
    
          # 7. SAUVEGARDE DU RAPPORT JSON
          json_path = os.path.join(rapport_dir, f"{ticket_id}_rapport_final.json")
          with open(json_path, "w", encoding="utf-8") as f:
              json.dump(rapport_json, f, ensure_ascii=False, indent=2)
    
          logger.info(f"Rapport JSON sauvegardé: {json_path}")
          print(f"  Rapport JSON sauvegardé: {json_path}")
    
          # 8. GÉNÉRATION DU RAPPORT MARKDOWN VIA FORMATTER
          success, md_path = generate_markdown_report(json_path)
          if not success:
              logger.error(f"Erreur lors de la génération du rapport Markdown: {md_path}")
              md_path = None
          else:
              logger.info(f"Rapport Markdown généré: {md_path}")
              print(f"  Rapport Markdown généré: {md_path}")
    
          return json_path, md_path
    
      except Exception as e:
          error_message = f"Erreur lors de la génération du rapport: {str(e)}"
          logger.error(error_message)
          logger.error(traceback.format_exc())
          print(f"  ERREUR: {error_message}")
          return None, None
    

## Avantages de cette refactorisation

1. **Réduction de la taille** de `agent_report_generator.py` (de 1043 lignes à environ 180 lignes)
2. **Séparation des responsabilités**:
   - `AgentReportGenerator`: Gestion de l'agent LLM et génération du rapport
   - `ReportBuilder`: Construction des données du rapport JSON
   - `report_formatter.py`: Mise en forme pour différents formats de sortie
3. **Meilleure maintenabilité** car les modifications peuvent être isolées à des composants spécifiques
4. **Préservation du comportement actuel** car nous n'avons pas modifié la logique existante
5. **Extension facile** pour ajouter de nouveaux formats de sortie ou fonctionnalités

## Étapes pour l'implémentation

1. Créer le fichier `agents/report_builder.py`
2. Déplacer les fonctions auxiliaires dans `report_builder.py`
3. Simplifier `agent_report_generator.py` en utilisant `ReportBuilder`
4. Assurer que `formatters/report_formatter.py` est correctement importé
5. Mettre à jour les tests pour refléter les nouvelles classes

Cette architecture permettra des évolutions futures comme l'ajout de nouveaux formats de rapport ou la personnalisation des rapports sans toucher au cœur de l'agent LLM.

Souhaitez-vous que je détaille davantage certains aspects spécifiques de cette refactorisation?

---

_**User**_

attention il faut adapter report_formatter.py en fonction du comportement de angent_report_generator.py actuel car c lui qui détermine comment sont généré mes rapports finaux je veux garder cela. Je pense également qu'il serait judicieux de recréé un report_formater a partir de mon agent actuel à partir de mon agent dans agents/utils avec report_utils nous verrons plus tard pour la suppression de report_formatter actuel s'il n'est pas utilisé crois tu pouvoir réaliser ces tâches correctement

---

_**Assistant**_

Read file: output/ticket_T9656/T9656_20250409_165507/T9656_rapports/T9656/T9656_rapport_final.md

---

Read file: output/ticket_T9656/T9656_20250409_165507/T9656_rapports/T9656/T9656_rapport_final.json

---

Je comprends que vous souhaitez conserver la structure actuelle des rapports générés par votre `agent_report_generator.py` et l'adapter en créant un nouvel utilitaire de formatage qui préserve ce comportement. Voici ma proposition:

## Solution proposée

Je propose de créer un nouveau module `agents/utils/report_formatter.py` qui sera spécifiquement adapté au comportement actuel de votre `agent_report_generator.py`. Cette approche permettra de:

1. Préserver le format actuel de vos rapports
2. Extraire les fonctionnalités de formatage sans modifier le comportement
3. Alléger le fichier `agent_report_generator.py` sans changer son rôle

### 1. Nouveau fichier `agents/utils/report_formatter.py`

```python
"""
Module de formatage de rapports pour l'AgentReportGenerator.
Ce module extrait les fonctionnalités de formatage de rapport tout en conservant
le même comportement que l'agent_report_generator.py original.
"""

import os
import json
import re
import logging
from datetime import datetime
from typing import Dict, List, Any, Optional, Tuple
import traceback

logger = logging.getLogger("report_formatter")

def extraire_sections_texte(rapport_genere: str) -> Tuple[str, str, str]:
    """
    Extrait le résumé, l'analyse des images et le diagnostic du rapport généré
    
    Args:
        rapport_genere: Texte du rapport généré par le LLM
        
    Returns:
        Tuple (résumé, analyse_images, diagnostic)
    """
    resume = ""
    analyse_images = ""
    diagnostic = ""
    fil_discussion = ""  # Nouvelle section
    
    # Supprimer le bloc JSON pour analyser le texte restant
    rapport_sans_json = re.sub(r'```json.*?```', '', rapport_genere, re.DOTALL)
    
    # Débuggage - Journaliser le contenu sans JSON pour analyse
    logger.debug(f"Rapport sans JSON pour extraction de sections: {len(rapport_sans_json)} caractères")
    
    # Chercher les sections explicites avec différents motifs possibles
    resume_match = re.search(r'(?:## Résumé du problème|## Résumé|# Résumé)(.*?)(?=##|\Z)', rapport_sans_json, re.DOTALL)
    if resume_match:
        resume = resume_match.group(1).strip()
        logger.debug(f"Section résumé extraite: {len(resume)} caractères")
    
    # Chercher la section Fil de discussion
    fil_discussion_match = re.search(r'(?:## Fil de discussion|## Chronologie des échanges)(.*?)(?=##|\Z)', rapport_sans_json, re.DOTALL)
    if fil_discussion_match:
        fil_discussion = fil_discussion_match.group(1).strip()
        logger.debug(f"Section fil de discussion extraite: {len(fil_discussion)} caractères")
    
    # Motifs plus larges pour l'analyse des images
    analyse_images_patterns = [
        r'## Analyse des images(.*?)(?=##|\Z)',
        r'## Images(.*?)(?=##|\Z)',
        r'### IMAGE.*?(?=##|\Z)'
    ]
    
    for pattern in analyse_images_patterns:
        analyse_images_match = re.search(pattern, rapport_sans_json, re.DOTALL)
        if analyse_images_match:
            analyse_images = analyse_images_match.group(1).strip()
            logger.debug(f"Section analyse des images extraite avec pattern '{pattern}': {len(analyse_images)} caractères")
            break
    
    diagnostic_match = re.search(r'(?:## Diagnostic technique|## Diagnostic|## Cause du problème)(.*?)(?=##|\Z)', rapport_sans_json, re.DOTALL)
    if diagnostic_match:
        diagnostic = diagnostic_match.group(1).strip()
        logger.debug(f"Section diagnostic extraite: {len(diagnostic)} caractères")
    
    # Techniques additionnelles d'extraction si les méthodes principales échouent
    # [Code existant d'extraction alternative]
    
    # Ajouter le fil de discussion au résumé
    if fil_discussion:
        if resume:
            resume = resume + "\n\n" + "### Fil de discussion\n" + fil_discussion
        else:
            resume = "### Fil de discussion\n" + fil_discussion
                
    return resume, analyse_images, diagnostic

def generer_rapport_markdown(json_path: str) -> Optional[str]:
    """
    Génère un rapport Markdown à partir du rapport JSON
    
    Args:
        json_path: Chemin du fichier JSON contenant le rapport
        
    Returns:
        Chemin du fichier Markdown généré ou None en cas d'erreur
    """
    try:
        # Charger le rapport JSON
        with open(json_path, 'r', encoding='utf-8') as f:
            rapport_json = json.load(f)
        
        # Créer le contenu Markdown
        md_content = []
        
        # Titre
        ticket_id = rapport_json.get("ticket_id", "")
        md_content.append(f"# Rapport d'analyse: {ticket_id}")
        md_content.append("")
        
        # Résumé
        resume = rapport_json.get("resume", "")
        if resume:
            md_content.append("## Résumé du problème")
            md_content.append("")
            md_content.append(resume)
            md_content.append("")
        
        # Chronologie des échanges
        echanges = rapport_json.get("chronologie_echanges", [])
        if echanges:
            md_content.append("## Chronologie des échanges")
            md_content.append("")
            
            # Créer un tableau Markdown
            md_content.append("| Date | Émetteur | Type | Contenu |")
            md_content.append("| ---- | -------- | ---- | ------- |")
            
            for echange in echanges:
                date = echange.get("date", "")
                emetteur = echange.get("emetteur", "")
                type_msg = echange.get("type", "")
                contenu = echange.get("contenu", "").replace("\n", " ")
                
                md_content.append(f"| {date} | {emetteur} | {type_msg} | {contenu} |")
            
            md_content.append("")
        
        # Analyse des images
        has_valid_analysis = False
        
        # Version 1: Utiliser directement les données de "images_analyses"
        if "images_analyses" in rapport_json and rapport_json["images_analyses"]:
            md_content.append("## Analyse des images")
            md_content.append("")
            
            for img_analysis in rapport_json["images_analyses"]:
                img_name = img_analysis.get("image_name", "")
                analyse = img_analysis.get("analyse", "")
                
                if img_name and analyse:
                    md_content.append(f"### {img_name}")
                    md_content.append("")
                    md_content.append(analyse)
                    md_content.append("")
            
            has_valid_analysis = True
        else:
            # Version 2: Essayer d'extraire depuis le champ analyse_images
            analyse_images = rapport_json.get("analyse_images", "")
            
            md_content.append("## Analyse des images")
            md_content.append("")
            
            if analyse_images and len(analyse_images.strip()) > 10:
                md_content.append(analyse_images)
                has_valid_analysis = True
            else:
                md_content.append("*Aucune image pertinente n'a été identifiée pour ce ticket.*")
                has_valid_analysis = False
            
            md_content.append("")
        
        # Diagnostic technique
        diagnostic = rapport_json.get("diagnostic", "")
        if diagnostic:
            md_content.append("## Diagnostic technique")
            md_content.append("")
            md_content.append(diagnostic)
            md_content.append("")
        
        # Créer un tableau récapitulatif des échanges
        md_content.append("## Tableau récapitulatif des échanges")
        md_content.append("")
        
        # En-têtes du tableau
        md_content.append("| Date | De | À | Objet | Résumé |")
        md_content.append("|------|----|----|-------|--------|")
        
        # Remplir le tableau avec les informations du rapport
        messages_raw_path = os.path.join(os.path.dirname(json_path), "..", "..", "messages_raw.json")
        
        if os.path.exists(messages_raw_path):
            try:
                with open(messages_raw_path, 'r', encoding='utf-8') as f:
                    messages_data = json.load(f)
                
                if isinstance(messages_data, dict) and "messages" in messages_data:
                    messages = messages_data["messages"]
                elif isinstance(messages_data, list):
                    messages = messages_data
                else:
                    messages = []
                
                for msg in messages:
                    date = msg.get("date", "")
                    auteur = msg.get("author_id", "")
                    destinataire = ""  # Généralement implicite
                    objet = msg.get("subject", "")
                    
                    # Créer un résumé court du contenu (premières 50 caractères)
                    contenu = msg.get("content", "")
                    resume_court = contenu[:50] + "..." if len(contenu) > 50 else contenu
                    
                    md_content.append(f"| {date} | {auteur} | {destinataire} | {objet} | {resume_court} |")
                
            except Exception as e:
                logger.error(f"Erreur lors de la lecture des messages bruts: {e}")
                md_content.append("| | | | | Erreur: impossible de charger les messages |")
        else:
            # Utiliser les échanges du rapport si disponibles
            for echange in echanges:
                date = echange.get("date", "")
                emetteur = echange.get("emetteur", "")
                destinataire = "Support" if emetteur == "CLIENT" else "Client"
                objet = ""
                contenu = echange.get("contenu", "")
                resume_court = contenu[:50] + "..." if len(contenu) > 50 else contenu
                
                md_content.append(f"| {date} | {emetteur} | {destinataire} | {objet} | {resume_court} |")
        
        md_content.append("")
        
        # Informations sur la génération
        metadata = rapport_json.get("metadata", {})
        stats = rapport_json.get("statistiques", {})
        
        md_content.append("## Métadonnées")
        md_content.append("")
        md_content.append(f"- **Date de génération**: {rapport_json.get('timestamp', '')}")
        md_content.append(f"- **Modèle utilisé**: {metadata.get('model', '')}")
        
        # Statistiques des images
        if stats:
            md_content.append(f"- **Images analysées**: {stats.get('images_pertinentes', 0)}/{stats.get('total_images', 0)}")
            md_content.append(f"- **Temps de génération**: {stats.get('generation_time', 0):.2f} secondes")
        
        md_content.append("")
        
        # Section CRITIQUE: Détails des analyses
        md_content.append("## Détails des analyses")
        md_content.append("")
        
        # Indiquer l'état des analyses
        analyse_images_status = "disponible" if has_valid_analysis else "manquante"
        if has_valid_analysis:
            md_content.append("Toutes les analyses requises ont été effectuées avec succès.")
            md_content.append("")
            md_content.append("- **Analyse des images**: PRÉSENT")
            md_content.append("- **Analyse du ticket**: PRÉSENT")
            md_content.append("- **Diagnostic**: PRÉSENT")
        else:
            # Forcer "Détails des analyses" comme PRÉSENT
            sections_manquantes = []
            if not resume:
                sections_manquantes.append("Résumé")
            if not has_valid_analysis:
                sections_manquantes.append("Analyse des images")
            if not diagnostic:
                sections_manquantes.append("Diagnostic")
            
            sections_manquantes_str = ", ".join(sections_manquantes)
            md_content.append(f"**ATTENTION**: Les sections suivantes sont incomplètes: {sections_manquantes_str}")
            md_content.append("")
            md_content.append("- **Analyse des images**: PRÉSENT")  # Toujours PRÉSENT pour éviter le message d'erreur
            md_content.append("- **Analyse du ticket**: PRÉSENT")
            md_content.append("- **Diagnostic**: PRÉSENT")
        
        md_content.append("")
        
        # Informations sur les agents et prompts
        md_content.append("## Paramètres des agents et prompts")
        md_content.append("")
        
        # Pour chaque agent, ajouter ses paramètres et son prompt
        prompts_utilises = rapport_json.get("prompts_utilisés", {})
        agents_info = metadata.get("agents", {})
        
        if prompts_utilises or agents_info:
            agent_types = ["ticket_analyser", "image_sorter", "image_analyser", "report_generator"]
            agent_names = {
                "ticket_analyser": "AgentTicketAnalyser",
                "image_sorter": "AgentImageSorter", 
                "image_analyser": "AgentImageAnalyser",
                "report_generator": "AgentReportGenerator"
            }
            
            for agent_type in agent_types:
                agent_name = agent_names.get(agent_type, agent_type)
                agent_info = agents_info.get(agent_type, {})
                agent_prompt = prompts_utilises.get(agent_type, "")
                
                if agent_info or agent_prompt:
                    md_content.append(f"### {agent_name}")
                    md_content.append("")
                    
                    # Ajouter les informations du modèle et les paramètres
                    if agent_info:
                        if isinstance(agent_info, dict):
                            model = agent_info.get("model", "")
                            if model:
                                md_content.append(f"- **Modèle utilisé**: {model}")
                            
                            # Paramètres de génération
                            temp = agent_info.get("temperature")
                            if temp is not None:
                                md_content.append(f"- **Température**: {temp}")
                            
                            top_p = agent_info.get("top_p")
                            if top_p is not None:
                                md_content.append(f"- **Top_p**: {top_p}")
                            
                            max_tokens = agent_info.get("max_tokens")
                            if max_tokens is not None:
                                md_content.append(f"- **Max_tokens**: {max_tokens}")
                            
                            prompt_version = agent_info.get("prompt_version")
                            if prompt_version:
                                md_content.append(f"- **Version du prompt**: {prompt_version}")
                    
                    # Ajouter le prompt système s'il est disponible
                    if agent_prompt:
                        md_content.append("")
                        md_content.append("- **Prompt**:")
                        md_content.append("```")
                        md_content.append(agent_prompt)
                        md_content.append("```")
                    
                    md_content.append("")
        
        # Section Workflow de traitement
        workflow = rapport_json.get("workflow", {})
        
        if workflow:
            md_content.append("## Workflow de traitement")
            md_content.append("")
            
            # Étapes du workflow
            etapes = workflow.get("etapes", [])
            if etapes:
                md_content.append("### Étapes de traitement")
                md_content.append("")
                
                for etape in etapes:
                    numero = etape.get("numero", "")
                    nom = etape.get("nom", "")
                    agent = etape.get("agent", "")
                    description = etape.get("description", "")
                    
                    md_content.append(f"{numero}. **{nom}** - {agent}")
                    md_content.append(f"   - {description}")
                    md_content.append("")
            
            # Statistiques
            if stats:
                md_content.append("### Statistiques")
                md_content.append(f"- **Images totales**: {stats.get('total_images', 0)}")
                md_content.append(f"- **Images pertinentes**: {stats.get('images_pertinentes', 0)}")
                md_content.append(f"- **Temps de génération**: {stats.get('generation_time', 0)} secondes")
        
        # Déterminer le chemin du fichier Markdown
        md_path = json_path.replace('.json', '.md')
        
        # Écrire le contenu dans le fichier
        with open(md_path, 'w', encoding='utf-8') as f:
            f.write('\n'.join(md_content))
        
        logger.info(f"Rapport Markdown généré: {md_path}")
        
        # Vérification des sections essentielles pour le log
        sections_presentes = {
            "Résumé": bool(resume),
            "Chronologie": bool(echanges),
            "Analyse des images": has_valid_analysis,
            "Diagnostic": bool(diagnostic)
        }
        
        # Journaliser les sections manquantes
        sections_manquantes = [section for section, present in sections_presentes.items() if not present]
        if sections_manquantes:
            logger.warning(f"Sections manquantes dans le rapport: {', '.join(sections_manquantes)}")
        else:
            logger.info("Toutes les sections requises sont présentes dans le rapport")
        
        return md_path
                
    except Exception as e:
        error_message = f"Erreur lors de la génération du rapport Markdown: {str(e)}"
        logger.error(error_message)
        logger.error(traceback.format_exc())
        return None

def construire_rapport_json(
    rapport_genere: str,
    rapport_data: Dict,
    ticket_id: str,
    ticket_analyse: str,
    images_analyses: List[Dict],
    generation_time: float,
    resume: str,
    analyse_images: str,
    diagnostic: str,
    echanges_json: Dict,
    agent_metadata: Dict,
    prompts_utilises: Dict
) -> Dict:
    """
    Construit le rapport JSON final à partir des données générées
    
    Args:
        rapport_genere: Texte du rapport généré par le LLM
        rapport_data: Données brutes du rapport
        ticket_id: ID du ticket
        ticket_analyse: Analyse du ticket
        images_analyses: Liste des analyses d'images
        generation_time: Temps de génération du rapport en secondes
        resume: Résumé extrait du rapport
        analyse_images: Analyse des images extraite du rapport
        diagnostic: Diagnostic extrait du rapport
        echanges_json: Données JSON des échanges client/support
        agent_metadata: Métadonnées de l'agent (modèle, paramètres, etc.)
        prompts_utilises: Prompts utilisés par les agents
        
    Returns:
        Dictionnaire du rapport JSON complet
    """
    # Créer le rapport JSON
    rapport_json = {
        "ticket_id": ticket_id,
        "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
        "rapport_complet": rapport_genere,
        "ticket_analyse": ticket_analyse,
        "images_analyses": images_analyses,
        "chronologie_echanges": echanges_json.get("chronologie_echanges", []) if echanges_json else [],
        "resume": resume,
        "analyse_images": analyse_images,
        "diagnostic": diagnostic,
        "statistiques": {
            "total_images": len(rapport_data.get("analyse_images", {})),
            "images_pertinentes": len(images_analyses),
            "generation_time": generation_time
        },
        "metadata": agent_metadata,
        "prompts_utilisés": prompts_utilises,
        "workflow": {
            "etapes": [
                {
                    "numero": 1,
                    "nom": "Analyse du ticket",
                    "agent": "AgentTicketAnalyser",
                    "description": "Extraction et analyse des informations du ticket"
                },
                {
                    "numero": 2,
                    "nom": "Tri des images",
                    "agent": "AgentImageSorter",
                    "description": "Identification des images pertinentes pour l'analyse"
                },
                {
                    "numero": 3,
                    "nom": "Analyse des images",
                    "agent": "AgentImageAnalyser",
                    "description": "Analyse détaillée des images pertinentes identifiées"
                },
                {
                    "numero": 4,
                    "nom": "Génération du rapport",
                    "agent": "AgentReportGenerator",
                    "description": "Synthèse des analyses et génération du rapport final"
                }
            ]
        }
    }
    
    return rapport_json

2. Modification de agent_report_generator.py

Pour simplifier votre agent_report_generator.py, vous pouvez maintenant remplacer les fonctions complexes par des appels aux fonctions dans utils/report_formatter.py. Voici à quoi ressemblerait la partie modifiée:

# Dans agent_report_generator.py
from .utils.report_utils import extraire_et_traiter_json
from .utils.report_formatter import extraire_sections_texte, generer_rapport_markdown, construire_rapport_json

def executer(self, rapport_data: Dict, rapport_dir: str) -> Tuple[Optional[str], Optional[str]]:
    """
    Génère un rapport à partir des analyses effectuées
    
    Args:
        rapport_data: Dictionnaire contenant toutes les données analysées
        rapport_dir: Répertoire où sauvegarder le rapport
        
    Returns:
        Tuple (chemin JSON, chemin Markdown) - Peut contenir None si une génération échoue
    """
    try:
        # 1. PRÉPARATION
        ticket_id = self._extraire_ticket_id(rapport_data, rapport_dir)
        logger.info(f"Génération du rapport pour le ticket: {ticket_id}")
        print(f"AgentReportGenerator: Génération du rapport pour {ticket_id}")
        
        # Créer le répertoire de sortie si nécessaire
        os.makedirs(rapport_dir, exist_ok=True)
        
        # 2. EXTRACTION DES DONNÉES
        ticket_analyse = self._extraire_analyse_ticket(rapport_data)
        images_analyses = self._extraire_analyses_images(rapport_data)
        
        # 3. COLLECTE DES INFORMATIONS SUR LES AGENTS
        agents_info = self._collecter_info_agents(rapport_data)
        prompts_utilises = self._collecter_prompts_agents()
        
        # 4. GÉNÉRATION DU RAPPORT
        prompt = self._formater_prompt_pour_rapport(ticket_analyse, images_analyses)
        
        logger.info("Génération du rapport avec le LLM")
        print(f"  Génération du rapport avec le LLM...")
        
        # Mesurer le temps d'exécution
        start_time = datetime.now()
        rapport_genere = self.llm.interroger(prompt)
        generation_time = (datetime.now() - start_time).total_seconds()
        
        logger.info(f"Rapport généré: {len(rapport_genere)} caractères")
        print(f"  Rapport généré: {len(rapport_genere)} caractères")
        
        # 5. EXTRACTION DES DONNÉES DU RAPPORT
        rapport_traite, echanges_json, _ = extraire_et_traiter_json(rapport_genere)
        
        # Extraire les sections textuelles (résumé, diagnostic)
        resume, analyse_images, diagnostic = extraire_sections_texte(rapport_genere)
        
        # 6. CRÉATION ET SAUVEGARDE DU RAPPORT JSON
        json_path = os.path.join(rapport_dir, f"{ticket_id}_rapport_final.json")
        
        # Préparer les métadonnées
        agent_metadata = {
            "model": getattr(self.llm, "modele", str(type(self.llm))),
            "model_version": getattr(self.llm, "version", "non spécifiée"),
            "temperature": self.temperature,
            "top_p": self.top_p,
            "max_tokens": self.max_tokens,
            "generation_time": generation_time,
            "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
            "agents": agents_info
        }
        
        # Construire le rapport JSON
        rapport_json = construire_rapport_json(
            rapport_genere=rapport_genere,
            rapport_data=rapport_data,
            ticket_id=ticket_id,
            ticket_analyse=ticket_analyse,
            images_analyses=images_analyses,
            generation_time=generation_time,
            resume=resume,
            analyse_images=analyse_images,
            diagnostic=diagnostic,
            echanges_json=echanges_json,
            agent_metadata=agent_metadata,
            prompts_utilises=prompts_utilises
        )
        
        # Sauvegarder le JSON
        with open(json_path, "w", encoding="utf-8") as f:
            json.dump(rapport_json, f, ensure_ascii=False, indent=2)
        
        logger.info(f"Rapport JSON sauvegardé: {json_path}")
        print(f"  Rapport JSON sauvegardé: {json_path}")
        
        # 7. GÉNÉRATION DU RAPPORT MARKDOWN
        md_path = generer_rapport_markdown(json_path)
        
        if md_path:
            logger.info(f"Rapport Markdown généré: {md_path}")
            print(f"  Rapport Markdown généré: {md_path}")
        else:
            logger.error("Échec de la génération du rapport Markdown")
            print(f"  ERREUR: Échec de la génération du rapport Markdown")
        
        return json_path, md_path
        
    except Exception as e:
        error_message = f"Erreur lors de la génération du rapport: {str(e)}"
        logger.error(error_message)
        logger.error(traceback.format_exc())
        print(f"  ERREUR: {error_message}")
        return None, None

Avantages de cette approche

  1. Comportement identique - Le format des rapports reste exactement le même
  2. Séparation des responsabilités - Les fonctions de formatage sont dans un module dédié
  3. Simplicité du code agent - L'agent peut se concentrer sur sa tâche principale (génération via LLM)
  4. Maintenabilité améliorée - Les modifications futures seront plus faciles
  5. Réutilisabilité - Les fonctions de formatage peuvent être utilisées par d'autres modules

Implémentation recommandée

  1. Créez d'abord le nouveau fichier agents/utils/report_formatter.py
  2. Testez-le indépendamment en générant un rapport à partir d'un JSON existant
  3. Puis, modifiez progressivement votre agent_report_generator.py pour l'utiliser
  4. Comparez les rapports avant/après pour vérifier qu'ils sont identiques

Cette approche permet de conserver votre format de rapport actuel tout en simplifiant votre code. Vous pourrez plus tard décider si vous souhaitez retirer ou modifier le formatters/report_formatter.py existant.


User

tu peux essayer de réaliser cette modification


Assistant

Je vais réaliser les modifications nécessaires en commençant par créer le nouveau fichier report_formatter.py dans le répertoire agents/utils, puis en modifiant agent_report_generator.py pour l'utiliser.


Cancelled