Files
leopost-full/backend/app/services/prompt_service.py
Michele 519a580679 Initial commit: Leopost Full — merge di Leopost, Post Generator e Autopilot OS
- Backend FastAPI con multi-LLM (Claude/OpenAI/Gemini)
- Publishing su Facebook, Instagram, YouTube, TikTok
- Calendario editoriale con awareness levels (PAS, AIDA, BAB...)
- Design system Editorial Fresh (Fraunces + DM Sans)
- Scheduler automatico, gestione commenti AI, affiliate links

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 17:23:16 +02:00

170 lines
5.7 KiB
Python

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