import asyncio import tempfile from datetime import datetime, timezone from pathlib import Path from config import ( CLAUDE_CLI, DOC_DIR, GENERATION_TIMEOUTS, MAX_CONCURRENT_GENERATIONS, STORAGE_DIR, ) from database import update_guide _semaphore = asyncio.Semaphore(MAX_CONCURRENT_GENERATIONS) _active_processes: dict[str, asyncio.subprocess.Process] = {} async def cancel_guide(guide_id: str) -> bool: process = _active_processes.get(guide_id) if process and process.returncode is None: process.kill() now = datetime.now(timezone.utc).isoformat() await update_guide(guide_id, status="error", progress=None, error_msg="Abgebrochen", updated_at=now) return True return False def _build_prompt(topic: str, format_name: str, html_path: Path, pdf_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") return f"""Erstelle einen Lern-Guide zum Thema "{topic}" im Format "{format_name}". 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} FORMAT-SPEZIFIKATION: {spec} REFERENZ-IMPLEMENTIERUNG (Stil-Vorlage, adaptiere für "{topic}"): {reference} """ 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 _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 while not stop_event.is_set(): await asyncio.sleep(2) if not html_seen and html_path.exists(): html_seen = True await _set_progress(guide_id, "HTML generiert…") 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}…") 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) 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)) 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, ) 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) 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) 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 await update_guide( guide_id, status="done", progress=None, html_path=str(html_path), pdf_path=str(pdf_path), updated_at=now, ) 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) 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) finally: _active_processes.pop(guide_id, None) Path(prompt_file).unlink(missing_ok=True)