From 86b12a156efa7c89b94e75573d82570fd23d2586 Mon Sep 17 00:00:00 2001 From: UserIsMH Date: Fri, 22 May 2026 02:32:55 +0200 Subject: [PATCH] feat(recency): Frische-Suchfeed (when:14d) + Aktualitaets-Score MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Damit die Pipeline das aktuelle Bild einfaengt, nicht nur das relevanteste (oft Monate alt). Bei der Test-Lage Qilin war der neueste Artikel 7 Wochen alt, die Masse 6-7 Monate — weil Google-News-Volltextsuche nach Relevanz rankt, nicht nach Datum. - build_news_search_feeds: neuer Parameter recency_days. Wenn gesetzt, wird der Google-News-Operator "when:Nd" an die Query gehaengt — der Feed liefert nur Artikel der letzten N Tage. Eigene Domain-Gruppe '...-recent'. - orchestrator._rss_pipeline: baut jetzt ZWEI Suchfeed-Saetze — einen Kontext-Feed (alle Zeiten) und einen Frische-Feed (when:14d). Beide laufen durch dieselbe Pipeline, Dedup entfernt Ueberschneidungen. - rss_parser._fetch_feed: relevance_score bekommt einen Aktualitaets-Bonus (<=3d +0.35, <=14d +0.20, <=60d +0.05) bzw. -Malus (>180d -0.15, >365d -0.30). Damit ueberleben frische Artikel den Domain-Cap statt von alten verdraengt zu werden. Nur adhoc-Pfad betroffen — research-Lagen ueberspringen die RSS-Pipeline ohnehin und behalten ihre volle historische Tiefe. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/agents/orchestrator.py | 16 +++++++++++++--- src/agents/researcher.py | 26 ++++++++++++++++++++------ src/feeds/rss_parser.py | 21 ++++++++++++++++++++- 3 files changed, 53 insertions(+), 10 deletions(-) diff --git a/src/agents/orchestrator.py b/src/agents/orchestrator.py index f58e499..5bdff2c 100644 --- a/src/agents/orchestrator.py +++ b/src/agents/orchestrator.py @@ -932,11 +932,21 @@ class AgentOrchestrator: _gnews_langs = list(source_lang_whitelist) else: _gnews_langs = list({output_language_iso, research_language_iso}) + # Zwei Sets: ein Kontext-Feed (alle Zeiten) + ein Frische-Feed + # (when:14d). Der Frische-Feed garantiert, dass das aktuelle + # Bild eingefangen wird, auch wenn aeltere Artikel relevanter + # ranken. Beide laufen durch dieselbe Pipeline; Dedup entfernt + # Ueberschneidungen. _gnews_feeds = build_news_search_feeds(keywords, _gnews_langs) - if _gnews_feeds: - logger.info(f"Google-News-Suchfeeds ergaenzt: {len(_gnews_feeds)}") + _gnews_recent = build_news_search_feeds(keywords, _gnews_langs, recency_days=14) + _all_gnews = _gnews_feeds + _gnews_recent + if _all_gnews: + logger.info( + f"Google-News-Suchfeeds ergaenzt: {len(_gnews_feeds)} Kontext " + f"+ {len(_gnews_recent)} Frische (when:14d)" + ) articles = await rss_parser.search_feeds_selective( - title, selected_feeds + _gnews_feeds, keywords=keywords, + title, selected_feeds + _all_gnews, keywords=keywords, ) else: articles = await rss_parser.search_feeds(title, international=international, tenant_id=tenant_id, keywords=keywords, user_id=user_id) diff --git a/src/agents/researcher.py b/src/agents/researcher.py index a0d6cfb..4845e1e 100644 --- a/src/agents/researcher.py +++ b/src/agents/researcher.py @@ -30,6 +30,7 @@ def build_news_search_feeds( keywords_by_lang: dict | list | None, languages: list[str], max_keywords: int = 4, + recency_days: int | None = None, ) -> list[dict]: """Baut dynamische Google-News-Volltext-Such-Feeds pro Sprache. @@ -44,6 +45,9 @@ def build_news_search_feeds( keywords_by_lang: Sprach-Dict {iso: [keyword,...]} aus der Keyword-Extraktion. languages: ISO-Codes, fuer die ein Suchfeed gebaut werden soll. max_keywords: wie viele (spezifischste) Keywords in die Such-Query gehen. + recency_days: wenn gesetzt, wird der Google-News-Operator "when:Nd" an die + Query gehaengt — der Feed liefert dann nur Artikel der letzten N Tage. + Fuer "Frische-Suchfeeds", die das aktuelle Bild garantiert einfangen. Returns: Liste von Feed-Config-Dicts (kompatibel mit RSSParser._fetch_feed). @@ -88,28 +92,38 @@ def build_news_search_feeds( if not deduped: continue query = " ".join(deduped) - if not query or query in seen_queries: + # when:Nd-Operator anhaengen (Google-News-Zeitfilter) + effective_query = query + if recency_days and recency_days > 0: + effective_query = f"{query} when:{recency_days}d" + if not effective_query or effective_query in seen_queries: continue - seen_queries.add(query) + seen_queries.add(effective_query) hl, gl = locale ceid_lang = hl.split("-")[0] url = ( "https://news.google.com/rss/search?q=" - + urllib.parse.quote(query) + + urllib.parse.quote(effective_query) + f"&hl={hl}&gl={gl}&ceid={gl}:{ceid_lang}" ) + if recency_days and recency_days > 0: + name = f"Google News Suche ({lang_key}, letzte {recency_days}d): {query}" + domain = f"google-news-search-{lang_key}-recent" + else: + name = f"Google News Suche ({lang_key}): {query}" + domain = f"google-news-search-{lang_key}" feeds.append({ - "name": f"Google News Suche ({lang_key}): {query}", + "name": name, "url": url, # Eigene Domain-Gruppe, damit der Domain-Cap die Such-Feeds NICHT mit # den site:-Google-News-Feeds in einen Topf wirft. - "domain": f"google-news-search-{lang_key}", + "domain": domain, "primary_language": lang_key, "category": "international", "media_type": "", }) - logger.info("Google-News-Suchfeed (%s): q=%r", lang_key, query) + logger.info("Google-News-Suchfeed (%s): q=%r", lang_key, effective_query) return feeds diff --git a/src/feeds/rss_parser.py b/src/feeds/rss_parser.py index 8af9199..1af2d52 100644 --- a/src/feeds/rss_parser.py +++ b/src/feeds/rss_parser.py @@ -218,14 +218,33 @@ class RSSParser: if match_count >= min_matches: published = None + published_dt = None if hasattr(entry, "published_parsed") and entry.published_parsed: try: - published = datetime(*entry.published_parsed[:6], tzinfo=timezone.utc).astimezone(TIMEZONE).isoformat() + published_dt = datetime(*entry.published_parsed[:6], tzinfo=timezone.utc) + published = published_dt.astimezone(TIMEZONE).isoformat() except (TypeError, ValueError): pass # Relevanz-Score: Anteil der gematchten Suchworte (0.0-1.0) relevance_score = match_count / len(search_words) if search_words else 0.0 + # Aktualitaets-Bonus/Malus: frische Artikel sollen den + # Domain-Cap (sortiert nach relevance_score) ueberleben und + # nicht von Monate alten verdraengt werden. Damit faengt die + # Pipeline das aktuelle Bild ein. Nur adhoc-Pfad — research + # nutzt diesen Code nicht. + if published_dt is not None: + age_days = (datetime.now(timezone.utc) - published_dt).days + if age_days <= 3: + relevance_score += 0.35 + elif age_days <= 14: + relevance_score += 0.20 + elif age_days <= 60: + relevance_score += 0.05 + elif age_days > 365: + relevance_score -= 0.30 + elif age_days > 180: + relevance_score -= 0.15 # Bei Google-News-Feeds: echten Publisher aus -Tag holen article_source = name -- 2.49.1