Skip to the content.

← Back to Chat with RAG Home

Development Guide

About this document

This page covers development workflows, testing, and contribution guidelines for the Chat-with-RAG project, including setup, debugging, and best practices.

Note: If you landed here directly (for example from documentation hosting or search), start with the repository README to see how to run the system locally and try the interactive demo.

Table of Contents


Development Setup

Prerequisites

Local Development Environment

1. Clone and Setup

# Clone repository
git clone https://github.com/vrraj/chat-with-rag.git
cd chat-with-rag

# Create virtual environment
python3 -m venv venv
source venv/bin/activate  # Windows: venv\Scripts\activate

# Install dependencies
pip install -r requirements.txt

# Setup environment
cp .env.example .env
# Edit .env with your API keys

2. Install Development Dependencies

# Install development tools
pip install black pylint pytest pytest-cov mypy

# Install pre-commit (optional)
pip install pre-commit

# Setup pre-commit hooks (optional)
pre-commit install
pre-commit run --all-files

3. Development Docker Setup

Create docker-compose.dev.yml:

version: '3.8'

services:
  app:
    build: .
    ports:
      - "8000:8000"
    environment:
      - DEBUG_VERBOSE=true
      - SHOW_PROCESSING_STEPS=true
    env_file:
      - .env
    volumes:
      - .:/app  # Mount source code
      - /app/venv  # Exclude venv
    depends_on:
      - qdrant
    command: python run.py  # Use run.py for hot reload

  qdrant:
    image: qdrant/qdrant:v1.14.1
    ports:
      - "6333:6333"
    volumes:
      - qdrant_dev_data:/qdrant/storage

volumes:
  qdrant_dev_data:

4. Start Development Environment

# Start services with hot reload
docker-compose -f docker-compose.dev.yml up

# Or run locally
make start
python run.py  # Hot reload enabled

IDE Setup

VS Code Configuration

Create .vscode/settings.json:

{
  "python.defaultInterpreterPath": "./venv/bin/python",
  "python.linting.enabled": true,
  "python.linting.pylintEnabled": true,
  "python.formatting.provider": "black",
  "python.formatting.blackArgs": ["--line-length", "100"],
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.organizeImports": true
  },
  "files.exclude": {
    "**/__pycache__": true,
    "**/*.pyc": true,
    ".pytest_cache": true,
    ".coverage": true
  }
}

Create .vscode/launch.json:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Python: FastAPI",
      "type": "python",
      "request": "launch",
      "program": "start.py",
      "console": "integratedTerminal",
      "env": {
        "PYTHONPATH": "${workspaceFolder}"
      }
    },
    {
      "name": "Python: Tests",
      "type": "python",
      "request": "launch",
      "module": "pytest",
      "args": ["tests/", "-v"],
      "console": "integratedTerminal"
    }
  ]
}

Code Organization

Project Structure

chat-with-rag/
├── backend/
│   ├── api/                 # API endpoints
│   ├── chat/               # Chat orchestration
│   ├── core/               # Core configuration and schemas
│   ├── crawler/            # Web crawling
│   ├── embeddings/         # Embedding management
│   ├── llm/                # LLM client abstraction
│   ├── scripts/            # Utility scripts
│   └── main.py             # FastAPI application
├── frontend/
│   ├── static/             # JavaScript, CSS
│   └── *.html              # HTML pages
├── docs/                   # Documentation
├── tests/                  # Test suite
├── scripts/                # Shell scripts
├── prompts/                # Prompt registry
└── docker-compose.yml      # Docker configuration

Key Components

Backend Architecture

# Core components
backend/
├── core/
   ├── config.py          # Configuration management
   ├── schemas.py         # Pydantic models
   └── file_utils.py      # File utilities
├── chat/
   ├── chat_manager.py    # Main chat orchestration
   └── chunked_history_manager.py  # Context management
├── embeddings/
   └── embeddings_manager.py  # Embedding operations
├── llm/
   └── llm_client.py      # LLM abstraction layer
└── main.py                # FastAPI routes

Frontend Architecture

// Frontend structure
frontend/
├── static/   ├── chat.js           # Main chat functionality
   ├── chat-embed.js     # Embeddable chat
   ├── embed-loader.js   # Embed loader
   └── styles.css        # Styles
├── chat.html             # Main chat interface
├── chat-embed.html       # Embeddable chat
└── index.html            # Landing page

Development Workflow

1. Feature Development

Create Feature Branch

# Create and switch to feature branch
git checkout -b feature/new-feature-name

