mirror of
http://88.130.71.182:3000/BlitTech/contexta_be.git
synced 2026-06-12 23:23:21 +00:00
feat: add appointments, campaigns, admin, storage, tests and various updates
- 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>
This commit is contained in:
223
app/models.py
223
app/models.py
@@ -1,8 +1,9 @@
|
||||
from pydantic import BaseModel, EmailStr, Field
|
||||
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 ────────────────────────────────────────────────────────────────────
|
||||
@@ -59,6 +60,7 @@ class UserResponse(BaseModel):
|
||||
email: str
|
||||
company_name: Optional[str] = None
|
||||
plan: str = "free"
|
||||
is_admin: bool = False
|
||||
created_at: Optional[datetime] = None
|
||||
|
||||
|
||||
@@ -100,6 +102,39 @@ class ChatbotCreate(BaseModel):
|
||||
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"
|
||||
@@ -116,12 +151,46 @@ class ChatbotCreate(BaseModel):
|
||||
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
|
||||
@@ -139,6 +208,7 @@ class ChatbotUpdate(BaseModel):
|
||||
handoff_message: Optional[str] = None
|
||||
handoff_email: Optional[str] = None
|
||||
handoff_keywords: Optional[List[str]] = None
|
||||
booking_enabled: Optional[bool] = None
|
||||
|
||||
|
||||
class ChatbotResponse(BaseModel):
|
||||
@@ -172,6 +242,7 @@ class ChatbotResponse(BaseModel):
|
||||
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):
|
||||
@@ -355,9 +426,16 @@ class LeadResponse(BaseModel):
|
||||
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):
|
||||
@@ -392,6 +470,8 @@ class InboxConversation(BaseModel):
|
||||
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
|
||||
|
||||
|
||||
@@ -405,13 +485,148 @@ class InboxMessage(BaseModel):
|
||||
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
|
||||
wa_keyword: Optional[str] = None
|
||||
wa_link: Optional[str] = None
|
||||
is_active: bool
|
||||
created_at: Optional[datetime] = None
|
||||
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
|
||||
Reference in New Issue
Block a user