"""Liveness and readiness routes for the EPUB search service.""" from __future__ import annotations import logging from http import HTTPStatus from typing import TYPE_CHECKING from fastapi import APIRouter from fastapi.responses import JSONResponse from sqlalchemy import literal, select from sqlalchemy.exc import SQLAlchemyError from python.ebook_search.api.dependencies import ( AppConfig, # noqa: TC001 FastAPI resolves this annotated dependency at runtime ) from python.ebook_search.bm25_corpus import bm25_index_exists, bm25_index_path, read_bm25_manifest from python.ebook_search.llm_interface import check_chat_endpoint, check_embedding_endpoint from python.fastapi_tools import DbSession # noqa: TC001 FastAPI resolves this annotated dependency at runtime if TYPE_CHECKING: from sqlalchemy.orm import Session from python.ebook_search.config import EbookSearchConfig logger = logging.getLogger(__name__) router = APIRouter() @router.get("/health") def health() -> dict[str, str]: """Liveness probe that returns ok without touching dependencies.""" return {"status": "ok"} @router.get("/ready") def ready(config: AppConfig, session: DbSession) -> JSONResponse: """Readiness probe reporting database, embedding endpoint, and BM25 index status.""" database_ok = check_database(session) embedding_ok = check_embedding_endpoint(config) chat_status = chat_endpoint_status(config) bm25_status = check_bm25_status(config) checks = { "database": "ok" if database_ok else "fail", "embedding": "ok" if embedding_ok else "fail", "chat": chat_status, "bm25": bm25_status, } if not database_ok: status = "unavailable" status_code = HTTPStatus.SERVICE_UNAVAILABLE elif not embedding_ok or chat_status == "fail" or bm25_status == "missing": status = "degraded" status_code = HTTPStatus.OK else: status = "ready" status_code = HTTPStatus.OK logger.info( "ebook_ready_check status=%s database=%s embedding=%s chat=%s bm25=%s", status, database_ok, embedding_ok, chat_status, bm25_status, ) return JSONResponse(content={"status": status, "checks": checks}, status_code=status_code) def chat_endpoint_status(config: EbookSearchConfig) -> str: """Return the answering chat endpoint status, or disabled when answers are off.""" if not config.answer_enabled: return "disabled" return "ok" if check_chat_endpoint(config) else "fail" def check_database(session: Session) -> bool: """Return whether the database answers a trivial query.""" try: session.execute(select(literal(1))) except SQLAlchemyError as error: logger.warning("ebook_ready_database_unavailable error=%s", error) return False return True def check_bm25_status(config: EbookSearchConfig) -> str: """Return the persisted BM25 index status without loading it into memory.""" index_path = bm25_index_path(config) manifest = read_bm25_manifest(index_path) if manifest is None or not bm25_index_exists(index_path, manifest): return "missing" if manifest.chunk_count == 0: return "empty" return "ok"