breed_evaluator: LLM-as-Judge Qualitätsbewertung via Claude Haiku

This commit is contained in:
rene 2026-04-24 18:27:15 +02:00
parent 1c80481f42
commit d80abf07e5
2 changed files with 152 additions and 0 deletions

View file

@ -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
# ------------------------------------------------------------------

View file

@ -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,
}