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

@@ -98,18 +98,99 @@ Antworte AUSSCHLIESSLICH als JSON-Objekt mit diesen Feldern:
Antworte NUR mit dem JSON-Objekt. Keine Einleitung, keine Erklärung."""
INCREMENTAL_ANALYSIS_PROMPT_TEMPLATE = """Du bist ein OSINT-Analyse-Agent für ein Lagemonitoring-System.
HEUTIGES DATUM: {today}
AUSGABESPRACHE: {output_language}
VORFALL: {title}
KONTEXT: {description}
BISHERIGES LAGEBILD:
{previous_summary}
BISHERIGE QUELLEN:
{previous_sources_text}
NEUE MELDUNGEN SEIT DEM LETZTEN UPDATE:
{new_articles_text}
AUFTRAG:
1. Aktualisiere das Lagebild basierend auf den neuen Meldungen (max. 500 Wörter)
2. Behalte bestätigte Fakten aus dem bisherigen Lagebild bei
3. Ergänze neue Erkenntnisse und markiere wichtige neue Entwicklungen
4. Aktualisiere die Quellenverweise — neue Quellen bekommen fortlaufende Nummern nach den bisherigen
5. Entferne veraltete oder widerlegte Informationen
STRUKTUR:
- Fließtext oder mit Markdown-Zwischenüberschriften (##) — je nach Komplexität
- Neue Entwicklungen mit **Fettdruck** hervorheben
REGELN:
- Neutral und sachlich - keine Wertungen oder Spekulationen
- Bei widersprüchlichen Angaben beide Seiten erwähnen
- Quellen immer mit [Nr] referenzieren
- Das sources-Array muss ALLE Quellen enthalten (bisherige + neue)
- Ältere Quellen zeitlich einordnen
Antworte AUSSCHLIESSLICH als JSON-Objekt mit diesen Feldern:
- "summary": Aktualisierte Zusammenfassung mit Quellenverweisen [1], [2] etc.
- "sources": VOLLSTÄNDIGES Array aller Quellen (alte + neue), je: {{"nr": 1, "name": "Quellenname", "url": "https://..."}}
- "key_facts": Array aller aktuellen Kernfakten (in Ausgabesprache)
- "translations": Array von Objekten mit "article_id", "headline_de", "content_de" (nur für neue fremdsprachige Artikel)
Antworte NUR mit dem JSON-Objekt. Keine Einleitung, keine Erklärung."""
INCREMENTAL_BRIEFING_PROMPT_TEMPLATE = """Du bist ein OSINT-Analyse-Agent für ein Lagemonitoring-System.
Du aktualisierst ein strukturiertes Briefing für eine Hintergrundrecherche.
HEUTIGES DATUM: {today}
AUSGABESPRACHE: {output_language}
THEMA: {title}
KONTEXT: {description}
BISHERIGES BRIEFING:
{previous_summary}
BISHERIGE QUELLEN:
{previous_sources_text}
NEUE QUELLEN SEIT DEM LETZTEN UPDATE:
{new_articles_text}
AUFTRAG:
Aktualisiere das Briefing (max. 800 Wörter) mit den neuen Erkenntnissen. Behalte die Struktur bei:
## ÜBERBLICK
## HINTERGRUND
## AKTEURE
## AKTUELLE LAGE
## EINSCHÄTZUNG
## QUELLENQUALITÄT
REGELN:
- Bisherige gesicherte Fakten beibehalten
- Neue Erkenntnisse einarbeiten und mit **Fettdruck** hervorheben
- Veraltete Informationen aktualisieren
- Quellen immer mit [Nr] referenzieren
- Das sources-Array muss ALLE Quellen enthalten (bisherige + neue)
- Markdown-Überschriften (##) für die Abschnitte verwenden
Antworte AUSSCHLIESSLICH als JSON-Objekt mit diesen Feldern:
- "summary": Das aktualisierte Briefing als Markdown-Text mit Quellenverweisen
- "sources": VOLLSTÄNDIGES Array aller Quellen (alte + neue), je: {{"nr": 1, "name": "Quellenname", "url": "https://..."}}
- "key_facts": Array aller gesicherten Kernfakten (in Ausgabesprache)
- "translations": Array von Objekten mit "article_id", "headline_de", "content_de" (nur für neue fremdsprachige Artikel)
Antworte NUR mit dem JSON-Objekt. Keine Einleitung, keine Erklärung."""
class AnalyzerAgent:
"""Analysiert und übersetzt Meldungen über Claude CLI."""
async def analyze(self, title: str, description: str, articles: list[dict], incident_type: str = "adhoc") -> tuple[dict | None, ClaudeUsage | None]:
"""Analysiert alle Meldungen zu einem Vorfall."""
if not articles:
return None, None
# Artikel-Text für Prompt aufbereiten
def _format_articles_text(self, articles: list[dict], max_articles: int = 30) -> str:
"""Formatiert Artikel als Text für den Prompt."""
articles_text = ""
for i, article in enumerate(articles[:30]): # Max 30 Artikel um Prompt-Länge zu begrenzen
for i, article in enumerate(articles[:max_articles]):
articles_text += f"\n--- Meldung {i+1} (ID: {article.get('id', 'neu')}) ---\n"
articles_text += f"Quelle: {article.get('source', 'Unbekannt')}\n"
url = article.get('source_url', '')
@@ -123,7 +204,15 @@ class AnalyzerAgent:
articles_text += f"Überschrift: {headline}\n"
content = article.get('content_de') or article.get('content_original', '')
if content:
articles_text += f"Inhalt: {content[:500]}\n"
articles_text += f"Inhalt: {content[:800]}\n"
return articles_text
async def analyze(self, title: str, description: str, articles: list[dict], incident_type: str = "adhoc") -> tuple[dict | None, ClaudeUsage | None]:
"""Erstanalyse: Analysiert alle Meldungen zu einem Vorfall (erster Refresh)."""
if not articles:
return None, None
articles_text = self._format_articles_text(articles)
from config import OUTPUT_LANGUAGE
today = datetime.now(TIMEZONE).strftime("%d.%m.%Y")
@@ -140,12 +229,69 @@ class AnalyzerAgent:
result, usage = await call_claude(prompt)
analysis = self._parse_response(result)
if analysis:
logger.info(f"Analyse abgeschlossen: {len(analysis.get('sources', []))} Quellen referenziert")
logger.info(f"Erstanalyse abgeschlossen: {len(analysis.get('sources', []))} Quellen referenziert")
return analysis, usage
except Exception as e:
logger.error(f"Analyse-Fehler: {e}")
return None, None
async def analyze_incremental(
self,
title: str,
description: str,
new_articles: list[dict],
previous_summary: str,
previous_sources_json: str | None,
incident_type: str = "adhoc",
) -> tuple[dict | None, ClaudeUsage | None]:
"""Inkrementelle Analyse: Aktualisiert das Lagebild mit nur den neuen Artikeln.
Spart erheblich Tokens, da nicht alle Artikel erneut gesendet werden.
"""
if not new_articles:
logger.info("Inkrementelle Analyse übersprungen: keine neuen Artikel")
return None, None
new_articles_text = self._format_articles_text(new_articles, max_articles=20)
previous_sources_text = "Keine bisherigen Quellen"
if previous_sources_json:
try:
sources = json.loads(previous_sources_json)
lines = []
for s in sources:
lines.append(f"[{s.get('nr', '?')}] {s.get('name', '?')}{s.get('url', '?')}")
previous_sources_text = "\n".join(lines)
except (json.JSONDecodeError, TypeError):
previous_sources_text = "Fehler beim Laden der bisherigen Quellen"
from config import OUTPUT_LANGUAGE
today = datetime.now(TIMEZONE).strftime("%d.%m.%Y")
template = INCREMENTAL_BRIEFING_PROMPT_TEMPLATE if incident_type == "research" else INCREMENTAL_ANALYSIS_PROMPT_TEMPLATE
prompt = template.format(
title=title,
description=description or "Keine weiteren Details",
previous_summary=previous_summary,
previous_sources_text=previous_sources_text,
new_articles_text=new_articles_text,
today=today,
output_language=OUTPUT_LANGUAGE,
)
try:
result, usage = await call_claude(prompt)
analysis = self._parse_response(result)
if analysis:
logger.info(
f"Inkrementelle Analyse abgeschlossen: {len(new_articles)} neue Artikel, "
f"{len(analysis.get('sources', []))} Quellen gesamt"
)
return analysis, usage
except Exception as e:
logger.error(f"Inkrementelle Analyse-Fehler: {e}")
return None, None
def _parse_response(self, response: str) -> dict | None:
"""Parst die Claude-Antwort als JSON-Objekt."""
try: