This commit is contained in:
Ladebeze66 2025-04-07 14:30:24 +02:00
parent 31f9e81645
commit 5ddc0fef57
23 changed files with 10512 additions and 480 deletions

View File

@ -1,6 +1,10 @@
from .base_agent import BaseAgent
from typing import Any
from typing import Any, Dict
import logging
import os
from PIL import Image
import base64
import io
logger = logging.getLogger("AgentImageAnalyser")
@ -9,16 +13,308 @@ class AgentImageAnalyser(BaseAgent):
Agent pour analyser les images et extraire les informations pertinentes.
"""
def __init__(self, llm):
super().__init__("AgentImageAnalyser", llm, "image_analyser")
super().__init__("AgentImageAnalyser", llm)
# Configuration locale de l'agent (remplace AgentConfig)
self.temperature = 0.3
self.top_p = 0.9
self.max_tokens = 1200
self.system_prompt = """Tu es un expert en analyse d'images pour le support technique de BRG_Lab.
Ta mission est d'analyser des captures d'écran ou des images techniques en tenant compte du contexte du ticket.
Pour chaque image, structure ton analyse ainsi:
1. Description factuelle: Ce que contient l'image (interface, message d'erreur, etc.)
2. Éléments techniques importants: Versions, codes d'erreur, paramètres visibles
3. Interprétation: Ce que cette image révèle sur le problème client
4. Relation avec le ticket: Comment cette image aide à résoudre le problème décrit
IMPORTANT: Ne commence JAMAIS ta réponse par "Je n'ai pas accès à l'image" ou "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".
Concentre-toi sur les éléments techniques pertinents pour le support logiciel."""
# 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
def executer(self, image_description: str, contexte: str) -> str:
logger.info(f"Analyse de l'image: {image_description} avec le contexte: {contexte}")
prompt = f"Analyse cette image en tenant compte du contexte suivant : {contexte}. Description de l'image : {image_description}"
# Appliquer les paramètres
if hasattr(self.llm, "configurer"):
params = {
"temperature": self.temperature,
"top_p": self.top_p,
"max_tokens": self.max_tokens
}
# Ajustements selon le type de modèle
if "mistral_medium" in self.llm.__class__.__name__.lower():
params["temperature"] += 0.05
params["max_tokens"] = 1000
elif "pixtral" in self.llm.__class__.__name__.lower():
params["temperature"] -= 0.05
elif "ollama" in self.llm.__class__.__name__.lower():
params["temperature"] += 0.1
params.update({
"num_ctx": 2048,
"repeat_penalty": 1.1,
})
self.llm.configurer(**params)
def _verifier_image(self, image_path: str) -> bool:
"""
Vérifie si l'image existe et est accessible
logger.info("Envoi de la requête au LLM")
response = self.llm.interroger(prompt)
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
logger.info(f"Réponse reçue pour l'image {image_description}: {response[:100]}...")
self.ajouter_historique("analyse_image", {"image": image_description, "contexte": contexte}, response)
return response
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 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
}
}
# Créer un prompt détaillé pour l'analyse d'image avec le contexte du ticket
prompt = f"""Analyse cette image en tenant compte du contexte suivant du ticket de support technique:
CONTEXTE DU TICKET:
{contexte}
Fournis une analyse structurée de l'image avec les sections suivantes:
1. Description factuelle de ce que montre l'image
2. Éléments techniques identifiables (messages d'erreur, versions, configurations visibles)
3. Interprétation de ce que cette image révèle sur le problème décrit dans le ticket
4. En quoi cette image est pertinente pour la résolution du problème
Sois précis et factuel. Ne fais pas de suppositions non fondées.
"""
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:
prompt_base64 = f"""Analyse cette image:
{img_base64}
En tenant compte du contexte suivant du ticket de support technique:
CONTEXTE DU TICKET:
{contexte}
Fournis une analyse structurée de l'image avec les sections suivantes:
1. Description factuelle de ce que montre l'image
2. Éléments techniques identifiables (messages d'erreur, versions, configurations visibles)
3. Interprétation de ce que cette image révèle sur le problème décrit dans le ticket
4. En quoi cette image est pertinente pour la résolution du problème
Sois précis et factuel. Ne fais pas de suppositions non fondées.
"""
response = self.llm.interroger(prompt_base64)
else:
error_message = "Impossible d'encoder l'image en base64"
logger.error(f"Erreur d'analyse pour {image_name}: {error_message}")
print(f" ERREUR: {error_message}")
# Retourner un résultat d'erreur explicite
return {
"analyse": f"ERREUR: {error_message}. Veuillez vérifier que l'image est dans un format standard.",
"error": True,
"raw_response": "",
"metadata": {
"image_path": image_path,
"image_name": image_name,
"timestamp": self._get_timestamp(),
"error": True
}
}
# Vérifier si la réponse contient des indications que le modèle ne peut pas analyser l'image
error_phrases = [
"je ne peux pas directement visualiser",
"je n'ai pas accès à l'image",
"je ne peux pas voir l'image",
"sans accès direct à l'image",
"je n'ai pas la possibilité de voir",
"je ne peux pas accéder directement",
"erreur: impossible d'analyser l'image"
]
# Vérifier si une des phrases d'erreur est présente dans la réponse
if any(phrase in response.lower() for phrase in error_phrases):
logger.warning(f"Le modèle indique qu'il ne peut pas analyser l'image: {image_name}")
error_message = "Le modèle n'a pas pu analyser l'image correctement"
logger.error(f"Erreur d'analyse pour {image_name}: {error_message}")
print(f" ERREUR: {error_message}")
# Retourner un résultat d'erreur explicite
return {
"analyse": f"ERREUR: {error_message}. Veuillez vérifier que le modèle a accès à l'image ou utiliser un modèle différent.",
"error": True,
"raw_response": response,
"metadata": {
"image_path": image_path,
"image_name": image_name,
"timestamp": self._get_timestamp(),
"error": True
}
}
logger.info(f"Réponse reçue pour l'image {image_name}: {response[:100]}...")
# Créer un dictionnaire de résultat avec l'analyse et les métadonnées
result = {
"analyse": response,
"metadata": {
"image_path": image_path,
"image_name": image_name,
"timestamp": self._get_timestamp(),
"model_info": {
"model": getattr(self.llm, "modele", str(type(self.llm))),
"temperature": self.temperature,
"top_p": self.top_p,
"max_tokens": self.max_tokens
}
}
}
# Enregistrer l'analyse dans l'historique avec contexte et prompt
self.ajouter_historique("analyse_image",
{
"image_path": image_path,
"contexte": contexte,
"prompt": prompt
},
response)
return result
except Exception as e:
error_message = f"Erreur lors de l'analyse de l'image: {str(e)}"
logger.error(error_message)
print(f" ERREUR: {error_message}")
# Retourner un résultat par défaut en cas d'erreur
return {
"analyse": f"ERREUR: {error_message}",
"error": True,
"metadata": {
"image_path": image_path,
"image_name": image_name,
"timestamp": self._get_timestamp(),
"error": True
}
}
def _get_timestamp(self) -> str:
"""Retourne un timestamp au format YYYYMMDD_HHMMSS"""
from datetime import datetime
return datetime.now().strftime("%Y%m%d_%H%M%S")

View File

@ -1,5 +1,10 @@
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")
@ -8,19 +13,373 @@ class AgentImageSorter(BaseAgent):
Agent pour trier les images et identifier celles qui sont pertinentes.
"""
def __init__(self, llm):
super().__init__("AgentImageSorter", llm, "image_sorter")
super().__init__("AgentImageSorter", llm)
# Configuration locale de l'agent (remplace AgentConfig)
self.temperature = 0.2
self.top_p = 0.8
self.max_tokens = 300
self.system_prompt = """Tu es un expert en tri d'images pour le support technique de BRG_Lab.
Ta mission est de déterminer si une image est pertinente pour le support technique de logiciels.
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
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"."""
# 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
def executer(self, image_path: str) -> bool:
logger.info(f"Évaluation de la pertinence de l'image: {image_path}")
print(f" AgentImageSorter: Évaluation de {image_path}")
# Appliquer les paramètres
if hasattr(self.llm, "configurer"):
params = {
"temperature": self.temperature,
"top_p": self.top_p,
"max_tokens": self.max_tokens
}
# Ajustements selon le type de modèle
if "mistral_medium" in self.llm.__class__.__name__.lower():
params["temperature"] += 0.05
params["max_tokens"] = 1000
elif "pixtral" in self.llm.__class__.__name__.lower():
params["temperature"] -= 0.05
elif "ollama" in self.llm.__class__.__name__.lower():
params["temperature"] += 0.1
params.update({
"num_ctx": 2048,
"repeat_penalty": 1.1,
})
self.llm.configurer(**params)
def _verifier_image(self, image_path: str) -> bool:
"""
Vérifie si l'image existe et est accessible
prompt = f"Détermine si cette image est pertinente pour un ticket technique: {image_path}"
response = self.llm.interroger(prompt)
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
# Simple heuristique: si la réponse contient "oui", "pertinent" ou "important"
is_relevant = any(mot in response.lower() for mot in ["oui", "pertinent", "important", "utile", "yes", "relevant"])
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 ""
logger.info(f"Image {image_path} considérée comme {'pertinente' if is_relevant else 'non pertinente'}")
self.ajouter_historique("tri_image", image_path, response)
return is_relevant
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
prompt = f"""Est-ce une image pertinente pour un ticket de support technique?
Réponds simplement par 'oui' ou 'non' suivi d'une brève explication."""
# 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 = f"""Analyse cette image:
{img_base64}
Est-ce une image pertinente pour un ticket de support technique?
Réponds simplement par 'oui' ou 'non' suivi d'une brève explication."""
response = self.llm.interroger(prompt_base64)
else:
error_message = "Impossible d'encoder l'image en base64"
logger.error(f"Erreur d'analyse pour {image_name}: {error_message}")
print(f" ERREUR: {error_message}")
return {
"is_relevant": False,
"reason": f"Erreur d'analyse: {error_message}",
"raw_response": "",
"error": True,
"metadata": {
"image_path": image_path,
"image_name": image_name,
"timestamp": self._get_timestamp(),
"error": True
}
}
# Vérifier si la réponse contient des indications que le modèle ne peut pas analyser l'image
error_phrases = [
"je ne peux pas directement visualiser",
"je n'ai pas accès à l'image",
"je ne peux pas voir l'image",
"sans accès direct à l'image",
"je n'ai pas la possibilité de voir",
"je ne peux pas accéder directement",
"erreur: impossible d'analyser l'image"
]
# Vérifier si une des phrases d'erreur est présente dans la réponse
if any(phrase in response.lower() for phrase in error_phrases):
logger.warning(f"Le modèle indique qu'il ne peut pas analyser l'image: {image_name}")
error_message = "Le modèle n'a pas pu analyser l'image correctement"
logger.error(f"Erreur d'analyse pour {image_name}: {error_message}")
print(f" ERREUR: {error_message}")
# Retourner un résultat d'erreur explicite
return {
"is_relevant": False,
"reason": f"Erreur d'analyse: {error_message}",
"raw_response": response,
"error": True,
"metadata": {
"image_path": image_path,
"image_name": image_name,
"timestamp": self._get_timestamp(),
"error": True
}
}
# Analyse de la réponse pour déterminer la pertinence
is_relevant, reason = self._analyser_reponse(response)
logger.info(f"Image {image_name} considérée comme {'pertinente' if is_relevant else 'non pertinente'}")
print(f" Décision: Image {image_name} {'pertinente' if is_relevant else 'non pertinente'}")
# Préparer le résultat
result = {
"is_relevant": is_relevant,
"reason": reason,
"raw_response": response,
"metadata": {
"image_path": image_path,
"image_name": image_name,
"timestamp": self._get_timestamp(),
"model_info": {
"model": getattr(self.llm, "modele", str(type(self.llm))),
"temperature": self.temperature,
"top_p": self.top_p,
"max_tokens": self.max_tokens
}
}
}
# Enregistrer la décision et le raisonnement dans l'historique
self.ajouter_historique("tri_image",
{
"image_path": image_path,
"prompt": prompt
},
{
"response": response,
"is_relevant": is_relevant,
"reason": reason
})
return result
except Exception as e:
logger.error(f"Erreur lors de l'analyse de l'image {image_name}: {str(e)}")
print(f" ERREUR: Impossible d'analyser l'image {image_name}")
# Retourner un résultat par défaut en cas d'erreur
return {
"is_relevant": False, # Par défaut, considérer non pertinent en cas d'erreur
"reason": f"Erreur d'analyse: {str(e)}",
"raw_response": "",
"error": True,
"metadata": {
"image_path": image_path,
"image_name": image_name,
"timestamp": self._get_timestamp(),
"error": True
}
}
def _analyser_reponse(self, response: str) -> Tuple[bool, str]:
"""
Analyse la réponse du LLM pour déterminer la pertinence et extraire le raisonnement
Args:
response: Réponse brute du LLM
Returns:
Tuple (is_relevant, reason) contenant la décision et le raisonnement
"""
# Convertir en minuscule pour faciliter la comparaison
response_lower = response.lower()
# Détection directe des réponses négatives en début de texte
first_line = response_lower.split('\n')[0] if '\n' in response_lower else response_lower[:50]
starts_with_non = first_line.strip().startswith("non") or first_line.strip().startswith("non.")
# Détection explicite d'une réponse négative au début de la réponse
explicit_negative = starts_with_non or any(neg_start in first_line for neg_start in ["non pertinent", "pas pertinent"])
# Détection explicite d'une réponse positive au début de la réponse
explicit_positive = first_line.strip().startswith("oui") or first_line.strip().startswith("pertinent")
# Si une réponse explicite est détectée, l'utiliser directement
if explicit_negative:
is_relevant = False
elif explicit_positive:
is_relevant = True
else:
# Sinon, utiliser l'analyse par mots-clés
# Mots clés positifs forts
positive_keywords = ["oui", "pertinent", "pertinente", "utile", "important", "relevante",
"capture d'écran", "message d'erreur", "interface logicielle",
"configuration", "technique", "diagnostic"]
# Mots clés négatifs forts
negative_keywords = ["non", "pas pertinent", "non pertinente", "inutile", "irrelevant",
"photo personnelle", "marketing", "sans rapport", "hors sujet",
"décorative", "logo"]
# Compter les occurrences de mots clés
positive_count = sum(1 for kw in positive_keywords if kw in response_lower)
negative_count = sum(1 for kw in negative_keywords if kw in response_lower)
# Heuristique de décision basée sur la prépondérance des mots clés
is_relevant = positive_count > negative_count
# Extraire le raisonnement (les dernières phrases de la réponse)
lines = response.split('\n')
reason_lines = []
for line in reversed(lines):
if line.strip():
reason_lines.insert(0, line.strip())
if len(reason_lines) >= 2: # Prendre les 2 dernières lignes non vides
break
reason = " ".join(reason_lines) if reason_lines else "Décision basée sur l'analyse des mots-clés"
# Log détaillé de l'analyse
logger.debug(f"Analyse de la réponse: \n - Réponse brute: {response[:100]}...\n"
f" - Commence par 'non': {starts_with_non}\n"
f" - Détection explicite négative: {explicit_negative}\n"
f" - Détection explicite positive: {explicit_positive}\n"
f" - Décision finale: {'pertinente' if is_relevant else 'non pertinente'}\n"
f" - Raison: {reason}")
return is_relevant, reason
def _get_timestamp(self) -> str:
"""Retourne un timestamp au format YYYYMMDD_HHMMSS"""
from datetime import datetime
return datetime.now().strftime("%Y%m%d_%H%M%S")

