update
This commit is contained in:
17
Makefile
17
Makefile
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: install dev prod stop logs remove auth sync sync-projects sync-all projects searxng ollama
|
.PHONY: install dev prod stop logs remove auth sync sync-projects sync-all sync-all-reverse projects searxng ollama
|
||||||
|
|
||||||
COMPOSE = docker compose
|
COMPOSE = docker compose
|
||||||
|
|
||||||
@@ -79,4 +79,19 @@ sync-projects:
|
|||||||
sync-all: sync sync-projects
|
sync-all: sync sync-projects
|
||||||
@echo "Voller Sync (Themen + Projekte) abgeschlossen."
|
@echo "Voller Sync (Themen + Projekte) abgeschlossen."
|
||||||
|
|
||||||
|
# Gegenstück zu sync-all: lokalen Stand auf den Server schieben (Local → Remote).
|
||||||
|
# Stoppt den Remote-Server (DB-Sicherheit), pusht DB + Themen + Projekte, startet ihn neu.
|
||||||
|
# --delete = exakter Spiegel: Remote-Dateien ohne lokales Pendant werden gelöscht.
|
||||||
|
sync-all-reverse: stop
|
||||||
|
@echo "ACHTUNG: überschreibt den Remote-Stand (DB, themen/, projects/) mit dem lokalen."
|
||||||
|
@[ -f storage/creator.db ] || { echo "Keine lokale DB — abgebrochen."; exit 1; }
|
||||||
|
ssh root@178.104.67.87 'cd /var/www/creator && docker compose down'
|
||||||
|
ssh root@178.104.67.87 'rm -f /var/www/creator/storage/creator.db-shm /var/www/creator/storage/creator.db-wal'
|
||||||
|
rsync -avz --progress storage/creator.db root@178.104.67.87:/var/www/creator/storage/
|
||||||
|
@[ -f storage/creator.db-wal ] && rsync -avz --progress storage/creator.db-wal root@178.104.67.87:/var/www/creator/storage/ || true
|
||||||
|
rsync -avz --progress --delete storage/themen/ root@178.104.67.87:/var/www/creator/storage/themen/
|
||||||
|
rsync -avz --progress --delete projects/ root@178.104.67.87:/var/www/creator/projects/
|
||||||
|
ssh root@178.104.67.87 'cd /var/www/creator && docker compose up -d --build'
|
||||||
|
@echo "Reverse-Sync abgeschlossen — Remote läuft wieder."
|
||||||
|
|
||||||
projects: sync-projects
|
projects: sync-projects
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ die Gesamtliste.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
import shutil
|
import shutil
|
||||||
@@ -19,11 +20,10 @@ import time
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import database as db
|
import database as db
|
||||||
from agents import kill_process, cancel_scope, clear_scope
|
from agents import kill_process, cancel_scope, clear_scope, run_agent
|
||||||
from config import KONSENS_GRACE, RECHERCHE_GRACE, KONSENS_MAX_RUNDEN, DEFAULT_PROVIDER, CRAWL_KEEP_PATTERNS, CRAWL_NOISE_PATTERNS, CRAWL_MIN_CHARS
|
from config import KONSENS_GRACE, RECHERCHE_GRACE, KONSENS_MAX_RUNDEN, DEFAULT_PROVIDER, CRAWL_KEEP_PATTERNS, CRAWL_NOISE_PATTERNS, CRAWL_MIN_CHARS
|
||||||
from fsutil import atomic_write_text, atomic_write_json
|
from fsutil import atomic_write_text, atomic_write_json
|
||||||
from jsonio import read_json_file as _json_datei
|
from jsonio import read_json_file as _json_datei
|
||||||
from lernen import FRAGETYPEN
|
|
||||||
from paths import arbeit_dir, bausteine_path, frage_muster_path, project_dir, subbausteine_path, quelle_path, quelle_crawl_dir, safe_ordner
|
from paths import arbeit_dir, bausteine_path, frage_muster_path, project_dir, subbausteine_path, quelle_path, quelle_crawl_dir, safe_ordner
|
||||||
from crawl import crawl
|
from crawl import crawl
|
||||||
from pipeline import (
|
from pipeline import (
|
||||||
@@ -51,6 +51,8 @@ SUBBAUSTEIN_KAPPE = 900 # Subbaustein-Finde-Loop je Chunk (15 min)
|
|||||||
KONSOLIDIERUNG_CHUNK = 600 # bis hierher EIN globaler Judge (dedupt alles); darüber chunked + Merge-Pass
|
KONSOLIDIERUNG_CHUNK = 600 # bis hierher EIN globaler Judge (dedupt alles); darüber chunked + Merge-Pass
|
||||||
# Frage-Muster-Chunks per LPT nach Sub-Last balancieren (Makespan), statt nach Baustein-Anzahl.
|
# Frage-Muster-Chunks per LPT nach Sub-Last balancieren (Makespan), statt nach Baustein-Anzahl.
|
||||||
FRAGE_CHUNK_SUBS = 50 # Ziel-Summe relevanter Subs je Chunk
|
FRAGE_CHUNK_SUBS = 50 # Ziel-Summe relevanter Subs je Chunk
|
||||||
|
FAKTEN_CHUNK_SUBS = 25 # Fakten-Extraktion: kleinere Chunks (Fakten sind umfangreicher als Muster)
|
||||||
|
FAKTEN_CHECK_PANEL = 3 # Judges je Chunk im Fakten-Check (Mehrheit beanstandet)
|
||||||
|
|
||||||
log = logging.getLogger("creator.bausteine")
|
log = logging.getLogger("creator.bausteine")
|
||||||
|
|
||||||
@@ -62,10 +64,14 @@ _bausteine_step: dict[str, int] = {}
|
|||||||
BAUSTEINE_STEPS = (
|
BAUSTEINE_STEPS = (
|
||||||
"Recherche", "Konsolidierung", "Klärung",
|
"Recherche", "Konsolidierung", "Klärung",
|
||||||
"Subbausteine finden", "Subbausteine wählen", "Subbausteine klären",
|
"Subbausteine finden", "Subbausteine wählen", "Subbausteine klären",
|
||||||
|
"Fakten finden", "Fakten prüfen", "Fakten fix",
|
||||||
"Stufen finden", "Stufen wählen", "Stufen klären",
|
"Stufen finden", "Stufen wählen", "Stufen klären",
|
||||||
"Relevanz finden", "Relevanz wählen", "Relevanz klären",
|
"Relevanz finden", "Relevanz wählen", "Relevanz klären",
|
||||||
|
"Gliederung",
|
||||||
"Fragen finden", "Fragen wählen", "Fragen klären", "Fragen prüfen",
|
"Fragen finden", "Fragen wählen", "Fragen klären", "Fragen prüfen",
|
||||||
|
"Karteikarten", "Beispiele", "Diagramme",
|
||||||
)
|
)
|
||||||
|
ARTEFAKT_TYPEN = ("karteikarte", "beispiel", "diagramm")
|
||||||
|
|
||||||
|
|
||||||
def lade_quelle(topic: str) -> dict:
|
def lade_quelle(topic: str) -> dict:
|
||||||
@@ -93,7 +99,8 @@ def _crawl_fertig(topic: str) -> bool:
|
|||||||
return (quelle_crawl_dir(topic) / ".done").exists() # Marker erst bei sauberem Abschluss
|
return (quelle_crawl_dir(topic) / ".done").exists() # Marker erst bei sauberem Abschluss
|
||||||
|
|
||||||
|
|
||||||
_STUFEN = ("einfach", "mittel", "schwer")
|
# Lernpfad-Stufen (anfaenger/fortgeschritten/experte); alte Schwierigkeits-Werte abwärtskompatibel.
|
||||||
|
_STUFEN = ("anfaenger", "fortgeschritten", "experte", "einfach", "mittel", "schwer")
|
||||||
|
|
||||||
|
|
||||||
async def subbausteine_titel(topic: str, baustein: str) -> list[str]:
|
async def subbausteine_titel(topic: str, baustein: str) -> list[str]:
|
||||||
@@ -115,19 +122,36 @@ async def lade_frage_muster(topic: str, baustein: str) -> list[dict]:
|
|||||||
"""Vordefinierte Frage-Muster eines Bausteins — DB-first, Fallback Sidecar (leer = Live)."""
|
"""Vordefinierte Frage-Muster eines Bausteins — DB-first, Fallback Sidecar (leer = Live)."""
|
||||||
rows = await db.list_frage_muster(topic, _norm_titel(baustein))
|
rows = await db.list_frage_muster(topic, _norm_titel(baustein))
|
||||||
if rows:
|
if rows:
|
||||||
return [{"subbaustein": r["sub_titel"], "typ": r["typ"], "frage": r["frage"]} for r in rows if r["frage"]]
|
return [{"subbaustein": r["sub_titel"], "frage": r["frage"]} for r in rows if r["frage"]]
|
||||||
fm = _json_datei(frage_muster_path(topic))
|
fm = _json_datei(frage_muster_path(topic))
|
||||||
if not isinstance(fm, dict):
|
if not isinstance(fm, dict):
|
||||||
return []
|
return []
|
||||||
return [
|
return [
|
||||||
{"subbaustein": str(e.get("subbaustein", "")).strip(),
|
{"subbaustein": str(e.get("subbaustein", "")).strip(), "frage": frage}
|
||||||
"typ": str(e.get("typ", "")).strip(),
|
|
||||||
"frage": frage}
|
|
||||||
for e in (fm.get(baustein) or [])
|
for e in (fm.get(baustein) or [])
|
||||||
if isinstance(e, dict) and (frage := str(e.get("frage", "")).strip())
|
if isinstance(e, dict) and (frage := str(e.get("frage", "")).strip())
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def subbausteine_frei(topic: str, baustein: str, max_ebene: int) -> list[str]:
|
||||||
|
"""Subbaustein-Titel bis zur freigeschalteten Ebene (≤ max_ebene). Fallback ohne
|
||||||
|
Ebenen-Wissen (Altbestand/Sidecar): alle Subbaustein-Titel."""
|
||||||
|
rows = await db.subs_mit_ebene(topic, baustein)
|
||||||
|
if not rows:
|
||||||
|
return await subbausteine_titel(topic, baustein)
|
||||||
|
return [s["titel"] for s in rows if s["ebene"] <= max_ebene and s["titel"]]
|
||||||
|
|
||||||
|
|
||||||
|
async def lade_frage_muster_frei(topic: str, baustein: str, max_ebene: int) -> list[dict]:
|
||||||
|
"""Frage-Muster, gefiltert auf Subbausteine bis zur freigeschalteten Ebene. Ohne
|
||||||
|
Ebenen-Wissen (Altbestand/Sidecar) ungefiltert."""
|
||||||
|
rows = await db.subs_mit_ebene(topic, baustein)
|
||||||
|
if not rows:
|
||||||
|
return await lade_frage_muster(topic, baustein)
|
||||||
|
frei = {s["norm"] for s in rows if s["ebene"] <= max_ebene}
|
||||||
|
return [m for m in await lade_frage_muster(topic, baustein) if _norm_titel(m["subbaustein"]) in frei]
|
||||||
|
|
||||||
|
|
||||||
async def lade_uebersicht(topic: str) -> list[dict]:
|
async def lade_uebersicht(topic: str) -> list[dict]:
|
||||||
"""Strukturierte Baustein-Liste für die Übersicht — DB-first (Konsens + Subs/Stufen/Relevanz),
|
"""Strukturierte Baustein-Liste für die Übersicht — DB-first (Konsens + Subs/Stufen/Relevanz),
|
||||||
Fallback bausteine.md + Sidecar (Alt-Themen)."""
|
Fallback bausteine.md + Sidecar (Alt-Themen)."""
|
||||||
@@ -140,7 +164,7 @@ async def lade_uebersicht(topic: str) -> list[dict]:
|
|||||||
"num": num, "titel": b["titel"], "beschreibung": b["beschreibung"],
|
"num": num, "titel": b["titel"], "beschreibung": b["beschreibung"],
|
||||||
"subbausteine": [
|
"subbausteine": [
|
||||||
{"titel": s["sub_titel"],
|
{"titel": s["sub_titel"],
|
||||||
"stufe": s["stufe"] if s["stufe"] in _STUFEN else "mittel",
|
"stufe": s["stufe"] if s["stufe"] in _STUFEN else "fortgeschritten",
|
||||||
"relevanz": s["relevanz"] if s["relevanz"] in ("relevant", "rand") else None}
|
"relevanz": s["relevanz"] if s["relevanz"] in ("relevant", "rand") else None}
|
||||||
for s in subs if s["sub_titel"]
|
for s in subs if s["sub_titel"]
|
||||||
],
|
],
|
||||||
@@ -157,7 +181,7 @@ async def lade_uebersicht(topic: str) -> list[dict]:
|
|||||||
subbausteine = [
|
subbausteine = [
|
||||||
{
|
{
|
||||||
"titel": t,
|
"titel": t,
|
||||||
"stufe": s.get("stufe") if s.get("stufe") in _STUFEN else "mittel",
|
"stufe": s.get("stufe") if s.get("stufe") in _STUFEN else "fortgeschritten",
|
||||||
"relevanz": s.get("relevanz") if s.get("relevanz") in ("relevant", "rand") else None,
|
"relevanz": s.get("relevanz") if s.get("relevanz") in ("relevant", "rand") else None,
|
||||||
}
|
}
|
||||||
for s in (sidecar.get(titel) or [])
|
for s in (sidecar.get(titel) or [])
|
||||||
@@ -177,9 +201,12 @@ def _bausteine_steps(topic: str) -> tuple:
|
|||||||
base = ("Recherche", "Konsolidierung", "Klärung")
|
base = ("Recherche", "Konsolidierung", "Klärung")
|
||||||
rest = (
|
rest = (
|
||||||
"Subbausteine finden", "Subbausteine wählen", "Subbausteine klären",
|
"Subbausteine finden", "Subbausteine wählen", "Subbausteine klären",
|
||||||
|
"Fakten finden", "Fakten prüfen", "Fakten fix",
|
||||||
"Stufen finden", "Stufen wählen", "Stufen klären",
|
"Stufen finden", "Stufen wählen", "Stufen klären",
|
||||||
"Relevanz finden", "Relevanz wählen", "Relevanz klären",
|
"Relevanz finden", "Relevanz wählen", "Relevanz klären",
|
||||||
|
"Gliederung",
|
||||||
"Fragen finden", "Fragen wählen", "Fragen klären", "Fragen prüfen",
|
"Fragen finden", "Fragen wählen", "Fragen klären", "Fragen prüfen",
|
||||||
|
"Karteikarten", "Beispiele", "Diagramme",
|
||||||
)
|
)
|
||||||
mitte = base + (("Ergänzung",) if q["type"] == "projekt" else ()) + rest
|
mitte = base + (("Ergänzung",) if q["type"] == "projekt" else ()) + rest
|
||||||
return (("Quelle aufbereiten",) if q["type"] == "link" else ()) + mitte
|
return (("Quelle aufbereiten",) if q["type"] == "link" else ()) + mitte
|
||||||
@@ -203,9 +230,12 @@ PHASEN = (
|
|||||||
("Quelle", ("Quelle aufbereiten",)),
|
("Quelle", ("Quelle aufbereiten",)),
|
||||||
("Inventar", ("Recherche", "Konsolidierung", "Klärung", "Ergänzung")),
|
("Inventar", ("Recherche", "Konsolidierung", "Klärung", "Ergänzung")),
|
||||||
("Subbausteine", ("Subbausteine finden", "Subbausteine wählen", "Subbausteine klären")),
|
("Subbausteine", ("Subbausteine finden", "Subbausteine wählen", "Subbausteine klären")),
|
||||||
|
("Fakten", ("Fakten finden", "Fakten prüfen", "Fakten fix")),
|
||||||
("Stufen", ("Stufen finden", "Stufen wählen", "Stufen klären")),
|
("Stufen", ("Stufen finden", "Stufen wählen", "Stufen klären")),
|
||||||
("Relevanz", ("Relevanz finden", "Relevanz wählen", "Relevanz klären")),
|
("Relevanz", ("Relevanz finden", "Relevanz wählen", "Relevanz klären")),
|
||||||
|
("Gliederung", ("Gliederung",)),
|
||||||
("Fragen", ("Fragen finden", "Fragen wählen", "Fragen klären", "Fragen prüfen")),
|
("Fragen", ("Fragen finden", "Fragen wählen", "Fragen klären", "Fragen prüfen")),
|
||||||
|
("Artefakte", ("Karteikarten", "Beispiele", "Diagramme")),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -244,22 +274,28 @@ def _bausteine_files(topic: str) -> dict:
|
|||||||
"mapping": {n: arbeit / f"auswahl-mapping-r{n}.json" for n in runden},
|
"mapping": {n: arbeit / f"auswahl-mapping-r{n}.json" for n in runden},
|
||||||
"ergaenzung": arbeit / "ergaenzung.json",
|
"ergaenzung": arbeit / "ergaenzung.json",
|
||||||
"sub_roh": arbeit / "subbausteine-roh.json",
|
"sub_roh": arbeit / "subbausteine-roh.json",
|
||||||
|
"fakten": arbeit / "subbausteine-fakten.json",
|
||||||
"sidecar": subbausteine_path(topic),
|
"sidecar": subbausteine_path(topic),
|
||||||
"frage_muster": frage_muster_path(topic),
|
"frage_muster": frage_muster_path(topic),
|
||||||
|
"gliederung": arbeit / "gliederung.json",
|
||||||
|
"gliederung_slots": [arbeit / f"gliederung-{i}.json" for i in (1, 2, 3)],
|
||||||
|
"artefakte": arbeit / "artefakte.json",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _alle_slot_dateien(files: dict) -> list[Path]:
|
def _alle_slot_dateien(files: dict) -> list[Path]:
|
||||||
arbeit = files["arbeit"]
|
arbeit = files["arbeit"]
|
||||||
# Subbaustein-/Stufen-Slots sind pro Chunk dynamisch — per Glob einsammeln.
|
# Subbaustein-/Stufen-Slots sind pro Chunk dynamisch — per Glob einsammeln.
|
||||||
dyn = (list(arbeit.glob("subbaustein-*")) + list(arbeit.glob("stufe-*")) + list(arbeit.glob("relevanz-*"))
|
dyn = (list(arbeit.glob("subbaustein-*")) + list(arbeit.glob("fakten-*")) + list(arbeit.glob("stufe-*")) + list(arbeit.glob("relevanz-*"))
|
||||||
+ list(arbeit.glob("frage-muster-*")) + list(arbeit.glob("recherche-*")) + list(arbeit.glob("konsolidierung-*"))
|
+ list(arbeit.glob("frage-muster-*")) + list(arbeit.glob("gliederung-*")) + list(arbeit.glob("artefakt-*"))
|
||||||
|
+ list(arbeit.glob("recherche-*")) + list(arbeit.glob("konsolidierung-*"))
|
||||||
+ list(arbeit.glob("klaerung*"))) if arbeit.is_dir() else []
|
+ list(arbeit.glob("klaerung*"))) if arbeit.is_dir() else []
|
||||||
return [
|
return [
|
||||||
*files["recherche"], files["recherche_mapping"],
|
*files["recherche"], files["recherche_mapping"],
|
||||||
*(p for slots in files["auswahl"].values() for p in slots),
|
*(p for slots in files["auswahl"].values() for p in slots),
|
||||||
*files["mapping"].values(), files["ergaenzung"],
|
*files["mapping"].values(), files["ergaenzung"],
|
||||||
files["sub_roh"], files["sidecar"], files["frage_muster"], *dyn,
|
files["sub_roh"], files["sidecar"], files["frage_muster"],
|
||||||
|
files["fakten"], files["gliederung"], files["artefakte"], *dyn,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -288,12 +324,21 @@ def _resume_step(topic: str) -> int:
|
|||||||
# Stufen fertig; nur noch Relevanz offen?
|
# Stufen fertig; nur noch Relevanz offen?
|
||||||
if not _relevanz_komplett(sidecar):
|
if not _relevanz_komplett(sidecar):
|
||||||
return _step_idx(topic, "Relevanz finden")
|
return _step_idx(topic, "Relevanz finden")
|
||||||
# Relevanz fertig; nur noch Frage-Muster offen?
|
# Relevanz fertig; Gliederung (Bausteine-Artefakt für den Guide) offen?
|
||||||
|
if not _gliederung_komplett(files):
|
||||||
|
return _step_idx(topic, "Gliederung")
|
||||||
|
# Gliederung fertig; Frage-Muster offen?
|
||||||
if not _frage_muster_komplett(topic):
|
if not _frage_muster_komplett(topic):
|
||||||
return _step_idx(topic, "Fragen finden")
|
return _step_idx(topic, "Fragen finden")
|
||||||
|
# Fragen fertig; Lern-Artefakte (Karteikarten/Beispiele/Diagramme) offen?
|
||||||
|
if not _artefakte_komplett(files):
|
||||||
|
return _step_idx(topic, "Karteikarten")
|
||||||
return len(_bausteine_steps(topic))
|
return len(_bausteine_steps(topic))
|
||||||
if _sub_roh_schema(_json_datei(files["sub_roh"])) is None:
|
if _sub_roh_schema(_json_datei(files["sub_roh"])) is None:
|
||||||
return _step_idx(topic, "Subbausteine finden")
|
return _step_idx(topic, "Subbausteine finden")
|
||||||
|
# Subbausteine fertig; Fakten noch offen? (Fakten kommen vor den Stufen.)
|
||||||
|
if not _fakten_komplett(files):
|
||||||
|
return _step_idx(topic, "Fakten finden")
|
||||||
return _step_idx(topic, "Stufen finden")
|
return _step_idx(topic, "Stufen finden")
|
||||||
|
|
||||||
|
|
||||||
@@ -356,12 +401,19 @@ def _reset_ab_phase(topic: str, label: str) -> None:
|
|||||||
for p in arbeit.glob(pat):
|
for p in arbeit.glob(pat):
|
||||||
p.unlink(missing_ok=True)
|
p.unlink(missing_ok=True)
|
||||||
|
|
||||||
if idx <= 5: # Fragen
|
# Phasen-Index: Quelle=0 · Inventar=1 · Subbausteine=2 · Fakten=3 · Stufen=4 · Relevanz=5 · Gliederung=6 · Fragen=7 · Artefakte=8
|
||||||
|
if idx <= 8: # Artefakte (Karteikarten/Beispiele/Diagramme)
|
||||||
|
files["artefakte"].unlink(missing_ok=True)
|
||||||
|
glob_del("artefakt-*")
|
||||||
|
if idx <= 7: # Fragen
|
||||||
files["frage_muster"].unlink(missing_ok=True)
|
files["frage_muster"].unlink(missing_ok=True)
|
||||||
glob_del("frage-muster-*")
|
glob_del("frage-muster-*")
|
||||||
if idx <= 4: # Relevanz
|
if idx <= 6: # Gliederung
|
||||||
|
files["gliederung"].unlink(missing_ok=True)
|
||||||
|
glob_del("gliederung-*")
|
||||||
|
if idx <= 5: # Relevanz
|
||||||
glob_del("relevanz-*")
|
glob_del("relevanz-*")
|
||||||
if idx <= 3: # Stufen + Relevanz teilen die Sidecar → ab Stufen ganz neu
|
if idx <= 4: # Stufen + Relevanz teilen die Sidecar → ab Stufen ganz neu
|
||||||
files["sidecar"].unlink(missing_ok=True)
|
files["sidecar"].unlink(missing_ok=True)
|
||||||
glob_del("stufe-*")
|
glob_del("stufe-*")
|
||||||
else: # ab Relevanz: Stufen behalten, nur Relevanz-Felder strippen
|
else: # ab Relevanz: Stufen behalten, nur Relevanz-Felder strippen
|
||||||
@@ -372,6 +424,9 @@ def _reset_ab_phase(topic: str, label: str) -> None:
|
|||||||
if isinstance(s, dict):
|
if isinstance(s, dict):
|
||||||
s.pop("relevanz", None)
|
s.pop("relevanz", None)
|
||||||
atomic_write_json(files["sidecar"], sc, indent=1)
|
atomic_write_json(files["sidecar"], sc, indent=1)
|
||||||
|
if idx <= 3: # Fakten (vor den Stufen) — Fakten-Map + Arbeitsdateien weg
|
||||||
|
files["fakten"].unlink(missing_ok=True)
|
||||||
|
glob_del("fakten-*")
|
||||||
if idx <= 2: # Subbausteine
|
if idx <= 2: # Subbausteine
|
||||||
files["sub_roh"].unlink(missing_ok=True)
|
files["sub_roh"].unlink(missing_ok=True)
|
||||||
glob_del("subbaustein-*")
|
glob_del("subbaustein-*")
|
||||||
@@ -474,7 +529,7 @@ def _sidecar_schema(data):
|
|||||||
if not isinstance(v, list) or not v:
|
if not isinstance(v, list) or not v:
|
||||||
return None
|
return None
|
||||||
for s in v:
|
for s in v:
|
||||||
if not isinstance(s, dict) or not str(s.get("titel", "")).strip() or s.get("stufe") not in ("einfach", "mittel", "schwer"):
|
if not isinstance(s, dict) or not str(s.get("titel", "")).strip() or s.get("stufe") not in _STUFEN:
|
||||||
return None
|
return None
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@@ -493,10 +548,10 @@ def _relevanz_komplett(data) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def _frage_muster_chunk_schema(data) -> list[dict] | None:
|
def _frage_muster_chunk_schema(data) -> list[dict] | None:
|
||||||
"""{"muster": [{baustein, subbaustein, typ, frage}, …]} → Liste valider Einträge · sonst None.
|
"""{"muster": [{baustein, subbaustein, frage}, …]} → Liste valider Einträge · sonst None.
|
||||||
|
|
||||||
Mit `baustein` (Zuordnung im 10er-Chunk). Ungültige Einzel-Einträge werden
|
Ein Muster je Subbaustein (kein Typ-Kreuzprodukt — die Schwierigkeit kommt erst bei der
|
||||||
übersprungen (nicht die ganze Liste verworfen)."""
|
Prüfung aus dem Lerner-Niveau). Ungültige Einzel-Einträge werden übersprungen."""
|
||||||
if not isinstance(data, dict) or not isinstance(data.get("muster"), list):
|
if not isinstance(data, dict) or not isinstance(data.get("muster"), list):
|
||||||
return None
|
return None
|
||||||
out = []
|
out = []
|
||||||
@@ -505,11 +560,10 @@ def _frage_muster_chunk_schema(data) -> list[dict] | None:
|
|||||||
continue
|
continue
|
||||||
bau = str(e.get("baustein", "")).strip()
|
bau = str(e.get("baustein", "")).strip()
|
||||||
sub = str(e.get("subbaustein", "")).strip()
|
sub = str(e.get("subbaustein", "")).strip()
|
||||||
typ = str(e.get("typ", "")).strip().casefold()
|
|
||||||
frage = str(e.get("frage", "")).strip()
|
frage = str(e.get("frage", "")).strip()
|
||||||
if not bau or not sub or typ not in FRAGETYPEN or not frage:
|
if not bau or not sub or not frage:
|
||||||
continue
|
continue
|
||||||
out.append({"baustein": bau, "subbaustein": sub, "typ": typ, "frage": frage})
|
out.append({"baustein": bau, "subbaustein": sub, "frage": frage})
|
||||||
return out or None
|
return out or None
|
||||||
|
|
||||||
|
|
||||||
@@ -801,10 +855,10 @@ async def _stufen_block(ctx: GenContext, set_p, files: dict, roh: dict, instruct
|
|||||||
timeout=_timeout("stufe_check", len(strittig)),
|
timeout=_timeout("stufe_check", len(strittig)),
|
||||||
)
|
)
|
||||||
if status == FAILED:
|
if status == FAILED:
|
||||||
_log(topic, f"Stufen-Klärung Paket {c} fehlgeschlagen — Default 'mittel'")
|
_log(topic, f"Stufen-Klärung Paket {c} fehlgeschlagen — Default 'fortgeschritten'")
|
||||||
entsch = entsch if isinstance(entsch, dict) else {}
|
entsch = entsch if isinstance(entsch, dict) else {}
|
||||||
# Strittige ohne Entscheid → 'mittel'; Vote-Gewinner bleiben; Judge überschreibt.
|
# Strittige ohne Entscheid → 'fortgeschritten'; Vote-Gewinner bleiben; Judge überschreibt.
|
||||||
ergebnis = {**{k: "mittel" for k in strittig}, **ergebnis, **entsch}
|
ergebnis = {**{k: "fortgeschritten" for k in strittig}, **ergebnis, **entsch}
|
||||||
return {item_idxs[k - 1] + 1: stufe for k, stufe in ergebnis.items()}
|
return {item_idxs[k - 1] + 1: stufe for k, stufe in ergebnis.items()}
|
||||||
|
|
||||||
parts = await _gather_fortschritt([_klaere(c, idxs) for c, idxs in enumerate(chunks, 1)], len(chunks), _melde_p(set_p, topic, "Stufen klären"))
|
parts = await _gather_fortschritt([_klaere(c, idxs) for c, idxs in enumerate(chunks, 1)], len(chunks), _melde_p(set_p, topic, "Stufen klären"))
|
||||||
@@ -813,12 +867,12 @@ async def _stufen_block(ctx: GenContext, set_p, files: dict, roh: dict, instruct
|
|||||||
stufe_by_id: dict[int, str] = {}
|
stufe_by_id: dict[int, str] = {}
|
||||||
for c, part in enumerate(parts, 1):
|
for c, part in enumerate(parts, 1):
|
||||||
if not isinstance(part, dict):
|
if not isinstance(part, dict):
|
||||||
# Klärung ist nicht fatal: Vote-Ergebnis + Default 'mittel' für Strittige.
|
# Klärung ist nicht fatal: Vote-Ergebnis + Default 'fortgeschritten' für Strittige.
|
||||||
if isinstance(part, BaseException):
|
if isinstance(part, BaseException):
|
||||||
_log(topic, f"Stufen-Klärung Paket {c}: {type(part).__name__}: {part}")
|
_log(topic, f"Stufen-Klärung Paket {c}: {type(part).__name__}: {part}")
|
||||||
ergebnis, strittig = vote_by_c[c]
|
ergebnis, strittig = vote_by_c[c]
|
||||||
item_idxs = chunks[c - 1]
|
item_idxs = chunks[c - 1]
|
||||||
merged = {**{k: "mittel" for k in strittig}, **ergebnis}
|
merged = {**{k: "fortgeschritten" for k in strittig}, **ergebnis}
|
||||||
part = {item_idxs[k - 1] + 1: s for k, s in merged.items()}
|
part = {item_idxs[k - 1] + 1: s for k, s in merged.items()}
|
||||||
stufe_by_id.update(part)
|
stufe_by_id.update(part)
|
||||||
|
|
||||||
@@ -829,11 +883,196 @@ async def _stufen_block(ctx: GenContext, set_p, files: dict, roh: dict, instruct
|
|||||||
lst = []
|
lst = []
|
||||||
for sub in subs:
|
for sub in subs:
|
||||||
gid += 1
|
gid += 1
|
||||||
lst.append({"titel": sub, "stufe": stufe_by_id.get(gid, "mittel")})
|
lst.append({"titel": sub, "stufe": stufe_by_id.get(gid, "fortgeschritten")})
|
||||||
sidecar[titel] = lst
|
sidecar[titel] = lst
|
||||||
return sidecar
|
return sidecar
|
||||||
|
|
||||||
|
|
||||||
|
_FAKTEN_FELDER = ("kernpunkte", "voraussetzungen", "huerden", "belegte_fakten", "beispiel_idee")
|
||||||
|
|
||||||
|
|
||||||
|
def _fakten_schema(data) -> list[dict] | None:
|
||||||
|
"""{"fakten": [{baustein, subbaustein, …}]} → valide Liste · sonst None.
|
||||||
|
Trennt belegte_fakten (mit Quelle) hart von beispiel_idee (generativ)."""
|
||||||
|
if not isinstance(data, dict) or not isinstance(data.get("fakten"), list):
|
||||||
|
return None
|
||||||
|
out = []
|
||||||
|
for e in data["fakten"]:
|
||||||
|
if not isinstance(e, dict):
|
||||||
|
continue
|
||||||
|
bau = str(e.get("baustein", "")).strip()
|
||||||
|
sub = str(e.get("subbaustein", "")).strip()
|
||||||
|
if not bau or not sub:
|
||||||
|
continue
|
||||||
|
bf = [{"text": t, "quelle": str(f.get("quelle", "")).strip()}
|
||||||
|
for f in (e.get("belegte_fakten") or []) if isinstance(f, dict) and (t := str(f.get("text", "")).strip())]
|
||||||
|
out.append({
|
||||||
|
"baustein": bau, "subbaustein": sub,
|
||||||
|
"kernpunkte": [k for x in (e.get("kernpunkte") or []) if (k := str(x).strip())],
|
||||||
|
"voraussetzungen": str(e.get("voraussetzungen", "")).strip(),
|
||||||
|
"huerden": str(e.get("huerden", "")).strip(),
|
||||||
|
"belegte_fakten": bf,
|
||||||
|
"beispiel_idee": str(e.get("beispiel_idee", "")).strip(),
|
||||||
|
})
|
||||||
|
return out or None
|
||||||
|
|
||||||
|
|
||||||
|
def _fakten_check_schema(data) -> list[str] | None:
|
||||||
|
"""Fakten-Check → beanstandete sub_norms · {ok:true}→[] · None bei ungültig."""
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return None
|
||||||
|
if data.get("ok") is True:
|
||||||
|
return []
|
||||||
|
pr = data.get("probleme")
|
||||||
|
if not isinstance(pr, list):
|
||||||
|
return None
|
||||||
|
return [sn for p in pr if isinstance(p, dict) and (sn := _norm_titel(str(p.get("subbaustein", ""))))]
|
||||||
|
|
||||||
|
|
||||||
|
def _fakten_zeilen(fk: dict) -> str:
|
||||||
|
z = []
|
||||||
|
if fk.get("kernpunkte"):
|
||||||
|
z.append("Kernpunkte: " + " · ".join(fk["kernpunkte"]))
|
||||||
|
if fk.get("voraussetzungen"):
|
||||||
|
z.append("Voraussetzung: " + fk["voraussetzungen"])
|
||||||
|
if fk.get("huerden"):
|
||||||
|
z.append("Hürde: " + fk["huerden"])
|
||||||
|
for bf in fk.get("belegte_fakten", []):
|
||||||
|
z.append(f"FAKT: {bf['text']} (Quelle: {bf.get('quelle', '?')})")
|
||||||
|
if fk.get("beispiel_idee"):
|
||||||
|
z.append("Beispiel: " + fk["beispiel_idee"])
|
||||||
|
return "\n".join(z)
|
||||||
|
|
||||||
|
|
||||||
|
def _fakten_komplett(files: dict) -> bool:
|
||||||
|
"""Fakten-Map existiert (Block durch)? {Baustein: {sub_norm: {...}}}."""
|
||||||
|
d = _json_datei(files["fakten"])
|
||||||
|
return isinstance(d, dict) and bool(d)
|
||||||
|
|
||||||
|
|
||||||
|
async def _fakten_block(ctx, set_p, files: dict, roh: dict, q: dict, ordner, instructions: str) -> dict | None:
|
||||||
|
"""Block: je Sub Quell-Fakten extrahieren (finden) → verifizieren (prüfen) → korrigieren (fix).
|
||||||
|
Extract-once-Grounding: das Ergebnis nährt Stufe/Relevanz/Fragen/Guide.
|
||||||
|
→ {Baustein-Titel: {sub_norm: fakten-dict}} oder None bei Abbruch/Fehler."""
|
||||||
|
topic, provider, is_cancelled = ctx.topic, ctx.provider, ctx.is_cancelled
|
||||||
|
arbeit = files["arbeit"]
|
||||||
|
caps = "files" if ordner else "full"
|
||||||
|
typ = q.get("type", "thema")
|
||||||
|
source = _prompt(_QUELLE_TEMPLATE[typ], project=ordner) if typ in _QUELLE_TEMPLATE else _prompt("Bausteine-Quelle-Thema", topic=topic)
|
||||||
|
bausteine = [(titel, [str(s).strip() for s in subs if str(s).strip()]) for titel, subs in roh.items() if subs]
|
||||||
|
if not bausteine:
|
||||||
|
return {}
|
||||||
|
chunks = _lpt_chunks([len(subs) for _, subs in bausteine], FAKTEN_CHUNK_SUBS)
|
||||||
|
|
||||||
|
def roh_path(ci): return arbeit / f"fakten-c{ci}.json"
|
||||||
|
def chk_path(ci, j): return arbeit / f"fakten-check-c{ci}-j{j}.json"
|
||||||
|
def fix_path(ci): return arbeit / f"fakten-fix-c{ci}.json"
|
||||||
|
def ctitel(idxs): return [bausteine[i][0] for i in idxs]
|
||||||
|
def block_text(idxs):
|
||||||
|
return "\n\n".join(
|
||||||
|
f"BAUSTEIN: {bausteine[i][0]}\nSUBBAUSTEINE:\n" + "\n".join(f"- {s}" for s in bausteine[i][1])
|
||||||
|
for i in idxs)
|
||||||
|
|
||||||
|
# Roh-Fakten eines Chunks → {Baustein: {sub_norm: {sub, …felder}}}, auf Chunk-Titel gematcht.
|
||||||
|
def roh_map(ci, path):
|
||||||
|
idxs = chunks[ci]
|
||||||
|
rel_by = {bausteine[i][0]: bausteine[i][1] for i in idxs}
|
||||||
|
ct = ctitel(idxs)
|
||||||
|
out: dict[str, dict] = {}
|
||||||
|
for e in _fakten_schema(_json_datei(path)) or []:
|
||||||
|
bt = _match_sub(e["baustein"], ct)
|
||||||
|
if bt not in rel_by:
|
||||||
|
continue
|
||||||
|
sub = _match_sub(e["subbaustein"], rel_by[bt])
|
||||||
|
out.setdefault(bt, {})[_norm_titel(sub)] = {"sub": sub, **{k: e[k] for k in _FAKTEN_FELDER}}
|
||||||
|
return out
|
||||||
|
|
||||||
|
# Phase „Fakten finden": 1 Generator je Chunk.
|
||||||
|
async def _finde(ci, idxs):
|
||||||
|
fp = roh_path(ci)
|
||||||
|
if _fakten_schema(_json_datei(fp)):
|
||||||
|
return True
|
||||||
|
subs_total = sum(len(bausteine[i][1]) for i in idxs)
|
||||||
|
status, _r = await run_single_slot(
|
||||||
|
ctx, f"Fakten {ci}", key=f"bausteine-{topic}-fakten-c{ci}",
|
||||||
|
prompt=_prompt("Fakten-Recherche", topic=topic, source=source, bausteine=block_text(idxs), out_path=fp, extra=_extra(instructions)),
|
||||||
|
role="guide", capabilities=caps,
|
||||||
|
payload=lambda result, p=fp: _fakten_schema(_json_datei(p)),
|
||||||
|
timeout=_timeout("inhalt", subs_total))
|
||||||
|
return status != FAILED and _fakten_schema(_json_datei(fp)) is not None
|
||||||
|
|
||||||
|
oks = await _gather_fortschritt([_finde(ci, idxs) for ci, idxs in enumerate(chunks)], len(chunks), _melde_p(set_p, topic, "Fakten finden"))
|
||||||
|
if is_cancelled():
|
||||||
|
return None
|
||||||
|
if not any(ok is True for ok in oks):
|
||||||
|
_bausteine_errors[topic] = "Fakten-Extraktion fehlgeschlagen"
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Phase „Fakten prüfen": FAKTEN_CHECK_PANEL Judges je Chunk → beanstandete sub_norms (Mehrheit).
|
||||||
|
async def _pruefe(ci, idxs):
|
||||||
|
per = roh_map(ci, roh_path(ci))
|
||||||
|
if not per:
|
||||||
|
return ci, set()
|
||||||
|
fakten_text = "\n\n".join(f"SUBBAUSTEIN: {fk['sub']}\n{_fakten_zeilen(fk)}" for fm in per.values() for fk in fm.values())
|
||||||
|
offen = [j for j in (1, 2, 3)[:FAKTEN_CHECK_PANEL] if _fakten_check_schema(_json_datei(chk_path(ci, j))) is None]
|
||||||
|
await asyncio.gather(*[
|
||||||
|
run_agent(f"bausteine-{topic}-fakten-check-c{ci}-j{j}",
|
||||||
|
_prompt("Fakten-Check", topic=topic, source=source, fakten=fakten_text, out_path=chk_path(ci, j), extra=_extra(instructions)),
|
||||||
|
_timeout("inhalt_check", len(per)), provider=provider, role="judge", capabilities=caps)
|
||||||
|
for j in offen], return_exceptions=True)
|
||||||
|
outs = [s for j in (1, 2, 3)[:FAKTEN_CHECK_PANEL] if (s := _fakten_check_schema(_json_datei(chk_path(ci, j)))) is not None]
|
||||||
|
votes: dict[str, int] = {}
|
||||||
|
for s in outs:
|
||||||
|
for sn in set(s):
|
||||||
|
votes[sn] = votes.get(sn, 0) + 1
|
||||||
|
schwelle = len(outs) / 2 if outs else 99
|
||||||
|
return ci, {sn for sn, v in votes.items() if v > schwelle}
|
||||||
|
|
||||||
|
pruef = await _gather_fortschritt([_pruefe(ci, idxs) for ci, idxs in enumerate(chunks)], len(chunks), _melde_p(set_p, topic, "Fakten prüfen"))
|
||||||
|
if is_cancelled():
|
||||||
|
return None
|
||||||
|
beanstandet = {ci: subs for r in pruef if isinstance(r, tuple) for ci, subs in [r]}
|
||||||
|
|
||||||
|
# Phase „Fakten fix": je Chunk mit beanstandeten Subs → diese neu extrahieren (frische Quelle).
|
||||||
|
n_problem = sum(len(s) for s in beanstandet.values())
|
||||||
|
if n_problem:
|
||||||
|
set_p(f"Fakten korrigieren ({n_problem})…", step=_step_idx(topic, "Fakten fix"))
|
||||||
|
async def _fix(ci):
|
||||||
|
subs_norm = beanstandet.get(ci, set())
|
||||||
|
if not subs_norm or _fakten_schema(_json_datei(fix_path(ci))):
|
||||||
|
return
|
||||||
|
idxs = chunks[ci]
|
||||||
|
rel_by = {bausteine[i][0]: bausteine[i][1] for i in idxs}
|
||||||
|
# nur die beanstandeten Subs als Block
|
||||||
|
ziel = []
|
||||||
|
for bt, subs in rel_by.items():
|
||||||
|
betroffen = [s for s in subs if _norm_titel(s) in subs_norm]
|
||||||
|
if betroffen:
|
||||||
|
ziel.append(f"BAUSTEIN: {bt}\nSUBBAUSTEINE:\n" + "\n".join(f"- {s}" for s in betroffen))
|
||||||
|
if not ziel:
|
||||||
|
return
|
||||||
|
await run_single_slot(
|
||||||
|
ctx, f"Fakten-Fix {ci}", key=f"bausteine-{topic}-fakten-fix-c{ci}",
|
||||||
|
prompt=_prompt("Fakten-Recherche", topic=topic, source=source, bausteine="\n\n".join(ziel), out_path=fix_path(ci), extra=_extra(instructions)),
|
||||||
|
role="guide", capabilities=caps,
|
||||||
|
payload=lambda result, p=fix_path(ci): _fakten_schema(_json_datei(p)),
|
||||||
|
timeout=_timeout("inhalt", len(subs_norm)))
|
||||||
|
await _gather_fortschritt([_fix(ci) for ci in beanstandet], len(beanstandet), _melde_p(set_p, topic, "Fakten fix"))
|
||||||
|
if is_cancelled():
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Zusammensetzen: roh + Fix-Overrides für beanstandete (Vollständigkeits-Guard: fehlt der Fix, bleibt roh).
|
||||||
|
ergebnis: dict[str, dict] = {}
|
||||||
|
for ci in range(len(chunks)):
|
||||||
|
per = roh_map(ci, roh_path(ci))
|
||||||
|
fix = roh_map(ci, fix_path(ci)) if fix_path(ci).exists() else {}
|
||||||
|
for bt, fm in per.items():
|
||||||
|
for sn, fk in fm.items():
|
||||||
|
gewinner = fix.get(bt, {}).get(sn, fk) if sn in beanstandet.get(ci, set()) else fk
|
||||||
|
ergebnis.setdefault(bt, {})[sn] = {k: gewinner[k] for k in _FAKTEN_FELDER}
|
||||||
|
return ergebnis
|
||||||
|
|
||||||
|
|
||||||
async def _relevanz_block(ctx: GenContext, set_p, files: dict, sidecar: dict, instructions: str) -> dict | None:
|
async def _relevanz_block(ctx: GenContext, set_p, files: dict, sidecar: dict, instructions: str) -> dict | None:
|
||||||
"""Block D: drei Phasen mit Barriere — Finden (relevant/rand), Wählen (Vote), Klären.
|
"""Block D: drei Phasen mit Barriere — Finden (relevant/rand), Wählen (Vote), Klären.
|
||||||
Items aus der Sidecar; lokale IDs 1..n pro Paket → globale gid.
|
Items aus der Sidecar; lokale IDs 1..n pro Paket → globale gid.
|
||||||
@@ -963,18 +1202,17 @@ async def _frage_muster_block(ctx: GenContext, set_p, files: dict, sidecar: dict
|
|||||||
"""Block E (10er-Chunks): Finden (1 Generator je ~10 Bausteine, parallel), Wählen (Code:
|
"""Block E (10er-Chunks): Finden (1 Generator je ~10 Bausteine, parallel), Wählen (Code:
|
||||||
je Baustein gruppieren + Dedup), Klären (1 Kritiker je Chunk), Prüfen (Nachrunde).
|
je Baustein gruppieren + Dedup), Klären (1 Kritiker je Chunk), Prüfen (Nachrunde).
|
||||||
Zuordnung je Eintrag über das `baustein`-Feld (Chunk-Datei trägt mehrere Bausteine).
|
Zuordnung je Eintrag über das `baustein`-Feld (Chunk-Datei trägt mehrere Bausteine).
|
||||||
→ {Baustein-Titel: [{subbaustein, typ, frage}, …]} oder None bei Abbruch."""
|
→ {Baustein-Titel: [{subbaustein, frage}, …]} oder None bei Abbruch."""
|
||||||
topic, is_cancelled = ctx.topic, ctx.is_cancelled
|
topic, is_cancelled = ctx.topic, ctx.is_cancelled
|
||||||
arbeit = files["arbeit"]
|
arbeit = files["arbeit"]
|
||||||
# Je Baustein nur RELEVANTE Subbausteine (relevanz != 'rand'; None zählt als relevant).
|
# ALLE Subbausteine (auch rand) bekommen ein Muster — Rand ist in der FGuide-Ebene prüfbar.
|
||||||
bausteine = []
|
bausteine = []
|
||||||
for titel, subs in sidecar.items():
|
for titel, subs in sidecar.items():
|
||||||
rel = [s["titel"] for s in subs if isinstance(s, dict) and s.get("relevanz") != "rand" and str(s.get("titel", "")).strip()]
|
alle = [s["titel"] for s in subs if isinstance(s, dict) and str(s.get("titel", "")).strip()]
|
||||||
if rel:
|
if alle:
|
||||||
bausteine.append((titel, rel))
|
bausteine.append((titel, alle))
|
||||||
if not bausteine:
|
if not bausteine:
|
||||||
return {}
|
return {}
|
||||||
typen_block = "\n".join(f"- {k}: {v}" for k, v in FRAGETYPEN.items())
|
|
||||||
chunks = _lpt_chunks([len(rel) for _, rel in bausteine], FRAGE_CHUNK_SUBS) # lastbalanciert nach Sub-Zahl
|
chunks = _lpt_chunks([len(rel) for _, rel in bausteine], FRAGE_CHUNK_SUBS) # lastbalanciert nach Sub-Zahl
|
||||||
|
|
||||||
def roh_path(ci):
|
def roh_path(ci):
|
||||||
@@ -1000,7 +1238,7 @@ async def _frage_muster_block(ctx: GenContext, set_p, files: dict, sidecar: dict
|
|||||||
ctx, f"Frage-Muster {ci}",
|
ctx, f"Frage-Muster {ci}",
|
||||||
key=f"bausteine-{topic}-frage-muster-c{ci}",
|
key=f"bausteine-{topic}-frage-muster-c{ci}",
|
||||||
prompt=_prompt("Frage-Muster-Recherche", topic=topic, bausteine=block,
|
prompt=_prompt("Frage-Muster-Recherche", topic=topic, bausteine=block,
|
||||||
typen=typen_block, out_path=fp, extra=_extra(instructions)),
|
out_path=fp, extra=_extra(instructions)),
|
||||||
role="fast", capabilities="files",
|
role="fast", capabilities="files",
|
||||||
payload=lambda result, p=fp: _frage_muster_chunk_schema(_json_datei(p)),
|
payload=lambda result, p=fp: _frage_muster_chunk_schema(_json_datei(p)),
|
||||||
timeout=_timeout("frage_muster", subs_total),
|
timeout=_timeout("frage_muster", subs_total),
|
||||||
@@ -1027,13 +1265,12 @@ async def _frage_muster_block(ctx: GenContext, set_p, files: dict, sidecar: dict
|
|||||||
titel = _match_sub(e["baustein"], ctitel)
|
titel = _match_sub(e["baustein"], ctitel)
|
||||||
if titel not in rel_by:
|
if titel not in rel_by:
|
||||||
continue # nicht zuordenbar → verwerfen
|
continue # nicht zuordenbar → verwerfen
|
||||||
norm = _norm_frage(e["frage"])
|
sub = _match_sub(e["subbaustein"], rel_by[titel])
|
||||||
seen = gesehen.setdefault(titel, set())
|
seen = gesehen.setdefault(titel, set())
|
||||||
if norm in seen:
|
if sub in seen:
|
||||||
continue
|
continue # genau ein Muster je Subbaustein
|
||||||
seen.add(norm)
|
seen.add(sub)
|
||||||
out.setdefault(titel, []).append(
|
out.setdefault(titel, []).append({"subbaustein": sub, "frage": e["frage"]})
|
||||||
{"subbaustein": _match_sub(e["subbaustein"], rel_by[titel]), "typ": e["typ"], "frage": e["frage"]})
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
def _waehle_all(ci_list):
|
def _waehle_all(ci_list):
|
||||||
@@ -1057,7 +1294,7 @@ async def _frage_muster_block(ctx: GenContext, set_p, files: dict, sidecar: dict
|
|||||||
eintraege = roh_by_titel.get(t) or []
|
eintraege = roh_by_titel.get(t) or []
|
||||||
if not eintraege:
|
if not eintraege:
|
||||||
continue
|
continue
|
||||||
zeilen = "\n".join(f"- [{e['typ']}] ({e['subbaustein']}) {e['frage']}" for e in eintraege)
|
zeilen = "\n".join(f"- ({e['subbaustein']}) {e['frage']}" for e in eintraege)
|
||||||
bloecke.append(f"BAUSTEIN: {t}\n{zeilen}")
|
bloecke.append(f"BAUSTEIN: {t}\n{zeilen}")
|
||||||
if not bloecke:
|
if not bloecke:
|
||||||
return # nichts zu klären in diesem Chunk
|
return # nichts zu klären in diesem Chunk
|
||||||
@@ -1093,7 +1330,7 @@ async def _frage_muster_block(ctx: GenContext, set_p, files: dict, sidecar: dict
|
|||||||
if titel not in rel_by:
|
if titel not in rel_by:
|
||||||
continue
|
continue
|
||||||
out.setdefault(titel, []).append(
|
out.setdefault(titel, []).append(
|
||||||
{"subbaustein": _match_sub(e["subbaustein"], rel_by[titel]), "typ": e["typ"], "frage": e["frage"]})
|
{"subbaustein": _match_sub(e["subbaustein"], rel_by[titel]), "frage": e["frage"]})
|
||||||
return out
|
return out
|
||||||
|
|
||||||
final_by_titel = _final_by_titel(range(len(chunks)))
|
final_by_titel = _final_by_titel(range(len(chunks)))
|
||||||
@@ -1415,6 +1652,226 @@ async def _klaere_inventar(ctx: GenContext, set_p, files: dict) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# --- Gliederung (Bausteine-Artefakt: Kapitel-Struktur, vom Guide nur gelesen) ---
|
||||||
|
|
||||||
|
def _gliederung_komplett(files: dict) -> bool:
|
||||||
|
"""Gliederung steht (Kapitel-Liste vorhanden)?"""
|
||||||
|
d = _json_datei(files["gliederung"])
|
||||||
|
return isinstance(d, dict) and isinstance(d.get("kapitel"), list) and bool(d.get("kapitel"))
|
||||||
|
|
||||||
|
|
||||||
|
def _gliederung_schema(data, valid: set[int]):
|
||||||
|
"""{"kapitel":[{titel,nummern}]} → bereinigt (valide Nummern, je genau einmal) ·
|
||||||
|
None bei <80 % Abdeckung (Agent/Judge hat zu viel weggelassen)."""
|
||||||
|
if not isinstance(data, dict) or not isinstance(data.get("kapitel"), list):
|
||||||
|
return None
|
||||||
|
out, seen = [], set()
|
||||||
|
for ch in data["kapitel"]:
|
||||||
|
if not isinstance(ch, dict):
|
||||||
|
continue
|
||||||
|
titel = str(ch.get("titel", "")).strip() or "Kapitel"
|
||||||
|
nums = []
|
||||||
|
for n in (ch.get("nummern") or []):
|
||||||
|
try:
|
||||||
|
n = int(n)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
continue
|
||||||
|
if n in valid and n not in seen:
|
||||||
|
seen.add(n)
|
||||||
|
nums.append(n)
|
||||||
|
if nums:
|
||||||
|
out.append({"titel": titel, "nummern": nums})
|
||||||
|
if not out or len(seen) < 0.8 * len(valid):
|
||||||
|
return None
|
||||||
|
return {"kapitel": out}
|
||||||
|
|
||||||
|
|
||||||
|
async def _gliederung_block(ctx: GenContext, set_p, files: dict, entries: dict, instructions: str) -> dict:
|
||||||
|
"""Format-agnostische Gliederung über ALLE Bausteine — 3 Vorschläge → Judge merged.
|
||||||
|
Bricht nie ab: 0 gültige → ein Kapitel mit allem; fehlende Bausteine landen in „Weitere".
|
||||||
|
→ {"kapitel":[{titel,nummern}]} (auch in files["gliederung"])."""
|
||||||
|
topic, is_cancelled = ctx.topic, ctx.is_cancelled
|
||||||
|
valid = set(entries)
|
||||||
|
liste = "\n".join(f"{n}. {t}" for n, t in entries.items())
|
||||||
|
step = _step_idx(topic, "Gliederung")
|
||||||
|
set_p("Gliederung — Vorschläge…", step=step)
|
||||||
|
|
||||||
|
async def _vorschlag(i, path):
|
||||||
|
if _gliederung_schema(_json_datei(path), valid):
|
||||||
|
return True
|
||||||
|
await run_single_slot(
|
||||||
|
ctx, f"Gliederung {i}", key=f"bausteine-{topic}-gliederung-{i}",
|
||||||
|
prompt=_prompt("Guide-Gliederung", topic=topic, bausteine=liste, out_path=path, extra=_extra(instructions)),
|
||||||
|
role="guide", capabilities="files",
|
||||||
|
payload=lambda result, p=path: _gliederung_schema(_json_datei(p), valid),
|
||||||
|
timeout=_timeout("plan", len(entries)))
|
||||||
|
return _gliederung_schema(_json_datei(path), valid) is not None
|
||||||
|
|
||||||
|
slots = files["gliederung_slots"]
|
||||||
|
await _gather_fortschritt([_vorschlag(i, p) for i, p in enumerate(slots, 1)], len(slots), _melde_p(set_p, topic, "Gliederung"))
|
||||||
|
if is_cancelled():
|
||||||
|
return {}
|
||||||
|
vorschlaege = [v for p in slots if (v := _gliederung_schema(_json_datei(p), valid))]
|
||||||
|
|
||||||
|
if not vorschlaege:
|
||||||
|
plan = {"kapitel": [{"titel": "Inhalte", "nummern": list(entries)}]}
|
||||||
|
elif len(vorschlaege) == 1:
|
||||||
|
plan = vorschlaege[0]
|
||||||
|
else:
|
||||||
|
set_p("Gliederung zusammenführen…", step=step)
|
||||||
|
bloecke = "\n\n".join(
|
||||||
|
f"### Vorschlag {i}\n" + "\n".join(
|
||||||
|
f"KAPITEL: {ch['titel']}\n Nummern: {', '.join(str(n) for n in ch['nummern'])}" for ch in v["kapitel"])
|
||||||
|
for i, v in enumerate(vorschlaege, 1))
|
||||||
|
await run_single_slot(
|
||||||
|
ctx, "Gliederung-Judge", key=f"bausteine-{topic}-gliederung-judge",
|
||||||
|
prompt=_prompt("Guide-Gliederung-Judge", topic=topic, format_name="den Guide",
|
||||||
|
zweck="alle Bausteine in einem roten Faden", n=len(vorschlaege),
|
||||||
|
bausteine=liste, gliederungen=bloecke, out_path=files["gliederung"], extra=_extra(instructions)),
|
||||||
|
role="judge", capabilities="files",
|
||||||
|
payload=lambda result: _gliederung_schema(_json_datei(files["gliederung"]), valid),
|
||||||
|
timeout=_timeout("plan_judge", len(entries)))
|
||||||
|
plan = _gliederung_schema(_json_datei(files["gliederung"]), valid) or vorschlaege[0]
|
||||||
|
|
||||||
|
# Vollständigkeit: jeder Baustein kommt vor — fehlende in „Weitere" (gegen weglassende Agenten/Judge).
|
||||||
|
drin = {n for ch in plan["kapitel"] for n in ch["nummern"]}
|
||||||
|
fehlen = [n for n in entries if n not in drin]
|
||||||
|
if fehlen:
|
||||||
|
plan["kapitel"].append({"titel": "Weitere", "nummern": fehlen})
|
||||||
|
atomic_write_json(files["gliederung"], plan, indent=1)
|
||||||
|
return plan
|
||||||
|
|
||||||
|
|
||||||
|
# --- Lern-Artefakte (Karteikarten/Beispiele/Diagramme aus den Fakten) ---
|
||||||
|
|
||||||
|
def _karten_schema(data):
|
||||||
|
"""{"karten":[{baustein,subbaustein,frage,antwort}]} → Liste (auch leer) · None bei kaputt."""
|
||||||
|
if not isinstance(data, dict) or not isinstance(data.get("karten"), list):
|
||||||
|
return None
|
||||||
|
out = []
|
||||||
|
for e in data["karten"]:
|
||||||
|
if isinstance(e, dict) and (f := str(e.get("frage", "")).strip()) and (a := str(e.get("antwort", "")).strip()):
|
||||||
|
out.append({"baustein": str(e.get("baustein", "")).strip(), "subbaustein": str(e.get("subbaustein", "")).strip(),
|
||||||
|
"frage": f, "antwort": a})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _beispiel_schema(data):
|
||||||
|
"""{"beispiele":[{baustein,subbaustein,problem,schritte,ergebnis}]} → Liste (auch leer) · None bei kaputt."""
|
||||||
|
if not isinstance(data, dict) or not isinstance(data.get("beispiele"), list):
|
||||||
|
return None
|
||||||
|
out = []
|
||||||
|
for e in data["beispiele"]:
|
||||||
|
if not isinstance(e, dict):
|
||||||
|
continue
|
||||||
|
problem = str(e.get("problem", "")).strip()
|
||||||
|
schritte = [s for x in (e.get("schritte") or []) if (s := str(x).strip())]
|
||||||
|
if problem and schritte:
|
||||||
|
out.append({"baustein": str(e.get("baustein", "")).strip(), "subbaustein": str(e.get("subbaustein", "")).strip(),
|
||||||
|
"problem": problem, "schritte": schritte, "ergebnis": str(e.get("ergebnis", "")).strip()})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _diagramm_schema(data):
|
||||||
|
"""{"diagramme":[{baustein,mermaid}]} → Liste (auch leer) · None bei kaputt."""
|
||||||
|
if not isinstance(data, dict) or not isinstance(data.get("diagramme"), list):
|
||||||
|
return None
|
||||||
|
out = []
|
||||||
|
for e in data["diagramme"]:
|
||||||
|
if isinstance(e, dict) and (m := str(e.get("mermaid", "")).strip()):
|
||||||
|
out.append({"baustein": str(e.get("baustein", "")).strip(), "mermaid": m})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
_ARTEFAKT_SCHEMA = {"karteikarte": _karten_schema, "beispiel": _beispiel_schema, "diagramm": _diagramm_schema}
|
||||||
|
_ARTEFAKT_PROMPT = {"karteikarte": "Artefakt-Karteikarte", "beispiel": "Artefakt-Beispiel", "diagramm": "Artefakt-Diagramm"}
|
||||||
|
_ARTEFAKT_SCHRITT = {"karteikarte": "Karteikarten", "beispiel": "Beispiele", "diagramm": "Diagramme"}
|
||||||
|
|
||||||
|
|
||||||
|
def _artefakte_komplett(files: dict) -> bool:
|
||||||
|
"""Artefakt-Map steht (alle Typen erzeugt)? Werte dürfen leer sein (content-aware)."""
|
||||||
|
d = _json_datei(files["artefakte"])
|
||||||
|
return isinstance(d, dict) and all(t in d for t in ARTEFAKT_TYPEN)
|
||||||
|
|
||||||
|
|
||||||
|
async def _artefakte_block(ctx: GenContext, set_p, files: dict, sidecar: dict, instructions: str) -> dict | None:
|
||||||
|
"""Lern-Artefakte je Typ aus den gespeicherten Fakten erzeugen — ein Generierungs-Durchlauf
|
||||||
|
je Typ über Chunks, keine Verifikation (Bonus-Material). → {typ: [eintraege]} (auch in files)."""
|
||||||
|
topic, is_cancelled = ctx.topic, ctx.is_cancelled
|
||||||
|
arbeit = files["arbeit"]
|
||||||
|
# Bausteine mit Subs + Fakten-Zeilen als Input-Block (extract-once aus den Fakten).
|
||||||
|
bausteine = []
|
||||||
|
for btitel, subs in sidecar.items():
|
||||||
|
if not isinstance(subs, list):
|
||||||
|
continue
|
||||||
|
zeilen = []
|
||||||
|
for s in subs:
|
||||||
|
if not isinstance(s, dict) or not (st := str(s.get("titel", "")).strip()):
|
||||||
|
continue
|
||||||
|
fk = s.get("fakten") if isinstance(s.get("fakten"), dict) else {}
|
||||||
|
zeile = f"- {st}"
|
||||||
|
if fk and (fk_text := _fakten_zeilen(fk)):
|
||||||
|
zeile += "\n" + "\n".join(" " + l for l in fk_text.split("\n"))
|
||||||
|
zeilen.append(zeile)
|
||||||
|
if zeilen:
|
||||||
|
bausteine.append((btitel, zeilen))
|
||||||
|
if not bausteine:
|
||||||
|
leer = {t: [] for t in ARTEFAKT_TYPEN}
|
||||||
|
atomic_write_json(files["artefakte"], leer, indent=1)
|
||||||
|
return leer
|
||||||
|
|
||||||
|
chunks = _lpt_chunks([len(z) for _, z in bausteine], FAKTEN_CHUNK_SUBS)
|
||||||
|
def block_text(idxs):
|
||||||
|
return "\n\n".join(f"BAUSTEIN: {bausteine[i][0]}\nSUBBAUSTEINE:\n" + "\n".join(bausteine[i][1]) for i in idxs)
|
||||||
|
|
||||||
|
ergebnis: dict[str, list] = {}
|
||||||
|
for typ in ARTEFAKT_TYPEN:
|
||||||
|
schema = _ARTEFAKT_SCHEMA[typ]
|
||||||
|
def apath(ci, t=typ): return arbeit / f"artefakt-{t}-c{ci}.json"
|
||||||
|
|
||||||
|
async def _gen(ci, idxs, t=typ, schema=schema):
|
||||||
|
p = apath(ci, t)
|
||||||
|
if schema(_json_datei(p)) is not None:
|
||||||
|
return True
|
||||||
|
await run_single_slot(
|
||||||
|
ctx, f"{_ARTEFAKT_SCHRITT[t]} {ci}", key=f"bausteine-{topic}-artefakt-{t}-c{ci}",
|
||||||
|
prompt=_prompt(_ARTEFAKT_PROMPT[t], topic=topic, bausteine=block_text(idxs), out_path=p, extra=_extra(instructions)),
|
||||||
|
role="guide", capabilities="files",
|
||||||
|
payload=lambda result, p=p, schema=schema: schema(_json_datei(p)),
|
||||||
|
timeout=_timeout("inhalt", sum(len(bausteine[i][1]) for i in idxs)))
|
||||||
|
return schema(_json_datei(p)) is not None
|
||||||
|
|
||||||
|
await _gather_fortschritt([_gen(ci, idxs) for ci, idxs in enumerate(chunks)], len(chunks), _melde_p(set_p, topic, _ARTEFAKT_SCHRITT[typ]))
|
||||||
|
if is_cancelled():
|
||||||
|
return None
|
||||||
|
eintraege: list = []
|
||||||
|
for ci in range(len(chunks)):
|
||||||
|
eintraege += schema(_json_datei(apath(ci))) or []
|
||||||
|
ergebnis[typ] = eintraege
|
||||||
|
atomic_write_json(files["artefakte"], ergebnis, indent=1)
|
||||||
|
return ergebnis
|
||||||
|
|
||||||
|
|
||||||
|
async def _mirror_artefakte_db(topic: str, sidecar: dict, artefakte: dict) -> None:
|
||||||
|
"""Artefakte in die DB spiegeln. Karteikarte/Beispiel je Sub (sub_norm), Diagramm je Baustein (sub_norm='')."""
|
||||||
|
await db.delete_sub_artefakte(topic)
|
||||||
|
btitel_list = list(sidecar.keys())
|
||||||
|
for typ in ("karteikarte", "beispiel"):
|
||||||
|
for e in artefakte.get(typ, []):
|
||||||
|
bt = _match_sub(e.get("baustein", ""), btitel_list)
|
||||||
|
bnorm, sn = _norm_titel(bt), _norm_titel(e.get("subbaustein", ""))
|
||||||
|
if not bnorm or not sn:
|
||||||
|
continue
|
||||||
|
daten = json.dumps({k: v for k, v in e.items() if k not in ("baustein", "subbaustein")}, ensure_ascii=False)
|
||||||
|
await db.put_sub_artefakt(topic, bnorm, sn, typ, daten, bt, e.get("subbaustein", ""))
|
||||||
|
for e in artefakte.get("diagramm", []):
|
||||||
|
bt = _match_sub(e.get("baustein", ""), btitel_list)
|
||||||
|
if not (bnorm := _norm_titel(bt)):
|
||||||
|
continue
|
||||||
|
daten = json.dumps({"mermaid": e.get("mermaid", "")}, ensure_ascii=False)
|
||||||
|
await db.put_sub_artefakt(topic, bnorm, "", "diagramm", daten, bt, "")
|
||||||
|
|
||||||
|
|
||||||
async def _mirror_sidecar_db(topic: str, sidecar: dict) -> None:
|
async def _mirror_sidecar_db(topic: str, sidecar: dict) -> None:
|
||||||
"""Sidecar {Baustein-Titel: [{titel, stufe, relevanz}]} in die DB-Tabelle subbausteine spiegeln."""
|
"""Sidecar {Baustein-Titel: [{titel, stufe, relevanz}]} in die DB-Tabelle subbausteine spiegeln."""
|
||||||
for btitel, subs in sidecar.items():
|
for btitel, subs in sidecar.items():
|
||||||
@@ -1428,12 +1885,14 @@ async def _mirror_sidecar_db(topic: str, sidecar: dict) -> None:
|
|||||||
sn = _norm_titel(st)
|
sn = _norm_titel(st)
|
||||||
if not sn:
|
if not sn:
|
||||||
continue
|
continue
|
||||||
|
fakten = json.dumps(s["fakten"], ensure_ascii=False) if isinstance(s.get("fakten"), dict) else None
|
||||||
await db.put_subbaustein(topic, bnorm, sn, btitel, st,
|
await db.put_subbaustein(topic, bnorm, sn, btitel, st,
|
||||||
stufe=s.get("stufe"), relevanz=s.get("relevanz"), status="konsens")
|
stufe=s.get("stufe"), relevanz=s.get("relevanz"),
|
||||||
|
fakten=fakten, status="konsens")
|
||||||
|
|
||||||
|
|
||||||
async def _mirror_frage_muster_db(topic: str, muster: dict) -> None:
|
async def _mirror_frage_muster_db(topic: str, muster: dict) -> None:
|
||||||
"""Frage-Muster {Baustein-Titel: [{subbaustein, typ, frage}]} in die DB-Tabelle frage_muster spiegeln."""
|
"""Frage-Muster {Baustein-Titel: [{subbaustein, frage}]} in die DB-Tabelle frage_muster spiegeln."""
|
||||||
await db.delete_frage_muster(topic)
|
await db.delete_frage_muster(topic)
|
||||||
for btitel, eintraege in muster.items():
|
for btitel, eintraege in muster.items():
|
||||||
bnorm = _norm_titel(btitel)
|
bnorm = _norm_titel(btitel)
|
||||||
@@ -1444,19 +1903,22 @@ async def _mirror_frage_muster_db(topic: str, muster: dict) -> None:
|
|||||||
continue
|
continue
|
||||||
sub = str(e.get("subbaustein", "")).strip()
|
sub = str(e.get("subbaustein", "")).strip()
|
||||||
sn = _norm_titel(sub)
|
sn = _norm_titel(sub)
|
||||||
typ = str(e.get("typ", "")).strip()
|
|
||||||
frage = str(e.get("frage", "")).strip()
|
frage = str(e.get("frage", "")).strip()
|
||||||
if not (sn and typ and frage):
|
if not (sn and frage):
|
||||||
continue
|
continue
|
||||||
await db.upsert_frage_muster(topic, bnorm, sn, btitel, sub, typ, frage)
|
await db.upsert_frage_muster(topic, bnorm, sn, btitel, sub, frage)
|
||||||
|
|
||||||
|
|
||||||
async def _reset_db_ab_phase(topic: str, label: str) -> None:
|
async def _reset_db_ab_phase(topic: str, label: str) -> None:
|
||||||
"""DB-Inhalt der Phasen ≥ `label` verwerfen (kanonische Reihenfolge Quelle…Fragen)."""
|
"""DB-Inhalt der Phasen ≥ `label` verwerfen (kanonische Reihenfolge Quelle…Artefakte)."""
|
||||||
idx = _phase_idx(label)
|
idx = _phase_idx(label)
|
||||||
if idx <= 5: # Fragen
|
if idx <= 8: # Artefakte (Karteikarten/Beispiele/Diagramme)
|
||||||
|
await db.delete_sub_artefakte(topic)
|
||||||
|
if idx <= 7: # Fragen
|
||||||
await db.delete_frage_muster(topic)
|
await db.delete_frage_muster(topic)
|
||||||
if idx <= 2: # Subbausteine (Stufen/Relevanz greifen über Sidecar→Mirror)
|
if idx <= 6: # Gliederung
|
||||||
|
await db.delete_gliederung(topic)
|
||||||
|
if idx <= 2: # Subbausteine (Fakten/Stufen/Relevanz greifen über Sidecar→Mirror)
|
||||||
await db.delete_subbausteine(topic)
|
await db.delete_subbausteine(topic)
|
||||||
if idx <= 1: # Inventar: Inventar + Recherche-Schritte — Sichtung bleibt
|
if idx <= 1: # Inventar: Inventar + Recherche-Schritte — Sichtung bleibt
|
||||||
await db.delete_bausteine(topic)
|
await db.delete_bausteine(topic)
|
||||||
@@ -1519,6 +1981,8 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
|
|||||||
await db.delete_subbausteine(topic)
|
await db.delete_subbausteine(topic)
|
||||||
await db.delete_frage_muster(topic)
|
await db.delete_frage_muster(topic)
|
||||||
await db.delete_coverage(topic)
|
await db.delete_coverage(topic)
|
||||||
|
await db.delete_gliederung(topic)
|
||||||
|
await db.delete_sub_artefakte(topic)
|
||||||
|
|
||||||
# Inventar (DB): Recherche-Loop → Konsolidierung → Klärung.
|
# Inventar (DB): Recherche-Loop → Konsolidierung → Klärung.
|
||||||
if not await _recherche_batch(ctx, set_p, files, q, ordner, instructions):
|
if not await _recherche_batch(ctx, set_p, files, q, ordner, instructions):
|
||||||
@@ -1590,12 +2054,30 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
|
|||||||
if roh is None:
|
if roh is None:
|
||||||
return # Fehler ist gesetzt
|
return # Fehler ist gesetzt
|
||||||
atomic_write_json(files["sub_roh"], roh, indent=1)
|
atomic_write_json(files["sub_roh"], roh, indent=1)
|
||||||
|
# Fakten je Sub (VOR der Stufe): Quell-Fakten extrahieren + verifizieren → fakten.json.
|
||||||
|
# Extract-once-Grounding — Stufe/Relevanz/Fragen/Guide nähren sich daraus.
|
||||||
|
if not _fakten_komplett(files):
|
||||||
|
fakten_map = await _fakten_block(ctx, set_p, files, roh, q, ordner, instructions)
|
||||||
|
if is_cancelled():
|
||||||
|
abgebrochen()
|
||||||
|
return
|
||||||
|
if fakten_map is None:
|
||||||
|
return # Fehler ist gesetzt
|
||||||
|
atomic_write_json(files["fakten"], fakten_map, indent=1)
|
||||||
sidecar = await _stufen_block(ctx, set_p, files, roh, instructions)
|
sidecar = await _stufen_block(ctx, set_p, files, roh, instructions)
|
||||||
if is_cancelled():
|
if is_cancelled():
|
||||||
abgebrochen()
|
abgebrochen()
|
||||||
return
|
return
|
||||||
if sidecar is None:
|
if sidecar is None:
|
||||||
return
|
return
|
||||||
|
# Fakten in die Sidecar-Subs mergen (DB-Spiegel + Guide-Nutzung).
|
||||||
|
fakten_map = _json_datei(files["fakten"])
|
||||||
|
if isinstance(fakten_map, dict):
|
||||||
|
for btitel, subs in sidecar.items():
|
||||||
|
fm = fakten_map.get(btitel, {})
|
||||||
|
for sub in subs:
|
||||||
|
if (fk := fm.get(_norm_titel(sub["titel"]))):
|
||||||
|
sub["fakten"] = fk
|
||||||
atomic_write_json(files["sidecar"], sidecar, indent=1)
|
atomic_write_json(files["sidecar"], sidecar, indent=1)
|
||||||
|
|
||||||
# Block D: Relevanz je Subbaustein (relevant/rand) → in die Sidecar mergen.
|
# Block D: Relevanz je Subbaustein (relevant/rand) → in die Sidecar mergen.
|
||||||
@@ -1616,6 +2098,14 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
|
|||||||
sub["relevanz"] = relevanz_by_id.get(gid, "relevant")
|
sub["relevanz"] = relevanz_by_id.get(gid, "relevant")
|
||||||
atomic_write_json(files["sidecar"], sidecar, indent=1)
|
atomic_write_json(files["sidecar"], sidecar, indent=1)
|
||||||
|
|
||||||
|
# Block D.5: Gliederung (Bausteine-Artefakt) — Kapitel-Struktur über ALLE Bausteine,
|
||||||
|
# vom Guide nur noch gelesen. Format-agnostisch; der Guide filtert je Format.
|
||||||
|
if not _gliederung_komplett(files):
|
||||||
|
await _gliederung_block(ctx, set_p, files, entries, instructions)
|
||||||
|
if is_cancelled():
|
||||||
|
abgebrochen()
|
||||||
|
return
|
||||||
|
|
||||||
# Block E: Frage-Muster je relevantem Subbaustein × Typ → eigenes Sidecar.
|
# Block E: Frage-Muster je relevantem Subbaustein × Typ → eigenes Sidecar.
|
||||||
# Zur Prüfungszeit zieht jeder Agent ein Muster ohne Zurücklegen und formuliert
|
# Zur Prüfungszeit zieht jeder Agent ein Muster ohne Zurücklegen und formuliert
|
||||||
# daraus eine Frage — distinkte Saat verhindert die Doppelfragen der Live-Generierung.
|
# daraus eine Frage — distinkte Saat verhindert die Doppelfragen der Live-Generierung.
|
||||||
@@ -1629,6 +2119,17 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
|
|||||||
return # Abbruch
|
return # Abbruch
|
||||||
atomic_write_json(files["frage_muster"], muster, indent=1)
|
atomic_write_json(files["frage_muster"], muster, indent=1)
|
||||||
|
|
||||||
|
# Block F: Lern-Artefakte (Karteikarten/Beispiele/Diagramme) aus den Fakten — Bonus,
|
||||||
|
# vom Frontend präsentiert. Bricht den Lauf nicht ab (Artefakte sind optional).
|
||||||
|
sidecar = _json_datei(files["sidecar"])
|
||||||
|
if _sidecar_schema(sidecar) is not None and not _artefakte_komplett(files):
|
||||||
|
artefakte = await _artefakte_block(ctx, set_p, files, sidecar, instructions)
|
||||||
|
if is_cancelled():
|
||||||
|
abgebrochen()
|
||||||
|
return
|
||||||
|
if artefakte is None:
|
||||||
|
return # Abbruch (Fehler/Cancel)
|
||||||
|
|
||||||
# DB-Spiegel (Brücke): finalen Sidecar- + Frage-Muster-Stand in die DB schreiben.
|
# DB-Spiegel (Brücke): finalen Sidecar- + Frage-Muster-Stand in die DB schreiben.
|
||||||
sidecar = _json_datei(files["sidecar"])
|
sidecar = _json_datei(files["sidecar"])
|
||||||
if _sidecar_schema(sidecar) is not None:
|
if _sidecar_schema(sidecar) is not None:
|
||||||
@@ -1636,6 +2137,19 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
|
|||||||
muster = _json_datei(files["frage_muster"])
|
muster = _json_datei(files["frage_muster"])
|
||||||
if isinstance(muster, dict) and muster:
|
if isinstance(muster, dict) and muster:
|
||||||
await _mirror_frage_muster_db(topic, muster)
|
await _mirror_frage_muster_db(topic, muster)
|
||||||
|
# Gliederung (Titel-basiert, robust gegen Nummern-Drift) → DB.
|
||||||
|
plan = _json_datei(files["gliederung"])
|
||||||
|
if isinstance(plan, dict) and plan.get("kapitel"):
|
||||||
|
kapitel = [
|
||||||
|
{"titel": ch.get("titel", "Kapitel"),
|
||||||
|
"bausteine": [_titel(entries[n]) for n in ch.get("nummern", []) if n in entries]}
|
||||||
|
for ch in plan["kapitel"]
|
||||||
|
]
|
||||||
|
await db.set_gliederung(topic, json.dumps({"kapitel": kapitel}, ensure_ascii=False))
|
||||||
|
# Artefakte → DB (Karteikarte/Beispiel je Sub, Diagramm je Baustein).
|
||||||
|
artefakte = _json_datei(files["artefakte"])
|
||||||
|
if isinstance(artefakte, dict) and _sidecar_schema(sidecar) is not None:
|
||||||
|
await _mirror_artefakte_db(topic, sidecar, artefakte)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception("[%s] Bausteine-Generierung fehlgeschlagen", topic)
|
log.exception("[%s] Bausteine-Generierung fehlgeschlagen", topic)
|
||||||
_bausteine_errors[topic] = str(e)[:2000]
|
_bausteine_errors[topic] = str(e)[:2000]
|
||||||
|
|||||||
@@ -103,13 +103,15 @@ CREATE TABLE IF NOT EXISTS subbausteine (
|
|||||||
nennungen INTEGER NOT NULL DEFAULT 1,
|
nennungen INTEGER NOT NULL DEFAULT 1,
|
||||||
stufe TEXT,
|
stufe TEXT,
|
||||||
relevanz TEXT,
|
relevanz TEXT,
|
||||||
|
fakten TEXT,
|
||||||
status TEXT NOT NULL DEFAULT 'kandidat',
|
status TEXT NOT NULL DEFAULT 'kandidat',
|
||||||
updated_at TEXT NOT NULL,
|
updated_at TEXT NOT NULL,
|
||||||
PRIMARY KEY (topic, baustein_norm, sub_norm)
|
PRIMARY KEY (topic, baustein_norm, sub_norm)
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Ein Frage-Muster je (Baustein, Subbaustein, Typ).
|
# Ein Frage-Muster je (Baustein, Subbaustein). Schwierigkeit kommt erst bei der Prüfung
|
||||||
|
# aus dem Lerner-Niveau, nicht aus dem Muster — daher kein Typ-Kreuzprodukt mehr.
|
||||||
CREATE_FRAGE_MUSTER = """
|
CREATE_FRAGE_MUSTER = """
|
||||||
CREATE TABLE IF NOT EXISTS frage_muster (
|
CREATE TABLE IF NOT EXISTS frage_muster (
|
||||||
topic TEXT NOT NULL,
|
topic TEXT NOT NULL,
|
||||||
@@ -117,10 +119,9 @@ CREATE TABLE IF NOT EXISTS frage_muster (
|
|||||||
sub_norm TEXT NOT NULL,
|
sub_norm TEXT NOT NULL,
|
||||||
baustein TEXT NOT NULL,
|
baustein TEXT NOT NULL,
|
||||||
sub_titel TEXT NOT NULL,
|
sub_titel TEXT NOT NULL,
|
||||||
typ TEXT NOT NULL,
|
|
||||||
frage TEXT NOT NULL,
|
frage TEXT NOT NULL,
|
||||||
updated_at TEXT NOT NULL,
|
updated_at TEXT NOT NULL,
|
||||||
PRIMARY KEY (topic, baustein_norm, sub_norm, typ)
|
PRIMARY KEY (topic, baustein_norm, sub_norm)
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -170,6 +171,33 @@ CREATE TABLE IF NOT EXISTS quelle (
|
|||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Gliederung (Kapitel → Baustein-Nummern) je Thema als JSON. In der Bausteine-Phase erzeugt,
|
||||||
|
# vom Guide nur noch gelesen + je Format gefiltert. Format-agnostisch (alle Bausteine).
|
||||||
|
CREATE_GUIDE_GLIEDERUNG = """
|
||||||
|
CREATE TABLE IF NOT EXISTS guide_gliederung (
|
||||||
|
topic TEXT PRIMARY KEY,
|
||||||
|
json TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Generischer Lern-Artefakt-Layer: je (Baustein, Subbaustein, Typ) ein Artefakt (JSON in `daten`).
|
||||||
|
# typ: karteikarte | beispiel | diagramm. sub_norm='' = Baustein-Ebene (z. B. Diagramm).
|
||||||
|
# In der Bausteine-Phase aus den Fakten erzeugt, vom Frontend präsentiert (Trennung Artefakt/Anzeige).
|
||||||
|
CREATE_SUB_ARTEFAKTE = """
|
||||||
|
CREATE TABLE IF NOT EXISTS sub_artefakte (
|
||||||
|
topic TEXT NOT NULL,
|
||||||
|
baustein_norm TEXT NOT NULL,
|
||||||
|
sub_norm TEXT NOT NULL DEFAULT '',
|
||||||
|
typ TEXT NOT NULL,
|
||||||
|
baustein TEXT NOT NULL DEFAULT '',
|
||||||
|
sub_titel TEXT NOT NULL DEFAULT '',
|
||||||
|
daten TEXT NOT NULL DEFAULT '{}',
|
||||||
|
updated_at TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (topic, baustein_norm, sub_norm, typ)
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
_db: aiosqlite.Connection | None = None
|
_db: aiosqlite.Connection | None = None
|
||||||
|
|
||||||
|
|
||||||
@@ -199,6 +227,8 @@ async def init_db():
|
|||||||
await db.execute(CREATE_PIPELINE_STATE)
|
await db.execute(CREATE_PIPELINE_STATE)
|
||||||
await db.execute(CREATE_GUIDE_CONTENT)
|
await db.execute(CREATE_GUIDE_CONTENT)
|
||||||
await db.execute(CREATE_QUELLE)
|
await db.execute(CREATE_QUELLE)
|
||||||
|
await db.execute(CREATE_GUIDE_GLIEDERUNG)
|
||||||
|
await db.execute(CREATE_SUB_ARTEFAKTE)
|
||||||
try: # Migration für Bestands-DBs ohne step-Spalte
|
try: # Migration für Bestands-DBs ohne step-Spalte
|
||||||
await db.execute("ALTER TABLE guides ADD COLUMN step INTEGER")
|
await db.execute("ALTER TABLE guides ADD COLUMN step INTEGER")
|
||||||
except aiosqlite.OperationalError:
|
except aiosqlite.OperationalError:
|
||||||
@@ -226,6 +256,16 @@ async def init_db():
|
|||||||
await db.execute(f"ALTER TABLE baustein_progress ADD COLUMN {_spalte} {_typ}")
|
await db.execute(f"ALTER TABLE baustein_progress ADD COLUMN {_spalte} {_typ}")
|
||||||
except aiosqlite.OperationalError:
|
except aiosqlite.OperationalError:
|
||||||
pass
|
pass
|
||||||
|
# Migration: frage_muster ohne typ-Spalte (1 Muster je Sub statt Sub×Typ-Kreuzprodukt).
|
||||||
|
# PK-Änderung → Tabelle einmalig neu bauen. Bestands-Muster gehen verloren (bewusst, kein Mapping).
|
||||||
|
cursor = await db.execute("PRAGMA table_info(frage_muster)")
|
||||||
|
if any(_r[1] == "typ" for _r in await cursor.fetchall()):
|
||||||
|
await db.execute("DROP TABLE frage_muster")
|
||||||
|
await db.execute(CREATE_FRAGE_MUSTER)
|
||||||
|
try: # Migration: subbausteine.fakten (Quell-Fakten je Sub, JSON-Blob) — Extract-once-Grounding.
|
||||||
|
await db.execute("ALTER TABLE subbausteine ADD COLUMN fakten TEXT") # nullable: COALESCE schützt Bestand
|
||||||
|
except aiosqlite.OperationalError:
|
||||||
|
pass
|
||||||
# Migration: alte vertiefungen-Tabelle → baustein_texte (Bestand = lange Form, art 'deepdive')
|
# Migration: alte vertiefungen-Tabelle → baustein_texte (Bestand = lange Form, art 'deepdive')
|
||||||
cursor = await db.execute("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'vertiefungen'")
|
cursor = await db.execute("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'vertiefungen'")
|
||||||
if await cursor.fetchone():
|
if await cursor.fetchone():
|
||||||
@@ -482,6 +522,21 @@ async def set_baustein_score(topic: str, baustein: str, score: int) -> int:
|
|||||||
return score
|
return score
|
||||||
|
|
||||||
|
|
||||||
|
async def set_baustein_score_and_streak(topic: str, baustein: str, score: int, streak: int) -> tuple[int, int]:
|
||||||
|
"""Setzt Score + Streak atomar (vom Aufrufer geclampt). Liefert (score, streak)."""
|
||||||
|
db = await get_db()
|
||||||
|
await db.execute(
|
||||||
|
"""INSERT INTO baustein_progress (topic, baustein, gute_antworten, streak, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT(topic, baustein) DO UPDATE SET
|
||||||
|
gute_antworten = excluded.gute_antworten, streak = excluded.streak,
|
||||||
|
updated_at = excluded.updated_at""",
|
||||||
|
(topic, baustein, score, streak, _now()),
|
||||||
|
)
|
||||||
|
await db.commit()
|
||||||
|
return score, streak
|
||||||
|
|
||||||
|
|
||||||
async def delete_baustein_progress(topic: str, baustein: str) -> None:
|
async def delete_baustein_progress(topic: str, baustein: str) -> None:
|
||||||
"""Fortschritt EINES Bausteins zurücksetzen: Zeile löschen (Score/Streak/Flags/offene Frage).
|
"""Fortschritt EINES Bausteins zurücksetzen: Zeile löschen (Score/Streak/Flags/offene Frage).
|
||||||
Fehlt die Zeile, liefert get_baustein_progress Defaults (0) — also voller Reset."""
|
Fehlt die Zeile, liefert get_baustein_progress Defaults (0) — also voller Reset."""
|
||||||
@@ -574,6 +629,75 @@ async def subs_count_alle() -> dict[tuple[str, str], int]:
|
|||||||
return {(t, bn): n for t, bn, n in await cursor.fetchall()}
|
return {(t, bn): n for t, bn, n in await cursor.fetchall()}
|
||||||
|
|
||||||
|
|
||||||
|
# Sub-Ebene aus den zwei orthogonalen Spalten: rand → 4 (V), sonst Stufe (Lernpfad-Position):
|
||||||
|
# anfaenger/NULL → 1, fortgeschritten → 2, experte → 3. Alte Werte (einfach/mittel/schwer) werden
|
||||||
|
# abwärtskompatibel mitgemappt. Steuert Guide-Ansicht A/F/E/V + cap (freigeschaltete Subs × 25).
|
||||||
|
_EBENE_CASE = """CASE
|
||||||
|
WHEN relevanz = 'rand' THEN 4
|
||||||
|
WHEN stufe IN ('fortgeschritten', 'mittel') THEN 2
|
||||||
|
WHEN stufe IN ('experte', 'schwer') THEN 3
|
||||||
|
ELSE 1
|
||||||
|
END"""
|
||||||
|
|
||||||
|
|
||||||
|
def _leere_ebenen() -> dict[int, int]:
|
||||||
|
return {1: 0, 2: 0, 3: 0, 4: 0}
|
||||||
|
|
||||||
|
|
||||||
|
async def subs_je_ebene(topic: str, baustein: str) -> dict[int, int]:
|
||||||
|
"""Konsens-Subbausteine eines Bausteins je Ebene 1–4. roher Baustein-Titel rein."""
|
||||||
|
from textkit import _norm_titel
|
||||||
|
db = await get_db()
|
||||||
|
cursor = await db.execute(
|
||||||
|
f"SELECT {_EBENE_CASE} AS ebene, COUNT(*) FROM subbausteine "
|
||||||
|
"WHERE topic = ? AND baustein_norm = ? AND status = 'konsens' GROUP BY ebene",
|
||||||
|
(topic, _norm_titel(baustein)),
|
||||||
|
)
|
||||||
|
out = _leere_ebenen()
|
||||||
|
for ebene, n in await cursor.fetchall():
|
||||||
|
out[ebene] = n
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
async def subs_je_ebene_roh(topic: str) -> dict[str, dict[int, int]]:
|
||||||
|
"""Subbausteine je Ebene, gruppiert nach ROHEM Baustein-Titel (= Guide-Section-Titel)."""
|
||||||
|
db = await get_db()
|
||||||
|
cursor = await db.execute(
|
||||||
|
f"SELECT baustein, {_EBENE_CASE} AS ebene, COUNT(*) FROM subbausteine "
|
||||||
|
"WHERE topic = ? AND status = 'konsens' GROUP BY baustein, ebene",
|
||||||
|
(topic,),
|
||||||
|
)
|
||||||
|
out: dict[str, dict[int, int]] = {}
|
||||||
|
for b, ebene, n in await cursor.fetchall():
|
||||||
|
out.setdefault(b, _leere_ebenen())[ebene] = n
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
async def subs_je_ebene_alle() -> dict[tuple[str, str], dict[int, int]]:
|
||||||
|
"""Subbausteine je Ebene je (topic, baustein_norm) — für die themenweite Stufen-Ableitung."""
|
||||||
|
db = await get_db()
|
||||||
|
cursor = await db.execute(
|
||||||
|
f"SELECT topic, baustein_norm, {_EBENE_CASE} AS ebene, COUNT(*) FROM subbausteine "
|
||||||
|
"WHERE status = 'konsens' GROUP BY topic, baustein_norm, ebene"
|
||||||
|
)
|
||||||
|
out: dict[tuple[str, str], dict[int, int]] = {}
|
||||||
|
for t, bn, ebene, n in await cursor.fetchall():
|
||||||
|
out.setdefault((t, bn), _leere_ebenen())[ebene] = n
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
async def subs_mit_ebene(topic: str, baustein: str) -> list[dict]:
|
||||||
|
"""Konsens-Subbausteine eines Bausteins mit Titel, sub_norm und Ebene 1–4."""
|
||||||
|
from textkit import _norm_titel
|
||||||
|
db = await get_db()
|
||||||
|
cursor = await db.execute(
|
||||||
|
f"SELECT sub_titel, sub_norm, {_EBENE_CASE} AS ebene FROM subbausteine "
|
||||||
|
"WHERE topic = ? AND baustein_norm = ? AND status = 'konsens'",
|
||||||
|
(topic, _norm_titel(baustein)),
|
||||||
|
)
|
||||||
|
return [{"titel": t, "norm": sn, "ebene": e} for t, sn, e in await cursor.fetchall()]
|
||||||
|
|
||||||
|
|
||||||
async def list_baustein_scores_all() -> list[tuple[str, str, int]]:
|
async def list_baustein_scores_all() -> list[tuple[str, str, int]]:
|
||||||
"""(topic, baustein, gute_antworten) je Baustein — Rohdaten für die Stufen-Ableitung."""
|
"""(topic, baustein, gute_antworten) je Baustein — Rohdaten für die Stufen-Ableitung."""
|
||||||
db = await get_db()
|
db = await get_db()
|
||||||
@@ -653,19 +777,21 @@ async def upsert_subbaustein(topic: str, baustein_norm: str, sub_norm: str, baus
|
|||||||
|
|
||||||
|
|
||||||
async def put_subbaustein(topic: str, baustein_norm: str, sub_norm: str, baustein: str, sub_titel: str,
|
async def put_subbaustein(topic: str, baustein_norm: str, sub_norm: str, baustein: str, sub_titel: str,
|
||||||
stufe: str | None = None, relevanz: str | None = None, status: str = "konsens") -> None:
|
stufe: str | None = None, relevanz: str | None = None,
|
||||||
"""Insert/Update OHNE Nennungszähler (Spiegel aus dem Sidecar). stufe/relevanz nur überschreiben,
|
fakten: str | None = None, status: str = "konsens") -> None:
|
||||||
wenn ein neuer Wert übergeben wird (COALESCE schützt Bestehendes)."""
|
"""Insert/Update OHNE Nennungszähler (Spiegel aus dem Sidecar). stufe/relevanz/fakten nur
|
||||||
|
überschreiben, wenn ein neuer Wert übergeben wird (COALESCE schützt Bestehendes)."""
|
||||||
db = await get_db()
|
db = await get_db()
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""INSERT INTO subbausteine (topic, baustein_norm, sub_norm, baustein, sub_titel, nennungen, stufe, relevanz, status, updated_at)
|
"""INSERT INTO subbausteine (topic, baustein_norm, sub_norm, baustein, sub_titel, nennungen, stufe, relevanz, fakten, status, updated_at)
|
||||||
VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT(topic, baustein_norm, sub_norm) DO UPDATE SET
|
ON CONFLICT(topic, baustein_norm, sub_norm) DO UPDATE SET
|
||||||
baustein = excluded.baustein, sub_titel = excluded.sub_titel,
|
baustein = excluded.baustein, sub_titel = excluded.sub_titel,
|
||||||
stufe = COALESCE(excluded.stufe, subbausteine.stufe),
|
stufe = COALESCE(excluded.stufe, subbausteine.stufe),
|
||||||
relevanz = COALESCE(excluded.relevanz, subbausteine.relevanz),
|
relevanz = COALESCE(excluded.relevanz, subbausteine.relevanz),
|
||||||
|
fakten = COALESCE(excluded.fakten, subbausteine.fakten),
|
||||||
status = excluded.status, updated_at = excluded.updated_at""",
|
status = excluded.status, updated_at = excluded.updated_at""",
|
||||||
(topic, baustein_norm, sub_norm, baustein, sub_titel, stufe, relevanz, status, _now()),
|
(topic, baustein_norm, sub_norm, baustein, sub_titel, stufe, relevanz, fakten, status, _now()),
|
||||||
)
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
@@ -700,14 +826,14 @@ async def delete_subbausteine(topic: str) -> None:
|
|||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
|
|
||||||
async def upsert_frage_muster(topic: str, baustein_norm: str, sub_norm: str, baustein: str, sub_titel: str, typ: str, frage: str) -> None:
|
async def upsert_frage_muster(topic: str, baustein_norm: str, sub_norm: str, baustein: str, sub_titel: str, frage: str) -> None:
|
||||||
db = await get_db()
|
db = await get_db()
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""INSERT INTO frage_muster (topic, baustein_norm, sub_norm, baustein, sub_titel, typ, frage, updated_at)
|
"""INSERT INTO frage_muster (topic, baustein_norm, sub_norm, baustein, sub_titel, frage, updated_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT(topic, baustein_norm, sub_norm, typ) DO UPDATE SET
|
ON CONFLICT(topic, baustein_norm, sub_norm) DO UPDATE SET
|
||||||
frage = excluded.frage, updated_at = excluded.updated_at""",
|
sub_titel = excluded.sub_titel, frage = excluded.frage, updated_at = excluded.updated_at""",
|
||||||
(topic, baustein_norm, sub_norm, baustein, sub_titel, typ, frage, _now()),
|
(topic, baustein_norm, sub_norm, baustein, sub_titel, frage, _now()),
|
||||||
)
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
@@ -849,10 +975,90 @@ async def delete_guide_content(topic: str, format: str | None = None) -> None:
|
|||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
async def set_gliederung(topic: str, gliederung_json: str) -> None:
|
||||||
|
"""Gliederung (Kapitel→Nummern, JSON) je Thema speichern — Bausteine-Artefakt für den Guide."""
|
||||||
|
db = await get_db()
|
||||||
|
await db.execute(
|
||||||
|
"""INSERT INTO guide_gliederung (topic, json, updated_at) VALUES (?, ?, ?)
|
||||||
|
ON CONFLICT(topic) DO UPDATE SET json = excluded.json, updated_at = excluded.updated_at""",
|
||||||
|
(topic, gliederung_json, _now()),
|
||||||
|
)
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_gliederung(topic: str) -> str | None:
|
||||||
|
db = await get_db()
|
||||||
|
cursor = await db.execute("SELECT json FROM guide_gliederung WHERE topic = ?", (topic,))
|
||||||
|
row = await cursor.fetchone()
|
||||||
|
return row[0] if row else None
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_gliederung(topic: str) -> None:
|
||||||
|
db = await get_db()
|
||||||
|
await db.execute("DELETE FROM guide_gliederung WHERE topic = ?", (topic,))
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
async def put_sub_artefakt(topic: str, baustein_norm: str, sub_norm: str, typ: str,
|
||||||
|
daten: str, baustein: str = "", sub_titel: str = "") -> None:
|
||||||
|
"""Ein Lern-Artefakt (karteikarte/beispiel/diagramm) als JSON in `daten` speichern."""
|
||||||
|
db = await get_db()
|
||||||
|
await db.execute(
|
||||||
|
"""INSERT INTO sub_artefakte (topic, baustein_norm, sub_norm, typ, baustein, sub_titel, daten, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT(topic, baustein_norm, sub_norm, typ) DO UPDATE SET
|
||||||
|
baustein = excluded.baustein, sub_titel = excluded.sub_titel,
|
||||||
|
daten = excluded.daten, updated_at = excluded.updated_at""",
|
||||||
|
(topic, baustein_norm, sub_norm, typ, baustein, sub_titel, daten, _now()),
|
||||||
|
)
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_sub_artefakte(topic: str, typ: str | None = None) -> list[dict]:
|
||||||
|
db = await get_db()
|
||||||
|
if typ is None:
|
||||||
|
cursor = await db.execute("SELECT * FROM sub_artefakte WHERE topic = ? ORDER BY rowid", (topic,))
|
||||||
|
else:
|
||||||
|
cursor = await db.execute(
|
||||||
|
"SELECT * FROM sub_artefakte WHERE topic = ? AND typ = ? ORDER BY rowid", (topic, typ)
|
||||||
|
)
|
||||||
|
rows = await cursor.fetchall()
|
||||||
|
return [_row_to_dict(row, cursor) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_sub_artefakte(topic: str) -> None:
|
||||||
|
db = await get_db()
|
||||||
|
await db.execute("DELETE FROM sub_artefakte WHERE topic = ?", (topic,))
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_baustein_huerden(topic: str, baustein_norm: str) -> list[str]:
|
||||||
|
"""Typische Irrtümer (huerden) aller Subs eines Bausteins — als Distraktor-Pool fürs Quiz.
|
||||||
|
Liest aus subbausteine.fakten (JSON-Blob); leere/fehlende werden übersprungen."""
|
||||||
|
db = await get_db()
|
||||||
|
cursor = await db.execute(
|
||||||
|
"SELECT fakten FROM subbausteine WHERE topic = ? AND baustein_norm = ?", (topic, baustein_norm)
|
||||||
|
)
|
||||||
|
rows = await cursor.fetchall()
|
||||||
|
huerden = []
|
||||||
|
for (fakten,) in rows:
|
||||||
|
if not fakten:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
fk = json.loads(fakten)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
continue
|
||||||
|
h = (fk.get("huerden") or "").strip() if isinstance(fk, dict) else ""
|
||||||
|
if h:
|
||||||
|
huerden.append(h)
|
||||||
|
return huerden
|
||||||
|
|
||||||
|
|
||||||
async def delete_topic_pipeline(topic: str) -> None:
|
async def delete_topic_pipeline(topic: str) -> None:
|
||||||
"""Bausteine-Bereich eines Themas verwerfen (Inventar/Subs/Muster/Coverage/State).
|
"""Bausteine-Bereich eines Themas verwerfen (Inventar/Subs/Muster/Coverage/State/Artefakte).
|
||||||
NICHT die Themen-Config `quelle` — die wird separat verwaltet (delete_quelle)."""
|
NICHT die Themen-Config `quelle` — die wird separat verwaltet (delete_quelle)."""
|
||||||
db = await get_db()
|
db = await get_db()
|
||||||
for tab in ("bausteine", "subbausteine", "frage_muster", "recherche_coverage", "pipeline_state"):
|
for tab in ("bausteine", "subbausteine", "frage_muster", "recherche_coverage",
|
||||||
|
"pipeline_state", "guide_gliederung", "sub_artefakte"):
|
||||||
await db.execute(f"DELETE FROM {tab} WHERE topic = ?", (topic,))
|
await db.execute(f"DELETE FROM {tab} WHERE topic = ?", (topic,))
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|||||||
186
backend/guide.py
186
backend/guide.py
@@ -24,7 +24,7 @@ from config import (
|
|||||||
LESBARKEIT_AKTIV, TEMPLATES_DIR,
|
LESBARKEIT_AKTIV, TEMPLATES_DIR,
|
||||||
)
|
)
|
||||||
import lesbarkeit
|
import lesbarkeit
|
||||||
from database import list_guides, update_guide, list_bausteine, list_subbausteine, set_guide_content, get_guide_content
|
from database import list_guides, update_guide, list_bausteine, list_subbausteine, set_guide_content, get_guide_content, get_gliederung
|
||||||
from fsutil import atomic_write_json, atomic_write_text
|
from fsutil import atomic_write_json, atomic_write_text
|
||||||
from jsonio import read_json_file as _json_datei, parse_json_text as _parse_json_text
|
from jsonio import read_json_file as _json_datei, parse_json_text as _parse_json_text
|
||||||
from paths import bausteine_path, guide_content_path, project_dir, subbausteine_path
|
from paths import bausteine_path, guide_content_path, project_dir, subbausteine_path
|
||||||
@@ -35,7 +35,7 @@ from pipeline import (
|
|||||||
is_guide_cancelled, run_single_slot,
|
is_guide_cancelled, run_single_slot,
|
||||||
)
|
)
|
||||||
from textkit import (
|
from textkit import (
|
||||||
_eindeutige_titel, _lade_bausteine, _parse_fragment, _split_chunks,
|
_eindeutige_titel, _lade_bausteine, _norm_titel, _parse_fragment, _split_chunks,
|
||||||
_titel, _titel_aufloesen, _titel_index,
|
_titel, _titel_aufloesen, _titel_index,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -57,14 +57,22 @@ CHECK_PANEL = 3
|
|||||||
LESE_RUNDEN = 1
|
LESE_RUNDEN = 1
|
||||||
|
|
||||||
|
|
||||||
|
# Gültige Stufen-Werte: neu (Lernpfad) + alt (Schwierigkeit) abwärtskompatibel.
|
||||||
|
_STUFEN_OK = ("anfaenger", "fortgeschritten", "experte", "einfach", "mittel", "schwer")
|
||||||
|
|
||||||
|
|
||||||
async def _load_subbausteine(topic: str) -> dict[str, list[dict]]:
|
async def _load_subbausteine(topic: str) -> dict[str, list[dict]]:
|
||||||
"""Subbausteine je Baustein — DB-first ({titel, stufe, relevanz}), Fallback Sidecar-Datei.
|
"""Subbausteine je Baustein — DB-first ({titel, stufe, relevanz}), Fallback Sidecar-Datei.
|
||||||
Fehlt beides → {} (Guide nimmt alles)."""
|
Fehlt beides → {} (Guide nimmt alles)."""
|
||||||
out: dict[str, list[dict]] = {}
|
out: dict[str, list[dict]] = {}
|
||||||
for r in await list_subbausteine(topic):
|
for r in await list_subbausteine(topic):
|
||||||
if r["status"] == "konsens" and r["sub_titel"] and r["stufe"] in ("einfach", "mittel", "schwer"):
|
if r["status"] == "konsens" and r["sub_titel"] and r["stufe"] in _STUFEN_OK:
|
||||||
|
try:
|
||||||
|
fakten = json.loads(r["fakten"]) if r.get("fakten") else {}
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
fakten = {}
|
||||||
out.setdefault(r["baustein"], []).append(
|
out.setdefault(r["baustein"], []).append(
|
||||||
{"titel": r["sub_titel"], "stufe": r["stufe"], "relevanz": r["relevanz"]})
|
{"titel": r["sub_titel"], "stufe": r["stufe"], "relevanz": r["relevanz"], "fakten": fakten})
|
||||||
if out:
|
if out:
|
||||||
return out
|
return out
|
||||||
data = _json_datei(subbausteine_path(topic))
|
data = _json_datei(subbausteine_path(topic))
|
||||||
@@ -74,21 +82,26 @@ async def _load_subbausteine(topic: str) -> dict[str, list[dict]]:
|
|||||||
if not isinstance(subs, list):
|
if not isinstance(subs, list):
|
||||||
continue
|
continue
|
||||||
gut = [s for s in subs if isinstance(s, dict) and str(s.get("titel", "")).strip()
|
gut = [s for s in subs if isinstance(s, dict) and str(s.get("titel", "")).strip()
|
||||||
and s.get("stufe") in ("einfach", "mittel", "schwer")]
|
and s.get("stufe") in _STUFEN_OK]
|
||||||
if gut:
|
if gut:
|
||||||
out[titel] = gut
|
out[titel] = gut
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _ebene_label(s: dict) -> str:
|
||||||
|
"""Ansichts-Ebene eines Subbausteins: rand → 'rand' (Ebene 4), sonst die Stufe (1–3)."""
|
||||||
|
return "rand" if s.get("relevanz") == "rand" else (s.get("stufe") or "anfaenger")
|
||||||
|
|
||||||
|
|
||||||
def _zuteilung_subs(chunk: list[dict], entries: dict[int, str], subs_by_titel: dict[str, list[dict]]) -> str:
|
def _zuteilung_subs(chunk: list[dict], entries: dict[int, str], subs_by_titel: dict[str, list[dict]]) -> str:
|
||||||
"""Listet je Kapitel die Bausteine, darunter ihre Subbausteine mit Stufe."""
|
"""Listet je Kapitel die Bausteine, darunter ihre Subbausteine mit Ebenen-Label."""
|
||||||
lines: list[str] = []
|
lines: list[str] = []
|
||||||
for ch in chunk:
|
for ch in chunk:
|
||||||
lines.append(f"KAPITEL: {ch['title']}")
|
lines.append(f"KAPITEL: {ch['title']}")
|
||||||
for num in ch["nums"]:
|
for num in ch["nums"]:
|
||||||
lines.append(f"- {entries[num]}")
|
lines.append(f"- {entries[num]}")
|
||||||
for s in subs_by_titel.get(_titel(entries[num]), []):
|
for s in subs_by_titel.get(_titel(entries[num]), []):
|
||||||
lines.append(f" [{s['stufe']}] {s['titel']}")
|
lines.append(f" [{_ebene_label(s)}] {s['titel']}")
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
@@ -249,6 +262,68 @@ def _mit_resten(plan: list[dict], entries: dict[int, str]) -> list[dict]:
|
|||||||
return [*plan, {"title": "Weitere", "nums": fehlen}] if fehlen else plan
|
return [*plan, {"title": "Weitere", "nums": fehlen}] if fehlen else plan
|
||||||
|
|
||||||
|
|
||||||
|
def _fakten_grounding(subs_raw: dict[str, list[dict]]) -> str:
|
||||||
|
"""Verifizierte Sub-Fakten (extract-once aus der Bausteine-Phase) als Grounding-Block für den
|
||||||
|
Inhalts-Agent. Leer, wenn keine Fakten gespeichert (Altbestand → Fallback auf Quelle-Hinweis)."""
|
||||||
|
bloecke = []
|
||||||
|
for titel, subs in subs_raw.items():
|
||||||
|
zeilen = []
|
||||||
|
for s in subs:
|
||||||
|
fk = s.get("fakten") if isinstance(s.get("fakten"), dict) else None
|
||||||
|
if not fk:
|
||||||
|
continue
|
||||||
|
teile = []
|
||||||
|
if fk.get("kernpunkte"):
|
||||||
|
teile.append("Kern: " + " · ".join(fk["kernpunkte"]))
|
||||||
|
for bf in fk.get("belegte_fakten", []):
|
||||||
|
teile.append(f"FAKT[{bf.get('quelle', '?')}]: {bf.get('text', '')}")
|
||||||
|
if fk.get("voraussetzungen"):
|
||||||
|
teile.append("Voraussetzung: " + fk["voraussetzungen"])
|
||||||
|
if fk.get("huerden"):
|
||||||
|
teile.append("Hürde: " + fk["huerden"])
|
||||||
|
if fk.get("beispiel_idee"):
|
||||||
|
teile.append("Beispiel: " + fk["beispiel_idee"])
|
||||||
|
if teile:
|
||||||
|
zeilen.append(f"- {s['titel']}: " + " | ".join(teile))
|
||||||
|
if zeilen:
|
||||||
|
bloecke.append(f"BAUSTEIN: {titel}\n" + "\n".join(zeilen))
|
||||||
|
if not bloecke:
|
||||||
|
return ""
|
||||||
|
return ("VERIFIZIERTE FAKTEN je Subbaustein — verbindliche Grundlage. Belegte Fakten (FAKT[Quelle]) "
|
||||||
|
"WÖRTLICH übernehmen, nichts dazu erfinden, NICHT neu recherchieren. Beispiele als Beispiel "
|
||||||
|
"nutzen, nie als Fakt.\n\n" + "\n\n".join(bloecke))
|
||||||
|
|
||||||
|
|
||||||
|
async def _gliederung_aus_db(topic: str, sel_entries: dict[int, str]) -> list[dict] | None:
|
||||||
|
"""Gliederung aus dem Bausteine-Artefakt (DB) lesen und auf die gewählten Bausteine mappen.
|
||||||
|
Titel-basiert (robust gegen Nummern-Drift): Bausteine außerhalb der Auswahl werden ignoriert
|
||||||
|
(Format-Filter), fehlende ergänzt später _mit_resten. None → kein Artefakt (Altbestand)."""
|
||||||
|
raw = await get_gliederung(topic)
|
||||||
|
if not raw:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
data = json.loads(raw)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return None
|
||||||
|
kapitel = data.get("kapitel") if isinstance(data, dict) else None
|
||||||
|
if not isinstance(kapitel, list):
|
||||||
|
return None
|
||||||
|
norm_to_num = {_norm_titel(_titel(t)): num for num, t in sel_entries.items()}
|
||||||
|
plan, seen = [], set()
|
||||||
|
for ch in kapitel:
|
||||||
|
if not isinstance(ch, dict):
|
||||||
|
continue
|
||||||
|
nums = []
|
||||||
|
for bt in ch.get("bausteine", []):
|
||||||
|
num = norm_to_num.get(_norm_titel(str(bt)))
|
||||||
|
if num is not None and num not in seen:
|
||||||
|
seen.add(num)
|
||||||
|
nums.append(num)
|
||||||
|
if nums:
|
||||||
|
plan.append({"title": str(ch.get("titel", "")).strip() or "Kapitel", "nums": nums})
|
||||||
|
return plan or None
|
||||||
|
|
||||||
|
|
||||||
async def _generate_sections(
|
async def _generate_sections(
|
||||||
guide_id: str, topic: str, format_name: str, entries: dict[int, str],
|
guide_id: str, topic: str, format_name: str, entries: dict[int, str],
|
||||||
facts: str, instructions: str, provider: str,
|
facts: str, instructions: str, provider: str,
|
||||||
@@ -265,20 +340,19 @@ async def _generate_sections(
|
|||||||
# Subbausteine je Baustein (DB-first) — früh geladen: steuert Auswahl + Sub-Filter je Format.
|
# Subbausteine je Baustein (DB-first) — früh geladen: steuert Auswahl + Sub-Filter je Format.
|
||||||
# Fehlt sie → {} (Fallback: Guide nimmt alles).
|
# Fehlt sie → {} (Fallback: Guide nimmt alles).
|
||||||
subs_raw = await _load_subbausteine(topic)
|
subs_raw = await _load_subbausteine(topic)
|
||||||
|
# Extract-once-Grounding: gespeicherte, verifizierte Fakten ersetzen den generischen
|
||||||
|
# Quelle-Hinweis. Der Inhalts-Agent formuliert daraus, statt die Quelle neu zu lesen.
|
||||||
|
if (fakten_block := _fakten_grounding(subs_raw)):
|
||||||
|
facts = fakten_block
|
||||||
|
|
||||||
def _hat_relevanz(num, art):
|
def _hat_relevanz(num, art):
|
||||||
return any(isinstance(s, dict) and s.get("relevanz") == art for s in subs_raw.get(_titel(entries[num]), []))
|
return any(isinstance(s, dict) and s.get("relevanz") == art for s in subs_raw.get(_titel(entries[num]), []))
|
||||||
|
|
||||||
# Auswahl (Teil von Gliederung). Deterministisch je Format:
|
# Auswahl: EIN Voll-Dokument mit ALLEN Bausteinen (inkl. Rand). Die Ansichten E/M/S/F
|
||||||
# Guide=relevante Bausteine · FullGuide=alle · Rest=reine Rand-Bausteine (keine im Guide).
|
# filtern später pro Subbaustein-Ebene. (FullGuide/Rest bleiben als Alt-Zweige bestehen.)
|
||||||
if format_name == "FullGuide":
|
if format_name == "Rest":
|
||||||
auswahl = list(entries)
|
|
||||||
elif format_name == "Rest":
|
|
||||||
auswahl = [num for num in entries if not _hat_relevanz(num, "relevant")]
|
auswahl = [num for num in entries if not _hat_relevanz(num, "relevant")]
|
||||||
else: # Guide
|
else: # Guide / FullGuide → alle Bausteine
|
||||||
auswahl = [num for num in entries if _hat_relevanz(num, "relevant")]
|
|
||||||
if not auswahl:
|
|
||||||
_log(topic, "Guide: keine Relevanz-Daten — alle Bausteine genommen")
|
|
||||||
auswahl = list(entries)
|
auswahl = list(entries)
|
||||||
if not auswahl:
|
if not auswahl:
|
||||||
await _fail(guide_id, "Keine passenden Bausteine für dieses Format")
|
await _fail(guide_id, "Keine passenden Bausteine für dieses Format")
|
||||||
@@ -289,9 +363,13 @@ async def _generate_sections(
|
|||||||
# Nummerierte Liste (ID = Baustein-Nummer aus entries) — Agenten/Judge ordnen per Nummer.
|
# Nummerierte Liste (ID = Baustein-Nummer aus entries) — Agenten/Judge ordnen per Nummer.
|
||||||
sel_liste = "\n".join(f"{num}. {t}" for num, t in sel_entries.items())
|
sel_liste = "\n".join(f"{num}. {t}" for num, t in sel_entries.items())
|
||||||
|
|
||||||
# Schritt 0: Gliederung — bis zu 3 Vorschläge (Grace), ein Judge merged sie. Bricht NIE ab:
|
# Schritt 0: Gliederung. Bevorzugt das Bausteine-Artefakt (DB) — der Guide präsentiert nur,
|
||||||
|
# gliedert nicht mehr selbst. Fehlt es (Altbestand) → bisherige Agenten/Judge-Logik als Fallback.
|
||||||
# 0 gültige → Code-Fallback, 1 → direkt, ≥2 → Judge (mit Vorschlag als Rückfall).
|
# 0 gültige → Code-Fallback, 1 → direkt, ≥2 → Judge (mit Vorschlag als Rückfall).
|
||||||
# Gültiges gliederung.json überspringt den Schritt.
|
plan = await _gliederung_aus_db(topic, sel_entries)
|
||||||
|
if plan is not None:
|
||||||
|
_log(topic, f"Gliederung aus Bausteine-Artefakt ({len(plan)} Kapitel)")
|
||||||
|
if plan is None:
|
||||||
plan = _resolve_gliederung(_json_datei(files["gliederung"]), sel_entries, soll, soll)
|
plan = _resolve_gliederung(_json_datei(files["gliederung"]), sel_entries, soll, soll)
|
||||||
if plan is None:
|
if plan is None:
|
||||||
await _set_step(guide_id, 0, "Gliederungs-Vorschläge (3 Agenten)…")
|
await _set_step(guide_id, 0, "Gliederungs-Vorschläge (3 Agenten)…")
|
||||||
@@ -366,14 +444,12 @@ async def _generate_sections(
|
|||||||
# Der Writer baut darunter pro Baustein (eigene feine Chunks, s.u.) → variable Längen.
|
# Der Writer baut darunter pro Baustein (eigene feine Chunks, s.u.) → variable Längen.
|
||||||
total_sections = sum(len(c["nums"]) for c in plan)
|
total_sections = sum(len(c["nums"]) for c in plan)
|
||||||
chunks = _split_chunks(plan, max(1, math.ceil(total_sections / GUIDE_CHUNK)))
|
chunks = _split_chunks(plan, max(1, math.ceil(total_sections / GUIDE_CHUNK)))
|
||||||
# Subbausteine je Baustein, je Format gefiltert: Guide=relevant · FullGuide=alle · Rest=Rand.
|
# Subbausteine je Baustein: Guide/FullGuide nehmen ALLE (inkl. Rand → Ebene 4 in der Ansicht);
|
||||||
# Subs ohne Relevanz-Feld (Alt-Themen) zählen als „nicht Rand" (→ Guide/FullGuide).
|
# nur der Alt-Zweig Rest filtert auf Rand. So trägt das eine Dokument alle Ebenen.
|
||||||
if format_name == "FullGuide":
|
if format_name == "Rest":
|
||||||
subs_by_titel = {t: list(subs) for t, subs in subs_raw.items()}
|
|
||||||
elif format_name == "Rest":
|
|
||||||
subs_by_titel = {t: [s for s in subs if s.get("relevanz") == "rand"] for t, subs in subs_raw.items()}
|
subs_by_titel = {t: [s for s in subs if s.get("relevanz") == "rand"] for t, subs in subs_raw.items()}
|
||||||
else: # Guide
|
else: # Guide / FullGuide
|
||||||
subs_by_titel = {t: [s for s in subs if s.get("relevanz") != "rand"] for t, subs in subs_raw.items()}
|
subs_by_titel = {t: list(subs) for t, subs in subs_raw.items()}
|
||||||
subs_by_titel = {t: subs for t, subs in subs_by_titel.items() if subs}
|
subs_by_titel = {t: subs for t, subs in subs_by_titel.items() if subs}
|
||||||
zuteilungen = [_zuteilung_subs(chunk, entries, subs_by_titel) for chunk in chunks]
|
zuteilungen = [_zuteilung_subs(chunk, entries, subs_by_titel) for chunk in chunks]
|
||||||
chunk_sizes = [sum(len(c["nums"]) for c in chunk) for chunk in chunks]
|
chunk_sizes = [sum(len(c["nums"]) for c in chunk) for chunk in chunks]
|
||||||
@@ -617,9 +693,15 @@ async def _generate_sections(
|
|||||||
def sections_text(nums: list[int]) -> str:
|
def sections_text(nums: list[int]) -> str:
|
||||||
return "\n\n".join(f"SECTION: {_titel(entries[num])}\n{by_num[num]['md']}" for num in nums)
|
return "\n\n".join(f"SECTION: {_titel(entries[num])}\n{by_num[num]['md']}" for num in nums)
|
||||||
|
|
||||||
|
def _sub_liste(num: int) -> str:
|
||||||
|
subs = subs_by_titel.get(_titel(entries[num]), [])
|
||||||
|
return "\n".join(f"- [{_ebene_label(s)}] {s['titel']}" for s in subs) or "(keine)"
|
||||||
|
|
||||||
def auftraege_text(nums: list[int], probleme: dict[int, str]) -> str:
|
def auftraege_text(nums: list[int], probleme: dict[int, str]) -> str:
|
||||||
return "\n\n".join(
|
return "\n\n".join(
|
||||||
f"SECTION: {_titel(entries[num])}\nPROBLEM: {probleme[num]}\nAKTUELLER INHALT:\n{by_num[num]['md']}"
|
f"SECTION: {_titel(entries[num])}\n"
|
||||||
|
f"SUBBAUSTEINE (je einen `<!-- sub: LABEL | titel -->`-Marker setzen, Label/Reihenfolge wie hier):\n{_sub_liste(num)}\n"
|
||||||
|
f"PROBLEM: {probleme[num]}\nAKTUELLER INHALT:\n{by_num[num]['md']}"
|
||||||
for num in nums
|
for num in nums
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -703,7 +785,13 @@ async def _generate_sections(
|
|||||||
continue
|
continue
|
||||||
for sec in _parse_fragment(p.read_text(encoding="utf-8")):
|
for sec in _parse_fragment(p.read_text(encoding="utf-8")):
|
||||||
num = _titel_aufloesen(idx, sec["titel"])
|
num = _titel_aufloesen(idx, sec["titel"])
|
||||||
if num in probleme_by_num and sec["md"].strip():
|
if num not in probleme_by_num or not sec["md"].strip():
|
||||||
|
continue
|
||||||
|
# Marker-Invariante: Verliert der Fix die Sub-Marker, obwohl das Original welche
|
||||||
|
# hatte, wird er verworfen — sonst stirbt der Stufen-Filter (E/M/S/F) still.
|
||||||
|
if by_num[num].get("subs") and not sec.get("subs"):
|
||||||
|
_log(topic, f"Lese-Fix für '{sec['titel']}' ohne Sub-Marker — verworfen, getaggtes Original bleibt")
|
||||||
|
continue
|
||||||
by_num[num] = sec
|
by_num[num] = sec
|
||||||
ersetzt.add(num)
|
ersetzt.add(num)
|
||||||
_log(topic, f"Lese-Prüfung Runde {runde}: {len(ersetzt)} Section(s) überarbeitet")
|
_log(topic, f"Lese-Prüfung Runde {runde}: {len(ersetzt)} Section(s) überarbeitet")
|
||||||
@@ -730,7 +818,9 @@ async def _generate_sections(
|
|||||||
for ch in plan:
|
for ch in plan:
|
||||||
sections = [
|
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", ""), "pruefbar": _pruefbar(num)}
|
"kompakt": by_num[num].get("kompakt", ""),
|
||||||
|
"anker": by_num[num].get("anker", ""), "anker_kompakt": by_num[num].get("anker_kompakt", ""),
|
||||||
|
"subs": by_num[num].get("subs", []), "pruefbar": _pruefbar(num)}
|
||||||
for num in ch["nums"] if num in by_num
|
for num in ch["nums"] if num in by_num
|
||||||
]
|
]
|
||||||
if sections:
|
if sections:
|
||||||
@@ -745,6 +835,34 @@ async def _generate_sections(
|
|||||||
return chapters
|
return chapters
|
||||||
|
|
||||||
|
|
||||||
|
_STUFE_EBENE = {"anfaenger": 1, "fortgeschritten": 2, "experte": 3, "rand": 4,
|
||||||
|
"einfach": 1, "mittel": 2, "schwer": 3} # alte Werte abwärtskompatibel
|
||||||
|
|
||||||
|
|
||||||
|
def _section_fuer_ebene(sec: dict, ebene: int) -> dict:
|
||||||
|
"""md/kompakt einer Section auf Subbausteine bis zur Ebene rekonstruieren (Anker bleibt)."""
|
||||||
|
subs = sec.get("subs") or []
|
||||||
|
if not subs:
|
||||||
|
return sec # keine Sub-Tags (Altbestand) → unverändert sichtbar
|
||||||
|
sichtbar = [s for s in subs if _STUFE_EBENE.get(s.get("stufe"), 1) <= ebene]
|
||||||
|
md = "\n\n".join(t for t in [sec.get("anker", ""), *(s.get("md", "") for s in sichtbar)] if t).strip()
|
||||||
|
kompakt = "\n".join(t for t in [sec.get("anker_kompakt", ""), *(s.get("kompakt", "") for s in sichtbar)] if t).strip()
|
||||||
|
return {**sec, "md": md, "kompakt": kompakt, "leer": not sichtbar}
|
||||||
|
|
||||||
|
|
||||||
|
def content_fuer_ebene(content: dict, ebene: int) -> dict:
|
||||||
|
"""Guide-Content auf eine Ansichts-Ebene (1=A · 2=F · 3=E · 4=V) filtern. Ebene 4 = Vollfassung.
|
||||||
|
Sections ohne sichtbare Subs werden ausgeblendet, leere Kapitel entfallen."""
|
||||||
|
if not isinstance(content, dict) or ebene >= 4:
|
||||||
|
return content
|
||||||
|
kapitel = []
|
||||||
|
for ch in content.get("chapters", []):
|
||||||
|
secs = [s for s in (_section_fuer_ebene(x, ebene) for x in ch.get("sections", [])) if not s.get("leer")]
|
||||||
|
if secs:
|
||||||
|
kapitel.append({**ch, "sections": secs})
|
||||||
|
return {**content, "chapters": kapitel}
|
||||||
|
|
||||||
|
|
||||||
async def reconcile_guides() -> None:
|
async def reconcile_guides() -> None:
|
||||||
"""DB↔Dateisystem abgleichen: status=done ohne Content-Datei → error.
|
"""DB↔Dateisystem abgleichen: status=done ohne Content-Datei → error.
|
||||||
|
|
||||||
@@ -894,11 +1012,21 @@ async def block_uebernehmen(topic: str, format_name: str, baustein: str, stelle:
|
|||||||
sec = _section_finden(content, baustein) if content else None
|
sec = _section_finden(content, baustein) if content else None
|
||||||
if sec is None:
|
if sec is None:
|
||||||
return None
|
return None
|
||||||
feld = "kompakt" if str(stelle).startswith("kompakt") else "md"
|
ist_kompakt = str(stelle).startswith("kompakt")
|
||||||
|
feld = "kompakt" if ist_kompakt else "md"
|
||||||
aktuell = sec.get(feld, "") or ""
|
aktuell = sec.get(feld, "") or ""
|
||||||
gefunden = alt in aktuell
|
gefunden = alt in aktuell
|
||||||
if gefunden:
|
if gefunden:
|
||||||
sec[feld] = aktuell.replace(alt, neu, 1)
|
sec[feld] = aktuell.replace(alt, neu, 1)
|
||||||
|
# Auch in Anker + Subs ersetzen (Quellen der gefilterten E/M/S-Ansicht), sonst zeigt
|
||||||
|
# die gestufte Ansicht weiter den alten Block.
|
||||||
|
anker_feld = "anker_kompakt" if ist_kompakt else "anker"
|
||||||
|
if alt in (sec.get(anker_feld) or ""):
|
||||||
|
sec[anker_feld] = sec[anker_feld].replace(alt, neu, 1)
|
||||||
|
for sub in sec.get("subs", []):
|
||||||
|
if alt in (sub.get(feld, "") or ""):
|
||||||
|
sub[feld] = sub[feld].replace(alt, neu, 1)
|
||||||
|
break
|
||||||
js = json.dumps(content, ensure_ascii=False)
|
js = json.dumps(content, ensure_ascii=False)
|
||||||
await set_guide_content(topic, format_name, js)
|
await set_guide_content(topic, format_name, js)
|
||||||
atomic_write_json(guide_content_path(topic, format_name), content, indent=1)
|
atomic_write_json(guide_content_path(topic, format_name), content, indent=1)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from datetime import datetime, timezone
|
|||||||
|
|
||||||
from agents import run_agent
|
from agents import run_agent
|
||||||
from config import DEFAULT_PROVIDER
|
from config import DEFAULT_PROVIDER
|
||||||
from database import create_element, list_elements
|
from database import create_element, list_elements, get_baustein_huerden
|
||||||
from elements import generate_element
|
from elements import generate_element
|
||||||
from jsonio import parse_json_text as _parse_json_text
|
from jsonio import parse_json_text as _parse_json_text
|
||||||
from pipeline import _prompt, _probleme_schema
|
from pipeline import _prompt, _probleme_schema
|
||||||
@@ -27,34 +27,68 @@ log = logging.getLogger("creator.lernen")
|
|||||||
STUFEN = (("anfaenger", 0.2), ("fortgeschritten", 0.4), ("experte", 0.6), ("meister", 1.0))
|
STUFEN = (("anfaenger", 0.2), ("fortgeschritten", 0.4), ("experte", 0.6), ("meister", 1.0))
|
||||||
|
|
||||||
|
|
||||||
def cap_fuer(n_subs: int) -> int:
|
PUNKT_BASIS = 25 # Punkte je Subbaustein. Meister-cap = (alle Subs) × 25.
|
||||||
"""Maximal-Score eines Bausteins: 4 Punkte je relevantem Subbaustein, gedeckelt [10, 50]."""
|
|
||||||
return max(10, min(50, 4 * n_subs))
|
|
||||||
|
def _ebenen(n_je_ebene: dict[int, int]) -> list[int]:
|
||||||
|
return [n_je_ebene.get(k, 0) for k in (1, 2, 3, 4)]
|
||||||
|
|
||||||
|
|
||||||
|
def schwellen(n_je_ebene: dict[int, int]) -> list[int]:
|
||||||
|
"""Kumulative Ebenen-Schwellen [S_1, S_2, S_3, S_4] = (n_1+…+n_k) × 25.
|
||||||
|
S_k ist der Score, ab dem Ebene k+1 (E→M→S→F) freigeschaltet ist; S_4 = cap_final."""
|
||||||
|
out, akk = [], 0
|
||||||
|
for n in _ebenen(n_je_ebene):
|
||||||
|
akk += n
|
||||||
|
out.append(akk * PUNKT_BASIS)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def cap_final(n_je_ebene: dict[int, int]) -> int:
|
||||||
|
"""Maximal-Score (Meister) = alle Subbausteine × 25."""
|
||||||
|
return schwellen(n_je_ebene)[-1]
|
||||||
|
|
||||||
|
|
||||||
|
def freie_ebene(score: int, n_je_ebene: dict[int, int]) -> int:
|
||||||
|
"""Höchste freigeschaltete Sub-Ebene 1–4. Ebene k+1 frei, sobald score ≥ S_k.
|
||||||
|
Leere Ebenen (n_k=0) werden automatisch übersprungen (S_k == S_{k-1})."""
|
||||||
|
s = schwellen(n_je_ebene)
|
||||||
|
e = 1
|
||||||
|
for k in range(3): # S_1..S_3 schalten Ebene 2..4 frei
|
||||||
|
if score >= s[k]:
|
||||||
|
e = k + 2
|
||||||
|
return e
|
||||||
|
|
||||||
|
|
||||||
|
def cap_aktuell(score: int, n_je_ebene: dict[int, int]) -> int:
|
||||||
|
"""Erreichbarer cap der aktuell freigeschalteten Ebene = freigeschaltete Subs × 25."""
|
||||||
|
return schwellen(n_je_ebene)[freie_ebene(score, n_je_ebene) - 1]
|
||||||
|
|
||||||
|
|
||||||
def _schwelle(p: float, cap: int) -> int:
|
def _schwelle(p: float, cap: int) -> int:
|
||||||
return round(p * cap)
|
return round(p * cap)
|
||||||
|
|
||||||
|
|
||||||
def stufe_aus_score(score: int, cap: int) -> str | None:
|
def stufe_aus_score(score: int, cap_final_wert: int) -> str | None:
|
||||||
"""Höchste erreichte Stufe (None unter 20 %)."""
|
"""Höchste erreichte Lernstufe (None unter 20 %), relativ zum cap_final."""
|
||||||
erreicht = None
|
erreicht = None
|
||||||
for key, p in STUFEN:
|
for key, p in STUFEN:
|
||||||
if score >= _schwelle(p, cap):
|
if score >= _schwelle(p, cap_final_wert):
|
||||||
erreicht = key
|
erreicht = key
|
||||||
return erreicht
|
return erreicht
|
||||||
|
|
||||||
|
|
||||||
def progressiver_malus(basis: int, cap: int) -> int:
|
def progressiver_malus(basis: int, cap_akt: int) -> int:
|
||||||
"""Fehler-Strafe nach Fortschritt: 0–20 %→0 · 21–40 %→−1 · 41–60 %→−2 · 61–100 %→−3."""
|
"""Fehler-Strafe nach Fortschritt in der aktuellen Ebene (gegen cap_aktuell):
|
||||||
pct = (basis / cap) if cap else 0.0
|
≤25 %→−5 · ≤50 %→−10 · ≤75 %→−15 · >75 %→−20."""
|
||||||
if pct <= 0.2:
|
pct = (basis / cap_akt) if cap_akt else 0.0
|
||||||
return 0
|
if pct <= 0.25:
|
||||||
if pct <= 0.4:
|
return -5
|
||||||
return -1
|
if pct <= 0.5:
|
||||||
if pct <= 0.6:
|
return -10
|
||||||
return -2
|
if pct <= 0.75:
|
||||||
return -3
|
return -15
|
||||||
|
return -20
|
||||||
CHAT_TIMEOUT = 240
|
CHAT_TIMEOUT = 240
|
||||||
PRUEFUNG_TIMEOUT = 120 # kurze JSON-Turns; deckelt die Serien-Latenz pro Prüfungs-Schritt
|
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
|
GRUENDLICH_TIMEOUT = 600 # „Gründlich prüfen": starkes Modell (role guide) braucht länger
|
||||||
@@ -70,34 +104,66 @@ FRAGETYPEN = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Antwort-Niveau (wie sehr der Kern getroffen ist) → Punkt-Delta.
|
# Antwort-Niveau → Basis-Punkte (neue 25er-Skala). „kaum" = −1 ist nur das Signal für den
|
||||||
|
# progressiven Malus (echter Wert kommt aus progressiver_malus). Positive Werte werden bei
|
||||||
|
# Streak hochmoduliert und auf [10, 40] geklemmt.
|
||||||
NIVEAUS = {
|
NIVEAUS = {
|
||||||
"unbeantwortbar": 0, # Frage selbst kaputt (Randthema/Verweis auf Nichtgezeigtes) → kein Malus
|
"unbeantwortbar": 0, # Frage selbst kaputt → keine Änderung
|
||||||
"kaum": -1, # < 25 % richtig
|
"kaum": -1, # < 25 % richtig → Malus
|
||||||
"teilweise": 0, # 25–49 %
|
"teilweise": 0, # 25–49 % → neutral
|
||||||
"solide": 1, # 50–74 %
|
"solide": 16, # 50–74 %
|
||||||
"stark": 2, # 75–99 %
|
"stark": 24, # 75–99 % (Quiz/Lücke-Treffer)
|
||||||
"komplett": 3, # 100 %
|
"komplett": 30, # 100 % (nur freies Erklären erreichbar)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Reihenfolge schwach→stark (für den Nachfrage-Deckel).
|
||||||
|
_NIVEAU_RANG = ("kaum", "teilweise", "solide", "stark", "komplett")
|
||||||
|
|
||||||
def score_berechnen(basis: int, delta: int, floor: int, cap: int) -> int:
|
|
||||||
"""Neuer Score · driftfrei aus der Basis. `delta` liefert die Route je Prüfungsform
|
def deckel_nachfrage(niveau: str, nachgefragt: bool) -> str:
|
||||||
(Quiz +1/+3, Lückentext +1/+3, Erklären −1…+3; falsch = progressiver_malus). Auf
|
"""Mit Nachfrage (Hinweis erhalten) höchstens „solide" — kein Voll-Score erschummeln."""
|
||||||
[floor, cap] geklemmt. floor = höchste erreichte Stufenschwelle (kein Rückfall);
|
if nachgefragt and niveau in ("stark", "komplett"):
|
||||||
cap = 4× relevante Subbausteine [10, 50]. Am Deckel (basis ≥ cap) eingefroren („Max")."""
|
return "solide"
|
||||||
if basis >= cap:
|
return niveau
|
||||||
|
|
||||||
|
|
||||||
|
def streak_punkte(basis_delta: int, streak_basis: int) -> int:
|
||||||
|
"""Positives Basis-Delta mit Streak hochmodulieren, auf [10, 40] geklemmt."""
|
||||||
|
faktor = min(1.33, 1 + 0.066 * min(streak_basis, 5))
|
||||||
|
return max(10, min(40, round(basis_delta * faktor)))
|
||||||
|
|
||||||
|
|
||||||
|
def punkte_delta(niveau: str, streak_basis: int, basis: int, cap_akt: int) -> tuple[int, int]:
|
||||||
|
"""Antwort-Niveau → (Punkt-Delta, neue Streak). Positiv: streak-moduliert, Streak +1.
|
||||||
|
Neutral (0): keine Änderung, Streak bleibt. Negativ: progressiver Malus, Streak-Reset 0."""
|
||||||
|
basis_delta = NIVEAUS.get(niveau, 0)
|
||||||
|
if basis_delta > 0:
|
||||||
|
return streak_punkte(basis_delta, streak_basis), streak_basis + 1
|
||||||
|
if basis_delta == 0:
|
||||||
|
return 0, streak_basis
|
||||||
|
return progressiver_malus(basis, cap_akt), 0
|
||||||
|
|
||||||
|
|
||||||
|
def score_berechnen(basis: int, delta: int, floor: int, cap_akt: int, cap_fin: int) -> int:
|
||||||
|
"""Neuer Score · driftfrei aus der Basis. Klemmt nach oben gegen `cap_akt` (Deckel der
|
||||||
|
aktuell freigeschalteten Ebene) und nach unten gegen `floor`. Eingefroren NUR am
|
||||||
|
absoluten Maximum (`basis ≥ cap_fin`) — sonst würde an jeder Ebenen-Schwelle blockiert."""
|
||||||
|
if basis >= cap_fin:
|
||||||
return basis
|
return basis
|
||||||
return max(floor, min(cap, basis + delta))
|
return max(floor, min(cap_akt, basis + delta))
|
||||||
|
|
||||||
|
|
||||||
def floor_aus_score(basis: int, cap: int) -> int:
|
def floor_aus_score(basis: int, cap_fin: int, s_schwellen: list[int]) -> int:
|
||||||
"""Untergrenze: höchste erreichte Stufenschwelle — kein Rückfall unter die Stufe."""
|
"""Untergrenze (kein Rückfall): höchste erreichte Lernstufen-Schwelle (über cap_final)
|
||||||
|
UND höchste erreichte Ebenen-Freischalt-Schwelle S_k. max beider Achsen."""
|
||||||
floor = 0
|
floor = 0
|
||||||
for _, p in STUFEN:
|
for _, p in STUFEN:
|
||||||
s = _schwelle(p, cap)
|
s = _schwelle(p, cap_fin)
|
||||||
if basis >= s:
|
if basis >= s:
|
||||||
floor = s
|
floor = max(floor, s)
|
||||||
|
for s in s_schwellen:
|
||||||
|
if basis >= s:
|
||||||
|
floor = max(floor, s)
|
||||||
return floor
|
return floor
|
||||||
|
|
||||||
|
|
||||||
@@ -199,7 +265,8 @@ def _doppelfrage_mangel(frage: str) -> str | None:
|
|||||||
|
|
||||||
async def _frage_mit_kritik(
|
async def _frage_mit_kritik(
|
||||||
topic: str, baustein: str, section_block: str, kompakt_block: str,
|
topic: str, baustein: str, section_block: str, kompakt_block: str,
|
||||||
transcript: str, vermeide_block: str, typ_block: str, fokus_block: str, provider: str,
|
transcript: str, vermeide_block: str, typ_block: str, fokus_block: str,
|
||||||
|
niveau_block: str, provider: str,
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
"""Frage generieren, vom Kritiker prüfen lassen, bei Mängeln neu (max KRITIK_MAX_RUNDEN)."""
|
"""Frage generieren, vom Kritiker prüfen lassen, bei Mängeln neu (max KRITIK_MAX_RUNDEN)."""
|
||||||
kritik_block = "(keine)"
|
kritik_block = "(keine)"
|
||||||
@@ -209,7 +276,7 @@ async def _frage_mit_kritik(
|
|||||||
"Baustein-Frage", "guide", _frage_schema, provider, lane="batch",
|
"Baustein-Frage", "guide", _frage_schema, provider, lane="batch",
|
||||||
topic=topic, baustein=baustein, section_block=section_block,
|
topic=topic, baustein=baustein, section_block=section_block,
|
||||||
kompakt_block=kompakt_block, transcript=transcript, vermeide_block=vermeide_block,
|
kompakt_block=kompakt_block, transcript=transcript, vermeide_block=vermeide_block,
|
||||||
typ_block=typ_block, fokus_block=fokus_block, kritik_block=kritik_block,
|
typ_block=typ_block, fokus_block=fokus_block, niveau_block=niveau_block, kritik_block=kritik_block,
|
||||||
)
|
)
|
||||||
if data is None:
|
if data is None:
|
||||||
return None
|
return None
|
||||||
@@ -277,13 +344,27 @@ def _vermeide_block(vermeide: list[str] | None) -> str:
|
|||||||
return "\n".join(f"- {f}" for f in eintraege) or "(keine)"
|
return "\n".join(f"- {f}" for f in eintraege) or "(keine)"
|
||||||
|
|
||||||
|
|
||||||
|
# Lerner-Niveau (aus dem Score abgeleitet) → Adressaten-Rolle für die Frage. So entsteht die
|
||||||
|
# Schwierigkeit: nicht „extra schwer machen", sondern „für einen Anfänger/Experten fragen".
|
||||||
|
NIVEAU_ROLLE = {
|
||||||
|
"anfaenger": "Der Lerner ist ANFÄNGER. Frage nach dem Grundverständnis — das Kernkonzept, einfach und direkt.",
|
||||||
|
"fortgeschritten": "Der Lerner ist FORTGESCHRITTEN. Frage etwas tiefer — Zusammenhänge und die typische Anwendung.",
|
||||||
|
"experte": "Der Lerner ist EXPERTE. Frage anspruchsvoll — Sonderfälle, Abgrenzungen, typische Tücken.",
|
||||||
|
"meister": "Der Lerner ist auf MEISTER-Niveau. Frage sehr anspruchsvoll — Edge-Cases, präzise Begründung, Transfer auf Neues.",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _niveau_block(niveau: str | None) -> str:
|
||||||
|
return NIVEAU_ROLLE.get(niveau or "", NIVEAU_ROLLE["anfaenger"])
|
||||||
|
|
||||||
|
|
||||||
async def pruefung_frage(
|
async def pruefung_frage(
|
||||||
topic: str, baustein: str, section: str, kompakt: str | None,
|
topic: str, baustein: str, section: str, kompakt: str | None,
|
||||||
messages: list[dict], subbausteine: list[str] | None = None,
|
messages: list[dict], subbausteine: list[str] | None = None,
|
||||||
vermeide: list[str] | None = None, provider: str = DEFAULT_PROVIDER,
|
vermeide: list[str] | None = None, niveau: str = "anfaenger", provider: str = DEFAULT_PROVIDER,
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
"""Aktion 'frage': eine Frage generieren — zufälliger Typ zu einem zufälligen Subbaustein,
|
"""Aktion 'frage': eine Frage generieren — zufälliger Typ zu einem zufälligen Subbaustein,
|
||||||
dann Kritiker (Generator → Kritiker sequenziell) · None bei Fehler."""
|
in der Adressaten-Rolle des Niveaus, dann Kritiker (sequenziell) · None bei Fehler."""
|
||||||
try:
|
try:
|
||||||
section_block, kompakt_block = _bloecke(section, kompakt)
|
section_block, kompakt_block = _bloecke(section, kompakt)
|
||||||
transcript = _transcript(messages) if messages else "(leer)"
|
transcript = _transcript(messages) if messages else "(leer)"
|
||||||
@@ -296,7 +377,7 @@ async def pruefung_frage(
|
|||||||
)
|
)
|
||||||
return await _frage_mit_kritik(
|
return await _frage_mit_kritik(
|
||||||
topic, baustein, section_block, kompakt_block, transcript,
|
topic, baustein, section_block, kompakt_block, transcript,
|
||||||
_vermeide_block(vermeide), typ_block, fokus_block, provider,
|
_vermeide_block(vermeide), typ_block, fokus_block, _niveau_block(niveau), provider,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
log.warning("[%s] Frage fehlgeschlagen (%s)", topic, baustein, exc_info=True)
|
log.warning("[%s] Frage fehlgeschlagen (%s)", topic, baustein, exc_info=True)
|
||||||
@@ -305,17 +386,17 @@ async def pruefung_frage(
|
|||||||
|
|
||||||
async def pruefung_frage_variante(
|
async def pruefung_frage_variante(
|
||||||
topic: str, baustein: str, section: str, kompakt: str | None,
|
topic: str, baustein: str, section: str, kompakt: str | None,
|
||||||
muster: str, provider: str = DEFAULT_PROVIDER,
|
muster: str, niveau: str = "anfaenger", provider: str = DEFAULT_PROVIDER,
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
"""Aktion 'frage' mit Muster: aus einem vordefinierten Muster eine konkrete, im
|
"""Aktion 'frage' mit Muster: aus einem vordefinierten Muster eine konkrete Frage in der
|
||||||
Wortlaut leicht andere Frage formulieren. Kein Kritiker (Muster ist build-geprüft).
|
Adressaten-Rolle des Niveaus formulieren. Kein Kritiker (Muster ist build-geprüft).
|
||||||
Stil-Guard bleibt als billiger Schutz gegen Doppelfragen · None bei Fehler."""
|
Stil-Guard bleibt als billiger Schutz gegen Doppelfragen · None bei Fehler."""
|
||||||
try:
|
try:
|
||||||
section_block, kompakt_block = _bloecke(section, kompakt)
|
section_block, kompakt_block = _bloecke(section, kompakt)
|
||||||
data = await _gen_call(
|
data = await _gen_call(
|
||||||
"Baustein-Frage-Variante", "guide", _frage_schema, provider, lane="batch",
|
"Baustein-Frage-Variante", "guide", _frage_schema, provider, lane="batch",
|
||||||
topic=topic, baustein=baustein, section_block=section_block,
|
topic=topic, baustein=baustein, section_block=section_block,
|
||||||
kompakt_block=kompakt_block, muster=muster,
|
kompakt_block=kompakt_block, muster=muster, niveau_block=_niveau_block(niveau),
|
||||||
)
|
)
|
||||||
if data is None:
|
if data is None:
|
||||||
return None
|
return None
|
||||||
@@ -341,17 +422,16 @@ def _optionen_schema(opts) -> list[dict] | None:
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def _quiz_schema(data, schwer: bool = False) -> dict | None:
|
def _quiz_schema(data) -> dict | None:
|
||||||
"""{"frage": str, "optionen": [{text, korrekt}]×4} → validiert · sonst None.
|
"""{"frage": str, "optionen": [{text, korrekt}]×4} → validiert · sonst None.
|
||||||
leicht: genau 1 richtig (Single) · schwer: mindestens 2 richtig (mehrdeutig)."""
|
Single-Choice: genau 1 richtig. Die Schwierigkeit steckt im Niveau, nicht in der Anzahl."""
|
||||||
if not isinstance(data, dict):
|
if not isinstance(data, dict):
|
||||||
return None
|
return None
|
||||||
frage = str(data.get("frage", "")).strip()
|
frage = str(data.get("frage", "")).strip()
|
||||||
out = _optionen_schema(data.get("optionen"))
|
out = _optionen_schema(data.get("optionen"))
|
||||||
if not frage or out is None:
|
if not frage or out is None:
|
||||||
return None
|
return None
|
||||||
n_korrekt = sum(o["korrekt"] for o in out)
|
if sum(o["korrekt"] for o in out) != 1:
|
||||||
if (n_korrekt < 2 if schwer else n_korrekt != 1):
|
|
||||||
return None
|
return None
|
||||||
return {"frage": frage, "optionen": out}
|
return {"frage": frage, "optionen": out}
|
||||||
|
|
||||||
@@ -367,24 +447,35 @@ def _lueckwahl_schema(data) -> dict | None:
|
|||||||
return {"satz": satz, "optionen": out}
|
return {"satz": satz, "optionen": out}
|
||||||
|
|
||||||
|
|
||||||
|
async def huerden_distraktor_block(topic: str, baustein: str) -> str:
|
||||||
|
"""Typische Irrtümer (Fakten-Hürden) des Bausteins als Distraktor-Quelle für Quiz/Lückenwahl.
|
||||||
|
Leer, wenn keine vorhanden (Altbestand) → der Prompt-Platzhalter verschwindet rückstandslos."""
|
||||||
|
try:
|
||||||
|
huerden = await get_baustein_huerden(topic, _norm_titel(baustein))
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
if not huerden:
|
||||||
|
return ""
|
||||||
|
zeilen = "\n".join(f"- {h}" for h in huerden[:8])
|
||||||
|
return ("TYPISCHE IRRTÜMER zu diesem Baustein (nutze sie als Distraktoren, wenn sie zur Frage passen):\n"
|
||||||
|
+ zeilen + "\n")
|
||||||
|
|
||||||
|
|
||||||
async def quiz_generieren(
|
async def quiz_generieren(
|
||||||
topic: str, baustein: str, section: str, kompakt: str | None,
|
topic: str, baustein: str, section: str, kompakt: str | None,
|
||||||
muster: str, schwer: bool = False, provider: str = DEFAULT_PROVIDER,
|
muster: str, niveau: str = "anfaenger", provider: str = DEFAULT_PROVIDER,
|
||||||
|
distraktor_block: str = "",
|
||||||
) -> dict | None:
|
) -> dict | None:
|
||||||
"""Aus einem Muster eine Multiple-Choice-Frage. schwer: 0–4 richtig (Multi).
|
"""Aus einem Muster eine Single-Choice-Frage (genau 1 richtig), im Anspruch des Niveaus.
|
||||||
leicht: genau 1 richtig (Single). Starkes Modell (role guide) für korrekte Flags.
|
Starkes Modell (role guide) für korrekte Flags. → {frage, optionen} · None bei Fehler.
|
||||||
→ {frage, optionen:[{text,korrekt}]} · None bei Fehler."""
|
distraktor_block: optionale typische Irrtümer (aus den Fakten-Hürden) als Distraktor-Quelle."""
|
||||||
try:
|
try:
|
||||||
section_block, kompakt_block = _bloecke(section, kompakt)
|
section_block, kompakt_block = _bloecke(section, kompakt)
|
||||||
anzahl_block = (
|
|
||||||
"MINDESTENS ZWEI Optionen sind richtig (gern auch drei) — NIEMALS nur eine. Mische bewusst, welche."
|
|
||||||
if schwer else
|
|
||||||
"GENAU EINE Option ist richtig, die anderen drei sind klar falsch."
|
|
||||||
)
|
|
||||||
return await _gen_call(
|
return await _gen_call(
|
||||||
"Baustein-Quiz", "guide", (lambda d: _quiz_schema(d, schwer=schwer)), provider, lane="batch",
|
"Baustein-Quiz", "guide", _quiz_schema, provider, lane="batch",
|
||||||
topic=topic, baustein=baustein, section_block=section_block,
|
topic=topic, baustein=baustein, section_block=section_block,
|
||||||
kompakt_block=kompakt_block, muster=muster, anzahl_block=anzahl_block,
|
kompakt_block=kompakt_block, muster=muster, niveau_block=_niveau_block(niveau),
|
||||||
|
distraktor_block=distraktor_block,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
log.warning("[%s] Quiz-Frage fehlgeschlagen (%s)", topic, baustein, exc_info=True)
|
log.warning("[%s] Quiz-Frage fehlgeschlagen (%s)", topic, baustein, exc_info=True)
|
||||||
@@ -393,16 +484,19 @@ async def quiz_generieren(
|
|||||||
|
|
||||||
async def lueckwahl_generieren(
|
async def lueckwahl_generieren(
|
||||||
topic: str, baustein: str, section: str, kompakt: str | None,
|
topic: str, baustein: str, section: str, kompakt: str | None,
|
||||||
muster: str, provider: str = DEFAULT_PROVIDER,
|
muster: str, niveau: str = "anfaenger", provider: str = DEFAULT_PROVIDER,
|
||||||
|
distraktor_block: str = "",
|
||||||
) -> dict | None:
|
) -> dict | None:
|
||||||
"""Lückentext mit Auswahl (leichte Variante): Satz mit ___ + 4 Begriffe, genau 1 richtig.
|
"""Lückentext mit Auswahl: Satz mit ___ + 4 Begriffe, genau 1 richtig — im Anspruch des Niveaus.
|
||||||
→ {satz, optionen:[{text,korrekt}]} · None bei Fehler."""
|
→ {satz, optionen:[{text,korrekt}]} · None bei Fehler.
|
||||||
|
distraktor_block: optionale typische Irrtümer (aus den Fakten-Hürden) als Distraktor-Quelle."""
|
||||||
try:
|
try:
|
||||||
section_block, kompakt_block = _bloecke(section, kompakt)
|
section_block, kompakt_block = _bloecke(section, kompakt)
|
||||||
return await _gen_call(
|
return await _gen_call(
|
||||||
"Baustein-Lueckwahl", "guide", _lueckwahl_schema, provider, lane="batch",
|
"Baustein-Lueckwahl", "guide", _lueckwahl_schema, provider, lane="batch",
|
||||||
topic=topic, baustein=baustein, section_block=section_block,
|
topic=topic, baustein=baustein, section_block=section_block,
|
||||||
kompakt_block=kompakt_block, muster=muster,
|
kompakt_block=kompakt_block, muster=muster, niveau_block=_niveau_block(niveau),
|
||||||
|
distraktor_block=distraktor_block,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
log.warning("[%s] Lückentext-Auswahl fehlgeschlagen (%s)", topic, baustein, exc_info=True)
|
log.warning("[%s] Lückentext-Auswahl fehlgeschlagen (%s)", topic, baustein, exc_info=True)
|
||||||
@@ -424,16 +518,16 @@ def _lueck_schema(data) -> dict | None:
|
|||||||
|
|
||||||
async def lueckentext_generieren(
|
async def lueckentext_generieren(
|
||||||
topic: str, baustein: str, section: str, kompakt: str | None,
|
topic: str, baustein: str, section: str, kompakt: str | None,
|
||||||
muster: str, provider: str = DEFAULT_PROVIDER,
|
muster: str, niveau: str = "anfaenger", provider: str = DEFAULT_PROVIDER,
|
||||||
) -> dict | None:
|
) -> dict | None:
|
||||||
"""Aus einem Muster eine Lückentext-Aufgabe (Satz mit ___, Lösung, Synonyme).
|
"""Aus einem Muster eine Lückentext-Aufgabe (Satz mit ___, Lösung, Synonyme), im Anspruch
|
||||||
→ {satz, loesung, alternativen} · None bei Fehler."""
|
des Niveaus. → {satz, loesung, alternativen} · None bei Fehler."""
|
||||||
try:
|
try:
|
||||||
section_block, kompakt_block = _bloecke(section, kompakt)
|
section_block, kompakt_block = _bloecke(section, kompakt)
|
||||||
return await _gen_call(
|
return await _gen_call(
|
||||||
"Baustein-Lueckentext", "guide", _lueck_schema, provider, lane="batch",
|
"Baustein-Lueckentext", "guide", _lueck_schema, provider, lane="batch",
|
||||||
topic=topic, baustein=baustein, section_block=section_block,
|
topic=topic, baustein=baustein, section_block=section_block,
|
||||||
kompakt_block=kompakt_block, muster=muster,
|
kompakt_block=kompakt_block, muster=muster, niveau_block=_niveau_block(niveau),
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
log.warning("[%s] Lückentext-Frage fehlgeschlagen (%s)", topic, baustein, exc_info=True)
|
log.warning("[%s] Lückentext-Frage fehlgeschlagen (%s)", topic, baustein, exc_info=True)
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ class BausteineQuelleResponse(BaseModel):
|
|||||||
|
|
||||||
class SubbausteinInfo(BaseModel):
|
class SubbausteinInfo(BaseModel):
|
||||||
titel: str
|
titel: str
|
||||||
stufe: Literal["einfach", "mittel", "schwer"]
|
stufe: Literal["anfaenger", "fortgeschritten", "experte", "einfach", "mittel", "schwer"]
|
||||||
relevanz: Literal["relevant", "rand"] | None = None
|
relevanz: Literal["relevant", "rand"] | None = None
|
||||||
|
|
||||||
|
|
||||||
@@ -230,8 +230,8 @@ class BausteinPruefungRequest(BaseModel):
|
|||||||
nachgefragt: bool = False # für diese Frage wurde nachgefragt → Gewinn auf +1 gedeckelt
|
nachgefragt: bool = False # für diese Frage wurde nachgefragt → Gewinn auf +1 gedeckelt
|
||||||
begruendung: str = Field(default="", max_length=2000) # „Gründlich prüfen": warum mit der Bewertung unzufrieden
|
begruendung: str = Field(default="", max_length=2000) # „Gründlich prüfen": warum mit der Bewertung unzufrieden
|
||||||
muster: str = Field(default="", max_length=2000) # gezogenes Frage-Muster (Saat); leer → Live-Generierung (Fallback)
|
muster: str = Field(default="", max_length=2000) # gezogenes Frage-Muster (Saat); leer → Live-Generierung (Fallback)
|
||||||
# Basis + cap werden serverseitig geführt (Anker bzw. 4×Subs) — Client-cap nur Hinweis.
|
# Basis + cap werden serverseitig geführt (Anker bzw. Subs×25) — Client-cap nur Hinweis.
|
||||||
cap: int = Field(default=10, ge=1, le=50) # Score-Deckel = clamp(4×relevante Subbausteine, 10, 50)
|
cap: int = Field(default=10, ge=1, le=10000) # Score-Deckel = freigeschaltete Subbausteine × 25
|
||||||
messages: list[ChatMessage] = [] # Dialog bisher; leer = erste Frage
|
messages: list[ChatMessage] = [] # Dialog bisher; leer = erste Frage
|
||||||
provider: ProviderType = "claude"
|
provider: ProviderType = "claude"
|
||||||
gruendlich: bool = False # „Gründlich prüfen": Bewertung mit starkem Modell (role guide)
|
gruendlich: bool = False # „Gründlich prüfen": Bewertung mit starkem Modell (role guide)
|
||||||
@@ -253,13 +253,16 @@ class BausteinPruefungResponse(BaseModel):
|
|||||||
loesung: str | None = None # Lückentext: erwarteter Begriff
|
loesung: str | None = None # Lückentext: erwarteter Begriff
|
||||||
alternativen: list[str] | None = None # Lückentext: akzeptierte Synonyme
|
alternativen: list[str] | None = None # Lückentext: akzeptierte Synonyme
|
||||||
gute_antworten: int
|
gute_antworten: int
|
||||||
cap: int = 10 # Maximal-Score dieses Bausteins (4×Subs, geklemmt) — Frontend leitet die Stufe ab
|
streak: int = 0 # aktuelle Serie korrekter Antworten (je Baustein)
|
||||||
|
cap: int = 10 # cap_final = alle Subs × 25 — Frontend leitet die Lernstufe ab
|
||||||
|
|
||||||
|
|
||||||
class BausteinLernstand(BaseModel):
|
class BausteinLernstand(BaseModel):
|
||||||
gute_antworten: int
|
gute_antworten: int
|
||||||
streak: int = 0
|
streak: int = 0
|
||||||
cap: int = 10 # 4×relevante Subbausteine, geklemmt [10, 50]
|
cap: int = 0 # cap_final = alle Subbausteine × 25
|
||||||
|
cap_aktuell: int = 0 # erreichbarer cap der aktuell freigeschalteten Ebene
|
||||||
|
freie_ebene: int = 1 # 1=A · 2=F · 3=E · 4=V
|
||||||
|
|
||||||
|
|
||||||
class BausteinLernstandResponse(BaseModel):
|
class BausteinLernstandResponse(BaseModel):
|
||||||
|
|||||||
@@ -133,10 +133,10 @@ def _runde_schema(data, final: bool = False):
|
|||||||
|
|
||||||
|
|
||||||
def _stufen_schema(data, ids: set[int] | None = None):
|
def _stufen_schema(data, ids: set[int] | None = None):
|
||||||
"""{"stufen": {"1": "einfach", …}} → {id: stufe} · sonst None.
|
"""{"stufen": {"1": "anfaenger", …}} → {id: stufe} · sonst None.
|
||||||
|
|
||||||
Stufe ∈ {einfach, mittel, schwer}. Sind `ids` gegeben, müssen mindestens
|
Stufe ∈ {anfaenger, fortgeschritten, experte} (alte Werte abwärtskompatibel). Sind `ids`
|
||||||
diese abgedeckt sein (Extras erlaubt); der Aufrufer filtert dann auf `ids`.
|
gegeben, müssen mindestens diese abgedeckt sein (Extras erlaubt); der Aufrufer filtert auf `ids`.
|
||||||
"""
|
"""
|
||||||
if not isinstance(data, dict) or not isinstance(data.get("stufen"), dict) or not data["stufen"]:
|
if not isinstance(data, dict) or not isinstance(data.get("stufen"), dict) or not data["stufen"]:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ Query-Schleifen mehr pro Guide.
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from database import list_baustein_scores_all, subs_count_alle, list_guides, list_progress_all
|
from database import list_baustein_scores_all, subs_je_ebene_alle, list_guides, list_progress_all
|
||||||
from guide import guide_slot_dateien
|
from guide import guide_slot_dateien
|
||||||
from lernen import cap_fuer, STUFEN, _schwelle
|
from lernen import cap_final, STUFEN, _schwelle
|
||||||
from paths import bausteine_path, guide_content_path
|
from paths import bausteine_path, guide_content_path
|
||||||
from textkit import _norm_titel
|
from textkit import _norm_titel
|
||||||
|
|
||||||
@@ -40,12 +40,12 @@ async def lade_lernstand() -> tuple[list[dict], dict[str, set[str]], dict[str, d
|
|||||||
Stufe je Baustein wird aus Score + cap (4×relevante Subs) abgeleitet.
|
Stufe je Baustein wird aus Score + cap (4×relevante Subs) abgeleitet.
|
||||||
"""
|
"""
|
||||||
scores = await list_baustein_scores_all()
|
scores = await list_baustein_scores_all()
|
||||||
caps = await subs_count_alle()
|
ebenen = await subs_je_ebene_alle()
|
||||||
levels: dict[str, dict[str, set[str]]] = {key: {} for key, _ in STUFEN}
|
levels: dict[str, dict[str, set[str]]] = {key: {} for key, _ in STUFEN}
|
||||||
for topic, baustein, score in scores:
|
for topic, baustein, score in scores:
|
||||||
cap = cap_fuer(caps.get((topic, _norm_titel(baustein)), 0))
|
cf = cap_final(ebenen.get((topic, _norm_titel(baustein)), {}))
|
||||||
for key, p in STUFEN:
|
for key, p in STUFEN:
|
||||||
if score >= _schwelle(p, cap):
|
if cf and score >= _schwelle(p, cf):
|
||||||
levels[key].setdefault(topic, set()).add(_norm_titel(baustein))
|
levels[key].setdefault(topic, set()).add(_norm_titel(baustein))
|
||||||
return await list_guides(), await list_progress_all(), levels
|
return await list_guides(), await list_progress_all(), levels
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
@@ -14,13 +15,15 @@ from database import (
|
|||||||
list_progress, set_progress, delete_progress,
|
list_progress, set_progress, delete_progress,
|
||||||
create_element, list_elements, get_element, update_element, delete_element,
|
create_element, list_elements, get_element, update_element, delete_element,
|
||||||
list_baustein_progress, get_baustein_progress, set_offene_frage,
|
list_baustein_progress, get_baustein_progress, set_offene_frage,
|
||||||
set_baustein_score, set_baustein_absolviert, delete_baustein_daten, delete_baustein_progress, count_relevant_subs, subs_count_roh,
|
set_baustein_score, set_baustein_score_and_streak, set_baustein_absolviert,
|
||||||
|
delete_baustein_daten, delete_baustein_progress, subs_je_ebene, subs_je_ebene_roh,
|
||||||
delete_topic_pipeline, delete_quelle, get_guide_content, delete_guide_content,
|
delete_topic_pipeline, delete_quelle, get_guide_content, delete_guide_content,
|
||||||
|
get_sub_artefakte,
|
||||||
)
|
)
|
||||||
from bausteine import generate_bausteine, cancel_bausteine, bausteine_status, active_bausteine, reset_bausteine, lade_quelle, lade_uebersicht, subbausteine_titel, lade_frage_muster
|
from bausteine import generate_bausteine, cancel_bausteine, bausteine_status, active_bausteine, reset_bausteine, lade_quelle, lade_uebersicht, subbausteine_titel, subbausteine_frei, lade_frage_muster, lade_frage_muster_frei
|
||||||
from elements import generate_element, chat_with_guide, chat_with_element, check_element, style_element, refine_suggestion
|
from elements import generate_element, chat_with_guide, chat_with_element, check_element, style_element, refine_suggestion
|
||||||
from lernen import NIVEAUS, cap_fuer, baustein_chat, baustein_diskussion, baustein_element_anlegen, pruefung_bewertung, pruefung_bewertung_schnell, pruefung_frage, pruefung_frage_variante, quiz_generieren, lueckwahl_generieren, lueckentext_generieren, lueckentext_pruefen, score_berechnen, floor_aus_score, progressiver_malus, stufe_aus_score
|
from lernen import baustein_chat, baustein_diskussion, baustein_element_anlegen, pruefung_bewertung, pruefung_bewertung_schnell, pruefung_frage, pruefung_frage_variante, quiz_generieren, lueckwahl_generieren, lueckentext_generieren, lueckentext_pruefen, huerden_distraktor_block, score_berechnen, floor_aus_score, stufe_aus_score, cap_final, cap_aktuell, freie_ebene, schwellen, punkte_delta, deckel_nachfrage
|
||||||
from guide import generate_guide, guide_slot_dateien, guide_fertig_step, block_pruefen, block_uebernehmen
|
from guide import generate_guide, guide_slot_dateien, guide_fertig_step, block_pruefen, block_uebernehmen, content_fuer_ebene
|
||||||
from pipeline import cancel_guide
|
from pipeline import cancel_guide
|
||||||
from regeln import FORMATE, formate_stats, guide_lock, ist_absolviert, lade_lernstand, thema_abgeschlossen
|
from regeln import FORMATE, formate_stats, guide_lock, ist_absolviert, lade_lernstand, thema_abgeschlossen
|
||||||
from models import (
|
from models import (
|
||||||
@@ -221,30 +224,59 @@ async def get_bausteine_uebersicht(topic: str):
|
|||||||
|
|
||||||
@router.get("/bausteine/frage-muster")
|
@router.get("/bausteine/frage-muster")
|
||||||
async def get_frage_muster(topic: str, baustein: str):
|
async def get_frage_muster(topic: str, baustein: str):
|
||||||
"""Vordefinierte Frage-Muster eines Bausteins (leer = Fallback auf Live-Generierung)."""
|
"""Freigeschaltete Frage-Muster eines Bausteins (bis zur aktuellen Ebene; leer = Live)."""
|
||||||
return {"muster": await lade_frage_muster(topic, baustein)}
|
stand = await get_baustein_progress(topic, baustein)
|
||||||
|
fe = freie_ebene(stand["gute_antworten"], await subs_je_ebene(topic, baustein))
|
||||||
|
return {"muster": await lade_frage_muster_frei(topic, baustein, fe)}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/bausteine/artefakte")
|
||||||
|
async def get_artefakte(topic: str, typ: str | None = None):
|
||||||
|
"""Lern-Artefakte (Karteikarten/Beispiele/Diagramme) je Thema, gruppiert nach Baustein-Norm.
|
||||||
|
Diagramm liegt auf Baustein-Ebene (sub_norm=''), Karteikarte/Beispiel je Subbaustein."""
|
||||||
|
rows = await get_sub_artefakte(topic, typ)
|
||||||
|
out: dict[str, dict] = {}
|
||||||
|
for r in rows:
|
||||||
|
b = out.setdefault(r["baustein_norm"], {"baustein": r["baustein"], "karteikarte": [], "beispiel": [], "diagramm": None})
|
||||||
|
if r["baustein"] and not b["baustein"]:
|
||||||
|
b["baustein"] = r["baustein"]
|
||||||
|
try:
|
||||||
|
daten = json.loads(r["daten"])
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
continue
|
||||||
|
if r["typ"] == "diagramm":
|
||||||
|
b["diagramm"] = daten
|
||||||
|
elif r["typ"] in ("karteikarte", "beispiel"):
|
||||||
|
b[r["typ"]].append({"subbaustein": r["sub_titel"], **daten})
|
||||||
|
return {"artefakte": out}
|
||||||
|
|
||||||
|
|
||||||
# --- Baustein-Lernen: Chat, Prüfung ---
|
# --- Baustein-Lernen: Chat, Prüfung ---
|
||||||
|
|
||||||
@router.get("/bausteine/lernstand", response_model=BausteinLernstandResponse)
|
@router.get("/bausteine/lernstand", response_model=BausteinLernstandResponse)
|
||||||
async def baustein_lernstand(topic: str):
|
async def baustein_lernstand(topic: str):
|
||||||
"""Prüfungs-Stand pro Baustein (roher Titel als Key). cap = 4×relevante Subs [10,50]
|
"""Prüfungs-Stand pro Baustein (roher Titel als Key). cap_final = alle Subs × 25;
|
||||||
— für ALLE Bausteine (auch noch nicht geprüfte), damit der cap sofort stimmt."""
|
cap_aktuell + freie_ebene aus dem Score — für ALLE Bausteine (auch ungeprüfte)."""
|
||||||
progress = {p["baustein"]: p for p in await list_baustein_progress(topic)}
|
progress = {p["baustein"]: p for p in await list_baustein_progress(topic)}
|
||||||
caps = await subs_count_roh(topic)
|
ebenen = await subs_je_ebene_roh(topic)
|
||||||
|
|
||||||
|
def _stand(score: int, streak: int, n_je_ebene: dict[int, int]) -> dict:
|
||||||
|
return {
|
||||||
|
"gute_antworten": score, "streak": streak,
|
||||||
|
"cap": cap_final(n_je_ebene),
|
||||||
|
"cap_aktuell": cap_aktuell(score, n_je_ebene),
|
||||||
|
"freie_ebene": freie_ebene(score, n_je_ebene),
|
||||||
|
}
|
||||||
|
|
||||||
bausteine = {
|
bausteine = {
|
||||||
b: {
|
b: _stand(progress[b]["gute_antworten"] if b in progress else 0,
|
||||||
"gute_antworten": progress[b]["gute_antworten"] if b in progress else 0,
|
progress[b]["streak"] if b in progress else 0, n)
|
||||||
"streak": progress[b]["streak"] if b in progress else 0,
|
for b, n in ebenen.items()
|
||||||
"cap": cap_fuer(n),
|
|
||||||
}
|
}
|
||||||
for b, n in caps.items()
|
# Altbestand-Bausteine mit Prüfung, aber ohne Subs → leere Ebenen (cap 0).
|
||||||
}
|
|
||||||
# Altbestand-Bausteine mit Prüfung, aber ohne (relevante) Subs → Default-cap.
|
|
||||||
for b, p in progress.items():
|
for b, p in progress.items():
|
||||||
if b not in bausteine:
|
if b not in bausteine:
|
||||||
bausteine[b] = {"gute_antworten": p["gute_antworten"], "streak": p["streak"], "cap": 10}
|
bausteine[b] = _stand(p["gute_antworten"], p["streak"], {})
|
||||||
return {"bausteine": bausteine}
|
return {"bausteine": bausteine}
|
||||||
|
|
||||||
|
|
||||||
@@ -282,54 +314,52 @@ def _farbe(punkte: int) -> str:
|
|||||||
return "gut" if punkte > 0 else ("neutral" if punkte == 0 else "schlecht")
|
return "gut" if punkte > 0 else ("neutral" if punkte == 0 else "schlecht")
|
||||||
|
|
||||||
|
|
||||||
def _erklaeren_delta(niveau: str, nachgefragt: bool) -> int:
|
async def _buche(req, frage: str, niveau: str, n_je_ebene: dict[int, int]) -> dict:
|
||||||
"""Erklären-Form: Punkt-Delta aus dem Niveau (−1…+3). Mit Nachfrage höchstens +1."""
|
"""Score+Streak driftfrei buchen (Lock + offene_frage/offene_streak-Anker). Niveau →
|
||||||
delta = NIVEAUS.get(niveau, 0)
|
Punkt-Delta (streak-moduliert) bzw. progressiver Malus bei Fehler. cap_aktuell wird aus
|
||||||
return min(delta, 1) if nachgefragt else delta
|
der Basis abgeleitet (verzögerte Freischaltung an der Ebenen-Schwelle); Element einmalig
|
||||||
|
ab Anfänger-Stufe. Re-Bewertung derselben Frage nutzt den offenen Streak-Anker → idempotent."""
|
||||||
|
|
||||||
async def _cap_fuer(topic: str, baustein: str) -> int:
|
|
||||||
"""Maximal-Score des Bausteins = clamp(4×relevante Subbausteine, 10, 50)."""
|
|
||||||
return cap_fuer(await count_relevant_subs(topic, baustein))
|
|
||||||
|
|
||||||
|
|
||||||
async def _buche(req, frage: str, delta: int, cap: int) -> dict:
|
|
||||||
"""Score driftfrei buchen (Lock + offene_frage-Anker). Fehler (delta<0) → progressiver
|
|
||||||
Malus nach Fortschritt. Element einmalig anlegen, sobald Anfänger (20 %) erreicht."""
|
|
||||||
async with _pruef_lock(req.topic, req.baustein):
|
async with _pruef_lock(req.topic, req.baustein):
|
||||||
stand = await get_baustein_progress(req.topic, req.baustein)
|
stand = await get_baustein_progress(req.topic, req.baustein)
|
||||||
war_stufe = stand["absolviert"] is not None # Element-Guard: schon je angelegt?
|
war_stufe = stand["absolviert"] is not None # Element-Guard: schon je angelegt?
|
||||||
basis, re_bewertung = _basis(stand, frage)
|
basis, re_bewertung = _basis(stand, frage)
|
||||||
|
streak_basis = stand["offene_streak"] if re_bewertung else stand["streak"]
|
||||||
if not re_bewertung:
|
if not re_bewertung:
|
||||||
await set_offene_frage(req.topic, req.baustein, frage, basis, 0)
|
await set_offene_frage(req.topic, req.baustein, frage, basis, stand["streak"])
|
||||||
floor = floor_aus_score(basis, cap)
|
s = schwellen(n_je_ebene)
|
||||||
d = progressiver_malus(basis, cap) if delta < 0 else delta
|
cf = s[-1]
|
||||||
score = score_berechnen(basis, d, floor, cap)
|
ca = cap_aktuell(basis, n_je_ebene)
|
||||||
|
floor = floor_aus_score(basis, cf, s)
|
||||||
|
d, neue_streak = punkte_delta(niveau, streak_basis, basis, ca)
|
||||||
|
score = score_berechnen(basis, d, floor, ca, cf)
|
||||||
punkte = score - basis
|
punkte = score - basis
|
||||||
gute = await set_baustein_score(req.topic, req.baustein, score)
|
gute, streak = await set_baustein_score_and_streak(req.topic, req.baustein, score, neue_streak)
|
||||||
# Lern-Element einmalig anlegen, sobald die erste Stufe (Anfänger) erreicht ist.
|
# Lern-Element einmalig anlegen, sobald die erste Stufe (Anfänger) erreicht ist.
|
||||||
if not war_stufe and stufe_aus_score(score, cap) is not None:
|
if not war_stufe and stufe_aus_score(score, cf) is not None:
|
||||||
if await set_baustein_absolviert(req.topic, req.baustein):
|
if await set_baustein_absolviert(req.topic, req.baustein):
|
||||||
asyncio.create_task(baustein_element_anlegen(req.topic, req.baustein, req.section, req.provider))
|
asyncio.create_task(baustein_element_anlegen(req.topic, req.baustein, req.section, req.provider))
|
||||||
return {"punkte": punkte, "bewertung": _farbe(punkte), "gute_antworten": gute, "cap": cap}
|
return {"punkte": punkte, "bewertung": _farbe(punkte), "gute_antworten": gute, "streak": streak, "cap": cf}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/bausteine/pruefung", response_model=BausteinPruefungResponse)
|
@router.post("/bausteine/pruefung", response_model=BausteinPruefungResponse)
|
||||||
async def baustein_pruefung_route(req: BausteinPruefungRequest):
|
async def baustein_pruefung_route(req: BausteinPruefungRequest):
|
||||||
stand = await get_baustein_progress(req.topic, req.baustein)
|
stand = await get_baustein_progress(req.topic, req.baustein)
|
||||||
gute = stand["gute_antworten"]
|
gute = stand["gute_antworten"]
|
||||||
cap = await _cap_fuer(req.topic, req.baustein)
|
n_je_ebene = await subs_je_ebene(req.topic, req.baustein)
|
||||||
|
cap = cap_final(n_je_ebene)
|
||||||
|
niveau = stufe_aus_score(gute, cap) or "anfaenger" # Adressaten-Rolle der Frage
|
||||||
|
fe = freie_ebene(gute, n_je_ebene) # nur freigeschaltete Subs prüfen
|
||||||
kompakt = req.section_kompakt
|
kompakt = req.section_kompakt
|
||||||
msgs = [m.model_dump() for m in req.messages]
|
msgs = [m.model_dump() for m in req.messages]
|
||||||
|
|
||||||
if req.aktion == "frage":
|
if req.aktion == "frage":
|
||||||
if req.muster.strip():
|
if req.muster.strip():
|
||||||
# Aus gezogenem Muster eine konkrete, leicht andere Frage formulieren (kein Dedup nötig).
|
# Aus gezogenem Muster eine konkrete Frage im Niveau formulieren (kein Dedup nötig).
|
||||||
frage = await pruefung_frage_variante(req.topic, req.baustein, req.section, kompakt, req.muster, provider=req.provider)
|
frage = await pruefung_frage_variante(req.topic, req.baustein, req.section, kompakt, req.muster, niveau=niveau, provider=req.provider)
|
||||||
else:
|
else:
|
||||||
# Fallback (kein Muster-Sidecar): Live-Generierung mit zufälligem Typ/Subbaustein.
|
# Fallback (kein Muster-Sidecar): Live-Generierung, Fokus nur auf freigeschaltete Subs.
|
||||||
subs = await subbausteine_titel(req.topic, req.baustein) # Fokus-Kandidaten (zufällig gewählt)
|
subs = await subbausteine_frei(req.topic, req.baustein, fe)
|
||||||
frage = await pruefung_frage(req.topic, req.baustein, req.section, kompakt, msgs, subbausteine=subs, vermeide=req.vermeide, provider=req.provider)
|
frage = await pruefung_frage(req.topic, req.baustein, req.section, kompakt, msgs, subbausteine=subs, vermeide=req.vermeide, niveau=niveau, provider=req.provider)
|
||||||
if frage is None:
|
if frage is None:
|
||||||
raise HTTPException(502, "Frage fehlgeschlagen — bitte erneut versuchen")
|
raise HTTPException(502, "Frage fehlgeschlagen — bitte erneut versuchen")
|
||||||
return {"frage": frage, "gute_antworten": gute, "cap": cap}
|
return {"frage": frage, "gute_antworten": gute, "cap": cap}
|
||||||
@@ -349,7 +379,8 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest):
|
|||||||
if req.aktion == "quiz_frage":
|
if req.aktion == "quiz_frage":
|
||||||
if not req.muster.strip():
|
if not req.muster.strip():
|
||||||
raise HTTPException(400, "Quiz braucht ein Muster")
|
raise HTTPException(400, "Quiz braucht ein Muster")
|
||||||
quiz = await quiz_generieren(req.topic, req.baustein, req.section, kompakt, req.muster, schwer=req.schwer, provider=req.provider)
|
distraktoren = await huerden_distraktor_block(req.topic, req.baustein)
|
||||||
|
quiz = await quiz_generieren(req.topic, req.baustein, req.section, kompakt, req.muster, niveau=niveau, provider=req.provider, distraktor_block=distraktoren)
|
||||||
if quiz is None:
|
if quiz is None:
|
||||||
raise HTTPException(502, "Quiz-Frage fehlgeschlagen — bitte erneut versuchen")
|
raise HTTPException(502, "Quiz-Frage fehlgeschlagen — bitte erneut versuchen")
|
||||||
return {"frage": quiz["frage"], "optionen": quiz["optionen"],
|
return {"frage": quiz["frage"], "optionen": quiz["optionen"],
|
||||||
@@ -359,8 +390,7 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest):
|
|||||||
if not req.frage.strip():
|
if not req.frage.strip():
|
||||||
raise HTTPException(400, "Quiz-Antwort braucht eine Frage")
|
raise HTTPException(400, "Quiz-Antwort braucht eine Frage")
|
||||||
getroffen = set(req.auswahl) == set(req.korrekt) # exakt die richtige Menge
|
getroffen = set(req.auswahl) == set(req.korrekt) # exakt die richtige Menge
|
||||||
delta = (3 if req.schwer else 1) if getroffen else -1
|
res = await _buche(req, req.frage, "stark" if getroffen else "kaum", n_je_ebene)
|
||||||
res = await _buche(req, req.frage, delta, cap)
|
|
||||||
res["feedback"] = "Richtig — alle korrekten getroffen." if getroffen else "Nicht ganz — die markierten waren richtig."
|
res["feedback"] = "Richtig — alle korrekten getroffen." if getroffen else "Nicht ganz — die markierten waren richtig."
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@@ -369,12 +399,13 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest):
|
|||||||
if not req.muster.strip():
|
if not req.muster.strip():
|
||||||
raise HTTPException(400, "Lückentext braucht ein Muster")
|
raise HTTPException(400, "Lückentext braucht ein Muster")
|
||||||
if req.schwer:
|
if req.schwer:
|
||||||
lt = await lueckentext_generieren(req.topic, req.baustein, req.section, kompakt, req.muster, provider=req.provider)
|
lt = await lueckentext_generieren(req.topic, req.baustein, req.section, kompakt, req.muster, niveau=niveau, provider=req.provider)
|
||||||
if lt is None:
|
if lt is None:
|
||||||
raise HTTPException(502, "Lückentext fehlgeschlagen — bitte erneut versuchen")
|
raise HTTPException(502, "Lückentext fehlgeschlagen — bitte erneut versuchen")
|
||||||
return {"satz": lt["satz"], "loesung": lt["loesung"], "alternativen": lt["alternativen"],
|
return {"satz": lt["satz"], "loesung": lt["loesung"], "alternativen": lt["alternativen"],
|
||||||
"gute_antworten": gute, "cap": cap}
|
"gute_antworten": gute, "cap": cap}
|
||||||
lw = await lueckwahl_generieren(req.topic, req.baustein, req.section, kompakt, req.muster, provider=req.provider)
|
distraktoren = await huerden_distraktor_block(req.topic, req.baustein)
|
||||||
|
lw = await lueckwahl_generieren(req.topic, req.baustein, req.section, kompakt, req.muster, niveau=niveau, provider=req.provider, distraktor_block=distraktoren)
|
||||||
if lw is None:
|
if lw is None:
|
||||||
raise HTTPException(502, "Lückentext fehlgeschlagen — bitte erneut versuchen")
|
raise HTTPException(502, "Lückentext fehlgeschlagen — bitte erneut versuchen")
|
||||||
return {"satz": lw["satz"], "optionen": lw["optionen"],
|
return {"satz": lw["satz"], "optionen": lw["optionen"],
|
||||||
@@ -389,7 +420,7 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest):
|
|||||||
else: # Begriff aus 4 gewählt → deterministisch
|
else: # Begriff aus 4 gewählt → deterministisch
|
||||||
ok = set(req.auswahl) == set(req.korrekt)
|
ok = set(req.auswahl) == set(req.korrekt)
|
||||||
feedback = "Richtig!" if ok else "Nicht ganz — der markierte Begriff war richtig."
|
feedback = "Richtig!" if ok else "Nicht ganz — der markierte Begriff war richtig."
|
||||||
res = await _buche(req, req.frage, (3 if req.schwer else 1) if ok else -1, cap)
|
res = await _buche(req, req.frage, "stark" if ok else "kaum", n_je_ebene)
|
||||||
res["feedback"] = feedback
|
res["feedback"] = feedback
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@@ -406,11 +437,14 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest):
|
|||||||
)
|
)
|
||||||
if data is None:
|
if data is None:
|
||||||
raise HTTPException(502, "Bewertung fehlgeschlagen — bitte erneut versuchen")
|
raise HTTPException(502, "Bewertung fehlgeschlagen — bitte erneut versuchen")
|
||||||
basis, _ = _basis(stand, req.frage)
|
basis, re_bew = _basis(stand, req.frage)
|
||||||
floor = floor_aus_score(basis, cap)
|
streak_basis = stand["offene_streak"] if re_bew else stand["streak"]
|
||||||
roh = _erklaeren_delta(data["niveau"], req.nachgefragt)
|
s = schwellen(n_je_ebene)
|
||||||
d = progressiver_malus(basis, cap) if roh < 0 else roh
|
ca = cap_aktuell(basis, n_je_ebene)
|
||||||
score = score_berechnen(basis, d, floor, cap)
|
floor = floor_aus_score(basis, s[-1], s)
|
||||||
|
niveau = deckel_nachfrage(data["niveau"], req.nachgefragt)
|
||||||
|
d, _ = punkte_delta(niveau, streak_basis, basis, ca)
|
||||||
|
score = score_berechnen(basis, d, floor, ca, s[-1])
|
||||||
punkte = score - basis
|
punkte = score - basis
|
||||||
return {"feedback": data["feedback"], "punkte": punkte, "bewertung": _farbe(punkte),
|
return {"feedback": data["feedback"], "punkte": punkte, "bewertung": _farbe(punkte),
|
||||||
"gute_antworten": gute, "cap": cap}
|
"gute_antworten": gute, "cap": cap}
|
||||||
@@ -424,8 +458,8 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest):
|
|||||||
)
|
)
|
||||||
if data is None:
|
if data is None:
|
||||||
raise HTTPException(502, "Bewertung fehlgeschlagen — bitte erneut versuchen")
|
raise HTTPException(502, "Bewertung fehlgeschlagen — bitte erneut versuchen")
|
||||||
roh = _erklaeren_delta(data["niveau"], req.nachgefragt)
|
niveau = deckel_nachfrage(data["niveau"], req.nachgefragt)
|
||||||
res = await _buche(req, req.frage, roh, cap) # kurzer Lock: Basis driftfrei über offene_frage-Anker
|
res = await _buche(req, req.frage, niveau, n_je_ebene) # kurzer Lock: Basis driftfrei über Anker
|
||||||
res["feedback"] = data["feedback"]
|
res["feedback"] = data["feedback"]
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@@ -483,19 +517,26 @@ async def get_one(guide_id: str):
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/guides/{guide_id}/content")
|
@router.get("/guides/{guide_id}/content")
|
||||||
async def guide_content(guide_id: str):
|
async def guide_content(guide_id: str, ebene: int = 4):
|
||||||
|
"""Guide-Inhalt. `ebene` (1=A · 2=F · 3=E · 4=V) filtert auf Subbausteine bis zu dieser
|
||||||
|
Ebene; 4 = Vollfassung (roh, unverändert)."""
|
||||||
guide = await get_guide(guide_id)
|
guide = await get_guide(guide_id)
|
||||||
if guide is None:
|
if guide is None:
|
||||||
raise HTTPException(404, "Guide nicht gefunden")
|
raise HTTPException(404, "Guide nicht gefunden")
|
||||||
if guide["status"] != "done":
|
if guide["status"] != "done":
|
||||||
raise HTTPException(404, "Inhalt nicht verfügbar")
|
raise HTTPException(404, "Inhalt nicht verfügbar")
|
||||||
stored = await get_guide_content(guide["topic"], guide["format"]) # DB-first
|
stored = await get_guide_content(guide["topic"], guide["format"]) # DB-first
|
||||||
if stored is not None:
|
if stored is None:
|
||||||
return Response(content=stored, media_type="application/json")
|
|
||||||
path = guide_content_path(guide["topic"], guide["format"]) # Fallback: Datei (Alt-Themen)
|
path = guide_content_path(guide["topic"], guide["format"]) # Fallback: Datei (Alt-Themen)
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
raise HTTPException(404, "Datei nicht gefunden")
|
raise HTTPException(404, "Datei nicht gefunden")
|
||||||
return FileResponse(path, media_type="application/json")
|
stored = path.read_text(encoding="utf-8")
|
||||||
|
if ebene >= 4:
|
||||||
|
return Response(content=stored, media_type="application/json") # Vollfassung roh
|
||||||
|
try:
|
||||||
|
return content_fuer_ebene(json.loads(stored), ebene)
|
||||||
|
except ValueError:
|
||||||
|
return Response(content=stored, media_type="application/json")
|
||||||
|
|
||||||
|
|
||||||
@router.post("/guides/{guide_id}/chat", response_model=GuideChatResponse)
|
@router.post("/guides/{guide_id}/chat", response_model=GuideChatResponse)
|
||||||
|
|||||||
@@ -102,16 +102,18 @@ _FRAGMENT_BAUSTEIN_RE = re.compile(r"<!--\s*baustein\s*:\s*(.*?)\s*-->", re.IGNO
|
|||||||
# Zwei Lese-Schichten je Section: kompakt (Merksätze) + ausführlich (Erklärung).
|
# Zwei Lese-Schichten je Section: kompakt (Merksätze) + ausführlich (Erklärung).
|
||||||
_FRAGMENT_KOMPAKT_RE = re.compile(r"<!--\s*kompakt\s*-->", re.IGNORECASE)
|
_FRAGMENT_KOMPAKT_RE = re.compile(r"<!--\s*kompakt\s*-->", re.IGNORECASE)
|
||||||
_FRAGMENT_AUSF_RE = re.compile(r"<!--\s*ausf(?:ü|ue)hrlich\s*-->", re.IGNORECASE)
|
_FRAGMENT_AUSF_RE = re.compile(r"<!--\s*ausf(?:ü|ue)hrlich\s*-->", re.IGNORECASE)
|
||||||
_STUFEN = ("einfach", "mittel", "schwer")
|
# Lernpfad-Stufen + Rand; alte Schwierigkeits-Werte abwärtskompatibel akzeptiert.
|
||||||
|
_STUFEN = ("anfaenger", "fortgeschritten", "experte", "rand", "einfach", "mittel", "schwer")
|
||||||
|
|
||||||
|
|
||||||
def _parse_fragment(text: str) -> list[dict]:
|
def _parse_fragment(text: str) -> list[dict]:
|
||||||
"""Parst eine Writer-Datei → [{"kapitel", "titel", "md", "kompakt", "subs"}] in Reihenfolge.
|
"""Parst eine Writer-Datei → [{kapitel, titel, md, kompakt, anker, anker_kompakt, subs}].
|
||||||
|
|
||||||
Zwei optionale Lese-Schichten je Section über `<!-- kompakt -->` / `<!-- ausführlich -->`:
|
Zwei Lese-Schichten je Section über `<!-- kompakt -->` / `<!-- ausführlich -->`. Innerhalb
|
||||||
Zeilen nach `kompakt` landen in `sec["kompakt"]`, sonst in `sec["md"]` (= ausführlich,
|
beider markieren `<!-- sub: stufe | titel -->`-Marker je Subbaustein einen Block; gleicher
|
||||||
auch der Default ohne Marker → rückwärtskompatibel). Optionale `<!-- sub: -->`-Marker
|
Sub-Titel in beiden Schichten wird gemergt → `sec["subs"] = [{stufe, titel, md, kompakt}]`.
|
||||||
sammeln sich weiter in `sec["subs"]`.
|
Text VOR dem ersten Sub-Marker ist der Anker (Einordnung) → `anker`/`anker_kompakt`.
|
||||||
|
`md`/`kompakt` bleiben die VOLLE Fassung (Anker + alle Subs) — rückwärtskompatibel.
|
||||||
"""
|
"""
|
||||||
sections: list[dict] = []
|
sections: list[dict] = []
|
||||||
kapitel = None
|
kapitel = None
|
||||||
@@ -128,7 +130,8 @@ def _parse_fragment(text: str) -> list[dict]:
|
|||||||
continue
|
continue
|
||||||
m = _FRAGMENT_SECTION_RE.match(s)
|
m = _FRAGMENT_SECTION_RE.match(s)
|
||||||
if m:
|
if m:
|
||||||
current = {"kapitel": kapitel, "titel": m.group(1), "md": [], "kompakt": [], "subs": []}
|
current = {"kapitel": kapitel, "titel": m.group(1), "md": [], "kompakt": [],
|
||||||
|
"anker_md": [], "anker_kompakt": [], "_submap": {}, "_suborder": []}
|
||||||
cur_sub = None
|
cur_sub = None
|
||||||
cur_layer = "md"
|
cur_layer = "md"
|
||||||
sections.append(current)
|
sections.append(current)
|
||||||
@@ -145,24 +148,40 @@ def _parse_fragment(text: str) -> list[dict]:
|
|||||||
if m and current is not None:
|
if m and current is not None:
|
||||||
teil = m.group(1).split("|", 1)
|
teil = m.group(1).split("|", 1)
|
||||||
stufe = teil[0].strip().casefold()
|
stufe = teil[0].strip().casefold()
|
||||||
cur_sub = {
|
titel = teil[1].strip() if len(teil) == 2 else ""
|
||||||
"stufe": stufe if stufe in _STUFEN else "einfach",
|
key = titel.casefold() or f"_pos{len(current['_suborder'])}"
|
||||||
"titel": teil[1].strip() if len(teil) == 2 else "",
|
cur_sub = current["_submap"].get(key)
|
||||||
"md": [],
|
if cur_sub is None:
|
||||||
}
|
cur_sub = {"stufe": stufe if stufe in _STUFEN else "anfaenger", "titel": titel, "md": [], "kompakt": []}
|
||||||
current["subs"].append(cur_sub)
|
current["_submap"][key] = cur_sub
|
||||||
|
current["_suborder"].append(key)
|
||||||
|
elif stufe in _STUFEN:
|
||||||
|
cur_sub["stufe"] = stufe
|
||||||
continue
|
continue
|
||||||
if current is not None:
|
if current is not None:
|
||||||
current[cur_layer].append(line)
|
current[cur_layer].append(line)
|
||||||
if cur_sub is not None and cur_layer == "md":
|
if cur_sub is not None:
|
||||||
cur_sub["md"].append(line)
|
cur_sub[cur_layer].append(line)
|
||||||
|
else:
|
||||||
|
current["anker_" + cur_layer].append(line)
|
||||||
|
out: list[dict] = []
|
||||||
for sec in sections:
|
for sec in sections:
|
||||||
sec["md"] = "\n".join(sec["md"]).strip()
|
subs = []
|
||||||
sec["kompakt"] = "\n".join(sec["kompakt"]).strip()
|
for key in sec["_suborder"]:
|
||||||
for sub in sec["subs"]:
|
sub = sec["_submap"][key]
|
||||||
sub["md"] = "\n".join(sub["md"]).strip()
|
sub["md"] = "\n".join(sub["md"]).strip()
|
||||||
sec["subs"] = [sub for sub in sec["subs"] if sub["md"]]
|
sub["kompakt"] = "\n".join(sub["kompakt"]).strip()
|
||||||
return sections
|
if sub["md"] or sub["kompakt"]:
|
||||||
|
subs.append(sub)
|
||||||
|
out.append({
|
||||||
|
"kapitel": sec["kapitel"], "titel": sec["titel"],
|
||||||
|
"md": "\n".join(sec["md"]).strip(),
|
||||||
|
"kompakt": "\n".join(sec["kompakt"]).strip(),
|
||||||
|
"anker": "\n".join(sec["anker_md"]).strip(),
|
||||||
|
"anker_kompakt": "\n".join(sec["anker_kompakt"]).strip(),
|
||||||
|
"subs": subs,
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def _parse_subbausteine(text: str) -> dict[str, list[str]]:
|
def _parse_subbausteine(text: str) -> dict[str, list[str]]:
|
||||||
|
|||||||
1069
frontend/package-lock.json
generated
1069
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,7 @@
|
|||||||
"katex": "^0.17.0",
|
"katex": "^0.17.0",
|
||||||
"marked": "^18.0.4",
|
"marked": "^18.0.4",
|
||||||
"marked-highlight": "^2.2.4",
|
"marked-highlight": "^2.2.4",
|
||||||
|
"mermaid": "^11.16.0",
|
||||||
"vue": "^3.5.32"
|
"vue": "^3.5.32"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import TopicDetail from './components/TopicDetail.vue'
|
|||||||
import BausteineUebersicht from './components/BausteineUebersicht.vue'
|
import BausteineUebersicht from './components/BausteineUebersicht.vue'
|
||||||
import ElementsSidebar from './components/elements/ElementsSidebar.vue'
|
import ElementsSidebar from './components/elements/ElementsSidebar.vue'
|
||||||
import ElementsOverview from './components/ElementsOverview.vue'
|
import ElementsOverview from './components/ElementsOverview.vue'
|
||||||
|
import GeneralExamPanel from './components/GeneralExamPanel.vue'
|
||||||
|
|
||||||
const guides = ref([])
|
const guides = ref([])
|
||||||
const backendTopics = ref([])
|
const backendTopics = ref([])
|
||||||
@@ -27,7 +28,9 @@ const provider = ref(localStorage.getItem('provider') || 'claude')
|
|||||||
const providers = ref([])
|
const providers = ref([])
|
||||||
const folders = ref({ projekt: [], uni: [] }) // Ordner für die Quellen-Auswahl
|
const folders = ref({ projekt: [], uni: [] }) // Ordner für die Quellen-Auswahl
|
||||||
const bausteineView = ref(false) // Bausteine-Übersicht im Hauptbereich
|
const bausteineView = ref(false) // Bausteine-Übersicht im Hauptbereich
|
||||||
|
const allgemeinView = ref(false) // Allgemeine Prüfung (themenweit) im Hauptbereich
|
||||||
const ansichtModus = ref('kompakt') // kompakt | erklärend — je Thema, Default kompakt
|
const ansichtModus = ref('kompakt') // kompakt | erklärend — je Thema, Default kompakt
|
||||||
|
const stufeAnsicht = ref(Number(localStorage.getItem('stufe')) || 4) // 1=A · 2=F · 3=E · 4=V (Stufen-Ansicht)
|
||||||
const stats = ref(null)
|
const stats = ref(null)
|
||||||
const fortschritt = ref({})
|
const fortschritt = ref({})
|
||||||
const locks = ref({}) // Sperr-Gründe pro Format (Backend = einzige Regel-Quelle)
|
const locks = ref({}) // Sperr-Gründe pro Format (Backend = einzige Regel-Quelle)
|
||||||
@@ -192,6 +195,7 @@ function selectTopic(topic) {
|
|||||||
elementsOpen.value = false
|
elementsOpen.value = false
|
||||||
elementsView.value = false
|
elementsView.value = false
|
||||||
bausteineView.value = true // Thema-Klick → Baustein-Übersicht (Guide erst auf Pill-Klick)
|
bausteineView.value = true // Thema-Klick → Baustein-Übersicht (Guide erst auf Pill-Klick)
|
||||||
|
allgemeinView.value = false
|
||||||
elementOpenId.value = null
|
elementOpenId.value = null
|
||||||
ansichtModus.value = localStorage.getItem('ansicht_' + topic) === 'erklärend' ? 'erklärend' : 'kompakt'
|
ansichtModus.value = localStorage.getItem('ansicht_' + topic) === 'erklärend' ? 'erklärend' : 'kompakt'
|
||||||
localStorage.setItem('lastTopic', topic)
|
localStorage.setItem('lastTopic', topic)
|
||||||
@@ -266,10 +270,16 @@ function setAnsicht(modus) {
|
|||||||
if (selectedTopic.value) localStorage.setItem('ansicht_' + selectedTopic.value, modus)
|
if (selectedTopic.value) localStorage.setItem('ansicht_' + selectedTopic.value, modus)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setStufe(k) {
|
||||||
|
stufeAnsicht.value = k
|
||||||
|
localStorage.setItem('stufe', String(k))
|
||||||
|
}
|
||||||
|
|
||||||
function handleOpenBausteineView() {
|
function handleOpenBausteineView() {
|
||||||
if (!selectedTopic.value) return
|
if (!selectedTopic.value) return
|
||||||
bausteineView.value = true
|
bausteineView.value = true
|
||||||
elementsView.value = false
|
elementsView.value = false
|
||||||
|
allgemeinView.value = false
|
||||||
previewGuide.value = null
|
previewGuide.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,12 +306,22 @@ function handlePreview(guide) {
|
|||||||
previewGuide.value = guide
|
previewGuide.value = guide
|
||||||
elementsView.value = false
|
elementsView.value = false
|
||||||
bausteineView.value = false
|
bausteineView.value = false
|
||||||
|
allgemeinView.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleGeneralExam() {
|
||||||
|
if (!selectedTopic.value) return
|
||||||
|
allgemeinView.value = true
|
||||||
|
bausteineView.value = false
|
||||||
|
elementsView.value = false
|
||||||
|
previewGuide.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleOpenElements() {
|
function handleOpenElements() {
|
||||||
if (!selectedTopic.value) return
|
if (!selectedTopic.value) return
|
||||||
elementsView.value = true
|
elementsView.value = true
|
||||||
bausteineView.value = false
|
bausteineView.value = false
|
||||||
|
allgemeinView.value = false
|
||||||
// Rechte Sidebar bleibt zu — sie öffnet erst beim Klick auf ein Element.
|
// Rechte Sidebar bleibt zu — sie öffnet erst beim Klick auf ein Element.
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,9 +400,12 @@ onMounted(async () => {
|
|||||||
:providers="providers"
|
:providers="providers"
|
||||||
:folders="folders"
|
:folders="folders"
|
||||||
:ansichtModus="ansichtModus"
|
:ansichtModus="ansichtModus"
|
||||||
|
:stufeAnsicht="stufeAnsicht"
|
||||||
@setProvider="setProvider"
|
@setProvider="setProvider"
|
||||||
@toggleDark="toggleDark"
|
@toggleDark="toggleDark"
|
||||||
@setAnsicht="setAnsicht"
|
@setAnsicht="setAnsicht"
|
||||||
|
@setStufe="setStufe"
|
||||||
|
@generalExam="handleGeneralExam"
|
||||||
@select="selectTopic"
|
@select="selectTopic"
|
||||||
@create="createTopic"
|
@create="createTopic"
|
||||||
@createThema="handleCreateThema"
|
@createThema="handleCreateThema"
|
||||||
@@ -413,6 +436,13 @@ onMounted(async () => {
|
|||||||
:version="elementsVersion"
|
:version="elementsVersion"
|
||||||
@open="handleOpenElementDetail"
|
@open="handleOpenElementDetail"
|
||||||
/>
|
/>
|
||||||
|
<GeneralExamPanel
|
||||||
|
v-else-if="selectedTopic && allgemeinView"
|
||||||
|
:topic="selectedTopic"
|
||||||
|
:provider="provider"
|
||||||
|
@progressChanged="loadStats(); loadBausteine()"
|
||||||
|
@fokus-active="fokusOffen = $event"
|
||||||
|
/>
|
||||||
<TopicDetail
|
<TopicDetail
|
||||||
v-else-if="selectedTopic"
|
v-else-if="selectedTopic"
|
||||||
:previewGuide="previewGuide"
|
:previewGuide="previewGuide"
|
||||||
@@ -422,6 +452,7 @@ onMounted(async () => {
|
|||||||
:doneByFormat="doneByFormat"
|
:doneByFormat="doneByFormat"
|
||||||
:themaAbgeschlossen="!!fortschritt.abgeschlossen"
|
:themaAbgeschlossen="!!fortschritt.abgeschlossen"
|
||||||
:ansichtModus="ansichtModus"
|
:ansichtModus="ansichtModus"
|
||||||
|
:stufeAnsicht="stufeAnsicht"
|
||||||
@progressChanged="loadStats(); loadBausteine()"
|
@progressChanged="loadStats(); loadBausteine()"
|
||||||
@setAnsicht="setAnsicht"
|
@setAnsicht="setAnsicht"
|
||||||
@open-sidebar="sidebarSticky = true"
|
@open-sidebar="sidebarSticky = true"
|
||||||
|
|||||||
@@ -146,12 +146,19 @@ export async function deleteGuide(id, slots = false) {
|
|||||||
await fetch(`${BASE}/guides/${id}${slots ? '?slots=1' : ''}`, { method: 'DELETE' })
|
await fetch(`${BASE}/guides/${id}${slots ? '?slots=1' : ''}`, { method: 'DELETE' })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchGuideContent(id) {
|
export async function fetchGuideContent(id, ebene = 4) {
|
||||||
const res = await fetch(`${BASE}/guides/${id}/content`)
|
const res = await fetch(`${BASE}/guides/${id}/content?ebene=${ebene}`)
|
||||||
if (!res.ok) throw new Error(`Inhalt nicht verfügbar (${res.status})`)
|
if (!res.ok) throw new Error(`Inhalt nicht verfügbar (${res.status})`)
|
||||||
return res.json()
|
return res.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Lern-Artefakte (Karteikarten/Beispiele/Diagramme) je Thema, gruppiert nach Baustein-Norm.
|
||||||
|
export async function fetchArtefakte(topic) {
|
||||||
|
const res = await fetch(`${BASE}/bausteine/artefakte?topic=${encodeURIComponent(topic)}`)
|
||||||
|
if (!res.ok) return { artefakte: {} }
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
|
||||||
// Einen Markdown-Block on-demand gegen die Guide-Regeln prüfen (Fokus, Rechtsklick).
|
// Einen Markdown-Block on-demand gegen die Guide-Regeln prüfen (Fokus, Rechtsklick).
|
||||||
export async function pruefeBlock(id, { baustein, stelle, block, hinweis = '', provider }) {
|
export async function pruefeBlock(id, { baustein, stelle, block, hinweis = '', provider }) {
|
||||||
const res = await fetch(`${BASE}/guides/${id}/block/pruefen`, {
|
const res = await fetch(`${BASE}/guides/${id}/block/pruefen`, {
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ const emit = defineEmits(['statusChanged', 'openFokus', 'setAnsicht'])
|
|||||||
|
|
||||||
// Lernstufen relativ zum cap (stufen.js): grün/blau/lila/gold @ 20/40/60/100 %.
|
// Lernstufen relativ zum cap (stufen.js): grün/blau/lila/gold @ 20/40/60/100 %.
|
||||||
// Prüfung IMMER zufällig aus 5 Formen (mit Muster); cap = 4×relevante Subbausteine.
|
// Prüfung IMMER zufällig aus 5 Formen (mit Muster); cap = 4×relevante Subbausteine.
|
||||||
const MODI = ['quiz_leicht', 'quiz_schwer', 'lueck_leicht', 'lueck_schwer', 'erklaeren']
|
// Vier Frageformen, zufällig. Kein leicht/schwer mehr — die Schwierigkeit setzt das Backend
|
||||||
|
// über das Lerner-Niveau (Adressaten-Rolle). lueckwahl = Begriff-Auswahl · lueckentext = frei.
|
||||||
|
const MODI = ['quiz', 'lueckwahl', 'lueckentext', 'erklaeren']
|
||||||
const st = computed(() => props.status || { gute_antworten: 0, streak: 0, cap: props.cap })
|
const st = computed(() => props.status || { gute_antworten: 0, streak: 0, cap: props.cap })
|
||||||
const score = computed(() => st.value.gute_antworten || 0)
|
const score = computed(() => st.value.gute_antworten || 0)
|
||||||
const cap = computed(() => st.value.cap || props.cap)
|
const cap = computed(() => st.value.cap || props.cap)
|
||||||
@@ -238,20 +240,20 @@ function naechsterModus() {
|
|||||||
|
|
||||||
// Ein Frage-Objekt eines bestimmten Modus generieren (aus gezogener Saat; Erklären hat Fallback).
|
// Ein Frage-Objekt eines bestimmten Modus generieren (aus gezogener Saat; Erklären hat Fallback).
|
||||||
function frageEinzeln(modus = naechsterModus()) {
|
function frageEinzeln(modus = naechsterModus()) {
|
||||||
const form = modus.startsWith('quiz') ? 'quiz' : modus.startsWith('lueck') ? 'lueckentext' : 'erklaeren'
|
const form = modus === 'quiz' ? 'quiz' : modus.startsWith('lueck') ? 'lueckentext' : 'erklaeren'
|
||||||
const istSchwer = modus.endsWith('_schwer')
|
const istLueckFrei = modus === 'lueckentext' // frei tippen vs. lueckwahl = Begriff aus 4
|
||||||
const muster = musterNehmen()
|
const muster = musterNehmen()
|
||||||
const basis = {
|
const basis = {
|
||||||
topic: props.topic, baustein: props.baustein, section: props.section,
|
topic: props.topic, baustein: props.baustein, section: props.section,
|
||||||
section_kompakt: props.sectionKompakt, provider: props.provider,
|
section_kompakt: props.sectionKompakt, provider: props.provider,
|
||||||
}
|
}
|
||||||
if (form === 'quiz' && muster) {
|
if (form === 'quiz' && muster) {
|
||||||
return pruefeBaustein({ ...basis, aktion: 'quiz_frage', muster, schwer: istSchwer })
|
return pruefeBaustein({ ...basis, aktion: 'quiz_frage', muster }) // Single-Choice, Niveau steuert
|
||||||
.then((r) => ({ form, schwer: istSchwer, frage: r.frage, optionen: r.optionen }))
|
.then((r) => ({ form, schwer: false, frage: r.frage, optionen: r.optionen }))
|
||||||
}
|
}
|
||||||
if (form === 'lueckentext' && muster) {
|
if (form === 'lueckentext' && muster) {
|
||||||
return pruefeBaustein({ ...basis, aktion: 'lueck_frage', muster, schwer: istSchwer })
|
return pruefeBaustein({ ...basis, aktion: 'lueck_frage', muster, schwer: istLueckFrei })
|
||||||
.then((r) => istSchwer
|
.then((r) => istLueckFrei
|
||||||
? ({ form, schwer: true, frage: r.satz, satz: r.satz, loesung: r.loesung, alternativen: r.alternativen })
|
? ({ form, schwer: true, frage: r.satz, satz: r.satz, loesung: r.loesung, alternativen: r.alternativen })
|
||||||
: ({ form, schwer: false, frage: r.satz, satz: r.satz, optionen: r.optionen }))
|
: ({ form, schwer: false, frage: r.satz, satz: r.satz, optionen: r.optionen }))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ const items = ref([])
|
|||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const error = ref(null)
|
const error = ref(null)
|
||||||
|
|
||||||
// Stufen-Reihenfolge + Anzeige-Label (Farbe via CSS-Klasse st-<key>)
|
// Lernpfad-Stufen: Reihenfolge + Label (Farbe via CSS-Klasse st-<key>).
|
||||||
const STUFEN = [
|
const STUFEN = [
|
||||||
{ key: 'einfach', label: 'Einfach' },
|
{ key: 'anfaenger', label: 'Anfänger' },
|
||||||
{ key: 'mittel', label: 'Mittel' },
|
{ key: 'fortgeschritten', label: 'Fortgeschritten' },
|
||||||
{ key: 'schwer', label: 'Schwer' },
|
{ key: 'experte', label: 'Experte' },
|
||||||
]
|
]
|
||||||
|
// Alt-Themen tragen noch einfach/mittel/schwer → auf die neuen Keys mappen.
|
||||||
|
const ALT_STUFE = { einfach: 'anfaenger', mittel: 'fortgeschritten', schwer: 'experte' }
|
||||||
|
|
||||||
watch(() => props.topic, load, { immediate: true })
|
watch(() => props.topic, load, { immediate: true })
|
||||||
|
|
||||||
@@ -43,7 +45,7 @@ function relevant(b) {
|
|||||||
// Nur nicht-leere Stufen-Gruppen je Baustein (v-if + v-for nicht auf einem Element)
|
// Nur nicht-leere Stufen-Gruppen je Baustein (v-if + v-for nicht auf einem Element)
|
||||||
function gruppen(b) {
|
function gruppen(b) {
|
||||||
return STUFEN
|
return STUFEN
|
||||||
.map((st) => ({ ...st, subs: (b.subbausteine || []).filter((s) => s.stufe === st.key) }))
|
.map((st) => ({ ...st, subs: (b.subbausteine || []).filter((s) => (ALT_STUFE[s.stufe] || s.stufe) === st.key) }))
|
||||||
.filter((g) => g.subs.length)
|
.filter((g) => g.subs.length)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,9 +187,9 @@ const subTotal = computed(() => items.value.reduce((n, b) => n + (b.subbausteine
|
|||||||
border-left: 3px solid var(--st);
|
border-left: 3px solid var(--st);
|
||||||
padding-left: 0.6rem;
|
padding-left: 0.6rem;
|
||||||
}
|
}
|
||||||
.bk-stufe.st-einfach { --st: var(--success-border); }
|
.bk-stufe.st-anfaenger { --st: var(--stufe-anfaenger); }
|
||||||
.bk-stufe.st-mittel { --st: var(--warning-border); }
|
.bk-stufe.st-fortgeschritten { --st: var(--stufe-fortgeschritten); }
|
||||||
.bk-stufe.st-schwer { --st: var(--danger); }
|
.bk-stufe.st-experte { --st: var(--stufe-experte); }
|
||||||
|
|
||||||
.bk-stufe-label {
|
.bk-stufe-label {
|
||||||
font-size: 0.66rem;
|
font-size: 0.66rem;
|
||||||
|
|||||||
106
frontend/src/components/FlashcardWidget.vue
Normal file
106
frontend/src/components/FlashcardWidget.vue
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { renderMarkdownInline } from '../markdown.js'
|
||||||
|
|
||||||
|
const props = defineProps({ karten: { type: Array, default: () => [] } })
|
||||||
|
|
||||||
|
const offen = ref(false)
|
||||||
|
const reihenfolge = ref([])
|
||||||
|
const pos = ref(0)
|
||||||
|
const flipped = ref(false)
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
reihenfolge.value = props.karten.map((_, i) => i)
|
||||||
|
pos.value = 0
|
||||||
|
flipped.value = false
|
||||||
|
}
|
||||||
|
watch(() => props.karten, reset, { immediate: true })
|
||||||
|
|
||||||
|
const aktuelle = computed(() => props.karten[reihenfolge.value[pos.value]] || null)
|
||||||
|
const zaehler = computed(() => `${Math.min(pos.value + 1, reihenfolge.value.length)} / ${reihenfolge.value.length}`)
|
||||||
|
|
||||||
|
function weiter(gewusst) {
|
||||||
|
if (gewusst) {
|
||||||
|
pos.value++
|
||||||
|
} else {
|
||||||
|
// „Nochmal" → Karte ans Ende der Runde schieben (leichtes Spacing).
|
||||||
|
const [k] = reihenfolge.value.splice(pos.value, 1)
|
||||||
|
reihenfolge.value.push(k)
|
||||||
|
}
|
||||||
|
if (pos.value >= reihenfolge.value.length) pos.value = 0
|
||||||
|
flipped.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="karten.length" class="flashcards">
|
||||||
|
<button class="art-head" @click="offen = !offen">
|
||||||
|
<span class="art-icon">🃏</span> Karteikarten
|
||||||
|
<span class="art-count">{{ karten.length }}</span>
|
||||||
|
<span class="art-toggle">{{ offen ? '▾' : '▸' }}</span>
|
||||||
|
</button>
|
||||||
|
<div v-if="offen && aktuelle" class="fc-body">
|
||||||
|
<div class="fc-card" :class="{ flipped }" @click="flipped = !flipped">
|
||||||
|
<div class="fc-zaehler">{{ zaehler }}</div>
|
||||||
|
<div v-if="!flipped" class="fc-seite">
|
||||||
|
<span class="fc-label">Frage</span>
|
||||||
|
<div class="fc-text" v-html="renderMarkdownInline(aktuelle.frage)"></div>
|
||||||
|
<span class="fc-hint">Klick zum Umdrehen</span>
|
||||||
|
</div>
|
||||||
|
<div v-else class="fc-seite">
|
||||||
|
<span class="fc-label">Antwort</span>
|
||||||
|
<div class="fc-text" v-html="renderMarkdownInline(aktuelle.antwort)"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="flipped" class="fc-aktionen">
|
||||||
|
<button class="fc-btn nochmal" @click="weiter(false)">Nochmal</button>
|
||||||
|
<button class="fc-btn gewusst" @click="weiter(true)">Gewusst</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.flashcards { margin-top: 0.75rem; }
|
||||||
|
.art-head {
|
||||||
|
display: flex; align-items: center; gap: 8px; width: 100%;
|
||||||
|
background: none; border: none; cursor: pointer; padding: 0.35rem 0;
|
||||||
|
font-size: 0.82rem; font-weight: 600; color: var(--text-muted);
|
||||||
|
}
|
||||||
|
.art-icon { font-size: 0.95rem; }
|
||||||
|
.art-count {
|
||||||
|
background: var(--panel-soft); border: 1px solid var(--border);
|
||||||
|
border-radius: 999px; padding: 0 0.45rem; font-size: 0.72rem;
|
||||||
|
}
|
||||||
|
.art-toggle { margin-left: auto; color: var(--text-faint); }
|
||||||
|
|
||||||
|
.fc-body { margin-top: 0.5rem; }
|
||||||
|
.fc-card {
|
||||||
|
position: relative; min-height: 120px; cursor: pointer;
|
||||||
|
border: 1px solid var(--border); border-radius: 10px;
|
||||||
|
padding: 1rem 1.1rem; background: var(--panel-soft);
|
||||||
|
display: flex; align-items: center; justify-content: center; text-align: center;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
}
|
||||||
|
.fc-card.flipped { border-color: var(--accent); }
|
||||||
|
.fc-zaehler {
|
||||||
|
position: absolute; top: 6px; right: 10px;
|
||||||
|
font-size: 0.68rem; color: var(--text-faint);
|
||||||
|
}
|
||||||
|
.fc-seite { display: flex; flex-direction: column; gap: 0.4rem; align-items: center; }
|
||||||
|
.fc-label {
|
||||||
|
font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.05em;
|
||||||
|
color: var(--text-faint); font-weight: 700;
|
||||||
|
}
|
||||||
|
.fc-text { font-size: 0.98rem; line-height: 1.45; }
|
||||||
|
.fc-hint { font-size: 0.7rem; color: var(--text-faint); margin-top: 0.2rem; }
|
||||||
|
|
||||||
|
.fc-aktionen { display: flex; gap: 8px; margin-top: 0.5rem; }
|
||||||
|
.fc-btn {
|
||||||
|
flex: 1; padding: 0.5rem; border-radius: 8px; cursor: pointer;
|
||||||
|
font-size: 0.82rem; font-weight: 600; border: 1px solid var(--border-strong);
|
||||||
|
background: var(--panel);
|
||||||
|
}
|
||||||
|
.fc-btn.gewusst { background: var(--success-soft); border-color: var(--success-border); color: var(--success); }
|
||||||
|
.fc-btn.nochmal { color: var(--text-muted); }
|
||||||
|
</style>
|
||||||
120
frontend/src/components/GeneralExamPanel.vue
Normal file
120
frontend/src/components/GeneralExamPanel.vue
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
<script setup>
|
||||||
|
// Themenweite Misch-Prüfung: wählt einen Baustein (priorisiert: noch nicht auf Max), darunter
|
||||||
|
// zufällig, und nutzt dafür das normale BausteinPanel. Jeder Baustein hat seinen eigenen
|
||||||
|
// Prüfungs-Slot (pruefungCache), also bleiben die Frage-Pools getrennt. Buchung läuft je
|
||||||
|
// Baustein über die normale Prüfungs-Route. Gesamt-cap = Σ cap_final aller prüfbaren Bausteine.
|
||||||
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { fetchGuides, fetchGuideContent, fetchBausteinLernstand } from '../api.js'
|
||||||
|
import BausteinPanel from './BausteinPanel.vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
topic: { type: String, required: true },
|
||||||
|
provider: { type: String, default: 'claude' },
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['progressChanged', 'fokus-active'])
|
||||||
|
|
||||||
|
const sections = ref([]) // [{titel, md, kompakt, num}] — prüfbare Sections der Vollfassung
|
||||||
|
const lernstand = ref({}) // titel -> {gute_antworten, streak, cap, cap_aktuell, freie_ebene}
|
||||||
|
const aktiv = ref(null) // aktueller Baustein-Titel
|
||||||
|
const ladefehler = ref(null)
|
||||||
|
|
||||||
|
const pruefbare = computed(() => sections.value.filter((s) => (lernstand.value[s.titel]?.cap || 0) > 0))
|
||||||
|
|
||||||
|
// Gesamt-Fortschritt: Σ min(score, cap_final) / Σ cap_final über alle prüfbaren Bausteine.
|
||||||
|
const gesamt = computed(() => {
|
||||||
|
let score = 0, cap = 0
|
||||||
|
for (const s of pruefbare.value) {
|
||||||
|
const l = lernstand.value[s.titel] || {}
|
||||||
|
score += Math.min(l.gute_antworten || 0, l.cap || 0)
|
||||||
|
cap += l.cap || 0
|
||||||
|
}
|
||||||
|
return { score, cap }
|
||||||
|
})
|
||||||
|
|
||||||
|
const aktivSection = computed(() => sections.value.find((s) => s.titel === aktiv.value) || null)
|
||||||
|
const aktivStatus = computed(() => lernstand.value[aktiv.value] || null)
|
||||||
|
|
||||||
|
// Priorität: Bausteine, die noch nicht auf cap_final sind; darunter zufällig.
|
||||||
|
function waehleBaustein() {
|
||||||
|
const offen = pruefbare.value.filter((s) => {
|
||||||
|
const l = lernstand.value[s.titel] || {}
|
||||||
|
return (l.gute_antworten || 0) < (l.cap || 0)
|
||||||
|
})
|
||||||
|
const pool = offen.length ? offen : pruefbare.value
|
||||||
|
aktiv.value = pool.length ? pool[Math.floor(Math.random() * pool.length)].titel : null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ladeLernstand() {
|
||||||
|
try {
|
||||||
|
lernstand.value = (await fetchBausteinLernstand(props.topic)).bausteine || {}
|
||||||
|
} catch { /* offline → alter Stand */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
emit('fokus-active', true)
|
||||||
|
try {
|
||||||
|
const guides = await fetchGuides()
|
||||||
|
const g = guides
|
||||||
|
.filter((x) => x.topic === props.topic && x.format === 'Guide' && x.status === 'done')
|
||||||
|
.sort((a, b) => (a.created_at < b.created_at ? 1 : -1))[0]
|
||||||
|
if (!g) { ladefehler.value = 'Kein fertiger Guide — erst einen Guide erstellen.'; return }
|
||||||
|
const content = await fetchGuideContent(g.id, 4) // Vollfassung (alle Subs)
|
||||||
|
sections.value = (content.chapters || [])
|
||||||
|
.flatMap((ch) => ch.sections || [])
|
||||||
|
.filter((s) => s.pruefbar !== false)
|
||||||
|
.map((s) => ({ titel: s.title, md: s.md, kompakt: s.kompakt, num: s.num }))
|
||||||
|
await ladeLernstand()
|
||||||
|
waehleBaustein()
|
||||||
|
} catch {
|
||||||
|
ladefehler.value = 'Konnte die Prüfung nicht laden.'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => emit('fokus-active', false))
|
||||||
|
|
||||||
|
async function onStatus() {
|
||||||
|
await ladeLernstand() // cap_aktuell/freie_ebene können nach einer Antwort gewachsen sein
|
||||||
|
emit('progressChanged')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="ge-panel">
|
||||||
|
<div class="ge-head">
|
||||||
|
<h2>Allgemeine Prüfung</h2>
|
||||||
|
<span v-if="!ladefehler" class="ge-score">{{ gesamt.score }} / {{ gesamt.cap }}</span>
|
||||||
|
<button v-if="aktiv" class="ge-next" title="Anderen Baustein prüfen" @click="waehleBaustein">⟳ Anderer Baustein</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="ladefehler" class="ge-msg">{{ ladefehler }}</p>
|
||||||
|
<p v-else-if="!pruefbare.length" class="ge-msg">Noch keine prüfbaren Bausteine.</p>
|
||||||
|
<p v-else-if="!aktiv" class="ge-msg">Alles auf Max — nichts mehr zu prüfen. 🎉</p>
|
||||||
|
|
||||||
|
<div v-else class="ge-body">
|
||||||
|
<div class="ge-baustein">Baustein: <strong>{{ aktiv }}</strong></div>
|
||||||
|
<BausteinPanel
|
||||||
|
:key="aktiv"
|
||||||
|
mode="full"
|
||||||
|
:topic="topic"
|
||||||
|
:baustein="aktiv"
|
||||||
|
:section="aktivSection?.md || ''"
|
||||||
|
:section-kompakt="aktivSection?.kompakt || ''"
|
||||||
|
:provider="provider"
|
||||||
|
:status="aktivStatus"
|
||||||
|
:cap="aktivStatus?.cap || 0"
|
||||||
|
@statusChanged="onStatus"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.ge-panel { padding: 16px 20px; max-width: 900px; margin: 0 auto; }
|
||||||
|
.ge-head { display: flex; align-items: center; gap: 14px; margin-bottom: 12px; }
|
||||||
|
.ge-head h2 { margin: 0; font-size: 1.1rem; }
|
||||||
|
.ge-score { font-variant-numeric: tabular-nums; color: var(--text-muted); font-weight: 600; }
|
||||||
|
.ge-next { margin-left: auto; padding: 5px 10px; border: 1px solid var(--border-strong); border-radius: 6px; background: var(--bg); cursor: pointer; }
|
||||||
|
.ge-next:hover { color: var(--accent-hover); }
|
||||||
|
.ge-msg { color: var(--text-muted); }
|
||||||
|
.ge-baustein { margin-bottom: 8px; color: var(--text-muted); font-size: 0.9rem; }
|
||||||
|
</style>
|
||||||
@@ -1,11 +1,22 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, reactive, ref, watch, nextTick, onMounted, onUnmounted } from 'vue'
|
import { computed, reactive, ref, watch, nextTick, onMounted, onUnmounted } from 'vue'
|
||||||
import { fetchGuideContent, chatGuide, fetchBausteinLernstand } from '../api.js'
|
import { fetchGuideContent, chatGuide, fetchBausteinLernstand, fetchArtefakte } from '../api.js'
|
||||||
import { renderMarkdown } from '../markdown.js'
|
import { renderMarkdown, renderMermaid } from '../markdown.js'
|
||||||
import { stufeFuer, schwelle } from '../stufen.js'
|
import { stufeFuer, schwelle } from '../stufen.js'
|
||||||
import { useChat } from '../composables/useChat.js'
|
import { useChat } from '../composables/useChat.js'
|
||||||
import BausteinPanel from './BausteinPanel.vue'
|
import BausteinPanel from './BausteinPanel.vue'
|
||||||
import BausteinFokus from './BausteinFokus.vue'
|
import BausteinFokus from './BausteinFokus.vue'
|
||||||
|
import FlashcardWidget from './FlashcardWidget.vue'
|
||||||
|
import WorkedExampleBlock from './WorkedExampleBlock.vue'
|
||||||
|
|
||||||
|
// Titel-Normalisierung wie backend _norm_titel (casefold ≈ toLowerCase + ß→ss) — für das
|
||||||
|
// Andocken der Artefakte (gekeyt nach baustein_norm) an die Section-Titel.
|
||||||
|
function normTitel(s) {
|
||||||
|
return (s || '').normalize('NFKC')
|
||||||
|
.replace(/[`'"<>„“”‚’«»*_]/g, '').replace(/[–—‐]/g, '-')
|
||||||
|
.replace(/\s+/g, ' ').trim().replace(/^[.:;]+|[.:;]+$/g, '').trim()
|
||||||
|
.toLowerCase().replace(/ß/g, 'ss')
|
||||||
|
}
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
previewGuide: { type: Object, default: null },
|
previewGuide: { type: Object, default: null },
|
||||||
@@ -15,6 +26,7 @@ const props = defineProps({
|
|||||||
doneByFormat: { type: Object, default: () => ({}) }, // Format → fertiger Guide (Themen-bezogen)
|
doneByFormat: { type: Object, default: () => ({}) }, // Format → fertiger Guide (Themen-bezogen)
|
||||||
themaAbgeschlossen: { type: Boolean, default: false },
|
themaAbgeschlossen: { type: Boolean, default: false },
|
||||||
ansichtModus: { type: String, default: 'kompakt' }, // kompakt | erklärend
|
ansichtModus: { type: String, default: 'kompakt' }, // kompakt | erklärend
|
||||||
|
stufeAnsicht: { type: Number, default: 4 }, // 1=A · 2=F · 3=E · 4=V
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['progressChanged', 'setAnsicht', 'openSidebar', 'fokusActive'])
|
const emit = defineEmits(['progressChanged', 'setAnsicht', 'openSidebar', 'fokusActive'])
|
||||||
@@ -27,6 +39,11 @@ const content = ref(null)
|
|||||||
const loadError = ref(null)
|
const loadError = ref(null)
|
||||||
const scrollEl = ref(null)
|
const scrollEl = ref(null)
|
||||||
const lernstand = ref({}) // Prüfungs-Stand pro Baustein-Titel — VOR dem immediate-Watch (loadContent nutzt es)
|
const lernstand = ref({}) // Prüfungs-Stand pro Baustein-Titel — VOR dem immediate-Watch (loadContent nutzt es)
|
||||||
|
const artefakte = ref({}) // baustein_norm → {karteikarte[], beispiel[], diagramm}
|
||||||
|
|
||||||
|
function artefakteVon(title) {
|
||||||
|
return artefakte.value[normTitel(title)] || null
|
||||||
|
}
|
||||||
|
|
||||||
// --- Lazy-Render + Markdown-Cache: nur sichtbare Sections parsen, jede nur einmal.
|
// --- Lazy-Render + Markdown-Cache: nur sichtbare Sections parsen, jede nur einmal.
|
||||||
// Behebt das Blockieren beim Öffnen (160× marked/highlight.js) und Re-Parse bei jedem Update. ---
|
// Behebt das Blockieren beim Öffnen (160× marked/highlight.js) und Re-Parse bei jedem Update. ---
|
||||||
@@ -53,15 +70,20 @@ function setupLazy() {
|
|||||||
if (!e.isIntersecting) continue
|
if (!e.isIntersecting) continue
|
||||||
sichtbar[Number(e.target.dataset.num)] = true
|
sichtbar[Number(e.target.dataset.num)] = true
|
||||||
mdObserver.unobserve(e.target)
|
mdObserver.unobserve(e.target)
|
||||||
|
nextTick(() => renderMermaid(e.target)) // Diagramme der nun sichtbaren Section rendern
|
||||||
}
|
}
|
||||||
}, { root: scrollEl.value, rootMargin: '800px 0px' })
|
}, { root: scrollEl.value, rootMargin: '800px 0px' })
|
||||||
for (const el of scrollEl.value.querySelectorAll('.section-card')) mdObserver.observe(el)
|
for (const el of scrollEl.value.querySelectorAll('.section-card')) mdObserver.observe(el)
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(content, () => nextTick(setupLazy))
|
watch(content, () => nextTick(setupLazy))
|
||||||
|
// Artefakte kommen asynchron nach dem Content — bereits sichtbare Diagramme nachrendern.
|
||||||
|
watch(artefakte, () => nextTick(() => renderMermaid(scrollEl.value)))
|
||||||
onUnmounted(() => mdObserver?.disconnect())
|
onUnmounted(() => mdObserver?.disconnect())
|
||||||
|
|
||||||
watch(() => props.previewGuide?.id, loadContent, { immediate: true })
|
watch(() => props.previewGuide?.id, loadContent, { immediate: true })
|
||||||
|
// Stufen-Ansicht (E/M/S/F) gewechselt → Guide-Inhalt neu mit passendem Ebenen-Filter laden.
|
||||||
|
watch(() => props.stufeAnsicht, loadContent)
|
||||||
|
|
||||||
async function loadContent() {
|
async function loadContent() {
|
||||||
content.value = null
|
content.value = null
|
||||||
@@ -72,7 +94,7 @@ async function loadContent() {
|
|||||||
const g = props.previewGuide
|
const g = props.previewGuide
|
||||||
if (!g || g.status !== 'done') return
|
if (!g || g.status !== 'done') return
|
||||||
try {
|
try {
|
||||||
content.value = await fetchGuideContent(g.id)
|
content.value = await fetchGuideContent(g.id, props.stufeAnsicht)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Fehler beim Laden des Guides:', e)
|
console.error('Fehler beim Laden des Guides:', e)
|
||||||
loadError.value = 'Inhalt nicht verfügbar — die Datei fehlt. Guide neu generieren (▶).'
|
loadError.value = 'Inhalt nicht verfügbar — die Datei fehlt. Guide neu generieren (▶).'
|
||||||
@@ -81,6 +103,9 @@ async function loadContent() {
|
|||||||
try {
|
try {
|
||||||
lernstand.value = (await fetchBausteinLernstand(g.topic)).bausteine || {}
|
lernstand.value = (await fetchBausteinLernstand(g.topic)).bausteine || {}
|
||||||
} catch { /* offline → leer */ }
|
} catch { /* offline → leer */ }
|
||||||
|
try {
|
||||||
|
artefakte.value = (await fetchArtefakte(g.topic)).artefakte || {}
|
||||||
|
} catch { artefakte.value = {} }
|
||||||
// Beim Öffnen zum ersten noch nicht gemeisterten prüfbaren Baustein scrollen.
|
// Beim Öffnen zum ersten noch nicht gemeisterten prüfbaren Baustein scrollen.
|
||||||
await nextTick()
|
await nextTick()
|
||||||
const ziel = bausteine.value.find((s) => istPruefbar(s) && stufeVon(s.title)?.key !== 'meister')
|
const ziel = bausteine.value.find((s) => istPruefbar(s) && stufeVon(s.title)?.key !== 'meister')
|
||||||
@@ -277,6 +302,11 @@ function extractContext() {
|
|||||||
</h3>
|
</h3>
|
||||||
<div v-if="sichtbar[s.num]" class="section-body markdown" v-html="htmlFor(s)"></div>
|
<div v-if="sichtbar[s.num]" class="section-body markdown" v-html="htmlFor(s)"></div>
|
||||||
<div v-else class="section-body skeleton"></div>
|
<div v-else class="section-body skeleton"></div>
|
||||||
|
<template v-if="sichtbar[s.num] && artefakteVon(s.title)">
|
||||||
|
<pre v-if="artefakteVon(s.title).diagramm" class="mermaid art-diagramm">{{ artefakteVon(s.title).diagramm.mermaid }}</pre>
|
||||||
|
<WorkedExampleBlock :beispiele="artefakteVon(s.title).beispiel || []" />
|
||||||
|
<FlashcardWidget :karten="artefakteVon(s.title).karteikarte || []" />
|
||||||
|
</template>
|
||||||
<BausteinPanel
|
<BausteinPanel
|
||||||
v-if="istPruefbar(s)"
|
v-if="istPruefbar(s)"
|
||||||
mode="trigger"
|
mode="trigger"
|
||||||
@@ -519,6 +549,19 @@ function extractContext() {
|
|||||||
damit der IntersectionObserver die folgenden Karten gestaffelt erkennt. */
|
damit der IntersectionObserver die folgenden Karten gestaffelt erkennt. */
|
||||||
.section-body.skeleton { min-height: 160px; }
|
.section-body.skeleton { min-height: 160px; }
|
||||||
|
|
||||||
|
/* Diagramm-Artefakt (Mermaid): zentriert, dezent gerahmt. Vor dem Rendern zeigt
|
||||||
|
<pre> den Quelltext — danach ersetzt mermaid.run ihn durch SVG. */
|
||||||
|
.art-diagramm {
|
||||||
|
margin: 0.75rem 0;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: var(--panel-soft);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.art-diagramm :deep(svg) { max-width: 100%; height: auto; }
|
||||||
|
|
||||||
.empty-preview {
|
.empty-preview {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -23,9 +23,10 @@ const props = defineProps({
|
|||||||
providers: { type: Array, default: () => [] },
|
providers: { type: Array, default: () => [] },
|
||||||
folders: { type: Object, default: () => ({ projekt: [], uni: [] }) },
|
folders: { type: Object, default: () => ({ projekt: [], uni: [] }) },
|
||||||
ansichtModus: { type: String, default: 'kompakt' }, // kompakt | erklärend
|
ansichtModus: { type: String, default: 'kompakt' }, // kompakt | erklärend
|
||||||
|
stufeAnsicht: { type: Number, default: 4 }, // 1=A · 2=F · 3=E · 4=V
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['select', 'create', 'createThema', 'updateQuelle', 'formatClick', 'bausteineClick', 'cancelBausteine', 'resetBausteine', 'deleteTopic', 'cancelGuide', 'deleteGuide', 'dismissError', 'dismissUiError', 'preview', 'openBausteineView', 'openElements', 'togglePin', 'sidebarLeave', 'toggleDark', 'setAnsicht', 'setProvider'])
|
const emit = defineEmits(['select', 'create', 'createThema', 'updateQuelle', 'formatClick', 'bausteineClick', 'cancelBausteine', 'resetBausteine', 'deleteTopic', 'cancelGuide', 'deleteGuide', 'dismissError', 'dismissUiError', 'preview', 'openBausteineView', 'openElements', 'togglePin', 'sidebarLeave', 'toggleDark', 'setAnsicht', 'setStufe', 'generalExam', 'setProvider'])
|
||||||
|
|
||||||
// Accordion: höchstens ein Panel offen. IDs: 'bausteine', 'fmt-<Format>', 'topic-<Name>'.
|
// Accordion: höchstens ein Panel offen. IDs: 'bausteine', 'fmt-<Format>', 'topic-<Name>'.
|
||||||
const openPanel = ref(null)
|
const openPanel = ref(null)
|
||||||
@@ -54,8 +55,14 @@ const trackerItems = computed(() => {
|
|||||||
|
|
||||||
const formats = [
|
const formats = [
|
||||||
{ key: 'Guide', label: 'Guide' },
|
{ key: 'Guide', label: 'Guide' },
|
||||||
{ key: 'FullGuide', label: 'FullGuide' },
|
]
|
||||||
{ key: 'Rest', label: 'Rest' },
|
|
||||||
|
// Stufen-Ansichten des EINEN Guides (gefiltert nach Subbaustein-Ebene).
|
||||||
|
const STUFEN_ANSICHT = [
|
||||||
|
{ k: 1, label: 'A', titel: 'Anfänger' },
|
||||||
|
{ k: 2, label: 'F', titel: 'Anfänger + Fortgeschritten' },
|
||||||
|
{ k: 3, label: 'E', titel: 'bis Experte' },
|
||||||
|
{ k: 4, label: 'V', titel: 'Vollständig (inkl. Rand)' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const bausteineState = computed(() => {
|
const bausteineState = computed(() => {
|
||||||
@@ -317,6 +324,11 @@ function saveQuelle() {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="selectedTopic" class="stufe-toggle" title="Tiefe der Guide-Ansicht">
|
||||||
|
<button v-for="s in STUFEN_ANSICHT" :key="s.k"
|
||||||
|
:class="{ active: stufeAnsicht === s.k }" :title="s.titel"
|
||||||
|
@click="emit('setStufe', s.k)">{{ s.label }}</button>
|
||||||
|
</div>
|
||||||
<button class="new-topic-toggle" :class="{ active: dlg }" title="Thema erstellen" @click="toggleErstellen">+</button>
|
<button class="new-topic-toggle" :class="{ active: dlg }" title="Thema erstellen" @click="toggleErstellen">+</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -462,6 +474,11 @@ function saveQuelle() {
|
|||||||
<button class="format-error-x" title="Ausblenden" @click="dismissError(f.key)">×</button>
|
<button class="format-error-x" title="Ausblenden" @click="dismissError(f.key)">×</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="format-row ord-pruefung">
|
||||||
|
<button class="format-name elements-btn" @click="emit('generalExam')">
|
||||||
|
<span class="format-label">Allgemeine Prüfung</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div class="format-row ord-elemente">
|
<div class="format-row ord-elemente">
|
||||||
<button class="format-name elements-btn" @click="emit('openElements')">
|
<button class="format-name elements-btn" @click="emit('openElements')">
|
||||||
<span class="format-label">Elemente</span>
|
<span class="format-label">Elemente</span>
|
||||||
@@ -607,6 +624,21 @@ function saveQuelle() {
|
|||||||
.new-topic .ansicht-toggle button:hover { color: var(--accent-hover); }
|
.new-topic .ansicht-toggle button:hover { color: var(--accent-hover); }
|
||||||
.new-topic .ansicht-toggle button.active { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
|
.new-topic .ansicht-toggle button.active { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
|
||||||
.ansicht-toggle svg { fill: currentColor; display: block; }
|
.ansicht-toggle svg { fill: currentColor; display: block; }
|
||||||
|
.stufe-toggle { display: inline-flex; margin-left: 4px; }
|
||||||
|
.new-topic .stufe-toggle button {
|
||||||
|
padding: 4px 6px;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text-muted);
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
min-width: 20px;
|
||||||
|
}
|
||||||
|
.new-topic .stufe-toggle button:first-child { border-radius: 6px 0 0 6px; }
|
||||||
|
.new-topic .stufe-toggle button:last-child { border-radius: 0 6px 6px 0; }
|
||||||
|
.new-topic .stufe-toggle button:not(:first-child) { border-left: none; }
|
||||||
|
.new-topic .stufe-toggle button:hover { color: var(--accent-hover); }
|
||||||
|
.new-topic .stufe-toggle button.active { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
|
||||||
|
|
||||||
.stats-bar {
|
.stats-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -800,10 +832,14 @@ function saveQuelle() {
|
|||||||
order: 2;
|
order: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ord-elemente {
|
.ord-pruefung {
|
||||||
order: 4;
|
order: 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ord-elemente {
|
||||||
|
order: 5;
|
||||||
|
}
|
||||||
|
|
||||||
.elements-btn {
|
.elements-btn {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
|
|||||||
67
frontend/src/components/WorkedExampleBlock.vue
Normal file
67
frontend/src/components/WorkedExampleBlock.vue
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { renderMarkdownInline } from '../markdown.js'
|
||||||
|
|
||||||
|
defineProps({ beispiele: { type: Array, default: () => [] } })
|
||||||
|
const offen = ref(false)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="beispiele.length" class="worked">
|
||||||
|
<button class="art-head" @click="offen = !offen">
|
||||||
|
<span class="art-icon">📝</span> Beispiele
|
||||||
|
<span class="art-count">{{ beispiele.length }}</span>
|
||||||
|
<span class="art-toggle">{{ offen ? '▾' : '▸' }}</span>
|
||||||
|
</button>
|
||||||
|
<div v-if="offen" class="we-body">
|
||||||
|
<div v-for="(b, i) in beispiele" :key="i" class="we-card">
|
||||||
|
<div v-if="b.subbaustein" class="we-sub">{{ b.subbaustein }}</div>
|
||||||
|
<div class="we-problem" v-html="renderMarkdownInline(b.problem)"></div>
|
||||||
|
<ol class="we-schritte">
|
||||||
|
<li v-for="(s, j) in b.schritte" :key="j" v-html="renderMarkdownInline(s)"></li>
|
||||||
|
</ol>
|
||||||
|
<div v-if="b.ergebnis" class="we-ergebnis">
|
||||||
|
<span class="we-label">Ergebnis</span>
|
||||||
|
<span v-html="renderMarkdownInline(b.ergebnis)"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.worked { margin-top: 0.5rem; }
|
||||||
|
.art-head {
|
||||||
|
display: flex; align-items: center; gap: 8px; width: 100%;
|
||||||
|
background: none; border: none; cursor: pointer; padding: 0.35rem 0;
|
||||||
|
font-size: 0.82rem; font-weight: 600; color: var(--text-muted);
|
||||||
|
}
|
||||||
|
.art-icon { font-size: 0.95rem; }
|
||||||
|
.art-count {
|
||||||
|
background: var(--panel-soft); border: 1px solid var(--border);
|
||||||
|
border-radius: 999px; padding: 0 0.45rem; font-size: 0.72rem;
|
||||||
|
}
|
||||||
|
.art-toggle { margin-left: auto; color: var(--text-faint); }
|
||||||
|
|
||||||
|
.we-body { margin-top: 0.4rem; display: flex; flex-direction: column; gap: 0.6rem; }
|
||||||
|
.we-card {
|
||||||
|
border: 1px solid var(--border); border-left: 3px solid var(--accent);
|
||||||
|
border-radius: 8px; padding: 0.7rem 0.9rem; background: var(--panel-soft);
|
||||||
|
font-size: 0.92rem; line-height: 1.5;
|
||||||
|
}
|
||||||
|
.we-sub {
|
||||||
|
font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.04em;
|
||||||
|
color: var(--text-faint); font-weight: 700; margin-bottom: 0.3rem;
|
||||||
|
}
|
||||||
|
.we-problem { font-weight: 600; margin-bottom: 0.4rem; }
|
||||||
|
.we-schritte { margin: 0 0 0.4rem 1.1rem; padding: 0; }
|
||||||
|
.we-schritte li { margin: 0.2rem 0; }
|
||||||
|
.we-ergebnis {
|
||||||
|
display: flex; gap: 6px; align-items: baseline;
|
||||||
|
padding-top: 0.35rem; border-top: 1px dashed var(--border);
|
||||||
|
}
|
||||||
|
.we-label {
|
||||||
|
font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.05em;
|
||||||
|
color: var(--success); font-weight: 700;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -6,6 +6,17 @@ import katex from 'katex'
|
|||||||
import 'katex/dist/katex.min.css'
|
import 'katex/dist/katex.min.css'
|
||||||
import DOMPurify from 'dompurify'
|
import DOMPurify from 'dompurify'
|
||||||
|
|
||||||
|
// Mermaid lazy laden (schwere Lib): erst beim ersten Diagramm, hält das Initial-Bundle klein.
|
||||||
|
// startOnLoad:false → wir rendern gezielt; securityLevel strict: kein HTML/JS in Graphen.
|
||||||
|
let _mermaid = null
|
||||||
|
async function getMermaid() {
|
||||||
|
if (!_mermaid) {
|
||||||
|
_mermaid = (await import('mermaid')).default
|
||||||
|
_mermaid.initialize({ startOnLoad: false, securityLevel: 'strict', theme: 'default', flowchart: { htmlLabels: false } })
|
||||||
|
}
|
||||||
|
return _mermaid
|
||||||
|
}
|
||||||
|
|
||||||
marked.use(markedHighlight({
|
marked.use(markedHighlight({
|
||||||
langPrefix: 'hljs language-',
|
langPrefix: 'hljs language-',
|
||||||
highlight(code, lang) {
|
highlight(code, lang) {
|
||||||
@@ -49,7 +60,33 @@ const inlineMath = {
|
|||||||
renderer(token) { return renderTex(token.text, false) },
|
renderer(token) { return renderTex(token.text, false) },
|
||||||
}
|
}
|
||||||
|
|
||||||
marked.use({ extensions: [blockMath, inlineMath] })
|
// ```mermaid → <pre class="mermaid">…</pre> (Quelltext escaped; mermaid.run liest den textContent).
|
||||||
|
const mermaidBlock = {
|
||||||
|
name: 'mermaidBlock',
|
||||||
|
level: 'block',
|
||||||
|
start(src) { const i = src.indexOf('```mermaid'); return i < 0 ? undefined : i },
|
||||||
|
tokenizer(src) {
|
||||||
|
const m = /^```mermaid[^\n]*\n([\s\S]+?)\n```/.exec(src)
|
||||||
|
if (m) return { type: 'mermaidBlock', raw: m[0], text: m[1] }
|
||||||
|
},
|
||||||
|
renderer(token) {
|
||||||
|
const esc = token.text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||||
|
return `<pre class="mermaid">${esc}</pre>`
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
marked.use({ extensions: [blockMath, inlineMath, mermaidBlock] })
|
||||||
|
|
||||||
|
// Mermaid-Knoten in einem Container rendern (nach v-html). Idempotent: bereits
|
||||||
|
// verarbeitete (data-processed) überspringt mermaid.run; defekte Graphen bleiben als Text.
|
||||||
|
export async function renderMermaid(root) {
|
||||||
|
const nodes = root?.querySelectorAll?.('pre.mermaid:not([data-processed="true"])')
|
||||||
|
if (!nodes || !nodes.length) return
|
||||||
|
try {
|
||||||
|
const m = await getMermaid()
|
||||||
|
await m.run({ nodes })
|
||||||
|
} catch { /* defektes Diagramm → Quelltext bleibt sichtbar */ }
|
||||||
|
}
|
||||||
|
|
||||||
// Rohes HTML im Markdown (z. B. <p>, <img> ohne Backticks aus Agenten-Output)
|
// Rohes HTML im Markdown (z. B. <p>, <img> ohne Backticks aus Agenten-Output)
|
||||||
// als Text anzeigen statt rendern — sonst verschluckt der Browser den Inhalt.
|
// als Text anzeigen statt rendern — sonst verschluckt der Browser den Inhalt.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Lernstufen je Baustein — identisch zum Backend (lernen.py STUFEN).
|
// Lernstufen je Baustein — identisch zum Backend (lernen.py STUFEN).
|
||||||
// Floor in % des cap (cap = clamp(4×relevante Subbausteine, 10, 50)).
|
// Floor in % des cap_final (cap_final = alle Subbausteine × 25).
|
||||||
// grün=Anfänger 20% · blau=Fortgeschritten 40% · lila=Experte 60% · gold=Meister 100%.
|
// grün=Anfänger 20% · blau=Fortgeschritten 40% · lila=Experte 60% · gold=Meister 100%.
|
||||||
export const STUFEN = [
|
export const STUFEN = [
|
||||||
{ key: 'anfaenger', label: 'Anfänger', kurz: '✓', floor: 0.2, farbe: 'var(--stufe-anfaenger)' },
|
{ key: 'anfaenger', label: 'Anfänger', kurz: '✓', floor: 0.2, farbe: 'var(--stufe-anfaenger)' },
|
||||||
@@ -25,11 +25,11 @@ export function naechste(score, cap) {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fehler-Strafe-Anzeige nach Fortschritt: 0–20%→0 · 21–40%→−1 · 41–60%→−2 · 61–100%→−3.
|
// Fehler-Strafe-Anzeige nach Fortschritt (gegen cap_aktuell): ≤25%→−5 · ≤50%→−10 · ≤75%→−15 · >75%→−20.
|
||||||
export function malusRegel(score, cap) {
|
export function malusRegel(score, cap) {
|
||||||
const pct = cap ? score / cap : 0
|
const pct = cap ? score / cap : 0
|
||||||
if (pct <= 0.2) return '0'
|
if (pct <= 0.25) return '−5'
|
||||||
if (pct <= 0.4) return '−1'
|
if (pct <= 0.5) return '−10'
|
||||||
if (pct <= 0.6) return '−2'
|
if (pct <= 0.75) return '−15'
|
||||||
return '−3'
|
return '−20'
|
||||||
}
|
}
|
||||||
|
|||||||
21
templates/Prompt/Artefakt-Beispiel.md
Normal file
21
templates/Prompt/Artefakt-Beispiel.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
Baue für die Subbausteine des Themas "{topic}" ein ausgearbeitetes Beispiel (Worked Example) — ein durchgearbeiteter Fall, der das Verständnis trägt.
|
||||||
|
|
||||||
|
BAUSTEINE MIT SUBBAUSTEINEN UND IHREN FAKTEN (gehe JEDEN Subbaustein durch):
|
||||||
|
{bausteine}
|
||||||
|
|
||||||
|
Ein gutes Worked Example:
|
||||||
|
- **problem**: eine konkrete, kleine Aufgabe/Frage zum Subbaustein (1 Satz).
|
||||||
|
- **schritte**: 2–5 nachvollziehbare Schritte vom Problem zur Lösung. Jeder Schritt ein knapper Satz, in der richtigen Reihenfolge.
|
||||||
|
- **ergebnis**: das Endergebnis / die Erkenntnis (1 Satz).
|
||||||
|
- Stütze dich auf `beispiel_idee` und die belegten Fakten des Subbausteins. Rechne sauber; erfinde keine Werte, die den Fakten widersprechen.
|
||||||
|
- **Nur wo es trägt:** Lässt sich ein Subbaustein nicht sinnvoll an einem Beispiel zeigen (reine Definition, Meta-Wissen), LASSE IHN WEG — kein erzwungenes Beispiel.
|
||||||
|
- `$…$` NUR für echte Mathematik. Code/Pfade/Bezeichner in Backticks (`` `…` ``).
|
||||||
|
|
||||||
|
Schreibe die Beispiele als EIN JSON in die Datei {out_path} (nutze dein Schreib-Werkzeug), GENAU so:
|
||||||
|
{{"beispiele": [
|
||||||
|
{{"baustein": "<exakter Baustein-Titel>", "subbaustein": "<exakter Subbaustein-Titel>",
|
||||||
|
"problem": "…", "schritte": ["…", "…"], "ergebnis": "…"}}
|
||||||
|
]}}
|
||||||
|
|
||||||
|
Gib sonst keinen Text aus.
|
||||||
|
{extra}
|
||||||
24
templates/Prompt/Artefakt-Diagramm.md
Normal file
24
templates/Prompt/Artefakt-Diagramm.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
Baue für jeden Baustein des Themas "{topic}" EIN Übersichts-Diagramm (Concept-Map) als Mermaid-Code. Es zeigt, wie die Subbausteine/Kernbegriffe des Bausteins zusammenhängen.
|
||||||
|
|
||||||
|
BAUSTEINE MIT SUBBAUSTEINEN UND IHREN FAKTEN:
|
||||||
|
{bausteine}
|
||||||
|
|
||||||
|
Regeln für das Diagramm:
|
||||||
|
- Genau EIN Mermaid-Graph je Baustein, Typ `flowchart TD` (oben→unten) oder `flowchart LR`.
|
||||||
|
- Knoten = die Subbausteine bzw. ihre Kernbegriffe. Kanten = echte Beziehungen (baut-auf, gehört-zu, führt-zu). Keine erfundenen Beziehungen.
|
||||||
|
- 3–8 Knoten. Lieber klar als vollständig. Stütze dich auf Voraussetzungen/Kernpunkte aus den Fakten.
|
||||||
|
- **Gültige Mermaid-Syntax, sonst rendert nichts:**
|
||||||
|
- Knoten-IDs sind kurze ASCII-Wörter ohne Leerzeichen (z. B. `dfa`, `nfa1`).
|
||||||
|
- Label in eckigen Klammern, Text schlicht: `dfa[Deterministischer Automat]`. KEINE Anführungszeichen, runden Klammern, Doppelpunkte oder `#` im Label.
|
||||||
|
- Kanten mit `-->` (optional Label: `a -->|baut auf| b`).
|
||||||
|
- Zeilen im `mermaid`-Feld mit `\n` trennen (ein JSON-String).
|
||||||
|
- Lässt sich ein Baustein nicht sinnvoll als Graph zeigen (ein einziger Begriff), LASSE IHN WEG.
|
||||||
|
|
||||||
|
Schreibe die Diagramme als EIN JSON in die Datei {out_path} (nutze dein Schreib-Werkzeug), GENAU so:
|
||||||
|
{{"diagramme": [
|
||||||
|
{{"baustein": "<exakter Baustein-Titel>",
|
||||||
|
"mermaid": "flowchart TD\n a[Alphabet] --> b[Wort]\n b --> c[Sprache]"}}
|
||||||
|
]}}
|
||||||
|
|
||||||
|
Gib sonst keinen Text aus.
|
||||||
|
{extra}
|
||||||
20
templates/Prompt/Artefakt-Karteikarte.md
Normal file
20
templates/Prompt/Artefakt-Karteikarte.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
Baue für jeden Subbaustein des Themas "{topic}" EINE Karteikarte (Frage→Antwort) zum aktiven Abrufen.
|
||||||
|
|
||||||
|
BAUSTEINE MIT SUBBAUSTEINEN UND IHREN FAKTEN (bearbeite JEDEN Subbaustein):
|
||||||
|
{bausteine}
|
||||||
|
|
||||||
|
Eine gute Karteikarte:
|
||||||
|
- **frage**: eine knappe Abruf-Frage, die genau einen Kernpunkt prüft (kein „Erkläre alles"). Eine Frage, eine Sache.
|
||||||
|
- **antwort**: die kurze, präzise Antwort — auf den belegten Fakten/Kernpunkten des Subbausteins. Nichts dazu erfinden.
|
||||||
|
- Stütze dich auf die gelieferten Fakten. Wo ein belegter Fakt existiert, muss die Antwort dazu passen.
|
||||||
|
- Knapp: Frage ≤ 15 Wörter, Antwort ≤ 25 Wörter. Keine Prosa, kein Vorspann.
|
||||||
|
- `$…$` NUR für echte Mathematik. Code/Pfade/Bezeichner in Backticks (`` `…` ``).
|
||||||
|
|
||||||
|
Schreibe ALLE Karten als EIN JSON in die Datei {out_path} (nutze dein Schreib-Werkzeug), GENAU so:
|
||||||
|
{{"karten": [
|
||||||
|
{{"baustein": "<exakter Baustein-Titel>", "subbaustein": "<exakter Subbaustein-Titel>",
|
||||||
|
"frage": "…", "antwort": "…"}}
|
||||||
|
]}}
|
||||||
|
|
||||||
|
Gib sonst keinen Text aus.
|
||||||
|
{extra}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
Du bist Prüfer in einem Lern-Guide zum Thema "{topic}", Baustein "{baustein}". Formuliere dem Lerner EINE Verständnisfrage. Du bekommst dafür ein **Muster** — eine Beispielfrage. Stelle dieselbe Sache, aber im Wortlaut **leicht anders** (andere Formulierung, gleicher Kern und gleiche Schwierigkeit).
|
Du bist Prüfer in einem Lern-Guide zum Thema "{topic}", Baustein "{baustein}". Formuliere dem Lerner EINE Verständnisfrage. Du bekommst dafür ein **Muster** — eine Beispielfrage zum Kernkonzept. Stelle dieselbe Sache, aber im Anspruch passend zum Lerner-Niveau (siehe unten) und im Wortlaut anders.
|
||||||
|
|
||||||
MUSTER (Vorlage — gleiche Sache, neuer Wortlaut):
|
MUSTER (Vorlage — gleicher Kern, neuer Wortlaut und Anspruch):
|
||||||
{muster}
|
{muster}
|
||||||
|
|
||||||
BAUSTEIN AUS DEM GUIDE (fachliche Referenz — die Frage darf ihr nie widersprechen):
|
BAUSTEIN AUS DEM GUIDE (fachliche Referenz — die Frage darf ihr nie widersprechen):
|
||||||
@@ -9,6 +9,9 @@ BAUSTEIN AUS DEM GUIDE (fachliche Referenz — die Frage darf ihr nie widersprec
|
|||||||
KOMPAKTE FASSUNG (Merksätze, falls vorhanden):
|
KOMPAKTE FASSUNG (Merksätze, falls vorhanden):
|
||||||
{kompakt_block}
|
{kompakt_block}
|
||||||
|
|
||||||
|
LERNER-NIVEAU (bestimmt den Anspruch der Frage):
|
||||||
|
{niveau_block}
|
||||||
|
|
||||||
ERDUNG AM BAUSTEIN (zuerst prüfen):
|
ERDUNG AM BAUSTEIN (zuerst prüfen):
|
||||||
- Das Muster wurde ohne den Guide-Text erstellt — es kann am Inhalt vorbeigehen.
|
- Das Muster wurde ohne den Guide-Text erstellt — es kann am Inhalt vorbeigehen.
|
||||||
- Prüfe das Muster gegen den BAUSTEIN oben: Lässt sich die Sache wirklich aus dem Material beantworten?
|
- Prüfe das Muster gegen den BAUSTEIN oben: Lässt sich die Sache wirklich aus dem Material beantworten?
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ GEWÜNSCHTER FRAGETYP:
|
|||||||
FOKUS:
|
FOKUS:
|
||||||
{fokus_block}
|
{fokus_block}
|
||||||
|
|
||||||
|
LERNER-NIVEAU (bestimmt den Anspruch der Frage — passe Tiefe und Anforderung daran an):
|
||||||
|
{niveau_block}
|
||||||
|
|
||||||
HARTE REGELN FÜR DIE FRAGE — wichtiger als alles andere:
|
HARTE REGELN FÜR DIE FRAGE — wichtiger als alles andere:
|
||||||
- GENAU EINE Frage. Ein einziges Fragezeichen. Eine einzige Sache.
|
- GENAU EINE Frage. Ein einziges Fragezeichen. Eine einzige Sache.
|
||||||
- Maximal 1–2 Sätze. Kein Szenario-Aufbau, keine Vorrede, kein "Angenommen … und außerdem …".
|
- Maximal 1–2 Sätze. Kein Szenario-Aufbau, keine Vorrede, kein "Angenommen … und außerdem …".
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ BAUSTEIN AUS DEM GUIDE (fachliche Referenz — der Satz darf ihr nie widersprech
|
|||||||
KOMPAKTE FASSUNG (Merksätze, falls vorhanden):
|
KOMPAKTE FASSUNG (Merksätze, falls vorhanden):
|
||||||
{kompakt_block}
|
{kompakt_block}
|
||||||
|
|
||||||
|
LERNER-NIVEAU (bestimmt den Anspruch von Satz und Lücke):
|
||||||
|
{niveau_block}
|
||||||
|
|
||||||
AUFGABE:
|
AUFGABE:
|
||||||
- Schreibe EINEN kurzen Aussagesatz (höchstens ~15 Wörter, keine Nebensatz-Kaskade).
|
- Schreibe EINEN kurzen Aussagesatz (höchstens ~15 Wörter, keine Nebensatz-Kaskade).
|
||||||
- Ersetze den **Schlüsselbegriff** durch `___` (genau eine Lücke).
|
- Ersetze den **Schlüsselbegriff** durch `___` (genau eine Lücke).
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ BAUSTEIN AUS DEM GUIDE (fachliche Referenz — der Satz darf ihr nie widersprech
|
|||||||
KOMPAKTE FASSUNG (Merksätze, falls vorhanden):
|
KOMPAKTE FASSUNG (Merksätze, falls vorhanden):
|
||||||
{kompakt_block}
|
{kompakt_block}
|
||||||
|
|
||||||
|
LERNER-NIVEAU (bestimmt den Anspruch von Satz und Optionen):
|
||||||
|
{niveau_block}
|
||||||
|
{distraktor_block}
|
||||||
AUFGABE:
|
AUFGABE:
|
||||||
- Schreibe EINEN kurzen Aussagesatz (höchstens ~15 Wörter). Ersetze den **Schlüsselbegriff** durch `___` (genau eine Lücke).
|
- Schreibe EINEN kurzen Aussagesatz (höchstens ~15 Wörter). Ersetze den **Schlüsselbegriff** durch `___` (genau eine Lücke).
|
||||||
- Die Lücke ist ein **Fachbegriff als Wort** — KEIN mathematisches Symbol/Formelteil
|
- Die Lücke ist ein **Fachbegriff als Wort** — KEIN mathematisches Symbol/Formelteil
|
||||||
|
|||||||
@@ -9,10 +9,13 @@ BAUSTEIN AUS DEM GUIDE (fachliche Referenz — Optionen dürfen ihr nie widerspr
|
|||||||
KOMPAKTE FASSUNG (Merksätze, falls vorhanden):
|
KOMPAKTE FASSUNG (Merksätze, falls vorhanden):
|
||||||
{kompakt_block}
|
{kompakt_block}
|
||||||
|
|
||||||
|
LERNER-NIVEAU (bestimmt den Anspruch von Frage und Distraktoren):
|
||||||
|
{niveau_block}
|
||||||
|
{distraktor_block}
|
||||||
AUFGABE:
|
AUFGABE:
|
||||||
- Formuliere EINE klare Frage und GENAU VIER Antwortoptionen.
|
- Formuliere EINE klare Frage und GENAU VIER Antwortoptionen.
|
||||||
- {anzahl_block}
|
- GENAU EINE Option ist richtig, die anderen drei sind klar falsch.
|
||||||
- Jede richtige Option ist sachlich aus dem Material belegbar. Jede falsche ist plausibel,
|
- Die richtige Option ist sachlich aus dem Material belegbar. Jede falsche ist plausibel,
|
||||||
aber klar falsch (typischer Irrtum, Verwechslung, Halbwahrheit) — keine Albernheiten.
|
aber klar falsch (typischer Irrtum, Verwechslung, Halbwahrheit) — keine Albernheiten.
|
||||||
- **Optionen SEHR KURZ:** ein Stichwort, Begriff oder Halbsatz, höchstens ~8 Wörter.
|
- **Optionen SEHR KURZ:** ein Stichwort, Begriff oder Halbsatz, höchstens ~8 Wörter.
|
||||||
KEINE ganzen Sätze, KEIN „Weil …"-Vorspann, keine Begründungs-Prosa. Nur der Kern.
|
KEINE ganzen Sätze, KEIN „Weil …"-Vorspann, keine Begründungs-Prosa. Nur der Kern.
|
||||||
@@ -25,5 +28,5 @@ Gib NUR dieses JSON aus (kein weiterer Text):
|
|||||||
{{"text": "Option A", "korrekt": true}},
|
{{"text": "Option A", "korrekt": true}},
|
||||||
{{"text": "Option B", "korrekt": false}},
|
{{"text": "Option B", "korrekt": false}},
|
||||||
{{"text": "Option C", "korrekt": false}},
|
{{"text": "Option C", "korrekt": false}},
|
||||||
{{"text": "Option D", "korrekt": true}}
|
{{"text": "Option D", "korrekt": false}}
|
||||||
]}}
|
]}}
|
||||||
|
|||||||
22
templates/Prompt/Fakten-Check.md
Normal file
22
templates/Prompt/Fakten-Check.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
Du bist Faktenprüfer für die Lern-Fakten des Themas "{topic}". Ein anderer Agent hat je Subbaustein Fakten extrahiert. Prüfe NUR die **belegten Fakten** auf Wahrheit — nicht die Beispiele.
|
||||||
|
|
||||||
|
{source}
|
||||||
|
|
||||||
|
ZU PRÜFENDE FAKTEN (je Subbaustein):
|
||||||
|
{fakten}
|
||||||
|
|
||||||
|
Prüfe je Subbaustein:
|
||||||
|
1. **Beleg-Treue**: Steht jeder `belegte_fakten`-Eintrag so in der Quelle (sinngemäß-genau)? Stimmt die Quellenangabe? Mit Quelldatei: gegen die Datei prüfen. Ohne Quelle: ist es gesichertes Standardwissen?
|
||||||
|
2. **Sachliche Korrektheit**: Sind Formeln, Werte, Definitionen, Signaturen fachlich richtig? Eine falsche Formel/ein falscher Wert ist ein Mangel.
|
||||||
|
3. **Fakt vs. Beispiel**: Ist ein selbst gerechnetes/erfundenes Beispiel fälschlich als `belegte_fakten` deklariert? Das ist ein Mangel — es gehört in `beispiel_idee`.
|
||||||
|
4. Beispiele (`beispiel_idee`) NICHT auf Quellen-Beleg prüfen — sie sind generativ.
|
||||||
|
|
||||||
|
Notiere nur ECHTE Mängel (falscher Fakt, falsche Quelle, Beispiel als Fakt getarnt). Geschmacksfragen nicht.
|
||||||
|
|
||||||
|
Schreibe NUR die JSON-Datei nach: {out_path}
|
||||||
|
|
||||||
|
Format — alles in Ordnung:
|
||||||
|
{{"ok": true}}
|
||||||
|
Sonst (Subbaustein-Titel EXAKT wie oben):
|
||||||
|
{{"probleme": [{{"subbaustein": "<exakter Subbaustein-Titel>", "problem": "…"}}]}}
|
||||||
|
{extra}
|
||||||
30
templates/Prompt/Fakten-Recherche.md
Normal file
30
templates/Prompt/Fakten-Recherche.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
Extrahiere die Lern-Fakten für jeden Subbaustein des Themas "{topic}". Diese Fakten sind die verbindliche Grundlage, aus der später der Guide-Text, die Stufen und die Prüfungsfragen entstehen — sie müssen **stimmen**.
|
||||||
|
|
||||||
|
{source}
|
||||||
|
|
||||||
|
BAUSTEINE MIT SUBBAUSTEINEN (bearbeite JEDEN Subbaustein):
|
||||||
|
{bausteine}
|
||||||
|
|
||||||
|
Sammle pro Subbaustein NUR das Wesentliche (überflüssiges Material schadet dem Lernen):
|
||||||
|
- **kernpunkte**: 1–3 knappe Aussagen — was muss man an diesem Subbaustein verstehen?
|
||||||
|
- **voraussetzungen**: was muss man vorher kennen (ein Halbsatz)? Leer, wenn nichts.
|
||||||
|
- **huerden**: typischer Anfänger-Irrtum (ein Halbsatz). Leer, wenn keiner.
|
||||||
|
- **belegte_fakten**: harte Fakten (Definitionen, Formeln, Werte, Namen, Signaturen) — **nur was du belegen kannst**. Jeder mit Quelle:
|
||||||
|
- Mit Quelldatei/Skript: zitiere **sinngemäß-genau** und gib die Stelle an (z. B. „Skript Def. 6.3, Z.66"). Erfinde keine Werte, rechne nichts selbst aus.
|
||||||
|
- Ohne Quelle (reines Thema): nur gesichertes Standardwissen; Unsicheres per Websuche prüfen; Quelle = „allgemein" oder die URL.
|
||||||
|
- **beispiel_idee**: EIN Beispiel, das das Verständnis trägt — frei formuliert. **Hier** gehören selbst gebildete Sätze, Mini-Szenarien, durchgerechnete Beispiele hin. Leer, wenn ein Beispiel nichts beiträgt.
|
||||||
|
|
||||||
|
HARTE TRENNUNG — wichtig:
|
||||||
|
- `belegte_fakten` = nur Belegbares aus Quelle/gesichertem Wissen. **NIEMALS** ein selbst gerechnetes oder erfundenes Beispiel als belegten Fakt ausgeben.
|
||||||
|
- Ein durchgerechnetes Beispiel, ein erdachter Satz, ein konstruierter Fall → gehört in `beispiel_idee`, nicht in `belegte_fakten`.
|
||||||
|
- Im Zweifel: lieber weglassen als falsch behaupten.
|
||||||
|
|
||||||
|
Schreibe ALLE Subbausteine als EIN JSON in die Datei {out_path} (nutze dein Schreib-Werkzeug), GENAU in diesem Format:
|
||||||
|
{{"fakten": [
|
||||||
|
{{"baustein": "<exakter Baustein-Titel>", "subbaustein": "<exakter Subbaustein-Titel>",
|
||||||
|
"kernpunkte": ["…"], "voraussetzungen": "…", "huerden": "…",
|
||||||
|
"belegte_fakten": [{{"text": "…", "quelle": "…"}}], "beispiel_idee": "…"}}
|
||||||
|
]}}
|
||||||
|
|
||||||
|
Gib sonst keinen Text aus.
|
||||||
|
{extra}
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
Du bist Qualitäts-Prüfer für den Fragen-Vorrat einer Lern-Prüfung zum Thema "{topic}". Ein anderer Agent hat Frage-Muster für **mehrere Bausteine** erstellt. Prüfe sie und gib eine **bereinigte** Endfassung zurück.
|
Du bist Qualitäts-Prüfer für den Fragen-Vorrat einer Lern-Prüfung zum Thema "{topic}". Ein anderer Agent hat Frage-Muster für **mehrere Bausteine** erstellt — genau ein Muster je Subbaustein. Prüfe sie und gib eine **bereinigte** Endfassung zurück.
|
||||||
|
|
||||||
ZU PRÜFENDE MUSTER (gruppiert je Baustein; Zeilen: [typ] (subbaustein) Frage):
|
ZU PRÜFENDE MUSTER (gruppiert je Baustein; Zeilen: (subbaustein) Frage):
|
||||||
{tabelle}
|
{tabelle}
|
||||||
|
|
||||||
PRÜFE UND BEREINIGE GEGEN DIESE KRITERIEN (je Baustein getrennt):
|
PRÜFE UND BEREINIGE GEGEN DIESE KRITERIEN (je Baustein getrennt):
|
||||||
- **Stil:** jede Frage GENAU EINE Frage, ein Fragezeichen, eine Sache. Kein "und"/"sowie", keine Aufzählung. Maximal 1–2 Sätze. Verstöße → umformulieren.
|
- **Stil:** jede Frage GENAU EINE Frage, ein Fragezeichen, eine Sache. Kein "und"/"sowie", keine Aufzählung. Maximal 1–2 Sätze. Verstöße → umformulieren.
|
||||||
- **Eindeutig beantwortbar:** aus dem Baustein-Wissen klar beantwortbar, keine erfundenen Annahmen. Unbeantwortbares → korrigieren oder entfernen.
|
- **Eindeutig beantwortbar:** aus dem Baustein-Wissen klar beantwortbar, keine erfundenen Annahmen. Unbeantwortbares → korrigieren.
|
||||||
- **Distinkt:** keine zwei Muster desselben Bausteins, die sinngemäß dieselbe Frage stellen (auch nicht über Typen hinweg). Dubletten → eines behalten, das andere entfernen.
|
- **Kern-Treffer:** die Frage trifft das Kernkonzept des Subbausteins, neutral formuliert (nicht extra leicht/schwer — die Schwierigkeit setzt später das Lerner-Niveau). Verfehlt sie den Kern → umformulieren.
|
||||||
- **Typ-Treue:** die Frage passt zum angegebenen Typ; sonst Typ anpassen oder Muster entfernen.
|
- **Genau eins je Subbaustein:** pro Subbaustein bleibt genau ein Muster. Doppelte → eines behalten.
|
||||||
- `baustein`, `subbaustein` und `typ` der Einträge unverändert übernehmen (nur `frage` darf sich ändern); nur entfernen, nicht neu erfinden.
|
- `baustein` und `subbaustein` der Einträge unverändert übernehmen (nur `frage` darf sich ändern); nicht neu erfinden.
|
||||||
|
|
||||||
Behalte gute Muster unverändert. Ändere nur, was wirklich gegen die Kriterien verstößt.
|
Behalte gute Muster unverändert. Ändere nur, was wirklich gegen die Kriterien verstößt.
|
||||||
|
|
||||||
Schreibe die bereinigte Endfassung ALLER Bausteine als EIN JSON in die Datei {out_path} (nutze dein Schreib-Werkzeug), GENAU in diesem Format:
|
Schreibe die bereinigte Endfassung ALLER Bausteine als EIN JSON in die Datei {out_path} (nutze dein Schreib-Werkzeug), GENAU in diesem Format:
|
||||||
{{"muster": [
|
{{"muster": [
|
||||||
{{"baustein": "<exakter Baustein-Titel>", "subbaustein": "<exakter Titel>", "typ": "<Schlüssel>", "frage": "<eine konkrete Frage>"}}
|
{{"baustein": "<exakter Baustein-Titel>", "subbaustein": "<exakter Titel>", "frage": "<eine konkrete Frage>"}}
|
||||||
]}}
|
]}}
|
||||||
|
|
||||||
Gib sonst keinen Text aus.
|
Gib sonst keinen Text aus.
|
||||||
|
|||||||
@@ -1,29 +1,26 @@
|
|||||||
Du baust den Fragen-Vorrat für eine Lern-Prüfung zum Thema "{topic}". Erzeuge **Frage-Muster** — je eine konkrete Beispielfrage pro Subbaustein und Fragetyp. Aus diesen Mustern werden später bei der Prüfung leicht abgewandelte Fragen formuliert.
|
Du baust den Fragen-Vorrat für eine Lern-Prüfung zum Thema "{topic}". Erzeuge **Frage-Muster** — **genau eine** konkrete Beispielfrage **pro Subbaustein**. Aus jedem Muster wird später bei der Prüfung eine leicht abgewandelte Frage formuliert — passend zum Niveau des Lerners (Anfänger bis Experte). Die Schwierigkeit steckt also NICHT im Muster, sondern kommt erst bei der Prüfung. Frag deshalb nach dem **Kernkonzept** des Subbausteins, neutral formuliert.
|
||||||
|
|
||||||
Du bearbeitest **mehrere Bausteine**. Jeder Baustein hat seine eigenen relevanten Subbausteine:
|
Du bearbeitest **mehrere Bausteine**. Jeder Baustein hat seine eigenen Subbausteine:
|
||||||
{bausteine}
|
{bausteine}
|
||||||
|
|
||||||
FRAGETYPEN (Schlüssel : Bedeutung):
|
|
||||||
{typen}
|
|
||||||
|
|
||||||
AUFGABE (für **jeden** der Bausteine oben):
|
AUFGABE (für **jeden** der Bausteine oben):
|
||||||
- Erzeuge für **jeden** seiner Subbausteine zu **jedem** passenden Fragetyp **ein** Muster (volles Kreuzprodukt).
|
- Erzeuge für **jeden** seiner Subbausteine **genau ein** Muster — eine konkrete, knappe Frage zum Kernkonzept dieses Subbausteins.
|
||||||
- Lasse eine Kombination **weg**, wenn der Typ klar nicht passt (z.B. Code-Vorhersage bei reiner Theorie ohne Code). Nichts erzwingen.
|
- Jedes Muster ist EINE konkrete Frage (1–2 Sätze, genau ein Fragezeichen, eine Sache) — kein Mehrteiler, keine Aufzählung.
|
||||||
- Jedes Muster ist EINE konkrete, knappe Frage (1–2 Sätze, genau ein Fragezeichen, eine Sache) — kein Mehrteiler, keine Aufzählung.
|
|
||||||
- Die Frage muss aus dem Baustein-Wissen beantwortbar sein. Erfinde keine Zusatzannahmen.
|
- Die Frage muss aus dem Baustein-Wissen beantwortbar sein. Erfinde keine Zusatzannahmen.
|
||||||
- WICHTIG: Du siehst nur Titel, NICHT den späteren Guide-Text. Frag deshalb nur nach dem KERNKONZEPT des Subbausteins. Setze NICHTS voraus, was evtl. nicht im Guide steht: keine „gezeigten" Code-Ausschnitte/Beispiele/Diagramme, keine konkreten Werte/Listen/Zahlen, keine Randthemen. Frag das Konzept selbst, nicht ein bestimmtes Artefakt.
|
- WICHTIG: Du siehst nur Titel, NICHT den späteren Guide-Text. Frag deshalb nur nach dem KERNKONZEPT des Subbausteins. Setze NICHTS voraus, was evtl. nicht im Guide steht: keine „gezeigten" Code-Ausschnitte/Beispiele/Diagramme, keine konkreten Werte/Listen/Zahlen.
|
||||||
- Innerhalb eines Subbausteins sollen die Muster verschiedener Typen **wirklich verschiedene** Aspekte treffen — keine Umformulierungen derselben Frage.
|
- Die Frage trifft den **Kern** des Subbausteins — den zentralen Punkt, den man verstanden haben muss. Nicht zu eng auf ein Detail, nicht zu breit über den ganzen Baustein.
|
||||||
- Der `typ` ist exakt einer der Schlüssel oben. `baustein` ist exakt einer der Baustein-Titel oben. `subbaustein` ist exakt einer der Subbaustein-Titel des jeweiligen Bausteins.
|
- `baustein` ist exakt einer der Baustein-Titel oben. `subbaustein` ist exakt einer der Subbaustein-Titel des jeweiligen Bausteins.
|
||||||
|
|
||||||
HARTE STIL-REGELN JE FRAGE:
|
HARTE STIL-REGELN JE FRAGE:
|
||||||
- Genau EINE Frage, ein Fragezeichen, eine Sache. Kein "und"/"sowie", kein "nenne drei …".
|
- Genau EINE Frage, ein Fragezeichen, eine Sache. Kein "und"/"sowie", kein "nenne drei …".
|
||||||
- Maximal 1–2 Sätze, keine Vorrede, kein Szenario-Aufbau.
|
- Maximal 1–2 Sätze, keine Vorrede, kein Szenario-Aufbau.
|
||||||
|
- Neutral formuliert — nicht extra leicht, nicht extra schwer. Die Schwierigkeit setzt die Prüfung über das Lerner-Niveau.
|
||||||
- Direkte Ansprache, klares Deutsch.
|
- Direkte Ansprache, klares Deutsch.
|
||||||
{extra}
|
{extra}
|
||||||
|
|
||||||
Schreibe ALLE Muster aller Bausteine als EIN JSON in die Datei {out_path} (nutze dein Schreib-Werkzeug), GENAU in diesem Format:
|
Schreibe ALLE Muster aller Bausteine als EIN JSON in die Datei {out_path} (nutze dein Schreib-Werkzeug), GENAU in diesem Format:
|
||||||
{{"muster": [
|
{{"muster": [
|
||||||
{{"baustein": "<exakter Baustein-Titel>", "subbaustein": "<exakter Subbaustein-Titel>", "typ": "<Schlüssel>", "frage": "<eine konkrete Frage>"}}
|
{{"baustein": "<exakter Baustein-Titel>", "subbaustein": "<exakter Subbaustein-Titel>", "frage": "<eine konkrete Frage>"}}
|
||||||
]}}
|
]}}
|
||||||
|
|
||||||
Gib sonst keinen Text aus.
|
Gib sonst keinen Text aus.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
Plane die Gliederung eines Lern-Guides zum Thema "{topic}" (Format: {format_name}).
|
Plane die Gliederung eines Lern-Guides zum Thema "{topic}".
|
||||||
|
|
||||||
GEWÄHLTE BAUSTEINE (jeder mit seiner NUMMER — die Auswahl steht fest, du ordnest nur):
|
BAUSTEINE (jeder mit seiner NUMMER — die Liste steht fest, du ordnest nur):
|
||||||
{bausteine}
|
{bausteine}
|
||||||
|
|
||||||
REIHENFOLGE — vom Bekannten zum Unbekannten:
|
REIHENFOLGE — vom Bekannten zum Unbekannten:
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
Identifiziere für jeden zugeteilten Baustein eines Lern-Guides zum Thema "{topic}", WAS ein Anfänger ohne Vorwissen verstehen muss. Du schreibst noch keinen Guide-Text — du sammelst nur die Inhalte, die später gelehrt werden.
|
Identifiziere für jeden zugeteilten Baustein eines Lern-Guides zum Thema "{topic}", WAS ein Anfänger ohne Vorwissen verstehen muss. Du schreibst noch keinen Guide-Text — du sammelst nur die Inhalte, die später gelehrt werden.
|
||||||
|
|
||||||
Dir zugeteilt sind folgende Kapitel und Bausteine — verbindlich: jeder zugeteilte Baustein muss vorkommen, keine zusätzlichen erfinden. Unter jedem Baustein stehen seine SUBBAUSTEINE mit Stufe (`[einfach]`/`[mittel]`/`[schwer]`) — die Teilpunkte, die gelehrt werden:
|
Dir zugeteilt sind folgende Kapitel und Bausteine — verbindlich: jeder zugeteilte Baustein muss vorkommen, keine zusätzlichen erfinden. Unter jedem Baustein stehen seine SUBBAUSTEINE mit Lernpfad-Stufe (`[anfaenger]`/`[fortgeschritten]`/`[experte]`) — die Teilpunkte, die gelehrt werden:
|
||||||
{zuteilung}
|
{zuteilung}
|
||||||
|
|
||||||
{facts}
|
{facts}
|
||||||
|
|
||||||
|
Leitprinzip: nur das **Wesentliche**. Überflüssiges Material schadet dem Lernen — sammle, was ein Anfänger WIRKLICH braucht, nicht alles, was man sagen könnte.
|
||||||
|
|
||||||
Sammle pro Baustein:
|
Sammle pro Baustein:
|
||||||
- Kernpunkte / Lernziele: was muss der Leser begreifen? Decke JEDEN gelisteten Subbaustein mit mindestens einem Kernpunkt ab. Hat ein Baustein keine Subbausteine, sammle 3–7 knappe Punkte.
|
- Kernpunkte / Lernziele: was muss der Leser begreifen? Decke JEDEN gelisteten Subbaustein mit GENAU dem einen, zentralen Kernpunkt ab — nicht mehrere überlappende. Hat ein Baustein keine Subbausteine, sammle 3–7 knappe Punkte.
|
||||||
- Voraussetzungen: welche Begriffe/Ideen muss man vorher kennen, um das hier zu verstehen? Jede in einem Halbsatz erklärbar — das sind die Ankerpunkte für Neulinge.
|
- Voraussetzungen: welche Begriffe/Ideen muss man vorher kennen, um das hier zu verstehen? Jede in einem Halbsatz erklärbar — das sind die Ankerpunkte für Neulinge.
|
||||||
- Typische Hürden: wo verstehen Anfänger es erfahrungsgemäß falsch oder stolpern?
|
- Typische Hürden: wo verstehen Anfänger es erfahrungsgemäß falsch oder stolpern?
|
||||||
- Belegte Fakten (Versionen, Namen, Werte) — nichts Unbelegtes; Unsicheres per Websuche prüfen.
|
- Belegte Fakten (Versionen, Namen, Werte) — nichts Unbelegtes; Unsicheres per Websuche prüfen.
|
||||||
- Eine konkrete Beispiel-Idee: was soll das Beispiel zeigen?
|
- Eine konkrete Beispiel-Idee NUR, wo ein Beispiel das Verständnis wirklich trägt — nicht pflichtmäßig bei jedem Subbaustein.
|
||||||
- Scope: nur was DIESER Baustein hergibt. Nicht mehr, nicht weniger. Am Rand Erwähntes nicht zum Thema aufblasen.
|
- Scope: nur was DIESER Baustein hergibt. Nicht mehr, nicht weniger. Am Rand Erwähntes nicht zum Thema aufblasen.
|
||||||
|
|
||||||
Schreibe NUR die Datei {out_path} — pro Baustein ein section-Marker (Titel EXAKT aus der Zuteilung), darunter die Punkte:
|
Schreibe NUR die Datei {out_path} — pro Baustein ein section-Marker (Titel EXAKT aus der Zuteilung), darunter die Punkte:
|
||||||
|
|||||||
@@ -14,8 +14,13 @@ Prüfe jede Section:
|
|||||||
- Eine Aufzählung (Schritte/Optionen/Anforderungen) als langer Fließtext-Satz, die eine Markdown-Liste sein sollte.
|
- Eine Aufzählung (Schritte/Optionen/Anforderungen) als langer Fließtext-Satz, die eine Markdown-Liste sein sollte.
|
||||||
- Textwand: ein dichter Block ohne Absätze, der sich in mehrere teilen ließe.
|
- Textwand: ein dichter Block ohne Absätze, der sich in mehrere teilen ließe.
|
||||||
- Mehr als ~4 neue Fachbegriffe ohne Erklärung beim ersten Auftreten.
|
- Mehr als ~4 neue Fachbegriffe ohne Erklärung beim ersten Auftreten.
|
||||||
3. Sind die Beispiele kurz, simpel, plausibel korrekt — und im themengerechten Format laut Spezifikation (kein Codeblock um Prosa-Beispiele, kein Prosa-Pseudo-Beispiel, wo Code gefragt ist)?
|
3. Knappheit (überflüssiges Material schadet dem Lernen — echte Mängel notieren):
|
||||||
4. Ist das Markdown sauber (keine abgebrochenen Code-Blöcke, keine Platzhalter, kein Fremdtext)?
|
- Wiederholung: dieselbe Aussage mehrfach, nur anders formuliert.
|
||||||
|
- Breit ausgewalztes Triviales, Füllsätze, Vorreden, Meta-Kommentare ohne neuen Inhalt.
|
||||||
|
- Ein Beispiel, das nichts zum Verständnis beiträgt.
|
||||||
|
→ notieren als „zu lang/redundant — kürzen ohne Info-Verlust". WICHTIG: Kürzen heißt nie, einen Subbaustein zu streichen — jeder bleibt erhalten.
|
||||||
|
4. Sind die Beispiele kurz, simpel, plausibel korrekt — und im themengerechten Format laut Spezifikation (kein Codeblock um Prosa-Beispiele, kein Prosa-Pseudo-Beispiel, wo Code gefragt ist)?
|
||||||
|
5. Ist das Markdown sauber (keine abgebrochenen Code-Blöcke, keine Platzhalter, kein Fremdtext)?
|
||||||
|
|
||||||
Du PRÜFST nur und notierst Probleme — du änderst nichts. Nur echte Mängel notieren, keine Geschmacksfragen.
|
Du PRÜFST nur und notierst Probleme — du änderst nichts. Nur echte Mängel notieren, keine Geschmacksfragen.
|
||||||
|
|
||||||
|
|||||||
@@ -5,23 +5,32 @@
|
|||||||
SECTION-SPEZIFIKATION:
|
SECTION-SPEZIFIKATION:
|
||||||
{spec}
|
{spec}
|
||||||
|
|
||||||
ZU ÜBERARBEITEN — je Section der aktuelle Inhalt und das notierte Problem:
|
ZU ÜBERARBEITEN — je Section ihre Subbausteine, das notierte Problem und der aktuelle Inhalt:
|
||||||
{auftraege}
|
{auftraege}
|
||||||
|
|
||||||
Behebe pro Section NUR das notierte Problem; was in Ordnung ist, bleibt inhaltlich erhalten.
|
Behebe pro Section NUR das notierte Problem; was in Ordnung ist, bleibt inhaltlich erhalten.
|
||||||
|
|
||||||
Jede Section hat ZWEI Fassungen mit denselben Inhalten: **kompakt** (je Subbaustein ein Merksatz als Stichpunkt) und **ausführlich** (zusammenhängender Anfänger-Text). Gib beide neu aus und halte sie deckungsgleich.
|
VERBINDLICHE STRUKTUR (wie der Writer) — sonst bricht die gestufte Anzeige:
|
||||||
|
- Jede Section hat ZWEI Fassungen: **kompakt** (je Subbaustein ein Merksatz) und **ausführlich** (zusammenhängender Anfänger-Text).
|
||||||
|
- In BEIDEN Fassungen steht je Subbaustein ein `<!-- sub: LABEL | Subbaustein-Titel -->`-Marker. LABEL und Titel EXAKT aus der Subbaustein-Liste oben, gleiche Reihenfolge in beiden Blöcken. Der ausführliche Text beginnt mit einem kurzen Anker (Einordnung) VOR dem ersten Marker.
|
||||||
|
- Die Marker sind unsichtbare Schnittstellen, KEINE sichtbaren Überschriften — schreibe trotzdem flüssig.
|
||||||
|
|
||||||
Schreibe NUR die Datei {out_path} in GENAU diesem Format — für JEDE beanstandete Section ein section-Marker (Titel EXAKT wie oben) mit `kompakt`- und `ausführlich`-Block:
|
KNAPP HALTEN (falls das Problem „zu lang/redundant" ist):
|
||||||
|
- Jeder Satz trägt neue Information. Streiche Wiederholungen, Füll- und Meta-Sätze, Vorreden.
|
||||||
|
- Erkläre Triviales kurz; gib Tiefe nur, wo der Stoff sie braucht. Im Zweifel weglassen, nicht hinzufügen.
|
||||||
|
- Ein Beispiel nur, wo es das Verständnis wirklich trägt — nicht pflichtmäßig bei jedem Subbaustein.
|
||||||
|
- Kürzen heißt nie: einen Subbaustein streichen. Jeder bleibt mit seinem Marker erhalten.
|
||||||
|
|
||||||
|
Schreibe NUR die Datei {out_path} in GENAU diesem Format — für JEDE beanstandete Section ein section-Marker (Titel EXAKT wie oben):
|
||||||
|
|
||||||
<!-- section: Exakter Section-Titel -->
|
<!-- section: Exakter Section-Titel -->
|
||||||
<!-- kompakt -->
|
<!-- kompakt -->
|
||||||
|
<!-- sub: anfaenger | Exakter Subbaustein-Titel -->
|
||||||
- Merksatz je Subbaustein (knapp, ohne Erklärung)
|
- Merksatz je Subbaustein (knapp, ohne Erklärung)
|
||||||
<!-- ausführlich -->
|
<!-- ausführlich -->
|
||||||
Erklärung (Einordnung → Wie/Warum) laut SECTION-SPEZIFIKATION…
|
Anker: kurze Einordnung des Bausteins — vor dem ersten Subbaustein.
|
||||||
|
<!-- sub: anfaenger | Exakter Subbaustein-Titel -->
|
||||||
### Beispiel
|
Anfängerfreundlicher Fließtext zu diesem Subbaustein.
|
||||||
(Beispiel im themengerechten Format laut SECTION-SPEZIFIKATION: Codeblock NUR bei Code-Themen, sonst Beispielsätze oder Mini-Szenario)
|
|
||||||
|
|
||||||
Die Marker-Zeilen exakt so schreiben. Kein Text außerhalb der Sections.
|
Die Marker-Zeilen exakt so schreiben. Kein Text außerhalb der Sections.
|
||||||
{extra}
|
{extra}
|
||||||
|
|||||||
@@ -1,23 +1,25 @@
|
|||||||
Schreibe Sections für einen Lern-Guide zum Thema "{topic}" (Format: {format_name}).
|
Schreibe Sections für einen Lern-Guide zum Thema "{topic}" (Format: {format_name}).
|
||||||
|
|
||||||
Dir zugeteilt sind folgende Kapitel und Bausteine — verbindlich: jede zugeteilte Section muss vorkommen, keine zusätzlichen erfinden. Unter jedem Baustein stehen seine SUBBAUSTEINE mit Stufe (`[einfach]`/`[mittel]`/`[schwer]`):
|
Dir zugeteilt sind folgende Kapitel und Bausteine — verbindlich: jede zugeteilte Section muss vorkommen, keine zusätzlichen erfinden. Unter jedem Baustein stehen seine SUBBAUSTEINE mit Ebenen-Label (`[anfaenger]`/`[fortgeschritten]`/`[experte]`/`[rand]`):
|
||||||
{zuteilung}
|
{zuteilung}
|
||||||
|
|
||||||
Das Format ({format_name}) beeinflusst NICHT, wie ein Baustein geschrieben wird — nur, wie viele Bausteine das Thema abdecken. Derselbe Baustein bekommt in MiniGuide, Guide und FullGuide denselben Text.
|
WICHTIG — jeder Subbaustein wird mit einem Marker abgegrenzt, damit der Guide später gestuft angezeigt werden kann (Anfänger sehen nur `anfaenger`, Fortgeschrittene mehr). Das Label kommt EXAKT aus der Zuteilung. Trotz Marker schreibst du FLÜSSIG und verwoben — der Marker ist eine unsichtbare Schnittstelle, KEINE sichtbare Überschrift.
|
||||||
|
|
||||||
Jeder Baustein bekommt ZWEI Fassungen mit DENSELBEN Inhalten (dieselben Subbausteine):
|
Der Inhalt ist UNABHÄNGIG von der Stufe: jeder Subbaustein wird **anfängerfreundlich** erklärt, egal welches Label. Das Label sagt nur, WANN im Lernpfad der Punkt drankommt (Fundament → Feinheit) — NICHT wie kompliziert er geschrieben wird. Ein `[experte]`-Punkt wird genauso simpel erklärt wie ein `[anfaenger]`-Punkt. Kein Perfektionismus, keine künstliche Tiefe: das Wesentliche klar, so dass ein Neuling es versteht.
|
||||||
- **kompakt**: je Subbaustein EIN Merksatz als Stichpunkt (`- …`). Zum Erinnern. Nur benennen, nicht erklären. Gleiche Reihenfolge wie ausführlich.
|
|
||||||
|
Jeder Baustein bekommt ZWEI Fassungen mit DENSELBEN Subbausteinen (gleiche Titel, gleiche Reihenfolge):
|
||||||
|
- **kompakt**: je Subbaustein EIN Merksatz als Stichpunkt (`- …`). Zum Erinnern. Nur benennen, nicht erklären.
|
||||||
- **ausführlich**: der zusammenhängende Anfänger-Lerntext (siehe unten).
|
- **ausführlich**: der zusammenhängende Anfänger-Lerntext (siehe unten).
|
||||||
Beide MÜSSEN dieselben Subbausteine abdecken. Nichts, das nur in einer Fassung steht.
|
|
||||||
|
|
||||||
SO SCHREIBST DU die ausführliche Fassung — EIN zusammenhängender Text für einen Junior, der das Thema NEU lernt:
|
SO SCHREIBST DU die ausführliche Fassung — EIN zusammenhängender Text für einen Junior, der das Thema NEU lernt:
|
||||||
- Beginne mit dem Anker (Einordnung): welches Problem/wozu, angeknüpft an etwas Bekanntes — erst dann das Neue. Pflicht, nie weglassen.
|
- Beginne mit dem Anker (Einordnung): welches Problem/wozu, angeknüpft an etwas Bekanntes — erst dann das Neue. Der Anker steht VOR dem ersten Subbaustein-Marker. Pflicht, nie weglassen.
|
||||||
- Löse JEDEN Fachbegriff bei Erstnennung in einem Halbsatz auf. Setze nichts voraus — auch keine Begriffe aus dem Titel oder anderen Bausteinen. Nutze die „Voraussetzung"-Hinweise aus den Inhalten als Anker, die „Hürde"-Hinweise, um Missverständnisse vorweg auszuräumen.
|
- Löse JEDEN Fachbegriff bei Erstnennung in einem Halbsatz auf. Setze nichts voraus — auch keine Begriffe aus dem Titel oder anderen Bausteinen. Nutze die „Voraussetzung"-Hinweise als Anker, die „Hürde"-Hinweise, um Missverständnisse vorweg auszuräumen.
|
||||||
- Die Subbausteine sind deine INHALTS-CHECKLISTE, keine Überschriften: webe sie in den Fließtext ein. Behandle jeden, aber als Teil eines Ganzen — nicht als isolierten Mini-Absatz.
|
- Behandle jeden Subbaustein unter seinem Marker — flüssig formuliert, erklärender Fließtext, kein isolierter Stichpunkt-Block.
|
||||||
- Reihenfolge: erst die `[einfach]`-Punkte (das Fundament), dann `[mittel]` (Details/Varianten), zuletzt `[schwer]` (Sonderfälle/Tücken — knapp, als fortgeschrittener Hinweis).
|
- Reihenfolge der Subbausteine wie in der Zuteilung: erst `[anfaenger]` (Fundament), dann `[fortgeschritten]`, dann `[experte]`, zuletzt `[rand]`.
|
||||||
- Diese Fassung trägt die volle Tiefe selbst — es gibt keine zweite Ausbaustufe. „Wie"-Abläufe Schritt für Schritt zeigen, nicht nur das Ergebnis.
|
- „Wie"-Abläufe Schritt für Schritt zeigen, nicht nur das Ergebnis.
|
||||||
- KEIN festes Längen-Limit. So lang wie nötig, damit ein Neuling es OHNE Vorwissen versteht. Im Zweifel ein Satz mehr und klar.
|
- KNAPP: Jeder Satz trägt neue Information. Keine Wiederholung, keine Füll- oder Meta-Sätze, keine Vorrede. Überflüssiges schadet dem Lernen — im Zweifel weglassen, nicht hinzufügen.
|
||||||
- Beispiele großzügig — auch kleine Mini-Beispiele mitten im Text, wo sie einen Punkt sofort greifbar machen, zusätzlich zum `### Beispiel` am Ende (Format laut Spezifikation).
|
- Länge folgt dem Inhalt: ein triviales Detail ein, zwei Sätze; ein komplexes Konzept so viel, wie es WIRKLICH braucht — nicht mehr. Kein „so lang wie möglich".
|
||||||
|
- Ein Beispiel NUR, wo es das Verständnis wirklich trägt — nicht pflichtmäßig bei jedem Subbaustein.
|
||||||
|
|
||||||
GEPRÜFTE INHALTE je Baustein — das ist verbindlich, was gelehrt werden muss:
|
GEPRÜFTE INHALTE je Baustein — das ist verbindlich, was gelehrt werden muss:
|
||||||
{inhalte}
|
{inhalte}
|
||||||
@@ -27,17 +29,21 @@ Recherchiere NICHT und suche NICHT im Web. Alle nötigen Fakten stehen in diesen
|
|||||||
SECTION-SPEZIFIKATION (gilt je Baustein):
|
SECTION-SPEZIFIKATION (gilt je Baustein):
|
||||||
{spec}
|
{spec}
|
||||||
|
|
||||||
Schreibe NUR die Datei {out_path} in GENAU diesem Format — pro Kapitel ein kapitel-Marker, pro Baustein ein section-Marker (Titel EXAKT aus der Zuteilung), darin ein `kompakt`- und ein `ausführlich`-Block:
|
Schreibe NUR die Datei {out_path} in GENAU diesem Format — pro Kapitel ein kapitel-Marker, pro Baustein ein section-Marker (Titel EXAKT aus der Zuteilung), darin ein `kompakt`- und ein `ausführlich`-Block. In BEIDEN Blöcken steht je Subbaustein ein `<!-- sub: LABEL | Subbaustein-Titel -->`-Marker (LABEL und Titel EXAKT aus der Zuteilung, gleiche Reihenfolge in beiden Blöcken):
|
||||||
|
|
||||||
<!-- kapitel: Kapiteltitel -->
|
<!-- kapitel: Kapiteltitel -->
|
||||||
<!-- section: Exakter Baustein-Titel -->
|
<!-- section: Exakter Baustein-Titel -->
|
||||||
<!-- kompakt -->
|
<!-- kompakt -->
|
||||||
- Merksatz je Subbaustein (knapp, ohne Erklärung)
|
<!-- sub: anfaenger | Exakter Subbaustein-Titel -->
|
||||||
|
- Merksatz zu diesem Subbaustein (knapp, ohne Erklärung)
|
||||||
|
<!-- sub: fortgeschritten | Exakter Subbaustein-Titel -->
|
||||||
|
- Merksatz zu diesem Subbaustein
|
||||||
<!-- ausführlich -->
|
<!-- ausführlich -->
|
||||||
Erklärung (Einordnung → Wie/Warum) laut SECTION-SPEZIFIKATION…
|
Anker: Einordnung des ganzen Bausteins (welches Problem, wozu) — vor dem ersten Subbaustein.
|
||||||
|
<!-- sub: anfaenger | Exakter Subbaustein-Titel -->
|
||||||
|
Anfängerfreundlicher Fließtext zu diesem Subbaustein, mit kleinem Beispiel.
|
||||||
|
<!-- sub: fortgeschritten | Exakter Subbaustein-Titel -->
|
||||||
|
Anfängerfreundlicher Fließtext zu diesem Subbaustein.
|
||||||
|
|
||||||
### Beispiel
|
Die Marker-Zeilen exakt so schreiben. Jede Section hat genau einen `<!-- kompakt -->`- und einen `<!-- ausführlich -->`-Block; die Subbaustein-Titel sind in beiden identisch. Kein Text außerhalb der Sections, kein Dokument-Titel, kein Inhaltsverzeichnis.
|
||||||
(Beispiel im themengerechten Format laut SECTION-SPEZIFIKATION: Codeblock NUR bei Code-Themen, sonst Beispielsätze oder Mini-Szenario)
|
|
||||||
|
|
||||||
Die Marker-Zeilen exakt so schreiben (jede Section hat genau einen `<!-- kompakt -->`- und einen `<!-- ausführlich -->`-Block). Kein Text außerhalb der Sections, kein Dokument-Titel, kein Inhaltsverzeichnis.
|
|
||||||
{extra}
|
{extra}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ STRITTIGE SUBBAUSTEINE (mit den abgegebenen Stimmen):
|
|||||||
{strittig}
|
{strittig}
|
||||||
|
|
||||||
Relevanz:
|
Relevanz:
|
||||||
- **relevant**: Kern-/Standardwissen, ein ernsthafter Lerner braucht es.
|
- **relevant**: Kern-/Standardwissen, das das Thema trägt — zentrale Konzepte, Definitionen, Eigenschaften. Bei theoretischen Themen auch zentrale Sätze ohne praktischen Gebrauch.
|
||||||
- **rand**: Randnotiz, Spezial-/Nischenfall, „nice to know" — nur für den Komplett-Guide.
|
- **rand**: Randnotiz, Nischen-/Spezialfall, „nice to know", Deprecated/Exotik, rein historische Details, konkrete Zahlenbeispiele, bloße Querverweise. NICHT rand: ein zentrales Theorem/eine fundamentale Eigenschaft. Nur für den Komplett-Guide.
|
||||||
|
|
||||||
Regeln:
|
Regeln:
|
||||||
- Wäge die Stimmen ab, entscheide fachlich. Im Zweifel **relevant**.
|
- Wäge die Stimmen ab, entscheide fachlich. Im Zweifel **relevant**.
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ SUBBAUSTEINE (Nummer in Klammern der zugehörige Baustein):
|
|||||||
{subbausteine}
|
{subbausteine}
|
||||||
|
|
||||||
Relevanz:
|
Relevanz:
|
||||||
- **relevant**: Kern- oder Standardwissen des Themas. Ein ernsthafter Lerner braucht es im normalen Gebrauch. Was man wirklich kennen sollte.
|
- **relevant**: Kern-/Standardwissen, das das Thema TRÄGT — zentrale Konzepte, Definitionen, Eigenschaften, gängige Praxis. Bei praktischen Themen: was man im Gebrauch braucht. Bei theoretischen Themen: zentrale Sätze/Eigenschaften, AUCH ohne praktischen Gebrauch. Was man wirklich kennen sollte.
|
||||||
- **rand**: Randnotiz, Spezial-/Nischenfall, „nice to know", Deprecated/Exotik. Für die meisten Lerner verzichtbar — gehört nur in einen Komplett-Guide.
|
- **rand**: Randnotiz, Spezial-/Nischenfall, „nice to know", Deprecated/Exotik — PLUS rein historische Details (wer/wann), konkrete Zahlenbeispiele eines allgemeinen Prinzips, bloße Querverweise. NICHT rand: ein zentrales Theorem oder eine fundamentale Eigenschaft, nur weil sie „theoretisch" wirkt. Gehört nur in einen Komplett-Guide.
|
||||||
|
|
||||||
Regeln:
|
Regeln:
|
||||||
- Bewerte jeden Punkt einzeln, gemessen an SEINEM Baustein und am Thema.
|
- Bewerte jeden Punkt einzeln, gemessen an SEINEM Baustein und am Thema.
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
Mehrere Agenten haben die Subbausteine des Themas "{topic}" nach Schwierigkeit eingestuft. Bei einigen sind sie sich uneinig. Entscheide für jeden strittigen Subbaustein die finale Stufe.
|
Mehrere Agenten haben die Subbausteine des Themas "{topic}" nach Lernpfad-Position eingestuft. Bei einigen sind sie sich uneinig. Entscheide für jeden strittigen Subbaustein die finale Stufe.
|
||||||
|
|
||||||
STRITTIGE SUBBAUSTEINE (mit den abgegebenen Stimmen):
|
STRITTIGE SUBBAUSTEINE (mit den abgegebenen Stimmen):
|
||||||
{strittig}
|
{strittig}
|
||||||
|
|
||||||
Stufe = Lern-/Verständnisaufwand (nicht Seltenheit/Obskurität):
|
Stufe = Lernpfad-Position (WANN man den Punkt braucht), NICHT Schwierigkeit:
|
||||||
- **einfach**: Basis-Fakt/Definition, sofort verständlich. Auch Versions-Details, Pfade, Konstanten, einzelne Fakten.
|
- **anfaenger**: Fundament — was man zuerst/unbedingt braucht; Voraussetzung für den Rest. Auch wenn gedanklich anspruchsvoll.
|
||||||
- **mittel**: braucht Erklärung — mehrere Teile, Zusammenhang, typische Anwendung.
|
- **fortgeschritten**: baut darauf auf; gängige Varianten, typische Anwendung.
|
||||||
- **schwer**: komplexes Zusammenspiel mehrerer Konzepte, tieferes Verständnis nötig. NICHT „schwer" nur weil selten/neu/Edge-Case.
|
- **experte**: Feinheiten, Sonderfälle, Nischen, seltene Details — auch wenn inhaltlich einfach (z. B. ein obskures Flag, ein Versions-Detail).
|
||||||
|
|
||||||
Regeln:
|
Regeln:
|
||||||
- Wäge die Stimmen ab, entscheide fachlich nach Lernaufwand. Im Zweifel die niedrigere Stufe.
|
- Ein einfaches Nischen-Detail ist `experte`; ein fundamentales komplexes Konzept ist `anfaenger`.
|
||||||
|
- Wäge die Stimmen ab, entscheide fachlich nach Lernpfad-Position.
|
||||||
- Für JEDE strittige Nummer genau eine Stufe.
|
- Für JEDE strittige Nummer genau eine Stufe.
|
||||||
|
|
||||||
Schreibe NUR die JSON-Datei nach: {out_path}
|
Schreibe NUR die JSON-Datei nach: {out_path}
|
||||||
|
|
||||||
Format (kein weiterer Text):
|
Format (kein weiterer Text):
|
||||||
{{"stufen": {{"1": "einfach", "4": "schwer"}}}}
|
{{"stufen": {{"1": "anfaenger", "4": "experte"}}}}
|
||||||
{extra}
|
{extra}
|
||||||
|
|||||||
@@ -1,27 +1,25 @@
|
|||||||
Stufe jeden Subbaustein des Themas "{topic}" nach **Lern-/Verständnisaufwand** ein: einfach, mittel oder schwer. Die Stufe steuert, wann ein Lerner den Punkt sieht (Anfänger sehen nur „einfach", Fortgeschrittene alles).
|
Ordne jeden Subbaustein des Themas "{topic}" seiner **Lernpfad-Position** zu: anfaenger, fortgeschritten oder experte. Die Stufe steuert, wann ein Lerner den Punkt sieht (Anfänger sehen nur „anfaenger", Fortgeschrittene mehr).
|
||||||
|
|
||||||
SUBBAUSTEINE (nach Baustein gruppiert):
|
SUBBAUSTEINE (nach Baustein gruppiert):
|
||||||
{subbausteine}
|
{subbausteine}
|
||||||
|
|
||||||
Was die Stufe misst — wie viel Aufwand, den Punkt zu VERSTEHEN und ANZUWENDEN:
|
ENTSCHEIDEND — die Stufe misst, WANN man den Punkt im Lernpfad braucht, NICHT wie schwer er zu verstehen ist:
|
||||||
- **einfach**: Basis-Fakt oder Definition. Sofort verständlich. Der Einstieg in den Baustein.
|
- **anfaenger**: Fundament. Was man als Erstes/unbedingt braucht, um den Baustein überhaupt zu verstehen. Voraussetzung für den Rest. Auch wenn das Konzept gedanklich anspruchsvoll ist — ist es das Fundament, gehört es hierher.
|
||||||
- **mittel**: braucht etwas Erklärung. Mehrere Teile, ein Zusammenhang, eine typische Anwendung.
|
- **fortgeschritten**: baut auf dem Fundament auf. Gängige Varianten, typische Anwendung, der nächste Schritt.
|
||||||
- **schwer**: komplexes Zusammenspiel mehrerer Konzepte. Setzt tieferes Verständnis voraus, ist gedanklich anspruchsvoll.
|
- **experte**: Feinheiten, Sonderfälle, Edge-Cases, Nischen, seltene Details. Was man erst braucht, wenn man die Basis beherrscht — **auch wenn es inhaltlich einfach ist** (z. B. ein obskures Flag, ein Versions-Detail).
|
||||||
|
|
||||||
WICHTIG — was NICHT die Stufe bestimmt:
|
WICHTIG — Schwierigkeit ist NICHT das Kriterium:
|
||||||
- Seltenheit ist KEINE Schwierigkeit. Ein Nischen-Feature kann trotzdem einfach sein.
|
- Ein **einfaches Nischen-Detail** (selten gebraucht, z. B. „seit 6.4.12.0") ist `experte`, nicht `anfaenger`.
|
||||||
- Versions-Details (z.B. „seit 6.4.12.0"), Konstanten, Pfade, einzelne Fakten → meist **einfach**, nicht schwer.
|
- Ein **fundamentales, aber komplexes Konzept** (man braucht es früh) ist `anfaenger`, nicht `experte`.
|
||||||
- Edge-Cases/Gotchas, die nur eine simple Regel sind (z.B. „Name muss dem Ordner entsprechen") → **einfach/mittel**, nicht schwer.
|
- Frag: „Wann im Lernpfad braucht man das?" — nicht „Wie schwer ist es?".
|
||||||
- „schwer" NUR, wenn der Punkt selbst gedanklich schwer ist — nicht weil er obskur oder neu ist.
|
|
||||||
|
|
||||||
Regeln:
|
Regeln:
|
||||||
- Stufe RELATIV im jeweiligen Baustein: vergleiche die Punkte eines Bausteins miteinander.
|
- Stufe RELATIV im jeweiligen Baustein: was ist hier Fundament, was Aufbau, was Feinheit?
|
||||||
- Die meisten Punkte sind einfach/mittel. „schwer" nur für die echten Brocken.
|
- Nicht jeder Baustein braucht einen `anfaenger`-Punkt. Manche Themen setzen Vorwissen voraus und sind ganz `fortgeschritten`/`experte` — das ist gewollt. Stufe ehrlich nach Lernpfad-Position, ohne künstlichen Einstieg zu erfinden.
|
||||||
- Jeder Baustein hat mindestens einen „einfach"-Punkt (den Einstieg). Im Zweifel die niedrigere Stufe.
|
|
||||||
- Urteile nur — erfinde nichts dazu, ändere keine Subbausteine.
|
- Urteile nur — erfinde nichts dazu, ändere keine Subbausteine.
|
||||||
|
|
||||||
Schreibe NUR die JSON-Datei nach: {out_path}
|
Schreibe NUR die JSON-Datei nach: {out_path}
|
||||||
|
|
||||||
Format (für JEDE Nummer genau eine Stufe; kein weiterer Text):
|
Format (für JEDE Nummer genau eine Stufe; kein weiterer Text):
|
||||||
{{"stufen": {{"1": "einfach", "2": "mittel", "3": "schwer"}}}}
|
{{"stufen": {{"1": "anfaenger", "2": "fortgeschritten", "3": "experte"}}}}
|
||||||
{extra}
|
{extra}
|
||||||
|
|||||||
Reference in New Issue
Block a user