diff --git a/backend/bausteine.py b/backend/bausteine.py index 99e8136..ee5b5ae 100644 --- a/backend/bausteine.py +++ b/backend/bausteine.py @@ -82,6 +82,17 @@ def _crawl_fertig(topic: str) -> bool: _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]: """Strukturierte Baustein-Liste für die Übersicht: Titel + Beschreibung + Subbausteine/Stufen. diff --git a/backend/lernen.py b/backend/lernen.py index 918c4fb..9aa546a 100644 --- a/backend/lernen.py +++ b/backend/lernen.py @@ -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). """ -import asyncio import logging +import random import re import uuid 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 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( 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 -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: """{"feedback": str, "bewertung": "gut"|"schlecht", "bestanden": bool} · sonst None.""" if not isinstance(data, dict): @@ -182,7 +183,7 @@ def _doppelfrage_mangel(frage: str) -> str | None: async def _frage_mit_kritik( 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: """Frage generieren, vom Kritiker prüfen lassen, bei Mängeln neu (max KRITIK_MAX_RUNDEN).""" kritik_block = "(keine)" @@ -192,7 +193,7 @@ async def _frage_mit_kritik( "Baustein-Frage", "fast", _frage_schema, provider, topic=topic, baustein=baustein, section_block=section_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: return None @@ -200,7 +201,8 @@ async def _frage_mit_kritik( probleme = await _kritik_call( "Baustein-Frage-Kritik", provider, role="guide", # starke KI prüft die Regeln 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 if hart: @@ -259,70 +261,30 @@ def _vermeide_block(vermeide: list[str] | None) -> str: async def pruefung_frage( 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: - """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: section_block, kompakt_block = _bloecke(section, kompakt) 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( - 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: log.warning("[%s] Frage fehlgeschlagen (%s)", topic, baustein, exc_info=True) 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( topic: str, baustein: str, section: str, kompakt: str | None, frage: str, messages: list[dict], gute_antworten: int, provider: str = DEFAULT_PROVIDER, @@ -345,28 +307,6 @@ async def pruefung_bewertung( 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( topic: str, baustein: str, section: str, kompakt: str | None, frage: str, letzte_bewertung: str | None, messages: list[dict], provider: str = DEFAULT_PROVIDER, diff --git a/backend/models.py b/backend/models.py index 1b3ea64..dadad3c 100644 --- a/backend/models.py +++ b/backend/models.py @@ -214,11 +214,10 @@ class BausteinPruefungRequest(BaseModel): baustein: str = Field(min_length=1, max_length=200) section: str = Field(default="", max_length=20000) # ausführliche Fassung 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 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 - 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. 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 @@ -229,7 +228,6 @@ class BausteinPruefungRequest(BaseModel): class BausteinPruefungResponse(BaseModel): frage: str | None = None - fragen: list[str] | None = None # aktion 'fragen': geplante, geprüfte Fragen reply: str | None = None feedback: str | None = None bewertung: Literal["gut", "schlecht"] | None = None diff --git a/backend/routes.py b/backend/routes.py index 736f2ad..9054b12 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -16,9 +16,9 @@ from database import ( 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, ) -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 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 pipeline import cancel_guide 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] 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: raise HTTPException(502, "Frage fehlgeschlagen — bitte erneut versuchen") 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 not req.frage.strip(): 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") 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). if not any(m.get("role") == "user" for m in msgs): raise HTTPException(400, "Antwort braucht eine Nutzer-Antwort") if not req.frage.strip(): raise HTTPException(400, "Antwort braucht eine laufende Frage") - if req.aktion == "antwort": - # Vorschau: nur Bewerter, kein Kritiker, KEIN Persist. Basis serverseitig (best effort). - 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). + # Verbindlich, persistiert. Serialisiert je Baustein, damit die Basis immer den + # geprüften Vorgänger-Stand sieht (kein Stale-Overwrite). Punkte kommen NUR hier. async with _pruef_lock(req.topic, req.baustein): stand = await get_baustein_progress(req.topic, req.baustein) # nach evtl. Vorgänger-Persist neu lesen absolviert = stand["absolviert"] is not None diff --git a/frontend/src/api.js b/frontend/src/api.js index fbe3915..9b355a5 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -77,13 +77,13 @@ export async function chatBaustein({ topic, baustein, section, section_kompakt = export async function pruefeBaustein({ 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, }) { const res = await fetch(`${BASE}/bausteine/pruefung`, { method: 'POST', 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) } diff --git a/frontend/src/components/BausteinPanel.vue b/frontend/src/components/BausteinPanel.vue index 83696c4..0638281 100644 --- a/frontend/src/components/BausteinPanel.vue +++ b/frontend/src/components/BausteinPanel.vue @@ -137,24 +137,27 @@ function vermeideListe() { return [...gestellt, ...pool.value] } -// Pool als BATCH auffüllen: EIN Planer entwirft die fehlenden Fragen auf einmal -// (verschieden by design), parallel geprüft → schnell und ohne ähnliche Dubletten. -function poolFuellen() { - if (inflight.value > 0) return - const brauche = POOL_ZIEL - pool.value.length - if (brauche <= 0 || poolMiss >= 3) return - inflight.value++ - pruefeBaustein({ +function frageEinzeln() { + // Ein Agent → eine Frage (Backend wählt zufälligen Typ + Subbaustein, dann Kritiker). + return pruefeBaustein({ topic: props.topic, baustein: props.baustein, section: props.section, section_kompakt: props.sectionKompakt, - provider: props.provider, aktion: 'fragen', anzahl: brauche, messages: pruefDialog(), vermeide: vermeideListe(), - }) - .then((res) => { - const neu = (res.fragen || []).filter((f) => f && !istBekannt(f)) - if (neu.length) { pool.value.push(...neu); poolMiss = 0 } - else poolMiss++ - }) - .catch(() => { poolMiss++ }) - .finally(() => { inflight.value--; if (pool.value.length < POOL_ZIEL) poolFuellen() }) + provider: props.provider, aktion: 'frage', messages: pruefDialog(), vermeide: vermeideListe(), + }).then((res) => res.frage) +} + +// 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++ + }) + .catch(() => { poolMiss++ }) + .finally(() => { inflight.value--; poolFuellen() }) + } } // 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 pruefenRun = 0 // nur die jüngste Hintergrund-Prüfung darf die Anzeige korrigieren +let letzteFeedbackMsg = null // Referenz auf die zuletzt gezeigte Bewertungs-Bubble +let pruefenRun = 0 // nur die jüngste Bewertung darf die Anzeige setzen -function bewerten(res) { - letztesFeedback.value = res.feedback || '' - 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' - 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) { +// Antwort verbindlich prüfen — der Prüfer (Bewerter + Kritiker) vergibt die Punkte. +// Während des Wartens zeigt das „…"-Bubble den Geprüft-Zustand. gruendlich=true: +// starkes Modell, re-bewertet dieselbe Frage und aktualisiert die bestehende Bubble. +async function bewertenLassen(gruendlich = false) { const meine = ++pruefenRun - const ziel = letzteFeedbackMsg - if (gruendlich) pruefLoading.value = true + pruefStick.value = true + pruefLoading.value = true + pruefScroll() try { const res = await pruefeBaustein({ 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 - // schon bei der nächsten Frage ist. Sonst geht die Korrektur verloren. + if (meine !== pruefenRun) return 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 + letztesFeedback.value = res.feedback || '' + if (gruendlich && letzteFeedbackMsg) { + letzteFeedbackMsg.content = res.feedback || letzteFeedbackMsg.content + letzteFeedbackMsg.bewertung = res.bewertung + } else { + 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 { - if (ziel) ziel.geprueft = true + if (meine === pruefenRun) pruefMessages.value.push({ role: 'assistant', kind: 'fehler', content: 'Hat nicht geklappt — bitte erneut.' }) } 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 pruefMessages.value.push({ role: 'user', kind: 'antwort', content: text }) pruefInput.value = '' - pruefSenden(antwortPayload(), bewerten) + bewertenLassen(false) } // 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). function verlaufBewerten() { 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() { if (pruefLoading.value) return - verdictPruefen(true) + bewertenLassen(true) } // 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() { if (!pruefLoading.value) return pruefRun++ @@ -296,7 +292,6 @@ onMounted(() => { if (props.mode !== 'full') return // Trigger-Modus (Liste): nur Tab-Leiste, keine Prüfungs-/Text-Logik window.addEventListener('keydown', onWindowKey) window.addEventListener('keydown', onPruefKey) - poolFuellen() // Pool schon beim Öffnen vorfüllen — keine Wartezeit bei der ersten Frage }) onUnmounted(() => { window.removeEventListener('keydown', onWindowKey) diff --git a/templates/Prompt/Baustein-Frage-Kritik.md b/templates/Prompt/Baustein-Frage-Kritik.md index 791ebe4..569e14b 100644 --- a/templates/Prompt/Baustein-Frage-Kritik.md +++ b/templates/Prompt/Baustein-Frage-Kritik.md @@ -12,6 +12,10 @@ BISHERIGER PRÜFUNGS-VERLAUF (nur frühere Fragen und Antworten): BEREITS GESTELLT / SCHON VORGEMERKT (Frage darf keiner davon gleichen): {vermeide_block} +GEWÜNSCHTER FRAGETYP / FOKUS (zur Orientierung): +{typ_block} +{fokus_block} + ZU PRÜFENDE FRAGE: {frage} diff --git a/templates/Prompt/Baustein-Frage.md b/templates/Prompt/Baustein-Frage.md index 9c486fc..eeea7c3 100644 --- a/templates/Prompt/Baustein-Frage.md +++ b/templates/Prompt/Baustein-Frage.md @@ -12,6 +12,12 @@ BISHERIGER PRÜFUNGS-VERLAUF (nur frühere Fragen und Antworten): BEREITS GESTELLT / SCHON VORGEMERKT — auch SINNGEMÄSS NICHT wiederholen: {vermeide_block} +GEWÜNSCHTER FRAGETYP: +{typ_block} + +FOKUS: +{fokus_block} + HARTE REGELN FÜR DIE FRAGE — wichtiger als alles andere: - GENAU EINE Frage. Ein einziges Fragezeichen. Eine einzige Sache. - Maximal 1–2 Sätze. Kein Szenario-Aufbau, keine Vorrede, kein "Angenommen … und außerdem …". diff --git a/templates/Prompt/Baustein-Fragen-Plan.md b/templates/Prompt/Baustein-Fragen-Plan.md deleted file mode 100644 index 20aab22..0000000 --- a/templates/Prompt/Baustein-Fragen-Plan.md +++ /dev/null @@ -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 1–2 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", "…"]}}