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 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 { 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 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 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> SearchAsync(string query, int maxResults = 5) { var results = new List(); var seen = new HashSet(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 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 }) }; }