View File

@ -1,25 +1,160 @@
from .base_agent import BaseAgent
from typing import Dict, Any
import logging
import json
logger = logging.getLogger("AgentJsonAnalyser")
logger = logging.getLogger("AgentJSONAnalyser")
class AgentJsonAnalyser(BaseAgent):
"""
Agent pour analyser les fichiers JSON et extraire les informations pertinentes.
Agent pour analyser les tickets JSON et en extraire les informations importantes.
"""
def __init__(self, llm):
super().__init__("AgentJsonAnalyser", llm, "json_analyser")
super().__init__("AgentJsonAnalyser", llm)
# Configuration locale de l'agent (remplace AgentConfig)
self.temperature = 0.1 # Besoin d'analyse très précise
self.top_p = 0.8
self.max_tokens = 1500
self.system_prompt = """Tu es un expert en analyse de tickets pour le support informatique de BRG_Lab.
Ton rôle est d'extraire et d'analyser les informations importantes des tickets JSON.
Organise ta réponse avec les sections suivantes:
1. Résumé du problème
2. Informations techniques essentielles (logiciels, versions, etc.)
3. Contexte client (urgence, impact)
4. Pistes d'analyse suggérées
Sois précis, factuel et synthétique dans ton analyse."""
# Appliquer la configuration au LLM
self._appliquer_config_locale()
logger.info("AgentJsonAnalyser 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
def executer(self, ticket_json: Dict) -> str:
logger.info(f"Analyse du JSON: {str(ticket_json)[:100]}...")
prompt = f"Analyse ce ticket JSON et identifie les éléments importants : {ticket_json}"
# Appliquer les paramètres
if hasattr(self.llm, "configurer"):
params = {
"temperature": self.temperature,
"top_p": self.top_p,
"max_tokens": self.max_tokens
}
# Ajustements selon le type de modèle
if "mistral_medium" in self.llm.__class__.__name__.lower():
params["temperature"] += 0.05
params["max_tokens"] = 1000
elif "pixtral" in self.llm.__class__.__name__.lower():
params["temperature"] -= 0.05
elif "ollama" in self.llm.__class__.__name__.lower():
params["temperature"] += 0.1
params.update({
"num_ctx": 2048,
"repeat_penalty": 1.1,
})
self.llm.configurer(**params)
def executer(self, ticket_data: Dict) -> str:
"""
Analyse un ticket JSON pour en extraire les informations pertinentes
logger.info("Envoi de la requête au LLM")
print(f" AgentJsonAnalyser: Envoi de la requête au LLM {self.llm.modele}")
response = self.llm.interroger(prompt)
Args:
ticket_data: Dictionnaire contenant les données du ticket à analyser
Returns:
Réponse formatée contenant l'analyse du ticket
"""
logger.info(f"Analyse du ticket: {ticket_data.get('code', 'Inconnu')}")
print(f"AgentJsonAnalyser: Analyse du ticket {ticket_data.get('code', 'Inconnu')}")
logger.info(f"Réponse reçue: {response[:100]}...")
self.ajouter_historique("analyse_json", ticket_json, response)
return response
# Préparer le ticket pour l'analyse
ticket_formate = self._formater_ticket_pour_analyse(ticket_data)
# Créer le prompt pour l'analyse
prompt = f"""Analyse ce ticket de support technique et fournis une synthèse structurée:
{ticket_formate}
Réponds de manière factuelle, en te basant uniquement sur les informations fournies."""
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_data.get("code", "Inconnu"),
"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
Args:
ticket_data: Les données du ticket
Returns:
Représentation textuelle formatée du ticket
"""
# Initialiser avec les informations de base
info = f"## TICKET {ticket_data.get('code', 'Inconnu')}: {ticket_data.get('name', 'Sans titre')}\n\n"
# Ajouter la description
description = ticket_data.get('description', '')
if description:
info += f"## DESCRIPTION\n{description}\n\n"
# Ajouter les informations du ticket
info += "## INFORMATIONS DU TICKET\n"
for key, value in ticket_data.items():
if key not in ['code', 'name', 'description', 'messages', 'metadata'] and value:
info += f"- {key}: {value}\n"
info += "\n"
# Ajouter les messages (conversations)
messages = ticket_data.get('messages', [])
if messages:
info += "## ÉCHANGES ET MESSAGES\n"
for i, msg in enumerate(messages):
sender = msg.get('from', 'Inconnu')
date = msg.get('date', 'Date inconnue')
content = msg.get('content', '')
info += f"### Message {i+1} - De: {sender} - Date: {date}\n{content}\n\n"
# Ajouter les métadonnées techniques si présentes
metadata = ticket_data.get('metadata', {})
if metadata:
info += "## MÉTADONNÉES TECHNIQUES\n"
info += json.dumps(metadata, indent=2, ensure_ascii=False)
info += "\n"
return info
def _get_timestamp(self) -> str:
"""Retourne un timestamp au format YYYYMMDD_HHMMSS"""
from datetime import datetime
return datetime.now().strftime("%Y%m%d_%H%M%S")

View File

@ -2,181 +2,363 @@ import json
import os
from .base_agent import BaseAgent
from datetime import datetime
from typing import Dict, Any, Tuple
from typing import Dict, Any, Tuple, Optional
import logging
logger = logging.getLogger("AgentReportGenerator")
class AgentReportGenerator(BaseAgent):
"""
Agent pour générer un rapport à partir des informations collectées.
Agent pour générer un rapport complet à partir des analyses de ticket et d'images
"""
def __init__(self, llm):
super().__init__("AgentReportGenerator", llm, "report_generator")
super().__init__("AgentReportGenerator", llm)
# Configuration locale de l'agent (remplace AgentConfig)
self.temperature = 0.4 # Génération de rapport factuelle mais bien structurée
self.top_p = 0.9
self.max_tokens = 2500
self.system_prompt = """Tu es un expert en génération de rapports techniques pour BRG_Lab.
Ta mission est de synthétiser toutes les analyses (JSON et images) en un rapport structuré et exploitable.
Structure ton rapport ainsi:
1. Résumé exécutif: Synthèse du problème et des conclusions principales
2. Analyse du ticket: Détails extraits du ticket client
3. Analyse des images: Résumé des images pertinentes et leur contribution
4. Diagnostic technique: Interprétation consolidée des informations
5. Recommandations: Actions suggérées pour résoudre le problème
Chaque section doit être factuelle, précise et orientée solution.
Inclus tous les détails techniques pertinents (versions, configurations, messages d'erreur).
Assure une traçabilité complète entre les données sources et tes conclusions."""
# Appliquer la configuration au LLM
self._appliquer_config_locale()
logger.info("AgentReportGenerator 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
def executer(self, rapport_data: Dict, filename: str) -> Tuple[str, str]:
logger.info(f"Génération du rapport pour le fichier: {filename}")
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
# Appliquer les paramètres
if hasattr(self.llm, "configurer"):
params = {
"temperature": self.temperature,
"top_p": self.top_p,
"max_tokens": self.max_tokens
}
# Ajustements selon le type de modèle
if "mistral_medium" in self.llm.__class__.__name__.lower():
params["temperature"] += 0.05
params["max_tokens"] = 1000
elif "pixtral" in self.llm.__class__.__name__.lower():
params["temperature"] -= 0.05
elif "ollama" in self.llm.__class__.__name__.lower():
params["temperature"] += 0.1
params.update({
"num_ctx": 2048,
"repeat_penalty": 1.1,
})
self.llm.configurer(**params)
# Ajouter les métadonnées des LLM utilisés
if "metadata" not in rapport_data:
rapport_data["metadata"] = {}
def executer(self, rapport_data: Dict, rapport_dir: str) -> Tuple[Optional[str], Optional[str]]:
"""
Génère un rapport à partir des analyses effectuées
rapport_data["metadata"]["report_generator"] = {
Args:
rapport_data: Dictionnaire contenant toutes les données analysées
rapport_dir: Répertoire sauvegarder le rapport
Returns:
Tuple (chemin vers le rapport JSON, chemin vers le rapport Markdown)
"""
# Récupérer l'ID du ticket depuis les données
ticket_id = rapport_data.get("ticket_id", "")
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", "")
if not ticket_id:
ticket_id = os.path.basename(os.path.dirname(rapport_dir))
if not ticket_id.startswith("T"):
# Dernier recours, utiliser le dernier segment du chemin
ticket_id = os.path.basename(rapport_dir)
logger.info(f"Génération du rapport pour le ticket: {ticket_id}")
print(f"AgentReportGenerator: Génération du rapport pour {ticket_id}")
# S'assurer que le répertoire existe
if not os.path.exists(rapport_dir):
os.makedirs(rapport_dir)
try:
# Préparer les données formatées pour l'analyse
ticket_analyse = rapport_data.get("ticket_analyse", "")
if not ticket_analyse and "analyse_json" in rapport_data:
ticket_analyse = rapport_data.get("analyse_json", "Aucune analyse de ticket disponible")
# Préparer les données d'analyse d'images
images_analyses = []
analyse_images_data = rapport_data.get("analyse_images", {})
# Collecter des informations sur les agents et LLM utilisés
agents_info = self._collecter_info_agents(rapport_data)
# Transformer les analyses d'images en liste structurée pour le prompt
for image_path, analyse_data in analyse_images_data.items():
image_name = os.path.basename(image_path)
# Récupérer l'analyse détaillée si elle existe
analyse_detail = None
if "analysis" in analyse_data and analyse_data["analysis"]:
if isinstance(analyse_data["analysis"], dict) and "analyse" in analyse_data["analysis"]:
analyse_detail = analyse_data["analysis"]["analyse"]
elif isinstance(analyse_data["analysis"], dict):
analyse_detail = str(analyse_data["analysis"])
# Si l'analyse n'a pas été trouvée mais que le tri indique que l'image est pertinente
if not analyse_detail and "sorting" in analyse_data and analyse_data["sorting"].get("is_relevant", False):
analyse_detail = f"Image marquée comme pertinente. Raison: {analyse_data['sorting'].get('reason', 'Non spécifiée')}"
# Ajouter l'analyse à la liste si elle existe
if analyse_detail:
images_analyses.append({
"image_name": image_name,
"analyse": analyse_detail
})
num_images = len(images_analyses)
# Créer un prompt détaillé
prompt = f"""Génère un rapport technique complet pour le ticket #{ticket_id}, en te basant sur les analyses suivantes.
## ANALYSE DU TICKET
{ticket_analyse}
## ANALYSES DES IMAGES ({num_images} images)
"""
# Ajouter l'analyse de chaque image
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"
prompt += f"""
Ton rapport doit être structuré avec les sections suivantes:
1. Résumé exécutif
2. Analyse détaillée du ticket
3. Analyse des images pertinentes
4. Diagnostic technique
5. Recommandations
Fournir le rapport en format Markdown, avec des titres clairs et une structure cohérente.
Assure-toi d'inclure toutes les informations techniques importantes et les liens entre les différentes analyses.
"""
# Appeler le LLM pour générer le rapport
logger.info("Interrogation du LLM pour la génération du rapport")
rapport_contenu = self.llm.interroger(prompt)
# Créer les noms de fichiers pour la sauvegarde
timestamp = self._get_timestamp()
base_filename = f"{ticket_id}_{timestamp}"
json_path = os.path.join(rapport_dir, f"{base_filename}.json")
md_path = os.path.join(rapport_dir, f"{base_filename}.md")
# Collecter les métadonnées du rapport avec détails sur les agents et LLM utilisés
metadata = {
"timestamp": timestamp,
"model": getattr(self.llm, "modele", str(type(self.llm))),
"temperature": self.temperature,
"top_p": self.top_p,
"max_tokens": self.max_tokens,
"system_prompt": self.system_prompt,
"agents_info": agents_info
}
# Sauvegarder le rapport au format JSON (données brutes + rapport généré)
rapport_data_complet = rapport_data.copy()
rapport_data_complet["rapport_genere"] = rapport_contenu
rapport_data_complet["metadata"] = metadata
with open(json_path, "w", encoding="utf-8") as f:
json.dump(rapport_data_complet, f, ensure_ascii=False, indent=2)
# Générer et sauvegarder le rapport au format Markdown basé directement sur le JSON
markdown_content = self._generer_markdown_depuis_json(rapport_data_complet)
with open(md_path, "w", encoding="utf-8") as f:
f.write(markdown_content)
logger.info(f"Rapport sauvegardé: {json_path} et {md_path}")
except Exception as e:
error_message = f"Erreur lors de la génération du rapport: {str(e)}"
logger.error(error_message)
print(f" ERREUR: {error_message}")
return None, None
# Enregistrer l'historique
self.ajouter_historique("generation_rapport",
{
"rapport_dir": rapport_dir,
"rapport_data": rapport_data,
"prompt": prompt,
"temperature": self.temperature,
"top_p": self.top_p,
"max_tokens": self.max_tokens
},
{
"json_path": json_path,
"md_path": md_path,
"rapport_contenu": rapport_contenu[:300] + ("..." if len(rapport_contenu) > 300 else "")
})
message = f"Rapports générés dans: {rapport_dir}"
print(f" {message}")
return json_path, md_path
def _collecter_info_agents(self, rapport_data: Dict) -> Dict:
"""
Collecte des informations sur les agents utilisés dans l'analyse
Args:
rapport_data: Données du rapport
Returns:
Dictionnaire contenant les informations sur les agents
"""
agents_info = {}
# Informations sur l'agent JSON Analyser
if "analyse_json" in rapport_data:
json_analysis = rapport_data["analyse_json"]
# Vérifier si l'analyse JSON contient des métadonnées
if isinstance(json_analysis, dict) and "metadata" in json_analysis:
agents_info["json_analyser"] = json_analysis["metadata"]
# Informations sur les agents d'image
if "analyse_images" in rapport_data and rapport_data["analyse_images"]:
# Image Sorter
sorter_info = {}
analyser_info = {}
for img_path, img_data in rapport_data["analyse_images"].items():
# Collecter info du sorter
if "sorting" in img_data and isinstance(img_data["sorting"], dict) and "metadata" in img_data["sorting"]:
if "model_info" in img_data["sorting"]["metadata"]:
sorter_info = img_data["sorting"]["metadata"]["model_info"]
# Collecter info de l'analyser
if "analysis" in img_data and img_data["analysis"] and isinstance(img_data["analysis"], dict) and "metadata" in img_data["analysis"]:
if "model_info" in img_data["analysis"]["metadata"]:
analyser_info = img_data["analysis"]["metadata"]["model_info"]
# Une fois qu'on a trouvé les deux, on peut sortir
if sorter_info and analyser_info:
break
if sorter_info:
agents_info["image_sorter"] = sorter_info
if analyser_info:
agents_info["image_analyser"] = analyser_info
# Ajouter les informations de l'agent report generator
agents_info["report_generator"] = {
"model": getattr(self.llm, "modele", str(type(self.llm))),
"configuration": self.config.to_dict()
"temperature": self.temperature,
"top_p": self.top_p,
"max_tokens": self.max_tokens
}
# Créer les dossiers si nécessaire
reports_dir = "../reports"
json_dir = os.path.join(reports_dir, "json_reports")
md_dir = os.path.join(reports_dir, "markdown_reports")
os.makedirs(json_dir, exist_ok=True)
os.makedirs(md_dir, exist_ok=True)
return agents_info
def _generer_markdown_depuis_json(self, rapport_data: Dict) -> str:
"""
Génère un rapport Markdown directement à partir des données JSON
# Sauvegarde JSON
json_path = f"{json_dir}/{filename}_{timestamp}.json"
with open(json_path, "w", encoding="utf-8") as f_json:
json.dump(rapport_data, f_json, ensure_ascii=False, indent=4)
Args:
rapport_data: Données JSON complètes du rapport
Returns:
Contenu Markdown du rapport
"""
ticket_id = rapport_data.get("ticket_id", "")
timestamp = rapport_data.get("metadata", {}).get("timestamp", self._get_timestamp())
logger.info(f"Rapport JSON sauvegardé à: {json_path}")
# Contenu de base du rapport (partie générée par le LLM)
rapport_contenu = rapport_data.get("rapport_genere", "")
# Sauvegarde Markdown
md_path = f"{md_dir}/{filename}_{timestamp}.md"
with open(md_path, "w", encoding="utf-8") as f_md:
# En-tête du rapport
ticket_id = rapport_data.get("ticket_id", filename)
f_md.write(f"# Rapport d'Analyse du Ticket {ticket_id}\n\n")
f_md.write(f"*Généré le: {datetime.now().strftime('%d/%m/%Y à %H:%M:%S')}*\n\n")
# Résumé
metadata = rapport_data.get("metadata", {})
etapes = metadata.get("etapes", [])
f_md.write("## Résumé\n\n")
f_md.write(f"- **ID Ticket**: {ticket_id}\n")
f_md.write(f"- **Date d'analyse**: {metadata.get('timestamp_debut', timestamp)}\n")
f_md.write(f"- **Nombre d'étapes**: {len(etapes)}\n\n")
# Vue d'ensemble des agents et modèles utilisés
f_md.write("## Modèles et Paramètres Utilisés\n\n")
f_md.write("### Vue d'ensemble\n\n")
f_md.write("| Agent | Modèle | Température | Top-P | Max Tokens |\n")
f_md.write("|-------|--------|-------------|-------|------------|\n")
for agent_name in ["json_agent", "image_sorter", "image_analyser", "report_generator"]:
agent_info = metadata.get(agent_name, {})
if agent_info.get("status") == "non configuré":
continue
model = agent_info.get("model", "N/A")
config = agent_info.get("configuration", {}).get("config", {})
temp = config.get("temperature", "N/A")
top_p = config.get("top_p", "N/A")
max_tokens = config.get("max_tokens", "N/A")
f_md.write(f"| {agent_name} | {model} | {temp} | {top_p} | {max_tokens} |\n")
f_md.write("\n")
# Détails des paramètres par agent
f_md.write("### Détails des Paramètres\n\n")
f_md.write("```json\n")
agents_config = {}
for agent_name in ["json_agent", "image_sorter", "image_analyser", "report_generator"]:
if agent_name in metadata and "configuration" in metadata[agent_name]:
agents_config[agent_name] = metadata[agent_name]["configuration"]
f_md.write(json.dumps(agents_config, indent=2))
f_md.write("\n```\n\n")
# Détails des prompts système
f_md.write("### Prompts Système\n\n")
for agent_name in ["json_agent", "image_sorter", "image_analyser", "report_generator"]:
agent_info = metadata.get(agent_name, {})
if agent_info.get("status") == "non configuré":
continue
config = agent_info.get("configuration", {}).get("config", {})
prompt = config.get("system_prompt", "")
if prompt:
f_md.write(f"**{agent_name}**:\n")
f_md.write("```\n")
f_md.write(prompt)
f_md.write("\n```\n\n")
# Étapes d'analyse
f_md.write("## Étapes d'Analyse\n\n")
for i, etape in enumerate(etapes, 1):
agent = etape.get("agent", "")
action = etape.get("action", "")
timestamp = etape.get("timestamp", "")
image = etape.get("image", "")
title = f"### {i}. {agent.replace('_', ' ').title()}"
if image:
title += f" - Image: {image}"
f_md.write(f"{title}\n\n")
f_md.write(f"- **Action**: {action}\n")
f_md.write(f"- **Timestamp**: {timestamp}\n")
# Métadonnées du modèle pour cette étape
etape_metadata = etape.get("metadata", {})
model = etape_metadata.get("model", "")
config = etape_metadata.get("configuration", {}).get("config", {})
duree = etape_metadata.get("duree_traitement", "")
f_md.write(f"- **Modèle**: {model}\n")
if duree:
f_md.write(f"- **Durée de traitement**: {duree}\n")
# Paramètres spécifiques pour cette exécution
if config:
f_md.write("- **Paramètres**:\n")
f_md.write("```json\n")
params = {k: v for k, v in config.items() if k != "system_prompt"}
f_md.write(json.dumps(params, indent=2))
f_md.write("\n```\n")
# Input/Output (limités en taille)
input_data = etape.get("input", "")
output_data = etape.get("output", "")
if input_data:
f_md.write("- **Entrée**:\n")
f_md.write("```\n")
f_md.write(str(input_data)[:300] + ("..." if len(str(input_data)) > 300 else ""))
f_md.write("\n```\n")
if output_data:
f_md.write("- **Sortie**:\n")
f_md.write("```\n")
f_md.write(str(output_data)[:300] + ("..." if len(str(output_data)) > 300 else ""))
f_md.write("\n```\n")
f_md.write("\n")
# Résultats des analyses
if "analyse_json" in rapport_data:
f_md.write("## Résultat de l'Analyse JSON\n\n")
f_md.write("```\n")
f_md.write(str(rapport_data["analyse_json"]))
f_md.write("\n```\n\n")
# Analyses des images
if "analyse_images" in rapport_data and rapport_data["analyse_images"]:
f_md.write("## Analyses des Images\n\n")
for image_path, analysis in rapport_data["analyse_images"].items():
image_name = os.path.basename(image_path)
f_md.write(f"### Image: {image_name}\n\n")
f_md.write("```\n")
f_md.write(str(analysis))
f_md.write("\n```\n\n")
# Informations supplémentaires
f_md.write("## Informations Supplémentaires\n\n")
f_md.write(f"Pour plus de détails, consultez le fichier JSON complet: `{os.path.basename(json_path)}`\n\n")
# Entête du document
markdown = f"# Rapport d'analyse du ticket #{ticket_id}\n\n"
markdown += f"*Généré le: {timestamp}*\n\n"
logger.info(f"Rapport généré: {md_path}")
self.ajouter_historique("generation_rapport", filename, f"Rapport généré: {md_path}")
return json_path, md_path
# Ajouter le rapport principal
markdown += rapport_contenu + "\n\n"
# Ajouter les informations techniques (agents, LLM, paramètres)
markdown += "## Informations techniques\n\n"
# Ajouter les informations sur les agents utilisés
agents_info = rapport_data.get("metadata", {}).get("agents_info", {})
if agents_info:
markdown += "### Agents et modèles utilisés\n\n"
# Agent JSON Analyser
if "json_analyser" in agents_info:
info = agents_info["json_analyser"]
markdown += "#### Agent d'analyse de texte\n"
markdown += f"- **Modèle**: {info.get('model', 'Non spécifié')}\n"
markdown += f"- **Température**: {info.get('temperature', 'Non spécifiée')}\n"
markdown += f"- **Top-p**: {info.get('top_p', 'Non spécifié')}\n"
markdown += f"- **Max tokens**: {info.get('max_tokens', 'Non spécifié')}\n\n"
# Agent Image Sorter
if "image_sorter" in agents_info:
info = agents_info["image_sorter"]
markdown += "#### Agent de tri d'images\n"
markdown += f"- **Modèle**: {info.get('model', 'Non spécifié')}\n"
markdown += f"- **Température**: {info.get('temperature', 'Non spécifiée')}\n"
markdown += f"- **Top-p**: {info.get('top_p', 'Non spécifié')}\n"
markdown += f"- **Max tokens**: {info.get('max_tokens', 'Non spécifié')}\n\n"
# Agent Image Analyser
if "image_analyser" in agents_info:
info = agents_info["image_analyser"]
markdown += "#### Agent d'analyse d'images\n"
markdown += f"- **Modèle**: {info.get('model', 'Non spécifié')}\n"
markdown += f"- **Température**: {info.get('temperature', 'Non spécifiée')}\n"
markdown += f"- **Top-p**: {info.get('top_p', 'Non spécifié')}\n"
markdown += f"- **Max tokens**: {info.get('max_tokens', 'Non spécifié')}\n\n"
# Agent Report Generator
if "report_generator" in agents_info:
info = agents_info["report_generator"]
markdown += "#### Agent de génération de rapport\n"
markdown += f"- **Modèle**: {info.get('model', 'Non spécifié')}\n"
markdown += f"- **Température**: {info.get('temperature', 'Non spécifiée')}\n"
markdown += f"- **Top-p**: {info.get('top_p', 'Non spécifié')}\n"
markdown += f"- **Max tokens**: {info.get('max_tokens', 'Non spécifié')}\n\n"
# Statistiques d'analyse
markdown += "### Statistiques\n\n"
if "metadata" in rapport_data:
metadata = rapport_data["metadata"]
if "images_analysees" in metadata:
markdown += f"- **Images analysées**: {metadata['images_analysees']}\n"
if "images_pertinentes" in metadata:
markdown += f"- **Images pertinentes**: {metadata['images_pertinentes']}\n"
return markdown
def _get_timestamp(self) -> str:
"""Retourne un timestamp au format YYYYMMDD_HHMMSS"""
return datetime.now().strftime("%Y%m%d_%H%M%S")

View File

@ -1,59 +1,19 @@
from abc import ABC, abstractmethod
from typing import List, Dict, Any, Optional
from .utils.agent_config import AgentConfig
class BaseAgent(ABC):
"""
Classe de base pour les agents.
"""
def __init__(self, nom: str, llm: Any, role: Optional[str] = None):
def __init__(self, nom: str, llm: Any):
self.nom = nom
self.llm = llm
self.historique: List[Dict[str, Any]] = []
# Détecter le type de modèle
model_type = self._detecter_model_type()
# Définir le rôle par défaut si non spécifié
agent_role = role if role is not None else nom.lower().replace("agent", "").strip()
# Créer la configuration d'agent
self.config = AgentConfig(agent_role, model_type)
# Appliquer les paramètres au LLM
self._appliquer_config()
def _detecter_model_type(self) -> str:
"""
Détecte le type de modèle LLM.
"""
llm_class_name = self.llm.__class__.__name__.lower()
if "mistral" in llm_class_name:
return "mistral"
elif "pixtral" in llm_class_name:
return "pixtral"
elif "ollama" in llm_class_name:
return "ollama"
else:
return "generic"
def _appliquer_config(self) -> None:
"""
Applique la configuration au modèle LLM.
"""
# Appliquer le prompt système
if hasattr(self.llm, "prompt_system"):
self.llm.prompt_system = self.config.get_system_prompt()
# Appliquer les paramètres
if hasattr(self.llm, "configurer"):
self.llm.configurer(**self.config.get_params())
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))),
"configuration": self.config.to_dict(),
"duree_traitement": str(getattr(self.llm, "dureeTraitement", "N/A"))
}

View File

@ -1,3 +0,0 @@
from .agent_config import AgentConfig
__all__ = ['AgentConfig']

View File

@ -1,114 +0,0 @@
from typing import Dict, Any, Optional
class AgentConfig:
"""
Classe pour configurer les paramètres LLM par rôle d'agent.
Cette classe permet d'harmoniser les paramètres entre différents LLM.
"""
# Configurations par défaut pour chaque rôle d'agent
DEFAULT_CONFIGS = {
"json_analyser": {
"temperature": 0.2, # Besoin d'analyse précise
"top_p": 0.9,
"max_tokens": 1500,
"system_prompt": "Tu es un assistant spécialisé dans l'analyse de tickets JSON. Extrais les informations pertinentes et structure ta réponse de manière claire.",
},
"image_sorter": {
"temperature": 0.3, # Décision de classification binaire
"top_p": 0.9,
"max_tokens": 200,
"system_prompt": "Tu es un assistant spécialisé dans le tri d'images. Tu dois décider si une image est pertinente ou non pour BRG_Lab.",
},
"image_analyser": {
"temperature": 0.5, # Analyse créative mais factuelle
"top_p": 0.95,
"max_tokens": 1000,
"system_prompt": "Tu es un assistant spécialisé dans l'analyse d'images. Décris ce que tu vois en tenant compte du contexte fourni.",
},
"report_generator": {
"temperature": 0.7, # Génération de rapport plus créative
"top_p": 1.0,
"max_tokens": 2000,
"system_prompt": "Tu es un assistant spécialisé dans la génération de rapports. Synthétise les informations fournies de manière claire et professionnelle.",
}
}
# Paramètres spécifiques à chaque type de modèle pour harmoniser les performances
MODEL_ADJUSTMENTS = {
"mistral": {}, # Pas d'ajustement nécessaire, modèle de référence
"pixtral": {
"temperature": -0.1, # Légèrement plus conservateur
},
"ollama": {
"temperature": +0.1, # Légèrement plus créatif
"num_ctx": 2048,
"repeat_penalty": 1.1,
}
}
def __init__(self, role: str, model_type: str = "mistral"):
"""
Initialise la configuration avec les paramètres appropriés pour le rôle et le modèle.
Args:
role: Rôle de l'agent ('json_analyser', 'image_sorter', etc.)
model_type: Type de modèle ('mistral', 'pixtral', 'ollama', etc.)
"""
self.role = role
self.model_type = model_type.lower()
# Récupérer la configuration de base pour ce rôle
if role in self.DEFAULT_CONFIGS:
self.config = self.DEFAULT_CONFIGS[role].copy()
else:
# Configuration par défaut
self.config = {
"temperature": 0.5,
"top_p": 0.9,
"max_tokens": 1000,
"system_prompt": ""
}
# Appliquer les ajustements spécifiques au modèle
if model_type.lower() in self.MODEL_ADJUSTMENTS:
adjustments = self.MODEL_ADJUSTMENTS[model_type.lower()]
for key, adjustment in adjustments.items():
if key in self.config:
if isinstance(adjustment, (int, float)) and isinstance(self.config[key], (int, float)):
self.config[key] += adjustment # Ajustement relatif
else:
self.config[key] = adjustment # Remplacement direct
else:
self.config[key] = adjustment # Ajout d'un nouveau paramètre
def get_params(self) -> Dict[str, Any]:
"""
Retourne tous les paramètres sauf le prompt système.
"""
params = self.config.copy()
if "system_prompt" in params:
params.pop("system_prompt")
return params
def get_system_prompt(self) -> str:
"""
Retourne le prompt système.
"""
return self.config.get("system_prompt", "")
def to_dict(self) -> Dict[str, Any]:
"""
Retourne toute la configuration sous forme de dictionnaire.
"""
return {
"role": self.role,
"model_type": self.model_type,
"config": self.config
}
def update(self, **kwargs) -> None:
"""
Met à jour la configuration avec les nouveaux paramètres.
"""
self.config.update(kwargs)

View File

@ -13,11 +13,10 @@ class BaseLLM(abc.ABC):
self.params: Dict[str, Any] = {
"temperature": 0.8,
"top_p": 0.9,
"top_k": 40,
"max_tokens": 1000,
"presence_penalty": 0,
"frequency_penalty": 0,
"stop": None
"stop": []
}
self.dureeTraitement: timedelta = timedelta()
@ -46,6 +45,20 @@ class BaseLLM(abc.ABC):
def _traiter_reponse(self, reponse: requests.Response) -> str:
pass
@abc.abstractmethod
def interroger_avec_image(self, image_path: str, question: str) -> str:
"""
Méthode abstraite pour interroger le LLM avec une image
Args:
image_path: Chemin vers l'image à analyser
question: Question ou instructions pour l'analyse de l'image
Returns:
Réponse du LLM
"""
pass
def interroger(self, question: str) -> str:
url = self.urlBase() + self.urlFonction()
headers = {"Content-Type": "application/json"}

View File

@ -1,5 +1,6 @@
from .base_llm import BaseLLM
import requests
import os
class MistralLarge(BaseLLM):
@ -29,3 +30,25 @@ class MistralLarge(BaseLLM):
def _traiter_reponse(self, reponse: requests.Response) -> str:
data = reponse.json()
return data["choices"][0]["message"]["content"]
def interroger_avec_image(self, image_path: str, question: str) -> str:
"""
Ce modèle ne supporte pas directement l'analyse d'images, cette méthode est fournie
pour la compatibilité avec l'interface BaseLLM mais utilise uniquement le texte.
Args:
image_path: Chemin vers l'image à analyser (ignoré par ce modèle)
question: Question ou instructions pour l'analyse
Returns:
Réponse du modèle à la question, en indiquant que l'analyse d'image n'est pas supportée
"""
image_name = os.path.basename(image_path)
prompt = f"""[Note: Ce modèle n'a pas accès à l'image demandée: {image_name}]
Question concernant l'image:
{question}
Veuillez noter que je ne peux pas traiter directement les images. Voici une réponse basée uniquement sur le texte de la question."""
return self.interroger(prompt)

View File

@ -1,5 +1,10 @@
from .base_llm import BaseLLM
import requests
import os
import base64
from PIL import Image
import io
from datetime import datetime
class MistralLargePixtral(BaseLLM):
@ -29,3 +34,116 @@ class MistralLargePixtral(BaseLLM):
def _traiter_reponse(self, reponse: requests.Response) -> str:
data = reponse.json()
return data["choices"][0]["message"]["content"]
def _encoder_image_base64(self, image_path: str) -> str:
"""
Encode une image en base64 pour l'API.
Args:
image_path: Chemin vers l'image à encoder
Returns:
Image encodée en base64 avec préfixe approprié
"""
if not os.path.isfile(image_path):
raise FileNotFoundError(f"L'image {image_path} n'a pas été trouvée")
try:
# Ouvrir l'image et la redimensionner si trop grande
with Image.open(image_path) as img:
# Redimensionner l'image si elle est trop grande (max 800x800)
max_size = 800
if img.width > max_size or img.height > max_size:
img.thumbnail((max_size, max_size), Image.Resampling.LANCZOS)
# Convertir en RGB si nécessaire (pour les formats comme PNG)
if img.mode != "RGB":
img = img.convert("RGB")
# Sauvegarder l'image en JPEG dans un buffer mémoire
buffer = io.BytesIO()
img.save(buffer, format="JPEG", quality=85)
buffer.seek(0)
# Encoder en base64
encoded_string = base64.b64encode(buffer.read()).decode("utf-8")
except Exception as e:
# Si échec avec PIL, essayer avec la méthode simple
with open(image_path, "rb") as image_file:
encoded_string = base64.b64encode(image_file.read()).decode("utf-8")
# Détecter le type de fichier
file_extension = os.path.splitext(image_path)[1].lower()
if file_extension in ['.jpg', '.jpeg']:
mime_type = 'image/jpeg'
elif file_extension == '.png':
mime_type = 'image/png'
elif file_extension == '.gif':
mime_type = 'image/gif'
elif file_extension in ['.webp']:
mime_type = 'image/webp'
else:
# Par défaut, on suppose JPEG
mime_type = 'image/jpeg'
return f"data:{mime_type};base64,{encoded_string}"
def interroger_avec_image(self, image_path: str, question: str) -> str:
"""
Analyse une image avec le modèle Pixtral
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
"""
url = self.urlBase() + self.urlFonction()
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.cleAPI()}"
}
try:
# Encoder l'image en base64
encoded_image = self._encoder_image_base64(image_path)
# 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
}
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
self.heureFin = datetime.now()
if self.heureDepart is not None and self.heureFin is not None:
self.dureeTraitement = self.heureFin - self.heureDepart
if response.status_code in [200, 201]:
self.reponseErreur = False
return self._traiter_reponse(response)
else:
self.reponseErreur = True
return f"Erreur API ({response.status_code}): {response.text}"
except Exception as e:
self.heureFin = datetime.now()
if self.heureDepart is not None and self.heureFin is not None:
self.dureeTraitement = self.heureFin - self.heureDepart
self.reponseErreur = True
return f"Erreur lors de l'analyse de l'image: {str(e)}"

View File

@ -1,22 +1,28 @@
from .base_llm import BaseLLM
import requests
import os
class MistralMedium(BaseLLM):
"""
Classe pour interagir avec le modèle Mistral Medium
"""
def __init__(self):
super().__init__("mistral-medium-latest")
self.configurer(temperature=0.2, top_p=1)
super().__init__("mistral-medium")
self.configurer(temperature=0.3, top_p=0.9) # Paramètres par défaut
def urlBase(self) -> str:
return "https://api.mistral.ai/v1/"
def cleAPI(self) -> str:
return "2iGzTzE9csRQ9IoASoUjplHwEjA200Vh"
return "2iGzTzE9csRQ9IoASoUjplHwEjA200Vh" # Clé API d'exemple, à remplacer par la vraie clé
def urlFonction(self) -> str:
return "chat/completions"
def _preparer_contenu(self, question: str) -> dict:
"""
Prépare le contenu de la requête pour l'API Mistral
"""
return {
"model": self.modele,
"messages": [
@ -27,5 +33,33 @@ class MistralMedium(BaseLLM):
}
def _traiter_reponse(self, reponse: requests.Response) -> str:
data = reponse.json()
return data["choices"][0]["message"]["content"]
"""
Traite la réponse de l'API Mistral
"""
try:
data = reponse.json()
return data["choices"][0]["message"]["content"]
except Exception as e:
return f"Erreur lors du traitement de la réponse: {str(e)}\nRéponse brute: {reponse.text}"
def interroger_avec_image(self, image_path: str, question: str) -> str:
"""
Ce modèle ne supporte pas directement l'analyse d'images, cette méthode est fournie
pour la compatibilité avec l'interface BaseLLM mais utilise uniquement le texte.
Args:
image_path: Chemin vers l'image à analyser (ignoré par ce modèle)
question: Question ou instructions pour l'analyse
Returns:
Réponse du modèle à la question, en indiquant que l'analyse d'image n'est pas supportée
"""
image_name = os.path.basename(image_path)
prompt = f"""[Note: Ce modèle n'a pas accès à l'image demandée: {image_name}]
Question concernant l'image:
{question}
Veuillez noter que je ne peux pas traiter directement les images. Voici une réponse basée uniquement sur le texte de la question."""
return self.interroger(prompt)

View File

@ -2,6 +2,7 @@ from .base_llm import BaseLLM
import requests
from datetime import timedelta
from typing import Dict, Any
import os
class Ollama(BaseLLM):
"""
@ -90,3 +91,25 @@ class Ollama(BaseLLM):
for key, value in kwargs.items():
if key in self.params:
self.params[key] = value
def interroger_avec_image(self, image_path: str, question: str) -> str:
"""
Cette implémentation d'Ollama ne supporte pas directement l'analyse d'images.
Cette méthode est fournie pour la compatibilité avec l'interface BaseLLM mais utilise uniquement le texte.
Args:
image_path: Chemin vers l'image à analyser (ignoré par ce modèle)
question: Question ou instructions pour l'analyse
Returns:
Réponse du modèle à la question, en indiquant que l'analyse d'image n'est pas supportée
"""
image_name = os.path.basename(image_path)
prompt = f"""[Note: Ce modèle n'a pas accès à l'image demandée: {image_name}]
Question concernant l'image:
{question}
Veuillez noter que je ne peux pas traiter directement les images. Voici une réponse basée uniquement sur le texte de la question."""
return self.interroger(prompt)

View File

@ -1,5 +1,10 @@
from .base_llm import BaseLLM
import requests
import os
import base64
from PIL import Image
import io
from datetime import datetime
class Pixtral12b(BaseLLM):
@ -29,3 +34,116 @@ class Pixtral12b(BaseLLM):
def _traiter_reponse(self, reponse: requests.Response) -> str:
data = reponse.json()
return data["choices"][0]["message"]["content"]
def _encoder_image_base64(self, image_path: str) -> str:
"""
Encode une image en base64 pour l'API.
Args:
image_path: Chemin vers l'image à encoder
Returns:
Image encodée en base64 avec préfixe approprié
"""
if not os.path.isfile(image_path):
raise FileNotFoundError(f"L'image {image_path} n'a pas été trouvée")
try:
# Ouvrir l'image et la redimensionner si trop grande
with Image.open(image_path) as img:
# Redimensionner l'image si elle est trop grande (max 800x800)
max_size = 800
if img.width > max_size or img.height > max_size:
img.thumbnail((max_size, max_size), Image.Resampling.LANCZOS)
# Convertir en RGB si nécessaire (pour les formats comme PNG)
if img.mode != "RGB":
img = img.convert("RGB")
# Sauvegarder l'image en JPEG dans un buffer mémoire
buffer = io.BytesIO()
img.save(buffer, format="JPEG", quality=85)
buffer.seek(0)
# Encoder en base64
encoded_string = base64.b64encode(buffer.read()).decode("utf-8")
except Exception as e:
# Si échec avec PIL, essayer avec la méthode simple
with open(image_path, "rb") as image_file:
encoded_string = base64.b64encode(image_file.read()).decode("utf-8")
# Détecter le type de fichier
file_extension = os.path.splitext(image_path)[1].lower()
if file_extension in ['.jpg', '.jpeg']:
mime_type = 'image/jpeg'
elif file_extension == '.png':
mime_type = 'image/png'
elif file_extension == '.gif':
mime_type = 'image/gif'
elif file_extension in ['.webp']:
mime_type = 'image/webp'
else:
# Par défaut, on suppose JPEG
mime_type = 'image/jpeg'
return f"data:{mime_type};base64,{encoded_string}"
def interroger_avec_image(self, image_path: str, question: str) -> str:
"""
Analyse une image avec le modèle Pixtral
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
"""
url = self.urlBase() + self.urlFonction()
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.cleAPI()}"
}
try:
# Encoder l'image en base64
encoded_image = self._encoder_image_base64(image_path)
# 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
}
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
self.heureFin = datetime.now()
if self.heureDepart is not None and self.heureFin is not None:
self.dureeTraitement = self.heureFin - self.heureDepart
if response.status_code in [200, 201]:
self.reponseErreur = False
return self._traiter_reponse(response)
else:
self.reponseErreur = True
return f"Erreur API ({response.status_code}): {response.text}"
except Exception as e:
self.heureFin = datetime.now()
if self.heureDepart is not None and self.heureFin is not None:
self.dureeTraitement = self.heureFin - self.heureDepart
self.reponseErreur = True
return f"Erreur lors de l'analyse de l'image: {str(e)}"

