git-chat / plan.txt
lakkiroy's picture
Upload folder using huggingface_hub
200bf6d verified
# Chat with GitHub Repo - GenAI Project
## Project Overview
A GenAI application that allows developers to paste a GitHub repository URL and ask natural language questions about the codebase. The system clones the repo, processes and embeds the code files, then uses RAG (Retrieval Augmented Generation) to answer questions about the code.
## Architecture
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Frontend │ │ Backend API │ │ Vector DB │
│ (React/Next) │───▶│ (FastAPI) │───▶│ (Pinecone/ │
│ │ │ │ │ Chroma) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
┌─────────────────┐
│ GitHub API │
│ + Git Clone │
└─────────────────┘
```
## Tech Stack
### Backend
- **FastAPI** - High-performance Python web framework
- **LangChain** - LLM orchestration and RAG implementation
- **Free LLM Options** - Ollama (local), Groq (fast inference), or Hugging Face
- **Free Embeddings** - Sentence Transformers (local) or Hugging Face
- **Chroma** - Vector database for embeddings
- **GitPython** - For cloning and processing repositories
- **Celery + Redis** - For background processing
### Frontend
- **Next.js** - React framework
- **TypeScript** - Type safety
- **Tailwind CSS** - Styling
- **Socket.io** - Real-time updates
## Backend Implementation
### 1. Project Structure
```
backend/
├── app/
│ ├── __init__.py
│ ├── main.py
│ ├── models/
│ │ ├── __init__.py
│ │ └── schemas.py
│ ├── services/
│ │ ├── __init__.py
│ │ ├── github_service.py
│ │ ├── embedding_service.py
│ │ └── chat_service.py
│ ├── utils/
│ │ ├── __init__.py
│ │ └── file_processor.py
│ └── config.py
├── requirements.txt
├── docker-compose.yml
└── Dockerfile
```
### 2. Core Dependencies (requirements.txt)
```txt
fastapi==0.104.1
uvicorn[standard]==0.24.0
python-multipart==0.0.6
pydantic==2.5.0
langchain==0.1.0
langchain-community==0.0.10
chromadb==0.4.18
GitPython==3.1.40
python-dotenv==1.0.0
celery==5.3.4
redis==5.0.1
socketio==5.10.0
python-socketio==5.10.0
aiofiles==23.2.1
# Free LLM & Embedding Options
sentence-transformers==2.2.2
transformers==4.36.0
torch==2.1.0
ollama==0.1.7
groq==0.4.1
huggingface-hub==0.19.4
```
### 3. Configuration (config.py)
```python
import os
from pydantic_settings import BaseSettings
from enum import Enum
class LLMProvider(str, Enum):
OLLAMA = "ollama"
GROQ = "groq"
HUGGINGFACE = "huggingface"
class EmbeddingProvider(str, Enum):
SENTENCE_TRANSFORMERS = "sentence_transformers"
HUGGINGFACE = "huggingface"
class Settings(BaseSettings):
# LLM Configuration
llm_provider: LLMProvider = LLMProvider.OLLAMA
ollama_base_url: str = "http://localhost:11434"
ollama_model: str = "llama2" # or codellama, mistral, etc.
groq_api_key: str = ""
groq_model: str = "mixtral-8x7b-32768"
huggingface_api_key: str = ""
huggingface_model: str = "microsoft/DialoGPT-medium"
# Embedding Configuration
embedding_provider: EmbeddingProvider = EmbeddingProvider.SENTENCE_TRANSFORMERS
sentence_transformer_model: str = "all-MiniLM-L6-v2" # Fast and good
# Alternative: "all-mpnet-base-v2" (better quality, slower)
# Other settings
github_token: str = ""
redis_url: str = "redis://localhost:6379"
vector_db_path: str = "./chroma_db"
max_file_size: int = 1024 * 1024 # 1MB
supported_extensions: list = [
".py", ".js", ".ts", ".jsx", ".tsx", ".java", ".cpp", ".c",
".cs", ".go", ".rs", ".php", ".rb", ".swift", ".kt", ".scala",
".md", ".txt", ".json", ".yaml", ".yml", ".toml"
]
class Config:
env_file = ".env"
settings = Settings()
```
### 4. Data Models (models/schemas.py)
```python
from pydantic import BaseModel, HttpUrl
from typing import List, Optional
from enum import Enum
class ProcessingStatus(str, Enum):
PENDING = "pending"
PROCESSING = "processing"
COMPLETED = "completed"
FAILED = "failed"
class RepoProcessRequest(BaseModel):
repo_url: HttpUrl
branch: Optional[str] = "main"
class ChatMessage(BaseModel):
message: str
repo_id: str
class ChatResponse(BaseModel):
response: str
sources: List[dict]
repo_id: str
class RepoStatus(BaseModel):
repo_id: str
status: ProcessingStatus
progress: int
message: str
total_files: Optional[int] = None
processed_files: Optional[int] = None
```
### 5. GitHub Service (services/github_service.py)
```python
import os
import tempfile
import shutil
from git import Repo
from typing import List, Tuple
import hashlib
from urllib.parse import urlparse
class GitHubService:
def __init__(self, github_token: str = ""):
self.github_token = github_token
def generate_repo_id(self, repo_url: str) -> str:
"""Generate a unique ID for the repository"""
return hashlib.md5(repo_url.encode()).hexdigest()
def parse_github_url(self, url: str) -> Tuple[str, str]:
"""Extract owner and repo name from GitHub URL"""
parsed = urlparse(url)
path_parts = parsed.path.strip('/').split('/')
if len(path_parts) >= 2:
return path_parts[0], path_parts[1]
raise ValueError("Invalid GitHub URL format")
async def clone_repository(self, repo_url: str, branch: str = "main") -> str:
"""Clone repository to temporary directory"""
temp_dir = tempfile.mkdtemp()
try:
if self.github_token:
# Use token for private repos or higher rate limits
auth_url = repo_url.replace("https://", f"https://{self.github_token}@")
Repo.clone_from(auth_url, temp_dir, branch=branch, depth=1)
else:
Repo.clone_from(repo_url, temp_dir, branch=branch, depth=1)
return temp_dir
except Exception as e:
shutil.rmtree(temp_dir, ignore_errors=True)
raise Exception(f"Failed to clone repository: {str(e)}")
def cleanup_repo(self, repo_path: str):
"""Clean up cloned repository"""
if os.path.exists(repo_path):
shutil.rmtree(repo_path, ignore_errors=True)
```
### 6. File Processor (utils/file_processor.py)
```python
import os
from typing import List, Dict, Generator
import mimetypes
from pathlib import Path
class FileProcessor:
def __init__(self, supported_extensions: List[str], max_file_size: int):
self.supported_extensions = supported_extensions
self.max_file_size = max_file_size
self.ignore_dirs = {
'.git', '__pycache__', 'node_modules', '.pytest_cache',
'venv', 'env', '.venv', 'build', 'dist', '.next',
'coverage', '.coverage', 'logs', 'log'
}
self.ignore_files = {
'.gitignore', '.env', '.env.local', '.DS_Store',
'package-lock.json', 'yarn.lock', 'poetry.lock'
}
def should_process_file(self, file_path: str) -> bool:
"""Check if file should be processed"""
path = Path(file_path)
# Check if any parent directory is in ignore list
for parent in path.parents:
if parent.name in self.ignore_dirs:
return False
# Check file name
if path.name in self.ignore_files:
return False
# Check extension
if path.suffix.lower() not in self.supported_extensions:
return False
# Check file size
try:
if os.path.getsize(file_path) > self.max_file_size:
return False
except OSError:
return False
return True
def extract_files(self, repo_path: str) -> Generator[Dict, None, None]:
"""Extract and yield file information"""
for root, dirs, files in os.walk(repo_path):
# Filter out ignored directories
dirs[:] = [d for d in dirs if d not in self.ignore_dirs]
for file in files:
file_path = os.path.join(root, file)
relative_path = os.path.relpath(file_path, repo_path)
if not self.should_process_file(file_path):
continue
try:
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
yield {
'path': relative_path,
'content': content,
'extension': Path(file_path).suffix.lower(),
'size': len(content)
}
except Exception as e:
print(f"Error reading file {relative_path}: {e}")
continue
```
### 7. Free Embedding Service (services/embedding_service.py)
```python
from sentence_transformers import SentenceTransformer
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.schema import Document
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import HuggingFaceEmbeddings, SentenceTransformerEmbeddings
import chromadb
from typing import List, Dict
import os
class FreeEmbeddingService:
def __init__(self, embedding_provider: str, vector_db_path: str, model_name: str = "all-MiniLM-L6-v2"):
self.vector_db_path = vector_db_path
self.embedding_provider = embedding_provider
# Initialize embedding function based on provider
if embedding_provider == "sentence_transformers":
self.embeddings = SentenceTransformerEmbeddings(
model_name=model_name,
cache_folder="./models" # Cache models locally
)
elif embedding_provider == "huggingface":
self.embeddings = HuggingFaceEmbeddings(
model_name=model_name,
cache_folder="./models"
)
else:
raise ValueError(f"Unsupported embedding provider: {embedding_provider}")
self.text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
separators=["\n\n", "\n", " ", ""]
)
def create_documents(self, files: List[Dict], repo_id: str) -> List[Document]:
"""Create documents from file contents"""
documents = []
for file_info in files:
# Create document with metadata
doc = Document(
page_content=file_info['content'],
metadata={
'path': file_info['path'],
'extension': file_info['extension'],
'repo_id': repo_id,
'size': file_info['size']
}
)
documents.append(doc)
return documents
def split_documents(self, documents: List[Document]) -> List[Document]:
"""Split documents into chunks"""
return self.text_splitter.split_documents(documents)
async def create_embeddings(self, files: List[Dict], repo_id: str):
"""Create and store embeddings for repository files"""
# Create documents
documents = self.create_documents(files, repo_id)
# Split into chunks
chunks = self.split_documents(documents)
# Create vector store
collection_name = f"repo_{repo_id}"
vectorstore = Chroma(
collection_name=collection_name,
embedding_function=self.embeddings,
persist_directory=self.vector_db_path
)
# Add documents to vector store in batches
batch_size = 100
for i in range(0, len(chunks), batch_size):
batch = chunks[i:i + batch_size]
vectorstore.add_documents(batch)
return vectorstore
def get_vectorstore(self, repo_id: str):
"""Get existing vector store for repository"""
collection_name = f"repo_{repo_id}"
return Chroma(
collection_name=collection_name,
embedding_function=self.embeddings,
persist_directory=self.vector_db_path
)
# Alternative: Direct SentenceTransformers implementation for more control
class DirectEmbeddingService:
def __init__(self, model_name: str = "all-MiniLM-L6-v2"):
self.model = SentenceTransformer(model_name, cache_folder="./models")
self.text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
separators=["\n\n", "\n", " ", ""]
)
def embed_texts(self, texts: List[str]) -> List[List[float]]:
"""Generate embeddings for texts"""
return self.model.encode(texts, convert_to_numpy=True).tolist()
def embed_query(self, query: str) -> List[float]:
"""Generate embedding for a single query"""
return self.model.encode([query], convert_to_numpy=True)[0].tolist()
```
### 8. Free Chat Service (services/chat_service.py)
```python
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate
from langchain_community.llms import Ollama
from typing import Dict, List
import json
import requests
import os
# For Groq (free tier available)
class GroqLLM:
def __init__(self, api_key: str, model: str = "mixtral-8x7b-32768"):
self.api_key = api_key
self.model = model
self.base_url = "https://api.groq.com/openai/v1"
def __call__(self, prompt: str) -> str:
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
data = {
"model": self.model,
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.1,
"max_tokens": 1024
}
response = requests.post(
f"{self.base_url}/chat/completions",
headers=headers,
json=data
)
if response.status_code == 200:
return response.json()["choices"][0]["message"]["content"]
else:
raise Exception(f"Groq API error: {response.text}")
# For Hugging Face Inference API
class HuggingFaceLLM:
def __init__(self, api_key: str, model: str = "microsoft/DialoGPT-medium"):
self.api_key = api_key
self.model = model
self.base_url = f"https://api-inference.huggingface.co/models/{model}"
def __call__(self, prompt: str) -> str:
headers = {"Authorization": f"Bearer {self.api_key}"}
data = {"inputs": prompt, "parameters": {"max_length": 1000, "temperature": 0.1}}
response = requests.post(self.base_url, headers=headers, json=data)
if response.status_code == 200:
result = response.json()
if isinstance(result, list) and len(result) > 0:
return result[0].get("generated_text", "").replace(prompt, "").strip()
return str(result)
else:
raise Exception(f"HuggingFace API error: {response.text}")
class FreeChatService:
def __init__(self, llm_provider: str, **kwargs):
self.llm_provider = llm_provider
if llm_provider == "ollama":
self.llm = Ollama(
model=kwargs.get("model", "llama2"),
base_url=kwargs.get("base_url", "http://localhost:11434"),
temperature=0.1
)
elif llm_provider == "groq":
self.llm = GroqLLM(
api_key=kwargs.get("api_key"),
model=kwargs.get("model", "mixtral-8x7b-32768")
)
elif llm_provider == "huggingface":
self.llm = HuggingFaceLLM(
api_key=kwargs.get("api_key"),
model=kwargs.get("model", "microsoft/DialoGPT-medium")
)
else:
raise ValueError(f"Unsupported LLM provider: {llm_provider}")
self.prompt_template = PromptTemplate(
input_variables=["context", "question"],
template="""
You are a helpful AI assistant that analyzes code repositories. Use the following code snippets to answer the user's question about the repository.
Context from repository:
{context}
Question: {question}
Please provide a detailed answer based on the code context provided. If you reference specific files or functions, mention their file paths. If the question cannot be fully answered from the provided context, say so clearly.
Answer:"""
)
async def answer_question(self, question: str, vectorstore, repo_id: str) -> Dict:
"""Answer question using RAG with free LLM"""
try:
if self.llm_provider == "ollama":
# Use LangChain's RetrievalQA for Ollama
qa_chain = RetrievalQA.from_chain_type(
llm=self.llm,
chain_type="stuff",
retriever=vectorstore.as_retriever(search_kwargs={"k": 5}),
chain_type_kwargs={"prompt": self.prompt_template},
return_source_documents=True
)
result = qa_chain({"query": question})
answer = result["result"]
source_docs = result.get("source_documents", [])
else:
# Manual RAG for other providers
docs = vectorstore.similarity_search(question, k=5)
context = "\n\n".join([doc.page_content for doc in docs])
prompt = self.prompt_template.format(
context=context,
question=question
)
answer = self.llm(prompt)
source_docs = docs
# Format sources
sources = []
for doc in source_docs:
sources.append({
"path": doc.metadata.get("path", "Unknown"),
"content_preview": doc.page_content[:200] + "..." if len(doc.page_content) > 200 else doc.page_content
})
return {
"response": answer,
"sources": sources,
"repo_id": repo_id
}
except Exception as e:
return {
"response": f"Error processing question: {str(e)}",
"sources": [],
"repo_id": repo_id
}
```
### 9. Updated Main FastAPI Application (main.py)
```python
from fastapi import FastAPI, HTTPException, BackgroundTasks
from fastapi.middleware.cors import CORSMiddleware
import uvicorn
from typing import Dict
import asyncio
from datetime import datetime
from models.schemas import RepoProcessRequest, ChatMessage, ChatResponse, RepoStatus, ProcessingStatus
from services.github_service import GitHubService
from services.embedding_service import FreeEmbeddingService
from services.chat_service import FreeChatService
from utils.file_processor import FileProcessor
from config import settings
app = FastAPI(title="Chat with GitHub Repo (Free Version)", version="1.0.0")
# CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Configure properly for production
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Initialize services based on configuration
github_service = GitHubService(settings.github_token)
embedding_service = FreeEmbeddingService(
embedding_provider=settings.embedding_provider.value,
vector_db_path=settings.vector_db_path,
model_name=settings.sentence_transformer_model
)
# Initialize chat service based on provider
chat_kwargs = {}
if settings.llm_provider.value == "ollama":
chat_kwargs = {
"model": settings.ollama_model,
"base_url": settings.ollama_base_url
}
elif settings.llm_provider.value == "groq":
chat_kwargs = {
"api_key": settings.groq_api_key,
"model": settings.groq_model
}
elif settings.llm_provider.value == "huggingface":
chat_kwargs = {
"api_key": settings.huggingface_api_key,
"model": settings.huggingface_model
}
chat_service = FreeChatService(
llm_provider=settings.llm_provider.value,
**chat_kwargs
)
file_processor = FileProcessor(settings.supported_extensions, settings.max_file_size)
# In-memory status tracking (use Redis in production)
repo_status: Dict[str, RepoStatus] = {}
async def process_repository(repo_url: str, branch: str, repo_id: str):
"""Background task to process repository"""
try:
repo_status[repo_id] = RepoStatus(
repo_id=repo_id,
status=ProcessingStatus.PROCESSING,
progress=10,
message="Cloning repository..."
)
# Clone repository
repo_path = await github_service.clone_repository(repo_url, branch)
repo_status[repo_id].progress = 30
repo_status[repo_id].message = "Processing files..."
# Extract files
files = list(file_processor.extract_files(repo_path))
repo_status[repo_id].total_files = len(files)
repo_status[repo_id].progress = 50
repo_status[repo_id].message = "Creating embeddings (this may take a while for large repos)..."
# Create embeddings
await embedding_service.create_embeddings(files, repo_id)
# Cleanup
github_service.cleanup_repo(repo_path)
repo_status[repo_id].status = ProcessingStatus.COMPLETED
repo_status[repo_id].progress = 100
repo_status[repo_id].message = f"Repository processed successfully! Using {settings.llm_provider.value} for chat."
except Exception as e:
repo_status[repo_id].status = ProcessingStatus.FAILED
repo_status[repo_id].message = f"Error: {str(e)}"
@app.post("/api/process-repo")
async def process_repo(request: RepoProcessRequest, background_tasks: BackgroundTasks):
"""Process a GitHub repository"""
repo_id = github_service.generate_repo_id(str(request.repo_url))
# Check if already processed
if repo_id in repo_status and repo_status[repo_id].status == ProcessingStatus.COMPLETED:
return {"repo_id": repo_id, "message": "Repository already processed"}
# Start processing
repo_status[repo_id] = RepoStatus(
repo_id=repo_id,
status=ProcessingStatus.PENDING,
progress=0,
message="Starting processing..."
)
background_tasks.add_task(process_repository, str(request.repo_url), request.branch, repo_id)
return {"repo_id": repo_id, "message": "Processing started"}
@app.get("/api/status/{repo_id}", response_model=RepoStatus)
async def get_repo_status(repo_id: str):
"""Get repository processing status"""
if repo_id not in repo_status:
raise HTTPException(status_code=404, detail="Repository not found")
return repo_status[repo_id]
@app.post("/api/chat", response_model=ChatResponse)
async def chat_with_repo(message: ChatMessage):
"""Chat with repository"""
repo_id = message.repo_id
# Check if repo is processed
if repo_id not in repo_status or repo_status[repo_id].status != ProcessingStatus.COMPLETED:
raise HTTPException(status_code=400, detail="Repository not processed")
try:
# Get vector store
vectorstore = embedding_service.get_vectorstore(repo_id)
# Get answer
result = await chat_service.answer_question(message.message, vectorstore, repo_id)
return ChatResponse(**result)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/health")
async def health_check():
return {
"status": "healthy",
"timestamp": datetime.utcnow(),
"llm_provider": settings.llm_provider.value,
"embedding_provider": settings.embedding_provider.value
}
@app.get("/api/config")
async def get_config():
"""Get current configuration"""
return {
"llm_provider": settings.llm_provider.value,
"embedding_provider": settings.embedding_provider.value,
"embedding_model": settings.sentence_transformer_model,
"supported_extensions": settings.supported_extensions
}
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000, reload=True)
```_service.create_embeddings(files, repo_id)
# Cleanup
github_service.cleanup_repo(repo_path)
repo_status[repo_id].status = ProcessingStatus.COMPLETED
repo_status[repo_id].progress = 100
repo_status[repo_id].message = "Repository processed successfully!"
except Exception as e:
repo_status[repo_id].status = ProcessingStatus.FAILED
repo_status[repo_id].message = f"Error: {str(e)}"
@app.post("/api/process-repo")
async def process_repo(request: RepoProcessRequest, background_tasks: BackgroundTasks):
"""Process a GitHub repository"""
repo_id = github_service.generate_repo_id(str(request.repo_url))
# Check if already processed
if repo_id in repo_status and repo_status[repo_id].status == ProcessingStatus.COMPLETED:
return {"repo_id": repo_id, "message": "Repository already processed"}
# Start processing
repo_status[repo_id] = RepoStatus(
repo_id=repo_id,
status=ProcessingStatus.PENDING,
progress=0,
message="Starting processing..."
)
background_tasks.add_task(process_repository, str(request.repo_url), request.branch, repo_id)
return {"repo_id": repo_id, "message": "Processing started"}
@app.get("/api/status/{repo_id}", response_model=RepoStatus)
async def get_repo_status(repo_id: str):
"""Get repository processing status"""
if repo_id not in repo_status:
raise HTTPException(status_code=404, detail="Repository not found")
return repo_status[repo_id]
@app.post("/api/chat", response_model=ChatResponse)
async def chat_with_repo(message: ChatMessage):
"""Chat with repository"""
repo_id = message.repo_id
# Check if repo is processed
if repo_id not in repo_status or repo_status[repo_id].status != ProcessingStatus.COMPLETED:
raise HTTPException(status_code=400, detail="Repository not processed")
try:
# Get vector store
vectorstore = embedding_service.get_vectorstore(repo_id)
# Get answer
result = await chat_service.answer_question(message.message, vectorstore, repo_id)
return ChatResponse(**result)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/health")
async def health_check():
return {"status": "healthy", "timestamp": datetime.utcnow()}
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000, reload=True)
```
### 10. Environment Configuration (.env)
```env
# LLM Provider Configuration
LLM_PROVIDER=ollama # Options: ollama, groq, huggingface
EMBEDDING_PROVIDER=sentence_transformers # Options: sentence_transformers, huggingface
# Ollama Configuration (Local LLM - Free)
OLLAMA_BASE_URL=http://localhost:11434
OLLAMA_MODEL=llama2 # Options: llama2, codellama, mistral, phi, etc.
# Groq Configuration (Fast inference - Free tier available)
GROQ_API_KEY=your_groq_api_key_here
GROQ_MODEL=mixtral-8x7b-32768 # Options: mixtral-8x7b-32768, llama2-70b-4096
# Hugging Face Configuration (Free inference API)
HUGGINGFACE_API_KEY=your_hf_api_key_here
HUGGINGFACE_MODEL=microsoft/DialoGPT-medium
# Embedding Model Configuration
SENTENCE_TRANSFORMER_MODEL=all-MiniLM-L6-v2 # Fast and good quality
# Other Configuration
GITHUB_TOKEN=your_github_token_here
REDIS_URL=redis://localhost:6379
VECTOR_DB_PATH=./chroma_db
```
### 11. Updated Docker Configuration
**Dockerfile:**
```dockerfile
FROM python:3.11-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
git \
build-essential \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Pre-download embedding models
RUN python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('all-MiniLM-L6-v2', cache_folder='./models')"
COPY . .
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
```
**docker-compose.yml:**
```yaml
version: '3.8'
services:
api:
build: .
ports:
- "8000:8000"
environment:
- LLM_PROVIDER=${LLM_PROVIDER}
- EMBEDDING_PROVIDER=${EMBEDDING_PROVIDER}
- OLLAMA_BASE_URL=http://ollama:11434
- OLLAMA_MODEL=${OLLAMA_MODEL}
- GROQ_API_KEY=${GROQ_API_KEY}
- GROQ_MODEL=${GROQ_MODEL}
- HUGGINGFACE_API_KEY=${HUGGINGFACE_API_KEY}
- HUGGINGFACE_MODEL=${HUGGINGFACE_MODEL}
- SENTENCE_TRANSFORMER_MODEL=${SENTENCE_TRANSFORMER_MODEL}
- GITHUB_TOKEN=${GITHUB_TOKEN}
- REDIS_URL=redis://redis:6379
volumes:
- ./chroma_db:/app/chroma_db
- ./models:/app/models # Cache for models
depends_on:
- redis
- ollama
ollama:
image: ollama/ollama:latest
ports:
- "11434:11434"
volumes:
- ollama_data:/root/.ollama
environment:
- OLLAMA_KEEP_ALIVE=24h
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
ollama_data:
```
**Setup script for Ollama models (setup_ollama.sh):**
```bash
#!/bin/bash
# Pull required models
docker exec chat-with-github-repo-ollama-1 ollama pull llama2
docker exec chat-with-github-repo-ollama-1 ollama pull codellama
docker exec chat-with-github-repo-ollama-1 ollama pull mistral
```
## Performance Comparison
| Provider | Speed | Quality | Cost | Setup Difficulty |
|----------|-------|---------|------|------------------|
| Ollama + Llama2 | Medium | Good | Free | Easy |
| Ollama + CodeLlama | Medium | Excellent (Code) | Free | Easy |
| Groq + Mixtral | Very Fast | Excellent | Free Tier | Very Easy |
| HuggingFace | Slow | Variable | Free | Very Easy |
| Local Transformers | Slow-Medium | Good | Free | Medium |
## Recommended Configurations
### For Development/Testing:
```env
LLM_PROVIDER=groq
GROQ_MODEL=mixtral-8x7b-32768
EMBEDDING_PROVIDER=sentence_transformers
SENTENCE_TRANSFORMER_MODEL=all-MiniLM-L6-v2
```
### For Production (Local):
```env
LLM_PROVIDER=ollama
OLLAMA_MODEL=codellama
EMBEDDING_PROVIDER=sentence_transformers
SENTENCE_TRANSFORMER_MODEL=all-mpnet-base-v2
```
### For Minimal Resources:
```env
LLM_PROVIDER=ollama
OLLAMA_MODEL=phi
EMBEDDING_PROVIDER=sentence_transformers
SENTENCE_TRANSFORMER_MODEL=all-MiniLM-L6-v2
```
## Troubleshooting
### Ollama Issues:
```bash
# Check if Ollama is running
curl http://localhost:11434/api/version
# List available models
ollama list
# Check logs
ollama logs
```
### Memory Issues:
- Use smaller models (`phi` instead of `llama2`)
- Reduce batch size in embedding service
- Use quantized models
- Process repos in smaller chunks
### Performance Optimization:
```python
# For faster embeddings
embedding_service = FreeEmbeddingService(
embedding_provider="sentence_transformers",
vector_db_path="./chroma_db",
model_name="all-MiniLM-L6-v2" # Faster than all-mpnet-base-v2
)
# Batch processing
async def create_embeddings_batch(self, files: List[Dict], repo_id: str, batch_size: int = 50):
for i in range(0, len(files), batch_size):
batch = files[i:i + batch_size]
# Process batch...
```
## Deployment & Scaling
### Project Structure
```
frontend/
├── src/
│ ├── app/
│ │ ├── page.tsx
│ │ ├── layout.tsx
│ │ └── globals.css
│ ├── components/
│ │ ├── RepoInput.tsx
│ │ ├── ChatInterface.tsx
│ │ ├── ProcessingStatus.tsx
│ │ └── SourceDisplay.tsx
│ └── lib/
│ └── api.ts
├── package.json
├── tailwind.config.js
└── next.config.js
```
### Key Features
- Repository URL input with validation
- Real-time processing status updates
- Chat interface with message history
- Source code display with syntax highlighting
- Responsive design
## Free Alternatives to OpenAI
### 1. **Ollama (Recommended for Local Deployment)**
**Pros:**
- 100% free and private
- Runs locally, no API calls
- Supports many models (Llama2, CodeLlama, Mistral, Phi, etc.)
- Good performance on decent hardware
**Setup:**
```bash
# Install Ollama
curl -fsSL https://ollama.ai/install.sh | sh
# Pull models
ollama pull llama2 # General purpose
ollama pull codellama # Better for code
ollama pull mistral # Good balance
ollama pull phi # Lightweight
# Start Ollama server
ollama serve
```
**Requirements:** 8GB+ RAM, preferably with GPU
### 2. **Groq (Fast Inference - Free Tier)**
**Pros:**
- Extremely fast inference
- Free tier: 100 requests/minute
- High-quality models (Mixtral, Llama2)
- Simple API
**Setup:**
1. Sign up at [groq.com](https://groq.com)
2. Get API key from console
3. Set `GROQ_API_KEY` in environment
**Free Limits:** 100 requests/minute, 1000 requests/day
### 3. **Hugging Face Inference API (Free)**
**Pros:**
- Completely free
- Access to thousands of models
- No setup required
- Good for experimentation
**Setup:**
1. Sign up at [huggingface.co](https://huggingface.co)
2. Get API token from settings
3. Set `HUGGINGFACE_API_KEY` in environment
**Note:** Can be slower due to cold starts
### 4. **Local Transformers (Completely Free)**
For maximum control, you can run models directly:
```python
# services/local_llm_service.py
from transformers import pipeline, AutoTokenizer, AutoModelForCausalLM
import torch
class LocalLLMService:
def __init__(self, model_name="microsoft/DialoGPT-small"):
self.tokenizer = AutoTokenizer.from_pretrained(model_name)
self.model = AutoModelForCausalLM.from_pretrained(model_name)
self.generator = pipeline(
"text-generation",
model=self.model,
tokenizer=self.tokenizer,
device=0 if torch.cuda.is_available() else -1
)
def generate_response(self, prompt: str, max_length: int = 512):
response = self.generator(
prompt,
max_length=max_length,
temperature=0.7,
do_sample=True,
pad_token_id=self.tokenizer.eos_token_id
)
return response[0]["generated_text"].replace(prompt, "").strip()
```
## Embedding Models Comparison
### 1. **Sentence Transformers (Recommended)**
```python
# Best models for code:
"all-MiniLM-L6-v2" # Fast, good quality
"all-mpnet-base-v2" # Better quality, slower
"multi-qa-MiniLM-L6-cos-v1" # Good for Q&A
```
### 2. **Hugging Face Embeddings**
```python
# Popular models:
"sentence-transformers/all-MiniLM-L6-v2"
"sentence-transformers/paraphrase-MiniLM-L6-v2"
```
## Quick Start Guide
### Option 1: Ollama (Local)
```bash
# 1. Install and start Ollama
curl -fsSL https://ollama.ai/install.sh | sh
ollama serve
# 2. Pull a model
ollama pull llama2
# 3. Set environment variables
export LLM_PROVIDER=ollama
export OLLAMA_MODEL=llama2
export EMBEDDING_PROVIDER=sentence_transformers
# 4. Start the application
python main.py
```
### Option 2: Groq (Cloud)
```bash
# 1. Get API key from groq.com
export GROQ_API_KEY=your_api_key_here
export LLM_PROVIDER=groq
export GROQ_MODEL=mixtral-8x7b-32768
export EMBEDDING_PROVIDER=sentence_transformers
# 2. Start the application
python main.py
```
### Option 3: Hugging Face (Cloud)
```bash
# 1. Get token from huggingface.co
export HUGGINGFACE_API_KEY=your_token_here
export LLM_PROVIDER=huggingface
export HUGGINGFACE_MODEL=microsoft/DialoGPT-medium
export EMBEDDING_PROVIDER=sentence_transformers
# 2. Start the application
python main.py
```
### Production Considerations
1. **Vector Database**: Use Pinecone for better scalability
2. **Background Processing**: Implement with Celery + Redis
3. **Caching**: Add Redis caching for frequent queries
4. **Rate Limiting**: Implement API rate limiting
5. **Authentication**: Add user authentication
6. **Monitoring**: Add logging and monitoring (Sentry, DataDog)
### Performance Optimizations
1. **Chunking Strategy**: Optimize chunk size and overlap
2. **Embedding Model**: Consider using smaller models for faster processing
3. **Retrieval**: Implement hybrid search (dense + sparse)
4. **Caching**: Cache embeddings and frequently asked questions
## Usage Examples
### Sample Questions Users Can Ask:
- "How is authentication handled in this project?"
- "Explain the UserService class and its methods"
- "What testing framework is used and where are the tests?"
- "How does the database connection work?"
- "What are the main API endpoints?"
- "Show me how error handling is implemented"
## Next Steps & Enhancements
1. **Multi-language Support**: Better handling of different programming languages
2. **Code Analysis**: Add static code analysis features
3. **Visualization**: Generate architecture diagrams
4. **Collaboration**: Multi-user support with shared repositories
5. **Integration**: GitHub webhook integration for auto-updates
6. **AI Features**: Code suggestions and improvements
This project provides a solid foundation for building a production-ready "Chat with GitHub Repo" application that developers will find incredibly useful for understanding and navigating codebases!