from sqlalchemy import ( JSON, Boolean, Column, DateTime, ForeignKey, Integer, String, Text, ) from sqlalchemy.orm import relationship from app.core.db import Base, utc_now class Conversation(Base): __tablename__ = "conversations" id = Column(String, primary_key=True) user_id = Column(String, ForeignKey("users.id"), nullable=False) started_at = Column(DateTime(timezone=True), default=utc_now) last_message_at = Column(DateTime(timezone=True), nullable=True) ended_at = Column(DateTime(timezone=True), nullable=True) duration_seconds = Column(Integer, default=0) summary = Column(Text, nullable=True) status = Column(String, default="active") current_topic = Column(String, nullable=True) conversation_stage = Column(String, nullable=True) deleted_at = Column(DateTime(timezone=True), nullable=True) # 内部评测 Playground:最近一次 GLM 对话评分快照(含逐轮分与对比文案) playground_conversation_judge_json = Column(JSON, nullable=True) user = relationship("User", back_populates="conversations") segments = relationship( "Segment", back_populates="conversation", cascade="all, delete-orphan" ) messages = relationship( "ConversationMessage", back_populates="conversation", cascade="all, delete-orphan", ) class Segment(Base): __tablename__ = "segments" id = Column(String, primary_key=True) conversation_id = Column(String, ForeignKey("conversations.id"), nullable=False) audio_url = Column(String, nullable=True) # 用户输入正文:语音 ASR 结果或键盘输入(历史列名 transcript_text) user_input_text = Column(Text, nullable=False) audio_duration_seconds = Column(Integer, nullable=True) created_at = Column(DateTime(timezone=True), default=utc_now) processed = Column(Boolean, default=False) # Phase 1 分类结果(回忆录 chapter 类目);非空表示 Phase 1 已完成 topic_category = Column(String, nullable=True) # Phase 2 已消费该段并完成叙事落库 narrated = Column(Boolean, default=False, server_default="false") # Phase 1 判定无需进故事管线(无 slots 且 LLM 判 none) skip_narrative = Column(Boolean, default=False, server_default="false") agent_response = Column(Text, nullable=True) tts_audio_urls = Column(JSON, nullable=True) # 用户轮次 durable message id(与 lineage_json 同步;便于查询) user_message_id = Column( String, ForeignKey("conversation_messages.id", ondelete="SET NULL"), nullable=True ) # DialogueLineage JSON(schema 见 conversation.lineage_schemas) lineage_json = Column(JSON, nullable=True) conversation = relationship("Conversation", back_populates="segments") class ConversationMessage(Base): """durable turn log aligned with Redis history shape (canonical chat source of truth).""" __tablename__ = "conversation_messages" id = Column(String, primary_key=True) conversation_id = Column(String, ForeignKey("conversations.id"), nullable=False) role = Column(String, nullable=False) # human / ai content = Column(Text, nullable=False) message_type = Column(String, nullable=False, default="text") voice_session_id = Column(String, nullable=True) duration_seconds = Column(Integer, nullable=True) tts_audio_urls = Column(JSON, nullable=True) segment_id = Column(String, ForeignKey("segments.id"), nullable=True) created_at = Column(DateTime(timezone=True), default=utc_now) # 本轮(与用户句配对)助手生成前检索到的 memory 证据 id 快照;Phase 8 可追溯 memory_retrieval_trace_json = Column(JSON, nullable=True) conversation = relationship("Conversation", back_populates="messages") segment = relationship("Segment", foreign_keys=[segment_id])