View File

@ -1,5 +1,10 @@
from .base_llm import BaseLLM
import requests
import os
import base64
from PIL import Image
import io
from datetime import datetime
class PixtralLarge(BaseLLM):
@ -29,3 +34,116 @@ class PixtralLarge(BaseLLM):
def _traiter_reponse(self, reponse: requests.Response) -> str:
data = reponse.json()
return data["choices"][0]["message"]["content"]
def _encoder_image_base64(self, image_path: str) -> str:
"""
Encode une image en base64 pour l'API.
Args:
image_path: Chemin vers l'image à encoder
Returns:
Image encodée en base64 avec préfixe approprié
"""
if not os.path.isfile(image_path):
raise FileNotFoundError(f"L'image {image_path} n'a pas été trouvée")
try:
# Ouvrir l'image et la redimensionner si trop grande
with Image.open(image_path) as img:
# Redimensionner l'image si elle est trop grande (max 800x800)
max_size = 800
if img.width > max_size or img.height > max_size:
img.thumbnail((max_size, max_size), Image.Resampling.LANCZOS)
# Convertir en RGB si nécessaire (pour les formats comme PNG)
if img.mode != "RGB":
img = img.convert("RGB")
# Sauvegarder l'image en JPEG dans un buffer mémoire
buffer = io.BytesIO()
img.save(buffer, format="JPEG", quality=85)
buffer.seek(0)
# Encoder en base64
encoded_string = base64.b64encode(buffer.read()).decode("utf-8")
except Exception as e:
# Si échec avec PIL, essayer avec la méthode simple
with open(image_path, "rb") as image_file:
encoded_string = base64.b64encode(image_file.read()).decode("utf-8")
# Détecter le type de fichier
file_extension = os.path.splitext(image_path)[1].lower()
if file_extension in ['.jpg', '.jpeg']:
mime_type = 'image/jpeg'
elif file_extension == '.png':
mime_type = 'image/png'
elif file_extension == '.gif':
mime_type = 'image/gif'
elif file_extension in ['.webp']:
mime_type = 'image/webp'
else:
# Par défaut, on suppose JPEG
mime_type = 'image/jpeg'
return f"data:{mime_type};base64,{encoded_string}"
def interroger_avec_image(self, image_path: str, question: str) -> str:
"""
Analyse une image avec le modèle Pixtral
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
"""
url = self.urlBase() + self.urlFonction()
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.cleAPI()}"
}
try:
# Encoder l'image en base64
encoded_image = self._encoder_image_base64(image_path)
# 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
}
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
self.heureFin = datetime.now()
if self.heureDepart is not None and self.heureFin is not None:
self.dureeTraitement = self.heureFin - self.heureDepart
if response.status_code in [200, 201]:
self.reponseErreur = False
return self._traiter_reponse(response)
else:
self.reponseErreur = True
return f"Erreur API ({response.status_code}): {response.text}"
except Exception as e:
self.heureFin = datetime.now()
if self.heureDepart is not None and self.heureFin is not None:
self.dureeTraitement = self.heureFin - self.heureDepart
self.reponseErreur = True
return f"Erreur lors de l'analyse de l'image: {str(e)}"

