llm_ticket3/agents/mistral_large/agent_ticket_analyser.py
2025-04-17 14:04:38 +02:00

215 lines
8.6 KiB
Python

from ..base_agent import BaseAgent
from typing import Dict, Any
import logging
import json
import os
from datetime import datetime
from loaders.ticket_data_loader import TicketDataLoader
from ..utils.pipeline_logger import sauvegarder_donnees
logger = logging.getLogger("AgentTicketAnalyser")
class AgentTicketAnalyser(BaseAgent):
"""
Agent pour analyser les tickets (JSON ou Markdown) et en extraire les informations importantes.
Utilisé pour contextualiser les tickets avant l'analyse des captures d'écran.
"""
def __init__(self, llm):
super().__init__("AgentTicketAnalyser", llm)
# Configuration adaptée à l'analyse structurée pour Mistral Large
self.temperature = 0.1
self.top_p = 0.8
self.max_tokens = 4000
# Prompt système clair, structuré, optimisé pour une analyse exhaustive
self.system_prompt = """Tu es un expert en analyse de tickets pour le support informatique de BRG-Lab (client : CBAO).
Tu dois :
1. Identifier le client et résumer la demande.
2. Reformuler les questions implicites du ticket (à partir du `name` et `description`).
3. Nettoyer et structurer les échanges client/support en conservant les informations utiles (liens, modules, options, erreurs) et en évitant d'introduire des éléments non pertinents.
4. Conserver tous les messages, même ceux qui semblent moins pertinents, pour maintenir le contexte complet de la discussion, y compris les détails spécifiques comme les couleurs ou les termes techniques.
5. Extraire les éléments à observer dans les captures (pour l'agent image).
Structure de sortie :
1. Résumé du contexte
2. Informations techniques détectées
3. Fil de discussion (filtré, classé, nettoyé)
4. Points visuels à analyser (éléments à retrouver dans les captures)
IMPORTANT :
- Ne propose pas de solution
- Garde tous les liens et noms de modules
- Ne transforme pas les propos sauf pour supprimer les bruits inutiles (signatures, mails automatiques)
- Ne reformule pas les messages sauf si cela clarifie les intentions sans altérer leur sens.
- Assure-toi que tous les messages sont inclus dans le fil de discussion, même s'ils semblent redondants ou peu clairs.
- Évite d'introduire des éléments fictifs ou non mentionnés dans les messages d'origine.
- Conserve le contenu intégral des messages, même s'ils contiennent des éléments de discussion qui semblent hors sujet.
"""
self.ticket_loader = TicketDataLoader()
self._appliquer_config_locale()
logger.info("AgentTicketAnalyser initialisé")
def _appliquer_config_locale(self) -> None:
"""Applique les paramètres et le prompt système au modèle"""
if hasattr(self.llm, "prompt_system"):
self.llm.prompt_system = self.system_prompt
if hasattr(self.llm, "configurer"):
self.llm.configurer(
temperature=self.temperature,
top_p=self.top_p,
max_tokens=self.max_tokens
)
def executer(self, ticket_data: Dict[str, Any]) -> str:
"""Analyse le ticket donné sous forme de dict"""
if isinstance(ticket_data, str) and os.path.exists(ticket_data):
ticket_data = self.ticket_loader.charger(ticket_data)
if not isinstance(ticket_data, dict):
logger.error("Les données du ticket ne sont pas valides")
return "ERREUR: Format de ticket invalide"
ticket_code = ticket_data.get("code", "Inconnu")
logger.info(f"Analyse du ticket {ticket_code}")
print(f"AgentTicketAnalyser: analyse du ticket {ticket_code}")
prompt = self._generer_prompt(ticket_data)
try:
response = self.llm.interroger(prompt)
logger.info("Analyse du ticket terminée avec succès")
print(f" Analyse terminée: {len(response)} caractères")
# Sauvegarde dans pipeline
result = {
"prompt": prompt,
"response": response,
"metadata": {
"timestamp": self._get_timestamp(),
"source_agent": self.nom,
"ticket_id": ticket_code,
"model_info": {
"model": getattr(self.llm, "modele", str(type(self.llm))),
**getattr(self.llm, "params", {})
}
}
}
sauvegarder_donnees(ticket_code, "analyse_ticket", result, base_dir="reports", is_resultat=True)
except Exception as e:
logger.error(f"Erreur d'analyse: {str(e)}")
return f"ERREUR: {str(e)}"
self.ajouter_historique(
"analyse_ticket",
{
"ticket_id": ticket_code,
"prompt": prompt,
"timestamp": self._get_timestamp()
},
response
)
return response
def _generer_prompt(self, ticket_data: Dict[str, Any]) -> str:
"""
Prépare un prompt texte structuré à partir des données brutes du ticket.
"""
ticket_code = ticket_data.get("code", "Inconnu")
name = ticket_data.get("name", "").strip()
description = ticket_data.get("description", "").strip()
# En-tête
texte = f"### TICKET {ticket_code}\n\n"
if name:
texte += f"#### NOM DU TICKET\n{name}\n\n"
if description:
texte += f"#### DESCRIPTION\n{description}\n\n"
# Informations techniques utiles (hors champs exclus)
champs_exclus = {"code", "name", "description", "messages", "metadata"}
info_techniques = [
f"- {k}: {json.dumps(v, ensure_ascii=False)}" if isinstance(v, (dict, list)) else f"- {k}: {v}"
for k, v in ticket_data.items()
if k not in champs_exclus and v
]
if info_techniques:
texte += "#### INFORMATIONS TECHNIQUES\n" + "\n".join(info_techniques) + "\n\n"
# Fil des échanges client/support
messages = ticket_data.get("messages", [])
if messages:
texte += "#### ÉCHANGES CLIENT / SUPPORT\n"
for i, msg in enumerate(messages):
if not isinstance(msg, dict):
continue
auteur = msg.get("author_id", "inconnu")
role = "CLIENT" if "client" in auteur.lower() else "SUPPORT" if "support" in auteur.lower() else "AUTRE"
date = self._formater_date(msg.get("date", "inconnue"))
contenu = msg.get("content", "").strip()
texte += f"{i+1}. [{role}] {auteur} ({date})\n{contenu}\n\n"
# Métadonnées utiles
metadata = ticket_data.get("metadata", {})
meta_text = [
f"- {k}: {json.dumps(v, ensure_ascii=False)}" if isinstance(v, (dict, list)) else f"- {k}: {v}"
for k, v in metadata.items()
if k not in {"format", "source_file"} and v
]
if meta_text:
texte += "#### MÉTADONNÉES\n" + "\n".join(meta_text) + "\n"
return texte
def _formater_date(self, date_str: str) -> str:
"""
Formate la date en DD/MM/YYYY HH:MM si possible
Args:
date_str: Chaîne de caractères représentant une date
Returns:
Date formatée ou la chaîne originale si le format n'est pas reconnu
"""
if not date_str:
return "date inconnue"
try:
# Essayer différents formats courants
formats = [
"%Y-%m-%dT%H:%M:%S.%fZ", # Format ISO avec microsecondes et Z
"%Y-%m-%dT%H:%M:%SZ", # Format ISO sans microsecondes avec Z
"%Y-%m-%dT%H:%M:%S", # Format ISO sans Z
"%Y-%m-%d %H:%M:%S", # Format standard
"%d/%m/%Y %H:%M:%S", # Format français
"%d/%m/%Y" # Format date simple
]
for fmt in formats:
try:
dt = datetime.strptime(date_str, fmt)
return dt.strftime("%d/%m/%Y %H:%M")
except ValueError:
continue
# Si aucun format ne correspond, retourner la chaîne originale
return date_str
except Exception:
return date_str
def _get_timestamp(self) -> str:
"""
Génère un timestamp au format YYYYMMDD_HHMMSS
Returns:
Timestamp formaté
"""
return datetime.now().strftime("%Y%m%d_%H%M%S")