Dynamische Keyword-Extraktion fuer RSS-Filterung + min_matches-Fix
- researcher.py: Neuer dedizierter Haiku-Call extract_dynamic_keywords() analysiert die letzten 30 Headlines und generiert 5 DE+EN Begriffspaare - orchestrator.py: Dynamische Keywords vor Feed-Selektion aus DB-Headlines - rss_parser.py: min_matches auf max 2 gedeckelt (vorher n/2, bei 10 Keywords = 5) - analyzer.py: Fettdruck-Anweisungen entfernt Vorher: 0 RSS-Treffer (min_matches=5 unerreichbar) Nachher: 22 RSS-Treffer (Tagesschau 11, Al Jazeera 5, BBC 4, NYT 2) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -114,6 +114,28 @@ Antworte NUR mit einem JSON-Objekt in diesem Format:
|
||||
{{"feeds": [1, 2, 5, 12], "keywords": ["begriff1", "begriff2", "begriff3"]}}"""
|
||||
|
||||
|
||||
KEYWORD_EXTRACTION_PROMPT = """Analysiere diese aktuellen Nachrichten-Headlines und extrahiere die wichtigsten Suchbegriffe fuer RSS-Feed-Filterung.
|
||||
|
||||
THEMA: {title}
|
||||
|
||||
AKTUELLE HEADLINES (die letzten Meldungen zu diesem Thema):
|
||||
{headlines}
|
||||
|
||||
AUFGABE:
|
||||
Generiere 5 Begriffspaare (DE + EN), mit denen neue RSS-Artikel zu diesem Thema gefunden werden.
|
||||
Ein Artikel gilt als relevant, wenn mindestens 2 dieser Begriffe im Titel oder der Beschreibung vorkommen.
|
||||
|
||||
REGELN:
|
||||
- Die ersten 2 Begriffspaare MUESSEN die zentralen Akteure/Laender/Themen sein (z.B. iran, israel, usa) — also die Begriffe, die in fast JEDEM Artikel zum Thema vorkommen
|
||||
- Die letzten 3 Begriffspaare sind aktuelle Entwicklungen aus den Headlines (Orte, Akteure, Schluesselwoerter der aktuellen Phase)
|
||||
- Begriffe muessen so gewaehlt sein, dass sie in kurzen RSS-Titeln matchen (einzelne Woerter, keine Phrasen)
|
||||
- Alle Begriffe in Kleinbuchstaben
|
||||
- Exakt 5 Begriffspaare
|
||||
|
||||
Antwort NUR als JSON-Array:
|
||||
[{{"de": "iran", "en": "iran"}}, {{"de": "israel", "en": "israel"}}, {{"de": "teheran", "en": "tehran"}}, {{"de": "luftangriff", "en": "airstrike"}}, {{"de": "trump", "en": "trump"}}]"""
|
||||
|
||||
|
||||
class ResearcherAgent:
|
||||
"""Führt OSINT-Recherchen über Claude CLI WebSearch durch."""
|
||||
|
||||
@@ -191,6 +213,62 @@ class ResearcherAgent:
|
||||
logger.warning(f"Feed-Selektion fehlgeschlagen ({e}), nutze alle Feeds")
|
||||
return feeds_metadata, None, None
|
||||
|
||||
|
||||
async def extract_dynamic_keywords(
|
||||
self, title: str, recent_headlines: list[str]
|
||||
) -> tuple[list[str] | None, ClaudeUsage | None]:
|
||||
"""Extrahiert aktuelle Suchbegriffe aus den letzten Headlines via Haiku.
|
||||
|
||||
Returns:
|
||||
(flache Keyword-Liste DE+EN, usage) oder (None, None) bei Fehler
|
||||
"""
|
||||
if not recent_headlines:
|
||||
return None, None
|
||||
|
||||
headlines_text = "\n".join(f"- {h}" for h in recent_headlines[:30])
|
||||
prompt = KEYWORD_EXTRACTION_PROMPT.format(
|
||||
title=title,
|
||||
headlines=headlines_text,
|
||||
)
|
||||
|
||||
try:
|
||||
result, usage = await call_claude(prompt, tools=None, model=CLAUDE_MODEL_FAST)
|
||||
|
||||
parsed = None
|
||||
try:
|
||||
parsed = json.loads(result)
|
||||
except json.JSONDecodeError:
|
||||
match = re.search(r'\[.*\]', result, re.DOTALL)
|
||||
if match:
|
||||
try:
|
||||
parsed = json.loads(match.group())
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
if not parsed or not isinstance(parsed, list):
|
||||
logger.warning("Keyword-Extraktion: Kein gueltiges JSON erhalten")
|
||||
return None, usage
|
||||
|
||||
# Flache Liste: alle DE + EN Begriffe
|
||||
keywords = []
|
||||
for entry in parsed:
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
de = entry.get("de", "").lower().strip()
|
||||
en = entry.get("en", "").lower().strip()
|
||||
if de:
|
||||
keywords.append(de)
|
||||
if en and en != de:
|
||||
keywords.append(en)
|
||||
|
||||
if keywords:
|
||||
logger.info(f"Dynamische Keywords ({len(keywords)}): {keywords}")
|
||||
return keywords if keywords else None, usage
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Keyword-Extraktion fehlgeschlagen: {e}")
|
||||
return None, None
|
||||
|
||||
async def search(self, title: str, description: str = "", incident_type: str = "adhoc", international: bool = True) -> tuple[list[dict], ClaudeUsage | None]:
|
||||
"""Sucht nach Informationen zu einem Vorfall."""
|
||||
from config import OUTPUT_LANGUAGE
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren