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:
@@ -102,18 +102,96 @@ async def generate_stale_deactivation_suggestions(
|
||||
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:
|
||||
"""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)
|
||||
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,
|
||||
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)
|
||||
strategy_count = await generate_strategy_escalation_suggestions(db)
|
||||
|
||||
logger.info("Starte Quellen-Vorschläge via Haiku...")
|
||||
|
||||
@@ -267,15 +345,15 @@ Nur das JSON-Array, kein anderer Text."""
|
||||
await db.commit()
|
||||
logger.info(
|
||||
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"${usage.cost_usd:.4f})"
|
||||
)
|
||||
return count + stale_count
|
||||
return count + stale_count + strategy_count
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei Quellen-Vorschlägen: {e}", exc_info=True)
|
||||
return stale_count
|
||||
return stale_count + strategy_count
|
||||
|
||||
|
||||
async def apply_suggestion(
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren