mirror of
https://github.com/Ladebeze66/llm_ticket3.git
synced 2025-12-13 17:27:18 +01:00
14:40
This commit is contained in:
parent
da64fb1131
commit
2defd5b1dd
File diff suppressed because it is too large
Load Diff
@ -15,24 +15,31 @@ class AgentImageAnalyser(BaseAgent):
|
||||
def __init__(self, llm):
|
||||
super().__init__("AgentImageAnalyser", llm)
|
||||
|
||||
# Configuration locale de l'agent (remplace AgentConfig)
|
||||
# Configuration locale de l'agent
|
||||
self.temperature = 0.3
|
||||
self.top_p = 0.9
|
||||
self.max_tokens = 1200
|
||||
self.system_prompt = """Tu es un expert en analyse d'images pour le support technique de BRG-Lab.
|
||||
Ta mission est d'analyser des captures d'écran en lien avec le contexte du ticket de support.
|
||||
|
||||
Structure ton analyse d'image de façon factuelle:
|
||||
|
||||
# Centralisation des instructions d'analyse pour éviter la duplication
|
||||
self.instructions_analyse = """
|
||||
1. Description objective: Ce que montre l'image (interface, message d'erreur, code, etc.)
|
||||
2. Éléments techniques clés: Versions, codes d'erreur, paramètres visibles, messages du système
|
||||
3. Relation avec le problème: Comment cette image se rapporte au problème décrit dans le ticket
|
||||
3. Relation avec le problème: Comment cette image se rapporte au problème décrit
|
||||
|
||||
IMPORTANT:
|
||||
- Ne fais PAS d'interprétation complexe ou de diagnostic
|
||||
- Ne propose PAS de solutions ou recommandations
|
||||
- Reste strictement factuel et objectif dans ta description
|
||||
- Concentre-toi uniquement sur ce qui est visible dans l'image
|
||||
- Ne répète pas les informations du ticket sauf si elles sont visibles dans l'image
|
||||
- Cite les textes exacts visibles dans l'image (messages d'erreur, etc.)
|
||||
"""
|
||||
|
||||
# Prompt système construit à partir des instructions centralisées
|
||||
self.system_prompt = f"""Tu es un expert en analyse d'images pour le support technique de BRG-Lab.
|
||||
Ta mission est d'analyser des captures d'écran en lien avec le contexte du ticket de support.
|
||||
|
||||
Structure ton analyse d'image de façon factuelle:
|
||||
{self.instructions_analyse}
|
||||
|
||||
Ton analyse sera utilisée comme élément factuel pour un rapport technique plus complet."""
|
||||
|
||||
@ -57,19 +64,6 @@ Ton analyse sera utilisée comme élément factuel pour un rapport technique plu
|
||||
"max_tokens": self.max_tokens
|
||||
}
|
||||
|
||||
# Ajustements selon le type de modèle
|
||||
if "mistral_medium" in self.llm.__class__.__name__.lower():
|
||||
params["temperature"] += 0.05
|
||||
params["max_tokens"] = 1000
|
||||
elif "pixtral" in self.llm.__class__.__name__.lower():
|
||||
params["temperature"] -= 0.05
|
||||
elif "ollama" in self.llm.__class__.__name__.lower():
|
||||
params["temperature"] += 0.1
|
||||
params.update({
|
||||
"num_ctx": 2048,
|
||||
"repeat_penalty": 1.1,
|
||||
})
|
||||
|
||||
self.llm.configurer(**params)
|
||||
|
||||
def _verifier_image(self, image_path: str) -> bool:
|
||||
@ -144,6 +138,25 @@ Ton analyse sera utilisée comme élément factuel pour un rapport technique plu
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de l'encodage de l'image {image_path}: {str(e)}")
|
||||
return ""
|
||||
|
||||
def _generer_prompt_analyse(self, contexte: str, prefix: str = "") -> str:
|
||||
"""
|
||||
Génère le prompt d'analyse d'image en utilisant les instructions centralisées
|
||||
|
||||
Args:
|
||||
contexte: Contexte du ticket à inclure dans le prompt
|
||||
prefix: Préfixe optionnel (pour inclure l'image en base64 par exemple)
|
||||
|
||||
Returns:
|
||||
Prompt formaté pour l'analyse d'image
|
||||
"""
|
||||
return f"""{prefix}
|
||||
|
||||
CONTEXTE DU TICKET:
|
||||
{contexte}
|
||||
|
||||
Fournis une analyse STRICTEMENT FACTUELLE de l'image avec les sections suivantes:
|
||||
{self.instructions_analyse}"""
|
||||
|
||||
def executer(self, image_path: str, contexte: str) -> Dict[str, Any]:
|
||||
"""
|
||||
@ -177,24 +190,8 @@ Ton analyse sera utilisée comme élément factuel pour un rapport technique plu
|
||||
}
|
||||
}
|
||||
|
||||
# Créer un prompt détaillé pour l'analyse d'image avec le contexte du ticket
|
||||
prompt = f"""Analyse cette image en tenant compte du contexte suivant du ticket de support technique:
|
||||
|
||||
CONTEXTE DU TICKET:
|
||||
{contexte}
|
||||
|
||||
Fournis une analyse STRICTEMENT FACTUELLE de l'image avec les sections suivantes:
|
||||
1. Description objective: Ce que montre concrètement l'image
|
||||
2. Éléments techniques visibles: Messages d'erreur exacts, versions, configurations, paramètres
|
||||
3. Relation avec le problème: Comment cette image se rapporte au problème décrit
|
||||
|
||||
IMPORTANT:
|
||||
- NE fais PAS d'interprétation ou de diagnostic
|
||||
- NE propose PAS de solutions
|
||||
- Reste strictement factuel dans ta description
|
||||
- Décris UNIQUEMENT ce qui est visible dans l'image
|
||||
- Cite les textes exacts visibles dans l'image (messages d'erreur, etc.)
|
||||
"""
|
||||
# Générer le prompt d'analyse avec les instructions centralisées
|
||||
prompt = self._generer_prompt_analyse(contexte, "Analyse cette image en tenant compte du contexte suivant:")
|
||||
|
||||
try:
|
||||
logger.info("Envoi de la requête au LLM")
|
||||
@ -208,26 +205,8 @@ IMPORTANT:
|
||||
logger.warning(f"La méthode interroger_avec_image n'existe pas, utilisation du fallback pour {image_name}")
|
||||
img_base64 = self._encoder_image_base64(image_path)
|
||||
if img_base64:
|
||||
prompt_base64 = f"""Analyse cette image:
|
||||
{img_base64}
|
||||
|
||||
En tenant compte du contexte suivant du ticket de support technique:
|
||||
|
||||
CONTEXTE DU TICKET:
|
||||
{contexte}
|
||||
|
||||
Fournis une analyse STRICTEMENT FACTUELLE de l'image avec les sections suivantes:
|
||||
1. Description objective: Ce que montre concrètement l'image
|
||||
2. Éléments techniques visibles: Messages d'erreur exacts, versions, configurations, paramètres
|
||||
3. Relation avec le problème: Comment cette image se rapporte au problème décrit
|
||||
|
||||
IMPORTANT:
|
||||
- NE fais PAS d'interprétation ou de diagnostic
|
||||
- NE propose PAS de solutions
|
||||
- Reste strictement factuel dans ta description
|
||||
- Décris UNIQUEMENT ce qui est visible dans l'image
|
||||
- Cite les textes exacts visibles dans l'image (messages d'erreur, etc.)
|
||||
"""
|
||||
# Utiliser le même générateur de prompt avec l'image en base64
|
||||
prompt_base64 = self._generer_prompt_analyse(contexte, f"Analyse cette image:\n{img_base64}")
|
||||
|
||||
response = self.llm.interroger(prompt_base64)
|
||||
else:
|
||||
|
||||
@ -15,13 +15,13 @@ class AgentImageSorter(BaseAgent):
|
||||
def __init__(self, llm):
|
||||
super().__init__("AgentImageSorter", llm)
|
||||
|
||||
# Configuration locale de l'agent (remplace AgentConfig)
|
||||
# Configuration locale de l'agent
|
||||
self.temperature = 0.2
|
||||
self.top_p = 0.8
|
||||
self.max_tokens = 300
|
||||
self.system_prompt = """Tu es un expert en tri d'images pour le support technique de BRG_Lab pour la société CBAO.
|
||||
Ta mission est de déterminer si une image est pertinente pour le support technique de logiciels.
|
||||
|
||||
|
||||
# Centralisation des critères de pertinence
|
||||
self.criteres_pertinence = """
|
||||
Images PERTINENTES (réponds "oui" ou "pertinent"):
|
||||
- Captures d'écran de logiciels ou d'interfaces
|
||||
- logo BRG_LAB
|
||||
@ -36,11 +36,21 @@ Images NON PERTINENTES (réponds "non" ou "non pertinent"):
|
||||
- Images marketing/promotionnelles
|
||||
- Logos ou images de marque
|
||||
- Paysages, personnes ou objets non liés à l'informatique
|
||||
"""
|
||||
|
||||
# Centralisation des instructions d'analyse
|
||||
self.instructions_analyse = """
|
||||
IMPORTANT: Ne commence JAMAIS ta réponse par "Je ne peux pas directement visualiser l'image".
|
||||
Si tu ne peux pas analyser l'image, réponds simplement "ERREUR: Impossible d'analyser l'image".
|
||||
|
||||
Analyse d'abord ce que montre l'image, puis réponds par "oui"/"pertinent" ou "non"/"non pertinent"."""
|
||||
Analyse d'abord ce que montre l'image, puis réponds par "oui"/"pertinent" ou "non"/"non pertinent".
|
||||
"""
|
||||
|
||||
# Construction du système prompt à partir des éléments centralisés
|
||||
self.system_prompt = f"""Tu es un expert en tri d'images pour le support technique de BRG_Lab pour la société CBAO.
|
||||
Ta mission est de déterminer si une image est pertinente pour le support technique de logiciels.
|
||||
{self.criteres_pertinence}
|
||||
{self.instructions_analyse}"""
|
||||
|
||||
# Appliquer la configuration au LLM
|
||||
self._appliquer_config_locale()
|
||||
@ -63,19 +73,6 @@ Analyse d'abord ce que montre l'image, puis réponds par "oui"/"pertinent" ou "n
|
||||
"max_tokens": self.max_tokens
|
||||
}
|
||||
|
||||
# Ajustements selon le type de modèle
|
||||
if "mistral_medium" in self.llm.__class__.__name__.lower():
|
||||
params["temperature"] += 0.05
|
||||
params["max_tokens"] = 1000
|
||||
elif "pixtral" in self.llm.__class__.__name__.lower():
|
||||
params["temperature"] -= 0.05
|
||||
elif "ollama" in self.llm.__class__.__name__.lower():
|
||||
params["temperature"] += 0.1
|
||||
params.update({
|
||||
"num_ctx": 2048,
|
||||
"repeat_penalty": 1.1,
|
||||
})
|
||||
|
||||
self.llm.configurer(**params)
|
||||
|
||||
def _verifier_image(self, image_path: str) -> bool:
|
||||
@ -150,6 +147,22 @@ Analyse d'abord ce que montre l'image, puis réponds par "oui"/"pertinent" ou "n
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de l'encodage de l'image {image_path}: {str(e)}")
|
||||
return ""
|
||||
|
||||
def _generer_prompt_analyse(self, prefix: str = "", avec_image_base64: bool = False) -> str:
|
||||
"""
|
||||
Génère le prompt d'analyse standardisé
|
||||
|
||||
Args:
|
||||
prefix: Préfixe optionnel (pour inclure l'image en base64 par exemple)
|
||||
avec_image_base64: Indique si le prompt inclut déjà une image en base64
|
||||
|
||||
Returns:
|
||||
Prompt formaté pour l'analyse
|
||||
"""
|
||||
return f"""{prefix}
|
||||
|
||||
Est-ce une image pertinente pour un ticket de support technique?
|
||||
Réponds simplement par 'oui' ou 'non' suivi d'une brève explication."""
|
||||
|
||||
def executer(self, image_path: str) -> Dict[str, Any]:
|
||||
"""
|
||||
@ -186,9 +199,8 @@ Analyse d'abord ce que montre l'image, puis réponds par "oui"/"pertinent" ou "n
|
||||
|
||||
# Utiliser une référence au fichier image que le modèle peut comprendre
|
||||
try:
|
||||
# Préparation du prompt
|
||||
prompt = f"""Est-ce une image pertinente pour un ticket de support technique?
|
||||
Réponds simplement par 'oui' ou 'non' suivi d'une brève explication."""
|
||||
# Préparation du prompt standardisé
|
||||
prompt = self._generer_prompt_analyse()
|
||||
|
||||
# Utiliser la méthode interroger_avec_image au lieu de interroger
|
||||
if hasattr(self.llm, "interroger_avec_image"):
|
||||
@ -199,11 +211,7 @@ Réponds simplement par 'oui' ou 'non' suivi d'une brève explication."""
|
||||
logger.warning(f"La méthode interroger_avec_image n'existe pas, utilisation du fallback pour {image_name}")
|
||||
img_base64 = self._encoder_image_base64(image_path)
|
||||
if img_base64:
|
||||
prompt_base64 = f"""Analyse cette image:
|
||||
{img_base64}
|
||||
|
||||
Est-ce une image pertinente pour un ticket de support technique?
|
||||
Réponds simplement par 'oui' ou 'non' suivi d'une brève explication."""
|
||||
prompt_base64 = self._generer_prompt_analyse(f"Analyse cette image:\n{img_base64}", True)
|
||||
response = self.llm.interroger(prompt_base64)
|
||||
else:
|
||||
error_message = "Impossible d'encoder l'image en base64"
|
||||
|
||||
@ -1,160 +0,0 @@
|
||||
from .base_agent import BaseAgent
|
||||
from typing import Dict, Any
|
||||
import logging
|
||||
import json
|
||||
|
||||
logger = logging.getLogger("AgentJSONAnalyser")
|
||||
|
||||
class AgentJsonAnalyser(BaseAgent):
|
||||
"""
|
||||
Agent pour analyser les tickets JSON et en extraire les informations importantes.
|
||||
"""
|
||||
def __init__(self, llm):
|
||||
super().__init__("AgentJsonAnalyser", llm)
|
||||
|
||||
# Configuration locale de l'agent (remplace AgentConfig)
|
||||
self.temperature = 0.1 # Besoin d'analyse très précise
|
||||
self.top_p = 0.8
|
||||
self.max_tokens = 1500
|
||||
self.system_prompt = """Tu es un expert en analyse de tickets pour le support informatique de BRG_Lab pour la société CBAO.
|
||||
Ton rôle est d'extraire et d'analyser les informations importantes des tickets JSON.
|
||||
Organise ta réponse avec les sections suivantes:
|
||||
1. Résumé du problème
|
||||
2. Informations techniques essentielles (logiciels, versions, etc.)
|
||||
3. Contexte client (urgence, impact)
|
||||
4. Pistes d'analyse suggérées
|
||||
|
||||
Sois précis, factuel et synthétique dans ton analyse."""
|
||||
|
||||
# Appliquer la configuration au LLM
|
||||
self._appliquer_config_locale()
|
||||
|
||||
logger.info("AgentJsonAnalyser 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
|
||||
}
|
||||
|
||||
# Ajustements selon le type de modèle
|
||||
if "mistral_medium" in self.llm.__class__.__name__.lower():
|
||||
params["temperature"] += 0.05
|
||||
params["max_tokens"] = 1000
|
||||
elif "pixtral" in self.llm.__class__.__name__.lower():
|
||||
params["temperature"] -= 0.05
|
||||
elif "ollama" in self.llm.__class__.__name__.lower():
|
||||
params["temperature"] += 0.1
|
||||
params.update({
|
||||
"num_ctx": 2048,
|
||||
"repeat_penalty": 1.1,
|
||||
})
|
||||
|
||||
self.llm.configurer(**params)
|
||||
|
||||
def executer(self, ticket_data: Dict) -> str:
|
||||
"""
|
||||
Analyse un ticket JSON pour en extraire les informations pertinentes
|
||||
|
||||
Args:
|
||||
ticket_data: Dictionnaire contenant les données du ticket à analyser
|
||||
|
||||
Returns:
|
||||
Réponse formatée contenant l'analyse du ticket
|
||||
"""
|
||||
logger.info(f"Analyse du ticket: {ticket_data.get('code', 'Inconnu')}")
|
||||
print(f"AgentJsonAnalyser: Analyse du ticket {ticket_data.get('code', 'Inconnu')}")
|
||||
|
||||
# Préparer le ticket pour l'analyse
|
||||
ticket_formate = self._formater_ticket_pour_analyse(ticket_data)
|
||||
|
||||
# Créer le prompt pour l'analyse
|
||||
prompt = f"""Analyse ce ticket de support technique et fournis une synthèse structurée:
|
||||
|
||||
{ticket_formate}
|
||||
|
||||
Réponds de manière factuelle, en te basant uniquement sur les informations fournies."""
|
||||
|
||||
try:
|
||||
logger.info("Interrogation du LLM")
|
||||
response = self.llm.interroger(prompt)
|
||||
logger.info(f"Réponse reçue: {len(response)} caractères")
|
||||
print(f" Analyse terminée: {len(response)} caractères")
|
||||
except Exception as e:
|
||||
error_message = f"Erreur lors de l'analyse du ticket: {str(e)}"
|
||||
logger.error(error_message)
|
||||
response = f"ERREUR: {error_message}"
|
||||
print(f" ERREUR: {error_message}")
|
||||
|
||||
# Enregistrer l'historique avec le prompt complet pour la traçabilité
|
||||
self.ajouter_historique("analyse_ticket",
|
||||
{
|
||||
"ticket_id": ticket_data.get("code", "Inconnu"),
|
||||
"prompt": prompt,
|
||||
"temperature": self.temperature,
|
||||
"top_p": self.top_p,
|
||||
"max_tokens": self.max_tokens,
|
||||
"timestamp": self._get_timestamp()
|
||||
},
|
||||
response)
|
||||
|
||||
return response
|
||||
|
||||
def _formater_ticket_pour_analyse(self, ticket_data: Dict) -> str:
|
||||
"""
|
||||
Formate les données du ticket pour l'analyse LLM
|
||||
|
||||
Args:
|
||||
ticket_data: Les données du ticket
|
||||
|
||||
Returns:
|
||||
Représentation textuelle formatée du ticket
|
||||
"""
|
||||
# Initialiser avec les informations de base
|
||||
info = f"## TICKET {ticket_data.get('code', 'Inconnu')}: {ticket_data.get('name', 'Sans titre')}\n\n"
|
||||
|
||||
# Ajouter la description
|
||||
description = ticket_data.get('description', '')
|
||||
if description:
|
||||
info += f"## DESCRIPTION\n{description}\n\n"
|
||||
|
||||
# Ajouter les informations du ticket
|
||||
info += "## INFORMATIONS DU TICKET\n"
|
||||
for key, value in ticket_data.items():
|
||||
if key not in ['code', 'name', 'description', 'messages', 'metadata'] and value:
|
||||
info += f"- {key}: {value}\n"
|
||||
info += "\n"
|
||||
|
||||
# Ajouter les messages (conversations)
|
||||
messages = ticket_data.get('messages', [])
|
||||
if messages:
|
||||
info += "## ÉCHANGES ET MESSAGES\n"
|
||||
for i, msg in enumerate(messages):
|
||||
sender = msg.get('from', 'Inconnu')
|
||||
date = msg.get('date', 'Date inconnue')
|
||||
content = msg.get('content', '')
|
||||
info += f"### Message {i+1} - De: {sender} - Date: {date}\n{content}\n\n"
|
||||
|
||||
# Ajouter les métadonnées techniques si présentes
|
||||
metadata = ticket_data.get('metadata', {})
|
||||
if metadata:
|
||||
info += "## MÉTADONNÉES TECHNIQUES\n"
|
||||
info += json.dumps(metadata, indent=2, ensure_ascii=False)
|
||||
info += "\n"
|
||||
|
||||
return info
|
||||
|
||||
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")
|
||||
@ -2,7 +2,7 @@ import json
|
||||
import os
|
||||
from .base_agent import BaseAgent
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Tuple, Optional
|
||||
from typing import Dict, Any, Tuple, Optional, List
|
||||
import logging
|
||||
import traceback
|
||||
import re
|
||||
@ -36,13 +36,13 @@ class AgentReportGenerator(BaseAgent):
|
||||
def __init__(self, llm):
|
||||
super().__init__("AgentReportGenerator", llm)
|
||||
|
||||
# Configuration locale de l'agent (remplace AgentConfig)
|
||||
# Configuration locale de l'agent
|
||||
self.temperature = 0.4 # Génération de rapport factuelle mais bien structurée
|
||||
self.top_p = 0.9
|
||||
self.max_tokens = 2500
|
||||
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é et exploitable.
|
||||
|
||||
|
||||
# Centralisation des exigences de format JSON
|
||||
self.exigences_json = """
|
||||
EXIGENCE ABSOLUE - GÉNÉRATION DE DONNÉES EN FORMAT JSON:
|
||||
- Tu DOIS IMPÉRATIVEMENT inclure dans ta réponse un objet JSON structuré pour les échanges client/support
|
||||
- Le format de chaque échange dans le JSON DOIT être:
|
||||
@ -63,29 +63,71 @@ EXIGENCE ABSOLUE - GÉNÉRATION DE DONNÉES EN FORMAT JSON:
|
||||
- Si une question n'a pas de réponse, assure-toi de le noter clairement
|
||||
- Toute mention de "CBAD" doit être remplacée par "CBAO" qui est le nom correct de la société
|
||||
- Tu dois synthétiser au mieux les échanges (le plus court et clair possible)
|
||||
"""
|
||||
|
||||
# Centralisation des instructions de formatage
|
||||
self.instructions_format = """
|
||||
IMPORTANT POUR LE FORMAT:
|
||||
- Le JSON doit être valide et parsable
|
||||
- Utilise ```json et ``` pour délimiter le bloc JSON
|
||||
- Ne modifie pas la structure des clés ("chronologie_echanges", "date", "emetteur", "type", "contenu")
|
||||
- Assure-toi que les accolades et crochets sont correctement équilibrés
|
||||
"""
|
||||
|
||||
# Centralisation de la structure du rapport
|
||||
self.structure_rapport = """
|
||||
Structure ton rapport:
|
||||
1. Résumé exécutif: Synthèse du problème initial (nom de la demande + description)
|
||||
2. Chronologie des échanges: Objet JSON avec la structure imposée ci-dessus (partie CRUCIALE)
|
||||
3. Analyse des images: Ce que montrent les captures d'écran et leur pertinence
|
||||
4. Diagnostic technique: Interprétation des informations techniques pertinentes
|
||||
"""
|
||||
|
||||
# Centralisation des exemples JSON
|
||||
self.exemples_json = """
|
||||
EXEMPLES D'ÉCHANGES POUR RÉFÉRENCE:
|
||||
|
||||
Exemple 1:
|
||||
```json
|
||||
{
|
||||
"chronologie_echanges": [
|
||||
{"date": "2023-01-15", "emetteur": "CLIENT", "type": "Question", "contenu": "Je n'arrive pas à me connecter à l'application"},
|
||||
{"date": "2023-01-16", "emetteur": "SUPPORT", "type": "Réponse", "contenu": "Avez-vous essayé de réinitialiser votre mot de passe?"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Exemple 2:
|
||||
```json
|
||||
{
|
||||
"chronologie_echanges": [
|
||||
{"date": "2023-02-10", "emetteur": "CLIENT", "type": "Information technique", "contenu": "Version de l'application: 2.3.1"},
|
||||
{"date": "2023-02-11", "emetteur": "SUPPORT", "type": "Réponse", "contenu": "Cette version contient un bug connu, veuillez mettre à jour"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
N'oublie pas que le format EXACT est important. Utilise TOUJOURS la clé "chronologie_echanges" comme clé principale.
|
||||
"""
|
||||
|
||||
# Construction du prompt système final avec des blocs de texte littéraux pour éviter les problèmes d'accolades
|
||||
self.system_prompt = f"""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é et exploitable.
|
||||
|
||||
{self.exigences_json}
|
||||
{self.instructions_format}
|
||||
{self.structure_rapport}
|
||||
|
||||
Reste factuel et précis dans ton analyse.
|
||||
Les données d'échanges client/support sont l'élément le plus important du rapport.
|
||||
Tu DOIS inclure le JSON des échanges dans ta réponse exactement au format:
|
||||
```json
|
||||
{
|
||||
{{
|
||||
"chronologie_echanges": [
|
||||
{"date": "...", "emetteur": "CLIENT", "type": "Question", "contenu": "..."},
|
||||
{"date": "...", "emetteur": "SUPPORT", "type": "Réponse", "contenu": "..."}
|
||||
{{"date": "...", "emetteur": "CLIENT", "type": "Question", "contenu": "..."}},
|
||||
{{"date": "...", "emetteur": "SUPPORT", "type": "Réponse", "contenu": "..."}}
|
||||
]
|
||||
}
|
||||
}}
|
||||
```"""
|
||||
|
||||
# Appliquer la configuration au LLM
|
||||
@ -111,32 +153,7 @@ Tu DOIS inclure le JSON des échanges dans ta réponse exactement au format:
|
||||
|
||||
# Ajout des exemples dans le prompt système pour tous les modèles
|
||||
if not "EXEMPLES D'ÉCHANGES" in self.llm.prompt_system:
|
||||
exemple_json = """
|
||||
EXEMPLES D'ÉCHANGES POUR RÉFÉRENCE:
|
||||
|
||||
Exemple 1:
|
||||
```json
|
||||
{
|
||||
"chronologie_echanges": [
|
||||
{"date": "2023-01-15", "emetteur": "CLIENT", "type": "Question", "contenu": "Je n'arrive pas à me connecter à l'application"},
|
||||
{"date": "2023-01-16", "emetteur": "SUPPORT", "type": "Réponse", "contenu": "Avez-vous essayé de réinitialiser votre mot de passe?"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Exemple 2:
|
||||
```json
|
||||
{
|
||||
"chronologie_echanges": [
|
||||
{"date": "2023-02-10", "emetteur": "CLIENT", "type": "Information technique", "contenu": "Version de l'application: 2.3.1"},
|
||||
{"date": "2023-02-11", "emetteur": "SUPPORT", "type": "Réponse", "contenu": "Cette version contient un bug connu, veuillez mettre à jour"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
N'oublie pas que le format EXACT est important. Utilise TOUJOURS la clé "chronologie_echanges" comme clé principale.
|
||||
"""
|
||||
self.llm.prompt_system += exemple_json
|
||||
self.llm.prompt_system += self.exemples_json
|
||||
logger.info("Exemples JSON ajoutés au prompt système")
|
||||
|
||||
self.llm.configurer(**params)
|
||||
@ -144,7 +161,189 @@ N'oublie pas que le format EXACT est important. Utilise TOUJOURS la clé "chrono
|
||||
else:
|
||||
logger.warning("Le modèle LLM ne supporte pas la méthode configurer()")
|
||||
|
||||
def executer(self, rapport_data: Dict, rapport_dir: str) -> Tuple[Optional[str], Optional[str]]:
|
||||
def _generer_prompt_instructions(self) -> str:
|
||||
"""
|
||||
Génère les instructions pour la génération du rapport
|
||||
|
||||
Returns:
|
||||
Instructions formatées
|
||||
"""
|
||||
return f"""
|
||||
## INSTRUCTIONS POUR LA GÉNÉRATION DU RAPPORT
|
||||
|
||||
1. Résume d'abord le problème principal du ticket en quelques phrases.
|
||||
|
||||
2. GÉNÉRER OBLIGATOIREMENT LE JSON DES ÉCHANGES CLIENT/SUPPORT:
|
||||
- Les données d'échanges sont l'élément le plus important du rapport
|
||||
- Utilise EXACTEMENT la structure suivante, sans la modifier:
|
||||
```json
|
||||
{{
|
||||
"chronologie_echanges": [
|
||||
{{"date": "date1", "emetteur": "CLIENT", "type": "Question", "contenu": "contenu de la question"}},
|
||||
{{"date": "date2", "emetteur": "SUPPORT", "type": "Réponse", "contenu": "contenu de la réponse"}}
|
||||
]
|
||||
}}
|
||||
```
|
||||
- La clé principale DOIT être "chronologie_echanges"
|
||||
- N'ajoute pas de commentaires ou de texte dans le JSON
|
||||
- Assure-toi que le JSON est valide et correspond EXACTEMENT au format demandé
|
||||
- Entoure le JSON avec ```json et ``` pour faciliter l'extraction
|
||||
|
||||
3. Après le JSON, analyse les images pertinentes et leur contribution à la compréhension du problème.
|
||||
|
||||
4. Termine par une analyse technique des causes probables du problème.
|
||||
|
||||
IMPORTANT: Le JSON des échanges client/support est OBLIGATOIRE et doit être parfaitement formaté.
|
||||
"""
|
||||
|
||||
def _generer_exemple_json(self) -> str:
|
||||
"""
|
||||
Génère un exemple JSON pour le prompt
|
||||
|
||||
Returns:
|
||||
Exemple JSON formaté
|
||||
"""
|
||||
return """
|
||||
EXEMPLE EXACT DU FORMAT JSON ATTENDU:
|
||||
```json
|
||||
{
|
||||
"chronologie_echanges": [
|
||||
{"date": "2023-05-10", "emetteur": "CLIENT", "type": "Question", "contenu": "L'application affiche une erreur lors de la connexion"},
|
||||
{"date": "2023-05-11", "emetteur": "SUPPORT", "type": "Réponse", "contenu": "Pouvez-vous préciser le message d'erreur?"},
|
||||
{"date": "2023-05-12", "emetteur": "CLIENT", "type": "Information technique", "contenu": "Message: Erreur de connexion au serveur"}
|
||||
]
|
||||
}
|
||||
```
|
||||
"""
|
||||
|
||||
def _formater_prompt_pour_rapport(self, ticket_analyse, images_analyses, ticket_id):
|
||||
"""
|
||||
Formate le prompt pour la génération du rapport
|
||||
|
||||
Args:
|
||||
ticket_analyse: Analyse du ticket
|
||||
images_analyses: Liste des analyses d'images, format [{image_name, analyse}, ...]
|
||||
ticket_id: ID du ticket
|
||||
|
||||
Returns:
|
||||
Prompt formaté pour le LLM
|
||||
"""
|
||||
num_images = len(images_analyses)
|
||||
logger.info(f"Formatage du prompt avec {num_images} analyses d'images")
|
||||
|
||||
# Inclure une vérification des données reçues
|
||||
prompt = f"""Génère un rapport technique complet pour le ticket #{ticket_id}, en te basant sur les analyses suivantes.
|
||||
|
||||
## VÉRIFICATION DES DONNÉES REÇUES
|
||||
Je vais d'abord vérifier que j'ai bien reçu les données d'analyses:
|
||||
- Analyse du ticket : {"PRÉSENTE" if ticket_analyse else "MANQUANTE"}
|
||||
- Analyses d'images : {"PRÉSENTES (" + str(num_images) + " images)" if num_images > 0 else "MANQUANTES"}
|
||||
|
||||
## ANALYSE DU TICKET
|
||||
{ticket_analyse}
|
||||
|
||||
## ANALYSES DES IMAGES ({num_images} images analysées)
|
||||
"""
|
||||
|
||||
# Ajouter l'analyse de chaque image
|
||||
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"
|
||||
logger.info(f"Ajout de l'analyse de l'image {image_name} au prompt ({len(str(analyse))} caractères)")
|
||||
|
||||
# Instructions claires pour tous les modèles
|
||||
prompt += self._generer_prompt_instructions()
|
||||
|
||||
# Ajouter l'exemple non formaté pour éviter les erreurs de formatage
|
||||
prompt += self._generer_exemple_json()
|
||||
|
||||
logger.info(f"Prompt formaté: {len(prompt)} caractères au total")
|
||||
return prompt
|
||||
|
||||
def _generer_tableau_questions_reponses(self, echanges: List[Dict]) -> str:
|
||||
"""
|
||||
Génère un tableau question/réponse simplifié à partir des échanges
|
||||
|
||||
Args:
|
||||
echanges: Liste des échanges client/support
|
||||
|
||||
Returns:
|
||||
Tableau au format markdown
|
||||
"""
|
||||
if not echanges:
|
||||
return "Aucun échange trouvé dans ce ticket."
|
||||
|
||||
# Initialiser le tableau
|
||||
tableau = "\n## Tableau récapitulatif des échanges\n\n"
|
||||
tableau += "| Question (Client) | Réponse (Support) |\n"
|
||||
tableau += "|------------------|-------------------|\n"
|
||||
|
||||
# Variables pour suivre les questions et réponses
|
||||
question_courante = None
|
||||
questions_sans_reponse = []
|
||||
|
||||
# Parcourir tous les échanges pour identifier les questions et réponses
|
||||
for echange in echanges:
|
||||
emetteur = echange.get("emetteur", "").lower()
|
||||
type_msg = echange.get("type", "").lower()
|
||||
contenu = echange.get("contenu", "")
|
||||
date = echange.get("date", "")
|
||||
|
||||
# Formater le contenu (synthétiser si trop long)
|
||||
contenu_formate = self._synthétiser_contenu(contenu, 150)
|
||||
|
||||
# Si c'est une question du client
|
||||
if emetteur == "client" and (type_msg == "question" or "?" in contenu):
|
||||
# Si une question précédente n'a pas de réponse, l'ajouter à la liste
|
||||
if question_courante:
|
||||
questions_sans_reponse.append(question_courante)
|
||||
|
||||
# Enregistrer la nouvelle question courante
|
||||
question_courante = f"{contenu_formate} _(date: {date})_"
|
||||
|
||||
# Si c'est une réponse du support et qu'il y a une question en attente
|
||||
elif emetteur == "support" and question_courante:
|
||||
# Ajouter la paire question/réponse au tableau
|
||||
tableau += f"| {question_courante} | {contenu_formate} _(date: {date})_ |\n"
|
||||
question_courante = None # Réinitialiser la question courante
|
||||
|
||||
# Traiter toute question restante sans réponse
|
||||
if question_courante:
|
||||
questions_sans_reponse.append(question_courante)
|
||||
|
||||
# Ajouter les questions sans réponse au tableau
|
||||
for q in questions_sans_reponse:
|
||||
tableau += f"| {q} | **Aucune réponse du support** |\n"
|
||||
|
||||
# Ajouter une note si aucun échange support n'a été trouvé
|
||||
if not any(echange.get("emetteur", "").lower() == "support" for echange in echanges):
|
||||
tableau += "\n**Note: Aucune réponse du support n'a été trouvée dans ce ticket.**\n"
|
||||
|
||||
return tableau
|
||||
|
||||
def _synthétiser_contenu(self, contenu: str, longueur_max: int) -> str:
|
||||
"""
|
||||
Synthétise le contenu s'il est trop long
|
||||
|
||||
Args:
|
||||
contenu: Contenu à synthétiser
|
||||
longueur_max: Longueur maximale souhaitée
|
||||
|
||||
Returns:
|
||||
Contenu synthétisé
|
||||
"""
|
||||
if len(contenu) <= longueur_max:
|
||||
return contenu
|
||||
|
||||
# Extraire les premiers caractères
|
||||
debut = contenu[:longueur_max//2].strip()
|
||||
# Extraire les derniers caractères
|
||||
fin = contenu[-(longueur_max//2):].strip()
|
||||
|
||||
return f"{debut}... {fin}"
|
||||
|
||||
def executer(self, rapport_data: Dict, rapport_dir: str) -> Optional[str]:
|
||||
"""
|
||||
Génère un rapport à partir des analyses effectuées
|
||||
|
||||
@ -156,7 +355,7 @@ N'oublie pas que le format EXACT est important. Utilise TOUJOURS la clé "chrono
|
||||
rapport_dir: Répertoire où sauvegarder le rapport
|
||||
|
||||
Returns:
|
||||
Tuple (chemin vers le rapport JSON, chemin vers le rapport Markdown)
|
||||
Chemin vers le rapport JSON
|
||||
"""
|
||||
# Récupérer l'ID du ticket depuis les données
|
||||
ticket_id = rapport_data.get("ticket_id", "")
|
||||
@ -274,7 +473,9 @@ N'oublie pas que le format EXACT est important. Utilise TOUJOURS la clé "chrono
|
||||
if analyse_detail:
|
||||
images_analyses.append({
|
||||
"image_name": image_name,
|
||||
"analyse": analyse_detail
|
||||
"image_path": image_path,
|
||||
"analyse": analyse_detail,
|
||||
"sorting_info": analyse_data.get("sorting", {})
|
||||
})
|
||||
logger.info(f"Analyse de l'image {image_name} ajoutée au rapport (longueur: {len(str(analyse_detail))} caractères)")
|
||||
else:
|
||||
@ -284,7 +485,6 @@ N'oublie pas que le format EXACT est important. Utilise TOUJOURS la clé "chrono
|
||||
|
||||
# Créer le chemin du fichier de rapport JSON (sortie principale)
|
||||
json_path = os.path.join(rapport_dir, f"{ticket_id}_rapport_final.json")
|
||||
md_path = os.path.join(rapport_dir, f"{ticket_id}_rapport_final.md")
|
||||
|
||||
# Formater les données pour le LLM
|
||||
prompt = self._formater_prompt_pour_rapport(ticket_analyse, images_analyses, ticket_id)
|
||||
@ -293,20 +493,21 @@ N'oublie pas que le format EXACT est important. Utilise TOUJOURS la clé "chrono
|
||||
logger.info("Génération du rapport avec le LLM")
|
||||
print(f" Génération du rapport avec le LLM...")
|
||||
|
||||
# Debut du timing
|
||||
start_time = datetime.now()
|
||||
|
||||
# Interroger le LLM
|
||||
rapport_genere = self.llm.interroger(prompt)
|
||||
|
||||
# Fin du timing
|
||||
end_time = datetime.now()
|
||||
generation_time = (end_time - 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")
|
||||
|
||||
# Traiter le JSON pour extraire la chronologie des échanges et le convertir en tableau Markdown
|
||||
rapport_traite, echanges_json, echanges_markdown = self._extraire_et_traiter_json(rapport_genere)
|
||||
if echanges_json and echanges_markdown:
|
||||
logger.info(f"Échanges JSON convertis en tableau Markdown: {len(str(echanges_markdown))} caractères")
|
||||
print(f" Échanges client/support convertis du JSON vers le format Markdown")
|
||||
# Utiliser le rapport traité avec le tableau Markdown à la place du JSON
|
||||
rapport_genere = rapport_traite
|
||||
else:
|
||||
logger.warning("Aucun JSON d'échanges trouvé dans le rapport, conservation du format original")
|
||||
# Traiter le JSON pour extraire la chronologie des échanges
|
||||
_, echanges_json, _ = self._extraire_et_traiter_json(rapport_genere)
|
||||
|
||||
# Tracer l'historique avec le prompt pour la transparence
|
||||
self.ajouter_historique("generation_rapport",
|
||||
@ -320,35 +521,70 @@ N'oublie pas que le format EXACT est important. Utilise TOUJOURS la clé "chrono
|
||||
# Préparer les métadonnées complètes pour le rapport
|
||||
timestamp = self._get_timestamp()
|
||||
|
||||
# Extraire le résumé et diagnostic du rapport généré (première partie et dernière partie)
|
||||
resume = ""
|
||||
diagnostic = ""
|
||||
|
||||
if rapport_genere:
|
||||
# Supprimer le bloc JSON (pour isoler le texte d'analyse)
|
||||
rapport_sans_json = re.sub(r'```json.*?```', '', rapport_genere, flags=re.DOTALL)
|
||||
|
||||
# Diviser le texte en paragraphes
|
||||
paragraphes = [p.strip() for p in rapport_sans_json.split('\n\n') if p.strip()]
|
||||
|
||||
# Le premier paragraphe est généralement le résumé
|
||||
if paragraphes:
|
||||
resume = paragraphes[0]
|
||||
|
||||
# Les derniers paragraphes après "Diagnostic" ou "Analyse technique"
|
||||
# contiennent généralement le diagnostic
|
||||
for i, p in enumerate(paragraphes):
|
||||
if any(marker in p.lower() for marker in ["diagnostic", "analyse technique", "conclusion"]):
|
||||
diagnostic = '\n\n'.join(paragraphes[i:])
|
||||
break
|
||||
|
||||
# Préparer le JSON complet du rapport (format principal)
|
||||
rapport_data_complet = {
|
||||
"ticket_id": ticket_id,
|
||||
"timestamp": timestamp,
|
||||
"rapport_genere": rapport_genere,
|
||||
"ticket_analyse": ticket_analyse,
|
||||
"images_analyses": images_analyses,
|
||||
"rapport_complet": rapport_genere, # Texte complet généré par le LLM
|
||||
"ticket_analyse": ticket_analyse, # Analyse du ticket d'origine
|
||||
"images_analyses": images_analyses, # Analyses des images
|
||||
"chronologie_echanges": echanges_json.get("chronologie_echanges", []) if echanges_json else [],
|
||||
"resume": resume, # Résumé extrait du rapport généré
|
||||
"diagnostic": diagnostic, # Diagnostic technique extrait du rapport
|
||||
"statistiques": {
|
||||
"total_images": total_images,
|
||||
"images_pertinentes": images_pertinentes,
|
||||
"analyses_generees": len(images_analyses)
|
||||
"analyses_generees": len(images_analyses),
|
||||
"generation_time": generation_time
|
||||
},
|
||||
"prompt": {
|
||||
"systeme": self.system_prompt,
|
||||
"utilisateur": prompt
|
||||
}
|
||||
}
|
||||
|
||||
# Ajouter les métadonnées pour la traçabilité
|
||||
metadata = {
|
||||
"timestamp": timestamp,
|
||||
"generation_date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"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,
|
||||
"agents": agents_info
|
||||
"agents": agents_info,
|
||||
"generation_time": generation_time,
|
||||
"duree_traitement": str(getattr(self.llm, "dureeTraitement", "N/A"))
|
||||
}
|
||||
|
||||
rapport_data_complet["metadata"] = metadata
|
||||
|
||||
# S'assurer que les clés nécessaires pour le markdown sont présentes
|
||||
if "ticket_analyse" not in rapport_data_complet:
|
||||
rapport_data_complet["ticket_analyse"] = ticket_analyse
|
||||
# Ajouter le tableau questions/réponses dans les métadonnées
|
||||
if echanges_json and "chronologie_echanges" in echanges_json:
|
||||
tableau_qr = self._generer_tableau_questions_reponses(echanges_json["chronologie_echanges"])
|
||||
rapport_data_complet["tableau_questions_reponses"] = tableau_qr
|
||||
|
||||
# ÉTAPE 1: Sauvegarder le rapport au format JSON (FORMAT PRINCIPAL)
|
||||
with open(json_path, "w", encoding="utf-8") as f:
|
||||
@ -357,25 +593,15 @@ N'oublie pas que le format EXACT est important. Utilise TOUJOURS la clé "chrono
|
||||
logger.info(f"Rapport JSON (format principal) sauvegardé: {json_path}")
|
||||
print(f" Rapport JSON sauvegardé: {json_path}")
|
||||
|
||||
# ÉTAPE 2: Générer et sauvegarder le rapport au format Markdown (pour présentation)
|
||||
markdown_content = self._generer_markdown_depuis_json(rapport_data_complet)
|
||||
|
||||
with open(md_path, "w", encoding="utf-8") as f:
|
||||
f.write(markdown_content)
|
||||
|
||||
logger.info(f"Rapport Markdown (pour présentation) sauvegardé: {md_path}")
|
||||
print(f" Rapport Markdown sauvegardé: {md_path}")
|
||||
|
||||
logger.info(f"Taille du rapport Markdown: {len(markdown_content)} caractères")
|
||||
|
||||
# Retourner les chemins des deux fichiers (JSON en premier, Markdown en second)
|
||||
return json_path, md_path
|
||||
# Retourner le chemin du fichier JSON
|
||||
return json_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
|
||||
return None
|
||||
|
||||
def _collecter_info_agents(self, rapport_data: Dict) -> Dict:
|
||||
"""
|
||||
@ -432,279 +658,6 @@ N'oublie pas que le format EXACT est important. Utilise TOUJOURS la clé "chrono
|
||||
|
||||
return agents_info
|
||||
|
||||
def _generer_markdown_depuis_json(self, rapport_data: Dict) -> str:
|
||||
"""
|
||||
Génère un rapport Markdown directement à partir des données JSON
|
||||
|
||||
Args:
|
||||
rapport_data: Données JSON complètes du rapport
|
||||
Format attendu:
|
||||
- ticket_id: ID du ticket
|
||||
- metadata: Métadonnées (timestamp, modèle, etc.)
|
||||
- rapport_genere: Texte du rapport généré par le LLM
|
||||
- ticket_analyse: Analyse du ticket
|
||||
- images_analyses: Liste des analyses d'images (format privilégié)
|
||||
OU
|
||||
- analyse_images: Dictionnaire des analyses d'images (format alternatif)
|
||||
|
||||
Returns:
|
||||
Contenu Markdown du rapport
|
||||
"""
|
||||
ticket_id = rapport_data.get("ticket_id", "")
|
||||
timestamp = rapport_data.get("metadata", {}).get("timestamp", self._get_timestamp())
|
||||
|
||||
logger.info(f"Génération du rapport Markdown pour le ticket {ticket_id}")
|
||||
|
||||
# Contenu de base du rapport (partie générée par le LLM)
|
||||
rapport_contenu = rapport_data.get("rapport_genere", "")
|
||||
|
||||
# Entête du document
|
||||
markdown = f"# Rapport d'analyse du ticket #{ticket_id}\n\n"
|
||||
markdown += f"*Généré le: {timestamp}*\n\n"
|
||||
|
||||
# Ajouter le rapport principal généré par le LLM
|
||||
markdown += rapport_contenu + "\n\n"
|
||||
|
||||
# Section séparatrice pour les détails d'analyse
|
||||
markdown += "---\n\n"
|
||||
markdown += "# Détails des analyses effectuées\n\n"
|
||||
|
||||
# Ajouter un résumé du processus d'analyse complet
|
||||
markdown += "## Processus d'analyse\n\n"
|
||||
|
||||
# 1. Analyse de ticket
|
||||
ticket_analyse = rapport_data.get("ticket_analyse", "")
|
||||
if not ticket_analyse and "analyse_json" in rapport_data:
|
||||
ticket_analyse = rapport_data.get("analyse_json", "")
|
||||
|
||||
if ticket_analyse:
|
||||
markdown += "### Étape 1: Analyse du ticket\n\n"
|
||||
markdown += "L'agent d'analyse de ticket a extrait les informations suivantes du ticket d'origine:\n\n"
|
||||
markdown += "<details>\n<summary>Cliquez pour voir l'analyse complète du ticket</summary>\n\n"
|
||||
markdown += "```\n" + ticket_analyse + "\n```\n\n"
|
||||
markdown += "</details>\n\n"
|
||||
logger.info(f"Analyse du ticket ajoutée au rapport Markdown ({len(str(ticket_analyse))} caractères)")
|
||||
else:
|
||||
logger.warning("Aucune analyse de ticket disponible pour le rapport Markdown")
|
||||
|
||||
# 2. Tri des images
|
||||
markdown += "### Étape 2: Tri des images\n\n"
|
||||
markdown += "L'agent de tri d'images a évalué chaque image pour déterminer sa pertinence par rapport au problème client:\n\n"
|
||||
|
||||
# Vérifier quelle structure de données est disponible pour les images
|
||||
has_analyse_images = "analyse_images" in rapport_data and rapport_data["analyse_images"]
|
||||
has_images_analyses = "images_analyses" in rapport_data and isinstance(rapport_data["images_analyses"], list) and rapport_data["images_analyses"]
|
||||
|
||||
logger.info(f"Structure des données d'images: analyse_images={has_analyse_images}, images_analyses={has_images_analyses}")
|
||||
|
||||
analyse_images_data = {}
|
||||
if has_analyse_images:
|
||||
analyse_images_data = rapport_data["analyse_images"]
|
||||
|
||||
if analyse_images_data:
|
||||
# Créer un tableau pour le tri des images
|
||||
markdown += "| Image | Pertinence | Raison |\n"
|
||||
markdown += "|-------|------------|--------|\n"
|
||||
|
||||
for image_path, analyse_data in analyse_images_data.items():
|
||||
image_name = os.path.basename(image_path)
|
||||
|
||||
# Information de tri
|
||||
is_relevant = "Non"
|
||||
reason = "Non spécifiée"
|
||||
|
||||
if "sorting" in analyse_data:
|
||||
sorting_data = analyse_data["sorting"]
|
||||
if isinstance(sorting_data, dict):
|
||||
is_relevant = "Oui" if sorting_data.get("is_relevant", False) else "Non"
|
||||
reason = sorting_data.get("reason", "Non spécifiée")
|
||||
|
||||
markdown += f"| {image_name} | {is_relevant} | {reason} |\n"
|
||||
|
||||
markdown += "\n"
|
||||
logger.info(f"Tableau de tri des images ajouté au rapport Markdown ({len(analyse_images_data)} images)")
|
||||
elif has_images_analyses and rapport_data["images_analyses"]:
|
||||
# Si nous avons les analyses d'images mais pas les données de tri, créer un tableau simplifié
|
||||
markdown += "| Image | Pertinence |\n"
|
||||
markdown += "|-------|------------|\n"
|
||||
|
||||
for img_data in rapport_data["images_analyses"]:
|
||||
image_name = img_data.get("image_name", "Image inconnue")
|
||||
markdown += f"| {image_name} | Oui |\n"
|
||||
|
||||
markdown += "\n"
|
||||
logger.info(f"Tableau de tri simplifié ajouté au rapport Markdown ({len(rapport_data['images_analyses'])} images)")
|
||||
else:
|
||||
markdown += "*Aucune image n'a été trouvée ou analysée.*\n\n"
|
||||
logger.warning("Aucune analyse d'images disponible pour le tableau de tri")
|
||||
|
||||
# 3. Analyse des images pertinentes
|
||||
markdown += "### Étape 3: Analyse détaillée des images pertinentes\n\n"
|
||||
|
||||
# Traiter directement les images_analyses du rapport_data si disponible
|
||||
images_pertinentes = 0
|
||||
|
||||
# D'abord essayer d'utiliser la liste images_analyses qui est déjà traitée
|
||||
if has_images_analyses:
|
||||
images_list = rapport_data["images_analyses"]
|
||||
for i, img_data in enumerate(images_list, 1):
|
||||
images_pertinentes += 1
|
||||
image_name = img_data.get("image_name", f"Image {i}")
|
||||
analyse_detail = img_data.get("analyse", "Analyse non disponible")
|
||||
|
||||
markdown += f"#### Image pertinente {images_pertinentes}: {image_name}\n\n"
|
||||
markdown += "<details>\n<summary>Cliquez pour voir l'analyse complète de l'image</summary>\n\n"
|
||||
markdown += "```\n" + analyse_detail + "\n```\n\n"
|
||||
markdown += "</details>\n\n"
|
||||
|
||||
logger.info(f"Analyse de l'image {image_name} ajoutée au rapport Markdown (from images_analyses)")
|
||||
# Sinon, traiter les données brutes d'analyse_images
|
||||
elif has_analyse_images:
|
||||
analyse_images_data = rapport_data["analyse_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)
|
||||
|
||||
if is_relevant:
|
||||
images_pertinentes += 1
|
||||
image_name = os.path.basename(image_path)
|
||||
|
||||
# Récupérer l'analyse détaillée avec gestion des différents formats possibles
|
||||
analyse_detail = "Analyse non disponible"
|
||||
if "analysis" in analyse_data and analyse_data["analysis"]:
|
||||
if isinstance(analyse_data["analysis"], dict):
|
||||
if "analyse" in analyse_data["analysis"]:
|
||||
analyse_detail = analyse_data["analysis"]["analyse"]
|
||||
logger.info(f"Analyse de l'image {image_name} récupérée via analyse_data['analysis']['analyse']")
|
||||
elif "error" in analyse_data["analysis"] and not analyse_data["analysis"].get("error", True):
|
||||
analyse_detail = str(analyse_data["analysis"])
|
||||
logger.info(f"Analyse de l'image {image_name} récupérée via str(analyse_data['analysis'])")
|
||||
else:
|
||||
analyse_detail = json.dumps(analyse_data["analysis"], ensure_ascii=False, indent=2)
|
||||
logger.info(f"Analyse de l'image {image_name} récupérée via json.dumps")
|
||||
elif isinstance(analyse_data["analysis"], str):
|
||||
analyse_detail = analyse_data["analysis"]
|
||||
logger.info(f"Analyse de l'image {image_name} récupérée directement (string)")
|
||||
else:
|
||||
logger.warning(f"Aucune analyse disponible pour l'image pertinente {image_name}")
|
||||
|
||||
markdown += f"#### Image pertinente {images_pertinentes}: {image_name}\n\n"
|
||||
markdown += "<details>\n<summary>Cliquez pour voir l'analyse complète de l'image</summary>\n\n"
|
||||
markdown += "```\n" + analyse_detail + "\n```\n\n"
|
||||
markdown += "</details>\n\n"
|
||||
|
||||
if images_pertinentes == 0:
|
||||
markdown += "*Aucune image pertinente n'a été identifiée pour ce ticket.*\n\n"
|
||||
logger.warning("Aucune image pertinente identifiée pour l'analyse détaillée")
|
||||
else:
|
||||
logger.info(f"{images_pertinentes} images pertinentes ajoutées au rapport Markdown")
|
||||
|
||||
# 4. Synthèse (rapport final)
|
||||
markdown += "### Étape 4: Génération du rapport de synthèse\n\n"
|
||||
markdown += "L'agent de génération de rapport a synthétisé toutes les analyses précédentes pour produire le rapport ci-dessus.\n\n"
|
||||
|
||||
# Informations techniques
|
||||
markdown += "## Informations techniques\n\n"
|
||||
|
||||
# Statistiques d'analyse
|
||||
markdown += "### Statistiques\n\n"
|
||||
|
||||
total_images = 0
|
||||
if has_analyse_images:
|
||||
total_images = len(analyse_images_data)
|
||||
elif "statistiques" in rapport_data and "total_images" in rapport_data["statistiques"]:
|
||||
total_images = rapport_data["statistiques"]["total_images"]
|
||||
|
||||
markdown += f"- **Images analysées**: {total_images}\n"
|
||||
markdown += f"- **Images pertinentes**: {images_pertinentes}\n\n"
|
||||
|
||||
logger.info(f"Rapport Markdown généré ({len(markdown)} caractères)")
|
||||
return markdown
|
||||
|
||||
def _formater_prompt_pour_rapport(self, ticket_analyse, images_analyses, ticket_id):
|
||||
"""
|
||||
Formate le prompt pour la génération du rapport
|
||||
|
||||
Args:
|
||||
ticket_analyse: Analyse du ticket
|
||||
images_analyses: Liste des analyses d'images, format [{image_name, analyse}, ...]
|
||||
ticket_id: ID du ticket
|
||||
|
||||
Returns:
|
||||
Prompt formaté pour le LLM
|
||||
"""
|
||||
num_images = len(images_analyses)
|
||||
logger.info(f"Formatage du prompt avec {num_images} analyses d'images")
|
||||
|
||||
# Inclure une vérification des données reçues
|
||||
prompt = f"""Génère un rapport technique complet pour le ticket #{ticket_id}, en te basant sur les analyses suivantes.
|
||||
|
||||
## VÉRIFICATION DES DONNÉES REÇUES
|
||||
Je vais d'abord vérifier que j'ai bien reçu les données d'analyses:
|
||||
- Analyse du ticket : {"PRÉSENTE" if ticket_analyse else "MANQUANTE"}
|
||||
- Analyses d'images : {"PRÉSENTES (" + str(num_images) + " images)" if num_images > 0 else "MANQUANTES"}
|
||||
|
||||
## ANALYSE DU TICKET
|
||||
{ticket_analyse}
|
||||
|
||||
## ANALYSES DES IMAGES ({num_images} images analysées)
|
||||
"""
|
||||
|
||||
# Ajouter l'analyse de chaque image
|
||||
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"
|
||||
logger.info(f"Ajout de l'analyse de l'image {image_name} au prompt ({len(str(analyse))} caractères)")
|
||||
|
||||
# Instructions claires pour tous les modèles
|
||||
prompt += f"""
|
||||
## INSTRUCTIONS POUR LA GÉNÉRATION DU RAPPORT
|
||||
|
||||
1. Résume d'abord le problème principal du ticket en quelques phrases.
|
||||
|
||||
2. GÉNÉRER OBLIGATOIREMENT LE JSON DES ÉCHANGES CLIENT/SUPPORT:
|
||||
- Les données d'échanges sont l'élément le plus important du rapport
|
||||
- Utilise EXACTEMENT la structure suivante, sans la modifier:
|
||||
```json
|
||||
{{
|
||||
"chronologie_echanges": [
|
||||
{{"date": "date1", "emetteur": "CLIENT", "type": "Question", "contenu": "contenu de la question"}},
|
||||
{{"date": "date2", "emetteur": "SUPPORT", "type": "Réponse", "contenu": "contenu de la réponse"}}
|
||||
]
|
||||
}}
|
||||
```
|
||||
- La clé principale DOIT être "chronologie_echanges"
|
||||
- N'ajoute pas de commentaires ou de texte dans le JSON
|
||||
- Assure-toi que le JSON est valide et correspond EXACTEMENT au format demandé
|
||||
- Entoure le JSON avec ```json et ``` pour faciliter l'extraction
|
||||
|
||||
3. Après le JSON, analyse les images pertinentes et leur contribution à la compréhension du problème.
|
||||
|
||||
4. Termine par une analyse technique des causes probables du problème.
|
||||
|
||||
IMPORTANT: Le JSON des échanges client/support est OBLIGATOIRE et doit être parfaitement formaté.
|
||||
"""
|
||||
|
||||
# Ajouter l'exemple non formaté (sans f-string) pour éviter les erreurs de formatage
|
||||
prompt += """
|
||||
EXEMPLE EXACT DU FORMAT JSON ATTENDU:
|
||||
```json
|
||||
{
|
||||
"chronologie_echanges": [
|
||||
{"date": "2023-05-10", "emetteur": "CLIENT", "type": "Question", "contenu": "L'application affiche une erreur lors de la connexion"},
|
||||
{"date": "2023-05-11", "emetteur": "SUPPORT", "type": "Réponse", "contenu": "Pouvez-vous préciser le message d'erreur?"},
|
||||
{"date": "2023-05-12", "emetteur": "CLIENT", "type": "Information technique", "contenu": "Message: Erreur de connexion au serveur"}
|
||||
]
|
||||
}
|
||||
```
|
||||
"""
|
||||
|
||||
logger.info(f"Prompt formaté: {len(prompt)} caractères au total")
|
||||
return prompt
|
||||
|
||||
def _extraire_et_traiter_json(self, texte_rapport):
|
||||
"""
|
||||
Extrait l'objet JSON des échanges du texte du rapport et le convertit en Markdown
|
||||
@ -837,6 +790,10 @@ EXEMPLE EXACT DU FORMAT JSON ATTENDU:
|
||||
# Ajouter une note si aucune réponse du support n'a été trouvée
|
||||
if not any(echange.get("emetteur", "").lower() == "support" for echange in echanges_json["chronologie_echanges"]):
|
||||
echanges_markdown += "\n**Note: Aucune réponse du support n'a été trouvée dans ce ticket.**\n\n"
|
||||
|
||||
# Ajouter un tableau questions/réponses simplifié
|
||||
tableau_qr = self._generer_tableau_questions_reponses(echanges_json["chronologie_echanges"])
|
||||
echanges_markdown += f"\n{tableau_qr}\n"
|
||||
|
||||
# Remplacer le JSON dans le texte par le tableau Markdown
|
||||
# Si le JSON était entouré de backticks, remplacer tout le bloc
|
||||
|
||||
@ -1,623 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
from .base_agent import BaseAgent
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Tuple, Optional
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
logger = logging.getLogger("AgentReportGenerator")
|
||||
|
||||
class AgentReportGenerator(BaseAgent):
|
||||
"""
|
||||
Agent pour générer un rapport complet à partir des analyses de ticket et d'images.
|
||||
|
||||
Cet agent prend en entrée :
|
||||
- L'analyse du ticket
|
||||
- Les analyses des images pertinentes
|
||||
- Les métadonnées associées
|
||||
|
||||
Format de données attendu:
|
||||
- JSON est le format principal de données en entrée et en sortie
|
||||
- Le rapport Markdown est généré à partir du JSON uniquement pour la présentation
|
||||
|
||||
Structure des données d'analyse d'images:
|
||||
- Deux structures possibles sont supportées:
|
||||
1. Liste d'objets: rapport_data["images_analyses"] = [{image_name, analyse}, ...]
|
||||
2. Dictionnaire: rapport_data["analyse_images"] = {chemin_image: {sorting: {...}, analysis: {...}}, ...}
|
||||
|
||||
Flux de traitement:
|
||||
1. Préparation des données d'entrée
|
||||
2. Génération du rapport avec le LLM
|
||||
3. Sauvegarde au format JSON (format principal)
|
||||
4. Conversion et sauvegarde au format Markdown (pour présentation)
|
||||
"""
|
||||
def __init__(self, llm):
|
||||
super().__init__("AgentReportGenerator", llm)
|
||||
|
||||
# Configuration locale de l'agent (remplace AgentConfig)
|
||||
self.temperature = 0.4 # Génération de rapport factuelle mais bien structurée
|
||||
self.top_p = 0.9
|
||||
self.max_tokens = 2500
|
||||
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é et exploitable.
|
||||
|
||||
EXIGENCE ABSOLUE - TABLEAU DES ÉCHANGES CLIENT/SUPPORT:
|
||||
- Tu DOIS IMPÉRATIVEMENT créer un TABLEAU MARKDOWN des échanges client/support
|
||||
- Le format du tableau DOIT être:
|
||||
| Date | Émetteur (CLIENT/SUPPORT) | Type (Question/Réponse) | Contenu |
|
||||
|------|---------------------------|-------------------------|---------|
|
||||
| date1 | CLIENT | Question | contenu... |
|
||||
| date2 | SUPPORT | Réponse | contenu... |
|
||||
- Chaque message du ticket doit apparaître dans une ligne du tableau
|
||||
- Indique clairement qui est CLIENT et qui est SUPPORT
|
||||
- Tu dois synthétiser au mieux les échanges(le plus court et clair possible) client/support(question/réponse) dans le tableau
|
||||
- TU dois spécifié si la question n'a pas de réponse
|
||||
- Le tableau DOIT être inclus dans la section "Chronologie des échanges"
|
||||
|
||||
Structure ton rapport:
|
||||
1. Résumé exécutif: Synthèse du problème initial (nom de la demande + description)
|
||||
2. Chronologie des échanges: TABLEAU des interactions client/support (format imposé ci-dessus)
|
||||
3. Analyse des images: Ce que montrent les captures d'écran et leur pertinence
|
||||
4. Diagnostic technique: Interprétation des informations techniques pertinentes
|
||||
|
||||
Reste factuel et précis dans ton analyse.
|
||||
Le tableau des échanges client/support est l'élément le plus important du rapport."""
|
||||
|
||||
# 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
|
||||
}
|
||||
|
||||
# Ajustements selon le type de modèle
|
||||
if "mistral_medium" in self.llm.__class__.__name__.lower():
|
||||
params["temperature"] += 0.05
|
||||
params["max_tokens"] = 1000
|
||||
elif "pixtral" in self.llm.__class__.__name__.lower():
|
||||
params["temperature"] -= 0.05
|
||||
elif "ollama" in self.llm.__class__.__name__.lower():
|
||||
params["temperature"] += 0.1
|
||||
params.update({
|
||||
"num_ctx": 2048,
|
||||
"repeat_penalty": 1.1,
|
||||
})
|
||||
|
||||
self.llm.configurer(**params)
|
||||
|
||||
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
|
||||
Doit contenir au moins une des clés:
|
||||
- "ticket_analyse" ou "analyse_json": Analyse du ticket
|
||||
- "analyse_images": Analyses des images (facultatif)
|
||||
rapport_dir: Répertoire où sauvegarder le rapport
|
||||
|
||||
Returns:
|
||||
Tuple (chemin vers le rapport JSON, chemin vers le rapport Markdown)
|
||||
"""
|
||||
# Récupérer l'ID du ticket depuis les données
|
||||
ticket_id = rapport_data.get("ticket_id", "")
|
||||
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", "")
|
||||
|
||||
if not ticket_id:
|
||||
ticket_id = os.path.basename(os.path.dirname(rapport_dir))
|
||||
if not ticket_id.startswith("T"):
|
||||
# Dernier recours, utiliser le dernier segment du chemin
|
||||
ticket_id = os.path.basename(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}")
|
||||
|
||||
# Validation des données d'entrée
|
||||
logger.info("Vérification de la complétude des données d'entrée:")
|
||||
if "ticket_data" in rapport_data:
|
||||
logger.info(f" - Données de ticket présentes: {len(str(rapport_data['ticket_data']))} caractères")
|
||||
else:
|
||||
logger.warning(" - Données de ticket manquantes")
|
||||
|
||||
# Vérification des analyses
|
||||
ticket_analyse_exists = False
|
||||
if "ticket_analyse" in rapport_data and rapport_data["ticket_analyse"]:
|
||||
ticket_analyse_exists = True
|
||||
logger.info(f" - Analyse du ticket présente: {len(rapport_data['ticket_analyse'])} caractères")
|
||||
elif "analyse_json" in rapport_data and rapport_data["analyse_json"]:
|
||||
ticket_analyse_exists = True
|
||||
logger.info(f" - Analyse JSON présente: {len(rapport_data['analyse_json'])} caractères")
|
||||
else:
|
||||
logger.warning(" - Analyse du ticket manquante")
|
||||
|
||||
# Vérification des analyses d'images
|
||||
if "analyse_images" in rapport_data and rapport_data["analyse_images"]:
|
||||
n_images = len(rapport_data["analyse_images"])
|
||||
n_relevant = sum(1 for _, data in rapport_data["analyse_images"].items()
|
||||
if "sorting" in data and isinstance(data["sorting"], dict) and data["sorting"].get("is_relevant", False))
|
||||
n_analyzed = sum(1 for _, data in rapport_data["analyse_images"].items()
|
||||
if "analysis" in data and data["analysis"])
|
||||
|
||||
logger.info(f" - Analyses d'images présentes: {n_images} images, {n_relevant} pertinentes, {n_analyzed} analysées")
|
||||
else:
|
||||
logger.warning(" - Analyses d'images manquantes")
|
||||
|
||||
# S'assurer que le répertoire existe
|
||||
if not os.path.exists(rapport_dir):
|
||||
os.makedirs(rapport_dir)
|
||||
logger.info(f"Répertoire de rapport créé: {rapport_dir}")
|
||||
|
||||
try:
|
||||
# Préparer les données formatées pour l'analyse
|
||||
ticket_analyse = None
|
||||
|
||||
# Vérifier que l'analyse du ticket est disponible sous l'une des clés possibles
|
||||
if "ticket_analyse" in rapport_data and rapport_data["ticket_analyse"]:
|
||||
ticket_analyse = rapport_data["ticket_analyse"]
|
||||
logger.info("Utilisation de ticket_analyse")
|
||||
elif "analyse_json" in rapport_data and rapport_data["analyse_json"]:
|
||||
ticket_analyse = rapport_data["analyse_json"]
|
||||
logger.info("Utilisation de analyse_json en fallback")
|
||||
else:
|
||||
# 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")
|
||||
ticket_analyse = f"Analyse par défaut du ticket:\nNom: {ticket_name}\nDescription: {ticket_desc}\n(Aucune analyse détaillée n'a été fournie par l'agent d'analyse de ticket)"
|
||||
|
||||
# Préparer les données d'analyse d'images
|
||||
images_analyses = []
|
||||
analyse_images_data = rapport_data.get("analyse_images", {})
|
||||
|
||||
# Statistiques pour les métadonnées
|
||||
total_images = len(analyse_images_data) if analyse_images_data else 0
|
||||
images_pertinentes = 0
|
||||
|
||||
# Collecter des informations sur les agents et LLM utilisés
|
||||
agents_info = self._collecter_info_agents(rapport_data)
|
||||
|
||||
# Transformer les analyses d'images en liste structurée pour le prompt
|
||||
for image_path, analyse_data in analyse_images_data.items():
|
||||
image_name = os.path.basename(image_path)
|
||||
|
||||
# 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)
|
||||
if is_relevant:
|
||||
images_pertinentes += 1
|
||||
|
||||
# Récupérer l'analyse détaillée si elle existe et que l'image est pertinente
|
||||
analyse_detail = None
|
||||
if is_relevant:
|
||||
if "analysis" in analyse_data and analyse_data["analysis"]:
|
||||
# Vérifier différentes structures possibles de l'analyse
|
||||
if isinstance(analyse_data["analysis"], dict):
|
||||
if "analyse" in analyse_data["analysis"]:
|
||||
analyse_detail = analyse_data["analysis"]["analyse"]
|
||||
elif "error" in analyse_data["analysis"] and not analyse_data["analysis"].get("error", True):
|
||||
# Si pas d'erreur et que l'analyse est directement dans le dictionnaire
|
||||
analyse_detail = str(analyse_data["analysis"])
|
||||
else:
|
||||
# Essayer de récupérer directement le contenu du dictionnaire
|
||||
analyse_detail = json.dumps(analyse_data["analysis"], ensure_ascii=False, indent=2)
|
||||
elif isinstance(analyse_data["analysis"], str):
|
||||
# Si l'analyse est directement une chaîne
|
||||
analyse_detail = analyse_data["analysis"]
|
||||
|
||||
# Si l'analyse n'a pas été trouvée mais que l'image est pertinente
|
||||
if not analyse_detail:
|
||||
analyse_detail = f"Image marquée comme pertinente. Raison: {analyse_data['sorting'].get('reason', 'Non spécifiée')}"
|
||||
|
||||
# Ajouter l'analyse à la liste si elle existe
|
||||
if analyse_detail:
|
||||
images_analyses.append({
|
||||
"image_name": image_name,
|
||||
"analyse": analyse_detail
|
||||
})
|
||||
logger.info(f"Analyse de l'image {image_name} ajoutée au rapport (longueur: {len(analyse_detail)})")
|
||||
else:
|
||||
logger.warning(f"Analyse non trouvée pour l'image pertinente {image_name}")
|
||||
else:
|
||||
logger.info(f"Image {image_name} ignorée car non pertinente")
|
||||
|
||||
# Créer le chemin du fichier de rapport JSON (sortie principale)
|
||||
json_path = os.path.join(rapport_dir, f"{ticket_id}_rapport_final.json")
|
||||
md_path = os.path.join(rapport_dir, f"{ticket_id}_rapport_final.md")
|
||||
|
||||
# Formater les données pour le LLM
|
||||
prompt = self._formater_prompt_pour_rapport(ticket_analyse, images_analyses, ticket_id)
|
||||
|
||||
# Générer le rapport avec le LLM
|
||||
logger.info("Génération du rapport avec le LLM")
|
||||
print(f" Génération du rapport avec le LLM...")
|
||||
|
||||
# Interroger le LLM
|
||||
rapport_genere = self.llm.interroger(prompt)
|
||||
logger.info(f"Rapport généré: {len(rapport_genere)} caractères")
|
||||
print(f" Rapport généré: {len(rapport_genere)} caractères")
|
||||
|
||||
# Tracer l'historique avec le prompt pour la transparence
|
||||
self.ajouter_historique("generation_rapport",
|
||||
{
|
||||
"ticket_id": ticket_id,
|
||||
"prompt_taille": len(prompt),
|
||||
"timestamp": self._get_timestamp()
|
||||
},
|
||||
rapport_genere)
|
||||
|
||||
# Préparer les métadonnées complètes pour le rapport
|
||||
timestamp = self._get_timestamp()
|
||||
|
||||
# Préparer le JSON complet du rapport (format principal)
|
||||
rapport_data_complet = {
|
||||
"ticket_id": ticket_id,
|
||||
"timestamp": timestamp,
|
||||
"rapport_genere": rapport_genere,
|
||||
"ticket_analyse": ticket_analyse,
|
||||
"images_analyses": images_analyses,
|
||||
"statistiques": {
|
||||
"total_images": total_images,
|
||||
"images_pertinentes": images_pertinentes,
|
||||
"analyses_generees": len(images_analyses)
|
||||
}
|
||||
}
|
||||
|
||||
# Ajouter les métadonnées pour la traçabilité
|
||||
metadata = {
|
||||
"timestamp": timestamp,
|
||||
"model": getattr(self.llm, "modele", str(type(self.llm))),
|
||||
"temperature": self.temperature,
|
||||
"top_p": self.top_p,
|
||||
"max_tokens": self.max_tokens,
|
||||
"agents": agents_info
|
||||
}
|
||||
|
||||
rapport_data_complet["metadata"] = metadata
|
||||
|
||||
# S'assurer que les clés nécessaires pour le markdown sont présentes
|
||||
if "ticket_analyse" not in rapport_data_complet:
|
||||
rapport_data_complet["ticket_analyse"] = ticket_analyse
|
||||
|
||||
# ÉTAPE 1: Sauvegarder le rapport au format JSON (FORMAT PRINCIPAL)
|
||||
with open(json_path, "w", encoding="utf-8") as f:
|
||||
json.dump(rapport_data_complet, f, ensure_ascii=False, indent=2)
|
||||
|
||||
logger.info(f"Rapport JSON (format principal) sauvegardé: {json_path}")
|
||||
print(f" Rapport JSON sauvegardé: {json_path}")
|
||||
|
||||
# ÉTAPE 2: Générer et sauvegarder le rapport au format Markdown (pour présentation)
|
||||
markdown_content = self._generer_markdown_depuis_json(rapport_data_complet)
|
||||
|
||||
with open(md_path, "w", encoding="utf-8") as f:
|
||||
f.write(markdown_content)
|
||||
|
||||
logger.info(f"Rapport Markdown (pour présentation) sauvegardé: {md_path}")
|
||||
print(f" Rapport Markdown sauvegardé: {md_path}")
|
||||
|
||||
logger.info(f"Taille du rapport Markdown: {len(markdown_content)} caractères")
|
||||
|
||||
# Retourner les chemins des deux fichiers (JSON en premier, Markdown en second)
|
||||
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)
|
||||
print(f" ERREUR: {error_message}")
|
||||
return None, 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
|
||||
if "analyse_json" in rapport_data:
|
||||
json_analysis = rapport_data["analyse_json"]
|
||||
# Vérifier si l'analyse JSON contient des métadonnées
|
||||
if isinstance(json_analysis, dict) and "metadata" in json_analysis:
|
||||
agents_info["json_analyser"] = json_analysis["metadata"]
|
||||
|
||||
# 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"]:
|
||||
if "model_info" in img_data["sorting"]["metadata"]:
|
||||
sorter_info = img_data["sorting"]["metadata"]["model_info"]
|
||||
|
||||
# Collecter info de l'analyser
|
||||
if "analysis" in img_data and img_data["analysis"] and isinstance(img_data["analysis"], dict) and "metadata" in img_data["analysis"]:
|
||||
if "model_info" in img_data["analysis"]["metadata"]:
|
||||
analyser_info = img_data["analysis"]["metadata"]["model_info"]
|
||||
|
||||
# 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
|
||||
}
|
||||
|
||||
return agents_info
|
||||
|
||||
def _generer_markdown_depuis_json(self, rapport_data: Dict) -> str:
|
||||
"""
|
||||
Génère un rapport Markdown directement à partir des données JSON
|
||||
|
||||
Args:
|
||||
rapport_data: Données JSON complètes du rapport
|
||||
Format attendu:
|
||||
- ticket_id: ID du ticket
|
||||
- metadata: Métadonnées (timestamp, modèle, etc.)
|
||||
- rapport_genere: Texte du rapport généré par le LLM
|
||||
- ticket_analyse: Analyse du ticket
|
||||
- images_analyses: Liste des analyses d'images (format privilégié)
|
||||
OU
|
||||
- analyse_images: Dictionnaire des analyses d'images (format alternatif)
|
||||
|
||||
Returns:
|
||||
Contenu Markdown du rapport
|
||||
"""
|
||||
ticket_id = rapport_data.get("ticket_id", "")
|
||||
timestamp = rapport_data.get("metadata", {}).get("timestamp", self._get_timestamp())
|
||||
|
||||
logger.info(f"Génération du rapport Markdown pour le ticket {ticket_id}")
|
||||
|
||||
# Contenu de base du rapport (partie générée par le LLM)
|
||||
rapport_contenu = rapport_data.get("rapport_genere", "")
|
||||
|
||||
# Entête du document
|
||||
markdown = f"# Rapport d'analyse du ticket #{ticket_id}\n\n"
|
||||
markdown += f"*Généré le: {timestamp}*\n\n"
|
||||
|
||||
# Ajouter le rapport principal généré par le LLM
|
||||
markdown += rapport_contenu + "\n\n"
|
||||
|
||||
# Section séparatrice pour les détails d'analyse
|
||||
markdown += "---\n\n"
|
||||
markdown += "# Détails des analyses effectuées\n\n"
|
||||
|
||||
# Ajouter un résumé du processus d'analyse complet
|
||||
markdown += "## Processus d'analyse\n\n"
|
||||
|
||||
# 1. Analyse de ticket
|
||||
ticket_analyse = rapport_data.get("ticket_analyse", "")
|
||||
if not ticket_analyse and "analyse_json" in rapport_data:
|
||||
ticket_analyse = rapport_data.get("analyse_json", "")
|
||||
|
||||
if ticket_analyse:
|
||||
markdown += "### Étape 1: Analyse du ticket\n\n"
|
||||
markdown += "L'agent d'analyse de ticket a extrait les informations suivantes du ticket d'origine:\n\n"
|
||||
markdown += "<details>\n<summary>Cliquez pour voir l'analyse complète du ticket</summary>\n\n"
|
||||
markdown += "```\n" + ticket_analyse + "\n```\n\n"
|
||||
markdown += "</details>\n\n"
|
||||
logger.info(f"Analyse du ticket ajoutée au rapport Markdown ({len(ticket_analyse)} caractères)")
|
||||
else:
|
||||
logger.warning("Aucune analyse de ticket disponible pour le rapport Markdown")
|
||||
|
||||
# 2. Tri des images
|
||||
markdown += "### Étape 2: Tri des images\n\n"
|
||||
markdown += "L'agent de tri d'images a évalué chaque image pour déterminer sa pertinence par rapport au problème client:\n\n"
|
||||
|
||||
# Vérifier quelle structure de données est disponible pour les images
|
||||
has_analyse_images = "analyse_images" in rapport_data and rapport_data["analyse_images"]
|
||||
has_images_analyses = "images_analyses" in rapport_data and isinstance(rapport_data["images_analyses"], list)
|
||||
|
||||
logger.info(f"Structure des données d'images: analyse_images={has_analyse_images}, images_analyses={has_images_analyses}")
|
||||
|
||||
analyse_images_data = {}
|
||||
if has_analyse_images:
|
||||
analyse_images_data = rapport_data["analyse_images"]
|
||||
|
||||
if analyse_images_data:
|
||||
# Créer un tableau pour le tri des images
|
||||
markdown += "| Image | Pertinence | Raison |\n"
|
||||
markdown += "|-------|------------|--------|\n"
|
||||
|
||||
for image_path, analyse_data in analyse_images_data.items():
|
||||
image_name = os.path.basename(image_path)
|
||||
|
||||
# Information de tri
|
||||
is_relevant = "Non"
|
||||
reason = "Non spécifiée"
|
||||
|
||||
if "sorting" in analyse_data:
|
||||
sorting_data = analyse_data["sorting"]
|
||||
if isinstance(sorting_data, dict):
|
||||
is_relevant = "Oui" if sorting_data.get("is_relevant", False) else "Non"
|
||||
reason = sorting_data.get("reason", "Non spécifiée")
|
||||
|
||||
markdown += f"| {image_name} | {is_relevant} | {reason} |\n"
|
||||
|
||||
markdown += "\n"
|
||||
logger.info(f"Tableau de tri des images ajouté au rapport Markdown ({len(analyse_images_data)} images)")
|
||||
else:
|
||||
markdown += "*Aucune image n'a été trouvée ou analysée.*\n\n"
|
||||
logger.warning("Aucune analyse d'images disponible pour le tableau de tri")
|
||||
|
||||
# 3. Analyse des images pertinentes
|
||||
markdown += "### Étape 3: Analyse détaillée des images pertinentes\n\n"
|
||||
|
||||
# Traiter directement les images_analyses du rapport_data si disponible
|
||||
images_pertinentes = 0
|
||||
|
||||
# D'abord essayer d'utiliser la liste images_analyses qui est déjà traitée
|
||||
if has_images_analyses:
|
||||
images_list = rapport_data["images_analyses"]
|
||||
for i, img_data in enumerate(images_list, 1):
|
||||
images_pertinentes += 1
|
||||
image_name = img_data.get("image_name", f"Image {i}")
|
||||
analyse_detail = img_data.get("analyse", "Analyse non disponible")
|
||||
|
||||
markdown += f"#### Image pertinente {images_pertinentes}: {image_name}\n\n"
|
||||
markdown += "<details>\n<summary>Cliquez pour voir l'analyse complète de l'image</summary>\n\n"
|
||||
markdown += "```\n" + analyse_detail + "\n```\n\n"
|
||||
markdown += "</details>\n\n"
|
||||
|
||||
logger.info(f"Analyse de l'image {image_name} ajoutée au rapport Markdown (from images_analyses)")
|
||||
# Sinon, traiter les données brutes d'analyse_images
|
||||
elif has_analyse_images:
|
||||
analyse_images_data = rapport_data["analyse_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)
|
||||
|
||||
if is_relevant:
|
||||
images_pertinentes += 1
|
||||
image_name = os.path.basename(image_path)
|
||||
|
||||
# Récupérer l'analyse détaillée avec gestion des différents formats possibles
|
||||
analyse_detail = "Analyse non disponible"
|
||||
if "analysis" in analyse_data and analyse_data["analysis"]:
|
||||
if isinstance(analyse_data["analysis"], dict):
|
||||
if "analyse" in analyse_data["analysis"]:
|
||||
analyse_detail = analyse_data["analysis"]["analyse"]
|
||||
logger.info(f"Analyse de l'image {image_name} récupérée via analyse_data['analysis']['analyse']")
|
||||
elif "error" in analyse_data["analysis"] and not analyse_data["analysis"].get("error", True):
|
||||
analyse_detail = str(analyse_data["analysis"])
|
||||
logger.info(f"Analyse de l'image {image_name} récupérée via str(analyse_data['analysis'])")
|
||||
else:
|
||||
analyse_detail = json.dumps(analyse_data["analysis"], ensure_ascii=False, indent=2)
|
||||
logger.info(f"Analyse de l'image {image_name} récupérée via json.dumps")
|
||||
elif isinstance(analyse_data["analysis"], str):
|
||||
analyse_detail = analyse_data["analysis"]
|
||||
logger.info(f"Analyse de l'image {image_name} récupérée directement (string)")
|
||||
else:
|
||||
logger.warning(f"Aucune analyse disponible pour l'image pertinente {image_name}")
|
||||
|
||||
markdown += f"#### Image pertinente {images_pertinentes}: {image_name}\n\n"
|
||||
markdown += "<details>\n<summary>Cliquez pour voir l'analyse complète de l'image</summary>\n\n"
|
||||
markdown += "```\n" + analyse_detail + "\n```\n\n"
|
||||
markdown += "</details>\n\n"
|
||||
|
||||
if images_pertinentes == 0:
|
||||
markdown += "*Aucune image pertinente n'a été identifiée pour ce ticket.*\n\n"
|
||||
logger.warning("Aucune image pertinente identifiée pour l'analyse détaillée")
|
||||
else:
|
||||
logger.info(f"{images_pertinentes} images pertinentes ajoutées au rapport Markdown")
|
||||
|
||||
# 4. Synthèse (rapport final)
|
||||
markdown += "### Étape 4: Génération du rapport de synthèse\n\n"
|
||||
markdown += "L'agent de génération de rapport a synthétisé toutes les analyses précédentes pour produire le rapport ci-dessus.\n\n"
|
||||
|
||||
# Informations techniques
|
||||
markdown += "## Informations techniques\n\n"
|
||||
|
||||
# Statistiques d'analyse
|
||||
markdown += "### Statistiques\n\n"
|
||||
|
||||
total_images = 0
|
||||
if has_analyse_images:
|
||||
total_images = len(analyse_images_data)
|
||||
elif "statistiques" in rapport_data and "total_images" in rapport_data["statistiques"]:
|
||||
total_images = rapport_data["statistiques"]["total_images"]
|
||||
|
||||
markdown += f"- **Images analysées**: {total_images}\n"
|
||||
markdown += f"- **Images pertinentes**: {images_pertinentes}\n\n"
|
||||
|
||||
logger.info(f"Rapport Markdown généré ({len(markdown)} caractères)")
|
||||
return markdown
|
||||
|
||||
def _formater_prompt_pour_rapport(self, ticket_analyse, images_analyses, ticket_id):
|
||||
"""
|
||||
Formate le prompt pour la génération du rapport
|
||||
|
||||
Args:
|
||||
ticket_analyse: Analyse du ticket
|
||||
images_analyses: Liste des analyses d'images, format [{image_name, analyse}, ...]
|
||||
ticket_id: ID du ticket
|
||||
|
||||
Returns:
|
||||
Prompt formaté pour le LLM
|
||||
"""
|
||||
num_images = len(images_analyses)
|
||||
logger.info(f"Formatage du prompt avec {num_images} analyses d'images")
|
||||
|
||||
# Créer un prompt détaillé en s'assurant que toutes les analyses sont incluses
|
||||
prompt = f"""Génère un rapport technique complet pour le ticket #{ticket_id}, en te basant sur les analyses suivantes.
|
||||
|
||||
## ANALYSE DU TICKET
|
||||
{ticket_analyse}
|
||||
|
||||
## ANALYSES DES IMAGES ({num_images} images analysées)
|
||||
"""
|
||||
|
||||
# Ajouter l'analyse de chaque image
|
||||
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"
|
||||
logger.info(f"Ajout de l'analyse de l'image {image_name} au prompt ({len(analyse)} caractères)")
|
||||
|
||||
prompt += f"""
|
||||
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é et exploitable.
|
||||
|
||||
EXIGENCE ABSOLUE - TABLEAU DES ÉCHANGES CLIENT/SUPPORT:
|
||||
- Tu DOIS IMPÉRATIVEMENT créer un TABLEAU MARKDOWN des échanges client/support
|
||||
- Le format du tableau DOIT être:
|
||||
| Date | Émetteur (CLIENT/SUPPORT) | Type (Question/Réponse) | Contenu |
|
||||
|------|---------------------------|-------------------------|---------|
|
||||
| date1 | CLIENT | Question | contenu... |
|
||||
| date2 | SUPPORT | Réponse | contenu... |
|
||||
- Chaque message du ticket doit apparaître dans une ligne du tableau
|
||||
- Indique clairement qui est CLIENT et qui est SUPPORT
|
||||
- Tu dois synthétiser au mieux les échanges(le plus court et clair possible) client/support(question/réponse) dans le tableau
|
||||
- TU dois spécifié si la question n'a pas de réponse
|
||||
- Le tableau DOIT être inclus dans la section "Chronologie des échanges"
|
||||
|
||||
Structure ton rapport:
|
||||
1. Résumé exécutif: Synthèse du problème initial (nom de la demande + description)
|
||||
2. Chronologie des échanges: TABLEAU des interactions client/support (format imposé ci-dessus)
|
||||
3. Analyse des images: Ce que montrent les captures d'écran et leur pertinence
|
||||
4. Diagnostic technique: Interprétation des informations techniques pertinentes
|
||||
|
||||
Reste factuel et précis dans ton analyse.
|
||||
Le tableau des échanges client/support est l'élément le plus important du rapport."""
|
||||
|
||||
logger.info(f"Prompt formaté: {len(prompt)} caractères au total")
|
||||
return prompt
|
||||
|
||||
def _get_timestamp(self) -> str:
|
||||
"""Retourne un timestamp au format YYYYMMDD_HHMMSS"""
|
||||
return datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
@ -1,717 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
from .base_agent import BaseAgent
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Tuple, Optional
|
||||
import logging
|
||||
import traceback
|
||||
import re
|
||||
|
||||
logger = logging.getLogger("AgentReportGenerator")
|
||||
|
||||
class AgentReportGenerator(BaseAgent):
|
||||
"""
|
||||
Agent pour générer un rapport complet à partir des analyses de ticket et d'images.
|
||||
|
||||
Cet agent prend en entrée :
|
||||
- L'analyse du ticket
|
||||
- Les analyses des images pertinentes
|
||||
- Les métadonnées associées
|
||||
|
||||
Format de données attendu:
|
||||
- JSON est le format principal de données en entrée et en sortie
|
||||
- Le rapport Markdown est généré à partir du JSON uniquement pour la présentation
|
||||
|
||||
Structure des données d'analyse d'images:
|
||||
- Deux structures possibles sont supportées:
|
||||
1. Liste d'objets: rapport_data["images_analyses"] = [{image_name, analyse}, ...]
|
||||
2. Dictionnaire: rapport_data["analyse_images"] = {chemin_image: {sorting: {...}, analysis: {...}}, ...}
|
||||
|
||||
Flux de traitement:
|
||||
1. Préparation des données d'entrée
|
||||
2. Génération du rapport avec le LLM
|
||||
3. Sauvegarde au format JSON (format principal)
|
||||
4. Conversion et sauvegarde au format Markdown (pour présentation)
|
||||
"""
|
||||
def __init__(self, llm):
|
||||
super().__init__("AgentReportGenerator", llm)
|
||||
|
||||
# Configuration locale de l'agent (remplace AgentConfig)
|
||||
self.temperature = 0.4 # Génération de rapport factuelle mais bien structurée
|
||||
self.top_p = 0.9
|
||||
self.max_tokens = 2500
|
||||
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é et exploitable.
|
||||
|
||||
EXIGENCE ABSOLUE - GÉNÉRATION DE DONNÉES EN FORMAT JSON:
|
||||
- Tu DOIS IMPÉRATIVEMENT inclure dans ta réponse un objet JSON structuré pour les échanges client/support
|
||||
- Le format de chaque échange dans le JSON DOIT être:
|
||||
{
|
||||
"chronologie_echanges": [
|
||||
{
|
||||
"date": "date de l'échange",
|
||||
"emetteur": "CLIENT ou SUPPORT",
|
||||
"type": "Question ou Réponse ou Information technique",
|
||||
"contenu": "contenu synthétisé de l'échange"
|
||||
},
|
||||
... autres échanges ...
|
||||
]
|
||||
}
|
||||
- Chaque message du ticket doit apparaître comme un objet dans la liste
|
||||
- Indique clairement qui est CLIENT et qui est SUPPORT dans le champ "emetteur"
|
||||
- Si une question n'a pas de réponse, assure-toi de le noter clairement
|
||||
- Toute mention de "CBAD" doit être remplacée par "CBAO" qui est le nom correct de la société
|
||||
- Tu dois synthétiser au mieux les échanges (le plus court et clair possible)
|
||||
|
||||
Structure ton rapport:
|
||||
1. Résumé exécutif: Synthèse du problème initial (nom de la demande + description)
|
||||
2. Chronologie des échanges: Objet JSON avec la structure imposée ci-dessus
|
||||
3. Analyse des images: Ce que montrent les captures d'écran et leur pertinence
|
||||
4. Diagnostic technique: Interprétation des informations techniques pertinentes
|
||||
|
||||
Reste factuel et précis dans ton analyse.
|
||||
Les données d'échanges client/support sont l'élément le plus important du rapport.
|
||||
Tu DOIS inclure le JSON des échanges dans ta réponse au format:
|
||||
```json
|
||||
{
|
||||
"chronologie_echanges": [
|
||||
{"date": "...", "emetteur": "CLIENT", "type": "Question", "contenu": "..."},
|
||||
{"date": "...", "emetteur": "SUPPORT", "type": "Réponse", "contenu": "..."}
|
||||
]
|
||||
}
|
||||
```"""
|
||||
|
||||
# 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
|
||||
}
|
||||
|
||||
# Ajustements selon le type de modèle
|
||||
if "mistral_medium" in self.llm.__class__.__name__.lower():
|
||||
params["temperature"] += 0.05
|
||||
params["max_tokens"] = 1000
|
||||
elif "pixtral" in self.llm.__class__.__name__.lower():
|
||||
params["temperature"] -= 0.05
|
||||
elif "ollama" in self.llm.__class__.__name__.lower():
|
||||
params["temperature"] += 0.1
|
||||
params.update({
|
||||
"num_ctx": 2048,
|
||||
"repeat_penalty": 1.1,
|
||||
})
|
||||
|
||||
self.llm.configurer(**params)
|
||||
|
||||
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
|
||||
Doit contenir au moins une des clés:
|
||||
- "ticket_analyse" ou "analyse_json": Analyse du ticket
|
||||
- "analyse_images": Analyses des images (facultatif)
|
||||
rapport_dir: Répertoire où sauvegarder le rapport
|
||||
|
||||
Returns:
|
||||
Tuple (chemin vers le rapport JSON, chemin vers le rapport Markdown)
|
||||
"""
|
||||
# Récupérer l'ID du ticket depuis les données
|
||||
ticket_id = rapport_data.get("ticket_id", "")
|
||||
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", "")
|
||||
|
||||
if not ticket_id:
|
||||
ticket_id = os.path.basename(os.path.dirname(rapport_dir))
|
||||
if not ticket_id.startswith("T"):
|
||||
# Dernier recours, utiliser le dernier segment du chemin
|
||||
ticket_id = os.path.basename(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}")
|
||||
|
||||
# Validation des données d'entrée
|
||||
logger.info("Vérification de la complétude des données d'entrée:")
|
||||
if "ticket_data" in rapport_data:
|
||||
logger.info(f" - Données de ticket présentes: {len(str(rapport_data['ticket_data']))} caractères")
|
||||
else:
|
||||
logger.warning(" - Données de ticket manquantes")
|
||||
|
||||
# Vérification des analyses
|
||||
ticket_analyse_exists = False
|
||||
if "ticket_analyse" in rapport_data and rapport_data["ticket_analyse"]:
|
||||
ticket_analyse_exists = True
|
||||
logger.info(f" - Analyse du ticket présente: {len(rapport_data['ticket_analyse'])} caractères")
|
||||
elif "analyse_json" in rapport_data and rapport_data["analyse_json"]:
|
||||
ticket_analyse_exists = True
|
||||
logger.info(f" - Analyse JSON présente: {len(rapport_data['analyse_json'])} caractères")
|
||||
else:
|
||||
logger.warning(" - Analyse du ticket manquante")
|
||||
|
||||
# Vérification des analyses d'images
|
||||
if "analyse_images" in rapport_data and rapport_data["analyse_images"]:
|
||||
n_images = len(rapport_data["analyse_images"])
|
||||
n_relevant = sum(1 for _, data in rapport_data["analyse_images"].items()
|
||||
if "sorting" in data and isinstance(data["sorting"], dict) and data["sorting"].get("is_relevant", False))
|
||||
n_analyzed = sum(1 for _, data in rapport_data["analyse_images"].items()
|
||||
if "analysis" in data and data["analysis"])
|
||||
|
||||
logger.info(f" - Analyses d'images présentes: {n_images} images, {n_relevant} pertinentes, {n_analyzed} analysées")
|
||||
else:
|
||||
logger.warning(" - Analyses d'images manquantes")
|
||||
|
||||
# S'assurer que le répertoire existe
|
||||
if not os.path.exists(rapport_dir):
|
||||
os.makedirs(rapport_dir)
|
||||
logger.info(f"Répertoire de rapport créé: {rapport_dir}")
|
||||
|
||||
try:
|
||||
# Préparer les données formatées pour l'analyse
|
||||
ticket_analyse = None
|
||||
|
||||
# Vérifier que l'analyse du ticket est disponible sous l'une des clés possibles
|
||||
if "ticket_analyse" in rapport_data and rapport_data["ticket_analyse"]:
|
||||
ticket_analyse = rapport_data["ticket_analyse"]
|
||||
logger.info("Utilisation de ticket_analyse")
|
||||
elif "analyse_json" in rapport_data and rapport_data["analyse_json"]:
|
||||
ticket_analyse = rapport_data["analyse_json"]
|
||||
logger.info("Utilisation de analyse_json en fallback")
|
||||
else:
|
||||
# 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")
|
||||
ticket_analyse = f"Analyse par défaut du ticket:\nNom: {ticket_name}\nDescription: {ticket_desc}\n(Aucune analyse détaillée n'a été fournie par l'agent d'analyse de ticket)"
|
||||
|
||||
# Préparer les données d'analyse d'images
|
||||
images_analyses = []
|
||||
analyse_images_data = rapport_data.get("analyse_images", {})
|
||||
|
||||
# Statistiques pour les métadonnées
|
||||
total_images = len(analyse_images_data) if analyse_images_data else 0
|
||||
images_pertinentes = 0
|
||||
|
||||
# Collecter des informations sur les agents et LLM utilisés
|
||||
agents_info = self._collecter_info_agents(rapport_data)
|
||||
|
||||
# Transformer les analyses d'images en liste structurée pour le prompt
|
||||
for image_path, analyse_data in analyse_images_data.items():
|
||||
image_name = os.path.basename(image_path)
|
||||
|
||||
# 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)
|
||||
if is_relevant:
|
||||
images_pertinentes += 1
|
||||
|
||||
# Récupérer l'analyse détaillée si elle existe et que l'image est pertinente
|
||||
analyse_detail = None
|
||||
if is_relevant:
|
||||
if "analysis" in analyse_data and analyse_data["analysis"]:
|
||||
# Vérifier différentes structures possibles de l'analyse
|
||||
if isinstance(analyse_data["analysis"], dict):
|
||||
if "analyse" in analyse_data["analysis"]:
|
||||
analyse_detail = analyse_data["analysis"]["analyse"]
|
||||
elif "error" in analyse_data["analysis"] and not analyse_data["analysis"].get("error", True):
|
||||
# Si pas d'erreur et que l'analyse est directement dans le dictionnaire
|
||||
analyse_detail = str(analyse_data["analysis"])
|
||||
else:
|
||||
# Essayer de récupérer directement le contenu du dictionnaire
|
||||
analyse_detail = json.dumps(analyse_data["analysis"], ensure_ascii=False, indent=2)
|
||||
elif isinstance(analyse_data["analysis"], str):
|
||||
# Si l'analyse est directement une chaîne
|
||||
analyse_detail = analyse_data["analysis"]
|
||||
|
||||
# Si l'analyse n'a pas été trouvée mais que l'image est pertinente
|
||||
if not analyse_detail:
|
||||
analyse_detail = f"Image marquée comme pertinente. Raison: {analyse_data['sorting'].get('reason', 'Non spécifiée')}"
|
||||
|
||||
# Analyse détaillée
|
||||
if analyse_detail:
|
||||
images_analyses.append({
|
||||
"image_name": image_name,
|
||||
"analyse": analyse_detail
|
||||
})
|
||||
logger.info(f"Analyse de l'image {image_name} ajoutée au rapport (longueur: {len(str(analyse_detail))} caractères)")
|
||||
else:
|
||||
logger.warning(f"Analyse non trouvée pour l'image pertinente {image_name}")
|
||||
else:
|
||||
logger.info(f"Image {image_name} ignorée car non pertinente")
|
||||
|
||||
# Créer le chemin du fichier de rapport JSON (sortie principale)
|
||||
json_path = os.path.join(rapport_dir, f"{ticket_id}_rapport_final.json")
|
||||
md_path = os.path.join(rapport_dir, f"{ticket_id}_rapport_final.md")
|
||||
|
||||
# Formater les données pour le LLM
|
||||
prompt = self._formater_prompt_pour_rapport(ticket_analyse, images_analyses, ticket_id)
|
||||
|
||||
# Générer le rapport avec le LLM
|
||||
logger.info("Génération du rapport avec le LLM")
|
||||
print(f" Génération du rapport avec le LLM...")
|
||||
|
||||
# Interroger le LLM
|
||||
rapport_genere = self.llm.interroger(prompt)
|
||||
logger.info(f"Rapport généré: {len(rapport_genere)} caractères")
|
||||
print(f" Rapport généré: {len(rapport_genere)} caractères")
|
||||
|
||||
# Traiter le JSON pour extraire la chronologie des échanges et le convertir en tableau Markdown
|
||||
rapport_traite, echanges_json, echanges_markdown = self._extraire_et_traiter_json(rapport_genere)
|
||||
if echanges_json and echanges_markdown:
|
||||
logger.info(f"Échanges JSON convertis en tableau Markdown: {len(str(echanges_markdown))} caractères")
|
||||
print(f" Échanges client/support convertis du JSON vers le format Markdown")
|
||||
# Utiliser le rapport traité avec le tableau Markdown à la place du JSON
|
||||
rapport_genere = rapport_traite
|
||||
else:
|
||||
logger.warning("Aucun JSON d'échanges trouvé dans le rapport, conservation du format original")
|
||||
|
||||
# Tracer l'historique avec le prompt pour la transparence
|
||||
self.ajouter_historique("generation_rapport",
|
||||
{
|
||||
"ticket_id": ticket_id,
|
||||
"prompt_taille": len(prompt),
|
||||
"timestamp": self._get_timestamp()
|
||||
},
|
||||
rapport_genere)
|
||||
|
||||
# Préparer les métadonnées complètes pour le rapport
|
||||
timestamp = self._get_timestamp()
|
||||
|
||||
# Préparer le JSON complet du rapport (format principal)
|
||||
rapport_data_complet = {
|
||||
"ticket_id": ticket_id,
|
||||
"timestamp": timestamp,
|
||||
"rapport_genere": rapport_genere,
|
||||
"ticket_analyse": ticket_analyse,
|
||||
"images_analyses": images_analyses,
|
||||
"statistiques": {
|
||||
"total_images": total_images,
|
||||
"images_pertinentes": images_pertinentes,
|
||||
"analyses_generees": len(images_analyses)
|
||||
}
|
||||
}
|
||||
|
||||
# Ajouter les métadonnées pour la traçabilité
|
||||
metadata = {
|
||||
"timestamp": timestamp,
|
||||
"model": getattr(self.llm, "modele", str(type(self.llm))),
|
||||
"temperature": self.temperature,
|
||||
"top_p": self.top_p,
|
||||
"max_tokens": self.max_tokens,
|
||||
"agents": agents_info
|
||||
}
|
||||
|
||||
rapport_data_complet["metadata"] = metadata
|
||||
|
||||
# S'assurer que les clés nécessaires pour le markdown sont présentes
|
||||
if "ticket_analyse" not in rapport_data_complet:
|
||||
rapport_data_complet["ticket_analyse"] = ticket_analyse
|
||||
|
||||
# ÉTAPE 1: Sauvegarder le rapport au format JSON (FORMAT PRINCIPAL)
|
||||
with open(json_path, "w", encoding="utf-8") as f:
|
||||
json.dump(rapport_data_complet, f, ensure_ascii=False, indent=2)
|
||||
|
||||
logger.info(f"Rapport JSON (format principal) sauvegardé: {json_path}")
|
||||
print(f" Rapport JSON sauvegardé: {json_path}")
|
||||
|
||||
# ÉTAPE 2: Générer et sauvegarder le rapport au format Markdown (pour présentation)
|
||||
markdown_content = self._generer_markdown_depuis_json(rapport_data_complet)
|
||||
|
||||
with open(md_path, "w", encoding="utf-8") as f:
|
||||
f.write(markdown_content)
|
||||
|
||||
logger.info(f"Rapport Markdown (pour présentation) sauvegardé: {md_path}")
|
||||
print(f" Rapport Markdown sauvegardé: {md_path}")
|
||||
|
||||
logger.info(f"Taille du rapport Markdown: {len(markdown_content)} caractères")
|
||||
|
||||
# Retourner les chemins des deux fichiers (JSON en premier, Markdown en second)
|
||||
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)
|
||||
print(f" ERREUR: {error_message}")
|
||||
return None, 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
|
||||
if "analyse_json" in rapport_data:
|
||||
json_analysis = rapport_data["analyse_json"]
|
||||
# Vérifier si l'analyse JSON contient des métadonnées
|
||||
if isinstance(json_analysis, dict) and "metadata" in json_analysis:
|
||||
agents_info["json_analyser"] = json_analysis["metadata"]
|
||||
|
||||
# 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"]:
|
||||
if "model_info" in img_data["sorting"]["metadata"]:
|
||||
sorter_info = img_data["sorting"]["metadata"]["model_info"]
|
||||
|
||||
# Collecter info de l'analyser
|
||||
if "analysis" in img_data and img_data["analysis"] and isinstance(img_data["analysis"], dict) and "metadata" in img_data["analysis"]:
|
||||
if "model_info" in img_data["analysis"]["metadata"]:
|
||||
analyser_info = img_data["analysis"]["metadata"]["model_info"]
|
||||
|
||||
# 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
|
||||
}
|
||||
|
||||
return agents_info
|
||||
|
||||
def _generer_markdown_depuis_json(self, rapport_data: Dict) -> str:
|
||||
"""
|
||||
Génère un rapport Markdown directement à partir des données JSON
|
||||
|
||||
Args:
|
||||
rapport_data: Données JSON complètes du rapport
|
||||
Format attendu:
|
||||
- ticket_id: ID du ticket
|
||||
- metadata: Métadonnées (timestamp, modèle, etc.)
|
||||
- rapport_genere: Texte du rapport généré par le LLM
|
||||
- ticket_analyse: Analyse du ticket
|
||||
- images_analyses: Liste des analyses d'images (format privilégié)
|
||||
OU
|
||||
- analyse_images: Dictionnaire des analyses d'images (format alternatif)
|
||||
|
||||
Returns:
|
||||
Contenu Markdown du rapport
|
||||
"""
|
||||
ticket_id = rapport_data.get("ticket_id", "")
|
||||
timestamp = rapport_data.get("metadata", {}).get("timestamp", self._get_timestamp())
|
||||
|
||||
logger.info(f"Génération du rapport Markdown pour le ticket {ticket_id}")
|
||||
|
||||
# Contenu de base du rapport (partie générée par le LLM)
|
||||
rapport_contenu = rapport_data.get("rapport_genere", "")
|
||||
|
||||
# Entête du document
|
||||
markdown = f"# Rapport d'analyse du ticket #{ticket_id}\n\n"
|
||||
markdown += f"*Généré le: {timestamp}*\n\n"
|
||||
|
||||
# Ajouter le rapport principal généré par le LLM
|
||||
markdown += rapport_contenu + "\n\n"
|
||||
|
||||
# Section séparatrice pour les détails d'analyse
|
||||
markdown += "---\n\n"
|
||||
markdown += "# Détails des analyses effectuées\n\n"
|
||||
|
||||
# Ajouter un résumé du processus d'analyse complet
|
||||
markdown += "## Processus d'analyse\n\n"
|
||||
|
||||
# 1. Analyse de ticket
|
||||
ticket_analyse = rapport_data.get("ticket_analyse", "")
|
||||
if not ticket_analyse and "analyse_json" in rapport_data:
|
||||
ticket_analyse = rapport_data.get("analyse_json", "")
|
||||
|
||||
if ticket_analyse:
|
||||
markdown += "### Étape 1: Analyse du ticket\n\n"
|
||||
markdown += "L'agent d'analyse de ticket a extrait les informations suivantes du ticket d'origine:\n\n"
|
||||
markdown += "<details>\n<summary>Cliquez pour voir l'analyse complète du ticket</summary>\n\n"
|
||||
markdown += "```\n" + ticket_analyse + "\n```\n\n"
|
||||
markdown += "</details>\n\n"
|
||||
logger.info(f"Analyse du ticket ajoutée au rapport Markdown ({len(str(ticket_analyse))} caractères)")
|
||||
else:
|
||||
logger.warning("Aucune analyse de ticket disponible pour le rapport Markdown")
|
||||
|
||||
# 2. Tri des images
|
||||
markdown += "### Étape 2: Tri des images\n\n"
|
||||
markdown += "L'agent de tri d'images a évalué chaque image pour déterminer sa pertinence par rapport au problème client:\n\n"
|
||||
|
||||
# Vérifier quelle structure de données est disponible pour les images
|
||||
has_analyse_images = "analyse_images" in rapport_data and rapport_data["analyse_images"]
|
||||
has_images_analyses = "images_analyses" in rapport_data and isinstance(rapport_data["images_analyses"], list) and rapport_data["images_analyses"]
|
||||
|
||||
logger.info(f"Structure des données d'images: analyse_images={has_analyse_images}, images_analyses={has_images_analyses}")
|
||||
|
||||
analyse_images_data = {}
|
||||
if has_analyse_images:
|
||||
analyse_images_data = rapport_data["analyse_images"]
|
||||
|
||||
if analyse_images_data:
|
||||
# Créer un tableau pour le tri des images
|
||||
markdown += "| Image | Pertinence | Raison |\n"
|
||||
markdown += "|-------|------------|--------|\n"
|
||||
|
||||
for image_path, analyse_data in analyse_images_data.items():
|
||||
image_name = os.path.basename(image_path)
|
||||
|
||||
# Information de tri
|
||||
is_relevant = "Non"
|
||||
reason = "Non spécifiée"
|
||||
|
||||
if "sorting" in analyse_data:
|
||||
sorting_data = analyse_data["sorting"]
|
||||
if isinstance(sorting_data, dict):
|
||||
is_relevant = "Oui" if sorting_data.get("is_relevant", False) else "Non"
|
||||
reason = sorting_data.get("reason", "Non spécifiée")
|
||||
|
||||
markdown += f"| {image_name} | {is_relevant} | {reason} |\n"
|
||||
|
||||
markdown += "\n"
|
||||
logger.info(f"Tableau de tri des images ajouté au rapport Markdown ({len(analyse_images_data)} images)")
|
||||
elif has_images_analyses and rapport_data["images_analyses"]:
|
||||
# Si nous avons les analyses d'images mais pas les données de tri, créer un tableau simplifié
|
||||
markdown += "| Image | Pertinence |\n"
|
||||
markdown += "|-------|------------|\n"
|
||||
|
||||
for img_data in rapport_data["images_analyses"]:
|
||||
image_name = img_data.get("image_name", "Image inconnue")
|
||||
markdown += f"| {image_name} | Oui |\n"
|
||||
|
||||
markdown += "\n"
|
||||
logger.info(f"Tableau de tri simplifié ajouté au rapport Markdown ({len(rapport_data['images_analyses'])} images)")
|
||||
else:
|
||||
markdown += "*Aucune image n'a été trouvée ou analysée.*\n\n"
|
||||
logger.warning("Aucune analyse d'images disponible pour le tableau de tri")
|
||||
|
||||
# 3. Analyse des images pertinentes
|
||||
markdown += "### Étape 3: Analyse détaillée des images pertinentes\n\n"
|
||||
|
||||
# Traiter directement les images_analyses du rapport_data si disponible
|
||||
images_pertinentes = 0
|
||||
|
||||
# D'abord essayer d'utiliser la liste images_analyses qui est déjà traitée
|
||||
if has_images_analyses:
|
||||
images_list = rapport_data["images_analyses"]
|
||||
for i, img_data in enumerate(images_list, 1):
|
||||
images_pertinentes += 1
|
||||
image_name = img_data.get("image_name", f"Image {i}")
|
||||
analyse_detail = img_data.get("analyse", "Analyse non disponible")
|
||||
|
||||
markdown += f"#### Image pertinente {images_pertinentes}: {image_name}\n\n"
|
||||
markdown += "<details>\n<summary>Cliquez pour voir l'analyse complète de l'image</summary>\n\n"
|
||||
markdown += "```\n" + analyse_detail + "\n```\n\n"
|
||||
markdown += "</details>\n\n"
|
||||
|
||||
logger.info(f"Analyse de l'image {image_name} ajoutée au rapport Markdown (from images_analyses)")
|
||||
# Sinon, traiter les données brutes d'analyse_images
|
||||
elif has_analyse_images:
|
||||
analyse_images_data = rapport_data["analyse_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)
|
||||
|
||||
if is_relevant:
|
||||
images_pertinentes += 1
|
||||
image_name = os.path.basename(image_path)
|
||||
|
||||
# Récupérer l'analyse détaillée avec gestion des différents formats possibles
|
||||
analyse_detail = "Analyse non disponible"
|
||||
if "analysis" in analyse_data and analyse_data["analysis"]:
|
||||
if isinstance(analyse_data["analysis"], dict):
|
||||
if "analyse" in analyse_data["analysis"]:
|
||||
analyse_detail = analyse_data["analysis"]["analyse"]
|
||||
logger.info(f"Analyse de l'image {image_name} récupérée via analyse_data['analysis']['analyse']")
|
||||
elif "error" in analyse_data["analysis"] and not analyse_data["analysis"].get("error", True):
|
||||
analyse_detail = str(analyse_data["analysis"])
|
||||
logger.info(f"Analyse de l'image {image_name} récupérée via str(analyse_data['analysis'])")
|
||||
else:
|
||||
analyse_detail = json.dumps(analyse_data["analysis"], ensure_ascii=False, indent=2)
|
||||
logger.info(f"Analyse de l'image {image_name} récupérée via json.dumps")
|
||||
elif isinstance(analyse_data["analysis"], str):
|
||||
analyse_detail = analyse_data["analysis"]
|
||||
logger.info(f"Analyse de l'image {image_name} récupérée directement (string)")
|
||||
else:
|
||||
logger.warning(f"Aucune analyse disponible pour l'image pertinente {image_name}")
|
||||
|
||||
markdown += f"#### Image pertinente {images_pertinentes}: {image_name}\n\n"
|
||||
markdown += "<details>\n<summary>Cliquez pour voir l'analyse complète de l'image</summary>\n\n"
|
||||
markdown += "```\n" + analyse_detail + "\n```\n\n"
|
||||
markdown += "</details>\n\n"
|
||||
|
||||
if images_pertinentes == 0:
|
||||
markdown += "*Aucune image pertinente n'a été identifiée pour ce ticket.*\n\n"
|
||||
logger.warning("Aucune image pertinente identifiée pour l'analyse détaillée")
|
||||
else:
|
||||
logger.info(f"{images_pertinentes} images pertinentes ajoutées au rapport Markdown")
|
||||
|
||||
# 4. Synthèse (rapport final)
|
||||
markdown += "### Étape 4: Génération du rapport de synthèse\n\n"
|
||||
markdown += "L'agent de génération de rapport a synthétisé toutes les analyses précédentes pour produire le rapport ci-dessus.\n\n"
|
||||
|
||||
# Informations techniques
|
||||
markdown += "## Informations techniques\n\n"
|
||||
|
||||
# Statistiques d'analyse
|
||||
markdown += "### Statistiques\n\n"
|
||||
|
||||
total_images = 0
|
||||
if has_analyse_images:
|
||||
total_images = len(analyse_images_data)
|
||||
elif "statistiques" in rapport_data and "total_images" in rapport_data["statistiques"]:
|
||||
total_images = rapport_data["statistiques"]["total_images"]
|
||||
|
||||
markdown += f"- **Images analysées**: {total_images}\n"
|
||||
markdown += f"- **Images pertinentes**: {images_pertinentes}\n\n"
|
||||
|
||||
logger.info(f"Rapport Markdown généré ({len(markdown)} caractères)")
|
||||
return markdown
|
||||
|
||||
def _formater_prompt_pour_rapport(self, ticket_analyse, images_analyses, ticket_id):
|
||||
"""
|
||||
Formate le prompt pour la génération du rapport
|
||||
|
||||
Args:
|
||||
ticket_analyse: Analyse du ticket
|
||||
images_analyses: Liste des analyses d'images, format [{image_name, analyse}, ...]
|
||||
ticket_id: ID du ticket
|
||||
|
||||
Returns:
|
||||
Prompt formaté pour le LLM
|
||||
"""
|
||||
num_images = len(images_analyses)
|
||||
logger.info(f"Formatage du prompt avec {num_images} analyses d'images")
|
||||
|
||||
# Inclure une vérification des données reçues
|
||||
prompt = f"""Génère un rapport technique complet pour le ticket #{ticket_id}, en te basant sur les analyses suivantes.
|
||||
|
||||
## VÉRIFICATION DES DONNÉES REÇUES
|
||||
Je vais d'abord vérifier que j'ai bien reçu les données d'analyses:
|
||||
- Analyse du ticket : {"PRÉSENTE" if ticket_analyse else "MANQUANTE"}
|
||||
- Analyses d'images : {"PRÉSENTES (" + str(num_images) + " images)" if num_images > 0 else "MANQUANTES"}
|
||||
|
||||
## ANALYSE DU TICKET
|
||||
{ticket_analyse}
|
||||
|
||||
## ANALYSES DES IMAGES ({num_images} images analysées)
|
||||
"""
|
||||
|
||||
# Ajouter l'analyse de chaque image
|
||||
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"
|
||||
logger.info(f"Ajout de l'analyse de l'image {image_name} au prompt ({len(str(analyse))} caractères)")
|
||||
|
||||
# Ne pas répéter les instructions déjà présentes dans le system_prompt
|
||||
prompt += f"""
|
||||
N'oublie pas d'inclure un objet JSON structuré des échanges client/support selon le format demandé.
|
||||
Assure-toi que le JSON est valide et clairement balisé avec ```json et ``` dans ta réponse.
|
||||
"""
|
||||
|
||||
logger.info(f"Prompt formaté: {len(prompt)} caractères au total")
|
||||
return prompt
|
||||
|
||||
def _extraire_et_traiter_json(self, texte_rapport):
|
||||
"""
|
||||
Extrait l'objet JSON des échanges du texte du rapport et le convertit en Markdown
|
||||
|
||||
Args:
|
||||
texte_rapport: Texte complet du rapport généré par le LLM
|
||||
|
||||
Returns:
|
||||
Tuple (rapport_traité, echanges_json, echanges_markdown)
|
||||
"""
|
||||
# Remplacer CBAD par CBAO dans tout le rapport
|
||||
texte_rapport = texte_rapport.replace("CBAD", "CBAO")
|
||||
|
||||
# Rechercher un objet JSON dans le texte
|
||||
json_match = re.search(r'```json\s*({.*?})\s*```', texte_rapport, re.DOTALL)
|
||||
|
||||
if not json_match:
|
||||
logger.warning("Aucun JSON trouvé dans le rapport")
|
||||
return texte_rapport, None, None
|
||||
|
||||
# Extraire le JSON et le parser
|
||||
json_text = json_match.group(1)
|
||||
try:
|
||||
echanges_json = json.loads(json_text)
|
||||
logger.info(f"JSON extrait avec succès: {len(json_text)} caractères")
|
||||
|
||||
# Convertir en tableau Markdown
|
||||
echanges_markdown = "| Date | Émetteur | Type | Contenu | Statut |\n"
|
||||
echanges_markdown += "|------|---------|------|---------|--------|\n"
|
||||
|
||||
if "chronologie_echanges" in echanges_json and isinstance(echanges_json["chronologie_echanges"], list):
|
||||
# Pré-traitement pour vérifier les questions sans réponse
|
||||
questions_sans_reponse = {}
|
||||
for i, echange in enumerate(echanges_json["chronologie_echanges"]):
|
||||
if echange.get("type", "").lower() == "question" and echange.get("emetteur", "").lower() == "client":
|
||||
has_response = False
|
||||
# Vérifier si la question a une réponse
|
||||
for j in range(i+1, len(echanges_json["chronologie_echanges"])):
|
||||
next_echange = echanges_json["chronologie_echanges"][j]
|
||||
if next_echange.get("type", "").lower() == "réponse" and next_echange.get("emetteur", "").lower() == "support":
|
||||
has_response = True
|
||||
break
|
||||
questions_sans_reponse[i] = not has_response
|
||||
|
||||
# Générer le tableau
|
||||
for i, echange in enumerate(echanges_json["chronologie_echanges"]):
|
||||
date = echange.get("date", "-")
|
||||
emetteur = echange.get("emetteur", "-")
|
||||
type_msg = echange.get("type", "-")
|
||||
contenu = echange.get("contenu", "-")
|
||||
|
||||
# Ajouter un statut pour les questions sans réponse
|
||||
statut = ""
|
||||
if emetteur.lower() == "client" and type_msg.lower() == "question" and questions_sans_reponse.get(i, False):
|
||||
statut = "**Sans réponse**"
|
||||
|
||||
echanges_markdown += f"| {date} | {emetteur} | {type_msg} | {contenu} | {statut} |\n"
|
||||
|
||||
# Ajouter une note si aucune réponse du support n'a été trouvée
|
||||
if not any(echange.get("emetteur", "").lower() == "support" for echange in echanges_json["chronologie_echanges"]):
|
||||
echanges_markdown += "\n**Note: Aucune réponse du support n'a été trouvée dans ce ticket.**\n\n"
|
||||
|
||||
# Remplacer le JSON dans le texte par le tableau Markdown
|
||||
rapport_traite = texte_rapport.replace(json_match.group(0), echanges_markdown)
|
||||
|
||||
return rapport_traite, echanges_json, echanges_markdown
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Erreur lors du décodage JSON: {e}")
|
||||
return texte_rapport, None, None
|
||||
|
||||
def _get_timestamp(self) -> str:
|
||||
"""Retourne un timestamp au format YYYYMMDD_HHMMSS"""
|
||||
return datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
@ -1,717 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
from .base_agent import BaseAgent
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Tuple, Optional
|
||||
import logging
|
||||
import traceback
|
||||
import re
|
||||
|
||||
logger = logging.getLogger("AgentReportGenerator")
|
||||
|
||||
class AgentReportGenerator(BaseAgent):
|
||||
"""
|
||||
Agent pour générer un rapport complet à partir des analyses de ticket et d'images.
|
||||
|
||||
Cet agent prend en entrée :
|
||||
- L'analyse du ticket
|
||||
- Les analyses des images pertinentes
|
||||
- Les métadonnées associées
|
||||
|
||||
Format de données attendu:
|
||||
- JSON est le format principal de données en entrée et en sortie
|
||||
- Le rapport Markdown est généré à partir du JSON uniquement pour la présentation
|
||||
|
||||
Structure des données d'analyse d'images:
|
||||
- Deux structures possibles sont supportées:
|
||||
1. Liste d'objets: rapport_data["images_analyses"] = [{image_name, analyse}, ...]
|
||||
2. Dictionnaire: rapport_data["analyse_images"] = {chemin_image: {sorting: {...}, analysis: {...}}, ...}
|
||||
|
||||
Flux de traitement:
|
||||
1. Préparation des données d'entrée
|
||||
2. Génération du rapport avec le LLM
|
||||
3. Sauvegarde au format JSON (format principal)
|
||||
4. Conversion et sauvegarde au format Markdown (pour présentation)
|
||||
"""
|
||||
def __init__(self, llm):
|
||||
super().__init__("AgentReportGenerator", llm)
|
||||
|
||||
# Configuration locale de l'agent (remplace AgentConfig)
|
||||
self.temperature = 0.4 # Génération de rapport factuelle mais bien structurée
|
||||
self.top_p = 0.9
|
||||
self.max_tokens = 2500
|
||||
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é et exploitable.
|
||||
|
||||
EXIGENCE ABSOLUE - GÉNÉRATION DE DONNÉES EN FORMAT JSON:
|
||||
- Tu DOIS IMPÉRATIVEMENT inclure dans ta réponse un objet JSON structuré pour les échanges client/support
|
||||
- Le format de chaque échange dans le JSON DOIT être:
|
||||
{
|
||||
"chronologie_echanges": [
|
||||
{
|
||||
"date": "date de l'échange",
|
||||
"emetteur": "CLIENT ou SUPPORT",
|
||||
"type": "Question ou Réponse ou Information technique",
|
||||
"contenu": "contenu synthétisé de l'échange"
|
||||
},
|
||||
... autres échanges ...
|
||||
]
|
||||
}
|
||||
- Chaque message du ticket doit apparaître comme un objet dans la liste
|
||||
- Indique clairement qui est CLIENT et qui est SUPPORT dans le champ "emetteur"
|
||||
- Si une question n'a pas de réponse, assure-toi de le noter clairement
|
||||
- Toute mention de "CBAD" doit être remplacée par "CBAO" qui est le nom correct de la société
|
||||
- Tu dois synthétiser au mieux les échanges (le plus court et clair possible)
|
||||
|
||||
Structure ton rapport:
|
||||
1. Résumé exécutif: Synthèse du problème initial (nom de la demande + description)
|
||||
2. Chronologie des échanges: Objet JSON avec la structure imposée ci-dessus
|
||||
3. Analyse des images: Ce que montrent les captures d'écran et leur pertinence
|
||||
4. Diagnostic technique: Interprétation des informations techniques pertinentes
|
||||
|
||||
Reste factuel et précis dans ton analyse.
|
||||
Les données d'échanges client/support sont l'élément le plus important du rapport.
|
||||
Tu DOIS inclure le JSON des échanges dans ta réponse au format:
|
||||
```json
|
||||
{
|
||||
"chronologie_echanges": [
|
||||
{"date": "...", "emetteur": "CLIENT", "type": "Question", "contenu": "..."},
|
||||
{"date": "...", "emetteur": "SUPPORT", "type": "Réponse", "contenu": "..."}
|
||||
]
|
||||
}
|
||||
```"""
|
||||
|
||||
# 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
|
||||
}
|
||||
|
||||
# Ajustements selon le type de modèle
|
||||
if "mistral_medium" in self.llm.__class__.__name__.lower():
|
||||
params["temperature"] += 0.05
|
||||
params["max_tokens"] = 1000
|
||||
elif "pixtral" in self.llm.__class__.__name__.lower():
|
||||
params["temperature"] -= 0.05
|
||||
elif "ollama" in self.llm.__class__.__name__.lower():
|
||||
params["temperature"] += 0.1
|
||||
params.update({
|
||||
"num_ctx": 2048,
|
||||
"repeat_penalty": 1.1,
|
||||
})
|
||||
|
||||
self.llm.configurer(**params)
|
||||
|
||||
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
|
||||
Doit contenir au moins une des clés:
|
||||
- "ticket_analyse" ou "analyse_json": Analyse du ticket
|
||||
- "analyse_images": Analyses des images (facultatif)
|
||||
rapport_dir: Répertoire où sauvegarder le rapport
|
||||
|
||||
Returns:
|
||||
Tuple (chemin vers le rapport JSON, chemin vers le rapport Markdown)
|
||||
"""
|
||||
# Récupérer l'ID du ticket depuis les données
|
||||
ticket_id = rapport_data.get("ticket_id", "")
|
||||
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", "")
|
||||
|
||||
if not ticket_id:
|
||||
ticket_id = os.path.basename(os.path.dirname(rapport_dir))
|
||||
if not ticket_id.startswith("T"):
|
||||
# Dernier recours, utiliser le dernier segment du chemin
|
||||
ticket_id = os.path.basename(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}")
|
||||
|
||||
# Validation des données d'entrée
|
||||
logger.info("Vérification de la complétude des données d'entrée:")
|
||||
if "ticket_data" in rapport_data:
|
||||
logger.info(f" - Données de ticket présentes: {len(str(rapport_data['ticket_data']))} caractères")
|
||||
else:
|
||||
logger.warning(" - Données de ticket manquantes")
|
||||
|
||||
# Vérification des analyses
|
||||
ticket_analyse_exists = False
|
||||
if "ticket_analyse" in rapport_data and rapport_data["ticket_analyse"]:
|
||||
ticket_analyse_exists = True
|
||||
logger.info(f" - Analyse du ticket présente: {len(rapport_data['ticket_analyse'])} caractères")
|
||||
elif "analyse_json" in rapport_data and rapport_data["analyse_json"]:
|
||||
ticket_analyse_exists = True
|
||||
logger.info(f" - Analyse JSON présente: {len(rapport_data['analyse_json'])} caractères")
|
||||
else:
|
||||
logger.warning(" - Analyse du ticket manquante")
|
||||
|
||||
# Vérification des analyses d'images
|
||||
if "analyse_images" in rapport_data and rapport_data["analyse_images"]:
|
||||
n_images = len(rapport_data["analyse_images"])
|
||||
n_relevant = sum(1 for _, data in rapport_data["analyse_images"].items()
|
||||
if "sorting" in data and isinstance(data["sorting"], dict) and data["sorting"].get("is_relevant", False))
|
||||
n_analyzed = sum(1 for _, data in rapport_data["analyse_images"].items()
|
||||
if "analysis" in data and data["analysis"])
|
||||
|
||||
logger.info(f" - Analyses d'images présentes: {n_images} images, {n_relevant} pertinentes, {n_analyzed} analysées")
|
||||
else:
|
||||
logger.warning(" - Analyses d'images manquantes")
|
||||
|
||||
# S'assurer que le répertoire existe
|
||||
if not os.path.exists(rapport_dir):
|
||||
os.makedirs(rapport_dir)
|
||||
logger.info(f"Répertoire de rapport créé: {rapport_dir}")
|
||||
|
||||
try:
|
||||
# Préparer les données formatées pour l'analyse
|
||||
ticket_analyse = None
|
||||
|
||||
# Vérifier que l'analyse du ticket est disponible sous l'une des clés possibles
|
||||
if "ticket_analyse" in rapport_data and rapport_data["ticket_analyse"]:
|
||||
ticket_analyse = rapport_data["ticket_analyse"]
|
||||
logger.info("Utilisation de ticket_analyse")
|
||||
elif "analyse_json" in rapport_data and rapport_data["analyse_json"]:
|
||||
ticket_analyse = rapport_data["analyse_json"]
|
||||
logger.info("Utilisation de analyse_json en fallback")
|
||||
else:
|
||||
# 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")
|
||||
ticket_analyse = f"Analyse par défaut du ticket:\nNom: {ticket_name}\nDescription: {ticket_desc}\n(Aucune analyse détaillée n'a été fournie par l'agent d'analyse de ticket)"
|
||||
|
||||
# Préparer les données d'analyse d'images
|
||||
images_analyses = []
|
||||
analyse_images_data = rapport_data.get("analyse_images", {})
|
||||
|
||||
# Statistiques pour les métadonnées
|
||||
total_images = len(analyse_images_data) if analyse_images_data else 0
|
||||
images_pertinentes = 0
|
||||
|
||||
# Collecter des informations sur les agents et LLM utilisés
|
||||
agents_info = self._collecter_info_agents(rapport_data)
|
||||
|
||||
# Transformer les analyses d'images en liste structurée pour le prompt
|
||||
for image_path, analyse_data in analyse_images_data.items():
|
||||
image_name = os.path.basename(image_path)
|
||||
|
||||
# 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)
|
||||
if is_relevant:
|
||||
images_pertinentes += 1
|
||||
|
||||
# Récupérer l'analyse détaillée si elle existe et que l'image est pertinente
|
||||
analyse_detail = None
|
||||
if is_relevant:
|
||||
if "analysis" in analyse_data and analyse_data["analysis"]:
|
||||
# Vérifier différentes structures possibles de l'analyse
|
||||
if isinstance(analyse_data["analysis"], dict):
|
||||
if "analyse" in analyse_data["analysis"]:
|
||||
analyse_detail = analyse_data["analysis"]["analyse"]
|
||||
elif "error" in analyse_data["analysis"] and not analyse_data["analysis"].get("error", True):
|
||||
# Si pas d'erreur et que l'analyse est directement dans le dictionnaire
|
||||
analyse_detail = str(analyse_data["analysis"])
|
||||
else:
|
||||
# Essayer de récupérer directement le contenu du dictionnaire
|
||||
analyse_detail = json.dumps(analyse_data["analysis"], ensure_ascii=False, indent=2)
|
||||
elif isinstance(analyse_data["analysis"], str):
|
||||
# Si l'analyse est directement une chaîne
|
||||
analyse_detail = analyse_data["analysis"]
|
||||
|
||||
# Si l'analyse n'a pas été trouvée mais que l'image est pertinente
|
||||
if not analyse_detail:
|
||||
analyse_detail = f"Image marquée comme pertinente. Raison: {analyse_data['sorting'].get('reason', 'Non spécifiée')}"
|
||||
|
||||
# Analyse détaillée
|
||||
if analyse_detail:
|
||||
images_analyses.append({
|
||||
"image_name": image_name,
|
||||
"analyse": analyse_detail
|
||||
})
|
||||
logger.info(f"Analyse de l'image {image_name} ajoutée au rapport (longueur: {len(str(analyse_detail))} caractères)")
|
||||
else:
|
||||
logger.warning(f"Analyse non trouvée pour l'image pertinente {image_name}")
|
||||
else:
|
||||
logger.info(f"Image {image_name} ignorée car non pertinente")
|
||||
|
||||
# Créer le chemin du fichier de rapport JSON (sortie principale)
|
||||
json_path = os.path.join(rapport_dir, f"{ticket_id}_rapport_final.json")
|
||||
md_path = os.path.join(rapport_dir, f"{ticket_id}_rapport_final.md")
|
||||
|
||||
# Formater les données pour le LLM
|
||||
prompt = self._formater_prompt_pour_rapport(ticket_analyse, images_analyses, ticket_id)
|
||||
|
||||
# Générer le rapport avec le LLM
|
||||
logger.info("Génération du rapport avec le LLM")
|
||||
print(f" Génération du rapport avec le LLM...")
|
||||
|
||||
# Interroger le LLM
|
||||
rapport_genere = self.llm.interroger(prompt)
|
||||
logger.info(f"Rapport généré: {len(rapport_genere)} caractères")
|
||||
print(f" Rapport généré: {len(rapport_genere)} caractères")
|
||||
|
||||
# Traiter le JSON pour extraire la chronologie des échanges et le convertir en tableau Markdown
|
||||
rapport_traite, echanges_json, echanges_markdown = self._extraire_et_traiter_json(rapport_genere)
|
||||
if echanges_json and echanges_markdown:
|
||||
logger.info(f"Échanges JSON convertis en tableau Markdown: {len(str(echanges_markdown))} caractères")
|
||||
print(f" Échanges client/support convertis du JSON vers le format Markdown")
|
||||
# Utiliser le rapport traité avec le tableau Markdown à la place du JSON
|
||||
rapport_genere = rapport_traite
|
||||
else:
|
||||
logger.warning("Aucun JSON d'échanges trouvé dans le rapport, conservation du format original")
|
||||
|
||||
# Tracer l'historique avec le prompt pour la transparence
|
||||
self.ajouter_historique("generation_rapport",
|
||||
{
|
||||
"ticket_id": ticket_id,
|
||||
"prompt_taille": len(prompt),
|
||||
"timestamp": self._get_timestamp()
|
||||
},
|
||||
rapport_genere)
|
||||
|
||||
# Préparer les métadonnées complètes pour le rapport
|
||||
timestamp = self._get_timestamp()
|
||||
|
||||
# Préparer le JSON complet du rapport (format principal)
|
||||
rapport_data_complet = {
|
||||
"ticket_id": ticket_id,
|
||||
"timestamp": timestamp,
|
||||
"rapport_genere": rapport_genere,
|
||||
"ticket_analyse": ticket_analyse,
|
||||
"images_analyses": images_analyses,
|
||||
"statistiques": {
|
||||
"total_images": total_images,
|
||||
"images_pertinentes": images_pertinentes,
|
||||
"analyses_generees": len(images_analyses)
|
||||
}
|
||||
}
|
||||
|
||||
# Ajouter les métadonnées pour la traçabilité
|
||||
metadata = {
|
||||
"timestamp": timestamp,
|
||||
"model": getattr(self.llm, "modele", str(type(self.llm))),
|
||||
"temperature": self.temperature,
|
||||
"top_p": self.top_p,
|
||||
"max_tokens": self.max_tokens,
|
||||
"agents": agents_info
|
||||
}
|
||||
|
||||
rapport_data_complet["metadata"] = metadata
|
||||
|
||||
# S'assurer que les clés nécessaires pour le markdown sont présentes
|
||||
if "ticket_analyse" not in rapport_data_complet:
|
||||
rapport_data_complet["ticket_analyse"] = ticket_analyse
|
||||
|
||||
# ÉTAPE 1: Sauvegarder le rapport au format JSON (FORMAT PRINCIPAL)
|
||||
with open(json_path, "w", encoding="utf-8") as f:
|
||||
json.dump(rapport_data_complet, f, ensure_ascii=False, indent=2)
|
||||
|
||||
logger.info(f"Rapport JSON (format principal) sauvegardé: {json_path}")
|
||||
print(f" Rapport JSON sauvegardé: {json_path}")
|
||||
|
||||
# ÉTAPE 2: Générer et sauvegarder le rapport au format Markdown (pour présentation)
|
||||
markdown_content = self._generer_markdown_depuis_json(rapport_data_complet)
|
||||
|
||||
with open(md_path, "w", encoding="utf-8") as f:
|
||||
f.write(markdown_content)
|
||||
|
||||
logger.info(f"Rapport Markdown (pour présentation) sauvegardé: {md_path}")
|
||||
print(f" Rapport Markdown sauvegardé: {md_path}")
|
||||
|
||||
logger.info(f"Taille du rapport Markdown: {len(markdown_content)} caractères")
|
||||
|
||||
# Retourner les chemins des deux fichiers (JSON en premier, Markdown en second)
|
||||
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)
|
||||
print(f" ERREUR: {error_message}")
|
||||
return None, 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
|
||||
if "analyse_json" in rapport_data:
|
||||
json_analysis = rapport_data["analyse_json"]
|
||||
# Vérifier si l'analyse JSON contient des métadonnées
|
||||
if isinstance(json_analysis, dict) and "metadata" in json_analysis:
|
||||
agents_info["json_analyser"] = json_analysis["metadata"]
|
||||
|
||||
# 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"]:
|
||||
if "model_info" in img_data["sorting"]["metadata"]:
|
||||
sorter_info = img_data["sorting"]["metadata"]["model_info"]
|
||||
|
||||
# Collecter info de l'analyser
|
||||
if "analysis" in img_data and img_data["analysis"] and isinstance(img_data["analysis"], dict) and "metadata" in img_data["analysis"]:
|
||||
if "model_info" in img_data["analysis"]["metadata"]:
|
||||
analyser_info = img_data["analysis"]["metadata"]["model_info"]
|
||||
|
||||
# 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
|
||||
}
|
||||
|
||||
return agents_info
|
||||
|
||||
def _generer_markdown_depuis_json(self, rapport_data: Dict) -> str:
|
||||
"""
|
||||
Génère un rapport Markdown directement à partir des données JSON
|
||||
|
||||
Args:
|
||||
rapport_data: Données JSON complètes du rapport
|
||||
Format attendu:
|
||||
- ticket_id: ID du ticket
|
||||
- metadata: Métadonnées (timestamp, modèle, etc.)
|
||||
- rapport_genere: Texte du rapport généré par le LLM
|
||||
- ticket_analyse: Analyse du ticket
|
||||
- images_analyses: Liste des analyses d'images (format privilégié)
|
||||
OU
|
||||
- analyse_images: Dictionnaire des analyses d'images (format alternatif)
|
||||
|
||||
Returns:
|
||||
Contenu Markdown du rapport
|
||||
"""
|
||||
ticket_id = rapport_data.get("ticket_id", "")
|
||||
timestamp = rapport_data.get("metadata", {}).get("timestamp", self._get_timestamp())
|
||||
|
||||
logger.info(f"Génération du rapport Markdown pour le ticket {ticket_id}")
|
||||
|
||||
# Contenu de base du rapport (partie générée par le LLM)
|
||||
rapport_contenu = rapport_data.get("rapport_genere", "")
|
||||
|
||||
# Entête du document
|
||||
markdown = f"# Rapport d'analyse du ticket #{ticket_id}\n\n"
|
||||
markdown += f"*Généré le: {timestamp}*\n\n"
|
||||
|
||||
# Ajouter le rapport principal généré par le LLM
|
||||
markdown += rapport_contenu + "\n\n"
|
||||
|
||||
# Section séparatrice pour les détails d'analyse
|
||||
markdown += "---\n\n"
|
||||
markdown += "# Détails des analyses effectuées\n\n"
|
||||
|
||||
# Ajouter un résumé du processus d'analyse complet
|
||||
markdown += "## Processus d'analyse\n\n"
|
||||
|
||||
# 1. Analyse de ticket
|
||||
ticket_analyse = rapport_data.get("ticket_analyse", "")
|
||||
if not ticket_analyse and "analyse_json" in rapport_data:
|
||||
ticket_analyse = rapport_data.get("analyse_json", "")
|
||||
|
||||
if ticket_analyse:
|
||||
markdown += "### Étape 1: Analyse du ticket\n\n"
|
||||
markdown += "L'agent d'analyse de ticket a extrait les informations suivantes du ticket d'origine:\n\n"
|
||||
markdown += "<details>\n<summary>Cliquez pour voir l'analyse complète du ticket</summary>\n\n"
|
||||
markdown += "```\n" + ticket_analyse + "\n```\n\n"
|
||||
markdown += "</details>\n\n"
|
||||
logger.info(f"Analyse du ticket ajoutée au rapport Markdown ({len(str(ticket_analyse))} caractères)")
|
||||
else:
|
||||
logger.warning("Aucune analyse de ticket disponible pour le rapport Markdown")
|
||||
|
||||
# 2. Tri des images
|
||||
markdown += "### Étape 2: Tri des images\n\n"
|
||||
markdown += "L'agent de tri d'images a évalué chaque image pour déterminer sa pertinence par rapport au problème client:\n\n"
|
||||
|
||||
# Vérifier quelle structure de données est disponible pour les images
|
||||
has_analyse_images = "analyse_images" in rapport_data and rapport_data["analyse_images"]
|
||||
has_images_analyses = "images_analyses" in rapport_data and isinstance(rapport_data["images_analyses"], list) and rapport_data["images_analyses"]
|
||||
|
||||
logger.info(f"Structure des données d'images: analyse_images={has_analyse_images}, images_analyses={has_images_analyses}")
|
||||
|
||||
analyse_images_data = {}
|
||||
if has_analyse_images:
|
||||
analyse_images_data = rapport_data["analyse_images"]
|
||||
|
||||
if analyse_images_data:
|
||||
# Créer un tableau pour le tri des images
|
||||
markdown += "| Image | Pertinence | Raison |\n"
|
||||
markdown += "|-------|------------|--------|\n"
|
||||
|
||||
for image_path, analyse_data in analyse_images_data.items():
|
||||
image_name = os.path.basename(image_path)
|
||||
|
||||
# Information de tri
|
||||
is_relevant = "Non"
|
||||
reason = "Non spécifiée"
|
||||
|
||||
if "sorting" in analyse_data:
|
||||
sorting_data = analyse_data["sorting"]
|
||||
if isinstance(sorting_data, dict):
|
||||
is_relevant = "Oui" if sorting_data.get("is_relevant", False) else "Non"
|
||||
reason = sorting_data.get("reason", "Non spécifiée")
|
||||
|
||||
markdown += f"| {image_name} | {is_relevant} | {reason} |\n"
|
||||
|
||||
markdown += "\n"
|
||||
logger.info(f"Tableau de tri des images ajouté au rapport Markdown ({len(analyse_images_data)} images)")
|
||||
elif has_images_analyses and rapport_data["images_analyses"]:
|
||||
# Si nous avons les analyses d'images mais pas les données de tri, créer un tableau simplifié
|
||||
markdown += "| Image | Pertinence |\n"
|
||||
markdown += "|-------|------------|\n"
|
||||
|
||||
for img_data in rapport_data["images_analyses"]:
|
||||
image_name = img_data.get("image_name", "Image inconnue")
|
||||
markdown += f"| {image_name} | Oui |\n"
|
||||
|
||||
markdown += "\n"
|
||||
logger.info(f"Tableau de tri simplifié ajouté au rapport Markdown ({len(rapport_data['images_analyses'])} images)")
|
||||
else:
|
||||
markdown += "*Aucune image n'a été trouvée ou analysée.*\n\n"
|
||||
logger.warning("Aucune analyse d'images disponible pour le tableau de tri")
|
||||
|
||||
# 3. Analyse des images pertinentes
|
||||
markdown += "### Étape 3: Analyse détaillée des images pertinentes\n\n"
|
||||
|
||||
# Traiter directement les images_analyses du rapport_data si disponible
|
||||
images_pertinentes = 0
|
||||
|
||||
# D'abord essayer d'utiliser la liste images_analyses qui est déjà traitée
|
||||
if has_images_analyses:
|
||||
images_list = rapport_data["images_analyses"]
|
||||
for i, img_data in enumerate(images_list, 1):
|
||||
images_pertinentes += 1
|
||||
image_name = img_data.get("image_name", f"Image {i}")
|
||||
analyse_detail = img_data.get("analyse", "Analyse non disponible")
|
||||
|
||||
markdown += f"#### Image pertinente {images_pertinentes}: {image_name}\n\n"
|
||||
markdown += "<details>\n<summary>Cliquez pour voir l'analyse complète de l'image</summary>\n\n"
|
||||
markdown += "```\n" + analyse_detail + "\n```\n\n"
|
||||
markdown += "</details>\n\n"
|
||||
|
||||
logger.info(f"Analyse de l'image {image_name} ajoutée au rapport Markdown (from images_analyses)")
|
||||
# Sinon, traiter les données brutes d'analyse_images
|
||||
elif has_analyse_images:
|
||||
analyse_images_data = rapport_data["analyse_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)
|
||||
|
||||
if is_relevant:
|
||||
images_pertinentes += 1
|
||||
image_name = os.path.basename(image_path)
|
||||
|
||||
# Récupérer l'analyse détaillée avec gestion des différents formats possibles
|
||||
analyse_detail = "Analyse non disponible"
|
||||
if "analysis" in analyse_data and analyse_data["analysis"]:
|
||||
if isinstance(analyse_data["analysis"], dict):
|
||||
if "analyse" in analyse_data["analysis"]:
|
||||
analyse_detail = analyse_data["analysis"]["analyse"]
|
||||
logger.info(f"Analyse de l'image {image_name} récupérée via analyse_data['analysis']['analyse']")
|
||||
elif "error" in analyse_data["analysis"] and not analyse_data["analysis"].get("error", True):
|
||||
analyse_detail = str(analyse_data["analysis"])
|
||||
logger.info(f"Analyse de l'image {image_name} récupérée via str(analyse_data['analysis'])")
|
||||
else:
|
||||
analyse_detail = json.dumps(analyse_data["analysis"], ensure_ascii=False, indent=2)
|
||||
logger.info(f"Analyse de l'image {image_name} récupérée via json.dumps")
|
||||
elif isinstance(analyse_data["analysis"], str):
|
||||
analyse_detail = analyse_data["analysis"]
|
||||
logger.info(f"Analyse de l'image {image_name} récupérée directement (string)")
|
||||
else:
|
||||
logger.warning(f"Aucune analyse disponible pour l'image pertinente {image_name}")
|
||||
|
||||
markdown += f"#### Image pertinente {images_pertinentes}: {image_name}\n\n"
|
||||
markdown += "<details>\n<summary>Cliquez pour voir l'analyse complète de l'image</summary>\n\n"
|
||||
markdown += "```\n" + analyse_detail + "\n```\n\n"
|
||||
markdown += "</details>\n\n"
|
||||
|
||||
if images_pertinentes == 0:
|
||||
markdown += "*Aucune image pertinente n'a été identifiée pour ce ticket.*\n\n"
|
||||
logger.warning("Aucune image pertinente identifiée pour l'analyse détaillée")
|
||||
else:
|
||||
logger.info(f"{images_pertinentes} images pertinentes ajoutées au rapport Markdown")
|
||||
|
||||
# 4. Synthèse (rapport final)
|
||||
markdown += "### Étape 4: Génération du rapport de synthèse\n\n"
|
||||
markdown += "L'agent de génération de rapport a synthétisé toutes les analyses précédentes pour produire le rapport ci-dessus.\n\n"
|
||||
|
||||
# Informations techniques
|
||||
markdown += "## Informations techniques\n\n"
|
||||
|
||||
# Statistiques d'analyse
|
||||
markdown += "### Statistiques\n\n"
|
||||
|
||||
total_images = 0
|
||||
if has_analyse_images:
|
||||
total_images = len(analyse_images_data)
|
||||
elif "statistiques" in rapport_data and "total_images" in rapport_data["statistiques"]:
|
||||
total_images = rapport_data["statistiques"]["total_images"]
|
||||
|
||||
markdown += f"- **Images analysées**: {total_images}\n"
|
||||
markdown += f"- **Images pertinentes**: {images_pertinentes}\n\n"
|
||||
|
||||
logger.info(f"Rapport Markdown généré ({len(markdown)} caractères)")
|
||||
return markdown
|
||||
|
||||
def _formater_prompt_pour_rapport(self, ticket_analyse, images_analyses, ticket_id):
|
||||
"""
|
||||
Formate le prompt pour la génération du rapport
|
||||
|
||||
Args:
|
||||
ticket_analyse: Analyse du ticket
|
||||
images_analyses: Liste des analyses d'images, format [{image_name, analyse}, ...]
|
||||
ticket_id: ID du ticket
|
||||
|
||||
Returns:
|
||||
Prompt formaté pour le LLM
|
||||
"""
|
||||
num_images = len(images_analyses)
|
||||
logger.info(f"Formatage du prompt avec {num_images} analyses d'images")
|
||||
|
||||
# Inclure une vérification des données reçues
|
||||
prompt = f"""Génère un rapport technique complet pour le ticket #{ticket_id}, en te basant sur les analyses suivantes.
|
||||
|
||||
## VÉRIFICATION DES DONNÉES REÇUES
|
||||
Je vais d'abord vérifier que j'ai bien reçu les données d'analyses:
|
||||
- Analyse du ticket : {"PRÉSENTE" if ticket_analyse else "MANQUANTE"}
|
||||
- Analyses d'images : {"PRÉSENTES (" + str(num_images) + " images)" if num_images > 0 else "MANQUANTES"}
|
||||
|
||||
## ANALYSE DU TICKET
|
||||
{ticket_analyse}
|
||||
|
||||
## ANALYSES DES IMAGES ({num_images} images analysées)
|
||||
"""
|
||||
|
||||
# Ajouter l'analyse de chaque image
|
||||
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"
|
||||
logger.info(f"Ajout de l'analyse de l'image {image_name} au prompt ({len(str(analyse))} caractères)")
|
||||
|
||||
# Ne pas répéter les instructions déjà présentes dans le system_prompt
|
||||
prompt += f"""
|
||||
N'oublie pas d'inclure un objet JSON structuré des échanges client/support selon le format demandé.
|
||||
Assure-toi que le JSON est valide et clairement balisé avec ```json et ``` dans ta réponse.
|
||||
"""
|
||||
|
||||
logger.info(f"Prompt formaté: {len(prompt)} caractères au total")
|
||||
return prompt
|
||||
|
||||
def _extraire_et_traiter_json(self, texte_rapport):
|
||||
"""
|
||||
Extrait l'objet JSON des échanges du texte du rapport et le convertit en Markdown
|
||||
|
||||
Args:
|
||||
texte_rapport: Texte complet du rapport généré par le LLM
|
||||
|
||||
Returns:
|
||||
Tuple (rapport_traité, echanges_json, echanges_markdown)
|
||||
"""
|
||||
# Remplacer CBAD par CBAO dans tout le rapport
|
||||
texte_rapport = texte_rapport.replace("CBAD", "CBAO")
|
||||
|
||||
# Rechercher un objet JSON dans le texte
|
||||
json_match = re.search(r'```json\s*({.*?})\s*```', texte_rapport, re.DOTALL)
|
||||
|
||||
if not json_match:
|
||||
logger.warning("Aucun JSON trouvé dans le rapport")
|
||||
return texte_rapport, None, None
|
||||
|
||||
# Extraire le JSON et le parser
|
||||
json_text = json_match.group(1)
|
||||
try:
|
||||
echanges_json = json.loads(json_text)
|
||||
logger.info(f"JSON extrait avec succès: {len(json_text)} caractères")
|
||||
|
||||
# Convertir en tableau Markdown
|
||||
echanges_markdown = "| Date | Émetteur | Type | Contenu | Statut |\n"
|
||||
echanges_markdown += "|------|---------|------|---------|--------|\n"
|
||||
|
||||
if "chronologie_echanges" in echanges_json and isinstance(echanges_json["chronologie_echanges"], list):
|
||||
# Pré-traitement pour vérifier les questions sans réponse
|
||||
questions_sans_reponse = {}
|
||||
for i, echange in enumerate(echanges_json["chronologie_echanges"]):
|
||||
if echange.get("type", "").lower() == "question" and echange.get("emetteur", "").lower() == "client":
|
||||
has_response = False
|
||||
# Vérifier si la question a une réponse
|
||||
for j in range(i+1, len(echanges_json["chronologie_echanges"])):
|
||||
next_echange = echanges_json["chronologie_echanges"][j]
|
||||
if next_echange.get("type", "").lower() == "réponse" and next_echange.get("emetteur", "").lower() == "support":
|
||||
has_response = True
|
||||
break
|
||||
questions_sans_reponse[i] = not has_response
|
||||
|
||||
# Générer le tableau
|
||||
for i, echange in enumerate(echanges_json["chronologie_echanges"]):
|
||||
date = echange.get("date", "-")
|
||||
emetteur = echange.get("emetteur", "-")
|
||||
type_msg = echange.get("type", "-")
|
||||
contenu = echange.get("contenu", "-")
|
||||
|
||||
# Ajouter un statut pour les questions sans réponse
|
||||
statut = ""
|
||||
if emetteur.lower() == "client" and type_msg.lower() == "question" and questions_sans_reponse.get(i, False):
|
||||
statut = "**Sans réponse**"
|
||||
|
||||
echanges_markdown += f"| {date} | {emetteur} | {type_msg} | {contenu} | {statut} |\n"
|
||||
|
||||
# Ajouter une note si aucune réponse du support n'a été trouvée
|
||||
if not any(echange.get("emetteur", "").lower() == "support" for echange in echanges_json["chronologie_echanges"]):
|
||||
echanges_markdown += "\n**Note: Aucune réponse du support n'a été trouvée dans ce ticket.**\n\n"
|
||||
|
||||
# Remplacer le JSON dans le texte par le tableau Markdown
|
||||
rapport_traite = texte_rapport.replace(json_match.group(0), echanges_markdown)
|
||||
|
||||
return rapport_traite, echanges_json, echanges_markdown
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Erreur lors du décodage JSON: {e}")
|
||||
return texte_rapport, None, None
|
||||
|
||||
def _get_timestamp(self) -> str:
|
||||
"""Retourne un timestamp au format YYYYMMDD_HHMMSS"""
|
||||
return datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
@ -24,9 +24,9 @@ class AgentTicketAnalyser(BaseAgent):
|
||||
self.temperature = 0.1 # Besoin d'analyse très précise
|
||||
self.top_p = 0.8
|
||||
self.max_tokens = 1500
|
||||
self.system_prompt = """Tu es un expert en analyse de tickets pour le support informatique de BRG-Lab.
|
||||
Ton rôle est d'extraire et d'analyser les informations importantes des tickets.
|
||||
|
||||
|
||||
# Centralisation des objectifs d'analyse
|
||||
self.objectifs_analyse = """
|
||||
Ta mission principale:
|
||||
1. Mettre en perspective le NOM DE LA DEMANDE qui contient souvent le problème soulevé par le client
|
||||
2. Analyser la DESCRIPTION du problème qui ajoute du contexte
|
||||
@ -34,14 +34,23 @@ Ta mission principale:
|
||||
- Les questions posées par le client
|
||||
- Les réponses fournies par le support
|
||||
- Les informations techniques fournies par chaque partie
|
||||
- Fourinir un tableau clair des questions/Réponses support/client sur deux colonnes
|
||||
"""
|
||||
|
||||
Sois factuel et reste dans une démarche technique. Ton analyse sera utilisée comme contexte pour l'analyse des images pertinentes.
|
||||
|
||||
# Centralisation de la structure de réponse
|
||||
self.structure_reponse = """
|
||||
Structure ta réponse:
|
||||
1. Analyse du problème initial (nom de la demande + description)
|
||||
2. Informations techniques essentielles (logiciels, versions, configurations)
|
||||
3. Chronologie des échanges client/support avec identification claire des questions/réponses
|
||||
"""
|
||||
|
||||
# Construction du prompt système
|
||||
self.system_prompt = f"""Tu es un expert en analyse de tickets pour le support informatique de BRG-Lab.
|
||||
Ton rôle est d'extraire et d'analyser les informations importantes des tickets.
|
||||
{self.objectifs_analyse}
|
||||
Sois factuel et reste dans une démarche technique. Ton analyse sera utilisée comme contexte pour l'analyse des images pertinentes.
|
||||
{self.structure_reponse}"""
|
||||
|
||||
# Initialiser le loader de données
|
||||
self.ticket_loader = TicketDataLoader()
|
||||
@ -67,21 +76,31 @@ Structure ta réponse:
|
||||
"max_tokens": self.max_tokens
|
||||
}
|
||||
|
||||
# Ajustements selon le type de modèle
|
||||
if "mistral_medium" in self.llm.__class__.__name__.lower():
|
||||
params["temperature"] += 0.05
|
||||
params["max_tokens"] = 1000
|
||||
elif "pixtral" in self.llm.__class__.__name__.lower():
|
||||
params["temperature"] -= 0.05
|
||||
elif "ollama" in self.llm.__class__.__name__.lower():
|
||||
params["temperature"] += 0.1
|
||||
params.update({
|
||||
"num_ctx": 2048,
|
||||
"repeat_penalty": 1.1,
|
||||
})
|
||||
|
||||
self.llm.configurer(**params)
|
||||
|
||||
def _generer_prompt_analyse(self, ticket_formate: str, source_format: str) -> str:
|
||||
"""
|
||||
Génère le prompt d'analyse standardisé
|
||||
|
||||
Args:
|
||||
ticket_formate: Texte du ticket formaté pour l'analyse
|
||||
source_format: Format source du ticket (JSON, Markdown, etc.)
|
||||
|
||||
Returns:
|
||||
Prompt formaté pour l'analyse du ticket
|
||||
"""
|
||||
return f"""Analyse ce ticket de support technique et fournis une synthèse structurée:
|
||||
|
||||
{ticket_formate}
|
||||
|
||||
Concentre-toi sur:
|
||||
1. L'analyse du problème initial décrit dans le nom de la demande et la description
|
||||
2. L'extraction des informations techniques importantes
|
||||
3. L'établissement d'une chronologie claire des échanges client/support en identifiant précisément les questions posées et les réponses fournies
|
||||
|
||||
Ce ticket provient d'un fichier au format {source_format.upper()}.
|
||||
Réponds de manière factuelle, en te basant uniquement sur les informations fournies."""
|
||||
|
||||
def executer(self, ticket_data: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Analyse un ticket pour en extraire les informations pertinentes
|
||||
@ -126,17 +145,7 @@ Structure ta réponse:
|
||||
ticket_formate = self._formater_ticket_pour_analyse(ticket_data)
|
||||
|
||||
# Créer le prompt pour l'analyse, adapté au format source
|
||||
prompt = f"""Analyse ce ticket de support technique et fournis une synthèse structurée:
|
||||
|
||||
{ticket_formate}
|
||||
|
||||
Concentre-toi sur:
|
||||
1. L'analyse du problème initial décrit dans le nom de la demande et la description
|
||||
2. L'extraction des informations techniques importantes
|
||||
3. L'établissement d'une chronologie claire des échanges client/support en identifiant précisément les questions posées et les réponses fournies
|
||||
|
||||
Ce ticket provient d'un fichier au format {source_format.upper()}.
|
||||
Réponds de manière factuelle, en te basant uniquement sur les informations fournies."""
|
||||
prompt = self._generer_prompt_analyse(ticket_formate, source_format)
|
||||
|
||||
try:
|
||||
logger.info("Interrogation du LLM")
|
||||
|
||||
@ -3,9 +3,10 @@ import json
|
||||
import logging
|
||||
import time
|
||||
import traceback
|
||||
from typing import List, Dict, Any, Optional, Union
|
||||
from typing import List, Dict, Any, Optional, Union, Mapping, cast
|
||||
from agents.base_agent import BaseAgent
|
||||
from utils.ticket_data_loader import TicketDataLoader
|
||||
from utils.report_formatter import generate_markdown_report
|
||||
|
||||
# Configuration du logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
@ -111,20 +112,34 @@ class Orchestrator:
|
||||
|
||||
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)
|
||||
Cherche les rapports disponibles (JSON et/ou MD) pour un ticket
|
||||
|
||||
Args:
|
||||
extraction_path: Chemin de l'extraction
|
||||
ticket_id: ID du ticket (ex: T0101)
|
||||
extraction_path: Chemin vers l'extraction
|
||||
ticket_id: ID du ticket
|
||||
|
||||
Returns:
|
||||
Un dictionnaire avec les chemins des fichiers JSON et MD s'ils sont trouvés
|
||||
Dictionnaire avec {"json": chemin_json, "markdown": chemin_md}
|
||||
"""
|
||||
# Utilise la nouvelle méthode de TicketDataLoader
|
||||
resultats = self.ticket_loader.trouver_ticket(extraction_path, ticket_id)
|
||||
if resultats is None:
|
||||
return {"json": None, "markdown": None}
|
||||
return resultats
|
||||
# Utiliser la méthode du TicketDataLoader pour trouver les fichiers
|
||||
result = self.ticket_loader.trouver_ticket(extraction_path, ticket_id)
|
||||
|
||||
# S'assurer que nous avons un dictionnaire avec la structure correcte
|
||||
rapports: Dict[str, Optional[str]] = {"json": None, "markdown": None} if result is None else result
|
||||
|
||||
# Si on a un JSON mais pas de Markdown, générer le Markdown à partir du JSON
|
||||
json_path = rapports.get("json")
|
||||
if json_path and not rapports.get("markdown"):
|
||||
logger.info(f"Rapport JSON trouvé sans Markdown correspondant, génération du Markdown: {json_path}")
|
||||
|
||||
success, md_path_or_error = generate_markdown_report(json_path)
|
||||
if success:
|
||||
rapports["markdown"] = md_path_or_error
|
||||
logger.info(f"Markdown généré avec succès: {md_path_or_error}")
|
||||
else:
|
||||
logger.warning(f"Erreur lors de la génération du Markdown: {md_path_or_error}")
|
||||
|
||||
return rapports
|
||||
|
||||
def traiter_ticket(self, ticket_path: str) -> bool:
|
||||
"""Traite un ticket spécifique et retourne True si le traitement a réussi"""
|
||||
@ -313,16 +328,29 @@ class Orchestrator:
|
||||
os.makedirs(rapport_path, exist_ok=True)
|
||||
|
||||
# Générer le rapport
|
||||
json_path, md_path = self.report_generator.executer(rapport_data, rapport_path)
|
||||
json_path = self.report_generator.executer(rapport_data, rapport_path)
|
||||
|
||||
if json_path and md_path:
|
||||
logger.info(f"Rapport généré à: {rapport_path}")
|
||||
print(f" Rapport généré avec succès")
|
||||
print(f" - JSON: {os.path.basename(json_path)}")
|
||||
print(f" - Markdown: {os.path.basename(md_path)}")
|
||||
if json_path:
|
||||
logger.info(f"Rapport JSON généré à: {rapport_path}")
|
||||
print(f" Rapport JSON généré avec succès: {os.path.basename(json_path)}")
|
||||
|
||||
# Générer le rapport Markdown à partir du JSON en utilisant report_formatter
|
||||
success, md_path = generate_markdown_report(json_path)
|
||||
|
||||
if success:
|
||||
logger.info(f"Rapport Markdown généré à: {rapport_path}")
|
||||
print(f" Rapport Markdown généré avec succès: {os.path.basename(md_path)}")
|
||||
# Vérifier si le rapport Markdown contient un tableau des échanges
|
||||
with open(md_path, "r", encoding="utf-8") as f:
|
||||
md_content = f.read()
|
||||
has_exchanges = "| Date | Émetteur |" in md_content
|
||||
logger.info(f"Vérification du rapport Markdown: Tableau des échanges {'présent' if has_exchanges else 'absent'}")
|
||||
else:
|
||||
logger.warning(f"Erreur lors de la génération du Markdown: {md_path}")
|
||||
print(f" ERREUR: Problème lors de la génération du rapport Markdown")
|
||||
else:
|
||||
logger.warning("Erreur lors de la génération du rapport")
|
||||
print(f" ERREUR: Problème lors de la génération du rapport")
|
||||
logger.warning("Erreur lors de la génération du rapport JSON")
|
||||
print(f" ERREUR: Problème lors de la génération du rapport JSON")
|
||||
else:
|
||||
logger.warning("Report Generator non disponible")
|
||||
print(" Report Generator non disponible, génération de rapport ignorée")
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,154 @@
|
||||
# Rapport d'analyse du ticket #T0101
|
||||
|
||||
*Généré le: 2025-04-08 12:02:34*
|
||||
|
||||
Résumé exécutif:
|
||||
Le client a réinstallé le logiciel ESQ sur un nouveau serveur pour permettre le télétravail. Cependant, lors de l'activation du logiciel, il est incertain si le numéro de licence a été modifié suite à un achat de version réseau en 2019 par JB Lafitte ou si le problème est différent.
|
||||
|
||||
## Chronologie des échanges
|
||||
|
||||
| Date | Émetteur | Type | Contenu | Statut |
|
||||
|------|---------|------|---------|--------|
|
||||
| 26/03/2020 | CLIENT | Question | Besoin d'aide pour l'activation du logiciel ESQ sur le nouveau serveur, incertain si le numéro de licence a été modifié ou non. | **Sans réponse** |
|
||||
| 26/03/2020 | CLIENT | Information technique | Réinstallation du logiciel ESQ sur un nouveau serveur, achat d'une version réseau en 2019 par JB Lafitte. | |
|
||||
|
||||
**Note: Aucune réponse du support n'a été trouvée dans ce ticket.**
|
||||
|
||||
## Analyse des images
|
||||
|
||||
### Image 1: image005.jpg
|
||||
|
||||
**Raison de la pertinence**: oui. L'image montre une fenêtre d'activation de logiciel, ce qui est pertinent pour le support technique de logiciels.
|
||||
|
||||
<details>
|
||||
<summary>Analyse détaillée de l'image</summary>
|
||||
|
||||
```
|
||||
### Analyse d'image
|
||||
|
||||
#### 1. Description objective
|
||||
L'image montre une fenêtre d'activation de logiciel intitulée "Activation du logiciel". La fenêtre contient un champ pour entrer l'ID du logiciel, un message d'instructions, et trois options pour l'activation du logiciel.
|
||||
|
||||
#### 2. Éléments techniques clés
|
||||
- **Titre de la fenêtre**: "Activation du logiciel"
|
||||
- **Champ d'ID du logiciel**: "ID du logiciel" avec un champ de texte vide
|
||||
- **Message d'instructions**:
|
||||
- "Afin d'activer votre logiciel, veuillez saisir l'ID du logiciel fourni par CBAD."
|
||||
- "Si vous ne disposez pas de votre ID de logiciel, veuillez contacter CBAD par mail à l'adresse suivante : support@cbad.com ou par téléphone au 01 60 61 53 15 ou en cliquant sur le bouton téléphone situé en haut à droite de la fenêtre."
|
||||
- **Options d'activation**:
|
||||
- "Activer le logiciel (par internet)"
|
||||
- "Activer plus tard (4 jours restants)"
|
||||
- "Activation par téléphone"
|
||||
|
||||
#### 3. Relation avec le problème
|
||||
L'image se rapporte au problème décrit dans le ticket de support concernant l'activation du logiciel ESQ. Le message d'instructions indique que le client doit contacter CBAD pour obtenir l'ID du logiciel, ce qui pourrait être pertinent pour résoudre le problème d'activation. Les options d'activation montrent que le logiciel permet plusieurs méthodes d'activation, y compris par internet et par téléphone, ce qui pourrait être utile pour le client en fonction de la situation.
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Diagnostic technique
|
||||
|
||||
Diagnostic technique:
|
||||
Le client a besoin d'aide pour activer le logiciel ESQ sur le nouveau serveur. Il est possible que le numéro de licence ait été modifié suite à l'achat de la version réseau en 2019. Le client doit contacter le support CBAO pour obtenir l'ID de logiciel nécessaire à l'activation.
|
||||
|
||||
---
|
||||
|
||||
# Détails des analyses effectuées
|
||||
|
||||
## Processus d'analyse
|
||||
|
||||
### Étape 1: Analyse du ticket
|
||||
|
||||
L'agent d'analyse de ticket a extrait les informations suivantes du ticket d'origine:
|
||||
|
||||
<details>
|
||||
<summary>Cliquez pour voir l'analyse complète du ticket</summary>
|
||||
|
||||
```
|
||||
1. Analyse du problème initial
|
||||
- Nom de la demande: Activation Logiciel
|
||||
- Description: Problème de licence
|
||||
- Problème initial: Le client a réinstallé le logiciel ESQ sur un autre serveur pour permettre le télétravail. Cependant, le logiciel demande une activation et le client est incertain si le numéro de licence a été modifié suite à un achat de version réseau en 2019 par JB Lafitte ou si le problème est différent.
|
||||
|
||||
2. Informations techniques essentielles
|
||||
- Logiciel: ESQ
|
||||
- Version: Non spécifiée dans les informations fournies
|
||||
- Configuration: Réinstallation sur un nouveau serveur pour le télétravail
|
||||
- Licence: Possibilité d'un changement de numéro de licence suite à un achat de version réseau en 2019
|
||||
|
||||
3. Chronologie des échanges client/support
|
||||
- Message 1 - [AUTRE] De: Inconnu - Date: 26/03/2020 14:43:45
|
||||
- Question du client: Le client demande de l'aide pour l'activation du logiciel ESQ sur le nouveau serveur, étant incertain si le numéro de licence a été modifié ou non.
|
||||
- Informations techniques fournies par le client: Réinstallation du logiciel ESQ sur un nouveau serveur, achat d'une version réseau en 2019 par JB Lafitte.
|
||||
- Pièces jointes: Deux images (image006.jpg et image005.jpg) montrant probablement la fenêtre d'activation du logiciel.
|
||||
|
||||
Aucune réponse du support n'est fournie dans les informations données.
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Étape 2: Tri des images
|
||||
|
||||
L'agent de tri d'images a évalué chaque image pour déterminer sa pertinence par rapport au problème client:
|
||||
|
||||
| Image | Pertinence | Raison |
|
||||
|-------|------------|--------|
|
||||
| image005.jpg | Oui | oui. L'image montre une fenêtre d'activation de logiciel, ce qui est pertinent pour le support technique de logiciels. |
|
||||
|
||||
### Étape 3: Analyse détaillée des images pertinentes
|
||||
|
||||
#### Image pertinente 1: image005.jpg
|
||||
|
||||
<details>
|
||||
<summary>Cliquez pour voir l'analyse complète de l'image</summary>
|
||||
|
||||
```
|
||||
### Analyse d'image
|
||||
|
||||
#### 1. Description objective
|
||||
L'image montre une fenêtre d'activation de logiciel intitulée "Activation du logiciel". La fenêtre contient un champ pour entrer l'ID du logiciel, un message d'instructions, et trois options pour l'activation du logiciel.
|
||||
|
||||
#### 2. Éléments techniques clés
|
||||
- **Titre de la fenêtre**: "Activation du logiciel"
|
||||
- **Champ d'ID du logiciel**: "ID du logiciel" avec un champ de texte vide
|
||||
- **Message d'instructions**:
|
||||
- "Afin d'activer votre logiciel, veuillez saisir l'ID du logiciel fourni par CBAD."
|
||||
- "Si vous ne disposez pas de votre ID de logiciel, veuillez contacter CBAD par mail à l'adresse suivante : support@cbad.com ou par téléphone au 01 60 61 53 15 ou en cliquant sur le bouton téléphone situé en haut à droite de la fenêtre."
|
||||
- **Options d'activation**:
|
||||
- "Activer le logiciel (par internet)"
|
||||
- "Activer plus tard (4 jours restants)"
|
||||
- "Activation par téléphone"
|
||||
|
||||
#### 3. Relation avec le problème
|
||||
L'image se rapporte au problème décrit dans le ticket de support concernant l'activation du logiciel ESQ. Le message d'instructions indique que le client doit contacter CBAD pour obtenir l'ID du logiciel, ce qui pourrait être pertinent pour résoudre le problème d'activation. Les options d'activation montrent que le logiciel permet plusieurs méthodes d'activation, y compris par internet et par téléphone, ce qui pourrait être utile pour le client en fonction de la situation.
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Étape 4: Génération du rapport de synthèse
|
||||
|
||||
L'agent de génération de rapport a synthétisé toutes les analyses précédentes pour produire le rapport ci-dessus.
|
||||
|
||||
## Informations techniques
|
||||
|
||||
### Statistiques
|
||||
|
||||
- **Images analysées**: 2
|
||||
- **Images pertinentes**: 1
|
||||
- **Temps de génération**: 12.27 secondes
|
||||
|
||||
### Modèle LLM utilisé
|
||||
|
||||
- **Modèle**: mistral-medium
|
||||
- **Version**: non spécifiée
|
||||
- **Température**: 0.4
|
||||
- **Top_p**: 0.9
|
||||
|
||||
### Agents impliqués
|
||||
|
||||
|
||||
#### Agent de tri d'images
|
||||
- **Modèle**: Non spécifié
|
||||
|
||||
#### Agent d'analyse d'images
|
||||
- **Modèle**: Non spécifié
|
||||
@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import os
|
||||
from agents.agent_json_analyser import AgentJsonAnalyser
|
||||
from agents.agent_ticket_analyser import AgentTicketAnalyser
|
||||
from agents.agent_image_sorter import AgentImageSorter
|
||||
from agents.agent_image_analyser import AgentImageAnalyser
|
||||
from agents.agent_report_generator import AgentReportGenerator
|
||||
@ -44,11 +45,11 @@ def test_different_models():
|
||||
print(f"Test avec le modèle {model_name}...")
|
||||
|
||||
# Créer l'agent avec ce modèle
|
||||
json_agent = AgentJsonAnalyser(model)
|
||||
json_agent = AgentTicketAnalyser(model)
|
||||
|
||||
# Tester les paramètres appliqués
|
||||
print(f" Paramètres: {json_agent.config.get_params()}")
|
||||
print(f" Prompt système: {json_agent.config.get_system_prompt()[:50]}...")
|
||||
# Afficher les paramètres de l'agent
|
||||
print(f" Température: {json_agent.temperature}")
|
||||
print(f" Prompt système: {json_agent.system_prompt[:50]}...")
|
||||
|
||||
# Exécuter le test
|
||||
try:
|
||||
@ -62,7 +63,7 @@ def test_different_models():
|
||||
results[model_name] = {
|
||||
"result": result,
|
||||
"success": success,
|
||||
"metadata": json_agent.historique[-1]["metadata"] if json_agent.historique else None
|
||||
"metadata": json_agent.historique[-1]["metadata"] if json_agent.historique and json_agent.historique else {}
|
||||
}
|
||||
|
||||
print(f" Succès: {success}")
|
||||
@ -71,14 +72,13 @@ def test_different_models():
|
||||
# Générer un rapport comparatif
|
||||
print("Génération du rapport comparatif...")
|
||||
report_generator = AgentReportGenerator(MistralLarge())
|
||||
json_path, md_path = report_generator.executer(
|
||||
json_path = report_generator.executer(
|
||||
{"resultats_comparatifs": results},
|
||||
"comparaison_modeles"
|
||||
)
|
||||
|
||||
print(f"Rapport généré avec succès!")
|
||||
print(f"JSON: {json_path}")
|
||||
print(f"Markdown: {md_path}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_different_models()
|
||||
8
test_import.py
Normal file
8
test_import.py
Normal file
@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import agents.agent_ticket_analyser
|
||||
import agents.agent_image_sorter
|
||||
import agents.agent_image_analyser
|
||||
import agents.agent_report_generator
|
||||
|
||||
print('Tests réussis! Tous les agents ont été importés correctement.')
|
||||
@ -84,25 +84,6 @@ def test_orchestrator(ticket_id=None):
|
||||
image_analyser = AgentImageAnalyser(image_analyser_llm)
|
||||
report_generator = AgentReportGenerator(report_generator_llm)
|
||||
|
||||
# Renforcer le system prompt du générateur de rapport pour s'assurer que le tableau est généré
|
||||
report_generator.system_prompt = """Tu es un expert en génération de rapports techniques pour BRG-Lab.
|
||||
Ta mission est de synthétiser les analyses (ticket et images) en un rapport structuré et exploitable.
|
||||
|
||||
IMPORTANCE DES ÉCHANGES CLIENT/SUPPORT:
|
||||
- Tu dois IMPÉRATIVEMENT présenter les échanges client/support sous forme d'un TABLEAU MARKDOWN clair
|
||||
- Chaque ligne du tableau doit contenir: Date | Émetteur | Type (Question/Réponse) | Contenu
|
||||
- Identifie clairement qui est l'émetteur (CLIENT ou SUPPORT)
|
||||
- Mets en évidence les questions posées et les réponses fournies
|
||||
|
||||
Structure ton rapport:
|
||||
1. Résumé exécutif: Synthèse du problème initial (nom de la demande + description)
|
||||
2. Chronologie des échanges: TABLEAU des interactions client/support
|
||||
3. Analyse des images: Ce que montrent les captures d'écran et leur pertinence
|
||||
4. Diagnostic technique: Interprétation des informations techniques pertinentes
|
||||
|
||||
Reste factuel et précis. Ne fais pas d'analyses inutiles ou de recommandations non fondées.
|
||||
Ton rapport doit mettre en avant la chronologie des échanges et les informations techniques clés."""
|
||||
|
||||
print("Tous les agents ont été créés")
|
||||
|
||||
# Initialisation de l'orchestrateur avec les agents
|
||||
|
||||
@ -83,26 +83,7 @@ def test_orchestrator(ticket_id=None):
|
||||
image_sorter = AgentImageSorter(image_sorter_llm)
|
||||
image_analyser = AgentImageAnalyser(image_analyser_llm)
|
||||
report_generator = AgentReportGenerator(report_generator_llm)
|
||||
|
||||
# Renforcer le system prompt du générateur de rapport pour s'assurer que le tableau est généré
|
||||
report_generator.system_prompt = """Tu es un expert en génération de rapports techniques pour BRG-Lab.
|
||||
Ta mission est de synthétiser les analyses (ticket et images) en un rapport structuré et exploitable.
|
||||
|
||||
IMPORTANCE DES ÉCHANGES CLIENT/SUPPORT:
|
||||
- Tu dois IMPÉRATIVEMENT présenter les échanges client/support sous forme d'un TABLEAU MARKDOWN clair
|
||||
- Chaque ligne du tableau doit contenir: Date | Émetteur | Type (Question/Réponse) | Contenu
|
||||
- Identifie clairement qui est l'émetteur (CLIENT ou SUPPORT)
|
||||
- Mets en évidence les questions posées et les réponses fournies
|
||||
|
||||
Structure ton rapport:
|
||||
1. Résumé exécutif: Synthèse du problème initial (nom de la demande + description)
|
||||
2. Chronologie des échanges: TABLEAU des interactions client/support
|
||||
3. Analyse des images: Ce que montrent les captures d'écran et leur pertinence
|
||||
4. Diagnostic technique: Interprétation des informations techniques pertinentes
|
||||
|
||||
Reste factuel et précis. Ne fais pas d'analyses inutiles ou de recommandations non fondées.
|
||||
Ton rapport doit mettre en avant la chronologie des échanges et les informations techniques clés."""
|
||||
|
||||
|
||||
print("Tous les agents ont été créés")
|
||||
|
||||
# Initialisation de l'orchestrateur avec les agents
|
||||
|
||||
@ -84,25 +84,6 @@ def test_orchestrator(ticket_id=None):
|
||||
image_analyser = AgentImageAnalyser(image_analyser_llm)
|
||||
report_generator = AgentReportGenerator(report_generator_llm)
|
||||
|
||||
# Renforcer le system prompt du générateur de rapport pour s'assurer que le tableau est généré
|
||||
report_generator.system_prompt = """Tu es un expert en génération de rapports techniques pour BRG-Lab.
|
||||
Ta mission est de synthétiser les analyses (ticket et images) en un rapport structuré et exploitable.
|
||||
|
||||
IMPORTANCE DES ÉCHANGES CLIENT/SUPPORT:
|
||||
- Tu dois IMPÉRATIVEMENT présenter les échanges client/support sous forme d'un TABLEAU MARKDOWN clair
|
||||
- Chaque ligne du tableau doit contenir: Date | Émetteur | Type (Question/Réponse) | Contenu
|
||||
- Identifie clairement qui est l'émetteur (CLIENT ou SUPPORT)
|
||||
- Mets en évidence les questions posées et les réponses fournies
|
||||
|
||||
Structure ton rapport:
|
||||
1. Résumé exécutif: Synthèse du problème initial (nom de la demande + description)
|
||||
2. Chronologie des échanges: TABLEAU des interactions client/support
|
||||
3. Analyse des images: Ce que montrent les captures d'écran et leur pertinence
|
||||
4. Diagnostic technique: Interprétation des informations techniques pertinentes
|
||||
|
||||
Reste factuel et précis. Ne fais pas d'analyses inutiles ou de recommandations non fondées.
|
||||
Ton rapport doit mettre en avant la chronologie des échanges et les informations techniques clés."""
|
||||
|
||||
print("Tous les agents ont été créés")
|
||||
|
||||
# Initialisation de l'orchestrateur avec les agents
|
||||
|
||||
98
test_tableau_qr.py
Normal file
98
test_tableau_qr.py
Normal file
@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from agents.agent_report_generator import AgentReportGenerator
|
||||
from llm_classes.ollama import Ollama # Pour avoir une instance LLM
|
||||
|
||||
def test_tableau_qr():
|
||||
"""Test de la génération du tableau questions/réponses"""
|
||||
|
||||
# Créer un exemple d'échanges
|
||||
echanges = [
|
||||
{
|
||||
"date": "2023-01-10",
|
||||
"emetteur": "CLIENT",
|
||||
"type": "Question",
|
||||
"contenu": "Bonjour, j'ai un problème avec l'activation de mon logiciel. Il me demande un code que je n'ai plus."
|
||||
},
|
||||
{
|
||||
"date": "2023-01-11",
|
||||
"emetteur": "SUPPORT",
|
||||
"type": "Réponse",
|
||||
"contenu": "Bonjour, pouvez-vous nous fournir votre numéro de licence qui se trouve sur votre contrat?"
|
||||
},
|
||||
{
|
||||
"date": "2023-01-12",
|
||||
"emetteur": "CLIENT",
|
||||
"type": "Question",
|
||||
"contenu": "J'ai regardé sur mon contrat et le numéro est BRG-12345. Mais l'application ne l'accepte pas. Y a-t-il un format particulier à respecter?"
|
||||
},
|
||||
{
|
||||
"date": "2023-01-12",
|
||||
"emetteur": "CLIENT",
|
||||
"type": "Information technique",
|
||||
"contenu": "Je suis sur Windows 10 version 21H2."
|
||||
},
|
||||
{
|
||||
"date": "2023-01-13",
|
||||
"emetteur": "SUPPORT",
|
||||
"type": "Réponse",
|
||||
"contenu": "Le format correct est BRG-xxxxx-yyyy où yyyy correspond à l'année de votre contrat. Essayez avec BRG-12345-2023."
|
||||
},
|
||||
{
|
||||
"date": "2023-01-14",
|
||||
"emetteur": "CLIENT",
|
||||
"type": "Question",
|
||||
"contenu": "Cela ne fonctionne toujours pas. Y a-t-il une autre solution?"
|
||||
}
|
||||
]
|
||||
|
||||
# Créer une instance de l'agent
|
||||
llm = Ollama("llama2") # Ollama est léger pour le test
|
||||
agent = AgentReportGenerator(llm)
|
||||
|
||||
# Tester la méthode _generer_tableau_questions_reponses
|
||||
tableau = agent._generer_tableau_questions_reponses(echanges)
|
||||
print("TABLEAU QUESTIONS/RÉPONSES:")
|
||||
print(tableau)
|
||||
|
||||
# Tester avec un long contenu pour voir la synthèse
|
||||
long_echange = [
|
||||
{
|
||||
"date": "2023-01-10",
|
||||
"emetteur": "CLIENT",
|
||||
"type": "Question",
|
||||
"contenu": "Bonjour, j'ai un problème très complexe avec l'activation de mon logiciel. " * 10
|
||||
},
|
||||
{
|
||||
"date": "2023-01-11",
|
||||
"emetteur": "SUPPORT",
|
||||
"type": "Réponse",
|
||||
"contenu": "Bonjour, nous avons bien reçu votre demande et nous allons vous aider à résoudre ce problème. " * 10
|
||||
}
|
||||
]
|
||||
|
||||
tableau_long = agent._generer_tableau_questions_reponses(long_echange)
|
||||
print("\nTABLEAU AVEC CONTENU LONG (SYNTHÉTISÉ):")
|
||||
print(tableau_long)
|
||||
|
||||
# Tester avec une question sans réponse
|
||||
sans_reponse = [
|
||||
{
|
||||
"date": "2023-01-10",
|
||||
"emetteur": "CLIENT",
|
||||
"type": "Question",
|
||||
"contenu": "Bonjour, j'ai un problème avec mon logiciel. Pouvez-vous m'aider?"
|
||||
}
|
||||
]
|
||||
|
||||
tableau_sans_reponse = agent._generer_tableau_questions_reponses(sans_reponse)
|
||||
print("\nTABLEAU AVEC QUESTION SANS RÉPONSE:")
|
||||
print(tableau_sans_reponse)
|
||||
|
||||
print("\nTest terminé avec succès!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_tableau_qr()
|
||||
432
utils/report_formatter.py
Normal file
432
utils/report_formatter.py
Normal file
@ -0,0 +1,432 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Module pour formater les rapports à partir des fichiers JSON générés par l'AgentReportGenerator.
|
||||
|
||||
Ce module prend en entrée un fichier JSON contenant les analyses et génère différents
|
||||
formats de sortie (Markdown, HTML, etc.) sans utiliser de LLM.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import argparse
|
||||
import sys
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
|
||||
def generate_markdown_report(json_path: str, output_path: Optional[str] = None) -> Tuple[bool, str]:
|
||||
"""
|
||||
Génère un rapport au format Markdown à partir d'un fichier JSON.
|
||||
|
||||
Args:
|
||||
json_path: Chemin vers le fichier JSON contenant les données du rapport
|
||||
output_path: Chemin de sortie pour le fichier Markdown (facultatif)
|
||||
|
||||
Returns:
|
||||
Tuple (succès, chemin du fichier généré ou message d'erreur)
|
||||
"""
|
||||
try:
|
||||
# Lire le fichier JSON
|
||||
with open(json_path, "r", encoding="utf-8") as f:
|
||||
rapport_data = json.load(f)
|
||||
|
||||
# Si le chemin de sortie n'est pas spécifié, le créer à partir du chemin d'entrée
|
||||
if not output_path:
|
||||
# Remplacer l'extension JSON par MD
|
||||
output_path = os.path.splitext(json_path)[0] + ".md"
|
||||
|
||||
# Générer le contenu Markdown
|
||||
markdown_content = _generate_markdown_content(rapport_data)
|
||||
|
||||
# Écrire le contenu dans le fichier de sortie
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
f.write(markdown_content)
|
||||
|
||||
print(f"Rapport Markdown généré avec succès: {output_path}")
|
||||
return True, output_path
|
||||
|
||||
except Exception as e:
|
||||
error_message = f"Erreur lors de la génération du rapport Markdown: {str(e)}"
|
||||
print(error_message)
|
||||
return False, error_message
|
||||
|
||||
def _generate_markdown_content(rapport_data: Dict) -> str:
|
||||
"""
|
||||
Génère le contenu Markdown à partir des données du rapport.
|
||||
|
||||
Args:
|
||||
rapport_data: Dictionnaire contenant les données du rapport
|
||||
|
||||
Returns:
|
||||
Contenu Markdown
|
||||
"""
|
||||
ticket_id = rapport_data.get("ticket_id", "")
|
||||
timestamp = rapport_data.get("metadata", {}).get("timestamp", "")
|
||||
generation_date = rapport_data.get("metadata", {}).get("generation_date", datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
||||
|
||||
# Entête du document
|
||||
markdown = f"# Rapport d'analyse du ticket #{ticket_id}\n\n"
|
||||
markdown += f"*Généré le: {generation_date}*\n\n"
|
||||
|
||||
# 1. Résumé exécutif
|
||||
if "resume" in rapport_data and rapport_data["resume"]:
|
||||
markdown += rapport_data["resume"] + "\n\n"
|
||||
|
||||
# 2. Chronologie des échanges (tableau)
|
||||
markdown += "## Chronologie des échanges\n\n"
|
||||
|
||||
if "chronologie_echanges" in rapport_data and rapport_data["chronologie_echanges"]:
|
||||
# Créer un tableau pour les échanges
|
||||
markdown += "| Date | Émetteur | Type | Contenu | Statut |\n"
|
||||
markdown += "|------|---------|------|---------|--------|\n"
|
||||
|
||||
# Prétraitement pour détecter les questions sans réponse
|
||||
questions_sans_reponse = {}
|
||||
echanges = rapport_data["chronologie_echanges"]
|
||||
|
||||
for i, echange in enumerate(echanges):
|
||||
if echange.get("type", "").lower() == "question" and echange.get("emetteur", "").lower() == "client":
|
||||
has_response = False
|
||||
# Vérifier si la question a une réponse
|
||||
for j in range(i+1, len(echanges)):
|
||||
next_echange = echanges[j]
|
||||
if next_echange.get("type", "").lower() == "réponse" and next_echange.get("emetteur", "").lower() == "support":
|
||||
has_response = True
|
||||
break
|
||||
questions_sans_reponse[i] = not has_response
|
||||
|
||||
# Générer les lignes du tableau
|
||||
for i, echange in enumerate(echanges):
|
||||
date = echange.get("date", "-")
|
||||
emetteur = echange.get("emetteur", "-")
|
||||
type_msg = echange.get("type", "-")
|
||||
contenu = echange.get("contenu", "-")
|
||||
|
||||
# Ajouter un statut pour les questions sans réponse
|
||||
statut = ""
|
||||
if emetteur.lower() == "client" and type_msg.lower() == "question" and questions_sans_reponse.get(i, False):
|
||||
statut = "**Sans réponse**"
|
||||
|
||||
markdown += f"| {date} | {emetteur} | {type_msg} | {contenu} | {statut} |\n"
|
||||
|
||||
# Ajouter une note si aucune réponse du support n'a été trouvée
|
||||
if not any(echange.get("emetteur", "").lower() == "support" for echange in echanges):
|
||||
markdown += "\n**Note: Aucune réponse du support n'a été trouvée dans ce ticket.**\n\n"
|
||||
else:
|
||||
markdown += "*Aucun échange détecté dans le ticket.*\n\n"
|
||||
|
||||
# 3. Analyse des images
|
||||
markdown += "## Analyse des images\n\n"
|
||||
|
||||
if "images_analyses" in rapport_data and rapport_data["images_analyses"]:
|
||||
images_list = rapport_data["images_analyses"]
|
||||
|
||||
if not images_list:
|
||||
markdown += "*Aucune image pertinente n'a été identifiée.*\n\n"
|
||||
else:
|
||||
for i, img_data in enumerate(images_list, 1):
|
||||
image_name = img_data.get("image_name", f"Image {i}")
|
||||
sorting_info = img_data.get("sorting_info", {})
|
||||
reason = sorting_info.get("reason", "Non spécifiée")
|
||||
|
||||
markdown += f"### Image {i}: {image_name}\n\n"
|
||||
|
||||
# Raison de la pertinence
|
||||
if reason:
|
||||
markdown += f"**Raison de la pertinence**: {reason}\n\n"
|
||||
|
||||
# Ajouter l'analyse détaillée dans une section dépliable
|
||||
analyse_detail = img_data.get("analyse", "Aucune analyse disponible")
|
||||
if analyse_detail:
|
||||
markdown += "<details>\n<summary>Analyse détaillée de l'image</summary>\n\n"
|
||||
markdown += "```\n" + analyse_detail + "\n```\n\n"
|
||||
markdown += "</details>\n\n"
|
||||
else:
|
||||
markdown += "*Aucune image pertinente n'a été analysée.*\n\n"
|
||||
|
||||
# 4. Diagnostic technique
|
||||
if "diagnostic" in rapport_data and rapport_data["diagnostic"]:
|
||||
markdown += "## Diagnostic technique\n\n"
|
||||
markdown += rapport_data["diagnostic"] + "\n\n"
|
||||
|
||||
# Tableau récapitulatif des échanges (nouveau)
|
||||
if "tableau_questions_reponses" in rapport_data and rapport_data["tableau_questions_reponses"]:
|
||||
markdown += rapport_data["tableau_questions_reponses"] + "\n\n"
|
||||
|
||||
# Section séparatrice
|
||||
markdown += "---\n\n"
|
||||
|
||||
# Détails des analyses effectuées
|
||||
markdown += "# Détails des analyses effectuées\n\n"
|
||||
markdown += "## Processus d'analyse\n\n"
|
||||
|
||||
# 1. Analyse de ticket
|
||||
ticket_analyse = rapport_data.get("ticket_analyse", "")
|
||||
if ticket_analyse:
|
||||
markdown += "### Étape 1: Analyse du ticket\n\n"
|
||||
markdown += "L'agent d'analyse de ticket a extrait les informations suivantes du ticket d'origine:\n\n"
|
||||
markdown += "<details>\n<summary>Cliquez pour voir l'analyse complète du ticket</summary>\n\n"
|
||||
markdown += "```\n" + str(ticket_analyse) + "\n```\n\n"
|
||||
markdown += "</details>\n\n"
|
||||
else:
|
||||
markdown += "### Étape 1: Analyse du ticket\n\n"
|
||||
markdown += "*Aucune analyse de ticket disponible*\n\n"
|
||||
|
||||
# 2. Tri des images
|
||||
markdown += "### Étape 2: Tri des images\n\n"
|
||||
markdown += "L'agent de tri d'images a évalué chaque image pour déterminer sa pertinence par rapport au problème client:\n\n"
|
||||
|
||||
# Création d'un tableau récapitulatif
|
||||
images_list = rapport_data.get("images_analyses", [])
|
||||
if images_list:
|
||||
markdown += "| Image | Pertinence | Raison |\n"
|
||||
markdown += "|-------|------------|--------|\n"
|
||||
|
||||
for img_data in images_list:
|
||||
image_name = img_data.get("image_name", "Image inconnue")
|
||||
sorting_info = img_data.get("sorting_info", {})
|
||||
is_relevant = "Oui" if sorting_info else "Oui" # Par défaut, si présent dans la liste c'est pertinent
|
||||
reason = sorting_info.get("reason", "Non spécifiée")
|
||||
|
||||
markdown += f"| {image_name} | {is_relevant} | {reason} |\n"
|
||||
|
||||
markdown += "\n"
|
||||
else:
|
||||
markdown += "*Aucune image n'a été triée pour ce ticket.*\n\n"
|
||||
|
||||
# 3. Analyse des images
|
||||
markdown += "### Étape 3: Analyse détaillée des images pertinentes\n\n"
|
||||
|
||||
if images_list:
|
||||
for i, img_data in enumerate(images_list, 1):
|
||||
image_name = img_data.get("image_name", f"Image {i}")
|
||||
analyse_detail = img_data.get("analyse", "Analyse non disponible")
|
||||
|
||||
markdown += f"#### Image pertinente {i}: {image_name}\n\n"
|
||||
markdown += "<details>\n<summary>Cliquez pour voir l'analyse complète de l'image</summary>\n\n"
|
||||
markdown += "```\n" + str(analyse_detail) + "\n```\n\n"
|
||||
markdown += "</details>\n\n"
|
||||
else:
|
||||
markdown += "*Aucune image pertinente n'a été identifiée pour ce ticket.*\n\n"
|
||||
|
||||
# 4. Génération du rapport
|
||||
markdown += "### Étape 4: Génération du rapport de synthèse\n\n"
|
||||
markdown += "L'agent de génération de rapport a synthétisé toutes les analyses précédentes pour produire le rapport ci-dessus.\n\n"
|
||||
|
||||
# Informations techniques et métadonnées
|
||||
markdown += "## Informations techniques\n\n"
|
||||
|
||||
# Statistiques
|
||||
statistiques = rapport_data.get("statistiques", {})
|
||||
metadata = rapport_data.get("metadata", {})
|
||||
|
||||
markdown += "### Statistiques\n\n"
|
||||
markdown += f"- **Images analysées**: {statistiques.get('total_images', 0)}\n"
|
||||
markdown += f"- **Images pertinentes**: {statistiques.get('images_pertinentes', 0)}\n"
|
||||
|
||||
if "generation_time" in statistiques:
|
||||
markdown += f"- **Temps de génération**: {statistiques['generation_time']:.2f} secondes\n"
|
||||
|
||||
# Modèle utilisé
|
||||
markdown += "\n### Modèle LLM utilisé\n\n"
|
||||
markdown += f"- **Modèle**: {metadata.get('model', 'Non spécifié')}\n"
|
||||
|
||||
if "model_version" in metadata:
|
||||
markdown += f"- **Version**: {metadata.get('model_version', 'Non spécifiée')}\n"
|
||||
|
||||
markdown += f"- **Température**: {metadata.get('temperature', 'Non spécifiée')}\n"
|
||||
markdown += f"- **Top_p**: {metadata.get('top_p', 'Non spécifié')}\n"
|
||||
|
||||
# Section sur les agents utilisés
|
||||
if "agents" in metadata:
|
||||
markdown += "\n### Agents impliqués\n\n"
|
||||
|
||||
agents = metadata["agents"]
|
||||
|
||||
# Agent d'analyse de ticket
|
||||
if "json_analyser" in agents:
|
||||
markdown += "#### Agent d'analyse du ticket\n"
|
||||
json_analyser = agents["json_analyser"]
|
||||
if "model_info" in json_analyser:
|
||||
markdown += f"- **Modèle**: {json_analyser['model_info'].get('name', 'Non spécifié')}\n"
|
||||
|
||||
# Agent de tri d'images
|
||||
if "image_sorter" in agents:
|
||||
markdown += "\n#### Agent de tri d'images\n"
|
||||
sorter = agents["image_sorter"]
|
||||
# Récupérer directement le modèle ou via model_info selon la structure
|
||||
if "model" in sorter:
|
||||
markdown += f"- **Modèle**: {sorter.get('model', 'Non spécifié')}\n"
|
||||
markdown += f"- **Température**: {sorter.get('temperature', 'Non spécifiée')}\n"
|
||||
markdown += f"- **Top_p**: {sorter.get('top_p', 'Non spécifié')}\n"
|
||||
elif "model_info" in sorter:
|
||||
markdown += f"- **Modèle**: {sorter['model_info'].get('name', 'Non spécifié')}\n"
|
||||
else:
|
||||
markdown += f"- **Modèle**: Non spécifié\n"
|
||||
|
||||
# Agent d'analyse d'images
|
||||
if "image_analyser" in agents:
|
||||
markdown += "\n#### Agent d'analyse d'images\n"
|
||||
analyser = agents["image_analyser"]
|
||||
# Récupérer directement le modèle ou via model_info selon la structure
|
||||
if "model" in analyser:
|
||||
markdown += f"- **Modèle**: {analyser.get('model', 'Non spécifié')}\n"
|
||||
markdown += f"- **Température**: {analyser.get('temperature', 'Non spécifiée')}\n"
|
||||
markdown += f"- **Top_p**: {analyser.get('top_p', 'Non spécifié')}\n"
|
||||
elif "model_info" in analyser:
|
||||
markdown += f"- **Modèle**: {analyser['model_info'].get('name', 'Non spécifié')}\n"
|
||||
else:
|
||||
markdown += f"- **Modèle**: Non spécifié\n"
|
||||
|
||||
return markdown
|
||||
|
||||
def generate_html_report(json_path: str, output_path: Optional[str] = None) -> Tuple[bool, str]:
|
||||
"""
|
||||
Génère un rapport au format HTML à partir d'un fichier JSON.
|
||||
|
||||
Args:
|
||||
json_path: Chemin vers le fichier JSON contenant les données du rapport
|
||||
output_path: Chemin de sortie pour le fichier HTML (facultatif)
|
||||
|
||||
Returns:
|
||||
Tuple (succès, chemin du fichier généré ou message d'erreur)
|
||||
"""
|
||||
try:
|
||||
# Générer d'abord le Markdown
|
||||
success, md_path_or_error = generate_markdown_report(json_path, None)
|
||||
|
||||
if not success:
|
||||
return False, md_path_or_error
|
||||
|
||||
# Lire le contenu Markdown
|
||||
with open(md_path_or_error, "r", encoding="utf-8") as f:
|
||||
markdown_content = f.read()
|
||||
|
||||
# Si le chemin de sortie n'est pas spécifié, le créer à partir du chemin d'entrée
|
||||
if not output_path:
|
||||
# Remplacer l'extension JSON par HTML
|
||||
output_path = os.path.splitext(json_path)[0] + ".html"
|
||||
|
||||
# Conversion Markdown → HTML (avec gestion de l'absence de mistune)
|
||||
html_content = _simple_markdown_to_html(markdown_content)
|
||||
|
||||
# Essayer d'utiliser mistune pour une meilleure conversion si disponible
|
||||
try:
|
||||
import mistune
|
||||
markdown = mistune.create_markdown(escape=False)
|
||||
html_content = markdown(markdown_content)
|
||||
print("Conversion HTML effectuée avec mistune")
|
||||
except ImportError:
|
||||
print("Module mistune non disponible, utilisation de la conversion HTML simplifiée")
|
||||
|
||||
# Créer un HTML complet avec un peu de style
|
||||
html_page = f"""<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Rapport d'analyse de ticket</title>
|
||||
<style>
|
||||
body {{ font-family: Arial, sans-serif; line-height: 1.6; margin: 0; padding: 20px; color: #333; max-width: 1200px; margin: 0 auto; }}
|
||||
h1 {{ color: #2c3e50; border-bottom: 2px solid #eee; padding-bottom: 10px; }}
|
||||
h2 {{ color: #3498db; margin-top: 30px; }}
|
||||
h3 {{ color: #2980b9; }}
|
||||
h4 {{ color: #16a085; }}
|
||||
table {{ border-collapse: collapse; width: 100%; margin: 20px 0; }}
|
||||
th, td {{ padding: 12px 15px; text-align: left; border-bottom: 1px solid #ddd; }}
|
||||
th {{ background-color: #f2f2f2; }}
|
||||
tr:hover {{ background-color: #f5f5f5; }}
|
||||
code, pre {{ background: #f8f8f8; border: 1px solid #ddd; border-radius: 3px; padding: 10px; overflow-x: auto; }}
|
||||
details {{ margin: 15px 0; }}
|
||||
summary {{ cursor: pointer; font-weight: bold; color: #2980b9; }}
|
||||
.status {{ color: #e74c3c; font-weight: bold; }}
|
||||
hr {{ border: 0; height: 1px; background: #eee; margin: 30px 0; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{html_content}
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
# Écrire le contenu dans le fichier de sortie
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
f.write(html_page)
|
||||
|
||||
print(f"Rapport HTML généré avec succès: {output_path}")
|
||||
return True, output_path
|
||||
|
||||
except Exception as e:
|
||||
error_message = f"Erreur lors de la génération du rapport HTML: {str(e)}"
|
||||
print(error_message)
|
||||
return False, error_message
|
||||
|
||||
def _simple_markdown_to_html(markdown_content: str) -> str:
|
||||
"""
|
||||
Convertit un contenu Markdown en HTML de façon simplifiée.
|
||||
|
||||
Args:
|
||||
markdown_content: Contenu Markdown à convertir
|
||||
|
||||
Returns:
|
||||
Contenu HTML
|
||||
"""
|
||||
html = markdown_content
|
||||
|
||||
# Titres
|
||||
html = re.sub(r'^# (.*?)$', r'<h1>\1</h1>', html, flags=re.MULTILINE)
|
||||
html = re.sub(r'^## (.*?)$', r'<h2>\1</h2>', html, flags=re.MULTILINE)
|
||||
html = re.sub(r'^### (.*?)$', r'<h3>\1</h3>', html, flags=re.MULTILINE)
|
||||
html = re.sub(r'^#### (.*?)$', r'<h4>\1</h4>', html, flags=re.MULTILINE)
|
||||
|
||||
# Emphase
|
||||
html = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', html)
|
||||
html = re.sub(r'\*(.*?)\*', r'<em>\1</em>', html)
|
||||
|
||||
# Lists
|
||||
html = re.sub(r'^- (.*?)$', r'<li>\1</li>', html, flags=re.MULTILINE)
|
||||
|
||||
# Paragraphes
|
||||
html = re.sub(r'([^\n])\n([^\n])', r'\1<br>\2', html)
|
||||
html = re.sub(r'\n\n', r'</p><p>', html)
|
||||
|
||||
# Tables simplifiées (sans analyser la structure)
|
||||
html = re.sub(r'\| (.*?) \|', r'<td>\1</td>', html)
|
||||
|
||||
# Code blocks
|
||||
html = re.sub(r'```(.*?)```', r'<pre><code>\1</code></pre>', html, flags=re.DOTALL)
|
||||
|
||||
# Envelopper dans des balises paragraphe
|
||||
html = f"<p>{html}</p>"
|
||||
|
||||
return html
|
||||
|
||||
def process_report(json_path: str, output_format: str = "markdown") -> None:
|
||||
"""
|
||||
Traite un rapport dans le format spécifié.
|
||||
|
||||
Args:
|
||||
json_path: Chemin vers le fichier JSON contenant les données du rapport
|
||||
output_format: Format de sortie (markdown ou html)
|
||||
"""
|
||||
if output_format.lower() == "markdown":
|
||||
generate_markdown_report(json_path)
|
||||
elif output_format.lower() == "html":
|
||||
generate_html_report(json_path)
|
||||
else:
|
||||
print(f"Format non supporté: {output_format}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Formateur de rapports à partir de fichiers JSON")
|
||||
parser.add_argument("json_path", help="Chemin vers le fichier JSON contenant les données du rapport")
|
||||
parser.add_argument("--format", "-f", choices=["markdown", "html"], default="markdown",
|
||||
help="Format de sortie (markdown par défaut)")
|
||||
parser.add_argument("--output", "-o", help="Chemin de sortie pour le rapport (facultatif)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.format == "markdown":
|
||||
generate_markdown_report(args.json_path, args.output)
|
||||
elif args.format == "html":
|
||||
generate_html_report(args.json_path, args.output)
|
||||
else:
|
||||
print(f"Format non supporté: {args.format}")
|
||||
Loading…
x
Reference in New Issue
Block a user