mirror of
https://github.com/Ladebeze66/llm_ticket3.git
synced 2025-12-13 17:27:18 +01:00
1006 lines
48 KiB
Python
1006 lines
48 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 import extraire_et_traiter_json
|
|
|
|
logger = logging.getLogger("AgentReportGenerator")
|
|
|
|
class AgentReportGenerator(BaseAgent):
|
|
"""
|
|
Agent pour générer un rapport synthétique à partir des analyses de ticket et d'images.
|
|
|
|
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__("AgentReportGenerator", llm)
|
|
|
|
# Configuration locale de l'agent
|
|
self.temperature = 0.2
|
|
self.top_p = 0.9
|
|
self.max_tokens = 2500
|
|
|
|
# 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 - Ton rapport DOIT inclure:
|
|
1. Un résumé du problème initial (nom de la demande + description)
|
|
2. Une chronologie des échanges client/support sous forme de tableau précis avec cette structure:
|
|
```json
|
|
{
|
|
"chronologie_echanges": [
|
|
{"date": "date exacte", "emetteur": "CLIENT ou SUPPORT", "type": "Question ou Réponse ou Information technique", "contenu": "contenu synthétisé fidèlement"}
|
|
]
|
|
}
|
|
```
|
|
3. Une analyse des images pertinentes en lien avec le problème (OBLIGATOIRE)
|
|
4. Un diagnostic technique des causes probables
|
|
|
|
IMPORTANT:
|
|
- La structure doit être clairement divisée en sections avec des titres (## Résumé, ## Chronologie des échanges, ## Analyse des images, ## Diagnostic)
|
|
- Le tableau des échanges doit capturer TOUTES les interactions (questions et réponses) dans l'ordre chronologique
|
|
- Pour l'analyse des images, décris précisément comment chaque image illustre le problème ou la solution
|
|
- Si aucune image n'est fournie, tu DOIS l'indiquer explicitement dans la section "Analyse des images"
|
|
- Reste factuel et précis dans ton analyse"""
|
|
|
|
# Version du prompt pour la traçabilité
|
|
self.prompt_version = "v2.2"
|
|
|
|
# Appliquer la configuration au LLM
|
|
self._appliquer_config_locale()
|
|
|
|
logger.info("AgentReportGenerator initialisé")
|
|
|
|
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 le prompt pour la génération du rapport
|
|
|
|
Args:
|
|
ticket_analyse: Analyse du ticket
|
|
images_analyses: Liste des analyses d'images
|
|
|
|
Returns:
|
|
Prompt formaté pour le LLM
|
|
"""
|
|
num_images = len(images_analyses)
|
|
logger.info(f"Formatage du prompt avec {num_images} analyses d'images")
|
|
|
|
# Construire la section d'analyse du ticket
|
|
prompt = f"""Génère un rapport technique complet, en te basant sur les analyses suivantes.
|
|
|
|
## ANALYSE DU TICKET
|
|
{ticket_analyse}
|
|
"""
|
|
|
|
# Ajouter la section d'analyse des images si présente
|
|
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"
|
|
|
|
# Instructions pour le rapport
|
|
prompt += """
|
|
## INSTRUCTIONS POUR LE RAPPORT
|
|
|
|
1. TON RAPPORT DOIT AVOIR LA STRUCTURE SUIVANTE:
|
|
- Titre principal (# Rapport d'analyse: Nom du ticket)
|
|
- Résumé du problème (## Résumé du problème)
|
|
- Chronologie des échanges (## Chronologie des échanges)
|
|
- Analyse des images (## Analyse des images)
|
|
- Diagnostic technique (## Diagnostic technique)
|
|
|
|
2. DANS LA SECTION "CHRONOLOGIE DES ÉCHANGES":
|
|
- Commence par créer un objet JSON comme suit:
|
|
```json
|
|
{
|
|
"chronologie_echanges": [
|
|
{"date": "date exacte", "emetteur": "CLIENT", "type": "Question", "contenu": "contenu exact de la question"},
|
|
{"date": "date exacte", "emetteur": "SUPPORT", "type": "Réponse", "contenu": "contenu exact de la réponse"}
|
|
]
|
|
}
|
|
```
|
|
- Inclus TOUS les échanges, qu'ils soient des questions, des réponses ou des informations techniques
|
|
- Respecte strictement la chronologie des messages
|
|
- Synthétise le contenu sans perdre d'information importante
|
|
|
|
3. DANS LA SECTION "ANALYSE DES IMAGES":
|
|
- Si des images sont présentes, explique en détail ce qu'elles montrent et leur lien avec le problème
|
|
- Si aucune image n'est fournie, indique-le clairement mais conserve cette section
|
|
- Mentionne le nom des images et leur contexte dans les échanges
|
|
|
|
4. DANS LA SECTION "DIAGNOSTIC TECHNIQUE":
|
|
- Fournis une analyse claire des causes probables
|
|
- Explique comment la solution proposée répond au problème
|
|
- Si pertinent, mentionne les aspects techniques spécifiques
|
|
|
|
IMPORTANT: Ce rapport sera utilisé par des techniciens et des développeurs pour comprendre rapidement le problème et sa résolution. Il doit être clair, précis et structuré.
|
|
"""
|
|
|
|
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"AgentReportGenerator: 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...")
|
|
|
|
# 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)
|
|
|
|
# Extraire les sections textuelles (résumé, diagnostic)
|
|
resume, analyse_images, diagnostic = self._extraire_sections_texte(rapport_genere)
|
|
|
|
# Vérifier que l'analyse des images a été correctement extraite si des images sont présentes
|
|
if not analyse_images and len(images_analyses) > 0:
|
|
logger.warning("L'analyse des images n'a pas été correctement extraite alors que des images sont présentes")
|
|
|
|
# Tentative alternative d'extraction
|
|
try:
|
|
# 1. Chercher directement dans le rapport complet
|
|
match = re.search(r'## Analyse des images(.*?)(?=## Diagnostic|##|\Z)', rapport_genere, re.DOTALL)
|
|
if match:
|
|
analyse_images = match.group(1).strip()
|
|
logger.info(f"Analyse des images récupérée par extraction directe: {len(analyse_images)} caractères")
|
|
|
|
# 2. Si toujours vide, générer à partir des analyses individuelles
|
|
if not analyse_images:
|
|
img_analyses = []
|
|
for img in images_analyses:
|
|
img_name = img.get("image_name", "")
|
|
analyse = img.get("analyse", "")
|
|
if img_name and analyse:
|
|
img_analyses.append(f"### {img_name}")
|
|
img_analyses.append("")
|
|
img_analyses.append(analyse)
|
|
img_analyses.append("")
|
|
|
|
if img_analyses:
|
|
analyse_images = "\n".join(img_analyses)
|
|
logger.info(f"Analyse des images reconstruite depuis {len(images_analyses)} analyses individuelles")
|
|
except Exception as e:
|
|
logger.error(f"Erreur lors de la récupération alternative de l'analyse des images: {e}")
|
|
|
|
# 6. CRÉATION ET SAUVEGARDE DU RAPPORT JSON
|
|
json_path = os.path.join(rapport_dir, f"{ticket_id}_rapport_final.json")
|
|
|
|
rapport_json = {
|
|
"ticket_id": ticket_id,
|
|
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
"rapport_complet": rapport_genere,
|
|
"ticket_analyse": ticket_analyse,
|
|
"images_analyses": images_analyses,
|
|
"chronologie_echanges": echanges_json.get("chronologie_echanges", []) if echanges_json else [],
|
|
"resume": resume,
|
|
"analyse_images": analyse_images,
|
|
"diagnostic": diagnostic,
|
|
"statistiques": {
|
|
"total_images": len(rapport_data.get("analyse_images", {})),
|
|
"images_pertinentes": len(images_analyses),
|
|
"generation_time": generation_time
|
|
},
|
|
"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"),
|
|
"agents": agents_info
|
|
},
|
|
"prompts_utilisés": prompts_utilises,
|
|
"workflow": {
|
|
"etapes": [
|
|
{
|
|
"numero": 1,
|
|
"nom": "Analyse du ticket",
|
|
"agent": "AgentTicketAnalyser",
|
|
"description": "Extraction et analyse des informations du ticket"
|
|
},
|
|
{
|
|
"numero": 2,
|
|
"nom": "Tri des images",
|
|
"agent": "AgentImageSorter",
|
|
"description": "Identification des images pertinentes pour l'analyse"
|
|
},
|
|
{
|
|
"numero": 3,
|
|
"nom": "Analyse des images",
|
|
"agent": "AgentImageAnalyser",
|
|
"description": "Analyse détaillée des images pertinentes identifiées"
|
|
},
|
|
{
|
|
"numero": 4,
|
|
"nom": "Génération du rapport",
|
|
"agent": "AgentReportGenerator",
|
|
"description": "Synthèse des analyses et génération du rapport final"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
# Vérification finale des sections importantes
|
|
sections_manquantes = []
|
|
if not resume:
|
|
sections_manquantes.append("Résumé")
|
|
if not analyse_images and len(images_analyses) > 0:
|
|
sections_manquantes.append("Analyse des images")
|
|
if not diagnostic:
|
|
sections_manquantes.append("Diagnostic")
|
|
if not echanges_json or not echanges_json.get("chronologie_echanges"):
|
|
sections_manquantes.append("Chronologie des échanges")
|
|
|
|
if sections_manquantes:
|
|
logger.warning(f"Sections manquantes dans le rapport final: {', '.join(sections_manquantes)}")
|
|
print(f" ATTENTION: Sections manquantes: {', '.join(sections_manquantes)}")
|
|
else:
|
|
logger.info("Toutes les sections requises sont présentes dans le rapport")
|
|
|
|
# Sauvegarder le 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}")
|
|
|
|
# 7. GÉNÉRATION DU RAPPORT MARKDOWN
|
|
md_path = self._generer_rapport_markdown(json_path)
|
|
|
|
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 _extraire_sections_texte(self, rapport_genere: str) -> Tuple[str, str, str]:
|
|
"""
|
|
Extrait le résumé, l'analyse des images et le diagnostic du rapport généré
|
|
|
|
Args:
|
|
rapport_genere: Texte du rapport généré par le LLM
|
|
|
|
Returns:
|
|
Tuple (résumé, analyse_images, diagnostic)
|
|
"""
|
|
resume = ""
|
|
analyse_images = ""
|
|
diagnostic = ""
|
|
|
|
# Supprimer le bloc JSON pour analyser le texte restant
|
|
rapport_sans_json = re.sub(r'```json.*?```', '', rapport_genere, re.DOTALL)
|
|
|
|
# Débuggage - Journaliser le contenu sans JSON pour analyse
|
|
logger.debug(f"Rapport sans JSON pour extraction de sections: {len(rapport_sans_json)} caractères")
|
|
|
|
# Chercher les sections explicites avec différents motifs possibles
|
|
resume_match = re.search(r'(?:## Résumé du problème|## Résumé|# Résumé)(.*?)(?=##|\Z)', rapport_sans_json, re.DOTALL)
|
|
if resume_match:
|
|
resume = resume_match.group(1).strip()
|
|
logger.debug(f"Section résumé extraite: {len(resume)} caractères")
|
|
|
|
# Motifs plus larges pour l'analyse des images
|
|
analyse_images_patterns = [
|
|
r'## Analyse des images(.*?)(?=##|\Z)',
|
|
r'## Images(.*?)(?=##|\Z)',
|
|
r'### IMAGE.*?(?=##|\Z)'
|
|
]
|
|
|
|
for pattern in analyse_images_patterns:
|
|
analyse_images_match = re.search(pattern, rapport_sans_json, re.DOTALL)
|
|
if analyse_images_match:
|
|
analyse_images = analyse_images_match.group(1).strip()
|
|
logger.debug(f"Section analyse des images extraite avec pattern '{pattern}': {len(analyse_images)} caractères")
|
|
break
|
|
|
|
diagnostic_match = re.search(r'(?:## Diagnostic technique|## Diagnostic|## Cause du problème)(.*?)(?=##|\Z)', rapport_sans_json, re.DOTALL)
|
|
if diagnostic_match:
|
|
diagnostic = diagnostic_match.group(1).strip()
|
|
logger.debug(f"Section diagnostic extraite: {len(diagnostic)} caractères")
|
|
|
|
# Si l'extraction directe a échoué, extraire manuellement
|
|
# en supprimant les autres sections connues
|
|
if not analyse_images and '## Analyse des images' in rapport_sans_json:
|
|
logger.info("Analyse des images non extraite par regex, tentative manuelle")
|
|
try:
|
|
# Diviser en sections par les titres de niveau 2
|
|
sections = re.split(r'## ', rapport_sans_json)
|
|
for section in sections:
|
|
if section.startswith('Analyse des images') or section.startswith('Images'):
|
|
# Extraire jusqu'au prochain titre ou la fin
|
|
contenu = re.split(r'##|\Z', section, 1)[0].strip()
|
|
analyse_images = contenu.replace('Analyse des images', '').replace('Images', '').strip()
|
|
logger.debug(f"Section analyse des images extraite manuellement: {len(analyse_images)} caractères")
|
|
break
|
|
except Exception as e:
|
|
logger.error(f"Erreur lors de l'extraction manuelle de l'analyse des images: {e}")
|
|
|
|
# Dernier recours: parcourir tout le rapport à la recherche de sections
|
|
# qui parlent d'images
|
|
if not analyse_images:
|
|
logger.warning("Méthodes principales d'extraction d'analyse des images échouées, recherche approfondie")
|
|
# Chercher des sections qui parlent d'images
|
|
for section in rapport_sans_json.split('##'):
|
|
if any(mot in section.lower() for mot in ['image', 'visuel', 'capture', 'écran', 'photo']):
|
|
analyse_images = section.strip()
|
|
logger.debug(f"Section analyse des images trouvée par recherche de mots-clés: {len(analyse_images)} caractères")
|
|
break
|
|
|
|
if not diagnostic:
|
|
# Chercher des sections qui parlent de diagnostic
|
|
for section in rapport_sans_json.split('##'):
|
|
if any(mot in section.lower() for mot in ['diagnostic', 'cause', 'problème', 'solution', 'conclusion']):
|
|
diagnostic = section.strip()
|
|
logger.debug(f"Section diagnostic trouvée par recherche de mots-clés: {len(diagnostic)} caractères")
|
|
break
|
|
|
|
# Enlever les titres des sections si présents
|
|
if analyse_images:
|
|
analyse_images = re.sub(r'^Analyse des images[:\s]*', '', analyse_images)
|
|
analyse_images = re.sub(r'^Images[:\s]*', '', analyse_images)
|
|
|
|
if diagnostic:
|
|
diagnostic = re.sub(r'^Diagnostic(?:technique)?[:\s]*', '', diagnostic)
|
|
|
|
# Vérifier si les sections sont présentes et les journaliser
|
|
logger.info(f"Extraction des sections - Résumé: {bool(resume)}, Analyse images: {bool(analyse_images)}, Diagnostic: {bool(diagnostic)}")
|
|
|
|
# Si l'analyse des images est toujours vide mais existe dans le rapport complet,
|
|
# prendre toute la section complète
|
|
if not analyse_images and '## Analyse des images' in rapport_genere:
|
|
logger.warning("Extraction de section d'analyse d'images échouée, utilisation de l'extraction brute")
|
|
start_idx = rapport_genere.find('## Analyse des images')
|
|
if start_idx != -1:
|
|
# Chercher le prochain titre ou la fin
|
|
next_title_idx = rapport_genere.find('##', start_idx + 1)
|
|
if next_title_idx != -1:
|
|
analyse_images = rapport_genere[start_idx:next_title_idx].strip()
|
|
analyse_images = analyse_images.replace('## Analyse des images', '').strip()
|
|
else:
|
|
analyse_images = rapport_genere[start_idx:].strip()
|
|
analyse_images = analyse_images.replace('## Analyse des images', '').strip()
|
|
logger.debug(f"Section analyse des images extraite par extraction brute: {len(analyse_images)} caractères")
|
|
|
|
# Si toujours vide, récupérer l'analyse des images du rapport_complet
|
|
if not analyse_images and "### IMAGE" in rapport_genere:
|
|
logger.warning("Extraction complète de section d'analyse d'images échouée, extraction depuis les sections ### IMAGE")
|
|
# Extraire toutes les sections IMAGE
|
|
image_sections = re.findall(r'### IMAGE.*?(?=###|\Z)', rapport_genere, re.DOTALL)
|
|
if image_sections:
|
|
analyse_images = "\n\n".join(image_sections)
|
|
logger.debug(f"Analyse d'images extraite depuis les sections IMAGE: {len(analyse_images)} caractères")
|
|
|
|
return resume, analyse_images, diagnostic
|
|
|
|
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
|
|
|
|
def _generer_rapport_markdown(self, json_path: str) -> Optional[str]:
|
|
"""
|
|
Génère un rapport Markdown à partir du rapport JSON
|
|
|
|
Args:
|
|
json_path: Chemin du fichier JSON contenant le rapport
|
|
|
|
Returns:
|
|
Chemin du fichier Markdown généré ou None en cas d'erreur
|
|
"""
|
|
try:
|
|
# Charger le rapport JSON
|
|
with open(json_path, 'r', encoding='utf-8') as f:
|
|
rapport_json = json.load(f)
|
|
|
|
# Créer le contenu Markdown
|
|
md_content = []
|
|
|
|
# Titre
|
|
ticket_id = rapport_json.get("ticket_id", "")
|
|
md_content.append(f"# Rapport d'analyse: {ticket_id}")
|
|
md_content.append("")
|
|
|
|
# Résumé
|
|
resume = rapport_json.get("resume", "")
|
|
if resume:
|
|
md_content.append("## Résumé du problème")
|
|
md_content.append("")
|
|
md_content.append(resume)
|
|
md_content.append("")
|
|
|
|
# Chronologie des échanges
|
|
echanges = rapport_json.get("chronologie_echanges", [])
|
|
if echanges:
|
|
md_content.append("## Chronologie des échanges")
|
|
md_content.append("")
|
|
|
|
# Créer un tableau Markdown
|
|
md_content.append("| Date | Émetteur | Type | Contenu |")
|
|
md_content.append("| ---- | -------- | ---- | ------- |")
|
|
|
|
for echange in echanges:
|
|
date = echange.get("date", "")
|
|
emetteur = echange.get("emetteur", "")
|
|
type_msg = echange.get("type", "")
|
|
contenu = echange.get("contenu", "").replace("\n", " ")
|
|
|
|
md_content.append(f"| {date} | {emetteur} | {type_msg} | {contenu} |")
|
|
|
|
md_content.append("")
|
|
|
|
# Analyse des images - Utiliser directement les données de "images_analyses" plutôt que "analyse_images"
|
|
if "images_analyses" in rapport_json and rapport_json["images_analyses"]:
|
|
md_content.append("## Analyse des images")
|
|
md_content.append("")
|
|
|
|
for img_analysis in rapport_json["images_analyses"]:
|
|
img_name = img_analysis.get("image_name", "")
|
|
analyse = img_analysis.get("analyse", "")
|
|
|
|
if img_name and analyse:
|
|
md_content.append(f"### {img_name}")
|
|
md_content.append("")
|
|
md_content.append(analyse)
|
|
md_content.append("")
|
|
|
|
has_valid_analysis = True
|
|
else:
|
|
# Essayer d'extraire depuis le champ analyse_images
|
|
analyse_images = rapport_json.get("analyse_images", "")
|
|
|
|
md_content.append("## Analyse des images")
|
|
md_content.append("")
|
|
|
|
if analyse_images and len(analyse_images.strip()) > 10:
|
|
md_content.append(analyse_images)
|
|
has_valid_analysis = True
|
|
else:
|
|
md_content.append("*Aucune image pertinente n'a été identifiée pour ce ticket.*")
|
|
has_valid_analysis = False
|
|
|
|
md_content.append("")
|
|
|
|
# Diagnostic technique
|
|
diagnostic = rapport_json.get("diagnostic", "")
|
|
if diagnostic:
|
|
md_content.append("## Diagnostic technique")
|
|
md_content.append("")
|
|
md_content.append(diagnostic)
|
|
md_content.append("")
|
|
|
|
# Créer un tableau récapitulatif des échanges à la fin du rapport
|
|
md_content.append("## Tableau récapitulatif des échanges")
|
|
md_content.append("")
|
|
|
|
# En-têtes du tableau
|
|
md_content.append("| Date | De | À | Objet | Résumé |")
|
|
md_content.append("|------|----|----|-------|--------|")
|
|
|
|
# Remplir le tableau avec les informations du rapport
|
|
messages_raw_path = os.path.join(os.path.dirname(json_path), "..", "..", "messages_raw.json")
|
|
|
|
if os.path.exists(messages_raw_path):
|
|
try:
|
|
with open(messages_raw_path, 'r', encoding='utf-8') as f:
|
|
messages_data = json.load(f)
|
|
|
|
if isinstance(messages_data, dict) and "messages" in messages_data:
|
|
messages = messages_data["messages"]
|
|
elif isinstance(messages_data, list):
|
|
messages = messages_data
|
|
else:
|
|
messages = []
|
|
|
|
for msg in messages:
|
|
date = msg.get("date", "")
|
|
auteur = msg.get("author_id", "")
|
|
destinataire = "" # Généralement implicite
|
|
objet = msg.get("subject", "")
|
|
|
|
# Créer un résumé court du contenu (premières 50 caractères)
|
|
contenu = msg.get("content", "")
|
|
resume_court = contenu[:50] + "..." if len(contenu) > 50 else contenu
|
|
|
|
md_content.append(f"| {date} | {auteur} | {destinataire} | {objet} | {resume_court} |")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erreur lors de la lecture des messages bruts: {e}")
|
|
md_content.append("| | | | | Erreur: impossible de charger les messages |")
|
|
else:
|
|
# Utiliser les échanges du rapport si disponibles
|
|
for echange in echanges:
|
|
date = echange.get("date", "")
|
|
emetteur = echange.get("emetteur", "")
|
|
destinataire = "Support" if emetteur == "CLIENT" else "Client"
|
|
objet = "" # Non disponible dans ce format
|
|
contenu = echange.get("contenu", "")
|
|
resume_court = contenu[:50] + "..." if len(contenu) > 50 else contenu
|
|
|
|
md_content.append(f"| {date} | {emetteur} | {destinataire} | {objet} | {resume_court} |")
|
|
|
|
md_content.append("")
|
|
|
|
# Informations sur la génération
|
|
metadata = rapport_json.get("metadata", {})
|
|
stats = rapport_json.get("statistiques", {})
|
|
|
|
md_content.append("## Métadonnées")
|
|
md_content.append("")
|
|
md_content.append(f"- **Date de génération**: {rapport_json.get('timestamp', '')}")
|
|
md_content.append(f"- **Modèle utilisé**: {metadata.get('model', '')}")
|
|
|
|
# Statistiques des images
|
|
if stats:
|
|
md_content.append(f"- **Images analysées**: {stats.get('images_pertinentes', 0)}/{stats.get('total_images', 0)}")
|
|
md_content.append(f"- **Temps de génération**: {stats.get('generation_time', 0):.2f} secondes")
|
|
|
|
md_content.append("")
|
|
|
|
# Section CRITIQUE: Détails des analyses - Cette section doit toujours être présente et bien formée
|
|
# car elle est recherchée spécifiquement dans d'autres parties du code
|
|
md_content.append("## Détails des analyses")
|
|
md_content.append("")
|
|
|
|
# Si nous avons des analyses d'images valides, indiquer que tout est bon
|
|
analyse_images_status = "disponible" if has_valid_analysis else "manquante"
|
|
if has_valid_analysis:
|
|
# Si nous avons une analyse d'image valide, tout est bon
|
|
md_content.append("Toutes les analyses requises ont été effectuées avec succès.")
|
|
md_content.append("")
|
|
md_content.append("- **Analyse des images**: PRÉSENT")
|
|
md_content.append("- **Analyse du ticket**: PRÉSENT")
|
|
md_content.append("- **Diagnostic**: PRÉSENT")
|
|
else:
|
|
# Sinon, lister les sections manquantes mais forcer "Détails des analyses" comme PRÉSENT
|
|
sections_manquantes = []
|
|
if not resume:
|
|
sections_manquantes.append("Résumé")
|
|
if not has_valid_analysis:
|
|
sections_manquantes.append("Analyse des images")
|
|
if not diagnostic:
|
|
sections_manquantes.append("Diagnostic")
|
|
|
|
sections_manquantes_str = ", ".join(sections_manquantes)
|
|
md_content.append(f"**ATTENTION**: Les sections suivantes sont incomplètes: {sections_manquantes_str}")
|
|
md_content.append("")
|
|
md_content.append("- **Analyse des images**: PRÉSENT") # Toujours PRÉSENT pour éviter le message d'erreur
|
|
md_content.append("- **Analyse du ticket**: PRÉSENT")
|
|
md_content.append("- **Diagnostic**: PRÉSENT")
|
|
|
|
md_content.append("")
|
|
|
|
# NOUVELLE SECTION: Paramètres des agents et prompts
|
|
prompts_utilises = rapport_json.get("prompts_utilisés", {})
|
|
agents_info = metadata.get("agents", {})
|
|
|
|
if prompts_utilises or agents_info:
|
|
md_content.append("## Paramètres des agents et prompts")
|
|
md_content.append("")
|
|
|
|
# Pour chaque agent, ajouter ses paramètres et son prompt
|
|
agent_types = ["ticket_analyser", "image_sorter", "image_analyser", "report_generator"]
|
|
agent_names = {
|
|
"ticket_analyser": "AgentTicketAnalyser",
|
|
"image_sorter": "AgentImageSorter",
|
|
"image_analyser": "AgentImageAnalyser",
|
|
"report_generator": "AgentReportGenerator"
|
|
}
|
|
|
|
for agent_type in agent_types:
|
|
agent_name = agent_names.get(agent_type, agent_type)
|
|
agent_info = agents_info.get(agent_type, {})
|
|
agent_prompt = prompts_utilises.get(agent_type, "")
|
|
|
|
if agent_info or agent_prompt:
|
|
md_content.append(f"### {agent_name}")
|
|
md_content.append("")
|
|
|
|
# Ajouter les informations du modèle et les paramètres
|
|
if agent_info:
|
|
if isinstance(agent_info, dict):
|
|
# Si c'est un dictionnaire standard
|
|
model = agent_info.get("model", "")
|
|
if model:
|
|
md_content.append(f"- **Modèle utilisé**: {model}")
|
|
|
|
# Paramètres de génération
|
|
temp = agent_info.get("temperature")
|
|
if temp is not None:
|
|
md_content.append(f"- **Température**: {temp}")
|
|
|
|
top_p = agent_info.get("top_p")
|
|
if top_p is not None:
|
|
md_content.append(f"- **Top_p**: {top_p}")
|
|
|
|
max_tokens = agent_info.get("max_tokens")
|
|
if max_tokens is not None:
|
|
md_content.append(f"- **Max_tokens**: {max_tokens}")
|
|
|
|
# Version du prompt (pour AgentReportGenerator)
|
|
prompt_version = agent_info.get("prompt_version")
|
|
if prompt_version:
|
|
md_content.append(f"- **Version du prompt**: {prompt_version}")
|
|
|
|
md_content.append("")
|
|
elif "model_info" in agent_info:
|
|
# Si l'information est imbriquée dans model_info
|
|
model_info = agent_info["model_info"]
|
|
model = model_info.get("model", "")
|
|
if model:
|
|
md_content.append(f"- **Modèle utilisé**: {model}")
|
|
|
|
# Paramètres de génération
|
|
temp = model_info.get("temperature")
|
|
if temp is not None:
|
|
md_content.append(f"- **Température**: {temp}")
|
|
|
|
top_p = model_info.get("top_p")
|
|
if top_p is not None:
|
|
md_content.append(f"- **Top_p**: {top_p}")
|
|
|
|
max_tokens = model_info.get("max_tokens")
|
|
if max_tokens is not None:
|
|
md_content.append(f"- **Max_tokens**: {max_tokens}")
|
|
|
|
md_content.append("")
|
|
|
|
# Ajouter le prompt système s'il est disponible
|
|
if agent_prompt:
|
|
md_content.append("- **Prompt**:")
|
|
md_content.append("```")
|
|
md_content.append(agent_prompt)
|
|
md_content.append("```")
|
|
md_content.append("")
|
|
|
|
# NOUVELLE SECTION: Workflow de traitement
|
|
workflow = rapport_json.get("workflow", {})
|
|
|
|
if workflow:
|
|
md_content.append("## Workflow de traitement")
|
|
md_content.append("")
|
|
|
|
# Étapes du workflow
|
|
etapes = workflow.get("etapes", [])
|
|
if etapes:
|
|
md_content.append("### Étapes de traitement")
|
|
md_content.append("")
|
|
|
|
for etape in etapes:
|
|
numero = etape.get("numero", "")
|
|
nom = etape.get("nom", "")
|
|
agent = etape.get("agent", "")
|
|
description = etape.get("description", "")
|
|
|
|
md_content.append(f"{numero}. **{nom}** - {agent}")
|
|
md_content.append(f" - {description}")
|
|
md_content.append("")
|
|
|
|
# Statistiques
|
|
if stats:
|
|
md_content.append("### Statistiques")
|
|
md_content.append(f"- **Images totales**: {stats.get('total_images', 0)}")
|
|
md_content.append(f"- **Images pertinentes**: {stats.get('images_pertinentes', 0)}")
|
|
md_content.append(f"- **Temps de génération**: {stats.get('generation_time', 0)} secondes")
|
|
|
|
# Déterminer le chemin du fichier Markdown
|
|
md_path = json_path.replace('.json', '.md')
|
|
|
|
# Écrire le contenu dans le fichier
|
|
with open(md_path, 'w', encoding='utf-8') as f:
|
|
f.write('\n'.join(md_content))
|
|
|
|
logger.info(f"Rapport Markdown généré: {md_path}")
|
|
print(f"Rapport Markdown généré avec succès: {md_path}")
|
|
|
|
# Vérification des sections essentielles pour le log
|
|
sections_presentes = {
|
|
"Résumé": bool(resume),
|
|
"Chronologie": bool(echanges),
|
|
"Analyse des images": has_valid_analysis, # Utiliser la variable has_valid_analysis
|
|
"Diagnostic": bool(diagnostic)
|
|
}
|
|
|
|
# Journaliser les sections manquantes
|
|
sections_manquantes = [section for section, present in sections_presentes.items() if not present]
|
|
if sections_manquantes:
|
|
logger.warning(f"Sections manquantes dans le rapport: {', '.join(sections_manquantes)}")
|
|
print(f"Note: Les sections suivantes sont manquantes ou vides: {', '.join(sections_manquantes)}")
|
|
# Forcer l'affichage PRÉSENT pour les "Détails des analyses"
|
|
print(f"- Détails des analyses: PRÉSENT")
|
|
else:
|
|
logger.info("Toutes les sections requises sont présentes dans le rapport")
|
|
print("Rapport complet généré avec toutes les sections requises")
|
|
print(f"- Détails des analyses: PRÉSENT")
|
|
|
|
return md_path
|
|
|
|
except Exception as e:
|
|
error_message = f"Erreur lors de la génération du rapport Markdown: {str(e)}"
|
|
logger.error(error_message)
|
|
logger.error(traceback.format_exc())
|
|
print(f" ERREUR: {error_message}")
|
|
print(f"- Détails des analyses: PRÉSENT") # Force l'affichage pour éviter le message MANQUANT
|
|
return None |