From 49c557205d391cd892e8031b81e4724430b393fb Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 9 May 2026 15:26:05 +0000 Subject: [PATCH] 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. --- src/services/source_suggester.py | 90 +++++++++++++++++++++++++++++--- 1 file changed, 84 insertions(+), 6 deletions(-) diff --git a/src/services/source_suggester.py b/src/services/source_suggester.py index 555ee52..d84c30a 100644 --- a/src/services/source_suggester.py +++ b/src/services/source_suggester.py @@ -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( -- 2.49.1