from pydantic import BaseModel, EmailStr, Field, field_validator
from typing import Optional, List, Dict, Any
from datetime import datetime
from enum import Enum
import uuid
import re
# ─── Enums ────────────────────────────────────────────────────────────────────
class PlanType(str, Enum):
free = "free"
starter = "starter"
business = "business"
agency = "agency"
enterprise = "enterprise"
class SubscriptionStatus(str, Enum):
active = "active"
canceled = "canceled"
past_due = "past_due"
unpaid = "unpaid"
trialing = "trialing"
class ChatbotVisibility(str, Enum):
preview = "preview"
published = "published"
class DocumentStatus(str, Enum):
pending = "pending"
processing = "processing"
completed = "completed"
failed = "failed"
class MessageRole(str, Enum):
user = "user"
assistant = "assistant"
system = "system"
# ─── Auth Models ──────────────────────────────────────────────────────────────
class UserSignup(BaseModel):
email: EmailStr
password: str = Field(min_length=8)
company_name: str = Field(min_length=2, max_length=100)
class UserLogin(BaseModel):
email: EmailStr
password: str
class UserResponse(BaseModel):
id: str
email: str
company_name: Optional[str] = None
plan: str = "free"
is_admin: bool = False
created_at: Optional[datetime] = None
language: Optional[str] = "fr"
class TokenResponse(BaseModel):
access_token: str
token_type: str = "bearer"
user: UserResponse
# ─── Company Models ────────────────────────────────────────────────────────────
class CompanyCreate(BaseModel):
name: str = Field(min_length=2, max_length=100)
website: Optional[str] = None
industry: Optional[str] = None
class CompanyUpdate(BaseModel):
name: Optional[str] = None
website: Optional[str] = None
industry: Optional[str] = None
logo_url: Optional[str] = None
class CompanyResponse(BaseModel):
id: str
owner_id: str
name: str
website: Optional[str] = None
industry: Optional[str] = None
logo_url: Optional[str] = None
created_at: Optional[datetime] = None
# ─── Chatbot Models ────────────────────────────────────────────────────────────
class ChatbotCreate(BaseModel):
name: str = Field(min_length=2, max_length=100)
description: Optional[str] = None
system_prompt: Optional[str] = None
model: str = "accounts/fireworks/models/kimi-k2-instruct"
@field_validator("name", mode="before")
@classmethod
def sanitize_name(cls, v: Any) -> Any:
if v:
v = str(v).strip()
if len(v) > 100:
raise ValueError("Name must be 100 characters or less")
return v
@field_validator("system_prompt", mode="before")
@classmethod
def sanitize_system_prompt(cls, v: Any) -> Any:
if v:
v = re.sub(r"", "", str(v), flags=re.DOTALL | re.IGNORECASE)
v = re.sub(r"javascript:", "", v, flags=re.IGNORECASE)
if len(v) > 10000:
raise ValueError("System prompt must be 10000 characters or less")
return v
@field_validator("description", mode="before")
@classmethod
def sanitize_description(cls, v: Any) -> Any:
if v and len(str(v)) > 2000:
raise ValueError("Description must be 2000 characters or less")
return v
@field_validator("welcome_message", mode="before")
@classmethod
def sanitize_welcome_message(cls, v: Any) -> Any:
if v and len(str(v)) > 500:
raise ValueError("Welcome message must be 500 characters or less")
return v
temperature: float = Field(default=0.7, ge=0.0, le=2.0)
max_tokens: int = Field(default=1000, ge=100, le=8000)
primary_color: str = "#6366f1"
welcome_message: str = "Hello! How can I help you today?"
logo_url: Optional[str] = None
category: Optional[str] = None
industry: Optional[str] = None
languages: List[str] = ["en"]
show_branding: bool = True
lead_capture_enabled: bool = False
lead_capture_fields: List[str] = ["email"]
lead_capture_trigger: str = "after_first_message"
handoff_enabled: bool = False
handoff_message: str = "I'll connect you with our team. Please wait."
handoff_email: Optional[str] = None
handoff_keywords: List[str] = ["human", "agent", "speak to someone", "talk to a person", "real person"]
booking_enabled: bool = False
class ChatbotUpdate(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
system_prompt: Optional[str] = None
@field_validator("name", mode="before")
@classmethod
def sanitize_name(cls, v: Any) -> Any:
if v:
v = str(v).strip()
if len(v) > 100:
raise ValueError("Name must be 100 characters or less")
return v
@field_validator("system_prompt", mode="before")
@classmethod
def sanitize_system_prompt(cls, v: Any) -> Any:
if v:
v = re.sub(r"", "", str(v), flags=re.DOTALL | re.IGNORECASE)
v = re.sub(r"javascript:", "", v, flags=re.IGNORECASE)
if len(v) > 10000:
raise ValueError("System prompt must be 10000 characters or less")
return v
@field_validator("description", mode="before")
@classmethod
def sanitize_description(cls, v: Any) -> Any:
if v and len(str(v)) > 2000:
raise ValueError("Description must be 2000 characters or less")
return v
@field_validator("welcome_message", mode="before")
@classmethod
def sanitize_welcome_message(cls, v: Any) -> Any:
if v and len(str(v)) > 500:
raise ValueError("Welcome message must be 500 characters or less")
return v
model: Optional[str] = None
temperature: Optional[float] = None
max_tokens: Optional[int] = None
primary_color: Optional[str] = None
welcome_message: Optional[str] = None
logo_url: Optional[str] = None
category: Optional[str] = None
industry: Optional[str] = None
languages: Optional[List[str]] = None
show_branding: Optional[bool] = None
lead_capture_enabled: Optional[bool] = None
lead_capture_fields: Optional[List[str]] = None
lead_capture_trigger: Optional[str] = None
handoff_enabled: Optional[bool] = None
handoff_message: Optional[str] = None
handoff_email: Optional[str] = None
handoff_keywords: Optional[List[str]] = None
booking_enabled: Optional[bool] = None
class ChatbotResponse(BaseModel):
id: str
company_id: str
name: str
description: Optional[str] = None
system_prompt: Optional[str] = None
model: str
temperature: float
max_tokens: int
primary_color: str
welcome_message: str
logo_url: Optional[str] = None
category: Optional[str] = None
industry: Optional[str] = None
languages: List[str]
visibility: str
is_published: bool
qdrant_collection_name: Optional[str] = None
document_count: int = 0
conversation_count: int = 0
average_rating: Optional[float] = None
created_at: Optional[datetime] = None
published_at: Optional[datetime] = None
show_branding: bool = True
lead_capture_enabled: bool = False
lead_capture_fields: List[str] = ["email"]
lead_capture_trigger: str = "after_first_message"
handoff_enabled: bool = False
handoff_message: str = "I'll connect you with our team. Please wait."
handoff_email: Optional[str] = None
handoff_keywords: List[str] = ["human", "agent", "speak to someone", "talk to a person", "real person"]
booking_enabled: bool = False
class ChatbotPublicResponse(BaseModel):
"""For marketplace display"""
id: str
name: str
description: Optional[str] = None
category: Optional[str] = None
industry: Optional[str] = None
languages: List[str]
primary_color: str
welcome_message: str
logo_url: Optional[str] = None
average_rating: Optional[float] = None
total_conversations: int = 0
company_name: Optional[str] = None
company_logo: Optional[str] = None
created_at: Optional[datetime] = None
published_at: Optional[datetime] = None
# ─── Document Models ───────────────────────────────────────────────────────────
class DocumentResponse(BaseModel):
id: str
chatbot_id: str
file_name: str
file_type: str
file_size: int
chunk_count: int
status: str
error_message: Optional[str] = None
created_at: Optional[datetime] = None
# ─── Chat Models ───────────────────────────────────────────────────────────────
class ChatMessage(BaseModel):
message: str = Field(min_length=1, max_length=4000)
session_id: Optional[str] = None
language: str = "en"
class SourceDocument(BaseModel):
document_name: str
chunk_text: str
score: float
page_number: Optional[int] = None
class ChatResponse(BaseModel):
response: str
session_id: str
sources: List[SourceDocument] = []
model_used: str
tokens_used: int = 0
needs_lead_capture: bool = False
handoff: bool = False
low_confidence: bool = False
class MessageResponse(BaseModel):
id: str
role: str
content: str
sources: Optional[List[Dict]] = None
created_at: Optional[datetime] = None
class ConversationResponse(BaseModel):
id: str
chatbot_id: str
session_id: Optional[str] = None
language: str
message_count: int
created_at: Optional[datetime] = None
messages: List[MessageResponse] = []
# ─── Subscription Models ───────────────────────────────────────────────────────
class SubscriptionResponse(BaseModel):
id: str
user_id: str
plan: str
status: str
stripe_customer_id: Optional[str] = None
current_period_start: Optional[datetime] = None
current_period_end: Optional[datetime] = None
chatbots_published: int = 0
conversations_used: int = 0
created_at: Optional[datetime] = None
class CheckoutSessionCreate(BaseModel):
plan: str # starter, business, or agency
success_url: str
cancel_url: str
class CheckoutSessionResponse(BaseModel):
checkout_url: str
session_id: str
# ─── Analytics Models ──────────────────────────────────────────────────────────
class ChatbotAnalytics(BaseModel):
chatbot_id: str
total_conversations: int = 0
unique_users: int = 0
average_conversation_length: float = 0.0
total_messages: int = 0
average_rating: float = 0.0
top_queries: List[str] = []
conversations_last_7_days: List[Dict] = []
conversations_last_30_days: int = 0
# ─── Marketplace Models ────────────────────────────────────────────────────────
class MarketplaceFilter(BaseModel):
category: Optional[str] = None
industry: Optional[str] = None
language: Optional[str] = None
search: Optional[str] = None
page: int = 1
limit: int = 20
class MarketplaceResponse(BaseModel):
chatbots: List[ChatbotPublicResponse]
total: int
page: int
limit: int
has_more: bool
class RatingCreate(BaseModel):
rating: int = Field(ge=1, le=5)
feedback: Optional[str] = None
# ─── Code Export Models ────────────────────────────────────────────────────────
class CodeExportRequest(BaseModel):
chatbot_id: str
include_frontend: bool = True
# ─── Generic Response Models ───────────────────────────────────────────────────
class MessageDetail(BaseModel):
detail: str
class SuccessResponse(BaseModel):
success: bool
message: str
class ErrorResponse(BaseModel):
error: str
detail: Optional[str] = None
# ─── Lead Models ───────────────────────────────────────────────────────────────
class LeadCreate(BaseModel):
email: Optional[str] = None
name: Optional[str] = None
phone: Optional[str] = None
company: Optional[str] = None
conversation_id: Optional[str] = None
class LeadResponse(BaseModel):
id: str
chatbot_id: str
conversation_id: Optional[str] = None
email: Optional[str] = None
name: Optional[str] = None
phone: Optional[str] = None
company: Optional[str] = None
status: str = "new"
notes: Optional[str] = None
created_at: Optional[datetime] = None
class LeadUpdate(BaseModel):
status: Optional[str] = None # new, contacted, qualified, closed, lost
notes: Optional[str] = None
# ─── URL Source Models ─────────────────────────────────────────────────────────
class UrlSourceCreate(BaseModel):
url: str
class UrlSourceResponse(BaseModel):
id: str
chatbot_id: str
url: str
status: str
page_title: Optional[str] = None
chunk_count: int = 0
error_message: Optional[str] = None
created_at: Optional[datetime] = None
# ─── Feedback Models ───────────────────────────────────────────────────────────
class FeedbackCreate(BaseModel):
message_id: str
feedback: str # 'positive' or 'negative'
# ─── Test Models ──────────────────────────────────────────────────────────────
class TestQuestion(BaseModel):
question: str
class TestChatRequest(BaseModel):
questions: List[str] = Field(min_length=1, max_length=10)
class TestChatResult(BaseModel):
question: str
response: str
confidence_score: float
sources: List[SourceDocument]
model_used: str
# ─── Inbox Models ─────────────────────────────────────────────────────────────
class InboxConversation(BaseModel):
id: str
chatbot_id: str
chatbot_name: str
session_id: Optional[str] = None
language: str
message_count: int
first_message: Optional[str] = None
status: str = "open"
last_agent_reply_at: Optional[datetime] = None
created_at: Optional[datetime] = None
class InboxMessage(BaseModel):
id: str
role: str
content: str
sources: Optional[List[Dict]] = None
confidence_score: Optional[float] = None
is_handoff: bool = False
created_at: Optional[datetime] = None
class ConversationStatusUpdate(BaseModel):
status: str # open, agent_handling, resolved
class AgentReplyCreate(BaseModel):
message: str = Field(min_length=1, max_length=4000)
# ─── Channel Models ────────────────────────────────────────────────────────────
class ChannelConnectionResponse(BaseModel):
id: str
channel: str
bot_username: Optional[str] = None
is_active: bool
created_at: Optional[datetime] = None
# ─── Admin Models ──────────────────────────────────────────────────────────────
class AdminUserListItem(BaseModel):
id: str
email: str
company_name: Optional[str] = None
plan: str = "free"
subscription_status: str = "active"
chatbot_count: int = 0
conversations_count: int = 0
is_suspended: bool = False
is_admin: bool = False
created_at: Optional[datetime] = None
class AdminUserDetail(AdminUserListItem):
website: Optional[str] = None
industry: Optional[str] = None
chatbots: List[Dict[str, Any]] = []
class AdminChangePlanRequest(BaseModel):
plan: str
reason: Optional[str] = None
class AdminSuspendRequest(BaseModel):
suspend: bool
reason: Optional[str] = None
class AdminStatsResponse(BaseModel):
total_users: int
total_chatbots: int
total_published_chatbots: int
total_conversations: int
total_messages: int
active_subscriptions: Dict[str, int]
class AdminChatbotListItem(BaseModel):
id: str
name: str
owner_email: Optional[str] = None
company_name: Optional[str] = None
is_published: bool = False
document_count: int = 0
conversation_count: int = 0
created_at: Optional[datetime] = None
class AdminSystemHealth(BaseModel):
db: str
qdrant: str
llm_providers: Dict[str, bool]
timestamp: datetime
class AdminConversationListItem(BaseModel):
id: str
chatbot_name: Optional[str] = None
session_id: Optional[str] = None
language: Optional[str] = None
message_count: int = 0
created_at: Optional[datetime] = None
first_message: Optional[str] = None
# ─── Appointment Models ────────────────────────────────────────────────────────
class BusinessHoursEntry(BaseModel):
day_of_week: int = Field(ge=0, le=6) # 0=Mon, 6=Sun
is_open: bool = True
open_time: str = "09:00" # HH:MM
close_time: str = "17:00"
slot_duration_minutes: int = Field(default=60, ge=15, le=480)
class BusinessHoursSave(BaseModel):
hours: List[BusinessHoursEntry]
class AppointmentCreate(BaseModel):
customer_name: str = Field(min_length=1, max_length=200)
customer_contact: str = Field(min_length=1, max_length=200)
service: Optional[str] = None
slot_start: datetime
notes: Optional[str] = None
conversation_id: Optional[str] = None
class AppointmentResponse(BaseModel):
id: str
chatbot_id: str
conversation_id: Optional[str] = None
customer_name: str
customer_contact: str
service: Optional[str] = None
slot_start: datetime
slot_end: datetime
status: str
notes: Optional[str] = None
created_at: Optional[datetime] = None
class AppointmentStatusUpdate(BaseModel):
status: str # pending, confirmed, cancelled, completed
# ─── Campaign Models ───────────────────────────────────────────────────────────
class CampaignCreate(BaseModel):
chatbot_id: str
title: str = Field(min_length=1, max_length=200)
message: str = Field(min_length=1, max_length=4000)
class CampaignResponse(BaseModel):
id: str
chatbot_id: str
title: str
message: str
status: str
recipients_count: int
sent_count: int
created_at: Optional[datetime] = None
sent_at: Optional[datetime] = None