Session 2026-04-21: SEO, Wiki-Anreicherung, Training, Lober
SEO & Crawler:
- robots.txt, llms.txt, sitemap.xml (508 Seiten bei Google)
- SSR-Seiten: /info, /wiki/rassen, /wiki/rasse/{slug}, /knigge
- Open Graph, JSON-LD, Breadcrumbs in index.html
Navigation:
- Training unter "Mein Hund", Wissen collapsible
- Welcome-Seite und Landing-Page auf 5-Gruppen-Struktur
Wiki:
- KI-Anreicherung (Claude API): beschreibung, vorkommen_de, Steckbrief
- "So einen hab ich" / Züchter-Verzeichnis
- Scheduler: 50 Rassen beim Start, 20/Nacht
Training:
- Session-Logging (Erfolgsquote, Stimmung, Zufriedenheit)
- Virtueller KI-Trainer (6h-Cache)
- Trainingskalender (Habit-Tracker)
- Top-Training → automatischer Tagebucheintrag
- Gamification ohne Druck: Badges, Streak, Stats
Fortschritts-Lober:
- Jeden Montag 09:00: Claude schreibt Lob-Text pro Hund
- Push + Karte im Tagebuch
Monitoring:
- 4× täglich Status-Mail mit Scheduler-Status + Wiki-Fortschritt
This commit is contained in:
parent
65d1cf6c7f
commit
180de32e57
22 changed files with 4351 additions and 189 deletions
168
backend/scraper/breed_enricher.py
Normal file
168
backend/scraper/breed_enricher.py
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
"""
|
||||
BAN YARO — Rassen-Anreicherung via KI
|
||||
|
||||
Nutzt ki.complete() um fehlende Rassen-Daten (Beschreibung, Vorkommen, etc.)
|
||||
per Claude API anzureichern und in wiki_rassen zurückzuschreiben.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Pfad zum Backend-Verzeichnis sicherstellen (beim direkten Aufruf)
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from database import db
|
||||
from ki import complete, KIUnavailableError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_SYSTEM = "Du bist ein Hunde-Experte."
|
||||
|
||||
_PROMPT_TEMPLATE = '''\
|
||||
Gib mir strukturierte Informationen über die Hunderasse "{name}" (Herkunft: {herkunft}) auf Deutsch.
|
||||
Antworte NUR mit einem JSON-Objekt, keine Erklärung darum.
|
||||
|
||||
Format:
|
||||
{{
|
||||
"beschreibung": "3-5 Sätze über Charakter und Wesen der Rasse",
|
||||
"vorkommen_de": "1-2 Sätze wie verbreitet die Rasse in Deutschland/DACH ist",
|
||||
"groesse": "klein|mittel|gross|sehr_gross",
|
||||
"gewicht_min_kg": Zahl_oder_null,
|
||||
"gewicht_max_kg": Zahl_oder_null,
|
||||
"lebensdauer": "X-Y Jahre oder null",
|
||||
"aktivitaet": "niedrig|mittel|hoch|sehr_hoch",
|
||||
"erfahrung": "anfaenger|fortgeschritten|experte",
|
||||
"kinder_geeignet": true_oder_false,
|
||||
"wohnung_geeignet": true_oder_false,
|
||||
"temperament": "kommagetrennte Eigenschaftsliste auf Deutsch, z.B. freundlich, verspielt, loyal"
|
||||
}}
|
||||
'''
|
||||
|
||||
# Felder die direkt in wiki_rassen geschrieben werden (wenn nicht null)
|
||||
_DIRECT_FIELDS = {
|
||||
"beschreibung", "vorkommen_de",
|
||||
"groesse", "gewicht_min_kg", "gewicht_max_kg",
|
||||
"lebensdauer", "aktivitaet", "erfahrung",
|
||||
"kinder_geeignet", "wohnung_geeignet", "temperament",
|
||||
}
|
||||
|
||||
|
||||
def _parse_json(raw: str) -> dict:
|
||||
"""JSON aus KI-Antwort extrahieren — toleriert ```json ... ``` Wrapper."""
|
||||
# Versuche direkt
|
||||
try:
|
||||
return json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
# Suche nach ```json ... ``` Block
|
||||
match = re.search(r"```(?:json)?\s*([\s\S]+?)\s*```", raw)
|
||||
if match:
|
||||
try:
|
||||
return json.loads(match.group(1))
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
# Suche nach erstem { ... } Block
|
||||
match = re.search(r"\{[\s\S]+\}", raw)
|
||||
if match:
|
||||
try:
|
||||
return json.loads(match.group(0))
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
raise ValueError(f"Kein gültiges JSON in Antwort gefunden: {raw[:200]}")
|
||||
|
||||
|
||||
async def enrich_breeds(limit: int = 10) -> int:
|
||||
"""
|
||||
Reichert bis zu `limit` Rassen an, bei denen ki_enriched = 0.
|
||||
|
||||
Returns:
|
||||
Anzahl erfolgreich angereicherter Rassen.
|
||||
"""
|
||||
with db() as conn:
|
||||
rassen = conn.execute(
|
||||
"""SELECT id, name, slug, herkunft FROM wiki_rassen
|
||||
WHERE ki_enriched = 0
|
||||
ORDER BY name ASC
|
||||
LIMIT ?""",
|
||||
(limit,),
|
||||
).fetchall()
|
||||
|
||||
if not rassen:
|
||||
logger.info("Keine Rassen zur Anreicherung gefunden (alle ki_enriched=1).")
|
||||
return 0
|
||||
|
||||
enriched_count = 0
|
||||
|
||||
for rasse in rassen:
|
||||
name = rasse["name"]
|
||||
herkunft = rasse["herkunft"] or "unbekannt"
|
||||
rasse_id = rasse["id"]
|
||||
|
||||
prompt = _PROMPT_TEMPLATE.format(name=name, herkunft=herkunft)
|
||||
|
||||
try:
|
||||
raw = await complete(
|
||||
prompt,
|
||||
system=_SYSTEM,
|
||||
max_tokens=600,
|
||||
requires_premium=False,
|
||||
)
|
||||
except KIUnavailableError as e:
|
||||
logger.warning("KI nicht verfügbar, Anreicherung abgebrochen: %s", e)
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error("Fehler bei KI-Anfrage für %s: %s", name, e)
|
||||
await asyncio.sleep(2)
|
||||
continue
|
||||
|
||||
try:
|
||||
data = _parse_json(raw)
|
||||
except ValueError as e:
|
||||
logger.warning("JSON-Parsing fehlgeschlagen für %s: %s", name, e)
|
||||
await asyncio.sleep(2)
|
||||
continue
|
||||
|
||||
# Nur bekannte Felder mit nicht-None-Wert übernehmen
|
||||
updates = {
|
||||
k: v for k, v in data.items()
|
||||
if k in _DIRECT_FIELDS and v is not None
|
||||
}
|
||||
updates["ki_enriched"] = 1
|
||||
|
||||
cols = ", ".join(f"{k}=?" for k in updates)
|
||||
values = list(updates.values()) + [rasse_id]
|
||||
|
||||
try:
|
||||
with db() as conn:
|
||||
conn.execute(
|
||||
f"UPDATE wiki_rassen SET {cols} WHERE id=?",
|
||||
values,
|
||||
)
|
||||
logger.info("Rasse angereichert: %s (%d Felder)", name, len(updates) - 1)
|
||||
enriched_count += 1
|
||||
except Exception as e:
|
||||
logger.error("DB-Update fehlgeschlagen für %s: %s", name, e)
|
||||
|
||||
await asyncio.sleep(2)
|
||||
|
||||
return enriched_count
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
|
||||
|
||||
parser = argparse.ArgumentParser(description="Rassen-Anreicherung via KI")
|
||||
parser.add_argument("--limit", type=int, default=10, help="Anzahl Rassen (default: 10)")
|
||||
args = parser.parse_args()
|
||||
|
||||
count = asyncio.run(enrich_breeds(args.limit))
|
||||
print(f"Angereichert: {count} Rassen")
|
||||
Loading…
Add table
Add a link
Reference in a new issue