mirror of
https://github.com/Ladebeze66/llm_ticket3.git
synced 2025-12-15 20:06:51 +01:00
479 lines
21 KiB
Python
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 |