from fastapi import APIRouter, HTTPException, Depends from app.models import ChatMessage, ChatResponse, ConversationResponse, MessageResponse from app.database import get_supabase from app.dependencies import get_current_user, get_optional_user from app.services.rag import rag_engine from typing import List, Optional import uuid import logging logger = logging.getLogger(__name__) router = APIRouter(tags=["Chat"]) def _get_public_chatbot(chatbot_id: str, supabase) -> dict: """Get a published chatbot (or any chatbot for preview)""" result = supabase.table("chatbots").select("*, companies(name, logo_url)").eq("id", chatbot_id).execute() if not result.data: raise HTTPException(status_code=404, detail="Chatbot not found") return result.data[0] @router.post("/chat/{chatbot_id}", response_model=ChatResponse) async def chat( chatbot_id: str, message: ChatMessage, user=Depends(get_optional_user), ): supabase = get_supabase() chatbot = _get_public_chatbot(chatbot_id, supabase) # Allow preview access for owner, require published for public if not chatbot.get("is_published"): if not user: raise HTTPException(status_code=403, detail="This chatbot is in preview mode") # Check ownership company = supabase.table("companies").select("id").eq("owner_id", user.id).execute() if not company.data or company.data[0]["id"] != chatbot.get("company_id"): raise HTTPException(status_code=403, detail="This chatbot is in preview mode") collection_name = chatbot.get("qdrant_collection_name") if not collection_name: raise HTTPException(status_code=400, detail="Chatbot has no knowledge base configured") # Get or create conversation session_id = message.session_id or str(uuid.uuid4()) conversation = _get_or_create_conversation( chatbot_id=chatbot_id, session_id=session_id, user_id=user.id if user else None, language=message.language, supabase=supabase, ) # Get conversation history history = _get_conversation_history(conversation["id"], supabase) # Get company info for context company_data = chatbot.get("companies", {}) or {} chatbot_config = { **chatbot, "company_name": company_data.get("name", ""), } # Run RAG result = await rag_engine.process_query( query=message.message, collection_name=collection_name, chatbot_config=chatbot_config, conversation_history=history, language=message.language, ) # Save messages _save_message(conversation["id"], "user", message.message, supabase) _save_message( conversation["id"], "assistant", result["response"], supabase, sources=[s.model_dump() for s in result.get("sources", [])], model=result.get("model", ""), ) # Update conversation message count supabase.table("conversations").update({ "message_count": len(history) + 2 }).eq("id", conversation["id"]).execute() return ChatResponse( response=result["response"], session_id=session_id, sources=result.get("sources", []), model_used=result.get("model", ""), tokens_used=result.get("tokens_used", 0), ) @router.get("/chat/{chatbot_id}/history/{session_id}", response_model=List[MessageResponse]) async def get_chat_history( chatbot_id: str, session_id: str, user=Depends(get_optional_user), ): supabase = get_supabase() conversation = supabase.table("conversations").select("*") \ .eq("chatbot_id", chatbot_id) \ .eq("session_id", session_id) \ .execute() if not conversation.data: return [] conv_id = conversation.data[0]["id"] messages = supabase.table("messages").select("*") \ .eq("conversation_id", conv_id) \ .order("created_at", desc=False) \ .execute() return [ MessageResponse( id=m["id"], role=m["role"], content=m["content"], sources=m.get("sources"), created_at=m.get("created_at"), ) for m in (messages.data or []) ] # ── Analytics endpoint ──────────────────────────────────────────────────────── @router.get("/analytics/{chatbot_id}") async def get_analytics(chatbot_id: str, user=Depends(get_current_user)): supabase = get_supabase() # Verify ownership company = supabase.table("companies").select("id").eq("owner_id", user.id).execute() if not company.data: raise HTTPException(status_code=404, detail="Company not found") chatbot = supabase.table("chatbots").select("id").eq("id", chatbot_id).eq("company_id", company.data[0]["id"]).execute() if not chatbot.data: raise HTTPException(status_code=404, detail="Chatbot not found") total_convs = supabase.table("conversations").select("id", count="exact").eq("chatbot_id", chatbot_id).execute() total_msgs = supabase.table("messages").select("id", count="exact").execute() return { "chatbot_id": chatbot_id, "total_conversations": total_convs.count or 0, "total_messages": total_msgs.count or 0, "average_rating": 0.0, "conversations_last_30_days": total_convs.count or 0, } # ── Helpers ─────────────────────────────────────────────────────────────────── def _get_or_create_conversation( chatbot_id: str, session_id: str, user_id: Optional[str], language: str, supabase, ) -> dict: existing = supabase.table("conversations").select("*") \ .eq("chatbot_id", chatbot_id) \ .eq("session_id", session_id) \ .execute() if existing.data: return existing.data[0] new_conv = { "id": str(uuid.uuid4()), "chatbot_id": chatbot_id, "user_id": user_id, "session_id": session_id, "language": language, "message_count": 0, } result = supabase.table("conversations").insert(new_conv).execute() return result.data[0] def _get_conversation_history(conversation_id: str, supabase) -> List[dict]: """ FIX: Changed from desc=True to desc=False (ascending/chronological order). The conversation history MUST be in chronological order (oldest first) for the LLM to correctly understand the conversation flow. Previously, messages were returned newest-first, which reversed the conversation and confused the model. """ messages = supabase.table("messages").select("role, content") \ .eq("conversation_id", conversation_id) \ .order("created_at", desc=False) \ .limit(20) \ .execute() return messages.data or [] def _save_message( conversation_id: str, role: str, content: str, supabase, sources: Optional[list] = None, model: str = "", ): supabase.table("messages").insert({ "id": str(uuid.uuid4()), "conversation_id": conversation_id, "role": role, "content": content, "sources": sources, "model": model, }).execute()