AuthorBuddy/Services/OllamaService.cs
2026-07-04 10:32:35 +02:00

227 lines
7.3 KiB
C#
Raw Permalink 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 ILLMBackend ActiveBackend =>
_backends.FirstOrDefault(b => b.Name == _settings.Backend) ?? _backends.First();
private readonly object _warmLock = new();
private bool _warmupAttempted;
private async Task EnsureRunningAsync()
{
if (ActiveBackend.Name != "Ollama")
return;
var reachable = await ActiveBackend.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 ActiveBackend.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[] { _settings.StyleModel, _settings.EmbedModel }
.Where(m => !string.IsNullOrWhiteSpace(m))
.Distinct()
.ToList();
if (modelsToWarm.Count == 0) return;
_opState.SetThought("Lade Modelle in den Arbeitsspeicher...");
foreach (var model in modelsToWarm)
{
_opState.AppendThought($" {model}...");
try
{
var warmCts = new CancellationTokenSource(TimeSpan.FromMinutes(3));
var dummy = await ActiveBackend.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()
{
await EnsureRunningAsync();
_opState.SetThought($"Teste Verbindung zu {ActiveBackend.Name}...");
var sw = Stopwatch.StartNew();
var result = await ActiveBackend.TestConnectionAsync();
sw.Stop();
_opState.AppendThought(result ? " OK" : " Fehlgeschlagen");
return (result, result ? sw.ElapsedMilliseconds : 0);
}
public async Task<IEnumerable<LLM_Model>> GetLocalModelsAsync()
{
await EnsureRunningAsync();
_opState.SetThought($"Lade Modelle von {ActiveBackend.Name}...");
var models = await ActiveBackend.GetModelsAsync();
_opState.AppendThought($" {models.Count()} gefunden");
return models;
}
public async Task<bool> TestEmbeddingAsync(string model)
{
await EnsureRunningAsync();
return ActiveBackend.TestEmbedding(model);
}
public async Task<float[]> GetEmbeddingAsync(string text)
{
await EnsureRunningAsync();
return await ActiveBackend.GetEmbeddingAsync(text, _settings.EmbedModel);
}
public async Task<string> QuickSpellCheckAsync(string text)
{
await EnsureRunningAsync();
return await Chat(_settings.SpellingModel, text, _settings.SpellingPrompt, _settings.SpellingTemp);
}
public async Task<string> StreamStyleCheckAsync(string text)
{
await EnsureRunningAsync();
return await Chat(_settings.StyleModel, text, _settings.LektorSystemPrompt, _settings.LektorTemp);
}
public async Task<string> AnswerBasedOnFactsAsync(string question, string facts)
{
await EnsureRunningAsync();
var prompt = _settings.GetFactCheckPrompt(question, facts);
return await Chat(_settings.EmbedModel, prompt, temperature: _settings.FactCheckTemp);
}
public async Task<bool> ModelExistsAsync(string model)
{
await EnsureRunningAsync();
return await ActiveBackend.ModelExistsAsync(model);
}
public async Task<(bool Success, int ContextWindow)> GetModelContextWindowAsync(string model)
{
await EnsureRunningAsync();
return await ActiveBackend.GetModelContextWindowAsync(model);
}
public async Task<long> MeasureLatencyAsync(string model)
{
await EnsureRunningAsync();
return await ActiveBackend.MeasureLatencyAsync(model);
}
public async Task<string> Chat(string model, string userMessage, string? systemPrompt = null, double? temperature = null)
{
await EnsureRunningAsync();
_opState.SetThought($"{ActiveBackend.Name}: Sende Anfrage an {model}...");
var result = await ActiveBackend.ChatAsync(model, userMessage, systemPrompt, temperature);
_opState.AppendThought(" Fertig");
return result;
}
public async Task ChatStream(string model, string userMessage, Func<string, Task> onToken, string? systemPrompt = null, double? temperature = null)
{
await EnsureRunningAsync();
_opState.SetThought($"{ActiveBackend.Name}: Verbinde mit Modell {model}...");
int tokenCount = 0;
await ActiveBackend.ChatStreamAsync(model, userMessage, async chunk =>
{
tokenCount++;
_opState.AppendThought(chunk);
await onToken(chunk);
}, systemPrompt, temperature);
_opState.AppendThought($" Fertig ({tokenCount} Token)");
}
public async Task<bool> PullModelAsync(string modelName, IProgress<string> progress)
{
await EnsureRunningAsync();
_opState.SetThought($"Lade Modell {modelName}...");
var result = await ActiveBackend.PullModelAsync(modelName, progress);
_opState.AppendThought(result ? " Erfolgreich" : " Fehlgeschlagen");
return result;
}
public async Task<bool> DeleteModelAsync(string modelName)
{
await EnsureRunningAsync();
_opState.SetThought($"Lösche Modell {modelName}...");
var result = await ActiveBackend.DeleteModelAsync(modelName);
_opState.AppendThought(result ? " Erfolgreich" : " Fehlgeschlagen");
return result;
}
}