update
This commit is contained in:
@@ -545,22 +545,33 @@ async def set_baustein_absolviert(topic: str, baustein: str) -> bool:
|
||||
return cursor.rowcount > 0
|
||||
|
||||
|
||||
async def list_baustein_levels_all() -> dict[str, dict[str, set[str]]]:
|
||||
"""Bausteine je Meilenstein in EINER Query: {"absolviert"/"verstanden"/"gemeistert": {topic → {Titel}}}."""
|
||||
async def count_relevant_subs(topic: str, baustein: str) -> int:
|
||||
"""Anzahl relevanter Konsens-Subbausteine eines Bausteins (für den cap = 2×Subs)."""
|
||||
from textkit import _norm_titel
|
||||
db = await get_db()
|
||||
cursor = await db.execute(
|
||||
"SELECT topic, baustein, absolviert, verstanden, gemeistert FROM baustein_progress"
|
||||
"SELECT COUNT(*) FROM subbausteine WHERE topic = ? AND baustein_norm = ? "
|
||||
"AND status = 'konsens' AND relevanz != 'rand'",
|
||||
(topic, _norm_titel(baustein)),
|
||||
)
|
||||
rows = await cursor.fetchall()
|
||||
levels: dict[str, dict[str, set[str]]] = {"absolviert": {}, "verstanden": {}, "gemeistert": {}}
|
||||
for topic, baustein, absolviert, verstanden, gemeistert in rows:
|
||||
if absolviert is not None:
|
||||
levels["absolviert"].setdefault(topic, set()).add(baustein)
|
||||
if verstanden is not None:
|
||||
levels["verstanden"].setdefault(topic, set()).add(baustein)
|
||||
if gemeistert is not None:
|
||||
levels["gemeistert"].setdefault(topic, set()).add(baustein)
|
||||
return levels
|
||||
return (await cursor.fetchone())[0]
|
||||
|
||||
|
||||
async def subs_count_alle() -> dict[tuple[str, str], int]:
|
||||
"""Relevante Konsens-Subbausteine je (topic, baustein_norm) — für die Stufen-Ableitung."""
|
||||
db = await get_db()
|
||||
cursor = await db.execute(
|
||||
"SELECT topic, baustein_norm, COUNT(*) FROM subbausteine "
|
||||
"WHERE status = 'konsens' AND relevanz != 'rand' GROUP BY topic, baustein_norm"
|
||||
)
|
||||
return {(t, bn): n for t, bn, n in await cursor.fetchall()}
|
||||
|
||||
|
||||
async def list_baustein_scores_all() -> list[tuple[str, str, int]]:
|
||||
"""(topic, baustein, gute_antworten) je Baustein — Rohdaten für die Stufen-Ableitung."""
|
||||
db = await get_db()
|
||||
cursor = await db.execute("SELECT topic, baustein, gute_antworten FROM baustein_progress")
|
||||
return [(t, b, n) for t, b, n in await cursor.fetchall()]
|
||||
|
||||
|
||||
async def delete_baustein_daten(topic: str) -> None:
|
||||
|
||||
@@ -21,12 +21,40 @@ from textkit import _norm_titel
|
||||
|
||||
log = logging.getLogger("creator.lernen")
|
||||
|
||||
# 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)
|
||||
# Lernstufen je Baustein — relativ zum cap (Floor in % des Maximal-Scores):
|
||||
# grün=Anfänger 20% · blau=Fortgeschritten 40% · lila=Experte 60% · gold=Meister 100%.
|
||||
# Prüfungsform ist immer zufällig (5 Formen); der cap skaliert mit der Stoffmenge.
|
||||
STUFEN = (("anfaenger", 0.2), ("fortgeschritten", 0.4), ("experte", 0.6), ("meister", 1.0))
|
||||
|
||||
|
||||
def cap_fuer(n_subs: int) -> int:
|
||||
"""Maximal-Score eines Bausteins: 2 Punkte je relevantem Subbaustein, gedeckelt [10, 50]."""
|
||||
return max(10, min(50, 2 * n_subs))
|
||||
|
||||
|
||||
def _schwelle(p: float, cap: int) -> int:
|
||||
return round(p * cap)
|
||||
|
||||
|
||||
def stufe_aus_score(score: int, cap: int) -> str | None:
|
||||
"""Höchste erreichte Stufe (None unter 20 %)."""
|
||||
erreicht = None
|
||||
for key, p in STUFEN:
|
||||
if score >= _schwelle(p, cap):
|
||||
erreicht = key
|
||||
return erreicht
|
||||
|
||||
|
||||
def progressiver_malus(basis: int, cap: int) -> int:
|
||||
"""Fehler-Strafe nach Fortschritt: 0–20 %→0 · 21–40 %→−1 · 41–60 %→−2 · 61–100 %→−3."""
|
||||
pct = (basis / cap) if cap else 0.0
|
||||
if pct <= 0.2:
|
||||
return 0
|
||||
if pct <= 0.4:
|
||||
return -1
|
||||
if pct <= 0.6:
|
||||
return -2
|
||||
return -3
|
||||
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
|
||||
@@ -54,30 +82,22 @@ NIVEAUS = {
|
||||
|
||||
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/+3, Lückentext +1/+3, Erklären −1…+3; falsch −1). Auf [floor, cap] geklemmt.
|
||||
floor = höchster erreichter Meilenstein (6/12/18); cap = Max des höchsten Formats.
|
||||
Am Format-Deckel (basis ≥ cap) eingefroren: „Max" — weder + noch −, bis das nächste
|
||||
Format freischaltet. Bei cap 30 (FullGuide) bleibt 30 endgültig."""
|
||||
(Quiz +1/+3, Lückentext +1/+3, Erklären −1…+3; falsch = progressiver_malus). Auf
|
||||
[floor, cap] geklemmt. floor = höchste erreichte Stufenschwelle (kein Rückfall);
|
||||
cap = 2× relevante Subbausteine [10, 50]. Am Deckel (basis ≥ cap) eingefroren („Max")."""
|
||||
if basis >= cap:
|
||||
return basis
|
||||
return max(floor, min(cap, basis + delta))
|
||||
|
||||
|
||||
def _zufall_malus(basis: int, delta: int) -> int:
|
||||
"""Im Zufallband (Score ≥ MEISTERN=18, FullGuide) kostet jeder Fehler −3 statt −1 —
|
||||
die 30 erreicht man nur mit Sicherheit. Richtige Antworten bleiben unverändert."""
|
||||
return -3 if (basis >= MEISTERN and delta < 0) else 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
|
||||
if verstanden:
|
||||
return MASTERY
|
||||
if absolviert:
|
||||
return NOETIG
|
||||
return 0
|
||||
def floor_aus_score(basis: int, cap: int) -> int:
|
||||
"""Untergrenze: höchste erreichte Stufenschwelle — kein Rückfall unter die Stufe."""
|
||||
floor = 0
|
||||
for _, p in STUFEN:
|
||||
s = _schwelle(p, cap)
|
||||
if basis >= s:
|
||||
floor = s
|
||||
return floor
|
||||
|
||||
|
||||
def _transcript(messages: list[dict]) -> str:
|
||||
|
||||
@@ -230,8 +230,8 @@ class BausteinPruefungRequest(BaseModel):
|
||||
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.
|
||||
cap: int = Field(default=6, ge=6, le=30) # Score-Deckel = Max des höchsten erstellten Formats (6/12/18/30)
|
||||
# Basis + cap werden serverseitig geführt (Anker bzw. 2×Subs) — Client-cap nur Hinweis.
|
||||
cap: int = Field(default=10, ge=1, le=50) # Score-Deckel = clamp(2×relevante Subbausteine, 10, 50)
|
||||
messages: list[ChatMessage] = [] # Dialog bisher; leer = erste Frage
|
||||
provider: ProviderType = "claude"
|
||||
gruendlich: bool = False # „Gründlich prüfen": Bewertung mit starkem Modell (role guide)
|
||||
@@ -253,17 +253,13 @@ class BausteinPruefungResponse(BaseModel):
|
||||
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
|
||||
gemeistert: bool = False
|
||||
cap: int = 10 # Maximal-Score dieses Bausteins (2×Subs, geklemmt) — Frontend leitet die Stufe ab
|
||||
|
||||
|
||||
class BausteinLernstand(BaseModel):
|
||||
gute_antworten: int
|
||||
streak: int = 0
|
||||
absolviert: bool
|
||||
verstanden: bool
|
||||
gemeistert: bool
|
||||
cap: int = 10 # 2×relevante Subbausteine, geklemmt [10, 50]
|
||||
|
||||
|
||||
class BausteinLernstandResponse(BaseModel):
|
||||
|
||||
@@ -11,36 +11,42 @@ Query-Schleifen mehr pro Guide.
|
||||
|
||||
import json
|
||||
|
||||
from database import list_baustein_levels_all, list_guides, list_progress_all
|
||||
from database import list_baustein_scores_all, subs_count_alle, list_guides, list_progress_all
|
||||
from guide import guide_slot_dateien
|
||||
from lernen import cap_fuer, STUFEN, _schwelle
|
||||
from paths import bausteine_path, guide_content_path
|
||||
from textkit import _norm_titel
|
||||
|
||||
MAX_OFFENE_GUIDES = 3
|
||||
# Nur noch EIN Format „Guide" (alle relevanten Bausteine, Prüfung 0–30). Keine Progression,
|
||||
# Nur noch EIN Format „Guide" (alle relevanten Bausteine, Prüfung 0–cap). Keine Progression,
|
||||
# keine Vorstufe → der Guide ist immer freischaltbar. „Rest"/FullGuide separat.
|
||||
VORSTUFE: dict[str, str] = {}
|
||||
FREISCHALT_LEVEL: dict[str, str] = {}
|
||||
FORMATE = ("Guide",)
|
||||
|
||||
# Geforderter Score je Freischalt-Niveau — nur für die Sperr-Meldung.
|
||||
# 4 Lernstufen (Floor in % des cap) — Schlüssel aus lernen.STUFEN.
|
||||
_LEVEL_WORT = {
|
||||
"absolviert": "absolvieren (alle Bausteine auf 6)",
|
||||
"verstanden": "verstehen (alle Bausteine auf 12)",
|
||||
"gemeistert": "meistern (alle Bausteine auf 18)",
|
||||
"anfaenger": "auf Anfänger (20 %)",
|
||||
"fortgeschritten": "auf Fortgeschritten (40 %)",
|
||||
"experte": "auf Experte (60 %)",
|
||||
"meister": "meistern (100 %)",
|
||||
}
|
||||
|
||||
|
||||
async def lade_lernstand() -> tuple[list[dict], dict[str, set[str]], dict[str, dict[str, set[str]]]]:
|
||||
"""Guides + Kapitel-Fortschritt + Bausteine je Meilenstein.
|
||||
"""Guides + Kapitel-Fortschritt + Bausteine je Stufe.
|
||||
|
||||
levels: {"absolviert"/"verstanden"/"gemeistert": {topic → normalisierte Titel}}.
|
||||
levels: {"anfaenger"/"fortgeschritten"/"experte"/"meister": {topic → normalisierte Titel}}.
|
||||
Stufe je Baustein wird aus Score + cap (2×relevante Subs) abgeleitet.
|
||||
"""
|
||||
roh = await list_baustein_levels_all()
|
||||
levels = {
|
||||
stufe: {topic: {_norm_titel(b) for b in titel} for topic, titel in pro_topic.items()}
|
||||
for stufe, pro_topic in roh.items()
|
||||
}
|
||||
scores = await list_baustein_scores_all()
|
||||
caps = await subs_count_alle()
|
||||
levels: dict[str, dict[str, set[str]]] = {key: {} for key, _ in STUFEN}
|
||||
for topic, baustein, score in scores:
|
||||
cap = cap_fuer(caps.get((topic, _norm_titel(baustein)), 0))
|
||||
for key, p in STUFEN:
|
||||
if score >= _schwelle(p, cap):
|
||||
levels[key].setdefault(topic, set()).add(_norm_titel(baustein))
|
||||
return await list_guides(), await list_progress_all(), levels
|
||||
|
||||
|
||||
@@ -91,13 +97,13 @@ def ist_level(topic: str, fmt: str, guides: list[dict], progress: dict[str, set[
|
||||
|
||||
|
||||
def ist_absolviert(topic: str, fmt: str, guides: list[dict], progress: dict[str, set[str]], levels: dict[str, dict[str, set[str]]]) -> bool:
|
||||
"""Alle Bausteine des neuesten fertigen Guides absolviert (≥3)?"""
|
||||
return ist_level(topic, fmt, guides, progress, levels["absolviert"])
|
||||
"""Alle Bausteine des neuesten fertigen Guides mindestens Anfänger (≥20 %)?"""
|
||||
return ist_level(topic, fmt, guides, progress, levels["anfaenger"])
|
||||
|
||||
|
||||
def thema_abgeschlossen(topic: str, guides: list[dict], progress: dict[str, set[str]], levels: dict[str, dict[str, set[str]]]) -> bool:
|
||||
"""Thema fertig: neuester fertiger Guide, alle Bausteine gemeistert (18)?"""
|
||||
return ist_level(topic, "Guide", guides, progress, levels["gemeistert"])
|
||||
"""Thema fertig: neuester fertiger Guide, alle Bausteine auf Meister (100 %)?"""
|
||||
return ist_level(topic, "Guide", guides, progress, levels["meister"])
|
||||
|
||||
|
||||
def formate_stats(guides: list[dict], progress: dict[str, set[str]], levels: dict[str, dict[str, set[str]]]) -> dict:
|
||||
@@ -105,7 +111,7 @@ def formate_stats(guides: list[dict], progress: dict[str, set[str]], levels: dic
|
||||
formate = {}
|
||||
for fmt in FORMATE:
|
||||
neueste = _neueste_done(guides, fmt)
|
||||
absolviert = sum(1 for g in neueste.values() if _guide_alle(g, progress, levels["absolviert"]))
|
||||
absolviert = sum(1 for g in neueste.values() if _guide_alle(g, progress, levels["anfaenger"]))
|
||||
formate[fmt] = {"erstellt": len(neueste), "absolviert": absolviert}
|
||||
return formate
|
||||
|
||||
|
||||
@@ -14,12 +14,13 @@ from database import (
|
||||
list_progress, set_progress, delete_progress,
|
||||
create_element, list_elements, get_element, update_element, delete_element,
|
||||
list_baustein_progress, get_baustein_progress, set_offene_frage,
|
||||
set_baustein_score, set_baustein_absolviert, set_baustein_verstanden, set_baustein_gemeistert, delete_baustein_daten,
|
||||
set_baustein_score, set_baustein_absolviert, delete_baustein_daten, count_relevant_subs, subs_count_alle,
|
||||
delete_topic_pipeline, delete_quelle, get_guide_content, delete_guide_content,
|
||||
)
|
||||
from textkit import _norm_titel
|
||||
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, 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, _zufall_malus
|
||||
from lernen import NIVEAUS, cap_fuer, 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_score, progressiver_malus, stufe_aus_score
|
||||
from guide import generate_guide, guide_slot_dateien, guide_fertig_step
|
||||
from pipeline import cancel_guide
|
||||
from regeln import FORMATE, formate_stats, guide_lock, ist_absolviert, lade_lernstand, thema_abgeschlossen
|
||||
@@ -221,15 +222,14 @@ async def get_frage_muster(topic: str, baustein: str):
|
||||
|
||||
@router.get("/bausteine/lernstand", response_model=BausteinLernstandResponse)
|
||||
async def baustein_lernstand(topic: str):
|
||||
"""Prüfungs-Stand pro Baustein (roher Titel als Key)."""
|
||||
"""Prüfungs-Stand pro Baustein (roher Titel als Key). cap = 2×relevante Subs [10,50]."""
|
||||
progress = await list_baustein_progress(topic)
|
||||
caps = await subs_count_alle()
|
||||
bausteine = {
|
||||
p["baustein"]: {
|
||||
"gute_antworten": p["gute_antworten"],
|
||||
"streak": p["streak"],
|
||||
"absolviert": p["absolviert"] is not None,
|
||||
"verstanden": p["verstanden"] is not None,
|
||||
"gemeistert": p["gemeistert"] is not None,
|
||||
"cap": cap_fuer(caps.get((topic, _norm_titel(p["baustein"])), 0)),
|
||||
}
|
||||
for p in progress
|
||||
}
|
||||
@@ -276,42 +276,37 @@ def _erklaeren_delta(niveau: str, nachgefragt: bool) -> int:
|
||||
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 def _cap_fuer(topic: str, baustein: str) -> int:
|
||||
"""Maximal-Score des Bausteins = clamp(2×relevante Subbausteine, 10, 50)."""
|
||||
return cap_fuer(await count_relevant_subs(topic, baustein))
|
||||
|
||||
|
||||
async def _buche(req, frage: str, delta: int, cap: int) -> dict:
|
||||
"""Score driftfrei buchen (Lock + offene_frage-Anker). Fehler (delta<0) → progressiver
|
||||
Malus nach Fortschritt. Element einmalig anlegen, sobald Anfänger (20 %) erreicht."""
|
||||
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
|
||||
war_stufe = stand["absolviert"] is not None # Element-Guard: schon je angelegt?
|
||||
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, _zufall_malus(basis, delta), floor, req.cap)
|
||||
floor = floor_aus_score(basis, cap)
|
||||
d = progressiver_malus(basis, cap) if delta < 0 else delta
|
||||
score = score_berechnen(basis, d, floor, cap)
|
||||
punkte = score - basis
|
||||
gute = await set_baustein_score(req.topic, req.baustein, score)
|
||||
if score >= NOETIG and not absolviert:
|
||||
absolviert = True
|
||||
# Lern-Element einmalig anlegen, sobald die erste Stufe (Anfänger) erreicht ist.
|
||||
if not war_stufe and stufe_aus_score(score, cap) is not None:
|
||||
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}
|
||||
return {"punkte": punkte, "bewertung": _farbe(punkte), "gute_antworten": gute, "cap": cap}
|
||||
|
||||
|
||||
@router.post("/bausteine/pruefung", response_model=BausteinPruefungResponse)
|
||||
async def baustein_pruefung_route(req: BausteinPruefungRequest):
|
||||
stand = await get_baustein_progress(req.topic, req.baustein)
|
||||
gute = stand["gute_antworten"]
|
||||
absolviert = stand["absolviert"] is not None
|
||||
verstanden = stand["verstanden"] is not None
|
||||
gemeistert = stand["gemeistert"] is not None
|
||||
cap = await _cap_fuer(req.topic, req.baustein)
|
||||
kompakt = req.section_kompakt
|
||||
msgs = [m.model_dump() for m in req.messages]
|
||||
|
||||
@@ -325,7 +320,7 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest):
|
||||
frage = await pruefung_frage(req.topic, req.baustein, req.section, kompakt, msgs, subbausteine=subs, vermeide=req.vermeide, provider=req.provider)
|
||||
if frage is None:
|
||||
raise HTTPException(502, "Frage fehlgeschlagen — bitte erneut versuchen")
|
||||
return {"frage": frage, "gute_antworten": gute, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert}
|
||||
return {"frage": frage, "gute_antworten": gute, "cap": cap}
|
||||
|
||||
if req.aktion == "diskussion":
|
||||
if not req.frage.strip():
|
||||
@@ -336,7 +331,7 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest):
|
||||
)
|
||||
if reply is None:
|
||||
raise HTTPException(502, "Diskussion fehlgeschlagen — bitte erneut versuchen")
|
||||
return {"reply": reply, "gute_antworten": gute, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert}
|
||||
return {"reply": reply, "gute_antworten": gute, "cap": cap}
|
||||
|
||||
# --- Quiz: leicht (1 von 4) +1/−1 · schwer (x von 4) +3/−1 — deterministisch ---
|
||||
if req.aktion == "quiz_frage":
|
||||
@@ -346,14 +341,14 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest):
|
||||
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}
|
||||
"gute_antworten": gute, "cap": cap}
|
||||
|
||||
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 = await _buche(req, req.frage, delta, cap)
|
||||
res["feedback"] = "Richtig — alle korrekten getroffen." if getroffen else "Nicht ganz — die markierten waren richtig."
|
||||
return res
|
||||
|
||||
@@ -366,12 +361,12 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest):
|
||||
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}
|
||||
"gute_antworten": gute, "cap": cap}
|
||||
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}
|
||||
"gute_antworten": gute, "cap": cap}
|
||||
|
||||
if req.aktion == "lueck_antwort":
|
||||
if not req.frage.strip():
|
||||
@@ -382,7 +377,7 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest):
|
||||
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 = await _buche(req, req.frage, (3 if req.schwer else 1) if ok else -1, cap)
|
||||
res["feedback"] = feedback
|
||||
return res
|
||||
|
||||
@@ -400,19 +395,19 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest):
|
||||
if data is None:
|
||||
raise HTTPException(502, "Bewertung fehlgeschlagen — bitte erneut versuchen")
|
||||
basis, _ = _basis(stand, req.frage)
|
||||
floor = floor_aus_meilensteinen(absolviert, verstanden, gemeistert)
|
||||
score = score_berechnen(basis, _zufall_malus(basis, _erklaeren_delta(data["niveau"], req.nachgefragt)), floor, req.cap)
|
||||
floor = floor_aus_score(basis, cap)
|
||||
roh = _erklaeren_delta(data["niveau"], req.nachgefragt)
|
||||
d = progressiver_malus(basis, cap) if roh < 0 else roh
|
||||
score = score_berechnen(basis, d, floor, cap)
|
||||
punkte = score - basis
|
||||
return {"feedback": data["feedback"], "punkte": punkte, "bewertung": _farbe(punkte),
|
||||
"gute_antworten": gute, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert}
|
||||
"gute_antworten": gute, "cap": cap}
|
||||
|
||||
# aktion "antwort_pruefen" (Agent 2 genau): verbindlich, persistiert. Serialisiert je Baustein,
|
||||
# Basis driftfrei über den offene_frage-Anker. NUR hier ändert sich der Score.
|
||||
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
|
||||
war_stufe = stand["absolviert"] is not None # Element-Guard
|
||||
basis, re_bewertung = _basis(stand, req.frage)
|
||||
if not re_bewertung:
|
||||
await set_offene_frage(req.topic, req.baustein, req.frage, basis, 0)
|
||||
@@ -424,22 +419,17 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest):
|
||||
if data is None:
|
||||
raise HTTPException(502, "Bewertung fehlgeschlagen — bitte erneut versuchen")
|
||||
|
||||
floor = floor_aus_meilensteinen(absolviert, verstanden, gemeistert)
|
||||
score = score_berechnen(basis, _zufall_malus(basis, _erklaeren_delta(data["niveau"], req.nachgefragt)), floor, req.cap)
|
||||
floor = floor_aus_score(basis, cap)
|
||||
roh = _erklaeren_delta(data["niveau"], req.nachgefragt)
|
||||
d = progressiver_malus(basis, cap) if roh < 0 else roh
|
||||
score = score_berechnen(basis, d, floor, cap)
|
||||
punkte = score - basis
|
||||
gute = await set_baustein_score(req.topic, req.baustein, score)
|
||||
if score >= NOETIG and not absolviert:
|
||||
absolviert = True
|
||||
if not war_stufe and stufe_aus_score(score, cap) is not None:
|
||||
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 {"feedback": data["feedback"], "punkte": punkte, "bewertung": _farbe(punkte),
|
||||
"gute_antworten": gute, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert}
|
||||
"gute_antworten": gute, "cap": cap}
|
||||
|
||||
|
||||
# --- Guides ---
|
||||
|
||||
Reference in New Issue
Block a user