1804-15:26

This commit is contained in:
Ladebeze66 2025-04-18 15:26:30 +02:00
parent 4e7a2178d3
commit 7419e4d1e1
20 changed files with 85 additions and 4957 deletions

159
README.md
View File

@ -1,159 +0,0 @@
# Système d'extraction de tickets Odoo
Ce projet permet d'extraire les informations des tickets Odoo (tâches, tickets de support) avec leurs messages et pièces jointes, et de les sauvegarder dans une structure organisée.
## Installation
1. Clonez le dépôt
2. Créez un environnement virtuel :
```bash
python3 -m venv venv
source venv/bin/activate # Sur Linux/Mac
# ou
venv\Scripts\activate # Sur Windows
```
3. Installez les dépendances :
```bash
pip install -r requirements.txt
```
## Configuration
Créez un fichier `config.json` basé sur le modèle `config.template.json` :
```json
{
"odoo_url": "https://votre-instance.odoo.com",
"odoo_db": "nom_de_la_base",
"odoo_username": "votre_email@exemple.com",
"odoo_api_key": "votre_clé_api_odoo",
"output_dir": "ticket_structure"
}
```
## Utilisation
Pour extraire un ticket, utilisez la commande :
```bash
python -m utils.retrieve_ticket CODE_TICKET
```
Options disponibles :
- `--output`, `-o` : Répertoire de sortie (défaut: "ticket_structure")
- `--config`, `-c` : Chemin vers le fichier de configuration (défaut: "config.json")
- `--verbose`, `-v` : Activer le mode verbeux
Exemple :
```bash
python -m utils.retrieve_ticket T1234 --output mes_tickets --verbose
```
## Structure des fichiers générés
Pour chaque ticket extrait, un répertoire est créé avec la structure suivante :
```
CODE_TICKET_DATE/
├── all_messages.json # Messages traités au format JSON
├── all_messages.txt # Messages au format texte
├── attachments/ # Répertoire contenant les pièces jointes
├── attachments_info.json # Métadonnées des pièces jointes
├── extraction_summary.json # Résumé de l'extraction
├── messages_raw.json # Messages bruts
├── structure.json # Structure du répertoire
├── ticket_info.json # Données complètes du ticket
└── ticket_summary.json # Résumé du ticket
```
## Gestionnaires disponibles
Le système est divisé en plusieurs gestionnaires :
- `AuthManager` : Gère l'authentification et les appels à l'API Odoo
- `TicketManager` : Gère la récupération des tickets et organise leur extraction
- `MessageManager` : Gère le traitement des messages (filtrage, nettoyage)
- `AttachmentManager` : Gère le téléchargement des pièces jointes
## Licence
Ce projet est sous licence MIT.
## Nouvelles fonctionnalités pour Qwen et DeepSeek
### Génération de rapports optimisée
Pour améliorer la qualité des rapports générés par Qwen et DeepSeek, nous avons introduit plusieurs nouvelles fonctionnalités :
1. **Agent de rapport optimisé** (`AgentReportGeneratorBis`)
- Instructions plus claires et plus directes pour les modèles Qwen et DeepSeek
- Format strictement défini pour les sections critiques (notamment "Synthèse globale des analyses d'images")
- Prompt système avec des exemples concrets pour le format JSON
2. **Extraction robuste de données** (`report_utils_bis.py`)
- Module spécialisé pour extraire le JSON des rapports Qwen/DeepSeek
- Reconstruction du JSON à partir du contenu textuel si nécessaire
- Détection d'une plus grande variété de formats de section
3. **Génération de CSV améliorée**
- Génération de deux formats CSV :
- Format complet avec toutes les colonnes (Date, Émetteur, Type, Contenu)
- Format simplifié Q&R (Question, Réponse) pour la compatibilité
4. **Scripts d'orchestration dédiés**
- `test_orchestrator_qwen_bis.py` : Pour tester avec Qwen/Ollama
- `test_orchestrator_deepseek_bis.py` : Pour tester avec DeepSeek
### Outils de diagnostic et correction
1. **Script `test_extraction_json.py`**
- Permet de tester l'extraction JSON sur des rapports existants
- Corrige les rapports JSON manquants ou mal formés
2. **Script `generate_csv.py`**
- Génère des fichiers CSV à partir de rapports JSON
- Supporte plusieurs formats de sortie
3. **Script `fix_reports.py`**
- Outil tout-en-un pour analyser et corriger les rapports existants
- Options pour analyser ou corriger des rapports spécifiques
- Détection intelligente des sections et fichiers manquants
### Exemples d'utilisation
#### Corriger un rapport existant
```bash
# Analyser un rapport spécifique sans le modifier
./fix_reports.py --analyze --ticket T9656
# Corriger automatiquement un rapport problématique
./fix_reports.py --fix --ticket T9656
# Analyser tous les rapports du répertoire
./fix_reports.py --analyze --dir output/
# Corriger tous les rapports problématiques
./fix_reports.py --fix --dir output/
```
#### Générer un CSV à partir d'un rapport JSON
```bash
./generate_csv.py output/ticket_T0101/T0101_rapports/T0101/T0101_rapport_final.json qwen
```
#### Tester l'extraction JSON d'un rapport
```bash
./test_extraction_json.py output/ticket_T0101/T0101_rapports/T0101/T0101_rapport_final.json
```
#### Générer un nouveau rapport avec l'agent optimisé
```bash
python test_orchestrator_qwen_bis.py T0101
python test_orchestrator_deepseek_bis.py T0101
```
Ces améliorations permettent d'obtenir des rapports de meilleure qualité avec les modèles Qwen et DeepSeek, tout en maintenant la compatibilité avec les rapports existants générés par Mistral.

View File

@ -1,120 +0,0 @@
# Améliorations du système d'analyse de tickets
## Contexte
Le système d'analyse de tickets a été amélioré pour mieux gérer différents formats de données, en particulier les formats JSON et Markdown. Ce document explique les changements apportés et comment utiliser le nouveau système.
## Changements principaux
1. **Remplacement de `AgentJsonAnalyser` par `AgentTicketAnalyser`**
- Le nouvel agent est plus flexible et peut traiter des données provenant de différentes sources
- Il utilise un prompt système amélioré qui inclut des instructions spécifiques pour l'analyse des tickets
- Il intègre une meilleure gestion des métadonnées sur la source des données
2. **Création d'une classe `TicketDataLoader`**
- Abstraction pour charger les données de tickets depuis différentes sources
- Implémentations spécifiques pour les formats JSON et Markdown
- Validation et normalisation des données chargées
- Gestion unifiée de la recherche de fichiers de tickets
3. **Mise à jour de l'orchestrateur**
- Adaptation pour utiliser le nouvel agent et le loader de données
- Simplification des méthodes de traitement des données
- Suppression du code redondant pour l'extraction des données Markdown
4. **Ajout d'un script de test**
- `test_ticket_analyse.py` permet de tester le système d'analyse de tickets
- Possibilité de tester l'analyse d'un fichier spécifique
- Possibilité de tester la recherche de fichiers de tickets
## Comment utiliser le nouveau système
### Dans les scripts existants
Remplacez les références à `AgentJsonAnalyser` par `AgentTicketAnalyser` :
```python
# Ancien code
from agents.agent_json_analyser import AgentJsonAnalyser
agent = AgentJsonAnalyser(llm)
# Nouveau code
from agents.agent_ticket_analyser import AgentTicketAnalyser
agent = AgentTicketAnalyser(llm)
```
### Analyser un fichier directement
Le nouvel agent peut analyser un fichier directement sans avoir à charger les données au préalable :
```python
from agents.agent_ticket_analyser import AgentTicketAnalyser
agent = AgentTicketAnalyser(llm)
resultat = agent.analyser_depuis_fichier("chemin/vers/ticket.json")
# ou
resultat = agent.analyser_depuis_fichier("chemin/vers/ticket.md")
```
### Charger des données de ticket avec le loader
```python
from utils.ticket_data_loader import TicketDataLoader
loader = TicketDataLoader()
# Charger un fichier JSON
donnees_json = loader.charger("chemin/vers/ticket.json")
# Charger un fichier Markdown
donnees_md = loader.charger("chemin/vers/ticket.md")
# Détecter automatiquement le format
donnees = loader.charger("chemin/vers/ticket.ext")
# Rechercher des fichiers de ticket
resultats = loader.trouver_ticket("dossier/extraction", "T0101")
if resultats.get("json"):
print(f"Fichier JSON trouvé: {resultats['json']}")
if resultats.get("markdown"):
print(f"Fichier Markdown trouvé: {resultats['markdown']}")
```
## Exécuter les tests
```bash
# Tester l'analyse d'un fichier
python test_ticket_analyse.py --file chemin/vers/ticket.json
# Rechercher un ticket par ID
python test_ticket_analyse.py --search T0101 --dir dossier/extraction
# Afficher l'aide
python test_ticket_analyse.py --help
```
## Compatibilité avec l'ancien système
Si vous avez encore des scripts qui utilisent l'ancien `AgentJsonAnalyser`, ceux-ci devraient continuer à fonctionner avec le nouvel agent, car l'interface de la méthode `executer()` est restée compatible.
## Structure des métadonnées
Le nouveau système ajoute des métadonnées sur la source des données, ce qui peut être utile pour le débogage et l'analyse :
```json
{
"metadata": {
"source_file": "chemin/vers/fichier.ext",
"format": "json|markdown",
"autres_metadonnees": "..."
}
}
```
## Prochaines améliorations possibles
1. Ajouter le support pour d'autres formats de données (CSV, XML, etc.)
2. Améliorer la validation des données chargées
3. Ajouter des tests unitaires pour chaque composant
4. Implémenter une détection plus avancée des formats de date
5. Ajouter une option pour normaliser les noms de champs entre différents formats

View File

@ -1,368 +1,106 @@
import json
import os
from ..base_agent import BaseAgent
from datetime import datetime
from typing import Dict, Any, Tuple, Optional, List
from typing import Dict, Any
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
import os
from datetime import datetime
from ..utils.pipeline_logger import sauvegarder_donnees
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 = 8000
# 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
self.params = {
"temperature": 0.2,
"top_p": 0.8,
"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.
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
Tu dois structurer ta réponse en format question/réponse de manière claire, en gardant l'intégralité des points importants.
Ne propose jamais de solution. Ne reformule pas le contexte.
Ta seule mission est de croiser les données textuelles et visuelles et d'en tirer des observations claires, en listant les éléments factuels visibles dans les captures qui appuient ou complètent le texte du ticket.
Structure du rapport attendu :
1. Contexte général (résumé du ticket textuel en une phrase)
2. Problèmes ou questions identifiés (sous forme de questions claires)
3. Résumé croisé image/texte pour chaque question
4. Liste d'observations supplémentaires pertinentes (si applicable)
Reste strictement factuel. Ne fais aucune hypothèse. Ne suggère pas d'étapes ni d'interprétation."""
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.
self.llm.configurer(**self.params)
## 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
def executer(self, rapport_data: Dict[str, Any], dossier_destination: str) -> str:
ticket_id = rapport_data.get("ticket_id", "Inconnu")
print(f"AgentReportGenerator : génération du rapport pour le ticket {ticket_id}")
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
prompt = self._generer_prompt(rapport_data)
response = self.llm.interroger(prompt)
logger.info(f"Rapport généré pour le ticket {ticket_id}, longueur: {len(response)} caractères")
result = {
"prompt": prompt,
"response": response,
"metadata": {
"ticket_id": ticket_id,
"timestamp": self._get_timestamp(),
"source_agent": self.nom,
"model_info": {
"model": getattr(self.llm, "modele", str(type(self.llm))),
**getattr(self.llm, "params", {})
}
}
}
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
sauvegarder_donnees(ticket_id, "rapport_final", result, base_dir=dossier_destination, is_resultat=True)
self.ajouter_historique("rapport_final", {
"ticket_id": ticket_id,
"prompt": prompt,
"timestamp": self._get_timestamp()
}, response)
return response
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
logger.error(f"Erreur lors de la génération du rapport : {str(e)}")
return f"ERREUR: {str(e)}"
def _generer_prompt(self, rapport_data: Dict[str, Any]) -> str:
ticket_text = rapport_data.get("ticket_analyse", "")
image_blocs = []
analyses_images = rapport_data.get("analyse_images", {})
for chemin_image, analyse_obj in analyses_images.items():
analyse = analyse_obj.get("analysis", {}).get("analyse", "")
if analyse:
image_blocs.append(f"--- IMAGE : {os.path.basename(chemin_image)} ---\n{analyse}\n")
bloc_images = "\n".join(image_blocs)
return (
f"Voici les données d'analyse pour un ticket de support :\n\n"
f"=== ANALYSE DU TICKET ===\n{ticket_text}\n\n"
f"=== ANALYSES D'IMAGES ===\n{bloc_images}\n\n"
f"Génère un rapport croisé en suivant les instructions précédentes."
)
def _get_timestamp(self) -> str:
from datetime import datetime
return datetime.now().strftime("%Y%m%d_%H%M%S")

View File

@ -1,106 +0,0 @@
from ..base_agent import BaseAgent
from typing import Dict, Any
import logging
import os
from datetime import datetime
from ..utils.pipeline_logger import sauvegarder_donnees
logger = logging.getLogger("AgentReportGenerator")
class AgentReportGenerator(BaseAgent):
def __init__(self, llm):
super().__init__("AgentReportGenerator", llm)
self.params = {
"temperature": 0.2,
"top_p": 0.8,
"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.
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
Tu dois structurer ta réponse en format question/réponse de manière claire, en gardant l'intégralité des points importants.
Ne propose jamais de solution. Ne reformule pas le contexte.
Ta seule mission est de croiser les données textuelles et visuelles et d'en tirer des observations claires, en listant les éléments factuels visibles dans les captures qui appuient ou complètent le texte du ticket.
Structure du rapport attendu :
1. Contexte général (résumé du ticket textuel en une phrase)
2. Problèmes ou questions identifiés (sous forme de questions claires)
3. Résumé croisé image/texte pour chaque question
4. Liste d'observations supplémentaires pertinentes (si applicable)
Reste strictement factuel. Ne fais aucune hypothèse. Ne suggère pas d'étapes ni d'interprétation."""
self._appliquer_config_locale()
logger.info("AgentReportGenerator initialisé")
def _appliquer_config_locale(self) -> None:
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, rapport_data: Dict[str, Any], dossier_destination: str) -> str:
ticket_id = rapport_data.get("ticket_id", "Inconnu")
print(f"AgentReportGenerator : génération du rapport pour le ticket {ticket_id}")
try:
prompt = self._generer_prompt(rapport_data)
response = self.llm.interroger(prompt)
logger.info(f"Rapport généré pour le ticket {ticket_id}, longueur: {len(response)} caractères")
result = {
"prompt": prompt,
"response": response,
"metadata": {
"ticket_id": ticket_id,
"timestamp": self._get_timestamp(),
"source_agent": self.nom,
"model_info": {
"model": getattr(self.llm, "modele", str(type(self.llm))),
**getattr(self.llm, "params", {})
}
}
}
sauvegarder_donnees(ticket_id, "rapport_final", result, base_dir=dossier_destination, is_resultat=True)
self.ajouter_historique("rapport_final", {
"ticket_id": ticket_id,
"prompt": prompt,
"timestamp": self._get_timestamp()
}, response)
return response
except Exception as e:
logger.error(f"Erreur lors de la génération du rapport : {str(e)}")
return f"ERREUR: {str(e)}"
def _generer_prompt(self, rapport_data: Dict[str, Any]) -> str:
ticket_text = rapport_data.get("ticket_analyse", "")
image_blocs = []
analyses_images = rapport_data.get("analyse_images", {})
for chemin_image, analyse_obj in analyses_images.items():
analyse = analyse_obj.get("analysis", {}).get("analyse", "")
if analyse:
image_blocs.append(f"--- IMAGE : {os.path.basename(chemin_image)} ---\n{analyse}\n")
bloc_images = "\n".join(image_blocs)
return (
f"Voici les données d'analyse pour un ticket de support :\n\n"
f"=== ANALYSE DU TICKET ===\n{ticket_text}\n\n"
f"=== ANALYSES D'IMAGES ===\n{bloc_images}\n\n"
f"Génère un rapport croisé en suivant les instructions précédentes."
)
def _get_timestamp(self) -> str:
from datetime import datetime
return datetime.now().strftime("%Y%m%d_%H%M%S")

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

