update
This commit is contained in:
@@ -10,6 +10,17 @@ UNI_DIR = PROJECT_ROOT / "uni"
|
||||
|
||||
MAX_CONCURRENT_GENERATIONS = 10
|
||||
|
||||
# Lesbarkeits-Gate: deterministischer Prüfer (kleines deutsches Komplexitäts-Modell,
|
||||
# Skala 1–7). Zu schwere Sections gehen in die Lese-Prüfungs-Überarbeitung.
|
||||
# Fehlen transformers/torch oder das Modell → Gate stumm aus.
|
||||
LESBARKEIT_AKTIV = True
|
||||
LESBARKEIT_MODELL = "MiriUll/distilbert-german-text-complexity"
|
||||
# Anker auf der 1–7-Skala (TextComplexityDE): Leichte Sprache ~1,2; Wikipedia-Schnitt
|
||||
# ~3,22; ab MOS > 4 gilt ein Satz als „echt komplex" (Vereinfachungs-Grenze des Papers).
|
||||
LESBARKEIT_MAX = 3.5 # Section zu schwer, wenn der Satz-Schnitt darüber liegt
|
||||
LESBARKEIT_HART = 4.0 # Einzelsatz ab hier „hart"
|
||||
LESBARKEIT_HART_ANTEIL = 0.30 # … ODER wenn dieser Anteil der Sätze hart ist
|
||||
|
||||
# Deckel für gleichzeitige CLI-Agenten-Prozesse (über alle Generierungen hinweg).
|
||||
# Eigene Spur für interaktive Aufrufe (Chat, Elemente), damit sie nicht hinter
|
||||
# laufenden Writern in der Warteschlange hängen.
|
||||
|
||||
@@ -312,39 +312,6 @@ def _now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
async def get_vertiefung(topic: str, baustein: str, art: str) -> str | None:
|
||||
db = await get_db()
|
||||
cursor = await db.execute(
|
||||
"SELECT md FROM baustein_texte WHERE topic = ? AND baustein = ? AND art = ?",
|
||||
(topic, baustein, art),
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
return row[0] if row else None
|
||||
|
||||
|
||||
async def set_vertiefung(topic: str, baustein: str, art: str, md: str) -> None:
|
||||
db = await get_db()
|
||||
now = _now()
|
||||
await db.execute(
|
||||
"""INSERT INTO baustein_texte (topic, baustein, art, md, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(topic, baustein, art) DO UPDATE SET md = excluded.md, updated_at = excluded.updated_at""",
|
||||
(topic, baustein, art, md, now, now),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def list_vertiefungen(topic: str) -> dict[str, set[str]]:
|
||||
"""Baustein-Titel → vorhandene Text-Arten ('vertiefung'/'deepdive')."""
|
||||
db = await get_db()
|
||||
cursor = await db.execute("SELECT baustein, art FROM baustein_texte WHERE topic = ?", (topic,))
|
||||
rows = await cursor.fetchall()
|
||||
out: dict[str, set[str]] = {}
|
||||
for baustein, art in rows:
|
||||
out.setdefault(baustein, set()).add(art)
|
||||
return out
|
||||
|
||||
|
||||
async def list_baustein_progress(topic: str) -> list[dict]:
|
||||
db = await get_db()
|
||||
cursor = await db.execute(
|
||||
|
||||
@@ -18,8 +18,9 @@ from agents import run_agent
|
||||
from bausteine import _pdfs_konvertieren, quelle_ordner
|
||||
from config import (
|
||||
DEFAULT_PROVIDER, FORMAT_ANTEIL, KONSENS_GRACE, KONSENS_MAX_RUNDEN,
|
||||
TEMPLATES_DIR,
|
||||
LESBARKEIT_AKTIV, TEMPLATES_DIR,
|
||||
)
|
||||
import lesbarkeit
|
||||
from database import list_guides, update_guide
|
||||
from fsutil import atomic_write_json
|
||||
from jsonio import read_json_file as _json_datei
|
||||
@@ -709,6 +710,17 @@ async def _generate_sections(
|
||||
num = _titel_aufloesen(idx, item["section"])
|
||||
if num in geltung and num in by_num and num not in probleme_by_num:
|
||||
probleme_by_num[num] = item["problem"]
|
||||
|
||||
# Deterministisches Lesbarkeits-Gate: zu schwere Sections in dieselbe
|
||||
# Überarbeitung einreihen (LLM-Beanstandung hat Vorrang). Gate aus → no-op.
|
||||
if LESBARKEIT_AKTIV:
|
||||
md_by_num = {num: by_num[num]["md"] for nums in scope for num in nums if num in by_num}
|
||||
hinweise = await asyncio.to_thread(lesbarkeit.bewerte_sections, md_by_num)
|
||||
if hinweise:
|
||||
_log(topic, f"Lesbarkeit: {len(hinweise)} Section(s) zu schwer")
|
||||
for num, hinweis in hinweise.items():
|
||||
probleme_by_num.setdefault(num, hinweis)
|
||||
|
||||
if not probleme_by_num:
|
||||
break
|
||||
|
||||
@@ -756,7 +768,8 @@ async def _generate_sections(
|
||||
chapters: list[dict] = []
|
||||
for ch in plan:
|
||||
sections = [
|
||||
{"num": num, "title": _titel(entries[num]), "md": by_num[num]["md"]}
|
||||
{"num": num, "title": _titel(entries[num]), "md": by_num[num]["md"],
|
||||
"kompakt": by_num[num].get("kompakt", "")}
|
||||
for num in ch["nums"] if num in by_num
|
||||
]
|
||||
if sections:
|
||||
|
||||
@@ -23,7 +23,6 @@ log = logging.getLogger("creator.lernen")
|
||||
NOETIG = 3 # gute Antworten bis "absolviert" (Tier 1)
|
||||
MASTERY = 10 # Score bis "verstanden" (Tier 2)
|
||||
MEISTERN = 25 # Score bis "gemeistert" (Tier 3, Maximum)
|
||||
VERTIEFUNG_TIMEOUT = 600
|
||||
CHAT_TIMEOUT = 240
|
||||
PRUEFUNG_TIMEOUT = 120 # kurze JSON-Turns; deckelt die Serien-Latenz pro Prüfungs-Schritt
|
||||
GRUENDLICH_TIMEOUT = 600 # „Gründlich prüfen": starkes Modell (role guide) braucht länger
|
||||
@@ -84,37 +83,13 @@ def _transcript(messages: list[dict]) -> str:
|
||||
) or "(leer)"
|
||||
|
||||
|
||||
async def vertiefung_generieren(topic: str, baustein: str, section: str, art: str = "vertiefung", provider: str = DEFAULT_PROVIDER) -> str | None:
|
||||
"""Ausführlichere Fassung des Bausteins als Markdown · None bei Fehler.
|
||||
|
||||
art "vertiefung" = gleicher Stoff, nur umfangreicher;
|
||||
"deepdive" = Label „Amateur": gleicher Stoff, für Einsteiger aufbereitet.
|
||||
"""
|
||||
try:
|
||||
prompt = _prompt(
|
||||
"Baustein-Deepdive" if art == "deepdive" else "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:
|
||||
async def baustein_chat(topic: str, baustein: str, section: str, kompakt: 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)",
|
||||
kompakt_block=(kompakt or "").strip() or "(keine)",
|
||||
transcript=_transcript(messages),
|
||||
)
|
||||
returncode, stdout, _ = await run_agent(
|
||||
@@ -197,7 +172,7 @@ def _doppelfrage_mangel(frage: str) -> str | None:
|
||||
|
||||
|
||||
async def _frage_mit_kritik(
|
||||
topic: str, baustein: str, section_block: str, vertiefung_block: str,
|
||||
topic: str, baustein: str, section_block: str, kompakt_block: str,
|
||||
transcript: str, provider: str,
|
||||
) -> str | None:
|
||||
"""Frage generieren, vom Kritiker prüfen lassen, bei Mängeln neu (max KRITIK_MAX_RUNDEN)."""
|
||||
@@ -207,7 +182,7 @@ async def _frage_mit_kritik(
|
||||
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,
|
||||
kompakt_block=kompakt_block, transcript=transcript, kritik_block=kritik_block,
|
||||
)
|
||||
if data is None:
|
||||
return None
|
||||
@@ -215,7 +190,7 @@ 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,
|
||||
vertiefung_block=vertiefung_block, transcript=transcript, frage=frage,
|
||||
kompakt_block=kompakt_block, transcript=transcript, frage=frage,
|
||||
)
|
||||
hart = _doppelfrage_mangel(frage) # erzwingt Neugenerierung, auch wenn der KI-Kritiker es übersah
|
||||
if hart:
|
||||
@@ -227,7 +202,7 @@ async def _frage_mit_kritik(
|
||||
|
||||
|
||||
async def _bewertung_mit_kritik(
|
||||
topic: str, baustein: str, section_block: str, vertiefung_block: str,
|
||||
topic: str, baustein: str, section_block: str, kompakt_block: str,
|
||||
frage: str, transcript: str, gute_antworten: int, provider: str, role: str = "judge",
|
||||
) -> dict | None:
|
||||
"""Antwort bewerten (gut/schlecht), vom Kritiker prüfen lassen, bei Fehlurteil neu.
|
||||
@@ -243,7 +218,7 @@ async def _bewertung_mit_kritik(
|
||||
bew = await _gen_call(
|
||||
"Baustein-Bewertung", role, _bewertung_schema, provider, timeout,
|
||||
topic=topic, baustein=baustein, section_block=section_block,
|
||||
vertiefung_block=vertiefung_block, frage=frage, transcript=transcript,
|
||||
kompakt_block=kompakt_block, frage=frage, transcript=transcript,
|
||||
gute_antworten=gute_antworten, noetig=NOETIG, kritik_block=kritik_block,
|
||||
)
|
||||
if bew is None:
|
||||
@@ -251,7 +226,7 @@ async def _bewertung_mit_kritik(
|
||||
probleme = await _kritik_call(
|
||||
"Baustein-Bewertung-Kritik", provider, role=role, timeout=timeout,
|
||||
topic=topic, baustein=baustein, section_block=section_block,
|
||||
vertiefung_block=vertiefung_block, frage=frage, transcript=transcript,
|
||||
kompakt_block=kompakt_block, frage=frage, transcript=transcript,
|
||||
bewertung_block=_bewertung_text(bew),
|
||||
)
|
||||
if not probleme:
|
||||
@@ -260,29 +235,29 @@ async def _bewertung_mit_kritik(
|
||||
return bew # best-effort nach der letzten Runde
|
||||
|
||||
|
||||
def _bloecke(section: str, vertiefung: str | None) -> tuple[str, str]:
|
||||
def _bloecke(section: str, kompakt: str | None) -> tuple[str, str]:
|
||||
return (
|
||||
section.strip() or "(keine Guide-Fassung übergeben)",
|
||||
(vertiefung or "").strip() or "(keine)",
|
||||
(kompakt or "").strip() or "(keine)",
|
||||
)
|
||||
|
||||
|
||||
async def pruefung_frage(
|
||||
topic: str, baustein: str, section: str, vertiefung: str | None,
|
||||
topic: str, baustein: str, section: str, kompakt: str | None,
|
||||
messages: list[dict], provider: str = DEFAULT_PROVIDER,
|
||||
) -> str | None:
|
||||
"""Aktion 'frage': nächste Frage generieren (Generator + Kritiker) · None bei Fehler."""
|
||||
try:
|
||||
section_block, vertiefung_block = _bloecke(section, vertiefung)
|
||||
section_block, kompakt_block = _bloecke(section, kompakt)
|
||||
transcript = _transcript(messages) if messages else "(leer)"
|
||||
return await _frage_mit_kritik(topic, baustein, section_block, vertiefung_block, transcript, provider)
|
||||
return await _frage_mit_kritik(topic, baustein, section_block, kompakt_block, transcript, provider)
|
||||
except Exception:
|
||||
log.warning("[%s] Frage fehlgeschlagen (%s)", topic, baustein, exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
async def pruefung_bewertung(
|
||||
topic: str, baustein: str, section: str, vertiefung: str | None,
|
||||
topic: str, baustein: str, section: str, kompakt: str | None,
|
||||
frage: str, messages: list[dict], gute_antworten: int, provider: str = DEFAULT_PROVIDER,
|
||||
role: str = "judge",
|
||||
) -> dict | None:
|
||||
@@ -292,10 +267,10 @@ async def pruefung_bewertung(
|
||||
Gibt {"feedback", "bewertung": gut|schlecht, "bestanden"} · None bei Fehler.
|
||||
"""
|
||||
try:
|
||||
section_block, vertiefung_block = _bloecke(section, vertiefung)
|
||||
section_block, kompakt_block = _bloecke(section, kompakt)
|
||||
transcript = _transcript(messages) if messages else "(leer)"
|
||||
return await _bewertung_mit_kritik(
|
||||
topic, baustein, section_block, vertiefung_block,
|
||||
topic, baustein, section_block, kompakt_block,
|
||||
frage.strip() or "(keine Frage übergeben)", transcript, gute_antworten, provider, role,
|
||||
)
|
||||
except Exception:
|
||||
@@ -304,7 +279,7 @@ async def pruefung_bewertung(
|
||||
|
||||
|
||||
async def pruefung_bewertung_schnell(
|
||||
topic: str, baustein: str, section: str, vertiefung: str | None,
|
||||
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.
|
||||
@@ -312,12 +287,12 @@ async def pruefung_bewertung_schnell(
|
||||
Wird optimistisch angezeigt; 'antwort_pruefen' liefert danach das geprüfte Urteil.
|
||||
"""
|
||||
try:
|
||||
section_block, vertiefung_block = _bloecke(section, vertiefung)
|
||||
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,
|
||||
vertiefung_block=vertiefung_block, frage=frage.strip() or "(keine Frage übergeben)",
|
||||
kompakt_block=kompakt_block, frage=frage.strip() or "(keine Frage übergeben)",
|
||||
transcript=transcript, gute_antworten=gute_antworten, noetig=NOETIG, kritik_block="(keine)",
|
||||
)
|
||||
except Exception:
|
||||
@@ -326,7 +301,7 @@ async def pruefung_bewertung_schnell(
|
||||
|
||||
|
||||
async def baustein_diskussion(
|
||||
topic: str, baustein: str, section: str, vertiefung: str | None,
|
||||
topic: str, baustein: str, section: str, kompakt: str | None,
|
||||
frage: str, letzte_bewertung: str | None, messages: list[dict], provider: str = DEFAULT_PROVIDER,
|
||||
) -> str | None:
|
||||
"""Aktion 'diskussion': Tutor erklärt/diskutiert die Frage oder eine Bewertung.
|
||||
@@ -334,11 +309,11 @@ async def baustein_diskussion(
|
||||
Kein Bewerten, kein Kritiker — hier ist der Mensch der Prüfer. None bei Fehler.
|
||||
"""
|
||||
try:
|
||||
section_block, vertiefung_block = _bloecke(section, vertiefung)
|
||||
section_block, kompakt_block = _bloecke(section, kompakt)
|
||||
prompt = _prompt(
|
||||
"Baustein-Pruefung-Diskussion",
|
||||
topic=topic, baustein=baustein,
|
||||
section_block=section_block, vertiefung_block=vertiefung_block,
|
||||
section_block=section_block, kompakt_block=kompakt_block,
|
||||
frage=frage.strip() or "(keine Frage übergeben)",
|
||||
letzte_bewertung_block=(letzte_bewertung or "").strip() or "(noch keine)",
|
||||
transcript=_transcript(messages) if messages else "(leer)",
|
||||
|
||||
115
backend/lesbarkeit.py
Normal file
115
backend/lesbarkeit.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""Deterministisches Lesbarkeits-Gate für Guide-Sections.
|
||||
|
||||
Ein kleines deutsches Komplexitäts-Modell (DistilBERT, GermEval 2022, Skala 1–7)
|
||||
bewertet die Verständlichkeit der Fließtext-Prosa. guide.py meldet zu schwere
|
||||
Sections in die bestehende Lese-Prüfungs-/Überarbeitungs-Schleife — kein Prompt,
|
||||
kein Raten.
|
||||
|
||||
Optional: fehlen `transformers`/`torch` oder lädt das Modell nicht, ist das Gate
|
||||
stumm deaktiviert (das Backend läuft unverändert weiter). CPU genügt; der Aufrufer
|
||||
wrappt die Bewertung in `asyncio.to_thread` (blockierende Modell-Inferenz).
|
||||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
|
||||
from config import (
|
||||
LESBARKEIT_AKTIV, LESBARKEIT_HART, LESBARKEIT_HART_ANTEIL, LESBARKEIT_MAX, LESBARKEIT_MODELL,
|
||||
)
|
||||
|
||||
log = logging.getLogger("creator.lesbarkeit")
|
||||
|
||||
_modell_cache = None # (tokenizer, model, torch) — Singleton
|
||||
_ladeversuch = False # schon versucht zu laden?
|
||||
|
||||
# Markup raus → reiner Fließtext (Code zählt nicht zur Lesbarkeit).
|
||||
_CODE_FENCE = re.compile(r"```.*?```", re.DOTALL)
|
||||
_COMMENT = re.compile(r"<!--.*?-->", re.DOTALL)
|
||||
_INLINE_CODE = re.compile(r"`[^`]*`")
|
||||
_LINK = re.compile(r"\[([^\]]*)\]\([^)]*\)")
|
||||
_MD_MARK = re.compile(r"^[ \t]*([#>]+|[-*+]\s)|[*_~|]", re.MULTILINE)
|
||||
_WS = re.compile(r"\s+")
|
||||
_SATZ = re.compile(r"(?<=[.!?])\s+")
|
||||
|
||||
|
||||
def _modell():
|
||||
"""Lädt das Modell einmalig. None = Gate aus (deaktiviert oder Lade-Fehler)."""
|
||||
global _modell_cache, _ladeversuch
|
||||
if _ladeversuch:
|
||||
return _modell_cache
|
||||
_ladeversuch = True
|
||||
if not LESBARKEIT_AKTIV:
|
||||
return None
|
||||
try:
|
||||
import torch
|
||||
from transformers import AutoModelForSequenceClassification, AutoTokenizer
|
||||
tok = AutoTokenizer.from_pretrained(LESBARKEIT_MODELL)
|
||||
model = AutoModelForSequenceClassification.from_pretrained(LESBARKEIT_MODELL)
|
||||
model.eval()
|
||||
_modell_cache = (tok, model, torch)
|
||||
log.info("Lesbarkeits-Modell geladen: %s (num_labels=%d)", LESBARKEIT_MODELL, model.config.num_labels)
|
||||
except Exception as e:
|
||||
log.warning("Lesbarkeits-Gate deaktiviert (Modell nicht ladbar): %s", e)
|
||||
_modell_cache = None
|
||||
return _modell_cache
|
||||
|
||||
|
||||
def _prosa(md: str) -> str:
|
||||
"""Markdown/Code strippen → reiner Fließtext für die Bewertung."""
|
||||
t = _CODE_FENCE.sub(" ", md)
|
||||
t = _COMMENT.sub(" ", t)
|
||||
t = _INLINE_CODE.sub(" ", t)
|
||||
t = _LINK.sub(r"\1", t)
|
||||
t = _MD_MARK.sub(" ", t)
|
||||
return _WS.sub(" ", t).strip()
|
||||
|
||||
|
||||
def _saetze(text: str) -> list[str]:
|
||||
"""Fließtext in Sätze splitten; sehr kurze Fragmente verwerfen."""
|
||||
return [s.strip() for s in _SATZ.split(text) if len(s.strip()) >= 15]
|
||||
|
||||
|
||||
def _scores(saetze: list[str]) -> list[float]:
|
||||
"""Komplexität je Satz (1–7). Regression (num_labels=1) oder Erwartungswert über Klassen."""
|
||||
tok, model, torch = _modell_cache
|
||||
werte: list[float] = []
|
||||
n = model.config.num_labels
|
||||
for i in range(0, len(saetze), 16):
|
||||
batch = saetze[i:i + 16]
|
||||
enc = tok(batch, return_tensors="pt", truncation=True, max_length=256, padding=True)
|
||||
with torch.no_grad():
|
||||
logits = model(**enc).logits
|
||||
if n == 1:
|
||||
vals = logits.reshape(-1).tolist()
|
||||
else:
|
||||
probs = torch.softmax(logits, dim=-1)
|
||||
stufen = torch.arange(1, n + 1, dtype=probs.dtype)
|
||||
vals = (probs * stufen).sum(-1).reshape(-1).tolist()
|
||||
werte.extend(vals)
|
||||
return werte
|
||||
|
||||
|
||||
def bewerte_sections(md_by_num: dict[int, str]) -> dict[int, str]:
|
||||
"""{num: section_md} → {num: Hinweis} nur für zu schwere Sections.
|
||||
|
||||
Leeres dict, wenn das Gate aus ist. Blockierend (CPU) — in to_thread aufrufen.
|
||||
"""
|
||||
if _modell() is None:
|
||||
return {}
|
||||
out: dict[int, str] = {}
|
||||
for num, md in md_by_num.items():
|
||||
saetze = _saetze(_prosa(md or ""))
|
||||
if len(saetze) < 2: # fast nur Code / zu kurz → überspringen
|
||||
continue
|
||||
werte = _scores(saetze)
|
||||
if not werte:
|
||||
continue
|
||||
schnitt = sum(werte) / len(werte)
|
||||
hart = sum(1 for w in werte if w > LESBARKEIT_HART) / len(werte)
|
||||
# Zu schwer = hoher Schnitt ODER zu viele harte Einzelsätze (Ausreißer-Nester).
|
||||
if schnitt > LESBARKEIT_MAX or hart >= LESBARKEIT_HART_ANTEIL:
|
||||
out[num] = (
|
||||
f"Zu schwer lesbar (Ø {schnitt:.1f}/7, {hart * 100:.0f}% harte Sätze): "
|
||||
"kürzere Sätze, einfachere Wörter, weniger Schachtelsätze, mehr Beispiele."
|
||||
)
|
||||
return out
|
||||
@@ -196,25 +196,11 @@ class ProgressResponse(BaseModel):
|
||||
|
||||
# --- Baustein-Lernen ---
|
||||
|
||||
VertiefungArt = Literal["vertiefung", "deepdive"]
|
||||
|
||||
|
||||
class VertiefungRequest(BaseModel):
|
||||
topic: str = Field(min_length=1, max_length=100)
|
||||
baustein: str = Field(min_length=1, max_length=200)
|
||||
section: str = Field(default="", max_length=20000)
|
||||
art: VertiefungArt = "vertiefung"
|
||||
provider: ProviderType = "claude"
|
||||
|
||||
|
||||
class VertiefungResponse(BaseModel):
|
||||
md: str
|
||||
|
||||
|
||||
class BausteinChatRequest(BaseModel):
|
||||
topic: str = Field(min_length=1, max_length=100)
|
||||
baustein: str = Field(min_length=1, max_length=200)
|
||||
section: str = Field(default="", max_length=20000)
|
||||
section: str = Field(default="", max_length=20000) # ausführliche Fassung
|
||||
section_kompakt: str = Field(default="", max_length=20000) # kompakte Fassung (Merksätze)
|
||||
messages: list[ChatMessage] = Field(min_length=1)
|
||||
provider: ProviderType = "claude"
|
||||
|
||||
@@ -226,7 +212,8 @@ class BausteinChatResponse(BaseModel):
|
||||
class BausteinPruefungRequest(BaseModel):
|
||||
topic: str = Field(min_length=1, max_length=100)
|
||||
baustein: str = Field(min_length=1, max_length=200)
|
||||
section: str = Field(default="", max_length=20000)
|
||||
section: str = Field(default="", max_length=20000) # ausführliche Fassung
|
||||
section_kompakt: str = Field(default="", max_length=20000) # kompakte Fassung (Merksätze)
|
||||
aktion: Literal["frage", "diskussion", "antwort", "antwort_pruefen"] = "frage"
|
||||
frage: str = Field(default="", max_length=2000) # aktuell geprüfte Frage (für diskussion/antwort)
|
||||
letzte_bewertung: str = Field(default="", max_length=2000) # Feedback der letzten Bewertung (Kontext für diskussion)
|
||||
@@ -257,8 +244,6 @@ class BausteinLernstand(BaseModel):
|
||||
absolviert: bool
|
||||
verstanden: bool
|
||||
gemeistert: bool
|
||||
vertiefung: bool
|
||||
deepdive: bool
|
||||
|
||||
|
||||
class BausteinLernstandResponse(BaseModel):
|
||||
|
||||
@@ -2,3 +2,5 @@ fastapi
|
||||
uvicorn[standard]
|
||||
aiosqlite
|
||||
playwright
|
||||
transformers
|
||||
torch
|
||||
|
||||
@@ -13,12 +13,11 @@ from database import (
|
||||
create_topic, list_topics as db_list_topics, delete_topic,
|
||||
list_progress, set_progress, delete_progress,
|
||||
create_element, list_elements, get_element, update_element, delete_element,
|
||||
get_vertiefung, set_vertiefung, list_vertiefungen,
|
||||
list_baustein_progress, set_baustein_score, set_baustein_streak, set_baustein_absolviert, set_baustein_verstanden, set_baustein_gemeistert, delete_baustein_daten,
|
||||
)
|
||||
from bausteine import generate_bausteine, cancel_bausteine, bausteine_status, active_bausteine, reset_bausteine, lade_quelle, lade_uebersicht
|
||||
from elements import generate_element, chat_with_guide, chat_with_element, check_element, style_element, refine_suggestion
|
||||
from lernen import NOETIG, MASTERY, MEISTERN, baustein_chat, baustein_diskussion, baustein_element_anlegen, pruefung_bewertung, pruefung_bewertung_schnell, pruefung_frage, score_berechnen, streak_berechnen, vertiefung_generieren
|
||||
from lernen import NOETIG, MASTERY, MEISTERN, baustein_chat, baustein_diskussion, baustein_element_anlegen, pruefung_bewertung, pruefung_bewertung_schnell, pruefung_frage, score_berechnen, streak_berechnen
|
||||
from guide import generate_guide, guide_slot_dateien
|
||||
from pipeline import cancel_guide
|
||||
from regeln import FORMATE, formate_stats, guide_lock, ist_absolviert, lade_lernstand, thema_abgeschlossen
|
||||
@@ -32,7 +31,6 @@ from models import (
|
||||
ElementRefineRequest, ElementRefineResponse,
|
||||
ProgressUpdate, ProgressResponse, ProjectResponse, ProviderInfo,
|
||||
FolderResponse, BausteineQuelleUpdate, BausteineQuelleResponse, BausteinUebersicht,
|
||||
VertiefungRequest, VertiefungResponse,
|
||||
BausteinChatRequest, BausteinChatResponse,
|
||||
BausteinPruefungRequest, BausteinPruefungResponse, BausteinLernstandResponse,
|
||||
)
|
||||
@@ -207,18 +205,12 @@ async def get_bausteine_uebersicht(topic: str):
|
||||
return lade_uebersicht(topic)
|
||||
|
||||
|
||||
# --- Baustein-Lernen: Vertiefung, Chat, Prüfung ---
|
||||
|
||||
async def _bester_text(topic: str, baustein: str) -> str | None:
|
||||
"""Kontext für Chat/Prüfung: Amateur-Fassung (art 'deepdive') bevorzugt, sonst Vertiefung."""
|
||||
return await get_vertiefung(topic, baustein, "deepdive") or await get_vertiefung(topic, baustein, "vertiefung")
|
||||
|
||||
# --- Baustein-Lernen: Chat, Prüfung ---
|
||||
|
||||
@router.get("/bausteine/lernstand", response_model=BausteinLernstandResponse)
|
||||
async def baustein_lernstand(topic: str):
|
||||
"""Prüfungs-Stand + Vertiefungs-/Deepdive-Existenz pro Baustein (roher Titel als Key)."""
|
||||
"""Prüfungs-Stand pro Baustein (roher Titel als Key)."""
|
||||
progress = await list_baustein_progress(topic)
|
||||
texte = await list_vertiefungen(topic)
|
||||
bausteine = {
|
||||
p["baustein"]: {
|
||||
"gute_antworten": p["gute_antworten"],
|
||||
@@ -226,44 +218,16 @@ async def baustein_lernstand(topic: str):
|
||||
"absolviert": p["absolviert"] is not None,
|
||||
"verstanden": p["verstanden"] is not None,
|
||||
"gemeistert": p["gemeistert"] is not None,
|
||||
"vertiefung": "vertiefung" in texte.get(p["baustein"], set()),
|
||||
"deepdive": "deepdive" in texte.get(p["baustein"], set()),
|
||||
}
|
||||
for p in progress
|
||||
}
|
||||
for b, arten in texte.items():
|
||||
if b not in bausteine:
|
||||
bausteine[b] = {
|
||||
"gute_antworten": 0, "streak": 0, "absolviert": False, "verstanden": False, "gemeistert": False,
|
||||
"vertiefung": "vertiefung" in arten, "deepdive": "deepdive" in arten,
|
||||
}
|
||||
return {"bausteine": bausteine}
|
||||
|
||||
|
||||
@router.get("/bausteine/vertiefung", response_model=VertiefungResponse)
|
||||
async def get_baustein_vertiefung(topic: str, baustein: str, art: str = "vertiefung"):
|
||||
if art not in ("vertiefung", "deepdive"):
|
||||
raise HTTPException(400, "Unbekannte Art")
|
||||
md = await get_vertiefung(topic, baustein, art)
|
||||
if md is None:
|
||||
raise HTTPException(404, "Kein Text vorhanden")
|
||||
return {"md": md}
|
||||
|
||||
|
||||
@router.post("/bausteine/vertiefung", response_model=VertiefungResponse)
|
||||
async def create_baustein_vertiefung(req: VertiefungRequest):
|
||||
md = await vertiefung_generieren(req.topic, req.baustein, req.section, art=req.art, provider=req.provider)
|
||||
if md is None:
|
||||
raise HTTPException(502, "Generierung fehlgeschlagen — bitte erneut versuchen")
|
||||
await set_vertiefung(req.topic, req.baustein, req.art, md)
|
||||
return {"md": md}
|
||||
|
||||
|
||||
@router.post("/bausteine/chat", response_model=BausteinChatResponse)
|
||||
async def baustein_chat_route(req: BausteinChatRequest):
|
||||
vertiefung = await _bester_text(req.topic, req.baustein)
|
||||
reply = await baustein_chat(
|
||||
req.topic, req.baustein, req.section, vertiefung,
|
||||
req.topic, req.baustein, req.section, req.section_kompakt,
|
||||
[m.model_dump() for m in req.messages], provider=req.provider,
|
||||
)
|
||||
return {"reply": reply}
|
||||
@@ -279,11 +243,11 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest):
|
||||
absolviert = stand["absolviert"] is not None
|
||||
verstanden = stand["verstanden"] is not None
|
||||
gemeistert = stand["gemeistert"] is not None
|
||||
vertiefung = await _bester_text(req.topic, req.baustein)
|
||||
kompakt = req.section_kompakt
|
||||
msgs = [m.model_dump() for m in req.messages]
|
||||
|
||||
if req.aktion == "frage":
|
||||
frage = await pruefung_frage(req.topic, req.baustein, req.section, vertiefung, msgs, provider=req.provider)
|
||||
frage = await pruefung_frage(req.topic, req.baustein, req.section, kompakt, msgs, provider=req.provider)
|
||||
if frage is None:
|
||||
raise HTTPException(502, "Frage fehlgeschlagen — bitte erneut versuchen")
|
||||
return {"frage": frage, "gute_antworten": gute, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert}
|
||||
@@ -292,7 +256,7 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest):
|
||||
if not req.frage.strip():
|
||||
raise HTTPException(400, "Diskussion braucht eine laufende Frage")
|
||||
reply = await baustein_diskussion(
|
||||
req.topic, req.baustein, req.section, vertiefung,
|
||||
req.topic, req.baustein, req.section, kompakt,
|
||||
req.frage, req.letzte_bewertung or None, msgs, provider=req.provider,
|
||||
)
|
||||
if reply is None:
|
||||
@@ -309,7 +273,7 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest):
|
||||
if req.aktion == "antwort":
|
||||
# Vorschau: nur Bewerter, kein Kritiker, KEIN Persist, KEINE Meilensteine.
|
||||
data = await pruefung_bewertung_schnell(
|
||||
req.topic, req.baustein, req.section, vertiefung, req.frage, msgs, gute, provider=req.provider,
|
||||
req.topic, req.baustein, req.section, kompakt, req.frage, msgs, gute, provider=req.provider,
|
||||
)
|
||||
if data is None:
|
||||
raise HTTPException(502, "Bewertung fehlgeschlagen — bitte erneut versuchen")
|
||||
@@ -319,7 +283,7 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest):
|
||||
|
||||
# aktion == "antwort_pruefen": verbindlich (Bewerter + Kritiker), persistiert Score + Meilensteine.
|
||||
data = await pruefung_bewertung(
|
||||
req.topic, req.baustein, req.section, vertiefung, req.frage, msgs, gute, provider=req.provider,
|
||||
req.topic, req.baustein, req.section, kompakt, req.frage, msgs, gute, provider=req.provider,
|
||||
role="guide" if req.gruendlich else "judge",
|
||||
)
|
||||
if data is None:
|
||||
|
||||
@@ -121,20 +121,25 @@ _FRAGMENT_KAPITEL_RE = re.compile(r"<!--\s*kapitel\s*:\s*(.*?)\s*-->", re.IGNORE
|
||||
_FRAGMENT_SECTION_RE = re.compile(r"<!--\s*section\s*:\s*(.*?)\s*-->", re.IGNORECASE)
|
||||
_FRAGMENT_SUB_RE = re.compile(r"<!--\s*sub\s*:\s*(.*?)\s*-->", re.IGNORECASE)
|
||||
_FRAGMENT_BAUSTEIN_RE = re.compile(r"<!--\s*baustein\s*:\s*(.*?)\s*-->", re.IGNORECASE)
|
||||
# Zwei Lese-Schichten je Section: kompakt (Merksätze) + ausführlich (Erklärung).
|
||||
_FRAGMENT_KOMPAKT_RE = re.compile(r"<!--\s*kompakt\s*-->", re.IGNORECASE)
|
||||
_FRAGMENT_AUSF_RE = re.compile(r"<!--\s*ausf(?:ü|ue)hrlich\s*-->", re.IGNORECASE)
|
||||
_STUFEN = ("einfach", "mittel", "schwer")
|
||||
|
||||
|
||||
def _parse_fragment(text: str) -> list[dict]:
|
||||
"""Parst eine Writer-Datei → [{"kapitel", "titel", "md", "subs"}] in Datei-Reihenfolge.
|
||||
"""Parst eine Writer-Datei → [{"kapitel", "titel", "md", "kompakt", "subs"}] in Reihenfolge.
|
||||
|
||||
Optionale Subbaustein-Marker `<!-- sub: stufe | Titel -->` innerhalb einer
|
||||
Section sammeln sich in `sec["subs"] = [{"stufe", "titel", "md"}]`. `sec["md"]`
|
||||
bleibt der vollständige Body (alle Sub-Texte zusammengesetzt) — auch ohne Subs.
|
||||
Zwei optionale Lese-Schichten je Section über `<!-- kompakt -->` / `<!-- ausführlich -->`:
|
||||
Zeilen nach `kompakt` landen in `sec["kompakt"]`, sonst in `sec["md"]` (= ausführlich,
|
||||
auch der Default ohne Marker → rückwärtskompatibel). Optionale `<!-- sub: -->`-Marker
|
||||
sammeln sich weiter in `sec["subs"]`.
|
||||
"""
|
||||
sections: list[dict] = []
|
||||
kapitel = None
|
||||
current = None
|
||||
cur_sub = None
|
||||
cur_layer = "md" # Default: alles ohne Schicht-Marker ist die ausführliche Fassung
|
||||
for line in text.splitlines():
|
||||
s = line.strip()
|
||||
m = _FRAGMENT_KAPITEL_RE.match(s)
|
||||
@@ -145,10 +150,19 @@ def _parse_fragment(text: str) -> list[dict]:
|
||||
continue
|
||||
m = _FRAGMENT_SECTION_RE.match(s)
|
||||
if m:
|
||||
current = {"kapitel": kapitel, "titel": m.group(1), "md": [], "subs": []}
|
||||
current = {"kapitel": kapitel, "titel": m.group(1), "md": [], "kompakt": [], "subs": []}
|
||||
cur_sub = None
|
||||
cur_layer = "md"
|
||||
sections.append(current)
|
||||
continue
|
||||
if current is not None and _FRAGMENT_KOMPAKT_RE.match(s):
|
||||
cur_layer = "kompakt"
|
||||
cur_sub = None
|
||||
continue
|
||||
if current is not None and _FRAGMENT_AUSF_RE.match(s):
|
||||
cur_layer = "md"
|
||||
cur_sub = None
|
||||
continue
|
||||
m = _FRAGMENT_SUB_RE.match(s)
|
||||
if m and current is not None:
|
||||
teil = m.group(1).split("|", 1)
|
||||
@@ -161,11 +175,12 @@ def _parse_fragment(text: str) -> list[dict]:
|
||||
current["subs"].append(cur_sub)
|
||||
continue
|
||||
if current is not None:
|
||||
current["md"].append(line)
|
||||
if cur_sub is not None:
|
||||
current[cur_layer].append(line)
|
||||
if cur_sub is not None and cur_layer == "md":
|
||||
cur_sub["md"].append(line)
|
||||
for sec in sections:
|
||||
sec["md"] = "\n".join(sec["md"]).strip()
|
||||
sec["kompakt"] = "\n".join(sec["kompakt"]).strip()
|
||||
for sub in sec["subs"]:
|
||||
sub["md"] = "\n".join(sub["md"]).strip()
|
||||
sec["subs"] = [sub for sub in sec["subs"] if sub["md"]]
|
||||
|
||||
Reference in New Issue
Block a user