update
This commit is contained in:
@@ -15,8 +15,11 @@ TIMEOUTS = {
|
|||||||
"recherche": (1800, 0), # fix 30 min
|
"recherche": (1800, 0), # fix 30 min
|
||||||
"auswahl": (600, 10),
|
"auswahl": (600, 10),
|
||||||
"auswahl_check": (300, 2),
|
"auswahl_check": (300, 2),
|
||||||
|
"guide_auswahl": (300, 5), # pro Baustein im Inventar
|
||||||
|
"guide_check": (300, 2), # Auswahl-/Gliederungs-Prüfung (nur Titellisten)
|
||||||
"plan": (300, 5),
|
"plan": (300, 5),
|
||||||
"writer": (600, 120), # pro Section im Chunk
|
"writer": (600, 120), # pro Section im Chunk
|
||||||
|
"lese_check": (300, 10), # pro Section im Paket
|
||||||
"onepager_recherche": (900, 0),
|
"onepager_recherche": (900, 0),
|
||||||
"onepager_bauen": (300, 0),
|
"onepager_bauen": (300, 0),
|
||||||
"onepager_verify": (300, 0),
|
"onepager_verify": (300, 0),
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ CREATE TABLE IF NOT EXISTS guides (
|
|||||||
instructions TEXT NOT NULL DEFAULT '',
|
instructions TEXT NOT NULL DEFAULT '',
|
||||||
status TEXT NOT NULL DEFAULT 'queued',
|
status TEXT NOT NULL DEFAULT 'queued',
|
||||||
progress TEXT,
|
progress TEXT,
|
||||||
|
step INTEGER,
|
||||||
error_msg TEXT,
|
error_msg TEXT,
|
||||||
created_at TEXT NOT NULL,
|
created_at TEXT NOT NULL,
|
||||||
updated_at TEXT NOT NULL
|
updated_at TEXT NOT NULL
|
||||||
@@ -47,6 +48,10 @@ async def init_db():
|
|||||||
await db.execute(CREATE_GUIDES)
|
await db.execute(CREATE_GUIDES)
|
||||||
await db.execute(CREATE_PROGRESS)
|
await db.execute(CREATE_PROGRESS)
|
||||||
await db.execute(CREATE_TOPICS)
|
await db.execute(CREATE_TOPICS)
|
||||||
|
try: # Migration für Bestands-DBs ohne step-Spalte
|
||||||
|
await db.execute("ALTER TABLE guides ADD COLUMN step INTEGER")
|
||||||
|
except aiosqlite.OperationalError:
|
||||||
|
pass
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"UPDATE guides SET status = 'error', progress = NULL, error_msg = 'Server-Neustart' "
|
"UPDATE guides SET status = 'error', progress = NULL, error_msg = 'Server-Neustart' "
|
||||||
"WHERE status IN ('queued', 'generating')"
|
"WHERE status IN ('queued', 'generating')"
|
||||||
|
|||||||
@@ -501,9 +501,12 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
|
|||||||
_bausteine_cancelled.discard(topic)
|
_bausteine_cancelled.discard(topic)
|
||||||
|
|
||||||
|
|
||||||
# --- Guide-Generierung: Bausteine → (Plan) → Writer → JSON ---
|
# --- Guide-Generierung: 6 Schritte mit Prüfung nach jeder Phase (OnePager hat einen eigenen Weg) ---
|
||||||
|
# Prüf-Agenten notieren nur Probleme; das Anpassen übernimmt der jeweilige Erzeuger-Typ.
|
||||||
|
# Schritt-Dateien bleiben liegen → Abbruch erhält Fortschritt, ▶ setzt am offenen Schritt fort.
|
||||||
|
|
||||||
|
GUIDE_STEPS = ("Auswahl", "Auswahl-Prüfung", "Gliederung", "Gliederungs-Prüfung", "Schreiben", "Lese-Prüfung")
|
||||||
|
|
||||||
# Parallele Writer pro Format (OnePager hat einen eigenen Weg).
|
|
||||||
# Writer skalieren mit der Section-Zahl: 1 Writer je ~30 Sections (gedeckelt).
|
# Writer skalieren mit der Section-Zahl: 1 Writer je ~30 Sections (gedeckelt).
|
||||||
# Kleine Pakete vermeiden Lazy-Output bei langen Listen und begrenzen den Schaden
|
# Kleine Pakete vermeiden Lazy-Output bei langen Listen und begrenzen den Schaden
|
||||||
# eines fehlgeschlagenen Writers.
|
# eines fehlgeschlagenen Writers.
|
||||||
@@ -511,6 +514,80 @@ WRITER_SECTIONS = 30
|
|||||||
WRITER_MAX = 20
|
WRITER_MAX = 20
|
||||||
|
|
||||||
|
|
||||||
|
def _guide_files(content_path: Path) -> dict:
|
||||||
|
d, stem = content_path.parent, content_path.stem
|
||||||
|
return {
|
||||||
|
"auswahl": d / f"{stem}.auswahl.json",
|
||||||
|
"auswahl_check": d / f"{stem}.auswahl-check.json",
|
||||||
|
"gliederung": d / f"{stem}.gliederung.json",
|
||||||
|
"gliederung_check": d / f"{stem}.gliederung-check.json",
|
||||||
|
# chunk-/lese-check-/fix-Dateien sind dynamisch: {stem}.chunk-i.md usw.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def guide_slot_dateien(content_path: Path) -> list[Path]:
|
||||||
|
"""Alle Schritt-Dateien eines Guides (für den Frischstart)."""
|
||||||
|
return [p for p in content_path.parent.glob(f"{content_path.stem}.*") if p != content_path]
|
||||||
|
|
||||||
|
|
||||||
|
async def _set_step(guide_id: str, step: int, progress: str) -> None:
|
||||||
|
now = datetime.now(timezone.utc).isoformat()
|
||||||
|
await update_guide(guide_id, step=step, progress=progress, updated_at=now)
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_auswahl(data, entries: dict[int, str], k_min: int, k_max: int) -> list[int] | None:
|
||||||
|
"""{"bausteine": [Titel]} → Nummern; None bei Schema-Verstoß/Drift/falschem Umfang."""
|
||||||
|
if not isinstance(data, dict) or not isinstance(data.get("bausteine"), list):
|
||||||
|
return None
|
||||||
|
idx = _titel_index(entries)
|
||||||
|
nums: list[int] = []
|
||||||
|
seen: set[int] = set()
|
||||||
|
total = unknown = 0
|
||||||
|
for t in data["bausteine"]:
|
||||||
|
total += 1
|
||||||
|
num = _titel_aufloesen(idx, t) if isinstance(t, str) else None
|
||||||
|
if num is None:
|
||||||
|
unknown += 1
|
||||||
|
elif num not in seen:
|
||||||
|
seen.add(num)
|
||||||
|
nums.append(num)
|
||||||
|
if total == 0 or (total - unknown) / total < 0.85:
|
||||||
|
return None
|
||||||
|
if len(nums) < 0.9 * k_min or len(nums) > 1.1 * k_max:
|
||||||
|
return None
|
||||||
|
return nums
|
||||||
|
|
||||||
|
|
||||||
|
def _probleme_schema(data):
|
||||||
|
"""{"ok": true} → [] · {"probleme": [str]} → Liste · sonst None."""
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return None
|
||||||
|
if data.get("ok") is True:
|
||||||
|
return []
|
||||||
|
p = data.get("probleme")
|
||||||
|
if not isinstance(p, list) or not p:
|
||||||
|
return None
|
||||||
|
out = [str(x).strip() for x in p if str(x).strip()]
|
||||||
|
return out or None
|
||||||
|
|
||||||
|
|
||||||
|
def _lese_probleme_schema(data):
|
||||||
|
"""{"ok": true} → [] · {"probleme": [{"section", "problem"}]} → Liste · sonst None."""
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return None
|
||||||
|
if data.get("ok") is True:
|
||||||
|
return []
|
||||||
|
p = data.get("probleme")
|
||||||
|
if not isinstance(p, list) or not p:
|
||||||
|
return None
|
||||||
|
out = []
|
||||||
|
for x in p:
|
||||||
|
if not isinstance(x, dict) or not isinstance(x.get("section"), str) or not isinstance(x.get("problem"), str):
|
||||||
|
return None
|
||||||
|
out.append({"section": x["section"].strip(), "problem": x["problem"].strip()})
|
||||||
|
return out or None
|
||||||
|
|
||||||
|
|
||||||
def _resolve_gliederung(data, entries: dict[int, str], soll_min: int, soll_max: int) -> list[dict] | None:
|
def _resolve_gliederung(data, entries: dict[int, str], soll_min: int, soll_max: int) -> list[dict] | None:
|
||||||
"""{"kapitel": [{"titel", "bausteine": [Titel]}]} → [{"title", "nums"}].
|
"""{"kapitel": [{"titel", "bausteine": [Titel]}]} → [{"title", "nums"}].
|
||||||
|
|
||||||
@@ -605,40 +682,38 @@ def _parse_fragment(text: str) -> list[dict]:
|
|||||||
|
|
||||||
async def _generate_onepager(
|
async def _generate_onepager(
|
||||||
guide_id: str, topic: str, instructions: str, provider: str,
|
guide_id: str, topic: str, instructions: str, provider: str,
|
||||||
project: Path | None, content_path: Path, fragment_paths: list[Path],
|
project: Path | None, content_path: Path,
|
||||||
) -> list[dict] | None:
|
) -> list[dict] | None:
|
||||||
def is_cancelled() -> bool:
|
def is_cancelled() -> bool:
|
||||||
return guide_id in _cancelled
|
return guide_id in _cancelled
|
||||||
|
|
||||||
PFLICHT_KARTEN = ("was ist", "welches problem", "wann nehmen", "einordnung", "so sieht", "fakten", "erste schritte")
|
# 3×3-Raster: 7 Karten mit festen Schlüsseln (Reihenfolge = Lesereihenfolge mobil)
|
||||||
|
KARTEN_KEYS = ("info", "eigenschaften", "beispiel", "zusammenhaenge", "voraussetzungen", "modern", "veraltet")
|
||||||
|
|
||||||
def karten_schema(data):
|
def karten_schema(data):
|
||||||
|
"""{"karten": {key: {titel, md}}} → Liste · sonst None."""
|
||||||
if not isinstance(data, dict):
|
if not isinstance(data, dict):
|
||||||
return None
|
return None
|
||||||
if data.get("ok") is True:
|
|
||||||
return "ok"
|
|
||||||
karten = data.get("karten")
|
karten = data.get("karten")
|
||||||
if not isinstance(karten, list) or not karten:
|
if not isinstance(karten, dict):
|
||||||
return None
|
return None
|
||||||
out = []
|
out = []
|
||||||
for k in karten:
|
for key in KARTEN_KEYS:
|
||||||
if not isinstance(k, dict) or not isinstance(k.get("titel"), str) or not isinstance(k.get("merksatz"), str):
|
k = karten.get(key)
|
||||||
|
if not isinstance(k, dict) or not isinstance(k.get("titel"), str) or not isinstance(k.get("md"), str):
|
||||||
return None
|
return None
|
||||||
titel, merksatz = k["titel"].strip(), k["merksatz"].strip()
|
titel, md = k["titel"].strip(), k["md"].strip()
|
||||||
if len(merksatz) < 5: # abgebrochene/leere Karten ("Per") sind ungültig
|
if not titel or len(md) < 5: # abgebrochene/leere Karten sind ungültig
|
||||||
return None
|
|
||||||
out.append({"titel": titel, "merksatz": merksatz})
|
|
||||||
vorhanden = [k["titel"].lower() for k in out]
|
|
||||||
for pflicht in PFLICHT_KARTEN:
|
|
||||||
if not any(t.startswith(pflicht) for t in vorhanden):
|
|
||||||
return None
|
return None
|
||||||
|
out.append({"key": key, "titel": titel, "md": md})
|
||||||
return out
|
return out
|
||||||
|
|
||||||
# Schritt 1: Recherche — eigene Faktenbasis, unabhängig von den Bausteinen
|
d, stem = content_path.parent, content_path.stem
|
||||||
await _set_progress(guide_id, "Recherchiere…")
|
recherche_path = d / f"{stem}.recherche.md"
|
||||||
recherche_path = content_path.parent / f"{content_path.stem}.recherche.md"
|
recherche_check_path = d / f"{stem}.recherche-check.json"
|
||||||
fragment_paths.append(recherche_path)
|
karten_path = d / f"{stem}.karten.json"
|
||||||
recherche_path.unlink(missing_ok=True)
|
check_path = d / f"{stem}.onepager-check.json"
|
||||||
|
|
||||||
# Projekte bekommen eigene Recherche-Dimensionen — Produkt-Fragen
|
# Projekte bekommen eigene Recherche-Dimensionen — Produkt-Fragen
|
||||||
# (Version, Lizenz, Alternativen) laufen dort ins Leere.
|
# (Version, Lizenz, Alternativen) laufen dort ins Leere.
|
||||||
if project:
|
if project:
|
||||||
@@ -647,62 +722,135 @@ async def _generate_onepager(
|
|||||||
else:
|
else:
|
||||||
source = _prompt("OnePager-Quelle-Thema", topic=topic)
|
source = _prompt("OnePager-Quelle-Thema", topic=topic)
|
||||||
recherche_template = "OnePager-Recherche"
|
recherche_template = "OnePager-Recherche"
|
||||||
slots = [{
|
|
||||||
"key": f"{guide_id}-recherche",
|
|
||||||
"prompt": _prompt(recherche_template, topic=topic, source=source, out_path=recherche_path, extra=_extra(instructions)),
|
|
||||||
"role": "quick", "capabilities": "files" if project else "full",
|
|
||||||
"payload": (lambda result: recherche_path.read_text(encoding="utf-8") if recherche_path.exists() else None),
|
|
||||||
}]
|
|
||||||
res = await _race(topic, "OnePager-Recherche", slots, 1, _timeout("onepager_recherche"), provider, cancelled=is_cancelled)
|
|
||||||
if is_cancelled():
|
|
||||||
return None
|
|
||||||
if res is None:
|
|
||||||
await _fail(guide_id, "OnePager-Recherche fehlgeschlagen")
|
|
||||||
return None
|
|
||||||
recherche = res[0]
|
|
||||||
|
|
||||||
# Schritt 2: Bauen — Karten nur aus der Faktenbasis (JSON)
|
def recherche_payload(result=None):
|
||||||
await _set_progress(guide_id, "Baue OnePager…")
|
if not recherche_path.exists():
|
||||||
karten_path = content_path.parent / f"{content_path.stem}.karten.json"
|
return None
|
||||||
fragment_paths.append(karten_path)
|
text = recherche_path.read_text(encoding="utf-8").strip()
|
||||||
karten_path.unlink(missing_ok=True)
|
return text or None
|
||||||
slots = [{
|
|
||||||
"key": f"{guide_id}-bauen",
|
|
||||||
"prompt": _prompt("OnePager-Bauen", topic=topic, recherche=recherche, out_path=karten_path, extra=_extra(instructions)),
|
|
||||||
"role": "fast", "capabilities": "files",
|
|
||||||
"payload": (lambda result: (k if isinstance(k := karten_schema(_json_datei(karten_path)), list) else None)),
|
|
||||||
}]
|
|
||||||
res = await _race(topic, "OnePager-Bauen", slots, 1, _timeout("onepager_bauen"), provider, cancelled=is_cancelled)
|
|
||||||
if is_cancelled():
|
|
||||||
return None
|
|
||||||
if res is None:
|
|
||||||
await _fail(guide_id, "OnePager-Bau fehlgeschlagen")
|
|
||||||
return None
|
|
||||||
karten = res[0]
|
|
||||||
|
|
||||||
# Schritt 3: Verifizieren — {"ok": true} oder vollständig korrigierte Liste (nicht fatal)
|
# Schritt 1: Recherche — vorhandene Datei wird übernommen (Resume)
|
||||||
await _set_progress(guide_id, "Verifiziere OnePager…")
|
recherche = recherche_payload()
|
||||||
check_path = content_path.parent / f"{content_path.stem}.onepager-check.json"
|
if recherche is None:
|
||||||
fragment_paths.append(check_path)
|
await _set_step(guide_id, 0, "Recherchiere…")
|
||||||
check_path.unlink(missing_ok=True)
|
slots = [{
|
||||||
karten_block = "\n".join(f"- {k['titel']} — {k['merksatz']}" for k in karten)
|
"key": f"{guide_id}-recherche",
|
||||||
slots = [{
|
"prompt": _prompt(recherche_template, topic=topic, source=source, out_path=recherche_path, extra=_extra(instructions)),
|
||||||
"key": f"{guide_id}-verify",
|
"role": "quick", "capabilities": "files" if project else "full",
|
||||||
"prompt": _prompt("OnePager-Verifikation", topic=topic, recherche=recherche, karten=karten_block, out_path=check_path),
|
"payload": recherche_payload,
|
||||||
"role": "fast", "capabilities": "files",
|
}]
|
||||||
"payload": (lambda result: karten_schema(_json_datei(check_path))),
|
res = await _race(topic, "OnePager-Recherche", slots, 1, _timeout("onepager_recherche"), provider, cancelled=is_cancelled)
|
||||||
}]
|
if is_cancelled():
|
||||||
res = await _race(topic, "OnePager-Verifikation", slots, 1, _timeout("onepager_verify"), provider, cancelled=is_cancelled)
|
return None
|
||||||
if is_cancelled():
|
if res is None:
|
||||||
return None
|
await _fail(guide_id, "OnePager-Recherche fehlgeschlagen")
|
||||||
if res is None:
|
return None
|
||||||
_log(topic, "OnePager-Verifikation fehlgeschlagen — ungeprüfte Version wird verwendet")
|
recherche = res[0]
|
||||||
elif isinstance(res[0], list):
|
|
||||||
_log(topic, "OnePager-Verifikation hat Korrekturen geliefert")
|
# Schritt 2: Recherche-Prüfung — notiert Probleme; Anpassung macht ein Recherche-Agent
|
||||||
|
if not recherche_check_path.exists():
|
||||||
|
await _set_step(guide_id, 1, "Prüfe Recherche…")
|
||||||
|
slots = [{
|
||||||
|
"key": f"{guide_id}-recherche-check",
|
||||||
|
"prompt": _prompt("OnePager-Recherche-Check", topic=topic, recherche=recherche, out_path=recherche_check_path),
|
||||||
|
"role": "fast", "capabilities": "files",
|
||||||
|
"payload": (lambda result: _probleme_schema(_json_datei(recherche_check_path))),
|
||||||
|
}]
|
||||||
|
res = await _race(topic, "Recherche-Prüfung", slots, 1, _timeout("onepager_verify"), provider, cancelled=is_cancelled)
|
||||||
|
if is_cancelled():
|
||||||
|
return None
|
||||||
|
if res is None:
|
||||||
|
await _fail(guide_id, "Recherche-Prüfung fehlgeschlagen")
|
||||||
|
return None
|
||||||
|
probleme = res[0]
|
||||||
|
if probleme:
|
||||||
|
_log(topic, f"Recherche-Prüfung: {len(probleme)} Problem(e) notiert")
|
||||||
|
await _set_step(guide_id, 1, "Passe Recherche an…")
|
||||||
|
slots = [{
|
||||||
|
"key": f"{guide_id}-recherche-fix",
|
||||||
|
"prompt": _prompt(
|
||||||
|
"OnePager-Recherche-Fix",
|
||||||
|
topic=topic, source=source, recherche=recherche,
|
||||||
|
probleme="\n".join(f"- {p}" for p in probleme),
|
||||||
|
out_path=recherche_path, extra=_extra(instructions),
|
||||||
|
),
|
||||||
|
"role": "quick", "capabilities": "files" if project else "full",
|
||||||
|
"payload": recherche_payload,
|
||||||
|
}]
|
||||||
|
res = await _race(topic, "Recherche-Fix", slots, 1, _timeout("onepager_recherche"), provider, cancelled=is_cancelled)
|
||||||
|
if is_cancelled():
|
||||||
|
return None
|
||||||
|
if res is None:
|
||||||
|
_log(topic, "Recherche-Fix ungültig — ursprüngliche Recherche bleibt")
|
||||||
|
else:
|
||||||
|
recherche = res[0]
|
||||||
|
|
||||||
|
# Schritt 3: Bauen — Karten nur aus der Faktenbasis (Resume: gültige Datei wird übernommen)
|
||||||
|
karten = karten_schema(_json_datei(karten_path))
|
||||||
|
if karten is None:
|
||||||
|
await _set_step(guide_id, 2, "Baue OnePager…")
|
||||||
|
karten_path.unlink(missing_ok=True)
|
||||||
|
slots = [{
|
||||||
|
"key": f"{guide_id}-bauen",
|
||||||
|
"prompt": _prompt("OnePager-Bauen", topic=topic, recherche=recherche, out_path=karten_path, extra=_extra(instructions)),
|
||||||
|
"role": "fast", "capabilities": "files",
|
||||||
|
"payload": (lambda result: karten_schema(_json_datei(karten_path))),
|
||||||
|
}]
|
||||||
|
res = await _race(topic, "OnePager-Bauen", slots, 1, _timeout("onepager_bauen"), provider, cancelled=is_cancelled)
|
||||||
|
if is_cancelled():
|
||||||
|
return None
|
||||||
|
if res is None:
|
||||||
|
await _fail(guide_id, "OnePager-Bau fehlgeschlagen")
|
||||||
|
return None
|
||||||
karten = res[0]
|
karten = res[0]
|
||||||
|
|
||||||
|
def karten_block() -> str:
|
||||||
|
return "\n\n".join(f"### {k['titel']} [{k['key']}]\n{k['md']}" for k in karten)
|
||||||
|
|
||||||
|
# Schritt 4: Prüfung — notiert Probleme; Anpassung macht ein Bauen-Agent
|
||||||
|
if not check_path.exists():
|
||||||
|
await _set_step(guide_id, 3, "Prüfe OnePager…")
|
||||||
|
slots = [{
|
||||||
|
"key": f"{guide_id}-verify",
|
||||||
|
"prompt": _prompt("OnePager-Verifikation", topic=topic, recherche=recherche, karten=karten_block(), out_path=check_path),
|
||||||
|
"role": "fast", "capabilities": "files",
|
||||||
|
"payload": (lambda result: _probleme_schema(_json_datei(check_path))),
|
||||||
|
}]
|
||||||
|
res = await _race(topic, "OnePager-Prüfung", slots, 1, _timeout("onepager_verify"), provider, cancelled=is_cancelled)
|
||||||
|
if is_cancelled():
|
||||||
|
return None
|
||||||
|
if res is None:
|
||||||
|
await _fail(guide_id, "OnePager-Prüfung fehlgeschlagen")
|
||||||
|
return None
|
||||||
|
probleme = res[0]
|
||||||
|
if probleme:
|
||||||
|
_log(topic, f"OnePager-Prüfung: {len(probleme)} Problem(e) notiert")
|
||||||
|
await _set_step(guide_id, 3, "Passe OnePager an…")
|
||||||
|
slots = [{
|
||||||
|
"key": f"{guide_id}-karten-fix",
|
||||||
|
"prompt": _prompt(
|
||||||
|
"OnePager-Fix",
|
||||||
|
topic=topic, recherche=recherche, karten=karten_block(),
|
||||||
|
probleme="\n".join(f"- {p}" for p in probleme),
|
||||||
|
out_path=karten_path, extra=_extra(instructions),
|
||||||
|
),
|
||||||
|
"role": "fast", "capabilities": "files",
|
||||||
|
"payload": (lambda result: karten_schema(_json_datei(karten_path))),
|
||||||
|
}]
|
||||||
|
res = await _race(topic, "OnePager-Fix", slots, 1, _timeout("onepager_bauen"), provider, cancelled=is_cancelled)
|
||||||
|
if is_cancelled():
|
||||||
|
return None
|
||||||
|
if res is None:
|
||||||
|
_log(topic, "OnePager-Fix ungültig — ursprüngliche Karten bleiben")
|
||||||
|
karten_path.write_text(
|
||||||
|
json.dumps({"karten": {k["key"]: {"titel": k["titel"], "md": k["md"]} for k in karten}}, ensure_ascii=False),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
karten = res[0]
|
||||||
|
|
||||||
sections = [
|
sections = [
|
||||||
{"num": i, "title": k["titel"], "md": k["merksatz"]}
|
{"num": i, "title": k["titel"], "md": k["md"], "key": k["key"]}
|
||||||
for i, k in enumerate(karten, 1)
|
for i, k in enumerate(karten, 1)
|
||||||
]
|
]
|
||||||
return [{"title": topic, "sections": sections}]
|
return [{"title": topic, "sections": sections}]
|
||||||
@@ -711,12 +859,13 @@ async def _generate_onepager(
|
|||||||
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,
|
||||||
content_path: Path, fragment_paths: list[Path],
|
content_path: Path,
|
||||||
) -> list[dict] | None:
|
) -> list[dict] | None:
|
||||||
def is_cancelled() -> bool:
|
def is_cancelled() -> bool:
|
||||||
return guide_id in _cancelled
|
return guide_id in _cancelled
|
||||||
|
|
||||||
spec = (TEMPLATES_DIR / "Format" / "Section.md").read_text(encoding="utf-8")
|
spec = (TEMPLATES_DIR / "Format" / "Section.md").read_text(encoding="utf-8")
|
||||||
|
files = _guide_files(content_path)
|
||||||
bausteine_liste = "\n".join(f"- {t}" for t in entries.values())
|
bausteine_liste = "\n".join(f"- {t}" for t in entries.values())
|
||||||
n = len(entries)
|
n = len(entries)
|
||||||
anteil_min, anteil_max, minimum, zweck = FORMAT_ANTEIL[format_name]
|
anteil_min, anteil_max, minimum, zweck = FORMAT_ANTEIL[format_name]
|
||||||
@@ -727,76 +876,289 @@ async def _generate_sections(
|
|||||||
"Wähle, was diesem Zweck dient — lass weg, was dafür nicht nötig ist."
|
"Wähle, was diesem Zweck dient — lass weg, was dafür nicht nötig ist."
|
||||||
)
|
)
|
||||||
|
|
||||||
await _set_progress(guide_id, "Wähle Bausteine & plane Gliederung…")
|
# Schritt 1: Auswahl — vorhandene gültige Datei wird übernommen (Resume)
|
||||||
plan_path = content_path.parent / f"{content_path.stem}.gliederung.json"
|
auswahl = _resolve_auswahl(_json_datei(files["auswahl"]), entries, k_min, k_max)
|
||||||
fragment_paths.append(plan_path)
|
if auswahl is None:
|
||||||
plan_path.unlink(missing_ok=True)
|
await _set_step(guide_id, 0, "Wähle Bausteine…")
|
||||||
slots = [{
|
files["auswahl"].unlink(missing_ok=True)
|
||||||
"key": f"{guide_id}-plan",
|
slots = [{
|
||||||
"prompt": _prompt(
|
"key": f"{guide_id}-auswahl",
|
||||||
"Guide-Plan",
|
"prompt": _prompt(
|
||||||
topic=topic, format_name=format_name, bausteine=bausteine_liste,
|
"Guide-Auswahl",
|
||||||
auswahl_auftrag=auswahl_auftrag, out_path=plan_path, extra=_extra(instructions),
|
topic=topic, format_name=format_name, bausteine=bausteine_liste,
|
||||||
),
|
auswahl_auftrag=auswahl_auftrag, out_path=files["auswahl"], extra=_extra(instructions),
|
||||||
"role": "guide", "capabilities": "files",
|
),
|
||||||
"payload": (lambda result: _resolve_gliederung(_json_datei(plan_path), entries, k_min, k_max)),
|
"role": "guide", "capabilities": "files",
|
||||||
}]
|
"payload": (lambda result: _resolve_auswahl(_json_datei(files["auswahl"]), entries, k_min, k_max)),
|
||||||
res = await _race(topic, "Gliederung", slots, 1, _timeout("plan", n), provider, cancelled=is_cancelled)
|
}]
|
||||||
if is_cancelled():
|
res = await _race(topic, "Guide-Auswahl", slots, 1, _timeout("guide_auswahl", n), provider, cancelled=is_cancelled)
|
||||||
return None
|
if is_cancelled():
|
||||||
if res is None:
|
return None
|
||||||
await _fail(guide_id, "Gliederung fehlgeschlagen")
|
if res is None:
|
||||||
return None
|
await _fail(guide_id, "Auswahl fehlgeschlagen")
|
||||||
plan = res[0]
|
return None
|
||||||
|
auswahl = res[0]
|
||||||
|
|
||||||
|
def auswahl_titel() -> str:
|
||||||
|
return "\n".join(f"- {_titel(entries[num])}" for num in auswahl)
|
||||||
|
|
||||||
|
def auswahl_json() -> str:
|
||||||
|
return json.dumps({"bausteine": [_titel(entries[num]) for num in auswahl]}, ensure_ascii=False)
|
||||||
|
|
||||||
|
# Schritt 2: Auswahl-Prüfung — notiert Probleme; Anpassung macht ein Auswahl-Agent
|
||||||
|
if not files["auswahl_check"].exists():
|
||||||
|
await _set_step(guide_id, 1, "Prüfe Auswahl…")
|
||||||
|
slots = [{
|
||||||
|
"key": f"{guide_id}-auswahl-check",
|
||||||
|
"prompt": _prompt(
|
||||||
|
"Guide-Auswahl-Check",
|
||||||
|
topic=topic, format_name=format_name, auswahl_auftrag=auswahl_auftrag,
|
||||||
|
bausteine=bausteine_liste, auswahl=auswahl_titel(),
|
||||||
|
out_path=files["auswahl_check"], extra=_extra(instructions),
|
||||||
|
),
|
||||||
|
"role": "fast", "capabilities": "files",
|
||||||
|
"payload": (lambda result: _probleme_schema(_json_datei(files["auswahl_check"]))),
|
||||||
|
}]
|
||||||
|
res = await _race(topic, "Auswahl-Prüfung", slots, 1, _timeout("guide_check", len(auswahl)), provider, cancelled=is_cancelled)
|
||||||
|
if is_cancelled():
|
||||||
|
return None
|
||||||
|
if res is None:
|
||||||
|
await _fail(guide_id, "Auswahl-Prüfung fehlgeschlagen")
|
||||||
|
return None
|
||||||
|
probleme = res[0]
|
||||||
|
if probleme:
|
||||||
|
_log(topic, f"Auswahl-Prüfung: {len(probleme)} Problem(e) notiert")
|
||||||
|
await _set_step(guide_id, 1, "Passe Auswahl an…")
|
||||||
|
slots = [{
|
||||||
|
"key": f"{guide_id}-auswahl-fix",
|
||||||
|
"prompt": _prompt(
|
||||||
|
"Guide-Auswahl-Fix",
|
||||||
|
topic=topic, format_name=format_name, auswahl_auftrag=auswahl_auftrag,
|
||||||
|
bausteine=bausteine_liste, auswahl=auswahl_titel(),
|
||||||
|
probleme="\n".join(f"- {p}" for p in probleme),
|
||||||
|
out_path=files["auswahl"], extra=_extra(instructions),
|
||||||
|
),
|
||||||
|
"role": "guide", "capabilities": "files",
|
||||||
|
"payload": (lambda result: _resolve_auswahl(_json_datei(files["auswahl"]), entries, k_min, k_max)),
|
||||||
|
}]
|
||||||
|
res = await _race(topic, "Auswahl-Fix", slots, 1, _timeout("guide_auswahl", n), provider, cancelled=is_cancelled)
|
||||||
|
if is_cancelled():
|
||||||
|
return None
|
||||||
|
if res is None:
|
||||||
|
_log(topic, "Auswahl-Fix ungültig — ursprüngliche Auswahl bleibt")
|
||||||
|
files["auswahl"].write_text(auswahl_json(), encoding="utf-8")
|
||||||
|
else:
|
||||||
|
auswahl = res[0]
|
||||||
|
|
||||||
|
sel_entries = {num: entries[num] for num in auswahl}
|
||||||
|
soll = len(sel_entries)
|
||||||
|
sel_liste = "\n".join(f"- {t}" for t in sel_entries.values())
|
||||||
|
|
||||||
|
# Schritt 3: Gliederung der festen Auswahl
|
||||||
|
plan = _resolve_gliederung(_json_datei(files["gliederung"]), sel_entries, soll, soll)
|
||||||
|
if plan is None:
|
||||||
|
await _set_step(guide_id, 2, "Plane Gliederung…")
|
||||||
|
files["gliederung"].unlink(missing_ok=True)
|
||||||
|
slots = [{
|
||||||
|
"key": f"{guide_id}-gliederung",
|
||||||
|
"prompt": _prompt(
|
||||||
|
"Guide-Gliederung",
|
||||||
|
topic=topic, format_name=format_name, bausteine=sel_liste,
|
||||||
|
out_path=files["gliederung"], extra=_extra(instructions),
|
||||||
|
),
|
||||||
|
"role": "guide", "capabilities": "files",
|
||||||
|
"payload": (lambda result: _resolve_gliederung(_json_datei(files["gliederung"]), sel_entries, soll, soll)),
|
||||||
|
}]
|
||||||
|
res = await _race(topic, "Gliederung", slots, 1, _timeout("plan", soll), provider, cancelled=is_cancelled)
|
||||||
|
if is_cancelled():
|
||||||
|
return None
|
||||||
|
if res is None:
|
||||||
|
await _fail(guide_id, "Gliederung fehlgeschlagen")
|
||||||
|
return None
|
||||||
|
plan = res[0]
|
||||||
|
|
||||||
|
def gliederung_text() -> str:
|
||||||
|
return "\n".join(_zuteilung_text([ch], {num: _titel(entries[num]) for num in ch["nums"]}) for ch in plan)
|
||||||
|
|
||||||
|
def gliederung_json() -> str:
|
||||||
|
return json.dumps(
|
||||||
|
{"kapitel": [{"titel": ch["title"], "bausteine": [_titel(entries[num]) for num in ch["nums"]]} for ch in plan]},
|
||||||
|
ensure_ascii=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Schritt 4: Gliederungs-Prüfung
|
||||||
|
if not files["gliederung_check"].exists():
|
||||||
|
await _set_step(guide_id, 3, "Prüfe Gliederung…")
|
||||||
|
slots = [{
|
||||||
|
"key": f"{guide_id}-gliederung-check",
|
||||||
|
"prompt": _prompt(
|
||||||
|
"Guide-Gliederung-Check",
|
||||||
|
topic=topic, format_name=format_name, zweck=zweck,
|
||||||
|
auswahl=auswahl_titel(), gliederung=gliederung_text(),
|
||||||
|
out_path=files["gliederung_check"], extra=_extra(instructions),
|
||||||
|
),
|
||||||
|
"role": "fast", "capabilities": "files",
|
||||||
|
"payload": (lambda result: _probleme_schema(_json_datei(files["gliederung_check"]))),
|
||||||
|
}]
|
||||||
|
res = await _race(topic, "Gliederungs-Prüfung", slots, 1, _timeout("guide_check", soll), provider, cancelled=is_cancelled)
|
||||||
|
if is_cancelled():
|
||||||
|
return None
|
||||||
|
if res is None:
|
||||||
|
await _fail(guide_id, "Gliederungs-Prüfung fehlgeschlagen")
|
||||||
|
return None
|
||||||
|
probleme = res[0]
|
||||||
|
if probleme:
|
||||||
|
_log(topic, f"Gliederungs-Prüfung: {len(probleme)} Problem(e) notiert")
|
||||||
|
await _set_step(guide_id, 3, "Passe Gliederung an…")
|
||||||
|
slots = [{
|
||||||
|
"key": f"{guide_id}-gliederung-fix",
|
||||||
|
"prompt": _prompt(
|
||||||
|
"Guide-Gliederung-Fix",
|
||||||
|
topic=topic, format_name=format_name,
|
||||||
|
auswahl=auswahl_titel(), gliederung=gliederung_text(),
|
||||||
|
probleme="\n".join(f"- {p}" for p in probleme),
|
||||||
|
out_path=files["gliederung"], extra=_extra(instructions),
|
||||||
|
),
|
||||||
|
"role": "guide", "capabilities": "files",
|
||||||
|
"payload": (lambda result: _resolve_gliederung(_json_datei(files["gliederung"]), sel_entries, soll, soll)),
|
||||||
|
}]
|
||||||
|
res = await _race(topic, "Gliederungs-Fix", slots, 1, _timeout("plan", soll), provider, cancelled=is_cancelled)
|
||||||
|
if is_cancelled():
|
||||||
|
return None
|
||||||
|
if res is None:
|
||||||
|
_log(topic, "Gliederungs-Fix ungültig — ursprüngliche Gliederung bleibt")
|
||||||
|
files["gliederung"].write_text(gliederung_json(), encoding="utf-8")
|
||||||
|
else:
|
||||||
|
plan = res[0]
|
||||||
|
|
||||||
|
# Schritt 5: Schreiben — vorhandene Chunk-Dateien werden übernommen (Resume)
|
||||||
total_sections = sum(len(c["nums"]) for c in plan)
|
total_sections = sum(len(c["nums"]) for c in plan)
|
||||||
chunks = _split_chunks(plan, min(WRITER_MAX, max(1, math.ceil(total_sections / WRITER_SECTIONS))))
|
chunks = _split_chunks(plan, min(WRITER_MAX, max(1, math.ceil(total_sections / WRITER_SECTIONS))))
|
||||||
zuteilungen = [_zuteilung_text(chunk, entries) for chunk in chunks]
|
zuteilungen = [_zuteilung_text(chunk, entries) 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]
|
||||||
|
|
||||||
writer_count = len(zuteilungen)
|
writer_count = len(zuteilungen)
|
||||||
await _set_progress(guide_id, f"Schreibe Sections ({writer_count} Writer)…" if writer_count > 1 else "Schreibe Sections…")
|
|
||||||
paths = [content_path.parent / f"{content_path.stem}.chunk-{i}.md" for i in range(1, writer_count + 1)]
|
paths = [content_path.parent / f"{content_path.stem}.chunk-{i}.md" for i in range(1, writer_count + 1)]
|
||||||
fragment_paths.extend(paths)
|
offen = [i for i, p in enumerate(paths) if not p.exists()]
|
||||||
results = await asyncio.gather(*[
|
if offen:
|
||||||
run_agent(
|
await _set_step(guide_id, 4, f"Schreibe Sections ({writer_count} Writer)…" if writer_count > 1 else "Schreibe Sections…")
|
||||||
f"{guide_id}-w{i}",
|
results = await asyncio.gather(*[
|
||||||
_prompt(
|
run_agent(
|
||||||
"Guide-Writer",
|
f"{guide_id}-w{i + 1}",
|
||||||
topic=topic, format_name=format_name, zuteilung=zuteilung,
|
_prompt(
|
||||||
facts=facts, spec=spec, out_path=path, extra=_extra(instructions),
|
"Guide-Writer",
|
||||||
),
|
topic=topic, format_name=format_name, zuteilung=zuteilungen[i],
|
||||||
_timeout("writer", size), provider=provider, role="guide", capabilities="full",
|
facts=facts, spec=spec, out_path=paths[i], extra=_extra(instructions),
|
||||||
)
|
),
|
||||||
for i, (zuteilung, path, size) in enumerate(zip(zuteilungen, paths, chunk_sizes), 1)
|
_timeout("writer", chunk_sizes[i]), provider=provider, role="guide", capabilities="full",
|
||||||
], return_exceptions=True)
|
)
|
||||||
if is_cancelled():
|
for i in offen
|
||||||
return None
|
], return_exceptions=True)
|
||||||
for i, (r, p) in enumerate(zip(results, paths), 1):
|
if is_cancelled():
|
||||||
if isinstance(r, BaseException):
|
return None
|
||||||
_log(topic, f"Writer {i}: {type(r).__name__}: {r}")
|
for i, r in zip(offen, results):
|
||||||
elif r[0] != 0:
|
if isinstance(r, BaseException):
|
||||||
_log(topic, f"Writer {i}: {_claude_error('Fehler', *r)}")
|
_log(topic, f"Writer {i + 1}: {type(r).__name__}: {r}")
|
||||||
elif not p.exists():
|
elif r[0] != 0:
|
||||||
_log(topic, f"Writer {i}: keine Ausgabedatei erstellt")
|
_log(topic, f"Writer {i + 1}: {_claude_error('Fehler', *r)}")
|
||||||
fragments: list[dict] = []
|
elif not paths[i].exists():
|
||||||
for p in paths:
|
_log(topic, f"Writer {i + 1}: keine Ausgabedatei erstellt")
|
||||||
if p.exists():
|
if not any(p.exists() for p in paths):
|
||||||
fragments.extend(_parse_fragment(p.read_text(encoding="utf-8")))
|
await _fail(guide_id, _gather_error("Writer-Fehler", list(results)))
|
||||||
if not fragments:
|
return None
|
||||||
await _fail(guide_id, _gather_error("Writer-Fehler", list(results)))
|
|
||||||
return None
|
|
||||||
|
|
||||||
await _set_progress(guide_id, "Setze zusammen…")
|
|
||||||
idx = _titel_index(entries)
|
idx = _titel_index(entries)
|
||||||
by_num: dict[int, dict] = {}
|
by_num: dict[int, dict] = {}
|
||||||
for sec in fragments:
|
for p in paths:
|
||||||
num = _titel_aufloesen(idx, sec["titel"])
|
if not p.exists():
|
||||||
if num is None:
|
|
||||||
_log(topic, f"Writer lieferte unbekannte Section '{sec['titel'][:40]}' (ignoriert)")
|
|
||||||
continue
|
continue
|
||||||
if num not in by_num:
|
for sec in _parse_fragment(p.read_text(encoding="utf-8")):
|
||||||
by_num[num] = sec
|
num = _titel_aufloesen(idx, sec["titel"])
|
||||||
|
if num is None:
|
||||||
|
_log(topic, f"Writer lieferte unbekannte Section '{sec['titel'][:40]}' (ignoriert)")
|
||||||
|
elif num not in by_num:
|
||||||
|
by_num[num] = sec
|
||||||
|
if not by_num:
|
||||||
|
await _fail(guide_id, "Keine Sections in der Writer-Ausgabe gefunden")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Schritt 6: Lese-Prüfung pro Writer-Paket — Fix beauftragt Writer nur mit beanstandeten Sections
|
||||||
|
chunk_nums = [[num for ch in chunk for num in ch["nums"] if num in by_num] for chunk in chunks]
|
||||||
|
check_paths = [content_path.parent / f"{content_path.stem}.lese-check-{i}.json" for i in range(1, writer_count + 1)]
|
||||||
|
offen_checks = [i for i, p in enumerate(check_paths) if _lese_probleme_schema(_json_datei(p)) is None and chunk_nums[i]]
|
||||||
|
if offen_checks:
|
||||||
|
await _set_step(guide_id, 5, f"Prüfe Lesbarkeit ({len(offen_checks)} Prüfer)…" if len(offen_checks) > 1 else "Prüfe Lesbarkeit…")
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
slots = [{
|
||||||
|
"key": f"{guide_id}-lese-check-{i + 1}",
|
||||||
|
"prompt": _prompt(
|
||||||
|
"Guide-Lese-Check",
|
||||||
|
topic=topic, format_name=format_name, spec=spec,
|
||||||
|
sections=sections_text(chunk_nums[i]),
|
||||||
|
out_path=check_paths[i], extra=_extra(instructions),
|
||||||
|
),
|
||||||
|
"role": "fast", "capabilities": "files",
|
||||||
|
"payload": (lambda result, p=check_paths[i]: _lese_probleme_schema(_json_datei(p))),
|
||||||
|
} for i in offen_checks]
|
||||||
|
res = await _race(topic, "Lese-Prüfung", slots, len(slots), _timeout("lese_check", max(chunk_sizes)), provider, cancelled=is_cancelled)
|
||||||
|
if is_cancelled():
|
||||||
|
return None
|
||||||
|
if res is None:
|
||||||
|
await _fail(guide_id, "Lese-Prüfung fehlgeschlagen")
|
||||||
|
return None
|
||||||
|
|
||||||
|
probleme_by_num: dict[int, str] = {}
|
||||||
|
for p in check_paths:
|
||||||
|
for item in (_lese_probleme_schema(_json_datei(p)) or []):
|
||||||
|
num = _titel_aufloesen(idx, item["section"])
|
||||||
|
if num in by_num and num not in probleme_by_num:
|
||||||
|
probleme_by_num[num] = item["problem"]
|
||||||
|
|
||||||
|
if probleme_by_num:
|
||||||
|
_log(topic, f"Lese-Prüfung: {len(probleme_by_num)} Section(s) beanstandet")
|
||||||
|
await _set_step(guide_id, 5, f"Überarbeite {len(probleme_by_num)} Section(s)…")
|
||||||
|
fix_chunks = [[num for num in nums if num in probleme_by_num] for nums in chunk_nums]
|
||||||
|
fix_offen = [i for i, nums in enumerate(fix_chunks) if nums]
|
||||||
|
fix_paths = [content_path.parent / f"{content_path.stem}.fix-{i + 1}.md" for i in range(writer_count)]
|
||||||
|
|
||||||
|
def auftraege_text(nums: list[int]) -> str:
|
||||||
|
return "\n\n".join(
|
||||||
|
f"SECTION: {_titel(entries[num])}\nPROBLEM: {probleme_by_num[num]}\nAKTUELLER INHALT:\n{by_num[num]['md']}"
|
||||||
|
for num in nums
|
||||||
|
)
|
||||||
|
|
||||||
|
results = await asyncio.gather(*[
|
||||||
|
run_agent(
|
||||||
|
f"{guide_id}-fix-w{i + 1}",
|
||||||
|
_prompt(
|
||||||
|
"Guide-Sections-Fix",
|
||||||
|
topic=topic, format_name=format_name, facts=facts, spec=spec,
|
||||||
|
auftraege=auftraege_text(fix_chunks[i]),
|
||||||
|
out_path=fix_paths[i], extra=_extra(instructions),
|
||||||
|
),
|
||||||
|
_timeout("writer", len(fix_chunks[i])), provider=provider, role="guide", capabilities="full",
|
||||||
|
)
|
||||||
|
for i in fix_offen
|
||||||
|
], return_exceptions=True)
|
||||||
|
if is_cancelled():
|
||||||
|
return None
|
||||||
|
for i, r in zip(fix_offen, results):
|
||||||
|
if isinstance(r, BaseException) or (not isinstance(r, BaseException) and r[0] != 0):
|
||||||
|
_log(topic, f"Sections-Fix {i + 1} fehlgeschlagen — Original bleibt")
|
||||||
|
ersetzt = 0
|
||||||
|
for i in fix_offen:
|
||||||
|
if not fix_paths[i].exists():
|
||||||
|
continue
|
||||||
|
for sec in _parse_fragment(fix_paths[i].read_text(encoding="utf-8")):
|
||||||
|
num = _titel_aufloesen(idx, sec["titel"])
|
||||||
|
if num in probleme_by_num and sec["md"].strip():
|
||||||
|
by_num[num] = sec
|
||||||
|
ersetzt += 1
|
||||||
|
_log(topic, f"Lese-Prüfung: {ersetzt} Section(s) überarbeitet")
|
||||||
|
|
||||||
|
await _set_progress(guide_id, "Setze zusammen…")
|
||||||
chapters: list[dict] = []
|
chapters: list[dict] = []
|
||||||
for ch in plan:
|
for ch in plan:
|
||||||
sections = [
|
sections = [
|
||||||
@@ -823,14 +1185,19 @@ async def generate_guide(guide_id: str, topic: str, format_name: str, instructio
|
|||||||
content_path = guide_content_path(topic, format_name)
|
content_path = guide_content_path(topic, format_name)
|
||||||
content_path.parent.mkdir(parents=True, exist_ok=True)
|
content_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
project = project_dir(topic) if project_dir(topic).is_dir() else None
|
project = project_dir(topic) if project_dir(topic).is_dir() else None
|
||||||
fragment_paths: list[Path] = []
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if guide_id in _cancelled:
|
if guide_id in _cancelled:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# „Neu erstellen": fertiger Guide → kompletter Frischstart.
|
||||||
|
# Sonst sind Schritt-Dateien Reste eines Abbruchs/Fehlers → Resume.
|
||||||
|
if content_path.exists():
|
||||||
|
for p_alt in guide_slot_dateien(content_path):
|
||||||
|
p_alt.unlink(missing_ok=True)
|
||||||
|
|
||||||
if format_name == "OnePager":
|
if format_name == "OnePager":
|
||||||
chapters = await _generate_onepager(guide_id, topic, instructions, provider, project, content_path, fragment_paths)
|
chapters = await _generate_onepager(guide_id, topic, instructions, provider, project, content_path)
|
||||||
else:
|
else:
|
||||||
alle = _lade_bausteine(bausteine_path(topic).read_text(encoding="utf-8"))
|
alle = _lade_bausteine(bausteine_path(topic).read_text(encoding="utf-8"))
|
||||||
if not alle:
|
if not alle:
|
||||||
@@ -840,7 +1207,7 @@ async def generate_guide(guide_id: str, topic: str, format_name: str, instructio
|
|||||||
facts = _prompt("Guide-Fakten-Projekt", project=project) if project else _prompt("Guide-Fakten-Thema")
|
facts = _prompt("Guide-Fakten-Projekt", project=project) if project else _prompt("Guide-Fakten-Thema")
|
||||||
chapters = await _generate_sections(
|
chapters = await _generate_sections(
|
||||||
guide_id, topic, format_name, entries,
|
guide_id, topic, format_name, entries,
|
||||||
facts, instructions, provider, content_path, fragment_paths,
|
facts, instructions, provider, content_path,
|
||||||
)
|
)
|
||||||
if chapters is None or guide_id in _cancelled:
|
if chapters is None or guide_id in _cancelled:
|
||||||
return
|
return
|
||||||
@@ -851,7 +1218,7 @@ async def generate_guide(guide_id: str, topic: str, format_name: str, instructio
|
|||||||
)
|
)
|
||||||
|
|
||||||
now = datetime.now(timezone.utc).isoformat()
|
now = datetime.now(timezone.utc).isoformat()
|
||||||
await update_guide(guide_id, status="done", progress=None, updated_at=now)
|
await update_guide(guide_id, status="done", progress=None, step=None, updated_at=now)
|
||||||
|
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
await _fail(guide_id, "Timeout bei der Generierung")
|
await _fail(guide_id, "Timeout bei der Generierung")
|
||||||
@@ -861,8 +1228,6 @@ async def generate_guide(guide_id: str, topic: str, format_name: str, instructio
|
|||||||
await _fail(guide_id, str(e)[:2000])
|
await _fail(guide_id, str(e)[:2000])
|
||||||
finally:
|
finally:
|
||||||
_cancelled.discard(guide_id)
|
_cancelled.discard(guide_id)
|
||||||
for p in fragment_paths:
|
|
||||||
p.unlink(missing_ok=True)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Tutor-Chat ---
|
# --- Tutor-Chat ---
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ class GuideResponse(BaseModel):
|
|||||||
format: str
|
format: str
|
||||||
status: str
|
status: str
|
||||||
progress: str | None = None
|
progress: str | None = None
|
||||||
|
step: int | None = None
|
||||||
error_msg: str | None = None
|
error_msg: str | None = None
|
||||||
created_at: str
|
created_at: str
|
||||||
updated_at: str
|
updated_at: str
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from database import (
|
|||||||
list_progress, set_progress, delete_progress,
|
list_progress, set_progress, delete_progress,
|
||||||
)
|
)
|
||||||
from generator import (
|
from generator import (
|
||||||
generate_guide, cancel_guide, chat_with_guide,
|
generate_guide, cancel_guide, chat_with_guide, guide_slot_dateien,
|
||||||
generate_bausteine, cancel_bausteine, bausteine_status, active_bausteine, reset_bausteine,
|
generate_bausteine, cancel_bausteine, bausteine_status, active_bausteine, reset_bausteine,
|
||||||
)
|
)
|
||||||
from models import (
|
from models import (
|
||||||
@@ -228,7 +228,10 @@ async def remove(guide_id: str):
|
|||||||
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")
|
||||||
guide_content_path(guide["topic"], guide["format"]).unlink(missing_ok=True)
|
content = guide_content_path(guide["topic"], guide["format"])
|
||||||
|
for p in guide_slot_dateien(content):
|
||||||
|
p.unlink(missing_ok=True)
|
||||||
|
content.unlink(missing_ok=True)
|
||||||
await delete_progress(guide_id)
|
await delete_progress(guide_id)
|
||||||
await delete_guide(guide_id)
|
await delete_guide(guide_id)
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|||||||
@@ -200,7 +200,12 @@ async function send() {
|
|||||||
>
|
>
|
||||||
<h2 class="chapter-title">{{ ch.title }}</h2>
|
<h2 class="chapter-title">{{ ch.title }}</h2>
|
||||||
<div class="sections">
|
<div class="sections">
|
||||||
<article v-for="s in ch.sections" :key="s.num" class="section-card">
|
<article
|
||||||
|
v-for="s in ch.sections"
|
||||||
|
:key="s.num"
|
||||||
|
class="section-card"
|
||||||
|
:style="isOnePager && s.key ? { gridArea: s.key } : null"
|
||||||
|
>
|
||||||
<h3>{{ s.title }}</h3>
|
<h3>{{ s.title }}</h3>
|
||||||
<div class="section-body markdown" v-html="renderMarkdown(s.md)"></div>
|
<div class="section-body markdown" v-html="renderMarkdown(s.md)"></div>
|
||||||
</article>
|
</article>
|
||||||
@@ -326,16 +331,43 @@ async function send() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* OnePager: dichtes Karten-Grid */
|
/* OnePager: festes 3×3-Raster über volle Breite und Höhe */
|
||||||
|
.guide-content.onepager {
|
||||||
|
max-width: none;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0.9rem 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-content.onepager .guide-head,
|
||||||
|
.guide-content.onepager .chapter-title {
|
||||||
|
display: none; /* Thema steht in der Info-Karte — Platz fürs Raster */
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-content.onepager .chapter {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.guide-content.onepager .sections {
|
.guide-content.onepager .sections {
|
||||||
|
height: 100%;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
grid-template-rows: repeat(3, 1fr);
|
||||||
|
grid-template-areas:
|
||||||
|
"info beispiel voraussetzungen"
|
||||||
|
"eigenschaften beispiel modern"
|
||||||
|
"eigenschaften zusammenhaenge veraltet";
|
||||||
gap: 0.6rem;
|
gap: 0.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.guide-content.onepager .section-card {
|
.guide-content.onepager .section-card {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
padding: 0.7rem 0.9rem;
|
padding: 0.7rem 0.9rem;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
font-size: 0.88rem;
|
font-size: 0.88rem;
|
||||||
@@ -347,6 +379,23 @@ async function send() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobil: eine Spalte in Quellreihenfolge (info → … → veraltet) */
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.guide-content.onepager {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-content.onepager .sections {
|
||||||
|
height: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-content.onepager .section-card {
|
||||||
|
overflow-y: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.ch-toggle {
|
.ch-toggle {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -451,9 +500,23 @@ async function send() {
|
|||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Lesbarkeit: ~17px Fließtext, Zeilenhöhe 1.6, Textspalte max. ~70 Zeichen —
|
||||||
|
Code-Blöcke dürfen die volle Kartenbreite nutzen */
|
||||||
.section-body {
|
.section-body {
|
||||||
font-size: 0.92rem;
|
font-size: 1.0625rem;
|
||||||
line-height: 1.55;
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-card .markdown :deep(p),
|
||||||
|
.section-card .markdown :deep(ul),
|
||||||
|
.section-card .markdown :deep(ol) {
|
||||||
|
max-width: 70ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onepager .section-card .markdown :deep(p),
|
||||||
|
.onepager .section-card .markdown :deep(ul),
|
||||||
|
.onepager .section-card .markdown :deep(ol) {
|
||||||
|
max-width: none; /* OnePager-Zellen sind selbst schmal genug */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Chat --- */
|
/* --- Chat --- */
|
||||||
|
|||||||
@@ -46,8 +46,6 @@ const formats = [
|
|||||||
{ key: 'FullGuide', label: 'FullGuide' },
|
{ key: 'FullGuide', label: 'FullGuide' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const BAUSTEINE_KEY = '__bausteine__'
|
|
||||||
|
|
||||||
const bausteineState = computed(() => {
|
const bausteineState = computed(() => {
|
||||||
if (props.bausteine.generating) return 'generating'
|
if (props.bausteine.generating) return 'generating'
|
||||||
return props.bausteine.ready ? 'done' : 'none'
|
return props.bausteine.ready ? 'done' : 'none'
|
||||||
@@ -89,10 +87,7 @@ function confirmResetBausteine() {
|
|||||||
|
|
||||||
function handleBausteinePlay() {
|
function handleBausteinePlay() {
|
||||||
if (bausteineState.value === 'generating') return
|
if (bausteineState.value === 'generating') return
|
||||||
const text = activeInput.value === BAUSTEINE_KEY ? inputText.value.trim() : ''
|
emit('bausteineClick', { instructions: '' })
|
||||||
emit('bausteineClick', { instructions: text })
|
|
||||||
activeInput.value = null
|
|
||||||
inputText.value = ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function guideStatus(format) {
|
function guideStatus(format) {
|
||||||
@@ -105,6 +100,21 @@ function guideStatus(format) {
|
|||||||
return latest.status
|
return latest.status
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Schritt-Kugeln der Guide-Pipelines
|
||||||
|
const GUIDE_STEPS = ['Auswahl', 'Auswahl-Prüfung', 'Gliederung', 'Gliederungs-Prüfung', 'Schreiben', 'Lese-Prüfung']
|
||||||
|
const ONEPAGER_STEPS = ['Recherche', 'Recherche-Prüfung', 'Bauen', 'Prüfung']
|
||||||
|
|
||||||
|
function guideSteps(format) {
|
||||||
|
const st = guideStatus(format)
|
||||||
|
if (st !== 'generating' && st !== 'queued') return []
|
||||||
|
const labels = format === 'OnePager' ? ONEPAGER_STEPS : GUIDE_STEPS
|
||||||
|
const step = props.latestByFormat[format]?.step ?? -1
|
||||||
|
return labels.map((label, i) => ({
|
||||||
|
label,
|
||||||
|
state: i < step ? 'done' : i === step ? 'active' : 'pending',
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
function errorMsg(format) {
|
function errorMsg(format) {
|
||||||
const latest = props.latestByFormat[format]
|
const latest = props.latestByFormat[format]
|
||||||
if (latest?.status === 'error') return latest.error_msg || 'Fehler bei der Generierung'
|
if (latest?.status === 'error') return latest.error_msg || 'Fehler bei der Generierung'
|
||||||
@@ -118,24 +128,8 @@ function handleFormatClick(format) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeInput = ref(null)
|
|
||||||
const inputText = ref('')
|
|
||||||
|
|
||||||
function toggleInput(format) {
|
|
||||||
if (activeInput.value === format) {
|
|
||||||
activeInput.value = null
|
|
||||||
inputText.value = ''
|
|
||||||
} else {
|
|
||||||
activeInput.value = format
|
|
||||||
inputText.value = ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePlay(format) {
|
function handlePlay(format) {
|
||||||
const text = activeInput.value === format ? inputText.value.trim() : ''
|
emit('formatClick', { format, instructions: '' })
|
||||||
emit('formatClick', { format, instructions: text })
|
|
||||||
activeInput.value = null
|
|
||||||
inputText.value = ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function dismissError(format) {
|
function dismissError(format) {
|
||||||
@@ -219,7 +213,7 @@ function confirmDeleteProject(name) {
|
|||||||
<div class="progress-info" v-if="activeGenerations.length">
|
<div class="progress-info" v-if="activeGenerations.length">
|
||||||
<div v-for="(line, i) in activeGenerations" :key="i">{{ line }}</div>
|
<div v-for="(line, i) in activeGenerations" :key="i">{{ line }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="format-row bausteine-row">
|
<div class="format-row bausteine-row ord-bausteine">
|
||||||
<div class="format-name bausteine-name">
|
<div class="format-name bausteine-name">
|
||||||
<span class="format-label">Bausteine</span>
|
<span class="format-label">Bausteine</span>
|
||||||
<span class="step-dots">
|
<span class="step-dots">
|
||||||
@@ -253,29 +247,26 @@ function confirmDeleteProject(name) {
|
|||||||
:title="bausteine.partial ? 'Fortsetzen' : bausteine.ready ? 'Bausteine neu erstellen' : 'Bausteine erstellen'"
|
:title="bausteine.partial ? 'Fortsetzen' : bausteine.ready ? 'Bausteine neu erstellen' : 'Bausteine erstellen'"
|
||||||
@click="handleBausteinePlay"
|
@click="handleBausteinePlay"
|
||||||
>▶</button>
|
>▶</button>
|
||||||
<button
|
|
||||||
class="action-btn pencil"
|
|
||||||
:class="{ active: activeInput === BAUSTEINE_KEY }"
|
|
||||||
title="Anweisungen"
|
|
||||||
@click="toggleInput(BAUSTEINE_KEY)"
|
|
||||||
>✎</button>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="bausteine.error" class="format-error">
|
<div v-if="bausteine.error" class="format-error ord-bausteine">
|
||||||
<span class="format-error-text">{{ bausteine.error }}</span>
|
<span class="format-error-text">{{ bausteine.error }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="activeInput === BAUSTEINE_KEY" class="format-input">
|
<!-- OnePager (unabhängig von Bausteinen) steht per CSS-order vor der Bausteine-Zeile -->
|
||||||
<input
|
<div v-for="f in formats" :key="f.key" :style="{ order: f.key === 'OnePager' ? 1 : 3 }">
|
||||||
v-model="inputText"
|
|
||||||
placeholder="Anweisungen (optional)…"
|
|
||||||
@keyup.enter="handleBausteinePlay"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-for="f in formats" :key="f.key">
|
|
||||||
<div :class="['format-row', 'fmt-' + guideStatus(f.key)]">
|
<div :class="['format-row', 'fmt-' + guideStatus(f.key)]">
|
||||||
<button class="format-name" @click="handleFormatClick(f.key)">
|
<button class="format-name" @click="handleFormatClick(f.key)">
|
||||||
<span class="format-label">{{ f.label }}</span>
|
<span class="format-label">{{ f.label }}</span>
|
||||||
|
<span class="step-dots" v-if="guideSteps(f.key).length">
|
||||||
|
<span
|
||||||
|
v-for="s in guideSteps(f.key)"
|
||||||
|
:key="s.label"
|
||||||
|
class="step-dot"
|
||||||
|
:class="s.state"
|
||||||
|
:title="s.state === 'active' ? (latestByFormat[f.key]?.progress || s.label) : s.label"
|
||||||
|
></span>
|
||||||
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="guideStatus(f.key) !== 'none'"
|
v-if="guideStatus(f.key) !== 'none'"
|
||||||
class="format-x"
|
class="format-x"
|
||||||
@@ -292,12 +283,6 @@ function confirmDeleteProject(name) {
|
|||||||
:disabled="f.key !== 'OnePager' && !bausteine.ready"
|
:disabled="f.key !== 'OnePager' && !bausteine.ready"
|
||||||
@click="handlePlay(f.key)"
|
@click="handlePlay(f.key)"
|
||||||
>▶</button>
|
>▶</button>
|
||||||
<button
|
|
||||||
class="action-btn pencil"
|
|
||||||
:class="{ active: activeInput === f.key }"
|
|
||||||
title="Anweisungen"
|
|
||||||
@click="toggleInput(f.key)"
|
|
||||||
>✎</button>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -305,13 +290,6 @@ function confirmDeleteProject(name) {
|
|||||||
<span class="format-error-text">{{ errorMsg(f.key) }}</span>
|
<span class="format-error-text">{{ errorMsg(f.key) }}</span>
|
||||||
<button class="format-error-x" title="Fehler entfernen" @click="dismissError(f.key)">×</button>
|
<button class="format-error-x" title="Fehler entfernen" @click="dismissError(f.key)">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="activeInput === f.key" class="format-input">
|
|
||||||
<input
|
|
||||||
v-model="inputText"
|
|
||||||
placeholder="Anweisungen (optional)…"
|
|
||||||
@keyup.enter="handlePlay(f.key)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -564,10 +542,6 @@ function confirmDeleteProject(name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Format section */
|
/* Format section */
|
||||||
.format-row.bausteine-row {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bausteine-name {
|
.bausteine-name {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -613,6 +587,13 @@ function confirmDeleteProject(name) {
|
|||||||
max-height: 60vh;
|
max-height: 60vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 0.5rem 0;
|
padding: 0.5rem 0;
|
||||||
|
/* flex + order: OnePager (order 1) vor Bausteine (order 2) vor den restlichen Formaten (order 3) */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ord-bausteine {
|
||||||
|
order: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-info {
|
.progress-info {
|
||||||
@@ -648,6 +629,7 @@ function confirmDeleteProject(name) {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.format-x {
|
.format-x {
|
||||||
@@ -754,36 +736,6 @@ function confirmDeleteProject(name) {
|
|||||||
border-color: var(--success-border);
|
border-color: var(--success-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn.pencil {
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn.pencil:hover,
|
|
||||||
.action-btn.pencil.active {
|
|
||||||
background: var(--accent-soft);
|
|
||||||
border-color: var(--accent-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.format-input {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
padding: 4px 0.75rem 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.format-input input {
|
|
||||||
flex: 1;
|
|
||||||
padding: 4px 8px;
|
|
||||||
border: 1px solid var(--border-strong);
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.format-input input:focus {
|
|
||||||
border-color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn:disabled {
|
.action-btn:disabled {
|
||||||
opacity: 0.35;
|
opacity: 0.35;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
|||||||
24
templates/Prompt/Guide-Auswahl-Check.md
Normal file
24
templates/Prompt/Guide-Auswahl-Check.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
Prüfe die Baustein-Auswahl für einen Lern-Guide zum Thema "{topic}" (Format: {format_name}).
|
||||||
|
|
||||||
|
Der Auftrag an die Auswahl war: {auswahl_auftrag}
|
||||||
|
|
||||||
|
INVENTAR (alle verfügbaren Bausteine):
|
||||||
|
{bausteine}
|
||||||
|
|
||||||
|
GETROFFENE AUSWAHL:
|
||||||
|
{auswahl}
|
||||||
|
|
||||||
|
Prüfe:
|
||||||
|
1. Fehlt etwas, das der Leser für diesen Zweck zwingend braucht?
|
||||||
|
2. Ist etwas drin, das dem Zweck nicht dient — Interna, Nischenfälle, Doppelungen (mehrere Lösungen fürs selbe Problem)?
|
||||||
|
3. Passt der Umfang zum Auftrag?
|
||||||
|
|
||||||
|
Du PRÜFST nur und notierst Probleme — du änderst die Auswahl nicht.
|
||||||
|
|
||||||
|
Schreibe NUR die JSON-Datei nach: {out_path}
|
||||||
|
|
||||||
|
Format — Auswahl in Ordnung:
|
||||||
|
{{"ok": true}}
|
||||||
|
Sonst (kurz und konkret, maximal 10 Punkte, Baustein-Titel exakt nennen):
|
||||||
|
{{"probleme": ["…", "…"]}}
|
||||||
|
{extra}
|
||||||
21
templates/Prompt/Guide-Auswahl-Fix.md
Normal file
21
templates/Prompt/Guide-Auswahl-Fix.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
Korrigiere die Baustein-Auswahl für einen Lern-Guide zum Thema "{topic}" (Format: {format_name}).
|
||||||
|
|
||||||
|
Der Auftrag an die Auswahl war: {auswahl_auftrag}
|
||||||
|
|
||||||
|
INVENTAR (alle verfügbaren Bausteine):
|
||||||
|
{bausteine}
|
||||||
|
|
||||||
|
BISHERIGE AUSWAHL:
|
||||||
|
{auswahl}
|
||||||
|
|
||||||
|
NOTIERTE PROBLEME (von der Prüfung):
|
||||||
|
{probleme}
|
||||||
|
|
||||||
|
Behebe NUR die notierten Probleme — alles andere bleibt unverändert.
|
||||||
|
Verwende die Titel EXAKT so, wie sie im Inventar stehen. Keine neuen erfinden.
|
||||||
|
|
||||||
|
Schreibe NUR die vollständige, korrigierte JSON-Datei nach: {out_path}
|
||||||
|
|
||||||
|
Format:
|
||||||
|
{{"bausteine": ["Exakter Titel", "Exakter Titel"]}}
|
||||||
|
{extra}
|
||||||
18
templates/Prompt/Guide-Auswahl.md
Normal file
18
templates/Prompt/Guide-Auswahl.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
Wähle die Bausteine für einen Lern-Guide zum Thema "{topic}" (Format: {format_name}).
|
||||||
|
|
||||||
|
BAUSTEINE (unsortiertes Inventar):
|
||||||
|
{bausteine}
|
||||||
|
|
||||||
|
{auswahl_auftrag}
|
||||||
|
|
||||||
|
Denke vom Ziel her: Was soll der Leser am Ende KÖNNEN?
|
||||||
|
- Wähle, was der Leser dafür praktisch braucht und wirklich benutzt.
|
||||||
|
- Lass weg: Interna (was das Werkzeug intern tut, ohne dass man es anfasst), Spezialfälle und Alternativen zum selben Problem — ein Weg reicht.
|
||||||
|
- "Klingt fundamental" ist kein Kriterium. Frage stattdessen: Fasst der Leser das selbst an?
|
||||||
|
- Verwende die Titel EXAKT so, wie sie in der Liste stehen. Keine neuen erfinden.
|
||||||
|
|
||||||
|
Schreibe NUR die JSON-Datei nach: {out_path}
|
||||||
|
|
||||||
|
Format:
|
||||||
|
{{"bausteine": ["Exakter Titel", "Exakter Titel"]}}
|
||||||
|
{extra}
|
||||||
24
templates/Prompt/Guide-Gliederung-Check.md
Normal file
24
templates/Prompt/Guide-Gliederung-Check.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
Prüfe die Gliederung eines Lern-Guides zum Thema "{topic}" (Format: {format_name}).
|
||||||
|
Zielgruppe: Anfänger. Zweck: {zweck}.
|
||||||
|
|
||||||
|
GEWÄHLTE BAUSTEINE (müssen alle vorkommen):
|
||||||
|
{auswahl}
|
||||||
|
|
||||||
|
GLIEDERUNG:
|
||||||
|
{gliederung}
|
||||||
|
|
||||||
|
Prüfe:
|
||||||
|
1. Kommt jeder gewählte Baustein in GENAU einem Kapitel vor (nichts fehlt, nichts doppelt, nichts erfunden)?
|
||||||
|
2. Führt Kapitel 1 zum schnellsten sichtbaren Ergebnis — oder beginnt es mit Theorie/Interna?
|
||||||
|
3. Stehen Voraussetzungen vor dem, was auf ihnen aufbaut? Konkretes vor Abstraktem?
|
||||||
|
4. Kapitelgrößen 3–7, Kapiteltitel kurz und konkret?
|
||||||
|
|
||||||
|
Du PRÜFST nur und notierst Probleme — du änderst die Gliederung nicht.
|
||||||
|
|
||||||
|
Schreibe NUR die JSON-Datei nach: {out_path}
|
||||||
|
|
||||||
|
Format — Gliederung in Ordnung:
|
||||||
|
{{"ok": true}}
|
||||||
|
Sonst (kurz und konkret, maximal 10 Punkte):
|
||||||
|
{{"probleme": ["…", "…"]}}
|
||||||
|
{extra}
|
||||||
20
templates/Prompt/Guide-Gliederung-Fix.md
Normal file
20
templates/Prompt/Guide-Gliederung-Fix.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
Korrigiere die Gliederung eines Lern-Guides zum Thema "{topic}" (Format: {format_name}).
|
||||||
|
|
||||||
|
GEWÄHLTE BAUSTEINE (müssen alle vorkommen):
|
||||||
|
{auswahl}
|
||||||
|
|
||||||
|
BISHERIGE GLIEDERUNG:
|
||||||
|
{gliederung}
|
||||||
|
|
||||||
|
NOTIERTE PROBLEME (von der Prüfung):
|
||||||
|
{probleme}
|
||||||
|
|
||||||
|
Behebe NUR die notierten Probleme — alles andere bleibt unverändert.
|
||||||
|
- JEDER gewählte Baustein landet in GENAU einem Kapitel.
|
||||||
|
- Verwende die Titel EXAKT so, wie sie in der Liste stehen.
|
||||||
|
|
||||||
|
Schreibe NUR die vollständige, korrigierte JSON-Datei nach: {out_path}
|
||||||
|
|
||||||
|
Format:
|
||||||
|
{{"kapitel": [{{"titel": "Grundlagen", "bausteine": ["Exakter Titel", "Exakter Titel"]}}]}}
|
||||||
|
{extra}
|
||||||
@@ -1,22 +1,15 @@
|
|||||||
Plane die Gliederung eines Lern-Guides zum Thema "{topic}" (Format: {format_name}).
|
Plane die Gliederung eines Lern-Guides zum Thema "{topic}" (Format: {format_name}).
|
||||||
|
|
||||||
BAUSTEINE (unsortiertes Inventar):
|
GEWÄHLTE BAUSTEINE (unsortiert — die Auswahl steht fest, du ordnest nur):
|
||||||
{bausteine}
|
{bausteine}
|
||||||
|
|
||||||
{auswahl_auftrag}
|
|
||||||
|
|
||||||
AUSWAHL — denke vom Ziel her: Was soll der Leser am Ende KÖNNEN?
|
|
||||||
- Wähle, was der Leser dafür praktisch braucht und wirklich benutzt.
|
|
||||||
- Lass weg: Interna (was das Werkzeug intern tut, ohne dass man es anfasst), Spezialfälle und Alternativen zum selben Problem — ein Weg reicht.
|
|
||||||
- "Klingt fundamental" ist kein Kriterium. Frage stattdessen: Fasst der Leser das selbst an?
|
|
||||||
|
|
||||||
REIHENFOLGE — vom Bekannten zum Unbekannten:
|
REIHENFOLGE — vom Bekannten zum Unbekannten:
|
||||||
- Kapitel 1 führt zum schnellsten sichtbaren Ergebnis (erster Erfolg), nicht zur Theorie.
|
- Kapitel 1 führt zum schnellsten sichtbaren Ergebnis (erster Erfolg), nicht zur Theorie.
|
||||||
- Konkretes vor Abstraktem, Einfaches vor Komplexem, Voraussetzungen vor dem, was auf ihnen aufbaut.
|
- Konkretes vor Abstraktem, Einfaches vor Komplexem, Voraussetzungen vor dem, was auf ihnen aufbaut.
|
||||||
- Jedes Kapitel baut auf den vorigen auf — ein roter Faden, keine Themensammlung.
|
- Jedes Kapitel baut auf den vorigen auf — ein roter Faden, keine Themensammlung.
|
||||||
|
|
||||||
Regeln:
|
Regeln:
|
||||||
- Jeder gewählte Baustein landet in GENAU einem Kapitel. Keine neuen erfinden.
|
- JEDER gewählte Baustein landet in GENAU einem Kapitel. Keinen weglassen, keine neuen erfinden.
|
||||||
- Verwende die Titel EXAKT so, wie sie in der Liste stehen.
|
- Verwende die Titel EXAKT so, wie sie in der Liste stehen.
|
||||||
- 3–7 Bausteine pro Kapitel; die Kapitelzahl folgt aus dem Thema.
|
- 3–7 Bausteine pro Kapitel; die Kapitelzahl folgt aus dem Thema.
|
||||||
- Kapiteltitel kurz und konkret.
|
- Kapiteltitel kurz und konkret.
|
||||||
23
templates/Prompt/Guide-Lese-Check.md
Normal file
23
templates/Prompt/Guide-Lese-Check.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
Prüfe geschriebene Sections eines Lern-Guides zum Thema "{topic}" (Format: {format_name}) auf Lesbarkeit.
|
||||||
|
Zielgruppe: Anfänger.
|
||||||
|
|
||||||
|
SECTION-SPEZIFIKATION (Soll-Zustand):
|
||||||
|
{spec}
|
||||||
|
|
||||||
|
SECTIONS:
|
||||||
|
{sections}
|
||||||
|
|
||||||
|
Prüfe jede Section:
|
||||||
|
1. Ist die Beschreibung für Anfänger verständlich und maximal 1–2 Sätze?
|
||||||
|
2. Sind die Beispiele kurz, simpel und plausibel korrekt?
|
||||||
|
3. 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.
|
||||||
|
|
||||||
|
Schreibe NUR die JSON-Datei nach: {out_path}
|
||||||
|
|
||||||
|
Format — alles in Ordnung:
|
||||||
|
{{"ok": true}}
|
||||||
|
Sonst (Section-Titel EXAKT wie oben):
|
||||||
|
{{"probleme": [{{"section": "Exakter Section-Titel", "problem": "…"}}]}}
|
||||||
|
{extra}
|
||||||
24
templates/Prompt/Guide-Sections-Fix.md
Normal file
24
templates/Prompt/Guide-Sections-Fix.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
Überarbeite einzelne Sections eines Lern-Guides zum Thema "{topic}" (Format: {format_name}).
|
||||||
|
|
||||||
|
{facts}
|
||||||
|
|
||||||
|
SECTION-SPEZIFIKATION:
|
||||||
|
{spec}
|
||||||
|
|
||||||
|
ZU ÜBERARBEITEN — je Section der aktuelle Inhalt und das notierte Problem:
|
||||||
|
{auftraege}
|
||||||
|
|
||||||
|
Behebe pro Section NUR das notierte Problem; was in Ordnung ist, bleibt inhaltlich erhalten.
|
||||||
|
|
||||||
|
Schreibe NUR die Datei {out_path} in GENAU diesem Format — für JEDE beanstandete Section ein section-Marker (Titel EXAKT wie oben), darunter der vollständige neue Markdown-Body:
|
||||||
|
|
||||||
|
<!-- section: Exakter Section-Titel -->
|
||||||
|
Beschreibung…
|
||||||
|
|
||||||
|
### Beispiel
|
||||||
|
```sprache
|
||||||
|
…
|
||||||
|
```
|
||||||
|
|
||||||
|
Die Marker-Zeilen exakt so schreiben. Kein Text außerhalb der Sections.
|
||||||
|
{extra}
|
||||||
@@ -1,21 +1,21 @@
|
|||||||
Baue aus der Faktenbasis einen OnePager zum Thema "{topic}" — ein Einordnungs- und Entscheidungsdokument auf einer Seite. Danach muss der Leser verstehen, was das Thema ist, wo es hingehört und ob es das ist, was er sucht.
|
Baue aus der Faktenbasis einen OnePager zum Thema "{topic}" — ein Übersichtsblatt im 3×3-Raster auf einer Seite.
|
||||||
|
|
||||||
FAKTENBASIS (alleinige Quelle, nichts hinzuerfinden):
|
FAKTENBASIS (alleinige Quelle, nichts hinzuerfinden):
|
||||||
{recherche}
|
{recherche}
|
||||||
|
|
||||||
Erstelle GENAU diese 7 Karten (Titel exakt so):
|
Erstelle GENAU diese 7 Karten (JSON-Schlüssel exakt so):
|
||||||
1. "Was ist {topic}?" — Definition in 1–2 Sätzen
|
- "info" — Titel: "{topic}". Kurzbeschreibung in 1–2 Sätzen, darunter technische Daten als Stichpunkte (Art/Typ, Version/Stand, Lizenz/Kosten).
|
||||||
2. "Welches Problem löst es?" — der Schmerzpunkt, für den es gebaut wurde
|
- "eigenschaften" — Titel: "Kerneigenschaften". Die 4–7 prägenden Eigenschaften des Systems als Stichpunkte.
|
||||||
3. "Wann nehmen — wann nicht?" — konkrete Entscheidungshilfe in 2–4 Stichpunkten
|
- "beispiel" — Titel: "Beispiel". EIN anschauliches, typisches Codebeispiel (Markdown-Codeblock) mit einem Satz Erklärung.
|
||||||
4. "Einordnung & Alternativen" — die wichtigsten Nachbarn und der Unterschied
|
- "zusammenhaenge" — Titel: "Zusammenhänge". Mit welchen Systemen/Themen es zusammenhängt — Stichpunkte mit je einem halben Satz.
|
||||||
5. "So sieht es aus" — EIN minimales, typisches Codebeispiel (Markdown-Codeblock)
|
- "voraussetzungen" — Titel: "Voraussetzungen". Was man vorher können oder haben muss.
|
||||||
6. "Fakten" — Version, Reife, Lizenz/Kosten, Verbreitung
|
- "modern" — Titel: "Moderne Features". Was aktuell ist und heute verwendet wird.
|
||||||
7. "Erste Schritte" — wie man anfängt, in 1–2 Zeilen
|
- "veraltet" — Titel: "Veraltete Features". Was es noch gibt, aber nicht mehr verwendet werden sollte. Gibt es nichts Veraltetes: ehrlich "Keine." mit einem Satz Begründung — nichts erfinden.
|
||||||
|
|
||||||
Inhalt pro Karte kompakt (1–4 Sätze bzw. Stichpunkte, Markdown erlaubt), auf DEUTSCH, alles aus der Faktenbasis belegbar.
|
Inhalt pro Karte kompakt (Markdown erlaubt), auf DEUTSCH, alles aus der Faktenbasis belegbar.
|
||||||
|
|
||||||
Schreibe NUR die JSON-Datei nach: {out_path}
|
Schreibe NUR die JSON-Datei nach: {out_path}
|
||||||
|
|
||||||
Format:
|
Format:
|
||||||
{{"karten": [{{"titel": "Was ist {topic}?", "merksatz": "…"}}]}}
|
{{"karten": {{"info": {{"titel": "{topic}", "md": "…"}}, "eigenschaften": {{"titel": "Kerneigenschaften", "md": "…"}}, "beispiel": {{"titel": "Beispiel", "md": "…"}}, "zusammenhaenge": {{"titel": "Zusammenhänge", "md": "…"}}, "voraussetzungen": {{"titel": "Voraussetzungen", "md": "…"}}, "modern": {{"titel": "Moderne Features", "md": "…"}}, "veraltet": {{"titel": "Veraltete Features", "md": "…"}}}}}}
|
||||||
{extra}
|
{extra}
|
||||||
18
templates/Prompt/OnePager-Fix.md
Normal file
18
templates/Prompt/OnePager-Fix.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
Korrigiere die Karten eines OnePagers zum Thema "{topic}".
|
||||||
|
|
||||||
|
FAKTENBASIS (alleinige Quelle, nichts hinzuerfinden):
|
||||||
|
{recherche}
|
||||||
|
|
||||||
|
BISHERIGE KARTEN:
|
||||||
|
{karten}
|
||||||
|
|
||||||
|
NOTIERTE PROBLEME (von der Prüfung):
|
||||||
|
{probleme}
|
||||||
|
|
||||||
|
Behebe NUR die notierten Probleme — alle anderen Karten bleiben unverändert.
|
||||||
|
|
||||||
|
Schreibe NUR die vollständige, korrigierte JSON-Datei (alle 7 Karten) nach: {out_path}
|
||||||
|
|
||||||
|
Format:
|
||||||
|
{{"karten": {{"info": {{"titel": "…", "md": "…"}}, "eigenschaften": {{"titel": "…", "md": "…"}}, "beispiel": {{"titel": "…", "md": "…"}}, "zusammenhaenge": {{"titel": "…", "md": "…"}}, "voraussetzungen": {{"titel": "…", "md": "…"}}, "modern": {{"titel": "…", "md": "…"}}, "veraltet": {{"titel": "…", "md": "…"}}}}}}
|
||||||
|
{extra}
|
||||||
27
templates/Prompt/OnePager-Recherche-Check.md
Normal file
27
templates/Prompt/OnePager-Recherche-Check.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
Prüfe die Faktenbasis für einen OnePager zum Thema "{topic}".
|
||||||
|
|
||||||
|
FAKTENBASIS:
|
||||||
|
{recherche}
|
||||||
|
|
||||||
|
Sie muss diese Dimensionen abdecken:
|
||||||
|
1. Kurzbeschreibung (1–2 Sätze)
|
||||||
|
2. Technische Daten (Art/Typ, Version/Stand, Lizenz/Kosten)
|
||||||
|
3. Kerneigenschaften des Systems
|
||||||
|
4. Ein typisches Beispiel
|
||||||
|
5. Zusammenhänge mit anderen Systemen/Themen
|
||||||
|
6. Voraussetzungen
|
||||||
|
7. Moderne vs. veraltete Features (oder die ausdrückliche Feststellung, dass nichts veraltet ist)
|
||||||
|
|
||||||
|
Prüfe:
|
||||||
|
1. Ist jede Dimension mit konkreten Fakten belegt (Namen, Versionen, Zahlen — nicht vage)?
|
||||||
|
2. Hat jeder Punkt eine Quelle?
|
||||||
|
3. Wirkt etwas erfunden oder widersprüchlich?
|
||||||
|
|
||||||
|
Du PRÜFST nur und notierst Probleme — du änderst nichts.
|
||||||
|
|
||||||
|
Schreibe NUR die JSON-Datei nach: {out_path}
|
||||||
|
|
||||||
|
Format — alles in Ordnung:
|
||||||
|
{{"ok": true}}
|
||||||
|
Sonst (kurz und konkret, maximal 10 Punkte):
|
||||||
|
{{"probleme": ["…", "…"]}}
|
||||||
16
templates/Prompt/OnePager-Recherche-Fix.md
Normal file
16
templates/Prompt/OnePager-Recherche-Fix.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
Überarbeite die Faktenbasis für einen OnePager zum Thema "{topic}".
|
||||||
|
|
||||||
|
{source}
|
||||||
|
|
||||||
|
BISHERIGE FAKTENBASIS:
|
||||||
|
{recherche}
|
||||||
|
|
||||||
|
NOTIERTE PROBLEME (von der Prüfung):
|
||||||
|
{probleme}
|
||||||
|
|
||||||
|
Behebe NUR die notierten Probleme — fehlende Dimensionen nachrecherchieren, Vages konkretisieren, Unbelegtes belegen oder streichen. Alles andere bleibt erhalten.
|
||||||
|
|
||||||
|
Schreibe die VOLLSTÄNDIGE, überarbeitete Markdown-Datei nach: {out_path}
|
||||||
|
|
||||||
|
Kompakt, faktenorientiert, mit Quelle pro Punkt.
|
||||||
|
{extra}
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
Sammle die Faktenbasis für einen OnePager — ein Einordnungs- und Entscheidungsdokument — zum Projekt "{topic}".
|
Sammle die Faktenbasis für einen OnePager — ein Übersichtsblatt auf einer Seite — zum Projekt "{topic}".
|
||||||
|
|
||||||
{source}
|
{source}
|
||||||
|
|
||||||
Erfasse gezielt diese Dimensionen:
|
Erfasse gezielt diese Dimensionen:
|
||||||
1. Definition: Was ist "{topic}" in 1–2 Sätzen (Art des Projekts, Gegenstand)?
|
1. Kurzbeschreibung: Was ist "{topic}" in 1–2 Sätzen (Art des Projekts, Gegenstand)?
|
||||||
2. Problem: Welches Problem löst es, für wen ist es gedacht?
|
2. Technische Daten: Technologie/Format, Umfang (Dateien/Seiten/Module), Stand/Aktualität.
|
||||||
3. Abgrenzung: Was deckt das Projekt ab, was ausdrücklich nicht?
|
3. Kerneigenschaften: die prägenden Konzepte, Komponenten oder Inhalte des Projekts.
|
||||||
4. Einordnung: In welchem Kontext steht es (Umfeld, Abhängigkeiten, angrenzende Systeme/Themen)?
|
4. Beispiel: ein typisches, konkretes Beispiel aus dem Projekt (zentraler Code-Flow bzw. Kerninhalt).
|
||||||
5. Anschauung: Ein typisches, konkretes Beispiel aus dem Projekt (zentraler Code-Flow bzw. Kerninhalt).
|
5. Zusammenhänge: in welchem Umfeld es steht (Abhängigkeiten, angrenzende Systeme/Themen).
|
||||||
6. Fakten: Technologie/Format, Umfang (Dateien/Seiten/Module), Stand/Aktualität.
|
6. Voraussetzungen: was man können oder haben muss, um es zu nutzen bzw. zu verstehen.
|
||||||
7. Einstieg: Wo fängt man an — wie startet man es bzw. was liest man zuerst?
|
7. Moderne vs. veraltete Teile: was aktueller Stand ist — und was als Altlast gilt (falls nichts veraltet ist, ausdrücklich notieren).
|
||||||
|
|
||||||
Schreibe NUR die Markdown-Datei nach: {out_path}
|
Schreibe NUR die Markdown-Datei nach: {out_path}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
Sammle die Faktenbasis für einen OnePager — ein Einordnungs- und Entscheidungsdokument — zum Thema "{topic}".
|
Sammle die Faktenbasis für einen OnePager — ein Übersichtsblatt auf einer Seite — zum Thema "{topic}".
|
||||||
|
|
||||||
{source}
|
{source}
|
||||||
|
|
||||||
Recherchiere gezielt diese Dimensionen:
|
Recherchiere gezielt diese Dimensionen:
|
||||||
1. Definition: Was ist "{topic}" in 1–2 Sätzen?
|
1. Kurzbeschreibung: Was ist "{topic}" in 1–2 Sätzen?
|
||||||
2. Problem: Welches Problem löst es, wer braucht es?
|
2. Technische Daten: Art/Typ, aktuelle Version/Stand, Lizenz/Kosten, Verbreitung.
|
||||||
3. Abgrenzung: Wofür ist es geeignet, wofür ausdrücklich nicht?
|
3. Kerneigenschaften: die prägenden Eigenschaften und Merkmale des Systems.
|
||||||
4. Einordnung: Die wichtigsten Alternativen/Nachbarn und wie sich "{topic}" davon unterscheidet.
|
4. Beispiel: ein minimales, typisches Code-/Anwendungsbeispiel.
|
||||||
5. Anschauung: Ein minimales, typisches Code-/Anwendungsbeispiel.
|
5. Zusammenhänge: mit welchen Systemen/Themen es zusammenhängt (Ökosystem, Nachbarn, typische Kombinationen).
|
||||||
6. Fakten: Aktuelle Version, Reife/Alter, Lizenz/Kosten, Verbreitung.
|
6. Voraussetzungen: was man vorher können oder haben muss.
|
||||||
7. Einstieg: Wie fängt man an (Installation/erster Schritt)?
|
7. Moderne vs. veraltete Features: was heute verwendet wird — und was es noch gibt, aber nicht mehr verwendet werden sollte (falls nichts veraltet ist, ausdrücklich notieren).
|
||||||
|
|
||||||
Schreibe NUR die Markdown-Datei nach: {out_path}
|
Schreibe NUR die Markdown-Datei nach: {out_path}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
Verifiziere einen OnePager zum Thema "{topic}" gegen seine Faktenbasis.
|
Prüfe einen OnePager zum Thema "{topic}" gegen seine Faktenbasis.
|
||||||
|
|
||||||
FAKTENBASIS:
|
FAKTENBASIS:
|
||||||
{recherche}
|
{recherche}
|
||||||
@@ -7,13 +7,15 @@ ONEPAGER-KARTEN:
|
|||||||
{karten}
|
{karten}
|
||||||
|
|
||||||
Prüfe:
|
Prüfe:
|
||||||
1. Sind alle 7 Pflicht-Karten vorhanden und vollständig ausgefüllt (keine abgebrochenen oder leeren Inhalte)? — "Was ist {topic}?", "Welches Problem löst es?", "Wann nehmen — wann nicht?", "Einordnung & Alternativen", "So sieht es aus", "Fakten", "Erste Schritte"
|
1. Sind alle 7 Karten vollständig ausgefüllt (keine abgebrochenen oder leeren Inhalte, keine Platzhalter)?
|
||||||
2. Stimmen alle Aussagen mit der Faktenbasis überein? Nichts Erfundenes?
|
2. Stimmen alle Aussagen mit der Faktenbasis überein? Nichts Erfundenes?
|
||||||
3. Beantwortet der OnePager die Leserfrage „Ist das das, was ich suche?" — ist die Abgrenzung konkret genug?
|
3. Ist jede Karte kompakt und für sich verständlich? Ist das Beispiel ein lauffähig plausibler Codeblock?
|
||||||
|
|
||||||
|
Du PRÜFST nur und notierst Probleme — du änderst nichts. Nenne die betroffene Karte über ihren Schlüssel (info, eigenschaften, beispiel, zusammenhaenge, voraussetzungen, modern, veraltet).
|
||||||
|
|
||||||
Schreibe NUR die JSON-Datei nach: {out_path}
|
Schreibe NUR die JSON-Datei nach: {out_path}
|
||||||
|
|
||||||
Format — alles in Ordnung:
|
Format — alles in Ordnung:
|
||||||
{{"ok": true}}
|
{{"ok": true}}
|
||||||
Sonst die vollständige, korrigierte Karten-Liste:
|
Sonst (kurz und konkret, maximal 10 Punkte):
|
||||||
{{"karten": [{{"titel": "…", "merksatz": "…"}}]}}
|
{{"probleme": ["beispiel: …", "fakten in info veraltet: …"]}}
|
||||||
|
|||||||
Reference in New Issue
Block a user