@ -1,265 +0,0 @@
from ..base_agent import BaseAgent
from typing import Dict, Any
import logging
import json
import os
from datetime import datetime
from loaders.ticket_data_loader import TicketDataLoader
from ..utils.pipeline_logger import sauvegarder_donnees
logger = logging.getLogger("AgentTicketAnalyser")
class AgentTicketAnalyser(BaseAgent):
"""
Agent pour analyser les tickets (JSON ou Markdown) et en extraire les informations importantes.
Utilisé pour contextualiser les tickets avant l'analyse des captures d'écran.
"""
def __init__(self, llm):
super().__init__("AgentTicketAnalyser", llm)
# Configuration adaptée à l'analyse structurée pour Mistral Large
self.temperature = 0.05
self.top_p = 0.8
self.max_tokens = 7000
# Prompt système clair, structuré, optimisé pour une analyse exhaustive
self.system_prompt = """Tu es un expert en analyse de tickets pour le support informatique de BRG-Lab (client : CBAO).
Je vais te donner un ticket de support avec plusieurs messages, et tu dois REPRODUIRE EXACTEMENT le contenu technique de chaque message, sans inventer ni reformuler.
STRUCTURE DE SORTIE ATTENDUE :
1. Résumé du contexte (bref, factuel)
2. Informations techniques détectées
3. Fil de discussion (REPRODUCTION EXACTE, SANS HALLUCINATION)
4. Points visuels à analyser (éléments à retrouver dans les captures)
RÈGLES ABSOLUES :
- Le NOM (titre) et la DESCRIPTION du ticket sont le PREMIER MESSAGE du client et contiennent souvent l'information cruciale
- REPRODUIS MOT POUR MOT le contenu des messages, notamment les termes techniques comme "essai au bleu"
- N'INVENTE JAMAIS de messages qui n'existent pas dans le ticket original
- N'INTRODUIS JAMAIS de termes comme "XYZ" ou autres références qui n'existent pas dans le texte original
- NE DÉDUIS PAS d'information qui n'est pas explicitement mentionnée
- NE REFORMULE PAS les messages, copie-les exactement
- GARDE TOUS les liens, noms d'essais et détails techniques tels quels
- Ne supprime que les signatures, formules de politesse et parties non pertinentes
ATTENTION AUX DÉTAILS :
- Un "essai au bleu" doit rester "essai au bleu", pas "essai" ni "essai XYZ"
- Les auteurs des messages doivent être correctement attribués
- L'ordre chronologique des messages doit être respecté
- Les URLs et références techniques doivent être préservées telles quelles
ASTUCE : Si tu n'es pas sûr d'un détail, reproduis-le tel quel plutôt que de l'interpréter.
"""
self.ticket_loader = TicketDataLoader()
self._appliquer_config_locale()
logger.info("AgentTicketAnalyser initialisé")
def _appliquer_config_locale(self) -> None:
"""Applique les paramètres et le prompt système au modèle"""
if hasattr(self.llm, "prompt_system"):
self.llm.prompt_system = self.system_prompt
if hasattr(self.llm, "configurer"):
self.llm.configurer(
temperature=self.temperature,
top_p=self.top_p,
max_tokens=self.max_tokens
)
def executer(self, ticket_data: Dict[str, Any]) -> str:
"""Analyse le ticket donné sous forme de dict"""
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"
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")
# Sauvegarde dans pipeline
result = {
"prompt": prompt,
"response": response,
"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", {})
}
}
}
sauvegarder_donnees(ticket_code, "analyse_ticket", result, base_dir="reports", is_resultat=True)
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
def _generer_prompt(self, ticket_data: Dict[str, Any]) -> str:
"""
Prépare un prompt texte structuré à partir des données brutes du ticket.
"""
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", "")
partner_id_name = ticket_data.get("partner_id_name", "").strip()
email_from = ticket_data.get("email_from", "").strip()
# En-tête avec instructions spécifiques
texte = f"### TICKET {ticket_code}\n\n"
texte += "IMPORTANT: Tu dois reproduire EXACTEMENT le contenu des messages sans reformulation ni invention.\n"
texte += "ATTENTION: Tu dois conserver les termes techniques EXACTS comme 'essai au bleu' sans les modifier.\n"
texte += "CRITIQUE: Le NOM et la DESCRIPTION du ticket sont le PREMIER MESSAGE du client et doivent être inclus au début du fil.\n"
texte += "INTERDIT: N'invente JAMAIS de messages qui n'existent pas dans le ticket.\n\n"
if name:
texte += f"#### NOM DU TICKET\n{name}\n\n"
if description:
texte += f"#### DESCRIPTION\n{description}\n\n"
# Informations techniques utiles (hors champs exclus)
champs_exclus = {"code", "name", "description", "messages", "metadata"}
info_techniques = [
f"- {k}: {json.dumps(v, ensure_ascii=False)}" if isinstance(v, (dict, list)) else f"- {k}: {v}"
for k, v in ticket_data.items()
if k not in champs_exclus and v
]
if info_techniques:
texte += "#### INFORMATIONS TECHNIQUES\n" + "\n".join(info_techniques) + "\n\n"
# Fil des échanges client/support - format amélioré
texte += "#### ÉCHANGES CLIENT / SUPPORT (À REPRODUIRE TEXTUELLEMENT)\n"
# Ajouter le titre et la description comme premier message si pertinent
if name or description:
texte += "--- MESSAGE INITIAL (Ouverture du ticket) ---\n"
texte += f"ID: ticket_creation\n"
texte += f"Auteur: {partner_id_name if partner_id_name else email_from if email_from else 'Client'}\n"
texte += f"Date: {create_date}\n"
texte += f"Type: Création du ticket\n"
texte += f"Sujet: {name}\n"
texte += "Contenu (à reproduire EXACTEMENT):\n"
if description and description != "<p><br></p>":
texte += f"{description}\n\n"
else:
texte += f"{name}\n\n"
# Ajouter les messages standards
messages = ticket_data.get("messages", [])
if messages:
for i, msg in enumerate(messages):
if not isinstance(msg, dict):
continue
# Extraction des métadonnées du message
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()
msg_id = msg.get("id", "").strip()
# Extraction du contenu avec préservation exacte
contenu = msg.get("content", "").strip()
# Format cohérent pour faciliter la compréhension du modèle
texte += f"--- MESSAGE {i+1} ---\n"
texte += f"ID: {msg_id}\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 (à reproduire EXACTEMENT):\n{contenu}\n\n"
# Instructions finales très explicites
texte += "INSTRUCTIONS FINALES:\n"
texte += "1. Reproduis EXACTEMENT le contenu de chaque message sans le modifier ni l'interpréter\n"
texte += "2. Conserve les termes techniques exacts (ex: 'essai au bleu')\n"
texte += "3. N'invente JAMAIS de messages ou d'éléments qui ne sont pas présents\n"
texte += "4. IMPORTANT: Le NOM et la DESCRIPTION du ticket sont le PREMIER MESSAGE du client\n"
texte += "5. Respecte l'ordre chronologique des messages\n"
texte += "6. Reste fidèle au texte original même s'il te paraît incomplet\n"
# Métadonnées utiles
metadata = ticket_data.get("metadata", {})
meta_text = [
f"- {k}: {json.dumps(v, ensure_ascii=False)}" if isinstance(v, (dict, list)) else f"- {k}: {v}"
for k, v in metadata.items()
if k not in {"format", "source_file"} and v
]
if meta_text:
texte += "#### MÉTADONNÉES\n" + "\n".join(meta_text) + "\n"
return texte
def _formater_date(self, date_str: str) -> str:
"""
Formate la date en DD/MM/YYYY HH:MM si possible
Args:
date_str: Chaîne de caractères représentant une date
Returns:
Date formatée ou la chaîne originale si le format n'est pas reconnu
"""
if not date_str:
return "date inconnue"
try:
# Essayer différents formats courants
formats = [
"%Y-%m-%dT%H:%M:%S.%fZ", # Format ISO avec microsecondes et Z
"%Y-%m-%dT%H:%M:%SZ", # Format ISO sans microsecondes avec Z
"%Y-%m-%dT%H:%M:%S", # Format ISO sans Z
"%Y-%m-%d %H:%M:%S", # Format standard
"%d/%m/%Y %H:%M:%S", # Format français
"%d/%m/%Y" # Format date simple
]
for fmt in formats:
try:
dt = datetime.strptime(date_str, fmt)
return dt.strftime("%d/%m/%Y %H:%M")
except ValueError:
continue
# Si aucun format ne correspond, retourner la chaîne originale
return date_str
except Exception:
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,321 +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 _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}")
# Utiliser la méthode _encoder_image_base64 du modèle directement
if hasattr(self.llm, "_encoder_image_base64"):
img_base64 = self.llm._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
}
}
else:
error_message = "Le modèle ne supporte pas l'encodage d'images 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 utiliser un modèle compatible avec l'analyse d'images.",
"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,169 +0,0 @@
from ..base_agent import BaseAgent
import logging
import os
from typing import Dict, Any, Tuple
from PIL import Image
from ..utils.pipeline_logger import sauvegarder_donnees
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 personnalisable
self.temperature = 0.2
self.top_p = 0.8
self.max_tokens = 300
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
"""
)
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".
"""
)
self.system_prompt = (
"""
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.
{criteres}
{instructions}
"""
).format(
criteres=self.criteres_pertinence,
instructions=self.instructions_analyse
)
self._appliquer_config_locale()
logger.info("AgentImageSorter initialisé")
def _appliquer_config_locale(self) -> None:
if hasattr(self.llm, "prompt_system"):
self.llm.prompt_system = self.system_prompt
if hasattr(self.llm, "configurer"):
self.llm.configurer(
temperature=self.temperature,
top_p=self.top_p,
max_tokens=self.max_tokens
)
def _verifier_image(self, image_path: str) -> bool:
try:
if not os.path.exists(image_path) or not os.access(image_path, os.R_OK):
return False
with Image.open(image_path) as img:
width, height = img.size
return width > 0 and height > 0
except Exception as e:
logger.error(f"Vérification impossible pour {image_path}: {e}")
return False
def _generer_prompt_analyse(self, prefix: str = "") -> str:
return f"{prefix}\n\nEst-ce une image pertinente pour un ticket de support technique?\nRéponds simplement par 'oui' ou 'non' suivi d'une brève explication."
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)
try:
prompt = self._generer_prompt_analyse()
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)
prompt = self._generer_prompt_analyse(f"Analyse cette image:\n{img_base64}")
response = self.llm.interroger(prompt)
else:
return self._erreur("Le modèle ne supporte pas les images", image_path)
if any(err in response.lower() for err in [
"je ne peux pas", "je n'ai pas accès", "impossible d'analyser"]):
return self._erreur("Réponse du modèle invalide", image_path, raw=response)
is_relevant, reason = self._analyser_reponse(response)
print(f" Décision: Image {image_name} {'pertinente' if is_relevant else 'non pertinente'}")
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))),
**getattr(self.llm, "params", {})
},
"source_agent": self.nom
}
}
# Sauvegarder les données dans le fichier
sauvegarder_donnees(None, "tri_image", result, base_dir="reports", is_resultat=True)
self.ajouter_historique("tri_image", {"image_path": image_path, "prompt": prompt}, result)
return result
except Exception as e:
return self._erreur(str(e), image_path)
def _analyser_reponse(self, response: str) -> Tuple[bool, str]:
r = response.lower()
first_line = r.split('\n')[0] if '\n' in r else r[:50].strip()
if first_line.startswith("non") or "non pertinent" in first_line:
return False, response.strip()
if first_line.startswith("oui") or "pertinent" in first_line:
return True, response.strip()
pos_keywords = ["pertinent", "utile", "important", "diagnostic"]
neg_keywords = ["inutile", "photo", "hors sujet", "marketing", "non pertinent"]
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:
from datetime import datetime
return datetime.now().strftime("%Y%m%d_%H%M%S")

View File

@ -1,3 +0,0 @@
"""
Module de tests pour les fonctionnalités core.
"""

View File

@ -1,61 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script de test pour comprendre le filtrage de clean_html.py
"""
from formatters.clean_html import pre_clean_html, clean_html
def test_verbose_clean():
html = """<p>Bonjour,<br>Le problème de passant qui remonte à 100% sur le dernier tamis est corrigé lors de la mise à jour disponible depuis ce matin.<br>Je reste à votre disposition pour toute explication ou demande supplémentaire.<br>L'objectif du Support Technique est de vous aider : n'hésitez jamais à nous contacter si vous rencontrez une difficulté, ou pour nous soumettre une ou des suggestions d'amélioration de nos logiciels ou de nos méthodes.<br>Cordialement.<br><br>Support Technique - CBAO<br><a target=\"_blank\" href=\"http://www.cbao.fr\">www.cbao.fr</a><br>80 rue Louis Braille<br>66000 PERPIGNAN<br>support@cbao.fr<br>Tél : 04 68 64 15 31<br>Fax : 04 68 64 31 69</p>"""
print("ANALYSE DU NETTOYAGE HTML AVEC PRE_CLEAN_HTML:")
# Nettoyage préliminaire
cleaned_content = pre_clean_html(html)
print("\nContenu après pre_clean_html:")
print("-" * 50)
print(cleaned_content)
print("-" * 50)
# Test avec la fonction clean_html complète
print("\n\nANALYSE DU NETTOYAGE HTML AVEC CLEAN_HTML COMPLET:")
full_cleaned = clean_html(html)
print("\nContenu après clean_html complet:")
print("-" * 50)
print(full_cleaned)
print("-" * 50)
# Vérifions si une des lignes de coordonnées est présente dans le résultat final
coordonnees = ["80 rue Louis Braille", "66000 PERPIGNAN", "support@cbao.fr", "Tél :", "Fax :"]
for coord in coordonnees:
if coord in full_cleaned:
print(f"TROUVÉ: '{coord}' est présent dans le résultat final de clean_html")
else:
print(f"MANQUANT: '{coord}' n'est PAS présent dans le résultat final de clean_html")
# Test avec le message body_original exact du fichier all_messages.json
body_original = "<p>Bonjour,<br>Le problème de passant qui remonte à 100% sur le dernier tamis est corrigé lors de la mise à jour disponible depuis ce matin.<br>Je reste à votre disposition pour toute explication ou demande supplémentaire.<br>L'objectif du Support Technique est de vous aider : n'hésitez jamais à nous contacter si vous rencontrez une difficulté, ou pour nous soumettre une ou des suggestions d'amélioration de nos logiciels ou de nos méthodes.<br>Cordialement.<br><br>Support Technique - CBAO<br><a target=\"_blank\" href=\"http://www.cbao.fr\">www.cbao.fr</a><br>80 rue Louis Braille<br>66000 PERPIGNAN<br>support@cbao.fr<br>Tél : 04 68 64 15 31<br>Fax : 04 68 64 31 69</p>"
print("\n\nTEST AVEC LE BODY_ORIGINAL EXACT:")
real_cleaned = clean_html(body_original)
print("\nContenu après clean_html avec body_original exact:")
print("-" * 50)
print(real_cleaned)
print("-" * 50)
# Vérifier si le contenu du corps est égal à "Contenu non extractible"
if real_cleaned == "*Contenu non extractible*":
print("\n⚠️ PROBLÈME DÉTECTÉ: le résultat est 'Contenu non extractible' ⚠️")
else:
print("\nLe résultat n'est pas 'Contenu non extractible'")
return {
"pre_cleaned": cleaned_content,
"full_cleaned": full_cleaned,
"real_cleaned": real_cleaned
}
if __name__ == "__main__":
test_verbose_clean()

View File

@ -1,503 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Fonctions utilitaires pour nettoyer le HTML et formater les dates.
Version simplifiée et robuste: ignore les lignes problématiques.
"""
import re
from datetime import datetime
import html
from bs4 import BeautifulSoup, Tag
from bs4.element import NavigableString, PageElement
from typing import Union, List, Tuple, Optional, Any, Dict, cast
import logging
import html2text
def clean_html(html_content: Union[str, None], is_forwarded: bool = False, is_description: bool = False, strategy: str = "standard", preserve_links: bool = False, preserve_images: bool = False):
"""
Nettoie le contenu HTML pour le Markdown en identifiant et ignorant les parties problématiques.
Args:
html_content (Union[str, None]): Contenu HTML à nettoyer
is_forwarded (bool): Indique si le message est transféré
is_description (bool): Paramètre de compatibilité (ignoré)
strategy (str): Paramètre de compatibilité (ignoré)
preserve_links (bool): Paramètre de compatibilité (ignoré)
preserve_images (bool): Paramètre de compatibilité (ignoré)
Returns:
str: Texte nettoyé
"""
if html_content is None or not isinstance(html_content, str) or html_content.strip() == "":
if is_forwarded:
return "*Message transféré - contenu non extractible*"
return "*Contenu non extractible*"
try:
# Sauvegarder les références d'images avant de nettoyer le HTML
image_references: List[Tuple[str, str]] = []
img_pattern = re.compile(r'<img[^>]+src=["\']([^"\']+)["\'][^>]*>')
for match in img_pattern.finditer(html_content):
full_tag = match.group(0)
img_url = match.group(1)
# Vérifier si c'est une image Odoo
if "/web/image/" in img_url:
image_references.append((full_tag, img_url))
# Nettoyer le HTML
soup = BeautifulSoup(html_content, 'html.parser')
# Supprimer les éléments script, style et head
for elem in soup.find_all(['script', 'style', 'head']):
elem.decompose()
# Supprimer les attributs de style et les classes
for tag in soup.recursiveChildGenerator():
if isinstance(tag, Tag):
if tag.attrs and 'style' in tag.attrs:
del tag.attrs['style']
if tag.attrs and 'class' in tag.attrs:
del tag.attrs['class']
# Conserver uniquement les balises HTML essentielles
allowed_tags = ['p', 'br', 'b', 'i', 'u', 'strong', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'ul', 'ol', 'li', 'a', 'img', 'blockquote', 'code', 'pre', 'hr', 'div', 'span',
'table', 'tr', 'td', 'th', 'thead', 'tbody']
# Supprimer les balises HTML inutiles mais conserver leur contenu
for tag in soup.find_all():
if isinstance(tag, Tag) and tag.name.lower() not in allowed_tags:
tag.unwrap()
# Amélioration: vérifier si nous avons du contenu significatif
text_content = soup.get_text().strip()
if not text_content and not image_references:
if is_forwarded:
return "*Message transféré - contenu non extractible*"
return "*Contenu non extractible*"
# Obtenir le HTML nettoyé
clean_content = str(soup)
# Vérifier si le contenu a été vidé par le nettoyage
if clean_content.strip() == "" or clean_content.strip() == "<html><body></body></html>":
# Si nous avons des références d'images mais pas de texte
if image_references:
image_descriptions = []
for _, img_url in image_references:
img_id = None
id_match = re.search(r"/web/image/(\d+)", img_url)
if id_match:
img_id = id_match.group(1)
image_descriptions.append(f"[Image {img_id}]")
# Retourner une description des images trouvées
if image_descriptions:
return "Message contenant uniquement des images: " + ", ".join(image_descriptions)
if is_forwarded:
return "*Message transféré - contenu non extractible*"
return "*Contenu non extractible*"
return clean_content
except Exception as e:
logging.error(f"Erreur lors du nettoyage HTML: {str(e)}")
if is_forwarded:
return "*Message transféré - contenu non extractible*"
return "*Contenu non extractible*"
def extract_from_complex_html(html_content, preserve_images=False):
"""
Extrait le contenu d'un HTML complexe en utilisant BeautifulSoup.
Cette fonction est spécialement conçue pour traiter les structures
HTML complexes qui posent problème avec l'approche standard.
Args:
html_content (str): Contenu HTML à traiter
preserve_images (bool): Conserver les images
Returns:
str: Contenu extrait et nettoyé
"""
try:
soup = BeautifulSoup(html_content, 'html.parser')
# Extraction d'images - Étape 1: Rechercher toutes les images avant toute modification
image_markdowns = []
if preserve_images or True: # Toujours préserver les images
# Chercher directement les balises img dans le HTML brut
img_matches = re.finditer(r'<img[^>]+src=["\']([^"\']+)["\'][^>]*>', html_content)
for match in img_matches:
src = match.group(1)
if '/web/image/' in src or 'access_token' in src or src.startswith('http'):
image_markdowns.append(f"![Image]({src})")
# Méthode alternative avec BeautifulSoup
images = soup.find_all('img')
for img in images:
try:
if isinstance(img, Tag) and img.has_attr('src'):
src = img['src']
if src and ('/web/image/' in src or 'access_token' in src or str(src).startswith('http')):
alt = img['alt'] if img.has_attr('alt') else 'Image'
image_markdowns.append(f"![{alt}]({src})")
except Exception:
continue
# 1. Rechercher d'abord le contenu du message principal
# Essayer différents sélecteurs en ordre de priorité
content_selectors = [
'.o_thread_message_content', # Contenu principal
'.o_mail_body', # Corps du message
'.o_mail_note_content', # Contenu d'une note
'.message_content', # Contenu du message (générique)
'div[style*="font-size:13px"]', # Recherche par style
]
main_content = None
for selector in content_selectors:
content_elements = soup.select(selector)
if content_elements:
main_content = content_elements[0]
break
# Si aucun contenu principal n'est trouvé, prendre le premier paragraphe non vide
if not main_content:
paragraphs = soup.find_all('p')
for p in paragraphs:
try:
if isinstance(p, Tag) and p.text.strip():
classes = p['class'] if p.has_attr('class') else []
if not any(cls in str(classes) for cls in ['o_mail_info', 'recipient_link']):
main_content = p
break
except Exception:
continue
# Si toujours rien, prendre la première div non vide
if not main_content:
divs = soup.find_all('div')
for div in divs:
try:
if isinstance(div, Tag) and div.text.strip():
classes = div['class'] if div.has_attr('class') else []
if not any(cls in str(classes) for cls in ['o_mail_info', 'o_thread']):
main_content = div
break
except Exception:
continue
# 2. Si on a trouvé du contenu, l'extraire
if main_content:
# Extraire toutes les images si demandé
if preserve_images or True: # Toujours préserver les images
try:
if isinstance(main_content, Tag):
content_images = main_content.find_all('img')
for img in content_images:
try:
if isinstance(img, Tag) and img.has_attr('src'):
src = img['src']
if src and ('/web/image/' in src or 'access_token' in src or str(src).startswith('http')):
alt = img['alt'] if img.has_attr('alt') else 'Image'
image_markdowns.append(f"![{alt}]({src})")
# Supprimer l'image pour éviter qu'elle apparaisse dans le texte
img.decompose()
except Exception:
continue
except Exception:
pass
# Extraire le texte
try:
if isinstance(main_content, Tag):
text_content = main_content.get_text(separator='\n', strip=True)
# Nettoyer le texte
text_content = re.sub(r'\n{3,}', '\n\n', text_content)
text_content = text_content.strip()
# Recherche spécifique pour certaines phrases clés
if "Je ne parviens pas à accéder" in html_content:
bonjour_match = re.search(r'<p[^>]*>.*?Bonjour.*?</p>', html_content, re.DOTALL)
acces_match = re.search(r'<p[^>]*>.*?Je ne parviens pas à accéder[^<]*</p>', html_content, re.DOTALL)
specific_content = []
if bonjour_match:
specific_content.append(pre_clean_html(bonjour_match.group(0)))
if acces_match:
specific_content.append(pre_clean_html(acces_match.group(0)))
# Extraire les contenus spécifiques du message "Je ne parviens pas..."
merci_match = re.search(r'<p[^>]*>.*?Merci par avance.*?</p>', html_content, re.DOTALL)
if merci_match:
specific_content.append(pre_clean_html(merci_match.group(0)))
cordial_match = re.search(r'<p[^>]*>.*?Cordialement.*?</p>', html_content, re.DOTALL)
if cordial_match:
specific_content.append(pre_clean_html(cordial_match.group(0)))
if specific_content:
text_content = '\n'.join(specific_content)
# Supprimer les duplications de lignes
lines = text_content.split('\n')
unique_lines = []
for line in lines:
if line not in unique_lines:
unique_lines.append(line)
text_content = '\n'.join(unique_lines)
# Ajouter les images à la fin
if image_markdowns:
# Supprimer les doublons d'images
unique_images = []
for img in image_markdowns:
if img not in unique_images:
unique_images.append(img)
text_content += "\n\n" + "\n".join(unique_images)
return text_content if text_content else "*Contenu non extractible*"
except Exception as e:
print(f"Erreur lors de l'extraction du texte: {e}")
# 3. Si on n'a rien trouvé, essayer une extraction plus générique
# Supprimer les éléments non pertinents
for elem in soup.select('.o_mail_info, .o_mail_tracking, .o_thread_tooltip, .o_thread_icons, .recipients_info'):
try:
elem.decompose()
except Exception:
continue
# Extraire le texte restant
try:
text = soup.get_text(separator='\n', strip=True)
text = re.sub(r'\n{3,}', '\n\n', text)
# Préserver les images si demandé
if preserve_images or True: # Toujours préserver les images
# Les images ont déjà été extraites au début de la fonction
if image_markdowns:
# Supprimer les doublons d'images
unique_images = []
for img in image_markdowns:
if img not in unique_images:
unique_images.append(img)
text += "\n\n" + "\n".join(unique_images)
# Si on a du contenu, le retourner
if text and len(text.strip()) > 5:
return text
except Exception as e:
print(f"Erreur lors de l'extraction générique: {e}")
# Si rien n'a fonctionné mais qu'on a des images, au moins les retourner
if image_markdowns:
unique_images = []
for img in image_markdowns:
if img not in unique_images:
unique_images.append(img)
if any("Je ne parviens pas à accéder" in html_content for img in image_markdowns):
return "Bonjour,\nJe ne parviens pas à accéder au l'essai au bleu :\n\n" + "\n".join(unique_images) + "\n\nMerci par avance pour votre.\nCordialement"
else:
return "Images extraites :\n\n" + "\n".join(unique_images)
return "*Contenu non extractible*"
except Exception as e:
print(f"Erreur lors de l'extraction complexe: {e}")
# Dernière tentative : extraction directe avec regex
try:
# Extraire des images
image_markdowns = []
img_matches = re.finditer(r'<img[^>]+src=["\']([^"\']+)["\'][^>]*>', html_content)
for match in img_matches:
src = match.group(1)
if '/web/image/' in src or 'access_token' in src or src.startswith('http'):
image_markdowns.append(f"![Image]({src})")
# Extraire du texte significatif
text_parts = []
bonjour_match = re.search(r'<p[^>]*>.*?Bonjour.*?</p>', html_content, re.DOTALL)
if bonjour_match:
text_parts.append(pre_clean_html(bonjour_match.group(0)))
content_match = re.search(r'<p[^>]*>.*?Je ne parviens pas à accéder.*?</p>', html_content, re.DOTALL)
if content_match:
text_parts.append(pre_clean_html(content_match.group(0)))
# Combiner texte et images
if text_parts or image_markdowns:
result = ""
if text_parts:
result += "\n".join(text_parts) + "\n\n"
if image_markdowns:
unique_images = []
for img in image_markdowns:
if img not in unique_images:
unique_images.append(img)
result += "\n".join(unique_images)
return result
except Exception:
pass
return "*Contenu non extractible*"
def pre_clean_html(html_content):
"""
Fonction interne pour nettoyer le HTML basique avant traitement avancé.
Args:
html_content: Contenu HTML à pré-nettoyer
Returns:
Texte avec les balises HTML basiques retirées
"""
if not html_content:
return ""
# Remplacer les balises <br>, <p>, <div> par des sauts de ligne
content = html_content.replace('<br>', '\n').replace('<br/>', '\n').replace('<br />', '\n')
content = content.replace('</p>', '\n').replace('</div>', '\n')
# Préserver les URLs des images
image_urls = []
img_matches = re.finditer(r'<img[^>]+src=["\']([^"\']+)["\'][^>]*>', content)
for match in img_matches:
if '/web/image/' in match.group(1) or match.group(1).startswith('http'):
image_urls.append(match.group(1))
# Supprimer les balises HTML
content = re.sub(r'<[^>]*>', '', content)
# Supprimer les espaces multiples
content = re.sub(r' {2,}', ' ', content)
# Supprimer les sauts de ligne multiples
content = re.sub(r'\n{3,}', '\n\n', content)
# Décoder les entités HTML courantes
content = content.replace('&nbsp;', ' ')
content = content.replace('&lt;', '<')
content = content.replace('&gt;', '>')
content = content.replace('&amp;', '&')
content = content.replace('&quot;', '"')
# Supprimer les tabulations
content = content.replace('\t', ' ')
# Ajouter les images préservées à la fin
if image_urls:
content += "\n\n"
for url in image_urls:
content += f"![Image]({url})\n"
return content.strip()
def format_date(date_str):
"""
Formate une date ISO en format lisible.
"""
if not date_str:
return ""
try:
dt = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
return dt.strftime("%d/%m/%Y %H:%M:%S")
except (ValueError, TypeError):
return date_str
if __name__ == "__main__":
# Tests
html = """<p>Bonjour,</p>
<p>Voici un message avec <b>du HTML</b> et une signature.</p>
<p>Cordialement,</p>
<p>John Doe</p>
<p>Support technique</p>
<p>Afin d'assurer une meilleure traçabilité et vous garantir une prise en charge optimale,
nous vous invitons à envoyer vos demandes d'assistance technique à support@exemple.fr</p>
<p>![CBAO - développeur de rentabilité - www.exemple.fr](data:image/png;base64,ABC123)</p>
"""
cleaned = clean_html(html)
print("HTML nettoyé :\n", cleaned)
# Test avec un message transféré
forwarded = """\\-------- Message transféré -------- Sujet : | Test message
---|---
Date : | Mon, 30 Mar 2020 11:18:20 +0200
De : | [test@example.com](mailto:test@example.com)
Pour : | John Doe [](mailto:john@example.com)
Copie à : | [other@example.com](mailto:other@example.com)
Bonjour John,
Voici un message de test.
Cordialement,
Test User
__________________________________________________________________ Ce message et toutes les pièces jointes sont confidentiels et établis à l'intention exclusive de ses destinataires. __________________________________________________________________"""
cleaned_forwarded = clean_html(forwarded)
print("\nMessage transféré nettoyé :\n", cleaned_forwarded)
# Test avec le cas problématique du ticket T0282
test_t0282 = """Bonjour,
Je reviens vers vous pour savoir si vous souhaitez toujours renommer le numéro d'identification de certaines formules dans BCN ou si vous avez trouvé une solution alternative ?
En vous remerciant par avance, je reste à votre disposition pour tout complément d'information.
Cordialement.
**Youness BENDEQ**
[
Affin d'assurer une meilleure traçabilité et vous garantir une prise en charge optimale, nous vous invitons à envoyer vos demandes d'assistance technique à support@cbao.fr Notre service est ouvert du lundi au vendredi de 9h à 12h et de 14h à 18h. Dès réception, un technicien prendra en charge votre demande et au besoin vous rappellera."""
cleaned_t0282 = clean_html(test_t0282)
print("\nTest ticket T0282 nettoyé :\n", cleaned_t0282)
# Test avec le cas problématique de bas de page avec formatage markdown
test_cbao_markdown = """Bonjour,
Voici un message de test pour vérifier la suppression des bas de page CBAO.
Cordialement,
Jean Dupont
[ CBAO S.A.R.L. ](https://example.com/link) .
![](/web/image/33748?access_token=13949e22-d47b-4af7-868e-e9c2575469f1) ![](/web/image/33748?access_token=13949e22-d47b-4af7-868e-e9c2575469f1)"""
cleaned_markdown = clean_html(test_cbao_markdown)
print("\nTest avec formatage Markdown CBAO nettoyé :\n", cleaned_markdown)
# Test avec le cas exact du rapport
test_rapport = """Bonjour,
Voici un message de test.
Cordialement,
Pierre Martin
Envoyé par [ CBAO S.A.R.L. ](https://ciibcee.r.af.d.sendibt2.com/tr/cl/h2uBsi9hBosNYeSHMsPH47KAmufMTuNZjreF6M_tfRE63xzft8fwSbEQNb0aYIor74WQB5L6TF4kR9szVpQnalHFa3PUn_0jeLw42JNzIwsESwVlYad_3xCC1xi7qt3-dQ7i_Rt62MG217XgidnJxyNVcXWaWG5B75sB0GoqJq13IZc-hQ) .
![](/web/image/33748?access_token=13949e22-d47b-4af7-868e-e9c2575469f1) ![](/web/image/33748?access_token=13949e22-d47b-4af7-868e-e9c2575469f1)"""
cleaned_rapport = clean_html(test_rapport)
print("\nTest avec cas exact du rapport nettoyé :\n", cleaned_rapport)

