import os import json import logging import time import traceback from typing import List, Dict, Any, Optional, Union from agents.base_agent import BaseAgent # 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: def __init__(self, output_dir: str = "output/", json_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.json_agent = json_agent self.image_sorter = image_sorter self.image_analyser = image_analyser self.report_generator = report_generator # 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: JSON={json_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 JSON if self.json_agent: agents_info["json_agent"] = self._get_agent_info(self.json_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_path) logger.info(f"Tickets trouvés: {len(tickets)}") print(f"Tickets détectés: {len(tickets)}") return tickets def lister_tickets(self) -> Dict[int, str]: """Liste les tickets disponibles et retourne un dictionnaire {index: chemin}""" tickets = self.detecter_tickets() ticket_dict = {} print("\nTickets disponibles:") for i, ticket_path in enumerate(tickets, 1): ticket_id = os.path.basename(ticket_path) ticket_dict[i] = ticket_path print(f"{i}. {ticket_id}") return ticket_dict def trouver_rapport(self, extraction_path: str, ticket_id: str) -> Dict[str, Optional[str]]: """ Cherche le rapport du ticket dans différents emplacements possibles (JSON ou MD) Args: extraction_path: Chemin de l'extraction ticket_id: ID du ticket (ex: T0101) Returns: Un dictionnaire avec les chemins des fichiers JSON et MD s'ils sont trouvés """ result: Dict[str, Optional[str]] = {"json_path": None, "md_path": None} # Liste des emplacements possibles pour les rapports possible_locations = [ # 1. Dans le répertoire d'extraction directement extraction_path, # 2. Dans un sous-répertoire "data" os.path.join(extraction_path, "data"), # 3. Dans un sous-répertoire spécifique au ticket pour les rapports os.path.join(extraction_path, f"{ticket_id}_rapports"), # 4. Dans un sous-répertoire "rapports" os.path.join(extraction_path, "rapports") ] # Vérifier chaque emplacement for base_location in possible_locations: # Chercher le fichier JSON json_path = os.path.join(base_location, f"{ticket_id}_rapport.json") if os.path.exists(json_path): logger.info(f"Rapport JSON trouvé à: {json_path}") result["json_path"] = json_path # Chercher le fichier Markdown md_path = os.path.join(base_location, f"{ticket_id}_rapport.md") if os.path.exists(md_path): logger.info(f"Rapport Markdown trouvé à: {md_path}") result["md_path"] = md_path if not result["json_path"] and not result["md_path"]: logger.warning(f"Aucun rapport trouvé pour {ticket_id} dans {extraction_path}") return result 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 JSON avec l'agent JSON if self.json_agent: logger.info("Exécution de l'agent JSON") print(" Analyse du ticket en cours...") # Log détaillé sur l'agent JSON agent_info = self._get_agent_info(self.json_agent) logger.info(f"Agent JSON: {json.dumps(agent_info, indent=2)}") json_analysis = self.json_agent.executer(ticket_data) logger.info("Analyse JSON terminée") else: logger.warning("Agent JSON non disponible") json_analysis = None print(" Agent JSON non disponible, analyse ignorée") # Traitement des images relevant_images = [] images_analyses = {} 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)}") images_count = 0 for attachment in os.listdir(attachments_dir): attachment_path = os.path.join(attachments_dir, attachment) if attachment.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif')): images_count += 1 if self.image_sorter: print(f" Analyse de l'image: {attachment}") sorting_result = self.image_sorter.executer(attachment_path) is_relevant = sorting_result.get("is_relevant", False) reason = sorting_result.get("reason", "") # Ajouter les métadonnées de tri à la liste des analyses images_analyses[attachment_path] = { "sorting": sorting_result, "analysis": None # Sera rempli plus tard si pertinent } if is_relevant: logger.info(f"Image pertinente identifiée: {attachment} ({reason})") print(f" => Pertinente: {reason}") relevant_images.append(attachment_path) else: logger.info(f"Image non pertinente: {attachment} ({reason})") print(f" => Non pertinente: {reason}") else: logger.warning("Image Sorter non disponible") 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 # Log détaillé sur l'agent Image Analyser if self.image_analyser: agent_info = self._get_agent_info(self.image_analyser) logger.info(f"Agent Image Analyser: {json.dumps(agent_info, indent=2)}") for image_path in relevant_images: if self.image_analyser and json_analysis: 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}") analysis_result = self.image_analyser.executer(image_path, contexte=json_analysis) # Ajouter l'analyse au dictionnaire des analyses d'images if image_path in images_analyses: images_analyses[image_path]["analysis"] = analysis_result else: images_analyses[image_path] = { "sorting": {"is_relevant": True, "reason": "Auto-sélectionné"}, "analysis": analysis_result } logger.info(f"Analyse complétée pour {image_name}") # Génération du rapport final rapport_data = { "ticket_data": ticket_data, "ticket_id": ticket_id, "analyse_json": json_analysis, "analyse_images": images_analyses, "metadata": { "timestamp_debut": self._get_timestamp(), "ticket_id": ticket_id, "images_analysees": images_count if 'images_count' in locals() else 0, "images_pertinentes": len(relevant_images) } } 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)}") rapport_path = os.path.join(rapports_dir, ticket_id) os.makedirs(rapport_path, exist_ok=True) self.report_generator.executer(rapport_data, rapport_path) logger.info(f"Rapport généré à: {rapport_path}") print(f" Rapport généré à: {rapport_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 des rapports trouvés (JSON et/ou MD) Args: rapports: Dictionnaire avec les chemins des rapports JSON et MD ticket_id: ID du ticket Returns: Dictionnaire avec les données du ticket, ou None si aucun rapport n'est trouvé """ ticket_data = None # Essayer d'abord le fichier JSON if rapports["json_path"]: try: with open(rapports["json_path"], 'r', encoding='utf-8') as file: ticket_data = json.load(file) logger.info(f"Données JSON chargées depuis: {rapports['json_path']}") print(f" Rapport JSON chargé: {os.path.basename(rapports['json_path'])}") except Exception as e: logger.error(f"Erreur lors du chargement du JSON: {e}") print(f" ERREUR: Impossible de charger le fichier JSON: {e}") # Si pas de JSON ou erreur, essayer le Markdown if not ticket_data and rapports["md_path"]: try: # Créer une structure de données à partir du contenu Markdown ticket_data = self._extraire_donnees_de_markdown(rapports["md_path"]) logger.info(f"Données Markdown chargées depuis: {rapports['md_path']}") print(f" Rapport Markdown chargé: {os.path.basename(rapports['md_path'])}") except Exception as e: logger.error(f"Erreur lors du chargement du Markdown: {e}") print(f" ERREUR: Impossible de charger le fichier Markdown: {e}") # Assurer que l'ID du ticket est correct if ticket_data: ticket_data["code"] = ticket_id return ticket_data def _extraire_donnees_de_markdown(self, md_path: str) -> Dict: """ Extrait les données d'un fichier Markdown et les structure Args: md_path: Chemin vers le fichier Markdown Returns: Dictionnaire structuré avec les données du ticket """ with open(md_path, 'r', encoding='utf-8') as file: content = file.read() # Initialiser la structure de données ticket_data = { "id": "", "code": "", "name": "", "description": "", "messages": [], "metadata": { "source_file": md_path, "format": "markdown" } } # Extraire le titre (première ligne) lines = content.split('\n') if lines and lines[0].startswith('# '): title = lines[0].replace('# ', '') ticket_parts = title.split(':') if len(ticket_parts) >= 1: ticket_data["code"] = ticket_parts[0].strip() if len(ticket_parts) >= 2: ticket_data["name"] = ticket_parts[1].strip() # Extraire la description description_section = self._extraire_section(content, "description") if description_section: ticket_data["description"] = description_section.strip() # Extraire les informations du ticket info_section = self._extraire_section(content, "Informations du ticket") if info_section: for line in info_section.split('\n'): if ':' in line and line.startswith('- **'): key = line.split('**')[1].strip() value = line.split(':')[1].strip() if key == "id": ticket_data["id"] = value elif key == "code": ticket_data["code"] = value # Ajouter d'autres champs au besoin # Extraire les messages messages_section = self._extraire_section(content, "Messages") if messages_section: message_blocks = messages_section.split("### Message ") for block in message_blocks[1:]: # Ignorer le premier élément (vide) message = {} # Extraire les en-têtes du message lines = block.split('\n') for i, line in enumerate(lines): if line.startswith('**') and ':' in line: key = line.split('**')[1].lower() value = line.split(':')[1].strip() message[key] = value # Extraire le contenu du message (tout ce qui n'est pas un en-tête) content_start = 0 for i, line in enumerate(lines): if i > 0 and not line.startswith('**') and line and content_start == 0: content_start = i break if content_start > 0: content_end = -1 for i in range(content_start, len(lines)): if lines[i].startswith('**attachment_ids**') or lines[i].startswith('---'): content_end = i break if content_end == -1: message["content"] = "\n".join(lines[content_start:]) else: message["content"] = "\n".join(lines[content_start:content_end]) # Extraire les pièces jointes attachments = [] for line in lines: if line.startswith('- ') and '[ID:' in line: attachments.append(line.strip('- ').strip()) if attachments: message["attachments"] = attachments ticket_data["messages"].append(message) return ticket_data def _extraire_section(self, content: str, section_title: str) -> Optional[str]: """ Extrait une section du contenu Markdown Args: content: Contenu Markdown complet section_title: Titre de la section à extraire Returns: Contenu de la section ou None si non trouvée """ import re # Chercher les sections de niveau 2 (##) pattern = r'## ' + re.escape(section_title) + r'\s*\n(.*?)(?=\n## |$)' match = re.search(pattern, content, re.DOTALL) if match: return match.group(1).strip() # Si pas trouvé, chercher les sections de niveau 3 (###) pattern = r'### ' + re.escape(section_title) + r'\s*\n(.*?)(?=\n### |$)' match = re.search(pattern, content, re.DOTALL) if match: return match.group(1).strip() return None def executer(self, ticket_specifique: Optional[str] = None): """ Exécute l'orchestrateur soit sur un ticket spécifique, soit permet de choisir Args: ticket_specifique: Chemin du ticket spécifique à traiter (optionnel) """ start_time = time.time() # Stocker le ticket spécifique self.ticket_specifique = ticket_specifique # Obtenir la liste des tickets if ticket_specifique: # Utiliser juste le ticket spécifique ticket_dirs = self.detecter_tickets() ticket_dirs = [t for t in ticket_dirs if t.endswith(ticket_specifique)] logger.info(f"Ticket spécifique à traiter: {ticket_specifique}") else: # Lister tous les tickets ticket_dirs = 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") 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: if ticket_specifique and not ticket_dir.endswith(ticket_specifique): continue 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 _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. """ 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