import logging from contextlib import asynccontextmanager import uvicorn from fastapi import FastAPI from fastmcp.utilities.lifespan import combine_lifespans from orrery_search.config import settings from orrery_search.db import engine from orrery_search.mcp import mcp from orrery_search.mcp.chat import close_chat_client from orrery_search.mcp.query import close_query_pool from orrery_search.routers import chat, health, observer, search from orrery_search.services.geoip import close_reader as geoip_close from orrery_search.services.geoip import init_reader as geoip_init logging.basicConfig(level=logging.WARNING, format="%(name)s: %(message)s") logging.getLogger("orrery_search").setLevel(logging.INFO) @asynccontextmanager async def lifespan(app: FastAPI): geoip_init(settings.geoip_db_path) yield geoip_close() await close_chat_client() await close_query_pool() await engine.dispose() mcp_app = mcp.http_app(path="/", stateless_http=True) app = FastAPI( title="pg_orrery Search", description="Semantic search and chat for the pg_orrery documentation", version="2026.03.01", lifespan=combine_lifespans(lifespan, mcp_app.lifespan), ) app.include_router(search.router, prefix="/api/search", tags=["search"]) app.include_router(chat.router, prefix="/api/chat", tags=["chat"]) app.include_router(observer.router, prefix="/api", tags=["observer"]) app.include_router(health.router, tags=["health"]) app.mount("/mcp", mcp_app) def run(): uvicorn.run( "orrery_search.main:app", host=settings.api_host, port=settings.api_port, log_level=settings.api_log_level, reload=True, )