Backend: - Character model: add brand_voice, target_audience, business_goals, products_services, content_rules (JSON do/dont), hashtag_profiles (JSON) - Content generation: inject full character context into LLM system prompt (voice, audience, goals, products, rules) - Hashtag generation: merge always-on tags from profile with AI-generated tags - Schema: update CharacterBase and CharacterUpdate with new fields Frontend: - CharacterForm: new sections "Identità e Voce", "Regole Contenuti", "Profili Hashtag" with dedicated editors - RulesEditor: do/don't list with add/remove - HashtagProfileEditor: per-platform tabs, fixed hashtags + max generated count - All fields loaded on edit, saved on submit DB migration: 6 new columns added to characters table Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
345 lines
8.8 KiB
Python
345 lines
8.8 KiB
Python
from datetime import datetime
|
|
from typing import List, Optional
|
|
|
|
from pydantic import BaseModel
|
|
|
|
|
|
# === Auth ===
|
|
|
|
class LoginRequest(BaseModel):
|
|
username: str
|
|
password: str
|
|
|
|
|
|
class Token(BaseModel):
|
|
access_token: str
|
|
token_type: str = "bearer"
|
|
user: Optional[dict] = None
|
|
|
|
|
|
# === Characters ===
|
|
|
|
class CharacterBase(BaseModel):
|
|
name: str
|
|
niche: str
|
|
topics: list[str] = []
|
|
tone: Optional[str] = None
|
|
brand_voice: Optional[str] = None
|
|
target_audience: Optional[str] = None
|
|
business_goals: Optional[str] = None
|
|
products_services: Optional[str] = None
|
|
content_rules: dict = {} # {"do": [...], "dont": [...]}
|
|
hashtag_profiles: dict = {} # per-platform: {"instagram": {"always": [], "pool": [], ...}}
|
|
visual_style: dict = {}
|
|
social_accounts: dict = {}
|
|
affiliate_links: list[dict] = []
|
|
avatar_url: Optional[str] = None
|
|
is_active: bool = True
|
|
|
|
|
|
class CharacterCreate(CharacterBase):
|
|
pass
|
|
|
|
|
|
class CharacterUpdate(BaseModel):
|
|
name: Optional[str] = None
|
|
niche: Optional[str] = None
|
|
topics: Optional[list[str]] = None
|
|
tone: Optional[str] = None
|
|
brand_voice: Optional[str] = None
|
|
target_audience: Optional[str] = None
|
|
business_goals: Optional[str] = None
|
|
products_services: Optional[str] = None
|
|
content_rules: Optional[dict] = None
|
|
hashtag_profiles: Optional[dict] = None
|
|
visual_style: Optional[dict] = None
|
|
social_accounts: Optional[dict] = None
|
|
affiliate_links: Optional[list[dict]] = None
|
|
avatar_url: Optional[str] = None
|
|
is_active: Optional[bool] = None
|
|
|
|
|
|
class CharacterResponse(CharacterBase):
|
|
id: int
|
|
created_at: datetime
|
|
updated_at: Optional[datetime] = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# === Posts / Content ===
|
|
|
|
class PostCreate(BaseModel):
|
|
character_id: int
|
|
content_type: str = "text"
|
|
platform_hint: str = "instagram"
|
|
text_content: Optional[str] = None
|
|
hashtags: list[str] = []
|
|
image_url: Optional[str] = None
|
|
video_url: Optional[str] = None
|
|
media_urls: list[str] = []
|
|
affiliate_links_used: list[dict] = []
|
|
status: str = "draft"
|
|
|
|
|
|
class PostUpdate(BaseModel):
|
|
text_content: Optional[str] = None
|
|
hashtags: Optional[list[str]] = None
|
|
image_url: Optional[str] = None
|
|
video_url: Optional[str] = None
|
|
status: Optional[str] = None
|
|
affiliate_links_used: Optional[list[dict]] = None
|
|
|
|
|
|
class PostResponse(BaseModel):
|
|
id: int
|
|
batch_id: Optional[str] = None
|
|
character_id: int
|
|
content_type: str
|
|
text_content: Optional[str] = None
|
|
hashtags: list[str] = []
|
|
image_url: Optional[str] = None
|
|
video_url: Optional[str] = None
|
|
media_urls: list[str] = []
|
|
affiliate_links_used: list[dict] = []
|
|
llm_provider: Optional[str] = None
|
|
llm_model: Optional[str] = None
|
|
platform_hint: Optional[str] = None
|
|
status: str
|
|
created_at: datetime
|
|
updated_at: Optional[datetime] = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class GenerateContentRequest(BaseModel):
|
|
character_id: int
|
|
platform: str = "instagram" # legacy single-platform (kept for compat)
|
|
content_type: str = "text" # legacy single type (kept for compat)
|
|
platforms: List[str] = [] # new: multi-platform (overrides platform if non-empty)
|
|
content_types: List[str] = [] # new: multi-type (overrides content_type if non-empty)
|
|
topic_hint: Optional[str] = None
|
|
brief: Optional[str] = None # editorial brief: technique + instructions for the LLM
|
|
include_affiliates: bool = True
|
|
provider: Optional[str] = None
|
|
model: Optional[str] = None
|
|
|
|
@property
|
|
def effective_platform(self) -> str:
|
|
return self.platforms[0] if self.platforms else self.platform
|
|
|
|
@property
|
|
def effective_content_type(self) -> str:
|
|
return self.content_types[0] if self.content_types else self.content_type
|
|
|
|
|
|
class GenerateImageRequest(BaseModel):
|
|
character_id: int
|
|
prompt: Optional[str] = None # auto-generated if not provided
|
|
style_hint: Optional[str] = None
|
|
size: str = "1024x1024"
|
|
provider: Optional[str] = None # dalle, replicate
|
|
|
|
|
|
# === Affiliate Links ===
|
|
|
|
class AffiliateLinkBase(BaseModel):
|
|
character_id: Optional[int] = None
|
|
network: str
|
|
name: str
|
|
url: str
|
|
tag: Optional[str] = None
|
|
topics: list[str] = []
|
|
is_active: bool = True
|
|
|
|
|
|
class AffiliateLinkCreate(AffiliateLinkBase):
|
|
pass
|
|
|
|
|
|
class AffiliateLinkUpdate(BaseModel):
|
|
network: Optional[str] = None
|
|
name: Optional[str] = None
|
|
url: Optional[str] = None
|
|
tag: Optional[str] = None
|
|
topics: Optional[list[str]] = None
|
|
is_active: Optional[bool] = None
|
|
|
|
|
|
class AffiliateLinkResponse(AffiliateLinkBase):
|
|
id: int
|
|
click_count: int = 0
|
|
created_at: datetime
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# === Editorial Plans ===
|
|
|
|
class EditorialPlanBase(BaseModel):
|
|
character_id: int
|
|
name: str
|
|
frequency: str = "daily"
|
|
posts_per_day: int = 1
|
|
platforms: list[str] = []
|
|
content_types: list[str] = ["text"]
|
|
posting_times: list[str] = ["09:00"]
|
|
start_date: Optional[datetime] = None
|
|
end_date: Optional[datetime] = None
|
|
is_active: bool = False
|
|
|
|
|
|
class EditorialPlanCreate(EditorialPlanBase):
|
|
pass
|
|
|
|
|
|
class EditorialPlanUpdate(BaseModel):
|
|
name: Optional[str] = None
|
|
frequency: Optional[str] = None
|
|
posts_per_day: Optional[int] = None
|
|
platforms: Optional[list[str]] = None
|
|
content_types: Optional[list[str]] = None
|
|
posting_times: Optional[list[str]] = None
|
|
start_date: Optional[datetime] = None
|
|
end_date: Optional[datetime] = None
|
|
is_active: Optional[bool] = None
|
|
|
|
|
|
class EditorialPlanResponse(EditorialPlanBase):
|
|
id: int
|
|
created_at: datetime
|
|
updated_at: Optional[datetime] = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# === Scheduled Posts ===
|
|
|
|
class ScheduledPostCreate(BaseModel):
|
|
plan_id: Optional[int] = None
|
|
post_id: int
|
|
platform: str
|
|
scheduled_at: datetime
|
|
|
|
|
|
class ScheduledPostResponse(BaseModel):
|
|
id: int
|
|
plan_id: Optional[int] = None
|
|
post_id: int
|
|
platform: str
|
|
scheduled_at: datetime
|
|
published_at: Optional[datetime] = None
|
|
status: str
|
|
error_message: Optional[str] = None
|
|
external_post_id: Optional[str] = None
|
|
created_at: datetime
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# === Social Accounts ===
|
|
|
|
class SocialAccountCreate(BaseModel):
|
|
character_id: int
|
|
platform: str
|
|
account_name: Optional[str] = None
|
|
account_id: Optional[str] = None
|
|
access_token: Optional[str] = None
|
|
refresh_token: Optional[str] = None
|
|
page_id: Optional[str] = None
|
|
extra_data: dict = {}
|
|
|
|
|
|
class SocialAccountUpdate(BaseModel):
|
|
account_name: Optional[str] = None
|
|
access_token: Optional[str] = None
|
|
refresh_token: Optional[str] = None
|
|
page_id: Optional[str] = None
|
|
extra_data: Optional[dict] = None
|
|
is_active: Optional[bool] = None
|
|
|
|
|
|
class SocialAccountResponse(BaseModel):
|
|
id: int
|
|
character_id: int
|
|
platform: str
|
|
account_name: Optional[str] = None
|
|
account_id: Optional[str] = None
|
|
page_id: Optional[str] = None
|
|
is_active: bool
|
|
token_expires_at: Optional[datetime] = None
|
|
created_at: datetime
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# === Comments ===
|
|
|
|
class CommentResponse(BaseModel):
|
|
id: int
|
|
scheduled_post_id: Optional[int] = None
|
|
platform: str
|
|
external_comment_id: Optional[str] = None
|
|
author_name: Optional[str] = None
|
|
content: Optional[str] = None
|
|
ai_suggested_reply: Optional[str] = None
|
|
approved_reply: Optional[str] = None
|
|
reply_status: str
|
|
replied_at: Optional[datetime] = None
|
|
created_at: datetime
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class CommentAction(BaseModel):
|
|
action: str # approve, edit, ignore
|
|
reply_text: Optional[str] = None # for edit action
|
|
|
|
|
|
# === System Settings ===
|
|
|
|
class SettingUpdate(BaseModel):
|
|
value: dict | str | list | int | bool | None
|
|
|
|
|
|
class SettingResponse(BaseModel):
|
|
key: str
|
|
value: dict | str | list | int | bool | None
|
|
updated_at: Optional[datetime] = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# === Editorial Calendar (from PostGenerator) ===
|
|
|
|
class CalendarGenerateRequest(BaseModel):
|
|
topics: list[str]
|
|
format_narrativo: Optional[str] = None # PAS, AIDA, BAB, Storytelling, Listicle, Dato_Implicazione
|
|
awareness_level: Optional[int] = None # 1-5 (Schwartz levels)
|
|
num_posts: int = 7
|
|
start_date: Optional[str] = None # YYYY-MM-DD
|
|
character_id: Optional[int] = None
|
|
|
|
|
|
class CalendarSlotResponse(BaseModel):
|
|
indice: int
|
|
topic: str
|
|
formato_narrativo: str
|
|
awareness_level: int
|
|
awareness_label: str
|
|
data_pubblicazione: str
|
|
note: Optional[str] = None
|
|
|
|
|
|
class CalendarResponse(BaseModel):
|
|
slots: list[CalendarSlotResponse]
|
|
totale_post: int
|