Newer
Older
navi-1 / navi / main.py
"""FastAPI application entry point."""

import asyncio
from pathlib import Path

import logging

import structlog
from fastapi import FastAPI, Request
from fastapi.responses import FileResponse, Response
from fastapi.staticfiles import StaticFiles

from navi.api.routes import agents, health, messages, sessions
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",
)

# Keep reference to background cleanup task so unhandled exceptions are not lost
_cleanup_task = None

app.include_router(health.router)
app.include_router(agents.router)
app.include_router(sessions.router)
app.include_router(messages.router)
app.include_router(ws_router)
app.include_router(eval_router)

_base = Path(__file__).parent.parent
_static_dir = _base / "old_webclient"
if _static_dir.exists():
    app.mount("/static", StaticFiles(directory=str(_static_dir)), name="static")
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_session_store
    from navi.content_store import ensure_tables
    from navi.session_files import cleanup_loop
    # Ensure content store tables exist (retry for race with Docker compose)
    for attempt in range(1, 6):
        try:
            await ensure_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))
    # 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.middleware("http")
async def no_cache_static(request: Request, call_next) -> Response:
    response = await call_next(request)
    if request.url.path.startswith("/static/"):
        response.headers["Cache-Control"] = "no-store"
    return response


@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"})