146 lines
No EOL
4.9 KiB
C#
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();
|
|
}
|
|
} |