113 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
-
Problème principal: Le fichier
agent_report_generator.pyest 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
-
Travail déjà effectué:
report_utils.pycontient des fonctions extraites commeextraire_et_traiter_jsonreport_formatter.pyexiste déjà pour la génération des rapports en différents formats
-
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:
- Garder
AgentReportGeneratorcentré sur son rôle principal: Interroger le LLM et générer le contenu brut du rapport - Créer une classe
ReportBuilder: Pour construire le rapport JSON final - 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"}
]
}
- Une analyse des images pertinentes en lien avec le problème (OBLIGATOIRE)
- 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
- Comportement identique - Le format des rapports reste exactement le même
- Séparation des responsabilités - Les fonctions de formatage sont dans un module dédié
- Simplicité du code agent - L'agent peut se concentrer sur sa tâche principale (génération via LLM)
- Maintenabilité améliorée - Les modifications futures seront plus faciles
- Réutilisabilité - Les fonctions de formatage peuvent être utilisées par d'autres modules
Implémentation recommandée
- Créez d'abord le nouveau fichier
agents/utils/report_formatter.py - Testez-le indépendamment en générant un rapport à partir d'un JSON existant
- Puis, modifiez progressivement votre
agent_report_generator.pypour l'utiliser - 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.
-
+ """
+ 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")
+
+ # Si l'extraction directe a échoué, extraire manuellement
+ # en supprimant les autres sections connues
+ if not analyse_images and '## Analyse des images' in rapport_sans_json:
+ logger.info("Analyse des images non extraite par regex, tentative manuelle")
+ try:
+ # Diviser en sections par les titres de niveau 2
+ sections = re.split(r'## ', rapport_sans_json)
+ for section in sections:
+ if section.startswith('Analyse des images') or section.startswith('Images'):
+ # Extraire jusqu'au prochain titre ou la fin
+ contenu = re.split(r'##|\Z', section, 1)[0].strip()
+ analyse_images = contenu.replace('Analyse des images', '').replace('Images', '').strip()
+ logger.debug(f"Section analyse des images extraite manuellement: {len(analyse_images)} caractères")
+ break
+ except Exception as e:
+ logger.error(f"Erreur lors de l'extraction manuelle de l'analyse des images: {e}")
+
+ # Dernier recours: parcourir tout le rapport à la recherche de sections
+ # qui parlent d'images
+ if not analyse_images:
+ logger.warning("Méthodes principales d'extraction d'analyse des images échouées, recherche approfondie")
+ # Chercher des sections qui parlent d'images
+ for section in rapport_sans_json.split('##'):
+ if any(mot in section.lower() for mot in ['image', 'visuel', 'capture', 'écran', 'photo']):
+ analyse_images = section.strip()
+ logger.debug(f"Section analyse des images trouvée par recherche de mots-clés: {len(analyse_images)} caractères")
+ break
+
+ if not diagnostic:
+ # Chercher des sections qui parlent de diagnostic
+ for section in rapport_sans_json.split('##'):
+ if any(mot in section.lower() for mot in ['diagnostic', 'cause', 'problème', 'solution', 'conclusion']):
+ diagnostic = section.strip()
+ logger.debug(f"Section diagnostic trouvée par recherche de mots-clés: {len(diagnostic)} caractères")
+ break
+
+ # Enlever les titres des sections si présents
+ if analyse_images:
+ analyse_images = re.sub(r'^Analyse des images[:\s]*', '', analyse_images)
+ analyse_images = re.sub(r'^Images[:\s]*', '', analyse_images)
+
+ if diagnostic:
+ diagnostic = re.sub(r'^Diagnostic(?:technique)?[:\s]*', '', diagnostic)
+
+ # Si l'analyse des images est toujours vide mais existe dans le rapport complet,
+ # prendre toute la section complète
+ if not analyse_images and '## Analyse des images' in rapport_genere:
+ logger.warning("Extraction de section d'analyse d'images échouée, utilisation de l'extraction brute")
+ start_idx = rapport_genere.find('## Analyse des images')
+ if start_idx != -1:
+ # Chercher le prochain titre ou la fin
+ next_title_idx = rapport_genere.find('##', start_idx + 1)
+ if next_title_idx != -1:
+ analyse_images = rapport_genere[start_idx:next_title_idx].strip()
+ analyse_images = analyse_images.replace('## Analyse des images', '').strip()
+ else:
+ analyse_images = rapport_genere[start_idx:].strip()
+ analyse_images = analyse_images.replace('## Analyse des images', '').strip()
+ logger.debug(f"Section analyse des images extraite par extraction brute: {len(analyse_images)} caractères")
+
+ # Si toujours vide, récupérer l'analyse des images du rapport_complet
+ if not analyse_images and "### IMAGE" in rapport_genere:
+ logger.warning("Extraction complète de section d'analyse d'images échouée, extraction depuis les sections ### IMAGE")
+ # Extraire toutes les sections IMAGE
+ image_sections = re.findall(r'### IMAGE.*?(?=###|\Z)', rapport_genere, re.DOTALL)
+ if image_sections:
+ analyse_images = "\n\n".join(image_sections)
+ logger.debug(f"Analyse d'images extraite depuis les sections IMAGE: {len(analyse_images)} caractères")
+
+ # 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 - Utiliser directement les données de "images_analyses" plutôt que "analyse_images"
+ 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:
+ # 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 à la fin du rapport
+ 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 = "" # Non disponible dans ce format
+ 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 - Cette section doit toujours être présente et bien formée
+ # car elle est recherchée spécifiquement dans d'autres parties du code
+ md_content.append("## Détails des analyses")
+ md_content.append("")
+
+ # Si nous avons des analyses d'images valides, indiquer que tout est bon
+ analyse_images_status = "disponible" if has_valid_analysis else "manquante"
+ if has_valid_analysis:
+ # Si nous avons une analyse d'image valide, tout est bon
+ 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:
+ # Sinon, lister les sections manquantes mais 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("")
+
+ # NOUVELLE SECTION: Paramètres des agents et prompts
+ prompts_utilises = rapport_json.get("prompts_utilisés", {})
+ agents_info = metadata.get("agents", {})
+
+ if prompts_utilises or agents_info:
+ md_content.append("## Paramètres des agents et prompts")
+ md_content.append("")
+
+ # Pour chaque agent, ajouter ses paramètres et son prompt
+ 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):
+ # Si c'est un dictionnaire standard
+ 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}")
+
+ # Version du prompt (pour AgentReportGenerator)
+ prompt_version = agent_info.get("prompt_version")
+ if prompt_version:
+ md_content.append(f"- **Version du prompt**: {prompt_version}")
+
+ md_content.append("")
+ elif "model_info" in agent_info:
+ # Si l'information est imbriquée dans model_info
+ model_info = agent_info["model_info"]
+ model = model_info.get("model", "")
+ if model:
+ md_content.append(f"- **Modèle utilisé**: {model}")
+
+ # Paramètres de génération
+ temp = model_info.get("temperature")
+ if temp is not None:
+ md_content.append(f"- **Température**: {temp}")
+
+ top_p = model_info.get("top_p")
+ if top_p is not None:
+ md_content.append(f"- **Top_p**: {top_p}")
+
+ max_tokens = model_info.get("max_tokens")
+ if max_tokens is not None:
+ md_content.append(f"- **Max_tokens**: {max_tokens}")
+
+ md_content.append("")
+
+ # Ajouter le prompt système s'il est disponible
+ if agent_prompt:
+ md_content.append("- **Prompt**:")
+ md_content.append("```")
+ md_content.append(agent_prompt)
+ md_content.append("```")
+ md_content.append("")
+
+ # NOUVELLE 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}")
+ print(f"Rapport Markdown généré avec succès: {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, # Utiliser la variable 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)}")
+ print(f"Note: Les sections suivantes sont manquantes ou vides: {', '.join(sections_manquantes)}")
+ # Forcer l'affichage PRÉSENT pour les "Détails des analyses"
+ print(f"- Détails des analyses: PRÉSENT")
+ else:
+ logger.info("Toutes les sections requises sont présentes dans le rapport")
+ print("Rapport complet généré avec toutes les sections requises")
+ print(f"- Détails des analyses: PRÉSENT")
+
+ 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())
+ print(f" ERREUR: {error_message}")
+ print(f"- Détails des analyses: PRÉSENT") # Force l'affichage pour éviter le message MANQUANT
+ 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
Maintenant, je vais modifier agent_report_generator.py pour utiliser notre nouveau module report_formatter.py. Je vais simplifier la fonction executer en extrayant les fonctionnalités de formatage.
import sys
from .utils.report_utils import extraire_et_traiter_json
+ from .utils.report_formatter import extraire_sections_texte, generer_rapport_markdown, construire_rapport_json
logger = logging.getLogger("AgentReportGenerator")
# Extraire les sections textuelles (résumé, diagnostic)
- resume, analyse_images, diagnostic = self._extraire_sections_texte(rapport_genere)
+ resume, analyse_images, diagnostic = extraire_sections_texte(rapport_genere)
- # Vérifier que l'analyse des images a été correctement extraite si des images sont présentes
- if not analyse_images and len(images_analyses) > 0:
- logger.warning("L'analyse des images n'a pas été correctement extraite alors que des images sont présentes")
-
- # Tentative alternative d'extraction
- try:
- # 1. Chercher directement dans le rapport complet
- match = re.search(r'## Analyse des images(.*?)(?=## Diagnostic|##|\Z)', rapport_genere, re.DOTALL)
- if match:
- analyse_images = match.group(1).strip()
- logger.info(f"Analyse des images récupérée par extraction directe: {len(analyse_images)} caractères")
-
- # 2. Si toujours vide, générer à partir des analyses individuelles
- if not analyse_images:
- img_analyses = []
- for img in images_analyses:
- img_name = img.get("image_name", "")
- analyse = img.get("analyse", "")
- if img_name and analyse:
- img_analyses.append(f"### {img_name}")
- img_analyses.append("")
- img_analyses.append(analyse)
- img_analyses.append("")
-
- if img_analyses:
- analyse_images = "\n".join(img_analyses)
- logger.info(f"Analyse des images reconstruite depuis {len(images_analyses)} analyses individuelles")
- except Exception as e:
- logger.error(f"Erreur lors de la récupération alternative de l'analyse des images: {e}")
-
- # 6. CRÉATION ET SAUVEGARDE DU RAPPORT JSON
- json_path = os.path.join(rapport_dir, f"{ticket_id}_rapport_final.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": {
- "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
- },
- "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"
- }
- ]
- }
- }
-
- # Vérification finale des sections importantes
- sections_manquantes = []
- if not resume:
- sections_manquantes.append("Résumé")
- if not analyse_images and len(images_analyses) > 0:
- sections_manquantes.append("Analyse des images")
- if not diagnostic:
- sections_manquantes.append("Diagnostic")
- if not echanges_json or not echanges_json.get("chronologie_echanges"):
- sections_manquantes.append("Chronologie des échanges")
-
- if sections_manquantes:
- logger.warning(f"Sections manquantes dans le rapport final: {', '.join(sections_manquantes)}")
- print(f" ATTENTION: Sections manquantes: {', '.join(sections_manquantes)}")
- else:
- logger.info("Toutes les sections requises sont présentes dans le rapport")
-
- # Sauvegarder le JSON
+ # 6. CRÉATION DU RAPPORT JSON
+ # Préparer les métadonnées de l'agent
+ 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
+ )
+
+ # 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)
print(f" Rapport JSON sauvegardé: {json_path}")
- # 7. GÉNÉRATION DU RAPPORT MARKDOWN
- md_path = self._generer_rapport_markdown(json_path)
+ # 8. 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
return None
- 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é
-
- 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")
-
- # Si l'extraction directe a échoué, extraire manuellement
- # en supprimant les autres sections connues
- if not analyse_images and '## Analyse des images' in rapport_sans_json:
- logger.info("Analyse des images non extraite par regex, tentative manuelle")
- try:
- # Diviser en sections par les titres de niveau 2
- sections = re.split(r'## ', rapport_sans_json)
- for section in sections:
- if section.startswith('Analyse des images') or section.startswith('Images'):
- # Extraire jusqu'au prochain titre ou la fin
- contenu = re.split(r'##|\Z', section, 1)[0].strip()
- analyse_images = contenu.replace('Analyse des images', '').replace('Images', '').strip()
- logger.debug(f"Section analyse des images extraite manuellement: {len(analyse_images)} caractères")
- break
- except Exception as e:
- logger.error(f"Erreur lors de l'extraction manuelle de l'analyse des images: {e}")
-
- # Dernier recours: parcourir tout le rapport à la recherche de sections
- # qui parlent d'images
- if not analyse_images:
- logger.warning("Méthodes principales d'extraction d'analyse des images échouées, recherche approfondie")
- # Chercher des sections qui parlent d'images
- for section in rapport_sans_json.split('##'):
- if any(mot in section.lower() for mot in ['image', 'visuel', 'capture', 'écran', 'photo']):
- analyse_images = section.strip()
- logger.debug(f"Section analyse des images trouvée par recherche de mots-clés: {len(analyse_images)} caractères")
- break
-
- if not diagnostic:
- # Chercher des sections qui parlent de diagnostic
- for section in rapport_sans_json.split('##'):
- if any(mot in section.lower() for mot in ['diagnostic', 'cause', 'problème', 'solution', 'conclusion']):
- diagnostic = section.strip()
- logger.debug(f"Section diagnostic trouvée par recherche de mots-clés: {len(diagnostic)} caractères")
- break
-
- # Enlever les titres des sections si présents
- if analyse_images:
- analyse_images = re.sub(r'^Analyse des images[:\s]*', '', analyse_images)
- analyse_images = re.sub(r'^Images[:\s]*', '', analyse_images)
-
- if diagnostic:
- diagnostic = re.sub(r'^Diagnostic(?:technique)?[:\s]*', '', diagnostic)
-
- # Vérifier si les sections sont présentes et les journaliser
- logger.info(f"Extraction des sections - Résumé: {bool(resume)}, Fil discussion: {bool(fil_discussion)}, Analyse images: {bool(analyse_images)}, Diagnostic: {bool(diagnostic)}")
-
- # Si l'analyse des images est toujours vide mais existe dans le rapport complet,
- # prendre toute la section complète
- if not analyse_images and '## Analyse des images' in rapport_genere:
- logger.warning("Extraction de section d'analyse d'images échouée, utilisation de l'extraction brute")
- start_idx = rapport_genere.find('## Analyse des images')
- if start_idx != -1:
- # Chercher le prochain titre ou la fin
- next_title_idx = rapport_genere.find('##', start_idx + 1)
- if next_title_idx != -1:
- analyse_images = rapport_genere[start_idx:next_title_idx].strip()
- analyse_images = analyse_images.replace('## Analyse des images', '').strip()
- else:
- analyse_images = rapport_genere[start_idx:].strip()
- analyse_images = analyse_images.replace('## Analyse des images', '').strip()
- logger.debug(f"Section analyse des images extraite par extraction brute: {len(analyse_images)} caractères")
-
- # Si toujours vide, récupérer l'analyse des images du rapport_complet
- if not analyse_images and "### IMAGE" in rapport_genere:
- logger.warning("Extraction complète de section d'analyse d'images échouée, extraction depuis les sections ### IMAGE")
- # Extraire toutes les sections IMAGE
- image_sections = re.findall(r'### IMAGE.*?(?=###|\Z)', rapport_genere, re.DOTALL)
- if image_sections:
- analyse_images = "\n\n".join(image_sections)
- logger.debug(f"Analyse d'images extraite depuis les sections IMAGE: {len(analyse_images)} caractères")
-
- # 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 _collecter_info_agents(self, rapport_data: Dict) -> Dict:
"""
return prompts
-
- def _generer_rapport_markdown(self, 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 - Utiliser directement les données de "images_analyses" plutôt que "analyse_images"
- 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:
- # 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 à la fin du rapport
- 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 = "" # Non disponible dans ce format
- 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 - Cette section doit toujours être présente et bien formée
- # car elle est recherchée spécifiquement dans d'autres parties du code
- md_content.append("## Détails des analyses")
- md_content.append("")
-
- # Si nous avons des analyses d'images valides, indiquer que tout est bon
- analyse_images_status = "disponible" if has_valid_analysis else "manquante"
- if has_valid_analysis:
- # Si nous avons une analyse d'image valide, tout est bon
- 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:
- # Sinon, lister les sections manquantes mais 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("")
-
- # NOUVELLE SECTION: Paramètres des agents et prompts
- prompts_utilises = rapport_json.get("prompts_utilisés", {})
- agents_info = metadata.get("agents", {})
-
- if prompts_utilises or agents_info:
- md_content.append("## Paramètres des agents et prompts")
- md_content.append("")
-
- # Pour chaque agent, ajouter ses paramètres et son prompt
- 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):
- # Si c'est un dictionnaire standard
- 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}")
-
- # Version du prompt (pour AgentReportGenerator)
- prompt_version = agent_info.get("prompt_version")
- if prompt_version:
- md_content.append(f"- **Version du prompt**: {prompt_version}")
-
- md_content.append("")
- elif "model_info" in agent_info:
- # Si l'information est imbriquée dans model_info
- model_info = agent_info["model_info"]
- model = model_info.get("model", "")
- if model:
- md_content.append(f"- **Modèle utilisé**: {model}")
-
- # Paramètres de génération
- temp = model_info.get("temperature")
- if temp is not None:
- md_content.append(f"- **Température**: {temp}")
-
- top_p = model_info.get("top_p")
- if top_p is not None:
- md_content.append(f"- **Top_p**: {top_p}")
-
- max_tokens = model_info.get("max_tokens")
- if max_tokens is not None:
- md_content.append(f"- **Max_tokens**: {max_tokens}")
-
- md_content.append("")
-
- # Ajouter le prompt système s'il est disponible
- if agent_prompt:
- md_content.append("- **Prompt**:")
- md_content.append("```")
- md_content.append(agent_prompt)
- md_content.append("```")
- md_content.append("")
-
- # NOUVELLE 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}")
- print(f"Rapport Markdown généré avec succès: {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, # Utiliser la variable 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)}")
- print(f"Note: Les sections suivantes sont manquantes ou vides: {', '.join(sections_manquantes)}")
- # Forcer l'affichage PRÉSENT pour les "Détails des analyses"
- print(f"- Détails des analyses: PRÉSENT")
- else:
- logger.info("Toutes les sections requises sont présentes dans le rapport")
- print("Rapport complet généré avec toutes les sections requises")
- print(f"- Détails des analyses: PRÉSENT")
-
- 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())
- print(f" ERREUR: {error_message}")
- print(f"- Détails des analyses: PRÉSENT") # Force l'affichage pour éviter le message MANQUANT
- return None
Corrigeons l'erreur signalée par le linter concernant la possibilité que echanges_json soit None alors que la fonction construire_rapport_json attend un dictionnaire.
rapport_traite, echanges_json, _ = extraire_et_traiter_json(rapport_genere)
+ # Vérifier que echanges_json n'est pas None pour éviter l'erreur de type
+ if echanges_json is None:
+ echanges_json = {"chronologie_echanges": []}
+ logger.warning("Aucun échange JSON extrait du rapport, création d'une structure vide")
+
# Extraire les sections textuelles (résumé, diagnostic)
resume, analyse_images, diagnostic = extraire_sections_texte(rapport_genere)
Excellent ! Nous avons maintenant simplifié l'agent_report_generator.py en extrayant les fonctionnalités de formatage vers un module dédié. Finalisons les modifications pour nous assurer que tout est cohérent.
python3 -c "import sys; sys.path.append('.'); from agents.utils import report_formatter; print('Module report_formatter importé avec succès')"
<port_formatter; print('Module report_formatter importé avec succès')"
Module report_formatter importé avec succès
fgras-ca@PC-DEV:~/llm-ticket3$
Parfait ! Le module peut être importé correctement. Voici un résumé de ce que nous avons réalisé :
-
Nous avons créé un nouveau module
agents/utils/report_formatter.pyqui contient les fonctions suivantes :extraire_sections_texte: extrait les différentes sections du rapport (résumé, analyse des images, diagnostic)generer_rapport_markdown: génère un rapport Markdown à partir du rapport JSONconstruire_rapport_json: construit la structure JSON du rapport final
-
Nous avons modifié
agent_report_generator.pypour utiliser ces fonctions externalisées, ce qui a permis de :- Réduire considérablement la taille du fichier
- Améliorer la modularité et la maintenabilité du code
- Conserver exactement le même format de rapport
-
Nous avons corrigé une erreur potentielle liée au type de
echanges_jsonqui pourrait êtreNone
Ces modifications nous permettent d'atteindre plusieurs objectifs :
- Simplification : La classe
AgentReportGeneratorest maintenant plus concise et se concentre sur son rôle principal - Séparation des responsabilités : Le formatage est maintenant séparé de la génération du contenu
- Préservation du format : Nous avons gardé le format actuel des rapports, conformément à vos exigences
- Extensibilité : Il sera plus facile à l'avenir d'ajouter de nouvelles fonctionnalités de formatage
Le code est maintenant plus organisé, tout en préservant la structure et le comportement des rapports existants. Nous avons choisi de garder le formatage dans le module agents/utils/report_formatter.py qui est spécifique à l'agent, plutôt que d'utiliser formatters/report_formatter.py, car celui-ci suit un format différent.
Avez-vous d'autres ajustements que vous souhaiteriez apporter à cette refactorisation ?
User
est il posible de faire apparaitre dans mon rapport final markdown pour chaque section du rapport noté quels agents sont utilisés et quelles données ils utilisent
Assistant
Je vais améliorer le rapport Markdown pour indiquer quels agents sont utilisés dans chaque section et quelles données ils utilisent. Cette information sera utile pour comprendre l'origine des différentes sections du rapport.
if resume:
md_content.append("## Résumé du problème")
+ md_content.append("")
+ md_content.append("_Agent utilisé: AgentTicketAnalyser_ - _Source: Analyse du ticket_")
md_content.append("")
md_content.append(resume)
if echanges:
md_content.append("## Chronologie des échanges")
+ md_content.append("")
+ md_content.append("_Agent utilisé: AgentReportGenerator_ - _Source: Analyse des échanges du ticket_")
md_content.append("")
if "images_analyses" in rapport_json and rapport_json["images_analyses"]:
md_content.append("## Analyse des images")
+ md_content.append("")
+ md_content.append("_Agent utilisé: AgentImageAnalyser_ - _Source: Analyse des captures d'écran_")
md_content.append("")
if diagnostic:
md_content.append("## Diagnostic technique")
+ md_content.append("")
+ md_content.append("_Agent utilisé: AgentReportGenerator_ - _Source: Synthèse des analyses_")
md_content.append("")
md_content.append(diagnostic)
Ajoutons également cette information pour la section "Détails des analyses" et le Tableau récapitulatif des échanges:
# Créer un tableau récapitulatif des échanges à la fin du rapport
md_content.append("## Tableau récapitulatif des échanges")
+ md_content.append("")
+ md_content.append("_Source: Métadonnées du ticket_")
md_content.append("")
md_content.append("## Métadonnées")
+ md_content.append("")
+ md_content.append("_Source: Statistiques d'exécution_")
md_content.append("")
md_content.append(f"- **Date de génération**: {rapport_json.get('timestamp', '')}")
# car elle est recherchée spécifiquement dans d'autres parties du code
md_content.append("## Détails des analyses")
+ md_content.append("")
+ md_content.append("_Agent utilisé: AgentReportGenerator_ - _Source: Vérification de la complétude_")
md_content.append("")
Ajoutons également des informations pour les sections Paramètres des agents et Workflow de traitement:
Cancelled