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 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 chronologie des échanges client/support sous forme de tableau précis 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"} ] } ``` 3. Une analyse des images pertinentes en lien avec le problème (OBLIGATOIRE) 4. Un diagnostic technique des causes probables IMPORTANT: - La structure doit être clairement divisée en sections avec des titres (## Résumé, ## Chronologie des échanges, ## Analyse des images, ## Diagnostic) - Le tableau des échanges doit capturer TOUTES les interactions (questions et réponses) dans l'ordre chronologique - 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" # 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 """ num_images = len(images_analyses) logger.info(f"Formatage du prompt avec {num_images} analyses d'images") # Construire la section d'analyse du ticket prompt = f"""Génère un rapport technique complet, en te basant sur les analyses suivantes. ## ANALYSE DU TICKET {ticket_analyse} """ # Ajouter la section d'analyse des images si présente if num_images > 0: prompt += f"\n## ANALYSES DES IMAGES ({num_images} images)\n" for i, img_analyse in enumerate(images_analyses, 1): image_name = img_analyse.get("image_name", f"Image {i}") analyse = img_analyse.get("analyse", "Analyse non disponible") prompt += f"\n### IMAGE {i}: {image_name}\n{analyse}\n" else: prompt += "\n## ANALYSES DES IMAGES\nAucune image n'a été fournie pour ce ticket.\n" # Instructions pour le rapport prompt += """ ## INSTRUCTIONS POUR LE RAPPORT 1. TON RAPPORT DOIT AVOIR LA STRUCTURE SUIVANTE: - Titre principal (# Rapport d'analyse: Nom du ticket) - Résumé du problème (## Résumé du problème) - Chronologie des échanges (## Chronologie des échanges) - Analyse des images (## Analyse des images) - Diagnostic technique (## Diagnostic technique) 2. DANS LA SECTION "CHRONOLOGIE DES ÉCHANGES": - Commence par créer un objet JSON comme suit: ```json { "chronologie_echanges": [ {"date": "date exacte", "emetteur": "CLIENT", "type": "Question", "contenu": "contenu exact de la question"}, {"date": "date exacte", "emetteur": "SUPPORT", "type": "Réponse", "contenu": "contenu exact de la réponse"} ] } ``` - Inclus TOUS les échanges, qu'ils soient des questions, des réponses ou des informations techniques - Respecte strictement la chronologie des messages - Synthétise le contenu sans perdre d'information importante 3. DANS LA SECTION "ANALYSE DES IMAGES": - Si des images sont présentes, explique en détail ce qu'elles montrent et leur lien avec le problème - Si aucune image n'est fournie, indique-le clairement mais conserve cette section - Mentionne le nom des images et leur contexte dans les échanges 4. DANS LA SECTION "DIAGNOSTIC TECHNIQUE": - Fournis une analyse claire des causes probables - Explique comment la solution proposée répond au problème - Si pertinent, mentionne les aspects techniques spécifiques IMPORTANT: Ce rapport sera utilisé par des techniciens et des développeurs pour comprendre rapidement le problème et sa résolution. Il doit être clair, précis et structuré. """ return prompt 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 # 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 (résumé, diagnostic) resume, analyse_images, diagnostic = self._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 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 = self._generer_rapport_markdown(json_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 def _extraire_ticket_id(self, rapport_data: Dict, rapport_dir: str) -> str: """Extrait l'ID du ticket des données ou du chemin""" # Essayer d'extraire depuis les données du rapport ticket_id = rapport_data.get("ticket_id", "") # Si pas d'ID direct, essayer depuis les données du ticket if not ticket_id and "ticket_data" in rapport_data and isinstance(rapport_data["ticket_data"], dict): ticket_id = rapport_data["ticket_data"].get("code", "") # En dernier recours, extraire depuis le chemin if not ticket_id: # Essayer d'extraire un ID de ticket (format Txxxx) du chemin match = re.search(r'T\d+', rapport_dir) if match: ticket_id = match.group(0) else: # Sinon, utiliser le dernier segment du chemin ticket_id = os.path.basename(rapport_dir) return ticket_id def _extraire_analyse_ticket(self, rapport_data: Dict) -> str: """Extrait l'analyse du ticket des données""" # Essayer les différentes clés possibles for key in ["ticket_analyse", "analyse_json", "analyse_ticket"]: if key in rapport_data and rapport_data[key]: logger.info(f"Utilisation de {key}") return rapport_data[key] # Créer une analyse par défaut si aucune n'est disponible logger.warning("Aucune analyse de ticket disponible, création d'un message par défaut") ticket_data = rapport_data.get("ticket_data", {}) ticket_name = ticket_data.get("name", "Sans titre") ticket_desc = ticket_data.get("description", "Pas de description disponible") return f"Analyse par défaut du ticket:\nNom: {ticket_name}\nDescription: {ticket_desc}\n(Aucune analyse détaillée n'a été fournie)" def _extraire_analyses_images(self, rapport_data: Dict) -> List[Dict]: """ Extrait et formate les analyses d'images pertinentes Args: rapport_data: Données du rapport contenant les analyses d'images Returns: Liste des analyses d'images pertinentes formatées """ images_analyses = [] analyse_images_data = rapport_data.get("analyse_images", {}) # Parcourir toutes les images for image_path, analyse_data in analyse_images_data.items(): # Vérifier si l'image est pertinente is_relevant = False if "sorting" in analyse_data and isinstance(analyse_data["sorting"], dict): is_relevant = analyse_data["sorting"].get("is_relevant", False) # Si l'image est pertinente, extraire son analyse if is_relevant: image_name = os.path.basename(image_path) analyse = self._extraire_analyse_image(analyse_data) if analyse: images_analyses.append({ "image_name": image_name, "image_path": image_path, "analyse": analyse, "sorting_info": analyse_data.get("sorting", {}), "metadata": analyse_data.get("analysis", {}).get("metadata", {}) }) logger.info(f"Analyse de l'image {image_name} ajoutée") return images_analyses def _extraire_analyse_image(self, analyse_data: Dict) -> Optional[str]: """ Extrait l'analyse d'une image depuis les données Args: analyse_data: Données d'analyse de l'image Returns: Texte d'analyse de l'image ou None si aucune analyse n'est disponible """ # Si pas de données d'analyse, retourner None if not "analysis" in analyse_data or not analyse_data["analysis"]: if "sorting" in analyse_data and isinstance(analyse_data["sorting"], dict): reason = analyse_data["sorting"].get("reason", "Non spécifiée") return f"Image marquée comme pertinente. Raison: {reason}" return None # Extraire l'analyse selon le format des données analysis = analyse_data["analysis"] # Structure type 1: {"analyse": "texte"} if isinstance(analysis, dict) and "analyse" in analysis: return analysis["analyse"] # Structure type 2: {"error": false, ...} - contient d'autres données utiles if isinstance(analysis, dict) and "error" in analysis and not analysis.get("error", True): return str(analysis) # Structure type 3: texte d'analyse direct if isinstance(analysis, str): return analysis # Structure type 4: autre format de dictionnaire - convertir en JSON if isinstance(analysis, dict): return json.dumps(analysis, ensure_ascii=False, indent=2) # Aucun format reconnu 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 = "" # 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") # 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)}, 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") return resume, analyse_images, diagnostic def _collecter_info_agents(self, rapport_data: Dict) -> Dict: """ Collecte des informations sur les agents utilisés dans l'analyse Args: rapport_data: Données du rapport Returns: Dictionnaire contenant les informations sur les agents """ agents_info = {} # Informations sur l'agent JSON Analyser (Ticket Analyser) ticket_analyses = {} for key in ["ticket_analyse", "analyse_json", "analyse_ticket"]: if key in rapport_data and isinstance(rapport_data[key], dict) and "metadata" in rapport_data[key]: ticket_analyses = rapport_data[key]["metadata"] break if ticket_analyses: agents_info["ticket_analyser"] = ticket_analyses # Informations sur les agents d'image if "analyse_images" in rapport_data and rapport_data["analyse_images"]: # Image Sorter sorter_info = {} analyser_info = {} for img_path, img_data in rapport_data["analyse_images"].items(): # Collecter info du sorter if "sorting" in img_data and isinstance(img_data["sorting"], dict) and "metadata" in img_data["sorting"]: sorter_info = img_data["sorting"]["metadata"] # Collecter info de l'analyser if "analysis" in img_data and isinstance(img_data["analysis"], dict) and "metadata" in img_data["analysis"]: analyser_info = img_data["analysis"]["metadata"] # Une fois qu'on a trouvé les deux, on peut sortir if sorter_info and analyser_info: break if sorter_info: agents_info["image_sorter"] = sorter_info if analyser_info: agents_info["image_analyser"] = analyser_info # Ajouter les informations de l'agent report generator agents_info["report_generator"] = { "model": getattr(self.llm, "modele", str(type(self.llm))), "temperature": self.temperature, "top_p": self.top_p, "max_tokens": self.max_tokens, "prompt_version": self.prompt_version } return agents_info def _collecter_prompts_agents(self) -> Dict[str, str]: """ Collecte les prompts système de tous les agents impliqués dans l'analyse. Returns: Dictionnaire contenant les prompts des agents """ prompts = { "rapport_generator": self.system_prompt } # Importer les classes d'agents pour accéder à leurs prompts try: # Importer les autres agents from .agent_ticket_analyser import AgentTicketAnalyser from .agent_image_analyser import AgentImageAnalyser from .agent_image_sorter import AgentImageSorter # Créer des instances temporaires pour récupérer les prompts # En passant None comme LLM pour éviter d'initialiser complètement les agents try: ticket_analyser = AgentTicketAnalyser(None) prompts["ticket_analyser"] = ticket_analyser.system_prompt logger.info("Prompt récupéré pour ticket_analyser") except Exception as e: logger.warning(f"Erreur lors de la récupération du prompt ticket_analyser: {str(e)}") try: image_analyser = AgentImageAnalyser(None) prompts["image_analyser"] = image_analyser.system_prompt logger.info("Prompt récupéré pour image_analyser") except Exception as e: logger.warning(f"Erreur lors de la récupération du prompt image_analyser: {str(e)}") try: image_sorter = AgentImageSorter(None) prompts["image_sorter"] = image_sorter.system_prompt logger.info("Prompt récupéré pour image_sorter") except Exception as e: logger.warning(f"Erreur lors de la récupération du prompt image_sorter: {str(e)}") except ImportError as e: logger.warning(f"Erreur lors de l'importation des classes d'agents: {str(e)}") 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