update
This commit is contained in:
132
backend/lernen.py
Normal file
132
backend/lernen.py
Normal file
@@ -0,0 +1,132 @@
|
||||
"""Baustein-Lernen: Vertiefung, Bausteinchat und Prüfung zu einzelnen Guide-Sections.
|
||||
|
||||
Alle Aufrufe sind interaktiv (stdout-Antwort, lane "interactive") und stateless —
|
||||
der Chat-/Prüfungs-Verlauf kommt vom Frontend, persistiert wird nur der
|
||||
Prüfungs-Zähler (DB) und die Vertiefung (DB).
|
||||
"""
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from agents import run_agent
|
||||
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 textkit import _norm_titel
|
||||
|
||||
log = logging.getLogger("creator.lernen")
|
||||
|
||||
NOETIG = 3 # gute Antworten bis "absolviert"
|
||||
VERTIEFUNG_TIMEOUT = 600
|
||||
CHAT_TIMEOUT = 240
|
||||
|
||||
|
||||
def _transcript(messages: list[dict]) -> str:
|
||||
return "\n".join(
|
||||
f"{'Nutzer' if m.get('role') == 'user' else 'Assistent'}: {m.get('content', '')}"
|
||||
for m in messages
|
||||
) or "(leer)"
|
||||
|
||||
|
||||
async def vertiefung_generieren(topic: str, baustein: str, section: str, provider: str = DEFAULT_PROVIDER) -> str | None:
|
||||
"""Ausführliche Fassung des Bausteins als Markdown · None bei Fehler."""
|
||||
try:
|
||||
prompt = _prompt(
|
||||
"Baustein-Vertiefung",
|
||||
topic=topic, baustein=baustein,
|
||||
section_block=section.strip() or "(keine Guide-Fassung übergeben)",
|
||||
)
|
||||
returncode, stdout, _ = await run_agent(
|
||||
"vertiefung-" + str(uuid.uuid4()), prompt, VERTIEFUNG_TIMEOUT,
|
||||
provider=provider, role="fast", capabilities="none", lane="interactive",
|
||||
)
|
||||
if returncode != 0:
|
||||
return None
|
||||
return stdout.strip() or None
|
||||
except Exception:
|
||||
log.warning("[%s] Vertiefung fehlgeschlagen (%s)", topic, baustein, exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
async def baustein_chat(topic: str, baustein: str, section: str, vertiefung: str | None, messages: list[dict], provider: str = DEFAULT_PROVIDER) -> str:
|
||||
try:
|
||||
prompt = _prompt(
|
||||
"Baustein-Chat",
|
||||
topic=topic, baustein=baustein,
|
||||
section_block=section.strip() or "(keine Guide-Fassung übergeben)",
|
||||
vertiefung_block=(vertiefung or "").strip() or "(keine)",
|
||||
transcript=_transcript(messages),
|
||||
)
|
||||
returncode, stdout, _ = await run_agent(
|
||||
"bausteinchat-" + str(uuid.uuid4()), prompt, CHAT_TIMEOUT,
|
||||
provider=provider, role="fast", capabilities="none", lane="interactive",
|
||||
)
|
||||
if returncode != 0:
|
||||
return "Entschuldigung, das hat nicht geklappt. Bitte versuche es erneut."
|
||||
reply = stdout.strip()
|
||||
return reply or "Entschuldigung, ich habe keine Antwort erhalten."
|
||||
except Exception:
|
||||
log.warning("[%s] Baustein-Chat fehlgeschlagen (%s)", topic, baustein, exc_info=True)
|
||||
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."""
|
||||
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):
|
||||
return None
|
||||
return {"reply": reply, "bewertung": bewertung, "bestanden": data.get("bestanden") is True}
|
||||
|
||||
|
||||
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."""
|
||||
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:
|
||||
return None
|
||||
return _pruefung_schema(_parse_json_text(stdout))
|
||||
except Exception:
|
||||
log.warning("[%s] Prüfung fehlgeschlagen (%s)", topic, baustein, exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
async def baustein_element_anlegen(topic: str, baustein: str, section: str, provider: str = DEFAULT_PROVIDER) -> None:
|
||||
"""Hintergrund-Task nach dem Absolvieren: Baustein als Element anlegen.
|
||||
|
||||
Dedup über normalisierte Titel — existiert schon ein Element zum Baustein,
|
||||
passiert nichts. Darf nie eine Exception nach außen werfen.
|
||||
"""
|
||||
try:
|
||||
vorhanden = {_norm_titel(e["title"]) for e in await list_elements(topic)}
|
||||
if _norm_titel(baustein) in vorhanden:
|
||||
return
|
||||
fields = await generate_element(topic, hint=baustein, provider=provider, extra_context=section)
|
||||
if _norm_titel(fields["title"]) in vorhanden:
|
||||
return
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
await create_element({"id": str(uuid.uuid4()), "topic": topic, **fields, "created_at": now, "updated_at": now})
|
||||
log.info("[%s] Baustein als Element angelegt: %s", topic, fields["title"])
|
||||
except Exception:
|
||||
log.warning("[%s] Element-Anlage nach Prüfung fehlgeschlagen (%s)", topic, baustein, exc_info=True)
|
||||
Reference in New Issue
Block a user