This commit is contained in:
Ladebeze66 2025-04-07 21:03:17 +02:00
parent 4452d6664b
commit 045078c2fe
9 changed files with 1205 additions and 586 deletions

1
.gitignore vendored
View File

@ -2,6 +2,7 @@
venv/
env/
ENV/
venv_new/
# Fichiers compilés Python
__pycache__/

View File

@ -5,12 +5,33 @@ 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
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)
@ -22,27 +43,43 @@ class AgentReportGenerator(BaseAgent):
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"
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: TABLEAU des interactions client/support (format imposé ci-dessus)
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.
Le tableau des échanges client/support est l'élément le plus important du rapport."""
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()
@ -86,6 +123,9 @@ Le tableau des échanges client/support est l'élément le plus important du rap
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:
@ -185,235 +225,130 @@ Le tableau des échanges client/support est l'élément le plus important du rap
analyse_detail = None
if is_relevant:
if "analysis" in analyse_data and analyse_data["analysis"]:
if isinstance(analyse_data["analysis"], dict) and "analyse" in analyse_data["analysis"]:
analyse_detail = analyse_data["analysis"]["analyse"]
elif isinstance(analyse_data["analysis"], dict):
analyse_detail = str(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
# Analyse détaillée
if analyse_detail:
images_analyses.append({
"image_name": image_name,
"analyse": analyse_detail
})
num_images = len(images_analyses)
# Afficher un résumé des données collectées
logger.info(f"Résumé des données préparées pour le rapport:")
logger.info(f" - Ticket ID: {ticket_id}")
logger.info(f" - Analyse du ticket: {len(ticket_analyse) if ticket_analyse else 0} caractères")
logger.info(f" - Images analysées: {total_images}, Images pertinentes: {images_pertinentes}")
logger.info(f" - Images avec analyse détaillée: {num_images}")
# Mettre à jour les métadonnées avec les statistiques
rapport_data.setdefault("metadata", {}).update({
"images_analysees": total_images,
"images_pertinentes": images_pertinentes,
"analyses_images_disponibles": num_images
})
# Extraire les messages pour aider à la création du tableau
messages_structure = []
try:
ticket_data = rapport_data.get("ticket_data", {})
if "messages" in ticket_data and isinstance(ticket_data["messages"], list):
for msg in ticket_data["messages"]:
if isinstance(msg, dict):
sender = msg.get("author_id", msg.get("from", "Inconnu"))
date = msg.get("date", "Date inconnue")
content = msg.get("content", "")
# Déterminer le type (client/support)
sender_type = "CLIENT" if "client" in sender.lower() else "SUPPORT" if "support" in sender.lower() else "AUTRE"
messages_structure.append({
"date": date,
"emetteur": sender_type,
"contenu": content[:100] + "..." if len(content) > 100 else content
})
logger.info(f" - {len(messages_structure)} messages extraits pour le tableau")
except Exception as e:
logger.warning(f"Erreur lors de l'extraction des messages: {e}")
# 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 pertinentes sur {total_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"
# Ajouter des informations sur les messages pour aider à la création du tableau
if messages_structure:
prompt += "\n## STRUCTURE DES MESSAGES POUR LE TABLEAU\n"
for i, msg in enumerate(messages_structure, 1):
prompt += f"{i}. Date: {msg['date']} | Émetteur: {msg['emetteur']} | Contenu: {msg['contenu']}\n"
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."""
# Appeler le LLM pour générer le rapport
logger.info("Interrogation du LLM pour la génération du rapport")
rapport_contenu = self.llm.interroger(prompt)
# Vérifier que le rapport généré contient bien un tableau pour les échanges
contains_table = "|" in rapport_contenu and (
"| Date |" in rapport_contenu or
"| Émetteur |" in rapport_contenu or
"| Type |" in rapport_contenu or
"| CLIENT |" in rapport_contenu
)
if not contains_table:
logger.warning("ATTENTION: Le rapport généré ne semble pas contenir de tableau pour les échanges client/support")
print(" ATTENTION: Le rapport ne contient pas de tableau pour les échanges")
# Tenter une seconde génération avec un prompt plus direct
logger.info("Tentative de régénération du rapport avec focus sur le tableau")
# Prompt simplifié, focalisé sur le tableau
second_prompt = f"""Pour le ticket #{ticket_id}, crée un rapport incluant IMPÉRATIVEMENT:
UN TABLEAU MARKDOWN DES ÉCHANGES CLIENT/SUPPORT:
| Date | Émetteur | Type | Contenu |
|------|----------|------|---------|
| date1 | CLIENT | Question | contenu... |
Voici la structure des messages:
"""
# Ajouter les messages directement
for i, msg in enumerate(messages_structure, 1):
second_prompt += f"{i}. Date: {msg['date']} | Émetteur: {msg['emetteur']} | Contenu: {msg['contenu']}\n"
second_prompt += """
Structure obligatoire:
1. Résumé exécutif (très court)
2. Chronologie des échanges: TABLEAU MARKDOWN (comme ci-dessus)
3. Bref diagnostic
Le tableau est l'élément le plus important."""
# Tenter avec un autre prompt
second_rapport = self.llm.interroger(second_prompt)
# Vérifier à nouveau
if "|" in second_rapport and (
"| Date |" in second_rapport or
"| Émetteur |" in second_rapport or
"| Type |" in second_rapport or
"| CLIENT |" in second_rapport
):
rapport_contenu = second_rapport
logger.info("Succès: Le rapport régénéré contient un tableau")
print(" Tableau des échanges généré avec succès dans la seconde tentative")
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.warning("Le tableau est toujours absent dans la seconde tentative")
logger.info(f"Image {image_name} ignorée car non pertinente")
# Créer les noms de fichiers pour la sauvegarde
# 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()
base_filename = f"{ticket_id}_{timestamp}"
json_path = os.path.join(rapport_dir, f"{base_filename}.json")
md_path = os.path.join(rapport_dir, f"{base_filename}.md")
# Collecter les métadonnées du rapport avec détails sur les agents et LLM utilisés
metadata = rapport_data.get("metadata", {})
metadata.update({
# 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,
"system_prompt": self.system_prompt,
"agents_info": agents_info,
"images_analysees": total_images,
"images_pertinentes": images_pertinentes,
"analyses_images_incluses": num_images
})
"agents": agents_info
}
# Sauvegarder le rapport au format JSON (données brutes + rapport généré)
rapport_data_complet = rapport_data.copy()
rapport_data_complet["rapport_genere"] = rapport_contenu
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}")
# Générer et sauvegarder le rapport au format Markdown basé directement sur le JSON
# É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 sauvegardé: {json_path} et {md_path}")
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
# Enregistrer l'historique
self.ajouter_historique("generation_rapport",
{
"rapport_dir": rapport_dir,
"ticket_id": ticket_id,
"total_images": total_images,
"images_pertinentes": images_pertinentes,
"temperature": self.temperature,
"top_p": self.top_p,
"max_tokens": self.max_tokens
},
{
"json_path": json_path,
"md_path": md_path,
"taille_rapport": len(markdown_content) if 'markdown_content' in locals() else 0,
"rapport_contenu": rapport_contenu[:300] + ("..." if len(rapport_contenu) > 300 else "")
})
message = f"Rapports générés dans: {rapport_dir}"
print(f" {message}")
print(f" - JSON: {os.path.basename(json_path)}")
print(f" - Markdown: {os.path.basename(md_path)}")
return json_path, md_path
def _collecter_info_agents(self, rapport_data: Dict) -> Dict:
"""
@ -476,6 +411,14 @@ Le tableau est l'élément le plus important."""
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
@ -483,6 +426,8 @@ Le tableau est l'élément le plus important."""
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", "")
@ -511,12 +456,23 @@ Le tableau est l'élément le plus important."""
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"
analyse_images_data = rapport_data.get("analyse_images", {})
# 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
@ -539,38 +495,84 @@ Le tableau est l'élément le plus important."""
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
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:
# 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 = os.path.basename(image_path)
# Récupérer l'analyse détaillée
analyse_detail = "Analyse non disponible"
if "analysis" in analyse_data and analyse_data["analysis"]:
if isinstance(analyse_data["analysis"], dict) and "analyse" in analyse_data["analysis"]:
analyse_detail = analyse_data["analysis"]["analyse"]
elif isinstance(analyse_data["analysis"], dict):
analyse_detail = str(analyse_data["analysis"])
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"
@ -579,57 +581,137 @@ Le tableau est l'élément le plus important."""
# Informations techniques
markdown += "## Informations techniques\n\n"
# Ajouter les informations sur les agents utilisés
agents_info = rapport_data.get("metadata", {}).get("agents_info", {})
if agents_info:
markdown += "### Agents et modèles utilisés\n\n"
# Agent JSON Analyser
if "json_analyser" in agents_info:
info = agents_info["json_analyser"]
markdown += "#### Agent d'analyse de texte\n"
markdown += f"- **Modèle**: {info.get('model', 'Non spécifié')}\n"
markdown += f"- **Température**: {info.get('temperature', 'Non spécifiée')}\n"
markdown += f"- **Top-p**: {info.get('top_p', 'Non spécifié')}\n"
markdown += f"- **Max tokens**: {info.get('max_tokens', 'Non spécifié')}\n\n"
# Agent Image Sorter
if "image_sorter" in agents_info:
info = agents_info["image_sorter"]
markdown += "#### Agent de tri d'images\n"
markdown += f"- **Modèle**: {info.get('model', 'Non spécifié')}\n"
markdown += f"- **Température**: {info.get('temperature', 'Non spécifiée')}\n"
markdown += f"- **Top-p**: {info.get('top_p', 'Non spécifié')}\n"
markdown += f"- **Max tokens**: {info.get('max_tokens', 'Non spécifié')}\n\n"
# Agent Image Analyser
if "image_analyser" in agents_info:
info = agents_info["image_analyser"]
markdown += "#### Agent d'analyse d'images\n"
markdown += f"- **Modèle**: {info.get('model', 'Non spécifié')}\n"
markdown += f"- **Température**: {info.get('temperature', 'Non spécifiée')}\n"
markdown += f"- **Top-p**: {info.get('top_p', 'Non spécifié')}\n"
markdown += f"- **Max tokens**: {info.get('max_tokens', 'Non spécifié')}\n\n"
# Agent Report Generator
if "report_generator" in agents_info:
info = agents_info["report_generator"]
markdown += "#### Agent de génération de rapport\n"
markdown += f"- **Modèle**: {info.get('model', 'Non spécifié')}\n"
markdown += f"- **Température**: {info.get('temperature', 'Non spécifiée')}\n"
markdown += f"- **Top-p**: {info.get('top_p', 'Non spécifié')}\n"
markdown += f"- **Max tokens**: {info.get('max_tokens', 'Non spécifié')}\n\n"
# Statistiques d'analyse
markdown += "### Statistiques\n\n"
total_images = len(analyse_images_data) if analyse_images_data else 0
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,48 +1,623 @@
import json
import os
from .base_agent import BaseAgent
from datetime import datetime
from typing import Dict, Any, Tuple
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 à partir des informations collectées.
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, "report_generator")
super().__init__("AgentReportGenerator", llm)
def executer(self, rapport_data: Dict, filename: str) -> Tuple[str, str]:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
# 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."""
# Ajouter les métadonnées des LLM utilisés
if "metadata" not in rapport_data:
rapport_data["metadata"] = {}
# Appliquer la configuration au LLM
self._appliquer_config_locale()
rapport_data["metadata"]["report_generator"] = {
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))),
"configuration": self.config.to_dict()
"temperature": self.temperature,
"top_p": self.top_p,
"max_tokens": self.max_tokens
}
# Sauvegarde JSON
json_path = f"../reports/json_reports/{filename}_{timestamp}.json"
with open(json_path, "w", encoding="utf-8") as f_json:
json.dump(rapport_data, f_json, ensure_ascii=False, indent=4)
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)
# Sauvegarde Markdown
md_path = f"../reports/markdown_reports/{filename}_{timestamp}.md"
with open(md_path, "w", encoding="utf-8") as f_md:
f_md.write(f"# Rapport {filename}\n\n")
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"
# Ajouter les métadonnées des modèles utilisés
if "metadata" in rapport_data:
f_md.write("## Modèles et paramètres utilisés\n\n")
f_md.write("```json\n")
f_md.write(json.dumps(rapport_data["metadata"], indent=2))
f_md.write("\n```\n\n")
# Ajouter le contenu du rapport
for key, value in rapport_data.items():
if key != "metadata": # Ignorer la section metadata déjà traitée
f_md.write(f"## {key.capitalize()}\n{value}\n\n")
for image_path, analyse_data in analyse_images_data.items():
image_name = os.path.basename(image_path)
self.ajouter_historique("generation_rapport", filename, "Rapport généré")
return json_path, md_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

