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.
Files changed (30) hide show
  1. griptape_nodes/bootstrap/utils/python_subprocess_executor.py +2 -2
  2. griptape_nodes/bootstrap/workflow_executors/local_session_workflow_executor.py +0 -5
  3. griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +9 -5
  4. griptape_nodes/bootstrap/workflow_executors/subprocess_workflow_executor.py +0 -1
  5. griptape_nodes/bootstrap/workflow_executors/workflow_executor.py +1 -3
  6. griptape_nodes/bootstrap/workflow_publishers/local_workflow_publisher.py +1 -1
  7. griptape_nodes/cli/commands/init.py +53 -7
  8. griptape_nodes/cli/shared.py +1 -0
  9. griptape_nodes/common/node_executor.py +216 -40
  10. griptape_nodes/exe_types/core_types.py +46 -0
  11. griptape_nodes/exe_types/node_types.py +272 -0
  12. griptape_nodes/machines/control_flow.py +222 -16
  13. griptape_nodes/machines/dag_builder.py +212 -1
  14. griptape_nodes/machines/parallel_resolution.py +237 -4
  15. griptape_nodes/node_library/workflow_registry.py +1 -1
  16. griptape_nodes/retained_mode/events/execution_events.py +5 -4
  17. griptape_nodes/retained_mode/events/flow_events.py +17 -67
  18. griptape_nodes/retained_mode/events/parameter_events.py +122 -1
  19. griptape_nodes/retained_mode/managers/event_manager.py +17 -13
  20. griptape_nodes/retained_mode/managers/flow_manager.py +316 -573
  21. griptape_nodes/retained_mode/managers/library_manager.py +32 -20
  22. griptape_nodes/retained_mode/managers/model_manager.py +19 -8
  23. griptape_nodes/retained_mode/managers/node_manager.py +463 -3
  24. griptape_nodes/retained_mode/managers/object_manager.py +2 -2
  25. griptape_nodes/retained_mode/managers/workflow_manager.py +37 -46
  26. griptape_nodes/retained_mode/retained_mode.py +297 -3
  27. {griptape_nodes-0.58.0.dist-info → griptape_nodes-0.59.0.dist-info}/METADATA +3 -2
  28. {griptape_nodes-0.58.0.dist-info → griptape_nodes-0.59.0.dist-info}/RECORD +30 -30
  29. {griptape_nodes-0.58.0.dist-info → griptape_nodes-0.59.0.dist-info}/WHEEL +1 -1
  30. {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,
@@ -76,7 +76,6 @@ class SubprocessWorkflowExecutor(LocalSessionWorkflowExecutor, PythonSubprocessE
76
76
 
77
77
  async def arun(
78
78
  self,
79
- workflow_name: str, # noqa: ARG002
80
79
  flow_input: Any,
81
80
  storage_backend: StorageBackend = StorageBackend.LOCAL,
82
81
  *,
@@ -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(workflow_name, flow_input, storage_backend, **kwargs))
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(workflow_name=workflow_name, flow_input={}, workflow_path=workflow_path)
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
- _handle_advanced_library_config(config)
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 _handle_advanced_library_config(config: InitConfig) -> bool | None:
222
- """Handle advanced library configuration step."""
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(register_advanced_library=register_advanced_library)
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 _build_libraries_list(*, register_advanced_library: bool) -> list[str]:
474
- """Builds the list of libraries to register based on the advanced library setting."""
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
 
@@ -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
- PackageNodeAsSerializedFlowRequest,
23
- PackageNodeAsSerializedFlowResultSuccess,
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.node_types import BaseNode
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
- output_parameter_prefix: str,
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
- output_parameter_prefix: Prefix for output parameters
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, output_parameter_prefix)
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, Path(workflow_result.file_path), result.file_name, result.output_parameter_prefix
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, published_workflow_filename, result.file_name, result.output_parameter_prefix
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 PackageNodeAsSerializedFlowRequest doesn't accept None types.
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
- request = PackageNodeAsSerializedFlowRequest(
236
- node_name=node.name,
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, PackageNodeAsSerializedFlowResultSuccess):
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, file_name=file_name, output_parameter_prefix=output_parameter_prefix
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, node: BaseNode, parameter_output_values: dict[str, Any], output_parameter_prefix: str
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'. That means that running the node failed, but was caught by the published flow.
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
- # We are grabbing all of the parameters on our end nodes that align with the node being published.
415
- if param_name.startswith(output_parameter_prefix):
416
- clean_param_name = param_name[len(output_parameter_prefix) :]
417
- # If the parameter exists on the node, then we need to set those values on the node.
418
- parameter = node.get_parameter_by_name(clean_param_name)
419
- # Don't set execution_environment, since that will be set to Local Execution on any published flow.
420
- if parameter is None:
421
- msg = (
422
- "Parameter '%s' from parameter output values not found on node '%s'",
423
- clean_param_name,
424
- node.name,
425
- )
426
- logger.error(msg)
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
- if parameter != node.execution_environment:
429
- if parameter.type != ParameterTypeBuiltin.CONTROL_TYPE:
430
- # If the node is control type, only set its value in parameter_output_values.
431
- node.set_parameter_value(clean_param_name, param_value)
432
- node.parameter_output_values[clean_param_name] = param_value
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
+ )