AuthorBuddy/Services/FactCheckService.cs
2026-08-30 08:11:01 +02:00

146 lines
No EOL
4.9 KiB
C#

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<List<FactCheckResult>> VerifyAsync(string answer, string? backendName = null, int maxClaims = 3)
{
var results = new List<FactCheckResult>();
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<FactCheckResult> GetVerdictAsync(string claim, List<DuckDuckGoResult> searchResults, string? backendName)
{
var sourcesText = new StringBuilder();
var sources = new List<string>();
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<string> ExtractClaims(string text)
{
var claims = new List<string>();
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();
}
}