From 822f6ee3e9f6c5866402a94cb422aee953c9aa3d Mon Sep 17 00:00:00 2001 From: team3 Date: Sun, 14 Jun 2026 12:24:49 +0200 Subject: [PATCH] update --- backend/lernen.py | 153 +++++++++++++++--- backend/models.py | 3 +- backend/routes.py | 2 +- frontend/src/components/BausteinPanel.vue | 20 ++- frontend/src/composables/useChat.js | 8 +- templates/Prompt/Baustein-Bewertung-Kritik.md | 25 +++ templates/Prompt/Baustein-Bewertung.md | 29 ++++ templates/Prompt/Baustein-Frage-Kritik.md | 25 +++ templates/Prompt/Baustein-Frage.md | 30 ++++ templates/Prompt/Baustein-Pruefung.md | 30 ---- 10 files changed, 266 insertions(+), 59 deletions(-) create mode 100644 templates/Prompt/Baustein-Bewertung-Kritik.md create mode 100644 templates/Prompt/Baustein-Bewertung.md create mode 100644 templates/Prompt/Baustein-Frage-Kritik.md create mode 100644 templates/Prompt/Baustein-Frage.md delete mode 100644 templates/Prompt/Baustein-Pruefung.md diff --git a/backend/lernen.py b/backend/lernen.py index 6f49438..c2ae4f1 100644 --- a/backend/lernen.py +++ b/backend/lernen.py @@ -14,7 +14,7 @@ from config import DEFAULT_PROVIDER from database import create_element, list_elements from elements import generate_element from jsonio import parse_json_text as _parse_json_text -from pipeline import _prompt +from pipeline import _prompt, _probleme_schema from textkit import _norm_titel log = logging.getLogger("creator.lernen") @@ -22,6 +22,8 @@ log = logging.getLogger("creator.lernen") NOETIG = 3 # gute Antworten bis "absolviert" VERTIEFUNG_TIMEOUT = 600 CHAT_TIMEOUT = 240 +PRUEFUNG_TIMEOUT = 120 # kurze JSON-Turns; deckelt die Serien-Latenz pro Prüfungs-Schritt +KRITIK_MAX_RUNDEN = 2 # Generator → Kritiker → ggf. Neu, höchstens so oft def _transcript(messages: list[dict]) -> str: @@ -76,40 +78,143 @@ async def baustein_chat(topic: str, baustein: str, section: str, vertiefung: str return "Entschuldigung, das hat nicht geklappt. Bitte versuche es erneut." -def _pruefung_schema(data) -> dict | None: - """{"reply": str, "bewertung": "gut"|"schlecht"|None, "bestanden": bool} · sonst None.""" +def _frage_schema(data) -> dict | None: + """{"frage": str} · sonst None.""" if not isinstance(data, dict): return None - reply = str(data.get("reply", "")).strip() - bewertung = data.get("bewertung") - if not reply or bewertung not in ("gut", "schlecht", None): + frage = str(data.get("frage", "")).strip() + return {"frage": frage} if frage else None + + +def _bewertung_schema(data) -> dict | None: + """{"feedback": str, "bewertung": "gut"|"schlecht", "bestanden": bool} · sonst None.""" + if not isinstance(data, dict): return None - return {"reply": reply, "bewertung": bewertung, "bestanden": data.get("bestanden") is True} + feedback = str(data.get("feedback", "")).strip() + bewertung = data.get("bewertung") + if not feedback or bewertung not in ("gut", "schlecht"): + return 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: + """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, + 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 + 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", + ) + if returncode != 0: + return [] + return _probleme_schema(_parse_json_text(stdout)) or [] + + +def _kritik_block(vorversion: str, probleme: list[str]) -> str: + punkte = "\n".join(f"- {p}" for p in probleme) + return ( + f"Deine vorige Fassung war:\n«{vorversion}»\n\n" + f"Der Prüfer bemängelt:\n{punkte}\n\nBehebe diese Punkte." + ) + + +def _bewertung_text(bew: dict) -> str: + return f"Bewertung: {bew['bewertung']}\nFeedback: {bew['feedback']}" + + +async def _frage_mit_kritik( + topic: str, baustein: str, section_block: str, vertiefung_block: str, + transcript: str, provider: str, +) -> str | None: + """Frage generieren, vom Kritiker prüfen lassen, bei Mängeln neu (max KRITIK_MAX_RUNDEN).""" + kritik_block = "(keine)" + frage = None + for _ in range(KRITIK_MAX_RUNDEN): + data = await _gen_call( + "Baustein-Frage", "fast", _frage_schema, provider, + topic=topic, baustein=baustein, section_block=section_block, + vertiefung_block=vertiefung_block, transcript=transcript, kritik_block=kritik_block, + ) + if data is None: + return None + frage = data["frage"] + probleme = await _kritik_call( + "Baustein-Frage-Kritik", provider, + topic=topic, baustein=baustein, section_block=section_block, + vertiefung_block=vertiefung_block, transcript=transcript, frage=frage, + ) + if not probleme: + return frage + kritik_block = _kritik_block(frage, probleme) + return frage # best-effort nach der letzten Runde + + +async def _bewertung_mit_kritik( + topic: str, baustein: str, section_block: str, vertiefung_block: str, + transcript: str, gute_antworten: int, provider: str, +) -> dict | None: + """Letzte Antwort bewerten, vom Kritiker prüfen lassen, bei Fehlurteil neu.""" + kritik_block = "(keine)" + bew = None + for _ in range(KRITIK_MAX_RUNDEN): + bew = await _gen_call( + "Baustein-Bewertung", "judge", _bewertung_schema, provider, + topic=topic, baustein=baustein, section_block=section_block, + vertiefung_block=vertiefung_block, transcript=transcript, + gute_antworten=gute_antworten, noetig=NOETIG, kritik_block=kritik_block, + ) + if bew is None: + return None + probleme = await _kritik_call( + "Baustein-Bewertung-Kritik", provider, + topic=topic, baustein=baustein, section_block=section_block, + vertiefung_block=vertiefung_block, transcript=transcript, + bewertung_block=_bewertung_text(bew), + ) + if not probleme: + return bew + kritik_block = _kritik_block(_bewertung_text(bew), probleme) + return bew # best-effort nach der letzten Runde async def baustein_pruefung( topic: str, baustein: str, section: str, vertiefung: str | None, messages: list[dict], gute_antworten: int, provider: str = DEFAULT_PROVIDER, ) -> dict | None: - """Ein Prüfungs-Turn: Frage stellen bzw. letzte Antwort bewerten · None bei Fehler.""" + """Ein Prüfungs-Turn: erst (falls Antwort vorliegt) bewerten, dann nächste Frage. + + Generator + Kritiker-Loop je Schritt. Gibt + {"feedback", "frage", "bewertung", "bestanden"} zurück · None bei Fehler. + Bewertet wird nur, wenn der Verlauf mit einer Nutzer-Antwort endet — sonst + bekäme der Evaluator keine Antwort zum Beurteilen. + """ try: - prompt = _prompt( - "Baustein-Pruefung", - topic=topic, baustein=baustein, - section_block=section.strip() or "(keine Guide-Fassung übergeben)", - vertiefung_block=(vertiefung or "").strip() or "(keine)", - transcript=_transcript(messages) if messages else "(leer)", - gute_antworten=gute_antworten, noetig=NOETIG, - ) - # role "judge": Bewertungen brauchen das starke, kalte Modell — - # M2.7 hat in der Praxis gegen die eigene Referenz halluziniert. - returncode, stdout, _ = await run_agent( - "pruefung-" + str(uuid.uuid4()), prompt, CHAT_TIMEOUT, - provider=provider, role="judge", capabilities="none", lane="interactive", - ) - if returncode != 0: + section_block = section.strip() or "(keine Guide-Fassung übergeben)" + vertiefung_block = (vertiefung or "").strip() or "(keine)" + transcript = _transcript(messages) if messages else "(leer)" + bewerten = bool(messages) and messages[-1].get("role") == "user" + + feedback, bewertung, bestanden = None, None, False + if bewerten: + bew = await _bewertung_mit_kritik( + topic, baustein, section_block, vertiefung_block, transcript, gute_antworten, provider, + ) + if bew is None: + return None + feedback, bewertung, bestanden = bew["feedback"], bew["bewertung"], bew["bestanden"] + + frage = await _frage_mit_kritik(topic, baustein, section_block, vertiefung_block, transcript, provider) + if frage is None: return None - return _pruefung_schema(_parse_json_text(stdout)) + return {"feedback": feedback, "frage": frage, "bewertung": bewertung, "bestanden": bestanden} except Exception: log.warning("[%s] Prüfung fehlgeschlagen (%s)", topic, baustein, exc_info=True) return None diff --git a/backend/models.py b/backend/models.py index 3e7c942..6655072 100644 --- a/backend/models.py +++ b/backend/models.py @@ -196,7 +196,8 @@ class BausteinPruefungRequest(BaseModel): class BausteinPruefungResponse(BaseModel): - reply: str + feedback: str | None = None + frage: str bewertung: Literal["gut", "schlecht"] | None = None gute_antworten: int absolviert: bool diff --git a/backend/routes.py b/backend/routes.py index 363ccc7..3c0eb96 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -227,7 +227,7 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest): absolviert = True if frisch: asyncio.create_task(baustein_element_anlegen(req.topic, req.baustein, req.section, req.provider)) - return {"reply": data["reply"], "bewertung": data["bewertung"], "gute_antworten": gute, "absolviert": absolviert} + return {"feedback": data["feedback"], "frage": data["frage"], "bewertung": data["bewertung"], "gute_antworten": gute, "absolviert": absolviert} # --- Guides --- diff --git a/frontend/src/components/BausteinPanel.vue b/frontend/src/components/BausteinPanel.vue index a2539cd..3bf38ce 100644 --- a/frontend/src/components/BausteinPanel.vue +++ b/frontend/src/components/BausteinPanel.vue @@ -95,7 +95,7 @@ async function startPruefung() { topic: props.topic, baustein: props.baustein, section: props.section, messages: [], provider: props.provider, }) - pruefung.messages.value.push({ role: 'assistant', content: res.reply }) + pruefung.messages.value.push({ role: 'assistant', content: res.frage, feedback: null }) applyPruefung(res) nextTick(() => pruefung.inputEl.value?.focus()) } catch { @@ -175,7 +175,10 @@ async function startPruefung() {
Erste Frage kommt…
Bewertet…
@@ -260,6 +263,19 @@ async function startPruefung() { .bp-msg.assistant { align-self: flex-start; background: var(--panel); border: 1px solid var(--border); } .bp-typing { color: var(--text-faint); font-style: italic; } +/* Bewertung der letzten Antwort — getrennt über der nächsten Frage */ +.bp-feedback { + align-self: flex-start; + max-width: 88%; + padding: 0.3rem 0.6rem; + border-radius: 8px; + font-size: 0.82rem; + line-height: 1.4; + border: 1px solid var(--border); +} +.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-input { display: flex; gap: 0.4rem; margin-top: 0.55rem; align-items: flex-end; } .bp-input textarea { flex: 1; diff --git a/frontend/src/composables/useChat.js b/frontend/src/composables/useChat.js index 6b22541..85a23be 100644 --- a/frontend/src/composables/useChat.js +++ b/frontend/src/composables/useChat.js @@ -52,7 +52,13 @@ export function useChat(performRequest) { try { const res = await performRequest(messages.value) if (current !== run) return null - messages.value.push({ role: 'assistant', content: res.reply || '…' }) + // Prüfung liefert `frage` (+ getrenntes `feedback`); andere Chats `reply`. + messages.value.push({ + role: 'assistant', + content: res.frage ?? res.reply ?? '…', + feedback: res.feedback ?? null, + bewertung: res.bewertung ?? null, + }) return res } catch { if (current !== run) return null diff --git a/templates/Prompt/Baustein-Bewertung-Kritik.md b/templates/Prompt/Baustein-Bewertung-Kritik.md new file mode 100644 index 0000000..e4a00fc --- /dev/null +++ b/templates/Prompt/Baustein-Bewertung-Kritik.md @@ -0,0 +1,25 @@ +Du bist Qualitäts-Prüfer für Bewertungen in einer Prüfung zum Baustein "{baustein}" aus dem Lern-Guide zum Thema "{topic}". Ein anderer Agent hat die letzte Antwort des Lerners bewertet. Prüfe, ob die Bewertung fair und korrekt ist. + +BAUSTEIN AUS DEM GUIDE: +{section_block} + +VERTIEFUNG (falls vorhanden): +{vertiefung_block} + +PRÜFUNGS-VERLAUF (die letzte Nutzer-Antwort wurde bewertet): +{transcript} + +ZU PRÜFENDE BEWERTUNG: +{bewertung_block} + +PRÜFE GEGEN DIESE KRITERIEN: +- Konsistenz mit dem Material: Die Bewertung darf der Guide-Fassung und der Vertiefung NICHT widersprechen. +- Keine Halluzination: Das Feedback behauptet nichts, was nicht aus dem Material folgt. Es bestraft den Lerner nicht für etwas, das gar nicht gefragt war. +- Fairness: Eine knappe, fachlich richtige Antwort muss "gut" sein. Eine falsche oder abgelesene Antwort muss "schlecht" sein. +- Hat der Lerner mit Bezug aufs Material recht, muss die Bewertung "gut" sein — auch wenn sie der Frage-Annahme widerspricht. + +Beanstande NUR echte Fehlurteile. Ist die Bewertung fair und materialtreu, ist sie in Ordnung. + +Gib NUR JSON aus (kein weiterer Text): +- Bewertung in Ordnung: {{"ok": true}} +- Sonst: {{"probleme": ["was an der Bewertung falsch ist"]}} diff --git a/templates/Prompt/Baustein-Bewertung.md b/templates/Prompt/Baustein-Bewertung.md new file mode 100644 index 0000000..23230ae --- /dev/null +++ b/templates/Prompt/Baustein-Bewertung.md @@ -0,0 +1,29 @@ +Du bewertest die LETZTE Antwort eines Lerners in einer Prüfung zum Baustein "{baustein}" aus dem Lern-Guide zum Thema "{topic}". + +BAUSTEIN AUS DEM GUIDE: +{section_block} + +VERTIEFUNG (falls vorhanden): +{vertiefung_block} + +STAND: {gute_antworten} von {noetig} Antworten waren bisher gut. + +PRÜFUNGS-VERLAUF (die letzte Nutzer-Antwort bewertest du): +{transcript} + +FACHLICHE REFERENZ — WICHTIG: +- Die Guide-Fassung und die Vertiefung oben sind die fachliche Referenz. Deine Bewertung darf ihr NIE widersprechen. +- Behaupte nichts, was nicht aus dem Material folgt. Erfinde keine Zusatzannahmen. +- Widerspricht dir der Lerner mit Bezug aufs Material: Prüfe ZUERST deine eigene Annahme gegen die Referenz. Hat der Lerner recht, gib es offen zu und bewerte als "gut". + +SO BEWERTEST DU: +- "gut" = die Erklärung zeigt echtes Verständnis in eigenen Worten. Eine knappe, richtige Antwort reicht — verlange keine Vollständigkeit über die Frage hinaus. +- "schlecht" = falsch, oberflächlich, abgelesen oder bloße Wiederholung einer früheren Antwort. +- `feedback`: max. 1 Satz, sprich den Lerner direkt an. KEINE neue Frage. Begründe knapp, warum gut oder schlecht. +- `bestanden`: true NUR, wenn du schon vor den {noetig} guten Antworten sicher bist, dass der Lerner den Baustein verstanden hat. Im Zweifel false. + +HINWEISE DES PRÜFERS ZUR LETZTEN FASSUNG: +{kritik_block} + +Gib NUR dieses JSON aus (kein weiterer Text): +{{"feedback": "Bewertung in einem Satz", "bewertung": "gut" | "schlecht", "bestanden": false}} diff --git a/templates/Prompt/Baustein-Frage-Kritik.md b/templates/Prompt/Baustein-Frage-Kritik.md new file mode 100644 index 0000000..a0a75aa --- /dev/null +++ b/templates/Prompt/Baustein-Frage-Kritik.md @@ -0,0 +1,25 @@ +Du bist Qualitäts-Prüfer für Prüfungsfragen in einem Lern-Guide zum Thema "{topic}", Baustein "{baustein}". Ein anderer Agent hat eine Frage formuliert. Prüfe sie streng. + +BAUSTEIN AUS DEM GUIDE: +{section_block} + +VERTIEFUNG (falls vorhanden): +{vertiefung_block} + +BISHERIGER PRÜFUNGS-VERLAUF (nur frühere Fragen und Antworten): +{transcript} + +ZU PRÜFENDE FRAGE: +{frage} + +PRÜFE GEGEN DIESE KRITERIEN: +- Stil: GENAU EINE Frage, ein einziges Fragezeichen, eine einzige Sache. Kein Mehrteiler ("und"/"sowie", "sowohl … als auch …", "nenne drei …"). +- Kürze: maximal 1–2 Sätze, kein Szenario-Aufbau über mehrere Sätze, keine lange Vorrede. +- Keine Wiederholung einer Frage aus dem Verlauf. +- Fachlich korrekt: Die Frage muss aus dem Material oben beantwortbar sein und darf der Referenz NICHT widersprechen. Keine erfundenen Zusatzannahmen. + +Beanstande NUR echte Verstöße. Ist die Frage knapp, einzeln und korrekt, ist sie in Ordnung — verlange nichts darüber hinaus. + +Gib NUR JSON aus (kein weiterer Text): +- Alles in Ordnung: {{"ok": true}} +- Sonst: {{"probleme": ["kurzer Mangel 1", "kurzer Mangel 2"]}} diff --git a/templates/Prompt/Baustein-Frage.md b/templates/Prompt/Baustein-Frage.md new file mode 100644 index 0000000..829dc39 --- /dev/null +++ b/templates/Prompt/Baustein-Frage.md @@ -0,0 +1,30 @@ +Du bist Prüfer in einem Lern-Guide zum Thema "{topic}". Stelle dem Lerner EINE Verständnisfrage zum Baustein "{baustein}". Der Lerner sieht das Material — frage nach Verständnis und Transfer, nicht nach Abgelesenem. + +BAUSTEIN AUS DEM GUIDE: +{section_block} + +VERTIEFUNG (falls vorhanden): +{vertiefung_block} + +BISHERIGER PRÜFUNGS-VERLAUF (nur frühere Fragen und Antworten): +{transcript} + +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 …". +- 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. +- Passt eine Transferfrage nicht in einen Satz, wähle eine einfachere Frage. +- Wiederhole keine Frage aus dem Verlauf. + +FACHLICHE REFERENZ — WICHTIG: +- Die Guide-Fassung und die Vertiefung oben sind die Referenz. Deine Frage darf ihr NIE widersprechen. +- Erfinde keine Zusatzannahmen (z. B. fehlende Eingaben, geänderte Definitionen). Frag nur, was aus dem Material folgt. + +HINWEISE DES PRÜFERS ZUR LETZTEN FASSUNG: +{kritik_block} + +Sprich den Lerner direkt an, klares Deutsch, keine Floskeln. + +Gib NUR dieses JSON aus (kein weiterer Text): +{{"frage": "genau eine kurze Frage"}} diff --git a/templates/Prompt/Baustein-Pruefung.md b/templates/Prompt/Baustein-Pruefung.md deleted file mode 100644 index d5ebd66..0000000 --- a/templates/Prompt/Baustein-Pruefung.md +++ /dev/null @@ -1,30 +0,0 @@ -Du prüfst das Verständnis eines Lerners zum Baustein "{baustein}" aus dem Lern-Guide zum Thema "{topic}". Der Lerner sieht das Material während der Prüfung — stelle deshalb VERSTÄNDNIS- und TRANSFERFRAGEN, keine Reproduktionsfragen (nichts, was sich ablesen lässt). - -BAUSTEIN AUS DEM GUIDE: -{section_block} - -VERTIEFUNG (falls vorhanden): -{vertiefung_block} - -STAND: {gute_antworten} von {noetig} Antworten waren bisher gut. Bei {noetig} guten Antworten ist der Baustein absolviert. - -BISHERIGER PRÜFUNGS-VERLAUF: -{transcript} - -FACHLICHE REFERENZ — WICHTIG: -- Die Guide-Fassung und die Vertiefung oben sind die fachliche Referenz. Deine Fragen und Bewertungen dürfen ihnen NIE widersprechen. -- Behaupte nichts, was nicht aus dem Material folgt. Erfinde keine Zusatzannahmen (z. B. fehlende Eingaben, geänderte Definitionen). -- Widerspricht dir der Lerner mit Bezug aufs Material: Prüfe ZUERST deine eigene Annahme gegen die Referenz. Hat der Lerner recht, gib es offen zu und bewerte die Antwort als "gut". - -Deine Aufgabe: -- Ist der Verlauf leer: Stelle die erste Frage. `bewertung` ist dann null. -- Sonst: Bewerte die LETZTE Nutzer-Antwort als "gut" oder "schlecht". Gut = die Erklärung zeigt echtes Verständnis in eigenen Worten. Schlecht = falsch, oberflächlich, abgelesen oder eine bloße Wiederholung einer früheren Antwort. -- Gib Feedback (max. 1 Satz) und stelle die nächste Frage — beides zusammen in `reply`. -- Fragen sind KURZ: 1–2 Sätze, EINE Sache pro Frage. Keine Mehrteiler („nenne drei…", „sowohl … als auch …"), keine Aufzähl-Forderungen, kein Szenario-Aufbau über mehrere Sätze. -- Gute Fragen: Warum-Fragen, Anwendung auf ein neues Beispiel, Abgrenzung zu Nachbarkonzepten, Fehler in einem Beispiel finden, Konsequenzen erklären. -- Eine knappe, richtige Antwort ist „gut" — verlange keine Vollständigkeit über die Frage hinaus. -- `bestanden`: true NUR, wenn du schon vor Erreichen der {noetig} guten Antworten überzeugt bist, dass der Lerner den Baustein sicher verstanden hat. Im Zweifel false. -- Sprich den Lerner direkt an, klares Deutsch, keine Floskeln. - -Gib NUR dieses JSON aus (kein weiterer Text): -{{"reply": "Feedback und nächste Frage", "bewertung": "gut" | "schlecht" | null, "bestanden": false}}