using Models; using Qdrant.Client.Grpc; using System.Collections; using System.ComponentModel.DataAnnotations; using System.Numerics; using System.Text; using ToolsServices; namespace Services; #region enum des type de documents public enum Domain { RH, Juridique, Global, Emails, CV, Technique } #endregion #region Classe statique d'extension pour Domain public static class DomainExtensions { private static readonly string _CollectionRH = "RH"; private static readonly string _CollectionJuridique = "Juridique"; private static readonly string _CollectionGlobal = "Global"; private static readonly string _CollectionEmails = "Emails"; private static readonly string _CollectionCV = "CV"; private static readonly string _CollectionTechnique = "Technique"; public static List CollectionsName = GetCollectionsName(); public static List CollectionsNameRestricted = GetCollectionsNameRestricted(false); private static List GetCollectionsName() { List strings = new() { _CollectionRH, _CollectionJuridique, _CollectionGlobal, _CollectionEmails, _CollectionCV, _CollectionTechnique }; return strings; } private static List GetCollectionsNameRestricted(bool isVeryRestricted) { List strings = new(); strings.Add(_CollectionGlobal); strings.Add(_CollectionTechnique); if (!isVeryRestricted) { strings.Add(_CollectionJuridique); strings.Add(_CollectionRH); } return strings; } /// /// Retourne le nom de collection Qdrant associé à un domaine. /// public static string ToCollectionName(this Domain domain) => domain switch { Domain.RH => _CollectionRH, Domain.Juridique => _CollectionJuridique, Domain.Global => _CollectionGlobal, Domain.Emails => _CollectionEmails, Domain.CV => _CollectionCV, Domain.Technique => _CollectionTechnique, _ => throw new ArgumentOutOfRangeException(nameof(domain), domain, null) }; /// /// Retourne le nom de collection Qdrant associé à un domaine. /// public static Domain GetDomainByCollectionName(string CollectionName) { Domain domain = Domain.Global; switch (CollectionName) { case "RH": domain = Domain.RH; break; case "Juridique": domain = Domain.Juridique; break; case "Global": domain = Domain.Global; break; case "Emails": domain = Domain.Emails; break; case "CV": domain = Domain.CV; break; case "Technique": domain = Domain.Technique; break; } return domain; } public static Domain GetDomainByPath(string filePath) { var dirs = Path.GetDirectoryName(filePath)? .Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries); if (dirs == null) return Domain.Global; var collections = GetCollectionsName(); // On cherche la première correspondance var collectionName = dirs.FirstOrDefault(d => collections.Any(c => string.Equals(c, d, StringComparison.OrdinalIgnoreCase))); return GetDomainByCollectionName(collectionName ?? "Global"); } } #endregion #region Classe QdrantService /// /// http://localhost:6333/dashboard /// public class QdrantService { #region Variables private static readonly string NomFichierData = FichiersInternesService.ParamsQdrant;// "paramsOllama.txt"; private readonly Qdrant.Client.QdrantClient _client; private const int EmbeddingDim = 768; private const Distance DefaultDistance = Distance.Cosine; #endregion #region Constructeur public QdrantService() { var parametres = LoadParametres(); //_client = new Qdrant.Client.QdrantClient(parametres!.Qdrant_URL, parametres!.Qdrant_Port, parametres!.Qdrant_IsHttps); var httpString = parametres!.Qdrant_IsHttps ? "https://" : "http://"; _client = new Qdrant.Client.QdrantClient(new Uri($"{httpString}{parametres!.Qdrant_URL}")); } #endregion #region Méthodes publiques #region Gestion des Paramètres public static ParametresQdrantService? LoadParametres() { ParametresQdrantService? item = new(); LoggerService.LogInfo("QdrantService.LoadParametres"); try { if (File.Exists(NomFichierData)) { string[] lignes = File.ReadAllLines(NomFichierData); if (lignes.Length > 0) item.Qdrant_URL = lignes[0]; if (lignes.Length > 1) { if (bool.TryParse(lignes[1], out bool isHttps)) item.Qdrant_IsHttps = isHttps; } /* if (lignes.Length > 2) { if (int.TryParse(lignes[1], out int port)) item.Qdrant_Port = port; } */ } return item; } catch { return null; } } public static bool SaveParametres(ParametresQdrantService item) { LoggerService.LogInfo("QdrantService.SaveParametres"); try { StringBuilder sb = new(); sb.AppendLine(item.Qdrant_URL); sb.AppendLine(item.Qdrant_IsHttps.ToString()); //sb.AppendLine(item.Qdrant_Port.ToString()); File.WriteAllText(NomFichierData, sb.ToString()); return true; } catch { return false; } } #endregion public async Task IsAlive() { try { var health = await _client.HealthAsync(); return (health != null); } catch { LoggerService.LogError("QdrantService.IsAlive - Qdrant n'est pas accessible."); return false; } } public async Task InitializeCollectionsAsync(bool isRAZCollections) { if(isRAZCollections) await DeleteCollectionAsync(); foreach (Domain domain in Enum.GetValues(typeof(Domain))) { await EnsureCollectionAsync(domain); } } public async Task DeleteDocumentInQdrant(Domain domain, string nomFichier) { var pointId = new PointId { Uuid = Guid.NewGuid().ToString() }; var collectionName = domain.ToCollectionName(); // 1. Définir le filtre pour trouver les points avec le même `nom_fichier` var filter = new Qdrant.Client.Grpc.Filter { Must = { new Condition { Field = new FieldCondition { Key = "nom_fichier", Match = new Match { Text = nomFichier } } } } }; // 2. Chercher les points existants avec ce filtre var searchResult = await _client.ScrollAsync( collectionName, filter: filter, limit: 1 // On n'a besoin de vérifier qu'un seul résultat ); // 3. Vérifier si des points ont été trouvés if (searchResult.Result.Count > 0) { LoggerService.LogDebug($"Un ou plusieurs documents avec le nom de fichier '{nomFichier}' ont été trouvés. Suppression..."); // Si des points existent, on utilise le même filtre pour les supprimer await _client.DeleteAsync( collectionName, filter ); LoggerService.LogDebug("Documents existants supprimés avec succès."); } } public async Task IngestDocument(Domain domain, float[] vector, string nomFichier, string nomChunck, string content, string emailObject = "", string proprietaire = "", string niveauAcces="") { var pointId = new PointId { Uuid = Guid.NewGuid().ToString() }; var collectionName = domain.ToCollectionName(); var vectorObj = new Qdrant.Client.Grpc.Vector(); vectorObj.Data.Add(vector); /* // 1. Définir le filtre pour trouver les points avec le même `nom_fichier` var filter = new Qdrant.Client.Grpc.Filter { Must = { new Condition { Field = new FieldCondition { Key = "nom_fichier", Match = new Match { Text = nomFichier } } } } }; // 2. Chercher les points existants avec ce filtre var searchResult = await _client.ScrollAsync( collectionName, filter: filter, limit: 1 // On n'a besoin de vérifier qu'un seul résultat ); // 3. Vérifier si des points ont été trouvés if (searchResult.Result.Count > 0) { LoggerService.LogDebug($"Un ou plusieurs documents avec le nom de fichier '{nomFichier}' ont été trouvés. Suppression..."); // Si des points existent, on utilise le même filtre pour les supprimer await _client.DeleteAsync( collectionName, filter ); LoggerService.LogDebug("Documents existants supprimés avec succès."); } */ await _client.UpsertAsync(collectionName, new[] { new PointStruct { Id = pointId, Vectors = new Vectors { Vector = vectorObj }, Payload = { ["categorie"] = collectionName, ["nom_fichier"] = nomFichier, ["email_object"] = emailObject, ["nom_chunck"] = nomChunck, ["niveau_acces"] = niveauAcces, ["content"] = content, ["proprietaire"] = proprietaire } } }); } //public async Task> SearchAsync(Domain domain, float[] queryVector, float seuilScore, int topK = 5, string adresseMail = "") public async Task> SearchAsync(Domain domain, float[] queryVector, float seuilScore, int topK=5, string adresseMail="") { var collectionName = domain.ToCollectionName(); Filter filter = new(); if (domain == Domain.Emails) { filter = new Qdrant.Client.Grpc.Filter { Must = { new Condition { Field = new FieldCondition { Key = "categorie", Match = new Match { Text = collectionName } } }, new Condition { Field = new FieldCondition { Key = "proprietaire", Match = new Match { Text = adresseMail } } } } }; } else { filter = new Qdrant.Client.Grpc.Filter { Must = { new Condition { Field = new FieldCondition { Key = "categorie", Match = new Match { Text = collectionName } } }} }; } var rep = await _client.SearchAsync(collectionName, queryVector, filter, null, (ulong)topK); var filteredChunks = rep .Where(r => r.Score >= seuilScore) .ToArray(); // var resultList = filteredChunks.Select(r => new SearchResult { Text = r.Payload["content"].StringValue, Nom_Fichier = r.Payload["nom_fichier"].StringValue }).ToList(); return resultList; //return filteredChunks; } #endregion #region Méthodes privées private async Task EnsureCollectionAsync(Domain domain) { var collectionName = domain.ToCollectionName(); var existing = await _client.ListCollectionsAsync(); if (!existing.Contains(collectionName)) { await _client.CreateCollectionAsync(collectionName, new VectorParams { Size = (ulong)EmbeddingDim, Distance = DefaultDistance }); } } private async Task DeleteCollectionAsync() { var collections = await _client.ListCollectionsAsync(); foreach (var col in collections) { await DeleteCollectionAsync(col); } } private async Task DeleteCollectionAsync(Domain domain) { var collectionName = domain.ToCollectionName(); await DeleteCollectionAsync(collectionName); } private async Task DeleteCollectionAsync(string collectionName) { await _client.DeleteCollectionAsync(collectionName); } #endregion } #endregion