AuthorBuddy/Services/FactSearchToolService.cs

210 lines
8.6 KiB
C#
Raw Permalink Normal View History

2026-08-30 08:11:01 +02:00
using AuthorBuddy.Web.Models;
using System.Text.Json;
namespace AuthorBuddy.Web.Services;
public class FactSearchToolService : IFactSearchToolService
{
private readonly IOllamaService _ollama;
private readonly IDuckDuckGoService _duckDuckGo;
private readonly IWikipediaService _wikipedia;
private readonly ISettingsService _settingsService;
private readonly OllamaSettings _settings;
public IReadOnlyList<McpToolDefinition> Tools { get; }
public FactSearchToolService(
IOllamaService ollama,
IDuckDuckGoService duckDuckGo,
IWikipediaService wikipedia,
ISettingsService settingsService,
OllamaSettings settings)
{
_ollama = ollama;
_duckDuckGo = duckDuckGo;
_wikipedia = wikipedia;
_settingsService = settingsService;
_settings = settings;
Tools = new List<McpToolDefinition>
{
new()
{
Name = "fact_search_query",
Description = "Erzeugt aus einer Behauptung (Fakt) eine präzise Suchanfrage für eine Websuche.",
InputSchemaJson = "{\"type\":\"object\",\"properties\":{\"fact\":{\"type\":\"string\"}},\"required\":[\"fact\"]}"
},
new()
{
Name = "fact_web_search",
Description = "Führt eine Websuche aus (DuckDuckGo, ggf. Wikipedia) und liefert Titel, Snippets und URLs.",
InputSchemaJson = "{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\"},\"maxResults\":{\"type\":\"integer\"}},\"required\":[\"query\"]}"
},
new()
{
Name = "fact_summarize",
Description = "Erstellt eine kurze Zusammenfassung über ein Thema anhand von Suchinformationen.",
InputSchemaJson = "{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\"},\"text\":{\"type\":\"string\"},\"url\":{\"type\":\"string\"}},\"required\":[\"query\",\"text\"]}"
}
};
}
public async Task<McpToolResult> CallAsync(string toolName, string argumentsJson)
{
try
{
using var doc = JsonDocument.Parse(string.IsNullOrWhiteSpace(argumentsJson) ? "{}" : argumentsJson);
var args = doc.RootElement;
switch (toolName)
{
case "fact_search_query":
{
var fact = GetString(args, "fact");
if (string.IsNullOrWhiteSpace(fact))
return Error(toolName, "Argument 'fact' fehlt.");
var query = await CreateSearchQueryAsync(fact);
return new McpToolResult { ToolName = toolName, Content = JsonSerializer.Serialize(new { query }), Data = query };
}
case "fact_web_search":
{
var query = GetString(args, "query");
if (string.IsNullOrWhiteSpace(query))
return Error(toolName, "Argument 'query' fehlt.");
var maxResults = args.TryGetProperty("maxResults", out var m) && m.TryGetInt32(out var mi) ? mi : 5;
var results = await SearchAsync(query, maxResults);
return new McpToolResult { ToolName = toolName, Content = JsonSerializer.Serialize(results), Data = results };
}
case "fact_summarize":
{
var query = GetString(args, "query");
var text = GetString(args, "text");
var url = GetString(args, "url");
if (string.IsNullOrWhiteSpace(query) || string.IsNullOrWhiteSpace(text))
return Error(toolName, "Argumente 'query' und 'text' sind erforderlich.");
var summary = await SummarizeAsync(query, text, url);
return new McpToolResult { ToolName = toolName, Content = JsonSerializer.Serialize(new { summary, url }), Data = summary };
}
default:
return Error(toolName, $"Unbekanntes Tool '{toolName}'.");
}
}
catch (Exception ex)
{
return Error(toolName, ex.Message);
}
}
public async Task<string> CreateSearchQueryAsync(string factText)
{
var prompt = await _settingsService.GetFactSearchPromptAsync()
?? _settings.GetFactSearchDefaultPrompt(_settings.Language);
var temperature = await _settingsService.GetFactSearchTempAsync();
var result = await _ollama.Chat(_settings.StyleModel, factText, prompt, temperature, _settings.StyleBackend);
return SanitizeQuery(result);
}
public async Task<List<FactSearchResult>> SearchAsync(string query, int maxResults = 5)
{
var results = new List<FactSearchResult>();
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
void Add(FactSearchResult r)
{
if (string.IsNullOrWhiteSpace(r.Url)) return;
if (!seen.Add(r.Url)) return;
results.Add(r);
}
// 1) Echte Websuche über DuckDuckGo HTML (liefert Titel, Snippet, URL)
try
{
var web = await _duckDuckGo.SearchWebAsync(query, maxResults);
foreach (var r in web)
Add(new FactSearchResult { Title = r.Heading, Snippet = r.AbstractText, Url = r.AbstractUrl });
}
catch
{
// Weiter mit nächster Quelle
}
// 2) DuckDuckGo Instant Answer (enzyklopädische Antworten)
if (results.Count < maxResults)
{
try
{
var ddg = await _duckDuckGo.SearchAsync(query, maxResults);
foreach (var r in ddg)
{
var url = string.IsNullOrWhiteSpace(r.AbstractUrl) ? r.RelatedUrls.FirstOrDefault() ?? string.Empty : r.AbstractUrl;
if (string.IsNullOrWhiteSpace(url)) continue;
var snippet = string.IsNullOrWhiteSpace(r.AbstractText)
? string.Join(" | ", r.RelatedTexts)
: r.AbstractText;
Add(new FactSearchResult
{
Title = string.IsNullOrWhiteSpace(r.Heading) ? url : r.Heading,
Snippet = snippet,
Url = url
});
}
}
catch
{
// Weiter
}
}
// 3) Wikipedia-Fallback
if (results.Count < maxResults)
{
try
{
var wiki = await _wikipedia.SearchAsync(query, maxResults);
foreach (var w in wiki)
Add(new FactSearchResult { Title = w.Title, Snippet = w.Snippet, Url = w.Url });
}
catch
{
// Keine Ergebnisse
}
}
return results.Take(maxResults).ToList();
}
public async Task<string> SummarizeAsync(string query, string text, string url)
{
var info = string.IsNullOrWhiteSpace(text) ? query : text;
if (info.Length > 2500) info = info[..2500];
var system = $"Du bist ein Faktenprüfer und Autor. Erstelle eine kurze, präzise Zusammenfassung (maximal 3 Sätze) über das Thema \"{query}\" auf Deutsch. Nutze nur die gegebenen Informationen. Gib NUR die Zusammenfassung zurück, ohne Anführungszeichen.";
var result = await _ollama.Chat(_settings.StyleModel, info, system, 0.3, _settings.StyleBackend);
return (result ?? string.Empty).Trim().Trim('"', '\'', '\n', '\r');
}
private static string SanitizeQuery(string raw)
{
if (string.IsNullOrWhiteSpace(raw)) return raw ?? string.Empty;
var q = raw.Trim();
q = q.Trim('"', '\'', '.', ':');
q = System.Text.RegularExpressions.Regex.Replace(q, @"^Suche\s*:\s*", "", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
q = System.Text.RegularExpressions.Regex.Replace(q, @"^Query\s*:\s*", "", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
return q;
}
private static string GetString(JsonElement element, string property)
{
if (element.TryGetProperty(property, out var value) && value.ValueKind == JsonValueKind.String)
return value.GetString() ?? string.Empty;
return string.Empty;
}
private static McpToolResult Error(string toolName, string message) =>
new() { IsError = true, ToolName = toolName, Content = JsonSerializer.Serialize(new { error = message }) };
}