fix: URL-Verifizierung fuer WebSearch-Ergebnisse

- Prompt-Verbesserung: Claude muss exakte URLs aus WebSearch kopieren, keine konstruierten URLs
- Neue _verify_article_urls() Funktion im Orchestrator
- HEAD-Request auf jede WebSearch-URL, GET-Fallback bei 405
- Bei 404/unerreichbar: Ersetzung durch Google-Suchlink (site:domain headline)
- Nur WebSearch-URLs werden geprueft, RSS-URLs sind bereits verifiziert
Dieser Commit ist enthalten in:
Claude Dev
2026-03-17 10:22:01 +01:00
Ursprung 742f49467e
Commit 474e2beca9
2 geänderte Dateien mit 83 neuen und 1 gelöschten Zeilen

Datei anzeigen

@@ -6,7 +6,9 @@ import re
from datetime import datetime from datetime import datetime
from config import TIMEZONE from config import TIMEZONE
from typing import Optional 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.claude_client import UsageAccumulator
from agents.factchecker import find_matching_claim, deduplicate_new_facts, TWOPHASE_MIN_FACTS 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) 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]): async def _background_discover_sources(articles: list[dict]):
"""Background-Task: Registriert seriöse, unbekannte Quellen aus Recherche-Ergebnissen.""" """Background-Task: Registriert seriöse, unbekannte Quellen aus Recherche-Ergebnissen."""
from database import get_db from database import get_db
@@ -692,6 +768,10 @@ class AgentOrchestrator:
(search_results, search_usage) = pipeline_results[1] (search_results, search_usage) = pipeline_results[1]
telegram_articles = pipeline_results[2][0] if include_telegram else [] 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: if rss_feed_usage:
usage_acc.add(rss_feed_usage) usage_acc.add(rss_feed_usage)
if search_usage: if search_usage:

Datei anzeigen

@@ -21,6 +21,7 @@ REGELN:
- KEINE Boulevardmedien (Bild, Sun, Daily Mail etc.) - KEINE Boulevardmedien (Bild, Sun, Daily Mail etc.)
{language_instruction} {language_instruction}
- Faktenbasiert und neutral - keine Spekulationen - 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 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 - 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) - KEIN Social Media (Twitter/X, Facebook, Instagram, TikTok, Reddit)
- KEINE Boulevardmedien (Bild, Sun, Daily Mail etc.) - KEINE Boulevardmedien (Bild, Sun, Daily Mail etc.)
- KEINE Meinungsblogs ohne Quellenbelege - 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. Gib die Ergebnisse AUSSCHLIESSLICH als JSON-Array zurück, ohne Erklärungen davor oder danach.
Jedes Element hat diese Felder: Jedes Element hat diese Felder: