PyDecisionGraph 0.3.2.post1__cp315-cp315t-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.
Files changed (148) hide show
  1. decision_graph/__infra__.pxd +1 -0
  2. decision_graph/__init__.pxd +1 -0
  3. decision_graph/__init__.py +39 -0
  4. decision_graph/decision_tree/__init__.pxd +2 -0
  5. decision_graph/decision_tree/__init__.py +85 -0
  6. decision_graph/decision_tree/bake/__infra__.pxd +502 -0
  7. decision_graph/decision_tree/bake/__init__.pxd +502 -0
  8. decision_graph/decision_tree/bake/__init__.py +29 -0
  9. decision_graph/decision_tree/bake/c_action.cp315t-win_amd64.pyd +0 -0
  10. decision_graph/decision_tree/bake/c_action.h +324 -0
  11. decision_graph/decision_tree/bake/c_action.pxd +54 -0
  12. decision_graph/decision_tree/bake/c_action.pyi +126 -0
  13. decision_graph/decision_tree/bake/c_action.pyx +65 -0
  14. decision_graph/decision_tree/bake/c_allocator_protocol.cp315t-win_amd64.pyd +0 -0
  15. decision_graph/decision_tree/bake/c_allocator_protocol.pxd +24 -0
  16. decision_graph/decision_tree/bake/c_allocator_protocol.pyi +61 -0
  17. decision_graph/decision_tree/bake/c_allocator_protocol.pyx +109 -0
  18. decision_graph/decision_tree/bake/c_bake.cp315t-win_amd64.pyd +0 -0
  19. decision_graph/decision_tree/bake/c_bake.h +629 -0
  20. decision_graph/decision_tree/bake/c_bake.pxd +52 -0
  21. decision_graph/decision_tree/bake/c_bake.pyi +151 -0
  22. decision_graph/decision_tree/bake/c_bake.pyx +142 -0
  23. decision_graph/decision_tree/bake/c_collections.cp315t-win_amd64.pyd +0 -0
  24. decision_graph/decision_tree/bake/c_collections.h +692 -0
  25. decision_graph/decision_tree/bake/c_collections.pxd +52 -0
  26. decision_graph/decision_tree/bake/c_collections.pyi +193 -0
  27. decision_graph/decision_tree/bake/c_collections.pyx +161 -0
  28. decision_graph/decision_tree/bake/c_const.cp315t-win_amd64.pyd +0 -0
  29. decision_graph/decision_tree/bake/c_const.h +395 -0
  30. decision_graph/decision_tree/bake/c_const.pxd +56 -0
  31. decision_graph/decision_tree/bake/c_const.pyi +305 -0
  32. decision_graph/decision_tree/bake/c_const.pyx +278 -0
  33. decision_graph/decision_tree/bake/c_edge.cp315t-win_amd64.pyd +0 -0
  34. decision_graph/decision_tree/bake/c_edge.h +511 -0
  35. decision_graph/decision_tree/bake/c_edge.pxd +117 -0
  36. decision_graph/decision_tree/bake/c_edge.pyi +192 -0
  37. decision_graph/decision_tree/bake/c_edge.pyx +270 -0
  38. decision_graph/decision_tree/bake/c_eval.h +848 -0
  39. decision_graph/decision_tree/bake/c_eval.pxd +40 -0
  40. decision_graph/decision_tree/bake/c_expr.cp315t-win_amd64.pyd +0 -0
  41. decision_graph/decision_tree/bake/c_expr.h +1900 -0
  42. decision_graph/decision_tree/bake/c_expr.pxd +129 -0
  43. decision_graph/decision_tree/bake/c_expr.pyi +280 -0
  44. decision_graph/decision_tree/bake/c_expr.pyx +272 -0
  45. decision_graph/decision_tree/bake/c_hierarchy.cp315t-win_amd64.pyd +0 -0
  46. decision_graph/decision_tree/bake/c_hierarchy.h +731 -0
  47. decision_graph/decision_tree/bake/c_hierarchy.pxd +71 -0
  48. decision_graph/decision_tree/bake/c_hierarchy.pyi +409 -0
  49. decision_graph/decision_tree/bake/c_hierarchy.pyx +277 -0
  50. decision_graph/decision_tree/bake/c_logic_group.cp315t-win_amd64.pyd +0 -0
  51. decision_graph/decision_tree/bake/c_logic_group.h +1226 -0
  52. decision_graph/decision_tree/bake/c_logic_group.pxd +162 -0
  53. decision_graph/decision_tree/bake/c_logic_group.pyi +264 -0
  54. decision_graph/decision_tree/bake/c_logic_group.pyx +308 -0
  55. decision_graph/decision_tree/bake/c_node.cp315t-win_amd64.pyd +0 -0
  56. decision_graph/decision_tree/bake/c_node.h +2660 -0
  57. decision_graph/decision_tree/bake/c_node.pxd +381 -0
  58. decision_graph/decision_tree/bake/c_node.pyi +438 -0
  59. decision_graph/decision_tree/bake/c_node.pyx +639 -0
  60. decision_graph/decision_tree/bake/c_reconstruct.cp315t-win_amd64.pyd +0 -0
  61. decision_graph/decision_tree/bake/c_reconstruct.pxd +63 -0
  62. decision_graph/decision_tree/bake/c_reconstruct.pyi +60 -0
  63. decision_graph/decision_tree/bake/c_reconstruct.pyx +252 -0
  64. decision_graph/decision_tree/bake/c_var.cp315t-win_amd64.pyd +0 -0
  65. decision_graph/decision_tree/bake/c_var.h +2054 -0
  66. decision_graph/decision_tree/bake/c_var.pxd +205 -0
  67. decision_graph/decision_tree/bake/c_var.pyi +271 -0
  68. decision_graph/decision_tree/bake/c_var.pyx +228 -0
  69. decision_graph/decision_tree/capi/__infra__.pxd +33 -0
  70. decision_graph/decision_tree/capi/__init__.pxd +33 -0
  71. decision_graph/decision_tree/capi/__init__.py +57 -0
  72. decision_graph/decision_tree/capi/c_abc.cp315t-win_amd64.pyd +0 -0
  73. decision_graph/decision_tree/capi/c_abc.pxd +246 -0
  74. decision_graph/decision_tree/capi/c_abc.pyi +770 -0
  75. decision_graph/decision_tree/capi/c_abc.pyx +1532 -0
  76. decision_graph/decision_tree/capi/c_collection.cp315t-win_amd64.pyd +0 -0
  77. decision_graph/decision_tree/capi/c_collection.pxd +22 -0
  78. decision_graph/decision_tree/capi/c_collection.pyi +245 -0
  79. decision_graph/decision_tree/capi/c_collection.pyx +138 -0
  80. decision_graph/decision_tree/capi/c_node.cp315t-win_amd64.pyd +0 -0
  81. decision_graph/decision_tree/capi/c_node.pxd +95 -0
  82. decision_graph/decision_tree/capi/c_node.pyi +492 -0
  83. decision_graph/decision_tree/capi/c_node.pyx +551 -0
  84. decision_graph/decision_tree/exc.py +189 -0
  85. decision_graph/decision_tree/native/__init__.py +57 -0
  86. decision_graph/decision_tree/native/abc.py +1167 -0
  87. decision_graph/decision_tree/native/collection.py +134 -0
  88. decision_graph/decision_tree/native/node.py +521 -0
  89. decision_graph/includes/decision_graph/decision_tree/bake/c_action.c +16320 -0
  90. decision_graph/includes/decision_graph/decision_tree/bake/c_action.h +324 -0
  91. decision_graph/includes/decision_graph/decision_tree/bake/c_allocator_protocol.c +12473 -0
  92. decision_graph/includes/decision_graph/decision_tree/bake/c_bake.c +16485 -0
  93. decision_graph/includes/decision_graph/decision_tree/bake/c_bake.h +629 -0
  94. decision_graph/includes/decision_graph/decision_tree/bake/c_collections.c +17381 -0
  95. decision_graph/includes/decision_graph/decision_tree/bake/c_collections.h +692 -0
  96. decision_graph/includes/decision_graph/decision_tree/bake/c_const.c +20815 -0
  97. decision_graph/includes/decision_graph/decision_tree/bake/c_const.h +395 -0
  98. decision_graph/includes/decision_graph/decision_tree/bake/c_edge.c +20696 -0
  99. decision_graph/includes/decision_graph/decision_tree/bake/c_edge.h +511 -0
  100. decision_graph/includes/decision_graph/decision_tree/bake/c_eval.h +848 -0
  101. decision_graph/includes/decision_graph/decision_tree/bake/c_expr.c +22065 -0
  102. decision_graph/includes/decision_graph/decision_tree/bake/c_expr.h +1900 -0
  103. decision_graph/includes/decision_graph/decision_tree/bake/c_hierarchy.c +23267 -0
  104. decision_graph/includes/decision_graph/decision_tree/bake/c_hierarchy.h +731 -0
  105. decision_graph/includes/decision_graph/decision_tree/bake/c_logic_group.c +22434 -0
  106. decision_graph/includes/decision_graph/decision_tree/bake/c_logic_group.h +1226 -0
  107. decision_graph/includes/decision_graph/decision_tree/bake/c_node.c +28385 -0
  108. decision_graph/includes/decision_graph/decision_tree/bake/c_node.h +2660 -0
  109. decision_graph/includes/decision_graph/decision_tree/bake/c_reconstruct.c +15955 -0
  110. decision_graph/includes/decision_graph/decision_tree/bake/c_var.c +16881 -0
  111. decision_graph/includes/decision_graph/decision_tree/bake/c_var.h +2054 -0
  112. decision_graph/includes/decision_graph/decision_tree/capi/c_abc.c +59196 -0
  113. decision_graph/includes/decision_graph/decision_tree/capi/c_collection.c +19967 -0
  114. decision_graph/includes/decision_graph/decision_tree/capi/c_node.c +39229 -0
  115. decision_graph/logic_group/__init__.py +25 -0
  116. decision_graph/logic_group/base.py +100 -0
  117. decision_graph/logic_group/pending_request.py +384 -0
  118. decision_graph/webui/__init__.py +24 -0
  119. decision_graph/webui/bake/__init__.py +65 -0
  120. decision_graph/webui/bake/app.py +488 -0
  121. decision_graph/webui/bake/static/d3.v7.min.js +2 -0
  122. decision_graph/webui/bake/static/script.js +1440 -0
  123. decision_graph/webui/bake/static/style.css +920 -0
  124. decision_graph/webui/bake/templates/index.html +183 -0
  125. decision_graph/webui/bake/templates/offline.html +184 -0
  126. decision_graph/webui/capi/__init__.py +54 -0
  127. decision_graph/webui/capi/app.py +366 -0
  128. decision_graph/webui/capi/main.py +53 -0
  129. decision_graph/webui/capi/static/d3.v7.min.js +2 -0
  130. decision_graph/webui/capi/static/script.js +1026 -0
  131. decision_graph/webui/capi/static/style.css +656 -0
  132. decision_graph/webui/capi/templates/index.html +93 -0
  133. decision_graph/webui/capi/templates/offline.html +94 -0
  134. decision_graph/webui/native/__init__.py +54 -0
  135. decision_graph/webui/native/app.py +366 -0
  136. decision_graph/webui/native/main.py +53 -0
  137. decision_graph/webui/native/static/d3.v7.min.js +2 -0
  138. decision_graph/webui/native/static/script.js +1026 -0
  139. decision_graph/webui/native/static/style.css +656 -0
  140. decision_graph/webui/native/templates/index.html +93 -0
  141. decision_graph/webui/native/templates/offline.html +94 -0
  142. pydecisiongraph-0.3.2.post1.dist-info/METADATA +136 -0
  143. pydecisiongraph-0.3.2.post1.dist-info/RECORD +148 -0
  144. pydecisiongraph-0.3.2.post1.dist-info/WHEEL +5 -0
  145. pydecisiongraph-0.3.2.post1.dist-info/licenses/LICENSE +373 -0
  146. pydecisiongraph-0.3.2.post1.dist-info/top_level.txt +2 -0
  147. tests/bake_c_evaluator.cp315t-win_amd64.pyd +0 -0
  148. tests/bake_eval_probe.cp315t-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">&#128204;</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">&#128204;</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)