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:
@@ -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:
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren