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")