"""FastAPI application — entry point for the API server.""" from __future__ import annotations import logging from contextlib import asynccontextmanager import httpx from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from nsct.config import AppSettings logger = logging.getLogger(__name__) # --------------------------------------------------------------------------- # Lifespan # --------------------------------------------------------------------------- @asynccontextmanager async def lifespan(app: FastAPI): """Application lifespan — startup and shutdown hooks.""" config: AppSettings = app.state.config logger.info("NSCT API starting up — version %s", app.state.version) # Pre-flight: validate LLM connectivity llm_base = config.llm.base_url if config and config.llm and config.llm.base_url else "" if llm_base: try: async with httpx.AsyncClient(timeout=5.0) as client: resp = await client.get(f"{llm_base}/models") if resp.status_code == 200: logger.info("LLM provider reachable — model endpoint returned 200") else: logger.warning("LLM provider returned %s", resp.status_code) except Exception as exc: logger.warning("LLM connectivity check failed: %s", exc) yield # --------------------------------------------------------------------------- # App factory # --------------------------------------------------------------------------- def create_app() -> FastAPI: """Create the FastAPI application with all routes and middleware.""" config = AppSettings.from_env() app = FastAPI( title="NSCT API", description="Neutral Search Crawler Tool — API server", version="0.1.0", docs_url="/docs" if config.debug else None, redoc_url="/redoc" if config.debug else None, lifespan=lifespan, ) app.state.config = config app.state.version = "0.1.0" # CORS — allow local development app.add_middleware( CORSMiddleware, allow_origins=["http://localhost:3000", "http://localhost:5173", "http://127.0.0.1:3000"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Mount health router from nsct.api.health import router as health_router app.include_router(health_router, tags=["system"]) # Mount search router from nsct.api.search import router as search_router app.include_router(search_router, tags=["search"]) # Mount crawler router from nsct.api.crawler import router as crawler_router app.include_router(crawler_router, tags=["crawler"]) # Mount planner router from nsct.api.planner import router as planner_router app.include_router(planner_router, tags=["planner"]) # Mount claim extraction router (Stage 5) from nsct.api.claims import router as claims_router app.include_router(claims_router, tags=["research"]) # Mount claim clustering router (Stage 7) from nsct.api.stage7 import router as stage7_router app.include_router(stage7_router, tags=["research"]) # Mount evidence scoring router (Stage 8) from nsct.api.stage8 import router as stage8_router app.include_router(stage8_router, tags=["research"]) # Mount synthesis router (Stage 9) from nsct.api.synthesis import router as synthesis_router app.include_router(synthesis_router, tags=["research"]) # Mount vision router (Stage 10) from nsct.api.vision import router as vision_router app.include_router(vision_router, tags=["vision"]) # Mount audio router (Stage 11) from nsct.api.audio import router as audio_router app.include_router(audio_router, tags=["audio"]) # Mount REST API router (Stage 14) from nsct.api.rest_research import router as rest_research_router app.include_router(rest_research_router, tags=["research"]) return app app = create_app()