From d973dc765145ed6bdd781c95333a18ab3b6ab6ce Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 9 May 2026 15:09:32 +0000 Subject: [PATCH] feat(source_suggester): Karteileichen-Heuristik vor Haiku-Stufe Neue Funktion generate_stale_deactivation_suggestions(db, days_threshold=60) erzeugt deactivate_source-Vorschlaege fuer aktive Quellen, die entweder - noch nie einen Artikel geliefert haben (article_count=0), oder - seit mehr als 60 Tagen stumm sind (last_seen_at < now - 60d). Reine SQL-Heuristik, kein KI-Aufruf. Wird zu Beginn von generate_suggestions ausgefuehrt, vor dem bestehenden Haiku-Lauf. Doppel-Vermeidung: existiert fuer eine source_id schon ein pending deactivate_source-Vorschlag, wird kein neuer eingefuegt. Hintergrund: Aktuell sind 106 Quellen mit Warning "Noch nie Artikel geliefert" und einige weitere mit "Letzter Artikel vor 49 Tagen" o.ae. Diese fluten den Health-Status-Tab. Mit der neuen Heuristik wandern sie automatisch in die Vorschlaege-Liste, wo der Admin sie per Klick deaktivieren kann. Schwelle 60 Tage als Konstante STALE_DEACTIVATE_THRESHOLD_DAYS oben in der Datei, falls spaeter noch justiert werden soll. --- src/services/source_suggester.py | 113 +++++++++++++++++++++++++++++-- 1 file changed, 108 insertions(+), 5 deletions(-) diff --git a/src/services/source_suggester.py b/src/services/source_suggester.py index 2e41937..555ee52 100644 --- a/src/services/source_suggester.py +++ b/src/services/source_suggester.py @@ -1,4 +1,4 @@ -"""KI-gestützte Quellen-Vorschläge via Haiku.""" +"""KI-gestützte Quellen-Vorschläge via Haiku + deterministische Karteileichen-Heuristik.""" import json import logging import re @@ -10,9 +10,111 @@ from config import CLAUDE_MODEL_FAST logger = logging.getLogger("osint.source_suggester") +# Schwelle für "stumm seit": eine Quelle, die seit mehr als so vielen Tagen +# keinen Artikel mehr geliefert hat, gilt als Karteileichen-Kandidat. +STALE_DEACTIVATE_THRESHOLD_DAYS = 60 + + +async def generate_stale_deactivation_suggestions( + db: aiosqlite.Connection, + days_threshold: int = STALE_DEACTIVATE_THRESHOLD_DAYS, +) -> int: + """Erzeugt deactivate_source-Vorschläge für Karteileichen-Quellen. + + Karteileiche = aktive Quelle, die entweder noch nie einen Artikel geliefert hat + (article_count = 0) oder seit mehr als days_threshold Tagen stumm ist + (last_seen_at älter als die Schwelle). Reine SQL-Heuristik, kein KI-Aufruf. + + Doppel-Vermeidung: existiert bereits ein pending deactivate-Vorschlag für + dieselbe source_id, wird kein neuer erzeugt. + + Returns: Anzahl neu erstellter Vorschläge. + """ + cursor = await db.execute( + f""" + SELECT id, name, url, domain, article_count, last_seen_at + FROM sources + WHERE status = 'active' + AND ( + COALESCE(article_count, 0) = 0 + OR (last_seen_at IS NOT NULL + AND last_seen_at < datetime('now', '-{int(days_threshold)} days')) + ) + """ + ) + 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 + if (c["article_count"] or 0) == 0: + reason = "Hat seit Anlage noch nie einen Artikel geliefert." + else: + reason = ( + f"Letzter Artikel vor mehr als {days_threshold} Tagen " + f"(last_seen_at={c['last_seen_at']})." + ) + title = f"{c['name']} (ID {sid}) - Karteileiche, deaktivieren?" + description = ( + f"Quelle: {c['name']} | URL: {c['url']} | Domain: {c['domain'] or '-'}\n" + f"Begründung: {reason}\n" + f"article_count={c['article_count'] or 0}, " + f"last_seen_at={c['last_seen_at'] or 'NULL'}\n" + "Hinweis: Quelle wurde automatisch als inaktiv erkannt. " + "Bitte vor Annahme prüfen, ob sie wirklich nicht mehr gebraucht wird." + ) + suggested_data = json.dumps( + {"action": "deactivate", "source_id": sid}, ensure_ascii=False + ) + await db.execute( + "INSERT INTO source_suggestions " + "(suggestion_type, title, description, source_id, suggested_data, " + " priority, status) VALUES " + "('deactivate_source', ?, ?, ?, ?, 'medium', 'pending')", + (title, description, sid, suggested_data), + ) + created += 1 + + if created > 0: + await db.commit() + logger.info( + "Karteileichen-Heuristik: %d neue deactivate-Vorschläge erstellt " + "(%d Kandidaten, %d bereits pending)", + created, len(candidates), len(already_pending), + ) + else: + logger.info( + "Karteileichen-Heuristik: keine neuen Vorschläge " + "(%d Kandidaten, alle bereits pending)", + len(candidates), + ) + return created + 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: + 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 + und schlägt weitere Verbesserungen vor (add_source, deactivate_source, + fix_url, ...). + Rückgabe ist die Gesamtzahl neu erzeugter Vorschläge beider Stufen. + """ + stale_count = await generate_stale_deactivation_suggestions(db) + logger.info("Starte Quellen-Vorschläge via Haiku...") # 1. Aktuelle Quellen laden @@ -164,15 +266,16 @@ Nur das JSON-Array, kein anderer Text.""" await db.commit() logger.info( - f"Quellen-Vorschläge: {count} neue Vorschläge generiert " + f"Quellen-Vorschläge: {count} neue Vorschläge generiert via Haiku " + f"(+{stale_count} aus Karteileichen-Heuristik) " f"(Haiku: {usage.input_tokens} in / {usage.output_tokens} out / " f"${usage.cost_usd:.4f})" ) - return count + return count + stale_count except Exception as e: logger.error(f"Fehler bei Quellen-Vorschlägen: {e}", exc_info=True) - return 0 + return stale_count async def apply_suggestion( -- 2.49.1