griptape-nodes 0.57.1__py3-none-any.whl → 0.58.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 (51) hide show
  1. griptape_nodes/api_client/__init__.py +9 -0
  2. griptape_nodes/api_client/client.py +279 -0
  3. griptape_nodes/api_client/request_client.py +273 -0
  4. griptape_nodes/app/app.py +57 -150
  5. griptape_nodes/bootstrap/utils/python_subprocess_executor.py +1 -1
  6. griptape_nodes/bootstrap/workflow_executors/local_session_workflow_executor.py +22 -50
  7. griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +6 -1
  8. griptape_nodes/bootstrap/workflow_executors/subprocess_workflow_executor.py +27 -46
  9. griptape_nodes/bootstrap/workflow_executors/utils/subprocess_script.py +7 -0
  10. griptape_nodes/bootstrap/workflow_publishers/local_workflow_publisher.py +3 -1
  11. griptape_nodes/bootstrap/workflow_publishers/subprocess_workflow_publisher.py +3 -1
  12. griptape_nodes/bootstrap/workflow_publishers/utils/subprocess_script.py +16 -1
  13. griptape_nodes/common/node_executor.py +466 -0
  14. griptape_nodes/drivers/storage/base_storage_driver.py +0 -11
  15. griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +7 -25
  16. griptape_nodes/drivers/storage/local_storage_driver.py +2 -2
  17. griptape_nodes/exe_types/connections.py +37 -9
  18. griptape_nodes/exe_types/core_types.py +1 -1
  19. griptape_nodes/exe_types/node_types.py +115 -22
  20. griptape_nodes/machines/control_flow.py +48 -7
  21. griptape_nodes/machines/parallel_resolution.py +98 -29
  22. griptape_nodes/machines/sequential_resolution.py +61 -22
  23. griptape_nodes/node_library/library_registry.py +24 -1
  24. griptape_nodes/node_library/workflow_registry.py +38 -2
  25. griptape_nodes/retained_mode/events/execution_events.py +8 -1
  26. griptape_nodes/retained_mode/events/flow_events.py +90 -3
  27. griptape_nodes/retained_mode/events/node_events.py +17 -10
  28. griptape_nodes/retained_mode/events/workflow_events.py +5 -0
  29. griptape_nodes/retained_mode/griptape_nodes.py +16 -219
  30. griptape_nodes/retained_mode/managers/config_manager.py +0 -46
  31. griptape_nodes/retained_mode/managers/engine_identity_manager.py +225 -74
  32. griptape_nodes/retained_mode/managers/flow_manager.py +1276 -230
  33. griptape_nodes/retained_mode/managers/library_manager.py +7 -8
  34. griptape_nodes/retained_mode/managers/node_manager.py +197 -9
  35. griptape_nodes/retained_mode/managers/secrets_manager.py +26 -0
  36. griptape_nodes/retained_mode/managers/session_manager.py +264 -227
  37. griptape_nodes/retained_mode/managers/settings.py +4 -38
  38. griptape_nodes/retained_mode/managers/static_files_manager.py +3 -3
  39. griptape_nodes/retained_mode/managers/version_compatibility_manager.py +135 -6
  40. griptape_nodes/retained_mode/managers/workflow_manager.py +206 -78
  41. griptape_nodes/servers/mcp.py +23 -15
  42. griptape_nodes/utils/async_utils.py +36 -0
  43. griptape_nodes/utils/dict_utils.py +8 -2
  44. griptape_nodes/version_compatibility/versions/v0_39_0/modified_parameters_set_removal.py +11 -6
  45. griptape_nodes/version_compatibility/workflow_versions/v0_7_0/local_executor_argument_addition.py +12 -5
  46. {griptape_nodes-0.57.1.dist-info → griptape_nodes-0.58.0.dist-info}/METADATA +4 -3
  47. {griptape_nodes-0.57.1.dist-info → griptape_nodes-0.58.0.dist-info}/RECORD +49 -47
  48. {griptape_nodes-0.57.1.dist-info → griptape_nodes-0.58.0.dist-info}/WHEEL +1 -1
  49. griptape_nodes/retained_mode/utils/engine_identity.py +0 -245
  50. griptape_nodes/servers/ws_request_manager.py +0 -268
  51. {griptape_nodes-0.57.1.dist-info → griptape_nodes-0.58.0.dist-info}/entry_points.txt +0 -0
@@ -6,6 +6,7 @@ from queue import Queue
6
6
  from typing import TYPE_CHECKING, Any, NamedTuple, cast
7
7
  from uuid import uuid4
8
8
 
9
+ from griptape_nodes.common.node_executor import NodeExecutor
9
10
  from griptape_nodes.exe_types.connections import Connections
