236 lines
8.5 KiB
C#
236 lines
8.5 KiB
C#
using System.Text.Json;
|
|
using AuthorBuddy.Web.Models;
|
|
|
|
namespace AuthorBuddy.Web.Services;
|
|
|
|
public class LlamaCppBackend : ILLMBackend
|
|
{
|
|
private readonly HttpClient _http;
|
|
private readonly OllamaSettings _settings;
|
|
|
|
public string Name => "llama.cpp";
|
|
public bool SupportsModelManagement => false;
|
|
|
|
public LlamaCppBackend(OllamaSettings settings)
|
|
{
|
|
_settings = settings;
|
|
_http = new HttpClient { Timeout = TimeSpan.FromSeconds(300) };
|
|
}
|
|
|
|
private string Url => _settings.LlamaCppUrl;
|
|
|
|
public async Task<bool> TestConnectionAsync()
|
|
{
|
|
try
|
|
{
|
|
var response = await _http.GetAsync($"{Url}/health");
|
|
return response.IsSuccessStatusCode;
|
|
}
|
|
catch { return false; }
|
|
}
|
|
|
|
public async Task<IEnumerable<LLM_Model>> GetModelsAsync()
|
|
{
|
|
try
|
|
{
|
|
var response = await _http.GetAsync($"{Url}/v1/models");
|
|
if (!response.IsSuccessStatusCode)
|
|
return new List<LLM_Model> { new LLM_Model("llama.cpp") };
|
|
|
|
var content = await response.Content.ReadAsStringAsync();
|
|
using var doc = JsonDocument.Parse(content);
|
|
if (!doc.RootElement.TryGetProperty("data", out var data))
|
|
return new List<LLM_Model> { new LLM_Model("llama.cpp") };
|
|
|
|
var list = new List<LLM_Model>();
|
|
foreach (var m in data.EnumerateArray())
|
|
{
|
|
var id = m.TryGetProperty("id", out var idProp) ? idProp.GetString() ?? "llama.cpp" : "llama.cpp";
|
|
list.Add(new LLM_Model(id));
|
|
}
|
|
return list;
|
|
}
|
|
catch { return new List<LLM_Model> { new LLM_Model("llama.cpp") }; }
|
|
}
|
|
|
|
public async Task<string> ChatAsync(string model, string prompt, string? systemPrompt = null, double? temperature = null)
|
|
{
|
|
try
|
|
{
|
|
var messages = new List<object>();
|
|
if (!string.IsNullOrEmpty(systemPrompt))
|
|
messages.Add(new { role = "system", content = systemPrompt });
|
|
messages.Add(new { role = "user", content = prompt });
|
|
|
|
var request = new Dictionary<string, object?>
|
|
{
|
|
["model"] = model,
|
|
["messages"] = messages,
|
|
["stream"] = false
|
|
};
|
|
if (temperature.HasValue) request["temperature"] = Math.Clamp(temperature.Value, 0.0, 2.0);
|
|
|
|
var response = await _http.PostAsync(
|
|
$"{Url}/v1/chat/completions",
|
|
new StringContent(JsonSerializer.Serialize(request), System.Text.Encoding.UTF8, "application/json")
|
|
);
|
|
if (!response.IsSuccessStatusCode) return "Fehler bei der Verarbeitung.";
|
|
|
|
var content = await response.Content.ReadAsStringAsync();
|
|
using var doc = JsonDocument.Parse(content);
|
|
var root = doc.RootElement;
|
|
if (root.TryGetProperty("choices", out var choices) && choices.GetArrayLength() > 0)
|
|
{
|
|
var first = choices[0];
|
|
if (first.TryGetProperty("message", out var msg) && msg.TryGetProperty("content", out var text))
|
|
return text.GetString() ?? "";
|
|
}
|
|
return "Fehler bei der Verarbeitung.";
|
|
}
|
|
catch (Exception ex) { return $"Error: {ex.Message}"; }
|
|
}
|
|
|
|
public async Task ChatStreamAsync(string model, string prompt, Func<string, Task> onToken, string? systemPrompt = null, double? temperature = null)
|
|
{
|
|
var messages = new List<object>();
|
|
if (!string.IsNullOrEmpty(systemPrompt))
|
|
messages.Add(new { role = "system", content = systemPrompt });
|
|
messages.Add(new { role = "user", content = prompt });
|
|
|
|
var request = new Dictionary<string, object?>
|
|
{
|
|
["model"] = model,
|
|
["messages"] = messages,
|
|
["stream"] = true
|
|
};
|
|
if (temperature.HasValue) request["temperature"] = Math.Clamp(temperature.Value, 0.0, 2.0);
|
|
|
|
using var response = await _http.PostAsync(
|
|
$"{Url}/v1/chat/completions",
|
|
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)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(line)) continue;
|
|
if (!line.StartsWith("data: ")) continue;
|
|
|
|
var json = line[6..];
|
|
if (json == "[DONE]") break;
|
|
|
|
try
|
|
{
|
|
using var doc = JsonDocument.Parse(json);
|
|
var root = doc.RootElement;
|
|
if (root.TryGetProperty("choices", out var choices) && choices.GetArrayLength() > 0)
|
|
{
|
|
var delta = choices[0].TryGetProperty("delta", out var d) ? d : default;
|
|
if (delta.ValueKind != JsonValueKind.Undefined && delta.TryGetProperty("content", out var content))
|
|
{
|
|
var text = content.GetString();
|
|
if (!string.IsNullOrEmpty(text))
|
|
await onToken(text);
|
|
}
|
|
}
|
|
}
|
|
catch { }
|
|
}
|
|
}
|
|
|
|
public async Task<float[]> GetEmbeddingAsync(string text, string? model = null)
|
|
{
|
|
try
|
|
{
|
|
var request = new { content = text };
|
|
var response = await _http.PostAsync(
|
|
$"{Url}/embedding",
|
|
new StringContent(JsonSerializer.Serialize(request), System.Text.Encoding.UTF8, "application/json")
|
|
);
|
|
if (!response.IsSuccessStatusCode) return Array.Empty<float>();
|
|
|
|
var content = await response.Content.ReadAsStringAsync();
|
|
using var doc = JsonDocument.Parse(content);
|
|
if (!doc.RootElement.TryGetProperty("embedding", out var emb)) return Array.Empty<float>();
|
|
var list = new List<float>();
|
|
foreach (var v in emb.EnumerateArray()) list.Add(v.GetSingle());
|
|
return list.ToArray();
|
|
}
|
|
catch { return Array.Empty<float>(); }
|
|
}
|
|
|
|
public Task<bool> PullModelAsync(string name, IProgress<string>? progress = null)
|
|
=> Task.FromResult(false);
|
|
|
|
public Task<bool> DeleteModelAsync(string name)
|
|
=> Task.FromResult(false);
|
|
|
|
public async Task<int?> GetContextSizeAsync(string model)
|
|
{
|
|
try
|
|
{
|
|
var response = await _http.GetAsync($"{Url}/slots");
|
|
if (!response.IsSuccessStatusCode) return 2048;
|
|
var content = await response.Content.ReadAsStringAsync();
|
|
using var doc = JsonDocument.Parse(content);
|
|
if (doc.RootElement.GetArrayLength() > 0)
|
|
{
|
|
var first = doc.RootElement[0];
|
|
if (first.TryGetProperty("n_ctx", out var ctx))
|
|
return ctx.GetInt32();
|
|
}
|
|
return 2048;
|
|
}
|
|
catch { return 2048; }
|
|
}
|
|
|
|
public async Task<long> MeasureLatencyAsync(string model)
|
|
{
|
|
try
|
|
{
|
|
var sw = System.Diagnostics.Stopwatch.StartNew();
|
|
var response = await _http.GetAsync($"{Url}/health");
|
|
sw.Stop();
|
|
return response.IsSuccessStatusCode ? sw.ElapsedMilliseconds : -1;
|
|
}
|
|
catch { return -1; }
|
|
}
|
|
|
|
public async Task<bool> ModelExistsAsync(string model)
|
|
{
|
|
try
|
|
{
|
|
var models = await GetModelsAsync();
|
|
return models.Any();
|
|
}
|
|
catch { return false; }
|
|
}
|
|
|
|
public async Task<(bool Success, int ContextWindow)> GetModelContextWindowAsync(string model)
|
|
{
|
|
var ctx = await GetContextSizeAsync(model);
|
|
return (ctx.HasValue, ctx ?? 2048);
|
|
}
|
|
|
|
public bool TestEmbedding(string model)
|
|
{
|
|
try
|
|
{
|
|
var response = _http.PostAsync(
|
|
$"{Url}/embedding",
|
|
new StringContent(JsonSerializer.Serialize(new { content = "test" }), System.Text.Encoding.UTF8, "application/json")
|
|
).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; }
|
|
}
|
|
}
|