royalsolution-ontosight 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ontosight/__init__.py +42 -0
- ontosight/core/__init__.py +30 -0
- ontosight/core/storage/__init__.py +13 -0
- ontosight/core/storage/base.py +55 -0
- ontosight/core/storage/graph.py +386 -0
- ontosight/core/storage/hypergraph.py +401 -0
- ontosight/core/storage/node.py +179 -0
- ontosight/core/views/__init__.py +17 -0
- ontosight/core/views/graph.py +148 -0
- ontosight/core/views/hypergraph.py +111 -0
- ontosight/core/views/node.py +119 -0
- ontosight/logging_config.py +237 -0
- ontosight/server/__init__.py +5 -0
- ontosight/server/app.py +61 -0
- ontosight/server/models/__init__.py +1 -0
- ontosight/server/models/api.py +151 -0
- ontosight/server/routes/__init__.py +1 -0
- ontosight/server/routes/chat.py +117 -0
- ontosight/server/routes/codegraph.py +62 -0
- ontosight/server/routes/data.py +203 -0
- ontosight/server/routes/meta.py +88 -0
- ontosight/server/routes/rankings.py +26 -0
- ontosight/server/routes/search.py +105 -0
- ontosight/server/state.py +296 -0
- ontosight/static/assets/GraphView-BAsvQRbE.js +2 -0
- ontosight/static/assets/GraphView-BAsvQRbE.js.map +1 -0
- ontosight/static/assets/HypergraphView-BeKyuFX9.js +2 -0
- ontosight/static/assets/HypergraphView-BeKyuFX9.js.map +1 -0
- ontosight/static/assets/NodeView-CT1Q_Qn-.js +2 -0
- ontosight/static/assets/NodeView-CT1Q_Qn-.js.map +1 -0
- ontosight/static/assets/PaginatedGridView-LOV9NPu5.js +12 -0
- ontosight/static/assets/PaginatedGridView-LOV9NPu5.js.map +1 -0
- ontosight/static/assets/index-BIj2skbt.js +181 -0
- ontosight/static/assets/index-BIj2skbt.js.map +1 -0
- ontosight/static/assets/index-C5nwEVeJ.css +1 -0
- ontosight/static/assets/preset-CO_q375h.js +190 -0
- ontosight/static/assets/preset-CO_q375h.js.map +1 -0
- ontosight/static/assets/worker-DNPIT6vh.js +14 -0
- ontosight/static/assets/worker-DNPIT6vh.js.map +1 -0
- ontosight/static/codegraph-panel.css +376 -0
- ontosight/static/codegraph-panel.js +554 -0
- ontosight/static/critical-nodes-panel.css +362 -0
- ontosight/static/critical-nodes-panel.js +337 -0
- ontosight/static/g6.min.js +89 -0
- ontosight/static/index.html +19 -0
- ontosight/static/logo.svg +29 -0
- ontosight/utils.py +160 -0
- royalsolution_ontosight-0.2.0.dist-info/METADATA +45 -0
- royalsolution_ontosight-0.2.0.dist-info/RECORD +50 -0
- royalsolution_ontosight-0.2.0.dist-info/WHEEL +4 -0
ontosight/__init__.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""OnToSight - Interactive visualization library for knowledge graphs and structured data.
|
|
2
|
+
|
|
3
|
+
A simple, powerful library for creating interactive visualizations of graphs,
|
|
4
|
+
trees, lists, and other structured data with Python.
|
|
5
|
+
|
|
6
|
+
Logging Configuration:
|
|
7
|
+
OnToSight provides automatic logging configuration. By default, it uses a
|
|
8
|
+
user-friendly log format that minimizes output for end users.
|
|
9
|
+
|
|
10
|
+
Environment Variables:
|
|
11
|
+
ONTOSIGHT_LOG_LEVEL: Log level (DEBUG, INFO, WARNING, ERROR)
|
|
12
|
+
ONTOSIGHT_LOG_FORMAT: Log format ("user_friendly" or "detailed")
|
|
13
|
+
ONTOSIGHT_QUIET: Set to "true" to disable all log output
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
import os
|
|
17
|
+
os.environ["ONTOSIGHT_LOG_LEVEL"] = "DEBUG"
|
|
18
|
+
os.environ["ONTOSIGHT_LOG_FORMAT"] = "detailed"
|
|
19
|
+
|
|
20
|
+
from ontosight import view_graph
|
|
21
|
+
# Now view_graph will output detailed debug logs
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
# 导入日志配置(在任何其他导入之前)
|
|
25
|
+
from ontosight import logging_config
|
|
26
|
+
|
|
27
|
+
from ontosight.core import (
|
|
28
|
+
view_graph,
|
|
29
|
+
view_hypergraph,
|
|
30
|
+
view_nodes,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__version__ = "0.2.0"
|
|
34
|
+
__author__ = "Yifan Feng"
|
|
35
|
+
__email__ = "evanfeng97@gmail.com"
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"view_graph",
|
|
39
|
+
"view_hypergraph",
|
|
40
|
+
"view_nodes",
|
|
41
|
+
"logging_config",
|
|
42
|
+
]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""OntoSight Core - High-level API for visualization management.
|
|
2
|
+
|
|
3
|
+
This module provides simple, intuitive functions for creating and managing
|
|
4
|
+
interactive visualizations of knowledge graphs and structured data.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> from ontosight.core import view_graph, view_hypergraph
|
|
8
|
+
>>>
|
|
9
|
+
>>> # Create a graph visualization
|
|
10
|
+
>>> nodes = [{"name": "Node 1"}, {"name": "Node 2"}]
|
|
11
|
+
>>> edges = [{"source": nodes[0], "target": nodes[1]}]
|
|
12
|
+
>>> view_graph(node_list=nodes, edge_list=edges)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from ontosight.core.views import (
|
|
16
|
+
view_nodes,
|
|
17
|
+
view_graph,
|
|
18
|
+
view_hypergraph,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from .storage import BaseStorage, GraphStorage, HypergraphStorage
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"view_nodes",
|
|
25
|
+
"view_graph",
|
|
26
|
+
"view_hypergraph",
|
|
27
|
+
"BaseStorage",
|
|
28
|
+
"GraphStorage",
|
|
29
|
+
"HypergraphStorage",
|
|
30
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Storage engines for graph, hypergraph, and node-only visualizations."""
|
|
2
|
+
|
|
3
|
+
from .base import BaseStorage
|
|
4
|
+
from .graph import GraphStorage
|
|
5
|
+
from .hypergraph import HypergraphStorage
|
|
6
|
+
from .node import NodeStorage
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"BaseStorage",
|
|
10
|
+
"GraphStorage",
|
|
11
|
+
"HypergraphStorage",
|
|
12
|
+
"NodeStorage",
|
|
13
|
+
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Base storage class for all storage engines."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BaseStorage:
|
|
7
|
+
"""Abstract base class for storage engines."""
|
|
8
|
+
|
|
9
|
+
def get_element(self, element_id: str) -> Optional[Dict[str, Any]]:
|
|
10
|
+
"""Get a single element by ID."""
|
|
11
|
+
raise NotImplementedError
|
|
12
|
+
|
|
13
|
+
def get_details(self, element_id: str) -> Optional[Dict[str, Any]]:
|
|
14
|
+
"""Get full details of an element."""
|
|
15
|
+
raise NotImplementedError
|
|
16
|
+
|
|
17
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
18
|
+
"""Get statistics about the storage."""
|
|
19
|
+
raise NotImplementedError
|
|
20
|
+
|
|
21
|
+
def get_sample(
|
|
22
|
+
self,
|
|
23
|
+
center_ids: Optional[List[str]] = None,
|
|
24
|
+
hops: int = 2,
|
|
25
|
+
highlight_center: bool = False,
|
|
26
|
+
min_nodes: int = 10,
|
|
27
|
+
max_attempts: int = 5,
|
|
28
|
+
) -> Dict[str, Any]:
|
|
29
|
+
"""Get sample around given nodes.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
center_ids: List of center node/element IDs
|
|
33
|
+
hops: Number of hops to expand (graph/hypergraph specific)
|
|
34
|
+
highlight_center: If True, mark center elements with highlighted=True
|
|
35
|
+
min_nodes: Minimum number of nodes to include in the sample
|
|
36
|
+
max_attempts: Maximum number of attempts to find a suitable center
|
|
37
|
+
"""
|
|
38
|
+
raise NotImplementedError
|
|
39
|
+
|
|
40
|
+
def get_sample_from_data(self, *args, highlight_center: bool = False, **kwargs) -> Dict[str, Any]:
|
|
41
|
+
"""Get sample based on raw data objects (ItemSchema/NodeSchema/EdgeSchema).
|
|
42
|
+
|
|
43
|
+
For list visualization: get_sample_from_data(item_list, highlight_center=False)
|
|
44
|
+
For graph visualization: get_sample_from_data(node_list, edge_list, highlight_center=False)
|
|
45
|
+
For hypergraph visualization: get_sample_from_data(node_list, hyperedge_list, highlight_center=False)
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
*args: Raw data objects to extract IDs from
|
|
49
|
+
highlight_center: If True, mark matching elements with highlighted=True
|
|
50
|
+
**kwargs: Additional keyword arguments
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Sample data around the provided elements
|
|
54
|
+
"""
|
|
55
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
"""Storage engine for graph visualization."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Optional, Set, Tuple, Callable, TypeVar
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
import random
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
from ontosight.utils import get_model_id, default_label_formatter
|
|
9
|
+
from .base import BaseStorage
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
NodeSchema = TypeVar("NodeSchema", bound=BaseModel)
|
|
14
|
+
EdgeSchema = TypeVar("EdgeSchema", bound=BaseModel)
|
|
15
|
+
|
|
16
|
+
_MIN_NODE_SIZE = 20
|
|
17
|
+
_MAX_NODE_SIZE = 48
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _node_size_from_importance(importance: float) -> int:
|
|
21
|
+
clamped = max(0.0, min(1.0, float(importance)))
|
|
22
|
+
return int(_MIN_NODE_SIZE + round(clamped * (_MAX_NODE_SIZE - _MIN_NODE_SIZE)))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _apply_topology_to_node(
|
|
26
|
+
node_entry: Dict[str, Any],
|
|
27
|
+
*,
|
|
28
|
+
degree: int,
|
|
29
|
+
ranking: Optional[Dict[str, Any]],
|
|
30
|
+
highlight: bool,
|
|
31
|
+
) -> None:
|
|
32
|
+
"""Enrich a node dict with topology metrics for the viewer."""
|
|
33
|
+
data = node_entry.setdefault("data", {})
|
|
34
|
+
data["degree"] = degree
|
|
35
|
+
if ranking:
|
|
36
|
+
data["importance"] = ranking.get("importance", 0)
|
|
37
|
+
data["betweenness"] = ranking.get("betweenness", 0)
|
|
38
|
+
data["tier"] = ranking.get("tier", "low")
|
|
39
|
+
node_entry["size"] = _node_size_from_importance(ranking.get("importance", 0))
|
|
40
|
+
else:
|
|
41
|
+
data["importance"] = min(1.0, degree / 10.0) if degree else 0.0
|
|
42
|
+
node_entry["size"] = _node_size_from_importance(data["importance"])
|
|
43
|
+
if highlight:
|
|
44
|
+
node_entry["highlighted"] = True
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class GraphStorage(BaseStorage):
|
|
48
|
+
"""Storage engine for graph visualization."""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
node_list: List[NodeSchema],
|
|
53
|
+
edge_list: List[EdgeSchema],
|
|
54
|
+
node_id_extractor: Callable[[NodeSchema], str],
|
|
55
|
+
node_ids_in_edge_extractor: Callable[[EdgeSchema], Tuple[str, str]],
|
|
56
|
+
edge_label_extractor: Callable[[EdgeSchema], str],
|
|
57
|
+
node_label_extractor: Optional[Callable[[NodeSchema], str]] = None,
|
|
58
|
+
node_rankings: Optional[List[Dict[str, Any]]] = None,
|
|
59
|
+
critical_node_ids: Optional[List[str]] = None,
|
|
60
|
+
):
|
|
61
|
+
"""Initialize graph storage from raw schema items.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
node_list: List of node schema objects
|
|
65
|
+
edge_list: List of edge schema objects
|
|
66
|
+
node_id_extractor: Function to extract unique ID from node
|
|
67
|
+
node_ids_in_edge_extractor: Function to extract (source_id, target_id) from edge
|
|
68
|
+
edge_label_extractor: Function to extract display label from edge
|
|
69
|
+
node_label_extractor: Optional function to extract display label from node
|
|
70
|
+
"""
|
|
71
|
+
node_label_extractor = (
|
|
72
|
+
node_label_extractor
|
|
73
|
+
if node_label_extractor
|
|
74
|
+
else lambda n: default_label_formatter(node_id_extractor(n))
|
|
75
|
+
)
|
|
76
|
+
edge_id_extractor = get_model_id
|
|
77
|
+
# Store extractors as class variables for later use
|
|
78
|
+
self.node_id_extractor = node_id_extractor
|
|
79
|
+
self.edge_id_extractor = edge_id_extractor
|
|
80
|
+
self.node_label_extractor = node_label_extractor
|
|
81
|
+
self.edge_label_extractor = edge_label_extractor
|
|
82
|
+
self.node_ids_in_edge_extractor = node_ids_in_edge_extractor
|
|
83
|
+
|
|
84
|
+
rankings_map = {r["node_id"]: r for r in (node_rankings or []) if r.get("node_id")}
|
|
85
|
+
critical_set = set(critical_node_ids or [])
|
|
86
|
+
|
|
87
|
+
self.nodes = {} # {id: {"id": id, "data": {"label": label, "raw": raw_data}}}
|
|
88
|
+
self.deg_node = {}
|
|
89
|
+
|
|
90
|
+
for node in node_list:
|
|
91
|
+
node_id = node_id_extractor(node)
|
|
92
|
+
label = node_label_extractor(node)
|
|
93
|
+
raw_data = node.model_dump() if hasattr(node, "model_dump") else dict(node)
|
|
94
|
+
self.nodes[node_id] = {"id": node_id, "data": {"label": label, "raw": raw_data}}
|
|
95
|
+
self.deg_node[node_id] = 0
|
|
96
|
+
|
|
97
|
+
# Build edges with formatted structure
|
|
98
|
+
self.edges = {} # {id: {"id": id, "source": src_id, "target": tgt_id, "data": {...}}}
|
|
99
|
+
self.adjacency: Dict[str, Set[str]] = {node_id: set() for node_id in self.nodes}
|
|
100
|
+
self.incident_edges: Dict[str, List[str]] = {node_id: [] for node_id in self.nodes}
|
|
101
|
+
|
|
102
|
+
for i, edge in enumerate(edge_list):
|
|
103
|
+
edge_label = edge_label_extractor(edge)
|
|
104
|
+
source_id, target_id = node_ids_in_edge_extractor(edge)
|
|
105
|
+
|
|
106
|
+
if source_id not in self.nodes or target_id not in self.nodes:
|
|
107
|
+
logger.warning(f"Edge references missing node IDs: {source_id} -> {target_id}")
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
edge_id = edge_id_extractor(edge)
|
|
111
|
+
|
|
112
|
+
if source_id != target_id:
|
|
113
|
+
self.deg_node[source_id] += 1
|
|
114
|
+
self.deg_node[target_id] += 1
|
|
115
|
+
|
|
116
|
+
raw_data = edge.model_dump() if hasattr(edge, "model_dump") else dict(edge)
|
|
117
|
+
self.edges[edge_id] = {
|
|
118
|
+
"id": edge_id,
|
|
119
|
+
"source": source_id,
|
|
120
|
+
"target": target_id,
|
|
121
|
+
"data": {"label": edge_label, "raw": raw_data},
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# Build adjacency
|
|
125
|
+
self.adjacency[source_id].add(target_id)
|
|
126
|
+
self.adjacency[target_id].add(source_id)
|
|
127
|
+
self.incident_edges[source_id].append(edge_id)
|
|
128
|
+
self.incident_edges[target_id].append(edge_id)
|
|
129
|
+
|
|
130
|
+
for node_id, node_entry in self.nodes.items():
|
|
131
|
+
_apply_topology_to_node(
|
|
132
|
+
node_entry,
|
|
133
|
+
degree=self.deg_node.get(node_id, 0),
|
|
134
|
+
ranking=rankings_map.get(node_id),
|
|
135
|
+
highlight=node_id in critical_set,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
self.stats = self._compute_stats()
|
|
139
|
+
logger.debug(f"GraphStorage initialized: {len(self.nodes)} nodes, {len(self.edges)} edges")
|
|
140
|
+
|
|
141
|
+
def _compute_stats(self) -> Dict[str, Any]:
|
|
142
|
+
"""Compute graph statistics."""
|
|
143
|
+
if not self.nodes:
|
|
144
|
+
return {"total_nodes": 0, "total_edges": 0, "avg_degree": 0}
|
|
145
|
+
|
|
146
|
+
total_degree = sum(self.deg_node.values())
|
|
147
|
+
return {
|
|
148
|
+
"total_nodes": len(self.nodes),
|
|
149
|
+
"total_edges": len(self.edges),
|
|
150
|
+
"avg_degree": total_degree / len(self.nodes) if self.nodes else 0,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
def get_element(self, element_id: str) -> Optional[Dict[str, Any]]:
|
|
154
|
+
"""Get node or edge by ID."""
|
|
155
|
+
if element_id in self.nodes:
|
|
156
|
+
return self.nodes[element_id]
|
|
157
|
+
elif element_id in self.edges:
|
|
158
|
+
return self.edges[element_id]
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
def get_details(self, element_id: str) -> Optional[Dict[str, Any]]:
|
|
162
|
+
"""Get full details of a node or edge."""
|
|
163
|
+
return self.get_element(element_id)
|
|
164
|
+
|
|
165
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
166
|
+
"""Get graph statistics."""
|
|
167
|
+
return self.stats
|
|
168
|
+
|
|
169
|
+
def export_full_graph(self) -> Dict[str, Any]:
|
|
170
|
+
"""Export all nodes and edges for API / client-side filtering."""
|
|
171
|
+
return {
|
|
172
|
+
"nodes": [dict(node) for node in self.nodes.values()],
|
|
173
|
+
"edges": [dict(edge) for edge in self.edges.values()],
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
def get_sample(
|
|
177
|
+
self,
|
|
178
|
+
center_ids: Optional[List[str]] = None,
|
|
179
|
+
hops: int = 2,
|
|
180
|
+
highlight_center: bool = False,
|
|
181
|
+
min_nodes: int = 10,
|
|
182
|
+
max_attempts: int = 5,
|
|
183
|
+
) -> Dict[str, Any]:
|
|
184
|
+
"""Get a subgraph around given center nodes/edges (or random if not provided).
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
center_ids: List of node or edge IDs to use as starting points
|
|
188
|
+
hops: Number of hops to expand
|
|
189
|
+
highlight_center: If True, mark center nodes/edges with highlighted=True
|
|
190
|
+
min_nodes: Minimum number of nodes to include in the sample
|
|
191
|
+
max_attempts: Maximum number of attempts to find a suitable center
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Dict with 'nodes' and 'edges' keys containing the subgraph
|
|
195
|
+
"""
|
|
196
|
+
if not center_ids:
|
|
197
|
+
all_nodes = list(self.nodes.keys())
|
|
198
|
+
if not all_nodes:
|
|
199
|
+
return {"nodes": [], "edges": []}
|
|
200
|
+
|
|
201
|
+
all_node_ids = set()
|
|
202
|
+
all_edge_ids = set()
|
|
203
|
+
result_nodes = []
|
|
204
|
+
result_edges = []
|
|
205
|
+
|
|
206
|
+
for _ in range(max_attempts):
|
|
207
|
+
current_center = [random.choice(all_nodes)]
|
|
208
|
+
|
|
209
|
+
sample = self._get_sample_with_center(current_center, hops, highlight_center)
|
|
210
|
+
|
|
211
|
+
for node in sample["nodes"]:
|
|
212
|
+
if node["id"] not in all_node_ids:
|
|
213
|
+
all_node_ids.add(node["id"])
|
|
214
|
+
result_nodes.append(node)
|
|
215
|
+
|
|
216
|
+
for edge in sample["edges"]:
|
|
217
|
+
if edge["id"] not in all_edge_ids:
|
|
218
|
+
all_edge_ids.add(edge["id"])
|
|
219
|
+
result_edges.append(edge)
|
|
220
|
+
|
|
221
|
+
if len(result_nodes) >= min_nodes:
|
|
222
|
+
return {"nodes": result_nodes, "edges": result_edges}
|
|
223
|
+
|
|
224
|
+
return {"nodes": result_nodes, "edges": result_edges}
|
|
225
|
+
|
|
226
|
+
return self._get_sample_with_center(center_ids, hops, highlight_center)
|
|
227
|
+
|
|
228
|
+
def _get_sample_with_center(
|
|
229
|
+
self,
|
|
230
|
+
center_ids: List[str],
|
|
231
|
+
hops: int,
|
|
232
|
+
highlight_center: bool,
|
|
233
|
+
) -> Dict[str, Any]:
|
|
234
|
+
"""Internal method to get sample with given center IDs."""
|
|
235
|
+
if not center_ids:
|
|
236
|
+
return {"nodes": [], "edges": []}
|
|
237
|
+
|
|
238
|
+
visited_nodes = set()
|
|
239
|
+
visited_edges = set()
|
|
240
|
+
center_node_ids = set()
|
|
241
|
+
center_edge_ids = set()
|
|
242
|
+
|
|
243
|
+
for element_id in center_ids:
|
|
244
|
+
if element_id in self.nodes:
|
|
245
|
+
visited_nodes.add(element_id)
|
|
246
|
+
if highlight_center:
|
|
247
|
+
center_node_ids.add(element_id)
|
|
248
|
+
elif element_id in self.edges:
|
|
249
|
+
visited_edges.add(element_id)
|
|
250
|
+
if highlight_center:
|
|
251
|
+
center_edge_ids.add(element_id)
|
|
252
|
+
edge_data = self.edges[element_id]
|
|
253
|
+
visited_nodes.add(edge_data["source"])
|
|
254
|
+
visited_nodes.add(edge_data["target"])
|
|
255
|
+
|
|
256
|
+
if not visited_nodes:
|
|
257
|
+
return {"nodes": [], "edges": []}
|
|
258
|
+
|
|
259
|
+
current_layer = set(visited_nodes)
|
|
260
|
+
|
|
261
|
+
for _ in range(hops):
|
|
262
|
+
next_layer = set()
|
|
263
|
+
for node_id in current_layer:
|
|
264
|
+
for edge_id in self.incident_edges.get(node_id, []):
|
|
265
|
+
if edge_id not in visited_edges:
|
|
266
|
+
visited_edges.add(edge_id)
|
|
267
|
+
edge_data = self.edges[edge_id]
|
|
268
|
+
other_node = (
|
|
269
|
+
edge_data["target"]
|
|
270
|
+
if edge_data["source"] == node_id
|
|
271
|
+
else edge_data["source"]
|
|
272
|
+
)
|
|
273
|
+
if other_node not in visited_nodes:
|
|
274
|
+
next_layer.add(other_node)
|
|
275
|
+
visited_nodes.add(other_node)
|
|
276
|
+
current_layer = next_layer
|
|
277
|
+
|
|
278
|
+
sub_nodes = []
|
|
279
|
+
for node_id in visited_nodes:
|
|
280
|
+
node_data = dict(self.nodes[node_id])
|
|
281
|
+
if highlight_center and node_id in center_node_ids:
|
|
282
|
+
node_data["highlighted"] = True
|
|
283
|
+
sub_nodes.append(node_data)
|
|
284
|
+
|
|
285
|
+
sub_edges = []
|
|
286
|
+
for edge_id in visited_edges:
|
|
287
|
+
edge_data = dict(self.edges[edge_id])
|
|
288
|
+
if highlight_center and edge_id in center_edge_ids:
|
|
289
|
+
edge_data["highlighted"] = True
|
|
290
|
+
sub_edges.append(edge_data)
|
|
291
|
+
|
|
292
|
+
logger.debug(f"[GraphStorage] get_sample: {len(sub_nodes)} nodes, {len(sub_edges)} edges")
|
|
293
|
+
return {"nodes": sub_nodes, "edges": sub_edges}
|
|
294
|
+
|
|
295
|
+
def get_all_nodes_paginated(self, page: int = 0, page_size: int = 30) -> Dict[str, Any]:
|
|
296
|
+
"""Get paginated list of all nodes."""
|
|
297
|
+
# Use values directly to keep the nested {id, data} structure
|
|
298
|
+
node_items = list(self.nodes.values())
|
|
299
|
+
|
|
300
|
+
total = len(node_items)
|
|
301
|
+
start = page * page_size
|
|
302
|
+
end = start + page_size
|
|
303
|
+
|
|
304
|
+
# For list view, we ensure 'label' and 'type' are at root
|
|
305
|
+
items = []
|
|
306
|
+
for node in node_items[start:end]:
|
|
307
|
+
item = dict(node)
|
|
308
|
+
item["label"] = node.get("data", {}).get("label", node.get("id"))
|
|
309
|
+
item["type"] = "node"
|
|
310
|
+
items.append(item)
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
"items": items,
|
|
314
|
+
"page": page,
|
|
315
|
+
"page_size": page_size,
|
|
316
|
+
"total": total,
|
|
317
|
+
"has_next": end < total,
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
def get_all_edges_paginated(self, page: int = 0, page_size: int = 30) -> Dict[str, Any]:
|
|
321
|
+
"""Get paginated list of all edges."""
|
|
322
|
+
edge_items = list(self.edges.values())
|
|
323
|
+
|
|
324
|
+
total = len(edge_items)
|
|
325
|
+
start = page * page_size
|
|
326
|
+
end = start + page_size
|
|
327
|
+
|
|
328
|
+
items = []
|
|
329
|
+
for edge in edge_items[start:end]:
|
|
330
|
+
item = dict(edge)
|
|
331
|
+
item["label"] = edge.get("data", {}).get("label", edge.get("id"))
|
|
332
|
+
item["type"] = "edge"
|
|
333
|
+
items.append(item)
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
"items": items,
|
|
337
|
+
"page": page,
|
|
338
|
+
"page_size": page_size,
|
|
339
|
+
"total": total,
|
|
340
|
+
"has_next": end < total,
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
def get_sample_from_data(
|
|
344
|
+
self,
|
|
345
|
+
node_list: List[NodeSchema],
|
|
346
|
+
edge_list: List[EdgeSchema],
|
|
347
|
+
hops: int = 2,
|
|
348
|
+
highlight_center: bool = False,
|
|
349
|
+
) -> Dict[str, Any]:
|
|
350
|
+
"""Get sample based on raw node and edge data objects.
|
|
351
|
+
|
|
352
|
+
Extracts IDs from the provided raw data using extractors and label_to_id mapping.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
node_list: List of NodeSchema objects to extract IDs from
|
|
356
|
+
edge_list: List of EdgeSchema objects to extract IDs from
|
|
357
|
+
hops: Number of hops for neighborhood expansion
|
|
358
|
+
highlight_center: If True, mark extracted nodes and edges with highlighted=True
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
Dict with 'nodes' and 'edges' keys containing the subgraph
|
|
362
|
+
"""
|
|
363
|
+
# Extract node IDs
|
|
364
|
+
node_ids = []
|
|
365
|
+
for node in node_list:
|
|
366
|
+
node_id = self.node_id_extractor(node)
|
|
367
|
+
if node_id in self.nodes:
|
|
368
|
+
node_ids.append(node_id)
|
|
369
|
+
else:
|
|
370
|
+
logger.debug(f"Node {self.node_label_extractor(node)} not found in graph")
|
|
371
|
+
|
|
372
|
+
# Extract edge IDs
|
|
373
|
+
edge_ids = []
|
|
374
|
+
for edge in edge_list:
|
|
375
|
+
edge_id = self.edge_id_extractor(edge)
|
|
376
|
+
if edge_id in self.edges:
|
|
377
|
+
edge_ids.append(edge_id)
|
|
378
|
+
else:
|
|
379
|
+
logger.debug(f"Edge {self.edge_label_extractor(edge)} not found in graph")
|
|
380
|
+
|
|
381
|
+
# Combine node and edge IDs and call get_sample with highlight_center
|
|
382
|
+
all_ids = node_ids + edge_ids
|
|
383
|
+
if all_ids:
|
|
384
|
+
return self.get_sample(center_ids=all_ids, hops=hops, highlight_center=highlight_center)
|
|
385
|
+
else:
|
|
386
|
+
return {"nodes": [], "edges": []}
|