1404-9:02

This commit is contained in:
Ladebeze66 2025-04-14 09:02:07 +02:00
parent 54dc4eff9e
commit d86e0f0fbc
32 changed files with 2 additions and 6859 deletions

2
.cursorindexingignore Normal file
View File

@ -0,0 +1,2 @@
# Don't index SpecStory auto-save files, but allow explicit context inclusion via @ references
.specstory/**

View File

@ -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)

View File

@ -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.')

View File

@ -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!")

View File

@ -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()

View File

@ -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 Question Réponse
2 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.

View File

@ -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"
}
]
}
}

View File

@ -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

View File

@ -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 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()

View File

@ -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()

View File

@ -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! ✅")

View File

@ -1,3 +0,0 @@
"""
Package tests: Tests unitaires et fonctionnels.
"""

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

@ -1,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 {}

View File

@ -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('&nbsp;', ' ')
content = content.replace('&lt;', '<')
content = content.replace('&gt;', '>')
content = content.replace('&amp;', '&')
content = content.replace('&quot;', '"')
# 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>![CBAO - développeur de rentabilité - www.exemple.fr](data:image/png;base64,ABC123)</p>
"""
cleaned = clean_html(html)
print("HTML nettoyé :\n", cleaned)
# Test avec un message transféré
forwarded = """\\-------- Message transféré -------- Sujet : | Test message
---|---
Date : | Mon, 30 Mar 2020 11:18:20 +0200
De : | [test@example.com](mailto:test@example.com)
Pour : | John Doe [](mailto:john@example.com)
Copie à : | [other@example.com](mailto:other@example.com)
Bonjour John,
Voici un message de test.
Cordialement,
Test User
__________________________________________________________________ Ce message et toutes les pièces jointes sont confidentiels et établis à l'intention exclusive de ses destinataires. __________________________________________________________________"""
cleaned_forwarded = clean_html(forwarded)
print("\nMessage transféré nettoyé :\n", cleaned_forwarded)
# Test avec le cas problématique du ticket T0282
test_t0282 = """Bonjour,
Je reviens vers vous pour savoir si vous souhaitez toujours renommer le numéro d'identification de certaines formules dans BCN ou si vous avez trouvé une solution alternative ?
En vous remerciant par avance, je reste à votre disposition pour tout complément d'information.
Cordialement.
**Youness BENDEQ**
[
Affin d'assurer une meilleure traçabilité et vous garantir une prise en charge optimale, nous vous invitons à envoyer vos demandes d'assistance technique à support@cbao.fr Notre service est ouvert du lundi au vendredi de 9h à 12h et de 14h à 18h. Dès réception, un technicien prendra en charge votre demande et au besoin vous rappellera."""
cleaned_t0282 = clean_html(test_t0282)
print("\nTest ticket T0282 nettoyé :\n", cleaned_t0282)
# Test avec le cas problématique de bas de page avec formatage markdown
test_cbao_markdown = """Bonjour,
Voici un message de test pour vérifier la suppression des bas de page CBAO.
Cordialement,
Jean Dupont
[ CBAO S.A.R.L. ](https://example.com/link) .
![](/web/image/33748?access_token=13949e22-d47b-4af7-868e-e9c2575469f1) ![](/web/image/33748?access_token=13949e22-d47b-4af7-868e-e9c2575469f1)"""
cleaned_markdown = clean_html(test_cbao_markdown)
print("\nTest avec formatage Markdown CBAO nettoyé :\n", cleaned_markdown)
# Test avec le cas exact du rapport
test_rapport = """Bonjour,
Voici un message de test.
Cordialement,
Pierre Martin
Envoyé par [ CBAO S.A.R.L. ](https://ciibcee.r.af.d.sendibt2.com/tr/cl/h2uBsi9hBosNYeSHMsPH47KAmufMTuNZjreF6M_tfRE63xzft8fwSbEQNb0aYIor74WQB5L6TF4kR9szVpQnalHFa3PUn_0jeLw42JNzIwsESwVlYad_3xCC1xi7qt3-dQ7i_Rt62MG217XgidnJxyNVcXWaWG5B75sB0GoqJq13IZc-hQ) .
![](/web/image/33748?access_token=13949e22-d47b-4af7-868e-e9c2575469f1) ![](/web/image/33748?access_token=13949e22-d47b-4af7-868e-e9c2575469f1)"""
cleaned_rapport = clean_html(test_rapport)
print("\nTest avec cas exact du rapport nettoyé :\n", cleaned_rapport)

View File

@ -1,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('&nbsp;', ' ')
content = content.replace('&lt;', '<')
content = content.replace('&gt;', '>')
content = content.replace('&amp;', '&')
content = content.replace('&quot;', '"')
# 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>![CBAO - développeur de rentabilité - www.exemple.fr](data:image/png;base64,ABC123)</p>
"""
cleaned = clean_html(html)
print("HTML nettoyé :\n", cleaned)
# Test avec un message transféré
forwarded = """\\-------- Message transféré -------- Sujet : | Test message
---|---
Date : | Mon, 30 Mar 2020 11:18:20 +0200
De : | [test@example.com](mailto:test@example.com)
Pour : | John Doe [](mailto:john@example.com)
Copie à : | [other@example.com](mailto:other@example.com)
Bonjour John,
Voici un message de test.
Cordialement,
Test User
__________________________________________________________________ Ce message et toutes les pièces jointes sont confidentiels et établis à l'intention exclusive de ses destinataires. __________________________________________________________________"""
cleaned_forwarded = clean_html(forwarded)
print("\nMessage transféré nettoyé :\n", cleaned_forwarded)
# Test avec le cas problématique du ticket T0282
test_t0282 = """Bonjour,
Je reviens vers vous pour savoir si vous souhaitez toujours renommer le numéro d'identification de certaines formules dans BCN ou si vous avez trouvé une solution alternative ?
En vous remerciant par avance, je reste à votre disposition pour tout complément d'information.
Cordialement.
**Youness BENDEQ**
[
Affin d'assurer une meilleure traçabilité et vous garantir une prise en charge optimale, nous vous invitons à envoyer vos demandes d'assistance technique à support@cbao.fr Notre service est ouvert du lundi au vendredi de 9h à 12h et de 14h à 18h. Dès réception, un technicien prendra en charge votre demande et au besoin vous rappellera."""
cleaned_t0282 = clean_html(test_t0282)
print("\nTest ticket T0282 nettoyé :\n", cleaned_t0282)
# Test avec le cas problématique de bas de page avec formatage markdown
test_cbao_markdown = """Bonjour,
Voici un message de test pour vérifier la suppression des bas de page CBAO.
Cordialement,
Jean Dupont
[ CBAO S.A.R.L. ](https://example.com/link) .
![](/web/image/33748?access_token=13949e22-d47b-4af7-868e-e9c2575469f1) ![](/web/image/33748?access_token=13949e22-d47b-4af7-868e-e9c2575469f1)"""
cleaned_markdown = clean_html(test_cbao_markdown)
print("\nTest avec formatage Markdown CBAO nettoyé :\n", cleaned_markdown)
# Test avec le cas exact du rapport
test_rapport = """Bonjour,
Voici un message de test.
Cordialement,
Pierre Martin
Envoyé par [ CBAO S.A.R.L. ](https://ciibcee.r.af.d.sendibt2.com/tr/cl/h2uBsi9hBosNYeSHMsPH47KAmufMTuNZjreF6M_tfRE63xzft8fwSbEQNb0aYIor74WQB5L6TF4kR9szVpQnalHFa3PUn_0jeLw42JNzIwsESwVlYad_3xCC1xi7qt3-dQ7i_Rt62MG217XgidnJxyNVcXWaWG5B75sB0GoqJq13IZc-hQ) .
![](/web/image/33748?access_token=13949e22-d47b-4af7-868e-e9c2575469f1) ![](/web/image/33748?access_token=13949e22-d47b-4af7-868e-e9c2575469f1)"""
cleaned_rapport = clean_html(test_rapport)
print("\nTest avec cas exact du rapport nettoyé :\n", cleaned_rapport)

View File

@ -1,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)

View File

@ -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)

View File

@ -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)

View 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)

View File

@ -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

View File

@ -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}")

View File

@ -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()

View File

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

View File

@ -1,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)
}

View File

@ -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)
}

View File

@ -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()