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