# Or for bug fixes
git checkout -b fix/bug-description

Development Process

# 1. Make changes
# Edit files...

# 2. Run tests
pytest tests/

# 3. Lint and format
black backend/ frontend/
pylint backend/

# 4. Type checking
mypy backend/

# 5. Test manually
python start.py
# Test in browser or via API

# 6. Commit changes
git add .
git commit -m "feat: add new feature description"

Commit Message Format

Follow Conventional Commits:

feat: add new feature
fix: resolve bug in chat pipeline  
docs: update API documentation
style: format code with black
refactor: improve chat manager structure
test: add tests for embedding manager
chore: update dependencies

2. Testing Strategy

Unit Tests

# tests/unit/test_embeddings_manager.py
import pytest
from backend.embeddings.embeddings_manager import EmbeddingsManager

class TestEmbeddingsManager:
    def setup_method(self):
        self.manager = EmbeddingsManager()
    
    def test_estimate_tokens(self):
        """Test token estimation"""
        text = "Test document for token estimation"
        tokens = self.manager.estimate_tokens(text)
        
        assert isinstance(tokens, int)
        assert tokens > 0
    
    def test_generate_embeddings(self):
        """Test embedding generation"""
        text = "Test document"
        embedding = self.manager.generate_embeddings(text)
        
        assert isinstance(embedding, list)
        assert len(embedding) == 1536  # OpenAI dimensions
        assert all(isinstance(x, float) for x in embedding)
    
    def test_batch_embeddings(self):
        """Test batch embedding"""
        texts = ["Doc 1", "Doc 2", "Doc 3"]
        embeddings = self.manager.generate_embeddings(texts)
        
        assert isinstance(embeddings, list)
        assert len(embeddings) == 3
        assert all(len(e) == 1536 for e in embeddings)

Integration Tests

# tests/integration/test_chat_pipeline.py
import pytest
from backend.chat.chat_manager import handle_chat

class TestChatPipeline:
    def test_chat_request(self):
        """Test end-to-end chat request"""
        request = {
            "message": "What is the capital of France?",
            "history": [],
            "params": {
                "top_k": 5,
                "temperature": 0.7
            }
        }
        
        response = handle_chat(request)
        
        assert "answer" in response
        assert "metrics" in response
        assert len(response["answer"]) > 0

API Tests

# tests/integration/test_api.py
import pytest
from fastapi.testclient import TestClient
from backend.main import app

client = TestClient(app)

def test_health_check():
    """Test health endpoint"""
    response = client.get("/health")
    assert response.status_code == 200
    assert response.json()["status"] == "healthy"

def test_chat_endpoint():
    """Test chat API"""
    payload = {
        "message": "Test message",
        "history": [],
        "params": {"top_k": 5}
    }
    
    response = client.post("/chat", json=payload)
    assert response.status_code == 200
    assert "answer" in response.json()

3. Running Tests

Local Testing

# Run all tests
pytest

# Run with coverage
pytest --cov=backend --cov-report=html

# Run specific test file
pytest tests/unit/test_embeddings_manager.py

# Run with verbose output
pytest -v

# Run integration tests only
pytest tests/integration/

Test Configuration

Create pytest.ini:

[tool:pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = 
    -v
    --tb=short
    --strict-markers
    --disable-warnings
markers =
    unit: Unit tests
    integration: Integration tests
    slow: Slow running tests

Debugging

1. Debug Configuration

VS Code Debugging

// .vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Debug FastAPI",
      "type": "python",
      "request": "launch",
      "program": "start.py",
      "console": "integratedTerminal",
      "env": {
        "DEBUG_VERBOSE": "true",
        "PYTHONPATH": "${workspaceFolder}"
      },
      "args": [],
      "justMyCode": false
    }
  ]
}

Python Debugging

# Add debug prints
import logging
logging.basicConfig(level=logging.DEBUG)

# Use pdb for debugging
import pdb; pdb.set_trace()

# Or use ipdb (better interface)
import ipdb; ipdb.set_trace()

2. Logging Configuration

Development Logging

# backend/core/logging.py
import logging
import sys

def setup_logging():
    """Setup development logging"""
    logging.basicConfig(
        level=logging.DEBUG,
        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
        handlers=[
            logging.StreamHandler(sys.stdout),
            logging.FileHandler('debug.log')
        ]
    )

# Use in modules
logger = logging.getLogger(__name__)
logger.info("Processing chat request")
logger.debug(f"Request data: {request_data}")

Structured Logging

import structlog

