Spaces:
Sleeping
Sleeping
| """ | |
| 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 ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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 βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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) | |
| async def health(): | |
| """Simple health check.""" | |
| return {"status": "ok", "service": "Codebase Oracle"} | |
| 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) | |
| 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)}") | |
| 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, | |
| ) | |
| 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, | |
| ) | |
| 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, | |
| ) |