update
This commit is contained in:
@@ -25,6 +25,7 @@ MEISTERN = 25 # Score bis "gemeistert" (Tier 3, Maximum)
|
|||||||
VERTIEFUNG_TIMEOUT = 600
|
VERTIEFUNG_TIMEOUT = 600
|
||||||
CHAT_TIMEOUT = 240
|
CHAT_TIMEOUT = 240
|
||||||
PRUEFUNG_TIMEOUT = 120 # kurze JSON-Turns; deckelt die Serien-Latenz pro Prüfungs-Schritt
|
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
|
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}
|
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."""
|
"""Generator-Agent: Template füllen, laufen lassen, per schema parsen · None bei Fehler."""
|
||||||
returncode, stdout, _ = await run_agent(
|
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",
|
provider=provider, role=role, capabilities="none", lane="interactive",
|
||||||
)
|
)
|
||||||
return schema(_parse_json_text(stdout)) if returncode == 0 else None
|
return schema(_parse_json_text(stdout)) if returncode == 0 else None
|
||||||
|
|
||||||
|
|
||||||
async def _kritik_call(name: str, provider: str, **kwargs) -> list[str]:
|
async def _kritik_call(name: str, provider: str, role: str = "judge", timeout: int = PRUEFUNG_TIMEOUT, **kwargs) -> list[str]:
|
||||||
"""Kritiker-Agent (role judge): leere Liste = in Ordnung. Fail-open: Ausfall des
|
"""Kritiker-Agent (Default role judge): leere Liste = in Ordnung. Fail-open: Ausfall des
|
||||||
Kritikers darf den Turn nicht blockieren, also dann ebenfalls leere Liste."""
|
Kritikers darf den Turn nicht blockieren, also dann ebenfalls leere Liste."""
|
||||||
returncode, stdout, _ = await run_agent(
|
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="judge", capabilities="none", lane="interactive",
|
provider=provider, role=role, capabilities="none", lane="interactive",
|
||||||
)
|
)
|
||||||
if returncode != 0:
|
if returncode != 0:
|
||||||
return []
|
return []
|
||||||
@@ -199,18 +200,20 @@ async def _frage_mit_kritik(
|
|||||||
|
|
||||||
async def _bewertung_mit_kritik(
|
async def _bewertung_mit_kritik(
|
||||||
topic: str, baustein: str, section_block: str, vertiefung_block: str,
|
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:
|
) -> dict | None:
|
||||||
"""Antwort bewerten (gut/schlecht), vom Kritiker prüfen lassen, bei Fehlurteil neu.
|
"""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
|
`frage` ankert die geprüfte Frage; der Dialog (transcript) liefert die Antwort und
|
||||||
eine etwaige Diskussion — so kann eine Re-Bewertung das Argument sehen.
|
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)"
|
kritik_block = "(keine)"
|
||||||
bew = None
|
bew = None
|
||||||
for _ in range(KRITIK_MAX_RUNDEN):
|
for _ in range(KRITIK_MAX_RUNDEN):
|
||||||
bew = await _gen_call(
|
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,
|
topic=topic, baustein=baustein, section_block=section_block,
|
||||||
vertiefung_block=vertiefung_block, frage=frage, transcript=transcript,
|
vertiefung_block=vertiefung_block, frage=frage, transcript=transcript,
|
||||||
gute_antworten=gute_antworten, noetig=NOETIG, kritik_block=kritik_block,
|
gute_antworten=gute_antworten, noetig=NOETIG, kritik_block=kritik_block,
|
||||||
@@ -218,7 +221,7 @@ async def _bewertung_mit_kritik(
|
|||||||
if bew is None:
|
if bew is None:
|
||||||
return None
|
return None
|
||||||
probleme = await _kritik_call(
|
probleme = await _kritik_call(
|
||||||
"Baustein-Bewertung-Kritik", provider,
|
"Baustein-Bewertung-Kritik", provider, role=role, timeout=timeout,
|
||||||
topic=topic, baustein=baustein, section_block=section_block,
|
topic=topic, baustein=baustein, section_block=section_block,
|
||||||
vertiefung_block=vertiefung_block, frage=frage, transcript=transcript,
|
vertiefung_block=vertiefung_block, frage=frage, transcript=transcript,
|
||||||
bewertung_block=_bewertung_text(bew),
|
bewertung_block=_bewertung_text(bew),
|
||||||
@@ -253,9 +256,11 @@ async def pruefung_frage(
|
|||||||
async def pruefung_bewertung(
|
async def pruefung_bewertung(
|
||||||
topic: str, baustein: str, section: str, vertiefung: str | None,
|
topic: str, baustein: str, section: str, vertiefung: 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,
|
||||||
|
role: str = "judge",
|
||||||
) -> dict | None:
|
) -> dict | None:
|
||||||
"""Aktion 'antwort_pruefen': verbindlich bewerten (Evaluator + Kritiker).
|
"""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.
|
Gibt {"feedback", "bewertung": gut|schlecht, "bestanden"} · None bei Fehler.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
@@ -263,7 +268,7 @@ async def pruefung_bewertung(
|
|||||||
transcript = _transcript(messages) if messages else "(leer)"
|
transcript = _transcript(messages) if messages else "(leer)"
|
||||||
return await _bewertung_mit_kritik(
|
return await _bewertung_mit_kritik(
|
||||||
topic, baustein, section_block, vertiefung_block,
|
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:
|
except Exception:
|
||||||
log.warning("[%s] Bewertung fehlgeschlagen (%s)", topic, baustein, exc_info=True)
|
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
|
tier3: bool = False # ganzer Guide verstanden (alle ≥10) → Meisterpfad, −2 bei falsch, Deckel 25
|
||||||
messages: list[ChatMessage] = [] # Dialog bisher; leer = erste Frage
|
messages: list[ChatMessage] = [] # Dialog bisher; leer = erste Frage
|
||||||
provider: ProviderType = "claude"
|
provider: ProviderType = "claude"
|
||||||
|
gruendlich: bool = False # „Gründlich prüfen": Bewertung mit starkem Modell (role guide)
|
||||||
|
|
||||||
|
|
||||||
class BausteinPruefungResponse(BaseModel):
|
class BausteinPruefungResponse(BaseModel):
|
||||||
|
|||||||
@@ -259,6 +259,7 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest):
|
|||||||
# aktion == "antwort_pruefen": verbindlich (Bewerter + Kritiker), persistiert Score + Meilensteine.
|
# aktion == "antwort_pruefen": verbindlich (Bewerter + Kritiker), persistiert Score + Meilensteine.
|
||||||
data = await pruefung_bewertung(
|
data = await pruefung_bewertung(
|
||||||
req.topic, req.baustein, req.section, vertiefung, req.frage, msgs, gute, provider=req.provider,
|
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:
|
if data is None:
|
||||||
raise HTTPException(502, "Bewertung fehlgeschlagen — bitte erneut versuchen")
|
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({
|
export async function pruefeBaustein({
|
||||||
topic, baustein, section, provider,
|
topic, baustein, section, provider,
|
||||||
aktion = 'frage', frage = '', letzte_bewertung = '', score_vor_frage = 0, streak = 0,
|
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`, {
|
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, 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)
|
return jsonOrThrow(res)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<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 { chatBaustein, createVertiefung, fetchVertiefung, pruefeBaustein } from '../api.js'
|
||||||
import { renderMarkdown } from '../markdown.js'
|
import { renderMarkdown } from '../markdown.js'
|
||||||
import { useChat, istUnten } from '../composables/useChat.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.
|
// 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 meine = ++pruefenRun
|
||||||
const ziel = letzteFeedbackMsg
|
const ziel = letzteFeedbackMsg
|
||||||
const warAbsolviert = st.value.absolviert
|
const warAbsolviert = st.value.absolviert
|
||||||
const warVerstanden = st.value.verstanden
|
const warVerstanden = st.value.verstanden
|
||||||
|
if (gruendlich) pruefLoading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await pruefeBaustein({
|
const res = await pruefeBaustein({
|
||||||
topic: props.topic, baustein: props.baustein, section: props.section,
|
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
|
if (meine !== pruefenRun) return // eine neuere Bewertung läuft → diese verwerfen
|
||||||
// Streak driftfrei aus streakVorFrage neu setzen; Reset nur am Deckel der
|
// Streak driftfrei aus streakVorFrage neu setzen; Reset nur am Deckel der
|
||||||
@@ -275,6 +277,8 @@ async function verdictPruefen() {
|
|||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
if (meine === pruefenRun && ziel) ziel.geprueft = true
|
if (meine === pruefenRun && ziel) ziel.geprueft = true
|
||||||
|
} finally {
|
||||||
|
if (gruendlich && meine === pruefenRun) pruefLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,9 +290,39 @@ function antwortAbgeben() {
|
|||||||
pruefSenden(antwortPayload(), bewerten)
|
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
|
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>
|
</script>
|
||||||
|
|
||||||
@@ -357,7 +391,7 @@ function neuBewerten() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Prüfung: gesteuerter Dialog -->
|
<!-- Prüfung: gesteuerter Dialog -->
|
||||||
<div v-else>
|
<div v-else @keydown="onPruefKey">
|
||||||
<p class="bp-hint">
|
<p class="bp-hint">
|
||||||
<template v-if="st.gemeistert">✓✓✓ Gemeistert ({{ MEISTERN }}/{{ MEISTERN }}) — Max. Du kannst dich weiter prüfen, ohne Punkte.</template>
|
<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>
|
<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 -->
|
<!-- Phase idle: Frage anfordern -->
|
||||||
<div v-if="pruefPhase === 'idle'" class="bp-actions">
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Phase frage_offen / bewertet: Textfeld + Aktionen -->
|
<!-- Phase frage_offen / bewertet: Textfeld + Aktionen -->
|
||||||
@@ -393,14 +428,15 @@ function neuBewerten() {
|
|||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="bp-actions">
|
<div class="bp-actions">
|
||||||
<template v-if="pruefPhase === 'frage_offen'">
|
<button v-if="pruefLoading" class="bp-action cancel" @click="pruefAbbrechen">Abbrechen</button>
|
||||||
<button class="bp-action" :disabled="pruefLoading || !pruefInput.trim()" @click="nachfragen">Nachfragen</button>
|
<template v-else-if="pruefPhase === 'frage_offen'">
|
||||||
<button class="bp-action primary" :disabled="pruefLoading || !pruefInput.trim()" @click="antwortAbgeben">Antwort abgeben</button>
|
<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>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<button class="bp-action" :disabled="pruefLoading || !pruefInput.trim()" @click="nachfragen">Nachhaken</button>
|
<button class="bp-action" :disabled="!pruefInput.trim()" @click="nachfragen">Nachhaken</button>
|
||||||
<button class="bp-action" :disabled="pruefLoading" @click="neuBewerten">Neu bewerten</button>
|
<button class="bp-action" @click="gruendlichPruefen">Gründlich prüfen</button>
|
||||||
<button class="bp-action primary" :disabled="pruefLoading" @click="naechsteFrageZeigen">Nächste Frage</button>
|
<button class="bp-action primary" @click="naechsteFrageZeigen">Nächste Frage</button>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -463,6 +499,7 @@ function neuBewerten() {
|
|||||||
.bp-action:disabled { opacity: 0.5; cursor: default; }
|
.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 { 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.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 { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.5rem; }
|
||||||
.bp-actions .bp-action { margin-top: 0; }
|
.bp-actions .bp-action { margin-top: 0; }
|
||||||
|
|||||||
Reference in New Issue
Block a user