from __future__ import annotations import logging from contextlib import asynccontextmanager from fastapi import FastAPI from .clients.bge import BGEClient from .clients.memory_api import MemoryApiClient from .clients.qdrant import QdrantReader from .config import get_settings from .models import HealthResponse from .routes import ingest as ingest_routes from .routes import search as search_routes @asynccontextmanager async def lifespan(app: FastAPI): settings = get_settings() logging.basicConfig( level=getattr(logging, settings.log_level.upper(), logging.INFO), format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", ) logger = logging.getLogger(__name__) logger.info("Starting artenschutz-router") logger.info(" Qdrant: %s (collection: %s)", settings.qdrant_url, settings.qdrant_collection) logger.info(" BGE TEI: %s", settings.bge_tei_url) logger.info(" memory-api: %s", settings.memory_api_url) logger.info(" agent_id: %s", settings.artenschutz_agent_id) qdrant = QdrantReader(url=settings.qdrant_url, collection=settings.qdrant_collection) bge = BGEClient(base_url=settings.bge_tei_url, timeout_s=settings.request_timeout_s) memory_api = MemoryApiClient(base_url=settings.memory_api_url, timeout_s=settings.request_timeout_s) app.state.settings = settings app.state.qdrant = qdrant app.state.bge = bge app.state.memory_api = memory_api try: yield finally: await qdrant.close() await bge.close() await memory_api.close() app = FastAPI( title="artenschutz-router", description=( "Sidecar router for Artenschutz domain queries against agent.memory.system. " "Writes proxy memory-api /memory/store; reads go directly to Qdrant." ), version="0.1.0", lifespan=lifespan, ) app.include_router(search_routes.router) app.include_router(ingest_routes.router) @app.get("/health", response_model=HealthResponse, tags=["health"]) async def health() -> HealthResponse: """Live-probes Qdrant, BGE-M3 TEI, memory-api. `degraded` if any fail.""" qdrant_ok = True bge_ok = True memory_api_ok = False try: await app.state.qdrant._client.get_collections() # noqa: SLF001 except Exception: qdrant_ok = False try: # TEI exposes /health; falling back to /info on older versions. resp = await app.state.bge._client.get("/health") # noqa: SLF001 bge_ok = resp.status_code == 200 except Exception: bge_ok = False try: memory_api_ok = await app.state.memory_api.health() except Exception: memory_api_ok = False overall = "ok" if all([qdrant_ok, bge_ok, memory_api_ok]) else "degraded" return HealthResponse( status=overall, qdrant=qdrant_ok, bge=bge_ok, memory_api=memory_api_ok, )