update
This commit is contained in:
@@ -21,7 +21,8 @@ from agents import kill_process
|
|||||||
from config import KONSENS_GRACE, KONSENS_MAX_RUNDEN, DEFAULT_PROVIDER
|
from config import KONSENS_GRACE, KONSENS_MAX_RUNDEN, DEFAULT_PROVIDER
|
||||||
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 paths import arbeit_dir, bausteine_path, project_dir, subbausteine_path, quelle_path, quelle_crawl_dir, safe_ordner
|
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 crawl import crawl
|
from crawl import crawl
|
||||||
from pipeline import (
|
from pipeline import (
|
||||||
CANCELLED, FAILED, GenContext, _extra, _log, _prompt, _race, _relevanz_schema, _rest_schema,
|
CANCELLED, FAILED, GenContext, _extra, _log, _prompt, _race, _relevanz_schema, _rest_schema,
|
||||||
@@ -51,6 +52,7 @@ BAUSTEINE_STEPS = (
|
|||||||
"Subbausteine finden", "Subbausteine wählen", "Subbausteine klären",
|
"Subbausteine finden", "Subbausteine wählen", "Subbausteine klären",
|
||||||
"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",
|
||||||
|
"Fragen finden", "Fragen wählen", "Fragen klären",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -93,6 +95,20 @@ def subbausteine_titel(topic: str, baustein: str) -> list[str]:
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def lade_frage_muster(topic: str, baustein: str) -> list[dict]:
|
||||||
|
"""Vordefinierte Frage-Muster eines Bausteins aus dem Sidecar (leer = Fallback auf Live)."""
|
||||||
|
fm = _json_datei(frage_muster_path(topic))
|
||||||
|
if not isinstance(fm, dict):
|
||||||
|
return []
|
||||||
|
return [
|
||||||
|
{"subbaustein": str(e.get("subbaustein", "")).strip(),
|
||||||
|
"typ": str(e.get("typ", "")).strip(),
|
||||||
|
"frage": frage}
|
||||||
|
for e in (fm.get(baustein) or [])
|
||||||
|
if isinstance(e, dict) and (frage := str(e.get("frage", "")).strip())
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def lade_uebersicht(topic: str) -> list[dict]:
|
def lade_uebersicht(topic: str) -> list[dict]:
|
||||||
"""Strukturierte Baustein-Liste für die Übersicht: Titel + Beschreibung + Subbausteine/Stufen.
|
"""Strukturierte Baustein-Liste für die Übersicht: Titel + Beschreibung + Subbausteine/Stufen.
|
||||||
|
|
||||||
@@ -132,6 +148,7 @@ def _bausteine_steps(topic: str) -> tuple:
|
|||||||
"Subbausteine finden", "Subbausteine wählen", "Subbausteine klären",
|
"Subbausteine finden", "Subbausteine wählen", "Subbausteine klären",
|
||||||
"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",
|
||||||
|
"Fragen finden", "Fragen wählen", "Fragen klären",
|
||||||
)
|
)
|
||||||
mitte = base + (("Ergänzung",) if q["type"] == "projekt" else ()) + rest
|
mitte = base + (("Ergänzung",) if q["type"] == "projekt" else ()) + rest
|
||||||
return (("Quelle laden",) if q["type"] == "link" else ()) + mitte
|
return (("Quelle laden",) if q["type"] == "link" else ()) + mitte
|
||||||
@@ -141,6 +158,40 @@ def _step_idx(topic: str, name: str) -> int:
|
|||||||
return _bausteine_steps(topic).index(name)
|
return _bausteine_steps(topic).index(name)
|
||||||
|
|
||||||
|
|
||||||
|
# Grobe Anzeige-Phasen: bündeln die Feinschritte (intern bleibt alles feingranular).
|
||||||
|
# Sonderschritte (Quelle laden, Ergänzung) gehören zur Phase „Inventar".
|
||||||
|
PHASEN = (
|
||||||
|
("Inventar", ("Quelle laden", "Recherche", "Konsolidierung", "Klärung", "Ergänzung")),
|
||||||
|
("Subbausteine", ("Subbausteine finden", "Subbausteine wählen", "Subbausteine klären")),
|
||||||
|
("Stufen", ("Stufen finden", "Stufen wählen", "Stufen klären")),
|
||||||
|
("Relevanz", ("Relevanz finden", "Relevanz wählen", "Relevanz klären")),
|
||||||
|
("Fragen", ("Fragen finden", "Fragen wählen", "Fragen klären")),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _phasen(topic: str) -> list[tuple[str, int]]:
|
||||||
|
"""[(grob_label, Anzahl vorhandener Feinschritte)] für die aktuelle Quelle."""
|
||||||
|
feine = _bausteine_steps(topic)
|
||||||
|
return [(label, n) for label, members in PHASEN if (n := sum(f in members for f in feine))]
|
||||||
|
|
||||||
|
|
||||||
|
def _phasen_status(topic: str, current: int | None) -> list[dict]:
|
||||||
|
"""Grobe Phasen-Zustände aus dem feinen Fortschritt `current` (None = alles pending,
|
||||||
|
len(feine) = alles done). → [{label, state}] mit state done/active/pending."""
|
||||||
|
out, start = [], 0
|
||||||
|
for label, n in _phasen(topic):
|
||||||
|
end = start + n
|
||||||
|
if current is None or current < start:
|
||||||
|
state = "pending"
|
||||||
|
elif current >= end:
|
||||||
|
state = "done"
|
||||||
|
else:
|
||||||
|
state = "active"
|
||||||
|
out.append({"label": label, "state": state})
|
||||||
|
start = end
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def _bausteine_files(topic: str) -> dict:
|
def _bausteine_files(topic: str) -> dict:
|
||||||
arbeit = arbeit_dir(topic)
|
arbeit = arbeit_dir(topic)
|
||||||
runden = range(1, KONSENS_MAX_RUNDEN + 1)
|
runden = range(1, KONSENS_MAX_RUNDEN + 1)
|
||||||
@@ -154,18 +205,19 @@ def _bausteine_files(topic: str) -> dict:
|
|||||||
"ergaenzung": arbeit / "ergaenzung.json",
|
"ergaenzung": arbeit / "ergaenzung.json",
|
||||||
"sub_roh": arbeit / "subbausteine-roh.json",
|
"sub_roh": arbeit / "subbausteine-roh.json",
|
||||||
"sidecar": subbausteine_path(topic),
|
"sidecar": subbausteine_path(topic),
|
||||||
|
"frage_muster": frage_muster_path(topic),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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-*"))) if arbeit.is_dir() else []
|
dyn = (list(arbeit.glob("subbaustein-*")) + list(arbeit.glob("stufe-*")) + list(arbeit.glob("relevanz-*")) + list(arbeit.glob("frage-muster-*"))) 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"], *dyn,
|
files["sub_roh"], files["sidecar"], files["frage_muster"], *dyn,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -199,38 +251,37 @@ def _resume_step(topic: str) -> int:
|
|||||||
sidecar = _json_datei(files["sidecar"])
|
sidecar = _json_datei(files["sidecar"])
|
||||||
if _sidecar_schema(sidecar) is not None:
|
if _sidecar_schema(sidecar) is not None:
|
||||||
# Stufen fertig; nur noch Relevanz offen?
|
# Stufen fertig; nur noch Relevanz offen?
|
||||||
if _relevanz_komplett(sidecar):
|
if not _relevanz_komplett(sidecar):
|
||||||
return len(_bausteine_steps(topic))
|
|
||||||
return _step_idx(topic, "Relevanz finden")
|
return _step_idx(topic, "Relevanz finden")
|
||||||
|
# Relevanz fertig; nur noch Frage-Muster offen?
|
||||||
|
if not _frage_muster_komplett(topic):
|
||||||
|
return _step_idx(topic, "Fragen finden")
|
||||||
|
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")
|
||||||
return _step_idx(topic, "Stufen finden")
|
return _step_idx(topic, "Stufen finden")
|
||||||
|
|
||||||
|
|
||||||
def bausteine_status(topic: str) -> dict:
|
def bausteine_status(topic: str) -> dict:
|
||||||
steps = _bausteine_steps(topic)
|
# Intern feingranular (Resume/Progress); für die Anzeige zu 5 groben Phasen gebündelt.
|
||||||
|
feine = _bausteine_steps(topic)
|
||||||
ready = bausteine_path(topic).exists()
|
ready = bausteine_path(topic).exists()
|
||||||
generating = topic in _bausteine_progress
|
generating = topic in _bausteine_progress
|
||||||
partial = False
|
partial = False
|
||||||
if generating:
|
if generating:
|
||||||
current = _bausteine_step.get(topic)
|
current = _bausteine_step.get(topic)
|
||||||
states = [
|
|
||||||
"pending" if current is None else "done" if i < current else "active" if i == current else "pending"
|
|
||||||
for i in range(len(steps))
|
|
||||||
]
|
|
||||||
elif ready:
|
elif ready:
|
||||||
states = ["done"] * len(steps)
|
current = len(feine) # alles fertig → alle Phasen done
|
||||||
else:
|
else:
|
||||||
nxt = _resume_step(topic)
|
current = _resume_step(topic)
|
||||||
partial = nxt > 0
|
partial = current > 0
|
||||||
states = ["done" if i < nxt else "pending" for i in range(len(steps))]
|
|
||||||
return {
|
return {
|
||||||
"ready": ready,
|
"ready": ready,
|
||||||
"generating": generating,
|
"generating": generating,
|
||||||
"progress": _bausteine_progress.get(topic),
|
"progress": _bausteine_progress.get(topic),
|
||||||
"error": _bausteine_errors.get(topic),
|
"error": _bausteine_errors.get(topic),
|
||||||
"partial": partial,
|
"partial": partial,
|
||||||
"steps": [{"label": label, "state": s} for label, s in zip(steps, states)],
|
"steps": _phasen_status(topic, current),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -242,12 +293,49 @@ def reset_bausteine(topic: str) -> None:
|
|||||||
files = _bausteine_files(topic)
|
files = _bausteine_files(topic)
|
||||||
files["final"].unlink(missing_ok=True)
|
files["final"].unlink(missing_ok=True)
|
||||||
files["sidecar"].unlink(missing_ok=True) # liegt im Themen-Root, nicht in arbeit/
|
files["sidecar"].unlink(missing_ok=True) # liegt im Themen-Root, nicht in arbeit/
|
||||||
|
files["frage_muster"].unlink(missing_ok=True) # ebenfalls im Themen-Root
|
||||||
quelle_path(topic).unlink(missing_ok=True)
|
quelle_path(topic).unlink(missing_ok=True)
|
||||||
shutil.rmtree(quelle_crawl_dir(topic), ignore_errors=True) # gecrawlte Link-Quelle
|
shutil.rmtree(quelle_crawl_dir(topic), ignore_errors=True) # gecrawlte Link-Quelle
|
||||||
shutil.rmtree(files["arbeit"], ignore_errors=True)
|
shutil.rmtree(files["arbeit"], ignore_errors=True)
|
||||||
_bausteine_errors.pop(topic, None)
|
_bausteine_errors.pop(topic, None)
|
||||||
|
|
||||||
|
|
||||||
|
def _reset_ab_phase(topic: str, phase: int) -> None:
|
||||||
|
"""Artefakte AB der groben Phase löschen (1 Inventar … 5 Fragen), frühere behalten.
|
||||||
|
Danach baut der normale Resume-Flow die fehlenden Phasen neu. Kumulativ: Re-Run ab P
|
||||||
|
löscht P…5. quelle.json + Crawl bleiben immer (Quellen-Wahl erhalten)."""
|
||||||
|
files = _bausteine_files(topic)
|
||||||
|
arbeit = files["arbeit"]
|
||||||
|
|
||||||
|
def glob_del(pat: str) -> None:
|
||||||
|
if arbeit.is_dir():
|
||||||
|
for p in arbeit.glob(pat):
|
||||||
|
p.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
if phase <= 5: # Fragen
|
||||||
|
files["frage_muster"].unlink(missing_ok=True)
|
||||||
|
glob_del("frage-muster-*")
|
||||||
|
if phase <= 4: # Relevanz
|
||||||
|
glob_del("relevanz-*")
|
||||||
|
if phase <= 3: # Stufen + Relevanz teilen die Sidecar → ab Stufen ganz neu
|
||||||
|
files["sidecar"].unlink(missing_ok=True)
|
||||||
|
glob_del("stufe-*")
|
||||||
|
else: # ab Relevanz: Stufen behalten, nur Relevanz-Felder strippen
|
||||||
|
sc = _json_datei(files["sidecar"])
|
||||||
|
if isinstance(sc, dict):
|
||||||
|
for subs in sc.values():
|
||||||
|
for s in (subs if isinstance(subs, list) else []):
|
||||||
|
if isinstance(s, dict):
|
||||||
|
s.pop("relevanz", None)
|
||||||
|
atomic_write_json(files["sidecar"], sc, indent=1)
|
||||||
|
if phase <= 2: # Subbausteine
|
||||||
|
files["sub_roh"].unlink(missing_ok=True)
|
||||||
|
glob_del("subbaustein-*")
|
||||||
|
if phase <= 1: # Inventar = kompletter Frischstart (alle Zwischendateien)
|
||||||
|
for p_alt in _alle_slot_dateien(files):
|
||||||
|
p_alt.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
|
||||||
def _ergaenzung_schema(data):
|
def _ergaenzung_schema(data):
|
||||||
"""{"bausteine": [{"titel", "beschreibung"}]} → Liste (leer erlaubt) · sonst None."""
|
"""{"bausteine": [{"titel", "beschreibung"}]} → Liste (leer erlaubt) · sonst None."""
|
||||||
if not isinstance(data, dict) or not isinstance(data.get("bausteine"), list):
|
if not isinstance(data, dict) or not isinstance(data.get("bausteine"), list):
|
||||||
@@ -357,6 +445,32 @@ def _relevanz_komplett(data) -> bool:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _frage_muster_schema(data) -> list[dict] | None:
|
||||||
|
"""{"muster": [{subbaustein, typ, frage}, …]} → Liste valider Einträge · sonst None.
|
||||||
|
|
||||||
|
typ muss ein bekannter Fragetyp sein; subbaustein + frage nicht leer. Leere Liste → None.
|
||||||
|
"""
|
||||||
|
if not isinstance(data, dict) or not isinstance(data.get("muster"), list):
|
||||||
|
return None
|
||||||
|
out = []
|
||||||
|
for e in data["muster"]:
|
||||||
|
if not isinstance(e, dict):
|
||||||
|
return None
|
||||||
|
sub = str(e.get("subbaustein", "")).strip()
|
||||||
|
typ = str(e.get("typ", "")).strip().casefold()
|
||||||
|
frage = str(e.get("frage", "")).strip()
|
||||||
|
if not sub or typ not in FRAGETYPEN or not frage:
|
||||||
|
return None
|
||||||
|
out.append({"subbaustein": sub, "typ": typ, "frage": frage})
|
||||||
|
return out or None
|
||||||
|
|
||||||
|
|
||||||
|
def _frage_muster_komplett(topic: str) -> bool:
|
||||||
|
"""Frage-Muster-Sidecar existiert (Build gelaufen)? Einzelne leere Bausteine
|
||||||
|
fallen zur Prüfungszeit auf Live-Generierung zurück — daher genügt die Datei."""
|
||||||
|
return isinstance(_json_datei(frage_muster_path(topic)), dict)
|
||||||
|
|
||||||
|
|
||||||
def _read(p: Path) -> str:
|
def _read(p: Path) -> str:
|
||||||
return p.read_text(encoding="utf-8") if p.exists() else ""
|
return p.read_text(encoding="utf-8") if p.exists() else ""
|
||||||
|
|
||||||
@@ -741,7 +855,106 @@ async def _relevanz_block(ctx: GenContext, set_p, files: dict, sidecar: dict, in
|
|||||||
return relevanz_by_id
|
return relevanz_by_id
|
||||||
|
|
||||||
|
|
||||||
async def generate_bausteine(topic: str, instructions: str = "", provider: str = DEFAULT_PROVIDER) -> None:
|
def _norm_frage(t: str) -> str:
|
||||||
|
return " ".join(str(t or "").lower().split())
|
||||||
|
|
||||||
|
|
||||||
|
async def _frage_muster_block(ctx: GenContext, set_p, files: dict, sidecar: dict, instructions: str) -> dict | None:
|
||||||
|
"""Block E: drei Phasen — Finden (1 Generator je Baustein, parallel), Wählen (Code:
|
||||||
|
relevante Subs + Dedup), Klären (Kritiker je Baustein bereinigt die Tabelle).
|
||||||
|
Generativ statt Vote: Muster sind Text, kein 3-Rater-Konsens sinnvoll.
|
||||||
|
→ {Baustein-Titel: [{subbaustein, typ, frage}, …]} oder None bei Abbruch."""
|
||||||
|
topic, is_cancelled = ctx.topic, ctx.is_cancelled
|
||||||
|
arbeit = files["arbeit"]
|
||||||
|
# Je Baustein nur RELEVANTE Subbausteine (relevanz != 'rand'; None zählt als relevant).
|
||||||
|
bausteine = []
|
||||||
|
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()]
|
||||||
|
if rel:
|
||||||
|
bausteine.append((titel, rel))
|
||||||
|
if not bausteine:
|
||||||
|
return {}
|
||||||
|
typen_block = "\n".join(f"- {k}: {v}" for k, v in FRAGETYPEN.items())
|
||||||
|
|
||||||
|
def roh_path(c):
|
||||||
|
return arbeit / f"frage-muster-c{c}.json"
|
||||||
|
|
||||||
|
def final_path(c):
|
||||||
|
return arbeit / f"frage-muster-final-c{c}.json"
|
||||||
|
|
||||||
|
# Phase „Fragen finden": pro Baustein 1 Generator, alle parallel.
|
||||||
|
async def _finde(c, titel, rel):
|
||||||
|
fp = roh_path(c)
|
||||||
|
if _frage_muster_schema(_json_datei(fp)):
|
||||||
|
return # Resume
|
||||||
|
sub_block = "\n".join(f"- {s}" for s in rel)
|
||||||
|
status, _ = await run_single_slot(
|
||||||
|
ctx, f"Frage-Muster {c}",
|
||||||
|
key=f"bausteine-{topic}-frage-muster-c{c}",
|
||||||
|
prompt=_prompt("Frage-Muster-Recherche", topic=topic, baustein=titel,
|
||||||
|
subbausteine=sub_block, typen=typen_block, out_path=fp, extra=_extra(instructions)),
|
||||||
|
role="fast", capabilities="files",
|
||||||
|
payload=lambda result, p=fp: _frage_muster_schema(_json_datei(p)),
|
||||||
|
timeout=_timeout("frage_muster", len(rel)),
|
||||||
|
)
|
||||||
|
if status == FAILED:
|
||||||
|
_log(topic, f"Frage-Muster Baustein {c} fehlgeschlagen — kein Muster (Fallback Live)")
|
||||||
|
|
||||||
|
set_p(f"Fragen finden ({len(bausteine)} Bausteine)…", step=_step_idx(topic, "Fragen finden"))
|
||||||
|
await asyncio.gather(*[_finde(c, t, r) for c, (t, r) in enumerate(bausteine, 1)], return_exceptions=True)
|
||||||
|
if is_cancelled():
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Phase „Fragen wählen": Code — nur relevante Subbausteine, Dubletten je Baustein raus.
|
||||||
|
set_p("Fragen wählen…", step=_step_idx(topic, "Fragen wählen"))
|
||||||
|
roh_by_c = {}
|
||||||
|
for c, (titel, rel) in enumerate(bausteine, 1):
|
||||||
|
eintraege = _frage_muster_schema(_json_datei(roh_path(c))) or []
|
||||||
|
rel_set = set(rel)
|
||||||
|
gesehen, sauber = set(), []
|
||||||
|
for e in eintraege:
|
||||||
|
norm = _norm_frage(e["frage"])
|
||||||
|
if e["subbaustein"] not in rel_set or norm in gesehen:
|
||||||
|
continue
|
||||||
|
gesehen.add(norm)
|
||||||
|
sauber.append(e)
|
||||||
|
roh_by_c[c] = sauber
|
||||||
|
|
||||||
|
# Phase „Fragen klären": Kritiker je Baustein bereinigt die Tabelle (eindeutig, distinkt).
|
||||||
|
async def _klaere(c, titel):
|
||||||
|
roh = roh_by_c[c]
|
||||||
|
fp = final_path(c)
|
||||||
|
if _frage_muster_schema(_json_datei(fp)) or not roh:
|
||||||
|
return
|
||||||
|
tabelle = "\n".join(f"{i}. [{e['typ']}] ({e['subbaustein']}) {e['frage']}" for i, e in enumerate(roh, 1))
|
||||||
|
status, _ = await run_single_slot(
|
||||||
|
ctx, f"Frage-Muster-Klärung {c}",
|
||||||
|
key=f"bausteine-{topic}-frage-muster-final-c{c}",
|
||||||
|
prompt=_prompt("Frage-Muster-Kritik", topic=topic, baustein=titel, tabelle=tabelle, out_path=fp, extra=_extra(instructions)),
|
||||||
|
role="judge", capabilities="files",
|
||||||
|
payload=lambda result, p=fp: _frage_muster_schema(_json_datei(p)),
|
||||||
|
timeout=_timeout("frage_muster_check", len(roh)),
|
||||||
|
)
|
||||||
|
if status == FAILED:
|
||||||
|
_log(topic, f"Frage-Muster-Klärung Baustein {c} fehlgeschlagen — Roh-Muster übernommen")
|
||||||
|
|
||||||
|
set_p(f"Fragen klären ({len(bausteine)} Bausteine)…", step=_step_idx(topic, "Fragen klären"))
|
||||||
|
await asyncio.gather(*[_klaere(c, t) for c, (t, _) in enumerate(bausteine, 1)], return_exceptions=True)
|
||||||
|
if is_cancelled():
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Final: geklärte Tabelle je Baustein, Fallback auf Roh-Muster.
|
||||||
|
ergebnis = {}
|
||||||
|
for c, (titel, rel) in enumerate(bausteine, 1):
|
||||||
|
rel_set = set(rel)
|
||||||
|
final = _frage_muster_schema(_json_datei(final_path(c)))
|
||||||
|
if not final:
|
||||||
|
final = roh_by_c[c]
|
||||||
|
ergebnis[titel] = [e for e in final if e["subbaustein"] in rel_set]
|
||||||
|
return ergebnis
|
||||||
|
|
||||||
|
|
||||||
|
async def generate_bausteine(topic: str, instructions: str = "", provider: str = DEFAULT_PROVIDER, ab_phase: int | None = None) -> None:
|
||||||
if topic in _bausteine_progress:
|
if topic in _bausteine_progress:
|
||||||
return
|
return
|
||||||
_bausteine_progress[topic] = "Wartend…"
|
_bausteine_progress[topic] = "Wartend…"
|
||||||
@@ -769,6 +982,10 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
|
|||||||
try:
|
try:
|
||||||
async with _semaphore:
|
async with _semaphore:
|
||||||
files["arbeit"].mkdir(parents=True, exist_ok=True)
|
files["arbeit"].mkdir(parents=True, exist_ok=True)
|
||||||
|
# Re-Run ab gewählter Phase: Artefakte ab dort löschen; der Frischstart-Block
|
||||||
|
# unten wird übersprungen (er würde bei erhaltener Sidecar sonst alles wischen).
|
||||||
|
if ab_phase is not None:
|
||||||
|
_reset_ab_phase(topic, ab_phase)
|
||||||
# Link-Quelle: erst crawlen (gleiche Domain, begrenzt) → wird zur Ordner-Quelle.
|
# Link-Quelle: erst crawlen (gleiche Domain, begrenzt) → wird zur Ordner-Quelle.
|
||||||
if q["type"] == "link" and not _crawl_fertig(topic):
|
if q["type"] == "link" and not _crawl_fertig(topic):
|
||||||
set_p("Quelle laden (Crawl)…", step=_step_idx(topic, "Quelle laden"))
|
set_p("Quelle laden (Crawl)…", step=_step_idx(topic, "Quelle laden"))
|
||||||
@@ -784,7 +1001,8 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
|
|||||||
# „Neu erstellen": NUR wenn wirklich alles fertig ist (bausteine.md UND
|
# „Neu erstellen": NUR wenn wirklich alles fertig ist (bausteine.md UND
|
||||||
# Sidecar) → kompletter Frischstart. Liegt bausteine.md ohne Sidecar vor,
|
# Sidecar) → kompletter Frischstart. Liegt bausteine.md ohne Sidecar vor,
|
||||||
# ist das ein Teil-Stand (Block B/C offen) → Resume, nicht wischen.
|
# ist das ein Teil-Stand (Block B/C offen) → Resume, nicht wischen.
|
||||||
fertig = final_path.exists() and _sidecar_schema(_json_datei(files["sidecar"])) is not None
|
# Bei explizitem Re-Run (ab_phase) hat _reset_ab_phase das schon erledigt.
|
||||||
|
fertig = ab_phase is None and final_path.exists() and _sidecar_schema(_json_datei(files["sidecar"])) is not None
|
||||||
if fertig:
|
if fertig:
|
||||||
for p_alt in _alle_slot_dateien(files):
|
for p_alt in _alle_slot_dateien(files):
|
||||||
p_alt.unlink(missing_ok=True)
|
p_alt.unlink(missing_ok=True)
|
||||||
@@ -1019,6 +1237,19 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
|
|||||||
gid += 1
|
gid += 1
|
||||||
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 E: Frage-Muster je relevantem Subbaustein × Typ → eigenes Sidecar.
|
||||||
|
# Zur Prüfungszeit zieht jeder Agent ein Muster ohne Zurücklegen und formuliert
|
||||||
|
# daraus eine Frage — distinkte Saat verhindert die Doppelfragen der Live-Generierung.
|
||||||
|
sidecar = _json_datei(files["sidecar"])
|
||||||
|
if _sidecar_schema(sidecar) is not None and _relevanz_komplett(sidecar) and not _frage_muster_komplett(topic):
|
||||||
|
muster = await _frage_muster_block(ctx, set_p, files, sidecar, instructions)
|
||||||
|
if is_cancelled():
|
||||||
|
abgebrochen()
|
||||||
|
return
|
||||||
|
if muster is None:
|
||||||
|
return # Abbruch
|
||||||
|
atomic_write_json(files["frage_muster"], muster, indent=1)
|
||||||
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]
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ TIMEOUTS = {
|
|||||||
"stufe_check": (300, 10), # Judge entscheidet strittige Stufen im Chunk
|
"stufe_check": (300, 10), # Judge entscheidet strittige Stufen im Chunk
|
||||||
"relevanz": (300, 10), # Subbausteine relevant/rand je Chunk
|
"relevanz": (300, 10), # Subbausteine relevant/rand je Chunk
|
||||||
"relevanz_check": (300, 10), # Judge entscheidet strittige Relevanz im Chunk
|
"relevanz_check": (300, 10), # Judge entscheidet strittige Relevanz im Chunk
|
||||||
|
"frage_muster": (300, 15), # Frage-Muster je Baustein (Subbausteine × Typen)
|
||||||
|
"frage_muster_check": (300, 10), # Kritiker bereinigt die Muster-Tabelle je Baustein
|
||||||
"writer": (600, 120), # pro Section im Chunk
|
"writer": (600, 120), # pro Section im Chunk
|
||||||
"lese_check": (300, 10), # pro Section im Paket
|
"lese_check": (300, 10), # pro Section im Paket
|
||||||
"onepager_recherche": (900, 0),
|
"onepager_recherche": (900, 0),
|
||||||
|
|||||||
@@ -273,6 +273,28 @@ async def pruefung_frage(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def pruefung_frage_variante(
|
||||||
|
topic: str, baustein: str, section: str, kompakt: str | None,
|
||||||
|
muster: str, provider: str = DEFAULT_PROVIDER,
|
||||||
|
) -> str | None:
|
||||||
|
"""Aktion 'frage' mit Muster: aus einem vordefinierten Muster eine konkrete, im
|
||||||
|
Wortlaut leicht andere Frage formulieren. Kein Kritiker (Muster ist build-geprüft).
|
||||||
|
Stil-Guard bleibt als billiger Schutz gegen Doppelfragen · None bei Fehler."""
|
||||||
|
try:
|
||||||
|
section_block, kompakt_block = _bloecke(section, kompakt)
|
||||||
|
data = await _gen_call(
|
||||||
|
"Baustein-Frage-Variante", "fast", _frage_schema, provider,
|
||||||
|
topic=topic, baustein=baustein, section_block=section_block,
|
||||||
|
kompakt_block=kompakt_block, muster=muster,
|
||||||
|
)
|
||||||
|
if data is None:
|
||||||
|
return None
|
||||||
|
return data["frage"]
|
||||||
|
except Exception:
|
||||||
|
log.warning("[%s] Frage-Variante fehlgeschlagen (%s)", topic, baustein, exc_info=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def pruefung_bewertung_schnell(
|
async def pruefung_bewertung_schnell(
|
||||||
topic: str, baustein: str, section: str, kompakt: str | None,
|
topic: str, baustein: str, section: str, kompakt: str | None,
|
||||||
frage: str, messages: list[dict], provider: str = DEFAULT_PROVIDER,
|
frage: str, messages: list[dict], provider: str = DEFAULT_PROVIDER,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class BausteineCreateRequest(BaseModel):
|
|||||||
provider: ProviderType = "claude"
|
provider: ProviderType = "claude"
|
||||||
source_type: SourceType = "thema"
|
source_type: SourceType = "thema"
|
||||||
source_ort: str = Field(default="", max_length=2000)
|
source_ort: str = Field(default="", max_length=2000)
|
||||||
|
ab_phase: int | None = Field(default=None, ge=1, le=5) # Re-Run ab grober Phase (1 Inventar … 5 Fragen); None = Resume/Fortsetzen ohne Löschen
|
||||||
|
|
||||||
|
|
||||||
class BausteineStep(BaseModel):
|
class BausteineStep(BaseModel):
|
||||||
@@ -220,6 +221,7 @@ class BausteinPruefungRequest(BaseModel):
|
|||||||
vermeide: list[str] = [] # schon gestellte + vorgemerkte Fragen — sinngemäß nicht wiederholen
|
vermeide: list[str] = [] # schon gestellte + vorgemerkte Fragen — sinngemäß nicht wiederholen
|
||||||
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)
|
||||||
# Basis wird serverseitig je Frage geführt (offene_frage-Anker) — nicht mehr vom Client.
|
# Basis wird serverseitig je Frage geführt (offene_frage-Anker) — nicht mehr vom Client.
|
||||||
tier2: bool = False # ganzer Guide absolviert (alle ≥3) → −1 bei falsch, Deckel 10
|
tier2: bool = False # ganzer Guide absolviert (alle ≥3) → −1 bei falsch, Deckel 10
|
||||||
tier3: bool = False # ganzer Guide verstanden (alle ≥10) → Meisterpfad, −2 bei falsch, Deckel 25
|
tier3: bool = False # ganzer Guide verstanden (alle ≥10) → Meisterpfad, −2 bei falsch, Deckel 25
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ def subbausteine_path(topic: str) -> Path:
|
|||||||
return topic_dir(topic) / "subbausteine.json"
|
return topic_dir(topic) / "subbausteine.json"
|
||||||
|
|
||||||
|
|
||||||
|
def frage_muster_path(topic: str) -> Path:
|
||||||
|
"""Sidecar: pro Baustein vordefinierte Frage-Muster (Subbaustein × Typ → Beispielfrage)."""
|
||||||
|
return topic_dir(topic) / "frage_muster.json"
|
||||||
|
|
||||||
|
|
||||||
def quelle_path(topic: str) -> Path:
|
def quelle_path(topic: str) -> Path:
|
||||||
"""Persistierte Quellen-Wahl pro Thema: {type, ort, spec}."""
|
"""Persistierte Quellen-Wahl pro Thema: {type, ort, spec}."""
|
||||||
return topic_dir(topic) / "quelle.json"
|
return topic_dir(topic) / "quelle.json"
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ from database import (
|
|||||||
list_baustein_progress, get_baustein_progress, set_offene_frage,
|
list_baustein_progress, get_baustein_progress, set_offene_frage,
|
||||||
set_baustein_score, set_baustein_absolviert, set_baustein_verstanden, set_baustein_gemeistert, delete_baustein_daten,
|
set_baustein_score, set_baustein_absolviert, set_baustein_verstanden, set_baustein_gemeistert, delete_baustein_daten,
|
||||||
)
|
)
|
||||||
from bausteine import generate_bausteine, cancel_bausteine, bausteine_status, active_bausteine, reset_bausteine, lade_quelle, lade_uebersicht, subbausteine_titel
|
from bausteine import generate_bausteine, cancel_bausteine, bausteine_status, active_bausteine, reset_bausteine, lade_quelle, lade_uebersicht, subbausteine_titel, lade_frage_muster
|
||||||
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 NOETIG, MASTERY, MEISTERN, baustein_chat, baustein_diskussion, baustein_element_anlegen, pruefung_bewertung, pruefung_bewertung_schnell, pruefung_frage, score_berechnen
|
from lernen import NOETIG, MASTERY, MEISTERN, baustein_chat, baustein_diskussion, baustein_element_anlegen, pruefung_bewertung, pruefung_bewertung_schnell, pruefung_frage, pruefung_frage_variante, score_berechnen
|
||||||
from guide import generate_guide, guide_slot_dateien
|
from guide import generate_guide, guide_slot_dateien
|
||||||
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
|
||||||
@@ -156,7 +156,7 @@ async def create_bausteine(req: BausteineCreateRequest):
|
|||||||
raise HTTPException(400, "Link muss mit http:// oder https:// beginnen.")
|
raise HTTPException(400, "Link muss mit http:// oder https:// beginnen.")
|
||||||
qp.parent.mkdir(parents=True, exist_ok=True)
|
qp.parent.mkdir(parents=True, exist_ok=True)
|
||||||
atomic_write_json(qp, {"type": typ, "ort": ort, "spec": req.instructions.strip()})
|
atomic_write_json(qp, {"type": typ, "ort": ort, "spec": req.instructions.strip()})
|
||||||
asyncio.create_task(generate_bausteine(topic, req.instructions.strip(), req.provider))
|
asyncio.create_task(generate_bausteine(topic, req.instructions.strip(), req.provider, ab_phase=req.ab_phase))
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
@@ -206,6 +206,12 @@ async def get_bausteine_uebersicht(topic: str):
|
|||||||
return lade_uebersicht(topic)
|
return lade_uebersicht(topic)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/bausteine/frage-muster")
|
||||||
|
async def get_frage_muster(topic: str, baustein: str):
|
||||||
|
"""Vordefinierte Frage-Muster eines Bausteins (leer = Fallback auf Live-Generierung)."""
|
||||||
|
return {"muster": lade_frage_muster(topic, baustein)}
|
||||||
|
|
||||||
|
|
||||||
# --- Baustein-Lernen: Chat, Prüfung ---
|
# --- Baustein-Lernen: Chat, Prüfung ---
|
||||||
|
|
||||||
@router.get("/bausteine/lernstand", response_model=BausteinLernstandResponse)
|
@router.get("/bausteine/lernstand", response_model=BausteinLernstandResponse)
|
||||||
@@ -270,6 +276,11 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest):
|
|||||||
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():
|
||||||
|
# Aus gezogenem Muster eine konkrete, leicht andere Frage formulieren (kein Dedup nötig).
|
||||||
|
frage = await pruefung_frage_variante(req.topic, req.baustein, req.section, kompakt, req.muster, provider=req.provider)
|
||||||
|
else:
|
||||||
|
# Fallback (kein Muster-Sidecar): Live-Generierung mit zufälligem Typ/Subbaustein.
|
||||||
subs = subbausteine_titel(req.topic, req.baustein) # Fokus-Kandidaten (zufällig gewählt)
|
subs = subbausteine_titel(req.topic, req.baustein) # Fokus-Kandidaten (zufällig gewählt)
|
||||||
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, provider=req.provider)
|
||||||
if frage is None:
|
if frage is None:
|
||||||
|
|||||||
@@ -233,11 +233,12 @@ async function handleResetBausteine() {
|
|||||||
await loadBausteine()
|
await loadBausteine()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleBausteineClick({ instructions }) {
|
async function handleBausteineClick({ instructions, abPhase = null }) {
|
||||||
if (!selectedTopic.value) return
|
if (!selectedTopic.value) return
|
||||||
uiError.value = null
|
uiError.value = null
|
||||||
try {
|
try {
|
||||||
await apiCreateBausteine(selectedTopic.value, instructions, provider.value)
|
// Quelle ist hier schon festgeschrieben; nur abPhase steuert den Re-Run-Umfang.
|
||||||
|
await apiCreateBausteine(selectedTopic.value, instructions, provider.value, undefined, undefined, abPhase)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
uiError.value = e.message
|
uiError.value = e.message
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -42,11 +42,11 @@ export async function fetchBausteineStatus(topic) {
|
|||||||
return res.json()
|
return res.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createBausteine(topic, instructions = '', provider = 'claude', sourceType = 'thema', sourceOrt = '') {
|
export async function createBausteine(topic, instructions = '', provider = 'claude', sourceType = 'thema', sourceOrt = '', abPhase = null) {
|
||||||
const res = await fetch(`${BASE}/bausteine`, {
|
const res = await fetch(`${BASE}/bausteine`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ topic, instructions, provider, source_type: sourceType, source_ort: sourceOrt }),
|
body: JSON.stringify({ topic, instructions, provider, source_type: sourceType, source_ort: sourceOrt, ab_phase: abPhase }),
|
||||||
})
|
})
|
||||||
return jsonOrThrow(res)
|
return jsonOrThrow(res)
|
||||||
}
|
}
|
||||||
@@ -78,16 +78,21 @@ export async function chatBaustein({ topic, baustein, section, section_kompakt =
|
|||||||
export async function pruefeBaustein({
|
export async function pruefeBaustein({
|
||||||
topic, baustein, section, section_kompakt = '', provider,
|
topic, baustein, section, section_kompakt = '', provider,
|
||||||
aktion = 'frage', frage = '', letzte_bewertung = '', vermeide = [],
|
aktion = 'frage', frage = '', letzte_bewertung = '', vermeide = [],
|
||||||
nachgefragt = false, begruendung = '', tier2 = false, tier3 = false, messages = [], gruendlich = false,
|
nachgefragt = false, begruendung = '', muster = '', tier2 = false, tier3 = false, messages = [], gruendlich = false,
|
||||||
}) {
|
}) {
|
||||||
const res = await fetch(`${BASE}/bausteine/pruefung`, {
|
const res = await fetch(`${BASE}/bausteine/pruefung`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ topic, baustein, section, section_kompakt, aktion, frage, letzte_bewertung, vermeide, nachgefragt, begruendung, tier2, tier3, messages, provider, gruendlich }),
|
body: JSON.stringify({ topic, baustein, section, section_kompakt, aktion, frage, letzte_bewertung, vermeide, nachgefragt, begruendung, muster, tier2, tier3, messages, provider, gruendlich }),
|
||||||
})
|
})
|
||||||
return jsonOrThrow(res)
|
return jsonOrThrow(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchFrageMuster(topic, baustein) {
|
||||||
|
const res = await fetch(`${BASE}/bausteine/frage-muster?topic=${encodeURIComponent(topic)}&baustein=${encodeURIComponent(baustein)}`)
|
||||||
|
return jsonOrThrow(res)
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchTopicFortschritt(topic) {
|
export async function fetchTopicFortschritt(topic) {
|
||||||
const res = await fetch(`${BASE}/topics/fortschritt?topic=${encodeURIComponent(topic)}`)
|
const res = await fetch(`${BASE}/topics/fortschritt?topic=${encodeURIComponent(topic)}`)
|
||||||
return res.json()
|
return res.json()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
|
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import { chatBaustein, pruefeBaustein } from '../api.js'
|
import { chatBaustein, pruefeBaustein, fetchFrageMuster } from '../api.js'
|
||||||
import { usePruefSlot } from '../pruefungCache.js'
|
import { usePruefSlot } from '../pruefungCache.js'
|
||||||
import { renderMarkdown } from '../markdown.js'
|
import { renderMarkdown } from '../markdown.js'
|
||||||
import { useChat, istUnten } from '../composables/useChat.js'
|
import { useChat, istUnten } from '../composables/useChat.js'
|
||||||
@@ -53,6 +53,9 @@ const pruefMessages = slot.messages // {role, kind: 'frage'|'nachfrage'|'antwort
|
|||||||
const pruefPhase = slot.phase
|
const pruefPhase = slot.phase
|
||||||
const aktuelleFrage = slot.aktuelleFrage // ankert Bewertung/Diskussion
|
const aktuelleFrage = slot.aktuelleFrage // ankert Bewertung/Diskussion
|
||||||
const letztesFeedback = slot.letztesFeedback // Kontext für die Diskussion über eine Bewertung
|
const letztesFeedback = slot.letztesFeedback // Kontext für die Diskussion über eine Bewertung
|
||||||
|
const musterQuelle = slot.musterQuelle // unveränderliche Voll-Liste der Frage-Muster (leer = Fallback)
|
||||||
|
const musterPool = slot.musterPool // Arbeitskopie, ohne Zurücklegen; leer → Reset
|
||||||
|
const musterGeladen = slot.musterGeladen
|
||||||
// Transient (pro Instanz): Eingabe-Entwurf, Lade-Spinner.
|
// Transient (pro Instanz): Eingabe-Entwurf, Lade-Spinner.
|
||||||
const pruefInput = ref('')
|
const pruefInput = ref('')
|
||||||
const pruefLoading = ref(false)
|
const pruefLoading = ref(false)
|
||||||
@@ -124,36 +127,76 @@ function gestellteFragen() {
|
|||||||
return new Set(pruefMessages.value.filter((m) => m.kind === 'frage').map((m) => normFrage(m.content)))
|
return new Set(pruefMessages.value.filter((m) => m.kind === 'frage').map((m) => normFrage(m.content)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Fragen-Pool: ZIEL vorgenerierte, geprüfte, unbeantwortete Fragen je Baustein (im Slot) ---
|
// --- Fragen-Pool: ZIEL vorformulierte, unbeantwortete Fragen je Baustein (im Slot) ---
|
||||||
const POOL_ZIEL = 5
|
const POOL_ZIEL = 5
|
||||||
const pool = slot.pool
|
const pool = slot.pool
|
||||||
const inflight = slot.inflight
|
const inflight = slot.inflight
|
||||||
let poolMiss = 0 // aufeinanderfolgende Dubletten/Fehler — Cap gegen Endlosschleife (kleine Bausteine)
|
let poolMiss = 0 // nur Fallback: aufeinanderfolgende Dubletten/Fehler — Cap gegen Endlosschleife
|
||||||
|
|
||||||
|
// Muster-Modus aktiv, sobald geladen und Muster vorhanden; sonst Fallback auf Live-Generierung.
|
||||||
|
const musterModus = computed(() => musterGeladen.value && musterQuelle.value.length > 0)
|
||||||
|
|
||||||
function istBekannt(text) {
|
function istBekannt(text) {
|
||||||
const n = normFrage(text)
|
const n = normFrage(text)
|
||||||
return !n || gestellteFragen().has(n) || pool.value.some((f) => normFrage(f) === n)
|
return !n || gestellteFragen().has(n) || pool.value.some((f) => normFrage(f) === n)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Schon gestellte + bereits vorgemerkte Fragen — der Generator/Kritiker soll sie
|
// Schon gestellte + bereits vorgemerkte Fragen — der Generator soll sie (auch
|
||||||
// (auch sinngemäß) meiden. Original-Texte, nicht normalisiert.
|
// sinngemäß) meiden. Nur Fallback-Modus. Original-Texte, nicht normalisiert.
|
||||||
function vermeideListe() {
|
function vermeideListe() {
|
||||||
const gestellt = pruefMessages.value.filter((m) => m.kind === 'frage').map((m) => m.content)
|
const gestellt = pruefMessages.value.filter((m) => m.kind === 'frage').map((m) => m.content)
|
||||||
return [...gestellt, ...pool.value]
|
return [...gestellt, ...pool.value]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Muster-Sidecar einmal je Baustein laden (Slot-cached). Leer → Fallback.
|
||||||
|
async function musterLaden() {
|
||||||
|
if (musterGeladen.value) return
|
||||||
|
try {
|
||||||
|
const res = await fetchFrageMuster(props.topic, props.baustein)
|
||||||
|
musterQuelle.value = (res.muster || []).map((m) => (m.frage || '').trim()).filter(Boolean)
|
||||||
|
} catch {
|
||||||
|
musterQuelle.value = []
|
||||||
|
}
|
||||||
|
musterGeladen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function mischen(arr) { // Fisher-Yates
|
||||||
|
const a = [...arr]
|
||||||
|
for (let i = a.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1))
|
||||||
|
;[a[i], a[j]] = [a[j], a[i]]
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ein Muster ohne Zurücklegen ziehen; Pool leer → Reset (neu mischen). null = Fallback.
|
||||||
|
function musterNehmen() {
|
||||||
|
if (!musterQuelle.value.length) return null
|
||||||
|
if (!musterPool.value.length) musterPool.value = mischen(musterQuelle.value)
|
||||||
|
return musterPool.value.shift()
|
||||||
|
}
|
||||||
|
|
||||||
function frageEinzeln() {
|
function frageEinzeln() {
|
||||||
// Ein Agent → eine Frage (Backend wählt zufälligen Typ + Subbaustein, dann Kritiker).
|
const muster = musterNehmen()
|
||||||
|
if (muster) {
|
||||||
|
// Muster-Modus: aus gezogener Saat eine konkrete, leicht andere Frage formulieren.
|
||||||
|
return pruefeBaustein({
|
||||||
|
topic: props.topic, baustein: props.baustein, section: props.section, section_kompakt: props.sectionKompakt,
|
||||||
|
provider: props.provider, aktion: 'frage', muster,
|
||||||
|
}).then((res) => res.frage)
|
||||||
|
}
|
||||||
|
// Fallback (kein Muster-Sidecar): Live-Generierung mit Vermeide-Liste.
|
||||||
return pruefeBaustein({
|
return pruefeBaustein({
|
||||||
topic: props.topic, baustein: props.baustein, section: props.section, section_kompakt: props.sectionKompakt,
|
topic: props.topic, baustein: props.baustein, section: props.section, section_kompakt: props.sectionKompakt,
|
||||||
provider: props.provider, aktion: 'frage', messages: pruefDialog(), vermeide: vermeideListe(),
|
provider: props.provider, aktion: 'frage', messages: pruefDialog(), vermeide: vermeideListe(),
|
||||||
}).then((res) => res.frage)
|
}).then((res) => res.frage)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pool PARALLEL auffüllen: pro fehlender Frage ein eigener Agent gleichzeitig.
|
// Pool PARALLEL auffüllen: pro fehlender Frage ein eigener Agent. Im Muster-Modus
|
||||||
// Initial 5 parallel; nach Verbrauch genau die fehlende(n) nach.
|
// zieht jeder ein DISTINKTES Muster (ohne Zurücklegen) → keine Kollision, kein Miss-Cap.
|
||||||
function poolFuellen() {
|
function poolFuellen() {
|
||||||
while (pool.value.length + inflight.value < POOL_ZIEL && poolMiss < 3) {
|
const cap = musterModus.value ? Infinity : 3
|
||||||
|
while (pool.value.length + inflight.value < POOL_ZIEL && poolMiss < cap) {
|
||||||
inflight.value++
|
inflight.value++
|
||||||
frageEinzeln()
|
frageEinzeln()
|
||||||
.then((text) => {
|
.then((text) => {
|
||||||
@@ -168,9 +211,10 @@ function poolFuellen() {
|
|||||||
// Eine Frage aus dem Pool nehmen; ist er leer, auf die erste eintreffende warten.
|
// Eine Frage aus dem Pool nehmen; ist er leer, auf die erste eintreffende warten.
|
||||||
async function poolNehmen() {
|
async function poolNehmen() {
|
||||||
if (!pool.value.length) {
|
if (!pool.value.length) {
|
||||||
|
const cap = musterModus.value ? Infinity : 3
|
||||||
pruefLoading.value = true
|
pruefLoading.value = true
|
||||||
try {
|
try {
|
||||||
while (!pool.value.length && (inflight.value > 0 || poolMiss < 3)) {
|
while (!pool.value.length && (inflight.value > 0 || poolMiss < cap)) {
|
||||||
if (!inflight.value) poolFuellen()
|
if (!inflight.value) poolFuellen()
|
||||||
await new Promise((r) => setTimeout(r, 120))
|
await new Promise((r) => setTimeout(r, 120))
|
||||||
}
|
}
|
||||||
@@ -179,10 +223,11 @@ async function poolNehmen() {
|
|||||||
return pool.value.length ? pool.value.shift() : null
|
return pool.value.length ? pool.value.shift() : null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Erste Frage (Phase idle): ZIEL parallel anstoßen, schnellste zuerst zeigen.
|
// Erste Frage (Phase idle): Muster laden, ZIEL parallel anstoßen, schnellste zuerst zeigen.
|
||||||
async function frageAnfordern() {
|
async function frageAnfordern() {
|
||||||
if (pruefLoading.value) return
|
if (pruefLoading.value) return
|
||||||
poolMiss = 0
|
poolMiss = 0
|
||||||
|
await musterLaden()
|
||||||
poolFuellen()
|
poolFuellen()
|
||||||
const text = await poolNehmen()
|
const text = await poolNehmen()
|
||||||
if (text) {
|
if (text) {
|
||||||
@@ -197,6 +242,7 @@ async function frageAnfordern() {
|
|||||||
// „Nächste Frage": sofort aus dem Pool, dann nachfüllen.
|
// „Nächste Frage": sofort aus dem Pool, dann nachfüllen.
|
||||||
async function naechsteFrageZeigen() {
|
async function naechsteFrageZeigen() {
|
||||||
if (pruefLoading.value) return
|
if (pruefLoading.value) return
|
||||||
|
await musterLaden()
|
||||||
const text = await poolNehmen()
|
const text = await poolNehmen()
|
||||||
if (text) {
|
if (text) {
|
||||||
frageZeigen(text)
|
frageZeigen(text)
|
||||||
|
|||||||
@@ -67,6 +67,17 @@ const bausteineState = computed(() => {
|
|||||||
return props.bausteine.ready ? 'done' : 'none'
|
return props.bausteine.ready ? 'done' : 'none'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Re-Run ab gewählter Phase (1-basiert). null = ab Anfang / Fortsetzen.
|
||||||
|
const gewaehltePhase = ref(null)
|
||||||
|
// Phasen anklickbar, sobald etwas (teil-)gebaut ist und gerade nicht generiert wird.
|
||||||
|
const phasenWaehlbar = computed(() => (props.bausteine.ready || props.bausteine.partial) && !props.bausteine.generating)
|
||||||
|
const gewaehltesLabel = computed(() => (props.bausteine.steps || [])[(gewaehltePhase.value || 0) - 1]?.label || '')
|
||||||
|
|
||||||
|
function phaseKlick(n) {
|
||||||
|
if (!phasenWaehlbar.value) return
|
||||||
|
gewaehltePhase.value = gewaehltePhase.value === n ? null : n
|
||||||
|
}
|
||||||
|
|
||||||
// Nur FREMDE Themen — das gewählte Thema zeigt seinen Fortschritt inline an der Zeile
|
// Nur FREMDE Themen — das gewählte Thema zeigt seinen Fortschritt inline an der Zeile
|
||||||
const activeGenerations = computed(() => {
|
const activeGenerations = computed(() => {
|
||||||
const bausteinLines = props.activeBausteine
|
const bausteinLines = props.activeBausteine
|
||||||
@@ -90,7 +101,10 @@ function confirmResetBausteine() {
|
|||||||
|
|
||||||
function handleBausteinePlay() {
|
function handleBausteinePlay() {
|
||||||
if (bausteineState.value === 'generating') return
|
if (bausteineState.value === 'generating') return
|
||||||
emit('bausteineClick', { instructions: '' })
|
// Nur „Neu generieren" (ready) nutzt eine Phase; Fortsetzen/Erstbau resumen ohne Löschen.
|
||||||
|
const abPhase = props.bausteine.ready ? (gewaehltePhase.value ?? 1) : null
|
||||||
|
emit('bausteineClick', { instructions: '', abPhase })
|
||||||
|
gewaehltePhase.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name-Klick = Primäraktion: fertige Bausteine → Übersicht, sonst Panel auf/zu.
|
// Name-Klick = Primäraktion: fertige Bausteine → Übersicht, sonst Panel auf/zu.
|
||||||
@@ -361,12 +375,13 @@ function saveQuelle() {
|
|||||||
>Pausiert</span>
|
>Pausiert</span>
|
||||||
<span class="step-dots">
|
<span class="step-dots">
|
||||||
<span
|
<span
|
||||||
v-for="s in (bausteine.steps || [])"
|
v-for="(s, i) in (bausteine.steps || [])"
|
||||||
:key="s.label"
|
:key="s.label"
|
||||||
class="step-dot"
|
class="step-pill"
|
||||||
:class="s.state"
|
:class="[s.state, { sel: gewaehltePhase === i + 1, klickbar: phasenWaehlbar }]"
|
||||||
:title="s.state === 'active' ? (bausteine.progress || s.label) : s.label"
|
:title="(s.state === 'active' ? (bausteine.progress || s.label) : s.label) + (phasenWaehlbar ? ' — Klick: ab hier neu' : '')"
|
||||||
></span>
|
@click.stop="phaseKlick(i + 1)"
|
||||||
|
>{{ i + 1 }}</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="panel-toggle" :class="{ open: isOpen('bausteine') }" title="Aktionen" @click.stop="togglePanel('bausteine')">▾</button>
|
<button class="panel-toggle" :class="{ open: isOpen('bausteine') }" title="Aktionen" @click.stop="togglePanel('bausteine')">▾</button>
|
||||||
@@ -376,7 +391,7 @@ function saveQuelle() {
|
|||||||
<button class="panel-btn danger" :class="{ armed: pendingConfirm === 'bausteine' }" @click="confirmCancelBausteine">{{ pendingConfirm === 'bausteine' ? 'Sicher?' : 'Abbrechen' }}</button>
|
<button class="panel-btn danger" :class="{ armed: pendingConfirm === 'bausteine' }" @click="confirmCancelBausteine">{{ pendingConfirm === 'bausteine' ? 'Sicher?' : 'Abbrechen' }}</button>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<button class="panel-btn play" @click="handleBausteinePlay">{{ bausteine.partial ? 'Fortsetzen' : bausteine.ready ? 'Neu generieren' : 'Generieren' }}</button>
|
<button class="panel-btn play" @click="handleBausteinePlay">{{ bausteine.partial ? 'Fortsetzen' : bausteine.ready ? (gewaehltePhase ? `Ab «${gewaehltesLabel}» neu` : 'Neu generieren') : 'Generieren' }}</button>
|
||||||
<button
|
<button
|
||||||
v-if="bausteine.ready || bausteine.partial"
|
v-if="bausteine.ready || bausteine.partial"
|
||||||
class="panel-btn danger"
|
class="panel-btn danger"
|
||||||
@@ -717,27 +732,45 @@ function saveQuelle() {
|
|||||||
|
|
||||||
.step-dots {
|
.step-dots {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
gap: 4px;
|
gap: 5px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.step-dot {
|
/* Grobe Phasen als nummerierte Pillen (1–5) — Anzeige + anklickbar für Re-Run ab hier. */
|
||||||
width: 7px;
|
.step-pill {
|
||||||
height: 7px;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 17px;
|
||||||
|
height: 17px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--border-strong);
|
background: var(--border-strong);
|
||||||
|
color: var(--bg);
|
||||||
|
font-size: 0.62rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
border: 1.5px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.step-dot.done {
|
.step-pill.done {
|
||||||
background: var(--success-border);
|
background: var(--success-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.step-dot.active {
|
.step-pill.active {
|
||||||
background: var(--warning-border);
|
background: var(--warning-border);
|
||||||
animation: dot-pulse 1.2s ease-in-out infinite;
|
animation: dot-pulse 1.2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.step-pill.klickbar {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-pill.sel {
|
||||||
|
border-color: var(--text);
|
||||||
|
box-shadow: 0 0 0 1px var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes dot-pulse {
|
@keyframes dot-pulse {
|
||||||
0%, 100% { opacity: 1; }
|
0%, 100% { opacity: 1; }
|
||||||
50% { opacity: 0.35; }
|
50% { opacity: 0.35; }
|
||||||
|
|||||||
@@ -15,8 +15,11 @@ export function usePruefSlot(key) {
|
|||||||
phase: ref('idle'),
|
phase: ref('idle'),
|
||||||
aktuelleFrage: ref(''),
|
aktuelleFrage: ref(''),
|
||||||
letztesFeedback: ref(''),
|
letztesFeedback: ref(''),
|
||||||
pool: ref([]), // vorgenerierte, geprüfte, unbeantwortete Fragen
|
pool: ref([]), // vorformulierte, unbeantwortete Fragen
|
||||||
inflight: ref(0), // laufende Pool-Generierungen (instanzübergreifend koordiniert)
|
inflight: ref(0), // laufende Pool-Generierungen (instanzübergreifend koordiniert)
|
||||||
|
musterQuelle: ref([]), // unveränderliche Voll-Liste der Frage-Muster (leer = Fallback)
|
||||||
|
musterPool: ref([]), // Arbeitskopie: gezogene Saat ohne Zurücklegen; leer → Reset
|
||||||
|
musterGeladen: ref(false), // Muster-Sidecar schon abgerufen?
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return store.get(key)
|
return store.get(key)
|
||||||
|
|||||||
21
templates/Prompt/Baustein-Frage-Variante.md
Normal file
21
templates/Prompt/Baustein-Frage-Variante.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
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).
|
||||||
|
|
||||||
|
MUSTER (Vorlage — gleiche Sache, neuer Wortlaut):
|
||||||
|
{muster}
|
||||||
|
|
||||||
|
BAUSTEIN AUS DEM GUIDE (fachliche Referenz — die Frage darf ihr nie widersprechen):
|
||||||
|
{section_block}
|
||||||
|
|
||||||
|
KOMPAKTE FASSUNG (Merksätze, falls vorhanden):
|
||||||
|
{kompakt_block}
|
||||||
|
|
||||||
|
HARTE REGELN:
|
||||||
|
- GENAU EINE Frage. Ein einziges Fragezeichen. Eine einzige Sache.
|
||||||
|
- Maximal 1–2 Sätze, keine Vorrede, kein Szenario-Aufbau.
|
||||||
|
- Verboten: zwei Fragen mit "und"/"sowie" verketten, "nenne drei …", Aufzähl-Forderungen.
|
||||||
|
- Gleiche Kernfrage wie das Muster, nur anders formuliert — KEINE neue Sache, KEINE wörtliche Kopie.
|
||||||
|
- Frag nur, was aus dem Material beantwortbar ist. Keine erfundenen Zusatzannahmen.
|
||||||
|
- Sprich den Lerner direkt an, klares Deutsch, keine Floskeln.
|
||||||
|
|
||||||
|
Gib NUR dieses JSON aus (kein weiterer Text):
|
||||||
|
{{"frage": "genau eine kurze Frage"}}
|
||||||
21
templates/Prompt/Frage-Muster-Kritik.md
Normal file
21
templates/Prompt/Frage-Muster-Kritik.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
Du bist Qualitäts-Prüfer für den Fragen-Vorrat einer Lern-Prüfung zum Thema "{topic}", Baustein "{baustein}". Ein anderer Agent hat Frage-Muster erstellt. Prüfe sie und gib eine **bereinigte** Endfassung zurück.
|
||||||
|
|
||||||
|
ZU PRÜFENDE MUSTER (Nummer. [typ] (subbaustein) Frage):
|
||||||
|
{tabelle}
|
||||||
|
|
||||||
|
PRÜFE UND BEREINIGE GEGEN DIESE KRITERIEN:
|
||||||
|
- **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.
|
||||||
|
- **Distinkt:** keine zwei Muster, die sinngemäß dieselbe Frage stellen (auch nicht über Typen hinweg). Dubletten → eines behalten, das andere entfernen.
|
||||||
|
- **Typ-Treue:** die Frage passt zum angegebenen Typ; sonst Typ anpassen oder Muster entfernen.
|
||||||
|
- `subbaustein` und `typ` der Einträge unverändert übernehmen (nur `frage` darf sich ändern); nur entfernen, nicht neu erfinden.
|
||||||
|
|
||||||
|
Behalte gute Muster unverändert. Ändere nur, was wirklich gegen die Kriterien verstößt.
|
||||||
|
|
||||||
|
Schreibe die bereinigte Endfassung als JSON in die Datei {out_path} (nutze dein Schreib-Werkzeug), GENAU in diesem Format:
|
||||||
|
{{"muster": [
|
||||||
|
{{"subbaustein": "<exakter Titel>", "typ": "<Schlüssel>", "frage": "<eine konkrete Frage>"}}
|
||||||
|
]}}
|
||||||
|
|
||||||
|
Gib sonst keinen Text aus.
|
||||||
|
{extra}
|
||||||
28
templates/Prompt/Frage-Muster-Recherche.md
Normal file
28
templates/Prompt/Frage-Muster-Recherche.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
Du baust den Fragen-Vorrat für eine Lern-Prüfung zum Thema "{topic}", Baustein "{baustein}". 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.
|
||||||
|
|
||||||
|
RELEVANTE SUBBAUSTEINE (nur zu diesen Fragen bauen):
|
||||||
|
{subbausteine}
|
||||||
|
|
||||||
|
FRAGETYPEN (Schlüssel : Bedeutung):
|
||||||
|
{typen}
|
||||||
|
|
||||||
|
AUFGABE:
|
||||||
|
- Erzeuge für **jeden** Subbaustein zu **jedem** passenden Fragetyp **ein** Muster (volles Kreuzprodukt).
|
||||||
|
- 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, 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.
|
||||||
|
- Innerhalb eines Subbausteins sollen die Muster verschiedener Typen **wirklich verschiedene** Aspekte treffen — keine Umformulierungen derselben Frage.
|
||||||
|
- Der `typ` ist exakt einer der Schlüssel oben. `subbaustein` ist exakt einer der Titel oben.
|
||||||
|
|
||||||
|
HARTE STIL-REGELN JE FRAGE:
|
||||||
|
- Genau EINE Frage, ein Fragezeichen, eine Sache. Kein "und"/"sowie", kein "nenne drei …".
|
||||||
|
- Maximal 1–2 Sätze, keine Vorrede, kein Szenario-Aufbau.
|
||||||
|
- Direkte Ansprache, klares Deutsch.
|
||||||
|
{extra}
|
||||||
|
|
||||||
|
Schreibe das Ergebnis als JSON in die Datei {out_path} (nutze dein Schreib-Werkzeug), GENAU in diesem Format:
|
||||||
|
{{"muster": [
|
||||||
|
{{"subbaustein": "<exakter Titel>", "typ": "<Schlüssel>", "frage": "<eine konkrete Frage>"}}
|
||||||
|
]}}
|
||||||
|
|
||||||
|
Gib sonst keinen Text aus.
|
||||||
Reference in New Issue
Block a user