Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

449 linhas
13KB

  1. using Models;
  2. using Qdrant.Client.Grpc;
  3. using System.Collections;
  4. using System.ComponentModel.DataAnnotations;
  5. using System.Numerics;
  6. using System.Text;
  7. using ToolsServices;
  8. namespace Services;
  9. #region enum des type de documents
  10. public enum Domain
  11. {
  12. RH,
  13. Juridique,
  14. Global,
  15. Emails,
  16. CV,
  17. Technique
  18. }
  19. #endregion
  20. #region Classe statique d'extension pour Domain
  21. public static class DomainExtensions
  22. {
  23. private static readonly string _CollectionRH = "RH";
  24. private static readonly string _CollectionJuridique = "Juridique";
  25. private static readonly string _CollectionGlobal = "Global";
  26. private static readonly string _CollectionEmails = "Emails";
  27. private static readonly string _CollectionCV = "CV";
  28. private static readonly string _CollectionTechnique = "Technique";
  29. public static List<string> CollectionsName = GetCollectionsName();
  30. public static List<string> CollectionsNameRestricted = GetCollectionsNameRestricted(false);
  31. private static List<string> GetCollectionsName()
  32. {
  33. List<string> strings = new()
  34. {
  35. _CollectionRH,
  36. _CollectionJuridique,
  37. _CollectionGlobal,
  38. _CollectionEmails,
  39. _CollectionCV,
  40. _CollectionTechnique
  41. };
  42. return strings;
  43. }
  44. private static List<string> GetCollectionsNameRestricted(bool isVeryRestricted)
  45. {
  46. List<string> strings = new();
  47. strings.Add(_CollectionGlobal);
  48. strings.Add(_CollectionTechnique);
  49. if (!isVeryRestricted)
  50. {
  51. strings.Add(_CollectionJuridique);
  52. strings.Add(_CollectionRH);
  53. }
  54. return strings;
  55. }
  56. /// <summary>
  57. /// Retourne le nom de collection Qdrant associé à un domaine.
  58. /// </summary>
  59. public static string ToCollectionName(this Domain domain) => domain switch
  60. {
  61. Domain.RH => _CollectionRH,
  62. Domain.Juridique => _CollectionJuridique,
  63. Domain.Global => _CollectionGlobal,
  64. Domain.Emails => _CollectionEmails,
  65. Domain.CV => _CollectionCV,
  66. Domain.Technique => _CollectionTechnique,
  67. _ => throw new ArgumentOutOfRangeException(nameof(domain), domain, null)
  68. };
  69. /// <summary>
  70. /// Retourne le nom de collection Qdrant associé à un domaine.
  71. /// </summary>
  72. public static Domain GetDomainByCollectionName(string CollectionName)
  73. {
  74. Domain domain = Domain.Global;
  75. switch (CollectionName)
  76. {
  77. case "RH":
  78. domain = Domain.RH;
  79. break;
  80. case "Juridique":
  81. domain = Domain.Juridique;
  82. break;
  83. case "Global":
  84. domain = Domain.Global;
  85. break;
  86. case "Emails":
  87. domain = Domain.Emails;
  88. break;
  89. case "CV":
  90. domain = Domain.CV;
  91. break;
  92. case "Technique":
  93. domain = Domain.Technique;
  94. break;
  95. }
  96. return domain;
  97. }
  98. public static Domain GetDomainByPath(string filePath)
  99. {
  100. var dirs = Path.GetDirectoryName(filePath)?
  101. .Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar,
  102. StringSplitOptions.RemoveEmptyEntries);
  103. if (dirs == null)
  104. return Domain.Global;
  105. var collections = GetCollectionsName();
  106. // On cherche la première correspondance
  107. var collectionName = dirs.FirstOrDefault(d =>
  108. collections.Any(c => string.Equals(c, d, StringComparison.OrdinalIgnoreCase)));
  109. return GetDomainByCollectionName(collectionName ?? "Global");
  110. }
  111. }
  112. #endregion
  113. #region Classe QdrantService
  114. /// <summary>
  115. /// http://localhost:6333/dashboard
  116. /// </summary>
  117. public class QdrantService
  118. {
  119. #region Variables
  120. private static readonly string NomFichierData = FichiersInternesService.ParamsQdrant;// "paramsOllama.txt";
  121. private readonly Qdrant.Client.QdrantClient _client;
  122. private const int EmbeddingDim = 768;
  123. private const Distance DefaultDistance = Distance.Cosine;
  124. #endregion
  125. #region Constructeur
  126. public QdrantService()
  127. {
  128. var parametres = LoadParametres();
  129. //_client = new Qdrant.Client.QdrantClient(parametres!.Qdrant_URL, parametres!.Qdrant_Port, parametres!.Qdrant_IsHttps);
  130. var httpString = parametres!.Qdrant_IsHttps ? "https://" : "http://";
  131. _client = new Qdrant.Client.QdrantClient(new Uri($"{httpString}{parametres!.Qdrant_URL}"));
  132. }
  133. #endregion
  134. #region Méthodes publiques
  135. #region Gestion des Paramètres
  136. public static ParametresQdrantService? LoadParametres()
  137. {
  138. ParametresQdrantService? item = new();
  139. LoggerService.LogInfo("QdrantService.LoadParametres");
  140. try
  141. {
  142. if (File.Exists(NomFichierData))
  143. {
  144. string[] lignes = File.ReadAllLines(NomFichierData);
  145. if (lignes.Length > 0)
  146. item.Qdrant_URL = lignes[0];
  147. if (lignes.Length > 1)
  148. {
  149. if (bool.TryParse(lignes[1], out bool isHttps))
  150. item.Qdrant_IsHttps = isHttps;
  151. }
  152. /*
  153. if (lignes.Length > 2)
  154. {
  155. if (int.TryParse(lignes[1], out int port))
  156. item.Qdrant_Port = port;
  157. }
  158. */
  159. }
  160. return item;
  161. }
  162. catch
  163. {
  164. return null;
  165. }
  166. }
  167. public static bool SaveParametres(ParametresQdrantService item)
  168. {
  169. LoggerService.LogInfo("QdrantService.SaveParametres");
  170. try
  171. {
  172. StringBuilder sb = new();
  173. sb.AppendLine(item.Qdrant_URL);
  174. sb.AppendLine(item.Qdrant_IsHttps.ToString());
  175. //sb.AppendLine(item.Qdrant_Port.ToString());
  176. File.WriteAllText(NomFichierData, sb.ToString());
  177. return true;
  178. }
  179. catch
  180. {
  181. return false;
  182. }
  183. }
  184. #endregion
  185. public async Task<bool> IsAlive()
  186. {
  187. try
  188. {
  189. var health = await _client.HealthAsync();
  190. return (health != null);
  191. }
  192. catch
  193. {
  194. LoggerService.LogError("QdrantService.IsAlive - Qdrant n'est pas accessible.");
  195. return false;
  196. }
  197. }
  198. public async Task InitializeCollectionsAsync(bool isRAZCollections)
  199. {
  200. if(isRAZCollections)
  201. await DeleteCollectionAsync();
  202. foreach (Domain domain in Enum.GetValues(typeof(Domain)))
  203. {
  204. await EnsureCollectionAsync(domain);
  205. }
  206. }
  207. public async Task DeleteDocumentInQdrant(Domain domain, string nomFichier)
  208. {
  209. var pointId = new PointId { Uuid = Guid.NewGuid().ToString() };
  210. var collectionName = domain.ToCollectionName();
  211. // 1. Définir le filtre pour trouver les points avec le même `nom_fichier`
  212. var filter = new Qdrant.Client.Grpc.Filter
  213. {
  214. Must =
  215. {
  216. new Condition
  217. {
  218. Field = new FieldCondition
  219. {
  220. Key = "nom_fichier",
  221. Match = new Match { Text = nomFichier }
  222. }
  223. }
  224. }
  225. };
  226. // 2. Chercher les points existants avec ce filtre
  227. var searchResult = await _client.ScrollAsync(
  228. collectionName,
  229. filter: filter,
  230. limit: 1 // On n'a besoin de vérifier qu'un seul résultat
  231. );
  232. // 3. Vérifier si des points ont été trouvés
  233. if (searchResult.Result.Count > 0)
  234. {
  235. LoggerService.LogDebug($"Un ou plusieurs documents avec le nom de fichier '{nomFichier}' ont été trouvés. Suppression...");
  236. // Si des points existent, on utilise le même filtre pour les supprimer
  237. await _client.DeleteAsync(
  238. collectionName,
  239. filter
  240. );
  241. LoggerService.LogDebug("Documents existants supprimés avec succès.");
  242. }
  243. }
  244. public async Task IngestDocument(Domain domain, float[] vector, string nomFichier, string nomChunck, string content, string emailObject = "", string proprietaire = "", string niveauAcces="")
  245. {
  246. var pointId = new PointId { Uuid = Guid.NewGuid().ToString() };
  247. var collectionName = domain.ToCollectionName();
  248. var vectorObj = new Qdrant.Client.Grpc.Vector();
  249. vectorObj.Data.Add(vector);
  250. /*
  251. // 1. Définir le filtre pour trouver les points avec le même `nom_fichier`
  252. var filter = new Qdrant.Client.Grpc.Filter
  253. {
  254. Must =
  255. {
  256. new Condition
  257. {
  258. Field = new FieldCondition
  259. {
  260. Key = "nom_fichier",
  261. Match = new Match { Text = nomFichier }
  262. }
  263. }
  264. }
  265. };
  266. // 2. Chercher les points existants avec ce filtre
  267. var searchResult = await _client.ScrollAsync(
  268. collectionName,
  269. filter: filter,
  270. limit: 1 // On n'a besoin de vérifier qu'un seul résultat
  271. );
  272. // 3. Vérifier si des points ont été trouvés
  273. if (searchResult.Result.Count > 0)
  274. {
  275. LoggerService.LogDebug($"Un ou plusieurs documents avec le nom de fichier '{nomFichier}' ont été trouvés. Suppression...");
  276. // Si des points existent, on utilise le même filtre pour les supprimer
  277. await _client.DeleteAsync(
  278. collectionName,
  279. filter
  280. );
  281. LoggerService.LogDebug("Documents existants supprimés avec succès.");
  282. }
  283. */
  284. await _client.UpsertAsync(collectionName, new[]
  285. {
  286. new PointStruct
  287. {
  288. Id = pointId,
  289. Vectors = new Vectors { Vector = vectorObj },
  290. Payload =
  291. {
  292. ["categorie"] = collectionName,
  293. ["nom_fichier"] = nomFichier,
  294. ["email_object"] = emailObject,
  295. ["nom_chunck"] = nomChunck,
  296. ["niveau_acces"] = niveauAcces,
  297. ["content"] = content,
  298. ["proprietaire"] = proprietaire
  299. }
  300. }
  301. });
  302. }
  303. //public async Task<IReadOnlyList<ScoredPoint>> SearchAsync(Domain domain, float[] queryVector, float seuilScore, int topK = 5, string adresseMail = "")
  304. public async Task<List<SearchResult>> SearchAsync(Domain domain, float[] queryVector, float seuilScore, int topK=5, string adresseMail="")
  305. {
  306. var collectionName = domain.ToCollectionName();
  307. Filter filter = new();
  308. if (domain == Domain.Emails)
  309. {
  310. filter = new Qdrant.Client.Grpc.Filter
  311. {
  312. Must = {
  313. new Condition
  314. {
  315. Field = new FieldCondition
  316. {
  317. Key = "categorie",
  318. Match = new Match { Text = collectionName }
  319. }
  320. },
  321. new Condition
  322. {
  323. Field = new FieldCondition
  324. {
  325. Key = "proprietaire",
  326. Match = new Match { Text = adresseMail }
  327. }
  328. }
  329. }
  330. };
  331. }
  332. else
  333. {
  334. filter = new Qdrant.Client.Grpc.Filter
  335. {
  336. Must = { new Condition
  337. {
  338. Field = new FieldCondition
  339. {
  340. Key = "categorie",
  341. Match = new Match { Text = collectionName }
  342. }
  343. }}
  344. };
  345. }
  346. var rep = await _client.SearchAsync(collectionName, queryVector, filter, null, (ulong)topK);
  347. var filteredChunks = rep
  348. .Where(r => r.Score >= seuilScore)
  349. .ToArray();
  350. //
  351. var resultList = filteredChunks.Select(r => new SearchResult
  352. {
  353. Text = r.Payload["content"].StringValue,
  354. Nom_Fichier = r.Payload["nom_fichier"].StringValue
  355. }).ToList();
  356. return resultList;
  357. //return filteredChunks;
  358. }
  359. #endregion
  360. #region Méthodes privées
  361. private async Task EnsureCollectionAsync(Domain domain)
  362. {
  363. var collectionName = domain.ToCollectionName();
  364. var existing = await _client.ListCollectionsAsync();
  365. if (!existing.Contains(collectionName))
  366. {
  367. await _client.CreateCollectionAsync(collectionName, new VectorParams
  368. {
  369. Size = (ulong)EmbeddingDim,
  370. Distance = DefaultDistance
  371. });
  372. }
  373. }
  374. private async Task DeleteCollectionAsync()
  375. {
  376. var collections = await _client.ListCollectionsAsync();
  377. foreach (var col in collections)
  378. {
  379. await DeleteCollectionAsync(col);
  380. }
  381. }
  382. private async Task DeleteCollectionAsync(Domain domain)
  383. {
  384. var collectionName = domain.ToCollectionName();
  385. await DeleteCollectionAsync(collectionName);
  386. }
  387. private async Task DeleteCollectionAsync(string collectionName)
  388. {
  389. await _client.DeleteCollectionAsync(collectionName);
  390. }
  391. #endregion
  392. }
  393. #endregion