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