diff --git a/backend/routes/admin.py b/backend/routes/admin.py index 5f9c4ee..3804ade 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -590,6 +590,16 @@ async def wiki_enrich(data: WikiEnrichBody, user=Depends(require_mod)): return {"enriched": enriched, "remaining": remaining} +# ------------------------------------------------------------------ +# GET /api/admin/wiki/evaluate — LLM-as-Judge Qualitätsbewertung +# ------------------------------------------------------------------ +@router.get("/wiki/evaluate") +async def wiki_evaluate(sample: int = 20, user=Depends(require_mod)): + from scraper.breed_evaluator import evaluate_enrichment + sample = max(5, min(sample, 50)) + return await evaluate_enrichment(sample_size=sample) + + # ------------------------------------------------------------------ # POST /api/admin/wiki/translate-temperament — einmalige Migration # ------------------------------------------------------------------ diff --git a/backend/scraper/breed_evaluator.py b/backend/scraper/breed_evaluator.py new file mode 100644 index 0000000..af655f7 --- /dev/null +++ b/backend/scraper/breed_evaluator.py @@ -0,0 +1,142 @@ +""" +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) -> dict: + """ + Bewertet `sample_size` zufällig gewählte angereicherte Rassen via Claude. + + 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 + + ANTHROPIC_KEY = os.getenv("ANTHROPIC_API_KEY", "") + if not ANTHROPIC_KEY: + raise RuntimeError("ANTHROPIC_API_KEY nicht gesetzt — Evaluierung benötigt Cloud.") + + 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 + FROM wiki_rassen + WHERE ki_enriched = 1 + ORDER BY RANDOM() + LIMIT ?""", + (sample_size,), + ).fetchall() + + if not rassen: + return {"error": "Keine angereicherten Rassen gefunden."} + + import anthropic + client = anthropic.Anthropic(api_key=ANTHROPIC_KEY) + + results = [] + 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: + def _call(): + return client.messages.create( + model="claude-haiku-4-5-20251001", + max_tokens=256, + system=[{ + "type": "text", + "text": "Du bist ein präziser Qualitätsprüfer. Antworte ausschließlich als JSON.", + "cache_control": {"type": "ephemeral"}, + }], + messages=[{"role": "user", "content": prompt}], + ) + loop = asyncio.get_event_loop() + resp = await loop.run_in_executor(None, _call) + raw = resp.content[0].text.strip() + + # JSON extrahieren + 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 {} + + return { + "sample_size": len(rassen), + "evaluated": count, + "averages": averages, + "results": results, + }