This commit is contained in:
Ladebeze66 2025-04-08 14:40:14 +02:00
parent da64fb1131
commit 2defd5b1dd
19 changed files with 6320 additions and 2757 deletions

View File

@ -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:

View File

@ -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"

View File

@ -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")

View File

@ -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 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

View File

@ -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")

View File

@ -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 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")

View File

@ -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")

View File

@ -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")

View File

@ -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

View File

@ -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é

View File

@ -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
View 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.')

View File

@ -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

View File

@ -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

View File

@ -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
View 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
View 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}")