sayou-visualizer 0.0.6__tar.gz → 0.0.9__tar.gz
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.
- {sayou_visualizer-0.0.6 → sayou_visualizer-0.0.9}/PKG-INFO +2 -2
- {sayou_visualizer-0.0.6 → sayou_visualizer-0.0.9}/pyproject.toml +2 -2
- sayou_visualizer-0.0.9/src/sayou/visualizer/__init__.py +5 -0
- {sayou_visualizer-0.0.6 → sayou_visualizer-0.0.9}/src/sayou/visualizer/pipeline.py +39 -8
- sayou_visualizer-0.0.9/src/sayou/visualizer/renderer/kg_renderer.py +300 -0
- {sayou_visualizer-0.0.6 → sayou_visualizer-0.0.9}/src/sayou/visualizer/renderer/pyvis_renderer.py +1 -1
- sayou_visualizer-0.0.9/src/sayou/visualizer/tracer/graph_tracer.py +101 -0
- sayou_visualizer-0.0.6/src/sayou/visualizer/tracer/graph_tracer.py +0 -94
- {sayou_visualizer-0.0.6 → sayou_visualizer-0.0.9}/.gitignore +0 -0
- {sayou_visualizer-0.0.6 → sayou_visualizer-0.0.9}/README.md +0 -0
- {sayou_visualizer-0.0.6 → sayou_visualizer-0.0.9}/examples/quick_start.py +0 -0
- {sayou_visualizer-0.0.6 → sayou_visualizer-0.0.9}/examples/quick_start_ws_client.py +0 -0
- {sayou_visualizer-0.0.6 → sayou_visualizer-0.0.9}/examples/quick_start_ws_server.py +0 -0
- {sayou_visualizer-0.0.6 → sayou_visualizer-0.0.9}/src/sayou/visualizer/core/exceptions.py +0 -0
- {sayou_visualizer-0.0.6 → sayou_visualizer-0.0.9}/src/sayou/visualizer/core/schemas.py +0 -0
- {sayou_visualizer-0.0.6 → sayou_visualizer-0.0.9}/src/sayou/visualizer/interfaces/base_renderer.py +0 -0
- {sayou_visualizer-0.0.6 → sayou_visualizer-0.0.9}/src/sayou/visualizer/tracer/rich_tracer.py +0 -0
- {sayou_visualizer-0.0.6 → sayou_visualizer-0.0.9}/src/sayou/visualizer/tracer/websocket_tracer.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sayou-visualizer
|
|
3
|
-
Version: 0.0.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.0.9
|
|
4
|
+
Summary: Visualizer components for the Sayou Data Platform
|
|
5
5
|
Project-URL: Homepage, https://www.sayouzone.com/
|
|
6
6
|
Project-URL: Documentation, https://sayouzone.github.io/sayou-fabric/
|
|
7
7
|
Project-URL: Repository, https://github.com/sayouzone/sayou-fabric
|
|
@@ -7,11 +7,11 @@ build-backend = "hatchling.build"
|
|
|
7
7
|
# -----------------
|
|
8
8
|
[project]
|
|
9
9
|
name = "sayou-visualizer"
|
|
10
|
-
version = "0.0.
|
|
10
|
+
version = "0.0.9"
|
|
11
11
|
authors = [
|
|
12
12
|
{ name = "Sayouzone", email = "contact@sayouzone.com" },
|
|
13
13
|
]
|
|
14
|
-
description = "
|
|
14
|
+
description = "Visualizer components for the Sayou Data Platform"
|
|
15
15
|
readme = "README.md"
|
|
16
16
|
license = { file = "../../LICENSE" }
|
|
17
17
|
requires-python = ">=3.9"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from sayou.core.base_component import BaseComponent
|
|
2
2
|
|
|
3
|
+
from .renderer.kg_renderer import KGRenderer
|
|
3
4
|
from .renderer.pyvis_renderer import PyVisRenderer
|
|
4
5
|
from .tracer.graph_tracer import GraphTracer
|
|
5
6
|
from .tracer.rich_tracer import RichConsoleTracer
|
|
@@ -22,30 +23,53 @@ class VisualizerPipeline(BaseComponent):
|
|
|
22
23
|
self._rich_tracer = None
|
|
23
24
|
self._ws_tracer = None
|
|
24
25
|
self._renderer = None
|
|
26
|
+
self._kg_renderer = None
|
|
25
27
|
|
|
26
28
|
def attach_to(self, target_pipeline: BaseComponent, mode: str = "report", **kwargs):
|
|
27
29
|
"""
|
|
28
|
-
Connects this visualizer to a target pipeline
|
|
30
|
+
Connects this visualizer to a target pipeline AND its children recursively.
|
|
29
31
|
"""
|
|
32
|
+
tracer = None
|
|
30
33
|
if mode == "report":
|
|
31
34
|
self._graph_tracer = GraphTracer()
|
|
32
|
-
|
|
35
|
+
tracer = self._graph_tracer
|
|
33
36
|
self._log("Attached GraphTracer for HTML reporting.")
|
|
34
|
-
|
|
35
37
|
elif mode == "live":
|
|
36
38
|
self._rich_tracer = RichConsoleTracer()
|
|
37
|
-
|
|
39
|
+
tracer = self._rich_tracer
|
|
38
40
|
self._log("Attached RichConsoleTracer for live monitoring.")
|
|
39
|
-
|
|
40
41
|
elif mode == "websocket":
|
|
41
42
|
url = kwargs.get("url")
|
|
42
43
|
if not url:
|
|
43
|
-
raise ValueError("WebSocket mode requires
|
|
44
|
-
|
|
44
|
+
raise ValueError("WebSocket mode requires 'url'.")
|
|
45
45
|
self._ws_tracer = WebSocketTracer(server_url=url)
|
|
46
|
-
|
|
46
|
+
tracer = self._ws_tracer
|
|
47
47
|
self._log(f"Attached WebSocketTracer to {url}")
|
|
48
48
|
|
|
49
|
+
if tracer:
|
|
50
|
+
self._recursive_attach(target_pipeline, tracer)
|
|
51
|
+
else:
|
|
52
|
+
self._log("No tracer attached. Visualizer will not record events.")
|
|
53
|
+
|
|
54
|
+
def _recursive_attach(self, component: BaseComponent, tracer):
|
|
55
|
+
"""
|
|
56
|
+
Helper to attach tracer to the component and all its sub-components.
|
|
57
|
+
"""
|
|
58
|
+
if hasattr(component, "_callbacks") and tracer in component._callbacks:
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
component.add_callback(tracer)
|
|
62
|
+
self._log(f"Attached tracer to {component.component_name}")
|
|
63
|
+
|
|
64
|
+
for attr_name, attr_value in component.__dict__.items():
|
|
65
|
+
if isinstance(attr_value, BaseComponent):
|
|
66
|
+
self._recursive_attach(attr_value, tracer)
|
|
67
|
+
|
|
68
|
+
elif isinstance(attr_value, list):
|
|
69
|
+
for item in attr_value:
|
|
70
|
+
if isinstance(item, BaseComponent):
|
|
71
|
+
self._recursive_attach(item, tracer)
|
|
72
|
+
|
|
49
73
|
def report(self, output_path: str = "report.html", **kwargs):
|
|
50
74
|
if self._graph_tracer.graph.number_of_nodes() == 0:
|
|
51
75
|
self._log("No events recorded. The graph is empty.", level="warning")
|
|
@@ -57,6 +81,13 @@ class VisualizerPipeline(BaseComponent):
|
|
|
57
81
|
self._renderer = PyVisRenderer()
|
|
58
82
|
self._renderer.render(self._graph_tracer.graph, output_path, **kwargs)
|
|
59
83
|
|
|
84
|
+
def render_kg(self, json_path: str, output_path: str = "kg_view.html"):
|
|
85
|
+
"""
|
|
86
|
+
Visualizes the OUTPUT JSON (Knowledge Graph).
|
|
87
|
+
"""
|
|
88
|
+
self._kg_renderer = KGRenderer()
|
|
89
|
+
self._kg_renderer.render(json_path, output_path)
|
|
90
|
+
|
|
60
91
|
def save_live_log(self, output_path="live_status.html"):
|
|
61
92
|
if self._rich_tracer:
|
|
62
93
|
self._rich_tracer.save_html(output_path)
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
|
|
5
|
+
from sayou.core.base_component import BaseComponent
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class KGRenderer(BaseComponent):
|
|
9
|
+
"""
|
|
10
|
+
Renders 3D KG with 'Virtual Hierarchy'.
|
|
11
|
+
Injects missing Parent Nodes based on metadata and applies Semantic Styling.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
component_name = "KGRenderer"
|
|
15
|
+
|
|
16
|
+
def render(self, json_path: str, output_path: str = "kg_view_3d.html"):
|
|
17
|
+
if not os.path.exists(json_path):
|
|
18
|
+
self._log(f"Output file not found: {json_path}", level="error")
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
with open(json_path, "r", encoding="utf-8") as f:
|
|
22
|
+
raw_data = json.load(f)
|
|
23
|
+
|
|
24
|
+
# ---------------------------------------------------------
|
|
25
|
+
# [Python Logic]
|
|
26
|
+
# ---------------------------------------------------------
|
|
27
|
+
nodes_by_source = defaultdict(list)
|
|
28
|
+
final_nodes = []
|
|
29
|
+
final_links = []
|
|
30
|
+
|
|
31
|
+
existing_ids = set()
|
|
32
|
+
|
|
33
|
+
for node in raw_data.get("nodes", []):
|
|
34
|
+
node_id = node.get("node_id")
|
|
35
|
+
existing_ids.add(node_id)
|
|
36
|
+
attrs = node.get("attributes", {})
|
|
37
|
+
meta = node.get("metadata", {})
|
|
38
|
+
|
|
39
|
+
source = meta.get("source") or attrs.get("sayou:source") or "Unknown Source"
|
|
40
|
+
nodes_by_source[source].append(node_id)
|
|
41
|
+
|
|
42
|
+
sem_type = attrs.get("sayou:semanticType", "text")
|
|
43
|
+
|
|
44
|
+
clean_attrs = {}
|
|
45
|
+
for k, v in attrs.items():
|
|
46
|
+
if isinstance(v, str) and len(v) > 300:
|
|
47
|
+
clean_attrs[k] = v[:200] + "..."
|
|
48
|
+
else:
|
|
49
|
+
clean_attrs[k] = v
|
|
50
|
+
|
|
51
|
+
display_label = attrs.get("schema:text", node_id)
|
|
52
|
+
if len(display_label) > 30:
|
|
53
|
+
display_label = display_label[:30] + "..."
|
|
54
|
+
|
|
55
|
+
group = "Chunk"
|
|
56
|
+
color = "#1e90ff"
|
|
57
|
+
val = 5
|
|
58
|
+
|
|
59
|
+
if sem_type in ["h1", "h2", "h3", "title"]:
|
|
60
|
+
group = "Header"
|
|
61
|
+
color = "#ffa502"
|
|
62
|
+
val = 12
|
|
63
|
+
elif "list" in sem_type:
|
|
64
|
+
group = "List"
|
|
65
|
+
color = "#2ed573"
|
|
66
|
+
val = 4
|
|
67
|
+
elif "table" in sem_type:
|
|
68
|
+
group = "Table"
|
|
69
|
+
color = "#a55eea"
|
|
70
|
+
val = 10
|
|
71
|
+
elif "code" in sem_type:
|
|
72
|
+
group = "Code"
|
|
73
|
+
color = "#ff4757"
|
|
74
|
+
val = 8
|
|
75
|
+
|
|
76
|
+
final_nodes.append(
|
|
77
|
+
{
|
|
78
|
+
"id": node_id,
|
|
79
|
+
"label": display_label,
|
|
80
|
+
"group": group,
|
|
81
|
+
"sem_type": sem_type,
|
|
82
|
+
"color": color,
|
|
83
|
+
"val": val,
|
|
84
|
+
"attributes": clean_attrs,
|
|
85
|
+
"source": source,
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
for edge in raw_data.get("links", []) + raw_data.get("edges", []):
|
|
90
|
+
final_links.append(
|
|
91
|
+
{
|
|
92
|
+
"source": edge.get("source"),
|
|
93
|
+
"target": edge.get("target"),
|
|
94
|
+
"relation": edge.get("relation", "relates_to"),
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
for source_name, child_ids in nodes_by_source.items():
|
|
99
|
+
if source_name in existing_ids:
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
virtual_doc_id = f"VIRTUAL_DOC:{source_name}"
|
|
103
|
+
|
|
104
|
+
final_nodes.append(
|
|
105
|
+
{
|
|
106
|
+
"id": virtual_doc_id,
|
|
107
|
+
"label": source_name,
|
|
108
|
+
"group": "Document",
|
|
109
|
+
"color": "#ff4757",
|
|
110
|
+
"val": 30,
|
|
111
|
+
"attributes": {
|
|
112
|
+
"type": "Virtual Parent",
|
|
113
|
+
"child_count": len(child_ids),
|
|
114
|
+
},
|
|
115
|
+
"is_virtual": True,
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
for child_id in child_ids:
|
|
120
|
+
final_links.append(
|
|
121
|
+
{
|
|
122
|
+
"source": virtual_doc_id,
|
|
123
|
+
"target": child_id,
|
|
124
|
+
"relation": "CONTAINS",
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
graph_data = {"nodes": final_nodes, "links": final_links}
|
|
129
|
+
self._log(f"Rendering KG with Virtual Hierarchy ({len(final_nodes)} nodes)...")
|
|
130
|
+
|
|
131
|
+
# ---------------------------------------------------------
|
|
132
|
+
# [JS Logic] Renderer
|
|
133
|
+
# ---------------------------------------------------------
|
|
134
|
+
html_content = f"""
|
|
135
|
+
<!DOCTYPE html>
|
|
136
|
+
<html lang="en">
|
|
137
|
+
<head>
|
|
138
|
+
<meta charset="UTF-8">
|
|
139
|
+
<title>Sayou Dataverse</title>
|
|
140
|
+
<style>
|
|
141
|
+
body {{ margin: 0; background-color: #000205; overflow: hidden; }}
|
|
142
|
+
#graph {{ width: 100%; height: 100vh; }}
|
|
143
|
+
|
|
144
|
+
#info {{
|
|
145
|
+
position: absolute; top: 20px; right: 20px; width: 300px;
|
|
146
|
+
background: rgba(5, 10, 20, 0.85);
|
|
147
|
+
border: 1px solid rgba(0, 242, 255, 0.3);
|
|
148
|
+
border-left: 3px solid #00f2ff;
|
|
149
|
+
box-shadow: 0 0 20px rgba(0, 242, 255, 0.1);
|
|
150
|
+
color: #dff9fb; padding: 20px;
|
|
151
|
+
font-family: 'Consolas', 'Monaco', monospace;
|
|
152
|
+
backdrop-filter: blur(5px);
|
|
153
|
+
display: none; pointer-events: none;
|
|
154
|
+
border-radius: 0 10px 10px 0;
|
|
155
|
+
}}
|
|
156
|
+
.tag {{
|
|
157
|
+
display: inline-block; padding: 2px 8px; border-radius: 2px;
|
|
158
|
+
font-size: 9px; font-weight: 800; color: #000; margin-bottom: 12px;
|
|
159
|
+
text-transform: uppercase; letter-spacing: 1px;
|
|
160
|
+
}}
|
|
161
|
+
h3 {{
|
|
162
|
+
margin: 0 0 12px 0; color: #fff;
|
|
163
|
+
border-bottom: 1px dashed #576574; padding-bottom: 8px;
|
|
164
|
+
font-size: 14px; line-height: 1.4;
|
|
165
|
+
}}
|
|
166
|
+
.row {{ font-size: 11px; margin-bottom: 4px; color: #a4b0be; }}
|
|
167
|
+
.key {{ color: #00d2d3; margin-right: 5px; }}
|
|
168
|
+
</style>
|
|
169
|
+
<script src="https://unpkg.com/three@0.160.0/build/three.min.js"></script>
|
|
170
|
+
<script src="https://unpkg.com/3d-force-graph@1.73.1/dist/3d-force-graph.min.js"></script>
|
|
171
|
+
</head>
|
|
172
|
+
<body>
|
|
173
|
+
<div id="graph"></div>
|
|
174
|
+
<div id="info"></div>
|
|
175
|
+
|
|
176
|
+
<script>
|
|
177
|
+
const gData = {json.dumps(graph_data)};
|
|
178
|
+
const infoDiv = document.getElementById('info');
|
|
179
|
+
|
|
180
|
+
const Graph = ForceGraph3D()(document.getElementById('graph'))
|
|
181
|
+
.graphData(gData)
|
|
182
|
+
.backgroundColor('#000205')
|
|
183
|
+
.showNavInfo(false)
|
|
184
|
+
|
|
185
|
+
// [Design Logic]
|
|
186
|
+
.nodeThreeObject(node => {{
|
|
187
|
+
let geometry, material;
|
|
188
|
+
|
|
189
|
+
// 1. Virtual Document
|
|
190
|
+
if (node.group === "Document") {{
|
|
191
|
+
const size = node.val;
|
|
192
|
+
geometry = new THREE.BoxGeometry(size, size, size);
|
|
193
|
+
|
|
194
|
+
const edges = new THREE.EdgesGeometry(geometry);
|
|
195
|
+
material = new THREE.LineBasicMaterial({{
|
|
196
|
+
color: node.color,
|
|
197
|
+
transparent: true,
|
|
198
|
+
opacity: 0.4
|
|
199
|
+
}});
|
|
200
|
+
const wireframe = new THREE.LineSegments(edges, material);
|
|
201
|
+
|
|
202
|
+
const coreGeo = new THREE.BoxGeometry(size*0.2, size*0.2, size*0.2);
|
|
203
|
+
const coreMat = new THREE.MeshBasicMaterial({{ color: node.color, wireframe: true }});
|
|
204
|
+
wireframe.add(new THREE.Mesh(coreGeo, coreMat));
|
|
205
|
+
|
|
206
|
+
return wireframe;
|
|
207
|
+
}}
|
|
208
|
+
|
|
209
|
+
// 2. Headers (H1~H3): [발광 다이아몬드]
|
|
210
|
+
else if (node.group === "Header") {{
|
|
211
|
+
geometry = new THREE.OctahedronGeometry(4);
|
|
212
|
+
material = new THREE.MeshPhongMaterial({{
|
|
213
|
+
color: node.color,
|
|
214
|
+
emissive: node.color,
|
|
215
|
+
emissiveIntensity: 0.5,
|
|
216
|
+
shininess: 100,
|
|
217
|
+
flatShading: true
|
|
218
|
+
}});
|
|
219
|
+
}}
|
|
220
|
+
|
|
221
|
+
// 3. Chunk / Text: [데이터 오브]
|
|
222
|
+
else {{
|
|
223
|
+
// 단순 구 대신 Icosahedron(정이십면체)을 써서 디지털 느낌
|
|
224
|
+
geometry = new THREE.IcosahedronGeometry(3, 1);
|
|
225
|
+
material = new THREE.MeshLambertMaterial({{
|
|
226
|
+
color: node.color,
|
|
227
|
+
transparent: true,
|
|
228
|
+
opacity: 0.8
|
|
229
|
+
}});
|
|
230
|
+
}}
|
|
231
|
+
|
|
232
|
+
return new THREE.Mesh(geometry, material);
|
|
233
|
+
}})
|
|
234
|
+
|
|
235
|
+
// [Link Design]
|
|
236
|
+
.linkWidth(link => link.relation === "CONTAINS" ? 0 : 0.5)
|
|
237
|
+
.linkColor(() => '#2f3542')
|
|
238
|
+
.linkDirectionalParticles(link => link.relation === "CONTAINS" ? 1 : 3)
|
|
239
|
+
.linkDirectionalParticleWidth(1.2)
|
|
240
|
+
.linkDirectionalParticleSpeed(0.006)
|
|
241
|
+
.linkDirectionalParticleColor(link => link.relation === "CONTAINS" ? '#57606f' : '#00f2ff')
|
|
242
|
+
|
|
243
|
+
// [Interaction]
|
|
244
|
+
.onNodeHover(node => {{
|
|
245
|
+
document.body.style.cursor = node ? 'crosshair' : null;
|
|
246
|
+
if (node) {{
|
|
247
|
+
infoDiv.style.display = 'block';
|
|
248
|
+
|
|
249
|
+
let tagColor = node.color;
|
|
250
|
+
if(node.group === 'Document') tagColor = '#ff4757';
|
|
251
|
+
|
|
252
|
+
let html = `<span class='tag' style='background:${{tagColor}}'>${{node.group}}</span>`;
|
|
253
|
+
if(node.sem_type) html += `<span class='tag' style='background:#2f3542; color:#fff; margin-left:5px'>${{node.sem_type}}</span>`;
|
|
254
|
+
|
|
255
|
+
html += `<h3>${{node.label}}</h3>`;
|
|
256
|
+
|
|
257
|
+
if (node.attributes) {{
|
|
258
|
+
for (const [k, v] of Object.entries(node.attributes)) {{
|
|
259
|
+
if(k === 'type' || k === 'child_count') continue;
|
|
260
|
+
let key = k.includes(':') ? k.split(':').pop() : k;
|
|
261
|
+
html += `<div class='row'><span class='key'>${{key}}:</span> ${{v}}</div>`;
|
|
262
|
+
}}
|
|
263
|
+
}}
|
|
264
|
+
infoDiv.innerHTML = html;
|
|
265
|
+
}} else {{
|
|
266
|
+
infoDiv.style.display = 'none';
|
|
267
|
+
}}
|
|
268
|
+
}})
|
|
269
|
+
.onNodeClick(node => {{
|
|
270
|
+
const distance = 70;
|
|
271
|
+
const distRatio = 1 + distance/Math.hypot(node.x, node.y, node.z);
|
|
272
|
+
const newPos = node.x || node.y || node.z
|
|
273
|
+
? {{ x: node.x * distRatio, y: node.y * distRatio, z: node.z * distRatio }}
|
|
274
|
+
: {{ x: 0, y: 0, z: distance }};
|
|
275
|
+
Graph.cameraPosition(newPos, node, 1500);
|
|
276
|
+
}});
|
|
277
|
+
|
|
278
|
+
// [Lighting]
|
|
279
|
+
const ambientLight = new THREE.AmbientLight(0x222222); // 어두운 기본광
|
|
280
|
+
Graph.scene().add(ambientLight);
|
|
281
|
+
|
|
282
|
+
const blueLight = new THREE.PointLight(0x00f2ff, 1, 100); // 청록색 포인트 조명
|
|
283
|
+
blueLight.position.set(50, 50, 50);
|
|
284
|
+
Graph.scene().add(blueLight);
|
|
285
|
+
|
|
286
|
+
const pinkLight = new THREE.PointLight(0xff00ff, 0.5, 100); // 핑크색 포인트 조명 (반대편)
|
|
287
|
+
pinkLight.position.set(-50, -50, -50);
|
|
288
|
+
Graph.scene().add(pinkLight);
|
|
289
|
+
|
|
290
|
+
// [Physics]
|
|
291
|
+
Graph.d3Force('charge').strength(-50);
|
|
292
|
+
Graph.d3Force('link').distance(link => link.relation === "CONTAINS" ? 60 : 30);
|
|
293
|
+
|
|
294
|
+
</script>
|
|
295
|
+
</body>
|
|
296
|
+
</html>
|
|
297
|
+
"""
|
|
298
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
299
|
+
f.write(html_content)
|
|
300
|
+
self._log(f"✅ Semantic 3D KG Showcase saved to: {output_path}")
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import networkx as nx
|
|
2
|
+
from sayou.core.callbacks import BaseCallback
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class GraphTracer(BaseCallback):
|
|
6
|
+
def __init__(self):
|
|
7
|
+
self.graph = nx.DiGraph()
|
|
8
|
+
self._execution_stack = ["Root"]
|
|
9
|
+
self.graph.add_node(
|
|
10
|
+
"Root",
|
|
11
|
+
label="Sayou Pipeline",
|
|
12
|
+
color="#ffffff",
|
|
13
|
+
shape="dot",
|
|
14
|
+
size=25,
|
|
15
|
+
font={"size": 20, "color": "black"},
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
def on_start(self, component_name, input_data, **kwargs):
|
|
19
|
+
try:
|
|
20
|
+
comp_node = f"Comp:{component_name}_{len(self._execution_stack)}"
|
|
21
|
+
|
|
22
|
+
color = "#a5b1c2"
|
|
23
|
+
size = 15
|
|
24
|
+
if "Pipeline" in component_name:
|
|
25
|
+
color = "#ff6b81"
|
|
26
|
+
size = 20
|
|
27
|
+
elif "Generator" in component_name or "Fetcher" in component_name:
|
|
28
|
+
color = "#2ed573"
|
|
29
|
+
elif "Parser" in component_name or "Converter" in component_name:
|
|
30
|
+
color = "#eccc68"
|
|
31
|
+
elif "Splitter" in component_name:
|
|
32
|
+
color = "#70a1ff"
|
|
33
|
+
|
|
34
|
+
if not self.graph.has_node(comp_node):
|
|
35
|
+
self.graph.add_node(
|
|
36
|
+
comp_node, label=component_name, shape="dot", color=color, size=size
|
|
37
|
+
)
|
|
38
|
+
parent_node = self._execution_stack[-1]
|
|
39
|
+
|
|
40
|
+
if "Generator" in parent_node and "Pipeline" in component_name:
|
|
41
|
+
for ancestor in reversed(self._execution_stack):
|
|
42
|
+
if "Pipeline" in ancestor and "Connector" not in ancestor:
|
|
43
|
+
parent_node = ancestor
|
|
44
|
+
break
|
|
45
|
+
self.graph.add_edge(parent_node, comp_node)
|
|
46
|
+
self._execution_stack.append(comp_node)
|
|
47
|
+
|
|
48
|
+
data_id = self._get_data_id(input_data)
|
|
49
|
+
if data_id:
|
|
50
|
+
data_node_id = f"Data:{data_id}"
|
|
51
|
+
if not self.graph.has_node(data_node_id):
|
|
52
|
+
label_text = str(data_id)
|
|
53
|
+
if len(label_text) > 20:
|
|
54
|
+
label_text = label_text[:17] + "..."
|
|
55
|
+
self.graph.add_node(
|
|
56
|
+
data_node_id,
|
|
57
|
+
label=label_text,
|
|
58
|
+
shape="square",
|
|
59
|
+
size=8,
|
|
60
|
+
color="#57606f",
|
|
61
|
+
)
|
|
62
|
+
self.graph.add_edge(comp_node, data_node_id)
|
|
63
|
+
|
|
64
|
+
except Exception as e:
|
|
65
|
+
print(f"!!! TRACER ERROR: {e}")
|
|
66
|
+
|
|
67
|
+
def on_finish(self, component_name, result_data, success, **kwargs):
|
|
68
|
+
try:
|
|
69
|
+
if len(self._execution_stack) > 1:
|
|
70
|
+
if component_name in self._execution_stack[-1]:
|
|
71
|
+
self._execution_stack.pop()
|
|
72
|
+
else:
|
|
73
|
+
for i in range(len(self._execution_stack) - 1, 0, -1):
|
|
74
|
+
if component_name in self._execution_stack[i]:
|
|
75
|
+
self._execution_stack = self._execution_stack[:i]
|
|
76
|
+
break
|
|
77
|
+
except Exception as e:
|
|
78
|
+
print(f"!!! TRACER POP ERROR: {e}")
|
|
79
|
+
|
|
80
|
+
def _get_data_id(self, data):
|
|
81
|
+
if isinstance(data, dict):
|
|
82
|
+
return data.get("source") or data.get("uri") or data.get("filename")
|
|
83
|
+
|
|
84
|
+
uri = getattr(data, "uri", None)
|
|
85
|
+
if uri:
|
|
86
|
+
return uri
|
|
87
|
+
|
|
88
|
+
source = getattr(data, "source", None)
|
|
89
|
+
if source:
|
|
90
|
+
return source
|
|
91
|
+
|
|
92
|
+
filename = getattr(data, "filename", None)
|
|
93
|
+
if filename:
|
|
94
|
+
return filename
|
|
95
|
+
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
def _get_data_id_from_result(self, data):
|
|
99
|
+
if hasattr(data, "task") and hasattr(data.task, "uri"):
|
|
100
|
+
return data.task.uri
|
|
101
|
+
return None
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import networkx as nx
|
|
2
|
-
from sayou.core.callbacks import BaseCallback
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class GraphTracer(BaseCallback):
|
|
6
|
-
def __init__(self):
|
|
7
|
-
self.graph = nx.DiGraph()
|
|
8
|
-
self.graph.add_node(
|
|
9
|
-
"Root",
|
|
10
|
-
label="Sayou Pipeline",
|
|
11
|
-
color="#ffffff",
|
|
12
|
-
shape="dot",
|
|
13
|
-
size=25,
|
|
14
|
-
font={"size": 20, "color": "black"},
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
def on_start(self, component_name, input_data, **kwargs):
|
|
18
|
-
try:
|
|
19
|
-
comp_node = f"Comp:{component_name}"
|
|
20
|
-
|
|
21
|
-
color = "#a5b1c2"
|
|
22
|
-
size = 15
|
|
23
|
-
|
|
24
|
-
if "ConnectorPipeline" in component_name:
|
|
25
|
-
color = "#ffffff"
|
|
26
|
-
size = 20
|
|
27
|
-
elif "Generator" in component_name:
|
|
28
|
-
color = "#00d2d3"
|
|
29
|
-
size = 15
|
|
30
|
-
elif "Fetcher" in component_name:
|
|
31
|
-
color = "#ff9f43"
|
|
32
|
-
size = 15
|
|
33
|
-
|
|
34
|
-
if not self.graph.has_node(comp_node):
|
|
35
|
-
self.graph.add_node(
|
|
36
|
-
comp_node, label=component_name, shape="dot", color=color, size=size
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
if "Pipeline" in component_name:
|
|
40
|
-
self.graph.add_edge("Root", comp_node)
|
|
41
|
-
else:
|
|
42
|
-
parent = "Comp:ConnectorPipeline"
|
|
43
|
-
if not self.graph.has_node(parent):
|
|
44
|
-
parent = "Root"
|
|
45
|
-
self.graph.add_edge(parent, comp_node)
|
|
46
|
-
|
|
47
|
-
data_id = self._get_data_id(input_data)
|
|
48
|
-
if data_id:
|
|
49
|
-
label_text = str(data_id)
|
|
50
|
-
if len(label_text) > 25:
|
|
51
|
-
label_text = label_text[:22] + "..."
|
|
52
|
-
|
|
53
|
-
node_id = f"Data:{data_id}"
|
|
54
|
-
if not self.graph.has_node(node_id):
|
|
55
|
-
self.graph.add_node(
|
|
56
|
-
node_id,
|
|
57
|
-
label=label_text,
|
|
58
|
-
title=str(data_id),
|
|
59
|
-
shape="dot",
|
|
60
|
-
size=8,
|
|
61
|
-
color={"background": "#57606f", "border": "#57606f"},
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
self.graph.add_edge(comp_node, node_id)
|
|
65
|
-
|
|
66
|
-
except Exception as e:
|
|
67
|
-
print(f"!!! TRACER ERROR: {e}")
|
|
68
|
-
|
|
69
|
-
def on_finish(self, component_name, result_data, success, **kwargs):
|
|
70
|
-
try:
|
|
71
|
-
data_id = self._get_data_id_from_result(result_data)
|
|
72
|
-
if data_id:
|
|
73
|
-
node_id = f"Data:{data_id}"
|
|
74
|
-
if self.graph.has_node(node_id):
|
|
75
|
-
color = "#54a0ff" if success else "#ff6b6b"
|
|
76
|
-
self.graph.nodes[node_id]["color"] = color
|
|
77
|
-
self.graph.nodes[node_id]["size"] = 10
|
|
78
|
-
except Exception as e:
|
|
79
|
-
print(f"!!! TRACER ERROR in on_finish: {e}")
|
|
80
|
-
pass
|
|
81
|
-
|
|
82
|
-
def _get_data_id(self, data):
|
|
83
|
-
if isinstance(data, dict):
|
|
84
|
-
return data.get("source") or data.get("uri")
|
|
85
|
-
if hasattr(data, "uri"):
|
|
86
|
-
return data.uri
|
|
87
|
-
if hasattr(data, "source"):
|
|
88
|
-
return data.source
|
|
89
|
-
return None
|
|
90
|
-
|
|
91
|
-
def _get_data_id_from_result(self, data):
|
|
92
|
-
if hasattr(data, "task") and hasattr(data.task, "uri"):
|
|
93
|
-
return data.task.uri
|
|
94
|
-
return None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sayou_visualizer-0.0.6 → sayou_visualizer-0.0.9}/src/sayou/visualizer/interfaces/base_renderer.py
RENAMED
|
File without changes
|
{sayou_visualizer-0.0.6 → sayou_visualizer-0.0.9}/src/sayou/visualizer/tracer/rich_tracer.py
RENAMED
|
File without changes
|
{sayou_visualizer-0.0.6 → sayou_visualizer-0.0.9}/src/sayou/visualizer/tracer/websocket_tracer.py
RENAMED
|
File without changes
|