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

import asyncio
from contextlib import asynccontextmanager
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 navi.core.container import create_container
from debug.eval.api import router as eval_router

structlog.configure(
    wrapper_class=structlog.make_filtering_bound_logger(
        getattr(logging, settings.log_level)
    ),
)

_base = Path(__file__).parent.parent


@asynccontextmanager
async def lifespan(app: FastAPI):
    log = structlog.get_logger()
    container = await create_container()
    app.state.container = container

    from navi.api.deps import set_container
    set_container(container)

    from navi.content_store import ensure_tables
    from navi.session_files import cleanup_loop
    from navi.auth import _ensure_auth_tables
    from navi.profiles._overrides import ensure_table, load_overrides
    from navi.api.routes.health import _check_embed
    from navi.core.scheduler import recall_scheduler_loop

    # Ensure auth tables first (navi_users is referenced by other DDL).
    for attempt in range(1, 6):
        try:
            await _ensure_auth_tables()
            await ensure_tables()
            break
        except Exception as e:
            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))

    # Apply persisted profile overrides
    try:
        pool = await container.database.pool()
        await ensure_table(pool)
        overrides = await load_overrides(pool)
        if overrides:
            for pid, is_admin_only in overrides.items():
                try:
                    profile = container.profile_registry.get(pid)
                    profile.is_admin_only = is_admin_only
                except Exception:
                    pass
            log.info("startup.profile_overrides_applied", count=len(overrides))
    except Exception:
        log.warning("startup.profile_overrides_failed", exc_info=True)

    # Check embedding backend health
    embed_status = await _check_embed()
    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 background tasks
    cleanup_task = asyncio.create_task(cleanup_loop(container.session_store))
    scheduler_task = asyncio.create_task(
        recall_scheduler_loop(container.scheduler, container.session_store, container.orchestrator)
    )

    yield

    # Shutdown
    scheduler_task.cancel()
    try:
        await scheduler_task
    except asyncio.CancelledError:
        pass
    cleanup_task.cancel()
    try:
        await cleanup_task
    except asyncio.CancelledError:
        pass

    from navi.tools.ssh_exec import close_all_connections
    close_all_connections()
    await container.shutdown()


app = FastAPI(
    title="Navi",
    description="Modular agent system — REST API and WebSocket",
    version="0.1.0",
    lifespan=lifespan,
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

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)

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


from fastapi.responses import RedirectResponse


@app.get("/sessions/{session_id}/files/{filename}", include_in_schema=False)
async def legacy_session_file_redirect(session_id: str, filename: str, download: bool = False):
    target = f"/api/sessions/{session_id}/files/{filename}"
    if download:
        target += "?download=1"
    return RedirectResponse(url=target, status_code=301)


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


@app.get("/admin", include_in_schema=False)
async def admin_panel() -> FileResponse:
    return FileResponse("admin/index.html", headers={"Cache-Control": "no-store"})