"""AtoCore — FastAPI application entry point.""" from contextlib import asynccontextmanager from fastapi import APIRouter, FastAPI from fastapi.routing import APIRoute from atocore import __version__ from atocore.api.routes import router import atocore.config as _config from atocore.context.project_state import init_project_state_schema from atocore.engineering.service import init_engineering_schema from atocore.ingestion.pipeline import get_source_status from atocore.models.database import init_db from atocore.observability.logger import get_logger, setup_logging log = get_logger("main") @asynccontextmanager async def lifespan(app: FastAPI): """Run setup before the first request and teardown after shutdown. Replaces the deprecated ``@app.on_event("startup")`` hook with the modern ``lifespan`` context manager. Setup runs synchronously (the underlying calls are blocking I/O) so no await is needed; the function still must be async per the FastAPI contract. """ setup_logging() _config.ensure_runtime_dirs() init_db() init_project_state_schema() init_engineering_schema() log.info( "startup_ready", env=_config.settings.env, db_path=str(_config.settings.db_path), chroma_path=str(_config.settings.chroma_path), source_status=get_source_status(), ) yield # No teardown work needed today; SQLite connections are short-lived # and the Chroma client cleans itself up on process exit. app = FastAPI( title="AtoCore", description="Personal Context Engine for LLM interactions", version=__version__, lifespan=lifespan, ) app.include_router(router) # Public API v1 — stable contract for external clients (AKC, OpenClaw, etc.). # Paths listed here are re-mounted under /v1 as aliases of the existing # unversioned handlers. Unversioned paths continue to work; new endpoints # land at the latest version; breaking schema changes bump the prefix. _V1_PUBLIC_PATHS = { "/entities", "/entities/{entity_id}", "/entities/{entity_id}/promote", "/entities/{entity_id}/reject", "/entities/{entity_id}/invalidate", "/entities/{entity_id}/supersede", "/entities/{entity_id}/audit", "/relationships", "/ingest", "/ingest/sources", "/context/build", "/query", "/projects", "/projects/{project_name}", "/projects/{project_name}/refresh", "/projects/{project_name}/mirror", "/projects/{project_name}/mirror.html", "/memory", "/memory/{memory_id}", "/memory/{memory_id}/audit", "/memory/{memory_id}/promote", "/memory/{memory_id}/reject", "/memory/{memory_id}/invalidate", "/memory/{memory_id}/supersede", "/project/state", "/project/state/{project_name}", "/interactions", "/interactions/{interaction_id}", "/interactions/{interaction_id}/reinforce", "/interactions/{interaction_id}/extract", "/health", "/sources", "/stats", # Issue F: asset store + evidence query "/assets", "/assets/{asset_id}", "/assets/{asset_id}/thumbnail", "/assets/{asset_id}/meta", "/entities/{entity_id}/evidence", # Issue D: engineering query surface (decisions, systems, components, # gaps, evidence, impact, changes) "/engineering/projects/{project_name}/systems", "/engineering/decisions", "/engineering/components/{component_id}/requirements", "/engineering/changes", "/engineering/gaps", "/engineering/gaps/orphan-requirements", "/engineering/gaps/risky-decisions", "/engineering/gaps/unsupported-claims", "/engineering/impact", "/engineering/evidence", } _v1_router = APIRouter(prefix="/v1", tags=["v1"]) for _route in list(router.routes): if isinstance(_route, APIRoute) and _route.path in _V1_PUBLIC_PATHS: _v1_router.add_api_route( _route.path, _route.endpoint, methods=list(_route.methods), response_model=_route.response_model, response_class=_route.response_class, name=f"v1_{_route.name}", include_in_schema=True, ) app.include_router(_v1_router) if __name__ == "__main__": import uvicorn uvicorn.run( "atocore.main:app", host=_config.settings.host, port=_config.settings.port, reload=True, )