Inkrementelle Analyse + Token-Optimierung + Relevanz-Scoring

TOKEN-OPTIMIERUNG:
- Inkrementelle Analyse: Folge-Refreshes senden nur noch das bisherige
  Lagebild + neue Artikel an Claude (statt alle Artikel erneut).
  Spart ~60-70% Tokens bei Lagen mit vielen Artikeln.
- Inkrementeller Faktencheck: Bestehende Fakten als Zusammenfassung,
  nur neue Artikel werden vollstaendig geprueft.
- Modell-Steuerung: Feed-Selektion nutzt jetzt Haiku (CLAUDE_MODEL_FAST)
  statt Opus. Spart ~50-70% bei Feed-Auswahl.
- Set-basierte DB-Deduplizierung: Bestehende URLs/Headlines einmal
  in Sets geladen statt N*M einzelne DB-Queries pro Artikel.

INHALTLICHE VERBESSERUNGEN:
- Relevanz-Scoring: Artikel nach Keyword-Dichte (40%),
  Quellen-Reputation (30%), Inhaltstiefe (20%), RSS-Score (10%).
- Flexibles RSS-Matching: min. Haelfte der Keywords statt alle.
  RSS-Artikel bekommen einen relevance_score.
- Fuzzy Claim-Matching: SequenceMatcher (0.7) statt exakter
  String-Vergleich. Verhindert Duplikat-Akkumulation.
- Translation-Fix: Nur gueltige DB-IDs (isinstance int).
- Researcher: WebFetch fuer Top-Artikel, erweiterte Zusammenfassungen.

DATEIEN:
- config.py: CLAUDE_MODEL_FAST
- claude_client.py: model-Parameter
- researcher.py: Haiku Feed-Selektion, erweiterte Prompts
- analyzer.py: Inkrementelle Analyse + analyze_incremental()
- factchecker.py: Inkrementeller Check + Fuzzy-Matching
- orchestrator.py: Set-Dedup, Relevanz-Scoring, inkrementeller Flow
- rss_parser.py: Flexibles Keyword-Matching + relevance_score
Dieser Commit ist enthalten in:
claude-dev
2026-03-04 20:22:47 +01:00
Ursprung 54d02d2c5b
Commit 3d9a827bc8
7 geänderte Dateien mit 541 neuen und 317 gelöschten Zeilen

Datei anzeigen

