This commit is contained in:
team3
2026-06-14 14:02:27 +02:00
parent 822f6ee3e9
commit 2b89e21cd3
18 changed files with 378 additions and 119 deletions

View File

@@ -159,16 +159,20 @@ async def _frage_mit_kritik(
async def _bewertung_mit_kritik(
topic: str, baustein: str, section_block: str, vertiefung_block: str,
transcript: str, gute_antworten: int, provider: str,
frage: str, transcript: str, gute_antworten: int, provider: str,
) -> dict | None:
"""Letzte Antwort bewerten, vom Kritiker prüfen lassen, bei Fehlurteil neu."""
"""Antwort zur Frage bewerten, vom Kritiker prüfen lassen, bei Fehlurteil neu.
`frage` ankert, welche Frage geprüft wird; der Dialog (transcript) liefert die
Antwort und eine etwaige Diskussion — so kann eine Re-Bewertung das Argument sehen.
"""
kritik_block = "(keine)"
bew = None
for _ in range(KRITIK_MAX_RUNDEN):
bew = await _gen_call(
"Baustein-Bewertung", "judge", _bewertung_schema, provider,
topic=topic, baustein=baustein, section_block=section_block,
vertiefung_block=vertiefung_block, transcript=transcript,
vertiefung_block=vertiefung_block, frage=frage, transcript=transcript,
gute_antworten=gute_antworten, noetig=NOETIG, kritik_block=kritik_block,
)
if bew is None:
@@ -176,7 +180,7 @@ async def _bewertung_mit_kritik(
probleme = await _kritik_call(
"Baustein-Bewertung-Kritik", provider,
topic=topic, baustein=baustein, section_block=section_block,
vertiefung_block=vertiefung_block, transcript=transcript,
vertiefung_block=vertiefung_block, frage=frage, transcript=transcript,
bewertung_block=_bewertung_text(bew),
)
if not probleme:
@@ -185,38 +189,74 @@ async def _bewertung_mit_kritik(
return bew # best-effort nach der letzten Runde
async def baustein_pruefung(
topic: str, baustein: str, section: str, vertiefung: str | None,
messages: list[dict], gute_antworten: int, provider: str = DEFAULT_PROVIDER,
) -> dict | None:
"""Ein Prüfungs-Turn: erst (falls Antwort vorliegt) bewerten, dann nächste Frage.
def _bloecke(section: str, vertiefung: str | None) -> tuple[str, str]:
return (
section.strip() or "(keine Guide-Fassung übergeben)",
(vertiefung or "").strip() or "(keine)",
)
Generator + Kritiker-Loop je Schritt. Gibt
{"feedback", "frage", "bewertung", "bestanden"} zurück · None bei Fehler.
Bewertet wird nur, wenn der Verlauf mit einer Nutzer-Antwort endet — sonst
bekäme der Evaluator keine Antwort zum Beurteilen.
async def pruefung_frage(
topic: str, baustein: str, section: str, vertiefung: str | None,
messages: list[dict], provider: str = DEFAULT_PROVIDER,
) -> str | None:
"""Aktion 'frage': nächste Frage generieren (Generator + Kritiker) · None bei Fehler."""
try:
section_block, vertiefung_block = _bloecke(section, vertiefung)
transcript = _transcript(messages) if messages else "(leer)"
return await _frage_mit_kritik(topic, baustein, section_block, vertiefung_block, transcript, provider)
except Exception:
log.warning("[%s] Frage fehlgeschlagen (%s)", topic, baustein, exc_info=True)
return None
async def pruefung_bewertung(
topic: str, baustein: str, section: str, vertiefung: str | None,
frage: str, messages: list[dict], gute_antworten: int, provider: str = DEFAULT_PROVIDER,
) -> dict | None:
"""Aktion 'antwort': Antwort zur Frage bewerten (Evaluator + Kritiker).
Gibt {"feedback", "bewertung", "bestanden"} · None bei Fehler.
"""
try:
section_block = section.strip() or "(keine Guide-Fassung übergeben)"
vertiefung_block = (vertiefung or "").strip() or "(keine)"
section_block, vertiefung_block = _bloecke(section, vertiefung)
transcript = _transcript(messages) if messages else "(leer)"
bewerten = bool(messages) and messages[-1].get("role") == "user"
feedback, bewertung, bestanden = None, None, False
if bewerten:
bew = await _bewertung_mit_kritik(
topic, baustein, section_block, vertiefung_block, transcript, gute_antworten, provider,
)
if bew is None:
return None
feedback, bewertung, bestanden = bew["feedback"], bew["bewertung"], bew["bestanden"]
frage = await _frage_mit_kritik(topic, baustein, section_block, vertiefung_block, transcript, provider)
if frage is None:
return None
return {"feedback": feedback, "frage": frage, "bewertung": bewertung, "bestanden": bestanden}
return await _bewertung_mit_kritik(
topic, baustein, section_block, vertiefung_block,
frage.strip() or "(keine Frage übergeben)", transcript, gute_antworten, provider,
)
except Exception:
log.warning("[%s] Prüfung fehlgeschlagen (%s)", topic, baustein, exc_info=True)
log.warning("[%s] Bewertung fehlgeschlagen (%s)", topic, baustein, exc_info=True)
return None
async def baustein_diskussion(
topic: str, baustein: str, section: str, vertiefung: str | None,
frage: str, letzte_bewertung: str | None, messages: list[dict], provider: str = DEFAULT_PROVIDER,
) -> str | None:
"""Aktion 'diskussion': Tutor erklärt/diskutiert die Frage oder eine Bewertung.
Kein Bewerten, kein Kritiker — hier ist der Mensch der Prüfer. None bei Fehler.
"""
try:
section_block, vertiefung_block = _bloecke(section, vertiefung)
prompt = _prompt(
"Baustein-Pruefung-Diskussion",
topic=topic, baustein=baustein,
section_block=section_block, vertiefung_block=vertiefung_block,
frage=frage.strip() or "(keine Frage übergeben)",
letzte_bewertung_block=(letzte_bewertung or "").strip() or "(noch keine)",
transcript=_transcript(messages) if messages else "(leer)",
)
returncode, stdout, _ = await run_agent(
"pruefungdiskussion-" + str(uuid.uuid4()), prompt, CHAT_TIMEOUT,
provider=provider, role="fast", capabilities="none", lane="interactive",
)
if returncode != 0:
return None
return stdout.strip() or None
except Exception:
log.warning("[%s] Prüfungs-Diskussion fehlgeschlagen (%s)", topic, baustein, exc_info=True)
return None

View File

@@ -191,13 +191,18 @@ class BausteinPruefungRequest(BaseModel):
topic: str = Field(min_length=1, max_length=100)
baustein: str = Field(min_length=1, max_length=200)
section: str = Field(default="", max_length=20000)
messages: list[ChatMessage] = [] # leer = KI stellt die erste Frage
aktion: Literal["frage", "diskussion", "antwort"] = "frage"
frage: str = Field(default="", max_length=2000) # aktuell geprüfte Frage (für diskussion/antwort)
letzte_bewertung: str = Field(default="", max_length=2000) # Feedback der letzten Bewertung (Kontext für diskussion)
frage_schon_gut: bool = False # diese Frage wurde schon einmal "gut" bewertet → nicht doppelt zählen
messages: list[ChatMessage] = [] # Dialog bisher; leer = erste Frage
provider: ProviderType = "claude"
class BausteinPruefungResponse(BaseModel):
frage: str | None = None
reply: str | None = None
feedback: str | None = None
frage: str
bewertung: Literal["gut", "schlecht"] | None = None
gute_antworten: int
absolviert: bool

View File

@@ -18,7 +18,7 @@ from database import (
)
from bausteine import generate_bausteine, cancel_bausteine, bausteine_status, active_bausteine, reset_bausteine
from elements import generate_element, chat_with_guide, chat_with_element, check_element, style_element, refine_suggestion
from lernen import NOETIG, baustein_chat, baustein_element_anlegen, baustein_pruefung, vertiefung_generieren
from lernen import NOETIG, baustein_chat, baustein_diskussion, baustein_element_anlegen, pruefung_bewertung, pruefung_frage, vertiefung_generieren
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
@@ -210,24 +210,48 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest):
(p for p in await list_baustein_progress(req.topic) if p["baustein"] == req.baustein),
{"gute_antworten": 0, "absolviert": None},
)
gute = stand["gute_antworten"]
absolviert = stand["absolviert"] is not None
vertiefung = await _bester_text(req.topic, req.baustein)
data = await baustein_pruefung(
req.topic, req.baustein, req.section, vertiefung,
[m.model_dump() for m in req.messages], stand["gute_antworten"], provider=req.provider,
msgs = [m.model_dump() for m in req.messages]
if req.aktion == "frage":
frage = await pruefung_frage(req.topic, req.baustein, req.section, vertiefung, msgs, provider=req.provider)
if frage is None:
raise HTTPException(502, "Frage fehlgeschlagen — bitte erneut versuchen")
return {"frage": frage, "gute_antworten": gute, "absolviert": absolviert}
if req.aktion == "diskussion":
if not req.frage.strip():
raise HTTPException(400, "Diskussion braucht eine laufende Frage")
reply = await baustein_diskussion(
req.topic, req.baustein, req.section, vertiefung,
req.frage, req.letzte_bewertung or None, msgs, provider=req.provider,
)
if reply is None:
raise HTTPException(502, "Diskussion fehlgeschlagen — bitte erneut versuchen")
return {"reply": reply, "gute_antworten": gute, "absolviert": absolviert}
# aktion == "antwort" — mindestens eine Nutzer-Antwort muss im Dialog stehen
# (nach einer Diskussion endet der Dialog mit dem Tutor; Re-Bewertung bleibt erlaubt).
if not any(m.get("role") == "user" for m in msgs):
raise HTTPException(400, "Antwort braucht eine Nutzer-Antwort")
if not req.frage.strip():
raise HTTPException(400, "Antwort braucht eine laufende Frage")
data = await pruefung_bewertung(
req.topic, req.baustein, req.section, vertiefung, req.frage, msgs, gute, provider=req.provider,
)
if data is None:
raise HTTPException(502, "Prüfung fehlgeschlagen — bitte erneut versuchen")
raise HTTPException(502, "Bewertung fehlgeschlagen — bitte erneut versuchen")
gute = stand["gute_antworten"]
if data["bewertung"] == "gut":
if data["bewertung"] == "gut" and not req.frage_schon_gut:
gute = await add_gute_antwort(req.topic, req.baustein)
absolviert = stand["absolviert"] is not None
if gute >= NOETIG or data["bestanden"]:
frisch = await set_baustein_absolviert(req.topic, req.baustein)
absolviert = True
if frisch:
asyncio.create_task(baustein_element_anlegen(req.topic, req.baustein, req.section, req.provider))
return {"feedback": data["feedback"], "frage": data["frage"], "bewertung": data["bewertung"], "gute_antworten": gute, "absolviert": absolviert}
return {"feedback": data["feedback"], "bewertung": data["bewertung"], "gute_antworten": gute, "absolviert": absolviert}
# --- Guides ---