From 66a48759b34296dd777177f03b5752e436119b49 Mon Sep 17 00:00:00 2001 From: Team3 Date: Mon, 25 May 2026 19:28:25 +0200 Subject: [PATCH] update --- .gitignore | 6 + backend/__pycache__/config.cpython-312.pyc | Bin 973 -> 0 bytes backend/__pycache__/database.cpython-312.pyc | Bin 5796 -> 0 bytes backend/__pycache__/generator.cpython-312.pyc | Bin 9386 -> 0 bytes backend/__pycache__/main.cpython-312.pyc | Bin 1154 -> 0 bytes backend/__pycache__/models.cpython-312.pyc | Bin 1205 -> 0 bytes backend/__pycache__/routes.cpython-312.pyc | Bin 5275 -> 0 bytes backend/config.py | 8 + backend/generator.py | 221 ++++++++++++------ backend/main.py | 1 + guides.db | Bin 12288 -> 0 bytes 11 files changed, 169 insertions(+), 67 deletions(-) create mode 100644 .gitignore delete mode 100644 backend/__pycache__/config.cpython-312.pyc delete mode 100644 backend/__pycache__/database.cpython-312.pyc delete mode 100644 backend/__pycache__/generator.cpython-312.pyc delete mode 100644 backend/__pycache__/main.cpython-312.pyc delete mode 100644 backend/__pycache__/models.cpython-312.pyc delete mode 100644 backend/__pycache__/routes.cpython-312.pyc delete mode 100644 guides.db diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7affc33 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +storage/ +guides.db +node_modules/ +frontend/dist/ +__pycache__/ +*.pyc diff --git a/backend/__pycache__/config.cpython-312.pyc b/backend/__pycache__/config.cpython-312.pyc deleted file mode 100644 index 997e3943b4c2fa75204c46263c667aecbf61c238..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 973 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 diff --git a/backend/__pycache__/generator.cpython-312.pyc b/backend/__pycache__/generator.cpython-312.pyc deleted file mode 100644 index d27611165a4e12afa362a10791bfee62ddab3b76..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9386 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 B66OE^ diff --git a/backend/__pycache__/main.cpython-312.pyc b/backend/__pycache__/main.cpython-312.pyc deleted file mode 100644 index ca895a8743cca5d71456b400a3a08409b298fc73..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1154 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 glrjBzVkzr7*JU(qEs3TW!T bool: return False -def _build_prompt(topic: str, format_name: str, html_path: Path, pdf_path: Path) -> str: +async def _set_progress(guide_id: str, progress: str) -> None: + now = datetime.now(timezone.utc).isoformat() + await update_guide(guide_id, progress=progress, updated_at=now) + + +async def _run_claude(guide_id: str, prompt: str, timeout: int) -> tuple[int, str, str]: + process = await asyncio.create_subprocess_exec( + CLAUDE_CLI, + "-p", + "--allowedTools", "Write,Bash,Read,WebSearch,WebFetch", + "--dangerously-skip-permissions", + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + _active_processes[guide_id] = process + try: + stdout, stderr = await asyncio.wait_for( + process.communicate(input=prompt.encode("utf-8")), + timeout=timeout, + ) + return process.returncode, stdout.decode("utf-8", errors="replace"), stderr.decode("utf-8", errors="replace") + finally: + _active_processes.pop(guide_id, None) + + +async def _render_pdf(html_path: Path, pdf_path: Path) -> tuple[bool, str]: + proc = await asyncio.create_subprocess_exec( + "weasyprint", str(html_path), str(pdf_path), + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + _, stderr = await asyncio.wait_for(proc.communicate(), timeout=120) + if proc.returncode != 0: + return False, stderr.decode("utf-8", errors="replace")[:1000] + return True, "" + + +async def _render_pngs(pdf_path: Path, preview_dir: Path) -> list[Path]: + preview_dir.mkdir(parents=True, exist_ok=True) + proc = await asyncio.create_subprocess_exec( + "python3", "-c", + f"from pdf2image import convert_from_path; pages = convert_from_path('{pdf_path}', dpi=120); [p.save('{preview_dir}/page_{{i}}.png') for i, p in enumerate(pages)]; print(len(pages))", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=60) + pngs = sorted(preview_dir.glob("page_*.png")) + return pngs + + +def _build_generator_prompt(topic: str, format_name: str, html_path: Path) -> str: spec = (DOC_DIR / "Format" / f"{format_name}.md").read_text(encoding="utf-8") reference = (DOC_DIR / "Referenz" / f"{format_name}.md").read_text(encoding="utf-8") @@ -35,7 +88,8 @@ def _build_prompt(topic: str, format_name: str, html_path: Path, pdf_path: Path) Recherchiere zuerst die aktuelle Version und aktuelle Fakten zu "{topic}" per Websuche, damit Versionsnummern und Angaben stimmen. Schreibe die HTML-Datei nach: {html_path} -Erstelle die PDF-Datei nach: {pdf_path} + +Schreibe NUR die HTML-Datei. Führe KEIN weasyprint aus, erzeuge KEINE PDF. Das übernimmt ein anderer Prozess. FORMAT-SPEZIFIKATION: {spec} @@ -45,84 +99,114 @@ REFERENZ-IMPLEMENTIERUNG (Stil-Vorlage, adaptiere für "{topic}"): """ -async def _set_progress(guide_id: str, progress: str) -> None: - now = datetime.now(timezone.utc).isoformat() - await update_guide(guide_id, progress=progress, updated_at=now) +def _build_fix_prompt(topic: str, format_name: str, html_path: Path, feedback: str) -> str: + return f"""Die HTML-Datei {html_path} für den "{format_name}" zum Thema "{topic}" hat Probleme. + +FEEDBACK VOM PRÜFER: +{feedback} + +Behebe die Probleme in der HTML-Datei {html_path}. Schreibe die korrigierte Version in dieselbe Datei. +Führe KEIN weasyprint aus, erzeuge KEINE PDF. +""" -async def _watch_files(guide_id: str, html_path: Path, pdf_path: Path, stop_event: asyncio.Event) -> None: - html_seen = False - pdf_mtime = 0.0 - iteration = 0 +def _build_review_prompt(format_name: str, png_paths: list[Path], page_count: int) -> str: + spec = (DOC_DIR / "Format" / f"{format_name}.md").read_text(encoding="utf-8") - while not stop_event.is_set(): - await asyncio.sleep(2) + png_list = "\n".join(str(p) for p in png_paths) - if not html_seen and html_path.exists(): - html_seen = True - await _set_progress(guide_id, "HTML generiert…") + return f"""Prüfe die folgenden Preview-Bilder eines generierten "{format_name}" Guides. - if pdf_path.exists(): - current_mtime = pdf_path.stat().st_mtime - if current_mtime > pdf_mtime: - pdf_mtime = current_mtime - iteration += 1 - await _set_progress(guide_id, f"Iteration {iteration}…") +Das PDF hat {page_count} Seite(n). Lies die Preview-Bilder und prüfe sie: +{png_list} + +FORMAT-SPEZIFIKATION (Prüfkriterien): +{spec} + +Prüfe anhand der Spezifikation: +- Stimmt die Seitenanzahl? (OnePager/Cheatsheet = exakt 1 Seite) +- Sind Elemente abgeschnitten oder überlappend? +- Fehlen Pflicht-Elemente (Cover, TOC, Recall-Boxen, Callouts, etc.)? +- Sind Code-Blöcke über Seitenumbrüche zerrissen? +- Ist das Layout korrekt (Spalten, Grid, Footer)? + +Antworte mit GENAU EINEM der folgenden Formate: + +Bei Bestehen: +PASS + +Bei Nicht-Bestehen: +FAIL +- Problem 1 +- Problem 2 +- ... +""" async def generate_guide(guide_id: str, topic: str, format_name: str) -> None: async with _semaphore: now = datetime.now(timezone.utc).isoformat() - await update_guide(guide_id, status="generating", progress="Lesen…", updated_at=now) + await update_guide(guide_id, status="generating", progress="Recherche…", updated_at=now) html_path = STORAGE_DIR / "html" / f"{guide_id}.html" pdf_path = STORAGE_DIR / "pdf" / f"{guide_id}.pdf" - - prompt = _build_prompt(topic, format_name, html_path, pdf_path) - await _set_progress(guide_id, "Generiere HTML…") - - with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False, encoding="utf-8") as f: - f.write(prompt) - prompt_file = f.name - - stop_event = asyncio.Event() - watcher = asyncio.create_task(_watch_files(guide_id, html_path, pdf_path, stop_event)) + preview_dir = STORAGE_DIR / "preview" / guide_id + timeout = GENERATION_TIMEOUTS.get(format_name, 600) + max_iter = MAX_ITERATIONS.get(format_name, 3) try: - timeout = GENERATION_TIMEOUTS.get(format_name, 600) - process = await asyncio.create_subprocess_exec( - CLAUDE_CLI, - "-p", - "--allowedTools", "Write,Bash,Read,WebSearch,WebFetch", - "--dangerously-skip-permissions", - stdin=asyncio.subprocess.PIPE, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - _active_processes[guide_id] = process - stdout, stderr = await asyncio.wait_for( - process.communicate(input=prompt.encode("utf-8")), - 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) + returncode, stdout, stderr = await _run_claude(guide_id, gen_prompt, timeout) - stop_event.set() - await watcher - - now = datetime.now(timezone.utc).isoformat() - - if process.returncode != 0: - error = stderr.decode("utf-8", errors="replace")[:2000] - await update_guide(guide_id, status="error", progress=None, error_msg=error, updated_at=now) + if returncode != 0: + await _fail(guide_id, f"Generator-Fehler: {stderr[:1000]}") return if not html_path.exists(): - await update_guide(guide_id, status="error", progress=None, error_msg="HTML-Datei wurde nicht erstellt", updated_at=now) + await _fail(guide_id, "HTML-Datei wurde nicht erstellt") return - if not pdf_path.exists(): - await update_guide(guide_id, status="error", progress=None, error_msg="PDF-Datei wurde nicht erstellt", updated_at=now) - return + # Step 2-N: Render → Review → Fix Loop + for iteration in range(1, max_iter + 1): + 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 + + # Fix-Agent + 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 + + # Final: PDF existiert bereits vom letzten Render + now = datetime.now(timezone.utc).isoformat() await update_guide( guide_id, status="done", @@ -133,15 +217,18 @@ async def generate_guide(guide_id: str, topic: str, format_name: str) -> None: ) except asyncio.TimeoutError: - stop_event.set() - await watcher - now = datetime.now(timezone.utc).isoformat() - await update_guide(guide_id, status="error", progress=None, error_msg=f"Timeout nach {timeout}s", updated_at=now) + await _fail(guide_id, f"Timeout nach {timeout}s") except Exception as e: - stop_event.set() - await watcher - now = datetime.now(timezone.utc).isoformat() - await update_guide(guide_id, status="error", progress=None, error_msg=str(e)[:2000], updated_at=now) + await _fail(guide_id, str(e)[:2000]) finally: _active_processes.pop(guide_id, None) - Path(prompt_file).unlink(missing_ok=True) + # Preview-PNGs aufräumen + if preview_dir.exists(): + for f in preview_dir.glob("*"): + f.unlink() + preview_dir.rmdir() + + +async def _fail(guide_id: str, msg: str) -> None: + now = datetime.now(timezone.utc).isoformat() + await update_guide(guide_id, status="error", progress=None, error_msg=msg, updated_at=now) diff --git a/backend/main.py b/backend/main.py index 283fc46..b0ff769 100644 --- a/backend/main.py +++ b/backend/main.py @@ -12,6 +12,7 @@ from routes import router async def lifespan(app: FastAPI): (STORAGE_DIR / "html").mkdir(parents=True, exist_ok=True) (STORAGE_DIR / "pdf").mkdir(parents=True, exist_ok=True) + (STORAGE_DIR / "preview").mkdir(parents=True, exist_ok=True) await init_db() yield diff --git a/guides.db b/guides.db deleted file mode 100644 index faab46e1a82395bad47c4cf490d21fdef7ef7c67..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI1O>7%Q6vx+1Ka<+7gi5GT#Ui1mAIxUHKNblnC47ja4XrJ~fkv~tvx%XxgYBS} zTLVJk#u0Ju5eaeR%n@-#oH%gi$blno{n4a$q>dwTVpnn;?|XhT{>^{hyqUf8{%Wi5 z!o1Vnw*8O==YmQlcs&e*Ah-*!Ie3k)3jA|?`~lvJ&riGV2GM6Uj+7W<<#PZ3zaWl>D%O<+uUtst~dO9 z?8fbt>y4GLas7?em2h}3ygVPm)yl%g$_I^b{r0%L7Bdv(rk4wwt#a z;mzji>d-LS_zu4Hq@~xl{oUS^-wEGbdFy&}wGl3Ty6bjbwloZ~)9q|_U2o_-yyd#x zPIqIww>bzfytmcgzPGVs`&$R@?PU4nt+eZG-(?%HA3vm9yF1yT*7+;fj-6b5?LsAJ zwKI3W_vt;@*KgR}erND`W5{WP4Bv}={pmtcRe05dOF`|o>i5-W&fZ^GoqzH4-=}^( z`RK&`vy_#6MO*EvQG=q zz|$urQ1*)mjucagus~Eah>RJJF^h<1RGMoT#~4r8Wz1E?m=T5z?R!zZ+Pt{*n3c0O zk(^MJ6P5t0L?FvEj;K)vTqJl*YpvC89YInY_{3cAedR*tY3b zC^7C$vNdAIY(ON6fuc&D{QJIbL;P{?+@W@pnSs52Ffeq6eucUtRh$;I5`v) z)rIJcR*drzsF%(FsC`jUjvcodMar5Kah7n%4O}Dba;~Is6jCaHI+~t*g3pG@E8;XJ z(36U|5P}_=6)1%)8VQBrCyy{)JzZ@+zpp3jtixDhlp9GA2NfaB96<>t*5#?CTI6>g zZ0DW!gQIH7r-|8Mc||-3i#I5VDO5BnB0vjIt~Uca0~ZrfBe@(Ws>`PU*1o2&G$b{t zMVd2ivdp9cF(_LQ$PaWS4OGdRDp;|dG?b4}9d%{)Xkj*1nNP$iRsv)=T0oZ99#@ou`(M=ErxOhX;_01w^X3S5RMdAxpI)wxu8=s zaIi41cRQaKJ1?7Tx}@7lw_N-2ax!QdtC$kVqMDEE%|q=*GCnREo;?m-yEy8aW0YGc zt5A|8FoB)e1+SM>N&K(dVhk*}~E1alu02=yO~?UpV?47@sK|eGW@c7mhwhg{KNfpL4R4 dg`>~W)``N==WJxIaP&EmI=(d5JmYhQ@GrpgpBexF