View File

@ -1,340 +0,0 @@
import os
import re
import json
import logging
from typing import Dict, Optional, Any, List, Union
from abc import ABC, abstractmethod
logger = logging.getLogger("TicketDataLoader")
class TicketDataSource(ABC):
"""Classe abstraite pour les sources de données de tickets"""
@abstractmethod
def charger(self, chemin_fichier: str) -> Dict[str, Any]:
"""Charge les données du ticket depuis un fichier source"""
pass
@abstractmethod
def get_format(self) -> str:
"""Retourne le format de la source de données"""
pass
def valider_donnees(self, donnees: Dict[str, Any]) -> bool:
"""Vérifie si les données chargées contiennent les champs obligatoires"""
champs_obligatoires = ["code", "name"]
return all(field in donnees for field in champs_obligatoires)
class JsonTicketSource(TicketDataSource):
"""Source de données pour les tickets au format JSON"""
def charger(self, chemin_fichier: str) -> Dict[str, Any]:
"""Charge les données du ticket depuis un fichier JSON"""
try:
with open(chemin_fichier, 'r', encoding='utf-8') as f:
donnees = json.load(f)
# Ajout de métadonnées sur la source
if "metadata" not in donnees:
donnees["metadata"] = {}
donnees["metadata"]["source_file"] = chemin_fichier
donnees["metadata"]["format"] = "json"
return donnees
except Exception as e:
logger.error(f"Erreur lors du chargement du fichier JSON {chemin_fichier}: {str(e)}")
raise ValueError(f"Impossible de charger le fichier JSON: {str(e)}")
def get_format(self) -> str:
return "json"
class MarkdownTicketSource(TicketDataSource):
"""Source de données pour les tickets au format Markdown"""
def charger(self, chemin_fichier: str) -> Dict[str, Any]:
"""Charge les données du ticket depuis un fichier Markdown"""
try:
with open(chemin_fichier, 'r', encoding='utf-8') as f:
contenu_md = f.read()
# Extraire les données du contenu Markdown
donnees = self._extraire_donnees_de_markdown(contenu_md)
# Ajout de métadonnées sur la source
if "metadata" not in donnees:
donnees["metadata"] = {}
donnees["metadata"]["source_file"] = chemin_fichier
donnees["metadata"]["format"] = "markdown"
return donnees
except Exception as e:
logger.error(f"Erreur lors du chargement du fichier Markdown {chemin_fichier}: {str(e)}")
raise ValueError(f"Impossible de charger le fichier Markdown: {str(e)}")
def get_format(self) -> str:
return "markdown"
def _extraire_donnees_de_markdown(self, contenu_md: str) -> Dict[str, Any]:
"""Extrait les données structurées d'un contenu Markdown"""
donnees = {}
# Diviser le contenu en sections
sections = re.split(r"\n## ", contenu_md)
# Traiter chaque section
for section in sections:
if section.startswith("Informations du ticket"):
ticket_info = self._analyser_infos_ticket(section)
donnees.update(ticket_info)
elif section.startswith("Messages"):
messages = self._analyser_messages(section)
donnees["messages"] = messages
elif section.startswith("Informations sur l'extraction"):
extraction_info = self._analyser_infos_extraction(section)
donnees.update(extraction_info)
# Réorganiser les champs pour que la description soit après "name"
ordered_fields = ["id", "code", "name", "description"]
ordered_data = {}
# D'abord ajouter les champs dans l'ordre spécifié
for field in ordered_fields:
if field in donnees:
ordered_data[field] = donnees[field]
# Ensuite ajouter les autres champs
for key, value in donnees.items():
if key not in ordered_data:
ordered_data[key] = value
# S'assurer que la description est présente
if "description" not in ordered_data:
ordered_data["description"] = ""
return ordered_data
def _analyser_infos_ticket(self, section: str) -> Dict[str, Any]:
"""Analyse la section d'informations du ticket"""
info = {}
description = []
capturing_description = False
lines = section.strip().split("\n")
i = 0
while i < len(lines):
line = lines[i]
# Si on est déjà en train de capturer la description
if capturing_description:
# Vérifie si on atteint une nouvelle section ou un nouveau champ
if i + 1 < len(lines) and (lines[i + 1].startswith("## ") or lines[i + 1].startswith("- **")):
capturing_description = False
info["description"] = "\n".join(description).strip()
else:
description.append(line)
i += 1
continue
# Détecte le début de la description
desc_match = re.match(r"- \*\*description\*\*:", line)
if desc_match:
capturing_description = True
i += 1 # Passe à la ligne suivante
continue
# Traite les autres champs normalement
match = re.match(r"- \*\*(.*?)\*\*: (.*)", line)
if match:
key, value = match.groups()
key = key.lower().replace("/", "_").replace(" ", "_")
info[key] = value.strip()
i += 1
# Si on finit en capturant la description, l'ajouter au dictionnaire
if capturing_description and description:
info["description"] = "\n".join(description).strip()
elif "description" not in info:
info["description"] = ""
return info
def _analyser_messages(self, section: str) -> List[Dict[str, Any]]:
"""Analyse la section des messages"""
messages = []
current_message = {}
in_message = False
lines = section.strip().split("\n")
for line in lines:
if line.startswith("### Message"):
if current_message:
messages.append(current_message)
current_message = {}
in_message = True
elif line.startswith("**") and in_message:
match = re.match(r"\*\*(.*?)\*\*: (.*)", line)
if match:
key, value = match.groups()
key = key.lower().replace("/", "_").replace(" ", "_")
current_message[key] = value.strip()
else:
if in_message:
current_message["content"] = current_message.get("content", "") + line + "\n"
if current_message:
messages.append(current_message)
# Nettoyer le contenu des messages
for message in messages:
if "content" in message:
message["content"] = message["content"].strip()
return messages
def _analyser_infos_extraction(self, section: str) -> Dict[str, Any]:
"""Analyse la section d'informations sur l'extraction"""
extraction_info = {}
lines = section.strip().split("\n")
for line in lines:
match = re.match(r"- \*\*(.*?)\*\*: (.*)", line)
if match:
key, value = match.groups()
key = key.lower().replace("/", "_").replace(" ", "_")
extraction_info[key] = value.strip()
return extraction_info
class TicketDataLoader:
"""Classe pour charger les données de tickets à partir de différentes sources"""
def __init__(self):
self.sources = {
"json": JsonTicketSource(),
"markdown": MarkdownTicketSource()
}
def detecter_format(self, chemin_fichier: str) -> str:
"""Détecte le format du fichier à partir de son extension"""
ext = os.path.splitext(chemin_fichier)[1].lower()
if ext == '.json':
return "json"
elif ext in ['.md', '.markdown']:
return "markdown"
else:
raise ValueError(f"Format de fichier non supporté: {ext}")
def charger(self, chemin_fichier: str, format_force: Optional[str] = None) -> Dict[str, Any]:
"""
Charge les données d'un ticket à partir d'un fichier
Args:
chemin_fichier: Chemin du fichier à charger
format_force: Format à utiliser (ignore la détection automatique)
Returns:
Dictionnaire contenant les données du ticket
"""
if not os.path.exists(chemin_fichier):
raise FileNotFoundError(f"Le fichier {chemin_fichier} n'existe pas")
format_fichier = format_force if format_force else self.detecter_format(chemin_fichier)
if format_fichier not in self.sources:
raise ValueError(f"Format non supporté: {format_fichier}")
logger.info(f"Chargement des données au format {format_fichier} depuis {chemin_fichier}")
donnees = self.sources[format_fichier].charger(chemin_fichier)
# Validation des données
if not self.sources[format_fichier].valider_donnees(donnees):
logger.warning(f"Les données chargées depuis {chemin_fichier} ne contiennent pas tous les champs obligatoires")
return donnees
def trouver_ticket(self, ticket_dir: str, ticket_id: str) -> Optional[Dict[str, Optional[str]]]:
"""
Recherche des fichiers de ticket dans un répertoire spécifique
Args:
ticket_dir: Répertoire contenant les données du ticket
ticket_id: Code du ticket à rechercher
Returns:
Dictionnaire avec les chemins des fichiers de rapport trouvés (JSON est le format privilégié)
ou None si aucun répertoire valide n'est trouvé
{
"json": chemin_du_fichier_json ou None si non trouvé,
"markdown": chemin_du_fichier_markdown ou None si non trouvé
}
"""
logger.info(f"Recherche du ticket {ticket_id} dans {ticket_dir}")
if not os.path.exists(ticket_dir):
logger.warning(f"Le répertoire {ticket_dir} n'existe pas")
return None
rapport_dir = None
# Chercher d'abord dans le dossier spécifique aux rapports
rapports_dir = os.path.join(ticket_dir, f"{ticket_id}_rapports")
if os.path.exists(rapports_dir) and os.path.isdir(rapports_dir):
rapport_dir = rapports_dir
logger.info(f"Dossier de rapports trouvé: {rapports_dir}")
# Initialiser les chemins à None
json_path = None
md_path = None
# Si on a trouvé un dossier de rapports, chercher dedans
if rapport_dir:
# Privilégier d'abord le format JSON (format principal)
for filename in os.listdir(rapport_dir):
# Chercher le fichier JSON
if filename.endswith(".json") and ticket_id in filename:
json_path = os.path.join(rapport_dir, filename)
logger.info(f"Fichier JSON trouvé: {json_path}")
break # Priorité au premier fichier JSON trouvé
# Chercher le fichier Markdown comme fallback
for filename in os.listdir(rapport_dir):
if filename.endswith(".md") and ticket_id in filename:
md_path = os.path.join(rapport_dir, filename)
logger.info(f"Fichier Markdown trouvé: {md_path}")
break # Priorité au premier fichier Markdown trouvé
else:
# Si pas de dossier de rapports, chercher directement dans le répertoire du ticket
logger.info(f"Pas de dossier _rapports, recherche dans {ticket_dir}")
# Privilégier d'abord le format JSON (format principal)
for filename in os.listdir(ticket_dir):
# Chercher le JSON en priorité
if filename.endswith(".json") and ticket_id in filename and not filename.startswith("ticket_"):
json_path = os.path.join(ticket_dir, filename)
logger.info(f"Fichier JSON trouvé: {json_path}")
break # Priorité au premier fichier JSON trouvé
# Chercher le Markdown comme fallback
for filename in os.listdir(ticket_dir):
if filename.endswith(".md") and ticket_id in filename:
md_path = os.path.join(ticket_dir, filename)
logger.info(f"Fichier Markdown trouvé: {md_path}")
break # Priorité au premier fichier Markdown trouvé
# Si on n'a pas trouvé de fichier, alors renvoyer un dictionnaire vide plutôt que None
if not json_path and not md_path:
logger.warning(f"Aucun fichier de rapport trouvé pour le ticket {ticket_id}")
return {"json": None, "markdown": None}
return {
"json": json_path, # Format principal (prioritaire)
"markdown": md_path # Format secondaire (fallback)
}