View File

@ -1,31 +0,0 @@
from .base_llm import BaseLLM
import requests
class PixtralMedium(BaseLLM):
def __init__(self):
super().__init__("pixtral-medium-latest")
self.configurer(temperature=0.2, top_p=1)
def urlBase(self) -> str:
return "https://api.mistral.ai/v1/"
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
}
def _traiter_reponse(self, reponse: requests.Response) -> str:
data = reponse.json()
return data["choices"][0]["message"]["content"]

View File

@ -1,7 +1,9 @@
import os
import json
import logging
from typing import List, Dict, Any, Optional
import time
import traceback
from typing import List, Dict, Any, Optional, Union
from agents.base_agent import BaseAgent
# Configuration du logging
@ -19,85 +21,215 @@ class Orchestrator:
self.output_dir = output_dir
# Assignation directe des agents (qui peuvent être injectés lors des tests)
# Assignation directe des agents
self.json_agent = json_agent
self.image_sorter = image_sorter
self.image_analyser = image_analyser
self.report_generator = report_generator
# Collecter et enregistrer les informations détaillées sur les agents
agents_info = self._collecter_info_agents()
logger.info(f"Orchestrator initialisé avec output_dir: {output_dir}")
logger.info(f"JSON Agent: {json_agent}")
logger.info(f"Image Sorter: {image_sorter}")
logger.info(f"Image Analyser: {image_analyser}")
logger.info(f"Report Generator: {report_generator}")
logger.info(f"Agents disponibles: JSON={json_agent is not None}, ImageSorter={image_sorter is not None}, ImageAnalyser={image_analyser is not None}, ReportGenerator={report_generator is not None}")
logger.info(f"Configuration des agents: {json.dumps(agents_info, indent=2)}")
def _collecter_info_agents(self) -> Dict[str, Dict[str, Any]]:
"""
Collecte des informations détaillées sur les agents configurés
"""
agents_info = {}
# Information sur l'agent JSON
if self.json_agent:
agents_info["json_agent"] = self._get_agent_info(self.json_agent)
# Information sur l'agent Image Sorter
if self.image_sorter:
agents_info["image_sorter"] = self._get_agent_info(self.image_sorter)
# Information sur l'agent Image Analyser
if self.image_analyser:
agents_info["image_analyser"] = self._get_agent_info(self.image_analyser)
# Information sur l'agent Report Generator
if self.report_generator:
agents_info["report_generator"] = self._get_agent_info(self.report_generator)
return agents_info
def detecter_tickets(self) -> List[str]:
"""Détecte tous les tickets disponibles dans le répertoire de sortie"""
logger.info(f"Recherche de tickets dans: {self.output_dir}")
tickets = []
if not os.path.exists(self.output_dir):
logger.warning(f"Le répertoire de sortie {self.output_dir} n'existe pas")
print(f"ERREUR: Le répertoire {self.output_dir} n'existe pas")
return tickets
for ticket_dir in os.listdir(self.output_dir):
ticket_path = os.path.join(self.output_dir, ticket_dir)
if os.path.isdir(ticket_path) and ticket_dir.startswith("ticket_"):
tickets.append(ticket_path)
logger.info(f"Tickets trouvés: {tickets}")
logger.info(f"Tickets trouvés: {len(tickets)}")
print(f"Tickets détectés: {len(tickets)}")
return tickets
def traiter_ticket(self, ticket_path: str):
logger.info(f"Début du traitement du ticket: {ticket_path}")
print(f"Traitement du ticket: {ticket_path}")
def lister_tickets(self) -> Dict[int, str]:
"""Liste les tickets disponibles et retourne un dictionnaire {index: chemin}"""
tickets = self.detecter_tickets()
ticket_dict = {}
print("\nTickets disponibles:")
for i, ticket_path in enumerate(tickets, 1):
ticket_id = os.path.basename(ticket_path)
ticket_dict[i] = ticket_path
print(f"{i}. {ticket_id}")
return ticket_dict
def trouver_rapport(self, extraction_path: str, ticket_id: str) -> Dict[str, Optional[str]]:
"""
Cherche le rapport du ticket dans différents emplacements possibles (JSON ou MD)
Args:
extraction_path: Chemin de l'extraction
ticket_id: ID du ticket (ex: T0101)
Returns:
Un dictionnaire avec les chemins des fichiers JSON et MD s'ils sont trouvés
"""
result: Dict[str, Optional[str]] = {"json_path": None, "md_path": None}
# Liste des emplacements possibles pour les rapports
possible_locations = [
# 1. Dans le répertoire d'extraction directement
extraction_path,
# 2. Dans un sous-répertoire "data"
os.path.join(extraction_path, "data"),
# 3. Dans un sous-répertoire spécifique au ticket pour les rapports
os.path.join(extraction_path, f"{ticket_id}_rapports"),
# 4. Dans un sous-répertoire "rapports"
os.path.join(extraction_path, "rapports")
]
# Vérifier chaque emplacement
for base_location in possible_locations:
# Chercher le fichier JSON
json_path = os.path.join(base_location, f"{ticket_id}_rapport.json")
if os.path.exists(json_path):
logger.info(f"Rapport JSON trouvé à: {json_path}")
result["json_path"] = json_path
# Chercher le fichier Markdown
md_path = os.path.join(base_location, f"{ticket_id}_rapport.md")
if os.path.exists(md_path):
logger.info(f"Rapport Markdown trouvé à: {md_path}")
result["md_path"] = md_path
if not result["json_path"] and not result["md_path"]:
logger.warning(f"Aucun rapport trouvé pour {ticket_id} dans {extraction_path}")
return result
def traiter_ticket(self, ticket_path: str) -> bool:
"""Traite un ticket spécifique et retourne True si le traitement a réussi"""
logger.info(f"Début du traitement du ticket: {ticket_path}")
print(f"\nTraitement du ticket: {os.path.basename(ticket_path)}")
success = False
extractions_trouvees = False
if not os.path.exists(ticket_path):
logger.error(f"Le chemin du ticket n'existe pas: {ticket_path}")
print(f"ERREUR: Le chemin du ticket n'existe pas: {ticket_path}")
return False
ticket_id = os.path.basename(ticket_path).replace("ticket_", "")
for extraction in os.listdir(ticket_path):
extraction_path = os.path.join(ticket_path, extraction)
if os.path.isdir(extraction_path):
extractions_trouvees = True
logger.info(f"Traitement de l'extraction: {extraction_path}")
logger.info(f"Traitement de l'extraction: {extraction}")
print(f" Traitement de l'extraction: {extraction}")
# Recherche des rapports (JSON et MD) dans différents emplacements
rapports = self.trouver_rapport(extraction_path, ticket_id)
# Dossier des pièces jointes
attachments_dir = os.path.join(extraction_path, "attachments")
rapport_json_path = os.path.join(extraction_path, f"{extraction.split('_')[0]}_rapport.json")
rapports_dir = os.path.join(extraction_path, f"{extraction.split('_')[0]}_rapports")
logger.info(f"Vérification des chemins: rapport_json_path={rapport_json_path}, existe={os.path.exists(rapport_json_path)}")
print(f" Vérification du rapport JSON: {os.path.exists(rapport_json_path)}")
# Dossier pour les rapports générés
rapports_dir = os.path.join(extraction_path, f"{ticket_id}_rapports")
os.makedirs(rapports_dir, exist_ok=True)
if os.path.exists(rapport_json_path):
with open(rapport_json_path, 'r', encoding='utf-8') as file:
ticket_json = json.load(file)
logger.info(f"Rapport JSON chargé: {rapport_json_path}")
# Préparer les données du ticket à partir des rapports trouvés
ticket_data = self._preparer_donnees_ticket(rapports, ticket_id)
if ticket_data:
success = True
logger.info(f"Données du ticket chargées avec succès")
print(f" Données du ticket chargées")
# Traitement JSON avec l'agent JSON
if self.json_agent:
logger.info("Exécution de l'agent JSON")
print(" Analyse du JSON en cours...")
json_analysis = self.json_agent.executer(ticket_json)
logger.info(f"Résultat de l'analyse JSON: {json_analysis[:100]}...")
print(" Analyse du ticket en cours...")
# Log détaillé sur l'agent JSON
agent_info = self._get_agent_info(self.json_agent)
logger.info(f"Agent JSON: {json.dumps(agent_info, indent=2)}")
json_analysis = self.json_agent.executer(ticket_data)
logger.info("Analyse JSON terminée")
else:
logger.warning("Agent JSON non disponible")
json_analysis = None
print(" Agent JSON non disponible, analyse ignorée")
# Traitement des images
relevant_images = []
images_analyses = {}
if os.path.exists(attachments_dir):
logger.info(f"Vérification des pièces jointes dans: {attachments_dir}")
print(f" Vérification des pièces jointes: {attachments_dir}")
print(f" Vérification des pièces jointes...")
# Log détaillé sur l'agent Image Sorter
if self.image_sorter:
agent_info = self._get_agent_info(self.image_sorter)
logger.info(f"Agent Image Sorter: {json.dumps(agent_info, indent=2)}")
images_count = 0
for attachment in os.listdir(attachments_dir):
attachment_path = os.path.join(attachments_dir, attachment)
if attachment.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif')):
images_count += 1
logger.info(f"Image trouvée: {attachment_path}")
if self.image_sorter:
logger.info(f"Analyse de la pertinence de l'image: {attachment_path}")
print(f" Analyse de la pertinence de l'image: {attachment}")
is_relevant = self.image_sorter.executer(attachment_path)
logger.info(f"Image pertinente: {is_relevant}")
print(f" Analyse de l'image: {attachment}")
sorting_result = self.image_sorter.executer(attachment_path)
is_relevant = sorting_result.get("is_relevant", False)
reason = sorting_result.get("reason", "")
# Ajouter les métadonnées de tri à la liste des analyses
images_analyses[attachment_path] = {
"sorting": sorting_result,
"analysis": None # Sera rempli plus tard si pertinent
}
if is_relevant:
logger.info(f"Image pertinente identifiée: {attachment} ({reason})")
print(f" => Pertinente: {reason}")
relevant_images.append(attachment_path)
else:
logger.info(f"Image non pertinente: {attachment} ({reason})")
print(f" => Non pertinente: {reason}")
else:
logger.warning("Image Sorter non disponible")
@ -105,55 +237,327 @@ class Orchestrator:
print(f" Images analysées: {images_count}, Images pertinentes: {len(relevant_images)}")
else:
logger.warning(f"Répertoire des pièces jointes non trouvé: {attachments_dir}")
print(f" Répertoire des pièces jointes non trouvé: {attachments_dir}")
print(f" Répertoire des pièces jointes non trouvé")
image_analysis_results = {}
# Analyse approfondie des images pertinentes
# Log détaillé sur l'agent Image Analyser
if self.image_analyser:
agent_info = self._get_agent_info(self.image_analyser)
logger.info(f"Agent Image Analyser: {json.dumps(agent_info, indent=2)}")
for image_path in relevant_images:
if self.image_analyser and json_analysis:
logger.info(f"Analyse de l'image: {image_path}")
print(f" Analyse de l'image: {os.path.basename(image_path)}")
image_analysis_results[image_path] = self.image_analyser.executer(
image_path,
contexte=json_analysis
)
logger.info(f"Résultat de l'analyse d'image: {image_analysis_results[image_path][:100]}...")
image_name = os.path.basename(image_path)
logger.info(f"Analyse approfondie de l'image: {image_name}")
print(f" Analyse approfondie de l'image: {image_name}")
analysis_result = self.image_analyser.executer(image_path, contexte=json_analysis)
# Ajouter l'analyse au dictionnaire des analyses d'images
if image_path in images_analyses:
images_analyses[image_path]["analysis"] = analysis_result
else:
images_analyses[image_path] = {
"sorting": {"is_relevant": True, "reason": "Auto-sélectionné"},
"analysis": analysis_result
}
logger.info(f"Analyse complétée pour {image_name}")
# Génération du rapport final
rapport_data = {
"ticket_data": ticket_data,
"ticket_id": ticket_id,
"analyse_json": json_analysis,
"analyse_images": image_analysis_results
"analyse_images": images_analyses,
"metadata": {
"timestamp_debut": self._get_timestamp(),
"ticket_id": ticket_id,
"images_analysees": images_count if 'images_count' in locals() else 0,
"images_pertinentes": len(relevant_images)
}
}
if self.report_generator:
logger.info("Génération du rapport final")
print(" Génération du rapport final")
rapport_path = os.path.join(rapports_dir, extraction.split('_')[0])
# Log détaillé sur l'agent Report Generator
agent_info = self._get_agent_info(self.report_generator)
logger.info(f"Agent Report Generator: {json.dumps(agent_info, indent=2)}")
rapport_path = os.path.join(rapports_dir, ticket_id)
os.makedirs(rapport_path, exist_ok=True)
self.report_generator.executer(rapport_data, rapport_path)
logger.info(f"Rapport généré à: {rapport_path}")
print(f" Rapport généré à: {rapport_path}")
else:
logger.warning("Report Generator non disponible")
print(" Report Generator non disponible, génération de rapport ignorée")
print(f"Traitement du ticket {ticket_path} terminé.\n")
logger.info(f"Traitement du ticket {ticket_path} terminé.")
print(f"Traitement du ticket {os.path.basename(ticket_path)} terminé avec succès.\n")
logger.info(f"Traitement du ticket {ticket_path} terminé avec succès.")
else:
logger.warning(f"Fichier rapport JSON non trouvé: {rapport_json_path}")
print(f" ERREUR: Fichier rapport JSON non trouvé: {rapport_json_path}")
logger.warning(f"Aucune donnée de ticket trouvée pour: {ticket_id}")
print(f" ERREUR: Aucune donnée de ticket trouvée pour {ticket_id}")
if not extractions_trouvees:
logger.warning(f"Aucune extraction trouvée dans le ticket: {ticket_path}")
print(f" ERREUR: Aucune extraction trouvée dans le ticket: {ticket_path}")
print(f" ERREUR: Aucune extraction trouvée dans le ticket")
return success
def _preparer_donnees_ticket(self, rapports: Dict[str, Optional[str]], ticket_id: str) -> Optional[Dict]:
"""
Prépare les données du ticket à partir des rapports trouvés (JSON et/ou MD)
Args:
rapports: Dictionnaire avec les chemins des rapports JSON et MD
ticket_id: ID du ticket
Returns:
Dictionnaire avec les données du ticket, ou None si aucun rapport n'est trouvé
"""
ticket_data = None
# Essayer d'abord le fichier JSON
if rapports["json_path"]:
try:
with open(rapports["json_path"], 'r', encoding='utf-8') as file:
ticket_data = json.load(file)
logger.info(f"Données JSON chargées depuis: {rapports['json_path']}")
print(f" Rapport JSON chargé: {os.path.basename(rapports['json_path'])}")
except Exception as e:
logger.error(f"Erreur lors du chargement du JSON: {e}")
print(f" ERREUR: Impossible de charger le fichier JSON: {e}")
# Si pas de JSON ou erreur, essayer le Markdown
if not ticket_data and rapports["md_path"]:
try:
# Créer une structure de données à partir du contenu Markdown
ticket_data = self._extraire_donnees_de_markdown(rapports["md_path"])
logger.info(f"Données Markdown chargées depuis: {rapports['md_path']}")
print(f" Rapport Markdown chargé: {os.path.basename(rapports['md_path'])}")
except Exception as e:
logger.error(f"Erreur lors du chargement du Markdown: {e}")
print(f" ERREUR: Impossible de charger le fichier Markdown: {e}")
# Assurer que l'ID du ticket est correct
if ticket_data:
ticket_data["code"] = ticket_id
return ticket_data
def _extraire_donnees_de_markdown(self, md_path: str) -> Dict:
"""
Extrait les données d'un fichier Markdown et les structure
Args:
md_path: Chemin vers le fichier Markdown
Returns:
Dictionnaire structuré avec les données du ticket
"""
with open(md_path, 'r', encoding='utf-8') as file:
content = file.read()
# Initialiser la structure de données
ticket_data = {
"id": "",
"code": "",
"name": "",
"description": "",
"messages": [],
"metadata": {
"source_file": md_path,
"format": "markdown"
}
}
# Extraire le titre (première ligne)
lines = content.split('\n')
if lines and lines[0].startswith('# '):
title = lines[0].replace('# ', '')
ticket_parts = title.split(':')
if len(ticket_parts) >= 1:
ticket_data["code"] = ticket_parts[0].strip()
if len(ticket_parts) >= 2:
ticket_data["name"] = ticket_parts[1].strip()
# Extraire la description
description_section = self._extraire_section(content, "description")
if description_section:
ticket_data["description"] = description_section.strip()
# Extraire les informations du ticket
info_section = self._extraire_section(content, "Informations du ticket")
if info_section:
for line in info_section.split('\n'):
if ':' in line and line.startswith('- **'):
key = line.split('**')[1].strip()
value = line.split(':')[1].strip()
if key == "id":
ticket_data["id"] = value
elif key == "code":
ticket_data["code"] = value
# Ajouter d'autres champs au besoin
# Extraire les messages
messages_section = self._extraire_section(content, "Messages")
if messages_section:
message_blocks = messages_section.split("### Message ")
for block in message_blocks[1:]: # Ignorer le premier élément (vide)
message = {}
# Extraire les en-têtes du message
lines = block.split('\n')
for i, line in enumerate(lines):
if line.startswith('**') and ':' in line:
key = line.split('**')[1].lower()
value = line.split(':')[1].strip()
message[key] = value
# Extraire le contenu du message (tout ce qui n'est pas un en-tête)
content_start = 0
for i, line in enumerate(lines):
if i > 0 and not line.startswith('**') and line and content_start == 0:
content_start = i
break
if content_start > 0:
content_end = -1
for i in range(content_start, len(lines)):
if lines[i].startswith('**attachment_ids**') or lines[i].startswith('---'):
content_end = i
break
if content_end == -1:
message["content"] = "\n".join(lines[content_start:])
else:
message["content"] = "\n".join(lines[content_start:content_end])
# Extraire les pièces jointes
attachments = []
for line in lines:
if line.startswith('- ') and '[ID:' in line:
attachments.append(line.strip('- ').strip())
if attachments:
message["attachments"] = attachments
ticket_data["messages"].append(message)
return ticket_data
def _extraire_section(self, content: str, section_title: str) -> Optional[str]:
"""
Extrait une section du contenu Markdown
Args:
content: Contenu Markdown complet
section_title: Titre de la section à extraire
Returns:
Contenu de la section ou None si non trouvée
"""
import re
# Chercher les sections de niveau 2 (##)
pattern = r'## ' + re.escape(section_title) + r'\s*\n(.*?)(?=\n## |$)'
match = re.search(pattern, content, re.DOTALL)
if match:
return match.group(1).strip()
# Si pas trouvé, chercher les sections de niveau 3 (###)
pattern = r'### ' + re.escape(section_title) + r'\s*\n(.*?)(?=\n### |$)'
match = re.search(pattern, content, re.DOTALL)
if match:
return match.group(1).strip()
return None
def executer(self):
def executer(self, ticket_specifique: Optional[str] = None):
"""
Exécute l'orchestrateur soit sur un ticket spécifique, soit permet de choisir
Args:
ticket_specifique: Chemin du ticket spécifique à traiter (optionnel)
"""
start_time = time.time()
# Stocker le ticket spécifique
self.ticket_specifique = ticket_specifique
# Obtenir la liste des tickets
if ticket_specifique:
# Utiliser juste le ticket spécifique
ticket_dirs = self.detecter_tickets()
ticket_dirs = [t for t in ticket_dirs if t.endswith(ticket_specifique)]
logger.info(f"Ticket spécifique à traiter: {ticket_specifique}")
else:
# Lister tous les tickets
ticket_dirs = self.detecter_tickets()
logger.info(f"Tickets à traiter: {len(ticket_dirs)}")
if not ticket_dirs:
logger.warning("Aucun ticket trouvé dans le répertoire de sortie")
return
# Un seul log de début d'exécution
logger.info("Début de l'exécution de l'orchestrateur")
print("Début de l'exécution de l'orchestrateur")
tickets = self.detecter_tickets()
if not tickets:
logger.warning("Aucun ticket détecté dans le répertoire de sortie")
print("ATTENTION: Aucun ticket détecté dans le répertoire de sortie")
# Traitement des tickets
for ticket_dir in ticket_dirs:
if ticket_specifique and not ticket_dir.endswith(ticket_specifique):
continue
try:
self.traiter_ticket(ticket_dir)
except Exception as e:
logger.error(f"Erreur lors du traitement du ticket {ticket_dir}: {str(e)}")
print(f"Erreur lors du traitement du ticket {ticket_dir}: {str(e)}")
traceback.print_exc()
for ticket in tickets:
self.traiter_ticket(ticket)
# Calcul de la durée d'exécution
duration = time.time() - start_time
logger.info(f"Fin de l'exécution de l'orchestrateur (durée: {duration:.2f} secondes)")
print(f"Fin de l'exécution de l'orchestrateur (durée: {duration:.2f} secondes)")
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")
def _get_agent_info(self, agent: Optional[BaseAgent]) -> Dict:
"""
Récupère les informations détaillées sur un agent.
"""
if not agent:
return {"status": "non configuré"}
logger.info("Fin de l'exécution de l'orchestrateur")
print("Fin de l'exécution de l'orchestrateur")
# Récupérer les informations du modèle
model_info = {
"nom": agent.nom,
"model": getattr(agent.llm, "modele", str(type(agent.llm))),
}
# Ajouter les paramètres de configuration s'ils sont disponibles directement dans l'agent
# Utiliser getattr avec une valeur par défaut pour éviter les erreurs
model_info["temperature"] = getattr(agent, "temperature", None)
model_info["top_p"] = getattr(agent, "top_p", None)
model_info["max_tokens"] = getattr(agent, "max_tokens", None)
# Ajouter le prompt système s'il est disponible
if hasattr(agent, "system_prompt"):
prompt_preview = getattr(agent, "system_prompt", "")
# Tronquer le prompt s'il est trop long
if prompt_preview and len(prompt_preview) > 200:
prompt_preview = prompt_preview[:200] + "..."
model_info["system_prompt_preview"] = prompt_preview
# Supprimer les valeurs None
model_info = {k: v for k, v in model_info.items() if v is not None}
return model_info

