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.
Files changed (50) hide show
  1. ontosight/__init__.py +42 -0
  2. ontosight/core/__init__.py +30 -0
  3. ontosight/core/storage/__init__.py +13 -0
  4. ontosight/core/storage/base.py +55 -0
  5. ontosight/core/storage/graph.py +386 -0
  6. ontosight/core/storage/hypergraph.py +401 -0
  7. ontosight/core/storage/node.py +179 -0
  8. ontosight/core/views/__init__.py +17 -0
  9. ontosight/core/views/graph.py +148 -0
  10. ontosight/core/views/hypergraph.py +111 -0
  11. ontosight/core/views/node.py +119 -0
  12. ontosight/logging_config.py +237 -0
  13. ontosight/server/__init__.py +5 -0
  14. ontosight/server/app.py +61 -0
  15. ontosight/server/models/__init__.py +1 -0
  16. ontosight/server/models/api.py +151 -0
  17. ontosight/server/routes/__init__.py +1 -0
  18. ontosight/server/routes/chat.py +117 -0
  19. ontosight/server/routes/codegraph.py +62 -0
  20. ontosight/server/routes/data.py +203 -0
  21. ontosight/server/routes/meta.py +88 -0
  22. ontosight/server/routes/rankings.py +26 -0
  23. ontosight/server/routes/search.py +105 -0
  24. ontosight/server/state.py +296 -0
  25. ontosight/static/assets/GraphView-BAsvQRbE.js +2 -0
  26. ontosight/static/assets/GraphView-BAsvQRbE.js.map +1 -0
  27. ontosight/static/assets/HypergraphView-BeKyuFX9.js +2 -0
  28. ontosight/static/assets/HypergraphView-BeKyuFX9.js.map +1 -0
  29. ontosight/static/assets/NodeView-CT1Q_Qn-.js +2 -0
  30. ontosight/static/assets/NodeView-CT1Q_Qn-.js.map +1 -0
  31. ontosight/static/assets/PaginatedGridView-LOV9NPu5.js +12 -0
  32. ontosight/static/assets/PaginatedGridView-LOV9NPu5.js.map +1 -0
  33. ontosight/static/assets/index-BIj2skbt.js +181 -0
  34. ontosight/static/assets/index-BIj2skbt.js.map +1 -0
  35. ontosight/static/assets/index-C5nwEVeJ.css +1 -0
  36. ontosight/static/assets/preset-CO_q375h.js +190 -0
  37. ontosight/static/assets/preset-CO_q375h.js.map +1 -0
  38. ontosight/static/assets/worker-DNPIT6vh.js +14 -0
  39. ontosight/static/assets/worker-DNPIT6vh.js.map +1 -0
  40. ontosight/static/codegraph-panel.css +376 -0
  41. ontosight/static/codegraph-panel.js +554 -0
  42. ontosight/static/critical-nodes-panel.css +362 -0
  43. ontosight/static/critical-nodes-panel.js +337 -0
  44. ontosight/static/g6.min.js +89 -0
  45. ontosight/static/index.html +19 -0
  46. ontosight/static/logo.svg +29 -0
  47. ontosight/utils.py +160 -0
  48. royalsolution_ontosight-0.2.0.dist-info/METADATA +45 -0
  49. royalsolution_ontosight-0.2.0.dist-info/RECORD +50 -0
  50. 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": []}