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:
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren