p14 / utils /md_parser.py
DidierHernandez's picture
Initialisation RAG Assistant de LR
8a995db
Raw History Blame Contribute Delete
6.17 kB
# Ce fichier poc/utils/md_parser.py
import os
import re
import yaml
import logging
from langchain_core.documents import Document
# Récupération du logger configuré centralement
logger = logging.getLogger(__name__)
def parse_md_to_documents(corpus_dir: str) -> list[Document]:
"""
Parcourt le dossier du corpus, lit chaque fichier Markdown (.md),
extrait le Front Matter YAML (métadonnées globales) et découpe le corps
du texte en chunks sémantiques basés sur les Titres 2 (##).
Chaque action ou bloc de hors-périmètre devient un unique Document LangChain.
:param corpus_dir: Chemin vers le dossier contenant les fichiers .md
:return: Liste d'objets Document utilisables directement par FAISS
"""
documents = []
if not os.path.exists(corpus_dir):
logger.error(f"Le dossier du corpus est introuvable : {corpus_dir}")
return documents
# 1. Parcourir tous les fichiers du dossier corpus
for filename in os.listdir(corpus_dir):
if not filename.endswith('.md'):
continue # On ignore les fichiers qui ne sont pas du Markdown
file_path = os.path.join(corpus_dir, filename)
logger.info(f"Début du traitement du fichier : {filename}")
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# 2. Extraction du Front Matter YAML (Métadonnées globales)
# Les métadonnées sont délimitées par des triples tirets --- en haut du fichier
front_matter = {}
body_text = content
if content.startswith('---'):
# Expression régulière pour isoler le bloc YAML et le reste du texte
match = re.match(r'^---\s*\n(.*?)\n---\s*\n(.*)', content, re.DOTALL)
if match:
yaml_content = match.group(1)
body_text = match.group(2)
# Chargement sécurisé du dictionnaire de métadonnées YAML
front_matter = yaml.safe_load(yaml_content) or {}
logger.debug(f"Front Matter extrait avec succès pour {filename}")
# 3. Découpage du texte par Action (Titre 2 : ##)
# On utilise un regex avec capture pour conserver les titres '## Nom de l'action'
# re.split va générer une liste alternant le texte précédent (souvent vide), le titre, puis le contenu
sections = re.split(r'^(##\s+.*)$', body_text, flags=re.MULTILINE)
# La première section contient le texte avant le premier ## (Ex: Titre 1 du Process)
# On peut l'extraire pour enrichir le contexte global si nécessaire
process_intro = sections[0].strip()
# 4. Itération par paires (Titre H2, Contenu du H2)
# On commence à l'index 1 et on avance de 2 en 2
for i in range(1, len(sections), 2):
h2_title = sections[i].strip()
h2_content = sections[i+1].strip() if (i+1) < len(sections) else ""
# Nettoyage cosmétique du titre de l'action (on enlève les '## ')
action_name = h2_title.lstrip('#').strip()
# Reconstitution du texte complet du chunk (Titre + Corps)
# On préserve scrupuleusement le formatage Markdown d'origine (comme le **Gras**)
full_chunk_text = f"{h2_title}\n\n{h2_content}"
# 5. Extraction des métadonnées locales spécifiques à l'action
# On cherche les lignes clés "Écran de départ : ..." et "Mots-clés : ..." dans le texte du bloc
ecran_depart_match = re.search(r'(?:Écran de départ|Ecran de depart)\s*:\s*(.*)', h2_content, re.IGNORECASE)
mots_cles_match = re.search(r'(?:Mots-clés|Mots-cles|Mots clés)\s*:\s*(.*)', h2_content, re.IGNORECASE)
ecran_depart = ecran_depart_match.group(1).strip() if ecran_depart_match else "N/A"
mots_cles = mots_cles_match.group(1).strip() if mots_cles_match else "N/A"
# 6. Fusion des métadonnées (Globales + Locales)
# C'est l'équivalent de votre mapping standardisé pour format_docs()
metadata = {
"source_file": filename,
"process": front_matter.get("titre", filename.replace(".md", "")),
"auteur": front_matter.get("auteur", "Inconnu"),
"version": front_matter.get("version", "1.0"),
"date_maj": front_matter.get("date_maj", "N/A"),
"action": action_name,
"ecran_depart": ecran_depart,
"mots_cles": mots_cles
}
# 7. Instanciation du Document LangChain
doc = Document(
page_content=full_chunk_text,
metadata=metadata
)
documents.append(doc)
logger.info(f"Fichier {filename} traité : {len(sections) // 2} chunks créés.")
except Exception as e:
logger.error(f"Erreur lors du parsing du fichier {filename} : {e}")
continue
logger.info(f"Fin du parsing global. Total de documents LangChain créés : {len(documents)}")
return documents
# Bloc de test unitaire rapide pour valider le comportement en isolation
if __name__ == "__main__":
# Configuration d'un logging basique pour le test
logging.basicConfig(level=logging.INFO)
# Simulation d'un appel vers un dossier de test local
test_dir = "./corpus"
print(f"--- Test du parser sur le dossier {test_dir} ---")
docs = parse_md_to_documents(test_dir)
if docs:
print(f"\nExemple de structure du premier Document LangChain généré :")
print(f"METADATA : {docs[0].metadata}")
print(f"CONTENT PREVIEW (200 chars) :\n{docs[0].page_content[:200]}...")