llm_ticket3/utils/ticket_analyzer.py
2025-04-02 09:01:55 +02:00

358 lines
14 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Utilitaire pour l'analyse de tickets de support utilisant les agents spécialisés.
"""
import os
import json
from datetime import datetime
from typing import Dict, List, Any, Optional
from agents import AgentFiltreImages, AgentAnalyseImage, AgentQuestionReponse
from post_process import normaliser_accents, corriger_markdown_accents
class TicketAnalyzer:
"""
Classe utilitaire pour analyser des tickets de support.
Coordonne le travail des différents agents spécialisés.
"""
def __init__(self, api_key: Optional[str] = None, llm_params: Optional[Dict[str, Any]] = None):
"""
Initialise l'analyseur de tickets.
Args:
api_key: Clé API pour les modèles LLM
llm_params: Paramètres globaux pour les LLM
"""
self.api_key = api_key
self.llm_params = llm_params or {}
# Initialisation des agents
self.agent_filtre = AgentFiltreImages(api_key=api_key)
self.agent_analyse = AgentAnalyseImage(api_key=api_key)
self.agent_qr = AgentQuestionReponse(api_key=api_key)
# Appliquer les paramètres globaux
self._appliquer_parametres_globaux()
# Journal d'analyse
self.entries = []
def _appliquer_parametres_globaux(self) -> None:
"""
Applique les paramètres globaux à tous les agents.
"""
if not self.llm_params:
return
print(f"Application des paramètres globaux LLM: {self.llm_params}")
self.agent_filtre.appliquer_parametres_globaux(self.llm_params)
self.agent_analyse.appliquer_parametres_globaux(self.llm_params)
self.agent_qr.appliquer_parametres_globaux(self.llm_params)
def filtrer_images(self, images_paths: List[str]) -> List[str]:
"""
Filtre les images pour ne conserver que celles pertinentes.
Args:
images_paths: Liste des chemins d'images à filtrer
Returns:
Liste des chemins d'images pertinentes
"""
images_pertinentes = []
for image_path in images_paths:
# Enregistrer le début de l'action
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
entry = {
"timestamp": timestamp,
"action": "filter_image",
"agent": self.agent_filtre.nom,
"llm": {"model": self.agent_filtre.llm.model},
"parametres_llm": self.agent_filtre.obtenir_parametres_llm(),
"image_path": image_path
}
# Exécuter le filtrage d'image
resultat = self.agent_filtre.executer(image_path)
# Ajouter le résultat à l'entrée
entry["response"] = resultat
# Ajouter au journal
self.entries.append(entry)
# Conserver l'image si pertinente
if resultat.get("pertinente", False):
images_pertinentes.append(image_path)
return images_pertinentes
def analyser_images(self, images_paths: List[str], contexte_ticket: Optional[str] = None) -> List[Dict[str, Any]]:
"""
Analyse les images pertinentes en détail.
Args:
images_paths: Liste des chemins d'images à analyser
contexte_ticket: Contexte du ticket pour une analyse plus pertinente
Returns:
Liste des résultats d'analyse
"""
resultats_analyses = []
for image_path in images_paths:
# Enregistrer le début de l'action
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
entry = {
"timestamp": timestamp,
"action": "analyze_image",
"agent": self.agent_analyse.nom,
"llm": {"model": self.agent_analyse.llm.model},
"parametres_llm": self.agent_analyse.obtenir_parametres_llm(),
"image_path": image_path
}
# Exécuter l'analyse d'image
resultat = self.agent_analyse.executer(image_path, contexte_ticket)
# Ajouter le résultat à l'entrée
entry["response"] = resultat.get("content", "")
if "usage" in resultat:
entry["tokens"] = resultat["usage"]
# Ajouter au journal
self.entries.append(entry)
# Ajouter aux résultats
if "error" not in resultat:
resultats_analyses.append({
"image_path": image_path,
"analyse": resultat.get("content", ""),
"usage": resultat.get("usage", {}),
"parametres_llm": self.agent_analyse.generer_rapport_parametres()
})
return resultats_analyses
def extraire_questions_reponses(self, messages: List[Dict[str, Any]], output_path: Optional[str] = None) -> Dict[str, Any]:
"""
Extrait les questions et réponses des messages du ticket.
Args:
messages: Liste des messages du ticket
output_path: Chemin où sauvegarder le tableau Markdown (optionnel)
Returns:
Résultat de l'extraction avec tableau Markdown
"""
# Enregistrer le début de l'action
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
entry = {
"timestamp": timestamp,
"action": "extract_questions_reponses",
"agent": self.agent_qr.nom,
"llm": {"model": self.agent_qr.llm.model},
"parametres_llm": self.agent_qr.obtenir_parametres_llm()
}
# Exécuter l'extraction des questions/réponses
resultat = self.agent_qr.executer(messages, output_path)
# Ajouter le résultat à l'entrée
entry["response"] = resultat
# Ajouter au journal
self.entries.append(entry)
return resultat
def generer_rapport(self, output_path: str) -> Dict[str, str]:
"""
Génère un rapport complet d'analyse au format JSON et Markdown.
Args:
output_path: Chemin où sauvegarder le rapport
Returns:
Dictionnaire avec les chemins des fichiers générés
"""
# Créer le répertoire de sortie si nécessaire
os.makedirs(output_path, exist_ok=True)
# Chemins des fichiers de sortie
json_path = os.path.join(output_path, "ticket_analysis.json")
md_path = os.path.join(output_path, "ticket_analysis.md")
# Sauvegarder au format JSON
with open(json_path, "w", encoding="utf-8") as f:
json.dump({"entries": self.entries}, f, indent=2, ensure_ascii=False)
# Générer le contenu Markdown
md_content = self._generer_markdown()
# Normaliser les accents dans le contenu Markdown avant de l'écrire
md_content = normaliser_accents(md_content)
# Sauvegarder au format Markdown
with open(md_path, "w", encoding="utf-8") as f:
f.write(md_content)
return {
"json": json_path,
"markdown": md_path
}
def _generer_markdown(self) -> str:
"""
Génère le contenu Markdown à partir des entrées du journal.
Returns:
Contenu Markdown formaté
"""
contenu = ["# Analyse de ticket de support\n"]
# Statistiques
stats = self._calculer_statistiques()
contenu.append("## Statistiques\n")
contenu.append(f"- Images analysées: {stats['images_total']} ({stats['images_pertinentes']} pertinentes)")
contenu.append(f"- Questions identifiées: {stats['questions']}")
contenu.append(f"- Réponses identifiées: {stats['reponses']}")
contenu.append("\n")
# Paramètres LLM globaux
if self.llm_params:
contenu.append("## Paramètres LLM globaux\n")
for param, valeur in self.llm_params.items():
contenu.append(f"- **{param}**: {valeur}")
contenu.append("\n")
# Paramètres LLM utilisés
contenu.append("## Paramètres LLM par agent\n")
# Filtre d'images
agent_filtre_params = self.agent_filtre.obtenir_parametres_llm()
contenu.append("### Agent de filtrage d'images\n")
contenu.append(f"- **Type de LLM**: {self.agent_filtre.llm.__class__.__name__}")
contenu.append(f"- **Modèle**: {agent_filtre_params.get('model', 'Non spécifié')}")
contenu.append(f"- **Température**: {agent_filtre_params.get('temperature', 'Non spécifiée')}")
contenu.append(f"- **Tokens max**: {agent_filtre_params.get('max_tokens', 'Non spécifié')}")
# Analyse d'images
agent_analyse_params = self.agent_analyse.obtenir_parametres_llm()
contenu.append("\n### Agent d'analyse d'images\n")
contenu.append(f"- **Type de LLM**: {self.agent_analyse.llm.__class__.__name__}")
contenu.append(f"- **Modèle**: {agent_analyse_params.get('model', 'Non spécifié')}")
contenu.append(f"- **Température**: {agent_analyse_params.get('temperature', 'Non spécifiée')}")
contenu.append(f"- **Tokens max**: {agent_analyse_params.get('max_tokens', 'Non spécifié')}")
# Questions-réponses
agent_qr_params = self.agent_qr.obtenir_parametres_llm()
contenu.append("\n### Agent d'extraction questions-réponses\n")
contenu.append(f"- **Type de LLM**: {self.agent_qr.llm.__class__.__name__}")
contenu.append(f"- **Modèle**: {agent_qr_params.get('model', 'Non spécifié')}")
contenu.append(f"- **Température**: {agent_qr_params.get('temperature', 'Non spécifiée')}")
contenu.append(f"- **Tokens max**: {agent_qr_params.get('max_tokens', 'Non spécifié')}")
contenu.append("\n")
# Actions chronologiques
contenu.append("## Journal d'actions\n")
for entry in self.entries:
timestamp = entry.get("timestamp", "")
action = entry.get("action", "")
agent = entry.get("agent", "")
model = entry.get("llm", {}).get("model", "")
# En-tête de l'action
contenu.append(f"### {timestamp} - {agent} ({model})")
contenu.append(f"**Action**: {action}")
# Détails spécifiques selon le type d'action
if action == "filter_image":
image_path = entry.get("image_path", "")
response = entry.get("response", {})
pertinente = response.get("pertinente", False)
type_image = response.get("type_image", "inconnue")
description = response.get("description", "")
contenu.append(f"**Image**: {os.path.basename(image_path)}")
contenu.append(f"**Résultat**: {'Pertinente' if pertinente else 'Non pertinente'}")
contenu.append(f"**Type**: {type_image}")
contenu.append(f"**Description**: {description}")
# Paramètres LLM utilisés
params_llm = response.get("parametres_llm", {})
if params_llm:
contenu.append("\n**Paramètres LLM utilisés:**")
temp = params_llm.get("parametres", {}).get("temperature", "N/A")
contenu.append(f"- Température: {temp}")
elif action == "analyze_image":
image_path = entry.get("image_path", "")
response = entry.get("response", "")
contenu.append(f"**Image analysée**: {os.path.basename(image_path)}")
contenu.append("\n**Analyse**:")
contenu.append(f"```\n{response}\n```")
elif action == "extract_questions_reponses":
response = entry.get("response", {})
tableau_md = response.get("tableau_md", "")
contenu.append(f"**Questions**: {response.get('nb_questions', 0)}")
contenu.append(f"**Réponses**: {response.get('nb_reponses', 0)}")
contenu.append("\n")
contenu.append(tableau_md)
# Paramètres LLM spécifiques à cette action
params_llm = entry.get("parametres_llm", {})
if params_llm and action != "filter_image": # Pour éviter la duplication avec le filtre d'image
contenu.append("\n**Paramètres LLM utilisés:**")
for key, value in params_llm.items():
if key != "system_prompt": # Éviter d'afficher le prompt système ici
contenu.append(f"- **{key}**: {value}")
# Tokens utilisés
if "tokens" in entry:
tokens = entry["tokens"]
contenu.append("\n**Tokens utilisés**:")
contenu.append(f"- Prompt: {tokens.get('prompt_tokens', 0)}")
contenu.append(f"- Completion: {tokens.get('completion_tokens', 0)}")
contenu.append(f"- Total: {tokens.get('total_tokens', 0)}")
contenu.append("\n---\n")
return "\n".join(contenu)
def _calculer_statistiques(self) -> Dict[str, int]:
"""
Calcule les statistiques à partir des entrées du journal.
Returns:
Dictionnaire de statistiques
"""
stats = {
"images_total": 0,
"images_pertinentes": 0,
"questions": 0,
"reponses": 0
}
for entry in self.entries:
action = entry.get("action", "")
if action == "filter_image":
stats["images_total"] += 1
if entry.get("response", {}).get("pertinente", False):
stats["images_pertinentes"] += 1
elif action == "extract_questions_reponses":
stats["questions"] = entry.get("response", {}).get("nb_questions", 0)
stats["reponses"] = entry.get("response", {}).get("nb_reponses", 0)
return stats