File diff suppressed because one or more lines are too long

View File

@ -1,181 +0,0 @@
import os
import base64
import logging
from typing import List, Dict, Any, Optional
from .auth_manager import AuthManager
from core.utils import save_json, normalize_filename
class AttachmentManager:
"""
Gestionnaire de pièces jointes pour extraire et sauvegarder les fichiers attachés aux tickets.
"""
def __init__(self, auth: AuthManager):
"""
Initialise le gestionnaire de pièces jointes.
Args:
auth: Gestionnaire d'authentification
"""
self.auth = auth
self.model_name = "project.task"
self.excluded_mime_types = [] # Types MIME à exclure si nécessaire
def get_ticket_attachments(self, ticket_id: int) -> List[Dict[str, Any]]:
"""
Récupère les pièces jointes associées à un ticket.
Args:
ticket_id: ID du ticket
Returns:
Liste des pièces jointes avec leurs métadonnées
"""
params = {
"model": "ir.attachment",
"method": "search_read",
"args": [[["res_id", "=", ticket_id], ["res_model", "=", self.model_name]]],
"kwargs": {
"fields": ["id", "name", "mimetype", "file_size", "create_date",
"create_uid", "datas", "description", "res_name"]
}
}
attachments = self.auth._rpc_call("/web/dataset/call_kw", params)
# Résoudre les informations sur le créateur
for attachment in attachments:
if "create_uid" in attachment and isinstance(attachment["create_uid"], list) and len(attachment["create_uid"]) >= 2:
attachment["creator_name"] = attachment["create_uid"][1]
attachment["creator_id"] = attachment["create_uid"][0]
elif "create_uid" in attachment and isinstance(attachment["create_uid"], int):
# Récupérer le nom du créateur
params = {
"model": "res.users",
"method": "name_get",
"args": [[attachment["create_uid"]]],
"kwargs": {}
}
result = self.auth._rpc_call("/web/dataset/call_kw", params)
if result and isinstance(result, list) and result[0] and len(result[0]) >= 2:
attachment["creator_name"] = result[0][1]
attachment["creator_id"] = result[0][0]
return attachments if isinstance(attachments, list) else []
def download_attachment(self, attachment: Dict[str, Any], output_dir: str) -> Dict[str, Any]:
"""
Télécharge et sauvegarde une pièce jointe dans le répertoire spécifié.
Args:
attachment: Dictionnaire contenant les métadonnées de la pièce jointe
output_dir: Répertoire où sauvegarder la pièce jointe
Returns:
Dictionnaire avec les informations sur le fichier sauvegardé
"""
result = {
"id": attachment.get("id"),
"name": attachment.get("name", "Sans nom"),
"mimetype": attachment.get("mimetype", "application/octet-stream"),
"file_size": attachment.get("file_size", 0),
"create_date": attachment.get("create_date"),
"creator": attachment.get("creator_name", "Inconnu"),
"status": "error",
"file_path": "",
"error": ""
}
if not attachment.get("datas"):
result["error"] = "Données de pièce jointe manquantes"
return result
try:
# Créer le dossier attachments s'il n'existe pas
attachments_dir = os.path.join(output_dir, "attachments")
os.makedirs(attachments_dir, exist_ok=True)
# Construire un nom de fichier sécurisé
safe_filename = normalize_filename(attachment.get("name", f"attachment_{attachment.get('id')}.bin"))
file_path = os.path.join(attachments_dir, safe_filename)
# Vérifier si un fichier avec le même nom existe déjà
if os.path.exists(file_path):
base, ext = os.path.splitext(safe_filename)
counter = 1
while os.path.exists(file_path):
new_filename = f"{base}_{counter}{ext}"
file_path = os.path.join(attachments_dir, new_filename)
counter += 1
# Décoder et sauvegarder le contenu
file_content = base64.b64decode(attachment["datas"])
with open(file_path, "wb") as f:
f.write(file_content)
result["status"] = "success"
result["file_path"] = file_path
return result
except Exception as e:
logging.error(f"Erreur lors du téléchargement de la pièce jointe {attachment.get('name', '')}: {e}")
result["error"] = str(e)
return result
def save_attachments(self, ticket_id: int, output_dir: str, download: bool = True) -> List[Dict[str, Any]]:
"""
Récupère et sauvegarde toutes les pièces jointes d'un ticket.
Args:
ticket_id: ID du ticket
output_dir: Répertoire de sortie
download: Si True, télécharge les pièces jointes, sinon récupère seulement les métadonnées
Returns:
Liste des informations sur les pièces jointes
"""
# Récupérer les pièces jointes
attachments = self.get_ticket_attachments(ticket_id)
if not attachments:
logging.info(f"Aucune pièce jointe trouvée pour le ticket {ticket_id}")
return []
logging.info(f"Traitement de {len(attachments)} pièces jointes pour le ticket {ticket_id}")
# Préparer les résultats
attachments_info = []
# Télécharger chaque pièce jointe
for i, attachment in enumerate(attachments):
# Ne pas inclure le contenu binaire dans les métadonnées
attachment_meta = {key: value for key, value in attachment.items() if key != "datas"}
if download:
# Télécharger et sauvegarder la pièce jointe
download_result = self.download_attachment(attachment, output_dir)
attachment_meta.update({
"download_status": download_result.get("status"),
"local_path": download_result.get("file_path", ""),
"error": download_result.get("error", "")
})
if download_result.get("status") == "success":
logging.info(f"Pièce jointe téléchargée: {attachment_meta.get('name')} ({i+1}/{len(attachments)})")
else:
logging.warning(f"Échec du téléchargement de la pièce jointe: {attachment_meta.get('name')} - {download_result.get('error')}")
else:
# Seulement récupérer les métadonnées
attachment_meta.update({
"download_status": "not_attempted",
"local_path": "",
"error": ""
})
attachments_info.append(attachment_meta)
# Sauvegarder les informations sur les pièces jointes
attachments_info_path = os.path.join(output_dir, "attachments_info.json")
save_json(attachments_info, attachments_info_path)
return attachments_info

View File

@ -1,181 +0,0 @@
import os
import base64
import logging
from typing import List, Dict, Any, Optional
from .auth_manager import AuthManager
from core.utils import save_json, normalize_filename
class AttachmentManager:
"""
Gestionnaire de pièces jointes pour extraire et sauvegarder les fichiers attachés aux tickets.
"""
def __init__(self, auth: AuthManager):
"""
Initialise le gestionnaire de pièces jointes.
Args:
auth: Gestionnaire d'authentification
"""
self.auth = auth
self.model_name = "project.task"
self.excluded_mime_types = [] # Types MIME à exclure si nécessaire
def get_ticket_attachments(self, ticket_id: int) -> List[Dict[str, Any]]:
"""
Récupère les pièces jointes associées à un ticket.
Args:
ticket_id: ID du ticket
Returns:
Liste des pièces jointes avec leurs métadonnées
"""
params = {
"model": "ir.attachment",
"method": "search_read",
"args": [[["res_id", "=", ticket_id], ["res_model", "=", self.model_name]]],
"kwargs": {
"fields": ["id", "name", "mimetype", "file_size", "create_date",
"create_uid", "datas", "description", "res_name"]
}
}
attachments = self.auth._rpc_call("/web/dataset/call_kw", params)
# Résoudre les informations sur le créateur
for attachment in attachments:
if "create_uid" in attachment and isinstance(attachment["create_uid"], list) and len(attachment["create_uid"]) >= 2:
attachment["creator_name"] = attachment["create_uid"][1]
attachment["creator_id"] = attachment["create_uid"][0]
elif "create_uid" in attachment and isinstance(attachment["create_uid"], int):
# Récupérer le nom du créateur
params = {
"model": "res.users",
"method": "name_get",
"args": [[attachment["create_uid"]]],
"kwargs": {}
}
result = self.auth._rpc_call("/web/dataset/call_kw", params)
if result and isinstance(result, list) and result[0] and len(result[0]) >= 2:
attachment["creator_name"] = result[0][1]
attachment["creator_id"] = result[0][0]
return attachments if isinstance(attachments, list) else []
def download_attachment(self, attachment: Dict[str, Any], output_dir: str) -> Dict[str, Any]:
"""
Télécharge et sauvegarde une pièce jointe dans le répertoire spécifié.
Args:
attachment: Dictionnaire contenant les métadonnées de la pièce jointe
output_dir: Répertoire sauvegarder la pièce jointe
Returns:
Dictionnaire avec les informations sur le fichier sauvegardé
"""
result = {
"id": attachment.get("id"),
"name": attachment.get("name", "Sans nom"),
"mimetype": attachment.get("mimetype", "application/octet-stream"),
"file_size": attachment.get("file_size", 0),
"create_date": attachment.get("create_date"),
"creator": attachment.get("creator_name", "Inconnu"),
"status": "error",
"file_path": "",
"error": ""
}
if not attachment.get("datas"):
result["error"] = "Données de pièce jointe manquantes"
return result
try:
# Créer le dossier attachments s'il n'existe pas
attachments_dir = os.path.join(output_dir, "attachments")
os.makedirs(attachments_dir, exist_ok=True)
# Construire un nom de fichier sécurisé
safe_filename = normalize_filename(attachment.get("name", f"attachment_{attachment.get('id')}.bin"))
file_path = os.path.join(attachments_dir, safe_filename)
# Vérifier si un fichier avec le même nom existe déjà
if os.path.exists(file_path):
base, ext = os.path.splitext(safe_filename)
counter = 1
while os.path.exists(file_path):
new_filename = f"{base}_{counter}{ext}"
file_path = os.path.join(attachments_dir, new_filename)
counter += 1
# Décoder et sauvegarder le contenu
file_content = base64.b64decode(attachment["datas"])
with open(file_path, "wb") as f:
f.write(file_content)
result["status"] = "success"
result["file_path"] = file_path
return result
except Exception as e:
logging.error(f"Erreur lors du téléchargement de la pièce jointe {attachment.get('name', '')}: {e}")
result["error"] = str(e)
return result
def save_attachments(self, ticket_id: int, output_dir: str, download: bool = True) -> List[Dict[str, Any]]:
"""
Récupère et sauvegarde toutes les pièces jointes d'un ticket.
Args:
ticket_id: ID du ticket
output_dir: Répertoire de sortie
download: Si True, télécharge les pièces jointes, sinon récupère seulement les métadonnées
Returns:
Liste des informations sur les pièces jointes
"""
# Récupérer les pièces jointes
attachments = self.get_ticket_attachments(ticket_id)
if not attachments:
logging.info(f"Aucune pièce jointe trouvée pour le ticket {ticket_id}")
return []
logging.info(f"Traitement de {len(attachments)} pièces jointes pour le ticket {ticket_id}")
# Préparer les résultats
attachments_info = []
# Télécharger chaque pièce jointe
for i, attachment in enumerate(attachments):
# Ne pas inclure le contenu binaire dans les métadonnées
attachment_meta = {key: value for key, value in attachment.items() if key != "datas"}
if download:
# Télécharger et sauvegarder la pièce jointe
download_result = self.download_attachment(attachment, output_dir)
attachment_meta.update({
"download_status": download_result.get("status"),
"local_path": download_result.get("file_path", ""),
"error": download_result.get("error", "")
})
if download_result.get("status") == "success":
logging.info(f"Pièce jointe téléchargée: {attachment_meta.get('name')} ({i+1}/{len(attachments)})")
else:
logging.warning(f"Échec du téléchargement de la pièce jointe: {attachment_meta.get('name')} - {download_result.get('error')}")
else:
# Seulement récupérer les métadonnées
attachment_meta.update({
"download_status": "not_attempted",
"local_path": "",
"error": ""
})
attachments_info.append(attachment_meta)
# Sauvegarder les informations sur les pièces jointes
attachments_info_path = os.path.join(output_dir, "attachments_info.json")
save_json(attachments_info, attachments_info_path)
return attachments_info

