feat(01-02): costanti dominio, schemas Pydantic, FormatSelector
- backend/constants.py: CANVA_FIELDS (33 col locked), PERSUASION_DISTRIBUTION (13), SCHWARTZ_DISTRIBUTION (13) - backend/schemas/calendar.py: CalendarSlot, CalendarRequest, CalendarResponse - backend/schemas/generate.py: SlideContent, GeneratedPost, TopicResult, GenerateRequest, PostResult, GenerateResponse - backend/data/format_mapping.json: matrice 6 tipi x 5 livelli (30 combinazioni) - backend/services/format_selector.py: FormatSelector con select_format e fallback PAS - fix .gitignore: backend/data/prompts/ e format_mapping.json non erano ignorabili
This commit is contained in:
194
backend/constants.py
Normal file
194
backend/constants.py
Normal file
@@ -0,0 +1,194 @@
|
||||
"""Costanti di dominio LOCKED per PostGenerator.
|
||||
|
||||
ATTENZIONE: Queste costanti sono fondamentali per la coerenza del sistema.
|
||||
Non modificare CANVA_FIELDS, PERSUASION_DISTRIBUTION o SCHWARTZ_DISTRIBUTION
|
||||
senza aggiornare anche il CSV builder e tutti i prompt LLM.
|
||||
"""
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Configurazione ciclo editoriale
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
POST_PER_CICLO: int = 13
|
||||
"""Numero fisso di post per ciclo editoriale completo."""
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Distribuzione Persuasion Nurturing (PN) — 13 slot per ciclo
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
PERSUASION_DISTRIBUTION: dict[str, int] = {
|
||||
"valore": 4,
|
||||
"storytelling": 2,
|
||||
"news": 2,
|
||||
"riprova_sociale": 3,
|
||||
"coinvolgimento": 1,
|
||||
"promozione": 1,
|
||||
}
|
||||
"""Distribuzione tipo_contenuto per ciclo di 13 post.
|
||||
|
||||
Totale: sum(values) == 13
|
||||
"""
|
||||
|
||||
# Verifica a load-time
|
||||
assert sum(PERSUASION_DISTRIBUTION.values()) == POST_PER_CICLO, (
|
||||
f"PERSUASION_DISTRIBUTION deve sommare a {POST_PER_CICLO}, "
|
||||
f"ora somma a {sum(PERSUASION_DISTRIBUTION.values())}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Distribuzione livelli Schwartz — 13 slot per ciclo
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
SCHWARTZ_DISTRIBUTION: dict[str, int] = {
|
||||
"L5": 3, # Inconsapevole del problema — storytelling + news
|
||||
"L4": 3, # Consapevole del problema — valore + news
|
||||
"L3": 4, # Consapevole della soluzione — valore + riprova_sociale
|
||||
"L2": 2, # Consapevole del prodotto — riprova_sociale + coinvolgimento
|
||||
"L1": 1, # Pronto all'acquisto — promozione
|
||||
}
|
||||
"""Distribuzione livelli consapevolezza Schwartz per ciclo di 13 post.
|
||||
|
||||
L5+L4 = 6 (fase Attira/Cattura — top of funnel)
|
||||
L3 = 4 (fase Coinvolgi — middle of funnel)
|
||||
L2+L1 = 3 (fase Converti — bottom of funnel)
|
||||
Totale: sum(values) == 13
|
||||
"""
|
||||
|
||||
# Verifica a load-time
|
||||
assert sum(SCHWARTZ_DISTRIBUTION.values()) == POST_PER_CICLO, (
|
||||
f"SCHWARTZ_DISTRIBUTION deve sommare a {POST_PER_CICLO}, "
|
||||
f"ora somma a {sum(SCHWARTZ_DISTRIBUTION.values())}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Colonne CSV per Canva Bulk Create — LOCKED
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
CANVA_FIELDS: list[str] = [
|
||||
# --- Metadati slot (8 colonne) ---
|
||||
"campagna",
|
||||
"fase_campagna",
|
||||
"tipo_contenuto",
|
||||
"formato_narrativo",
|
||||
"funzione",
|
||||
"livello_schwartz",
|
||||
"target_nicchia",
|
||||
"data_pub_suggerita",
|
||||
# --- Cover slide (3 colonne) ---
|
||||
"cover_title",
|
||||
"cover_subtitle",
|
||||
"cover_image_keyword",
|
||||
# --- Slide 2 (3 colonne) ---
|
||||
"s2_headline",
|
||||
"s2_body",
|
||||
"s2_image_keyword",
|
||||
# --- Slide 3 (3 colonne) ---
|
||||
"s3_headline",
|
||||
"s3_body",
|
||||
"s3_image_keyword",
|
||||
# --- Slide 4 (3 colonne) ---
|
||||
"s4_headline",
|
||||
"s4_body",
|
||||
"s4_image_keyword",
|
||||
# --- Slide 5 (3 colonne) ---
|
||||
"s5_headline",
|
||||
"s5_body",
|
||||
"s5_image_keyword",
|
||||
# --- Slide 6 (3 colonne) ---
|
||||
"s6_headline",
|
||||
"s6_body",
|
||||
"s6_image_keyword",
|
||||
# --- Slide 7 (3 colonne) ---
|
||||
"s7_headline",
|
||||
"s7_body",
|
||||
"s7_image_keyword",
|
||||
# --- CTA slide (3 colonne) ---
|
||||
"cta_text",
|
||||
"cta_subtext",
|
||||
"cta_image_keyword",
|
||||
# --- Extra (1 colonna) ---
|
||||
"caption_instagram",
|
||||
]
|
||||
"""Lista ORDINATA di tutte le colonne del CSV per Canva Bulk Create.
|
||||
|
||||
Struttura:
|
||||
- 8 metadati slot
|
||||
- 24 campi slide (8 slide x 3 campi: headline/title, body/subtitle, image_keyword)
|
||||
Nota: image_keyword contiene parole chiave testuali (NON URL).
|
||||
Gli URL Unsplash verranno aggiunti in Phase 4.
|
||||
- 1 caption Instagram
|
||||
Totale: 33 colonne
|
||||
|
||||
LOCKED: Non aggiungere/rimuovere colonne senza aggiornare tutti i prompt LLM
|
||||
e il CSV builder.
|
||||
"""
|
||||
|
||||
# Verifica a load-time
|
||||
_expected_count = 8 + 24 + 1 # 33
|
||||
assert len(CANVA_FIELDS) == _expected_count, (
|
||||
f"CANVA_FIELDS deve avere {_expected_count} elementi, "
|
||||
f"ne ha {len(CANVA_FIELDS)}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Formati narrativi disponibili (7 formati)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
FORMATI_NARRATIVI: list[str] = [
|
||||
"PAS", # Problema → Agitazione → Soluzione
|
||||
"AIDA", # Attenzione → Interesse → Desiderio → Azione
|
||||
"BAB", # Before → After → Bridge
|
||||
"Listicle", # Lista numerata di punti/consigli
|
||||
"Storytelling", # Narrativa emotiva di trasformazione
|
||||
"Dato_Implicazione", # Dato/statistica → Implicazione → Azione
|
||||
"Obiezione_Risposta", # Obiezione comune → Confutazione → Soluzione
|
||||
]
|
||||
"""I 7 formati narrativi supportati per i caroselli Instagram."""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Funzioni contenuto (4 macro-funzioni editoriali)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
FUNZIONI_CONTENUTO: list[str] = [
|
||||
"Intrattenere",
|
||||
"Educare",
|
||||
"Persuadere",
|
||||
"Convertire",
|
||||
]
|
||||
"""Le 4 macro-funzioni editoriali di ogni post."""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fasi campagna (funnel AIDA semplificato)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
FASI_CAMPAGNA: list[str] = [
|
||||
"Attira", # Top of funnel — inconsapevoli (L5)
|
||||
"Cattura", # Upper middle — consapevoli del problema (L4+L3)
|
||||
"Coinvolgi", # Lower middle — consapevoli della soluzione (L3+L2)
|
||||
"Converti", # Bottom of funnel — pronti all'acquisto (L1+L2)
|
||||
]
|
||||
"""Le 4 fasi del funnel di acquisizione clienti."""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Nicchie target predefinite
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
NICCHIE_DEFAULT: list[str] = [
|
||||
"generico",
|
||||
"dentisti",
|
||||
"avvocati",
|
||||
"ecommerce",
|
||||
"local_business",
|
||||
"agenzie",
|
||||
]
|
||||
"""Lista di nicchie target predefinite.
|
||||
|
||||
"generico" è sempre incluso e viene usato per il 50% degli slot.
|
||||
Le altre nicchie vengono ruotate per il restante 50%.
|
||||
"""
|
||||
45
backend/data/format_mapping.json
Normal file
45
backend/data/format_mapping.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"_comment": "Matrice di mapping tipo_contenuto x livello_schwartz -> formato_narrativo. Scegliere il formato più efficace per ogni combinazione in base al livello di consapevolezza del pubblico.",
|
||||
"valore": {
|
||||
"L5": "Listicle",
|
||||
"L4": "PAS",
|
||||
"L3": "PAS",
|
||||
"L2": "Obiezione_Risposta",
|
||||
"L1": "AIDA"
|
||||
},
|
||||
"storytelling": {
|
||||
"L5": "BAB",
|
||||
"L4": "Storytelling",
|
||||
"L3": "BAB",
|
||||
"L2": "Storytelling",
|
||||
"L1": "AIDA"
|
||||
},
|
||||
"news": {
|
||||
"L5": "Dato_Implicazione",
|
||||
"L4": "Dato_Implicazione",
|
||||
"L3": "PAS",
|
||||
"L2": "Obiezione_Risposta",
|
||||
"L1": "AIDA"
|
||||
},
|
||||
"riprova_sociale": {
|
||||
"L5": "Storytelling",
|
||||
"L4": "BAB",
|
||||
"L3": "Obiezione_Risposta",
|
||||
"L2": "PAS",
|
||||
"L1": "AIDA"
|
||||
},
|
||||
"coinvolgimento": {
|
||||
"L5": "Listicle",
|
||||
"L4": "PAS",
|
||||
"L3": "Obiezione_Risposta",
|
||||
"L2": "Obiezione_Risposta",
|
||||
"L1": "AIDA"
|
||||
},
|
||||
"promozione": {
|
||||
"L5": "AIDA",
|
||||
"L4": "AIDA",
|
||||
"L3": "PAS",
|
||||
"L2": "PAS",
|
||||
"L1": "AIDA"
|
||||
}
|
||||
}
|
||||
1
backend/schemas/__init__.py
Normal file
1
backend/schemas/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Pydantic schemas per PostGenerator."""
|
||||
108
backend/schemas/calendar.py
Normal file
108
backend/schemas/calendar.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""Pydantic schemas per il calendario editoriale.
|
||||
|
||||
Questi modelli rappresentano il piano di pubblicazione dei 13 slot PN
|
||||
generato da CalendarService, prima della generazione LLM del contenuto.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class CalendarSlot(BaseModel):
|
||||
"""Un singolo slot del calendario editoriale con metadati strategici."""
|
||||
|
||||
indice: int = Field(
|
||||
...,
|
||||
ge=0,
|
||||
lt=13,
|
||||
description="Indice 0-based dello slot nel ciclo di 13 post",
|
||||
)
|
||||
tipo_contenuto: str = Field(
|
||||
...,
|
||||
description="Tipo Persuasion Nurturing: valore, storytelling, news, "
|
||||
"riprova_sociale, coinvolgimento, promozione",
|
||||
)
|
||||
livello_schwartz: str = Field(
|
||||
...,
|
||||
description="Livello di consapevolezza del pubblico: L1-L5",
|
||||
)
|
||||
formato_narrativo: str = Field(
|
||||
...,
|
||||
description="Formato narrativo selezionato: PAS, AIDA, BAB, Listicle, "
|
||||
"Storytelling, Dato_Implicazione, Obiezione_Risposta",
|
||||
)
|
||||
funzione: str = Field(
|
||||
...,
|
||||
description="Funzione editoriale: Intrattenere, Educare, Persuadere, Convertire",
|
||||
)
|
||||
fase_campagna: str = Field(
|
||||
...,
|
||||
description="Fase del funnel: Attira, Cattura, Coinvolgi, Converti",
|
||||
)
|
||||
target_nicchia: str = Field(
|
||||
...,
|
||||
description="Nicchia target: es. generico, dentisti, avvocati, ecommerce",
|
||||
)
|
||||
data_pub_suggerita: str = Field(
|
||||
...,
|
||||
description="Data di pubblicazione suggerita in formato YYYY-MM-DD",
|
||||
)
|
||||
topic: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Topic specifico del post. None finché non generato dall'LLM "
|
||||
"o specificato dall'utente.",
|
||||
)
|
||||
|
||||
|
||||
class CalendarRequest(BaseModel):
|
||||
"""Richiesta per generare un calendario editoriale."""
|
||||
|
||||
obiettivo_campagna: str = Field(
|
||||
...,
|
||||
min_length=10,
|
||||
description="Obiettivo principale della campagna (es. 'Acquisire nuovi "
|
||||
"clienti dentisti nel Nord Italia')",
|
||||
)
|
||||
settimane: int = Field(
|
||||
default=2,
|
||||
ge=1,
|
||||
le=12,
|
||||
description="Durata del ciclo in settimane (default: 2 settimane per 13 post)",
|
||||
)
|
||||
nicchie: Optional[list[str]] = Field(
|
||||
default=None,
|
||||
description="Lista di nicchie target. Se None, usa NICCHIE_DEFAULT. "
|
||||
"'generico' viene sempre incluso automaticamente.",
|
||||
)
|
||||
frequenza_post: int = Field(
|
||||
default=3,
|
||||
ge=1,
|
||||
le=7,
|
||||
description="Numero di post a settimana (default: 3 — lun, mer, ven)",
|
||||
)
|
||||
data_inizio: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Data di inizio del calendario in formato YYYY-MM-DD. "
|
||||
"Se None, usa la data corrente.",
|
||||
)
|
||||
|
||||
|
||||
class CalendarResponse(BaseModel):
|
||||
"""Risposta con il calendario editoriale generato."""
|
||||
|
||||
campagna: str = Field(
|
||||
...,
|
||||
description="Riepilogo sintetico dell'obiettivo campagna",
|
||||
)
|
||||
slots: list[CalendarSlot] = Field(
|
||||
...,
|
||||
description="Lista di 13 slot del calendario ordinati per sequenza campagna "
|
||||
"(Attira → Cattura → Coinvolgi → Converti)",
|
||||
)
|
||||
totale_post: int = Field(
|
||||
...,
|
||||
description="Numero totale di slot generati (sempre 13 per ciclo completo)",
|
||||
)
|
||||
187
backend/schemas/generate.py
Normal file
187
backend/schemas/generate.py
Normal file
@@ -0,0 +1,187 @@
|
||||
"""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",
|
||||
)
|
||||
1
backend/services/__init__.py
Normal file
1
backend/services/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Servizi di dominio per PostGenerator."""
|
||||
91
backend/services/format_selector.py
Normal file
91
backend/services/format_selector.py
Normal file
@@ -0,0 +1,91 @@
|
||||
"""FormatSelector — mappa tipo_contenuto x livello_schwartz -> formato_narrativo.
|
||||
|
||||
Carica la matrice di mapping da format_mapping.json e seleziona il formato
|
||||
narrativo più efficace per ogni combinazione di tipo e livello.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# Percorso default al file JSON (relativo a questo modulo)
|
||||
_DEFAULT_MAPPING_PATH = Path(__file__).parent.parent / "data" / "format_mapping.json"
|
||||
|
||||
# Fallback se la combinazione non è presente nella matrice
|
||||
_FALLBACK_FORMAT = "PAS"
|
||||
|
||||
|
||||
class FormatSelector:
|
||||
"""Seleziona il formato narrativo ottimale per un dato tipo_contenuto e livello_schwartz.
|
||||
|
||||
Carica la matrice di mapping da un file JSON e la mantiene in memoria.
|
||||
La selezione è deterministica e basata sulla tabella (nessuna logica LLM).
|
||||
|
||||
Esempio:
|
||||
selector = FormatSelector()
|
||||
formato = selector.select_format("valore", "L4") # -> "PAS"
|
||||
formato = selector.select_format("storytelling", "L5") # -> "BAB"
|
||||
"""
|
||||
|
||||
def __init__(self, mapping_path: Path | None = None) -> None:
|
||||
"""Carica il mapping da file JSON.
|
||||
|
||||
Args:
|
||||
mapping_path: Percorso al file format_mapping.json.
|
||||
Default: backend/data/format_mapping.json
|
||||
"""
|
||||
path = mapping_path or _DEFAULT_MAPPING_PATH
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(
|
||||
f"File format_mapping.json non trovato: {path}. "
|
||||
"Assicurati che backend/data/format_mapping.json esista."
|
||||
)
|
||||
|
||||
with path.open(encoding="utf-8") as f:
|
||||
raw = json.load(f)
|
||||
|
||||
# Filtra i commenti (chiavi che iniziano con "_")
|
||||
self._mapping: dict[str, dict[str, str]] = {
|
||||
k: v for k, v in raw.items() if not k.startswith("_")
|
||||
}
|
||||
|
||||
def select_format(self, tipo_contenuto: str, livello_schwartz: str) -> str:
|
||||
"""Ritorna il formato narrativo per la combinazione data.
|
||||
|
||||
Args:
|
||||
tipo_contenuto: Tipo PN (es. "valore", "storytelling", "promozione")
|
||||
livello_schwartz: Livello consapevolezza (es. "L1", "L3", "L5")
|
||||
|
||||
Returns:
|
||||
Nome del formato narrativo (es. "PAS", "BAB", "AIDA").
|
||||
Ritorna "PAS" come fallback se la combinazione non è nella matrice.
|
||||
"""
|
||||
tipo_map = self._mapping.get(tipo_contenuto)
|
||||
if tipo_map is None:
|
||||
return _FALLBACK_FORMAT
|
||||
|
||||
return tipo_map.get(livello_schwartz, _FALLBACK_FORMAT)
|
||||
|
||||
def get_mapping(self) -> dict[str, dict[str, str]]:
|
||||
"""Ritorna la tabella di mapping completa.
|
||||
|
||||
Returns:
|
||||
Dizionario { tipo_contenuto: { livello_schwartz: formato_narrativo } }
|
||||
"""
|
||||
return dict(self._mapping)
|
||||
|
||||
def get_supported_types(self) -> list[str]:
|
||||
"""Ritorna la lista dei tipi_contenuto supportati dalla matrice."""
|
||||
return list(self._mapping.keys())
|
||||
|
||||
def get_supported_levels(self) -> list[str]:
|
||||
"""Ritorna la lista dei livelli_schwartz supportati dalla matrice.
|
||||
|
||||
Inferisce i livelli dal primo tipo disponibile (la matrice è consistente).
|
||||
"""
|
||||
if not self._mapping:
|
||||
return []
|
||||
first_type = next(iter(self._mapping.values()))
|
||||
return sorted(first_type.keys())
|
||||
Reference in New Issue
Block a user