PyDecisionGraph 0.3.2.post1__cp315-cp315-win_amd64.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.
- decision_graph/__infra__.pxd +1 -0
- decision_graph/__init__.pxd +1 -0
- decision_graph/__init__.py +39 -0
- decision_graph/decision_tree/__init__.pxd +2 -0
- decision_graph/decision_tree/__init__.py +85 -0
- decision_graph/decision_tree/bake/__infra__.pxd +502 -0
- decision_graph/decision_tree/bake/__init__.pxd +502 -0
- decision_graph/decision_tree/bake/__init__.py +29 -0
- decision_graph/decision_tree/bake/c_action.cp315-win_amd64.pyd +0 -0
- decision_graph/decision_tree/bake/c_action.h +324 -0
- decision_graph/decision_tree/bake/c_action.pxd +54 -0
- decision_graph/decision_tree/bake/c_action.pyi +126 -0
- decision_graph/decision_tree/bake/c_action.pyx +65 -0
- decision_graph/decision_tree/bake/c_allocator_protocol.cp315-win_amd64.pyd +0 -0
- decision_graph/decision_tree/bake/c_allocator_protocol.pxd +24 -0
- decision_graph/decision_tree/bake/c_allocator_protocol.pyi +61 -0
- decision_graph/decision_tree/bake/c_allocator_protocol.pyx +109 -0
- decision_graph/decision_tree/bake/c_bake.cp315-win_amd64.pyd +0 -0
- decision_graph/decision_tree/bake/c_bake.h +629 -0
- decision_graph/decision_tree/bake/c_bake.pxd +52 -0
- decision_graph/decision_tree/bake/c_bake.pyi +151 -0
- decision_graph/decision_tree/bake/c_bake.pyx +142 -0
- decision_graph/decision_tree/bake/c_collections.cp315-win_amd64.pyd +0 -0
- decision_graph/decision_tree/bake/c_collections.h +692 -0
- decision_graph/decision_tree/bake/c_collections.pxd +52 -0
- decision_graph/decision_tree/bake/c_collections.pyi +193 -0
- decision_graph/decision_tree/bake/c_collections.pyx +161 -0
- decision_graph/decision_tree/bake/c_const.cp315-win_amd64.pyd +0 -0
- decision_graph/decision_tree/bake/c_const.h +395 -0
- decision_graph/decision_tree/bake/c_const.pxd +56 -0
- decision_graph/decision_tree/bake/c_const.pyi +305 -0
- decision_graph/decision_tree/bake/c_const.pyx +278 -0
- decision_graph/decision_tree/bake/c_edge.cp315-win_amd64.pyd +0 -0
- decision_graph/decision_tree/bake/c_edge.h +511 -0
- decision_graph/decision_tree/bake/c_edge.pxd +117 -0
- decision_graph/decision_tree/bake/c_edge.pyi +192 -0
- decision_graph/decision_tree/bake/c_edge.pyx +270 -0
- decision_graph/decision_tree/bake/c_eval.h +848 -0
- decision_graph/decision_tree/bake/c_eval.pxd +40 -0
- decision_graph/decision_tree/bake/c_expr.cp315-win_amd64.pyd +0 -0
- decision_graph/decision_tree/bake/c_expr.h +1900 -0
- decision_graph/decision_tree/bake/c_expr.pxd +129 -0
- decision_graph/decision_tree/bake/c_expr.pyi +280 -0
- decision_graph/decision_tree/bake/c_expr.pyx +272 -0
- decision_graph/decision_tree/bake/c_hierarchy.cp315-win_amd64.pyd +0 -0
- decision_graph/decision_tree/bake/c_hierarchy.h +731 -0
- decision_graph/decision_tree/bake/c_hierarchy.pxd +71 -0
- decision_graph/decision_tree/bake/c_hierarchy.pyi +409 -0
- decision_graph/decision_tree/bake/c_hierarchy.pyx +277 -0
- decision_graph/decision_tree/bake/c_logic_group.cp315-win_amd64.pyd +0 -0
- decision_graph/decision_tree/bake/c_logic_group.h +1226 -0
- decision_graph/decision_tree/bake/c_logic_group.pxd +162 -0
- decision_graph/decision_tree/bake/c_logic_group.pyi +264 -0
- decision_graph/decision_tree/bake/c_logic_group.pyx +308 -0
- decision_graph/decision_tree/bake/c_node.cp315-win_amd64.pyd +0 -0
- decision_graph/decision_tree/bake/c_node.h +2660 -0
- decision_graph/decision_tree/bake/c_node.pxd +381 -0
- decision_graph/decision_tree/bake/c_node.pyi +438 -0
- decision_graph/decision_tree/bake/c_node.pyx +639 -0
- decision_graph/decision_tree/bake/c_reconstruct.cp315-win_amd64.pyd +0 -0
- decision_graph/decision_tree/bake/c_reconstruct.pxd +63 -0
- decision_graph/decision_tree/bake/c_reconstruct.pyi +60 -0
- decision_graph/decision_tree/bake/c_reconstruct.pyx +252 -0
- decision_graph/decision_tree/bake/c_var.cp315-win_amd64.pyd +0 -0
- decision_graph/decision_tree/bake/c_var.h +2054 -0
- decision_graph/decision_tree/bake/c_var.pxd +205 -0
- decision_graph/decision_tree/bake/c_var.pyi +271 -0
- decision_graph/decision_tree/bake/c_var.pyx +228 -0
- decision_graph/decision_tree/capi/__infra__.pxd +33 -0
- decision_graph/decision_tree/capi/__init__.pxd +33 -0
- decision_graph/decision_tree/capi/__init__.py +57 -0
- decision_graph/decision_tree/capi/c_abc.cp315-win_amd64.pyd +0 -0
- decision_graph/decision_tree/capi/c_abc.pxd +246 -0
- decision_graph/decision_tree/capi/c_abc.pyi +770 -0
- decision_graph/decision_tree/capi/c_abc.pyx +1532 -0
- decision_graph/decision_tree/capi/c_collection.cp315-win_amd64.pyd +0 -0
- decision_graph/decision_tree/capi/c_collection.pxd +22 -0
- decision_graph/decision_tree/capi/c_collection.pyi +245 -0
- decision_graph/decision_tree/capi/c_collection.pyx +138 -0
- decision_graph/decision_tree/capi/c_node.cp315-win_amd64.pyd +0 -0
- decision_graph/decision_tree/capi/c_node.pxd +95 -0
- decision_graph/decision_tree/capi/c_node.pyi +492 -0
- decision_graph/decision_tree/capi/c_node.pyx +551 -0
- decision_graph/decision_tree/exc.py +189 -0
- decision_graph/decision_tree/native/__init__.py +57 -0
- decision_graph/decision_tree/native/abc.py +1167 -0
- decision_graph/decision_tree/native/collection.py +134 -0
- decision_graph/decision_tree/native/node.py +521 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_action.c +16320 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_action.h +324 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_allocator_protocol.c +12473 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_bake.c +16485 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_bake.h +629 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_collections.c +17381 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_collections.h +692 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_const.c +20815 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_const.h +395 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_edge.c +20696 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_edge.h +511 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_eval.h +848 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_expr.c +22065 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_expr.h +1900 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_hierarchy.c +23267 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_hierarchy.h +731 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_logic_group.c +22434 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_logic_group.h +1226 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_node.c +28385 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_node.h +2660 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_reconstruct.c +15955 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_var.c +16881 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_var.h +2054 -0
- decision_graph/includes/decision_graph/decision_tree/capi/c_abc.c +59196 -0
- decision_graph/includes/decision_graph/decision_tree/capi/c_collection.c +19967 -0
- decision_graph/includes/decision_graph/decision_tree/capi/c_node.c +39229 -0
- decision_graph/logic_group/__init__.py +25 -0
- decision_graph/logic_group/base.py +100 -0
- decision_graph/logic_group/pending_request.py +384 -0
- decision_graph/webui/__init__.py +24 -0
- decision_graph/webui/bake/__init__.py +65 -0
- decision_graph/webui/bake/app.py +488 -0
- decision_graph/webui/bake/static/d3.v7.min.js +2 -0
- decision_graph/webui/bake/static/script.js +1440 -0
- decision_graph/webui/bake/static/style.css +920 -0
- decision_graph/webui/bake/templates/index.html +183 -0
- decision_graph/webui/bake/templates/offline.html +184 -0
- decision_graph/webui/capi/__init__.py +54 -0
- decision_graph/webui/capi/app.py +366 -0
- decision_graph/webui/capi/main.py +53 -0
- decision_graph/webui/capi/static/d3.v7.min.js +2 -0
- decision_graph/webui/capi/static/script.js +1026 -0
- decision_graph/webui/capi/static/style.css +656 -0
- decision_graph/webui/capi/templates/index.html +93 -0
- decision_graph/webui/capi/templates/offline.html +94 -0
- decision_graph/webui/native/__init__.py +54 -0
- decision_graph/webui/native/app.py +366 -0
- decision_graph/webui/native/main.py +53 -0
- decision_graph/webui/native/static/d3.v7.min.js +2 -0
- decision_graph/webui/native/static/script.js +1026 -0
- decision_graph/webui/native/static/style.css +656 -0
- decision_graph/webui/native/templates/index.html +93 -0
- decision_graph/webui/native/templates/offline.html +94 -0
- pydecisiongraph-0.3.2.post1.dist-info/METADATA +136 -0
- pydecisiongraph-0.3.2.post1.dist-info/RECORD +148 -0
- pydecisiongraph-0.3.2.post1.dist-info/WHEEL +5 -0
- pydecisiongraph-0.3.2.post1.dist-info/licenses/LICENSE +373 -0
- pydecisiongraph-0.3.2.post1.dist-info/top_level.txt +2 -0
- tests/bake_c_evaluator.cp315-win_amd64.pyd +0 -0
- tests/bake_eval_probe.cp315-win_amd64.pyd +0 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Decision Tree Visualizer</title>
|
|
7
|
+
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
|
8
|
+
<script src="{{ url_for('static', filename='d3.v7.min.js') }}"></script>
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div id="header">
|
|
12
|
+
<h1>Decision Tree Visualization (Tree ID: {{ tree_id }})</h1>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<!-- Main layout: vertical sidebar (logic groups) + content -->
|
|
16
|
+
<div id="main">
|
|
17
|
+
<!-- Vertical nav sidebar for logic groups -->
|
|
18
|
+
<aside id="logic-group-tabs" role="region" aria-label="Logic groups">
|
|
19
|
+
<button class="tab-button active" data-group="*">All</button>
|
|
20
|
+
<!-- Other tabs inserted by JS -->
|
|
21
|
+
</aside>
|
|
22
|
+
|
|
23
|
+
<!-- Content area: controls bar + tree -->
|
|
24
|
+
<div id="content">
|
|
25
|
+
<!-- Controls bar: left = highlight toggle, right = export/theme buttons -->
|
|
26
|
+
<div id="controls-bar">
|
|
27
|
+
<!-- highlight-control will be injected by JS if window.with_eval is true -->
|
|
28
|
+
<!-- export controls will be injected here by script.js -->
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<!-- Tree Container (no native scrollbars; zoom/pan handles navigation) -->
|
|
32
|
+
<div id="tree-container"
|
|
33
|
+
data-initial-tree='{{ initial_tree_data | tojson | e }}'
|
|
34
|
+
data-tree-id="{{ tree_id }}">
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div id="node-info" style="display: none;">
|
|
40
|
+
<div class="node-info-header">
|
|
41
|
+
<h3>Node Details</h3>
|
|
42
|
+
<button id="node-info-pin" title="Pin">📌</button>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="node-info-body">
|
|
45
|
+
<table class="node-info-table">
|
|
46
|
+
<tbody>
|
|
47
|
+
<tr>
|
|
48
|
+
<td class="node-info-label">ID</td>
|
|
49
|
+
<td class="node-info-value" id="info-id"></td>
|
|
50
|
+
</tr>
|
|
51
|
+
<tr>
|
|
52
|
+
<td class="node-info-label">Name</td>
|
|
53
|
+
<td class="node-info-value" id="info-name"></td>
|
|
54
|
+
</tr>
|
|
55
|
+
<tr>
|
|
56
|
+
<td class="node-info-label">Repr</td>
|
|
57
|
+
<td class="node-info-value" id="info-repr"></td>
|
|
58
|
+
</tr>
|
|
59
|
+
<tr>
|
|
60
|
+
<td class="node-info-label">Type</td>
|
|
61
|
+
<td class="node-info-value" id="info-type"></td>
|
|
62
|
+
</tr>
|
|
63
|
+
<tr>
|
|
64
|
+
<td class="node-info-label">Labels</td>
|
|
65
|
+
<td class="node-info-value" id="info-labels"></td>
|
|
66
|
+
</tr>
|
|
67
|
+
<tr>
|
|
68
|
+
<td class="node-info-label">Autogen</td>
|
|
69
|
+
<td class="node-info-value" id="info-autogen"></td>
|
|
70
|
+
</tr>
|
|
71
|
+
<tr>
|
|
72
|
+
<td class="node-info-label">Expression</td>
|
|
73
|
+
<td class="node-info-value" id="info-expr"></td>
|
|
74
|
+
</tr>
|
|
75
|
+
</tbody>
|
|
76
|
+
</table>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<script>
|
|
81
|
+
window.with_eval = {{ 'true' if with_eval else 'false' }};
|
|
82
|
+
window.with_watch = {{ 'true' if with_watch else 'false' }};
|
|
83
|
+
</script>
|
|
84
|
+
|
|
85
|
+
<script src="{{ url_for('static', filename='script.js') }}"></script>
|
|
86
|
+
<script>
|
|
87
|
+
const container = d3.select("#tree-container");
|
|
88
|
+
const initialTreeData = JSON.parse(container.attr("data-initial-tree"));
|
|
89
|
+
const treeId = container.attr("data-tree-id");
|
|
90
|
+
visualizeTree(initialTreeData);
|
|
91
|
+
</script>
|
|
92
|
+
</body>
|
|
93
|
+
</html>
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Decision Tree Visualizer (Offline)</title>
|
|
7
|
+
<style>
|
|
8
|
+
{{ css_content | safe }}
|
|
9
|
+
</style>
|
|
10
|
+
<script>{{ d3_content | safe }}</script>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div id="header">
|
|
14
|
+
<h1>Decision Tree Visualization (Offline)</h1>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<!-- Main layout: vertical sidebar (logic groups) + content -->
|
|
18
|
+
<div id="main">
|
|
19
|
+
<!-- Vertical nav sidebar for logic groups -->
|
|
20
|
+
<aside id="logic-group-tabs" role="region" aria-label="Logic groups">
|
|
21
|
+
<button class="tab-button active" data-group="*">All</button>
|
|
22
|
+
<!-- Other tabs inserted by JS -->
|
|
23
|
+
</aside>
|
|
24
|
+
|
|
25
|
+
<!-- Content area: controls bar + tree -->
|
|
26
|
+
<div id="content">
|
|
27
|
+
<!-- Controls bar: left = highlight toggle, right = export/theme buttons -->
|
|
28
|
+
<div id="controls-bar">
|
|
29
|
+
<!-- highlight-control will be injected by JS if window.with_eval is true -->
|
|
30
|
+
<!-- export controls will be injected here by script.js -->
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<!-- Tree Container (no native scrollbars; zoom/pan handles navigation) -->
|
|
34
|
+
<div id="tree-container"
|
|
35
|
+
data-initial-tree='{{ initial_tree_data | tojson | e }}'
|
|
36
|
+
data-tree-id="offline">
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div id="node-info" style="display: none;">
|
|
42
|
+
<div class="node-info-header">
|
|
43
|
+
<h3>Node Details</h3>
|
|
44
|
+
<button id="node-info-pin" title="Pin">📌</button>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="node-info-body">
|
|
47
|
+
<table class="node-info-table">
|
|
48
|
+
<tbody>
|
|
49
|
+
<tr>
|
|
50
|
+
<td class="node-info-label">ID</td>
|
|
51
|
+
<td class="node-info-value" id="info-id"></td>
|
|
52
|
+
</tr>
|
|
53
|
+
<tr>
|
|
54
|
+
<td class="node-info-label">Name</td>
|
|
55
|
+
<td class="node-info-value" id="info-name"></td>
|
|
56
|
+
</tr>
|
|
57
|
+
<tr>
|
|
58
|
+
<td class="node-info-label">Repr</td>
|
|
59
|
+
<td class="node-info-value" id="info-repr"></td>
|
|
60
|
+
</tr>
|
|
61
|
+
<tr>
|
|
62
|
+
<td class="node-info-label">Type</td>
|
|
63
|
+
<td class="node-info-value" id="info-type"></td>
|
|
64
|
+
</tr>
|
|
65
|
+
<tr>
|
|
66
|
+
<td class="node-info-label">Labels</td>
|
|
67
|
+
<td class="node-info-value" id="info-labels"></td>
|
|
68
|
+
</tr>
|
|
69
|
+
<tr>
|
|
70
|
+
<td class="node-info-label">Autogen</td>
|
|
71
|
+
<td class="node-info-value" id="info-autogen"></td>
|
|
72
|
+
</tr>
|
|
73
|
+
<tr>
|
|
74
|
+
<td class="node-info-label">Expression</td>
|
|
75
|
+
<td class="node-info-value" id="info-expr"></td>
|
|
76
|
+
</tr>
|
|
77
|
+
</tbody>
|
|
78
|
+
</table>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<script>
|
|
83
|
+
window.with_eval = {{ 'true' if with_eval else 'false' }};
|
|
84
|
+
window.with_watch = false;
|
|
85
|
+
</script>
|
|
86
|
+
<script>
|
|
87
|
+
{{ js_content | safe }}
|
|
88
|
+
const container = d3.select("#tree-container");
|
|
89
|
+
const initialTreeData = JSON.parse(container.attr("data-initial-tree"));
|
|
90
|
+
const treeId = container.attr("data-tree-id");
|
|
91
|
+
visualizeTree(initialTreeData);
|
|
92
|
+
</script>
|
|
93
|
+
</body>
|
|
94
|
+
</html>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""The web UI of the pure-Python layer (``decision_graph.decision_tree.native``).
|
|
2
|
+
|
|
3
|
+
The tree it draws is a native ``LogicNode`` tree, read through the native
|
|
4
|
+
surface alone — see ``.app`` for the one place that surface is named.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
from .. import LOGGER
|
|
10
|
+
from decision_graph.decision_tree.native import LogicNode, RootLogicNode
|
|
11
|
+
|
|
12
|
+
LOGGER = LOGGER.getChild('WebUI.Native')
|
|
13
|
+
|
|
14
|
+
from . import app
|
|
15
|
+
from .app import DecisionTreeWebUi
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def show(root: LogicNode, with_eval: bool = True, **kwargs):
|
|
19
|
+
_app = DecisionTreeWebUi(
|
|
20
|
+
host=kwargs.get('host', "127.0.0.1"),
|
|
21
|
+
port=kwargs.get('port', 5000),
|
|
22
|
+
debug=kwargs.get('debug', False)
|
|
23
|
+
)
|
|
24
|
+
_app.show(
|
|
25
|
+
node=root,
|
|
26
|
+
with_eval=with_eval
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def watch(root: RootLogicNode, interval: float = .5, block: bool = False, **kwargs):
|
|
31
|
+
_app = DecisionTreeWebUi(
|
|
32
|
+
host=kwargs.get('host', "127.0.0.1"),
|
|
33
|
+
port=kwargs.get('port', 5000),
|
|
34
|
+
debug=kwargs.get('debug', False)
|
|
35
|
+
)
|
|
36
|
+
_app.watch(
|
|
37
|
+
node=root,
|
|
38
|
+
interval=interval,
|
|
39
|
+
block=block
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def to_html(root: LogicNode, file_name: str, with_eval: bool = True):
|
|
44
|
+
DecisionTreeWebUi.to_html(
|
|
45
|
+
node=root,
|
|
46
|
+
file_name=file_name,
|
|
47
|
+
with_eval=with_eval
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def set_logger(logger: logging.Logger):
|
|
52
|
+
global LOGGER
|
|
53
|
+
LOGGER = logger.getChild('WebUI.Native')
|
|
54
|
+
app.LOGGER = LOGGER
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import pathlib
|
|
4
|
+
import queue
|
|
5
|
+
import socket
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from . import LOGGER
|
|
11
|
+
# Scoped on purpose: this UI draws what the pure-Python layer built, and reaches
|
|
12
|
+
# it by name rather than through the package that re-exports whichever layer won.
|
|
13
|
+
from decision_graph.decision_tree.native import (
|
|
14
|
+
LogicNode, ActionNode, BreakpointNode,
|
|
15
|
+
TRUE_CONDITION, FALSE_CONDITION, ELSE_CONDITION, NO_CONDITION,
|
|
16
|
+
RootLogicNode,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DecisionTreeWebUi(object):
|
|
21
|
+
"""Class to manage the Flask web UI for visualizing LogicNode trees."""
|
|
22
|
+
_builtin_node_type = [
|
|
23
|
+
'LogicNode', 'RootLogicNode', 'BreakpointNode',
|
|
24
|
+
'ActionNode', 'NoAction', 'LongAction', 'ShortAction', 'CancelAction', 'ClearAction',
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
def __init__(self, host: str, port: int, debug: bool):
|
|
28
|
+
"""
|
|
29
|
+
Initializes the web UI manager.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
host (str): The host address for the Flask server.
|
|
33
|
+
port (int): The port for the Flask server.
|
|
34
|
+
debug (bool): Whether to run Flask in debug mode.
|
|
35
|
+
"""
|
|
36
|
+
from flask import Flask
|
|
37
|
+
|
|
38
|
+
self.host = host
|
|
39
|
+
self.port = port
|
|
40
|
+
self.debug = debug
|
|
41
|
+
self.app = Flask(__name__, template_folder='templates', static_folder='static')
|
|
42
|
+
self.current_tree_data: dict[str, Any] | None = None
|
|
43
|
+
self.current_tree_id: str | None = None
|
|
44
|
+
self.node: LogicNode | None = None
|
|
45
|
+
self.with_eval = False
|
|
46
|
+
self.with_watch = False
|
|
47
|
+
|
|
48
|
+
self._setup_routes()
|
|
49
|
+
|
|
50
|
+
def _setup_routes(self):
|
|
51
|
+
"""Configures the Flask routes for the application."""
|
|
52
|
+
from flask import render_template, jsonify
|
|
53
|
+
|
|
54
|
+
@self.app.route('/')
|
|
55
|
+
def index():
|
|
56
|
+
if self.current_tree_data is None:
|
|
57
|
+
return render_template(
|
|
58
|
+
'index.html',
|
|
59
|
+
initial_tree_data={},
|
|
60
|
+
tree_id="empty",
|
|
61
|
+
with_eval=self.with_eval,
|
|
62
|
+
with_watch=False
|
|
63
|
+
)
|
|
64
|
+
else:
|
|
65
|
+
return render_template(
|
|
66
|
+
'index.html',
|
|
67
|
+
initial_tree_data=self.current_tree_data,
|
|
68
|
+
tree_id=self.current_tree_id,
|
|
69
|
+
with_eval=self.with_eval,
|
|
70
|
+
with_watch=self.with_watch
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
@self.app.route('/api/tree_data')
|
|
74
|
+
def get_tree_data():
|
|
75
|
+
if self.current_tree_data is None:
|
|
76
|
+
return jsonify({"error": "No tree data available"}), 404
|
|
77
|
+
return jsonify({"tree_data": self.current_tree_data, "tree_id": self.current_tree_id})
|
|
78
|
+
|
|
79
|
+
@self.app.route('/api/active_nodes')
|
|
80
|
+
def get_active_nodes():
|
|
81
|
+
# Returns the current set of active node IDs as JSON
|
|
82
|
+
if self.node is not None:
|
|
83
|
+
try:
|
|
84
|
+
if isinstance(self.node, RootLogicNode):
|
|
85
|
+
active_ids = [str(n.uid) for n in self.node.eval_path]
|
|
86
|
+
else:
|
|
87
|
+
active_ids = [str(n.uid) for n in self.node.eval_recursively()[1]]
|
|
88
|
+
return jsonify({'active_ids': active_ids})
|
|
89
|
+
except Exception:
|
|
90
|
+
LOGGER.error("Error getting active nodes", exc_info=True)
|
|
91
|
+
return jsonify({'active_ids': []})
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def _convert_node_to_dict(
|
|
95
|
+
cls,
|
|
96
|
+
node: LogicNode,
|
|
97
|
+
visited_nodes: dict[str, dict[str, Any]],
|
|
98
|
+
virtual_parent_links: list[dict[str, Any]],
|
|
99
|
+
activated_node_ids: set = None
|
|
100
|
+
) -> dict[str, Any]:
|
|
101
|
+
"""Recursively converts a LogicNode tree into a dictionary format suitable for JSON/D3."""
|
|
102
|
+
node_id = str(node.uid)
|
|
103
|
+
if node_id in visited_nodes:
|
|
104
|
+
return {"id": node_id, "is_reference": True}
|
|
105
|
+
|
|
106
|
+
node_type = node.__class__.__name__
|
|
107
|
+
if node_type not in cls._builtin_node_type:
|
|
108
|
+
if isinstance(node, ActionNode):
|
|
109
|
+
node_type = "ActionNode"
|
|
110
|
+
else:
|
|
111
|
+
node_type = "LogicNode"
|
|
112
|
+
|
|
113
|
+
# Determine if node is activated (only if activated_node_ids is provided)
|
|
114
|
+
is_activated = activated_node_ids is None or node_id in activated_node_ids
|
|
115
|
+
|
|
116
|
+
node_obj: dict[str, Any] = {
|
|
117
|
+
"id": node_id,
|
|
118
|
+
"name": node.repr,
|
|
119
|
+
"repr": repr(node),
|
|
120
|
+
"type": node_type,
|
|
121
|
+
"labels": node.labels,
|
|
122
|
+
"autogen": node.autogen,
|
|
123
|
+
"_children": [],
|
|
124
|
+
"activated": is_activated
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
visited_nodes[node_id] = node_obj
|
|
128
|
+
|
|
129
|
+
# Process children
|
|
130
|
+
if node_type == 'BreakpointNode':
|
|
131
|
+
# For BreakpointNode, should not scan its only child, but just add to virtual_parent_links
|
|
132
|
+
node: BreakpointNode
|
|
133
|
+
child_node = node.linked_to
|
|
134
|
+
if child_node is not None:
|
|
135
|
+
condition_type = "unconditional"
|
|
136
|
+
if child_node.parent is node:
|
|
137
|
+
child_dict = cls._convert_node_to_dict(child_node, visited_nodes, virtual_parent_links, activated_node_ids)
|
|
138
|
+
child_with_condition = child_dict.copy()
|
|
139
|
+
child_with_condition['condition_to_child'] = "unconditional"
|
|
140
|
+
child_with_condition['condition_type'] = condition_type
|
|
141
|
+
node_obj["_children"].append(child_with_condition)
|
|
142
|
+
else:
|
|
143
|
+
virtual_parent_links.append(
|
|
144
|
+
{
|
|
145
|
+
"source": node_id,
|
|
146
|
+
"target": str(child_node.uid),
|
|
147
|
+
"type": "virtual_parent"
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
else:
|
|
151
|
+
for condition, child_node in node.children.items():
|
|
152
|
+
if condition == TRUE_CONDITION:
|
|
153
|
+
condition_type = "true"
|
|
154
|
+
elif condition == FALSE_CONDITION:
|
|
155
|
+
condition_type = "false"
|
|
156
|
+
elif condition == ELSE_CONDITION:
|
|
157
|
+
condition_type = "else"
|
|
158
|
+
elif condition == NO_CONDITION:
|
|
159
|
+
condition_type = "unconditional"
|
|
160
|
+
else:
|
|
161
|
+
condition_type = "other"
|
|
162
|
+
|
|
163
|
+
child_dict = cls._convert_node_to_dict(child_node, visited_nodes, virtual_parent_links, activated_node_ids)
|
|
164
|
+
child_with_condition = child_dict.copy()
|
|
165
|
+
child_with_condition['condition_to_child'] = f'<{str(condition)}>' if condition is not None else "<Unknown>"
|
|
166
|
+
child_with_condition['condition_type'] = condition_type
|
|
167
|
+
node_obj["_children"].append(child_with_condition)
|
|
168
|
+
return node_obj
|
|
169
|
+
|
|
170
|
+
@classmethod
|
|
171
|
+
def _convert_tree_to_d3_format(cls, root_node: LogicNode, activated_node_ids: set = None) -> dict[str, Any]:
|
|
172
|
+
"""Converts the LogicNode tree into a D3 hierarchical format."""
|
|
173
|
+
visited_nodes = {}
|
|
174
|
+
virtual_parent_links = []
|
|
175
|
+
|
|
176
|
+
root_dict = cls._convert_node_to_dict(root_node, visited_nodes, virtual_parent_links, activated_node_ids)
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
"root": root_dict,
|
|
180
|
+
"virtual_links": virtual_parent_links
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
def _port_is_free(self, port: int) -> bool:
|
|
184
|
+
try:
|
|
185
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
186
|
+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
187
|
+
s.bind((self.host, port))
|
|
188
|
+
return True
|
|
189
|
+
except OSError:
|
|
190
|
+
return False
|
|
191
|
+
|
|
192
|
+
def _auto_port(self, max_tries: int = 200) -> int:
|
|
193
|
+
requested_port = self.port
|
|
194
|
+
if not self._port_is_free(requested_port):
|
|
195
|
+
LOGGER.warning(f"Port {requested_port} is in use — searching for a free port...")
|
|
196
|
+
# Try next N ports, then fall back to ephemeral port
|
|
197
|
+
for i in range(max_tries):
|
|
198
|
+
requested_port += 1
|
|
199
|
+
if self._port_is_free(requested_port):
|
|
200
|
+
return requested_port
|
|
201
|
+
# If no port found in range, get an ephemeral port
|
|
202
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
203
|
+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
204
|
+
s.bind((self.host, 0))
|
|
205
|
+
requested_port = s.getsockname()[1]
|
|
206
|
+
LOGGER.info(f"Selected alternative port {requested_port} for this session")
|
|
207
|
+
return requested_port
|
|
208
|
+
|
|
209
|
+
@staticmethod
|
|
210
|
+
def _open_browser(url):
|
|
211
|
+
import webbrowser
|
|
212
|
+
def open_browser():
|
|
213
|
+
time.sleep(1)
|
|
214
|
+
try:
|
|
215
|
+
webbrowser.open(url)
|
|
216
|
+
except Exception:
|
|
217
|
+
LOGGER.exception("Failed to open browser for URL: %s", url)
|
|
218
|
+
|
|
219
|
+
browser_thread = threading.Thread(target=open_browser)
|
|
220
|
+
browser_thread.daemon = True
|
|
221
|
+
browser_thread.start()
|
|
222
|
+
|
|
223
|
+
def show(self, node: LogicNode, with_eval: bool = True):
|
|
224
|
+
"""Starts the Flask web UI to visualize a LogicNode tree."""
|
|
225
|
+
if not isinstance(node, LogicNode):
|
|
226
|
+
raise TypeError("The 'node' argument must be an instance of LogicNode or its subclass.")
|
|
227
|
+
|
|
228
|
+
LOGGER.info(f"Preparing to visualize LogicNode tree starting at {node}")
|
|
229
|
+
|
|
230
|
+
activated_node_ids = None if not with_eval \
|
|
231
|
+
else {str(n.uid) for n in node.eval_path} if isinstance(node, RootLogicNode) \
|
|
232
|
+
else {str(n.uid) for n in node.eval_recursively()[1]}
|
|
233
|
+
self.with_eval = with_eval
|
|
234
|
+
self.current_tree_data = self._convert_tree_to_d3_format(node, activated_node_ids)
|
|
235
|
+
self.current_tree_id = str(node.uid)
|
|
236
|
+
self.with_watch = False
|
|
237
|
+
|
|
238
|
+
port_to_use = self._auto_port()
|
|
239
|
+
url = f"http://{self.host}:{port_to_use}"
|
|
240
|
+
self._open_browser(url)
|
|
241
|
+
LOGGER.info(f"Starting Flask server on {url} (with_eval={with_eval})")
|
|
242
|
+
self.app.run(host=self.host, port=port_to_use, debug=self.debug, use_reloader=False, threaded=True)
|
|
243
|
+
|
|
244
|
+
def watch(self, node: RootLogicNode, interval: float = 0.5, block: bool = False):
|
|
245
|
+
"""
|
|
246
|
+
Starts a watch server that streams activation diffs via SSE.
|
|
247
|
+
Each client connection gets its own worker and queue, so multiple tabs/windows do not interfere.
|
|
248
|
+
If block is False, runs Flask in a background thread and returns immediately.
|
|
249
|
+
"""
|
|
250
|
+
from flask import Response, stream_with_context
|
|
251
|
+
last_activated = set(str(n.uid) for n in node.eval_path)
|
|
252
|
+
self.current_tree_data = self._convert_tree_to_d3_format(node, last_activated)
|
|
253
|
+
self.current_tree_id = str(node.uid)
|
|
254
|
+
self.with_eval = True
|
|
255
|
+
self.with_watch = True
|
|
256
|
+
self.node = node
|
|
257
|
+
|
|
258
|
+
@self.app.route('/watch')
|
|
259
|
+
def sse_watch():
|
|
260
|
+
q = queue.Queue()
|
|
261
|
+
stop_event = threading.Event()
|
|
262
|
+
|
|
263
|
+
def worker():
|
|
264
|
+
nonlocal last_activated
|
|
265
|
+
while not stop_event.is_set():
|
|
266
|
+
activated_now = set(str(n.uid) for n in node.eval_path)
|
|
267
|
+
added = list(activated_now - last_activated)
|
|
268
|
+
removed = list(last_activated - activated_now)
|
|
269
|
+
if added or removed:
|
|
270
|
+
q.put(json.dumps({'added': added, 'removed': removed}))
|
|
271
|
+
last_activated = activated_now
|
|
272
|
+
time.sleep(interval)
|
|
273
|
+
|
|
274
|
+
t = threading.Thread(target=worker)
|
|
275
|
+
t.daemon = True
|
|
276
|
+
t.start()
|
|
277
|
+
|
|
278
|
+
def event_stream():
|
|
279
|
+
try:
|
|
280
|
+
while True:
|
|
281
|
+
try:
|
|
282
|
+
diff = q.get(timeout=interval)
|
|
283
|
+
except queue.Empty:
|
|
284
|
+
continue
|
|
285
|
+
LOGGER.info(f"Watching {diff}")
|
|
286
|
+
yield f"data: {diff}\n\n"
|
|
287
|
+
except GeneratorExit:
|
|
288
|
+
stop_event.set()
|
|
289
|
+
|
|
290
|
+
return Response(stream_with_context(event_stream()), mimetype='text/event-stream')
|
|
291
|
+
|
|
292
|
+
port_to_use = self._auto_port()
|
|
293
|
+
url = f"http://{self.host}:{port_to_use}"
|
|
294
|
+
self._open_browser(url)
|
|
295
|
+
LOGGER.info(f"Starting SSE watch server on port {port_to_use}")
|
|
296
|
+
if block:
|
|
297
|
+
self.app.run(host=self.host, port=port_to_use, debug=self.debug, use_reloader=False, threaded=True)
|
|
298
|
+
else:
|
|
299
|
+
def run_flask():
|
|
300
|
+
self.app.run(host=self.host, port=port_to_use, debug=self.debug, use_reloader=False, threaded=True)
|
|
301
|
+
|
|
302
|
+
flask_thread = threading.Thread(target=run_flask)
|
|
303
|
+
flask_thread.daemon = True
|
|
304
|
+
flask_thread.start()
|
|
305
|
+
return flask_thread
|
|
306
|
+
|
|
307
|
+
@classmethod
|
|
308
|
+
def to_html(cls, node: LogicNode, file_name: str, with_eval: bool = True):
|
|
309
|
+
"""
|
|
310
|
+
Exports a LogicNode tree as a self-contained offline HTML file.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
node (LogicNode): The root node of the tree to visualize.
|
|
314
|
+
file_name (str): Output HTML file path.
|
|
315
|
+
with_eval (bool): Whether to include evaluation (activation) data.
|
|
316
|
+
"""
|
|
317
|
+
from jinja2 import Environment, FileSystemLoader
|
|
318
|
+
|
|
319
|
+
if not isinstance(node, LogicNode):
|
|
320
|
+
raise TypeError("The 'node' argument must be an instance of LogicNode or its subclass.")
|
|
321
|
+
|
|
322
|
+
if with_eval:
|
|
323
|
+
try:
|
|
324
|
+
if isinstance(node, RootLogicNode) and node.eval_path:
|
|
325
|
+
activated_node_ids = {str(n.uid) for n in node.eval_path}
|
|
326
|
+
else:
|
|
327
|
+
activated_node_ids = {str(n.uid) for n in node.eval_recursively()[1]}
|
|
328
|
+
except Exception:
|
|
329
|
+
LOGGER.error(f"Could not find evaluation path for node {node}", exc_info=True)
|
|
330
|
+
activated_node_ids = None
|
|
331
|
+
else:
|
|
332
|
+
activated_node_ids = None
|
|
333
|
+
|
|
334
|
+
tree_data = cls._convert_tree_to_d3_format(node, activated_node_ids)
|
|
335
|
+
|
|
336
|
+
# Locate resource directories
|
|
337
|
+
module_dir = pathlib.Path(__file__).parent
|
|
338
|
+
template_dir = module_dir / "templates"
|
|
339
|
+
static_dir = module_dir / "static"
|
|
340
|
+
css_path = static_dir / "style.css"
|
|
341
|
+
js_path = static_dir / "script.js"
|
|
342
|
+
d3_path = static_dir / "d3.v7.min.js"
|
|
343
|
+
|
|
344
|
+
with open(css_path, "r", encoding="utf-8") as f:
|
|
345
|
+
css_content = f.read()
|
|
346
|
+
|
|
347
|
+
with open(js_path, "r", encoding="utf-8") as f:
|
|
348
|
+
js_content = f.read()
|
|
349
|
+
|
|
350
|
+
with open(d3_path, "r", encoding="utf-8") as f:
|
|
351
|
+
d3_content = f.read()
|
|
352
|
+
|
|
353
|
+
env = Environment(loader=FileSystemLoader(template_dir))
|
|
354
|
+
template = env.get_template("offline.html")
|
|
355
|
+
html_output = template.render(
|
|
356
|
+
initial_tree_data=tree_data,
|
|
357
|
+
with_eval=with_eval,
|
|
358
|
+
css_content=css_content,
|
|
359
|
+
js_content=js_content,
|
|
360
|
+
d3_content=d3_content
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
with open(file_name, "w", encoding="utf-8") as f:
|
|
364
|
+
f.write(html_output)
|
|
365
|
+
|
|
366
|
+
LOGGER.info(f'Offline HTML exported to: "{os.path.realpath(file_name)}"')
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# decision_graph/webui/native/main.py
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
# --- Configuration ---
|
|
7
|
+
DEFAULT_HOST = "127.0.0.1"
|
|
8
|
+
DEFAULT_PORT = 5000
|
|
9
|
+
FLASK_DEBUG = False # Set to True for development debugging
|
|
10
|
+
LOG_LEVEL = logging.INFO # Or logging.DEBUG for more detail
|
|
11
|
+
# --- End Configuration ---
|
|
12
|
+
|
|
13
|
+
logging.basicConfig(level=LOG_LEVEL)
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parse() -> argparse.Namespace:
|
|
18
|
+
"""Parses command-line arguments."""
|
|
19
|
+
parser = argparse.ArgumentParser(description="Visualize a Decision Tree.")
|
|
20
|
+
parser.add_argument('--host', type=str, default=DEFAULT_HOST, help='Host address for the web server (default: %(default)s)')
|
|
21
|
+
parser.add_argument('--port', type=int, default=DEFAULT_PORT, help='Port for the web server (default: %(default)s)')
|
|
22
|
+
parser.add_argument('--debug', action='store_true', help='Enable Flask debug mode')
|
|
23
|
+
# Note: The actual LogicNode object cannot be passed via command line.
|
|
24
|
+
# This parser is for server configuration. The `show` function needs the node object passed programmatically.
|
|
25
|
+
# We'll add a dummy argument for demonstration or potential future use (e.g., loading from file).
|
|
26
|
+
parser.add_argument('--dummy_node_arg', type=str, help='Placeholder for future node loading mechanism (e.g., from file). Currently unused.')
|
|
27
|
+
return parser.parse_args()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def main(args: argparse.Namespace):
|
|
31
|
+
"""
|
|
32
|
+
Main execution function using parsed arguments.
|
|
33
|
+
Note: This example creates a dummy node for demonstration.
|
|
34
|
+
In practice, you would obtain the LogicNode object from elsewhere.
|
|
35
|
+
"""
|
|
36
|
+
logger.info(f"Parsing arguments: {args}")
|
|
37
|
+
|
|
38
|
+
# Example: Create a dummy node for demonstration purposes only.
|
|
39
|
+
# In a real scenario, you would pass the actual root node object to the `show` method.
|
|
40
|
+
# dummy_node = LogicNode(expression=LogicExpression.cast(True, repr="Dummy Root"))
|
|
41
|
+
# ui_instance = DecisionTreeWebUi(host=args.host, port=args.port, debug=args.debug)
|
|
42
|
+
# ui_instance.show(dummy_node)
|
|
43
|
+
|
|
44
|
+
if args.dummy_node_arg:
|
|
45
|
+
logger.warning("The --dummy_node_arg is currently a placeholder and does nothing.")
|
|
46
|
+
|
|
47
|
+
logger.info("To use the UI, call `DecisionTreeWebUi.show(your_logic_node)` from your code.")
|
|
48
|
+
logger.info(f"Example server config: Host={args.host}, Port={args.port}, Debug={args.debug}")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if __name__ == '__main__':
|
|
52
|
+
args = parse()
|
|
53
|
+
main(args)
|