feat(02-01): backend prompts router with 4 CRUD endpoints
- GET /api/prompts — list all prompts with modified/default flag
- GET /api/prompts/{name} — read prompt content + required variables
- PUT /api/prompts/{name} — save modified prompt with validation
- POST /api/prompts/{name}/reset — restore prompt to default
- Lazy PromptService init to handle lifespan directory creation
- Router registered in main.py before SPA catch-all
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,7 @@ from fastapi.responses import FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from backend.config import CAMPAIGNS_PATH, CONFIG_PATH, OUTPUTS_PATH, PROMPTS_PATH
|
||||
from backend.routers import calendar, export, generate, settings
|
||||
from backend.routers import calendar, export, generate, prompts, settings
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -91,6 +91,7 @@ app.include_router(calendar.router)
|
||||
app.include_router(generate.router)
|
||||
app.include_router(export.router)
|
||||
app.include_router(settings.router)
|
||||
app.include_router(prompts.router)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
212
backend/routers/prompts.py
Normal file
212
backend/routers/prompts.py
Normal file
@@ -0,0 +1,212 @@
|
||||
"""Router per la gestione dei prompt LLM.
|
||||
|
||||
Endpoint:
|
||||
- GET /api/prompts — lista prompt disponibili con flag modificato/default
|
||||
- GET /api/prompts/{name} — contenuto, variabili richieste, flag modificato
|
||||
- PUT /api/prompts/{name} — salva contenuto modificato con validazione
|
||||
- POST /api/prompts/{name}/reset — ripristina il prompt al contenuto default originale
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from backend.config import PROMPTS_PATH
|
||||
from backend.services.prompt_service import PromptService
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api/prompts", tags=["prompts"])
|
||||
|
||||
# Directory dei prompt default (inclusi nel source, usati per confronto e reset)
|
||||
_DEFAULT_PROMPTS_DIR = Path(__file__).parent.parent / "data" / "prompts"
|
||||
|
||||
# PromptService creato lazily perche' PROMPTS_PATH viene creato nel lifespan di FastAPI
|
||||
_prompt_service: PromptService | None = None
|
||||
|
||||
|
||||
def _get_prompt_service() -> PromptService:
|
||||
"""Ritorna l'istanza PromptService, creandola al primo accesso.
|
||||
|
||||
La creazione e' lazy perche' la directory PROMPTS_PATH viene creata
|
||||
durante il lifespan di FastAPI (main.py), non al momento dell'import.
|
||||
"""
|
||||
global _prompt_service
|
||||
if _prompt_service is None:
|
||||
# Assicura che la directory esista (normalmente gia' creata dal lifespan)
|
||||
PROMPTS_PATH.mkdir(parents=True, exist_ok=True)
|
||||
_prompt_service = PromptService(PROMPTS_PATH)
|
||||
return _prompt_service
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Response / Request schemas
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class PromptInfo(BaseModel):
|
||||
"""Info sintetica di un prompt per la lista."""
|
||||
name: str
|
||||
modified: bool
|
||||
|
||||
|
||||
class PromptListResponse(BaseModel):
|
||||
"""Risposta per GET / — lista di tutti i prompt."""
|
||||
prompts: list[PromptInfo]
|
||||
|
||||
|
||||
class PromptDetailResponse(BaseModel):
|
||||
"""Risposta per GET /{name} — contenuto completo di un prompt."""
|
||||
name: str
|
||||
content: str
|
||||
variables: list[str]
|
||||
modified: bool
|
||||
|
||||
|
||||
class SavePromptRequest(BaseModel):
|
||||
"""Body per PUT /{name} — salvataggio prompt modificato."""
|
||||
content: str = Field(..., min_length=10, description="Contenuto del prompt")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _is_modified(name: str) -> bool:
|
||||
"""Confronta il prompt corrente con il default originale.
|
||||
|
||||
Returns:
|
||||
True se il contenuto differisce dal default o se il default non esiste
|
||||
(il prompt e' considerato custom).
|
||||
"""
|
||||
default_path = _DEFAULT_PROMPTS_DIR / f"{name}.txt"
|
||||
if not default_path.exists():
|
||||
return True # No default = considered custom/modified
|
||||
current = _get_prompt_service().load_prompt(name)
|
||||
default = default_path.read_text(encoding="utf-8")
|
||||
return current != default
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Endpoint
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@router.get("/", response_model=PromptListResponse)
|
||||
async def list_prompts() -> PromptListResponse:
|
||||
"""Lista tutti i prompt .txt disponibili con flag modificato/default.
|
||||
|
||||
Returns:
|
||||
PromptListResponse con la lista di PromptInfo.
|
||||
"""
|
||||
names = _get_prompt_service().list_prompts()
|
||||
prompts = [
|
||||
PromptInfo(name=name, modified=_is_modified(name))
|
||||
for name in names
|
||||
]
|
||||
return PromptListResponse(prompts=prompts)
|
||||
|
||||
|
||||
@router.get("/{name}", response_model=PromptDetailResponse)
|
||||
async def get_prompt(name: str) -> PromptDetailResponse:
|
||||
"""Leggi il contenuto di un prompt con le variabili richieste.
|
||||
|
||||
Args:
|
||||
name: Nome del prompt senza estensione (es. "pas_valore")
|
||||
|
||||
Returns:
|
||||
PromptDetailResponse con contenuto, variabili, e flag modificato.
|
||||
|
||||
Raises:
|
||||
HTTPException 404: Se il prompt non esiste.
|
||||
"""
|
||||
if not _get_prompt_service().prompt_exists(name):
|
||||
raise HTTPException(status_code=404, detail=f"Prompt '{name}' non trovato")
|
||||
|
||||
content = _get_prompt_service().load_prompt(name)
|
||||
variables = _get_prompt_service().get_required_variables(name)
|
||||
modified = _is_modified(name)
|
||||
|
||||
return PromptDetailResponse(
|
||||
name=name,
|
||||
content=content,
|
||||
variables=variables,
|
||||
modified=modified,
|
||||
)
|
||||
|
||||
|
||||
@router.put("/{name}", response_model=PromptDetailResponse)
|
||||
async def save_prompt(name: str, body: SavePromptRequest) -> PromptDetailResponse:
|
||||
"""Salva il contenuto modificato di un prompt.
|
||||
|
||||
Dopo il salvataggio, ritorna i dati aggiornati incluse le variabili
|
||||
ricalcolate dal nuovo contenuto.
|
||||
|
||||
Args:
|
||||
name: Nome del prompt senza estensione
|
||||
body: SavePromptRequest con il contenuto da salvare
|
||||
|
||||
Returns:
|
||||
PromptDetailResponse con i dati aggiornati.
|
||||
|
||||
Raises:
|
||||
HTTPException 400: Se il nome contiene caratteri non validi.
|
||||
"""
|
||||
try:
|
||||
_get_prompt_service().save_prompt(name, body.content)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
# Ricarica variabili dal contenuto appena salvato
|
||||
variables = _get_prompt_service().get_required_variables(name)
|
||||
modified = _is_modified(name)
|
||||
|
||||
logger.info("Prompt '%s' salvato | variables=%s | modified=%s", name, variables, modified)
|
||||
|
||||
return PromptDetailResponse(
|
||||
name=name,
|
||||
content=body.content,
|
||||
variables=variables,
|
||||
modified=modified,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{name}/reset", response_model=PromptDetailResponse)
|
||||
async def reset_prompt(name: str) -> PromptDetailResponse:
|
||||
"""Ripristina un prompt al contenuto default originale.
|
||||
|
||||
Copia il file default dalla directory sorgente sovrascrivendo il corrente.
|
||||
|
||||
Args:
|
||||
name: Nome del prompt senza estensione
|
||||
|
||||
Returns:
|
||||
PromptDetailResponse con il contenuto ripristinato.
|
||||
|
||||
Raises:
|
||||
HTTPException 404: Se il default non esiste per questo prompt.
|
||||
"""
|
||||
default_path = _DEFAULT_PROMPTS_DIR / f"{name}.txt"
|
||||
if not default_path.exists():
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Nessun default disponibile per il prompt '{name}'",
|
||||
)
|
||||
|
||||
# Leggi il default e sovrascrivilo nella directory runtime
|
||||
default_content = default_path.read_text(encoding="utf-8")
|
||||
_get_prompt_service().save_prompt(name, default_content)
|
||||
|
||||
variables = _get_prompt_service().get_required_variables(name)
|
||||
|
||||
logger.info("Prompt '%s' ripristinato al default | variables=%s", name, variables)
|
||||
|
||||
return PromptDetailResponse(
|
||||
name=name,
|
||||
content=default_content,
|
||||
variables=variables,
|
||||
modified=False, # Appena ripristinato = identico al default
|
||||
)
|
||||
Reference in New Issue
Block a user