Plugin dedup: LLM als zweite Stufe für Grenzfälle
Regelbasierte Dedup-Prüfung bleibt maßgeblich; nur Grenzfälle gehen an ein LLM: Titel mit Wort-Überschneidung unter der Fuzzy-Schwelle (Editionen, Big Boxes, Übersetzungen wie 'El Grande Big Box' vs. 'El Grande: 25 Jahre') und ein ergänzender Call zum Verlags-/Titel-Hinweis bei Verlags-Konflikt (Verlags-Aliase). Klare Treffer und Non-Treffer bleiben regelbasiert. - Neu: ki_pruefung.py — Env-Konfiguration (SPIELE_DEDUP_LLM_*), robuster JSON-Parser (Code-Fences, Zusatztext), OpenAI-kompatibler Client via httpx mit Timeout und genau einem Retry; liefert bei jedem Fehler None. - pruefung.py: bestätigte Grenzfälle laufen in die üblichen Prüfungen a)-d) zurück; deutscher Titel füllt die Titel-Empfehlung, Verlags-Empfehlung landet im Konflikt-Detail. Neues Feld PruefErgebnis.ki_befunde. - __init__.py: Aktivierungsprüfung vor jeder Fabrik — aus/unvollständig heißt nie ein LLM-Aufruf; KI-Befunde werden über die audit-log-API protokolliert (Modell, Konfidenz, Entscheidung), best effort. - Tests: gemockte HTTP-Antworten (MockTransport/Fakes), Fallback-Fälle (aktiv=0, kein Key, Timeout, kaputtes JSON), Konfidenz-Schwelle, Parsing-Robustheit; kein echter LLM-Call in CI. - README.md um die neuen Env-Variablen und den Zweitprüfungs-Abschnitt ergänzt.
This commit is contained in:
@@ -15,6 +15,17 @@ in `pruefung.py` beschrieben. Jede Prüfung wird in der eigenen Tabelle
|
||||
Zusatzdaten von BoardGameGeek (Alternate-Names, Erweiterungs-Relationen)
|
||||
können über SPIELE_DEDUP_BGG_AKTIV=0 abgeschaltet werden (Standard: an);
|
||||
ohne Netzwerk degradiert die Prüfung automatisch auf die Heuristik.
|
||||
|
||||
Als zweite Stufe kann ein LLM Grenzfälle bewerten — Titel mit
|
||||
Wort-Überschneidung unter der Fuzzy-Schwelle (Editionen, Big Boxes,
|
||||
Übersetzungen) und Verlags-Konflikte mit ähnlichem Titel (Verlags-Aliase).
|
||||
Standardmäßig aus (`SPIELE_DEDUP_LLM_AKTIV=0`); konfiguriert wird eine
|
||||
OpenAI-kompatible Chat-Completions-API über `SPIELE_DEDUP_LLM_*`-Variablen
|
||||
(siehe ki_pruefung.py / README). Ohne Aktivierung, ohne Key oder bei jedem
|
||||
Fehler verhält sich die Prüfung exakt wie rein regelbasiert; KI-Befunde
|
||||
unterhalb der Mindest-Konfidenz (`SPIELE_DEDUP_LLM_KONFIDENZ_MIN`,
|
||||
Standard 0.7) werden verworfen. Übernommene Befunde laufen in die üblichen
|
||||
Prüfungen a)–d) zurück und werden im audit-log protokolliert.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -30,6 +41,7 @@ from redaktionskern.auth.models import User
|
||||
from redaktionskern.contracts import BasePlugin, Migration, NavEntry
|
||||
|
||||
from .bgg import BggPruefClient
|
||||
from .ki_pruefung import KiPruefClient, lade_konfiguration
|
||||
from .models import Pruefung
|
||||
from .pruefung import KONFLIKT_ANZEIGE, PruefErgebnis, fuehre_pruefung_durch
|
||||
|
||||
@@ -59,6 +71,12 @@ class DedupPlugin(BasePlugin):
|
||||
#: Von Tests überschreibbare Fabrik für den BGG-Hilfsclient.
|
||||
self.bgg_client_fabrik = None
|
||||
|
||||
#: Konfiguration der KI-Zweitprüfung (wird in on_load aus Env gelesen).
|
||||
self.ki_konfiguration = lade_konfiguration({})
|
||||
self._ki_client: KiPruefClient | None = None
|
||||
#: Von Tests überschreibbare Fabrik für den KI-Client (Mock-Transport).
|
||||
self.ki_client_fabrik = None
|
||||
|
||||
self._routen_registrieren()
|
||||
|
||||
# ---------- Plugin-Vertrag ----------
|
||||
@@ -72,11 +90,17 @@ class DedupPlugin(BasePlugin):
|
||||
def on_load(self, context) -> None:
|
||||
super().on_load(context)
|
||||
self._bgg_aktiv = os.environ.get("SPIELE_DEDUP_BGG_AKTIV", "1").strip() != "0"
|
||||
# KI-Zweitprüfung (Standard aus, siehe ki_pruefung.py): ohne Key oder
|
||||
# mit SPIELE_DEDUP_LLM_AKTIV=0 läuft die Prüfung exakt wie bisher.
|
||||
self.ki_konfiguration = lade_konfiguration()
|
||||
|
||||
def on_unload(self) -> None:
|
||||
if self._bgg_client is not None:
|
||||
self._bgg_client.schliessen()
|
||||
self._bgg_client = None
|
||||
if self._ki_client is not None:
|
||||
self._ki_client.schliessen()
|
||||
self._ki_client = None
|
||||
super().on_unload()
|
||||
|
||||
# ---------- Öffentliche Prüf-API für andere Plugins ----------
|
||||
@@ -102,6 +126,7 @@ class DedupPlugin(BasePlugin):
|
||||
id_wert = None
|
||||
|
||||
client = self._hilfsclient()
|
||||
ki_client = self._ki_hilfsclient()
|
||||
try:
|
||||
ergebnis = fuehre_pruefung_durch(
|
||||
self.context.session_factory,
|
||||
@@ -110,15 +135,57 @@ class DedupPlugin(BasePlugin):
|
||||
verlag=verlag,
|
||||
bgg_id=id_wert,
|
||||
user_id=user.id if user is not None else None,
|
||||
ki_client=ki_client,
|
||||
ki_konfidenz_min=self.ki_konfiguration.konfidenz_min,
|
||||
)
|
||||
finally:
|
||||
if client is not None:
|
||||
client.schliessen()
|
||||
if ki_client is not None:
|
||||
ki_client.schliessen()
|
||||
self._protokolliere(ergebnis, user)
|
||||
await self._auditiere_ki_befunde(ergebnis, user)
|
||||
return ergebnis
|
||||
|
||||
# ---------- Internas ----------
|
||||
|
||||
def _ki_hilfsclient(self) -> KiPruefClient | None:
|
||||
"""KI-Client nur bei aktivierter, vollständiger Konfiguration (sonst None).
|
||||
|
||||
Die Aktivierungsprüfung gilt vor jeder injizierten Fabrik — ist die
|
||||
Zweitprüfung deaktiviert oder unvollständig konfiguriert, wird nie
|
||||
ein Client gebaut und damit nie ein LLM-Aufruf getätigt.
|
||||
"""
|
||||
if self.context is None or not self.ki_konfiguration.vollstaendig:
|
||||
return None
|
||||
if self.ki_client_fabrik is not None:
|
||||
return self.ki_client_fabrik()
|
||||
if self._ki_client is None:
|
||||
self._ki_client = KiPruefClient(self.ki_konfiguration)
|
||||
return self._ki_client
|
||||
|
||||
async def _auditiere_ki_befunde(self, ergebnis: PruefErgebnis, user) -> None:
|
||||
"""LLM-Befunde ins audit-log schreiben (best effort, nie blockierend).
|
||||
|
||||
Ein Eintrag je Prüfung mit allen KI-Entscheidungen: Modell, Konfidenz
|
||||
und Entscheidung je bewertetem Grenzfall.
|
||||
"""
|
||||
if not ergebnis.ki_befunde:
|
||||
return
|
||||
registry = self.context.registry if self.context is not None else None
|
||||
audit = registry.get("audit-log") if registry is not None else None
|
||||
if audit is None:
|
||||
return
|
||||
details = {
|
||||
"titel": ergebnis.titel,
|
||||
"modell": ergebnis.ki_befunde[0].get("modell", ""),
|
||||
"befunde": ergebnis.ki_befunde,
|
||||
}
|
||||
try:
|
||||
await audit.log(user, "geprüft", "dedup_ki", None, details)
|
||||
except Exception:
|
||||
_logger.exception("dedup: KI-Befunde konnten nicht auditiert werden.")
|
||||
|
||||
def _hilfsclient(self) -> BggPruefClient | None:
|
||||
if self.bgg_client_fabrik is not None:
|
||||
return self.bgg_client_fabrik()
|
||||
|
||||
Reference in New Issue
Block a user