This commit is contained in:
Team3
2026-06-06 00:14:43 +02:00
parent 3ed5f7c3e5
commit a8fbf83059
39 changed files with 7347 additions and 472 deletions

178
backend/generator.py Normal file
View File

@@ -0,0 +1,178 @@
import asyncio
import uuid
from datetime import datetime, timezone
from pathlib import Path
from agents import run_agent, kill_process
from config import (
AGENT_TIMEOUT,
DEFAULT_PROVIDER,
TEMPLATES_DIR,
MAX_CONCURRENT_GENERATIONS,
)
from database import update_guide
from paths import final_html_path, project_dir
_semaphore = asyncio.Semaphore(MAX_CONCURRENT_GENERATIONS)
_cancelled: set[str] = set()
async def cancel_guide(guide_id: str) -> bool:
_cancelled.add(guide_id)
kill_process(guide_id)
now = datetime.now(timezone.utc).isoformat()
await update_guide(guide_id, status="error", progress=None, error_msg="Abgebrochen", updated_at=now)
return True
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)
# Welche Baustein-Kategorien jedes Format abdeckt.
FORMAT_COVERAGE = {
"OnePager": "NUR die KERN-Bausteine, maximal verdichtet",
"MiniGuide": "NUR die KERN-Bausteine",
"Guide": "die KERN- und WICHTIG-Bausteine",
}
def _prompt(name: str, **kwargs) -> str:
template = (TEMPLATES_DIR / "Prompt" / f"{name}.md").read_text(encoding="utf-8")
return template.format(**kwargs)
def _extra(instructions: str) -> str:
return f"\n\nZUSÄTZLICHE ANWEISUNGEN VOM NUTZER:\n{instructions}\n" if instructions else ""
def _build_bausteine_prompt(topic: str, bausteine_path: Path, instructions: str = "", project: Path | None = None) -> str:
if project:
source = _prompt("Bausteine-Quelle-Projekt", project=project)
else:
source = _prompt("Bausteine-Quelle-Thema", topic=topic)
return _prompt(
"Bausteine",
topic=topic, source=source, bausteine_path=bausteine_path, extra=_extra(instructions),
)
def _build_guide_prompt(topic: str, format_name: str, html_path: Path, bausteine: str, instructions: str = "", project: Path | None = None) -> str:
spec = (TEMPLATES_DIR / "Format" / f"{format_name}.md").read_text(encoding="utf-8")
reference = (TEMPLATES_DIR / "Referenz" / f"{format_name}.md").read_text(encoding="utf-8")
if project:
facts = _prompt("Guide-Fakten-Projekt", project=project)
else:
facts = _prompt("Guide-Fakten-Thema")
return _prompt(
"Guide",
topic=topic, format_name=format_name, html_path=html_path,
bausteine=bausteine, coverage=FORMAT_COVERAGE[format_name],
facts=facts, spec=spec, reference=reference, extra=_extra(instructions),
)
async def generate_guide(guide_id: str, topic: str, format_name: str, instructions: str = "", provider: str = DEFAULT_PROVIDER) -> None:
async with _semaphore:
now = datetime.now(timezone.utc).isoformat()
await update_guide(guide_id, status="generating", progress="Ermittle Bausteine…", updated_at=now)
html_path = final_html_path(topic, format_name)
bausteine_path = html_path.with_suffix(".bausteine.md")
project = project_dir(topic) if project_dir(topic).is_dir() else None
try:
if guide_id in _cancelled:
return
# Step 1: Bausteine ermitteln (Thema: Websuche, Projekt: Dateien lesen)
current_step = "Bausteine"
bs_prompt = _build_bausteine_prompt(topic, bausteine_path, instructions, project)
returncode, bs_out, bs_err = await run_agent(
guide_id, bs_prompt, AGENT_TIMEOUT,
provider=provider, role="fast", capabilities="files" if project else "full",
)
if guide_id in _cancelled:
return
if returncode != 0:
await _fail(guide_id, _claude_error("Baustein-Fehler", returncode, bs_out, bs_err))
return
if not bausteine_path.exists():
await _fail(guide_id, "Baustein-Datei wurde nicht erstellt")
return
bausteine = bausteine_path.read_text(encoding="utf-8")
# Step 2: Generator-Agent erstellt HTML nach Bausteinen
await _set_progress(guide_id, "Generiere HTML…")
current_step = "Generierung"
gen_prompt = _build_guide_prompt(topic, format_name, html_path, bausteine, instructions, project)
returncode, stdout, stderr = await run_agent(guide_id, gen_prompt, AGENT_TIMEOUT, provider=provider, role="guide", capabilities="full")
if guide_id in _cancelled:
return
if returncode != 0:
await _fail(guide_id, _claude_error("Generator-Fehler", returncode, stdout, stderr))
return
if not html_path.exists():
await _fail(guide_id, "HTML-Datei wurde nicht erstellt")
return
now = datetime.now(timezone.utc).isoformat()
await update_guide(
guide_id, status="done", progress=None, updated_at=now,
)
except asyncio.TimeoutError:
await _fail(guide_id, f"Timeout bei {current_step} nach {AGENT_TIMEOUT}s")
except Exception as e:
await _fail(guide_id, str(e)[:2000])
finally:
_cancelled.discard(guide_id)
def _claude_error(label: str, returncode: int, stdout: str, stderr: str) -> str:
stderr = (stderr or "").strip()
if stderr:
return f"{label}: {stderr[:1000]}"
tail = (stdout or "").strip()[-500:]
if tail:
return f"{label} (exit {returncode}, stderr leer): …{tail}"
return f"{label} (exit {returncode}, ohne Ausgabe)"
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)
def _build_guide_chat_prompt(topic: str, format_name: str, section: str, outline: str, messages: list[dict]) -> str:
transcript = "\n".join(
f"{'Nutzer' if m.get('role') == 'user' else 'Assistent'}: {m.get('content', '')}"
for m in messages
)
return _prompt(
"Chat",
topic=topic, format_name=format_name,
outline_block=outline.strip() or "(keine)",
section_block=section.strip() or "(kein Abschnitt erkannt)",
transcript=transcript,
)
async def chat_with_guide(topic: str, format_name: str, section: str, outline: str, messages: list[dict], provider: str = DEFAULT_PROVIDER) -> str:
try:
prompt = _build_guide_chat_prompt(topic, format_name, section, outline, messages)
returncode, stdout, stderr = await run_agent(
"chat-" + str(uuid.uuid4()), prompt, 240, provider=provider, role="fast", capabilities="none"
)
if returncode != 0:
return "Entschuldigung, das hat nicht geklappt. Bitte versuche es erneut."
reply = stdout.strip()
return reply or "Entschuldigung, ich habe keine Antwort erhalten."
except Exception:
return "Entschuldigung, das hat nicht geklappt. Bitte versuche es erneut."