10
11
  from griptape_nodes.exe_types.core_types import (
11
12
  Parameter,
@@ -29,6 +30,7 @@ from griptape_nodes.machines.dag_builder import DagBuilder
29
30
  from griptape_nodes.machines.parallel_resolution import ParallelResolutionMachine
30
31
  from griptape_nodes.machines.sequential_resolution import SequentialResolutionMachine
31
32
  from griptape_nodes.node_library.library_registry import LibraryNameAndVersion, LibraryRegistry
33
+ from griptape_nodes.node_library.workflow_registry import LibraryNameAndNodeType
32
34
  from griptape_nodes.retained_mode.events.base_events import (
33
35
  ExecutionEvent,
34
36
  ExecutionGriptapeNodeEvent,
@@ -103,9 +105,14 @@ from griptape_nodes.retained_mode.events.flow_events import (
103
105
  ListNodesInFlowRequest,
104
106
  ListNodesInFlowResultFailure,
105
107
  ListNodesInFlowResultSuccess,
108
+ OriginalNodeParameter,
106
109
  PackageNodeAsSerializedFlowRequest,
107
110
  PackageNodeAsSerializedFlowResultFailure,
108
111
  PackageNodeAsSerializedFlowResultSuccess,
112
+ PackageNodesAsSerializedFlowRequest,
113
+ PackageNodesAsSerializedFlowResultFailure,
114
+ PackageNodesAsSerializedFlowResultSuccess,
115
+ SanitizedParameterName,
109
116
  SerializedFlowCommands,
110
117
  SerializeFlowToCommandsRequest,
111
118
  SerializeFlowToCommandsResultFailure,
@@ -177,11 +184,20 @@ class PackageNodeInfo(NamedTuple):
177
184
  package_flow_name: str
178
185
 
179
186
 
187
+ class StartNodeIncomingDataResult(NamedTuple):
188
+ """Result of processing incoming data connections for a start node."""
189
+
190
+ parameter_commands: list[AddParameterToNodeRequest]
191
+ data_connections: list[SerializedFlowCommands.IndirectConnectionSerialization]
192
+ input_shape_data: WorkflowShapeNodes
193
+ parameter_value_commands: list[SerializedNodeCommands.IndirectSetParameterValueCommand]
194
+
195
+
180
196
  class PackagingStartNodeResult(NamedTuple):
181
- """Result of creating start node commands and data connections for flow packaging."""
197
+ """Result of creating start node commands and connections for flow packaging."""
182
198
 
183
199
  start_node_commands: SerializedNodeCommands
184
- start_to_package_data_connections: list[SerializedFlowCommands.IndirectConnectionSerialization]
200
+ start_to_package_connections: list[SerializedFlowCommands.IndirectConnectionSerialization]
185
201
  input_shape_data: WorkflowShapeNodes
186
202
  start_node_parameter_value_commands: list[SerializedNodeCommands.IndirectSetParameterValueCommand]
187
203
 
@@ -190,10 +206,18 @@ class PackagingEndNodeResult(NamedTuple):
190
206
  """Result of creating end node commands and data connections for flow packaging."""
191
207
 
192
208
  end_node_commands: SerializedNodeCommands
193
- package_to_end_data_connections: list[SerializedFlowCommands.IndirectConnectionSerialization]
209
+ package_to_end_connections: list[SerializedFlowCommands.IndirectConnectionSerialization]
194
210
  output_shape_data: WorkflowShapeNodes
195
211
 
196
212
 
213
+ class MultiNodeEndNodeResult(NamedTuple):
214
+ """Result of creating end node commands and parameter mappings for multi-node packaging."""
215
+
216
+ packaging_result: PackagingEndNodeResult
217
+ parameter_name_mappings: dict[SanitizedParameterName, OriginalNodeParameter]
218
+ alter_parameter_commands: list[AlterParameterDetailsRequest]
219
+
220
+
197
221
  class FlowManager:
198
222
  _name_to_parent_name: dict[str, str | None]
199
223
  _flow_to_referenced_workflow_name: dict[ControlFlow, str]
@@ -204,6 +228,7 @@ class FlowManager:
204
228
  _global_control_flow_machine: ControlFlowMachine | None
205
229
  _global_single_node_resolution: bool
206
230
  _global_dag_builder: DagBuilder
231
+ _node_executor: NodeExecutor
207
232
 
208
233
  def __init__(self, event_manager: EventManager) -> None:
209
234
  event_manager.assign_manager_to_request_type(CreateFlowRequest, self.on_create_flow_request)
@@ -240,6 +265,9 @@ class FlowManager:
240
265
  event_manager.assign_manager_to_request_type(
241
266
  PackageNodeAsSerializedFlowRequest, self.on_package_node_as_serialized_flow_request
242
267
  )
268
+ event_manager.assign_manager_to_request_type(
269
+ PackageNodesAsSerializedFlowRequest, self.on_package_nodes_as_serialized_flow_request
270
+ )
243
271
  event_manager.assign_manager_to_request_type(FlushParameterChangesRequest, self.on_flush_request)
244
272
 
245
273
  self._name_to_parent_name = {}
@@ -251,6 +279,7 @@ class FlowManager:
251
279
  self._global_control_flow_machine = None # Track the current control flow machine
252
280
  self._global_single_node_resolution = False
253
281
  self._global_dag_builder = DagBuilder()
282
+ self._node_executor = NodeExecutor()
254
283
 
255
284
  @property
256
285
  def global_single_node_resolution(self) -> bool:
@@ -264,6 +293,10 @@ class FlowManager:
264
293
  def global_dag_builder(self) -> DagBuilder:
265
294
  return self._global_dag_builder
266
295
 
296
+ @property
297
+ def node_executor(self) -> NodeExecutor:
298
+ return self._node_executor
299
+
267
300
  def get_connections(self) -> Connections:
268
301
  """Get the connections instance."""
269
302
  return self._connections
@@ -1249,48 +1282,6 @@ class FlowManager:
1249
1282
  start_end_library_metadata = start_end_library.get_metadata()
1250
1283
  return start_end_library_metadata.library_version
1251
1284
 
1252
- def _analyze_package_node_connections(
1253
- self, package_node: BaseNode, node_name: str
1254
- ) -> ConnectionAnalysis | PackageNodeAsSerializedFlowResultFailure:
1255
- """Analyze package node connections and separate control from data connections."""
1256
- # Get connection details using the efficient approach
1257
- list_connections_request = ListConnectionsForNodeRequest(node_name=node_name)
1258
- list_connections_result = GriptapeNodes.NodeManager().on_list_connections_for_node_request(
1259
- list_connections_request
1260
- )
1261
-
1262
- if not isinstance(list_connections_result, ListConnectionsForNodeResultSuccess):
1263
- details = f"Attempted to analyze connections for package node '{node_name}'. Failed because connection listing failed."
1264
- return PackageNodeAsSerializedFlowResultFailure(result_details=details)
1265
-
1266
- # Separate control connections from data connections based on package node's parameter types
1267
- incoming_data_connections = []
1268
- incoming_control_connections = []
1269
- for incoming_conn in list_connections_result.incoming_connections:
1270
- # Get the package node's parameter to check if it's a control type
1271
- package_param = package_node.get_parameter_by_name(incoming_conn.target_parameter_name)
1272
- if package_param and ParameterTypeBuiltin.CONTROL_TYPE.value in package_param.input_types:
1273
- incoming_control_connections.append(incoming_conn)
1274
- else:
1275
- incoming_data_connections.append(incoming_conn)
1276
-
1277
- outgoing_data_connections = []
1278
- outgoing_control_connections = []
1279
- for outgoing_conn in list_connections_result.outgoing_connections:
1280
- # Get the package node's parameter to check if it's a control type
1281
- package_param = package_node.get_parameter_by_name(outgoing_conn.source_parameter_name)
1282
- if package_param and ParameterTypeBuiltin.CONTROL_TYPE.value == package_param.output_type:
1283
- outgoing_control_connections.append(outgoing_conn)
1284
- else:
1285
- outgoing_data_connections.append(outgoing_conn)
1286
-
1287
- return ConnectionAnalysis(
1288
- incoming_data_connections=incoming_data_connections,
1289
- incoming_control_connections=incoming_control_connections,
1290
- outgoing_data_connections=outgoing_data_connections,
1291
- outgoing_control_connections=outgoing_control_connections,
1292
- )
1293
-
1294
1285
  def _serialize_package_node(
1295
1286
  self,
1296
1287
  node_name: str,
@@ -1372,7 +1363,7 @@ class FlowManager:
1372
1363
 
1373
1364
  # Create parameter modification commands and connection mappings for the start node based on incoming DATA connections
1374
1365
  start_node_parameter_commands = []
1375
- start_to_package_data_connections = []
1366
+ start_to_package_connections = []
1376
1367
  start_node_parameter_value_commands = []
1377
1368
  input_shape_data: WorkflowShapeNodes = {}
1378
1369
 
@@ -1437,7 +1428,7 @@ class FlowManager:
1437
1428
  target_node_uuid=package_node_uuid,
1438
1429
  target_parameter_name=param_name,
1439
1430
  )
1440
- start_to_package_data_connections.append(start_to_package_connection)
1431
+ start_to_package_connections.append(start_to_package_connection)
1441
1432
 
1442
1433
  # Build complete SerializedNodeCommands for start node
1443
1434
  start_node_dependencies = NodeDependencies()
@@ -1452,7 +1443,7 @@ class FlowManager:
1452
1443
 
1453
1444
  return PackagingStartNodeResult(
1454
1445
  start_node_commands=start_node_commands,
1455
- start_to_package_data_connections=start_to_package_data_connections,
1446
+ start_to_package_connections=start_to_package_connections,
1456
1447
  input_shape_data=input_shape_data,
1457
1448
  start_node_parameter_value_commands=start_node_parameter_value_commands,
1458
1449
  )
@@ -1488,7 +1479,7 @@ class FlowManager:
1488
1479
  # Process ALL package node parameters to create end node parameters and connections
1489
1480
  # Note: PROPERTY-only parameters are guaranteed to have OUTPUT mode after serialization
1490
1481
  end_node_parameter_commands = []
1491
- package_to_end_data_connections = []
1482
+ package_to_end_connections = []
1492
1483
  output_shape_data: WorkflowShapeNodes = {}
1493
1484
 
1494
1485
  for package_param in package_node.parameters:
@@ -1531,7 +1522,7 @@ class FlowManager:
1531
1522
  target_node_uuid=end_node_uuid,
1532
1523
  target_parameter_name=end_param_name,
1533
1524
  )
1534
- package_to_end_data_connections.append(package_to_end_connection)
1525
+ package_to_end_connections.append(package_to_end_connection)
1535
1526
 
1536
1527
  # Build complete SerializedNodeCommands for end node
1537
1528
  end_node_dependencies = NodeDependencies()
@@ -1546,54 +1537,10 @@ class FlowManager:
1546
1537
 
1547
1538
  return PackagingEndNodeResult(
1548
1539
  end_node_commands=end_node_commands,
1549
- package_to_end_data_connections=package_to_end_data_connections,
1540
+ package_to_end_connections=package_to_end_connections,
1550
1541
  output_shape_data=output_shape_data,
1551
1542
  )
1552
1543
 
1553
- def _create_start_node_control_connection(
1554
- self,
1555
- entry_control_parameter_name: str | None,
1556
- start_node_uuid: SerializedNodeCommands.NodeUUID,
1557
- package_node_uuid: SerializedNodeCommands.NodeUUID,
1558
- package_node: BaseNode,
1559
- ) -> SerializedFlowCommands.IndirectConnectionSerialization | PackageNodeAsSerializedFlowResultFailure:
1560
- """Create control flow connection from start node to package node.
1561
-
1562
- Connects the start node's first control output to the specified or first available package node control input.
1563
- """
1564
- if entry_control_parameter_name is not None:
1565
- # Case 1: Specific entry parameter name provided
1566
- package_control_input_name = entry_control_parameter_name
1567
- else:
1568
- # Case 2: Find the first available control input parameter
1569
- package_control_input_name = None
1570
- for param in package_node.parameters:
1571
- if ParameterTypeBuiltin.CONTROL_TYPE.value in param.input_types:
1572
- package_control_input_name = param.name
1573
- logger.warning(
1574
- "No entry_control_parameter_name specified for packaging node '%s'. "
1575
- "Using first available control input parameter: '%s'",
1576
- package_node.name,
1577
- package_control_input_name,
1578
- )
1579
- break
1580
-
1581
- if package_control_input_name is None:
1582
- details = f"Attempted to package node '{package_node.name}'. Failed because no control input parameters found on the node, so cannot create control flow connection."
1583
- return PackageNodeAsSerializedFlowResultFailure(result_details=details)
1584
-
1585
- # StartNode always has a control output parameter with name "exec_out"
1586
- source_control_parameter_name = "exec_out"
1587
-
1588
- # Create the connection
1589
- control_connection = SerializedFlowCommands.IndirectConnectionSerialization(
1590
- source_node_uuid=start_node_uuid,
1591
- source_parameter_name=source_control_parameter_name,
1592
- target_node_uuid=package_node_uuid,
1593
- target_parameter_name=package_control_input_name,
1594
- )
1595
- return control_connection
1596
-
1597
1544
  def _assemble_serialized_flow( # noqa: PLR0913
1598
1545
  self,
1599
1546
  serialized_package_result: SerializeNodeToCommandsResultSuccess,
@@ -1607,8 +1554,8 @@ class FlowManager:
1607
1554
  """Assemble the complete SerializedFlowCommands from all components."""
1608
1555
  # Combine all connections: Start->Package + Package->End + Control Flow
1609
1556
  all_connections = (
1610
- start_node_result.start_to_package_data_connections
1611
- + end_node_result.package_to_end_data_connections
1557
+ start_node_result.start_to_package_connections
1558
+ + end_node_result.package_to_end_connections
1612
1559
  + control_flow_connections
1613
1560
  )
1614
1561
 
@@ -1650,6 +1597,11 @@ class FlowManager:
1650
1597
  )
1651
1598
  packaged_dependencies.libraries.add(start_end_library_dependency)
1652
1599
 
1600
+ # Aggregate node types used
1601
+ packaged_node_types_used = self._aggregate_node_types_used(
1602
+ serialized_node_commands=all_serialized_nodes, sub_flows_commands=[]
1603
+ )
1604
+
1653
1605
  # Build the complete SerializedFlowCommands
1654
1606
  return SerializedFlowCommands(
1655
1607
  flow_initialization_command=create_packaged_flow_request,
@@ -1663,153 +1615,1198 @@ class FlowManager:
1663
1615
  set_lock_commands_per_node=set_lock_commands_per_node,
1664
1616
  sub_flows_commands=[],
1665
1617
  node_dependencies=packaged_dependencies,
1618
+ node_types_used=packaged_node_types_used,
1666
1619
  )
1667
1620
 
1668
- async def on_start_flow_request(self, request: StartFlowRequest) -> ResultPayload: # noqa: C901, PLR0911, PLR0912
1669
- # which flow
1670
- flow_name = request.flow_name
1671
- if not flow_name:
1672
- details = "Must provide flow name to start a flow."
1621
+ def on_package_nodes_as_serialized_flow_request( # noqa: C901, PLR0911
1622
+ self, request: PackageNodesAsSerializedFlowRequest
1623
+ ) -> ResultPayload:
1624
+ """Handle request to package multiple nodes as a serialized flow.
1673
1625
 
1674
- return StartFlowResultFailure(validation_exceptions=[], result_details=details)
1675
- # get the flow by ID
1676
- try:
1677
- flow = self.get_flow_by_name(flow_name)
1678
- except KeyError as err:
1679
- details = f"Cannot start flow. Error: {err}"
1680
- return StartFlowResultFailure(validation_exceptions=[err], result_details=details)
1681
- # Check to see if the flow is already running.
1682
- if self.check_for_existing_running_flow():
1683
- details = "Cannot start flow. Flow is already running."
1684
- return StartFlowResultFailure(validation_exceptions=[], result_details=details)
1685
- # A node has been provided to either start or to run up to.
1686
- if request.flow_node_name:
1687
- flow_node_name = request.flow_node_name
1688
- flow_node = GriptapeNodes.ObjectManager().attempt_get_object_by_name_as_type(flow_node_name, BaseNode)
1689
- if not flow_node:
1690
- details = f"Provided node with name {flow_node_name} does not exist"
1691
- return StartFlowResultFailure(validation_exceptions=[], result_details=details)
1692
- # lets get the first control node in the flow!
1693
- start_node = self.get_start_node_from_node(flow, flow_node)
1694
- # if the start is not the node provided, set a breakpoint at the stop (we're running up until there)
1695
- if not start_node:
1696
- details = f"Start node for node with name {flow_node_name} does not exist"
1697
- return StartFlowResultFailure(validation_exceptions=[], result_details=details)
1698
- if start_node != flow_node:
1699
- flow_node.stop_flow = True
1700
- else:
1701
- # we wont hit this if we dont have a request id, our requests always have nodes
1702
- # If there is a request, reinitialize the queue
1703
- self.get_start_node_queue() # initialize the start flow queue!
1704
- start_node = None
1705
- # Run Validation before starting a flow
1706
- result = await self.on_validate_flow_dependencies_request(
1707
- ValidateFlowDependenciesRequest(flow_name=flow_name, flow_node_name=start_node.name if start_node else None)
1708
- )
1709
- try:
1710
- if not result.succeeded():
1711
- details = f"Couldn't start flow with name {flow_name}. Flow Validation Failed"
1712
- return StartFlowResultFailure(validation_exceptions=[], result_details=details)
1713
- result = cast("ValidateFlowDependenciesResultSuccess", result)
1626
+ Creates a self-contained flow with Start → [Selected Nodes] → End structure,
1627
+ where artificial start/end nodes interface with external connections only.
1628
+ """
1629
+ # Step 1: Reject empty node list
1630
+ if not request.node_names:
1631
+ return PackageNodesAsSerializedFlowResultFailure(
1632
+ result_details="Attempted to package nodes as serialized flow. Failed because no nodes were specified in the node_names list."
1633
+ )
1714
1634
 
1715
- if not result.validation_succeeded:
1716
- details = f"Couldn't start flow with name {flow_name}. Flow Validation Failed."
1717
- if len(result.exceptions) > 0:
1718
- for exception in result.exceptions:
1719
- details = f"{details}\n\t{exception}"
1720
- return StartFlowResultFailure(validation_exceptions=result.exceptions, result_details=details)
1721
- except Exception as e:
1722
- details = f"Couldn't start flow with name {flow_name}. Flow Validation Failed: {e}"
1723
- return StartFlowResultFailure(validation_exceptions=[e], result_details=details)
1724
- # By now, it has been validated with no exceptions.
1725
- try:
1726
- await self.start_flow(flow, start_node, debug_mode=request.debug_mode)
1727
- except Exception as e:
1728
- details = f"Failed to kick off flow with name {flow_name}. Exception occurred: {e} "
1729
- return StartFlowResultFailure(validation_exceptions=[e], result_details=details)
1635
+ # Step 2: Validate library and get version
1636
+ library_version = self._validate_and_get_multi_node_library_info(request=request)
1637
+ if isinstance(library_version, PackageNodesAsSerializedFlowResultFailure):
1638
+ return library_version
1730
1639
 
1731
- details = f"Successfully kicked off flow with name {flow_name}"
1640
+ # Step 3: Validate all nodes exist
1641
+ validation_result = self._validate_multi_node_request(request)
1642
+ if validation_result is not None:
1643
+ return validation_result
1732
1644
 
1733
- return StartFlowResultSuccess(result_details=details)
1645
+ # Get the actual node objects for processing
1646
+ nodes_to_package = []
1647
+ for node_name in request.node_names:
1648
+ node = GriptapeNodes.NodeManager().get_node_by_name(node_name)
1649
+ nodes_to_package.append(node)
1734
1650
 
1735
- def on_get_flow_state_request(self, event: GetFlowStateRequest) -> ResultPayload:
1736
- flow_name = event.flow_name
1737
- if not flow_name:
1738
- details = "Could not get flow state. No flow name was provided."
1739
- return GetFlowStateResultFailure(result_details=details)
1740
- try:
1741
- flow = self.get_flow_by_name(flow_name)
1742
- except KeyError as err:
1743
- details = f"Could not get flow state. Error: {err}"
1744
- return GetFlowStateResultFailure(result_details=details)
1745
- try:
1746
- control_nodes, resolving_nodes = self.flow_state(flow)
1747
- except Exception as e:
1748
- details = f"Failed to get flow state of flow with name {flow_name}. Exception occurred: {e} "
1749
- logger.exception(details)
1750
- return GetFlowStateResultFailure(result_details=details)
1751
- details = f"Successfully got flow state for flow with name {flow_name}."
1752
- return GetFlowStateResultSuccess(
1753
- control_nodes=control_nodes,
1754
- resolving_node=resolving_nodes,
1755
- result_details=details,
1651
+ # Step 4: Initialize tracking variables and mappings (moved up so package node serialization can use them)
1652
+ unique_parameter_uuid_to_values = {}
1653
+ serialized_parameter_value_tracker = SerializedParameterValueTracker()
1654
+ node_name_to_uuid: dict[str, SerializedNodeCommands.NodeUUID] = {}
1655
+ packaged_nodes_set_parameter_value_commands: dict[
1656
+ SerializedNodeCommands.NodeUUID, list[SerializedNodeCommands.IndirectSetParameterValueCommand]
1657
+ ] = {}
1658
+ packaged_nodes_internal_connections: list[SerializedFlowCommands.IndirectConnectionSerialization] = []
1659
+
1660
+ # Step 5: Serialize nodes with local execution environment to prevent recursive loops
1661
+ serialized_package_nodes = self._serialize_package_nodes_for_local_execution(
1662
+ nodes_to_package=nodes_to_package,
1663
+ unique_parameter_uuid_to_values=unique_parameter_uuid_to_values,
1664
+ serialized_parameter_value_tracker=serialized_parameter_value_tracker,
1665
+ node_name_to_uuid=node_name_to_uuid,
1666
+ set_parameter_value_commands=packaged_nodes_set_parameter_value_commands,
1667
+ internal_connections=packaged_nodes_internal_connections,
1668
+ )
1669
+ if isinstance(serialized_package_nodes, PackageNodesAsSerializedFlowResultFailure):
1670
+ return serialized_package_nodes
1671
+
1672
+ # Step 6: Inject OUTPUT mode changes for PROPERTY-only parameters to enable value reconciliation after the
1673
+ # packaged workflow is run.
1674
+ # Example: Nodes A -> B -> C. If B has property-only parameters, those values may change during execution,
1675
+ # so we need to send the value back after the packaged flow has run. We do this by making connections from
1676
+ # B's property-only parameters to the End Node, ensuring they're reflected when the packaged flow returns.
1677
+ # Since connections require an OUTPUT parameter mode, we inject that here.
1678
+ self._inject_output_mode_for_property_parameters(nodes_to_package, serialized_package_nodes)
1679
+
1680
+ # Step 7: Analyze external connections (connections from/to nodes outside our selection)
1681
+ node_connections_dict = self._analyze_multi_node_external_connections(package_nodes=nodes_to_package)
1682
+ if isinstance(node_connections_dict, PackageNodesAsSerializedFlowResultFailure):
1683
+ return node_connections_dict
1684
+
1685
+ # Step 8: Create start node with parameters for external incoming connections
1686
+ start_node_result = self._create_multi_node_start_node_with_connections(
1687
+ request=request,
1688
+ library_version=library_version,
1689
+ unique_parameter_uuid_to_values=unique_parameter_uuid_to_values,
1690
+ serialized_parameter_value_tracker=serialized_parameter_value_tracker,
1691
+ node_name_to_uuid=node_name_to_uuid,
1692
+ external_connections_dict=node_connections_dict,
1756
1693
  )
1694
+ if isinstance(start_node_result, PackageNodesAsSerializedFlowResultFailure):
1695
+ return start_node_result
1757
1696
 
1758
- def on_cancel_flow_request(self, request: CancelFlowRequest) -> ResultPayload:
1759
- flow_name = request.flow_name
1760
- if not flow_name:
1761
- details = "Could not cancel flow execution. No flow name was provided."
1697
+ # Step 9: Create end node with parameters for external outgoing connections and parameter mappings
1698
+ end_node_result = self._create_multi_node_end_node_with_connections(
1699
+ request=request,
1700
+ package_nodes=nodes_to_package,
1701
+ node_name_to_uuid=node_name_to_uuid,
1702
+ library_version=library_version,
1703
+ node_connections_dict=node_connections_dict,
1704
+ )
1705
+ if isinstance(end_node_result, PackageNodesAsSerializedFlowResultFailure):
1706
+ return end_node_result
1762
1707
 
1763
- return CancelFlowResultFailure(result_details=details)
1764
- try:
1765
- self.get_flow_by_name(flow_name)
1766
- except KeyError as err:
1767
- details = f"Could not cancel flow execution. Error: {err}"
1708
+ end_node_packaging_result = end_node_result.packaging_result
1709
+ parameter_name_mappings = end_node_result.parameter_name_mappings
1768
1710
 
1769
- return CancelFlowResultFailure(result_details=details)
1770
- try:
1771
- self.cancel_flow_run()
1772
- except Exception as e:
1773
- details = f"Could not cancel flow execution. Exception: {e}"
1711
+ # Step 10: Assemble final SerializedFlowCommands
1712
+ # Collect all connections from start/end nodes and internal package connections
1713
+ all_connections = self._collect_all_connections_for_multi_node_package(
1714
+ start_node_result=start_node_result,
1715
+ end_node_packaging_result=end_node_packaging_result,
1716
+ packaged_nodes_internal_connections=packaged_nodes_internal_connections,
1717
+ )
1774
1718
 
1775
- return CancelFlowResultFailure(result_details=details)
1776
- details = f"Successfully cancelled flow execution with name {flow_name}"
1719
+ # Build WorkflowShape from collected parameter shape data
1720
+ workflow_shape = GriptapeNodes.WorkflowManager().build_workflow_shape_from_parameter_info(
1721
+ input_node_params=start_node_result.input_shape_data,
1722
+ output_node_params=end_node_packaging_result.output_shape_data,
1723
+ )
1777
1724
 
1778
- return CancelFlowResultSuccess(result_details=details)
1725
+ # Create set parameter value commands dict
1726
+ set_parameter_value_commands = {
1727
+ start_node_result.start_node_commands.node_uuid: start_node_result.start_node_parameter_value_commands,
1728
+ **packaged_nodes_set_parameter_value_commands,
1729
+ }
1779
1730
 
1780
- async def on_single_node_step_request(self, request: SingleNodeStepRequest) -> ResultPayload:
1781
- flow_name = request.flow_name
1782
- if not flow_name:
1783
- details = "Could not advance to the next step of a running workflow. No flow name was provided."
1731
+ # Collect all serialized nodes
1732
+ all_serialized_nodes = [
1733
+ start_node_result.start_node_commands,
1734
+ *serialized_package_nodes,
1735
+ end_node_packaging_result.end_node_commands,
1736
+ ]
1784
1737
 
1785
- return SingleNodeStepResultFailure(validation_exceptions=[], result_details=details)
1786
- try:
1787
- self.get_flow_by_name(flow_name)
1788
- except KeyError as err:
1789
- details = f"Could not advance to the next step of a running workflow. No flow with name {flow_name} exists. Error: {err}"
1738
+ # Create comprehensive dependencies from all nodes
1739
+ combined_dependencies = NodeDependencies()
1740
+ for serialized_node in all_serialized_nodes:
1741
+ combined_dependencies.aggregate_from(serialized_node.node_dependencies)
1790
1742
 
1791
- return SingleNodeStepResultFailure(validation_exceptions=[err], result_details=details)
1792
- try:
1793
- flow = self.get_flow_by_name(flow_name)
1794
- await self.single_node_step(flow)
1795
- except Exception as e:
1796
- details = f"Could not advance to the next step of a running workflow. Exception: {e}"
1797
- return SingleNodeStepResultFailure(validation_exceptions=[], result_details=details)
1743
+ # Extract lock commands from serialized nodes (they're embedded in SerializedNodeCommands)
1744
+ set_lock_commands_per_node = {}
1745
+ for serialized_node in all_serialized_nodes:
1746
+ if serialized_node.lock_node_command:
1747
+ set_lock_commands_per_node[serialized_node.node_uuid] = serialized_node.lock_node_command
1798
1748
 
1799
- # All completed happily
1800
- details = f"Successfully advanced to the next step of a running workflow with name {flow_name}"
1749
+ # Create a CreateFlowRequest for the packaged flow so that it can
1750
+ # run as a standalone workflow
1751
+ packaged_flow_metadata = {} # Keep it simple until we have reason to populate it
1801
1752
 
1802
- return SingleNodeStepResultSuccess(result_details=details)
1753
+ create_packaged_flow_request = CreateFlowRequest(
1754
+ parent_flow_name=None, # Standalone flow
1755
+ set_as_new_context=False, # Let deserializer decide
1756
+ metadata=packaged_flow_metadata,
1757
+ )
1803
1758
 
1804
- async def on_single_execution_step_request(self, request: SingleExecutionStepRequest) -> ResultPayload:
1805
- flow_name = request.flow_name
1806
- if not flow_name:
1807
- details = "Could not advance to the next step of a running workflow. No flow name was provided."
1759
+ # Aggregate node types used
1760
+ combined_node_types_used = self._aggregate_node_types_used(
1761
+ serialized_node_commands=all_serialized_nodes, sub_flows_commands=[]
1762
+ )
1808
1763
 
1809
- return SingleExecutionStepResultFailure(result_details=details)
1810
- try:
1811
- flow = self.get_flow_by_name(flow_name)
1812
- except KeyError as err:
1764
+ # Build the complete serialized flow
1765
+ final_serialized_flow = SerializedFlowCommands(
1766
+ flow_initialization_command=create_packaged_flow_request,
1767
+ serialized_node_commands=all_serialized_nodes,
1768
+ serialized_connections=all_connections,
1769
+ unique_parameter_uuid_to_values=unique_parameter_uuid_to_values,
1770
+ set_parameter_value_commands=set_parameter_value_commands,
1771
+ set_lock_commands_per_node=set_lock_commands_per_node,
1772
+ sub_flows_commands=[],
1773
+ node_dependencies=combined_dependencies,
1774
+ node_types_used=combined_node_types_used,
1775
+ )
1776
+
1777
+ return PackageNodesAsSerializedFlowResultSuccess(
1778
+ serialized_flow_commands=final_serialized_flow,
1779
+ workflow_shape=workflow_shape,
1780
+ packaged_node_names=request.node_names,
1781
+ parameter_name_mappings=parameter_name_mappings,
1782
+ result_details=f"Successfully packaged {len(request.node_names)} nodes as serialized flow.",
1783
+ )
1784
+
1785
+ def _validate_and_get_multi_node_library_info(
1786
+ self, request: PackageNodesAsSerializedFlowRequest
1787
+ ) -> str | PackageNodesAsSerializedFlowResultFailure:
1788
+ """Validate start/end node types exist in library and return library version."""
1789
+ # Early validation - ensure both start and end node types exist in the specified library
1790
+ try:
1791
+ start_end_library = LibraryRegistry.get_library_for_node_type(
1792
+ node_type=request.start_node_type, specific_library_name=request.start_end_specific_library_name
1793
+ )
1794
+ except KeyError as err:
1795
+ details = f"Attempted to package nodes with start node type '{request.start_node_type}' from library '{request.start_end_specific_library_name}'. Failed because start node type was not found in library. Error: {err}."
1796
+ return PackageNodesAsSerializedFlowResultFailure(result_details=details)
1797
+
1798
+ try:
1799
+ LibraryRegistry.get_library_for_node_type(
1800
+ node_type=request.end_node_type, specific_library_name=request.start_end_specific_library_name
1801
+ )
1802
+ except KeyError as err:
1803
+ details = f"Attempted to package nodes with end node type '{request.end_node_type}' from library '{request.start_end_specific_library_name}'. Failed because end node type was not found in library. Error: {err}."
1804
+ return PackageNodesAsSerializedFlowResultFailure(result_details=details)
1805
+
1806
+ # Get the actual library version
1807
+ start_end_library_metadata = start_end_library.get_metadata()
1808
+ return start_end_library_metadata.library_version
1809
+
1810
+ def _validate_multi_node_request(
1811
+ self, request: PackageNodesAsSerializedFlowRequest
1812
+ ) -> None | PackageNodesAsSerializedFlowResultFailure:
1813
+ """Validate that all requested nodes exist and control flow configuration is valid."""
1814
+ # Validate all nodes exist
1815
+ missing_nodes = []
1816
+ for node_name in request.node_names:
1817
+ try:
1818
+ GriptapeNodes.NodeManager().get_node_by_name(node_name)
1819
+ except Exception:
1820
+ missing_nodes.append(node_name)
1821
+
1822
+ if missing_nodes:
1823
+ return PackageNodesAsSerializedFlowResultFailure(
1824
+ result_details=f"Attempted to package nodes as serialized flow. Failed because the following nodes were not found: {', '.join(missing_nodes)}."
1825
+ )
1826
+
1827
+ # Validate control flow configuration for non-empty node lists
1828
+ if request.node_names and request.entry_control_parameter_name and not request.entry_control_node_name:
1829
+ return PackageNodesAsSerializedFlowResultFailure(
1830
+ result_details="Attempted to package nodes as serialized flow. Failed because entry_control_parameter_name was specified but entry_control_node_name was not. For multi-node packaging with a non-empty node list, both must be specified to avoid ambiguity about which node should receive the control connection."
1831
+ )
1832
+
1833
+ # Validate entry_control_node_name exists and is in our package list
1834
+ if request.entry_control_node_name and request.entry_control_node_name not in request.node_names:
1835
+ return PackageNodesAsSerializedFlowResultFailure(
1836
+ result_details=f"Attempted to package nodes as serialized flow. Failed because entry_control_node_name '{request.entry_control_node_name}' is not in the list of nodes to package: {request.node_names}."
1837
+ )
1838
+
1839
+ return None
1840
+
1841
+ def _serialize_package_nodes_for_local_execution( # noqa: C901, PLR0913
1842
+ self,
1843
+ nodes_to_package: list[BaseNode],
1844
+ unique_parameter_uuid_to_values: dict[SerializedNodeCommands.UniqueParameterValueUUID, Any],
1845
+ serialized_parameter_value_tracker: SerializedParameterValueTracker,
1846
+ node_name_to_uuid: dict[str, SerializedNodeCommands.NodeUUID], # OUTPUT: will be populated
1847
+ set_parameter_value_commands: dict[ # OUTPUT: will be populated
1848
+ SerializedNodeCommands.NodeUUID, list[SerializedNodeCommands.IndirectSetParameterValueCommand]
1849
+ ],
1850
+ internal_connections: list[SerializedFlowCommands.IndirectConnectionSerialization], # OUTPUT: will be populated
1851
+ ) -> list[SerializedNodeCommands] | PackageNodesAsSerializedFlowResultFailure:
1852
+ """Serialize package nodes while temporarily setting execution environment to local to prevent recursive loops.
1853
+
1854
+ This method temporarily overrides each node's execution_environment parameter to LOCAL_EXECUTION
1855
+ during serialization, then restores the original values. This prevents recursive packaging loops
1856
+ that could occur if nodes were configured for remote execution environments.
1857
+
1858
+ Args:
1859
+ nodes_to_package: List of nodes to serialize
1860
+ unique_parameter_uuid_to_values: Shared dictionary for deduplicating parameter values across all nodes
1861
+ serialized_parameter_value_tracker: Tracker for serialized parameter values
1862
+ node_name_to_uuid: OUTPUT - Dictionary mapping node names to UUIDs (populated by this method)
1863
+ set_parameter_value_commands: OUTPUT - Dict mapping node UUIDs to parameter value commands (populated by this method)
1864
+ internal_connections: OUTPUT - List of connections between package nodes (populated by this method)
1865
+
1866
+ Returns:
1867
+ List of SerializedNodeCommands on success, or PackageNodesAsSerializedFlowResultFailure on failure
1868
+ """
1869
+ # Intercept execution_environment for all nodes before serialization
1870
+ original_execution_environments = {}
1871
+ for node in nodes_to_package:
1872
+ original_value = node.get_parameter_value("execution_environment")
1873
+ original_execution_environments[node.name] = original_value
1874
+ node.set_parameter_value("execution_environment", LOCAL_EXECUTION)
1875
+
1876
+ try:
1877
+ # Serialize each node using shared unique_parameter_uuid_to_values dictionary for deduplication
1878
+ serialized_node_commands = []
1879
+
1880
+ for node in nodes_to_package:
1881
+ # Serialize this node using shared dictionaries for value deduplication
1882
+ serialize_request = SerializeNodeToCommandsRequest(
1883
+ node_name=node.name,
1884
+ unique_parameter_uuid_to_values=unique_parameter_uuid_to_values,
1885
+ serialized_parameter_value_tracker=serialized_parameter_value_tracker,
1886
+ )
1887
+ serialize_result = GriptapeNodes.NodeManager().on_serialize_node_to_commands(serialize_request)
1888
+
1889
+ if not isinstance(serialize_result, SerializeNodeToCommandsResultSuccess):
1890
+ return PackageNodesAsSerializedFlowResultFailure(
1891
+ result_details=f"Attempted to package nodes as serialized flow. Failed to serialize node '{node.name}': {serialize_result.result_details}"
1892
+ )
1893
+
1894
+ # Collect serialized node
1895
+ serialized_node_commands.append(serialize_result.serialized_node_commands)
1896
+
1897
+ # Populate the shared node_name_to_uuid mapping
1898
+ if serialize_result.serialized_node_commands.create_node_command.node_name is not None:
1899
+ node_name_to_uuid[serialize_result.serialized_node_commands.create_node_command.node_name] = (
1900
+ serialize_result.serialized_node_commands.node_uuid
1901
+ )
1902
+
1903
+ # Collect set parameter value commands (references to unique_parameter_uuid_to_values)
1904
+ if serialize_result.set_parameter_value_commands:
1905
+ set_parameter_value_commands[serialize_result.serialized_node_commands.node_uuid] = (
1906
+ serialize_result.set_parameter_value_commands
1907
+ )
1908
+
1909
+ # Build internal connections between package nodes
1910
+ package_node_names_set = {n.name for n in nodes_to_package}
1911
+ for node in nodes_to_package:
1912
+ # Get connections FROM this node TO other nodes in the package
1913
+ list_connections_request = ListConnectionsForNodeRequest(node_name=node.name)
1914
+ list_connections_result = GriptapeNodes.NodeManager().on_list_connections_for_node_request(
1915
+ list_connections_request
1916
+ )
1917
+
1918
+ if not isinstance(list_connections_result, ListConnectionsForNodeResultSuccess):
1919
+ return PackageNodesAsSerializedFlowResultFailure(
1920
+ result_details=f"Attempted to package nodes as serialized flow. Failed to list connections for node '{node.name}': {list_connections_result.result_details}"
1921
+ )
1922
+
1923
+ # Only include connections where BOTH source and target are in the package
1924
+ for outgoing_conn in list_connections_result.outgoing_connections:
1925
+ if outgoing_conn.target_node_name in package_node_names_set:
1926
+ source_uuid = node_name_to_uuid[node.name]
1927
+ target_uuid = node_name_to_uuid[outgoing_conn.target_node_name]
1928
+ internal_connections.append(
1929
+ SerializedFlowCommands.IndirectConnectionSerialization(
1930
+ source_node_uuid=source_uuid,
1931
+ source_parameter_name=outgoing_conn.source_parameter_name,
1932
+ target_node_uuid=target_uuid,
1933
+ target_parameter_name=outgoing_conn.target_parameter_name,
1934
+ )
1935
+ )
1936
+ finally:
1937
+ # Always restore original execution_environment values, even on failure
1938
+ for node_name, original_value in original_execution_environments.items():
1939
+ restore_node = GriptapeNodes.NodeManager().get_node_by_name(node_name)
1940
+ restore_node.set_parameter_value("execution_environment", original_value)
1941
+
1942
+ return serialized_node_commands
1943
+
1944
+ def _inject_output_mode_for_property_parameters(
1945
+ self, nodes_to_package: list[BaseNode], serialized_package_nodes: list[SerializedNodeCommands]
1946
+ ) -> None:
1947
+ """Inject OUTPUT mode for PROPERTY-only parameters to enable value reconciliation.
1948
+
1949
+ This method adds ALTER parameter commands to serialized nodes for parameters that have
1950
+ PROPERTY mode but not OUTPUT mode. This allows the orchestrator/caller to reconcile
1951
+ the packaged node's values after execution.
1952
+
1953
+ Args:
1954
+ nodes_to_package: List of nodes being packaged
1955
+ serialized_package_nodes: The serialized node commands to modify
1956
+ """
1957
+ # Apply ALTER parameter commands for PROPERTY-only parameters to enable OUTPUT mode
1958
+ # We need these to emit their values back so that the orchestrator/caller
1959
+ # can reconcile the packaged node's values after it is executed.
1960
+ for package_node in nodes_to_package:
1961
+ # Find the corresponding serialized node
1962
+ serialized_node = None
1963
+ for serialized_node_command in serialized_package_nodes:
1964
+ if serialized_node_command.create_node_command.node_name == package_node.name:
1965
+ serialized_node = serialized_node_command
1966
+ break
1967
+
1968
+ if serialized_node is None:
1969
+ error_msg = f"Data integrity error: Could not find serialized node for package node '{package_node.name}'. This indicates a logic error in the serialization process."
1970
+ logger.error(error_msg)
1971
+ raise RuntimeError(error_msg)
1972
+
1973
+ package_alter_parameter_commands = []
1974
+ for package_param in package_node.parameters:
1975
+ has_output_mode = ParameterMode.OUTPUT in package_param.allowed_modes
1976
+ has_property_mode = ParameterMode.PROPERTY in package_param.allowed_modes
1977
+ # If has PROPERTY but not OUTPUT, add ALTER command to enable OUTPUT
1978
+ if has_property_mode and not has_output_mode:
1979
+ alter_param_request = AlterParameterDetailsRequest(
1980
+ parameter_name=package_param.name,
1981
+ node_name=package_node.name,
1982
+ mode_allowed_output=True,
1983
+ )
1984
+ package_alter_parameter_commands.append(alter_param_request)
1985
+
1986
+ # If we have alter parameter commands, append them to the existing element_modification_commands
1987
+ if package_alter_parameter_commands:
1988
+ serialized_node.element_modification_commands.extend(package_alter_parameter_commands)
1989
+
1990
+ def _analyze_multi_node_external_connections(
1991
+ self, package_nodes: list[BaseNode]
1992
+ ) -> dict[str, ConnectionAnalysis] | PackageNodesAsSerializedFlowResultFailure:
1993
+ """Analyze external connections for each package node using filtered single-node analysis.
1994
+
1995
+ This method reuses the existing single-node connection analysis method but applies filtering
1996
+ to only capture "external" connections - those that cross the package boundary.
1997
+
1998
+ External connections are:
1999
+ - Incoming connections where the source node is NOT in the package
2000
+ - Outgoing connections where the target node is NOT in the package
2001
+
2002
+ Internal connections (between nodes within the package) are filtered out by passing
2003
+ the set of package node names to the single-node analysis method.
2004
+
2005
+ Args:
2006
+ package_nodes: List of nodes being packaged together
2007
+
2008
+ Returns:
2009
+ Dictionary mapping node_name -> ConnectionAnalysis, where each ConnectionAnalysis
2010
+ contains only the external connections for that node, or failure result on error
2011
+ """
2012
+ package_node_names_set = {node.name for node in package_nodes}
2013
+ node_connections = {}
2014
+
2015
+ for package_node in package_nodes:
2016
+ # Perform a single node analysis, filtering out internal connections
2017
+ connection_analysis = self._analyze_package_node_connections(
2018
+ package_node=package_node,
2019
+ node_name=package_node.name,
2020
+ package_node_names=package_node_names_set,
2021
+ )
2022
+ if isinstance(connection_analysis, PackageNodeAsSerializedFlowResultFailure):
2023
+ return PackageNodesAsSerializedFlowResultFailure(result_details=connection_analysis.result_details)
2024
+
2025
+ node_connections[package_node.name] = connection_analysis
2026
+
2027
+ return node_connections
2028
+
2029
+ def _analyze_package_node_connections(
2030
+ self, package_node: BaseNode, node_name: str, package_node_names: set[str] | None = None
2031
+ ) -> ConnectionAnalysis | PackageNodeAsSerializedFlowResultFailure:
2032
+ """Analyze package node connections and separate control from data connections."""
2033
+ # Get connection details using the efficient approach
2034
+ list_connections_request = ListConnectionsForNodeRequest(node_name=node_name)
2035
+ list_connections_result = GriptapeNodes.NodeManager().on_list_connections_for_node_request(
2036
+ list_connections_request
2037
+ )
2038
+
2039
+ if not isinstance(list_connections_result, ListConnectionsForNodeResultSuccess):
2040
+ details = f"Attempted to analyze connections for package node '{node_name}'. Failed because connection listing failed."
2041
+ return PackageNodeAsSerializedFlowResultFailure(result_details=details)
2042
+
2043
+ # Separate control connections from data connections based on package node's parameter types
2044
+ incoming_data_connections = []
2045
+ incoming_control_connections = []
2046
+ for incoming_conn in list_connections_result.incoming_connections:
2047
+ # Filter out internal connections if package_node_names is provided
2048
+ if package_node_names is not None and incoming_conn.source_node_name in package_node_names:
2049
+ continue
2050
+
2051
+ # Get the package node's parameter to check if it's a control type
2052
+ package_param = package_node.get_parameter_by_name(incoming_conn.target_parameter_name)
2053
+ if package_param and ParameterTypeBuiltin.CONTROL_TYPE.value in package_param.input_types:
2054
+ incoming_control_connections.append(incoming_conn)
2055
+ else:
2056
+ incoming_data_connections.append(incoming_conn)
2057
+
2058
+ outgoing_data_connections = []
2059
+ outgoing_control_connections = []
2060
+ for outgoing_conn in list_connections_result.outgoing_connections:
2061
+ # Filter out internal connections if package_node_names is provided
2062
+ if package_node_names is not None and outgoing_conn.target_node_name in package_node_names:
2063
+ continue
2064
+
2065
+ # Get the package node's parameter to check if it's a control type
2066
+ package_param = package_node.get_parameter_by_name(outgoing_conn.source_parameter_name)
2067
+ if package_param and ParameterTypeBuiltin.CONTROL_TYPE.value == package_param.output_type:
2068
+ outgoing_control_connections.append(outgoing_conn)
2069
+ else:
2070
+ outgoing_data_connections.append(outgoing_conn)
2071
+
2072
+ return ConnectionAnalysis(
2073
+ incoming_data_connections=incoming_data_connections,
2074
+ incoming_control_connections=incoming_control_connections,
2075
+ outgoing_data_connections=outgoing_data_connections,
2076
+ outgoing_control_connections=outgoing_control_connections,
2077
+ )
2078
+
2079
+ def _create_multi_node_end_node_with_connections(
2080
+ self,
2081
+ request: PackageNodesAsSerializedFlowRequest,
2082
+ package_nodes: list[BaseNode],
2083
+ node_name_to_uuid: dict[str, SerializedNodeCommands.NodeUUID],
2084
+ library_version: str,
2085
+ node_connections_dict: dict[str, ConnectionAnalysis],
2086
+ ) -> MultiNodeEndNodeResult | PackageNodesAsSerializedFlowResultFailure:
2087
+ """Create end node commands and connections for ALL package parameters that meet criteria (copied from single-node)."""
2088
+ # Generate UUID and name for end node
2089
+ end_node_uuid = SerializedNodeCommands.NodeUUID(str(uuid4()))
2090
+ end_node_name = "End_Multi_Package"
2091
+
2092
+ # Build end node CreateNodeRequest
2093
+ end_create_node_command = CreateNodeRequest(
2094
+ node_type=request.end_node_type,
2095
+ specific_library_name=request.start_end_specific_library_name,
2096
+ node_name=end_node_name,
2097
+ metadata={},
2098
+ initial_setup=True,
2099
+ create_error_proxy_on_failure=False,
2100
+ )
2101
+
2102
+ # Create library details
2103
+ end_node_library_details = LibraryNameAndVersion(
2104
+ library_name=request.start_end_specific_library_name,
2105
+ library_version=library_version,
2106
+ )
2107
+
2108
+ # Initialize collections for building the end node
2109
+ end_node_parameter_commands = []
2110
+ package_to_end_connections = []
2111
+ output_shape_data: WorkflowShapeNodes = {}
2112
+ # Parameter name mappings (rosetta stone): maps mangled end node parameter names back to original (node_name, parameter_name)
2113
+ # This is essential for callers to understand which end node outputs correspond to which original node parameters
2114
+ parameter_name_mappings: dict[SanitizedParameterName, OriginalNodeParameter] = {}
2115
+
2116
+ # Handle external control connections first
2117
+ self._create_end_node_control_connections(
2118
+ request=request,
2119
+ package_nodes=package_nodes,
2120
+ node_connections_dict=node_connections_dict,
2121
+ node_name_to_uuid=node_name_to_uuid,
2122
+ end_node_uuid=end_node_uuid,
2123
+ end_node_name=end_node_name,
2124
+ end_node_parameter_commands=end_node_parameter_commands,
2125
+ package_to_end_connections=package_to_end_connections,
2126
+ parameter_name_mappings=parameter_name_mappings,
2127
+ output_shape_data=output_shape_data,
2128
+ )
2129
+
2130
+ # Process ALL parameters with OUTPUT or PROPERTY modes for comprehensive coverage
2131
+ self._create_end_node_data_parameters_and_connections(
2132
+ request=request,
2133
+ package_nodes=package_nodes,
2134
+ node_name_to_uuid=node_name_to_uuid,
2135
+ end_node_uuid=end_node_uuid,
2136
+ end_node_name=end_node_name,
2137
+ end_node_parameter_commands=end_node_parameter_commands,
2138
+ package_to_end_connections=package_to_end_connections,
2139
+ parameter_name_mappings=parameter_name_mappings,
2140
+ output_shape_data=output_shape_data,
2141
+ )
2142
+
2143
+ # Build complete SerializedNodeCommands for end node
2144
+ end_node_dependencies = NodeDependencies()
2145
+ end_node_dependencies.libraries.add(end_node_library_details)
2146
+
2147
+ end_node_commands = SerializedNodeCommands(
2148
+ create_node_command=end_create_node_command,
2149
+ element_modification_commands=end_node_parameter_commands,
2150
+ node_dependencies=end_node_dependencies,
2151
+ node_uuid=end_node_uuid,
2152
+ )
2153
+
2154
+ end_node_result = PackagingEndNodeResult(
2155
+ end_node_commands=end_node_commands,
2156
+ package_to_end_connections=package_to_end_connections,
2157
+ output_shape_data=output_shape_data,
2158
+ )
2159
+
2160
+ return MultiNodeEndNodeResult(
2161
+ packaging_result=end_node_result,
2162
+ parameter_name_mappings=parameter_name_mappings,
2163
+ alter_parameter_commands=[],
2164
+ )
2165
+
2166
+ def _create_end_node_control_connections( # noqa: PLR0913
2167
+ self,
2168
+ request: PackageNodesAsSerializedFlowRequest,
2169
+ package_nodes: list[BaseNode],
2170
+ node_connections_dict: dict[str, ConnectionAnalysis], # Contains only EXTERNAL connections
2171
+ node_name_to_uuid: dict[
2172
+ str, SerializedNodeCommands.NodeUUID
2173
+ ], # Map node names to UUIDs for connection creation
2174
+ end_node_uuid: SerializedNodeCommands.NodeUUID,
2175
+ end_node_name: str,
2176
+ end_node_parameter_commands: list[
2177
+ AddParameterToNodeRequest
2178
+ ], # OUTPUT: Will populate with parameters to add to end node
2179
+ package_to_end_connections: list[
2180
+ SerializedFlowCommands.IndirectConnectionSerialization
2181
+ ], # OUTPUT: Will populate with connections to add
2182
+ parameter_name_mappings: dict[
2183
+ SanitizedParameterName, OriginalNodeParameter
2184
+ ], # OUTPUT: Will populate rosetta stone for parameter names so customer knows how to map mangled names back to original nodes.
2185
+ output_shape_data: WorkflowShapeNodes, # OUTPUT: Will populate with workflow shape data
2186
+ ) -> None:
2187
+ """Create control connections and parameters on end node for EXTERNAL control flow connections."""
2188
+ for package_node in package_nodes:
2189
+ node_connection_analysis = node_connections_dict.get(package_node.name)
2190
+ if node_connection_analysis is None:
2191
+ # This node has no external connections (neither incoming nor outgoing), skip it
2192
+ continue
2193
+
2194
+ # Handle external outgoing control connections
2195
+ for control_conn in node_connection_analysis.outgoing_control_connections:
2196
+ # Get the source parameter for validation and processing
2197
+ source_param = package_node.get_parameter_by_name(control_conn.source_parameter_name)
2198
+ if source_param is None:
2199
+ msg = f"External control connection references parameter '{control_conn.source_parameter_name}' on node '{package_node.name}' which does not exist. This indicates a data consistency issue."
2200
+ raise ValueError(msg)
2201
+
2202
+ # Use comprehensive helper to process this control parameter
2203
+ self._process_parameter_for_end_node(
2204
+ request=request,
2205
+ parameter=source_param,
2206
+ node_name=package_node.name,
2207
+ node_uuid=node_name_to_uuid[package_node.name],
2208
+ end_node_name=end_node_name,
2209
+ end_node_uuid=end_node_uuid,
2210
+ tooltip=f"Control output {control_conn.source_parameter_name} from packaged node {package_node.name}",
2211
+ end_node_parameter_commands=end_node_parameter_commands,
2212
+ package_to_end_connections=package_to_end_connections,
2213
+ parameter_name_mappings=parameter_name_mappings,
2214
+ output_shape_data=output_shape_data,
2215
+ )
2216
+
2217
+ def _create_end_node_data_parameters_and_connections( # noqa: PLR0913
2218
+ self,
2219
+ request: PackageNodesAsSerializedFlowRequest,
2220
+ package_nodes: list[BaseNode],
2221
+ node_name_to_uuid: dict[str, SerializedNodeCommands.NodeUUID],
2222
+ end_node_uuid: SerializedNodeCommands.NodeUUID,
2223
+ end_node_name: str,
2224
+ end_node_parameter_commands: list[
2225
+ AddParameterToNodeRequest
2226
+ ], # OUTPUT: Will populate with parameters to add to end node
2227
+ package_to_end_connections: list[
2228
+ SerializedFlowCommands.IndirectConnectionSerialization
2229
+ ], # OUTPUT: Will populate with connections to add
2230
+ parameter_name_mappings: dict[
2231
+ SanitizedParameterName, OriginalNodeParameter
2232
+ ], # OUTPUT: Will populate rosetta stone for parameter names
2233
+ output_shape_data: WorkflowShapeNodes, # OUTPUT: Will populate with workflow shape data
2234
+ ) -> None:
2235
+ """Create data parameters and connections on end node for all OUTPUT/PROPERTY parameters from packaged nodes."""
2236
+ for package_node in package_nodes:
2237
+ package_node_uuid = node_name_to_uuid[package_node.name]
2238
+
2239
+ for package_param in package_node.parameters:
2240
+ # Only process parameters with OUTPUT or PROPERTY mode
2241
+ has_output_mode = ParameterMode.OUTPUT in package_param.allowed_modes
2242
+ has_property_mode = ParameterMode.PROPERTY in package_param.allowed_modes
2243
+
2244
+ if not has_output_mode and not has_property_mode:
2245
+ continue
2246
+
2247
+ # Skip control parameters - those are handled by the control connections helper
2248
+ if package_param.output_type == ParameterTypeBuiltin.CONTROL_TYPE.value:
2249
+ continue
2250
+
2251
+ # Use comprehensive helper to process this data parameter
2252
+ self._process_parameter_for_end_node(
2253
+ request=request,
2254
+ parameter=package_param,
2255
+ node_name=package_node.name,
2256
+ node_uuid=package_node_uuid,
2257
+ end_node_name=end_node_name,
2258
+ end_node_uuid=end_node_uuid,
2259
+ tooltip=f"Output parameter {package_param.name} from packaged node {package_node.name}",
2260
+ end_node_parameter_commands=end_node_parameter_commands,
2261
+ package_to_end_connections=package_to_end_connections,
2262
+ parameter_name_mappings=parameter_name_mappings,
2263
+ output_shape_data=output_shape_data,
2264
+ )
2265
+
2266
+ def _process_parameter_for_end_node( # noqa: PLR0913
2267
+ self,
2268
+ request: PackageNodesAsSerializedFlowRequest,
2269
+ parameter: Parameter,
2270
+ node_name: str,
2271
+ node_uuid: SerializedNodeCommands.NodeUUID,
2272
+ end_node_name: str,
2273
+ end_node_uuid: SerializedNodeCommands.NodeUUID,
2274
+ tooltip: str,
2275
+ end_node_parameter_commands: list[
2276
+ AddParameterToNodeRequest
2277
+ ], # OUTPUT: Will populate with parameters to add to end node
2278
+ package_to_end_connections: list[
2279
+ SerializedFlowCommands.IndirectConnectionSerialization
2280
+ ], # OUTPUT: Will populate with connections to add
2281
+ parameter_name_mappings: dict[
2282
+ SanitizedParameterName, OriginalNodeParameter
2283
+ ], # OUTPUT: Will populate rosetta stone for parameter names
2284
+ output_shape_data: WorkflowShapeNodes, # OUTPUT: Will populate with workflow shape data
2285
+ ) -> None:
2286
+ """Process a single parameter for inclusion in the end node, handling all aspects of parameter creation and connection."""
2287
+ # Create sanitized parameter name with collision avoidance
2288
+ sanitized_param_name = self._generate_sanitized_parameter_name(
2289
+ prefix=request.output_parameter_prefix,
2290
+ node_name=node_name,
2291
+ parameter_name=parameter.name,
2292
+ )
2293
+
2294
+ # Build parameter name mapping for rosetta stone
2295
+ parameter_name_mappings[sanitized_param_name] = OriginalNodeParameter(
2296
+ node_name=node_name,
2297
+ parameter_name=parameter.name,
2298
+ )
2299
+
2300
+ # Extract parameter shape info for workflow shape (outputs to external consumers)
2301
+ param_shape_info = GriptapeNodes.WorkflowManager().extract_parameter_shape_info(
2302
+ parameter, include_control_params=True
2303
+ )
2304
+ if param_shape_info is not None:
2305
+ if end_node_name not in output_shape_data:
2306
+ output_shape_data[end_node_name] = {}
2307
+ output_shape_data[end_node_name][sanitized_param_name] = param_shape_info
2308
+
2309
+ # Create parameter command for end node
2310
+ add_param_request = AddParameterToNodeRequest(
2311
+ node_name=end_node_name,
2312
+ parameter_name=sanitized_param_name,
2313
+ type=parameter.output_type,
2314
+ default_value=None,
2315
+ tooltip=tooltip,
2316
+ initial_setup=True,
2317
+ )
2318
+ end_node_parameter_commands.append(add_param_request)
2319
+
2320
+ # Create connection from package node to end node
2321
+ package_to_end_connection = SerializedFlowCommands.IndirectConnectionSerialization(
2322
+ source_node_uuid=node_uuid,
2323
+ source_parameter_name=parameter.name,
2324
+ target_node_uuid=end_node_uuid,
2325
+ target_parameter_name=sanitized_param_name,
2326
+ )
2327
+ package_to_end_connections.append(package_to_end_connection)
2328
+
2329
+ def _create_multi_node_start_node_with_connections( # noqa: PLR0913
2330
+ self,
2331
+ request: PackageNodesAsSerializedFlowRequest,
2332
+ library_version: str,
2333
+ unique_parameter_uuid_to_values: dict[SerializedNodeCommands.UniqueParameterValueUUID, Any],
2334
+ serialized_parameter_value_tracker: SerializedParameterValueTracker,
2335
+ node_name_to_uuid: dict[str, SerializedNodeCommands.NodeUUID],
2336
+ external_connections_dict: dict[
2337
+ str, ConnectionAnalysis
2338
+ ], # Contains EXTERNAL connections only - used to determine which parameters need start node inputs
2339
+ ) -> PackagingStartNodeResult | PackageNodesAsSerializedFlowResultFailure:
2340
+ """Create start node commands and connections for external incoming connections."""
2341
+ # Generate UUID and name for start node
2342
+ start_node_uuid = SerializedNodeCommands.NodeUUID(str(uuid4()))
2343
+ start_node_name = "Start_Package_MultiNode"
2344
+
2345
+ # Build start node CreateNodeRequest
2346
+ start_create_node_command = CreateNodeRequest(
2347
+ node_type=request.start_node_type,
2348
+ specific_library_name=request.start_end_specific_library_name,
2349
+ node_name=start_node_name,
2350
+ metadata={},
2351
+ initial_setup=True,
2352
+ create_error_proxy_on_failure=False,
2353
+ )
2354
+
2355
+ # Create library details
2356
+ start_node_library_details = LibraryNameAndVersion(
2357
+ library_name=request.start_end_specific_library_name,
2358
+ library_version=library_version,
2359
+ )
2360
+
2361
+ # Create parameter modification commands and connection mappings for the start node
2362
+ start_node_parameter_commands = []
2363
+ start_to_package_connections = []
2364
+ start_node_parameter_value_commands = []
2365
+ input_shape_data: WorkflowShapeNodes = {}
2366
+
2367
+ # Iterate through all EXTERNAL, INCOMING, DATA connections.
2368
+ # We will then, for each connection:
2369
+ # 1. Generate a mangled name
2370
+ # 2. Add parameter with the mangled name for each connection source on the Start Node object
2371
+ # 3. Extract the value from the source node and have it assigned to the Start Node so that it can be propagated
2372
+ # 4. Create a connection from the Start Node to the package node
2373
+ for target_node_name, connection_analysis in external_connections_dict.items():
2374
+ result = self._create_start_node_parameters_and_connections_for_incoming_data(
2375
+ target_node_name=target_node_name,
2376
+ incoming_data_connections=connection_analysis.incoming_data_connections,
2377
+ output_parameter_prefix=request.output_parameter_prefix,
2378
+ start_node_name=start_node_name,
2379
+ start_node_uuid=start_node_uuid,
2380
+ start_create_node_command=start_create_node_command,
2381
+ node_name_to_uuid=node_name_to_uuid,
2382
+ unique_parameter_uuid_to_values=unique_parameter_uuid_to_values,
2383
+ serialized_parameter_value_tracker=serialized_parameter_value_tracker,
2384
+ )
2385
+ if isinstance(result, PackageNodesAsSerializedFlowResultFailure):
2386
+ return result
2387
+
2388
+ # Accumulate results from helper
2389
+ start_node_parameter_commands.extend(result.parameter_commands)
2390
+ start_to_package_connections.extend(result.data_connections)
2391
+ start_node_parameter_value_commands.extend(result.parameter_value_commands)
2392
+ # Merge input shape data
2393
+ for node_name, params in result.input_shape_data.items():
2394
+ if node_name not in input_shape_data:
2395
+ input_shape_data[node_name] = {}
2396
+ input_shape_data[node_name].update(params)
2397
+
2398
+ # Create all control connections
2399
+ control_connections = self._create_start_node_control_connections(
2400
+ request=request,
2401
+ start_node_uuid=start_node_uuid,
2402
+ node_name_to_uuid=node_name_to_uuid,
2403
+ )
2404
+ if isinstance(control_connections, PackageNodesAsSerializedFlowResultFailure):
2405
+ return control_connections
2406
+
2407
+ # Add control connections to the same list as data connections
2408
+ start_to_package_connections.extend(control_connections)
2409
+
2410
+ # Build complete SerializedNodeCommands for start node
2411
+ start_node_dependencies = NodeDependencies()
2412
+ start_node_dependencies.libraries.add(start_node_library_details)
2413
+
2414
+ start_node_commands = SerializedNodeCommands(
2415
+ create_node_command=start_create_node_command,
2416
+ element_modification_commands=start_node_parameter_commands,
2417
+ node_dependencies=start_node_dependencies,
2418
+ node_uuid=start_node_uuid,
2419
+ )
2420
+
2421
+ return PackagingStartNodeResult(
2422
+ start_node_commands=start_node_commands,
2423
+ start_to_package_connections=start_to_package_connections,
2424
+ input_shape_data=input_shape_data,
2425
+ start_node_parameter_value_commands=start_node_parameter_value_commands,
2426
+ )
2427
+
2428
+ def _create_start_node_parameters_and_connections_for_incoming_data( # noqa: PLR0913
2429
+ self,
2430
+ target_node_name: str,
2431
+ incoming_data_connections: list[IncomingConnection],
2432
+ output_parameter_prefix: str,
2433
+ start_node_name: str,
2434
+ start_node_uuid: SerializedNodeCommands.NodeUUID,
2435
+ start_create_node_command: CreateNodeRequest,
2436
+ node_name_to_uuid: dict[str, SerializedNodeCommands.NodeUUID],
2437
+ unique_parameter_uuid_to_values: dict[SerializedNodeCommands.UniqueParameterValueUUID, Any],
2438
+ serialized_parameter_value_tracker: SerializedParameterValueTracker,
2439
+ ) -> StartNodeIncomingDataResult | PackageNodesAsSerializedFlowResultFailure:
2440
+ """Create parameters and connections for incoming data connections to a specific target node."""
2441
+ start_node_parameter_commands = []
2442
+ start_to_package_connections = []
2443
+ start_node_parameter_value_commands = []
2444
+ input_shape_data: WorkflowShapeNodes = {}
2445
+
2446
+ for connection in incoming_data_connections:
2447
+ target_parameter_name = connection.target_parameter_name
2448
+
2449
+ # Create sanitized parameter name with prefix + node + parameter
2450
+ param_name = self._generate_sanitized_parameter_name(
2451
+ prefix=output_parameter_prefix,
2452
+ node_name=target_node_name,
2453
+ parameter_name=target_parameter_name,
2454
+ )
2455
+
2456
+ # Get the source node to determine parameter type (from the external connection)
2457
+ try:
2458
+ source_node = GriptapeNodes.NodeManager().get_node_by_name(connection.source_node_name)
2459
+ except ValueError as err:
2460
+ details = f"Attempted to package nodes as serialized flow. Failed because source node '{connection.source_node_name}' from incoming connection could not be found. Error: {err}."
2461
+ return PackageNodesAsSerializedFlowResultFailure(result_details=details)
2462
+
2463
+ # Get the source parameter
2464
+ source_param = source_node.get_parameter_by_name(connection.source_parameter_name)
2465
+ if not source_param:
2466
+ details = f"Attempted to package nodes as serialized flow. Failed because source parameter '{connection.source_parameter_name}' on node '{connection.source_node_name}' from incoming connection could not be found."
2467
+ return PackageNodesAsSerializedFlowResultFailure(result_details=details)
2468
+
2469
+ # Extract parameter shape info for workflow shape (inputs from external sources)
2470
+ param_shape_info = GriptapeNodes.WorkflowManager().extract_parameter_shape_info(
2471
+ source_param, include_control_params=True
2472
+ )
2473
+ if param_shape_info is not None:
2474
+ if start_node_name not in input_shape_data:
2475
+ input_shape_data[start_node_name] = {}
2476
+ input_shape_data[start_node_name][param_name] = param_shape_info
2477
+
2478
+ # Extract parameter value from source node to set on start node
2479
+ param_value_commands = GriptapeNodes.NodeManager().handle_parameter_value_saving(
2480
+ parameter=source_param,
2481
+ node=source_node,
2482
+ unique_parameter_uuid_to_values=unique_parameter_uuid_to_values,
2483
+ serialized_parameter_value_tracker=serialized_parameter_value_tracker,
2484
+ create_node_request=start_create_node_command,
2485
+ )
2486
+ if param_value_commands is not None:
2487
+ # Modify each command to target the start node parameter instead
2488
+ for param_value_command in param_value_commands:
2489
+ param_value_command.set_parameter_value_command.node_name = start_node_name
2490
+ param_value_command.set_parameter_value_command.parameter_name = param_name
2491
+ start_node_parameter_value_commands.append(param_value_command)
2492
+
2493
+ # Create parameter command for start node (following single-node pattern exactly)
2494
+ add_param_request = AddParameterToNodeRequest(
2495
+ node_name=start_node_name,
2496
+ parameter_name=param_name,
2497
+ type=source_param.output_type,
2498
+ default_value=None,
2499
+ tooltip=f"Parameter {target_parameter_name} from node {target_node_name} in packaged flow",
2500
+ initial_setup=True,
2501
+ )
2502
+ start_node_parameter_commands.append(add_param_request)
2503
+
2504
+ # Get target node UUID from mapping
2505
+ target_node_uuid = node_name_to_uuid.get(target_node_name)
2506
+ if target_node_uuid is None:
2507
+ details = f"Attempted to package nodes as serialized flow. Failed because target node '{target_node_name}' UUID not found in mapping."
2508
+ return PackageNodesAsSerializedFlowResultFailure(result_details=details)
2509
+
2510
+ # Create connection from start node to target node
2511
+ start_to_package_connections.append(
2512
+ SerializedFlowCommands.IndirectConnectionSerialization(
2513
+ source_node_uuid=start_node_uuid,
2514
+ source_parameter_name=param_name,
2515
+ target_node_uuid=target_node_uuid,
2516
+ target_parameter_name=target_parameter_name,
2517
+ )
2518
+ )
2519
+
2520
+ return StartNodeIncomingDataResult(
2521
+ parameter_commands=start_node_parameter_commands,
2522
+ data_connections=start_to_package_connections,
2523
+ input_shape_data=input_shape_data,
2524
+ parameter_value_commands=start_node_parameter_value_commands,
2525
+ )
2526
+
2527
+ def _create_start_node_control_connections(
2528
+ self,
2529
+ request: PackageNodesAsSerializedFlowRequest,
2530
+ start_node_uuid: SerializedNodeCommands.NodeUUID,
2531
+ node_name_to_uuid: dict[str, SerializedNodeCommands.NodeUUID],
2532
+ ) -> list[SerializedFlowCommands.IndirectConnectionSerialization] | PackageNodesAsSerializedFlowResultFailure:
2533
+ """Create control connection from start node to entry control node.
2534
+
2535
+ Args:
2536
+ request: The packaging request with control flow configuration
2537
+ start_node_uuid: UUID of the start node
2538
+ node_name_to_uuid: Mapping of node names to UUIDs for lookup
2539
+
2540
+ Returns:
2541
+ List of control connections or failure result
2542
+ """
2543
+ control_connections = []
2544
+
2545
+ # Connect start node to specified entry control node (if specified)
2546
+ if request.entry_control_node_name:
2547
+ # Get the entry node (already validated to exist)
2548
+ entry_node = GriptapeNodes.NodeManager().get_node_by_name(request.entry_control_node_name)
2549
+ entry_node_uuid = node_name_to_uuid[request.entry_control_node_name]
2550
+
2551
+ # Connect start node to specified entry control node
2552
+ control_connection_result = self._create_start_node_control_connection(
2553
+ entry_control_parameter_name=request.entry_control_parameter_name,
2554
+ start_node_uuid=start_node_uuid,
2555
+ package_node_uuid=entry_node_uuid,
2556
+ package_node=entry_node,
2557
+ )
2558
+ if isinstance(control_connection_result, PackageNodeAsSerializedFlowResultFailure):
2559
+ return PackageNodesAsSerializedFlowResultFailure(
2560
+ result_details=control_connection_result.result_details
2561
+ )
2562
+
2563
+ control_connections.append(control_connection_result)
2564
+
2565
+ return control_connections
2566
+
2567
+ def _create_start_node_control_connection(
2568
+ self,
2569
+ entry_control_parameter_name: str | None,
2570
+ start_node_uuid: SerializedNodeCommands.NodeUUID,
2571
+ package_node_uuid: SerializedNodeCommands.NodeUUID,
2572
+ package_node: BaseNode,
2573
+ ) -> SerializedFlowCommands.IndirectConnectionSerialization | PackageNodeAsSerializedFlowResultFailure:
2574
+ """Create control flow connection from start node to package node.
2575
+
2576
+ Connects the start node's first control output to the specified or first available package node control input.
2577
+ """
2578
+ if entry_control_parameter_name is not None:
2579
+ # Case 1: Specific entry parameter name provided
2580
+ package_control_input_name = entry_control_parameter_name
2581
+ else:
2582
+ # Case 2: Find the first available control input parameter
2583
+ package_control_input_name = None
2584
+ for param in package_node.parameters:
2585
+ if ParameterTypeBuiltin.CONTROL_TYPE.value in param.input_types:
2586
+ package_control_input_name = param.name
2587
+ logger.warning(
2588
+ "No entry_control_parameter_name specified for packaging node '%s'. "
2589
+ "Using first available control input parameter: '%s'",
2590
+ package_node.name,
2591
+ package_control_input_name,
2592
+ )
2593
+ break
2594
+
2595
+ if package_control_input_name is None:
2596
+ details = f"Attempted to package node '{package_node.name}'. Failed because no control input parameters found on the node, so cannot create control flow connection."
2597
+ return PackageNodeAsSerializedFlowResultFailure(result_details=details)
2598
+
2599
+ # StartNode always has a control output parameter with name "exec_out"
2600
+ source_control_parameter_name = "exec_out"
2601
+
2602
+ # Create the connection
2603
+ control_connection = SerializedFlowCommands.IndirectConnectionSerialization(
2604
+ source_node_uuid=start_node_uuid,
2605
+ source_parameter_name=source_control_parameter_name,
2606
+ target_node_uuid=package_node_uuid,
2607
+ target_parameter_name=package_control_input_name,
2608
+ )
2609
+ return control_connection
2610
+
2611
+ def _collect_all_connections_for_multi_node_package(
2612
+ self,
2613
+ start_node_result: PackagingStartNodeResult,
2614
+ end_node_packaging_result: PackagingEndNodeResult,
2615
+ packaged_nodes_internal_connections: list[SerializedFlowCommands.IndirectConnectionSerialization],
2616
+ ) -> list[SerializedFlowCommands.IndirectConnectionSerialization]:
2617
+ """Collect all connections for the multi-node packaged flow.
2618
+
2619
+ Returns a list containing:
2620
+ 1. Start node connections (data + control)
2621
+ 2. End node connections (data)
2622
+ 3. Internal package node connections
2623
+
2624
+ Args:
2625
+ start_node_result: Result containing start node and its connections
2626
+ end_node_packaging_result: Result containing end node and its connections
2627
+ packaged_nodes_internal_connections: Internal connections between package nodes
2628
+
2629
+ Returns:
2630
+ List of all connections for the packaged flow
2631
+ """
2632
+ all_connections = []
2633
+
2634
+ # Add start and end node connections
2635
+ all_connections.extend(start_node_result.start_to_package_connections)
2636
+ all_connections.extend(end_node_packaging_result.package_to_end_connections)
2637
+
2638
+ # Add internal package node connections
2639
+ all_connections.extend(packaged_nodes_internal_connections)
2640
+
2641
+ return all_connections
2642
+
2643
+ def _generate_sanitized_parameter_name(self, prefix: str, node_name: str, parameter_name: str) -> str:
2644
+ """Generate a sanitized parameter name for multi-node packaging.
2645
+
2646
+ Creates a parameter name in the format: prefix + sanitized_node_name + _ + parameter_name
2647
+ Node names are sanitized by replacing spaces and dots with underscores.
2648
+
2649
+ Args:
2650
+ prefix: Prefix for the parameter name (e.g., "packaged_node_")
2651
+ node_name: Original node name (may contain spaces, dots, etc.)
2652
+ parameter_name: Original parameter name
2653
+
2654
+ Returns:
2655
+ Sanitized parameter name safe for use (e.g., "packaged_node_Merge_Texts_merge_string")
2656
+ """
2657
+ sanitized_node_name = node_name.replace(" ", "_").replace(".", "_")
2658
+ return f"{prefix}{sanitized_node_name}_{parameter_name}"
2659
+
2660
+ async def on_start_flow_request(self, request: StartFlowRequest) -> ResultPayload: # noqa: C901, PLR0911, PLR0912
2661
+ # which flow
2662
+ flow_name = request.flow_name
2663
+ if not flow_name:
2664
+ details = "Must provide flow name to start a flow."
2665
+
2666
+ return StartFlowResultFailure(validation_exceptions=[], result_details=details)
2667
+ # get the flow by ID
2668
+ try:
2669
+ flow = self.get_flow_by_name(flow_name)
2670
+ except KeyError as err:
2671
+ details = f"Cannot start flow. Error: {err}"
2672
+ return StartFlowResultFailure(validation_exceptions=[err], result_details=details)
2673
+ # Check to see if the flow is already running.
2674
+ if self.check_for_existing_running_flow():
2675
+ details = "Cannot start flow. Flow is already running."
2676
+ return StartFlowResultFailure(validation_exceptions=[], result_details=details)
2677
+ # A node has been provided to either start or to run up to.
2678
+ if request.flow_node_name:
2679
+ flow_node_name = request.flow_node_name
2680
+ flow_node = GriptapeNodes.ObjectManager().attempt_get_object_by_name_as_type(flow_node_name, BaseNode)
2681
+ if not flow_node:
2682
+ details = f"Provided node with name {flow_node_name} does not exist"
2683
+ return StartFlowResultFailure(validation_exceptions=[], result_details=details)
2684
+ # lets get the first control node in the flow!
2685
+ start_node = self.get_start_node_from_node(flow, flow_node)
2686
+ # if the start is not the node provided, set a breakpoint at the stop (we're running up until there)
2687
+ if not start_node:
2688
+ details = f"Start node for node with name {flow_node_name} does not exist"
2689
+ return StartFlowResultFailure(validation_exceptions=[], result_details=details)
2690
+ if start_node != flow_node:
2691
+ flow_node.stop_flow = True
2692
+ else:
2693
+ # we wont hit this if we dont have a request id, our requests always have nodes
2694
+ # If there is a request, reinitialize the queue
2695
+ self.get_start_node_queue() # initialize the start flow queue!
2696
+ start_node = None
2697
+ # Run Validation before starting a flow
2698
+ result = await self.on_validate_flow_dependencies_request(
2699
+ ValidateFlowDependenciesRequest(flow_name=flow_name, flow_node_name=start_node.name if start_node else None)
2700
+ )
2701
+ try:
2702
+ if not result.succeeded():
2703
+ details = f"Couldn't start flow with name {flow_name}. Flow Validation Failed"
2704
+ return StartFlowResultFailure(validation_exceptions=[], result_details=details)
2705
+ result = cast("ValidateFlowDependenciesResultSuccess", result)
2706
+
2707
+ if not result.validation_succeeded:
2708
+ details = f"Couldn't start flow with name {flow_name}. Flow Validation Failed."
2709
+ if len(result.exceptions) > 0:
2710
+ for exception in result.exceptions:
2711
+ details = f"{details}\n\t{exception}"
2712
+ return StartFlowResultFailure(validation_exceptions=result.exceptions, result_details=details)
2713
+ except Exception as e:
2714
+ details = f"Couldn't start flow with name {flow_name}. Flow Validation Failed: {e}"
2715
+ return StartFlowResultFailure(validation_exceptions=[e], result_details=details)
2716
+ # By now, it has been validated with no exceptions.
2717
+ try:
2718
+ await self.start_flow(
2719
+ flow,
2720
+ start_node,
2721
+ debug_mode=request.debug_mode,
2722
+ pickle_control_flow_result=request.pickle_control_flow_result,
2723
+ )
2724
+ except Exception as e:
2725
+ details = f"Failed to kick off flow with name {flow_name}. Exception occurred: {e} "
2726
+ return StartFlowResultFailure(validation_exceptions=[e], result_details=details)
2727
+
2728
+ details = f"Successfully kicked off flow with name {flow_name}"
2729
+
2730
+ return StartFlowResultSuccess(result_details=details)
2731
+
2732
+ def on_get_flow_state_request(self, event: GetFlowStateRequest) -> ResultPayload:
2733
+ flow_name = event.flow_name
2734
+ if not flow_name:
2735
+ details = "Could not get flow state. No flow name was provided."
2736
+ return GetFlowStateResultFailure(result_details=details)
2737
+ try:
2738
+ flow = self.get_flow_by_name(flow_name)
2739
+ except KeyError as err:
2740
+ details = f"Could not get flow state. Error: {err}"
2741
+ return GetFlowStateResultFailure(result_details=details)
2742
+ try:
2743
+ control_nodes, resolving_nodes = self.flow_state(flow)
2744
+ except Exception as e:
2745
+ details = f"Failed to get flow state of flow with name {flow_name}. Exception occurred: {e} "
2746
+ logger.exception(details)
2747
+ return GetFlowStateResultFailure(result_details=details)
2748
+ details = f"Successfully got flow state for flow with name {flow_name}."
2749
+ return GetFlowStateResultSuccess(
2750
+ control_nodes=control_nodes,
2751
+ resolving_node=resolving_nodes,
2752
+ result_details=details,
2753
+ )
2754
+
2755
+ async def on_cancel_flow_request(self, request: CancelFlowRequest) -> ResultPayload:
2756
+ flow_name = request.flow_name
2757
+ if not flow_name:
2758
+ details = "Could not cancel flow execution. No flow name was provided."
2759
+
2760
+ return CancelFlowResultFailure(result_details=details)
2761
+ try:
2762
+ self.get_flow_by_name(flow_name)
2763
+ except KeyError as err:
2764
+ details = f"Could not cancel flow execution. Error: {err}"
2765
+
2766
+ return CancelFlowResultFailure(result_details=details)
2767
+ try:
2768
+ await self.cancel_flow_run()
2769
+ except Exception as e:
2770
+ details = f"Could not cancel flow execution. Exception: {e}"
2771
+
2772
+ return CancelFlowResultFailure(result_details=details)
2773
+ details = f"Successfully cancelled flow execution with name {flow_name}"
2774
+
2775
+ return CancelFlowResultSuccess(result_details=details)
2776
+
2777
+ async def on_single_node_step_request(self, request: SingleNodeStepRequest) -> ResultPayload:
2778
+ flow_name = request.flow_name
2779
+ if not flow_name:
2780
+ details = "Could not advance to the next step of a running workflow. No flow name was provided."
2781
+
2782
+ return SingleNodeStepResultFailure(validation_exceptions=[], result_details=details)
2783
+ try:
2784
+ self.get_flow_by_name(flow_name)
2785
+ except KeyError as err:
2786
+ details = f"Could not advance to the next step of a running workflow. No flow with name {flow_name} exists. Error: {err}"
2787
+
2788
+ return SingleNodeStepResultFailure(validation_exceptions=[err], result_details=details)
2789
+ try:
2790
+ flow = self.get_flow_by_name(flow_name)
2791
+ await self.single_node_step(flow)
2792
+ except Exception as e:
2793
+ details = f"Could not advance to the next step of a running workflow. Exception: {e}"
2794
+ return SingleNodeStepResultFailure(validation_exceptions=[], result_details=details)
2795
+
2796
+ # All completed happily
2797
+ details = f"Successfully advanced to the next step of a running workflow with name {flow_name}"
2798
+
2799
+ return SingleNodeStepResultSuccess(result_details=details)
2800
+
2801
+ async def on_single_execution_step_request(self, request: SingleExecutionStepRequest) -> ResultPayload:
2802
+ flow_name = request.flow_name
2803
+ if not flow_name:
2804
+ details = "Could not advance to the next step of a running workflow. No flow name was provided."
2805
+
2806
+ return SingleExecutionStepResultFailure(result_details=details)
2807
+ try:
2808
+ flow = self.get_flow_by_name(flow_name)
2809
+ except KeyError as err:
1813
2810
  details = f"Could not advance to the next step of a running workflow. Error: {err}."
1814
2811
 
1815
2812
  return SingleExecutionStepResultFailure(result_details=details)
@@ -1820,7 +2817,7 @@ class FlowManager:
1820
2817
  # We REALLY don't want to fail here, else we'll take the whole engine down
1821
2818
  try:
1822
2819
  if self.check_for_existing_running_flow():
1823
- self.cancel_flow_run()
2820
+ await self.cancel_flow_run()
1824
2821
  except Exception as e_inner:
1825
2822
  details = f"Could not cancel flow execution. Exception: {e_inner}"
1826
2823
 
@@ -1941,6 +2938,38 @@ class FlowManager:
1941
2938
 
1942
2939
  return aggregated_deps
1943
2940
 
2941
+ def _aggregate_node_types_used(
2942
+ self, serialized_node_commands: list[SerializedNodeCommands], sub_flows_commands: list[SerializedFlowCommands]
2943
+ ) -> set[LibraryNameAndNodeType]:
2944
+ """Aggregate node types used from nodes and sub-flows.
2945
+
2946
+ Args:
2947
+ serialized_node_commands: List of serialized node commands to aggregate from
2948
+ sub_flows_commands: List of sub-flow commands to aggregate from
2949
+
2950
+ Returns:
2951
+ Set of LibraryNameAndNodeType with all node types used
2952
+
2953
+ Raises:
2954
+ ValueError: If a node command has no library name specified
2955
+ """
2956
+ node_types_used: set[LibraryNameAndNodeType] = set()
2957
+
2958
+ # Collect node types from all nodes in this flow
2959
+ for node_cmd in serialized_node_commands:
2960
+ node_type = node_cmd.create_node_command.node_type
2961
+ library_name = node_cmd.create_node_command.specific_library_name
2962
+ if library_name is None:
2963
+ msg = f"Node type '{node_type}' has no library name specified during serialization"
2964
+ raise ValueError(msg)
2965
+ node_types_used.add(LibraryNameAndNodeType(library_name=library_name, node_type=node_type))
2966
+
2967
+ # Aggregate node types from all sub-flows
2968
+ for sub_flow_cmd in sub_flows_commands:
2969
+ node_types_used.update(sub_flow_cmd.node_types_used)
2970
+
2971
+ return node_types_used
2972
+
1944
2973
  # TODO: https://github.com/griptape-ai/griptape-nodes/issues/861
1945
2974
  # similar manager refactors: https://github.com/griptape-ai/griptape-nodes/issues/806
1946
2975
  def on_serialize_flow_to_commands(self, request: SerializeFlowToCommandsRequest) -> ResultPayload: # noqa: C901, PLR0911, PLR0912, PLR0915
@@ -2086,6 +3115,7 @@ class FlowManager:
2086
3115
  set_lock_commands_per_node={},
2087
3116
  sub_flows_commands=[],
2088
3117
  node_dependencies=sub_flow_dependencies,
3118
+ node_types_used=set(),
2089
3119
  )
2090
3120
  sub_flow_commands.append(serialized_flow)
2091
3121
  else:
@@ -2102,6 +3132,13 @@ class FlowManager:
2102
3132
  # Aggregate all dependencies from nodes and sub-flows
2103
3133
  aggregated_dependencies = self._aggregate_flow_dependencies(serialized_node_commands, sub_flow_commands)
2104
3134
 
3135
+ # Aggregate all node types used from nodes and sub-flows
3136
+ try:
3137
+ aggregated_node_types_used = self._aggregate_node_types_used(serialized_node_commands, sub_flow_commands)
3138
+ except ValueError as e:
3139
+ details = f"Attempted to serialize Flow '{flow_name}' to commands. Failed while aggregating node types: {e}"
3140
+ return SerializeFlowToCommandsResultFailure(result_details=details)
3141
+
2105
3142
  serialized_flow = SerializedFlowCommands(
2106
3143
  flow_initialization_command=create_flow_request,
2107
3144
  serialized_node_commands=serialized_node_commands,
@@ -2111,6 +3148,7 @@ class FlowManager:
2111
3148
  set_lock_commands_per_node=set_lock_commands_per_node,
2112
3149
  sub_flows_commands=sub_flow_commands,
2113
3150
  node_dependencies=aggregated_dependencies,
3151
+ node_types_used=aggregated_node_types_used,
2114
3152
  )
2115
3153
  details = f"Successfully serialized Flow '{flow_name}' into commands."
2116
3154
  result = SerializeFlowToCommandsResultSuccess(serialized_flow_commands=serialized_flow, result_details=details)
@@ -2258,6 +3296,7 @@ class FlowManager:
2258
3296
  start_node: BaseNode | None = None,
2259
3297
  *,
2260
3298
  debug_mode: bool = False,
3299
+ pickle_control_flow_result: bool = False,
2261
3300
  ) -> None:
2262
3301
  if self.check_for_existing_running_flow():
2263
3302
  # If flow already exists, throw an error
@@ -2274,13 +3313,15 @@ class FlowManager:
2274
3313
 
2275
3314
  # Initialize global control flow machine and DAG builder
2276
3315
 
2277
- self._global_control_flow_machine = ControlFlowMachine(flow.name)
3316
+ self._global_control_flow_machine = ControlFlowMachine(
3317
+ flow.name, pickle_control_flow_result=pickle_control_flow_result
3318
+ )
2278
3319
  # Set off the request here.
2279
3320
  try:
2280
3321
  await self._global_control_flow_machine.start_flow(start_node, debug_mode)
2281
3322
  except Exception:
2282
3323
  if self.check_for_existing_running_flow():
2283
- self.cancel_flow_run()
3324
+ await self.cancel_flow_run()
2284
3325
  raise
2285
3326
  GriptapeNodes.EventManager().put_event(
2286
3327
  ExecutionGriptapeNodeEvent(wrapped_event=ExecutionEvent(payload=InvolvedNodesEvent(involved_nodes=[])))
@@ -2298,11 +3339,16 @@ class FlowManager:
2298
3339
  and self._global_control_flow_machine.context.resolution_machine.is_started()
2299
3340
  )
2300
3341
 
2301
- def cancel_flow_run(self) -> None:
3342
+ async def cancel_flow_run(self) -> None:
2302
3343
  if not self.check_for_existing_running_flow():
2303
3344
  errormsg = "Flow has not yet been started. Cannot cancel flow that hasn't begun."
2304
3345
  raise RuntimeError(errormsg)
2305
3346
  self._global_flow_queue.queue.clear()
3347
+
3348
+ # Request cancellation on all nodes and wait for them to complete
3349
+ if self._global_control_flow_machine is not None:
3350
+ await self._global_control_flow_machine.cancel_flow()
3351
+
2306
3352
  # Reset control flow machine
2307
3353
  if self._global_control_flow_machine is not None:
2308
3354
  self._global_control_flow_machine.reset_machine(cancel=True)
@@ -2429,8 +3475,8 @@ class FlowManager:
2429
3475
  except Exception as e:
2430
3476
  logger.exception("Exception during single node resolution")
2431
3477
  if self.check_for_existing_running_flow():
2432
- self.cancel_flow_run()
2433
- raise RuntimeError(e) from e
3478
+ await self.cancel_flow_run()
3479
+ raise RuntimeError(e) from e
2434
3480
  if resolution_machine.is_complete():
2435
3481
  self._global_single_node_resolution = False
2436
3482
  self._global_control_flow_machine.context.current_nodes = []