View File

@ -0,0 +1,111 @@
{
"ticket_data": {
"id": "113",
"code": "T0101",
"name": "ACTIVATION LOGICIEL",
"description": "Problème de licence.",
"project_name": "Demandes",
"stage_name": "Clôturé",
"user_id": "",
"partner_id_email_from": "PROVENCALE S.A, Bruno Vernet <bruno.vernet@provencale.com>",
"create_date": "26/03/2020 14:46:36",
"write_date_last_modification": "03/10/2024 13:10:50",
"date_deadline": "25/05/2020 00:00:00",
"messages": [
{
"author_id": "PROVENCALE S.A",
"date": "26/03/2020 14:43:45",
"message_type": "E-mail",
"subject": "ACTIVATION LOGICIEL",
"id": "10758",
"content": "Bonjour,\n\nAu vu de la situation liée au Coronavirus, nous avons dû passer en télétravail.\n\nPour ce faire et avoir accès aux différents logiciels nécessaires, ESQ a été réinstallé sur un autre serveur afin de pouvoir travailler en bureau à distance.\n\nDu coup le logiciel nous demande une activation mais je ne sais pas si le N° de licence a été modifié suite à un achat version réseau faite par JB Lafitte en 2019 ou si le problème est autre.\n\nCi-dessous la fenêtre au lancement du logiciel.\n\nMerci davance pour votre aide.\n\nCordialement\n\n\n- image006.jpg (image/jpeg) [ID: 31760]\n- image005.jpg (image/jpeg) [ID: 31758]\n\n---\n"
}
],
"date_d'extraction": "04/04/2025 17:02:42",
"répertoire": "output/ticket_T0101/T0101_20250404_170239"
},
"ticket_id": "T0101",
"analyse_json": "1. Résumé du problème\nLe client PROVENCALE S.A a réinstallé le logiciel ESQ sur un nouveau serveur pour permettre le télétravail de ses employés. Cependant, lors du lancement du logiciel, une activation est demandée et le client n'est pas certain si le numéro de licence a été modifié suite à un achat de version réseau en 2019.\n2. Informations techniques essentielles\nLogiciel : ESQ\nVersion : non spécifiée\nNouveau serveur installé pour le télétravail\nAchat d'une version réseau en 2019 : possible modification du numéro de licence\n3. Contexte client (urgence, impact)\nLe client a dû passer en télétravail en raison de la situation liée au Coronavirus. Le problème d'activation du logiciel ESQ sur le nouveau serveur peut avoir un impact sur la productivité des employés de PROVENCALE S.A. La date limite pour résoudre ce problème est le 25/05/2020.\n4. Pistes d'analyse suggérées\nVérifier les informations de licence pour la version réseau achetée en 2019 et comparer avec le numéro de licence actuellement utilisé. Si nécessaire, contacter le fournisseur du logiciel ESQ pour obtenir de l'aide pour l'activation ou pour obtenir un nouveau numéro de licence. Vérifier également la configuration du nouveau serveur pour s'assurer qu'il répond aux exigences système du logiciel ESQ.",
"analyse_images": {
"output/ticket_T0101/T0101_20250404_170239/attachments/image006.jpg": {
"sorting": {
"is_relevant": false,
"reason": "Non. Cette image montre le logo d'une entreprise nommée \"Provençale Carbone de Calcium\". Elle n'est pas pertinente pour le support technique de logiciels, car elle ne contient pas de captures d'écran de logiciels, de messages d'erreur, de configurations système, ou d'autres éléments techniques liés à l'informatique.",
"raw_response": "Non. Cette image montre le logo d'une entreprise nommée \"Provençale Carbone de Calcium\". Elle n'est pas pertinente pour le support technique de logiciels, car elle ne contient pas de captures d'écran de logiciels, de messages d'erreur, de configurations système, ou d'autres éléments techniques liés à l'informatique.",
"metadata": {
"image_path": "output/ticket_T0101/T0101_20250404_170239/attachments/image006.jpg",
"image_name": "image006.jpg",
"timestamp": "20250407_142051",
"model_info": {
"model": "pixtral-12b-latest",
"temperature": 0.2,
"top_p": 0.8,
"max_tokens": 300
}
}
},
"analysis": null
},
"output/ticket_T0101/T0101_20250404_170239/attachments/image005.jpg": {
"sorting": {
"is_relevant": true,
"reason": "oui. L'image montre une fenêtre d'activation de logiciel, ce qui est pertinent pour le support technique de logiciels.",
"raw_response": "oui. L'image montre une fenêtre d'activation de logiciel, ce qui est pertinent pour le support technique de logiciels.",
"metadata": {
"image_path": "output/ticket_T0101/T0101_20250404_170239/attachments/image005.jpg",
"image_name": "image005.jpg",
"timestamp": "20250407_142051",
"model_info": {
"model": "pixtral-12b-latest",
"temperature": 0.2,
"top_p": 0.8,
"max_tokens": 300
}
}
},
"analysis": {
"analyse": "### Analyse de l'image\n\n#### 1. Description factuelle de ce que montre l'image\nL'image montre une interface de fenêtre de logiciel intitulée \"Activation du logiciel\". La fenêtre contient un champ pour entrer l'ID du logiciel, un message d'instructions, et trois options pour l'activation du logiciel : par internet, par carte (4 ans restants), et par téléphone.\n\n#### 2. Éléments techniques identifiables\n- **Titre de la fenêtre** : \"Activation du logiciel\"\n- **Champ ID du logiciel** : Pré-rempli avec \"ID00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"metadata": {
"image_path": "output/ticket_T0101/T0101_20250404_170239/attachments/image005.jpg",
"image_name": "image005.jpg",
"timestamp": "20250407_142056",
"model_info": {
"model": "pixtral-12b-latest",
"temperature": 0.3,
"top_p": 0.9,
"max_tokens": 1200
}
}
}
}
},
"metadata": {
"timestamp": "20250407_142114",
"model": "mistral-medium",
"temperature": 0.4,
"top_p": 0.9,
"max_tokens": 2500,
"system_prompt": "Tu es un expert en génération de rapports techniques pour BRG_Lab.\nTa mission est de synthétiser toutes les analyses (JSON et images) en un rapport structuré et exploitable.\n\nStructure ton rapport ainsi:\n1. Résumé exécutif: Synthèse du problème et des conclusions principales\n2. Analyse du ticket: Détails extraits du ticket client\n3. Analyse des images: Résumé des images pertinentes et leur contribution\n4. Diagnostic technique: Interprétation consolidée des informations\n5. Recommandations: Actions suggérées pour résoudre le problème\n\nChaque section doit être factuelle, précise et orientée solution.\nInclus tous les détails techniques pertinents (versions, configurations, messages d'erreur).\nAssure une traçabilité complète entre les données sources et tes conclusions.",
"agents_info": {
"image_sorter": {
"model": "pixtral-12b-latest",
"temperature": 0.2,
"top_p": 0.8,
"max_tokens": 300
},
"image_analyser": {
"model": "pixtral-12b-latest",
"temperature": 0.3,
"top_p": 0.9,
"max_tokens": 1200
},
"report_generator": {
"model": "mistral-medium",
"temperature": 0.4,
"top_p": 0.9,
"max_tokens": 2500
}
}
},
"rapport_genere": "# Rapport technique pour le ticket #T0101\n\n## 1. Résumé exécutif\n\nLe client PROVENCALE S.A a récemment installé le logiciel ESQ sur un nouveau serveur pour permettre le télétravail de ses employés. Cependant, lors de l'activation du logiciel, le client n'est pas certain si le numéro de licence a été modifié suite à un achat de version réseau en 2019.\n\nLes analyses effectuées indiquent que le numéro de licence actuellement utilisé ne correspond pas à celui d'une version réseau. Il est recommandé de contacter le fournisseur du logiciel ESQ pour obtenir de l'aide pour l'activation ou pour obtenir un nouveau numéro de licence correspondant à la version réseau achetée en 2019.\n\n## 2. Analyse détaillée du ticket\n\nLe client PROVENCALE S.A a réinstallé le logiciel ESQ sur un nouveau serveur pour permettre le télétravail de ses employés. Cependant, lors du lancement du logiciel, une activation est demandée et le client n'est pas certain si le numéro de licence a été modifié suite à un achat de version réseau en 2019.\n\nLes informations techniques essentielles fournies par le client sont les suivantes :\n\n- Logiciel : ESQ\n- Version : non spécifiée\n- Nouveau serveur installé pour le télétravail\n- Achat d'une version réseau en 2019 : possible modification du numéro de licence\n\nLe client a dû passer en télétravail en raison de la situation liée au Coronavirus. Le problème d'activation du logiciel ESQ sur le nouveau serveur peut avoir un impact sur la productivité des employés de PROVENCALE S.A. La date limite pour résoudre ce problème est le 25/05/2020.\n\n## 3. Analyse des images pertinentes\n\n### 3.1 Image 1 : image005.jpg\n\n#### 3.1.1 Description factuelle\n\nL'image montre une interface de fenêtre de logiciel intitulée \"Activation du logiciel\". La fenêtre contient un champ pour entrer l'ID du logiciel, un message d'instructions, et trois options pour l'activation du logiciel : par internet, par carte (4 ans restants), et par téléphone.\n\n#### 3.1.2 Éléments techniques identifiables\n\n- **Titre de la fenêtre** : \"Activation du logiciel\"\n- **Champ ID du logiciel** : Pré-rempli avec \"ID00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
}

