"""Pydantic schemas per la generazione LLM di post e caroselli. Questi modelli rappresentano l'output del processo di generazione, dalla singola slide fino alla risposta batch completa. """ from __future__ import annotations from typing import Literal, Optional from pydantic import BaseModel, Field from backend.schemas.calendar import CalendarSlot # --------------------------------------------------------------------------- # Output LLM — struttura interna del carosello generato # --------------------------------------------------------------------------- class SlideContent(BaseModel): """Contenuto di una singola slide centrale (s2-s7) del carosello.""" headline: str = Field( ..., max_length=80, description="Titolo breve della slide (max 80 caratteri, impatto immediato)", ) body: str = Field( ..., max_length=300, description="Testo corpo della slide (max 300 caratteri, concreto e diretto)", ) image_keyword: str = Field( ..., max_length=100, description="Parola chiave per la ricerca immagine (es. 'dentista sorridente " "studio moderno'). NON un URL — Phase 4 aggiungerà gli URL Unsplash.", ) class GeneratedPost(BaseModel): """Carosello Instagram completo generato dall'LLM. Struttura: cover + 6 slide centrali (s2-s7) + CTA = 8 slide totali. Corrisponde esattamente ai campi CANVA_FIELDS per il CSV export. """ # --- Cover slide --- cover_title: str = Field( ..., max_length=80, description="Titolo principale della cover — deve fermare lo scroll", ) cover_subtitle: str = Field( ..., max_length=150, description="Sottotitolo della cover — contestualizza il titolo", ) cover_image_keyword: str = Field( ..., max_length=100, description="Keyword per immagine cover (es. 'studio dentistico moderno arredamento')", ) # --- Slide centrali (s2-s7) --- slides: list[SlideContent] = Field( ..., min_length=6, max_length=6, description="Esattamente 6 slide centrali (s2-s7 nel CSV Canva)", ) # --- CTA slide --- cta_text: str = Field( ..., max_length=80, description="Call-to-action principale — verbo d'azione + beneficio", ) cta_subtext: str = Field( ..., max_length=200, description="Testo di supporto alla CTA — cosa fare concretamente", ) cta_image_keyword: str = Field( ..., max_length=100, description="Keyword per immagine CTA (es. 'handshake accordo professionale')", ) # --- Caption Instagram --- caption_instagram: str = Field( ..., max_length=2200, description="Caption completa per Instagram: hook + testo + hashtag. " "Max 2200 caratteri (limite Instagram).", ) class TopicResult(BaseModel): """Risultato della generazione topic per uno slot del calendario. Usato da LLMService.generate_topic() con il loop retry/validation standard. L'LLM genera UN topic specifico per lo slot dato. """ topic: str = Field( ..., min_length=5, max_length=100, description="Topic specifico e concreto per il post (max 100 caratteri). " "Es: '3 errori che fanno perdere pazienti al tuo studio dentistico'", ) # --------------------------------------------------------------------------- # Request/Response per generazione # --------------------------------------------------------------------------- class GenerateRequest(BaseModel): """Richiesta per generare il contenuto di un singolo slot.""" slot: CalendarSlot = Field( ..., description="Slot del calendario con metadati strategici", ) obiettivo_campagna: str = Field( ..., description="Obiettivo principale della campagna — mantiene coerenza tra post", ) brand_name: Optional[str] = Field( default=None, description="Nome del brand/studio — usato nella CTA e nel brand voice", ) tono: Optional[str] = Field( default=None, description="Tono di voce specifico (es. 'professionale ma amichevole', " "'provocatorio', 'tecnico'). Se None, usa il default del prompt.", ) class PostResult(BaseModel): """Risultato della generazione di un singolo post nel batch.""" slot_index: int = Field( ..., ge=0, description="Indice dello slot nel calendario (0-based)", ) status: Literal["success", "failed", "pending"] = Field( ..., description="Stato della generazione: success, failed (con errore), pending", ) post: Optional[GeneratedPost] = Field( default=None, description="Post generato — presente solo se status='success'", ) error: Optional[str] = Field( default=None, description="Messaggio di errore — presente solo se status='failed'", ) class GenerateResponse(BaseModel): """Risposta batch con tutti i risultati di generazione del ciclo.""" campagna: str = Field( ..., description="Riepilogo sintetico dell'obiettivo campagna", ) results: list[PostResult] = Field( ..., description="Lista di risultati per ogni slot del calendario", ) total: int = Field( ..., description="Numero totale di slot nel batch", ) success_count: int = Field( ..., ge=0, description="Numero di post generati con successo", ) failed_count: int = Field( ..., ge=0, description="Numero di post falliti — esclusi dal CSV export", )