from pgvector.sqlalchemy import Vector from sqlalchemy import ( JSON, Boolean, Column, DateTime, Float, ForeignKey, Integer, String, Text, ) from sqlalchemy.orm import relationship from sqlalchemy.dialects.postgresql import TSVECTOR as TSVector from app.core.db import Base, utc_now pgvector_type = Vector(1536) class MemorySource(Base): __tablename__ = "memory_sources" id = Column(String, primary_key=True) user_id = Column(String, ForeignKey("users.id"), nullable=False, index=True) source_type = Column(String, nullable=False) # transcript / note / draft raw_text = Column(Text, nullable=True) storage_key = Column(String, nullable=True) speaker = Column(String, nullable=True) captured_at = Column(DateTime(timezone=True), nullable=True) status = Column(String, default="active") conversation_id = Column(String, ForeignKey("conversations.id"), nullable=True) created_at = Column(DateTime(timezone=True), default=utc_now) chunks = relationship( "MemoryChunk", back_populates="source", cascade="all, delete-orphan" ) class MemoryChunk(Base): __tablename__ = "memory_chunks" id = Column(String, primary_key=True) source_id = Column( String, ForeignKey("memory_sources.id"), nullable=False, index=True ) user_id = Column(String, ForeignKey("users.id"), nullable=False, index=True) content = Column(Text, nullable=False) # pgvector embedding — Alembic migration 负责 CREATE EXTENSION vector 及列类型 embedding = Column(pgvector_type, nullable=True) # PostgreSQL FTS — Alembic migration 负责 generated tsvector 列 + GIN index content_tsv = Column(TSVector, nullable=True) chunk_index = Column(Integer, nullable=False) speaker = Column(String, nullable=True) event_year = Column(Integer, nullable=True) metadata_json = Column(JSON, nullable=True) is_excluded = Column(Boolean, default=False) created_at = Column(DateTime(timezone=True), default=utc_now) source = relationship("MemorySource", back_populates="chunks") class MemorySummary(Base): __tablename__ = "memory_summaries" id = Column(String, primary_key=True) user_id = Column(String, ForeignKey("users.id"), nullable=False, index=True) summary_type = Column(String, nullable=False) # session / rolling / topic content = Column(Text, nullable=False) source_chunk_ids = Column(JSON, nullable=True) created_at = Column(DateTime(timezone=True), default=utc_now) updated_at = Column(DateTime(timezone=True), default=utc_now, onupdate=utc_now) class MemoryFact(Base): __tablename__ = "memory_facts" id = Column(String, primary_key=True) user_id = Column(String, ForeignKey("users.id"), nullable=False, index=True) fact_type = Column( String, nullable=False ) # person / event / relation / place / milestone subject = Column(String, nullable=True) predicate = Column(String, nullable=True) object_json = Column(JSON, nullable=True) confidence = Column(Float, default=0.0) source_chunk_id = Column(String, ForeignKey("memory_chunks.id"), nullable=True) status = Column(String, default="candidate") # candidate / confirmed / rejected created_at = Column(DateTime(timezone=True), default=utc_now) class TimelineEvent(Base): __tablename__ = "timeline_events" id = Column(String, primary_key=True) user_id = Column(String, ForeignKey("users.id"), nullable=False, index=True) event_year = Column(Integer, nullable=True) event_date = Column(String, nullable=True) title = Column(String, nullable=False) description = Column(Text, nullable=True) person_refs = Column(JSON, nullable=True) source_fact_ids = Column(JSON, nullable=True) created_at = Column(DateTime(timezone=True), default=utc_now) class MemoryCurationAction(Base): __tablename__ = "memory_curation_actions" id = Column(String, primary_key=True) user_id = Column(String, ForeignKey("users.id"), nullable=False, index=True) action_type = Column( String, nullable=False ) # exclude / restore / correct / merge / confirm / reject target_type = Column( String, nullable=False ) # chunk / fact / summary / timeline_event target_id = Column(String, nullable=False) details = Column(JSON, nullable=True) created_at = Column(DateTime(timezone=True), default=utc_now)