# """ # Chat API Endpoints (WITH AUTHENTICATION) # RESTful API for the Banking RAG Chatbot # NOW REQUIRES JWT TOKEN FOR ALL ENDPOINTS! # Endpoints: # - POST /chat - Send a message and get response (PROTECTED) # - GET /chat/history/{conversation_id} - Get conversation history (PROTECTED) # - POST /chat/conversation - Create new conversation (PROTECTED) # - GET /chat/conversations - List user's conversations (PROTECTED) # - DELETE /chat/conversation/{conversation_id} - Delete conversation (PROTECTED) # - GET /chat/health - Health check (PUBLIC) # """ # from fastapi import APIRouter, HTTPException, status, Depends # from pydantic import BaseModel, Field # from typing import List, Dict, Optional # from datetime import datetime # from app.services.chat_service import chat_service # from app.db.repositories.conversation_repository import ConversationRepository # from app.utils.dependencies import get_current_user # AUTH DEPENDENCY # from app.models.user import TokenData # USER DATA FROM TOKEN # # ============================================================================ # # CREATE ROUTER # # ============================================================================ # router = APIRouter() # # ============================================================================ # # DEPENDENCY: Get ConversationRepository instance # # ============================================================================ # def get_conversation_repo() -> ConversationRepository: # """ # Dependency that provides ConversationRepository instance. # This ensures MongoDB is connected before repository is used. # """ # return ConversationRepository() # # ============================================================================ # # PYDANTIC MODELS (Request/Response schemas) # # ============================================================================ # class ChatRequest(BaseModel): # """Request model for chat endpoint""" # query: str = Field(..., description="User query text", min_length=1, max_length=1000) # conversation_id: Optional[str] = Field(None, description="Optional conversation ID") # class Config: # json_schema_extra = { # "example": { # "query": "What is my account balance?", # "conversation_id": "conv-123" # } # } # class ChatResponse(BaseModel): # """Response model for chat endpoint""" # response: str = Field(..., description="Generated response text") # conversation_id: str = Field(..., description="Conversation ID") # policy_action: str = Field(..., description="Policy decision: FETCH or NO_FETCH") # policy_confidence: float = Field(..., description="Policy confidence score (0-1)") # documents_retrieved: int = Field(..., description="Number of documents retrieved") # top_doc_score: Optional[float] = Field(None, description="Best document similarity score") # total_time_ms: float = Field(..., description="Total processing time in milliseconds") # timestamp: str = Field(..., description="Response timestamp (ISO format)") # class ConversationCreateResponse(BaseModel): # """Response after creating a conversation""" # conversation_id: str = Field(..., description="Created conversation ID") # created_at: str = Field(..., description="Creation timestamp") # class MessageModel(BaseModel): # """Single message in conversation history""" # role: str = Field(..., description="Message role: user or assistant") # content: str = Field(..., description="Message content") # timestamp: str = Field(..., description="Message timestamp") # metadata: Optional[Dict] = Field(None, description="Optional metadata") # class ConversationHistoryResponse(BaseModel): # """Response containing conversation history""" # conversation_id: str # messages: List[MessageModel] # message_count: int # # ============================================================================ # # ENDPOINTS (ALL PROTECTED WITH JWT) # # ============================================================================ # @router.post("/", response_model=ChatResponse, status_code=status.HTTP_200_OK) # async def chat( # request: ChatRequest, # current_user: TokenData = Depends(get_current_user), # repo: ConversationRepository = Depends(get_conversation_repo) # ← INJECT REPO # ): # """ # Main chat endpoint - Send a query and get a response. # **REQUIRES AUTHENTICATION** - JWT token must be provided in Authorization header. # """ # try: # # Get user_id from token # user_id = current_user.user_id # # If no conversation_id provided, create a new conversation # conversation_id = request.conversation_id # if not conversation_id: # conversation_id = await repo.create_conversation(user_id=user_id) # else: # # Verify user owns this conversation # conversation = await repo.get_conversation(conversation_id) # if not conversation: # raise HTTPException( # status_code=status.HTTP_404_NOT_FOUND, # detail="Conversation not found" # ) # if conversation["user_id"] != user_id: # raise HTTPException( # status_code=status.HTTP_403_FORBIDDEN, # detail="Access denied - you don't own this conversation" # ) # # Get conversation history # history = await repo.get_conversation_history( # conversation_id=conversation_id, # max_messages=10 # ) # # Save user message # await repo.add_message( # conversation_id=conversation_id, # message={ # 'role': 'user', # 'content': request.query, # 'timestamp': datetime.now() # } # ) # # Process query through RAG pipeline # result = await chat_service.process_query( # query=request.query, # conversation_history=history, # user_id=user_id # ) # # Save assistant message # await repo.add_message( # conversation_id=conversation_id, # message={ # 'role': 'assistant', # 'content': result['response'], # 'timestamp': datetime.now(), # 'metadata': { # 'policy_action': result['policy_action'], # 'policy_confidence': result['policy_confidence'], # 'documents_retrieved': result['documents_retrieved'], # 'top_doc_score': result['top_doc_score'] # } # } # ) # # Log retrieval data for RL training # await repo.log_retrieval({ # 'conversation_id': conversation_id, # 'user_id': user_id, # 'query': request.query, # 'policy_action': result['policy_action'], # 'policy_confidence': result['policy_confidence'], # 'should_retrieve': result['should_retrieve'], # 'documents_retrieved': result['documents_retrieved'], # 'top_doc_score': result['top_doc_score'], # 'response': result['response'], # 'retrieval_time_ms': result['retrieval_time_ms'], # 'generation_time_ms': result['generation_time_ms'], # 'total_time_ms': result['total_time_ms'], # 'retrieved_docs_metadata': result.get('retrieved_docs_metadata', []), # 'timestamp': datetime.now() # }) # # Return response # return ChatResponse( # response=result['response'], # conversation_id=conversation_id, # policy_action=result['policy_action'], # policy_confidence=result['policy_confidence'], # documents_retrieved=result['documents_retrieved'], # top_doc_score=result['top_doc_score'], # total_time_ms=result['total_time_ms'], # timestamp=result['timestamp'] # ) # except HTTPException: # raise # except Exception as e: # print(f"❌ Chat endpoint error: {e}") # import traceback # traceback.print_exc() # raise HTTPException( # status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, # detail=f"Failed to process chat request: {str(e)}" # ) # @router.post("/conversation", response_model=ConversationCreateResponse, status_code=status.HTTP_201_CREATED) # async def create_conversation( # current_user: TokenData = Depends(get_current_user), # repo: ConversationRepository = Depends(get_conversation_repo) # ): # """Create a new conversation""" # try: # conversation_id = await repo.create_conversation(user_id=current_user.user_id) # return ConversationCreateResponse( # conversation_id=conversation_id, # created_at=datetime.now().isoformat() # ) # except Exception as e: # raise HTTPException( # status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, # detail=f"Failed to create conversation: {str(e)}" # ) # @router.get("/history/{conversation_id}", response_model=ConversationHistoryResponse) # async def get_conversation_history( # conversation_id: str, # current_user: TokenData = Depends(get_current_user), # repo: ConversationRepository = Depends(get_conversation_repo) # ): # """Get conversation history by ID""" # try: # conversation = await repo.get_conversation(conversation_id) # if not conversation: # raise HTTPException( # status_code=status.HTTP_404_NOT_FOUND, # detail=f"Conversation {conversation_id} not found" # ) # if conversation["user_id"] != current_user.user_id: # raise HTTPException( # status_code=status.HTTP_403_FORBIDDEN, # detail="Access denied - you don't own this conversation" # ) # messages = [] # for msg in conversation.get('messages', []): # messages.append(MessageModel( # role=msg['role'], # content=msg['content'], # timestamp=msg['timestamp'].isoformat() if isinstance(msg['timestamp'], datetime) else msg['timestamp'], # metadata=msg.get('metadata') # )) # return ConversationHistoryResponse( # conversation_id=conversation_id, # messages=messages, # message_count=len(messages) # ) # except HTTPException: # raise # except Exception as e: # raise HTTPException( # status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, # detail=f"Failed to fetch conversation history: {str(e)}" # ) # @router.get("/conversations") # async def list_user_conversations( # limit: int = 10, # skip: int = 0, # current_user: TokenData = Depends(get_current_user), # repo: ConversationRepository = Depends(get_conversation_repo) # ): # """List all conversations for the authenticated user""" # try: # conversations = await repo.get_user_conversations( # user_id=current_user.user_id, # limit=limit, # skip=skip # ) # return { # "user_id": current_user.user_id, # "user_email": current_user.email, # "conversations": [ # { # "conversation_id": conv['conversation_id'], # "created_at": conv['created_at'].isoformat() if isinstance(conv['created_at'], datetime) else conv['created_at'], # "updated_at": conv['updated_at'].isoformat() if isinstance(conv['updated_at'], datetime) else conv['updated_at'], # "message_count": len(conv.get('messages', [])) # } # for conv in conversations # ], # "total": len(conversations) # } # except Exception as e: # raise HTTPException( # status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, # detail=f"Failed to fetch conversations: {str(e)}" # ) # @router.delete("/conversation/{conversation_id}") # async def delete_conversation( # conversation_id: str, # current_user: TokenData = Depends(get_current_user), # repo: ConversationRepository = Depends(get_conversation_repo) # ): # """Delete a conversation""" # try: # conversation = await repo.get_conversation(conversation_id) # if not conversation: # raise HTTPException( # status_code=status.HTTP_404_NOT_FOUND, # detail=f"Conversation {conversation_id} not found" # ) # if conversation["user_id"] != current_user.user_id: # raise HTTPException( # status_code=status.HTTP_403_FORBIDDEN, # detail="Access denied - you don't own this conversation" # ) # success = await repo.delete_conversation(conversation_id) # if success: # return { # "message": "Conversation deleted successfully", # "conversation_id": conversation_id # } # else: # raise HTTPException( # status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, # detail="Failed to delete conversation" # ) # except HTTPException: # raise # except Exception as e: # raise HTTPException( # status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, # detail=f"Failed to delete conversation: {str(e)}" # ) # @router.get("/health") # async def chat_health(): # """Health check for chat service (PUBLIC)""" # try: # health = await chat_service.health_check() # return { # "status": "healthy", # "service": "chat", # "components": health['components'], # "timestamp": datetime.now().isoformat() # } # except Exception as e: # return { # "status": "unhealthy", # "service": "chat", # "error": str(e), # "timestamp": datetime.now().isoformat() # } # # ============================================================================ # # """ # # Chat API Endpoints (WITH AUTHENTICATION) # # RESTful API for the Banking RAG Chatbot # # NOW REQUIRES JWT TOKEN FOR ALL ENDPOINTS! # # Endpoints: # # - POST /chat - Send a message and get response (PROTECTED) # # - GET /chat/history/{conversation_id} - Get conversation history (PROTECTED) # # - POST /chat/conversation - Create new conversation (PROTECTED) # # - GET /chat/conversations - List user's conversations (PROTECTED) # # - DELETE /chat/conversation/{conversation_id} - Delete conversation (PROTECTED) # # - GET /chat/health - Health check (PUBLIC) # # """ # # from fastapi import APIRouter, HTTPException, status, Depends # # from pydantic import BaseModel, Field # # from typing import List, Dict, Optional # # from datetime import datetime # # from app.services.chat_service import chat_service # # from app.db.repositories.conversation_repository import ConversationRepository # # from app.utils.dependencies import get_current_user # AUTH DEPENDENCY # # from app.models.user import TokenData # USER DATA FROM TOKEN # # # ============================================================================ # # # CREATE ROUTER # # # ============================================================================ # # router = APIRouter() # # # Initialize repository # # conversation_repo = ConversationRepository() # # # ============================================================================ # # # PYDANTIC MODELS (Request/Response schemas) # # # ============================================================================ # # class ChatRequest(BaseModel): # # """ # # Request model for chat endpoint. # # NOTE: user_id is now extracted from JWT token, not from request body! # # Example: # # { # # "query": "What is my account balance?", # # "conversation_id": "abc-123" # # } # # """ # # query: str = Field(..., description="User query text", min_length=1, max_length=1000) # # conversation_id: Optional[str] = Field(None, description="Optional conversation ID") # # class Config: # # json_schema_extra = { # # "example": { # # "query": "What is my account balance?", # # "conversation_id": "conv-123" # # } # # } # # class ChatResponse(BaseModel): # # """ # # Response model for chat endpoint. # # Contains the generated response plus metadata about the RAG pipeline. # # """ # # response: str = Field(..., description="Generated response text") # # conversation_id: str = Field(..., description="Conversation ID") # # policy_action: str = Field(..., description="Policy decision: FETCH or NO_FETCH") # # policy_confidence: float = Field(..., description="Policy confidence score (0-1)") # # documents_retrieved: int = Field(..., description="Number of documents retrieved") # # top_doc_score: Optional[float] = Field(None, description="Best document similarity score") # # total_time_ms: float = Field(..., description="Total processing time in milliseconds") # # timestamp: str = Field(..., description="Response timestamp (ISO format)") # # class ConversationCreateRequest(BaseModel): # # """Request to create a new conversation (no user_id needed - from token)""" # # pass # Empty - user_id comes from JWT token # # class ConversationCreateResponse(BaseModel): # # """Response after creating a conversation""" # # conversation_id: str = Field(..., description="Created conversation ID") # # created_at: str = Field(..., description="Creation timestamp") # # class MessageModel(BaseModel): # # """Single message in conversation history""" # # role: str = Field(..., description="Message role: user or assistant") # # content: str = Field(..., description="Message content") # # timestamp: str = Field(..., description="Message timestamp") # # metadata: Optional[Dict] = Field(None, description="Optional metadata") # # class ConversationHistoryResponse(BaseModel): # # """Response containing conversation history""" # # conversation_id: str # # messages: List[MessageModel] # # message_count: int # # # ============================================================================ # # # ENDPOINTS (ALL PROTECTED WITH JWT) # # # ============================================================================ # # @router.post("/", response_model=ChatResponse, status_code=status.HTTP_200_OK) # # async def chat( # # request: ChatRequest, # # current_user: TokenData = Depends(get_current_user) # ← REQUIRES AUTH! # # ): # # """ # # Main chat endpoint - Send a query and get a response. # # **REQUIRES AUTHENTICATION** - JWT token must be provided in Authorization header. # # This endpoint: # # 1. Extracts user_id from JWT token # # 2. Processes the query through the RAG pipeline # # 3. Saves messages to MongoDB # # 4. Logs retrieval data for RL training # # 5. Returns response with metadata # # Args: # # request: ChatRequest with query and optional conversation_id # # current_user: Authenticated user data from JWT token # # Returns: # # ChatResponse: Generated response with metadata # # Raises: # # HTTPException: If processing fails or user not authenticated # # """ # # try: # # # Get user_id from token (NOT from request body!) # # user_id = current_user.user_id # # # If no conversation_id provided, create a new conversation # # conversation_id = request.conversation_id # # if not conversation_id: # # conversation_id = await conversation_repo.create_conversation( # # user_id=user_id # # ) # # else: # # # Verify user owns this conversation # # conversation = await conversation_repo.get_conversation(conversation_id) # # if not conversation: # # raise HTTPException( # # status_code=status.HTTP_404_NOT_FOUND, # # detail="Conversation not found" # # ) # # if conversation["user_id"] != user_id: # # raise HTTPException( # # status_code=status.HTTP_403_FORBIDDEN, # # detail="Access denied - you don't own this conversation" # # ) # # # Get conversation history # # history = await conversation_repo.get_conversation_history( # # conversation_id=conversation_id, # # max_messages=10 # Last 5 turns (10 messages) # # ) # # # Save user message to database # # await conversation_repo.add_message( # # conversation_id=conversation_id, # # message={ # # 'role': 'user', # # 'content': request.query, # # 'timestamp': datetime.now() # # } # # ) # # # Process query through RAG pipeline # # result = await chat_service.process_query( # # query=request.query, # # conversation_history=history, # # user_id=user_id # # ) # # # Save assistant message to database # # await conversation_repo.add_message( # # conversation_id=conversation_id, # # message={ # # 'role': 'assistant', # # 'content': result['response'], # # 'timestamp': datetime.now(), # # 'metadata': { # # 'policy_action': result['policy_action'], # # 'policy_confidence': result['policy_confidence'], # # 'documents_retrieved': result['documents_retrieved'], # # 'top_doc_score': result['top_doc_score'] # # } # # } # # ) # # # Log retrieval data for RL training # # await conversation_repo.log_retrieval({ # # 'conversation_id': conversation_id, # # 'user_id': user_id, # # 'query': request.query, # # 'policy_action': result['policy_action'], # # 'policy_confidence': result['policy_confidence'], # # 'should_retrieve': result['should_retrieve'], # # 'documents_retrieved': result['documents_retrieved'], # # 'top_doc_score': result['top_doc_score'], # # 'response': result['response'], # # 'retrieval_time_ms': result['retrieval_time_ms'], # # 'generation_time_ms': result['generation_time_ms'], # # 'total_time_ms': result['total_time_ms'], # # 'retrieved_docs_metadata': result.get('retrieved_docs_metadata', []), # # 'timestamp': datetime.now() # # }) # # # Return response # # return ChatResponse( # # response=result['response'], # # conversation_id=conversation_id, # # policy_action=result['policy_action'], # # policy_confidence=result['policy_confidence'], # # documents_retrieved=result['documents_retrieved'], # # top_doc_score=result['top_doc_score'], # # total_time_ms=result['total_time_ms'], # # timestamp=result['timestamp'] # # ) # # except HTTPException: # # raise # Re-raise HTTP exceptions # # except Exception as e: # # print(f"❌ Chat endpoint error: {e}") # # raise HTTPException( # # status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, # # detail=f"Failed to process chat request: {str(e)}" # # ) # # @router.post("/conversation", response_model=ConversationCreateResponse, status_code=status.HTTP_201_CREATED) # # async def create_conversation( # # current_user: TokenData = Depends(get_current_user) # ← REQUIRES AUTH! # # ): # # """ # # Create a new conversation. # # **REQUIRES AUTHENTICATION** - User ID is extracted from JWT token. # # Args: # # current_user: Authenticated user data from JWT token # # Returns: # # ConversationCreateResponse: Created conversation ID # # """ # # try: # # conversation_id = await conversation_repo.create_conversation( # # user_id=current_user.user_id # # ) # # return ConversationCreateResponse( # # conversation_id=conversation_id, # # created_at=datetime.now().isoformat() # # ) # # except Exception as e: # # raise HTTPException( # # status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, # # detail=f"Failed to create conversation: {str(e)}" # # ) # # @router.get("/history/{conversation_id}", response_model=ConversationHistoryResponse) # # async def get_conversation_history( # # conversation_id: str, # # current_user: TokenData = Depends(get_current_user) # ← REQUIRES AUTH! # # ): # # """ # # Get conversation history by ID. # # **REQUIRES AUTHENTICATION** - User can only access their own conversations. # # Args: # # conversation_id: Conversation ID # # current_user: Authenticated user data from JWT token # # Returns: # # ConversationHistoryResponse: List of messages # # Raises: # # HTTPException: If conversation not found or user doesn't own it # # """ # # try: # # # Get conversation # # conversation = await conversation_repo.get_conversation(conversation_id) # # if not conversation: # # raise HTTPException( # # status_code=status.HTTP_404_NOT_FOUND, # # detail=f"Conversation {conversation_id} not found" # # ) # # # Verify user owns this conversation # # if conversation["user_id"] != current_user.user_id: # # raise HTTPException( # # status_code=status.HTTP_403_FORBIDDEN, # # detail="Access denied - you don't own this conversation" # # ) # # # Format messages # # messages = [] # # for msg in conversation.get('messages', []): # # messages.append(MessageModel( # # role=msg['role'], # # content=msg['content'], # # timestamp=msg['timestamp'].isoformat() if isinstance(msg['timestamp'], datetime) else msg['timestamp'], # # metadata=msg.get('metadata') # # )) # # return ConversationHistoryResponse( # # conversation_id=conversation_id, # # messages=messages, # # message_count=len(messages) # # ) # # except HTTPException: # # raise # # except Exception as e: # # raise HTTPException( # # status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, # # detail=f"Failed to fetch conversation history: {str(e)}" # # ) # # @router.get("/conversations") # # async def list_user_conversations( # # limit: int = 10, # # skip: int = 0, # # current_user: TokenData = Depends(get_current_user) # ← REQUIRES AUTH! # # ): # # """ # # List all conversations for the authenticated user. # # **REQUIRES AUTHENTICATION** - User ID is extracted from JWT token. # # Args: # # limit: Maximum conversations to return (default: 10) # # skip: Number to skip for pagination (default: 0) # # current_user: Authenticated user data from JWT token # # Returns: # # dict: List of conversations for current user # # """ # # try: # # conversations = await conversation_repo.get_user_conversations( # # user_id=current_user.user_id, # From JWT token! # # limit=limit, # # skip=skip # # ) # # # Format response # # return { # # "user_id": current_user.user_id, # # "user_email": current_user.email, # # "conversations": [ # # { # # "conversation_id": conv['conversation_id'], # # "created_at": conv['created_at'].isoformat() if isinstance(conv['created_at'], datetime) else conv['created_at'], # # "updated_at": conv['updated_at'].isoformat() if isinstance(conv['updated_at'], datetime) else conv['updated_at'], # # "message_count": len(conv.get('messages', [])) # # } # # for conv in conversations # # ], # # "total": len(conversations) # # } # # except Exception as e: # # raise HTTPException( # # status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, # # detail=f"Failed to fetch conversations: {str(e)}" # # ) # # @router.delete("/conversation/{conversation_id}") # # async def delete_conversation( # # conversation_id: str, # # current_user: TokenData = Depends(get_current_user) # ← REQUIRES AUTH! # # ): # # """ # # Delete a conversation. # # **REQUIRES AUTHENTICATION** - User can only delete their own conversations. # # Args: # # conversation_id: Conversation ID to delete # # current_user: Authenticated user data from JWT token # # Returns: # # dict: Success message # # Raises: # # HTTPException: If conversation not found or user doesn't own it # # """ # # try: # # # Get conversation # # conversation = await conversation_repo.get_conversation(conversation_id) # # if not conversation: # # raise HTTPException( # # status_code=status.HTTP_404_NOT_FOUND, # # detail=f"Conversation {conversation_id} not found" # # ) # # # Verify user owns this conversation # # if conversation["user_id"] != current_user.user_id: # # raise HTTPException( # # status_code=status.HTTP_403_FORBIDDEN, # # detail="Access denied - you don't own this conversation" # # ) # # # Delete conversation # # success = await conversation_repo.delete_conversation(conversation_id) # # if success: # # return { # # "message": "Conversation deleted successfully", # # "conversation_id": conversation_id # # } # # else: # # raise HTTPException( # # status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, # # detail="Failed to delete conversation" # # ) # # except HTTPException: # # raise # # except Exception as e: # # raise HTTPException( # # status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, # # detail=f"Failed to delete conversation: {str(e)}" # # ) # # @router.get("/health") # # async def chat_health(): # # """ # # Health check for chat service. # # **PUBLIC ENDPOINT** - No authentication required. # # Returns: # # dict: Health status of chat service components # # """ # # try: # # health = await chat_service.health_check() # # return { # # "status": "healthy", # # "service": "chat", # # "components": health['components'], # # "timestamp": datetime.now().isoformat() # # } # # except Exception as e: # # return { # # "status": "unhealthy", # # "service": "chat", # # "error": str(e), # # "timestamp": datetime.now().isoformat() # # } # # # ============================================================================ # # # USAGE DOCUMENTATION # # # ============================================================================ # # """ # # === API USAGE EXAMPLES (WITH AUTHENTICATION) === # # ALL ENDPOINTS (except /health) NOW REQUIRE JWT TOKEN IN AUTHORIZATION HEADER! # # 1. Register user: # # POST /api/v1/auth/register # # Body: { # # "email": "user@example.com", # # "password": "SecurePass123", # # "full_name": "John Doe" # # } # # Response: { "access_token": "eyJ...", "user": {...} } # # 2. Login: # # POST /api/v1/auth/login # # Body: { # # "email": "user@example.com", # # "password": "SecurePass123" # # } # # Response: { "access_token": "eyJ...", "user": {...} } # # 3. Send chat message (WITH TOKEN): # # POST /api/v1/chat/ # # Headers: { "Authorization": "Bearer eyJ..." } # # Body: { # # "query": "What is my account balance?", # # "conversation_id": "conv_abc" // optional # # } # # 4. Get conversation history (WITH TOKEN): # # GET /api/v1/chat/history/conv_abc # # Headers: { "Authorization": "Bearer eyJ..." } # # 5. List conversations (WITH TOKEN): # # GET /api/v1/chat/conversations?limit=10 # # Headers: { "Authorization": "Bearer eyJ..." } # # === TESTING WITH CURL === # # # 1. Register # # TOKEN=$(curl -X POST "http://localhost:8000/api/v1/auth/register" \ # # -H "Content-Type: application/json" \ # # -d '{"email":"test@test.com","password":"test123","full_name":"Test User"}' \ # # | jq -r '.access_token') # # # 2. Send chat message with token # # curl -X POST "http://localhost:8000/api/v1/chat/" \ # # -H "Content-Type: application/json" \ # # -H "Authorization: Bearer $TOKEN" \ # # -d '{"query": "What is my balance?"}' # # """ # # # ============================================================================ # # # ====================================================================================================== # # # OLD CODE # # # ====================================================================================================== # # # """ # # # Chat API Endpoints # # # RESTful API for the Banking RAG Chatbot # # # Endpoints: # # # - POST /chat - Send a message and get response # # # - GET /chat/history/{conversation_id} - Get conversation history # # # - POST /chat/conversation - Create new conversation # # # - GET /chat/conversations - List user's conversations # # # - GET /chat/health - Health check for chat service # # # """ # # # from fastapi import APIRouter, HTTPException, status # # # from pydantic import BaseModel, Field # # # from typing import List, Dict, Optional # # # from datetime import datetime # # # from app.services.chat_service import chat_service # # # from app.db.repositories.conversation_repository import ConversationRepository # # # # ============================================================================ # # # # CREATE ROUTER # # # # ============================================================================ # # # router = APIRouter() # # # # Initialize repository # # # conversation_repo = ConversationRepository() # # # # ============================================================================ # # # # PYDANTIC MODELS (Request/Response schemas) # # # # ============================================================================ # # # class ChatRequest(BaseModel): # # # """ # # # Request model for chat endpoint. # # # Example: # # # { # # # "query": "What is my account balance?", # # # "conversation_id": "abc-123", # # # "user_id": "user_456" # # # } # # # """ # # # query: str = Field(..., description="User query text", min_length=1, max_length=1000) # # # conversation_id: Optional[str] = Field(None, description="Optional conversation ID") # # # user_id: str = Field(..., description="User ID") # # # class Config: # # # json_schema_extra = { # # # "example": { # # # "query": "What is my account balance?", # # # "conversation_id": "conv-123", # # # "user_id": "user-456" # # # } # # # } # # # class ChatResponse(BaseModel): # # # """ # # # Response model for chat endpoint. # # # Contains the generated response plus metadata about the RAG pipeline. # # # """ # # # response: str = Field(..., description="Generated response text") # # # conversation_id: str = Field(..., description="Conversation ID") # # # policy_action: str = Field(..., description="Policy decision: FETCH or NO_FETCH") # # # policy_confidence: float = Field(..., description="Policy confidence score (0-1)") # # # documents_retrieved: int = Field(..., description="Number of documents retrieved") # # # top_doc_score: Optional[float] = Field(None, description="Best document similarity score") # # # total_time_ms: float = Field(..., description="Total processing time in milliseconds") # # # timestamp: str = Field(..., description="Response timestamp (ISO format)") # # # class ConversationCreateRequest(BaseModel): # # # """Request to create a new conversation""" # # # user_id: str = Field(..., description="User ID") # # # class ConversationCreateResponse(BaseModel): # # # """Response after creating a conversation""" # # # conversation_id: str = Field(..., description="Created conversation ID") # # # created_at: str = Field(..., description="Creation timestamp") # # # class MessageModel(BaseModel): # # # """Single message in conversation history""" # # # role: str = Field(..., description="Message role: user or assistant") # # # content: str = Field(..., description="Message content") # # # timestamp: str = Field(..., description="Message timestamp") # # # metadata: Optional[Dict] = Field(None, description="Optional metadata") # # # class ConversationHistoryResponse(BaseModel): # # # """Response containing conversation history""" # # # conversation_id: str # # # messages: List[MessageModel] # # # message_count: int # # # # ============================================================================ # # # # ENDPOINTS # # # # ============================================================================ # # # @router.post("/", response_model=ChatResponse, status_code=status.HTTP_200_OK) # # # async def chat(request: ChatRequest): # # # """ # # # Main chat endpoint - Send a query and get a response. # # # This endpoint: # # # 1. Processes the query through the RAG pipeline # # # 2. Saves messages to MongoDB # # # 3. Logs retrieval data for RL training # # # 4. Returns response with metadata # # # Args: # # # request: ChatRequest with query, conversation_id, user_id # # # Returns: # # # ChatResponse: Generated response with metadata # # # Raises: # # # HTTPException: If processing fails # # # """ # # # try: # # # # If no conversation_id provided, create a new conversation # # # conversation_id = request.conversation_id # # # if not conversation_id: # # # conversation_id = await conversation_repo.create_conversation( # # # user_id=request.user_id # # # ) # # # # Get conversation history # # # history = await conversation_repo.get_conversation_history( # # # conversation_id=conversation_id, # # # max_messages=10 # Last 5 turns (10 messages) # # # ) # # # # Save user message to database # # # await conversation_repo.add_message( # # # conversation_id=conversation_id, # # # message={ # # # 'role': 'user', # # # 'content': request.query, # # # 'timestamp': datetime.now() # # # } # # # ) # # # # Process query through RAG pipeline # # # result = await chat_service.process_query( # # # query=request.query, # # # conversation_history=history, # # # user_id=request.user_id # # # ) # # # # Save assistant message to database # # # await conversation_repo.add_message( # # # conversation_id=conversation_id, # # # message={ # # # 'role': 'assistant', # # # 'content': result['response'], # # # 'timestamp': datetime.now(), # # # 'metadata': { # # # 'policy_action': result['policy_action'], # # # 'policy_confidence': result['policy_confidence'], # # # 'documents_retrieved': result['documents_retrieved'], # # # 'top_doc_score': result['top_doc_score'] # # # } # # # } # # # ) # # # # Log retrieval data for RL training # # # await conversation_repo.log_retrieval({ # # # 'conversation_id': conversation_id, # # # 'user_id': request.user_id, # # # 'query': request.query, # # # 'policy_action': result['policy_action'], # # # 'policy_confidence': result['policy_confidence'], # # # 'should_retrieve': result['should_retrieve'], # # # 'documents_retrieved': result['documents_retrieved'], # # # 'top_doc_score': result['top_doc_score'], # # # 'response': result['response'], # # # 'retrieval_time_ms': result['retrieval_time_ms'], # # # 'generation_time_ms': result['generation_time_ms'], # # # 'total_time_ms': result['total_time_ms'], # # # 'retrieved_docs_metadata': result.get('retrieved_docs_metadata', []), # # # 'timestamp': datetime.now() # # # }) # # # # Return response # # # return ChatResponse( # # # response=result['response'], # # # conversation_id=conversation_id, # # # policy_action=result['policy_action'], # # # policy_confidence=result['policy_confidence'], # # # documents_retrieved=result['documents_retrieved'], # # # top_doc_score=result['top_doc_score'], # # # total_time_ms=result['total_time_ms'], # # # timestamp=result['timestamp'] # # # ) # # # except Exception as e: # # # print(f"❌ Chat endpoint error: {e}") # # # raise HTTPException( # # # status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, # # # detail=f"Failed to process chat request: {str(e)}" # # # ) # # # @router.post("/conversation", response_model=ConversationCreateResponse, status_code=status.HTTP_201_CREATED) # # # async def create_conversation(request: ConversationCreateRequest): # # # """ # # # Create a new conversation. # # # Args: # # # request: ConversationCreateRequest with user_id # # # Returns: # # # ConversationCreateResponse: Created conversation ID # # # """ # # # try: # # # conversation_id = await conversation_repo.create_conversation( # # # user_id=request.user_id # # # ) # # # return ConversationCreateResponse( # # # conversation_id=conversation_id, # # # created_at=datetime.now().isoformat() # # # ) # # # except Exception as e: # # # raise HTTPException( # # # status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, # # # detail=f"Failed to create conversation: {str(e)}" # # # ) # # # @router.get("/history/{conversation_id}", response_model=ConversationHistoryResponse) # # # async def get_conversation_history(conversation_id: str): # # # """ # # # Get conversation history by ID. # # # Args: # # # conversation_id: Conversation ID # # # Returns: # # # ConversationHistoryResponse: List of messages # # # """ # # # try: # # # # Get conversation # # # conversation = await conversation_repo.get_conversation(conversation_id) # # # if not conversation: # # # raise HTTPException( # # # status_code=status.HTTP_404_NOT_FOUND, # # # detail=f"Conversation {conversation_id} not found" # # # ) # # # # Format messages # # # messages = [] # # # for msg in conversation.get('messages', []): # # # messages.append(MessageModel( # # # role=msg['role'], # # # content=msg['content'], # # # timestamp=msg['timestamp'].isoformat() if isinstance(msg['timestamp'], datetime) else msg['timestamp'], # # # metadata=msg.get('metadata') # # # )) # # # return ConversationHistoryResponse( # # # conversation_id=conversation_id, # # # messages=messages, # # # message_count=len(messages) # # # ) # # # except HTTPException: # # # raise # # # except Exception as e: # # # raise HTTPException( # # # status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, # # # detail=f"Failed to fetch conversation history: {str(e)}" # # # ) # # # @router.get("/conversations") # # # async def list_user_conversations(user_id: str, limit: int = 10, skip: int = 0): # # # """ # # # List all conversations for a user. # # # Args: # # # user_id: User ID # # # limit: Maximum conversations to return (default: 10) # # # skip: Number to skip for pagination (default: 0) # # # Returns: # # # dict: List of conversations # # # """ # # # try: # # # conversations = await conversation_repo.get_user_conversations( # # # user_id=user_id, # # # limit=limit, # # # skip=skip # # # ) # # # # Format response # # # return { # # # "user_id": user_id, # # # "conversations": [ # # # { # # # "conversation_id": conv['conversation_id'], # # # "created_at": conv['created_at'].isoformat() if isinstance(conv['created_at'], datetime) else conv['created_at'], # # # "updated_at": conv['updated_at'].isoformat() if isinstance(conv['updated_at'], datetime) else conv['updated_at'], # # # "message_count": len(conv.get('messages', [])) # # # } # # # for conv in conversations # # # ], # # # "total": len(conversations) # # # } # # # except Exception as e: # # # raise HTTPException( # # # status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, # # # detail=f"Failed to fetch conversations: {str(e)}" # # # ) # # # @router.get("/health") # # # async def chat_health(): # # # """ # # # Health check for chat service. # # # Returns: # # # dict: Health status of chat service components # # # """ # # # try: # # # health = await chat_service.health_check() # # # return { # # # "status": "healthy", # # # "service": "chat", # # # "components": health['components'], # # # "timestamp": datetime.now().isoformat() # # # } # # # except Exception as e: # # # return { # # # "status": "unhealthy", # # # "service": "chat", # # # "error": str(e), # # # "timestamp": datetime.now().isoformat() # # # } # # # # ============================================================================ # # # # USAGE DOCUMENTATION # # # # ============================================================================ # # # """ # # # === API USAGE EXAMPLES === # # # 1. Send a chat message: # # # POST /api/v1/chat/ # # # Body: { # # # "query": "What is my account balance?", # # # "user_id": "user_123", # # # "conversation_id": "conv_abc" // optional # # # } # # # 2. Create new conversation: # # # POST /api/v1/chat/conversation # # # Body: { # # # "user_id": "user_123" # # # } # # # 3. Get conversation history: # # # GET /api/v1/chat/history/conv_abc # # # 4. List user's conversations: # # # GET /api/v1/chat/conversations?user_id=user_123&limit=10&skip=0 # # # 5. Check health: # # # GET /api/v1/chat/health # # # === TESTING WITH CURL === # # # # Send chat message # # # curl -X POST "http://localhost:8000/api/v1/chat/" \ # # # -H "Content-Type: application/json" \ # # # -d '{ # # # "query": "What is my balance?", # # # "user_id": "user_123" # # # }' # # # # Get history # # # curl "http://localhost:8000/api/v1/chat/history/conv_123" # # # === TESTING WITH SWAGGER UI === # # # After starting the server, visit: # # # http://localhost:8000/docs # # # Interactive API documentation with "Try it out" buttons! # # # """