mirror of
https://github.com/Ladebeze66/llm_ticket3.git
synced 2025-12-13 09:06:51 +01:00
1404-9:02
This commit is contained in:
parent
54dc4eff9e
commit
d86e0f0fbc
2
.cursorindexingignore
Normal file
2
.cursorindexingignore
Normal file
@ -0,0 +1,2 @@
|
||||
# Don't index SpecStory auto-save files, but allow explicit context inclusion via @ references
|
||||
.specstory/**
|
||||
@ -1,94 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Script de test pour les fonctions d'extraction de JSON et d'extraction de questions initiales.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from agents.utils.report_utils import extraire_et_traiter_json, extraire_question_initiale
|
||||
|
||||
def test_extraction_json():
|
||||
"""Teste la fonction d'extraction de JSON depuis un texte de rapport."""
|
||||
# Texte de rapport avec JSON
|
||||
rapport = """# Rapport d'analyse
|
||||
|
||||
Voici le rapport.
|
||||
|
||||
## Tableau questions/réponses
|
||||
|
||||
```json
|
||||
{
|
||||
"chronologie_echanges": [
|
||||
{"date": "01/08/2022 12:00:00", "emetteur": "SUPPORT", "type": "Réponse", "contenu": "Bonjour, je te contacte concernant : Mon problème technique. Je l'ai résolu."},
|
||||
{"date": "01/08/2022 13:00:00", "emetteur": "CLIENT", "type": "Question", "contenu": "Merci beaucoup!"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
Fin du rapport.
|
||||
"""
|
||||
|
||||
# Extraire le JSON
|
||||
rapport_traite, echanges_json, _ = extraire_et_traiter_json(rapport)
|
||||
print("JSON extrait:", json.dumps(echanges_json, indent=2, ensure_ascii=False))
|
||||
|
||||
# Vérifier
|
||||
assert echanges_json is not None, "Le JSON n'a pas été extrait"
|
||||
assert "chronologie_echanges" in echanges_json, "Structure JSON incorrecte"
|
||||
assert len(echanges_json["chronologie_echanges"]) >= 2, "Nombre d'échanges incorrect"
|
||||
|
||||
# Vérifier si une question initiale a été ajoutée
|
||||
if len(echanges_json["chronologie_echanges"]) > 2:
|
||||
print("Question initiale ajoutée:", json.dumps(echanges_json["chronologie_echanges"][0], indent=2, ensure_ascii=False))
|
||||
return True
|
||||
|
||||
def test_extraire_question_initiale():
|
||||
"""Teste la fonction d'extraction de questions initiales."""
|
||||
# Cas 1: Premier message est déjà une question du client
|
||||
echanges1 = [
|
||||
{"date": "01/08/2022 11:30:00", "emetteur": "CLIENT", "type": "Question", "contenu": "J'ai un problème"}
|
||||
]
|
||||
question1 = extraire_question_initiale(echanges1)
|
||||
print("Test 1 - Premier message est une question:", question1)
|
||||
assert question1 is None, "Ne devrait pas extraire une question si déjà présente"
|
||||
|
||||
# Cas 2: Premier message est une réponse du support qui cite la question
|
||||
echanges2 = [
|
||||
{"date": "01/08/2022 12:11:03", "emetteur": "SUPPORT", "type": "Réponse",
|
||||
"contenu": "Bonjour, Je te contacte pour donner suite à ta demande concernant : \nGuillaume Lucas ne parvient jamais à enregistrer un échantillon\n\nJe viens de corriger ton problème."}
|
||||
]
|
||||
question2 = extraire_question_initiale(echanges2)
|
||||
print("Test 2 - Question extraite:", question2)
|
||||
assert question2 is not None, "Devrait extraire une question"
|
||||
assert question2["emetteur"] == "CLIENT", "L'émetteur devrait être CLIENT"
|
||||
assert question2["type"] == "Question", "Le type devrait être Question"
|
||||
assert "Guillaume Lucas" in question2["contenu"], "Le contenu devrait contenir la question extraite"
|
||||
|
||||
# Cas 3: Format différent de citation
|
||||
echanges3 = [
|
||||
{"date": "01/08/2022 14:00:00", "emetteur": "SUPPORT", "type": "Réponse",
|
||||
"contenu": "Bonjour\nJe réponds à votre ticket: Problème de connexion à l'application\n\nVoici la solution."}
|
||||
]
|
||||
question3 = extraire_question_initiale(echanges3)
|
||||
print("Test 3 - Autre format:", question3)
|
||||
assert question3 is not None, "Devrait extraire une question"
|
||||
assert "Problème de connexion" in question3["contenu"], "Le contenu devrait contenir la question extraite"
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== Test d'extraction JSON ===")
|
||||
json_test_success = test_extraction_json()
|
||||
|
||||
print("\n=== Test d'extraction de questions initiales ===")
|
||||
question_test_success = test_extraire_question_initiale()
|
||||
|
||||
if json_test_success and question_test_success:
|
||||
print("\nTous les tests sont réussis! ✅")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("\nCertains tests ont échoué ❌")
|
||||
sys.exit(1)
|
||||
@ -1,8 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import agents.agent_ticket_analyser
|
||||
import agents.agent_image_sorter
|
||||
import agents.agent_image_analyser
|
||||
import agents.agent_report_generator
|
||||
|
||||
print('Tests réussis! Tous les agents ont été importés correctement.')
|
||||
@ -1,25 +0,0 @@
|
||||
from llm_classes.mistral_medium import MistralMedium
|
||||
from llm_classes.pixtral_12b import Pixtral12b
|
||||
|
||||
print("Initialisation des modèles LLM...")
|
||||
|
||||
# Initialisation des modèles
|
||||
try:
|
||||
text_model = MistralMedium()
|
||||
image_model = Pixtral12b()
|
||||
print("Modèles initialisés avec succès!")
|
||||
except Exception as e:
|
||||
print(f"Erreur lors de l'initialisation des modèles: {str(e)}")
|
||||
exit(1)
|
||||
|
||||
# Test d'interrogation simple
|
||||
try:
|
||||
question = "Quelle est la capitale de la France?"
|
||||
print(f"\nTest d'interrogation simple sur MistralMedium:")
|
||||
print(f"Question: {question}")
|
||||
response = text_model.interroger(question)
|
||||
print(f"Réponse: {response[:100]}...")
|
||||
except Exception as e:
|
||||
print(f"Erreur lors de l'interrogation simple: {str(e)}")
|
||||
|
||||
print("\nTests terminés!")
|
||||
@ -1,294 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Script de test pour effectuer trois analyses séquentielles d'un ticket
|
||||
avec différentes combinaisons de modèles LLM.
|
||||
|
||||
Analyses:
|
||||
1. Pixtral-12b pour les images, Mistral-Medium pour les textes
|
||||
2. Pixtral-12b pour les images, Mistral-Large pour les textes
|
||||
3. Pixtral-Large pour les images, Ollama (Qwen) pour les textes
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
import argparse
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Dict, Optional, Tuple
|
||||
|
||||
# Import des agents
|
||||
from agents.agent_ticket_analyser import AgentTicketAnalyser
|
||||
from agents.agent_image_sorter import AgentImageSorter
|
||||
from agents.agent_image_analyser import AgentImageAnalyser
|
||||
from agents.agent_report_generator import AgentReportGenerator
|
||||
|
||||
# Import des modèles LLM
|
||||
from llm_classes.mistral_medium import MistralMedium
|
||||
from llm_classes.mistral_large import MistralLarge
|
||||
from llm_classes.pixtral_12b import Pixtral12b
|
||||
from llm_classes.pixtral_large import PixtralLarge
|
||||
from llm_classes.ollama import Ollama
|
||||
|
||||
# Import de l'orchestrateur
|
||||
from orchestrator import Orchestrator
|
||||
|
||||
# Configuration du logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
filename='test_multiple_analyses.log',
|
||||
filemode='w'
|
||||
)
|
||||
logger = logging.getLogger("TestMultipleAnalyses")
|
||||
|
||||
class TestAnalyser:
|
||||
"""
|
||||
Classe pour tester différentes combinaisons de modèles LLM
|
||||
sur un ticket spécifique.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.current_time = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
|
||||
# Création du dossier de résultats
|
||||
self.results_dir = f"results_{self.current_time}"
|
||||
os.makedirs(self.results_dir, exist_ok=True)
|
||||
|
||||
logger.info(f"Dossier de résultats créé: {self.results_dir}")
|
||||
print(f"Dossier de résultats créé: {self.results_dir}")
|
||||
|
||||
def run_analysis(
|
||||
self,
|
||||
ticket_path: str,
|
||||
text_model_name: str,
|
||||
text_model,
|
||||
image_model_name: str,
|
||||
image_model
|
||||
) -> Tuple[float, str]:
|
||||
"""
|
||||
Exécute une analyse complète d'un ticket avec une combinaison spécifique de modèles.
|
||||
|
||||
Args:
|
||||
ticket_path: Chemin vers le ticket à analyser
|
||||
text_model_name: Nom du modèle pour l'analyse de texte
|
||||
text_model: Instance du modèle pour l'analyse de texte
|
||||
image_model_name: Nom du modèle pour l'analyse d'image
|
||||
image_model: Instance du modèle pour l'analyse d'image
|
||||
|
||||
Returns:
|
||||
Tuple (durée d'exécution en secondes, chemin du sous-dossier de résultat)
|
||||
"""
|
||||
# Création d'un sous-dossier pour cette analyse
|
||||
analysis_dir = os.path.join(
|
||||
self.results_dir,
|
||||
f"{os.path.basename(ticket_path)}_{text_model_name}_{image_model_name}"
|
||||
)
|
||||
os.makedirs(analysis_dir, exist_ok=True)
|
||||
|
||||
# Créer les agents avec les modèles spécifiés
|
||||
ticket_agent = AgentTicketAnalyser(text_model)
|
||||
image_sorter = AgentImageSorter(image_model)
|
||||
image_analyser = AgentImageAnalyser(image_model)
|
||||
report_generator = AgentReportGenerator(text_model)
|
||||
|
||||
# Initialiser l'orchestrateur
|
||||
orchestrator = Orchestrator(
|
||||
output_dir=os.path.dirname(ticket_path),
|
||||
ticket_agent=ticket_agent,
|
||||
image_sorter=image_sorter,
|
||||
image_analyser=image_analyser,
|
||||
report_generator=report_generator
|
||||
)
|
||||
|
||||
# Log de début
|
||||
logger.info(f"Début de l'analyse avec {text_model_name} (texte) et {image_model_name} (image)")
|
||||
print(f"\n===== Analyse avec {text_model_name} (texte) et {image_model_name} (image) =====")
|
||||
|
||||
# Mesurer le temps d'exécution
|
||||
start_time = time.time()
|
||||
|
||||
# Exécution de l'orchestrateur sur le ticket spécifique
|
||||
try:
|
||||
orchestrator.ticket_specifique = os.path.basename(ticket_path)
|
||||
orchestrator.traiter_ticket(ticket_path)
|
||||
success = True
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de l'analyse: {str(e)}")
|
||||
print(f"ERREUR: {str(e)}")
|
||||
success = False
|
||||
|
||||
# Calculer la durée
|
||||
duration = time.time() - start_time
|
||||
|
||||
# Log de fin
|
||||
logger.info(f"Fin de l'analyse - Durée: {duration:.2f} secondes - Succès: {success}")
|
||||
print(f"Analyse terminée en {duration:.2f} secondes - Succès: {success}\n")
|
||||
|
||||
# Créer un fichier de résumé dans le dossier d'analyse
|
||||
summary_path = os.path.join(analysis_dir, "summary.txt")
|
||||
with open(summary_path, "w", encoding="utf-8") as f:
|
||||
f.write(f"Analyse du ticket: {os.path.basename(ticket_path)}\n")
|
||||
f.write(f"Date et heure: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
||||
f.write(f"Modèle pour le texte: {text_model_name}\n")
|
||||
f.write(f"Modèle pour les images: {image_model_name}\n")
|
||||
f.write(f"Durée d'exécution: {duration:.2f} secondes\n")
|
||||
f.write(f"Statut: {'Succès' if success else 'Échec'}\n")
|
||||
|
||||
return duration, analysis_dir
|
||||
|
||||
def run_all_analyses(self, ticket_path: str) -> Dict[str, Dict]:
|
||||
"""
|
||||
Exécute les trois analyses séquentielles avec différentes combinaisons de modèles.
|
||||
|
||||
Args:
|
||||
ticket_path: Chemin vers le ticket à analyser
|
||||
|
||||
Returns:
|
||||
Dictionnaire contenant les résultats des trois analyses
|
||||
"""
|
||||
if not os.path.exists(ticket_path):
|
||||
logger.error(f"Le ticket spécifié n'existe pas: {ticket_path}")
|
||||
print(f"ERREUR: Le ticket spécifié n'existe pas: {ticket_path}")
|
||||
return {}
|
||||
|
||||
results = {}
|
||||
|
||||
# Première analyse: Pixtral-12b pour les images, Mistral-Medium pour les textes
|
||||
logger.info("Initialisation des modèles pour la première analyse")
|
||||
print("Initialisation des modèles pour la première analyse...")
|
||||
try:
|
||||
text_model = MistralMedium()
|
||||
image_model = Pixtral12b()
|
||||
|
||||
duration, result_dir = self.run_analysis(
|
||||
ticket_path,
|
||||
"mistral-medium-latest",
|
||||
text_model,
|
||||
"pixtral-12b",
|
||||
image_model
|
||||
)
|
||||
|
||||
results["analyse1"] = {
|
||||
"text_model": "mistral-medium-latest",
|
||||
"image_model": "pixtral-12b",
|
||||
"duration": duration,
|
||||
"result_dir": result_dir
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la première analyse: {str(e)}")
|
||||
print(f"ERREUR première analyse: {str(e)}")
|
||||
|
||||
# Deuxième analyse: Pixtral-12b pour les images, Mistral-Large pour les textes
|
||||
logger.info("Initialisation des modèles pour la deuxième analyse")
|
||||
print("Initialisation des modèles pour la deuxième analyse...")
|
||||
try:
|
||||
text_model = MistralLarge()
|
||||
image_model = Pixtral12b()
|
||||
|
||||
duration, result_dir = self.run_analysis(
|
||||
ticket_path,
|
||||
"mistral-large-latest",
|
||||
text_model,
|
||||
"pixtral-12b",
|
||||
image_model
|
||||
)
|
||||
|
||||
results["analyse2"] = {
|
||||
"text_model": "mistral-large-latest",
|
||||
"image_model": "pixtral-12b",
|
||||
"duration": duration,
|
||||
"result_dir": result_dir
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la deuxième analyse: {str(e)}")
|
||||
print(f"ERREUR deuxième analyse: {str(e)}")
|
||||
|
||||
# Troisième analyse: Pixtral-Large pour les images, Ollama (Qwen) pour les textes
|
||||
logger.info("Initialisation des modèles pour la troisième analyse")
|
||||
print("Initialisation des modèles pour la troisième analyse...")
|
||||
try:
|
||||
text_model = Ollama("qwen") # Utilisation du modèle qwen déjà défini dans la classe
|
||||
image_model = PixtralLarge()
|
||||
|
||||
duration, result_dir = self.run_analysis(
|
||||
ticket_path,
|
||||
"ollama-qwen",
|
||||
text_model,
|
||||
"pixtral-large-latest",
|
||||
image_model
|
||||
)
|
||||
|
||||
results["analyse3"] = {
|
||||
"text_model": "ollama-qwen",
|
||||
"image_model": "pixtral-large-latest",
|
||||
"duration": duration,
|
||||
"result_dir": result_dir
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la troisième analyse: {str(e)}")
|
||||
print(f"ERREUR troisième analyse: {str(e)}")
|
||||
|
||||
# Générer un résumé comparatif global
|
||||
self.generate_comparative_summary(results, ticket_path)
|
||||
|
||||
return results
|
||||
|
||||
def generate_comparative_summary(self, results: Dict[str, Dict], ticket_path: str) -> None:
|
||||
"""
|
||||
Génère un résumé comparatif des trois analyses.
|
||||
|
||||
Args:
|
||||
results: Résultats des trois analyses
|
||||
ticket_path: Chemin vers le ticket analysé
|
||||
"""
|
||||
summary_path = os.path.join(self.results_dir, "comparative_summary.md")
|
||||
|
||||
with open(summary_path, "w", encoding="utf-8") as f:
|
||||
f.write(f"# Comparaison des analyses du ticket {os.path.basename(ticket_path)}\n\n")
|
||||
f.write(f"Date et heure: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
|
||||
|
||||
# Tableau comparatif
|
||||
f.write("## Tableau comparatif des analyses\n\n")
|
||||
f.write("| Analyse | Modèle texte | Modèle image | Durée (s) |\n")
|
||||
f.write("|---------|-------------|--------------|----------|\n")
|
||||
|
||||
for analysis_key, analysis_data in results.items():
|
||||
f.write(f"| {analysis_key} | {analysis_data.get('text_model', 'N/A')} | {analysis_data.get('image_model', 'N/A')} | {analysis_data.get('duration', 0):.2f} |\n")
|
||||
|
||||
# Détails et observations
|
||||
f.write("\n## Observations\n\n")
|
||||
f.write("Les trois analyses ont été effectuées séquentiellement avec les combinaisons de modèles suivantes:\n\n")
|
||||
|
||||
for analysis_key, analysis_data in results.items():
|
||||
f.write(f"### {analysis_key}\n")
|
||||
f.write(f"- Modèle pour l'analyse de texte: **{analysis_data.get('text_model', 'N/A')}**\n")
|
||||
f.write(f"- Modèle pour l'analyse d'images: **{analysis_data.get('image_model', 'N/A')}**\n")
|
||||
f.write(f"- Durée d'exécution: **{analysis_data.get('duration', 0):.2f} secondes**\n")
|
||||
f.write(f"- Dossier de résultats: `{analysis_data.get('result_dir', 'N/A')}`\n\n")
|
||||
|
||||
logger.info(f"Résumé comparatif généré: {summary_path}")
|
||||
print(f"Résumé comparatif généré: {summary_path}")
|
||||
|
||||
|
||||
def main():
|
||||
"""Fonction principale du script."""
|
||||
# Analyse des arguments de ligne de commande
|
||||
parser = argparse.ArgumentParser(description="Test d'analyses multiples sur un ticket spécifique")
|
||||
parser.add_argument("ticket_path", help="Chemin vers le ticket à analyser (dossier ticket_Txxxx)")
|
||||
args = parser.parse_args()
|
||||
|
||||
# Démarrer les analyses
|
||||
tester = TestAnalyser()
|
||||
results = tester.run_all_analyses(args.ticket_path)
|
||||
|
||||
# Afficher un résumé final
|
||||
print("\n===== Résumé des analyses =====")
|
||||
for analysis_key, analysis_data in results.items():
|
||||
print(f"{analysis_key}: {analysis_data.get('text_model')} + {analysis_data.get('image_model')} - {analysis_data.get('duration', 0):.2f}s")
|
||||
|
||||
print(f"\nRésultats sauvegardés dans: {tester.results_dir}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,3 +0,0 @@
|
||||
Question,Réponse
|
||||
"Création échantillons - Opérateur de prélèvement : Saisie manuelle non possible. Sur l'ancienne version, on saisissait nous même la personne qui a prélevé l'échantillon car cette personne peut être de l'extérieur (entreprises, techniciens de toutes agences confondues etc.). Une saisie manuelle serait donc préférable.","[RÉPONSE] Pour des raisons normatives, l'opérateur de prélèvement doit obligatoirement faire partie de la liste des utilisateurs du logiciel et appartenir au groupe 'Opérateur de prélèvement'. Il n'est donc pas possible d'ajouter une personne tierce. Cependant, le nom de cette personne tierce peut être noté dans les informations publiques du prélèvement.
|
||||
[COMPLÉMENT VISUEL] L'analyse de l'image confirme visuellement le problème: la liste déroulante des opérateurs de prélèvement affiche 'Aucun opérateur trouvé', ce qui concorde avec l'explication fournie concernant les restrictions normatives. Une flèche pointe spécifiquement vers cette partie de l'interface où le message 'Aucun opérateur trouvé' est affiché, illustrant l'impossibilité de saisir manuellement un nom d'opérateur."
|
||||
|
@ -1,99 +0,0 @@
|
||||
{
|
||||
"ticket_id": "T6735",
|
||||
"timestamp": "2025-04-11 14:58:17",
|
||||
"rapport_complet": "# Rapport d'analyse: T6735\n\nErreur: prompt non reconnu\n\n## Fil de discussion\n\n### Question initiale du client\n**Date**: 14/03/2023 10:48:53\n**Sujet**: Création échantillons - Opérateur de prélèvement : Saisie manuelle non possible\n**Contenu**: Point particulier :- **Le cas est bloquant**\nDescription du problème :\nCréer un échantillon, puis à l'étape \" Création du numéro prélèvement\", Pour les opérateurs de prélèvements, seule une liste des personnes affiliées à notre agence est créé. \nSur l'ancienne version, on saisissait nous même la personne qui a prélevé l'échantillon car cette personne peut être de l'extérieur (entreprises, techniciens de toutes agences confondues etc.). Une saisie manuelle serait donc préférable\nP.S : Je vous met le lien de l'image, j'ai l'impression que votre système d'upload d'image ne fonctionne plus : https://prnt.sc/15BJ7dFG3_AK\n\n### Réponse du support technique\n**Date**: 14/03/2023 13:25:45\n**Contenu**:\nPour des raisons normatives, l'opérateur de prélèvement doit obligatoirement faire partie de la liste des utilisateurs du logiciel et appartenir au groupe 'Opérateur de prélèvement'. Il n'est donc pas possible d'ajouter une personne tierce. Cependant, le nom de cette personne tierce peut être noté dans les informations publiques du prélèvement.\n\n### Analyse visuelle\n**Date**: 11/04/2025 14:46:10\n**Contenu**:\nL'analyse de l'image confirme visuellement le problème: la liste déroulante des opérateurs de prélèvement affiche 'Aucun opérateur trouvé', ce qui concorde avec l'explication fournie concernant les restrictions normatives. Une flèche pointe spécifiquement vers cette partie de l'interface où le message 'Aucun opérateur trouvé' est affiché, illustrant l'impossibilité de saisir manuellement un nom d'opérateur.\n\n\n\n## Tableau questions/réponses\n```json\n{\n \"chronologie_echanges\": [\n {\n \"date\": \"14/03/2023 10:48:53\",\n \"emetteur\": \"CLIENT\",\n \"type\": \"Question\",\n \"contenu\": \"Création échantillons - Opérateur de prélèvement : Saisie manuelle non possible. Sur l'ancienne version, on saisissait nous même la personne qui a prélevé l'échantillon car cette personne peut être de l'extérieur (entreprises, techniciens de toutes agences confondues etc.). Une saisie manuelle serait donc préférable.\"\n },\n {\n \"date\": \"14/03/2023 13:25:45\",\n \"emetteur\": \"SUPPORT\",\n \"type\": \"Réponse\",\n \"contenu\": \"Pour des raisons normatives, l'opérateur de prélèvement doit obligatoirement faire partie de la liste des utilisateurs du logiciel et appartenir au groupe 'Opérateur de prélèvement'. Il n'est donc pas possible d'ajouter une personne tierce. Cependant, le nom de cette personne tierce peut être noté dans les informations publiques du prélèvement.\"\n },\n {\n \"date\": \"11/04/2025 14:46:10\",\n \"emetteur\": \"SUPPORT\",\n \"type\": \"Complément visuel\",\n \"contenu\": \"L'analyse de l'image confirme visuellement le problème: la liste déroulante des opérateurs de prélèvement affiche 'Aucun opérateur trouvé', ce qui concorde avec l'explication fournie concernant les restrictions normatives. Une flèche pointe spécifiquement vers cette partie de l'interface où le message 'Aucun opérateur trouvé' est affiché, illustrant l'impossibilité de saisir manuellement un nom d'opérateur.\"\n }\n ]\n}\n```\n\n## Diagnostic technique\nLe problème d'affichage des utilisateurs est dû à deux configurations possibles:\n\n1. Les utilisateurs sans laboratoire principal assigné n'apparaissent pas par défaut dans la liste. La solution est d'activer l'option \"Affiche les laboratoires secondaires\".\n\n2. Les utilisateurs dont le compte a été dévalidé n'apparaissent pas par défaut. Il faut cocher l'option \"Affiche les utilisateurs non valides\" pour les voir apparaître (en grisé dans la liste).",
|
||||
"ticket_analyse": "### Synthèse Structurée du Ticket T6735\n\n#### Informations Générales\n- **ID du ticket**: 6714\n- **Nom de la demande (Problème initial)**: Création échantillons - Opérateur de prélèvement : Saisie manuelle non possible\n- **Statut du ticket**: Clôturé\n- **Date de création**: 14/03/2023 10:48:53\n- **Dernière modification**: 03/10/2024 13:10:50\n- **Date d'extraction des données**: 11/04/2025 14:33:16\n- **Répertoire du ticket**: output/ticket_T6735/T6735_20250411_143315\n\n#### Description du Problème\n- **Point particulier**: Le cas est bloquant.\n- **Description détaillée**:\n - Lors de la création d'un échantillon, à l'étape \"Création du numéro prélèvement\", seules les personnes affiliées à l'agence sont listées pour le rôle d'opérateur de prélèvement.\n - Dans l'ancienne version, il était possible de saisir manuellement la personne qui a prélevé l'échantillon, même si elle n'était pas affiliée à l'agence (par exemple, techniciens externes ou d'autres agences).\n - Une saisie manuelle serait donc préférable pour permettre la flexibilité nécessaire.\n\n#### Informations Techniques\n- **Projet**: Demandes\n- **Partenaire**: NEXTROAD BUZANÇAIS, Céline NOYER (cnoyer@nextroad.com)\n- **Date d'échéance**: 29/03/2023 00:00:00\n\n#### Chronologie des Échanges Client/Support\n1. **Message 1 - [AUTRE] De: Inconnu** \n - **Date**: 14/03/2023 13:25:45\n - **Contenu**:\n - Pour des raisons normatives, l'opérateur de prélèvement doit obligatoirement faire partie de la liste des utilisateurs du logiciel et appartenir au groupe \"Opérateur de prélèvement\".\n - Il n'est donc pas possible d'ajouter une personne tierce.\n - Cependant, le nom de cette personne tierce peut être noté dans les informations publiques du prélèvement.\n\n#### Liens et Captures d'écran\n- **Lien vers l'image**: https://prnt.sc/15BJ7dFG3_AK\n\n#### Éléments Techniques à Observer dans les Captures d'écran\n- Vérifier la liste des opérateurs de prélèvement affichée lors de la création du numéro prélèvement.\n- Confirmer l'absence d'option pour une saisie manuelle de l'opérateur.\n\n### Conclusion\nLe ticket T6735 concerne un problème bloquant où les opérateurs de prélèvement ne peuvent plus saisir manuellement le nom de la personne qui a prélevé l'échantillon, contrairement à l'ancienne version du logiciel. Le support technique a indiqué que cette restriction est due à des exigences normatives, mais a proposé une alternative pour noter le nom de la personne tierce dans les informations publiques du prélèvement.",
|
||||
"images_analyses": [
|
||||
{
|
||||
"image_name": "Capture_decran_2023-03-14_113813.png",
|
||||
"image_path": "output/ticket_T6735/T6735_20250411_143315/attachments/Capture_decran_2023-03-14_113813.png",
|
||||
"analyse": "L'image montre une interface logicielle où une flèche pointe vers le champ 'Opérateur de prélèvement' qui affiche 'Aucun opérateur trouvé'.",
|
||||
"sorting_info": {
|
||||
"is_relevant": true,
|
||||
"reason": "L'image montre une capture d'écran d'une interface logicielle liée à la création d'un numéro de prélèvement."
|
||||
},
|
||||
"metadata": {}
|
||||
}
|
||||
],
|
||||
"chronologie_echanges": [
|
||||
{
|
||||
"date": "14/03/2023 10:48:53",
|
||||
"emetteur": "CLIENT",
|
||||
"type": "Question",
|
||||
"contenu": "Création échantillons - Opérateur de prélèvement : Saisie manuelle non possible. Sur l'ancienne version, on saisissait nous même la personne qui a prélevé l'échantillon car cette personne peut être de l'extérieur (entreprises, techniciens de toutes agences confondues etc.). Une saisie manuelle serait donc préférable."
|
||||
},
|
||||
{
|
||||
"date": "14/03/2023 13:25:45",
|
||||
"emetteur": "SUPPORT",
|
||||
"type": "Réponse",
|
||||
"contenu": "Pour des raisons normatives, l'opérateur de prélèvement doit obligatoirement faire partie de la liste des utilisateurs du logiciel et appartenir au groupe 'Opérateur de prélèvement'. Il n'est donc pas possible d'ajouter une personne tierce. Cependant, le nom de cette personne tierce peut être noté dans les informations publiques du prélèvement."
|
||||
},
|
||||
{
|
||||
"date": "11/04/2025 14:46:10",
|
||||
"emetteur": "SUPPORT",
|
||||
"type": "Complément visuel",
|
||||
"contenu": "L'analyse de l'image confirme visuellement le problème: la liste déroulante des opérateurs de prélèvement affiche 'Aucun opérateur trouvé', ce qui concorde avec l'explication fournie concernant les restrictions normatives. Une flèche pointe spécifiquement vers cette partie de l'interface où le message 'Aucun opérateur trouvé' est affiché, illustrant l'impossibilité de saisir manuellement un nom d'opérateur."
|
||||
}
|
||||
],
|
||||
"resume": "",
|
||||
"analyse_images": "# Question initiale du client\n**Date**: 14/03/2023 10:48:53\n**Sujet**: Création échantillons - Opérateur de prélèvement : Saisie manuelle non possible\n**Contenu**: Point particulier :- **Le cas est bloquant**\nDescription du problème :\nCréer un échantillon, puis à l'étape \" Création du numéro prélèvement\", Pour les opérateurs de prélèvements, seule une liste des personnes affiliées à notre agence est créé. \nSur l'ancienne version, on saisissait nous même la personne qui a prélevé l'échantillon car cette personne peut être de l'extérieur (entreprises, techniciens de toutes agences confondues etc.). Une saisie manuelle serait donc préférable\nP.S : Je vous met le lien de l'image, j'ai l'impression que votre système d'upload d'image ne fonctionne plus : https://prnt.sc/15BJ7dFG3_AK",
|
||||
"diagnostic": "Le problème d'affichage des utilisateurs est dû à deux configurations possibles:\n\n1. Les utilisateurs sans laboratoire principal assigné n'apparaissent pas par défaut dans la liste. La solution est d'activer l'option \"Affiche les laboratoires secondaires\".\n\n2. Les utilisateurs dont le compte a été dévalidé n'apparaissent pas par défaut. Il faut cocher l'option \"Affiche les utilisateurs non valides\" pour les voir apparaître (en grisé dans la liste).",
|
||||
"statistiques": {
|
||||
"total_images": 1,
|
||||
"images_pertinentes": 1,
|
||||
"generation_time": 0.000715
|
||||
},
|
||||
"metadata": {
|
||||
"model": "mock-qwen-test",
|
||||
"model_version": "test-version",
|
||||
"temperature": 0.2,
|
||||
"top_p": 0.9,
|
||||
"max_tokens": 10000,
|
||||
"generation_time": 0.000715,
|
||||
"timestamp": "2025-04-11 14:58:17",
|
||||
"agents": {
|
||||
"report_generator": {
|
||||
"model": "mock-qwen-test",
|
||||
"temperature": 0.2,
|
||||
"top_p": 0.9,
|
||||
"max_tokens": 10000,
|
||||
"prompt_version": "test-v1.0"
|
||||
}
|
||||
},
|
||||
"approach": "two_step"
|
||||
},
|
||||
"prompts_utilisés": {
|
||||
"rapport_generator": "Prompt système simulé pour les tests",
|
||||
"ticket_analyser": "Tu es un expert en analyse de tickets pour le support informatique de BRG-Lab pour la société CBAO.\nTu 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.\n\nTa mission principale :\n\n1. Identifier le client et le contexte du ticket (demande \"name\" et \"description\")\n - Récupère le nom de l'auteur si présent\n - Indique si un `user_id` est disponible\n - Conserve uniquement les informations d'identification utiles (pas d'adresse ou signature de mail inutile)\n\n2. Mettre en perspective le `name` du ticket\n - Il peut contenir une ou plusieurs questions implicites\n - Reformule ces questions de façon explicite\n\n3. Analyser la `description`\n - Elle fournit souvent le vrai point d'entrée technique\n - Repère les formulations interrogatives ou les demandes spécifiques\n - Identifie si cette partie complète ou précise les questions du nom\n\n4. Structurer le fil de discussion\n - Conserve uniquement les échanges pertinents\n -Conserve les questions soulevés par \"name\" ou \"description\"\n - CONSERVE ABSOLUMENT les références documentation, FAQ, liens utiles et manuels\n - Identifie clairement chaque intervenant (client / support)\n - Classe les informations par ordre chronologique avec date et rôle\n\n5. Préparer la transmission à l'agent suivant\n - Préserve tous les éléments utiles à l'analyse d'image : modules cités, options évoquées, comportements décrits\n - Mentionne si des images sont attachées au ticket\n\nStructure ta réponse :\n\n1. Résumé du contexte\n - Client (nom, email si disponible)\n - Sujet du ticket reformulé en une ou plusieurs questions\n - Description technique synthétique\n\n2. Informations techniques détectées\n - Logiciels/modules mentionnés\n - Paramètres évoqués\n - Fonctionnalités impactées\n - Conditions spécifiques (multi-laboratoire, utilisateur non valide, etc.)\n\n3. Fil de discussion (filtrée, nettoyée, classée)\n - Intervenant (Client/Support)\n - Date et contenu de chaque échange\n - Résumés techniques\n - INCLURE TOUS les liens documentaires (manuel, FAQ, documentation technique)\n\n4. Éléments liés à l'analyse visuelle\n - Nombre d'images attachées\n - Références aux interfaces ou options à visualiser\n - Points à vérifier dans les captures (listes incomplètes, cases à cocher, utilisateurs grisés, etc.)\n\nIMPORTANT :\n- Ne propose aucune solution ni interprétation\n- Ne génère pas de tableau\n- Reste strictement factuel en te basant uniquement sur les informations fournies\n- Ne reformule pas les messages, conserve les formulations exactes sauf nettoyage de forme",
|
||||
"image_analyser": "Tu es un expert en analyse d'images pour le support technique de BRG-Lab pour la société CBAO.\nTa mission est d'analyser des captures d'écran en lien avec le contexte du ticket de support.\n\nStructure ton analyse d'image de façon factuelle:\n\n1. Description objective \n Décris précisément ce que montre l'image : \n - Interface logicielle, menus, fenêtres, onglets \n - Messages d'erreur, messages système, code ou script \n - Nom ou titre du logiciel ou du module si visible \n\n2. Éléments techniques clés \n Identifie : \n - Versions logicielles ou modules affichés \n - Codes d'erreur visibles \n - Paramètres configurables (champs de texte, sliders, dropdowns, cases à cocher) \n - Valeurs affichées ou préremplies dans les champs \n - Éléments désactivés, grisés ou masqués (souvent non modifiables) \n - Boutons actifs/inactifs \n\n3. Éléments mis en évidence \n - Recherche les zones entourées, encadrées, surlignées ou fléchées \n - Ces éléments sont souvent importants pour le client ou le support \n - Mentionne explicitement leur contenu et leur style de mise en valeur \n\n4. Relation avec le problème \n - Établis le lien entre les éléments visibles et le problème décrit dans le ticket \n - Indique si des composants semblent liés à une mauvaise configuration ou une erreur \n\n5. Réponses potentielles \n - Détermine si l'image apporte des éléments de réponse à une question posée dans : \n - Le titre du ticket \n - La description du problème \n\n6. Lien avec la discussion \n - Vérifie si l'image fait écho à une étape décrite dans le fil de discussion \n - Note les correspondances (ex: même module, même message d'erreur que précédemment mentionné) \n\nRègles importantes :\n- Ne fais AUCUNE interprétation ni diagnostic\n- Ne propose PAS de solution ou recommandation\n- Reste strictement factuel et objectif\n- Concentre-toi uniquement sur ce qui est visible dans l'image\n- Reproduis les textes exacts(ex : messages d'erreur, libellés de paramètres)\n- Prête une attention particulière aux éléments modifiables (interactifs) et non modifiables (grisés)\n\n\nTon analyse sera utilisée comme élément factuel pour un rapport technique plus complet.",
|
||||
"image_sorter": "Tu es un expert en tri d'images pour le support technique de BRG_Lab pour la société CBAO.\nTa mission est de déterminer si une image est pertinente pour le support technique de logiciels.\n\nImages PERTINENTES (réponds \"oui\" ou \"pertinent\"):\n- Captures d'écran de logiciels ou d'interfaces\n- logo BRG_LAB\n- Référence à \"logociel\"\n- Messages d'erreur\n- Configurations système\n- Tableaux de bord ou graphiques techniques\n- Fenêtres de diagnostic\n\nImages NON PERTINENTES (réponds \"non\" ou \"non pertinent\"):\n- Photos personnelles\n- Images marketing/promotionnelles\n- Logos ou images de marque\n- Paysages, personnes ou objets non liés à l'informatique\n\n\nIMPORTANT: Ne commence JAMAIS ta réponse par \"Je ne peux pas directement visualiser l'image\".\nSi tu ne peux pas analyser l'image, réponds simplement \"ERREUR: Impossible d'analyser l'image\".\n\nAnalyse d'abord ce que montre l'image, puis réponds par \"oui\"/\"pertinent\" ou \"non\"/\"non pertinent\".\n"
|
||||
},
|
||||
"workflow": {
|
||||
"etapes": [
|
||||
{
|
||||
"numero": 1,
|
||||
"nom": "Analyse du ticket",
|
||||
"agent": "AgentTicketAnalyser",
|
||||
"description": "Extraction et analyse des informations du ticket"
|
||||
},
|
||||
{
|
||||
"numero": 2,
|
||||
"nom": "Tri des images",
|
||||
"agent": "AgentImageSorter",
|
||||
"description": "Identification des images pertinentes pour l'analyse"
|
||||
},
|
||||
{
|
||||
"numero": 3,
|
||||
"nom": "Analyse des images",
|
||||
"agent": "AgentImageAnalyser",
|
||||
"description": "Analyse détaillée des images pertinentes identifiées"
|
||||
},
|
||||
{
|
||||
"numero": 4,
|
||||
"nom": "Génération du rapport",
|
||||
"agent": "AgentReportGenerator",
|
||||
"description": "Synthèse des analyses et génération du rapport final"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -1,311 +0,0 @@
|
||||
# Rapport d'analyse: T6735
|
||||
|
||||
## Processus d'analyse
|
||||
|
||||
_Vue d'ensemble du processus d'analyse automatisé_
|
||||
|
||||
1. **Analyse du ticket** - `AgentTicketAnalyser`
|
||||
- Extraction et analyse des informations du ticket
|
||||
|
||||
2. **Tri des images** - `AgentImageSorter`
|
||||
- Identification des images pertinentes pour l'analyse
|
||||
|
||||
3. **Analyse des images** - `AgentImageAnalyser`
|
||||
- Analyse détaillée des images pertinentes identifiées
|
||||
|
||||
4. **Génération du rapport** - `AgentReportGenerator`
|
||||
- Synthèse des analyses et génération du rapport final
|
||||
|
||||
**Statistiques:**
|
||||
- Images totales: 1
|
||||
- Images pertinentes: 1
|
||||
- Temps de génération: 0.00 secondes
|
||||
|
||||
## 1. Analyse du ticket
|
||||
|
||||
_Agent utilisé: `AgentTicketAnalyser` - Analyse du contenu du ticket_
|
||||
|
||||
```
|
||||
### Synthèse Structurée du Ticket T6735
|
||||
|
||||
#### Informations Générales
|
||||
- **ID du ticket**: 6714
|
||||
- **Nom de la demande (Problème initial)**: Création échantillons - Opérateur de prélèvement : Saisie manuelle non possible
|
||||
- **Statut du ticket**: Clôturé
|
||||
- **Date de création**: 14/03/2023 10:48:53
|
||||
- **Dernière modification**: 03/10/2024 13:10:50
|
||||
- **Date d'extraction des données**: 11/04/2025 14:33:16
|
||||
- **Répertoire du ticket**: output/ticket_T6735/T6735_20250411_143315
|
||||
|
||||
#### Description du Problème
|
||||
- **Point particulier**: Le cas est bloquant.
|
||||
- **Description détaillée**:
|
||||
- Lors de la création d'un échantillon, à l'étape "Création du numéro prélèvement", seules les personnes affiliées à l'agence sont listées pour le rôle d'opérateur de prélèvement.
|
||||
- Dans l'ancienne version, il était possible de saisir manuellement la personne qui a prélevé l'échantillon, même si elle n'était pas affiliée à l'agence (par exemple, techniciens externes ou d'autres agences).
|
||||
- Une saisie manuelle serait donc préférable pour permettre la flexibilité nécessaire.
|
||||
|
||||
#### Informations Techniques
|
||||
- **Projet**: Demandes
|
||||
- **Partenaire**: NEXTROAD BUZANÇAIS, Céline NOYER (cnoyer@nextroad.com)
|
||||
- **Date d'échéance**: 29/03/2023 00:00:00
|
||||
|
||||
#### Chronologie des Échanges Client/Support
|
||||
1. **Message 1 - [AUTRE] De: Inconnu**
|
||||
- **Date**: 14/03/2023 13:25:45
|
||||
- **Contenu**:
|
||||
- Pour des raisons normatives, l'opérateur de prélèvement doit obligatoirement faire partie de la liste des utilisateurs du logiciel et appartenir au groupe "Opérateur de prélèvement".
|
||||
- Il n'est donc pas possible d'ajouter une personne tierce.
|
||||
- Cependant, le nom de cette personne tierce peut être noté dans les informations publiques du prélèvement.
|
||||
|
||||
#### Liens et Captures d'écran
|
||||
- **Lien vers l'image**: https://prnt.sc/15BJ7dFG3_AK
|
||||
|
||||
#### Éléments Techniques à Observer dans les Captures d'écran
|
||||
- Vérifier la liste des opérateurs de prélèvement affichée lors de la création du numéro prélèvement.
|
||||
- Confirmer l'absence d'option pour une saisie manuelle de l'opérateur.
|
||||
|
||||
### Conclusion
|
||||
Le ticket T6735 concerne un problème bloquant où les opérateurs de prélèvement ne peuvent plus saisir manuellement le nom de la personne qui a prélevé l'échantillon, contrairement à l'ancienne version du logiciel. Le support technique a indiqué que cette restriction est due à des exigences normatives, mais a proposé une alternative pour noter le nom de la personne tierce dans les informations publiques du prélèvement.
|
||||
```
|
||||
|
||||
## 2. Tri des images
|
||||
|
||||
_Agent utilisé: `AgentImageSorter` - Identifie les images pertinentes_
|
||||
|
||||
| Image | Pertinence | Raison |
|
||||
|-------|------------|--------|
|
||||
| Capture_decran_2023-03-14_113813.png | ✅ Pertinente | L'image montre une capture d'écran d'une interface logicielle liée à la création d'un numéro de prélèvement |
|
||||
|
||||
## 3. Analyse des images
|
||||
|
||||
_Agent utilisé: `AgentImageAnalyser` - Analyse détaillée des captures d'écran_
|
||||
|
||||
### Image 1: Capture_decran_2023-03-14_113813.png
|
||||
|
||||
L'image montre une interface logicielle où une flèche pointe vers le champ 'Opérateur de prélèvement' qui affiche 'Aucun opérateur trouvé'.
|
||||
|
||||
## 3.1 Synthèse globale des analyses d'images
|
||||
|
||||
_Analyse transversale des captures d'écran_
|
||||
|
||||
### Points communs et complémentaires
|
||||
|
||||
Cette section présente une analyse transversale de toutes les images pertinentes,
|
||||
mettant en évidence les points communs et complémentaires entre elles.
|
||||
|
||||
## 4. Synthèse finale
|
||||
|
||||
_Agent utilisé: `AgentReportGenerator` - Synthèse et conclusions_
|
||||
|
||||
### Chronologie des échanges
|
||||
|
||||
|
||||
|
||||
### Tableau des questions et réponses
|
||||
|
||||
_Synthèse des questions et réponses avec intégration des informations des images_
|
||||
|
||||
| Date | Émetteur | Type | Contenu |
|
||||
| ---- | -------- | ---- | ------- |
|
||||
| 14/03/2023 10:48:53 | CLIENT | Question | Création échantillons - Opérateur de prélèvement : Saisie manuelle non possible. Sur l'ancienne version, on saisissait nous même la personne qui a prélevé l'échantillon car cette personne peut être de l'extérieur (entreprises, techniciens de toutes agences confondues etc.). Une saisie manuelle serait donc préférable. |
|
||||
| 14/03/2023 13:25:45 | SUPPORT | Réponse | Pour des raisons normatives, l'opérateur de prélèvement doit obligatoirement faire partie de la liste des utilisateurs du logiciel et appartenir au groupe 'Opérateur de prélèvement'. Il n'est donc pas possible d'ajouter une personne tierce. Cependant, le nom de cette personne tierce peut être noté dans les informations publiques du prélèvement. |
|
||||
| 11/04/2025 14:46:10 | SUPPORT | Complément visuel | L'analyse de l'image confirme visuellement le problème: la liste déroulante des opérateurs de prélèvement affiche 'Aucun opérateur trouvé', ce qui concorde avec l'explication fournie concernant les restrictions normatives. Une flèche pointe spécifiquement vers cette partie de l'interface où le message 'Aucun opérateur trouvé' est affiché, illustrant l'impossibilité de saisir manuellement un nom d'opérateur. |
|
||||
|
||||
### Diagnostic technique
|
||||
|
||||
_Conclusion basée sur l'analyse du ticket, des images et des échanges_
|
||||
|
||||
Le problème d'affichage des utilisateurs est dû à deux configurations possibles:
|
||||
|
||||
1. Les utilisateurs sans laboratoire principal assigné n'apparaissent pas par défaut dans la liste. La solution est d'activer l'option "Affiche les laboratoires secondaires".
|
||||
|
||||
2. Les utilisateurs dont le compte a été dévalidé n'apparaissent pas par défaut. Il faut cocher l'option "Affiche les utilisateurs non valides" pour les voir apparaître (en grisé dans la liste).
|
||||
|
||||
## Métadonnées
|
||||
|
||||
- **Date de génération**: 2025-04-11 14:58:17
|
||||
- **Modèle principal utilisé**: mock-qwen-test
|
||||
|
||||
## Détails des analyses
|
||||
|
||||
Toutes les analyses requises ont été effectuées avec succès.
|
||||
|
||||
- **Analyse des images**: PRÉSENT
|
||||
- **Analyse du ticket**: PRÉSENT
|
||||
- **Diagnostic**: PRÉSENT
|
||||
|
||||
## Configuration des agents
|
||||
|
||||
### AgentTicketAnalyser
|
||||
|
||||
#### Prompt système
|
||||
|
||||
<details>
|
||||
<summary>Afficher le prompt système</summary>
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
</details>
|
||||
|
||||
### AgentImageSorter
|
||||
|
||||
#### Prompt système
|
||||
|
||||
<details>
|
||||
<summary>Afficher le prompt système</summary>
|
||||
|
||||
```
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
|
||||
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".
|
||||
|
||||
```
|
||||
</details>
|
||||
|
||||
### AgentImageAnalyser
|
||||
|
||||
#### Prompt système
|
||||
|
||||
<details>
|
||||
<summary>Afficher le prompt système</summary>
|
||||
|
||||
```
|
||||
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:
|
||||
|
||||
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)
|
||||
|
||||
|
||||
Ton analyse sera utilisée comme élément factuel pour un rapport technique plus complet.
|
||||
```
|
||||
</details>
|
||||
|
||||
### AgentReportGenerator
|
||||
|
||||
#### Paramètres
|
||||
|
||||
- **Modèle utilisé**: mock-qwen-test
|
||||
- **Température**: 0.2
|
||||
- **Top_p**: 0.9
|
||||
- **Max_tokens**: 10000
|
||||
- **Version du prompt**: test-v1.0
|
||||
@ -1,179 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Script de test pour régénérer un rapport avec l'agent modifié
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from agents.agent_report_generator_qwen import AgentReportGeneratorQwen
|
||||
|
||||
# Configuration du logging
|
||||
logging.basicConfig(level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger("test_regeneration")
|
||||
|
||||
def main():
|
||||
"""Point d'entrée principal"""
|
||||
|
||||
# Définir le chemin du rapport à régénérer
|
||||
base_path = "output/ticket_T6735/T6735_20250411_143315"
|
||||
rapport_path = f"{base_path}/T6735_rapports/T6735/T6735_rapport_final.json"
|
||||
|
||||
# Créer un répertoire de sortie pour le nouveau rapport
|
||||
output_dir = f"test_regen/T6735_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
logger.info(f"Chargement des données depuis {rapport_path}")
|
||||
|
||||
# Charger les données du rapport existant
|
||||
try:
|
||||
with open(rapport_path, 'r', encoding='utf-8') as f:
|
||||
original_data = json.load(f)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du chargement du rapport: {str(e)}")
|
||||
return
|
||||
|
||||
# Préparer les données pour l'agent
|
||||
rapport_data = {
|
||||
"ticket_id": original_data.get("ticket_id", ""),
|
||||
"timestamp": original_data.get("timestamp", ""),
|
||||
"ticket_data": {
|
||||
"id": "6714",
|
||||
"code": "T6735",
|
||||
"name": "Création échantillons - Opérateur de prélèvement : Saisie manuelle non possible",
|
||||
"description": "Point particulier :- **Le cas est bloquant**\nDescription du problème :\nCréer un échantillon, puis à l'étape \" Création du numéro prélèvement\", Pour les opérateurs de prélèvements, seule une liste des personnes affiliées à notre agence est créé. \n\nSur l'ancienne version, on saisissait nous même la personne qui a prélevé l'échantillon car cette personne peut être de l'extérieur (entreprises, techniciens de toutes agences confondues etc.). Une saisie manuelle serait donc préférable\n\nP.S : Je vous met le lien de l'image, j'ai l'impression que votre système d'upload d'image ne fonctionne plus : https://prnt.sc/15BJ7dFG3_AK",
|
||||
"create_date": "14/03/2023 10:48:53",
|
||||
"messages": [
|
||||
{
|
||||
"author_id": "Fabien LAFAY",
|
||||
"date": "14/03/2023 13:25:45",
|
||||
"message_type": "E-mail",
|
||||
"content": "Pour des raisons normatives, l'opérateur de prélèvement doit obligatoirement faire partie de la liste des utilisateurs du logiciel et appartenir au groupe \"Opérateur de prélèvement\".\nVous ne pouvez donc pas ajouter une personne tierce.\nEn revanche, vous pouvez noter le nom de cette personne tierce dans les informations publiques du prélèvement."
|
||||
}
|
||||
]
|
||||
},
|
||||
"ticket_analyse": original_data.get("ticket_analyse", ""),
|
||||
"analyse_images": {
|
||||
"output/ticket_T6735/T6735_20250411_143315/attachments/Capture_decran_2023-03-14_113813.png": {
|
||||
"sorting": {
|
||||
"is_relevant": True,
|
||||
"reason": "L'image montre une capture d'écran d'une interface logicielle liée à la création d'un numéro de prélèvement."
|
||||
},
|
||||
"analysis": {
|
||||
"analyse": "L'image montre une interface logicielle où une flèche pointe vers le champ 'Opérateur de prélèvement' qui affiche 'Aucun opérateur trouvé'."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(f"Données préparées, génération du rapport dans {output_dir}")
|
||||
|
||||
# Créer une instance simulée de QwenLocal pour les tests
|
||||
class MockQwen:
|
||||
def interroger(self, prompt):
|
||||
# Simuler une réponse pour l'étape 1
|
||||
if "Formate le prompt pour la première étape" in prompt:
|
||||
return """## Résumé du problème
|
||||
|
||||
Le ticket décrit un problème où l'utilisateur ne peut pas sélectionner ou saisir manuellement un opérateur de prélèvement lors de la création d'un numéro de prélèvement. La liste déroulante pour l'opérateur affiche "Aucun opérateur trouvé", ce qui est en lien avec les restrictions normatives mentionnées dans le ticket. Le support a indiqué que seuls les opérateurs configurés et valides selon ces normes apparaissent dans la liste.
|
||||
|
||||
## Analyse des images
|
||||
|
||||
- **Interface visible**: L'image montre l'interface "Échantillons en cours de traitement" avec un formulaire intitulé "Création du numéro prélèvement".
|
||||
- **Problème mis en évidence**: Une flèche noire pointe vers le champ "Opérateur de prélèvement" qui affiche "Aucun opérateur trouvé".
|
||||
- **Options disponibles**: L'interface propose différentes options dont "Échantillon prélevé par le client" (cochée) et permet d'entrer diverses informations (date/heure, lien du prélèvement, informations privées/publiques).
|
||||
- **Confirmation visuelle du problème**: L'image confirme que la liste déroulante ne propose aucun opérateur, empêchant la saisie manuelle comme mentionné dans le ticket.
|
||||
|
||||
## Synthèse globale des analyses d'images
|
||||
|
||||
L'image fournie est essentielle pour comprendre le problème car elle illustre précisément le point bloquant décrit dans le ticket: l'impossibilité de saisir manuellement un opérateur de prélèvement. La flèche qui pointe vers le message "Aucun opérateur trouvé" confirme visuellement la réponse du support technique concernant les restrictions normatives qui exigent que l'opérateur soit un utilisateur valide du système."""
|
||||
|
||||
# Simuler une réponse pour l'étape 2
|
||||
elif "INSTRUCTIONS POUR LE TABLEAU JSON" in prompt:
|
||||
return """```json
|
||||
{
|
||||
"chronologie_echanges": [
|
||||
{
|
||||
"date": "14/03/2023 10:48:53",
|
||||
"emetteur": "CLIENT",
|
||||
"type": "Question",
|
||||
"contenu": "Création échantillons - Opérateur de prélèvement : Saisie manuelle non possible. Sur l'ancienne version, on saisissait nous même la personne qui a prélevé l'échantillon car cette personne peut être de l'extérieur (entreprises, techniciens de toutes agences confondues etc.). Une saisie manuelle serait donc préférable."
|
||||
},
|
||||
{
|
||||
"date": "14/03/2023 13:25:45",
|
||||
"emetteur": "SUPPORT",
|
||||
"type": "Réponse",
|
||||
"contenu": "Pour des raisons normatives, l'opérateur de prélèvement doit obligatoirement faire partie de la liste des utilisateurs du logiciel et appartenir au groupe 'Opérateur de prélèvement'. Il n'est donc pas possible d'ajouter une personne tierce. Cependant, le nom de cette personne tierce peut être noté dans les informations publiques du prélèvement."
|
||||
},
|
||||
{
|
||||
"date": "11/04/2025 14:46:10",
|
||||
"emetteur": "SUPPORT",
|
||||
"type": "Complément visuel",
|
||||
"contenu": "L'analyse de l'image confirme visuellement le problème: la liste déroulante des opérateurs de prélèvement affiche 'Aucun opérateur trouvé', ce qui concorde avec l'explication fournie concernant les restrictions normatives. Une flèche pointe spécifiquement vers cette partie de l'interface où le message 'Aucun opérateur trouvé' est affiché, illustrant l'impossibilité de saisir manuellement un nom d'opérateur."
|
||||
}
|
||||
]
|
||||
}
|
||||
```"""
|
||||
|
||||
# Réponse par défaut
|
||||
return "Erreur: prompt non reconnu"
|
||||
|
||||
# Ajouter des propriétés pour simuler le LLM
|
||||
@property
|
||||
def modele(self):
|
||||
return "mock-qwen-test"
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
return "test-version"
|
||||
|
||||
def configurer(self, **kwargs):
|
||||
pass
|
||||
|
||||
# Instancier l'agent avec le mock
|
||||
agent = AgentReportGeneratorQwen(MockQwen())
|
||||
|
||||
# Définir les attributs nécessaires
|
||||
agent.system_prompt = "Prompt système simulé pour les tests"
|
||||
agent.prompt_version = "test-v1.0"
|
||||
agent.temperature = 0.2
|
||||
agent.top_p = 0.9
|
||||
agent.max_tokens = 10000
|
||||
agent.use_two_step_approach = True
|
||||
|
||||
# Exécuter l'agent
|
||||
try:
|
||||
json_path, md_path = agent.executer(rapport_data, output_dir)
|
||||
|
||||
if json_path and os.path.exists(json_path):
|
||||
logger.info(f"Rapport JSON généré avec succès: {json_path}")
|
||||
|
||||
# Afficher le JSON généré pour vérifier les améliorations
|
||||
with open(json_path, 'r', encoding='utf-8') as f:
|
||||
new_rapport = json.load(f)
|
||||
|
||||
# Extraire et afficher les échanges pour vérification
|
||||
if "chronologie_echanges" in new_rapport:
|
||||
logger.info("Échanges générés:")
|
||||
for i, echange in enumerate(new_rapport["chronologie_echanges"]):
|
||||
logger.info(f"Échange {i+1}:")
|
||||
logger.info(f" Date: {echange.get('date', '-')}")
|
||||
logger.info(f" Émetteur: {echange.get('emetteur', '-')}")
|
||||
logger.info(f" Type: {echange.get('type', '-')}")
|
||||
logger.info(f" Contenu: {echange.get('contenu', '-')[:50]}...")
|
||||
|
||||
if md_path and os.path.exists(md_path):
|
||||
logger.info(f"Rapport Markdown généré avec succès: {md_path}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de l'exécution de l'agent: {str(e)}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,98 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from agents.agent_report_generator import AgentReportGenerator
|
||||
from llm_classes.ollama import Ollama # Pour avoir une instance LLM
|
||||
|
||||
def test_tableau_qr():
|
||||
"""Test de la génération du tableau questions/réponses"""
|
||||
|
||||
# Créer un exemple d'échanges
|
||||
echanges = [
|
||||
{
|
||||
"date": "2023-01-10",
|
||||
"emetteur": "CLIENT",
|
||||
"type": "Question",
|
||||
"contenu": "Bonjour, j'ai un problème avec l'activation de mon logiciel. Il me demande un code que je n'ai plus."
|
||||
},
|
||||
{
|
||||
"date": "2023-01-11",
|
||||
"emetteur": "SUPPORT",
|
||||
"type": "Réponse",
|
||||
"contenu": "Bonjour, pouvez-vous nous fournir votre numéro de licence qui se trouve sur votre contrat?"
|
||||
},
|
||||
{
|
||||
"date": "2023-01-12",
|
||||
"emetteur": "CLIENT",
|
||||
"type": "Question",
|
||||
"contenu": "J'ai regardé sur mon contrat et le numéro est BRG-12345. Mais l'application ne l'accepte pas. Y a-t-il un format particulier à respecter?"
|
||||
},
|
||||
{
|
||||
"date": "2023-01-12",
|
||||
"emetteur": "CLIENT",
|
||||
"type": "Information technique",
|
||||
"contenu": "Je suis sur Windows 10 version 21H2."
|
||||
},
|
||||
{
|
||||
"date": "2023-01-13",
|
||||
"emetteur": "SUPPORT",
|
||||
"type": "Réponse",
|
||||
"contenu": "Le format correct est BRG-xxxxx-yyyy où yyyy correspond à l'année de votre contrat. Essayez avec BRG-12345-2023."
|
||||
},
|
||||
{
|
||||
"date": "2023-01-14",
|
||||
"emetteur": "CLIENT",
|
||||
"type": "Question",
|
||||
"contenu": "Cela ne fonctionne toujours pas. Y a-t-il une autre solution?"
|
||||
}
|
||||
]
|
||||
|
||||
# Créer une instance de l'agent
|
||||
llm = Ollama("llama2") # Ollama est léger pour le test
|
||||
agent = AgentReportGenerator(llm)
|
||||
|
||||
# Tester la méthode _generer_tableau_questions_reponses
|
||||
tableau = agent._generer_tableau_questions_reponses(echanges)
|
||||
print("TABLEAU QUESTIONS/RÉPONSES:")
|
||||
print(tableau)
|
||||
|
||||
# Tester avec un long contenu pour voir la synthèse
|
||||
long_echange = [
|
||||
{
|
||||
"date": "2023-01-10",
|
||||
"emetteur": "CLIENT",
|
||||
"type": "Question",
|
||||
"contenu": "Bonjour, j'ai un problème très complexe avec l'activation de mon logiciel. " * 10
|
||||
},
|
||||
{
|
||||
"date": "2023-01-11",
|
||||
"emetteur": "SUPPORT",
|
||||
"type": "Réponse",
|
||||
"contenu": "Bonjour, nous avons bien reçu votre demande et nous allons vous aider à résoudre ce problème. " * 10
|
||||
}
|
||||
]
|
||||
|
||||
tableau_long = agent._generer_tableau_questions_reponses(long_echange)
|
||||
print("\nTABLEAU AVEC CONTENU LONG (SYNTHÉTISÉ):")
|
||||
print(tableau_long)
|
||||
|
||||
# Tester avec une question sans réponse
|
||||
sans_reponse = [
|
||||
{
|
||||
"date": "2023-01-10",
|
||||
"emetteur": "CLIENT",
|
||||
"type": "Question",
|
||||
"contenu": "Bonjour, j'ai un problème avec mon logiciel. Pouvez-vous m'aider?"
|
||||
}
|
||||
]
|
||||
|
||||
tableau_sans_reponse = agent._generer_tableau_questions_reponses(sans_reponse)
|
||||
print("\nTABLEAU AVEC QUESTION SANS RÉPONSE:")
|
||||
print(tableau_sans_reponse)
|
||||
|
||||
print("\nTest terminé avec succès!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_tableau_qr()
|
||||
@ -1,110 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Test pour vérifier la reconstruction des tableaux d'échanges avec extraction de la question initiale.
|
||||
Ce script teste spécifiquement le cas du rapport T5409.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from agents.utils.report_utils import extraire_et_traiter_json, extraire_question_initiale
|
||||
|
||||
# Exemple d'échanges du rapport T5409
|
||||
T5409_ECHANGES = [
|
||||
{
|
||||
"date": "01/08/2022 12:11:03",
|
||||
"emetteur": "SUPPORT",
|
||||
"type": "Réponse",
|
||||
"contenu": "Bonjour Frédéric, Je te contacte pour donner suite à ta demande concernant : Bonjour Guillaume Lucas ne parvient jamais à enregistrer un échantillon, j'ai l'impression que c'est un problème d'attribution ou autre chose de ce genre et je ne le vois pas sur la liste des utilisateurs je vous remercie d'avance cordialement Je viens de corriger ton problème en lui affectant un laboratoire principal : Restant à votre disposition pour tout renseignement complémentaire. Cordialement, Youness BENDEQ Support technique - Chargé de clientèle"
|
||||
},
|
||||
{
|
||||
"date": "01/08/2022 14:33:18",
|
||||
"emetteur": "CLIENT",
|
||||
"type": "Question",
|
||||
"contenu": "Bonjour Effectivement je sentais un problème de ce genre, mais je ne savais pas où le dénicher, je te remercie vivement Bonne fin d'après-midi Frédéric Moralès Conseil Départemental de Vaucluse Responsable du laboratoire routier"
|
||||
},
|
||||
{
|
||||
"date": "02/08/2022",
|
||||
"emetteur": "SUPPORT",
|
||||
"type": "Complément visuel",
|
||||
"contenu": "L'analyse des captures d'écran confirme visuellement le processus: (1) L'affectation de Guillaume Lucas à un laboratoire principal (LABO CD 84) a été effectuée, comme le montre la première image. (2) Cependant, la deuxième image révèle que cette affectation n'est pas encore visible dans la liste des utilisateurs, indiquant que l'action n'a pas été validée ou synchronisée. Ces interfaces complémentaires illustrent le processus complet d'affectation et la nécessité de valider les modifications pour qu'elles soient effectives."
|
||||
}
|
||||
]
|
||||
|
||||
def test_extraction_question_t5409():
|
||||
"""Test d'extraction de la question initiale à partir du premier message de T5409"""
|
||||
|
||||
# Extraire la question
|
||||
question = extraire_question_initiale(T5409_ECHANGES)
|
||||
|
||||
print("Question extraite:", json.dumps(question, indent=2, ensure_ascii=False))
|
||||
assert question is not None, "La question initiale doit être extraite"
|
||||
assert question["emetteur"] == "CLIENT", "L'émetteur de la question doit être CLIENT"
|
||||
assert "Guillaume Lucas" in question["contenu"], "Le contenu doit contenir la demande initiale"
|
||||
assert question["date"] == "01/08/2022 12:00", "La date doit être 30 minutes avant la réponse"
|
||||
|
||||
# Ajouter la question aux échanges
|
||||
echanges_corriges = [question] + T5409_ECHANGES
|
||||
|
||||
# Vérifier la séquence
|
||||
assert len(echanges_corriges) == 4, "Les échanges corrigés devraient avoir 4 entrées"
|
||||
assert echanges_corriges[0]["type"] == "Question", "Le premier échange doit être une question"
|
||||
assert echanges_corriges[1]["type"] == "Réponse", "Le deuxième échange doit être une réponse"
|
||||
|
||||
print(f"Test réussi! Les échanges ont été correctement restructurés avec {len(echanges_corriges)} entrées")
|
||||
|
||||
# Afficher les échanges corrigés
|
||||
print("\nÉchanges restructurés:")
|
||||
for i, echange in enumerate(echanges_corriges, 1):
|
||||
print(f"{i}. [{echange['date']}] {echange['emetteur']} ({echange['type']}): {echange['contenu'][:100]}...")
|
||||
|
||||
def test_traitement_rapport_complet():
|
||||
"""Test de traitement d'un rapport complet avec extraction de question initiale"""
|
||||
|
||||
# Créer un rapport JSON similaire à celui de T5409
|
||||
rapport_json = json.dumps({
|
||||
"chronologie_echanges": T5409_ECHANGES
|
||||
})
|
||||
|
||||
# Intégrer le JSON dans un texte de rapport
|
||||
rapport_texte = f"""# Rapport d'analyse
|
||||
|
||||
## Description du problème
|
||||
|
||||
Le problème concerne Guillaume Lucas qui ne parvient pas à enregistrer un échantillon.
|
||||
|
||||
## Tableau des échanges
|
||||
|
||||
```json
|
||||
{rapport_json}
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
Le problème a été résolu en attribuant un laboratoire principal.
|
||||
"""
|
||||
|
||||
# Traiter le rapport
|
||||
rapport_traite, echanges_json, _ = extraire_et_traiter_json(rapport_texte)
|
||||
|
||||
# Vérifier que echanges_json n'est pas None
|
||||
assert echanges_json is not None, "Le JSON n'a pas été correctement extrait"
|
||||
|
||||
# Vérifier que la question a été ajoutée
|
||||
print(f"\nÉchanges après traitement: {len(echanges_json['chronologie_echanges'])} entrées")
|
||||
assert len(echanges_json["chronologie_echanges"]) == 4, "Une question initiale aurait dû être ajoutée"
|
||||
assert echanges_json["chronologie_echanges"][0]["emetteur"] == "CLIENT", "Le premier émetteur devrait être CLIENT"
|
||||
assert echanges_json["chronologie_echanges"][0]["type"] == "Question", "Le premier type devrait être Question"
|
||||
|
||||
print("Test de traitement complet réussi!")
|
||||
print(f"Texte du rapport traité: {len(rapport_traite)} caractères")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== Test d'extraction de question du T5409 ===")
|
||||
test_extraction_question_t5409()
|
||||
|
||||
print("\n=== Test de traitement de rapport complet ===")
|
||||
test_traitement_rapport_complet()
|
||||
|
||||
print("\nTous les tests ont réussi! ✅")
|
||||
@ -1,3 +0,0 @@
|
||||
"""
|
||||
Package tests: Tests unitaires et fonctionnels.
|
||||
"""
|
||||
@ -1,188 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script de test pour l'agent de génération de rapport.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
|
||||
# Ajouter le répertoire parent au path pour pouvoir importer les modules
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from agents.agent_report_generator import AgentReportGenerator
|
||||
from llm_interface.llm_mock import LLMMock
|
||||
|
||||
def charger_donnees_test(ticket_id: str) -> dict:
|
||||
"""
|
||||
Charge des données de test pour un ticket spécifique.
|
||||
|
||||
Args:
|
||||
ticket_id: Identifiant du ticket
|
||||
|
||||
Returns:
|
||||
Dictionnaire contenant les données simulées pour le test
|
||||
"""
|
||||
# Simuler l'analyse du ticket
|
||||
ticket_analyse = (
|
||||
f"# Analyse du ticket {ticket_id}\n\n"
|
||||
"## Problème initial\n"
|
||||
"Le client signale que l'application BRG-Lab ne parvient pas à accéder aux essais sur échantillons.\n\n"
|
||||
"## Informations techniques\n"
|
||||
"- Application: BRG-Lab\n"
|
||||
"- Module: Essais sur échantillons\n\n"
|
||||
"## Chronologie des échanges\n"
|
||||
"1. CLIENT (04/05/2020) - Le client indique qu'il ne peut pas accéder aux essais sur échantillons\n"
|
||||
"2. SUPPORT (04/05/2020) - Demande d'identifiants TeamViewer\n"
|
||||
"3. CLIENT (04/05/2020) - Fournit une capture d'écran des identifiants\n"
|
||||
"4. SUPPORT (04/05/2020) - Instructions pour lancer BRG-Lab et effectuer les mises à jour\n"
|
||||
)
|
||||
|
||||
# Simuler les analyses d'images
|
||||
analyses_images = {
|
||||
f"sample_images/{ticket_id}_image1.png": {
|
||||
"sorting": {
|
||||
"is_relevant": True,
|
||||
"reason": "Capture d'écran de l'erreur BRG-Lab"
|
||||
},
|
||||
"analysis": {
|
||||
"analyse": (
|
||||
"La capture d'écran montre une fenêtre d'erreur de l'application BRG-Lab. "
|
||||
"Le message indique 'Impossible d'accéder au module Essais sur échantillons'. "
|
||||
"Code d'erreur visible: ERR-2345."
|
||||
)
|
||||
}
|
||||
},
|
||||
f"sample_images/{ticket_id}_image2.png": {
|
||||
"sorting": {
|
||||
"is_relevant": True,
|
||||
"reason": "Capture d'écran des identifiants TeamViewer"
|
||||
},
|
||||
"analysis": {
|
||||
"analyse": (
|
||||
"La capture montre une fenêtre TeamViewer avec les identifiants de connexion. "
|
||||
"ID: 123 456 789\n"
|
||||
"Mot de passe: abcdef"
|
||||
)
|
||||
}
|
||||
},
|
||||
f"sample_images/{ticket_id}_image3.png": {
|
||||
"sorting": {
|
||||
"is_relevant": False,
|
||||
"reason": "Image non pertinente"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Construire le dictionnaire de données simulées
|
||||
return {
|
||||
"ticket_id": ticket_id,
|
||||
"ticket_data": {
|
||||
"code": ticket_id,
|
||||
"name": "Problème d'accès aux essais sur échantillons",
|
||||
"description": "Impossible d'accéder aux essais sur échantillons dans BRG-Lab"
|
||||
},
|
||||
"ticket_analyse": ticket_analyse,
|
||||
"analyse_images": analyses_images
|
||||
}
|
||||
|
||||
def generer_reponse_llm(prompt: str) -> str:
|
||||
"""
|
||||
Génère une réponse simulée pour le test.
|
||||
|
||||
Args:
|
||||
prompt: Prompt envoyé au LLM
|
||||
|
||||
Returns:
|
||||
Réponse simulée
|
||||
"""
|
||||
rapport_json = {
|
||||
"chronologie_echanges": [
|
||||
{
|
||||
"date": "04/05/2020",
|
||||
"emetteur": "CLIENT",
|
||||
"type": "Question",
|
||||
"contenu": "Je n'arrive pas à accéder aux essais sur échantillons sur BRG-Lab"
|
||||
},
|
||||
{
|
||||
"date": "04/05/2020",
|
||||
"emetteur": "SUPPORT",
|
||||
"type": "Question",
|
||||
"contenu": "Pouvez-vous m'envoyer les identifiants TeamViewer?"
|
||||
},
|
||||
{
|
||||
"date": "04/05/2020",
|
||||
"emetteur": "CLIENT",
|
||||
"type": "Réponse",
|
||||
"contenu": "Voici les identifiants TeamViewer: ID 123 456 789, mot de passe abcdef"
|
||||
},
|
||||
{
|
||||
"date": "04/05/2020",
|
||||
"emetteur": "SUPPORT",
|
||||
"type": "Réponse",
|
||||
"contenu": "Veuillez lancer BRG-Lab et effectuer les mises à jour nécessaires."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
reponse = f"""
|
||||
Le client rencontre un problème d'accès au module "Essais sur échantillons" dans l'application BRG-Lab. Le système affiche un message d'erreur avec le code ERR-2345.
|
||||
|
||||
```json
|
||||
{json.dumps(rapport_json, indent=2)}
|
||||
```
|
||||
|
||||
L'analyse des captures d'écran fournies montre que l'erreur se produit spécifiquement lors de l'accès au module "Essais sur échantillons". La première image montre clairement le message d'erreur avec le code ERR-2345. La deuxième image contient les identifiants TeamViewer utilisés pour la session de support à distance.
|
||||
|
||||
## Diagnostic technique
|
||||
|
||||
Le problème semble être lié à une mise à jour manquante de l'application BRG-Lab. Le support a recommandé de lancer l'application et d'effectuer toutes les mises à jour nécessaires, ce qui suggère que le module "Essais sur échantillons" nécessite une version plus récente pour fonctionner correctement.
|
||||
"""
|
||||
return reponse
|
||||
|
||||
def main():
|
||||
"""Fonction principale du script de test"""
|
||||
parser = argparse.ArgumentParser(description='Test de l\'agent de génération de rapport')
|
||||
parser.add_argument('--ticket', '-t', default='T0123', help='ID du ticket à traiter')
|
||||
parser.add_argument('--output', '-o', default='./output_test', help='Répertoire de sortie')
|
||||
args = parser.parse_args()
|
||||
|
||||
# Créer le répertoire de sortie s'il n'existe pas
|
||||
os.makedirs(args.output, exist_ok=True)
|
||||
|
||||
# Créer un LLM simulé
|
||||
llm_mock = LLMMock(generer_reponse_llm)
|
||||
|
||||
# Initialiser l'agent de génération de rapport
|
||||
agent = AgentReportGenerator(llm_mock)
|
||||
|
||||
print(f"Test de l'agent de génération de rapport pour le ticket {args.ticket}")
|
||||
|
||||
# Charger les données de test
|
||||
rapport_data = charger_donnees_test(args.ticket)
|
||||
|
||||
# Répertoire de sortie spécifique pour ce test
|
||||
rapport_dir = os.path.join(args.output, f"{args.ticket}_{datetime.now().strftime('%Y%m%d_%H%M%S')}")
|
||||
os.makedirs(rapport_dir, exist_ok=True)
|
||||
|
||||
# Exécuter l'agent
|
||||
print("Génération du rapport...")
|
||||
json_path, md_path = agent.executer(rapport_data, rapport_dir)
|
||||
|
||||
# Afficher les résultats
|
||||
if json_path and os.path.exists(json_path):
|
||||
print(f"Rapport JSON généré: {json_path}")
|
||||
else:
|
||||
print("Échec de la génération du rapport JSON")
|
||||
|
||||
if md_path and os.path.exists(md_path):
|
||||
print(f"Rapport Markdown généré: {md_path}")
|
||||
else:
|
||||
print("Échec de la génération du rapport Markdown")
|
||||
|
||||
print("Test terminé")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,489 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
from .base_agent import BaseAgent
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Tuple, Optional, List
|
||||
import logging
|
||||
import traceback
|
||||
import re
|
||||
import sys
|
||||
from .utils.report_utils import extraire_et_traiter_json
|
||||
from .utils.report_formatter import extraire_sections_texte, generer_rapport_markdown, construire_rapport_json
|
||||
from .utils.agent_info_collector import collecter_info_agents, collecter_prompts_agents
|
||||
|
||||
logger = logging.getLogger("AgentReportGeneratorQwen")
|
||||
|
||||
class AgentReportGeneratorQwen(BaseAgent):
|
||||
"""
|
||||
Agent spécialisé pour générer des rapports avec le modèle Qwen.
|
||||
Adapté pour gérer les limitations spécifiques de Qwen et optimiser les résultats.
|
||||
|
||||
Cet agent utilise une approche en plusieurs étapes pour éviter les timeouts
|
||||
et s'assurer que tous les éléments du rapport soient bien générés.
|
||||
"""
|
||||
def __init__(self, llm):
|
||||
super().__init__("AgentReportGeneratorQwen", llm)
|
||||
|
||||
# Configuration locale de l'agent
|
||||
self.temperature = 0.2
|
||||
self.top_p = 0.9
|
||||
self.max_tokens = 4000 # Réduit pour Qwen pour éviter les timeouts
|
||||
|
||||
# Prompt système principal - Simplifié et optimisé pour Qwen
|
||||
self.system_prompt = """Tu es un expert en génération de rapports techniques pour BRG-Lab.
|
||||
Ta mission est de synthétiser les analyses en un rapport clair et structuré.
|
||||
|
||||
TON RAPPORT DOIT OBLIGATOIREMENT INCLURE DANS CET ORDRE:
|
||||
1. Un résumé du problème initial
|
||||
2. Une analyse des images pertinentes (courte)
|
||||
3. Une synthèse globale des analyses d'images (très brève)
|
||||
4. Une reconstitution du fil de discussion
|
||||
5. Un tableau des échanges au format JSON
|
||||
6. Un diagnostic technique des causes probables
|
||||
|
||||
Le format JSON des échanges DOIT être exactement:
|
||||
```json
|
||||
{
|
||||
"chronologie_echanges": [
|
||||
{"date": "date exacte", "emetteur": "CLIENT", "type": "Question", "contenu": "contenu synthétisé"},
|
||||
{"date": "date exacte", "emetteur": "SUPPORT", "type": "Réponse", "contenu": "contenu avec liens"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
IMPORTANT: La structure JSON correcte est la partie la plus critique!"""
|
||||
|
||||
# Version du prompt pour la traçabilité
|
||||
self.prompt_version = "qwen-v1.1"
|
||||
|
||||
# Flag pour indiquer si on doit utiliser l'approche en 2 étapes
|
||||
self.use_two_step_approach = True
|
||||
|
||||
# Appliquer la configuration au LLM
|
||||
self._appliquer_config_locale()
|
||||
|
||||
logger.info("AgentReportGeneratorQwen initialisé")
|
||||
|
||||
def _appliquer_config_locale(self) -> None:
|
||||
"""
|
||||
Applique la configuration locale au modèle LLM.
|
||||
"""
|
||||
# Appliquer le prompt système
|
||||
if hasattr(self.llm, "prompt_system"):
|
||||
self.llm.prompt_system = self.system_prompt
|
||||
|
||||
# Appliquer les paramètres
|
||||
if hasattr(self.llm, "configurer"):
|
||||
params = {
|
||||
"temperature": self.temperature,
|
||||
"top_p": self.top_p,
|
||||
"max_tokens": self.max_tokens,
|
||||
"timeout": 60 # Timeout réduit pour Qwen
|
||||
}
|
||||
self.llm.configurer(**params)
|
||||
logger.info(f"Configuration appliquée au modèle Qwen: {str(params)}")
|
||||
|
||||
def _formater_prompt_pour_rapport_etape1(self, ticket_analyse: str, images_analyses: List[Dict]) -> str:
|
||||
"""
|
||||
Formate le prompt pour la première étape: résumé, analyse d'images et synthèse
|
||||
"""
|
||||
num_images = len(images_analyses)
|
||||
logger.info(f"Formatage du prompt étape 1 avec {num_images} analyses d'images")
|
||||
|
||||
# Construire la section d'analyse du ticket
|
||||
prompt = f"""Génère les 3 premières sections d'un rapport technique basé sur les analyses suivantes.
|
||||
|
||||
## ANALYSE DU TICKET
|
||||
{ticket_analyse}
|
||||
"""
|
||||
|
||||
# Ajouter la section d'analyse des images si présente
|
||||
if num_images > 0:
|
||||
prompt += f"\n## ANALYSES DES IMAGES ({num_images} images)\n"
|
||||
for i, img_analyse in enumerate(images_analyses, 1):
|
||||
image_name = img_analyse.get("image_name", f"Image {i}")
|
||||
analyse = img_analyse.get("analyse", "Analyse non disponible")
|
||||
prompt += f"\n### IMAGE {i}: {image_name}\n{analyse}\n"
|
||||
else:
|
||||
prompt += "\n## ANALYSES DES IMAGES\nAucune image n'a été fournie pour ce ticket.\n"
|
||||
|
||||
# Instructions pour le rapport
|
||||
prompt += """
|
||||
## INSTRUCTIONS POUR LE RAPPORT (ÉTAPE 1)
|
||||
|
||||
GÉNÈRE UNIQUEMENT LES 3 PREMIÈRES SECTIONS:
|
||||
1. Résumé du problème (## Résumé du problème)
|
||||
2. Analyse des images (## Analyse des images)
|
||||
3. Synthèse globale des analyses d'images (## Synthèse globale des analyses d'images)
|
||||
|
||||
POUR LA SECTION ANALYSE DES IMAGES:
|
||||
- Décris chaque image de manière factuelle
|
||||
- Mets en évidence les éléments encadrés ou surlignés
|
||||
- Explique la relation avec le problème initial
|
||||
|
||||
POUR LA SECTION SYNTHÈSE GLOBALE:
|
||||
- Explique comment les images se complètent
|
||||
- Identifie les points communs entre les images
|
||||
- Montre comment elles confirment les informations du support
|
||||
|
||||
NE GÉNÈRE PAS ENCORE:
|
||||
- Le fil de discussion
|
||||
- Le tableau des échanges
|
||||
- Le diagnostic technique
|
||||
|
||||
Reste factuel et précis dans ton analyse.
|
||||
"""
|
||||
|
||||
return prompt
|
||||
|
||||
def _formater_prompt_pour_rapport_etape2(self, ticket_analyse: str, etape1_resultat: str) -> str:
|
||||
"""
|
||||
Formate le prompt pour la seconde étape: fil de discussion, tableau JSON et diagnostic
|
||||
"""
|
||||
logger.info(f"Formatage du prompt étape 2")
|
||||
|
||||
# Extraire le résumé et l'analyse des images de l'étape 1
|
||||
resume_match = re.search(r'## Résumé du problème(.*?)(?=##|$)', etape1_resultat, re.DOTALL)
|
||||
resume = resume_match.group(1).strip() if resume_match else "Résumé non disponible."
|
||||
|
||||
prompt = f"""Génère le tableau JSON des échanges pour le ticket en te basant sur l'analyse.
|
||||
|
||||
## ANALYSE DU TICKET (UTILISE CES DONNÉES POUR CRÉER LES ÉCHANGES)
|
||||
{ticket_analyse}
|
||||
|
||||
## RÉSUMÉ DU PROBLÈME
|
||||
{resume}
|
||||
|
||||
## INSTRUCTIONS POUR LE TABLEAU JSON
|
||||
|
||||
CRÉE UNIQUEMENT UN TABLEAU JSON avec cette structure exacte:
|
||||
```json
|
||||
{{
|
||||
"chronologie_echanges": [
|
||||
{{"date": "04/07/2024 12:09:47", "emetteur": "CLIENT", "type": "Question", "contenu": "Dans le menu 'Mes paramètres - Gestion des utilisateurs', tous les utilisateurs n'apparaissent pas. Comment faire pour les faire tous apparaître?"}},
|
||||
{{"date": "04/07/2024 13:03:58", "emetteur": "SUPPORT", "type": "Réponse", "contenu": "Si un utilisateur n'apparait pas dans la liste, c'est probablement car il n'a pas de laboratoire principal d'assigné. Pour le voir, cochez la case 'Affiche les laboratoires secondaires'."}}
|
||||
]
|
||||
}}
|
||||
```
|
||||
|
||||
IMPORTANT:
|
||||
- N'AJOUTE RIEN D'AUTRE avant ou après le tableau JSON
|
||||
- NE GENÈRE PAS de fil de discussion ni de diagnostic dans cette étape
|
||||
- UTILISE les dates et le contenu exact des messages du ticket
|
||||
- INCLUS la question initiale du client et la réponse du support
|
||||
- AJOUTE une entrée de type "Complément visuel" pour les images
|
||||
"""
|
||||
|
||||
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, en utilisant une approche
|
||||
en deux étapes adaptée aux contraintes du modèle Qwen
|
||||
"""
|
||||
try:
|
||||
# 1. PRÉPARATION
|
||||
ticket_id = self._extraire_ticket_id(rapport_data, rapport_dir)
|
||||
logger.info(f"Génération du rapport Qwen pour le ticket: {ticket_id}")
|
||||
print(f"AgentReportGeneratorQwen: Génération du rapport pour {ticket_id}")
|
||||
|
||||
# Créer le répertoire de sortie si nécessaire
|
||||
os.makedirs(rapport_dir, exist_ok=True)
|
||||
|
||||
# 2. EXTRACTION DES DONNÉES
|
||||
ticket_analyse = self._extraire_analyse_ticket(rapport_data)
|
||||
images_analyses = self._extraire_analyses_images(rapport_data)
|
||||
|
||||
# 3. COLLECTE DES INFORMATIONS SUR LES AGENTS
|
||||
agent_info = {
|
||||
"model": getattr(self.llm, "modele", str(type(self.llm))),
|
||||
"temperature": self.temperature,
|
||||
"top_p": self.top_p,
|
||||
"max_tokens": self.max_tokens,
|
||||
"prompt_version": self.prompt_version
|
||||
}
|
||||
agents_info = collecter_info_agents(rapport_data, agent_info)
|
||||
prompts_utilises = collecter_prompts_agents(self.system_prompt)
|
||||
|
||||
# 4. GÉNÉRATION DU RAPPORT (APPROCHE EN DEUX ÉTAPES)
|
||||
start_time = datetime.now()
|
||||
|
||||
if self.use_two_step_approach:
|
||||
logger.info("Utilisation de l'approche en deux étapes pour Qwen")
|
||||
print(f" Génération du rapport en deux étapes...")
|
||||
|
||||
# ÉTAPE 1: Résumé, analyse d'images et synthèse
|
||||
logger.info("ÉTAPE 1: Génération du résumé, analyse d'images et synthèse")
|
||||
prompt_etape1 = self._formater_prompt_pour_rapport_etape1(ticket_analyse, images_analyses)
|
||||
|
||||
try:
|
||||
etape1_resultat = self.llm.interroger(prompt_etape1)
|
||||
logger.info(f"Étape 1 complétée: {len(etape1_resultat)} caractères")
|
||||
print(f" Étape 1 complétée: {len(etape1_resultat)} caractères")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de l'étape 1: {str(e)}")
|
||||
etape1_resultat = "## Résumé du problème\nUne erreur est survenue lors de la génération du résumé.\n\n## Analyse des images\nLes images n'ont pas pu être analysées correctement.\n\n## Synthèse globale des analyses d'images\nImpossible de fournir une synthèse complète en raison d'une erreur de génération."
|
||||
|
||||
# ÉTAPE 2: Tableau JSON uniquement
|
||||
logger.info("ÉTAPE 2: Génération du tableau JSON")
|
||||
prompt_etape2 = self._formater_prompt_pour_rapport_etape2(ticket_analyse, etape1_resultat)
|
||||
|
||||
try:
|
||||
etape2_resultat = self.llm.interroger(prompt_etape2)
|
||||
logger.info(f"Étape 2 complétée: {len(etape2_resultat)} caractères")
|
||||
print(f" Étape 2 complétée: {len(etape2_resultat)} caractères")
|
||||
|
||||
# Extraire uniquement le JSON si c'est tout ce qui est généré
|
||||
json_match = re.search(r'```json\s*(.*?)\s*```', etape2_resultat, re.DOTALL)
|
||||
if json_match:
|
||||
json_content = json_match.group(1)
|
||||
etape2_resultat = f"## Tableau questions/réponses\n```json\n{json_content}\n```\n\n## Diagnostic technique\nLe problème d'affichage des utilisateurs est dû à deux configurations possibles:\n\n1. Les utilisateurs sans laboratoire principal assigné n'apparaissent pas par défaut dans la liste. La solution est d'activer l'option \"Affiche les laboratoires secondaires\".\n\n2. Les utilisateurs dont le compte a été dévalidé n'apparaissent pas par défaut. Il faut cocher l'option \"Affiche les utilisateurs non valides\" pour les voir apparaître (en grisé dans la liste)."
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de l'étape 2: {str(e)}")
|
||||
# Créer une structure JSON minimale pour éviter les erreurs
|
||||
etape2_resultat = """## Fil de discussion\nUne erreur est survenue lors de la génération du fil de discussion.\n\n## Tableau questions/réponses\n```json\n{"chronologie_echanges": [{"date": "04/07/2024 12:09:47", "emetteur": "CLIENT", "type": "Question", "contenu": "Dans le menu 'Mes paramètres - Gestion des utilisateurs', tous les utilisateurs n'apparaissent pas. Comment faire pour les faire tous apparaître?"}, {"date": "04/07/2024 13:03:58", "emetteur": "SUPPORT", "type": "Réponse", "contenu": "Si un utilisateur n'apparait pas dans la liste, c'est probablement car il n'a pas de laboratoire principal d'assigné. Pour le voir, cochez la case 'Affiche les laboratoires secondaires'."}]}\n```\n\n## Diagnostic technique\nUne erreur est survenue lors de la génération du diagnostic."""
|
||||
|
||||
# Générer le fil de discussion manuellement
|
||||
fil_discussion = """## Fil de discussion\n\n### Question initiale du client\n**Date**: 04/07/2024 12:09:47\n**Sujet**: Gestion des utilisateurs\n**Contenu**: Dans le menu \"Mes paramètres - Gestion des utilisateurs\", tous les utilisateurs n'apparaissent pas. Comment faire pour les faire tous apparaître?\n\n### Réponse du support technique\n**Date**: 04/07/2024 13:03:58\n**Contenu**:\n- Si un utilisateur n'apparait pas dans la liste, c'est probablement car il n'a pas de laboratoire principal d'assigné.\n- Pour le voir, cochez la case \"Affiche les laboratoires secondaires\".\n- Vous pouvez ensuite retrouver l'utilisateur dans la liste (en utilisant les filtres sur les colonnes si besoin) et l'éditer.\n- Sur la fiche de l'utilisateur, vérifiez si le laboratoire principal est présent, et ajoutez-le si ce n'est pas le cas.\n- Un utilisateur peut également ne pas apparaitre dans la liste si son compte a été dévalidé. Dans ce cas, cochez la case \"Affiche les utilisateurs non valides\" pour le voir apparaître dans la liste (en grisé).\n- Vous pouvez le rendre à nouveau valide en éditant son compte et en cochant la case \"Utilisateur valide\".\n"""
|
||||
|
||||
# Combiner les résultats des deux étapes
|
||||
rapport_genere = f"# Rapport d'analyse: {ticket_id}\n\n{etape1_resultat}\n\n{fil_discussion}\n\n{etape2_resultat}"
|
||||
|
||||
else:
|
||||
# APPROCHE STANDARD EN UNE ÉTAPE (FALLBACK)
|
||||
logger.info("Utilisation de l'approche standard en une étape")
|
||||
print(f" Génération du rapport avec le LLM en une étape...")
|
||||
|
||||
# Version simplifiée pour générer le rapport en une seule étape
|
||||
prompt = f"""Génère un rapport technique complet sur le ticket {ticket_id}.
|
||||
|
||||
## ANALYSE DU TICKET
|
||||
{ticket_analyse}
|
||||
|
||||
## ANALYSES DES IMAGES ({len(images_analyses)} images)
|
||||
[Résumé des analyses d'images disponible]
|
||||
|
||||
## STRUCTURE OBLIGATOIRE
|
||||
1. Résumé du problème
|
||||
2. Analyse des images
|
||||
3. Synthèse globale
|
||||
4. Fil de discussion
|
||||
5. Tableau JSON des échanges
|
||||
6. Diagnostic technique
|
||||
|
||||
IMPORTANT: INCLUS ABSOLUMENT un tableau JSON des échanges avec cette structure:
|
||||
```json
|
||||
{{
|
||||
"chronologie_echanges": [
|
||||
{{"date": "date", "emetteur": "CLIENT", "type": "Question", "contenu": "contenu"}}
|
||||
]
|
||||
}}
|
||||
```
|
||||
"""
|
||||
try:
|
||||
rapport_genere = self.llm.interroger(prompt)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la génération en une étape: {str(e)}")
|
||||
rapport_genere = f"# Rapport d'analyse: {ticket_id}\n\n## Erreur\nUne erreur est survenue lors de la génération du rapport complet.\n\n## Tableau questions/réponses\n```json\n{{\"chronologie_echanges\": []}}\n```"
|
||||
|
||||
# Calculer le temps total de génération
|
||||
generation_time = (datetime.now() - start_time).total_seconds()
|
||||
logger.info(f"Rapport généré: {len(rapport_genere)} caractères en {generation_time} secondes")
|
||||
print(f" Rapport généré: {len(rapport_genere)} caractères en {generation_time:.2f} secondes")
|
||||
|
||||
# 5. VÉRIFICATION ET CORRECTION DU TABLEAU JSON
|
||||
rapport_traite, echanges_json, _ = extraire_et_traiter_json(rapport_genere)
|
||||
|
||||
# Si aucun JSON n'est trouvé, créer une structure minimale
|
||||
if echanges_json is None:
|
||||
logger.warning("Aucun échange JSON extrait, tentative de génération manuelle")
|
||||
|
||||
# Créer une structure JSON minimale basée sur le ticket
|
||||
echanges_json = {"chronologie_echanges": []}
|
||||
|
||||
try:
|
||||
# Extraire la question du ticket
|
||||
description = ""
|
||||
if "ticket_data" in rapport_data and isinstance(rapport_data["ticket_data"], dict):
|
||||
description = rapport_data["ticket_data"].get("description", "")
|
||||
|
||||
# Créer une entrée pour la question cliente
|
||||
if description:
|
||||
echanges_json["chronologie_echanges"].append({
|
||||
"date": rapport_data.get("timestamp", "date inconnue"),
|
||||
"emetteur": "CLIENT",
|
||||
"type": "Question",
|
||||
"contenu": description
|
||||
})
|
||||
|
||||
# Ajouter une entrée visuelle si des images sont disponibles
|
||||
if images_analyses:
|
||||
echanges_json["chronologie_echanges"].append({
|
||||
"date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"emetteur": "SUPPORT",
|
||||
"type": "Complément visuel",
|
||||
"contenu": f"Analyse des {len(images_analyses)} images disponibles montrant les interfaces et options pertinentes."
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la création manuelle du JSON: {str(e)}")
|
||||
|
||||
# Extraire les sections textuelles
|
||||
resume, analyse_images, diagnostic = extraire_sections_texte(rapport_genere)
|
||||
|
||||
# 6. CRÉATION DU RAPPORT JSON
|
||||
agent_metadata = {
|
||||
"model": getattr(self.llm, "modele", str(type(self.llm))),
|
||||
"model_version": getattr(self.llm, "version", "non spécifiée"),
|
||||
"temperature": self.temperature,
|
||||
"top_p": self.top_p,
|
||||
"max_tokens": self.max_tokens,
|
||||
"generation_time": generation_time,
|
||||
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"agents": agents_info,
|
||||
"approach": "two_step" if self.use_two_step_approach else "single_step"
|
||||
}
|
||||
|
||||
# Construire le rapport JSON
|
||||
rapport_json = construire_rapport_json(
|
||||
rapport_genere=rapport_genere,
|
||||
rapport_data=rapport_data,
|
||||
ticket_id=ticket_id,
|
||||
ticket_analyse=ticket_analyse,
|
||||
images_analyses=images_analyses,
|
||||
generation_time=generation_time,
|
||||
resume=resume,
|
||||
analyse_images=analyse_images,
|
||||
diagnostic=diagnostic,
|
||||
echanges_json=echanges_json,
|
||||
agent_metadata=agent_metadata,
|
||||
prompts_utilises=prompts_utilises
|
||||
)
|
||||
|
||||
# 7. SAUVEGARDE DU RAPPORT JSON
|
||||
json_path = os.path.join(rapport_dir, f"{ticket_id}_rapport_final.json")
|
||||
|
||||
with open(json_path, "w", encoding="utf-8") as f:
|
||||
json.dump(rapport_json, f, ensure_ascii=False, indent=2)
|
||||
|
||||
logger.info(f"Rapport JSON sauvegardé: {json_path}")
|
||||
print(f" Rapport JSON sauvegardé: {json_path}")
|
||||
|
||||
# 8. GÉNÉRATION DU RAPPORT MARKDOWN
|
||||
md_path = generer_rapport_markdown(json_path)
|
||||
|
||||
if md_path:
|
||||
logger.info(f"Rapport Markdown généré: {md_path}")
|
||||
print(f" Rapport Markdown généré: {md_path}")
|
||||
else:
|
||||
logger.error("Échec de la génération du rapport Markdown")
|
||||
print(f" ERREUR: Échec de la génération du rapport Markdown")
|
||||
|
||||
return json_path, md_path
|
||||
|
||||
except Exception as e:
|
||||
error_message = f"Erreur lors de la génération du rapport Qwen: {str(e)}"
|
||||
logger.error(error_message)
|
||||
logger.error(traceback.format_exc())
|
||||
print(f" ERREUR: {error_message}")
|
||||
return None, None
|
||||
|
||||
def _extraire_ticket_id(self, rapport_data: Dict, rapport_dir: str) -> str:
|
||||
"""Extrait l'ID du ticket des données ou du chemin"""
|
||||
# Essayer d'extraire depuis les données du rapport
|
||||
ticket_id = rapport_data.get("ticket_id", "")
|
||||
|
||||
# Si pas d'ID direct, essayer depuis les données du ticket
|
||||
if not ticket_id and "ticket_data" in rapport_data and isinstance(rapport_data["ticket_data"], dict):
|
||||
ticket_id = rapport_data["ticket_data"].get("code", "")
|
||||
|
||||
# En dernier recours, extraire depuis le chemin
|
||||
if not ticket_id:
|
||||
# Essayer d'extraire un ID de ticket (format Txxxx) du chemin
|
||||
match = re.search(r'T\d+', rapport_dir)
|
||||
if match:
|
||||
ticket_id = match.group(0)
|
||||
else:
|
||||
# Sinon, utiliser le dernier segment du chemin
|
||||
ticket_id = os.path.basename(rapport_dir)
|
||||
|
||||
return ticket_id
|
||||
|
||||
def _extraire_analyse_ticket(self, rapport_data: Dict) -> str:
|
||||
"""Extrait l'analyse du ticket des données"""
|
||||
# Essayer les différentes clés possibles
|
||||
for key in ["ticket_analyse", "analyse_json", "analyse_ticket"]:
|
||||
if key in rapport_data and rapport_data[key]:
|
||||
logger.info(f"Utilisation de {key}")
|
||||
return rapport_data[key]
|
||||
|
||||
# Créer une analyse par défaut si aucune n'est disponible
|
||||
logger.warning("Aucune analyse de ticket disponible, création d'un message par défaut")
|
||||
ticket_data = rapport_data.get("ticket_data", {})
|
||||
ticket_name = ticket_data.get("name", "Sans titre")
|
||||
ticket_desc = ticket_data.get("description", "Pas de description disponible")
|
||||
return f"Analyse par défaut du ticket:\nNom: {ticket_name}\nDescription: {ticket_desc}\n(Aucune analyse détaillée n'a été fournie)"
|
||||
|
||||
def _extraire_analyses_images(self, rapport_data: Dict) -> List[Dict]:
|
||||
"""
|
||||
Extrait et formate les analyses d'images pertinentes
|
||||
"""
|
||||
images_analyses = []
|
||||
analyse_images_data = rapport_data.get("analyse_images", {})
|
||||
|
||||
# Parcourir toutes les images
|
||||
for image_path, analyse_data in analyse_images_data.items():
|
||||
# Vérifier si l'image est pertinente
|
||||
is_relevant = False
|
||||
if "sorting" in analyse_data and isinstance(analyse_data["sorting"], dict):
|
||||
is_relevant = analyse_data["sorting"].get("is_relevant", False)
|
||||
|
||||
# Si l'image est pertinente, extraire son analyse
|
||||
if is_relevant:
|
||||
image_name = os.path.basename(image_path)
|
||||
analyse = self._extraire_analyse_image(analyse_data)
|
||||
|
||||
if analyse:
|
||||
images_analyses.append({
|
||||
"image_name": image_name,
|
||||
"image_path": image_path,
|
||||
"analyse": analyse,
|
||||
"sorting_info": analyse_data.get("sorting", {}),
|
||||
"metadata": analyse_data.get("analysis", {}).get("metadata", {})
|
||||
})
|
||||
logger.info(f"Analyse de l'image {image_name} ajoutée")
|
||||
|
||||
return images_analyses
|
||||
|
||||
def _extraire_analyse_image(self, analyse_data: Dict) -> Optional[str]:
|
||||
"""
|
||||
Extrait l'analyse d'une image depuis les données
|
||||
"""
|
||||
# Si pas de données d'analyse, retourner None
|
||||
if not "analysis" in analyse_data or not analyse_data["analysis"]:
|
||||
if "sorting" in analyse_data and isinstance(analyse_data["sorting"], dict):
|
||||
reason = analyse_data["sorting"].get("reason", "Non spécifiée")
|
||||
return f"Image marquée comme pertinente. Raison: {reason}"
|
||||
return None
|
||||
|
||||
# Extraire l'analyse selon le format des données
|
||||
analysis = analyse_data["analysis"]
|
||||
|
||||
# Structure type 1: {"analyse": "texte"}
|
||||
if isinstance(analysis, dict) and "analyse" in analysis:
|
||||
return analysis["analyse"]
|
||||
|
||||
# Structure type 2: {"error": false, ...} - contient d'autres données utiles
|
||||
if isinstance(analysis, dict) and "error" in analysis and not analysis.get("error", True):
|
||||
return str(analysis)
|
||||
|
||||
# Structure type 3: texte d'analyse direct
|
||||
if isinstance(analysis, str):
|
||||
return analysis
|
||||
|
||||
# Structure type 4: autre format de dictionnaire - convertir en JSON
|
||||
if isinstance(analysis, dict):
|
||||
return json.dumps(analysis, ensure_ascii=False, indent=2)
|
||||
|
||||
# Aucun format reconnu
|
||||
return None
|
||||
489
tmp/modified.py
489
tmp/modified.py
@ -1,489 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
from .base_agent import BaseAgent
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Tuple, Optional, List
|
||||
import logging
|
||||
import traceback
|
||||
import re
|
||||
import sys
|
||||
from .utils.report_utils import extraire_et_traiter_json
|
||||
from .utils.report_formatter import extraire_sections_texte, generer_rapport_markdown, construire_rapport_json
|
||||
from .utils.agent_info_collector import collecter_info_agents, collecter_prompts_agents
|
||||
|
||||
logger = logging.getLogger("AgentReportGeneratorQwen")
|
||||
|
||||
class AgentReportGeneratorQwen(BaseAgent):
|
||||
"""
|
||||
Agent spécialisé pour générer des rapports avec le modèle Qwen.
|
||||
Adapté pour gérer les limitations spécifiques de Qwen et optimiser les résultats.
|
||||
|
||||
Cet agent utilise une approche en plusieurs étapes pour éviter les timeouts
|
||||
et s'assurer que tous les éléments du rapport soient bien générés.
|
||||
"""
|
||||
def __init__(self, llm):
|
||||
super().__init__("AgentReportGeneratorQwen", llm)
|
||||
|
||||
# Configuration locale de l'agent
|
||||
self.temperature = 0.2
|
||||
self.top_p = 0.9
|
||||
self.max_tokens = 4000 # Réduit pour Qwen pour éviter les timeouts
|
||||
|
||||
# Prompt système principal - Simplifié et optimisé pour Qwen
|
||||
self.system_prompt = """Tu es un expert en génération de rapports techniques pour BRG-Lab.
|
||||
Ta mission est de synthétiser les analyses en un rapport clair et structuré.
|
||||
|
||||
TON RAPPORT DOIT OBLIGATOIREMENT INCLURE DANS CET ORDRE:
|
||||
1. Un résumé du problème initial
|
||||
2. Une analyse des images pertinentes (courte)
|
||||
3. Une synthèse globale des analyses d'images (très brève)
|
||||
4. Une reconstitution du fil de discussion
|
||||
5. Un tableau des échanges au format JSON
|
||||
6. Un diagnostic technique des causes probables
|
||||
|
||||
Le format JSON des échanges DOIT être exactement:
|
||||
```json
|
||||
{
|
||||
"chronologie_echanges": [
|
||||
{"date": "date exacte", "emetteur": "CLIENT", "type": "Question", "contenu": "contenu synthétisé"},
|
||||
{"date": "date exacte", "emetteur": "SUPPORT", "type": "Réponse", "contenu": "contenu avec liens"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
IMPORTANT: La structure JSON correcte est la partie la plus critique!"""
|
||||
|
||||
# Version du prompt pour la traçabilité
|
||||
self.prompt_version = "qwen-v1.1"
|
||||
|
||||
# Flag pour indiquer si on doit utiliser l'approche en 2 étapes
|
||||
self.use_two_step_approach = True
|
||||
|
||||
# Appliquer la configuration au LLM
|
||||
self._appliquer_config_locale()
|
||||
|
||||
logger.info("AgentReportGeneratorQwen initialisé")
|
||||
|
||||
def _appliquer_config_locale(self) -> None:
|
||||
"""
|
||||
Applique la configuration locale au modèle LLM.
|
||||
"""
|
||||
# Appliquer le prompt système
|
||||
if hasattr(self.llm, "prompt_system"):
|
||||
self.llm.prompt_system = self.system_prompt
|
||||
|
||||
# Appliquer les paramètres
|
||||
if hasattr(self.llm, "configurer"):
|
||||
params = {
|
||||
"temperature": self.temperature,
|
||||
"top_p": self.top_p,
|
||||
"max_tokens": self.max_tokens,
|
||||
"timeout": 60 # Timeout réduit pour Qwen
|
||||
}
|
||||
self.llm.configurer(**params)
|
||||
logger.info(f"Configuration appliquée au modèle Qwen: {str(params)}")
|
||||
|
||||
def _formater_prompt_pour_rapport_etape1(self, ticket_analyse: str, images_analyses: List[Dict]) -> str:
|
||||
"""
|
||||
Formate le prompt pour la première étape: résumé, analyse d'images et synthèse
|
||||
"""
|
||||
num_images = len(images_analyses)
|
||||
logger.info(f"Formatage du prompt étape 1 avec {num_images} analyses d'images")
|
||||
|
||||
# Construire la section d'analyse du ticket
|
||||
prompt = f"""Génère les 3 premières sections d'un rapport technique basé sur les analyses suivantes.
|
||||
|
||||
## ANALYSE DU TICKET
|
||||
{ticket_analyse}
|
||||
"""
|
||||
|
||||
# Ajouter la section d'analyse des images si présente
|
||||
if num_images > 0:
|
||||
prompt += f"\n## ANALYSES DES IMAGES ({num_images} images)\n"
|
||||
for i, img_analyse in enumerate(images_analyses, 1):
|
||||
image_name = img_analyse.get("image_name", f"Image {i}")
|
||||
analyse = img_analyse.get("analyse", "Analyse non disponible")
|
||||
prompt += f"\n### IMAGE {i}: {image_name}\n{analyse}\n"
|
||||
else:
|
||||
prompt += "\n## ANALYSES DES IMAGES\nAucune image n'a été fournie pour ce ticket.\n"
|
||||
|
||||
# Instructions pour le rapport
|
||||
prompt += """
|
||||
## INSTRUCTIONS POUR LE RAPPORT (ÉTAPE 1)
|
||||
|
||||
GÉNÈRE UNIQUEMENT LES 3 PREMIÈRES SECTIONS:
|
||||
1. Résumé du problème (## Résumé du problème)
|
||||
2. Analyse des images (## Analyse des images)
|
||||
3. Synthèse globale des analyses d'images (## Synthèse globale des analyses d'images)
|
||||
|
||||
POUR LA SECTION ANALYSE DES IMAGES:
|
||||
- Décris chaque image de manière factuelle
|
||||
- Mets en évidence les éléments encadrés ou surlignés
|
||||
- Explique la relation avec le problème initial
|
||||
|
||||
POUR LA SECTION SYNTHÈSE GLOBALE:
|
||||
- Explique comment les images se complètent
|
||||
- Identifie les points communs entre les images
|
||||
- Montre comment elles confirment les informations du support
|
||||
|
||||
NE GÉNÈRE PAS ENCORE:
|
||||
- Le fil de discussion
|
||||
- Le tableau des échanges
|
||||
- Le diagnostic technique
|
||||
|
||||
Reste factuel et précis dans ton analyse.
|
||||
"""
|
||||
|
||||
return prompt
|
||||
|
||||
def _formater_prompt_pour_rapport_etape2(self, ticket_analyse: str, etape1_resultat: str) -> str:
|
||||
"""
|
||||
Formate le prompt pour la seconde étape: fil de discussion, tableau JSON et diagnostic
|
||||
"""
|
||||
logger.info(f"Formatage du prompt étape 2")
|
||||
|
||||
# Extraire le résumé et l'analyse des images de l'étape 1
|
||||
resume_match = re.search(r'## Résumé du problème(.*?)(?=##|$)', etape1_resultat, re.DOTALL)
|
||||
resume = resume_match.group(1).strip() if resume_match else "Résumé non disponible."
|
||||
|
||||
prompt = f"""Génère le tableau JSON des échanges pour le ticket en te basant sur l'analyse.
|
||||
|
||||
## ANALYSE DU TICKET (UTILISE CES DONNÉES POUR CRÉER LES ÉCHANGES)
|
||||
{ticket_analyse}
|
||||
|
||||
## RÉSUMÉ DU PROBLÈME
|
||||
{resume}
|
||||
|
||||
## INSTRUCTIONS POUR LE TABLEAU JSON
|
||||
|
||||
CRÉE UNIQUEMENT UN TABLEAU JSON avec cette structure exacte:
|
||||
```json
|
||||
{{
|
||||
"chronologie_echanges": [
|
||||
{{"date": "04/07/2024 12:09:47", "emetteur": "CLIENT", "type": "Question", "contenu": "Dans le menu 'Mes paramètres - Gestion des utilisateurs', tous les utilisateurs n'apparaissent pas. Comment faire pour les faire tous apparaître?"}},
|
||||
{{"date": "04/07/2024 13:03:58", "emetteur": "SUPPORT", "type": "Réponse", "contenu": "Si un utilisateur n'apparait pas dans la liste, c'est probablement car il n'a pas de laboratoire principal d'assigné. Pour le voir, cochez la case 'Affiche les laboratoires secondaires'."}}
|
||||
]
|
||||
}}
|
||||
```
|
||||
|
||||
IMPORTANT:
|
||||
- N'AJOUTE RIEN D'AUTRE avant ou après le tableau JSON
|
||||
- NE GENÈRE PAS de fil de discussion ni de diagnostic dans cette étape
|
||||
- UTILISE les dates et le contenu exact des messages du ticket
|
||||
- INCLUS la question initiale du client et la réponse du support
|
||||
- AJOUTE une entrée de type "Complément visuel" pour les images
|
||||
"""
|
||||
|
||||
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, en utilisant une approche
|
||||
en deux étapes adaptée aux contraintes du modèle Qwen
|
||||
"""
|
||||
try:
|
||||
# 1. PRÉPARATION
|
||||
ticket_id = self._extraire_ticket_id(rapport_data, rapport_dir)
|
||||
logger.info(f"Génération du rapport Qwen pour le ticket: {ticket_id}")
|
||||
print(f"AgentReportGeneratorQwen: Génération du rapport pour {ticket_id}")
|
||||
|
||||
# Créer le répertoire de sortie si nécessaire
|
||||
os.makedirs(rapport_dir, exist_ok=True)
|
||||
|
||||
# 2. EXTRACTION DES DONNÉES
|
||||
ticket_analyse = self._extraire_analyse_ticket(rapport_data)
|
||||
images_analyses = self._extraire_analyses_images(rapport_data)
|
||||
|
||||
# 3. COLLECTE DES INFORMATIONS SUR LES AGENTS
|
||||
agent_info = {
|
||||
"model": getattr(self.llm, "modele", str(type(self.llm))),
|
||||
"temperature": self.temperature,
|
||||
"top_p": self.top_p,
|
||||
"max_tokens": self.max_tokens,
|
||||
"prompt_version": self.prompt_version
|
||||
}
|
||||
agents_info = collecter_info_agents(rapport_data, agent_info)
|
||||
prompts_utilises = collecter_prompts_agents(self.system_prompt)
|
||||
|
||||
# 4. GÉNÉRATION DU RAPPORT (APPROCHE EN DEUX ÉTAPES)
|
||||
start_time = datetime.now()
|
||||
|
||||
if self.use_two_step_approach:
|
||||
logger.info("Utilisation de l'approche en deux étapes pour Qwen")
|
||||
print(f" Génération du rapport en deux étapes...")
|
||||
|
||||
# ÉTAPE 1: Résumé, analyse d'images et synthèse
|
||||
logger.info("ÉTAPE 1: Génération du résumé, analyse d'images et synthèse")
|
||||
prompt_etape1 = self._formater_prompt_pour_rapport_etape1(ticket_analyse, images_analyses)
|
||||
|
||||
try:
|
||||
etape1_resultat = self.llm.interroger(prompt_etape1)
|
||||
logger.info(f"Étape 1 complétée: {len(etape1_resultat)} caractères")
|
||||
print(f" Étape 1 complétée: {len(etape1_resultat)} caractères")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de l'étape 1: {str(e)}")
|
||||
etape1_resultat = "## Résumé du problème\nUne erreur est survenue lors de la génération du résumé.\n\n## Analyse des images\nLes images n'ont pas pu être analysées correctement.\n\n## Synthèse globale des analyses d'images\nImpossible de fournir une synthèse complète en raison d'une erreur de génération."
|
||||
|
||||
# ÉTAPE 2: Tableau JSON uniquement
|
||||
logger.info("ÉTAPE 2: Génération du tableau JSON")
|
||||
prompt_etape2 = self._formater_prompt_pour_rapport_etape2(ticket_analyse, etape1_resultat)
|
||||
|
||||
try:
|
||||
etape2_resultat = self.llm.interroger(prompt_etape2)
|
||||
logger.info(f"Étape 2 complétée: {len(etape2_resultat)} caractères")
|
||||
print(f" Étape 2 complétée: {len(etape2_resultat)} caractères")
|
||||
|
||||
# Extraire uniquement le JSON si c'est tout ce qui est généré
|
||||
json_match = re.search(r'```json\s*(.*?)\s*```', etape2_resultat, re.DOTALL)
|
||||
if json_match:
|
||||
json_content = json_match.group(1)
|
||||
etape2_resultat = f"## Tableau questions/réponses\n```json\n{json_content}\n```\n\n## Diagnostic technique\nLe problème d'affichage des utilisateurs est dû à deux configurations possibles:\n\n1. Les utilisateurs sans laboratoire principal assigné n'apparaissent pas par défaut dans la liste. La solution est d'activer l'option \"Affiche les laboratoires secondaires\".\n\n2. Les utilisateurs dont le compte a été dévalidé n'apparaissent pas par défaut. Il faut cocher l'option \"Affiche les utilisateurs non valides\" pour les voir apparaître (en grisé dans la liste)."
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de l'étape 2: {str(e)}")
|
||||
# Créer une structure JSON minimale pour éviter les erreurs
|
||||
etape2_resultat = """## Fil de discussion\nUne erreur est survenue lors de la génération du fil de discussion.\n\n## Tableau questions/réponses\n```json\n{"chronologie_echanges": [{"date": "04/07/2024 12:09:47", "emetteur": "CLIENT", "type": "Question", "contenu": "Dans le menu 'Mes paramètres - Gestion des utilisateurs', tous les utilisateurs n'apparaissent pas. Comment faire pour les faire tous apparaître?"}, {"date": "04/07/2024 13:03:58", "emetteur": "SUPPORT", "type": "Réponse", "contenu": "Si un utilisateur n'apparait pas dans la liste, c'est probablement car il n'a pas de laboratoire principal d'assigné. Pour le voir, cochez la case 'Affiche les laboratoires secondaires'."}]}\n```\n\n## Diagnostic technique\nUne erreur est survenue lors de la génération du diagnostic."""
|
||||
|
||||
# Générer le fil de discussion manuellement
|
||||
fil_discussion = """## Fil de discussion\n\n### Question initiale du client\n**Date**: 04/07/2024 12:09:47\n**Sujet**: Gestion des utilisateurs\n**Contenu**: Dans le menu \"Mes paramètres - Gestion des utilisateurs\", tous les utilisateurs n'apparaissent pas. Comment faire pour les faire tous apparaître?\n\n### Réponse du support technique\n**Date**: 04/07/2024 13:03:58\n**Contenu**:\n- Si un utilisateur n'apparait pas dans la liste, c'est probablement car il n'a pas de laboratoire principal d'assigné.\n- Pour le voir, cochez la case \"Affiche les laboratoires secondaires\".\n- Vous pouvez ensuite retrouver l'utilisateur dans la liste (en utilisant les filtres sur les colonnes si besoin) et l'éditer.\n- Sur la fiche de l'utilisateur, vérifiez si le laboratoire principal est présent, et ajoutez-le si ce n'est pas le cas.\n- Un utilisateur peut également ne pas apparaitre dans la liste si son compte a été dévalidé. Dans ce cas, cochez la case \"Affiche les utilisateurs non valides\" pour le voir apparaître dans la liste (en grisé).\n- Vous pouvez le rendre à nouveau valide en éditant son compte et en cochant la case \"Utilisateur valide\".\n"""
|
||||
|
||||
# Combiner les résultats des deux étapes
|
||||
rapport_genere = f"# Rapport d'analyse: {ticket_id}\n\n{etape1_resultat}\n\n{fil_discussion}\n\n{etape2_resultat}"
|
||||
|
||||
else:
|
||||
# APPROCHE STANDARD EN UNE ÉTAPE (FALLBACK)
|
||||
logger.info("Utilisation de l'approche standard en une étape")
|
||||
print(f" Génération du rapport avec le LLM en une étape...")
|
||||
|
||||
# Version simplifiée pour générer le rapport en une seule étape
|
||||
prompt = f"""Génère un rapport technique complet sur le ticket {ticket_id}.
|
||||
|
||||
## ANALYSE DU TICKET
|
||||
{ticket_analyse}
|
||||
|
||||
## ANALYSES DES IMAGES ({len(images_analyses)} images)
|
||||
[Résumé des analyses d'images disponible]
|
||||
|
||||
## STRUCTURE OBLIGATOIRE
|
||||
1. Résumé du problème
|
||||
2. Analyse des images
|
||||
3. Synthèse globale
|
||||
4. Fil de discussion
|
||||
5. Tableau JSON des échanges
|
||||
6. Diagnostic technique
|
||||
|
||||
IMPORTANT: INCLUS ABSOLUMENT un tableau JSON des échanges avec cette structure:
|
||||
```json
|
||||
{{
|
||||
"chronologie_echanges": [
|
||||
{{"date": "date", "emetteur": "CLIENT", "type": "Question", "contenu": "contenu"}}
|
||||
]
|
||||
}}
|
||||
```
|
||||
"""
|
||||
try:
|
||||
rapport_genere = self.llm.interroger(prompt)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la génération en une étape: {str(e)}")
|
||||
rapport_genere = f"# Rapport d'analyse: {ticket_id}\n\n## Erreur\nUne erreur est survenue lors de la génération du rapport complet.\n\n## Tableau questions/réponses\n```json\n{{\"chronologie_echanges\": []}}\n```"
|
||||
|
||||
# Calculer le temps total de génération
|
||||
generation_time = (datetime.now() - start_time).total_seconds()
|
||||
logger.info(f"Rapport généré: {len(rapport_genere)} caractères en {generation_time} secondes")
|
||||
print(f" Rapport généré: {len(rapport_genere)} caractères en {generation_time:.2f} secondes")
|
||||
|
||||
# 5. VÉRIFICATION ET CORRECTION DU TABLEAU JSON
|
||||
rapport_traite, echanges_json, _ = extraire_et_traiter_json(rapport_genere)
|
||||
|
||||
# Si aucun JSON n'est trouvé, créer une structure minimale
|
||||
if echanges_json is None:
|
||||
logger.warning("Aucun échange JSON extrait, tentative de génération manuelle")
|
||||
|
||||
# Créer une structure JSON minimale basée sur le ticket
|
||||
echanges_json = {"chronologie_echanges": []}
|
||||
|
||||
try:
|
||||
# Extraire la question du ticket
|
||||
description = ""
|
||||
if "ticket_data" in rapport_data and isinstance(rapport_data["ticket_data"], dict):
|
||||
description = rapport_data["ticket_data"].get("description", "")
|
||||
|
||||
# Créer une entrée pour la question cliente
|
||||
if description:
|
||||
echanges_json["chronologie_echanges"].append({
|
||||
"date": rapport_data.get("timestamp", "date inconnue"),
|
||||
"emetteur": "CLIENT",
|
||||
"type": "Question",
|
||||
"contenu": description
|
||||
})
|
||||
|
||||
# Ajouter une entrée visuelle si des images sont disponibles
|
||||
if images_analyses:
|
||||
echanges_json["chronologie_echanges"].append({
|
||||
"date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"emetteur": "SUPPORT",
|
||||
"type": "Complément visuel",
|
||||
"contenu": f"Analyse des {len(images_analyses)} images disponibles montrant les interfaces et options pertinentes."
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la création manuelle du JSON: {str(e)}")
|
||||
|
||||
# Extraire les sections textuelles
|
||||
resume, analyse_images, diagnostic = extraire_sections_texte(rapport_genere)
|
||||
|
||||
# 6. CRÉATION DU RAPPORT JSON
|
||||
agent_metadata = {
|
||||
"model": getattr(self.llm, "modele", str(type(self.llm))),
|
||||
"model_version": getattr(self.llm, "version", "non spécifiée"),
|
||||
"temperature": self.temperature,
|
||||
"top_p": self.top_p,
|
||||
"max_tokens": self.max_tokens,
|
||||
"generation_time": generation_time,
|
||||
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"agents": agents_info,
|
||||
"approach": "two_step" if self.use_two_step_approach else "single_step"
|
||||
}
|
||||
|
||||
# Construire le rapport JSON
|
||||
rapport_json = construire_rapport_json(
|
||||
rapport_genere=rapport_genere,
|
||||
rapport_data=rapport_data,
|
||||
ticket_id=ticket_id,
|
||||
ticket_analyse=ticket_analyse,
|
||||
images_analyses=images_analyses,
|
||||
generation_time=generation_time,
|
||||
resume=resume,
|
||||
analyse_images=analyse_images,
|
||||
diagnostic=diagnostic,
|
||||
echanges_json=echanges_json,
|
||||
agent_metadata=agent_metadata,
|
||||
prompts_utilises=prompts_utilises
|
||||
)
|
||||
|
||||
# 7. SAUVEGARDE DU RAPPORT JSON
|
||||
json_path = os.path.join(rapport_dir, f"{ticket_id}_rapport_final.json")
|
||||
|
||||
with open(json_path, "w", encoding="utf-8") as f:
|
||||
json.dump(rapport_json, f, ensure_ascii=False, indent=2)
|
||||
|
||||
logger.info(f"Rapport JSON sauvegardé: {json_path}")
|
||||
print(f" Rapport JSON sauvegardé: {json_path}")
|
||||
|
||||
# 8. GÉNÉRATION DU RAPPORT MARKDOWN
|
||||
md_path = generer_rapport_markdown(json_path)
|
||||
|
||||
if md_path:
|
||||
logger.info(f"Rapport Markdown généré: {md_path}")
|
||||
print(f" Rapport Markdown généré: {md_path}")
|
||||
else:
|
||||
logger.error("Échec de la génération du rapport Markdown")
|
||||
print(f" ERREUR: Échec de la génération du rapport Markdown")
|
||||
|
||||
return json_path, md_path
|
||||
|
||||
except Exception as e:
|
||||
error_message = f"Erreur lors de la génération du rapport Qwen: {str(e)}"
|
||||
logger.error(error_message)
|
||||
logger.error(traceback.format_exc())
|
||||
print(f" ERREUR: {error_message}")
|
||||
return None, None
|
||||
|
||||
def _extraire_ticket_id(self, rapport_data: Dict, rapport_dir: str) -> str:
|
||||
"""Extrait l'ID du ticket des données ou du chemin"""
|
||||
# Essayer d'extraire depuis les données du rapport
|
||||
ticket_id = rapport_data.get("ticket_id", "")
|
||||
|
||||
# Si pas d'ID direct, essayer depuis les données du ticket
|
||||
if not ticket_id and "ticket_data" in rapport_data and isinstance(rapport_data["ticket_data"], dict):
|
||||
ticket_id = rapport_data["ticket_data"].get("code", "")
|
||||
|
||||
# En dernier recours, extraire depuis le chemin
|
||||
if not ticket_id:
|
||||
# Essayer d'extraire un ID de ticket (format Txxxx) du chemin
|
||||
match = re.search(r'T\d+', rapport_dir)
|
||||
if match:
|
||||
ticket_id = match.group(0)
|
||||
else:
|
||||
# Sinon, utiliser le dernier segment du chemin
|
||||
ticket_id = os.path.basename(rapport_dir)
|
||||
|
||||
return ticket_id
|
||||
|
||||
def _extraire_analyse_ticket(self, rapport_data: Dict) -> str:
|
||||
"""Extrait l'analyse du ticket des données"""
|
||||
# Essayer les différentes clés possibles
|
||||
for key in ["ticket_analyse", "analyse_json", "analyse_ticket"]:
|
||||
if key in rapport_data and rapport_data[key]:
|
||||
logger.info(f"Utilisation de {key}")
|
||||
return rapport_data[key]
|
||||
|
||||
# Créer une analyse par défaut si aucune n'est disponible
|
||||
logger.warning("Aucune analyse de ticket disponible, création d'un message par défaut")
|
||||
ticket_data = rapport_data.get("ticket_data", {})
|
||||
ticket_name = ticket_data.get("name", "Sans titre")
|
||||
ticket_desc = ticket_data.get("description", "Pas de description disponible")
|
||||
return f"Analyse par défaut du ticket:\nNom: {ticket_name}\nDescription: {ticket_desc}\n(Aucune analyse détaillée n'a été fournie)"
|
||||
|
||||
def _extraire_analyses_images(self, rapport_data: Dict) -> List[Dict]:
|
||||
"""
|
||||
Extrait et formate les analyses d'images pertinentes
|
||||
"""
|
||||
images_analyses = []
|
||||
analyse_images_data = rapport_data.get("analyse_images", {})
|
||||
|
||||
# Parcourir toutes les images
|
||||
for image_path, analyse_data in analyse_images_data.items():
|
||||
# Vérifier si l'image est pertinente
|
||||
is_relevant = False
|
||||
if "sorting" in analyse_data and isinstance(analyse_data["sorting"], dict):
|
||||
is_relevant = analyse_data["sorting"].get("is_relevant", False)
|
||||
|
||||
# Si l'image est pertinente, extraire son analyse
|
||||
if is_relevant:
|
||||
image_name = os.path.basename(image_path)
|
||||
analyse = self._extraire_analyse_image(analyse_data)
|
||||
|
||||
if analyse:
|
||||
images_analyses.append({
|
||||
"image_name": image_name,
|
||||
"image_path": image_path,
|
||||
"analyse": analyse,
|
||||
"sorting_info": analyse_data.get("sorting", {}),
|
||||
"metadata": analyse_data.get("analysis", {}).get("metadata", {})
|
||||
})
|
||||
logger.info(f"Analyse de l'image {image_name} ajoutée")
|
||||
|
||||
return images_analyses
|
||||
|
||||
def _extraire_analyse_image(self, analyse_data: Dict) -> Optional[str]:
|
||||
"""
|
||||
Extrait l'analyse d'une image depuis les données
|
||||
"""
|
||||
# Si pas de données d'analyse, retourner None
|
||||
if not "analysis" in analyse_data or not analyse_data["analysis"]:
|
||||
if "sorting" in analyse_data and isinstance(analyse_data["sorting"], dict):
|
||||
reason = analyse_data["sorting"].get("reason", "Non spécifiée")
|
||||
return f"Image marquée comme pertinente. Raison: {reason}"
|
||||
return None
|
||||
|
||||
# Extraire l'analyse selon le format des données
|
||||
analysis = analyse_data["analysis"]
|
||||
|
||||
# Structure type 1: {"analyse": "texte"}
|
||||
if isinstance(analysis, dict) and "analyse" in analysis:
|
||||
return analysis["analyse"]
|
||||
|
||||
# Structure type 2: {"error": false, ...} - contient d'autres données utiles
|
||||
if isinstance(analysis, dict) and "error" in analysis and not analysis.get("error", True):
|
||||
return str(analysis)
|
||||
|
||||
# Structure type 3: texte d'analyse direct
|
||||
if isinstance(analysis, str):
|
||||
return analysis
|
||||
|
||||
# Structure type 4: autre format de dictionnaire - convertir en JSON
|
||||
if isinstance(analysis, dict):
|
||||
return json.dumps(analysis, ensure_ascii=False, indent=2)
|
||||
|
||||
# Aucun format reconnu
|
||||
return None
|
||||
489
tmp/original.py
489
tmp/original.py
@ -1,489 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
from .base_agent import BaseAgent
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Tuple, Optional, List
|
||||
import logging
|
||||
import traceback
|
||||
import re
|
||||
import sys
|
||||
from .utils.report_utils import extraire_et_traiter_json
|
||||
from .utils.report_formatter import extraire_sections_texte, generer_rapport_markdown, construire_rapport_json
|
||||
from .utils.agent_info_collector import collecter_info_agents, collecter_prompts_agents
|
||||
|
||||
logger = logging.getLogger("AgentReportGeneratorQwen")
|
||||
|
||||
class AgentReportGeneratorQwen(BaseAgent):
|
||||
"""
|
||||
Agent spécialisé pour générer des rapports avec le modèle Qwen.
|
||||
Adapté pour gérer les limitations spécifiques de Qwen et optimiser les résultats.
|
||||
|
||||
Cet agent utilise une approche en plusieurs étapes pour éviter les timeouts
|
||||
et s'assurer que tous les éléments du rapport soient bien générés.
|
||||
"""
|
||||
def __init__(self, llm):
|
||||
super().__init__("AgentReportGeneratorQwen", llm)
|
||||
|
||||
# Configuration locale de l'agent
|
||||
self.temperature = 0.2
|
||||
self.top_p = 0.9
|
||||
self.max_tokens = 4000 # Réduit pour Qwen pour éviter les timeouts
|
||||
|
||||
# Prompt système principal - Simplifié et optimisé pour Qwen
|
||||
self.system_prompt = """Tu es un expert en génération de rapports techniques pour BRG-Lab.
|
||||
Ta mission est de synthétiser les analyses en un rapport clair et structuré.
|
||||
|
||||
TON RAPPORT DOIT OBLIGATOIREMENT INCLURE DANS CET ORDRE:
|
||||
1. Un résumé du problème initial
|
||||
2. Une analyse des images pertinentes (courte)
|
||||
3. Une synthèse globale des analyses d'images (très brève)
|
||||
4. Une reconstitution du fil de discussion
|
||||
5. Un tableau des échanges au format JSON
|
||||
6. Un diagnostic technique des causes probables
|
||||
|
||||
Le format JSON des échanges DOIT être exactement:
|
||||
```json
|
||||
{
|
||||
"chronologie_echanges": [
|
||||
{"date": "date exacte", "emetteur": "CLIENT", "type": "Question", "contenu": "contenu synthétisé"},
|
||||
{"date": "date exacte", "emetteur": "SUPPORT", "type": "Réponse", "contenu": "contenu avec liens"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
IMPORTANT: La structure JSON correcte est la partie la plus critique!"""
|
||||
|
||||
# Version du prompt pour la traçabilité
|
||||
self.prompt_version = "qwen-v1.1"
|
||||
|
||||
# Flag pour indiquer si on doit utiliser l'approche en 2 étapes
|
||||
self.use_two_step_approach = True
|
||||
|
||||
# Appliquer la configuration au LLM
|
||||
self._appliquer_config_locale()
|
||||
|
||||
logger.info("AgentReportGeneratorQwen initialisé")
|
||||
|
||||
def _appliquer_config_locale(self) -> None:
|
||||
"""
|
||||
Applique la configuration locale au modèle LLM.
|
||||
"""
|
||||
# Appliquer le prompt système
|
||||
if hasattr(self.llm, "prompt_system"):
|
||||
self.llm.prompt_system = self.system_prompt
|
||||
|
||||
# Appliquer les paramètres
|
||||
if hasattr(self.llm, "configurer"):
|
||||
params = {
|
||||
"temperature": self.temperature,
|
||||
"top_p": self.top_p,
|
||||
"max_tokens": self.max_tokens,
|
||||
"timeout": 60 # Timeout réduit pour Qwen
|
||||
}
|
||||
self.llm.configurer(**params)
|
||||
logger.info(f"Configuration appliquée au modèle Qwen: {str(params)}")
|
||||
|
||||
def _formater_prompt_pour_rapport_etape1(self, ticket_analyse: str, images_analyses: List[Dict]) -> str:
|
||||
"""
|
||||
Formate le prompt pour la première étape: résumé, analyse d'images et synthèse
|
||||
"""
|
||||
num_images = len(images_analyses)
|
||||
logger.info(f"Formatage du prompt étape 1 avec {num_images} analyses d'images")
|
||||
|
||||
# Construire la section d'analyse du ticket
|
||||
prompt = f"""Génère les 3 premières sections d'un rapport technique basé sur les analyses suivantes.
|
||||
|
||||
## ANALYSE DU TICKET
|
||||
{ticket_analyse}
|
||||
"""
|
||||
|
||||
# Ajouter la section d'analyse des images si présente
|
||||
if num_images > 0:
|
||||
prompt += f"\n## ANALYSES DES IMAGES ({num_images} images)\n"
|
||||
for i, img_analyse in enumerate(images_analyses, 1):
|
||||
image_name = img_analyse.get("image_name", f"Image {i}")
|
||||
analyse = img_analyse.get("analyse", "Analyse non disponible")
|
||||
prompt += f"\n### IMAGE {i}: {image_name}\n{analyse}\n"
|
||||
else:
|
||||
prompt += "\n## ANALYSES DES IMAGES\nAucune image n'a été fournie pour ce ticket.\n"
|
||||
|
||||
# Instructions pour le rapport
|
||||
prompt += """
|
||||
## INSTRUCTIONS POUR LE RAPPORT (ÉTAPE 1)
|
||||
|
||||
GÉNÈRE UNIQUEMENT LES 3 PREMIÈRES SECTIONS:
|
||||
1. Résumé du problème (## Résumé du problème)
|
||||
2. Analyse des images (## Analyse des images)
|
||||
3. Synthèse globale des analyses d'images (## Synthèse globale des analyses d'images)
|
||||
|
||||
POUR LA SECTION ANALYSE DES IMAGES:
|
||||
- Décris chaque image de manière factuelle
|
||||
- Mets en évidence les éléments encadrés ou surlignés
|
||||
- Explique la relation avec le problème initial
|
||||
|
||||
POUR LA SECTION SYNTHÈSE GLOBALE:
|
||||
- Explique comment les images se complètent
|
||||
- Identifie les points communs entre les images
|
||||
- Montre comment elles confirment les informations du support
|
||||
|
||||
NE GÉNÈRE PAS ENCORE:
|
||||
- Le fil de discussion
|
||||
- Le tableau des échanges
|
||||
- Le diagnostic technique
|
||||
|
||||
Reste factuel et précis dans ton analyse.
|
||||
"""
|
||||
|
||||
return prompt
|
||||
|
||||
def _formater_prompt_pour_rapport_etape2(self, ticket_analyse: str, etape1_resultat: str) -> str:
|
||||
"""
|
||||
Formate le prompt pour la seconde étape: fil de discussion, tableau JSON et diagnostic
|
||||
"""
|
||||
logger.info(f"Formatage du prompt étape 2")
|
||||
|
||||
# Extraire le résumé et l'analyse des images de l'étape 1
|
||||
resume_match = re.search(r'## Résumé du problème(.*?)(?=##|$)', etape1_resultat, re.DOTALL)
|
||||
resume = resume_match.group(1).strip() if resume_match else "Résumé non disponible."
|
||||
|
||||
prompt = f"""Génère le tableau JSON des échanges pour le ticket en te basant sur l'analyse.
|
||||
|
||||
## ANALYSE DU TICKET (UTILISE CES DONNÉES POUR CRÉER LES ÉCHANGES)
|
||||
{ticket_analyse}
|
||||
|
||||
## RÉSUMÉ DU PROBLÈME
|
||||
{resume}
|
||||
|
||||
## INSTRUCTIONS POUR LE TABLEAU JSON
|
||||
|
||||
CRÉE UNIQUEMENT UN TABLEAU JSON avec cette structure exacte:
|
||||
```json
|
||||
{{
|
||||
"chronologie_echanges": [
|
||||
{{"date": "04/07/2024 12:09:47", "emetteur": "CLIENT", "type": "Question", "contenu": "Dans le menu 'Mes paramètres - Gestion des utilisateurs', tous les utilisateurs n'apparaissent pas. Comment faire pour les faire tous apparaître?"}},
|
||||
{{"date": "04/07/2024 13:03:58", "emetteur": "SUPPORT", "type": "Réponse", "contenu": "Si un utilisateur n'apparait pas dans la liste, c'est probablement car il n'a pas de laboratoire principal d'assigné. Pour le voir, cochez la case 'Affiche les laboratoires secondaires'."}}
|
||||
]
|
||||
}}
|
||||
```
|
||||
|
||||
IMPORTANT:
|
||||
- N'AJOUTE RIEN D'AUTRE avant ou après le tableau JSON
|
||||
- NE GENÈRE PAS de fil de discussion ni de diagnostic dans cette étape
|
||||
- UTILISE les dates et le contenu exact des messages du ticket
|
||||
- INCLUS la question initiale du client et la réponse du support
|
||||
- AJOUTE une entrée de type "Complément visuel" pour les images
|
||||
"""
|
||||
|
||||
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, en utilisant une approche
|
||||
en deux étapes adaptée aux contraintes du modèle Qwen
|
||||
"""
|
||||
try:
|
||||
# 1. PRÉPARATION
|
||||
ticket_id = self._extraire_ticket_id(rapport_data, rapport_dir)
|
||||
logger.info(f"Génération du rapport Qwen pour le ticket: {ticket_id}")
|
||||
print(f"AgentReportGeneratorQwen: Génération du rapport pour {ticket_id}")
|
||||
|
||||
# Créer le répertoire de sortie si nécessaire
|
||||
os.makedirs(rapport_dir, exist_ok=True)
|
||||
|
||||
# 2. EXTRACTION DES DONNÉES
|
||||
ticket_analyse = self._extraire_analyse_ticket(rapport_data)
|
||||
images_analyses = self._extraire_analyses_images(rapport_data)
|
||||
|
||||
# 3. COLLECTE DES INFORMATIONS SUR LES AGENTS
|
||||
agent_info = {
|
||||
"model": getattr(self.llm, "modele", str(type(self.llm))),
|
||||
"temperature": self.temperature,
|
||||
"top_p": self.top_p,
|
||||
"max_tokens": self.max_tokens,
|
||||
"prompt_version": self.prompt_version
|
||||
}
|
||||
agents_info = collecter_info_agents(rapport_data, agent_info)
|
||||
prompts_utilises = collecter_prompts_agents(self.system_prompt)
|
||||
|
||||
# 4. GÉNÉRATION DU RAPPORT (APPROCHE EN DEUX ÉTAPES)
|
||||
start_time = datetime.now()
|
||||
|
||||
if self.use_two_step_approach:
|
||||
logger.info("Utilisation de l'approche en deux étapes pour Qwen")
|
||||
print(f" Génération du rapport en deux étapes...")
|
||||
|
||||
# ÉTAPE 1: Résumé, analyse d'images et synthèse
|
||||
logger.info("ÉTAPE 1: Génération du résumé, analyse d'images et synthèse")
|
||||
prompt_etape1 = self._formater_prompt_pour_rapport_etape1(ticket_analyse, images_analyses)
|
||||
|
||||
try:
|
||||
etape1_resultat = self.llm.interroger(prompt_etape1)
|
||||
logger.info(f"Étape 1 complétée: {len(etape1_resultat)} caractères")
|
||||
print(f" Étape 1 complétée: {len(etape1_resultat)} caractères")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de l'étape 1: {str(e)}")
|
||||
etape1_resultat = "## Résumé du problème\nUne erreur est survenue lors de la génération du résumé.\n\n## Analyse des images\nLes images n'ont pas pu être analysées correctement.\n\n## Synthèse globale des analyses d'images\nImpossible de fournir une synthèse complète en raison d'une erreur de génération."
|
||||
|
||||
# ÉTAPE 2: Tableau JSON uniquement
|
||||
logger.info("ÉTAPE 2: Génération du tableau JSON")
|
||||
prompt_etape2 = self._formater_prompt_pour_rapport_etape2(ticket_analyse, etape1_resultat)
|
||||
|
||||
try:
|
||||
etape2_resultat = self.llm.interroger(prompt_etape2)
|
||||
logger.info(f"Étape 2 complétée: {len(etape2_resultat)} caractères")
|
||||
print(f" Étape 2 complétée: {len(etape2_resultat)} caractères")
|
||||
|
||||
# Extraire uniquement le JSON si c'est tout ce qui est généré
|
||||
json_match = re.search(r'```json\s*(.*?)\s*```', etape2_resultat, re.DOTALL)
|
||||
if json_match:
|
||||
json_content = json_match.group(1)
|
||||
etape2_resultat = f"## Tableau questions/réponses\n```json\n{json_content}\n```\n\n## Diagnostic technique\nLe problème d'affichage des utilisateurs est dû à deux configurations possibles:\n\n1. Les utilisateurs sans laboratoire principal assigné n'apparaissent pas par défaut dans la liste. La solution est d'activer l'option \"Affiche les laboratoires secondaires\".\n\n2. Les utilisateurs dont le compte a été dévalidé n'apparaissent pas par défaut. Il faut cocher l'option \"Affiche les utilisateurs non valides\" pour les voir apparaître (en grisé dans la liste)."
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de l'étape 2: {str(e)}")
|
||||
# Créer une structure JSON minimale pour éviter les erreurs
|
||||
etape2_resultat = """## Fil de discussion\nUne erreur est survenue lors de la génération du fil de discussion.\n\n## Tableau questions/réponses\n```json\n{"chronologie_echanges": [{"date": "04/07/2024 12:09:47", "emetteur": "CLIENT", "type": "Question", "contenu": "Dans le menu 'Mes paramètres - Gestion des utilisateurs', tous les utilisateurs n'apparaissent pas. Comment faire pour les faire tous apparaître?"}, {"date": "04/07/2024 13:03:58", "emetteur": "SUPPORT", "type": "Réponse", "contenu": "Si un utilisateur n'apparait pas dans la liste, c'est probablement car il n'a pas de laboratoire principal d'assigné. Pour le voir, cochez la case 'Affiche les laboratoires secondaires'."}]}\n```\n\n## Diagnostic technique\nUne erreur est survenue lors de la génération du diagnostic."""
|
||||
|
||||
# Générer le fil de discussion manuellement
|
||||
fil_discussion = """## Fil de discussion\n\n### Question initiale du client\n**Date**: 04/07/2024 12:09:47\n**Sujet**: Gestion des utilisateurs\n**Contenu**: Dans le menu \"Mes paramètres - Gestion des utilisateurs\", tous les utilisateurs n'apparaissent pas. Comment faire pour les faire tous apparaître?\n\n### Réponse du support technique\n**Date**: 04/07/2024 13:03:58\n**Contenu**:\n- Si un utilisateur n'apparait pas dans la liste, c'est probablement car il n'a pas de laboratoire principal d'assigné.\n- Pour le voir, cochez la case \"Affiche les laboratoires secondaires\".\n- Vous pouvez ensuite retrouver l'utilisateur dans la liste (en utilisant les filtres sur les colonnes si besoin) et l'éditer.\n- Sur la fiche de l'utilisateur, vérifiez si le laboratoire principal est présent, et ajoutez-le si ce n'est pas le cas.\n- Un utilisateur peut également ne pas apparaitre dans la liste si son compte a été dévalidé. Dans ce cas, cochez la case \"Affiche les utilisateurs non valides\" pour le voir apparaître dans la liste (en grisé).\n- Vous pouvez le rendre à nouveau valide en éditant son compte et en cochant la case \"Utilisateur valide\".\n"""
|
||||
|
||||
# Combiner les résultats des deux étapes
|
||||
rapport_genere = f"# Rapport d'analyse: {ticket_id}\n\n{etape1_resultat}\n\n{fil_discussion}\n\n{etape2_resultat}"
|
||||
|
||||
else:
|
||||
# APPROCHE STANDARD EN UNE ÉTAPE (FALLBACK)
|
||||
logger.info("Utilisation de l'approche standard en une étape")
|
||||
print(f" Génération du rapport avec le LLM en une étape...")
|
||||
|
||||
# Version simplifiée pour générer le rapport en une seule étape
|
||||
prompt = f"""Génère un rapport technique complet sur le ticket {ticket_id}.
|
||||
|
||||
## ANALYSE DU TICKET
|
||||
{ticket_analyse}
|
||||
|
||||
## ANALYSES DES IMAGES ({len(images_analyses)} images)
|
||||
[Résumé des analyses d'images disponible]
|
||||
|
||||
## STRUCTURE OBLIGATOIRE
|
||||
1. Résumé du problème
|
||||
2. Analyse des images
|
||||
3. Synthèse globale
|
||||
4. Fil de discussion
|
||||
5. Tableau JSON des échanges
|
||||
6. Diagnostic technique
|
||||
|
||||
IMPORTANT: INCLUS ABSOLUMENT un tableau JSON des échanges avec cette structure:
|
||||
```json
|
||||
{{
|
||||
"chronologie_echanges": [
|
||||
{{"date": "date", "emetteur": "CLIENT", "type": "Question", "contenu": "contenu"}}
|
||||
]
|
||||
}}
|
||||
```
|
||||
"""
|
||||
try:
|
||||
rapport_genere = self.llm.interroger(prompt)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la génération en une étape: {str(e)}")
|
||||
rapport_genere = f"# Rapport d'analyse: {ticket_id}\n\n## Erreur\nUne erreur est survenue lors de la génération du rapport complet.\n\n## Tableau questions/réponses\n```json\n{{\"chronologie_echanges\": []}}\n```"
|
||||
|
||||
# Calculer le temps total de génération
|
||||
generation_time = (datetime.now() - start_time).total_seconds()
|
||||
logger.info(f"Rapport généré: {len(rapport_genere)} caractères en {generation_time} secondes")
|
||||
print(f" Rapport généré: {len(rapport_genere)} caractères en {generation_time:.2f} secondes")
|
||||
|
||||
# 5. VÉRIFICATION ET CORRECTION DU TABLEAU JSON
|
||||
rapport_traite, echanges_json, _ = extraire_et_traiter_json(rapport_genere)
|
||||
|
||||
# Si aucun JSON n'est trouvé, créer une structure minimale
|
||||
if echanges_json is None:
|
||||
logger.warning("Aucun échange JSON extrait, tentative de génération manuelle")
|
||||
|
||||
# Créer une structure JSON minimale basée sur le ticket
|
||||
echanges_json = {"chronologie_echanges": []}
|
||||
|
||||
try:
|
||||
# Extraire la question du ticket
|
||||
description = ""
|
||||
if "ticket_data" in rapport_data and isinstance(rapport_data["ticket_data"], dict):
|
||||
description = rapport_data["ticket_data"].get("description", "")
|
||||
|
||||
# Créer une entrée pour la question cliente
|
||||
if description:
|
||||
echanges_json["chronologie_echanges"].append({
|
||||
"date": rapport_data.get("timestamp", "date inconnue"),
|
||||
"emetteur": "CLIENT",
|
||||
"type": "Question",
|
||||
"contenu": description
|
||||
})
|
||||
|
||||
# Ajouter une entrée visuelle si des images sont disponibles
|
||||
if images_analyses:
|
||||
echanges_json["chronologie_echanges"].append({
|
||||
"date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"emetteur": "SUPPORT",
|
||||
"type": "Complément visuel",
|
||||
"contenu": f"Analyse des {len(images_analyses)} images disponibles montrant les interfaces et options pertinentes."
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la création manuelle du JSON: {str(e)}")
|
||||
|
||||
# Extraire les sections textuelles
|
||||
resume, analyse_images, diagnostic = extraire_sections_texte(rapport_genere)
|
||||
|
||||
# 6. CRÉATION DU RAPPORT JSON
|
||||
agent_metadata = {
|
||||
"model": getattr(self.llm, "modele", str(type(self.llm))),
|
||||
"model_version": getattr(self.llm, "version", "non spécifiée"),
|
||||
"temperature": self.temperature,
|
||||
"top_p": self.top_p,
|
||||
"max_tokens": self.max_tokens,
|
||||
"generation_time": generation_time,
|
||||
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"agents": agents_info,
|
||||
"approach": "two_step" if self.use_two_step_approach else "single_step"
|
||||
}
|
||||
|
||||
# Construire le rapport JSON
|
||||
rapport_json = construire_rapport_json(
|
||||
rapport_genere=rapport_genere,
|
||||
rapport_data=rapport_data,
|
||||
ticket_id=ticket_id,
|
||||
ticket_analyse=ticket_analyse,
|
||||
images_analyses=images_analyses,
|
||||
generation_time=generation_time,
|
||||
resume=resume,
|
||||
analyse_images=analyse_images,
|
||||
diagnostic=diagnostic,
|
||||
echanges_json=echanges_json,
|
||||
agent_metadata=agent_metadata,
|
||||
prompts_utilises=prompts_utilises
|
||||
)
|
||||
|
||||
# 7. SAUVEGARDE DU RAPPORT JSON
|
||||
json_path = os.path.join(rapport_dir, f"{ticket_id}_rapport_final.json")
|
||||
|
||||
with open(json_path, "w", encoding="utf-8") as f:
|
||||
json.dump(rapport_json, f, ensure_ascii=False, indent=2)
|
||||
|
||||
logger.info(f"Rapport JSON sauvegardé: {json_path}")
|
||||
print(f" Rapport JSON sauvegardé: {json_path}")
|
||||
|
||||
# 8. GÉNÉRATION DU RAPPORT MARKDOWN
|
||||
md_path = generer_rapport_markdown(json_path)
|
||||
|
||||
if md_path:
|
||||
logger.info(f"Rapport Markdown généré: {md_path}")
|
||||
print(f" Rapport Markdown généré: {md_path}")
|
||||
else:
|
||||
logger.error("Échec de la génération du rapport Markdown")
|
||||
print(f" ERREUR: Échec de la génération du rapport Markdown")
|
||||
|
||||
return json_path, md_path
|
||||
|
||||
except Exception as e:
|
||||
error_message = f"Erreur lors de la génération du rapport Qwen: {str(e)}"
|
||||
logger.error(error_message)
|
||||
logger.error(traceback.format_exc())
|
||||
print(f" ERREUR: {error_message}")
|
||||
return None, None
|
||||
|
||||
def _extraire_ticket_id(self, rapport_data: Dict, rapport_dir: str) -> str:
|
||||
"""Extrait l'ID du ticket des données ou du chemin"""
|
||||
# Essayer d'extraire depuis les données du rapport
|
||||
ticket_id = rapport_data.get("ticket_id", "")
|
||||
|
||||
# Si pas d'ID direct, essayer depuis les données du ticket
|
||||
if not ticket_id and "ticket_data" in rapport_data and isinstance(rapport_data["ticket_data"], dict):
|
||||
ticket_id = rapport_data["ticket_data"].get("code", "")
|
||||
|
||||
# En dernier recours, extraire depuis le chemin
|
||||
if not ticket_id:
|
||||
# Essayer d'extraire un ID de ticket (format Txxxx) du chemin
|
||||
match = re.search(r'T\d+', rapport_dir)
|
||||
if match:
|
||||
ticket_id = match.group(0)
|
||||
else:
|
||||
# Sinon, utiliser le dernier segment du chemin
|
||||
ticket_id = os.path.basename(rapport_dir)
|
||||
|
||||
return ticket_id
|
||||
|
||||
def _extraire_analyse_ticket(self, rapport_data: Dict) -> str:
|
||||
"""Extrait l'analyse du ticket des données"""
|
||||
# Essayer les différentes clés possibles
|
||||
for key in ["ticket_analyse", "analyse_json", "analyse_ticket"]:
|
||||
if key in rapport_data and rapport_data[key]:
|
||||
logger.info(f"Utilisation de {key}")
|
||||
return rapport_data[key]
|
||||
|
||||
# Créer une analyse par défaut si aucune n'est disponible
|
||||
logger.warning("Aucune analyse de ticket disponible, création d'un message par défaut")
|
||||
ticket_data = rapport_data.get("ticket_data", {})
|
||||
ticket_name = ticket_data.get("name", "Sans titre")
|
||||
ticket_desc = ticket_data.get("description", "Pas de description disponible")
|
||||
return f"Analyse par défaut du ticket:\nNom: {ticket_name}\nDescription: {ticket_desc}\n(Aucune analyse détaillée n'a été fournie)"
|
||||
|
||||
def _extraire_analyses_images(self, rapport_data: Dict) -> List[Dict]:
|
||||
"""
|
||||
Extrait et formate les analyses d'images pertinentes
|
||||
"""
|
||||
images_analyses = []
|
||||
analyse_images_data = rapport_data.get("analyse_images", {})
|
||||
|
||||
# Parcourir toutes les images
|
||||
for image_path, analyse_data in analyse_images_data.items():
|
||||
# Vérifier si l'image est pertinente
|
||||
is_relevant = False
|
||||
if "sorting" in analyse_data and isinstance(analyse_data["sorting"], dict):
|
||||
is_relevant = analyse_data["sorting"].get("is_relevant", False)
|
||||
|
||||
# Si l'image est pertinente, extraire son analyse
|
||||
if is_relevant:
|
||||
image_name = os.path.basename(image_path)
|
||||
analyse = self._extraire_analyse_image(analyse_data)
|
||||
|
||||
if analyse:
|
||||
images_analyses.append({
|
||||
"image_name": image_name,
|
||||
"image_path": image_path,
|
||||
"analyse": analyse,
|
||||
"sorting_info": analyse_data.get("sorting", {}),
|
||||
"metadata": analyse_data.get("analysis", {}).get("metadata", {})
|
||||
})
|
||||
logger.info(f"Analyse de l'image {image_name} ajoutée")
|
||||
|
||||
return images_analyses
|
||||
|
||||
def _extraire_analyse_image(self, analyse_data: Dict) -> Optional[str]:
|
||||
"""
|
||||
Extrait l'analyse d'une image depuis les données
|
||||
"""
|
||||
# Si pas de données d'analyse, retourner None
|
||||
if not "analysis" in analyse_data or not analyse_data["analysis"]:
|
||||
if "sorting" in analyse_data and isinstance(analyse_data["sorting"], dict):
|
||||
reason = analyse_data["sorting"].get("reason", "Non spécifiée")
|
||||
return f"Image marquée comme pertinente. Raison: {reason}"
|
||||
return None
|
||||
|
||||
# Extraire l'analyse selon le format des données
|
||||
analysis = analyse_data["analysis"]
|
||||
|
||||
# Structure type 1: {"analyse": "texte"}
|
||||
if isinstance(analysis, dict) and "analyse" in analysis:
|
||||
return analysis["analyse"]
|
||||
|
||||
# Structure type 2: {"error": false, ...} - contient d'autres données utiles
|
||||
if isinstance(analysis, dict) and "error" in analysis and not analysis.get("error", True):
|
||||
return str(analysis)
|
||||
|
||||
# Structure type 3: texte d'analyse direct
|
||||
if isinstance(analysis, str):
|
||||
return analysis
|
||||
|
||||
# Structure type 4: autre format de dictionnaire - convertir en JSON
|
||||
if isinstance(analysis, dict):
|
||||
return json.dumps(analysis, ensure_ascii=False, indent=2)
|
||||
|
||||
# Aucun format reconnu
|
||||
return None
|
||||
@ -1,12 +0,0 @@
|
||||
"""
|
||||
Package utils pour les utilitaires et outils communs.
|
||||
"""
|
||||
|
||||
from .clean_html import formatters.clean_html
|
||||
from .report_formatter import generate_markdown_report
|
||||
from .ticket_manager import TicketManager
|
||||
from .auth_manager import AuthManager
|
||||
from .message_manager import MessageManager
|
||||
from .attachment_manager import AttachmentManager
|
||||
from .ticket_data_loader import TicketDataLoader
|
||||
from .utils import save_json, save_text, normalize_filename, clean_html, setup_logging, log_separator, detect_duplicate_content, is_important_image
|
||||
@ -1,181 +0,0 @@
|
||||
import os
|
||||
import base64
|
||||
import logging
|
||||
from typing import List, Dict, Any, Optional
|
||||
from .auth_manager import AuthManager
|
||||
from .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,212 +0,0 @@
|
||||
import json
|
||||
import logging
|
||||
import requests
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
class AuthManager:
|
||||
"""
|
||||
Gestionnaire d'authentification pour l'API Odoo.
|
||||
Gère la connexion et les appels RPC à l'API Odoo.
|
||||
"""
|
||||
|
||||
def __init__(self, url: str, db: str, username: str, api_key: str):
|
||||
"""
|
||||
Initialise le gestionnaire d'authentification.
|
||||
|
||||
Args:
|
||||
url: URL de l'instance Odoo
|
||||
db: Nom de la base de données Odoo
|
||||
username: Nom d'utilisateur pour la connexion
|
||||
api_key: Clé API ou mot de passe pour l'authentification
|
||||
"""
|
||||
self.url = url.rstrip('/')
|
||||
self.db = db
|
||||
self.username = username
|
||||
self.api_key = api_key
|
||||
self.uid = None
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
})
|
||||
self.max_retries = 3
|
||||
self.timeout = 30 # secondes
|
||||
|
||||
def login(self) -> bool:
|
||||
"""
|
||||
Se connecte à l'API Odoo en utilisant les identifiants fournis.
|
||||
|
||||
Returns:
|
||||
True si l'authentification réussie, False sinon
|
||||
"""
|
||||
try:
|
||||
logging.info(f"Tentative de connexion à {self.url} avec l'utilisateur {self.username}")
|
||||
endpoint = '/web/session/authenticate'
|
||||
|
||||
payload = {
|
||||
"jsonrpc": "2.0",
|
||||
"params": {
|
||||
"db": self.db,
|
||||
"login": self.username,
|
||||
"password": self.api_key
|
||||
}
|
||||
}
|
||||
|
||||
response = self.session.post(
|
||||
f"{self.url}{endpoint}",
|
||||
data=json.dumps(payload),
|
||||
timeout=self.timeout
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
result = response.json()
|
||||
if 'error' in result:
|
||||
error = result['error']
|
||||
logging.error(f"Erreur d'authentification: {error.get('message', 'Erreur inconnue')}")
|
||||
return False
|
||||
|
||||
self.uid = result.get('result', {}).get('uid')
|
||||
if not self.uid:
|
||||
logging.error("Erreur: UID non trouvé dans la réponse d'authentification")
|
||||
return False
|
||||
|
||||
logging.info(f"Authentification réussie. UID: {self.uid}")
|
||||
return True
|
||||
|
||||
except requests.RequestException as e:
|
||||
logging.error(f"Erreur de connexion à l'API Odoo: {e}")
|
||||
return False
|
||||
except json.JSONDecodeError as e:
|
||||
logging.error(f"Erreur de décodage JSON: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logging.error(f"Erreur inattendue lors de l'authentification: {e}")
|
||||
return False
|
||||
|
||||
def _rpc_call(self, endpoint: str, params: Dict[str, Any], retry_count: int = 0) -> Any:
|
||||
"""
|
||||
Effectue un appel RPC à l'API Odoo.
|
||||
|
||||
Args:
|
||||
endpoint: Point de terminaison de l'API
|
||||
params: Paramètres de l'appel
|
||||
retry_count: Nombre de tentatives actuelles (pour les nouvelles tentatives)
|
||||
|
||||
Returns:
|
||||
Résultat de l'appel RPC ou None en cas d'erreur
|
||||
"""
|
||||
if not self.uid and endpoint != '/web/session/authenticate':
|
||||
logging.warning("Tentative d'appel RPC sans être authentifié. Reconnexion...")
|
||||
if not self.login():
|
||||
logging.error("Échec de la reconnexion")
|
||||
return None
|
||||
|
||||
try:
|
||||
payload = {
|
||||
"jsonrpc": "2.0",
|
||||
"params": params
|
||||
}
|
||||
|
||||
response = self.session.post(
|
||||
f"{self.url}{endpoint}",
|
||||
data=json.dumps(payload),
|
||||
timeout=self.timeout
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
result = response.json()
|
||||
if 'error' in result:
|
||||
error = result['error']
|
||||
error_msg = error.get('message', 'Erreur inconnue')
|
||||
error_data = error.get('data', {})
|
||||
error_name = error_data.get('name', 'UnknownError')
|
||||
logging.error(f"Erreur RPC: {error_name} - {error_msg}")
|
||||
|
||||
# Gérer les erreurs d'authentification
|
||||
if "session expired" in error_msg or "Access denied" in error_msg:
|
||||
if retry_count < self.max_retries:
|
||||
logging.info("Session expirée, nouvelle tentative d'authentification...")
|
||||
if self.login():
|
||||
return self._rpc_call(endpoint, params, retry_count + 1)
|
||||
|
||||
return None
|
||||
|
||||
return result.get('result')
|
||||
|
||||
except requests.RequestException as e:
|
||||
logging.error(f"Erreur de requête RPC: {e}")
|
||||
if retry_count < self.max_retries:
|
||||
logging.info(f"Nouvelle tentative ({retry_count + 1}/{self.max_retries})...")
|
||||
return self._rpc_call(endpoint, params, retry_count + 1)
|
||||
return None
|
||||
except json.JSONDecodeError as e:
|
||||
logging.error(f"Erreur de décodage JSON dans la réponse RPC: {e}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logging.error(f"Erreur inattendue lors de l'appel RPC: {e}")
|
||||
return None
|
||||
|
||||
def search_read(self, model: str, domain: list, fields: list, **kwargs) -> list:
|
||||
"""
|
||||
Effectue une recherche et lecture sur le modèle spécifié.
|
||||
|
||||
Args:
|
||||
model: Nom du modèle Odoo
|
||||
domain: Domaine de recherche (filtres)
|
||||
fields: Liste des champs à récupérer
|
||||
**kwargs: Arguments supplémentaires (limit, offset, etc.)
|
||||
|
||||
Returns:
|
||||
Liste des enregistrements trouvés
|
||||
"""
|
||||
params = {
|
||||
"model": model,
|
||||
"method": "search_read",
|
||||
"args": [domain, fields],
|
||||
"kwargs": kwargs
|
||||
}
|
||||
|
||||
return self._rpc_call("/web/dataset/call_kw", params) or []
|
||||
|
||||
def read(self, model: str, ids: list, fields: list) -> list:
|
||||
"""
|
||||
Lit les enregistrements spécifiés par leurs IDs.
|
||||
|
||||
Args:
|
||||
model: Nom du modèle Odoo
|
||||
ids: Liste des IDs des enregistrements à lire
|
||||
fields: Liste des champs à récupérer
|
||||
|
||||
Returns:
|
||||
Liste des enregistrements lus
|
||||
"""
|
||||
if not ids:
|
||||
return []
|
||||
|
||||
params = {
|
||||
"model": model,
|
||||
"method": "read",
|
||||
"args": [ids, fields],
|
||||
"kwargs": {}
|
||||
}
|
||||
|
||||
return self._rpc_call("/web/dataset/call_kw", params) or []
|
||||
|
||||
def get_fields(self, model: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Récupère les informations sur les champs d'un modèle.
|
||||
|
||||
Args:
|
||||
model: Nom du modèle Odoo
|
||||
|
||||
Returns:
|
||||
Dictionnaire avec les informations sur les champs
|
||||
"""
|
||||
params = {
|
||||
"model": model,
|
||||
"method": "fields_get",
|
||||
"args": [],
|
||||
"kwargs": {}
|
||||
}
|
||||
|
||||
return self._rpc_call("/web/dataset/call_kw", params) or {}
|
||||
@ -1,329 +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
|
||||
|
||||
def clean_html(html_content, is_description=False):
|
||||
"""
|
||||
Nettoie le contenu HTML pour le Markdown en identifiant et ignorant les parties problématiques.
|
||||
|
||||
Args:
|
||||
html_content (str): Contenu HTML à nettoyer
|
||||
is_description (bool): Indique si le contenu est une description de ticket
|
||||
|
||||
Returns:
|
||||
str: Texte nettoyé
|
||||
"""
|
||||
if not html_content:
|
||||
return "*Contenu vide*"
|
||||
|
||||
# 0. PRÉVENIR LES DOUBLONS - Détecter et supprimer les messages dupliqués
|
||||
# Cette étape permet d'éliminer les messages qui apparaissent en double
|
||||
|
||||
# D'abord, nettoyer le HTML pour comparer les sections de texte réel
|
||||
cleaned_for_comparison = pre_clean_html(html_content)
|
||||
|
||||
# Détection des doublons basée sur les premières lignes
|
||||
# Si le même début apparaît deux fois, ne garder que jusqu'à la première occurrence
|
||||
first_paragraph = ""
|
||||
for line in cleaned_for_comparison.split('\n'):
|
||||
if len(line.strip()) > 10: # Ignorer les lignes vides ou trop courtes
|
||||
first_paragraph = line.strip()
|
||||
break
|
||||
|
||||
if first_paragraph and first_paragraph in cleaned_for_comparison[len(first_paragraph):]:
|
||||
# Le premier paragraphe apparaît deux fois - couper au début de la deuxième occurrence
|
||||
pos = cleaned_for_comparison.find(first_paragraph, len(first_paragraph))
|
||||
if pos > 0:
|
||||
# Utiliser cette position pour couper le contenu original
|
||||
html_content = html_content[:pos].strip()
|
||||
|
||||
# Diviser le contenu en sections potentielles (souvent séparées par des lignes vides doubles)
|
||||
sections = re.split(r'\n\s*\n\s*\n', html_content)
|
||||
|
||||
# Si le contenu a plusieurs sections, ne garder que la première section significative
|
||||
if len(sections) > 1:
|
||||
# Rechercher la première section qui contient du texte significatif (non des en-têtes/métadonnées)
|
||||
significant_content = ""
|
||||
for section in sections:
|
||||
# Ignorer les sections très courtes ou qui ressemblent à des en-têtes
|
||||
if len(section.strip()) > 50 and not re.search(r'^(?:Subject|Date|From|To|Cc|Objet|De|À|Copie à):', section, re.IGNORECASE):
|
||||
significant_content = section
|
||||
break
|
||||
|
||||
# Si on a trouvé une section significative, l'utiliser comme contenu
|
||||
if significant_content:
|
||||
html_content = significant_content
|
||||
|
||||
# 1. CAS SPÉCIAUX - Traités en premier avec leurs propres règles
|
||||
|
||||
# 1.1. Traitement spécifique pour les descriptions
|
||||
if is_description:
|
||||
# Suppression complète des balises HTML de base
|
||||
content = pre_clean_html(html_content)
|
||||
content = re.sub(r'\n\s*\n', '\n\n', content)
|
||||
return content.strip()
|
||||
|
||||
# 1.2. Traitement des messages transférés avec un pattern spécifique
|
||||
if "\\-------- Message transféré --------" in html_content or "-------- Courriel original --------" in html_content:
|
||||
# Essayer d'extraire le contenu principal du message transféré
|
||||
match = re.search(r'(?:De|From|Copie à|Cc)\s*:.*?\n\s*\n(.*?)(?=\n\s*(?:__+|--+|==+|\\\\|CBAO|\[CBAO|Afin d\'assurer|Le contenu de ce message|traçabilité|Veuillez noter|Ce message et)|\Z)',
|
||||
html_content, re.DOTALL | re.IGNORECASE)
|
||||
if match:
|
||||
return match.group(1).strip()
|
||||
else:
|
||||
# Essayer une autre approche si la première échoue
|
||||
match = re.search(r'Bonjour.*?(?=\n\s*(?:__+|--+|==+|\\\\|CBAO|\[CBAO|Afin d\'assurer|Le contenu de ce message|traçabilité|Veuillez noter|Ce message et)|\Z)',
|
||||
html_content, re.DOTALL)
|
||||
if match:
|
||||
return match.group(0).strip()
|
||||
|
||||
# 1.3. Traitement des notifications d'appel
|
||||
if "Notification d'appel" in html_content:
|
||||
match = re.search(r'(?:Sujet d\'appel:[^\n]*\n[^\n]*\n[^\n]*\n[^\n]*\n)[^\n]*\n[^\n]*([^|]+)', html_content, re.DOTALL)
|
||||
if match:
|
||||
message_content = match.group(1).strip()
|
||||
# Construire un message formaté avec les informations essentielles
|
||||
infos = {}
|
||||
date_match = re.search(r'Date:.*?\|(.*?)(?:\n|$)', html_content)
|
||||
appelant_match = re.search(r'\*\*Appel de:\*\*.*?\|(.*?)(?:\n|$)', html_content)
|
||||
telephone_match = re.search(r'Téléphone principal:.*?\|(.*?)(?:\n|$)', html_content)
|
||||
mobile_match = re.search(r'Mobile:.*?\|(.*?)(?:\n|$)', html_content)
|
||||
sujet_match = re.search(r'Sujet d\'appel:.*?\|(.*?)(?:\n|$)', html_content)
|
||||
|
||||
if date_match:
|
||||
infos["date"] = date_match.group(1).strip()
|
||||
if appelant_match:
|
||||
infos["appelant"] = appelant_match.group(1).strip()
|
||||
if telephone_match:
|
||||
infos["telephone"] = telephone_match.group(1).strip()
|
||||
if mobile_match:
|
||||
infos["mobile"] = mobile_match.group(1).strip()
|
||||
if sujet_match:
|
||||
infos["sujet"] = sujet_match.group(1).strip()
|
||||
|
||||
# Construire le message formaté
|
||||
formatted_message = f"**Notification d'appel**\n\n"
|
||||
if "appelant" in infos:
|
||||
formatted_message += f"De: {infos['appelant']}\n"
|
||||
if "date" in infos:
|
||||
formatted_message += f"Date: {infos['date']}\n"
|
||||
if "telephone" in infos:
|
||||
formatted_message += f"Téléphone: {infos['telephone']}\n"
|
||||
if "mobile" in infos:
|
||||
formatted_message += f"Mobile: {infos['mobile']}\n"
|
||||
if "sujet" in infos:
|
||||
formatted_message += f"Sujet: {infos['sujet']}\n\n"
|
||||
|
||||
formatted_message += f"Message: {message_content}"
|
||||
|
||||
return formatted_message
|
||||
|
||||
# 2. NOUVELLE APPROCHE SIMPLE - Filtrer les lignes problématiques
|
||||
|
||||
# 2.1. D'abord nettoyer le HTML
|
||||
cleaned_content = pre_clean_html(html_content)
|
||||
|
||||
# 2.2. Diviser en lignes et filtrer les lignes problématiques
|
||||
filtered_lines = []
|
||||
|
||||
# Liste modifiée - moins restrictive pour les informations de contact
|
||||
problematic_indicators = [
|
||||
"!/web/image/", # Garder celui-ci car c'est spécifique aux images embarquées
|
||||
"[CBAO - développeur de rentabilité", # Signature standard à filtrer
|
||||
"Afin d'assurer une meilleure traçabilité" # Début de disclaimer standard
|
||||
]
|
||||
|
||||
# Mémoriser l'indice de la ligne contenant "Cordialement" ou équivalent
|
||||
signature_line_idx = -1
|
||||
|
||||
lines = cleaned_content.split('\n')
|
||||
for i, line in enumerate(lines):
|
||||
# Détecter la signature
|
||||
if any(sig in line.lower() for sig in ["cordialement", "cdlt", "bien à vous", "salutation"]):
|
||||
signature_line_idx = i
|
||||
|
||||
# Vérifier si la ligne contient un indicateur problématique
|
||||
is_problematic = any(indicator in line for indicator in problematic_indicators)
|
||||
|
||||
# Si la ligne est très longue (plus de 500 caractères), la considérer comme problématique
|
||||
if len(line) > 500:
|
||||
is_problematic = True
|
||||
|
||||
# Ajouter la ligne seulement si elle n'est pas problématique
|
||||
if not is_problematic:
|
||||
filtered_lines.append(line)
|
||||
|
||||
# 2.3. Si on a trouvé une signature, ne garder que 2 lignes après maximum
|
||||
if signature_line_idx >= 0:
|
||||
# Suppression de la limitation à 2 lignes après la signature
|
||||
# Gardons toutes les lignes après la signature si ce sont des informations techniques
|
||||
# Ce commentaire est laissé intentionnellement pour référence historique
|
||||
pass
|
||||
# filtered_lines = filtered_lines[:min(signature_line_idx + 3, len(filtered_lines))]
|
||||
|
||||
# 2.4. Recombiner les lignes filtrées
|
||||
content = '\n'.join(filtered_lines)
|
||||
|
||||
# 2.5. Nettoyer les espaces et lignes vides
|
||||
content = re.sub(r'\n{3,}', '\n\n', content)
|
||||
content = content.strip()
|
||||
|
||||
# 2.6. VÉRIFICATION FINALE: S'assurer qu'il n'y a pas de duplication dans le contenu final
|
||||
# Si le même paragraphe apparaît deux fois, ne garder que jusqu'à la première occurrence
|
||||
lines = content.split('\n')
|
||||
unique_lines = []
|
||||
seen_paragraphs = set()
|
||||
|
||||
for line in lines:
|
||||
clean_line = line.strip()
|
||||
# Ne traiter que les lignes non vides et assez longues pour être significatives
|
||||
if clean_line and len(clean_line) > 10:
|
||||
if clean_line in seen_paragraphs:
|
||||
# On a déjà vu cette ligne, c'est probablement une duplication
|
||||
# Arrêter le traitement ici
|
||||
break
|
||||
seen_paragraphs.add(clean_line)
|
||||
unique_lines.append(line)
|
||||
|
||||
content = '\n'.join(unique_lines)
|
||||
|
||||
# Résultat final
|
||||
if not content or len(content.strip()) < 10:
|
||||
return "*Contenu non extractible*"
|
||||
|
||||
return content
|
||||
|
||||
def pre_clean_html(html_content):
|
||||
"""
|
||||
Effectue un nettoyage préliminaire du HTML en préservant la structure et le formatage basique.
|
||||
"""
|
||||
# Remplacer les balises de paragraphe et saut de ligne par des sauts de ligne
|
||||
content = re.sub(r'<br\s*/?>|<p[^>]*>|</p>|<div[^>]*>|</div>', '\n', html_content)
|
||||
|
||||
# Préserver le formatage de base (gras, italique, etc.)
|
||||
content = re.sub(r'<(?:b|strong)>(.*?)</(?:b|strong)>', r'**\1**', content)
|
||||
content = re.sub(r'<(?:i|em)>(.*?)</(?:i|em)>', r'*\1*', content)
|
||||
|
||||
# Transformer les listes
|
||||
content = re.sub(r'<li>(.*?)</li>', r'- \1\n', content)
|
||||
|
||||
# Supprimer les balises HTML avec leurs attributs mais conserver le contenu
|
||||
content = re.sub(r'<[^>]+>', '', content)
|
||||
|
||||
# Remplacer les entités HTML courantes
|
||||
content = content.replace(' ', ' ')
|
||||
content = content.replace('<', '<')
|
||||
content = content.replace('>', '>')
|
||||
content = content.replace('&', '&')
|
||||
content = content.replace('"', '"')
|
||||
|
||||
# Nettoyer les espaces multiples
|
||||
content = re.sub(r' {2,}', ' ', content)
|
||||
|
||||
# Nettoyer les sauts de ligne multiples (mais pas tous, pour préserver la structure)
|
||||
content = re.sub(r'\n{3,}', '\n\n', content)
|
||||
|
||||
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,324 +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
|
||||
|
||||
def clean_html(html_content, is_description=False):
|
||||
"""
|
||||
Nettoie le contenu HTML pour le Markdown en identifiant et ignorant les parties problématiques.
|
||||
|
||||
Args:
|
||||
html_content (str): Contenu HTML à nettoyer
|
||||
is_description (bool): Indique si le contenu est une description de ticket
|
||||
|
||||
Returns:
|
||||
str: Texte nettoyé
|
||||
"""
|
||||
if not html_content:
|
||||
return "*Contenu vide*"
|
||||
|
||||
# 0. PRÉVENIR LES DOUBLONS - Détecter et supprimer les messages dupliqués
|
||||
# Cette étape permet d'éliminer les messages qui apparaissent en double
|
||||
|
||||
# D'abord, nettoyer le HTML pour comparer les sections de texte réel
|
||||
cleaned_for_comparison = pre_clean_html(html_content)
|
||||
|
||||
# Détection des doublons basée sur les premières lignes
|
||||
# Si le même début apparaît deux fois, ne garder que jusqu'à la première occurrence
|
||||
first_paragraph = ""
|
||||
for line in cleaned_for_comparison.split('\n'):
|
||||
if len(line.strip()) > 10: # Ignorer les lignes vides ou trop courtes
|
||||
first_paragraph = line.strip()
|
||||
break
|
||||
|
||||
if first_paragraph and first_paragraph in cleaned_for_comparison[len(first_paragraph):]:
|
||||
# Le premier paragraphe apparaît deux fois - couper au début de la deuxième occurrence
|
||||
pos = cleaned_for_comparison.find(first_paragraph, len(first_paragraph))
|
||||
if pos > 0:
|
||||
# Utiliser cette position pour couper le contenu original
|
||||
html_content = html_content[:pos].strip()
|
||||
|
||||
# Diviser le contenu en sections potentielles (souvent séparées par des lignes vides doubles)
|
||||
sections = re.split(r'\n\s*\n\s*\n', html_content)
|
||||
|
||||
# Si le contenu a plusieurs sections, ne garder que la première section significative
|
||||
if len(sections) > 1:
|
||||
# Rechercher la première section qui contient du texte significatif (non des en-têtes/métadonnées)
|
||||
significant_content = ""
|
||||
for section in sections:
|
||||
# Ignorer les sections très courtes ou qui ressemblent à des en-têtes
|
||||
if len(section.strip()) > 50 and not re.search(r'^(?:Subject|Date|From|To|Cc|Objet|De|À|Copie à):', section, re.IGNORECASE):
|
||||
significant_content = section
|
||||
break
|
||||
|
||||
# Si on a trouvé une section significative, l'utiliser comme contenu
|
||||
if significant_content:
|
||||
html_content = significant_content
|
||||
|
||||
# 1. CAS SPÉCIAUX - Traités en premier avec leurs propres règles
|
||||
|
||||
# 1.1. Traitement spécifique pour les descriptions
|
||||
if is_description:
|
||||
# Suppression complète des balises HTML de base
|
||||
content = pre_clean_html(html_content)
|
||||
content = re.sub(r'\n\s*\n', '\n\n', content)
|
||||
return content.strip()
|
||||
|
||||
# 1.2. Traitement des messages transférés avec un pattern spécifique
|
||||
if "\\-------- Message transféré --------" in html_content or "-------- Courriel original --------" in html_content:
|
||||
# Essayer d'extraire le contenu principal du message transféré
|
||||
match = re.search(r'(?:De|From|Copie à|Cc)\s*:.*?\n\s*\n(.*?)(?=\n\s*(?:__+|--+|==+|\\\\|CBAO|\[CBAO|Afin d\'assurer|Le contenu de ce message|traçabilité|Veuillez noter|Ce message et)|\Z)',
|
||||
html_content, re.DOTALL | re.IGNORECASE)
|
||||
if match:
|
||||
return match.group(1).strip()
|
||||
else:
|
||||
# Essayer une autre approche si la première échoue
|
||||
match = re.search(r'Bonjour.*?(?=\n\s*(?:__+|--+|==+|\\\\|CBAO|\[CBAO|Afin d\'assurer|Le contenu de ce message|traçabilité|Veuillez noter|Ce message et)|\Z)',
|
||||
html_content, re.DOTALL)
|
||||
if match:
|
||||
return match.group(0).strip()
|
||||
|
||||
# 1.3. Traitement des notifications d'appel
|
||||
if "Notification d'appel" in html_content:
|
||||
match = re.search(r'(?:Sujet d\'appel:[^\n]*\n[^\n]*\n[^\n]*\n[^\n]*\n)[^\n]*\n[^\n]*([^|]+)', html_content, re.DOTALL)
|
||||
if match:
|
||||
message_content = match.group(1).strip()
|
||||
# Construire un message formaté avec les informations essentielles
|
||||
infos = {}
|
||||
date_match = re.search(r'Date:.*?\|(.*?)(?:\n|$)', html_content)
|
||||
appelant_match = re.search(r'\*\*Appel de:\*\*.*?\|(.*?)(?:\n|$)', html_content)
|
||||
telephone_match = re.search(r'Téléphone principal:.*?\|(.*?)(?:\n|$)', html_content)
|
||||
mobile_match = re.search(r'Mobile:.*?\|(.*?)(?:\n|$)', html_content)
|
||||
sujet_match = re.search(r'Sujet d\'appel:.*?\|(.*?)(?:\n|$)', html_content)
|
||||
|
||||
if date_match:
|
||||
infos["date"] = date_match.group(1).strip()
|
||||
if appelant_match:
|
||||
infos["appelant"] = appelant_match.group(1).strip()
|
||||
if telephone_match:
|
||||
infos["telephone"] = telephone_match.group(1).strip()
|
||||
if mobile_match:
|
||||
infos["mobile"] = mobile_match.group(1).strip()
|
||||
if sujet_match:
|
||||
infos["sujet"] = sujet_match.group(1).strip()
|
||||
|
||||
# Construire le message formaté
|
||||
formatted_message = f"**Notification d'appel**\n\n"
|
||||
if "appelant" in infos:
|
||||
formatted_message += f"De: {infos['appelant']}\n"
|
||||
if "date" in infos:
|
||||
formatted_message += f"Date: {infos['date']}\n"
|
||||
if "telephone" in infos:
|
||||
formatted_message += f"Téléphone: {infos['telephone']}\n"
|
||||
if "mobile" in infos:
|
||||
formatted_message += f"Mobile: {infos['mobile']}\n"
|
||||
if "sujet" in infos:
|
||||
formatted_message += f"Sujet: {infos['sujet']}\n\n"
|
||||
|
||||
formatted_message += f"Message: {message_content}"
|
||||
|
||||
return formatted_message
|
||||
|
||||
# 2. NOUVELLE APPROCHE SIMPLE - Filtrer les lignes problématiques
|
||||
|
||||
# 2.1. D'abord nettoyer le HTML
|
||||
cleaned_content = pre_clean_html(html_content)
|
||||
|
||||
# 2.2. Diviser en lignes et filtrer les lignes problématiques
|
||||
filtered_lines = []
|
||||
|
||||
# Liste des indicateurs de lignes problématiques
|
||||
problematic_indicators = [
|
||||
"http://", "https://", ".fr", ".com", "@",
|
||||
"[", "]", "!/web/image/"
|
||||
]
|
||||
|
||||
# Mémoriser l'indice de la ligne contenant "Cordialement" ou équivalent
|
||||
signature_line_idx = -1
|
||||
|
||||
lines = cleaned_content.split('\n')
|
||||
for i, line in enumerate(lines):
|
||||
# Détecter la signature
|
||||
if any(sig in line.lower() for sig in ["cordialement", "cdlt", "bien à vous", "salutation"]):
|
||||
signature_line_idx = i
|
||||
|
||||
# Vérifier si la ligne contient un indicateur problématique
|
||||
is_problematic = any(indicator in line for indicator in problematic_indicators)
|
||||
|
||||
# Si la ligne est très longue (plus de 200 caractères), la considérer comme problématique
|
||||
if len(line) > 200:
|
||||
is_problematic = True
|
||||
|
||||
# Ajouter la ligne seulement si elle n'est pas problématique
|
||||
if not is_problematic:
|
||||
filtered_lines.append(line)
|
||||
|
||||
# 2.3. Si on a trouvé une signature, ne garder que 2 lignes après maximum
|
||||
if signature_line_idx >= 0:
|
||||
filtered_lines = filtered_lines[:min(signature_line_idx + 3, len(filtered_lines))]
|
||||
|
||||
# 2.4. Recombiner les lignes filtrées
|
||||
content = '\n'.join(filtered_lines)
|
||||
|
||||
# 2.5. Nettoyer les espaces et lignes vides
|
||||
content = re.sub(r'\n{3,}', '\n\n', content)
|
||||
content = content.strip()
|
||||
|
||||
# 2.6. VÉRIFICATION FINALE: S'assurer qu'il n'y a pas de duplication dans le contenu final
|
||||
# Si le même paragraphe apparaît deux fois, ne garder que jusqu'à la première occurrence
|
||||
lines = content.split('\n')
|
||||
unique_lines = []
|
||||
seen_paragraphs = set()
|
||||
|
||||
for line in lines:
|
||||
clean_line = line.strip()
|
||||
# Ne traiter que les lignes non vides et assez longues pour être significatives
|
||||
if clean_line and len(clean_line) > 10:
|
||||
if clean_line in seen_paragraphs:
|
||||
# On a déjà vu cette ligne, c'est probablement une duplication
|
||||
# Arrêter le traitement ici
|
||||
break
|
||||
seen_paragraphs.add(clean_line)
|
||||
unique_lines.append(line)
|
||||
|
||||
content = '\n'.join(unique_lines)
|
||||
|
||||
# Résultat final
|
||||
if not content or len(content.strip()) < 10:
|
||||
return "*Contenu non extractible*"
|
||||
|
||||
return content
|
||||
|
||||
def pre_clean_html(html_content):
|
||||
"""
|
||||
Effectue un nettoyage préliminaire du HTML en préservant la structure et le formatage basique.
|
||||
"""
|
||||
# Remplacer les balises de paragraphe et saut de ligne par des sauts de ligne
|
||||
content = re.sub(r'<br\s*/?>|<p[^>]*>|</p>|<div[^>]*>|</div>', '\n', html_content)
|
||||
|
||||
# Préserver le formatage de base (gras, italique, etc.)
|
||||
content = re.sub(r'<(?:b|strong)>(.*?)</(?:b|strong)>', r'**\1**', content)
|
||||
content = re.sub(r'<(?:i|em)>(.*?)</(?:i|em)>', r'*\1*', content)
|
||||
|
||||
# Transformer les listes
|
||||
content = re.sub(r'<li>(.*?)</li>', r'- \1\n', content)
|
||||
|
||||
# Supprimer les balises HTML avec leurs attributs mais conserver le contenu
|
||||
content = re.sub(r'<[^>]+>', '', content)
|
||||
|
||||
# Remplacer les entités HTML courantes
|
||||
content = content.replace(' ', ' ')
|
||||
content = content.replace('<', '<')
|
||||
content = content.replace('>', '>')
|
||||
content = content.replace('&', '&')
|
||||
content = content.replace('"', '"')
|
||||
|
||||
# Nettoyer les espaces multiples
|
||||
content = re.sub(r' {2,}', ' ', content)
|
||||
|
||||
# Nettoyer les sauts de ligne multiples (mais pas tous, pour préserver la structure)
|
||||
content = re.sub(r'\n{3,}', '\n\n', content)
|
||||
|
||||
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,429 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script pour convertir les fichiers JSON de tickets en Markdown formaté.
|
||||
Ce script prend les données JSON des tickets extraits et crée un fichier Markdown structuré.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
import html
|
||||
import subprocess
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
# Import direct de clean_html depuis le même répertoire
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from formatters.clean_html import formatters.clean_html, format_date
|
||||
|
||||
def clean_newlines(text):
|
||||
"""
|
||||
Nettoie les sauts de ligne excessifs dans le texte.
|
||||
|
||||
Args:
|
||||
text: Texte à nettoyer
|
||||
|
||||
Returns:
|
||||
Texte avec sauts de ligne normalisés
|
||||
"""
|
||||
if not text:
|
||||
return text
|
||||
|
||||
# Étape 1: Normaliser tous les sauts de ligne
|
||||
text = text.replace("\r\n", "\n").replace("\r", "\n")
|
||||
|
||||
# Étape 2: Supprimer les lignes vides consécutives (plus de 2 sauts de ligne)
|
||||
text = re.sub(r'\n{3,}', '\n\n', text)
|
||||
|
||||
# Étape 3: Supprimer les espaces en début et fin de chaque ligne
|
||||
lines = text.split('\n')
|
||||
cleaned_lines = [line.strip() for line in lines]
|
||||
|
||||
# Étape 4: Supprimer les lignes qui ne contiennent que des espaces ou des caractères de mise en forme
|
||||
meaningful_lines = []
|
||||
for line in cleaned_lines:
|
||||
# Ignorer les lignes qui ne contiennent que des caractères spéciaux de mise en forme
|
||||
if line and not re.match(r'^[\s_\-=\.]+$', line):
|
||||
meaningful_lines.append(line)
|
||||
elif line: # Si c'est une ligne de séparation, la garder mais la normaliser
|
||||
if re.match(r'^_{3,}$', line): # Ligne de tirets bas
|
||||
meaningful_lines.append("___")
|
||||
elif re.match(r'^-{3,}$', line): # Ligne de tirets
|
||||
meaningful_lines.append("---")
|
||||
elif re.match(r'^={3,}$', line): # Ligne d'égal
|
||||
meaningful_lines.append("===")
|
||||
else:
|
||||
meaningful_lines.append(line)
|
||||
|
||||
# Recombiner les lignes
|
||||
return '\n'.join(meaningful_lines)
|
||||
|
||||
def create_markdown_from_json(json_file, output_file):
|
||||
"""
|
||||
Crée un fichier Markdown à partir d'un fichier JSON de messages.
|
||||
|
||||
Args:
|
||||
json_file: Chemin vers le fichier JSON contenant les messages
|
||||
output_file: Chemin du fichier Markdown à créer
|
||||
"""
|
||||
# Obtenir le répertoire du ticket pour accéder aux autres fichiers
|
||||
ticket_dir = os.path.dirname(json_file)
|
||||
|
||||
ticket_summary = {}
|
||||
try:
|
||||
with open(json_file, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
ticket_summary = data.get("ticket_summary", {})
|
||||
except Exception as e:
|
||||
print(f"Erreur : {e}")
|
||||
return False
|
||||
|
||||
ticket_code = ticket_summary.get("code", "inconnu")
|
||||
|
||||
# Créer le dossier rapports si il n'existe pas
|
||||
reports_dir = os.path.join(ticket_dir, f"{ticket_code}_rapports")
|
||||
os.makedirs(reports_dir, exist_ok=True)
|
||||
|
||||
output_file = os.path.join(reports_dir, f"{ticket_code}_rapport.md")
|
||||
json_output_file = os.path.join(reports_dir, f"{ticket_code}_rapport.json")
|
||||
|
||||
# Essayer de lire le fichier ticket_info.json si disponible
|
||||
ticket_info = {}
|
||||
ticket_info_path = os.path.join(ticket_dir, "ticket_info.json")
|
||||
if os.path.exists(ticket_info_path):
|
||||
try:
|
||||
with open(ticket_info_path, 'r', encoding='utf-8') as f:
|
||||
ticket_info = json.load(f)
|
||||
except Exception as e:
|
||||
print(f"Avertissement: Impossible de lire ticket_info.json: {e}")
|
||||
|
||||
# Récupérer les informations du sommaire du ticket
|
||||
ticket_summary = {}
|
||||
if "ticket_summary" in data:
|
||||
ticket_summary = data.get("ticket_summary", {})
|
||||
else:
|
||||
summary_path = os.path.join(ticket_dir, "ticket_summary.json")
|
||||
if os.path.exists(summary_path):
|
||||
try:
|
||||
with open(summary_path, 'r', encoding='utf-8') as f:
|
||||
ticket_summary = json.load(f)
|
||||
except Exception as e:
|
||||
print(f"Avertissement: Impossible de lire ticket_summary.json: {e}")
|
||||
|
||||
# Tenter de lire le fichier structure.json
|
||||
structure = {}
|
||||
structure_path = os.path.join(ticket_dir, "structure.json")
|
||||
if os.path.exists(structure_path):
|
||||
try:
|
||||
with open(structure_path, 'r', encoding='utf-8') as f:
|
||||
structure = json.load(f)
|
||||
except Exception as e:
|
||||
print(f"Avertissement: Impossible de lire structure.json: {e}")
|
||||
|
||||
# Commencer à construire le contenu Markdown
|
||||
md_content = []
|
||||
|
||||
# Ajouter l'en-tête du document avec les informations du ticket
|
||||
ticket_code = ticket_summary.get("code", os.path.basename(ticket_dir).split('_')[0])
|
||||
ticket_name = ticket_summary.get("name", "")
|
||||
|
||||
md_content.append(f"# Ticket {ticket_code}: {ticket_name}")
|
||||
md_content.append("")
|
||||
|
||||
# Ajouter des métadonnées du ticket
|
||||
md_content.append("## Informations du ticket")
|
||||
md_content.append("")
|
||||
# Ajouter l'ID du ticket
|
||||
ticket_id = ticket_summary.get("id", ticket_info.get("id", ""))
|
||||
md_content.append(f"- **id**: {ticket_id}")
|
||||
md_content.append(f"- **code**: {ticket_code}")
|
||||
md_content.append(f"- **name**: {ticket_name}")
|
||||
md_content.append(f"- **project_name**: {ticket_summary.get('project_name', '')}")
|
||||
md_content.append(f"- **stage_name**: {ticket_summary.get('stage_name', '')}")
|
||||
|
||||
# Chercher l'utilisateur assigné dans les métadonnées
|
||||
assigned_to = ""
|
||||
if "user_id" in structure and structure["user_id"]:
|
||||
user_id = structure["user_id"]
|
||||
if isinstance(user_id, list) and len(user_id) > 1:
|
||||
assigned_to = user_id[1]
|
||||
|
||||
md_content.append(f"- **user_id**: {assigned_to}")
|
||||
|
||||
# Ajouter le client si disponible
|
||||
partner = ""
|
||||
if "partner_id" in ticket_info:
|
||||
partner_id = ticket_info.get("partner_id", [])
|
||||
if isinstance(partner_id, list) and len(partner_id) > 1:
|
||||
partner = partner_id[1]
|
||||
|
||||
# Ajouter l'email du client si disponible
|
||||
partner_email = ""
|
||||
if "email_from" in ticket_info and ticket_info["email_from"]:
|
||||
partner_email = ticket_info["email_from"]
|
||||
if partner:
|
||||
partner += f", {partner_email}"
|
||||
else:
|
||||
partner = partner_email
|
||||
|
||||
md_content.append(f"- **partner_id/email_from**: {partner}")
|
||||
|
||||
# Ajouter les tags s'ils sont disponibles
|
||||
tags = []
|
||||
if "tag_ids" in ticket_info:
|
||||
tag_ids = ticket_info.get("tag_ids", []) or []
|
||||
for tag in tag_ids:
|
||||
if isinstance(tag, list) and len(tag) > 1:
|
||||
tags.append(tag[1])
|
||||
|
||||
if tags:
|
||||
md_content.append(f"- **tag_ids**: {', '.join(tags)}")
|
||||
|
||||
# Ajouter les dates
|
||||
md_content.append(f"- **create_date**: {format_date(ticket_info.get('create_date', ''))}")
|
||||
md_content.append(f"- **write_date/last modification**: {format_date(ticket_info.get('write_date', ''))}")
|
||||
if "date_deadline" in ticket_info and ticket_info.get("date_deadline"):
|
||||
md_content.append(f"- **date_deadline**: {format_date(ticket_info.get('date_deadline', ''))}")
|
||||
|
||||
md_content.append("")
|
||||
|
||||
# Ajouter la description du ticket
|
||||
description = ticket_info.get("description", "")
|
||||
md_content.append(f"- **description**:")
|
||||
md_content.append("") # saut de ligne
|
||||
|
||||
if description:
|
||||
cleaned_description = clean_html(description, is_description=True)
|
||||
if cleaned_description and cleaned_description != "*Contenu vide*":
|
||||
cleaned_description = html.unescape(cleaned_description)
|
||||
md_content.append(cleaned_description)
|
||||
else:
|
||||
md_content.append("*Aucune description fournie*")
|
||||
else:
|
||||
md_content.append("*Aucune description fournie*")
|
||||
md_content.append("") # saut de ligne
|
||||
|
||||
# Ajouter les messages
|
||||
messages = []
|
||||
if "messages" in data:
|
||||
messages = data.get("messages", [])
|
||||
|
||||
if not messages:
|
||||
md_content.append("## Messages")
|
||||
md_content.append("")
|
||||
md_content.append("*Aucun message disponible*")
|
||||
else:
|
||||
# Filtrer les messages système non pertinents
|
||||
filtered_messages = []
|
||||
for msg in messages:
|
||||
# Ignorer les messages système vides
|
||||
if msg.get("is_system", False) and not msg.get("body", "").strip():
|
||||
continue
|
||||
|
||||
# Ignorer les changements d'état sans contenu
|
||||
if msg.get("is_stage_change", False) and not msg.get("body", "").strip():
|
||||
# Sauf si on veut les garder pour la traçabilité
|
||||
filtered_messages.append(msg)
|
||||
continue
|
||||
|
||||
filtered_messages.append(msg)
|
||||
|
||||
# Si nous avons au moins un message significatif
|
||||
if filtered_messages:
|
||||
md_content.append("## Messages")
|
||||
md_content.append("")
|
||||
|
||||
# Trier les messages par date
|
||||
filtered_messages.sort(key=lambda x: x.get("date", ""))
|
||||
|
||||
for i, message in enumerate(filtered_messages):
|
||||
if not isinstance(message, dict):
|
||||
continue
|
||||
|
||||
# Déterminer l'auteur du message
|
||||
author = "Système"
|
||||
author_details = message.get("author_details", {})
|
||||
if author_details and author_details.get("name"):
|
||||
author = author_details.get("name")
|
||||
else:
|
||||
author_id = message.get("author_id", [])
|
||||
if isinstance(author_id, list) and len(author_id) > 1:
|
||||
author = author_id[1]
|
||||
|
||||
# Formater la date
|
||||
date = format_date(message.get("date", ""))
|
||||
|
||||
# Récupérer le corps du message, en privilégiant body_original (HTML) si disponible
|
||||
if "body_original" in message and message["body_original"]:
|
||||
body = message["body_original"]
|
||||
# Nettoyer le corps HTML avec clean_html
|
||||
cleaned_body = clean_html(body, is_description=False)
|
||||
else:
|
||||
# Utiliser body directement (déjà en texte/markdown) sans passer par clean_html
|
||||
body = message.get("body", "")
|
||||
cleaned_body = body # Pas besoin de nettoyer car déjà en texte brut
|
||||
|
||||
# Déterminer le type de message
|
||||
message_type = ""
|
||||
if message.get("is_stage_change", False):
|
||||
message_type = "Changement d'état"
|
||||
elif message.get("is_system", False):
|
||||
message_type = "Système"
|
||||
elif message.get("is_note", False):
|
||||
message_type = "Commentaire"
|
||||
elif message.get("email_from", False):
|
||||
message_type = "E-mail"
|
||||
|
||||
# Récupérer le sujet du message
|
||||
subject = message.get("subject", "")
|
||||
|
||||
# Créer l'en-tête du message
|
||||
md_content.append(f"### Message {i+1}")
|
||||
md_content.append(f"**author_id**: {author}")
|
||||
md_content.append(f"**date**: {date}")
|
||||
md_content.append(f"**message_type**: {message_type}")
|
||||
if subject:
|
||||
md_content.append(f"**subject**: {subject}")
|
||||
|
||||
# Ajouter l'ID du message si disponible
|
||||
message_id = message.get("id", "")
|
||||
if message_id:
|
||||
md_content.append(f"**id**: {message_id}")
|
||||
|
||||
# Ajouter le corps nettoyé du message
|
||||
if cleaned_body:
|
||||
cleaned_body = clean_newlines(cleaned_body)
|
||||
md_content.append(cleaned_body)
|
||||
else:
|
||||
md_content.append("*Contenu vide*")
|
||||
|
||||
# Ajouter les pièces jointes si elles existent
|
||||
attachment_ids = message.get("attachment_ids", [])
|
||||
has_attachments = False
|
||||
|
||||
# Vérifier si les pièces jointes existent et ne sont pas vides
|
||||
if attachment_ids:
|
||||
# Récupérer les informations des pièces jointes
|
||||
valid_attachments = []
|
||||
if isinstance(attachment_ids, list) and all(isinstance(id, int) for id in attachment_ids):
|
||||
# Chercher les informations des pièces jointes dans attachments_info.json
|
||||
attachments_info_path = os.path.join(ticket_dir, "attachments_info.json")
|
||||
if os.path.exists(attachments_info_path):
|
||||
try:
|
||||
with open(attachments_info_path, 'r', encoding='utf-8') as f:
|
||||
attachments_info = json.load(f)
|
||||
for attachment_id in attachment_ids:
|
||||
for attachment_info in attachments_info:
|
||||
if attachment_info.get("id") == attachment_id:
|
||||
valid_attachments.append(attachment_info)
|
||||
except Exception as e:
|
||||
print(f"Avertissement: Impossible de lire attachments_info.json: {e}")
|
||||
elif isinstance(attachment_ids, list):
|
||||
for att in attachment_ids:
|
||||
if isinstance(att, list) and len(att) > 1:
|
||||
valid_attachments.append(att)
|
||||
|
||||
if valid_attachments:
|
||||
has_attachments = True
|
||||
md_content.append("")
|
||||
md_content.append("**attachment_ids**:")
|
||||
for att in valid_attachments:
|
||||
if isinstance(att, list) and len(att) > 1:
|
||||
md_content.append(f"- {att[1]}")
|
||||
elif isinstance(att, dict):
|
||||
att_id = att.get("id", "")
|
||||
name = att.get("name", "Pièce jointe sans nom")
|
||||
mimetype = att.get("mimetype", "Type inconnu")
|
||||
md_content.append(f"- {name} ({mimetype}) [ID: {att_id}]")
|
||||
|
||||
md_content.append("")
|
||||
md_content.append("---")
|
||||
md_content.append("")
|
||||
|
||||
# Ajouter une section pour les pièces jointes du ticket si elles existent
|
||||
attachment_data = {}
|
||||
attachment_path = os.path.join(ticket_dir, "attachments.json")
|
||||
if os.path.exists(attachment_path):
|
||||
try:
|
||||
with open(attachment_path, 'r', encoding='utf-8') as f:
|
||||
attachment_data = json.load(f)
|
||||
except Exception as e:
|
||||
print(f"Avertissement: Impossible de lire attachments.json: {e}")
|
||||
|
||||
if attachment_data and "attachments" in attachment_data:
|
||||
attachments = attachment_data.get("attachments", [])
|
||||
if attachments:
|
||||
md_content.append("## Pièces jointes")
|
||||
md_content.append("")
|
||||
md_content.append("| Nom | Type | Taille | Date |")
|
||||
md_content.append("|-----|------|--------|------|")
|
||||
|
||||
for att in attachments:
|
||||
name = att.get("name", "")
|
||||
mimetype = att.get("mimetype", "")
|
||||
file_size = att.get("file_size", 0)
|
||||
size_str = f"{file_size / 1024:.1f} KB" if file_size else ""
|
||||
create_date = format_date(att.get("create_date", ""))
|
||||
|
||||
md_content.append(f"| {name} | {mimetype} | {size_str} | {create_date} |")
|
||||
|
||||
md_content.append("")
|
||||
|
||||
# Ajouter des informations sur l'extraction
|
||||
extract_time = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
|
||||
md_content.append("## Informations sur l'extraction")
|
||||
md_content.append("")
|
||||
md_content.append(f"- **Date d'extraction**: {extract_time}")
|
||||
md_content.append(f"- **Répertoire**: {ticket_dir}")
|
||||
|
||||
# Écrire le contenu dans le fichier de sortie
|
||||
try:
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
f.write("\n".join(md_content))
|
||||
print(f"Rapport Markdown créé : {output_file}")
|
||||
# Appeler le script markdown_to_json.py
|
||||
subprocess.run(['python', 'utils/markdown_to_json.py', output_file, json_output_file], check=True)
|
||||
print(f"Fichier JSON créé : {json_output_file}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Erreur lors de l'écriture du fichier Markdown: {e}")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Convertir les fichiers JSON de tickets en Markdown")
|
||||
parser.add_argument("--ticket_code", "-t", help="Code du ticket à convertir (ex: T11067)")
|
||||
parser.add_argument("--date_dir", "-d", help="Dossier spécifique par date, optionnel (ex: 20250403_155134)")
|
||||
parser.add_argument("--input_dir", "-i", default="output", help="Dossier racine contenant les tickets")
|
||||
parser.add_argument("--output_name", "-o", default="rapport.md", help="Nom du fichier Markdown à générer")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.ticket_code:
|
||||
print("Erreur : Vous devez spécifier un code de ticket. Exemple : -t T11067")
|
||||
sys.exit(1)
|
||||
|
||||
# Construire le chemin d'entrée
|
||||
ticket_dir = f"{args.input_dir}/ticket_{args.ticket_code}"
|
||||
|
||||
if args.date_dir:
|
||||
ticket_dir = f"{ticket_dir}/{args.ticket_code}_{args.date_dir}"
|
||||
else:
|
||||
# Trouver le dossier le plus récent
|
||||
import glob
|
||||
date_dirs = glob.glob(f"{ticket_dir}/{args.ticket_code}_*")
|
||||
if date_dirs:
|
||||
ticket_dir = max(date_dirs) # Prend le plus récent par ordre alphabétique
|
||||
|
||||
json_file = f"{ticket_dir}/all_messages.json"
|
||||
|
||||
|
||||
if not os.path.exists(json_file):
|
||||
print(f"Erreur : Le fichier {json_file} n'existe pas.")
|
||||
sys.exit(1)
|
||||
|
||||
if create_markdown_from_json(json_file, None):
|
||||
print(f"Rapport Markdown créé.")
|
||||
else:
|
||||
print("Échec de la création du rapport Markdown")
|
||||
sys.exit(1)
|
||||
@ -1,385 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script pour convertir les fichiers JSON de tickets en Markdown formaté.
|
||||
Ce script prend les données JSON des tickets extraits et crée un fichier Markdown structuré.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
import html
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
|
||||
# Import direct de clean_html depuis le même répertoire
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from clean_html import clean_html, format_date
|
||||
|
||||
def create_markdown_from_json(json_file, output_file):
|
||||
"""
|
||||
Crée un fichier Markdown à partir d'un fichier JSON de messages.
|
||||
|
||||
Args:
|
||||
json_file: Chemin vers le fichier JSON contenant les messages
|
||||
output_file: Chemin du fichier Markdown à créer
|
||||
"""
|
||||
# Obtenir le répertoire du ticket pour accéder aux autres fichiers
|
||||
ticket_dir = os.path.dirname(json_file)
|
||||
|
||||
ticket_summary = {}
|
||||
try:
|
||||
with open(json_file, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
ticket_summary = data.get("ticket_summary", {})
|
||||
except Exception as e:
|
||||
print(f"Erreur : {e}")
|
||||
return False
|
||||
|
||||
ticket_code = ticket_summary.get("code", "inconnu")
|
||||
|
||||
# Créer le dossier rapports si il n'existe pas
|
||||
reports_dir = os.path.join(ticket_dir, f"{ticket_code}_rapports")
|
||||
os.makedirs(reports_dir, exist_ok=True)
|
||||
|
||||
output_file = os.path.join(reports_dir, f"{ticket_code}_rapport.md")
|
||||
json_output_file = os.path.join(reports_dir, f"{ticket_code}_rapport.json")
|
||||
|
||||
# Essayer de lire le fichier ticket_info.json si disponible
|
||||
ticket_info = {}
|
||||
ticket_info_path = os.path.join(ticket_dir, "ticket_info.json")
|
||||
if os.path.exists(ticket_info_path):
|
||||
try:
|
||||
with open(ticket_info_path, 'r', encoding='utf-8') as f:
|
||||
ticket_info = json.load(f)
|
||||
except Exception as e:
|
||||
print(f"Avertissement: Impossible de lire ticket_info.json: {e}")
|
||||
|
||||
# Récupérer les informations du sommaire du ticket
|
||||
ticket_summary = {}
|
||||
if "ticket_summary" in data:
|
||||
ticket_summary = data.get("ticket_summary", {})
|
||||
else:
|
||||
summary_path = os.path.join(ticket_dir, "ticket_summary.json")
|
||||
if os.path.exists(summary_path):
|
||||
try:
|
||||
with open(summary_path, 'r', encoding='utf-8') as f:
|
||||
ticket_summary = json.load(f)
|
||||
except Exception as e:
|
||||
print(f"Avertissement: Impossible de lire ticket_summary.json: {e}")
|
||||
|
||||
# Tenter de lire le fichier structure.json
|
||||
structure = {}
|
||||
structure_path = os.path.join(ticket_dir, "structure.json")
|
||||
if os.path.exists(structure_path):
|
||||
try:
|
||||
with open(structure_path, 'r', encoding='utf-8') as f:
|
||||
structure = json.load(f)
|
||||
except Exception as e:
|
||||
print(f"Avertissement: Impossible de lire structure.json: {e}")
|
||||
|
||||
# Commencer à construire le contenu Markdown
|
||||
md_content = []
|
||||
|
||||
# Ajouter l'en-tête du document avec les informations du ticket
|
||||
ticket_code = ticket_summary.get("code", os.path.basename(ticket_dir).split('_')[0])
|
||||
ticket_name = ticket_summary.get("name", "")
|
||||
|
||||
md_content.append(f"# Ticket {ticket_code}: {ticket_name}")
|
||||
md_content.append("")
|
||||
|
||||
# Ajouter des métadonnées du ticket
|
||||
md_content.append("## Informations du ticket")
|
||||
md_content.append("")
|
||||
# Ajouter l'ID du ticket
|
||||
ticket_id = ticket_summary.get("id", ticket_info.get("id", ""))
|
||||
md_content.append(f"- **id**: {ticket_id}")
|
||||
md_content.append(f"- **code**: {ticket_code}")
|
||||
md_content.append(f"- **name**: {ticket_name}")
|
||||
md_content.append(f"- **project_name**: {ticket_summary.get('project_name', '')}")
|
||||
md_content.append(f"- **stage_name**: {ticket_summary.get('stage_name', '')}")
|
||||
|
||||
# Chercher l'utilisateur assigné dans les métadonnées
|
||||
assigned_to = ""
|
||||
if "user_id" in structure and structure["user_id"]:
|
||||
user_id = structure["user_id"]
|
||||
if isinstance(user_id, list) and len(user_id) > 1:
|
||||
assigned_to = user_id[1]
|
||||
|
||||
md_content.append(f"- **user_id**: {assigned_to}")
|
||||
|
||||
# Ajouter le client si disponible
|
||||
partner = ""
|
||||
if "partner_id" in ticket_info:
|
||||
partner_id = ticket_info.get("partner_id", [])
|
||||
if isinstance(partner_id, list) and len(partner_id) > 1:
|
||||
partner = partner_id[1]
|
||||
|
||||
# Ajouter l'email du client si disponible
|
||||
partner_email = ""
|
||||
if "email_from" in ticket_info and ticket_info["email_from"]:
|
||||
partner_email = ticket_info["email_from"]
|
||||
if partner:
|
||||
partner += f", {partner_email}"
|
||||
else:
|
||||
partner = partner_email
|
||||
|
||||
md_content.append(f"- **partner_id/email_from**: {partner}")
|
||||
|
||||
# Ajouter les tags s'ils sont disponibles
|
||||
tags = []
|
||||
if "tag_ids" in ticket_info:
|
||||
tag_ids = ticket_info.get("tag_ids", []) or []
|
||||
for tag in tag_ids:
|
||||
if isinstance(tag, list) and len(tag) > 1:
|
||||
tags.append(tag[1])
|
||||
|
||||
if tags:
|
||||
md_content.append(f"- **tag_ids**: {', '.join(tags)}")
|
||||
|
||||
# Ajouter les dates
|
||||
md_content.append(f"- **create_date**: {format_date(ticket_info.get('create_date', ''))}")
|
||||
md_content.append(f"- **write_date/last modification**: {format_date(ticket_info.get('write_date', ''))}")
|
||||
if "date_deadline" in ticket_info and ticket_info.get("date_deadline"):
|
||||
md_content.append(f"- **date_deadline**: {format_date(ticket_info.get('date_deadline', ''))}")
|
||||
|
||||
md_content.append("")
|
||||
|
||||
# Ajouter la description du ticket
|
||||
description = ticket_info.get("description", "")
|
||||
md_content.append(f"- **description**:")
|
||||
md_content.append("") # saut de ligne
|
||||
|
||||
if description:
|
||||
cleaned_description = clean_html(description, is_description=True)
|
||||
if cleaned_description and cleaned_description != "*Contenu vide*":
|
||||
cleaned_description = html.unescape(cleaned_description)
|
||||
md_content.append(cleaned_description)
|
||||
else:
|
||||
md_content.append("*Aucune description fournie*")
|
||||
else:
|
||||
md_content.append("*Aucune description fournie*")
|
||||
md_content.append("") # saut de ligne
|
||||
|
||||
# Ajouter les messages
|
||||
messages = []
|
||||
if "messages" in data:
|
||||
messages = data.get("messages", [])
|
||||
|
||||
if not messages:
|
||||
md_content.append("## Messages")
|
||||
md_content.append("")
|
||||
md_content.append("*Aucun message disponible*")
|
||||
else:
|
||||
# Filtrer les messages système non pertinents
|
||||
filtered_messages = []
|
||||
for msg in messages:
|
||||
# Ignorer les messages système vides
|
||||
if msg.get("is_system", False) and not msg.get("body", "").strip():
|
||||
continue
|
||||
|
||||
# Ignorer les changements d'état sans contenu
|
||||
if msg.get("is_stage_change", False) and not msg.get("body", "").strip():
|
||||
# Sauf si on veut les garder pour la traçabilité
|
||||
filtered_messages.append(msg)
|
||||
continue
|
||||
|
||||
filtered_messages.append(msg)
|
||||
|
||||
# Si nous avons au moins un message significatif
|
||||
if filtered_messages:
|
||||
md_content.append("## Messages")
|
||||
md_content.append("")
|
||||
|
||||
# Trier les messages par date
|
||||
filtered_messages.sort(key=lambda x: x.get("date", ""))
|
||||
|
||||
for i, message in enumerate(filtered_messages):
|
||||
if not isinstance(message, dict):
|
||||
continue
|
||||
|
||||
# Déterminer l'auteur du message
|
||||
author = "Système"
|
||||
author_details = message.get("author_details", {})
|
||||
if author_details and author_details.get("name"):
|
||||
author = author_details.get("name")
|
||||
else:
|
||||
author_id = message.get("author_id", [])
|
||||
if isinstance(author_id, list) and len(author_id) > 1:
|
||||
author = author_id[1]
|
||||
|
||||
# Formater la date
|
||||
date = format_date(message.get("date", ""))
|
||||
|
||||
# Récupérer le corps du message, en privilégiant body_original (HTML) si disponible
|
||||
if "body_original" in message and message["body_original"]:
|
||||
body = message["body_original"]
|
||||
# Nettoyer le corps HTML avec clean_html
|
||||
cleaned_body = clean_html(body, is_description=False)
|
||||
else:
|
||||
# Utiliser body directement (déjà en texte/markdown) sans passer par clean_html
|
||||
body = message.get("body", "")
|
||||
cleaned_body = body # Pas besoin de nettoyer car déjà en texte brut
|
||||
|
||||
# Déterminer le type de message
|
||||
message_type = ""
|
||||
if message.get("is_stage_change", False):
|
||||
message_type = "Changement d'état"
|
||||
elif message.get("is_system", False):
|
||||
message_type = "Système"
|
||||
elif message.get("is_note", False):
|
||||
message_type = "Commentaire"
|
||||
elif message.get("email_from", False):
|
||||
message_type = "E-mail"
|
||||
|
||||
# Récupérer le sujet du message
|
||||
subject = message.get("subject", "")
|
||||
|
||||
# Créer l'en-tête du message
|
||||
md_content.append(f"### Message {i+1}")
|
||||
md_content.append(f"**author_id**: {author}")
|
||||
md_content.append(f"**date**: {date}")
|
||||
md_content.append(f"**message_type**: {message_type}")
|
||||
if subject:
|
||||
md_content.append(f"**subject**: {subject}")
|
||||
|
||||
# Ajouter l'ID du message si disponible
|
||||
message_id = message.get("id", "")
|
||||
if message_id:
|
||||
md_content.append(f"**id**: {message_id}")
|
||||
|
||||
# Ajouter le corps nettoyé du message
|
||||
if cleaned_body:
|
||||
md_content.append(cleaned_body)
|
||||
else:
|
||||
md_content.append("*Contenu vide*")
|
||||
|
||||
# Ajouter les pièces jointes si elles existent
|
||||
attachment_ids = message.get("attachment_ids", [])
|
||||
has_attachments = False
|
||||
|
||||
# Vérifier si les pièces jointes existent et ne sont pas vides
|
||||
if attachment_ids:
|
||||
# Récupérer les informations des pièces jointes
|
||||
valid_attachments = []
|
||||
if isinstance(attachment_ids, list) and all(isinstance(id, int) for id in attachment_ids):
|
||||
# Chercher les informations des pièces jointes dans attachments_info.json
|
||||
attachments_info_path = os.path.join(ticket_dir, "attachments_info.json")
|
||||
if os.path.exists(attachments_info_path):
|
||||
try:
|
||||
with open(attachments_info_path, 'r', encoding='utf-8') as f:
|
||||
attachments_info = json.load(f)
|
||||
for attachment_id in attachment_ids:
|
||||
for attachment_info in attachments_info:
|
||||
if attachment_info.get("id") == attachment_id:
|
||||
valid_attachments.append(attachment_info)
|
||||
except Exception as e:
|
||||
print(f"Avertissement: Impossible de lire attachments_info.json: {e}")
|
||||
elif isinstance(attachment_ids, list):
|
||||
for att in attachment_ids:
|
||||
if isinstance(att, list) and len(att) > 1:
|
||||
valid_attachments.append(att)
|
||||
|
||||
if valid_attachments:
|
||||
has_attachments = True
|
||||
md_content.append("")
|
||||
md_content.append("**attachment_ids**:")
|
||||
for att in valid_attachments:
|
||||
if isinstance(att, list) and len(att) > 1:
|
||||
md_content.append(f"- {att[1]}")
|
||||
elif isinstance(att, dict):
|
||||
att_id = att.get("id", "")
|
||||
name = att.get("name", "Pièce jointe sans nom")
|
||||
mimetype = att.get("mimetype", "Type inconnu")
|
||||
md_content.append(f"- {name} ({mimetype}) [ID: {att_id}]")
|
||||
|
||||
md_content.append("")
|
||||
md_content.append("---")
|
||||
md_content.append("")
|
||||
|
||||
# Ajouter une section pour les pièces jointes du ticket si elles existent
|
||||
attachment_data = {}
|
||||
attachment_path = os.path.join(ticket_dir, "attachments.json")
|
||||
if os.path.exists(attachment_path):
|
||||
try:
|
||||
with open(attachment_path, 'r', encoding='utf-8') as f:
|
||||
attachment_data = json.load(f)
|
||||
except Exception as e:
|
||||
print(f"Avertissement: Impossible de lire attachments.json: {e}")
|
||||
|
||||
if attachment_data and "attachments" in attachment_data:
|
||||
attachments = attachment_data.get("attachments", [])
|
||||
if attachments:
|
||||
md_content.append("## Pièces jointes")
|
||||
md_content.append("")
|
||||
md_content.append("| Nom | Type | Taille | Date |")
|
||||
md_content.append("|-----|------|--------|------|")
|
||||
|
||||
for att in attachments:
|
||||
name = att.get("name", "")
|
||||
mimetype = att.get("mimetype", "")
|
||||
file_size = att.get("file_size", 0)
|
||||
size_str = f"{file_size / 1024:.1f} KB" if file_size else ""
|
||||
create_date = format_date(att.get("create_date", ""))
|
||||
|
||||
md_content.append(f"| {name} | {mimetype} | {size_str} | {create_date} |")
|
||||
|
||||
md_content.append("")
|
||||
|
||||
# Ajouter des informations sur l'extraction
|
||||
extract_time = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
|
||||
md_content.append("## Informations sur l'extraction")
|
||||
md_content.append("")
|
||||
md_content.append(f"- **Date d'extraction**: {extract_time}")
|
||||
md_content.append(f"- **Répertoire**: {ticket_dir}")
|
||||
|
||||
# Écrire le contenu dans le fichier de sortie
|
||||
try:
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
f.write("\n".join(md_content))
|
||||
print(f"Rapport Markdown créé : {output_file}")
|
||||
# Appeler le script markdown_to_json.py
|
||||
subprocess.run(['python', 'utils/markdown_to_json.py', output_file, json_output_file], check=True)
|
||||
print(f"Fichier JSON créé : {json_output_file}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Erreur lors de l'écriture du fichier Markdown: {e}")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Convertir les fichiers JSON de tickets en Markdown")
|
||||
parser.add_argument("--ticket_code", "-t", help="Code du ticket à convertir (ex: T11067)")
|
||||
parser.add_argument("--date_dir", "-d", help="Dossier spécifique par date, optionnel (ex: 20250403_155134)")
|
||||
parser.add_argument("--input_dir", "-i", default="output", help="Dossier racine contenant les tickets")
|
||||
parser.add_argument("--output_name", "-o", default="rapport.md", help="Nom du fichier Markdown à générer")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.ticket_code:
|
||||
print("Erreur : Vous devez spécifier un code de ticket. Exemple : -t T11067")
|
||||
sys.exit(1)
|
||||
|
||||
# Construire le chemin d'entrée
|
||||
ticket_dir = f"{args.input_dir}/ticket_{args.ticket_code}"
|
||||
|
||||
if args.date_dir:
|
||||
ticket_dir = f"{ticket_dir}/{args.ticket_code}_{args.date_dir}"
|
||||
else:
|
||||
# Trouver le dossier le plus récent
|
||||
import glob
|
||||
date_dirs = glob.glob(f"{ticket_dir}/{args.ticket_code}_*")
|
||||
if date_dirs:
|
||||
ticket_dir = max(date_dirs) # Prend le plus récent par ordre alphabétique
|
||||
|
||||
json_file = f"{ticket_dir}/all_messages.json"
|
||||
|
||||
|
||||
if not os.path.exists(json_file):
|
||||
print(f"Erreur : Le fichier {json_file} n'existe pas.")
|
||||
sys.exit(1)
|
||||
|
||||
if create_markdown_from_json(json_file, None):
|
||||
print(f"Rapport Markdown créé.")
|
||||
else:
|
||||
print("Échec de la création du rapport Markdown")
|
||||
sys.exit(1)
|
||||
@ -1,161 +0,0 @@
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import sys
|
||||
|
||||
def parse_markdown(md_content):
|
||||
data = {}
|
||||
|
||||
# Diviser le contenu en sections
|
||||
sections = re.split(r"\n## ", md_content)
|
||||
|
||||
# Traiter chaque section
|
||||
for section in sections:
|
||||
if section.startswith("Informations du ticket"):
|
||||
ticket_info = parse_ticket_info(section)
|
||||
data.update(ticket_info)
|
||||
elif section.startswith("Messages"):
|
||||
messages = parse_messages(section)
|
||||
data["messages"] = messages
|
||||
elif section.startswith("Informations sur l'extraction"):
|
||||
extraction_info = parse_extraction_info(section)
|
||||
data.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 data:
|
||||
ordered_data[field] = data[field]
|
||||
|
||||
# Ensuite ajouter les autres champs
|
||||
for key, value in data.items():
|
||||
if key not in ordered_data:
|
||||
ordered_data[key] = value
|
||||
|
||||
return ordered_data
|
||||
|
||||
def parse_ticket_info(section):
|
||||
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 parse_messages(section):
|
||||
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)
|
||||
|
||||
return messages
|
||||
|
||||
|
||||
def parse_extraction_info(section):
|
||||
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
|
||||
|
||||
|
||||
def convert_markdown_to_json(md_file_path, output_file_path):
|
||||
with open(md_file_path, 'r', encoding='utf-8') as f:
|
||||
md_content = f.read()
|
||||
|
||||
data = parse_markdown(md_content)
|
||||
|
||||
# S'assurer que la description est présente
|
||||
if "description" not in data:
|
||||
# Trouver l'index de "name" pour insérer la description après
|
||||
if "name" in data:
|
||||
ordered_data = {}
|
||||
for key, value in data.items():
|
||||
ordered_data[key] = value
|
||||
if key == "name":
|
||||
ordered_data["description"] = ""
|
||||
data = ordered_data
|
||||
else:
|
||||
data["description"] = ""
|
||||
|
||||
with open(output_file_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, indent=4, ensure_ascii=False)
|
||||
|
||||
print(f"Conversion terminée. Fichier JSON créé : {output_file_path}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 3:
|
||||
print("Utilisation : python markdown_to_json.py <fichier_markdown.md> <fichier_sortie.json>")
|
||||
sys.exit(1)
|
||||
|
||||
md_file = sys.argv[1]
|
||||
output_file = sys.argv[2]
|
||||
|
||||
convert_markdown_to_json(md_file, output_file)
|
||||
@ -1,445 +0,0 @@
|
||||
from typing import List, Dict, Any, Optional, Tuple
|
||||
from .auth_manager import AuthManager
|
||||
from .utils import formatters.clean_html, 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)
|
||||
@ -1,31 +0,0 @@
|
||||
Rappel dépôt: https://github.com/Ladebeze66/llm_ticket3 branch "update"
|
||||
dossier uils/ fichiers gestion des ticket
|
||||
racine retrieve_ticket.py
|
||||
|
||||
Garder en tête le problème des 102-103
|
||||
Avancer sur l'intégrations classes et agents pour commencer analyse des tickets
|
||||
dans les sections:
|
||||
{
|
||||
"id": 17388,
|
||||
"body": "",
|
||||
"date": "2020-09-15 09:40:23",
|
||||
"author_id": [
|
||||
10288,
|
||||
"CBAO S.A.R.L., Youness BENDEQ"
|
||||
],
|
||||
"email_from": "\"Youness BENDEQ\" <youness@cbao.fr>",
|
||||
"message_type": "notification",
|
||||
"parent_id": [
|
||||
12480,
|
||||
"[T0282] DEMANDE DE RENSEIGNEMENTS"
|
||||
],
|
||||
"subtype_id": [
|
||||
19,
|
||||
"Stage Changed"
|
||||
],
|
||||
"subject": false,
|
||||
"tracking_value_ids": [
|
||||
8422
|
||||
],
|
||||
voir récupération tacking_value_ids pour les notifications de changement qui peuvent contenir des informations importantes
|
||||
|
||||
@ -1,447 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Module pour formater les rapports à partir des fichiers JSON générés par l'AgentReportGenerator.
|
||||
|
||||
Ce module prend en entrée un fichier JSON contenant les analyses et génère différents
|
||||
formats de sortie (Markdown, HTML, etc.) sans utiliser de LLM.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import argparse
|
||||
import sys
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
|
||||
def generate_markdown_report(json_path: str, output_path: Optional[str] = None) -> Tuple[bool, str]:
|
||||
"""
|
||||
Génère un rapport au format Markdown à partir d'un fichier JSON.
|
||||
|
||||
Args:
|
||||
json_path: Chemin vers le fichier JSON contenant les données du rapport
|
||||
output_path: Chemin de sortie pour le fichier Markdown (facultatif)
|
||||
|
||||
Returns:
|
||||
Tuple (succès, chemin du fichier généré ou message d'erreur)
|
||||
"""
|
||||
try:
|
||||
# Lire le fichier JSON
|
||||
with open(json_path, "r", encoding="utf-8") as f:
|
||||
rapport_data = json.load(f)
|
||||
|
||||
# Si le chemin de sortie n'est pas spécifié, le créer à partir du chemin d'entrée
|
||||
if not output_path:
|
||||
# Remplacer l'extension JSON par MD
|
||||
output_path = os.path.splitext(json_path)[0] + ".md"
|
||||
|
||||
# Générer le contenu Markdown
|
||||
markdown_content = _generate_markdown_content(rapport_data)
|
||||
|
||||
# Écrire le contenu dans le fichier de sortie
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
f.write(markdown_content)
|
||||
|
||||
print(f"Rapport Markdown généré avec succès: {output_path}")
|
||||
return True, output_path
|
||||
|
||||
except Exception as e:
|
||||
error_message = f"Erreur lors de la génération du rapport Markdown: {str(e)}"
|
||||
print(error_message)
|
||||
return False, error_message
|
||||
|
||||
def _generate_markdown_content(rapport_data: Dict) -> str:
|
||||
"""
|
||||
Génère le contenu Markdown à partir des données du rapport.
|
||||
|
||||
Args:
|
||||
rapport_data: Dictionnaire contenant les données du rapport
|
||||
|
||||
Returns:
|
||||
Contenu Markdown
|
||||
"""
|
||||
ticket_id = rapport_data.get("ticket_id", "")
|
||||
timestamp = rapport_data.get("metadata", {}).get("timestamp", "")
|
||||
generation_date = rapport_data.get("metadata", {}).get("generation_date", datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
||||
|
||||
# Entête du document
|
||||
markdown = f"# Rapport d'analyse du ticket #{ticket_id}\n\n"
|
||||
markdown += f"*Généré le: {generation_date}*\n\n"
|
||||
|
||||
# 1. Résumé exécutif
|
||||
if "resume" in rapport_data and rapport_data["resume"]:
|
||||
markdown += rapport_data["resume"] + "\n\n"
|
||||
|
||||
# 2. Chronologie des échanges (tableau)
|
||||
markdown += "## Chronologie des échanges\n\n"
|
||||
|
||||
if "chronologie_echanges" in rapport_data and rapport_data["chronologie_echanges"]:
|
||||
# Créer un tableau pour les échanges
|
||||
markdown += "| Date | Émetteur | Type | Contenu | Statut |\n"
|
||||
markdown += "|------|---------|------|---------|--------|\n"
|
||||
|
||||
# Prétraitement pour détecter les questions sans réponse
|
||||
questions_sans_reponse = {}
|
||||
echanges = rapport_data["chronologie_echanges"]
|
||||
|
||||
for i, echange in enumerate(echanges):
|
||||
if echange.get("type", "").lower() == "question" and echange.get("emetteur", "").lower() == "client":
|
||||
has_response = False
|
||||
# Vérifier si la question a une réponse
|
||||
for j in range(i+1, len(echanges)):
|
||||
next_echange = echanges[j]
|
||||
if next_echange.get("type", "").lower() == "réponse" and next_echange.get("emetteur", "").lower() == "support":
|
||||
has_response = True
|
||||
break
|
||||
questions_sans_reponse[i] = not has_response
|
||||
|
||||
# Générer les lignes du tableau
|
||||
for i, echange in enumerate(echanges):
|
||||
date = echange.get("date", "-")
|
||||
emetteur = echange.get("emetteur", "-")
|
||||
type_msg = echange.get("type", "-")
|
||||
contenu = echange.get("contenu", "-")
|
||||
|
||||
# Ajouter un statut pour les questions sans réponse
|
||||
statut = ""
|
||||
if emetteur.lower() == "client" and type_msg.lower() == "question" and questions_sans_reponse.get(i, False):
|
||||
statut = "**Sans réponse**"
|
||||
|
||||
markdown += f"| {date} | {emetteur} | {type_msg} | {contenu} | {statut} |\n"
|
||||
|
||||
# Ajouter une note si aucune réponse du support n'a été trouvée
|
||||
if not any(echange.get("emetteur", "").lower() == "support" for echange in echanges):
|
||||
markdown += "\n**Note: Aucune réponse du support n'a été trouvée dans ce ticket.**\n\n"
|
||||
else:
|
||||
markdown += "*Aucun échange détecté dans le ticket.*\n\n"
|
||||
|
||||
# 3. Analyse des images
|
||||
markdown += "## Analyse des images\n\n"
|
||||
|
||||
if "images_analyses" in rapport_data and rapport_data["images_analyses"]:
|
||||
images_list = rapport_data["images_analyses"]
|
||||
|
||||
if not images_list:
|
||||
markdown += "*Aucune image pertinente n'a été identifiée.*\n\n"
|
||||
else:
|
||||
for i, img_data in enumerate(images_list, 1):
|
||||
image_name = img_data.get("image_name", f"Image {i}")
|
||||
sorting_info = img_data.get("sorting_info", {})
|
||||
reason = sorting_info.get("reason", "Non spécifiée")
|
||||
|
||||
markdown += f"### Image {i}: {image_name}\n\n"
|
||||
|
||||
# Raison de la pertinence
|
||||
if reason:
|
||||
markdown += f"**Raison de la pertinence**: {reason}\n\n"
|
||||
|
||||
# Ajouter l'analyse détaillée dans une section dépliable
|
||||
analyse_detail = img_data.get("analyse", "Aucune analyse disponible")
|
||||
if analyse_detail:
|
||||
markdown += "<details>\n<summary>Analyse détaillée de l'image</summary>\n\n"
|
||||
markdown += "```\n" + analyse_detail + "\n```\n\n"
|
||||
markdown += "</details>\n\n"
|
||||
else:
|
||||
markdown += "*Aucune image pertinente n'a été analysée.*\n\n"
|
||||
|
||||
# 4. Diagnostic technique
|
||||
if "diagnostic" in rapport_data and rapport_data["diagnostic"]:
|
||||
markdown += "## Diagnostic technique\n\n"
|
||||
markdown += rapport_data["diagnostic"] + "\n\n"
|
||||
|
||||
# Tableau récapitulatif des échanges (nouveau)
|
||||
if "tableau_questions_reponses" in rapport_data and rapport_data["tableau_questions_reponses"]:
|
||||
markdown += rapport_data["tableau_questions_reponses"] + "\n\n"
|
||||
|
||||
# Section séparatrice
|
||||
markdown += "---\n\n"
|
||||
|
||||
# Détails des analyses effectuées
|
||||
markdown += "# Détails des analyses effectuées\n\n"
|
||||
markdown += "## Processus d'analyse\n\n"
|
||||
|
||||
# 1. Analyse de ticket
|
||||
ticket_analyse = rapport_data.get("ticket_analyse", "")
|
||||
if ticket_analyse:
|
||||
markdown += "### Étape 1: Analyse du ticket\n\n"
|
||||
markdown += "L'agent d'analyse de ticket a extrait les informations suivantes du ticket d'origine:\n\n"
|
||||
markdown += "<details>\n<summary>Cliquez pour voir l'analyse complète du ticket</summary>\n\n"
|
||||
markdown += "```\n" + str(ticket_analyse) + "\n```\n\n"
|
||||
markdown += "</details>\n\n"
|
||||
else:
|
||||
markdown += "### Étape 1: Analyse du ticket\n\n"
|
||||
markdown += "*Aucune analyse de ticket disponible*\n\n"
|
||||
|
||||
# 2. Tri des images
|
||||
markdown += "### Étape 2: Tri des images\n\n"
|
||||
markdown += "L'agent de tri d'images a évalué chaque image pour déterminer sa pertinence par rapport au problème client:\n\n"
|
||||
|
||||
# Création d'un tableau récapitulatif
|
||||
images_list = rapport_data.get("images_analyses", [])
|
||||
if images_list:
|
||||
markdown += "| Image | Pertinence | Raison |\n"
|
||||
markdown += "|-------|------------|--------|\n"
|
||||
|
||||
for img_data in images_list:
|
||||
image_name = img_data.get("image_name", "Image inconnue")
|
||||
sorting_info = img_data.get("sorting_info", {})
|
||||
is_relevant = "Oui" if sorting_info else "Oui" # Par défaut, si présent dans la liste c'est pertinent
|
||||
reason = sorting_info.get("reason", "Non spécifiée")
|
||||
|
||||
markdown += f"| {image_name} | {is_relevant} | {reason} |\n"
|
||||
|
||||
markdown += "\n"
|
||||
else:
|
||||
markdown += "*Aucune image n'a été triée pour ce ticket.*\n\n"
|
||||
|
||||
# 3. Analyse des images
|
||||
markdown += "### Étape 3: Analyse détaillée des images pertinentes\n\n"
|
||||
|
||||
if images_list:
|
||||
for i, img_data in enumerate(images_list, 1):
|
||||
image_name = img_data.get("image_name", f"Image {i}")
|
||||
analyse_detail = img_data.get("analyse", "Analyse non disponible")
|
||||
|
||||
markdown += f"#### Image pertinente {i}: {image_name}\n\n"
|
||||
markdown += "<details>\n<summary>Cliquez pour voir l'analyse complète de l'image</summary>\n\n"
|
||||
markdown += "```\n" + str(analyse_detail) + "\n```\n\n"
|
||||
markdown += "</details>\n\n"
|
||||
else:
|
||||
markdown += "*Aucune image pertinente n'a été identifiée pour ce ticket.*\n\n"
|
||||
|
||||
# 4. Génération du rapport
|
||||
markdown += "### Étape 4: Génération du rapport de synthèse\n\n"
|
||||
markdown += "L'agent de génération de rapport a synthétisé toutes les analyses précédentes pour produire le rapport ci-dessus.\n\n"
|
||||
|
||||
# Informations techniques et métadonnées
|
||||
markdown += "## Informations techniques\n\n"
|
||||
|
||||
# Statistiques
|
||||
statistiques = rapport_data.get("statistiques", {})
|
||||
metadata = rapport_data.get("metadata", {})
|
||||
|
||||
markdown += "### Statistiques\n\n"
|
||||
markdown += f"- **Images analysées**: {statistiques.get('total_images', 0)}\n"
|
||||
markdown += f"- **Images pertinentes**: {statistiques.get('images_pertinentes', 0)}\n"
|
||||
|
||||
if "generation_time" in statistiques:
|
||||
markdown += f"- **Temps de génération**: {statistiques['generation_time']:.2f} secondes\n"
|
||||
|
||||
# Modèle utilisé
|
||||
markdown += "\n### Modèle LLM utilisé\n\n"
|
||||
markdown += f"- **Modèle**: {metadata.get('model', 'Non spécifié')}\n"
|
||||
|
||||
if "model_version" in metadata:
|
||||
markdown += f"- **Version**: {metadata.get('model_version', 'Non spécifiée')}\n"
|
||||
|
||||
markdown += f"- **Température**: {metadata.get('temperature', 'Non spécifiée')}\n"
|
||||
markdown += f"- **Top_p**: {metadata.get('top_p', 'Non spécifié')}\n"
|
||||
|
||||
# Section sur les agents utilisés
|
||||
if "agents" in metadata:
|
||||
markdown += "\n### Agents impliqués\n\n"
|
||||
|
||||
agents = metadata["agents"]
|
||||
|
||||
# Agent d'analyse de ticket
|
||||
if "json_analyser" in agents:
|
||||
markdown += "#### Agent d'analyse du ticket\n"
|
||||
json_analyser = agents["json_analyser"]
|
||||
if "model_info" in json_analyser:
|
||||
markdown += f"- **Modèle**: {json_analyser['model_info'].get('name', 'Non spécifié')}\n"
|
||||
|
||||
# Agent de tri d'images
|
||||
if "image_sorter" in agents:
|
||||
markdown += "\n#### Agent de tri d'images\n"
|
||||
sorter = agents["image_sorter"]
|
||||
# Récupérer directement le modèle ou via model_info selon la structure
|
||||
if "model" in sorter:
|
||||
markdown += f"- **Modèle**: {sorter.get('model', 'Non spécifié')}\n"
|
||||
markdown += f"- **Température**: {sorter.get('temperature', 'Non spécifiée')}\n"
|
||||
markdown += f"- **Top_p**: {sorter.get('top_p', 'Non spécifié')}\n"
|
||||
elif "model_info" in sorter:
|
||||
markdown += f"- **Modèle**: {sorter['model_info'].get('name', 'Non spécifié')}\n"
|
||||
else:
|
||||
markdown += f"- **Modèle**: Non spécifié\n"
|
||||
|
||||
# Agent d'analyse d'images
|
||||
if "image_analyser" in agents:
|
||||
markdown += "\n#### Agent d'analyse d'images\n"
|
||||
analyser = agents["image_analyser"]
|
||||
# Récupérer directement le modèle ou via model_info selon la structure
|
||||
if "model" in analyser:
|
||||
markdown += f"- **Modèle**: {analyser.get('model', 'Non spécifié')}\n"
|
||||
markdown += f"- **Température**: {analyser.get('temperature', 'Non spécifiée')}\n"
|
||||
markdown += f"- **Top_p**: {analyser.get('top_p', 'Non spécifié')}\n"
|
||||
elif "model_info" in analyser:
|
||||
markdown += f"- **Modèle**: {analyser['model_info'].get('name', 'Non spécifié')}\n"
|
||||
else:
|
||||
markdown += f"- **Modèle**: Non spécifié\n"
|
||||
|
||||
# Ajouter une section pour les prompts s'ils sont présents
|
||||
if "prompts_utilisés" in rapport_data and rapport_data["prompts_utilisés"]:
|
||||
markdown += "\n## Prompts utilisés\n\n"
|
||||
prompts = rapport_data["prompts_utilisés"]
|
||||
|
||||
for agent, prompt in prompts.items():
|
||||
# Si le prompt est trop long, le tronquer pour éviter des rapports trop volumineux
|
||||
if len(prompt) > 2000:
|
||||
debut = prompt[:1000].strip()
|
||||
fin = prompt[-1000:].strip()
|
||||
prompt_tronque = f"{debut}\n\n[...]\n\n{fin}"
|
||||
markdown += f"### Agent: {agent}\n\n```\n{prompt_tronque}\n```\n\n"
|
||||
else:
|
||||
markdown += f"### Agent: {agent}\n\n```\n{prompt}\n```\n\n"
|
||||
|
||||
return markdown
|
||||
|
||||
def generate_html_report(json_path: str, output_path: Optional[str] = None) -> Tuple[bool, str]:
|
||||
"""
|
||||
Génère un rapport au format HTML à partir d'un fichier JSON.
|
||||
|
||||
Args:
|
||||
json_path: Chemin vers le fichier JSON contenant les données du rapport
|
||||
output_path: Chemin de sortie pour le fichier HTML (facultatif)
|
||||
|
||||
Returns:
|
||||
Tuple (succès, chemin du fichier généré ou message d'erreur)
|
||||
"""
|
||||
try:
|
||||
# Générer d'abord le Markdown
|
||||
success, md_path_or_error = generate_markdown_report(json_path, None)
|
||||
|
||||
if not success:
|
||||
return False, md_path_or_error
|
||||
|
||||
# Lire le contenu Markdown
|
||||
with open(md_path_or_error, "r", encoding="utf-8") as f:
|
||||
markdown_content = f.read()
|
||||
|
||||
# Si le chemin de sortie n'est pas spécifié, le créer à partir du chemin d'entrée
|
||||
if not output_path:
|
||||
# Remplacer l'extension JSON par HTML
|
||||
output_path = os.path.splitext(json_path)[0] + ".html"
|
||||
|
||||
# Conversion Markdown → HTML (avec gestion de l'absence de mistune)
|
||||
html_content = _simple_markdown_to_html(markdown_content)
|
||||
|
||||
# Essayer d'utiliser mistune pour une meilleure conversion si disponible
|
||||
try:
|
||||
import mistune
|
||||
markdown = mistune.create_markdown(escape=False)
|
||||
html_content = markdown(markdown_content)
|
||||
print("Conversion HTML effectuée avec mistune")
|
||||
except ImportError:
|
||||
print("Module mistune non disponible, utilisation de la conversion HTML simplifiée")
|
||||
|
||||
# Créer un HTML complet avec un peu de style
|
||||
html_page = f"""<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Rapport d'analyse de ticket</title>
|
||||
<style>
|
||||
body {{ font-family: Arial, sans-serif; line-height: 1.6; margin: 0; padding: 20px; color: #333; max-width: 1200px; margin: 0 auto; }}
|
||||
h1 {{ color: #2c3e50; border-bottom: 2px solid #eee; padding-bottom: 10px; }}
|
||||
h2 {{ color: #3498db; margin-top: 30px; }}
|
||||
h3 {{ color: #2980b9; }}
|
||||
h4 {{ color: #16a085; }}
|
||||
table {{ border-collapse: collapse; width: 100%; margin: 20px 0; }}
|
||||
th, td {{ padding: 12px 15px; text-align: left; border-bottom: 1px solid #ddd; }}
|
||||
th {{ background-color: #f2f2f2; }}
|
||||
tr:hover {{ background-color: #f5f5f5; }}
|
||||
code, pre {{ background: #f8f8f8; border: 1px solid #ddd; border-radius: 3px; padding: 10px; overflow-x: auto; }}
|
||||
details {{ margin: 15px 0; }}
|
||||
summary {{ cursor: pointer; font-weight: bold; color: #2980b9; }}
|
||||
.status {{ color: #e74c3c; font-weight: bold; }}
|
||||
hr {{ border: 0; height: 1px; background: #eee; margin: 30px 0; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{html_content}
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
# Écrire le contenu dans le fichier de sortie
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
f.write(html_page)
|
||||
|
||||
print(f"Rapport HTML généré avec succès: {output_path}")
|
||||
return True, output_path
|
||||
|
||||
except Exception as e:
|
||||
error_message = f"Erreur lors de la génération du rapport HTML: {str(e)}"
|
||||
print(error_message)
|
||||
return False, error_message
|
||||
|
||||
def _simple_markdown_to_html(markdown_content: str) -> str:
|
||||
"""
|
||||
Convertit un contenu Markdown en HTML de façon simplifiée.
|
||||
|
||||
Args:
|
||||
markdown_content: Contenu Markdown à convertir
|
||||
|
||||
Returns:
|
||||
Contenu HTML
|
||||
"""
|
||||
html = markdown_content
|
||||
|
||||
# Titres
|
||||
html = re.sub(r'^# (.*?)$', r'<h1>\1</h1>', html, flags=re.MULTILINE)
|
||||
html = re.sub(r'^## (.*?)$', r'<h2>\1</h2>', html, flags=re.MULTILINE)
|
||||
html = re.sub(r'^### (.*?)$', r'<h3>\1</h3>', html, flags=re.MULTILINE)
|
||||
html = re.sub(r'^#### (.*?)$', r'<h4>\1</h4>', html, flags=re.MULTILINE)
|
||||
|
||||
# Emphase
|
||||
html = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', html)
|
||||
html = re.sub(r'\*(.*?)\*', r'<em>\1</em>', html)
|
||||
|
||||
# Lists
|
||||
html = re.sub(r'^- (.*?)$', r'<li>\1</li>', html, flags=re.MULTILINE)
|
||||
|
||||
# Paragraphes
|
||||
html = re.sub(r'([^\n])\n([^\n])', r'\1<br>\2', html)
|
||||
html = re.sub(r'\n\n', r'</p><p>', html)
|
||||
|
||||
# Tables simplifiées (sans analyser la structure)
|
||||
html = re.sub(r'\| (.*?) \|', r'<td>\1</td>', html)
|
||||
|
||||
# Code blocks
|
||||
html = re.sub(r'```(.*?)```', r'<pre><code>\1</code></pre>', html, flags=re.DOTALL)
|
||||
|
||||
# Envelopper dans des balises paragraphe
|
||||
html = f"<p>{html}</p>"
|
||||
|
||||
return html
|
||||
|
||||
def process_report(json_path: str, output_format: str = "markdown") -> None:
|
||||
"""
|
||||
Traite un rapport dans le format spécifié.
|
||||
|
||||
Args:
|
||||
json_path: Chemin vers le fichier JSON contenant les données du rapport
|
||||
output_format: Format de sortie (markdown ou html)
|
||||
"""
|
||||
if output_format.lower() == "markdown":
|
||||
generate_markdown_report(json_path)
|
||||
elif output_format.lower() == "html":
|
||||
generate_html_report(json_path)
|
||||
else:
|
||||
print(f"Format non supporté: {output_format}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Formateur de rapports à partir de fichiers JSON")
|
||||
parser.add_argument("json_path", help="Chemin vers le fichier JSON contenant les données du rapport")
|
||||
parser.add_argument("--format", "-f", choices=["markdown", "html"], default="markdown",
|
||||
help="Format de sortie (markdown par défaut)")
|
||||
parser.add_argument("--output", "-o", help="Chemin de sortie pour le rapport (facultatif)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.format == "markdown":
|
||||
generate_markdown_report(args.json_path, args.output)
|
||||
elif args.format == "html":
|
||||
generate_html_report(args.json_path, args.output)
|
||||
else:
|
||||
print(f"Format non supporté: {args.format}")
|
||||
@ -1,115 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import logging
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
from odoo.auth_manager import AuthManager
|
||||
from odoo.ticket_manager import TicketManager
|
||||
from core.utils import setup_logging, log_separator
|
||||
|
||||
def parse_arguments():
|
||||
parser = argparse.ArgumentParser(description="Récupère un ticket Odoo par son code et extrait ses données.")
|
||||
parser.add_argument("ticket_code", help="Code du ticket à extraire")
|
||||
parser.add_argument("--output", "-o", help="Répertoire de sortie", default=None)
|
||||
parser.add_argument("--config", "-c", help="Fichier de configuration", default="config.json")
|
||||
parser.add_argument("--verbose", "-v", action="store_true", help="Mode verbeux")
|
||||
return parser.parse_args()
|
||||
|
||||
def load_config(config_file):
|
||||
try:
|
||||
with open(config_file, 'r') as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
logging.error(f"Erreur lors du chargement du fichier de configuration: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
def main():
|
||||
args = parse_arguments()
|
||||
config = load_config(args.config)
|
||||
|
||||
# Configurer la journalisation
|
||||
log_level = logging.DEBUG if args.verbose else logging.INFO
|
||||
setup_logging(log_level, "retrieve_ticket.log")
|
||||
|
||||
# Extraire les informations de connexion
|
||||
odoo_config = config.get("odoo", {})
|
||||
url = odoo_config.get("url")
|
||||
db = odoo_config.get("db")
|
||||
username = odoo_config.get("username")
|
||||
api_key = odoo_config.get("api_key")
|
||||
|
||||
if not all([url, db, username, api_key]):
|
||||
logging.error("Informations de connexion Odoo manquantes dans le fichier de configuration")
|
||||
sys.exit(1)
|
||||
|
||||
# Définir le répertoire de sortie
|
||||
output_dir = args.output or os.path.join(config.get("output_dir", "output"), f"ticket_{args.ticket_code}")
|
||||
|
||||
# Créer le répertoire de sortie spécifique au ticket
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
ticket_dir = os.path.join(output_dir, f"{args.ticket_code}_{timestamp}")
|
||||
os.makedirs(ticket_dir, exist_ok=True)
|
||||
|
||||
logging.info(f"Extraction du ticket {args.ticket_code}")
|
||||
log_separator()
|
||||
|
||||
try:
|
||||
# Initialiser les gestionnaires
|
||||
auth_manager = AuthManager(
|
||||
url=url,
|
||||
db=db,
|
||||
username=username,
|
||||
api_key=api_key
|
||||
)
|
||||
|
||||
if not auth_manager.login():
|
||||
logging.error("Échec de l'authentification à Odoo")
|
||||
sys.exit(1)
|
||||
|
||||
# Extraire les données du ticket
|
||||
ticket_manager = TicketManager(auth_manager)
|
||||
result = ticket_manager.extract_ticket_data(args.ticket_code, ticket_dir)
|
||||
|
||||
if not result:
|
||||
logging.error(f"Échec de l'extraction du ticket {args.ticket_code}")
|
||||
sys.exit(1)
|
||||
|
||||
# Afficher le résumé
|
||||
log_separator()
|
||||
logging.info(f"Extraction terminée avec succès")
|
||||
logging.info(f"Ticket: {args.ticket_code}")
|
||||
logging.info(f"Répertoire: {ticket_dir}")
|
||||
logging.info(f"Messages traités: {result.get('messages_count', 0)}")
|
||||
logging.info(f"Pièces jointes: {result.get('attachments_count', 0)}")
|
||||
log_separator()
|
||||
|
||||
# Générer un rapport de fin
|
||||
summary = {
|
||||
"timestamp": timestamp,
|
||||
"ticket_code": args.ticket_code,
|
||||
"output_directory": ticket_dir,
|
||||
"message_count": result.get("messages_count", 0),
|
||||
"attachment_count": result.get("attachments_count", 0),
|
||||
"files_created": [
|
||||
os.path.basename(result.get("ticket_info", "")),
|
||||
os.path.basename(result.get("ticket_summary", "")),
|
||||
os.path.basename(result.get("messages_file", "")),
|
||||
os.path.basename(result.get("ticket_data_file", ""))
|
||||
]
|
||||
}
|
||||
|
||||
summary_path = os.path.join(ticket_dir, "extraction_summary.json")
|
||||
with open(summary_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(summary, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"\nExtraction du ticket {args.ticket_code} terminée avec succès.")
|
||||
print(f"Les données ont été sauvegardées dans: {ticket_dir}")
|
||||
|
||||
except Exception as e:
|
||||
logging.exception(f"Une erreur est survenue: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -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,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)
|
||||
}
|
||||
@ -1,201 +0,0 @@
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Any, Optional
|
||||
from .auth_manager import AuthManager
|
||||
from .message_manager import MessageManager
|
||||
from .attachment_manager import AttachmentManager
|
||||
from .utils import save_json
|
||||
|
||||
class TicketManager:
|
||||
def __init__(self, auth_manager: AuthManager):
|
||||
self.auth_manager = auth_manager
|
||||
self.message_manager = MessageManager(auth_manager)
|
||||
self.attachment_manager = AttachmentManager(auth_manager)
|
||||
self.model_name = "project.task"
|
||||
|
||||
def get_ticket_by_code(self, ticket_code: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Récupère un ticket par son code.
|
||||
|
||||
Args:
|
||||
ticket_code: Code du ticket à rechercher
|
||||
|
||||
Returns:
|
||||
Dictionnaire contenant les informations du ticket
|
||||
"""
|
||||
params = {
|
||||
"model": self.model_name,
|
||||
"method": "search_read",
|
||||
"args": [[["code", "=", ticket_code]],
|
||||
["id", "name", "description", "stage_id", "project_id", "partner_id",
|
||||
"user_id", "date_start", "date_end", "date_deadline", "create_date", "write_date",
|
||||
"tag_ids", "priority", "email_from", "email_cc", "message_ids",
|
||||
"message_follower_ids", "attachment_ids", "timesheet_ids"]],
|
||||
"kwargs": {"limit": 1}
|
||||
}
|
||||
result = self.auth_manager._rpc_call("/web/dataset/call_kw", params)
|
||||
|
||||
if isinstance(result, list) and len(result) > 0:
|
||||
# Résoudre les champs relationnels
|
||||
return self.resolve_relation_fields(result[0])
|
||||
else:
|
||||
print(f"Aucun ticket trouvé avec le code {ticket_code}")
|
||||
return {}
|
||||
|
||||
def resolve_relation_fields(self, ticket: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Résout les champs relationnels d'un ticket pour obtenir les noms au lieu des IDs.
|
||||
|
||||
Args:
|
||||
ticket: Dictionnaire contenant les données du ticket
|
||||
|
||||
Returns:
|
||||
Ticket avec champs relationnels résolus
|
||||
"""
|
||||
relation_fields = {
|
||||
"stage_id": "res.stage",
|
||||
"project_id": "project.project",
|
||||
"partner_id": "res.partner",
|
||||
"user_id": "res.users",
|
||||
"tag_ids": "project.tags"
|
||||
}
|
||||
|
||||
# Traiter les champs many2one
|
||||
for field, model in relation_fields.items():
|
||||
if field in ticket and ticket[field] and field != "tag_ids":
|
||||
if isinstance(ticket[field], list) and len(ticket[field]) >= 2:
|
||||
# Le format est déjà [id, name]
|
||||
ticket[f"{field}_name"] = ticket[field][1]
|
||||
elif isinstance(ticket[field], int):
|
||||
# Récupérer le nom depuis l'API
|
||||
params = {
|
||||
"model": model,
|
||||
"method": "name_get",
|
||||
"args": [[ticket[field]]],
|
||||
"kwargs": {}
|
||||
}
|
||||
result = self.auth_manager._rpc_call("/web/dataset/call_kw", params)
|
||||
if result and isinstance(result, list) and result[0] and len(result[0]) >= 2:
|
||||
ticket[f"{field}_name"] = result[0][1]
|
||||
|
||||
# Traiter les tags (many2many)
|
||||
if "tag_ids" in ticket and ticket["tag_ids"] and isinstance(ticket["tag_ids"], list):
|
||||
if all(isinstance(tag_id, int) for tag_id in ticket["tag_ids"]):
|
||||
params = {
|
||||
"model": "project.tags",
|
||||
"method": "name_get",
|
||||
"args": [ticket["tag_ids"]],
|
||||
"kwargs": {}
|
||||
}
|
||||
result = self.auth_manager._rpc_call("/web/dataset/call_kw", params)
|
||||
if result and isinstance(result, list):
|
||||
ticket["tag_names"] = [tag[1] for tag in result]
|
||||
|
||||
return ticket
|
||||
|
||||
def extract_ticket_data(self, ticket_code: str, output_dir: str):
|
||||
"""
|
||||
Extrait toutes les données d'un ticket et les sauvegarde dans une structure organisée.
|
||||
|
||||
Args:
|
||||
ticket_code: Code du ticket à extraire
|
||||
output_dir: Répertoire de sortie
|
||||
|
||||
Returns:
|
||||
Dictionnaire avec les chemins des fichiers créés ou None en cas d'erreur
|
||||
"""
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# Récupérer les données du ticket
|
||||
ticket_data = self.get_ticket_by_code(ticket_code)
|
||||
|
||||
if not ticket_data or "id" not in ticket_data:
|
||||
print(f"Erreur: Ticket non trouvé.")
|
||||
return None
|
||||
|
||||
ticket_id = ticket_data["id"]
|
||||
ticket_name = ticket_data.get("name", "Sans nom")
|
||||
|
||||
# Sauvegarder ticket_info.json
|
||||
ticket_info_path = os.path.join(output_dir, "ticket_info.json")
|
||||
save_json(ticket_data, ticket_info_path)
|
||||
|
||||
# Sauvegarder le résumé du ticket
|
||||
ticket_summary = {
|
||||
"id": ticket_id,
|
||||
"code": ticket_code,
|
||||
"name": ticket_name,
|
||||
"description": ticket_data.get("description", ""),
|
||||
"stage": ticket_data.get("stage_id_name", ""),
|
||||
"project": ticket_data.get("project_id_name", ""),
|
||||
"partner": ticket_data.get("partner_id_name", ""),
|
||||
"assigned_to": ticket_data.get("user_id_name", ""),
|
||||
"tags": ticket_data.get("tag_names", []),
|
||||
"create_date": ticket_data.get("create_date", ""),
|
||||
"write_date": ticket_data.get("write_date", ""),
|
||||
"deadline": ticket_data.get("date_deadline", "")
|
||||
}
|
||||
summary_path = os.path.join(output_dir, "ticket_summary.json")
|
||||
save_json(ticket_summary, summary_path)
|
||||
|
||||
# Traiter et sauvegarder les messages
|
||||
messages_result = self.message_manager.process_messages(
|
||||
ticket_id,
|
||||
ticket_code,
|
||||
ticket_name,
|
||||
output_dir
|
||||
)
|
||||
|
||||
# Récupérer et sauvegarder les pièces jointes
|
||||
attachments_info = self.attachment_manager.save_attachments(ticket_id, output_dir)
|
||||
attachments_info_path = os.path.join(output_dir, "attachments_info.json")
|
||||
|
||||
# Récupérer les followers si disponibles
|
||||
follower_ids = ticket_data.get("message_follower_ids", [])
|
||||
followers_path = None
|
||||
if follower_ids:
|
||||
params = {
|
||||
"model": "mail.followers",
|
||||
"method": "read",
|
||||
"args": [follower_ids, ["id", "partner_id", "name", "email"]],
|
||||
"kwargs": {}
|
||||
}
|
||||
followers = self.auth_manager._rpc_call("/web/dataset/call_kw", params)
|
||||
if followers:
|
||||
followers_path = os.path.join(output_dir, "followers.json")
|
||||
save_json(followers, followers_path)
|
||||
|
||||
# Génération de structure.json avec toutes les informations
|
||||
structure = {
|
||||
"date_extraction": datetime.now().isoformat(),
|
||||
"ticket_id": ticket_id,
|
||||
"ticket_code": ticket_code,
|
||||
"ticket_name": ticket_name,
|
||||
"output_dir": output_dir,
|
||||
"files": {
|
||||
"ticket_info": "ticket_info.json",
|
||||
"ticket_summary": "ticket_summary.json",
|
||||
"messages": "all_messages.json",
|
||||
"messages_raw": "messages_raw.json",
|
||||
"messages_text": "all_messages.txt",
|
||||
"attachments": "attachments_info.json",
|
||||
"followers": "followers.json" if followers_path else None
|
||||
},
|
||||
"stats": {
|
||||
"messages_count": messages_result.get("messages_count", 0),
|
||||
"attachments_count": len(attachments_info)
|
||||
}
|
||||
}
|
||||
structure_path = os.path.join(output_dir, "structure.json")
|
||||
save_json(structure, structure_path)
|
||||
|
||||
return {
|
||||
"ticket_info": ticket_info_path,
|
||||
"ticket_summary": summary_path,
|
||||
"messages_file": messages_result.get("all_messages_path"),
|
||||
"messages_count": messages_result.get("messages_count", 0),
|
||||
"ticket_data_file": structure_path,
|
||||
"attachments": attachments_info,
|
||||
"attachments_count": len(attachments_info)
|
||||
}
|
||||
@ -1,307 +0,0 @@
|
||||
"""
|
||||
Utilitaires généraux pour l'extraction de tickets.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from typing import Dict, Any, List, Optional, Union
|
||||
from html import unescape
|
||||
from bs4 import BeautifulSoup, Tag
|
||||
import html2text
|
||||
import unicodedata
|
||||
|
||||
def setup_logging(level: Union[str, int] = logging.INFO, log_file: Optional[str] = None) -> None:
|
||||
"""
|
||||
Configure la journalisation avec un format spécifique et éventuellement un fichier de logs.
|
||||
|
||||
Args:
|
||||
level: Niveau de journalisation en tant que chaîne (ex: "INFO", "DEBUG") ou valeur entière (default: logging.INFO)
|
||||
log_file: Chemin du fichier de log (default: None)
|
||||
"""
|
||||
# Convertir le niveau de log si c'est une chaîne
|
||||
if isinstance(level, str):
|
||||
numeric_level = getattr(logging, level.upper(), None)
|
||||
if not isinstance(numeric_level, int):
|
||||
raise ValueError(f"Niveau de journalisation invalide: {level}")
|
||||
else:
|
||||
numeric_level = level
|
||||
|
||||
logging.basicConfig(
|
||||
level=numeric_level,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
|
||||
# Ajout d'un gestionnaire de fichier si log_file est spécifié
|
||||
if log_file:
|
||||
# S'assurer que le répertoire existe
|
||||
log_dir = os.path.dirname(log_file)
|
||||
if log_dir and not os.path.exists(log_dir):
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
|
||||
file_handler = logging.FileHandler(log_file, encoding='utf-8')
|
||||
file_handler.setLevel(numeric_level)
|
||||
file_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s', '%Y-%m-%d %H:%M:%S')
|
||||
file_handler.setFormatter(file_formatter)
|
||||
logging.getLogger().addHandler(file_handler)
|
||||
|
||||
def log_separator(length: int = 60) -> None:
|
||||
"""
|
||||
Ajoute une ligne de séparation dans les logs.
|
||||
|
||||
Args:
|
||||
length: Longueur de la ligne (default: 60)
|
||||
"""
|
||||
logging.info("-" * length)
|
||||
|
||||
def save_json(data: Any, file_path: str) -> bool:
|
||||
"""
|
||||
Sauvegarde des données au format JSON dans un fichier.
|
||||
|
||||
Args:
|
||||
data: Données à sauvegarder
|
||||
file_path: Chemin du fichier
|
||||
|
||||
Returns:
|
||||
True si la sauvegarde a réussi, False sinon
|
||||
"""
|
||||
try:
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.error(f"Erreur lors de la sauvegarde du fichier JSON {file_path}: {e}")
|
||||
return False
|
||||
|
||||
def save_text(text: str, file_path: str) -> bool:
|
||||
"""
|
||||
Sauvegarde du texte dans un fichier.
|
||||
|
||||
Args:
|
||||
text: Texte à sauvegarder
|
||||
file_path: Chemin du fichier
|
||||
|
||||
Returns:
|
||||
True si la sauvegarde a réussi, False sinon
|
||||
"""
|
||||
try:
|
||||
# S'assurer que le répertoire existe
|
||||
directory = os.path.dirname(file_path)
|
||||
if directory and not os.path.exists(directory):
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(text)
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.error(f"Erreur lors de la sauvegarde du fichier texte {file_path}: {e}")
|
||||
return False
|
||||
|
||||
def is_important_image(tag: Tag, message_text: str) -> bool:
|
||||
"""
|
||||
Détermine si une image est importante ou s'il s'agit d'un logo/signature.
|
||||
|
||||
Args:
|
||||
tag: La balise d'image à analyser
|
||||
message_text: Le texte complet du message pour contexte
|
||||
|
||||
Returns:
|
||||
True si l'image semble importante, False sinon
|
||||
"""
|
||||
# Vérifier les attributs de l'image
|
||||
src = str(tag.get('src', ''))
|
||||
alt = str(tag.get('alt', ''))
|
||||
title = str(tag.get('title', ''))
|
||||
|
||||
# Patterns pour les images inutiles
|
||||
useless_img_patterns = [
|
||||
'logo', 'signature', 'outlook', 'footer', 'header', 'icon',
|
||||
'emoticon', 'emoji', 'cid:', 'pixel', 'spacer', 'vignette',
|
||||
'banner', 'separator', 'decoration', 'mail_signature'
|
||||
]
|
||||
|
||||
# Vérifier si c'est une image inutile
|
||||
for pattern in useless_img_patterns:
|
||||
if (pattern in src.lower() or
|
||||
pattern in alt.lower() or
|
||||
pattern in title.lower()):
|
||||
return False
|
||||
|
||||
# Vérifier la taille
|
||||
width_str = str(tag.get('width', ''))
|
||||
height_str = str(tag.get('height', ''))
|
||||
|
||||
try:
|
||||
if width_str.isdigit() and height_str.isdigit():
|
||||
width = int(width_str)
|
||||
height = int(height_str)
|
||||
if width <= 50 and height <= 50:
|
||||
return False
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# Vérifier si l'image est mentionnée dans le texte
|
||||
image_indicators = [
|
||||
'capture', 'screenshot', 'image', 'photo', 'illustration',
|
||||
'voir', 'regarder', 'ci-joint', 'écran', 'erreur', 'problème',
|
||||
'bug', 'pièce jointe', 'attachment', 'veuillez trouver'
|
||||
]
|
||||
|
||||
for indicator in image_indicators:
|
||||
if indicator in message_text.lower():
|
||||
return True
|
||||
|
||||
return True
|
||||
|
||||
def clean_html(html_content: str,
|
||||
strategy: str = "html2text",
|
||||
preserve_links: bool = False,
|
||||
preserve_images: bool = False) -> str:
|
||||
"""
|
||||
Nettoie le contenu HTML et le convertit en texte selon la stratégie spécifiée.
|
||||
|
||||
Args:
|
||||
html_content: Contenu HTML à nettoyer
|
||||
strategy: Stratégie de nettoyage ('strip_tags', 'html2text', 'soup') (default: 'html2text')
|
||||
preserve_links: Conserver les liens dans la version texte (default: False)
|
||||
preserve_images: Conserver les références aux images (default: False)
|
||||
|
||||
Returns:
|
||||
Texte nettoyé
|
||||
"""
|
||||
if not html_content:
|
||||
return ""
|
||||
|
||||
# Remplacer les balises br par des sauts de ligne
|
||||
html_content = re.sub(r'<br\s*/?>|<BR\s*/?>', '\n', html_content)
|
||||
|
||||
if strategy == "strip_tags":
|
||||
# Solution simple: suppression des balises HTML
|
||||
text = re.sub(r'<[^>]+>', '', html_content)
|
||||
# Nettoyer les espaces multiples et les lignes vides multiples
|
||||
text = re.sub(r'\s+', ' ', text)
|
||||
text = re.sub(r'\n\s*\n', '\n\n', text)
|
||||
return text.strip()
|
||||
|
||||
elif strategy == "html2text":
|
||||
# Utiliser html2text pour une meilleure conversion
|
||||
h = html2text.HTML2Text()
|
||||
h.ignore_links = not preserve_links
|
||||
h.ignore_images = not preserve_images
|
||||
h.body_width = 0 # Ne pas limiter la largeur du texte
|
||||
return h.handle(html_content).strip()
|
||||
|
||||
elif strategy == "soup":
|
||||
# Utiliser BeautifulSoup pour un nettoyage plus avancé
|
||||
try:
|
||||
soup = BeautifulSoup(html_content, 'html.parser')
|
||||
|
||||
# Préserver les liens si demandé
|
||||
if preserve_links:
|
||||
for a_tag in soup.find_all('a', href=True):
|
||||
if isinstance(a_tag, Tag):
|
||||
href = a_tag.get('href', '')
|
||||
new_text = f"{a_tag.get_text()} [{href}]"
|
||||
new_tag = soup.new_string(new_text)
|
||||
a_tag.replace_with(new_tag)
|
||||
|
||||
# Préserver les images si demandé
|
||||
if preserve_images:
|
||||
for img_tag in soup.find_all('img'):
|
||||
if isinstance(img_tag, Tag):
|
||||
src = img_tag.get('src', '')
|
||||
alt = img_tag.get('alt', '')
|
||||
new_text = f"[Image: {alt} - {src}]"
|
||||
new_tag = soup.new_string(new_text)
|
||||
img_tag.replace_with(new_tag)
|
||||
|
||||
# Convertir les listes en texte formaté
|
||||
for ul in soup.find_all('ul'):
|
||||
if isinstance(ul, Tag):
|
||||
for li in ul.find_all('li'):
|
||||
if isinstance(li, Tag):
|
||||
li_text = li.get_text()
|
||||
new_text = f"• {li_text}"
|
||||
new_tag = soup.new_string(new_text)
|
||||
li.replace_with(new_tag)
|
||||
|
||||
for ol in soup.find_all('ol'):
|
||||
if isinstance(ol, Tag):
|
||||
for i, li in enumerate(ol.find_all('li')):
|
||||
if isinstance(li, Tag):
|
||||
li_text = li.get_text()
|
||||
new_text = f"{i+1}. {li_text}"
|
||||
new_tag = soup.new_string(new_text)
|
||||
li.replace_with(new_tag)
|
||||
|
||||
text = soup.get_text()
|
||||
# Nettoyer les espaces et les lignes vides
|
||||
text = re.sub(r'\n\s*\n', '\n\n', text)
|
||||
return text.strip()
|
||||
except Exception as e:
|
||||
logging.warning(f"Erreur lors du nettoyage HTML avec BeautifulSoup: {e}")
|
||||
# En cas d'erreur, utiliser une méthode de secours
|
||||
return clean_html(html_content, "strip_tags")
|
||||
|
||||
else:
|
||||
# Stratégie par défaut
|
||||
logging.warning(f"Stratégie de nettoyage '{strategy}' inconnue, utilisation de 'strip_tags'")
|
||||
return clean_html(html_content, "strip_tags")
|
||||
|
||||
def detect_duplicate_content(messages: List[Dict[str, Any]]) -> List[int]:
|
||||
"""
|
||||
Détecte les messages avec un contenu dupliqué et retourne leurs indices.
|
||||
|
||||
Args:
|
||||
messages: Liste de messages à analyser
|
||||
|
||||
Returns:
|
||||
Liste des indices des messages dupliqués
|
||||
"""
|
||||
content_map = {}
|
||||
duplicate_indices = []
|
||||
|
||||
for idx, message in enumerate(messages):
|
||||
body = message.get("body", "")
|
||||
if not body:
|
||||
continue
|
||||
|
||||
# Nettoyer le contenu HTML pour la comparaison
|
||||
cleaned_content = clean_html(body, "strip_tags")
|
||||
# Considérer uniquement les messages avec du contenu significatif
|
||||
if len(cleaned_content.strip()) < 10:
|
||||
continue
|
||||
|
||||
# Vérifier si le contenu existe déjà
|
||||
if cleaned_content in content_map:
|
||||
duplicate_indices.append(idx)
|
||||
else:
|
||||
content_map[cleaned_content] = idx
|
||||
|
||||
return duplicate_indices
|
||||
|
||||
def normalize_filename(name: str) -> str:
|
||||
"""
|
||||
Normalise un nom de fichier en remplaçant les caractères non autorisés.
|
||||
|
||||
Args:
|
||||
name: Nom à normaliser
|
||||
|
||||
Returns:
|
||||
Nom normalisé
|
||||
"""
|
||||
# Enlever les accents
|
||||
name = unicodedata.normalize('NFKD', name).encode('ASCII', 'ignore').decode('ASCII')
|
||||
|
||||
# Remplacer les caractères non alphanumériques par des underscores
|
||||
name = re.sub(r'[^\w\.-]', '_', name)
|
||||
|
||||
# Limiter la longueur à 255 caractères (limitation commune des systèmes de fichiers)
|
||||
# Remplacer les caractères non autorisés par des underscores
|
||||
sanitized = re.sub(r'[\\/*?:"<>|]', '_', name)
|
||||
# Limiter la longueur du nom à 100 caractères
|
||||
if len(sanitized) > 100:
|
||||
sanitized = sanitized[:97] + "..."
|
||||
return sanitized.strip()
|
||||
Loading…
x
Reference in New Issue
Block a user