diff --git a/backend/lernen.py b/backend/lernen.py index b751b14..8b77770 100644 --- a/backend/lernen.py +++ b/backend/lernen.py @@ -25,6 +25,7 @@ MEISTERN = 25 # Score bis "gemeistert" (Tier 3, Maximum) VERTIEFUNG_TIMEOUT = 600 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 KRITIK_MAX_RUNDEN = 2 # Generator → Kritiker → ggf. Neu, höchstens so oft @@ -137,21 +138,21 @@ def _bewertung_schema(data) -> dict | None: return {"feedback": feedback, "bewertung": bewertung, "bestanden": data.get("bestanden") is True} -async def _gen_call(name: str, role: str, schema, provider: str, **kwargs) -> dict | None: +async def _gen_call(name: str, role: str, schema, provider: str, timeout: int = PRUEFUNG_TIMEOUT, **kwargs) -> dict | None: """Generator-Agent: Template füllen, laufen lassen, per schema parsen · None bei Fehler.""" returncode, stdout, _ = await run_agent( - name.lower() + "-" + str(uuid.uuid4()), _prompt(name, **kwargs), PRUEFUNG_TIMEOUT, + name.lower() + "-" + str(uuid.uuid4()), _prompt(name, **kwargs), timeout, provider=provider, role=role, capabilities="none", lane="interactive", ) return schema(_parse_json_text(stdout)) if returncode == 0 else None -async def _kritik_call(name: str, provider: str, **kwargs) -> list[str]: - """Kritiker-Agent (role judge): leere Liste = in Ordnung. Fail-open: Ausfall des +async def _kritik_call(name: str, provider: str, role: str = "judge", timeout: int = PRUEFUNG_TIMEOUT, **kwargs) -> list[str]: + """Kritiker-Agent (Default role judge): leere Liste = in Ordnung. Fail-open: Ausfall des Kritikers darf den Turn nicht blockieren, also dann ebenfalls leere Liste.""" returncode, stdout, _ = await run_agent( - name.lower() + "-" + str(uuid.uuid4()), _prompt(name, **kwargs), PRUEFUNG_TIMEOUT, - provider=provider, role="judge", capabilities="none", lane="interactive", + name.lower() + "-" + str(uuid.uuid4()), _prompt(name, **kwargs), timeout, + provider=provider, role=role, capabilities="none", lane="interactive", ) if returncode != 0: return [] @@ -199,18 +200,20 @@ async def _frage_mit_kritik( async def _bewertung_mit_kritik( topic: str, baustein: str, section_block: str, vertiefung_block: str, - frage: str, transcript: str, gute_antworten: int, provider: str, + frage: str, transcript: str, gute_antworten: int, provider: str, role: str = "judge", ) -> dict | None: """Antwort bewerten (gut/schlecht), vom Kritiker prüfen lassen, bei Fehlurteil neu. `frage` ankert die geprüfte Frage; der Dialog (transcript) liefert die Antwort und eine etwaige Diskussion — so kann eine Re-Bewertung das Argument sehen. + `role` = "judge" (schnell) oder "guide" (gründlich, starkes Modell mit Thinking). """ + timeout = GRUENDLICH_TIMEOUT if role == "guide" else PRUEFUNG_TIMEOUT kritik_block = "(keine)" bew = None for _ in range(KRITIK_MAX_RUNDEN): bew = await _gen_call( - "Baustein-Bewertung", "judge", _bewertung_schema, provider, + "Baustein-Bewertung", role, _bewertung_schema, provider, timeout, topic=topic, baustein=baustein, section_block=section_block, vertiefung_block=vertiefung_block, frage=frage, transcript=transcript, gute_antworten=gute_antworten, noetig=NOETIG, kritik_block=kritik_block, @@ -218,7 +221,7 @@ async def _bewertung_mit_kritik( if bew is None: return None probleme = await _kritik_call( - "Baustein-Bewertung-Kritik", provider, + "Baustein-Bewertung-Kritik", provider, role=role, timeout=timeout, topic=topic, baustein=baustein, section_block=section_block, vertiefung_block=vertiefung_block, frage=frage, transcript=transcript, bewertung_block=_bewertung_text(bew), @@ -253,9 +256,11 @@ async def pruefung_frage( 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, + role: str = "judge", ) -> dict | None: """Aktion 'antwort_pruefen': verbindlich bewerten (Evaluator + Kritiker). + `role` = "guide" für „Gründlich prüfen" (starkes Modell), sonst "judge". Gibt {"feedback", "bewertung": gut|schlecht, "bestanden"} · None bei Fehler. """ try: @@ -263,7 +268,7 @@ async def pruefung_bewertung( transcript = _transcript(messages) if messages else "(leer)" return await _bewertung_mit_kritik( topic, baustein, section_block, vertiefung_block, - frage.strip() or "(keine Frage übergeben)", transcript, gute_antworten, provider, + frage.strip() or "(keine Frage übergeben)", transcript, gute_antworten, provider, role, ) except Exception: log.warning("[%s] Bewertung fehlgeschlagen (%s)", topic, baustein, exc_info=True) diff --git a/backend/models.py b/backend/models.py index 61cf26e..6f4c655 100644 --- a/backend/models.py +++ b/backend/models.py @@ -200,6 +200,7 @@ class BausteinPruefungRequest(BaseModel): tier3: bool = False # ganzer Guide verstanden (alle ≥10) → Meisterpfad, −2 bei falsch, Deckel 25 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 BausteinPruefungResponse(BaseModel): diff --git a/backend/routes.py b/backend/routes.py index 7de8683..df75f31 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -259,6 +259,7 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest): # aktion == "antwort_pruefen": verbindlich (Bewerter + Kritiker), persistiert Score + Meilensteine. data = await pruefung_bewertung( req.topic, req.baustein, req.section, vertiefung, req.frage, msgs, gute, provider=req.provider, + role="guide" if req.gruendlich else "judge", ) if data is None: raise HTTPException(502, "Bewertung fehlgeschlagen — bitte erneut versuchen") diff --git a/frontend/src/api.js b/frontend/src/api.js index 3626617..0914cfc 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -94,12 +94,12 @@ export async function chatBaustein({ topic, baustein, section, messages, provide export async function pruefeBaustein({ topic, baustein, section, provider, aktion = 'frage', frage = '', letzte_bewertung = '', score_vor_frage = 0, streak = 0, - tier2 = false, tier3 = false, messages = [], + tier2 = false, tier3 = false, messages = [], gruendlich = false, }) { const res = await fetch(`${BASE}/bausteine/pruefung`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ topic, baustein, section, aktion, frage, letzte_bewertung, score_vor_frage, streak, tier2, tier3, messages, provider }), + body: JSON.stringify({ topic, baustein, section, aktion, frage, letzte_bewertung, score_vor_frage, streak, tier2, tier3, messages, provider, gruendlich }), }) return jsonOrThrow(res) } diff --git a/frontend/src/components/BausteinPanel.vue b/frontend/src/components/BausteinPanel.vue index c088fa0..bc719b6 100644 --- a/frontend/src/components/BausteinPanel.vue +++ b/frontend/src/components/BausteinPanel.vue @@ -1,5 +1,5 @@ @@ -357,7 +391,7 @@ function neuBewerten() { -
✓✓✓ Gemeistert ({{ MEISTERN }}/{{ MEISTERN }}) — Max. Du kannst dich weiter prüfen, ohne Punkte. Meisterpfad: {{ st.gute_antworten }}/{{ MEISTERN }}. Richtig = +1, falsch = −2 (nicht unter {{ MAX }}). Bei {{ MEISTERN }} gemeistert. @@ -379,7 +413,8 @@ function neuBewerten() {