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")