Plugin neuheiten: optionale LLM-Hilfsstufe für den Web-Quellen-Crawler
Neues Modul quellen/ki_hilfe.py (konsistent zum dedup-LLM-Muster): - Struktur-Erkennung: bei verdächtig leerem Parser-Ergebnis schlägt das LLM Pagination-/Filter-Folge-URLs vor; hart gefiltert auf gleiche Domain, max. 20 je Seite, nicht erreichbare Vorschläge brechen den Lauf nicht ab. - Feld-Extraktion: nur unsichere Treffer (fehlender Verlag, verdächtiger Titel); korrigiert ausschließlich titel/verlag/autor, niemals die URL. - Env-Konfiguration SPIELE_NEUHEITEN_KI_* (AKTIV default 0, KONFIDENZ_MIN default 0.7), OpenAI-kompatible Chat-Completions via httpx mit Timeout und genau einem Retry. - Fallback-Pflicht: ohne Konfiguration oder bei jedem Fehler läuft exakt der klassische Crawler; KI-Fehler blockieren den Sync nie. - Audit: KI-Eingriffe je Quelle und Lauf ins audit-log (Modell, Konfidenz, korrigierte Felder, verfolgte Folge-URLs). 20 neue Tests (gemocktes HTTP/LLM): Fallback-Fälle, Konfidenz-Schwelle, Domain-Filter, Max-20-Grenze, URL-Unveränderbarkeit, Audit.
This commit is contained in:
@@ -32,6 +32,7 @@ from typing import Callable
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from .quellen.ki_hilfe import KiHilfe, lade_konfiguration as lade_ki_konfiguration
|
||||
from .models import STATUS_NEUHEIT, Neuheit, QuellenStatus
|
||||
from .quellen import (
|
||||
ADAPTER_KLASSEN,
|
||||
@@ -108,20 +109,27 @@ class WebQuellenSyncService:
|
||||
dedup=None,
|
||||
client_fabrik: Callable[[dict], WebQuellenClient] | None = None,
|
||||
umgebung=None,
|
||||
ki_fabrik: Callable[[], KiHilfe] | None = None,
|
||||
audit=None,
|
||||
) -> None:
|
||||
self.session_factory = session_factory
|
||||
self.adapter_klassen = adapter_klassen
|
||||
self.dedup = dedup
|
||||
self.audit = audit
|
||||
self.umgebung = umgebung if umgebung is not None else os.environ
|
||||
self._client_fabrik = client_fabrik or self._standard_client_fabrik
|
||||
# KI-Hilfsstufe: Fabrik für Tests; ohne vollständige Konfiguration wird
|
||||
# nie ein Client gebaut (und damit nie ein LLM-Aufruf getätigt).
|
||||
self._ki_fabrik = ki_fabrik
|
||||
|
||||
# ---------- Gesamtdurchlauf ----------
|
||||
|
||||
def laufe(self) -> list[QuellenLaufZeile]:
|
||||
zeilen: list[QuellenLaufZeile] = []
|
||||
ki = self._ki_hilfe()
|
||||
for klasse in self.adapter_klassen:
|
||||
try:
|
||||
zeile = self._eine_quelle(klasse)
|
||||
zeile = self._eine_quelle(klasse, ki)
|
||||
except Exception as exc: # letzte Verteidigungslinie pro Quelle
|
||||
_logger.exception("Quellen-Sync fehlgeschlagen für %s", klasse.name)
|
||||
zeile = QuellenLaufZeile(
|
||||
@@ -141,7 +149,28 @@ class WebQuellenSyncService:
|
||||
|
||||
# ---------- Eine Quelle ----------
|
||||
|
||||
def _eine_quelle(self, klasse: type[QuellenAdapter]) -> QuellenLaufZeile:
|
||||
def _ki_hilfe(self) -> KiHilfe | None:
|
||||
"""KI-Hilfsstufe nur bei vollständiger Konfiguration (sonst None).
|
||||
|
||||
Die Aktivierungsprüfung gilt vor jeder injizierten Fabrik — ist die
|
||||
Hilfsstufe aus oder unvollständig konfiguriert, wird nie ein Client
|
||||
gebaut und damit nie ein LLM-Aufruf getätigt.
|
||||
"""
|
||||
try:
|
||||
konfiguration = lade_ki_konfiguration(self.umgebung)
|
||||
if not konfiguration.vollstaendig:
|
||||
return None
|
||||
if self._ki_fabrik is not None:
|
||||
return self._ki_fabrik()
|
||||
return KiHilfe(konfiguration)
|
||||
except Exception:
|
||||
# Auch ein kaputtes KI-Setup darf den Sync nie blockieren.
|
||||
_logger.exception("neuheiten/KI: Hilfsstufe nicht verfügbar — klassischer Lauf.")
|
||||
return None
|
||||
|
||||
def _eine_quelle(
|
||||
self, klasse: type[QuellenAdapter], ki: KiHilfe | None = None
|
||||
) -> QuellenLaufZeile:
|
||||
zeile = QuellenLaufZeile(klasse.name, klasse.anzeigename)
|
||||
zeile.url = klasse.start_urls[0] if klasse.start_urls else None
|
||||
if not quelle_aktiv(klasse.name, self.umgebung):
|
||||
@@ -152,6 +181,7 @@ class WebQuellenSyncService:
|
||||
client = self._client_fabrik(self._validatoren_laden(klasse.name))
|
||||
try:
|
||||
adapter = klasse(client)
|
||||
adapter.ki_hilfe = ki # None = rein regelbasiert (Fallback-Pflicht)
|
||||
try:
|
||||
ergebnis = adapter.sammle()
|
||||
except Exception as exc:
|
||||
@@ -160,6 +190,7 @@ class WebQuellenSyncService:
|
||||
zeile.fehlermeldung = str(exc)[:300]
|
||||
return zeile
|
||||
zeile.seiten = ergebnis.seiten
|
||||
self._ki_auditieren(klasse.name, ergebnis)
|
||||
if ergebnis.unveraendert:
|
||||
zeile.unveraendert = True
|
||||
else:
|
||||
@@ -263,6 +294,37 @@ class WebQuellenSyncService:
|
||||
return True
|
||||
return not ergebnis.hat_konflikte
|
||||
|
||||
# ---------- KI-Audit ----------
|
||||
|
||||
def _ki_auditieren(self, quelle: str, ergebnis) -> None:
|
||||
"""KI-Eingriffe des Laufs ins audit-log schreiben (best effort).
|
||||
|
||||
Ein Eintrag je Quelle und Lauf mit Modell, Konfidenz, korrigierten
|
||||
Feldern und den verfolgten KI-Folge-URLs. Ohne audit-log (Plugin
|
||||
nicht geladen) oder bei Fehlern wird nur protokolliert — der Sync
|
||||
wird nie blockiert.
|
||||
"""
|
||||
eingriffe = list(getattr(ergebnis, "ki_eingriffe", []) or [])
|
||||
folge_urls = list(getattr(ergebnis, "ki_folge_urls", []) or [])
|
||||
if not eingriffe and not folge_urls:
|
||||
return
|
||||
if self.audit is None:
|
||||
_logger.info(
|
||||
"Quelle %s: %d KI-Korrekturen, %d KI-Folge-URLs (kein audit-log geladen).",
|
||||
quelle, len(eingriffe), len(folge_urls),
|
||||
)
|
||||
return
|
||||
details = {
|
||||
"quelle": quelle,
|
||||
"modell": eingriffe[0].get("modell", "") if eingriffe else "",
|
||||
"eingriffe": eingriffe,
|
||||
"folge_urls": folge_urls,
|
||||
}
|
||||
try:
|
||||
self.audit.log_sync(None, "korrigiert", "neuheiten_ki_crawler", quelle, details)
|
||||
except Exception:
|
||||
_logger.exception("neuheiten/KI: Audit-Eintrag konnte nicht geschrieben werden.")
|
||||
|
||||
# ---------- Statusprotokoll ----------
|
||||
|
||||
def _status_speichern(self, zeile: QuellenLaufZeile) -> None:
|
||||
|
||||
Reference in New Issue
Block a user