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