From 770a906d468f0fd6d6122a7aea51045f5b55de4d Mon Sep 17 00:00:00 2001 From: Gopalrajdev Date: Thu, 22 Jan 2026 13:17:03 -0800 Subject: [PATCH 1/2] Add RAGChatbot blueprint --- RAGChatbot/.gitignore | 2 + RAGChatbot/README.md | 227 ++++++ RAGChatbot/TROUBLESHOOTING.md | 103 +++ RAGChatbot/api/Dockerfile | 19 + RAGChatbot/api/README.md | 693 ++++++++++++++++++ RAGChatbot/api/config.py | 58 ++ RAGChatbot/api/models.py | 61 ++ RAGChatbot/api/requirements.txt | 15 + RAGChatbot/api/server.py | 229 ++++++ RAGChatbot/api/services/__init__.py | 21 + RAGChatbot/api/services/api_client.py | 252 +++++++ RAGChatbot/api/services/pdf_service.py | 86 +++ RAGChatbot/api/services/retrieval_service.py | 231 ++++++ RAGChatbot/api/services/vector_service.py | 152 ++++ RAGChatbot/api/test_api.py | 215 ++++++ RAGChatbot/docker-compose.yml | 42 ++ RAGChatbot/images/RAG Model System Design.png | Bin 0 -> 47246 bytes RAGChatbot/images/ui.png | Bin 0 -> 139797 bytes RAGChatbot/ui/.gitignore | 25 + RAGChatbot/ui/Dockerfile | 20 + RAGChatbot/ui/README.md | 189 +++++ RAGChatbot/ui/index.html | 14 + RAGChatbot/ui/package.json | 32 + RAGChatbot/ui/postcss.config.js | 7 + RAGChatbot/ui/src/App.jsx | 77 ++ .../ui/src/components/ChatInterface.jsx | 184 +++++ RAGChatbot/ui/src/components/Header.jsx | 28 + RAGChatbot/ui/src/components/PDFUploader.jsx | 155 ++++ RAGChatbot/ui/src/components/StatusBar.jsx | 54 ++ RAGChatbot/ui/src/index.css | 34 + RAGChatbot/ui/src/main.jsx | 11 + RAGChatbot/ui/src/services/api.js | 85 +++ RAGChatbot/ui/tailwind.config.js | 27 + RAGChatbot/ui/vite.config.js | 18 + 34 files changed, 3366 insertions(+) create mode 100644 RAGChatbot/.gitignore create mode 100644 RAGChatbot/README.md create mode 100644 RAGChatbot/TROUBLESHOOTING.md create mode 100644 RAGChatbot/api/Dockerfile create mode 100644 RAGChatbot/api/README.md create mode 100644 RAGChatbot/api/config.py create mode 100644 RAGChatbot/api/models.py create mode 100644 RAGChatbot/api/requirements.txt create mode 100644 RAGChatbot/api/server.py create mode 100644 RAGChatbot/api/services/__init__.py create mode 100644 RAGChatbot/api/services/api_client.py create mode 100644 RAGChatbot/api/services/pdf_service.py create mode 100644 RAGChatbot/api/services/retrieval_service.py create mode 100644 RAGChatbot/api/services/vector_service.py create mode 100644 RAGChatbot/api/test_api.py create mode 100644 RAGChatbot/docker-compose.yml create mode 100644 RAGChatbot/images/RAG Model System Design.png create mode 100644 RAGChatbot/images/ui.png create mode 100644 RAGChatbot/ui/.gitignore create mode 100644 RAGChatbot/ui/Dockerfile create mode 100644 RAGChatbot/ui/README.md create mode 100644 RAGChatbot/ui/index.html create mode 100644 RAGChatbot/ui/package.json create mode 100644 RAGChatbot/ui/postcss.config.js create mode 100644 RAGChatbot/ui/src/App.jsx create mode 100644 RAGChatbot/ui/src/components/ChatInterface.jsx create mode 100644 RAGChatbot/ui/src/components/Header.jsx create mode 100644 RAGChatbot/ui/src/components/PDFUploader.jsx create mode 100644 RAGChatbot/ui/src/components/StatusBar.jsx create mode 100644 RAGChatbot/ui/src/index.css create mode 100644 RAGChatbot/ui/src/main.jsx create mode 100644 RAGChatbot/ui/src/services/api.js create mode 100644 RAGChatbot/ui/tailwind.config.js create mode 100644 RAGChatbot/ui/vite.config.js diff --git a/RAGChatbot/.gitignore b/RAGChatbot/.gitignore new file mode 100644 index 0000000000..1972fc44d8 --- /dev/null +++ b/RAGChatbot/.gitignore @@ -0,0 +1,2 @@ +**/.env +**/test.txt \ No newline at end of file diff --git a/RAGChatbot/README.md b/RAGChatbot/README.md new file mode 100644 index 0000000000..a4e774009f --- /dev/null +++ b/RAGChatbot/README.md @@ -0,0 +1,227 @@ +## RAG Chatbot + +A full-stack Retrieval-Augmented Generation (RAG) application that enables intelligent, document-based question answering. +The system integrates a FastAPI backend powered by LangChain, FAISS, and AI models, alongside a modern React + Vite + Tailwind CSS frontend for an intuitive chat experience. + +## Table of Contents + +- [Project Overview](#project-overview) +- [Features](#features) +- [Architecture](#architecture) +- [Prerequisites](#prerequisites) +- [Quick Start Deployment](#quick-start-deployment) +- [User Interface](#user-interface) +- [Troubleshooting](#troubleshooting) + +--- + +## Project Overview + +The **RAG Chatbot** demonstrates how retrieval-augmented generation can be used to build intelligent, document-grounded conversational systems. It retrieves relevant information from a knowledge base, passes it to a large language model, and generates a concise and reliable answer to the user’s query. This project integrates seamlessly with cloud-hosted APIs or local model endpoints, offering flexibility for research, enterprise, or educational use. + +--- + +## Features + +**Backend** + +- Clean PDF upload with validation +- LangChain-powered document processing +- FAISS-CPU vector store for efficient similarity search +- Enterprise inference endpoints for embeddings and LLM +- Keycloak authentication for secure API access +- Comprehensive error handling and logging +- File validation and size limits +- CORS enabled for web integration +- Health check endpoints +- Modular architecture (routes + services) + +**Frontend** + +- PDF file upload with drag-and-drop support +- Real-time chat interface +- Modern, responsive design with Tailwind CSS +- Built with Vite for fast development +- Live status updates +- Mobile-friendly + +--- + +## Architecture + +Below is the architecture as it consists of a server that waits for documents to embed and index into a vector database. Once documents have been uploaded, the server will wait for user queries which initiates a similarity search in the vector database before calling the LLM service to summarize the findings. + +![Architecture Diagram](./images/RAG%20Model%20System%20Design.png) + +**Service Components:** + +1. **React Web UI (Port 3000)** - Provides intuitive chat interface with drag-and-drop PDF upload, real-time messaging, and document-grounded Q&A interaction + +2. **FastAPI Backend (Port 5001)** - Handles document processing, FAISS vector storage, LangChain integration, and orchestrates retrieval-augmented generation for accurate responses + +**Typical Flow:** + +1. User uploads a document through the web UI. +2. The backend processes the document by splitting it and transforming it into embeddings before storing it in the vector database. +3. User sends a question through the web UI. +4. The backend retrieves relevant content from stored documents. +5. The model generates a response based on retrieved context. +6. The answer is displayed to the user via the UI. + +--- + +## Prerequisites + +### System Requirements + +Before you begin, ensure you have the following installed: + +- **Docker and Docker Compose** +- **Enterprise inference endpoint access** (Keycloak authentication) + +### Verify Docker Installation + +```bash +# Check Docker version +docker --version + +# Check Docker Compose version +docker compose version + +# Verify Docker is running +docker ps +``` + +## Quick Start Deployment + +### Clone the Repository + +```bash +git clone https://github.com/opea-project/GenAIExamples.git +cd GenAIExamples/RAGChatbot +``` + +### Set up the Environment + +This application requires an `.env` file in the `api` directory for proper configuration. Create it with the commands below: + +```bash +# Create the .env file in the api directory +mkdir -p api +cat > api/.env << EOF +# Backend API URL (accessible from frontend) +VITE_API_URL=https://backend:5000 + +# Required - Enterprise/Keycloak Configuration +BASE_URL=https://api.example.com +KEYCLOAK_REALM=master +KEYCLOAK_CLIENT_ID=api +KEYCLOAK_CLIENT_SECRET=your_client_secret + +# Required - Model Configuration +EMBEDDING_MODEL_ENDPOINT=bge-base-en-v1.5 +INFERENCE_MODEL_ENDPOINT=Llama-3.1-8B-Instruct +EMBEDDING_MODEL_NAME=bge-base-en-v1.5 +INFERENCE_MODEL_NAME=meta-llama/Llama-3.1-8B-Instruct +EOF +``` + +Or manually create `api/.env` with: + +```bash +# Backend API URL (accessible from frontend) +VITE_API_URL=https://backend:5000 + +# Required - Enterprise/Keycloak Configuration +BASE_URL=https://api.example.com +KEYCLOAK_REALM=master +KEYCLOAK_CLIENT_ID=api +KEYCLOAK_CLIENT_SECRET=your_client_secret + +# Required - Model Configuration +EMBEDDING_MODEL_ENDPOINT=bge-base-en-v1.5 +INFERENCE_MODEL_ENDPOINT=Llama-3.1-8B-Instruct +EMBEDDING_MODEL_NAME=bge-base-en-v1.5 +INFERENCE_MODEL_NAME=meta-llama/Llama-3.1-8B-Instruct +``` + +**Note**: The docker-compose.yml file automatically loads environment variables from `./api/.env` for the backend service. + +### Running the Application + +Start both API and UI services together with Docker Compose: + +```bash +# From the rag-chatbot directory +docker compose up --build + +# Or run in detached mode (background) +docker compose up -d --build +``` + +The API will be available at: `http://localhost:5001` +The UI will be available at: `http://localhost:3000` + +**View logs**: + +```bash +# All services +docker compose logs -f + +# Backend only +docker compose logs -f backend + +# Frontend only +docker compose logs -f frontend +``` + +**Verify the services are running**: + +```bash +# Check API health +curl http://localhost:5001/health + +# Check if containers are running +docker compose ps +``` + +## User Interface + +**Using the Application** + +Make sure you are at the localhost:3000 url + +You will be directed to the main page which has each feature + +![User Interface](images/ui.png) + +Upload a PDF: + +- Drag and drop a PDF file, or +- Click "Browse Files" to select a file +- Wait for processing to complete + +Start chatting: + +- Type your question in the input field +- Press Enter or click Send +- Get AI-powered answers based on your document + +**UI Configuration** + +When running with Docker Compose, the UI automatically connects to the backend API. The frontend is available at `http://localhost:3000` and the API at `http://localhost:5001`. + +For production deployments, you may want to configure a reverse proxy or update the API URL in the frontend configuration. + +### Stopping the Application + + +```bash +docker compose down +``` + +## Troubleshooting + +For comprehensive troubleshooting guidance, common issues, and solutions, refer to: + +[Troubleshooting Guide - TROUBLESHOOTING.md](./TROUBLESHOOTING.md) diff --git a/RAGChatbot/TROUBLESHOOTING.md b/RAGChatbot/TROUBLESHOOTING.md new file mode 100644 index 0000000000..34b352991e --- /dev/null +++ b/RAGChatbot/TROUBLESHOOTING.md @@ -0,0 +1,103 @@ +# Troubleshooting Guide + +This document contains all common issues encountered during development and their solutions. + +## Table of Contents + +- [API Common Issues](#api-common-issues) +- [UI Common Issues](#ui-common-issues) + +### API Common Issues + +#### "OPENAI_API_KEY not found in environment variables" + +**Solution**: + +1. Create a `.env` file in the `api` directory +2. Add your OpenAI API key: `OPENAI_API_KEY=your_key_here` +3. Restart the server + +#### "No documents uploaded" + +**Solution**: + +- Upload a PDF first using the `/upload-pdf` endpoint +- Check server logs for any upload errors +- Verify the PDF is not corrupted or empty + +#### "Could not load vector store" + +**Solution**: + +- The vector store is created when you upload your first PDF +- Check that the application has write permissions in the directory +- Verify `dmv_index/` directory exists and is accessible + +#### Import errors + +**Solution**: + +1. Ensure all dependencies are installed: `pip install -r requirements.txt` +2. Verify you're using Python 3.10 or higher: `python --version` +3. Activate your virtual environment if using one + +#### Server won't start + +**Solution**: + +1. Check if port 5000 is already in use: `lsof -i :5000` (Unix) or `netstat -ano | findstr :5000` (Windows) +2. Use a different port: `uvicorn server:app --port 5001` +3. Check the logs for specific error messages + +#### PDF upload fails + +**Solution**: + +1. Verify the file is a valid PDF +2. Check file size (must be under 50MB by default) +3. Ensure the PDF contains extractable text (not just images) +4. Check server logs for detailed error messages + +#### Query returns no answer + +**Solution**: + +1. Verify a document has been uploaded successfully +2. Try rephrasing your question +3. Check if the document contains relevant information +4. Increase `TOP_K_DOCUMENTS` in `config.py` for broader search + +## UI Common Issues + +### API Connection Issues + +**Problem**: "Failed to upload PDF" or "Failed to get response" + +**Solution**: + +1. Ensure the API server is running on `http://localhost:5000` +2. Check browser console for detailed errors +3. Verify CORS is enabled in the API + +### Build Issues + +**Problem**: Build fails with dependency errors + +**Solution**: + +```bash +# Clear node_modules and reinstall +rm -rf node_modules package-lock.json +npm install +``` + +### Styling Issues + +**Problem**: Styles not applying + +**Solution**: + +```bash +# Rebuild Tailwind CSS +npm run dev +``` diff --git a/RAGChatbot/api/Dockerfile b/RAGChatbot/api/Dockerfile new file mode 100644 index 0000000000..4424ff4c43 --- /dev/null +++ b/RAGChatbot/api/Dockerfile @@ -0,0 +1,19 @@ +FROM python:3.9-slim + +# Set the working directory in the container +WORKDIR /app + +COPY requirements.txt . + + +# Install Python dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Copy the rest of the application files into the container +COPY server.py . + +# Expose the port the service runs on +EXPOSE 5001 + +# Command to run the application +CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "5001", "--reload"] \ No newline at end of file diff --git a/RAGChatbot/api/README.md b/RAGChatbot/api/README.md new file mode 100644 index 0000000000..7434f91397 --- /dev/null +++ b/RAGChatbot/api/README.md @@ -0,0 +1,693 @@ +# RAG Chatbot API + +A production-ready RAG (Retrieval-Augmented Generation) chatbot API built with FastAPI, LangChain, and FAISS for document-based question answering. + +## Table of Contents + +- [Features](#features) +- [Quick Start](#quick-start) +- [Installation](#installation) +- [Configuration](#configuration) +- [Running the Server](#running-the-server) +- [API Endpoints](#api-endpoints) +- [Project Structure](#project-structure) +- [Testing](#testing) +- [Development](#development) +- [Troubleshooting](#troubleshooting) + +## Features + +- Clean PDF upload with validation +- LangChain-powered document processing +- FAISS-CPU vector store for efficient similarity search +- Enterprise inference endpoints for embeddings and LLM +- Keycloak authentication for secure API access +- Comprehensive error handling and logging +- File validation and size limits +- CORS enabled for web integration +- Health check endpoints +- Modular architecture (routes + services) + +## Quick Start + +Get up and running in 3 minutes using Docker Compose: + +```bash +# 1. Navigate to the rag-chatbot directory +cd /path/to/rag-chatbot + +# 2. Create .env file in the api directory with enterprise configuration +mkdir -p api +cat > api/.env << EOF +BASE_URL=https://api.example.com +KEYCLOAK_REALM=master +KEYCLOAK_CLIENT_ID=api +KEYCLOAK_CLIENT_SECRET=your_client_secret +EMBEDDING_MODEL_ENDPOINT=bge-base-en-v1.5 +INFERENCE_MODEL_ENDPOINT=Llama-3.1-8B-Instruct +EMBEDDING_MODEL_NAME=bge-base-en-v1.5 +INFERENCE_MODEL_NAME=meta-llama/Llama-3.1-8B-Instruct +EOF + +# 3. Start both API and UI services with Docker Compose +docker compose up --build + +# 4. Access the application +# API: http://localhost:5001/docs +# UI: http://localhost:3000 +``` + +The application will automatically start both the backend API and frontend UI. Visit http://localhost:5001/docs for interactive API documentation. + +## Installation + +### Prerequisites + +- Docker and Docker Compose installed +- Enterprise inference endpoint access (Keycloak authentication) + +### Docker Compose Setup + +Docker Compose will start both the API and UI services together. + +1. **Set up environment variables**: + +Create a `.env` file in the `api` directory (relative to `rag-chatbot/`): + +```bash +cd rag-chatbot +mkdir -p api +cat > api/.env << EOF +# Backend API URL (accessible from frontend) +VITE_API_URL=https://backend:5000 + +# Required - Enterprise/Keycloak Configuration +BASE_URL=https://api.example.com +KEYCLOAK_REALM=master +KEYCLOAK_CLIENT_ID=api +KEYCLOAK_CLIENT_SECRET=your_client_secret + +# Required - Model Configuration +EMBEDDING_MODEL_ENDPOINT=bge-base-en-v1.5 +INFERENCE_MODEL_ENDPOINT=Llama-3.1-8B-Instruct +EMBEDDING_MODEL_NAME=bge-base-en-v1.5 +INFERENCE_MODEL_NAME=meta-llama/Llama-3.1-8B-Instruct +EOF +``` + +2. **Start the services**: + +```bash +# From the rag-chatbot directory +docker compose up --build +``` + +This will: +- Build the backend API container +- Build the frontend UI container +- Start both services automatically +- Make API available at http://localhost:5001 +- Make UI available at http://localhost:3000 + +### Dependencies + +The main dependencies include: + +- `fastapi==0.109.0` - Web framework +- `uvicorn[standard]==0.27.0` - ASGI server +- `langchain==0.1.0` - LLM framework +- `faiss-cpu==1.7.4` - Vector similarity search +- `pypdf==4.0.1` - PDF processing + +See `requirements.txt` for complete list. + +## Configuration + +All configuration is centralized in `config.py`. You can modify settings by editing this file or using environment variables. + +### Environment Variables + +For Docker Compose, create a `.env` file in the `api/` directory (relative to `rag-chatbot/`): + +```bash +# Backend API URL (accessible from frontend) +VITE_API_URL=https://backend:5000 + +# Required - Enterprise/Keycloak Configuration +BASE_URL=https://api.example.com +KEYCLOAK_REALM=master +KEYCLOAK_CLIENT_ID=api +KEYCLOAK_CLIENT_SECRET=your_client_secret + +# Required - Model Configuration +EMBEDDING_MODEL_ENDPOINT=bge-base-en-v1.5 +INFERENCE_MODEL_ENDPOINT=Llama-3.1-8B-Instruct +EMBEDDING_MODEL_NAME=bge-base-en-v1.5 +INFERENCE_MODEL_NAME=meta-llama/Llama-3.1-8B-Instruct + +# Optional (with defaults shown) +# VECTOR_STORE_PATH=./dmv_index +# MAX_FILE_SIZE_MB=50 +``` + +**Note**: The docker-compose.yml file automatically loads environment variables from `./api/.env` for the backend service. + +### Configuration Settings + +Edit `config.py` to customize: + +#### File Upload Settings + +```python +MAX_FILE_SIZE = 50 * 1024 * 1024 # 50MB +ALLOWED_EXTENSIONS = {".pdf"} +``` + +#### Text Processing Settings + +```python +CHUNK_SIZE = 1000 # Characters per chunk +CHUNK_OVERLAP = 200 # Overlap between chunks +SEPARATORS = ["\n\n", "\n", " ", ""] # Text splitting separators +``` + +#### Vector Store Settings + +```python +VECTOR_STORE_PATH = "./dmv_index" # Where to store FAISS index +``` + +#### LLM Settings + +```python +LLM_TEMPERATURE = 0 # Response randomness (0-1) +TOP_K_DOCUMENTS = 4 # Documents to retrieve +# Model endpoints and names are configured via environment variables: +# EMBEDDING_MODEL_ENDPOINT, INFERENCE_MODEL_ENDPOINT +# EMBEDDING_MODEL_NAME, INFERENCE_MODEL_NAME +``` + +#### CORS Settings + +```python +CORS_ALLOW_ORIGINS = ["*"] # Update with specific origins in production +CORS_ALLOW_CREDENTIALS = True +CORS_ALLOW_METHODS = ["*"] +CORS_ALLOW_HEADERS = ["*"] +``` + +## Running the Server + +**Start both API and UI together**: + +```bash +# From the rag-chatbot directory +docker compose up --build + +# Or run in detached mode (background) +docker compose up -d --build +``` + +**Stop the services**: + +```bash +docker compose down +``` + +The API will be available at: `http://localhost:5001` +The UI will be available at: `http://localhost:3000` + +**View logs**: + +```bash +# All services +docker compose logs -f + +# Backend only +docker compose logs -f backend + +# Frontend only +docker compose logs -f frontend +``` + +### Verifying the Server + +```bash +# Check if API server is running +curl http://localhost:5001/ + +# Check health status +curl http://localhost:5001/health + +# Check if containers are running +docker compose ps +``` + +## API Endpoints + +### Health Check + +**GET /** - Basic health check + +```bash +curl http://localhost:5001/ +``` + +Response: + +```json +{ + "message": "RAG Chatbot API is running", + "version": "2.0.0", + "status": "healthy", + "vectorstore_loaded": true +} +``` + +**GET /health** - Detailed health status + +```bash +curl http://localhost:5001/health +``` + +Response: + +```json +{ + "status": "healthy", + "vectorstore_available": true, + "enterprise_inference_configured": true +} +``` + +### Upload PDF + +**POST /upload-pdf** - Upload and process a PDF document + +```bash +curl -X POST "http://localhost:5001/upload-pdf" \ + -H "Content-Type: multipart/form-data" \ + -F "file=@/path/to/document.pdf" +``` + +Response: + +```json +{ + "message": "Successfully uploaded and processed 'document.pdf'", + "num_chunks": 45, + "status": "success" +} +``` + +**Validation Rules**: + +- File must be PDF format +- Maximum size: 50MB (configurable) +- File must not be empty +- Content must be extractable + +### Query Documents + +**POST /query** - Ask questions about uploaded documents + +```bash +curl -X POST "http://localhost:5001/query" \ + -H "Content-Type: application/json" \ + -d '{"query": "What are the main topics in the document?"}' +``` + +Response: + +```json +{ + "answer": "The main topics covered in the document are...", + "query": "What are the main topics in the document?" +} +``` + +### Delete Vector Store + +**DELETE /vectorstore** - Delete the current vector store + +```bash +curl -X DELETE "http://localhost:5001/vectorstore" +``` + +Response: + +```json +{ + "message": "Vector store deleted successfully", + "status": "success" +} +``` + +### Interactive API Documentation + +FastAPI provides automatic interactive documentation: + +- **Swagger UI**: http://localhost:5001/docs +- **ReDoc**: http://localhost:5001/redoc + +## Project Structure + +The application follows a modular architecture with clear separation of concerns: + +``` +api/ +├── server.py # FastAPI app with routes (main entry point) +├── config.py # Configuration settings +├── models.py # Pydantic models for request/response validation +├── services/ # Business logic layer +│ ├── __init__.py +│ ├── pdf_service.py # PDF processing and validation +│ ├── vector_service.py # Vector store operations (FAISS) +│ └── retrieval_service.py # Query processing and LLM integration +├── requirements.txt # Python dependencies +├── test_api.py # Automated test suite +├── .env # Environment variables (create this) +└── dmv_index/ # FAISS vector store (auto-generated) +``` + +### Architecture Overview + +``` +Client Request + ↓ +server.py (Routes) + ↓ +models.py (Validation) + ↓ +services/ (Business Logic) + ├── pdf_service.py + ├── vector_service.py + └── retrieval_service.py + ↓ +External Services (Enterprise Inference Endpoints, FAISS) +``` + +**Layered Architecture**: + +- **Routes Layer** (`server.py`): HTTP handling, routing, error responses +- **Validation Layer** (`models.py`): Request/response validation +- **Business Logic Layer** (`services/`): Core functionality +- **Configuration Layer** (`config.py`): Settings management + +## Testing + +### Automated Test Suite + +Run the included test suite: + +```bash +# Basic tests (no PDF required) +python test_api.py + +# Full tests with PDF upload +python test_api.py /path/to/your/document.pdf +``` + +The test suite includes: + +- Health check tests +- Upload validation tests +- Query functionality tests +- Error handling tests +- Colored output for easy reading + +### Manual Testing + +1. **Start the services**: + +```bash +docker compose up +``` + +2. **Upload a PDF**: + +```bash +curl -X POST "http://localhost:5001/upload-pdf" \ + -F "file=@sample.pdf" +``` + +3. **Query the document**: + +```bash +curl -X POST "http://localhost:5001/query" \ + -H "Content-Type: application/json" \ + -d '{"query": "What is this document about?"}' +``` + +4. **Check health**: + +```bash +curl http://localhost:5001/health +``` + +## Development + +### Project Setup for Development + +1. Fork/clone the repository +2. Set up your `.env` file in the `api` directory +3. Run with Docker Compose for development: `docker compose up --build` +4. Make changes to code (changes are reflected with volume mounts in docker-compose.yml) + +### Adding New Features + +#### Add a New Service + +1. Create new file in `services/` directory: + +```python +# services/new_service.py +def new_function(param): + """Your business logic""" + return result +``` + +2. Export from `services/__init__.py`: + +```python +from .new_service import new_function +``` + +3. Use in routes: + +```python +# server.py +from services import new_function + +@app.post("/new-endpoint") +def new_endpoint(): + result = new_function(data) + return result +``` + +#### Add a New Endpoint + +1. Define model in `models.py`: + +```python +class NewRequest(BaseModel): + field: str +``` + +2. Add route in `server.py`: + +```python +@app.post("/new-endpoint") +def new_endpoint(request: NewRequest): + # Your logic here + return {"result": "success"} +``` + +### Modifying Configuration + +Edit `config.py` to change default settings: + +```python +# Example: Increase file size limit +MAX_FILE_SIZE = 100 * 1024 * 1024 # 100MB + +# Example: Change chunk size +CHUNK_SIZE = 1500 + +# Example: Use different model +LLM_MODEL = "gpt-4" +``` + +### Code Style + +- Use type hints for all functions +- Add docstrings to all public functions +- Follow PEP 8 style guide +- Keep functions focused (single responsibility) +- Log important operations + +## Troubleshooting + +### Common Issues + +#### "Keycloak authentication or model endpoints not configured" + +**Solution**: + +1. Create a `.env` file in the `api` directory (relative to `rag-chatbot/`) +2. Add required configuration: + ```bash + BASE_URL=https://api.example.com + KEYCLOAK_REALM=master + KEYCLOAK_CLIENT_ID=api + KEYCLOAK_CLIENT_SECRET=your_client_secret + EMBEDDING_MODEL_ENDPOINT=bge-base-en-v1.5 + INFERENCE_MODEL_ENDPOINT=Llama-3.1-8B-Instruct + EMBEDDING_MODEL_NAME=bge-base-en-v1.5 + INFERENCE_MODEL_NAME=meta-llama/Llama-3.1-8B-Instruct + ``` +3. Restart the services with `docker compose restart backend` or `docker compose down && docker compose up` + +#### "No documents uploaded" + +**Solution**: + +- Upload a PDF first using the `/upload-pdf` endpoint +- Check server logs for any upload errors +- Verify the PDF is not corrupted or empty + +#### "Could not load vector store" + +**Solution**: + +- The vector store is created when you upload your first PDF +- Check that the application has write permissions in the directory +- Verify `dmv_index/` directory exists and is accessible + +#### Import errors + +**Solution**: + +1. Rebuild the Docker containers: `docker compose down && docker compose build --no-cache && docker compose up` +2. Check container logs: `docker compose logs backend` + +#### Server won't start + +**Solution**: + +1. Check if ports 5001 or 3000 are already in use: `lsof -i :5001` or `lsof -i :3000` (Unix) or `netstat -ano | findstr :5001` (Windows) +2. Check container logs: `docker compose logs backend` +3. Try rebuilding containers: `docker compose down && docker compose build --no-cache && docker compose up` +4. Check the logs for specific error messages + +#### PDF upload fails + +**Solution**: + +1. Verify the file is a valid PDF +2. Check file size (must be under 50MB by default) +3. Ensure the PDF contains extractable text (not just images) +4. Check server logs for detailed error messages + +#### Query returns no answer + +**Solution**: + +1. Verify a document has been uploaded successfully +2. Try rephrasing your question +3. Check if the document contains relevant information +4. Increase `TOP_K_DOCUMENTS` in `config.py` for broader search + +### Logging + +The application logs important events to the console: + +- **INFO**: Normal operations (PDF processing, queries) +- **WARNING**: Non-critical issues +- **ERROR**: Critical errors with stack traces + +To view logs: + +```bash +# View all logs +docker compose logs -f + +# View backend logs only +docker compose logs -f backend +``` + +### Getting Help + +1. View logs with `docker compose logs -f` +2. Visit the health endpoint: `http://localhost:5001/health` +3. Review the error messages in API responses +4. Check the interactive documentation: `http://localhost:5001/docs` + +## Production Deployment + +### Checklist + +Before deploying to production: + +- [ ] Configure secure `KEYCLOAK_CLIENT_SECRET` +- [ ] Set up proper `BASE_URL` for enterprise endpoints +- [ ] Configure specific CORS origins (not `["*"]`) +- [ ] Enable HTTPS +- [ ] Set up monitoring and alerting +- [ ] Configure logging to files +- [ ] Implement rate limiting +- [ ] Verify Keycloak authentication is working +- [ ] Set up backup for vector stores +- [ ] Configure firewall rules +- [ ] Use environment-specific configuration + +### Docker Compose Production Deployment + +The provided `docker-compose.yml` already includes both API and UI services. For production: + +1. **Set up environment variables** in `api/.env`: + +```bash +# Enterprise/Keycloak Configuration +BASE_URL=https://api.example.com +KEYCLOAK_REALM=master +KEYCLOAK_CLIENT_ID=api +KEYCLOAK_CLIENT_SECRET=your_production_client_secret + +# Model Configuration +EMBEDDING_MODEL_ENDPOINT=bge-base-en-v1.5 +INFERENCE_MODEL_ENDPOINT=Llama-3.1-8B-Instruct +EMBEDDING_MODEL_NAME=bge-base-en-v1.5 +INFERENCE_MODEL_NAME=meta-llama/Llama-3.1-8B-Instruct +``` + +2. **Run in detached mode**: + +```bash +docker compose up -d --build +``` + +3. **Monitor logs**: + +```bash +docker compose logs -f +``` + +## License + +MIT + +## Support + +For issues, questions, or contributions: + +1. Check this README for solutions +2. Review the troubleshooting section +3. Check container logs: `docker compose logs -f` +4. Visit the interactive docs at `http://localhost:5001/docs` + +--- + +**Version**: 2.0.0 +**Last Updated**: 2025 +**API Documentation**: http://localhost:5001/docs diff --git a/RAGChatbot/api/config.py b/RAGChatbot/api/config.py new file mode 100644 index 0000000000..190375c0f3 --- /dev/null +++ b/RAGChatbot/api/config.py @@ -0,0 +1,58 @@ +""" +Configuration settings for RAG Chatbot API +""" + +import os +from dotenv import load_dotenv + +# Load environment variables from .env file +load_dotenv() + +# API Configuration +OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") + +# Custom API Configuration +BASE_URL = os.getenv("BASE_URL", "https://api.example.com") +KEYCLOAK_REALM = os.getenv("KEYCLOAK_REALM", "master") +KEYCLOAK_CLIENT_ID = os.getenv("KEYCLOAK_CLIENT_ID", "api") +KEYCLOAK_CLIENT_SECRET = os.getenv("KEYCLOAK_CLIENT_SECRET") + +# Model Configuration +EMBEDDING_MODEL_ENDPOINT = os.getenv("EMBEDDING_MODEL_ENDPOINT", "bge-base-en-v1.5") +INFERENCE_MODEL_ENDPOINT = os.getenv("INFERENCE_MODEL_ENDPOINT", "Llama-3.1-8B-Instruct") +EMBEDDING_MODEL_NAME = os.getenv("EMBEDDING_MODEL_NAME", "bge-base-en-v1.5") +INFERENCE_MODEL_NAME = os.getenv("INFERENCE_MODEL_NAME", "meta-llama/Llama-3.1-8B-Instruct") + +# Validate required configuration +if not OPENAI_API_KEY and not KEYCLOAK_CLIENT_SECRET: + raise ValueError("Either OPENAI_API_KEY or KEYCLOAK_CLIENT_SECRET must be set in environment variables") + +# Application Settings +APP_TITLE = "RAG QnA Chatbot" +APP_DESCRIPTION = "A RAG-based chatbot API using LangChain and FAISS" +APP_VERSION = "2.0.0" + +# File Upload Settings +MAX_FILE_SIZE = 50 * 1024 * 1024 # 50MB +ALLOWED_EXTENSIONS = {".pdf"} + +# Vector Store Settings +VECTOR_STORE_PATH = "./dmv_index" + +# Text Splitting Settings +CHUNK_SIZE = 1000 +CHUNK_OVERLAP = 200 +SEPARATORS = ["\n\n", "\n", " ", ""] + +# Retrieval Settings +TOP_K_DOCUMENTS = 4 +LLM_MODEL = "gpt-3.5-turbo" +LLM_TEMPERATURE = 0 +EMBEDDING_MODEL = "text-embedding-ada-002" + +# CORS Settings +CORS_ALLOW_ORIGINS = ["*"] # Update with specific origins in production +CORS_ALLOW_CREDENTIALS = True +CORS_ALLOW_METHODS = ["*"] +CORS_ALLOW_HEADERS = ["*"] + diff --git a/RAGChatbot/api/models.py b/RAGChatbot/api/models.py new file mode 100644 index 0000000000..1daafe9e89 --- /dev/null +++ b/RAGChatbot/api/models.py @@ -0,0 +1,61 @@ +""" +Pydantic models for request/response validation +""" + +from pydantic import BaseModel, Field + + +class QueryRequest(BaseModel): + """Request model for querying documents""" + query: str = Field(..., min_length=1, description="Natural language question") + + class Config: + json_schema_extra = { + "example": { + "query": "What are the main topics covered in the document?" + } + } + + +class UploadResponse(BaseModel): + """Response model for PDF upload""" + message: str = Field(..., description="Success message") + num_chunks: int = Field(..., description="Number of chunks created") + status: str = Field(..., description="Operation status") + + class Config: + json_schema_extra = { + "example": { + "message": "Successfully uploaded and processed 'document.pdf'", + "num_chunks": 45, + "status": "success" + } + } + + +class QueryResponse(BaseModel): + """Response model for document queries""" + answer: str = Field(..., description="Answer to the query") + query: str = Field(..., description="Original query") + + class Config: + json_schema_extra = { + "example": { + "answer": "The main topics covered in the document are...", + "query": "What are the main topics covered in the document?" + } + } + + +class HealthResponse(BaseModel): + """Response model for health check""" + status: str = Field(..., description="Health status") + vectorstore_available: bool = Field(..., description="Whether vectorstore is loaded") + openai_key_configured: bool = Field(..., description="Whether OpenAI key is configured") + + +class DeleteResponse(BaseModel): + """Response model for delete operations""" + message: str = Field(..., description="Result message") + status: str = Field(..., description="Operation status") + diff --git a/RAGChatbot/api/requirements.txt b/RAGChatbot/api/requirements.txt new file mode 100644 index 0000000000..8a77040e54 --- /dev/null +++ b/RAGChatbot/api/requirements.txt @@ -0,0 +1,15 @@ +fastapi>=0.109.0 +uvicorn[standard]>=0.27.0 +python-dotenv>=1.0.0 +langchain>=0.1.0 +langchain-community>=0.0.10 +langchain-openai>=0.0.5 +faiss-cpu>=1.7.4 +pypdf>=4.0.0 +openai>=1.10.0 +python-multipart>=0.0.6 +pydantic>=2.5.0 +pydantic-settings>=2.1.0 +cryptography>=3.1.0 +httpx>=0.24.0 +requests>=2.31.0 \ No newline at end of file diff --git a/RAGChatbot/api/server.py b/RAGChatbot/api/server.py new file mode 100644 index 0000000000..1aac509da1 --- /dev/null +++ b/RAGChatbot/api/server.py @@ -0,0 +1,229 @@ +""" +FastAPI server with routes for RAG Chatbot API +""" + +import os +import tempfile +import logging +from contextlib import asynccontextmanager +from fastapi import FastAPI, File, UploadFile, HTTPException, status +from fastapi.middleware.cors import CORSMiddleware + +import config +from models import ( + QueryRequest, UploadResponse, QueryResponse, + HealthResponse, DeleteResponse +) +from services import ( + validate_pdf_file, load_and_split_pdf, + store_in_vector_storage, load_vector_store, delete_vector_store, + query_documents +) + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """Lifespan context manager for FastAPI app""" + # Startup + app.state.vectorstore = load_vector_store(config.OPENAI_API_KEY) + if app.state.vectorstore: + logger.info("✓ FAISS vector store loaded successfully") + else: + logger.info("! No existing vector store found. Please upload a PDF document.") + + yield + + # Shutdown + logger.info("Shutting down RAG Chatbot API") + + +# Initialize FastAPI app +app = FastAPI( + title=config.APP_TITLE, + description=config.APP_DESCRIPTION, + version=config.APP_VERSION, + lifespan=lifespan +) + +# Add CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=config.CORS_ALLOW_ORIGINS, + allow_credentials=config.CORS_ALLOW_CREDENTIALS, + allow_methods=config.CORS_ALLOW_METHODS, + allow_headers=config.CORS_ALLOW_HEADERS, +) + + +# ==================== Routes ==================== + +@app.get("/") +def root(): + """Health check endpoint""" + return { + "message": "RAG Chatbot API is running", + "version": config.APP_VERSION, + "status": "healthy", + "vectorstore_loaded": app.state.vectorstore is not None + } + + +@app.get("/health", response_model=HealthResponse) +def health_check(): + """Detailed health check""" + return HealthResponse( + status="healthy", + vectorstore_available=app.state.vectorstore is not None, + openai_key_configured=bool(config.OPENAI_API_KEY) + ) + + +@app.post("/upload-pdf", response_model=UploadResponse) +async def upload_pdf(file: UploadFile = File(...)): + """ + Upload a PDF file, process it, create embeddings, and store in FAISS + + - **file**: PDF file to upload (max 50MB) + """ + # Validate file + validate_pdf_file(file) + + tmp_path = None + try: + # Read file content + content = await file.read() + file_size = len(content) + + # Check file size + if file_size > config.MAX_FILE_SIZE: + raise HTTPException( + status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, + detail=f"File too large. Maximum size is {config.MAX_FILE_SIZE / (1024*1024)}MB" + ) + + if file_size == 0: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Empty file uploaded" + ) + + logger.info(f"Processing PDF: {file.filename} ({file_size / 1024:.2f} KB)") + + # Save to temporary file + with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp: + tmp.write(content) + tmp_path = tmp.name + logger.info(f"Saved to temporary path: {tmp_path}") + + # Load and split PDF + chunks = load_and_split_pdf(tmp_path) + + if not chunks: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="No text content could be extracted from the PDF" + ) + + # Create embeddings and store in FAISS + vectorstore = store_in_vector_storage(chunks, config.OPENAI_API_KEY) + + # Update app state + app.state.vectorstore = vectorstore + + logger.info(f"✓ Successfully processed PDF: {file.filename}") + + return UploadResponse( + message=f"Successfully uploaded and processed '{file.filename}'", + num_chunks=len(chunks), + status="success" + ) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error processing PDF: {str(e)}", exc_info=True) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error processing PDF: {str(e)}" + ) + finally: + # Clean up temporary file + if tmp_path and os.path.exists(tmp_path): + try: + os.remove(tmp_path) + logger.info(f"Cleaned up temporary file: {tmp_path}") + except Exception as e: + logger.warning(f"Could not remove temporary file: {str(e)}") + + +@app.post("/query", response_model=QueryResponse) +def query_endpoint(request: QueryRequest): + """ + Query the uploaded documents using RAG + + - **query**: Natural language question about the documents + """ + if not app.state.vectorstore: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="No documents uploaded. Please upload a PDF first using /upload-pdf endpoint." + ) + + if not request.query or not request.query.strip(): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Query cannot be empty" + ) + + try: + result = query_documents( + request.query, + app.state.vectorstore, + config.OPENAI_API_KEY + ) + return QueryResponse(**result) + except Exception as e: + logger.error(f"Error processing query: {str(e)}", exc_info=True) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error processing query: {str(e)}" + ) + + +@app.delete("/vectorstore", response_model=DeleteResponse) +def delete_vectorstore_endpoint(): + """Delete the current vector store""" + try: + deleted = delete_vector_store() + app.state.vectorstore = None + + if deleted: + return DeleteResponse( + message="Vector store deleted successfully", + status="success" + ) + else: + return DeleteResponse( + message="No vector store found to delete", + status="success" + ) + except Exception as e: + logger.error(f"Error deleting vector store: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error deleting vector store: {str(e)}" + ) + + +# Entry point for running with uvicorn +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=5001) + diff --git a/RAGChatbot/api/services/__init__.py b/RAGChatbot/api/services/__init__.py new file mode 100644 index 0000000000..2802bf9895 --- /dev/null +++ b/RAGChatbot/api/services/__init__.py @@ -0,0 +1,21 @@ +""" +Services package for RAG Chatbot API +""" + +from .pdf_service import load_and_split_pdf, validate_pdf_file +from .vector_service import store_in_vector_storage, load_vector_store, delete_vector_store +from .retrieval_service import build_retrieval_chain, query_documents +from .api_client import APIClient, get_api_client + +__all__ = [ + 'load_and_split_pdf', + 'validate_pdf_file', + 'store_in_vector_storage', + 'load_vector_store', + 'delete_vector_store', + 'build_retrieval_chain', + 'query_documents', + 'APIClient', + 'get_api_client' +] + diff --git a/RAGChatbot/api/services/api_client.py b/RAGChatbot/api/services/api_client.py new file mode 100644 index 0000000000..111c737a9a --- /dev/null +++ b/RAGChatbot/api/services/api_client.py @@ -0,0 +1,252 @@ +""" +API Client for authentication and API calls +Similar to simple-client/main.py implementation +""" + +import logging +import requests +import httpx +from typing import Optional +import config + +logger = logging.getLogger(__name__) + + +class APIClient: + """ + Client for handling authentication and API calls + """ + + def __init__(self): + self.base_url = config.BASE_URL + self.token = None + self.http_client = None + self._authenticate() + + def _authenticate(self) -> None: + """ + Authenticate and obtain access token from Keycloak + """ + token_url = f"{self.base_url}/token" + payload = { + "grant_type": "client_credentials", + "client_id": config.KEYCLOAK_CLIENT_ID, + "client_secret": config.KEYCLOAK_CLIENT_SECRET, + } + + try: + response = requests.post(token_url, data=payload, verify=False) + + if response.status_code == 200: + self.token = response.json().get("access_token") + logger.info(f"✓ Access token obtained: {self.token[:20]}..." if self.token else "Failed to get token") + + # Create httpx client with SSL verification disabled (like -k in curl) + self.http_client = httpx.Client(verify=False) + + else: + logger.error(f"Error obtaining token: {response.status_code} - {response.text}") + raise Exception(f"Authentication failed: {response.status_code}") + + except Exception as e: + logger.error(f"Error during authentication: {str(e)}") + raise + + def get_embedding_client(self): + """ + Get OpenAI-style client for embeddings + Uses bge-base-en-v1.5 endpoint + """ + from openai import OpenAI + + return OpenAI( + api_key=self.token, + base_url=f"{self.base_url}/{config.EMBEDDING_MODEL_ENDPOINT}/v1", + http_client=self.http_client + ) + + def get_inference_client(self): + """ + Get OpenAI-style client for inference/completions + Uses Llama-3.1-8B-Instruct endpoint + """ + from openai import OpenAI + + return OpenAI( + api_key=self.token, + base_url=f"{self.base_url}/{config.INFERENCE_MODEL_ENDPOINT}/v1", + http_client=self.http_client + ) + + def embed_text(self, text: str) -> list: + """ + Get embedding for text + Uses the bge-base-en-v1.5 embedding model + + Args: + text: Text to embed + + Returns: + List of embedding values + """ + try: + client = self.get_embedding_client() + # Call the embeddings endpoint + response = client.embeddings.create( + model=config.EMBEDDING_MODEL_NAME, + input=text + ) + return response.data[0].embedding + except Exception as e: + logger.error(f"Error generating embedding: {str(e)}") + raise + + def embed_texts(self, texts: list) -> list: + """ + Get embeddings for multiple texts + Batches requests to avoid exceeding API limits (max batch size: 32) + + Args: + texts: List of texts to embed + + Returns: + List of embedding vectors + """ + try: + BATCH_SIZE = 32 # Maximum allowed batch size + all_embeddings = [] + client = self.get_embedding_client() + + # Process in batches of 32 + for i in range(0, len(texts), BATCH_SIZE): + batch = texts[i:i + BATCH_SIZE] + logger.info(f"Processing embedding batch {i//BATCH_SIZE + 1}/{(len(texts) + BATCH_SIZE - 1)//BATCH_SIZE} ({len(batch)} texts)") + + response = client.embeddings.create( + model=config.EMBEDDING_MODEL_NAME, + input=batch + ) + batch_embeddings = [data.embedding for data in response.data] + all_embeddings.extend(batch_embeddings) + + return all_embeddings + except Exception as e: + logger.error(f"Error generating embeddings: {str(e)}") + raise + + def complete(self, prompt: str, max_tokens: int = 50, temperature: float = 0) -> str: + """ + Get completion from the inference model + Uses Llama-3.1-8B-Instruct for inference + + Args: + prompt: Input prompt + max_tokens: Maximum tokens to generate + temperature: Temperature for generation + + Returns: + Generated text + """ + try: + client = self.get_inference_client() + logger.info(f"Calling inference client with model: {config.INFERENCE_MODEL_NAME}") + response = client.completions.create( + model=config.INFERENCE_MODEL_NAME, + prompt=prompt, + max_tokens=max_tokens, + temperature=temperature + ) + + # Handle response structure + if hasattr(response, 'choices') and len(response.choices) > 0: + choice = response.choices[0] + if hasattr(choice, 'text'): + return choice.text + else: + logger.error(f"Unexpected choice structure: {type(choice)}, {choice}") + return str(choice) + else: + logger.error(f"Unexpected response: {type(response)}, {response}") + return "" + except Exception as e: + logger.error(f"Error generating completion: {str(e)}", exc_info=True) + raise + + def chat_complete(self, messages: list, max_tokens: int = 150, temperature: float = 0) -> str: + """ + Get chat completion from the inference model + + Args: + messages: List of message dicts with 'role' and 'content' + max_tokens: Maximum tokens to generate + temperature: Temperature for generation + + Returns: + Generated text + """ + try: + client = self.get_inference_client() + # Convert messages to a prompt for the completions endpoint + # (since Llama models use completions, not chat.completions) + prompt = "" + for msg in messages: + role = msg.get('role', 'user') + content = msg.get('content', '') + if role == 'system': + prompt += f"System: {content}\n\n" + elif role == 'user': + prompt += f"User: {content}\n\n" + elif role == 'assistant': + prompt += f"Assistant: {content}\n\n" + prompt += "Assistant:" + + logger.info(f"Calling inference with prompt length: {len(prompt)}") + + response = client.completions.create( + model=config.INFERENCE_MODEL_NAME, + prompt=prompt, + max_tokens=max_tokens, + temperature=temperature + ) + + # Handle response structure + if hasattr(response, 'choices') and len(response.choices) > 0: + choice = response.choices[0] + if hasattr(choice, 'text'): + return choice.text + elif hasattr(choice, 'message') and hasattr(choice.message, 'content'): + return choice.message.content + else: + logger.error(f"Unexpected response structure: {type(choice)}, {choice}") + return str(choice) + else: + logger.error(f"Unexpected response: {type(response)}, {response}") + return "" + except Exception as e: + logger.error(f"Error generating chat completion: {str(e)}", exc_info=True) + raise + + def __del__(self): + """ + Cleanup: close httpx client + """ + if self.http_client: + self.http_client.close() + + +# Global API client instance +_api_client: Optional[APIClient] = None + + +def get_api_client() -> APIClient: + """ + Get or create the global API client instance + + Returns: + APIClient instance + """ + global _api_client + if _api_client is None: + _api_client = APIClient() + return _api_client + diff --git a/RAGChatbot/api/services/pdf_service.py b/RAGChatbot/api/services/pdf_service.py new file mode 100644 index 0000000000..9fbc2032af --- /dev/null +++ b/RAGChatbot/api/services/pdf_service.py @@ -0,0 +1,86 @@ +""" +PDF processing service +Handles PDF validation, loading, and text splitting +""" + +import logging +from pathlib import Path +from fastapi import UploadFile, HTTPException, status +from langchain_community.document_loaders import PyPDFLoader +from langchain_text_splitters import RecursiveCharacterTextSplitter + +logger = logging.getLogger(__name__) + +# Constants +ALLOWED_EXTENSIONS = {".pdf"} +MAX_FILE_SIZE = 50 * 1024 * 1024 # 50MB + + +def validate_pdf_file(file: UploadFile) -> None: + """ + Validate uploaded PDF file + + Args: + file: UploadFile object from FastAPI + + Raises: + HTTPException: If file validation fails + """ + if not file.filename: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="No filename provided" + ) + + file_ext = Path(file.filename).suffix.lower() + if file_ext not in ALLOWED_EXTENSIONS: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Invalid file type. Only PDF files are allowed. Got: {file_ext}" + ) + + if not file.content_type or "pdf" not in file.content_type.lower(): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Invalid content type. Expected PDF, got: {file.content_type}" + ) + + +def load_and_split_pdf(path: str) -> list: + """ + Load PDF and split into chunks using RecursiveCharacterTextSplitter + + Args: + path: Path to the PDF file + + Returns: + List of document chunks + + Raises: + ValueError: If no content can be extracted + Exception: For other processing errors + """ + try: + # Load PDF documents + loader = PyPDFLoader(file_path=path) + documents = loader.load() + logger.info(f"Loaded {len(documents)} pages from PDF") + + if not documents: + raise ValueError("No content extracted from PDF") + + # Split text into chunks with better strategy + text_splitter = RecursiveCharacterTextSplitter( + chunk_size=1000, + chunk_overlap=200, + length_function=len, + separators=["\n\n", "\n", " ", ""] + ) + chunks = text_splitter.split_documents(documents) + logger.info(f"Split into {len(chunks)} chunks") + + return chunks + except Exception as e: + logger.error(f"Error loading and splitting PDF: {str(e)}") + raise + diff --git a/RAGChatbot/api/services/retrieval_service.py b/RAGChatbot/api/services/retrieval_service.py new file mode 100644 index 0000000000..a5efe82252 --- /dev/null +++ b/RAGChatbot/api/services/retrieval_service.py @@ -0,0 +1,231 @@ +""" +Retrieval service +Handles query processing and retrieval chain operations +""" + +import logging +from langchain_openai import ChatOpenAI +from langchain_community.vectorstores import FAISS +from langchain.chains.retrieval import create_retrieval_chain +from langchain.chains.combine_documents import create_stuff_documents_chain +from langchain import hub +from langchain_core.language_models.chat_models import BaseChatModel +from langchain_core.language_models.llms import LLM +from langchain_core.outputs import LLMResult, Generation +from langchain_core.messages import HumanMessage, AIMessage, SystemMessage, BaseMessage +from typing import List, Optional, Any +import config + +logger = logging.getLogger(__name__) + + +class CustomLLM(LLM): + """ + Custom LLM class that uses the Llama-3.1-8B-Instruct endpoint + """ + + @property + def _llm_type(self) -> str: + """Return type of LLM.""" + return "custom_llm" + + def _call( + self, + prompt: str, + stop: Optional[List[str]] = None, + run_manager: Optional[Any] = None, + **kwargs: Any, + ) -> str: + """Call the LLM on the given prompt.""" + from .api_client import get_api_client + api_client = get_api_client() + return api_client.complete(prompt, max_tokens=kwargs.get('max_tokens', 150), temperature=kwargs.get('temperature', 0)) + + +class CustomChatModel(BaseChatModel): + """ + Custom Chat Model that uses the Llama-3.1-8B-Instruct endpoint + """ + + @property + def _llm_type(self) -> str: + """Return type of LLM.""" + return "custom_chat" + + def _generate( + self, + messages: List[BaseMessage], + stop: Optional[List[str]] = None, + run_manager: Optional[Any] = None, + **kwargs: Any, + ) -> LLMResult: + """Generate response from messages.""" + from .api_client import get_api_client + api_client = get_api_client() + + # Convert messages to a prompt string + # Build the prompt from all messages + prompt_parts = [] + + for msg in messages: + if isinstance(msg, SystemMessage): + prompt_parts.append(f"System: {msg.content}") + elif isinstance(msg, HumanMessage): + prompt_parts.append(f"User: {msg.content}") + elif isinstance(msg, AIMessage): + prompt_parts.append(f"Assistant: {msg.content}") + + # Join all parts and add assistant prompt suffix + full_prompt = "\n\n".join(prompt_parts) + if not full_prompt.endswith("Assistant:"): + full_prompt += "\n\nAssistant:" + + logger.info(f"Sending prompt to LLM (length: {len(full_prompt)} chars)") + + # Use the complete method which directly sends the prompt + # This calls: Llama-3.1-8B-Instruct/v1/completions with prompt + response_text = api_client.complete( + full_prompt, + max_tokens=kwargs.get('max_tokens', 150), + temperature=kwargs.get('temperature', 0) + ) + + generations = [Generation(text=response_text)] + return LLMResult(generations=[generations]) + + +def get_llm(api_key: str) -> BaseChatModel: + """ + Get LLM instance (ChatOpenAI or CustomChatModel based on config) + + Args: + api_key: API key + + Returns: + LLM instance + """ + # Check if using custom API + if hasattr(config, 'KEYCLOAK_CLIENT_SECRET') and config.KEYCLOAK_CLIENT_SECRET: + return CustomChatModel() + else: + # Fallback to OpenAI ChatOpenAI + return ChatOpenAI( + model="gpt-3.5-turbo", + temperature=0, + openai_api_key=api_key + ) + + +def build_retrieval_chain(vectorstore: FAISS, api_key: str): + """ + Build retrieval chain with LLM (ChatOpenAI or CustomChatModel) + + Args: + vectorstore: FAISS vectorstore instance + api_key: API key + + Returns: + Configured retrieval chain + + Raises: + Exception: If chain building fails + """ + try: + retrieval_qa_chat_prompt = hub.pull("langchain-ai/retrieval-qa-chat") + llm = get_llm(api_key) + combine_docs_chain = create_stuff_documents_chain(llm, retrieval_qa_chat_prompt) + retrieval_chain = create_retrieval_chain( + vectorstore.as_retriever(search_kwargs={"k": 4}), + combine_docs_chain + ) + return retrieval_chain + except Exception as e: + logger.error(f"Error building retrieval chain: {str(e)}") + raise + + +def query_documents(query: str, vectorstore: FAISS, api_key: str) -> dict: + """ + Query the documents using RAG with custom embedding and inference + + Simple workflow: + 1. Create embedding for the query + 2. Search for similar documents in the vectorstore + 3. Format the retrieved context + 4. Summarize using Llama inference endpoint + + Args: + query: User's question + vectorstore: FAISS vectorstore instance + api_key: API key + + Returns: + Dictionary with answer and query + + Raises: + Exception: If query processing fails + """ + try: + logger.info(f"Processing query: {query}") + + # Step 1: Create embedding for the query + logger.info("Creating query embedding...") + from .api_client import get_api_client + api_client = get_api_client() + + query_embedding = api_client.embed_text(query) + logger.info(f"Query embedding created (dimension: {len(query_embedding)})") + + # Step 2: Search for similar documents (similarity search) + logger.info("Searching for similar documents...") + similar_docs = vectorstore.similarity_search_by_vector(query_embedding, k=4) + logger.info(f"Found {len(similar_docs)} similar documents") + + if not similar_docs: + return { + "answer": "I couldn't find any relevant documents to answer your question.", + "query": query + } + + # Step 3: Format the retrieved context + context_parts = [] + for i, doc in enumerate(similar_docs): + context_parts.append(f"Document {i+1}:\n{doc.page_content}") + + context = "\n\n".join(context_parts) + logger.info(f"Context length: {len(context)} characters") + + # Step 4: Create prompt for summarization using Llama + prompt = f"""Based on the following documents, provide a comprehensive summary that addresses the question. + +Documents: +{context} + +Question: {query} + +Summary:""" + + logger.info(f"Calling Llama inference with prompt length: {len(prompt)}") + + # Call Llama inference endpoint for summarization + answer = api_client.complete( + prompt=prompt, + max_tokens=200, + temperature=0 + ) + + answer = answer.strip() + + if not answer: + answer = "I couldn't find a relevant answer in the documents." + + logger.info("✓ Query completed successfully") + + return { + "answer": answer, + "query": query + } + except Exception as e: + logger.error(f"Error processing query: {str(e)}", exc_info=True) + raise + diff --git a/RAGChatbot/api/services/vector_service.py b/RAGChatbot/api/services/vector_service.py new file mode 100644 index 0000000000..516c969bd0 --- /dev/null +++ b/RAGChatbot/api/services/vector_service.py @@ -0,0 +1,152 @@ +""" +Vector store service +Handles FAISS vector store operations +""" + +import os +import logging +import shutil +from typing import Optional +from langchain_openai import OpenAIEmbeddings +from langchain_community.vectorstores import FAISS +from langchain_core.embeddings import Embeddings +import config + +logger = logging.getLogger(__name__) + +# Constants +VECTOR_STORE_PATH = "./dmv_index" + + +class CustomEmbeddings(Embeddings): + """ + Custom embeddings class that uses the bge-base-en-v1.5 endpoint + """ + + def __init__(self): + from .api_client import get_api_client + self.api_client = get_api_client() + + def embed_documents(self, texts: list[str]) -> list[list[float]]: + """ + Embed multiple documents + Note: Batches are handled automatically by api_client (max batch size: 32) + + Args: + texts: List of texts to embed + + Returns: + List of embedding vectors + """ + return self.api_client.embed_texts(texts) + + def embed_query(self, text: str) -> list[float]: + """ + Embed a single query + + Args: + text: Text to embed + + Returns: + Embedding vector + """ + return self.api_client.embed_text(text) + + +def get_embeddings(api_key: str) -> Embeddings: + """ + Create embeddings instance + + Args: + api_key: API key (for compatibility, not used with custom endpoint) + + Returns: + Embeddings instance (CustomEmbeddings if using custom API, OpenAIEmbeddings otherwise) + """ + # Check if using custom API + if hasattr(config, 'KEYCLOAK_CLIENT_SECRET') and config.KEYCLOAK_CLIENT_SECRET: + return CustomEmbeddings() + else: + # Fallback to OpenAI + return OpenAIEmbeddings( + model="text-embedding-ada-002", + openai_api_key=api_key + ) + + +def store_in_vector_storage(chunks: list, api_key: str) -> FAISS: + """ + Create embeddings and store in FAISS vector store + + Args: + chunks: List of document chunks + api_key: OpenAI API key + + Returns: + FAISS vectorstore instance + + Raises: + Exception: If storage operation fails + """ + try: + embeddings = get_embeddings(api_key) + vectorstore = FAISS.from_documents(chunks, embeddings) + + # Ensure directory exists + os.makedirs( + os.path.dirname(VECTOR_STORE_PATH) if os.path.dirname(VECTOR_STORE_PATH) else ".", + exist_ok=True + ) + vectorstore.save_local(VECTOR_STORE_PATH) + logger.info(f"Saved vector store to {VECTOR_STORE_PATH}") + + return vectorstore + except Exception as e: + logger.error(f"Error storing vectors: {str(e)}") + raise + + +def load_vector_store(api_key: str) -> Optional[FAISS]: + """ + Load existing FAISS vector store + + Args: + api_key: OpenAI API key + + Returns: + FAISS vectorstore instance or None if not found + """ + try: + embeddings = get_embeddings(api_key) + vectorstore = FAISS.load_local( + VECTOR_STORE_PATH, + embeddings, + allow_dangerous_deserialization=True + ) + logger.info("Loaded existing FAISS vector store") + return vectorstore + except Exception as e: + logger.warning(f"Could not load vector store: {str(e)}") + return None + + +def delete_vector_store() -> bool: + """ + Delete the vector store from disk + + Returns: + True if deleted successfully, False otherwise + + Raises: + Exception: If deletion fails + """ + try: + if os.path.exists(VECTOR_STORE_PATH): + shutil.rmtree(VECTOR_STORE_PATH) + logger.info("Deleted vector store") + return True + return False + except Exception as e: + logger.error(f"Error deleting vector store: {str(e)}") + raise + diff --git a/RAGChatbot/api/test_api.py b/RAGChatbot/api/test_api.py new file mode 100644 index 0000000000..ac4baf6be5 --- /dev/null +++ b/RAGChatbot/api/test_api.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +""" +Test script for RAG Chatbot API +Tests PDF upload and query functionality + +Usage: + python test_api.py # Run basic tests + python test_api.py /path/to/file.pdf # Run full tests with PDF upload +""" + +import requests +import sys +import time +from pathlib import Path + +BASE_URL = "http://localhost:5000" + +def print_status(message, status="info"): + """Print colored status messages""" + colors = { + "info": "\033[94m", # Blue + "success": "\033[92m", # Green + "error": "\033[91m", # Red + "warning": "\033[93m" # Yellow + } + reset = "\033[0m" + print(f"{colors.get(status, '')}{message}{reset}") + + +def test_health_check(): + """Test health check endpoint""" + print_status("\n1. Testing health check endpoint...", "info") + try: + response = requests.get(f"{BASE_URL}/") + response.raise_for_status() + data = response.json() + print_status(f"✓ Health check passed: {data['message']}", "success") + print(f" Version: {data.get('version', 'N/A')}") + print(f" Vectorstore loaded: {data.get('vectorstore_loaded', False)}") + return True + except Exception as e: + print_status(f"✗ Health check failed: {str(e)}", "error") + return False + + +def test_detailed_health(): + """Test detailed health endpoint""" + print_status("\n2. Testing detailed health endpoint...", "info") + try: + response = requests.get(f"{BASE_URL}/health") + response.raise_for_status() + data = response.json() + print_status("✓ Detailed health check passed", "success") + print(f" Status: {data.get('status')}") + print(f" Vectorstore available: {data.get('vectorstore_available')}") + print(f" OpenAI key configured: {data.get('openai_key_configured')}") + return True + except Exception as e: + print_status(f"✗ Detailed health check failed: {str(e)}", "error") + return False + + +def test_upload_pdf(pdf_path=None): + """Test PDF upload endpoint""" + print_status("\n3. Testing PDF upload...", "info") + + if pdf_path and Path(pdf_path).exists(): + file_path = pdf_path + else: + print_status(" No PDF file provided. Skipping upload test.", "warning") + print_status(" To test upload, run: python test_api.py /path/to/file.pdf", "warning") + return None + + try: + print(f" Uploading: {file_path}") + with open(file_path, 'rb') as f: + files = {'file': (Path(file_path).name, f, 'application/pdf')} + response = requests.post(f"{BASE_URL}/upload-pdf", files=files) + response.raise_for_status() + data = response.json() + + print_status(f"✓ Upload successful!", "success") + print(f" Message: {data['message']}") + print(f" Number of chunks: {data['num_chunks']}") + print(f" Status: {data['status']}") + return True + except requests.exceptions.HTTPError as e: + print_status(f"✗ Upload failed: {e}", "error") + try: + error_detail = e.response.json() + print(f" Error details: {error_detail}") + except: + pass + return False + except Exception as e: + print_status(f"✗ Upload failed: {str(e)}", "error") + return False + + +def test_query(query="What is this document about?"): + """Test query endpoint""" + print_status("\n4. Testing query endpoint...", "info") + print(f" Query: '{query}'") + + try: + response = requests.post( + f"{BASE_URL}/query", + json={"query": query} + ) + response.raise_for_status() + data = response.json() + + print_status("✓ Query successful!", "success") + print(f" Answer: {data['answer'][:200]}{'...' if len(data['answer']) > 200 else ''}") + return True + except requests.exceptions.HTTPError as e: + if e.response.status_code == 400: + print_status("✗ No documents uploaded yet. Upload a PDF first.", "warning") + else: + print_status(f"✗ Query failed: {e}", "error") + try: + error_detail = e.response.json() + print(f" Error details: {error_detail}") + except: + pass + return False + except Exception as e: + print_status(f"✗ Query failed: {str(e)}", "error") + return False + + +def test_invalid_upload(): + """Test upload validation with invalid file""" + print_status("\n5. Testing upload validation...", "info") + + try: + # Try uploading a text file + files = {'file': ('test.txt', b'This is not a PDF', 'text/plain')} + response = requests.post(f"{BASE_URL}/upload-pdf", files=files) + + if response.status_code == 400: + print_status("✓ Validation working: Invalid file rejected correctly", "success") + return True + else: + print_status("✗ Validation issue: Invalid file was accepted", "error") + return False + except Exception as e: + print_status(f"✗ Validation test failed: {str(e)}", "error") + return False + + +def main(): + """Run all tests""" + print_status("=" * 60) + print_status("RAG Chatbot API Test Suite", "info") + print_status("=" * 60) + + # Check if server is running + print_status("\nChecking if server is running...", "info") + try: + requests.get(BASE_URL, timeout=2) + except requests.exceptions.RequestException: + print_status("✗ Server is not running!", "error") + print_status(f"Please start the server first:", "warning") + print_status(f" cd /Users/raghavdarisi/projects/GenAISamples/rag-chatbot/api", "warning") + print_status(f" uvicorn server:app --reload", "warning") + print_status(f" OR: python server.py", "warning") + sys.exit(1) + + print_status("✓ Server is running", "success") + + # Get PDF path from command line if provided + pdf_path = sys.argv[1] if len(sys.argv) > 1 else None + + # Run tests + results = [] + results.append(("Health Check", test_health_check())) + results.append(("Detailed Health", test_detailed_health())) + + upload_result = test_upload_pdf(pdf_path) + if upload_result is not None: + results.append(("PDF Upload", upload_result)) + + if upload_result: + # Wait a moment for processing + time.sleep(1) + results.append(("Query", test_query())) + results.append(("Query 2", test_query("Summarize the main points"))) + + results.append(("Validation", test_invalid_upload())) + + # Print summary + print_status("\n" + "=" * 60) + print_status("Test Summary", "info") + print_status("=" * 60) + + passed = sum(1 for _, result in results if result) + total = len(results) + + for test_name, result in results: + status = "success" if result else "error" + symbol = "✓" if result else "✗" + print_status(f"{symbol} {test_name}", status) + + print_status(f"\nPassed: {passed}/{total}", "success" if passed == total else "warning") + + if pdf_path is None: + print_status("\nNote: PDF upload test was skipped", "warning") + print_status("To run full tests with PDF upload:", "info") + print_status(f" python test_api.py /path/to/your/document.pdf", "info") + + +if __name__ == "__main__": + main() + diff --git a/RAGChatbot/docker-compose.yml b/RAGChatbot/docker-compose.yml new file mode 100644 index 0000000000..020d0c1656 --- /dev/null +++ b/RAGChatbot/docker-compose.yml @@ -0,0 +1,42 @@ +services: + # backend Gateway (Python) + backend: + build: + context: ./api + dockerfile: Dockerfile + container_name: backend + ports: + - "5001:5001" + env_file: + - ./api/.env + volumes: + - ./api:/app + networks: + - app_network + restart: unless-stopped + + + # Frontend (React) + frontend: + build: + context: ./ui + dockerfile: Dockerfile + container_name: frontend + ports: + - "3000:3000" + depends_on: + - backend + networks: + - app_network + restart: unless-stopped + +################################## +# 🔗 Shared Network +################################## +networks: + app_network: + driver: bridge + +volumes: + audio-files: + driver: local \ No newline at end of file diff --git a/RAGChatbot/images/RAG Model System Design.png b/RAGChatbot/images/RAG Model System Design.png new file mode 100644 index 0000000000000000000000000000000000000000..7838fe5870f6d505333d274dead955b2e91588e8 GIT binary patch literal 47246 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 C6il}O literal 0 HcmV?d00001 diff --git a/RAGChatbot/ui/.gitignore b/RAGChatbot/ui/.gitignore new file mode 100644 index 0000000000..d600b6c76d --- /dev/null +++ b/RAGChatbot/ui/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + diff --git a/RAGChatbot/ui/Dockerfile b/RAGChatbot/ui/Dockerfile new file mode 100644 index 0000000000..8a5acb3a82 --- /dev/null +++ b/RAGChatbot/ui/Dockerfile @@ -0,0 +1,20 @@ +FROM node:18 + +# Set the working directory +WORKDIR /app + +# Copy package.json and package-lock.json +COPY package.json ./ +COPY package-lock.json ./ + +# Install dependencies +RUN npm install + +# Copy the rest of the application files +COPY . . + +# Expose the port the app runs on +EXPOSE 3000 + +# Command to run the application +CMD ["npm", "run", "dev", "--", "--host"] \ No newline at end of file diff --git a/RAGChatbot/ui/README.md b/RAGChatbot/ui/README.md new file mode 100644 index 0000000000..532316281f --- /dev/null +++ b/RAGChatbot/ui/README.md @@ -0,0 +1,189 @@ +# RAG Chatbot UI + +A clean and elegant React-based user interface for the RAG Chatbot application. + +## Features + +- PDF file upload with drag-and-drop support +- Real-time chat interface +- Modern, responsive design with Tailwind CSS +- Built with Vite for fast development +- Live status updates +- Mobile-friendly + +## Quick Start + +The UI runs automatically when using Docker Compose. See the main project README for setup instructions. + +The UI will be available at `http://localhost:3000` + +## Development + +This UI runs as part of the Docker Compose setup. For local development without Docker, you can use the scripts below, but Docker Compose is the recommended approach. + +### Available Scripts (Local Development Only) + +```bash +# Start development server with hot reload +npm run dev + +# Build for production +npm run build + +# Preview production build +npm run preview + +# Lint code +npm run lint +``` + +### Project Structure + +``` +ui/ +├── src/ +│ ├── components/ +│ │ ├── Header.jsx # App header +│ │ ├── StatusBar.jsx # Document status display +│ │ ├── PDFUploader.jsx # PDF upload component +│ │ └── ChatInterface.jsx # Chat UI +│ ├── services/ +│ │ └── api.js # API client +│ ├── App.jsx # Main app component +│ ├── main.jsx # Entry point +│ └── index.css # Global styles +├── public/ # Static assets +├── index.html # HTML template +├── vite.config.js # Vite configuration +├── tailwind.config.js # Tailwind CSS config +└── package.json # Dependencies +``` + +## Configuration + +When running with Docker Compose, the UI automatically connects to the backend. Configuration is handled through the docker-compose.yml file. + +## Usage + +1. **Start the application** using Docker Compose (from the `rag-chatbot` directory): + + ```bash + docker compose up --build + ``` + +2. **Upload a PDF**: + + - Drag and drop a PDF file, or + - Click "Browse Files" to select a file + - Wait for processing to complete + +4. **Start chatting**: + - Type your question in the input field + - Press Enter or click Send + - Get AI-powered answers based on your document + +## Features in Detail + +### PDF Upload + +- Drag-and-drop support +- File validation (PDF only, max 50MB) +- Upload progress indicator +- Success/error notifications + +### Chat Interface + +- Real-time messaging +- Message history +- Typing indicators +- Timestamp display +- Error handling + +### Status Bar + +- Document upload status +- Progress tracking +- Quick reset functionality + +## Building for Production + +```bash +# Build the production bundle +npm run build + +# The built files will be in the dist/ directory +# Serve with any static file server +``` + +### Deploy with Docker Compose + +The UI is automatically deployed when using Docker Compose from the root `rag-chatbot` directory. The Dockerfile in this directory is used by the docker-compose.yml configuration. + +## Customization + +### Styling + +The UI uses Tailwind CSS. Customize colors and theme in `tailwind.config.js`: + +```javascript +theme: { + extend: { + colors: { + primary: { + // Your custom colors + } + } + } +} +``` + +### Backend Integration + +The UI communicates with the backend through `src/services/api.js`. When running with Docker Compose, the backend is automatically available. + +## Troubleshooting + +### Build Issues + +**Problem**: Build fails with dependency errors + +**Solution**: + +```bash +# Clear node_modules and reinstall +rm -rf node_modules package-lock.json +npm install +``` + +### Styling Issues + +**Problem**: Styles not applying + +**Solution**: + +```bash +# Rebuild Tailwind CSS +npm run dev +``` + +## Browser Support + +- Chrome/Edge (latest) +- Firefox (latest) +- Safari (latest) +- Mobile browsers (iOS Safari, Chrome Mobile) + +## Performance + +- Optimized bundle size with Vite +- Code splitting for faster loads +- Lazy loading of components +- Efficient re-renders with React + +## License + +MIT + +--- + +**Built with**: React, Vite, Tailwind CSS, Axios, and Lucide Icons diff --git a/RAGChatbot/ui/index.html b/RAGChatbot/ui/index.html new file mode 100644 index 0000000000..c6a3e65988 --- /dev/null +++ b/RAGChatbot/ui/index.html @@ -0,0 +1,14 @@ + + + + + + + RAG Chatbot + + +

