"""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 )