griptape-nodes 0.63.10__py3-none-any.whl → 0.64.1__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 (32) hide show
  1. griptape_nodes/common/node_executor.py +95 -171
  2. griptape_nodes/exe_types/connections.py +51 -2
  3. griptape_nodes/exe_types/flow.py +3 -3
  4. griptape_nodes/exe_types/node_types.py +330 -202
  5. griptape_nodes/exe_types/param_components/artifact_url/__init__.py +1 -0
  6. griptape_nodes/exe_types/param_components/artifact_url/public_artifact_url_parameter.py +155 -0
  7. griptape_nodes/exe_types/param_components/progress_bar_component.py +1 -1
  8. griptape_nodes/exe_types/param_types/parameter_string.py +27 -0
  9. griptape_nodes/machines/control_flow.py +64 -203
  10. griptape_nodes/machines/dag_builder.py +85 -238
  11. griptape_nodes/machines/parallel_resolution.py +9 -236
  12. griptape_nodes/machines/sequential_resolution.py +133 -11
  13. griptape_nodes/retained_mode/events/agent_events.py +2 -0
  14. griptape_nodes/retained_mode/events/flow_events.py +5 -6
  15. griptape_nodes/retained_mode/events/node_events.py +151 -1
  16. griptape_nodes/retained_mode/events/workflow_events.py +10 -0
  17. griptape_nodes/retained_mode/managers/agent_manager.py +33 -1
  18. griptape_nodes/retained_mode/managers/flow_manager.py +213 -290
  19. griptape_nodes/retained_mode/managers/library_manager.py +24 -7
  20. griptape_nodes/retained_mode/managers/node_manager.py +400 -77
  21. griptape_nodes/retained_mode/managers/version_compatibility_manager.py +113 -69
  22. griptape_nodes/retained_mode/managers/workflow_manager.py +45 -10
  23. griptape_nodes/servers/mcp.py +32 -0
  24. griptape_nodes/version_compatibility/versions/v0_63_8/__init__.py +1 -0
  25. griptape_nodes/version_compatibility/versions/v0_63_8/deprecated_nodegroup_parameters.py +105 -0
  26. {griptape_nodes-0.63.10.dist-info → griptape_nodes-0.64.1.dist-info}/METADATA +3 -1
  27. {griptape_nodes-0.63.10.dist-info → griptape_nodes-0.64.1.dist-info}/RECORD +31 -28
  28. griptape_nodes/version_compatibility/workflow_versions/__init__.py +0 -1
  29. /griptape_nodes/version_compatibility/{workflow_versions → versions}/v0_7_0/__init__.py +0 -0
  30. /griptape_nodes/version_compatibility/{workflow_versions → versions}/v0_7_0/local_executor_argument_addition.py +0 -0
  31. {griptape_nodes-0.63.10.dist-info → griptape_nodes-0.64.1.dist-info}/WHEEL +0 -0
  32. {griptape_nodes-0.63.10.dist-info → griptape_nodes-0.64.1.dist-info}/entry_points.txt +0 -0
@@ -20,7 +20,7 @@ from griptape_nodes.exe_types.node_types import (
20
20
  EndLoopNode,
21
21
  ErrorProxyNode,
22
22
  NodeDependencies,
23
- NodeGroupProxyNode,
23
+ NodeGroupNode,
24
24
  NodeResolutionState,
25
25
  StartLoopNode,
26
26
  )
@@ -60,15 +60,24 @@ from griptape_nodes.retained_mode.events.library_events import (
60
60
  GetLibraryMetadataResultSuccess,
61
61
  )
