griptape-nodes 0.58.0__py3-none-any.whl → 0.59.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/bootstrap/utils/python_subprocess_executor.py +2 -2
- griptape_nodes/bootstrap/workflow_executors/local_session_workflow_executor.py +0 -5
- griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +9 -5
- griptape_nodes/bootstrap/workflow_executors/subprocess_workflow_executor.py +0 -1
- griptape_nodes/bootstrap/workflow_executors/workflow_executor.py +1 -3
- griptape_nodes/bootstrap/workflow_publishers/local_workflow_publisher.py +1 -1
- griptape_nodes/cli/commands/init.py +53 -7
- griptape_nodes/cli/shared.py +1 -0
- griptape_nodes/common/node_executor.py +216 -40
- griptape_nodes/exe_types/core_types.py +46 -0
- griptape_nodes/exe_types/node_types.py +272 -0
- griptape_nodes/machines/control_flow.py +222 -16
- griptape_nodes/machines/dag_builder.py +212 -1
- griptape_nodes/machines/parallel_resolution.py +237 -4
- griptape_nodes/node_library/workflow_registry.py +1 -1
- griptape_nodes/retained_mode/events/execution_events.py +5 -4
- griptape_nodes/retained_mode/events/flow_events.py +17 -67
- griptape_nodes/retained_mode/events/parameter_events.py +122 -1
- griptape_nodes/retained_mode/managers/event_manager.py +17 -13
- griptape_nodes/retained_mode/managers/flow_manager.py +316 -573
- griptape_nodes/retained_mode/managers/library_manager.py +32 -20
- griptape_nodes/retained_mode/managers/model_manager.py +19 -8
- griptape_nodes/retained_mode/managers/node_manager.py +463 -3
- griptape_nodes/retained_mode/managers/object_manager.py +2 -2
- griptape_nodes/retained_mode/managers/workflow_manager.py +37 -46
- griptape_nodes/retained_mode/retained_mode.py +297 -3
- {griptape_nodes-0.58.0.dist-info → griptape_nodes-0.59.0.dist-info}/METADATA +3 -2
- {griptape_nodes-0.58.0.dist-info → griptape_nodes-0.59.0.dist-info}/RECORD +30 -30
- {griptape_nodes-0.58.0.dist-info → griptape_nodes-0.59.0.dist-info}/WHEEL +1 -1
- {griptape_nodes-0.58.0.dist-info → griptape_nodes-0.59.0.dist-info}/entry_points.txt +0 -0
|
@@ -52,8 +52,8 @@ class PythonSubprocessExecutor:
|
|
|
52
52
|
|
|
53
53
|
stdout_bytes, stderr_bytes = await self._process.communicate()
|
|
54
54
|
returncode = self._process.returncode
|
|
55
|
-
stdout = stdout_bytes.decode() if stdout_bytes else ""
|
|
56
|
-
stderr = stderr_bytes.decode() if stderr_bytes else ""
|
|
55
|
+
stdout = stdout_bytes.decode(errors="replace") if stdout_bytes else ""
|
|
56
|
+
stderr = stderr_bytes.decode(errors="replace") if stderr_bytes else ""
|
|
57
57
|
|
|
58
58
|
# Log all output regardless of return code
|
|
59
59
|
if stdout:
|
|
@@ -109,7 +109,6 @@ class LocalSessionWorkflowExecutor(LocalWorkflowExecutor):
|
|
|
109
109
|
|
|
110
110
|
async def arun(
|
|
111
111
|
self,
|
|
112
|
-
workflow_name: str,
|
|
113
112
|
flow_input: Any,
|
|
114
113
|
storage_backend: StorageBackend | None = None,
|
|
115
114
|
**kwargs: Any,
|
|
@@ -120,7 +119,6 @@ class LocalSessionWorkflowExecutor(LocalWorkflowExecutor):
|
|
|
120
119
|
loading the user-defined workflow, and running the specified workflow.
|
|
121
120
|
|
|
122
121
|
Parameters:
|
|
123
|
-
workflow_name: The name of the workflow to execute.
|
|
124
122
|
flow_input: Input data for the flow, typically a dictionary.
|
|
125
123
|
storage_backend: The storage backend to use for the workflow execution.
|
|
126
124
|
|
|
@@ -129,7 +127,6 @@ class LocalSessionWorkflowExecutor(LocalWorkflowExecutor):
|
|
|
129
127
|
"""
|
|
130
128
|
try:
|
|
131
129
|
await self._arun(
|
|
132
|
-
workflow_name=workflow_name,
|
|
133
130
|
flow_input=flow_input,
|
|
134
131
|
storage_backend=storage_backend,
|
|
135
132
|
**kwargs,
|
|
@@ -151,14 +148,12 @@ class LocalSessionWorkflowExecutor(LocalWorkflowExecutor):
|
|
|
151
148
|
|
|
152
149
|
async def _arun( # noqa: C901, PLR0915
|
|
153
150
|
self,
|
|
154
|
-
workflow_name: str,
|
|
155
151
|
flow_input: Any,
|
|
156
152
|
storage_backend: StorageBackend | None = None,
|
|
157
153
|
**kwargs: Any,
|
|
158
154
|
) -> None:
|
|
159
155
|
"""Internal async run method with detailed event handling and websocket integration."""
|
|
160
156
|
flow_name = await self.aprepare_workflow_for_run(
|
|
161
|
-
workflow_name=workflow_name,
|
|
162
157
|
flow_input=flow_input,
|
|
163
158
|
storage_backend=storage_backend,
|
|
164
159
|
**kwargs,
|
|
@@ -52,6 +52,15 @@ class LocalWorkflowExecutor(WorkflowExecutor):
|
|
|
52
52
|
# TODO: Broadcast shutdown https://github.com/griptape-ai/griptape-nodes/issues/2149
|
|
53
53
|
return
|
|
54
54
|
|
|
55
|
+
def _get_workflow_name(self) -> str:
|
|
56
|
+
try:
|
|
57
|
+
context_manager = GriptapeNodes.ContextManager()
|
|
58
|
+
return context_manager.get_current_workflow_name()
|
|
59
|
+
except Exception as e:
|
|
60
|
+
msg = f"Failed to get current workflow from context manager: {e}"
|
|
61
|
+
logger.exception(msg)
|
|
62
|
+
raise LocalExecutorError(msg) from e
|
|
63
|
+
|
|
55
64
|
def _load_flow_for_workflow(self) -> str:
|
|
56
65
|
try:
|
|
57
66
|
context_manager = GriptapeNodes.ContextManager()
|
|
@@ -153,7 +162,6 @@ class LocalWorkflowExecutor(WorkflowExecutor):
|
|
|
153
162
|
|
|
154
163
|
async def aprepare_workflow_for_run(
|
|
155
164
|
self,
|
|
156
|
-
workflow_name: str,
|
|
157
165
|
flow_input: Any,
|
|
158
166
|
storage_backend: StorageBackend | None = None,
|
|
159
167
|
**kwargs: Any,
|
|
@@ -164,7 +172,6 @@ class LocalWorkflowExecutor(WorkflowExecutor):
|
|
|
164
172
|
initializing event listeners, registering libraries, loading the user-defined
|
|
165
173
|
workflow, and preparing the specified workflow for execution.
|
|
166
174
|
Parameters:
|
|
167
|
-
workflow_name: The name of the workflow to prepare.
|
|
168
175
|
flow_input: Input data for the flow, typically a dictionary.
|
|
169
176
|
storage_backend: The storage backend to use for the workflow execution.
|
|
170
177
|
|
|
@@ -175,7 +182,6 @@ class LocalWorkflowExecutor(WorkflowExecutor):
|
|
|
175
182
|
msg = "The storage_backend parameter is deprecated. Pass `storage_backend` to the constructor instead."
|
|
176
183
|
raise ValueError(msg)
|
|
177
184
|
|
|
178
|
-
logger.info("Executing workflow: %s", workflow_name)
|
|
179
185
|
GriptapeNodes.EventManager().initialize_queue()
|
|
180
186
|
|
|
181
187
|
# Load workflow from file if workflow_path is provided
|
|
@@ -192,7 +198,6 @@ class LocalWorkflowExecutor(WorkflowExecutor):
|
|
|
192
198
|
|
|
193
199
|
async def arun(
|
|
194
200
|
self,
|
|
195
|
-
workflow_name: str,
|
|
196
201
|
flow_input: Any,
|
|
197
202
|
storage_backend: StorageBackend | None = None,
|
|
198
203
|
**kwargs: Any,
|
|
@@ -211,7 +216,6 @@ class LocalWorkflowExecutor(WorkflowExecutor):
|
|
|
211
216
|
None
|
|
212
217
|
"""
|
|
213
218
|
flow_name = await self.aprepare_workflow_for_run(
|
|
214
|
-
workflow_name=workflow_name,
|
|
215
219
|
flow_input=flow_input,
|
|
216
220
|
storage_backend=storage_backend,
|
|
217
221
|
**kwargs,
|
|
@@ -28,17 +28,15 @@ class WorkflowExecutor:
|
|
|
28
28
|
|
|
29
29
|
def run(
|
|
30
30
|
self,
|
|
31
|
-
workflow_name: str,
|
|
32
31
|
flow_input: Any,
|
|
33
32
|
storage_backend: StorageBackend = StorageBackend.LOCAL,
|
|
34
33
|
**kwargs: Any,
|
|
35
34
|
) -> None:
|
|
36
|
-
return asyncio.run(self.arun(
|
|
35
|
+
return asyncio.run(self.arun(flow_input, storage_backend, **kwargs))
|
|
37
36
|
|
|
38
37
|
@abstractmethod
|
|
39
38
|
async def arun(
|
|
40
39
|
self,
|
|
41
|
-
workflow_name: str,
|
|
42
40
|
flow_input: Any,
|
|
43
41
|
storage_backend: StorageBackend = StorageBackend.LOCAL,
|
|
44
42
|
**kwargs: Any,
|
|
@@ -27,7 +27,7 @@ class LocalWorkflowPublisher(LocalWorkflowExecutor):
|
|
|
27
27
|
**kwargs: Any,
|
|
28
28
|
) -> None:
|
|
29
29
|
# Load the workflow into memory
|
|
30
|
-
await self.aprepare_workflow_for_run(
|
|
30
|
+
await self.aprepare_workflow_for_run(flow_input={}, workflow_path=workflow_path)
|
|
31
31
|
pickle_control_flow_result = kwargs.get("pickle_control_flow_result", False)
|
|
32
32
|
publish_workflow_request = PublishWorkflowRequest(
|
|
33
33
|
workflow_name=workflow_name,
|
|
@@ -47,6 +47,13 @@ def init_command( # noqa: PLR0913
|
|
|
47
47
|
help="Install the Griptape Nodes Advanced Image Library.",
|
|
48
48
|
),
|
|
49
49
|
] = None,
|
|
50
|
+
register_griptape_cloud_library: Annotated[
|
|
51
|
+
bool | None,
|
|
52
|
+
typer.Option(
|
|
53
|
+
"--register-griptape-cloud-library/--no-register-griptape-cloud-library",
|
|
54
|
+
help="Install the Griptape Cloud Library.",
|
|
55
|
+
),
|
|
56
|
+
] = None,
|
|
50
57
|
libraries_sync: Annotated[
|
|
51
58
|
bool | None,
|
|
52
59
|
typer.Option("--libraries-sync/--no-libraries-sync", help="Sync the Griptape Nodes libraries."),
|
|
@@ -83,6 +90,7 @@ def init_command( # noqa: PLR0913
|
|
|
83
90
|
api_key=api_key,
|
|
84
91
|
storage_backend=storage_backend,
|
|
85
92
|
register_advanced_library=register_advanced_library,
|
|
93
|
+
register_griptape_cloud_library=register_griptape_cloud_library,
|
|
86
94
|
config_values=config_values,
|
|
87
95
|
secret_values=secret_values,
|
|
88
96
|
libraries_sync=libraries_sync,
|
|
@@ -134,7 +142,7 @@ def _run_init_configuration(config: InitConfig) -> None:
|
|
|
134
142
|
_handle_storage_backend_config(config)
|
|
135
143
|
_handle_bucket_config(config)
|
|
136
144
|
_handle_hf_token_config(config)
|
|
137
|
-
|
|
145
|
+
_handle_additional_library_config(config)
|
|
138
146
|
_handle_arbitrary_configs(config)
|
|
139
147
|
|
|
140
148
|
|
|
@@ -218,17 +226,24 @@ def _handle_hf_token_config(config: InitConfig) -> str | None:
|
|
|
218
226
|
return hf_token
|
|
219
227
|
|
|
220
228
|
|
|
221
|
-
def
|
|
222
|
-
"""Handle
|
|
229
|
+
def _handle_additional_library_config(config: InitConfig) -> bool | None:
|
|
230
|
+
"""Handle additional library configuration step."""
|
|
223
231
|
register_advanced_library = config.register_advanced_library
|
|
232
|
+
register_griptape_cloud_library = config.register_griptape_cloud_library
|
|
224
233
|
|
|
225
234
|
if config.interactive:
|
|
226
235
|
register_advanced_library = _prompt_for_advanced_media_library(
|
|
227
236
|
default_prompt_for_advanced_media_library=register_advanced_library
|
|
228
237
|
)
|
|
238
|
+
register_griptape_cloud_library = _prompt_for_griptape_cloud_library(
|
|
239
|
+
default_prompt_for_griptape_cloud_library=register_griptape_cloud_library
|
|
240
|
+
)
|
|
229
241
|
|
|
230
|
-
if register_advanced_library is not None:
|
|
231
|
-
libraries_to_register = _build_libraries_list(
|
|
242
|
+
if register_advanced_library is not None or register_griptape_cloud_library is not None:
|
|
243
|
+
libraries_to_register = _build_libraries_list(
|
|
244
|
+
register_advanced_library=register_advanced_library,
|
|
245
|
+
register_griptape_cloud_library=register_griptape_cloud_library,
|
|
246
|
+
)
|
|
232
247
|
config_manager.set_config_value(
|
|
233
248
|
"app_events.on_app_initialization_complete.libraries_to_register", libraries_to_register
|
|
234
249
|
)
|
|
@@ -470,8 +485,25 @@ def _prompt_for_advanced_media_library(*, default_prompt_for_advanced_media_libr
|
|
|
470
485
|
return Confirm.ask("Register Advanced Media Library?", default=default_prompt_for_advanced_media_library)
|
|
471
486
|
|
|
472
487
|
|
|
473
|
-
def
|
|
474
|
-
"""
|
|
488
|
+
def _prompt_for_griptape_cloud_library(*, default_prompt_for_griptape_cloud_library: bool | None = None) -> bool:
|
|
489
|
+
"""Prompts the user whether to register the Griptape Cloud Library."""
|
|
490
|
+
if default_prompt_for_griptape_cloud_library is None:
|
|
491
|
+
default_prompt_for_griptape_cloud_library = False
|
|
492
|
+
explainer = """[bold cyan]Griptape Cloud Library[/bold cyan]
|
|
493
|
+
Would you like to install the Griptape Nodes Griptape Cloud Library?
|
|
494
|
+
This node library makes Griptape Cloud APIs and functionality available within Griptape Nodes.
|
|
495
|
+
For example, nodes are available for invoking Structures, Assistants, or even publishing a Workflow to Griptape Cloud.
|
|
496
|
+
The Griptape Nodes Griptape Cloud Library can be added later by following instructions here: [bold blue][link=https://docs.griptapenodes.com]https://docs.griptapenodes.com[/link][/bold blue].
|
|
497
|
+
"""
|
|
498
|
+
console.print(Panel(explainer, expand=False))
|
|
499
|
+
|
|
500
|
+
return Confirm.ask("Register Griptape Cloud Library?", default=default_prompt_for_griptape_cloud_library)
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def _build_libraries_list(
|
|
504
|
+
*, register_advanced_library: bool | None = False, register_griptape_cloud_library: bool | None = False
|
|
505
|
+
) -> list[str]:
|
|
506
|
+
"""Builds the list of libraries to register based on library settings."""
|
|
475
507
|
# TODO: https://github.com/griptape-ai/griptape-nodes/issues/929
|
|
476
508
|
libraries_key = "app_events.on_app_initialization_complete.libraries_to_register"
|
|
477
509
|
library_base_dir = Path(ENV_LIBRARIES_BASE_DIR)
|
|
@@ -509,6 +541,20 @@ def _build_libraries_list(*, register_advanced_library: bool) -> list[str]:
|
|
|
509
541
|
for lib in libraries_to_remove:
|
|
510
542
|
new_libraries.remove(lib)
|
|
511
543
|
|
|
544
|
+
griptape_cloud_library = str(library_base_dir / "griptape_cloud/griptape_nodes_library.json")
|
|
545
|
+
griptape_cloud_identifier = _get_library_identifier(griptape_cloud_library)
|
|
546
|
+
if register_griptape_cloud_library:
|
|
547
|
+
# If the griptape cloud library is not registered, add it
|
|
548
|
+
if griptape_cloud_identifier not in current_identifiers:
|
|
549
|
+
new_libraries.append(griptape_cloud_library)
|
|
550
|
+
else:
|
|
551
|
+
# If the griptape cloud library is registered, remove it
|
|
552
|
+
libraries_to_remove = [
|
|
553
|
+
lib for lib in new_libraries if _get_library_identifier(lib) == griptape_cloud_identifier
|
|
554
|
+
]
|
|
555
|
+
for lib in libraries_to_remove:
|
|
556
|
+
new_libraries.remove(lib)
|
|
557
|
+
|
|
512
558
|
return new_libraries
|
|
513
559
|
|
|
514
560
|
|
griptape_nodes/cli/shared.py
CHANGED
|
@@ -18,6 +18,7 @@ class InitConfig:
|
|
|
18
18
|
api_key: str | None = None
|
|
19
19
|
storage_backend: str | None = None
|
|
20
20
|
register_advanced_library: bool | None = None
|
|
21
|
+
register_griptape_cloud_library: bool | None = None
|
|
21
22
|
config_values: dict[str, Any] | None = None
|
|
22
23
|
secret_values: dict[str, str] | None = None
|
|
23
24
|
libraries_sync: bool | None = None
|
|
@@ -13,14 +13,17 @@ from griptape_nodes.exe_types.node_types import (
|
|
|
13
13
|
CONTROL_INPUT_PARAMETER,
|
|
14
14
|
LOCAL_EXECUTION,
|
|
15
15
|
PRIVATE_EXECUTION,
|
|
16
|
+
BaseNode,
|
|
16
17
|
EndNode,
|
|
18
|
+
NodeGroup,
|
|
19
|
+
NodeGroupProxyNode,
|
|
17
20
|
StartNode,
|
|
18
21
|
)
|
|
19
22
|
from griptape_nodes.node_library.library_registry import Library, LibraryRegistry
|
|
20
23
|
from griptape_nodes.node_library.workflow_registry import WorkflowRegistry
|
|
21
24
|
from griptape_nodes.retained_mode.events.flow_events import (
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
PackageNodesAsSerializedFlowRequest,
|
|
26
|
+
PackageNodesAsSerializedFlowResultSuccess,
|
|
24
27
|
)
|
|
25
28
|
from griptape_nodes.retained_mode.events.workflow_events import (
|
|
26
29
|
DeleteWorkflowRequest,
|
|
@@ -34,7 +37,7 @@ from griptape_nodes.retained_mode.events.workflow_events import (
|
|
|
34
37
|
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
35
38
|
|
|
36
39
|
if TYPE_CHECKING:
|
|
37
|
-
from griptape_nodes.exe_types.
|
|
40
|
+
from griptape_nodes.exe_types.connections import Connections
|
|
38
41
|
from griptape_nodes.retained_mode.events.node_events import SerializedNodeCommands
|
|
39
42
|
from griptape_nodes.retained_mode.managers.library_manager import LibraryManager
|
|
40
43
|
|
|
@@ -47,6 +50,7 @@ class PublishLocalWorkflowResult(NamedTuple):
|
|
|
47
50
|
workflow_result: SaveWorkflowFileFromSerializedFlowResultSuccess
|
|
48
51
|
file_name: str
|
|
49
52
|
output_parameter_prefix: str
|
|
53
|
+
package_result: PackageNodesAsSerializedFlowResultSuccess
|
|
50
54
|
|
|
51
55
|
|
|
52
56
|
class NodeExecutor:
|
|
@@ -69,6 +73,7 @@ class NodeExecutor:
|
|
|
69
73
|
library_name: The library that the execute method should come from.
|
|
70
74
|
"""
|
|
71
75
|
execution_type = node.get_parameter_value(node.execution_environment.name)
|
|
76
|
+
|
|
72
77
|
if execution_type == LOCAL_EXECUTION:
|
|
73
78
|
await node.aprocess()
|
|
74
79
|
elif execution_type == PRIVATE_EXECUTION:
|
|
@@ -81,7 +86,7 @@ class NodeExecutor:
|
|
|
81
86
|
node: BaseNode,
|
|
82
87
|
workflow_path: Path,
|
|
83
88
|
file_name: str,
|
|
84
|
-
|
|
89
|
+
package_result: PackageNodesAsSerializedFlowResultSuccess,
|
|
85
90
|
) -> None:
|
|
86
91
|
"""Execute workflow in subprocess and apply results to node.
|
|
87
92
|
|
|
@@ -89,11 +94,11 @@ class NodeExecutor:
|
|
|
89
94
|
node: The node to apply results to
|
|
90
95
|
workflow_path: Path to workflow file to execute
|
|
91
96
|
file_name: Name of workflow for logging
|
|
92
|
-
|
|
97
|
+
package_result: The packaging result containing parameter mappings
|
|
93
98
|
"""
|
|
94
99
|
my_subprocess_result = await self._execute_subprocess(workflow_path, file_name)
|
|
95
100
|
parameter_output_values = self._extract_parameter_output_values(my_subprocess_result)
|
|
96
|
-
self._apply_parameter_values_to_node(node, parameter_output_values,
|
|
101
|
+
self._apply_parameter_values_to_node(node, parameter_output_values, package_result)
|
|
97
102
|
|
|
98
103
|
async def _execute_private_workflow(self, node: BaseNode) -> None:
|
|
99
104
|
"""Execute node in private subprocess environment.
|
|
@@ -116,7 +121,10 @@ class NodeExecutor:
|
|
|
116
121
|
|
|
117
122
|
try:
|
|
118
123
|
await self._execute_and_apply_workflow(
|
|
119
|
-
node,
|
|
124
|
+
node=node,
|
|
125
|
+
workflow_path=Path(workflow_result.file_path),
|
|
126
|
+
file_name=result.file_name,
|
|
127
|
+
package_result=result.package_result,
|
|
120
128
|
)
|
|
121
129
|
except RuntimeError:
|
|
122
130
|
raise
|
|
@@ -188,7 +196,10 @@ class NodeExecutor:
|
|
|
188
196
|
|
|
189
197
|
try:
|
|
190
198
|
await self._execute_and_apply_workflow(
|
|
191
|
-
node,
|
|
199
|
+
node,
|
|
200
|
+
published_workflow_filename,
|
|
201
|
+
result.file_name,
|
|
202
|
+
result.package_result,
|
|
192
203
|
)
|
|
193
204
|
except RuntimeError:
|
|
194
205
|
raise
|
|
@@ -220,7 +231,7 @@ class NodeExecutor:
|
|
|
220
231
|
"""
|
|
221
232
|
sanitized_node_name = node.name.replace(" ", "_")
|
|
222
233
|
output_parameter_prefix = f"{sanitized_node_name}_packaged_node_"
|
|
223
|
-
# We have to make our defaults strings because the
|
|
234
|
+
# We have to make our defaults strings because the PackageNodesAsSerializedFlowRequest doesn't accept None types.
|
|
224
235
|
library_name = "Griptape Nodes Library"
|
|
225
236
|
start_node_type = "StartFlow"
|
|
226
237
|
end_node_type = "EndFlow"
|
|
@@ -232,19 +243,28 @@ class NodeExecutor:
|
|
|
232
243
|
end_node_type = end_nodes[0]
|
|
233
244
|
library_name = library.get_library_data().name
|
|
234
245
|
sanitized_library_name = library_name.replace(" ", "_")
|
|
235
|
-
|
|
236
|
-
|
|
246
|
+
# If we are packaging a NodeGroupProxyNode, that means that we are packaging multiple nodes together, so we have to get the list of nodes from the proxy node.
|
|
247
|
+
if isinstance(node, NodeGroupProxyNode):
|
|
248
|
+
node_names = list(node.node_group_data.nodes.keys())
|
|
249
|
+
else:
|
|
250
|
+
# Otherwise, it's a list of one node!
|
|
251
|
+
node_names = [node.name]
|
|
252
|
+
|
|
253
|
+
# Pass the proxy node if this is a NodeGroupProxyNode so serialization can use stored connections
|
|
254
|
+
proxy_node_for_packaging = node if isinstance(node, NodeGroupProxyNode) else None
|
|
255
|
+
|
|
256
|
+
request = PackageNodesAsSerializedFlowRequest(
|
|
257
|
+
node_names=node_names,
|
|
237
258
|
start_node_type=start_node_type,
|
|
238
259
|
end_node_type=end_node_type,
|
|
239
260
|
start_end_specific_library_name=library_name,
|
|
240
|
-
entry_control_parameter_name=node._entry_control_parameter.name
|
|
241
|
-
if node._entry_control_parameter is not None
|
|
242
|
-
else None,
|
|
243
261
|
output_parameter_prefix=output_parameter_prefix,
|
|
262
|
+
entry_control_node_name=None,
|
|
263
|
+
entry_control_parameter_name=None,
|
|
264
|
+
proxy_node=proxy_node_for_packaging,
|
|
244
265
|
)
|
|
245
|
-
|
|
246
266
|
package_result = GriptapeNodes.handle_request(request)
|
|
247
|
-
if not isinstance(package_result,
|
|
267
|
+
if not isinstance(package_result, PackageNodesAsSerializedFlowResultSuccess):
|
|
248
268
|
msg = f"Failed to package node '{node.name}'. Error: {package_result.result_details}"
|
|
249
269
|
raise RuntimeError(msg) # noqa: TRY004
|
|
250
270
|
|
|
@@ -262,7 +282,10 @@ class NodeExecutor:
|
|
|
262
282
|
raise RuntimeError(msg) # noqa: TRY004
|
|
263
283
|
|
|
264
284
|
return PublishLocalWorkflowResult(
|
|
265
|
-
workflow_result=workflow_result,
|
|
285
|
+
workflow_result=workflow_result,
|
|
286
|
+
file_name=file_name,
|
|
287
|
+
output_parameter_prefix=output_parameter_prefix,
|
|
288
|
+
package_result=package_result,
|
|
266
289
|
)
|
|
267
290
|
|
|
268
291
|
async def _publish_library_workflow(
|
|
@@ -311,7 +334,6 @@ class NodeExecutor:
|
|
|
311
334
|
try:
|
|
312
335
|
async with subprocess_executor as executor:
|
|
313
336
|
await executor.arun(
|
|
314
|
-
workflow_name=file_name,
|
|
315
337
|
flow_input={},
|
|
316
338
|
storage_backend=await self._get_storage_backend(),
|
|
317
339
|
pickle_control_flow_result=pickle_control_flow_result,
|
|
@@ -398,38 +420,78 @@ class NodeExecutor:
|
|
|
398
420
|
return stored_value
|
|
399
421
|
return stored_value
|
|
400
422
|
|
|
401
|
-
def _apply_parameter_values_to_node(
|
|
402
|
-
self,
|
|
423
|
+
def _apply_parameter_values_to_node( # noqa: C901
|
|
424
|
+
self,
|
|
425
|
+
node: BaseNode,
|
|
426
|
+
parameter_output_values: dict[str, Any],
|
|
427
|
+
package_result: PackageNodesAsSerializedFlowResultSuccess,
|
|
403
428
|
) -> None:
|
|
404
429
|
"""Apply deserialized parameter values back to the node.
|
|
405
430
|
|
|
406
431
|
Sets parameter values on the node and updates parameter_output_values dictionary.
|
|
432
|
+
Uses parameter_name_mappings from package_result to map packaged parameters back to original nodes.
|
|
433
|
+
Works for both single-node and multi-node packages.
|
|
407
434
|
"""
|
|
408
|
-
# If the packaged flow fails, the End Flow Node in the library published workflow will have entered from 'failed'
|
|
409
|
-
# In this case, we should fail the node, since it didn't complete properly.
|
|
435
|
+
# If the packaged flow fails, the End Flow Node in the library published workflow will have entered from 'failed'
|
|
410
436
|
if "failed" in parameter_output_values and parameter_output_values["failed"] == CONTROL_INPUT_PARAMETER:
|
|
411
437
|
msg = f"Failed to execute node: {node.name}, with exception: {parameter_output_values.get('result_details', 'No result details were returned.')}"
|
|
412
438
|
raise RuntimeError(msg)
|
|
439
|
+
|
|
440
|
+
# Use parameter mappings to apply values back to original nodes
|
|
441
|
+
parameter_name_mappings = package_result.parameter_name_mappings
|
|
413
442
|
for param_name, param_value in parameter_output_values.items():
|
|
414
|
-
#
|
|
415
|
-
if param_name
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
443
|
+
# Check if this parameter has a mapping back to an original node parameter
|
|
444
|
+
if param_name not in parameter_name_mappings:
|
|
445
|
+
continue
|
|
446
|
+
|
|
447
|
+
original_node_param = parameter_name_mappings[param_name]
|
|
448
|
+
target_node_name = original_node_param.node_name
|
|
449
|
+
target_param_name = original_node_param.parameter_name
|
|
450
|
+
|
|
451
|
+
# For multi-node packages, get the target node from the group
|
|
452
|
+
# For single-node packages, use the node itself
|
|
453
|
+
if isinstance(node, NodeGroupProxyNode):
|
|
454
|
+
if target_node_name not in node.node_group_data.nodes:
|
|
455
|
+
msg = f"Target node '{target_node_name}' not found in node group for proxy node '{node.name}'. Available nodes: {list(node.node_group_data.nodes.keys())}"
|
|
427
456
|
raise RuntimeError(msg)
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
457
|
+
target_node = node.node_group_data.nodes[target_node_name]
|
|
458
|
+
else:
|
|
459
|
+
target_node = node
|
|
460
|
+
|
|
461
|
+
# Get the parameter from the target node
|
|
462
|
+
target_param = target_node.get_parameter_by_name(target_param_name)
|
|
463
|
+
|
|
464
|
+
# Skip if parameter not found or is special parameter (execution_environment, node_group)
|
|
465
|
+
if target_param is None or target_param in (
|
|
466
|
+
target_node.execution_environment,
|
|
467
|
+
target_node.node_group,
|
|
468
|
+
):
|
|
469
|
+
logger.debug(
|
|
470
|
+
"Skipping special or missing parameter '%s' on node '%s'", target_param_name, target_node_name
|
|
471
|
+
)
|
|
472
|
+
continue
|
|
473
|
+
|
|
474
|
+
# Set the value on the target node
|
|
475
|
+
if target_param.type != ParameterTypeBuiltin.CONTROL_TYPE:
|
|
476
|
+
target_node.set_parameter_value(target_param_name, param_value)
|
|
477
|
+
target_node.parameter_output_values[target_param_name] = param_value
|
|
478
|
+
|
|
479
|
+
# For multi-node packages, also set the value on the proxy node's corresponding output parameter
|
|
480
|
+
if isinstance(node, NodeGroupProxyNode):
|
|
481
|
+
sanitized_node_name = target_node_name.replace(" ", "_")
|
|
482
|
+
proxy_param_name = f"{sanitized_node_name}__{target_param_name}"
|
|
483
|
+
proxy_param = node.get_parameter_by_name(proxy_param_name)
|
|
484
|
+
if proxy_param is not None:
|
|
485
|
+
if target_param.type != ParameterTypeBuiltin.CONTROL_TYPE:
|
|
486
|
+
node.set_parameter_value(proxy_param_name, param_value)
|
|
487
|
+
node.parameter_output_values[proxy_param_name] = param_value
|
|
488
|
+
|
|
489
|
+
logger.debug(
|
|
490
|
+
"Set parameter '%s' on node '%s' to value: %s",
|
|
491
|
+
target_param_name,
|
|
492
|
+
target_node_name,
|
|
493
|
+
param_value,
|
|
494
|
+
)
|
|
433
495
|
|
|
434
496
|
async def _delete_workflow(self, workflow_name: str, workflow_path: Path) -> None:
|
|
435
497
|
try:
|
|
@@ -464,3 +526,117 @@ class NodeExecutor:
|
|
|
464
526
|
except ValueError:
|
|
465
527
|
storage_backend = StorageBackend.LOCAL
|
|
466
528
|
return storage_backend
|
|
529
|
+
|
|
530
|
+
def _toggle_directional_control_connections(
|
|
531
|
+
self,
|
|
532
|
+
proxy_node: BaseNode,
|
|
533
|
+
node_group: NodeGroup,
|
|
534
|
+
connections: Connections,
|
|
535
|
+
*,
|
|
536
|
+
restore_to_original: bool,
|
|
537
|
+
is_incoming: bool,
|
|
538
|
+
) -> None:
|
|
539
|
+
"""Toggle control connections between proxy and original nodes for a specific direction.
|
|
540
|
+
|
|
541
|
+
When a NodeGroupProxyNode is created, control connections from/to the original nodes are
|
|
542
|
+
redirected to/from the proxy node. Before packaging the flow for execution, we need to
|
|
543
|
+
temporarily restore these connections back to the original nodes so the packaged flow
|
|
544
|
+
has the correct control flow structure. After packaging, we toggle them back to the proxy.
|
|
545
|
+
|
|
546
|
+
Args:
|
|
547
|
+
proxy_node: The proxy node containing the node group
|
|
548
|
+
node_group: The node group data containing original nodes and connection mappings
|
|
549
|
+
connections: The connections manager that tracks all connections via indexes
|
|
550
|
+
restore_to_original: If True, restore connections to original nodes (for packaging);
|
|
551
|
+
if False, remap connections to proxy (after packaging)
|
|
552
|
+
is_incoming: If True, handle incoming connections (target_node/target_parameter);
|
|
553
|
+
if False, handle outgoing connections (source_node/source_parameter)
|
|
554
|
+
"""
|
|
555
|
+
# Select the appropriate connection list, mapping, and index based on direction
|
|
556
|
+
if is_incoming:
|
|
557
|
+
# Incoming: connections pointing TO nodes in this group
|
|
558
|
+
connection_list = node_group.external_incoming_connections
|
|
559
|
+
original_nodes_map = node_group.original_incoming_targets
|
|
560
|
+
index = connections.incoming_index
|
|
561
|
+
else:
|
|
562
|
+
# Outgoing: connections originating FROM nodes in this group
|
|
563
|
+
connection_list = node_group.external_outgoing_connections
|
|
564
|
+
original_nodes_map = node_group.original_outgoing_sources
|
|
565
|
+
index = connections.outgoing_index
|
|
566
|
+
|
|
567
|
+
for conn in connection_list:
|
|
568
|
+
# Get the parameter based on connection direction (target for incoming, source for outgoing)
|
|
569
|
+
parameter = conn.target_parameter if is_incoming else conn.source_parameter
|
|
570
|
+
|
|
571
|
+
# Only toggle control flow connections, skip data connections
|
|
572
|
+
if parameter.type != ParameterTypeBuiltin.CONTROL_TYPE:
|
|
573
|
+
continue
|
|
574
|
+
|
|
575
|
+
conn_id = id(conn)
|
|
576
|
+
original_node = original_nodes_map.get(conn_id)
|
|
577
|
+
|
|
578
|
+
# Validate we have the original node mapping
|
|
579
|
+
# Incoming connections must have originals (error if missing)
|
|
580
|
+
# Outgoing connections may not have originals in some cases (skip if missing)
|
|
581
|
+
if original_node is None:
|
|
582
|
+
if is_incoming:
|
|
583
|
+
msg = f"No original target found for connection {conn_id} in node group '{node_group.group_id}'"
|
|
584
|
+
raise RuntimeError(msg)
|
|
585
|
+
continue
|
|
586
|
+
|
|
587
|
+
# Build the proxy parameter name: {sanitized_node_name}__{parameter_name}
|
|
588
|
+
# Example: "My Node" with param "enter" -> "My_Node__enter"
|
|
589
|
+
sanitized_node_name = original_node.name.replace(" ", "_")
|
|
590
|
+
proxy_param_name = f"{sanitized_node_name}__{parameter.name}"
|
|
591
|
+
|
|
592
|
+
# Determine the direction of the toggle
|
|
593
|
+
if restore_to_original:
|
|
594
|
+
# Restore: proxy -> original (for packaging)
|
|
595
|
+
# Before: External -> Proxy -> (internal nodes)
|
|
596
|
+
# After: External -> Original node in group
|
|
597
|
+
from_node = proxy_node
|
|
598
|
+
from_param = proxy_param_name
|
|
599
|
+
to_node = original_node
|
|
600
|
+
to_param = parameter.name
|
|
601
|
+
else:
|
|
602
|
+
# Remap: original -> proxy (after packaging)
|
|
603
|
+
# Before: External -> Original node in group
|
|
604
|
+
# After: External -> Proxy -> (internal nodes)
|
|
605
|
+
from_node = original_node
|
|
606
|
+
from_param = parameter.name
|
|
607
|
+
to_node = proxy_node
|
|
608
|
+
to_param = proxy_param_name
|
|
609
|
+
|
|
610
|
+
# Step 1: Remove connection reference from the old node's index
|
|
611
|
+
if from_node.name in index and from_param in index[from_node.name]:
|
|
612
|
+
index[from_node.name][from_param].remove(conn_id)
|
|
613
|
+
|
|
614
|
+
# Step 2: Update the connection object to point to the new node
|
|
615
|
+
if is_incoming:
|
|
616
|
+
conn.target_node = to_node
|
|
617
|
+
else:
|
|
618
|
+
conn.source_node = to_node
|
|
619
|
+
|
|
620
|
+
# Step 3: Add connection reference to the new node's index
|
|
621
|
+
index.setdefault(to_node.name, {}).setdefault(to_param, []).append(conn_id)
|
|
622
|
+
|
|
623
|
+
def _toggle_control_connections(self, proxy_node: BaseNode, *, restore_to_original: bool) -> None:
|
|
624
|
+
"""Toggle control connections between proxy node and original nodes.
|
|
625
|
+
|
|
626
|
+
Args:
|
|
627
|
+
proxy_node: The proxy node containing the node group
|
|
628
|
+
restore_to_original: If True, restore connections from proxy to original nodes.
|
|
629
|
+
If False, remap connections from original nodes back to proxy.
|
|
630
|
+
"""
|
|
631
|
+
if not isinstance(proxy_node, NodeGroupProxyNode):
|
|
632
|
+
return
|
|
633
|
+
node_group = proxy_node.node_group_data
|
|
634
|
+
connections = GriptapeNodes.FlowManager().get_connections()
|
|
635
|
+
|
|
636
|
+
# Toggle both incoming and outgoing connections
|
|
637
|
+
self._toggle_directional_control_connections(
|
|
638
|
+
proxy_node, node_group, connections, restore_to_original=restore_to_original, is_incoming=True
|
|
639
|
+
)
|
|
640
|
+
self._toggle_directional_control_connections(
|
|
641
|
+
proxy_node, node_group, connections, restore_to_original=restore_to_original, is_incoming=False
|
|
642
|
+
)
|