This commit is contained in:
team3
2026-06-15 17:02:53 +02:00
parent 19b520a3b1
commit 466818c47c
4 changed files with 70 additions and 6 deletions

View File

@@ -241,7 +241,7 @@ 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,
) -> dict | None: ) -> dict | None:
"""Aktion 'antwort': Antwort bewerten (Evaluator + Kritiker). """Aktion 'antwort_pruefen': verbindlich bewerten (Evaluator + Kritiker).
Gibt {"feedback", "bewertung": gut|schlecht, "bestanden"} · None bei Fehler. Gibt {"feedback", "bewertung": gut|schlecht, "bestanden"} · None bei Fehler.
""" """
@@ -257,6 +257,28 @@ async def pruefung_bewertung(
return None return None
async def pruefung_bewertung_schnell(
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' (Vorschau): nur Evaluator, KEIN Kritiker — sofortiges Urteil.
Wird optimistisch angezeigt; 'antwort_pruefen' liefert danach das geprüfte Urteil.
"""
try:
section_block, vertiefung_block = _bloecke(section, vertiefung)
transcript = _transcript(messages) if messages else "(leer)"
return await _gen_call(
"Baustein-Bewertung", "judge", _bewertung_schema, provider,
topic=topic, baustein=baustein, section_block=section_block,
vertiefung_block=vertiefung_block, frage=frage.strip() or "(keine Frage übergeben)",
transcript=transcript, gute_antworten=gute_antworten, noetig=NOETIG, kritik_block="(keine)",
)
except Exception:
log.warning("[%s] Schnell-Bewertung fehlgeschlagen (%s)", topic, baustein, exc_info=True)
return None
async def baustein_diskussion( async def baustein_diskussion(
topic: str, baustein: str, section: str, vertiefung: str | None, topic: str, baustein: str, section: str, vertiefung: str | None,
frage: str, letzte_bewertung: str | None, messages: list[dict], provider: str = DEFAULT_PROVIDER, frage: str, letzte_bewertung: str | None, messages: list[dict], provider: str = DEFAULT_PROVIDER,

View File

@@ -191,7 +191,7 @@ class BausteinPruefungRequest(BaseModel):
topic: str = Field(min_length=1, max_length=100) topic: str = Field(min_length=1, max_length=100)
baustein: str = Field(min_length=1, max_length=200) baustein: str = Field(min_length=1, max_length=200)
section: str = Field(default="", max_length=20000) section: str = Field(default="", max_length=20000)
aktion: Literal["frage", "diskussion", "antwort"] = "frage" aktion: Literal["frage", "diskussion", "antwort", "antwort_pruefen"] = "frage"
frage: str = Field(default="", max_length=2000) # aktuell geprüfte Frage (für diskussion/antwort) 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) letzte_bewertung: str = Field(default="", max_length=2000) # Feedback der letzten Bewertung (Kontext für diskussion)
score_vor_frage: int = 0 # Score, als die Frage gestellt wurde → driftfreies (Re-)Bewerten score_vor_frage: int = 0 # Score, als die Frage gestellt wurde → driftfreies (Re-)Bewerten

View File

@@ -18,7 +18,7 @@ from database import (
) )
from bausteine import generate_bausteine, cancel_bausteine, bausteine_status, active_bausteine, reset_bausteine 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 elements import generate_element, chat_with_guide, chat_with_element, check_element, style_element, refine_suggestion
from lernen import NOETIG, MASTERY, MEISTERN, baustein_chat, baustein_diskussion, baustein_element_anlegen, pruefung_bewertung, pruefung_frage, score_berechnen, vertiefung_generieren from lernen import NOETIG, MASTERY, MEISTERN, baustein_chat, baustein_diskussion, baustein_element_anlegen, pruefung_bewertung, pruefung_bewertung_schnell, pruefung_frage, score_berechnen, vertiefung_generieren
from guide import generate_guide, guide_slot_dateien from guide import generate_guide, guide_slot_dateien
from pipeline import cancel_guide from pipeline import cancel_guide
from regeln import FORMATE, formate_stats, guide_lock, ist_absolviert, lade_lernstand, thema_abgeschlossen from regeln import FORMATE, formate_stats, guide_lock, ist_absolviert, lade_lernstand, thema_abgeschlossen
@@ -238,12 +238,25 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest):
raise HTTPException(502, "Diskussion fehlgeschlagen — bitte erneut versuchen") raise HTTPException(502, "Diskussion fehlgeschlagen — bitte erneut versuchen")
return {"reply": reply, "gute_antworten": gute, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert} return {"reply": reply, "gute_antworten": gute, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert}
# aktion == "antwort" — mindestens eine Nutzer-Antwort muss im Dialog stehen # aktion "antwort"/"antwort_pruefen" — mindestens eine Nutzer-Antwort muss im Dialog stehen
# (nach einer Diskussion endet der Dialog mit dem Tutor; Re-Bewertung bleibt erlaubt). # (nach einer Diskussion endet der Dialog mit dem Tutor; Re-Bewertung bleibt erlaubt).
if not any(m.get("role") == "user" for m in msgs): if not any(m.get("role") == "user" for m in msgs):
raise HTTPException(400, "Antwort braucht eine Nutzer-Antwort") raise HTTPException(400, "Antwort braucht eine Nutzer-Antwort")
if not req.frage.strip(): if not req.frage.strip():
raise HTTPException(400, "Antwort braucht eine laufende Frage") raise HTTPException(400, "Antwort braucht eine laufende Frage")
if req.aktion == "antwort":
# Vorschau: nur Bewerter, kein Kritiker, KEIN Persist, KEINE Meilensteine.
data = await pruefung_bewertung_schnell(
req.topic, req.baustein, req.section, vertiefung, req.frage, msgs, gute, provider=req.provider,
)
if data is None:
raise HTTPException(502, "Bewertung fehlgeschlagen — bitte erneut versuchen")
score = score_berechnen(req.score_vor_frage, data["bewertung"] == "gut", req.tier2, req.tier3, absolviert, gemeistert)
return {"feedback": data["feedback"], "bewertung": data["bewertung"], "gute_antworten": score,
"absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert}
# 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,
) )

View File

@@ -193,10 +193,15 @@ function nachfragen() {
) )
} }
let letzteFeedbackMsg = null // Referenz auf die zuletzt gezeigte (provisorische) Bewertungs-Bubble
let pruefenRun = 0 // nur die jüngste Hintergrund-Prüfung darf die Anzeige korrigieren
function bewerten(res) { function bewerten(res) {
letztesFeedback.value = res.feedback || '' letztesFeedback.value = res.feedback || ''
pruefMessages.value.push({ role: 'assistant', kind: 'feedback', content: res.feedback || '', bewertung: res.bewertung }) pruefMessages.value.push({ role: 'assistant', kind: 'feedback', content: res.feedback || '', bewertung: res.bewertung, geprueft: false })
letzteFeedbackMsg = pruefMessages.value[pruefMessages.value.length - 1]
pruefPhase.value = 'bewertet' pruefPhase.value = 'bewertet'
verdictPruefen() // im Hintergrund verbindlich prüfen lassen
} }
function antwortPayload() { function antwortPayload() {
@@ -206,6 +211,29 @@ function antwortPayload() {
} }
} }
// Hintergrund: voller Bewerter+Kritiker. Persistiert serverseitig; korrigiert die Anzeige bei Abweichung.
async function verdictPruefen() {
const meine = ++pruefenRun
const ziel = letzteFeedbackMsg
try {
const res = await pruefeBaustein({
topic: props.topic, baustein: props.baustein, section: props.section,
provider: props.provider, messages: pruefDialog(), ...antwortPayload(), aktion: 'antwort_pruefen',
})
if (meine !== pruefenRun) return // eine neuere Bewertung läuft → diese verwerfen
applyPruefung(res)
if (ziel) {
if (res.bewertung !== ziel.bewertung || (res.feedback && res.feedback !== ziel.content)) {
ziel.content = res.feedback || ziel.content
ziel.bewertung = res.bewertung
}
ziel.geprueft = true
}
} catch {
if (meine === pruefenRun && ziel) ziel.geprueft = true
}
}
function antwortAbgeben() { function antwortAbgeben() {
const text = pruefInput.value.trim() const text = pruefInput.value.trim()
if (!text || pruefLoading.value) return if (!text || pruefLoading.value) return
@@ -297,7 +325,7 @@ function neuBewerten() {
<div v-if="pruefMessages.length" ref="pruefMessagesEl" class="bp-messages" @scroll="onPruefScroll"> <div v-if="pruefMessages.length" ref="pruefMessagesEl" class="bp-messages" @scroll="onPruefScroll">
<template v-for="(m, i) in pruefMessages" :key="i"> <template v-for="(m, i) in pruefMessages" :key="i">
<div v-if="m.kind === 'feedback'" class="bp-feedback" :class="m.bewertung">{{ m.content }}</div> <div v-if="m.kind === 'feedback'" class="bp-feedback" :class="m.bewertung">{{ m.content }}<span v-if="!m.geprueft" class="bp-pruefend"> · wird geprüft</span></div>
<div v-else-if="m.kind === 'fehler'" class="bp-error">{{ m.content }}</div> <div v-else-if="m.kind === 'fehler'" class="bp-error">{{ m.content }}</div>
<div v-else-if="m.role === 'assistant'" class="bp-msg assistant markdown" v-html="renderMarkdown(m.content)"></div> <div v-else-if="m.role === 'assistant'" class="bp-msg assistant markdown" v-html="renderMarkdown(m.content)"></div>
<div v-else class="bp-msg user">{{ m.content }}</div> <div v-else class="bp-msg user">{{ m.content }}</div>
@@ -418,6 +446,7 @@ function neuBewerten() {
line-height: 1.4; line-height: 1.4;
border: 1px solid var(--border); border: 1px solid var(--border);
} }
.bp-pruefend { font-style: italic; opacity: 0.7; font-size: 0.92em; }
.bp-feedback.gut { background: var(--success-soft); border-color: var(--success-border); color: var(--success); } .bp-feedback.gut { background: var(--success-soft); border-color: var(--success-border); color: var(--success); }
.bp-feedback.schlecht { background: var(--danger-soft, #fee2e2); border-color: var(--danger-border, #f87171); color: var(--danger); } .bp-feedback.schlecht { background: var(--danger-soft, #fee2e2); border-color: var(--danger-border, #f87171); color: var(--danger); }