62
62
  from griptape_nodes.retained_mode.events.node_events import (
63
+ AddNodesToNodeGroupRequest,
64
+ AddNodesToNodeGroupResultFailure,
65
+ AddNodesToNodeGroupResultSuccess,
63
66
  BatchSetNodeMetadataRequest,
64
67
  BatchSetNodeMetadataResultFailure,
65
68
  BatchSetNodeMetadataResultSuccess,
66
69
  CanResetNodeToDefaultsRequest,
67
70
  CanResetNodeToDefaultsResultFailure,
68
71
  CanResetNodeToDefaultsResultSuccess,
72
+ CreateNodeGroupRequest,
73
+ CreateNodeGroupResultFailure,
74
+ CreateNodeGroupResultSuccess,
69
75
  CreateNodeRequest,
70
76
  CreateNodeResultFailure,
71
77
  CreateNodeResultSuccess,
78
+ DeleteNodeGroupRequest,
79
+ DeleteNodeGroupResultFailure,
80
+ DeleteNodeGroupResultSuccess,
72
81
  DeleteNodeRequest,
73
82
  DeleteNodeResultFailure,
74
83
  DeleteNodeResultSuccess,
@@ -96,6 +105,9 @@ from griptape_nodes.retained_mode.events.node_events import (
96
105
  ListParametersOnNodeRequest,
97
106
  ListParametersOnNodeResultFailure,
98
107
  ListParametersOnNodeResultSuccess,
108
+ RemoveNodeFromNodeGroupRequest,
109
+ RemoveNodeFromNodeGroupResultFailure,
110
+ RemoveNodeFromNodeGroupResultSuccess,
99
111
  ResetNodeToDefaultsRequest,
100
112
  ResetNodeToDefaultsResultFailure,
101
113
  ResetNodeToDefaultsResultSuccess,
@@ -200,6 +212,14 @@ class NodeManager:
200
212
  self._name_to_parent_flow_name = {}
201
213
 
202
214
  event_manager.assign_manager_to_request_type(CreateNodeRequest, self.on_create_node_request)
215
+ event_manager.assign_manager_to_request_type(CreateNodeGroupRequest, self.on_create_node_group_request)
216
+ event_manager.assign_manager_to_request_type(
217
+ AddNodesToNodeGroupRequest, self.on_add_nodes_to_node_group_request
218
+ )
219
+ event_manager.assign_manager_to_request_type(
220
+ RemoveNodeFromNodeGroupRequest, self.on_remove_node_from_node_group_request
221
+ )
222
+ event_manager.assign_manager_to_request_type(DeleteNodeGroupRequest, self.on_delete_node_group_request)
203
223
  event_manager.assign_manager_to_request_type(DeleteNodeRequest, self.on_delete_node_request)
204
224
  event_manager.assign_manager_to_request_type(
205
225
  GetNodeResolutionStateRequest, self.on_get_node_resolution_state_request
@@ -474,6 +494,288 @@ class NodeManager:
474
494
  result_details=ResultDetails(message=details, level=log_level),
475
495
  )
476
496
 
497
+ def on_create_node_group_request(self, request: CreateNodeGroupRequest) -> ResultPayload: # noqa: C901
498
+ """Handle CreateNodeGroupRequest to create a new NodeGroupNode."""
499
+ flow_name = request.flow_name
500
+ flow = None
501
+
502
+ if flow_name is None:
503
+ if not GriptapeNodes.ContextManager().has_current_flow():
504
+ details = "Attempted to create NodeGroup in the Current Context. Failed because the Current Context was empty."
505
+ return CreateNodeGroupResultFailure(result_details=details)
506
+ flow = GriptapeNodes.ContextManager().get_current_flow()
507
+ flow_name = flow.name
508
+
509
+ if flow is None:
510
+ flow_mgr = GriptapeNodes.FlowManager()
511
+ try:
512
+ flow = flow_mgr.get_flow_by_name(flow_name)
513
+ except KeyError as err:
514
+ details = f"Attempted to create NodeGroup. Failed when attempting to find the parent Flow. Error: {err}"
515
+ return CreateNodeGroupResultFailure(result_details=details)
516
+
517
+ requested_node_group_name = request.node_group_name
518
+ if requested_node_group_name is None:
519
+ requested_node_group_name = "NodeGroup"
520
+
521
+ obj_mgr = GriptapeNodes.ObjectManager()
522
+ final_node_group_name = obj_mgr.generate_name_for_object(
523
+ type_name="NodeGroupNode", requested_name=requested_node_group_name
524
+ )
525
+
526
+ try:
527
+ # Create metadata with required keys for serialization
528
+ metadata = request.metadata if request.metadata else {}
529
+ metadata["node_type"] = "NodeGroupNode"
530
+
531
+ node_group = NodeGroupNode(name=final_node_group_name, metadata=metadata)
532
+ except Exception as err:
533
+ details = f"Could not create NodeGroup '{final_node_group_name}': {err}"
534
+ return CreateNodeGroupResultFailure(result_details=details)
535
+
536
+ if request.node_names_to_add:
537
+ nodes_to_add = []
538
+ for node_name in request.node_names_to_add:
539
+ try:
540
+ node = self.get_node_by_name(node_name)
541
+ except KeyError:
542
+ details = f"Attempted to add node '{node_name}' to NodeGroup '{final_node_group_name}'. Failed because node was not found."
543
+ return CreateNodeGroupResultFailure(result_details=details)
544
+ nodes_to_add.append(node)
545
+ # Add Nodes manually here, so we don't have to add the NodeGroup and remove it if it fails.
546
+ try:
547
+ node_group.add_nodes_to_group(nodes_to_add)
548
+ except Exception:
549
+ details = f"Failed to add nodes to NodeGroup '{final_node_group_name}'."
550
+ return CreateNodeGroupResultFailure(result_details=details)
551
+ flow.add_node(node_group)
552
+ obj_mgr.add_object_by_name(node_group.name, node_group)
553
+ self._name_to_parent_flow_name[node_group.name] = flow_name
554
+ if request.flow_name is None:
555
+ details = (
556
+ f"Successfully created NodeGroup '{final_node_group_name}' in the Current Context (Flow '{flow_name}')"
557
+ )
558
+ else:
559
+ details = f"Successfully created NodeGroup '{final_node_group_name}' in Flow '{flow_name}'"
560
+
561
+ return CreateNodeGroupResultSuccess(
562
+ node_group_name=node_group.name, result_details=ResultDetails(message=details, level=logging.DEBUG)
563
+ )
564
+
565
+ def _get_flow_for_node_group_operation(self, flow_name: str | None) -> AddNodesToNodeGroupResultFailure | None:
566
+ """Get the flow for a node group operation."""
567
+ if flow_name is None:
568
+ if not GriptapeNodes.ContextManager().has_current_flow():
569
+ details = "Attempted to add node to NodeGroup in the Current Context. Failed because the Current Context was empty."
570
+ return AddNodesToNodeGroupResultFailure(result_details=details)
571
+ else:
572
+ try:
573
+ GriptapeNodes.FlowManager().get_flow_by_name(flow_name)
574
+ except KeyError as err:
575
+ details = (
576
+ f"Attempted to add node to NodeGroup. Failed when attempting to find the parent Flow. Error: {err}"
577
+ )
578
+ return AddNodesToNodeGroupResultFailure(result_details=details)
579
+ return None
580
+
581
+ def _get_nodes_for_group_operation(
582
+ self, node_names: list[str], node_group_name: str
583
+ ) -> list[BaseNode] | AddNodesToNodeGroupResultFailure:
584
+ """Get the list of nodes to add to a group.
585
+
586
+ Collects all errors and returns them together if multiple nodes fail.
587
+ """
588
+ obj_mgr = GriptapeNodes.ObjectManager()
589
+ nodes = []
590
+ errors = []
591
+
592
+ for node_name in node_names:
593
+ try:
594
+ node = obj_mgr.get_object_by_name(node_name)
595
+ except KeyError:
596
+ errors.append(f"Node '{node_name}' was not found")
597
+ continue
598
+
599
+ if not isinstance(node, BaseNode):
600
+ errors.append(f"'{node_name}' is not a node")
601
+ continue
602
+
603
+ nodes.append(node)
604
+
605
+ if errors:
606
+ details = f"Attempted to add nodes to NodeGroup '{node_group_name}'. Failed for the following nodes: {'; '.join(errors)}"
607
+ return AddNodesToNodeGroupResultFailure(result_details=details)
608
+
609
+ return nodes
610
+
611
+ def _get_node_group(
612
+ self, node_group_name: str, node_names: list[str]
613
+ ) -> NodeGroupNode | AddNodesToNodeGroupResultFailure:
614
+ """Get the NodeGroup node."""
615
+ try:
616
+ node_group = GriptapeNodes.ObjectManager().get_object_by_name(node_group_name)
617
+ except KeyError:
618
+ details = f"Attempted to add nodes '{node_names}' to NodeGroup '{node_group_name}'. Failed because NodeGroup was not found."
619
+ return AddNodesToNodeGroupResultFailure(result_details=details)
620
+
621
+ if not isinstance(node_group, NodeGroupNode):
622
+ details = f"Attempted to add nodes '{node_names}' to '{node_group_name}'. Failed because '{node_group_name}' is not a NodeGroup."
623
+ return AddNodesToNodeGroupResultFailure(result_details=details)
624
+
625
+ return node_group
626
+
627
+ def on_add_nodes_to_node_group_request(self, request: AddNodesToNodeGroupRequest) -> ResultPayload:
628
+ """Handle AddNodeToNodeGroupRequest to add a node to an existing NodeGroup."""
629
+ flow_result = self._get_flow_for_node_group_operation(request.flow_name)
630
+ if isinstance(flow_result, AddNodesToNodeGroupResultFailure):
631
+ return flow_result
632
+
633
+ nodes_result = self._get_nodes_for_group_operation(request.node_names, request.node_group_name)
634
+ if isinstance(nodes_result, AddNodesToNodeGroupResultFailure):
635
+ return nodes_result
636
+ nodes = nodes_result
637
+
638
+ node_group_result = self._get_node_group(request.node_group_name, request.node_names)
639
+ if isinstance(node_group_result, AddNodesToNodeGroupResultFailure):
640
+ return node_group_result
641
+ node_group = node_group_result
642
+
643
+ try:
644
+ node_group.add_nodes_to_group(nodes)
645
+ except Exception as err:
646
+ details = f"Attempted to add node '{request.node_names}' to NodeGroup '{request.node_group_name}'. Failed with error: {err}"
647
+ return AddNodesToNodeGroupResultFailure(result_details=details)
648
+
649
+ details = f"Successfully added node '{request.node_names}' to NodeGroup '{request.node_group_name}'"
650
+ return AddNodesToNodeGroupResultSuccess(
651
+ result_details=ResultDetails(message=details, level=logging.DEBUG),
652
+ )
653
+
654
+ def _get_flow_for_remove_operation(self, flow_name: str | None) -> RemoveNodeFromNodeGroupResultFailure | None:
655
+ """Get the flow for a remove node from group operation."""
656
+ if flow_name is None:
657
+ if not GriptapeNodes.ContextManager().has_current_flow():
658
+ details = "Attempted to remove nodes from NodeGroup in the Current Context. Failed because the Current Context was empty."
659
+ return RemoveNodeFromNodeGroupResultFailure(result_details=details)
660
+ else:
661
+ try:
662
+ GriptapeNodes.FlowManager().get_flow_by_name(flow_name)
663
+ except KeyError as err:
664
+ details = f"Attempted to remove nodes from NodeGroup. Failed when attempting to find the parent Flow. Error: {err}"
665
+ return RemoveNodeFromNodeGroupResultFailure(result_details=details)
666
+ return None
667
+
668
+ def _get_nodes_for_remove_operation(
669
+ self, node_names: list[str], node_group_name: str
670
+ ) -> list[BaseNode] | RemoveNodeFromNodeGroupResultFailure:
671
+ """Get the list of nodes to remove from a group.
672
+
673
+ Collects all errors and returns them together if multiple nodes fail.
674
+ """
675
+ obj_mgr = GriptapeNodes.ObjectManager()
676
+ nodes = []
677
+ errors = []
678
+
679
+ for node_name in node_names:
680
+ try:
681
+ node = obj_mgr.get_object_by_name(node_name)
682
+ except KeyError:
683
+ errors.append(f"Node '{node_name}' was not found")
684
+ continue
685
+
686
+ if not isinstance(node, BaseNode):
687
+ errors.append(f"'{node_name}' is not a node")
688
+ continue
689
+
690
+ nodes.append(node)
691
+
692
+ if errors:
693
+ details = f"Attempted to remove nodes from NodeGroup '{node_group_name}'. Failed for the following nodes: {'; '.join(errors)}"
694
+ return RemoveNodeFromNodeGroupResultFailure(result_details=details)
695
+
696
+ return nodes
697
+
698
+ def _get_node_group_for_remove(
699
+ self, node_group_name: str, node_names: list[str]
700
+ ) -> NodeGroupNode | RemoveNodeFromNodeGroupResultFailure:
701
+ """Get the NodeGroup node for remove operation."""
702
+ try:
703
+ node_group = GriptapeNodes.ObjectManager().get_object_by_name(node_group_name)
704
+ except KeyError:
705
+ details = f"Attempted to remove nodes '{node_names}' from NodeGroup '{node_group_name}'. Failed because NodeGroup was not found."
706
+ return RemoveNodeFromNodeGroupResultFailure(result_details=details)
707
+
708
+ if not isinstance(node_group, NodeGroupNode):
709
+ details = f"Attempted to remove nodes '{node_names}' from '{node_group_name}'. Failed because '{node_group_name}' is not a NodeGroup."
710
+ return RemoveNodeFromNodeGroupResultFailure(result_details=details)
711
+
712
+ return node_group
713
+
714
+ def on_remove_node_from_node_group_request(self, request: RemoveNodeFromNodeGroupRequest) -> ResultPayload:
715
+ """Handle RemoveNodeFromNodeGroupRequest to remove nodes from an existing NodeGroup."""
716
+ flow_result = self._get_flow_for_remove_operation(request.flow_name)
717
+ if isinstance(flow_result, RemoveNodeFromNodeGroupResultFailure):
718
+ return flow_result
719
+
720
+ nodes_result = self._get_nodes_for_remove_operation(request.node_names, request.node_group_name)
721
+ if isinstance(nodes_result, RemoveNodeFromNodeGroupResultFailure):
722
+ return nodes_result
723
+ nodes = nodes_result
724
+
725
+ node_group_result = self._get_node_group_for_remove(request.node_group_name, request.node_names)
726
+ if isinstance(node_group_result, RemoveNodeFromNodeGroupResultFailure):
727
+ return node_group_result
728
+ node_group = node_group_result
729
+
730
+ try:
731
+ node_group.remove_nodes_from_group(nodes)
732
+ except ValueError as err:
733
+ details = f"Attempted to remove nodes '{request.node_names}' from NodeGroup '{request.node_group_name}'. Failed with error: {err}"
734
+ return RemoveNodeFromNodeGroupResultFailure(result_details=details)
735
+
736
+ details = f"Successfully removed nodes '{request.node_names}' from NodeGroup '{request.node_group_name}'"
737
+ return RemoveNodeFromNodeGroupResultSuccess(
738
+ result_details=ResultDetails(message=details, level=logging.DEBUG),
739
+ )
740
+
741
+ def on_delete_node_group_request(self, request: DeleteNodeGroupRequest) -> ResultPayload:
742
+ """Handle DeleteNodeGroupRequest to delete a NodeGroup and remove all its nodes."""
743
+ # Get the NodeGroup
744
+ obj_mgr = GriptapeNodes.ObjectManager()
745
+ try:
746
+ node_group = obj_mgr.get_object_by_name(request.node_group_name)
747
+ except KeyError:
748
+ details = (
749
+ f"Attempted to delete NodeGroup '{request.node_group_name}'. Failed because NodeGroup was not found."
750
+ )
751
+ return DeleteNodeGroupResultFailure(result_details=details)
752
+
753
+ if not isinstance(node_group, NodeGroupNode):
754
+ details = (
755
+ f"Attempted to delete '{request.node_group_name}' as NodeGroup. Failed because it is not a NodeGroup."
756
+ )
757
+ return DeleteNodeGroupResultFailure(result_details=details)
758
+
759
+ # Remove all nodes from the group first
760
+ if node_group.nodes:
761
+ nodes_to_remove = list(node_group.nodes.values())
762
+ try:
763
+ node_group.remove_nodes_from_group(nodes_to_remove)
764
+ except ValueError as err:
765
+ details = f"Attempted to delete NodeGroup '{request.node_group_name}'. Failed to remove nodes from group: {err}"
766
+ return DeleteNodeGroupResultFailure(result_details=details)
767
+
768
+ # Now delete the NodeGroup node itself
769
+ delete_node_request = DeleteNodeRequest(node_name=request.node_group_name)
770
+ delete_result = self.on_delete_node_request(delete_node_request)
771
+
772
+ if delete_result.failed():
773
+ details = f"Attempted to delete NodeGroup '{request.node_group_name}'. Failed to delete the NodeGroup node: {delete_result.result_details}"
774
+ return DeleteNodeGroupResultFailure(result_details=details)
775
+
776
+ details = f"Successfully deleted NodeGroup '{request.node_group_name}'"
777
+ return DeleteNodeGroupResultSuccess(result_details=ResultDetails(message=details, level=logging.DEBUG))
778
+
477
779
  def cancel_conditionally(
478
780
  self, parent_flow: ControlFlow, parent_flow_name: str, node: BaseNode
479
781
  ) -> ResultPayload | None:
@@ -1610,6 +1912,14 @@ class NodeManager:
1610
1912
  details = f"Attempted to set parameter '{param_name}' value on node '{node_name}'. Failed because the Node was locked."
1611
1913
  return SetParameterValueResultFailure(result_details=details)
1612
1914
 
1915
+ # Let versioning system potentially squelch removed parameters.
1916
+ # This check must run BEFORE we validate parameter existence, since removed parameters won't exist.
1917
+ version_compat_result = GriptapeNodes.VersionCompatibilityManager().check_set_parameter_version_compatibility(
1918
+ node, param_name, request.value
1919
+ )
1920
+ if version_compat_result is not None:
1921
+ return version_compat_result
1922
+
1613
1923
  # Handle ErrorProxyNode parameter value requests
1614
1924
  if isinstance(node, ErrorProxyNode):
1615
1925
  if request.initial_setup:
@@ -1624,36 +1934,10 @@ class NodeManager:
1624
1934
  # Reject runtime parameter value changes on ErrorProxy
1625
1935
  details = f"Cannot set parameter '{param_name}' on placeholder node '{node_name}'. This placeholder preserves your workflow structure but doesn't allow parameter changes, as they could cause issues when the original node is restored."
1626
1936
  return SetParameterValueResultFailure(result_details=details)
1627
- elif isinstance(node, NodeGroupProxyNode):
1628
- # For NodeGroupProxyNode, set the value on both the proxy AND the original node
1629
- node.set_parameter_value(param_name, request.value)
1630
-
1631
- # Forward the value to the original node if this proxy parameter maps to one
1632
- result = None
1633
- if param_name in node._proxy_param_to_node_param:
1634
- original_node, original_param_name = node._proxy_param_to_node_param[param_name]
1635
- result = GriptapeNodes.handle_request(
1636
- SetParameterValueRequest(
1637
- parameter_name=original_param_name,
1638
- node_name=original_node.name,
1639
- value=request.value,
1640
- data_type=request.data_type,
1641
- incoming_connection_source_node_name=request.incoming_connection_source_node_name,
1642
- incoming_connection_source_parameter_name=request.incoming_connection_source_parameter_name,
1643
- )
1644
- )
1645
- logger.debug(
1646
- "Forwarded parameter value from proxy '%s.%s' to original '%s.%s'",
1647
- node.name,
1648
- param_name,
1649
- original_node.name,
1650
- original_param_name,
1651
- )
1652
- details = f"Attempted to set parameter value for '{node_name}.{param_name}'. Successfully set value on the NodeGroupProxyNode '{node_name}', but failed to set value on the original node'."
1653
- return result if result else SetParameterValueResultFailure(result_details=details)
1654
1937
 
1655
1938
  # Does the Parameter actually exist on the Node?
1656
1939
  parameter = node.get_parameter_by_name(param_name)
1940
+
1657
1941
  if parameter is None:
1658
1942
  details = f"Attempted to set parameter value for '{node_name}.{param_name}'. Failed because no parameter with that name could be found."
1659
1943
 
@@ -2144,55 +2428,81 @@ class NodeManager:
2144
2428
 
2145
2429
  # This is our current dude.
2146
2430
  with GriptapeNodes.ContextManager().node(node=node):
2147
- # Get the library and version details.
2148
- library_used = node.metadata["library"]
2149
- # Get the library metadata so we can get the version.
2150
- library_metadata_request = GetLibraryMetadataRequest(library=library_used)
2151
- # Call LibraryManager directly to avoid error toasts when library is unavailable (expected for ErrorProxyNode)
2152
- # Per https://github.com/griptape-ai/griptape-nodes/issues/1940
2153
- library_metadata_result = GriptapeNodes.LibraryManager().get_library_metadata_request(
2154
- library_metadata_request
2155
- )
2156
-
2157
- if not isinstance(library_metadata_result, GetLibraryMetadataResultSuccess):
2158
- if isinstance(node, ErrorProxyNode):
2159
- # For ErrorProxyNode, use descriptive message when original library unavailable
2160
- library_version = "<version unavailable; workflow was saved when library was unable to be loaded>"
2161
- library_details = LibraryNameAndVersion(library_name=library_used, library_version=library_version)
2162
- details = f"Serializing Node '{node_name}' (original type: {node.original_node_type}) with unavailable library '{library_used}'. Saving as ErrorProxy with placeholder version. Fix the missing library and reload the workflow to restore the original node."
2163
- logger.warning(details)
2164
- else:
2165
- # For regular nodes, this is still an error
2166
- details = f"Attempted to serialize Node '{node_name}' to commands. Failed to get metadata for library '{library_used}'."
2167
- return SerializeNodeToCommandsResultFailure(result_details=details)
2431
+ # Handle NodeGroupNode specially - skip library lookup entirely
2432
+ if isinstance(node, NodeGroupNode):
2433
+ # NodeGroupNode doesn't have a library dependency
2434
+ library_details = None
2168
2435
  else:
2169
- library_version = library_metadata_result.metadata.library_version
2170
- library_details = LibraryNameAndVersion(library_name=library_used, library_version=library_version)
2436
+ # Get the library and version details for regular nodes
2437
+ library_used = node.metadata["library"]
2438
+ # Get the library metadata so we can get the version.
2439
+ library_metadata_request = GetLibraryMetadataRequest(library=library_used)
2440
+ # Call LibraryManager directly to avoid error toasts when library is unavailable (expected for ErrorProxyNode)
2441
+ # Per https://github.com/griptape-ai/griptape-nodes/issues/1940
2442
+ library_metadata_result = GriptapeNodes.LibraryManager().get_library_metadata_request(
2443
+ library_metadata_request
2444
+ )
2171
2445
 
2172
- # Handle ErrorProxyNode serialization - serialize as original node type
2446
+ if not isinstance(library_metadata_result, GetLibraryMetadataResultSuccess):
2447
+ if isinstance(node, ErrorProxyNode):
2448
+ # For ErrorProxyNode, use descriptive message when original library unavailable
2449
+ library_version = (
2450
+ "<version unavailable; workflow was saved when library was unable to be loaded>"
2451
+ )
2452
+ library_details = LibraryNameAndVersion(
2453
+ library_name=library_used, library_version=library_version
2454
+ )
2455
+ details = f"Serializing Node '{node_name}' (original type: {node.original_node_type}) with unavailable library '{library_used}'. Saving as ErrorProxy with placeholder version. Fix the missing library and reload the workflow to restore the original node."
2456
+ logger.warning(details)
2457
+ else:
2458
+ # For regular nodes, this is still an error
2459
+ details = f"Attempted to serialize Node '{node_name}' to commands. Failed to get metadata for library '{library_used}'."
2460
+ return SerializeNodeToCommandsResultFailure(result_details=details)
2461
+ else:
2462
+ library_version = library_metadata_result.metadata.library_version
2463
+ library_details = LibraryNameAndVersion(library_name=library_used, library_version=library_version)
2173
2464
 
2174
- if isinstance(node, ErrorProxyNode):
2175
- serialized_node_type = node.original_node_type
2176
- serialized_library_name = node.original_library_name
2465
+ # Handle NodeGroupNode specially - emit CreateNodeGroupRequest instead
2466
+ if isinstance(node, NodeGroupNode):
2467
+ create_node_request = CreateNodeGroupRequest(
2468
+ node_group_name=node_name,
2469
+ node_names_to_add=list(node.nodes),
2470
+ metadata=copy.deepcopy(node.metadata),
2471
+ )
2177
2472
  else:
2178
- serialized_node_type = node.__class__.__name__
2179
- serialized_library_name = library_details.library_name
2473
+ # For non-NodeGroupNode, library_details should always be set
2474
+ if library_details is None:
2475
+ details = f"Attempted to serialize Node '{node_name}' to commands. Library details missing."
2476
+ return SerializeNodeToCommandsResultFailure(result_details=details)
2180
2477
 
2181
- # Get the creation details.
2182
- create_node_request = CreateNodeRequest(
2183
- node_type=serialized_node_type,
2184
- node_name=node_name,
2185
- specific_library_name=serialized_library_name,
2186
- metadata=copy.deepcopy(node.metadata),
2187
- # If it is actively resolving, mark as unresolved.
2188
- resolution=node.state.value,
2189
- initial_setup=True,
2190
- )
2478
+ # Handle ErrorProxyNode serialization - serialize as original node type
2479
+ if isinstance(node, ErrorProxyNode):
2480
+ serialized_node_type = node.original_node_type
2481
+ serialized_library_name = node.original_library_name
2482
+ else:
2483
+ serialized_node_type = node.__class__.__name__
2484
+ serialized_library_name = library_details.library_name
2485
+
2486
+ # Get the creation details for regular nodes
2487
+ create_node_request = CreateNodeRequest(
2488
+ node_type=serialized_node_type,
2489
+ node_name=node_name,
2490
+ specific_library_name=serialized_library_name,
2491
+ metadata=copy.deepcopy(node.metadata),
2492
+ # If it is actively resolving, mark as unresolved.
2493
+ resolution=node.state.value,
2494
+ initial_setup=True,
2495
+ )
2191
2496
 
2192
2497
  # We're going to compare this node instance vs. a canonical one. Rez that one up.
2193
2498
  # For ErrorProxyNode, we can't create a reference node, so skip comparison
2194
2499
  if isinstance(node, ErrorProxyNode):
2195
2500
  reference_node = None
2501
+ elif isinstance(node, NodeGroupNode):
2502
+ # For NodeGroupNode, create a fresh reference instance the same way we create NodeGroupNodes
2503
+ reference_node = NodeGroupNode(
2504
+ name="REFERENCE NODE", metadata={"library": "griptape_nodes", "node_type": "NodeGroupNode"}
2505
+ )
2196
2506
  else:
2197
2507
  reference_node = type(node)(name="REFERENCE NODE")
2198
2508
 
@@ -2255,6 +2565,7 @@ class NodeManager:
2255
2565
  )
2256
2566
  if set_param_value_requests is not None:
2257
2567
  set_value_commands.extend(set_param_value_requests)
2568
+
2258
2569
  # now check if locked
2259
2570
  if node.lock:
2260
2571
  lock_command = SetLockNodeStateRequest(node_name=None, lock=True)
@@ -2267,8 +2578,9 @@ class NodeManager:
2267
2578
  # Ensure we always have a NodeDependencies object, even if empty
2268
2579
  node_dependencies = NodeDependencies()
2269
2580
 
2270
- # Add the library dependency to the node dependencies
2271
- node_dependencies.libraries.add(library_details)
2581
+ # Add the library dependency to the node dependencies (if applicable)
2582
+ if library_details is not None:
2583
+ node_dependencies.libraries.add(library_details)
2272
2584
 
2273
2585
  # Hooray
2274
2586
  serialized_node_commands = SerializedNodeCommands(
@@ -2397,12 +2709,21 @@ class NodeManager:
2397
2709
  # Issue the creation command first.
2398
2710
  create_node_request = request.serialized_node_commands.create_node_command
2399
2711
  create_node_result = GriptapeNodes().handle_request(create_node_request)
2400
- if not isinstance(create_node_result, CreateNodeResultSuccess):
2401
- details = f"Attempted to deserialize a serialized set of Node Creation commands. Failed to create node '{create_node_request.node_name}'."
2712
+ if not isinstance(create_node_result, (CreateNodeResultSuccess, CreateNodeGroupResultSuccess)):
2713
+ req_node_name = (
2714
+ create_node_request.node_group_name
2715
+ if isinstance(create_node_request, CreateNodeGroupRequest)
2716
+ else create_node_request.node_name
2717
+ )
2718
+ details = f"Attempted to deserialize a serialized set of Node Creation commands. Failed to create node '{req_node_name}'."
2402
2719
  return DeserializeNodeFromCommandsResultFailure(result_details=details)
2403
2720
 
2404
2721
  # Adopt the newly-created node as our current context.
2405
- node_name = create_node_result.node_name
2722
+ node_name = (
2723
+ create_node_result.node_group_name
2724
+ if isinstance(create_node_result, CreateNodeGroupResultSuccess)
2725
+ else create_node_result.node_name
2726
+ )
2406
2727
  node = GriptapeNodes.ObjectManager().attempt_get_object_by_name_as_type(node_name, BaseNode)
2407
2728
  if node is None:
2408
2729
  details = f"Attempted to deserialize a serialized set of Node Creation commands. Failed to get node '{node_name}'."
@@ -2683,7 +3004,7 @@ class NodeManager:
2683
3004
  node: BaseNode,
2684
3005
  unique_parameter_uuid_to_values: dict[SerializedNodeCommands.UniqueParameterValueUUID, Any],
2685
3006
  serialized_parameter_value_tracker: SerializedParameterValueTracker,
2686
- create_node_request: CreateNodeRequest,
3007
+ create_node_request: CreateNodeRequest | CreateNodeGroupRequest,
2687
3008
  ) -> list[SerializedNodeCommands.IndirectSetParameterValueCommand] | None:
2688
3009
  """Generates code to save a parameter value for a node in a Griptape workflow.
2689
3010
 
@@ -2735,8 +3056,9 @@ class NodeManager:
2735
3056
  if internal_command is None:
2736
3057
  details = f"Attempted to serialize set value for parameter '{parameter.name}' on node '{node.name}'. The set value will not be restored in anything that attempts to deserialize or save this node. The value for this parameter was not serialized because it did not match Griptape Nodes' criteria for serializability. To remedy, either update the value's type to support serializability or mark the parameter as not serializable by setting serializable=False when creating the parameter."
2737
3058
  logger.warning(details)
2738
- # Set node to unresolved when serialization fails
2739
- create_node_request.resolution = NodeResolutionState.UNRESOLVED.value
3059
+ # Set node to unresolved when serialization fails (only for CreateNodeRequest)
3060
+ if isinstance(create_node_request, CreateNodeRequest):
3061
+ create_node_request.resolution = NodeResolutionState.UNRESOLVED.value
2740
3062
  else:
2741
3063
  commands.append(internal_command)
2742
3064
  if output_value is not None:
@@ -2752,8 +3074,9 @@ class NodeManager:
2752
3074
  if output_command is None:
2753
3075
  details = f"Attempted to serialize output value for parameter '{parameter.name}' on node '{node.name}'. The output value will not be restored in anything that attempts to deserialize or save this node. The value for this parameter was not serialized because it did not match Griptape Nodes' criteria for serializability. To remedy, either update the value's type to support serializability or mark the parameter as not serializable by setting serializable=False when creating the parameter."
2754
3076
  logger.warning(details)
2755
- # Set node to unresolved when serialization fails
2756
- create_node_request.resolution = NodeResolutionState.UNRESOLVED.value
3077
+ # Set node to unresolved when serialization fails (only for CreateNodeRequest)
3078
+ if isinstance(create_node_request, CreateNodeRequest):
3079
+ create_node_request.resolution = NodeResolutionState.UNRESOLVED.value
2757
3080
  else:
2758
3081
  commands.append(output_command)
2759
3082
  return commands if commands else None