mirror of
http://88.130.71.182:3000/BlitTech/contexta_be.git
synced 2026-06-13 08:30:07 +00:00
- Add new routers: admin, appointments, campaigns - Add storage service and logging config - Add migrations directory and test suite with pytest config - Add supabase_migration_features.sql - Update models, dependencies, config, and existing routers - Remove whatsapp_service (deleted) - Update pyproject.toml and uv.lock dependencies Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
632 lines
19 KiB
Python
632 lines
19 KiB
Python
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
|
|
|
|
|
|
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-0905"
|
|
|
|
@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"<script[^>]*>.*?</script>", "", 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"<script[^>]*>.*?</script>", "", 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
|
|
|
|
|
|
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'
|
|
|
|
|
|
# ─── 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 |