llm_ticket3/agents/agent_report_generator_bis.py
2025-04-11 08:45:52 +02:00

479 lines
21 KiB
Python

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_bis import extraire_et_traiter_json
from .utils.report_formatter import extraire_sections_texte, generer_rapport_markdown, construire_rapport_json
logger = logging.getLogger("AgentReportGeneratorBis")
class AgentReportGeneratorBis(BaseAgent):
"""
Agent pour générer un rapport synthétique à partir des analyses de ticket et d'images.
Version optimisée pour les modèles Qwen et DeepSeek.
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__("AgentReportGeneratorBis", llm)
# Configuration locale de l'agent
self.temperature = 0.2
self.top_p = 0.9
self.max_tokens = 20000
# 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 - Tu dois OBLIGATOIREMENT suivre cette structure dans ton rapport:
1. Un résumé du problème initial (nom de la demande + description)
2. Une analyse détaillée des images pertinentes en lien avec le problème
3. UNE SECTION EXACTEMENT INTITULÉE "## Synthèse globale des analyses d'images" (SECTION OBLIGATOIRE)
4. Une reconstitution du fil de discussion client/support
5. Un tableau des informations essentielles dans ce format JSON EXACT:
```json
{
"chronologie_echanges": [
{"date": "date exacte", "emetteur": "CLIENT ou SUPPORT", "type": "Question ou Réponse ou Information technique", "contenu": "contenu synthétisé fidèlement"}
]
}
```
6. Un diagnostic technique des causes probables
CRUCIAL - RESPECTE SCRUPULEUSEMENT CET ORDRE:
- ANALYSE D'ABORD chaque image individuellement en profondeur
- CRÉE ENSUITE une section dédiée "## Synthèse globale des analyses d'images" (UTILISE EXACTEMENT CE TITRE)
- ENFIN, construis le tableau chronologique des échanges en intégrant cette vision globale
INSTRUCTIONS POUR L'ANALYSE DES IMAGES:
- Pour chaque image, examine attentivement:
* Les éléments surlignés ou encadrés
* La relation avec le problème décrit
* Le lien avec la discussion client/support
- OBLIGATOIRE: CRÉE UNE SECTION AVEC LE TITRE EXACT "## Synthèse globale des analyses d'images" qui:
* Replace les images dans leur contexte chronologique
* Combine les éléments importants de toutes les images
* Explique comment ces éléments interagissent pour résoudre le problème
* Illustre le processus complet à travers les différentes captures d'écran
INSTRUCTIONS POUR LE TABLEAU JSON:
- COMMENCE par inclure les questions du NOM DE LA DEMANDE et de la DESCRIPTION
- CONSERVE TOUS les liens techniques importants (manuels, FAQ, documentation)
- ASSURE-TOI d'inclure ce tableau au format JSON PRÉCIS:
```json
{
"chronologie_echanges": [
{"date": "date exacte", "emetteur": "CLIENT ou SUPPORT", "type": "Question ou Réponse ou Information technique", "contenu": "contenu synthétisé fidèlement"}
]
}
```
- IMPORTANT: Ce JSON sera automatiquement extrait pour générer un CSV
- Le JSON DOIT être correctement formaté avec guillemets doubles et échappements appropriés
- Assure-toi que CHAQUE élément contient TOUTES les clés: date, emetteur, type, contenu
- Si une date exacte est inconnue, utilise la date d'ouverture du ticket
- N'OUBLIE PAS d'inclure un élément qui synthétise l'apport des images
TA MÉTHODE DE TRAVAIL:
1. Analyse d'abord le ticket et extrais toutes les questions et références
2. Analyse ensuite chaque image individuellement
3. Crée une section EXACTEMENT NOMMÉE "## Synthèse globale des analyses d'images"
4. Reconstruit le fil chronologique complet des échanges
5. Génère le tableau JSON en respectant SCRUPULEUSEMENT le format demandé
6. Conclue avec un diagnostic technique
GARANTIS ABSOLUMENT:
- La présence d'une section dédiée "## Synthèse globale des analyses d'images" avec EXACTEMENT ce titre
- La création d'un JSON correctement formaté avec toutes les clés requises
- La conservation de tous les liens importants dans le contenu du tableau
- Le respect de l'ordre des sections tel que défini ci-dessus
- Ne modifie JAMAIS les titres des sections demandées"""
# Version du prompt pour la traçabilité
self.prompt_version = "v3.1-qwen-deepseek"
# Appliquer la configuration au LLM
self._appliquer_config_locale()
logger.info("AgentReportGeneratorBis initialisé avec prompt optimisé pour Qwen/DeepSeek")
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 uniquement les données à transmettre au prompt système
sans répéter les instructions déjà présentes dans le system_prompt.
"""
num_images = len(images_analyses)
logger.info(f"Formatage du prompt avec {num_images} analyses d'images")
# Formater simplement l'analyse du ticket
prompt = f"""## ANALYSE DU TICKET\n{ticket_analyse}\n"""
# Ajouter seulement les analyses des images, sans instructions supplémentaires
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"
# Fin : pas d'instructions, déjà dans system_prompt !
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"AgentReportGeneratorBis: 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 (version optimisée pour Qwen/DeepSeek)...")
# 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)
# 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)
# 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"),
"prompt_version": self.prompt_version,
"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)
logger.info(f"Rapport JSON sauvegardé: {json_path}")
print(f" Rapport JSON sauvegardé: {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
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 _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