84 lines
4.2 KiB
Python
84 lines
4.2 KiB
Python
"""Read-only readiness of versioned content and the configured answer model."""
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
from functools import lru_cache
|
|
from pathlib import Path
|
|
|
|
from neo4j import Query
|
|
|
|
from tools.answers.model_client import generation_status
|
|
from tools.graphrag.graph_db import create_neo4j_driver, get_neo4j_settings
|
|
from tools.knowledge.publication import read_state, state_matches_graph
|
|
from tools.knowledge.store import SCOPE
|
|
from tools.runtime.tool_health import PROBE_TIMEOUT_SECONDS, readiness
|
|
from tools.sync.index_state import IndexUnavailableError, index_snapshot
|
|
|
|
|
|
@lru_cache(maxsize=2)
|
|
def _checkpoint(path: str, inode: int, size: int, modified_ns: int, changed_ns: int):
|
|
content = Path(path).read_bytes()
|
|
index = json.loads(content)
|
|
return {"version": index.get("version"), "index_version": index.get("index_version"),
|
|
"sha256": hashlib.sha256(content).hexdigest(), "bytes": len(content)}
|
|
|
|
|
|
def content_readiness(db_file: Path, version: dict | None, *, graph_available: bool) -> dict:
|
|
try:
|
|
# tools_readiness released its lock: recheck the generation before reading.
|
|
with index_snapshot(db_file.parent) as current:
|
|
if not version or current != version:
|
|
return readiness(False, "snapshot_changed")
|
|
state = read_state(db_file.parent)
|
|
path = db_file.parent / "knowledge/index.json"
|
|
if not path.is_file():
|
|
return readiness(False, "content_index_missing")
|
|
if not isinstance(state, dict) or state.get("schema") != 1:
|
|
return readiness(False, "content_publication_required")
|
|
st = path.stat()
|
|
actual = _checkpoint(str(path), st.st_ino, st.st_size, st.st_mtime_ns, st.st_ctime_ns)
|
|
if state.get("index_version") != version or actual["index_version"] != version:
|
|
return readiness(False, "content_version_mismatch")
|
|
if (actual["version"] != state.get("version")
|
|
or actual["sha256"] != state.get("checkpoint_sha256")
|
|
or actual["bytes"] != state.get("checkpoint_bytes")):
|
|
return readiness(False, "content_checkpoint_invalid")
|
|
details = {"index_version": version, "content_version": state["version"],
|
|
"checkpoint_sha256": actual["sha256"], "profiles": state.get("profiles"),
|
|
"units": state.get("units")}
|
|
graph = readiness(False, "content_graph_disabled")
|
|
if state.get("graph_ready"):
|
|
graph = _graph_readiness(state) if graph_available else readiness(False, "neo4j_unavailable")
|
|
return {**readiness(True, **details), "graph": graph}
|
|
except IndexUnavailableError:
|
|
return readiness(False, "content_publication_incomplete")
|
|
except (OSError, ValueError, TypeError, KeyError):
|
|
return readiness(False, "content_checkpoint_invalid")
|
|
|
|
|
|
def _graph_readiness(state):
|
|
try:
|
|
settings = get_neo4j_settings()
|
|
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:
|
|
with driver.session(database=settings.database) as session:
|
|
row = session.run(Query(
|
|
"MATCH (s:KnowledgeState {scope:$scope}) RETURN properties(s) AS state",
|
|
timeout=PROBE_TIMEOUT_SECONDS), scope=SCOPE).single()
|
|
actual = dict(row["state"]) if row else None
|
|
if not state_matches_graph(state, actual):
|
|
return readiness(False, "content_graph_version_mismatch")
|
|
return readiness(True, content_version=state["version"])
|
|
except Exception:
|
|
return readiness(False, "content_graph_unavailable")
|
|
|
|
|
|
def answer_capabilities(db_file, tools, version):
|
|
content = content_readiness(db_file, version, graph_available=tools["neo4j"]["ready"])
|
|
generation = generation_status()
|
|
return {"text_search": tools["rag"], "metadata_graph": tools["graphrag"],
|
|
"knowledge": content, "generation": generation}
|