banyaro/backend/scraper/breed_evaluator.py
rene f7370028da KI-Vision-Model, Breed-Scraper, Karte/Routen + Release v1292
Parallele Arbeit (auf Staging mitgetestet): KI-Vision-Model (VISION_MODEL in
ki.py/routes, im KI-Status sichtbar), Breed-Scraper-Anpassungen
(breed_enricher/breed_evaluator, evaluate_enrichment mit user_id),
Karten-/Routen-Änderungen (map.js, routes.js), kleinere UI-Anpassungen
(admin.js, components.css), docker-compose, MARKETING, nav-loop-Test.

Version-Bump auf 1292 (VERSION, sw.js, app.js, index.html, landing.html).
2026-06-14 20:23:21 +02:00

145 lines
5 KiB
Python

"""
Qualitätsbewertung der KI-Rassen-Anreicherung via Claude (LLM-as-Judge).
Bewertet eine Zufallsstichprobe angereicherter Rassen nach:
- Vollständigkeit (alle Felder befüllt?)
- Korrektheit (plausible Fakten?)
- Sprachqualität (natürliches Deutsch?)
- Konsistenz (Felder stimmen untereinander überein?)
"""
import json
import logging
import os
import asyncio
logger = logging.getLogger(__name__)
_EVAL_PROMPT = '''\
Du bist ein Qualitätsprüfer für Hunderassen-Daten. Bewerte den folgenden \
Datensatz für die Rasse "{name}" auf einer Skala von 1-5.
Datensatz:
{data}
Antworte NUR als JSON:
{{
"vollstaendigkeit": <1-5>,
"korrektheit": <1-5>,
"sprachqualitaet": <1-5>,
"konsistenz": <1-5>,
"gesamt": <1-5>,
"hinweis": "Kurze Begründung oder auffällige Mängel (max 1 Satz)"
}}
Bewertungskriterien:
- Vollständigkeit: Sind beschreibung, vorkommen_de, groesse, gewicht, \
lebensdauer, aktivitaet, erfahrung, kinder_geeignet, wohnung_geeignet, \
temperament alle befüllt?
- Korrektheit: Stimmen die Angaben mit bekannten Fakten überein?
- Sprachqualität: Ist der deutsche Text natürlich, fehlerfrei und informativ?
- Konsistenz: Passen die Felder zueinander (z.B. Gewicht zur Größe, \
Aktivität zur Erfahrung)?
'''
async def evaluate_enrichment(sample_size: int = 20, user_id: int | None = None) -> dict:
"""
Bewertet `sample_size` zufällig gewählte angereicherte Rassen als LLM-as-Judge.
Läuft über die zentrale KI-Abstraktion (ki.complete). Admins/Moderatoren werden
dort Cloud-priorisiert (Claude); ist die Cloud nicht erreichbar, fällt die
Bewertung sauber auf das lokale Modell zurück, statt hart abzubrechen.
Returns dict mit aggregierten Scores und Einzelergebnissen.
"""
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from database import db
import ki
if ki.KI_MODE == "off":
raise RuntimeError("KI ist deaktiviert (KI_MODE=off) — Evaluierung nicht möglich.")
with db() as conn:
rassen = conn.execute(
"""SELECT name, beschreibung, vorkommen_de, groesse,
gewicht_min_kg, gewicht_max_kg, lebensdauer,
aktivitaet, erfahrung, kinder_geeignet,
wohnung_geeignet, temperament, ki_model
FROM wiki_rassen
WHERE ki_enriched = 1
AND (ki_model IS NULL OR ki_model NOT LIKE 'claude%')
ORDER BY RANDOM()
LIMIT ?""",
(sample_size,),
).fetchall()
if not rassen:
return {"error": "Keine angereicherten Rassen gefunden."}
_EVAL_SYSTEM = "Du bist ein präziser Qualitätsprüfer. Antworte ausschließlich als JSON."
results = []
sources = set()
totals = {"vollstaendigkeit": 0, "korrektheit": 0,
"sprachqualitaet": 0, "konsistenz": 0, "gesamt": 0}
for rasse in rassen:
name = rasse["name"]
data = {
"beschreibung": rasse["beschreibung"],
"vorkommen_de": rasse["vorkommen_de"],
"groesse": rasse["groesse"],
"gewicht_min_kg": rasse["gewicht_min_kg"],
"gewicht_max_kg": rasse["gewicht_max_kg"],
"lebensdauer": rasse["lebensdauer"],
"aktivitaet": rasse["aktivitaet"],
"erfahrung": rasse["erfahrung"],
"kinder_geeignet": rasse["kinder_geeignet"],
"wohnung_geeignet": rasse["wohnung_geeignet"],
"temperament": rasse["temperament"],
}
prompt = _EVAL_PROMPT.format(
name=name,
data=json.dumps(data, ensure_ascii=False, indent=2),
)
try:
raw, source = await ki.complete(
prompt,
system=_EVAL_SYSTEM,
max_tokens=256,
json_mode=True,
user_id=user_id,
return_source=True,
)
sources.add(source)
# JSON extrahieren (lokale Modelle wrappen gern in ```json … ```)
import re
match = re.search(r"\{[\s\S]+\}", raw)
scores = json.loads(match.group(0)) if match else {}
entry = {"name": name, **scores}
results.append(entry)
for key in totals:
totals[key] += scores.get(key, 0)
except Exception as e:
logger.error("Evaluierung fehlgeschlagen für %s: %s", name, e)
results.append({"name": name, "error": str(e)})
await asyncio.sleep(0.5)
count = len([r for r in results if "error" not in r])
averages = {k: round(v / count, 2) for k, v in totals.items()} if count else {}
judge_source = "/".join(sorted(sources)) if sources else "unbekannt"
return {
"sample_size": len(rassen),
"evaluated": count,
"averages": averages,
"judge_source": judge_source, # "cloud" (Claude) oder "local" (LM Studio)
"results": results,
}