llm_ticket3/agents/llama_vision/agent_report_generator.py
2025-04-21 16:56:29 +02:00

273 lines
14 KiB
Python

from ..base_agent import BaseAgent
from typing import Dict, Any
import logging
import os
import json
import traceback
from datetime import datetime
from ..utils.pipeline_logger import sauvegarder_donnees
# Configuration du logger pour plus de détails
logger = logging.getLogger("AgentReportGenerator")
logger.setLevel(logging.DEBUG) # Augmenter le niveau de logging pour le débogage
class AgentReportGenerator(BaseAgent):
def __init__(self, llm):
super().__init__("AgentReportGenerator", llm)
self.params = {
"temperature": 0.2,
"top_p": 0.8,
"max_tokens": 8000
}
self.system_prompt = """Tu es un expert en support technique chargé de générer un rapport final à partir des analyses d'un ticket de support.
Ton rôle est de croiser les informations provenant :
- de l'analyse textuelle du ticket client
- des analyses détaillées de plusieurs captures d'écran
Tu dois structurer ta réponse en format question/réponse de manière claire, en gardant l'intégralité des points importants.
Ne propose jamais de solution. Ne reformule pas le contexte.
Ta seule mission est de croiser les données textuelles et visuelles et d'en tirer des observations claires, en listant les éléments factuels visibles dans les captures qui appuient ou complètent le texte du ticket.
Structure du rapport attendu :
1. Contexte général (résumé du ticket textuel en une phrase)
2. Problèmes ou questions identifiés (sous forme de questions claires)
3. Résumé croisé image/texte pour chaque question
4. Liste d'observations supplémentaires pertinentes (si applicable)
5. Tableau chronologique d'échanges
- Inclure un tableau structuré des échanges entre client et support
- Format : Émetteur | Type | Date | Contenu | Éléments visuels pertinents
- Ne pas mentionner les noms réels des personnes, utiliser "CLIENT" et "SUPPORT"
- Synthétiser le contenu tout en conservant les informations importantes
- Associer les éléments visuels des captures d'écran aux échanges correspondants
Règles pour le tableau d'échanges :
- TYPE peut être : question, réponse, information, complément visuel
- Pour chaque échange du client mentionnant un problème, ajoute les éléments visuels des captures qui contextualisent ce problème
- Pour chaque réponse du support, ajoute les éléments visuels qui confirment ou infirment la réponse
- N'invente aucun contenu ni aucune date
- Utilise les données factuelles des images pour enrichir la compréhension des échanges
Reste strictement factuel. Ne fais aucune hypothèse. Ne suggère pas d'étapes ni d'interprétation."""
self._appliquer_config_locale()
logger.info("AgentReportGenerator initialisé")
def _appliquer_config_locale(self) -> None:
if hasattr(self.llm, "prompt_system"):
self.llm.prompt_system = self.system_prompt
if hasattr(self.llm, "configurer"):
self.llm.configurer(**self.params)
def _verifier_donnees_entree(self, rapport_data: Dict[str, Any]) -> bool:
"""
Vérifie que les données d'entrée contiennent les éléments nécessaires.
Args:
rapport_data: Données pour générer le rapport
Returns:
bool: True si les données sont valides, False sinon
"""
ticket_id = rapport_data.get("ticket_id")
if not ticket_id:
logger.error("Erreur de validation: ticket_id manquant")
return False
ticket_analyse = rapport_data.get("ticket_analyse")
if not ticket_analyse:
logger.error(f"Erreur de validation pour {ticket_id}: analyse de ticket manquante")
return False
analyses_images = rapport_data.get("analyse_images", {})
if not analyses_images:
logger.warning(f"Avertissement pour {ticket_id}: aucune analyse d'image disponible")
# On continue quand même car on peut générer un rapport sans images
# Vérifier si au moins une image a été analysée
images_analysees = 0
for img_path, img_data in analyses_images.items():
if img_data.get("analysis") and img_data["analysis"].get("analyse"):
images_analysees += 1
if images_analysees == 0 and analyses_images:
logger.warning(f"Avertissement pour {ticket_id}: {len(analyses_images)} images trouvées mais aucune n'a été analysée")
logger.info(f"Validation pour {ticket_id}: OK, {images_analysees} images analysées sur {len(analyses_images)} images")
return True
def executer(self, rapport_data: Dict[str, Any]) -> str:
ticket_id = rapport_data.get("ticket_id", "Inconnu")
print(f"AgentReportGenerator : génération du rapport pour le ticket {ticket_id}")
try:
# Vérifier et enregistrer les données d'entrée pour le débogage
logger.debug(f"Données reçues pour {ticket_id}: {json.dumps(rapport_data, default=str)[:500]}...")
# Vérifier que les données d'entrée sont valides
if not self._verifier_donnees_entree(rapport_data):
error_msg = f"Impossible de générer le rapport: données d'entrée invalides pour {ticket_id}"
print(f"ERREUR: {error_msg}")
return f"ERREUR: {error_msg}"
print(f"Préparation du prompt pour le ticket {ticket_id}...")
prompt = self._generer_prompt(rapport_data)
logger.debug(f"Prompt généré ({len(prompt)} caractères): {prompt[:500]}...")
print(f"Analyse en cours pour le ticket {ticket_id}...")
response = self.llm.interroger(prompt)
print(f"Analyse terminée: {len(response)} caractères")
logger.debug(f"Réponse reçue ({len(response)} caractères): {response[:500]}...")
# Création du résultat complet avec métadonnées
result = {
"prompt": prompt,
"response": response,
"metadata": {
"ticket_id": ticket_id,
"timestamp": self._get_timestamp(),
"source_agent": self.nom,
"model_info": {
"model": getattr(self.llm, "modele", str(type(self.llm))),
**getattr(self.llm, "params", {})
}
}
}
# Sauvegarder le résultat dans le pipeline
logger.info(f"Sauvegarde du rapport final pour le ticket {ticket_id}")
# Créer un fichier de débogage direct pour vérifier le problème de sauvegarde
try:
debug_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../debug"))
os.makedirs(debug_dir, exist_ok=True)
debug_path = os.path.join(debug_dir, f"rapport_debug_{ticket_id}.json")
with open(debug_path, "w", encoding="utf-8") as f:
json.dump(result, f, ensure_ascii=False, indent=2)
print(f"Fichier de débogage créé: {debug_path}")
except Exception as e:
print(f"Erreur lors de la création du fichier de débogage: {str(e)}")
# Utiliser uniquement la fonction standard de sauvegarde
try:
sauvegarder_donnees(ticket_id, "rapport_final", result, base_dir=None, is_resultat=True)
print(f"Rapport final généré et sauvegardé pour le ticket {ticket_id}")
except Exception as e:
logger.error(f"Erreur lors de la sauvegarde via sauvegarder_donnees: {str(e)}")
print(f"Erreur de sauvegarde standard: {str(e)}")
# Sauvegarder aussi une version en texte brut pour faciliter la lecture
try:
# Trouver le chemin de ticket le plus récent
output_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../output"))
ticket_dir = os.path.join(output_dir, f"ticket_{ticket_id}")
if os.path.exists(ticket_dir):
# Trouver l'extraction la plus récente
extractions = [d for d in os.listdir(ticket_dir) if os.path.isdir(os.path.join(ticket_dir, d)) and d.startswith(ticket_id)]
if extractions:
extractions.sort(reverse=True)
latest_extraction = extractions[0]
rapports_dir = os.path.join(ticket_dir, latest_extraction, f"{ticket_id}_rapports")
pipeline_dir = os.path.join(rapports_dir, "pipeline")
# S'assurer que le répertoire existe
os.makedirs(pipeline_dir, exist_ok=True)
# Sauvegarder en format texte uniquement
rapport_txt_path = os.path.join(pipeline_dir, f"rapport_final_mistral-large-latest.txt")
with open(rapport_txt_path, "w", encoding="utf-8") as f:
f.write(f"RAPPORT D'ANALYSE DU TICKET {ticket_id}\n")
f.write("="*50 + "\n\n")
f.write(response)
print(f"Version texte sauvegardée dans: {rapport_txt_path}")
except Exception as e:
print(f"Erreur lors de la sauvegarde de la version texte: {str(e)}")
logger.error(f"Erreur de sauvegarde texte: {str(e)}")
# Sauvegarder aussi dans le dossier reports pour compatibilité
try:
reports_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../reports"))
ticket_reports_dir = os.path.join(reports_dir, ticket_id)
os.makedirs(ticket_reports_dir, exist_ok=True)
# Sauvegarder aussi en texte pour faciliter la lecture
rapport_txt_path = os.path.join(ticket_reports_dir, f"rapport_final_{ticket_id}.txt")
with open(rapport_txt_path, "w", encoding="utf-8") as f:
f.write(f"RAPPORT D'ANALYSE DU TICKET {ticket_id}\n")
f.write("="*50 + "\n\n")
f.write(response)
logger.info(f"Rapport texte sauvegardé dans {rapport_txt_path}")
print(f"Rapport également sauvegardé en texte dans {ticket_reports_dir}")
except Exception as e:
logger.warning(f"Impossible de sauvegarder le rapport texte dans reports/: {str(e)}")
print(f"Erreur de sauvegarde reports/: {str(e)}")
# Ajouter à l'historique
self.ajouter_historique("rapport_final", {
"ticket_id": ticket_id,
"prompt": prompt,
"timestamp": self._get_timestamp()
}, response)
print(f"Traitement du rapport terminé pour le ticket {ticket_id}")
return response
except Exception as e:
logger.error(f"Erreur lors de la génération du rapport : {str(e)}")
logger.error(traceback.format_exc())
print(f"ERREUR CRITIQUE lors de la génération du rapport: {str(e)}")
return f"ERREUR: {str(e)}"
def _generer_prompt(self, rapport_data: Dict[str, Any]) -> str:
ticket_text = rapport_data.get("ticket_analyse", "")
image_blocs = []
analyses_images = rapport_data.get("analyse_images", {})
# Ajouter des logs pour vérifier les données d'images
logger.info(f"Nombre d'images à analyser: {len(analyses_images)}")
for chemin_image, analyse_obj in analyses_images.items():
# Vérifier si l'image est pertinente
is_relevant = analyse_obj.get("sorting", {}).get("is_relevant", False)
# Récupérer l'analyse si elle existe
analyse = ""
if "analysis" in analyse_obj and analyse_obj["analysis"]:
analyse = analyse_obj["analysis"].get("analyse", "")
if analyse:
image_blocs.append(f"--- IMAGE : {os.path.basename(chemin_image)} ---\n{analyse}\n")
logger.info(f"Ajout de l'analyse de l'image {os.path.basename(chemin_image)} ({len(analyse)} caractères)")
else:
logger.warning(f"Image {os.path.basename(chemin_image)} sans analyse")
bloc_images = "\n".join(image_blocs)
# Log pour vérifier la taille des données
logger.info(f"Taille de l'analyse ticket: {len(ticket_text)} caractères")
logger.info(f"Taille du bloc images: {len(bloc_images)} caractères")
prompt = (
f"Voici les données d'analyse pour un ticket de support :\n\n"
f"=== ANALYSE DU TICKET ===\n{ticket_text}\n\n"
f"=== ANALYSES D'IMAGES ===\n{bloc_images}\n\n"
f"Génère un rapport croisé en suivant les instructions précédentes, incluant un tableau chronologique des échanges entre CLIENT et SUPPORT. "
f"Utilise le format suivant pour le tableau :\n"
f"| ÉMETTEUR | TYPE | DATE | CONTENU | ÉLÉMENTS VISUELS |\n"
f"| --- | --- | --- | --- | --- |\n"
f"| CLIENT | question | date | texte de la question | éléments pertinents des images |\n"
f"| SUPPORT | réponse | date | texte de la réponse | éléments pertinents des images |\n\n"
f"Ce tableau doit synthétiser les échanges tout en intégrant les données pertinentes des images avec le maximum de contexte technique."
)
return prompt
def _get_timestamp(self) -> str:
from datetime import datetime
return datetime.now().strftime("%Y%m%d_%H%M%S")