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