@ -13,6 +13,21 @@ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(
logger = logging.getLogger("Orchestrator")
class Orchestrator:
"""
Orchestrateur pour l'analyse de tickets et la génération de rapports.
Stratégie de gestion des formats:
- JSON est le format principal pour le traitement des données et l'analyse
- Markdown est utilisé uniquement comme format de présentation finale
- Les agents LLM travaillent principalement avec le format JSON
- La conversion JSON->Markdown se fait uniquement à la fin du processus pour la présentation
Cette approche permet de:
1. Simplifier le code des agents
2. Réduire les redondances et incohérences entre formats
3. Améliorer la performance des agents LLM avec un format plus structuré
4. Faciliter la maintenance et l'évolution du système
"""
def __init__(self,
output_dir: str = "output/",
ticket_agent: Optional[BaseAgent] = None,
@ -342,23 +357,33 @@ class Orchestrator:
logger.warning(f"Aucun rapport trouvé pour le ticket {ticket_id}")
return None
# Essayer d'abord le fichier JSON
# Privilégier le format JSON (format principal)
if rapports.get("json") and rapports["json"] is not None:
try:
ticket_data = self.ticket_loader.charger(rapports["json"])
logger.info(f"Données JSON chargées depuis: {rapports['json']}")
print(f" Rapport JSON chargé: {os.path.basename(rapports['json'])}")
# Ajouter une métadonnée sur le format source
if ticket_data and "metadata" not in ticket_data:
ticket_data["metadata"] = {}
if ticket_data:
ticket_data["metadata"]["format_source"] = "json"
except Exception as e:
logger.error(f"Erreur lors du chargement du JSON: {e}")
print(f" ERREUR: Impossible de charger le fichier JSON: {e}")
# Si pas de JSON ou erreur, essayer le Markdown
# Fallback sur le Markdown uniquement si JSON non disponible
if not ticket_data and rapports.get("markdown") and rapports["markdown"] is not None:
try:
# Utiliser le loader pour charger les données depuis le Markdown
ticket_data = self.ticket_loader.charger(rapports["markdown"])
logger.info(f"Données Markdown chargées depuis: {rapports['markdown']}")
print(f" Rapport Markdown chargé: {os.path.basename(rapports['markdown'])}")
logger.info(f"Données Markdown chargées depuis: {rapports['markdown']} (fallback)")
print(f" Rapport Markdown chargé (fallback): {os.path.basename(rapports['markdown'])}")
# Ajouter une métadonnée sur le format source
if ticket_data and "metadata" not in ticket_data:
ticket_data["metadata"] = {}
if ticket_data:
ticket_data["metadata"]["format_source"] = "markdown"
except Exception as e:
logger.error(f"Erreur lors du chargement du Markdown: {e}")
print(f" ERREUR: Impossible de charger le fichier Markdown: {e}")

View File

@ -1,119 +0,0 @@
{
"ticket_data": {
"id": "113",
"code": "T0101",
"name": "ACTIVATION LOGICIEL",
"description": "Problème de licence.",
"project_name": "Demandes",
"stage_name": "Clôturé",
"user_id": "",
"partner_id_email_from": "PROVENCALE S.A, Bruno Vernet <bruno.vernet@provencale.com>",
"create_date": "26/03/2020 14:46:36",
"write_date_last_modification": "03/10/2024 13:10:50",
"date_deadline": "25/05/2020 00:00:00",
"messages": [
{
"author_id": "PROVENCALE S.A",
"date": "26/03/2020 14:43:45",
"message_type": "E-mail",
"subject": "ACTIVATION LOGICIEL",
"id": "10758",
"content": "Bonjour,\n\nAu vu de la situation liée au Coronavirus, nous avons dû passer en télétravail.\n\nPour ce faire et avoir accès aux différents logiciels nécessaires, ESQ a été réinstallé sur un autre serveur afin de pouvoir travailler en bureau à distance.\n\nDu coup le logiciel nous demande une activation mais je ne sais pas si le N° de licence a été modifié suite à un achat version réseau faite par JB Lafitte en 2019 ou si le problème est autre.\n\nCi-dessous la fenêtre au lancement du logiciel.\n\nMerci davance pour votre aide.\n\nCordialement\n\n\n- image006.jpg (image/jpeg) [ID: 31760]\n- image005.jpg (image/jpeg) [ID: 31758]\n\n---\n"
}
],
"date_d'extraction": "04/04/2025 17:02:42",
"répertoire": "output/ticket_T0101/T0101_20250404_170239",
"metadata": {}
},
"ticket_id": "T0101",
"ticket_analyse": "1. Analyse du problème initial\n Le nom de la demande indique \"ACTIVATION LOGICIEL\", suggérant que le client est confronté à un problème d'activation d'un logiciel. La description du problème mentionne spécifiquement un \"Problème de licence\".\n\n2. Informations techniques essentielles\n - Logiciel : ESQ\n - Version : Non spécifiée dans les informations fournies\n - Configuration : Le logiciel a été réinstallé sur un autre serveur pour permettre le travail à distance.\n - Licence : Il y a une incertitude quant à la modification du numéro de licence suite à un achat de version réseau en 2019.\n\n3. Chronologie des échanges client/support\n - Message 1 - [AUTRE] De : Inconnu - Date : 26/03/2020 14:43:45\n - Question du client : Le client demande de l'aide pour l'activation du logiciel ESQ, car il y a un problème de licence. Ils ne savent pas si le numéro de licence a été modifié suite à un achat de version réseau en 2019 ou si le problème est autre.\n - Informations techniques fournies par le client : Ils ont réinstallé le logiciel ESQ sur un autre serveur pour le travail à distance.\n - Pièces jointes : Deux images (image006.jpg et image005.jpg) montrant probablement la fenêtre d'activation du logiciel avec le message d'erreur.\n\nAucune réponse du support n'est fournie dans les informations du ticket.",
"analyse_images": {
"output/ticket_T0101/T0101_20250404_170239/attachments/image006.jpg": {
"sorting": {
"is_relevant": false,
"reason": "Non. Cette image montre le logo d'une entreprise nommée \"Provençale Carbone de Calcium\". Elle n'est pas pertinente pour un ticket de support technique, car elle ne contient pas de captures d'écran de logiciels, de messages d'erreur, de configurations système, ou d'autres éléments techniques liés à l'informatique.",
"raw_response": "Non. Cette image montre le logo d'une entreprise nommée \"Provençale Carbone de Calcium\". Elle n'est pas pertinente pour un ticket de support technique, car elle ne contient pas de captures d'écran de logiciels, de messages d'erreur, de configurations système, ou d'autres éléments techniques liés à l'informatique.",
"metadata": {
"image_path": "output/ticket_T0101/T0101_20250404_170239/attachments/image006.jpg",
"image_name": "image006.jpg",
"timestamp": "20250407_172758",
"model_info": {
"model": "pixtral-12b-latest",
"temperature": 0.2,
"top_p": 0.8,
"max_tokens": 300
}
}
},
"analysis": null
},
"output/ticket_T0101/T0101_20250404_170239/attachments/image005.jpg": {
"sorting": {
"is_relevant": true,
"reason": "oui. L'image montre une fenêtre d'activation de logiciel, ce qui est pertinent pour le support technique car elle peut aider à diagnostiquer et résoudre des problèmes liés à l'activation du logiciel.",
"raw_response": "oui. L'image montre une fenêtre d'activation de logiciel, ce qui est pertinent pour le support technique car elle peut aider à diagnostiquer et résoudre des problèmes liés à l'activation du logiciel.",
"metadata": {
"image_path": "output/ticket_T0101/T0101_20250404_170239/attachments/image005.jpg",
"image_name": "image005.jpg",
"timestamp": "20250407_172759",
"model_info": {
"model": "pixtral-12b-latest",
"temperature": 0.2,
"top_p": 0.8,
"max_tokens": 300
}
}
},
"analysis": {
"analyse": "### Analyse d'image\n\n#### 1. Description objective\nL'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 activer le logiciel.\n\n#### 2. Éléments techniques clés\n- **Titre de la fenêtre** : \"Activation du logiciel\"\n- **Champ d'ID du logiciel** : \"ID du logiciel\" avec un champ de texte vide\n- **Message d'instructions** :\n - \"Afin d'activer votre logiciel, veuillez saisir l'ID du logiciel fourni par CBAD.\"\n - \"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 65 31 31 ou en cliquant sur le bouton téléphone ci-dessous.\"\n- **Options d'activation** :\n - \"Activer le logiciel (par internet)\"\n - \"Activer plus tard (4 jours restants)\"\n - \"Activation par téléphone\"\n\n#### 3. Relation avec le problème\nL'image se rapporte au problème décrit dans le ticket de support concernant l'activation du logiciel ESQ. Le message d'instructions et les options d'activation indiquent que le client doit entrer un ID de logiciel pour activer le logiciel, ce qui est en lien avec le problème de licence mentionné dans le ticket. Le champ d'ID du logiciel est vide, suggérant que le client n'a pas encore entré l'ID nécessaire pour l'activation.",
"metadata": {
"image_path": "output/ticket_T0101/T0101_20250404_170239/attachments/image005.jpg",
"image_name": "image005.jpg",
"timestamp": "20250407_172803",
"model_info": {
"model": "pixtral-12b-latest",
"temperature": 0.3,
"top_p": 0.9,
"max_tokens": 1200
}
}
}
}
},
"metadata": {
"timestamp_debut": "20250407_172803",
"ticket_id": "T0101",
"images_analysees": 2,
"images_pertinentes": 1,
"analyses_images_disponibles": 1,
"timestamp": "20250407_172813",
"model": "mistral-medium",
"temperature": 0.4,
"top_p": 0.9,
"max_tokens": 2500,
"system_prompt": "Tu es un expert en génération de rapports techniques pour BRG-Lab.\nTa mission est de synthétiser les analyses (ticket et images) en un rapport structuré et exploitable.\n\nIMPORTANCE DES ÉCHANGES CLIENT/SUPPORT:\n- Tu dois IMPÉRATIVEMENT présenter les échanges client/support sous forme d'un TABLEAU MARKDOWN clair\n- Chaque ligne du tableau doit contenir: Date | Émetteur | Type (Question/Réponse) | Contenu\n- Identifie clairement qui est l'émetteur (CLIENT ou SUPPORT)\n- Mets en évidence les questions posées et les réponses fournies\n\nStructure ton rapport:\n1. Résumé exécutif: Synthèse du problème initial (nom de la demande + description)\n2. Chronologie des échanges: TABLEAU des interactions client/support\n3. Analyse des images: Ce que montrent les captures d'écran et leur pertinence\n4. Diagnostic technique: Interprétation des informations techniques pertinentes\n\nReste factuel et précis. Ne fais pas d'analyses inutiles ou de recommandations non fondées.\nTon rapport doit mettre en avant la chronologie des échanges et les informations techniques clés.",
"agents_info": {
"image_sorter": {
"model": "pixtral-12b-latest",
"temperature": 0.2,
"top_p": 0.8,
"max_tokens": 300
},
"image_analyser": {
"model": "pixtral-12b-latest",
"temperature": 0.3,
"top_p": 0.9,
"max_tokens": 1200
},
"report_generator": {
"model": "mistral-medium",
"temperature": 0.4,
"top_p": 0.9,
"max_tokens": 2500
}
},
"analyses_images_incluses": 1
},
"analyse_json": "1. Analyse du problème initial\n Le nom de la demande indique \"ACTIVATION LOGICIEL\", suggérant que le client est confronté à un problème d'activation d'un logiciel. La description du problème mentionne spécifiquement un \"Problème de licence\".\n\n2. Informations techniques essentielles\n - Logiciel : ESQ\n - Version : Non spécifiée dans les informations fournies\n - Configuration : Le logiciel a été réinstallé sur un autre serveur pour permettre le travail à distance.\n - Licence : Il y a une incertitude quant à la modification du numéro de licence suite à un achat de version réseau en 2019.\n\n3. Chronologie des échanges client/support\n - Message 1 - [AUTRE] De : Inconnu - Date : 26/03/2020 14:43:45\n - Question du client : Le client demande de l'aide pour l'activation du logiciel ESQ, car il y a un problème de licence. Ils ne savent pas si le numéro de licence a été modifié suite à un achat de version réseau en 2019 ou si le problème est autre.\n - Informations techniques fournies par le client : Ils ont réinstallé le logiciel ESQ sur un autre serveur pour le travail à distance.\n - Pièces jointes : Deux images (image006.jpg et image005.jpg) montrant probablement la fenêtre d'activation du logiciel avec le message d'erreur.\n\nAucune réponse du support n'est fournie dans les informations du ticket.",
"rapport_genere": "**Rapport technique pour le ticket #T0101**\n\n**1. Résumé exécutif**\n\n*Nom de la demande :* Activation logiciel\n*Description :* Le client rencontre un problème de licence lors de l'activation du logiciel ESQ. Ils ont réinstallé le logiciel sur un autre serveur pour le travail à distance et sont incertains quant à la modification du numéro de licence suite à un achat de version réseau en 2019.\n\n**2. Chronologie des échanges**\n\n| Date | Émetteur (CLIENT/SUPPORT) | Type (Question/Réponse) | Contenu |\n| --- | --- | --- | --- |\n| 26/03/2020 14:43:45 | CLIENT | Question | Problème d'activation du logiciel ESQ en raison d'un problème de licence. Ils ont réinstallé le logiciel sur un autre serveur pour le travail à distance. |\n| - | SUPPORT | Réponse | (Aucune réponse fournie) |\n\n**3. Analyse des images**\n\n*Image 1 : image005.jpg*\n\nCette image montre une fenêtre d'activation de logiciel avec un champ pour entrer l'ID du logiciel, un message d'instructions et trois options pour activer le logiciel. Le champ d'ID du logiciel est vide, indiquant que le client n'a pas encore entré l'ID nécessaire pour l'activation.\n\n**4. Diagnostic technique**\n\nLe client a réinstallé le logiciel ESQ sur un autre serveur pour le travail à distance, ce qui a peut-être entraîné un problème de licence. Ils sont incertains quant à la modification du numéro de licence suite à un achat de version réseau en 2019. Le support devra clarifier la situation de la licence et fournir les instructions nécessaires pour activer le logiciel."
}

View File

@ -1,132 +0,0 @@
# Rapport d'analyse du ticket #T0101
*Généré le: 20250407_172813*
**Rapport technique pour le ticket #T0101**
**1. Résumé exécutif**
*Nom de la demande :* Activation logiciel
*Description :* Le client rencontre un problème de licence lors de l'activation du logiciel ESQ. Ils ont réinstallé le logiciel sur un autre serveur pour le travail à distance et sont incertains quant à la modification du numéro de licence suite à un achat de version réseau en 2019.
**2. Chronologie des échanges**
| Date | Émetteur (CLIENT/SUPPORT) | Type (Question/Réponse) | Contenu |
| --- | --- | --- | --- |
| 26/03/2020 14:43:45 | CLIENT | Question | Problème d'activation du logiciel ESQ en raison d'un problème de licence. Ils ont réinstallé le logiciel sur un autre serveur pour le travail à distance. |
| - | SUPPORT | Réponse | (Aucune réponse fournie) |
**3. Analyse des images**
*Image 1 : image005.jpg*
Cette image montre une fenêtre d'activation de logiciel avec un champ pour entrer l'ID du logiciel, un message d'instructions et trois options pour activer le logiciel. Le champ d'ID du logiciel est vide, indiquant que le client n'a pas encore entré l'ID nécessaire pour l'activation.
**4. Diagnostic technique**
Le client a réinstallé le logiciel ESQ sur un autre serveur pour le travail à distance, ce qui a peut-être entraîné un problème de licence. Ils sont incertains quant à la modification du numéro de licence suite à un achat de version réseau en 2019. Le support devra clarifier la situation de la licence et fournir les instructions nécessaires pour activer le logiciel.
---
# 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
Le nom de la demande indique "ACTIVATION LOGICIEL", suggérant que le client est confronté à un problème d'activation d'un logiciel. La description du problème mentionne spécifiquement un "Problème de licence".
2. Informations techniques essentielles
- Logiciel : ESQ
- Version : Non spécifiée dans les informations fournies
- Configuration : Le logiciel a été réinstallé sur un autre serveur pour permettre le travail à distance.
- Licence : Il y a une incertitude quant à la modification du 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, car il y a un problème de licence. Ils ne savent pas si le numéro de licence a été modifié suite à un achat de version réseau en 2019 ou si le problème est autre.
- Informations techniques fournies par le client : Ils ont réinstallé le logiciel ESQ sur un autre serveur pour le travail à distance.
- Pièces jointes : Deux images (image006.jpg et image005.jpg) montrant probablement la fenêtre d'activation du logiciel avec le message d'erreur.
Aucune réponse du support n'est fournie dans les informations du ticket.
```
</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 |
|-------|------------|--------|
| image006.jpg | Non | Non. Cette image montre le logo d'une entreprise nommée "Provençale Carbone de Calcium". Elle n'est pas pertinente pour un ticket de support technique, car elle ne contient pas de captures d'écran de logiciels, de messages d'erreur, de configurations système, ou d'autres éléments techniques liés à l'informatique. |
| image005.jpg | Oui | oui. L'image montre une fenêtre d'activation de logiciel, ce qui est pertinent pour le support technique car elle peut aider à diagnostiquer et résoudre des problèmes liés à l'activation du logiciel. |
### É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 activer le 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 65 31 31 ou en cliquant sur le bouton téléphone ci-dessous."
- **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 et les options d'activation indiquent que le client doit entrer un ID de logiciel pour activer le logiciel, ce qui est en lien avec le problème de licence mentionné dans le ticket. Le champ d'ID du logiciel est vide, suggérant que le client n'a pas encore entré l'ID nécessaire pour l'activation.
```
</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
### Agents et modèles utilisés
#### Agent de tri d'images
- **Modèle**: pixtral-12b-latest
- **Température**: 0.2
- **Top-p**: 0.8
- **Max tokens**: 300
#### Agent d'analyse d'images
- **Modèle**: pixtral-12b-latest
- **Température**: 0.3
- **Top-p**: 0.9
- **Max tokens**: 1200
#### Agent de génération de rapport
- **Modèle**: mistral-medium
- **Température**: 0.4
- **Top-p**: 0.9
- **Max tokens**: 2500
### Statistiques
- **Images analysées**: 2
- **Images pertinentes**: 1

View File

@ -0,0 +1,44 @@
{
"ticket_id": "T0101",
"timestamp": "20250407_205814",
"rapport_genere": "| Date | Émetteur | Type | Contenu | Statut |\n|------|---------|------|---------|--------|\n| 26/03/2020 14:43:45 | CLIENT | Question | Demande d'aide pour l'activation du logiciel ESQ sur un nouveau serveur, incertitude quant au numéro de licence suite à un achat de version réseau en 2019 par JB Lafitte. | **Sans réponse** |\n| 26/03/2020 14:43:45 | CLIENT | Information technique | Réinstallation du logiciel ESQ sur un nouveau serveur, achat d'une version réseau en 2019 par JB Lafitte. | |\n\n**Note: Aucune réponse du support n'a été trouvée dans ce ticket.**\n\n\n\nRapport technique pour le ticket #T0101\n\n1. Résumé exécutif\n - Nom de la demande: Activation Logiciel\n - Description: Le client a réinstallé le logiciel ESQ sur un nouveau 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.\n\n2. Chronologie des échanges\n - Voir l'objet JSON ci-dessus pour les détails des échanges client/support.\n\n3. Analyse des images\n - Image 1 (image005.jpg): Cette capture d'écran montre une fenêtre d'activation de logiciel avec un champ pour entrer l'ID du logiciel, un message d'instructions, et trois options pour activer le logiciel. Cette image est pertinente pour le problème car elle montre que le client doit entrer un ID de logiciel fourni par CBAO pour activer le logiciel, ce qui suggère que le problème pourrait être lié à l'absence ou à l'incertitude concernant le numéro de licence correct.\n\n4. Diagnostic technique\n - Le client a réinstallé le logiciel ESQ sur un nouveau serveur pour permettre le télétravail, mais il rencontre un problème d'activation. 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. Le support technique devrait vérifier les informations de licence du client pour confirmer si le numéro de licence a été modifié ou non, et fournir des instructions pour activer le logiciel si nécessaire. Si le problème n'est pas lié à la licence, le support technique devrait enquêter davantage pour déterminer la cause du problème et fournir une solution appropriée.",
"ticket_analyse": "1. Analyse du problème initial\n - Nom de la demande: Activation Logiciel\n - Description: Problème de licence\n - 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.\n\n2. Informations techniques essentielles\n - Logiciel: ESQ\n - Version: Non spécifiée dans les informations fournies\n - Configuration: Réinstallation sur un nouveau serveur pour le télétravail\n - Licence: Possibilité d'un changement de numéro de licence suite à un achat de version réseau en 2019\n\n3. Chronologie des échanges client/support\n - Message 1 - [AUTRE] De: Inconnu - Date: 26/03/2020 14:43:45\n - 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 si le problème est autre.\n - 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.\n - Pièces jointes: Deux images (image006.jpg et image005.jpg) montrant probablement la fenêtre d'activation du logiciel.\n - Réponse du support: Pas de réponse fournie dans les informations fournies.",
"images_analyses": [
{
"image_name": "image005.jpg",
"analyse": "### Analyse d'Image\n\n#### 1. Description Objective\nL'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 activer le logiciel.\n\n#### 2. Éléments Techniques Clés\n- **Titre de la fenêtre**: \"Activation du logiciel\"\n- **Champ d'ID du logiciel**: \"ID du logiciel\" avec un champ de texte vide\n- **Message d'instructions**:\n - \"Afin d'activer votre logiciel, veuillez saisir l'ID du logiciel fourni par CBAD.\"\n - \"Si vous ne disposez pas de votre ID de logiciel, veuillez contacter CBAD par mail à support@cbad.com ou par téléphone au 04 68 61 53 15 ou en cliquant sur le bouton téléphone ci-dessous et laisser un message.\"\n- **Options d'activation**:\n - \"Activer le logiciel (par internet)\"\n - \"Activer plus tard (4 jours restants)\"\n - \"Activation par téléphone\"\n\n#### 3. Relation avec le Problème\nL'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 entrer un ID de logiciel fourni par CBAD pour activer le logiciel. Cela suggère que le problème pourrait être lié à l'absence ou à l'incertitude concernant le numéro de licence correct. Le client est invité à contacter CBAD pour obtenir de l'aide, ce qui pourrait être pertinent pour résoudre le problème de licence."
}
],
"statistiques": {
"total_images": 2,
"images_pertinentes": 1,
"analyses_generees": 1
},
"metadata": {
"timestamp": "20250407_205814",
"model": "mistral-medium",
"temperature": 0.4,
"top_p": 0.9,
"max_tokens": 2500,
"agents": {
"image_sorter": {
"model": "pixtral-12b-latest",
"temperature": 0.2,
"top_p": 0.8,
"max_tokens": 300
},
"image_analyser": {
"model": "pixtral-12b-latest",
"temperature": 0.3,
"top_p": 0.9,
"max_tokens": 1200
},
"report_generator": {
"model": "mistral-medium",
"temperature": 0.4,
"top_p": 0.9,
"max_tokens": 2500
}
}
}
}

View File

@ -0,0 +1,112 @@
# Rapport d'analyse du ticket #T0101
*Généré le: 20250407_205814*
| Date | Émetteur | Type | Contenu | Statut |
|------|---------|------|---------|--------|
| 26/03/2020 14:43:45 | CLIENT | Question | Demande d'aide pour l'activation du logiciel ESQ sur un nouveau serveur, incertitude quant au numéro de licence suite à un achat de version réseau en 2019 par JB Lafitte. | **Sans réponse** |
| 26/03/2020 14:43:45 | 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.**
Rapport technique pour le ticket #T0101
1. Résumé exécutif
- Nom de la demande: Activation Logiciel
- Description: Le client a réinstallé le logiciel ESQ sur un nouveau 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. Chronologie des échanges
- Voir l'objet JSON ci-dessus pour les détails des échanges client/support.
3. Analyse des images
- Image 1 (image005.jpg): Cette capture d'écran montre une fenêtre d'activation de logiciel avec un champ pour entrer l'ID du logiciel, un message d'instructions, et trois options pour activer le logiciel. Cette image est pertinente pour le problème car elle montre que le client doit entrer un ID de logiciel fourni par CBAO pour activer le logiciel, ce qui suggère que le problème pourrait être lié à l'absence ou à l'incertitude concernant le numéro de licence correct.
4. Diagnostic technique
- Le client a réinstallé le logiciel ESQ sur un nouveau serveur pour permettre le télétravail, mais il rencontre un problème d'activation. 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. Le support technique devrait vérifier les informations de licence du client pour confirmer si le numéro de licence a été modifié ou non, et fournir des instructions pour activer le logiciel si nécessaire. Si le problème n'est pas lié à la licence, le support technique devrait enquêter davantage pour déterminer la cause du problème et fournir une solution appropriée.
---
# 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 si le problème est autre.
- 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.
- Réponse du support: Pas de réponse fournie dans les informations fournies.
```
</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 |
|-------|------------|
| image005.jpg | Oui |
### É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 activer le 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 à support@cbad.com ou par téléphone au 04 68 61 53 15 ou en cliquant sur le bouton téléphone ci-dessous et laisser un message."
- **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 entrer un ID de logiciel fourni par CBAD pour activer le logiciel. Cela suggère que le problème pourrait être lié à l'absence ou à l'incertitude concernant le numéro de licence correct. Le client est invité à contacter CBAD pour obtenir de l'aide, ce qui pourrait être pertinent pour résoudre le problème de licence.
```
</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

View File

@ -260,50 +260,81 @@ class TicketDataLoader:
return donnees
def trouver_ticket(self, dossier: str, ticket_id: str) -> Optional[Dict[str, Optional[str]]]:
def trouver_ticket(self, ticket_dir: str, ticket_id: str) -> Optional[Dict[str, Optional[str]]]:
"""
Recherche les fichiers de ticket dans différents emplacements possibles
Recherche des fichiers de ticket dans un répertoire spécifique
Args:
dossier: Dossier de base pour la recherche
ticket_id: ID du ticket (ex: T0101)
ticket_dir: Répertoire contenant les données du ticket
ticket_id: Code du ticket à rechercher
Returns:
Dictionnaire avec les chemins des fichiers trouvés par format ou None si aucun fichier trouvé
Dictionnaire avec les chemins des fichiers de rapport trouvés (JSON est le format privilégié)
ou None si aucun répertoire valide n'est trouvé
{
"json": chemin_du_fichier_json ou None si non trouvé,
"markdown": chemin_du_fichier_markdown ou None si non trouvé
}
"""
resultats: Dict[str, Optional[str]] = {"json": None, "markdown": None}
logger.info(f"Recherche du ticket {ticket_id} dans {ticket_dir}")
# Liste des emplacements possibles pour les rapports
emplacements_possibles = [
# 1. Dans le répertoire d'extraction directement
dossier,
# 2. Dans un sous-répertoire "data"
os.path.join(dossier, "data"),
# 3. Dans un sous-répertoire spécifique au ticket pour les rapports
os.path.join(dossier, f"{ticket_id}_rapports"),
# 4. Dans un sous-répertoire "rapports"
os.path.join(dossier, "rapports")
]
# Vérifier chaque emplacement
for base_location in emplacements_possibles:
# Chercher le fichier JSON
json_path = os.path.join(base_location, f"{ticket_id}_rapport.json")
if os.path.exists(json_path):
logger.info(f"Rapport JSON trouvé à: {json_path}")
resultats["json"] = json_path
# Chercher le fichier Markdown
md_path = os.path.join(base_location, f"{ticket_id}_rapport.md")
if os.path.exists(md_path):
logger.info(f"Rapport Markdown trouvé à: {md_path}")
resultats["markdown"] = md_path
if not resultats["json"] and not resultats["markdown"]:
logger.warning(f"Aucun rapport trouvé pour {ticket_id} dans {dossier}")
if not os.path.exists(ticket_dir):
logger.warning(f"Le répertoire {ticket_dir} n'existe pas")
return None
return resultats
rapport_dir = None
# Chercher d'abord dans le dossier spécifique aux rapports
rapports_dir = os.path.join(ticket_dir, f"{ticket_id}_rapports")
if os.path.exists(rapports_dir) and os.path.isdir(rapports_dir):
rapport_dir = rapports_dir
logger.info(f"Dossier de rapports trouvé: {rapports_dir}")
# Initialiser les chemins à None
json_path = None
md_path = None
# Si on a trouvé un dossier de rapports, chercher dedans
if rapport_dir:
# Privilégier d'abord le format JSON (format principal)
for filename in os.listdir(rapport_dir):
# Chercher le fichier JSON
if filename.endswith(".json") and ticket_id in filename:
json_path = os.path.join(rapport_dir, filename)
logger.info(f"Fichier JSON trouvé: {json_path}")
break # Priorité au premier fichier JSON trouvé
# Chercher le fichier Markdown comme fallback
for filename in os.listdir(rapport_dir):
if filename.endswith(".md") and ticket_id in filename:
md_path = os.path.join(rapport_dir, filename)
logger.info(f"Fichier Markdown trouvé: {md_path}")
break # Priorité au premier fichier Markdown trouvé
else:
# Si pas de dossier de rapports, chercher directement dans le répertoire du ticket
logger.info(f"Pas de dossier _rapports, recherche dans {ticket_dir}")
# Privilégier d'abord le format JSON (format principal)
for filename in os.listdir(ticket_dir):
# Chercher le JSON en priorité
if filename.endswith(".json") and ticket_id in filename and not filename.startswith("ticket_"):
json_path = os.path.join(ticket_dir, filename)
logger.info(f"Fichier JSON trouvé: {json_path}")
break # Priorité au premier fichier JSON trouvé
# Chercher le Markdown comme fallback
for filename in os.listdir(ticket_dir):
if filename.endswith(".md") and ticket_id in filename:
md_path = os.path.join(ticket_dir, filename)
logger.info(f"Fichier Markdown trouvé: {md_path}")
break # Priorité au premier fichier Markdown trouvé
# Si on n'a pas trouvé de fichier, alors renvoyer un dictionnaire vide plutôt que None
if not json_path and not md_path:
logger.warning(f"Aucun fichier de rapport trouvé pour le ticket {ticket_id}")
return {"json": None, "markdown": None}
return {
"json": json_path, # Format principal (prioritaire)
"markdown": md_path # Format secondaire (fallback)
}