Spaces:
Sleeping
Sleeping
File size: 15,332 Bytes
944f820 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 | """
main.py
-------
FastAPI backend for the Codebase Oracle system.
This is the HTTP layer β thin wrapper around inference.py.
Endpoints:
POST /index β ingest + embed a codebase from given path
POST /query β run a query (macro / micro / cross_module)
GET /status β check if a codebase is indexed and ready
GET /tree β return parsed codebase tree for UI sidebar
GET /health β simple health check
Run:
uvicorn main:app --reload --port 8000
Depends on:
- inference.py
- embedder.py
- call_graph.py
- ast_parser.py
- vector_store.py
- fastapi, uvicorn, pydantic, python-dotenv
"""
import os
from contextlib import asynccontextmanager
from dotenv import load_dotenv
import tempfile
import zipfile
import shutil
from fastapi import FastAPI, HTTPException, UploadFile, File
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from pydantic import BaseModel, Field
from rich.console import Console
from inference.inference import get_engine, InferenceRequest
from ingest.embed import embed_codebase
from store.call_graph import build_and_save, get_call_graph, CALL_GRAPH_PATH
from ingest.parse_ast import parse_codebase
from store.vector_store import get_vector_store
load_dotenv()
console = Console()
# ββ App Lifespan ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Initialize shared resources on startup."""
console.rule("[bold cyan]Codebase Oracle β Starting[/bold cyan]")
# Pre-warm the inference engine (loads embedding model once)
get_engine()
console.print("[green]β[/green] Server ready.\n")
yield
console.print("[dim]Server shutting down.[/dim]")
# ββ App βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
app = FastAPI(
title="Codebase Oracle",
description="AI-powered monolithic codebase comprehension system.",
version="1.0.0",
lifespan=lifespan,
)
# Allow UI (served from same origin or localhost dev)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
# Serve UI static files
UI_DIR = os.path.join(os.path.dirname(__file__), "ui")
if os.path.exists(UI_DIR):
app.mount("/ui", StaticFiles(directory=UI_DIR), name="ui")
app.mount("/static", StaticFiles(directory=os.path.join(UI_DIR, "static")), name="static")
# ββ Pydantic Request / Response Models ββββββββββββββββββββββββββββββββββββββββ
class IndexRequest(BaseModel):
"""Request body for POST /index"""
root_path: str = Field(
...,
description="Absolute path to the monolithic codebase root directory.",
example="/home/user/projects/my-django-app"
)
class QueryRequest(BaseModel):
"""Request body for POST /query"""
query_type: str = Field(
...,
description="One of: 'macro', 'micro', 'cross_module'",
example="micro"
)
query: str = Field(
...,
description="Natural language developer query.",
example="What does process_payment do and how do I use it?"
)
subtype: str = Field(
default="",
description="Macro subtype: 'overall_architecture' | 'module_responsibility' | 'data_flow'",
example="overall_architecture"
)
function_name: str = Field(
default="",
description="Target function/method name for micro and cross_module queries.",
example="process_payment"
)
class_name: str = Field(
default="",
description="Target class name if function is a method.",
example="PaymentProcessor"
)
module_name: str = Field(
default="",
description="Target module name for macro module_responsibility queries.",
example="payments"
)
followup: bool = Field(
default=False,
description="True if this is a follow-up to a previous response."
)
previous_response: str = Field(
default="",
description="Previous LLM response for follow-up context."
)
class IndexResponse(BaseModel):
success: bool
message: str
class_chunks: int = 0
function_chunks: int = 0
total_chunks: int = 0
graph_nodes: int = 0
graph_edges: int = 0
class QueryResponse(BaseModel):
success: bool
content: str
error: str = ""
metadata: dict = {}
class StatusResponse(BaseModel):
indexed: bool
class_chunks: int
function_chunks: int
total_chunks: int
graph_loaded: bool
graph_nodes: int
class TreeNode(BaseModel):
name: str
type: str # "module" | "file" | "class" | "function"
children: list["TreeNode"] = []
TreeNode.model_rebuild()
class TreeResponse(BaseModel):
success: bool
tree: list[TreeNode] = []
error: str = ""
# ββ Endpoints βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@app.post("/upload-index", response_model=IndexResponse)
async def upload_index(file: UploadFile = File(...)):
"""
Accept a ZIP file, extract it to a temp directory, and index it.
Allows deployment without requiring local filesystem access.
"""
if not file.filename.endswith(".zip"):
raise HTTPException(status_code=400, detail="Only .zip files are accepted.")
tmp_dir = tempfile.mkdtemp()
try:
zip_path = os.path.join(tmp_dir, file.filename)
with open(zip_path, "wb") as f:
shutil.copyfileobj(file.file, f)
with zipfile.ZipFile(zip_path, "r") as zf:
zf.extractall(tmp_dir)
os.remove(zip_path)
# Find the extracted root β skip __MACOSX and similar artifacts
candidates = [
os.path.join(tmp_dir, d)
for d in os.listdir(tmp_dir)
if os.path.isdir(os.path.join(tmp_dir, d)) and not d.startswith("__")
]
root = candidates[0] if candidates else tmp_dir
console.rule(f"[bold cyan]Indexing ZIP: {file.filename}[/bold cyan]")
embed_codebase(root)
graph = build_and_save(root)
graph_stats = graph.stats()
store = get_vector_store()
vstats = store.stats()
console.print("[bold green]β ZIP Indexing complete.[/bold green]\n")
return IndexResponse(
success=True,
message=f"ZIP indexed successfully: {file.filename}",
class_chunks=vstats["class_chunks"],
function_chunks=vstats["function_chunks"],
total_chunks=vstats["total"],
graph_nodes=graph_stats["total_nodes"],
graph_edges=graph_stats["total_edges"],
)
except zipfile.BadZipFile:
raise HTTPException(status_code=400, detail="Invalid or corrupted ZIP file.")
except Exception as e:
console.print(f"[red]β ZIP indexing failed: {e}[/red]")
raise HTTPException(status_code=500, detail=f"ZIP indexing failed: {str(e)}")
finally:
shutil.rmtree(tmp_dir, ignore_errors=True)
@app.get("/health")
async def health():
"""Simple health check."""
return {"status": "ok", "service": "Codebase Oracle"}
@app.get("/", response_class=FileResponse)
async def serve_ui():
"""Serve the UI index.html at root."""
ui_path = os.path.join(UI_DIR, "index.html")
if not os.path.exists(ui_path):
raise HTTPException(
status_code=404,
detail="UI not found. Place index.html in the ui/ directory."
)
return FileResponse(ui_path)
@app.post("/index", response_model=IndexResponse)
async def index_codebase(req: IndexRequest):
"""
Ingest, parse, embed, and index a monolithic codebase.
Builds both ChromaDB vector index and call_graph.json.
This is the first endpoint to call before any queries.
"""
root = req.root_path.strip()
if not os.path.exists(root):
raise HTTPException(
status_code=400,
detail=f"Path does not exist: {root}"
)
if not os.path.isdir(root):
raise HTTPException(
status_code=400,
detail=f"Path is not a directory: {root}"
)
try:
console.rule(f"[bold cyan]Indexing: {root}[/bold cyan]")
# Step 1 β Embed codebase into ChromaDB
embed_codebase(root)
# Step 2 β Build and save call graph
graph = build_and_save(root)
graph_stats = graph.stats()
# Step 3 β Fetch vector store stats
store = get_vector_store()
vstats = store.stats()
console.print("[bold green]β Indexing complete.[/bold green]\n")
return IndexResponse(
success=True,
message=f"Codebase indexed successfully: {root}",
class_chunks=vstats["class_chunks"],
function_chunks=vstats["function_chunks"],
total_chunks=vstats["total"],
graph_nodes=graph_stats["total_nodes"],
graph_edges=graph_stats["total_edges"],
)
except Exception as e:
console.print(f"[red]β Indexing failed: {e}[/red]")
raise HTTPException(status_code=500, detail=f"Indexing failed: {str(e)}")
@app.post("/query", response_model=QueryResponse)
async def query(req: QueryRequest):
"""
Run a macro, micro, or cross-module query against the indexed codebase.
Returns a markdown-formatted response string.
"""
store = get_vector_store()
if not store.is_indexed():
raise HTTPException(
status_code=400,
detail="Codebase is not indexed yet. Call POST /index first."
)
engine = get_engine()
inference_req = InferenceRequest(
query_type=req.query_type,
query=req.query,
subtype=req.subtype,
function_name=req.function_name,
class_name=req.class_name,
module_name=req.module_name,
followup=req.followup,
previous_response=req.previous_response,
)
resp = engine.infer(inference_req)
return QueryResponse(
success=resp.success,
content=resp.content,
error=resp.error,
metadata=resp.metadata,
)
@app.get("/status", response_model=StatusResponse)
async def status():
"""
Check whether the codebase is indexed and the system is ready for queries.
"""
store = get_vector_store()
vstats = store.stats()
graph_loaded = False
graph_nodes = 0
if os.path.exists(CALL_GRAPH_PATH):
try:
graph = get_call_graph()
graph_loaded = graph.is_loaded()
graph_nodes = graph.stats()["total_nodes"]
except Exception:
pass
return StatusResponse(
indexed=store.is_indexed(),
class_chunks=vstats["class_chunks"],
function_chunks=vstats["function_chunks"],
total_chunks=vstats["total"],
graph_loaded=graph_loaded,
graph_nodes=graph_nodes,
)
@app.get("/tree", response_model=TreeResponse)
async def get_tree():
"""
Return the parsed codebase structure as a nested tree.
Used by the UI sidebar to render the codebase explorer.
"""
store = get_vector_store()
if not store.is_indexed():
return TreeResponse(
success=False,
error="Codebase not indexed yet. Call POST /index first."
)
try:
# Fetch both class and function chunks to reconstruct tree
class_results = store.get_all("class_chunks", limit=500)
func_results = store.get_all("function_chunks", limit=500)
# Group by module β file β classes/functions
modules: dict[str, dict[str, dict[str, set]]] = {}
# --- classes ---
for chunk in class_results:
mod = chunk.module
file = chunk.file
modules.setdefault(mod, {}).setdefault(file, {"classes": set(), "functions": set()})
modules[mod][file]["classes"].add(chunk.name)
# --- functions (top-level only) ---
for chunk in func_results:
if not chunk.class_name:
mod = chunk.module
file = chunk.file
modules.setdefault(mod, {}).setdefault(file, {"classes": set(), "functions": set()})
modules[mod][file]["functions"].add(chunk.name)
# Also fetch function chunks for top-level functions
func_results = store.get_all("function_chunks", limit=500)
func_by_file: dict[str, list[str]] = {}
for chunk in func_results:
if not chunk.class_name: # top-level only
func_by_file.setdefault(chunk.file, []).append(chunk.name)
# Build tree structure
all_files = set()
for files in modules.values():
for file_path in files:
all_files.add(file_path)
# Derive root directory name from common first path component
first_parts = [f.split("/")[0] for f in all_files if "/" in f]
root_name = first_parts[0] if first_parts else "codebase"
root_node = TreeNode(name=root_name, type="module")
for module_name, files in sorted(modules.items()):
module_node = TreeNode(name=module_name, type="module")
for file_path, content in sorted(files.items()):
file_node = TreeNode(
name=os.path.basename(file_path),
type="file"
)
for cls_name in sorted(content["classes"]):
file_node.children.append(
TreeNode(name=cls_name, type="class")
)
for fn_name in sorted(content["functions"]):
file_node.children.append(
TreeNode(name=fn_name, type="function")
)
module_node.children.append(file_node)
root_node.children.append(module_node)
return TreeResponse(success=True, tree=[root_node])
except Exception as e:
console.print(f"[red]β Tree build failed: {e}[/red]")
return TreeResponse(success=False, error=str(e))
# ββ Entry Point βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"main:app",
port=8000,
reload=True,
) |