"""Author RAG Chatbot SaaS — Chatbot API Schemas.""" from pydantic import BaseModel, EmailStr, Field class ChatRequest(BaseModel): """Incoming chat message from the widget.""" session_id: str message: str = Field(..., min_length=1, max_length=2000) selected_book_id: str | None = None ask_all_books: bool = False class LinkItem(BaseModel): label: str url: str type: str icon: str class BookSelectorItem(BaseModel): id: str title: str tagline: str | None cover_url: str | None class ChatResponse(BaseModel): """Response from one chat turn.""" text: str links: list[LinkItem] = [] has_links: bool = False book_selector: list[BookSelectorItem] | None = None session_id: str intent: str = "question" follow_ups: list[str] = [] class BookInfo(BaseModel): id: str title: str tagline: str | None cover_path: str | None ai_summary: str | None = None purchase_url: str | None = None class FarewellRequest(BaseModel): session_id: str selected_book_id: str | None = None class SessionInitRequest(BaseModel): """Optional body for session/init — carries the visitor_uid from widget localStorage. The widget sends the visitor_uid it stored on the previous visit. If absent or null, the server generates a new one (first visit). HF Spaces note: Using request body instead of cookies because the widget runs inside an iframe (third-party context) where cookies are blocked by Safari ITP and Firefox ETP. """ visitor_uid: str | None = Field( default=None, min_length=36, max_length=36, description="UUID previously returned by session/init and stored in widget localStorage.", ) visitor_name: str | None = Field( default=None, min_length=1, max_length=100, description="Visitor display name collected before first chat.", ) visitor_email: EmailStr | None = Field( default=None, description="Visitor email collected before first chat.", ) class WidgetAppearanceResponse(BaseModel): """Subscription-authenticated widget chrome (theme/position/auto-open). Used before chat opens so the bubble matches Widget Config without creating a session (profile gate may block session/init). """ bot_name: str widget_theme: str widget_position: str widget_auto_open_delay: int = 0 require_visitor_profile: bool = True class SessionInitResponse(BaseModel): """Response from session initialization.""" session_id: str bot_name: str welcome_message: str widget_theme: str widget_position: str = "bottom-right" widget_auto_open_delay: int = 0 books: list[BookInfo] show_book_selector: bool visitor_uid: str require_visitor_profile: bool = True profile_complete: bool = True visitor_name: str | None = None visitor_email: str | None = None remove_branding: bool = False custom_widget_css: str | None = None # ── Phase 7: Polling Streaming ──────────────────────────────────────────────── class StreamStartResponse(BaseModel): """Returned immediately when a streaming job is queued. Phase 7: Replaces SSE (blocked by nginx). Widget polls /poll/{job_id} until status='done', then renders the full response. """ job_id: str status: str = "queued" class PollResponse(BaseModel): """Returned on each poll of /poll/{job_id}. status: 'pending' | 'done' | 'error' result: only present when status='done' error: only present when status='error' """ job_id: str status: str result: ChatResponse | None = None error: str | None = None # ── Visitor Interaction Schemas (Phase 6B for chat.py) ─────────────────────── class TrackClickRequest(BaseModel): """Track purchase/preview link click.""" session_id: str | None = None book_id: str | None = None link_type: str = "unknown" class FeedbackRequest(BaseModel): """Thumbs up/down on a bot message.""" session_id: str | None = None message_index: int | None = Field(None, ge=0) reaction: str = Field(..., pattern=r"^(up|down)$") class RateSessionRequest(BaseModel): """Star rating for a chat session.""" session_id: str | None = None rating: int = Field(..., ge=1, le=5)