This commit is contained in:
team3
2026-06-19 14:04:19 +02:00
parent 26e097dc5a
commit 0868a768a2
7 changed files with 219 additions and 157 deletions

View File

@@ -39,53 +39,41 @@ FRAGETYPEN = {
}
def score_berechnen(
score_vor_frage: int, gut: bool, streak: int, tier2: bool, tier3: bool,
absolviert: bool, verstanden: bool, gemeistert: bool,
) -> int:
"""Neuer Score nach einer Antwort · driftfrei (immer aus dem Basis-Score gerechnet).
# Antwort-Niveau (wie sehr der Kern getroffen ist) → Punkt-Delta.
NIVEAUS = {
"kaum": -1, # < 25 % richtig
"teilweise": 0, # 2549 %
"solide": 1, # 5074 %
"stark": 2, # 7599 %
"komplett": 3, # 100 %
}
Drei Stufen, freigeschaltet über Guide-Flags:
- Tier 1 (tier2=False): +1 bei richtig, KEINE Strafe, Deckel NOETIG (3).
- Tier 2 (tier2, nicht tier3): +1 / 1, Boden 3, Deckel MASTERY (10).
- Tier 3 (tier3, Meisterpfad): +1 / 2, Boden 10, Deckel MEISTERN (25).
Streak-Bonus bei richtig: ab 3 in Folge +2 (Tier 2/3), ab 5 in Folge +3 (Tier 3).
`streak` = bisherige Folge VOR dieser Antwort; mit dieser Antwort zählt streak+1.
Boden vor dem Absolvieren ist 0; gemeistert friert bei MEISTERN ein.
Re-Bewertung nutzt denselben Basis-Score + dieselbe Streak (kein Doppelzählen).
def score_berechnen(
score_vor_frage: int, niveau: str, nachgefragt: bool,
tier2: bool, tier3: bool, absolviert: bool, verstanden: bool, gemeistert: bool,
) -> int:
"""Neuer Score · driftfrei (immer aus dem Basis-Score). Delta aus dem Antwort-Niveau:
je besser, desto mehr (1/0/+1/+2/+3). Bei Nachfragen ist der Gewinn auf +1 gedeckelt.
Danach auf Boden/Deckel der aktiven Stufe geklemmt (3 / 10 / 25). Kein Streak mehr.
"""
if gemeistert:
return MEISTERN
# Stufe richtet sich nach dem Baustein-Fortschritt, nicht nur nach den
# Guide-Flags: ein frischer Baustein durchläuft erst Tier 1 (0→3), auch
# wenn der Guide schon tier2/tier3 freigibt. Tier 2 erst ab absolviert,
# Tier 3 erst ab verstanden.
# Stufe nach Baustein-Fortschritt: frisch erst Tier 1 (0→3), dann ab absolviert/verstanden.
tier2 = tier2 and absolviert
tier3 = tier3 and verstanden
if not tier2:
floor, cap, strafe = (NOETIG if absolviert else 0), NOETIG, 0
floor, cap = (NOETIG if absolviert else 0), NOETIG
elif not tier3:
floor, cap, strafe = NOETIG, MASTERY, -1
floor, cap = NOETIG, MASTERY
else:
floor, cap, strafe = MASTERY, MEISTERN, -2
if gut:
s = streak + 1
delta = 3 if (tier3 and s >= 5) else (2 if (tier2 and s >= 3) else 1)
else:
delta = strafe
floor, cap = MASTERY, MEISTERN
delta = NIVEAUS.get(niveau, 0)
if nachgefragt:
delta = min(delta, 1) # mit Hilfe (Nachfrage) höchstens +1
return max(floor, min(cap, score_vor_frage + delta))
def streak_berechnen(streak_vor: int, gut: bool, tier2: bool, tier3: bool, neu_absolviert: bool, neu_verstanden: bool) -> int:
"""Neue Streak nach einer Antwort · persistiert. Reset NUR am Deckel der aktiven Stufe:
Tier 1 → bei neu-absolviert (3), Tier 2 → bei neu-verstanden (10). Sonst läuft sie durch."""
if not gut:
return 0
if (not tier2 and neu_absolviert) or (tier2 and not tier3 and neu_verstanden):
return 0
return streak_vor + 1
def _transcript(messages: list[dict]) -> str:
return "\n".join(
f"{'Nutzer' if m.get('role') == 'user' else 'Assistent'}: {m.get('content', '')}"
@@ -124,14 +112,14 @@ def _frage_schema(data) -> dict | None:
def _bewertung_schema(data) -> dict | None:
"""{"feedback": str, "bewertung": "gut"|"schlecht", "bestanden": bool} · sonst None."""
"""{"feedback": str, "niveau": ∈ NIVEAUS} · sonst None."""
if not isinstance(data, dict):
return None
feedback = str(data.get("feedback", "")).strip()
bewertung = data.get("bewertung")
if not feedback or bewertung not in ("gut", "schlecht"):
niveau = data.get("niveau")
if not feedback or niveau not in NIVEAUS:
return None
return {"feedback": feedback, "bewertung": bewertung, "bestanden": data.get("bestanden") is True}
return {"feedback": feedback, "niveau": niveau}
async def _gen_call(name: str, role: str, schema, provider: str, timeout: int = PRUEFUNG_TIMEOUT, **kwargs) -> dict | None:
@@ -164,7 +152,7 @@ def _kritik_block(vorversion: str, probleme: list[str]) -> str:
def _bewertung_text(bew: dict) -> str:
return f"Bewertung: {bew['bewertung']}\nFeedback: {bew['feedback']}"
return f"Niveau: {bew['niveau']}\nFeedback: {bew['feedback']}"
# Deterministischer Guard gegen Doppelfragen — der KI-Kritiker übersieht „…, und welchen…".
@@ -215,12 +203,12 @@ async def _frage_mit_kritik(
async def _bewertung_mit_kritik(
topic: str, baustein: str, section_block: str, kompakt_block: str,
frage: str, transcript: str, gute_antworten: int, provider: str, role: str = "judge",
frage: str, transcript: str, begruendung_block: str, provider: str, role: str = "judge",
) -> dict | None:
"""Antwort bewerten (gut/schlecht), vom Kritiker prüfen lassen, bei Fehlurteil neu.
"""Antwort bewerten (Niveau), vom Kritiker prüfen lassen, bei Fehlurteil neu.
`frage` ankert die geprüfte Frage; der Dialog (transcript) liefert die Antwort und
eine etwaige Diskussion — so kann eine Re-Bewertung das Argument sehen.
`frage` ankert die geprüfte Frage; der Dialog (transcript) liefert Antwort + Diskussion.
`begruendung_block` = optionale Unzufriedenheit des Lerners (nur bei „Gründlich prüfen").
`role` = "judge" (schnell) oder "guide" (gründlich, starkes Modell mit Thinking).
"""
timeout = GRUENDLICH_TIMEOUT if role == "guide" else PRUEFUNG_TIMEOUT
@@ -231,7 +219,7 @@ async def _bewertung_mit_kritik(
"Baustein-Bewertung", role, _bewertung_schema, provider, timeout,
topic=topic, baustein=baustein, section_block=section_block,
kompakt_block=kompakt_block, frage=frage, transcript=transcript,
gute_antworten=gute_antworten, noetig=NOETIG, kritik_block=kritik_block,
begruendung_block=begruendung_block, kritik_block=kritik_block,
)
if bew is None:
return None
@@ -285,22 +273,42 @@ async def pruefung_frage(
return None
async def pruefung_bewertung_schnell(
topic: str, baustein: str, section: str, kompakt: str | None,
frage: str, messages: list[dict], provider: str = DEFAULT_PROVIDER,
) -> dict | None:
"""Aktion 'antwort' (Agent 1, schnell): nur Evaluator, kein Kritiker. → {feedback, niveau}."""
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,
begruendung_block="(keine)", kritik_block="(keine)",
)
except Exception:
log.warning("[%s] Schnell-Bewertung fehlgeschlagen (%s)", topic, baustein, exc_info=True)
return None
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,
role: str = "judge",
frage: str, messages: list[dict], provider: str = DEFAULT_PROVIDER,
role: str = "judge", begruendung: str = "",
) -> dict | None:
"""Aktion 'antwort_pruefen': verbindlich bewerten (Evaluator + Kritiker).
"""Aktion 'antwort_pruefen' (Agent 2, genau): Evaluator + Kritiker. → {feedback, niveau}.
`role` = "guide" für „Gründlich prüfen" (starkes Modell), sonst "judge".
Gibt {"feedback", "bewertung": gut|schlecht, "bestanden"} · None bei Fehler.
`role` = "guide" für „Gründlich prüfen" (starkes Modell). `begruendung` = optionale
Unzufriedenheit des Lerners mit einer früheren Bewertung.
"""
try:
section_block, kompakt_block = _bloecke(section, kompakt)
transcript = _transcript(messages) if messages else "(leer)"
begruendung_block = begruendung.strip() or "(keine)"
return await _bewertung_mit_kritik(
topic, baustein, section_block, kompakt_block,
frage.strip() or "(keine Frage übergeben)", transcript, gute_antworten, provider, role,
frage.strip() or "(keine Frage übergeben)", transcript, begruendung_block, provider, role,
)
except Exception:
log.warning("[%s] Bewertung fehlgeschlagen (%s)", topic, baustein, exc_info=True)