diff --git a/backend/config.py b/backend/config.py index 83f90c5..79c74b4 100644 --- a/backend/config.py +++ b/backend/config.py @@ -24,6 +24,15 @@ FORMAT_META = { "ExtendedGuide": {"pages": "47-60 Seiten", "time": "~5h"}, } +REVIEW_TIMEOUTS = { + "OnePager": 120, + "Cheatsheet": 120, + "MiniGuide": 180, + "BeginnerGuide": 600, + "IntermediateGuide": 600, + "ExtendedGuide": 900, +} + GENERATION_TIMEOUTS = { "OnePager": 900, "Cheatsheet": 900, @@ -34,12 +43,12 @@ GENERATION_TIMEOUTS = { } MAX_CONCURRENT_GENERATIONS = 6 -MAX_ITERATIONS = { - "OnePager": 3, - "Cheatsheet": 3, - "MiniGuide": 3, - "BeginnerGuide": 5, - "IntermediateGuide": 5, - "ExtendedGuide": 5, +CONTENT_REVIEW_ITERATIONS = { + "OnePager": 1, + "Cheatsheet": 1, + "MiniGuide": 2, + "BeginnerGuide": 2, + "IntermediateGuide": 2, + "ExtendedGuide": 2, } CLAUDE_CLI = "claude" diff --git a/backend/generator.py b/backend/generator.py index e056e40..0afec01 100644 --- a/backend/generator.py +++ b/backend/generator.py @@ -6,10 +6,11 @@ from pathlib import Path from config import ( CLAUDE_CLI, + CONTENT_REVIEW_ITERATIONS, TEMPLATES_DIR, GENERATION_TIMEOUTS, MAX_CONCURRENT_GENERATIONS, - MAX_ITERATIONS, + REVIEW_TIMEOUTS, STORAGE_DIR, ) from database import update_guide @@ -138,6 +139,40 @@ Führe KEIN weasyprint aus, erzeuge KEINE PDF. """ +def _build_content_review_prompt(topic: str, format_name: str, html_path: Path) -> str: + spec = (TEMPLATES_DIR / "Format" / f"{format_name}.md").read_text(encoding="utf-8") + + return f"""Prüfe den Inhalt der HTML-Datei {html_path} für den "{format_name}" zum Thema "{topic}". + +SCHRITT 1 — HTML-Datei lesen: +Öffne die Datei {html_path} mit dem Read-Tool. + +SCHRITT 2 — Fakten per Websuche prüfen: +Recherchiere mit WebSearch, ob Versionsnummern, Jahreszahlen und zentrale Fakten zu "{topic}" aktuell und korrekt sind. + +SCHRITT 3 — Vollständigkeit prüfen anhand dieser Spezifikation: +{spec} + +Prüfkriterien: +- Sind alle Pflicht-Kapitel/Sektionen vorhanden? +- Stimmen Versionsnummern und Fakten? +- Ist der Inhalt fachlich korrekt und aktuell? +- Entspricht der Schwierigkeitsgrad dem Format? +- Sind Pflicht-Elemente vorhanden (Cover, TOC, Recall-Boxen, Callouts, Code-Beispiele)? + +SCHRITT 4 — Antworte mit GENAU EINEM der folgenden Formate: + +Bei Bestehen: +PASS + +Bei Nicht-Bestehen: +FAIL +- Problem 1 +- Problem 2 +- ... +""" + + def _build_review_prompt(format_name: str, png_paths: list[Path], page_count: int) -> str: spec = (TEMPLATES_DIR / "Format" / f"{format_name}.md").read_text(encoding="utf-8") @@ -184,12 +219,16 @@ async def generate_guide(guide_id: str, topic: str, format_name: str, instructio pdf_path = STORAGE_DIR / "pdf" / f"{guide_id}.pdf" preview_dir = STORAGE_DIR / "preview" / guide_id timeout = GENERATION_TIMEOUTS.get(format_name, 600) - max_iter = MAX_ITERATIONS.get(format_name, 3) + content_iters = CONTENT_REVIEW_ITERATIONS.get(format_name, 2) + review_timeout = REVIEW_TIMEOUTS.get(format_name, 120) try: if guide_id in _cancelled: return + current_step = "Generierung" + current_timeout = timeout + # Step 1: Generator-Agent erstellt HTML await _set_progress(guide_id, "Generiere HTML…") gen_prompt = _build_generator_prompt(topic, format_name, html_path, instructions) @@ -205,39 +244,32 @@ async def generate_guide(guide_id: str, topic: str, format_name: str, instructio await _fail(guide_id, "HTML-Datei wurde nicht erstellt") return - # Step 2-N: Render → Review → Fix Loop - for iteration in range(1, max_iter + 1): + # Phase 1: Inhalts-Review (HTML lesen + Websuche, kein PDF) + for iteration in range(1, content_iters + 1): if guide_id in _cancelled: return - await _set_progress(guide_id, f"Rendere PDF… (Iteration {iteration})") - ok, err = await _render_pdf(html_path, pdf_path) - if not ok: - await _fail(guide_id, f"WeasyPrint-Fehler: {err}") - return - - await _set_progress(guide_id, f"Prüfe… (Iteration {iteration})") - pngs = await _render_pngs(pdf_path, preview_dir) - page_count = len(pngs) - - review_prompt = _build_review_prompt(format_name, pngs, page_count) - returncode, review_out, review_err = await _run_claude(guide_id, review_prompt, 120) + await _set_progress(guide_id, f"Prüfe Inhalt… ({iteration}/{content_iters})") + current_step = f"Inhalts-Review ({iteration}/{content_iters})" + current_timeout = review_timeout + content_prompt = _build_content_review_prompt(topic, format_name, html_path) + returncode, review_out, review_err = await _run_claude(guide_id, content_prompt, review_timeout) if returncode != 0: - await _fail(guide_id, f"Review-Fehler: {review_err[:1000]}") + await _fail(guide_id, f"Inhalts-Review-Fehler: {review_err[:1000]}") return review_text = review_out.strip() - if review_text.startswith("PASS"): break - if iteration == max_iter: + if iteration == content_iters: break - # Fix-Agent feedback = review_text.replace("FAIL", "").strip() - await _set_progress(guide_id, f"Korrigiere… (Iteration {iteration})") + await _set_progress(guide_id, f"Korrigiere Inhalt… ({iteration}/{content_iters})") + current_step = f"Inhalts-Korrektur ({iteration}/{content_iters})" + current_timeout = timeout fix_prompt = _build_fix_prompt(topic, format_name, html_path, feedback) returncode, _, fix_err = await _run_claude(guide_id, fix_prompt, timeout) @@ -245,7 +277,49 @@ async def generate_guide(guide_id: str, topic: str, format_name: str, instructio await _fail(guide_id, f"Fix-Fehler: {fix_err[:1000]}") return - # Final: PDF existiert bereits vom letzten Render + # Phase 2: Stil-Review (1x PDF rendern + PNGs prüfen) + if guide_id in _cancelled: + return + + await _set_progress(guide_id, "Rendere PDF…") + ok, err = await _render_pdf(html_path, pdf_path) + if not ok: + await _fail(guide_id, f"WeasyPrint-Fehler: {err}") + return + + await _set_progress(guide_id, "Prüfe Layout…") + pngs = await _render_pngs(pdf_path, preview_dir) + page_count = len(pngs) + + current_step = "Stil-Review" + current_timeout = review_timeout + review_prompt = _build_review_prompt(format_name, pngs, page_count) + returncode, review_out, review_err = await _run_claude(guide_id, review_prompt, review_timeout) + + if returncode != 0: + await _fail(guide_id, f"Stil-Review-Fehler: {review_err[:1000]}") + return + + review_text = review_out.strip() + if not review_text.startswith("PASS"): + feedback = review_text.replace("FAIL", "").strip() + await _set_progress(guide_id, "Korrigiere Layout…") + current_step = "Stil-Korrektur" + current_timeout = timeout + fix_prompt = _build_fix_prompt(topic, format_name, html_path, feedback) + returncode, _, fix_err = await _run_claude(guide_id, fix_prompt, timeout) + + if returncode != 0: + await _fail(guide_id, f"Fix-Fehler: {fix_err[:1000]}") + return + + # Finales PDF nach Stil-Fix + await _set_progress(guide_id, "Rendere finales PDF…") + ok, err = await _render_pdf(html_path, pdf_path) + if not ok: + await _fail(guide_id, f"WeasyPrint-Fehler: {err}") + return + now = datetime.now(timezone.utc).isoformat() await update_guide( guide_id, @@ -257,7 +331,7 @@ async def generate_guide(guide_id: str, topic: str, format_name: str, instructio ) except asyncio.TimeoutError: - await _fail(guide_id, f"Timeout nach {timeout}s") + await _fail(guide_id, f"Timeout bei {current_step} nach {current_timeout}s") except Exception as e: await _fail(guide_id, str(e)[:2000]) finally: @@ -277,14 +351,15 @@ async def rework_guide(guide_id: str, topic: str, format_name: str, instructions html_path = STORAGE_DIR / "html" / f"{guide_id}.html" pdf_path = STORAGE_DIR / "pdf" / f"{guide_id}.pdf" - preview_dir = STORAGE_DIR / "preview" / guide_id timeout = GENERATION_TIMEOUTS.get(format_name, 600) - max_iter = MAX_ITERATIONS.get(format_name, 3) try: if guide_id in _cancelled: return + current_step = "Überarbeitung" + current_timeout = timeout + rework_prompt = _build_rework_prompt(topic, format_name, html_path, instructions) returncode, stdout, stderr = await _run_claude(guide_id, rework_prompt, timeout) @@ -298,40 +373,11 @@ async def rework_guide(guide_id: str, topic: str, format_name: str, instructions await _fail(guide_id, "HTML-Datei wurde nicht erstellt") return - for iteration in range(1, max_iter + 1): - if guide_id in _cancelled: - return - - await _set_progress(guide_id, f"Rendere PDF… (Iteration {iteration})") - ok, err = await _render_pdf(html_path, pdf_path) - if not ok: - await _fail(guide_id, f"WeasyPrint-Fehler: {err}") - return - - await _set_progress(guide_id, f"Prüfe… (Iteration {iteration})") - pngs = await _render_pngs(pdf_path, preview_dir) - page_count = len(pngs) - - review_prompt = _build_review_prompt(format_name, pngs, page_count) - returncode, review_out, review_err = await _run_claude(guide_id, review_prompt, 120) - - if returncode != 0: - await _fail(guide_id, f"Review-Fehler: {review_err[:1000]}") - return - - review_text = review_out.strip() - if review_text.startswith("PASS"): - break - if iteration == max_iter: - break - - feedback = review_text.replace("FAIL", "").strip() - await _set_progress(guide_id, f"Korrigiere… (Iteration {iteration})") - fix_prompt = _build_fix_prompt(topic, format_name, html_path, feedback) - returncode, _, fix_err = await _run_claude(guide_id, fix_prompt, timeout) - if returncode != 0: - await _fail(guide_id, f"Fix-Fehler: {fix_err[:1000]}") - return + await _set_progress(guide_id, "Rendere PDF…") + ok, err = await _render_pdf(html_path, pdf_path) + if not ok: + await _fail(guide_id, f"WeasyPrint-Fehler: {err}") + return now = datetime.now(timezone.utc).isoformat() await update_guide( @@ -340,16 +386,12 @@ async def rework_guide(guide_id: str, topic: str, format_name: str, instructions ) except asyncio.TimeoutError: - await _fail(guide_id, f"Timeout nach {timeout}s") + await _fail(guide_id, f"Timeout bei {current_step} nach {current_timeout}s") except Exception as e: await _fail(guide_id, str(e)[:2000]) finally: _active_processes.pop(guide_id, None) _cancelled.discard(guide_id) - if preview_dir.exists(): - for f in preview_dir.glob("*"): - f.unlink() - preview_dir.rmdir() async def _fail(guide_id: str, msg: str) -> None: