This commit is contained in:
team3
2026-06-19 12:59:13 +02:00
parent 3168ec9094
commit 26e097dc5a
9 changed files with 107 additions and 199 deletions

View File

@@ -82,6 +82,17 @@ def _crawl_fertig(topic: str) -> bool:
_STUFEN = ("einfach", "mittel", "schwer") _STUFEN = ("einfach", "mittel", "schwer")
def subbausteine_titel(topic: str, baustein: str) -> list[str]:
"""Subbaustein-Titel eines Bausteins aus der Sidecar (leer, wenn keine)."""
sc = _json_datei(subbausteine_path(topic))
if not isinstance(sc, dict):
return []
return [
t for s in (sc.get(baustein) or [])
if isinstance(s, dict) and (t := str(s.get("titel", "")).strip())
]
def lade_uebersicht(topic: str) -> list[dict]: def lade_uebersicht(topic: str) -> list[dict]:
"""Strukturierte Baustein-Liste für die Übersicht: Titel + Beschreibung + Subbausteine/Stufen. """Strukturierte Baustein-Liste für die Übersicht: Titel + Beschreibung + Subbausteine/Stufen.

View File

@@ -5,8 +5,8 @@ der Chat-/Prüfungs-Verlauf kommt vom Frontend, persistiert wird nur der
Prüfungs-Zähler (DB) und die Vertiefung (DB). Prüfungs-Zähler (DB) und die Vertiefung (DB).
""" """
import asyncio
import logging import logging
import random
import re import re
import uuid import uuid
from datetime import datetime, timezone from datetime import datetime, timezone
@@ -29,6 +29,15 @@ PRUEFUNG_TIMEOUT = 120 # kurze JSON-Turns; deckelt die Serien-Latenz pro Prüfu
GRUENDLICH_TIMEOUT = 600 # „Gründlich prüfen": starkes Modell (role guide) braucht länger 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
# Fragetypen für Active Recall — pro Frage einer, zufällig gewählt. Schafft Vielfalt.
FRAGETYPEN = {
"abruf": "Free Recall: Lass den Lerner die Kernidee frei aus dem Kopf erklären (offene Verständnisfrage).",
"punkt": "Cued Recall: Frag gezielt EIN konkretes Detail oder eine Abgrenzung ab.",
"warum": "Warum-Frage: Frag nach dem Grund/Mechanismus — warum funktioniert oder gilt das so?",
"anwendung": "Anwendung: Lass das Konzept auf EIN kurzes, neues Beispiel/Szenario anwenden.",
"pruefen": "Bei Code-/Tool-Themen: kleinen Schnipsel zeigen — Output vorhersagen ODER den Fehler finden. Kein Code-Thema → stattdessen eine Anwendungsfrage.",
}
def score_berechnen( def score_berechnen(
score_vor_frage: int, gut: bool, streak: int, tier2: bool, tier3: bool, score_vor_frage: int, gut: bool, streak: int, tier2: bool, tier3: bool,
@@ -114,14 +123,6 @@ def _frage_schema(data) -> dict | None:
return {"frage": frage} if frage else None return {"frage": frage} if frage else None
def _fragen_schema(data) -> list[str] | None:
"""{"fragen": [str, …]} → Liste nicht-leerer Fragen · sonst None."""
if not isinstance(data, dict) or not isinstance(data.get("fragen"), list):
return None
fragen = [str(f).strip() for f in data["fragen"] if str(f).strip()]
return fragen or None
def _bewertung_schema(data) -> dict | None: def _bewertung_schema(data) -> dict | None:
"""{"feedback": str, "bewertung": "gut"|"schlecht", "bestanden": bool} · sonst None.""" """{"feedback": str, "bewertung": "gut"|"schlecht", "bestanden": bool} · sonst None."""
if not isinstance(data, dict): if not isinstance(data, dict):
@@ -182,7 +183,7 @@ def _doppelfrage_mangel(frage: str) -> str | None:
async def _frage_mit_kritik( async def _frage_mit_kritik(
topic: str, baustein: str, section_block: str, kompakt_block: str, topic: str, baustein: str, section_block: str, kompakt_block: str,
transcript: str, vermeide_block: str, provider: str, transcript: str, vermeide_block: str, typ_block: str, fokus_block: str, provider: str,
) -> str | None: ) -> str | None:
"""Frage generieren, vom Kritiker prüfen lassen, bei Mängeln neu (max KRITIK_MAX_RUNDEN).""" """Frage generieren, vom Kritiker prüfen lassen, bei Mängeln neu (max KRITIK_MAX_RUNDEN)."""
kritik_block = "(keine)" kritik_block = "(keine)"
@@ -192,7 +193,7 @@ async def _frage_mit_kritik(
"Baustein-Frage", "fast", _frage_schema, provider, "Baustein-Frage", "fast", _frage_schema, provider,
topic=topic, baustein=baustein, section_block=section_block, topic=topic, baustein=baustein, section_block=section_block,
kompakt_block=kompakt_block, transcript=transcript, vermeide_block=vermeide_block, kompakt_block=kompakt_block, transcript=transcript, vermeide_block=vermeide_block,
kritik_block=kritik_block, typ_block=typ_block, fokus_block=fokus_block, kritik_block=kritik_block,
) )
if data is None: if data is None:
return None return None
@@ -200,7 +201,8 @@ async def _frage_mit_kritik(
probleme = await _kritik_call( probleme = await _kritik_call(
"Baustein-Frage-Kritik", provider, role="guide", # starke KI prüft die Regeln "Baustein-Frage-Kritik", provider, role="guide", # starke KI prüft die Regeln
topic=topic, baustein=baustein, section_block=section_block, topic=topic, baustein=baustein, section_block=section_block,
kompakt_block=kompakt_block, transcript=transcript, vermeide_block=vermeide_block, frage=frage, kompakt_block=kompakt_block, transcript=transcript, vermeide_block=vermeide_block,
typ_block=typ_block, fokus_block=fokus_block, frage=frage,
) )
hart = _doppelfrage_mangel(frage) # erzwingt Neugenerierung, auch wenn der KI-Kritiker es übersah hart = _doppelfrage_mangel(frage) # erzwingt Neugenerierung, auch wenn der KI-Kritiker es übersah
if hart: if hart:
@@ -259,70 +261,30 @@ def _vermeide_block(vermeide: list[str] | None) -> str:
async def pruefung_frage( async def pruefung_frage(
topic: str, baustein: str, section: str, kompakt: str | None, topic: str, baustein: str, section: str, kompakt: str | None,
messages: list[dict], vermeide: list[str] | None = None, provider: str = DEFAULT_PROVIDER, messages: list[dict], subbausteine: list[str] | None = None,
vermeide: list[str] | None = None, provider: str = DEFAULT_PROVIDER,
) -> str | None: ) -> str | None:
"""Aktion 'frage': nächste Frage generieren (Generator + Kritiker) · None bei Fehler.""" """Aktion 'frage': eine Frage generieren — zufälliger Typ zu einem zufälligen Subbaustein,
dann Kritiker (Generator → Kritiker sequenziell) · None bei Fehler."""
try: try:
section_block, kompakt_block = _bloecke(section, kompakt) section_block, kompakt_block = _bloecke(section, kompakt)
transcript = _transcript(messages) if messages else "(leer)" transcript = _transcript(messages) if messages else "(leer)"
typ_block = FRAGETYPEN[random.choice(list(FRAGETYPEN))]
subs = [s for s in (subbausteine or []) if s and s.strip()]
fokus = random.choice(subs) if subs else ""
fokus_block = (
f"Konzentriere die Frage auf diesen Subbaustein: „{fokus}\"" if fokus
else "(ganzer Baustein — kein bestimmter Subbaustein)"
)
return await _frage_mit_kritik( return await _frage_mit_kritik(
topic, baustein, section_block, kompakt_block, transcript, _vermeide_block(vermeide), provider, topic, baustein, section_block, kompakt_block, transcript,
_vermeide_block(vermeide), typ_block, fokus_block, provider,
) )
except Exception: except Exception:
log.warning("[%s] Frage fehlgeschlagen (%s)", topic, baustein, exc_info=True) log.warning("[%s] Frage fehlgeschlagen (%s)", topic, baustein, exc_info=True)
return None return None
async def _frage_ok(topic, baustein, section_block, kompakt_block, transcript, vermeide_block, frage, provider) -> bool:
"""Eine geplante Frage prüfen: Doppelfrage-Guard + Kritiker. True = in Ordnung."""
if _doppelfrage_mangel(frage):
return False
probleme = await _kritik_call(
"Baustein-Frage-Kritik", provider, role="guide",
topic=topic, baustein=baustein, section_block=section_block,
kompakt_block=kompakt_block, transcript=transcript, vermeide_block=vermeide_block, frage=frage,
)
return not probleme
async def pruefung_fragen(
topic: str, baustein: str, section: str, kompakt: str | None,
messages: list[dict], vermeide: list[str] | None = None, anzahl: int = 5,
provider: str = DEFAULT_PROVIDER,
) -> list[str]:
"""Aktion 'fragen': EIN Planer entwirft `anzahl` verschiedene Fragen, parallel geprüft.
Diversität kommt vom Planer; der Kritiker prüft Qualität + Ähnlichkeit zu Gestelltem.
→ Liste gültiger Fragen (kann < anzahl sein)."""
try:
section_block, kompakt_block = _bloecke(section, kompakt)
transcript = _transcript(messages) if messages else "(leer)"
vermeide = [f for f in (vermeide or []) if f and f.strip()]
gesammelt: list[str] = []
for _ in range(2): # Plan + eine Nachrunde für Abgelehnte
brauche = anzahl - len(gesammelt)
if brauche <= 0:
break
vblock = _vermeide_block(vermeide + gesammelt)
kandidaten = await _gen_call(
"Baustein-Fragen-Plan", "fast", _fragen_schema, provider, CHAT_TIMEOUT,
topic=topic, baustein=baustein, section_block=section_block, kompakt_block=kompakt_block,
transcript=transcript, vermeide_block=vblock, anzahl=brauche,
)
if not kandidaten:
break
oks = await asyncio.gather(
*[_frage_ok(topic, baustein, section_block, kompakt_block, transcript, vblock, f, provider) for f in kandidaten],
return_exceptions=True,
)
for f, ok in zip(kandidaten, oks):
if ok is True and f not in gesammelt:
gesammelt.append(f)
return gesammelt[:anzahl]
except Exception:
log.warning("[%s] Fragen-Plan fehlgeschlagen (%s)", topic, baustein, exc_info=True)
return []
async def pruefung_bewertung( async def pruefung_bewertung(
topic: str, baustein: str, section: str, kompakt: str | None, topic: str, baustein: str, section: str, kompakt: 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,
@@ -345,28 +307,6 @@ async def pruefung_bewertung(
return None return None
async def pruefung_bewertung_schnell(
topic: str, baustein: str, section: str, kompakt: 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, kompakt_block = _bloecke(section, kompakt)
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,
kompakt_block=kompakt_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, kompakt: str | None, topic: str, baustein: str, section: str, kompakt: 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

@@ -214,11 +214,10 @@ class BausteinPruefungRequest(BaseModel):
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) # ausführliche Fassung section: str = Field(default="", max_length=20000) # ausführliche Fassung
section_kompakt: str = Field(default="", max_length=20000) # kompakte Fassung (Merksätze) section_kompakt: str = Field(default="", max_length=20000) # kompakte Fassung (Merksätze)
aktion: Literal["frage", "fragen", "diskussion", "antwort", "antwort_pruefen"] = "frage" aktion: Literal["frage", "diskussion", "antwort_pruefen"] = "frage"
frage: str = Field(default="", max_length=2000) # aktuell geprüfte Frage (für diskussion/antwort); Anker der Basis frage: str = Field(default="", max_length=2000) # aktuell geprüfte Frage (für diskussion/antwort); Anker der Basis
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)
vermeide: list[str] = [] # schon gestellte + vorgemerkte Fragen — sinngemäß nicht wiederholen vermeide: list[str] = [] # schon gestellte + vorgemerkte Fragen — sinngemäß nicht wiederholen
anzahl: int = 5 # aktion 'fragen': wie viele neue Fragen der Planer entwerfen soll
# Basis/Streak werden serverseitig je Frage geführt (offene_frage-Anker) — nicht mehr vom Client. # Basis/Streak werden serverseitig je Frage geführt (offene_frage-Anker) — nicht mehr vom Client.
tier2: bool = False # ganzer Guide absolviert (alle ≥3) → 1 bei falsch, Deckel 10 tier2: bool = False # ganzer Guide absolviert (alle ≥3) → 1 bei falsch, Deckel 10
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
@@ -229,7 +228,6 @@ class BausteinPruefungRequest(BaseModel):
class BausteinPruefungResponse(BaseModel): class BausteinPruefungResponse(BaseModel):
frage: str | None = None frage: str | None = None
fragen: list[str] | None = None # aktion 'fragen': geplante, geprüfte Fragen
reply: str | None = None reply: str | None = None
feedback: str | None = None feedback: str | None = None
bewertung: Literal["gut", "schlecht"] | None = None bewertung: Literal["gut", "schlecht"] | None = None

View File

@@ -16,9 +16,9 @@ from database import (
list_baustein_progress, get_baustein_progress, set_offene_frage, list_baustein_progress, get_baustein_progress, set_offene_frage,
set_baustein_score, set_baustein_streak, set_baustein_absolviert, set_baustein_verstanden, set_baustein_gemeistert, delete_baustein_daten, set_baustein_score, set_baustein_streak, set_baustein_absolviert, set_baustein_verstanden, set_baustein_gemeistert, delete_baustein_daten,
) )
from bausteine import generate_bausteine, cancel_bausteine, bausteine_status, active_bausteine, reset_bausteine, lade_quelle, lade_uebersicht from bausteine import generate_bausteine, cancel_bausteine, bausteine_status, active_bausteine, reset_bausteine, lade_quelle, lade_uebersicht, subbausteine_titel
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_bewertung_schnell, pruefung_frage, pruefung_fragen, score_berechnen, streak_berechnen from lernen import NOETIG, MASTERY, MEISTERN, baustein_chat, baustein_diskussion, baustein_element_anlegen, pruefung_bewertung, pruefung_frage, score_berechnen, streak_berechnen
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
@@ -268,15 +268,12 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest):
msgs = [m.model_dump() for m in req.messages] msgs = [m.model_dump() for m in req.messages]
if req.aktion == "frage": if req.aktion == "frage":
frage = await pruefung_frage(req.topic, req.baustein, req.section, kompakt, msgs, vermeide=req.vermeide, provider=req.provider) subs = subbausteine_titel(req.topic, req.baustein) # Fokus-Kandidaten (zufällig gewählt)
frage = await pruefung_frage(req.topic, req.baustein, req.section, kompakt, msgs, subbausteine=subs, vermeide=req.vermeide, provider=req.provider)
if frage is None: if frage is None:
raise HTTPException(502, "Frage fehlgeschlagen — bitte erneut versuchen") raise HTTPException(502, "Frage fehlgeschlagen — bitte erneut versuchen")
return {"frage": frage, "gute_antworten": gute, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert} return {"frage": frage, "gute_antworten": gute, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert}
if req.aktion == "fragen":
fragen = await pruefung_fragen(req.topic, req.baustein, req.section, kompakt, msgs, vermeide=req.vermeide, anzahl=req.anzahl, provider=req.provider)
return {"fragen": fragen, "gute_antworten": gute, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert}
if req.aktion == "diskussion": if req.aktion == "diskussion":
if not req.frage.strip(): if not req.frage.strip():
raise HTTPException(400, "Diskussion braucht eine laufende Frage") raise HTTPException(400, "Diskussion braucht eine laufende Frage")
@@ -288,27 +285,15 @@ 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"/"antwort_pruefen" — mindestens eine Nutzer-Antwort muss im Dialog stehen # aktion == "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": # Verbindlich, persistiert. Serialisiert je Baustein, damit die Basis immer den
# Vorschau: nur Bewerter, kein Kritiker, KEIN Persist. Basis serverseitig (best effort). # geprüften Vorgänger-Stand sieht (kein Stale-Overwrite). Punkte kommen NUR hier.
data = await pruefung_bewertung_schnell(
req.topic, req.baustein, req.section, kompakt, req.frage, msgs, gute, provider=req.provider,
)
if data is None:
raise HTTPException(502, "Bewertung fehlgeschlagen — bitte erneut versuchen")
basis, streak_basis, _ = _basis(stand, req.frage)
score = score_berechnen(basis, data["bewertung"] == "gut", streak_basis, req.tier2, req.tier3, absolviert, verstanden, gemeistert)
return {"feedback": data["feedback"], "bewertung": data["bewertung"], "gute_antworten": score,
"absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert}
# aktion == "antwort_pruefen": verbindlich, persistiert. Serialisiert je Baustein, damit
# die Basis immer den geprüften Vorgänger-Stand sieht (kein Stale-Overwrite).
async with _pruef_lock(req.topic, req.baustein): async with _pruef_lock(req.topic, req.baustein):
stand = await get_baustein_progress(req.topic, req.baustein) # nach evtl. Vorgänger-Persist neu lesen stand = await get_baustein_progress(req.topic, req.baustein) # nach evtl. Vorgänger-Persist neu lesen
absolviert = stand["absolviert"] is not None absolviert = stand["absolviert"] is not None

View File

@@ -77,13 +77,13 @@ export async function chatBaustein({ topic, baustein, section, section_kompakt =
export async function pruefeBaustein({ export async function pruefeBaustein({
topic, baustein, section, section_kompakt = '', provider, topic, baustein, section, section_kompakt = '', provider,
aktion = 'frage', frage = '', letzte_bewertung = '', vermeide = [], anzahl = 5, aktion = 'frage', frage = '', letzte_bewertung = '', vermeide = [],
tier2 = false, tier3 = false, messages = [], gruendlich = false, 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, section_kompakt, aktion, frage, letzte_bewertung, vermeide, anzahl, tier2, tier3, messages, provider, gruendlich }), body: JSON.stringify({ topic, baustein, section, section_kompakt, aktion, frage, letzte_bewertung, vermeide, tier2, tier3, messages, provider, gruendlich }),
}) })
return jsonOrThrow(res) return jsonOrThrow(res)
} }

View File

@@ -137,24 +137,27 @@ function vermeideListe() {
return [...gestellt, ...pool.value] return [...gestellt, ...pool.value]
} }
// Pool als BATCH auffüllen: EIN Planer entwirft die fehlenden Fragen auf einmal function frageEinzeln() {
// (verschieden by design), parallel geprüft → schnell und ohne ähnliche Dubletten. // Ein Agent → eine Frage (Backend wählt zufälligen Typ + Subbaustein, dann Kritiker).
function poolFuellen() { return pruefeBaustein({
if (inflight.value > 0) return
const brauche = POOL_ZIEL - pool.value.length
if (brauche <= 0 || poolMiss >= 3) return
inflight.value++
pruefeBaustein({
topic: props.topic, baustein: props.baustein, section: props.section, section_kompakt: props.sectionKompakt, topic: props.topic, baustein: props.baustein, section: props.section, section_kompakt: props.sectionKompakt,
provider: props.provider, aktion: 'fragen', anzahl: brauche, messages: pruefDialog(), vermeide: vermeideListe(), provider: props.provider, aktion: 'frage', messages: pruefDialog(), vermeide: vermeideListe(),
}) }).then((res) => res.frage)
.then((res) => { }
const neu = (res.fragen || []).filter((f) => f && !istBekannt(f))
if (neu.length) { pool.value.push(...neu); poolMiss = 0 } // Pool PARALLEL auffüllen: pro fehlender Frage ein eigener Agent gleichzeitig.
// Initial 5 parallel; nach Verbrauch genau die fehlende(n) nach.
function poolFuellen() {
while (pool.value.length + inflight.value < POOL_ZIEL && poolMiss < 3) {
inflight.value++
frageEinzeln()
.then((text) => {
if (text && !istBekannt(text)) { pool.value.push(text); poolMiss = 0 }
else poolMiss++ else poolMiss++
}) })
.catch(() => { poolMiss++ }) .catch(() => { poolMiss++ })
.finally(() => { inflight.value--; if (pool.value.length < POOL_ZIEL) poolFuellen() }) .finally(() => { inflight.value--; poolFuellen() })
}
} }
// Eine Frage aus dem Pool nehmen; ist er leer, auf die erste eintreffende warten. // Eine Frage aus dem Pool nehmen; ist er leer, auf die erste eintreffende warten.
@@ -211,47 +214,40 @@ function nachfragen() {
) )
} }
let letzteFeedbackMsg = null // Referenz auf die zuletzt gezeigte (provisorische) Bewertungs-Bubble let letzteFeedbackMsg = null // Referenz auf die zuletzt gezeigte Bewertungs-Bubble
let pruefenRun = 0 // nur die jüngste Hintergrund-Prüfung darf die Anzeige korrigieren let pruefenRun = 0 // nur die jüngste Bewertung darf die Anzeige setzen
function bewerten(res) { // Antwort verbindlich prüfen — der Prüfer (Bewerter + Kritiker) vergibt die Punkte.
letztesFeedback.value = res.feedback || '' // Während des Wartens zeigt das „…"-Bubble den Geprüft-Zustand. gruendlich=true:
pruefMessages.value.push({ role: 'assistant', kind: 'feedback', content: res.feedback || '', bewertung: res.bewertung, geprueft: false }) // starkes Modell, re-bewertet dieselbe Frage und aktualisiert die bestehende Bubble.
letzteFeedbackMsg = pruefMessages.value[pruefMessages.value.length - 1] async function bewertenLassen(gruendlich = false) {
pruefPhase.value = 'bewertet'
verdictPruefen() // im Hintergrund verbindlich prüfen lassen
}
function antwortPayload() {
// Basis/Streak führt der Server je Frage (frage = Anker). Frontend schickt sie nicht mehr.
return { aktion: 'antwort', frage: aktuelleFrage.value, tier2: props.tier2, tier3: props.tier3 }
}
// Hintergrund: voller Bewerter+Kritiker. Persistiert serverseitig; korrigiert die Anzeige bei Abweichung.
// 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 pruefStick.value = true
if (gruendlich) pruefLoading.value = true pruefLoading.value = true
pruefScroll()
try { try {
const res = await pruefeBaustein({ const res = await pruefeBaustein({
topic: props.topic, baustein: props.baustein, section: props.section, section_kompakt: props.sectionKompakt, topic: props.topic, baustein: props.baustein, section: props.section, section_kompakt: props.sectionKompakt,
provider: props.provider, messages: pruefDialog(), ...antwortPayload(), aktion: 'antwort_pruefen', gruendlich, provider: props.provider, messages: pruefDialog(),
aktion: 'antwort_pruefen', frage: aktuelleFrage.value, tier2: props.tier2, tier3: props.tier3, gruendlich,
}) })
// Server führt den Score (serialisiert, absolut) — IMMER übernehmen, auch wenn man if (meine !== pruefenRun) return
// schon bei der nächsten Frage ist. Sonst geht die Korrektur verloren.
applyPruefung(res) applyPruefung(res)
if (ziel) { letztesFeedback.value = res.feedback || ''
if (res.bewertung !== ziel.bewertung || (res.feedback && res.feedback !== ziel.content)) { if (gruendlich && letzteFeedbackMsg) {
ziel.content = res.feedback || ziel.content letzteFeedbackMsg.content = res.feedback || letzteFeedbackMsg.content
ziel.bewertung = res.bewertung letzteFeedbackMsg.bewertung = res.bewertung
} } else {
ziel.geprueft = true pruefMessages.value.push({ role: 'assistant', kind: 'feedback', content: res.feedback || '', bewertung: res.bewertung, geprueft: true })
letzteFeedbackMsg = pruefMessages.value[pruefMessages.value.length - 1]
pruefPhase.value = 'bewertet'
} }
pruefScroll()
nextTick(() => pruefInputEl.value?.focus())
} catch { } catch {
if (ziel) ziel.geprueft = true if (meine === pruefenRun) pruefMessages.value.push({ role: 'assistant', kind: 'fehler', content: 'Hat nicht geklappt — bitte erneut.' })
} finally { } finally {
if (gruendlich && meine === pruefenRun) pruefLoading.value = false if (meine === pruefenRun) pruefLoading.value = false
} }
} }
@@ -260,7 +256,7 @@ function antwortAbgeben() {
if (!text || pruefLoading.value) return if (!text || pruefLoading.value) return
pruefMessages.value.push({ role: 'user', kind: 'antwort', content: text }) pruefMessages.value.push({ role: 'user', kind: 'antwort', content: text })
pruefInput.value = '' pruefInput.value = ''
pruefSenden(antwortPayload(), bewerten) bewertenLassen(false)
} }
// Lief schon eine Diskussion? Dann darf der Verlauf direkt bewertet werden. // Lief schon eine Diskussion? Dann darf der Verlauf direkt bewertet werden.
@@ -269,17 +265,17 @@ const hatDiskussion = computed(() => pruefMessages.value.some((m) => m.kind ===
// „Verlauf bewerten": den bisherigen Dialog als Antwort werten (Feld wird ignoriert). // „Verlauf bewerten": den bisherigen Dialog als Antwort werten (Feld wird ignoriert).
function verlaufBewerten() { function verlaufBewerten() {
if (pruefLoading.value) return if (pruefLoading.value) return
pruefSenden(antwortPayload(), bewerten) bewertenLassen(false)
} }
// „Gründlich prüfen": dieselbe Antwort, aber vom starken Modell (role guide) verbindlich neu bewerten. // „Gründlich prüfen": dieselbe Antwort vom starken Modell (role guide) neu bewerten.
function gruendlichPruefen() { function gruendlichPruefen() {
if (pruefLoading.value) return if (pruefLoading.value) return
verdictPruefen(true) bewertenLassen(true)
} }
// Abbruch wie im Chat: Run-Counter hochzählen → laufendes Ergebnis (pruefSenden // Abbruch wie im Chat: Run-Counter hochzählen → laufendes Ergebnis (pruefSenden
// und verdictPruefen) wird verworfen, Buttons sofort frei. Agent läuft serverseitig aus. // und bewertenLassen) wird verworfen, Buttons sofort frei. Agent läuft serverseitig aus.
function pruefAbbrechen() { function pruefAbbrechen() {
if (!pruefLoading.value) return if (!pruefLoading.value) return
pruefRun++ pruefRun++
@@ -296,7 +292,6 @@ onMounted(() => {
if (props.mode !== 'full') return // Trigger-Modus (Liste): nur Tab-Leiste, keine Prüfungs-/Text-Logik if (props.mode !== 'full') return // Trigger-Modus (Liste): nur Tab-Leiste, keine Prüfungs-/Text-Logik
window.addEventListener('keydown', onWindowKey) window.addEventListener('keydown', onWindowKey)
window.addEventListener('keydown', onPruefKey) window.addEventListener('keydown', onPruefKey)
poolFuellen() // Pool schon beim Öffnen vorfüllen — keine Wartezeit bei der ersten Frage
}) })
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener('keydown', onWindowKey) window.removeEventListener('keydown', onWindowKey)

View File

@@ -12,6 +12,10 @@ BISHERIGER PRÜFUNGS-VERLAUF (nur frühere Fragen und Antworten):
BEREITS GESTELLT / SCHON VORGEMERKT (Frage darf keiner davon gleichen): BEREITS GESTELLT / SCHON VORGEMERKT (Frage darf keiner davon gleichen):
{vermeide_block} {vermeide_block}
GEWÜNSCHTER FRAGETYP / FOKUS (zur Orientierung):
{typ_block}
{fokus_block}
ZU PRÜFENDE FRAGE: ZU PRÜFENDE FRAGE:
{frage} {frage}

View File

@@ -12,6 +12,12 @@ BISHERIGER PRÜFUNGS-VERLAUF (nur frühere Fragen und Antworten):
BEREITS GESTELLT / SCHON VORGEMERKT — auch SINNGEMÄSS NICHT wiederholen: BEREITS GESTELLT / SCHON VORGEMERKT — auch SINNGEMÄSS NICHT wiederholen:
{vermeide_block} {vermeide_block}
GEWÜNSCHTER FRAGETYP:
{typ_block}
FOKUS:
{fokus_block}
HARTE REGELN FÜR DIE FRAGE — wichtiger als alles andere: HARTE REGELN FÜR DIE FRAGE — wichtiger als alles andere:
- GENAU EINE Frage. Ein einziges Fragezeichen. Eine einzige Sache. - GENAU EINE Frage. Ein einziges Fragezeichen. Eine einzige Sache.
- Maximal 12 Sätze. Kein Szenario-Aufbau, keine Vorrede, kein "Angenommen … und außerdem …". - Maximal 12 Sätze. Kein Szenario-Aufbau, keine Vorrede, kein "Angenommen … und außerdem …".

View File

@@ -1,31 +0,0 @@
Du bist Prüfer in einem Lern-Guide zum Thema "{topic}". Plane {anzahl} Verständnisfragen zum Baustein "{baustein}" — auf einmal, damit sie sich klar UNTERSCHEIDEN. Der Lerner sieht das Material; frage nach Verständnis und Transfer, nicht nach Abgelesenem.
BAUSTEIN AUS DEM GUIDE:
{section_block}
KOMPAKTE FASSUNG (Merksätze, falls vorhanden):
{kompakt_block}
BISHERIGER PRÜFUNGS-VERLAUF (nur frühere Fragen und Antworten):
{transcript}
BEREITS GESTELLT / SCHON VORGEMERKT — auch SINNGEMÄSS NICHT wiederholen:
{vermeide_block}
WICHTIG — VIELFALT:
- Die {anzahl} Fragen müssen verschiedene Aspekte/Subbausteine treffen. KEINE zwei Fragen zur gleichen Kernsache, auch nicht umformuliert.
- Decke möglichst unterschiedliche Subbausteine ab (einfach → mittel → schwer gemischt).
HARTE REGELN JE FRAGE — wichtiger als alles andere:
- GENAU EINE Frage. Ein einziges Fragezeichen. Eine einzige Sache.
- Maximal 12 Sätze. Kein Szenario-Aufbau, keine Vorrede, kein "Angenommen … und außerdem …".
- Verboten: zwei Fragen mit "und"/"sowie" verketten, "nenne drei …", "sowohl … als auch …", Aufzähl-Forderungen.
- Frag nach EINEM Gedanken: ein Warum, eine Konsequenz, eine Abgrenzung, die Anwendung auf EIN kurzes Beispiel, einen Fehler finden.
- KEIN Faktenabruf ("welche Daten…", "wie viele…", "was enthält…").
- Frag nur zu dem, was der Abschnitt wirklich erklärt — nicht zu am Rand Erwähntem.
- Fachlich korrekt: aus dem Material beantwortbar, der Referenz NIE widersprechen, keine erfundenen Zusatzannahmen.
Sprich den Lerner direkt an, klares Deutsch, keine Floskeln.
Gib NUR dieses JSON aus (kein weiterer Text), genau {anzahl} verschiedene Fragen:
{{"fragen": ["genau eine kurze Frage", "…"]}}