This commit is contained in:
team3
2026-06-14 12:24:49 +02:00
parent 8382d6f27a
commit 822f6ee3e9
10 changed files with 266 additions and 59 deletions

View File

@@ -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