Kansho/backend/routers/prompts.py
2026-08-25 13:57:23 +02:00

196 lines
6.8 KiB
Python

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)