update
This commit is contained in:
@@ -82,6 +82,17 @@ def _crawl_fertig(topic: str) -> bool:
|
|||||||
_STUFEN = ("einfach", "mittel", "schwer")
|
_STUFEN = ("einfach", "mittel", "schwer")
|
||||||
|
|
||||||
|
|
||||||
|
def subbausteine_titel(topic: str, baustein: str) -> list[str]:
|
||||||
|
"""Subbaustein-Titel eines Bausteins aus der Sidecar (leer, wenn keine)."""
|
||||||
|
sc = _json_datei(subbausteine_path(topic))
|
||||||
|
if not isinstance(sc, dict):
|
||||||
|
return []
|
||||||
|
return [
|
||||||
|
t for s in (sc.get(baustein) or [])
|
||||||
|
if isinstance(s, dict) and (t := str(s.get("titel", "")).strip())
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def lade_uebersicht(topic: str) -> list[dict]:
|
def lade_uebersicht(topic: str) -> list[dict]:
|
||||||
"""Strukturierte Baustein-Liste für die Übersicht: Titel + Beschreibung + Subbausteine/Stufen.
|
"""Strukturierte Baustein-Liste für die Übersicht: Titel + Beschreibung + Subbausteine/Stufen.
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ der Chat-/Prüfungs-Verlauf kommt vom Frontend, persistiert wird nur der
|
|||||||
Prüfungs-Zähler (DB) und die Vertiefung (DB).
|
Prüfungs-Zähler (DB) und die Vertiefung (DB).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
|
import random
|
||||||
import re
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
@@ -29,6 +29,15 @@ PRUEFUNG_TIMEOUT = 120 # kurze JSON-Turns; deckelt die Serien-Latenz pro Prüfu
|
|||||||
GRUENDLICH_TIMEOUT = 600 # „Gründlich prüfen": starkes Modell (role guide) braucht länger
|
GRUENDLICH_TIMEOUT = 600 # „Gründlich prüfen": starkes Modell (role guide) braucht länger
|
||||||
KRITIK_MAX_RUNDEN = 2 # Generator → Kritiker → ggf. Neu, höchstens so oft
|
KRITIK_MAX_RUNDEN = 2 # Generator → Kritiker → ggf. Neu, höchstens so oft
|
||||||
|
|
||||||
|
# Fragetypen für Active Recall — pro Frage einer, zufällig gewählt. Schafft Vielfalt.
|
||||||
|
FRAGETYPEN = {
|
||||||
|
"abruf": "Free Recall: Lass den Lerner die Kernidee frei aus dem Kopf erklären (offene Verständnisfrage).",
|
||||||
|
"punkt": "Cued Recall: Frag gezielt EIN konkretes Detail oder eine Abgrenzung ab.",
|
||||||
|
"warum": "Warum-Frage: Frag nach dem Grund/Mechanismus — warum funktioniert oder gilt das so?",
|
||||||
|
"anwendung": "Anwendung: Lass das Konzept auf EIN kurzes, neues Beispiel/Szenario anwenden.",
|
||||||
|
"pruefen": "Bei Code-/Tool-Themen: kleinen Schnipsel zeigen — Output vorhersagen ODER den Fehler finden. Kein Code-Thema → stattdessen eine Anwendungsfrage.",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def score_berechnen(
|
def score_berechnen(
|
||||||
score_vor_frage: int, gut: bool, streak: int, tier2: bool, tier3: bool,
|
score_vor_frage: int, gut: bool, streak: int, tier2: bool, tier3: bool,
|
||||||
@@ -114,14 +123,6 @@ def _frage_schema(data) -> dict | None:
|
|||||||
return {"frage": frage} if frage else None
|
return {"frage": frage} if frage else None
|
||||||
|
|
||||||
|
|
||||||
def _fragen_schema(data) -> list[str] | None:
|
|
||||||
"""{"fragen": [str, …]} → Liste nicht-leerer Fragen · sonst None."""
|
|
||||||
if not isinstance(data, dict) or not isinstance(data.get("fragen"), list):
|
|
||||||
return None
|
|
||||||
fragen = [str(f).strip() for f in data["fragen"] if str(f).strip()]
|
|
||||||
return fragen or None
|
|
||||||
|
|
||||||
|
|
||||||
def _bewertung_schema(data) -> dict | None:
|
def _bewertung_schema(data) -> dict | None:
|
||||||
"""{"feedback": str, "bewertung": "gut"|"schlecht", "bestanden": bool} · sonst None."""
|
"""{"feedback": str, "bewertung": "gut"|"schlecht", "bestanden": bool} · sonst None."""
|
||||||
if not isinstance(data, dict):
|
if not isinstance(data, dict):
|
||||||
@@ -182,7 +183,7 @@ def _doppelfrage_mangel(frage: str) -> str | None:
|
|||||||
|
|
||||||
async def _frage_mit_kritik(
|
async def _frage_mit_kritik(
|
||||||
topic: str, baustein: str, section_block: str, kompakt_block: str,
|
topic: str, baustein: str, section_block: str, kompakt_block: str,
|
||||||
transcript: str, vermeide_block: str, provider: str,
|
transcript: str, vermeide_block: str, typ_block: str, fokus_block: str, provider: str,
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
"""Frage generieren, vom Kritiker prüfen lassen, bei Mängeln neu (max KRITIK_MAX_RUNDEN)."""
|
"""Frage generieren, vom Kritiker prüfen lassen, bei Mängeln neu (max KRITIK_MAX_RUNDEN)."""
|
||||||
kritik_block = "(keine)"
|
kritik_block = "(keine)"
|
||||||
@@ -192,7 +193,7 @@ async def _frage_mit_kritik(
|
|||||||
"Baustein-Frage", "fast", _frage_schema, provider,
|
"Baustein-Frage", "fast", _frage_schema, provider,
|
||||||
topic=topic, baustein=baustein, section_block=section_block,
|
topic=topic, baustein=baustein, section_block=section_block,
|
||||||
kompakt_block=kompakt_block, transcript=transcript, vermeide_block=vermeide_block,
|
kompakt_block=kompakt_block, transcript=transcript, vermeide_block=vermeide_block,
|
||||||
kritik_block=kritik_block,
|
typ_block=typ_block, fokus_block=fokus_block, kritik_block=kritik_block,
|
||||||
)
|
)
|
||||||
if data is None:
|
if data is None:
|
||||||
return None
|
return None
|
||||||
@@ -200,7 +201,8 @@ async def _frage_mit_kritik(
|
|||||||
probleme = await _kritik_call(
|
probleme = await _kritik_call(
|
||||||
"Baustein-Frage-Kritik", provider, role="guide", # starke KI prüft die Regeln
|
"Baustein-Frage-Kritik", provider, role="guide", # starke KI prüft die Regeln
|
||||||
topic=topic, baustein=baustein, section_block=section_block,
|
topic=topic, baustein=baustein, section_block=section_block,
|
||||||
kompakt_block=kompakt_block, transcript=transcript, vermeide_block=vermeide_block, frage=frage,
|
kompakt_block=kompakt_block, transcript=transcript, vermeide_block=vermeide_block,
|
||||||
|
typ_block=typ_block, fokus_block=fokus_block, frage=frage,
|
||||||
)
|
)
|
||||||
hart = _doppelfrage_mangel(frage) # erzwingt Neugenerierung, auch wenn der KI-Kritiker es übersah
|
hart = _doppelfrage_mangel(frage) # erzwingt Neugenerierung, auch wenn der KI-Kritiker es übersah
|
||||||
if hart:
|
if hart:
|
||||||
@@ -259,70 +261,30 @@ def _vermeide_block(vermeide: list[str] | None) -> str:
|
|||||||
|
|
||||||
async def pruefung_frage(
|
async def pruefung_frage(
|
||||||
topic: str, baustein: str, section: str, kompakt: str | None,
|
topic: str, baustein: str, section: str, kompakt: str | None,
|
||||||
messages: list[dict], vermeide: list[str] | None = None, provider: str = DEFAULT_PROVIDER,
|
messages: list[dict], subbausteine: list[str] | None = None,
|
||||||
|
vermeide: list[str] | None = None, provider: str = DEFAULT_PROVIDER,
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
"""Aktion 'frage': nächste Frage generieren (Generator + Kritiker) · None bei Fehler."""
|
"""Aktion 'frage': eine Frage generieren — zufälliger Typ zu einem zufälligen Subbaustein,
|
||||||
|
dann Kritiker (Generator → Kritiker sequenziell) · None bei Fehler."""
|
||||||
try:
|
try:
|
||||||
section_block, kompakt_block = _bloecke(section, kompakt)
|
section_block, kompakt_block = _bloecke(section, kompakt)
|
||||||
transcript = _transcript(messages) if messages else "(leer)"
|
transcript = _transcript(messages) if messages else "(leer)"
|
||||||
|
typ_block = FRAGETYPEN[random.choice(list(FRAGETYPEN))]
|
||||||
|
subs = [s for s in (subbausteine or []) if s and s.strip()]
|
||||||
|
fokus = random.choice(subs) if subs else ""
|
||||||
|
fokus_block = (
|
||||||
|
f"Konzentriere die Frage auf diesen Subbaustein: „{fokus}\"" if fokus
|
||||||
|
else "(ganzer Baustein — kein bestimmter Subbaustein)"
|
||||||
|
)
|
||||||
return await _frage_mit_kritik(
|
return await _frage_mit_kritik(
|
||||||
topic, baustein, section_block, kompakt_block, transcript, _vermeide_block(vermeide), provider,
|
topic, baustein, section_block, kompakt_block, transcript,
|
||||||
|
_vermeide_block(vermeide), typ_block, fokus_block, provider,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
log.warning("[%s] Frage fehlgeschlagen (%s)", topic, baustein, exc_info=True)
|
log.warning("[%s] Frage fehlgeschlagen (%s)", topic, baustein, exc_info=True)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def _frage_ok(topic, baustein, section_block, kompakt_block, transcript, vermeide_block, frage, provider) -> bool:
|
|
||||||
"""Eine geplante Frage prüfen: Doppelfrage-Guard + Kritiker. True = in Ordnung."""
|
|
||||||
if _doppelfrage_mangel(frage):
|
|
||||||
return False
|
|
||||||
probleme = await _kritik_call(
|
|
||||||
"Baustein-Frage-Kritik", provider, role="guide",
|
|
||||||
topic=topic, baustein=baustein, section_block=section_block,
|
|
||||||
kompakt_block=kompakt_block, transcript=transcript, vermeide_block=vermeide_block, frage=frage,
|
|
||||||
)
|
|
||||||
return not probleme
|
|
||||||
|
|
||||||
|
|
||||||
async def pruefung_fragen(
|
|
||||||
topic: str, baustein: str, section: str, kompakt: str | None,
|
|
||||||
messages: list[dict], vermeide: list[str] | None = None, anzahl: int = 5,
|
|
||||||
provider: str = DEFAULT_PROVIDER,
|
|
||||||
) -> list[str]:
|
|
||||||
"""Aktion 'fragen': EIN Planer entwirft `anzahl` verschiedene Fragen, parallel geprüft.
|
|
||||||
Diversität kommt vom Planer; der Kritiker prüft Qualität + Ähnlichkeit zu Gestelltem.
|
|
||||||
→ Liste gültiger Fragen (kann < anzahl sein)."""
|
|
||||||
try:
|
|
||||||
section_block, kompakt_block = _bloecke(section, kompakt)
|
|
||||||
transcript = _transcript(messages) if messages else "(leer)"
|
|
||||||
vermeide = [f for f in (vermeide or []) if f and f.strip()]
|
|
||||||
gesammelt: list[str] = []
|
|
||||||
for _ in range(2): # Plan + eine Nachrunde für Abgelehnte
|
|
||||||
brauche = anzahl - len(gesammelt)
|
|
||||||
if brauche <= 0:
|
|
||||||
break
|
|
||||||
vblock = _vermeide_block(vermeide + gesammelt)
|
|
||||||
kandidaten = await _gen_call(
|
|
||||||
"Baustein-Fragen-Plan", "fast", _fragen_schema, provider, CHAT_TIMEOUT,
|
|
||||||
topic=topic, baustein=baustein, section_block=section_block, kompakt_block=kompakt_block,
|
|
||||||
transcript=transcript, vermeide_block=vblock, anzahl=brauche,
|
|
||||||
)
|
|
||||||
if not kandidaten:
|
|
||||||
break
|
|
||||||
oks = await asyncio.gather(
|
|
||||||
*[_frage_ok(topic, baustein, section_block, kompakt_block, transcript, vblock, f, provider) for f in kandidaten],
|
|
||||||
return_exceptions=True,
|
|
||||||
)
|
|
||||||
for f, ok in zip(kandidaten, oks):
|
|
||||||
if ok is True and f not in gesammelt:
|
|
||||||
gesammelt.append(f)
|
|
||||||
return gesammelt[:anzahl]
|
|
||||||
except Exception:
|
|
||||||
log.warning("[%s] Fragen-Plan fehlgeschlagen (%s)", topic, baustein, exc_info=True)
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
async def pruefung_bewertung(
|
async def pruefung_bewertung(
|
||||||
topic: str, baustein: str, section: str, kompakt: str | None,
|
topic: str, baustein: str, section: str, kompakt: str | None,
|
||||||
frage: str, messages: list[dict], gute_antworten: int, provider: str = DEFAULT_PROVIDER,
|
frage: str, messages: list[dict], gute_antworten: int, provider: str = DEFAULT_PROVIDER,
|
||||||
@@ -345,28 +307,6 @@ async def pruefung_bewertung(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def pruefung_bewertung_schnell(
|
|
||||||
topic: str, baustein: str, section: str, kompakt: str | None,
|
|
||||||
frage: str, messages: list[dict], gute_antworten: int, provider: str = DEFAULT_PROVIDER,
|
|
||||||
) -> dict | None:
|
|
||||||
"""Aktion 'antwort' (Vorschau): nur Evaluator, KEIN Kritiker — sofortiges Urteil.
|
|
||||||
|
|
||||||
Wird optimistisch angezeigt; 'antwort_pruefen' liefert danach das geprüfte Urteil.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
section_block, kompakt_block = _bloecke(section, kompakt)
|
|
||||||
transcript = _transcript(messages) if messages else "(leer)"
|
|
||||||
return await _gen_call(
|
|
||||||
"Baustein-Bewertung", "judge", _bewertung_schema, provider,
|
|
||||||
topic=topic, baustein=baustein, section_block=section_block,
|
|
||||||
kompakt_block=kompakt_block, frage=frage.strip() or "(keine Frage übergeben)",
|
|
||||||
transcript=transcript, gute_antworten=gute_antworten, noetig=NOETIG, kritik_block="(keine)",
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
log.warning("[%s] Schnell-Bewertung fehlgeschlagen (%s)", topic, baustein, exc_info=True)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
async def baustein_diskussion(
|
async def baustein_diskussion(
|
||||||
topic: str, baustein: str, section: str, kompakt: str | None,
|
topic: str, baustein: str, section: str, kompakt: str | None,
|
||||||
frage: str, letzte_bewertung: str | None, messages: list[dict], provider: str = DEFAULT_PROVIDER,
|
frage: str, letzte_bewertung: str | None, messages: list[dict], provider: str = DEFAULT_PROVIDER,
|
||||||
|
|||||||
@@ -214,11 +214,10 @@ class BausteinPruefungRequest(BaseModel):
|
|||||||
baustein: str = Field(min_length=1, max_length=200)
|
baustein: str = Field(min_length=1, max_length=200)
|
||||||
section: str = Field(default="", max_length=20000) # ausführliche Fassung
|
section: str = Field(default="", max_length=20000) # ausführliche Fassung
|
||||||
section_kompakt: str = Field(default="", max_length=20000) # kompakte Fassung (Merksätze)
|
section_kompakt: str = Field(default="", max_length=20000) # kompakte Fassung (Merksätze)
|
||||||
aktion: Literal["frage", "fragen", "diskussion", "antwort", "antwort_pruefen"] = "frage"
|
aktion: Literal["frage", "diskussion", "antwort_pruefen"] = "frage"
|
||||||
frage: str = Field(default="", max_length=2000) # aktuell geprüfte Frage (für diskussion/antwort); Anker der Basis
|
frage: str = Field(default="", max_length=2000) # aktuell geprüfte Frage (für diskussion/antwort); Anker der Basis
|
||||||
letzte_bewertung: str = Field(default="", max_length=2000) # Feedback der letzten Bewertung (Kontext für diskussion)
|
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
|
vermeide: list[str] = [] # schon gestellte + vorgemerkte Fragen — sinngemäß nicht wiederholen
|
||||||
anzahl: int = 5 # aktion 'fragen': wie viele neue Fragen der Planer entwerfen soll
|
|
||||||
# Basis/Streak werden serverseitig je Frage geführt (offene_frage-Anker) — nicht mehr vom Client.
|
# Basis/Streak werden 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
|
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
|
tier3: bool = False # ganzer Guide verstanden (alle ≥10) → Meisterpfad, −2 bei falsch, Deckel 25
|
||||||
@@ -229,7 +228,6 @@ class BausteinPruefungRequest(BaseModel):
|
|||||||
|
|
||||||
class BausteinPruefungResponse(BaseModel):
|
class BausteinPruefungResponse(BaseModel):
|
||||||
frage: str | None = None
|
frage: str | None = None
|
||||||
fragen: list[str] | None = None # aktion 'fragen': geplante, geprüfte Fragen
|
|
||||||
reply: str | None = None
|
reply: str | None = None
|
||||||
feedback: str | None = None
|
feedback: str | None = None
|
||||||
bewertung: Literal["gut", "schlecht"] | None = None
|
bewertung: Literal["gut", "schlecht"] | None = None
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ from database import (
|
|||||||
list_baustein_progress, get_baustein_progress, set_offene_frage,
|
list_baustein_progress, get_baustein_progress, set_offene_frage,
|
||||||
set_baustein_score, set_baustein_streak, set_baustein_absolviert, set_baustein_verstanden, set_baustein_gemeistert, delete_baustein_daten,
|
set_baustein_score, set_baustein_streak, set_baustein_absolviert, set_baustein_verstanden, set_baustein_gemeistert, delete_baustein_daten,
|
||||||
)
|
)
|
||||||
from bausteine import generate_bausteine, cancel_bausteine, bausteine_status, active_bausteine, reset_bausteine, lade_quelle, lade_uebersicht
|
from bausteine import generate_bausteine, cancel_bausteine, bausteine_status, active_bausteine, reset_bausteine, lade_quelle, lade_uebersicht, subbausteine_titel
|
||||||
from elements import generate_element, chat_with_guide, chat_with_element, check_element, style_element, refine_suggestion
|
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_fragen, score_berechnen, streak_berechnen
|
from lernen import NOETIG, MASTERY, MEISTERN, baustein_chat, baustein_diskussion, baustein_element_anlegen, pruefung_bewertung, pruefung_frage, score_berechnen, streak_berechnen
|
||||||
from guide import generate_guide, guide_slot_dateien
|
from guide import generate_guide, guide_slot_dateien
|
||||||
from pipeline import cancel_guide
|
from pipeline import cancel_guide
|
||||||
from regeln import FORMATE, formate_stats, guide_lock, ist_absolviert, lade_lernstand, thema_abgeschlossen
|
from regeln import FORMATE, formate_stats, guide_lock, ist_absolviert, lade_lernstand, thema_abgeschlossen
|
||||||
@@ -268,15 +268,12 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest):
|
|||||||
msgs = [m.model_dump() for m in req.messages]
|
msgs = [m.model_dump() for m in req.messages]
|
||||||
|
|
||||||
if req.aktion == "frage":
|
if req.aktion == "frage":
|
||||||
frage = await pruefung_frage(req.topic, req.baustein, req.section, kompakt, msgs, vermeide=req.vermeide, provider=req.provider)
|
subs = subbausteine_titel(req.topic, req.baustein) # Fokus-Kandidaten (zufällig gewählt)
|
||||||
|
frage = await pruefung_frage(req.topic, req.baustein, req.section, kompakt, msgs, subbausteine=subs, vermeide=req.vermeide, provider=req.provider)
|
||||||
if frage is None:
|
if frage is None:
|
||||||
raise HTTPException(502, "Frage fehlgeschlagen — bitte erneut versuchen")
|
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, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert}
|
||||||
|
|
||||||
if req.aktion == "fragen":
|
|
||||||
fragen = await pruefung_fragen(req.topic, req.baustein, req.section, kompakt, msgs, vermeide=req.vermeide, anzahl=req.anzahl, provider=req.provider)
|
|
||||||
return {"fragen": fragen, "gute_antworten": gute, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert}
|
|
||||||
|
|
||||||
if req.aktion == "diskussion":
|
if req.aktion == "diskussion":
|
||||||
if not req.frage.strip():
|
if not req.frage.strip():
|
||||||
raise HTTPException(400, "Diskussion braucht eine laufende Frage")
|
raise HTTPException(400, "Diskussion braucht eine laufende Frage")
|
||||||
@@ -288,27 +285,15 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest):
|
|||||||
raise HTTPException(502, "Diskussion fehlgeschlagen — bitte erneut versuchen")
|
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, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert}
|
||||||
|
|
||||||
# aktion "antwort"/"antwort_pruefen" — mindestens eine Nutzer-Antwort muss im Dialog stehen
|
# aktion == "antwort_pruefen" — mindestens eine Nutzer-Antwort muss im Dialog stehen
|
||||||
# (nach einer Diskussion endet der Dialog mit dem Tutor; Re-Bewertung bleibt erlaubt).
|
# (nach einer Diskussion endet der Dialog mit dem Tutor; Re-Bewertung bleibt erlaubt).
|
||||||
if not any(m.get("role") == "user" for m in msgs):
|
if not any(m.get("role") == "user" for m in msgs):
|
||||||
raise HTTPException(400, "Antwort braucht eine Nutzer-Antwort")
|
raise HTTPException(400, "Antwort braucht eine Nutzer-Antwort")
|
||||||
if not req.frage.strip():
|
if not req.frage.strip():
|
||||||
raise HTTPException(400, "Antwort braucht eine laufende Frage")
|
raise HTTPException(400, "Antwort braucht eine laufende Frage")
|
||||||
|
|
||||||
if req.aktion == "antwort":
|
# Verbindlich, persistiert. Serialisiert je Baustein, damit die Basis immer den
|
||||||
# Vorschau: nur Bewerter, kein Kritiker, KEIN Persist. Basis serverseitig (best effort).
|
# geprüften Vorgänger-Stand sieht (kein Stale-Overwrite). Punkte kommen NUR hier.
|
||||||
data = await pruefung_bewertung_schnell(
|
|
||||||
req.topic, req.baustein, req.section, kompakt, req.frage, msgs, gute, provider=req.provider,
|
|
||||||
)
|
|
||||||
if data is None:
|
|
||||||
raise HTTPException(502, "Bewertung fehlgeschlagen — bitte erneut versuchen")
|
|
||||||
basis, streak_basis, _ = _basis(stand, req.frage)
|
|
||||||
score = score_berechnen(basis, data["bewertung"] == "gut", streak_basis, req.tier2, req.tier3, absolviert, verstanden, gemeistert)
|
|
||||||
return {"feedback": data["feedback"], "bewertung": data["bewertung"], "gute_antworten": score,
|
|
||||||
"absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert}
|
|
||||||
|
|
||||||
# aktion == "antwort_pruefen": verbindlich, persistiert. Serialisiert je Baustein, damit
|
|
||||||
# die Basis immer den geprüften Vorgänger-Stand sieht (kein Stale-Overwrite).
|
|
||||||
async with _pruef_lock(req.topic, req.baustein):
|
async with _pruef_lock(req.topic, req.baustein):
|
||||||
stand = await get_baustein_progress(req.topic, req.baustein) # nach evtl. Vorgänger-Persist neu lesen
|
stand = await get_baustein_progress(req.topic, req.baustein) # nach evtl. Vorgänger-Persist neu lesen
|
||||||
absolviert = stand["absolviert"] is not None
|
absolviert = stand["absolviert"] is not None
|
||||||
|
|||||||
@@ -77,13 +77,13 @@ export async function chatBaustein({ topic, baustein, section, section_kompakt =
|
|||||||
|
|
||||||
export async function pruefeBaustein({
|
export async function pruefeBaustein({
|
||||||
topic, baustein, section, section_kompakt = '', provider,
|
topic, baustein, section, section_kompakt = '', provider,
|
||||||
aktion = 'frage', frage = '', letzte_bewertung = '', vermeide = [], anzahl = 5,
|
aktion = 'frage', frage = '', letzte_bewertung = '', vermeide = [],
|
||||||
tier2 = false, tier3 = false, messages = [], gruendlich = false,
|
tier2 = false, tier3 = false, messages = [], gruendlich = false,
|
||||||
}) {
|
}) {
|
||||||
const res = await fetch(`${BASE}/bausteine/pruefung`, {
|
const res = await fetch(`${BASE}/bausteine/pruefung`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ topic, baustein, section, section_kompakt, aktion, frage, letzte_bewertung, vermeide, anzahl, tier2, tier3, messages, provider, gruendlich }),
|
body: JSON.stringify({ topic, baustein, section, section_kompakt, aktion, frage, letzte_bewertung, vermeide, tier2, tier3, messages, provider, gruendlich }),
|
||||||
})
|
})
|
||||||
return jsonOrThrow(res)
|
return jsonOrThrow(res)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,24 +137,27 @@ function vermeideListe() {
|
|||||||
return [...gestellt, ...pool.value]
|
return [...gestellt, ...pool.value]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pool als BATCH auffüllen: EIN Planer entwirft die fehlenden Fragen auf einmal
|
function frageEinzeln() {
|
||||||
// (verschieden by design), parallel geprüft → schnell und ohne ähnliche Dubletten.
|
// Ein Agent → eine Frage (Backend wählt zufälligen Typ + Subbaustein, dann Kritiker).
|
||||||
function poolFuellen() {
|
return pruefeBaustein({
|
||||||
if (inflight.value > 0) return
|
|
||||||
const brauche = POOL_ZIEL - pool.value.length
|
|
||||||
if (brauche <= 0 || poolMiss >= 3) return
|
|
||||||
inflight.value++
|
|
||||||
pruefeBaustein({
|
|
||||||
topic: props.topic, baustein: props.baustein, section: props.section, section_kompakt: props.sectionKompakt,
|
topic: props.topic, baustein: props.baustein, section: props.section, section_kompakt: props.sectionKompakt,
|
||||||
provider: props.provider, aktion: 'fragen', anzahl: brauche, messages: pruefDialog(), vermeide: vermeideListe(),
|
provider: props.provider, aktion: 'frage', messages: pruefDialog(), vermeide: vermeideListe(),
|
||||||
})
|
}).then((res) => res.frage)
|
||||||
.then((res) => {
|
}
|
||||||
const neu = (res.fragen || []).filter((f) => f && !istBekannt(f))
|
|
||||||
if (neu.length) { pool.value.push(...neu); poolMiss = 0 }
|
// Pool PARALLEL auffüllen: pro fehlender Frage ein eigener Agent gleichzeitig.
|
||||||
|
// Initial 5 parallel; nach Verbrauch genau die fehlende(n) nach.
|
||||||
|
function poolFuellen() {
|
||||||
|
while (pool.value.length + inflight.value < POOL_ZIEL && poolMiss < 3) {
|
||||||
|
inflight.value++
|
||||||
|
frageEinzeln()
|
||||||
|
.then((text) => {
|
||||||
|
if (text && !istBekannt(text)) { pool.value.push(text); poolMiss = 0 }
|
||||||
else poolMiss++
|
else poolMiss++
|
||||||
})
|
})
|
||||||
.catch(() => { poolMiss++ })
|
.catch(() => { poolMiss++ })
|
||||||
.finally(() => { inflight.value--; if (pool.value.length < POOL_ZIEL) poolFuellen() })
|
.finally(() => { inflight.value--; poolFuellen() })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Eine Frage aus dem Pool nehmen; ist er leer, auf die erste eintreffende warten.
|
// Eine Frage aus dem Pool nehmen; ist er leer, auf die erste eintreffende warten.
|
||||||
@@ -211,47 +214,40 @@ function nachfragen() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
let letzteFeedbackMsg = null // Referenz auf die zuletzt gezeigte (provisorische) Bewertungs-Bubble
|
let letzteFeedbackMsg = null // Referenz auf die zuletzt gezeigte Bewertungs-Bubble
|
||||||
let pruefenRun = 0 // nur die jüngste Hintergrund-Prüfung darf die Anzeige korrigieren
|
let pruefenRun = 0 // nur die jüngste Bewertung darf die Anzeige setzen
|
||||||
|
|
||||||
function bewerten(res) {
|
// Antwort verbindlich prüfen — der Prüfer (Bewerter + Kritiker) vergibt die Punkte.
|
||||||
letztesFeedback.value = res.feedback || ''
|
// Während des Wartens zeigt das „…"-Bubble den Geprüft-Zustand. gruendlich=true:
|
||||||
pruefMessages.value.push({ role: 'assistant', kind: 'feedback', content: res.feedback || '', bewertung: res.bewertung, geprueft: false })
|
// starkes Modell, re-bewertet dieselbe Frage und aktualisiert die bestehende Bubble.
|
||||||
letzteFeedbackMsg = pruefMessages.value[pruefMessages.value.length - 1]
|
async function bewertenLassen(gruendlich = false) {
|
||||||
pruefPhase.value = 'bewertet'
|
|
||||||
verdictPruefen() // im Hintergrund verbindlich prüfen lassen
|
|
||||||
}
|
|
||||||
|
|
||||||
function antwortPayload() {
|
|
||||||
// Basis/Streak führt der Server je Frage (frage = Anker). Frontend schickt sie nicht mehr.
|
|
||||||
return { aktion: 'antwort', frage: aktuelleFrage.value, tier2: props.tier2, tier3: props.tier3 }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hintergrund: voller Bewerter+Kritiker. Persistiert serverseitig; korrigiert die Anzeige bei Abweichung.
|
|
||||||
// gruendlich=true: starkes Modell (role guide), manuell ausgelöst → sichtbarer Ladezustand.
|
|
||||||
async function verdictPruefen(gruendlich = false) {
|
|
||||||
const meine = ++pruefenRun
|
const meine = ++pruefenRun
|
||||||
const ziel = letzteFeedbackMsg
|
pruefStick.value = true
|
||||||
if (gruendlich) pruefLoading.value = true
|
pruefLoading.value = true
|
||||||
|
pruefScroll()
|
||||||
try {
|
try {
|
||||||
const res = await pruefeBaustein({
|
const res = await pruefeBaustein({
|
||||||
topic: props.topic, baustein: props.baustein, section: props.section, section_kompakt: props.sectionKompakt,
|
topic: props.topic, baustein: props.baustein, section: props.section, section_kompakt: props.sectionKompakt,
|
||||||
provider: props.provider, messages: pruefDialog(), ...antwortPayload(), aktion: 'antwort_pruefen', gruendlich,
|
provider: props.provider, messages: pruefDialog(),
|
||||||
|
aktion: 'antwort_pruefen', frage: aktuelleFrage.value, tier2: props.tier2, tier3: props.tier3, gruendlich,
|
||||||
})
|
})
|
||||||
// Server führt den Score (serialisiert, absolut) — IMMER übernehmen, auch wenn man
|
if (meine !== pruefenRun) return
|
||||||
// schon bei der nächsten Frage ist. Sonst geht die Korrektur verloren.
|
|
||||||
applyPruefung(res)
|
applyPruefung(res)
|
||||||
if (ziel) {
|
letztesFeedback.value = res.feedback || ''
|
||||||
if (res.bewertung !== ziel.bewertung || (res.feedback && res.feedback !== ziel.content)) {
|
if (gruendlich && letzteFeedbackMsg) {
|
||||||
ziel.content = res.feedback || ziel.content
|
letzteFeedbackMsg.content = res.feedback || letzteFeedbackMsg.content
|
||||||
ziel.bewertung = res.bewertung
|
letzteFeedbackMsg.bewertung = res.bewertung
|
||||||
}
|
} else {
|
||||||
ziel.geprueft = true
|
pruefMessages.value.push({ role: 'assistant', kind: 'feedback', content: res.feedback || '', bewertung: res.bewertung, geprueft: true })
|
||||||
|
letzteFeedbackMsg = pruefMessages.value[pruefMessages.value.length - 1]
|
||||||
|
pruefPhase.value = 'bewertet'
|
||||||
}
|
}
|
||||||
|
pruefScroll()
|
||||||
|
nextTick(() => pruefInputEl.value?.focus())
|
||||||
} catch {
|
} catch {
|
||||||
if (ziel) ziel.geprueft = true
|
if (meine === pruefenRun) pruefMessages.value.push({ role: 'assistant', kind: 'fehler', content: 'Hat nicht geklappt — bitte erneut.' })
|
||||||
} finally {
|
} finally {
|
||||||
if (gruendlich && meine === pruefenRun) pruefLoading.value = false
|
if (meine === pruefenRun) pruefLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,7 +256,7 @@ function antwortAbgeben() {
|
|||||||
if (!text || pruefLoading.value) return
|
if (!text || pruefLoading.value) return
|
||||||
pruefMessages.value.push({ role: 'user', kind: 'antwort', content: text })
|
pruefMessages.value.push({ role: 'user', kind: 'antwort', content: text })
|
||||||
pruefInput.value = ''
|
pruefInput.value = ''
|
||||||
pruefSenden(antwortPayload(), bewerten)
|
bewertenLassen(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lief schon eine Diskussion? Dann darf der Verlauf direkt bewertet werden.
|
// Lief schon eine Diskussion? Dann darf der Verlauf direkt bewertet werden.
|
||||||
@@ -269,17 +265,17 @@ const hatDiskussion = computed(() => pruefMessages.value.some((m) => m.kind ===
|
|||||||
// „Verlauf bewerten": den bisherigen Dialog als Antwort werten (Feld wird ignoriert).
|
// „Verlauf bewerten": den bisherigen Dialog als Antwort werten (Feld wird ignoriert).
|
||||||
function verlaufBewerten() {
|
function verlaufBewerten() {
|
||||||
if (pruefLoading.value) return
|
if (pruefLoading.value) return
|
||||||
pruefSenden(antwortPayload(), bewerten)
|
bewertenLassen(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// „Gründlich prüfen": dieselbe Antwort, aber vom starken Modell (role guide) verbindlich neu bewerten.
|
// „Gründlich prüfen": dieselbe Antwort vom starken Modell (role guide) neu bewerten.
|
||||||
function gruendlichPruefen() {
|
function gruendlichPruefen() {
|
||||||
if (pruefLoading.value) return
|
if (pruefLoading.value) return
|
||||||
verdictPruefen(true)
|
bewertenLassen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Abbruch wie im Chat: Run-Counter hochzählen → laufendes Ergebnis (pruefSenden
|
// Abbruch wie im Chat: Run-Counter hochzählen → laufendes Ergebnis (pruefSenden
|
||||||
// und verdictPruefen) wird verworfen, Buttons sofort frei. Agent läuft serverseitig aus.
|
// und bewertenLassen) wird verworfen, Buttons sofort frei. Agent läuft serverseitig aus.
|
||||||
function pruefAbbrechen() {
|
function pruefAbbrechen() {
|
||||||
if (!pruefLoading.value) return
|
if (!pruefLoading.value) return
|
||||||
pruefRun++
|
pruefRun++
|
||||||
@@ -296,7 +292,6 @@ onMounted(() => {
|
|||||||
if (props.mode !== 'full') return // Trigger-Modus (Liste): nur Tab-Leiste, keine Prüfungs-/Text-Logik
|
if (props.mode !== 'full') return // Trigger-Modus (Liste): nur Tab-Leiste, keine Prüfungs-/Text-Logik
|
||||||
window.addEventListener('keydown', onWindowKey)
|
window.addEventListener('keydown', onWindowKey)
|
||||||
window.addEventListener('keydown', onPruefKey)
|
window.addEventListener('keydown', onPruefKey)
|
||||||
poolFuellen() // Pool schon beim Öffnen vorfüllen — keine Wartezeit bei der ersten Frage
|
|
||||||
})
|
})
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('keydown', onWindowKey)
|
window.removeEventListener('keydown', onWindowKey)
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ BISHERIGER PRÜFUNGS-VERLAUF (nur frühere Fragen und Antworten):
|
|||||||
BEREITS GESTELLT / SCHON VORGEMERKT (Frage darf keiner davon gleichen):
|
BEREITS GESTELLT / SCHON VORGEMERKT (Frage darf keiner davon gleichen):
|
||||||
{vermeide_block}
|
{vermeide_block}
|
||||||
|
|
||||||
|
GEWÜNSCHTER FRAGETYP / FOKUS (zur Orientierung):
|
||||||
|
{typ_block}
|
||||||
|
{fokus_block}
|
||||||
|
|
||||||
ZU PRÜFENDE FRAGE:
|
ZU PRÜFENDE FRAGE:
|
||||||
{frage}
|
{frage}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ BISHERIGER PRÜFUNGS-VERLAUF (nur frühere Fragen und Antworten):
|
|||||||
BEREITS GESTELLT / SCHON VORGEMERKT — auch SINNGEMÄSS NICHT wiederholen:
|
BEREITS GESTELLT / SCHON VORGEMERKT — auch SINNGEMÄSS NICHT wiederholen:
|
||||||
{vermeide_block}
|
{vermeide_block}
|
||||||
|
|
||||||
|
GEWÜNSCHTER FRAGETYP:
|
||||||
|
{typ_block}
|
||||||
|
|
||||||
|
FOKUS:
|
||||||
|
{fokus_block}
|
||||||
|
|
||||||
HARTE REGELN FÜR DIE FRAGE — wichtiger als alles andere:
|
HARTE REGELN FÜR DIE FRAGE — wichtiger als alles andere:
|
||||||
- GENAU EINE Frage. Ein einziges Fragezeichen. Eine einzige Sache.
|
- GENAU EINE Frage. Ein einziges Fragezeichen. Eine einzige Sache.
|
||||||
- Maximal 1–2 Sätze. Kein Szenario-Aufbau, keine Vorrede, kein "Angenommen … und außerdem …".
|
- Maximal 1–2 Sätze. Kein Szenario-Aufbau, keine Vorrede, kein "Angenommen … und außerdem …".
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
Du bist Prüfer in einem Lern-Guide zum Thema "{topic}". Plane {anzahl} Verständnisfragen zum Baustein "{baustein}" — auf einmal, damit sie sich klar UNTERSCHEIDEN. Der Lerner sieht das Material; frage nach Verständnis und Transfer, nicht nach Abgelesenem.
|
|
||||||
|
|
||||||
BAUSTEIN AUS DEM GUIDE:
|
|
||||||
{section_block}
|
|
||||||
|
|
||||||
KOMPAKTE FASSUNG (Merksätze, falls vorhanden):
|
|
||||||
{kompakt_block}
|
|
||||||
|
|
||||||
BISHERIGER PRÜFUNGS-VERLAUF (nur frühere Fragen und Antworten):
|
|
||||||
{transcript}
|
|
||||||
|
|
||||||
BEREITS GESTELLT / SCHON VORGEMERKT — auch SINNGEMÄSS NICHT wiederholen:
|
|
||||||
{vermeide_block}
|
|
||||||
|
|
||||||
WICHTIG — VIELFALT:
|
|
||||||
- Die {anzahl} Fragen müssen verschiedene Aspekte/Subbausteine treffen. KEINE zwei Fragen zur gleichen Kernsache, auch nicht umformuliert.
|
|
||||||
- Decke möglichst unterschiedliche Subbausteine ab (einfach → mittel → schwer gemischt).
|
|
||||||
|
|
||||||
HARTE REGELN JE FRAGE — wichtiger als alles andere:
|
|
||||||
- GENAU EINE Frage. Ein einziges Fragezeichen. Eine einzige Sache.
|
|
||||||
- Maximal 1–2 Sätze. Kein Szenario-Aufbau, keine Vorrede, kein "Angenommen … und außerdem …".
|
|
||||||
- Verboten: zwei Fragen mit "und"/"sowie" verketten, "nenne drei …", "sowohl … als auch …", Aufzähl-Forderungen.
|
|
||||||
- Frag nach EINEM Gedanken: ein Warum, eine Konsequenz, eine Abgrenzung, die Anwendung auf EIN kurzes Beispiel, einen Fehler finden.
|
|
||||||
- KEIN Faktenabruf ("welche Daten…", "wie viele…", "was enthält…").
|
|
||||||
- Frag nur zu dem, was der Abschnitt wirklich erklärt — nicht zu am Rand Erwähntem.
|
|
||||||
- Fachlich korrekt: aus dem Material beantwortbar, der Referenz NIE widersprechen, keine erfundenen Zusatzannahmen.
|
|
||||||
|
|
||||||
Sprich den Lerner direkt an, klares Deutsch, keine Floskeln.
|
|
||||||
|
|
||||||
Gib NUR dieses JSON aus (kein weiterer Text), genau {anzahl} verschiedene Fragen:
|
|
||||||
{{"fragen": ["genau eine kurze Frage", "…"]}}
|
|
||||||
Reference in New Issue
Block a user