Promote develop -> main (source_suggester: Karteileichen-Heuristik) #22
@@ -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(
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren