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:
@@ -21,6 +21,7 @@ die Testsuite führt keine echten Netzwerkaufrufe durch.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
import xml.etree.ElementTree # noqa: F401 (dokumentiert: keine XML-Nutzung hier)
|
||||
@@ -31,6 +32,10 @@ from typing import ClassVar
|
||||
import httpx
|
||||
from rapidfuzz import fuzz
|
||||
|
||||
from .ki_hilfe import KiHilfe
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
#: Freundlicher User-Agent für alle Redaktions-Crawler (Courtesy-Regeln).
|
||||
USER_AGENT = "SpieleRedaktionBot/1.0 (Redaktions-Tool; Kontakt siehe Repo)"
|
||||
|
||||
@@ -76,6 +81,12 @@ class SammelErgebnis:
|
||||
#: True = mindestens eine Seite kam per 304 als unverändert zurück;
|
||||
#: der Lauf wurde dann abgekürzt (Quelle übersprungen, kein Fehler).
|
||||
unveraendert: bool = False
|
||||
#: KI-Feldkorrekturen dieses Laufs (Protokoll fürs Audit-Log); leer bei
|
||||
#: rein regelbasiertem Lauf.
|
||||
ki_eingriffe: list[dict] = field(default_factory=list)
|
||||
#: Von der KI vorgeschlagene und tatsächlich verfolgte Folge-URLs
|
||||
#: (Struktur-Erkennung); leer bei rein regelbasiertem Lauf.
|
||||
ki_folge_urls: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
def jahr_aus_datumsangabe(angabe: str | None) -> int | None:
|
||||
@@ -236,18 +247,40 @@ class QuellenAdapter:
|
||||
|
||||
def __init__(self, client: WebQuellenClient) -> None:
|
||||
self.client = client
|
||||
#: Optionale LLM-Hilfsstufe (`ki_hilfe.KiHilfe` oder gleiches Protokoll).
|
||||
#: None (Standard) = rein regelbasiert; KI-Fehler blockieren den Lauf nie.
|
||||
self.ki_hilfe: KiHilfe | None = None
|
||||
|
||||
def sammle(self) -> SammelErgebnis:
|
||||
"""Crawlt die Quelle: Startseiten + gefundene Folge-Links (BFS)."""
|
||||
"""Crawlt die Quelle: Startseiten + gefundene Folge-Links (BFS).
|
||||
|
||||
Mit gesetzter `ki_hilfe`: Bei verdächtig leer geparsten Seiten darf
|
||||
das LLM Pagination-/Filter-Folge-URLs vorschlagen (nur gleiche
|
||||
Domain, max. 20), unsichere Treffer werden per LLM nachgebessert
|
||||
(nur titel/verlag/autor, nie die URL). Jeder KI-Fehler fällt auf den
|
||||
klassischen Befund zurück.
|
||||
"""
|
||||
ergebnis = SammelErgebnis()
|
||||
warteschlange = list(self.start_urls)
|
||||
besucht: set[str] = set()
|
||||
ki_vorschlaege: set[str] = set()
|
||||
while warteschlange and len(besucht) < self.max_seiten:
|
||||
url = warteschlange.pop(0)
|
||||
if url in besucht:
|
||||
continue
|
||||
besucht.add(url)
|
||||
antwort = self.client.hole(url)
|
||||
try:
|
||||
antwort = self.client.hole(url)
|
||||
except Exception:
|
||||
if url in ki_vorschlaege:
|
||||
# Eine nicht erreichbare KI-Suggestion darf den klassischen
|
||||
# Lauf nie abbrechen — überspringen statt Fehler.
|
||||
_logger.warning(
|
||||
"KI-vorgeschlagene URL nicht erreichbar (%s) — übersprungen.",
|
||||
url,
|
||||
)
|
||||
continue
|
||||
raise
|
||||
if antwort.status_code == 304:
|
||||
# Unverändert → Quelle überspringen (kein Fehler).
|
||||
ergebnis.unveraendert = True
|
||||
@@ -258,8 +291,48 @@ class QuellenAdapter:
|
||||
for link in folge_links:
|
||||
if link not in besucht:
|
||||
warteschlange.append(link)
|
||||
if self.ki_hilfe is not None:
|
||||
for link in self._ki_folge_urls(treffer, antwort.content, url, besucht):
|
||||
if link not in besucht and link not in warteschlange:
|
||||
warteschlange.append(link)
|
||||
ki_vorschlaege.add(link)
|
||||
ergebnis.ki_folge_urls.append(link)
|
||||
if self.ki_hilfe is not None:
|
||||
self._ki_nachbessern(ergebnis)
|
||||
return ergebnis
|
||||
|
||||
def _ki_folge_urls(
|
||||
self,
|
||||
treffer: list[QuellenTreffer],
|
||||
inhalt: bytes,
|
||||
url: str,
|
||||
besucht: set[str],
|
||||
) -> list[str]:
|
||||
"""Struktur-Erkennung der Hilfsstufe (best effort — wirft nicht)."""
|
||||
try:
|
||||
return self.ki_hilfe.folge_urls_bei_bedarf(
|
||||
len(treffer), inhalt, url, besucht
|
||||
)
|
||||
except Exception:
|
||||
_logger.exception(
|
||||
"KI-Struktur-Erkennung fehlgeschlagen (%s) — klassischer Lauf bleibt.",
|
||||
url,
|
||||
)
|
||||
return []
|
||||
|
||||
def _ki_nachbessern(self, ergebnis: SammelErgebnis) -> None:
|
||||
"""Feld-Korrektur unsicherer Treffer (best effort — wirft nicht)."""
|
||||
try:
|
||||
korrigiert, eingriffe = self.ki_hilfe.treffer_nachbessern(ergebnis.treffer)
|
||||
except Exception:
|
||||
_logger.exception(
|
||||
"KI-Feldkorrektur fehlgeschlagen — klassischer Parserbefund bleibt."
|
||||
)
|
||||
return
|
||||
if korrigiert is not None:
|
||||
ergebnis.treffer = korrigiert
|
||||
ergebnis.ki_eingriffe = eingriffe or []
|
||||
|
||||
def seite_verarbeiten(
|
||||
self, inhalt: bytes, url: str
|
||||
) -> tuple[list[QuellenTreffer], list[str]]:
|
||||
|
||||
Reference in New Issue
Block a user