logger = structlog.get_logger()

def process_chat(request):
    logger.info("chat_started", 
                user_id=request.user_id,
                message_length=len(request.message))
    
    try:
        result = chat_pipeline(request)
        logger.info("chat_completed",
                    user_id=request.user_id,
                    response_length=len(result.answer))
        return result
    except Exception as e:
        logger.error("chat_failed",
                    user_id=request.user_id,
                    error=str(e),
                    exc_info=True)
        raise

3. Common Debugging Scenarios

API Issues

# Debug API requests
import requests
import logging

# Enable request logging
logging.basicConfig(level=logging.DEBUG)
requests_log = logging.getLogger("requests.packages.urllib3")
requests_log.setLevel(logging.DEBUG)
requests_log.propagate = True

# Test API call
response = requests.post(url, json=payload)
print(f"Status: {response.status_code}")
print(f"Response: {response.text}")

Database Issues

# Debug Qdrant operations
from qdrant_client import QdrantClient
from qdrant_client.http.models import Filter

def debug_qdrant():
    client = QdrantClient(host="localhost", port=6333)
    
    # Check collections
    collections = client.get_collections()
    print(f"Collections: {collections}")
    
    # Check collection info
    collection_info = client.get_collection_info("document_index")
    print(f"Collection info: {collection_info}")
    
    # Test search
    try:
        results = client.search(
            collection_name="document_index",
            query_vector=[0.1] * 1536,
            limit=5
        )
        print(f"Search results: {len(results)}")
    except Exception as e:
        print(f"Search error: {e}")

Embedding Issues

# Debug embedding generation
from backend.embeddings.embeddings_manager import EmbeddingsManager
from backend.llm.llm_client import embed

def debug_embeddings():
    manager = EmbeddingsManager()
    
    # Test single embedding
    text = "Test document for debugging"
    try:
        embedding = manager.generate_embeddings(text)
        print(f"Embedding shape: {len(embedding)}")
        print(f"Sample values: {embedding[:5]}")
    except Exception as e:
        print(f"Embedding error: {e}")
    
    # Test batch embedding
    texts = ["Doc 1", "Doc 2", "Doc 3"]
    try:
        embeddings = manager.generate_embeddings(texts)
        print(f"Batch embeddings: {len(embeddings)} x {len(embeddings[0])}")
    except Exception as e:
        print(f"Batch embedding error: {e}")
    
    # Test direct LLM client
    try:
        embedding = embed(model_key="openai:embed_small", texts=text)
        print(f"Direct embed: {len(embedding)} dimensions")
    except Exception as e:
        print(f"Direct embed error: {e}")

Adding Features

1. Adding New API Endpoints

Step 1: Define Pydantic Models

# backend/core/schemas.py
from pydantic import BaseModel
from typing import List, Optional, Dict, Any

class NewFeatureRequest(BaseModel):
    input_data: str
    options: Optional[Dict[str, Any]] = None

class NewFeatureResponse(BaseModel):
    result: str
    metadata: Optional[Dict[str, Any]] = None

Step 2: Implement Business Logic

# backend/api/endpoints/new_feature.py
from fastapi import APIRouter
from backend.core.schemas import NewFeatureRequest, NewFeatureResponse

router = APIRouter()

@router.post("/new-feature", response_model=NewFeatureResponse)
async def handle_new_feature(request: NewFeatureRequest):
    """Handle new feature request"""
    # Implement logic here
    result = process_new_feature(request.input_data, request.options)
    
    return NewFeatureResponse(
        result=result["output"],
        metadata=result.get("metadata")
    )

Step 3: Register Route

# backend/main.py
from backend.api.endpoints.new_feature import router as new_feature_router

app.include_router(new_feature_router, tags=["New Feature"])

Step 4: Add Tests

# tests/unit/test_new_feature.py
import pytest
from backend.api.endpoints.new_feature import handle_new_feature
from backend.core.schemas import NewFeatureRequest

def test_new_feature():
    request = NewFeatureRequest(
        input_data="test input",
        options={"option1": "value1"}
    )
    
    response = await handle_new_feature(request)
    
    assert response.result is not None
    assert isinstance(response.metadata, dict)

2. Adding New LLM Providers

Step 1: Create Provider Class

# backend/llm/providers/new_provider.py
from backend.llm.llm_client import LLMProvider