+ + + + diff --git a/RAGChatbot/ui/package.json b/RAGChatbot/ui/package.json new file mode 100644 index 0000000000..c4249ab4f3 --- /dev/null +++ b/RAGChatbot/ui/package.json @@ -0,0 +1,32 @@ +{ + "name": "rag-chatbot-ui", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "axios": "^1.6.0", + "lucide-react": "^0.294.0" + }, + "devDependencies": { + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.16", + "eslint": "^8.55.0", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.5", + "postcss": "^8.4.32", + "tailwindcss": "^3.3.6", + "vite": "^5.0.8" + } +} + diff --git a/RAGChatbot/ui/postcss.config.js b/RAGChatbot/ui/postcss.config.js new file mode 100644 index 0000000000..b4a6220e2d --- /dev/null +++ b/RAGChatbot/ui/postcss.config.js @@ -0,0 +1,7 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} + diff --git a/RAGChatbot/ui/src/App.jsx b/RAGChatbot/ui/src/App.jsx new file mode 100644 index 0000000000..42ab09fd23 --- /dev/null +++ b/RAGChatbot/ui/src/App.jsx @@ -0,0 +1,77 @@ +import { useState } from 'react' +import ChatInterface from './components/ChatInterface' +import PDFUploader from './components/PDFUploader' +import Header from './components/Header' +import StatusBar from './components/StatusBar' + +function App() { + const [documentUploaded, setDocumentUploaded] = useState(false) + const [documentName, setDocumentName] = useState('') + const [uploadProgress, setUploadProgress] = useState(0) + const [isUploading, setIsUploading] = useState(false) + + const handleUploadSuccess = (fileName, numChunks) => { + setDocumentUploaded(true) + setDocumentName(fileName) + setUploadProgress(100) + setTimeout(() => { + setIsUploading(false) + setUploadProgress(0) + }, 1000) + } + + const handleUploadStart = () => { + setIsUploading(true) + setUploadProgress(0) + } + + const handleUploadProgress = (progress) => { + setUploadProgress(progress) + } + + const handleReset = () => { + setDocumentUploaded(false) + setDocumentName('') + setUploadProgress(0) + } + + return ( +
+
+ +
+ {/* Status Bar */} + + +
+ {/* Left Panel - PDF Upload */} +
+ +
+ + {/* Right Panel - Chat Interface */} +
+ +
+
+
+
+ ) +} + +export default App + diff --git a/RAGChatbot/ui/src/components/ChatInterface.jsx b/RAGChatbot/ui/src/components/ChatInterface.jsx new file mode 100644 index 0000000000..ee26d0a02a --- /dev/null +++ b/RAGChatbot/ui/src/components/ChatInterface.jsx @@ -0,0 +1,184 @@ +import { useState, useRef, useEffect } from 'react' +import { Send, Bot, User, AlertCircle } from 'lucide-react' +import { queryDocument } from '../services/api' + +export default function ChatInterface({ documentUploaded, documentName }) { + const [messages, setMessages] = useState([]) + const [input, setInput] = useState('') + const [isLoading, setIsLoading] = useState(false) + const messagesEndRef = useRef(null) + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }) + } + + useEffect(() => { + scrollToBottom() + }, [messages]) + + useEffect(() => { + // Reset messages when document changes + if (documentUploaded) { + setMessages([ + { + type: 'bot', + content: `Document "${documentName}" has been uploaded successfully! You can now ask me questions about it.`, + timestamp: new Date() + } + ]) + } else { + setMessages([]) + } + }, [documentUploaded, documentName]) + + const handleSubmit = async (e) => { + e.preventDefault() + + if (!input.trim() || !documentUploaded) return + + const userMessage = { + type: 'user', + content: input, + timestamp: new Date() + } + + setMessages(prev => [...prev, userMessage]) + setInput('') + setIsLoading(true) + + try { + const response = await queryDocument(input) + + const botMessage = { + type: 'bot', + content: response.answer, + timestamp: new Date() + } + + setMessages(prev => [...prev, botMessage]) + } catch (error) { + const errorMessage = { + type: 'error', + content: error.message || 'Failed to get response. Please try again.', + timestamp: new Date() + } + setMessages(prev => [...prev, errorMessage]) + } finally { + setIsLoading(false) + } + } + + return ( +
+ {/* Chat Header */} +
+

+ + Chat Assistant +

+

+ {documentUploaded + ? 'Ask questions about your document' + : 'Upload a document to start chatting'} +

+
+ + {/* Messages Container */} +
+ {!documentUploaded && messages.length === 0 && ( +
+
+ +

Upload a PDF document to start chatting

+
+
+ )} + + {messages.map((message, index) => ( +
+
+ {message.type === 'user' ? ( + + ) : message.type === 'error' ? ( + + ) : ( + + )} +
+ +
+
+

{message.content}

+
+

+ {message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} +

+
+
+ ))} + + {isLoading && ( +
+
+ +
+
+
+
+
+
+
+
+
+ )} + +
+
+ + {/* Input Form */} +
+
+ setInput(e.target.value)} + placeholder={documentUploaded ? "Ask a question..." : "Upload a document first..."} + disabled={!documentUploaded || isLoading} + className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed" + /> + +
+

+ Press Enter to send • The AI will answer based on your uploaded document +

+
+
+ ) +} + diff --git a/RAGChatbot/ui/src/components/Header.jsx b/RAGChatbot/ui/src/components/Header.jsx new file mode 100644 index 0000000000..0cfd2b11ce --- /dev/null +++ b/RAGChatbot/ui/src/components/Header.jsx @@ -0,0 +1,28 @@ +import { MessageSquare, FileText } from 'lucide-react' + +export default function Header() { + return ( +
+
+
+
+
+ +
+
+

+ RAG Chatbot +

+

Ask questions about your documents

+
+
+ +
+ +
+
+
+
+ ) +} + diff --git a/RAGChatbot/ui/src/components/PDFUploader.jsx b/RAGChatbot/ui/src/components/PDFUploader.jsx new file mode 100644 index 0000000000..e6a549f42e --- /dev/null +++ b/RAGChatbot/ui/src/components/PDFUploader.jsx @@ -0,0 +1,155 @@ +import { useState, useRef } from 'react' +import { Upload, FileText, CheckCircle, AlertCircle } from 'lucide-react' +import { uploadPDF } from '../services/api' + +export default function PDFUploader({ onUploadSuccess, onUploadStart, onUploadProgress, documentUploaded }) { + const [dragActive, setDragActive] = useState(false) + const [error, setError] = useState('') + const fileInputRef = useRef(null) + + const handleDrag = (e) => { + e.preventDefault() + e.stopPropagation() + if (e.type === "dragenter" || e.type === "dragover") { + setDragActive(true) + } else if (e.type === "dragleave") { + setDragActive(false) + } + } + + const handleDrop = async (e) => { + e.preventDefault() + e.stopPropagation() + setDragActive(false) + + if (e.dataTransfer.files && e.dataTransfer.files[0]) { + await handleFile(e.dataTransfer.files[0]) + } + } + + const handleChange = async (e) => { + e.preventDefault() + if (e.target.files && e.target.files[0]) { + await handleFile(e.target.files[0]) + } + } + + const handleFile = async (file) => { + setError('') + + // Validate file type + if (!file.name.endsWith('.pdf')) { + setError('Please upload a PDF file') + return + } + + // Validate file size (50MB) + if (file.size > 50 * 1024 * 1024) { + setError('File size must be less than 50MB') + return + } + + onUploadStart() + + try { + // Simulate progress + onUploadProgress(30) + + const result = await uploadPDF(file) + + onUploadProgress(90) + + onUploadSuccess(file.name, result.num_chunks) + setError('') + } catch (err) { + setError(err.message || 'Failed to upload file') + onUploadProgress(0) + } + } + + const handleButtonClick = () => { + fileInputRef.current?.click() + } + + return ( +
+
+

+ + Upload Document +

+

+ Upload a PDF to start asking questions +

+
+ +
+ + + {documentUploaded ? ( +
+ +

Document uploaded successfully!

+ +
+ ) : ( +
+ +
+

Drop your PDF here

+

or

+
+ +

PDF files only, max 50MB

+
+ )} +
+ + {error && ( +
+ +

{error}

+
+ )} + +
+

Instructions:

+
    +
  • Upload a PDF document (max 50MB)
  • +
  • Wait for processing to complete
  • +
  • Start asking questions in the chat
  • +
  • Get intelligent answers based on your document
  • +
+
+
+ ) +} + diff --git a/RAGChatbot/ui/src/components/StatusBar.jsx b/RAGChatbot/ui/src/components/StatusBar.jsx new file mode 100644 index 0000000000..2957d014c5 --- /dev/null +++ b/RAGChatbot/ui/src/components/StatusBar.jsx @@ -0,0 +1,54 @@ +import { CheckCircle, AlertCircle, Loader, Trash2 } from 'lucide-react' + +export default function StatusBar({ documentUploaded, documentName, isUploading, uploadProgress, onReset }) { + return ( +
+
+
+ {isUploading && ( + <> + +
+

Uploading and processing...

+
+
+
+
+ + )} + + {!isUploading && documentUploaded && ( + <> + +
+

Document Ready

+

{documentName}

+
+ + )} + + {!isUploading && !documentUploaded && ( + <> + +

No document uploaded

+ + )} +
+ + {documentUploaded && !isUploading && ( + + )} +
+
+ ) +} + diff --git a/RAGChatbot/ui/src/index.css b/RAGChatbot/ui/src/index.css new file mode 100644 index 0000000000..a523f51e54 --- /dev/null +++ b/RAGChatbot/ui/src/index.css @@ -0,0 +1,34 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} + +@layer utilities { + .scrollbar-hide::-webkit-scrollbar { + display: none; + } + + .scrollbar-hide { + -ms-overflow-style: none; + scrollbar-width: none; + } +} + diff --git a/RAGChatbot/ui/src/main.jsx b/RAGChatbot/ui/src/main.jsx new file mode 100644 index 0000000000..299bc52310 --- /dev/null +++ b/RAGChatbot/ui/src/main.jsx @@ -0,0 +1,11 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.jsx' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')).render( + + + , +) + diff --git a/RAGChatbot/ui/src/services/api.js b/RAGChatbot/ui/src/services/api.js new file mode 100644 index 0000000000..87ffb0667d --- /dev/null +++ b/RAGChatbot/ui/src/services/api.js @@ -0,0 +1,85 @@ +import axios from 'axios' + +// API base URL - uses Vite proxy in development (proxies to localhost:5000) +const API_BASE_URL = import.meta.env.VITE_API_URL || '/api' + +const api = axios.create({ + baseURL: API_BASE_URL, + headers: { + 'Content-Type': 'application/json', + }, +}) + +/** + * Upload a PDF file to the API + * @param {File} file - The PDF file to upload + * @returns {Promise} Response with upload status and chunk count + */ +export const uploadPDF = async (file) => { + const formData = new FormData() + formData.append('file', file) + + try { + const response = await api.post('/upload-pdf', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }) + return response.data + } catch (error) { + console.error('Upload error:', error) + throw new Error( + error.response?.data?.detail || 'Failed to upload PDF. Please try again.' + ) + } +} + +/** + * Query the uploaded document + * @param {string} query - The question to ask + * @returns {Promise} Response with the answer + */ +export const queryDocument = async (query) => { + try { + const response = await api.post('/query', { query }) + return response.data + } catch (error) { + console.error('Query error:', error) + throw new Error( + error.response?.data?.detail || 'Failed to get response. Please try again.' + ) + } +} + +/** + * Check API health + * @returns {Promise} Health status + */ +export const checkHealth = async () => { + try { + const response = await api.get('/health') + return response.data + } catch (error) { + console.error('Health check error:', error) + throw new Error('API is not available') + } +} + +/** + * Delete the vector store + * @returns {Promise} Deletion status + */ +export const deleteVectorStore = async () => { + try { + const response = await api.delete('/vectorstore') + return response.data + } catch (error) { + console.error('Delete error:', error) + throw new Error( + error.response?.data?.detail || 'Failed to delete vector store' + ) + } +} + +export default api + diff --git a/RAGChatbot/ui/tailwind.config.js b/RAGChatbot/ui/tailwind.config.js new file mode 100644 index 0000000000..037cbd7203 --- /dev/null +++ b/RAGChatbot/ui/tailwind.config.js @@ -0,0 +1,27 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}", + ], + theme: { + extend: { + colors: { + primary: { + 50: '#f0f9ff', + 100: '#e0f2fe', + 200: '#bae6fd', + 300: '#7dd3fc', + 400: '#38bdf8', + 500: '#0ea5e9', + 600: '#0284c7', + 700: '#0369a1', + 800: '#075985', + 900: '#0c4a6e', + } + } + }, + }, + plugins: [], +} + diff --git a/RAGChatbot/ui/vite.config.js b/RAGChatbot/ui/vite.config.js new file mode 100644 index 0000000000..a20f408af9 --- /dev/null +++ b/RAGChatbot/ui/vite.config.js @@ -0,0 +1,18 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + server: { + host: true, + port: 3000, + proxy: { + '/api': { + target: 'http://backend:5001', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api/, '') + } + } + } +}) + From 3a2f45828261b8b8f70ef1d26592bf61f8780304 Mon Sep 17 00:00:00 2001 From: Gopalrajdev Date: Thu, 29 Jan 2026 15:06:50 -0800 Subject: [PATCH 2/2] add local URL support with docker package-lock removal and documentation update --- RAGChatbot/.gitignore | 42 +++++- RAGChatbot/README.md | 130 +++++++++++++------ RAGChatbot/TROUBLESHOOTING.md | 94 +++++++++++++- RAGChatbot/api/.env.example | 22 ++++ RAGChatbot/api/config.py | 20 ++- RAGChatbot/api/models.py | 2 +- RAGChatbot/api/server.py | 8 +- RAGChatbot/api/services/api_client.py | 53 ++------ RAGChatbot/api/services/retrieval_service.py | 8 +- RAGChatbot/api/services/vector_service.py | 8 +- RAGChatbot/docker-compose.yml | 2 + RAGChatbot/ui/Dockerfile | 3 +- 12 files changed, 283 insertions(+), 109 deletions(-) create mode 100644 RAGChatbot/api/.env.example diff --git a/RAGChatbot/.gitignore b/RAGChatbot/.gitignore index 1972fc44d8..31703cdcff 100644 --- a/RAGChatbot/.gitignore +++ b/RAGChatbot/.gitignore @@ -1,2 +1,42 @@ +# Environment files **/.env -**/test.txt \ No newline at end of file + +# Test files +**/test.txt + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +*.egg-info/ +dist/ +build/ + +# Virtual environments +venv/ +env/ +ENV/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Application specific +dmv_index/ +*.log + +# Node.js +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +package-lock.json \ No newline at end of file diff --git a/RAGChatbot/README.md b/RAGChatbot/README.md index a4e774009f..5d264b6d80 100644 --- a/RAGChatbot/README.md +++ b/RAGChatbot/README.md @@ -12,6 +12,7 @@ The system integrates a FastAPI backend powered by LangChain, FAISS, and AI mode - [Quick Start Deployment](#quick-start-deployment) - [User Interface](#user-interface) - [Troubleshooting](#troubleshooting) +- [Additional Info](#additional-info) --- @@ -29,7 +30,7 @@ The **RAG Chatbot** demonstrates how retrieval-augmented generation can be used - LangChain-powered document processing - FAISS-CPU vector store for efficient similarity search - Enterprise inference endpoints for embeddings and LLM -- Keycloak authentication for secure API access +- Token-based authentication for inference API - Comprehensive error handling and logging - File validation and size limits - CORS enabled for web integration @@ -77,7 +78,36 @@ Below is the architecture as it consists of a server that waits for documents to Before you begin, ensure you have the following installed: - **Docker and Docker Compose** -- **Enterprise inference endpoint access** (Keycloak authentication) +- **Enterprise inference endpoint access** (token-based authentication) + +### Required API Configuration + +**For Inference Service (RAG Chatbot):** + +This application supports multiple inference deployment patterns: + +- **GenAI Gateway**: Provide your GenAI Gateway URL and API key +- **APISIX Gateway**: Provide your APISIX Gateway URL and authentication token + +Configuration requirements: +- INFERENCE_API_ENDPOINT: URL to your inference service (GenAI Gateway, APISIX Gateway, etc.) +- INFERENCE_API_TOKEN: Authentication token/API key for your chosen service + +### Local Development Configuration + +**For Local Testing Only (Optional)** + +If you're testing with a local inference endpoint using a custom domain (e.g., `inference.example.com` mapped to localhost in your hosts file): + +1. Edit `api/.env` and set: + ```bash + LOCAL_URL_ENDPOINT=inference.example.com + ``` + (Use the domain name from your INFERENCE_API_ENDPOINT without `https://`) + +2. This allows Docker containers to resolve your local domain correctly. + +**Note:** For public domains or cloud-hosted endpoints, leave the default value `not-needed`. ### Verify Docker Installation @@ -91,6 +121,7 @@ docker compose version # Verify Docker is running docker ps ``` +--- ## Quick Start Deployment @@ -103,56 +134,70 @@ cd GenAIExamples/RAGChatbot ### Set up the Environment -This application requires an `.env` file in the `api` directory for proper configuration. Create it with the commands below: +This application requires **two `.env` files** for proper configuration: + +1. **Root `.env` file** (for Docker Compose variables) +2. **`api/.env` file** (for backend application configuration) + +#### Step 1: Create Root `.env` File ```bash -# Create the .env file in the api directory -mkdir -p api -cat > api/.env << EOF -# Backend API URL (accessible from frontend) -VITE_API_URL=https://backend:5000 - -# Required - Enterprise/Keycloak Configuration -BASE_URL=https://api.example.com -KEYCLOAK_REALM=master -KEYCLOAK_CLIENT_ID=api -KEYCLOAK_CLIENT_SECRET=your_client_secret - -# Required - Model Configuration -EMBEDDING_MODEL_ENDPOINT=bge-base-en-v1.5 -INFERENCE_MODEL_ENDPOINT=Llama-3.1-8B-Instruct -EMBEDDING_MODEL_NAME=bge-base-en-v1.5 -INFERENCE_MODEL_NAME=meta-llama/Llama-3.1-8B-Instruct +# From the RAGChatbot directory +cat > .env << EOF +# Docker Compose Configuration +LOCAL_URL_ENDPOINT=not-needed EOF ``` -Or manually create `api/.env` with: +**Note:** If using a local domain (e.g., `inference.example.com` mapped to localhost), replace `not-needed` with your domain name (without `https://`). + +#### Step 2: Create `api/.env` File + +You can either copy from the example file: ```bash -# Backend API URL (accessible from frontend) -VITE_API_URL=https://backend:5000 - -# Required - Enterprise/Keycloak Configuration -BASE_URL=https://api.example.com -KEYCLOAK_REALM=master -KEYCLOAK_CLIENT_ID=api -KEYCLOAK_CLIENT_SECRET=your_client_secret - -# Required - Model Configuration -EMBEDDING_MODEL_ENDPOINT=bge-base-en-v1.5 -INFERENCE_MODEL_ENDPOINT=Llama-3.1-8B-Instruct +cp api/.env.example api/.env +``` + +Then edit `api/.env` with your actual credentials, **OR** create it directly: + +```bash +mkdir -p api +cat > api/.env << EOF +# Inference API Configuration +# INFERENCE_API_ENDPOINT: URL to your inference service (without /v1 suffix) +# - For GenAI Gateway: https://genai-gateway.example.com +# - For APISIX Gateway: https://apisix-gateway.example.com/inference +INFERENCE_API_ENDPOINT=https://your-actual-api-endpoint.com +INFERENCE_API_TOKEN=your-actual-token-here + +# Model Configuration +# IMPORTANT: Use the full model names as they appear in your inference service +# Check available models: curl https://your-api-endpoint.com/v1/models -H "Authorization: Bearer your-token" EMBEDDING_MODEL_NAME=bge-base-en-v1.5 INFERENCE_MODEL_NAME=meta-llama/Llama-3.1-8B-Instruct + +# Local URL Endpoint (for Docker) +LOCAL_URL_ENDPOINT=not-needed +EOF ``` -**Note**: The docker-compose.yml file automatically loads environment variables from `./api/.env` for the backend service. +**Important Configuration Notes:** + +- **INFERENCE_API_ENDPOINT**: Your actual inference service URL (replace `https://your-actual-api-endpoint.com`) +- **INFERENCE_API_TOKEN**: Your actual pre-generated authentication token +- **EMBEDDING_MODEL_NAME** and **INFERENCE_MODEL_NAME**: Use the exact model names from your inference service + - To check available models: `curl https://your-api-endpoint.com/v1/models -H "Authorization: Bearer your-token"` +- **LOCAL_URL_ENDPOINT**: Only needed if using local domain mapping (see [Local Development Configuration](#local-development-configuration)) + +**Note**: The docker-compose.yml file automatically loads environment variables from both `.env` (root) and `./api/.env` (backend) files. ### Running the Application Start both API and UI services together with Docker Compose: ```bash -# From the rag-chatbot directory +# From the RAGChatbot directory docker compose up --build # Or run in detached mode (background) @@ -189,7 +234,7 @@ docker compose ps **Using the Application** -Make sure you are at the localhost:3000 url +Make sure you are at the `http://localhost:3000` URL You will be directed to the main page which has each feature @@ -215,7 +260,6 @@ For production deployments, you may want to configure a reverse proxy or update ### Stopping the Application - ```bash docker compose down ``` @@ -225,3 +269,15 @@ docker compose down For comprehensive troubleshooting guidance, common issues, and solutions, refer to: [Troubleshooting Guide - TROUBLESHOOTING.md](./TROUBLESHOOTING.md) + +--- + +## Additional Info + +The following models have been validated with RAGChatbot: + +| Model | Hardware | +|-------|----------| +| **meta-llama/Llama-3.1-8B-Instruct** | Gaudi | +| **BAAI/bge-base-en-v1.5** (embeddings) | Gaudi | +| **Qwen/Qwen3-4B-Instruct** | Xeon | diff --git a/RAGChatbot/TROUBLESHOOTING.md b/RAGChatbot/TROUBLESHOOTING.md index 34b352991e..a4d2142b0c 100644 --- a/RAGChatbot/TROUBLESHOOTING.md +++ b/RAGChatbot/TROUBLESHOOTING.md @@ -4,17 +4,105 @@ This document contains all common issues encountered during development and thei ## Table of Contents +- [Docker Compose Issues](#docker-compose-issues) - [API Common Issues](#api-common-issues) - [UI Common Issues](#ui-common-issues) -### API Common Issues +## Docker Compose Issues -#### "OPENAI_API_KEY not found in environment variables" +### Error: "LOCAL_URL_ENDPOINT variable is not set" + +**Problem**: +``` +level=warning msg="The \"LOCAL_URL_ENDPOINT\" variable is not set. Defaulting to a blank string." +decoding failed due to the following error(s): +'services[backend].extra_hosts' bad host name '' +``` + +**Solution**: + +1. Create a `.env` file in the **root** `rag-chatbot` directory (not in `api/`): + ```bash + echo "LOCAL_URL_ENDPOINT=not-needed" > .env + ``` +2. If using a local domain (e.g., `inference.example.com`), replace `not-needed` with your domain name (without `https://`) +3. Restart Docker Compose: `docker compose down && docker compose up` + +### Error: "404 Not Found" when uploading PDF + +**Problem**: +``` +HTTP Request: POST https://api.example.com/BAAI/bge-base-en-v1.5/v1/embeddings "HTTP/1.1 404 Not Found" +openai.NotFoundError: Error code: 404 - {'detail': 'Not Found'} +``` + +**Solution**: + +1. Verify your `api/.env` file has the **correct** API endpoint (not the placeholder): + ```bash + INFERENCE_API_ENDPOINT=https://your-actual-api-endpoint.com + INFERENCE_API_TOKEN=your-actual-token-here + ``` + +2. Check available models on your inference service: + ```bash + curl https://your-api-endpoint.com/v1/models \ + -H "Authorization: Bearer your-token" + ``` + +3. Update model names to match the exact names from your API: + ```bash + EMBEDDING_MODEL_NAME=BAAI/bge-base-en-v1.5 + INFERENCE_MODEL_NAME=Qwen/Qwen3-4B-Instruct-2507 + ``` + +4. Restart containers: `docker compose down && docker compose up --build` + +### Containers fail to start + +**Problem**: Docker containers won't start or crash immediately + +**Solution**: + +1. Check logs for specific errors: + ```bash + docker compose logs backend + docker compose logs frontend + ``` + +2. Ensure ports 5001 and 3000 are available: + ```bash + # Windows + netstat -ano | findstr :5001 + netstat -ano | findstr :3000 + + # Unix/Mac + lsof -i :5001 + lsof -i :3000 + ``` + +3. Clean up and rebuild: + ```bash + docker compose down -v + docker compose up --build + ``` + +4. Restart Docker Desktop if issues persist + +## API Common Issues + +#### "INFERENCE_API_ENDPOINT and INFERENCE_API_TOKEN must be set" **Solution**: 1. Create a `.env` file in the `api` directory -2. Add your OpenAI API key: `OPENAI_API_KEY=your_key_here` +2. Add your inference configuration: + ```bash + INFERENCE_API_ENDPOINT=https://your-actual-api-endpoint.com + INFERENCE_API_TOKEN=your-actual-token-here + EMBEDDING_MODEL_NAME=BAAI/bge-base-en-v1.5 + INFERENCE_MODEL_NAME=Qwen/Qwen3-4B-Instruct-2507 + ``` 3. Restart the server #### "No documents uploaded" diff --git a/RAGChatbot/api/.env.example b/RAGChatbot/api/.env.example new file mode 100644 index 0000000000..b632f68128 --- /dev/null +++ b/RAGChatbot/api/.env.example @@ -0,0 +1,22 @@ +# Inference API Configuration +# INFERENCE_API_ENDPOINT: URL to your inference service (without /v1 suffix) +# - For GenAI Gateway: https://genai-gateway.example.com +# - For APISIX Gateway: https://apisix-gateway.example.com/inference +# +# INFERENCE_API_TOKEN: Authentication token/API key for the inference service +# - For GenAI Gateway: Your GenAI Gateway API key +# - For APISIX Gateway: Your APISIX authentication token +INFERENCE_API_ENDPOINT=https://api.example.com +INFERENCE_API_TOKEN=your-pre-generated-token-here + +# Model Configuration +# IMPORTANT: Use the full model names as they appear in your inference service +# Check available models: curl https://your-api-endpoint.com/v1/models -H "Authorization: Bearer your-token" +EMBEDDING_MODEL_NAME=BAAI/bge-base-en-v1.5 +INFERENCE_MODEL_NAME=meta-llama/Llama-3.1-8B-Instruct + +# Local URL Endpoint (only needed for non-public domains) +# If using a local domain like inference.example.com mapped to localhost: +# Set this to: inference.example.com (domain without https://) +# If using a public domain, set any placeholder value like: not-needed +LOCAL_URL_ENDPOINT=not-needed diff --git a/RAGChatbot/api/config.py b/RAGChatbot/api/config.py index 190375c0f3..3ca2f2addc 100644 --- a/RAGChatbot/api/config.py +++ b/RAGChatbot/api/config.py @@ -8,24 +8,20 @@ # Load environment variables from .env file load_dotenv() -# API Configuration -OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") - -# Custom API Configuration -BASE_URL = os.getenv("BASE_URL", "https://api.example.com") -KEYCLOAK_REALM = os.getenv("KEYCLOAK_REALM", "master") -KEYCLOAK_CLIENT_ID = os.getenv("KEYCLOAK_CLIENT_ID", "api") -KEYCLOAK_CLIENT_SECRET = os.getenv("KEYCLOAK_CLIENT_SECRET") +# Inference API Configuration +# Supports multiple inference deployment patterns: +# - GenAI Gateway: Provide your GenAI Gateway URL and API key +# - APISIX Gateway: Provide your APISIX Gateway URL and authentication token +INFERENCE_API_ENDPOINT = os.getenv("INFERENCE_API_ENDPOINT", "https://api.example.com") +INFERENCE_API_TOKEN = os.getenv("INFERENCE_API_TOKEN") # Model Configuration -EMBEDDING_MODEL_ENDPOINT = os.getenv("EMBEDDING_MODEL_ENDPOINT", "bge-base-en-v1.5") -INFERENCE_MODEL_ENDPOINT = os.getenv("INFERENCE_MODEL_ENDPOINT", "Llama-3.1-8B-Instruct") EMBEDDING_MODEL_NAME = os.getenv("EMBEDDING_MODEL_NAME", "bge-base-en-v1.5") INFERENCE_MODEL_NAME = os.getenv("INFERENCE_MODEL_NAME", "meta-llama/Llama-3.1-8B-Instruct") # Validate required configuration -if not OPENAI_API_KEY and not KEYCLOAK_CLIENT_SECRET: - raise ValueError("Either OPENAI_API_KEY or KEYCLOAK_CLIENT_SECRET must be set in environment variables") +if not INFERENCE_API_ENDPOINT or not INFERENCE_API_TOKEN: + raise ValueError("INFERENCE_API_ENDPOINT and INFERENCE_API_TOKEN must be set in environment variables") # Application Settings APP_TITLE = "RAG QnA Chatbot" diff --git a/RAGChatbot/api/models.py b/RAGChatbot/api/models.py index 1daafe9e89..ae9452a81c 100644 --- a/RAGChatbot/api/models.py +++ b/RAGChatbot/api/models.py @@ -51,7 +51,7 @@ class HealthResponse(BaseModel): """Response model for health check""" status: str = Field(..., description="Health status") vectorstore_available: bool = Field(..., description="Whether vectorstore is loaded") - openai_key_configured: bool = Field(..., description="Whether OpenAI key is configured") + openai_key_configured: bool = Field(..., description="Whether inference API token is configured") class DeleteResponse(BaseModel): diff --git a/RAGChatbot/api/server.py b/RAGChatbot/api/server.py index 1aac509da1..3186c48bc3 100644 --- a/RAGChatbot/api/server.py +++ b/RAGChatbot/api/server.py @@ -32,7 +32,7 @@ async def lifespan(app: FastAPI): """Lifespan context manager for FastAPI app""" # Startup - app.state.vectorstore = load_vector_store(config.OPENAI_API_KEY) + app.state.vectorstore = load_vector_store(config.INFERENCE_API_TOKEN) if app.state.vectorstore: logger.info("✓ FAISS vector store loaded successfully") else: @@ -81,7 +81,7 @@ def health_check(): return HealthResponse( status="healthy", vectorstore_available=app.state.vectorstore is not None, - openai_key_configured=bool(config.OPENAI_API_KEY) + openai_key_configured=bool(config.INFERENCE_API_TOKEN) ) @@ -132,7 +132,7 @@ async def upload_pdf(file: UploadFile = File(...)): ) # Create embeddings and store in FAISS - vectorstore = store_in_vector_storage(chunks, config.OPENAI_API_KEY) + vectorstore = store_in_vector_storage(chunks, config.INFERENCE_API_TOKEN) # Update app state app.state.vectorstore = vectorstore @@ -186,7 +186,7 @@ def query_endpoint(request: QueryRequest): result = query_documents( request.query, app.state.vectorstore, - config.OPENAI_API_KEY + config.INFERENCE_API_TOKEN ) return QueryResponse(**result) except Exception as e: diff --git a/RAGChatbot/api/services/api_client.py b/RAGChatbot/api/services/api_client.py index 111c737a9a..8b00942b40 100644 --- a/RAGChatbot/api/services/api_client.py +++ b/RAGChatbot/api/services/api_client.py @@ -14,67 +14,38 @@ class APIClient: """ - Client for handling authentication and API calls + Client for handling API calls with token-based authentication """ - + def __init__(self): - self.base_url = config.BASE_URL - self.token = None - self.http_client = None - self._authenticate() - - def _authenticate(self) -> None: - """ - Authenticate and obtain access token from Keycloak - """ - token_url = f"{self.base_url}/token" - payload = { - "grant_type": "client_credentials", - "client_id": config.KEYCLOAK_CLIENT_ID, - "client_secret": config.KEYCLOAK_CLIENT_SECRET, - } - - try: - response = requests.post(token_url, data=payload, verify=False) - - if response.status_code == 200: - self.token = response.json().get("access_token") - logger.info(f"✓ Access token obtained: {self.token[:20]}..." if self.token else "Failed to get token") - - # Create httpx client with SSL verification disabled (like -k in curl) - self.http_client = httpx.Client(verify=False) - - else: - logger.error(f"Error obtaining token: {response.status_code} - {response.text}") - raise Exception(f"Authentication failed: {response.status_code}") - - except Exception as e: - logger.error(f"Error during authentication: {str(e)}") - raise + self.base_url = config.INFERENCE_API_ENDPOINT + self.token = config.INFERENCE_API_TOKEN + self.http_client = httpx.Client(verify=False) + logger.info(f"✓ API Client initialized with endpoint: {self.base_url}") def get_embedding_client(self): """ Get OpenAI-style client for embeddings - Uses bge-base-en-v1.5 endpoint + Uses bge-base-en-v1.5 model """ from openai import OpenAI - + return OpenAI( api_key=self.token, - base_url=f"{self.base_url}/{config.EMBEDDING_MODEL_ENDPOINT}/v1", + base_url=f"{self.base_url}/v1", http_client=self.http_client ) def get_inference_client(self): """ Get OpenAI-style client for inference/completions - Uses Llama-3.1-8B-Instruct endpoint + Uses Llama-3.1-8B-Instruct model """ from openai import OpenAI - + return OpenAI( api_key=self.token, - base_url=f"{self.base_url}/{config.INFERENCE_MODEL_ENDPOINT}/v1", + base_url=f"{self.base_url}/v1", http_client=self.http_client ) diff --git a/RAGChatbot/api/services/retrieval_service.py b/RAGChatbot/api/services/retrieval_service.py index a5efe82252..15e4862abd 100644 --- a/RAGChatbot/api/services/retrieval_service.py +++ b/RAGChatbot/api/services/retrieval_service.py @@ -97,15 +97,15 @@ def _generate( def get_llm(api_key: str) -> BaseChatModel: """ Get LLM instance (ChatOpenAI or CustomChatModel based on config) - + Args: api_key: API key - + Returns: LLM instance """ - # Check if using custom API - if hasattr(config, 'KEYCLOAK_CLIENT_SECRET') and config.KEYCLOAK_CLIENT_SECRET: + # Check if using custom inference endpoint + if hasattr(config, 'INFERENCE_API_TOKEN') and config.INFERENCE_API_TOKEN: return CustomChatModel() else: # Fallback to OpenAI ChatOpenAI diff --git a/RAGChatbot/api/services/vector_service.py b/RAGChatbot/api/services/vector_service.py index 516c969bd0..0884a9c0db 100644 --- a/RAGChatbot/api/services/vector_service.py +++ b/RAGChatbot/api/services/vector_service.py @@ -56,15 +56,15 @@ def embed_query(self, text: str) -> list[float]: def get_embeddings(api_key: str) -> Embeddings: """ Create embeddings instance - + Args: api_key: API key (for compatibility, not used with custom endpoint) - + Returns: Embeddings instance (CustomEmbeddings if using custom API, OpenAIEmbeddings otherwise) """ - # Check if using custom API - if hasattr(config, 'KEYCLOAK_CLIENT_SECRET') and config.KEYCLOAK_CLIENT_SECRET: + # Check if using custom inference endpoint + if hasattr(config, 'INFERENCE_API_TOKEN') and config.INFERENCE_API_TOKEN: return CustomEmbeddings() else: # Fallback to OpenAI diff --git a/RAGChatbot/docker-compose.yml b/RAGChatbot/docker-compose.yml index 020d0c1656..e0f1382da5 100644 --- a/RAGChatbot/docker-compose.yml +++ b/RAGChatbot/docker-compose.yml @@ -13,6 +13,8 @@ services: - ./api:/app networks: - app_network + extra_hosts: + - "${LOCAL_URL_ENDPOINT}:host-gateway" restart: unless-stopped diff --git a/RAGChatbot/ui/Dockerfile b/RAGChatbot/ui/Dockerfile index 8a5acb3a82..7dab0c57a7 100644 --- a/RAGChatbot/ui/Dockerfile +++ b/RAGChatbot/ui/Dockerfile @@ -3,9 +3,8 @@ FROM node:18 # Set the working directory WORKDIR /app -# Copy package.json and package-lock.json +# Copy package.json COPY package.json ./ -COPY package-lock.json ./ # Install dependencies RUN npm install