| Server IP : 3.147.158.171 / Your IP : 216.73.216.88 Web Server : Apache/2.4.67 (Amazon Linux) OpenSSL/3.5.5 System : Linux ip-172-31-2-178.us-east-2.compute.internal 6.1.172-216.329.amzn2023.x86_64 #1 SMP PREEMPT_DYNAMIC Wed May 20 06:31:34 UTC 2026 x86_64 User : ec2-user ( 1000) PHP Version : 8.4.21 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /tsai/www/html/api.turmansolutions.ai/app/ |
Upload File : |
from pydantic import BaseModel, ConfigDict, Field, AliasChoices
from enum import Enum
from typing import Optional, List, Any
class ContentSchedule(BaseModel):
"""Calendar-based scheduling for articles and comments."""
daysOfWeek: Optional[List[int]] = None # 0=Sunday, 1=Monday, etc.
daysOfMonth: Optional[List[int]] = None # 1-31
preferredHours: Optional[List[int]] = None # 0-23
class ArticleResponse(BaseModel):
title: str
body: str
category: str
tags: List[str]
featured_image_description: Optional[str] = None
secondary_image_description: Optional[str] = None
featured_image_caption: Optional[str] = Field(
default=None,
description="A short, single-sentence caption (max ~15 words) shown under the featured image. Reader-facing prose, not an image-generation prompt. No surrounding quotes."
)
secondary_image_caption: Optional[str] = Field(
default=None,
description="A short, single-sentence caption (max ~15 words) shown under the secondary image. Reader-facing prose, not an image-generation prompt. No surrounding quotes."
)
series_summary: Optional[str] = None # AI summary for series continuity
class CorrectionResponse(BaseModel):
edits_needed: bool
revised_body: Optional[str] = None # full corrected HTML article body; set only when edits_needed
comment: str # markdown documenting the corrections (or why none were needed)
class RevisionResponse(BaseModel):
revised_body: str # full revised HTML article body
summary: str # one or two sentences describing what changed
class ArticleProgressEvent(BaseModel):
status: str
progress: int
message: str
article: Optional[ArticleResponse] = None
article_id: Optional[int] = None
error: Optional[str] = None
class ArticleCreationResult(BaseModel):
article: ArticleResponse
post_id: int
actual_model: "LLModel"
permalink: Optional[str] = None
featured_image_url: Optional[str] = None
secondary_image_url: Optional[str] = None
search_results: Optional[List[Any]] = None
fact_check_posted: bool = False
corrections_posted: bool = False
series_term_id: Optional[int] = None
series_part_number: Optional[int] = None
class PostingRecord(BaseModel):
"""A single record of a cron posting attempt (article or comment).
Persisted append-only to posting_history_{env}.jsonl and read back by the
schedule calendar to render concrete "what was posted" events with links.
"""
ts: str # ISO-8601 local timestamp of the attempt
date: str # YYYY-MM-DD (for cheap day-window filtering)
site: str # site slug
kind: str # "article" | "comment"
character: str # character display name
status: str # "success" | "failed"
character_slug: Optional[str] = None
storage_id: Optional[str] = None
post_id: Optional[int] = None
url: Optional[str] = None
title: Optional[str] = None
excerpt: Optional[str] = None
model: Optional[str] = None
error: Optional[str] = None
class UsageRecord(BaseModel):
"""A single AI-provider usage event, keyed by site slug.
Persisted append-only to usage_{env}.jsonl (see services/usage_history.py)
and aggregated by the /usage endpoint into per-model / per-provider totals.
Token counts apply to OpenAI/Claude; tavily_credits applies to Tavily search.
"""
ts: str # ISO-8601 local timestamp
date: str # YYYY-MM-DD (for cheap day-window filtering)
site: str # site slug
provider: str # "OpenAI" | "Claude" | "Tavily"
model: str # model name (Tavily: "search"; images: "gpt-image-2")
operation: str # "article" | "comment" | "fact_check" | "corrections" | "research" | "chat" | "image_studio" | "article_image" | ...
input_tokens: int = 0
output_tokens: int = 0
tavily_credits: int = 0
# Image generation is priced per image (by size/quality), not by tokens.
images: int = 0
image_size: Optional[str] = None # e.g. "1536x1024" (resolved from 'auto')
image_quality: Optional[str] = None # "low" | "medium" | "high"
class ModelUsage(BaseModel):
"""Per-(provider, model) usage roll-up with estimated cost, camelCase for the UI."""
model_config = ConfigDict(populate_by_name=True)
provider: str
model: str
input_tokens: int = Field(0, serialization_alias="inputTokens")
output_tokens: int = Field(0, serialization_alias="outputTokens")
tavily_credits: int = Field(0, serialization_alias="tavilyCredits")
images: int = Field(0, serialization_alias="imagesCount")
estimated_cost: float = Field(0.0, serialization_alias="estimatedCost")
price_known: bool = Field(True, serialization_alias="priceKnown")
class ProviderUsage(BaseModel):
"""All models for one provider plus the provider cost subtotal."""
model_config = ConfigDict(populate_by_name=True)
provider: str
models: List[ModelUsage]
subtotal_cost: float = Field(0.0, serialization_alias="subtotalCost")
class UsageSummaryResponse(BaseModel):
"""Aggregated usage for a single site over a trailing window."""
model_config = ConfigDict(populate_by_name=True)
slug: str
since_days: int = Field(30, serialization_alias="sinceDays")
providers: List[ProviderUsage]
total_cost: float = Field(0.0, serialization_alias="totalCost")
currency: str = "USD"
generated_at: str = Field('', serialization_alias="generatedAt")
class DailyUsage(BaseModel):
"""Estimated spend for a single calendar day (for the spend-over-time view)."""
model_config = ConfigDict(populate_by_name=True)
date: str # YYYY-MM-DD
cost: float = 0.0
class OperationUsage(BaseModel):
"""Estimated spend rolled up by operation type (article, comment, image, ...)."""
model_config = ConfigDict(populate_by_name=True)
operation: str
cost: float = 0.0
input_tokens: int = Field(0, serialization_alias="inputTokens")
output_tokens: int = Field(0, serialization_alias="outputTokens")
images: int = Field(0, serialization_alias="imagesCount")
tavily_credits: int = Field(0, serialization_alias="tavilyCredits")
class UsageDetailResponse(BaseModel):
"""Detailed usage for a single site: per-provider/model plus daily and per-operation
breakdowns. Powers the Creator app's detailed Usage & Cost page."""
model_config = ConfigDict(populate_by_name=True)
slug: str
since_days: int = Field(30, serialization_alias="sinceDays")
providers: List[ProviderUsage]
daily: List[DailyUsage]
operations: List[OperationUsage]
total_cost: float = Field(0.0, serialization_alias="totalCost")
currency: str = "USD"
generated_at: str = Field('', serialization_alias="generatedAt")
class CharacterHistoryEvent(BaseModel):
timestamp: str
description: str
class Character(BaseModel):
slug: Optional[str] = None
name: str
email: str
description: str
enabled: bool
date: str
siteId: str = ''
storageId: Optional[str] = None
wp_user: Optional["WpUser"] = None
articles: Optional[List[Any]] = None
model: Optional["LLModel"] = None
researchCurrentEvents: Optional[bool] = False
createFeaturedImage: Optional[bool] = False
createSecondaryImage: Optional[bool] = False
enableFactChecking: Optional[bool] = False
enableCorrections: Optional[bool] = False
includePreviousArticles: Optional[bool] = True
searchQuery: Optional[str] = None
searchDomainsIncluded: Optional[List[str]] = None
searchDomainsExcluded: Optional[List[str]] = None
searchTopicType: Optional[str] = 'general'
searchTimeRange: Optional[str] = None
maxSearchResults: Optional[int] = 5
searchDepth: Optional[str] = 'basic'
chunksPerSource: Optional[int] = 3
# Calendar-based scheduling
articleSchedule: Optional[ContentSchedule] = None
commentSchedule: Optional[ContentSchedule] = None
shared: Optional[bool] = True
systemWide: Optional[bool] = False
history: Optional[List["CharacterHistoryEvent"]] = None
class Constants(Enum):
NEW_LINE = "\n"
CB_NEW_LINE = "__chatbot_br__"
CB_END_STREAM = "__chatbot_end__"
class CreateArticleRequest(BaseModel):
character: Character
character_description_override: Optional[str] = ''
research_current_events: Optional[bool] = False
enable_fact_checking: Optional[bool] = True
enable_corrections: Optional[bool] = False
generate_featured_image: Optional[bool] = True
generate_secondary_image: Optional[bool] = True
include_previous_articles: Optional[bool] = True
series_name: Optional[str] = None # Series name (creates new if doesn't exist)
continue_series_id: Optional[int] = None # WP taxonomy term ID to continue
seed_image_url: Optional[str] = None # Reference image URL for the featured image (single article or series Part 1 only)
seed_image_instructions: Optional[str] = None # Free-text guidance on how the seed image should be used
class CreateCommentRequest(BaseModel):
url: str
character: Character
user_instructions: Optional[str] = ''
class CreateCharacterRequest(BaseModel):
character: Character
class DrupalLogin(BaseModel):
username: str
password: str
class CreateDrupalUserRequest(BaseModel):
"""Request model for creating a Drupal user."""
username: str
email_domain: str = "turmansolutions.ai"
class CreateDrupalUserResponse(BaseModel):
"""Response model for Drupal user creation."""
success: bool
uuid: str
username: str
email: str
password: str
message: str
# class DeleteCharacterRequest(BaseModel):
# id: str
class LLMProvider(str, Enum):
OPEN_AI = 'OpenAI'
CLAUDE = 'Claude'
class LLModel(BaseModel):
# Accept both snake_case (stored character bodies) and camelCase (frontend
# `maxTokens`) on input, and serialize as camelCase so the Angular frontend
# actually receives the value. The Python attribute stays `max_tokens`.
model_config = ConfigDict(populate_by_name=True)
provider: LLMProvider
name: str
temperature: Optional[float] = 0.7
max_tokens: Optional[int] = Field(
default=None,
validation_alias=AliasChoices("max_tokens", "maxTokens"),
serialization_alias="maxTokens",
)
stream: Optional[bool] = True
class LoginRequest(BaseModel):
username: str
password: str
site: str
class ServiceLoginRequest(BaseModel):
site: str
class LoginResponse(BaseModel):
wp_token: Any
class LogoutRequest(BaseModel):
# username: str
token: str
class LogoutResponse(BaseModel):
detail: str
class RegisterRequest(BaseModel):
username: str
email: str
password: str
site: str
first_name: Optional[str] = None
last_name: Optional[str] = None
roles: Optional[List[str]] = None
character_uuid: Optional[str] = None
class RegisterResponse(BaseModel):
success: bool
user_id: Optional[int] = None
username: Optional[str] = None
email: Optional[str] = None
message: str
class ImageCompressionRequest(BaseModel):
url: str
compression_level: int
class Message(BaseModel):
userId: int
body: str
instructions: Optional[str] = 'You are a helpful assistant.'
conversation: Optional[List['Message']] = None
model: Optional['LLModel'] = None
use_structured_output: Optional[bool] = False
response_schema: Optional[dict] = None
images: Optional[List[str]] = None
site: Optional['SiteConfig'] = None
# WordPress JWT of the logged-in user. Only the Article Studio requires it:
# publishing writes to a live public site, so Origin-based site scoping alone
# is not enough. Validated against the site's WP install before the agent runs.
wp_token: Optional[str] = None
class PatchCharacterRequest(BaseModel):
character: Character
class CharacterResponse(BaseModel):
data: Character
class CharactersListResponse(BaseModel):
data: List[Character]
count: int
# Slug of the requesting site, so clients (e.g. the WP schedule page) can
# build the slug-scoped calendar subscribe URL. Optional for back-compat.
site_slug: Optional[str] = None
class Widget(BaseModel):
storageId: Optional[str] = None
siteId: str = ''
id: str = ''
name: str = ''
category: str = ''
date: str = ''
enabled: bool = False
hidden: bool = False
config: dict = {}
shared: Optional[bool] = True
systemWide: Optional[bool] = False
class CreateWidgetRequest(BaseModel):
widget: Widget
class PatchWidgetRequest(BaseModel):
widget: Widget
class WidgetResponse(BaseModel):
data: dict
class WidgetListResponse(BaseModel):
data: List[dict]
count: int
class PathSegment(Enum):
TMP_DIR = '/fastapi/tmp/'
WP_FILES = '/wp-content/uploads'
DRUPAL_WEB = '/drupal/web'
DRUPAL_FILES = '/sites/default/files'
CHATBOT_CREATE = '/create'
CHATBOT_EDIT = '/edit'
CHATBOT_UPLOAD = '/upload'
CHATBOT_VARIANT = '/variant'
class RunAgentsRequest(BaseModel):
slug: str
class SiteConfig(BaseModel):
cb_host: str
api_host: str
wp_host: str
wp_dir: str
drupal_host: str
drupal_api: str
slug: str
drupal_uid: Optional[str] = None
drupal_target_id: Optional[int] = None
drupal_node_id: Optional[str] = None
class IndexedSite(BaseModel):
"""Site data gathered by the site indexing service"""
slug: str
wp_host: str = Field(serialization_alias="wpHost")
name: str
tagline: str = ""
link: str
chat_url: str = Field(serialization_alias="chatUrl")
logo_full: str = Field("", serialization_alias="logoFull")
logo_72: str = Field("", serialization_alias="logo72")
logo_150: str = Field("", serialization_alias="logo150")
logo_300: str = Field("", serialization_alias="logo300")
indexed_at: str = Field(serialization_alias="indexedAt")
promoted: bool = True
class LeaderboardEntry(BaseModel):
"""A single aggregated quiz leaderboard row from a WordPress site's live
/wp-json/tsai/v1/quiz/leaderboard endpoint."""
# The WP plugin deliberately withholds identity_key (internal grouping key),
# so default it — the UI keys on display_name/rank, never identity_key.
identity_key: str = Field("", serialization_alias="identityKey")
display_name: str = Field("", serialization_alias="displayName")
total_points: int = Field(0, serialization_alias="totalPoints")
quizzes_completed: int = Field(0, serialization_alias="quizzesCompleted")
class SiteRequest(BaseModel):
data: str
class UploadRequest(BaseModel):
imgPath: str
class WpUser(BaseModel):
id: str
name: str
class StructuredResponse(BaseModel):
content: str
structured_data: Optional[dict] = None
class ModelInfo(BaseModel):
provider: str
name: str
description: Optional[str] = None
max_tokens: Optional[int] = None
supports_streaming: bool = True
supports_structured_output: bool = False
supports_vision: bool = False
class ResponsesImageRequest(BaseModel):
"""Request model for OpenAI Responses API image generation"""
prompt: str
user: str
model: str = "gpt-4.1-mini"
stream: bool = True
class GptImageSize(str, Enum):
_1024x1024 = '1024x1024'
_1536x1024 = '1536x1024'
_1024x1536 = '1024x1536'
auto = 'auto'
class GptImageQuality(str, Enum):
low = 'low'
medium = 'medium'
high = 'high'
class GptImageFormat(str, Enum):
png = 'png'
jpeg = 'jpeg'
webp = 'webp'
class GptImageInputFidelity(str, Enum):
low = 'low'
high = 'high'
class GptImage2Background(str, Enum):
transparent = 'transparent'
opaque = 'opaque'
auto = 'auto'
class GptImage2Request(BaseModel):
"""Request model for gpt-image-2 generation via the Responses API image_generation tool.
gpt-image-2 is reachable only via responses.create with
tools=[{type:"image_generation", ...}], driven by a text model
(driver_model). The underlying image model is selected by OpenAI.
"""
prompt: str
user: str
driver_model: str = "gpt-5"
stream: bool = True
size: GptImageSize = GptImageSize.auto
quality: GptImageQuality = GptImageQuality.medium
output_format: Optional[GptImageFormat] = None
background: Optional[GptImage2Background] = None
input_fidelity: Optional[GptImageInputFidelity] = None
partial_images: Optional[int] = None
input_images: Optional[List[str]] = None
previous_response_id: Optional[str] = None
class BrowseImageItem(BaseModel):
"""Single image item returned by the browse endpoint"""
filename: str
type: str
thumb_url: str
full_url: str
modified: float
class BrowseImagesResponse(BaseModel):
"""Response from the image browse endpoint"""
images: List[BrowseImageItem]
next_cursor: Optional[str] = None
total: int