View File

@ -0,0 +1,62 @@
# Rapport d'analyse du ticket #T0101
*Généré le: 20250407_142114*
# Rapport technique pour le ticket #T0101
## 1. Résumé exécutif
Le client PROVENCALE S.A a récemment installé le logiciel ESQ sur un nouveau serveur pour permettre le télétravail de ses employés. Cependant, lors de l'activation du logiciel, le client n'est pas certain si le numéro de licence a été modifié suite à un achat de version réseau en 2019.
Les analyses effectuées indiquent que le numéro de licence actuellement utilisé ne correspond pas à celui d'une version réseau. Il est recommandé de contacter le fournisseur du logiciel ESQ pour obtenir de l'aide pour l'activation ou pour obtenir un nouveau numéro de licence correspondant à la version réseau achetée en 2019.
## 2. Analyse détaillée du ticket
Le client PROVENCALE S.A a réinstallé le logiciel ESQ sur un nouveau serveur pour permettre le télétravail de ses employés. Cependant, lors du lancement du logiciel, une activation est demandée et le client n'est pas certain si le numéro de licence a été modifié suite à un achat de version réseau en 2019.
Les informations techniques essentielles fournies par le client sont les suivantes :
- Logiciel : ESQ
- Version : non spécifiée
- Nouveau serveur installé pour le télétravail
- Achat d'une version réseau en 2019 : possible modification du numéro de licence
Le client a dû passer en télétravail en raison de la situation liée au Coronavirus. Le problème d'activation du logiciel ESQ sur le nouveau serveur peut avoir un impact sur la productivité des employés de PROVENCALE S.A. La date limite pour résoudre ce problème est le 25/05/2020.
## 3. Analyse des images pertinentes
### 3.1 Image 1 : image005.jpg
#### 3.1.1 Description factuelle
L'image montre une interface de fenêtre de logiciel intitulée "Activation du logiciel". La fenêtre contient un champ pour entrer l'ID du logiciel, un message d'instructions, et trois options pour l'activation du logiciel : par internet, par carte (4 ans restants), et par téléphone.
#### 3.1.2 Éléments techniques identifiables
- **Titre de la fenêtre** : "Activation du logiciel"
- **Champ ID du logiciel** : Pré-rempli avec "ID00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
## Informations techniques
### Agents et modèles utilisés
#### Agent de tri d'images
- **Modèle**: pixtral-12b-latest
- **Température**: 0.2
- **Top-p**: 0.8
- **Max tokens**: 300
#### Agent d'analyse d'images
- **Modèle**: pixtral-12b-latest
- **Température**: 0.3
- **Top-p**: 0.9
- **Max tokens**: 1200
#### Agent de génération de rapport
- **Modèle**: mistral-medium
- **Température**: 0.4
- **Top-p**: 0.9
- **Max tokens**: 2500
### Statistiques

