mirror of
https://github.com/Ladebeze66/llm_ticket3.git
synced 2026-02-04 08:40:23 +01:00
1404-15:34
This commit is contained in:
parent
b190e772c7
commit
84332beb49
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
16
agents/__init__.py
Normal file
16
agents/__init__.py
Normal 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
|
||||
3
agents/llama_vision3_2/__init__.py
Normal file
3
agents/llama_vision3_2/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
"""
|
||||
Package agents.llama_vision3_2 contenant les agents utilisant le modèle Llama Vision 3.2.
|
||||
"""
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
179
agents/mistral_large/agent_ticket_analyser.py.bak
Normal file
179
agents/mistral_large/agent_ticket_analyser.py.bak
Normal 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 où 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
|
||||
3
agents/mistral_medium/__init__.py
Normal file
3
agents/mistral_medium/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
"""
|
||||
Package agents.mistral_medium contenant les agents utilisant le modèle Mistral Medium.
|
||||
"""
|
||||
@ -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}")
|
||||
|
||||
@ -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")
|
||||
@ -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")
|
||||
@ -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
|
||||
@ -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")
|
||||
@ -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
|
||||
3
agents/pixtral12b/__init__.py
Normal file
3
agents/pixtral12b/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
"""
|
||||
Package agents.pixtral12b contenant les agents utilisant le modèle Pixtral 12B.
|
||||
"""
|
||||
@ -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.
|
||||
"""
|
||||
|
||||
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
|
||||
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):
|
||||
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.
|
||||
"""
|
||||
Analyse une liste d'images pour en extraire les informations pertinentes.
|
||||
|
||||
def executer(self, image_path: str, contexte: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
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 = []
|
||||
|
||||
if not images:
|
||||
logger.warning("Aucune image à analyser")
|
||||
return results
|
||||
|
||||
logger.info(f"Analyse de {len(images)} images")
|
||||
print(f" Analyse de {len(images)} images")
|
||||
|
||||
# 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))
|
||||
image_name = os.path.basename(image_path)
|
||||
logger.info(f"Analyse détaillée de l'image: {image_name}")
|
||||
|
||||
# Vérifier que l'image existe
|
||||
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.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 l'image {i}/{len(images)}: {image_name}")
|
||||
print(f" Analyse de l'image {i}/{len(images)}: {image_name}")
|
||||
# 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."
|
||||
|
||||
start_time = time.time()
|
||||
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"]
|
||||
}}"""
|
||||
|
||||
# Effectuer l'analyse via le LLM
|
||||
try:
|
||||
# Encoder l'image en base64
|
||||
image_base64 = self._encoder_image_base64(image_path)
|
||||
# Utiliser la méthode d'interrogation avec image
|
||||
resultat_brut = self.llm.interroger_avec_image(image_path, prompt)
|
||||
|
||||
# Construire le prompt pour l'analyse
|
||||
prompt = self._construire_prompt_image(image_name, ticket_analyse)
|
||||
# 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)
|
||||
|
||||
# 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
|
||||
)
|
||||
# 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"
|
||||
|
||||
# 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
|
||||
})
|
||||
# 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:
|
||||
error_message = f"Erreur lors de l'analyse de l'image {image_name}: {str(e)}"
|
||||
logger.error(error_message)
|
||||
print(f" ERREUR: {error_message}")
|
||||
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()
|
||||
}
|
||||
|
||||
results.append({
|
||||
"image": image_info,
|
||||
"analyse": f"ERREUR: {error_message}",
|
||||
"pertinent": False
|
||||
})
|
||||
|
||||
return results
|
||||
|
||||
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('}')
|
||||
|
||||
def _construire_prompt_image(self, image_name: str, ticket_analyse: Optional[str] = None) -> str:
|
||||
if debut != -1 and fin != -1 and fin > debut:
|
||||
return texte[debut:fin+1]
|
||||
|
||||
return None
|
||||
|
||||
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")
|
||||
@ -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.
|
||||
|
||||
# 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
|
||||
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
|
||||
|
||||
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
|
||||
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"""
|
||||
|
||||
self.image_batch_size = 3 # Nombre d'images à analyser par lot
|
||||
|
||||
def executer(self, attachments_dir: str, contexte: Optional[Dict] = None) -> Dict[str, Dict[str, Any]]:
|
||||
"""
|
||||
|
||||
# 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
|
||||
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
|
||||
logger.info(f"Tri des images dans: {attachments_dir}")
|
||||
|
||||
# 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 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:
|
||||
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'))]
|
||||
|
||||
# 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
|
||||
if not images:
|
||||
logger.info(f"Aucune image trouvée dans {attachments_dir}")
|
||||
return {}
|
||||
|
||||
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
|
||||
logger.info(f"Nombre d'images trouvées: {len(images)}")
|
||||
|
||||
def _encoder_image_base64(self, image_path: str) -> str:
|
||||
"""
|
||||
Encode l'image en base64 pour l'inclure directement dans le prompt
|
||||
# Analyser les images individuellement ou par lots selon la configuration
|
||||
resultats = {}
|
||||
|
||||
Args:
|
||||
image_path: Chemin vers l'image
|
||||
# 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}"
|
||||
|
||||
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
|
||||
"""
|
||||
# Traitement image par image
|
||||
for image_path in images:
|
||||
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}")
|
||||
logger.info(f"Analyse de l'image: {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}")
|
||||
prompt = f"""Analyse cette image dans le contexte suivant:
|
||||
{contexte_analyse}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
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
|
||||
}}"""
|
||||
|
||||
# Utiliser une référence au fichier image que le modèle peut comprendre
|
||||
# Analyser l'image
|
||||
try:
|
||||
# Préparation du prompt standardisé
|
||||
prompt = self._generer_prompt_analyse()
|
||||
resultat_brut = self.llm.interroger_avec_image(image_path, prompt)
|
||||
|
||||
# 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)
|
||||
# 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:
|
||||
# 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"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
|
||||
}
|
||||
}
|
||||
|
||||
# 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
|
||||
}
|
||||
resultats[image_path] = {
|
||||
"description": "Erreur d'analyse",
|
||||
"pertinence": "Inconnue",
|
||||
"justification": f"Exception: {str(e)}",
|
||||
"contenu_technique": False,
|
||||
"image_path": image_path
|
||||
}
|
||||
|
||||
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"]
|
||||
Args:
|
||||
resultats: Dictionnaire avec les résultats d'analyse des images
|
||||
|
||||
# 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"]
|
||||
Returns:
|
||||
Liste des chemins des images pertinentes
|
||||
"""
|
||||
pertinentes = []
|
||||
|
||||
# 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)
|
||||
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
|
||||
# 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)
|
||||
|
||||
# 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")
|
||||
return pertinentes
|
||||
3
agents/pixtral_large/__init__.py
Normal file
3
agents/pixtral_large/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
"""
|
||||
Package agents.pixtral_large contenant les agents utilisant le modèle Pixtral Large.
|
||||
"""
|
||||
3
agents/qwen2_5/__init__.py
Normal file
3
agents/qwen2_5/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
"""
|
||||
Package agents.qwen2_5 contenant les agents utilisant le modèle Qwen 2.5.
|
||||
"""
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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")
|
||||
|
||||
705
agents/utils/report_formatter.py.bak
Normal file
705
agents/utils/report_formatter.py.bak
Normal 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
3
llm_classes/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
"""
|
||||
Package llm_classes contenant les classes d'interfaçage avec les différents modèles de langage (LLM).
|
||||
"""
|
||||
@ -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)
|
||||
|
||||
def urlBase(self) -> str:
|
||||
return "https://api.mistral.ai/v1/"
|
||||
# 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"
|
||||
|
||||
def cleAPI(self) -> str:
|
||||
return "2iGzTzE9csRQ9IoASoUjplHwEjA200Vh"
|
||||
|
||||
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
|
||||
# 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
|
||||
}
|
||||
|
||||
def _traiter_reponse(self, reponse: requests.Response) -> str:
|
||||
data = reponse.json()
|
||||
return data["choices"][0]["message"]["content"]
|
||||
# Timeout de requête adapté au modèle
|
||||
self.request_timeout = 450 # 7.5 minutes
|
||||
|
||||
def _encoder_image_base64(self, image_path: str) -> str:
|
||||
# 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:
|
||||
"""
|
||||
Encode une image en base64 pour l'API.
|
||||
Retourne l'URL de base de l'API Ollama.
|
||||
"""
|
||||
return "http://217.182.105.173:11434/"
|
||||
|
||||
def cleAPI(self) -> str:
|
||||
"""
|
||||
Ollama ne nécessite pas de clé API.
|
||||
"""
|
||||
return ""
|
||||
|
||||
def urlFonction(self) -> str:
|
||||
"""
|
||||
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)
|
||||
self.heureDepart = datetime.now()
|
||||
response = requests.post(url=url, headers=headers, json=contenu, timeout=self.request_timeout)
|
||||
self.heureFin = datetime.now()
|
||||
|
||||
# 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'
|
||||
if self.heureDepart is not None:
|
||||
self.dureeTraitement = self.heureFin - self.heureDepart
|
||||
else:
|
||||
# Par défaut, on suppose JPEG
|
||||
mime_type = 'image/jpeg'
|
||||
self.dureeTraitement = timedelta(0)
|
||||
|
||||
return f"data:{mime_type};base64,{encoded_string}"
|
||||
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:
|
||||
|
||||
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 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'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 and self.heureFin is not None:
|
||||
if self.heureDepart is not None:
|
||||
self.dureeTraitement = self.heureFin - self.heureDepart
|
||||
else:
|
||||
self.dureeTraitement = timedelta(0)
|
||||
self.reponseErreur = True
|
||||
return f"Erreur lors de l'analyse de l'image: {str(e)}"
|
||||
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 où exporter. Si vide, un nom basé sur la date est généré.
|
||||
|
||||
Returns:
|
||||
Chemin du fichier où 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 ""
|
||||
|
||||
@ -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
|
||||
114
orchestrator.py
114
orchestrator.py
@ -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
|
||||
# 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)
|
||||
|
||||
# Tri des images
|
||||
for img in images:
|
||||
img_path = os.path.join(attachments_dir, img)
|
||||
# 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...")
|
||||
|
||||
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", "")
|
||||
# Exécuter l'agent de tri d'images avec le répertoire complet
|
||||
sorting_result = self.image_sorter.executer(attachments_dir)
|
||||
|
||||
# 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")
|
||||
# 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}")
|
||||
|
||||
# Ajouter les métadonnées de tri à la liste des analyses
|
||||
# Initialiser le dictionnaire d'analyses pour chaque image
|
||||
for img_path, img_analyse in sorting_result.items():
|
||||
images_analyses[img_path] = {
|
||||
"sorting": sorting_result,
|
||||
"analysis": None # Sera rempli plus tard si pertinent
|
||||
}
|
||||
|
||||
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)"},
|
||||
"sorting": img_analyse,
|
||||
"analysis": None
|
||||
}
|
||||
print(f" => Auto-sélectionné (pas de tri)")
|
||||
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}")
|
||||
|
||||
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)}")
|
||||
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)}")
|
||||
|
||||
@ -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 d’en avertir immédiatement l’expé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"
|
||||
}
|
||||
@ -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 d’en avertir immédiatement l’expé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
|
||||
232
output/ticket_T9656/T9656_20250414_151053/all_messages.json
Normal file
232
output/ticket_T9656/T9656_20250414_151053/all_messages.json
Normal file
File diff suppressed because one or more lines are too long
95
output/ticket_T9656/T9656_20250414_151053/all_messages.txt
Normal file
95
output/ticket_T9656/T9656_20250414_151053/all_messages.txt
Normal 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 d’en avertir immédiatement l’expé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
|
||||
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
BIN
output/ticket_T9656/T9656_20250414_151053/attachments/image.png
Normal file
BIN
output/ticket_T9656/T9656_20250414_151053/attachments/image.png
Normal file
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 |
@ -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": ""
|
||||
}
|
||||
]
|
||||
16
output/ticket_T9656/T9656_20250414_151053/followers.json
Normal file
16
output/ticket_T9656/T9656_20250414_151053/followers.json
Normal file
@ -0,0 +1,16 @@
|
||||
[
|
||||
{
|
||||
"id": 76486,
|
||||
"partner_id": [
|
||||
28961,
|
||||
"Fabien LAFAY"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 76493,
|
||||
"partner_id": [
|
||||
29511,
|
||||
"CHAUSSON MATERIAUX, Christophe SAUVAGET"
|
||||
]
|
||||
}
|
||||
]
|
||||
218
output/ticket_T9656/T9656_20250414_151053/messages_raw.json
Normal file
218
output/ticket_T9656/T9656_20250414_151053/messages_raw.json
Normal file
File diff suppressed because one or more lines are too long
20
output/ticket_T9656/T9656_20250414_151053/structure.json
Normal file
20
output/ticket_T9656/T9656_20250414_151053/structure.json
Normal 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
|
||||
}
|
||||
}
|
||||
54
output/ticket_T9656/T9656_20250414_151053/ticket_info.json
Normal file
54
output/ticket_T9656/T9656_20250414_151053/ticket_info.json
Normal 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"
|
||||
]
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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
|
||||
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")
|
||||
|
||||
report_generator_llm = MistralMedium()
|
||||
logger.info("LLM MistralMedium initialisé pour la génération de rapports")
|
||||
|
||||
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)
|
||||
|
||||
181
test_orchestrator_mistral_medium.py
Normal file
181
test_orchestrator_mistral_medium.py
Normal 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é")
|
||||
Loading…
x
Reference in New Issue
Block a user