using AuthorBuddy.Web.Models; using System.Text; namespace AuthorBuddy.Web.Services; public class FactCheckService : IFactCheckService { private readonly IDuckDuckGoService _duckDuckGo; private readonly IOllamaService _ollama; private readonly OllamaSettings _settings; public FactCheckService(IDuckDuckGoService duckDuckGo, IOllamaService ollama, OllamaSettings settings) { _duckDuckGo = duckDuckGo; _ollama = ollama; _settings = settings; } public async Task> VerifyAsync(string answer, string? backendName = null, int maxClaims = 3) { var results = new List(); var claims = ExtractClaims(answer); foreach (var claim in claims.Take(maxClaims)) { var searchResults = await _duckDuckGo.SearchAsync(claim); if (searchResults.Count == 0) { results.Add(new FactCheckResult { Claim = claim, Verdict = FactCheckVerdict.NoInformation, Reasoning = "Keine Quellen gefunden." }); continue; } var verdict = await GetVerdictAsync(claim, searchResults, backendName); results.Add(verdict); } return results; } private async Task GetVerdictAsync(string claim, List searchResults, string? backendName) { var sourcesText = new StringBuilder(); var sources = new List(); foreach (var r in searchResults) { if (!string.IsNullOrWhiteSpace(r.AbstractText)) sourcesText.AppendLine($"- {r.AbstractText}"); if (!string.IsNullOrWhiteSpace(r.Answer)) sourcesText.AppendLine($"- {r.Answer}"); foreach (var related in r.RelatedTexts) sourcesText.AppendLine($"- {related}"); if (!string.IsNullOrWhiteSpace(r.AbstractUrl)) sources.Add(r.AbstractUrl); } if (sourcesText.Length == 0) { return new FactCheckResult { Claim = claim, Verdict = FactCheckVerdict.NoInformation, Reasoning = "Keine verwertbaren Quellen gefunden.", Sources = sources }; } var prompt = $@"Du bist ein Faktenprüfer. Vergleiche die folgende Behauptung mit den DuckDuckGo-Suchergebnissen. BEHAUPTUNG: {claim} SUCHERGEBNISSE: {sourcesText} Bewerte, ob die Behauptung durch die Suchergebnisse bestätigt oder widerlegt wird. Antworte NUR in diesem Format: URTEIL: BESTAETIGT oder WIDERSPROCHEN oder TEILWEISE oder KEINE_INFORMATION BEGRUENDUNG: Maximal 2 Sätze."; var raw = await _ollama.Chat(_settings.StyleModel, prompt, temperature: 0.1, backendName: backendName ?? _settings.StyleBackend); return new FactCheckResult { Claim = claim, Verdict = ParseVerdict(raw), Reasoning = ExtractReasoning(raw), Sources = sources }; } private static List ExtractClaims(string text) { var claims = new List(); if (string.IsNullOrWhiteSpace(text)) return claims; var sentences = System.Text.RegularExpressions.Regex.Split(text, @"(?<=[.!?])\s+"); foreach (var sentence in sentences) { var trimmed = sentence.Trim().Trim('"'); if (trimmed.Length < 25) continue; if (trimmed.EndsWith("?")) continue; if (trimmed.StartsWith("```") || trimmed.StartsWith("#")) continue; claims.Add(trimmed.Length > 200 ? trimmed[..200] + "..." : trimmed); if (claims.Count >= 5) break; } return claims; } private static FactCheckVerdict ParseVerdict(string raw) { var upper = raw.ToUpperInvariant(); if (upper.Contains("BESTAETIGT") || upper.Contains("CONFIRMED") || upper.Contains("BESTÄTIGT")) return FactCheckVerdict.Confirmed; if (upper.Contains("WIDERSPROCHEN") || upper.Contains("CONTRADICT") || upper.Contains("REFUTED")) return FactCheckVerdict.Contradicted; if (upper.Contains("TEILWEISE") || upper.Contains("PARTIAL")) return FactCheckVerdict.Partial; return FactCheckVerdict.NoInformation; } private static string ExtractReasoning(string raw) { var idx = raw.IndexOf("BEGRUENDUNG:", StringComparison.OrdinalIgnoreCase); if (idx < 0) idx = raw.IndexOf("BEGRÜNDUNG:", StringComparison.OrdinalIgnoreCase); if (idx < 0) idx = raw.IndexOf("REASONING:", StringComparison.OrdinalIgnoreCase); if (idx >= 0) { var reasoning = raw[(idx + 1)..]; if (reasoning.Contains(':')) reasoning = reasoning[(reasoning.IndexOf(':') + 1)..]; return reasoning.Trim(); } return raw.Trim(); } }