update
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user