llm_ticket3/agents/agent_image_analyser.py
2025-04-07 14:30:24 +02:00

320 lines
14 KiB
Python

from .base_agent import BaseAgent
from typing import Any, Dict
import logging
import os
from PIL import Image
import base64
import io
logger = logging.getLogger("AgentImageAnalyser")
class AgentImageAnalyser(BaseAgent):
"""
Agent pour analyser les images et extraire les informations pertinentes.
"""
def __init__(self, llm):
super().__init__("AgentImageAnalyser", llm)
# Configuration locale de l'agent (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
# 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
Args:
image_path: Chemin vers l'image
Returns:
True si l'image existe et est accessible, False sinon
"""
try:
# Vérifier que le fichier existe
if not os.path.exists(image_path):
logger.error(f"L'image n'existe pas: {image_path}")
return False
# Vérifier que le fichier est accessible en lecture
if not os.access(image_path, os.R_OK):
logger.error(f"L'image n'est pas accessible en lecture: {image_path}")
return False
# Vérifier que le fichier peut être ouvert comme une image
with Image.open(image_path) as img:
# Vérifier les dimensions de l'image
width, height = img.size
if width <= 0 or height <= 0:
logger.error(f"Dimensions d'image invalides: {width}x{height}")
return False
logger.info(f"Image vérifiée avec succès: {image_path} ({width}x{height})")
return True
except Exception as e:
logger.error(f"Erreur lors de la vérification de l'image {image_path}: {str(e)}")
return False
def _encoder_image_base64(self, image_path: str) -> str:
"""
Encode l'image en base64 pour l'inclure directement dans le prompt
Args:
image_path: Chemin vers l'image
Returns:
Chaîne de caractères au format data URI avec l'image encodée en base64
"""
try:
# Ouvrir l'image et la redimensionner si trop grande
with Image.open(image_path) as img:
# Redimensionner l'image si elle est trop grande (max 800x800)
max_size = 800
if img.width > max_size or img.height > max_size:
img.thumbnail((max_size, max_size), Image.Resampling.LANCZOS)
# Convertir en RGB si nécessaire (pour les formats comme PNG)
if img.mode != "RGB":
img = img.convert("RGB")
# Sauvegarder l'image en JPEG dans un buffer mémoire
buffer = io.BytesIO()
img.save(buffer, format="JPEG", quality=85)
buffer.seek(0)
# Encoder en base64
img_base64 = base64.b64encode(buffer.read()).decode("utf-8")
# Construire le data URI
data_uri = f"data:image/jpeg;base64,{img_base64}"
return data_uri
except Exception as e:
logger.error(f"Erreur lors de l'encodage de l'image {image_path}: {str(e)}")
return ""
def 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")