using System.Diagnostics; using System.Text.Json; using System.Text.RegularExpressions; using AuthorBuddy.Web.Models; namespace AuthorBuddy.Web.Services; public class OllamaBackend : ILLMBackend { private readonly HttpClient _http; private readonly OllamaSettings _settings; public string Name => "Ollama"; public bool SupportsModelManagement => true; public OllamaBackend(OllamaSettings settings) { _settings = settings; _http = new HttpClient { Timeout = TimeSpan.FromSeconds(300) }; } private string Url => _settings.OllamaUrl; public async Task TestConnectionAsync() { try { var sw = Stopwatch.StartNew(); var response = await _http.GetAsync($"{Url}/api/tags"); sw.Stop(); return response.IsSuccessStatusCode; } catch { return false; } } public async Task> GetModelsAsync() { try { var response = await _http.GetAsync($"{Url}/api/tags"); if (!response.IsSuccessStatusCode) return Enumerable.Empty(); var content = await response.Content.ReadAsStringAsync(); using var doc = JsonDocument.Parse(content); if (!doc.RootElement.TryGetProperty("models", out var models)) return Enumerable.Empty(); var list = new List(); foreach (var m in models.EnumerateArray()) list.Add(new LLM_Model(m)); return list; } catch { return Enumerable.Empty(); } } public async Task ChatAsync(string model, string prompt, string? systemPrompt = null, double? temperature = null) { try { var request = new Dictionary { ["model"] = model, ["prompt"] = prompt, ["stream"] = false }; if (!string.IsNullOrEmpty(systemPrompt)) request["system"] = systemPrompt; if (temperature.HasValue) request["temperature"] = Math.Clamp(temperature.Value, 0.0, 2.0); var response = await _http.PostAsJsonAsync($"{Url}/api/generate", request); if (!response.IsSuccessStatusCode) return "Fehler bei der Verarbeitung."; var content = await response.Content.ReadAsStringAsync(); using var doc = JsonDocument.Parse(content); if (doc.RootElement.TryGetProperty("response", out var resp)) return resp.GetString() ?? ""; return "Fehler bei der Verarbeitung."; } catch (Exception ex) { return $"Error: {ex.Message}"; } } public async Task ChatStreamAsync(string model, string prompt, Func onToken, string? systemPrompt = null, double? temperature = null) { var request = new Dictionary { ["model"] = model, ["prompt"] = prompt, ["stream"] = true }; if (!string.IsNullOrEmpty(systemPrompt)) request["system"] = systemPrompt; if (temperature.HasValue) request["temperature"] = Math.Clamp(temperature.Value, 0.0, 2.0); using var response = await _http.PostAsync( $"{Url}/api/generate", new StringContent(JsonSerializer.Serialize(request), System.Text.Encoding.UTF8, "application/json") ); if (!response.IsSuccessStatusCode) return; using var stream = await response.Content.ReadAsStreamAsync(); using var reader = new StreamReader(stream); string? line; while ((line = await reader.ReadLineAsync()) != null) { var text = ExtractResponseText(line); if (!string.IsNullOrEmpty(text)) await onToken(text); } } private string ExtractResponseText(string jsonLine) { if (string.IsNullOrWhiteSpace(jsonLine)) return string.Empty; try { using var doc = JsonDocument.Parse(jsonLine); if (doc.RootElement.TryGetProperty("response", out var resp)) return resp.GetString() ?? string.Empty; } catch { } return string.Empty; } public async Task GetEmbeddingAsync(string text, string? model = null) { try { var m = model ?? _settings.EmbedModel; var request = new { model = m, prompt = text }; var response = await _http.PostAsJsonAsync($"{Url}/api/embeddings", request); if (!response.IsSuccessStatusCode) return Array.Empty(); var content = await response.Content.ReadAsStringAsync(); using var doc = JsonDocument.Parse(content); if (!doc.RootElement.TryGetProperty("embedding", out var emb)) return Array.Empty(); var list = new List(); foreach (var v in emb.EnumerateArray()) list.Add(v.GetSingle()); return list.ToArray(); } catch { return Array.Empty(); } } public async Task PullModelAsync(string name, IProgress? progress = null) { try { var request = new { name, stream = true }; var response = await _http.PostAsync( $"{Url}/api/pull", new StringContent(JsonSerializer.Serialize(request), System.Text.Encoding.UTF8, "application/json") ); if (!response.IsSuccessStatusCode) return false; using var stream = await response.Content.ReadAsStreamAsync(); using var reader = new StreamReader(stream); string? line; while ((line = await reader.ReadLineAsync()) != null) { if (string.IsNullOrWhiteSpace(line)) continue; try { using var doc = JsonDocument.Parse(line); var root = doc.RootElement; if (root.TryGetProperty("status", out var status)) { var st = status.GetString() ?? ""; var msg = st; if (root.TryGetProperty("completed", out var c) && root.TryGetProperty("total", out var t)) { var comp = c.GetInt64(); var tot = t.GetInt64(); if (tot > 0) msg = $"{st}: {comp}/{tot} ({(int)(comp * 100 / tot)}%)"; } progress?.Report(msg); if (st == "success") return true; } } catch { } } return false; } catch { return false; } } public async Task DeleteModelAsync(string name) { try { var req = new HttpRequestMessage(HttpMethod.Delete, $"{Url}/api/delete") { Content = new StringContent(JsonSerializer.Serialize(new { name }), System.Text.Encoding.UTF8, "application/json") }; var response = await _http.SendAsync(req); return response.IsSuccessStatusCode; } catch { return false; } } public async Task GetContextSizeAsync(string model) { try { var response = await _http.PostAsJsonAsync($"{Url}/api/show", new { model }); if (!response.IsSuccessStatusCode) return null; var content = await response.Content.ReadAsStringAsync(); using var doc = JsonDocument.Parse(content); if (doc.RootElement.TryGetProperty("modelfile", out var mf)) { var modelfile = mf.GetString() ?? ""; var match = Regex.Match(modelfile, @"num_ctx\s+(\d+)"); if (match.Success && int.TryParse(match.Groups[1].Value, out var ctx)) return ctx; } return 2048; } catch { return null; } } public async Task MeasureLatencyAsync(string model) { try { var sw = Stopwatch.StartNew(); var request = new { model, prompt = "Hallo", stream = false }; var response = await _http.PostAsJsonAsync($"{Url}/api/generate", request); sw.Stop(); return response.IsSuccessStatusCode ? sw.ElapsedMilliseconds : -1; } catch { return -1; } } public async Task ModelExistsAsync(string model) { if (string.IsNullOrWhiteSpace(model)) return false; try { var models = await GetModelsAsync(); return models.Any(m => m.Name == model || m.Name.StartsWith(model.Split(':')[0])); } catch { return false; } } public async Task<(bool Success, int ContextWindow)> GetModelContextWindowAsync(string model) { var ctx = await GetContextSizeAsync(model); return (ctx.HasValue, ctx ?? 0); } public bool TestEmbedding(string model) { try { var request = new { model, prompt = "test" }; var response = _http.PostAsJsonAsync($"{Url}/api/embeddings", request).GetAwaiter().GetResult(); if (!response.IsSuccessStatusCode) return false; var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); using var doc = JsonDocument.Parse(content); if (doc.RootElement.TryGetProperty("embedding", out var emb)) return emb.EnumerateArray().Any(); return false; } catch { return false; } } }