cartographer / backend /main.py
umanggarg's picture
Phase 4: artifact provenance β€” model-tracking + debug endpoint
9eea8ac
"""
main.py β€” FastAPI application entry point.
This file is intentionally thin: lifespan (service wiring), app creation,
CORS, MCP mount, router registration, and health check only.
All route logic lives in backend/routers/:
ingestion.py β€” /ingest, /repos
query.py β€” /search, /query, /query/stream
agent.py β€” /agent/*
diagrams.py β€” /repos/{owner}/{name}/tour|diagram
mcp_routes.py β€” /mcp-status, /mcp-prompt
Shared service instances and dependency providers live in backend/dependencies.py.
"""
import os
from contextlib import asynccontextmanager
import posthog as _posthog
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from backend.config import settings
from backend.dependencies import services
from backend.mcp_server import mcp, init_services as init_mcp_services
from backend.mcp_client import MCPClient
from backend.services.ingestion_service import IngestionService
from backend.services.generation import GenerationService
from backend.services.agent import AgentService
from backend.services.diagram_service import DiagramService
from backend.services.repo_map_service import RepoMapService
from backend.services.readme_service import ReadmeService
from retrieval.retrieval import RetrievalService, Reranker
from ingestion.qdrant_store import QdrantStore
from ingestion.embedder import Embedder
from backend.routers import ingestion, query, agent, diagrams, mcp_routes, sessions
@asynccontextmanager
async def lifespan(app: FastAPI):
"""
Startup: initialise all services and wire MCP.
Shutdown: flush analytics.
Services are expensive to create (model loads, connection pools).
We create them once here and store them on the `services` container
imported by all routers via backend/dependencies.py.
"""
if settings.posthog_api_key:
_posthog.api_key = settings.posthog_api_key
_posthog.host = settings.posthog_host
print("Starting up β€” loading models and connecting to Qdrant...")
# One shared Embedder (600MB model) and one shared QdrantStore (connection pool).
# Both are passed into every service that needs them β€” no duplicate loads.
_embedder = Embedder()
_qdrant_store = QdrantStore()
_reranker = Reranker()
services.generation = GenerationService()
services.retrieval = RetrievalService(
embedder=_embedder, store=_qdrant_store,
reranker=_reranker, gen=services.generation,
)
services.ingestion = IngestionService(
store=_qdrant_store, embedder=_embedder, gen=services.generation,
)
services.diagram = DiagramService(_qdrant_store, services.generation)
services.repo_map = RepoMapService(_qdrant_store)
services.readme = ReadmeService(services.repo_map, services.generation, _qdrant_store)
init_mcp_services(services.retrieval, _qdrant_store)
if any([
settings.cerebras_api_key, settings.groq_api_key,
settings.gemini_api_key, settings.openrouter_api_key,
settings.anthropic_api_key,
]):
_port = int(os.getenv("PORT", "8000"))
services.mcp_client = MCPClient(server_url=f"http://localhost:{_port}/mcp")
services.agent = AgentService(services.mcp_client, repo_map_svc=services.repo_map)
print("AgentService ready (MCP-powered agentic RAG enabled).")
else:
print("No LLM API key found β€” /agent/* endpoints disabled.")
print("All services ready.\n")
async with mcp.session_manager.run():
yield
if settings.posthog_api_key:
_posthog.flush()
print("Shutting down.")
# ── App ────────────────────────────────────────────────────────────────────────
app = FastAPI(
title="Cartographer",
description="Ask questions about any GitHub repository. Powered by MCP.",
version="0.2.0",
lifespan=lifespan,
)
# ── CORS ───────────────────────────────────────────────────────────────────────
_origins = []
if settings.frontend_url:
_origins.append(settings.frontend_url)
app.add_middleware(
CORSMiddleware,
allow_origins=_origins,
allow_origin_regex=r"http://localhost:\d+",
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ── MCP sub-app ────────────────────────────────────────────────────────────────
# Mounted before routers so /mcp is handled by FastMCP, not FastAPI.
app.mount("/mcp", mcp.streamable_http_app())
# ── Routers ────────────────────────────────────────────────────────────────────
app.include_router(ingestion.router)
app.include_router(query.router)
app.include_router(agent.router)
app.include_router(diagrams.router)
app.include_router(mcp_routes.router)
app.include_router(sessions.router)
app.include_router(sessions.artifacts_router)
# ── Health check ───────────────────────────────────────────────────────────────
@app.get("/health", tags=["meta"])
async def health():
"""Simple liveness check."""
return {"status": "ok"}