dp-zp-agent/tools/runtime/answer_health.py

92 lines
4.9 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 {"extraction_schema":index.get("extraction_schema"),"extraction_policy":index.get("extraction_policy"),
"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")
from tools.knowledge.evidence import EXTRACTION_SCHEMA, EXTRACTION_POLICY
if actual["extraction_schema"] != EXTRACTION_SCHEMA or actual["extraction_policy"] != EXTRACTION_POLICY:
return readiness(False,"content_extraction_policy_mismatch")
details = {"extraction_schema":EXTRACTION_SCHEMA,"extraction_policy":EXTRACTION_POLICY,
"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")
from tools.knowledge.store import CONTENT_GRAPH_SCHEMA
if actual.get('projection_schema') != CONTENT_GRAPH_SCHEMA:
return readiness(False, 'content_graph_projection_required')
return readiness(True, content_version=state["version"], projection_schema=CONTENT_GRAPH_SCHEMA)
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}