This commit is contained in:
team3
2026-06-17 19:59:06 +02:00
parent c487e9cfcd
commit 0ff33271a0
10 changed files with 382 additions and 18 deletions

View File

@@ -38,7 +38,7 @@ from textkit import (
log = logging.getLogger("creator.guide")
GUIDE_STEPS = ("Auswahl", "Gliederung", "Schreiben", "Lese-Prüfung")
GUIDE_STEPS = ("Auswahl", "Gliederung", "Inhalte", "Inhalts-Check", "Schreiben", "Lese-Prüfung")
# Writer skalieren mit der Section-Zahl: 1 Writer je ~30 Sections (gedeckelt).
# Kleine Pakete vermeiden Lazy-Output bei langen Listen und begrenzen den Schaden
@@ -455,22 +455,128 @@ async def _generate_sections(
await _fail(guide_id, "Gliederung fehlgeschlagen (Judge ohne gültiges Ergebnis)")
return None
# Schritt 2: Schreiben — vorhandene Chunk-Dateien werden übernommen (Resume)
# Chunks festlegen — Inhalte, Inhalts-Prüfung, Writer und Lese-Check nutzen
# dieselbe Aufteilung: 1 Agent je ~30 Bausteine (gedeckelt).
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))))
zuteilungen = [_zuteilung_text(chunk, entries) 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(chunks)
idx = _titel_index(entries)
# Schritt 2: Inhalte je Baustein identifizieren — pro Chunk ein Agent (Marker-Output, Resume).
inhalt_paths = [content_path.parent / f"{content_path.stem}.inhalt-chunk-{i}.md" for i in range(1, writer_count + 1)]
offen = [i for i, p in enumerate(inhalt_paths) if not p.exists()]
if offen:
await _set_step(guide_id, 2, f"Sammle Inhalte ({writer_count} Agenten)…" if writer_count > 1 else "Sammle Inhalte…")
results = await asyncio.gather(*[
run_agent(
f"{guide_id}-inhalt-{i + 1}",
_prompt(
"Guide-Inhalt",
topic=topic, zuteilung=zuteilungen[i], facts=facts,
out_path=inhalt_paths[i], extra=_extra(instructions),
),
_timeout("inhalt", chunk_sizes[i]), provider=provider, role="guide", capabilities="full",
)
for i in offen
], return_exceptions=True)
if is_cancelled():
return None
if not any(p.exists() for p in inhalt_paths):
await _fail(guide_id, _gather_error("Inhalts-Fehler", list(results)))
return None
inhalt_by_num: dict[int, str] = {}
for p in inhalt_paths:
if not p.exists():
continue
for sec in _parse_fragment(p.read_text(encoding="utf-8")):
num = _titel_aufloesen(idx, sec["titel"])
if num is not None and num not in inhalt_by_num and sec["md"].strip():
inhalt_by_num[num] = sec["md"]
if not inhalt_by_num:
await _fail(guide_id, "Keine Inhalte identifiziert")
return None
inhalt_chunk_nums = [[num for ch in chunk for num in ch["nums"] if num in inhalt_by_num] for chunk in chunks]
# Schritt 3: Inhalte prüfen (pro Chunk ein Check) + beanstandete einmal überarbeiten.
check_paths = [content_path.parent / f"{content_path.stem}.inhalt-check-{i}.json" for i in range(1, writer_count + 1)]
offen_checks = [i for i, p in enumerate(check_paths) if inhalt_chunk_nums[i] and _lese_probleme_schema(_json_datei(p)) is None]
if offen_checks:
await _set_step(guide_id, 3, "Prüfe Inhalte…")
slots = [{
"key": f"{guide_id}-inhalt-check-{i + 1}",
"prompt": _prompt(
"Guide-Inhalt-Check",
topic=topic, format_name=format_name,
sections="\n\n".join(f"SECTION: {_titel(entries[num])}\n{inhalt_by_num[num]}" for num in inhalt_chunk_nums[i]),
out_path=check_paths[i], extra=_extra(instructions),
),
"role": "judge", "capabilities": "files",
"payload": (lambda result, p=check_paths[i]: _lese_probleme_schema(_json_datei(p))),
} for i in offen_checks]
await _race(topic, "Inhalts-Prüfung", slots, len(slots), _timeout("inhalt_check", max(chunk_sizes)), provider, cancelled=is_cancelled)
if is_cancelled():
return None
probleme_by_num: dict[int, str] = {}
for i, p in enumerate(check_paths):
geltung = set(inhalt_chunk_nums[i])
for item in (_lese_probleme_schema(_json_datei(p)) or []):
num = _titel_aufloesen(idx, item["section"])
if num in geltung and num in inhalt_by_num and num not in probleme_by_num:
probleme_by_num[num] = item["problem"]
if probleme_by_num:
_log(topic, f"Inhalts-Prüfung: {len(probleme_by_num)} Baustein(e) beanstandet")
await _set_step(guide_id, 3, f"Überarbeite {len(probleme_by_num)} Inhalt(e)…")
fix_chunks = [[num for num in nums if num in probleme_by_num] for nums in inhalt_chunk_nums]
fix_paths = [content_path.parent / f"{content_path.stem}.inhalt-fix-{i + 1}.md" for i in range(writer_count)]
fix_offen = [i for i, nums in enumerate(fix_chunks) if nums and not fix_paths[i].exists()]
results = await asyncio.gather(*[
run_agent(
f"{guide_id}-inhalt-fix-{i + 1}",
_prompt(
"Guide-Inhalt-Fix",
topic=topic, facts=facts,
auftraege="\n\n".join(
f"SECTION: {_titel(entries[num])}\nPROBLEM: {probleme_by_num[num]}\nAKTUELL:\n{inhalt_by_num[num]}"
for num in fix_chunks[i]
),
out_path=fix_paths[i], extra=_extra(instructions),
),
_timeout("inhalt", len(fix_chunks[i])), provider=provider, role="guide", capabilities="full",
)
for i in fix_offen
], return_exceptions=True)
if is_cancelled():
return None
for p in fix_paths:
if not p.exists():
continue
for sec in _parse_fragment(p.read_text(encoding="utf-8")):
num = _titel_aufloesen(idx, sec["titel"])
if num in probleme_by_num and sec["md"].strip():
inhalt_by_num[num] = sec["md"]
# Schritt 4: Schreiben — Writer formuliert die geprüften Inhalte aus (Resume).
def inhalte_text(chunk) -> str:
nums = [num for ch in chunk for num in ch["nums"] if num in inhalt_by_num]
return "\n\n".join(f"<!-- section: {_titel(entries[num])} -->\n{inhalt_by_num[num]}" for num in nums)
paths = [content_path.parent / f"{content_path.stem}.chunk-{i}.md" for i in range(1, writer_count + 1)]
offen = [i for i, p in enumerate(paths) if not p.exists()]
if offen:
await _set_step(guide_id, 2, f"Schreibe Sections ({writer_count} Writer)…" if writer_count > 1 else "Schreibe Sections…")
await _set_step(guide_id, 4, f"Schreibe Sections ({writer_count} Writer)…" if writer_count > 1 else "Schreibe Sections…")
results = await asyncio.gather(*[
run_agent(
f"{guide_id}-w{i + 1}",
_prompt(
"Guide-Writer",
topic=topic, format_name=format_name, zuteilung=zuteilungen[i],
inhalte=inhalte_text(chunks[i]),
facts=facts, spec=spec, out_path=paths[i], extra=_extra(instructions),
),
_timeout("writer", chunk_sizes[i]), provider=provider, role="guide", capabilities="full",
@@ -490,7 +596,6 @@ async def _generate_sections(
await _fail(guide_id, _gather_error("Writer-Fehler", list(results)))
return None
idx = _titel_index(entries)
by_num: dict[int, dict] = {}
for p in paths:
if not p.exists():
@@ -524,7 +629,7 @@ async def _generate_sections(
check_paths = [content_path.parent / f"{content_path.stem}.lese-check-r{runde}-{i}.json" for i in range(1, writer_count + 1)]
offen_checks = [i for i, p in enumerate(check_paths) if scope[i] and _lese_probleme_schema(_json_datei(p)) is None]
if offen_checks:
await _set_step(guide_id, 3, f"Prüfe Lesbarkeit (Runde {runde}/{KONSENS_MAX_RUNDEN})…")
await _set_step(guide_id, 5, f"Prüfe Lesbarkeit (Runde {runde}/{KONSENS_MAX_RUNDEN})…")
slots = [{
"key": f"{guide_id}-lese-check-r{runde}-{i + 1}",
"prompt": _prompt(
@@ -557,7 +662,7 @@ async def _generate_sections(
break
_log(topic, f"Lese-Prüfung Runde {runde}: {len(probleme_by_num)} Section(s) beanstandet")
await _set_step(guide_id, 3, f"Überarbeite {len(probleme_by_num)} Section(s) (Runde {runde})…")
await _set_step(guide_id, 5, f"Überarbeite {len(probleme_by_num)} Section(s) (Runde {runde})…")
fix_chunks = [[num for num in nums if num in probleme_by_num] for nums in chunk_nums]
fix_paths = [content_path.parent / f"{content_path.stem}.fix-r{runde}-{i + 1}.md" for i in range(writer_count)]
fix_offen = [i for i, nums in enumerate(fix_chunks) if nums and not fix_paths[i].exists()]