import asyncio import uuid from datetime import datetime, timezone from fastapi import APIRouter, HTTPException from fastapi.responses import FileResponse from config import FORMAT_META from database import ( create_guide, delete_guide, get_guide, list_guides, create_baustein as db_create_baustein, list_bausteine, get_baustein, delete_baustein as db_delete_baustein, list_suggestions, get_suggestion, update_suggestion, delete_suggestion, ) from generator import generate_guide, rework_guide, cancel_guide, generate_suggestions, generate_baustein_detail, rework_baustein, sort_bausteine, suggest_topics, is_suggestions_generating, is_sorting from models import ( GuideCreateRequest, GuideReworkRequest, GuideResponse, BausteinCreateRequest, BausteinReworkRequest, BausteinSortRequest, BausteinResponse, SuggestionResponse, TopicSuggestRequest, TopicSuggestion, ) from paths import final_paths router = APIRouter(prefix="/api") @router.get("/formats") async def get_formats(): return FORMAT_META @router.post("/topic-suggestions", response_model=list[TopicSuggestion]) async def topic_suggestions(req: TopicSuggestRequest): guides = await list_guides() existing_topics = sorted({g["topic"] for g in guides}) return await suggest_topics(req.problem.strip(), existing_topics) @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"])) 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_paths(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}/rework") async def rework(guide_id: str, req: GuideReworkRequest): guide = await get_guide(guide_id) if guide is None: raise HTTPException(404, "Guide nicht gefunden") if guide["status"] != "done": raise HTTPException(400, "Guide muss fertig sein") asyncio.create_task(rework_guide(guide_id, guide["topic"], guide["format"], req.instructions.strip())) return {"ok": True} @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.get("/guides/{guide_id}/pdf") async def download_pdf(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, "PDF nicht verfügbar") _, pdf_path = final_paths(guide["topic"], guide["format"]) if not pdf_path.exists(): raise HTTPException(404, "Datei nicht gefunden") return FileResponse(pdf_path, filename=pdf_path.name, media_type="application/pdf") @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, pdf_path = final_paths(guide["topic"], guide["format"]) html_path.unlink(missing_ok=True) pdf_path.unlink(missing_ok=True) await delete_guide(guide_id) return {"ok": True} # --- Bausteine --- @router.get("/bausteine", response_model=list[BausteinResponse]) async def get_bausteine(topic: str): return await list_bausteine(topic) @router.post("/bausteine", response_model=BausteinResponse) async def add_baustein(req: BausteinCreateRequest): now = datetime.now(timezone.utc).isoformat() baustein = { "id": str(uuid.uuid4()), "topic": req.topic.strip(), "title": req.title.strip(), "description": "", "purpose": "", "example": "", "created_at": now, "updated_at": now, } await db_create_baustein(baustein) asyncio.create_task(generate_baustein_detail(baustein["id"], baustein["topic"], baustein["title"], req.instructions.strip())) return baustein @router.delete("/bausteine/{baustein_id}") async def remove_baustein(baustein_id: str): b = await get_baustein(baustein_id) if b is None: raise HTTPException(404, "Baustein nicht gefunden") await db_delete_baustein(baustein_id) return {"ok": True} @router.post("/bausteine/{baustein_id}/rework") async def rework_baustein_route(baustein_id: str, req: BausteinReworkRequest): b = await get_baustein(baustein_id) if b is None: raise HTTPException(404, "Baustein nicht gefunden") import json try: examples = json.loads(b.get("example") or "[]") except Exception: examples = [] current = { "description": b.get("description", ""), "purpose": b.get("purpose", ""), "examples": examples, } asyncio.create_task(rework_baustein(baustein_id, b["topic"], b["title"], current, req.instructions.strip())) return {"ok": True} @router.post("/bausteine/sort") async def sort_bausteine_route(topic: str, req: BausteinSortRequest): if is_sorting(topic): return {"ok": True, "status": "already_sorting"} bausteine = await list_bausteine(topic) if not bausteine: return {"ok": True} asyncio.create_task(sort_bausteine(topic, bausteine, req.instructions.strip())) return {"ok": True} @router.get("/bausteine/sort/status") async def sort_status(topic: str): return {"sorting": is_sorting(topic)} # --- Baustein Suggestions --- @router.get("/bausteine/suggestions", response_model=list[SuggestionResponse]) async def get_suggestions(topic: str): return await list_suggestions(topic) @router.post("/bausteine/suggestions/generate") async def trigger_suggestions(topic: str): if is_suggestions_generating(topic): return {"ok": True, "status": "already_generating"} guides = await list_guides() html_paths = [] for g in guides: if g["topic"] == topic and g["status"] == "done": html_path, _ = final_paths(g["topic"], g["format"]) if html_path.exists(): html_paths.append(html_path) asyncio.create_task(generate_suggestions(topic, html_paths)) return {"ok": True} @router.get("/bausteine/suggestions/status") async def suggestions_status(topic: str): return {"generating": is_suggestions_generating(topic)} @router.post("/bausteine/suggestions/{suggestion_id}/add") async def accept_suggestion(suggestion_id: str): s = await get_suggestion(suggestion_id) if s is None: raise HTTPException(404, "Vorschlag nicht gefunden") now = datetime.now(timezone.utc).isoformat() baustein = { "id": str(uuid.uuid4()), "topic": s["topic"], "title": s["title"], "description": s["description"], "purpose": s["purpose"], "example": s["example"], "created_at": now, "updated_at": now, } await db_create_baustein(baustein) await delete_suggestion(suggestion_id) return baustein @router.post("/bausteine/suggestions/{suggestion_id}/ignore") async def ignore_suggestion(suggestion_id: str): s = await get_suggestion(suggestion_id) if s is None: raise HTTPException(404, "Vorschlag nicht gefunden") await update_suggestion(suggestion_id, status="ignored") return {"ok": True}