"""PromptService — carica, lista e compila prompt .txt con variabili. Gestisce i file .txt dei prompt LLM nella directory PROMPTS_PATH. Usa la sintassi {{variabile}} per i placeholder (doppia graffa). """ from __future__ import annotations import re from pathlib import Path from typing import Optional # Pattern per trovare le variabili {{nome}} nei template _VARIABLE_PATTERN = re.compile(r"\{\{(\w+)\}\}") class PromptService: """Servizio per gestire i prompt .txt del sistema di generazione. Fornisce metodi per: - Elencare i prompt disponibili - Caricare il contenuto di un prompt - Compilare un prompt sostituendo le variabili {{...}} - Salvare un prompt (per l'editor di Phase 2) - Estrarre la lista di variabili richieste da un template """ def __init__(self, prompts_dir: Path) -> None: """Inizializza il servizio con la directory dei prompt. Args: prompts_dir: Path alla directory contenente i file .txt dei prompt. Tipicamente PROMPTS_PATH da backend.config. Raises: FileNotFoundError: Se la directory non esiste. """ if not prompts_dir.exists(): raise FileNotFoundError( f"Directory prompt non trovata: {prompts_dir}. " "Verifica che PROMPTS_PATH sia configurato correttamente." ) if not prompts_dir.is_dir(): raise NotADirectoryError( f"Il percorso non è una directory: {prompts_dir}" ) self._prompts_dir = prompts_dir def list_prompts(self) -> list[str]: """Elenca tutti i prompt .txt disponibili nella directory. Returns: Lista di nomi file senza estensione, ordinata alfabeticamente. Es: ['aida_promozione', 'bab_storytelling', 'system_prompt', ...] """ return sorted( p.stem for p in self._prompts_dir.glob("*.txt") if p.is_file() ) def load_prompt(self, name: str) -> str: """Carica il contenuto grezzo di un prompt .txt. Args: name: Nome del prompt senza estensione (es. "pas_valore") Returns: Contenuto testuale del file prompt Raises: FileNotFoundError: Se il file non esiste """ path = self._get_path(name) if not path.exists(): available = self.list_prompts() raise FileNotFoundError( f"Prompt '{name}' non trovato in {self._prompts_dir}. " f"Prompt disponibili: {available}" ) return path.read_text(encoding="utf-8") def compile_prompt(self, name: str, variables: dict[str, str]) -> str: """Carica un prompt e sostituisce tutte le variabili {{nome}} con i valori forniti. Args: name: Nome del prompt senza estensione variables: Dizionario { nome_variabile: valore } Returns: Testo del prompt con tutte le variabili sostituite Raises: FileNotFoundError: Se il prompt non esiste ValueError: Se una variabile nel template non ha corrispondenza nel dict """ template = self.load_prompt(name) # Verifica che tutte le variabili del template siano nel dict required = set(_VARIABLE_PATTERN.findall(template)) provided = set(variables.keys()) missing = required - provided if missing: raise ValueError( f"Variabili mancanti per il prompt '{name}': {sorted(missing)}. " f"Fornire: {sorted(required)}" ) def replace_var(match: re.Match) -> str: var_name = match.group(1) return variables[var_name] return _VARIABLE_PATTERN.sub(replace_var, template) def save_prompt(self, name: str, content: str) -> None: """Salva il contenuto di un prompt nel file .txt. Usato dall'editor di prompt in Phase 2. Args: name: Nome del prompt senza estensione content: Contenuto testuale da salvare Raises: ValueError: Se il nome contiene caratteri non sicuri """ # Sicurezza: validazione nome file (solo lettere, cifre, underscore, trattino) if not re.match(r"^[\w\-]+$", name): raise ValueError( f"Nome prompt non valido: '{name}'. " "Usa solo lettere, cifre, underscore e trattino." ) path = self._get_path(name) path.write_text(content, encoding="utf-8") def get_required_variables(self, name: str) -> list[str]: """Analizza il template e ritorna la lista delle variabili richieste. Args: name: Nome del prompt senza estensione Returns: Lista ordinata di nomi variabile (senza doppie graffe) Es: ['brand_name', 'livello_schwartz', 'obiettivo_campagna', 'target_nicchia', 'topic'] Raises: FileNotFoundError: Se il prompt non esiste """ template = self.load_prompt(name) variables = sorted(set(_VARIABLE_PATTERN.findall(template))) return variables def prompt_exists(self, name: str) -> bool: """Verifica se un prompt esiste. Args: name: Nome del prompt senza estensione Returns: True se il file esiste """ return self._get_path(name).exists() # --------------------------------------------------------------------------- # Metodi privati # --------------------------------------------------------------------------- def _get_path(self, name: str) -> Path: """Costruisce il percorso completo per un file prompt.""" return self._prompts_dir / f"{name}.txt"