GenerateResponse now includes calendar field from backend. OutputReview merges CalendarSlot into PostResult via slot_index, enabling BadgePN, BadgeSchwartz rendering and Retry button. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
192 lines
5.8 KiB
Python
192 lines
5.8 KiB
Python
"""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 CalendarResponse, 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",
|
|
)
|
|
calendar: Optional[CalendarResponse] = Field(
|
|
default=None,
|
|
description="Calendario con gli slot originali — usato dal frontend per badge PN/Schwartz",
|
|
)
|