2026-06-21 07:52:03 +02:00
|
|
|
|
using System.Diagnostics;
|
2026-05-14 12:31:35 +02:00
|
|
|
|
using AuthorBuddy.Web.Models;
|
|
|
|
|
|
|
|
|
|
|
|
namespace AuthorBuddy.Web.Services;
|
|
|
|
|
|
|
2026-05-16 16:13:35 +02:00
|
|
|
|
public class OllamaService : IOllamaService
|
2026-05-14 12:31:35 +02:00
|
|
|
|
{
|
2026-05-31 19:31:27 +02:00
|
|
|
|
private readonly IEnumerable<ILLMBackend> _backends;
|
2026-05-17 07:27:18 +02:00
|
|
|
|
private readonly OllamaSettings _settings;
|
2026-05-31 19:31:27 +02:00
|
|
|
|
private readonly OperationState _opState;
|
2026-06-21 07:52:03 +02:00
|
|
|
|
private readonly object _startLock = new();
|
|
|
|
|
|
private bool _startAttempted;
|
2026-05-14 12:31:35 +02:00
|
|
|
|
|
2026-05-31 19:31:27 +02:00
|
|
|
|
public OllamaService(IEnumerable<ILLMBackend> backends, OllamaSettings settings, OperationState opState)
|
2026-05-14 12:31:35 +02:00
|
|
|
|
{
|
2026-05-31 19:31:27 +02:00
|
|
|
|
_backends = backends;
|
2026-05-17 07:27:18 +02:00
|
|
|
|
_settings = settings;
|
2026-05-31 19:31:27 +02:00
|
|
|
|
_opState = opState;
|
2026-05-14 12:31:35 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-04 10:32:35 +02:00
|
|
|
|
private readonly object _warmLock = new();
|
|
|
|
|
|
private bool _warmupAttempted;
|
|
|
|
|
|
|
2026-08-30 08:11:01 +02:00
|
|
|
|
private ILLMBackend GetBackend(string? name) =>
|
|
|
|
|
|
_backends.FirstOrDefault(b => b.Name == (name ?? _settings.Backend)) ?? _backends.First();
|
|
|
|
|
|
|
|
|
|
|
|
private async Task EnsureRunningAsync(string? backendName = null)
|
2026-06-21 07:52:03 +02:00
|
|
|
|
{
|
2026-08-30 08:11:01 +02:00
|
|
|
|
var backend = GetBackend(backendName);
|
|
|
|
|
|
if (backend.Name != "Ollama")
|
2026-06-21 07:52:03 +02:00
|
|
|
|
return;
|
|
|
|
|
|
|
2026-08-30 08:11:01 +02:00
|
|
|
|
var reachable = await backend.TestConnectionAsync();
|
2026-06-21 07:52:03 +02:00
|
|
|
|
if (reachable)
|
2026-07-04 10:32:35 +02:00
|
|
|
|
{
|
|
|
|
|
|
await WarmupModelAsync();
|
2026-06-21 07:52:03 +02:00
|
|
|
|
return;
|
2026-07-04 10:32:35 +02:00
|
|
|
|
}
|
2026-06-21 07:52:03 +02:00
|
|
|
|
|
|
|
|
|
|
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);
|
2026-08-30 08:11:01 +02:00
|
|
|
|
reachable = await backend.TestConnectionAsync();
|
2026-06-21 07:52:03 +02:00
|
|
|
|
if (reachable)
|
|
|
|
|
|
{
|
|
|
|
|
|
_opState.AppendThought(" Ollama ist bereit");
|
2026-07-04 10:32:35 +02:00
|
|
|
|
await WarmupModelAsync();
|
2026-06-21 07:52:03 +02:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
_opState.AppendThought(".");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_opState.AppendThought(" Zeitüberschreitung");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-04 10:32:35 +02:00
|
|
|
|
private async Task WarmupModelAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
lock (_warmLock)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_warmupAttempted) return;
|
|
|
|
|
|
_warmupAttempted = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-30 08:11:01 +02:00
|
|
|
|
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)
|
2026-07-04 10:32:35 +02:00
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
|
|
if (modelsToWarm.Count == 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
_opState.SetThought("Lade Modelle in den Arbeitsspeicher...");
|
2026-08-30 08:11:01 +02:00
|
|
|
|
foreach (var (model, backendName) in modelsToWarm)
|
2026-07-04 10:32:35 +02:00
|
|
|
|
{
|
2026-08-30 08:11:01 +02:00
|
|
|
|
var backend = GetBackend(backendName);
|
|
|
|
|
|
if (backend.Name != "Ollama")
|
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
|
|
_opState.AppendThought($" {model} ({backendName})...");
|
2026-07-04 10:32:35 +02:00
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-08-30 08:11:01 +02:00
|
|
|
|
var dummy = await backend.ChatAsync(model, " ", null, 0.0);
|
2026-07-04 10:32:35 +02:00
|
|
|
|
_opState.AppendThought(" ✓");
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
_opState.AppendThought($" ✗ ({ex.Message})");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
_opState.AppendThought(" Fertig");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-16 16:13:35 +02:00
|
|
|
|
public void Reconfigure(string url)
|
2026-05-14 12:31:35 +02:00
|
|
|
|
{
|
2026-05-31 19:31:27 +02:00
|
|
|
|
_settings.OllamaUrl = url;
|
2026-05-14 12:31:35 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-31 19:31:27 +02:00
|
|
|
|
public async Task<(bool Success, long LatencyMs)> TestConnectionAsync()
|
2026-05-20 06:12:54 +02:00
|
|
|
|
{
|
2026-08-30 08:11:01 +02:00
|
|
|
|
var backend = GetBackend(null);
|
2026-06-21 07:52:03 +02:00
|
|
|
|
await EnsureRunningAsync();
|
2026-08-30 08:11:01 +02:00
|
|
|
|
_opState.SetThought($"Teste Verbindung zu {backend.Name}...");
|
2026-06-21 07:52:03 +02:00
|
|
|
|
var sw = Stopwatch.StartNew();
|
2026-08-30 08:11:01 +02:00
|
|
|
|
var result = await backend.TestConnectionAsync();
|
2026-05-31 19:31:27 +02:00
|
|
|
|
sw.Stop();
|
|
|
|
|
|
_opState.AppendThought(result ? " OK" : " Fehlgeschlagen");
|
|
|
|
|
|
return (result, result ? sw.ElapsedMilliseconds : 0);
|
2026-05-20 06:12:54 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-30 08:11:01 +02:00
|
|
|
|
public bool SupportsModelManagement => GetBackend(null).SupportsModelManagement;
|
|
|
|
|
|
|
2026-05-31 19:31:27 +02:00
|
|
|
|
public async Task<IEnumerable<LLM_Model>> GetLocalModelsAsync()
|
2026-05-20 06:12:54 +02:00
|
|
|
|
{
|
2026-08-30 08:11:01 +02:00
|
|
|
|
var backend = GetBackend(null);
|
2026-06-21 07:52:03 +02:00
|
|
|
|
await EnsureRunningAsync();
|
2026-08-30 08:11:01 +02:00
|
|
|
|
_opState.SetThought($"Lade Modelle von {backend.Name}...");
|
|
|
|
|
|
var models = await backend.GetModelsAsync();
|
2026-05-31 19:31:27 +02:00
|
|
|
|
_opState.AppendThought($" {models.Count()} gefunden");
|
|
|
|
|
|
return models;
|
2026-05-20 06:12:54 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-30 08:11:01 +02:00
|
|
|
|
public async Task<IEnumerable<LLM_Model>> GetModelsForBackendAsync(string backendName)
|
2026-05-14 12:31:35 +02:00
|
|
|
|
{
|
2026-08-30 08:11:01 +02:00
|
|
|
|
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;
|
2026-05-31 19:31:27 +02:00
|
|
|
|
}
|
2026-05-16 16:13:35 +02:00
|
|
|
|
|
2026-08-30 08:11:01 +02:00
|
|
|
|
public async Task<bool> TestEmbeddingAsync(string model, string? backendName = null)
|
2026-05-31 19:31:27 +02:00
|
|
|
|
{
|
2026-08-30 08:11:01 +02:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-05-14 12:31:35 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-17 07:27:18 +02:00
|
|
|
|
public async Task<string> QuickSpellCheckAsync(string text)
|
|
|
|
|
|
{
|
2026-08-30 08:11:01 +02:00
|
|
|
|
_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);
|
|
|
|
|
|
}
|
2026-05-17 07:27:18 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task<string> StreamStyleCheckAsync(string text)
|
|
|
|
|
|
{
|
2026-08-30 08:11:01 +02:00
|
|
|
|
_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);
|
|
|
|
|
|
}
|
2026-05-17 07:27:18 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task<string> AnswerBasedOnFactsAsync(string question, string facts)
|
|
|
|
|
|
{
|
2026-08-30 08:11:01 +02:00
|
|
|
|
_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);
|
|
|
|
|
|
}
|
2026-05-17 07:27:18 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-30 08:11:01 +02:00
|
|
|
|
public async Task<bool> ModelExistsAsync(string model, string? backendName = null)
|
2026-05-14 12:31:35 +02:00
|
|
|
|
{
|
2026-08-30 08:11:01 +02:00
|
|
|
|
var backend = GetBackend(backendName);
|
|
|
|
|
|
await EnsureRunningAsync(backendName);
|
|
|
|
|
|
return await backend.ModelExistsAsync(model);
|
2026-05-14 12:31:35 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-30 08:11:01 +02:00
|
|
|
|
public async Task<(bool Success, int ContextWindow)> GetModelContextWindowAsync(string model, string? backendName = null)
|
2026-05-14 12:31:35 +02:00
|
|
|
|
{
|
2026-08-30 08:11:01 +02:00
|
|
|
|
var backend = GetBackend(backendName);
|
|
|
|
|
|
await EnsureRunningAsync(backendName);
|
|
|
|
|
|
return await backend.GetModelContextWindowAsync(model);
|
2026-05-14 12:31:35 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-30 08:11:01 +02:00
|
|
|
|
public async Task<long> MeasureLatencyAsync(string model, string? backendName = null)
|
2026-05-14 12:31:35 +02:00
|
|
|
|
{
|
2026-08-30 08:11:01 +02:00
|
|
|
|
var backend = GetBackend(backendName);
|
|
|
|
|
|
await EnsureRunningAsync(backendName);
|
|
|
|
|
|
return await backend.MeasureLatencyAsync(model);
|
2026-05-31 19:31:27 +02:00
|
|
|
|
}
|
2026-05-16 16:13:35 +02:00
|
|
|
|
|
2026-08-30 08:11:01 +02:00
|
|
|
|
public async Task<string> Chat(string model, string userMessage, string? systemPrompt = null, double? temperature = null, string? backendName = null)
|
2026-05-31 19:31:27 +02:00
|
|
|
|
{
|
2026-08-30 08:11:01 +02:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-05-14 12:31:35 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-30 08:11:01 +02:00
|
|
|
|
public async Task ChatStream(string model, string userMessage, Func<string, Task> onToken, string? systemPrompt = null, double? temperature = null, string? backendName = null)
|
2026-05-14 12:31:35 +02:00
|
|
|
|
{
|
2026-08-30 08:11:01 +02:00
|
|
|
|
var backend = GetBackend(backendName);
|
|
|
|
|
|
_opState.SetBusy(true);
|
|
|
|
|
|
_opState.SetThought($"{backend.Name}: Verbinde mit Modell {model}...");
|
|
|
|
|
|
try
|
2026-05-16 16:13:35 +02:00
|
|
|
|
{
|
2026-08-30 08:11:01 +02:00
|
|
|
|
await EnsureRunningAsync(backendName);
|
|
|
|
|
|
|
|
|
|
|
|
int tokenCount = 0;
|
|
|
|
|
|
await backend.ChatStreamAsync(model, userMessage, async chunk =>
|
|
|
|
|
|
{
|
|
|
|
|
|
tokenCount++;
|
|
|
|
|
|
_opState.AppendThought(chunk);
|
|
|
|
|
|
await onToken(chunk);
|
|
|
|
|
|
}, systemPrompt, temperature);
|
2026-05-31 19:31:27 +02:00
|
|
|
|
|
2026-08-30 08:11:01 +02:00
|
|
|
|
_opState.AppendThought($" Fertig ({tokenCount} Token)");
|
|
|
|
|
|
}
|
|
|
|
|
|
catch
|
|
|
|
|
|
{
|
|
|
|
|
|
_opState.SetThought($"Fehler bei {model}");
|
|
|
|
|
|
throw;
|
|
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
_opState.SetBusy(false);
|
|
|
|
|
|
}
|
2026-05-14 12:31:35 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-16 16:13:35 +02:00
|
|
|
|
public async Task<bool> PullModelAsync(string modelName, IProgress<string> progress)
|
2026-05-14 12:31:35 +02:00
|
|
|
|
{
|
2026-08-30 08:11:01 +02:00
|
|
|
|
var backend = GetBackend(null);
|
2026-06-21 07:52:03 +02:00
|
|
|
|
await EnsureRunningAsync();
|
2026-05-31 19:31:27 +02:00
|
|
|
|
_opState.SetThought($"Lade Modell {modelName}...");
|
2026-08-30 08:11:01 +02:00
|
|
|
|
var result = await backend.PullModelAsync(modelName, progress);
|
2026-05-31 19:31:27 +02:00
|
|
|
|
_opState.AppendThought(result ? " Erfolgreich" : " Fehlgeschlagen");
|
|
|
|
|
|
return result;
|
2026-05-14 12:31:35 +02:00
|
|
|
|
}
|
2026-07-04 10:32:35 +02:00
|
|
|
|
|
|
|
|
|
|
public async Task<bool> DeleteModelAsync(string modelName)
|
|
|
|
|
|
{
|
2026-08-30 08:11:01 +02:00
|
|
|
|
var backend = GetBackend(null);
|
2026-07-04 10:32:35 +02:00
|
|
|
|
await EnsureRunningAsync();
|
|
|
|
|
|
_opState.SetThought($"Lösche Modell {modelName}...");
|
2026-08-30 08:11:01 +02:00
|
|
|
|
var result = await backend.DeleteModelAsync(modelName);
|
2026-07-04 10:32:35 +02:00
|
|
|
|
_opState.AppendThought(result ? " Erfolgreich" : " Fehlgeschlagen");
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
2026-05-14 12:31:35 +02:00
|
|
|
|
}
|