mirror of
https://github.com/Ladebeze66/llm_ticket3.git
synced 2025-12-13 14:06:51 +01:00
266 lines
11 KiB
Plaintext
266 lines
11 KiB
Plaintext
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.05
|
|
self.top_p = 0.8
|
|
self.max_tokens = 7000
|
|
|
|
# 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).
|
|
|
|
Je vais te donner un ticket de support avec plusieurs messages, et tu dois REPRODUIRE EXACTEMENT le contenu technique de chaque message, sans inventer ni reformuler.
|
|
|
|
STRUCTURE DE SORTIE ATTENDUE :
|
|
1. Résumé du contexte (bref, factuel)
|
|
2. Informations techniques détectées
|
|
3. Fil de discussion (REPRODUCTION EXACTE, SANS HALLUCINATION)
|
|
4. Points visuels à analyser (éléments à retrouver dans les captures)
|
|
|
|
RÈGLES ABSOLUES :
|
|
- Le NOM (titre) et la DESCRIPTION du ticket sont le PREMIER MESSAGE du client et contiennent souvent l'information cruciale
|
|
- REPRODUIS MOT POUR MOT le contenu des messages, notamment les termes techniques comme "essai au bleu"
|
|
- N'INVENTE JAMAIS de messages qui n'existent pas dans le ticket original
|
|
- N'INTRODUIS JAMAIS de termes comme "XYZ" ou autres références qui n'existent pas dans le texte original
|
|
- NE DÉDUIS PAS d'information qui n'est pas explicitement mentionnée
|
|
- NE REFORMULE PAS les messages, copie-les exactement
|
|
- GARDE TOUS les liens, noms d'essais et détails techniques tels quels
|
|
- Ne supprime que les signatures, formules de politesse et parties non pertinentes
|
|
|
|
ATTENTION AUX DÉTAILS :
|
|
- Un "essai au bleu" doit rester "essai au bleu", pas "essai" ni "essai XYZ"
|
|
- Les auteurs des messages doivent être correctement attribués
|
|
- L'ordre chronologique des messages doit être respecté
|
|
- Les URLs et références techniques doivent être préservées telles quelles
|
|
|
|
ASTUCE : Si tu n'es pas sûr d'un détail, reproduis-le tel quel plutôt que de l'interpréter.
|
|
"""
|
|
|
|
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()
|
|
create_date = ticket_data.get("create_date", "")
|
|
partner_id_name = ticket_data.get("partner_id_name", "").strip()
|
|
email_from = ticket_data.get("email_from", "").strip()
|
|
|
|
# En-tête avec instructions spécifiques
|
|
texte = f"### TICKET {ticket_code}\n\n"
|
|
texte += "IMPORTANT: Tu dois reproduire EXACTEMENT le contenu des messages sans reformulation ni invention.\n"
|
|
texte += "ATTENTION: Tu dois conserver les termes techniques EXACTS comme 'essai au bleu' sans les modifier.\n"
|
|
texte += "CRITIQUE: Le NOM et la DESCRIPTION du ticket sont le PREMIER MESSAGE du client et doivent être inclus au début du fil.\n"
|
|
texte += "INTERDIT: N'invente JAMAIS de messages qui n'existent pas dans le ticket.\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 - format amélioré
|
|
texte += "#### ÉCHANGES CLIENT / SUPPORT (À REPRODUIRE TEXTUELLEMENT)\n"
|
|
|
|
# Ajouter le titre et la description comme premier message si pertinent
|
|
if name or description:
|
|
texte += "--- MESSAGE INITIAL (Ouverture du ticket) ---\n"
|
|
texte += f"ID: ticket_creation\n"
|
|
texte += f"Auteur: {partner_id_name if partner_id_name else email_from if email_from else 'Client'}\n"
|
|
texte += f"Date: {create_date}\n"
|
|
texte += f"Type: Création du ticket\n"
|
|
texte += f"Sujet: {name}\n"
|
|
texte += "Contenu (à reproduire EXACTEMENT):\n"
|
|
if description and description != "<p><br></p>":
|
|
texte += f"{description}\n\n"
|
|
else:
|
|
texte += f"{name}\n\n"
|
|
|
|
# Ajouter les messages standards
|
|
messages = ticket_data.get("messages", [])
|
|
if messages:
|
|
for i, msg in enumerate(messages):
|
|
if not isinstance(msg, dict):
|
|
continue
|
|
|
|
# Extraction des métadonnées du message
|
|
auteur = msg.get("author_id", "inconnu")
|
|
date = self._formater_date(msg.get("date", "inconnue"))
|
|
sujet = msg.get("subject", "").strip()
|
|
message_type = msg.get("message_type", "").strip()
|
|
msg_id = msg.get("id", "").strip()
|
|
|
|
# Extraction du contenu avec préservation exacte
|
|
contenu = msg.get("content", "").strip()
|
|
|
|
# Format cohérent pour faciliter la compréhension du modèle
|
|
texte += f"--- MESSAGE {i+1} ---\n"
|
|
texte += f"ID: {msg_id}\n"
|
|
texte += f"Auteur: {auteur}\n"
|
|
texte += f"Date: {date}\n"
|
|
texte += f"Type: {message_type}\n"
|
|
if sujet:
|
|
texte += f"Sujet: {sujet}\n"
|
|
texte += f"Contenu (à reproduire EXACTEMENT):\n{contenu}\n\n"
|
|
|
|
# Instructions finales très explicites
|
|
texte += "INSTRUCTIONS FINALES:\n"
|
|
texte += "1. Reproduis EXACTEMENT le contenu de chaque message sans le modifier ni l'interpréter\n"
|
|
texte += "2. Conserve les termes techniques exacts (ex: 'essai au bleu')\n"
|
|
texte += "3. N'invente JAMAIS de messages ou d'éléments qui ne sont pas présents\n"
|
|
texte += "4. IMPORTANT: Le NOM et la DESCRIPTION du ticket sont le PREMIER MESSAGE du client\n"
|
|
texte += "5. Respecte l'ordre chronologique des messages\n"
|
|
texte += "6. Reste fidèle au texte original même s'il te paraît incomplet\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")
|