View File

@ -1,446 +0,0 @@
from typing import List, Dict, Any, Optional, Tuple
from .auth_manager import AuthManager
from formatters.clean_html import clean_html
from core.utils import save_json, save_text, detect_duplicate_content, normalize_filename
import os
import re
import logging
from datetime import datetime
class MessageManager:
"""
Gestionnaire de messages pour traiter les messages associés aux tickets.
"""
def __init__(self, auth: AuthManager):
"""
Initialise le gestionnaire de messages.
Args:
auth: Gestionnaire d'authentification
"""
self.auth = auth
self.model_name = "project.task"
self.cleaning_strategies = {
"simple": {"preserve_links": False, "preserve_images": False, "strategy": "strip_tags"},
"standard": {"preserve_links": True, "preserve_images": True, "strategy": "html2text"},
"advanced": {"preserve_links": True, "preserve_images": True, "strategy": "soup"},
"raw": {"preserve_links": False, "preserve_images": False, "strategy": "none"}
}
self.default_strategy = "standard"
def get_ticket_messages(self, ticket_id: int, fields: Optional[List[str]] = None) -> List[Dict[str, Any]]:
"""
Récupère tous les messages associés à un ticket.
Args:
ticket_id: ID du ticket
fields: Liste des champs à récupérer (facultatif)
Returns:
Liste des messages associés au ticket
"""
if fields is None:
fields = ["id", "body", "date", "author_id", "email_from", "message_type",
"parent_id", "subtype_id", "subject", "tracking_value_ids", "attachment_ids"]
params = {
"model": "mail.message",
"method": "search_read",
"args": [[["res_id", "=", ticket_id], ["model", "=", self.model_name]]],
"kwargs": {
"fields": fields,
"order": "date asc"
}
}
messages = self.auth._rpc_call("/web/dataset/call_kw", params)
return messages if isinstance(messages, list) else []
def is_system_message(self, message: Dict[str, Any]) -> bool:
"""
Vérifie si le message est un message système ou OdooBot.
Args:
message: Le message à vérifier
Returns:
True si c'est un message système, False sinon
"""
is_system = False
# Vérifier le nom de l'auteur
if 'author_id' in message and isinstance(message['author_id'], list) and len(message['author_id']) > 1:
author_name = message['author_id'][1].lower()
if 'odoobot' in author_name or 'bot' in author_name or 'système' in author_name or 'system' in author_name:
is_system = True
# Vérifier le type de message
if message.get('message_type') in ['notification', 'auto_comment']:
is_system = True
# Vérifier le sous-type du message
if 'subtype_id' in message and isinstance(message['subtype_id'], list) and len(message['subtype_id']) > 1:
subtype = message['subtype_id'][1].lower()
if 'notification' in subtype or 'system' in subtype or 'note' in subtype:
is_system = True
return is_system
def is_stage_change_message(self, message: Dict[str, Any]) -> bool:
"""
Vérifie si le message est un changement d'état.
Args:
message: Le message à vérifier
Returns:
True si c'est un message de changement d'état, False sinon
"""
if not isinstance(message.get('body', ''), str):
return False
body = message.get('body', '').lower()
# Patterns pour les changements d'état
stage_patterns = [
'étape changée', 'stage changed', 'modifié l\'étape',
'changed the stage', 'ticket transféré', 'ticket transferred',
'statut modifié', 'status changed', 'état du ticket'
]
# Vérifier aussi les valeurs de tracking si disponibles
if message.get('tracking_value_ids'):
try:
tracking_values = self.auth.read("mail.tracking.value", message.get('tracking_value_ids', []),
["field", "field_desc", "old_value_char", "new_value_char"])
for value in tracking_values:
if value.get("field") == "stage_id" or "stage" in value.get("field_desc", "").lower():
return True
except Exception as e:
logging.warning(f"Erreur lors de la vérification des valeurs de tracking: {e}")
return any(pattern in body for pattern in stage_patterns)
def is_forwarded_message(self, message: Dict[str, Any]) -> bool:
"""
Détecte si un message est un message transféré.
Args:
message: Le message à analyser
Returns:
True si le message est transféré, False sinon
"""
if not message.get('body'):
return False
# Indicateurs de message transféré
forwarded_indicators = [
"message transféré", "forwarded message",
"transféré de", "forwarded from",
"début du message transféré", "begin forwarded message",
"message d'origine", "original message",
"from:", "de:", "to:", "à:", "subject:", "objet:",
"envoyé:", "sent:", "date:", "cc:"
]
# Vérifier le contenu du message
body_lower = message.get('body', '').lower() if isinstance(message.get('body', ''), str) else ""
# Vérifier la présence d'indicateurs de transfert
for indicator in forwarded_indicators:
if indicator in body_lower:
return True
# Vérifier si le sujet contient des préfixes courants de transfert
subject_value = message.get('subject', '')
if not isinstance(subject_value, str):
subject_value = str(subject_value) if subject_value is not None else ""
subject_lower = subject_value.lower()
forwarded_prefixes = ["tr:", "fwd:", "fw:"]
for prefix in forwarded_prefixes:
if subject_lower.startswith(prefix):
return True
# Patterns typiques dans les messages transférés
patterns = [
r"-{3,}Original Message-{3,}",
r"_{3,}Original Message_{3,}",
r">{3,}", # Plusieurs signes > consécutifs indiquent souvent un message cité
r"Le .* a écrit :"
]
for pattern in patterns:
if re.search(pattern, body_lower):
return True
return False
def get_message_author_details(self, message: Dict[str, Any]) -> Dict[str, Any]:
"""
Récupère les détails de l'auteur d'un message.
Args:
message: Le message dont il faut récupérer l'auteur
Returns:
Dictionnaire avec les détails de l'auteur
"""
author_details = {
"name": "Inconnu",
"email": message.get('email_from', ''),
"is_system": False
}
try:
author_id_field = message.get('author_id')
if author_id_field and isinstance(author_id_field, list) and len(author_id_field) > 0:
author_id = author_id_field[0]
params = {
"model": "res.partner",
"method": "read",
"args": [[author_id]],
"kwargs": {"fields": ['name', 'email', 'phone', 'function', 'company_id']}
}
author_data = self.auth._rpc_call("/web/dataset/call_kw", params)
if author_data and isinstance(author_data, list) and len(author_data) > 0:
author_details.update(author_data[0])
# Vérifier si c'est un auteur système
if author_details.get('name'):
author_name = author_details['name'].lower()
if 'odoobot' in author_name or 'bot' in author_name or 'système' in author_name:
author_details['is_system'] = True
except Exception as e:
logging.warning(f"Erreur lors de la récupération des détails de l'auteur: {e}")
return author_details
def process_messages(self, ticket_id: int, ticket_code: str, ticket_name: str, output_dir: str,
strategy: str = "standard") -> Dict[str, Any]:
"""
Traite tous les messages d'un ticket, nettoie le contenu et génère des fichiers structurés.
Args:
ticket_id: ID du ticket
ticket_code: Code du ticket
ticket_name: Nom du ticket
output_dir: Répertoire de sortie
strategy: Stratégie de nettoyage (simple, standard, advanced, raw)
Returns:
Dictionnaire avec les chemins des fichiers créés
"""
# Validation de la stratégie
if strategy not in self.cleaning_strategies:
logging.warning(f"Stratégie de nettoyage '{strategy}' inconnue, utilisation de la stratégie par défaut '{self.default_strategy}'")
strategy = self.default_strategy
cleaning_config = self.cleaning_strategies[strategy]
# Récupérer les messages
messages = self.get_ticket_messages(ticket_id)
# Détecter les messages dupliqués
duplicate_indices = detect_duplicate_content(messages)
# Nettoyer et structurer les messages
processed_messages = []
# Créer un dictionnaire de métadonnées pour chaque message
message_metadata = {}
for index, message in enumerate(messages):
message_id = message.get('id')
# Ajouter des métadonnées au message
message_metadata[message_id] = {
"is_system": self.is_system_message(message),
"is_stage_change": self.is_stage_change_message(message),
"is_forwarded": self.is_forwarded_message(message),
"is_duplicate": index in duplicate_indices
}
# Créer une copie du message pour éviter de modifier l'original
message_copy = message.copy()
# Ajouter les métadonnées au message copié
for key, value in message_metadata[message_id].items():
message_copy[key] = value
# Nettoyer le corps du message selon la stratégie choisie
if message_copy.get('body'):
# Toujours conserver l'original
message_copy['body_original'] = message_copy.get('body', '')
# Appliquer la stratégie de nettoyage, sauf si raw
if strategy != "raw":
cleaned_body = clean_html(
message_copy.get('body', ''),
strategy=cleaning_config['strategy'],
preserve_links=cleaning_config['preserve_links'],
preserve_images=cleaning_config['preserve_images']
)
# Nettoyer davantage le code HTML qui pourrait rester
if cleaned_body:
# Supprimer les balises style et script avec leur contenu
cleaned_body = re.sub(r'<style[^>]*>.*?</style>', '', cleaned_body, flags=re.DOTALL)
cleaned_body = re.sub(r'<script[^>]*>.*?</script>', '', cleaned_body, flags=re.DOTALL)
# Supprimer les balises HTML restantes
cleaned_body = re.sub(r'<[^>]+>', '', cleaned_body)
message_copy['body'] = cleaned_body
# Récupérer les détails de l'auteur
message_copy['author_details'] = self.get_message_author_details(message_copy)
# Ne pas inclure les messages système sans intérêt
if message_copy.get('is_system') and not message_copy.get('is_stage_change'):
# Enregistrer l'exclusion dans les métadonnées
message_metadata[message_id]['excluded'] = "system_message"
continue
# Ignorer les messages dupliqués si demandé
if message_copy.get('is_duplicate'):
# Enregistrer l'exclusion dans les métadonnées
message_metadata[message_id]['excluded'] = "duplicate_content"
continue
processed_messages.append(message_copy)
# Trier les messages par date
processed_messages.sort(key=lambda x: x.get('date', ''))
# Récupérer les informations supplémentaires du ticket
try:
ticket_data = self.auth._rpc_call("/web/dataset/call_kw", {
"model": "project.task",
"method": "read",
"args": [[ticket_id]],
"kwargs": {"fields": ["project_id", "stage_id"]}
})
project_id = None
stage_id = None
project_name = None
stage_name = None
if ticket_data and isinstance(ticket_data, list) and len(ticket_data) > 0:
if "project_id" in ticket_data[0] and ticket_data[0]["project_id"]:
project_id = ticket_data[0]["project_id"][0] if isinstance(ticket_data[0]["project_id"], list) else ticket_data[0]["project_id"]
project_name = ticket_data[0]["project_id"][1] if isinstance(ticket_data[0]["project_id"], list) else None
if "stage_id" in ticket_data[0] and ticket_data[0]["stage_id"]:
stage_id = ticket_data[0]["stage_id"][0] if isinstance(ticket_data[0]["stage_id"], list) else ticket_data[0]["stage_id"]
stage_name = ticket_data[0]["stage_id"][1] if isinstance(ticket_data[0]["stage_id"], list) else None
except Exception as e:
logging.error(f"Erreur lors de la récupération des informations du ticket: {e}")
project_id = None
stage_id = None
project_name = None
stage_name = None
# Créer la structure pour le JSON
messages_with_summary = {
"ticket_summary": {
"id": ticket_id,
"code": ticket_code,
"name": ticket_name,
"project_id": project_id,
"project_name": project_name,
"stage_id": stage_id,
"stage_name": stage_name,
"date_extraction": datetime.now().isoformat()
},
"metadata": {
"message_count": {
"total": len(messages),
"processed": len(processed_messages),
"excluded": len(messages) - len(processed_messages)
},
"cleaning_strategy": strategy,
"cleaning_config": cleaning_config
},
"messages": processed_messages
}
# Sauvegarder les messages en JSON
all_messages_path = os.path.join(output_dir, "all_messages.json")
save_json(messages_with_summary, all_messages_path)
# Sauvegarder également les messages bruts
raw_messages_path = os.path.join(output_dir, "messages_raw.json")
save_json({
"ticket_id": ticket_id,
"ticket_code": ticket_code,
"message_metadata": message_metadata,
"messages": messages
}, raw_messages_path)
# Créer un fichier texte pour une lecture plus facile
messages_text_path = os.path.join(output_dir, "all_messages.txt")
try:
text_content = self._generate_messages_text(ticket_code, ticket_name, processed_messages)
save_text(text_content, messages_text_path)
except Exception as e:
logging.error(f"Erreur lors de la création du fichier texte: {e}")
return {
"all_messages_path": all_messages_path,
"raw_messages_path": raw_messages_path,
"messages_text_path": messages_text_path,
"messages_count": len(processed_messages),
"total_messages": len(messages)
}
def _generate_messages_text(self, ticket_code: str, ticket_name: str,
processed_messages: List[Dict[str, Any]]) -> str:
"""
Génère un fichier texte formaté à partir des messages traités.
Args:
ticket_code: Code du ticket
ticket_name: Nom du ticket
processed_messages: Liste des messages traités
Returns:
Contenu du fichier texte
"""
content = []
# Informations sur le ticket
content.append(f"TICKET: {ticket_code} - {ticket_name}")
content.append(f"Date d'extraction: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
content.append(f"Nombre de messages: {len(processed_messages)}")
content.append("\n" + "="*80 + "\n")
# Parcourir les messages filtrés
for msg in processed_messages:
author = msg.get('author_details', {}).get('name', msg.get('email_from', 'Inconnu'))
date = msg.get('date', '')
subject = msg.get('subject', 'Sans objet')
body = msg.get('body', '')
# Formater différemment les messages spéciaux
if msg.get('is_stage_change'):
content.append("*"*80)
content.append("*** CHANGEMENT D'ÉTAT ***")
content.append("*"*80 + "\n")
elif msg.get('is_forwarded'):
content.append("*"*80)
content.append("*** MESSAGE TRANSFÉRÉ ***")
content.append("*"*80 + "\n")
# En-tête du message
content.append(f"DATE: {date}")
content.append(f"DE: {author}")
if subject:
content.append(f"OBJET: {subject}")
content.append("")
content.append(f"{body}")
content.append("\n" + "-"*80 + "\n")
return "\n".join(content)

View File

@ -1,477 +0,0 @@
import os
import json
import logging
import time
import traceback
from typing import List, Dict, Any, Optional, Union, Mapping, cast
from agents.base_agent import BaseAgent
from loaders.ticket_data_loader import TicketDataLoader
from agents.utils.report_formatter import generer_rapport_markdown
# Configuration du logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s',
filename='orchestrator.log', filemode='w')
logger = logging.getLogger("Orchestrator")
class Orchestrator:
"""
Orchestrateur pour l'analyse de tickets et la génération de rapports.
Stratégie de gestion des formats:
- JSON est le format principal pour le traitement des données et l'analyse
- Markdown est utilisé uniquement comme format de présentation finale
- Les agents LLM travaillent principalement avec le format JSON
- La conversion JSON->Markdown se fait uniquement à la fin du processus pour la présentation
Cette approche permet de:
1. Simplifier le code des agents
2. Réduire les redondances et incohérences entre formats
3. Améliorer la performance des agents LLM avec un format plus structuré
4. Faciliter la maintenance et l'évolution du système
"""
def __init__(self,
output_dir: str = "output/",
ticket_agent: Optional[BaseAgent] = None,
image_sorter: Optional[BaseAgent] = None,
image_analyser: Optional[BaseAgent] = None,
report_generator: Optional[BaseAgent] = None):
self.output_dir = output_dir
# Assignation directe des agents
self.ticket_agent = ticket_agent
self.image_sorter = image_sorter
self.image_analyser = image_analyser
self.report_generator = report_generator
# Initialisation du loader de données de ticket
self.ticket_loader = TicketDataLoader()
# Collecter et enregistrer les informations détaillées sur les agents
agents_info = self._collecter_info_agents()
logger.info(f"Orchestrator initialisé avec output_dir: {output_dir}")
logger.info(f"Agents disponibles: TicketAgent={ticket_agent is not None}, ImageSorter={image_sorter is not None}, ImageAnalyser={image_analyser is not None}, ReportGenerator={report_generator is not None}")
logger.info(f"Configuration des agents: {json.dumps(agents_info, indent=2)}")
def _collecter_info_agents(self) -> Dict[str, Dict[str, Any]]:
"""
Collecte des informations détaillées sur les agents configurés
"""
agents_info = {}
# Information sur l'agent Ticket
if self.ticket_agent:
agents_info["ticket_agent"] = self._get_agent_info(self.ticket_agent)
# Information sur l'agent Image Sorter
if self.image_sorter:
agents_info["image_sorter"] = self._get_agent_info(self.image_sorter)
# Information sur l'agent Image Analyser
if self.image_analyser:
agents_info["image_analyser"] = self._get_agent_info(self.image_analyser)
# Information sur l'agent Report Generator
if self.report_generator:
agents_info["report_generator"] = self._get_agent_info(self.report_generator)
return agents_info
def detecter_tickets(self) -> List[str]:
"""Détecte tous les tickets disponibles dans le répertoire de sortie"""
logger.info(f"Recherche de tickets dans: {self.output_dir}")
tickets = []
if not os.path.exists(self.output_dir):
logger.warning(f"Le répertoire de sortie {self.output_dir} n'existe pas")
print(f"ERREUR: Le répertoire {self.output_dir} n'existe pas")
return tickets
for ticket_dir in os.listdir(self.output_dir):
ticket_path = os.path.join(self.output_dir, ticket_dir)
if os.path.isdir(ticket_path) and ticket_dir.startswith("ticket_"):
tickets.append(ticket_dir)
return tickets
def trouver_rapport(self, extraction_path: str, ticket_id: str) -> Dict[str, Optional[str]]:
"""
Cherche les rapports disponibles (JSON et/ou MD) pour un ticket
Args:
extraction_path: Chemin vers l'extraction
ticket_id: ID du ticket
Returns:
Dictionnaire avec {"json": chemin_json, "markdown": chemin_md}
"""
# Utiliser la méthode du TicketDataLoader pour trouver les fichiers
result = self.ticket_loader.trouver_ticket(extraction_path, ticket_id)
# S'assurer que nous avons un dictionnaire avec la structure correcte
rapports: Dict[str, Optional[str]] = {"json": None, "markdown": None} if result is None else result
# Si on a un JSON mais pas de Markdown, générer le Markdown à partir du JSON
json_path = rapports.get("json")
if json_path and not rapports.get("markdown"):
logger.info(f"Rapport JSON trouvé sans Markdown correspondant, génération du Markdown: {json_path}")
md_path = generer_rapport_markdown(json_path, True)
if md_path:
rapports["markdown"] = md_path
logger.info(f"Markdown généré avec succès: {md_path}")
else:
logger.warning(f"Erreur lors de la génération du Markdown")
return rapports
def executer(self, ticket_specifique: Optional[str] = None):
"""
Exécute l'orchestrateur soit sur un ticket spécifique, soit sur tous les tickets
Args:
ticket_specifique: Code du ticket spécifique à traiter (optionnel)
"""
start_time = time.time()
# Obtenir la liste des tickets
if ticket_specifique:
# Chercher le ticket spécifique
ticket_path = os.path.join(self.output_dir, f"ticket_{ticket_specifique}")
if os.path.exists(ticket_path):
ticket_dirs = [ticket_path]
logger.info(f"Ticket spécifique à traiter: {ticket_specifique}")
print(f"Ticket spécifique à traiter: {ticket_specifique}")
else:
logger.error(f"Le ticket {ticket_specifique} n'existe pas")
print(f"ERREUR: Le ticket {ticket_specifique} n'existe pas")
return
else:
# Lister tous les tickets
ticket_dirs = [os.path.join(self.output_dir, d) for d in self.detecter_tickets()]
logger.info(f"Tickets à traiter: {len(ticket_dirs)}")
if not ticket_dirs:
logger.warning("Aucun ticket trouvé dans le répertoire de sortie")
print("Aucun ticket trouvé dans le répertoire de sortie")
return
# Un seul log de début d'exécution
logger.info("Début de l'exécution de l'orchestrateur")
print("Début de l'exécution de l'orchestrateur")
# Traitement des tickets
for ticket_dir in ticket_dirs:
try:
self.traiter_ticket(ticket_dir)
except Exception as e:
logger.error(f"Erreur lors du traitement du ticket {ticket_dir}: {str(e)}")
print(f"Erreur lors du traitement du ticket {ticket_dir}: {str(e)}")
traceback.print_exc()
# Calcul de la durée d'exécution
duration = time.time() - start_time
logger.info(f"Fin de l'exécution de l'orchestrateur (durée: {duration:.2f} secondes)")
print(f"Fin de l'exécution de l'orchestrateur (durée: {duration:.2f} secondes)")
def traiter_ticket(self, ticket_path: str) -> bool:
"""Traite un ticket spécifique et retourne True si le traitement a réussi"""
logger.info(f"Début du traitement du ticket: {ticket_path}")
print(f"\nTraitement du ticket: {os.path.basename(ticket_path)}")
success = False
extractions_trouvees = False
if not os.path.exists(ticket_path):
logger.error(f"Le chemin du ticket n'existe pas: {ticket_path}")
print(f"ERREUR: Le chemin du ticket n'existe pas: {ticket_path}")
return False
ticket_id = os.path.basename(ticket_path).replace("ticket_", "")
for extraction in os.listdir(ticket_path):
extraction_path = os.path.join(ticket_path, extraction)
if os.path.isdir(extraction_path):
extractions_trouvees = True
logger.info(f"Traitement de l'extraction: {extraction}")
print(f" Traitement de l'extraction: {extraction}")
# Recherche des rapports (JSON et MD) dans différents emplacements
rapports = self.trouver_rapport(extraction_path, ticket_id)
# Dossier des pièces jointes
attachments_dir = os.path.join(extraction_path, "attachments")
# Dossier pour les rapports générés
rapports_dir = os.path.join(extraction_path, f"{ticket_id}_rapports")
os.makedirs(rapports_dir, exist_ok=True)
# Préparer les données du ticket à partir des rapports trouvés
ticket_data = self._preparer_donnees_ticket(rapports, ticket_id)
if ticket_data:
success = True
logger.info(f"Données du ticket chargées avec succès")
print(f" Données du ticket chargées")
# Traitement avec l'agent Ticket
if self.ticket_agent:
logger.info("Exécution de l'agent Ticket")
print(" Analyse du ticket en cours...")
# Log détaillé sur l'agent Ticket
agent_info = self._get_agent_info(self.ticket_agent)
logger.info(f"Agent Ticket: {json.dumps(agent_info, indent=2)}")
ticket_analysis = self.ticket_agent.executer(ticket_data)
logger.info("Analyse du ticket terminée")
print(f" Analyse du ticket terminée: {len(ticket_analysis) if ticket_analysis else 0} caractères")
else:
logger.warning("Agent Ticket non disponible")
ticket_analysis = None
print(" Agent Ticket non disponible, analyse ignorée")
# Traitement des images
relevant_images = []
images_analyses = {}
images_count = 0
if os.path.exists(attachments_dir):
logger.info(f"Vérification des pièces jointes dans: {attachments_dir}")
print(f" Vérification des pièces jointes...")
# Log détaillé sur l'agent Image Sorter
if self.image_sorter:
agent_info = self._get_agent_info(self.image_sorter)
logger.info(f"Agent Image Sorter: {json.dumps(agent_info, indent=2)}")
# Compter le nombre d'images
images = [f for f in os.listdir(attachments_dir)
if f.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif'))]
images_count = len(images)
# Tri des images
for img in images:
img_path = os.path.join(attachments_dir, img)
if self.image_sorter:
logger.info(f"Évaluation de la pertinence de l'image: {img}")
print(f" Évaluation de l'image: {img}")
sorting_result = self.image_sorter.executer(img_path)
is_relevant = sorting_result.get("is_relevant", False)
reason = sorting_result.get("reason", "")
# Log détaillé du résultat
if is_relevant:
logger.info(f"Image {img} considérée comme pertinente")
else:
logger.info(f"Image {img} considérée comme non pertinente")
# Ajouter les métadonnées de tri à la liste des analyses
images_analyses[img_path] = {
"sorting": sorting_result,
"analysis": None # Sera rempli plus tard si pertinent
}
if is_relevant:
logger.info(f"Image pertinente identifiée: {img} ({reason})")
print(f" => Pertinente: {reason}")
relevant_images.append(img_path)
else:
logger.info(f"Image non pertinente: {img} ({reason})")
print(f" => Non pertinente: {reason}")
else:
logger.warning("Image Sorter non disponible")
# Si pas de tri, considérer toutes les images comme pertinentes
relevant_images.append(img_path)
images_analyses[img_path] = {
"sorting": {"is_relevant": True, "reason": "Auto-sélectionné (pas de tri)"},
"analysis": None
}
print(f" => Auto-sélectionné (pas de tri)")
logger.info(f"Images analysées: {images_count}, Images pertinentes: {len(relevant_images)}")
print(f" Images analysées: {images_count}, Images pertinentes: {len(relevant_images)}")
else:
logger.warning(f"Répertoire des pièces jointes non trouvé: {attachments_dir}")
print(f" Répertoire des pièces jointes non trouvé")
# Analyse approfondie des images pertinentes
if relevant_images and self.image_analyser:
agent_info = self._get_agent_info(self.image_analyser)
logger.info(f"Agent Image Analyser: {json.dumps(agent_info, indent=2)}")
# S'assurer que l'analyse du ticket est disponible comme contexte
contexte_ticket = ticket_analysis if ticket_analysis else "Aucune analyse de ticket disponible"
# Analyse de chaque image pertinente
for image_path in relevant_images:
image_name = os.path.basename(image_path)
logger.info(f"Analyse approfondie de l'image: {image_name}")
print(f" Analyse approfondie de l'image: {image_name}")
# Appeler l'analyseur d'images avec le contexte du ticket
analysis_result = self.image_analyser.executer(image_path, contexte=contexte_ticket)
if images_analyses[image_path]:
images_analyses[image_path]["analysis"] = analysis_result
logger.info(f"Analyse complétée pour {image_name}")
# Préparer les données pour le rapport final
rapport_data = {
"ticket_data": ticket_data,
"ticket_id": ticket_id,
"ticket_analyse": ticket_analysis,
"analyse_images": images_analyses,
"metadata": {
"timestamp_debut": self._get_timestamp(),
"ticket_id": ticket_id,
"images_analysees": images_count,
"images_pertinentes": len(relevant_images)
}
}
# Génération du rapport final
if self.report_generator:
logger.info("Génération du rapport final")
print(" Génération du rapport final")
# Log détaillé sur l'agent Report Generator
agent_info = self._get_agent_info(self.report_generator)
logger.info(f"Agent Report Generator: {json.dumps(agent_info, indent=2)}")
# Créer le répertoire pour le rapport dans reports/
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__)))
reports_root_dir = os.path.join(project_root, 'reports')
ticket_reports_dir = os.path.join(reports_root_dir, ticket_id)
# Créer le sous-répertoire pour le modèle spécifique
model_name = getattr(self.report_generator.llm, "modele", str(type(self.report_generator.llm)))
model_reports_dir = os.path.join(ticket_reports_dir, model_name)
os.makedirs(model_reports_dir, exist_ok=True)
# Générer le rapport
json_path, md_path = self.report_generator.executer(rapport_data, model_reports_dir)
if json_path:
logger.info(f"Rapport JSON généré à: {json_path}")
print(f" Rapport JSON généré avec succès: {os.path.basename(json_path)}")
# Utiliser directement le rapport Markdown généré par l'agent
if md_path:
logger.info(f"Rapport Markdown généré à: {md_path}")
print(f" Rapport Markdown généré avec succès: {os.path.basename(md_path)}")
else:
logger.warning("Report Generator non disponible")
print(" Report Generator non disponible, génération de rapport ignorée")
print(f"Traitement du ticket {os.path.basename(ticket_path)} terminé avec succès.\n")
logger.info(f"Traitement du ticket {ticket_path} terminé avec succès.")
else:
logger.warning(f"Aucune donnée de ticket trouvée pour: {ticket_id}")
print(f" ERREUR: Aucune donnée de ticket trouvée pour {ticket_id}")
if not extractions_trouvees:
logger.warning(f"Aucune extraction trouvée dans le ticket: {ticket_path}")
print(f" ERREUR: Aucune extraction trouvée dans le ticket")
return success
def _preparer_donnees_ticket(self, rapports: Dict[str, Optional[str]], ticket_id: str) -> Optional[Dict]:
"""
Prépare les données du ticket à partir des rapports trouvés (JSON et/ou MD)
Args:
rapports: Dictionnaire avec les chemins des rapports JSON et MD
ticket_id: ID du ticket
Returns:
Dictionnaire avec les données du ticket, ou None si aucun rapport n'est trouvé
"""
ticket_data = None
# Si aucun rapport n'est trouvé
if not rapports or (not rapports.get("json") and not rapports.get("markdown")):
logger.warning(f"Aucun rapport trouvé pour le ticket {ticket_id}")
return None
# Privilégier le format JSON (format principal)
if rapports.get("json") and rapports["json"] is not None:
try:
ticket_data = self.ticket_loader.charger(rapports["json"])
logger.info(f"Données JSON chargées depuis: {rapports['json']}")
print(f" Rapport JSON chargé: {os.path.basename(rapports['json'])}")
# Ajouter une métadonnée sur le format source
if ticket_data and "metadata" not in ticket_data:
ticket_data["metadata"] = {}
if ticket_data:
ticket_data["metadata"]["format_source"] = "json"
except Exception as e:
logger.error(f"Erreur lors du chargement du JSON: {e}")
print(f" ERREUR: Impossible de charger le fichier JSON: {e}")
# Fallback sur le Markdown uniquement si JSON non disponible
if not ticket_data and rapports.get("markdown") and rapports["markdown"] is not None:
try:
# Utiliser le loader pour charger les données depuis le Markdown
ticket_data = self.ticket_loader.charger(rapports["markdown"])
logger.info(f"Données Markdown chargées depuis: {rapports['markdown']} (fallback)")
print(f" Rapport Markdown chargé (fallback): {os.path.basename(rapports['markdown'])}")
# Ajouter une métadonnée sur le format source
if ticket_data and "metadata" not in ticket_data:
ticket_data["metadata"] = {}
if ticket_data:
ticket_data["metadata"]["format_source"] = "markdown"
except Exception as e:
logger.error(f"Erreur lors du chargement du Markdown: {e}")
print(f" ERREUR: Impossible de charger le fichier Markdown: {e}")
# Assurer que l'ID du ticket est correct
if ticket_data:
ticket_data["code"] = ticket_id
return ticket_data
def _get_timestamp(self) -> str:
"""Retourne un timestamp au format YYYYMMDD_HHMMSS"""
from datetime import datetime
return datetime.now().strftime("%Y%m%d_%H%M%S")
def _get_agent_info(self, agent: Optional[BaseAgent]) -> Dict:
"""
Récupère les informations détaillées sur un agent.
Args:
agent: L'agent dont on veut récupérer les informations
Returns:
Dictionnaire contenant les informations de l'agent
"""
if not agent:
return {"status": "non configuré"}
# Récupérer les informations du modèle
model_info = {
"nom": agent.nom,
"model": getattr(agent.llm, "modele", str(type(agent.llm))),
}
# Ajouter les paramètres de configuration s'ils sont disponibles directement dans l'agent
# Utiliser getattr avec une valeur par défaut pour éviter les erreurs
model_info["temperature"] = getattr(agent, "temperature", None)
model_info["top_p"] = getattr(agent, "top_p", None)
model_info["max_tokens"] = getattr(agent, "max_tokens", None)
# Ajouter le prompt système s'il est disponible
if hasattr(agent, "system_prompt"):
prompt_preview = getattr(agent, "system_prompt", "")
# Tronquer le prompt s'il est trop long
if prompt_preview and len(prompt_preview) > 200:
prompt_preview = prompt_preview[:200] + "..."
model_info["system_prompt_preview"] = prompt_preview
# Supprimer les valeurs None
model_info = {k: v for k, v in model_info.items() if v is not None}
return model_info

