148 lines
5.3 KiB
Python
148 lines
5.3 KiB
Python
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)
|