import asyncio import uuid from collections.abc import AsyncIterator, Awaitable, Callable from contextlib import asynccontextmanager from fastapi import FastAPI, Request, Response from app.api import api_router from app.db import async_session_factory from app.errors import register_exception_handlers from app.help_import import import_help_documents from app.ingestion.handlers import ensure_retention_scheduled from app.ingestion.queue import run_queue from app.llm.overrides import bootstrap_llm_settings from app.llm.overrides import load_config as load_llm_config from app.log import correlation_id, setup_logging from app.prompts.overrides import load_config as load_prompt_config from app.template_catalog import seed_starter_templates @asynccontextmanager async def lifespan(app: FastAPI) -> AsyncIterator[None]: setup_logging() async with async_session_factory() as db: await ensure_retention_scheduled(db) await seed_starter_templates(db) await import_help_documents(db) await bootstrap_llm_settings(db) await load_llm_config(db) await load_prompt_config(db) stop_event = asyncio.Event() queue_task = asyncio.create_task(run_queue(stop_event)) yield stop_event.set() try: await asyncio.wait_for(queue_task, timeout=10) except TimeoutError: # pragma: no cover — a handler refused to finish queue_task.cancel() app = FastAPI(title="Pablan", version="0.1.0", lifespan=lifespan) register_exception_handlers(app) @app.middleware("http") async def add_correlation_id( request: Request, call_next: Callable[[Request], Awaitable[Response]] ) -> Response: cid = request.headers.get("x-request-id") or uuid.uuid4().hex[:16] token = correlation_id.set(cid) try: response = await call_next(request) finally: correlation_id.reset(token) response.headers["x-request-id"] = cid return response app.include_router(api_router)