mirror of
https://github.com/Ladebeze66/llm_ticket3.git
synced 2025-12-13 09:06:51 +01:00
1804-15:26
This commit is contained in:
parent
4e7a2178d3
commit
7419e4d1e1
159
README.md
159
README.md
@ -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.
|
||||
@ -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
|
||||
@ -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")
|
||||
|
||||
@ -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")
|
||||
@ -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")
|
||||
@ -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")
|
||||
@ -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")
|
||||
@ -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")
|
||||
@ -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")
|
||||
@ -1,3 +0,0 @@
|
||||
"""
|
||||
Module de tests pour les fonctionnalités core.
|
||||
"""
|
||||
@ -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()
|
||||
@ -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"")
|
||||
|
||||
# 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"")
|
||||
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"")
|
||||
|
||||
# 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"")
|
||||
|
||||
# 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(' ', ' ')
|
||||
content = content.replace('<', '<')
|
||||
content = content.replace('>', '>')
|
||||
content = content.replace('&', '&')
|
||||
content = content.replace('"', '"')
|
||||
|
||||
# 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"\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></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) .
|
||||
|
||||
 """
|
||||
|
||||
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) .
|
||||
|
||||
 """
|
||||
|
||||
cleaned_rapport = clean_html(test_rapport)
|
||||
print("\nTest avec cas exact du rapport nettoyé :\n", cleaned_rapport)
|
||||
@ -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
@ -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
|
||||
@ -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
|
||||
@ -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)
|
||||
477
orchestrator.bak
477
orchestrator.bak
@ -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
|
||||
@ -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()
|
||||
@ -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())
|
||||
Loading…
x
Reference in New Issue
Block a user