View File

@ -1,3 +1,4 @@
requests>=2.25.0
beautifulsoup4>=4.9.0
html2text>=2020.0.0
html2text>=2020.0.0
Pillow>=9.0.0

25
test_models.py Normal file
View File

@ -0,0 +1,25 @@
from llm_classes.mistral_medium import MistralMedium
from llm_classes.pixtral_12b import Pixtral12b
print("Initialisation des modèles LLM...")
# Initialisation des modèles
try:
text_model = MistralMedium()
image_model = Pixtral12b()
print("Modèles initialisés avec succès!")
except Exception as e:
print(f"Erreur lors de l'initialisation des modèles: {str(e)}")
exit(1)
# Test d'interrogation simple
try:
question = "Quelle est la capitale de la France?"
print(f"\nTest d'interrogation simple sur MistralMedium:")
print(f"Question: {question}")
response = text_model.interroger(question)
print(f"Réponse: {response[:100]}...")
except Exception as e:
print(f"Erreur lors de l'interrogation simple: {str(e)}")
print("\nTests terminés!")

View File

@ -4,19 +4,27 @@ from agents.agent_image_sorter import AgentImageSorter
from agents.agent_image_analyser import AgentImageAnalyser
from agents.agent_report_generator import AgentReportGenerator
from llm_classes.mistral_large import MistralLarge
# Utilisation des modèles Medium pour les tests
from llm_classes.mistral_medium import MistralMedium
from llm_classes.pixtral_12b import Pixtral12b
from llm_classes.ollama import Ollama
import os
import logging
import sys
import time
# Configuration du logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s',
filename='test_orchestrator.log', filemode='w')
logger = logging.getLogger("TestOrchestrator")
def test_orchestrator():
def test_orchestrator(ticket_id=None):
"""
Exécute l'orchestrateur avec les agents définis
Args:
ticket_id: Identifiant du ticket à traiter (optionnel)
"""
# Vérifier que le dossier output existe
if not os.path.exists("output/"):
os.makedirs("output/")
@ -25,42 +33,44 @@ def test_orchestrator():
# 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/: {tickets}")
print(f"Tickets existants dans output/: {tickets}")
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 LLM
json_llm = MistralLarge()
logger.info("LLM MistralLarge initialisé")
print("LLM MistralLarge initialisé")
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
json_llm = MistralMedium()
logger.info("LLM MistralMedium initialisé pour l'analyse JSON")
# Utilisation de Pixtral12b pour le tri et l'analyse d'images
image_sorter_llm = Pixtral12b()
logger.info("LLM Pixtral12b initialisé")
print("LLM Pixtral12b initialisé")
logger.info("LLM Pixtral12b initialisé pour le tri d'images")
image_analyser_llm = Ollama()
logger.info("LLM Ollama initialisé")
print("LLM Ollama initialisé")
image_analyser_llm = Pixtral12b()
logger.info("LLM Pixtral12b initialisé pour l'analyse d'images")
report_generator_llm = MistralLarge()
logger.info("Deuxième LLM MistralLarge initialisé")
print("Deuxième LLM MistralLarge initialisé")
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
print("Création des agents...")
json_agent = AgentJsonAnalyser(json_llm)
logger.info("Agent JSON analyser initialisé")
print("Agent JSON analyser initialisé")
image_sorter = AgentImageSorter(image_sorter_llm)
logger.info("Agent Image sorter initialisé")
print("Agent Image sorter initialisé")
image_analyser = AgentImageAnalyser(image_analyser_llm)
logger.info("Agent Image analyser initialisé")
print("Agent Image analyser initialisé")
report_generator = AgentReportGenerator(report_generator_llm)
logger.info("Agent Rapport générateur initialisé")
print("Agent Rapport générateur initialisé")
print("Tous les agents ont été créés")
# Initialisation de l'orchestrateur avec les agents
logger.info("Initialisation de l'orchestrateur")
@ -74,16 +84,39 @@ def test_orchestrator():
report_generator=report_generator
)
# Exécution complète de l'orchestrateur
# Vérification du ticket spécifique si fourni
specific_ticket_path = None
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")
orchestrator.executer()
orchestrator.executer(specific_ticket_path)
logger.info("Fin de l'exécution de l'orchestrateur")
print("Fin de l'exécution de l'orchestrateur")
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")
test_orchestrator()
# 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é")

View File

@ -2,4 +2,4 @@ home = /usr/bin
include-system-site-packages = false
version = 3.12.3
executable = /usr/bin/python3.12
command = /home/fgras-ca/llm-ticket3/venv/bin/python3 -m venv /home/fgras-ca/llm-ticket3/venv
command = /usr/bin/python3 -m venv /home/fgras-ca/llm-ticket3/venv