griptape-nodes 0.35.0__py3-none-any.whl → 0.36.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. griptape_nodes/__init__.py +259 -92
  2. griptape_nodes/app/__init__.py +6 -1
  3. griptape_nodes/app/app_websocket.py +481 -0
  4. griptape_nodes/bootstrap/bootstrap_script.py +12 -231
  5. griptape_nodes/bootstrap/workflow_runners/__init__.py +1 -0
  6. griptape_nodes/bootstrap/workflow_runners/bootstrap_workflow_runner.py +28 -0
  7. griptape_nodes/bootstrap/workflow_runners/local_workflow_runner.py +237 -0
  8. griptape_nodes/bootstrap/workflow_runners/subprocess_workflow_runner.py +58 -0
  9. griptape_nodes/bootstrap/workflow_runners/workflow_runner.py +11 -0
  10. griptape_nodes/exe_types/core_types.py +50 -0
  11. griptape_nodes/machines/node_resolution.py +3 -3
  12. griptape_nodes/node_library/library_registry.py +18 -7
  13. griptape_nodes/retained_mode/events/node_events.py +2 -2
  14. griptape_nodes/retained_mode/events/parameter_events.py +4 -49
  15. griptape_nodes/retained_mode/managers/flow_manager.py +5 -5
  16. griptape_nodes/retained_mode/managers/node_manager.py +192 -21
  17. griptape_nodes/retained_mode/managers/object_manager.py +2 -0
  18. griptape_nodes/retained_mode/managers/workflow_manager.py +14 -4
  19. {griptape_nodes-0.35.0.dist-info → griptape_nodes-0.36.0.dist-info}/METADATA +1 -1
  20. {griptape_nodes-0.35.0.dist-info → griptape_nodes-0.36.0.dist-info}/RECORD +23 -17
  21. {griptape_nodes-0.35.0.dist-info → griptape_nodes-0.36.0.dist-info}/WHEEL +0 -0
  22. {griptape_nodes-0.35.0.dist-info → griptape_nodes-0.36.0.dist-info}/entry_points.txt +0 -0
  23. {griptape_nodes-0.35.0.dist-info → griptape_nodes-0.36.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,34 +1,10 @@
1
1
  import argparse
2
- import importlib.util
3
2
  import json
4
3
  import logging
5
- import os
6
- import sys
7
- from pathlib import Path
8
- from queue import Queue
9
- from typing import Any
10
4
 
11
5
  from dotenv import load_dotenv
12
- from griptape.artifacts import TextArtifact
13
- from griptape.drivers.event_listener.griptape_cloud_event_listener_driver import GriptapeCloudEventListenerDriver
14
- from griptape.events import BaseEvent, EventBus, EventListener, FinishStructureRunEvent
15
- from register_libraries_script import ( # type: ignore[import] - This import is used in the runtime environment
16
- PATHS,
17
- register_libraries,
18
- )
19
6
 
20
- from griptape_nodes.exe_types.flow import ControlFlow
21
- from griptape_nodes.exe_types.node_types import EndNode, StartNode
22
- from griptape_nodes.retained_mode.events.base_events import (
23
- AppEvent,
24
- EventRequest,
25
- ExecutionGriptapeNodeEvent,
26
- GriptapeNodeEvent,
27
- ProgressEvent,
28
- )
29
- from griptape_nodes.retained_mode.events.execution_events import SingleExecutionStepRequest, StartFlowRequest
30
- from griptape_nodes.retained_mode.events.parameter_events import SetParameterValueRequest
31
- from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
7
+ from griptape_nodes.bootstrap.workflow_runners.bootstrap_workflow_runner import BootstrapWorkflowRunner
32
8
 
33
9
  logging.basicConfig(
34
10
  level=logging.INFO,
@@ -38,211 +14,6 @@ logger.setLevel(logging.INFO)
38
14
 
39
15
  load_dotenv()
40
16
 
41
- queue = Queue()
42
-
43
-
44
- def _load_user_workflow(path_to_workflow: str) -> None:
45
- # Ensure file_path is a Path object
46
- file_path = Path(path_to_workflow)
47
-
48
- # Generate a unique module name
49
- module_name = f"dynamic_module_{file_path.name.replace('.', '_')}_{hash(str(file_path))}"
50
-
51
- # Load the module specification
52
- spec = importlib.util.spec_from_file_location(module_name, file_path)
53
- if spec is None or spec.loader is None:
54
- msg = f"Could not load module specification from {file_path}"
55
- raise ImportError(msg)
56
-
57
- # Create the module
58
- module = importlib.util.module_from_spec(spec)
59
-
60
- # Add to sys.modules to handle recursive imports
61
- sys.modules[module_name] = module
62
-
63
- # Execute the module
64
- spec.loader.exec_module(module)
65
-
66
-
67
- def _load_flow_for_workflow() -> ControlFlow:
68
- context_manager = GriptapeNodes.ContextManager()
69
- return context_manager.get_current_flow()
70
-
71
-
72
- def _set_workflow_context(workflow_name: str) -> None:
73
- context_manager = GriptapeNodes.ContextManager()
74
- context_manager.push_workflow(workflow_name=workflow_name)
75
-
76
-
77
- def _handle_event(event: BaseEvent) -> None:
78
- try:
79
- if isinstance(event, GriptapeNodeEvent):
80
- __handle_node_event(event)
81
- elif isinstance(event, ExecutionGriptapeNodeEvent):
82
- __handle_execution_node_event(event)
83
- elif isinstance(event, ProgressEvent):
84
- __handle_progress_event(event)
85
- elif isinstance(event, AppEvent):
86
- __handle_app_event(event)
87
- else:
88
- msg = f"Unknown event type: {type(event)}"
89
- logger.info(msg)
90
- queue.put(event)
91
- except Exception as e:
92
- logger.info(e)
93
-
94
-
95
- def __handle_node_event(event: GriptapeNodeEvent) -> None:
96
- result_event = event.wrapped_event
97
- event_json = result_event.json()
98
- event_log = f"GriptapeNodeEvent: {event_json}"
99
- logger.info(event_log)
100
-
101
-
102
- def __handle_execution_node_event(event: ExecutionGriptapeNodeEvent) -> None:
103
- result_event = event.wrapped_event
104
- if type(result_event.payload).__name__ == "NodeStartProcessEvent":
105
- event_log = f"NodeStartProcessEvent: {result_event.payload}"
106
- logger.info(event_log)
107
-
108
- elif type(result_event.payload).__name__ == "ResumeNodeProcessingEvent":
109
- event_log = f"ResumeNodeProcessingEvent: {result_event.payload}"
110
- logger.info(event_log)
111
-
112
- # Here we need to handle the resume event since this is the callback mechanism
113
- # for the flow to be resumed for any Node that yields a generator in its process method.
114
- node_name = result_event.payload.node_name
115
- flow_name = GriptapeNodes.NodeManager().get_node_parent_flow_by_name(node_name)
116
- event_request = EventRequest(request=SingleExecutionStepRequest(flow_name=flow_name))
117
- GriptapeNodes.handle_request(event_request.request)
118
-
119
- elif type(result_event.payload).__name__ == "NodeFinishProcessEvent":
120
- event_log = f"NodeFinishProcessEvent: {result_event.payload}"
121
- logger.info(event_log)
122
-
123
- else:
124
- event_log = f"ExecutionGriptapeNodeEvent: {result_event.payload}"
125
- logger.info(event_log)
126
-
127
- queue.put(event)
128
-
129
-
130
- def __handle_progress_event(gt_event: ProgressEvent) -> None:
131
- event_log = f"ProgressEvent: {gt_event}"
132
- logger.info(event_log)
133
-
134
-
135
- def __handle_app_event(event: AppEvent) -> None:
136
- event_log = f"AppEvent: {event.payload}"
137
- logger.info(event_log)
138
-
139
-
140
- def _submit_output(output: dict) -> None:
141
- if "GT_CLOUD_STRUCTURE_RUN_ID" in os.environ:
142
- kwargs: dict = {
143
- "batched": False,
144
- }
145
- if "GT_CLOUD_BASE_URL" in os.environ:
146
- base_url = os.environ["GT_CLOUD_BASE_URL"]
147
- if "http://localhost" in base_url or "http://127.0.0.1" in base_url:
148
- kwargs["headers"] = {}
149
- gtc_event_listener = GriptapeCloudEventListenerDriver(**kwargs)
150
- gtc_event_listener.try_publish_event_payload(
151
- FinishStructureRunEvent(output_task_output=TextArtifact(json.dumps(output))).to_dict()
152
- )
153
-
154
-
155
- def _set_input_for_flow(flow_name: str, flow_input: dict[str, dict]) -> None:
156
- control_flow = GriptapeNodes.FlowManager().get_flow_by_name(flow_name)
157
- nodes = control_flow.nodes
158
- for node_name, node in nodes.items():
159
- if isinstance(node, StartNode):
160
- param_map: dict | None = flow_input.get(node_name)
161
- if param_map is not None:
162
- for parameter_name, parameter_value in param_map.items():
163
- set_parameter_value_request = SetParameterValueRequest(
164
- parameter_name=parameter_name,
165
- value=parameter_value,
166
- node_name=node_name,
167
- )
168
- set_parameter_value_result = GriptapeNodes.handle_request(set_parameter_value_request)
169
-
170
- if set_parameter_value_result.failed():
171
- msg = f"Failed to set parameter {parameter_name} for node {node_name}."
172
- raise ValueError(msg)
173
-
174
-
175
- def _get_output_for_flow(flow_name: str) -> dict:
176
- control_flow = GriptapeNodes.FlowManager().get_flow_by_name(flow_name)
177
- nodes = control_flow.nodes
178
- output = {}
179
- for node_name, node in nodes.items():
180
- if isinstance(node, EndNode):
181
- output[node_name] = node.parameter_values
182
-
183
- return output
184
-
185
-
186
- def run(workflow_name: str, flow_input: Any) -> None:
187
- """Executes a published workflow.
188
-
189
- Executes a workflow by setting up event listeners, registering libraries,
190
- loading the user-defined workflow, and running the specified workflow.
191
-
192
- Parameters:
193
- workflow_name: The name of the workflow to execute.
194
- flow_input: Input data for the flow, typically a dictionary.
195
-
196
- Returns:
197
- None
198
- """
199
- EventBus.add_event_listener(
200
- event_listener=EventListener(
201
- on_event=_handle_event,
202
- )
203
- )
204
-
205
- # Register all of our relevant libraries
206
- register_libraries(PATHS)
207
-
208
- # Required to set the workflow_context before loading the workflow
209
- # or nothing works. The name can be anything, but how about the workflow_name.
210
- _set_workflow_context(workflow_name=workflow_name)
211
- _load_user_workflow("workflow.py")
212
- flow = _load_flow_for_workflow()
213
- flow_name = flow.name
214
- # Now let's set the input to the flow
215
- _set_input_for_flow(flow_name=flow_name, flow_input=flow_input)
216
-
217
- # Now send the run command to actually execute it
218
- start_flow_request = StartFlowRequest(flow_name=flow_name)
219
- start_flow_result = GriptapeNodes.handle_request(start_flow_request)
220
-
221
- if start_flow_result.failed():
222
- msg = f"Failed to start flow {workflow_name}"
223
- raise ValueError(msg)
224
-
225
- logger.info("Workflow started!")
226
-
227
- # Wait for the control flow to finish
228
- is_flow_finished = False
229
- while not is_flow_finished:
230
- try:
231
- event = queue.get(block=True)
232
- if isinstance(event, ExecutionGriptapeNodeEvent):
233
- result_event = event.wrapped_event
234
-
235
- if type(result_event.payload).__name__ == "ControlFlowResolvedEvent":
236
- _submit_output(_get_output_for_flow(flow_name=flow_name))
237
- is_flow_finished = True
238
- logger.info("Workflow finished!")
239
-
240
- queue.task_done()
241
-
242
- except Exception as e:
243
- msg = f"Error handling queue event: {e}"
244
- logger.info(msg)
245
-
246
17
 
247
18
  if __name__ == "__main__":
248
19
  parser = argparse.ArgumentParser()
@@ -258,10 +29,17 @@ if __name__ == "__main__":
258
29
  default=None,
259
30
  help="The input to the flow",
260
31
  )
32
+ parser.add_argument(
33
+ "-s",
34
+ "--storage-backend",
35
+ default="local",
36
+ help="The storage backend to use",
37
+ )
261
38
 
262
39
  args = parser.parse_args()
263
40
  workflow_name = args.workflow_name
264
41
  flow_input = args.input
42
+ storage_backend = args.storage_backend
265
43
 
266
44
  try:
267
45
  flow_input = json.loads(flow_input) if flow_input else {}
@@ -270,4 +48,7 @@ if __name__ == "__main__":
270
48
  logger.info(msg)
271
49
  raise
272
50
 
273
- run(workflow_name=workflow_name, flow_input=flow_input)
51
+ workflow_runner = BootstrapWorkflowRunner()
52
+ workflow_runner.run(
53
+ workflow_path="workflow.py", workflow_name=workflow_name, flow_input=flow_input, storage_backend=storage_backend
54
+ )
@@ -0,0 +1 @@
1
+ """Workflow runners package."""
@@ -0,0 +1,28 @@
1
+ import json
2
+ import os
3
+
4
+ from griptape.artifacts import TextArtifact
5
+ from griptape.drivers.event_listener.griptape_cloud_event_listener_driver import GriptapeCloudEventListenerDriver
6
+ from griptape.events import FinishStructureRunEvent
7
+ from register_libraries_script import PATHS # type: ignore[import] - This import is used in the runtime environment
8
+
9
+ from griptape_nodes.bootstrap.workflow_runners.local_workflow_runner import LocalWorkflowRunner
10
+
11
+
12
+ class BootstrapWorkflowRunner(LocalWorkflowRunner):
13
+ def __init__(self) -> None:
14
+ super().__init__(libraries=PATHS)
15
+
16
+ def _submit_output(self, output: dict) -> None:
17
+ if "GT_CLOUD_STRUCTURE_RUN_ID" in os.environ:
18
+ kwargs: dict = {
19
+ "batched": False,
20
+ }
21
+ if "GT_CLOUD_BASE_URL" in os.environ:
22
+ base_url = os.environ["GT_CLOUD_BASE_URL"]
23
+ if "http://localhost" in base_url or "http://127.0.0.1" in base_url:
24
+ kwargs["headers"] = {}
25
+ gtc_event_listener = GriptapeCloudEventListenerDriver(**kwargs)
26
+ gtc_event_listener.try_publish_event_payload(
27
+ FinishStructureRunEvent(output_task_output=TextArtifact(json.dumps(output))).to_dict()
28
+ )
@@ -0,0 +1,237 @@
1
+ import importlib.util
2
+ import logging
3
+ import sys
4
+ from pathlib import Path
5
+ from queue import Queue
6
+ from typing import Any
7
+
8
+ from griptape.events import BaseEvent, EventBus, EventListener
9
+
10
+ from griptape_nodes.bootstrap.register_libraries_script import register_libraries
11
+ from griptape_nodes.bootstrap.workflow_runners.workflow_runner import WorkflowRunner
12
+ from griptape_nodes.exe_types.node_types import EndNode, StartNode
13
+ from griptape_nodes.retained_mode.events.base_events import (
14
+ AppEvent,
15
+ EventRequest,
16
+ ExecutionGriptapeNodeEvent,
17
+ GriptapeNodeEvent,
18
+ ProgressEvent,
19
+ )
20
+ from griptape_nodes.retained_mode.events.execution_events import SingleExecutionStepRequest, StartFlowRequest
21
+ from griptape_nodes.retained_mode.events.parameter_events import SetParameterValueRequest
22
+ from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class LocalWorkflowRunner(WorkflowRunner):
28
+ def __init__(self, libraries: list[Path]) -> None:
29
+ self.registered_libraries = False
30
+ self.libraries = libraries
31
+ self.queue = Queue()
32
+
33
+ def _load_user_workflow(self, path_to_workflow: str) -> None:
34
+ # Ensure file_path is a Path object
35
+ file_path = Path(path_to_workflow)
36
+
37
+ # Generate a unique module name
38
+ module_name = f"dynamic_module_{file_path.name.replace('.', '_')}_{hash(str(file_path))}"
39
+
40
+ # Load the module specification
41
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
42
+ if spec is None or spec.loader is None:
43
+ msg = f"Could not load module specification from {file_path}"
44
+ raise ImportError(msg)
45
+
46
+ # Create the module
47
+ module = importlib.util.module_from_spec(spec)
48
+
49
+ # Add to sys.modules to handle recursive imports
50
+ sys.modules[module_name] = module
51
+
52
+ # Execute the module
53
+ spec.loader.exec_module(module)
54
+
55
+ def _load_flow_for_workflow(self) -> str:
56
+ context_manager = GriptapeNodes.ContextManager()
57
+ return context_manager.get_current_flow().name
58
+
59
+ def _set_storage_backend(self, storage_backend: str) -> None:
60
+ from griptape_nodes.retained_mode.managers.config_manager import ConfigManager
61
+
62
+ config_manager = ConfigManager()
63
+ config_manager.set_config_value(
64
+ key="storage_backend",
65
+ value=storage_backend,
66
+ )
67
+
68
+ def _register_libraries(self) -> None:
69
+ if not self.registered_libraries:
70
+ register_libraries([str(p) for p in self.libraries])
71
+ self.registered_libraries = True
72
+
73
+ def _set_workflow_context(self, workflow_name: str) -> None:
74
+ context_manager = GriptapeNodes.ContextManager()
75
+ context_manager.push_workflow(workflow_name=workflow_name)
76
+
77
+ def _handle_event(self, event: BaseEvent) -> None:
78
+ try:
79
+ if isinstance(event, GriptapeNodeEvent):
80
+ self.__handle_node_event(event)
81
+ elif isinstance(event, ExecutionGriptapeNodeEvent):
82
+ self.__handle_execution_node_event(event)
83
+ elif isinstance(event, ProgressEvent):
84
+ self.__handle_progress_event(event)
85
+ elif isinstance(event, AppEvent):
86
+ self.__handle_app_event(event)
87
+ else:
88
+ msg = f"Unknown event type: {type(event)}"
89
+ logger.info(msg)
90
+ self.queue.put(event)
91
+ except Exception as e:
92
+ logger.info(e)
93
+
94
+ def __handle_node_event(self, event: GriptapeNodeEvent) -> None:
95
+ result_event = event.wrapped_event
96
+ event_json = result_event.json()
97
+ event_log = f"GriptapeNodeEvent: {event_json}"
98
+ logger.info(event_log)
99
+
100
+ def __handle_execution_node_event(self, event: ExecutionGriptapeNodeEvent) -> None:
101
+ result_event = event.wrapped_event
102
+ if type(result_event.payload).__name__ == "NodeStartProcessEvent":
103
+ event_log = f"NodeStartProcessEvent: {result_event.payload}"
104
+ logger.info(event_log)
105
+
106
+ elif type(result_event.payload).__name__ == "ResumeNodeProcessingEvent":
107
+ event_log = f"ResumeNodeProcessingEvent: {result_event.payload}"
108
+ logger.info(event_log)
109
+
110
+ # Here we need to handle the resume event since this is the callback mechanism
111
+ # for the flow to be resumed for any Node that yields a generator in its process method.
112
+ node_name = result_event.payload.node_name
113
+ flow_name = GriptapeNodes.NodeManager().get_node_parent_flow_by_name(node_name)
114
+ event_request = EventRequest(request=SingleExecutionStepRequest(flow_name=flow_name))
115
+ GriptapeNodes.handle_request(event_request.request)
116
+
117
+ elif type(result_event.payload).__name__ == "NodeFinishProcessEvent":
118
+ event_log = f"NodeFinishProcessEvent: {result_event.payload}"
119
+ logger.info(event_log)
120
+
121
+ else:
122
+ event_log = f"ExecutionGriptapeNodeEvent: {result_event.payload}"
123
+ logger.info(event_log)
124
+
125
+ self.queue.put(event)
126
+
127
+ def __handle_progress_event(self, gt_event: ProgressEvent) -> None:
128
+ event_log = f"ProgressEvent: {gt_event}"
129
+ logger.info(event_log)
130
+
131
+ def __handle_app_event(self, event: AppEvent) -> None:
132
+ event_log = f"AppEvent: {event.payload}"
133
+ logger.info(event_log)
134
+
135
+ def _submit_output(self, output: dict) -> None:
136
+ self.output = output
137
+
138
+ def _set_input_for_flow(self, flow_name: str, flow_input: dict[str, dict]) -> None:
139
+ control_flow = GriptapeNodes.FlowManager().get_flow_by_name(flow_name)
140
+ nodes = control_flow.nodes
141
+ for node_name, node in nodes.items():
142
+ if isinstance(node, StartNode):
143
+ param_map: dict | None = flow_input.get(node_name)
144
+ if param_map is not None:
145
+ for parameter_name, parameter_value in param_map.items():
146
+ set_parameter_value_request = SetParameterValueRequest(
147
+ parameter_name=parameter_name,
148
+ value=parameter_value,
149
+ node_name=node_name,
150
+ )
151
+ set_parameter_value_result = GriptapeNodes.handle_request(set_parameter_value_request)
152
+
153
+ if set_parameter_value_result.failed():
154
+ msg = f"Failed to set parameter {parameter_name} for node {node_name}."
155
+ raise ValueError(msg)
156
+
157
+ def _get_output_for_flow(self, flow_name: str) -> dict:
158
+ control_flow = GriptapeNodes.FlowManager().get_flow_by_name(flow_name)
159
+ nodes = control_flow.nodes
160
+ output = {}
161
+ for node_name, node in nodes.items():
162
+ if isinstance(node, EndNode):
163
+ output[node_name] = node.parameter_values
164
+
165
+ return output
166
+
167
+ def run(self, workflow_path: str, workflow_name: str, flow_input: Any, storage_backend: str = "local") -> None:
168
+ """Executes a published workflow.
169
+
170
+ Executes a workflow by setting up event listeners, registering libraries,
171
+ loading the user-defined workflow, and running the specified workflow.
172
+
173
+ Parameters:
174
+ workflow_name: The name of the workflow to execute.
175
+ flow_input: Input data for the flow, typically a dictionary.
176
+
177
+ Returns:
178
+ None
179
+ """
180
+ EventBus.add_event_listener(
181
+ event_listener=EventListener(
182
+ on_event=self._handle_event,
183
+ )
184
+ )
185
+
186
+ # Set the storage backend
187
+ self._set_storage_backend(storage_backend=storage_backend)
188
+
189
+ # Register all of our relevant libraries
190
+ self._register_libraries()
191
+
192
+ # Required to set the workflow_context before loading the workflow
193
+ # or nothing works. The name can be anything, but how about the workflow_name.
194
+ self._set_workflow_context(workflow_name=workflow_name)
195
+ self._load_user_workflow(workflow_path)
196
+ flow_name = self._load_flow_for_workflow()
197
+ # Now let's set the input to the flow
198
+ self._set_input_for_flow(flow_name=flow_name, flow_input=flow_input)
199
+
200
+ # Now send the run command to actually execute it
201
+ start_flow_request = StartFlowRequest(flow_name=flow_name)
202
+ start_flow_result = GriptapeNodes.handle_request(start_flow_request)
203
+
204
+ if start_flow_result.failed():
205
+ msg = f"Failed to start flow {workflow_name}"
206
+ raise ValueError(msg)
207
+
208
+ logger.info("Workflow started!")
209
+
210
+ # Wait for the control flow to finish
211
+ is_flow_finished = False
212
+ error: Exception | None = None
213
+ while not is_flow_finished:
214
+ try:
215
+ event = self.queue.get(block=True)
216
+
217
+ if isinstance(event, ExecutionGriptapeNodeEvent):
218
+ result_event = event.wrapped_event
219
+
220
+ if type(result_event.payload).__name__ == "ControlFlowResolvedEvent":
221
+ self._submit_output(self._get_output_for_flow(flow_name=flow_name))
222
+ is_flow_finished = True
223
+ logger.info("Workflow finished!")
224
+ elif type(result_event.payload).__name__ == "ControlFlowCancelledEvent":
225
+ msg = "Control flow cancelled"
226
+ is_flow_finished = True
227
+ logger.error(msg)
228
+ error = ValueError(msg)
229
+
230
+ self.queue.task_done()
231
+
232
+ except Exception as e:
233
+ msg = f"Error handling queue event: {e}"
234
+ logger.info(msg)
235
+
236
+ if error is not None:
237
+ raise error
@@ -0,0 +1,58 @@
1
+ import logging
2
+ import threading
3
+ from multiprocessing import Process, Queue
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from griptape_nodes.app.app import _serve_static_server
8
+ from griptape_nodes.bootstrap.workflow_runners.local_workflow_runner import LocalWorkflowRunner
9
+ from griptape_nodes.bootstrap.workflow_runners.workflow_runner import WorkflowRunner
10
+
11
+
12
+ class SubprocessWorkflowRunner(WorkflowRunner):
13
+ def __init__(self, libraries: list[Path]) -> None:
14
+ self.libraries = libraries
15
+
16
+ @staticmethod
17
+ def _subprocess_entry(
18
+ exception_queue: Queue,
19
+ libraries: list[Path],
20
+ workflow_path: str,
21
+ workflow_name: str,
22
+ flow_input: Any,
23
+ ) -> None:
24
+ # Reset logging to avoid duplicate logs in tests - this does not remove logs
25
+ # because griptape nodes is doing some configuration of its own that seems
26
+ # difficult to control.
27
+ logger = logging.getLogger()
28
+ for handler in logger.handlers[:]:
29
+ logger.removeHandler(handler)
30
+ logger.setLevel(logging.NOTSET)
31
+
32
+ try:
33
+ threading.Thread(target=_serve_static_server, daemon=True).start()
34
+ workflow_runner = LocalWorkflowRunner(libraries)
35
+ workflow_runner.run(workflow_path, workflow_name, flow_input, "local")
36
+ except Exception as e:
37
+ exception_queue.put(e)
38
+ raise
39
+
40
+ def run(self, workflow_path: str, workflow_name: str, flow_input: Any, storage_backend: str = "local") -> None: # noqa: ARG002
41
+ exception_queue = Queue()
42
+ process = Process(
43
+ target=self._subprocess_entry,
44
+ args=(exception_queue, self.libraries, workflow_path, workflow_name, flow_input),
45
+ )
46
+ process.start()
47
+ process.join()
48
+
49
+ if not exception_queue.empty():
50
+ exception = exception_queue.get_nowait()
51
+ if isinstance(exception, Exception):
52
+ raise exception
53
+ msg = f"Expected an Exception but got: {type(exception)}"
54
+ raise RuntimeError(msg)
55
+
56
+ if process.exitcode != 0:
57
+ msg = f"Process exited with code {process.exitcode} but no exception was raised."
58
+ raise RuntimeError(msg)
@@ -0,0 +1,11 @@
1
+ import logging
2
+ from abc import ABC, abstractmethod
3
+ from typing import Any
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+
8
+ class WorkflowRunner(ABC):
9
+ @abstractmethod
10
+ def run(self, workflow_path: str, workflow_name: str, flow_input: Any, storage_backend: str = "local") -> None:
11
+ pass
@@ -11,6 +11,7 @@ if TYPE_CHECKING:
11
11
  from collections.abc import Callable
12
12
  from types import TracebackType
13
13
 
14
+ from griptape_nodes.exe_types.node_types import BaseNode
14
15
  T = TypeVar("T", bound="Parameter")
15
16
  N = TypeVar("N", bound="BaseNodeElement")
16
17
 
@@ -271,6 +272,31 @@ class BaseNodeElement:
271
272
  """Return the element on top of the stack, or None if no active element."""
272
273
  return cls._stack[-1] if cls._stack else None
273
274
 
275
+ def to_event(self, node: BaseNode) -> dict:
276
+ """Serializes the node element and its children into a dictionary representation.
277
+
278
+ This method is used to create a data payload for AlterElementEvent to communicate changes or the current state of an element.
279
+ The resulting dictionary includes the element's ID, type, name, the name of the
280
+ provided BaseNode, and a recursively serialized list of its children.
281
+
282
+ For new BaseNodeElement types that require different serialization logic and fields, this method should be overridden to provide the necessary data.
283
+
284
+ Args:
285
+ node: The BaseNode instance to which this element is associated.
286
+ Used to include the node's name in the event data.
287
+
288
+ Returns:
289
+ A dictionary containing the serialized data of the element and its children.
290
+ """
291
+ event_data = {
292
+ "element_id": self.element_id,
293
+ "element_type": self.element_type,
294
+ "name": self.name,
295
+ "node_name": node.name,
296
+ "children": [child.to_event(node) for child in self.children],
297
+ }
298
+ return event_data
299
+
274
300
 
275
301
  @dataclass(kw_only=True)
276
302
  class ParameterMessage(BaseNodeElement):
@@ -327,6 +353,13 @@ class ParameterMessage(BaseNodeElement):
327
353
 
328
354
  return data
329
355
 
356
+ def to_event(self, node: BaseNode) -> dict:
357
+ event_data = super().to_event(node)
358
+ dict_data = self.to_dict()
359
+ # Combine them both to get what we need for the UI.
360
+ event_data.update(dict_data)
361
+ return event_data
362
+
330
363
 
331
364
  @dataclass(kw_only=True)
332
365
  class ParameterGroup(BaseNodeElement):
@@ -359,6 +392,11 @@ class ParameterGroup(BaseNodeElement):
359
392
  our_dict["ui_options"] = self.ui_options
360
393
  return our_dict
361
394
 
395
+ def to_event(self, node: BaseNode) -> dict:
396
+ event_data = super().to_event(node)
397
+ event_data["ui_options"] = self.ui_options
398
+ return event_data
399
+
362
400
  def equals(self, other: ParameterGroup) -> dict:
363
401
  self_dict = {"name": self.name, "ui_options": self.ui_options}
364
402
  other_dict = {"name": other.name, "ui_options": other.ui_options}
@@ -545,6 +583,18 @@ class Parameter(BaseNodeElement):
545
583
 
546
584
  return our_dict
547
585
 
586
+ def to_event(self, node: BaseNode) -> dict:
587
+ event_dict = self.to_dict()
588
+ event_data = super().to_event(node)
589
+ event_dict.update(event_data)
590
+ # Update for our name with the right values
591
+ name = event_dict.pop("name")
592
+ event_dict["parameter_name"] = name
593
+ # Update with value
594
+ if node is not None:
595
+ event_dict["value"] = node.get_parameter_value(self.name)
596
+ return event_dict
597
+
548
598
  @property
549
599
  def type(self) -> str:
550
600
  return self._custom_getter_for_property_type()