llm_ticket3/agents/utils/report_utils.py
2025-04-09 13:36:42 +02:00

255 lines
11 KiB
Python

"""
Utilitaires pour la génération de rapports d'analyse, extraits de AgentReportGenerator.
"""
import re
import json
import logging
from typing import Dict, List, Any, Tuple, Optional
from datetime import datetime
logger = logging.getLogger("report_utils")
def get_timestamp() -> str:
"""
Retourne un timestamp au format YYYYMMDD_HHMMSS pour identifier les fichiers et données.
Returns:
Chaîne formatée avec le timestamp actuel
"""
return datetime.now().strftime("%Y%m%d_%H%M%S")
def generer_tableau_questions_reponses(echanges: List[Dict]) -> str:
"""
Génère un tableau question/réponse simplifié à partir des échanges
Args:
echanges: Liste des échanges client/support
Returns:
Tableau au format markdown
"""
if not echanges:
return "Aucun échange trouvé dans ce ticket."
# Initialiser le tableau
tableau = "\n## Tableau récapitulatif des échanges\n\n"
tableau += "| Question (Client) | Réponse (Support) |\n"
tableau += "|------------------|-------------------|\n"
# Variables pour suivre les questions et réponses
question_courante = None
questions_sans_reponse = []
# Parcourir tous les échanges pour identifier les questions et réponses
for echange in echanges:
emetteur = echange.get("emetteur", "").lower()
type_msg = echange.get("type", "").lower()
contenu = echange.get("contenu", "")
date = echange.get("date", "")
# Formater le contenu (synthétiser si trop long)
contenu_formate = synthétiser_contenu(contenu, 150)
# Si c'est une question du client
if emetteur == "client" and (type_msg == "question" or "?" in contenu):
# Si une question précédente n'a pas de réponse, l'ajouter à la liste
if question_courante:
questions_sans_reponse.append(question_courante)
# Enregistrer la nouvelle question courante
question_courante = f"{contenu_formate} _(date: {date})_"
# Si c'est une réponse du support et qu'il y a une question en attente
elif emetteur == "support" and question_courante:
# Ajouter la paire question/réponse au tableau
tableau += f"| {question_courante} | {contenu_formate} _(date: {date})_ |\n"
question_courante = None # Réinitialiser la question courante
# Traiter toute question restante sans réponse
if question_courante:
questions_sans_reponse.append(question_courante)
# Ajouter les questions sans réponse au tableau
for q in questions_sans_reponse:
tableau += f"| {q} | **Aucune réponse du support** |\n"
# Ajouter une note si aucun échange support n'a été trouvé
if not any(echange.get("emetteur", "").lower() == "support" for echange in echanges):
tableau += "\n**Note: Aucune réponse du support n'a été trouvée dans ce ticket.**\n"
return tableau
def synthétiser_contenu(contenu: str, longueur_max: int) -> str:
"""
Synthétise le contenu s'il est trop long
Args:
contenu: Contenu à synthétiser
longueur_max: Longueur maximale souhaitée
Returns:
Contenu synthétisé
"""
if len(contenu) <= longueur_max:
return contenu
# Extraire les premiers caractères
debut = contenu[:longueur_max//2].strip()
# Extraire les derniers caractères
fin = contenu[-(longueur_max//2):].strip()
return f"{debut}... {fin}"
def extraire_et_traiter_json(texte_rapport: str) -> Tuple[str, Optional[Dict], Optional[str]]:
"""
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")
# Patterns de recherche plus variés pour s'adapter aux différents modèles
patterns = [
r'```json\s*({.*?})\s*```', # Pattern standard avec backticks triples
r'```\s*({.*?"chronologie_echanges".*?})\s*```', # Pattern sans spécifier json mais avec le contenu attendu
r'{[\s\n]*"chronologie_echanges"[\s\n]*:[\s\n]*\[.*?\][\s\n]*}', # Pattern sans backticks
r'<json>(.*?)</json>' # Pattern alternatif avec balises xml
]
# Essayer chaque pattern
json_text = None
json_match = None
for pattern in patterns:
json_match = re.search(pattern, texte_rapport, re.DOTALL)
if json_match:
json_text = json_match.group(1).strip()
logger.info(f"JSON trouvé avec le pattern: {pattern[:20]}...")
break
# Si aucun pattern n'a fonctionné, tenter une approche plus agressive pour extraire le JSON
if not json_text:
# Chercher des indices de début de JSON dans le texte
potential_starts = [
texte_rapport.find('{"chronologie_echanges"'),
texte_rapport.find('{\n "chronologie_echanges"'),
texte_rapport.find('{ "chronologie_echanges"')
]
# Filtrer les indices valides (non -1)
valid_starts = [idx for idx in potential_starts if idx != -1]
if valid_starts:
# Prendre l'indice le plus petit (premier dans le texte)
start_idx = min(valid_starts)
# Chercher la fin du JSON (accolade fermante suivie d'une nouvelle ligne ou de la fin du texte)
json_extract = texte_rapport[start_idx:]
# Compter les accolades pour trouver la fermeture du JSON
open_braces = 0
close_idx = -1
for i, char in enumerate(json_extract):
if char == '{':
open_braces += 1
elif char == '}':
open_braces -= 1
if open_braces == 0:
close_idx = i
break
if close_idx != -1:
json_text = json_extract[:close_idx + 1]
logger.info(f"JSON extrait par analyse d'accolades: {len(json_text)} caractères")
if not json_text:
logger.warning("Aucun JSON trouvé dans le rapport")
return texte_rapport, None, None
# Nettoyage supplémentaire du JSON
# Enlever caractères non imprimables ou indésirables qui pourraient être ajoutés par certains modèles
json_text = re.sub(r'[\x00-\x1F\x7F]', '', json_text)
try:
# Vérifier que le texte commence par { et se termine par }
if not (json_text.startswith('{') and json_text.endswith('}')):
logger.warning(f"Format JSON incorrect, tentative de correction. Texte: {json_text[:50]}...")
# Chercher les délimiteurs du JSON
start = json_text.find('{')
end = json_text.rfind('}')
if start != -1 and end != -1 and start < end:
json_text = json_text[start:end+1]
echanges_json = json.loads(json_text)
logger.info(f"JSON extrait avec succès: {len(json_text)} caractères")
# Vérifier si le JSON a la structure attendue
if not isinstance(echanges_json, dict) or "chronologie_echanges" not in echanges_json:
# Tenter de corriger la structure si possible
if len(echanges_json) > 0 and isinstance(list(echanges_json.values())[0], list):
# Prendre la première liste comme chronologie
key = list(echanges_json.keys())[0]
echanges_json = {"chronologie_echanges": echanges_json[key]}
logger.info(f"Structure JSON corrigée en utilisant la clé: {key}")
else:
logger.warning("Structure JSON incorrecte et non réparable")
return texte_rapport, None, None
# 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"
# Ajouter un tableau questions/réponses simplifié
tableau_qr = generer_tableau_questions_reponses(echanges_json["chronologie_echanges"])
echanges_markdown += f"\n{tableau_qr}\n"
# Remplacer le JSON dans le texte par le tableau Markdown
# Si le JSON était entouré de backticks, remplacer tout le bloc
if json_match:
rapport_traite = texte_rapport.replace(json_match.group(0), echanges_markdown)
else:
# Sinon, remplacer juste le texte JSON
rapport_traite = texte_rapport.replace(json_text, echanges_markdown)
return rapport_traite, echanges_json, echanges_markdown
except json.JSONDecodeError as e:
logger.error(f"Erreur lors du décodage JSON: {e}")
logger.debug(f"Contenu JSON problématique: {json_text[:100]}...")
return texte_rapport, None, None