using System.Diagnostics; using AuthorBuddy.Web.Models; namespace AuthorBuddy.Web.Services; public class OllamaService : IOllamaService { private readonly IEnumerable _backends; private readonly OllamaSettings _settings; private readonly OperationState _opState; private readonly object _startLock = new(); private bool _startAttempted; public OllamaService(IEnumerable backends, OllamaSettings settings, OperationState opState) { _backends = backends; _settings = settings; _opState = opState; } private readonly object _warmLock = new(); private bool _warmupAttempted; private ILLMBackend GetBackend(string? name) => _backends.FirstOrDefault(b => b.Name == (name ?? _settings.Backend)) ?? _backends.First(); private async Task EnsureRunningAsync(string? backendName = null) { var backend = GetBackend(backendName); if (backend.Name != "Ollama") return; var reachable = await backend.TestConnectionAsync(); if (reachable) { await WarmupModelAsync(); return; } lock (_startLock) { if (_startAttempted) return; _startAttempted = true; } _opState.SetThought("Ollama läuft nicht – starte ollama serve..."); try { var psi = new ProcessStartInfo("ollama", "serve") { CreateNoWindow = true, UseShellExecute = false }; Process.Start(psi); } catch (Exception ex) { _opState.AppendThought($" Fehler: {ex.Message}"); return; } for (int i = 0; i < 30; i++) { await Task.Delay(1000); reachable = await backend.TestConnectionAsync(); if (reachable) { _opState.AppendThought(" Ollama ist bereit"); await WarmupModelAsync(); return; } _opState.AppendThought("."); } _opState.AppendThought(" Zeitüberschreitung"); } private async Task WarmupModelAsync() { lock (_warmLock) { if (_warmupAttempted) return; _warmupAttempted = true; } var modelsToWarm = new[] { (Model: _settings.StyleModel, Backend: _settings.StyleBackend), (Model: _settings.EmbedModel, Backend: _settings.RagBackend) } .Where(m => !string.IsNullOrWhiteSpace(m.Model)) .DistinctBy(m => m.Model) .ToList(); if (modelsToWarm.Count == 0) return; _opState.SetThought("Lade Modelle in den Arbeitsspeicher..."); foreach (var (model, backendName) in modelsToWarm) { var backend = GetBackend(backendName); if (backend.Name != "Ollama") continue; _opState.AppendThought($" {model} ({backendName})..."); try { var dummy = await backend.ChatAsync(model, " ", null, 0.0); _opState.AppendThought(" ✓"); } catch (Exception ex) { _opState.AppendThought($" ✗ ({ex.Message})"); } } _opState.AppendThought(" Fertig"); } public void Reconfigure(string url) { _settings.OllamaUrl = url; } public async Task<(bool Success, long LatencyMs)> TestConnectionAsync() { var backend = GetBackend(null); await EnsureRunningAsync(); _opState.SetThought($"Teste Verbindung zu {backend.Name}..."); var sw = Stopwatch.StartNew(); var result = await backend.TestConnectionAsync(); sw.Stop(); _opState.AppendThought(result ? " OK" : " Fehlgeschlagen"); return (result, result ? sw.ElapsedMilliseconds : 0); } public bool SupportsModelManagement => GetBackend(null).SupportsModelManagement; public async Task> GetLocalModelsAsync() { var backend = GetBackend(null); await EnsureRunningAsync(); _opState.SetThought($"Lade Modelle von {backend.Name}..."); var models = await backend.GetModelsAsync(); _opState.AppendThought($" {models.Count()} gefunden"); return models; } public async Task> GetModelsForBackendAsync(string backendName) { var backend = GetBackend(backendName); if (backend.Name == "Ollama") await EnsureRunningAsync(); _opState.SetThought($"Lade Modelle von {backend.Name}..."); var models = await backend.GetModelsAsync(); _opState.AppendThought($" {models.Count()} gefunden"); return models; } public async Task TestEmbeddingAsync(string model, string? backendName = null) { var backend = GetBackend(backendName); await EnsureRunningAsync(backendName); return backend.TestEmbedding(model); } public async Task GetEmbeddingAsync(string text, string? backendName = null) { var backend = GetBackend(backendName); _opState.SetBusy(true); _opState.SetThought($"{backend.Name}: Erzeuge Embeddings..."); try { await EnsureRunningAsync(backendName); return await backend.GetEmbeddingAsync(text, _settings.EmbedModel); } catch { _opState.SetThought("Fehler bei Embeddings"); throw; } finally { _opState.SetBusy(false); } } public async Task QuickSpellCheckAsync(string text) { _opState.SetBusy(true); _opState.SetThought("Rechtschreibprüfung läuft..."); try { await EnsureRunningAsync(_settings.SpellingBackend); return await Chat(_settings.SpellingModel, text, _settings.SpellingPrompt, _settings.SpellingTemp, _settings.SpellingBackend); } catch { _opState.SetThought("Fehler bei Rechtschreibprüfung"); throw; } finally { _opState.SetBusy(false); } } public async Task StreamStyleCheckAsync(string text) { _opState.SetBusy(true); _opState.SetThought("Stilprüfung läuft..."); try { await EnsureRunningAsync(_settings.StyleBackend); return await Chat(_settings.StyleModel, text, _settings.LektorSystemPrompt, _settings.LektorTemp, _settings.StyleBackend); } catch { _opState.SetThought("Fehler bei Stilprüfung"); throw; } finally { _opState.SetBusy(false); } } public async Task AnswerBasedOnFactsAsync(string question, string facts) { _opState.SetBusy(true); _opState.SetThought("Faktencheck läuft..."); try { await EnsureRunningAsync(_settings.RagBackend); var prompt = _settings.GetFactCheckPrompt(question, facts); return await Chat(_settings.EmbedModel, prompt, temperature: _settings.FactCheckTemp, backendName: _settings.RagBackend); } catch { _opState.SetThought("Fehler bei Faktencheck"); throw; } finally { _opState.SetBusy(false); } } public async Task ModelExistsAsync(string model, string? backendName = null) { var backend = GetBackend(backendName); await EnsureRunningAsync(backendName); return await backend.ModelExistsAsync(model); } public async Task<(bool Success, int ContextWindow)> GetModelContextWindowAsync(string model, string? backendName = null) { var backend = GetBackend(backendName); await EnsureRunningAsync(backendName); return await backend.GetModelContextWindowAsync(model); } public async Task MeasureLatencyAsync(string model, string? backendName = null) { var backend = GetBackend(backendName); await EnsureRunningAsync(backendName); return await backend.MeasureLatencyAsync(model); } public async Task Chat(string model, string userMessage, string? systemPrompt = null, double? temperature = null, string? backendName = null) { var backend = GetBackend(backendName); _opState.SetBusy(true); _opState.SetThought($"{backend.Name}: Sende Anfrage an {model}..."); try { await EnsureRunningAsync(backendName); var result = await backend.ChatAsync(model, userMessage, systemPrompt, temperature); _opState.AppendThought(" Fertig"); return result; } catch { _opState.SetThought($"Fehler bei {model}"); throw; } finally { _opState.SetBusy(false); } } public async Task ChatStream(string model, string userMessage, Func onToken, string? systemPrompt = null, double? temperature = null, string? backendName = null) { var backend = GetBackend(backendName); _opState.SetBusy(true); _opState.SetThought($"{backend.Name}: Verbinde mit Modell {model}..."); try { await EnsureRunningAsync(backendName); int tokenCount = 0; await backend.ChatStreamAsync(model, userMessage, async chunk => { tokenCount++; _opState.AppendThought(chunk); await onToken(chunk); }, systemPrompt, temperature); _opState.AppendThought($" Fertig ({tokenCount} Token)"); } catch { _opState.SetThought($"Fehler bei {model}"); throw; } finally { _opState.SetBusy(false); } } public async Task PullModelAsync(string modelName, IProgress progress) { var backend = GetBackend(null); await EnsureRunningAsync(); _opState.SetThought($"Lade Modell {modelName}..."); var result = await backend.PullModelAsync(modelName, progress); _opState.AppendThought(result ? " Erfolgreich" : " Fehlgeschlagen"); return result; } public async Task DeleteModelAsync(string modelName) { var backend = GetBackend(null); await EnsureRunningAsync(); _opState.SetThought($"Lösche Modell {modelName}..."); var result = await backend.DeleteModelAsync(modelName); _opState.AppendThought(result ? " Erfolgreich" : " Fehlgeschlagen"); return result; } }