File size: 3,006 Bytes
7c6ffa6 3bcdb36 7c6ffa6 3bcdb36 7c6ffa6 3bcdb36 7c6ffa6 3bcdb36 7c6ffa6 3bcdb36 7c6ffa6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | """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")
|