feat(source_suggester): Strategie-Eskalations-Heuristik

Neue Funktion generate_strategy_escalation_suggestions(db) erkennt aktive
Quellen, deren fetch_strategy bereits auf googlebot oder paywall eskaliert
wurde, beim Reachability-Check aber weiterhin status=error melden.

Beispiel: Rheinische Post hat fetch_strategy=googlebot, kriegt aber HTTP 403.
-> Auch der Googlebot-UA-Workaround greift nicht. Quelle wird automatisch
als deactivate-Vorschlag mit priority=high markiert.

Doppel-Vermeidung wie in der Karteileichen-Heuristik: nur wenn fuer die
source_id noch kein pending deactivate-Vorschlag existiert.

Aufgerufen in generate_suggestions als zweite deterministische Stufe,
zwischen Karteileichen-Heuristik und Haiku-Aufruf. Counter im Log
gibt jetzt alle drei Quellen-Beitraege getrennt aus.
Dieser Commit ist enthalten in:
Claude
2026-05-09 15:26:05 +00:00
Ursprung d973dc7651
Commit 49c557205d

Datei anzeigen

@@ -102,18 +102,96 @@ async def generate_stale_deactivation_suggestions(
return created return created
async def generate_strategy_escalation_suggestions(db: aiosqlite.Connection) -> int:
"""Erzeugt deactivate_source-Vorschläge für Quellen, bei denen die fetch_strategy
bereits eskaliert wurde (googlebot oder paywall) und der Reachability-Check
trotzdem error meldet.
Beispiel: Rheinische Post hat fetch_strategy=googlebot, kriegt aber HTTP 403.
-> Strategie greift nicht, Quelle ist faktisch nicht abrufbar. Vorschlag: deaktivieren.
Doppel-Vermeidung wie in der Karteileichen-Heuristik: nur wenn noch kein pending
deactivate-Vorschlag für die source_id existiert.
Returns: Anzahl neu erstellter Vorschläge.
"""
cursor = await db.execute(
"""
SELECT s.id, s.name, s.url, s.domain, s.fetch_strategy, h.message
FROM sources s
JOIN source_health_checks h ON h.source_id = s.id
WHERE s.status = 'active'
AND s.fetch_strategy IN ('googlebot', 'paywall')
AND h.check_type = 'reachability'
AND h.status = 'error'
"""
)
candidates = [dict(row) for row in await cursor.fetchall()]
if not candidates:
return 0
cursor = await db.execute(
"SELECT DISTINCT source_id FROM source_suggestions "
"WHERE status = 'pending' AND suggestion_type = 'deactivate_source' "
"AND source_id IS NOT NULL"
)
already_pending = {row["source_id"] for row in await cursor.fetchall()}
created = 0
for c in candidates:
sid = c["id"]
if sid in already_pending:
continue
title = f"{c['name']} (ID {sid}) - Strategie greift nicht"
description = (
f"Quelle: {c['name']} | URL: {c['url']} | Domain: {c['domain'] or '-'}\n"
f"fetch_strategy='{c['fetch_strategy']}' wurde bereits zur Eskalation gesetzt, "
f"liefert beim Health-Check aber weiter einen Fehler:\n"
f" {c['message']}\n"
"Vorschlag: deaktivieren oder fetch_strategy='skip' setzen, damit die Quelle "
"den Health-Check nicht weiter verfälscht.\n"
"Hinweis: Quelle wurde automatisch erkannt. Bitte vor Annahme prüfen."
)
suggested_data = json.dumps(
{"action": "deactivate", "source_id": sid,
"reason": "fetch_strategy_failed", "current_strategy": c["fetch_strategy"]},
ensure_ascii=False,
)
await db.execute(
"INSERT INTO source_suggestions "
"(suggestion_type, title, description, source_id, suggested_data, "
" priority, status) VALUES "
"('deactivate_source', ?, ?, ?, ?, 'high', 'pending')",
(title, description, sid, suggested_data),
)
created += 1
if created > 0:
await db.commit()
logger.info(
"Strategie-Eskalations-Heuristik: %d neue deactivate-Vorschläge "
"(%d Kandidaten, %d bereits pending)",
created, len(candidates), len(already_pending),
)
return created
async def generate_suggestions(db: aiosqlite.Connection) -> int: async def generate_suggestions(db: aiosqlite.Connection) -> int:
"""Generiert Quellen-Vorschläge basierend auf Health-Checks und Lückenanalyse. """Generiert Quellen-Vorschläge basierend auf Health-Checks und Lückenanalyse.
Zwei Stufen: Drei Stufen:
1. Deterministisch: Karteileichen-Heuristik (article_count=0 oder >60d stumm) 1. Deterministisch: Karteileichen-Heuristik (article_count=0 oder >60d stumm)
erzeugt sofort deactivate_source-Vorschläge ohne KI-Aufruf. erzeugt sofort deactivate_source-Vorschläge ohne KI-Aufruf.
2. KI-basiert: Haiku schaut sich Quellensammlung + Health-Probleme an 2. Deterministisch: Strategie-Eskalations-Heuristik (fetch_strategy=googlebot
oder paywall, aber Reachability weiter error) erzeugt deactivate_source-
Vorschläge mit Priorität 'high'.
3. KI-basiert: Haiku schaut sich Quellensammlung + Health-Probleme an
und schlägt weitere Verbesserungen vor (add_source, deactivate_source, und schlägt weitere Verbesserungen vor (add_source, deactivate_source,
fix_url, ...). fix_url, ...).
Rückgabe ist die Gesamtzahl neu erzeugter Vorschläge beider Stufen. Rückgabe ist die Gesamtzahl neu erzeugter Vorschläge aller Stufen.
""" """
stale_count = await generate_stale_deactivation_suggestions(db) stale_count = await generate_stale_deactivation_suggestions(db)
strategy_count = await generate_strategy_escalation_suggestions(db)
logger.info("Starte Quellen-Vorschläge via Haiku...") logger.info("Starte Quellen-Vorschläge via Haiku...")
@@ -267,15 +345,15 @@ Nur das JSON-Array, kein anderer Text."""
await db.commit() await db.commit()
logger.info( logger.info(
f"Quellen-Vorschläge: {count} neue Vorschläge generiert via Haiku " f"Quellen-Vorschläge: {count} neue Vorschläge generiert via Haiku "
f"(+{stale_count} aus Karteileichen-Heuristik) " f"(+{stale_count} Karteileichen, +{strategy_count} Strategie-Eskalation) "
f"(Haiku: {usage.input_tokens} in / {usage.output_tokens} out / " f"(Haiku: {usage.input_tokens} in / {usage.output_tokens} out / "
f"${usage.cost_usd:.4f})" f"${usage.cost_usd:.4f})"
) )
return count + stale_count return count + stale_count + strategy_count
except Exception as e: except Exception as e:
logger.error(f"Fehler bei Quellen-Vorschlägen: {e}", exc_info=True) logger.error(f"Fehler bei Quellen-Vorschlägen: {e}", exc_info=True)
return stale_count return stale_count + strategy_count
async def apply_suggestion( async def apply_suggestion(