update
This commit is contained in:
@@ -130,6 +130,16 @@ async def init_db():
|
||||
"UPDATE guides SET status = 'error', progress = NULL, error_msg = 'Server-Neustart' "
|
||||
"WHERE status IN ('queued', 'generating')"
|
||||
)
|
||||
# Milestone-Migration auf Score-Skala 0–30: absolviert≥6, verstanden≥12, gemeistert≥18.
|
||||
# Leitet die Meilensteine aus dem aktuellen Score ab (bestehende Timestamps bleiben),
|
||||
# idempotent — nach einem Lauf stabil, im Normalbetrieb deckungsgleich mit der Prüf-Route.
|
||||
await db.execute("UPDATE baustein_progress SET gute_antworten = 30 WHERE gute_antworten > 30") # Alt-Scores deckeln
|
||||
for _spalte, _schwelle in (("absolviert", 6), ("verstanden", 12), ("gemeistert", 18)):
|
||||
await db.execute(
|
||||
f"UPDATE baustein_progress SET {_spalte} = COALESCE({_spalte}, datetime('now')) "
|
||||
f"WHERE gute_antworten >= {_schwelle}"
|
||||
)
|
||||
await db.execute(f"UPDATE baustein_progress SET {_spalte} = NULL WHERE gute_antworten < {_schwelle}")
|
||||
await db.commit()
|
||||
|
||||
|
||||
|
||||
@@ -21,9 +21,12 @@ from textkit import _norm_titel
|
||||
|
||||
log = logging.getLogger("creator.lernen")
|
||||
|
||||
NOETIG = 3 # gute Antworten bis "absolviert" (Tier 1)
|
||||
MASTERY = 10 # Score bis "verstanden" (Tier 2)
|
||||
MEISTERN = 25 # Score bis "gemeistert" (Tier 3, Maximum)
|
||||
# Meilensteine je Baustein (Score 0–30). Form ergibt sich aus dem Score-Band:
|
||||
# 0–6 Quiz, 7–12 Lückentext, 13–18 Erklären, 19–30 frei.
|
||||
NOETIG = 6 # MiniGuide-Meilenstein → „absolviert" (Quiz-Band durch)
|
||||
MASTERY = 12 # Guide-Meilenstein → „verstanden" (Lückentext-Band durch)
|
||||
MEISTERN = 18 # ProGuide-Meilenstein → „gemeistert" (Erklären-Band durch, Thema fertig)
|
||||
MAX_VOLL = 30 # FullGuide-Maximum (freie Formwahl ab 18)
|
||||
CHAT_TIMEOUT = 240
|
||||
PRUEFUNG_TIMEOUT = 120 # kurze JSON-Turns; deckelt die Serien-Latenz pro Prüfungs-Schritt
|
||||
GRUENDLICH_TIMEOUT = 600 # „Gründlich prüfen": starkes Modell (role guide) braucht länger
|
||||
@@ -49,29 +52,22 @@ NIVEAUS = {
|
||||
}
|
||||
|
||||
|
||||
def score_berechnen(
|
||||
score_vor_frage: int, niveau: str, nachgefragt: bool,
|
||||
tier2: bool, tier3: bool, absolviert: bool, verstanden: bool, gemeistert: bool,
|
||||
) -> int:
|
||||
"""Neuer Score · driftfrei (immer aus dem Basis-Score). Delta aus dem Antwort-Niveau:
|
||||
je besser, desto mehr (−1/0/+1/+2/+3). Bei Nachfragen ist der Gewinn auf +1 gedeckelt.
|
||||
Danach auf Boden/Deckel der aktiven Stufe geklemmt (3 / 10 / 25). Kein Streak mehr.
|
||||
"""
|
||||
def score_berechnen(basis: int, delta: int, floor: int, cap: int) -> int:
|
||||
"""Neuer Score · driftfrei aus der Basis. `delta` liefert die Route je Prüfungsform
|
||||
(Quiz +1/−2, Lückentext +1/−1, Erklären −1…+3). Auf [floor, cap] geklemmt.
|
||||
floor = höchster erreichter Meilenstein (10/20/30); cap = Max des höchsten Formats."""
|
||||
return max(floor, min(cap, basis + delta))
|
||||
|
||||
|
||||
def floor_aus_meilensteinen(absolviert: bool, verstanden: bool, gemeistert: bool) -> int:
|
||||
"""Untergrenze des Scores: ein erreichter Meilenstein hält (kein Rückfall ins Band davor)."""
|
||||
if gemeistert:
|
||||
return MEISTERN
|
||||
# Stufe nach Baustein-Fortschritt: frisch erst Tier 1 (0→3), dann ab absolviert/verstanden.
|
||||
tier2 = tier2 and absolviert
|
||||
tier3 = tier3 and verstanden
|
||||
if not tier2:
|
||||
floor, cap = (NOETIG if absolviert else 0), NOETIG
|
||||
elif not tier3:
|
||||
floor, cap = NOETIG, MASTERY
|
||||
else:
|
||||
floor, cap = MASTERY, MEISTERN
|
||||
delta = NIVEAUS.get(niveau, 0)
|
||||
if nachgefragt:
|
||||
delta = min(delta, 1) # mit Hilfe (Nachfrage) höchstens +1
|
||||
return max(floor, min(cap, score_vor_frage + delta))
|
||||
if verstanden:
|
||||
return MASTERY
|
||||
if absolviert:
|
||||
return NOETIG
|
||||
return 0
|
||||
|
||||
|
||||
def _transcript(messages: list[dict]) -> str:
|
||||
@@ -295,6 +291,149 @@ async def pruefung_frage_variante(
|
||||
return None
|
||||
|
||||
|
||||
def _optionen_schema(opts) -> list[dict] | None:
|
||||
"""[{text, korrekt}]×4 → validierte Liste · sonst None."""
|
||||
if not isinstance(opts, list) or len(opts) != 4:
|
||||
return None
|
||||
out = []
|
||||
for o in opts:
|
||||
if not isinstance(o, dict):
|
||||
return None
|
||||
text = str(o.get("text", "")).strip()
|
||||
korrekt = o.get("korrekt")
|
||||
if not text or not isinstance(korrekt, bool):
|
||||
return None
|
||||
out.append({"text": text, "korrekt": korrekt})
|
||||
return out
|
||||
|
||||
|
||||
def _quiz_schema(data, genau_einer: bool = False) -> dict | None:
|
||||
"""{"frage": str, "optionen": [{text, korrekt}]×4} → validiert · sonst None.
|
||||
genau_einer (leichte Variante): exakt eine Option muss korrekt sein."""
|
||||
if not isinstance(data, dict):
|
||||
return None
|
||||
frage = str(data.get("frage", "")).strip()
|
||||
out = _optionen_schema(data.get("optionen"))
|
||||
if not frage or out is None:
|
||||
return None
|
||||
if genau_einer and sum(o["korrekt"] for o in out) != 1:
|
||||
return None
|
||||
return {"frage": frage, "optionen": out}
|
||||
|
||||
|
||||
def _lueckwahl_schema(data) -> dict | None:
|
||||
"""{"satz": str (mit ___), "optionen": [{text, korrekt}]×4} → genau 1 korrekt · sonst None."""
|
||||
if not isinstance(data, dict):
|
||||
return None
|
||||
satz = str(data.get("satz", "")).strip()
|
||||
out = _optionen_schema(data.get("optionen"))
|
||||
if not satz or "___" not in satz or out is None or sum(o["korrekt"] for o in out) != 1:
|
||||
return None
|
||||
return {"satz": satz, "optionen": out}
|
||||
|
||||
|
||||
async def quiz_generieren(
|
||||
topic: str, baustein: str, section: str, kompakt: str | None,
|
||||
muster: str, schwer: bool = False, provider: str = DEFAULT_PROVIDER,
|
||||
) -> dict | None:
|
||||
"""Aus einem Muster eine Multiple-Choice-Frage. schwer: 0–4 richtig (Multi).
|
||||
leicht: genau 1 richtig (Single). Starkes Modell (role guide) für korrekte Flags.
|
||||
→ {frage, optionen:[{text,korrekt}]} · None bei Fehler."""
|
||||
try:
|
||||
section_block, kompakt_block = _bloecke(section, kompakt)
|
||||
anzahl_block = (
|
||||
"0 bis 4 Optionen dürfen richtig sein (nicht zwingend genau eine). Mische bewusst."
|
||||
if schwer else
|
||||
"GENAU EINE Option ist richtig, die anderen drei sind klar falsch."
|
||||
)
|
||||
return await _gen_call(
|
||||
"Baustein-Quiz", "guide", (lambda d: _quiz_schema(d, genau_einer=not schwer)), provider,
|
||||
topic=topic, baustein=baustein, section_block=section_block,
|
||||
kompakt_block=kompakt_block, muster=muster, anzahl_block=anzahl_block,
|
||||
)
|
||||
except Exception:
|
||||
log.warning("[%s] Quiz-Frage fehlgeschlagen (%s)", topic, baustein, exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
async def lueckwahl_generieren(
|
||||
topic: str, baustein: str, section: str, kompakt: str | None,
|
||||
muster: str, provider: str = DEFAULT_PROVIDER,
|
||||
) -> dict | None:
|
||||
"""Lückentext mit Auswahl (leichte Variante): Satz mit ___ + 4 Begriffe, genau 1 richtig.
|
||||
→ {satz, optionen:[{text,korrekt}]} · None bei Fehler."""
|
||||
try:
|
||||
section_block, kompakt_block = _bloecke(section, kompakt)
|
||||
return await _gen_call(
|
||||
"Baustein-Lueckwahl", "guide", _lueckwahl_schema, provider,
|
||||
topic=topic, baustein=baustein, section_block=section_block,
|
||||
kompakt_block=kompakt_block, muster=muster,
|
||||
)
|
||||
except Exception:
|
||||
log.warning("[%s] Lückentext-Auswahl fehlgeschlagen (%s)", topic, baustein, exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
def _lueck_schema(data) -> dict | None:
|
||||
"""{"satz": str (mit ___), "loesung": str, "alternativen": [str]} → validiert · sonst None."""
|
||||
if not isinstance(data, dict):
|
||||
return None
|
||||
satz = str(data.get("satz", "")).strip()
|
||||
loesung = str(data.get("loesung", "")).strip()
|
||||
alt = data.get("alternativen", [])
|
||||
if not satz or "___" not in satz or not loesung:
|
||||
return None
|
||||
alternativen = [str(a).strip() for a in alt if isinstance(a, str) and str(a).strip()] if isinstance(alt, list) else []
|
||||
return {"satz": satz, "loesung": loesung, "alternativen": alternativen}
|
||||
|
||||
|
||||
async def lueckentext_generieren(
|
||||
topic: str, baustein: str, section: str, kompakt: str | None,
|
||||
muster: str, provider: str = DEFAULT_PROVIDER,
|
||||
) -> dict | None:
|
||||
"""Aus einem Muster eine Lückentext-Aufgabe (Satz mit ___, Lösung, Synonyme).
|
||||
→ {satz, loesung, alternativen} · None bei Fehler."""
|
||||
try:
|
||||
section_block, kompakt_block = _bloecke(section, kompakt)
|
||||
return await _gen_call(
|
||||
"Baustein-Lueckentext", "fast", _lueck_schema, provider,
|
||||
topic=topic, baustein=baustein, section_block=section_block,
|
||||
kompakt_block=kompakt_block, muster=muster,
|
||||
)
|
||||
except Exception:
|
||||
log.warning("[%s] Lückentext-Frage fehlgeschlagen (%s)", topic, baustein, exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
def _norm_begriff(t: str) -> str:
|
||||
return re.sub(r"[^\wäöüß]", "", str(t or "").lower())
|
||||
|
||||
|
||||
def _richtig_schema(data) -> dict | None:
|
||||
if not isinstance(data, dict) or not isinstance(data.get("richtig"), bool):
|
||||
return None
|
||||
return {"richtig": data["richtig"]}
|
||||
|
||||
|
||||
async def lueckentext_pruefen(
|
||||
topic: str, baustein: str, satz: str, loesung: str, alternativen: list[str],
|
||||
eingabe: str, provider: str = DEFAULT_PROVIDER,
|
||||
) -> bool:
|
||||
"""Lückentext-Antwort prüfen: erst normalisierter Vergleich (Lösung + Synonyme),
|
||||
sonst 1 KI-Call für Synonym-Toleranz. Fail-open zu RICHTIG nur bei exaktem Match."""
|
||||
if not eingabe.strip():
|
||||
return False
|
||||
norm = _norm_begriff(eingabe)
|
||||
if norm and norm in {_norm_begriff(loesung), *(_norm_begriff(a) for a in alternativen)}:
|
||||
return True
|
||||
data = await _gen_call(
|
||||
"Baustein-Lueckentext-Pruefung", "fast", _richtig_schema, provider,
|
||||
topic=topic, baustein=baustein, satz=satz, loesung=loesung,
|
||||
alternativen=", ".join(alternativen) or "(keine)", eingabe=eingabe,
|
||||
)
|
||||
return bool(data and data["richtig"])
|
||||
|
||||
|
||||
async def pruefung_bewertung_schnell(
|
||||
topic: str, baustein: str, section: str, kompakt: str | None,
|
||||
frage: str, messages: list[dict], provider: str = DEFAULT_PROVIDER,
|
||||
|
||||
@@ -215,27 +215,44 @@ class BausteinPruefungRequest(BaseModel):
|
||||
baustein: str = Field(min_length=1, max_length=200)
|
||||
section: str = Field(default="", max_length=20000) # ausführliche Fassung
|
||||
section_kompakt: str = Field(default="", max_length=20000) # kompakte Fassung (Merksätze)
|
||||
aktion: Literal["frage", "diskussion", "antwort", "antwort_pruefen"] = "frage"
|
||||
aktion: Literal[
|
||||
"frage", "diskussion", "antwort", "antwort_pruefen",
|
||||
"quiz_frage", "quiz_antwort", "lueck_frage", "lueck_antwort",
|
||||
] = "frage"
|
||||
frage: str = Field(default="", max_length=2000) # aktuell geprüfte Frage (für diskussion/antwort); Anker der Basis
|
||||
auswahl: list[int] = [] # Quiz/Lückentext-Auswahl: vom Lerner gewählte Options-Indizes
|
||||
korrekt: list[int] = [] # Quiz/Lückentext-Auswahl: korrekte Indizes (Client hält sie aus der Generierung)
|
||||
loesung: str = Field(default="", max_length=500) # Lückentext frei: erwarteter Begriff
|
||||
alternativen: list[str] = [] # Lückentext frei: akzeptierte Synonyme
|
||||
eingabe: str = Field(default="", max_length=500) # Lückentext frei: getippter Begriff
|
||||
schwer: bool = False # Variante: leicht (+1/−1) vs schwer (+3/−1)
|
||||
letzte_bewertung: str = Field(default="", max_length=2000) # Feedback der letzten Bewertung (Kontext für diskussion)
|
||||
vermeide: list[str] = [] # schon gestellte + vorgemerkte Fragen — sinngemäß nicht wiederholen
|
||||
nachgefragt: bool = False # für diese Frage wurde nachgefragt → Gewinn auf +1 gedeckelt
|
||||
begruendung: str = Field(default="", max_length=2000) # „Gründlich prüfen": warum mit der Bewertung unzufrieden
|
||||
muster: str = Field(default="", max_length=2000) # gezogenes Frage-Muster (Saat); leer → Live-Generierung (Fallback)
|
||||
# Basis wird serverseitig je Frage geführt (offene_frage-Anker) — nicht mehr vom Client.
|
||||
tier2: bool = False # ganzer Guide absolviert (alle ≥3) → −1 bei falsch, Deckel 10
|
||||
tier3: bool = False # ganzer Guide verstanden (alle ≥10) → Meisterpfad, −2 bei falsch, Deckel 25
|
||||
cap: int = Field(default=6, ge=6, le=30) # Score-Deckel = Max des höchsten erstellten Formats (6/12/18/30)
|
||||
messages: list[ChatMessage] = [] # Dialog bisher; leer = erste Frage
|
||||
provider: ProviderType = "claude"
|
||||
gruendlich: bool = False # „Gründlich prüfen": Bewertung mit starkem Modell (role guide)
|
||||
|
||||
|
||||
class QuizOption(BaseModel):
|
||||
text: str
|
||||
korrekt: bool
|
||||
|
||||
|
||||
class BausteinPruefungResponse(BaseModel):
|
||||
frage: str | None = None
|
||||
reply: str | None = None
|
||||
feedback: str | None = None
|
||||
punkte: int | None = None # Punkt-Delta dieser Antwort (−1 … +3); schnell = voraussichtlich
|
||||
punkte: int | None = None # Punkt-Delta dieser Antwort (−2 … +3); schnell = voraussichtlich
|
||||
bewertung: Literal["gut", "neutral", "schlecht"] | None = None # aus Vorzeichen, fürs Einfärben
|
||||
optionen: list[QuizOption] | None = None # Quiz: 4 Optionen + Korrekt-Flags
|
||||
satz: str | None = None # Lückentext: Satz mit Lücke (___)
|
||||
loesung: str | None = None # Lückentext: erwarteter Begriff
|
||||
alternativen: list[str] | None = None # Lückentext: akzeptierte Synonyme
|
||||
gute_antworten: int
|
||||
absolviert: bool
|
||||
verstanden: bool = False
|
||||
|
||||
@@ -18,12 +18,20 @@ from paths import bausteine_path, guide_content_path
|
||||
from textkit import _norm_titel
|
||||
|
||||
MAX_OFFENE_GUIDES = 3
|
||||
VORSTUFE = {"Guide": "MiniGuide", "ProGuide": "Guide", "FullGuide": "Guide"}
|
||||
# Welches Niveau die Vorstufe erreichen muss, um das Format freizuschalten.
|
||||
# ProGuide hängt parallel zu FullGuide an „Guide absolviert" — kein Pflicht-Gate vor FullGuide.
|
||||
FREISCHALT_LEVEL = {"Guide": "absolviert", "ProGuide": "absolviert", "FullGuide": "verstanden"}
|
||||
# Lineare Progression: jedes Format schaltet das nächste frei, sobald ALLE Bausteine
|
||||
# der Vorstufe das geforderte Niveau (Score-Meilenstein 10/20/30) erreicht haben.
|
||||
VORSTUFE = {"Guide": "MiniGuide", "ProGuide": "Guide", "FullGuide": "ProGuide"}
|
||||
# absolviert=6 · verstanden=12 · gemeistert=18 (siehe lernen.NOETIG/MASTERY/MEISTERN).
|
||||
FREISCHALT_LEVEL = {"Guide": "absolviert", "ProGuide": "verstanden", "FullGuide": "gemeistert"}
|
||||
FORMATE = ("MiniGuide", "Guide", "ProGuide", "FullGuide")
|
||||
|
||||
# Geforderter Score je Freischalt-Niveau — nur für die Sperr-Meldung.
|
||||
_LEVEL_WORT = {
|
||||
"absolviert": "absolvieren (alle Bausteine auf 6)",
|
||||
"verstanden": "verstehen (alle Bausteine auf 12)",
|
||||
"gemeistert": "meistern (alle Bausteine auf 18)",
|
||||
}
|
||||
|
||||
|
||||
async def lade_lernstand() -> tuple[list[dict], dict[str, set[str]], dict[str, dict[str, set[str]]]]:
|
||||
"""Guides + Kapitel-Fortschritt + Bausteine je Meilenstein.
|
||||
@@ -127,10 +135,9 @@ def guide_lock(topic: str, fmt: str, guides: list[dict], progress: dict[str, set
|
||||
if fmt != "OnePager" and not content.exists() and not guide_slot_dateien(content):
|
||||
vorstufe = VORSTUFE.get(fmt)
|
||||
if vorstufe:
|
||||
stufe = FREISCHALT_LEVEL[fmt] # "absolviert" (alle 3) oder "verstanden" (alle 10)
|
||||
stufe = FREISCHALT_LEVEL[fmt] # absolviert=10 · verstanden=20 · gemeistert=30
|
||||
if not ist_level(topic, vorstufe, guides, progress, levels[stufe]):
|
||||
wort = "verstehen (alle Bausteine auf 10)" if stufe == "verstanden" else "absolvieren (alle Bausteine prüfen)"
|
||||
return f"Erst den {vorstufe} dieses Themas {wort}"
|
||||
return f"Erst den {vorstufe} dieses Themas {_LEVEL_WORT[stufe]}"
|
||||
stat = formate_stats(guides, progress, levels).get(fmt, {"erstellt": 0, "absolviert": 0})
|
||||
offen = stat["erstellt"] - stat["absolviert"]
|
||||
if offen >= MAX_OFFENE_GUIDES:
|
||||
|
||||
@@ -18,7 +18,7 @@ from database import (
|
||||
)
|
||||
from bausteine import generate_bausteine, cancel_bausteine, bausteine_status, active_bausteine, reset_bausteine, lade_quelle, lade_uebersicht, subbausteine_titel, lade_frage_muster
|
||||
from elements import generate_element, chat_with_guide, chat_with_element, check_element, style_element, refine_suggestion
|
||||
from lernen import NOETIG, MASTERY, MEISTERN, baustein_chat, baustein_diskussion, baustein_element_anlegen, pruefung_bewertung, pruefung_bewertung_schnell, pruefung_frage, pruefung_frage_variante, score_berechnen
|
||||
from lernen import NOETIG, MASTERY, MEISTERN, NIVEAUS, baustein_chat, baustein_diskussion, baustein_element_anlegen, pruefung_bewertung, pruefung_bewertung_schnell, pruefung_frage, pruefung_frage_variante, quiz_generieren, lueckwahl_generieren, lueckentext_generieren, lueckentext_pruefen, score_berechnen, floor_aus_meilensteinen
|
||||
from guide import generate_guide, guide_slot_dateien
|
||||
from pipeline import cancel_guide
|
||||
from regeln import FORMATE, formate_stats, guide_lock, ist_absolviert, lade_lernstand, thema_abgeschlossen
|
||||
@@ -265,6 +265,41 @@ def _farbe(punkte: int) -> str:
|
||||
return "gut" if punkte > 0 else ("neutral" if punkte == 0 else "schlecht")
|
||||
|
||||
|
||||
def _erklaeren_delta(niveau: str, nachgefragt: bool) -> int:
|
||||
"""Erklären-Form: Punkt-Delta aus dem Niveau (−1…+3). Mit Nachfrage höchstens +1."""
|
||||
delta = NIVEAUS.get(niveau, 0)
|
||||
return min(delta, 1) if nachgefragt else delta
|
||||
|
||||
|
||||
async def _punkte_buchen(req, frage: str, delta: int) -> dict:
|
||||
"""Score driftfrei buchen (Lock + offene_frage-Anker + Meilensteine). Gemeinsam für
|
||||
Quiz/Lückentext (deterministisch/schnell). → Response-Felder ohne feedback."""
|
||||
async with _pruef_lock(req.topic, req.baustein):
|
||||
stand = await get_baustein_progress(req.topic, req.baustein)
|
||||
absolviert = stand["absolviert"] is not None
|
||||
verstanden = stand["verstanden"] is not None
|
||||
gemeistert = stand["gemeistert"] is not None
|
||||
basis, re_bewertung = _basis(stand, frage)
|
||||
if not re_bewertung:
|
||||
await set_offene_frage(req.topic, req.baustein, frage, basis, 0)
|
||||
floor = floor_aus_meilensteinen(absolviert, verstanden, gemeistert)
|
||||
score = score_berechnen(basis, delta, floor, req.cap)
|
||||
punkte = score - basis
|
||||
gute = await set_baustein_score(req.topic, req.baustein, score)
|
||||
if score >= NOETIG and not absolviert:
|
||||
absolviert = True
|
||||
if await set_baustein_absolviert(req.topic, req.baustein):
|
||||
asyncio.create_task(baustein_element_anlegen(req.topic, req.baustein, req.section, req.provider))
|
||||
if score >= MASTERY and not verstanden:
|
||||
await set_baustein_verstanden(req.topic, req.baustein)
|
||||
verstanden = True
|
||||
if score >= MEISTERN and not gemeistert:
|
||||
await set_baustein_gemeistert(req.topic, req.baustein)
|
||||
gemeistert = True
|
||||
return {"punkte": punkte, "bewertung": _farbe(punkte), "gute_antworten": gute,
|
||||
"absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert}
|
||||
|
||||
|
||||
@router.post("/bausteine/pruefung", response_model=BausteinPruefungResponse)
|
||||
async def baustein_pruefung_route(req: BausteinPruefungRequest):
|
||||
stand = await get_baustein_progress(req.topic, req.baustein)
|
||||
@@ -298,6 +333,54 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest):
|
||||
raise HTTPException(502, "Diskussion fehlgeschlagen — bitte erneut versuchen")
|
||||
return {"reply": reply, "gute_antworten": gute, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert}
|
||||
|
||||
# --- Quiz: leicht (1 von 4) +1/−1 · schwer (x von 4) +3/−1 — deterministisch ---
|
||||
if req.aktion == "quiz_frage":
|
||||
if not req.muster.strip():
|
||||
raise HTTPException(400, "Quiz braucht ein Muster")
|
||||
quiz = await quiz_generieren(req.topic, req.baustein, req.section, kompakt, req.muster, schwer=req.schwer, provider=req.provider)
|
||||
if quiz is None:
|
||||
raise HTTPException(502, "Quiz-Frage fehlgeschlagen — bitte erneut versuchen")
|
||||
return {"frage": quiz["frage"], "optionen": quiz["optionen"],
|
||||
"gute_antworten": gute, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert}
|
||||
|
||||
if req.aktion == "quiz_antwort":
|
||||
if not req.frage.strip():
|
||||
raise HTTPException(400, "Quiz-Antwort braucht eine Frage")
|
||||
getroffen = set(req.auswahl) == set(req.korrekt) # exakt die richtige Menge
|
||||
delta = (3 if req.schwer else 1) if getroffen else -1
|
||||
res = await _punkte_buchen(req, req.frage, delta)
|
||||
res["feedback"] = "Richtig — alle korrekten getroffen." if getroffen else "Nicht ganz — die markierten waren richtig."
|
||||
return res
|
||||
|
||||
# --- Lückentext: leicht (Begriff aus 4) +1/−1 · schwer (frei tippen) +3/−1 ---
|
||||
if req.aktion == "lueck_frage":
|
||||
if not req.muster.strip():
|
||||
raise HTTPException(400, "Lückentext braucht ein Muster")
|
||||
if req.schwer:
|
||||
lt = await lueckentext_generieren(req.topic, req.baustein, req.section, kompakt, req.muster, provider=req.provider)
|
||||
if lt is None:
|
||||
raise HTTPException(502, "Lückentext fehlgeschlagen — bitte erneut versuchen")
|
||||
return {"satz": lt["satz"], "loesung": lt["loesung"], "alternativen": lt["alternativen"],
|
||||
"gute_antworten": gute, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert}
|
||||
lw = await lueckwahl_generieren(req.topic, req.baustein, req.section, kompakt, req.muster, provider=req.provider)
|
||||
if lw is None:
|
||||
raise HTTPException(502, "Lückentext fehlgeschlagen — bitte erneut versuchen")
|
||||
return {"satz": lw["satz"], "optionen": lw["optionen"],
|
||||
"gute_antworten": gute, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert}
|
||||
|
||||
if req.aktion == "lueck_antwort":
|
||||
if not req.frage.strip():
|
||||
raise HTTPException(400, "Lückentext-Antwort braucht einen Satz")
|
||||
if req.schwer: # frei getippt → Synonym-tolerante KI-Prüfung
|
||||
ok = await lueckentext_pruefen(req.topic, req.baustein, req.frage, req.loesung, req.alternativen, req.eingabe, provider=req.provider)
|
||||
feedback = "Richtig!" if ok else f"Nicht ganz — erwartet war „{req.loesung}“."
|
||||
else: # Begriff aus 4 gewählt → deterministisch
|
||||
ok = set(req.auswahl) == set(req.korrekt)
|
||||
feedback = "Richtig!" if ok else "Nicht ganz — der markierte Begriff war richtig."
|
||||
res = await _punkte_buchen(req, req.frage, (3 if req.schwer else 1) if ok else -1)
|
||||
res["feedback"] = feedback
|
||||
return res
|
||||
|
||||
# aktion "antwort" (Agent 1 schnell) / "antwort_pruefen" (Agent 2 genau).
|
||||
if not any(m.get("role") == "user" for m in msgs):
|
||||
raise HTTPException(400, "Antwort braucht eine Nutzer-Antwort")
|
||||
@@ -312,7 +395,8 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest):
|
||||
if data is None:
|
||||
raise HTTPException(502, "Bewertung fehlgeschlagen — bitte erneut versuchen")
|
||||
basis, _ = _basis(stand, req.frage)
|
||||
score = score_berechnen(basis, data["niveau"], req.nachgefragt, req.tier2, req.tier3, absolviert, verstanden, gemeistert)
|
||||
floor = floor_aus_meilensteinen(absolviert, verstanden, gemeistert)
|
||||
score = score_berechnen(basis, _erklaeren_delta(data["niveau"], req.nachgefragt), floor, req.cap)
|
||||
punkte = score - basis
|
||||
return {"feedback": data["feedback"], "punkte": punkte, "bewertung": _farbe(punkte),
|
||||
"gute_antworten": gute, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert}
|
||||
@@ -335,7 +419,8 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest):
|
||||
if data is None:
|
||||
raise HTTPException(502, "Bewertung fehlgeschlagen — bitte erneut versuchen")
|
||||
|
||||
score = score_berechnen(basis, data["niveau"], req.nachgefragt, req.tier2, req.tier3, absolviert, verstanden, gemeistert)
|
||||
floor = floor_aus_meilensteinen(absolviert, verstanden, gemeistert)
|
||||
score = score_berechnen(basis, _erklaeren_delta(data["niveau"], req.nachgefragt), floor, req.cap)
|
||||
punkte = score - basis
|
||||
gute = await set_baustein_score(req.topic, req.baustein, score)
|
||||
if score >= NOETIG and not absolviert:
|
||||
|
||||
Reference in New Issue
Block a user