# 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]}...")