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.
- griptape_nodes/__init__.py +259 -92
- griptape_nodes/app/__init__.py +6 -1
- griptape_nodes/app/app_websocket.py +481 -0
- griptape_nodes/bootstrap/bootstrap_script.py +12 -231
- griptape_nodes/bootstrap/workflow_runners/__init__.py +1 -0
- griptape_nodes/bootstrap/workflow_runners/bootstrap_workflow_runner.py +28 -0
- griptape_nodes/bootstrap/workflow_runners/local_workflow_runner.py +237 -0
- griptape_nodes/bootstrap/workflow_runners/subprocess_workflow_runner.py +58 -0
- griptape_nodes/bootstrap/workflow_runners/workflow_runner.py +11 -0
- griptape_nodes/exe_types/core_types.py +50 -0
- griptape_nodes/machines/node_resolution.py +3 -3
- griptape_nodes/node_library/library_registry.py +18 -7
- griptape_nodes/retained_mode/events/node_events.py +2 -2
- griptape_nodes/retained_mode/events/parameter_events.py +4 -49
- griptape_nodes/retained_mode/managers/flow_manager.py +5 -5
- griptape_nodes/retained_mode/managers/node_manager.py +192 -21
- griptape_nodes/retained_mode/managers/object_manager.py +2 -0
- griptape_nodes/retained_mode/managers/workflow_manager.py +14 -4
- {griptape_nodes-0.35.0.dist-info → griptape_nodes-0.36.0.dist-info}/METADATA +1 -1
- {griptape_nodes-0.35.0.dist-info → griptape_nodes-0.36.0.dist-info}/RECORD +23 -17
- {griptape_nodes-0.35.0.dist-info → griptape_nodes-0.36.0.dist-info}/WHEEL +0 -0
- {griptape_nodes-0.35.0.dist-info → griptape_nodes-0.36.0.dist-info}/entry_points.txt +0 -0
- {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.
|
|
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
|
-
|
|
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()
|