from datetime import datetime from enum import Enum from typing import List, Optional from pydantic import BaseModel # ============ Enums ============ class AssistantConfigMode(str, Enum): PLATFORM = "platform" DIFY = "dify" FASTGPT = "fastgpt" NONE = "none" class LLMModelType(str, Enum): TEXT = "text" EMBEDDING = "embedding" RERANK = "rerank" class ASRLanguage(str, Enum): ZH = "zh" EN = "en" MULTILINGUAL = "Multi-lingual" class VoiceGender(str, Enum): MALE = "Male" FEMALE = "Female" class CallRecordSource(str, Enum): DEBUG = "debug" EXTERNAL = "external" class CallRecordStatus(str, Enum): CONNECTED = "connected" MISSED = "missed" FAILED = "failed" # ============ Voice ============ class VoiceBase(BaseModel): name: str vendor: str gender: str # "Male" | "Female" language: str # "zh" | "en" description: str = "" class VoiceCreate(VoiceBase): id: Optional[str] = None model: Optional[str] = None # 厂商语音模型标识 voice_key: Optional[str] = None # 厂商voice_key speed: float = 1.0 gain: int = 0 pitch: int = 0 enabled: bool = True class VoiceUpdate(BaseModel): name: Optional[str] = None description: Optional[str] = None model: Optional[str] = None voice_key: Optional[str] = None speed: Optional[float] = None gain: Optional[int] = None pitch: Optional[int] = None enabled: Optional[bool] = None class VoiceOut(VoiceBase): id: str user_id: Optional[int] = None model: Optional[str] = None voice_key: Optional[str] = None speed: float = 1.0 gain: int = 0 pitch: int = 0 enabled: bool = True is_system: bool = False created_at: Optional[datetime] = None class Config: from_attributes = True class VoicePreviewRequest(BaseModel): text: str speed: Optional[float] = None gain: Optional[int] = None pitch: Optional[int] = None class VoicePreviewResponse(BaseModel): success: bool audio_url: Optional[str] = None duration_ms: Optional[int] = None error: Optional[str] = None # ============ LLM Model ============ class LLMModelBase(BaseModel): name: str vendor: str type: LLMModelType base_url: str api_key: str model_name: Optional[str] = None temperature: Optional[float] = None context_length: Optional[int] = None enabled: bool = True class LLMModelCreate(LLMModelBase): id: Optional[str] = None class LLMModelUpdate(BaseModel): name: Optional[str] = None base_url: Optional[str] = None api_key: Optional[str] = None model_name: Optional[str] = None temperature: Optional[float] = None context_length: Optional[int] = None enabled: Optional[bool] = None class LLMModelOut(LLMModelBase): id: str user_id: int created_at: Optional[datetime] = None updated_at: Optional[datetime] = None class Config: from_attributes = True class LLMModelTestResponse(BaseModel): success: bool latency_ms: Optional[int] = None message: Optional[str] = None # ============ ASR Model ============ class ASRModelBase(BaseModel): name: str vendor: str language: str # "zh" | "en" | "Multi-lingual" base_url: str api_key: str model_name: Optional[str] = None enabled: bool = True class ASRModelCreate(ASRModelBase): id: Optional[str] = None hotwords: List[str] = [] enable_punctuation: bool = True enable_normalization: bool = True class ASRModelUpdate(BaseModel): name: Optional[str] = None language: Optional[str] = None base_url: Optional[str] = None api_key: Optional[str] = None model_name: Optional[str] = None hotwords: Optional[List[str]] = None enable_punctuation: Optional[bool] = None enable_normalization: Optional[bool] = None enabled: Optional[bool] = None class ASRModelOut(ASRModelBase): id: str user_id: int hotwords: List[str] = [] enable_punctuation: bool = True enable_normalization: bool = True created_at: Optional[datetime] = None class Config: from_attributes = True class ASRTestRequest(BaseModel): audio_url: Optional[str] = None audio_data: Optional[str] = None # base64 encoded class ASRTestResponse(BaseModel): success: bool transcript: Optional[str] = None language: Optional[str] = None confidence: Optional[float] = None duration_ms: Optional[int] = None latency_ms: Optional[int] = None message: Optional[str] = None error: Optional[str] = None # ============ Assistant ============ class AssistantBase(BaseModel): name: str opener: str = "" prompt: str = "" knowledgeBaseId: Optional[str] = None language: str = "zh" voice: Optional[str] = None speed: float = 1.0 hotwords: List[str] = [] tools: List[str] = [] interruptionSensitivity: int = 500 configMode: str = "platform" apiUrl: Optional[str] = None apiKey: Optional[str] = None # 模型关联 llmModelId: Optional[str] = None asrModelId: Optional[str] = None embeddingModelId: Optional[str] = None rerankModelId: Optional[str] = None class AssistantCreate(AssistantBase): pass class AssistantUpdate(BaseModel): name: Optional[str] = None opener: Optional[str] = None prompt: Optional[str] = None knowledgeBaseId: Optional[str] = None language: Optional[str] = None voice: Optional[str] = None speed: Optional[float] = None hotwords: Optional[List[str]] = None tools: Optional[List[str]] = None interruptionSensitivity: Optional[int] = None configMode: Optional[str] = None apiUrl: Optional[str] = None apiKey: Optional[str] = None llmModelId: Optional[str] = None asrModelId: Optional[str] = None embeddingModelId: Optional[str] = None rerankModelId: Optional[str] = None class AssistantOut(AssistantBase): id: str callCount: int = 0 created_at: Optional[datetime] = None updated_at: Optional[datetime] = None class Config: from_attributes = True class AssistantStats(BaseModel): assistant_id: str total_calls: int = 0 connected_calls: int = 0 missed_calls: int = 0 avg_duration_seconds: float = 0.0 today_calls: int = 0 # ============ Knowledge Base ============ class KnowledgeDocument(BaseModel): id: str name: str size: str fileType: str = "txt" storageUrl: Optional[str] = None status: str = "pending" chunkCount: int = 0 uploadDate: str class KnowledgeDocumentCreate(BaseModel): name: str size: str fileType: str = "txt" storageUrl: Optional[str] = None class KnowledgeDocumentUpdate(BaseModel): status: Optional[str] = None chunkCount: Optional[int] = None errorMessage: Optional[str] = None class KnowledgeBaseBase(BaseModel): name: str description: str = "" embeddingModel: str = "text-embedding-3-small" chunkSize: int = 500 chunkOverlap: int = 50 class KnowledgeBaseCreate(KnowledgeBaseBase): pass class KnowledgeBaseUpdate(BaseModel): name: Optional[str] = None description: Optional[str] = None embeddingModel: Optional[str] = None chunkSize: Optional[int] = None chunkOverlap: Optional[int] = None status: Optional[str] = None class KnowledgeBaseOut(KnowledgeBaseBase): id: str docCount: int = 0 chunkCount: int = 0 status: str = "active" createdAt: Optional[datetime] = None updatedAt: Optional[datetime] = None documents: List[KnowledgeDocument] = [] class Config: from_attributes = True # ============ Knowledge Search ============ class KnowledgeSearchQuery(BaseModel): query: str kb_id: str nResults: int = 5 class KnowledgeSearchResult(BaseModel): query: str results: List[dict] class DocumentIndexRequest(BaseModel): document_id: str content: str class KnowledgeStats(BaseModel): kb_id: str docCount: int chunkCount: int # ============ Workflow ============ class WorkflowNode(BaseModel): name: str type: str isStart: Optional[bool] = None metadata: dict prompt: Optional[str] = None messagePlan: Optional[dict] = None variableExtractionPlan: Optional[dict] = None tool: Optional[dict] = None globalNodePlan: Optional[dict] = None class WorkflowEdge(BaseModel): from_: str to: str label: Optional[str] = None class Config: populate_by_name = True class WorkflowBase(BaseModel): name: str nodeCount: int = 0 createdAt: str = "" updatedAt: str = "" globalPrompt: Optional[str] = None nodes: List[dict] = [] edges: List[dict] = [] class WorkflowCreate(WorkflowBase): pass class WorkflowUpdate(BaseModel): name: Optional[str] = None nodeCount: Optional[int] = None nodes: Optional[List[dict]] = None edges: Optional[List[dict]] = None globalPrompt: Optional[str] = None class WorkflowOut(WorkflowBase): id: str class Config: from_attributes = True # ============ Call Record ============ class TranscriptSegment(BaseModel): turnIndex: int speaker: str # human/ai content: str confidence: Optional[float] = None startMs: int endMs: int durationMs: Optional[int] = None audioUrl: Optional[str] = None emotion: Optional[str] = None class CallRecordCreate(BaseModel): user_id: int assistant_id: Optional[str] = None source: str = "debug" status: Optional[str] = None cost: Optional[float] = None class CallRecordUpdate(BaseModel): status: Optional[str] = None summary: Optional[str] = None duration_seconds: Optional[int] = None ended_at: Optional[str] = None cost: Optional[float] = None metadata: Optional[dict] = None class CallRecordOut(BaseModel): id: str user_id: int assistant_id: Optional[str] = None source: str status: str started_at: str ended_at: Optional[str] = None duration_seconds: Optional[int] = None summary: Optional[str] = None cost: float = 0.0 metadata: dict = {} created_at: Optional[datetime] = None transcripts: List[TranscriptSegment] = [] class Config: from_attributes = True # ============ Call Transcript ============ class TranscriptCreate(BaseModel): turn_index: int speaker: str content: str confidence: Optional[float] = None start_ms: int end_ms: int duration_ms: Optional[int] = None emotion: Optional[str] = None class TranscriptOut(TranscriptCreate): id: int audio_url: Optional[str] = None class Config: from_attributes = True # ============ History Stats ============ class HistoryStats(BaseModel): total_calls: int = 0 connected_calls: int = 0 missed_calls: int = 0 failed_calls: int = 0 avg_duration_seconds: float = 0.0 total_cost: float = 0.0 by_status: dict = {} by_source: dict = {} daily_trend: List[dict] = [] # ============ Dashboard ============ class DashboardStats(BaseModel): totalCalls: int answerRate: int avgDuration: str humanTransferCount: int trend: List[dict] # ============ API Response ============ class Message(BaseModel): message: str class DocumentIndexRequest(BaseModel): content: str class ListResponse(BaseModel): total: int page: int limit: int list: List class SearchResult(BaseModel): id: str started_at: str matched_content: Optional[str] = None