NeuroOracle / neurooracle /src /storage.py
zxcvb20001's picture
Deploy: atom-aligned KG + UI
dc6bcac verified
Raw
History Blame Contribute Delete
4.67 kB
"""JSON serialization and deserialization for the knowledge graph."""
from __future__ import annotations
import gzip
import io
import json
import logging
from datetime import datetime
from pathlib import Path
from typing import Optional
from .graph_manager import KnowledgeGraph
from .schema import ConceptNode, DISPLAY_TIERS_DEFAULT, Edge
logger = logging.getLogger(__name__)
DEFAULT_PATH = Path(__file__).parent.parent / "data" / "knowledge_graph.json"
def _resolve_read_path(path: Path) -> Path:
"""If `path` doesn't exist but `path.gz` does, return the gz variant."""
if path.exists():
return path
gz = path.with_suffix(path.suffix + ".gz")
if gz.exists():
return gz
return path
def _open_for_read(path: Path):
"""Open a JSON file for text read, transparently handling .gz."""
if str(path).endswith(".gz"):
return io.TextIOWrapper(gzip.open(path, "rb"), encoding="utf-8")
return open(path, "r", encoding="utf-8")
def _open_for_write(path: Path):
"""Open a JSON file for text write, transparently handling .gz (level 9)."""
path.parent.mkdir(parents=True, exist_ok=True)
if str(path).endswith(".gz"):
return io.TextIOWrapper(gzip.open(path, "wb", compresslevel=9), encoding="utf-8")
return open(path, "w", encoding="utf-8")
def save_graph(kg: KnowledgeGraph, path: Optional[Path] = None) -> Path:
"""Save knowledge graph to JSON file. Compresses transparently if path ends with .gz."""
path = Path(path) if path else DEFAULT_PATH
path.parent.mkdir(parents=True, exist_ok=True)
edges = []
for src, tgt, edata in kg.G.edges(data=True):
# Ensure source_id and target_id are always present
edge_dict = dict(edata)
edge_dict["source_id"] = src
edge_dict["target_id"] = tgt
edges.append(edge_dict)
data = {
"metadata": {
"version": "0.1",
"created": datetime.now().isoformat(),
"stats": kg.stats(),
},
"concepts": {nid: node.to_dict() for nid, node in kg._index.items()},
"edges": edges,
}
with _open_for_write(path) as f:
json.dump(data, f, ensure_ascii=False, indent=2)
logger.info(f"saved graph to {path}: {kg.stats()['n_concepts']} concepts, {kg.stats()['n_edges']} edges")
return path
def load_graph(path: Optional[Path] = None) -> KnowledgeGraph:
"""Load knowledge graph from JSON file. Auto-detects .gz fallback."""
path = Path(path) if path else DEFAULT_PATH
path = _resolve_read_path(path)
if not path.exists():
logger.info(f"no graph file at {path}, returning empty graph")
return KnowledgeGraph()
with _open_for_read(path) as f:
data = json.load(f)
kg = KnowledgeGraph()
for nid, ndata in data.get("concepts", {}).items():
node = ConceptNode.from_dict(ndata)
kg.add_concept(node)
for edata in data.get("edges", []):
try:
edge = Edge.from_dict(edata)
kg.add_edge(edge)
except (TypeError, KeyError) as e:
logger.warning(f"skipping malformed edge: {e}")
continue
stats = kg.stats()
logger.info(f"loaded graph from {path}: {stats['n_concepts']} concepts, {stats['n_edges']} edges")
return kg
def save_display_graph(
kg: KnowledgeGraph,
path: Path,
tiers: Optional[set[str]] = None,
) -> Path:
"""Save the display-tier subgraph to JSON, for HF Space / public consumption.
Drops provenance / inverse / bridge edges and orphaned nodes — see
`KnowledgeGraph.export_display_subgraph`.
"""
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
sub = kg.export_display_subgraph(tiers=tiers)
keep_ids = set(sub.nodes())
edges = []
for src, tgt, edata in sub.edges(data=True):
edge_dict = dict(edata)
edge_dict["source_id"] = src
edge_dict["target_id"] = tgt
edges.append(edge_dict)
data = {
"metadata": {
"version": "0.1-display",
"created": datetime.now().isoformat(),
"tiers": sorted(tiers if tiers is not None else DISPLAY_TIERS_DEFAULT),
"n_concepts": len(keep_ids),
"n_edges": len(edges),
},
"concepts": {
nid: node.to_dict()
for nid, node in kg._index.items()
if nid in keep_ids
},
"edges": edges,
}
with _open_for_write(path) as f:
json.dump(data, f, ensure_ascii=False, indent=2)
logger.info(f"saved display graph to {path}: {len(keep_ids)} concepts, {len(edges)} edges")
return path