"""Author RAG — Conversation Viewer API. Provides read-only access to conversation history for: - Admin panel: author sees their chatbot's real conversation transcripts - SuperAdmin: cross-author audit trail - Analytics: session-level review Routes: GET /api/admin/{slug}/conversations → paginated list of sessions GET /api/admin/{slug}/conversations/{id} → full message thread for a session """ from datetime import datetime, timezone from typing import Optional from fastapi import APIRouter, Depends, Query, HTTPException from sqlalchemy import select, func, desc from sqlalchemy.ext.asyncio import AsyncSession from app.dependencies import get_current_author, get_db from app.models.chat_session import ChatSession, ChatMessage router = APIRouter() @router.get("/{slug}/conversations") async def list_conversations( slug: str, page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), search: Optional[str] = Query(None, max_length=200), db: AsyncSession = Depends(get_db), author=Depends(get_current_author), ): """List recent chat sessions for this author's chatbot. Paginated. Each item includes session metadata (start time, message count, last user message preview) for the conversation list UI. """ if author.id != slug: raise HTTPException(403, "Forbidden") offset = (page - 1) * page_size # Base query: sessions for this author query = select(ChatSession).where(ChatSession.author_id == slug) # Optional: filter sessions that contain a specific search term # (searches first user message as a proxy — full search in Phase 3) if search: query = query.where(ChatSession.session_id.ilike(f"%{search}%")) # Count total count_result = await db.execute( select(func.count()).select_from(query.subquery()) ) total = count_result.scalar() or 0 # Fetch page result = await db.execute( query.order_by(desc(ChatSession.created_at)) .offset(offset) .limit(page_size) ) sessions = result.scalars().all() # For each session, get the message count and last user message items = [] for s in sessions: msg_result = await db.execute( select(ChatMessage) .where(ChatMessage.session_id == s.session_id) .order_by(ChatMessage.created_at) ) msgs = msg_result.scalars().all() user_msgs = [m for m in msgs if m.role == "user"] items.append({ "session_id": s.session_id, "visitor_id": s.visitor_id if hasattr(s, "visitor_id") else None, "book_id": s.book_id if hasattr(s, "book_id") else None, "started_at": s.created_at.isoformat() if s.created_at else None, "message_count": len(msgs), "first_message": user_msgs[0].content[:120] if user_msgs else None, "last_message": user_msgs[-1].content[:120] if user_msgs else None, }) return { "sessions": items, "total": total, "page": page, "page_size": page_size, "total_pages": max(1, (total + page_size - 1) // page_size), } @router.get("/{slug}/conversations/{session_id}") async def get_conversation( slug: str, session_id: str, db: AsyncSession = Depends(get_db), author=Depends(get_current_author), ): """Get full message thread for a specific chat session. Returns the complete conversation as an ordered list of messages for the conversation viewer UI in the admin panel. """ if author.id != slug: raise HTTPException(403, "Forbidden") # Verify session belongs to this author session_result = await db.execute( select(ChatSession).where( ChatSession.session_id == session_id, ChatSession.author_id == slug, ) ) session = session_result.scalar_one_or_none() if not session: raise HTTPException(404, "Session not found") # Get messages msg_result = await db.execute( select(ChatMessage) .where(ChatMessage.session_id == session_id) .order_by(ChatMessage.created_at) ) messages = msg_result.scalars().all() return { "session_id": session_id, "author_id": slug, "book_id": session.book_id if hasattr(session, "book_id") else None, "started_at": session.created_at.isoformat() if session.created_at else None, "messages": [ { "id": m.id, "role": m.role, "content": m.content, "tokens_used": m.tokens_used if hasattr(m, "tokens_used") else None, "intent": m.intent if hasattr(m, "intent") else None, "created_at": m.created_at.isoformat() if m.created_at else None, } for m in messages ], }