using Services.Models; using System.Diagnostics; using System.Net.Http.Json; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; using ToolsServices; using OllamaService.Models; namespace Services.Ollama { public static class OllamaService { #region Variables private static HttpClient? _HttpClient; private static readonly string NomFichierData = FichiersInternesService.ParamsOllama;// "paramsOllama.txt"; private static List ImagesFullFilename = new(); #endregion #region Méthodes publiques #region InterActions /// /// Vérifie si Ollama est actif /// /// /// public static async Task IsOllamaActif(bool isOnDemand) { var precisionContext = isOnDemand ? "OnDemand" : "Batch"; LoggerService.LogInfo($"OllamaService.IsOllamaActif {precisionContext}"); try { ParametresOllamaService? ollama = LoadParametres(); if (ollama == null) { var msg = "Erreur de chargement des paramètres Ollama."; LoggerService.LogWarning(msg); return false; } var url = ""; if(isOnDemand) url = $"{ollama.Ollama_URL}/api/version"; else url = $"{ollama.Ollama_Batch_URL}/api/version"; _HttpClient = new HttpClient(); _HttpClient.Timeout = TimeSpan.FromSeconds(30); var response = await _HttpClient.GetAsync(url); LoggerService.LogDebug($"OllamaService.IsOllamaActif {precisionContext} : {response.IsSuccessStatusCode}"); return response.IsSuccessStatusCode; } catch(Exception ex) { LoggerService.LogError($"OllamaService.IsOllamaActif {precisionContext} : False --> {ex.Message}"); return false; } } /// /// Appelle l'api Generate de Ollama /// /// /// /// /// /// public static async Task<(bool, string)> GenererAsync(string prompt, string precision, string model, bool isOnDemand) { var precisionContext = isOnDemand ? "OnDemand" : "Batch"; LoggerService.LogInfo($"OllamaService.GenererAsync {precisionContext} : {model}"); string? sReturn = ""; Stopwatch chrono = new Stopwatch(); chrono.Start(); bool isSuccess = true; ParametresOllamaService? ollama = LoadParametres(); if (ollama == null) { var msg = "Erreur de chargement des paramètres Ollama."; LoggerService.LogWarning(msg); return (false, msg); } try { //prompt = "Donne tes réponses dans la langue Française." + "\n" + prompt; prompt = PromptService.GetPrompt(PromptService.ePrompt.OllamaService_PromptSystem) + "\n" + prompt; var requestBody = new { model = model, prompt = TruncatePromptBySentenceAsync(prompt), temperature = isOnDemand ? ollama.Ollama_Temperature : ollama.Ollama_Batch_Temperature, // → 0 : réponses plus précises et conservatrices ; 1 : plus créatives top_p = isOnDemand ? ollama.Ollama_Top_p : ollama.Ollama_Batch_Top_p, // → on considère uniquement les mots parmi les plus probables (limite l’invention) max_tokens = isOnDemand ? ollama.Ollama_Max_tokens : ollama.Ollama_Batch_Max_tokens, // → pour ne pas générer des réponses trop longues stream = false }; var url = isOnDemand ? ollama.Ollama_URL : ollama.Ollama_Batch_URL; int timeOut = isOnDemand ? ollama.Ollama_TimeOut : ollama.Ollama_Batch_TimeOut; url = $"{url}/api/generate"; LoggerService.LogDebug($"Génération Ollama : {precision} - Model : {model} - URL : {url} - Time-out : {timeOut}"); LoggerService.LogDebug($"Prompt : {requestBody.prompt}"); _HttpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(timeOut) }; var response = await _HttpClient.PostAsJsonAsync($"{url}", requestBody); if (!response.IsSuccessStatusCode) { var err = await response.Content.ReadAsStringAsync(); var msg = $"Erreur Ollama({response.StatusCode}) : {err}"; LoggerService.LogWarning(msg); return (false, $"{msg}"); } var json = await response.Content.ReadFromJsonAsync(); sReturn = json!.GetProperty("response")!.GetString(); if (sReturn != null) { sReturn = EpureReponse(sReturn); } LoggerService.LogDebug($"OllamaService.GenerateAsync : {sReturn}"); } catch (HttpRequestException ex) { isSuccess = false; LoggerService.LogError("Erreur HTTP : " + ex.Message); } catch (OperationCanceledException) { isSuccess = false; LoggerService.LogError($"La requête a été annulée après {ollama.Ollama_TimeOut} secondes."); } catch (Exception ex) { isSuccess = false; sReturn = $"Erreur lors de la génération : {ex.Message}"; LoggerService.LogError(sReturn); //return $"{msg}"; } finally { chrono.Stop(); LoggerService.LogInfo($"Temps de la génération : {chrono.ElapsedMilliseconds / 1000} s ({precision})"); } return (isSuccess,$"{sReturn}"); } /// /// Appelle l'api Chat de Ollama /// /// /// /// public static async Task<(bool, string)> ChatAsync(string model, List messages) { LoggerService.LogInfo($"OllamaService.SendMessageAsync : {model}"); Stopwatch chrono = new Stopwatch(); chrono.Start(); string? sReturn = ""; bool isSuccess = true; ParametresOllamaService? ollama = LoadParametres(); if (ollama == null) { var msg = "Erreur de chargement des paramètres Ollama."; LoggerService.LogWarning(msg); return (false, msg); } try { var promptSystem = PromptService.GetPrompt(PromptService.ePrompt.OllamaService_PromptSystem); var systemPrompt = new ChatMessage { Role = "system", Content = promptSystem, Model = model }; var allMessages = new List { systemPrompt }; messages[0].Content = promptSystem + "\n" + messages[0].Content; allMessages.AddRange(messages); var request = new { model = model, messages = allMessages.Select(m => new { role = m.Role, content = m.Content }).ToList(), temperature = ollama.Ollama_Temperature, // → 0 : réponses plus précises et conservatrices ; 1 : plus créatives top_p = ollama.Ollama_Top_p, // → on considère uniquement les mots parmi les plus probables (limite l’invention) max_tokens = ollama.Ollama_Max_tokens, // → pour ne pas générer des réponses trop longues stream = false }; var url = $"{ollama.Ollama_URL}/api/chat"; LoggerService.LogDebug($"Génération Ollama : CHAT - Model : {model} - URL : {url} - Time-out : {ollama.Ollama_TimeOut}"); LoggerService.LogDebug($"Prompt : {request.messages[1].content}"); _HttpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(ollama.Ollama_TimeOut) }; var response = await _HttpClient.PostAsync(url, new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json")); if (!response.IsSuccessStatusCode) { var err = await response.Content.ReadAsStringAsync(); var msg = $"Erreur Ollama({response.StatusCode}) : {err}"; LoggerService.LogWarning(msg); return (false, $"{msg}"); } var result = await response.Content.ReadAsStringAsync(); using var doc = JsonDocument.Parse(result); sReturn = doc.RootElement.GetProperty("message").GetProperty("content").GetString(); if (sReturn != null) { sReturn = EpureReponse(sReturn); } LoggerService.LogDebug($"OllamaService.SendMessageAsync : {sReturn}"); } catch (Exception ex) { isSuccess = false; sReturn = $"Erreur lors de la génération : {ex.Message}"; LoggerService.LogError(sReturn); } finally { chrono.Stop(); LoggerService.LogInfo($"Temps de la génération : {chrono.ElapsedMilliseconds / 1000} s (LLM)"); if (sReturn == null) sReturn = ""; } return (isSuccess, $"{sReturn}"); } /// /// Appelle l'api Generate de Ollama avec envoi d'images dans le prompt /// /// /// /// /// public static async Task<(bool, string)> GenererAsync(string model, string prompt, List imagesFullFilename) { LoggerService.LogInfo($"OllamaService.SendMessageAsync : {model}"); Stopwatch chrono = new Stopwatch(); chrono.Start(); bool isSuccess = true; var imagesBase64 = new List(); ParametresOllamaService? ollama = LoadParametres(); if (ollama == null) { var msg = "Erreur de chargement des paramètres Ollama."; LoggerService.LogWarning(msg); return (false, msg); } foreach (var uneImage in imagesFullFilename) { byte[] imageBytes = File.ReadAllBytes(uneImage); imagesBase64.Add(Convert.ToBase64String(imageBytes)); } var request = new OllamaRequest { Model = model, Prompt = PromptService.GetPrompt(PromptService.ePrompt.OllamaService_PromptSystem) + "\n" + prompt, Temperature = ollama.Ollama_Temperature, // → 0 : réponses plus précises et conservatrices ; 1 : plus créatives Top_p = ollama.Ollama_Top_p, // → on considère uniquement les mots parmi les plus probables (limite l’invention) Max_tokens = ollama.Ollama_Max_tokens, // → pour ne pas générer des réponses trop longues Images = imagesBase64, Stream = false }; string? sReturn = ""; try { var url = $"{ollama.Ollama_URL}/api/generate"; LoggerService.LogDebug($"Prompt envoyé à {url}"); _HttpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(ollama.Ollama_TimeOut) }; var response = await _HttpClient.PostAsJsonAsync(url, request); if (!response.IsSuccessStatusCode) { var err = await response.Content.ReadAsStringAsync(); var msg = $"Erreur Ollama({response.StatusCode}) : {err}"; LoggerService.LogWarning(msg); return (false, $"{msg}"); } var result = await response.Content.ReadFromJsonAsync(); if (result != null) { sReturn = result.Completion.Trim(); sReturn = EpureReponse(sReturn); } LoggerService.LogDebug($"OllamaService.SendMessageAsync : {sReturn}"); } catch (Exception ex) { isSuccess=false; sReturn = $"Erreur lors de la génération : {ex.Message}"; LoggerService.LogError(sReturn); //return sReturn; } finally { chrono.Stop(); LoggerService.LogInfo($"Temps de la génération : {chrono.ElapsedMilliseconds / 1000} s (Interprétation images)"); if (sReturn == null) sReturn = ""; } return (isSuccess, $"{sReturn}"); } #endregion #region Gestion des paramètres public static ParametresOllamaService? LoadParametres() { LoggerService.LogInfo("OllamaService.LoadParametres"); ParametresOllamaService SelectedItem = new(); try { if (File.Exists(NomFichierData)) { string[] lignes = File.ReadAllLines(NomFichierData); if (lignes.Length > 0) { SelectedItem.Ollama_URL = lignes[0]; } if (lignes.Length > 1) { if(int.TryParse(lignes[1], out int timeout)) SelectedItem.Ollama_TimeOut = timeout; } if (lignes.Length > 2) { if (double.TryParse(lignes[2], out double temperature)) SelectedItem.Ollama_Temperature = temperature; } if (lignes.Length > 3) { if (double.TryParse(lignes[3], out double top_p)) SelectedItem.Ollama_Top_p = top_p; } if (lignes.Length > 4) { if (int.TryParse(lignes[4], out int max_tokens)) SelectedItem.Ollama_Max_tokens = max_tokens; } if (lignes.Length > 5) { SelectedItem.Ollama_Batch_URL = lignes[5]; } if (lignes.Length > 6) { if (int.TryParse(lignes[6], out int timeout)) SelectedItem.Ollama_Batch_TimeOut = timeout; } if (lignes.Length > 7) { if (double.TryParse(lignes[7], out double temperature)) SelectedItem.Ollama_Batch_Temperature = temperature; } if (lignes.Length > 8) { if (double.TryParse(lignes[8], out double top_p)) SelectedItem.Ollama_Batch_Top_p = top_p; } if (lignes.Length > 9) { if (int.TryParse(lignes[9], out int max_tokens)) SelectedItem.Ollama_Batch_Max_tokens = max_tokens; } } return SelectedItem; } catch { return null; } } public static bool SaveParametres(ParametresOllamaService selectedItem) { LoggerService.LogInfo("OllamaService.SaveParametres"); try { StringBuilder sb = new(); sb.AppendLine(selectedItem.Ollama_URL); sb.AppendLine(selectedItem.Ollama_TimeOut.ToString()); sb.AppendLine(selectedItem.Ollama_Temperature.ToString()); sb.AppendLine(selectedItem.Ollama_Top_p.ToString()); sb.AppendLine(selectedItem.Ollama_Max_tokens.ToString()); sb.AppendLine(selectedItem.Ollama_Batch_URL); sb.AppendLine(selectedItem.Ollama_Batch_TimeOut.ToString()); sb.AppendLine(selectedItem.Ollama_Batch_Temperature.ToString()); sb.AppendLine(selectedItem.Ollama_Batch_Top_p.ToString()); sb.AppendLine(selectedItem.Ollama_Batch_Max_tokens.ToString()); File.WriteAllText(NomFichierData, sb.ToString()); return true; } catch { return false; } } public static async Task> GetInstalledModelsAsync(bool isOnDemand) { LoggerService.LogInfo("OllamaService.GetInstalledModelsAsync"); try { ParametresOllamaService? ollama = LoadParametres(); if (ollama == null) { return new List { new OllamaModel { Name = "Erreur de chargement des paramètres Ollama." } }; } var url = ""; int timeOut = 360; if (isOnDemand) { url = $"{ollama.Ollama_URL}/api/tags"; timeOut = ollama.Ollama_TimeOut; } else { url = $"{ollama.Ollama_Batch_URL}/api/tags"; timeOut = ollama.Ollama_Batch_TimeOut; } _HttpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(timeOut) }; var response = await _HttpClient.GetAsync($"{url}"); LoggerService.LogDebug($"Requête envoyée à {url}"); response.EnsureSuccessStatusCode(); var json = await response.Content.ReadAsStringAsync(); var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = false }; var result = JsonSerializer.Deserialize(json, options); if (result?.Models == null) { return new List { new OllamaModel { Name = "Erreur de chargement des paramètres Ollama." } }; } return result.Models; } catch (Exception ex) { LoggerService.LogError($"Erreur lors de la récupération des modèles installés : {ex.Message}"); return new List { new OllamaModel { Name = $"Erreur : {ex.Message}" } }; } } public static string GetModeleIA(string useCase) { LoggerService.LogInfo("OllamaService.GetModeleIA"); ModelSelector selector = new(); var models = selector.GetModelsForUseCase(useCase); if(models.ToList().Count>1) return models.First(); return ""; } public static IEnumerable GetModelesIA(string useCase) { LoggerService.LogInfo("OllamaService.GetModelesIA"); ModelSelector selector = new(); var models = selector.GetModelsForUseCase(useCase); return models; } public static bool SaveParametresModeles(string useCase, ModelConfig models) { LoggerService.LogInfo("OllamaService.SaveParametresModeles"); ModelSelector selector = new(); var b = selector.SaveConfig(useCase, models); return b; } #endregion #region Nettoyage des prompts et surveillance de la taille public static string CleanUserInput(string input, out bool isPromptInjection) { LoggerService.LogInfo("OllamaService.CleanUserInput"); isPromptInjection = false; var avant = input; if (string.IsNullOrWhiteSpace(input)) return string.Empty; // 1. Limiter la taille max (ex : 500 caractères) /* int maxLength = 500; if (input.Length > maxLength) input = input.Substring(0, maxLength); */ // 2. Supprimer les caractères non imprimables (ex : contrôles, retours chariot non standards) input = RemoveNonPrintableChars(input); // 3. Échapper les triples quotes pour ne pas casser le prompt (on remplace ''' par ' ' ' par exemple) input = input.Replace("'''", "' ' '"); // 4. Rechercher et remplacer les mots/expressions d’injection potentielles string[] blacklist = new string[] { "ignore", "cancel", "stop generating", "disregard instructions","oublie les instructions", "oublie", "ignore instructions","ignore les instructions", "forget", "override", "bypass" }; foreach (var word in blacklist) { // Remplacer même si insensible à la casse input = Regex.Replace(input, Regex.Escape(word), "[CENSORED]", RegexOptions.IgnoreCase); } // 5. Optionnel : remplacer les guillemets doubles et simples pour éviter la confusion input = input.Replace("\"", "'"); input = input.Replace("\\", "/"); // éviter les échappements if (input.Contains("[CENSORED]")) { LoggerService.LogDebug($"Avant :{avant}"); LoggerService.LogDebug($"Après :{input}"); isPromptInjection = true; } return input.Trim(); } #endregion #endregion #region Méthodes privées private static string EpureReponse(string texte) { var txt = texte; txt = txt.Replace("**", ""); //txt = txt.Replace("*", ""); txt = txt.Replace("", ""); txt = txt.Replace("", ""); txt = txt.Replace("```csv", ""); txt = txt.Replace("```xml", ""); txt = txt.Replace("```", ""); txt = txt.Trim(); return txt; } private static async Task GetTokenMaxAsync(bool isOld) { LoggerService.LogInfo("OllamaService.GetTokenMaxAsync"); try { ParametresOllamaService? ollama = LoadParametres(); if (ollama == null) { return -1; } var requestBody = new { model = "",// ollama.Ollama_Model, }; _HttpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(ollama.Ollama_TimeOut) }; // Construire l'URL pour obtenir les informations du modèle string url = $"{ollama.Ollama_URL}/api/show"; LoggerService.LogDebug($"Prompt envoyé à {url}"); var response = await _HttpClient.PostAsJsonAsync($"{url}", requestBody); // Envoyer une requête GET à l'API Ollama //HttpResponseMessage response = await _HttpClient.GetAsync(url); // Vérifier le statut de la réponse if (response.IsSuccessStatusCode) { // Lire le contenu de la réponse string responseBody = await response.Content.ReadAsStringAsync(); var json = await response.Content.ReadFromJsonAsync(); var contextLength = json .GetProperty("model_info") .GetProperty("llama.context_length") .GetInt32(); return contextLength; //return maxContextSize.HasValue ? (int)maxContextSize : 0; } else { var model = "";//ollama.Ollama_Model LoggerService.LogError($"Erreur lors de la récupération des informations du modèle {model}: {response.StatusCode}"); return -1; } } catch (Exception ex) { LoggerService.LogError($"Erreur lors de la récupération du modèle : {ex.Message}"); return -3; // Erreur lors de la récupération du modèle } } // Supprime les caractères non imprimables private static string RemoveNonPrintableChars(string text) { LoggerService.LogInfo("OllamaService.RemoveNonPrintableChars"); var sb = new StringBuilder(); foreach (char c in text) { // Exclure uniquement les caractères de contrôle ASCII (< 32), sauf les classiques utiles if (!char.IsControl(c) || c == '\n' || c == '\r' || c == '\t') { sb.Append(c); } } return sb.ToString(); } private static int GetTokenMaxAsync() { LoggerService.LogInfo("OllamaService.GetTokenMaxAsync"); try { //await Task.Delay(1); return 4096; } catch (Exception ex) { LoggerService.LogError($"Erreur lors de la récupération du modèle : {ex.Message}"); return -3; // Erreur lors de la récupération du modèle } } private static int CountTokensAsync(string prompt) { LoggerService.LogInfo("OllamaService.CountTokensAsync"); try { // 1 token, en langue francaise, c'est 1.25 mots (environ) int estimatedTokens = prompt.Split(' ').Length * 5 / 4; return estimatedTokens; } catch (Exception ex) { LoggerService.LogError($"Erreur lors de la récupération du modèle : {ex.Message}"); return -1; // Erreur lors de la récupération du modèle } } private static string TruncatePromptBySentenceAsync(string prompt) { LoggerService.LogInfo("OllamaService.TruncatePromptBySentenceAsync"); prompt = prompt.Trim(); LoggerService.LogDebug($"Taille avant : {prompt.Length}"); int reserveReponse = 512 * 3; // Réserve pour la réponse var maxTokens = GetTokenMaxAsync(); var nbrTokens = CountTokensAsync(prompt); // Cas simple : le prompt tient dans la limite // On garde 512 * 3 de réserve pour la réponse if ((nbrTokens + reserveReponse) <= maxTokens) { LoggerService.LogDebug($"Taille après : {prompt.Length}"); return prompt; } // Découpe le texte en phrases (naïvement en utilisant les points) var sentences = prompt.Split(new[] { ". ", "? ", "! " }, StringSplitOptions.RemoveEmptyEntries); StringBuilder builder = new(); foreach (var sentence in sentences) { var next = builder.Length > 0 ? builder + ". " + sentence.Trim() : sentence.Trim(); var tokenCount = CountTokensAsync(next.ToString()); if ((tokenCount + reserveReponse) > maxTokens) { break; // Trop long, on s'arrête } builder.Clear(); builder.Append(next); } var rep = builder.ToString().Trim(); LoggerService.LogDebug($"Taille après : {rep.Length}"); return rep;// + "\n\n[Tronqué pour respecter la limite de contexte]"; } #endregion } }