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).
145 lines
5 KiB
Python
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,
|
|
}
|