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).
This commit is contained in:
rene 2026-06-14 20:23:21 +02:00
parent 51aad6cf1b
commit f7370028da
17 changed files with 322 additions and 100 deletions

View file

@ -360,30 +360,47 @@ async def _fetch_wikimedia_photo(name: str) -> str | None:
return None
async def _haiku_complete(prompt: str) -> str:
"""Claude Haiku direkt aufrufen (immer Cloud, für maximale Genauigkeit)."""
import anthropic
async def _haiku_complete(prompt: str) -> tuple[str, str]:
"""
Fakten-Extraktion. Bevorzugt Claude Haiku (günstig + genau); ist kein
Cloud-Key gesetzt oder die Cloud nicht erreichbar, fällt es sauber auf das
lokale Modell (LM Studio) zurück, statt hart abzubrechen.
Returns (text, model) model fließt in wiki_rassen.ki_model, damit der
Evaluator lokal-angereicherte Rassen weiterhin zur QC erkennt.
"""
key = os.getenv("ANTHROPIC_API_KEY", "")
if not key:
raise RuntimeError("ANTHROPIC_API_KEY nicht gesetzt")
def _call():
client = anthropic.Anthropic(api_key=key)
return client.messages.create(
model=_HAIKU_MODEL,
max_tokens=700,
system=[{
"type": "text",
"text": _SYSTEM,
"cache_control": {"type": "ephemeral"},
}],
messages=[{"role": "user", "content": prompt}],
)
# 1. Bevorzugt: Claude Haiku direkt (günstigstes Cloud-Modell)
if key:
try:
import anthropic
loop = asyncio.get_event_loop()
resp = await loop.run_in_executor(None, _call)
return resp.content[0].text.strip()
def _call():
client = anthropic.Anthropic(api_key=key)
return client.messages.create(
model=_HAIKU_MODEL,
max_tokens=700,
system=[{
"type": "text",
"text": _SYSTEM,
"cache_control": {"type": "ephemeral"},
}],
messages=[{"role": "user", "content": prompt}],
)
loop = asyncio.get_event_loop()
resp = await loop.run_in_executor(None, _call)
return resp.content[0].text.strip(), _HAIKU_MODEL
except Exception as e:
logger.warning("Haiku (Cloud) nicht erreichbar, Fallback lokal: %s", e)
# 2. Fallback: lokales Modell über die zentrale KI-Abstraktion
import ki
if ki.KI_MODE == "off":
raise RuntimeError("Kein Cloud-Key und KI_MODE=off — Anreicherung nicht möglich.")
text = await ki._local_complete(prompt, _SYSTEM, max_tokens=700, json_mode=False)
return text, ki.LOCAL_MODEL
async def _enrich_one(rasse, dry_run: bool = False) -> bool:
@ -411,12 +428,12 @@ async def _enrich_one(rasse, dry_run: bool = False) -> bool:
logger.info("[DRY-RUN] Gefunden: %s (WP-%s, %d Zeichen)", name, wiki_lang.upper(), len(wiki_text))
return True
# 2. Haiku extrahiert Fakten aus dem Quelltext
# 2. KI extrahiert Fakten aus dem Quelltext (Haiku, sonst lokaler Fallback)
prompt = _PROMPT.format(name=name, lang=wiki_lang.upper(), wiki_text=wiki_text)
try:
raw = await _haiku_complete(prompt)
raw, used_model = await _haiku_complete(prompt)
except Exception as e:
logger.error("Haiku-Anfrage fehlgeschlagen für %s: %s", name, e)
logger.error("KI-Anfrage fehlgeschlagen für %s: %s", name, e)
await asyncio.sleep(3)
return False
@ -435,7 +452,7 @@ async def _enrich_one(rasse, dry_run: bool = False) -> bool:
if "temperament" in updates:
updates["temperament"] = translate_temperament(updates["temperament"])
updates["ki_enriched"] = 1
updates["ki_model"] = _HAIKU_MODEL
updates["ki_model"] = used_model
updates["ki_source"] = f"wikipedia_{wiki_lang}"
cols = ", ".join(f"{k}=?" for k in updates)

View file

@ -43,19 +43,23 @@ Aktivität zur Erfahrung)?
'''
async def evaluate_enrichment(sample_size: int = 20) -> dict:
async def evaluate_enrichment(sample_size: int = 20, user_id: int | None = None) -> dict:
"""
Bewertet `sample_size` zufällig gewählte angereicherte Rassen via Claude.
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
ANTHROPIC_KEY = os.getenv("ANTHROPIC_API_KEY", "")
if not ANTHROPIC_KEY:
raise RuntimeError("ANTHROPIC_API_KEY nicht gesetzt — Evaluierung benötigt Cloud.")
if ki.KI_MODE == "off":
raise RuntimeError("KI ist deaktiviert (KI_MODE=off) — Evaluierung nicht möglich.")
with db() as conn:
rassen = conn.execute(
@ -65,8 +69,7 @@ async def evaluate_enrichment(sample_size: int = 20) -> dict:
wohnung_geeignet, temperament, ki_model
FROM wiki_rassen
WHERE ki_enriched = 1
AND ki_model IS NOT NULL
AND ki_model NOT LIKE 'claude%'
AND (ki_model IS NULL OR ki_model NOT LIKE 'claude%')
ORDER BY RANDOM()
LIMIT ?""",
(sample_size,),
@ -75,10 +78,10 @@ async def evaluate_enrichment(sample_size: int = 20) -> dict:
if not rassen:
return {"error": "Keine angereicherten Rassen gefunden."}
import anthropic
client = anthropic.Anthropic(api_key=ANTHROPIC_KEY)
_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}
@ -102,22 +105,17 @@ async def evaluate_enrichment(sample_size: int = 20) -> dict:
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()
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
# 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 {}
@ -136,9 +134,12 @@ async def evaluate_enrichment(sample_size: int = 20) -> dict:
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,
}