diff --git a/src/agents/orchestrator.py b/src/agents/orchestrator.py index 7dd7648..feae838 100644 --- a/src/agents/orchestrator.py +++ b/src/agents/orchestrator.py @@ -6,7 +6,9 @@ import re from datetime import datetime from config import TIMEZONE from typing import Optional -from urllib.parse import urlparse, urlunparse +from urllib.parse import urlparse, urlunparse, quote_plus + +import httpx from agents.claude_client import UsageAccumulator from agents.factchecker import find_matching_claim, deduplicate_new_facts, TWOPHASE_MIN_FACTS @@ -132,6 +134,80 @@ def _score_relevance(article: dict, search_words: list[str] = None) -> float: return min(1.0, score) + +async def _verify_article_urls( + articles: list[dict], + concurrency: int = 10, + timeout: float = 8.0, +) -> list[dict]: + """Prueft WebSearch-URLs auf Erreichbarkeit. Ersetzt unerreichbare URLs durch Suchlinks.""" + if not articles: + return [] + + sem = asyncio.Semaphore(concurrency) + results: list[dict | None] = [None] * len(articles) + + async def _check(idx: int, article: dict, client: httpx.AsyncClient): + url = article.get("source_url", "").strip() + if not url: + results[idx] = article # Kein URL -> behalten (wird eh nicht verlinkt) + return + async with sem: + try: + resp = await client.head(url) + if resp.status_code == 405: + # Manche Server unterstuetzen kein HEAD + resp = await client.get(url, headers={"Range": "bytes=0-0"}) + if 200 <= resp.status_code < 400: + results[idx] = article + return + # 404 oder anderer Fehler -> Fallback-Suchlink + logger.info(f"URL-Verifizierung: {resp.status_code} fuer {url}") + except Exception as e: + logger.debug(f"URL-Verifizierung fehlgeschlagen fuer {url}: {e}") + + # Fallback: Google-Suchlink aus Headline + Source-Domain + headline = article.get("headline", "") + source = article.get("source", "") + domain = "" + try: + from urllib.parse import urlparse as _urlparse + domain = _urlparse(url).netloc + except Exception: + pass + if headline: + search_query = f"site:{domain} {headline}" if domain else f"{source} {headline}" + fallback_url = f"https://www.google.com/search?q={quote_plus(search_query)}" + article_copy = dict(article) + article_copy["source_url"] = fallback_url + article_copy["_url_repaired"] = True + results[idx] = article_copy + logger.info(f"URL-Fallback: {url} -> Google-Suche fuer \"{headline[:60]}...\"") + else: + results[idx] = article # Kein Headline -> Original behalten + + async with httpx.AsyncClient( + timeout=timeout, + follow_redirects=True, + headers={"User-Agent": "Mozilla/5.0 (compatible; AegisSight-Monitor/1.0)"}, + ) as client: + await asyncio.gather(*[_check(i, a, client) for i, a in enumerate(articles)]) + + verified = [r for r in results if r is not None] + repaired = sum(1 for r in verified if r.get("_url_repaired")) + ok = len(verified) - repaired + + if repaired > 0: + logger.warning( + f"URL-Verifizierung: {ok} OK, {repaired} durch Suchlinks ersetzt " + f"(von {len(articles)} WebSearch-Artikeln)" + ) + else: + logger.info(f"URL-Verifizierung: Alle {len(articles)} WebSearch-URLs erreichbar") + + return verified + + async def _background_discover_sources(articles: list[dict]): """Background-Task: Registriert seriöse, unbekannte Quellen aus Recherche-Ergebnissen.""" from database import get_db @@ -692,6 +768,10 @@ class AgentOrchestrator: (search_results, search_usage) = pipeline_results[1] telegram_articles = pipeline_results[2][0] if include_telegram else [] + # URL-Verifizierung nur fuer WebSearch-Ergebnisse (RSS-URLs sind bereits verifiziert) + if search_results: + search_results = await _verify_article_urls(search_results) + if rss_feed_usage: usage_acc.add(rss_feed_usage) if search_usage: diff --git a/src/agents/researcher.py b/src/agents/researcher.py index f163fda..b9f16a1 100644 --- a/src/agents/researcher.py +++ b/src/agents/researcher.py @@ -21,6 +21,7 @@ REGELN: - KEINE Boulevardmedien (Bild, Sun, Daily Mail etc.) {language_instruction} - Faktenbasiert und neutral - keine Spekulationen +- KRITISCH für source_url: Kopiere die EXAKTE URL aus den WebSearch-Ergebnissen. Erfinde oder konstruiere NIEMALS URLs aus Mustern oder Erinnerung. Wenn du die exakte URL eines Artikels nicht aus den Suchergebnissen hast, lass diesen Artikel komplett weg. - Nutze removepaywalls.com für Paywall-geschützte Artikel (z.B. Spiegel+, Zeit+, SZ+): https://www.removepaywalls.com/search?url=ARTIKEL_URL - Nutze WebFetch um die 3-5 wichtigsten Artikel vollständig abzurufen und zusammenzufassen @@ -82,6 +83,7 @@ AUSSCHLUSS: - KEIN Social Media (Twitter/X, Facebook, Instagram, TikTok, Reddit) - KEINE Boulevardmedien (Bild, Sun, Daily Mail etc.) - KEINE Meinungsblogs ohne Quellenbelege +- KEINE erfundenen oder konstruierten URLs — gib bei source_url NUR die EXAKTE URL zurueck, die WebSearch tatsaechlich angezeigt hat. Wenn du die URL nicht aus den Suchergebnissen kopieren kannst, lass den Artikel weg. Gib die Ergebnisse AUSSCHLIESSLICH als JSON-Array zurück, ohne Erklärungen davor oder danach. Jedes Element hat diese Felder: