mirror of
https://github.com/Ladebeze66/llm_ticket3.git
synced 2025-12-15 19:16:52 +01:00
447 lines
19 KiB
Python
447 lines
19 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Module pour formater les rapports à partir des fichiers JSON générés par l'AgentReportGenerator.
|
|
|
|
Ce module prend en entrée un fichier JSON contenant les analyses et génère différents
|
|
formats de sortie (Markdown, HTML, etc.) sans utiliser de LLM.
|
|
"""
|
|
|
|
import os
|
|
import json
|
|
import argparse
|
|
import sys
|
|
import re
|
|
from datetime import datetime
|
|
from typing import Dict, List, Any, Optional, Tuple
|
|
|
|
def generate_markdown_report(json_path: str, output_path: Optional[str] = None) -> Tuple[bool, str]:
|
|
"""
|
|
Génère un rapport au format Markdown à partir d'un fichier JSON.
|
|
|
|
Args:
|
|
json_path: Chemin vers le fichier JSON contenant les données du rapport
|
|
output_path: Chemin de sortie pour le fichier Markdown (facultatif)
|
|
|
|
Returns:
|
|
Tuple (succès, chemin du fichier généré ou message d'erreur)
|
|
"""
|
|
try:
|
|
# Lire le fichier JSON
|
|
with open(json_path, "r", encoding="utf-8") as f:
|
|
rapport_data = json.load(f)
|
|
|
|
# Si le chemin de sortie n'est pas spécifié, le créer à partir du chemin d'entrée
|
|
if not output_path:
|
|
# Remplacer l'extension JSON par MD
|
|
output_path = os.path.splitext(json_path)[0] + ".md"
|
|
|
|
# Générer le contenu Markdown
|
|
markdown_content = _generate_markdown_content(rapport_data)
|
|
|
|
# Écrire le contenu dans le fichier de sortie
|
|
with open(output_path, "w", encoding="utf-8") as f:
|
|
f.write(markdown_content)
|
|
|
|
print(f"Rapport Markdown généré avec succès: {output_path}")
|
|
return True, output_path
|
|
|
|
except Exception as e:
|
|
error_message = f"Erreur lors de la génération du rapport Markdown: {str(e)}"
|
|
print(error_message)
|
|
return False, error_message
|
|
|
|
def _generate_markdown_content(rapport_data: Dict) -> str:
|
|
"""
|
|
Génère le contenu Markdown à partir des données du rapport.
|
|
|
|
Args:
|
|
rapport_data: Dictionnaire contenant les données du rapport
|
|
|
|
Returns:
|
|
Contenu Markdown
|
|
"""
|
|
ticket_id = rapport_data.get("ticket_id", "")
|
|
timestamp = rapport_data.get("metadata", {}).get("timestamp", "")
|
|
generation_date = rapport_data.get("metadata", {}).get("generation_date", datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
|
|
|
# Entête du document
|
|
markdown = f"# Rapport d'analyse du ticket #{ticket_id}\n\n"
|
|
markdown += f"*Généré le: {generation_date}*\n\n"
|
|
|
|
# 1. Résumé exécutif
|
|
if "resume" in rapport_data and rapport_data["resume"]:
|
|
markdown += rapport_data["resume"] + "\n\n"
|
|
|
|
# 2. Chronologie des échanges (tableau)
|
|
markdown += "## Chronologie des échanges\n\n"
|
|
|
|
if "chronologie_echanges" in rapport_data and rapport_data["chronologie_echanges"]:
|
|
# Créer un tableau pour les échanges
|
|
markdown += "| Date | Émetteur | Type | Contenu | Statut |\n"
|
|
markdown += "|------|---------|------|---------|--------|\n"
|
|
|
|
# Prétraitement pour détecter les questions sans réponse
|
|
questions_sans_reponse = {}
|
|
echanges = rapport_data["chronologie_echanges"]
|
|
|
|
for i, echange in enumerate(echanges):
|
|
if echange.get("type", "").lower() == "question" and echange.get("emetteur", "").lower() == "client":
|
|
has_response = False
|
|
# Vérifier si la question a une réponse
|
|
for j in range(i+1, len(echanges)):
|
|
next_echange = echanges[j]
|
|
if next_echange.get("type", "").lower() == "réponse" and next_echange.get("emetteur", "").lower() == "support":
|
|
has_response = True
|
|
break
|
|
questions_sans_reponse[i] = not has_response
|
|
|
|
# Générer les lignes du tableau
|
|
for i, echange in enumerate(echanges):
|
|
date = echange.get("date", "-")
|
|
emetteur = echange.get("emetteur", "-")
|
|
type_msg = echange.get("type", "-")
|
|
contenu = echange.get("contenu", "-")
|
|
|
|
# Ajouter un statut pour les questions sans réponse
|
|
statut = ""
|
|
if emetteur.lower() == "client" and type_msg.lower() == "question" and questions_sans_reponse.get(i, False):
|
|
statut = "**Sans réponse**"
|
|
|
|
markdown += f"| {date} | {emetteur} | {type_msg} | {contenu} | {statut} |\n"
|
|
|
|
# Ajouter une note si aucune réponse du support n'a été trouvée
|
|
if not any(echange.get("emetteur", "").lower() == "support" for echange in echanges):
|
|
markdown += "\n**Note: Aucune réponse du support n'a été trouvée dans ce ticket.**\n\n"
|
|
else:
|
|
markdown += "*Aucun échange détecté dans le ticket.*\n\n"
|
|
|
|
# 3. Analyse des images
|
|
markdown += "## Analyse des images\n\n"
|
|
|
|
if "images_analyses" in rapport_data and rapport_data["images_analyses"]:
|
|
images_list = rapport_data["images_analyses"]
|
|
|
|
if not images_list:
|
|
markdown += "*Aucune image pertinente n'a été identifiée.*\n\n"
|
|
else:
|
|
for i, img_data in enumerate(images_list, 1):
|
|
image_name = img_data.get("image_name", f"Image {i}")
|
|
sorting_info = img_data.get("sorting_info", {})
|
|
reason = sorting_info.get("reason", "Non spécifiée")
|
|
|
|
markdown += f"### Image {i}: {image_name}\n\n"
|
|
|
|
# Raison de la pertinence
|
|
if reason:
|
|
markdown += f"**Raison de la pertinence**: {reason}\n\n"
|
|
|
|
# Ajouter l'analyse détaillée dans une section dépliable
|
|
analyse_detail = img_data.get("analyse", "Aucune analyse disponible")
|
|
if analyse_detail:
|
|
markdown += "<details>\n<summary>Analyse détaillée de l'image</summary>\n\n"
|
|
markdown += "```\n" + analyse_detail + "\n```\n\n"
|
|
markdown += "</details>\n\n"
|
|
else:
|
|
markdown += "*Aucune image pertinente n'a été analysée.*\n\n"
|
|
|
|
# 4. Diagnostic technique
|
|
if "diagnostic" in rapport_data and rapport_data["diagnostic"]:
|
|
markdown += "## Diagnostic technique\n\n"
|
|
markdown += rapport_data["diagnostic"] + "\n\n"
|
|
|
|
# Tableau récapitulatif des échanges (nouveau)
|
|
if "tableau_questions_reponses" in rapport_data and rapport_data["tableau_questions_reponses"]:
|
|
markdown += rapport_data["tableau_questions_reponses"] + "\n\n"
|
|
|
|
# Section séparatrice
|
|
markdown += "---\n\n"
|
|
|
|
# Détails des analyses effectuées
|
|
markdown += "# Détails des analyses effectuées\n\n"
|
|
markdown += "## Processus d'analyse\n\n"
|
|
|
|
# 1. Analyse de ticket
|
|
ticket_analyse = rapport_data.get("ticket_analyse", "")
|
|
if ticket_analyse:
|
|
markdown += "### Étape 1: Analyse du ticket\n\n"
|
|
markdown += "L'agent d'analyse de ticket a extrait les informations suivantes du ticket d'origine:\n\n"
|
|
markdown += "<details>\n<summary>Cliquez pour voir l'analyse complète du ticket</summary>\n\n"
|
|
markdown += "```\n" + str(ticket_analyse) + "\n```\n\n"
|
|
markdown += "</details>\n\n"
|
|
else:
|
|
markdown += "### Étape 1: Analyse du ticket\n\n"
|
|
markdown += "*Aucune analyse de ticket disponible*\n\n"
|
|
|
|
# 2. Tri des images
|
|
markdown += "### Étape 2: Tri des images\n\n"
|
|
markdown += "L'agent de tri d'images a évalué chaque image pour déterminer sa pertinence par rapport au problème client:\n\n"
|
|
|
|
# Création d'un tableau récapitulatif
|
|
images_list = rapport_data.get("images_analyses", [])
|
|
if images_list:
|
|
markdown += "| Image | Pertinence | Raison |\n"
|
|
markdown += "|-------|------------|--------|\n"
|
|
|
|
for img_data in images_list:
|
|
image_name = img_data.get("image_name", "Image inconnue")
|
|
sorting_info = img_data.get("sorting_info", {})
|
|
is_relevant = "Oui" if sorting_info else "Oui" # Par défaut, si présent dans la liste c'est pertinent
|
|
reason = sorting_info.get("reason", "Non spécifiée")
|
|
|
|
markdown += f"| {image_name} | {is_relevant} | {reason} |\n"
|
|
|
|
markdown += "\n"
|
|
else:
|
|
markdown += "*Aucune image n'a été triée pour ce ticket.*\n\n"
|
|
|
|
# 3. Analyse des images
|
|
markdown += "### Étape 3: Analyse détaillée des images pertinentes\n\n"
|
|
|
|
if images_list:
|
|
for i, img_data in enumerate(images_list, 1):
|
|
image_name = img_data.get("image_name", f"Image {i}")
|
|
analyse_detail = img_data.get("analyse", "Analyse non disponible")
|
|
|
|
markdown += f"#### Image pertinente {i}: {image_name}\n\n"
|
|
markdown += "<details>\n<summary>Cliquez pour voir l'analyse complète de l'image</summary>\n\n"
|
|
markdown += "```\n" + str(analyse_detail) + "\n```\n\n"
|
|
markdown += "</details>\n\n"
|
|
else:
|
|
markdown += "*Aucune image pertinente n'a été identifiée pour ce ticket.*\n\n"
|
|
|
|
# 4. Génération du rapport
|
|
markdown += "### Étape 4: Génération du rapport de synthèse\n\n"
|
|
markdown += "L'agent de génération de rapport a synthétisé toutes les analyses précédentes pour produire le rapport ci-dessus.\n\n"
|
|
|
|
# Informations techniques et métadonnées
|
|
markdown += "## Informations techniques\n\n"
|
|
|
|
# Statistiques
|
|
statistiques = rapport_data.get("statistiques", {})
|
|
metadata = rapport_data.get("metadata", {})
|
|
|
|
markdown += "### Statistiques\n\n"
|
|
markdown += f"- **Images analysées**: {statistiques.get('total_images', 0)}\n"
|
|
markdown += f"- **Images pertinentes**: {statistiques.get('images_pertinentes', 0)}\n"
|
|
|
|
if "generation_time" in statistiques:
|
|
markdown += f"- **Temps de génération**: {statistiques['generation_time']:.2f} secondes\n"
|
|
|
|
# Modèle utilisé
|
|
markdown += "\n### Modèle LLM utilisé\n\n"
|
|
markdown += f"- **Modèle**: {metadata.get('model', 'Non spécifié')}\n"
|
|
|
|
if "model_version" in metadata:
|
|
markdown += f"- **Version**: {metadata.get('model_version', 'Non spécifiée')}\n"
|
|
|
|
markdown += f"- **Température**: {metadata.get('temperature', 'Non spécifiée')}\n"
|
|
markdown += f"- **Top_p**: {metadata.get('top_p', 'Non spécifié')}\n"
|
|
|
|
# Section sur les agents utilisés
|
|
if "agents" in metadata:
|
|
markdown += "\n### Agents impliqués\n\n"
|
|
|
|
agents = metadata["agents"]
|
|
|
|
# Agent d'analyse de ticket
|
|
if "json_analyser" in agents:
|
|
markdown += "#### Agent d'analyse du ticket\n"
|
|
json_analyser = agents["json_analyser"]
|
|
if "model_info" in json_analyser:
|
|
markdown += f"- **Modèle**: {json_analyser['model_info'].get('name', 'Non spécifié')}\n"
|
|
|
|
# Agent de tri d'images
|
|
if "image_sorter" in agents:
|
|
markdown += "\n#### Agent de tri d'images\n"
|
|
sorter = agents["image_sorter"]
|
|
# Récupérer directement le modèle ou via model_info selon la structure
|
|
if "model" in sorter:
|
|
markdown += f"- **Modèle**: {sorter.get('model', 'Non spécifié')}\n"
|
|
markdown += f"- **Température**: {sorter.get('temperature', 'Non spécifiée')}\n"
|
|
markdown += f"- **Top_p**: {sorter.get('top_p', 'Non spécifié')}\n"
|
|
elif "model_info" in sorter:
|
|
markdown += f"- **Modèle**: {sorter['model_info'].get('name', 'Non spécifié')}\n"
|
|
else:
|
|
markdown += f"- **Modèle**: Non spécifié\n"
|
|
|
|
# Agent d'analyse d'images
|
|
if "image_analyser" in agents:
|
|
markdown += "\n#### Agent d'analyse d'images\n"
|
|
analyser = agents["image_analyser"]
|
|
# Récupérer directement le modèle ou via model_info selon la structure
|
|
if "model" in analyser:
|
|
markdown += f"- **Modèle**: {analyser.get('model', 'Non spécifié')}\n"
|
|
markdown += f"- **Température**: {analyser.get('temperature', 'Non spécifiée')}\n"
|
|
markdown += f"- **Top_p**: {analyser.get('top_p', 'Non spécifié')}\n"
|
|
elif "model_info" in analyser:
|
|
markdown += f"- **Modèle**: {analyser['model_info'].get('name', 'Non spécifié')}\n"
|
|
else:
|
|
markdown += f"- **Modèle**: Non spécifié\n"
|
|
|
|
# Ajouter une section pour les prompts s'ils sont présents
|
|
if "prompts_utilisés" in rapport_data and rapport_data["prompts_utilisés"]:
|
|
markdown += "\n## Prompts utilisés\n\n"
|
|
prompts = rapport_data["prompts_utilisés"]
|
|
|
|
for agent, prompt in prompts.items():
|
|
# Si le prompt est trop long, le tronquer pour éviter des rapports trop volumineux
|
|
if len(prompt) > 2000:
|
|
debut = prompt[:1000].strip()
|
|
fin = prompt[-1000:].strip()
|
|
prompt_tronque = f"{debut}\n\n[...]\n\n{fin}"
|
|
markdown += f"### Agent: {agent}\n\n```\n{prompt_tronque}\n```\n\n"
|
|
else:
|
|
markdown += f"### Agent: {agent}\n\n```\n{prompt}\n```\n\n"
|
|
|
|
return markdown
|
|
|
|
def generate_html_report(json_path: str, output_path: Optional[str] = None) -> Tuple[bool, str]:
|
|
"""
|
|
Génère un rapport au format HTML à partir d'un fichier JSON.
|
|
|
|
Args:
|
|
json_path: Chemin vers le fichier JSON contenant les données du rapport
|
|
output_path: Chemin de sortie pour le fichier HTML (facultatif)
|
|
|
|
Returns:
|
|
Tuple (succès, chemin du fichier généré ou message d'erreur)
|
|
"""
|
|
try:
|
|
# Générer d'abord le Markdown
|
|
success, md_path_or_error = generate_markdown_report(json_path, None)
|
|
|
|
if not success:
|
|
return False, md_path_or_error
|
|
|
|
# Lire le contenu Markdown
|
|
with open(md_path_or_error, "r", encoding="utf-8") as f:
|
|
markdown_content = f.read()
|
|
|
|
# Si le chemin de sortie n'est pas spécifié, le créer à partir du chemin d'entrée
|
|
if not output_path:
|
|
# Remplacer l'extension JSON par HTML
|
|
output_path = os.path.splitext(json_path)[0] + ".html"
|
|
|
|
# Conversion Markdown → HTML (avec gestion de l'absence de mistune)
|
|
html_content = _simple_markdown_to_html(markdown_content)
|
|
|
|
# Essayer d'utiliser mistune pour une meilleure conversion si disponible
|
|
try:
|
|
import mistune
|
|
markdown = mistune.create_markdown(escape=False)
|
|
html_content = markdown(markdown_content)
|
|
print("Conversion HTML effectuée avec mistune")
|
|
except ImportError:
|
|
print("Module mistune non disponible, utilisation de la conversion HTML simplifiée")
|
|
|
|
# Créer un HTML complet avec un peu de style
|
|
html_page = f"""<!DOCTYPE html>
|
|
<html lang="fr">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Rapport d'analyse de ticket</title>
|
|
<style>
|
|
body {{ font-family: Arial, sans-serif; line-height: 1.6; margin: 0; padding: 20px; color: #333; max-width: 1200px; margin: 0 auto; }}
|
|
h1 {{ color: #2c3e50; border-bottom: 2px solid #eee; padding-bottom: 10px; }}
|
|
h2 {{ color: #3498db; margin-top: 30px; }}
|
|
h3 {{ color: #2980b9; }}
|
|
h4 {{ color: #16a085; }}
|
|
table {{ border-collapse: collapse; width: 100%; margin: 20px 0; }}
|
|
th, td {{ padding: 12px 15px; text-align: left; border-bottom: 1px solid #ddd; }}
|
|
th {{ background-color: #f2f2f2; }}
|
|
tr:hover {{ background-color: #f5f5f5; }}
|
|
code, pre {{ background: #f8f8f8; border: 1px solid #ddd; border-radius: 3px; padding: 10px; overflow-x: auto; }}
|
|
details {{ margin: 15px 0; }}
|
|
summary {{ cursor: pointer; font-weight: bold; color: #2980b9; }}
|
|
.status {{ color: #e74c3c; font-weight: bold; }}
|
|
hr {{ border: 0; height: 1px; background: #eee; margin: 30px 0; }}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
{html_content}
|
|
</body>
|
|
</html>"""
|
|
|
|
# Écrire le contenu dans le fichier de sortie
|
|
with open(output_path, "w", encoding="utf-8") as f:
|
|
f.write(html_page)
|
|
|
|
print(f"Rapport HTML généré avec succès: {output_path}")
|
|
return True, output_path
|
|
|
|
except Exception as e:
|
|
error_message = f"Erreur lors de la génération du rapport HTML: {str(e)}"
|
|
print(error_message)
|
|
return False, error_message
|
|
|
|
def _simple_markdown_to_html(markdown_content: str) -> str:
|
|
"""
|
|
Convertit un contenu Markdown en HTML de façon simplifiée.
|
|
|
|
Args:
|
|
markdown_content: Contenu Markdown à convertir
|
|
|
|
Returns:
|
|
Contenu HTML
|
|
"""
|
|
html = markdown_content
|
|
|
|
# Titres
|
|
html = re.sub(r'^# (.*?)$', r'<h1>\1</h1>', html, flags=re.MULTILINE)
|
|
html = re.sub(r'^## (.*?)$', r'<h2>\1</h2>', html, flags=re.MULTILINE)
|
|
html = re.sub(r'^### (.*?)$', r'<h3>\1</h3>', html, flags=re.MULTILINE)
|
|
html = re.sub(r'^#### (.*?)$', r'<h4>\1</h4>', html, flags=re.MULTILINE)
|
|
|
|
# Emphase
|
|
html = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', html)
|
|
html = re.sub(r'\*(.*?)\*', r'<em>\1</em>', html)
|
|
|
|
# Lists
|
|
html = re.sub(r'^- (.*?)$', r'<li>\1</li>', html, flags=re.MULTILINE)
|
|
|
|
# Paragraphes
|
|
html = re.sub(r'([^\n])\n([^\n])', r'\1<br>\2', html)
|
|
html = re.sub(r'\n\n', r'</p><p>', html)
|
|
|
|
# Tables simplifiées (sans analyser la structure)
|
|
html = re.sub(r'\| (.*?) \|', r'<td>\1</td>', html)
|
|
|
|
# Code blocks
|
|
html = re.sub(r'```(.*?)```', r'<pre><code>\1</code></pre>', html, flags=re.DOTALL)
|
|
|
|
# Envelopper dans des balises paragraphe
|
|
html = f"<p>{html}</p>"
|
|
|
|
return html
|
|
|
|
def process_report(json_path: str, output_format: str = "markdown") -> None:
|
|
"""
|
|
Traite un rapport dans le format spécifié.
|
|
|
|
Args:
|
|
json_path: Chemin vers le fichier JSON contenant les données du rapport
|
|
output_format: Format de sortie (markdown ou html)
|
|
"""
|
|
if output_format.lower() == "markdown":
|
|
generate_markdown_report(json_path)
|
|
elif output_format.lower() == "html":
|
|
generate_html_report(json_path)
|
|
else:
|
|
print(f"Format non supporté: {output_format}")
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(description="Formateur de rapports à partir de fichiers JSON")
|
|
parser.add_argument("json_path", help="Chemin vers le fichier JSON contenant les données du rapport")
|
|
parser.add_argument("--format", "-f", choices=["markdown", "html"], default="markdown",
|
|
help="Format de sortie (markdown par défaut)")
|
|
parser.add_argument("--output", "-o", help="Chemin de sortie pour le rapport (facultatif)")
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.format == "markdown":
|
|
generate_markdown_report(args.json_path, args.output)
|
|
elif args.format == "html":
|
|
generate_html_report(args.json_path, args.output)
|
|
else:
|
|
print(f"Format non supporté: {args.format}") |