update
This commit is contained in:
@@ -24,6 +24,15 @@ FORMAT_META = {
|
|||||||
"ExtendedGuide": {"pages": "47-60 Seiten", "time": "~5h"},
|
"ExtendedGuide": {"pages": "47-60 Seiten", "time": "~5h"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
REVIEW_TIMEOUTS = {
|
||||||
|
"OnePager": 120,
|
||||||
|
"Cheatsheet": 120,
|
||||||
|
"MiniGuide": 180,
|
||||||
|
"BeginnerGuide": 600,
|
||||||
|
"IntermediateGuide": 600,
|
||||||
|
"ExtendedGuide": 900,
|
||||||
|
}
|
||||||
|
|
||||||
GENERATION_TIMEOUTS = {
|
GENERATION_TIMEOUTS = {
|
||||||
"OnePager": 900,
|
"OnePager": 900,
|
||||||
"Cheatsheet": 900,
|
"Cheatsheet": 900,
|
||||||
@@ -34,12 +43,12 @@ GENERATION_TIMEOUTS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MAX_CONCURRENT_GENERATIONS = 6
|
MAX_CONCURRENT_GENERATIONS = 6
|
||||||
MAX_ITERATIONS = {
|
CONTENT_REVIEW_ITERATIONS = {
|
||||||
"OnePager": 3,
|
"OnePager": 1,
|
||||||
"Cheatsheet": 3,
|
"Cheatsheet": 1,
|
||||||
"MiniGuide": 3,
|
"MiniGuide": 2,
|
||||||
"BeginnerGuide": 5,
|
"BeginnerGuide": 2,
|
||||||
"IntermediateGuide": 5,
|
"IntermediateGuide": 2,
|
||||||
"ExtendedGuide": 5,
|
"ExtendedGuide": 2,
|
||||||
}
|
}
|
||||||
CLAUDE_CLI = "claude"
|
CLAUDE_CLI = "claude"
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ from pathlib import Path
|
|||||||
|
|
||||||
from config import (
|
from config import (
|
||||||
CLAUDE_CLI,
|
CLAUDE_CLI,
|
||||||
|
CONTENT_REVIEW_ITERATIONS,
|
||||||
TEMPLATES_DIR,
|
TEMPLATES_DIR,
|
||||||
GENERATION_TIMEOUTS,
|
GENERATION_TIMEOUTS,
|
||||||
MAX_CONCURRENT_GENERATIONS,
|
MAX_CONCURRENT_GENERATIONS,
|
||||||
MAX_ITERATIONS,
|
REVIEW_TIMEOUTS,
|
||||||
STORAGE_DIR,
|
STORAGE_DIR,
|
||||||
)
|
)
|
||||||
from database import update_guide
|
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:
|
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")
|
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"
|
pdf_path = STORAGE_DIR / "pdf" / f"{guide_id}.pdf"
|
||||||
preview_dir = STORAGE_DIR / "preview" / guide_id
|
preview_dir = STORAGE_DIR / "preview" / guide_id
|
||||||
timeout = GENERATION_TIMEOUTS.get(format_name, 600)
|
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:
|
try:
|
||||||
if guide_id in _cancelled:
|
if guide_id in _cancelled:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
current_step = "Generierung"
|
||||||
|
current_timeout = timeout
|
||||||
|
|
||||||
# Step 1: Generator-Agent erstellt HTML
|
# Step 1: Generator-Agent erstellt HTML
|
||||||
await _set_progress(guide_id, "Generiere HTML…")
|
await _set_progress(guide_id, "Generiere HTML…")
|
||||||
gen_prompt = _build_generator_prompt(topic, format_name, html_path, instructions)
|
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")
|
await _fail(guide_id, "HTML-Datei wurde nicht erstellt")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Step 2-N: Render → Review → Fix Loop
|
# Phase 1: Inhalts-Review (HTML lesen + Websuche, kein PDF)
|
||||||
for iteration in range(1, max_iter + 1):
|
for iteration in range(1, content_iters + 1):
|
||||||
if guide_id in _cancelled:
|
if guide_id in _cancelled:
|
||||||
return
|
return
|
||||||
|
|
||||||
await _set_progress(guide_id, f"Rendere PDF… (Iteration {iteration})")
|
await _set_progress(guide_id, f"Prüfe Inhalt… ({iteration}/{content_iters})")
|
||||||
ok, err = await _render_pdf(html_path, pdf_path)
|
current_step = f"Inhalts-Review ({iteration}/{content_iters})"
|
||||||
if not ok:
|
current_timeout = review_timeout
|
||||||
await _fail(guide_id, f"WeasyPrint-Fehler: {err}")
|
content_prompt = _build_content_review_prompt(topic, format_name, html_path)
|
||||||
return
|
returncode, review_out, review_err = await _run_claude(guide_id, content_prompt, review_timeout)
|
||||||
|
|
||||||
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:
|
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
|
return
|
||||||
|
|
||||||
review_text = review_out.strip()
|
review_text = review_out.strip()
|
||||||
|
|
||||||
if review_text.startswith("PASS"):
|
if review_text.startswith("PASS"):
|
||||||
break
|
break
|
||||||
|
|
||||||
if iteration == max_iter:
|
if iteration == content_iters:
|
||||||
break
|
break
|
||||||
|
|
||||||
# Fix-Agent
|
|
||||||
feedback = review_text.replace("FAIL", "").strip()
|
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)
|
fix_prompt = _build_fix_prompt(topic, format_name, html_path, feedback)
|
||||||
returncode, _, fix_err = await _run_claude(guide_id, fix_prompt, timeout)
|
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]}")
|
await _fail(guide_id, f"Fix-Fehler: {fix_err[:1000]}")
|
||||||
return
|
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()
|
now = datetime.now(timezone.utc).isoformat()
|
||||||
await update_guide(
|
await update_guide(
|
||||||
guide_id,
|
guide_id,
|
||||||
@@ -257,7 +331,7 @@ async def generate_guide(guide_id: str, topic: str, format_name: str, instructio
|
|||||||
)
|
)
|
||||||
|
|
||||||
except asyncio.TimeoutError:
|
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:
|
except Exception as e:
|
||||||
await _fail(guide_id, str(e)[:2000])
|
await _fail(guide_id, str(e)[:2000])
|
||||||
finally:
|
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"
|
html_path = STORAGE_DIR / "html" / f"{guide_id}.html"
|
||||||
pdf_path = STORAGE_DIR / "pdf" / f"{guide_id}.pdf"
|
pdf_path = STORAGE_DIR / "pdf" / f"{guide_id}.pdf"
|
||||||
preview_dir = STORAGE_DIR / "preview" / guide_id
|
|
||||||
timeout = GENERATION_TIMEOUTS.get(format_name, 600)
|
timeout = GENERATION_TIMEOUTS.get(format_name, 600)
|
||||||
max_iter = MAX_ITERATIONS.get(format_name, 3)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if guide_id in _cancelled:
|
if guide_id in _cancelled:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
current_step = "Überarbeitung"
|
||||||
|
current_timeout = timeout
|
||||||
|
|
||||||
rework_prompt = _build_rework_prompt(topic, format_name, html_path, instructions)
|
rework_prompt = _build_rework_prompt(topic, format_name, html_path, instructions)
|
||||||
returncode, stdout, stderr = await _run_claude(guide_id, rework_prompt, timeout)
|
returncode, stdout, stderr = await _run_claude(guide_id, rework_prompt, timeout)
|
||||||
|
|
||||||
@@ -298,41 +373,12 @@ async def rework_guide(guide_id: str, topic: str, format_name: str, instructions
|
|||||||
await _fail(guide_id, "HTML-Datei wurde nicht erstellt")
|
await _fail(guide_id, "HTML-Datei wurde nicht erstellt")
|
||||||
return
|
return
|
||||||
|
|
||||||
for iteration in range(1, max_iter + 1):
|
await _set_progress(guide_id, "Rendere PDF…")
|
||||||
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)
|
ok, err = await _render_pdf(html_path, pdf_path)
|
||||||
if not ok:
|
if not ok:
|
||||||
await _fail(guide_id, f"WeasyPrint-Fehler: {err}")
|
await _fail(guide_id, f"WeasyPrint-Fehler: {err}")
|
||||||
return
|
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
|
|
||||||
|
|
||||||
now = datetime.now(timezone.utc).isoformat()
|
now = datetime.now(timezone.utc).isoformat()
|
||||||
await update_guide(
|
await update_guide(
|
||||||
guide_id, status="done", progress=None,
|
guide_id, status="done", progress=None,
|
||||||
@@ -340,16 +386,12 @@ async def rework_guide(guide_id: str, topic: str, format_name: str, instructions
|
|||||||
)
|
)
|
||||||
|
|
||||||
except asyncio.TimeoutError:
|
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:
|
except Exception as e:
|
||||||
await _fail(guide_id, str(e)[:2000])
|
await _fail(guide_id, str(e)[:2000])
|
||||||
finally:
|
finally:
|
||||||
_active_processes.pop(guide_id, None)
|
_active_processes.pop(guide_id, None)
|
||||||
_cancelled.discard(guide_id)
|
_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:
|
async def _fail(guide_id: str, msg: str) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user