update
This commit is contained in:
@@ -12,20 +12,31 @@ die Gesamtliste.
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
import math
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from agents import kill_process
|
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
|
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
|
from paths import arbeit_dir, bausteine_path, project_dir, subbausteine_path
|
||||||
from pipeline import (
|
from pipeline import (
|
||||||
CANCELLED, FAILED, GenContext, _extra, _log, _prompt, _race, _rest_schema,
|
CANCELLED, FAILED, GenContext, _extra, _log, _prompt, _race, _rest_schema,
|
||||||
_runde_schema, _semaphore, _str_liste, _timeout, run_single_slot,
|
_runde_schema, _semaphore, _str_liste, _stufen_schema, _timeout, run_single_slot,
|
||||||
)
|
)
|
||||||
from textkit import _eindeutige_titel, _parse_auswahl, _titel_aufloesen, _titel_index, _vormerge
|
from textkit import (
|
||||||
|
_eindeutige_titel, _norm_titel, _parse_auswahl, _parse_subbausteine, _titel,
|
||||||
|
_titel_aufloesen, _titel_index, _vormerge,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Subbausteine + Stufen entstehen pro Baustein → wie die Writer chunken:
|
||||||
|
# 1 Agent je ~30 Bausteine, gedeckelt.
|
||||||
|
SUBBAUSTEIN_CHUNK = 30
|
||||||
|
SUBBAUSTEIN_MAX = 20
|
||||||
|
# Einstufen ist billig (kurzes Urteil, keine Websuche) → größere Pakete, weniger Dateien/Agenten.
|
||||||
|
STUFE_CHUNK = 100
|
||||||
|
|
||||||
log = logging.getLogger("creator.bausteine")
|
log = logging.getLogger("creator.bausteine")
|
||||||
|
|
||||||
@@ -34,14 +45,31 @@ _bausteine_errors: dict[str, str] = {}
|
|||||||
_bausteine_cancelled: set[str] = set()
|
_bausteine_cancelled: set[str] = set()
|
||||||
_bausteine_step: dict[str, int] = {}
|
_bausteine_step: dict[str, int] = {}
|
||||||
|
|
||||||
BAUSTEINE_STEPS = ("Recherche", "Konsolidierung", "Klärung")
|
BAUSTEINE_STEPS = (
|
||||||
|
"Recherche", "Konsolidierung", "Klärung",
|
||||||
|
"Subbausteine finden", "Subbausteine wählen", "Subbausteine klären",
|
||||||
|
"Stufen finden", "Stufen wählen", "Stufen klären",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _bausteine_steps(topic: str) -> tuple:
|
def _bausteine_steps(topic: str) -> tuple:
|
||||||
"""Projekte haben einen zusätzlichen Schritt: Themenfeld-Ergänzung per Web-Recherche."""
|
"""Projekte haben einen zusätzlichen Schritt (Ergänzung), nach der Klärung eingefügt.
|
||||||
|
|
||||||
|
Subbausteine + Stufen sind je drei Phasen (Finden, Wählen, Klären). Pro Phase
|
||||||
|
laufen alle Pakete parallel; der Schritt bleibt, bis das letzte Paket fertig ist.
|
||||||
|
"""
|
||||||
|
base = ("Recherche", "Konsolidierung", "Klärung")
|
||||||
|
rest = (
|
||||||
|
"Subbausteine finden", "Subbausteine wählen", "Subbausteine klären",
|
||||||
|
"Stufen finden", "Stufen wählen", "Stufen klären",
|
||||||
|
)
|
||||||
if project_dir(topic).is_dir():
|
if project_dir(topic).is_dir():
|
||||||
return BAUSTEINE_STEPS + ("Ergänzung",)
|
return base + ("Ergänzung",) + rest
|
||||||
return BAUSTEINE_STEPS
|
return base + rest
|
||||||
|
|
||||||
|
|
||||||
|
def _step_idx(topic: str, name: str) -> int:
|
||||||
|
return _bausteine_steps(topic).index(name)
|
||||||
|
|
||||||
|
|
||||||
def _bausteine_files(topic: str) -> dict:
|
def _bausteine_files(topic: str) -> dict:
|
||||||
@@ -55,14 +83,20 @@ def _bausteine_files(topic: str) -> dict:
|
|||||||
"auswahl": {n: [arbeit / f"auswahl-r{n}-{i}.json" for i in (1, 2, 3)] for n in runden},
|
"auswahl": {n: [arbeit / f"auswahl-r{n}-{i}.json" for i in (1, 2, 3)] for n in runden},
|
||||||
"mapping": {n: arbeit / f"auswahl-mapping-r{n}.json" for n in runden},
|
"mapping": {n: arbeit / f"auswahl-mapping-r{n}.json" for n in runden},
|
||||||
"ergaenzung": arbeit / "ergaenzung.json",
|
"ergaenzung": arbeit / "ergaenzung.json",
|
||||||
|
"sub_roh": arbeit / "subbausteine-roh.json",
|
||||||
|
"sidecar": subbausteine_path(topic),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _alle_slot_dateien(files: dict) -> list[Path]:
|
def _alle_slot_dateien(files: dict) -> list[Path]:
|
||||||
|
arbeit = files["arbeit"]
|
||||||
|
# Subbaustein-/Stufen-Slots sind pro Chunk dynamisch — per Glob einsammeln.
|
||||||
|
dyn = (list(arbeit.glob("subbaustein-*")) + list(arbeit.glob("stufe-*"))) 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,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -89,8 +123,12 @@ def _resume_step(topic: str) -> int:
|
|||||||
if not geklaert:
|
if not geklaert:
|
||||||
return 2
|
return 2
|
||||||
if project_dir(topic).is_dir() and not files["ergaenzung"].exists():
|
if project_dir(topic).is_dir() and not files["ergaenzung"].exists():
|
||||||
return 3
|
return _step_idx(topic, "Ergänzung")
|
||||||
return len(_bausteine_steps(topic))
|
if _sidecar_schema(_json_datei(files["sidecar"])) is not None:
|
||||||
|
return len(_bausteine_steps(topic))
|
||||||
|
if _sub_roh_schema(_json_datei(files["sub_roh"])) is None:
|
||||||
|
return _step_idx(topic, "Subbausteine finden")
|
||||||
|
return _step_idx(topic, "Stufen finden")
|
||||||
|
|
||||||
|
|
||||||
def bausteine_status(topic: str) -> dict:
|
def bausteine_status(topic: str) -> dict:
|
||||||
@@ -127,6 +165,7 @@ def active_bausteine() -> list[dict]:
|
|||||||
def reset_bausteine(topic: str) -> None:
|
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/
|
||||||
shutil.rmtree(files["arbeit"], ignore_errors=True)
|
shutil.rmtree(files["arbeit"], ignore_errors=True)
|
||||||
_bausteine_errors.pop(topic, None)
|
_bausteine_errors.pop(topic, None)
|
||||||
|
|
||||||
@@ -200,6 +239,307 @@ def _mapping_schema(data):
|
|||||||
return bausteine, rest
|
return bausteine, rest
|
||||||
|
|
||||||
|
|
||||||
|
def _sub_roh_schema(data):
|
||||||
|
"""{Baustein-Titel: [Subbaustein, …]} → dict · sonst None (Zwischenstand Block B)."""
|
||||||
|
if not isinstance(data, dict) or not data:
|
||||||
|
return None
|
||||||
|
out: dict[str, list[str]] = {}
|
||||||
|
for k, v in data.items():
|
||||||
|
subs = _str_liste(v) if isinstance(v, list) else None
|
||||||
|
if not isinstance(k, str) or not k.strip() or not subs:
|
||||||
|
return None
|
||||||
|
out[k] = subs
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _sidecar_schema(data):
|
||||||
|
"""{Baustein-Titel: [{titel, stufe}, …]} → dict · sonst None (finale Sidecar)."""
|
||||||
|
if not isinstance(data, dict) or not data:
|
||||||
|
return None
|
||||||
|
for v in data.values():
|
||||||
|
if not isinstance(v, list) or not v:
|
||||||
|
return None
|
||||||
|
for s in v:
|
||||||
|
if not isinstance(s, dict) or not str(s.get("titel", "")).strip() or s.get("stufe") not in ("einfach", "mittel", "schwer"):
|
||||||
|
return None
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def _read(p: Path) -> str:
|
||||||
|
return p.read_text(encoding="utf-8") if p.exists() else ""
|
||||||
|
|
||||||
|
|
||||||
|
def _chunk_nums(items: list, n: int) -> list[list]:
|
||||||
|
"""Teilt eine flache Liste in n möglichst gleich große Chunks."""
|
||||||
|
n = max(1, n)
|
||||||
|
size = max(1, math.ceil(len(items) / n))
|
||||||
|
return [items[i:i + size] for i in range(0, len(items), size)]
|
||||||
|
|
||||||
|
|
||||||
|
def _n_chunks(count: int, size: int = SUBBAUSTEIN_CHUNK) -> int:
|
||||||
|
return min(SUBBAUSTEIN_MAX, max(1, math.ceil(count / size)))
|
||||||
|
|
||||||
|
|
||||||
|
def _merge_finder(num: int, idx: dict, finder: list[dict]) -> tuple[list[str], list[str]]:
|
||||||
|
"""Subbausteine eines Bausteins über die Finder mergen → (konsens ≥2, rest ==1)."""
|
||||||
|
counts: dict[str, int] = {}
|
||||||
|
repr_text: dict[str, str] = {}
|
||||||
|
order: list[str] = []
|
||||||
|
for d in finder:
|
||||||
|
# die Marker-Liste dieses Finders für genau diesen Baustein
|
||||||
|
subs = next((s for marker, s in d.items() if _titel_aufloesen(idx, marker) == num), [])
|
||||||
|
gesehen: set[str] = set()
|
||||||
|
for sub in subs:
|
||||||
|
key = _norm_titel(sub)
|
||||||
|
if not key or key in gesehen:
|
||||||
|
continue
|
||||||
|
gesehen.add(key)
|
||||||
|
if key not in counts:
|
||||||
|
counts[key], repr_text[key] = 0, sub
|
||||||
|
order.append(key)
|
||||||
|
counts[key] += 1
|
||||||
|
konsens = [repr_text[k] for k in order if counts[k] >= 2]
|
||||||
|
rest = [repr_text[k] for k in order if counts[k] == 1]
|
||||||
|
return konsens, rest
|
||||||
|
|
||||||
|
|
||||||
|
def _final_text(chunk: list[int], entries: dict, daten: dict) -> str:
|
||||||
|
"""Marker-Datei aus reinem Konsens (wenn kein Judge nötig)."""
|
||||||
|
teile = []
|
||||||
|
for num in chunk:
|
||||||
|
konsens = daten[num][0]
|
||||||
|
if konsens:
|
||||||
|
teile.append(f"<!-- baustein: {_titel(entries[num])} -->\n" + "\n".join(f"- {s}" for s in konsens))
|
||||||
|
return "\n".join(teile) + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def _judge_block(chunk: list[int], entries: dict, daten: dict) -> str:
|
||||||
|
"""Eingabe für den Subbaustein-Judge: pro Baustein Konsens + Strittiges."""
|
||||||
|
lines = []
|
||||||
|
for num in chunk:
|
||||||
|
konsens, rest = daten[num]
|
||||||
|
lines.append(f"BAUSTEIN: {_titel(entries[num])}")
|
||||||
|
lines.append("Konsens:")
|
||||||
|
lines.extend(f"- {s}" for s in konsens)
|
||||||
|
if not konsens:
|
||||||
|
lines.append("- (noch keiner)")
|
||||||
|
if rest:
|
||||||
|
lines.append("Strittig:")
|
||||||
|
lines.extend(f"- {s}" for s in rest)
|
||||||
|
lines.append("")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
async def _subbausteine_block(ctx: GenContext, set_p, files: dict, entries: dict, instructions: str) -> dict | None:
|
||||||
|
"""Block B: drei Phasen mit Barriere — Finden, Wählen (Code-Merge), Klären.
|
||||||
|
Pro Phase laufen alle Pakete parallel; der Schritt bleibt, bis das letzte fertig ist.
|
||||||
|
→ {Baustein-Titel: [Subbaustein, …]} oder None bei Abbruch/Fehler."""
|
||||||
|
topic, provider, is_cancelled = ctx.topic, ctx.provider, ctx.is_cancelled
|
||||||
|
arbeit = files["arbeit"]
|
||||||
|
idx = _titel_index(entries)
|
||||||
|
caps = "files" if project_dir(topic).is_dir() else "full"
|
||||||
|
nums = list(entries)
|
||||||
|
chunks = _chunk_nums(nums, _n_chunks(len(nums)))
|
||||||
|
n = len(chunks)
|
||||||
|
|
||||||
|
def finder_paths(c):
|
||||||
|
return [arbeit / f"subbaustein-c{c}-{i}.md" for i in (1, 2, 3)]
|
||||||
|
|
||||||
|
def final_path(c):
|
||||||
|
return arbeit / f"subbaustein-final-c{c}.md"
|
||||||
|
|
||||||
|
# Phase „Subbausteine finden": pro Paket 3 Finder (min. 2), alle Pakete parallel.
|
||||||
|
async def _finde(c, chunk):
|
||||||
|
paths = finder_paths(c)
|
||||||
|
vorhanden = sum(1 for p in paths if _parse_subbausteine(_read(p)))
|
||||||
|
if vorhanden >= 2:
|
||||||
|
return True
|
||||||
|
zuteilung = "\n".join(f"- {entries[num]}" for num in chunk)
|
||||||
|
offen = [(i, p) for i, p in enumerate(paths, 1) if not _parse_subbausteine(_read(p))]
|
||||||
|
slots = [{
|
||||||
|
"key": f"bausteine-{topic}-subbaustein-c{c}-{i}",
|
||||||
|
"prompt": _prompt("Subbaustein-Recherche", topic=topic, zuteilung=zuteilung, out_path=p, extra=_extra(instructions)),
|
||||||
|
"role": "quick", "capabilities": caps,
|
||||||
|
"payload": (lambda result, p=p: _parse_subbausteine(_read(p)) or None),
|
||||||
|
} for i, p in offen]
|
||||||
|
neu = await _race(topic, f"Subbausteine Paket {c}", slots, 2 - vorhanden, _timeout("subbaustein", len(chunk)), provider, cancelled=is_cancelled, grace=KONSENS_GRACE)
|
||||||
|
return not is_cancelled() and neu is not None
|
||||||
|
|
||||||
|
set_p(f"Subbausteine finden ({n} Pakete)…", step=_step_idx(topic, "Subbausteine finden"))
|
||||||
|
oks = await asyncio.gather(*[_finde(c, chunk) for c, chunk in enumerate(chunks, 1)], return_exceptions=True)
|
||||||
|
if is_cancelled():
|
||||||
|
return None
|
||||||
|
if not all(ok is True for ok in oks):
|
||||||
|
_bausteine_errors[topic] = "Subbausteine fehlgeschlagen (Recherche)"
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Phase „Subbausteine wählen": Code-Merge je Paket (instant, kein Agent).
|
||||||
|
set_p(f"Subbausteine wählen ({n} Pakete)…", step=_step_idx(topic, "Subbausteine wählen"))
|
||||||
|
daten_by_c = {}
|
||||||
|
for c, chunk in enumerate(chunks, 1):
|
||||||
|
finder = [d for p in finder_paths(c) if (d := _parse_subbausteine(_read(p)))]
|
||||||
|
daten_by_c[c] = {num: _merge_finder(num, idx, finder) for num in chunk}
|
||||||
|
|
||||||
|
# Phase „Subbausteine klären": Judge je Paket mit Strittigem, alle parallel.
|
||||||
|
async def _klaere(c, chunk):
|
||||||
|
daten = daten_by_c[c]
|
||||||
|
fp = final_path(c)
|
||||||
|
if _parse_subbausteine(_read(fp)):
|
||||||
|
return
|
||||||
|
if not any(daten[num][1] for num in chunk):
|
||||||
|
atomic_write_text(fp, _final_text(chunk, entries, daten))
|
||||||
|
return
|
||||||
|
status, _ = await run_single_slot(
|
||||||
|
ctx, f"Subbaustein-Klärung {c}",
|
||||||
|
key=f"bausteine-{topic}-subbaustein-final-c{c}",
|
||||||
|
prompt=_prompt("Subbaustein-Mapping", topic=topic, bausteine=_judge_block(chunk, entries, daten), out_path=fp, extra=_extra(instructions)),
|
||||||
|
role="judge", capabilities="files",
|
||||||
|
payload=lambda result, p=fp: _parse_subbausteine(_read(p)) or None,
|
||||||
|
timeout=_timeout("subbaustein_check", len(chunk)),
|
||||||
|
)
|
||||||
|
if status == FAILED:
|
||||||
|
_log(topic, f"Subbaustein-Klärung Paket {c} fehlgeschlagen — nur Konsens übernommen")
|
||||||
|
|
||||||
|
set_p(f"Subbausteine klären ({n} Pakete)…", step=_step_idx(topic, "Subbausteine klären"))
|
||||||
|
await asyncio.gather(*[_klaere(c, chunk) for c, chunk in enumerate(chunks, 1)], return_exceptions=True)
|
||||||
|
if is_cancelled():
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Fehlende finale Dateien → Konsens-Fallback; dann alle parsen.
|
||||||
|
roh: dict[str, list[str]] = {}
|
||||||
|
for c, chunk in enumerate(chunks, 1):
|
||||||
|
fp = final_path(c)
|
||||||
|
if not _parse_subbausteine(_read(fp)):
|
||||||
|
atomic_write_text(fp, _final_text(chunk, entries, daten_by_c[c]))
|
||||||
|
for marker, subs in (_parse_subbausteine(_read(fp)) or {}).items():
|
||||||
|
num = _titel_aufloesen(idx, marker)
|
||||||
|
if num is not None:
|
||||||
|
roh[_titel(entries[num])] = subs
|
||||||
|
if not roh:
|
||||||
|
_bausteine_errors[topic] = "Keine Subbausteine ermittelt"
|
||||||
|
return None
|
||||||
|
return roh
|
||||||
|
|
||||||
|
|
||||||
|
async def _stufen_block(ctx: GenContext, set_p, files: dict, roh: dict, instructions: str) -> dict | None:
|
||||||
|
"""Block C: drei Phasen mit Barriere — Finden (einstufen), Wählen (Vote), Klären.
|
||||||
|
Lokale IDs 1..n pro Paket, hinterher auf globale gid gemappt.
|
||||||
|
→ {Baustein-Titel: [{titel, stufe}, …]} oder None."""
|
||||||
|
topic, provider, is_cancelled = ctx.topic, ctx.provider, ctx.is_cancelled
|
||||||
|
arbeit = files["arbeit"]
|
||||||
|
items = [(titel, sub) for titel, subs in roh.items() for sub in subs] # globale id = index+1
|
||||||
|
if not items:
|
||||||
|
return {titel: [] for titel in roh}
|
||||||
|
chunks = _chunk_nums(list(range(len(items))), _n_chunks(len(items), STUFE_CHUNK))
|
||||||
|
n = len(chunks)
|
||||||
|
|
||||||
|
def rater_paths(c):
|
||||||
|
return [arbeit / f"stufe-c{c}-{i}.json" for i in (1, 2, 3)]
|
||||||
|
|
||||||
|
def lset(item_idxs):
|
||||||
|
return set(range(1, len(item_idxs) + 1))
|
||||||
|
|
||||||
|
# Phase „Stufen finden": pro Paket 3 Rater (min. 2), lokale IDs.
|
||||||
|
async def _rate(c, item_idxs):
|
||||||
|
local_set = lset(item_idxs)
|
||||||
|
paths = rater_paths(c)
|
||||||
|
vorhanden = sum(1 for p in paths if _stufen_schema(_json_datei(p), local_set))
|
||||||
|
if vorhanden >= 2:
|
||||||
|
return True
|
||||||
|
enum = "\n".join(f"{k}. [{items[j][0]}] {items[j][1]}" for k, j in enumerate(item_idxs, 1))
|
||||||
|
offen = [(i, p) for i, p in enumerate(paths, 1) if not _stufen_schema(_json_datei(p), local_set)]
|
||||||
|
slots = [{
|
||||||
|
"key": f"bausteine-{topic}-stufe-c{c}-{i}",
|
||||||
|
"prompt": _prompt("Stufen-Recherche", topic=topic, subbausteine=enum, out_path=p, extra=_extra(instructions)),
|
||||||
|
"role": "fast", "capabilities": "files",
|
||||||
|
"payload": (lambda result, p=p, ids=local_set: _stufen_schema(_json_datei(p), ids)),
|
||||||
|
} for i, p in offen]
|
||||||
|
neu = await _race(topic, f"Stufen Paket {c}", slots, 2 - vorhanden, _timeout("stufe", len(item_idxs)), provider, cancelled=is_cancelled, grace=KONSENS_GRACE)
|
||||||
|
return not is_cancelled() and neu is not None
|
||||||
|
|
||||||
|
set_p(f"Stufen finden ({n} Pakete)…", step=_step_idx(topic, "Stufen finden"))
|
||||||
|
oks = await asyncio.gather(*[_rate(c, idxs) for c, idxs in enumerate(chunks, 1)], return_exceptions=True)
|
||||||
|
if is_cancelled():
|
||||||
|
return None
|
||||||
|
if not all(ok is True for ok in oks):
|
||||||
|
_bausteine_errors[topic] = "Einstufung fehlgeschlagen (Recherche)"
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Phase „Stufen wählen": Code-Vote je Paket → (ergebnis, strittig).
|
||||||
|
set_p(f"Stufen wählen ({n} Pakete)…", step=_step_idx(topic, "Stufen wählen"))
|
||||||
|
vote_by_c = {}
|
||||||
|
for c, item_idxs in enumerate(chunks, 1):
|
||||||
|
local_set = lset(item_idxs)
|
||||||
|
rater = [d for p in rater_paths(c) if (d := _stufen_schema(_json_datei(p), local_set))]
|
||||||
|
ergebnis: dict[int, str] = {}
|
||||||
|
strittig: dict[int, list[str]] = {}
|
||||||
|
for k in range(1, len(item_idxs) + 1):
|
||||||
|
stimmen = [d[k] for d in rater if k in d]
|
||||||
|
zaehler: dict[str, int] = {}
|
||||||
|
for s in stimmen:
|
||||||
|
zaehler[s] = zaehler.get(s, 0) + 1
|
||||||
|
best = max(zaehler.values(), default=0)
|
||||||
|
gewinner = [s for s, v in zaehler.items() if v == best]
|
||||||
|
if len(gewinner) == 1 and best >= 2:
|
||||||
|
ergebnis[k] = gewinner[0]
|
||||||
|
else:
|
||||||
|
strittig[k] = stimmen
|
||||||
|
vote_by_c[c] = (ergebnis, strittig)
|
||||||
|
|
||||||
|
# Phase „Stufen klären": Judge je Paket mit Strittigem, alle parallel.
|
||||||
|
async def _klaere(c, item_idxs):
|
||||||
|
ergebnis, strittig = vote_by_c[c]
|
||||||
|
if strittig:
|
||||||
|
judge_path = arbeit / f"stufe-final-c{c}.json"
|
||||||
|
entsch = _stufen_schema(_json_datei(judge_path), set(strittig))
|
||||||
|
if entsch is None:
|
||||||
|
strittig_block = "\n".join(
|
||||||
|
f"{k}. [{items[item_idxs[k - 1]][0]}] {items[item_idxs[k - 1]][1]} — Stimmen: {', '.join(stimmen) or 'keine'}"
|
||||||
|
for k, stimmen in strittig.items()
|
||||||
|
)
|
||||||
|
status, entsch = await run_single_slot(
|
||||||
|
ctx, f"Stufen-Klärung {c}",
|
||||||
|
key=f"bausteine-{topic}-stufe-final-c{c}",
|
||||||
|
prompt=_prompt("Stufen-Mapping", topic=topic, strittig=strittig_block, out_path=judge_path, extra=_extra(instructions)),
|
||||||
|
role="judge", capabilities="files",
|
||||||
|
payload=lambda result, p=judge_path, ids=set(strittig): _stufen_schema(_json_datei(p), ids),
|
||||||
|
timeout=_timeout("stufe_check", len(strittig)),
|
||||||
|
)
|
||||||
|
if status == FAILED:
|
||||||
|
_log(topic, f"Stufen-Klärung Paket {c} fehlgeschlagen — Default 'mittel'")
|
||||||
|
entsch = entsch if isinstance(entsch, dict) else {}
|
||||||
|
# Strittige ohne Entscheid → 'mittel'; Vote-Gewinner bleiben; Judge überschreibt.
|
||||||
|
ergebnis = {**{k: "mittel" for k in strittig}, **ergebnis, **entsch}
|
||||||
|
return {item_idxs[k - 1] + 1: stufe for k, stufe in ergebnis.items()}
|
||||||
|
|
||||||
|
set_p(f"Stufen klären ({n} Pakete)…", step=_step_idx(topic, "Stufen klären"))
|
||||||
|
parts = await asyncio.gather(*[_klaere(c, idxs) for c, idxs in enumerate(chunks, 1)], return_exceptions=True)
|
||||||
|
if is_cancelled():
|
||||||
|
return None
|
||||||
|
stufe_by_id: dict[int, str] = {}
|
||||||
|
for c, part in enumerate(parts, 1):
|
||||||
|
if not isinstance(part, dict):
|
||||||
|
# Klärung ist nicht fatal: Vote-Ergebnis + Default 'mittel' für Strittige.
|
||||||
|
if isinstance(part, BaseException):
|
||||||
|
_log(topic, f"Stufen-Klärung Paket {c}: {type(part).__name__}: {part}")
|
||||||
|
ergebnis, strittig = vote_by_c[c]
|
||||||
|
item_idxs = chunks[c - 1]
|
||||||
|
merged = {**{k: "mittel" for k in strittig}, **ergebnis}
|
||||||
|
part = {item_idxs[k - 1] + 1: s for k, s in merged.items()}
|
||||||
|
stufe_by_id.update(part)
|
||||||
|
|
||||||
|
# Sidecar zusammensetzen — gleiche Reihenfolge wie items → gid stimmt
|
||||||
|
sidecar: dict[str, list[dict]] = {}
|
||||||
|
gid = 0
|
||||||
|
for titel, subs in roh.items():
|
||||||
|
lst = []
|
||||||
|
for sub in subs:
|
||||||
|
gid += 1
|
||||||
|
lst.append({"titel": sub, "stufe": stufe_by_id.get(gid, "mittel")})
|
||||||
|
sidecar[titel] = lst
|
||||||
|
return sidecar
|
||||||
|
|
||||||
|
|
||||||
async def generate_bausteine(topic: str, instructions: str = "", provider: str = DEFAULT_PROVIDER) -> None:
|
async def generate_bausteine(topic: str, instructions: str = "", provider: str = DEFAULT_PROVIDER) -> None:
|
||||||
if topic in _bausteine_progress:
|
if topic in _bausteine_progress:
|
||||||
return
|
return
|
||||||
@@ -228,9 +568,11 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
|
|||||||
files["arbeit"].mkdir(parents=True, exist_ok=True)
|
files["arbeit"].mkdir(parents=True, exist_ok=True)
|
||||||
if project:
|
if project:
|
||||||
await asyncio.to_thread(_pdfs_konvertieren, project)
|
await asyncio.to_thread(_pdfs_konvertieren, project)
|
||||||
# „Neu erstellen": fertige Bausteine → kompletter Frischstart.
|
# „Neu erstellen": NUR wenn wirklich alles fertig ist (bausteine.md UND
|
||||||
# Sonst sind Slot-Dateien Reste eines Abbruchs/Fehlers → Resume.
|
# Sidecar) → kompletter Frischstart. Liegt bausteine.md ohne Sidecar vor,
|
||||||
if final_path.exists():
|
# 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
|
||||||
|
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)
|
||||||
|
|
||||||
@@ -425,6 +767,27 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
|
|||||||
# Titel eindeutig machen und unsortiertes Inventar schreiben
|
# Titel eindeutig machen und unsortiertes Inventar schreiben
|
||||||
entries = _eindeutige_titel(entries)
|
entries = _eindeutige_titel(entries)
|
||||||
atomic_write_text(final_path, "\n".join(f"{i}. {t}" for i, t in entries.items()) + "\n")
|
atomic_write_text(final_path, "\n".join(f"{i}. {t}" for i, t in entries.items()) + "\n")
|
||||||
|
|
||||||
|
# Block B + C: Subbausteine je Baustein + Stufen → Sidecar subbausteine.json.
|
||||||
|
# Nicht-destruktiv: bausteine.md steht schon; fehlt die Sidecar, wird beim
|
||||||
|
# nächsten Lauf nur dieser Teil neu versucht. Guide fällt ohne Sidecar zurück.
|
||||||
|
if _sidecar_schema(_json_datei(files["sidecar"])) is None:
|
||||||
|
roh = _sub_roh_schema(_json_datei(files["sub_roh"]))
|
||||||
|
if roh is None:
|
||||||
|
roh = await _subbausteine_block(ctx, set_p, files, entries, instructions)
|
||||||
|
if is_cancelled():
|
||||||
|
abgebrochen()
|
||||||
|
return
|
||||||
|
if roh is None:
|
||||||
|
return # Fehler ist gesetzt
|
||||||
|
atomic_write_json(files["sub_roh"], roh, indent=1)
|
||||||
|
sidecar = await _stufen_block(ctx, set_p, files, roh, instructions)
|
||||||
|
if is_cancelled():
|
||||||
|
abgebrochen()
|
||||||
|
return
|
||||||
|
if sidecar is None:
|
||||||
|
return
|
||||||
|
atomic_write_json(files["sidecar"], sidecar, 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]
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ TIMEOUTS = {
|
|||||||
"plan_judge": (600, 5), # Judge liest bis zu 5 Gliederungen, n = Sections
|
"plan_judge": (600, 5), # Judge liest bis zu 5 Gliederungen, n = Sections
|
||||||
"inhalt": (600, 90), # Inhalte je Baustein im Chunk identifizieren (Websuche)
|
"inhalt": (600, 90), # Inhalte je Baustein im Chunk identifizieren (Websuche)
|
||||||
"inhalt_check": (300, 10), # Inhalts-Prüfung je Baustein im Paket
|
"inhalt_check": (300, 10), # Inhalts-Prüfung je Baustein im Paket
|
||||||
|
"subbaustein": (900, 45), # Subbausteine je Baustein im Chunk finden (Websuche)
|
||||||
|
"subbaustein_check": (300, 15), # Judge entscheidet strittige Subbausteine im Chunk
|
||||||
|
"stufe": (300, 10), # Subbausteine einstufen je Chunk
|
||||||
|
"stufe_check": (300, 10), # Judge entscheidet strittige Stufen im Chunk
|
||||||
"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),
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ from database import list_guides, update_guide
|
|||||||
from fsutil import atomic_write_json
|
from fsutil import atomic_write_json
|
||||||
from jsonio import read_json_file as _json_datei
|
from jsonio import read_json_file as _json_datei
|
||||||
from onepager import _generate_onepager
|
from onepager import _generate_onepager
|
||||||
from paths import bausteine_path, guide_content_path, project_dir
|
from paths import bausteine_path, guide_content_path, project_dir, subbausteine_path
|
||||||
from pipeline import (
|
from pipeline import (
|
||||||
CANCELLED, FAILED, GenContext, _claude_error, _extra,
|
CANCELLED, FAILED, GenContext, _claude_error, _extra,
|
||||||
_fail, _gather_error, _log, _prompt, _race, _rest_schema, _runde_schema,
|
_fail, _gather_error, _log, _prompt, _race, _rest_schema, _runde_schema,
|
||||||
@@ -47,6 +47,41 @@ WRITER_SECTIONS = 30
|
|||||||
WRITER_MAX = 20
|
WRITER_MAX = 20
|
||||||
|
|
||||||
|
|
||||||
|
def _load_subbausteine(topic: str) -> dict[str, list[dict]]:
|
||||||
|
"""Sidecar laden: {Baustein-Titel: [{titel, stufe}, …]}. Fehlt sie → {} (Fallback)."""
|
||||||
|
data = _json_datei(subbausteine_path(topic))
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return {}
|
||||||
|
out: dict[str, list[dict]] = {}
|
||||||
|
for titel, subs in data.items():
|
||||||
|
if not isinstance(subs, list):
|
||||||
|
continue
|
||||||
|
gut = [s for s in subs if isinstance(s, dict) and str(s.get("titel", "")).strip()
|
||||||
|
and s.get("stufe") in ("einfach", "mittel", "schwer")]
|
||||||
|
if gut:
|
||||||
|
out[titel] = gut
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _section_markup(sec: dict) -> str:
|
||||||
|
"""Section-Text mit Sub-Markern (für Lese-Check/Fix), damit die Struktur erhalten bleibt."""
|
||||||
|
if not sec.get("subs"):
|
||||||
|
return sec["md"]
|
||||||
|
return "\n\n".join(f"<!-- sub: {s['stufe']} | {s['titel']} -->\n{s['md']}" for s in sec["subs"])
|
||||||
|
|
||||||
|
|
||||||
|
def _zuteilung_subs(chunk: list[dict], entries: dict[int, str], subs_by_titel: dict[str, list[dict]]) -> str:
|
||||||
|
"""Wie _zuteilung_text, aber listet pro Baustein seine Subbausteine mit Stufe."""
|
||||||
|
lines: list[str] = []
|
||||||
|
for ch in chunk:
|
||||||
|
lines.append(f"KAPITEL: {ch['title']}")
|
||||||
|
for num in ch["nums"]:
|
||||||
|
lines.append(f"- {entries[num]}")
|
||||||
|
for s in subs_by_titel.get(_titel(entries[num]), []):
|
||||||
|
lines.append(f" [{s['stufe']}] {s['titel']}")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
def _guide_files(content_path: Path) -> dict:
|
def _guide_files(content_path: Path) -> dict:
|
||||||
d, stem = content_path.parent, content_path.stem
|
d, stem = content_path.parent, content_path.stem
|
||||||
runden = range(1, KONSENS_MAX_RUNDEN + 1)
|
runden = range(1, KONSENS_MAX_RUNDEN + 1)
|
||||||
@@ -459,7 +494,10 @@ async def _generate_sections(
|
|||||||
# dieselbe Aufteilung: 1 Agent je ~30 Bausteine (gedeckelt).
|
# dieselbe Aufteilung: 1 Agent je ~30 Bausteine (gedeckelt).
|
||||||
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]
|
# Subbausteine je Baustein (Sidecar). Fehlt sie → leer → Inhalte/Writer laufen
|
||||||
|
# auf Baustein-Granularität weiter (Fallback für Alt-Themen ohne Sidecar).
|
||||||
|
subs_by_titel = _load_subbausteine(topic)
|
||||||
|
zuteilungen = [_zuteilung_subs(chunk, entries, subs_by_titel) for chunk in chunks]
|
||||||
chunk_sizes = [sum(len(c["nums"]) for c in chunk) for chunk in chunks]
|
chunk_sizes = [sum(len(c["nums"]) for c in chunk) for chunk in chunks]
|
||||||
writer_count = len(chunks)
|
writer_count = len(chunks)
|
||||||
idx = _titel_index(entries)
|
idx = _titel_index(entries)
|
||||||
@@ -616,11 +654,11 @@ async def _generate_sections(
|
|||||||
chunk_nums = [[num for ch in chunk for num in ch["nums"] if num in by_num] for chunk in chunks]
|
chunk_nums = [[num for ch in chunk for num in ch["nums"] if num in by_num] for chunk in chunks]
|
||||||
|
|
||||||
def sections_text(nums: list[int]) -> str:
|
def sections_text(nums: list[int]) -> str:
|
||||||
return "\n\n".join(f"SECTION: {_titel(entries[num])}\n{by_num[num]['md']}" for num in nums)
|
return "\n\n".join(f"SECTION: {_titel(entries[num])}\n{_section_markup(by_num[num])}" for num in nums)
|
||||||
|
|
||||||
def auftraege_text(nums: list[int], probleme: dict[int, str]) -> str:
|
def auftraege_text(nums: list[int], probleme: dict[int, str]) -> str:
|
||||||
return "\n\n".join(
|
return "\n\n".join(
|
||||||
f"SECTION: {_titel(entries[num])}\nPROBLEM: {probleme[num]}\nAKTUELLER INHALT:\n{by_num[num]['md']}"
|
f"SECTION: {_titel(entries[num])}\nPROBLEM: {probleme[num]}\nAKTUELLER INHALT:\n{_section_markup(by_num[num])}"
|
||||||
for num in nums
|
for num in nums
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -702,12 +740,16 @@ async def _generate_sections(
|
|||||||
scope = [[num for num in nums if num in ersetzt] for nums in chunk_nums]
|
scope = [[num for num in nums if num in ersetzt] for nums in chunk_nums]
|
||||||
|
|
||||||
await _set_progress(guide_id, "Setze zusammen…")
|
await _set_progress(guide_id, "Setze zusammen…")
|
||||||
|
def _section(num: int) -> dict:
|
||||||
|
sec = by_num[num]
|
||||||
|
d = {"num": num, "title": _titel(entries[num]), "md": sec["md"]}
|
||||||
|
if sec.get("subs"):
|
||||||
|
d["subbausteine"] = [{"stufe": s["stufe"], "titel": s["titel"], "md": s["md"]} for s in sec["subs"]]
|
||||||
|
return d
|
||||||
|
|
||||||
chapters: list[dict] = []
|
chapters: list[dict] = []
|
||||||
for ch in plan:
|
for ch in plan:
|
||||||
sections = [
|
sections = [_section(num) for num in ch["nums"] if num in by_num]
|
||||||
{"num": num, "title": _titel(entries[num]), "md": by_num[num]["md"]}
|
|
||||||
for num in ch["nums"] if num in by_num
|
|
||||||
]
|
|
||||||
if sections:
|
if sections:
|
||||||
chapters.append({"title": ch["title"], "sections": sections})
|
chapters.append({"title": ch["title"], "sections": sections})
|
||||||
geplant = {num for ch in plan for num in ch["nums"]}
|
geplant = {num for ch in plan for num in ch["nums"]}
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ def bausteine_path(topic: str) -> Path:
|
|||||||
return topic_dir(topic) / "bausteine.md"
|
return topic_dir(topic) / "bausteine.md"
|
||||||
|
|
||||||
|
|
||||||
|
def subbausteine_path(topic: str) -> Path:
|
||||||
|
"""Sidecar: pro Baustein die Subbausteine mit Stufe (von allen Guides geteilt)."""
|
||||||
|
return topic_dir(topic) / "subbausteine.json"
|
||||||
|
|
||||||
|
|
||||||
def guide_content_path(topic: str, format_name: str) -> Path:
|
def guide_content_path(topic: str, format_name: str) -> Path:
|
||||||
return topic_dir(topic) / "guides" / f"{format_name}.json"
|
return topic_dir(topic) / "guides" / f"{format_name}.json"
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from agents import run_agent, kill_process
|
|||||||
from config import MAX_CONCURRENT_GENERATIONS, TEMPLATES_DIR, TIMEOUTS
|
from config import MAX_CONCURRENT_GENERATIONS, TEMPLATES_DIR, TIMEOUTS
|
||||||
from database import update_guide
|
from database import update_guide
|
||||||
from jsonio import read_json_file as _json_datei
|
from jsonio import read_json_file as _json_datei
|
||||||
|
from textkit import _STUFEN
|
||||||
|
|
||||||
log = logging.getLogger("creator.pipeline")
|
log = logging.getLogger("creator.pipeline")
|
||||||
|
|
||||||
@@ -134,6 +135,29 @@ def _runde_schema(data, final: bool = False):
|
|||||||
return aufnehmen, rest
|
return aufnehmen, rest
|
||||||
|
|
||||||
|
|
||||||
|
def _stufen_schema(data, ids: set[int] | None = None):
|
||||||
|
"""{"stufen": {"1": "einfach", …}} → {id: stufe} · sonst None.
|
||||||
|
|
||||||
|
Stufe ∈ {einfach, mittel, schwer}. Sind `ids` gegeben, müssen mindestens
|
||||||
|
diese abgedeckt sein (Extras erlaubt); der Aufrufer filtert dann auf `ids`.
|
||||||
|
"""
|
||||||
|
if not isinstance(data, dict) or not isinstance(data.get("stufen"), dict) or not data["stufen"]:
|
||||||
|
return None
|
||||||
|
out: dict[int, str] = {}
|
||||||
|
for k, v in data["stufen"].items():
|
||||||
|
try:
|
||||||
|
num = int(k)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return None
|
||||||
|
stufe = str(v).strip().casefold()
|
||||||
|
if stufe not in _STUFEN:
|
||||||
|
return None
|
||||||
|
out[num] = stufe
|
||||||
|
if ids is not None and not ids <= set(out):
|
||||||
|
return None
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
_MAX_RESTARTS = 2
|
_MAX_RESTARTS = 2
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -119,32 +119,81 @@ def _lade_bausteine(text: str) -> dict[int, str]:
|
|||||||
|
|
||||||
_FRAGMENT_KAPITEL_RE = re.compile(r"<!--\s*kapitel\s*:\s*(.*?)\s*-->", re.IGNORECASE)
|
_FRAGMENT_KAPITEL_RE = re.compile(r"<!--\s*kapitel\s*:\s*(.*?)\s*-->", re.IGNORECASE)
|
||||||
_FRAGMENT_SECTION_RE = re.compile(r"<!--\s*section\s*:\s*(.*?)\s*-->", re.IGNORECASE)
|
_FRAGMENT_SECTION_RE = re.compile(r"<!--\s*section\s*:\s*(.*?)\s*-->", re.IGNORECASE)
|
||||||
|
_FRAGMENT_SUB_RE = re.compile(r"<!--\s*sub\s*:\s*(.*?)\s*-->", re.IGNORECASE)
|
||||||
|
_FRAGMENT_BAUSTEIN_RE = re.compile(r"<!--\s*baustein\s*:\s*(.*?)\s*-->", re.IGNORECASE)
|
||||||
|
_STUFEN = ("einfach", "mittel", "schwer")
|
||||||
|
|
||||||
|
|
||||||
def _parse_fragment(text: str) -> list[dict]:
|
def _parse_fragment(text: str) -> list[dict]:
|
||||||
"""Parst eine Writer-Datei → [{"kapitel", "titel", "md"}] in Datei-Reihenfolge."""
|
"""Parst eine Writer-Datei → [{"kapitel", "titel", "md", "subs"}] in Datei-Reihenfolge.
|
||||||
|
|
||||||
|
Optionale Subbaustein-Marker `<!-- sub: stufe | Titel -->` innerhalb einer
|
||||||
|
Section sammeln sich in `sec["subs"] = [{"stufe", "titel", "md"}]`. `sec["md"]`
|
||||||
|
bleibt der vollständige Body (alle Sub-Texte zusammengesetzt) — auch ohne Subs.
|
||||||
|
"""
|
||||||
sections: list[dict] = []
|
sections: list[dict] = []
|
||||||
kapitel = None
|
kapitel = None
|
||||||
current = None
|
current = None
|
||||||
|
cur_sub = None
|
||||||
for line in text.splitlines():
|
for line in text.splitlines():
|
||||||
s = line.strip()
|
s = line.strip()
|
||||||
m = _FRAGMENT_KAPITEL_RE.match(s)
|
m = _FRAGMENT_KAPITEL_RE.match(s)
|
||||||
if m:
|
if m:
|
||||||
kapitel = m.group(1)
|
kapitel = m.group(1)
|
||||||
current = None
|
current = None
|
||||||
|
cur_sub = None
|
||||||
continue
|
continue
|
||||||
m = _FRAGMENT_SECTION_RE.match(s)
|
m = _FRAGMENT_SECTION_RE.match(s)
|
||||||
if m:
|
if m:
|
||||||
current = {"kapitel": kapitel, "titel": m.group(1), "md": []}
|
current = {"kapitel": kapitel, "titel": m.group(1), "md": [], "subs": []}
|
||||||
|
cur_sub = None
|
||||||
sections.append(current)
|
sections.append(current)
|
||||||
continue
|
continue
|
||||||
|
m = _FRAGMENT_SUB_RE.match(s)
|
||||||
|
if m and current is not None:
|
||||||
|
teil = m.group(1).split("|", 1)
|
||||||
|
stufe = teil[0].strip().casefold()
|
||||||
|
cur_sub = {
|
||||||
|
"stufe": stufe if stufe in _STUFEN else "einfach",
|
||||||
|
"titel": teil[1].strip() if len(teil) == 2 else "",
|
||||||
|
"md": [],
|
||||||
|
}
|
||||||
|
current["subs"].append(cur_sub)
|
||||||
|
continue
|
||||||
if current is not None:
|
if current is not None:
|
||||||
current["md"].append(line)
|
current["md"].append(line)
|
||||||
|
if cur_sub is not None:
|
||||||
|
cur_sub["md"].append(line)
|
||||||
for sec in sections:
|
for sec in sections:
|
||||||
sec["md"] = "\n".join(sec["md"]).strip()
|
sec["md"] = "\n".join(sec["md"]).strip()
|
||||||
|
for sub in sec["subs"]:
|
||||||
|
sub["md"] = "\n".join(sub["md"]).strip()
|
||||||
|
sec["subs"] = [sub for sub in sec["subs"] if sub["md"]]
|
||||||
return sections
|
return sections
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_subbausteine(text: str) -> dict[str, list[str]]:
|
||||||
|
"""Parst eine Subbaustein-Datei → {Baustein-Titel: [Subbaustein, …]} in Reihenfolge.
|
||||||
|
|
||||||
|
Format: `<!-- baustein: Titel -->` gefolgt von Listenzeilen `- Subbaustein`.
|
||||||
|
"""
|
||||||
|
out: dict[str, list[str]] = {}
|
||||||
|
current = None
|
||||||
|
for line in text.splitlines():
|
||||||
|
s = line.strip()
|
||||||
|
m = _FRAGMENT_BAUSTEIN_RE.match(s)
|
||||||
|
if m:
|
||||||
|
current = m.group(1).strip()
|
||||||
|
out.setdefault(current, [])
|
||||||
|
continue
|
||||||
|
if current is None:
|
||||||
|
continue
|
||||||
|
m = re.match(r"[-*]\s+(.*\S)", s)
|
||||||
|
if m:
|
||||||
|
out[current].append(m.group(1).strip())
|
||||||
|
return {k: v for k, v in out.items() if v}
|
||||||
|
|
||||||
|
|
||||||
def _split_chunks(chapters: list[dict], n: int) -> list[list[dict]]:
|
def _split_chunks(chapters: list[dict], n: int) -> list[list[dict]]:
|
||||||
"""Teilt Kapitel in bis zu n zusammenhängende Chunks, balanciert nach Section-Anzahl."""
|
"""Teilt Kapitel in bis zu n zusammenhängende Chunks, balanciert nach Section-Anzahl."""
|
||||||
n = max(1, min(n, len(chapters)))
|
n = max(1, min(n, len(chapters)))
|
||||||
|
|||||||
@@ -440,6 +440,10 @@ onMounted(async () => {
|
|||||||
--warning-soft: #fef3c7;
|
--warning-soft: #fef3c7;
|
||||||
--warning-border: #fbbf24;
|
--warning-border: #fbbf24;
|
||||||
--danger: #991b1b;
|
--danger: #991b1b;
|
||||||
|
/* Subbaustein-Stufen als Ampel (Schwierigkeit), bewusst NICHT die Stand-Farben. */
|
||||||
|
--stufe-einfach: #22c55e;
|
||||||
|
--stufe-mittel: #f59e0b;
|
||||||
|
--stufe-schwer: #ef4444;
|
||||||
--code-bg: #1e2a3a;
|
--code-bg: #1e2a3a;
|
||||||
--code-fg: #e6e6e6;
|
--code-fg: #e6e6e6;
|
||||||
--shadow: rgba(0, 0, 0, 0.12);
|
--shadow: rgba(0, 0, 0, 0.12);
|
||||||
@@ -468,6 +472,9 @@ html.dark {
|
|||||||
--warning-soft: #3a2c0a;
|
--warning-soft: #3a2c0a;
|
||||||
--warning-border: #a06a12;
|
--warning-border: #a06a12;
|
||||||
--danger: #f87171;
|
--danger: #f87171;
|
||||||
|
--stufe-einfach: #22c55e;
|
||||||
|
--stufe-mittel: #f59e0b;
|
||||||
|
--stufe-schwer: #ef4444;
|
||||||
--shadow: rgba(0, 0, 0, 0.5);
|
--shadow: rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import BausteinPanel from './BausteinPanel.vue'
|
import BausteinPanel from './BausteinPanel.vue'
|
||||||
|
import SubbausteinView from './SubbausteinView.vue'
|
||||||
import { renderMarkdown } from '../markdown.js'
|
import { renderMarkdown } from '../markdown.js'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -52,7 +53,8 @@ const standFarbe = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="fokus-body">
|
<div class="fokus-body">
|
||||||
<div class="fokus-col left">
|
<div class="fokus-col left">
|
||||||
<div class="markdown" v-html="renderMarkdown(baustein.md)"></div>
|
<div v-if="!baustein.subbausteine?.length" class="markdown" v-html="renderMarkdown(baustein.md)"></div>
|
||||||
|
<SubbausteinView v-else :subbausteine="baustein.subbausteine" :md="baustein.md" />
|
||||||
</div>
|
</div>
|
||||||
<div class="fokus-col right">
|
<div class="fokus-col right">
|
||||||
<BausteinPanel
|
<BausteinPanel
|
||||||
|
|||||||
105
frontend/src/components/SubbausteinView.vue
Normal file
105
frontend/src/components/SubbausteinView.vue
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { renderMarkdown } from '../markdown.js'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
subbausteine: { type: Array, default: null },
|
||||||
|
md: { type: String, default: '' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const STUFEN = ['einfach', 'mittel', 'schwer']
|
||||||
|
const rang = (s) => {
|
||||||
|
const i = STUFEN.indexOf(s)
|
||||||
|
return i < 0 ? 0 : i
|
||||||
|
}
|
||||||
|
// 0 = nur einfach (Default), 1 = + mittel, 2 = alle
|
||||||
|
const stufe = ref(0)
|
||||||
|
const labels = ['Einfach', '+ Mittel', 'Alle']
|
||||||
|
|
||||||
|
const hatSubs = computed(() => Array.isArray(props.subbausteine) && props.subbausteine.length > 0)
|
||||||
|
const sichtbar = computed(() => {
|
||||||
|
if (!hatSubs.value) return []
|
||||||
|
const gefiltert = props.subbausteine.filter((s) => rang(s.stufe) <= stufe.value)
|
||||||
|
return gefiltert.length ? gefiltert : props.subbausteine // nie ganz leer anzeigen
|
||||||
|
})
|
||||||
|
const verborgen = computed(() => (hatSubs.value ? props.subbausteine.length - sichtbar.value.length : 0))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="hatSubs">
|
||||||
|
<div class="subs-filter">
|
||||||
|
<button v-for="(lab, i) in labels" :key="i" :class="{ active: stufe === i }" @click="stufe = i">{{ lab }}</button>
|
||||||
|
<span v-if="verborgen > 0" class="subs-hidden">{{ verborgen }} ausgeblendet</span>
|
||||||
|
</div>
|
||||||
|
<div v-for="(s, i) in sichtbar" :key="i" :class="['sub', 'stufe-' + s.stufe]">
|
||||||
|
<div class="sub-head">
|
||||||
|
<span class="sub-pill">{{ s.stufe }}</span>
|
||||||
|
<span class="sub-titel">{{ s.titel }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="markdown" v-html="renderMarkdown(s.md)"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="markdown" v-html="renderMarkdown(md)"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.subs-filter {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.4rem;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 0.9rem;
|
||||||
|
}
|
||||||
|
.subs-filter button {
|
||||||
|
padding: 0.2rem 0.7rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--panel);
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
.subs-filter button.active {
|
||||||
|
background: var(--accent);
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--on-accent);
|
||||||
|
}
|
||||||
|
.subs-hidden {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ampel je Stufe: farbiger linker Rand + zarter Hintergrund. */
|
||||||
|
.sub {
|
||||||
|
border-left: 3px solid var(--sub-c);
|
||||||
|
background: color-mix(in srgb, var(--sub-c) 7%, var(--panel));
|
||||||
|
border-radius: 0 8px 8px 0;
|
||||||
|
padding: 0.6rem 0.9rem;
|
||||||
|
margin-bottom: 0.7rem;
|
||||||
|
}
|
||||||
|
.sub.stufe-einfach { --sub-c: var(--stufe-einfach); }
|
||||||
|
.sub.stufe-mittel { --sub-c: var(--stufe-mittel); }
|
||||||
|
.sub.stufe-schwer { --sub-c: var(--stufe-schwer); }
|
||||||
|
.sub-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.35rem;
|
||||||
|
}
|
||||||
|
.sub-pill {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
padding: 0.08rem 0.45rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
color: #fff;
|
||||||
|
background: var(--sub-c);
|
||||||
|
}
|
||||||
|
.sub-titel {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.sub .markdown :deep(p:last-child) { margin-bottom: 0; }
|
||||||
|
</style>
|
||||||
@@ -5,6 +5,7 @@ import { renderMarkdown } from '../markdown.js'
|
|||||||
import { useChat } from '../composables/useChat.js'
|
import { useChat } from '../composables/useChat.js'
|
||||||
import BausteinPanel from './BausteinPanel.vue'
|
import BausteinPanel from './BausteinPanel.vue'
|
||||||
import BausteinFokus from './BausteinFokus.vue'
|
import BausteinFokus from './BausteinFokus.vue'
|
||||||
|
import SubbausteinView from './SubbausteinView.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
previewGuide: { type: Object, default: null },
|
previewGuide: { type: Object, default: null },
|
||||||
@@ -199,7 +200,8 @@ function extractContext() {
|
|||||||
<span v-else-if="lernstand[s.title]?.verstanden" class="baustein-done verstanden" title="Vollständig verstanden (10/10)">✓✓ Verstanden</span>
|
<span v-else-if="lernstand[s.title]?.verstanden" class="baustein-done verstanden" title="Vollständig verstanden (10/10)">✓✓ Verstanden</span>
|
||||||
<span v-else-if="lernstand[s.title]?.absolviert" class="baustein-done" title="Prüfung bestanden">✓ Absolviert</span>
|
<span v-else-if="lernstand[s.title]?.absolviert" class="baustein-done" title="Prüfung bestanden">✓ Absolviert</span>
|
||||||
</h3>
|
</h3>
|
||||||
<div class="section-body markdown" v-html="renderMarkdown(s.md)"></div>
|
<div v-if="isOnePager || !s.subbausteine?.length" class="section-body markdown" v-html="renderMarkdown(s.md)"></div>
|
||||||
|
<SubbausteinView v-else class="section-body" :subbausteine="s.subbausteine" :md="s.md" />
|
||||||
<BausteinPanel
|
<BausteinPanel
|
||||||
v-if="!isOnePager"
|
v-if="!isOnePager"
|
||||||
mode="trigger"
|
mode="trigger"
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
Identifiziere für jeden zugeteilten Baustein eines Lern-Guides zum Thema "{topic}", WAS ein Anfänger ohne Vorwissen verstehen muss. Du schreibst noch keinen Guide-Text — du sammelst nur die Inhalte, die später gelehrt werden.
|
Identifiziere für jeden zugeteilten Baustein eines Lern-Guides zum Thema "{topic}", WAS ein Anfänger ohne Vorwissen verstehen muss. Du schreibst noch keinen Guide-Text — du sammelst nur die Inhalte, die später gelehrt werden.
|
||||||
|
|
||||||
Dir zugeteilt sind folgende Kapitel und Bausteine — verbindlich: jeder zugeteilte Baustein muss vorkommen, keine zusätzlichen erfinden:
|
Dir zugeteilt sind folgende Kapitel und Bausteine — verbindlich: jeder zugeteilte Baustein muss vorkommen, keine zusätzlichen erfinden. Unter jedem Baustein stehen seine SUBBAUSTEINE mit Stufe (`[einfach]`/`[mittel]`/`[schwer]`) — die Teilpunkte, die gelehrt werden:
|
||||||
{zuteilung}
|
{zuteilung}
|
||||||
|
|
||||||
{facts}
|
{facts}
|
||||||
|
|
||||||
Sammle pro Baustein:
|
Sammle pro Baustein:
|
||||||
- Kernpunkte / Lernziele: was muss der Leser begreifen? 3–7 knappe Punkte, jeder eine Aussage.
|
- Kernpunkte / Lernziele: was muss der Leser begreifen? Decke JEDEN gelisteten Subbaustein mit mindestens einem Kernpunkt ab. Hat ein Baustein keine Subbausteine, sammle 3–7 knappe Punkte.
|
||||||
- Belegte Fakten (Versionen, Namen, Werte) — nichts Unbelegtes; Unsicheres per Websuche prüfen.
|
- Belegte Fakten (Versionen, Namen, Werte) — nichts Unbelegtes; Unsicheres per Websuche prüfen.
|
||||||
- Eine konkrete Beispiel-Idee: was soll das Beispiel zeigen?
|
- Eine konkrete Beispiel-Idee: was soll das Beispiel zeigen?
|
||||||
- Scope: nur was DIESER Baustein hergibt. Nicht mehr, nicht weniger. Am Rand Erwähntes nicht zum Thema aufblasen.
|
- Scope: nur was DIESER Baustein hergibt. Nicht mehr, nicht weniger. Am Rand Erwähntes nicht zum Thema aufblasen.
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ ZU ÜBERARBEITEN — je Section der aktuelle Inhalt und das notierte Problem:
|
|||||||
|
|
||||||
Behebe pro Section NUR das notierte Problem; was in Ordnung ist, bleibt inhaltlich erhalten.
|
Behebe pro Section NUR das notierte Problem; was in Ordnung ist, bleibt inhaltlich erhalten.
|
||||||
|
|
||||||
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:
|
Enthält eine Section `<!-- sub: stufe | Titel -->`-Marker (Subbausteine), BEHALTE diese Struktur bei: gib pro Subbaustein wieder den Marker und darunter den überarbeiteten Text. Stufe und Subbaustein-Titel exakt übernehmen, keine Subbausteine entfernen oder hinzufügen.
|
||||||
|
|
||||||
|
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 (mit den `<!-- sub: … -->`-Markern, falls die Section welche hatte):
|
||||||
|
|
||||||
<!-- section: Exakter Section-Titel -->
|
<!-- section: Exakter Section-Titel -->
|
||||||
Erklärung (Einordnung → Wie/Warum) laut SECTION-SPEZIFIKATION…
|
<!-- sub: einfach | Subbaustein-Titel -->
|
||||||
|
Überarbeiteter Text zu diesem Teilpunkt…
|
||||||
### Beispiel
|
|
||||||
(Beispiel im themengerechten Format laut SECTION-SPEZIFIKATION: Codeblock NUR bei Code-Themen, sonst Beispielsätze oder Mini-Szenario)
|
|
||||||
|
|
||||||
Die Marker-Zeilen exakt so schreiben. Kein Text außerhalb der Sections.
|
Die Marker-Zeilen exakt so schreiben. Kein Text außerhalb der Sections.
|
||||||
{extra}
|
{extra}
|
||||||
|
|||||||
@@ -1,28 +1,40 @@
|
|||||||
Schreibe Sections für einen Lern-Guide zum Thema "{topic}" (Format: {format_name}).
|
Schreibe Sections für einen Lern-Guide zum Thema "{topic}" (Format: {format_name}).
|
||||||
|
|
||||||
Dir zugeteilt sind folgende Kapitel und Bausteine — verbindlich: jede zugeteilte Section muss vorkommen, keine zusätzlichen erfinden:
|
Dir zugeteilt sind folgende Kapitel und Bausteine — verbindlich: jede zugeteilte Section muss vorkommen, keine zusätzlichen erfinden. Unter jedem Baustein stehen seine SUBBAUSTEINE mit Stufe (`[einfach]`, `[mittel]`, `[schwer]`):
|
||||||
{zuteilung}
|
{zuteilung}
|
||||||
|
|
||||||
{facts}
|
{facts}
|
||||||
|
|
||||||
Das Format ({format_name}) beeinflusst NICHT, wie ein einzelner Baustein geschrieben wird — nur, wie viele Bausteine das Thema abdeckt. Derselbe Baustein bekommt in MiniGuide, Guide und FullGuide denselben Text. Wie lang und tief ein Baustein ist, richtet sich allein nach SEINER Schwierigkeit (laut Spezifikation): ein einfacher Baustein bleibt kurz, ein kniffliger darf länger sein. Bausteine untereinander sind also verschieden lang — derselbe Baustein über die Formate hinweg aber gleich.
|
Das Format ({format_name}) beeinflusst NICHT, wie ein Baustein geschrieben wird — nur, wie viele Bausteine das Thema abdecken. Derselbe Baustein bekommt in MiniGuide, Guide und FullGuide denselben Text.
|
||||||
|
|
||||||
|
SUBBAUSTEINE — so schreibst du:
|
||||||
|
- Pro zugeteiltem Subbaustein schreibst du EINEN kurzen, eigenständigen Abschnitt. Ein Subbaustein = ein Teilpunkt.
|
||||||
|
- Jeder Subbaustein-Abschnitt steht für sich: allein verständlich, ohne die anderen gelesen zu haben. Ein Anfänger darf später nur die `[einfach]`-Abschnitte sehen — die müssen für sich Sinn ergeben.
|
||||||
|
- Die Stufe steuert die Tiefe: `[einfach]` = Grundlage, knapp und sofort verständlich. `[mittel]` = ein Detail/eine Variante. `[schwer]` = Sonderfall/Tücke.
|
||||||
|
- So lang wie nötig, so kurz wie möglich: 1–4 Sätze je Subbaustein. Ein Beispiel NUR, wo es wirklich hilft (Format laut Spezifikation).
|
||||||
|
- Ein Baustein mit vielen Subbausteinen wird dadurch lang, einer mit wenigen kurz — das ist gewollt.
|
||||||
|
|
||||||
GEPRÜFTE INHALTE je Baustein — das ist verbindlich, was gelehrt werden muss:
|
GEPRÜFTE INHALTE je Baustein — das ist verbindlich, was gelehrt werden muss:
|
||||||
{inhalte}
|
{inhalte}
|
||||||
|
|
||||||
Nutze diese Inhalte. Erfinde nichts dazu, lass nichts Wesentliches weg. Formuliere sie als lesbaren Lern-Text laut SECTION-SPEZIFIKATION. Keine feste Länge — die Länge folgt den Inhalten. Lehre simpel: kurze Sätze, Listen für Aufzählungen, ein Anfänger versteht es sofort.
|
Nutze diese Inhalte. Erfinde nichts dazu, lass nichts Wesentliches weg. Verteile sie auf die Subbausteine. Lehre simpel: kurze Sätze, Listen für Aufzählungen, ein Anfänger versteht es sofort.
|
||||||
|
|
||||||
SECTION-SPEZIFIKATION:
|
SECTION-SPEZIFIKATION (gilt je Subbaustein-Abschnitt):
|
||||||
{spec}
|
{spec}
|
||||||
|
|
||||||
Schreibe NUR die Datei {out_path} in GENAU diesem Format — pro Kapitel ein kapitel-Marker, pro Baustein ein section-Marker (Titel EXAKT aus der Zuteilung), darunter der Markdown-Body:
|
Schreibe NUR die Datei {out_path} in GENAU diesem Format — pro Kapitel ein kapitel-Marker, pro Baustein ein section-Marker (Titel EXAKT aus der Zuteilung), darunter PRO Subbaustein ein sub-Marker `<!-- sub: stufe | Subbaustein-Titel -->` und dann der Markdown-Body dieses Subbausteins:
|
||||||
|
|
||||||
<!-- kapitel: Kapiteltitel -->
|
<!-- kapitel: Kapiteltitel -->
|
||||||
<!-- section: Exakter Baustein-Titel -->
|
<!-- section: Exakter Baustein-Titel -->
|
||||||
Erklärung (Einordnung → Wie/Warum) laut SECTION-SPEZIFIKATION…
|
<!-- sub: einfach | Erster Subbaustein-Titel -->
|
||||||
|
Kurzer, eigenständiger Text zu diesem Teilpunkt…
|
||||||
|
|
||||||
### Beispiel
|
<!-- sub: mittel | Zweiter Subbaustein-Titel -->
|
||||||
(Beispiel im themengerechten Format laut SECTION-SPEZIFIKATION: Codeblock NUR bei Code-Themen, sonst Beispielsätze oder Mini-Szenario)
|
Kurzer Text…
|
||||||
|
|
||||||
Die Marker-Zeilen exakt so schreiben. Kein Text außerhalb der Sections, kein Dokument-Titel, kein Inhaltsverzeichnis.
|
Regeln für die Marker:
|
||||||
|
- `stufe` ist genau eines von: einfach, mittel, schwer (aus der Zuteilung übernehmen).
|
||||||
|
- Der Subbaustein-Titel nach `|` EXAKT aus der Zuteilung.
|
||||||
|
- Hat ein Baustein in der Zuteilung KEINE Subbausteine, schreibe direkt unter den section-Marker einen normalen Section-Body (ohne sub-Marker).
|
||||||
|
- Kein Text außerhalb der Marker, kein Dokument-Titel, kein Inhaltsverzeichnis.
|
||||||
{extra}
|
{extra}
|
||||||
19
templates/Prompt/Stufen-Mapping.md
Normal file
19
templates/Prompt/Stufen-Mapping.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
Mehrere Agenten haben die Subbausteine des Themas "{topic}" nach Schwierigkeit eingestuft. Bei einigen sind sie sich uneinig. Entscheide für jeden strittigen Subbaustein die finale Stufe.
|
||||||
|
|
||||||
|
STRITTIGE SUBBAUSTEINE (mit den abgegebenen Stimmen):
|
||||||
|
{strittig}
|
||||||
|
|
||||||
|
Stufen:
|
||||||
|
- **einfach**: Grundlegend, Anfänger versteht es sofort, Kernfunktion.
|
||||||
|
- **mittel**: Braucht etwas Erklärung, gängige Variante oder wichtiges Detail.
|
||||||
|
- **schwer**: Sonderfall, Tücke, fortgeschritten, leicht falsch zu machen.
|
||||||
|
|
||||||
|
Regeln:
|
||||||
|
- Wäge die Stimmen ab, entscheide fachlich. Im Zweifel die niedrigere Stufe.
|
||||||
|
- Für JEDE strittige Nummer genau eine Stufe.
|
||||||
|
|
||||||
|
Schreibe NUR die JSON-Datei nach: {out_path}
|
||||||
|
|
||||||
|
Format (kein weiterer Text):
|
||||||
|
{{"stufen": {{"1": "einfach", "4": "schwer"}}}}
|
||||||
|
{extra}
|
||||||
20
templates/Prompt/Stufen-Recherche.md
Normal file
20
templates/Prompt/Stufen-Recherche.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
Stufe jeden Subbaustein des Themas "{topic}" nach Schwierigkeit ein: einfach, mittel oder schwer. Die Stufe steuert, wann ein Lerner den Punkt sieht (Anfänger sehen nur „einfach", Fortgeschrittene alles).
|
||||||
|
|
||||||
|
SUBBAUSTEINE (Nummer in Klammern der zugehörige Baustein):
|
||||||
|
{subbausteine}
|
||||||
|
|
||||||
|
Stufen:
|
||||||
|
- **einfach**: Grundlegend. Ein Anfänger versteht es sofort. Die Kernfunktion, ohne die der Baustein nicht nutzbar ist.
|
||||||
|
- **mittel**: Braucht etwas Erklärung. Eine gängige Variante, ein wichtiges Detail, ein zweiter Schritt.
|
||||||
|
- **schwer**: Sonderfall, Tücke, fortgeschritten. Leicht falsch zu machen, selten gebraucht, oder setzt mehr Verständnis voraus.
|
||||||
|
|
||||||
|
Regeln:
|
||||||
|
- Stufe jeden Punkt einzeln, gemessen an SEINEM Baustein. Im Zweifel die niedrigere Stufe.
|
||||||
|
- Jeder Baustein sollte mindestens einen „einfach"-Punkt haben (den Einstieg).
|
||||||
|
- Urteile nur — erfinde nichts dazu, ändere keine Subbausteine.
|
||||||
|
|
||||||
|
Schreibe NUR die JSON-Datei nach: {out_path}
|
||||||
|
|
||||||
|
Format (für JEDE Nummer genau eine Stufe; kein weiterer Text):
|
||||||
|
{{"stufen": {{"1": "einfach", "2": "mittel", "3": "schwer"}}}}
|
||||||
|
{extra}
|
||||||
19
templates/Prompt/Subbaustein-Mapping.md
Normal file
19
templates/Prompt/Subbaustein-Mapping.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
Mehrere Agenten haben unabhängig die Subbausteine je Baustein des Themas "{topic}" ermittelt. Pro Baustein ist der KONSENS (von mindestens zwei Agenten genannt) und der strittige REST (nur einmal genannt) gelistet. Entscheide je Baustein die finale Subbaustein-Liste.
|
||||||
|
|
||||||
|
{bausteine}
|
||||||
|
|
||||||
|
Regeln:
|
||||||
|
- Übernimm alle Konsens-Subbausteine.
|
||||||
|
- Einen strittigen Rest-Punkt AUFNEHMEN, wenn er ein eigener, korrekter Teilpunkt des Bausteins ist, den der Konsens nicht abdeckt.
|
||||||
|
- VERWERFEN, wenn er eine Dublette/Variante eines vorhandenen Punkts ist, zu fein granular, am Rand des Themas oder fachlich zweifelhaft.
|
||||||
|
- Die Anzahl folgt der Schwierigkeit: trivialer Baustein wenige Punkte, komplexer viele. Blähe nichts auf, lass nichts Wesentliches weg.
|
||||||
|
- Subbausteine wörtlich übernehmen, nicht umformulieren. Jeder Punkt atomar (eine Aussage).
|
||||||
|
|
||||||
|
Schreibe NUR die Datei {out_path} — pro Baustein ein baustein-Marker (Titel EXAKT wie oben), darunter die finale Subbaustein-Liste:
|
||||||
|
|
||||||
|
<!-- baustein: Exakter Baustein-Titel -->
|
||||||
|
- Subbaustein
|
||||||
|
- Subbaustein
|
||||||
|
|
||||||
|
Die Marker-Zeile exakt so schreiben. Jeder Baustein muss vorkommen. Kein Text außerhalb der Bausteine.
|
||||||
|
{extra}
|
||||||
28
templates/Prompt/Subbaustein-Recherche.md
Normal file
28
templates/Prompt/Subbaustein-Recherche.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
Zerlege jeden zugeteilten Baustein des Themas "{topic}" in seine SUBBAUSTEINE — die einzelnen lernbaren Teilpunkte, aus denen das Konzept besteht. Ein späterer Guide wird PRO Subbaustein einen kurzen Text schreiben.
|
||||||
|
|
||||||
|
Dir zugeteilt — verbindlich: jeder Baustein muss vorkommen, keine zusätzlichen erfinden:
|
||||||
|
{zuteilung}
|
||||||
|
|
||||||
|
Was ein Subbaustein ist:
|
||||||
|
- Ein einzelner Teilpunkt, den man am Baustein lernen muss. Eine Aussage, ein Aspekt, eine Tücke.
|
||||||
|
- Beispiele: Baustein `<img>` → `src` (Bildquelle), `alt` (Alternativtext), leeres `alt` für Deko-Bilder, `width`/`height` gegen Layout-Sprünge, Void-Element ohne Endtag. Baustein `<p>` → Textabsatz als Block, erlaubte Inline-Kinder, keine Verschachtelung.
|
||||||
|
|
||||||
|
ENTSCHEIDEND — die Anzahl folgt der Schwierigkeit:
|
||||||
|
- Ein einfacher, trivialer Baustein hat WENIGE Subbausteine (1–2). Blähe ihn NICHT auf.
|
||||||
|
- Ein komplexer, reichhaltiger Baustein hat VIELE Subbausteine. Lass nichts Wesentliches weg.
|
||||||
|
- Es gibt KEINE Zielzahl. `<img>` muss deutlich mehr Subbausteine haben als `<p>`.
|
||||||
|
|
||||||
|
Regeln:
|
||||||
|
- Jeder Subbaustein ist atomar: genau ein Teilpunkt. Keine zwei Aspekte in einen Punkt.
|
||||||
|
- Nur was DIESER Baustein hergibt (Scope). Nichts am Rand Erwähntes aufblasen.
|
||||||
|
- Belegt, nicht erfunden. Unsicheres per Websuche prüfen.
|
||||||
|
- Subbaustein-Titel auf DEUTSCH (Code-Bezeichner bleiben original), max. ~10 Wörter, eine Aussage.
|
||||||
|
|
||||||
|
Schreibe NUR die Datei {out_path} — pro Baustein ein baustein-Marker (Titel EXAKT aus der Zuteilung), darunter die Subbausteine als Liste:
|
||||||
|
|
||||||
|
<!-- baustein: Exakter Baustein-Titel -->
|
||||||
|
- Erster Subbaustein
|
||||||
|
- Zweiter Subbaustein
|
||||||
|
|
||||||
|
Die Marker-Zeile exakt so schreiben. Kein Text außerhalb der Bausteine.
|
||||||
|
{extra}
|
||||||
Reference in New Issue
Block a user