"""Independent, sanitized readiness results for the two retrieval backends.""" from __future__ import annotations import json import sqlite3 from contextlib import closing from pathlib import Path from neo4j import Query from tools.graphrag.graph_db import create_neo4j_driver, get_neo4j_settings from tools.graphrag.graphrag import GRAPH_SCOPE from tools.sync.index_state import IndexUnavailableError, index_snapshot, sqlite_version from tools.retrieval.publication import PUBLICATION_POLICY_VERSION from tools.rag.search_core import verify_search_schema, embedding_index_info PROBE_TIMEOUT_SECONDS = 2.0 def readiness(ready: bool, reason: str | None = None, **details) -> dict: return { "ready": ready, "status": "ready" if ready else "unavailable", "reason": reason, **details, } def rag_readiness(db_file: Path, version: dict | None, *, embedding_ready: bool) -> dict: if not db_file.is_file(): return readiness(False, "index_missing") try: if version is not None and sqlite_version(db_file) != version: return readiness(False, "version_mismatch") with closing( sqlite3.connect(db_file.resolve().as_uri() + "?mode=ro", uri=True, timeout=1) ) as conn: conn.row_factory = sqlite3.Row verify_search_schema(conn) model, dimensions = embedding_index_info(conn) conn.execute("SELECT rowid FROM chunks_fts LIMIT 1").fetchone() if not embedding_ready: return readiness(False, "embedding_not_ready", embedding_model=model) return readiness(True, embedding_model=model, dimensions=dimensions, index_version=version) except (sqlite3.Error, RuntimeError, ValueError, OSError): return readiness(False, "index_invalid") def neo4j_readiness(version: dict | None) -> tuple[dict, dict]: try: settings = get_neo4j_settings() except RuntimeError: return readiness(False, "not_configured"), readiness(False, "neo4j_not_configured") try: with create_neo4j_driver( settings, connection_timeout=PROBE_TIMEOUT_SECONDS, connection_acquisition_timeout=PROBE_TIMEOUT_SECONDS, connection_write_timeout=PROBE_TIMEOUT_SECONDS, max_transaction_retry_time=0, max_connection_pool_size=1, ) as driver: driver.verify_connectivity() with driver.session(database=settings.database) as session: record = session.run( Query( """MATCH (n) WHERE n.graph_scope=$scope WITH count(n) AS nodes OPTIONAL MATCH (state:IndexState {id:$scope}) RETURN nodes, state.metadata_json AS metadata""", timeout=PROBE_TIMEOUT_SECONDS, ), scope=GRAPH_SCOPE, ).single() except Exception: # Credentials, connection strings and raw driver errors stay server-side. return readiness(False, "unreachable"), readiness(False, "neo4j_unavailable") neo4j = readiness(True) if not record or record["nodes"] == 0: return neo4j, readiness(False, "graph_empty") try: actual = json.loads(record["metadata"]) if record["metadata"] else None except (ValueError, TypeError): return neo4j, readiness(False, "graph_version_invalid") if version is not None and actual != version: return neo4j, readiness(False, "version_mismatch") if version is None or version.get("publication_policy") != PUBLICATION_POLICY_VERSION: return neo4j, readiness(False, "publication_sync_required") return neo4j, readiness(True, index_version=actual, nodes=record["nodes"]) def tools_readiness( db_file: Path, *, security_configured: bool, embedding_ready: bool ) -> tuple[dict, dict | None]: version = None try: with index_snapshot(db_file.parent) as version: rag = rag_readiness(db_file, version, embedding_ready=embedding_ready) neo4j, graph = neo4j_readiness(version) except IndexUnavailableError: neo4j, _ = neo4j_readiness(None) rag = readiness(False, "snapshot_unavailable") graph = readiness(False, "snapshot_unavailable") if not security_configured: rag = readiness(False, "api_not_configured") graph = readiness(False, "api_not_configured") return {"rag": rag, "neo4j": neo4j, "graphrag": graph}, version