Skip to content

Commit e36d0b6

Browse files
jayhacktkucar
authored andcommitted
[wip] Modal RAG example (#388)
# Motivation <!-- Why is this change necessary? --> # Content <!-- Please include a summary of the change --> # Testing <!-- How was the change tested? --> # Please check the following before marking your PR as ready for review - [ ] I have added tests for my changes - [ ] I have updated the documentation or added new documentation as needed
1 parent dd54bf9 commit e36d0b6

File tree

6 files changed

+386
-0
lines changed

6 files changed

+386
-0
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Repository Analyzer API
2+
3+
A simple Modal API endpoint that analyzes GitHub repositories using Codegen. The API returns basic metrics about any public GitHub repository including:
4+
5+
- Total number of files
6+
- Number of functions
7+
- Number of classes
8+
9+
## Running Locally
10+
11+
1. Install dependencies:
12+
13+
```bash
14+
uv add modal
15+
```
16+
17+
2. Start the API server:
18+
19+
```bash
20+
modal serve src/codegen/extensions/modal/api.py
21+
```
22+
23+
3. Test with curl:
24+
25+
```bash
26+
# Replace with your local Modal endpoint URL
27+
curl "{URL}?repo_name=fastapi/fastapi"
28+
```
29+
30+
## Response Format
31+
32+
The API returns JSON in this format:
33+
34+
```json
35+
{
36+
"status": "success",
37+
"error": "",
38+
"num_files": 123,
39+
"num_functions": 456,
40+
"num_classes": 78
41+
}
42+
```
43+
44+
If there's an error, you'll get:
45+
46+
```json
47+
{
48+
"status": "error",
49+
"error": "Error message here",
50+
"num_files": 0,
51+
"num_functions": 0,
52+
"num_classes": 0
53+
}
54+
```
55+
56+
## Development
57+
58+
The API is built using:
59+
60+
- Modal for serverless deployment
61+
- FastAPI for the web endpoint
62+
- Codegen for repository analysis
63+
64+
To deploy changes:
65+
66+
```bash
67+
modal deploy src/codegen/extensions/modal/api.py
68+
```
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""Modal API endpoint for repository analysis."""
2+
3+
import modal # deptry: ignore
4+
from codegen import Codebase
5+
from pydantic import BaseModel
6+
7+
# Create image with dependencies
8+
image = modal.Image.debian_slim(python_version="3.13").apt_install("git").pip_install("fastapi[standard]", "codegen>=0.5.30")
9+
10+
# Create Modal app
11+
app = modal.App("codegen-repo-analyzer")
12+
13+
14+
class RepoMetrics(BaseModel):
15+
"""Response model for repository metrics."""
16+
17+
num_files: int = 0
18+
num_functions: int = 0
19+
num_classes: int = 0
20+
status: str = "success"
21+
error: str = ""
22+
23+
24+
@app.function(image=image)
25+
@modal.web_endpoint(method="GET")
26+
def analyze_repo(repo_name: str) -> RepoMetrics:
27+
"""Analyze a GitHub repository and return metrics.
28+
29+
Args:
30+
repo_name: Repository name in format 'owner/repo'
31+
32+
Returns:
33+
RepoMetrics object containing repository metrics or error information
34+
"""
35+
try:
36+
# Validate input
37+
if "/" not in repo_name:
38+
return RepoMetrics(status="error", error="Repository name must be in format 'owner/repo'")
39+
40+
# Initialize codebase
41+
codebase = Codebase.from_repo(repo_name)
42+
43+
# Calculate metrics
44+
num_files = len(codebase.files(extensions="*")) # Get all files
45+
num_functions = len(codebase.functions)
46+
num_classes = len(codebase.classes)
47+
48+
return RepoMetrics(
49+
num_files=num_files,
50+
num_functions=num_functions,
51+
num_classes=num_classes,
52+
)
53+
54+
except Exception as e:
55+
return RepoMetrics(status="error", error=str(e))
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[project]
2+
name = "codegen-repo-analyzer"
3+
version = "0.1.0"
4+
description = "Modal API endpoint for analyzing GitHub repositories using Codegen"
5+
requires-python = ">=3.13"
6+
dependencies = ["modal>=0.73.25", "fastapi[standard]", "codegen>=0.5.30"]
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# Codegen RAG Q&A API
2+
3+
<p align="center">
4+
<a href="https://docs.codegen.com">
5+
<img src="https://i.imgur.com/6RF9W0z.jpeg" />
6+
</a>
7+
</p>
8+
9+
<h2 align="center">
10+
Answer questions about any GitHub repository using RAG
11+
</h2>
12+
13+
<div align="center">
14+
15+
[![Documentation](https://img.shields.io/badge/Docs-docs.codegen.com-purple?style=flat-square)](https://docs.codegen.com)
16+
[![License](https://img.shields.io/badge/Code%20License-Apache%202.0-gray?&color=gray)](https://github.com/codegen-sh/codegen-sdk/tree/develop?tab=Apache-2.0-1-ov-file)
17+
18+
</div>
19+
20+
This example demonstrates how to build a RAG-powered code Q&A API using Codegen's VectorIndex and Modal. The API can answer questions about any GitHub repository by:
21+
22+
1. Creating embeddings for all files in the repository
23+
1. Finding the most relevant files for a given question
24+
1. Using GPT-4 to generate an answer based on the context
25+
26+
## Quick Start
27+
28+
1. Install dependencies:
29+
30+
```bash
31+
pip install modal-client codegen openai
32+
```
33+
34+
2. Create a Modal volume for storing indices:
35+
36+
```bash
37+
modal volume create codegen-indices
38+
```
39+
40+
3. Start the API server:
41+
42+
```bash
43+
modal serve api.py
44+
```
45+
46+
4. Test with curl:
47+
48+
```bash
49+
curl -X POST "http://localhost:8000/answer_code_question" \
50+
-H "Content-Type: application/json" \
51+
-d '{
52+
"repo_name": "fastapi/fastapi",
53+
"query": "How does FastAPI handle dependency injection?"
54+
}'
55+
```
56+
57+
## API Reference
58+
59+
### POST /answer_code_question
60+
61+
Request body:
62+
63+
```json
64+
{
65+
"repo_name": "owner/repo",
66+
"query": "Your question about the code"
67+
}
68+
```
69+
70+
Response format:
71+
72+
```json
73+
{
74+
"status": "success",
75+
"error": "",
76+
"answer": "Detailed answer based on the code...",
77+
"context": [
78+
{
79+
"filepath": "path/to/file.py",
80+
"snippet": "Relevant code snippet..."
81+
}
82+
]
83+
}
84+
```
85+
86+
## How It Works
87+
88+
1. The API uses Codegen to clone and analyze the repository
89+
1. It creates/loads a VectorIndex of all files using OpenAI's embeddings
90+
1. For each question:
91+
- Finds the most semantically similar files
92+
- Extracts relevant code snippets
93+
- Uses GPT-4 to generate an answer based on the context
94+
95+
## Development
96+
97+
The API is built using:
98+
99+
- Modal for serverless deployment
100+
- Codegen for repository analysis
101+
- OpenAI for embeddings and Q&A
102+
- FastAPI for the web endpoint
103+
104+
To deploy changes:
105+
106+
```bash
107+
modal deploy api.py
108+
```
109+
110+
## Environment Variables
111+
112+
Required environment variables:
113+
114+
- `OPENAI_API_KEY`: Your OpenAI API key
115+
116+
## Learn More
117+
118+
- [Codegen Documentation](https://docs.codegen.com)
119+
- [Modal Documentation](https://modal.com/docs)
120+
- [VectorIndex Tutorial](https://docs.codegen.com/building-with-codegen/semantic-code-search)
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
"""Modal API endpoint for RAG-based code Q&A using Codegen's VectorIndex."""
2+
3+
import modal
4+
from codegen import Codebase
5+
from codegen.extensions import VectorIndex
6+
from pydantic import BaseModel
7+
8+
# Create image with dependencies
9+
image = (
10+
modal.Image.debian_slim(python_version="3.13")
11+
.apt_install("git")
12+
.pip_install(
13+
"fastapi[standard]",
14+
"codegen>=0.5.30",
15+
"openai>=1.1.0",
16+
)
17+
)
18+
19+
# Create Modal app
20+
app = modal.App("codegen-rag-qa")
21+
22+
# Create stub for persistent volume to store vector indices
23+
stub = modal.Stub("codegen-rag-qa")
24+
volume = modal.Volume.from_name("codegen-indices")
25+
26+
27+
class QARequest(BaseModel):
28+
"""Request model for code Q&A."""
29+
30+
repo_name: str
31+
query: str
32+
33+
34+
class QAResponse(BaseModel):
35+
"""Response model for code Q&A."""
36+
37+
answer: str = ""
38+
context: list[dict[str, str]] = [] # List of {filepath, snippet} used for answer
39+
status: str = "success"
40+
error: str = ""
41+
42+
43+
@stub.function(
44+
image=image,
45+
volumes={"/root/.codegen/indices": volume},
46+
timeout=600,
47+
)
48+
@modal.web_endpoint(method="POST")
49+
async def answer_code_question(request: QARequest) -> QAResponse:
50+
"""Answer questions about code using RAG with Codegen's VectorIndex.
51+
52+
Args:
53+
request: QARequest containing repository name and query
54+
55+
Returns:
56+
QAResponse containing answer and context snippets
57+
"""
58+
try:
59+
# Validate input
60+
if "/" not in request.repo_name:
61+
return QAResponse(status="error", error="Repository name must be in format 'owner/repo'")
62+
63+
# Initialize codebase
64+
codebase = Codebase.from_repo(request.repo_name)
65+
66+
# Initialize vector index
67+
index = VectorIndex(codebase)
68+
69+
# Try to load existing index or create new one
70+
try:
71+
index.load(f"/root/.codegen/indices/{request.repo_name.replace('/', '_')}.pkl")
72+
except FileNotFoundError:
73+
# Create new index if none exists
74+
index.create()
75+
index.save(f"/root/.codegen/indices/{request.repo_name.replace('/', '_')}.pkl")
76+
77+
# Find relevant files
78+
results = index.similarity_search(request.query, k=3)
79+
80+
# Collect context from relevant files
81+
context = []
82+
for filepath, score in results:
83+
try:
84+
file = codebase.get_file(filepath)
85+
if file:
86+
context.append(
87+
{
88+
"filepath": filepath,
89+
"snippet": file.content[:1000], # First 1000 chars as preview
90+
"score": f"{score:.3f}",
91+
}
92+
)
93+
except Exception as e:
94+
print(f"Error reading file {filepath}: {e}")
95+
96+
# Format context for prompt
97+
context_str = "\n\n".join([f"File: {c['filepath']}\nScore: {c['score']}\n```\n{c['snippet']}\n```" for c in context])
98+
99+
# Create prompt for OpenAI
100+
prompt = f"""Given the following code context and question, provide a clear and accurate answer.
101+
Focus on the specific code shown in the context.
102+
103+
Question: {request.query}
104+
105+
Relevant code context:
106+
{context_str}
107+
108+
Answer:"""
109+
110+
# Get answer from OpenAI
111+
from openai import OpenAI
112+
113+
client = OpenAI()
114+
response = client.chat.completions.create(
115+
model="gpt-4-turbo-preview",
116+
messages=[
117+
{"role": "system", "content": "You are a helpful code assistant. Answer questions about code accurately and concisely based on the provided context."},
118+
{"role": "user", "content": prompt},
119+
],
120+
temperature=0,
121+
)
122+
123+
return QAResponse(answer=response.choices[0].message.content, context=[{"filepath": c["filepath"], "snippet": c["snippet"]} for c in context])
124+
125+
except Exception as e:
126+
return QAResponse(status="error", error=str(e))
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[project]
2+
name = "codegen-rag-qa"
3+
version = "0.1.0"
4+
description = "Modal API endpoint for embeddings-based RAG & Q&A on Codegen"
5+
requires-python = ">=3.13"
6+
dependencies = [
7+
"modal>=0.73.25",
8+
"fastapi[standard]",
9+
"codegen>=0.5.30",
10+
"openai>=1.1.0",
11+
]

0 commit comments

Comments
 (0)