Feat: Claude-Keywords für RSS-Suche, Jahreszahlen-Filter, strikteres Matching

- rss_researcher liefert jetzt Keywords zurück, die direkt für RSS-Suche genutzt werden
- Neue _clean_search_words() filtert rein-numerische Begriffe (Jahreszahlen etc.)
- Matching-Schwelle aufgerundet: bei 3 Keywords müssen mindestens 2 matchen

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
claude-dev
2026-03-05 18:36:08 +01:00
Ursprung faa273f0de
Commit 536e7f585b
2 geänderte Dateien mit 38 neuen und 19 gelöschten Zeilen

Datei anzeigen

@@ -579,14 +579,15 @@ class AgentOrchestrator:
all_feeds = await get_feeds_with_metadata(tenant_id=tenant_id) all_feeds = await get_feeds_with_metadata(tenant_id=tenant_id)
feed_usage = None feed_usage = None
keywords = None
if len(all_feeds) > 20: if len(all_feeds) > 20:
selected_feeds, feed_usage = await rss_researcher.select_relevant_feeds( selected_feeds, keywords, feed_usage = await rss_researcher.select_relevant_feeds(
title, description, international, all_feeds title, description, international, all_feeds
) )
logger.info(f"Feed-Selektion: {len(selected_feeds)} von {len(all_feeds)} Feeds ausgewählt") logger.info(f"Feed-Selektion: {len(selected_feeds)} von {len(all_feeds)} Feeds ausgewählt")
articles = await rss_parser.search_feeds_selective(title, selected_feeds) articles = await rss_parser.search_feeds_selective(title, selected_feeds, keywords=keywords)
else: else:
articles = await rss_parser.search_feeds(title, international=international, tenant_id=tenant_id) articles = await rss_parser.search_feeds(title, international=international, tenant_id=tenant_id, keywords=keywords)
logger.info(f"RSS: {len(articles)} relevante Artikel gefunden (international={international})") logger.info(f"RSS: {len(articles)} relevante Artikel gefunden (international={international})")
return articles, feed_usage return articles, feed_usage

Datei anzeigen

@@ -20,21 +20,33 @@ class RSSParser:
"über", "unter", "ist", "sind", "hat", "the", "and", "for", "with", "from", "über", "unter", "ist", "sind", "hat", "the", "and", "for", "with", "from",
} }
async def search_feeds(self, search_term: str, international: bool = True, tenant_id: int = None) -> list[dict]: @staticmethod
def _clean_search_words(words: list[str]) -> list[str]:
"""Entfernt rein-numerische Wörter (Jahreszahlen etc.) aus Suchbegriffen."""
cleaned = [w for w in words if not w.isdigit()]
return cleaned if cleaned else words
async def search_feeds(self, search_term: str, international: bool = True, tenant_id: int = None, keywords: list[str] | None = None) -> list[dict]:
"""Durchsucht RSS-Feeds nach einem Suchbegriff. """Durchsucht RSS-Feeds nach einem Suchbegriff.
Args: Args:
search_term: Suchbegriff search_term: Suchbegriff
international: Wenn False, nur deutsche Feeds + Behoerden (keine internationalen) international: Wenn False, nur deutsche Feeds + Behoerden (keine internationalen)
tenant_id: Optionale Org-ID fuer tenant-spezifische Quellen tenant_id: Optionale Org-ID fuer tenant-spezifische Quellen
keywords: Optionale Claude-generierte Keywords (bevorzugt gegenüber Title-Split)
""" """
all_articles = [] all_articles = []
if keywords:
search_words = [w.lower().strip() for w in keywords if w.strip()]
logger.info(f"RSS-Suche mit Claude-Keywords: {search_words}")
else:
search_words = [ search_words = [
w for w in search_term.lower().split() w for w in search_term.lower().split()
if w not in self.STOP_WORDS and len(w) >= 3 if w not in self.STOP_WORDS and len(w) >= 3
] ]
if not search_words: if not search_words:
search_words = search_term.lower().split()[:2] search_words = search_term.lower().split()[:2]
search_words = self._clean_search_words(search_words)
rss_feeds = await self._get_rss_feeds(tenant_id=tenant_id) rss_feeds = await self._get_rss_feeds(tenant_id=tenant_id)
@@ -62,20 +74,26 @@ class RSSParser:
all_articles = self._apply_domain_cap(all_articles) all_articles = self._apply_domain_cap(all_articles)
return all_articles return all_articles
async def search_feeds_selective(self, search_term: str, selected_feeds: list[dict]) -> list[dict]: async def search_feeds_selective(self, search_term: str, selected_feeds: list[dict], keywords: list[str] | None = None) -> list[dict]:
"""Durchsucht nur die übergebenen Feeds (vorselektiert durch Claude). """Durchsucht nur die übergebenen Feeds (vorselektiert durch Claude).
Args: Args:
search_term: Suchbegriff search_term: Suchbegriff
selected_feeds: Liste von Feed-Dicts mit mindestens {"name", "url"} selected_feeds: Liste von Feed-Dicts mit mindestens {"name", "url"}
keywords: Optionale Claude-generierte Keywords (bevorzugt gegenüber Title-Split)
""" """
all_articles = [] all_articles = []
if keywords:
search_words = [w.lower().strip() for w in keywords if w.strip()]
logger.info(f"RSS-Selektiv mit Claude-Keywords: {search_words}")
else:
search_words = [ search_words = [
w for w in search_term.lower().split() w for w in search_term.lower().split()
if w not in self.STOP_WORDS and len(w) >= 3 if w not in self.STOP_WORDS and len(w) >= 3
] ]
if not search_words: if not search_words:
search_words = search_term.lower().split()[:2] search_words = search_term.lower().split()[:2]
search_words = self._clean_search_words(search_words)
tasks = [] tasks = []
for feed_config in selected_feeds: for feed_config in selected_feeds:
@@ -124,8 +142,8 @@ class RSSParser:
summary = entry.get("summary", "") summary = entry.get("summary", "")
text = f"{title} {summary}".lower() text = f"{title} {summary}".lower()
# Flexibles Keyword-Matching: mindestens die Hälfte der Suchworte muss vorkommen # Flexibles Keyword-Matching: mindestens die Hälfte der Suchworte muss vorkommen (aufgerundet)
min_matches = max(1, len(search_words) // 2) min_matches = max(1, (len(search_words) + 1) // 2)
match_count = sum(1 for word in search_words if word in text) match_count = sum(1 for word in search_words if word in text)
if match_count >= min_matches: if match_count >= min_matches: