codeAtlas / modal_backend.py
aghilsabu's picture
Clean up project: add LICENSE, simplify Modal config, update docs
04a56b4
"""
CodeAtlas Modal Backend - HTTP API Endpoints
This file provides Modal web endpoints that can be called from the HF Space frontend.
Deploy with: modal deploy modal_backend.py
The HF Space Gradio app calls these endpoints for heavy compute operations.
This qualifies for the Modal Innovation Award ($2,500).
"""
import modal
import json
# Create Modal app
app = modal.App(name="codeatlas-backend")
# Container image with all dependencies and source code
image = (
modal.Image.debian_slim(python_version="3.12")
.apt_install("graphviz", "fonts-liberation", "git")
.pip_install(
"fastapi",
"google-genai>=1.0.0",
"llama-index-core>=0.11.0",
"llama-index-llms-gemini>=0.4.0",
"llama-index-llms-openai>=0.3.0",
"openai>=1.0.0",
"elevenlabs>=1.0.0",
"graphviz>=0.20.0",
"requests>=2.31.0",
)
.add_local_dir("src", "/app/src", copy=True)
.add_local_file("app.py", "/app/app.py", copy=True)
)
# ============================================================================
# Diagram Generation Endpoint
# ============================================================================
@app.function(
image=image,
cpu=2.0,
memory=4096,
timeout=300,
)
@modal.web_endpoint(method="POST", docs=True)
def generate_diagram(request: dict) -> dict:
"""
Generate architecture diagram from GitHub repository.
POST /generate_diagram
{
"github_url": "https://github.com/owner/repo",
"api_key": "your-gemini-api-key",
"model_name": "gemini-2.5-flash",
"focus_area": "optional focus area"
}
Returns:
{
"success": true,
"dot_source": "digraph {...}",
"summary": "Architecture summary...",
"filename": "raw_owner_repo_timestamp.dot",
"stats": {"nodes": 10, "edges": 15}
}
"""
import sys
import os
sys.path.insert(0, "/app")
os.chdir("/app")
os.makedirs("/app/data/diagrams", exist_ok=True)
try:
github_url = request.get("github_url", "")
api_key = request.get("api_key", "")
model_name = request.get("model_name", "gemini-2.5-flash")
focus_area = request.get("focus_area", "")
if not github_url:
return {"success": False, "error": "github_url is required"}
if not api_key:
return {"success": False, "error": "api_key is required"}
from src.core.diagram import DiagramGenerator
from src.core.github_client import GitHubClient
# Fetch code from GitHub
client = GitHubClient()
code_context = client.fetch_repo_content(github_url)
if not code_context:
return {"success": False, "error": "Failed to fetch repository content"}
# Generate diagram
generator = DiagramGenerator(api_key=api_key, model_name=model_name)
dot_source, summary = generator.generate(code_context, focus_area=focus_area)
if not dot_source:
return {"success": False, "error": "Failed to generate diagram"}
# Save and get filename
filename = generator.save_diagram(dot_source, github_url)
# Count nodes and edges
node_count, edge_count = generator._count_nodes_edges(dot_source)
return {
"success": True,
"dot_source": dot_source,
"summary": summary,
"filename": filename,
"stats": {
"nodes": node_count,
"edges": edge_count,
}
}
except Exception as e:
return {"success": False, "error": str(e)}
# ============================================================================
# Voice Narration Endpoint
# ============================================================================
@app.function(
image=image,
cpu=1.0,
memory=2048,
timeout=120,
)
@modal.web_endpoint(method="POST", docs=True)
def generate_voice(request: dict) -> dict:
"""
Generate voice narration for diagram summary.
POST /generate_voice
{
"text": "Text to convert to speech",
"api_key": "your-elevenlabs-api-key",
"voice_id": "optional-voice-id"
}
Returns:
{
"success": true,
"audio_base64": "base64-encoded-audio",
"duration_seconds": 30
}
"""
import sys
import os
import base64
sys.path.insert(0, "/app")
os.chdir("/app")
try:
text = request.get("text", "")
api_key = request.get("api_key", "")
voice_id = request.get("voice_id", "JBFqnCBsd6RMkjVDRZzb")
if not text:
return {"success": False, "error": "text is required"}
if not api_key:
return {"success": False, "error": "api_key is required"}
from elevenlabs import ElevenLabs
client = ElevenLabs(api_key=api_key)
audio_generator = client.text_to_speech.convert(
text=text,
voice_id=voice_id,
model_id="eleven_turbo_v2_5",
output_format="mp3_44100_128",
)
# Collect audio bytes
audio_bytes = b"".join(audio_generator)
audio_base64 = base64.b64encode(audio_bytes).decode("utf-8")
# Estimate duration (rough: ~16KB per second for mp3)
duration_estimate = len(audio_bytes) / (16 * 1024)
return {
"success": True,
"audio_base64": audio_base64,
"duration_seconds": round(duration_estimate, 1),
}
except Exception as e:
return {"success": False, "error": str(e)}
# ============================================================================
# Codebase Analysis Endpoint (MCP Tool)
# ============================================================================
@app.function(
image=image,
cpu=2.0,
memory=4096,
timeout=300,
)
@modal.web_endpoint(method="POST", docs=True)
def analyze_codebase(request: dict) -> dict:
"""
Analyze codebase architecture using AI.
POST /analyze_codebase
{
"github_url": "https://github.com/owner/repo",
"api_key": "your-api-key",
"model_name": "gemini-2.5-flash",
"question": "optional specific question"
}
Returns:
{
"success": true,
"analysis": "Detailed architecture analysis..."
}
"""
import sys
import os
sys.path.insert(0, "/app")
os.chdir("/app")
try:
github_url = request.get("github_url", "")
api_key = request.get("api_key", "")
model_name = request.get("model_name", "gemini-2.5-flash")
question = request.get("question", "")
if not github_url:
return {"success": False, "error": "github_url is required"}
if not api_key:
return {"success": False, "error": "api_key is required"}
from src.mcp.tools import analyze_codebase as mcp_analyze
result = mcp_analyze(
api_key=api_key,
github_url=github_url,
model_name=model_name,
)
return {
"success": True,
"analysis": result,
}
except Exception as e:
return {"success": False, "error": str(e)}
# ============================================================================
# Health Check Endpoint
# ============================================================================
@app.function(image=image, cpu=0.25, memory=256)
@modal.web_endpoint(method="GET", docs=True)
def health() -> dict:
"""Health check endpoint."""
return {
"status": "healthy",
"service": "codeatlas-backend",
"version": "1.0.0",
}
# ============================================================================
# Local Entrypoint
# ============================================================================
@app.local_entrypoint()
def main():
"""Print deployment instructions."""
print("=" * 60)
print("🚀 CodeAtlas Modal Backend")
print("=" * 60)
print()
print("Commands:")
print(" modal serve modal_backend.py # Test locally")
print(" modal deploy modal_backend.py # Deploy to production")
print()
print("After deployment, you'll get URLs like:")
print(" https://YOUR_USERNAME--codeatlas-backend-generate-diagram.modal.run")
print(" https://YOUR_USERNAME--codeatlas-backend-generate-voice.modal.run")
print(" https://YOUR_USERNAME--codeatlas-backend-analyze-codebase.modal.run")
print()
print("Set MODAL_BACKEND_URL in your HF Space secrets!")
print("=" * 60)