from datetime import datetime from typing import List, Optional from sqlalchemy import String, Integer, DateTime, Text, Float, ForeignKey, JSON, Enum from sqlalchemy.orm import Mapped, mapped_column, relationship from .db import Base class User(Base): __tablename__ = "users" id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) email: Mapped[str] = mapped_column(String(255), unique=True, index=True, nullable=False) password_hash: Mapped[str] = mapped_column(String(255), nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) # ============ Voice ============ class Voice(Base): __tablename__ = "voices" id: Mapped[str] = mapped_column(String(64), primary_key=True) user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("users.id"), index=True, nullable=True) name: Mapped[str] = mapped_column(String(128), nullable=False) vendor: Mapped[str] = mapped_column(String(64), nullable=False) gender: Mapped[str] = mapped_column(String(32), nullable=False) language: Mapped[str] = mapped_column(String(16), nullable=False) description: Mapped[str] = mapped_column(String(255), nullable=False) model: Mapped[Optional[str]] = mapped_column(String(128), nullable=True) # 厂商语音模型标识 voice_key: Mapped[Optional[str]] = mapped_column(String(128), nullable=True) # 厂商voice_key api_key: Mapped[Optional[str]] = mapped_column(String(512), nullable=True) # 每个声音独立 API key base_url: Mapped[Optional[str]] = mapped_column(String(512), nullable=True) # 每个声音独立 OpenAI-compatible base_url speed: Mapped[float] = mapped_column(Float, default=1.0) gain: Mapped[int] = mapped_column(Integer, default=0) pitch: Mapped[int] = mapped_column(Integer, default=0) enabled: Mapped[bool] = mapped_column(default=True) is_system: Mapped[bool] = mapped_column(default=False) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) user = relationship("User", foreign_keys=[user_id]) # ============ LLM Model ============ class LLMModel(Base): __tablename__ = "llm_models" id: Mapped[str] = mapped_column(String(64), primary_key=True) user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), index=True) name: Mapped[str] = mapped_column(String(128), nullable=False) vendor: Mapped[str] = mapped_column(String(64), nullable=False) type: Mapped[str] = mapped_column(String(32), nullable=False) # text/embedding/rerank base_url: Mapped[str] = mapped_column(String(512), nullable=False) api_key: Mapped[str] = mapped_column(String(512), nullable=False) model_name: Mapped[Optional[str]] = mapped_column(String(128), nullable=True) temperature: Mapped[Optional[float]] = mapped_column(Float, nullable=True) context_length: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) enabled: Mapped[bool] = mapped_column(default=True) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) user = relationship("User") # ============ ASR Model ============ class ASRModel(Base): __tablename__ = "asr_models" id: Mapped[str] = mapped_column(String(64), primary_key=True) user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), index=True) name: Mapped[str] = mapped_column(String(128), nullable=False) vendor: Mapped[str] = mapped_column(String(64), nullable=False) language: Mapped[str] = mapped_column(String(32), nullable=False) # zh/en/Multi-lingual base_url: Mapped[str] = mapped_column(String(512), nullable=False) api_key: Mapped[str] = mapped_column(String(512), nullable=False) model_name: Mapped[Optional[str]] = mapped_column(String(128), nullable=True) hotwords: Mapped[dict] = mapped_column(JSON, default=list) enable_punctuation: Mapped[bool] = mapped_column(default=True) enable_normalization: Mapped[bool] = mapped_column(default=True) enabled: Mapped[bool] = mapped_column(default=True) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) user = relationship("User") # ============ Tool Resource ============ class ToolResource(Base): __tablename__ = "tool_resources" id: Mapped[str] = mapped_column(String(64), primary_key=True) user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("users.id"), index=True, nullable=True) name: Mapped[str] = mapped_column(String(128), nullable=False) description: Mapped[str] = mapped_column(String(512), nullable=False, default="") category: Mapped[str] = mapped_column(String(32), nullable=False, default="system") # system/query icon: Mapped[str] = mapped_column(String(64), nullable=False, default="Wrench") http_method: Mapped[str] = mapped_column(String(16), nullable=False, default="GET") http_url: Mapped[Optional[str]] = mapped_column(String(1024), nullable=True) http_headers: Mapped[dict] = mapped_column(JSON, default=dict) http_timeout_ms: Mapped[int] = mapped_column(Integer, default=10000) parameter_schema: Mapped[dict] = mapped_column(JSON, default=dict) parameter_defaults: Mapped[dict] = mapped_column(JSON, default=dict) wait_for_response: Mapped[bool] = mapped_column(default=False) enabled: Mapped[bool] = mapped_column(default=True) is_system: Mapped[bool] = mapped_column(default=False) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) user = relationship("User") # ============ Assistant ============ class Assistant(Base): __tablename__ = "assistants" id: Mapped[str] = mapped_column(String(64), primary_key=True) user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), index=True) name: Mapped[str] = mapped_column(String(255), nullable=False) call_count: Mapped[int] = mapped_column(Integer, default=0) first_turn_mode: Mapped[str] = mapped_column(String(32), default="bot_first") opener: Mapped[str] = mapped_column(Text, default="") manual_opener_tool_calls: Mapped[list] = mapped_column(JSON, default=list) generated_opener_enabled: Mapped[bool] = mapped_column(default=False) prompt: Mapped[str] = mapped_column(Text, default="") knowledge_base_id: Mapped[Optional[str]] = mapped_column(String(64), nullable=True) language: Mapped[str] = mapped_column(String(16), default="zh") voice_output_enabled: Mapped[bool] = mapped_column(default=True) voice: Mapped[Optional[str]] = mapped_column(String(64), nullable=True) speed: Mapped[float] = mapped_column(Float, default=1.0) hotwords: Mapped[dict] = mapped_column(JSON, default=list) tools: Mapped[dict] = mapped_column(JSON, default=list) asr_interim_enabled: Mapped[bool] = mapped_column(default=False) bot_cannot_be_interrupted: Mapped[bool] = mapped_column(default=False) interruption_sensitivity: Mapped[int] = mapped_column(Integer, default=500) config_mode: Mapped[str] = mapped_column(String(32), default="platform") api_url: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) api_key: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) # 模型关联 llm_model_id: Mapped[Optional[str]] = mapped_column(String(64), nullable=True) asr_model_id: Mapped[Optional[str]] = mapped_column(String(64), nullable=True) embedding_model_id: Mapped[Optional[str]] = mapped_column(String(64), nullable=True) rerank_model_id: Mapped[Optional[str]] = mapped_column(String(64), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) user = relationship("User") call_records = relationship("CallRecord", back_populates="assistant") opener_audio = relationship("AssistantOpenerAudio", back_populates="assistant", uselist=False, cascade="all, delete-orphan") class AssistantOpenerAudio(Base): __tablename__ = "assistant_opener_audio" assistant_id: Mapped[str] = mapped_column(String(64), ForeignKey("assistants.id"), primary_key=True) enabled: Mapped[bool] = mapped_column(default=False) file_path: Mapped[Optional[str]] = mapped_column(String(512), nullable=True) encoding: Mapped[str] = mapped_column(String(32), default="pcm_s16le") sample_rate_hz: Mapped[int] = mapped_column(Integer, default=16000) channels: Mapped[int] = mapped_column(Integer, default=1) duration_ms: Mapped[int] = mapped_column(Integer, default=0) text_hash: Mapped[Optional[str]] = mapped_column(String(128), nullable=True) tts_fingerprint: Mapped[Optional[str]] = mapped_column(String(256), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) assistant = relationship("Assistant", back_populates="opener_audio") # ============ Knowledge Base ============ class KnowledgeBase(Base): __tablename__ = "knowledge_bases" id: Mapped[str] = mapped_column(String(64), primary_key=True) user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), index=True) name: Mapped[str] = mapped_column(String(255), nullable=False) description: Mapped[str] = mapped_column(Text, default="") embedding_model: Mapped[str] = mapped_column(String(64), default="text-embedding-3-small") chunk_size: Mapped[int] = mapped_column(Integer, default=500) chunk_overlap: Mapped[int] = mapped_column(Integer, default=50) doc_count: Mapped[int] = mapped_column(Integer, default=0) chunk_count: Mapped[int] = mapped_column(Integer, default=0) status: Mapped[str] = mapped_column(String(32), default="active") created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) user = relationship("User") documents = relationship("KnowledgeDocument", back_populates="kb") class KnowledgeDocument(Base): __tablename__ = "knowledge_documents" id: Mapped[str] = mapped_column(String(64), primary_key=True) kb_id: Mapped[str] = mapped_column(String(64), ForeignKey("knowledge_bases.id"), index=True) name: Mapped[str] = mapped_column(String(255), nullable=False) size: Mapped[str] = mapped_column(String(64), nullable=False) file_type: Mapped[str] = mapped_column(String(32), default="txt") storage_url: Mapped[Optional[str]] = mapped_column(String(512), nullable=True) status: Mapped[str] = mapped_column(String(32), default="pending") # pending/processing/completed/failed chunk_count: Mapped[int] = mapped_column(Integer, default=0) error_message: Mapped[Optional[str]] = mapped_column(Text, nullable=True) upload_date: Mapped[str] = mapped_column(String(32), nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) processed_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) kb = relationship("KnowledgeBase", back_populates="documents") # ============ Workflow ============ class Workflow(Base): __tablename__ = "workflows" id: Mapped[str] = mapped_column(String(64), primary_key=True) user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), index=True) name: Mapped[str] = mapped_column(String(255), nullable=False) node_count: Mapped[int] = mapped_column(Integer, default=0) created_at: Mapped[str] = mapped_column(String(32), default="") updated_at: Mapped[str] = mapped_column(String(32), default="") global_prompt: Mapped[Optional[str]] = mapped_column(Text, nullable=True) nodes: Mapped[dict] = mapped_column(JSON, default=list) edges: Mapped[dict] = mapped_column(JSON, default=list) user = relationship("User") # ============ Call Record ============ class CallRecord(Base): __tablename__ = "call_records" id: Mapped[str] = mapped_column(String(64), primary_key=True) user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), index=True) assistant_id: Mapped[Optional[str]] = mapped_column(String(64), ForeignKey("assistants.id"), index=True) source: Mapped[str] = mapped_column(String(32), default="debug") status: Mapped[str] = mapped_column(String(32), default="connected") started_at: Mapped[str] = mapped_column(String(32), nullable=False) ended_at: Mapped[Optional[str]] = mapped_column(String(32), nullable=True) duration_seconds: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) summary: Mapped[Optional[str]] = mapped_column(Text, nullable=True) cost: Mapped[float] = mapped_column(Float, default=0.0) call_metadata: Mapped[dict] = mapped_column(JSON, default=dict) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) user = relationship("User") assistant = relationship("Assistant", back_populates="call_records") transcripts = relationship("CallTranscript", back_populates="call_record") audio_segments = relationship("CallAudioSegment", back_populates="call_record") class CallTranscript(Base): __tablename__ = "call_transcripts" id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) call_id: Mapped[str] = mapped_column(String(64), ForeignKey("call_records.id"), index=True) turn_index: Mapped[int] = mapped_column(Integer, nullable=False) speaker: Mapped[str] = mapped_column(String(16), nullable=False) # human/ai content: Mapped[str] = mapped_column(Text, nullable=False) confidence: Mapped[Optional[float]] = mapped_column(Float, nullable=True) start_ms: Mapped[int] = mapped_column(Integer, nullable=False) end_ms: Mapped[int] = mapped_column(Integer, nullable=False) duration_ms: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) emotion: Mapped[Optional[str]] = mapped_column(String(32), nullable=True) call_record = relationship("CallRecord", back_populates="transcripts") class CallAudioSegment(Base): __tablename__ = "call_audio_segments" id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) call_id: Mapped[str] = mapped_column(String(64), ForeignKey("call_records.id"), index=True) transcript_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("call_transcripts.id"), nullable=True) turn_index: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) audio_url: Mapped[str] = mapped_column(String(512), nullable=False) audio_format: Mapped[str] = mapped_column(String(16), default="mp3") file_size_bytes: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) start_ms: Mapped[int] = mapped_column(Integer, nullable=False) end_ms: Mapped[int] = mapped_column(Integer, nullable=False) duration_ms: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) call_record = relationship("CallRecord", back_populates="audio_segments")