215 lines
6.8 KiB
Python
215 lines
6.8 KiB
Python
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 PROJECTS_DIR, PROVIDERS
|
|
from database import (
|
|
create_guide, delete_guide, get_guide, list_guides,
|
|
create_topic, list_topics as db_list_topics, delete_topic,
|
|
list_progress, set_progress, delete_progress,
|
|
)
|
|
from generator import (
|
|
generate_guide, cancel_guide, chat_with_guide,
|
|
generate_bausteine, cancel_bausteine, bausteine_status, active_bausteine, reset_bausteine,
|
|
)
|
|
from models import (
|
|
GuideCreateRequest, GuideResponse,
|
|
TopicCreateRequest,
|
|
BausteineCreateRequest, BausteineStatusResponse,
|
|
GuideChatRequest, GuideChatResponse,
|
|
ProgressUpdate, ProgressResponse, ProjectResponse, ProviderInfo,
|
|
)
|
|
from paths import bausteine_path, bausteine_topics, guide_content_path, project_dir, topic_dir
|
|
|
|
router = APIRouter(prefix="/api")
|
|
|
|
|
|
@router.get("/providers", response_model=list[ProviderInfo])
|
|
async def get_providers():
|
|
return [{"id": pid, "available": provider_available(pid)} for pid in PROVIDERS]
|
|
|
|
|
|
@router.get("/topics")
|
|
async def get_topics():
|
|
db_topics = await db_list_topics()
|
|
guides = await list_guides()
|
|
derived = {g["topic"] for g in guides}
|
|
derived.update(bausteine_topics())
|
|
derived.update(job["topic"] for job in active_bausteine())
|
|
# DB ist führend (Reihenfolge: neueste zuerst); Abgeleitetes ohne DB-Eintrag hinten anhängen
|
|
return db_topics + sorted(derived - set(db_topics))
|
|
|
|
|
|
@router.post("/topics")
|
|
async def add_topic(req: TopicCreateRequest):
|
|
await create_topic(req.name.strip())
|
|
return {"ok": True}
|
|
|
|
|
|
@router.delete("/topics")
|
|
async def remove_topic(topic: str):
|
|
await delete_topic(topic)
|
|
shutil.rmtree(topic_dir(topic), ignore_errors=True)
|
|
return {"ok": True}
|
|
|
|
|
|
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}
|
|
|
|
|
|
# --- Bausteine ---
|
|
|
|
@router.get("/bausteine/status", response_model=BausteineStatusResponse)
|
|
async def get_bausteine_status(topic: str):
|
|
return bausteine_status(topic)
|
|
|
|
|
|
@router.get("/bausteine/active")
|
|
async def get_active_bausteine():
|
|
return active_bausteine()
|
|
|
|
|
|
@router.post("/bausteine")
|
|
async def create_bausteine(req: BausteineCreateRequest):
|
|
topic = req.topic.strip()
|
|
if bausteine_status(topic)["generating"]:
|
|
return {"ok": True, "status": "already_generating"}
|
|
await create_topic(topic)
|
|
asyncio.create_task(generate_bausteine(topic, req.instructions.strip(), req.provider))
|
|
return {"ok": True}
|
|
|
|
|
|
@router.post("/bausteine/cancel")
|
|
async def cancel_bausteine_route(topic: str):
|
|
if not cancel_bausteine(topic):
|
|
raise HTTPException(404, "Keine laufende Generierung")
|
|
return {"ok": True}
|
|
|
|
|
|
@router.delete("/bausteine")
|
|
async def remove_bausteine(topic: str):
|
|
reset_bausteine(topic)
|
|
return {"ok": True}
|
|
|
|
|
|
# --- Guides ---
|
|
|
|
@router.post("/guides", response_model=GuideResponse)
|
|
async def create(req: GuideCreateRequest):
|
|
if req.format != "OnePager" and not bausteine_path(req.topic.strip()).exists():
|
|
raise HTTPException(400, "Erst Bausteine erstellen")
|
|
await create_topic(req.topic.strip())
|
|
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}/content")
|
|
async def guide_content(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, "Inhalt nicht verfügbar")
|
|
path = guide_content_path(guide["topic"], guide["format"])
|
|
if not path.exists():
|
|
raise HTTPException(404, "Datei nicht gefunden")
|
|
return FileResponse(path, media_type="application/json")
|
|
|
|
|
|
@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")
|
|
guide_content_path(guide["topic"], guide["format"]).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)}
|