from __future__ import annotations import uuid from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, Field from auth import require_admin_dep, require_auth from db import get_db, row_to_dict from engine import EngineError, execute_prompt, preview_prompt router = APIRouter(prefix="/api/prompts", tags=["prompts"]) LIST_FIELDS = """ id, slug, name, description, category, prompt_type, template, required_feature, active, is_system_default, sort_order, created, updated """ class PromptWrite(BaseModel): slug: str = Field(min_length=1, max_length=80) name: str = Field(min_length=1) description: str = "" category: str = "uncategorized" prompt_type: str = "base" template: str = "" required_feature: str = "ai_calls" active: bool = True class PreviewRequest(BaseModel): slug: str | None = None prompt_id: str | None = None class ExecuteRequest(BaseModel): slug: str | None = None prompt_id: str | None = None purpose: str = "reflection" data_class: str = Field(default="B") def _http(exc: EngineError): raise HTTPException(status_code=exc.status_code, detail={"code": exc.code, "message": exc.message}) from exc def _load(conn, prompt_id: str | None, slug: str | None) -> dict: if prompt_id: row = conn.execute("SELECT * FROM ai_prompts WHERE id = ?", (prompt_id,)).fetchone() elif slug: row = conn.execute("SELECT * FROM ai_prompts WHERE slug = ?", (slug,)).fetchone() else: raise HTTPException(400, "prompt_id oder slug ist erforderlich") prompt = row_to_dict(row) if not prompt: raise HTTPException(404, "Prompt nicht gefunden") return prompt @router.get("") def list_prompts(session: dict = Depends(require_admin_dep)): with get_db() as conn: rows = conn.execute(f"SELECT {LIST_FIELDS} FROM ai_prompts ORDER BY sort_order, name").fetchall() return [row_to_dict(row) for row in rows] @router.post("") def create_prompt(req: PromptWrite, session: dict = Depends(require_admin_dep)): if req.prompt_type not in {"base", "pipeline", "workflow"}: raise HTTPException(400, "Ungültiger Prompt-Typ") prompt_id = str(uuid.uuid4()) if req.prompt_type == "base" and req.template: try: preview_prompt( { "id": prompt_id, "slug": req.slug.strip(), "prompt_type": req.prompt_type, "template": req.template, }, {}, ) except EngineError as exc: _http(exc) try: with get_db() as conn: conn.execute( """ INSERT INTO ai_prompts (id, slug, name, description, category, prompt_type, template, required_feature, active, default_template) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( prompt_id, req.slug.strip(), req.name.strip(), req.description, req.category.strip() or "uncategorized", req.prompt_type, req.template, req.required_feature, 1 if req.active else 0, req.template, ), ) return row_to_dict(conn.execute(f"SELECT {LIST_FIELDS} FROM ai_prompts WHERE id = ?", (prompt_id,)).fetchone()) except Exception as exc: if "UNIQUE" in str(exc).upper(): raise HTTPException(409, "Slug ist bereits vergeben") from exc raise @router.put("/{prompt_id}") def update_prompt(prompt_id: str, req: PromptWrite, session: dict = Depends(require_admin_dep)): with get_db() as conn: current = row_to_dict(conn.execute("SELECT id FROM ai_prompts WHERE id = ?", (prompt_id,)).fetchone()) if not current: raise HTTPException(404, "Prompt nicht gefunden") if req.prompt_type == "base" and req.template: try: preview_prompt( { "id": prompt_id, "slug": req.slug.strip(), "prompt_type": req.prompt_type, "template": req.template, }, {}, ) except EngineError as exc: _http(exc) conn.execute( """ UPDATE ai_prompts SET slug = ?, name = ?, description = ?, category = ?, prompt_type = ?, template = ?, required_feature = ?, active = ?, updated = datetime('now') WHERE id = ? """, ( req.slug.strip(), req.name.strip(), req.description, req.category.strip() or "uncategorized", req.prompt_type, req.template, req.required_feature, 1 if req.active else 0, prompt_id, ), ) return row_to_dict(conn.execute(f"SELECT {LIST_FIELDS} FROM ai_prompts WHERE id = ?", (prompt_id,)).fetchone()) @router.post("/{prompt_id}/reset") def reset_prompt(prompt_id: str, session: dict = Depends(require_admin_dep)): with get_db() as conn: prompt = row_to_dict(conn.execute("SELECT * FROM ai_prompts WHERE id = ?", (prompt_id,)).fetchone()) if not prompt: raise HTTPException(404, "Prompt nicht gefunden") if not prompt["is_system_default"]: raise HTTPException(400, "Nur System-Defaults können zurückgesetzt werden") conn.execute( "UPDATE ai_prompts SET template = default_template, updated = datetime('now') WHERE id = ?", (prompt_id,), ) return row_to_dict(conn.execute(f"SELECT {LIST_FIELDS} FROM ai_prompts WHERE id = ?", (prompt_id,)).fetchone()) @router.post("/preview") def preview(req: PreviewRequest, session: dict = Depends(require_admin_dep)): with get_db() as conn: prompt = _load(conn, req.prompt_id, req.slug) try: return preview_prompt(prompt, {"profile_id": session["profile_id"]}) except EngineError as exc: _http(exc) @router.post("/execute") def execute(req: ExecuteRequest, session: dict = Depends(require_auth)): with get_db() as conn: prompt = _load(conn, req.prompt_id, req.slug) if not prompt["active"] and session.get("role") != "admin": raise HTTPException(404, "Prompt nicht gefunden") try: return execute_prompt( prompt, profile_id=session["profile_id"], purpose=req.purpose, data_class=req.data_class, context={"profile_id": session["profile_id"]}, ) except EngineError as exc: _http(exc)