Spaces:
Running
Running
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 | """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") | |