mirror of
https://github.com/Ladebeze66/llm_ticket3.git
synced 2025-12-13 17:27:18 +01:00
340 lines
14 KiB
Plaintext
340 lines
14 KiB
Plaintext
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)
|
|
} |