import asyncio import shutil import uuid from datetime import datetime, timezone from fastapi import APIRouter, HTTPException from fastapi.responses import FileResponse from agents import provider_available from config import FORMAT_META, PROJECTS_DIR, PROVIDERS from database import ( create_guide, delete_guide, get_guide, list_guides, list_progress, set_progress, delete_progress, ) from generator import generate_guide, cancel_guide, chat_with_guide from models import ( GuideCreateRequest, GuideResponse, GuideChatRequest, GuideChatResponse, ProgressUpdate, ProgressResponse, ProjectResponse, ProviderInfo, ) from paths import final_html_path, project_dir router = APIRouter(prefix="/api") @router.get("/formats") async def get_formats(): return FORMAT_META @router.get("/providers", response_model=list[ProviderInfo]) async def get_providers(): return [{"id": pid, "available": provider_available(pid)} for pid in PROVIDERS] def _safe_project_name(name: str) -> str: if not name or "/" in name or "\\" in name or ".." in name or "\x00" in name: raise HTTPException(400, "Ungültiger Projektname") return name @router.get("/projects", response_model=list[ProjectResponse]) async def list_projects(): if not PROJECTS_DIR.is_dir(): return [] return [{"name": entry.name} for entry in sorted(PROJECTS_DIR.iterdir()) if entry.is_dir()] @router.delete("/projects/{name}") async def remove_project(name: str): _safe_project_name(name) pdir = project_dir(name) if not pdir.is_dir(): raise HTTPException(404, "Projekt nicht gefunden") shutil.rmtree(pdir) return {"ok": True} @router.post("/guides", response_model=GuideResponse) async def create(req: GuideCreateRequest): now = datetime.now(timezone.utc).isoformat() guide = { "id": str(uuid.uuid4()), "topic": req.topic.strip(), "format": req.format, "instructions": req.instructions.strip(), "status": "queued", "progress": None, "created_at": now, "updated_at": now, } await create_guide(guide) asyncio.create_task(generate_guide(guide["id"], guide["topic"], guide["format"], guide["instructions"], req.provider)) return guide @router.get("/guides", response_model=list[GuideResponse]) async def list_all(): return await list_guides() @router.get("/guides/{guide_id}", response_model=GuideResponse) async def get_one(guide_id: str): guide = await get_guide(guide_id) if guide is None: raise HTTPException(404, "Guide nicht gefunden") return guide @router.get("/guides/{guide_id}/html") async def download_html(guide_id: str): guide = await get_guide(guide_id) if guide is None: raise HTTPException(404, "Guide nicht gefunden") if guide["status"] != "done": raise HTTPException(404, "HTML nicht verfügbar") html_path = final_html_path(guide["topic"], guide["format"]) if not html_path.exists(): raise HTTPException(404, "Datei nicht gefunden") return FileResponse(html_path, media_type="text/html", content_disposition_type="inline") @router.post("/guides/{guide_id}/chat", response_model=GuideChatResponse) async def guide_chat(guide_id: str, req: GuideChatRequest): guide = await get_guide(guide_id) if guide is None: raise HTTPException(404, "Guide nicht gefunden") reply = await chat_with_guide( guide["topic"], guide["format"], req.section, req.outline, [m.model_dump() for m in req.messages], provider=req.provider, ) return {"reply": reply} @router.post("/guides/{guide_id}/cancel") async def cancel(guide_id: str): cancelled = await cancel_guide(guide_id) if not cancelled: raise HTTPException(404, "Kein aktiver Prozess gefunden") return {"ok": True} @router.delete("/guides/{guide_id}") async def remove(guide_id: str): guide = await get_guide(guide_id) if guide is None: raise HTTPException(404, "Guide nicht gefunden") html_path = final_html_path(guide["topic"], guide["format"]) html_path.unlink(missing_ok=True) html_path.with_suffix(".bausteine.md").unlink(missing_ok=True) await delete_progress(guide_id) await delete_guide(guide_id) return {"ok": True} @router.get("/guides/{guide_id}/progress", response_model=ProgressResponse) async def get_progress(guide_id: str): guide = await get_guide(guide_id) if guide is None: raise HTTPException(404, "Guide nicht gefunden") return {"chapters": await list_progress(guide_id)} @router.post("/guides/{guide_id}/progress", response_model=ProgressResponse) async def update_progress(guide_id: str, req: ProgressUpdate): guide = await get_guide(guide_id) if guide is None: raise HTTPException(404, "Guide nicht gefunden") await set_progress(guide_id, req.chapter, req.done) return {"chapters": await list_progress(guide_id)}