class NewLLMProvider(LLMProvider):
    def __init__(self, api_key: str, base_url: str = None):
        self.api_key = api_key
        self.base_url = base_url or "https://api.newprovider.com/v1"
    
    async def chat_completion(self, messages, **kwargs):
        """Implement chat completion"""
        # Provider-specific implementation
        pass
    
    async def embed(self, texts, **kwargs):
        """Implement embedding"""
        # Provider-specific implementation
        pass

Step 2: Register Provider

# backend/llm/llm_client.py
from backend.llm.providers.new_provider import NewLLMProvider

class LLMClient:
    def __init__(self):
        self.providers = {
            "openai": OpenAIProvider,
            "gemini": GeminiProvider,
            "new_provider": NewLLMProvider  # Add new provider
        }

Step 3: Update Model Registry

# Add to model registry configuration
"new_provider:model-name": {
    "provider": "new_provider",
    "model": "model-name",
    "api_type": "chat_completions",
    "max_tokens": 4096,
    "input_cost_per_1k": 0.001,
    "output_cost_per_1k": 0.002,
    "supports_tools": True,
    "supports_reasoning": False
}

3. Adding New Tools

Step 1: Define Tool Function

# backend/chat/tools/new_tool.py
from typing import Dict, Any

async def new_tool_function(param1: str, param2: int) -> Dict[str, Any]:
    """New tool implementation"""
    # Tool logic here
    result = {
        "output": f"Processed {param1} with {param2}",
        "success": True
    }
    return result

Step 2: Register Tool

# backend/chat/tool_manager.py
from backend.chat.tools.new_tool import new_tool_function

class ToolManager:
    def __init__(self):
        self.tools = {
            "get_weather": get_weather,
            "get_airports": get_airports,
            "new_tool": new_tool_function  # Add new tool
        }

Step 3: Update Tool Schema

# Update tool schema for LLM
NEW_TOOL_SCHEMA = {
    "type": "function",
    "function": {
        "name": "new_tool",
        "description": "Description of what the tool does",
        "parameters": {
            "type": "object",
            "properties": {
                "param1": {"type": "string", "description": "First parameter"},
                "param2": {"type": "integer", "description": "Second parameter"}
            },
            "required": ["param1", "param2"]
        }
    }
}

Contribution Guidelines

1. Code Style

Python Style Guide

Follow PEP 8 and use these tools:

# Format code
black backend/ frontend/

# Lint code
pylint backend/
flake8 backend/

# Type checking
mypy backend/

# Import sorting
isort backend/

JavaScript Style Guide

# Format JavaScript
npx prettier --write frontend/static/*.js

# Lint JavaScript
npx eslint frontend/static/*.js

2. Documentation Standards

Code Documentation

def process_chat_request(request: ChatRequest) -> ChatResponse:
    """
    Process a chat request and generate response.
    
    Args:
        request: Chat request containing message and parameters
        
    Returns:
        Chat response with answer and metadata
        
    Raises:
        ValueError: If request is invalid
        LLMError: If LLM processing fails
        
    Example:
        >>> request = ChatRequest(message="Hello")
        >>> response = process_chat_request(request)
        >>> print(response.answer)
    """
    pass

API Documentation

@app.post("/chat", 
          tags=["Chat"],
          summary="Process chat message",
          response_model=ChatResponse,
          responses={404: {"model": ErrorResponse}})
async def chat_endpoint(request: ChatRequest):
    """
    Process a chat message using the RAG pipeline.
    
    - **message**: User message to process
    - **params**: Optional parameters for customization
    """
    pass

3. Testing Requirements

Coverage Requirements

Test Categories

# Mark tests appropriately
@pytest.mark.unit  # Unit tests
@pytest.mark.integration  # Integration tests
@pytest.mark.slow  # Slow tests (run separately)

4. Pull Request Process

PR Checklist

PR Template

## Description
Brief description of changes

## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update

## Testing
- [ ] Unit tests pass
- [ ] Integration tests pass
- [ ] Manual testing completed

## Checklist
- [ ] Code follows style guidelines
- [ ] Self-review completed
- [ ] Documentation updated

5. Release Process

Version Management

Use semantic versioning (semver):

Release Steps

# 1. Update version
# Update version in pyproject.toml and __init__.py

# 2. Update changelog
# Add changes to CHANGELOG.md

# 3. Create release tag
git tag -a v1.2.3 -m "Release version 1.2.3"
git push origin v1.2.3

# 4. Create GitHub release
# Use tag and changelog

Development Tips

1. Productivity Tips

2. Debugging Tips

3. Performance Tips

4. Common Pitfalls

This development guide provides comprehensive coverage for contributing to the chat-with-rag project effectively.