Promote develop -> main
This commit was merged in pull request #22.
Dieser Commit ist enthalten in:
@@ -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 json
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
@@ -10,9 +10,111 @@ from config import CLAUDE_MODEL_FAST
|
|||||||
|
|
||||||
logger = logging.getLogger("osint.source_suggester")
|
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:
|
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...")
|
logger.info("Starte Quellen-Vorschläge via Haiku...")
|
||||||
|
|
||||||
# 1. Aktuelle Quellen laden
|
# 1. Aktuelle Quellen laden
|
||||||
@@ -164,15 +266,16 @@ 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 "
|
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"(Haiku: {usage.input_tokens} in / {usage.output_tokens} out / "
|
||||||
f"${usage.cost_usd:.4f})"
|
f"${usage.cost_usd:.4f})"
|
||||||
)
|
)
|
||||||
return count
|
return count + stale_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 0
|
return stale_count
|
||||||
|
|
||||||
|
|
||||||
async def apply_suggestion(
|
async def apply_suggestion(
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren