Arag / app /models /chat_session.py
AuthorBot
feat: show IP address in sessions + persistent login until logout - Add ip_address field to ChatSession model (VARCHAR 45, handles IPv4+IPv6) - Store real client IP at session/init time via get_real_ip() - Expose ip_address in /sessions list and /sessions/{id}/transcript responses - DB migration: ip_address column added to chat_sessions - Bump REFRESH_TOKEN_EXPIRE_DAYS from 7 to 30 days (stay logged in 1 month) - Fix auth-client.js: refreshAccessToken only clears session on 401/403, not on network/5xx errors (HF cold-start safe) - Fix restoreSession: keeps session alive when refresh fails transiently, only clears on explicit auth rejection
cb1aefd
Raw
History Blame Contribute Delete
4 kB
"""Author RAG Chatbot SaaS — Chat Session and Chat Message Models."""
from sqlalchemy import Boolean, Float, ForeignKey, Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base, TimestampMixin, generate_uuid
class ChatSession(Base, TimestampMixin):
"""One chat session — corresponds to one visitor interaction."""
__tablename__ = "chat_sessions"
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid)
author_id: Mapped[str] = mapped_column(
String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
)
visitor_fingerprint: Mapped[str] = mapped_column(String(64), nullable=False)
visitor_uid: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True)
ip_address: Mapped[str | None] = mapped_column(String(45), nullable=True) # IPv4 or IPv6 (max 45 chars)
visitor_name: Mapped[str | None] = mapped_column(String(100), nullable=True)
visitor_email: Mapped[str | None] = mapped_column(String(255), nullable=True)
country_code: Mapped[str | None] = mapped_column(String(2), nullable=True)
country_name: Mapped[str | None] = mapped_column(String(100), nullable=True)
city: Mapped[str | None] = mapped_column(String(100), nullable=True)
device_type: Mapped[str | None] = mapped_column(String(20), nullable=True)
browser: Mapped[str | None] = mapped_column(String(100), nullable=True)
os: Mapped[str | None] = mapped_column(String(100), nullable=True)
turn_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
link_clicked: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
blocked: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
rating: Mapped[int | None] = mapped_column(Integer, nullable=True)
summary: Mapped[str | None] = mapped_column(Text, nullable=True)
# Relationships
messages: Mapped[list] = relationship(
"ChatMessage", back_populates="session", cascade="all, delete-orphan"
)
def __repr__(self) -> str:
return f"<ChatSession id={self.id} author={self.author_id}>"
class ChatMessage(Base, TimestampMixin):
"""One message (user or assistant) within a chat session."""
__tablename__ = "chat_messages"
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid)
session_id: Mapped[str] = mapped_column(
String(36), ForeignKey("chat_sessions.id", ondelete="CASCADE"), nullable=False, index=True
)
role: Mapped[str] = mapped_column(String(20), nullable=False) # 'user' | 'assistant'
content: Mapped[str] = mapped_column(Text, nullable=False)
# Assistant-only fields
intent: Mapped[str | None] = mapped_column(String(50), nullable=True)
intent_confidence: Mapped[float | None] = mapped_column(Float, nullable=True)
faithfulness_score: Mapped[float | None] = mapped_column(Float, nullable=True)
hallucination_detected: Mapped[bool | None] = mapped_column(Boolean, nullable=True)
boundary_triggered: Mapped[bool | None] = mapped_column(Boolean, nullable=True)
upsell_strategy: Mapped[str | None] = mapped_column(String(50), nullable=True)
link_shown: Mapped[bool | None] = mapped_column(Boolean, nullable=True)
prompt_tokens: Mapped[int | None] = mapped_column(Integer, nullable=True)
completion_tokens: Mapped[int | None] = mapped_column(Integer, nullable=True)
response_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)
# Conversation viewer fields
annotation: Mapped[str | None] = mapped_column(Text, nullable=True)
flag_type: Mapped[str | None] = mapped_column(String(20), nullable=True) # 'spam'|'quality'|'escalation'
user_feedback: Mapped[str | None] = mapped_column(String(10), nullable=True) # 'up'|'down'
# Relationship
session: Mapped["ChatSession"] = relationship("ChatSession", back_populates="messages")