diff --git a/modules/rag_manager.py b/modules/rag_manager.py index cbe27a9..bf97d3c 100644 --- a/modules/rag_manager.py +++ b/modules/rag_manager.py @@ -1,11 +1,15 @@ import os +import threading from datetime import datetime from modules.logger import log_event +# IMPORT DE NOTRE MOTEUR FAISS (Le nouveau cerveau) +from modules.vector_rag import rechercher_contexte_global, indexer_dossier_thematique + # Définition du chemin racine sécurisé sur ton RAID RAG_BASE_DIR = "/DATA/AppData/MULTI-IA-AETHAS38/RAG" -# Les thématiques pour organiser l'apprentissage (Ajout de Général et Éducation) +# Les thématiques pour organiser l'apprentissage THEMATIQUES_RAG = [ "Minecraft_KubeJS", "Juridique_Loi_Francaise", @@ -31,44 +35,23 @@ def initialiser_arborescence_rag(): except Exception as e: log_event("ERROR", "RAG", f"Erreur lors de l'initialisation de l'arborescence : {e}") -def extraire_contexte_rag(thematique): - """ - Lit tous les fichiers .txt ou .md d'une thématique donnée - et les concatène pour les injecter dans le prompt de l'IA. - """ - contexte_global = "" - chemin_theme = os.path.join(RAG_BASE_DIR, thematique) - - # Si le thème est "aucun" ou introuvable, on tape dans les connaissances générales - if thematique == "aucun" or not os.path.exists(chemin_theme): - chemin_theme = os.path.join(RAG_BASE_DIR, "Connaissances_Generales") - - try: - if os.path.exists(chemin_theme): - for fichier in os.listdir(chemin_theme): - if fichier.endswith(".txt") or fichier.endswith(".md"): - chemin_fichier = os.path.join(chemin_theme, fichier) - with open(chemin_fichier, 'r', encoding='utf-8') as f: - contexte_global += f"\n--- Extrait de {fichier} ---\n" - contexte_global += f.read() + "\n" - except Exception as e: - log_event("ERROR", "RAG", f"Erreur de lecture du contexte {thematique} : {e}") - - return contexte_global - def rechercher_contexte_rag(prompt, categorie="general"): """ - Récupère le contenu des fichiers de la thématique pour alimenter l'IA. + Récupère le contenu des fichiers via la recherche sémantique FAISS + Ollama. + (Remplace l'ancienne méthode d'extraction basique). """ try: - return extraire_contexte_rag(categorie) + # On fouille dans toute la base de connaissances et on récupère les 3 meilleurs blocs + contexte = rechercher_contexte_global(prompt, top_k=3) + return contexte except Exception as e: - log_event("ERROR", "RAG_MANAGER", f"Erreur lors de la recherche RAG : {e}") + log_event("ERROR", "RAG_MANAGER", f"Erreur lors de la recherche FAISS : {e}") return "" def auto_apprendre_rag(prompt, reponse, thematique_simplifiee): """ - Sauvegarde systématiquement TOUTES les réponses pour enrichir la base de connaissances. + Sauvegarde systématiquement la réponse en Markdown, + puis déclenche l'indexation FAISS pour auto-incrémenter le RAG. """ # Mapping élargi mapping = { @@ -80,8 +63,6 @@ def auto_apprendre_rag(prompt, reponse, thematique_simplifiee): "education_nationale": "Education_Nationale" } - # Si le thème n'est pas dans le mapping (ex: "aucun", "physique", "cuisine"), - # on le force dans le dossier "Connaissances_Generales" au lieu de l'ignorer. theme_reel = mapping.get(thematique_simplifiee.lower(), "Connaissances_Generales") try: @@ -95,10 +76,20 @@ def auto_apprendre_rag(prompt, reponse, thematique_simplifiee): contenu = f"## Question utilisateur\n{prompt}\n\n## Réponse de l'IA (Contexte validé)\n{reponse}\n" + # 1. Sauvegarde physique du fichier with open(chemin_fichier, 'w', encoding='utf-8') as f: f.write(contenu) log_event("INFO", "RAG_MANAGER", f"Connaissance sauvegardée dans {theme_reel} ({nom_fichier}).") + + # 2. AUTO-INCRÉMENTATION : Lancement de l'indexation FAISS + # On utilise un Thread (processus en arrière-plan) pour que l'indexation via Ollama + # se fasse silencieusement sans bloquer l'interface web de l'utilisateur. + tache_indexation = threading.Thread(target=indexer_dossier_thematique, args=(theme_reel,)) + tache_indexation.start() + + log_event("INFO", "RAG_MANAGER", f"Indexation vectorielle lancée en tâche de fond pour {theme_reel}.") + except Exception as e: log_event("ERROR", "RAG_MANAGER", f"Erreur lors de l'auto-apprentissage : {e}") diff --git a/modules/vector_rag.py b/modules/vector_rag.py index b4e6d4c..9d0b2aa 100644 --- a/modules/vector_rag.py +++ b/modules/vector_rag.py @@ -118,4 +118,66 @@ def rechercher_contexte_vectoriel(thematique: str, question: str, top_k: int = 3 except Exception as e: print(f"Erreur de recherche FAISS : {e}") - return "" \ No newline at end of file + return "" + +def indexer_tout_le_rag(): + """Parcourt tous les sous-dossiers de RAG_BASE_DIR et les indexe automatiquement.""" + print("=== LANCEMENT DE L'INDEXATION GLOBALE ===") + if not os.path.exists(RAG_BASE_DIR): + print(f"Dossier introuvable : {RAG_BASE_DIR}") + return + + for element in os.listdir(RAG_BASE_DIR): + chemin = os.path.join(RAG_BASE_DIR, element) + if os.path.isdir(chemin): + # Appelle la fonction existante pour chaque dossier trouvé + indexer_dossier_thematique(element) + print("=== INDEXATION GLOBALE TERMINÉE ===") + +def rechercher_contexte_global(question: str, top_k: int = 3): + """Cherche la réponse dans TOUTES les thématiques indexées et garde les meilleurs extraits.""" + if not os.path.exists(VECTOR_DB_DIR): + return "" + + vecteur_q = obtenir_embedding(question) + if not vecteur_q: + return "" + + vecteur_q_np = np.array([vecteur_q]).astype('float32') + tous_fragments = [] + + # On fouille dans chaque dossier de la base vectorielle + for thematique in os.listdir(VECTOR_DB_DIR): + dossier_db = os.path.join(VECTOR_DB_DIR, thematique) + fichier_index = os.path.join(dossier_db, "index.faiss") + fichier_chunks = os.path.join(dossier_db, "chunks.json") + + if os.path.exists(fichier_index) and os.path.exists(fichier_chunks): + try: + index = faiss.read_index(fichier_index) + with open(fichier_chunks, 'r', encoding='utf-8') as f: + chunks = json.load(f) + + # Recherche dans ce dossier spécifique + distances, indices = index.search(vecteur_q_np, top_k) + + # On stocke les résultats avec leur score de pertinence (distance) + for dist, i in zip(distances[0], indices[0]): + if i < len(chunks) and i != -1: + tous_fragments.append((dist, chunks[i]["texte"], thematique)) + except Exception: + continue + + if not tous_fragments: + return "" + + # On trie TOUS les fragments trouvés pour ne garder que les (top_k) les plus pertinents, tous dossiers confondus + # (FAISS utilise la distance L2 : plus le score est petit, plus le texte correspond à la question) + tous_fragments.sort(key=lambda x: x[0]) + meilleurs = tous_fragments[:top_k] + + contexte_final = [] + for dist, texte, theme in meilleurs: + contexte_final.append(f"[Source : Dossier {theme}]\n{texte}") + + return "\n\n---\n\n".join(contexte_final) \ No newline at end of file