from datetime import datetime from typing import List, Optional from sqlalchemy import String, Integer, DateTime, Text, Float, ForeignKey, JSON 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) class Voice(Base): __tablename__ = "voices" id: Mapped[str] = mapped_column(String(64), primary_key=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) voice_params: Mapped[dict] = mapped_column(JSON, default=dict) 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) opener: Mapped[str] = mapped_column(Text, default="") 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: 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) 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) 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") 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") 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") 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")