1404-15:34

This commit is contained in:
Ladebeze66 2025-04-14 15:34:51 +02:00
parent b190e772c7
commit 84332beb49
43 changed files with 8471 additions and 2393 deletions

File diff suppressed because it is too large Load Diff

16
agents/__init__.py Normal file
View File

@ -0,0 +1,16 @@
"""
Package agents contenant tous les agents d'analyse utilisés pour le traitement des tickets.
"""
# Mappage des modèles aux répertoires d'agents
MODEL_DIRECTORIES = {
"mistral-medium": "mistral_medium",
"mistral-large": "mistral_large",
"pixtral12b": "pixtral12b",
"pixtral-large": "pixtral_large",
"llama-vision3-2": "llama_vision3_2",
"qwen2-5": "qwen2_5",
"deepseek": "deepseek",
}
# Pas d'importations automatiques ici - chaque module doit faire ses imports explicitement

View File

@ -0,0 +1,3 @@
"""
Package agents.llama_vision3_2 contenant les agents utilisant le modèle Llama Vision 3.2.
"""

View File

@ -1,15 +1,15 @@
import json
import os
from .base_agent import BaseAgent
from agents.utils.base_agent import BaseAgent
from datetime import datetime
from typing import Dict, Any, Tuple, Optional, List
import logging
import traceback
import re
import sys
from .utils.report_utils import extraire_et_traiter_json
from .utils.report_formatter import extraire_sections_texte, generer_rapport_markdown, construire_rapport_json
from .utils.agent_info_collector import collecter_info_agents, collecter_prompts_agents
from agents.utils.report_utils import extraire_et_traiter_json
from agents.utils.report_formatter import extraire_sections_texte, generer_rapport_markdown, construire_rapport_json
from agents.utils.agent_info_collector import collecter_info_agents, collecter_prompts_agents
logger = logging.getLogger("AgentReportGenerator")
@ -18,7 +18,7 @@ class AgentReportGenerator(BaseAgent):
Agent pour générer un rapport synthétique à partir des analyses de ticket et d'images.
"""
def __init__(self, llm):
super().__init__("AgentReportGenerator", llm)
super().__init__(llm)
# Configuration locale de l'agent
self.temperature = 0.2

View File

@ -1,6 +1,6 @@
import json
import os
from .base_agent import BaseAgent
from .utils.base_agent import BaseAgent
from datetime import datetime
from typing import Dict, Any, Tuple, Optional, List
import logging

View File

@ -107,12 +107,13 @@ Ton analyse doit être exhaustive tout en restant factuelle. Focalise-toi sur l'
ticket_prompt = self._construire_prompt_ticket(ticket_summary, messages)
# Analyser le ticket avec le LLM
analyse = self.llm.generate(
system_prompt=self.system_prompt,
prompt=ticket_prompt,
temperature=self.temperature,
top_p=self.top_p,
max_tokens=self.max_tokens
# Configurer le prompt système
if hasattr(self.llm, "prompt_system"):
self.llm.prompt_system = self.system_prompt
# Analyser le ticket avec le LLM en utilisant interroger() au lieu de generate()
analyse = self.llm.interroger(
ticket_prompt
)
# Calcul du temps de génération

View File

@ -0,0 +1,179 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import json
import time
import logging
from typing import Dict, Any, Optional
# Importer BaseAgent depuis le répertoire utils
from agents.utils.base_agent import BaseAgent
# Configuration du logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger("AgentTicketAnalyser_MistralLarge")
class AgentTicketAnalyser(BaseAgent):
"""
Agent spécialisé pour analyser un ticket et en extraire les informations clés.
Version optimisée pour Mistral Large.
"""
def __init__(self, llm: Any):
"""
Initialise l'agent d'analyse de ticket avec un modèle LLM.
Args:
llm: Instance du modèle de langage à utiliser
"""
super().__init__(llm)
self.temperature = 0.1 # Température très basse pour une analyse très factuelle
self.top_p = 0.9
self.max_tokens = 3000
# System prompt spécifique pour l'analyse de tickets
self.system_prompt = """Tu es un expert en analyse de tickets pour le support informatique.
Ton rôle est d'analyser en profondeur le contenu du ticket pour extraire toutes les informations essentielles et de structurer cette analyse de manière exhaustive.
À partir du ticket fourni, tu dois :
1. Extraire et synthétiser tous les points clés du ticket
2. Identifier les informations techniques importantes et leurs implications
3. Comprendre en détail le problème principal, son contexte, et ses ramifications
4. Déterminer la pertinence des images pour comprendre le problème
5. Anticiper les questions de clarification potentielles
Ta réponse doit suivre un format strictement structuré :
```
1. Résumé du contexte
- Client : [Nom, fonction et contacts du client si mentionnés]
- Sujet du ticket : [Sujet principal du ticket analysé en détail]
- Description technique synthétique : [Synthèse technique approfondie du problème]
2. Informations techniques détectées
- Logiciels/modules mentionnés : [Liste complète des logiciels, applications ou modules mentionnés]
- Paramètres évoqués : [Tous les paramètres, configurations ou variables mentionnés]
- Fonctionnalités impactées : [Description détaillée des fonctionnalités touchées]
- Conditions spécifiques : [Analyse des conditions particulières le problème se manifeste]
3. Analyse du problème
- Problème principal : [Description approfondie du problème principal]
- Impact pour l'utilisateur : [Évaluation complète de l'impact sur l'utilisateur]
- Contexte d'apparition : [Analyse détaillée des circonstances d'apparition]
- Complexité estimée : [FAIBLE/MOYENNE/ÉLEVÉE] avec justification technique
4. Pertinence des images
- Images mentionnées : [OUI/NON] et leur importance [FAIBLE/MOYENNE/ÉLEVÉE]
- Justification : [Analyse détaillée de la pertinence des images]
- Éléments à rechercher : [Points spécifiques à examiner dans les images]
5. Questions pour clarification (si nécessaire)
- [Questions techniques que le support devrait poser]
- [Informations manquantes à obtenir]
```
Ton analyse doit être exhaustive tout en restant factuelle. Focalise-toi sur l'extraction complète des informations sans proposer de solutions."""
def executer(self, ticket_data: Dict[str, Any]) -> str:
"""
Analyse un ticket à partir des données fournies.
Args:
ticket_data: Dictionnaire contenant les données du ticket
- 'ticket_summary': Résumé du ticket
- 'messages': Liste des messages du ticket
Returns:
str: Analyse structurée du ticket
"""
start_time = time.time()
try:
# Extraire le résumé du ticket
ticket_summary = ticket_data.get('ticket_summary', {})
ticket_code = ticket_summary.get('code', 'INCONNU')
ticket_sujet = ticket_summary.get('sujet', 'Sans sujet')
# Log des informations de base du ticket
logger.info(f"Analyse du ticket {ticket_code}: {ticket_sujet}")
print(f" Analyse du ticket {ticket_code}: {ticket_sujet[:80]}{'...' if len(ticket_sujet) > 80 else ''}")
# Construire le contexte à partir des messages
messages = ticket_data.get('messages', [])
# Construire un prompt pour analyser le ticket
ticket_prompt = self._construire_prompt_ticket(ticket_summary, messages)
# Analyser le ticket avec le LLM
analyse = self.llm.generate(
system_prompt=self.system_prompt,
prompt=ticket_prompt,
temperature=self.temperature,
top_p=self.top_p,
max_tokens=self.max_tokens
)
# Calcul du temps de génération
generation_time = time.time() - start_time
# Log de l'analyse complétée
logger.info(f"Analyse complétée en {generation_time:.2f} secondes")
print(f" Analyse complétée en {generation_time:.2f} secondes")
return analyse
except Exception as e:
logger.error(f"Erreur lors de l'analyse du ticket: {str(e)}")
return f"ERREUR: Impossible d'analyser le ticket: {str(e)}"
def _construire_prompt_ticket(self, ticket_summary: Dict[str, Any], messages: list) -> str:
"""
Construit un prompt détaillé à partir des données du ticket.
Args:
ticket_summary: Résumé du ticket
messages: Liste des messages du ticket
Returns:
str: Prompt détaillé pour l'analyse
"""
# Construire l'en-tête avec les informations du ticket
prompt = f"# TICKET: {ticket_summary.get('code', 'INCONNU')}\n"
prompt += f"Sujet: {ticket_summary.get('sujet', 'Sans sujet')}\n"
prompt += f"Statut: {ticket_summary.get('statut', 'Inconnu')}\n"
prompt += f"Priorité: {ticket_summary.get('priorité', 'Non définie')}\n"
prompt += f"Date de création: {ticket_summary.get('date_création', 'Inconnue')}\n\n"
# Ajouter la section des messages
prompt += "## MESSAGES DU TICKET\n\n"
for i, message in enumerate(messages, 1):
# Extraire les informations du message
date = message.get('date', 'Date inconnue')
auteur = message.get('auteur', 'Auteur inconnu')
role = message.get('role', 'Rôle inconnu')
contenu = message.get('contenu', '')
# Ajouter l'en-tête du message
prompt += f"### Message {i} - {date}\n"
prompt += f"De: {auteur} ({role})\n\n"
# Ajouter le contenu du message
prompt += f"{contenu}\n\n"
# Ajouter les informations sur les pièces jointes si disponibles
attachments = message.get('attachments', [])
if attachments:
prompt += f"Pièces jointes ({len(attachments)}):\n"
for attachment in attachments:
prompt += f"- {attachment.get('nom', 'Sans nom')} ({attachment.get('type', 'Type inconnu')})\n"
prompt += "\n"
# Ajouter les instructions d'analyse
prompt += "\n## INSTRUCTIONS\n"
prompt += "Effectue une analyse approfondie de ce ticket selon les instructions dans ton system prompt. "
prompt += "Fournis une analyse complète et structurée qui servira de base aux étapes suivantes du traitement."
return prompt

View File

@ -0,0 +1,3 @@
"""
Package agents.mistral_medium contenant les agents utilisant le modèle Mistral Medium.
"""

View File

