AuthorBuddy/Services/OllamaService.cs
2026-08-30 08:11:01 +02:00

341 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Diagnostics;
using AuthorBuddy.Web.Models;
namespace AuthorBuddy.Web.Services;
public class OllamaService : IOllamaService
{
private readonly IEnumerable<ILLMBackend> _backends;
private readonly OllamaSettings _settings;
private readonly OperationState _opState;
private readonly object _startLock = new();
private bool _startAttempted;
public OllamaService(IEnumerable<ILLMBackend> 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<IEnumerable<LLM_Model>> 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<IEnumerable<LLM_Model>> 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<bool> TestEmbeddingAsync(string model, string? backendName = null)
{
var backend = GetBackend(backendName);
await EnsureRunningAsync(backendName);
return backend.TestEmbedding(model);
}
public async Task<float[]> 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<string> 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<string> 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<string> 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<bool> 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<long> MeasureLatencyAsync(string model, string? backendName = null)
{
var backend = GetBackend(backendName);
await EnsureRunningAsync(backendName);
return await backend.MeasureLatencyAsync(model);
}
public async Task<string> 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<string, Task> 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<bool> PullModelAsync(string modelName, IProgress<string> 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<bool> 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;
}
}