Progressive enhancement chain: GeoIP auto-detect -> browser GPS -> manual entry -> works without any location. When set, the observer coordinates are injected into chat requests so the LLM can answer "Where is Jupiter?" with actual azimuth/elevation from the user's location instead of placeholder coordinates. Backend: - GeoIP service (MaxMind GeoLite2-City) with lazy init, private IP filtering, and IPv4-mapped IPv6 unwrapping - GET /api/geolocate endpoint (sync to avoid blocking event loop on mmap I/O, rightmost X-Forwarded-For for Caddy trust chain) - ObserverContext model on both chat endpoints with shared _observer_prefix() helper that sanitizes label against prompt injection Frontend: - Location bar between header and messages with pin icon, GPS button, edit/clear controls, and inline manual entry parser (accepts "40.7N 74.0W", decimal lat/lon, pg_orrery observer format) - GeoIP auto-detect on first visit, localStorage persistence - Observer coordinates sent with every chat request Infrastructure: - api-data volume for GeoIP database, Caddy handle_4 for /api/geolocate - update_geoip.sh using MaxMind Basic auth (key stays out of ps/proc)
55 lines
1.6 KiB
Python
55 lines
1.6 KiB
Python
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,
|
|
)
|