"""FastAPI application entry point."""
import asyncio
from pathlib import Path
import logging
import structlog
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from navi.api.routes import agents, auth, health, messages, sessions, webhooks
from navi.api.routes.admin import router as admin_router
from navi.api.websocket import router as ws_router
from navi.config import settings
from debug.eval.api import router as eval_router
structlog.configure(
wrapper_class=structlog.make_filtering_bound_logger(
getattr(logging, settings.log_level)
),
)
app = FastAPI(
title="Navi",
description="Modular agent system — REST API and WebSocket",
version="0.1.0",
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Keep reference to background cleanup task so unhandled exceptions are not lost
_cleanup_task = None
app.include_router(health.router)
app.include_router(auth.router)
app.include_router(agents.router)
app.include_router(sessions.router)
app.include_router(messages.router)
app.include_router(ws_router)
app.include_router(webhooks.router)
app.include_router(admin_router)
app.include_router(eval_router)
_base = Path(__file__).parent.parent
app.mount("/assets", StaticFiles(directory=str(_base / "webclient" / "dist" / "assets")), name="assets")
app.mount("/images", StaticFiles(directory=str(_base / "webclient" / "dist" / "images")), name="images")
app.mount("/content-viewers", StaticFiles(directory=str(_base / "webclient" / "dist" / "content-viewers")), name="content_viewers")
app.mount("/content", StaticFiles(directory=str(_base / "navi" / "content")), name="content")
@app.on_event("startup")
async def _on_startup() -> None:
from navi.api.deps import get_registries, get_session_store
from navi.content_store import ensure_tables
from navi.session_files import cleanup_loop
from navi.auth import _ensure_auth_tables
# Ensure content store and auth tables exist (retry for race with Docker compose)
for attempt in range(1, 6):
try:
await ensure_tables()
await _ensure_auth_tables()
break
except Exception as e:
log = structlog.get_logger()
if attempt < 5:
log.warning("startup.ensure_tables_retry", attempt=attempt, error=str(e))
await asyncio.sleep(2)
else:
log.error("startup.ensure_tables_failed", error=str(e))
# Initialize registries before embed health check. The memory store gets its
# embedding backend wired during registry construction.
get_registries()
# Check embedding backend health and log status
from navi.api.routes.health import _check_embed
embed_status = await _check_embed()
log = structlog.get_logger()
if embed_status["ok"]:
log.info("startup.embed_ready", backend=embed_status["backend"])
else:
log.warning("startup.embed_unavailable", backend=embed_status["backend"], error=embed_status["error"])
# Start session file cleanup background task
global _cleanup_task
_cleanup_task = asyncio.create_task(cleanup_loop(get_session_store()))
@app.on_event("shutdown")
async def _on_shutdown() -> None:
from navi.tools.ssh_exec import close_all_connections
close_all_connections()
@app.get("/", include_in_schema=False)
async def index() -> FileResponse:
return FileResponse(str(_base / "webclient" / "dist" / "index.html"), headers={"Cache-Control": "no-store"})
@app.get("/debug", include_in_schema=False)
async def debug() -> FileResponse:
return FileResponse("debug/index.html", headers={"Cache-Control": "no-store"})
@app.get("/debug/eval", include_in_schema=False)
async def debug_eval() -> FileResponse:
return FileResponse("debug/eval/index.html", headers={"Cache-Control": "no-store"})