llm_ticket3/orchestrator.py
2025-04-07 14:30:24 +02:00

563 lines
26 KiB
Python

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