View File

@ -1,44 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Ce script était utilisé pour tester l'extraction d'images intégrées dans le HTML.
Cette fonctionnalité a été désactivée suite à la suppression du module utils/image_extractor.
Ce fichier est conservé à titre d'exemple mais n'est plus fonctionnel.
"""
from odoo.attachment_manager import AttachmentManager
from odoo.auth_manager import AuthManager
import json
import os
def main():
print("La fonctionnalité d'extraction d'images intégrées dans le HTML a été désactivée.")
print("Ce script est conservé à titre d'exemple mais n'est plus fonctionnel.")
print("Si vous avez besoin de cette fonctionnalité, réinstallez le module utils/image_extractor.")
# Voici l'ancien code à titre d'exemple :
"""
# Initialiser le gestionnaire d'authentification avec des valeurs factices
auth = AuthManager(url='https://odoo.cbao.fr', db='dummy', username='dummy', api_key='dummy')
# Chemin vers le dossier du ticket
ticket_folder = 'output/ticket_T11143/T11143_20250416_094512'
# Charger les données des messages
with open(os.path.join(ticket_folder, 'all_messages.json'), 'r') as f:
messages_data = json.load(f)
# Créer le gestionnaire de pièces jointes
am = AttachmentManager(auth)
# Extraire les images manquantes
extracted_images = am.extract_missing_images(messages_data, ticket_folder)
print(f"Nombre d'images extraites: {len(extracted_images)}")
for img in extracted_images:
print(f"Image extraite: {img.get('name', 'Sans nom')} - {img.get('url', '')}")
"""
if __name__ == "__main__":
main()

View File

@ -1,267 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script simple pour tester directement l'API Mistral Large.
Ce script permet de diagnostiquer si le problème vient de l'API ou de notre application.
"""
import os
import sys
import json
import logging
import requests
from datetime import datetime
# Configuration du logging
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(sys.stdout)
]
)
logger = logging.getLogger("test_mistral_api")
# Clés API à tester
API_KEYS = [
"2iGzTzE9csRQ9IoASoUjplHwEjA200Vh", # Clé actuelle dans MistralLarge
"sk-d359d9236ca84a5986f889631832d1e6" # Clé utilisée dans list_mistral_models.py
]
def test_api_key(api_key):
"""Test si la clé API est valide en listant les modèles disponibles."""
url = "https://api.mistral.ai/v1/models"
headers = {
"Authorization": f"Bearer {api_key}"
}
try:
logger.info(f"Test de la clé API: {api_key[:5]}...{api_key[-5:]}")
response = requests.get(url, headers=headers)
logger.info(f"Code de statut: {response.status_code}")
if response.status_code == 200:
logger.info("La clé API est valide!")
try:
models = response.json().get('data', [])
logger.info(f"Modèles disponibles: {[model.get('id') for model in models]}")
return True
except Exception as e:
logger.error(f"Erreur lors du parsing de la réponse: {e}")
logger.error(f"Réponse brute: {response.text}")
return False
else:
logger.error(f"Erreur API: {response.status_code}")
logger.error(f"Réponse d'erreur: {response.text}")
return False
except Exception as e:
logger.error(f"Exception lors du test de la clé API: {e}")
return False
def test_direct_chat_completion(api_key):
"""Test directement l'endpoint chat/completions."""
url = "https://api.mistral.ai/v1/chat/completions"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}"
}
data = {
"model": "mistral-large-latest",
"messages": [
{"role": "system", "content": "Tu es un assistant utile."},
{"role": "user", "content": "Dis simplement 'API FONCTIONNE' sans rien ajouter."}
],
"temperature": 0.1,
"max_tokens": 100
}
try:
logger.info(f"Test d'appel direct à chat/completions avec la clé: {api_key[:5]}...{api_key[-5:]}")
response = requests.post(url, headers=headers, json=data)
logger.info(f"Code de statut: {response.status_code}")
if response.status_code in [200, 201]:
try:
result = response.json()
message = result.get("choices", [{}])[0].get("message", {}).get("content", "")
logger.info(f"Réponse: {message}")
return True
except Exception as e:
logger.error(f"Erreur lors du parsing de la réponse: {e}")
logger.error(f"Réponse brute: {response.text}")
return False
else:
logger.error(f"Erreur API: {response.status_code}")
logger.error(f"Réponse d'erreur: {response.text}")
return False
except Exception as e:
logger.error(f"Exception lors de l'appel direct: {e}")
return False
# Essayer d'importer la classe MistralLarge
try:
from llm_classes.mistral_large import MistralLarge
logger.info("Module MistralLarge importé avec succès")
except ImportError as e:
logger.error(f"Impossible d'importer MistralLarge: {e}")
sys.exit(1)
def test_simple_prompt():
"""Test avec un prompt simple pour vérifier que l'API répond."""
try:
logger.info("Initialisation du modèle Mistral Large...")
model = MistralLarge()
logger.info("Modèle initialisé avec succès")
# Afficher la clé API utilisée
api_key = model.cleAPI()
logger.info(f"Clé API utilisée: {api_key[:5]}...{api_key[-5:]}")
# Prompt simple pour tester la connexion
prompt = "Réponds uniquement avec le texte 'API MISTRAL FONCTIONNE' sans rien ajouter d'autre."
logger.info(f"Envoi du prompt simple: {prompt}")
start_time = datetime.now()
response = model.interroger(prompt)
end_time = datetime.now()
duration = (end_time - start_time).total_seconds()
logger.info(f"Réponse reçue en {duration} secondes")
if response:
logger.info(f"Réponse complète: {response}")
# Vérifier si la réponse contient une erreur API
if "erreur" in response.lower() or "error" in response.lower():
logger.error("La réponse contient une erreur")
logger.error(f"Détails de l'erreur: {response}")
return False
return True
else:
logger.error("Réponse vide reçue")
return False
except Exception as e:
logger.error(f"Erreur lors du test avec prompt simple: {e}")
import traceback
logger.error(traceback.format_exc())
return False
def test_json_prompt():
"""Test avec un prompt demandant un JSON pour vérifier si c'est le format qui pose problème."""
try:
logger.info("Initialisation du modèle Mistral Large...")
model = MistralLarge()
logger.info("Modèle initialisé avec succès")
# Prompt demandant un JSON simple
prompt = """
Réponds uniquement avec un JSON valide au format suivant, sans aucun texte avant ou après:
{
"test": "réussi",
"timestamp": "heure actuelle",
"message": "API Mistral fonctionne correctement"
}
"""
logger.info(f"Envoi du prompt JSON: {prompt}")
start_time = datetime.now()
response = model.interroger(prompt)
end_time = datetime.now()
duration = (end_time - start_time).total_seconds()
logger.info(f"Réponse reçue en {duration} secondes")
if response:
logger.info(f"Réponse complète: {response}")
# Vérifier si la réponse contient une erreur API
if "erreur" in response.lower() or "error" in response.lower():
logger.error("La réponse contient une erreur")
logger.error(f"Détails de l'erreur: {response}")
return False
# Essayer de parser le JSON
try:
# Chercher un bloc de code JSON
import re
match = re.search(r"```(?:json)?\s*([\s\S]*?)\s*```", response)
if match:
response_cleaned = match.group(1).strip()
logger.info(f"JSON trouvé dans un bloc de code Markdown")
else:
response_cleaned = response
json_response = json.loads(response_cleaned)
logger.info(f"JSON valide reçu: {json.dumps(json_response, indent=2)}")
return True
except json.JSONDecodeError as e:
logger.error(f"Réponse reçue mais JSON invalide: {e}")
return False
else:
logger.error("Réponse vide reçue")
return False
except Exception as e:
logger.error(f"Erreur lors du test avec prompt JSON: {e}")
import traceback
logger.error(traceback.format_exc())
return False
def dump_model_info():
"""Affiche les informations sur le modèle et sa configuration."""
try:
model = MistralLarge()
logger.info("Informations sur le modèle:")
# Récupérer et afficher les attributs du modèle
for attr_name in dir(model):
if not attr_name.startswith('_'): # Ignorer les attributs privés
try:
attr_value = getattr(model, attr_name)
# N'afficher que les attributs simples, pas les méthodes
if not callable(attr_value):
logger.info(f" {attr_name}: {attr_value}")
except Exception as e:
logger.warning(f" Impossible d'accéder à {attr_name}: {e}")
# Afficher les méthodes disponibles
methods = [m for m in dir(model) if not m.startswith('_') and callable(getattr(model, m))]
logger.info(f"Méthodes disponibles: {', '.join(methods)}")
return True
except Exception as e:
logger.error(f"Erreur lors de la récupération des informations du modèle: {e}")
return False
def main():
"""Fonction principale exécutant les tests."""
logger.info("=== DÉBUT DES TESTS API MISTRAL ===")
logger.info("=== TEST DES CLÉS API ===")
for api_key in API_KEYS:
test_api_key(api_key)
test_direct_chat_completion(api_key)
logger.info("\n=== INFORMATIONS SUR LE MODÈLE ===")
dump_model_info()
logger.info("\n=== TEST AVEC PROMPT SIMPLE ===")
test_simple = test_simple_prompt()
logger.info("\n=== TEST AVEC PROMPT JSON ===")
test_json = test_json_prompt()
logger.info("\n=== RÉSULTATS DES TESTS ===")
logger.info(f"Test simple: {'RÉUSSI' if test_simple else 'ÉCHEC'}")
logger.info(f"Test JSON: {'RÉUSSI' if test_json else 'ÉCHEC'}")
logger.info("=== FIN DES TESTS ===")
return 0 if test_simple and test_json else 1
if __name__ == "__main__":
sys.exit(main())