import os import json import logging import time import traceback from typing import List, Dict, Any, Optional, Union, Mapping, cast from agents.base_agent import BaseAgent from loaders.ticket_data_loader import TicketDataLoader from agents.utils.report_formatter import generer_rapport_markdown from utils.image_dedup import filtrer_images_uniques # Configuration du logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', filename='orchestrator.log', filemode='w') logger = logging.getLogger("Orchestrator") class Orchestrator: """ Orchestrateur pour l'analyse de tickets et la génération de rapports. """ def __init__(self, output_dir: str = "output/", ticket_agent: Optional[BaseAgent] = None, image_sorter: Optional[BaseAgent] = None, image_analyser: Optional[BaseAgent] = None, report_generator: Optional[BaseAgent] = None): self.output_dir = output_dir # Assignation directe des agents self.ticket_agent = ticket_agent self.image_sorter = image_sorter self.image_analyser = image_analyser self.report_generator = report_generator # Initialisation du loader de données de ticket self.ticket_loader = TicketDataLoader() # Collecter et enregistrer les informations détaillées sur les agents agents_info = self._collecter_info_agents() logger.info(f"Orchestrator initialisé avec output_dir: {output_dir}") logger.info(f"Agents disponibles: TicketAgent={ticket_agent is not None}, ImageSorter={image_sorter is not None}, ImageAnalyser={image_analyser is not None}, ReportGenerator={report_generator is not None}") logger.info(f"Configuration des agents: {json.dumps(agents_info, indent=2)}") def _collecter_info_agents(self) -> Dict[str, Dict[str, Any]]: """ Collecte des informations détaillées sur les agents configurés """ agents_info = {} # Information sur l'agent Ticket if self.ticket_agent: agents_info["ticket_agent"] = self._get_agent_info(self.ticket_agent) # Information sur l'agent Image Sorter if self.image_sorter: agents_info["image_sorter"] = self._get_agent_info(self.image_sorter) # Information sur l'agent Image Analyser if self.image_analyser: agents_info["image_analyser"] = self._get_agent_info(self.image_analyser) # Information sur l'agent Report Generator if self.report_generator: agents_info["report_generator"] = self._get_agent_info(self.report_generator) return agents_info def detecter_tickets(self) -> List[str]: """Détecte tous les tickets disponibles dans le répertoire de sortie""" logger.info(f"Recherche de tickets dans: {self.output_dir}") tickets = [] if not os.path.exists(self.output_dir): logger.warning(f"Le répertoire de sortie {self.output_dir} n'existe pas") print(f"ERREUR: Le répertoire {self.output_dir} n'existe pas") return tickets for ticket_dir in os.listdir(self.output_dir): ticket_path = os.path.join(self.output_dir, ticket_dir) if os.path.isdir(ticket_path) and ticket_dir.startswith("ticket_"): tickets.append(ticket_dir) return tickets def trouver_rapport(self, extraction_path: str, ticket_id: str) -> Dict[str, Optional[str]]: """ Cherche le rapport JSON. """ result = self.ticket_loader.trouver_ticket(extraction_path, ticket_id) if not result: logger.warning(f"Aucun fichier JSON trouvé pour le ticket {ticket_id}") return {"json": None} return {"json": result} def executer(self, ticket_specifique: Optional[str] = None): """ Exécute l'orchestrateur soit sur un ticket spécifique, soit sur tous les tickets Args: ticket_specifique: Code du ticket spécifique à traiter (optionnel) """ start_time = time.time() # Obtenir la liste des tickets if ticket_specifique: # Chercher le ticket spécifique ticket_path = os.path.join(self.output_dir, f"ticket_{ticket_specifique}") if os.path.exists(ticket_path): ticket_dirs = [ticket_path] logger.info(f"Ticket spécifique à traiter: {ticket_specifique}") print(f"Ticket spécifique à traiter: {ticket_specifique}") else: logger.error(f"Le ticket {ticket_specifique} n'existe pas") print(f"ERREUR: Le ticket {ticket_specifique} n'existe pas") return else: # Lister tous les tickets ticket_dirs = [os.path.join(self.output_dir, d) for d in self.detecter_tickets()] logger.info(f"Tickets à traiter: {len(ticket_dirs)}") if not ticket_dirs: logger.warning("Aucun ticket trouvé dans le répertoire de sortie") print("Aucun ticket trouvé dans le répertoire de sortie") return # Un seul log de début d'exécution logger.info("Début de l'exécution de l'orchestrateur") print("Début de l'exécution de l'orchestrateur") # Traitement des tickets for ticket_dir in ticket_dirs: try: self.traiter_ticket(ticket_dir) except Exception as e: logger.error(f"Erreur lors du traitement du ticket {ticket_dir}: {str(e)}") print(f"Erreur lors du traitement du ticket {ticket_dir}: {str(e)}") traceback.print_exc() # Calcul de la durée d'exécution duration = time.time() - start_time logger.info(f"Fin de l'exécution de l'orchestrateur (durée: {duration:.2f} secondes)") print(f"Fin de l'exécution de l'orchestrateur (durée: {duration:.2f} secondes)") def traiter_ticket(self, ticket_path: str) -> bool: """Traite un ticket spécifique et retourne True si le traitement a réussi""" logger.info(f"Début du traitement du ticket: {ticket_path}") print(f"\nTraitement du ticket: {os.path.basename(ticket_path)}") success = False extractions_trouvees = False if not os.path.exists(ticket_path): logger.error(f"Le chemin du ticket n'existe pas: {ticket_path}") print(f"ERREUR: Le chemin du ticket n'existe pas: {ticket_path}") return False ticket_id = os.path.basename(ticket_path).replace("ticket_", "") for extraction in os.listdir(ticket_path): extraction_path = os.path.join(ticket_path, extraction) if os.path.isdir(extraction_path): extractions_trouvees = True logger.info(f"Traitement de l'extraction: {extraction}") print(f" Traitement de l'extraction: {extraction}") # Recherche des rapports (JSON et MD) dans différents emplacements rapports = self.trouver_rapport(extraction_path, ticket_id) # Dossier des pièces jointes attachments_dir = os.path.join(extraction_path, "attachments") # Dossier pour les rapports générés rapports_dir = os.path.join(extraction_path, f"{ticket_id}_rapports") os.makedirs(rapports_dir, exist_ok=True) # Préparer les données du ticket à partir des rapports trouvés ticket_data = self._preparer_donnees_ticket(rapports, ticket_id) if ticket_data: success = True logger.info(f"Données du ticket chargées avec succès") print(f" Données du ticket chargées") # Traitement avec l'agent Ticket if self.ticket_agent: logger.info("Exécution de l'agent Ticket") print(" Analyse du ticket en cours...") # Log détaillé sur l'agent Ticket agent_info = self._get_agent_info(self.ticket_agent) logger.info(f"Agent Ticket: {json.dumps(agent_info, indent=2)}") ticket_analysis = self.ticket_agent.executer(ticket_data) logger.info("Analyse du ticket terminée") print(f" Analyse du ticket terminée: {len(ticket_analysis) if ticket_analysis else 0} caractères") else: logger.warning("Agent Ticket non disponible") ticket_analysis = None print(" Agent Ticket non disponible, analyse ignorée") # Traitement des images relevant_images = [] images_analyses = {} images_count = 0 if os.path.exists(attachments_dir): logger.info(f"Vérification des pièces jointes dans: {attachments_dir}") print(f" Vérification des pièces jointes...") # Log détaillé sur l'agent Image Sorter if self.image_sorter: agent_info = self._get_agent_info(self.image_sorter) logger.info(f"Agent Image Sorter: {json.dumps(agent_info, indent=2)}") # Compter le nombre d'images images = [f for f in os.listdir(attachments_dir) if f.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif'))] image_paths = [os.path.join(attachments_dir, img) for img in images] image_paths_uniques = filtrer_images_uniques(image_paths) images = [os.path.basename(p) for p in image_paths_uniques] images_count = len(images) # Tri des images for img in images: img_path = os.path.join(attachments_dir, img) if self.image_sorter: logger.info(f"Évaluation de la pertinence de l'image: {img}") print(f" Évaluation de l'image: {img}") sorting_result = self.image_sorter.executer(img_path) is_relevant = sorting_result.get("is_relevant", False) reason = sorting_result.get("reason", "") # Log détaillé du résultat if is_relevant: logger.info(f"Image {img} considérée comme pertinente") else: logger.info(f"Image {img} considérée comme non pertinente") # Ajouter les métadonnées de tri à la liste des analyses images_analyses[img_path] = { "sorting": sorting_result, "analysis": None # Sera rempli plus tard si pertinent } if is_relevant: logger.info(f"Image pertinente identifiée: {img} ({reason})") print(f" => Pertinente: {reason}") relevant_images.append(img_path) else: logger.info(f"Image non pertinente: {img} ({reason})") print(f" => Non pertinente: {reason}") else: logger.warning("Image Sorter non disponible") # Si pas de tri, considérer toutes les images comme pertinentes relevant_images.append(img_path) images_analyses[img_path] = { "sorting": {"is_relevant": True, "reason": "Auto-sélectionné (pas de tri)"}, "analysis": None } print(f" => Auto-sélectionné (pas de tri)") logger.info(f"Images analysées: {images_count}, Images pertinentes: {len(relevant_images)}") print(f" Images analysées: {images_count}, Images pertinentes: {len(relevant_images)}") else: logger.warning(f"Répertoire des pièces jointes non trouvé: {attachments_dir}") print(f" Répertoire des pièces jointes non trouvé") # Analyse approfondie des images pertinentes if relevant_images and self.image_analyser: agent_info = self._get_agent_info(self.image_analyser) logger.info(f"Agent Image Analyser: {json.dumps(agent_info, indent=2)}") # S'assurer que l'analyse du ticket est disponible comme contexte contexte_ticket = ticket_analysis if ticket_analysis else "Aucune analyse de ticket disponible" # Analyse de chaque image pertinente for image_path in relevant_images: image_name = os.path.basename(image_path) logger.info(f"Analyse approfondie de l'image: {image_name}") print(f" Analyse approfondie de l'image: {image_name}") # Appeler l'analyseur d'images avec le contexte du ticket analysis_result = self.image_analyser.executer(image_path, contexte=contexte_ticket) if images_analyses[image_path]: images_analyses[image_path]["analysis"] = analysis_result logger.info(f"Analyse complétée pour {image_name}") # Préparer les données pour le rapport final rapport_data = { "ticket_data": ticket_data, "ticket_id": ticket_id, "ticket_analyse": ticket_analysis, "analyse_images": images_analyses, "metadata": { "timestamp_debut": self._get_timestamp(), "ticket_id": ticket_id, "images_analysees": images_count, "images_pertinentes": len(relevant_images) } } # Génération du rapport final if self.report_generator: logger.info("Génération du rapport final") print(" Génération du rapport final") # Log détaillé sur l'agent Report Generator agent_info = self._get_agent_info(self.report_generator) logger.info(f"Agent Report Generator: {json.dumps(agent_info, indent=2)}") # Créer le répertoire pour le rapport dans reports/ project_root = os.path.abspath(os.path.join(os.path.dirname(__file__))) reports_root_dir = os.path.join(project_root, 'reports') ticket_reports_dir = os.path.join(reports_root_dir, ticket_id) # Créer le sous-répertoire pour le modèle spécifique model_name = getattr(self.report_generator.llm, "modele", str(type(self.report_generator.llm))) model_reports_dir = os.path.join(ticket_reports_dir, model_name) os.makedirs(model_reports_dir, exist_ok=True) # Générer le rapport json_path, md_path = self.report_generator.executer(rapport_data, model_reports_dir) if json_path: logger.info(f"Rapport JSON généré à: {json_path}") print(f" Rapport JSON généré avec succès: {os.path.basename(json_path)}") # Utiliser directement le rapport Markdown généré par l'agent if md_path: logger.info(f"Rapport Markdown généré à: {md_path}") print(f" Rapport Markdown généré avec succès: {os.path.basename(md_path)}") else: logger.warning("Report Generator non disponible") print(" Report Generator non disponible, génération de rapport ignorée") print(f"Traitement du ticket {os.path.basename(ticket_path)} terminé avec succès.\n") logger.info(f"Traitement du ticket {ticket_path} terminé avec succès.") else: logger.warning(f"Aucune donnée de ticket trouvée pour: {ticket_id}") print(f" ERREUR: Aucune donnée de ticket trouvée pour {ticket_id}") if not extractions_trouvees: logger.warning(f"Aucune extraction trouvée dans le ticket: {ticket_path}") print(f" ERREUR: Aucune extraction trouvée dans le ticket") return success def _preparer_donnees_ticket(self, rapports: Dict[str, Optional[str]], ticket_id: str) -> Optional[Dict]: """ Prépare les données du ticket à partir du rapport trouvé (JSON) """ ticket_data = None # Charger le fichier JSON si le chemin existe json_path = rapports.get("json") if json_path is not None: try: ticket_data = self.ticket_loader.charger(json_path) logger.info(f"Données JSON chargées depuis: {json_path}") print(f" Rapport JSON chargé: {os.path.basename(json_path)}") # Ajouter une métadonnée sur le format source if "metadata" not in ticket_data: ticket_data["metadata"] = {} ticket_data["metadata"]["format_source"] = "json" except Exception as e: logger.error(f"Erreur lors du chargement du JSON: {e}") print(f" ERREUR: Impossible de charger le fichier JSON: {e}") return None else: logger.warning(f"Aucun fichier JSON trouvé pour le ticket {ticket_id}") print(f" ERREUR: Aucun fichier JSON trouvé pour le ticket {ticket_id}") return None def _get_timestamp(self) -> str: """Retourne un timestamp au format YYYYMMDD_HHMMSS""" from datetime import datetime return datetime.now().strftime("%Y%m%d_%H%M%S") def _get_agent_info(self, agent: Optional[BaseAgent]) -> Dict: """ Récupère les informations détaillées sur un agent. Args: agent: L'agent dont on veut récupérer les informations Returns: Dictionnaire contenant les informations de l'agent """ if not agent: return {"status": "non configuré"} # Récupérer les informations du modèle model_info = { "nom": agent.nom, "model": getattr(agent.llm, "modele", str(type(agent.llm))), } # Ajouter les paramètres de configuration s'ils sont disponibles directement dans l'agent # Utiliser getattr avec une valeur par défaut pour éviter les erreurs model_info["temperature"] = getattr(agent, "temperature", None) model_info["top_p"] = getattr(agent, "top_p", None) model_info["max_tokens"] = getattr(agent, "max_tokens", None) # Ajouter le prompt système s'il est disponible if hasattr(agent, "system_prompt"): prompt_preview = getattr(agent, "system_prompt", "") # Tronquer le prompt s'il est trop long if prompt_preview and len(prompt_preview) > 200: prompt_preview = prompt_preview[:200] + "..." model_info["system_prompt_preview"] = prompt_preview # Supprimer les valeurs None model_info = {k: v for k, v in model_info.items() if v is not None} return model_info