from fastapi import APIRouter, HTTPException, Depends, Query from app.database import get_supabase from app.dependencies import get_current_user from app.config import PLAN_LIMITS from app.models import InboxConversation, InboxMessage, ConversationStatusUpdate, AgentReplyCreate from typing import List, Optional import uuid import logging logger = logging.getLogger(__name__) router = APIRouter(prefix="/inbox", tags=["Inbox"]) def _check_inbox_access(user_id: str, supabase): sub = supabase.table("subscriptions").select("plan").eq("user_id", user_id).eq("status", "active").execute() plan = sub.data[0]["plan"] if sub.data else "free" if plan not in ("starter", "business", "agency", "enterprise"): raise HTTPException(status_code=402, detail="Conversation inbox requires Starter plan or higher") return plan def _get_user_company_id(user_id: str, supabase) -> str: result = supabase.table("companies").select("id").eq("owner_id", user_id).execute() if not result.data: raise HTTPException(status_code=404, detail="Company not found") return result.data[0]["id"] @router.get("/conversations", response_model=List[InboxConversation]) async def list_inbox_conversations( chatbot_id: Optional[str] = Query(None), page: int = Query(1, ge=1), limit: int = Query(20, ge=1, le=100), user=Depends(get_current_user), ): """List conversations for all (or one) of the user's chatbots.""" supabase = get_supabase() _check_inbox_access(user.id, supabase) company_id = _get_user_company_id(user.id, supabase) # Get user's chatbots chatbots_query = supabase.table("chatbots").select("id, name").eq("company_id", company_id) if chatbot_id: chatbots_query = chatbots_query.eq("id", chatbot_id) chatbots_result = chatbots_query.execute() chatbot_map = {c["id"]: c["name"] for c in (chatbots_result.data or [])} if not chatbot_map: return [] chatbot_ids = list(chatbot_map.keys()) offset = (page - 1) * limit # Query conversations convs = supabase.table("conversations").select("*") \ .in_("chatbot_id", chatbot_ids) \ .order("created_at", desc=True) \ .range(offset, offset + limit - 1) \ .execute() results = [] for conv in (convs.data or []): cid = conv["id"] # Get first user message for preview first_msg = supabase.table("messages").select("content") \ .eq("conversation_id", cid) \ .eq("role", "user") \ .order("created_at", desc=False) \ .limit(1) \ .execute() first_message_text = first_msg.data[0]["content"] if first_msg.data else None results.append(InboxConversation( id=cid, chatbot_id=conv["chatbot_id"], chatbot_name=chatbot_map.get(conv["chatbot_id"], "Unknown"), session_id=conv.get("session_id"), language=conv.get("language", "en"), message_count=conv.get("message_count", 0), first_message=first_message_text, status=conv.get("status", "open"), last_agent_reply_at=conv.get("last_agent_reply_at"), created_at=conv.get("created_at"), )) return results @router.get("/conversations/{conversation_id}") async def get_inbox_conversation( conversation_id: str, user=Depends(get_current_user), ): """Get a full conversation thread with all messages.""" supabase = get_supabase() _check_inbox_access(user.id, supabase) company_id = _get_user_company_id(user.id, supabase) # Verify ownership conv = supabase.table("conversations").select("*, chatbots(company_id, name)") \ .eq("id", conversation_id) \ .execute() if not conv.data: raise HTTPException(status_code=404, detail="Conversation not found") c = conv.data[0] chatbot_data = c.get("chatbots") or {} if chatbot_data.get("company_id") != company_id: raise HTTPException(status_code=403, detail="Not authorized") # Get messages msgs = supabase.table("messages").select("*") \ .eq("conversation_id", conversation_id) \ .order("created_at", desc=False) \ .execute() messages = [ InboxMessage( id=m["id"], role=m["role"], content=m["content"], sources=m.get("sources"), confidence_score=m.get("confidence_score"), is_handoff=m.get("is_handoff", False), created_at=m.get("created_at"), ) for m in (msgs.data or []) ] return { "conversation_id": conversation_id, "chatbot_name": chatbot_data.get("name", "Unknown"), "language": c.get("language", "en"), "session_id": c.get("session_id"), "created_at": c.get("created_at"), "messages": [m.model_dump() for m in messages], } @router.patch("/conversations/{conversation_id}/status") async def update_conversation_status( conversation_id: str, data: ConversationStatusUpdate, user=Depends(get_current_user), ): """Update conversation status (open, agent_handling, resolved).""" if data.status not in ("open", "agent_handling", "resolved"): raise HTTPException(status_code=400, detail="Invalid status") supabase = get_supabase() _check_inbox_access(user.id, supabase) company_id = _get_user_company_id(user.id, supabase) conv = supabase.table("conversations").select("*, chatbots(company_id)") \ .eq("id", conversation_id).execute() if not conv.data: raise HTTPException(status_code=404, detail="Conversation not found") if conv.data[0].get("chatbots", {}).get("company_id") != company_id: raise HTTPException(status_code=403, detail="Not authorized") supabase.table("conversations").update({"status": data.status}).eq("id", conversation_id).execute() return {"success": True, "status": data.status} @router.post("/conversations/{conversation_id}/reply") async def agent_reply( conversation_id: str, data: AgentReplyCreate, user=Depends(get_current_user), ): """Send an agent reply to a conversation.""" supabase = get_supabase() _check_inbox_access(user.id, supabase) company_id = _get_user_company_id(user.id, supabase) conv = supabase.table("conversations").select("*, chatbots(company_id)") \ .eq("id", conversation_id).execute() if not conv.data: raise HTTPException(status_code=404, detail="Conversation not found") if conv.data[0].get("chatbots", {}).get("company_id") != company_id: raise HTTPException(status_code=403, detail="Not authorized") msg_id = str(uuid.uuid4()) supabase.table("messages").insert({ "id": msg_id, "conversation_id": conversation_id, "role": "agent", "content": data.message, }).execute() # Mark as agent_handling if not already, and record reply time current_status = conv.data[0].get("status", "open") update_data: dict = {"last_agent_reply_at": "now()"} if current_status == "open": update_data["status"] = "agent_handling" supabase.table("conversations").update(update_data).eq("id", conversation_id).execute() return {"success": True, "message_id": msg_id} @router.delete("/conversations/{conversation_id}") async def delete_inbox_conversation( conversation_id: str, user=Depends(get_current_user), ): """Delete a conversation and all its messages.""" supabase = get_supabase() _check_inbox_access(user.id, supabase) company_id = _get_user_company_id(user.id, supabase) # Verify ownership conv = supabase.table("conversations").select("*, chatbots(company_id)") \ .eq("id", conversation_id).execute() if not conv.data: raise HTTPException(status_code=404, detail="Conversation not found") chatbot_data = conv.data[0].get("chatbots") or {} if chatbot_data.get("company_id") != company_id: raise HTTPException(status_code=403, detail="Not authorized") supabase.table("conversations").delete().eq("id", conversation_id).execute() return {"success": True}