import uuid from typing import Any from sqlalchemy import Enum, ForeignKey, Text from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import Mapped, mapped_column, relationship from app.models.base import Base, TimestampMixin, UUIDPrimaryKeyMixin from app.models.enums import ConversationMode, ConversationStatus, MessageRole class Conversation(UUIDPrimaryKeyMixin, TimestampMixin, Base): """Chat thread. The only core mode is query (RAG Q&A); EE adds insight. Capture is no longer a conversation — it writes a Document directly (see app/authoring/) — so this table holds no per-turn engine state any more. """ __tablename__ = "conversations" mode: Mapped[ConversationMode] = mapped_column( Enum(ConversationMode, native_enum=False, length=32) ) status: Mapped[ConversationStatus] = mapped_column( Enum(ConversationStatus, native_enum=False, length=32), default=ConversationStatus.active, ) user_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("users.id", ondelete="CASCADE"), index=True ) messages: Mapped[list["Message"]] = relationship( back_populates="conversation", cascade="all, delete-orphan", order_by="Message.created_at", ) class Message(UUIDPrimaryKeyMixin, TimestampMixin, Base): __tablename__ = "messages" conversation_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("conversations.id", ondelete="CASCADE"), index=True ) role: Mapped[MessageRole] = mapped_column( Enum(MessageRole, native_enum=False, length=32) ) content: Mapped[str] = mapped_column(Text) # Assistant turns snapshot their citations here ({"sources": [...]}) so # they survive reload and re-indexing — chunks are disposable, the # rendered citation is not. meta: Mapped[dict[str, Any]] = mapped_column( JSONB, default=dict, server_default="{}" ) conversation: Mapped[Conversation] = relationship(back_populates="messages")