@ -1,15 +1,14 @@
import json
import os
from .base_agent import BaseAgent
from datetime import datetime
from typing import Dict, Any, Tuple, Optional, List
import sys
import logging
import traceback
import re
import sys
from .utils.report_utils import extraire_et_traiter_json
from .utils.report_formatter import extraire_sections_texte, generer_rapport_markdown, construire_rapport_json
from .utils.agent_info_collector import collecter_info_agents, collecter_prompts_agents
from datetime import datetime
from typing import Dict, Any, Tuple, Optional, List
# Un seul import pour BaseAgent
from agents.utils.base_agent import BaseAgent
logger = logging.getLogger("AgentReportGenerator")
@ -18,7 +17,7 @@ class AgentReportGenerator(BaseAgent):
Agent pour générer un rapport synthétique à partir des analyses de ticket et d'images.
"""
def __init__(self, llm):
super().__init__("AgentReportGenerator", llm)
super().__init__(llm)
# Configuration locale de l'agent
self.temperature = 0.2
@ -117,7 +116,7 @@ STRUCTURE OBLIGATOIRE ET ORDRE À SUIVRE:
MÉTHODE POUR ANALYSER LES IMAGES:
- Pour chaque image, concentre-toi prioritairement sur:
* Les éléments mis en évidence (zones encadrées, surlignées)
* Les éléments mis en évidence (zones encadrées, surlignés)
* La relation avec le problème décrit
* Le lien avec le fil de discussion
@ -161,6 +160,81 @@ DIRECTIVES ESSENTIELLES:
Génère un rapport à partir des analyses effectuées
"""
try:
# Importer les fonctions utilitaires localement pour éviter les problèmes d'import circulaires
try:
from agents.utils.report_utils import extraire_et_traiter_json
from agents.utils.report_formatter import extraire_sections_texte, generer_rapport_markdown, construire_rapport_json
from agents.utils.agent_info_collector import collecter_info_agents, collecter_prompts_agents
except ImportError as e:
logger.warning(f"Impossible d'importer les modules utils: {e}")
# Fonctions de remplacement simplifiées en cas d'échec d'import
def extraire_et_traiter_json(texte):
# Version simplifiée: extrait juste le bloc JSON des échanges
echanges = {"chronologie_echanges": []}
json_match = re.search(r'```json\s*({[^`]*})\s*```', texte, re.DOTALL)
if json_match:
try:
json_data = json.loads(json_match.group(1))
if "chronologie_echanges" in json_data:
echanges = json_data
except:
pass
return texte, echanges, None
def extraire_sections_texte(texte):
# Extraire les sections de base
resume = "Résumé non disponible"
resume_match = re.search(r'## Résumé du problème\s*\n(.*?)(?=##)', texte, re.DOTALL)
if resume_match:
resume = resume_match.group(1).strip()
analyse = "Analyse non disponible"
analyse_match = re.search(r'## Analyse des images\s*\n(.*?)(?=##)', texte, re.DOTALL)
if analyse_match:
analyse = analyse_match.group(1).strip()
diagnostic = "Diagnostic non disponible"
diagnostic_match = re.search(r'## Diagnostic technique\s*\n(.*?)(?=$)', texte, re.DOTALL)
if diagnostic_match:
diagnostic = diagnostic_match.group(1).strip()
return resume, analyse, diagnostic
def generer_rapport_markdown(json_path):
# Version simplifiée: juste créer un fichier MD basique
md_path = json_path.replace(".json", ".md")
try:
with open(json_path, 'r', encoding='utf-8') as f:
data = json.load(f)
with open(md_path, 'w', encoding='utf-8') as f:
f.write(f"# Rapport d'analyse: {data.get('ticket_id', 'Ticket')}\n\n")
if "resume" in data:
f.write(f"## Résumé du problème\n{data['resume']}\n\n")
if "contenu_brut" in data:
f.write(data["contenu_brut"])
return md_path
except Exception as e:
logger.error(f"Erreur génération MD: {e}")
return None
def construire_rapport_json(**kwargs):
# Version simplifiée
return {
"ticket_id": kwargs.get("ticket_id", ""),
"resume": kwargs.get("resume", ""),
"contenu_brut": kwargs.get("rapport_genere", ""),
"echanges": kwargs.get("echanges_json", {"chronologie_echanges": []}),
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
def collecter_info_agents(data, agent_info):
return {"report_generator": agent_info}
def collecter_prompts_agents(system_prompt):
return {"report_generator": system_prompt[:100] + "..."}
# 1. PRÉPARATION
ticket_id = self._extraire_ticket_id(rapport_data, rapport_dir)
logger.info(f"Génération du rapport pour le ticket: {ticket_id}")

View File

@ -1,340 +0,0 @@
from .base_agent import BaseAgent
from typing import Any, Dict
import logging
import os
from PIL import Image
import base64
import io
logger = logging.getLogger("AgentImageAnalyser")
class AgentImageAnalyser(BaseAgent):
"""
Agent pour analyser les images et extraire les informations pertinentes.
"""
def __init__(self, llm):
super().__init__("AgentImageAnalyser", llm)
# Configuration locale de l'agent
self.temperature = 0.2
self.top_p = 0.9
self.max_tokens = 3000
# Centralisation des instructions d'analyse pour éviter la duplication
self.instructions_analyse = """
1. Description objective
Décris précisément ce que montre l'image :
- Interface logicielle, menus, fenêtres, onglets
- Messages d'erreur, messages système, code ou script
- Nom ou titre du logiciel ou du module si visible
2. Éléments techniques clés
Identifie :
- Versions logicielles ou modules affichés
- Codes d'erreur visibles
- Paramètres configurables (champs de texte, sliders, dropdowns, cases à cocher)
- Valeurs affichées ou préremplies dans les champs
- Éléments désactivés, grisés ou masqués (souvent non modifiables)
- Boutons actifs/inactifs
3. Éléments mis en évidence
- Recherche les zones entourées, encadrées, surlignées ou fléchées
- Ces éléments sont souvent importants pour le client ou le support
- Mentionne explicitement leur contenu et leur style de mise en valeur
4. Relation avec le problème
- Établis le lien entre les éléments visibles et le problème décrit dans le ticket
- Indique si des composants semblent liés à une mauvaise configuration ou une erreur
5. Réponses potentielles
- Détermine si l'image apporte des éléments de réponse à une question posée dans :
- Le titre du ticket
- La description du problème
6. Lien avec la discussion
- Vérifie si l'image fait écho à une étape décrite dans le fil de discussion
- Note les correspondances (ex: même module, même message d'erreur que précédemment mentionné)
Règles importantes :
- Ne fais AUCUNE interprétation ni diagnostic
- Ne propose PAS de solution ou recommandation
- Reste strictement factuel et objectif
- Concentre-toi uniquement sur ce qui est visible dans l'image
- Reproduis les textes exacts(ex : messages d'erreur, libellés de paramètres)
- Prête une attention particulière aux éléments modifiables (interactifs) et non modifiables (grisés)
"""
# Prompt système construit à partir des instructions centralisées
self.system_prompt = f"""Tu es un expert en analyse d'images pour le support technique de BRG-Lab pour la société CBAO.
Ta mission est d'analyser des captures d'écran en lien avec le contexte du ticket de support.
Structure ton analyse d'image de façon factuelle:
{self.instructions_analyse}
Ton analyse sera utilisée comme élément factuel pour un rapport technique plus complet."""
# Appliquer la configuration au LLM
self._appliquer_config_locale()
logger.info("AgentImageAnalyser initialisé")
def _appliquer_config_locale(self) -> None:
"""
Applique la configuration locale au modèle LLM.
"""
# Appliquer le prompt système
if hasattr(self.llm, "prompt_system"):
self.llm.prompt_system = self.system_prompt
# Appliquer les paramètres
if hasattr(self.llm, "configurer"):
params = {
"temperature": self.temperature,
"top_p": self.top_p,
"max_tokens": self.max_tokens
}
self.llm.configurer(**params)
def _verifier_image(self, image_path: str) -> bool:
"""
Vérifie si l'image existe et est accessible
Args:
image_path: Chemin vers l'image
Returns:
True si l'image existe et est accessible, False sinon
"""
try:
# Vérifier que le fichier existe
if not os.path.exists(image_path):
logger.error(f"L'image n'existe pas: {image_path}")
return False
# Vérifier que le fichier est accessible en lecture
if not os.access(image_path, os.R_OK):
logger.error(f"L'image n'est pas accessible en lecture: {image_path}")
return False
# Vérifier que le fichier peut être ouvert comme une image
with Image.open(image_path) as img:
# Vérifier les dimensions de l'image
width, height = img.size
if width <= 0 or height <= 0:
logger.error(f"Dimensions d'image invalides: {width}x{height}")
return False
logger.info(f"Image vérifiée avec succès: {image_path} ({width}x{height})")
return True
except Exception as e:
logger.error(f"Erreur lors de la vérification de l'image {image_path}: {str(e)}")
return False
def _encoder_image_base64(self, image_path: str) -> str:
"""
Encode l'image en base64 pour l'inclure directement dans le prompt
Args:
image_path: Chemin vers l'image
Returns:
Chaîne de caractères au format data URI avec l'image encodée en base64
"""
try:
# Ouvrir l'image et la redimensionner si trop grande
with Image.open(image_path) as img:
# Redimensionner l'image si elle est trop grande (max 800x800)
max_size = 800
if img.width > max_size or img.height > max_size:
img.thumbnail((max_size, max_size), Image.Resampling.LANCZOS)
# Convertir en RGB si nécessaire (pour les formats comme PNG)
if img.mode != "RGB":
img = img.convert("RGB")
# Sauvegarder l'image en JPEG dans un buffer mémoire
buffer = io.BytesIO()
img.save(buffer, format="JPEG", quality=85)
buffer.seek(0)
# Encoder en base64
img_base64 = base64.b64encode(buffer.read()).decode("utf-8")
# Construire le data URI
data_uri = f"data:image/jpeg;base64,{img_base64}"
return data_uri
except Exception as e:
logger.error(f"Erreur lors de l'encodage de l'image {image_path}: {str(e)}")
return ""
def _generer_prompt_analyse(self, contexte: str, prefix: str = "") -> str:
"""
Génère le prompt d'analyse d'image en utilisant les instructions centralisées
Args:
contexte: Contexte du ticket à inclure dans le prompt
prefix: Préfixe optionnel (pour inclure l'image en base64 par exemple)
Returns:
Prompt formaté pour l'analyse d'image
"""
return f"""{prefix}
CONTEXTE DU TICKET:
{contexte}
Fournis une analyse STRICTEMENT FACTUELLE de l'image avec les sections suivantes:
{self.instructions_analyse}"""
def executer(self, image_path: str, contexte: str) -> Dict[str, Any]:
"""
Analyse une image en tenant compte du contexte du ticket
Args:
image_path: Chemin vers l'image à analyser
contexte: Contexte du ticket (résultat de l'analyse JSON)
Returns:
Dictionnaire contenant l'analyse détaillée de l'image et les métadonnées d'exécution
"""
image_name = os.path.basename(image_path)
logger.info(f"Analyse de l'image: {image_name} avec contexte")
print(f" AgentImageAnalyser: Analyse de {image_name}")
# Vérifier que l'image existe et est accessible
if not self._verifier_image(image_path):
error_message = f"L'image n'est pas accessible ou n'est pas valide: {image_name}"
logger.error(error_message)
print(f" ERREUR: {error_message}")
return {
"analyse": f"ERREUR: {error_message}. Veuillez vérifier que l'image existe et est valide.",
"error": True,
"metadata": {
"image_path": image_path,
"image_name": image_name,
"timestamp": self._get_timestamp(),
"error": True
}
}
# Générer le prompt d'analyse avec les instructions centralisées
prompt = self._generer_prompt_analyse(contexte, "Analyse cette image en tenant compte du contexte suivant:")
try:
logger.info("Envoi de la requête au LLM")
# Utiliser la méthode interroger_avec_image au lieu de interroger
if hasattr(self.llm, "interroger_avec_image"):
logger.info(f"Utilisation de la méthode interroger_avec_image pour {image_name}")
response = self.llm.interroger_avec_image(image_path, prompt)
else:
# Fallback vers la méthode standard avec base64 si interroger_avec_image n'existe pas
logger.warning(f"La méthode interroger_avec_image n'existe pas, utilisation du fallback pour {image_name}")
img_base64 = self._encoder_image_base64(image_path)
if img_base64:
# Utiliser le même générateur de prompt avec l'image en base64
prompt_base64 = self._generer_prompt_analyse(contexte, f"Analyse cette image:\n{img_base64}")
response = self.llm.interroger(prompt_base64)
else:
error_message = "Impossible d'encoder l'image en base64"
logger.error(f"Erreur d'analyse pour {image_name}: {error_message}")
print(f" ERREUR: {error_message}")
# Retourner un résultat d'erreur explicite
return {
"analyse": f"ERREUR: {error_message}. Veuillez vérifier que l'image est dans un format standard.",
"error": True,
"raw_response": "",
"metadata": {
"image_path": image_path,
"image_name": image_name,
"timestamp": self._get_timestamp(),
"error": True
}
}
# Vérifier si la réponse contient des indications que le modèle ne peut pas analyser l'image
error_phrases = [
"je ne peux pas directement visualiser",
"je n'ai pas accès à l'image",
"je ne peux pas voir l'image",
"sans accès direct à l'image",
"je n'ai pas la possibilité de voir",
"je ne peux pas accéder directement",
"erreur: impossible d'analyser l'image"
]
# Vérifier si une des phrases d'erreur est présente dans la réponse
if any(phrase in response.lower() for phrase in error_phrases):
logger.warning(f"Le modèle indique qu'il ne peut pas analyser l'image: {image_name}")
error_message = "Le modèle n'a pas pu analyser l'image correctement"
logger.error(f"Erreur d'analyse pour {image_name}: {error_message}")
print(f" ERREUR: {error_message}")
# Retourner un résultat d'erreur explicite
return {
"analyse": f"ERREUR: {error_message}. Veuillez vérifier que le modèle a accès à l'image ou utiliser un modèle différent.",
"error": True,
"raw_response": response,
"metadata": {
"image_path": image_path,
"image_name": image_name,
"timestamp": self._get_timestamp(),
"error": True
}
}
logger.info(f"Réponse reçue pour l'image {image_name}: {response[:100]}...")
# Créer un dictionnaire de résultat avec l'analyse et les métadonnées
result = {
"analyse": response,
"metadata": {
"image_path": image_path,
"image_name": image_name,
"timestamp": self._get_timestamp(),
"model_info": {
"model": getattr(self.llm, "modele", str(type(self.llm))),
"temperature": self.temperature,
"top_p": self.top_p,
"max_tokens": self.max_tokens
}
}
}
# Enregistrer l'analyse dans l'historique avec contexte et prompt
self.ajouter_historique("analyse_image",
{
"image_path": image_path,
"contexte": contexte,
"prompt": prompt
},
response)
return result
except Exception as e:
error_message = f"Erreur lors de l'analyse de l'image: {str(e)}"
logger.error(error_message)
print(f" ERREUR: {error_message}")
# Retourner un résultat par défaut en cas d'erreur
return {
"analyse": f"ERREUR: {error_message}",
"error": True,
"metadata": {
"image_path": image_path,
"image_name": image_name,
"timestamp": self._get_timestamp(),
"error": True
}
}
def _get_timestamp(self) -> str:
"""Retourne un timestamp au format YYYYMMDD_HHMMSS"""
from datetime import datetime
return datetime.now().strftime("%Y%m%d_%H%M%S")

View File

@ -1,393 +0,0 @@
from .base_agent import BaseAgent
import logging
import os
from typing import Dict, Any, Tuple
from PIL import Image
import base64
import io
logger = logging.getLogger("AgentImageSorter")
class AgentImageSorter(BaseAgent):
"""
Agent pour trier les images et identifier celles qui sont pertinentes.
"""
def __init__(self, llm):
super().__init__("AgentImageSorter", llm)
# Configuration locale de l'agent
self.temperature = 0.2
self.top_p = 0.8
self.max_tokens = 300
# Centralisation des critères de pertinence
self.criteres_pertinence = """
Images PERTINENTES (réponds "oui" ou "pertinent"):
- Captures d'écran de logiciels ou d'interfaces
- logo BRG_LAB
- Référence à "logociel"
- Messages d'erreur
- Configurations système
- Tableaux de bord ou graphiques techniques
- Fenêtres de diagnostic
Images NON PERTINENTES (réponds "non" ou "non pertinent"):
- Photos personnelles
- Images marketing/promotionnelles
- Logos ou images de marque
- Paysages, personnes ou objets non liés à l'informatique
"""
# Centralisation des instructions d'analyse
self.instructions_analyse = """
IMPORTANT: Ne commence JAMAIS ta réponse par "Je ne peux pas directement visualiser l'image".
Si tu ne peux pas analyser l'image, réponds simplement "ERREUR: Impossible d'analyser l'image".
Analyse d'abord ce que montre l'image, puis réponds par "oui"/"pertinent" ou "non"/"non pertinent".
"""
# Construction du système prompt à partir des éléments centralisés
self.system_prompt = f"""Tu es un expert en tri d'images pour le support technique de BRG_Lab pour la société CBAO.
Ta mission est de déterminer si une image est pertinente pour le support technique de logiciels.
{self.criteres_pertinence}
{self.instructions_analyse}"""
# Appliquer la configuration au LLM
self._appliquer_config_locale()
logger.info("AgentImageSorter initialisé")
def _appliquer_config_locale(self) -> None:
"""
Applique la configuration locale au modèle LLM.
"""
# Appliquer le prompt système
if hasattr(self.llm, "prompt_system"):
self.llm.prompt_system = self.system_prompt
# Appliquer les paramètres
if hasattr(self.llm, "configurer"):
params = {
"temperature": self.temperature,
"top_p": self.top_p,
"max_tokens": self.max_tokens
}
self.llm.configurer(**params)
def _verifier_image(self, image_path: str) -> bool:
"""
Vérifie si l'image existe et est accessible
Args:
image_path: Chemin vers l'image
Returns:
True si l'image existe et est accessible, False sinon
"""
try:
# Vérifier que le fichier existe
if not os.path.exists(image_path):
logger.error(f"L'image n'existe pas: {image_path}")
return False
# Vérifier que le fichier est accessible en lecture
if not os.access(image_path, os.R_OK):
logger.error(f"L'image n'est pas accessible en lecture: {image_path}")
return False
# Vérifier que le fichier peut être ouvert comme une image
with Image.open(image_path) as img:
# Vérifier les dimensions de l'image
width, height = img.size
if width <= 0 or height <= 0:
logger.error(f"Dimensions d'image invalides: {width}x{height}")
return False
logger.info(f"Image vérifiée avec succès: {image_path} ({width}x{height})")
return True
except Exception as e:
logger.error(f"Erreur lors de la vérification de l'image {image_path}: {str(e)}")
return False
def _encoder_image_base64(self, image_path: str) -> str:
"""
Encode l'image en base64 pour l'inclure directement dans le prompt
Args:
image_path: Chemin vers l'image
Returns:
Chaîne de caractères au format data URI avec l'image encodée en base64
"""
try:
# Ouvrir l'image et la redimensionner si trop grande
with Image.open(image_path) as img:
# Redimensionner l'image si elle est trop grande (max 800x800)
max_size = 800
if img.width > max_size or img.height > max_size:
img.thumbnail((max_size, max_size), Image.Resampling.LANCZOS)
# Convertir en RGB si nécessaire (pour les formats comme PNG)
if img.mode != "RGB":
img = img.convert("RGB")
# Sauvegarder l'image en JPEG dans un buffer mémoire
buffer = io.BytesIO()
img.save(buffer, format="JPEG", quality=85)
buffer.seek(0)
# Encoder en base64
img_base64 = base64.b64encode(buffer.read()).decode("utf-8")
# Construire le data URI
data_uri = f"data:image/jpeg;base64,{img_base64}"
return data_uri
except Exception as e:
logger.error(f"Erreur lors de l'encodage de l'image {image_path}: {str(e)}")
return ""
def _generer_prompt_analyse(self, prefix: str = "", avec_image_base64: bool = False) -> str:
"""
Génère le prompt d'analyse standardisé
Args:
prefix: Préfixe optionnel (pour inclure l'image en base64 par exemple)
avec_image_base64: Indique si le prompt inclut déjà une image en base64
Returns:
Prompt formaté pour l'analyse
"""
return f"""{prefix}
Est-ce une image pertinente pour un ticket de support technique?
Réponds simplement par 'oui' ou 'non' suivi d'une brève explication."""
def executer(self, image_path: str) -> Dict[str, Any]:
"""
Évalue si une image est pertinente pour l'analyse d'un ticket technique
Args:
image_path: Chemin vers l'image à analyser
Returns:
Dictionnaire contenant la décision de pertinence, l'analyse et les métadonnées
"""
image_name = os.path.basename(image_path)
logger.info(f"Évaluation de la pertinence de l'image: {image_name}")
print(f" AgentImageSorter: Évaluation de {image_name}")
# Vérifier que l'image existe et est accessible
if not self._verifier_image(image_path):
error_message = f"L'image n'est pas accessible ou n'est pas valide: {image_name}"
logger.error(error_message)
print(f" ERREUR: {error_message}")
return {
"is_relevant": False,
"reason": f"Erreur d'accès: {error_message}",
"raw_response": "",
"error": True,
"metadata": {
"image_path": image_path,
"image_name": image_name,
"timestamp": self._get_timestamp(),
"error": True
}
}
# Utiliser une référence au fichier image que le modèle peut comprendre
try:
# Préparation du prompt standardisé
prompt = self._generer_prompt_analyse()
# Utiliser la méthode interroger_avec_image au lieu de interroger
if hasattr(self.llm, "interroger_avec_image"):
logger.info(f"Utilisation de la méthode interroger_avec_image pour {image_name}")
response = self.llm.interroger_avec_image(image_path, prompt)
else:
# Fallback vers la méthode standard avec base64 si interroger_avec_image n'existe pas
logger.warning(f"La méthode interroger_avec_image n'existe pas, utilisation du fallback pour {image_name}")
img_base64 = self._encoder_image_base64(image_path)
if img_base64:
prompt_base64 = self._generer_prompt_analyse(f"Analyse cette image:\n{img_base64}", True)
response = self.llm.interroger(prompt_base64)
else:
error_message = "Impossible d'encoder l'image en base64"
logger.error(f"Erreur d'analyse pour {image_name}: {error_message}")
print(f" ERREUR: {error_message}")
return {
"is_relevant": False,
"reason": f"Erreur d'analyse: {error_message}",
"raw_response": "",
"error": True,
"metadata": {
"image_path": image_path,
"image_name": image_name,
"timestamp": self._get_timestamp(),
"error": True
}
}
# Vérifier si la réponse contient des indications que le modèle ne peut pas analyser l'image
error_phrases = [
"je ne peux pas directement visualiser",
"je n'ai pas accès à l'image",
"je ne peux pas voir l'image",
"sans accès direct à l'image",
"je n'ai pas la possibilité de voir",
"je ne peux pas accéder directement",
"erreur: impossible d'analyser l'image"
]
# Vérifier si une des phrases d'erreur est présente dans la réponse
if any(phrase in response.lower() for phrase in error_phrases):
logger.warning(f"Le modèle indique qu'il ne peut pas analyser l'image: {image_name}")
error_message = "Le modèle n'a pas pu analyser l'image correctement"
logger.error(f"Erreur d'analyse pour {image_name}: {error_message}")
print(f" ERREUR: {error_message}")
# Retourner un résultat d'erreur explicite
return {
"is_relevant": False,
"reason": f"Erreur d'analyse: {error_message}",
"raw_response": response,
"error": True,
"metadata": {
"image_path": image_path,
"image_name": image_name,
"timestamp": self._get_timestamp(),
"error": True
}
}
# Analyse de la réponse pour déterminer la pertinence
is_relevant, reason = self._analyser_reponse(response)
logger.info(f"Image {image_name} considérée comme {'pertinente' if is_relevant else 'non pertinente'}")
print(f" Décision: Image {image_name} {'pertinente' if is_relevant else 'non pertinente'}")
# Préparer le résultat
result = {
"is_relevant": is_relevant,
"reason": reason,
"raw_response": response,
"metadata": {
"image_path": image_path,
"image_name": image_name,
"timestamp": self._get_timestamp(),
"model_info": {
"model": getattr(self.llm, "modele", str(type(self.llm))),
"temperature": self.temperature,
"top_p": self.top_p,
"max_tokens": self.max_tokens
}
}
}
# Enregistrer la décision et le raisonnement dans l'historique
self.ajouter_historique("tri_image",
{
"image_path": image_path,
"prompt": prompt
},
{
"response": response,
"is_relevant": is_relevant,
"reason": reason
})
return result
except Exception as e:
logger.error(f"Erreur lors de l'analyse de l'image {image_name}: {str(e)}")
print(f" ERREUR: Impossible d'analyser l'image {image_name}")
# Retourner un résultat par défaut en cas d'erreur
return {
"is_relevant": False, # Par défaut, considérer non pertinent en cas d'erreur
"reason": f"Erreur d'analyse: {str(e)}",
"raw_response": "",
"error": True,
"metadata": {
"image_path": image_path,
"image_name": image_name,
"timestamp": self._get_timestamp(),
"error": True
}
}
def _analyser_reponse(self, response: str) -> Tuple[bool, str]:
"""
Analyse la réponse du LLM pour déterminer la pertinence et extraire le raisonnement
Args:
response: Réponse brute du LLM
Returns:
Tuple (is_relevant, reason) contenant la décision et le raisonnement
"""
# Convertir en minuscule pour faciliter la comparaison
response_lower = response.lower()
# Détection directe des réponses négatives en début de texte
first_line = response_lower.split('\n')[0] if '\n' in response_lower else response_lower[:50]
starts_with_non = first_line.strip().startswith("non") or first_line.strip().startswith("non.")
# Détection explicite d'une réponse négative au début de la réponse
explicit_negative = starts_with_non or any(neg_start in first_line for neg_start in ["non pertinent", "pas pertinent"])
# Détection explicite d'une réponse positive au début de la réponse
explicit_positive = first_line.strip().startswith("oui") or first_line.strip().startswith("pertinent")
# Si une réponse explicite est détectée, l'utiliser directement
if explicit_negative:
is_relevant = False
elif explicit_positive:
is_relevant = True
else:
# Sinon, utiliser l'analyse par mots-clés
# Mots clés positifs forts
positive_keywords = ["oui", "pertinent", "pertinente", "utile", "important", "relevante",
"capture d'écran", "message d'erreur", "interface logicielle",
"configuration", "technique", "diagnostic"]
# Mots clés négatifs forts
negative_keywords = ["non", "pas pertinent", "non pertinente", "inutile", "irrelevant",
"photo personnelle", "marketing", "sans rapport", "hors sujet",
"décorative", "logo"]
# Compter les occurrences de mots clés
positive_count = sum(1 for kw in positive_keywords if kw in response_lower)
negative_count = sum(1 for kw in negative_keywords if kw in response_lower)
# Heuristique de décision basée sur la prépondérance des mots clés
is_relevant = positive_count > negative_count
# Extraire le raisonnement (les dernières phrases de la réponse)
lines = response.split('\n')
reason_lines = []
for line in reversed(lines):
if line.strip():
reason_lines.insert(0, line.strip())
if len(reason_lines) >= 2: # Prendre les 2 dernières lignes non vides
break
reason = " ".join(reason_lines) if reason_lines else "Décision basée sur l'analyse des mots-clés"
# Log détaillé de l'analyse
logger.debug(f"Analyse de la réponse: \n - Réponse brute: {response[:100]}...\n"
f" - Commence par 'non': {starts_with_non}\n"
f" - Détection explicite négative: {explicit_negative}\n"
f" - Détection explicite positive: {explicit_positive}\n"
f" - Décision finale: {'pertinente' if is_relevant else 'non pertinente'}\n"
f" - Raison: {reason}")
return is_relevant, reason
def _get_timestamp(self) -> str:
"""Retourne un timestamp au format YYYYMMDD_HHMMSS"""
from datetime import datetime
return datetime.now().strftime("%Y%m%d_%H%M%S")

View File

@ -1,609 +0,0 @@
import json
import os
from .base_agent import BaseAgent
from datetime import datetime
from typing import Dict, Any, Tuple, Optional, List
import logging
import traceback
import re
import sys
from .utils.report_utils import extraire_et_traiter_json
from .utils.report_formatter import extraire_sections_texte, generer_rapport_markdown, construire_rapport_json
from .utils.agent_info_collector import collecter_info_agents, collecter_prompts_agents
logger = logging.getLogger("AgentReportGeneratorQwen")
class AgentReportGeneratorQwen(BaseAgent):
"""
Agent spécialisé pour générer des rapports avec le modèle Qwen.
Adapté pour gérer les limitations spécifiques de Qwen et optimiser les résultats.
Cet agent utilise une approche en plusieurs étapes pour éviter les timeouts
et s'assurer que tous les éléments du rapport soient bien générés.
"""
def __init__(self, llm):
super().__init__("AgentReportGeneratorQwen", llm)
# Configuration locale de l'agent
self.temperature = 0.2
self.top_p = 0.9
self.max_tokens = 10000 # Réduit pour Qwen pour éviter les timeouts
# Prompt système principal - Simplifié et optimisé pour Qwen
self.system_prompt = """Tu es un expert en génération de rapports techniques pour BRG-Lab.
Ta mission est de synthétiser les analyses en un rapport clair et structuré.
TON RAPPORT DOIT OBLIGATOIREMENT INCLURE DANS CET ORDRE:
1. Un résumé du problème initial
2. Une analyse des images pertinentes (courte)
3. Une synthèse globale des analyses d'images (très brève)
4. Une reconstitution du fil de discussion
5. Un tableau des échanges au format JSON
6. Un diagnostic technique des causes probables
Le format JSON des échanges DOIT être exactement:
```json
{
"chronologie_echanges": [
{"date": "date exacte", "emetteur": "CLIENT", "type": "Question", "contenu": "contenu synthétisé"},
{"date": "date exacte", "emetteur": "SUPPORT", "type": "Réponse", "contenu": "contenu avec liens"}
]
}
```
IMPORTANT: La structure JSON correcte est la partie la plus critique!"""
# Version du prompt pour la traçabilité
self.prompt_version = "qwen-v1.1"
# Flag pour indiquer si on doit utiliser l'approche en 2 étapes
self.use_two_step_approach = True
# Appliquer la configuration au LLM
self._appliquer_config_locale()
logger.info("AgentReportGeneratorQwen initialisé")
def _appliquer_config_locale(self) -> None:
"""
Applique la configuration locale au modèle LLM.
"""
# Appliquer le prompt système
if hasattr(self.llm, "prompt_system"):
self.llm.prompt_system = self.system_prompt
# Appliquer les paramètres
if hasattr(self.llm, "configurer"):
params = {
"temperature": self.temperature,
"top_p": self.top_p,
"max_tokens": self.max_tokens,
"timeout": 60 # Timeout réduit pour Qwen
}
self.llm.configurer(**params)
logger.info(f"Configuration appliquée au modèle Qwen: {str(params)}")
def _formater_prompt_pour_rapport_etape1(self, ticket_analyse: str, images_analyses: List[Dict]) -> str:
"""
Formate le prompt pour la première étape: résumé, analyse d'images et synthèse
"""
num_images = len(images_analyses)
logger.info(f"Formatage du prompt étape 1 avec {num_images} analyses d'images")
# Construire la section d'analyse du ticket
prompt = f"""Génère les 3 premières sections d'un rapport technique basé sur les analyses suivantes.
## ANALYSE DU TICKET
{ticket_analyse}
"""
# Ajouter la section d'analyse des images si présente
if num_images > 0:
prompt += f"\n## ANALYSES DES IMAGES ({num_images} images)\n"
for i, img_analyse in enumerate(images_analyses, 1):
image_name = img_analyse.get("image_name", f"Image {i}")
analyse = img_analyse.get("analyse", "Analyse non disponible")
prompt += f"\n### IMAGE {i}: {image_name}\n{analyse}\n"
else:
prompt += "\n## ANALYSES DES IMAGES\nAucune image n'a été fournie pour ce ticket.\n"
# Instructions pour le rapport
prompt += """
## INSTRUCTIONS POUR LE RAPPORT (ÉTAPE 1)
GÉNÈRE UNIQUEMENT LES 3 PREMIÈRES SECTIONS:
1. Résumé du problème (## Résumé du problème)
2. Analyse des images (## Analyse des images)
3. Synthèse globale des analyses d'images (## 3.1 Synthèse globale des analyses d'images)
POUR LA SECTION ANALYSE DES IMAGES:
- Décris chaque image de manière factuelle
- Mets en évidence les éléments encadrés ou surlignés
- Explique la relation avec le problème initial
POUR LA SECTION SYNTHÈSE GLOBALE:
- Titre à utiliser OBLIGATOIREMENT: ## 3.1 Synthèse globale des analyses d'images
- Premier sous-titre à utiliser OBLIGATOIREMENT: _Analyse transversale des captures d'écran_
- Explique comment les images se complètent
- Identifie les points communs entre les images
- Montre comment elles confirment les informations du support
NE GÉNÈRE PAS ENCORE:
- Le fil de discussion
- Le tableau des échanges
- Le diagnostic technique
Reste factuel et précis dans ton analyse.
"""
return prompt
def _formater_prompt_pour_rapport_etape2(self, ticket_analyse: str, etape1_resultat: str) -> str:
"""
Formate le prompt pour la seconde étape: fil de discussion, tableau JSON et diagnostic
"""
logger.info(f"Formatage du prompt étape 2")
# Extraire le résumé et l'analyse des images de l'étape 1
resume_match = re.search(r'## Résumé du problème(.*?)(?=##|$)', etape1_resultat, re.DOTALL)
resume = resume_match.group(1).strip() if resume_match else "Résumé non disponible."
prompt = f"""Génère le tableau JSON des échanges pour le ticket en te basant sur l'analyse du ticket.
## ANALYSE DU TICKET (UTILISE CES DONNÉES POUR CRÉER LES ÉCHANGES)
{ticket_analyse}
## RÉSUMÉ DU PROBLÈME
{resume}
## INSTRUCTIONS POUR LE TABLEAU JSON
CRÉE UNIQUEMENT UN TABLEAU JSON avec cette structure:
```json
{{
"chronologie_echanges": [
{{"date": "14/03/2023 10:48:53", "emetteur": "CLIENT", "type": "Question", "contenu": "Création échantillons - Opérateur de prélèvement : Saisie manuelle non possible. Dans l'ancienne version, on saisissait nous même la personne qui a prélevé l'échantillon, mais cette option ne semble plus disponible."}},
{{"date": "14/03/2023 13:25:45", "emetteur": "SUPPORT", "type": "Réponse", "contenu": "Pour des raisons normatives, l'opérateur de prélèvement doit obligatoirement faire partie de la liste des utilisateurs du logiciel et appartenir au groupe 'Opérateur de prélèvement'. Il n'est donc pas possible d'ajouter une personne tierce."}}
]
}}
```
IMPORTANT:
- AJOUTE OBLIGATOIREMENT une entrée pour la question initiale du client extraite du nom ou de la description du ticket
- INCLUS OBLIGATOIREMENT la réponse du support
- AJOUTE OBLIGATOIREMENT une entrée "Complément visuel" qui synthétise l'apport des images
- UTILISE les dates et le contenu exact des messages du ticket
- Format à suivre pour le complément visuel:
```json
{{
"chronologie_echanges": [
// ... question et réponse ...
{{"date": "DATE_ACTUELLE", "emetteur": "SUPPORT", "type": "Complément visuel", "contenu": "L'analyse de l'image confirme visuellement le problème: la liste déroulante des opérateurs de prélèvement affiche 'Aucun opérateur trouvé', ce qui concorde avec l'explication fournie concernant les restrictions normatives."}}
]
}}
```
"""
return prompt
def _creer_fil_discussion_dynamique(self, ticket_data: Dict, echanges_json: Dict) -> str:
"""
Génère un fil de discussion dynamiquement à partir des données du ticket et des échanges
"""
logger.info("Génération du fil de discussion dynamique")
# Initialiser le fil de discussion
fil_discussion = "## Fil de discussion\n\n"
# Extraire les informations du ticket
ticket_name = ticket_data.get("name", "")
ticket_description = ticket_data.get("description", "")
ticket_create_date = ticket_data.get("create_date", "")
# Générer la section question initiale
fil_discussion += "### Question initiale du client\n"
if ticket_create_date:
fil_discussion += f"**Date**: {ticket_create_date}\n"
if ticket_name:
fil_discussion += f"**Sujet**: {ticket_name}\n"
if ticket_description:
# Nettoyer et formater la description
description_clean = ticket_description.replace("\n\n", "\n").strip()
fil_discussion += f"**Contenu**: {description_clean}\n\n"
# Ajouter les réponses du support et compléments visuels
if echanges_json and "chronologie_echanges" in echanges_json:
for echange in echanges_json["chronologie_echanges"]:
emetteur = echange.get("emetteur", "")
type_msg = echange.get("type", "")
date = echange.get("date", "")
contenu = echange.get("contenu", "")
# Uniquement les messages du support, pas les questions client déjà incluses
if emetteur.upper() == "SUPPORT":
if type_msg.upper() == "RÉPONSE" or type_msg.upper() == "REPONSE":
fil_discussion += f"### Réponse du support technique\n"
if date:
fil_discussion += f"**Date**: {date}\n"
fil_discussion += f"**Contenu**:\n{contenu}\n\n"
elif type_msg.upper() == "COMPLÉMENT VISUEL" or type_msg.upper() == "COMPLEMENT VISUEL":
fil_discussion += f"### Analyse visuelle\n"
if date:
fil_discussion += f"**Date**: {date}\n"
fil_discussion += f"**Contenu**:\n{contenu}\n\n"
return fil_discussion
def executer(self, rapport_data: Dict, rapport_dir: str) -> Tuple[Optional[str], Optional[str]]:
"""
Génère un rapport à partir des analyses effectuées, en utilisant une approche
en deux étapes adaptée aux contraintes du modèle Qwen
"""
try:
# 1. PRÉPARATION
ticket_id = self._extraire_ticket_id(rapport_data, rapport_dir)
logger.info(f"Génération du rapport Qwen pour le ticket: {ticket_id}")
print(f"AgentReportGeneratorQwen: Génération du rapport pour {ticket_id}")
# Créer le répertoire de sortie si nécessaire
os.makedirs(rapport_dir, exist_ok=True)
# 2. EXTRACTION DES DONNÉES
ticket_analyse = self._extraire_analyse_ticket(rapport_data)
images_analyses = self._extraire_analyses_images(rapport_data)
# Extraire les données du ticket pour utilisation ultérieure
ticket_data = rapport_data.get("ticket_data", {})
# 3. COLLECTE DES INFORMATIONS SUR LES AGENTS
agent_info = {
"model": getattr(self.llm, "modele", str(type(self.llm))),
"temperature": self.temperature,
"top_p": self.top_p,
"max_tokens": self.max_tokens,
"prompt_version": self.prompt_version
}
agents_info = collecter_info_agents(rapport_data, agent_info)
prompts_utilises = collecter_prompts_agents(self.system_prompt)
# 4. GÉNÉRATION DU RAPPORT (APPROCHE EN DEUX ÉTAPES)
start_time = datetime.now()
if self.use_two_step_approach:
logger.info("Utilisation de l'approche en deux étapes pour Qwen")
print(f" Génération du rapport en deux étapes...")
# ÉTAPE 1: Résumé, analyse d'images et synthèse
logger.info("ÉTAPE 1: Génération du résumé, analyse d'images et synthèse")
prompt_etape1 = self._formater_prompt_pour_rapport_etape1(ticket_analyse, images_analyses)
try:
etape1_resultat = self.llm.interroger(prompt_etape1)
logger.info(f"Étape 1 complétée: {len(etape1_resultat)} caractères")
print(f" Étape 1 complétée: {len(etape1_resultat)} caractères")
except Exception as e:
logger.error(f"Erreur lors de l'étape 1: {str(e)}")
etape1_resultat = "## Résumé du problème\nUne erreur est survenue lors de la génération du résumé.\n\n## Analyse des images\nLes images n'ont pas pu être analysées correctement.\n\n## Synthèse globale des analyses d'images\nImpossible de fournir une synthèse complète en raison d'une erreur de génération."
# ÉTAPE 2: Tableau JSON uniquement
logger.info("ÉTAPE 2: Génération du tableau JSON")
prompt_etape2 = self._formater_prompt_pour_rapport_etape2(ticket_analyse, etape1_resultat)
try:
etape2_resultat = self.llm.interroger(prompt_etape2)
logger.info(f"Étape 2 complétée: {len(etape2_resultat)} caractères")
print(f" Étape 2 complétée: {len(etape2_resultat)} caractères")
# Extraire uniquement le JSON si c'est tout ce qui est généré
json_match = re.search(r'```json\s*(.*?)\s*```', etape2_resultat, re.DOTALL)
if json_match:
json_content = json_match.group(1)
etape2_resultat = f"## Tableau questions/réponses\n```json\n{json_content}\n```\n\n## Diagnostic technique\nLe problème d'affichage des utilisateurs est dû à deux configurations possibles:\n\n1. Les utilisateurs sans laboratoire principal assigné n'apparaissent pas par défaut dans la liste. La solution est d'activer l'option \"Affiche les laboratoires secondaires\".\n\n2. Les utilisateurs dont le compte a été dévalidé n'apparaissent pas par défaut. Il faut cocher l'option \"Affiche les utilisateurs non valides\" pour les voir apparaître (en grisé dans la liste)."
except Exception as e:
logger.error(f"Erreur lors de l'étape 2: {str(e)}")
# Créer une structure JSON minimale pour éviter les erreurs
etape2_resultat = """## Tableau questions/réponses\n```json\n{"chronologie_echanges": []}\n```\n\n## Diagnostic technique\nUne erreur est survenue lors de la génération du diagnostic."""
# Extraire le JSON généré ou utiliser un JSON par défaut
json_match = re.search(r'```json\s*(.*?)\s*```', etape2_resultat, re.DOTALL)
if json_match:
try:
echanges_json = json.loads(json_match.group(1))
except:
echanges_json = {"chronologie_echanges": []}
else:
echanges_json = {"chronologie_echanges": []}
# AJOUT: S'assurer qu'il y a une question initiale du client
if not any(e.get("emetteur", "").upper() == "CLIENT" and e.get("type", "").upper() == "QUESTION" for e in echanges_json.get("chronologie_echanges", [])):
# Ajouter une question initiale extraite du ticket
question_initiale = {
"date": ticket_data.get("create_date", datetime.now().strftime("%d/%m/%Y %H:%M:%S")),
"emetteur": "CLIENT",
"type": "Question",
"contenu": f"{ticket_data.get('name', '')}. {ticket_data.get('description', '').split('\n')[0]}"
}
# Insérer au début de la chronologie
if "chronologie_echanges" in echanges_json and echanges_json["chronologie_echanges"]:
echanges_json["chronologie_echanges"].insert(0, question_initiale)
else:
echanges_json["chronologie_echanges"] = [question_initiale]
# AJOUT: S'assurer qu'il y a un complément visuel si des images sont disponibles
if images_analyses and not any(e.get("type", "").upper() in ["COMPLÉMENT VISUEL", "COMPLEMENT VISUEL"] for e in echanges_json.get("chronologie_echanges", [])):
# Créer un complément visuel basé sur les images disponibles
complement_visuel = {
"date": datetime.now().strftime("%d/%m/%Y %H:%M:%S"),
"emetteur": "SUPPORT",
"type": "Complément visuel",
"contenu": f"L'analyse de {len(images_analyses)} image(s) confirme visuellement le problème: la liste déroulante des opérateurs de prélèvement affiche 'Aucun opérateur trouvé', ce qui concorde avec l'explication fournie concernant les restrictions normatives."
}
# Ajouter à la fin de la chronologie
if "chronologie_echanges" in echanges_json:
echanges_json["chronologie_echanges"].append(complement_visuel)
# Mettre à jour le JSON dans etape2_resultat
etape2_resultat_updated = re.sub(
r'```json\s*.*?\s*```',
f'```json\n{json.dumps(echanges_json, indent=2, ensure_ascii=False)}\n```',
etape2_resultat,
flags=re.DOTALL
)
# Générer le fil de discussion dynamiquement à partir des données réelles
fil_discussion = self._creer_fil_discussion_dynamique(ticket_data, echanges_json)
# Combiner les résultats des deux étapes
rapport_genere = f"# Rapport d'analyse: {ticket_id}\n\n{etape1_resultat}\n\n{fil_discussion}\n\n{etape2_resultat_updated}"
else:
# APPROCHE STANDARD EN UNE ÉTAPE (FALLBACK)
logger.info("Utilisation de l'approche standard en une étape")
print(f" Génération du rapport avec le LLM en une étape...")
# Version simplifiée pour générer le rapport en une seule étape
prompt = f"""Génère un rapport technique complet sur le ticket {ticket_id}.
## ANALYSE DU TICKET
{ticket_analyse}
## ANALYSES DES IMAGES ({len(images_analyses)} images)
[Résumé des analyses d'images disponible]
## STRUCTURE OBLIGATOIRE
1. Résumé du problème
2. Analyse des images
3. Synthèse globale
4. Fil de discussion
5. Tableau JSON des échanges
6. Diagnostic technique
IMPORTANT: INCLUS ABSOLUMENT un tableau JSON des échanges avec cette structure:
```json
{{
"chronologie_echanges": [
{{"date": "date", "emetteur": "CLIENT", "type": "Question", "contenu": "contenu"}}
]
}}
```
"""
try:
rapport_genere = self.llm.interroger(prompt)
except Exception as e:
logger.error(f"Erreur lors de la génération en une étape: {str(e)}")
rapport_genere = f"# Rapport d'analyse: {ticket_id}\n\n## Erreur\nUne erreur est survenue lors de la génération du rapport complet.\n\n## Tableau questions/réponses\n```json\n{{\"chronologie_echanges\": []}}\n```"
# Calculer le temps total de génération
generation_time = (datetime.now() - start_time).total_seconds()
logger.info(f"Rapport généré: {len(rapport_genere)} caractères en {generation_time} secondes")
print(f" Rapport généré: {len(rapport_genere)} caractères en {generation_time:.2f} secondes")
# 5. VÉRIFICATION ET CORRECTION DU TABLEAU JSON
rapport_traite, echanges_json, _ = extraire_et_traiter_json(rapport_genere)
# Si aucun JSON n'est trouvé, créer une structure minimale
if echanges_json is None:
logger.warning("Aucun échange JSON extrait, tentative de génération manuelle")
# Créer une structure JSON minimale basée sur le ticket
echanges_json = {"chronologie_echanges": []}
try:
# Extraire la question du ticket
ticket_name = ticket_data.get("name", "")
ticket_description = ticket_data.get("description", "")
# Créer une entrée pour la question cliente
echanges_json["chronologie_echanges"].append({
"date": ticket_data.get("create_date", datetime.now().strftime("%d/%m/%Y %H:%M:%S")),
"emetteur": "CLIENT",
"type": "Question",
"contenu": f"{ticket_name}. {ticket_description.split('\n')[0] if ticket_description else ''}"
})
# Ajouter les réponses support
for message in ticket_data.get("messages", []):
author = message.get("author_id", "")
date = message.get("date", "")
content = message.get("content", "")
if author and date and content:
echanges_json["chronologie_echanges"].append({
"date": date,
"emetteur": "SUPPORT",
"type": "Réponse",
"contenu": content.split("\n\n")[0] if "\n\n" in content else content
})
# Ajouter une entrée visuelle si des images sont disponibles
if images_analyses:
echanges_json["chronologie_echanges"].append({
"date": datetime.now().strftime("%d/%m/%Y %H:%M:%S"),
"emetteur": "SUPPORT",
"type": "Complément visuel",
"contenu": f"Analyse des {len(images_analyses)} images disponibles montrant les interfaces et options pertinentes."
})
except Exception as e:
logger.error(f"Erreur lors de la création manuelle du JSON: {str(e)}")
# Extraire les sections textuelles
resume, analyse_images, diagnostic = extraire_sections_texte(rapport_genere)
# 6. CRÉATION DU RAPPORT JSON
agent_metadata = {
"model": getattr(self.llm, "modele", str(type(self.llm))),
"model_version": getattr(self.llm, "version", "non spécifiée"),
"temperature": self.temperature,
"top_p": self.top_p,
"max_tokens": self.max_tokens,
"generation_time": generation_time,
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"agents": agents_info,
"approach": "two_step" if self.use_two_step_approach else "single_step"
}
# Construire le rapport JSON
rapport_json = construire_rapport_json(
rapport_genere=rapport_genere,
rapport_data=rapport_data,
ticket_id=ticket_id,
ticket_analyse=ticket_analyse,
images_analyses=images_analyses,
generation_time=generation_time,
resume=resume,
analyse_images=analyse_images,
diagnostic=diagnostic,
echanges_json=echanges_json,
agent_metadata=agent_metadata,
prompts_utilises=prompts_utilises
)
# 7. SAUVEGARDE DU RAPPORT JSON
json_path = os.path.join(rapport_dir, f"{ticket_id}_rapport_final.json")
with open(json_path, "w", encoding="utf-8") as f:
json.dump(rapport_json, f, ensure_ascii=False, indent=2)
logger.info(f"Rapport JSON sauvegardé: {json_path}")
print(f" Rapport JSON sauvegardé: {json_path}")
# 8. GÉNÉRATION DU RAPPORT MARKDOWN
md_path = generer_rapport_markdown(json_path)
if md_path:
logger.info(f"Rapport Markdown généré: {md_path}")
print(f" Rapport Markdown généré: {md_path}")
else:
logger.error("Échec de la génération du rapport Markdown")
print(f" ERREUR: Échec de la génération du rapport Markdown")
return json_path, md_path
except Exception as e:
error_message = f"Erreur lors de la génération du rapport Qwen: {str(e)}"
logger.error(error_message)
logger.error(traceback.format_exc())
print(f" ERREUR: {error_message}")
return None, None
def _extraire_ticket_id(self, rapport_data: Dict, rapport_dir: str) -> str:
"""Extrait l'ID du ticket des données ou du chemin"""
# Essayer d'extraire depuis les données du rapport
ticket_id = rapport_data.get("ticket_id", "")
# Si pas d'ID direct, essayer depuis les données du ticket
if not ticket_id and "ticket_data" in rapport_data and isinstance(rapport_data["ticket_data"], dict):
ticket_id = rapport_data["ticket_data"].get("code", "")
# En dernier recours, extraire depuis le chemin
if not ticket_id:
# Essayer d'extraire un ID de ticket (format Txxxx) du chemin
match = re.search(r'T\d+', rapport_dir)
if match:
ticket_id = match.group(0)
else:
# Sinon, utiliser le dernier segment du chemin
ticket_id = os.path.basename(rapport_dir)
return ticket_id
def _extraire_analyse_ticket(self, rapport_data: Dict) -> str:
"""Extrait l'analyse du ticket des données"""
# Essayer les différentes clés possibles
for key in ["ticket_analyse", "analyse_json", "analyse_ticket"]:
if key in rapport_data and rapport_data[key]:
logger.info(f"Utilisation de {key}")
return rapport_data[key]
# Créer une analyse par défaut si aucune n'est disponible
logger.warning("Aucune analyse de ticket disponible, création d'un message par défaut")
ticket_data = rapport_data.get("ticket_data", {})
ticket_name = ticket_data.get("name", "Sans titre")
ticket_desc = ticket_data.get("description", "Pas de description disponible")
return f"Analyse par défaut du ticket:\nNom: {ticket_name}\nDescription: {ticket_desc}\n(Aucune analyse détaillée n'a été fournie)"
def _extraire_analyses_images(self, rapport_data: Dict) -> List[Dict]:
"""
Extrait et formate les analyses d'images pertinentes
"""
images_analyses = []
analyse_images_data = rapport_data.get("analyse_images", {})
# Parcourir toutes les images
for image_path, analyse_data in analyse_images_data.items():
# Vérifier si l'image est pertinente
is_relevant = False
if "sorting" in analyse_data and isinstance(analyse_data["sorting"], dict):
is_relevant = analyse_data["sorting"].get("is_relevant", False)
# Si l'image est pertinente, extraire son analyse
if is_relevant:
image_name = os.path.basename(image_path)
analyse = self._extraire_analyse_image(analyse_data)
if analyse:
images_analyses.append({
"image_name": image_name,
"image_path": image_path,
"analyse": analyse,
"sorting_info": analyse_data.get("sorting", {}),
"metadata": analyse_data.get("analysis", {}).get("metadata", {})
})
logger.info(f"Analyse de l'image {image_name} ajoutée")
return images_analyses
def _extraire_analyse_image(self, analyse_data: Dict) -> Optional[str]:
"""
Extrait l'analyse d'une image depuis les données
"""
# Si pas de données d'analyse, retourner None
if not "analysis" in analyse_data or not analyse_data["analysis"]:
if "sorting" in analyse_data and isinstance(analyse_data["sorting"], dict):
reason = analyse_data["sorting"].get("reason", "Non spécifiée")
return f"Image marquée comme pertinente. Raison: {reason}"
return None
# Extraire l'analyse selon le format des données
analysis = analyse_data["analysis"]
# Structure type 1: {"analyse": "texte"}
if isinstance(analysis, dict) and "analyse" in analysis:
return analysis["analyse"]
# Structure type 2: {"error": false, ...} - contient d'autres données utiles
if isinstance(analysis, dict) and "error" in analysis and not analysis.get("error", True):
return str(analysis)
# Structure type 3: texte d'analyse direct
if isinstance(analysis, str):
return analysis
# Structure type 4: autre format de dictionnaire - convertir en JSON
if isinstance(analysis, dict):
return json.dumps(analysis, ensure_ascii=False, indent=2)
# Aucun format reconnu
return None

View File

@ -1,301 +0,0 @@
from .base_agent import BaseAgent
from typing import Dict, Any, Optional
import logging
import json
import os
import sys
from datetime import datetime
# Ajout du chemin des utilitaires au PATH pour pouvoir les importer
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from loaders.ticket_data_loader import TicketDataLoader
logger = logging.getLogger("AgentTicketAnalyser")
class AgentTicketAnalyser(BaseAgent):
"""
Agent pour analyser les tickets (JSON ou Markdown) et en extraire les informations importantes.
Remplace l'ancien AgentJsonAnalyser avec des fonctionnalités améliorées.
"""
def __init__(self, llm):
super().__init__("AgentTicketAnalyser", llm)
# Configuration locale de l'agent
self.temperature = 0.1 # Besoin d'analyse très précise
self.top_p = 0.8
self.max_tokens = 8000
# Prompt système optimisé
self.system_prompt = """Tu es un expert en analyse de tickets pour le support informatique de BRG-Lab pour la société CBAO.
Tu interviens avant l'analyse des captures d'écran pour contextualiser le ticket, identifier les questions posées, et structurer les échanges de manière claire.
Ta mission principale :
1. Identifier le client et le contexte du ticket (demande "name" et "description")
- Récupère le nom de l'auteur si présent
- Indique si un `user_id` est disponible
- Conserve uniquement les informations d'identification utiles (pas d'adresse ou signature de mail inutile)
2. Mettre en perspective le `name` du ticket
- Il peut contenir une ou plusieurs questions implicites
- Reformule ces questions de façon explicite
3. Analyser la `description`
- Elle fournit souvent le vrai point d'entrée technique
- Repère les formulations interrogatives ou les demandes spécifiques
- Identifie si cette partie complète ou précise les questions du nom
4. Structurer le fil de discussion
- Conserve uniquement les échanges pertinents
-Conserve les questions soulevés par "name" ou "description"
- CONSERVE ABSOLUMENT les références documentation, FAQ, liens utiles et manuels
- Identifie clairement chaque intervenant (client / support)
- Classe les informations par ordre chronologique avec date et rôle
5. Préparer la transmission à l'agent suivant
- Préserve tous les éléments utiles à l'analyse d'image : modules cités, options évoquées, comportements décrits
- Mentionne si des images sont attachées au ticket
Structure ta réponse :
1. Résumé du contexte
- Client (nom, email si disponible)
- Sujet du ticket reformulé en une ou plusieurs questions
- Description technique synthétique
2. Informations techniques détectées
- Logiciels/modules mentionnés
- Paramètres évoqués
- Fonctionnalités impactées
- Conditions spécifiques (multi-laboratoire, utilisateur non valide, etc.)
3. Fil de discussion (filtrée, nettoyée, classée)
- Intervenant (Client/Support)
- Date et contenu de chaque échange
- Résumés techniques
- INCLURE TOUS les liens documentaires (manuel, FAQ, documentation technique)
4. Éléments liés à l'analyse visuelle
- Nombre d'images attachées
- Références aux interfaces ou options à visualiser
- Points à vérifier dans les captures (listes incomplètes, cases à cocher, utilisateurs grisés, etc.)
IMPORTANT :
- Ne propose aucune solution ni interprétation
- Ne génère pas de tableau
- Reste strictement factuel en te basant uniquement sur les informations fournies
- Ne reformule pas les messages, conserve les formulations exactes sauf nettoyage de forme"""
# Initialiser le loader de données
self.ticket_loader = TicketDataLoader()
# Appliquer la configuration au LLM
self._appliquer_config_locale()
logger.info("AgentTicketAnalyser initialisé")
def _appliquer_config_locale(self) -> None:
"""
Applique la configuration locale au modèle LLM.
"""
# Appliquer le prompt système
if hasattr(self.llm, "prompt_system"):
self.llm.prompt_system = self.system_prompt
# Appliquer les paramètres
if hasattr(self.llm, "configurer"):
params = {
"temperature": self.temperature,
"top_p": self.top_p,
"max_tokens": self.max_tokens
}
self.llm.configurer(**params)
def executer(self, ticket_data: Dict[str, Any]) -> str:
"""
Analyse un ticket pour en extraire les informations pertinentes
Args:
ticket_data: Dictionnaire contenant les données du ticket à analyser
ou chemin vers un fichier de ticket (JSON ou Markdown)
Returns:
Réponse formatée contenant l'analyse du ticket
"""
# Détecter si ticket_data est un chemin de fichier ou un dictionnaire
if isinstance(ticket_data, str) and os.path.exists(ticket_data):
try:
ticket_data = self.ticket_loader.charger(ticket_data)
logger.info(f"Données chargées depuis le fichier: {ticket_data}")
except Exception as e:
error_message = f"Erreur lors du chargement du fichier: {str(e)}"
logger.error(error_message)
return f"ERREUR: {error_message}"
# Vérifier que les données sont bien un dictionnaire
if not isinstance(ticket_data, dict):
error_message = "Les données du ticket doivent être un dictionnaire ou un chemin de fichier valide"
logger.error(error_message)
return f"ERREUR: {error_message}"
ticket_code = ticket_data.get('code', 'Inconnu')
logger.info(f"Analyse du ticket: {ticket_code}")
print(f"AgentTicketAnalyser: Analyse du ticket {ticket_code}")
# Récupérer les métadonnées sur la source des données
source_format = "inconnu"
source_file = "non spécifié"
if "metadata" in ticket_data and isinstance(ticket_data["metadata"], dict):
source_format = ticket_data["metadata"].get("format", "inconnu")
source_file = ticket_data["metadata"].get("source_file", "non spécifié")
logger.info(f"Format source: {source_format}, Fichier source: {source_file}")
# Préparer le ticket pour l'analyse
ticket_formate = self._formater_ticket_pour_analyse(ticket_data)
# Créer le prompt pour l'analyse, adapté au format source
prompt = f"""Analyse ce ticket pour en extraire les informations clés et préparer une synthèse structurée.
SOURCE: {source_format.upper()}
{ticket_formate}
RAPPEL IMPORTANT:
- CONSERVE TOUS les liens (FAQ, documentation, manuels) présents dans les messages
- Extrais et organise chronologiquement les échanges client/support
- Identifie les éléments techniques à observer dans les captures d'écran
- Reste factuel et précis sans proposer de solution"""
try:
logger.info("Interrogation du LLM")
response = self.llm.interroger(prompt)
logger.info(f"Réponse reçue: {len(response)} caractères")
print(f" Analyse terminée: {len(response)} caractères")
except Exception as e:
error_message = f"Erreur lors de l'analyse du ticket: {str(e)}"
logger.error(error_message)
response = f"ERREUR: {error_message}"
print(f" ERREUR: {error_message}")
# Enregistrer l'historique avec le prompt complet pour la traçabilité
self.ajouter_historique("analyse_ticket",
{
"ticket_id": ticket_code,
"format_source": source_format,
"source_file": source_file,
"prompt": prompt,
"temperature": self.temperature,
"top_p": self.top_p,
"max_tokens": self.max_tokens,
"timestamp": self._get_timestamp()
},
response)
return response
def _formater_ticket_pour_analyse(self, ticket_data: Dict) -> str:
"""
Formate les données du ticket pour l'analyse LLM, avec une meilleure
gestion des différents formats et structures de données.
Args:
ticket_data: Les données du ticket
Returns:
Représentation textuelle formatée du ticket
"""
# Initialiser avec les informations de base
ticket_name = ticket_data.get('name', 'Sans titre')
ticket_code = ticket_data.get('code', 'Inconnu')
info = f"## TICKET {ticket_code}: {ticket_name}\n\n"
info += f"## NOM DE LA DEMANDE (PROBLÈME INITIAL)\n{ticket_name}\n\n"
# Ajouter la description
description = ticket_data.get('description', '')
if description:
info += f"## DESCRIPTION DU PROBLÈME\n{description}\n\n"
# Ajouter les informations du ticket (exclure certains champs spécifiques)
champs_a_exclure = ['code', 'name', 'description', 'messages', 'metadata']
info += "## INFORMATIONS TECHNIQUES DU TICKET\n"
for key, value in ticket_data.items():
if key not in champs_a_exclure and value:
# Formater les valeurs complexes si nécessaire
if isinstance(value, (dict, list)):
value = json.dumps(value, ensure_ascii=False, indent=2)
info += f"- {key}: {value}\n"
info += "\n"
# Ajouter les messages (conversations) avec un formatage amélioré pour distinguer client/support
messages = ticket_data.get('messages', [])
if messages:
info += "## CHRONOLOGIE DES ÉCHANGES CLIENT/SUPPORT\n"
for i, msg in enumerate(messages):
# Vérifier que le message est bien un dictionnaire
if not isinstance(msg, dict):
continue
sender = msg.get('from', 'Inconnu')
date = msg.get('date', 'Date inconnue')
content = msg.get('content', '')
# Identifier si c'est client ou support
sender_type = "CLIENT" if "client" in sender.lower() else "SUPPORT" if "support" in sender.lower() else "AUTRE"
# Formater correctement la date si possible
try:
if date != 'Date inconnue':
# Essayer différents formats de date
for date_format in ['%Y-%m-%d %H:%M:%S', '%Y-%m-%d', '%d/%m/%Y']:
try:
date_obj = datetime.strptime(date, date_format)
date = date_obj.strftime('%d/%m/%Y %H:%M')
break
except ValueError:
continue
except Exception:
pass # Garder la date d'origine en cas d'erreur
info += f"### Message {i+1} - [{sender_type}] De: {sender} - Date: {date}\n{content}\n\n"
# Ajouter les métadonnées techniques si présentes
metadata = ticket_data.get('metadata', {})
# Exclure certaines métadonnées internes
for key in ['source_file', 'format']:
if key in metadata:
metadata.pop(key)
if metadata:
info += "## MÉTADONNÉES TECHNIQUES\n"
for key, value in metadata.items():
if isinstance(value, (dict, list)):
value = json.dumps(value, ensure_ascii=False, indent=2)
info += f"- {key}: {value}\n"
info += "\n"
return info
def analyser_depuis_fichier(self, chemin_fichier: str) -> str:
"""
Analyse un ticket à partir d'un fichier (JSON ou Markdown)
Args:
chemin_fichier: Chemin vers le fichier à analyser
Returns:
Résultat de l'analyse
"""
try:
ticket_data = self.ticket_loader.charger(chemin_fichier)
return self.executer(ticket_data)
except Exception as e:
error_message = f"Erreur lors de l'analyse du fichier {chemin_fichier}: {str(e)}"
logger.error(error_message)
return f"ERREUR: {error_message}"
def _get_timestamp(self) -> str:
"""Retourne un timestamp au format YYYYMMDD_HHMMSS"""
return datetime.now().strftime("%Y%m%d_%H%M%S")

View File

@ -1,29 +0,0 @@
from abc import ABC, abstractmethod
from typing import List, Dict, Any, Optional
class BaseAgent(ABC):
"""
Classe de base pour les agents.
"""
def __init__(self, nom: str, llm: Any):
self.nom = nom
self.llm = llm
self.historique: List[Dict[str, Any]] = []
def ajouter_historique(self, action: str, input_data: Any, output_data: Any):
# Ajouter les informations sur le modèle et les paramètres utilisés
metadata = {
"model": getattr(self.llm, "modele", str(type(self.llm))),
"duree_traitement": str(getattr(self.llm, "dureeTraitement", "N/A"))
}
self.historique.append({
"action": action,
"input": input_data,
"output": output_data,
"metadata": metadata
})
@abstractmethod
def executer(self, *args, **kwargs) -> Any:
pass

View File

@ -0,0 +1,3 @@
"""
Package agents.pixtral12b contenant les agents utilisant le modèle Pixtral 12B.
"""

View File

@ -3,193 +3,187 @@
import os
import json
import time
import logging
import base64
from typing import Dict, Any, List, Optional, Tuple
from typing import Dict, Any, Optional
from datetime import datetime
# Importer BaseAgent depuis le répertoire utils
from agents.utils.base_agent import BaseAgent
# Configuration du logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger("AgentImageAnalyser_Pixtral12b")
class AgentImageAnalyser(BaseAgent):
"""
Agent spécialisé pour analyser des images et en extraire les informations pertinentes.
Version optimisée pour Pixtral 12B.
Agent pour analyser en détail les images des tickets.
Utilise un LLM avec capacités de vision pour décrire et analyser le contenu des images.
"""
def __init__(self, llm: Any):
"""
Initialise l'agent d'analyse d'images avec un modèle LLM.
Args:
llm: Instance du modèle de langage à utiliser
"""
def __init__(self, llm):
super().__init__(llm)
self.temperature = 0.1 # Température basse pour des analyses factuelles
self.top_p = 0.9
self.max_tokens = 3000
# System prompt spécifique pour l'analyse d'images
self.system_prompt = """Tu es un expert en analyse d'images de captures d'écran d'applications métier.
Tu vas analyser des images techniques pour en extraire des informations pertinentes.
# Configuration locale de l'agent
self.system_prompt = """Tu es un expert en analyse d'images techniques.
Ta mission est d'analyser en détail des captures d'écran et images techniques pour le support informatique.
Pour chaque image, tu dois :
1. Identifier le type d'interface visible (formulaire, tableau, menu, etc.)
2. Extraire tous les éléments visuels importants (champs, boutons, menus, messages)
3. Repérer les anomalies ou problèmes visibles (erreurs, incohérences, éléments manquants)
4. Identifier le contexte fonctionnel de l'image (à quelle fonctionnalité elle correspond)
Tu dois:
1. Décrire précisément le contenu visible de l'image
2. Identifier tout texte, message d'erreur ou information technique visible
3. Repérer les problèmes potentiels ou anomalies visibles
4. Fournir un contexte sur ce que l'image montre dans le cadre d'un problème informatique
Ta réponse suivra ce format structuré :
```
## Analyse de l'image: [Titre basé sur le contenu]
### Description générale
- Type d'interface: [type d'interface identifié]
- Éléments principaux: [liste des éléments UI dominants]
- Contexte fonctionnel: [fonctionnalité ou module apparent]
### Éléments détaillés
- [Liste détaillée des éléments visibles importants]
- [Valeurs de champs, options sélectionnées, etc.]
- [Messages système ou d'erreur si présents]
### Anomalies détectées
- [Description précise des problèmes visibles]
- [Éléments manquants ou incohérents]
- [Messages d'erreur et leur contexte]
### Interprétation technique
- [Explication technique de ce qui est affiché]
- [Relation avec le problème décrit dans le ticket]
- [Indications sur la source probable du problème]
```
Reste factuel et précis. Ne spécule pas au-delà de ce qui est clairement visible.
Concentre-toi sur les détails techniques plutôt que sur l'esthétique de l'interface."""
def executer(self, images: List[Dict[str, Any]], ticket_analyse: Optional[str] = None) -> List[Dict[str, Any]]:
Sois factuel, précis et exhaustif dans ton analyse. Concentre-toi sur les aspects techniques.
Format ta réponse de manière structurée pour faciliter la compréhension.
"""
def executer(self, image_path: str, contexte: Optional[str] = None) -> Dict[str, Any]:
"""
Analyse une liste d'images pour en extraire les informations pertinentes.
Analyse une image en détail pour en extraire les informations pertinentes.
Args:
images: Liste de dictionnaires contenant les informations sur les images
- 'path': Chemin de l'image
- 'type': Type de l'image
- 'nom': Nom de l'image
ticket_analyse: Analyse du ticket (contexte pour l'analyse des images)
image_path: Chemin vers l'image à analyser
contexte: Contexte optionnel sur le ticket pour aider à l'analyse
Returns:
Liste de dictionnaires contenant les analyses d'images
Dictionnaire contenant l'analyse détaillée de l'image
"""
results = []
image_name = os.path.basename(image_path)
logger.info(f"Analyse détaillée de l'image: {image_name}")
if not images:
logger.warning("Aucune image à analyser")
return results
# Vérifier que l'image existe
if not os.path.exists(image_path):
logger.error(f"L'image n'existe pas: {image_path}")
return {
"error": True,
"message": f"L'image n'existe pas: {image_name}",
"image_path": image_path
}
logger.info(f"Analyse de {len(images)} images")
print(f" Analyse de {len(images)} images")
# Préparer le prompt avec le contexte si disponible
prompt_base = "Analyse cette image en détail et réponds au format JSON."
if contexte:
prompt_base = f"Analyse cette image en détail dans le contexte suivant:\n{contexte}\n\nRéponds au format JSON."
prompt = f"""{prompt_base}
Analyse chaque élément important visible et fournit une description détaillée.
Format JSON attendu:
{{
"description_generale": "Description générale de ce que montre l'image",
"elements_techniques": ["Liste des éléments techniques visibles"],
"texte_visible": "Tout texte important visible dans l'image",
"messages_erreur": ["Liste des messages d'erreur si présents"],
"problemes_identifies": ["Liste des problèmes potentiels identifiés"],
"contexte_technique": "Explication du contexte technique de cette image",
"recommandations": ["Suggestions basées sur ce qui est visible"]
}}"""
# Analyser chaque image
for i, image_info in enumerate(images, 1):
image_path = image_info.get('path', '')
image_name = image_info.get('nom', os.path.basename(image_path))
# Effectuer l'analyse via le LLM
try:
# Utiliser la méthode d'interrogation avec image
resultat_brut = self.llm.interroger_avec_image(image_path, prompt)
if not os.path.exists(image_path):
logger.warning(f"Image non trouvée: {image_path}")
results.append({
"image": image_info,
"analyse": f"ERREUR: Image non trouvée: {image_path}",
"pertinent": False
})
continue
logger.info(f"Analyse de l'image {i}/{len(images)}: {image_name}")
print(f" Analyse de l'image {i}/{len(images)}: {image_name}")
start_time = time.time()
try:
# Encoder l'image en base64
image_base64 = self._encoder_image_base64(image_path)
# Construire le prompt pour l'analyse
prompt = self._construire_prompt_image(image_name, ticket_analyse)
# Analyser l'image avec le LLM
analyse = self.llm.generate_vision(
system_prompt=self.system_prompt,
prompt=prompt,
image_base64=image_base64,
temperature=self.temperature,
top_p=self.top_p,
max_tokens=self.max_tokens
)
# Calculer le temps d'analyse
analysis_time = time.time() - start_time
# Log de l'analyse complétée
logger.info(f"Analyse de l'image {image_name} complétée en {analysis_time:.2f} secondes")
print(f" Analyse complétée en {analysis_time:.2f} secondes")
# Ajouter le résultat à la liste
results.append({
"image": image_info,
"analyse": analyse,
"pertinent": True,
"analysis_time": analysis_time
})
except Exception as e:
error_message = f"Erreur lors de l'analyse de l'image {image_name}: {str(e)}"
logger.error(error_message)
print(f" ERREUR: {error_message}")
results.append({
"image": image_info,
"analyse": f"ERREUR: {error_message}",
"pertinent": False
})
return results
# Tenter d'extraire le JSON de la réponse
json_str = self._extraire_json(resultat_brut)
if json_str:
try:
# Charger le JSON
analyse = json.loads(json_str)
# Ajouter des métadonnées
analyse["image_path"] = image_path
analyse["image_name"] = image_name
analyse["timestamp"] = self._get_timestamp()
analyse["source"] = "agent_image_analyser"
# Ajouter la réponse brute pour référence
analyse["raw_response"] = resultat_brut
logger.info(f"Analyse complétée avec succès pour {image_name}")
return analyse
except json.JSONDecodeError as e:
logger.error(f"Erreur de décodage JSON pour {image_name}: {e}")
# Fournir une analyse de secours plus simple
return self._analyse_fallback(resultat_brut, image_path)
else:
logger.warning(f"Format de réponse non-JSON pour {image_name}")
# Fournir une analyse de secours plus simple
return self._analyse_fallback(resultat_brut, image_path)
except Exception as e:
logger.error(f"Erreur lors de l'analyse de l'image {image_name}: {str(e)}")
return {
"error": True,
"message": f"Erreur d'analyse: {str(e)}",
"image_path": image_path,
"image_name": image_name,
"timestamp": self._get_timestamp()
}
def _encoder_image_base64(self, image_path: str) -> str:
def _extraire_json(self, texte: str) -> Optional[str]:
"""
Encode une image en base64.
Extrait le contenu JSON d'une chaîne de texte.
Args:
image_path: Chemin de l'image à encoder
texte: Texte contenant potentiellement du JSON
Returns:
Chaîne encodée en base64
Chaîne JSON extraite ou None si aucun JSON n'est trouvé
"""
with open(image_path, "rb") as image_file:
return base64.b64encode(image_file.read()).decode('utf-8')
# Chercher des accolades ouvrantes et fermantes
debut = texte.find('{')
fin = texte.rfind('}')
if debut != -1 and fin != -1 and fin > debut:
return texte[debut:fin+1]
return None
def _construire_prompt_image(self, image_name: str, ticket_analyse: Optional[str] = None) -> str:
def _analyse_fallback(self, texte: str, image_path: str) -> Dict[str, Any]:
"""
Construit un prompt pour l'analyse d'une image.
Crée une analyse de secours quand le format JSON n'est pas utilisable.
Args:
image_name: Nom de l'image à analyser
ticket_analyse: Analyse du ticket (contexte pour l'analyse de l'image)
texte: Texte de l'analyse brute
image_path: Chemin de l'image
Returns:
Prompt pour l'analyse de l'image
Dictionnaire avec l'analyse simplifiée
"""
prompt = f"Analyse cette capture d'écran: {image_name}\n\n"
image_name = os.path.basename(image_path)
if ticket_analyse:
prompt += "### Contexte du ticket\n"
prompt += f"{ticket_analyse[:1000]}...\n\n" if len(ticket_analyse) > 1000 else f"{ticket_analyse}\n\n"
# Diviser le texte en paragraphes
paragraphes = [p.strip() for p in texte.split('\n\n') if p.strip()]
prompt += "Examine attentivement tous les éléments visuels, repère les anomalies, et identifie les informations techniques pertinentes. "
prompt += "Fournis une analyse complète et structurée de cette image selon le format demandé."
# Extraire ce qui pourrait être une description générale (premier paragraphe)
description = paragraphes[0] if paragraphes else "Analyse non disponible au format JSON"
return prompt
# Chercher des éléments techniques ou messages d'erreur
elements_techniques = []
messages_erreur = []
for p in paragraphes[1:]:
if "erreur" in p.lower() or "error" in p.lower() or "exception" in p.lower():
messages_erreur.append(p)
elif any(terme in p.lower() for terme in ["technique", "système", "logiciel", "interface", "configuration"]):
elements_techniques.append(p)
# Construire un dictionnaire simplifié
analyse = {
"description_generale": description,
"elements_techniques": elements_techniques,
"messages_erreur": messages_erreur,
"texte_visible": "Extraction de texte non disponible",
"problemes_identifies": [],
"contexte_technique": "Contexte non disponible au format JSON",
"recommandations": [],
"image_path": image_path,
"image_name": image_name,
"timestamp": self._get_timestamp(),
"source": "agent_image_analyser",
"format_fallback": True,
"raw_response": texte
}
return analyse
def _get_timestamp(self) -> str:
"""Retourne un timestamp au format YYYYMMDD_HHMMSS"""
return datetime.now().strftime("%Y%m%d_%H%M%S")

View File

@ -1,393 +1,193 @@
from .base_agent import BaseAgent
from agents.utils.base_agent import BaseAgent
import logging
import os
from typing import Dict, Any, Tuple
from PIL import Image
import base64
import io
from typing import List, Dict, Any, Optional
import json
logger = logging.getLogger("AgentImageSorter")
class AgentImageSorter(BaseAgent):
"""
Agent pour trier les images et identifier celles qui sont pertinentes.
Agent pour trier les images et identifier celles qui sont pertinentes pour l'analyse.
"""
def __init__(self, llm):
super().__init__("AgentImageSorter", llm)
super().__init__(llm)
# Configuration locale de l'agent
self.temperature = 0.2
self.top_p = 0.8
self.max_tokens = 300
self.system_prompt = """Tu es un agent spécialisé dans l'analyse et le tri d'images pour le support technique.
Ta mission est d'identifier les images pertinentes pour comprendre un problème technique, en distinguant
celles qui contiennent des informations utiles (captures d'écran, photos de produits défectueux, etc.)
de celles qui sont décoratives ou non informatives.
Suis ces directives pour évaluer chaque image:
1. Identifie le contenu principal de l'image (capture d'écran, photo, schéma, etc.)
2. Évalue si l'image contient des informations utiles pour comprendre le problème technique
3. Détermine si l'image montre un problème, une erreur, ou une situation anormale
4. Examine si l'image contient du texte ou des messages d'erreur importants
Pour chaque image, tu dois fournir:
- Une description concise du contenu (1-2 phrases)
- Un niveau de pertinence (Élevé/Moyen/Faible)
- Une justification de ton évaluation"""
# Centralisation des critères de pertinence
self.criteres_pertinence = """
Images PERTINENTES (réponds "oui" ou "pertinent"):
- Captures d'écran de logiciels ou d'interfaces
- logo BRG_LAB
- Référence à "logociel"
- Messages d'erreur
- Configurations système
- Tableaux de bord ou graphiques techniques
- Fenêtres de diagnostic
Images NON PERTINENTES (réponds "non" ou "non pertinent"):
- Photos personnelles
- Images marketing/promotionnelles
- Logos ou images de marque
- Paysages, personnes ou objets non liés à l'informatique
"""
# Centralisation des instructions d'analyse
self.instructions_analyse = """
IMPORTANT: Ne commence JAMAIS ta réponse par "Je ne peux pas directement visualiser l'image".
Si tu ne peux pas analyser l'image, réponds simplement "ERREUR: Impossible d'analyser l'image".
Analyse d'abord ce que montre l'image, puis réponds par "oui"/"pertinent" ou "non"/"non pertinent".
"""
# Construction du système prompt à partir des éléments centralisés
self.system_prompt = f"""Tu es un expert en tri d'images pour le support technique de BRG_Lab pour la société CBAO.
Ta mission est de déterminer si une image est pertinente pour le support technique de logiciels.
{self.criteres_pertinence}
{self.instructions_analyse}"""
self.image_batch_size = 3 # Nombre d'images à analyser par lot
# Appliquer la configuration au LLM
self._appliquer_config_locale()
logger.info("AgentImageSorter initialisé")
def _appliquer_config_locale(self) -> None:
def executer(self, attachments_dir: str, contexte: Optional[Dict] = None) -> Dict[str, Dict[str, Any]]:
"""
Applique la configuration locale au modèle LLM.
"""
# Appliquer le prompt système
if hasattr(self.llm, "prompt_system"):
self.llm.prompt_system = self.system_prompt
# Appliquer les paramètres
if hasattr(self.llm, "configurer"):
params = {
"temperature": self.temperature,
"top_p": self.top_p,
"max_tokens": self.max_tokens
}
self.llm.configurer(**params)
def _verifier_image(self, image_path: str) -> bool:
"""
Vérifie si l'image existe et est accessible
Trie les images dans un répertoire de pièces jointes et identifie celles qui sont pertinentes.
Args:
image_path: Chemin vers l'image
attachments_dir: Chemin vers le répertoire des pièces jointes
contexte: Contexte optionnel sur le ticket pour aider à l'analyse
Returns:
True si l'image existe et est accessible, False sinon
Dictionnaire avec les chemins des images comme clés et les résultats d'analyse comme valeurs
"""
try:
# Vérifier que le fichier existe
if not os.path.exists(image_path):
logger.error(f"L'image n'existe pas: {image_path}")
return False
# Vérifier que le fichier est accessible en lecture
if not os.access(image_path, os.R_OK):
logger.error(f"L'image n'est pas accessible en lecture: {image_path}")
return False
# Vérifier que le fichier peut être ouvert comme une image
with Image.open(image_path) as img:
# Vérifier les dimensions de l'image
width, height = img.size
if width <= 0 or height <= 0:
logger.error(f"Dimensions d'image invalides: {width}x{height}")
return False
logger.info(f"Image vérifiée avec succès: {image_path} ({width}x{height})")
return True
except Exception as e:
logger.error(f"Erreur lors de la vérification de l'image {image_path}: {str(e)}")
return False
def _encoder_image_base64(self, image_path: str) -> str:
"""
Encode l'image en base64 pour l'inclure directement dans le prompt
logger.info(f"Tri des images dans: {attachments_dir}")
Args:
image_path: Chemin vers l'image
Returns:
Chaîne de caractères au format data URI avec l'image encodée en base64
"""
try:
# Ouvrir l'image et la redimensionner si trop grande
with Image.open(image_path) as img:
# Redimensionner l'image si elle est trop grande (max 800x800)
max_size = 800
if img.width > max_size or img.height > max_size:
img.thumbnail((max_size, max_size), Image.Resampling.LANCZOS)
# Convertir en RGB si nécessaire (pour les formats comme PNG)
if img.mode != "RGB":
img = img.convert("RGB")
# Sauvegarder l'image en JPEG dans un buffer mémoire
buffer = io.BytesIO()
img.save(buffer, format="JPEG", quality=85)
buffer.seek(0)
# Encoder en base64
img_base64 = base64.b64encode(buffer.read()).decode("utf-8")
# Construire le data URI
data_uri = f"data:image/jpeg;base64,{img_base64}"
return data_uri
except Exception as e:
logger.error(f"Erreur lors de l'encodage de l'image {image_path}: {str(e)}")
return ""
def _generer_prompt_analyse(self, prefix: str = "", avec_image_base64: bool = False) -> str:
"""
Génère le prompt d'analyse standardisé
Args:
prefix: Préfixe optionnel (pour inclure l'image en base64 par exemple)
avec_image_base64: Indique si le prompt inclut déjà une image en base64
Returns:
Prompt formaté pour l'analyse
"""
return f"""{prefix}
Est-ce une image pertinente pour un ticket de support technique?
Réponds simplement par 'oui' ou 'non' suivi d'une brève explication."""
def executer(self, image_path: str) -> Dict[str, Any]:
"""
Évalue si une image est pertinente pour l'analyse d'un ticket technique
Args:
image_path: Chemin vers l'image à analyser
Returns:
Dictionnaire contenant la décision de pertinence, l'analyse et les métadonnées
"""
image_name = os.path.basename(image_path)
logger.info(f"Évaluation de la pertinence de l'image: {image_name}")
print(f" AgentImageSorter: Évaluation de {image_name}")
# Vérifier que l'image existe et est accessible
if not self._verifier_image(image_path):
error_message = f"L'image n'est pas accessible ou n'est pas valide: {image_name}"
logger.error(error_message)
print(f" ERREUR: {error_message}")
return {
"is_relevant": False,
"reason": f"Erreur d'accès: {error_message}",
"raw_response": "",
"error": True,
"metadata": {
"image_path": image_path,
"image_name": image_name,
"timestamp": self._get_timestamp(),
"error": True
}
}
# Utiliser une référence au fichier image que le modèle peut comprendre
try:
# Préparation du prompt standardisé
prompt = self._generer_prompt_analyse()
# Utiliser la méthode interroger_avec_image au lieu de interroger
if hasattr(self.llm, "interroger_avec_image"):
logger.info(f"Utilisation de la méthode interroger_avec_image pour {image_name}")
response = self.llm.interroger_avec_image(image_path, prompt)
# Vérifier si attachments_dir est un fichier ou un dossier
if os.path.isfile(attachments_dir):
logger.info(f"Le chemin fourni est un fichier et non un dossier: {attachments_dir}")
# Si c'est un fichier image, on le traite directement
if attachments_dir.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif', '.tiff')):
images = [attachments_dir]
# Le vrai dossier est le répertoire parent
attachments_dir = os.path.dirname(attachments_dir)
else:
# Fallback vers la méthode standard avec base64 si interroger_avec_image n'existe pas
logger.warning(f"La méthode interroger_avec_image n'existe pas, utilisation du fallback pour {image_name}")
img_base64 = self._encoder_image_base64(image_path)
if img_base64:
prompt_base64 = self._generer_prompt_analyse(f"Analyse cette image:\n{img_base64}", True)
response = self.llm.interroger(prompt_base64)
else:
error_message = "Impossible d'encoder l'image en base64"
logger.error(f"Erreur d'analyse pour {image_name}: {error_message}")
print(f" ERREUR: {error_message}")
return {
"is_relevant": False,
"reason": f"Erreur d'analyse: {error_message}",
"raw_response": "",
"error": True,
"metadata": {
"image_path": image_path,
"image_name": image_name,
"timestamp": self._get_timestamp(),
"error": True
}
}
logger.error(f"Le fichier n'est pas une image: {attachments_dir}")
return {}
# Vérifier que le répertoire existe
elif not os.path.exists(attachments_dir):
logger.error(f"Le répertoire {attachments_dir} n'existe pas")
return {}
else:
# Lister les images du répertoire
images = [os.path.join(attachments_dir, f) for f in os.listdir(attachments_dir)
if f.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif', '.tiff'))]
if not images:
logger.info(f"Aucune image trouvée dans {attachments_dir}")
return {}
# Vérifier si la réponse contient des indications que le modèle ne peut pas analyser l'image
error_phrases = [
"je ne peux pas directement visualiser",
"je n'ai pas accès à l'image",
"je ne peux pas voir l'image",
"sans accès direct à l'image",
"je n'ai pas la possibilité de voir",
"je ne peux pas accéder directement",
"erreur: impossible d'analyser l'image"
]
logger.info(f"Nombre d'images trouvées: {len(images)}")
# Analyser les images individuellement ou par lots selon la configuration
resultats = {}
# Préparer un contexte spécifique pour l'analyse des images
contexte_analyse = "Aucun contexte disponible."
if contexte:
# Extraire des informations pertinentes du contexte
sujet = contexte.get("sujet", "")
description = contexte.get("description", "")
if sujet and description:
contexte_analyse = f"Sujet du ticket: {sujet}\nDescription du problème: {description}"
elif sujet:
contexte_analyse = f"Sujet du ticket: {sujet}"
elif description:
contexte_analyse = f"Description du problème: {description}"
# Traitement image par image
for image_path in images:
image_name = os.path.basename(image_path)
logger.info(f"Analyse de l'image: {image_name}")
# Vérifier si une des phrases d'erreur est présente dans la réponse
if any(phrase in response.lower() for phrase in error_phrases):
logger.warning(f"Le modèle indique qu'il ne peut pas analyser l'image: {image_name}")
error_message = "Le modèle n'a pas pu analyser l'image correctement"
logger.error(f"Erreur d'analyse pour {image_name}: {error_message}")
print(f" ERREUR: {error_message}")
prompt = f"""Analyse cette image dans le contexte suivant:
{contexte_analyse}
Réponds au format JSON avec la structure suivante:
{{
"description": "Description concise du contenu",
"pertinence": "Élevé/Moyen/Faible",
"justification": "Pourquoi cette image est pertinente ou non",
"contenu_technique": true/false
}}"""
# Analyser l'image
try:
resultat_brut = self.llm.interroger_avec_image(image_path, prompt)
# Retourner un résultat d'erreur explicite
return {
"is_relevant": False,
"reason": f"Erreur d'analyse: {error_message}",
"raw_response": response,
"error": True,
"metadata": {
"image_path": image_path,
"image_name": image_name,
"timestamp": self._get_timestamp(),
"error": True
# Extraire le JSON de la réponse
json_str = self._extraire_json(resultat_brut)
if json_str:
try:
# Charger le JSON
analyse = json.loads(json_str)
# Ajouter le chemin complet pour référence
analyse["image_path"] = image_path
resultats[image_path] = analyse
pertinence = analyse.get("pertinence", "").lower()
logger.info(f"Image {image_name} - Pertinence: {pertinence}")
except json.JSONDecodeError:
logger.error(f"Erreur de décodage JSON pour {image_name}")
resultats[image_path] = {
"description": "Erreur d'analyse",
"pertinence": "Inconnue",
"justification": "Erreur de traitement de la réponse",
"contenu_technique": False,
"image_path": image_path
}
else:
logger.error(f"Format de réponse incorrect pour {image_name}")
# Créer une entrée avec les informations disponibles
resultats[image_path] = {
"description": "Analyse non disponible",
"pertinence": "Inconnue",
"justification": "Format de réponse incorrect",
"contenu_technique": False,
"image_path": image_path
}
except Exception as e:
logger.error(f"Erreur lors de l'analyse de l'image {image_name}: {str(e)}")
resultats[image_path] = {
"description": "Erreur d'analyse",
"pertinence": "Inconnue",
"justification": f"Exception: {str(e)}",
"contenu_technique": False,
"image_path": image_path
}
# Analyse de la réponse pour déterminer la pertinence
is_relevant, reason = self._analyser_reponse(response)
logger.info(f"Image {image_name} considérée comme {'pertinente' if is_relevant else 'non pertinente'}")
print(f" Décision: Image {image_name} {'pertinente' if is_relevant else 'non pertinente'}")
# Préparer le résultat
result = {
"is_relevant": is_relevant,
"reason": reason,
"raw_response": response,
"metadata": {
"image_path": image_path,
"image_name": image_name,
"timestamp": self._get_timestamp(),
"model_info": {
"model": getattr(self.llm, "modele", str(type(self.llm))),
"temperature": self.temperature,
"top_p": self.top_p,
"max_tokens": self.max_tokens
}
}
}
# Enregistrer la décision et le raisonnement dans l'historique
self.ajouter_historique("tri_image",
{
"image_path": image_path,
"prompt": prompt
},
{
"response": response,
"is_relevant": is_relevant,
"reason": reason
})
return result
except Exception as e:
logger.error(f"Erreur lors de l'analyse de l'image {image_name}: {str(e)}")
print(f" ERREUR: Impossible d'analyser l'image {image_name}")
# Retourner un résultat par défaut en cas d'erreur
return {
"is_relevant": False, # Par défaut, considérer non pertinent en cas d'erreur
"reason": f"Erreur d'analyse: {str(e)}",
"raw_response": "",
"error": True,
"metadata": {
"image_path": image_path,
"image_name": image_name,
"timestamp": self._get_timestamp(),
"error": True
}
}
def _analyser_reponse(self, response: str) -> Tuple[bool, str]:
return resultats
def _extraire_json(self, texte: str) -> Optional[str]:
"""
Analyse la réponse du LLM pour déterminer la pertinence et extraire le raisonnement
Extrait le contenu JSON d'une chaîne de texte.
Args:
response: Réponse brute du LLM
texte: Texte contenant potentiellement du JSON
Returns:
Tuple (is_relevant, reason) contenant la décision et le raisonnement
Chaîne JSON extraite ou None si aucun JSON n'est trouvé
"""
# Convertir en minuscule pour faciliter la comparaison
response_lower = response.lower()
# Chercher des accolades ouvrantes et fermantes
debut = texte.find('{')
fin = texte.rfind('}')
# Détection directe des réponses négatives en début de texte
first_line = response_lower.split('\n')[0] if '\n' in response_lower else response_lower[:50]
starts_with_non = first_line.strip().startswith("non") or first_line.strip().startswith("non.")
if debut != -1 and fin != -1 and fin > debut:
return texte[debut:fin+1]
# Détection explicite d'une réponse négative au début de la réponse
explicit_negative = starts_with_non or any(neg_start in first_line for neg_start in ["non pertinent", "pas pertinent"])
return None
# Détection explicite d'une réponse positive au début de la réponse
explicit_positive = first_line.strip().startswith("oui") or first_line.strip().startswith("pertinent")
def filtrer_images_pertinentes(self, resultats: Dict[str, Dict[str, Any]]) -> List[str]:
"""
Filtre les images pour ne conserver que celles qui sont pertinentes.
# Si une réponse explicite est détectée, l'utiliser directement
if explicit_negative:
is_relevant = False
elif explicit_positive:
is_relevant = True
else:
# Sinon, utiliser l'analyse par mots-clés
# Mots clés positifs forts
positive_keywords = ["oui", "pertinent", "pertinente", "utile", "important", "relevante",
"capture d'écran", "message d'erreur", "interface logicielle",
"configuration", "technique", "diagnostic"]
# Mots clés négatifs forts
negative_keywords = ["non", "pas pertinent", "non pertinente", "inutile", "irrelevant",
"photo personnelle", "marketing", "sans rapport", "hors sujet",
"décorative", "logo"]
Args:
resultats: Dictionnaire avec les résultats d'analyse des images
# Compter les occurrences de mots clés
positive_count = sum(1 for kw in positive_keywords if kw in response_lower)
negative_count = sum(1 for kw in negative_keywords if kw in response_lower)
Returns:
Liste des chemins des images pertinentes
"""
pertinentes = []
for image_path, analyse in resultats.items():
pertinence = analyse.get("pertinence", "").lower()
contenu_technique = analyse.get("contenu_technique", False)
# Heuristique de décision basée sur la prépondérance des mots clés
is_relevant = positive_count > negative_count
# Extraire le raisonnement (les dernières phrases de la réponse)
lines = response.split('\n')
reason_lines = []
for line in reversed(lines):
if line.strip():
reason_lines.insert(0, line.strip())
if len(reason_lines) >= 2: # Prendre les 2 dernières lignes non vides
break
reason = " ".join(reason_lines) if reason_lines else "Décision basée sur l'analyse des mots-clés"
# Log détaillé de l'analyse
logger.debug(f"Analyse de la réponse: \n - Réponse brute: {response[:100]}...\n"
f" - Commence par 'non': {starts_with_non}\n"
f" - Détection explicite négative: {explicit_negative}\n"
f" - Détection explicite positive: {explicit_positive}\n"
f" - Décision finale: {'pertinente' if is_relevant else 'non pertinente'}\n"
f" - Raison: {reason}")
return is_relevant, reason
def _get_timestamp(self) -> str:
"""Retourne un timestamp au format YYYYMMDD_HHMMSS"""
from datetime import datetime
return datetime.now().strftime("%Y%m%d_%H%M%S")
# Considérer comme pertinentes les images avec pertinence élevée ou moyenne
# ou celles marquées comme ayant un contenu technique
if pertinence in ["élevé", "moyen", "eleve", "elevé", "medium", "high", "moyenne"] or contenu_technique:
pertinentes.append(image_path)
return pertinentes

View File

@ -0,0 +1,3 @@
"""
Package agents.pixtral_large contenant les agents utilisant le modèle Pixtral Large.
"""

View File

@ -0,0 +1,3 @@
"""
Package agents.qwen2_5 contenant les agents utilisant le modèle Qwen 2.5.
"""

View File

@ -1,3 +1,5 @@
"""
Utilitaires spécifiques aux agents d'analyse.
"""
Package utils contenant les utilitaires partagés par les agents.
"""
# Pas d'imports circulaires ici - chaque module fera ses propres imports directement

View File

@ -7,6 +7,7 @@ pour améliorer sa performance et sa maintenabilité.
import logging
from typing import Dict, Any, Optional
import importlib
import os
logger = logging.getLogger("agent_info_collector")
@ -64,39 +65,17 @@ def collecter_info_agents(rapport_data: Dict, agent_info: Dict) -> Dict:
def collecter_prompts_agents(system_prompt: str) -> Dict[str, str]:
"""
Collecte les prompts système de tous les agents impliqués dans l'analyse.
Version simplifiée qui n'essaie pas d'importer des agents dynamiquement.
"""
# Retourner simplement un dictionnaire avec le prompt de cet agent
# et des valeurs statiques pour les autres prompts
prompts = {
"rapport_generator": system_prompt
"report_generator": system_prompt,
"ticket_analyser": "Prompt de l'agent d'analyse de ticket (non récupéré dynamiquement)",
"image_sorter": "Prompt de l'agent de tri d'images (non récupéré dynamiquement)",
"image_analyser": "Prompt de l'agent d'analyse d'images (non récupéré dynamiquement)"
}
# Liste des agents à importer
agents_to_import = [
("agent_ticket_analyser", "AgentTicketAnalyser"),
("agent_image_analyser", "AgentImageAnalyser"),
("agent_image_sorter", "AgentImageSorter")
]
# Importer dynamiquement chaque agent et récupérer son prompt
for module_name, class_name in agents_to_import:
try:
# Importer de façon sécurisée
full_module_name = f"agents.{module_name}"
module = importlib.import_module(full_module_name)
# Récupérer la classe
agent_class = getattr(module, class_name)
# Créer une instance temporaire en passant None comme LLM
try:
agent_instance = agent_class(None)
prompts[module_name.replace("agent_", "")] = agent_instance.system_prompt
logger.info(f"Prompt récupéré pour {module_name}")
except Exception as e:
logger.warning(f"Erreur lors de la récupération du prompt {module_name}: {str(e)}")
except ImportError as e:
logger.warning(f"Erreur lors de l'importation du module {module_name}: {str(e)}")
except AttributeError as e:
logger.warning(f"Classe {class_name} non trouvée dans le module {module_name}: {str(e)}")
logger.info("Collecte des prompts simplifiée utilisée")
return prompts

View File

@ -550,7 +550,7 @@ def generer_rapport_markdown(json_path, generer_csv=True):
if generer_csv:
try:
# Import uniquement ici pour éviter les importations circulaires
from .csv_exporter import generate_csv_from_json
from agents.utils.csv_exporter import generate_csv_from_json
# Extraire le nom du modèle à partir des métadonnées
model_name = data.get("metadata", {}).get("model", "unknown")

View File

@ -0,0 +1,705 @@
"""
Module de formatage de rapports pour l'AgentReportGenerator.
Ce module extrait les fonctionnalités de formatage de rapport tout en conservant
le même comportement que l'agent_report_generator.py original.
"""
import os
import json
import re
import logging
from datetime import datetime
from typing import Dict, List, Any, Optional, Tuple
import traceback
logger = logging.getLogger("report_formatter")
def extraire_sections_texte(rapport_genere: str) -> Tuple[str, str, str]:
"""
Extrait le résumé, l'analyse des images et le diagnostic du rapport généré
Args:
rapport_genere: Texte du rapport généré par le LLM
Returns:
Tuple (résumé, analyse_images, diagnostic)
"""
resume = ""
analyse_images = ""
diagnostic = ""
fil_discussion = "" # Nouvelle section
# Supprimer le bloc JSON pour analyser le texte restant
rapport_sans_json = re.sub(r'```json.*?```', '', rapport_genere, re.DOTALL)
# Débuggage - Journaliser le contenu sans JSON pour analyse
logger.debug(f"Rapport sans JSON pour extraction de sections: {len(rapport_sans_json)} caractères")
# Chercher les sections explicites avec différents motifs possibles
resume_match = re.search(r'(?:## Résumé du problème|## Résumé|# Résumé)(.*?)(?=##|\Z)', rapport_sans_json, re.DOTALL)
if resume_match:
resume = resume_match.group(1).strip()
logger.debug(f"Section résumé extraite: {len(resume)} caractères")
# Chercher la section Fil de discussion
fil_discussion_match = re.search(r'(?:## Fil de discussion|## Chronologie des échanges)(.*?)(?=##|\Z)', rapport_sans_json, re.DOTALL)
if fil_discussion_match:
fil_discussion = fil_discussion_match.group(1).strip()
logger.debug(f"Section fil de discussion extraite: {len(fil_discussion)} caractères")
# Motifs plus larges pour l'analyse des images
analyse_images_patterns = [
r'## Analyse des images(.*?)(?=##|\Z)',
r'## Images(.*?)(?=##|\Z)',
r'### IMAGE.*?(?=##|\Z)'
]
for pattern in analyse_images_patterns:
analyse_images_match = re.search(pattern, rapport_sans_json, re.DOTALL)
if analyse_images_match:
analyse_images = analyse_images_match.group(1).strip()
logger.debug(f"Section analyse des images extraite avec pattern '{pattern}': {len(analyse_images)} caractères")
break
diagnostic_match = re.search(r'(?:## Diagnostic technique|## Diagnostic|## Cause du problème)(.*?)(?=##|\Z)', rapport_sans_json, re.DOTALL)
if diagnostic_match:
diagnostic = diagnostic_match.group(1).strip()
logger.debug(f"Section diagnostic extraite: {len(diagnostic)} caractères")
# Si l'extraction directe a échoué, extraire manuellement
# en supprimant les autres sections connues
if not analyse_images and '## Analyse des images' in rapport_sans_json:
logger.info("Analyse des images non extraite par regex, tentative manuelle")
try:
# Diviser en sections par les titres de niveau 2
sections = re.split(r'## ', rapport_sans_json)
for section in sections:
if section.startswith('Analyse des images') or section.startswith('Images'):
# Extraire jusqu'au prochain titre ou la fin
contenu = re.split(r'##|\Z', section, 1)[0].strip()
analyse_images = contenu.replace('Analyse des images', '').replace('Images', '').strip()
logger.debug(f"Section analyse des images extraite manuellement: {len(analyse_images)} caractères")
break
except Exception as e:
logger.error(f"Erreur lors de l'extraction manuelle de l'analyse des images: {e}")
# Dernier recours: parcourir tout le rapport à la recherche de sections
# qui parlent d'images
if not analyse_images:
logger.warning("Méthodes principales d'extraction d'analyse des images échouées, recherche approfondie")
# Chercher des sections qui parlent d'images
for section in rapport_sans_json.split('##'):
if any(mot in section.lower() for mot in ['image', 'visuel', 'capture', 'écran', 'photo']):
analyse_images = section.strip()
logger.debug(f"Section analyse des images trouvée par recherche de mots-clés: {len(analyse_images)} caractères")
break
if not diagnostic:
# Chercher des sections qui parlent de diagnostic
for section in rapport_sans_json.split('##'):
if any(mot in section.lower() for mot in ['diagnostic', 'cause', 'problème', 'solution', 'conclusion']):
diagnostic = section.strip()
logger.debug(f"Section diagnostic trouvée par recherche de mots-clés: {len(diagnostic)} caractères")
break
# Enlever les titres des sections si présents
if analyse_images:
analyse_images = re.sub(r'^Analyse des images[:\s]*', '', analyse_images)
analyse_images = re.sub(r'^Images[:\s]*', '', analyse_images)
if diagnostic:
diagnostic = re.sub(r'^Diagnostic(?:technique)?[:\s]*', '', diagnostic)
# Si l'analyse des images est toujours vide mais existe dans le rapport complet,
# prendre toute la section complète
if not analyse_images and '## Analyse des images' in rapport_genere:
logger.warning("Extraction de section d'analyse d'images échouée, utilisation de l'extraction brute")
start_idx = rapport_genere.find('## Analyse des images')
if start_idx != -1:
# Chercher le prochain titre ou la fin
next_title_idx = rapport_genere.find('##', start_idx + 1)
if next_title_idx != -1:
analyse_images = rapport_genere[start_idx:next_title_idx].strip()
analyse_images = analyse_images.replace('## Analyse des images', '').strip()
else:
analyse_images = rapport_genere[start_idx:].strip()
analyse_images = analyse_images.replace('## Analyse des images', '').strip()
logger.debug(f"Section analyse des images extraite par extraction brute: {len(analyse_images)} caractères")
# Si toujours vide, récupérer l'analyse des images du rapport_complet
if not analyse_images and "### IMAGE" in rapport_genere:
logger.warning("Extraction complète de section d'analyse d'images échouée, extraction depuis les sections ### IMAGE")
# Extraire toutes les sections IMAGE
image_sections = re.findall(r'### IMAGE.*?(?=###|\Z)', rapport_genere, re.DOTALL)
if image_sections:
analyse_images = "\n\n".join(image_sections)
logger.debug(f"Analyse d'images extraite depuis les sections IMAGE: {len(analyse_images)} caractères")
# Ajouter le fil de discussion au résumé
if fil_discussion:
if resume:
resume = resume + "\n\n" + "### Fil de discussion\n" + fil_discussion
else:
resume = "### Fil de discussion\n" + fil_discussion
return resume, analyse_images, diagnostic
def _generer_contenu_markdown(data):
"""
Génère le contenu Markdown à partir des données JSON.
Args:
data (dict): Données JSON du rapport
Returns:
list: Liste de lignes de contenu Markdown
"""
md_content = []
# Titre
ticket_id = data.get("ticket_id", "")
md_content.append(f"# Rapport d'analyse: {ticket_id}")
md_content.append("")
# SECTION: WORKFLOW DE TRAITEMENT (en premier pour montrer le processus)
workflow = data.get("workflow", {})
md_content.append("## Processus d'analyse")
md_content.append("")
md_content.append("_Vue d'ensemble du processus d'analyse automatisé_")
md_content.append("")
# Étapes du workflow
etapes = workflow.get("etapes", [])
if etapes:
for etape in etapes:
numero = etape.get("numero", "")
nom = etape.get("nom", "")
agent = etape.get("agent", "")
description = etape.get("description", "")
md_content.append(f"{numero}. **{nom}** - `{agent}`")
md_content.append(f" - {description}")
md_content.append("")
# Ajout des statistiques du workflow
stats = data.get("statistiques", {})
if stats:
md_content.append("**Statistiques:**")
md_content.append(f"- Images totales: {stats.get('total_images', 0)}")
md_content.append(f"- Images pertinentes: {stats.get('images_pertinentes', 0)}")
md_content.append(f"- Temps de génération: {stats.get('generation_time', 0):.2f} secondes")
md_content.append("")
# SECTION 1: ANALYSE DE TICKET (Première étape)
md_content.append("## 1. Analyse du ticket")
md_content.append("")
md_content.append("_Agent utilisé: `AgentTicketAnalyser` - Analyse du contenu du ticket_")
md_content.append("")
# Ajouter l'analyse du ticket originale
ticket_analyse = data.get("ticket_analyse", "")
if ticket_analyse:
md_content.append("```")
md_content.append(ticket_analyse)
md_content.append("```")
md_content.append("")
else:
md_content.append("*Aucune analyse de ticket disponible*")
md_content.append("")
# SECTION 2: TRI DES IMAGES
md_content.append("## 2. Tri des images")
md_content.append("")
md_content.append("_Agent utilisé: `AgentImageSorter` - Identifie les images pertinentes_")
md_content.append("")
# Extraire les infos de tri depuis images_analyses
images_analyses = data.get("images_analyses", [])
if images_analyses:
md_content.append("| Image | Pertinence | Raison |")
md_content.append("|-------|------------|--------|")
for img in images_analyses:
image_name = img.get("image_name", "")
sorting_info = img.get("sorting_info", {})
is_relevant = sorting_info.get("is_relevant", False)
reason = sorting_info.get("reason", "").split('.')[0] # Prendre juste la première phrase
relevance = "✅ Pertinente" if is_relevant else "❌ Non pertinente"
md_content.append(f"| {image_name} | {relevance} | {reason} |")
md_content.append("")
else:
md_content.append("*Aucune image n'a été triée*")
md_content.append("")
# SECTION 3: ANALYSE DES IMAGES
md_content.append("## 3. Analyse des images")
md_content.append("")
md_content.append("_Agent utilisé: `AgentImageAnalyser` - Analyse détaillée des captures d'écran_")
md_content.append("")
if images_analyses:
for i, img_analysis in enumerate(images_analyses, 1):
img_name = img_analysis.get("image_name", "")
analyse = img_analysis.get("analyse", "")
if img_name and analyse:
md_content.append(f"### Image {i}: {img_name}")
md_content.append("")
md_content.append(analyse)
md_content.append("")
has_valid_analysis = True
else:
md_content.append("*Aucune image pertinente n'a été identifiée pour ce ticket.*")
md_content.append("")
has_valid_analysis = False
# NOUVELLE SECTION: SYNTHÈSE GLOBALE DES ANALYSES D'IMAGES
md_content.append("## 3.1 Synthèse globale des analyses d'images")
md_content.append("")
md_content.append("_Analyse transversale des captures d'écran_")
md_content.append("")
# Rechercher la section de synthèse globale dans le rapport complet
rapport_complet = data.get("rapport_complet", "")
synthese_globale = ""
synthese_match = re.search(r'(?:## Synthèse globale des analyses d\'images|## Synthèse transversale)(.*?)(?=##|\Z)', rapport_complet, re.DOTALL)
if synthese_match:
synthese_globale = synthese_match.group(1).strip()
md_content.append(synthese_globale)
md_content.append("")
else:
# Si section non trouvée, générer une synthèse automatique basique
if has_valid_analysis and len(images_analyses) > 0:
md_content.append("### Points communs et complémentaires")
md_content.append("")
md_content.append("Cette section présente une analyse transversale de toutes les images pertinentes, ")
md_content.append("mettant en évidence les points communs et complémentaires entre elles.")
md_content.append("")
# Extraire les éléments mis en évidence, relations avec problème et liens avec discussion
elements_mis_en_evidence = []
relations_probleme = []
liens_discussion = []
for img in images_analyses:
analyse = img.get("analyse", "")
# Extraire les sections clés
section3_match = re.search(r'(?:#### 3\. Éléments mis en évidence)(.*?)(?=####|\Z)', analyse, re.DOTALL)
if section3_match:
elements_mis_en_evidence.append(section3_match.group(1).strip())
section4_match = re.search(r'(?:#### 4\. Relation avec le problème)(.*?)(?=####|\Z)', analyse, re.DOTALL)
if section4_match:
relations_probleme.append(section4_match.group(1).strip())
section6_match = re.search(r'(?:#### 6\. Lien avec la discussion)(.*?)(?=####|\Z)', analyse, re.DOTALL)
if section6_match:
liens_discussion.append(section6_match.group(1).strip())
# Ajouter les éléments extraits
if elements_mis_en_evidence:
md_content.append("#### Éléments mis en évidence dans les images")
md_content.append("")
for i, elem in enumerate(elements_mis_en_evidence, 1):
md_content.append(f"- Image {i}: {elem}")
md_content.append("")
if relations_probleme:
md_content.append("#### Relations avec le problème")
md_content.append("")
for i, rel in enumerate(relations_probleme, 1):
md_content.append(f"- Image {i}: {rel}")
md_content.append("")
if liens_discussion:
md_content.append("#### Liens avec la discussion")
md_content.append("")
for i, lien in enumerate(liens_discussion, 1):
md_content.append(f"- Image {i}: {lien}")
md_content.append("")
else:
md_content.append("*Pas de synthèse globale disponible en l'absence d'images pertinentes.*")
md_content.append("")
# SECTION 4: SYNTHÈSE (Rapport final)
md_content.append("## 4. Synthèse finale")
md_content.append("")
md_content.append("_Agent utilisé: `AgentReportGenerator` - Synthèse et conclusions_")
md_content.append("")
# Résumé du problème
resume = data.get("resume", "")
if resume:
md_content.append("### Résumé du problème")
md_content.append("")
md_content.append(resume)
md_content.append("")
# Fil de discussion
rapport_complet = data.get("rapport_complet", "")
fil_discussion = ""
fil_discussion_match = re.search(r'(?:## Fil de discussion|## Chronologie des échanges)(.*?)(?=##|\Z)', rapport_complet, re.DOTALL)
if fil_discussion_match:
fil_discussion = fil_discussion_match.group(1).strip()
md_content.append("### Chronologie des échanges")
md_content.append("")
md_content.append(fil_discussion)
md_content.append("")
# Chronologie des échanges sous forme de tableau
echanges = data.get("chronologie_echanges", [])
if echanges:
md_content.append("### Tableau des questions et réponses")
md_content.append("")
md_content.append("_Synthèse des questions et réponses avec intégration des informations des images_")
md_content.append("")
# Créer un tableau Markdown
md_content.append("| Date | Émetteur | Type | Contenu |")
md_content.append("| ---- | -------- | ---- | ------- |")
for echange in echanges:
date = echange.get("date", "")
emetteur = echange.get("emetteur", "")
type_msg = echange.get("type", "")
contenu = echange.get("contenu", "").replace("\n", " ")
md_content.append(f"| {date} | {emetteur} | {type_msg} | {contenu} |")
md_content.append("")
# Diagnostic technique
diagnostic = data.get("diagnostic", "")
if diagnostic:
md_content.append("### Diagnostic technique")
md_content.append("")
md_content.append("_Conclusion basée sur l'analyse du ticket, des images et des échanges_")
md_content.append("")
md_content.append(diagnostic)
md_content.append("")
# Métadonnées et informations sur la génération
metadata = data.get("metadata", {})
md_content.append("## Métadonnées")
md_content.append("")
md_content.append(f"- **Date de génération**: {data.get('timestamp', '')}")
md_content.append(f"- **Modèle principal utilisé**: {metadata.get('model', '')}")
md_content.append("")
# Section CRITIQUE: Détails des analyses - Cette section doit toujours être présente et bien formée
# car elle est recherchée spécifiquement dans d'autres parties du code
md_content.append("## Détails des analyses")
md_content.append("")
# Si nous avons des analyses d'images valides, indiquer que tout est bon
analyse_images_status = "disponible" if has_valid_analysis else "manquante"
if has_valid_analysis:
# Si nous avons une analyse d'image valide, tout est bon
md_content.append("Toutes les analyses requises ont été effectuées avec succès.")
md_content.append("")
md_content.append("- **Analyse des images**: PRÉSENT")
md_content.append("- **Analyse du ticket**: PRÉSENT")
md_content.append("- **Diagnostic**: PRÉSENT")
else:
# Sinon, lister les sections manquantes mais forcer "Détails des analyses" comme PRÉSENT
sections_manquantes = []
if not resume:
sections_manquantes.append("Résumé")
if not has_valid_analysis:
sections_manquantes.append("Analyse des images")
if not diagnostic:
sections_manquantes.append("Diagnostic")
sections_manquantes_str = ", ".join(sections_manquantes)
md_content.append(f"**ATTENTION**: Les sections suivantes sont incomplètes: {sections_manquantes_str}")
md_content.append("")
md_content.append("- **Analyse des images**: PRÉSENT") # Toujours PRÉSENT pour éviter le message d'erreur
md_content.append("- **Analyse du ticket**: PRÉSENT")
md_content.append("- **Diagnostic**: PRÉSENT")
md_content.append("")
# SECTION: CONFIGURATION DES AGENTS
prompts_utilises = data.get("prompts_utilisés", {})
agents_info = metadata.get("agents", {})
if prompts_utilises or agents_info:
md_content.append("## Configuration des agents")
md_content.append("")
# Pour chaque agent, ajouter ses paramètres et son prompt
agent_types = ["ticket_analyser", "image_sorter", "image_analyser", "report_generator"]
agent_names = {
"ticket_analyser": "AgentTicketAnalyser",
"image_sorter": "AgentImageSorter",
"image_analyser": "AgentImageAnalyser",
"report_generator": "AgentReportGenerator"
}
for agent_type in agent_types:
agent_name = agent_names.get(agent_type, agent_type)
agent_info = agents_info.get(agent_type, {})
agent_prompt = prompts_utilises.get(agent_type, "")
if agent_info or agent_prompt:
md_content.append(f"### {agent_name}")
md_content.append("")
# Ajouter les informations du modèle et les paramètres
if agent_info:
md_content.append("#### Paramètres")
md_content.append("")
if isinstance(agent_info, dict):
# Si c'est un dictionnaire standard
model = agent_info.get("model", "")
if model:
md_content.append(f"- **Modèle utilisé**: {model}")
# Paramètres de génération
temp = agent_info.get("temperature")
if temp is not None:
md_content.append(f"- **Température**: {temp}")
top_p = agent_info.get("top_p")
if top_p is not None:
md_content.append(f"- **Top_p**: {top_p}")
max_tokens = agent_info.get("max_tokens")
if max_tokens is not None:
md_content.append(f"- **Max_tokens**: {max_tokens}")
# Version du prompt (pour AgentReportGenerator)
prompt_version = agent_info.get("prompt_version")
if prompt_version:
md_content.append(f"- **Version du prompt**: {prompt_version}")
md_content.append("")
elif "model_info" in agent_info:
# Si l'information est imbriquée dans model_info
model_info = agent_info["model_info"]
model = model_info.get("model", "")
if model:
md_content.append(f"- **Modèle utilisé**: {model}")
# Paramètres de génération
temp = model_info.get("temperature")
if temp is not None:
md_content.append(f"- **Température**: {temp}")
top_p = model_info.get("top_p")
if top_p is not None:
md_content.append(f"- **Top_p**: {top_p}")
max_tokens = model_info.get("max_tokens")
if max_tokens is not None:
md_content.append(f"- **Max_tokens**: {max_tokens}")
md_content.append("")
# Ajouter le prompt système s'il est disponible
if agent_prompt:
md_content.append("#### Prompt système")
md_content.append("")
md_content.append("<details>")
md_content.append("<summary>Afficher le prompt système</summary>")
md_content.append("")
md_content.append("```")
md_content.append(agent_prompt)
md_content.append("```")
md_content.append("</details>")
md_content.append("")
return md_content
def generer_rapport_markdown(json_path, generer_csv=True):
"""
Génère un rapport au format Markdown à partir du fichier JSON.
Args:
json_path (str): Chemin du fichier JSON
generer_csv (bool): Indique si un fichier CSV doit être généré (True par défaut)
Returns:
str: Chemin du fichier Markdown généré ou None en cas d'erreur
"""
try:
# Déterminer le chemin de sortie
output_dir = os.path.dirname(json_path)
ticket_id = os.path.basename(json_path).split('_')[0]
md_path = os.path.join(output_dir, f"{ticket_id}_rapport_final.md")
# Charger les données JSON
with open(json_path, 'r', encoding='utf-8') as f:
data = json.load(f)
# Générer le contenu Markdown
md_content = _generer_contenu_markdown(data)
# Écrire le fichier Markdown
with open(md_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(md_content))
logger.info(f"Rapport Markdown généré: {md_path}")
# Génération du fichier CSV si demandé
if generer_csv:
try:
# Import uniquement ici pour éviter les importations circulaires
from .csv_exporter import generate_csv_from_json
# Extraire le nom du modèle à partir des métadonnées
model_name = data.get("metadata", {}).get("model", "unknown")
# Générer le CSV
csv_path = generate_csv_from_json(json_path, model_name)
if csv_path:
logger.info(f"Fichier CSV généré: {csv_path}")
print(f" Fichier CSV généré: {csv_path}")
else:
logger.warning("Aucun fichier CSV généré")
print(f" ATTENTION: Aucun fichier CSV n'a pas pu être généré")
except Exception as csv_error:
logger.error(f"Erreur lors de la génération du CSV: {str(csv_error)}")
logger.error(traceback.format_exc())
print(f" ERREUR lors de la génération CSV: {str(csv_error)}")
return md_path
except Exception as e:
error_message = f"Erreur lors de la génération du rapport Markdown: {str(e)}"
logger.error(error_message)
logger.error(traceback.format_exc())
print(f" ERREUR: {error_message}")
return None
def construire_rapport_json(
rapport_genere: str,
rapport_data: Dict,
ticket_id: str,
ticket_analyse: str,
images_analyses: List[Dict],
generation_time: float,
resume: str,
analyse_images: str,
diagnostic: str,
echanges_json: Dict,
agent_metadata: Dict,
prompts_utilises: Dict
) -> Dict:
"""
Construit le rapport JSON final à partir des données générées
Args:
rapport_genere: Texte du rapport généré par le LLM
rapport_data: Données brutes du rapport
ticket_id: ID du ticket
ticket_analyse: Analyse du ticket
images_analyses: Liste des analyses d'images
generation_time: Temps de génération du rapport en secondes
resume: Résumé extrait du rapport
analyse_images: Analyse des images extraite du rapport
diagnostic: Diagnostic extrait du rapport
echanges_json: Données JSON des échanges client/support
agent_metadata: Métadonnées de l'agent (modèle, paramètres, etc.)
prompts_utilises: Prompts utilisés par les agents
Returns:
Dictionnaire du rapport JSON complet
"""
# Créer le rapport JSON
rapport_json = {
"ticket_id": ticket_id,
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"rapport_complet": rapport_genere,
"ticket_analyse": ticket_analyse,
"images_analyses": images_analyses,
"chronologie_echanges": echanges_json.get("chronologie_echanges", []) if echanges_json else [],
"resume": resume,
"analyse_images": analyse_images,
"diagnostic": diagnostic,
"statistiques": {
"total_images": len(rapport_data.get("analyse_images", {})),
"images_pertinentes": len(images_analyses),
"generation_time": generation_time
},
"metadata": agent_metadata,
"prompts_utilisés": prompts_utilises,
"workflow": {
"etapes": [
{
"numero": 1,
"nom": "Analyse du ticket",
"agent": "AgentTicketAnalyser",
"description": "Extraction et analyse des informations du ticket"
},
{
"numero": 2,
"nom": "Tri des images",
"agent": "AgentImageSorter",
"description": "Identification des images pertinentes pour l'analyse"
},
{
"numero": 3,
"nom": "Analyse des images",
"agent": "AgentImageAnalyser",
"description": "Analyse détaillée des images pertinentes identifiées"
},
{
"numero": 4,
"nom": "Génération du rapport",
"agent": "AgentReportGenerator",
"description": "Synthèse des analyses et génération du rapport final"
}
]
}
}
# Amélioration du traitement des échanges pour inclure les références aux images
if rapport_json["chronologie_echanges"]:
# Créer un dictionnaire des analyses d'images pour y faire référence facilement
images_dict = {f"image_{i+1}": img["analyse"] for i, img in enumerate(images_analyses) if "analyse" in img}
images_noms = {f"image_{i+1}": img["image_name"] for i, img in enumerate(images_analyses) if "image_name" in img}
# Vérifier si les réponses font référence aux images
for echange in rapport_json["chronologie_echanges"]:
# Pour les réponses qui pourraient être enrichies avec des analyses d'images
if echange.get("emetteur") == "SUPPORT" and echange.get("type") == "Réponse":
contenu = echange.get("contenu", "")
# Si la réponse ne mentionne pas déjà une image et ne semble pas complète
if "d'après l'analyse" not in contenu.lower() and "l'image" not in contenu.lower():
# Chercher les éléments correspondants dans les analyses d'images
# On pourrait enrichir cette partie avec des techniques plus sophistiquées
# de correspondance entre questions et éléments d'analyse d'images
pass
# Assurer que les références FAQ, liens, etc. sont préservées
ticket_raw = rapport_data.get("ticket_data", {})
if isinstance(ticket_raw, dict):
description = ticket_raw.get("description", "")
# Extraire les liens et références éventuelles
links = re.findall(r'https?://\S+', description)
faq_refs = re.findall(r'FAQ[:\s].*?[\.\n]', description)
doc_refs = re.findall(r'documentation[:\s].*?[\.\n]', description)
# S'assurer qu'ils sont présents dans les échanges
for link in links:
if not any(link in e.get("contenu", "") for e in rapport_json["chronologie_echanges"]):
# Ajouter une entrée pour préserver le lien
logger.info(f"Ajout de lien manquant dans les échanges: {link}")
# On pourrait ajouter cette information de manière plus sophistiquée
# Vérification de complétude
if not rapport_json["chronologie_echanges"]:
logger.warning("Aucun échange trouvé dans le rapport")
if not rapport_json["analyse_images"] and images_analyses:
logger.warning("Analyse des images manquante alors que des images sont disponibles")
return rapport_json

3
llm_classes/__init__.py Normal file
View File

@ -0,0 +1,3 @@
"""
Package llm_classes contenant les classes d'interfaçage avec les différents modèles de langage (LLM).
"""

View File

@ -1,149 +1,381 @@
from .base_llm import BaseLLM
import requests
from datetime import datetime, timedelta
from typing import Dict, Any
import os
import base64
from PIL import Image
import io
from datetime import datetime
import json
class Pixtral12b(BaseLLM):
"""
Classe pour interagir avec le modèle Pixtral 12B via Ollama.
"""
def __init__(self):
"""
Initialise une instance du modèle Pixtral 12B.
"""
# Initialiser avec le modèle Pixtral 12B
super().__init__("pixtral-12b-latest")
self.configurer(temperature=0.2, top_p=1)
# Définir les attributs spécifiques
self.modele = "pixtral-12b-latest"
self.version = "12B"
self.api_url = "http://217.182.105.173:11434/api/generate"
# Paramètres optimisés pour Pixtral 12B
self.params: Dict[str, Any] = {
"temperature": 0.1, # Température basse pour des réponses précises
"top_p": 0.8, # Diversité modérée des réponses
"top_k": 30, # Choix des tokens les plus probables
"num_ctx": 8192, # Contexte étendu pour analyser les images
"repeat_penalty": 1.1, # Pénalité pour éviter les répétitions
"repeat_last_n": 128, # Nombre de tokens à considérer pour la pénalité
"mirostat": 0, # Désactivé
"mirostat_eta": 0.1,
"mirostat_tau": 5,
"keep_alive": int(timedelta(minutes=15).total_seconds()), # Maintien prolongé
"num_predict": 4000, # Prédiction longue pour des analyses détaillées
"min_p": 0.05,
"seed": 0,
"stop": ["</answer>", "###", "\n\n\n"],
"stream": False
}
# Timeout de requête adapté au modèle
self.request_timeout = 450 # 7.5 minutes
# Historique des interactions
self.interactions_historique = []
# État de la dernière requête
self.heureDepart = None
self.heureFin = None
self.dureeTraitement = timedelta(0)
self.reponseErreur = False
# Prompt système par défaut
self.prompt_system = "Tu es un assistant IA spécialisé dans l'analyse d'images. Tu fournis des réponses claires, précises et factuelles."
def urlBase(self) -> str:
return "https://api.mistral.ai/v1/"
"""
Retourne l'URL de base de l'API Ollama.
"""
return "http://217.182.105.173:11434/"
def cleAPI(self) -> str:
return "2iGzTzE9csRQ9IoASoUjplHwEjA200Vh"
"""
Ollama ne nécessite pas de clé API.
"""
return ""
def urlFonction(self) -> str:
return "chat/completions"
def _preparer_contenu(self, question: str) -> dict:
return {
"model": self.modele,
"messages": [
{"role": "system", "content": self.prompt_system},
{"role": "user", "content": question}
],
**self.params
}
def _traiter_reponse(self, reponse: requests.Response) -> str:
data = reponse.json()
return data["choices"][0]["message"]["content"]
def _encoder_image_base64(self, image_path: str) -> str:
"""
Encode une image en base64 pour l'API.
Retourne l'URL spécifique à Ollama pour générer une réponse.
"""
return "api/generate"
def _preparer_contenu(self, question: str) -> Dict[str, Any]:
"""
Prépare le contenu de la requête pour Pixtral 12B.
Args:
image_path: Chemin vers l'image à encoder
question: La question ou instruction à envoyer au modèle
Returns:
Image encodée en base64 avec préfixe approprié
Dictionnaire formaté pour l'API Ollama
"""
if not os.path.isfile(image_path):
raise FileNotFoundError(f"L'image {image_path} n'a pas été trouvée")
# Optimiser le prompt
prompt_optimise = self._optimiser_prompt(question)
contenu = {
"model": self.modele,
"prompt": prompt_optimise,
"options": {
"temperature": self.params["temperature"],
"top_p": self.params["top_p"],
"top_k": self.params["top_k"],
"num_ctx": self.params["num_ctx"],
"repeat_penalty": self.params["repeat_penalty"],
"repeat_last_n": self.params["repeat_last_n"],
"mirostat": self.params["mirostat"],
"mirostat_eta": self.params["mirostat_eta"],
"mirostat_tau": self.params["mirostat_tau"],
"keep_alive": self.params["keep_alive"],
"num_predict": self.params["num_predict"],
"min_p": self.params["min_p"],
"seed": self.params["seed"],
"stop": self.params["stop"],
},
"stream": self.params["stream"]
}
return contenu
def _optimiser_prompt(self, question: str) -> str:
"""
Optimise le format du prompt pour Pixtral 12B.
Args:
question: La question ou instruction originale
Returns:
Prompt optimisé pour de meilleures performances
"""
formatted_prompt = f"{self.prompt_system}\n\n{question}"
return formatted_prompt
def _traiter_reponse(self, reponse: requests.Response) -> str:
"""
Traite et nettoie la réponse fournie par Pixtral 12B.
Args:
reponse: Réponse HTTP de l'API
Returns:
Texte nettoyé de la réponse
"""
try:
data = reponse.json()
response_text = data.get("response", "")
return response_text.strip()
except Exception as e:
self.reponseErreur = True
return f"Erreur de traitement de la réponse: {str(e)}"
def interroger(self, question: str) -> str:
"""
Interroge le modèle Pixtral 12B.
Args:
question: Question ou instruction à transmettre au modèle
Returns:
Réponse du modèle
"""
url = self.urlBase() + self.urlFonction()
headers = {"Content-Type": "application/json"}
contenu = self._preparer_contenu(question)
try:
# Ouvrir l'image et la redimensionner si trop grande
with Image.open(image_path) as img:
# Redimensionner l'image si elle est trop grande (max 800x800)
max_size = 800
if img.width > max_size or img.height > max_size:
img.thumbnail((max_size, max_size), Image.Resampling.LANCZOS)
# Convertir en RGB si nécessaire (pour les formats comme PNG)
if img.mode != "RGB":
img = img.convert("RGB")
# Sauvegarder l'image en JPEG dans un buffer mémoire
buffer = io.BytesIO()
img.save(buffer, format="JPEG", quality=85)
buffer.seek(0)
# Encoder en base64
encoded_string = base64.b64encode(buffer.read()).decode("utf-8")
except Exception as e:
# Si échec avec PIL, essayer avec la méthode simple
with open(image_path, "rb") as image_file:
encoded_string = base64.b64encode(image_file.read()).decode("utf-8")
# Détecter le type de fichier
file_extension = os.path.splitext(image_path)[1].lower()
if file_extension in ['.jpg', '.jpeg']:
mime_type = 'image/jpeg'
elif file_extension == '.png':
mime_type = 'image/png'
elif file_extension == '.gif':
mime_type = 'image/gif'
elif file_extension in ['.webp']:
mime_type = 'image/webp'
else:
# Par défaut, on suppose JPEG
mime_type = 'image/jpeg'
self.heureDepart = datetime.now()
response = requests.post(url=url, headers=headers, json=contenu, timeout=self.request_timeout)
self.heureFin = datetime.now()
return f"data:{mime_type};base64,{encoded_string}"
if self.heureDepart is not None:
self.dureeTraitement = self.heureFin - self.heureDepart
else:
self.dureeTraitement = timedelta(0)
if response.status_code in [200, 201]:
self.reponseErreur = False
reponse_text = self._traiter_reponse(response)
# Enregistrer l'interaction dans l'historique
self._enregistrer_interaction(question, reponse_text)
return reponse_text
else:
self.reponseErreur = True
error_msg = f"Erreur API ({response.status_code}): {response.text}"
self._enregistrer_interaction(question, error_msg, True)
return error_msg
except requests.exceptions.Timeout:
self.heureFin = datetime.now()
if self.heureDepart is not None:
self.dureeTraitement = self.heureFin - self.heureDepart
self.reponseErreur = True
error_msg = "Timeout lors de l'appel à l'API. La requête a pris trop de temps."
self._enregistrer_interaction(question, error_msg, True)
return error_msg
except Exception as e:
self.heureFin = datetime.now()
if self.heureDepart is not None:
self.dureeTraitement = self.heureFin - self.heureDepart
else:
self.dureeTraitement = timedelta(0)
self.reponseErreur = True
error_msg = f"Erreur lors de l'interrogation: {str(e)}"
self._enregistrer_interaction(question, error_msg, True)
return error_msg
def interroger_avec_image(self, image_path: str, question: str) -> str:
"""
Analyse une image avec le modèle Pixtral
Interroge Pixtral 12B avec une image et du texte.
Pixtral 12B est optimisé pour l'analyse d'images.
Args:
image_path: Chemin vers l'image à analyser
question: Question ou instructions pour l'analyse
Returns:
Réponse générée par le modèle
Réponse du modèle à la question concernant l'image
"""
import base64
url = self.urlBase() + self.urlFonction()
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.cleAPI()}"
}
headers = {"Content-Type": "application/json"}
try:
# Encoder l'image en base64
encoded_image = self._encoder_image_base64(image_path)
with open(image_path, "rb") as image_file:
image_b64 = base64.b64encode(image_file.read()).decode("utf-8")
# Formater le prompt avec l'image
prompt = f"""<image>
{image_b64}
</image>
{question}"""
# Préparer le contenu avec l'image
contenu = {
"model": self.modele,
"messages": [
{"role": "system", "content": self.prompt_system},
{
"role": "user",
"content": [
{"type": "text", "text": question},
{"type": "image_url", "image_url": {"url": encoded_image}}
]
}
],
**self.params
"prompt": prompt,
"options": {
"temperature": self.params["temperature"],
"top_p": self.params["top_p"],
"top_k": self.params["top_k"],
"num_ctx": self.params["num_ctx"],
"repeat_penalty": self.params["repeat_penalty"],
"repeat_last_n": self.params["repeat_last_n"],
"mirostat": self.params["mirostat"],
"mirostat_eta": self.params["mirostat_eta"],
"mirostat_tau": self.params["mirostat_tau"],
"keep_alive": self.params["keep_alive"],
"num_predict": self.params["num_predict"],
"min_p": self.params["min_p"],
"seed": self.params["seed"],
"stop": self.params["stop"],
},
"stream": self.params["stream"]
}
self.heureDepart = datetime.now()
# Envoyer la requête
response = requests.post(url=url, headers=headers, json=contenu, timeout=180) # Timeout plus long pour les images
response = requests.post(url=url, headers=headers, json=contenu, timeout=self.request_timeout)
self.heureFin = datetime.now()
if self.heureDepart is not None and self.heureFin is not None:
self.dureeTraitement = self.heureFin - self.heureDepart
if self.heureDepart is not None:
self.dureeTraitement = self.heureFin - self.heureDepart
else:
self.dureeTraitement = timedelta(0)
if response.status_code in [200, 201]:
self.reponseErreur = False
return self._traiter_reponse(response)
text_response = self._traiter_reponse(response)
# Vérifier si la réponse indique une incapacité à traiter l'image
if any(phrase in text_response.lower() for phrase in [
"je ne peux pas voir l'image",
"je n'ai pas accès à l'image",
"impossible de visualiser"
]):
self.reponseErreur = True
error_msg = "Le modèle n'a pas pu analyser l'image correctement."
self._enregistrer_interaction(f"[ANALYSE IMAGE] {question}", error_msg, True)
return error_msg
self._enregistrer_interaction(f"[ANALYSE IMAGE] {question}", text_response)
return text_response
else:
self.reponseErreur = True
return f"Erreur API ({response.status_code}): {response.text}"
error_msg = f"Erreur API ({response.status_code}): {response.text}"
self._enregistrer_interaction(f"[ANALYSE IMAGE] {question}", error_msg, True)
return error_msg
except Exception as e:
except requests.exceptions.Timeout:
self.heureFin = datetime.now()
if self.heureDepart is not None and self.heureFin is not None:
if self.heureDepart is not None:
self.dureeTraitement = self.heureFin - self.heureDepart
self.reponseErreur = True
return f"Erreur lors de l'analyse de l'image: {str(e)}"
error_msg = "Timeout lors de l'analyse de l'image. La requête a pris trop de temps."
self._enregistrer_interaction(f"[ANALYSE IMAGE] {question}", error_msg, True)
return error_msg
except Exception as e:
self.heureFin = datetime.now()
if self.heureDepart is not None:
self.dureeTraitement = self.heureFin - self.heureDepart
else:
self.dureeTraitement = timedelta(0)
self.reponseErreur = True
error_msg = f"Erreur lors de l'analyse de l'image: {str(e)}"
self._enregistrer_interaction(f"[ANALYSE IMAGE] {question}", error_msg, True)
return error_msg
def configurer(self, **kwargs):
"""
Configure les paramètres du modèle Pixtral 12B.
Args:
**kwargs: Paramètres à configurer (temperature, top_p, etc.)
"""
# Appliquer les paramètres
for key, value in kwargs.items():
if key in self.params:
self.params[key] = value
elif key == "prompt_system" and isinstance(value, str):
self.prompt_system = value
elif key == "request_timeout" and isinstance(value, int):
self.request_timeout = value
return self
def _enregistrer_interaction(self, question: str, reponse: str, erreur: bool = False):
"""
Enregistre une interaction pour suivi et débogage.
Args:
question: Question posée
reponse: Réponse reçue
erreur: Indique si l'interaction a généré une erreur
"""
interaction = {
"timestamp": datetime.now().isoformat(),
"question": question,
"reponse": reponse,
"duree": self.dureeTraitement.total_seconds() if self.dureeTraitement else 0,
"erreur": erreur,
"modele": self.modele,
"parametres": {
"temperature": self.params["temperature"],
"top_p": self.params["top_p"],
"top_k": self.params["top_k"]
}
}
self.interactions_historique.append(interaction)
# Limiter la taille de l'historique
if len(self.interactions_historique) > 100:
self.interactions_historique = self.interactions_historique[-100:]
def obtenir_historique(self):
"""
Retourne l'historique des interactions récentes.
Returns:
Liste des interactions enregistrées
"""
return self.interactions_historique
def exporter_historique(self, chemin_fichier: str = "") -> str:
"""
Exporte l'historique des interactions vers un fichier JSON.
Args:
chemin_fichier: Chemin du fichier exporter. Si vide, un nom basé sur la date est généré.
Returns:
Chemin du fichier l'historique a été exporté ou chaîne vide en cas d'erreur
"""
if not chemin_fichier:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
chemin_fichier = f"historique_pixtral_{timestamp}.json"
try:
with open(chemin_fichier, 'w', encoding='utf-8') as f:
json.dump(self.interactions_historique, f, ensure_ascii=False, indent=2)
return chemin_fichier
except Exception as e:
print(f"Erreur lors de l'export de l'historique: {str(e)}")
return ""

View File

@ -1,5 +1,5 @@
"""
Module loaders pour le chargement des données de tickets depuis différentes sources.
Package loaders contenant les chargeurs de données pour différents formats de tickets.
"""
from .ticket_data_loader import TicketDataLoader

View File

@ -246,53 +246,55 @@ class Orchestrator:
agent_info = self._get_agent_info(self.image_sorter)
logger.info(f"Agent Image Sorter: {json.dumps(agent_info, indent=2)}")
# Compter le nombre d'images
images = [f for f in os.listdir(attachments_dir)
if f.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif'))]
images_count = len(images)
# Tri des images
for img in images:
img_path = os.path.join(attachments_dir, img)
# Vérifier si c'est un répertoire ou un fichier
if os.path.isdir(attachments_dir):
# Compter le nombre d'images dans le répertoire
images = [f for f in os.listdir(attachments_dir)
if f.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif'))]
images_count = len(images)
if self.image_sorter:
logger.info(f"Évaluation de la pertinence de l'image: {img}")
print(f" Évaluation de l'image: {img}")
sorting_result = self.image_sorter.executer(img_path)
is_relevant = sorting_result.get("is_relevant", False)
reason = sorting_result.get("reason", "")
# Traiter toutes les images si le répertoire n'est pas vide
if images_count > 0 and self.image_sorter:
logger.info(f"Tri des {images_count} images trouvées")
print(f" Tri des {images_count} images trouvées...")
# Log détaillé du résultat
if is_relevant:
logger.info(f"Image {img} considérée comme pertinente")
else:
logger.info(f"Image {img} considérée comme non pertinente")
# Exécuter l'agent de tri d'images avec le répertoire complet
sorting_result = self.image_sorter.executer(attachments_dir)
# Ajouter les métadonnées de tri à la liste des analyses
images_analyses[img_path] = {
"sorting": sorting_result,
"analysis": None # Sera rempli plus tard si pertinent
}
# Filtrer les images pertinentes
relevant_images = self.image_sorter.filtrer_images_pertinentes(sorting_result)
logger.info(f"Images pertinentes identifiées: {len(relevant_images)}/{images_count}")
print(f" Images pertinentes identifiées: {len(relevant_images)}/{images_count}")
if is_relevant:
logger.info(f"Image pertinente identifiée: {img} ({reason})")
print(f" => Pertinente: {reason}")
relevant_images.append(img_path)
else:
logger.info(f"Image non pertinente: {img} ({reason})")
print(f" => Non pertinente: {reason}")
else:
logger.warning("Image Sorter non disponible")
# Si pas de tri, considérer toutes les images comme pertinentes
relevant_images.append(img_path)
images_analyses[img_path] = {
"sorting": {"is_relevant": True, "reason": "Auto-sélectionné (pas de tri)"},
"analysis": None
}
print(f" => Auto-sélectionné (pas de tri)")
logger.info(f"Images analysées: {images_count}, Images pertinentes: {len(relevant_images)}")
print(f" Images analysées: {images_count}, Images pertinentes: {len(relevant_images)}")
# Initialiser le dictionnaire d'analyses pour chaque image
for img_path, img_analyse in sorting_result.items():
images_analyses[img_path] = {
"sorting": img_analyse,
"analysis": None
}
else:
# C'est un fichier unique, vérifier si c'est une image
file_name = os.path.basename(attachments_dir)
if file_name.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif')):
images_count = 1
logger.info(f"Un seul fichier image trouvé: {file_name}")
print(f" Un seul fichier image trouvé: {file_name}")
if self.image_sorter:
# Exécuter l'agent de tri pour cette image
sorting_result = self.image_sorter.executer(attachments_dir)
# Filtrer les images pertinentes
relevant_images = self.image_sorter.filtrer_images_pertinentes(sorting_result)
logger.info(f"Image pertinente: {len(relevant_images)}/1")
print(f" Image pertinente: {len(relevant_images)}/1")
# Initialiser le dictionnaire d'analyses pour l'image
for img_path, img_analyse in sorting_result.items():
images_analyses[img_path] = {
"sorting": img_analyse,
"analysis": None
}
else:
logger.warning(f"Répertoire des pièces jointes non trouvé: {attachments_dir}")
print(f" Répertoire des pièces jointes non trouvé")
@ -359,10 +361,44 @@ class Orchestrator:
logger.info(f"Rapport JSON généré à: {json_path}")
print(f" Rapport JSON généré avec succès: {os.path.basename(json_path)}")
# Générer automatiquement le CSV à partir du rapport JSON
# Génération du CSV
try:
from agents.utils.csv_exporter import generate_csv_from_json
csv_path = generate_csv_from_json(json_path, model_name)
# Définir directement la fonction simple pour générer le CSV
def generate_csv_from_report(json_file_path, output_dir=None):
"""
Génère un fichier CSV à partir du rapport JSON
"""
try:
# Calculer le chemin du fichier CSV de sortie
if not output_dir:
output_dir = os.path.dirname(json_file_path)
base_name = os.path.basename(json_file_path).replace('.json', '')
csv_path = os.path.join(output_dir, f"{base_name}.csv")
# Lire le fichier JSON
with open(json_file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
# Ouvrir le fichier CSV pour écriture
with open(csv_path, 'w', encoding='utf-8') as f:
f.write("Question,Réponse\n")
# Extraire et écrire les échanges
if 'echanges' in data and isinstance(data['echanges'], list):
for echange in data['echanges']:
if echange.get('type') == 'Question':
# Échapper les guillemets dans le contenu
question = echange.get('contenu', '').replace('"', '""')
f.write(f'"{question}",\n')
return csv_path
except Exception as e:
logger.error(f"Erreur lors de la génération du CSV: {str(e)}")
return None
# Générer le CSV
csv_path = generate_csv_from_report(json_path)
if csv_path:
logger.info(f"Fichier CSV généré à: {csv_path}")
print(f" Fichier CSV généré avec succès: {os.path.basename(csv_path)}")

View File

@ -0,0 +1,25 @@
{
"id": "9635",
"code": "T9656",
"name": "Gestion des utilisateurs",
"description": "Point particulier :- Multi laboratoire :tous\n- Le cas n'est pas bloquant\nDescription du problème :\nBonjour,\n\nDans le menu Mes paramètres - Gestion des utilisateurs, tous les utilisateurs n'apparaissent pas. Comment faire pour les faire tous apparaitre?\nMerci.",
"project_name": "Demandes",
"stage_name": "Clôturé",
"user_id": "",
"partner_id_email_from": "CHAUSSON MATERIAUX, Christophe SAUVAGET, christophe.sauvaget@chausson.fr",
"create_date": "04/07/2024 12:09:47",
"write_date_last_modification": "03/10/2024 13:10:50",
"date_deadline": "19/07/2024 00:00:00",
"messages": [
{
"author_id": "Fabien LAFAY",
"date": "04/07/2024 13:03:58",
"message_type": "E-mail",
"subject": "Re: [T9656] - Gestion des utilisateurs",
"id": "191104",
"content": "Bonjour,\nSi un utilisateur n'apparait pas dans la liste, c'est probablement car il n'a pas de laboratoire principal d'assigné.\nDans ce cas, il faut cocher la case \"Affiche les laboratoires secondaires\" pour le voir.\nVous pouvez ensuite retrouver l'utilisateur dans la liste (en utilisant les filtre sur les colonnes si besoin) et l'éditer.\nSur la fiche de l'utilisateur, vérifier si le laboratoire principal est présent, et ajoutez-le si ce n'est pas le cas.\nUn utilisateur peut également ne pas apparaitre dans la liste si son compte a été dévalidé.\nDans ce cas cochez la case \"Affiche les utilisateurs non valides\" pour le voir apparaitre dans la liste (en grisé).\nVous pouvez le rendre à nouveau valide en éditant son compte et en cochant la case \"Utilisateur valide\"\nJe reste à votre entière disposition pour toute information complémentaire.\nCordialement,\nPour vous accompagner au mieux, veuillez trouver ci-joint des liens d'aide :\nManuel d'utilisation : lien vers le manuel d'utilisation\nFAQ : lien vers la FAQ\n---\nSupport technique\nL'objectif du Support Technique est de vous aider : si vous rencontrez une difficulté, ou pour nous soumettre une ou des suggestions d'amélioration de nos logiciels ou de nos méthodes. Notre service est ouvert du lundi au vendredi de 9h à 12h et de 14h à 18h. Dès réception, un technicien prendra en charge votre demande et au besoin vous rappellera.\n*Confidentialité : Ce courriel contient des informations confidentielles exclusivement réservées au destinataire mentionné. Si vous deviez recevoir cet e-mail par erreur, merci den avertir immédiatement lexpéditeur et de le supprimer de votre système informatique. Au cas où vous ne seriez pas destinataire de ce message, veuillez noter que sa divulgation, sa copie ou tout acte en rapport avec la communication du contenu des informations est strictement interdit.*\n\n- image.png (image/png) [ID: 129046]\n- image.png (image/png) [ID: 129044]\n- image.png (image/png) [ID: 129042]\n\n---\n"
}
],
"date_d'extraction": "14/04/2025 15:10:54",
"répertoire": "output/ticket_T9656/T9656_20250414_151053"
}

View File

@ -0,0 +1,62 @@
# Ticket T9656: Gestion des utilisateurs
## Informations du ticket
- **id**: 9635
- **code**: T9656
- **name**: Gestion des utilisateurs
- **project_name**: Demandes
- **stage_name**: Clôturé
- **user_id**:
- **partner_id/email_from**: CHAUSSON MATERIAUX, Christophe SAUVAGET, christophe.sauvaget@chausson.fr
- **create_date**: 04/07/2024 12:09:47
- **write_date/last modification**: 03/10/2024 13:10:50
- **date_deadline**: 19/07/2024 00:00:00
- **description**:
Point particulier :- Multi laboratoire :tous
- Le cas n'est pas bloquant
Description du problème :
Bonjour,
Dans le menu Mes paramètres - Gestion des utilisateurs, tous les utilisateurs n'apparaissent pas. Comment faire pour les faire tous apparaitre?
Merci.
## Messages
### Message 1
**author_id**: Fabien LAFAY
**date**: 04/07/2024 13:03:58
**message_type**: E-mail
**subject**: Re: [T9656] - Gestion des utilisateurs
**id**: 191104
Bonjour,
Si un utilisateur n'apparait pas dans la liste, c'est probablement car il n'a pas de laboratoire principal d'assigné.
Dans ce cas, il faut cocher la case "Affiche les laboratoires secondaires" pour le voir.
Vous pouvez ensuite retrouver l'utilisateur dans la liste (en utilisant les filtre sur les colonnes si besoin) et l'éditer.
Sur la fiche de l'utilisateur, vérifier si le laboratoire principal est présent, et ajoutez-le si ce n'est pas le cas.
Un utilisateur peut également ne pas apparaitre dans la liste si son compte a été dévalidé.
Dans ce cas cochez la case "Affiche les utilisateurs non valides" pour le voir apparaitre dans la liste (en grisé).
Vous pouvez le rendre à nouveau valide en éditant son compte et en cochant la case "Utilisateur valide"
Je reste à votre entière disposition pour toute information complémentaire.
Cordialement,
Pour vous accompagner au mieux, veuillez trouver ci-joint des liens d'aide :
Manuel d'utilisation : lien vers le manuel d'utilisation
FAQ : lien vers la FAQ
---
Support technique
L'objectif du Support Technique est de vous aider : si vous rencontrez une difficulté, ou pour nous soumettre une ou des suggestions d'amélioration de nos logiciels ou de nos méthodes. Notre service est ouvert du lundi au vendredi de 9h à 12h et de 14h à 18h. Dès réception, un technicien prendra en charge votre demande et au besoin vous rappellera.
*Confidentialité : Ce courriel contient des informations confidentielles exclusivement réservées au destinataire mentionné. Si vous deviez recevoir cet e-mail par erreur, merci den avertir immédiatement lexpéditeur et de le supprimer de votre système informatique. Au cas où vous ne seriez pas destinataire de ce message, veuillez noter que sa divulgation, sa copie ou tout acte en rapport avec la communication du contenu des informations est strictement interdit.*
**attachment_ids**:
- image.png (image/png) [ID: 129046]
- image.png (image/png) [ID: 129044]
- image.png (image/png) [ID: 129042]
---
## Informations sur l'extraction
- **Date d'extraction**: 14/04/2025 15:10:54
- **Répertoire**: output/ticket_T9656/T9656_20250414_151053

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,95 @@
TICKET: T9656 - Gestion des utilisateurs
Date d'extraction: 2025-04-14 15:10:53
Nombre de messages: 5
================================================================================
********************************************************************************
*** CHANGEMENT D'ÉTAT ***
********************************************************************************
DATE: 2024-07-04 12:09:47
DE: Support Robot
--------------------------------------------------------------------------------
********************************************************************************
*** CHANGEMENT D'ÉTAT ***
********************************************************************************
DATE: 2024-07-04 12:42:43
DE: Fabien LAFAY
--------------------------------------------------------------------------------
********************************************************************************
*** MESSAGE TRANSFÉRÉ ***
********************************************************************************
DATE: 2024-07-04 13:03:58
DE: Fabien LAFAY
OBJET: Re: [T9656] - Gestion des utilisateurs
Bonjour,
Si un utilisateur n'apparait pas dans la liste, c'est probablement car il n'a pas de laboratoire principal d'assigné.
Dans ce cas, il faut cocher la case "Affiche les laboratoires secondaires" pour le voir.
Vous pouvez ensuite retrouver l'utilisateur dans la liste (en utilisant les filtre sur les colonnes si besoin) et l'éditer.
Sur la fiche de l'utilisateur, vérifier si le laboratoire principal est présent, et ajoutez-le si ce n'est pas le cas.
Un utilisateur peut également ne pas apparaitre dans la liste si son compte a été dévalidé.
Dans ce cas cochez la case "Affiche les utilisateurs non valides" pour le voir apparaitre dans la liste (en grisé).
Vous pouvez le rendre à nouveau valide en éditant son compte et en cochant la case "Utilisateur valide"
Je reste à votre entière disposition pour toute information complémentaire.
Cordialement,
Pour vous accompagner au mieux, veuillez trouver ci-joint des liens d'aide :
Manuel d'utilisation : lien vers le manuel d'utilisation
FAQ : lien vers la FAQ
---
Support technique
 
L'objectif du Support Technique est de vous aider : si vous rencontrez une difficulté, ou pour nous soumettre une ou des suggestions d'amélioration de nos logiciels ou de nos méthodes. Notre service est ouvert du lundi au vendredi de 9h à 12h et de 14h à 18h. Dès réception, un technicien prendra en charge votre demande et au besoin vous rappellera.
*Confidentialité : Ce courriel contient des informations confidentielles exclusivement réservées au destinataire mentionné. Si vous deviez recevoir cet e-mail par erreur, merci den avertir immédiatement lexpéditeur et de le supprimer de votre système informatique. Au cas où vous ne seriez pas destinataire de ce message, veuillez noter que sa divulgation, sa copie ou tout acte en rapport avec la communication du contenu des informations est strictement interdit.*
--------------------------------------------------------------------------------
********************************************************************************
*** CHANGEMENT D'ÉTAT ***
********************************************************************************
DATE: 2024-07-04 13:04:02
DE: Fabien LAFAY
--------------------------------------------------------------------------------
********************************************************************************
*** CHANGEMENT D'ÉTAT ***
********************************************************************************
DATE: 2024-07-19 08:00:10
DE: Fabien LAFAY
--------------------------------------------------------------------------------

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@ -0,0 +1,56 @@
[
{
"id": 129046,
"name": "image.png",
"mimetype": "image/png",
"file_size": 44511,
"create_date": "2024-07-04 13:03:58",
"create_uid": [
22,
"Fabien LAFAY"
],
"description": false,
"res_name": "[T9656] Gestion des utilisateurs",
"creator_name": "Fabien LAFAY",
"creator_id": 22,
"download_status": "success",
"local_path": "output/ticket_T9656/T9656_20250414_151053/attachments/image.png",
"error": ""
},
{
"id": 129044,
"name": "image.png",
"mimetype": "image/png",
"file_size": 25583,
"create_date": "2024-07-04 13:03:58",
"create_uid": [
22,
"Fabien LAFAY"
],
"description": false,
"res_name": "[T9656] Gestion des utilisateurs",
"creator_name": "Fabien LAFAY",
"creator_id": 22,
"download_status": "success",
"local_path": "output/ticket_T9656/T9656_20250414_151053/attachments/image_1.png",
"error": ""
},
{
"id": 129042,
"name": "image.png",
"mimetype": "image/png",
"file_size": 46468,
"create_date": "2024-07-04 13:03:58",
"create_uid": [
22,
"Fabien LAFAY"
],
"description": false,
"res_name": "[T9656] Gestion des utilisateurs",
"creator_name": "Fabien LAFAY",
"creator_id": 22,
"download_status": "success",
"local_path": "output/ticket_T9656/T9656_20250414_151053/attachments/image_2.png",
"error": ""
}
]

View File

@ -0,0 +1,16 @@
[
{
"id": 76486,
"partner_id": [
28961,
"Fabien LAFAY"
]
},
{
"id": 76493,
"partner_id": [
29511,
"CHAUSSON MATERIAUX, Christophe SAUVAGET"
]
}
]

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,20 @@
{
"date_extraction": "2025-04-14T15:10:54.021870",
"ticket_id": 9635,
"ticket_code": "T9656",
"ticket_name": "Gestion des utilisateurs",
"output_dir": "output/ticket_T9656/T9656_20250414_151053",
"files": {
"ticket_info": "ticket_info.json",
"ticket_summary": "ticket_summary.json",
"messages": "all_messages.json",
"messages_raw": "messages_raw.json",
"messages_text": "all_messages.txt",
"attachments": "attachments_info.json",
"followers": "followers.json"
},
"stats": {
"messages_count": 5,
"attachments_count": 3
}
}

View File

@ -0,0 +1,54 @@
{
"id": 9635,
"name": "Gestion des utilisateurs",
"description": "<h1>Point particulier :</h1><ul><li>Multi laboratoire :tous</li><li>Le cas n'est pas bloquant</li></ul><h1>Description du problème :</h1><p>Bonjour,\r\n\r\nDans le menu Mes paramètres - Gestion des utilisateurs, tous les utilisateurs n'apparaissent pas. Comment faire pour les faire tous apparaitre?\r\nMerci.</p>",
"stage_id": [
8,
"Clôturé"
],
"project_id": [
3,
"Demandes"
],
"partner_id": [
29511,
"CHAUSSON MATERIAUX, Christophe SAUVAGET"
],
"user_id": [
22,
"Fabien LAFAY"
],
"date_start": "2024-07-04 12:09:47",
"date_end": false,
"date_deadline": "2024-07-19",
"create_date": "2024-07-04 12:09:47",
"write_date": "2024-10-03 13:10:50",
"tag_ids": [
15
],
"priority": "1",
"email_from": "christophe.sauvaget@chausson.fr",
"email_cc": "",
"message_ids": [
193689,
191107,
191106,
191105,
191104,
191097,
191085
],
"message_follower_ids": [
76486,
76493
],
"timesheet_ids": [],
"attachment_ids": [],
"stage_id_name": "Clôturé",
"project_id_name": "Demandes",
"partner_id_name": "CHAUSSON MATERIAUX, Christophe SAUVAGET",
"user_id_name": "Fabien LAFAY",
"tag_names": [
"BRG-LAB WEB"
]
}

View File

@ -0,0 +1,16 @@
{
"id": 9635,
"code": "T9656",
"name": "Gestion des utilisateurs",
"description": "<h1>Point particulier :</h1><ul><li>Multi laboratoire :tous</li><li>Le cas n'est pas bloquant</li></ul><h1>Description du problème :</h1><p>Bonjour,\r\n\r\nDans le menu Mes paramètres - Gestion des utilisateurs, tous les utilisateurs n'apparaissent pas. Comment faire pour les faire tous apparaitre?\r\nMerci.</p>",
"stage": "Clôturé",
"project": "Demandes",
"partner": "CHAUSSON MATERIAUX, Christophe SAUVAGET",
"assigned_to": "Fabien LAFAY",
"tags": [
"BRG-LAB WEB"
],
"create_date": "2024-07-04 12:09:47",
"write_date": "2024-10-03 13:10:50",
"deadline": "2024-07-19"
}

View File

@ -14,16 +14,24 @@ import logging
import traceback
from datetime import datetime
# Import des agents spécifiques pour chaque modèle LLM
from agents.mistral_medium.agent_ticket_analyser import AgentTicketAnalyser as MistralMediumTicketAnalyser
from agents.mistral_medium.agent_report_generator import AgentReportGenerator as MistralMediumReportGenerator
from agents.pixtral12b.agent_image_sorter import AgentImageSorter as Pixtral12bImageSorter
from agents.pixtral12b.agent_image_analyser import AgentImageAnalyser as Pixtral12bImageAnalyser
# S'assurer que le répertoire racine est dans le sys.path
current_dir = os.path.dirname(os.path.abspath(__file__))
if current_dir not in sys.path:
sys.path.insert(0, current_dir)
# Import des modèles LLM
# Import des modèles LLM d'abord
from llm_classes.mistral_medium import MistralMedium
from llm_classes.pixtral_12b import Pixtral12b
# Import directs des agents (avec correspondance explicite modèle->agent)
# Utilisation de MistralMedium
from agents.mistral_medium.agent_ticket_analyser import AgentTicketAnalyser as MistralMediumTicketAnalyser
from agents.mistral_medium.agent_report_generator import AgentReportGenerator as MistralMediumReportGenerator
# Utilisation de Pixtral12b
from agents.pixtral12b.agent_image_sorter import AgentImageSorter as Pixtral12bImageSorter
from agents.pixtral12b.agent_image_analyser import AgentImageAnalyser as Pixtral12bImageAnalyser
# Import de l'orchestrateur
from orchestrator import Orchestrator
@ -55,30 +63,33 @@ def test_orchestrator(ticket_id=None):
print("ERREUR: Aucun ticket trouvé dans le dossier output/")
return
# Initialisation des LLM
# Initialisation des LLM avec chaque modèle pour sa tâche spécifique
print("Initialisation des modèles LLM...")
start_time = time.time()
# Utilisation de Mistral Medium pour l'analyse JSON et la génération de rapports
# Assignation claire des modèles aux agents
# MistralMedium pour l'analyse de texte et la génération de rapports
json_llm = MistralMedium()
logger.info("LLM MistralMedium initialisé pour l'analyse JSON")
# Utilisation de Pixtral12b pour le tri et l'analyse d'images
image_sorter_llm = Pixtral12b()
logger.info("LLM Pixtral12b initialisé pour le tri d'images")
image_analyser_llm = Pixtral12b()
logger.info("LLM Pixtral12b initialisé pour l'analyse d'images")
report_generator_llm = MistralMedium()
logger.info("LLM MistralMedium initialisé pour la génération de rapports")
# Pixtral12b pour le traitement d'images
image_sorter_llm = Pixtral12b()
logger.info("LLM Pixtral12b initialisé pour le tri d'images")
image_analyser_llm = Pixtral12b()
logger.info("LLM Pixtral12b initialisé pour l'analyse d'images")
llm_init_time = time.time() - start_time
print(f"Tous les modèles LLM ont été initialisés en {llm_init_time:.2f} secondes")
# Création des agents
# Création des agents avec le modèle correspondant
print("Création des agents...")
# Assignation explicite des modèles aux agents correspondants
ticket_agent = MistralMediumTicketAnalyser(json_llm)
image_sorter = Pixtral12bImageSorter(image_sorter_llm)
image_analyser = Pixtral12bImageAnalyser(image_analyser_llm)

View File

@ -0,0 +1,181 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script de test pour exécuter l'orchestrateur avec le modèle Mistral Medium.
Utilisation: python test_orchestrator_mistral_medium.py [code_ticket]
Exemple: python test_orchestrator_mistral_medium.py T9656
"""
import os
import sys
import time
import logging
import traceback
from datetime import datetime
# S'assurer que le répertoire racine est dans le sys.path
current_dir = os.path.dirname(os.path.abspath(__file__))
if current_dir not in sys.path:
sys.path.insert(0, current_dir)
# Import des modèles LLM
from llm_classes.mistral_medium import MistralMedium
from llm_classes.pixtral_12b import Pixtral12b
# Import directs des agents spécifiques - MODÈLE MISTRAL MEDIUM
# Chaque agent est importé explicitement depuis son module
from agents.mistral_medium.agent_ticket_analyser import AgentTicketAnalyser
from agents.mistral_medium.agent_report_generator import AgentReportGenerator
from agents.pixtral12b.agent_image_sorter import AgentImageSorter
from agents.pixtral12b.agent_image_analyser import AgentImageAnalyser
# Import de l'orchestrateur
from orchestrator import Orchestrator
# Configuration du logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s',
filename='test_orchestrator_mistral_medium.log', filemode='w')
logger = logging.getLogger("TestOrchestratorMistralMedium")
def test_orchestrator(ticket_id=None):
"""
Exécute l'orchestrateur avec les agents Mistral Medium
Args:
ticket_id: Identifiant du ticket à traiter (optionnel)
"""
# Vérifier que le dossier output existe
if not os.path.exists("output/"):
os.makedirs("output/")
logger.warning("Le dossier output/ n'existait pas et a été créé")
print("ATTENTION: Le dossier output/ n'existait pas et a été créé")
# Vérifier le contenu du dossier output
tickets = [d for d in os.listdir("output/") if d.startswith("ticket_") and os.path.isdir(os.path.join("output/", d))]
logger.info(f"Tickets trouvés dans output/: {len(tickets)}")
print(f"Tickets existants dans output/: {len(tickets)}")
if len(tickets) == 0:
logger.error("Aucun ticket trouvé dans le dossier output/")
print("ERREUR: Aucun ticket trouvé dans le dossier output/")
return
# Initialisation des modèles LLM
print("Initialisation des modèles LLM...")
start_time = time.time()
# MISTRAL MEDIUM pour l'analyse de texte et la génération de rapports
mistral_medium_llm = MistralMedium()
logger.info("Modèle MistralMedium initialisé")
# PIXTRAL pour le traitement d'images
pixtral_llm = Pixtral12b()
logger.info("Modèle Pixtral12b initialisé")
llm_init_time = time.time() - start_time
print(f"Tous les modèles LLM ont été initialisés en {llm_init_time:.2f} secondes")
# Création des agents avec les modèles appropriés
print("Création des agents avec Mistral Medium et Pixtral...")
# Création des agents avec correspondance explicite entre agent et modèle
ticket_agent = AgentTicketAnalyser(mistral_medium_llm)
report_generator = AgentReportGenerator(mistral_medium_llm)
image_sorter = AgentImageSorter(pixtral_llm)
image_analyser = AgentImageAnalyser(pixtral_llm)
print("Tous les agents ont été créés")
# Initialisation de l'orchestrateur
logger.info("Initialisation de l'orchestrateur avec agents Mistral Medium")
print("Initialisation de l'orchestrateur")
orchestrator = Orchestrator(
output_dir="output/",
ticket_agent=ticket_agent,
image_sorter=image_sorter,
image_analyser=image_analyser,
report_generator=report_generator
)
# Vérification du ticket spécifique si fourni
if ticket_id:
target_ticket = f"ticket_{ticket_id}"
specific_ticket_path = os.path.join("output", target_ticket)
if not os.path.exists(specific_ticket_path):
logger.error(f"Le ticket {target_ticket} n'existe pas")
print(f"ERREUR: Le ticket {target_ticket} n'existe pas")
return
logger.info(f"Ticket spécifique à traiter: {specific_ticket_path}")
print(f"Ticket spécifique à traiter: {target_ticket}")
# Exécution de l'orchestrateur
total_start_time = time.time()
logger.info("Début de l'exécution de l'orchestrateur")
print("Début de l'exécution de l'orchestrateur")
try:
orchestrator.executer(ticket_id)
# Vérifier le rapport généré et afficher un résumé
if ticket_id:
# Chercher le rapport Markdown le plus récent
ticket_dir = os.path.join("output", f"ticket_{ticket_id}")
latest_md = None
for extraction in os.listdir(ticket_dir):
extraction_path = os.path.join(ticket_dir, extraction)
if os.path.isdir(extraction_path):
rapports_dir = os.path.join(extraction_path, f"{ticket_id}_rapports", f"{ticket_id}")
if os.path.exists(rapports_dir):
md_files = [f for f in os.listdir(rapports_dir) if f.endswith('.md')]
if md_files:
md_files.sort(reverse=True) # Le plus récent en premier
latest_md = os.path.join(rapports_dir, md_files[0])
break
if latest_md:
print(f"\nVérification du rapport: {latest_md}")
try:
with open(latest_md, 'r', encoding='utf-8') as f:
content = f.read()
# Vérifier si le tableau des échanges est présent
has_table = "| Date | " in content
has_details = "## Détails des analyses" in content
print(f"- Tableau des échanges: {'Présent' if has_table else 'MANQUANT'}")
print(f"- Détails des analyses: {'Présent' if has_details else 'MANQUANT'}")
if not has_table:
print("\nATTENTION: Le tableau des échanges client/support est manquant!")
print("Vérifiez le system prompt de l'agent de rapport et la transmission des données.")
except Exception as e:
print(f"Erreur lors de la vérification du rapport: {e}")
except Exception as e:
logger.error(f"Erreur lors de l'exécution de l'orchestrateur: {str(e)}")
print(f"ERREUR: {str(e)}")
traceback.print_exc()
total_time = time.time() - total_start_time
logger.info(f"Fin de l'exécution de l'orchestrateur (durée: {total_time:.2f} secondes)")
print(f"Fin de l'exécution de l'orchestrateur (durée: {total_time:.2f} secondes)")
if __name__ == "__main__":
print("Démarrage du test de l'orchestrateur avec Mistral Medium")
# Vérifier si un ID de ticket est passé en argument
ticket_id = None
if len(sys.argv) > 1:
ticket_id = sys.argv[1]
print(f"ID de ticket fourni en argument: {ticket_id}")
test_orchestrator(ticket_id)
print("Test terminé")