@@ -2,6 +2,7 @@
import json
import logging
import re
from difflib import SequenceMatcher
from agents.claude_client import call_claude, ClaudeUsage
logger = logging.getLogger("osint.factchecker")
@@ -81,17 +82,138 @@ Antworte AUSSCHLIESSLICH als JSON-Array. Jedes Element hat:
Antworte NUR mit dem JSON-Array. Keine Einleitung, keine Erklärung."""
# --- Inkrementelle Faktencheck-Prompts (für Folge-Refreshes) ---
INCREMENTAL_FACTCHECK_PROMPT_TEMPLATE = """Du bist ein Faktencheck-Agent für ein OSINT-Lagemonitoring-System.
AUSGABESPRACHE: {output_language}
VORFALL: {title}
BEREITS GEPRÜFTE FAKTEN:
{existing_facts_text}
NEUE MELDUNGEN:
{articles_text}
STRENGE REGELN - KEINE HALLUZINATIONEN:
- Du darfst NUR Fakten bewerten, die aus den Meldungen oder bereits geprüften Fakten stammen
- KEINE Fakten aus deinem Trainingskorpus
- Nutze WebSearch zur Verifikation
- Rufe gefundene URLs per WebFetch ab
AUFTRAG:
1. Prüfe ob die neuen Meldungen bereits geprüfte Fakten BESTÄTIGEN, WIDERLEGEN oder ERGÄNZEN
2. Aktualisiere den Status bestehender Fakten wenn nötig (z.B. "unconfirmed""confirmed")
3. Identifiziere 3-5 NEUE Faktenaussagen aus den neuen Meldungen
4. Prüfe neue Claims per WebSearch gegen unabhängige Quellen
5. Markiere wichtige Statusänderungen und neue Entwicklungen mit is_notification: true
Status-Kategorien:
- "confirmed": 2+ unabhängige seriöse Quellen mit URL
- "unconfirmed": Nur 1 Quelle
- "contradicted": Widersprüchliche Informationen
- "developing": Situation unklar
Antworte AUSSCHLIESSLICH als JSON-Array mit ALLEN Fakten (bestehende aktualisiert + neue).
Jedes Element hat:
- "claim": Die Faktenaussage auf {output_language}
- "status": "confirmed" | "unconfirmed" | "contradicted" | "developing"
- "sources_count": Anzahl unabhängiger Quellen
- "evidence": Begründung MIT konkreten Quellen-URLs
- "is_notification": true/false
Antworte NUR mit dem JSON-Array."""
INCREMENTAL_RESEARCH_FACTCHECK_PROMPT_TEMPLATE = """Du bist ein Faktencheck-Agent für eine Hintergrundrecherche in einem OSINT-Lagemonitoring-System.
AUSGABESPRACHE: {output_language}
THEMA: {title}
BEREITS GEPRÜFTE FAKTEN:
{existing_facts_text}
NEUE QUELLEN:
{articles_text}
STRENGE REGELN - KEINE HALLUZINATIONEN:
- Du darfst NUR Fakten bewerten, die aus den Quellen oder bereits geprüften Fakten stammen
- KEINE Fakten aus deinem Trainingskorpus
- Nutze WebSearch zur Verifikation
- Rufe gefundene URLs per WebFetch ab
AUFTRAG:
1. Prüfe ob die neuen Quellen bereits geprüfte Fakten bestätigen, widerlegen oder ergänzen
2. Aktualisiere den Status bestehender Fakten wenn nötig
3. Identifiziere 3-5 NEUE Faktenaussagen aus den neuen Quellen
4. Prüfe neue Claims per WebSearch
Status-Kategorien:
- "established": 3+ unabhängige Quellen mit URL
- "disputed": Verschiedene Positionen dokumentiert
- "unverified": Nicht unabhängig verifizierbar
- "developing": Faktenlage im Fluss
Antworte AUSSCHLIESSLICH als JSON-Array mit ALLEN Fakten (bestehende aktualisiert + neue).
Jedes Element hat:
- "claim": Die Faktenaussage auf {output_language}
- "status": "established" | "disputed" | "unverified" | "developing"
- "sources_count": Anzahl unabhängiger Quellen
- "evidence": Begründung MIT konkreten Quellen-URLs
- "is_notification": true/false
Antworte NUR mit dem JSON-Array."""
def normalize_claim(claim: str) -> str:
"""Normalisiert einen Claim für Ähnlichkeitsvergleich."""
c = claim.lower().strip()
# Umlaute normalisieren
c = c.replace("ä", "ae").replace("ö", "oe").replace("ü", "ue").replace("ß", "ss")
c = re.sub(r'[^\w\s]', '', c)
c = re.sub(r'\s+', ' ', c).strip()
return c
def find_matching_claim(new_claim: str, existing_claims: list[dict], threshold: float = 0.7) -> dict | None:
"""Findet den besten passenden bestehenden Claim per Fuzzy-Matching.
Args:
new_claim: Der neue Claim-Text
existing_claims: Liste von Dicts mit mindestens {"id", "claim", "status"}
threshold: Mindest-Ähnlichkeit (0.0-1.0), Standard 0.7
Returns:
Das passende Dict oder None wenn kein Match über dem Schwellwert
"""
norm_new = normalize_claim(new_claim)
if not norm_new:
return None
best_match = None
best_ratio = 0.0
for existing in existing_claims:
norm_existing = normalize_claim(existing.get("claim", ""))
if not norm_existing:
continue
ratio = SequenceMatcher(None, norm_new, norm_existing).ratio()
if ratio > best_ratio:
best_ratio = ratio
best_match = existing
if best_ratio >= threshold:
logger.debug(f"Claim-Match ({best_ratio:.2f}): '{new_claim[:50]}...''{best_match['claim'][:50]}...'")
return best_match
return None
class FactCheckerAgent:
"""Prüft Fakten über Claude CLI gegen unabhängige Quellen."""
async def check(self, title: str, articles: list[dict], incident_type: str = "adhoc") -> tuple[list[dict], ClaudeUsage | None]:
"""Führt Faktencheck für eine Lage durch."""
if not articles:
return [], None
def _format_articles_text(self, articles: list[dict], max_articles: int = 20) -> str:
"""Formatiert Artikel als Text für den Prompt."""
articles_text = ""
for i, article in enumerate(articles[:20]):
for i, article in enumerate(articles[:max_articles]):
articles_text += f"\n--- Meldung {i+1} ---\n"
articles_text += f"Quelle: {article.get('source', 'Unbekannt')}\n"
source_url = article.get('source_url', '')
@@ -101,7 +223,27 @@ class FactCheckerAgent:
articles_text += f"Überschrift: {headline}\n"
content = article.get('content_de') or article.get('content_original', '')
if content:
articles_text += f"Inhalt: {content[:300]}\n"
articles_text += f"Inhalt: {content[:500]}\n"
return articles_text
def _format_existing_facts(self, facts: list[dict]) -> str:
"""Formatiert bestehende Fakten als Text für den inkrementellen Prompt."""
if not facts:
return "Keine bisherigen Fakten"
lines = []
for fc in facts:
status = fc.get("status", "developing")
claim = fc.get("claim", "")
sources = fc.get("sources_count", 0)
lines.append(f"- [{status}] ({sources} Quellen) {claim}")
return "\n".join(lines)
async def check(self, title: str, articles: list[dict], incident_type: str = "adhoc") -> tuple[list[dict], ClaudeUsage | None]:
"""Führt vollständigen Faktencheck durch (erster Refresh)."""
if not articles:
return [], None
articles_text = self._format_articles_text(articles)
from config import OUTPUT_LANGUAGE
template = RESEARCH_FACTCHECK_PROMPT_TEMPLATE if incident_type == "research" else FACTCHECK_PROMPT_TEMPLATE
@@ -120,6 +262,46 @@ class FactCheckerAgent:
logger.error(f"Faktencheck-Fehler: {e}")
return [], None
async def check_incremental(
self,
title: str,
new_articles: list[dict],
existing_facts: list[dict],
incident_type: str = "adhoc",
) -> tuple[list[dict], ClaudeUsage | None]:
"""Inkrementeller Faktencheck: Prüft nur neue Artikel gegen bestehende Fakten.
Spart Tokens, da nur neue Artikel + Zusammenfassung der bestehenden Fakten gesendet werden.
"""
if not new_articles:
logger.info("Inkrementeller Faktencheck übersprungen: keine neuen Artikel")
return [], None
articles_text = self._format_articles_text(new_articles, max_articles=15)
existing_facts_text = self._format_existing_facts(existing_facts)
from config import OUTPUT_LANGUAGE
if incident_type == "research":
template = INCREMENTAL_RESEARCH_FACTCHECK_PROMPT_TEMPLATE
else:
template = INCREMENTAL_FACTCHECK_PROMPT_TEMPLATE
prompt = template.format(
title=title,
articles_text=articles_text,
existing_facts_text=existing_facts_text,
output_language=OUTPUT_LANGUAGE,
)
try:
result, usage = await call_claude(prompt)
facts = self._parse_response(result)
logger.info(f"Inkrementeller Faktencheck: {len(facts)} Fakten (neu + aktualisiert)")
return facts, usage
except Exception as e:
logger.error(f"Inkrementeller Faktencheck-Fehler: {e}")
return [], None
def _parse_response(self, response: str) -> list[dict]:
"""Parst die Claude-Antwort als JSON-Array."""
try: