This commit is contained in:
team3
2026-06-17 11:54:45 +02:00
parent e8214598ea
commit 390ec12bbf
5 changed files with 69 additions and 25 deletions

View File

@@ -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)

View File

@@ -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):

View File

@@ -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")

View File

@@ -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)
}

View File

@@ -1,5 +1,5 @@
<script setup>
import { computed, nextTick, ref } from 'vue'
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
import { chatBaustein, createVertiefung, fetchVertiefung, pruefeBaustein } from '../api.js'
import { renderMarkdown } from '../markdown.js'
import { useChat, istUnten } from '../composables/useChat.js'
@@ -245,15 +245,17 @@ function antwortPayload() {
}
// Hintergrund: voller Bewerter+Kritiker. Persistiert serverseitig; korrigiert die Anzeige bei Abweichung.
async function verdictPruefen() {
// gruendlich=true: starkes Modell (role guide), manuell ausgelöst → sichtbarer Ladezustand.
async function verdictPruefen(gruendlich = false) {
const meine = ++pruefenRun
const ziel = letzteFeedbackMsg
const warAbsolviert = st.value.absolviert
const warVerstanden = st.value.verstanden
if (gruendlich) pruefLoading.value = true
try {
const res = await pruefeBaustein({
topic: props.topic, baustein: props.baustein, section: props.section,
provider: props.provider, messages: pruefDialog(), ...antwortPayload(), aktion: 'antwort_pruefen',
provider: props.provider, messages: pruefDialog(), ...antwortPayload(), aktion: 'antwort_pruefen', gruendlich,
})
if (meine !== pruefenRun) return // eine neuere Bewertung läuft → diese verwerfen
// Streak driftfrei aus streakVorFrage neu setzen; Reset nur am Deckel der
@@ -275,6 +277,8 @@ async function verdictPruefen() {
}
} catch {
if (meine === pruefenRun && ziel) ziel.geprueft = true
} finally {
if (gruendlich && meine === pruefenRun) pruefLoading.value = false
}
}
@@ -286,9 +290,39 @@ function antwortAbgeben() {
pruefSenden(antwortPayload(), bewerten)
}
function neuBewerten() {
// „Gründlich prüfen": dieselbe Antwort, aber vom starken Modell (role guide) verbindlich neu bewerten.
function gruendlichPruefen() {
if (pruefLoading.value) return
pruefSenden(antwortPayload(), bewerten)
verdictPruefen(true)
}
// Abbruch wie im Chat: Run-Counter hochzählen → laufendes Ergebnis (pruefSenden
// und verdictPruefen) wird verworfen, Buttons sofort frei. Agent läuft serverseitig aus.
function pruefAbbrechen() {
if (!pruefLoading.value) return
pruefRun++
pruefenRun++
pruefLoading.value = false
pruefMessages.value.push({ role: 'assistant', kind: 'fehler', content: 'Abgebrochen.' })
}
// ESC bricht ab — window-Listener, da nach Button-Klick der Fokus das Panel verlässt.
function onWindowKey(e) {
if (e.key === 'Escape' && pruefLoading.value) { e.preventDefault(); pruefAbbrechen() }
}
onMounted(() => window.addEventListener('keydown', onWindowKey))
onUnmounted(() => window.removeEventListener('keydown', onWindowKey))
// ALT+1 Nachfragen/Nachhaken · ALT+2 primär (Antwort abgeben / Nächste Frage) · ALT+3 Gründlich prüfen.
function onPruefKey(e) {
if (!e.altKey || e.repeat) return
if (e.key === '1') { e.preventDefault(); nachfragen() }
else if (e.key === '2') {
e.preventDefault()
pruefPhase.value === 'frage_offen' ? antwortAbgeben() : naechsteFrageZeigen()
} else if (e.key === '3' && pruefPhase.value === 'bewertet') {
e.preventDefault(); gruendlichPruefen()
}
}
</script>
@@ -357,7 +391,7 @@ function neuBewerten() {
</div>
<!-- Prüfung: gesteuerter Dialog -->
<div v-else>
<div v-else @keydown="onPruefKey">
<p class="bp-hint">
<template v-if="st.gemeistert"> Gemeistert ({{ MEISTERN }}/{{ MEISTERN }}) Max. Du kannst dich weiter prüfen, ohne Punkte.</template>
<template v-else-if="st.verstanden && tier3">Meisterpfad: {{ st.gute_antworten }}/{{ MEISTERN }}. Richtig = +1, falsch = 2 (nicht unter {{ MAX }}). Bei {{ MEISTERN }} gemeistert.</template>
@@ -379,7 +413,8 @@ function neuBewerten() {
<!-- Phase idle: Frage anfordern -->
<div v-if="pruefPhase === 'idle'" class="bp-actions">
<button class="bp-action primary" :disabled="pruefLoading" @click="frageAnfordern">Frage anfordern</button>
<button v-if="pruefLoading" class="bp-action cancel" @click="pruefAbbrechen">Abbrechen</button>
<button v-else class="bp-action primary" @click="frageAnfordern">Frage anfordern</button>
</div>
<!-- Phase frage_offen / bewertet: Textfeld + Aktionen -->
@@ -393,14 +428,15 @@ function neuBewerten() {
></textarea>
</div>
<div class="bp-actions">
<template v-if="pruefPhase === 'frage_offen'">
<button class="bp-action" :disabled="pruefLoading || !pruefInput.trim()" @click="nachfragen">Nachfragen</button>
<button class="bp-action primary" :disabled="pruefLoading || !pruefInput.trim()" @click="antwortAbgeben">Antwort abgeben</button>
<button v-if="pruefLoading" class="bp-action cancel" @click="pruefAbbrechen">Abbrechen</button>
<template v-else-if="pruefPhase === 'frage_offen'">
<button class="bp-action" :disabled="!pruefInput.trim()" @click="nachfragen">Nachfragen</button>
<button class="bp-action primary" :disabled="!pruefInput.trim()" @click="antwortAbgeben">Antwort abgeben</button>
</template>
<template v-else>
<button class="bp-action" :disabled="pruefLoading || !pruefInput.trim()" @click="nachfragen">Nachhaken</button>
<button class="bp-action" :disabled="pruefLoading" @click="neuBewerten">Neu bewerten</button>
<button class="bp-action primary" :disabled="pruefLoading" @click="naechsteFrageZeigen">Nächste Frage</button>
<button class="bp-action" :disabled="!pruefInput.trim()" @click="nachfragen">Nachhaken</button>
<button class="bp-action" @click="gruendlichPruefen">Gründlich prüfen</button>
<button class="bp-action primary" @click="naechsteFrageZeigen">Nächste Frage</button>
</template>
</div>
</template>
@@ -463,6 +499,7 @@ function neuBewerten() {
.bp-action:disabled { opacity: 0.5; cursor: default; }
.bp-action.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.bp-action.primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
.bp-action.cancel { background: var(--danger); border-color: var(--danger); color: var(--on-accent); }
.bp-actions { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.5rem; }
.bp-actions .bp-action { margin-top: 0; }