"""Chat session and message persistence models.""" from datetime import datetime from sqlalchemy import JSON, DateTime, ForeignKey, Index, String, Text, UniqueConstraint, func from sqlalchemy.orm import Mapped, mapped_column, relationship from app.core.database import Base from app.utils.ids import prefixed_id class ChatSession(Base): __tablename__ = "chat_sessions" __table_args__ = ( Index("ix_chat_sessions_user_updated_at", "user_id", "updated_at"), ) id: Mapped[str] = mapped_column( String(40), primary_key=True, default=lambda: prefixed_id("csess"), ) user_id: Mapped[str] = mapped_column( String(40), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True, ) source_id: Mapped[str | None] = mapped_column(String(40), nullable=True, index=True) subject: Mapped[str] = mapped_column(String(120), nullable=False, default="") title: Mapped[str] = mapped_column(String(255), nullable=False, default="New chat") # Structured academic origin (Tuition question, chapter/topic, uploaded # source, and safe return route). This keeps continuity independent of the # display title or free-form first message. context_data: Mapped[dict[str, object]] = mapped_column( JSON, nullable=False, default=dict, ) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False, ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False, ) messages: Mapped[list["ChatMessageRecord"]] = relationship( back_populates="session", cascade="all, delete-orphan", order_by="ChatMessageRecord.created_at", ) class ChatMessageRecord(Base): __tablename__ = "chat_messages" __table_args__ = ( UniqueConstraint( "session_id", "client_turn_id", "role", name="uq_chat_messages_session_turn_role", ), ) id: Mapped[str] = mapped_column( String(40), primary_key=True, default=lambda: prefixed_id("cmsg"), ) session_id: Mapped[str] = mapped_column( String(40), ForeignKey("chat_sessions.id", ondelete="CASCADE"), nullable=False, index=True, ) role: Mapped[str] = mapped_column(String(20), nullable=False) content: Mapped[str] = mapped_column(Text, nullable=False) intent: Mapped[str | None] = mapped_column(String(40), nullable=True) evidence_label: Mapped[str | None] = mapped_column(String(255), nullable=True) client_turn_id: Mapped[str | None] = mapped_column(String(64), nullable=True) web_sources: Mapped[list[dict[str, object]]] = mapped_column( JSON, nullable=False, default=list, ) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False, ) session: Mapped["ChatSession"] = relationship(back_populates="messages")