update
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user