using Reranker; using System.Text; using ToolsServices; namespace Services { public static class RAGService { #region Variables private static Services.ReActAgent.ReActAgent _Agent = new(); private static string NomFichierData = FichiersInternesService.ParamsRAG;//"paramsRAG.txt"; #endregion #region Méthodes publiques #region Gestion des paramètres public static ParametresRAGService LoadParametres() { LoggerService.LogInfo("RAGService.LoadParametres"); ParametresRAGService SelectedItem = new(); try { if (File.Exists(NomFichierData)) { string[] lignes = File.ReadAllLines(NomFichierData); if (lignes.Length > 0) SelectedItem.RAG_Path_ToVectoralize = lignes[0]; if (lignes.Length > 1) SelectedItem.RAG_Path_Vectoralized = lignes[1]; } return SelectedItem; } catch { return new(); } } public static bool SaveParametres(ParametresRAGService selectedItem) { LoggerService.LogInfo("RAGService.SaveParametres"); try { StringBuilder sb = new(); sb.AppendLine(selectedItem.RAG_Path_ToVectoralize); sb.AppendLine(selectedItem.RAG_Path_Vectoralized); File.WriteAllText(NomFichierData, sb.ToString()); return true; } catch { return false; } } #endregion private static async Task ChargementDocuments(QdrantService qdrant, bool isRAZCollections) { LoggerService.LogInfo("RAGService.ChargementDocuments"); ParametresRAGService? ragParam = LoadParametres(); // Chargement documents await IngestDocuments(ragParam.RAG_Path_ToVectoralize, ragParam.RAG_Path_Vectoralized, qdrant, isRAZCollections); } /* public static async Task<(bool, string, string)> RechercheDansDocuments(string query, string model) { LoggerService.LogInfo("RAGService.RechercheDansDocuments"); await ChargementDocuments(); // Cela inclut pour quey : le split en chunck + tokenisation + vectorisation LoggerService.LogDebug("Debut de RAGService.RechercheDansDocuments"); var (reponse, m) = await _Agent.RechercheRAG(query, "Recherche RAG", model); LoggerService.LogDebug("Fin de RAGService.RechercheDansDocuments"); return (true, reponse, m); } */ public static async Task SaveDocumentAsync(byte[] data, string fileName, Domain domain) { ParametresRAGService? ragParam = LoadParametres(); // Sécurisation du filename pour éviter ../ etc. var safeName = Path.GetFileName(fileName); // Chemin final var fullPath = Path.Combine(ragParam.RAG_Path_ToVectoralize, domain.ToCollectionName(), safeName); // Écriture du fichier await File.WriteAllBytesAsync(fullPath, data); return fullPath; } /// /// Recherche dans la base vectorielle Qdrant /// /// /// /// public static async Task<(bool, string, string)> RechercheDansDocuments(Domain domain, string query, string model, bool isRAZCollections) { LoggerService.LogInfo("RAGService.RechercheDansDocuments"); var qdrant = new QdrantService(); await ChargementDocuments(qdrant, isRAZCollections); // Cela inclut pour quey : le split en chunck + tokenisation + vectorisation LoggerService.LogDebug("Debut de RAGService.RechercheDansDocuments"); (bool b, float[]? queryEmbedding) = await _Agent.GetEmbeddingAsync(query, true); if(b && queryEmbedding != null) { LoggerService.LogDebug("Embedding de la requête OK"); } else { LoggerService.LogWarning("Problème lors de la vectorisation de la requête"); return (false, "Problème lors de la vectorisation de la requête", ""); } var results = await qdrant.SearchAsync(domain, queryEmbedding, 0.1f, topK: 20); /* var retrievedChunks = results .Select(r => (r.Text + " - source : " + r.Nom_Fichier)) .ToArray(); */ // --- Reranking : Charger tokenizer et cross-encoder ONNX //var reranker = new BgeReranker(); var ranked = BgeReranker.Rerank(query, results, topK: 5); if (ranked.Count() > 0) { //var (reponse, m) = await _Agent.RechercheRAG(query, "Recheche RAG", model, retrievedChunks); var (reponse, m) = await _Agent.RechercheRAG(query, "Recheche RAG", model, ranked, domain.ToCollectionName()); LoggerService.LogDebug("Fin de RAGService.RechercheDansDocuments avec demande au LLM"); return (true, reponse, m); } else { LoggerService.LogDebug("Fin de RAGService.RechercheDansDocuments sans demande au LLM"); return (true, "Pas de réponse contenue dans la base vectorielle", "Pas de LLM sollicité"); } } /// /// Recherche dans les collections CV ou Emails /// /// /// /// public static async Task Search(Domain domain, string query, string adresseMail="") { LoggerService.LogInfo("RAGService.Search"); var qdrant = new QdrantService(); //await ChargementDocuments(qdrant, false); // Cela inclut pour quey : le split en chunck + tokenisation + vectorisation (bool b, float[]? queryEmbedding) = await _Agent.GetEmbeddingAsync(query, true); if (b && queryEmbedding != null) { LoggerService.LogDebug("Embedding de la requête OK"); } else { LoggerService.LogWarning("Problème lors de la vectorisation de la requête"); return (""); } StringBuilder sb = new(); var results = await qdrant.SearchAsync(domain, queryEmbedding,0.1f, topK: 20, adresseMail: adresseMail); /* var retrievedChunks = results .Select(r => r.Text) .ToArray(); */ var ranked = BgeReranker.Rerank(query, results, topK: 5); if (ranked.Count() > 0) { foreach (var entry in ranked) { sb.AppendLine($"Document: {entry.Nom_Fichier}"); /* if (entry.Payload.TryGetValue("nom_fichier", out var value2)) { if (value2.StringValue != "") { sb.AppendLine($"Email: {value2.StringValue}"); } } */ sb.AppendLine("------"); } return (sb.ToString()); } else { LoggerService.LogDebug("Fin de RAGService.RechercheDansDocuments sans demande au LLM"); return (""); } } // Documents pas forcément RAG donc Domain en fonction du contexte public static async Task IngestDocument(Domain domain,bool isOnDemand, bool isRAZCollections, string content, string filename, string emailObject = "", string proprietaire = "", string niveauAcces="") { LoggerService.LogInfo("RAGService.IngestDocument"); try { QdrantService qdrant = new(); var chunks = ChunkText(content); await qdrant.InitializeCollectionsAsync(isRAZCollections); for (int i = 0; i < chunks.Length; i++) { var chunk = chunks[i]; (bool b, float[]? embedding) = await _Agent.GetEmbeddingAsync(chunk, isOnDemand); if (b && embedding != null) { var id = $"{Path.GetFileName(filename)}_chunk_{i}"; await qdrant.IngestDocument(domain, embedding, filename, id, chunk, emailObject, proprietaire, niveauAcces); } } return true; } catch (Exception ex) { LoggerService.LogError($"RAGService.IngestDocument : {ex.Message}"); return false; } } public static string[] ChunkText(string text, int chunkSize = 250, int overlap = 50) { var words = text.Split(' ', StringSplitOptions.RemoveEmptyEntries); var chunks = new List(); /* for (int i = 0; i < words.Length; i += (chunkSize - overlap)) { var chunkWords = words.Skip(i).Take(chunkSize); chunks.Add(string.Join(' ', chunkWords)); } */ int start = 0; while (start < words.Length) { int length = Math.Min(chunkSize, words.Length - start); var chunk = string.Join(' ', words.Skip(start).Take(length)); chunks.Add(chunk); start += chunkSize - overlap; } return chunks.ToArray(); } #endregion #region Méthodes privées // Pour l'instant Documents RAG private static async Task IngestDocuments(string pathDocs, string pathDestinationDocs, QdrantService qdrant, bool isRAZCollections) { LoggerService.LogInfo("RAGService.IngestDocuments"); // boucler sur les documents et envoyer à _Agent.IngestDocumentAsync // Cela inclut le split en chunck + tokenisation + vectorisation + sauvegarde des vecteurs try { if (!System.IO.Directory.Exists(pathDocs)) { LoggerService.LogWarning($"Le dossier n'existe pas : {pathDocs}"); return false; } await qdrant.InitializeCollectionsAsync(isRAZCollections); List filesToDeplace = new(); var files = Directory.GetFiles(pathDocs, "*.*", SearchOption.AllDirectories); foreach (var file in files) { var relativePath = System.IO.Path.GetRelativePath(pathDocs, file); var content = FilesService.ExtractText(file); if (content != string.Empty) { var domain = DomainExtensions.GetDomainByPath(file); // Si ce document y est déjà, le contenu de la base vectorielle sera supprimé avant d'être ingéré de nouveau await qdrant.DeleteDocumentInQdrant(domain, Path.GetFileName(file)); var chunks = ChunkText(content); for (int i = 0; i < chunks.Length; i++) { var chunk = chunks[i]; (bool b, float[]? embedding) = await _Agent.GetEmbeddingAsync(chunk, true); if (b && embedding != null) { var id = $"{Path.GetFileName(file)}_chunk_{i}"; await qdrant.IngestDocument(domain, embedding, Path.GetFileName(file), id, chunk); } } filesToDeplace.Add(file); } } foreach (var file in filesToDeplace) { // on déplace les fichiers une fois vectorisés // Chemin relatif pour recréer les sous-dossiers var relativePath = System.IO.Path.GetRelativePath(pathDocs, file); var destPath = System.IO.Path.Combine(pathDestinationDocs, relativePath); // Crée le dossier de destination si nécessaire var destDir = System.IO.Path.GetDirectoryName(destPath); if (destDir != null && !Directory.Exists(destDir)) { Directory.CreateDirectory(destDir); } // Déplace le fichier if(System.IO.File.Exists(file)) File.Move(file, destPath, overwrite: true); } LoggerService.LogDebug($"Fin de RAGService.IngestDocument : {files.Length} document(s)"); return true; } catch (Exception ex) { LoggerService.LogError($"RAGService.IngestDocument : {ex.Message}"); return false; } } #endregion } }