AI Development Team
feat: Complete RAG chatbot with ChatKit SDK and Better-Auth integration
f051da6
import os
import sys
import time
from uuid import uuid4
from fastapi import APIRouter, Depends, HTTPException, Header, Request
from fastapi.responses import JSONResponse
import logging
from qdrant_client import QdrantClient
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from services.rag_service import RAGService
from auth.jwt_utils import get_current_user_id_from_token
router = APIRouter()
# Configure OpenRouter and RAG service
openrouter_api_key = os.getenv("OPENAI_API_KEY")
qdrant_url = os.getenv("QDRANT_URL")
qdrant_api_key = os.getenv("QDRANT_API_KEY")
collection_name = os.getenv("QDRANT_COLLECTION", "project_documents")
if openrouter_api_key and openrouter_api_key != "your_openrouter_api_key_here":
# Initialize Qdrant client
try:
# Check if cloud Qdrant is configured
qdrant_host = os.getenv("QDRANT_HOST")
if qdrant_api_key and qdrant_host and "qdrant.io" in qdrant_host:
# Use Qdrant Cloud
qdrant_client = QdrantClient(
url=qdrant_host, # Use QDRANT_HOST which has the full URL
api_key=qdrant_api_key,
prefer_grpc=False
)
else:
# Use local Qdrant - extract host from URL if needed
if qdrant_url and qdrant_url.startswith("http"):
# Extract host and port from URL
from urllib.parse import urlparse
parsed = urlparse(qdrant_url)
host = parsed.hostname or "localhost"
port = parsed.port or 6333
else:
host = qdrant_url or "localhost"
port = int(os.getenv("QDRANT_PORT", 6333))
qdrant_client = QdrantClient(host=host, port=port)
except Exception as e:
logger.error(f"Failed to initialize Qdrant client: {str(e)}")
# Create a dummy client to allow service to work without Qdrant
qdrant_client = None
# Initialize RAG service with OpenRouter
rag_service = RAGService(openrouter_api_key, qdrant_client, collection_name)
else:
rag_service = None
logger = logging.getLogger(__name__)
# JWT verification dependency
def verify_jwt_token(authorization: str = Header(None)) -> dict:
"""Verify JWT token from Authorization header"""
if not authorization:
raise HTTPException(status_code=401, detail="Authorization header missing")
if not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Invalid authorization format")
token = authorization[7:] # Remove "Bearer " prefix
user_id = get_current_user_id_from_token(token)
if not user_id:
raise HTTPException(status_code=401, detail="Invalid or expired token")
return {"user_id": user_id}
@router.post("/chat")
async def chat(payload: dict, user_data: dict = Depends(verify_jwt_token)):
user_msg = payload["message"]
selected_text = payload.get("selected_text", "")
# If selected text is provided, try to use RAG service to answer based only on that text
if selected_text and rag_service:
try:
# Use the RAG service to answer based on selected text only (with OpenRouter)
answer = rag_service.query_rag(selected_text, user_msg)
return {"answer": answer}
except Exception as e:
logger.error(f"RAG service failed: {str(e)}")
# Fall through to fallback response below
elif selected_text and not rag_service:
logger.warning("RAG service not available, using fallback")
# Fallback response when API is unavailable or not configured
fallback_responses = {
"hello": "Hello! I'm your AI textbook assistant. Feel free to ask questions about the content you're studying!",
"hi": "Hi there! I'm here to help you understand the AI and robotics concepts in your textbook. What would you like to know?",
"help": "I can help explain concepts from your AI and robotics textbook! Please select some text and ask questions about it.",
"default": f"I'm currently unable to process your request about '{user_msg}'. This might be because the AI service is temporarily unavailable or needs to be configured with a valid API key. The system is working properly but requires a valid OPENROUTER_API_KEY to provide AI-generated responses."
}
response_text = fallback_responses.get(user_msg.lower().strip(), fallback_responses["default"])
result = {"answer": response_text}
if not rag_service:
result["setup_needed"] = "Please configure a valid OPENROUTER_API_KEY in the .env file to enable AI responses"
return result
# ============================================================
# OpenAI ChatKit SDK Compatible Endpoint
# Provides OpenAI Chat Completions API format for ChatKit React
# ============================================================
@router.post("/chatkit/session")
async def chatkit_session(request: Request, user_data: dict = Depends(verify_jwt_token)):
"""
Create a ChatKit session. Returns a client_secret for the ChatKit React component.
This is a simplified session endpoint for self-hosted ChatKit.
"""
session_id = str(uuid4())
return {
"client_secret": f"chatkit_{session_id}_{user_data['user_id']}",
"session_id": session_id,
"user_id": user_data["user_id"],
}
@router.post("/chatkit/chat")
async def chatkit_chat(payload: dict, user_data: dict = Depends(verify_jwt_token)):
"""
ChatKit-compatible chat endpoint.
Accepts OpenAI Chat Completions format and routes through RAG service.
Used by the @openai/chatkit-react frontend component.
"""
messages = payload.get("messages", [])
selected_text = payload.get("selected_text", "")
# Extract the last user message
user_msg = ""
for msg in reversed(messages):
if msg.get("role") == "user":
user_msg = msg.get("content", "")
break
if not user_msg:
user_msg = payload.get("message", "")
# Process through RAG service
answer = ""
if selected_text and rag_service:
try:
answer = rag_service.query_rag(selected_text, user_msg)
except Exception as e:
logger.error(f"ChatKit RAG service failed: {str(e)}")
answer = "Is sawal ka jawab provided data me mojood nahi hai."
elif rag_service:
try:
answer = rag_service.query_rag(user_msg, user_msg)
except Exception as e:
logger.error(f"ChatKit RAG service failed: {str(e)}")
answer = "Please select text from the textbook to ask questions."
else:
answer = "AI service is not configured. Please set up OPENROUTER_API_KEY."
# Return in OpenAI Chat Completions format (ChatKit compatible)
return {
"id": f"chatcmpl-{uuid4().hex[:12]}",
"object": "chat.completion",
"created": int(time.time()),
"model": "openai/gpt-3.5-turbo",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": answer,
},
"finish_reason": "stop",
}
],
"usage": {
"prompt_tokens": len(user_msg.split()),
"completion_tokens": len(answer.split()),
"total_tokens": len(user_msg.split()) + len(answer.split()),
},
}