Spaces:
Running
Running
| """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 | |