This commit is contained in:
Ladebeze66 2025-04-23 16:45:15 +02:00
parent 3190c5dd02
commit 9571d96157
49 changed files with 2153 additions and 3004 deletions

View File

@ -0,0 +1,4 @@
Émetteur,Type,Date,Contenu,Éléments visuels
Client,Question,Inconnu,Contenu insuffisant pour l'analyse,-
Support,Demande de plus d'informations,Inconnu,Demande de détails supplémentaires pour aider le problème,-
Client,Réponse,2025-04-23 16:36:57,Image fournie (image.png) de la page d'accueil par défaut de Tomcat,"Capture d'écran de la page d'accueil par défaut de Tomcat, indiquant une configuration réussie"
1 Émetteur Type Date Contenu Éléments visuels
2 Client Question Inconnu Contenu insuffisant pour l'analyse -
3 Support Demande de plus d'informations Inconnu Demande de détails supplémentaires pour aider le problème -
4 Client Réponse 2025-04-23 16:36:57 Image fournie (image.png) de la page d'accueil par défaut de Tomcat Capture d'écran de la page d'accueil par défaut de Tomcat, indiquant une configuration réussie

View File

@ -1,20 +1,22 @@
from ..base_agent import BaseAgent
import logging
import os
from typing import Dict, Any
from typing import Dict, Any, List, Optional
from PIL import Image
from ..utils.pipeline_logger import sauvegarder_donnees
from utils.translate_utils import fr_to_en, en_to_fr
logger = logging.getLogger("AgentImageAnalyser")
class AgentImageAnalyser(BaseAgent):
"""
Agent pour analyser les images et extraire les informations pertinentes.
Agent for analyzing images and extracting relevant information.
Works in English and translates to French for compatibility.
"""
def __init__(self, llm):
super().__init__("AgentImageAnalyser", llm)
# Configuration personnalisable
# Configurable parameters
self.params = {
"temperature": 0.2,
"top_p": 0.8,
@ -23,92 +25,103 @@ class AgentImageAnalyser(BaseAgent):
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
- Distingue clairement le nom complet des essais/tests/modules (par exemple, "Essai au bleu de méthylène" au lieu de simplement "essai bleu")
1. Objective Description
Describe precisely what the image shows:
- Software interface, menus, windows, tabs
- Error messages, system messages, code or script
- Software or module name/title if visible
- Clearly distinguish the complete name of tests/modules (for example, "Methylene blue test" instead of simply "blue test")
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
- Boutons RAZ ou réinitialisation (souvent marqués "RAZ" et non "PAZ")
- Précise si des éléments colorés font partie de l'interface standard (ex: bouton toujours rouge) ou s'ils semblent être liés au problème
2. Key Technical Elements
Identify:
- Software versions or displayed modules
- Visible error codes
- Configurable parameters (text fields, sliders, dropdowns, checkboxes)
- Values displayed or pre-filled in fields
- Disabled, grayed out or hidden elements (often non-modifiable)
- Active/inactive buttons
- Reset or initialization buttons (often marked "RAZ" and not "PAZ")
- Specify if colored elements are part of the standard interface (e.g., always red button) or if they seem to be related to the problem
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
- Vérifie spécifiquement si des messages d'erreur sont visibles en bas ou en haut de l'écran
3. URLs and Links
- Identify and explicitly copy ALL URLs visible in the image
- Hyperlinks in blue or underlined text
- API endpoints, server addresses
- Format each URL on its own line for clarity: [URL] https://example.com
- For masked/shortened URLs, clearly indicate what text is displayed
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
- Précise le nom complet du module/essai concerné par le problème (par exemple "Essai au bleu de méthylène (MB)" et pas seulement "essai bleu")
- Identifie si l'utilisateur a accès à l'écran d'essai mais avec des erreurs, ou s'il n'y a pas d'accès du tout
4. Highlighted Elements
- Look for circled, framed, highlighted or arrowed areas
- These elements are often important for the client or support
- Explicitly mention their content and highlighting style
- Specifically check if error messages are visible at the bottom or top of the screen
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
- Tente d'extrapoler le contexte technique précis en observant l'interface (ex: "l'essai au bleu" mentionné par le client correspond clairement à "l'essai au bleu de méthylène (MB) - NF EN 933-9")
5. Relationship with the Problem
- Establish the link between visible elements and the problem described in the ticket
- Indicate if components seem related to a misconfiguration or error
- Specify the complete name of the module/test concerned by the problem (for example "Methylene blue test (MB)" and not just "blue test")
- Identify if the user has access to the test screen but with errors, or if there is no access at all
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é)
- Établis des connections explicites entre le vocabulaire utilisé par le client et ce qui est visible dans l'interface
6. Potential Answers
- Determine if the image provides elements of answer to a question asked in:
- The ticket title
- The problem description
- Try to extrapolate the precise technical context by observing the interface (e.g., the "blue test" mentioned by the client clearly corresponds to "methylene blue test (MB) - NF EN 933-9")
7. Contexte technique élargi
- Identifie le contexte plus large de l'application (laboratoire, tests techniques, essais normalisés)
- Relève toutes les références à des normes ou standards (ex: NF EN 933-9)
- Mentionne tous les codes ou identifiants visibles qui pourraient être utiles (ex: numéros d'échantillons)
7. Link with the Discussion
- Check if the image echoes a step described in the discussion thread
- Note correspondences (e.g., same module, same error message as previously mentioned)
- Establish explicit connections between the vocabulary used by the client and what's visible in the interface
Règles importantes :
- Ne fais AUCUNE interprétation ni diagnostic sur les causes possibles
- Ne propose PAS de solution ou recommandation
- Reste strictement factuel et objectif, mais fais des liens explicites avec les termes utilisés par le client
- 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)
- Utilise systématiquement le nom complet et précis des modules et essais
- Vérifie la lecture correcte des boutons et menus (attention aux confusions comme PAZ/RAZ)
8. Broader Technical Context
- Identify the wider context of the application (laboratory, technical tests, standardized tests)
- Note any references to standards or norms (e.g., NF EN 933-9)
- Mention any visible codes or identifiers that might be useful (e.g., sample numbers)
Important Rules:
- Do NOT make ANY interpretation or diagnosis about possible causes
- Do NOT propose solutions or recommendations
- Remain strictly factual and objective, but make explicit links with terms used by the client
- Focus only on what is visible in the image
- Reproduce exact texts (e.g., error messages, parameter labels)
- Pay special attention to modifiable (interactive) and non-modifiable (grayed out) elements
- Systematically use the complete and precise name of modules and tests
- Verify correct reading of buttons and menus (beware of confusions like PAZ/RAZ)
- ALWAYS list URLs and links in a separate dedicated section
"""
)
self.system_prompt = (
"""
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.
You are an expert in image analysis for BRG-Lab technical support for CBAO company.
Your mission is to analyze screenshots related to the support ticket context.
Tu dois être extrêmement précis dans ta lecture des interfaces et des éléments techniques.
Les clients utilisent souvent des termes abrégés (comme "essai bleu") alors que l'interface montre le terme complet ("Essai au bleu de méthylène"). Tu dois faire le lien entre ces termes.
You must be extremely precise in your reading of interfaces and technical elements.
Clients often use abbreviated terms (like "blue test") while the interface shows the full term ("Methylene blue test"). You must make the connection between these terms.
Certains éléments dans l'interface peuvent prêter à confusion :
- Les boutons "RAZ" (réinitialisation) sont parfois difficiles à lire
- Des éléments en couleur peuvent faire partie de l'interface standard (et non du problème)
- Les messages d'erreur sont souvent en bas de l'écran et contiennent des informations cruciales
Some elements in the interface may cause confusion:
- "RAZ" buttons (reset) are sometimes difficult to read
- Colored elements may be part of the standard interface (and not part of the problem)
- Error messages are often at the bottom of the screen and contain crucial information
- URLs and links must be explicitly captured and listed separately
Structure ton analyse d'image de façon factuelle:
Structure your image analysis factually:
{instructions}
Ton analyse sera utilisée comme élément factuel pour un rapport technique plus complet et pour faire le lien entre le vocabulaire du client et les éléments techniques réels.
Your analysis will be used as a factual element for a more complete technical report and to link the client's vocabulary with the actual technical elements.
IMPORTANT: All responses should be in English. Translation to French will be handled separately.
"""
).format(
instructions=self.instructions_analyse
)
self._appliquer_config_locale()
logger.info("AgentImageAnalyser initiali")
logger.info("AgentImageAnalyser initialized")
def _appliquer_config_locale(self) -> None:
"""
Applique la configuration locale au modèle LLM.
Applies local configuration to the LLM model.
"""
if hasattr(self.llm, "prompt_system"):
self.llm.prompt_system = self.system_prompt
@ -118,7 +131,7 @@ class AgentImageAnalyser(BaseAgent):
def _verifier_image(self, image_path: str) -> bool:
"""
Vérifie si l'image existe et est accessible
Checks if the image exists and is accessible
"""
try:
if not os.path.exists(image_path) or not os.access(image_path, os.R_OK):
@ -128,109 +141,185 @@ class AgentImageAnalyser(BaseAgent):
width, height = img.size
return width > 0 and height > 0
except Exception as e:
logger.error(f"Vérification impossible pour {image_path}: {e}")
logger.error(f"Verification failed for {image_path}: {e}")
return False
def _generer_prompt_analyse(self, contexte: str, prefix: str = "") -> str:
def _extraire_urls(self, texte: str) -> List[str]:
"""
Génère le prompt d'analyse d'image
"""
return f"""{prefix}
CONTEXTE DU TICKET:
{contexte}
INSTRUCTIONS IMPORTANTES:
- Lis attentivement tous les textes visibles dans l'image et reporte-les avec précision.
- Veille à bien distinguer les boutons "RAZ" (réinitialisation) s'ils sont présents.
- Identifie précisément le nom complet des essais ou tests mentionnés (ex: "Essai au bleu de méthylène" plutôt que juste "essai bleu").
- Si l'interface montre un essai ou module en particulier, précise son nom complet et sa norme associée.
- Fais attention aux messages d'erreur qui peuvent apparaître en bas ou en haut de l'écran.
Fournis une analyse STRICTEMENT FACTUELLE de l'image avec les sections suivantes:
{self.instructions_analyse}"""
Extracts URLs from a text
def executer(self, image_path: str, contexte: str = "") -> Dict[str, Any]:
Args:
texte: The text to analyze
Returns:
List of extracted URLs
"""
Analyse une image en tenant compte du contexte du ticket
import re
# Pattern to detect URLs (more complete than a simple http:// search)
url_pattern = r'https?://(?:[-\w.]|(?:%[\da-fA-F]{2}))+'
# Search in the text with a broader pattern to capture context
url_mentions = re.finditer(r'(?:URL|link|adresse|href|http)[^\n]*?(https?://[^\s\)\]\"\']+)', texte, re.IGNORECASE)
# List to store URLs with their context
urls = []
# Add URLs extracted with the generic pattern
for url in re.findall(url_pattern, texte):
if url not in urls:
urls.append(url)
# Add URLs extracted from the broader context
for match in url_mentions:
url = match.group(1)
if url not in urls:
urls.append(url)
return urls
def _construire_prompt(self, image_path: str, contexte: Dict[str, Any]) -> str:
"""
Construit le prompt pour l'analyse d'image avec contexte.
Args:
image_path: Chemin vers l'image à analyser
contexte: Contexte d'analyse du ticket
Returns:
Prompt formaté avec instructions et contexte
"""
image_name = os.path.basename(image_path)
print(f" AgentImageAnalyser: Analyse de {image_name}")
if not self._verifier_image(image_path):
return self._erreur("Erreur d'accès ou image invalide", image_path)
# Extraire le contexte du ticket (résumé en anglais)
ticket_content_en = ""
if isinstance(contexte, dict):
if "response_en" in contexte:
ticket_content_en = contexte["response_en"]
elif "response" in contexte:
ticket_content_en = contexte["response"]
# Extraire le texte OCR si disponible dans le contexte
ocr_text = ""
if isinstance(contexte, dict) and "ocr_text" in contexte:
ocr_text = contexte["ocr_text"]
elif isinstance(contexte, dict) and "ocr_info" in contexte:
ocr_text = contexte["ocr_info"].get("texte_en", "")
# Construire le prompt avec instructions précises
prompt = f"""[ENGLISH RESPONSE REQUESTED]
Analyze this image in the context of a technical support ticket.
IMAGE: {image_name}
"""
# Ajouter le texte OCR s'il est disponible
if ocr_text and len(ocr_text.strip()) > 10:
prompt += f"""OCR TEXT DETECTED IN IMAGE:
{ocr_text}
"""
# Ajouter le contexte du ticket
prompt += f"""SUPPORT TICKET CONTEXT:
{ticket_content_en[:1500]}
INSTRUCTIONS:
1. Describe what is shown in this image in detail
2. Identify any error messages, technical information, or interface elements
3. Explain how this image relates to the support ticket context provided
4. Note any version numbers, status indicators, or dates visible
5. Extract specific technical details that might help diagnose the issue
If the image contains text, code, or error messages, transcribe all important parts.
Structure your analysis clearly with headers and bullet points.
"""
return prompt
def executer(self, image_path: str, contexte: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""
Analyse une image dans le contexte d'un ticket de support.
Args:
image_path: Chemin vers l'image à analyser
contexte: Contexte d'analyse du ticket (optionnel)
Returns:
Dictionnaire contenant les résultats d'analyse
"""
image_name = os.path.basename(image_path)
print(f" AgentImageAnalyser: Analyse de {image_name}")
if not image_path or not os.path.exists(image_path):
return self._error_response(f"Image introuvable: {image_path}")
# Si pas de contexte, utiliser un objet vide
if contexte is None:
contexte = {}
# Extraire le ticket_id pour la sauvegarde
ticket_id = self._extraire_ticket_id(image_path, contexte)
try:
prompt = self._generer_prompt_analyse(contexte, "Analyse cette image en tenant compte du contexte suivant:")
# Générer le prompt avec le contexte et les résultats OCR
prompt = self._construire_prompt(image_path, contexte)
# Interroger le modèle avec l'image
if hasattr(self.llm, "interroger_avec_image"):
response = self.llm.interroger_avec_image(image_path, prompt)
elif hasattr(self.llm, "_encoder_image_base64"):
img_base64 = self.llm._encoder_image_base64(image_path)
if img_base64:
prompt_base64 = self._generer_prompt_analyse(contexte, f"Analyse cette image:\n{img_base64}")
response = self.llm.interroger(prompt_base64)
else:
return self._erreur("Impossible d'encoder l'image en base64", image_path)
response_en = self.llm.interroger_avec_image(image_path, prompt, english_only=True)
else:
return self._erreur("Le modèle ne supporte pas l'analyse d'images", image_path)
return self._error_response("Le modèle ne supporte pas l'analyse d'images", ticket_id)
# 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"
]
if any(phrase in response.lower() for phrase in error_phrases):
return self._erreur("Le modèle n'a pas pu analyser l'image correctement", image_path, raw=response)
# Effectuer des corrections sur la réponse si nécessaire
response = self._corriger_termes_courants(response)
# Traduire les résultats en français pour la compatibilité
response_fr = en_to_fr(response_en)
# Formater la réponse avec métadonnées
result = {
"analyse": response,
"raw_response": response,
"analyse": response_fr,
"analyse_en": response_en,
"metadata": {
"image_path": image_path,
"image_name": image_name,
"ticket_id": ticket_id,
"timestamp": self._get_timestamp(),
"model_info": {
"model": getattr(self.llm, "modele", str(type(self.llm))),
**getattr(self.llm, "params", {})
},
"ocr_used": "ocr_text" in contexte or "ocr_info" in contexte,
"model": getattr(self.llm, "modele", "unknown"),
"source_agent": self.nom
}
}
# Sauvegarder les données dans le fichier
sauvegarder_donnees(None, "analyse_image", result, base_dir="reports", is_resultat=True)
# Sauvegarder les résultats
if ticket_id and ticket_id != "UNKNOWN":
sauvegarder_donnees(ticket_id, "analyse_image",
{"image_path": image_path, "analysis": result},
is_resultat=True)
# Enregistrer l'analyse dans l'historique
# Ajouter à l'historique
self.ajouter_historique(
"analyse_image",
{"image_path": image_path, "contexte": contexte, "prompt": prompt},
"analyse_image",
{"image_path": image_path, "prompt": prompt},
result
)
return result
except Exception as e:
return self._erreur(str(e), image_path)
logger.error(f"Erreur lors de l'analyse de l'image {image_name}: {str(e)}")
return self._error_response(f"Erreur: {str(e)}", ticket_id)
def _corriger_termes_courants(self, texte: str) -> str:
"""
Corrige certains termes couramment mal interprétés par le modèle.
Corrects commonly misinterpreted terms by the model.
"""
corrections = {
"PAZ": "RAZ",
"Essai bleu": "Essai au bleu de méthylène",
"essai bleu": "essai au bleu de méthylène",
"Essai au bleu": "Essai au bleu de méthylène"
"Essai au bleu": "Essai au bleu de méthylène",
"Methylene blue test": "Essai au bleu de méthylène",
"Blue test": "Essai au bleu de méthylène"
}
for terme_incorrect, terme_correct in corrections.items():
@ -240,10 +329,10 @@ Fournis une analyse STRICTEMENT FACTUELLE de l'image avec les sections suivantes
def _erreur(self, message: str, path: str, raw: str = "") -> Dict[str, Any]:
"""
Crée un dictionnaire de réponse en cas d'erreur
Creates an error response dictionary
"""
return {
"analyse": f"ERREUR: {message}",
"analyse": f"ERROR: {message}",
"raw_response": raw,
"error": True,
"metadata": {
@ -256,6 +345,59 @@ Fournis une analyse STRICTEMENT FACTUELLE de l'image avec les sections suivantes
}
def _get_timestamp(self) -> str:
"""Retourne un timestamp au format YYYYMMDD_HHMMSS"""
"""Returns a timestamp in YYYYMMDD_HHMMSS format"""
from datetime import datetime
return datetime.now().strftime("%Y%m%d_%H%M%S")
return datetime.now().strftime("%Y%m%d_%H%M%S")
def _extraire_ticket_id(self, image_path: str, contexte: Dict[str, Any]) -> str:
"""
Extrait l'ID du ticket à partir du chemin de l'image ou du contexte.
Args:
image_path: Chemin vers l'image
contexte: Contexte d'analyse du ticket
Returns:
ID du ticket ou "UNKNOWN" si non trouvé
"""
# D'abord, chercher dans le contexte
if isinstance(contexte, dict):
if "metadata" in contexte and "ticket_id" in contexte["metadata"]:
return contexte["metadata"]["ticket_id"]
if "ticket_id" in contexte:
return contexte["ticket_id"]
# Ensuite, chercher dans le chemin de l'image
parts = image_path.split(os.path.sep)
for part in parts:
# Format T12345
if part.startswith("T") and part[1:].isdigit():
return part
# Format ticket_T12345
if part.startswith("ticket_T"):
return part.replace("ticket_", "")
return "UNKNOWN"
def _error_response(self, message: str, ticket_id: str = "UNKNOWN") -> Dict[str, Any]:
"""
Crée une réponse d'erreur standardisée.
Args:
message: Message d'erreur
ticket_id: ID du ticket
Returns:
Dictionnaire avec la réponse d'erreur formatée
"""
return {
"analyse": f"ERREUR: {message}",
"analyse_en": f"ERROR: {message}",
"error": True,
"metadata": {
"timestamp": self._get_timestamp(),
"error": True,
"ticket_id": ticket_id,
"source_agent": self.nom
}
}

View File

@ -1,157 +1,181 @@
import logging
import os
from PIL import Image
from typing import Dict, Any, Tuple
from typing import Dict, Any, Tuple, List
from datetime import datetime
from ..base_agent import BaseAgent
from ..utils.pipeline_logger import sauvegarder_donnees
from utils.ocr_utils import extraire_texte_fr
from utils.translate_utils import fr_to_en, en_to_fr, sauvegarder_ocr_traduction
logger = logging.getLogger("AgentImageSorter")
class AgentImageSorter(BaseAgent):
"""
Agent de tri d'image optimisé pour llama_vision.
Réalise un OCR en français, le traduit en anglais, génère un prompt enrichi, et analyse l'image.
Détermine si une image est pertinente pour l'analyse d'un ticket de support.
"""
def __init__(self, llm):
super().__init__("AgentImageSorter", llm)
self.params = {
"temperature": 0.2,
"temperature": 0.1,
"top_p": 0.8,
"max_tokens": 300
"max_tokens": 1000
}
# Collecteur de résultats pour traitement par lots
self.resultats = []
# Amélioration du prompt système pour obtenir une réponse plus précise
self.system_prompt = """You are an expert image evaluator for technical support tickets.
Your task is to determine if an image is relevant to a technical support issue for BRG_Lab software (CBAO enterprise).
Follow these guidelines when analyzing images:
1. Technical screenshots (interfaces, error messages, logs) are RELEVANT
2. System configuration pages are RELEVANT
3. Personal photos, generic web pages, marketing materials are NOT RELEVANT
4. Corrupted or unreadable images are NOT RELEVANT
Respond with only 'Yes' or 'No' at the beginning, followed by a brief explanation.
DO NOT attempt to perform OCR or analyze text - focus only on the image format and content type."""
self._appliquer_config_locale()
logger.info("AgentImageSorter (llama_vision) initialisé")
def _appliquer_config_locale(self) -> None:
"""Applique la configuration locale au modèle."""
if hasattr(self.llm, "prompt_system"):
self.llm.prompt_system = self.system_prompt
if hasattr(self.llm, "configurer"):
self.llm.configurer(**self.params)
def _verifier_image(self, image_path: str) -> bool:
"""
Vérifie si l'image existe, est accessible et a un format valide.
Ne réalise aucun prétraitement.
Args:
image_path: Chemin vers l'image
Returns:
True si l'image est valide, False sinon
"""
try:
# Vérifier que le fichier existe et est accessible
if not os.path.exists(image_path) or not os.access(image_path, os.R_OK):
logger.warning(f"Image inaccessible: {image_path}")
return False
# Vérifier que c'est une image valide avec PIL
with Image.open(image_path) as img:
width, height = img.size
return width > 0 and height > 0
if width < 10 or height < 10:
logger.warning(f"Image trop petite: {image_path} ({width}x{height})")
return False
# Vérifier le format
format = img.format.lower() if img.format else "unknown"
valid_formats = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp']
if format.lower() not in valid_formats:
logger.warning(f"Format d'image non supporté: {image_path} ({format})")
return False
return True
except Exception as e:
logger.error(f"Vérification impossible pour {image_path}: {e}")
logger.error(f"Erreur lors de la vérification de l'image {image_path}: {e}")
return False
def _generer_prompt(self, ocr_fr: str, ocr_en: str) -> str:
return (
"[ENGLISH RESPONSE REQUESTED]\n\n"
"The following image is from a technical support ticket at CBAO "
"for the BRG_Lab software system.\n\n"
f"OCR detected French text:\n[FR] {ocr_fr or ''}\n[EN] {ocr_en or ''}\n\n"
"Please analyze the image and determine:\n"
"- Is it relevant for a technical support issue?\n"
"- Answer only 'yes' or 'no', then briefly explain why in English."
def _extraire_ticket_id_depuis_path(self, image_path: str) -> str:
"""Extrait l'ID du ticket depuis le chemin de l'image."""
parts = image_path.split(os.sep)
for part in parts:
if part.startswith("T") and part[1:].isdigit():
return part
if part.startswith("ticket_T") and part[8:].isdigit():
return "T" + part[8:]
return "UNKNOWN"
def _generer_prompt(self, ocr_text: str = "") -> str:
"""Génère le prompt pour le LLM."""
prompt = (
"Is this image relevant for a technical support issue related to the BRG_Lab software from CBAO enterprise?\n\n"
"Technical screenshots, error messages, configuration screens, and system logs ARE relevant.\n"
"Personal photos, generic web pages, corrupted images, and marketing materials are NOT relevant.\n\n"
)
# Ajouter le texte OCR au prompt si disponible
if ocr_text and len(ocr_text.strip()) > 10:
prompt += f"Text detected in the image (OCR):\n\"{ocr_text}\"\n\n"
prompt += "Answer only with 'Yes' or 'No' at the beginning, followed by a brief explanation."
return prompt
def _analyser_reponse(self, response: str) -> Tuple[bool, str]:
r = response.lower()
first_line = r.split('\n')[0] if '\n' in r else r.strip()[:50]
"""Analyse la réponse du LLM pour déterminer la pertinence de l'image."""
response_lower = response.lower()
# Analyse simplifiée basée sur la première ligne
first_line = response_lower.split('\n')[0] if '\n' in response_lower else response_lower.strip()[:50]
# Détection pour réponses en anglais
if first_line.startswith("yes"):
return True, response.strip()
if first_line.startswith("no"):
return False, response.strip()
# Fallback: analyse basée sur les mots-clés
positive_count = sum(1 for word in ["relevant", "useful", "error message", "interface", "technical", "system"] if word in response_lower)
negative_count = sum(1 for word in ["not relevant", "irrelevant", "unrelated", "personal", "marketing", "corrupted"] if word in response_lower)
return positive_count > negative_count, response.strip()
def sauvegarder_resultats(self) -> None:
"""Sauvegarde tous les résultats collectés."""
logger.info(f"Sauvegarde de {len(self.resultats)} résultats de tri d'images")
for resultat in self.resultats:
ticket_id = resultat.get("metadata", {}).get("ticket_id", "UNKNOWN")
image_path = resultat.get("metadata", {}).get("image_path", "")
# Détection pour réponses en français (fallback)
if first_line.startswith("oui"):
return True, response.strip()
if first_line.startswith("non"):
return False, response.strip()
if not image_path or not os.path.exists(image_path):
continue
# Sauvegarde dans la structure de répertoires appropriée
try:
sauvegarder_donnees(ticket_id, "tri_image", resultat, base_dir="reports", is_resultat=True)
except Exception as e:
logger.error(f"Erreur lors de la sauvegarde des résultats pour {image_path}: {e}")
# Réinitialiser la liste après sauvegarde
self.resultats = []
# Analyse basée sur mots-clés (anglais et français)
pos_keywords = ["pertinent", "utile", "interface", "message", "diagnostic",
"useful", "relevant", "interface", "message", "diagnostic", "helpful"]
neg_keywords = ["inutile", "photo", "irrelevant", "hors sujet",
"useless", "irrelevant", "unrelated", "not relevant"]
score = sum(kw in r for kw in pos_keywords) - sum(kw in r for kw in neg_keywords)
return score > 0, response.strip()
def _erreur(self, message: str, path: str, raw: str = "") -> Dict[str, Any]:
return {
"is_relevant": False,
"reason": message,
"raw_response": raw,
"error": True,
"metadata": {
"image_path": path,
"image_name": os.path.basename(path),
"timestamp": self._get_timestamp(),
"error": True,
"source_agent": self.nom
}
}
def _get_timestamp(self) -> str:
return datetime.now().strftime("%Y%m%d_%H%M%S")
def _extraire_ticket_id_depuis_path(self, image_path: str) -> str:
def executer(self, image_path: str, ocr_context: str = "") -> Dict[str, Any]:
"""
Extrait l'ID du ticket depuis le chemin de l'image.
Cherche d'abord dans les segments du chemin, puis examine la structure de dossiers.
Supporte les formats comme:
- output/ticket_T12345/T12345_date/...
- .../T12345/...
Filtre une image pour déterminer si elle est pertinente pour un ticket de support.
Ne fait pas d'OCR, se concentre uniquement sur la pertinence visuelle.
Args:
image_path: Chemin de l'image
image_path: Chemin vers l'image à analyser
ocr_context: Texte extrait de l'image par OCR (optionnel)
Returns:
ID du ticket ou "unknown_ticket" si non trouvé
Dictionnaire contenant les résultats d'analyse
"""
# Normaliser le chemin pour éviter les problèmes de séparateurs
norm_path = os.path.normpath(image_path)
parts = norm_path.split(os.sep)
# Première passe: chercher directement un segment "Txxxx"
for part in parts:
if part.startswith("T") and part[1:].isdigit():
return part
# Deuxième passe: chercher dans la structure de dossiers output/ticket_Txxxx/
for i, part in enumerate(parts):
if part == "output" and i+1 < len(parts):
next_part = parts[i+1]
if next_part.startswith("ticket_T"):
return next_part.replace("ticket_", "")
# Pas d'ID de ticket trouvé
logger.warning(f"Impossible d'extraire l'ID de ticket depuis le chemin: {image_path}")
return "unknown_ticket"
def executer(self, image_path: str) -> Dict[str, Any]:
image_name = os.path.basename(image_path)
print(f" AgentImageSorter: Évaluation de {image_name}")
if not self._verifier_image(image_path):
return self._erreur("Erreur d'accès ou image invalide", image_path)
return self._erreur("Erreur d'accès ou format d'image invalide", image_path)
try:
# Pré-traitement OCR et traduction
# Extraction du ticket ID pour la sauvegarde
ticket_id = self._extraire_ticket_id_depuis_path(image_path)
ocr_fr = extraire_texte_fr(image_path)
ocr_en = fr_to_en(ocr_fr)
ocr_en_back_fr = en_to_fr(ocr_en) if ocr_en else ""
# Prompt pour LlamaVision (avec contexte OCR si disponible)
prompt = self._generer_prompt(ocr_context)
# Sauvegarde OCR + Traductions
sauvegarder_ocr_traduction(image_path, ticket_id, ocr_fr, ocr_en, ocr_en_back_fr, base_dir=None)
# Prompt en anglais enrichi
prompt = self._generer_prompt(ocr_fr, ocr_en)
# Appel au LLM
# Utilisation directe de la vision pour l'analyse d'image
if hasattr(self.llm, "interroger_avec_image"):
response = self.llm.interroger_avec_image(image_path, prompt)
else:
@ -164,27 +188,63 @@ class AgentImageSorter(BaseAgent):
is_relevant, reason = self._analyser_reponse(response)
print(f" Décision: Image {image_name} {'pertinente' if is_relevant else 'non pertinente'}")
# Construction du résultat
result = {
"is_relevant": is_relevant,
"reason": reason,
"raw_response": response,
"ocr_fr": ocr_fr,
"ocr_en": ocr_en,
"ocr_used": bool(ocr_context and len(ocr_context.strip()) > 10),
"metadata": {
"image_path": image_path,
"image_name": image_name,
"timestamp": self._get_timestamp(),
"model_info": {
"model": getattr(self.llm, "modele", str(type(self.llm))),
**getattr(self.llm, "params", {})
**self.params
},
"ticket_id": ticket_id,
"source_agent": self.nom
}
}
sauvegarder_donnees(ticket_id, "tri_image", result, base_dir=None, is_resultat=True)
self.ajouter_historique("tri_image", {"image_path": image_path, "prompt": prompt}, result)
# Ajouter au collecteur de résultats
self.resultats.append(result)
# Ajouter à l'historique
self.ajouter_historique(
"tri_image",
{"image_path": image_path, "prompt": prompt, "ocr_context": ocr_context},
result
)
return result
except Exception as e:
return self._erreur(str(e), image_path)
return self._erreur(f"Erreur inattendue: {str(e)}", image_path)
def _erreur(self, message: str, path: str, raw: str = "") -> Dict[str, Any]:
"""Génère un rapport d'erreur standardisé."""
ticket_id = self._extraire_ticket_id_depuis_path(path)
result = {
"is_relevant": False, # Par défaut, les images avec erreur ne sont pas considérées pertinentes
"reason": f"ERROR: {message}",
"raw_response": raw,
"error": True,
"metadata": {
"image_path": path,
"image_name": os.path.basename(path),
"ticket_id": ticket_id,
"timestamp": self._get_timestamp(),
"error": True,
"source_agent": self.nom
}
}
# Ajouter au collecteur de résultats même en cas d'erreur
self.resultats.append(result)
return result
def _get_timestamp(self) -> str:
"""Génère un timestamp au format YYYYMMDD_HHMMSS."""
return datetime.now().strftime("%Y%m%d_%H%M%S")

View File

@ -6,10 +6,11 @@ import json
import traceback
from datetime import datetime
from ..utils.pipeline_logger import sauvegarder_donnees
from utils.translate_utils import fr_to_en, en_to_fr
# Configuration du logger pour plus de détails
# Configuration for detailed logging
logger = logging.getLogger("AgentReportGenerator")
logger.setLevel(logging.DEBUG) # Augmenter le niveau de logging pour le débogage
logger.setLevel(logging.DEBUG) # Increase logging level for debugging
class AgentReportGenerator(BaseAgent):
def __init__(self, llm):
@ -21,7 +22,8 @@ class AgentReportGenerator(BaseAgent):
"max_tokens": 8000
}
self.system_prompt = """Tu es un expert en support technique chargé de générer un rapport final à partir des analyses d'un ticket de support.
# System prompt in French (preserved for French-speaking models)
self.system_prompt_fr = """Tu es un expert en support technique chargé de générer un rapport final à partir des analyses d'un ticket de support.
Ton rôle est de croiser les informations provenant :
- de l'analyse textuelle du ticket client
- des analyses détaillées de plusieurs captures d'écran
@ -54,79 +56,145 @@ Règles pour le tableau d'échanges :
Reste strictement factuel. Ne fais aucune hypothèse. Ne suggère pas d'étapes ni d'interprétation."""
# System prompt in English for LlamaVision
self.system_prompt_en = """You are a technical support expert responsible for generating a final report from the analyses of a support ticket.
Your role is to cross-reference information from:
- textual analysis of the customer ticket
- detailed analyses of multiple screenshots
You must structure your response in a clear question/answer format, keeping all important points.
Never propose a solution. Do not reformulate the context.
Your only mission is to cross-reference textual and visual data to draw clear observations, listing factual elements visible in the screenshots that support or complement the ticket text.
Expected report structure:
1. General context (textual ticket summary in one sentence)
2. Identified problems or questions (in the form of clear questions)
3. Cross-referenced image/text summary for each question
4. List of additional relevant observations (if applicable)
5. Chronological exchange table
- Include a structured table of exchanges between client and support
- Format: Sender | Type | Date | Content | Relevant visual elements
- Do not mention real names, use "CLIENT" and "SUPPORT"
- Synthesize content while preserving important information
- Preserve additional information provided by images (example: client: blue test, image analysis: Methylene blue test (MB) - NF EN 933-9 (02-2022))
- Preserve useful links (documentation, FAQ, manual, links to web pages, etc.)
- Associate visual elements from screenshots with corresponding exchanges
Rules for the exchange table:
- TYPE can be: question, answer, information, visual complement
- For each client exchange mentioning a problem, add visual elements from screenshots that contextualize the problem
- For each support response, add visual elements that confirm or contradict the response
- Do not invent any content or date
- Use factual data from images to enrich understanding of exchanges
Stay strictly factual. Make no assumptions. Do not suggest steps or interpretation."""
self._appliquer_config_locale()
logger.info("AgentReportGenerator initialisé")
logger.info("AgentReportGenerator initialized")
def _appliquer_config_locale(self) -> None:
"""
Applies local configuration based on the model used (LlamaVision or other)
"""
if hasattr(self.llm, "prompt_system"):
self.llm.prompt_system = self.system_prompt
model_name = getattr(self.llm, "modele", "").lower()
# Use English prompt for LlamaVision
if "llama" in model_name or "vision" in model_name:
self.llm.prompt_system = self.system_prompt_en
logger.info("LlamaVision mode detected: using English system prompt")
else:
self.llm.prompt_system = self.system_prompt_fr
logger.info("Using French system prompt")
if hasattr(self.llm, "configurer"):
self.llm.configurer(**self.params)
def _verifier_donnees_entree(self, rapport_data: Dict[str, Any]) -> bool:
"""
Vérifie que les données d'entrée contiennent les éléments nécessaires.
Verifies that the input data contains the necessary elements.
Args:
rapport_data: Données pour générer le rapport
rapport_data: Data for generating the report
Returns:
bool: True si les données sont valides, False sinon
bool: True if data is valid, False otherwise
"""
ticket_id = rapport_data.get("ticket_id")
if not ticket_id:
logger.error("Erreur de validation: ticket_id manquant")
logger.error("Validation error: missing ticket_id")
return False
ticket_analyse = rapport_data.get("ticket_analyse")
if not ticket_analyse:
logger.error(f"Erreur de validation pour {ticket_id}: analyse de ticket manquante")
logger.error(f"Validation error for {ticket_id}: missing ticket analysis")
return False
analyses_images = rapport_data.get("analyse_images", {})
if not analyses_images:
logger.warning(f"Avertissement pour {ticket_id}: aucune analyse d'image disponible")
# On continue quand même car on peut générer un rapport sans images
logger.warning(f"Warning for {ticket_id}: no image analysis available")
# Continue anyway because we can generate a report without images
# Vérifier si au moins une image a été analysée
# Check if at least one image has been analyzed
images_analysees = 0
for img_path, img_data in analyses_images.items():
if img_data.get("analysis") and img_data["analysis"].get("analyse"):
images_analysees += 1
if images_analysees == 0 and analyses_images:
logger.warning(f"Avertissement pour {ticket_id}: {len(analyses_images)} images trouvées mais aucune n'a été analysée")
logger.warning(f"Warning for {ticket_id}: {len(analyses_images)} images found but none analyzed")
logger.info(f"Validation pour {ticket_id}: OK, {images_analysees} images analysées sur {len(analyses_images)} images")
logger.info(f"Validation for {ticket_id}: OK, {images_analysees} images analyzed out of {len(analyses_images)} images")
return True
def executer(self, rapport_data: Dict[str, Any]) -> str:
ticket_id = rapport_data.get("ticket_id", "Inconnu")
print(f"AgentReportGenerator : génération du rapport pour le ticket {ticket_id}")
ticket_id = rapport_data.get("ticket_id", "Unknown")
print(f"AgentReportGenerator: generating report for ticket {ticket_id}")
try:
# Vérifier et enregistrer les données d'entrée pour le débogage
logger.debug(f"Données reçues pour {ticket_id}: {json.dumps(rapport_data, default=str)[:500]}...")
# Check and log input data for debugging
logger.debug(f"Data received for {ticket_id}: {json.dumps(rapport_data, default=str)[:500]}...")
# Vérifier que les données d'entrée sont valides
# Verify that input data is valid
if not self._verifier_donnees_entree(rapport_data):
error_msg = f"Impossible de générer le rapport: données d'entrée invalides pour {ticket_id}"
print(f"ERREUR: {error_msg}")
return f"ERREUR: {error_msg}"
error_msg = f"Unable to generate report: invalid input data for {ticket_id}"
print(f"ERROR: {error_msg}")
return f"ERROR: {error_msg}"
print(f"Préparation du prompt pour le ticket {ticket_id}...")
print(f"Preparing prompt for ticket {ticket_id}...")
prompt = self._generer_prompt(rapport_data)
logger.debug(f"Prompt généré ({len(prompt)} caractères): {prompt[:500]}...")
logger.debug(f"Generated prompt ({len(prompt)} characters): {prompt[:500]}...")
print(f"Analyse en cours pour le ticket {ticket_id}...")
response = self.llm.interroger(prompt)
print(f"Analyse terminée: {len(response)} caractères")
logger.debug(f"Réponse reçue ({len(response)} caractères): {response[:500]}...")
# Determine if the model is LlamaVision and translate if necessary
model_name = getattr(self.llm, "modele", "").lower()
need_translation = "llama" in model_name or "vision" in model_name
if need_translation:
# Add explicit marker and translate prompt
prompt_en = f"[ENGLISH RESPONSE REQUESTED]\n\n{fr_to_en(prompt)}"
logger.info(f"Translating prompt to English for LlamaVision ({len(prompt_en)} characters)")
else:
prompt_en = prompt
print(f"Analysis in progress for ticket {ticket_id}...")
response = self.llm.interroger(prompt_en)
# Translate response to French if necessary
if need_translation:
response_fr = en_to_fr(response)
logger.info(f"Translating response to French ({len(response_fr)} characters)")
else:
response_fr = response
print(f"Analysis completed: {len(response_fr)} characters")
logger.debug(f"Response received ({len(response_fr)} characters): {response_fr[:500]}...")
# Création du résultat complet avec métadonnées
# Create complete result with metadata
result = {
"prompt": prompt,
"response": response,
"prompt_en": prompt_en if need_translation else None,
"response": response_fr, # French version for compatibility
"response_en": response if need_translation else None, # Original English version
"metadata": {
"ticket_id": ticket_id,
"timestamp": self._get_timestamp(),
@ -134,40 +202,48 @@ Reste strictement factuel. Ne fais aucune hypothèse. Ne suggère pas d'étapes
"model_info": {
"model": getattr(self.llm, "modele", str(type(self.llm))),
**getattr(self.llm, "params", {})
}
},
"language": "en-fr" if need_translation else "fr" # Indicates the language used
}
}
# Sauvegarder le résultat dans le pipeline
logger.info(f"Sauvegarde du rapport final pour le ticket {ticket_id}")
# Save the result in the pipeline
logger.info(f"Saving final report for ticket {ticket_id}")
# Créer un fichier de débogage direct pour vérifier le problème de sauvegarde
# Create direct debug file to check for save issues
try:
debug_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../debug"))
os.makedirs(debug_dir, exist_ok=True)
debug_path = os.path.join(debug_dir, f"rapport_debug_{ticket_id}.json")
with open(debug_path, "w", encoding="utf-8") as f:
json.dump(result, f, ensure_ascii=False, indent=2)
print(f"Fichier de débogage créé: {debug_path}")
print(f"Debug file created: {debug_path}")
# If we translated, also save EN/FR versions for debugging
if need_translation:
with open(os.path.join(debug_dir, f"rapport_debug_{ticket_id}_en.txt"), "w", encoding="utf-8") as f:
f.write(response)
with open(os.path.join(debug_dir, f"rapport_debug_{ticket_id}_fr.txt"), "w", encoding="utf-8") as f:
f.write(response_fr)
except Exception as e:
print(f"Erreur lors de la création du fichier de débogage: {str(e)}")
print(f"Error creating debug file: {str(e)}")
# Utiliser uniquement la fonction standard de sauvegarde
# Use only the standard save function
try:
sauvegarder_donnees(ticket_id, "rapport_final", result, base_dir=None, is_resultat=True)
print(f"Rapport final généré et sauvegardé pour le ticket {ticket_id}")
print(f"Final report generated and saved for ticket {ticket_id}")
except Exception as e:
logger.error(f"Erreur lors de la sauvegarde via sauvegarder_donnees: {str(e)}")
print(f"Erreur de sauvegarde standard: {str(e)}")
logger.error(f"Error when saving via sauvegarder_donnees: {str(e)}")
print(f"Standard save error: {str(e)}")
# Sauvegarder aussi une version en texte brut pour faciliter la lecture
# Also save a plain text version for easier reading
try:
# Trouver le chemin de ticket le plus récent
# Find the most recent ticket path
output_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../output"))
ticket_dir = os.path.join(output_dir, f"ticket_{ticket_id}")
if os.path.exists(ticket_dir):
# Trouver l'extraction la plus récente
# Find the most recent extraction
extractions = [d for d in os.listdir(ticket_dir) if os.path.isdir(os.path.join(ticket_dir, d)) and d.startswith(ticket_id)]
if extractions:
extractions.sort(reverse=True)
@ -175,91 +251,128 @@ Reste strictement factuel. Ne fais aucune hypothèse. Ne suggère pas d'étapes
rapports_dir = os.path.join(ticket_dir, latest_extraction, f"{ticket_id}_rapports")
pipeline_dir = os.path.join(rapports_dir, "pipeline")
# S'assurer que le répertoire existe
# Ensure directory exists
os.makedirs(pipeline_dir, exist_ok=True)
# Récupérer le nom du modèle
# Get model name
model_name = getattr(self.llm, "modele", "unknown")
# Sauvegarder en format texte uniquement avec le nom du modèle
# Save in text format with the model name
rapport_txt_path = os.path.join(pipeline_dir, f"rapport_final_{model_name}_results.txt")
with open(rapport_txt_path, "w", encoding="utf-8") as f:
f.write(f"RAPPORT D'ANALYSE DU TICKET {ticket_id}\n")
f.write("="*50 + "\n\n")
f.write(response)
print(f"Version texte sauvegardée dans: {rapport_txt_path}")
f.write(response_fr) # French version
print(f"Text version saved in: {rapport_txt_path}")
# If we translated, also save the English version
if need_translation:
rapport_txt_en_path = os.path.join(pipeline_dir, f"rapport_final_{model_name}_results_en.txt")
with open(rapport_txt_en_path, "w", encoding="utf-8") as f:
f.write(f"ANALYSIS REPORT FOR TICKET {ticket_id}\n")
f.write("="*50 + "\n\n")
f.write(response) # English version
print(f"English text version saved in: {rapport_txt_en_path}")
except Exception as e:
print(f"Erreur lors de la sauvegarde de la version texte: {str(e)}")
logger.error(f"Erreur de sauvegarde texte: {str(e)}")
print(f"Error saving text version: {str(e)}")
logger.error(f"Text save error: {str(e)}")
# Sauvegarder aussi dans le dossier reports pour compatibilité
# Also save in the reports folder for compatibility
try:
reports_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../reports"))
ticket_reports_dir = os.path.join(reports_dir, ticket_id)
os.makedirs(ticket_reports_dir, exist_ok=True)
# Récupérer le nom du modèle
# Get model name
model_name = getattr(self.llm, "modele", "unknown")
# Sauvegarder aussi en texte pour faciliter la lecture
# Also save in text for easier reading
rapport_txt_path = os.path.join(ticket_reports_dir, f"rapport_final_{ticket_id}_{model_name}.txt")
with open(rapport_txt_path, "w", encoding="utf-8") as f:
f.write(f"RAPPORT D'ANALYSE DU TICKET {ticket_id}\n")
f.write("="*50 + "\n\n")
f.write(response)
f.write(response_fr) # French version
logger.info(f"Rapport texte sauvegardé dans {rapport_txt_path}")
print(f"Rapport également sauvegardé en texte dans {ticket_reports_dir}")
# If we translated, also save the English version
if need_translation:
rapport_txt_en_path = os.path.join(ticket_reports_dir, f"rapport_final_{ticket_id}_{model_name}_en.txt")
with open(rapport_txt_en_path, "w", encoding="utf-8") as f:
f.write(f"ANALYSIS REPORT FOR TICKET {ticket_id}\n")
f.write("="*50 + "\n\n")
f.write(response) # English version
logger.info(f"Text report saved in {rapport_txt_path}")
print(f"Report also saved as text in {ticket_reports_dir}")
except Exception as e:
logger.warning(f"Impossible de sauvegarder le rapport texte dans reports/: {str(e)}")
print(f"Erreur de sauvegarde reports/: {str(e)}")
logger.warning(f"Unable to save text report in reports/: {str(e)}")
print(f"Error saving to reports/: {str(e)}")
# Ajouter à l'historique
# Add to history
self.ajouter_historique("rapport_final", {
"ticket_id": ticket_id,
"prompt": prompt,
"timestamp": self._get_timestamp()
}, response)
}, response_fr) # French version for history
print(f"Traitement du rapport terminé pour le ticket {ticket_id}")
return response
print(f"Report processing completed for ticket {ticket_id}")
return response_fr # Return French version for pipeline consistency
except Exception as e:
logger.error(f"Erreur lors de la génération du rapport : {str(e)}")
logger.error(f"Error generating report: {str(e)}")
logger.error(traceback.format_exc())
print(f"ERREUR CRITIQUE lors de la génération du rapport: {str(e)}")
return f"ERREUR: {str(e)}"
print(f"CRITICAL ERROR during report generation: {str(e)}")
return f"ERROR: {str(e)}"
def _generer_prompt(self, rapport_data: Dict[str, Any]) -> str:
"""
Generates the prompt for the report generator
Args:
rapport_data: Report data containing ticket analysis and image analyses
Returns:
str: Generated prompt
"""
ticket_text = rapport_data.get("ticket_analyse", "")
image_blocs = []
analyses_images = rapport_data.get("analyse_images", {})
# Ajouter des logs pour vérifier les données d'images
logger.info(f"Nombre d'images à analyser: {len(analyses_images)}")
# Add logs to check image data
logger.info(f"Number of images to analyze: {len(analyses_images)}")
for chemin_image, analyse_obj in analyses_images.items():
# Vérifier si l'image est pertinente
# Check if the image is relevant
is_relevant = analyse_obj.get("sorting", {}).get("is_relevant", False)
# Récupérer l'analyse si elle existe
# Get analysis if it exists
analyse = ""
if "analysis" in analyse_obj and analyse_obj["analysis"]:
analyse = analyse_obj["analysis"].get("analyse", "")
# Check if there is an English or French version of the analysis
if "analyse_en" in analyse_obj["analysis"]:
# Use directly the English version if the model is LlamaVision
model_name = getattr(self.llm, "modele", "").lower()
if "llama" in model_name or "vision" in model_name:
analyse = analyse_obj["analysis"].get("analyse_en", "")
else:
analyse = analyse_obj["analysis"].get("analyse", "")
else:
# Use standard analysis if no language versions
analyse = analyse_obj["analysis"].get("analyse", "")
if analyse:
image_blocs.append(f"--- IMAGE : {os.path.basename(chemin_image)} ---\n{analyse}\n")
logger.info(f"Ajout de l'analyse de l'image {os.path.basename(chemin_image)} ({len(analyse)} caractères)")
logger.info(f"Adding analysis of image {os.path.basename(chemin_image)} ({len(analyse)} characters)")
else:
logger.warning(f"Image {os.path.basename(chemin_image)} sans analyse")
logger.warning(f"Image {os.path.basename(chemin_image)} without analysis")
bloc_images = "\n".join(image_blocs)
# Log pour vérifier la taille des données
logger.info(f"Taille de l'analyse ticket: {len(ticket_text)} caractères")
logger.info(f"Taille du bloc images: {len(bloc_images)} caractères")
# Log to check data size
logger.info(f"Size of ticket analysis: {len(ticket_text)} characters")
logger.info(f"Size of image block: {len(bloc_images)} characters")
# Keep the prompt in French as output will be in French or translated from English
prompt = (
f"Voici les données d'analyse pour un ticket de support :\n\n"
f"=== ANALYSE DU TICKET ===\n{ticket_text}\n\n"
@ -276,5 +389,6 @@ Reste strictement factuel. Ne fais aucune hypothèse. Ne suggère pas d'étapes
return prompt
def _get_timestamp(self) -> str:
"""Returns a timestamp in YYYYMMDD_HHMMSS format"""
from datetime import datetime
return datetime.now().strftime("%Y%m%d_%H%M%S")

View File

@ -1,11 +1,13 @@
from ..base_agent import BaseAgent
from typing import Dict, Any
from typing import Dict, Any, List, Union
import logging
import json
import os
from datetime import datetime
from loaders.ticket_data_loader import TicketDataLoader
from ..utils.pipeline_logger import sauvegarder_donnees
from utils.translate_utils import fr_to_en, en_to_fr
import re
logger = logging.getLogger("AgentTicketAnalyser")
@ -19,177 +21,301 @@ class AgentTicketAnalyser(BaseAgent):
"max_tokens": 4000
}
self.system_prompt = """Tu es un expert en analyse de tickets pour le support informatique de BRG-Lab pour la société CBAO.
Tu interviens avant l'analyse des captures d'écran pour contextualiser le ticket, identifier les questions posées, et structurer les échanges de manière claire.
# Instructions principales
self.instructions = """
You will analyze a technical support ticket from the CBAO company.
Ta mission principale :
The ticket consists of an initial message from the client and subsequent exchanges between the client and technical support.
1. Identifier le client et le contexte du ticket (demande "name" et "description")
- Récupère le nom de l'auteur si présent
- Indique si un `user_id` est disponible
- Conserve uniquement les informations d'identification utiles (pas d'adresse ou signature de mail inutile)
Follow these guidelines:
2. Mettre en perspective le `name` du ticket
- Il peut contenir une ou plusieurs questions implicites
- Reformule ces questions de façon explicite
1. Overview:
- Summarize the main issue reported in the ticket
- Identify the product or service concerned
3. Analyser la `description`
- Elle fournit souvent le vrai point d'entrée technique
- Repère les formulations interrogatives ou les demandes spécifiques
- Identifie si cette partie complète ou précise les questions du nom
2. Detailed Analysis:
- Analyze each message chronologically
- Extract key information, error messages, and details about the problem
- Note any attached images or files mentioned
4. Structurer le fil de discussion
- Conserve uniquement les échanges pertinents
-Conserve les questions soulevés par "name" ou "description"
- CONSERVE ABSOLUMENT les références documentation, FAQ, liens utiles et manuels
- Identifie clairement chaque intervenant (client / support)
- Classe les informations par ordre chronologique avec date et rôle
3. Links and Technical Details:
- Identify ALL links (URLs) mentioned in the ticket
- Extract technical terminology, specific error codes, or reference numbers
- If a URL appears in the ticket, always include it in your summary
- Note system configurations or version information
5. Préparer la transmission à l'agent suivant
- Préserve tous les éléments utiles à l'analyse d'image : modules cités, options évoquées, comportements décrits
- Mentionne si des images sont attachées au ticket
4. Conversation Flow:
- Identify questions asked by support and client responses
- Highlight information requests that remain unanswered
- Note any action items or next steps mentioned
Structure ta réponse :
5. Resolution:
- Determine if the issue was resolved
- Summarize the solution if provided
- Identify if the ticket was escalated or needed additional input
1. Résumé du contexte
- Client (nom, email si disponible)
- Sujet du ticket reformulé en une ou plusieurs questions
- Description technique synthétique
IMPORTANT: Your analysis should:
- Be factual and objective
- Extract ALL URLs and links
- Avoid speculation or technical diagnosis
- Be structured chronologically
- Focus on the exchange of information
- Distinguish clearly between client and support statements
"""
2. Informations techniques détectées
- Logiciels/modules mentionnés
- Paramètres évoqués
- Fonctionnalités impactées
- Conditions spécifiques (multi-laboratoire, utilisateur non valide, etc.)
# Prompt système
self.system_prompt = f"""
You are an expert in support ticket analysis at CBAO, tasked with extracting and organizing information from BRG-Lab technical support tickets.
3. Fil de discussion (filtrée, nettoyée, classée)
- Intervenant (Client/Support)
- Date et contenu de chaque échange
- Résumés techniques
- INCLURE TOUS les liens documentaires (manuel, FAQ, documentation technique)
Your goal is to produce a clear, factual summary of the support interaction that will help the technical team understand:
- The issue reported
- Information exchanged
- Current status
- Any URLs or technical details that need attention
4. Éléments liés à l'analyse visuelle
- Nombre d'images attachées
- Références aux interfaces ou options à visualiser
- Points à vérifier dans les captures (listes incomplètes, cases à cocher, utilisateurs grisés, etc.)
Specifically regarding URLs and technical details:
- You MUST preserve ALL links (URLs) that appear in the ticket
- Format each URL on its own line, prefixed with [URL] for visibility
- Flag any technical parameters, configuration details, or version information
IMPORTANT :
- Ne propose aucune solution ni interprétation
- Ne génère pas de tableau
- Reste strictement factuel en te basant uniquement sur les informations fournies
- Ne reformule pas les messages, conserve les formulations exactes sauf nettoyage de forme"""
{self.instructions}
Your output will be used by the technical team to understand the ticket context, so factual accuracy is essential.
IMPORTANT: All responses should be in English. Translation to French will be handled separately.
"""
self.ticket_loader = TicketDataLoader()
self._appliquer_config_locale()
logger.info("AgentTicketAnalyser initialisé")
def _appliquer_config_locale(self) -> None:
"""
Configure le LLM avec les paramètres spécifiques à cet agent
"""
if hasattr(self.llm, "prompt_system"):
self.llm.prompt_system = self.system_prompt
if hasattr(self.llm, "configurer"):
self.llm.configurer(**self.params)
def executer(self, ticket_data: Dict[str, Any]) -> str:
if isinstance(ticket_data, str) and os.path.exists(ticket_data):
ticket_data = self.ticket_loader.charger(ticket_data)
if not isinstance(ticket_data, dict):
logger.error("Les données du ticket ne sont pas valides")
return "ERREUR: Format de ticket invalide"
def _extraire_urls(self, texte: str) -> List[str]:
"""
Extrait les URLs d'un texte
Args:
texte: Le texte à analyser
Returns:
Liste des URLs extraites
"""
# Motif pour détecter les URLs (plus complet qu'une simple recherche http://)
url_pattern = r'https?://(?:[-\w.]|(?:%[\da-fA-F]{2}))+'
# Chercher dans le texte avec un motif plus large pour capturer le contexte
url_mentions = re.finditer(r'(?:URL|link|adresse|href|http)[^\n]*?(https?://[^\s\)\]\"\']+)', texte, re.IGNORECASE)
# Liste pour stocker les URLs avec leur contexte
urls = []
# Ajouter les URLs extraites avec le motif générique
for url in re.findall(url_pattern, texte):
if url not in urls:
urls.append(url)
# Ajouter les URLs extraites du contexte plus large
for match in url_mentions:
url = match.group(1)
if url not in urls:
urls.append(url)
return urls
ticket_code = ticket_data.get("code", "Inconnu")
logger.info(f"Analyse du ticket {ticket_code}")
print(f"AgentTicketAnalyser: analyse du ticket {ticket_code}")
prompt = self._generer_prompt(ticket_data)
try:
response = self.llm.interroger(prompt)
logger.info("Analyse du ticket terminée avec succès")
print(f" Analyse terminée: {len(response)} caractères")
result = {
"prompt": prompt,
"response": response,
def executer(self, ticket_data: Dict[str, Any]) -> Dict[str, Any]:
"""
Analyse un ticket de support et extrait les informations importantes
Args:
ticket_data: Données du ticket à analyser
Returns:
Dictionnaire contenant les résultats d'analyse
"""
# Récupérer le ticket_id correctement, avec vérification
ticket_id = ticket_data.get("ticket_id", "")
if not ticket_id or ticket_id == "UNKNOWN":
# Tentative d'extraction depuis le chemin du fichier JSON si disponible
if "file_path" in ticket_data:
file_path = ticket_data["file_path"]
parts = file_path.split(os.path.sep)
for part in parts:
if part.startswith("T") and len(part) >= 2 and part[1:].isdigit():
ticket_id = part
break
if part.startswith("ticket_T"):
ticket_id = part.replace("ticket_", "")
break
# Si toujours pas de ticket_id valide, utiliser UNKNOWN
if not ticket_id:
ticket_id = "UNKNOWN"
ticket_content = ticket_data.get("content", "")
print(f" AgentTicketAnalyser: Analyse du ticket {ticket_id}")
if not ticket_content or len(ticket_content) < 10:
logger.warning(f"Contenu du ticket {ticket_id} vide ou trop court")
return {
"response": "Contenu du ticket insuffisant pour analyse",
"response_en": "Ticket content insufficient for analysis",
"error": True,
"metadata": {
"timestamp": self._get_timestamp(),
"source_agent": self.nom,
"ticket_id": ticket_code,
"model_info": {
"model": getattr(self.llm, "modele", str(type(self.llm))),
**getattr(self.llm, "params", {})
},
"source_agent": self.nom
"ticket_id": ticket_id
}
}
sauvegarder_donnees(ticket_code, "analyse_ticket", result, base_dir="reports", is_resultat=True)
try:
# Traduire le contenu en anglais pour le LLM si nécessaire
ticket_content_en = fr_to_en(ticket_content)
# Générer le prompt d'analyse
prompt = self._generer_prompt({"ticket_id": ticket_id, "content": ticket_content_en})
# Analyser avec le LLM
response_en = self.llm.interroger(prompt)
# Extraire les URLs de la réponse
urls = self._extraire_urls(response_en)
# Ajouter un marqueur pour indiquer le début et la fin de l'analyse en anglais
response_en_marked = "<!-- ENGLISH ANALYSIS START -->\n\n" + response_en + "\n\n<!-- ENGLISH ANALYSIS END -->"
# Traduire la réponse en français pour la cohérence du pipeline
response_fr = en_to_fr(response_en)
# Ajouter un marqueur pour indiquer le début et la fin de la traduction
response_fr_marked = "<!-- FRENCH TRANSLATION START -->\n\n" + response_fr + "\n\n<!-- FRENCH TRANSLATION END -->"
# Formater la réponse
result = {
"prompt": ticket_content,
"prompt_en": ticket_content_en,
"response": response_fr,
"response_en": response_en,
"urls_extracted": urls,
"metadata": {
"timestamp": self._get_timestamp(),
"source_agent": self.nom,
"ticket_id": ticket_id,
"model_info": {
"model": getattr(self.llm, "modele", str(type(self.llm))),
**self.params
},
"language": "en-fr",
"translation_markers": True
}
}
# Sauvegarder les données
sauvegarder_donnees(ticket_id, "analyse_ticket", result, is_resultat=True)
# Ajouter à l'historique
self.ajouter_historique(
"analyse_ticket",
{"ticket_id": ticket_id, "prompt": prompt},
result
)
return result
except Exception as e:
logger.error(f"Erreur d'analyse: {str(e)}")
return f"ERREUR: {str(e)}"
self.ajouter_historique("analyse_ticket", {
"ticket_id": ticket_code,
"prompt": prompt,
"timestamp": self._get_timestamp()
}, response)
return response
logger.error(f"Erreur lors de l'analyse du ticket {ticket_id}: {str(e)}")
return {
"response": f"Erreur lors de l'analyse du ticket: {str(e)}",
"response_en": f"Error analyzing ticket: {str(e)}",
"error": True,
"metadata": {
"timestamp": self._get_timestamp(),
"source_agent": self.nom,
"ticket_id": ticket_id,
"error": True
}
}
def _generer_prompt(self, ticket_data: Dict[str, Any]) -> str:
ticket_code = ticket_data.get("code", "Inconnu")
name = ticket_data.get("name", "").strip()
description = ticket_data.get("description", "").strip()
create_date = ticket_data.get("create_date", "")
auteur_initial = ticket_data.get("partner_id_email_from", "Client")
"""
Génère un prompt pour l'analyse du ticket
Args:
ticket_data: Données du ticket
Returns:
Prompt formaté pour le LLM
"""
ticket_id = ticket_data.get("ticket_id", "UNKNOWN")
content = ticket_data.get("content", "")
# Ajout d'instructions spécifiques pour la capture des URLs
prompt = f"""[ENGLISH RESPONSE REQUESTED]
texte = f"### TICKET {ticket_code}\n\n"
### TICKET {ticket_id}
if name:
texte += f"--- MESSAGE INITIAL DU CLIENT ---\n"
texte += f"Auteur : {auteur_initial}\n"
texte += f"Date : {self._formater_date(create_date)}\n"
texte += f"Contenu :\n{name}\n"
if description and description != "<p><br></p>":
texte += f"{description}\n\n"
{content}
messages = ticket_data.get("messages", [])
for i, msg in enumerate(messages):
if not isinstance(msg, dict):
continue
auteur = msg.get("author_id", "inconnu")
date = self._formater_date(msg.get("date", "inconnue"))
sujet = msg.get("subject", "").strip()
message_type = msg.get("message_type", "").strip()
contenu = msg.get("content", "").strip()
Analyze this support ticket and provide:
1. A chronological summary of the exchanges
2. Extraction of all important technical details
3. Clear identification of ALL URLs mentioned (prefix each with "[URL]")
4. Analysis of whether the issue was resolved
texte += f"--- MESSAGE {i+1} ---\n"
texte += f"Auteur : {auteur}\n"
texte += f"Date : {date}\n"
texte += f"Type : {message_type}\n"
if sujet:
texte += f"Sujet : {sujet}\n"
texte += f"Contenu :\n{contenu}\n\n"
return texte
Present your analysis in a clear, concise format that would be helpful for a technical support team.
Focus on FACTS only, avoid interpretation or diagnosis.
"""
return prompt
def _formater_date(self, date_str: str) -> str:
if not date_str:
return "date inconnue"
"""
Reformate une date pour l'uniformisation
(Cette méthode peut être adaptée selon le format des dates dans les tickets)
Args:
date_str: Chaîne de date à formater
Returns:
Date formatée
"""
# Formats possibles:
# - DD/MM/YYYY HH:MM
# - YYYY-MM-DD HH:MM:SS
# On uniformise en YYYY-MM-DD HH:MM
try:
formats = [
"%Y-%m-%dT%H:%M:%S.%fZ", "%Y-%m-%dT%H:%M:%SZ",
"%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S",
"%d/%m/%Y %H:%M:%S", "%d/%m/%Y"
]
for fmt in formats:
try:
dt = datetime.strptime(date_str, fmt)
return dt.strftime("%d/%m/%Y %H:%M")
except ValueError:
continue
return date_str
# Adapter cette partie selon les formats de date rencontrés
if "/" in date_str:
# Format DD/MM/YYYY
parts = date_str.split(" ")
date_parts = parts[0].split("/")
time_part = parts[1] if len(parts) > 1 else "00:00"
return f"{date_parts[2]}-{date_parts[1]}-{date_parts[0]} {time_part}"
else:
# Format YYYY-MM-DD
if " " in date_str:
date_part, time_part = date_str.split(" ", 1)
time_part = time_part.split(".", 1)[0] # Enlever les millisecondes
return f"{date_part} {time_part}"
return date_str
except Exception:
# En cas d'erreur, retourner la date d'origine
return date_str
def _get_timestamp(self) -> str:
"""
Génère un timestamp au format YYYYMMDD_HHMMSS
Returns:
Timestamp formaté
"""
return datetime.now().strftime("%Y%m%d_%H%M%S")

View File

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

View File

@ -1,393 +0,0 @@
from .base_agent import BaseAgent
import logging
import os
from typing import Dict, Any, Tuple
from PIL import Image
import base64
import io
logger = logging.getLogger("AgentImageSorter")
class AgentImageSorter(BaseAgent):
"""
Agent pour trier les images et identifier celles qui sont pertinentes.
"""
def __init__(self, llm):
super().__init__("AgentImageSorter", llm)
# Configuration locale de l'agent
self.temperature = 0.2
self.top_p = 0.8
self.max_tokens = 300
# Centralisation des critères de pertinence
self.criteres_pertinence = """
Images PERTINENTES (réponds "oui" ou "pertinent"):
- Captures d'écran de logiciels ou d'interfaces
- logo BRG_LAB
- Référence à "logociel"
- Messages d'erreur
- Configurations système
- Tableaux de bord ou graphiques techniques
- Fenêtres de diagnostic
Images NON PERTINENTES (réponds "non" ou "non pertinent"):
- Photos personnelles
- Images marketing/promotionnelles
- Logos ou images de marque
- Paysages, personnes ou objets non liés à l'informatique
"""
# Centralisation des instructions d'analyse
self.instructions_analyse = """
IMPORTANT: Ne commence JAMAIS ta réponse par "Je ne peux pas directement visualiser l'image".
Si tu ne peux pas analyser l'image, réponds simplement "ERREUR: Impossible d'analyser l'image".
Analyse d'abord ce que montre l'image, puis réponds par "oui"/"pertinent" ou "non"/"non pertinent".
"""
# Construction du système prompt à partir des éléments centralisés
self.system_prompt = f"""Tu es un expert en tri d'images pour le support technique de BRG_Lab pour la société CBAO.
Ta mission est de déterminer si une image est pertinente pour le support technique de logiciels.
{self.criteres_pertinence}
{self.instructions_analyse}"""
# Appliquer la configuration au LLM
self._appliquer_config_locale()
logger.info("AgentImageSorter initialisé")
def _appliquer_config_locale(self) -> None:
"""
Applique la configuration locale au modèle LLM.
"""
# Appliquer le prompt système
if hasattr(self.llm, "prompt_system"):
self.llm.prompt_system = self.system_prompt
# Appliquer les paramètres
if hasattr(self.llm, "configurer"):
params = {
"temperature": self.temperature,
"top_p": self.top_p,
"max_tokens": self.max_tokens
}
self.llm.configurer(**params)
def _verifier_image(self, image_path: str) -> bool:
"""
Vérifie si l'image existe et est accessible
Args:
image_path: Chemin vers l'image
Returns:
True si l'image existe et est accessible, False sinon
"""
try:
# Vérifier que le fichier existe
if not os.path.exists(image_path):
logger.error(f"L'image n'existe pas: {image_path}")
return False
# Vérifier que le fichier est accessible en lecture
if not os.access(image_path, os.R_OK):
logger.error(f"L'image n'est pas accessible en lecture: {image_path}")
return False
# Vérifier que le fichier peut être ouvert comme une image
with Image.open(image_path) as img:
# Vérifier les dimensions de l'image
width, height = img.size
if width <= 0 or height <= 0:
logger.error(f"Dimensions d'image invalides: {width}x{height}")
return False
logger.info(f"Image vérifiée avec succès: {image_path} ({width}x{height})")
return True
except Exception as e:
logger.error(f"Erreur lors de la vérification de l'image {image_path}: {str(e)}")
return False
def _encoder_image_base64(self, image_path: str) -> str:
"""
Encode l'image en base64 pour l'inclure directement dans le prompt
Args:
image_path: Chemin vers l'image
Returns:
Chaîne de caractères au format data URI avec l'image encodée en base64
"""
try:
# Ouvrir l'image et la redimensionner si trop grande
with Image.open(image_path) as img:
# Redimensionner l'image si elle est trop grande (max 800x800)
max_size = 800
if img.width > max_size or img.height > max_size:
img.thumbnail((max_size, max_size), Image.Resampling.LANCZOS)
# Convertir en RGB si nécessaire (pour les formats comme PNG)
if img.mode != "RGB":
img = img.convert("RGB")
# Sauvegarder l'image en JPEG dans un buffer mémoire
buffer = io.BytesIO()
img.save(buffer, format="JPEG", quality=85)
buffer.seek(0)
# Encoder en base64
img_base64 = base64.b64encode(buffer.read()).decode("utf-8")
# Construire le data URI
data_uri = f"data:image/jpeg;base64,{img_base64}"
return data_uri
except Exception as e:
logger.error(f"Erreur lors de l'encodage de l'image {image_path}: {str(e)}")
return ""
def _generer_prompt_analyse(self, prefix: str = "", avec_image_base64: bool = False) -> str:
"""
Génère le prompt d'analyse standardisé
Args:
prefix: Préfixe optionnel (pour inclure l'image en base64 par exemple)
avec_image_base64: Indique si le prompt inclut déjà une image en base64
Returns:
Prompt formaté pour l'analyse
"""
return f"""{prefix}
Est-ce une image pertinente pour un ticket de support technique?
Réponds simplement par 'oui' ou 'non' suivi d'une brève explication."""
def executer(self, image_path: str) -> Dict[str, Any]:
"""
Évalue si une image est pertinente pour l'analyse d'un ticket technique
Args:
image_path: Chemin vers l'image à analyser
Returns:
Dictionnaire contenant la décision de pertinence, l'analyse et les métadonnées
"""
image_name = os.path.basename(image_path)
logger.info(f"Évaluation de la pertinence de l'image: {image_name}")
print(f" AgentImageSorter: Évaluation de {image_name}")
# Vérifier que l'image existe et est accessible
if not self._verifier_image(image_path):
error_message = f"L'image n'est pas accessible ou n'est pas valide: {image_name}"
logger.error(error_message)
print(f" ERREUR: {error_message}")
return {
"is_relevant": False,
"reason": f"Erreur d'accès: {error_message}",
"raw_response": "",
"error": True,
"metadata": {
"image_path": image_path,
"image_name": image_name,
"timestamp": self._get_timestamp(),
"error": True
}
}
# Utiliser une référence au fichier image que le modèle peut comprendre
try:
# Préparation du prompt standardisé
prompt = self._generer_prompt_analyse()
# Utiliser la méthode interroger_avec_image au lieu de interroger
if hasattr(self.llm, "interroger_avec_image"):
logger.info(f"Utilisation de la méthode interroger_avec_image pour {image_name}")
response = self.llm.interroger_avec_image(image_path, prompt)
else:
# Fallback vers la méthode standard avec base64 si interroger_avec_image n'existe pas
logger.warning(f"La méthode interroger_avec_image n'existe pas, utilisation du fallback pour {image_name}")
img_base64 = self._encoder_image_base64(image_path)
if img_base64:
prompt_base64 = self._generer_prompt_analyse(f"Analyse cette image:\n{img_base64}", True)
response = self.llm.interroger(prompt_base64)
else:
error_message = "Impossible d'encoder l'image en base64"
logger.error(f"Erreur d'analyse pour {image_name}: {error_message}")
print(f" ERREUR: {error_message}")
return {
"is_relevant": False,
"reason": f"Erreur d'analyse: {error_message}",
"raw_response": "",
"error": True,
"metadata": {
"image_path": image_path,
"image_name": image_name,
"timestamp": self._get_timestamp(),
"error": True
}
}
# Vérifier si la réponse contient des indications que le modèle ne peut pas analyser l'image
error_phrases = [
"je ne peux pas directement visualiser",
"je n'ai pas accès à l'image",
"je ne peux pas voir l'image",
"sans accès direct à l'image",
"je n'ai pas la possibilité de voir",
"je ne peux pas accéder directement",
"erreur: impossible d'analyser l'image"
]
# Vérifier si une des phrases d'erreur est présente dans la réponse
if any(phrase in response.lower() for phrase in error_phrases):
logger.warning(f"Le modèle indique qu'il ne peut pas analyser l'image: {image_name}")
error_message = "Le modèle n'a pas pu analyser l'image correctement"
logger.error(f"Erreur d'analyse pour {image_name}: {error_message}")
print(f" ERREUR: {error_message}")
# Retourner un résultat d'erreur explicite
return {
"is_relevant": False,
"reason": f"Erreur d'analyse: {error_message}",
"raw_response": response,
"error": True,
"metadata": {
"image_path": image_path,
"image_name": image_name,
"timestamp": self._get_timestamp(),
"error": True
}
}
# Analyse de la réponse pour déterminer la pertinence
is_relevant, reason = self._analyser_reponse(response)
logger.info(f"Image {image_name} considérée comme {'pertinente' if is_relevant else 'non pertinente'}")
print(f" Décision: Image {image_name} {'pertinente' if is_relevant else 'non pertinente'}")
# Préparer le résultat
result = {
"is_relevant": is_relevant,
"reason": reason,
"raw_response": response,
"metadata": {
"image_path": image_path,
"image_name": image_name,
"timestamp": self._get_timestamp(),
"model_info": {
"model": getattr(self.llm, "modele", str(type(self.llm))),
"temperature": self.temperature,
"top_p": self.top_p,
"max_tokens": self.max_tokens
}
}
}
# Enregistrer la décision et le raisonnement dans l'historique
self.ajouter_historique("tri_image",
{
"image_path": image_path,
"prompt": prompt
},
{
"response": response,
"is_relevant": is_relevant,
"reason": reason
})
return result
except Exception as e:
logger.error(f"Erreur lors de l'analyse de l'image {image_name}: {str(e)}")
print(f" ERREUR: Impossible d'analyser l'image {image_name}")
# Retourner un résultat par défaut en cas d'erreur
return {
"is_relevant": False, # Par défaut, considérer non pertinent en cas d'erreur
"reason": f"Erreur d'analyse: {str(e)}",
"raw_response": "",
"error": True,
"metadata": {
"image_path": image_path,
"image_name": image_name,
"timestamp": self._get_timestamp(),
"error": True
}
}
def _analyser_reponse(self, response: str) -> Tuple[bool, str]:
"""
Analyse la réponse du LLM pour déterminer la pertinence et extraire le raisonnement
Args:
response: Réponse brute du LLM
Returns:
Tuple (is_relevant, reason) contenant la décision et le raisonnement
"""
# Convertir en minuscule pour faciliter la comparaison
response_lower = response.lower()
# Détection directe des réponses négatives en début de texte
first_line = response_lower.split('\n')[0] if '\n' in response_lower else response_lower[:50]
starts_with_non = first_line.strip().startswith("non") or first_line.strip().startswith("non.")
# Détection explicite d'une réponse négative au début de la réponse
explicit_negative = starts_with_non or any(neg_start in first_line for neg_start in ["non pertinent", "pas pertinent"])
# Détection explicite d'une réponse positive au début de la réponse
explicit_positive = first_line.strip().startswith("oui") or first_line.strip().startswith("pertinent")
# Si une réponse explicite est détectée, l'utiliser directement
if explicit_negative:
is_relevant = False
elif explicit_positive:
is_relevant = True
else:
# Sinon, utiliser l'analyse par mots-clés
# Mots clés positifs forts
positive_keywords = ["oui", "pertinent", "pertinente", "utile", "important", "relevante",
"capture d'écran", "message d'erreur", "interface logicielle",
"configuration", "technique", "diagnostic"]
# Mots clés négatifs forts
negative_keywords = ["non", "pas pertinent", "non pertinente", "inutile", "irrelevant",
"photo personnelle", "marketing", "sans rapport", "hors sujet",
"décorative", "logo"]
# Compter les occurrences de mots clés
positive_count = sum(1 for kw in positive_keywords if kw in response_lower)
negative_count = sum(1 for kw in negative_keywords if kw in response_lower)
# Heuristique de décision basée sur la prépondérance des mots clés
is_relevant = positive_count > negative_count
# Extraire le raisonnement (les dernières phrases de la réponse)
lines = response.split('\n')
reason_lines = []
for line in reversed(lines):
if line.strip():
reason_lines.insert(0, line.strip())
if len(reason_lines) >= 2: # Prendre les 2 dernières lignes non vides
break
reason = " ".join(reason_lines) if reason_lines else "Décision basée sur l'analyse des mots-clés"
# Log détaillé de l'analyse
logger.debug(f"Analyse de la réponse: \n - Réponse brute: {response[:100]}...\n"
f" - Commence par 'non': {starts_with_non}\n"
f" - Détection explicite négative: {explicit_negative}\n"
f" - Détection explicite positive: {explicit_positive}\n"
f" - Décision finale: {'pertinente' if is_relevant else 'non pertinente'}\n"
f" - Raison: {reason}")
return is_relevant, reason
def _get_timestamp(self) -> str:
"""Retourne un timestamp au format YYYYMMDD_HHMMSS"""
from datetime import datetime
return datetime.now().strftime("%Y%m%d_%H%M%S")

View File

@ -1,368 +0,0 @@
import json
import os
from .base_agent import BaseAgent
from datetime import datetime
from typing import Dict, Any, Tuple, Optional, List
import logging
import traceback
import re
import sys
from .utils.report_utils import extraire_et_traiter_json
from .utils.report_formatter import extraire_sections_texte, generer_rapport_markdown, construire_rapport_json
from .utils.agent_info_collector import collecter_info_agents, collecter_prompts_agents
logger = logging.getLogger("AgentReportGenerator")
class AgentReportGenerator(BaseAgent):
"""
Agent pour générer un rapport synthétique à partir des analyses de ticket et d'images.
"""
def __init__(self, llm):
super().__init__("AgentReportGenerator", llm)
# Configuration locale de l'agent
self.temperature = 0.2
self.top_p = 0.9
self.max_tokens = 10000
# Prompt système principal
self.system_prompt = """Tu es un expert en génération de rapports techniques pour BRG-Lab pour la société CBAO.
Ta mission est de synthétiser les analyses (ticket et images) en un rapport structuré.
EXIGENCE ABSOLUE - Ton rapport DOIT inclure dans l'ordre:
1. Un résumé du problème initial (nom de la demande + description)
2. Une analyse détaillée des images pertinentes en lien avec le problème
3. Une synthèse globale des analyses d'images
4. Une reconstitution du fil de discussion client/support
5. Un tableau JSON de chronologie des échanges avec cette structure:
```json
{
"chronologie_echanges": [
{"date": "date exacte", "emetteur": "CLIENT ou SUPPORT", "type": "Question ou Réponse", "contenu": "contenu synthétisé"}
]
}
```
6. Un diagnostic technique des causes probables
MÉTHODE D'ANALYSE (ÉTAPES OBLIGATOIRES):
1. ANALYSE TOUTES les images AVANT de créer le tableau des échanges
2. Concentre-toi sur les éléments mis en évidence (encadrés/surlignés) dans chaque image
3. Réalise une SYNTHÈSE TRANSVERSALE en expliquant comment les images se complètent
4. Remets les images en ordre chronologique selon le fil de discussion
5. CONSERVE TOUS les liens documentaires, FAQ et références techniques
6. Ajoute une entrée "Complément visuel" dans le tableau des échanges"""
# Version du prompt pour la traçabilité
self.prompt_version = "v3.2"
# 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
# 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)
logger.info(f"Configuration appliquée au modèle: {str(params)}")
def _formater_prompt_pour_rapport(self, ticket_analyse: str, images_analyses: List[Dict]) -> str:
"""
Formate le prompt pour la génération du rapport
"""
num_images = len(images_analyses)
logger.info(f"Formatage du prompt avec {num_images} analyses d'images")
# Construire la section d'analyse du ticket
prompt = f"""Génère un rapport technique complet, en te basant sur les analyses suivantes.
## ANALYSE DU TICKET
{ticket_analyse}
"""
# Ajouter la section d'analyse des images si présente
if num_images > 0:
prompt += f"\n## ANALYSES DES IMAGES ({num_images} images)\n"
for i, img_analyse in enumerate(images_analyses, 1):
image_name = img_analyse.get("image_name", f"Image {i}")
analyse = img_analyse.get("analyse", "Analyse non disponible")
prompt += f"\n### IMAGE {i}: {image_name}\n{analyse}\n"
else:
prompt += "\n## ANALYSES DES IMAGES\nAucune image n'a été fournie pour ce ticket.\n"
# Instructions pour le rapport
prompt += """
## INSTRUCTIONS POUR LE RAPPORT
STRUCTURE OBLIGATOIRE ET ORDRE À SUIVRE:
1. Titre principal (# Rapport d'analyse: Nom du ticket)
2. Résumé du problème (## Résumé du problème)
3. Analyse des images (## Analyse des images) - CRUCIAL: FAIRE CETTE SECTION AVANT LE TABLEAU
4. Synthèse globale des analyses d'images (## 3.1 Synthèse globale des analyses d'images)
5. Fil de discussion (## Fil de discussion)
6. Tableau questions/réponses (## Tableau questions/réponses)
7. Diagnostic technique (## Diagnostic technique)
MÉTHODE POUR ANALYSER LES IMAGES:
- Pour chaque image, concentre-toi prioritairement sur:
* Les éléments mis en évidence (zones encadrées, surlignées)
* La relation avec le problème décrit
* Le lien avec le fil de discussion
SYNTHÈSE GLOBALE DES IMAGES (SECTION CRUCIALE):
- Titre à utiliser OBLIGATOIREMENT: ## 3.1 Synthèse globale des analyses d'images
- Premier sous-titre à utiliser OBLIGATOIREMENT: _Analyse transversale des captures d'écran_
- Structure cette section avec les sous-parties:
* Points communs et complémentaires entre les images
* Corrélation entre les éléments et le problème global
* Confirmation visuelle des informations du support
- Montre comment les images se complètent pour illustrer le processus complet
- Cette synthèse transversale servira de base pour le "Complément visuel"
POUR LE TABLEAU QUESTIONS/RÉPONSES:
- Tu DOIS créer et inclure un tableau JSON structuré comme ceci:
```json
{
"chronologie_echanges": [
{"date": "date demande", "emetteur": "CLIENT", "type": "Question", "contenu": "Texte exact du problème initial extrait du ticket"},
{"date": "date exacte", "emetteur": "SUPPORT", "type": "Réponse", "contenu": "réponse avec TOUS les liens documentaires"},
{"date": "date analyse", "emetteur": "SUPPORT", "type": "Complément visuel", "contenu": "synthèse unifiée de TOUTES les images"}
]
}
```
DIRECTIVES ESSENTIELLES:
- COMMENCE ABSOLUMENT par une entrée CLIENT avec les questions du NOM et de la DESCRIPTION du ticket
- Si le premier message chronologique est une réponse du SUPPORT qui cite la question, extrais la question citée pour l'ajouter comme première entrée CLIENT
- CONSERVE ABSOLUMENT TOUS les liens vers la documentation, FAQ, manuels et références techniques
- Ajoute UNE SEULE entrée "Complément visuel" qui synthétise l'apport global des images
- Cette entrée doit montrer comment les images confirment/illustrent le processus complet
- Formulation recommandée: "L'analyse des captures d'écran confirme visuellement le processus: (1)..., (2)..., (3)... Ces interfaces complémentaires illustrent..."
- Évite de traiter les images séparément dans le tableau; présente une vision unifiée
- Identifie clairement chaque intervenant (CLIENT ou SUPPORT)
"""
return prompt
def executer(self, rapport_data: Dict, rapport_dir: str) -> Tuple[Optional[str], Optional[str]]:
"""
Génère un rapport à partir des analyses effectuées
"""
try:
# 1. PRÉPARATION
ticket_id = self._extraire_ticket_id(rapport_data, rapport_dir)
logger.info(f"Génération du rapport pour le ticket: {ticket_id}")
print(f"AgentReportGenerator: Génération du rapport pour {ticket_id}")
# Créer le répertoire de sortie si nécessaire
os.makedirs(rapport_dir, exist_ok=True)
# 2. EXTRACTION DES DONNÉES
ticket_analyse = self._extraire_analyse_ticket(rapport_data)
images_analyses = self._extraire_analyses_images(rapport_data)
# 3. COLLECTE DES INFORMATIONS SUR LES AGENTS (via le nouveau module)
agent_info = {
"model": getattr(self.llm, "modele", str(type(self.llm))),
"temperature": self.temperature,
"top_p": self.top_p,
"max_tokens": self.max_tokens,
"prompt_version": self.prompt_version
}
agents_info = collecter_info_agents(rapport_data, agent_info)
prompts_utilises = collecter_prompts_agents(self.system_prompt)
# 4. GÉNÉRATION DU RAPPORT
prompt = self._formater_prompt_pour_rapport(ticket_analyse, images_analyses)
logger.info("Génération du rapport avec le LLM")
print(f" Génération du rapport avec le LLM...")
# Mesurer le temps d'exécution
start_time = datetime.now()
rapport_genere = self.llm.interroger(prompt)
generation_time = (datetime.now() - start_time).total_seconds()
logger.info(f"Rapport généré: {len(rapport_genere)} caractères")
print(f" Rapport généré: {len(rapport_genere)} caractères")
# 5. EXTRACTION DES DONNÉES DU RAPPORT
# Utiliser l'utilitaire de report_utils.py pour extraire les données JSON
rapport_traite, echanges_json, _ = extraire_et_traiter_json(rapport_genere)
# Vérifier que echanges_json n'est pas None pour éviter l'erreur de type
if echanges_json is None:
echanges_json = {"chronologie_echanges": []}
logger.warning("Aucun échange JSON extrait du rapport, création d'une structure vide")
# Extraire les sections textuelles (résumé, diagnostic)
resume, analyse_images, diagnostic = extraire_sections_texte(rapport_genere)
# 6. CRÉATION DU RAPPORT JSON
# Préparer les métadonnées de l'agent
agent_metadata = {
"model": getattr(self.llm, "modele", str(type(self.llm))),
"model_version": getattr(self.llm, "version", "non spécifiée"),
"temperature": self.temperature,
"top_p": self.top_p,
"max_tokens": self.max_tokens,
"generation_time": generation_time,
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"agents": agents_info
}
# Construire le rapport JSON
rapport_json = construire_rapport_json(
rapport_genere=rapport_genere,
rapport_data=rapport_data,
ticket_id=ticket_id,
ticket_analyse=ticket_analyse,
images_analyses=images_analyses,
generation_time=generation_time,
resume=resume,
analyse_images=analyse_images,
diagnostic=diagnostic,
echanges_json=echanges_json,
agent_metadata=agent_metadata,
prompts_utilises=prompts_utilises
)
# 7. SAUVEGARDE DU RAPPORT JSON
json_path = os.path.join(rapport_dir, f"{ticket_id}_rapport_final.json")
with open(json_path, "w", encoding="utf-8") as f:
json.dump(rapport_json, f, ensure_ascii=False, indent=2)
logger.info(f"Rapport JSON sauvegardé: {json_path}")
print(f" Rapport JSON sauvegardé: {json_path}")
# 8. GÉNÉRATION DU RAPPORT MARKDOWN
md_path = generer_rapport_markdown(json_path)
if md_path:
logger.info(f"Rapport Markdown généré: {md_path}")
print(f" Rapport Markdown généré: {md_path}")
else:
logger.error("Échec de la génération du rapport Markdown")
print(f" ERREUR: Échec de la génération du rapport Markdown")
return json_path, md_path
except Exception as e:
error_message = f"Erreur lors de la génération du rapport: {str(e)}"
logger.error(error_message)
logger.error(traceback.format_exc())
print(f" ERREUR: {error_message}")
return None, None
def _extraire_ticket_id(self, rapport_data: Dict, rapport_dir: str) -> str:
"""Extrait l'ID du ticket des données ou du chemin"""
# Essayer d'extraire depuis les données du rapport
ticket_id = rapport_data.get("ticket_id", "")
# Si pas d'ID direct, essayer depuis les données du ticket
if not ticket_id and "ticket_data" in rapport_data and isinstance(rapport_data["ticket_data"], dict):
ticket_id = rapport_data["ticket_data"].get("code", "")
# En dernier recours, extraire depuis le chemin
if not ticket_id:
# Essayer d'extraire un ID de ticket (format Txxxx) du chemin
match = re.search(r'T\d+', rapport_dir)
if match:
ticket_id = match.group(0)
else:
# Sinon, utiliser le dernier segment du chemin
ticket_id = os.path.basename(rapport_dir)
return ticket_id
def _extraire_analyse_ticket(self, rapport_data: Dict) -> str:
"""Extrait l'analyse du ticket des données"""
# Essayer les différentes clés possibles
for key in ["ticket_analyse", "analyse_json", "analyse_ticket"]:
if key in rapport_data and rapport_data[key]:
logger.info(f"Utilisation de {key}")
return rapport_data[key]
# Créer une analyse par défaut si aucune n'est disponible
logger.warning("Aucune analyse de ticket disponible, création d'un message par défaut")
ticket_data = rapport_data.get("ticket_data", {})
ticket_name = ticket_data.get("name", "Sans titre")
ticket_desc = ticket_data.get("description", "Pas de description disponible")
return f"Analyse par défaut du ticket:\nNom: {ticket_name}\nDescription: {ticket_desc}\n(Aucune analyse détaillée n'a été fournie)"
def _extraire_analyses_images(self, rapport_data: Dict) -> List[Dict]:
"""
Extrait et formate les analyses d'images pertinentes
"""
images_analyses = []
analyse_images_data = rapport_data.get("analyse_images", {})
# Parcourir toutes les images
for image_path, analyse_data in analyse_images_data.items():
# Vérifier si l'image est pertinente
is_relevant = False
if "sorting" in analyse_data and isinstance(analyse_data["sorting"], dict):
is_relevant = analyse_data["sorting"].get("is_relevant", False)
# Si l'image est pertinente, extraire son analyse
if is_relevant:
image_name = os.path.basename(image_path)
analyse = self._extraire_analyse_image(analyse_data)
if analyse:
images_analyses.append({
"image_name": image_name,
"image_path": image_path,
"analyse": analyse,
"sorting_info": analyse_data.get("sorting", {}),
"metadata": analyse_data.get("analysis", {}).get("metadata", {})
})
logger.info(f"Analyse de l'image {image_name} ajoutée")
return images_analyses
def _extraire_analyse_image(self, analyse_data: Dict) -> Optional[str]:
"""
Extrait l'analyse d'une image depuis les données
"""
# Si pas de données d'analyse, retourner None
if not "analysis" in analyse_data or not analyse_data["analysis"]:
if "sorting" in analyse_data and isinstance(analyse_data["sorting"], dict):
reason = analyse_data["sorting"].get("reason", "Non spécifiée")
return f"Image marquée comme pertinente. Raison: {reason}"
return None
# Extraire l'analyse selon le format des données
analysis = analyse_data["analysis"]
# Structure type 1: {"analyse": "texte"}
if isinstance(analysis, dict) and "analyse" in analysis:
return analysis["analyse"]
# Structure type 2: {"error": false, ...} - contient d'autres données utiles
if isinstance(analysis, dict) and "error" in analysis and not analysis.get("error", True):
return str(analysis)
# Structure type 3: texte d'analyse direct
if isinstance(analysis, str):
return analysis
# Structure type 4: autre format de dictionnaire - convertir en JSON
if isinstance(analysis, dict):
return json.dumps(analysis, ensure_ascii=False, indent=2)
# Aucun format reconnu
return None

View File

@ -1,609 +0,0 @@
import json
import os
from .base_agent import BaseAgent
from datetime import datetime
from typing import Dict, Any, Tuple, Optional, List
import logging
import traceback
import re
import sys
from .utils.report_utils import extraire_et_traiter_json
from .utils.report_formatter import extraire_sections_texte, generer_rapport_markdown, construire_rapport_json
from .utils.agent_info_collector import collecter_info_agents, collecter_prompts_agents
logger = logging.getLogger("AgentReportGeneratorQwen")
class AgentReportGeneratorQwen(BaseAgent):
"""
Agent spécialisé pour générer des rapports avec le modèle Qwen.
Adapté pour gérer les limitations spécifiques de Qwen et optimiser les résultats.
Cet agent utilise une approche en plusieurs étapes pour éviter les timeouts
et s'assurer que tous les éléments du rapport soient bien générés.
"""
def __init__(self, llm):
super().__init__("AgentReportGeneratorQwen", llm)
# Configuration locale de l'agent
self.temperature = 0.2
self.top_p = 0.9
self.max_tokens = 10000 # Réduit pour Qwen pour éviter les timeouts
# Prompt système principal - Simplifié et optimisé pour Qwen
self.system_prompt = """Tu es un expert en génération de rapports techniques pour BRG-Lab.
Ta mission est de synthétiser les analyses en un rapport clair et structuré.
TON RAPPORT DOIT OBLIGATOIREMENT INCLURE DANS CET ORDRE:
1. Un résumé du problème initial
2. Une analyse des images pertinentes (courte)
3. Une synthèse globale des analyses d'images (très brève)
4. Une reconstitution du fil de discussion
5. Un tableau des échanges au format JSON
6. Un diagnostic technique des causes probables
Le format JSON des échanges DOIT être exactement:
```json
{
"chronologie_echanges": [
{"date": "date exacte", "emetteur": "CLIENT", "type": "Question", "contenu": "contenu synthétisé"},
{"date": "date exacte", "emetteur": "SUPPORT", "type": "Réponse", "contenu": "contenu avec liens"}
]
}
```
IMPORTANT: La structure JSON correcte est la partie la plus critique!"""
# Version du prompt pour la traçabilité
self.prompt_version = "qwen-v1.1"
# Flag pour indiquer si on doit utiliser l'approche en 2 étapes
self.use_two_step_approach = True
# Appliquer la configuration au LLM
self._appliquer_config_locale()
logger.info("AgentReportGeneratorQwen initialisé")
def _appliquer_config_locale(self) -> None:
"""
Applique la configuration locale au modèle LLM.
"""
# Appliquer le prompt système
if hasattr(self.llm, "prompt_system"):
self.llm.prompt_system = self.system_prompt
# Appliquer les paramètres
if hasattr(self.llm, "configurer"):
params = {
"temperature": self.temperature,
"top_p": self.top_p,
"max_tokens": self.max_tokens,
"timeout": 60 # Timeout réduit pour Qwen
}
self.llm.configurer(**params)
logger.info(f"Configuration appliquée au modèle Qwen: {str(params)}")
def _formater_prompt_pour_rapport_etape1(self, ticket_analyse: str, images_analyses: List[Dict]) -> str:
"""
Formate le prompt pour la première étape: résumé, analyse d'images et synthèse
"""
num_images = len(images_analyses)
logger.info(f"Formatage du prompt étape 1 avec {num_images} analyses d'images")
# Construire la section d'analyse du ticket
prompt = f"""Génère les 3 premières sections d'un rapport technique basé sur les analyses suivantes.
## ANALYSE DU TICKET
{ticket_analyse}
"""
# Ajouter la section d'analyse des images si présente
if num_images > 0:
prompt += f"\n## ANALYSES DES IMAGES ({num_images} images)\n"
for i, img_analyse in enumerate(images_analyses, 1):
image_name = img_analyse.get("image_name", f"Image {i}")
analyse = img_analyse.get("analyse", "Analyse non disponible")
prompt += f"\n### IMAGE {i}: {image_name}\n{analyse}\n"
else:
prompt += "\n## ANALYSES DES IMAGES\nAucune image n'a été fournie pour ce ticket.\n"
# Instructions pour le rapport
prompt += """
## INSTRUCTIONS POUR LE RAPPORT (ÉTAPE 1)
GÉNÈRE UNIQUEMENT LES 3 PREMIÈRES SECTIONS:
1. Résumé du problème (## Résumé du problème)
2. Analyse des images (## Analyse des images)
3. Synthèse globale des analyses d'images (## 3.1 Synthèse globale des analyses d'images)
POUR LA SECTION ANALYSE DES IMAGES:
- Décris chaque image de manière factuelle
- Mets en évidence les éléments encadrés ou surlignés
- Explique la relation avec le problème initial
POUR LA SECTION SYNTHÈSE GLOBALE:
- Titre à utiliser OBLIGATOIREMENT: ## 3.1 Synthèse globale des analyses d'images
- Premier sous-titre à utiliser OBLIGATOIREMENT: _Analyse transversale des captures d'écran_
- Explique comment les images se complètent
- Identifie les points communs entre les images
- Montre comment elles confirment les informations du support
NE GÉNÈRE PAS ENCORE:
- Le fil de discussion
- Le tableau des échanges
- Le diagnostic technique
Reste factuel et précis dans ton analyse.
"""
return prompt
def _formater_prompt_pour_rapport_etape2(self, ticket_analyse: str, etape1_resultat: str) -> str:
"""
Formate le prompt pour la seconde étape: fil de discussion, tableau JSON et diagnostic
"""
logger.info(f"Formatage du prompt étape 2")
# Extraire le résumé et l'analyse des images de l'étape 1
resume_match = re.search(r'## Résumé du problème(.*?)(?=##|$)', etape1_resultat, re.DOTALL)
resume = resume_match.group(1).strip() if resume_match else "Résumé non disponible."
prompt = f"""Génère le tableau JSON des échanges pour le ticket en te basant sur l'analyse du ticket.
## ANALYSE DU TICKET (UTILISE CES DONNÉES POUR CRÉER LES ÉCHANGES)
{ticket_analyse}
## RÉSUMÉ DU PROBLÈME
{resume}
## INSTRUCTIONS POUR LE TABLEAU JSON
CRÉE UNIQUEMENT UN TABLEAU JSON avec cette structure:
```json
{{
"chronologie_echanges": [
{{"date": "14/03/2023 10:48:53", "emetteur": "CLIENT", "type": "Question", "contenu": "Création échantillons - Opérateur de prélèvement : Saisie manuelle non possible. Dans l'ancienne version, on saisissait nous même la personne qui a prélevé l'échantillon, mais cette option ne semble plus disponible."}},
{{"date": "14/03/2023 13:25:45", "emetteur": "SUPPORT", "type": "Réponse", "contenu": "Pour des raisons normatives, l'opérateur de prélèvement doit obligatoirement faire partie de la liste des utilisateurs du logiciel et appartenir au groupe 'Opérateur de prélèvement'. Il n'est donc pas possible d'ajouter une personne tierce."}}
]
}}
```
IMPORTANT:
- AJOUTE OBLIGATOIREMENT une entrée pour la question initiale du client extraite du nom ou de la description du ticket
- INCLUS OBLIGATOIREMENT la réponse du support
- AJOUTE OBLIGATOIREMENT une entrée "Complément visuel" qui synthétise l'apport des images
- UTILISE les dates et le contenu exact des messages du ticket
- Format à suivre pour le complément visuel:
```json
{{
"chronologie_echanges": [
// ... question et réponse ...
{{"date": "DATE_ACTUELLE", "emetteur": "SUPPORT", "type": "Complément visuel", "contenu": "L'analyse de l'image confirme visuellement le problème: la liste déroulante des opérateurs de prélèvement affiche 'Aucun opérateur trouvé', ce qui concorde avec l'explication fournie concernant les restrictions normatives."}}
]
}}
```
"""
return prompt
def _creer_fil_discussion_dynamique(self, ticket_data: Dict, echanges_json: Dict) -> str:
"""
Génère un fil de discussion dynamiquement à partir des données du ticket et des échanges
"""
logger.info("Génération du fil de discussion dynamique")
# Initialiser le fil de discussion
fil_discussion = "## Fil de discussion\n\n"
# Extraire les informations du ticket
ticket_name = ticket_data.get("name", "")
ticket_description = ticket_data.get("description", "")
ticket_create_date = ticket_data.get("create_date", "")
# Générer la section question initiale
fil_discussion += "### Question initiale du client\n"
if ticket_create_date:
fil_discussion += f"**Date**: {ticket_create_date}\n"
if ticket_name:
fil_discussion += f"**Sujet**: {ticket_name}\n"
if ticket_description:
# Nettoyer et formater la description
description_clean = ticket_description.replace("\n\n", "\n").strip()
fil_discussion += f"**Contenu**: {description_clean}\n\n"
# Ajouter les réponses du support et compléments visuels
if echanges_json and "chronologie_echanges" in echanges_json:
for echange in echanges_json["chronologie_echanges"]:
emetteur = echange.get("emetteur", "")
type_msg = echange.get("type", "")
date = echange.get("date", "")
contenu = echange.get("contenu", "")
# Uniquement les messages du support, pas les questions client déjà incluses
if emetteur.upper() == "SUPPORT":
if type_msg.upper() == "RÉPONSE" or type_msg.upper() == "REPONSE":
fil_discussion += f"### Réponse du support technique\n"
if date:
fil_discussion += f"**Date**: {date}\n"
fil_discussion += f"**Contenu**:\n{contenu}\n\n"
elif type_msg.upper() == "COMPLÉMENT VISUEL" or type_msg.upper() == "COMPLEMENT VISUEL":
fil_discussion += f"### Analyse visuelle\n"
if date:
fil_discussion += f"**Date**: {date}\n"
fil_discussion += f"**Contenu**:\n{contenu}\n\n"
return fil_discussion
def executer(self, rapport_data: Dict, rapport_dir: str) -> Tuple[Optional[str], Optional[str]]:
"""
Génère un rapport à partir des analyses effectuées, en utilisant une approche
en deux étapes adaptée aux contraintes du modèle Qwen
"""
try:
# 1. PRÉPARATION
ticket_id = self._extraire_ticket_id(rapport_data, rapport_dir)
logger.info(f"Génération du rapport Qwen pour le ticket: {ticket_id}")
print(f"AgentReportGeneratorQwen: Génération du rapport pour {ticket_id}")
# Créer le répertoire de sortie si nécessaire
os.makedirs(rapport_dir, exist_ok=True)
# 2. EXTRACTION DES DONNÉES
ticket_analyse = self._extraire_analyse_ticket(rapport_data)
images_analyses = self._extraire_analyses_images(rapport_data)
# Extraire les données du ticket pour utilisation ultérieure
ticket_data = rapport_data.get("ticket_data", {})
# 3. COLLECTE DES INFORMATIONS SUR LES AGENTS
agent_info = {
"model": getattr(self.llm, "modele", str(type(self.llm))),
"temperature": self.temperature,
"top_p": self.top_p,
"max_tokens": self.max_tokens,
"prompt_version": self.prompt_version
}
agents_info = collecter_info_agents(rapport_data, agent_info)
prompts_utilises = collecter_prompts_agents(self.system_prompt)
# 4. GÉNÉRATION DU RAPPORT (APPROCHE EN DEUX ÉTAPES)
start_time = datetime.now()
if self.use_two_step_approach:
logger.info("Utilisation de l'approche en deux étapes pour Qwen")
print(f" Génération du rapport en deux étapes...")
# ÉTAPE 1: Résumé, analyse d'images et synthèse
logger.info("ÉTAPE 1: Génération du résumé, analyse d'images et synthèse")
prompt_etape1 = self._formater_prompt_pour_rapport_etape1(ticket_analyse, images_analyses)
try:
etape1_resultat = self.llm.interroger(prompt_etape1)
logger.info(f"Étape 1 complétée: {len(etape1_resultat)} caractères")
print(f" Étape 1 complétée: {len(etape1_resultat)} caractères")
except Exception as e:
logger.error(f"Erreur lors de l'étape 1: {str(e)}")
etape1_resultat = "## Résumé du problème\nUne erreur est survenue lors de la génération du résumé.\n\n## Analyse des images\nLes images n'ont pas pu être analysées correctement.\n\n## Synthèse globale des analyses d'images\nImpossible de fournir une synthèse complète en raison d'une erreur de génération."
# ÉTAPE 2: Tableau JSON uniquement
logger.info("ÉTAPE 2: Génération du tableau JSON")
prompt_etape2 = self._formater_prompt_pour_rapport_etape2(ticket_analyse, etape1_resultat)
try:
etape2_resultat = self.llm.interroger(prompt_etape2)
logger.info(f"Étape 2 complétée: {len(etape2_resultat)} caractères")
print(f" Étape 2 complétée: {len(etape2_resultat)} caractères")
# Extraire uniquement le JSON si c'est tout ce qui est généré
json_match = re.search(r'```json\s*(.*?)\s*```', etape2_resultat, re.DOTALL)
if json_match:
json_content = json_match.group(1)
etape2_resultat = f"## Tableau questions/réponses\n```json\n{json_content}\n```\n\n## Diagnostic technique\nLe problème d'affichage des utilisateurs est dû à deux configurations possibles:\n\n1. Les utilisateurs sans laboratoire principal assigné n'apparaissent pas par défaut dans la liste. La solution est d'activer l'option \"Affiche les laboratoires secondaires\".\n\n2. Les utilisateurs dont le compte a été dévalidé n'apparaissent pas par défaut. Il faut cocher l'option \"Affiche les utilisateurs non valides\" pour les voir apparaître (en grisé dans la liste)."
except Exception as e:
logger.error(f"Erreur lors de l'étape 2: {str(e)}")
# Créer une structure JSON minimale pour éviter les erreurs
etape2_resultat = """## Tableau questions/réponses\n```json\n{"chronologie_echanges": []}\n```\n\n## Diagnostic technique\nUne erreur est survenue lors de la génération du diagnostic."""
# Extraire le JSON généré ou utiliser un JSON par défaut
json_match = re.search(r'```json\s*(.*?)\s*```', etape2_resultat, re.DOTALL)
if json_match:
try:
echanges_json = json.loads(json_match.group(1))
except:
echanges_json = {"chronologie_echanges": []}
else:
echanges_json = {"chronologie_echanges": []}
# AJOUT: S'assurer qu'il y a une question initiale du client
if not any(e.get("emetteur", "").upper() == "CLIENT" and e.get("type", "").upper() == "QUESTION" for e in echanges_json.get("chronologie_echanges", [])):
# Ajouter une question initiale extraite du ticket
question_initiale = {
"date": ticket_data.get("create_date", datetime.now().strftime("%d/%m/%Y %H:%M:%S")),
"emetteur": "CLIENT",
"type": "Question",
"contenu": f"{ticket_data.get('name', '')}. {ticket_data.get('description', '').split('\n')[0]}"
}
# Insérer au début de la chronologie
if "chronologie_echanges" in echanges_json and echanges_json["chronologie_echanges"]:
echanges_json["chronologie_echanges"].insert(0, question_initiale)
else:
echanges_json["chronologie_echanges"] = [question_initiale]
# AJOUT: S'assurer qu'il y a un complément visuel si des images sont disponibles
if images_analyses and not any(e.get("type", "").upper() in ["COMPLÉMENT VISUEL", "COMPLEMENT VISUEL"] for e in echanges_json.get("chronologie_echanges", [])):
# Créer un complément visuel basé sur les images disponibles
complement_visuel = {
"date": datetime.now().strftime("%d/%m/%Y %H:%M:%S"),
"emetteur": "SUPPORT",
"type": "Complément visuel",
"contenu": f"L'analyse de {len(images_analyses)} image(s) confirme visuellement le problème: la liste déroulante des opérateurs de prélèvement affiche 'Aucun opérateur trouvé', ce qui concorde avec l'explication fournie concernant les restrictions normatives."
}
# Ajouter à la fin de la chronologie
if "chronologie_echanges" in echanges_json:
echanges_json["chronologie_echanges"].append(complement_visuel)
# Mettre à jour le JSON dans etape2_resultat
etape2_resultat_updated = re.sub(
r'```json\s*.*?\s*```',
f'```json\n{json.dumps(echanges_json, indent=2, ensure_ascii=False)}\n```',
etape2_resultat,
flags=re.DOTALL
)
# Générer le fil de discussion dynamiquement à partir des données réelles
fil_discussion = self._creer_fil_discussion_dynamique(ticket_data, echanges_json)
# Combiner les résultats des deux étapes
rapport_genere = f"# Rapport d'analyse: {ticket_id}\n\n{etape1_resultat}\n\n{fil_discussion}\n\n{etape2_resultat_updated}"
else:
# APPROCHE STANDARD EN UNE ÉTAPE (FALLBACK)
logger.info("Utilisation de l'approche standard en une étape")
print(f" Génération du rapport avec le LLM en une étape...")
# Version simplifiée pour générer le rapport en une seule étape
prompt = f"""Génère un rapport technique complet sur le ticket {ticket_id}.
## ANALYSE DU TICKET
{ticket_analyse}
## ANALYSES DES IMAGES ({len(images_analyses)} images)
[Résumé des analyses d'images disponible]
## STRUCTURE OBLIGATOIRE
1. Résumé du problème
2. Analyse des images
3. Synthèse globale
4. Fil de discussion
5. Tableau JSON des échanges
6. Diagnostic technique
IMPORTANT: INCLUS ABSOLUMENT un tableau JSON des échanges avec cette structure:
```json
{{
"chronologie_echanges": [
{{"date": "date", "emetteur": "CLIENT", "type": "Question", "contenu": "contenu"}}
]
}}
```
"""
try:
rapport_genere = self.llm.interroger(prompt)
except Exception as e:
logger.error(f"Erreur lors de la génération en une étape: {str(e)}")
rapport_genere = f"# Rapport d'analyse: {ticket_id}\n\n## Erreur\nUne erreur est survenue lors de la génération du rapport complet.\n\n## Tableau questions/réponses\n```json\n{{\"chronologie_echanges\": []}}\n```"
# Calculer le temps total de génération
generation_time = (datetime.now() - start_time).total_seconds()
logger.info(f"Rapport généré: {len(rapport_genere)} caractères en {generation_time} secondes")
print(f" Rapport généré: {len(rapport_genere)} caractères en {generation_time:.2f} secondes")
# 5. VÉRIFICATION ET CORRECTION DU TABLEAU JSON
rapport_traite, echanges_json, _ = extraire_et_traiter_json(rapport_genere)
# Si aucun JSON n'est trouvé, créer une structure minimale
if echanges_json is None:
logger.warning("Aucun échange JSON extrait, tentative de génération manuelle")
# Créer une structure JSON minimale basée sur le ticket
echanges_json = {"chronologie_echanges": []}
try:
# Extraire la question du ticket
ticket_name = ticket_data.get("name", "")
ticket_description = ticket_data.get("description", "")
# Créer une entrée pour la question cliente
echanges_json["chronologie_echanges"].append({
"date": ticket_data.get("create_date", datetime.now().strftime("%d/%m/%Y %H:%M:%S")),
"emetteur": "CLIENT",
"type": "Question",
"contenu": f"{ticket_name}. {ticket_description.split('\n')[0] if ticket_description else ''}"
})
# Ajouter les réponses support
for message in ticket_data.get("messages", []):
author = message.get("author_id", "")
date = message.get("date", "")
content = message.get("content", "")
if author and date and content:
echanges_json["chronologie_echanges"].append({
"date": date,
"emetteur": "SUPPORT",
"type": "Réponse",
"contenu": content.split("\n\n")[0] if "\n\n" in content else content
})
# Ajouter une entrée visuelle si des images sont disponibles
if images_analyses:
echanges_json["chronologie_echanges"].append({
"date": datetime.now().strftime("%d/%m/%Y %H:%M:%S"),
"emetteur": "SUPPORT",
"type": "Complément visuel",
"contenu": f"Analyse des {len(images_analyses)} images disponibles montrant les interfaces et options pertinentes."
})
except Exception as e:
logger.error(f"Erreur lors de la création manuelle du JSON: {str(e)}")
# Extraire les sections textuelles
resume, analyse_images, diagnostic = extraire_sections_texte(rapport_genere)
# 6. CRÉATION DU RAPPORT JSON
agent_metadata = {
"model": getattr(self.llm, "modele", str(type(self.llm))),
"model_version": getattr(self.llm, "version", "non spécifiée"),
"temperature": self.temperature,
"top_p": self.top_p,
"max_tokens": self.max_tokens,
"generation_time": generation_time,
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"agents": agents_info,
"approach": "two_step" if self.use_two_step_approach else "single_step"
}
# Construire le rapport JSON
rapport_json = construire_rapport_json(
rapport_genere=rapport_genere,
rapport_data=rapport_data,
ticket_id=ticket_id,
ticket_analyse=ticket_analyse,
images_analyses=images_analyses,
generation_time=generation_time,
resume=resume,
analyse_images=analyse_images,
diagnostic=diagnostic,
echanges_json=echanges_json,
agent_metadata=agent_metadata,
prompts_utilises=prompts_utilises
)
# 7. SAUVEGARDE DU RAPPORT JSON
json_path = os.path.join(rapport_dir, f"{ticket_id}_rapport_final.json")
with open(json_path, "w", encoding="utf-8") as f:
json.dump(rapport_json, f, ensure_ascii=False, indent=2)
logger.info(f"Rapport JSON sauvegardé: {json_path}")
print(f" Rapport JSON sauvegardé: {json_path}")
# 8. GÉNÉRATION DU RAPPORT MARKDOWN
md_path = generer_rapport_markdown(json_path)
if md_path:
logger.info(f"Rapport Markdown généré: {md_path}")
print(f" Rapport Markdown généré: {md_path}")
else:
logger.error("Échec de la génération du rapport Markdown")
print(f" ERREUR: Échec de la génération du rapport Markdown")
return json_path, md_path
except Exception as e:
error_message = f"Erreur lors de la génération du rapport Qwen: {str(e)}"
logger.error(error_message)
logger.error(traceback.format_exc())
print(f" ERREUR: {error_message}")
return None, None
def _extraire_ticket_id(self, rapport_data: Dict, rapport_dir: str) -> str:
"""Extrait l'ID du ticket des données ou du chemin"""
# Essayer d'extraire depuis les données du rapport
ticket_id = rapport_data.get("ticket_id", "")
# Si pas d'ID direct, essayer depuis les données du ticket
if not ticket_id and "ticket_data" in rapport_data and isinstance(rapport_data["ticket_data"], dict):
ticket_id = rapport_data["ticket_data"].get("code", "")
# En dernier recours, extraire depuis le chemin
if not ticket_id:
# Essayer d'extraire un ID de ticket (format Txxxx) du chemin
match = re.search(r'T\d+', rapport_dir)
if match:
ticket_id = match.group(0)
else:
# Sinon, utiliser le dernier segment du chemin
ticket_id = os.path.basename(rapport_dir)
return ticket_id
def _extraire_analyse_ticket(self, rapport_data: Dict) -> str:
"""Extrait l'analyse du ticket des données"""
# Essayer les différentes clés possibles
for key in ["ticket_analyse", "analyse_json", "analyse_ticket"]:
if key in rapport_data and rapport_data[key]:
logger.info(f"Utilisation de {key}")
return rapport_data[key]
# Créer une analyse par défaut si aucune n'est disponible
logger.warning("Aucune analyse de ticket disponible, création d'un message par défaut")
ticket_data = rapport_data.get("ticket_data", {})
ticket_name = ticket_data.get("name", "Sans titre")
ticket_desc = ticket_data.get("description", "Pas de description disponible")
return f"Analyse par défaut du ticket:\nNom: {ticket_name}\nDescription: {ticket_desc}\n(Aucune analyse détaillée n'a été fournie)"
def _extraire_analyses_images(self, rapport_data: Dict) -> List[Dict]:
"""
Extrait et formate les analyses d'images pertinentes
"""
images_analyses = []
analyse_images_data = rapport_data.get("analyse_images", {})
# Parcourir toutes les images
for image_path, analyse_data in analyse_images_data.items():
# Vérifier si l'image est pertinente
is_relevant = False
if "sorting" in analyse_data and isinstance(analyse_data["sorting"], dict):
is_relevant = analyse_data["sorting"].get("is_relevant", False)
# Si l'image est pertinente, extraire son analyse
if is_relevant:
image_name = os.path.basename(image_path)
analyse = self._extraire_analyse_image(analyse_data)
if analyse:
images_analyses.append({
"image_name": image_name,
"image_path": image_path,
"analyse": analyse,
"sorting_info": analyse_data.get("sorting", {}),
"metadata": analyse_data.get("analysis", {}).get("metadata", {})
})
logger.info(f"Analyse de l'image {image_name} ajoutée")
return images_analyses
def _extraire_analyse_image(self, analyse_data: Dict) -> Optional[str]:
"""
Extrait l'analyse d'une image depuis les données
"""
# Si pas de données d'analyse, retourner None
if not "analysis" in analyse_data or not analyse_data["analysis"]:
if "sorting" in analyse_data and isinstance(analyse_data["sorting"], dict):
reason = analyse_data["sorting"].get("reason", "Non spécifiée")
return f"Image marquée comme pertinente. Raison: {reason}"
return None
# Extraire l'analyse selon le format des données
analysis = analyse_data["analysis"]
# Structure type 1: {"analyse": "texte"}
if isinstance(analysis, dict) and "analyse" in analysis:
return analysis["analyse"]
# Structure type 2: {"error": false, ...} - contient d'autres données utiles
if isinstance(analysis, dict) and "error" in analysis and not analysis.get("error", True):
return str(analysis)
# Structure type 3: texte d'analyse direct
if isinstance(analysis, str):
return analysis
# Structure type 4: autre format de dictionnaire - convertir en JSON
if isinstance(analysis, dict):
return json.dumps(analysis, ensure_ascii=False, indent=2)
# Aucun format reconnu
return None

View File

@ -1,301 +0,0 @@
from .base_agent import BaseAgent
from typing import Dict, Any, Optional
import logging
import json
import os
import sys
from datetime import datetime
# Ajout du chemin des utilitaires au PATH pour pouvoir les importer
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from loaders.ticket_data_loader import TicketDataLoader
logger = logging.getLogger("AgentTicketAnalyser")
class AgentTicketAnalyser(BaseAgent):
"""
Agent pour analyser les tickets (JSON ou Markdown) et en extraire les informations importantes.
Remplace l'ancien AgentJsonAnalyser avec des fonctionnalités améliorées.
"""
def __init__(self, llm):
super().__init__("AgentTicketAnalyser", llm)
# Configuration locale de l'agent
self.temperature = 0.1 # Besoin d'analyse très précise
self.top_p = 0.8
self.max_tokens = 8000
# Prompt système optimisé
self.system_prompt = """Tu es un expert en analyse de tickets pour le support informatique de BRG-Lab pour la société CBAO.
Tu interviens avant l'analyse des captures d'écran pour contextualiser le ticket, identifier les questions posées, et structurer les échanges de manière claire.
Ta mission principale :
1. Identifier le client et le contexte du ticket (demande "name" et "description")
- Récupère le nom de l'auteur si présent
- Indique si un `user_id` est disponible
- Conserve uniquement les informations d'identification utiles (pas d'adresse ou signature de mail inutile)
2. Mettre en perspective le `name` du ticket
- Il peut contenir une ou plusieurs questions implicites
- Reformule ces questions de façon explicite
3. Analyser la `description`
- Elle fournit souvent le vrai point d'entrée technique
- Repère les formulations interrogatives ou les demandes spécifiques
- Identifie si cette partie complète ou précise les questions du nom
4. Structurer le fil de discussion
- Conserve uniquement les échanges pertinents
-Conserve les questions soulevés par "name" ou "description"
- CONSERVE ABSOLUMENT les références documentation, FAQ, liens utiles et manuels
- Identifie clairement chaque intervenant (client / support)
- Classe les informations par ordre chronologique avec date et rôle
5. Préparer la transmission à l'agent suivant
- Préserve tous les éléments utiles à l'analyse d'image : modules cités, options évoquées, comportements décrits
- Mentionne si des images sont attachées au ticket
Structure ta réponse :
1. Résumé du contexte
- Client (nom, email si disponible)
- Sujet du ticket reformulé en une ou plusieurs questions
- Description technique synthétique
2. Informations techniques détectées
- Logiciels/modules mentionnés
- Paramètres évoqués
- Fonctionnalités impactées
- Conditions spécifiques (multi-laboratoire, utilisateur non valide, etc.)
3. Fil de discussion (filtrée, nettoyée, classée)
- Intervenant (Client/Support)
- Date et contenu de chaque échange
- Résumés techniques
- INCLURE TOUS les liens documentaires (manuel, FAQ, documentation technique)
4. Éléments liés à l'analyse visuelle
- Nombre d'images attachées
- Références aux interfaces ou options à visualiser
- Points à vérifier dans les captures (listes incomplètes, cases à cocher, utilisateurs grisés, etc.)
IMPORTANT :
- Ne propose aucune solution ni interprétation
- Ne génère pas de tableau
- Reste strictement factuel en te basant uniquement sur les informations fournies
- Ne reformule pas les messages, conserve les formulations exactes sauf nettoyage de forme"""
# Initialiser le loader de données
self.ticket_loader = TicketDataLoader()
# Appliquer la configuration au LLM
self._appliquer_config_locale()
logger.info("AgentTicketAnalyser initialisé")
def _appliquer_config_locale(self) -> None:
"""
Applique la configuration locale au modèle LLM.
"""
# Appliquer le prompt système
if hasattr(self.llm, "prompt_system"):
self.llm.prompt_system = self.system_prompt
# Appliquer les paramètres
if hasattr(self.llm, "configurer"):
params = {
"temperature": self.temperature,
"top_p": self.top_p,
"max_tokens": self.max_tokens
}
self.llm.configurer(**params)
def executer(self, ticket_data: Dict[str, Any]) -> str:
"""
Analyse un ticket pour en extraire les informations pertinentes
Args:
ticket_data: Dictionnaire contenant les données du ticket à analyser
ou chemin vers un fichier de ticket (JSON ou Markdown)
Returns:
Réponse formatée contenant l'analyse du ticket
"""
# Détecter si ticket_data est un chemin de fichier ou un dictionnaire
if isinstance(ticket_data, str) and os.path.exists(ticket_data):
try:
ticket_data = self.ticket_loader.charger(ticket_data)
logger.info(f"Données chargées depuis le fichier: {ticket_data}")
except Exception as e:
error_message = f"Erreur lors du chargement du fichier: {str(e)}"
logger.error(error_message)
return f"ERREUR: {error_message}"
# Vérifier que les données sont bien un dictionnaire
if not isinstance(ticket_data, dict):
error_message = "Les données du ticket doivent être un dictionnaire ou un chemin de fichier valide"
logger.error(error_message)
return f"ERREUR: {error_message}"
ticket_code = ticket_data.get('code', 'Inconnu')
logger.info(f"Analyse du ticket: {ticket_code}")
print(f"AgentTicketAnalyser: Analyse du ticket {ticket_code}")
# Récupérer les métadonnées sur la source des données
source_format = "inconnu"
source_file = "non spécifié"
if "metadata" in ticket_data and isinstance(ticket_data["metadata"], dict):
source_format = ticket_data["metadata"].get("format", "inconnu")
source_file = ticket_data["metadata"].get("source_file", "non spécifié")
logger.info(f"Format source: {source_format}, Fichier source: {source_file}")
# Préparer le ticket pour l'analyse
ticket_formate = self._formater_ticket_pour_analyse(ticket_data)
# Créer le prompt pour l'analyse, adapté au format source
prompt = f"""Analyse ce ticket pour en extraire les informations clés et préparer une synthèse structurée.
SOURCE: {source_format.upper()}
{ticket_formate}
RAPPEL IMPORTANT:
- CONSERVE TOUS les liens (FAQ, documentation, manuels) présents dans les messages
- Extrais et organise chronologiquement les échanges client/support
- Identifie les éléments techniques à observer dans les captures d'écran
- Reste factuel et précis sans proposer de solution"""
try:
logger.info("Interrogation du LLM")
response = self.llm.interroger(prompt)
logger.info(f"Réponse reçue: {len(response)} caractères")
print(f" Analyse terminée: {len(response)} caractères")
except Exception as e:
error_message = f"Erreur lors de l'analyse du ticket: {str(e)}"
logger.error(error_message)
response = f"ERREUR: {error_message}"
print(f" ERREUR: {error_message}")
# Enregistrer l'historique avec le prompt complet pour la traçabilité
self.ajouter_historique("analyse_ticket",
{
"ticket_id": ticket_code,
"format_source": source_format,
"source_file": source_file,
"prompt": prompt,
"temperature": self.temperature,
"top_p": self.top_p,
"max_tokens": self.max_tokens,
"timestamp": self._get_timestamp()
},
response)
return response
def _formater_ticket_pour_analyse(self, ticket_data: Dict) -> str:
"""
Formate les données du ticket pour l'analyse LLM, avec une meilleure
gestion des différents formats et structures de données.
Args:
ticket_data: Les données du ticket
Returns:
Représentation textuelle formatée du ticket
"""
# Initialiser avec les informations de base
ticket_name = ticket_data.get('name', 'Sans titre')
ticket_code = ticket_data.get('code', 'Inconnu')
info = f"## TICKET {ticket_code}: {ticket_name}\n\n"
info += f"## NOM DE LA DEMANDE (PROBLÈME INITIAL)\n{ticket_name}\n\n"
# Ajouter la description
description = ticket_data.get('description', '')
if description:
info += f"## DESCRIPTION DU PROBLÈME\n{description}\n\n"
# Ajouter les informations du ticket (exclure certains champs spécifiques)
champs_a_exclure = ['code', 'name', 'description', 'messages', 'metadata']
info += "## INFORMATIONS TECHNIQUES DU TICKET\n"
for key, value in ticket_data.items():
if key not in champs_a_exclure and value:
# Formater les valeurs complexes si nécessaire
if isinstance(value, (dict, list)):
value = json.dumps(value, ensure_ascii=False, indent=2)
info += f"- {key}: {value}\n"
info += "\n"
# Ajouter les messages (conversations) avec un formatage amélioré pour distinguer client/support
messages = ticket_data.get('messages', [])
if messages:
info += "## CHRONOLOGIE DES ÉCHANGES CLIENT/SUPPORT\n"
for i, msg in enumerate(messages):
# Vérifier que le message est bien un dictionnaire
if not isinstance(msg, dict):
continue
sender = msg.get('from', 'Inconnu')
date = msg.get('date', 'Date inconnue')
content = msg.get('content', '')
# Identifier si c'est client ou support
sender_type = "CLIENT" if "client" in sender.lower() else "SUPPORT" if "support" in sender.lower() else "AUTRE"
# Formater correctement la date si possible
try:
if date != 'Date inconnue':
# Essayer différents formats de date
for date_format in ['%Y-%m-%d %H:%M:%S', '%Y-%m-%d', '%d/%m/%Y']:
try:
date_obj = datetime.strptime(date, date_format)
date = date_obj.strftime('%d/%m/%Y %H:%M')
break
except ValueError:
continue
except Exception:
pass # Garder la date d'origine en cas d'erreur
info += f"### Message {i+1} - [{sender_type}] De: {sender} - Date: {date}\n{content}\n\n"
# Ajouter les métadonnées techniques si présentes
metadata = ticket_data.get('metadata', {})
# Exclure certaines métadonnées internes
for key in ['source_file', 'format']:
if key in metadata:
metadata.pop(key)
if metadata:
info += "## MÉTADONNÉES TECHNIQUES\n"
for key, value in metadata.items():
if isinstance(value, (dict, list)):
value = json.dumps(value, ensure_ascii=False, indent=2)
info += f"- {key}: {value}\n"
info += "\n"
return info
def analyser_depuis_fichier(self, chemin_fichier: str) -> str:
"""
Analyse un ticket à partir d'un fichier (JSON ou Markdown)
Args:
chemin_fichier: Chemin vers le fichier à analyser
Returns:
Résultat de l'analyse
"""
try:
ticket_data = self.ticket_loader.charger(chemin_fichier)
return self.executer(ticket_data)
except Exception as e:
error_message = f"Erreur lors de l'analyse du fichier {chemin_fichier}: {str(e)}"
logger.error(error_message)
return f"ERREUR: {error_message}"
def _get_timestamp(self) -> str:
"""Retourne un timestamp au format YYYYMMDD_HHMMSS"""
return datetime.now().strftime("%Y%m%d_%H%M%S")

View File

@ -226,9 +226,34 @@ def sauvegarder_donnees(ticket_id: Optional[str] = None, step_name: str = "", da
if is_resultat and data_list:
# Extraire le nom du modèle LLM des métadonnées du premier élément
first_item = data_list[0]
llm_name = first_item.get("metadata", {}).get("model_info", {}).get("model", "unknown_model")
llm_name = first_item.get("metadata", {}).get("model_info", {}).get("model", "")
# Si le modèle est vide ou 'unknown', vérifier s'il existe déjà un fichier pour cette étape
# afin d'éviter de créer des doublons avec "unknown_model"
if not llm_name or llm_name.lower() == "unknown":
# Rechercher un fichier existant pour la même étape
existing_files = [f for f in os.listdir(pipeline_dir)
if f.startswith(f"{step_name}_") and f.endswith("_results.json")]
if existing_files:
# Utiliser le même nom de modèle que le fichier existant
for file in existing_files:
if "unknown_model" not in file:
# Extraire le nom du modèle du fichier existant
parts = file.split('_')
if len(parts) > 2:
# Reconstruire le nom du modèle (tout ce qui est entre step_name_ et _results.json)
model_part = '_'.join(parts[1:-1])
llm_name = model_part
print(f"Utilisation du nom de modèle existant: {llm_name}")
break
# Si toujours vide ou "unknown", utiliser un nom générique
if not llm_name or llm_name.lower() == "unknown":
llm_name = "unknown_model"
# Nettoyer le nom du modèle pour éviter les caractères problématiques dans le nom de fichier
safe_llm_name = llm_name.lower().replace("/", "_").replace(" ", "_")
safe_llm_name = llm_name.lower().replace("/", "_").replace(" ", "_").replace(":", "_")
file_name = f"{step_name}_{safe_llm_name}_results.json"
else:
file_name = f"{step_name}.json"
@ -236,53 +261,76 @@ def sauvegarder_donnees(ticket_id: Optional[str] = None, step_name: str = "", da
file_path = os.path.join(pipeline_dir, file_name)
try:
# Charger les données existantes si le fichier existe déjà
existing_data = []
# Vérifier si le fichier existe déjà pour éviter de créer des doublons
if os.path.exists(file_path):
print(f"Le fichier {file_path} existe déjà, mise à jour des données")
# Charger les données existantes
try:
with open(file_path, "r", encoding="utf-8") as f:
file_content = f.read().strip()
if file_content: # Vérifier que le fichier n'est pas vide
existing_data = json.loads(file_content)
# Si les données existantes ne sont pas une liste, les convertir en liste
if not isinstance(existing_data, list):
existing_data = [existing_data]
except json.JSONDecodeError:
print(f"Le fichier existant {file_path} n'est pas un JSON valide, création d'un nouveau fichier")
existing_data = json.loads(file_content) if file_content else []
# Si les données existantes ne sont pas une liste, les convertir
if not isinstance(existing_data, list):
existing_data = [existing_data]
except Exception as e:
print(f"Erreur lors de la lecture des données existantes: {e}")
existing_data = []
# Gérer l'ajout de données et la déduplication
updated_data = existing_data.copy()
# Gérer les entrées existantes pour éviter les doublons
if step_name == "rapport_final":
# Pour le rapport final, toujours remplacer complètement
updated_data = data_list
else:
# Pour les analyses d'images, vérifier si l'image a déjà été analysée
processed_paths = set()
existing_data = []
# Fusionner les nouvelles données avec les données existantes
combined_data = existing_data.copy()
# Pour chaque nouvel élément, vérifier s'il existe déjà un élément similaire
for new_item in data_list:
# Vérifier si un élément similaire existe déjà
is_duplicate = False
# Filtrer les entrées existantes pour éviter les doublons d'images
for item in data_list:
image_path = item.get("metadata", {}).get("image_path", "")
if image_path:
processed_paths.add(image_path)
# Supprimer les entrées existantes pour cette image
updated_data = [entry for entry in updated_data
if entry.get("metadata", {}).get("image_path", "") != image_path]
# Ajouter la nouvelle entrée
updated_data.append(item)
else:
# Si pas de chemin d'image, ajouter simplement
updated_data.append(item)
# Pour la déduplication, on compare certains champs clés
if isinstance(new_item, dict):
for idx, existing_item in enumerate(existing_data):
if isinstance(existing_item, dict):
# Comparer les métadonnées pour détecter les doublons
new_meta = new_item.get("metadata", {})
existing_meta = existing_item.get("metadata", {})
# Si les deux éléments ont des chemins d'image et que ces chemins sont identiques,
# ou s'ils ont le même ticket_id et timestamp, on considère que c'est un doublon
if (new_meta.get("image_path") and existing_meta.get("image_path") and
new_meta["image_path"] == existing_meta["image_path"]):
is_duplicate = True
# Mettre à jour l'élément existant avec le nouveau
combined_data[idx] = new_item
break
# Pour les rapports finaux, comparer par ticket_id
elif (step_name == "rapport_final" and
new_meta.get("ticket_id") and existing_meta.get("ticket_id") and
new_meta["ticket_id"] == existing_meta["ticket_id"] and
new_meta.get("source_agent") == existing_meta.get("source_agent")):
is_duplicate = True
# Mettre à jour l'élément existant avec le nouveau
combined_data[idx] = new_item
break
# Si ce n'est pas un doublon, l'ajouter aux données combinées
if not is_duplicate:
combined_data.append(new_item)
# Si on n'a qu'un seul élément à la fin, le sortir de la liste pour garder
# la même structure que précédemment
final_data = combined_data[0] if len(combined_data) == 1 else combined_data
# Sauvegarder les données combinées
with open(file_path, "w", encoding="utf-8") as f:
json.dump(updated_data, f, indent=2, ensure_ascii=False)
print(f"Données sauvegardées dans {file_path} ({len(updated_data)} entrées)")
json.dump(final_data, f, ensure_ascii=False, indent=2)
# Générer une version texte pour tous les fichiers JSON
generer_version_texte(updated_data, ticket_id, step_name, file_path)
print(f"Données sauvegardées dans {file_path} ({len(combined_data)} entrées)")
# Générer également une version texte pour faciliter la lecture
generer_version_texte(final_data, ticket_id, step_name, file_path)
except Exception as e:
print(f"Erreur lors de la sauvegarde des données: {e}")
print(f"Erreur lors de la sauvegarde des données dans {file_path}: {e}")

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,33 @@
**Cross Report**
**Summary**
The support ticket analysis reveals insufficient content for a thorough analysis, but the provided image suggests that the issue might be related to Tomcat setup or configuration.
**Chronological Table of Exchanges**
| Transmitter | Type | Date | Contents | Visual elements |
| --- | --- | --- | --- | --- |
| Customer | Question | Unknown | Insufficient content for analysis | - |
| Support | Request for more information | Unknown | Request for additional details to assist with the issue | - |
| Customer | Response | 2025-04-23 16:36:57 | Provided image (image.png) of Tomcat default home page | Screenshot of Tomcat default home page, indicating successful setup |
**Image Analysis**
The provided image is a screenshot of the Tomcat default home page, which indicates that Tomcat has been set up successfully. The page displays technical information such as:
* Tomcat version: 7
* `catalina_home` rental: `/usr/share/tomcat7`
* `catalina_base` rental: `/var/lib/tomcat7`
* Reference to `/etc/tomcat7/tomcat-users.xml`
**Technical Context**
The image suggests that the issue might be related to Tomcat setup or configuration. The fact that the default Tomcat home page is displayed indicates that Tomcat is running, but there may be issues with specific applications or configurations.
**Recommendations**
* Request additional information from the customer to clarify the issue.
* Verify the Tomcat configuration and setup to ensure it matches the expected settings.
* Consider installing suggested packages (Tomcat 7 Docs, Tomcat 7 Example, Tomcat 7 Admin) to provide additional functionality.
Note: The table only includes one exchange between the customer and support, as there is insufficient information to create a more detailed chronological table.

View File

@ -0,0 +1,33 @@
** Rapport croisé **
**Résumé**
L'analyse des billets de support révèle un contenu insuffisant pour une analyse approfondie, mais l'image fournie suggère que le problème pourrait être lié à la configuration ou à la configuration de Tomcat.
** Tableau chronologique des échanges **
| Émetteur | Type | Date | Contenu | Éléments visuels |
| --- | --- | --- | --- | --- |
| Client | Question | Inconnu | Contenu insuffisant pour l'analyse | - |
| Support | Demande de plus d'informations | Inconnu | Demande de détails supplémentaires pour aider le problème | - |
| Client | Réponse | 2025-04-23 16:36:57 | Image fournie (image.png) de la page d'accueil par défaut de Tomcat | Capture d'écran de la page d'accueil par défaut de Tomcat, indiquant une configuration réussie |
** Analyse d'image **
L'image fournie est une capture d'écran de la page d'accueil par défaut de Tomcat, ce qui indique que Tomcat a été configuré avec succès. La page affiche des informations techniques telles que:
* Version Tomcat: 7
* `Catalina_Home` Rental:` / usr / share / tomcat7`
* `Catalina_Base` Rental:` / var / lib / tomcat7`
* Référence à `/ etc / tomcat7 / tomcat-users.xml`
** Contexte technique **
L'image suggère que le problème pourrait être lié à la configuration ou à la configuration de Tomcat. Le fait que la page d'accueil de Tomcat par défaut s'affiche indique que Tomcat est en cours d'exécution, mais il peut y avoir des problèmes avec des applications ou des configurations spécifiques.
** Recommandations **
* Demandez des informations supplémentaires au client pour clarifier le problème.
* Vérifiez la configuration et la configuration de Tomcat pour vous assurer qu'elle correspond aux paramètres attendus.
* Envisagez d'installer des packages suggérés (Tomcat 7 Docs, Tomcat 7 Exemple, Tomcat 7 Admin) pour fournir des fonctionnalités supplémentaires.
Remarque: Le tableau ne comprend qu'un seul échange entre le client et le support, car il n'y a pas suffisamment d'informations pour créer un tableau chronologique plus détaillé.

View File

@ -0,0 +1,25 @@
OCR Langue: eng
Langue détectée: en
--------------------------------------------------
(© Ocumam B\o
¢2@6
©) Oem Gap Pim Otam
lt works !
If you're seeing this page via a web browser, it means you've setup Tomcat successfully. Congratulations!
This is the default Tomcat home page. It can be found on the local filesystem at: /var/Lib/tomcat7/webapps/ROOT/ index.html
Tomcat? veterans might be pleased to learn that this system instance of Tomcat is installed with CATALINA_HOME in /usr/share/tomcat7 and CATALINA_BASE in /var/lib/tomcat7, following the rules from /usr/share/doc/tomcat?-common/ RUNNING. txt .gz.
You might consider installing the following packages, if you haven't already done so:
tomcat?7-docs: This package installs a web application that allows to browse the Tomcat 7 documentation locally. Once imstalled, you can access it by clicking here.
tomcat7-examples: This package installs a web application that allows to access the Tomcat 7 Servlet and JSP examples. Once installed, you can access it by clicking here.
tomecat7-admin: This package installs two web applications that can help managing this Tomcat instance. Once installed, you can access the manager webapp and the host-manager webapp.
NOTE: For security reasons, using the manager webapp is restricted to users with role "manager-gui"_ The host-manager webapp is restricted to users with role "admin-gui". Users are defined in /etc/tomcat7/tomcat-users.»xml_

View File

@ -0,0 +1,6 @@
OCR Langue: fra
Langue détectée: es
--------------------------------------------------
Mlnpoisbiañe
2 0 PL EURE 0

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

View File

@ -1,62 +1,143 @@
from .base_llm import BaseLLM
import requests
import base64
"""
Module for Llama Vision support.
Optimized for English-only mode, eliminating intermediate translations.
"""
import os
import json
import logging
import time
import base64
import requests
from typing import Dict, Any, Optional, List, Union
from PIL import Image
import io
from datetime import datetime, timedelta
from typing import Dict, Any
from .base_llm import BaseLLM
logger = logging.getLogger("LlamaVision")
class LlamaVision(BaseLLM):
"""
Classe optimisée pour interagir avec l'API Llama Vision.
Interface class with Llama Vision model via its API.
Optimized to work exclusively in English.
"""
def __init__(self, modele: str = "llama3.2-vision:90b-instruct-q8_0"):
super().__init__(modele)
self.api_url = "http://217.182.105.173:11434/api/generate"
# Timeout de requête augmenté pour les images volumineuses
self.request_timeout = 300 # 5 minutes
# Configuration standard via la méthode héritée
# Default configuration optimized for English
self.configurer(
temperature=0.1,
temperature=0.2,
top_p=0.8,
max_tokens=1024,
max_tokens=4000
)
# Request timeout in seconds
self.request_timeout = 600
# Default system prompt (in English)
self.prompt_system = """You are a helpful AI assistant with vision capabilities.
You are interacting with images provided by the user.
Respond in English unless specifically instructed otherwise."""
logger.info(f"Initializing LlamaVision with model {modele} (English-only mode)")
def urlBase(self) -> str:
"""
Retourne l'URL de base de l'API Ollama.
Returns the base URL of the Ollama API.
"""
return "http://217.182.105.173:11434/"
def cleAPI(self) -> str:
"""
Ollama ne nécessite pas de clé API par défaut.
Ollama doesn't require an API key by default.
"""
return ""
def urlFonction(self) -> str:
"""
Retourne l'URL spécifique à Ollama pour générer une réponse.
Returns the specific Ollama URL for generating a response.
"""
return "api/generate"
def _encoder_image_base64(self, chemin_image: str) -> str:
"""
Encodes an image in base64 for the API.
Args:
chemin_image: Path to the image to encode
Returns:
Base64 encoded image or empty string in case of error
"""
try:
# Check image size and reduce if too large
with Image.open(chemin_image) as img:
# If the image is too large, resize it
max_dim = 800 # Maximum dimension
width, height = img.size
if width > max_dim or height > max_dim:
# Calculate ratio to maintain proportions
ratio = min(max_dim / width, max_dim / height)
new_width = int(width * ratio)
new_height = int(height * ratio)
# Resize image
img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
# Convert to RGB if necessary (for formats like PNG with alpha channel)
if img.mode in ("RGBA", "LA", "P"):
# Create a white background and compose the image on top to handle transparency
background = Image.new("RGB", img.size, (255, 255, 255))
if img.mode == "P":
img = img.convert("RGBA")
background.paste(img, mask=img.split()[3] if img.mode == "RGBA" else None)
img = background
elif img.mode != "RGB":
img = img.convert("RGB")
# Temporarily save the resized image
buffer = io.BytesIO()
img.save(buffer, format="JPEG", quality=85)
buffer.seek(0)
# Encode in base64
encoded = base64.b64encode(buffer.read()).decode("utf-8")
return encoded
except Exception as e:
logger.error(f"Base64 encoding error for {chemin_image}: {e}")
try:
# Second attempt with a simpler approach
with Image.open(chemin_image) as img:
# Convert directly to RGB regardless of the image
img = img.convert("RGB")
buffer = io.BytesIO()
img.save(buffer, format="JPEG", quality=75)
buffer.seek(0)
encoded = base64.b64encode(buffer.read()).decode("utf-8")
return encoded
except Exception as e2:
logger.error(f"Second error during image optimization: {str(e2)}")
# Last resort: encode the original image without optimization
with open(chemin_image, "rb") as image_file:
encoded = base64.b64encode(image_file.read()).decode("utf-8")
return encoded
def _preparer_contenu(self, question: str) -> Dict[str, Any]:
"""
Prépare le contenu de la requête spécifique pour Ollama avec le modèle Llama Vision.
Prepares the request content specific to Ollama.
"""
contenu = {
return {
"model": self.modele,
"prompt": question,
"options": {
"temperature": self.params["temperature"],
"top_p": self.params["top_p"],
"temperature": self.params.get("temperature", 0.2),
"top_p": self.params.get("top_p", 0.8),
"num_predict": self.params.get("max_tokens", 1024),
"stop": self.params.get("stop", []),
# Paramètres Ollama spécifiques avec valeurs par défaut
"top_k": 30,
"num_ctx": 1024,
"repeat_penalty": 1.1,
@ -70,156 +151,84 @@ class LlamaVision(BaseLLM):
},
"stream": False
}
return contenu
def _traiter_reponse(self, reponse: requests.Response) -> str:
"""
Traite et retourne la réponse fournie par Ollama.
Processes and returns the response provided by Ollama.
"""
data = reponse.json()
return data.get("response", "")
def _encoder_image_base64(self, image_path: str) -> str:
def interroger_avec_image(self, image_path: str, question: str, english_only: bool = True) -> str:
"""
Encode une image en base64, avec optimisation de la taille si nécessaire.
"""
try:
# Vérifier la taille de l'image et la réduire si trop grande
with Image.open(image_path) as img:
# Si l'image est trop grande, la redimensionner
max_dim = 800 # Dimension maximale
width, height = img.size
if width > max_dim or height > max_dim:
# Calculer le ratio pour conserver les proportions
ratio = min(max_dim / width, max_dim / height)
new_width = int(width * ratio)
new_height = int(height * ratio)
# Redimensionner l'image
img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
# Convertir en RGB si nécessaire (pour les formats comme PNG avec canal alpha)
if img.mode in ("RGBA", "LA", "P"):
# Créer un fond blanc et composer l'image dessus pour gérer la transparence
background = Image.new("RGB", img.size, (255, 255, 255))
if img.mode == "P":
img = img.convert("RGBA")
background.paste(img, mask=img.split()[3] if img.mode == "RGBA" else None)
img = background
elif img.mode != "RGB":
img = img.convert("RGB")
# Sauvegarder temporairement l'image redimensionnée
buffer = io.BytesIO()
img.save(buffer, format="JPEG", quality=85)
buffer.seek(0)
# Encoder en base64
encoded = base64.b64encode(buffer.read()).decode("utf-8")
return encoded
except Exception as e:
print(f"Erreur lors de l'optimisation de l'image: {str(e)}")
try:
# Seconde tentative avec une approche plus simple
with Image.open(image_path) as img:
# Convertir directement en RGB quelle que soit l'image
img = img.convert("RGB")
buffer = io.BytesIO()
img.save(buffer, format="JPEG", quality=75)
buffer.seek(0)
encoded = base64.b64encode(buffer.read()).decode("utf-8")
return encoded
except Exception as e2:
print(f"Deuxième erreur lors de l'optimisation de l'image: {str(e2)}")
# Dernier recours: encoder l'image originale sans optimisation
with open(image_path, "rb") as image_file:
encoded = base64.b64encode(image_file.read()).decode("utf-8")
return encoded
def _optimiser_prompt(self, question: str) -> str:
"""
Optimise le prompt pour des réponses plus rapides.
Ne modifie pas la langue ou le contenu de la question.
"""
# On retourne la question telle quelle sans imposer de format
return question
def interroger_avec_image(self, image_path: str, question: str) -> str:
"""
Interroge le modèle Llama Vision avec une image et une question via Ollama.
Optimisé pour éviter les timeouts.
Sends a multimodal request (image + text) to the API.
Args:
image_path: Chemin vers l'image à analyser
question: Question ou instructions pour l'analyse
image_path: Path to the image
question: The prompt to send
english_only: If True, forces the response in English
Returns:
Réponse du modèle à la question
Model response
"""
url = self.urlBase() + self.urlFonction()
headers = {"Content-Type": "application/json"}
# Check that the image exists
if not os.path.exists(image_path):
logger.error(f"Image does not exist: {image_path}")
return f"Error: image {image_path} does not exist."
try:
# Encoder l'image en base64 avec optimisation de taille
# Encode image in base64
image_b64 = self._encoder_image_base64(image_path)
# Optimiser la question pour des réponses plus courtes et plus rapides
optimised_question = self._optimiser_prompt(question)
# Ensure the question starts with an instruction to respond in English
if english_only and not question.lower().startswith("[english"):
question = "[ENGLISH RESPONSE REQUESTED]\n\n" + question
# Préparer le prompt avec le format spécial pour Ollama multimodal
# Special format for Ollama multimodal
prompt = f"""<image>
{image_b64}
</image>
{optimised_question}"""
contenu = {
"model": self.modele,
"prompt": prompt,
"options": {
"temperature": self.params["temperature"],
"top_p": self.params["top_p"],
"num_predict": self.params.get("max_tokens", 1024),
"stop": self.params.get("stop", []),
# Paramètres Ollama spécifiques
"top_k": 30,
"num_ctx": 1024,
"repeat_penalty": 1.1,
"repeat_last_n": 64,
"mirostat": 0,
"mirostat_eta": 0.1,
"mirostat_tau": 5,
"keep_alive": int(timedelta(minutes=2).total_seconds()),
"min_p": 0,
"seed": 0,
},
"stream": False
}
self.heureDepart = datetime.now()
response = requests.post(url=url, headers=headers, json=contenu, timeout=self.request_timeout)
self.heureFin = datetime.now()
{question}"""
# Prepare the request using the base method
contenu = self._preparer_contenu(prompt)
self.heureDepart = datetime.now()
# Send request
response = requests.post(
url=url,
headers=headers,
json=contenu,
timeout=self.request_timeout
)
self.heureFin = datetime.now()
if self.heureDepart is not None:
self.dureeTraitement = self.heureFin - self.heureDepart
else:
self.dureeTraitement = timedelta(0)
# Response verification
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}"
return f"LlamaVision API Error ({response.status_code}): {response.text}"
except requests.exceptions.Timeout:
self.heureFin = datetime.now()
if self.heureDepart is not None:
self.dureeTraitement = self.heureFin - self.heureDepart
else:
self.dureeTraitement = timedelta(0)
self.reponseErreur = True
return "Timeout lors de l'appel à l'API. L'analyse de l'image a pris trop de temps."
return "Timeout during API call. Image analysis took too long."
except Exception as e:
self.heureFin = datetime.now()
@ -228,16 +237,4 @@ class LlamaVision(BaseLLM):
else:
self.dureeTraitement = timedelta(0)
self.reponseErreur = True
return f"Erreur lors de l'analyse de l'image: {str(e)}"
def configurer(self, **kwargs):
"""
Mise à jour des paramètres du modèle.
Utilise la méthode de BaseLLM pour mettre à jour les paramètres.
"""
# Si request_timeout est spécifié, le mettre à jour séparément
if "request_timeout" in kwargs and isinstance(kwargs["request_timeout"], int):
self.request_timeout = kwargs.pop("request_timeout")
# Utiliser la méthode de la classe parente pour mettre à jour les paramètres standards
super().configurer(**kwargs)
return f"Multimodal communication error: {str(e)}"

View File

@ -26,13 +26,19 @@ def parse_arguments():
parser.add_argument("ticket_id", help="ID du ticket à analyser (ex: T1234)")
parser.add_argument("--output_dir", default="output", help="Répertoire de sortie (par défaut:)")
#Options de configuration
# Options de configuration
parser.add_argument("--no-dedup", action="store_true", help="Désactiver le préfiltrage des doublons d'images")
parser.add_argument("--seuil", type=int, default=5, help="Seuil de similarité pour la déduplication (0-10)")
parser.add_argument("--save-results", action="store_true", help="Sauvegarder les résultats intermédiaires")
parser.add_argument("--debug", action="store_true", help="Activer le mode debug")
parser.add_argument("--reports-dir", default="reports", help="Répertoire de suvegarde des rapports")
# Etapes falcutatives
# Options OCR et traduction
parser.add_argument("--no-ocr", action="store_true", help="Désactiver l'OCR sur les images")
parser.add_argument("--no-english", action="store_true", help="Utiliser la langue originale au lieu de l'anglais")
# Etapes facultatives
parser.add_argument("--skip-ticket-analysis", action="store_true", help="Sauter l'analyse du ticket")
parser.add_argument("--skip-image-sorting", action="store_true", help="Sauter le tri d'images")
parser.add_argument("--skip-image-analysis", action="store_true", help="Sauter l'analyse des images")
@ -55,7 +61,9 @@ def main():
"dedup_threshold": args.seuil,
"save_results": args.save_results,
"debug_mode": args.debug,
"reports_dir": args.reports_dir
"reports_dir": args.reports_dir,
"ocr_enabled": not args.no_ocr,
"english_only": not args.no_english
}
logger.info(f"Configuration: {json.dumps(config, indent=2)}")

View File

@ -3,11 +3,13 @@ import json
import logging
import time
import traceback
from typing import List, Dict, Any, Optional
from typing import List, Dict, Any, Optional, Tuple
from agents.base_agent import BaseAgent
from loaders.ticket_data_loader import TicketDataLoader
from utils.image_dedup import filtrer_images_uniques
from utils.ocr_utils import extraire_texte
from utils.translate_utils import fr_to_en, en_to_fr, sauvegarder_ocr_traduction
# Configuration du logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
@ -38,7 +40,9 @@ class OrchestratorLlamaVision:
"dedup_threshold": 5,
"save_results": True,
"debug_mode": False,
"reports_dir": "reports"
"reports_dir": "reports",
"ocr_enabled": True,
"english_only": True
}
if config:
@ -78,6 +82,17 @@ class OrchestratorLlamaVision:
if not ticket_data:
return
# Ajouter le chemin du fichier JSON au ticket_data pour faciliter l'extraction du ticket_id
if json_path:
ticket_data["file_path"] = json_path
# Traduire le contenu du ticket en anglais avant l'analyse
if self.config.get("english_only") and ticket_data.get("content"):
logger.info(f"Traduction du contenu du ticket {ticket_id} en anglais")
ticket_data["content_original"] = ticket_data["content"]
ticket_data["content"] = fr_to_en(ticket_data["content"])
logger.info(f"Traduction terminée: {len(ticket_data['content'])} caractères")
ticket_analysis = None
if self.ticket_agent:
try:
@ -88,11 +103,41 @@ class OrchestratorLlamaVision:
logger.error(traceback.format_exc())
images_analyses, relevant_images = {}, []
ocr_results = {}
if os.path.exists(attachments_dir):
images = self._lister_images(attachments_dir)
if self.config.get("dedup_enabled", True):
images = filtrer_images_uniques(images, seuil_hamming=self.config["dedup_threshold"], ticket_id=ticket_id)
# Réaliser l'OCR sur toutes les images avant le tri
if self.config.get("ocr_enabled", True):
logger.info(f"Traitement OCR de {len(images)} images")
for img in images:
try:
ocr_fr, langue = extraire_texte(img, lang="auto")
# Traduire le texte extrait en anglais pour une meilleure analyse
ocr_en = fr_to_en(ocr_fr) if ocr_fr else ""
# Traduire à nouveau en français pour vérification (optionnel)
ocr_en_back_fr = en_to_fr(ocr_en) if ocr_en else ""
# Sauvegarder les résultats OCR
sauvegarder_ocr_traduction(img, ticket_id, ocr_fr, ocr_en, ocr_en_back_fr, base_dir=rapport_dir)
# Stocker le résultat de l'OCR pour utilisation ultérieure
ocr_results[img] = {
"texte_fr": ocr_fr,
"texte_en": ocr_en,
"langue_detectee": langue
}
logger.info(f"OCR terminé pour {os.path.basename(img)}: {len(ocr_fr)} caractères ({langue})")
except Exception as e:
logger.warning(f"Erreur OCR pour {os.path.basename(img)}: {e}")
ocr_results[img] = {"texte_fr": "", "texte_en": "", "langue_detectee": "unknown"}
# Traiter toutes les images avec l'agent de tri
if self.image_sorter:
logger.info(f"Traitement de {len(images)} images uniques avec l'agent de tri")
@ -100,7 +145,9 @@ class OrchestratorLlamaVision:
# Analyser toutes les images
for img in images:
try:
result_sort = self.image_sorter.executer(img)
# Inclure l'OCR avec le chemin de l'image pour aider au tri
ocr_context = ocr_results.get(img, {"texte_en": ""}).get("texte_en", "")
result_sort = self.image_sorter.executer(img, ocr_context=ocr_context)
is_relevant = result_sort.get("is_relevant", True)
if is_relevant:
@ -108,7 +155,8 @@ class OrchestratorLlamaVision:
images_analyses[img] = {
"sorting": result_sort or {"is_relevant": True},
"analysis": None
"analysis": None,
"ocr": ocr_results.get(img, {})
}
except Exception as e:
logger.warning(f"Erreur tri image {os.path.basename(img)}: {e}")
@ -132,14 +180,19 @@ class OrchestratorLlamaVision:
for img in images:
images_analyses[img] = {
"sorting": {"is_relevant": True},
"analysis": None
"analysis": None,
"ocr": ocr_results.get(img, {})
}
# Analyser les images pertinentes avec l'agent d'analyse d'images
if self.image_analyser and ticket_analysis:
for img in relevant_images:
try:
result = self.image_analyser.executer(img, contexte=ticket_analysis)
# Intégrer les résultats de l'OCR dans le contexte
ocr_info = ocr_results.get(img, {})
contexte_enrichi = self._enrichir_contexte(ticket_analysis, ocr_info)
result = self.image_analyser.executer(img, contexte=contexte_enrichi)
images_analyses[img]["analysis"] = result
except Exception as e:
logger.warning(f"Erreur analyse image {os.path.basename(img)}: {e}")
@ -181,6 +234,35 @@ class OrchestratorLlamaVision:
logger.error(traceback.format_exc())
logger.info(f"Traitement terminé pour le ticket {ticket_id}")
def _enrichir_contexte(self, ticket_analysis: Dict[str, Any], ocr_info: Dict[str, Any]) -> Dict[str, Any]:
"""
Enrichit le contexte de l'analyse du ticket avec les informations OCR
Args:
ticket_analysis: Résultat de l'analyse du ticket
ocr_info: Informations OCR d'une image
Returns:
Contexte enrichi
"""
if not isinstance(ticket_analysis, dict):
return ticket_analysis
# Créer une copie pour ne pas modifier l'original
contexte_enrichi = ticket_analysis.copy()
# Ajouter les informations OCR
if ocr_info:
contexte_enrichi["ocr_info"] = ocr_info
# Utiliser la version anglaise du texte OCR pour l'analyse
if self.config.get("english_only") and ocr_info.get("texte_en"):
contexte_enrichi["ocr_text"] = ocr_info["texte_en"]
else:
contexte_enrichi["ocr_text"] = ocr_info.get("texte_fr", "")
return contexte_enrichi
def _charger_ticket(self, json_path: Optional[str]) -> Optional[Dict[str, Any]]:
if not json_path:

View File

@ -5,7 +5,7 @@
"translation_en_back_fr": "",
"metadata": {
"ticket_id": "T11143",
"timestamp": "20250423_142039",
"timestamp": "20250423_163703",
"source_module": "ocr_utils + translate_utils",
"lang_detected": "fr"
}

View File

@ -5,7 +5,7 @@
"translation_en_back_fr": "",
"metadata": {
"ticket_id": "T11143",
"timestamp": "20250423_142058",
"timestamp": "20250423_163703",
"source_module": "ocr_utils + translate_utils",
"lang_detected": "fr"
}

View File

@ -0,0 +1,12 @@
{
"image_name": "image.png",
"ocr_fr": "(© Ocumam B\\o\n\n¢2@6\n©) Oem Gap Pim Otam\n\nlt works !\n\nIf you're seeing this page via a web browser, it means you've setup Tomcat successfully. Congratulations!\n\nThis is the default Tomcat home page. It can be found on the local filesystem at: /var/Lib/tomcat7/webapps/ROOT/ index.html\n\nTomcat? veterans might be pleased to learn that this system instance of Tomcat is installed with CATALINA_HOME in /usr/share/tomcat7 and CATALINA_BASE in /var/lib/tomcat7, following the rules from /usr/share/doc/tomcat?-common/ RUNNING. txt .gz.\n\nYou might consider installing the following packages, if you haven't already done so:\n\ntomcat?7-docs: This package installs a web application that allows to browse the Tomcat 7 documentation locally. Once imstalled, you can access it by clicking here.\n\ntomcat7-examples: This package installs a web application that allows to access the Tomcat 7 Servlet and JSP examples. Once installed, you can access it by clicking here.\n\ntomecat7-admin: This package installs two web applications that can help managing this Tomcat instance. Once installed, you can access the manager webapp and the host-manager webapp.\n\nNOTE: For security reasons, using the manager webapp is restricted to users with role \"manager-gui\"_ The host-manager webapp is restricted to users with role \"admin-gui\". Users are defined in /etc/tomcat7/tomcat-users.»xml_",
"translation_en": "(© OCUMAM B \\ O\n\n¢ 2@6\n©) OEM GAP PIM OTAM\n\nLt Works!\n\nIf you are seeing this page via a web browser, it means you've setup tomcat successfully. Congratulations!\n\nThis is the Default Tomcat Home Page. It can be found on the local Filesystem at:/var/lib/tomcat7/webapps/root/index.html\n\nTomcat? veterans might be pleased to read this system instance of tomcat is installed with catalina_home in/usr/share/tomcat7 and catalina_base in/var/lib/tomcat7, following the rules from/usr/share/doc/tomcat? -common/Running. txt .gz.\n\nYou might consider installing the following packages, if you have alreni done so:\n\nTomcat? 7-Docs: This Package Installes A Web Application that Allows to Browse the Tomcat 7 Locally documentation. Once Imstalled, you can access it by clicking here.\n\nTomcat7-Example: This Package Installes A Web Application that Allows to Access the Tomcat 7 Servlet and JSP Examples. Once Installed, you can access it by clicking here.\n\nTomecat7-Admin: this package installations Two Web applications that can help managing this tomcat instance. Once Installed, you can access the Manager Webapp and the Host-Manager Webapp.\n\nNote: For Security Reasons, Using the Manager Webapp is restricted to users with role \"manager-gui\" _ the host-manager webapp is restricted to users with role \"admin-gui\". USERS are defined in /etc/tomcat7/tomcat-users.c",
"translation_en_back_fr": "(© ocumam b \\ o\n\n¢ 2 @ 6\n©) OEM GAP PIM OTAM\n\nLT fonctionne!\n\nSi vous voyez cette page via un navigateur Web, cela signifie que vous avez configuré Tomcat avec succès. Félicitations!\n\nIl s'agit de la page d'accueil par défaut de Tomcat. Il peut être trouvé sur le système de fichiers local à: /var/lib/tomcat7/webapps/root/index.html\n\nMatou? Les anciens combattants pourraient être heureux de lire cette instance système de Tomcat est installé avec Catalina_Home dans / USR / Share / Tomcat7 et Catalina_Base dans / var / lib / tomcat7, en suivant les règles de / usr / share / doc / tomcat? -Common / Running. txt .gz.\n\nVous pourriez envisager d'installer les packages suivants, si vous avez fait Alreni:\n\nMatou? 7-DOCS: Ce package installe une application Web qui permet de parcourir la documentation Tomcat 7 localement. Une fois iMStalled, vous pouvez y accéder en cliquant ici.\n\nTomcat7-Exemple: Ce package installe une application Web qui permet d'accéder aux exemples de servlet Tomcat 7 et JSP. Une fois installé, vous pouvez y accéder en cliquant ici.\n\nTomecat7-admin: Ce package installe deux applications Web qui peuvent aider à gérer cette instance Tomcat. Une fois installé, vous pouvez accéder au Manager WebApp et au Host-Manager WebApp.\n\nRemarque: Pour des raisons de sécurité, l'utilisation du gestionnaire WebApp est limitée aux utilisateurs avec un rôle \"Manager-Gui\" _ Le manager host-manager est limité aux utilisateurs avec le rôle \"Admin-Gui\". Les utilisateurs sont définis dans /etc/tomcat7/tomcat-users.c",
"metadata": {
"ticket_id": "T11143",
"timestamp": "20250423_163700",
"source_module": "ocr_utils + translate_utils",
"lang_detected": "fr"
}
}

View File

@ -0,0 +1,12 @@
{
"image_name": "image_145435.png",
"ocr_fr": "Mlnpoisbiañe\n\n2 0 PL EURE 0",
"translation_en": "Mlnpoisbiañe\n\n2 0 Pl Eure 0",
"translation_en_back_fr": "Mlnpoisbiañe\n\n2 0 Pl eure 0",
"metadata": {
"ticket_id": "T11143",
"timestamp": "20250423_163701",
"source_module": "ocr_utils + translate_utils",
"lang_detected": "fr"
}
}

View File

@ -0,0 +1,89 @@
image.png
[FR] (© Ocumam B\o
¢2@6
©) Oem Gap Pim Otam
lt works !
If you're seeing this page via a web browser, it means you've setup Tomcat successfully. Congratulations!
This is the default Tomcat home page. It can be found on the local filesystem at: /var/Lib/tomcat7/webapps/ROOT/ index.html
Tomcat? veterans might be pleased to learn that this system instance of Tomcat is installed with CATALINA_HOME in /usr/share/tomcat7 and CATALINA_BASE in /var/lib/tomcat7, following the rules from /usr/share/doc/tomcat?-common/ RUNNING. txt .gz.
You might consider installing the following packages, if you haven't already done so:
tomcat?7-docs: This package installs a web application that allows to browse the Tomcat 7 documentation locally. Once imstalled, you can access it by clicking here.
tomcat7-examples: This package installs a web application that allows to access the Tomcat 7 Servlet and JSP examples. Once installed, you can access it by clicking here.
tomecat7-admin: This package installs two web applications that can help managing this Tomcat instance. Once installed, you can access the manager webapp and the host-manager webapp.
NOTE: For security reasons, using the manager webapp is restricted to users with role "manager-gui"_ The host-manager webapp is restricted to users with role "admin-gui". Users are defined in /etc/tomcat7/tomcat-users.»xml_
[EN] (© OCUMAM B \ O
¢ 2@6
©) OEM GAP PIM OTAM
Lt Works!
If you are seeing this page via a web browser, it means you've setup tomcat successfully. Congratulations!
This is the Default Tomcat Home Page. It can be found on the local Filesystem at:/var/lib/tomcat7/webapps/root/index.html
Tomcat? veterans might be pleased to read this system instance of tomcat is installed with catalina_home in/usr/share/tomcat7 and catalina_base in/var/lib/tomcat7, following the rules from/usr/share/doc/tomcat? -common/Running. txt .gz.
You might consider installing the following packages, if you have alreni done so:
Tomcat? 7-Docs: This Package Installes A Web Application that Allows to Browse the Tomcat 7 Locally documentation. Once Imstalled, you can access it by clicking here.
Tomcat7-Example: This Package Installes A Web Application that Allows to Access the Tomcat 7 Servlet and JSP Examples. Once Installed, you can access it by clicking here.
Tomecat7-Admin: this package installations Two Web applications that can help managing this tomcat instance. Once Installed, you can access the Manager Webapp and the Host-Manager Webapp.
Note: For Security Reasons, Using the Manager Webapp is restricted to users with role "manager-gui" _ the host-manager webapp is restricted to users with role "admin-gui". USERS are defined in /etc/tomcat7/tomcat-users.c
[EN→FR] (© ocumam b \ o
¢ 2 @ 6
©) OEM GAP PIM OTAM
LT fonctionne!
Si vous voyez cette page via un navigateur Web, cela signifie que vous avez configuré Tomcat avec succès. Félicitations!
Il s'agit de la page d'accueil par défaut de Tomcat. Il peut être trouvé sur le système de fichiers local à: /var/lib/tomcat7/webapps/root/index.html
Matou? Les anciens combattants pourraient être heureux de lire cette instance système de Tomcat est installé avec Catalina_Home dans / USR / Share / Tomcat7 et Catalina_Base dans / var / lib / tomcat7, en suivant les règles de / usr / share / doc / tomcat? -Common / Running. txt .gz.
Vous pourriez envisager d'installer les packages suivants, si vous avez fait Alreni:
Matou? 7-DOCS: Ce package installe une application Web qui permet de parcourir la documentation Tomcat 7 localement. Une fois iMStalled, vous pouvez y accéder en cliquant ici.
Tomcat7-Exemple: Ce package installe une application Web qui permet d'accéder aux exemples de servlet Tomcat 7 et JSP. Une fois installé, vous pouvez y accéder en cliquant ici.
Tomecat7-admin: Ce package installe deux applications Web qui peuvent aider à gérer cette instance Tomcat. Une fois installé, vous pouvez accéder au Manager WebApp et au Host-Manager WebApp.
Remarque: Pour des raisons de sécurité, l'utilisation du gestionnaire WebApp est limitée aux utilisateurs avec un rôle "Manager-Gui" _ Le manager host-manager est limité aux utilisateurs avec le rôle "Admin-Gui". Les utilisateurs sont définis dans /etc/tomcat7/tomcat-users.c
image_145435.png
[FR] Mlnpoisbiañe
2 0 PL EURE 0
[EN] Mlnpoisbiañe
2 0 Pl Eure 0
[EN→FR] Mlnpoisbiañe
2 0 Pl eure 0
543d7da1b54c29ff43ce5712d1a9aa4962ed21795c4e943fcb8cb84fd4d7465a.jpg
[FR] _
[EN] _
[EN→FR] _
a20f7697fd5e1d1fca3296c6d01228220e0e112c46b4440cc938f74d10934e98.gif
[FR] _
[EN] _
[EN→FR] _

View File

@ -0,0 +1,16 @@
{
"image_path": "output/ticket_T11143/T11143_20250422_084617/attachments/image.png",
"analysis": {
"analyse": "** Analyse d'image **\n\nL'image fournie semble être une capture d'écran d'une page Web affichant la page d'accueil de Tomcat par défaut. La page indique que Tomcat a été configuré avec succès.\n\n** Description détaillée **\n\n* Le haut de la page affiche le texte \"Si vous voyez cette page via un navigateur Web, cela signifie que vous configurez Tomcat avec succès. Félicitations!\"\n* Ci-dessous de ce message, il existe une section intitulée \"Les vétérans Tomcat pourraient être heureux de lire\" qui fournit des informations sur l'instance système de Tomcat, y compris les emplacements de `Catalina_home` et` Catalina_base`.\n* La page suggère également d'installer des packages supplémentaires:\n\t+ Tomcat 7 Docs: permet de parcourir la documentation locale\n\t+ Tomcat 7 Exemple: donne accès aux exemples de servlet et de JSP\n\t+ Tomcat 7 Admin: installe deux applications Web pour la gestion de l'instance Tomcat (Manager WebApp et Host-Manager WebApp)\n* Une note au bas de la page mentionne les restrictions de sécurité pour l'utilisation du Manager WebApp et Host-Manager WebApp, faisant référence aux rôles utilisateur définis dans `/ etc / tomcat7 / tomcat-users.xml`.\n\n** Messages d'erreur, informations techniques ou éléments d'interface **\n\n* Aucun message d'erreur n'est visible sur cette page.\n* Les informations techniques suivantes sont présentes:\n\t+ Version Tomcat: 7\n\t+ `Catalina_Home` Emplacement:` / usr / share / tomcat7`\n\t+ `Catalina_Base` Emplacement:` / var / lib / tomcat7`\n\t+ Référence à `/ etc / tomcat7 / tomcat- users.xml` pour les rôles utilisateur\n\n** Relation avec le contexte du ticket de support **\n\nLe contexte du ticket de support est insuffisant pour l'analyse, mais cette image suggère que le problème pourrait être lié à la configuration ou à la configuration de Tomcat. Le fait que la page d'accueil de Tomcat par défaut s'affiche indique que Tomcat est en cours d'exécution, mais il peut y avoir des problèmes avec des applications ou des configurations spécifiques.\n\n** Numéros de version, indicateurs d'état ou dates **\n\n* Version Tomcat: 7\n* Aucun indicateur ou datte d'état n'est visible sur cette page.\n\n** Détails techniques extraits **\n\n* `Catalina_Home` Emplacement:` / usr / share / tomcat7`\n* `Catalina_Base` Emplacement:` / var / lib / tomcat7`\n* Référence à `/ etc / tomcat7 / tomcat-users.xml` pour les rôles utilisateur\n* Packages suggérés pour l'installation:\n\t+ Tomcat 7 Docs\n\t+ Tomcat 7 Exemple\n\t+ Tomcat 7 Admin",
"analyse_en": "**Image Analysis**\n\nThe provided image appears to be a screenshot of a web page displaying the default Tomcat home page. The page indicates that Tomcat has been set up successfully.\n\n**Detailed Description**\n\n* The top of the page displays the text \"If you are seeing this page via a web browser, it means you've setup tomcat successfully. Congratulations!\"\n* Below this message, there is a section titled \"Tomcat veterans might be pleased to read\" that provides information about the system instance of Tomcat, including the locations of `catalina_home` and `catalina_base`.\n* The page also suggests installing additional packages:\n\t+ Tomcat 7 Docs: allows browsing local documentation\n\t+ Tomcat 7 Example: provides access to Servlet and JSP examples\n\t+ Tomcat 7 Admin: installs two web applications for managing the Tomcat instance (Manager Webapp and Host-Manager Webapp)\n* A note at the bottom of the page mentions security restrictions for using the Manager Webapp and Host-Manager Webapp, referencing user roles defined in `/etc/tomcat7/tomcat-users.xml`.\n\n**Error Messages, Technical Information, or Interface Elements**\n\n* No error messages are visible on this page.\n* The following technical information is present:\n\t+ Tomcat version: 7\n\t+ `catalina_home` location: `/usr/share/tomcat7`\n\t+ `catalina_base` location: `/var/lib/tomcat7`\n\t+ Reference to `/etc/tomcat7/tomcat-users.xml` for user roles\n\n**Relation to Support Ticket Context**\n\nThe support ticket context is insufficient for analysis, but this image suggests that the issue might be related to Tomcat setup or configuration. The fact that the default Tomcat home page is displayed indicates that Tomcat is running, but there may be issues with specific applications or configurations.\n\n**Version Numbers, Status Indicators, or Dates**\n\n* Tomcat version: 7\n* No status indicators or dates are visible on this page.\n\n**Extracted Technical Details**\n\n* `catalina_home` location: `/usr/share/tomcat7`\n* `catalina_base` location: `/var/lib/tomcat7`\n* Reference to `/etc/tomcat7/tomcat-users.xml` for user roles\n* Suggested packages for installation:\n\t+ Tomcat 7 Docs\n\t+ Tomcat 7 Example\n\t+ Tomcat 7 Admin",
"metadata": {
"image_path": "output/ticket_T11143/T11143_20250422_084617/attachments/image.png",
"image_name": "image.png",
"ticket_id": "T11143",
"timestamp": "20250423_163937",
"ocr_used": true,
"model": "llama3.2-vision:90b-instruct-q8_0",
"source_agent": "AgentImageAnalyser"
}
}
}

View File

@ -0,0 +1,48 @@
RÉSULTATS DE L'ANALYSE ANALYSE_IMAGE - TICKET T11143
================================================================================
** Analyse d'image **
L'image fournie semble être une capture d'écran d'une page Web affichant la page d'accueil de Tomcat par défaut. La page indique que Tomcat a été configuré avec succès.
** Description détaillée **
* Le haut de la page affiche le texte "Si vous voyez cette page via un navigateur Web, cela signifie que vous configurez Tomcat avec succès. Félicitations!"
* Ci-dessous de ce message, il existe une section intitulée "Les vétérans Tomcat pourraient être heureux de lire" qui fournit des informations sur l'instance système de Tomcat, y compris les emplacements de `Catalina_home` et` Catalina_base`.
* La page suggère également d'installer des packages supplémentaires:
+ Tomcat 7 Docs: permet de parcourir la documentation locale
+ Tomcat 7 Exemple: donne accès aux exemples de servlet et de JSP
+ Tomcat 7 Admin: installe deux applications Web pour la gestion de l'instance Tomcat (Manager WebApp et Host-Manager WebApp)
* Une note au bas de la page mentionne les restrictions de sécurité pour l'utilisation du Manager WebApp et Host-Manager WebApp, faisant référence aux rôles utilisateur définis dans `/ etc / tomcat7 / tomcat-users.xml`.
** Messages d'erreur, informations techniques ou éléments d'interface **
* Aucun message d'erreur n'est visible sur cette page.
* Les informations techniques suivantes sont présentes:
+ Version Tomcat: 7
+ `Catalina_Home` Emplacement:` / usr / share / tomcat7`
+ `Catalina_Base` Emplacement:` / var / lib / tomcat7`
+ Référence à `/ etc / tomcat7 / tomcat- users.xml` pour les rôles utilisateur
** Relation avec le contexte du ticket de support **
Le contexte du ticket de support est insuffisant pour l'analyse, mais cette image suggère que le problème pourrait être lié à la configuration ou à la configuration de Tomcat. Le fait que la page d'accueil de Tomcat par défaut s'affiche indique que Tomcat est en cours d'exécution, mais il peut y avoir des problèmes avec des applications ou des configurations spécifiques.
** Numéros de version, indicateurs d'état ou dates **
* Version Tomcat: 7
* Aucun indicateur ou datte d'état n'est visible sur cette page.
** Détails techniques extraits **
* `Catalina_Home` Emplacement:` / usr / share / tomcat7`
* `Catalina_Base` Emplacement:` / var / lib / tomcat7`
* Référence à `/ etc / tomcat7 / tomcat-users.xml` pour les rôles utilisateur
* Packages suggérés pour l'installation:
+ Tomcat 7 Docs
+ Tomcat 7 Exemple
+ Tomcat 7 Admin
================================================================================
Fichier original: analyse_image_image_pixtral-large-latest_results.json

View File

@ -1,12 +0,0 @@
{
"image_name": "image.png",
"ocr_fr": "Apache Tomcat x +\n\nCG A 3 zkl.brg-lab.com\n\n@ Andre 7 Demo 7% Devmat @ Base modèle\n\nIt works !\n\nIf you're seeing this page via a web browser, it means you've setup Tomcat successfully. Congratulations!\n\nThis is the default Tomcat home page. It can be found on the local filesystem at: /var/lib/tomcat7/webapps/ROOT/index.html\n\nTomcat}? veterans might be pleased to learn that this system instance of Tomcat is installed with CATALINA_HOME in /usr/share/tomcat7 and CATALINA BASE in /var/lib/tomcat7, following the rules from /usr/share/doc/tomcat7-common/RUNNING. txt. gz.\nYou might consider installing the following packages, if you haven't already done so:\n\ntomcat7-docs: This package installs a web application that allows to browse the Tomcat 7 documentation locally. Once installed, you can access it by clicking here.\n\ntomcat7-examples: This package installs a web application that allows to access the Tomcat 7 Servlet and JSP examples. Once installed, you can access it by clicking here.\n\ntomcat7-admin: This package installs two web applications that can help managing this Tomcat instance. Once installed, you can access the manager webapp and the host-manager webapp.\n\nNOTE: For security reasons, using the manager webapp is restricted to users with role \"“manager-gui\". The host-manager webapp is restricted to users with role \"admin-gui\". Users are defined in /etc/tomcat7/tomcat-users.xml.",
"translation_en": "Apache Tomcat x +\n\nCG A 3 zkl.brg-lab.com\n\n@ Andre 7 Demo 7% Devmat @ model base\n\nIt works!\n\nIf you are seeing this page via a web browser, it means you've setup tomcat successfully. Congratulations!\n\nThis is the Default Tomcat Home Page. It can be found on the local Filesystem at: /var/lib/tomcat7/webapps/root/index.html\n\nTomcat}? veterans might be pleased to read this system instance of tomcat is installed with catalina_home in/usr/tomcat7 and catalina base in/var/lib/tomcat7, following the rules from/usr/share/doc/tomcat7-common/Running. TXT. Gz.\nYou might consider installing the following packages, if you have alreni done so:\n\nTomcat7-Docs: This Package Installes A Web Application that Allows to Browse the Tomcat 7 Locally documentation. Once Installed, you can access it by clicking here.\n\nTomcat7-Example: This Package Installes A Web Application that Allows to Access the Tomcat 7 Servlet and JSP Examples. Once Installed, you can access it by clicking here.\n\nTomcat7-Admin: This Package Installes Two Web Applications that can help managing this tomcat instance. Once Installed, you can access the Manager Webapp and the Host-Manager Webapp.\n\nNote: For Security Reasons, Using the Manager Webapp is restricted to users with Role \"Manager-Gui\". The Host-Manager Webapp is restricted to users with role \"admin-guui\". USERS are defined in /etc/tomcat7/tomcat-users.xml.",
"translation_en_back_fr": "Apache Tomcat x +\n\nCG a 3 zkl.brg-lab.com\n\n@ Andre 7 Demo 7% Devmat @ Model Base\n\nÇa marche!\n\nSi vous voyez cette page via un navigateur Web, cela signifie que vous avez configuré Tomcat avec succès. Félicitations!\n\nIl s'agit de la page d'accueil par défaut de Tomcat. Il peut être trouvé sur le système de fichiers local à: /var/lib/tomcat7/webapps/root/index.html\n\nMatou}? Les vétérans pourraient être heureux de lire cette instance système de Tomcat est installé avec Catalina_Home dans / USR / Tomcat7 et Catalina Base dans / var / lib / tomcat7, en suivant les règles de / usr / share / doc / tomcat7-Common / Running. SMS. GZ.\nVous pourriez envisager d'installer les packages suivants, si vous avez fait Alreni:\n\nTomcat7-Docs: Ce package installe une application Web qui permet de parcourir la documentation Tomcat 7 localement. Une fois installé, vous pouvez y accéder en cliquant ici.\n\nTomcat7-Exemple: Ce package installe une application Web qui permet d'accéder aux exemples de servlet Tomcat 7 et JSP. Une fois installé, vous pouvez y accéder en cliquant ici.\n\nTomcat7-admin: Ce package installe deux applications Web qui peuvent aider à gérer cette instance Tomcat. Une fois installé, vous pouvez accéder au Manager WebApp et au Host-Manager WebApp.\n\nRemarque: Pour des raisons de sécurité, l'utilisation du gestionnaire WebApp est limitée aux utilisateurs avec le rôle \"Manager-Gui\". Le manager host-manager est limité aux utilisateurs avec un rôle \"Admin-GUUI\". Les utilisateurs sont définis dans /etc/tomcat7/tomcat-users.xml.",
"metadata": {
"ticket_id": "T11143",
"timestamp": "20250423_141944",
"source_module": "ocr_utils + translate_utils",
"lang_detected": "fr"
}
}

View File

@ -1,12 +0,0 @@
{
"image_name": "image_145435.png",
"ocr_fr": "[6] 25 giraudbrg-lobcom/BRG-LAB/PAGE_ programmeEssai/xE4AAHDVNGOAA\n\n| BRGLAS CD Béton C9 foumétew bo 4 Masse\n\nEchantillon n°2500075 réceptionné le 02/04/2025 par BOLLEE Victor - prlevii Le 02/04/2025 por BOLLEE Victor n° prétèvement : 25-6007\nMatériau Sable 0/20 - CARRIERE ADCEG\n\nNREGISTRER\n\nLMPRMER\n\nle de trouver Fadeessé IP du serveur de zk1.brg-lab.com.",
"translation_en": "[6] 25 GIRAUDBRG-LOBCOM/BRG-LAB/PAGE_ PROGRAMESSAI/XE4AAHDVNGOAAA\n\n| Brglas CD concrete C9 Foumetew Bo 4 Mass\n\nSample n ° 2500075 received on 02/04/2025 by Bollee Victor - PRLEVII on 02/04/2025 POR BOLLEE Victor N ° PRETREMENT: 25-6007\nSand material 0/20 - CARRIERE ADCEG\n\nRegister\n\nLmprmer\n\nThe to find Fadeessé IP of the ZK1.brg-lab.com server.",
"translation_en_back_fr": "[6] 25 Giraudbrg-Lobcom / Brg-Lab / Page_ Programessai / Xe4aahdvngoaaa\n\n| Brglas cd béton c9 foumetew bo 4 masse\n\nÉchantillon N ° 2500075 Reçu le 02/04/2025 par Bollee Victor - Prllevii le 02/04/2025 Por Bollee Victor N ° Pretection: 25-6007\nMatériau de sable 0/20 - Carriere adceg\n\nRegistre\n\nLMPRMER\n\nLe pour trouver Fadeessé IP du serveur ZK1.brg-lab.com.",
"metadata": {
"ticket_id": "T11143",
"timestamp": "20250423_142010",
"source_module": "ocr_utils + translate_utils",
"lang_detected": "fr"
}
}

View File

@ -1,116 +0,0 @@
image.png
[FR] Apache Tomcat x +
CG A 3 zkl.brg-lab.com
@ Andre 7 Demo 7% Devmat @ Base modèle
It works !
If you're seeing this page via a web browser, it means you've setup Tomcat successfully. Congratulations!
This is the default Tomcat home page. It can be found on the local filesystem at: /var/lib/tomcat7/webapps/ROOT/index.html
Tomcat}? veterans might be pleased to learn that this system instance of Tomcat is installed with CATALINA_HOME in /usr/share/tomcat7 and CATALINA BASE in /var/lib/tomcat7, following the rules from /usr/share/doc/tomcat7-common/RUNNING. txt. gz.
You might consider installing the following packages, if you haven't already done so:
tomcat7-docs: This package installs a web application that allows to browse the Tomcat 7 documentation locally. Once installed, you can access it by clicking here.
tomcat7-examples: This package installs a web application that allows to access the Tomcat 7 Servlet and JSP examples. Once installed, you can access it by clicking here.
tomcat7-admin: This package installs two web applications that can help managing this Tomcat instance. Once installed, you can access the manager webapp and the host-manager webapp.
NOTE: For security reasons, using the manager webapp is restricted to users with role "“manager-gui". The host-manager webapp is restricted to users with role "admin-gui". Users are defined in /etc/tomcat7/tomcat-users.xml.
[EN] Apache Tomcat x +
CG A 3 zkl.brg-lab.com
@ Andre 7 Demo 7% Devmat @ model base
It works!
If you are seeing this page via a web browser, it means you've setup tomcat successfully. Congratulations!
This is the Default Tomcat Home Page. It can be found on the local Filesystem at: /var/lib/tomcat7/webapps/root/index.html
Tomcat}? veterans might be pleased to read this system instance of tomcat is installed with catalina_home in/usr/tomcat7 and catalina base in/var/lib/tomcat7, following the rules from/usr/share/doc/tomcat7-common/Running. TXT. Gz.
You might consider installing the following packages, if you have alreni done so:
Tomcat7-Docs: This Package Installes A Web Application that Allows to Browse the Tomcat 7 Locally documentation. Once Installed, you can access it by clicking here.
Tomcat7-Example: This Package Installes A Web Application that Allows to Access the Tomcat 7 Servlet and JSP Examples. Once Installed, you can access it by clicking here.
Tomcat7-Admin: This Package Installes Two Web Applications that can help managing this tomcat instance. Once Installed, you can access the Manager Webapp and the Host-Manager Webapp.
Note: For Security Reasons, Using the Manager Webapp is restricted to users with Role "Manager-Gui". The Host-Manager Webapp is restricted to users with role "admin-guui". USERS are defined in /etc/tomcat7/tomcat-users.xml.
[EN→FR] Apache Tomcat x +
CG a 3 zkl.brg-lab.com
@ Andre 7 Demo 7% Devmat @ Model Base
Ça marche!
Si vous voyez cette page via un navigateur Web, cela signifie que vous avez configuré Tomcat avec succès. Félicitations!
Il s'agit de la page d'accueil par défaut de Tomcat. Il peut être trouvé sur le système de fichiers local à: /var/lib/tomcat7/webapps/root/index.html
Matou}? Les vétérans pourraient être heureux de lire cette instance système de Tomcat est installé avec Catalina_Home dans / USR / Tomcat7 et Catalina Base dans / var / lib / tomcat7, en suivant les règles de / usr / share / doc / tomcat7-Common / Running. SMS. GZ.
Vous pourriez envisager d'installer les packages suivants, si vous avez fait Alreni:
Tomcat7-Docs: Ce package installe une application Web qui permet de parcourir la documentation Tomcat 7 localement. Une fois installé, vous pouvez y accéder en cliquant ici.
Tomcat7-Exemple: Ce package installe une application Web qui permet d'accéder aux exemples de servlet Tomcat 7 et JSP. Une fois installé, vous pouvez y accéder en cliquant ici.
Tomcat7-admin: Ce package installe deux applications Web qui peuvent aider à gérer cette instance Tomcat. Une fois installé, vous pouvez accéder au Manager WebApp et au Host-Manager WebApp.
Remarque: Pour des raisons de sécurité, l'utilisation du gestionnaire WebApp est limitée aux utilisateurs avec le rôle "Manager-Gui". Le manager host-manager est limité aux utilisateurs avec un rôle "Admin-GUUI". Les utilisateurs sont définis dans /etc/tomcat7/tomcat-users.xml.
image_145435.png
[FR] [6] 25 giraudbrg-lobcom/BRG-LAB/PAGE_ programmeEssai/xE4AAHDVNGOAA
| BRGLAS CD Béton C9 foumétew bo 4 Masse
Echantillon n°2500075 réceptionné le 02/04/2025 par BOLLEE Victor - prlevii Le 02/04/2025 por BOLLEE Victor n° prétèvement : 25-6007
Matériau Sable 0/20 - CARRIERE ADCEG
NREGISTRER
LMPRMER
le de trouver Fadeessé IP du serveur de zk1.brg-lab.com.
[EN] [6] 25 GIRAUDBRG-LOBCOM/BRG-LAB/PAGE_ PROGRAMESSAI/XE4AAHDVNGOAAA
| Brglas CD concrete C9 Foumetew Bo 4 Mass
Sample n ° 2500075 received on 02/04/2025 by Bollee Victor - PRLEVII on 02/04/2025 POR BOLLEE Victor N ° PRETREMENT: 25-6007
Sand material 0/20 - CARRIERE ADCEG
Register
Lmprmer
The to find Fadeessé IP of the ZK1.brg-lab.com server.
[EN→FR] [6] 25 Giraudbrg-Lobcom / Brg-Lab / Page_ Programessai / Xe4aahdvngoaaa
| Brglas cd béton c9 foumetew bo 4 masse
Échantillon N ° 2500075 Reçu le 02/04/2025 par Bollee Victor - Prllevii le 02/04/2025 Por Bollee Victor N ° Pretection: 25-6007
Matériau de sable 0/20 - Carriere adceg
Registre
LMPRMER
Le pour trouver Fadeessé IP du serveur ZK1.brg-lab.com.
543d7da1b54c29ff43ce5712d1a9aa4962ed21795c4e943fcb8cb84fd4d7465a.jpg
[FR] _
[EN] _
[EN→FR] _
a20f7697fd5e1d1fca3296c6d01228220e0e112c46b4440cc938f74d10934e98.gif
[FR] _
[EN] _
[EN→FR] _

View File

@ -0,0 +1,36 @@
RAPPORT D'ANALYSE DU TICKET T11143
==================================================
** Rapport croisé **
**Résumé**
L'analyse des billets de support révèle un contenu insuffisant pour une analyse approfondie, mais l'image fournie suggère que le problème pourrait être lié à la configuration ou à la configuration de Tomcat.
** Tableau chronologique des échanges **
| Émetteur | Type | Date | Contenu | Éléments visuels |
| --- | --- | --- | --- | --- |
| Client | Question | Inconnu | Contenu insuffisant pour l'analyse | - |
| Support | Demande de plus d'informations | Inconnu | Demande de détails supplémentaires pour aider le problème | - |
| Client | Réponse | 2025-04-23 16:36:57 | Image fournie (image.png) de la page d'accueil par défaut de Tomcat | Capture d'écran de la page d'accueil par défaut de Tomcat, indiquant une configuration réussie |
** Analyse d'image **
L'image fournie est une capture d'écran de la page d'accueil par défaut de Tomcat, ce qui indique que Tomcat a été configuré avec succès. La page affiche des informations techniques telles que:
* Version Tomcat: 7
* `Catalina_Home` Rental:` / usr / share / tomcat7`
* `Catalina_Base` Rental:` / var / lib / tomcat7`
* Référence à `/ etc / tomcat7 / tomcat-users.xml`
** Contexte technique **
L'image suggère que le problème pourrait être lié à la configuration ou à la configuration de Tomcat. Le fait que la page d'accueil de Tomcat par défaut s'affiche indique que Tomcat est en cours d'exécution, mais il peut y avoir des problèmes avec des applications ou des configurations spécifiques.
** Recommandations **
* Demandez des informations supplémentaires au client pour clarifier le problème.
* Vérifiez la configuration et la configuration de Tomcat pour vous assurer qu'elle correspond aux paramètres attendus.
* Envisagez d'installer des packages suggérés (Tomcat 7 Docs, Tomcat 7 Exemple, Tomcat 7 Admin) pour fournir des fonctionnalités supplémentaires.
Remarque: Le tableau ne comprend qu'un seul échange entre le client et le support, car il n'y a pas suffisamment d'informations pour créer un tableau chronologique plus détaillé.

View File

@ -0,0 +1,36 @@
ANALYSIS REPORT FOR TICKET T11143
==================================================
**Cross Report**
**Summary**
The support ticket analysis reveals insufficient content for a thorough analysis, but the provided image suggests that the issue might be related to Tomcat setup or configuration.
**Chronological Table of Exchanges**
| Transmitter | Type | Date | Contents | Visual elements |
| --- | --- | --- | --- | --- |
| Customer | Question | Unknown | Insufficient content for analysis | - |
| Support | Request for more information | Unknown | Request for additional details to assist with the issue | - |
| Customer | Response | 2025-04-23 16:36:57 | Provided image (image.png) of Tomcat default home page | Screenshot of Tomcat default home page, indicating successful setup |
**Image Analysis**
The provided image is a screenshot of the Tomcat default home page, which indicates that Tomcat has been set up successfully. The page displays technical information such as:
* Tomcat version: 7
* `catalina_home` rental: `/usr/share/tomcat7`
* `catalina_base` rental: `/var/lib/tomcat7`
* Reference to `/etc/tomcat7/tomcat-users.xml`
**Technical Context**
The image suggests that the issue might be related to Tomcat setup or configuration. The fact that the default Tomcat home page is displayed indicates that Tomcat is running, but there may be issues with specific applications or configurations.
**Recommendations**
* Request additional information from the customer to clarify the issue.
* Verify the Tomcat configuration and setup to ensure it matches the expected settings.
* Consider installing suggested packages (Tomcat 7 Docs, Tomcat 7 Example, Tomcat 7 Admin) to provide additional functionality.
Note: The table only includes one exchange between the customer and support, as there is insufficient information to create a more detailed chronological table.

View File

@ -0,0 +1,23 @@
{
"prompt": "Voici les données d'analyse pour un ticket de support :\n\n=== ANALYSE DU TICKET ===\n{'response': 'Contenu du ticket insuffisant pour analyse', 'response_en': 'Ticket content insufficient for analysis', 'error': True, 'metadata': {'timestamp': '20250423_163657', 'source_agent': 'AgentTicketAnalyser', 'ticket_id': 'T11143'}}\n\n=== ANALYSES D'IMAGES ===\n--- IMAGE : image.png ---\n**Image Analysis**\n\nThe provided image appears to be a screenshot of a web page displaying the default Tomcat home page. The page indicates that Tomcat has been set up successfully.\n\n**Detailed Description**\n\n* The top of the page displays the text \"If you are seeing this page via a web browser, it means you've setup tomcat successfully. Congratulations!\"\n* Below this message, there is a section titled \"Tomcat veterans might be pleased to read\" that provides information about the system instance of Tomcat, including the locations of `catalina_home` and `catalina_base`.\n* The page also suggests installing additional packages:\n\t+ Tomcat 7 Docs: allows browsing local documentation\n\t+ Tomcat 7 Example: provides access to Servlet and JSP examples\n\t+ Tomcat 7 Admin: installs two web applications for managing the Tomcat instance (Manager Webapp and Host-Manager Webapp)\n* A note at the bottom of the page mentions security restrictions for using the Manager Webapp and Host-Manager Webapp, referencing user roles defined in `/etc/tomcat7/tomcat-users.xml`.\n\n**Error Messages, Technical Information, or Interface Elements**\n\n* No error messages are visible on this page.\n* The following technical information is present:\n\t+ Tomcat version: 7\n\t+ `catalina_home` location: `/usr/share/tomcat7`\n\t+ `catalina_base` location: `/var/lib/tomcat7`\n\t+ Reference to `/etc/tomcat7/tomcat-users.xml` for user roles\n\n**Relation to Support Ticket Context**\n\nThe support ticket context is insufficient for analysis, but this image suggests that the issue might be related to Tomcat setup or configuration. The fact that the default Tomcat home page is displayed indicates that Tomcat is running, but there may be issues with specific applications or configurations.\n\n**Version Numbers, Status Indicators, or Dates**\n\n* Tomcat version: 7\n* No status indicators or dates are visible on this page.\n\n**Extracted Technical Details**\n\n* `catalina_home` location: `/usr/share/tomcat7`\n* `catalina_base` location: `/var/lib/tomcat7`\n* Reference to `/etc/tomcat7/tomcat-users.xml` for user roles\n* Suggested packages for installation:\n\t+ Tomcat 7 Docs\n\t+ Tomcat 7 Example\n\t+ Tomcat 7 Admin\n\n\nGénère un rapport croisé en suivant les instructions précédentes, incluant un tableau chronologique des échanges entre CLIENT et SUPPORT. Utilise le format suivant pour le tableau :\n| ÉMETTEUR | TYPE | DATE | CONTENU | ÉLÉMENTS VISUELS |\n| --- | --- | --- | --- | --- |\n| CLIENT | question | date | texte de la question | éléments pertinents des images |\n| SUPPORT | réponse | date | texte de la réponse | éléments pertinents des images |\n\nCe tableau doit synthétiser les échanges tout en intégrant les données pertinentes des images avec le maximum de contexte technique.",
"prompt_en": "[ENGLISH RESPONSE REQUESTED]\n\nHere are the analysis data for a support ticket:\n\n=== Ticket analysis ====\n{'Response': 'Insufficient ticket content for analysis', 'Response_en': 'Insufficient content content for analysis', 'error': true, 'metadata': {'timestamp': '20250423_163657', 'source_agent': 'agenticketalyser', 'tick_id': 't11143'}}}}}}}}}}\n\n=== Image analyzes ====\n--- Image: image.png ---\n** Image Analysis **\n\nThe Provided Image Appears to Be A Screenshot of A Web Page Displaying the Default Tomcat Home Page. The page indicates that tomcat has been set up successfully.\n\n** Detaled Description **\n\n* The Top of the Page Displays The Text \"If you are seeing this page via a web browser, it means you've setup tomcat successfully. Congratulations!\"\n* Below this message, there is section titled \"Tomcat veterans might be pleased to read\" that provids about the system instance of tomcat, include the rentals of `catalina_home 'and` catalina_base`.\n* The page also suggests installing additional packages:\n\t+ Tomcat 7 Docs: Allows Browsing Local Documentation\n\t+ Tomcat 7 Example: Provids Access to Servlet and JSP Example\n\t+ Tomcat 7 Admin: Instalels Two Web Applications for Managing the Tomcat Instance (Manager Webapp and Host-Manager Webapp)\n* A Note at the Bottom of the page mentions Security Restrictions for using the Manager Webapp and Host-Manager Webapp, Referencing User Roles Defined in `/etc/Tomcat7/Tomcat -use.xm\n\n** Error messages, Technical Information, Or interface elements **\n\n* No error messages are visible on this page.\n* The Following Technical Information is present:\n\t+ Tomcat version: 7\n\t+ `catalina_home 'rental:`/usr/share/tomcat7'\n\t+ `catalina_base` rental:`/var/lib/tomcat7`\n\t+ Reference to `/etc/tomcat7/tomcat -use.xm\n\n** Relation to support Ticket Context **\n\nThe support tick context is insufficient for analysis, but this image suggests that the ISSUE Might be related to tomcat setup or configuration. The Fact that the Default Tomcat Home Page is Displayed Indicates that Tomcat is running, but there may be from specific applications or configurations.\n\n** Numbers version, status indicators, gold dates **\n\n* Tomcat version: 7\n* No status indicators or dates are visible on this page.\n\n** Extracted Technical Details **\n\n* `Catalina_home 'Rental:`/usr/Share/Tomcat7'\n* `catalina_base` rental:`/var/lib/tomcat7`\n* Reference to `/etc/tomcat7/tomcat -use.xm\n* Suggestized Packages for Installation:\n\t+ Tomcat 7 Docs\n\t+ Tomcat 7 Example\n\t+ Tomcat 7 admin\n\n\nGenerates a cross report following the previous instructions, including a chronological table of exchanges between customers and support. Use the following format for the table:\n| Transmitter | Type | Date | Contents | Visual elements |\n| --- | --- | --- | --- | --- |\n| Customer | Question | Date | Text of the question | Relevant elements of images |\n| Support | Answer | Date | Response text | Relevant elements of images |\n\nThis table must synthesize exchanges while integrating the relevant data of images with the maximum technical context.",
"response": "** Rapport croisé **\n\n**Résumé**\nL'analyse des billets de support révèle un contenu insuffisant pour une analyse approfondie, mais l'image fournie suggère que le problème pourrait être lié à la configuration ou à la configuration de Tomcat.\n\n** Tableau chronologique des échanges **\n\n| Émetteur | Type | Date | Contenu | Éléments visuels |\n| --- | --- | --- | --- | --- |\n| Client | Question | Inconnu | Contenu insuffisant pour l'analyse | - |\n| Support | Demande de plus d'informations | Inconnu | Demande de détails supplémentaires pour aider le problème | - |\n| Client | Réponse | 2025-04-23 16:36:57 | Image fournie (image.png) de la page d'accueil par défaut de Tomcat | Capture d'écran de la page d'accueil par défaut de Tomcat, indiquant une configuration réussie |\n\n** Analyse d'image **\n\nL'image fournie est une capture d'écran de la page d'accueil par défaut de Tomcat, ce qui indique que Tomcat a été configuré avec succès. La page affiche des informations techniques telles que:\n\n* Version Tomcat: 7\n* `Catalina_Home` Rental:` / usr / share / tomcat7`\n* `Catalina_Base` Rental:` / var / lib / tomcat7`\n* Référence à `/ etc / tomcat7 / tomcat-users.xml`\n\n** Contexte technique **\n\nL'image suggère que le problème pourrait être lié à la configuration ou à la configuration de Tomcat. Le fait que la page d'accueil de Tomcat par défaut s'affiche indique que Tomcat est en cours d'exécution, mais il peut y avoir des problèmes avec des applications ou des configurations spécifiques.\n\n** Recommandations **\n\n* Demandez des informations supplémentaires au client pour clarifier le problème.\n* Vérifiez la configuration et la configuration de Tomcat pour vous assurer qu'elle correspond aux paramètres attendus.\n* Envisagez d'installer des packages suggérés (Tomcat 7 Docs, Tomcat 7 Exemple, Tomcat 7 Admin) pour fournir des fonctionnalités supplémentaires.\n\nRemarque: Le tableau ne comprend qu'un seul échange entre le client et le support, car il n'y a pas suffisamment d'informations pour créer un tableau chronologique plus détaillé.",
"response_en": "**Cross Report**\n\n**Summary**\nThe support ticket analysis reveals insufficient content for a thorough analysis, but the provided image suggests that the issue might be related to Tomcat setup or configuration.\n\n**Chronological Table of Exchanges**\n\n| Transmitter | Type | Date | Contents | Visual elements |\n| --- | --- | --- | --- | --- |\n| Customer | Question | Unknown | Insufficient content for analysis | - |\n| Support | Request for more information | Unknown | Request for additional details to assist with the issue | - |\n| Customer | Response | 2025-04-23 16:36:57 | Provided image (image.png) of Tomcat default home page | Screenshot of Tomcat default home page, indicating successful setup |\n\n**Image Analysis**\n\nThe provided image is a screenshot of the Tomcat default home page, which indicates that Tomcat has been set up successfully. The page displays technical information such as:\n\n* Tomcat version: 7\n* `catalina_home` rental: `/usr/share/tomcat7`\n* `catalina_base` rental: `/var/lib/tomcat7`\n* Reference to `/etc/tomcat7/tomcat-users.xml`\n\n**Technical Context**\n\nThe image suggests that the issue might be related to Tomcat setup or configuration. The fact that the default Tomcat home page is displayed indicates that Tomcat is running, but there may be issues with specific applications or configurations.\n\n**Recommendations**\n\n* Request additional information from the customer to clarify the issue.\n* Verify the Tomcat configuration and setup to ensure it matches the expected settings.\n* Consider installing suggested packages (Tomcat 7 Docs, Tomcat 7 Example, Tomcat 7 Admin) to provide additional functionality.\n\nNote: The table only includes one exchange between the customer and support, as there is insufficient information to create a more detailed chronological table.",
"metadata": {
"ticket_id": "T11143",
"timestamp": "20250423_164104",
"source_agent": "AgentReportGenerator",
"model_info": {
"model": "llama3.2-vision:90b-instruct-q8_0",
"temperature": 0.2,
"top_p": 0.8,
"max_tokens": 8000,
"presence_penalty": 0,
"frequency_penalty": 0,
"stop": [],
"stream": false,
"n": 1
},
"language": "en-fr"
}
}

View File

@ -0,0 +1,40 @@
RÉSULTATS DE L'ANALYSE RAPPORT_FINAL - TICKET T11143
================================================================================
** Rapport croisé **
**Résumé**
L'analyse des billets de support révèle un contenu insuffisant pour une analyse approfondie, mais l'image fournie suggère que le problème pourrait être lié à la configuration ou à la configuration de Tomcat.
** Tableau chronologique des échanges **
| Émetteur | Type | Date | Contenu | Éléments visuels |
| --- | --- | --- | --- | --- |
| Client | Question | Inconnu | Contenu insuffisant pour l'analyse | - |
| Support | Demande de plus d'informations | Inconnu | Demande de détails supplémentaires pour aider le problème | - |
| Client | Réponse | 2025-04-23 16:36:57 | Image fournie (image.png) de la page d'accueil par défaut de Tomcat | Capture d'écran de la page d'accueil par défaut de Tomcat, indiquant une configuration réussie |
** Analyse d'image **
L'image fournie est une capture d'écran de la page d'accueil par défaut de Tomcat, ce qui indique que Tomcat a été configuré avec succès. La page affiche des informations techniques telles que:
* Version Tomcat: 7
* `Catalina_Home` Rental:` / usr / share / tomcat7`
* `Catalina_Base` Rental:` / var / lib / tomcat7`
* Référence à `/ etc / tomcat7 / tomcat-users.xml`
** Contexte technique **
L'image suggère que le problème pourrait être lié à la configuration ou à la configuration de Tomcat. Le fait que la page d'accueil de Tomcat par défaut s'affiche indique que Tomcat est en cours d'exécution, mais il peut y avoir des problèmes avec des applications ou des configurations spécifiques.
** Recommandations **
* Demandez des informations supplémentaires au client pour clarifier le problème.
* Vérifiez la configuration et la configuration de Tomcat pour vous assurer qu'elle correspond aux paramètres attendus.
* Envisagez d'installer des packages suggérés (Tomcat 7 Docs, Tomcat 7 Exemple, Tomcat 7 Admin) pour fournir des fonctionnalités supplémentaires.
Remarque: Le tableau ne comprend qu'un seul échange entre le client et le support, car il n'y a pas suffisamment d'informations pour créer un tableau chronologique plus détaillé.
================================================================================
Fichier original: rapport_final_llama3.2-vision_90b-instruct-q8_0_results.json

View File

@ -0,0 +1,14 @@
{
"is_relevant": false,
"reason": "ERROR: Erreur d'accès ou format d'image invalide",
"raw_response": "",
"error": true,
"metadata": {
"image_path": "output/ticket_T11143/T11143_20250422_084617/attachments/a20f7697fd5e1d1fca3296c6d01228220e0e112c46b4440cc938f74d10934e98.gif",
"image_name": "a20f7697fd5e1d1fca3296c6d01228220e0e112c46b4440cc938f74d10934e98.gif",
"ticket_id": "T11143",
"timestamp": "20250423_163747",
"error": true,
"source_agent": "AgentImageSorter"
}
}

View File

@ -0,0 +1,8 @@
RÉSULTATS DE L'ANALYSE TRI_IMAGE - TICKET T11143
================================================================================
================================================================================
Fichier original: tri_image_image_llama3.2-vision_90b-instruct-q8_0_results.json

View File

@ -1,98 +0,0 @@
[
{
"is_relevant": true,
"reason": "Yes.\n\nThis image appears to be a screenshot of the default Apache Tomcat homepage, indicating that the server is running successfully. However, this does not necessarily indicate a technical support issue. In fact, it suggests that the setup process has been completed correctly. \n\nA technical support issue would typically involve an error message or unexpected behavior, which is not present in this image. Therefore, while the image may be relevant to a technical support conversation (e.g., as proof of successful installation), it does not in itself indicate a problem requiring support.",
"raw_response": "Yes.\n\nThis image appears to be a screenshot of the default Apache Tomcat homepage, indicating that the server is running successfully. However, this does not necessarily indicate a technical support issue. In fact, it suggests that the setup process has been completed correctly. \n\nA technical support issue would typically involve an error message or unexpected behavior, which is not present in this image. Therefore, while the image may be relevant to a technical support conversation (e.g., as proof of successful installation), it does not in itself indicate a problem requiring support.",
"ocr_fr": "Apache Tomcat x +\n\nCG A 3 zkl.brg-lab.com\n\n@ Andre 7 Demo 7% Devmat @ Base modèle\n\nIt works !\n\nIf you're seeing this page via a web browser, it means you've setup Tomcat successfully. Congratulations!\n\nThis is the default Tomcat home page. It can be found on the local filesystem at: /var/lib/tomcat7/webapps/ROOT/index.html\n\nTomcat}? veterans might be pleased to learn that this system instance of Tomcat is installed with CATALINA_HOME in /usr/share/tomcat7 and CATALINA BASE in /var/lib/tomcat7, following the rules from /usr/share/doc/tomcat7-common/RUNNING. txt. gz.\nYou might consider installing the following packages, if you haven't already done so:\n\ntomcat7-docs: This package installs a web application that allows to browse the Tomcat 7 documentation locally. Once installed, you can access it by clicking here.\n\ntomcat7-examples: This package installs a web application that allows to access the Tomcat 7 Servlet and JSP examples. Once installed, you can access it by clicking here.\n\ntomcat7-admin: This package installs two web applications that can help managing this Tomcat instance. Once installed, you can access the manager webapp and the host-manager webapp.\n\nNOTE: For security reasons, using the manager webapp is restricted to users with role \"“manager-gui\". The host-manager webapp is restricted to users with role \"admin-gui\". Users are defined in /etc/tomcat7/tomcat-users.xml.",
"ocr_en": "Apache Tomcat x +\n\nCG A 3 zkl.brg-lab.com\n\n@ Andre 7 Demo 7% Devmat @ model base\n\nIt works!\n\nIf you are seeing this page via a web browser, it means you've setup tomcat successfully. Congratulations!\n\nThis is the Default Tomcat Home Page. It can be found on the local Filesystem at: /var/lib/tomcat7/webapps/root/index.html\n\nTomcat}? veterans might be pleased to read this system instance of tomcat is installed with catalina_home in/usr/tomcat7 and catalina base in/var/lib/tomcat7, following the rules from/usr/share/doc/tomcat7-common/Running. TXT. Gz.\nYou might consider installing the following packages, if you have alreni done so:\n\nTomcat7-Docs: This Package Installes A Web Application that Allows to Browse the Tomcat 7 Locally documentation. Once Installed, you can access it by clicking here.\n\nTomcat7-Example: This Package Installes A Web Application that Allows to Access the Tomcat 7 Servlet and JSP Examples. Once Installed, you can access it by clicking here.\n\nTomcat7-Admin: This Package Installes Two Web Applications that can help managing this tomcat instance. Once Installed, you can access the Manager Webapp and the Host-Manager Webapp.\n\nNote: For Security Reasons, Using the Manager Webapp is restricted to users with Role \"Manager-Gui\". The Host-Manager Webapp is restricted to users with role \"admin-guui\". USERS are defined in /etc/tomcat7/tomcat-users.xml.",
"metadata": {
"image_path": "output/ticket_T11143/T11143_20250422_084617/attachments/image.png",
"image_name": "image.png",
"timestamp": "20250423_142009",
"model_info": {
"model": "llama3.2-vision:90b-instruct-q8_0",
"temperature": 0.2,
"top_p": 0.8,
"max_tokens": 300,
"presence_penalty": 0,
"frequency_penalty": 0,
"stop": [],
"stream": false,
"n": 1
},
"source_agent": "AgentImageSorter"
}
},
{
"is_relevant": true,
"reason": "Yes.\n\nThis image appears to be a screenshot from the BRG_Lab software system, showing a specific page related to concrete testing (Béton C9 Foumétew Bo 4 Masse). The text includes details about a sample received on a particular date, material information, and an error message or prompt (\"le de trouver Fadeessé IP du serveur de zk1.brg-lab.com\") that suggests a technical issue with connecting to the server. This information is relevant for a technical support issue as it provides context about the problem being experienced by the user.",
"raw_response": "Yes.\n\nThis image appears to be a screenshot from the BRG_Lab software system, showing a specific page related to concrete testing (Béton C9 Foumétew Bo 4 Masse). The text includes details about a sample received on a particular date, material information, and an error message or prompt (\"le de trouver Fadeessé IP du serveur de zk1.brg-lab.com\") that suggests a technical issue with connecting to the server. This information is relevant for a technical support issue as it provides context about the problem being experienced by the user.",
"ocr_fr": "[6] 25 giraudbrg-lobcom/BRG-LAB/PAGE_ programmeEssai/xE4AAHDVNGOAA\n\n| BRGLAS CD Béton C9 foumétew bo 4 Masse\n\nEchantillon n°2500075 réceptionné le 02/04/2025 par BOLLEE Victor - prlevii Le 02/04/2025 por BOLLEE Victor n° prétèvement : 25-6007\nMatériau Sable 0/20 - CARRIERE ADCEG\n\nNREGISTRER\n\nLMPRMER\n\nle de trouver Fadeessé IP du serveur de zk1.brg-lab.com.",
"ocr_en": "[6] 25 GIRAUDBRG-LOBCOM/BRG-LAB/PAGE_ PROGRAMESSAI/XE4AAHDVNGOAAA\n\n| Brglas CD concrete C9 Foumetew Bo 4 Mass\n\nSample n ° 2500075 received on 02/04/2025 by Bollee Victor - PRLEVII on 02/04/2025 POR BOLLEE Victor N ° PRETREMENT: 25-6007\nSand material 0/20 - CARRIERE ADCEG\n\nRegister\n\nLmprmer\n\nThe to find Fadeessé IP of the ZK1.brg-lab.com server.",
"metadata": {
"image_path": "output/ticket_T11143/T11143_20250422_084617/attachments/image_145435.png",
"image_name": "image_145435.png",
"timestamp": "20250423_142039",
"model_info": {
"model": "llama3.2-vision:90b-instruct-q8_0",
"temperature": 0.2,
"top_p": 0.8,
"max_tokens": 300,
"presence_penalty": 0,
"frequency_penalty": 0,
"stop": [],
"stream": false,
"n": 1
},
"source_agent": "AgentImageSorter"
}
},
{
"is_relevant": false,
"reason": "No.\n\nThe image appears to be a jumbled collection of characters, including French text, but it does not contain any meaningful information that would be relevant to a technical support issue. The text is likely the result of an OCR (Optical Character Recognition) error or a corrupted file, rather than an actual screenshot or log from the BRG_Lab software system.",
"raw_response": "No.\n\nThe image appears to be a jumbled collection of characters, including French text, but it does not contain any meaningful information that would be relevant to a technical support issue. The text is likely the result of an OCR (Optical Character Recognition) error or a corrupted file, rather than an actual screenshot or log from the BRG_Lab software system.",
"ocr_fr": "",
"ocr_en": "",
"metadata": {
"image_path": "output/ticket_T11143/T11143_20250422_084617/attachments/543d7da1b54c29ff43ce5712d1a9aa4962ed21795c4e943fcb8cb84fd4d7465a.jpg",
"image_name": "543d7da1b54c29ff43ce5712d1a9aa4962ed21795c4e943fcb8cb84fd4d7465a.jpg",
"timestamp": "20250423_142058",
"model_info": {
"model": "llama3.2-vision:90b-instruct-q8_0",
"temperature": 0.2,
"top_p": 0.8,
"max_tokens": 300,
"presence_penalty": 0,
"frequency_penalty": 0,
"stop": [],
"stream": false,
"n": 1
},
"source_agent": "AgentImageSorter"
}
},
{
"is_relevant": true,
"reason": "Yes.\n\nThe image appears to be a screenshot of an error message or a system log from the BRG_Lab software system, which is likely relevant to a technical support issue. The presence of French text and what seems to be a stack trace or debug information suggests that it may be related to a specific problem or bug that the user is experiencing with the software.",
"raw_response": "Yes.\n\nThe image appears to be a screenshot of an error message or a system log from the BRG_Lab software system, which is likely relevant to a technical support issue. The presence of French text and what seems to be a stack trace or debug information suggests that it may be related to a specific problem or bug that the user is experiencing with the software.",
"ocr_fr": "",
"ocr_en": "",
"metadata": {
"image_path": "output/ticket_T11143/T11143_20250422_084617/attachments/a20f7697fd5e1d1fca3296c6d01228220e0e112c46b4440cc938f74d10934e98.gif",
"image_name": "a20f7697fd5e1d1fca3296c6d01228220e0e112c46b4440cc938f74d10934e98.gif",
"timestamp": "20250423_142115",
"model_info": {
"model": "llama3.2-vision:90b-instruct-q8_0",
"temperature": 0.2,
"top_p": 0.8,
"max_tokens": 300,
"presence_penalty": 0,
"frequency_penalty": 0,
"stop": [],
"stream": false,
"n": 1
},
"source_agent": "AgentImageSorter"
}
}
]

View File

@ -1,40 +0,0 @@
RÉSULTATS DE L'ANALYSE TRI_IMAGE - TICKET T11143
================================================================================
--- ÉLÉMENT 1 ---
Yes.
This image appears to be a screenshot of the default Apache Tomcat homepage, indicating that the server is running successfully. However, this does not necessarily indicate a technical support issue. In fact, it suggests that the setup process has been completed correctly.
A technical support issue would typically involve an error message or unexpected behavior, which is not present in this image. Therefore, while the image may be relevant to a technical support conversation (e.g., as proof of successful installation), it does not in itself indicate a problem requiring support.
----------------------------------------
--- ÉLÉMENT 2 ---
Yes.
This image appears to be a screenshot from the BRG_Lab software system, showing a specific page related to concrete testing (Béton C9 Foumétew Bo 4 Masse). The text includes details about a sample received on a particular date, material information, and an error message or prompt ("le de trouver Fadeessé IP du serveur de zk1.brg-lab.com") that suggests a technical issue with connecting to the server. This information is relevant for a technical support issue as it provides context about the problem being experienced by the user.
----------------------------------------
--- ÉLÉMENT 3 ---
No.
The image appears to be a jumbled collection of characters, including French text, but it does not contain any meaningful information that would be relevant to a technical support issue. The text is likely the result of an OCR (Optical Character Recognition) error or a corrupted file, rather than an actual screenshot or log from the BRG_Lab software system.
----------------------------------------
--- ÉLÉMENT 4 ---
Yes.
The image appears to be a screenshot of an error message or a system log from the BRG_Lab software system, which is likely relevant to a technical support issue. The presence of French text and what seems to be a stack trace or debug information suggests that it may be related to a specific problem or bug that the user is experiencing with the software.
----------------------------------------
================================================================================
Fichier original: tri_image_llama3.2-vision:90b-instruct-q8_0_results.json

View File

@ -0,0 +1,59 @@
[
{
"is_relevant": true,
"reason": "Yes.\n\nThis image appears to be a technical screenshot of a Tomcat server setup success page, which may be relevant for troubleshooting or configuration purposes related to the BRG_Lab software from CBAO enterprise, especially if the software uses Tomcat as part of its infrastructure. The text detected in the image provides information about the Tomcat installation and configuration, which could be useful for technical support issues.",
"raw_response": "Yes.\n\nThis image appears to be a technical screenshot of a Tomcat server setup success page, which may be relevant for troubleshooting or configuration purposes related to the BRG_Lab software from CBAO enterprise, especially if the software uses Tomcat as part of its infrastructure. The text detected in the image provides information about the Tomcat installation and configuration, which could be useful for technical support issues.",
"ocr_used": true,
"metadata": {
"image_path": "output/ticket_T11143/T11143_20250422_084617/attachments/image.png",
"image_name": "image.png",
"timestamp": "20250423_163722",
"model_info": {
"model": "llama3.2-vision:90b-instruct-q8_0",
"temperature": 0.1,
"top_p": 0.8,
"max_tokens": 1000
},
"ticket_id": "T11143",
"source_agent": "AgentImageSorter"
}
},
{
"is_relevant": false,
"reason": "No\n\nThe text detected in the image appears to be unrelated to technical support or software issues, and the content seems to be a personal or generic message rather than a technical screenshot or error message.",
"raw_response": "No\n\nThe text detected in the image appears to be unrelated to technical support or software issues, and the content seems to be a personal or generic message rather than a technical screenshot or error message.",
"ocr_used": true,
"metadata": {
"image_path": "output/ticket_T11143/T11143_20250422_084617/attachments/image_145435.png",
"image_name": "image_145435.png",
"timestamp": "20250423_163735",
"model_info": {
"model": "llama3.2-vision:90b-instruct-q8_0",
"temperature": 0.1,
"top_p": 0.8,
"max_tokens": 1000
},
"ticket_id": "T11143",
"source_agent": "AgentImageSorter"
}
},
{
"is_relevant": false,
"reason": "No\n\nThe provided text appears to be a jumbled collection of characters, likely a corrupted image file, rather than an actual image. It does not contain any visual information that could be related to a technical support issue with BRG_Lab software.",
"raw_response": "No\n\nThe provided text appears to be a jumbled collection of characters, likely a corrupted image file, rather than an actual image. It does not contain any visual information that could be related to a technical support issue with BRG_Lab software.",
"ocr_used": false,
"metadata": {
"image_path": "output/ticket_T11143/T11143_20250422_084617/attachments/543d7da1b54c29ff43ce5712d1a9aa4962ed21795c4e943fcb8cb84fd4d7465a.jpg",
"image_name": "543d7da1b54c29ff43ce5712d1a9aa4962ed21795c4e943fcb8cb84fd4d7465a.jpg",
"timestamp": "20250423_163747",
"model_info": {
"model": "llama3.2-vision:90b-instruct-q8_0",
"temperature": 0.1,
"top_p": 0.8,
"max_tokens": 1000
},
"ticket_id": "T11143",
"source_agent": "AgentImageSorter"
}
}
]

View File

@ -0,0 +1,30 @@
RÉSULTATS DE L'ANALYSE TRI_IMAGE - TICKET T11143
================================================================================
--- ÉLÉMENT 1 ---
Yes.
This image appears to be a technical screenshot of a Tomcat server setup success page, which may be relevant for troubleshooting or configuration purposes related to the BRG_Lab software from CBAO enterprise, especially if the software uses Tomcat as part of its infrastructure. The text detected in the image provides information about the Tomcat installation and configuration, which could be useful for technical support issues.
----------------------------------------
--- ÉLÉMENT 2 ---
No
The text detected in the image appears to be unrelated to technical support or software issues, and the content seems to be a personal or generic message rather than a technical screenshot or error message.
----------------------------------------
--- ÉLÉMENT 3 ---
No
The provided text appears to be a jumbled collection of characters, likely a corrupted image file, rather than an actual image. It does not contain any visual information that could be related to a technical support issue with BRG_Lab software.
----------------------------------------
================================================================================
Fichier original: tri_image_llama3.2-vision_90b-instruct-q8_0_results.json

View File

@ -0,0 +1,36 @@
RAPPORT D'ANALYSE DU TICKET T11143
==================================================
** Rapport croisé **
**Résumé**
L'analyse des billets de support révèle un contenu insuffisant pour une analyse approfondie, mais l'image fournie suggère que le problème pourrait être lié à la configuration ou à la configuration de Tomcat.
** Tableau chronologique des échanges **
| Émetteur | Type | Date | Contenu | Éléments visuels |
| --- | --- | --- | --- | --- |
| Client | Question | Inconnu | Contenu insuffisant pour l'analyse | - |
| Support | Demande de plus d'informations | Inconnu | Demande de détails supplémentaires pour aider le problème | - |
| Client | Réponse | 2025-04-23 16:36:57 | Image fournie (image.png) de la page d'accueil par défaut de Tomcat | Capture d'écran de la page d'accueil par défaut de Tomcat, indiquant une configuration réussie |
** Analyse d'image **
L'image fournie est une capture d'écran de la page d'accueil par défaut de Tomcat, ce qui indique que Tomcat a été configuré avec succès. La page affiche des informations techniques telles que:
* Version Tomcat: 7
* `Catalina_Home` Rental:` / usr / share / tomcat7`
* `Catalina_Base` Rental:` / var / lib / tomcat7`
* Référence à `/ etc / tomcat7 / tomcat-users.xml`
** Contexte technique **
L'image suggère que le problème pourrait être lié à la configuration ou à la configuration de Tomcat. Le fait que la page d'accueil de Tomcat par défaut s'affiche indique que Tomcat est en cours d'exécution, mais il peut y avoir des problèmes avec des applications ou des configurations spécifiques.
** Recommandations **
* Demandez des informations supplémentaires au client pour clarifier le problème.
* Vérifiez la configuration et la configuration de Tomcat pour vous assurer qu'elle correspond aux paramètres attendus.
* Envisagez d'installer des packages suggérés (Tomcat 7 Docs, Tomcat 7 Exemple, Tomcat 7 Admin) pour fournir des fonctionnalités supplémentaires.
Remarque: Le tableau ne comprend qu'un seul échange entre le client et le support, car il n'y a pas suffisamment d'informations pour créer un tableau chronologique plus détaillé.

View File

@ -0,0 +1,36 @@
ANALYSIS REPORT FOR TICKET T11143
==================================================
**Cross Report**
**Summary**
The support ticket analysis reveals insufficient content for a thorough analysis, but the provided image suggests that the issue might be related to Tomcat setup or configuration.
**Chronological Table of Exchanges**
| Transmitter | Type | Date | Contents | Visual elements |
| --- | --- | --- | --- | --- |
| Customer | Question | Unknown | Insufficient content for analysis | - |
| Support | Request for more information | Unknown | Request for additional details to assist with the issue | - |
| Customer | Response | 2025-04-23 16:36:57 | Provided image (image.png) of Tomcat default home page | Screenshot of Tomcat default home page, indicating successful setup |
**Image Analysis**
The provided image is a screenshot of the Tomcat default home page, which indicates that Tomcat has been set up successfully. The page displays technical information such as:
* Tomcat version: 7
* `catalina_home` rental: `/usr/share/tomcat7`
* `catalina_base` rental: `/var/lib/tomcat7`
* Reference to `/etc/tomcat7/tomcat-users.xml`
**Technical Context**
The image suggests that the issue might be related to Tomcat setup or configuration. The fact that the default Tomcat home page is displayed indicates that Tomcat is running, but there may be issues with specific applications or configurations.
**Recommendations**
* Request additional information from the customer to clarify the issue.
* Verify the Tomcat configuration and setup to ensure it matches the expected settings.
* Consider installing suggested packages (Tomcat 7 Docs, Tomcat 7 Example, Tomcat 7 Admin) to provide additional functionality.
Note: The table only includes one exchange between the customer and support, as there is insufficient information to create a more detailed chronological table.

View File

@ -1,19 +1,23 @@
# utils/ocr_utils.py
from PIL import Image, ImageEnhance
from PIL import Image, ImageEnhance, ImageFilter
import pytesseract
import logging
import os
import io
import numpy as np
import cv2
from langdetect import detect, LangDetectException
logger = logging.getLogger("OCR")
def pretraiter_image(image_path: str) -> Image.Image:
def pretraiter_image(image_path: str, optimize_for_text: bool = True) -> Image.Image:
"""
Prétraite l'image pour améliorer la qualité de l'OCR.
Prétraite l'image pour améliorer la qualité de l'OCR avec des techniques avancées.
Args:
image_path: Chemin de l'image
optimize_for_text: Si True, applique des optimisations spécifiques pour le texte
Returns:
Image prétraitée
@ -24,14 +28,35 @@ def pretraiter_image(image_path: str) -> Image.Image:
# Convertir en niveaux de gris si l'image est en couleur
if img.mode != 'L':
img = img.convert('L')
# Conversion en array numpy pour traitement avec OpenCV
img_np = np.array(img)
if optimize_for_text:
# Appliquer une binarisation adaptative pour améliorer la lisibilité du texte
# Cette technique s'adapte aux variations de luminosité dans l'image
if img_np.dtype != np.uint8:
img_np = img_np.astype(np.uint8)
# Utiliser OpenCV pour la binarisation adaptative
img_np = cv2.adaptiveThreshold(
img_np, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 11, 2
)
# Débruitage pour éliminer les artefacts
img_np = cv2.fastNlMeansDenoising(img_np, None, 10, 7, 21)
# Reconvertir en image PIL
img = Image.fromarray(img_np)
# Améliorer le contraste
enhancer = ImageEnhance.Contrast(img)
img = enhancer.enhance(1.5) # Facteur de contraste 1.5
img = enhancer.enhance(2.0) # Augmenter le facteur de contraste
# Augmenter la netteté
enhancer = ImageEnhance.Sharpness(img)
img = enhancer.enhance(1.5) # Facteur de netteté 1.5
img = enhancer.enhance(2.0) # Augmenter le facteur de netteté
# Agrandir l'image si elle est petite
width, height = img.size
@ -44,58 +69,153 @@ def pretraiter_image(image_path: str) -> Image.Image:
return img
except Exception as e:
logger.error(f"Erreur lors du prétraitement de l'image {image_path}: {e}")
return Image.open(image_path) # Retourner l'image originale en cas d'erreur
# En cas d'erreur, retourner l'image originale
try:
return Image.open(image_path)
except:
# Si même l'ouverture simple échoue, retourner une image vide
logger.critical(f"Impossible d'ouvrir l'image {image_path}")
return Image.new('L', (100, 100), 255)
def detecter_langue_texte(texte: str) -> str:
"""
Détecte la langue principale d'un texte.
Args:
texte: Texte à analyser
Returns:
Code de langue ('fr', 'en', etc.) ou 'unknown' en cas d'échec
"""
if not texte or len(texte.strip()) < 10:
return "unknown"
try:
return detect(texte)
except LangDetectException:
return "unknown"
def extraire_texte(image_path: str, lang: str = "auto") -> tuple:
"""
Effectue un OCR sur une image avec détection automatique de la langue.
Args:
image_path: Chemin vers l'image
lang: Langue pour l'OCR ('auto', 'fra', 'eng', 'fra+eng')
Returns:
Tuple (texte extrait, langue détectée)
"""
if not os.path.exists(image_path) or not os.access(image_path, os.R_OK):
logger.warning(f"Image inaccessible ou introuvable: {image_path}")
return "", "unknown"
logger.info(f"Traitement OCR pour {image_path} (langue: {lang})")
# Prétraiter l'image avec différentes configurations
img_standard = pretraiter_image(image_path, optimize_for_text=False)
img_optimized = pretraiter_image(image_path, optimize_for_text=True)
# Configurer pytesseract
config = '--psm 3 --oem 3' # Page segmentation mode: 3 (auto), OCR Engine mode: 3 (default)
# Déterminer la langue pour l'OCR
ocr_lang = lang
if lang == "auto":
# Essayer d'extraire du texte avec plusieurs langues
try:
texte_fr = pytesseract.image_to_string(img_optimized, lang="fra", config=config)
texte_en = pytesseract.image_to_string(img_optimized, lang="eng", config=config)
texte_multi = pytesseract.image_to_string(img_optimized, lang="fra+eng", config=config)
# Choisir le meilleur résultat basé sur la longueur et la qualité
results = [
(texte_fr, "fra", len(texte_fr.strip())),
(texte_en, "eng", len(texte_en.strip())),
(texte_multi, "fra+eng", len(texte_multi.strip()))
]
results.sort(key=lambda x: x[2], reverse=True)
best_text, best_lang, _ = results[0]
# Détection secondaire basée sur le contenu
detected_lang = detecter_langue_texte(best_text)
if detected_lang in ["fr", "fra"]:
ocr_lang = "fra"
elif detected_lang in ["en", "eng"]:
ocr_lang = "eng"
else:
ocr_lang = best_lang
logger.info(f"Langue détectée: {ocr_lang}")
except Exception as e:
logger.warning(f"Détection de langue échouée: {e}, utilisation de fra+eng")
ocr_lang = "fra+eng"
# Réaliser l'OCR avec la langue choisie
try:
# Essayer d'abord avec l'image optimisée pour le texte
texte = pytesseract.image_to_string(img_optimized, lang=ocr_lang, config=config)
# Si le résultat est trop court, essayer avec l'image standard
if len(texte.strip()) < 10:
texte_standard = pytesseract.image_to_string(img_standard, lang=ocr_lang, config=config)
if len(texte_standard.strip()) > len(texte.strip()):
texte = texte_standard
logger.info("Utilisation du résultat de l'image standard (meilleur résultat)")
except Exception as ocr_err:
logger.warning(f"OCR échoué: {ocr_err}, tentative avec l'image originale")
# En cas d'échec, essayer avec l'image originale
try:
with Image.open(image_path) as original_img:
texte = pytesseract.image_to_string(original_img, lang=ocr_lang, config=config)
except Exception as e:
logger.error(f"OCR échoué complètement: {e}")
return "", "unknown"
# Nettoyer le texte
texte = texte.strip()
# Détecter la langue du texte extrait pour confirmation
detected_lang = detecter_langue_texte(texte) if texte else "unknown"
# Sauvegarder l'image prétraitée pour debug si OCR réussi
if texte:
try:
debug_dir = "debug_ocr"
os.makedirs(debug_dir, exist_ok=True)
img_name = os.path.basename(image_path)
img_optimized.save(os.path.join(debug_dir, f"optimized_{img_name}"), format="JPEG")
img_standard.save(os.path.join(debug_dir, f"standard_{img_name}"), format="JPEG")
# Sauvegarder aussi le texte extrait
with open(os.path.join(debug_dir, f"ocr_{img_name}.txt"), "w", encoding="utf-8") as f:
f.write(f"OCR Langue: {ocr_lang}\n")
f.write(f"Langue détectée: {detected_lang}\n")
f.write("-" * 50 + "\n")
f.write(texte)
logger.info(f"Images prétraitées et résultat OCR sauvegardés dans {debug_dir}")
except Exception as e:
logger.warning(f"Impossible de sauvegarder les fichiers de débogage: {e}")
# Journaliser le résultat
logger.info(f"OCR réussi [{image_path}] — {len(texte)} caractères: {texte[:100]}...")
else:
logger.warning(f"OCR vide (aucun texte détecté) pour {image_path}")
return texte, detected_lang
def extraire_texte_fr(image_path: str) -> str:
"""
Effectue un OCR sur une image en langue française.
Retourne le texte brut extrait (chaîne vide si aucun texte détecté ou en cas d'erreur).
Chaque appel est isolé et tracé dans les logs.
Effectue un OCR sur une image en langue française (pour compatibilité).
Utilise la nouvelle fonction plus avancée avec détection automatique.
Args:
image_path: Chemin vers l'image
Returns:
Texte extrait
"""
try:
if not os.path.exists(image_path) or not os.access(image_path, os.R_OK):
logger.warning(f"Image inaccessible ou introuvable: {image_path}")
return ""
logger.info(f"Traitement OCR pour {image_path}")
# Configurer pytesseract
config = '--psm 3 --oem 3' # Page segmentation mode: 3 (auto), OCR Engine mode: 3 (default)
# Prétraiter l'image
img = pretraiter_image(image_path)
logger.info(f"Image prétraitée: dimensions={img.size}, mode={img.mode}")
# Réaliser l'OCR avec fallback
try:
texte = pytesseract.image_to_string(img, lang="fra", config=config)
except Exception as ocr_err:
logger.warning(f"Première tentative OCR échouée: {ocr_err}, tentative avec image originale")
# En cas d'échec, essayer avec l'image originale
with Image.open(image_path) as original_img:
texte = pytesseract.image_to_string(original_img, lang="fra", config=config)
# Nettoyer le texte
texte = texte.strip()
# Sauvegarder l'image prétraitée pour debug si OCR réussi
if texte:
try:
debug_dir = "debug_ocr"
os.makedirs(debug_dir, exist_ok=True)
img_name = os.path.basename(image_path)
img.save(os.path.join(debug_dir, f"pretreated_{img_name}"), format="JPEG")
logger.info(f"Image prétraitée sauvegardée dans {debug_dir}/pretreated_{img_name}")
except Exception as e:
logger.warning(f"Impossible de sauvegarder l'image prétraitée: {e}")
# Journaliser le résultat
if texte:
logger.info(f"OCR réussi [{image_path}] — {len(texte)} caractères: {texte[:100]}...")
else:
logger.warning(f"OCR vide (aucun texte détecté) pour {image_path}")
return texte
except Exception as e:
logger.error(f"Erreur lors de l'OCR de {image_path}: {e}")
return ""
texte, _ = extraire_texte(image_path, lang="auto")
return texte

View File

@ -5,27 +5,162 @@ import json
import os
from datetime import datetime
import logging
from typing import Optional
from typing import Optional, Dict, Any
import hashlib
from functools import lru_cache
logger = logging.getLogger("Translate")
def fr_to_en(text: str) -> str:
try:
if not text.strip():
return ""
return GoogleTranslator(source="fr", target="en").translate(text)
except Exception as e:
logger.error(f"Traduction FR->EN échouée: {e}")
# Cache global pour les traductions (conservé entre les appels)
TRANSLATION_CACHE: Dict[str, str] = {}
MAX_CACHE_SIZE = 1000 # Nombre maximal d'entrées dans le cache
def _get_cache_key(text: str, source: str, target: str) -> str:
"""
Génère une clé de cache unique pour une traduction donnée.
Args:
text: Texte à traduire
source: Langue source
target: Langue cible
Returns:
Clé de cache
"""
# Limiter la taille du texte pour la clé de cache
text_snippet = text[:500] if text else ""
hash_key = hashlib.md5(f"{text_snippet}|{source}|{target}".encode('utf-8')).hexdigest()
return hash_key
def _clean_cache_if_needed() -> None:
"""
Nettoie le cache si sa taille dépasse la limite maximale.
"""
global TRANSLATION_CACHE
if len(TRANSLATION_CACHE) > MAX_CACHE_SIZE:
# Garder seulement 75% des entrées les plus récentes (approximativement)
items = list(TRANSLATION_CACHE.items())
keep_count = int(MAX_CACHE_SIZE * 0.75)
TRANSLATION_CACHE = dict(items[-keep_count:])
logger.info(f"Cache de traduction nettoyé : {len(TRANSLATION_CACHE)} entrées conservées")
def translate_text(text: str, source: str, target: str, use_cache: bool = True) -> str:
"""
Fonction générique de traduction avec gestion de cache.
Args:
text: Texte à traduire
source: Langue source ('fr', 'en', etc.)
target: Langue cible ('fr', 'en', etc.)
use_cache: Si True, utilise le cache de traduction
Returns:
Texte traduit
"""
if not text or not text.strip():
return ""
# Vérifier le cache
if use_cache:
cache_key = _get_cache_key(text, source, target)
if cache_key in TRANSLATION_CACHE:
logger.debug(f"Traduction récupérée du cache pour {source}->{target}")
return TRANSLATION_CACHE[cache_key]
# Limiter la taille du texte pour éviter les problèmes avec l'API
# Les longs textes sont découpés et traduits par morceaux
MAX_TEXT_LENGTH = 5000
if len(text) > MAX_TEXT_LENGTH:
chunks = _split_text_into_chunks(text, MAX_TEXT_LENGTH)
translated_chunks = []
for chunk in chunks:
translated_chunk = translate_text(chunk, source, target, use_cache)
translated_chunks.append(translated_chunk)
result = ' '.join(translated_chunks)
else:
try:
translator = GoogleTranslator(source=source, target=target)
result = translator.translate(text)
except Exception as e:
logger.error(f"Traduction {source}->{target} échouée: {e}")
return text # Retourner le texte original en cas d'erreur
# Mettre en cache
if use_cache:
cache_key = _get_cache_key(text, source, target)
TRANSLATION_CACHE[cache_key] = result
_clean_cache_if_needed()
return result
def _split_text_into_chunks(text: str, max_length: int) -> list:
"""
Découpe un texte en morceaux plus petits en respectant les phrases.
Args:
text: Texte à découper
max_length: Longueur maximale de chaque morceau
Returns:
Liste des morceaux de texte
"""
chunks = []
current_chunk = ""
# Split by paragraphs
paragraphs = text.split('\n')
for paragraph in paragraphs:
# If paragraph is too long, split by sentences
if len(paragraph) > max_length:
sentences = paragraph.replace('. ', '.\n').replace('! ', '!\n').replace('? ', '?\n').split('\n')
for sentence in sentences:
if len(current_chunk) + len(sentence) + 1 <= max_length:
if current_chunk:
current_chunk += ' ' + sentence
else:
current_chunk = sentence
else:
chunks.append(current_chunk)
current_chunk = sentence
else:
if len(current_chunk) + len(paragraph) + 1 <= max_length:
if current_chunk:
current_chunk += '\n' + paragraph
else:
current_chunk = paragraph
else:
chunks.append(current_chunk)
current_chunk = paragraph
if current_chunk:
chunks.append(current_chunk)
return chunks
def fr_to_en(text: str) -> str:
"""
Traduit du français vers l'anglais.
Args:
text: Texte en français
Returns:
Texte traduit en anglais
"""
return translate_text(text, "fr", "en")
def en_to_fr(text: str) -> str:
try:
if not text.strip():
return ""
return GoogleTranslator(source="en", target="fr").translate(text)
except Exception as e:
logger.error(f"Traduction EN->FR échouée: {e}")
return ""
"""
Traduit de l'anglais vers le français.
Args:
text: Texte en anglais
Returns:
Texte traduit en français
"""
return translate_text(text, "en", "fr")
def determiner_repertoire_ticket(ticket_id: str):
"""
@ -134,3 +269,10 @@ def sauvegarder_ocr_traduction(
except Exception as e:
logger.error(f"Erreur lors de la sauvegarde OCR+TRAD pour {image_path}: {e}")
# Fonction pour effacer le cache de traduction (utile pour les tests)
def clear_translation_cache():
"""Vide le cache de traduction."""
global TRANSLATION_CACHE
TRANSLATION_CACHE = {}
logger.info("Cache de traduction vidé")