griptape-nodes 0.63.9__py3-none-any.whl → 0.64.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 (27) 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 +391 -77
  21. griptape_nodes/retained_mode/managers/workflow_manager.py +45 -10
  22. griptape_nodes/servers/mcp.py +32 -0
  23. griptape_nodes/updater/__init__.py +1 -1
  24. {griptape_nodes-0.63.9.dist-info → griptape_nodes-0.64.0.dist-info}/METADATA +3 -1
  25. {griptape_nodes-0.63.9.dist-info → griptape_nodes-0.64.0.dist-info}/RECORD +27 -25
  26. {griptape_nodes-0.63.9.dist-info → griptape_nodes-0.64.0.dist-info}/WHEEL +0 -0
  27. {griptape_nodes-0.63.9.dist-info → griptape_nodes-0.64.0.dist-info}/entry_points.txt +0 -0
@@ -13,7 +13,7 @@ from collections import defaultdict
13
13
  from dataclasses import dataclass, field
14
14
  from importlib.resources import files
15
15
  from pathlib import Path
16
- from typing import TYPE_CHECKING, cast
16
+ from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast
17
17
 
18
18
  from packaging.requirements import InvalidRequirement, Requirement
19
19
  from pydantic import ValidationError
@@ -145,6 +145,8 @@ if TYPE_CHECKING:
145
145
  logger = logging.getLogger("griptape_nodes")
146
146
  console = Console()
147
147
 
148
+ TRegisteredEventData = TypeVar("TRegisteredEventData")
149
+
148
150
 
149
151
  class LibraryManager:
150
152
  SANDBOX_LIBRARY_NAME = "Sandbox Library"
@@ -167,11 +169,16 @@ class LibraryManager:
167
169
  _library_file_path_to_info: dict[str, LibraryInfo]
168
170
 
169
171
  @dataclass
170
- class RegisteredEventHandler:
171
- """Information regarding an event handler from a registered library."""
172
+ class RegisteredEventHandler(Generic[TRegisteredEventData]):
173
+ """Information regarding an event handler from a registered library.
174
+
175
+ The generic type parameter TRegisteredEventData allows each event type
176
+ to specify its own structured additional data.
177
+ """
172
178
 
173
179
  handler: Callable[[RequestPayload], ResultPayload]
174
180
  library_data: LibrarySchema
181
+ event_data: TRegisteredEventData | None = None
175
182
 
176
183
  # Stable module namespace mappings for workflow serialization
177
184
  # These mappings ensure that dynamically loaded modules can be reliably imported
@@ -197,7 +204,9 @@ class LibraryManager:
197
204
  self._dynamic_to_stable_module_mapping = {}
198
205
  self._stable_to_dynamic_module_mapping = {}
199
206
  self._library_to_stable_modules = {}
200
- self._library_event_handler_mappings: dict[type[Payload], dict[str, LibraryManager.RegisteredEventHandler]] = {}
207
+ self._library_event_handler_mappings: dict[
208
+ type[Payload], dict[str, LibraryManager.RegisteredEventHandler[Any]]
209
+ ] = {}
201
210
  # LibraryDirectory owns the FSMs and manages library lifecycle
202
211
  self._library_directory = LibraryDirectory()
203
212
  self._libraries_loading_complete = asyncio.Event()
@@ -357,15 +366,23 @@ class LibraryManager:
357
366
  request_type: type[RequestPayload],
358
367
  handler: Callable[[RequestPayload], ResultPayload],
359
368
  library_data: LibrarySchema,
369
+ event_data: object | None = None,
360
370
  ) -> None:
361
- """Register an event handler for a specific request type from a library."""
371
+ """Register an event handler for a specific request type from a library.
372
+
373
+ Args:
374
+ request_type: The type of request payload this handler processes
375
+ handler: The callable handler function
376
+ library_data: Schema data for the library registering this handler
377
+ event_data: Optional structured data specific to this event type
378
+ """
362
379
  if self._library_event_handler_mappings.get(request_type) is None:
363
380
  self._library_event_handler_mappings[request_type] = {}
364
381
  self._library_event_handler_mappings[request_type][library_data.name] = LibraryManager.RegisteredEventHandler(
365
- handler=handler, library_data=library_data
382
+ handler=handler, library_data=library_data, event_data=event_data
366
383
  )
367
384
 
368
- def get_registered_event_handlers(self, request_type: type[Payload]) -> dict[str, RegisteredEventHandler]:
385
+ def get_registered_event_handlers(self, request_type: type[Payload]) -> dict[str, RegisteredEventHandler[Any]]:
369
386
  """Get all registered event handlers for a specific request type."""
370
387
  return self._library_event_handler_mappings.get(request_type, {})
371
388
 
@@ -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:
@@ -1624,33 +1926,6 @@ class NodeManager:
1624
1926
  # Reject runtime parameter value changes on ErrorProxy
1625
1927
  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
1928
  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
1929
 
1655
1930
  # Does the Parameter actually exist on the Node?
1656
1931
  parameter = node.get_parameter_by_name(param_name)
@@ -2144,55 +2419,81 @@ class NodeManager:
2144
2419
 
2145
2420
  # This is our current dude.
2146
2421
  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)
2422
+ # Handle NodeGroupNode specially - skip library lookup entirely
2423
+ if isinstance(node, NodeGroupNode):
2424
+ # NodeGroupNode doesn't have a library dependency
2425
+ library_details = None
2168
2426
  else:
2169
- library_version = library_metadata_result.metadata.library_version
2170
- library_details = LibraryNameAndVersion(library_name=library_used, library_version=library_version)
2427
+ # Get the library and version details for regular nodes
2428
+ library_used = node.metadata["library"]
2429
+ # Get the library metadata so we can get the version.
2430
+ library_metadata_request = GetLibraryMetadataRequest(library=library_used)
2431
+ # Call LibraryManager directly to avoid error toasts when library is unavailable (expected for ErrorProxyNode)
2432
+ # Per https://github.com/griptape-ai/griptape-nodes/issues/1940
2433
+ library_metadata_result = GriptapeNodes.LibraryManager().get_library_metadata_request(
2434
+ library_metadata_request
2435
+ )
2171
2436
 
2172
- # Handle ErrorProxyNode serialization - serialize as original node type
2437
+ if not isinstance(library_metadata_result, GetLibraryMetadataResultSuccess):
2438
+ if isinstance(node, ErrorProxyNode):
2439
+ # For ErrorProxyNode, use descriptive message when original library unavailable
2440
+ library_version = (
2441
+ "<version unavailable; workflow was saved when library was unable to be loaded>"
2442
+ )
2443
+ library_details = LibraryNameAndVersion(
2444
+ library_name=library_used, library_version=library_version
2445
+ )
2446
+ 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."
2447
+ logger.warning(details)
2448
+ else:
2449
+ # For regular nodes, this is still an error
2450
+ details = f"Attempted to serialize Node '{node_name}' to commands. Failed to get metadata for library '{library_used}'."
2451
+ return SerializeNodeToCommandsResultFailure(result_details=details)
2452
+ else:
2453
+ library_version = library_metadata_result.metadata.library_version
2454
+ library_details = LibraryNameAndVersion(library_name=library_used, library_version=library_version)
2173
2455
 
2174
- if isinstance(node, ErrorProxyNode):
2175
- serialized_node_type = node.original_node_type
2176
- serialized_library_name = node.original_library_name
2456
+ # Handle NodeGroupNode specially - emit CreateNodeGroupRequest instead
2457
+ if isinstance(node, NodeGroupNode):
2458
+ create_node_request = CreateNodeGroupRequest(
2459
+ node_group_name=node_name,
2460
+ node_names_to_add=list(node.nodes),
2461
+ metadata=copy.deepcopy(node.metadata),
2462
+ )
2177
2463
  else:
2178
- serialized_node_type = node.__class__.__name__
2179
- serialized_library_name = library_details.library_name
2464
+ # For non-NodeGroupNode, library_details should always be set
2465
+ if library_details is None:
2466
+ details = f"Attempted to serialize Node '{node_name}' to commands. Library details missing."
2467
+ return SerializeNodeToCommandsResultFailure(result_details=details)
2180
2468
 
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
- )
2469
+ # Handle ErrorProxyNode serialization - serialize as original node type
2470
+ if isinstance(node, ErrorProxyNode):
2471
+ serialized_node_type = node.original_node_type
2472
+ serialized_library_name = node.original_library_name
2473
+ else:
2474
+ serialized_node_type = node.__class__.__name__
2475
+ serialized_library_name = library_details.library_name
2476
+
2477
+ # Get the creation details for regular nodes
2478
+ create_node_request = CreateNodeRequest(
2479
+ node_type=serialized_node_type,
2480
+ node_name=node_name,
2481
+ specific_library_name=serialized_library_name,
2482
+ metadata=copy.deepcopy(node.metadata),
2483
+ # If it is actively resolving, mark as unresolved.
2484
+ resolution=node.state.value,
2485
+ initial_setup=True,
2486
+ )
2191
2487
 
2192
2488
  # We're going to compare this node instance vs. a canonical one. Rez that one up.
2193
2489
  # For ErrorProxyNode, we can't create a reference node, so skip comparison
2194
2490
  if isinstance(node, ErrorProxyNode):
2195
2491
  reference_node = None
2492
+ elif isinstance(node, NodeGroupNode):
2493
+ # For NodeGroupNode, create a fresh reference instance the same way we create NodeGroupNodes
2494
+ reference_node = NodeGroupNode(
2495
+ name="REFERENCE NODE", metadata={"library": "griptape_nodes", "node_type": "NodeGroupNode"}
2496
+ )
2196
2497
  else:
2197
2498
  reference_node = type(node)(name="REFERENCE NODE")
2198
2499
 
@@ -2255,6 +2556,7 @@ class NodeManager:
2255
2556
  )
2256
2557
  if set_param_value_requests is not None:
2257
2558
  set_value_commands.extend(set_param_value_requests)
2559
+
2258
2560
  # now check if locked
2259
2561
  if node.lock:
2260
2562
  lock_command = SetLockNodeStateRequest(node_name=None, lock=True)
@@ -2267,8 +2569,9 @@ class NodeManager:
2267
2569
  # Ensure we always have a NodeDependencies object, even if empty
2268
2570
  node_dependencies = NodeDependencies()
2269
2571
 
2270
- # Add the library dependency to the node dependencies
2271
- node_dependencies.libraries.add(library_details)
2572
+ # Add the library dependency to the node dependencies (if applicable)
2573
+ if library_details is not None:
2574
+ node_dependencies.libraries.add(library_details)
2272
2575
 
2273
2576
  # Hooray
2274
2577
  serialized_node_commands = SerializedNodeCommands(
@@ -2397,12 +2700,21 @@ class NodeManager:
2397
2700
  # Issue the creation command first.
2398
2701
  create_node_request = request.serialized_node_commands.create_node_command
2399
2702
  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}'."
2703
+ if not isinstance(create_node_result, (CreateNodeResultSuccess, CreateNodeGroupResultSuccess)):
2704
+ req_node_name = (
2705
+ create_node_request.node_group_name
2706
+ if isinstance(create_node_request, CreateNodeGroupRequest)
2707
+ else create_node_request.node_name
2708
+ )
2709
+ details = f"Attempted to deserialize a serialized set of Node Creation commands. Failed to create node '{req_node_name}'."
2402
2710
  return DeserializeNodeFromCommandsResultFailure(result_details=details)
2403
2711
 
2404
2712
  # Adopt the newly-created node as our current context.
2405
- node_name = create_node_result.node_name
2713
+ node_name = (
2714
+ create_node_result.node_group_name
2715
+ if isinstance(create_node_result, CreateNodeGroupResultSuccess)
2716
+ else create_node_result.node_name
2717
+ )
2406
2718
  node = GriptapeNodes.ObjectManager().attempt_get_object_by_name_as_type(node_name, BaseNode)
2407
2719
  if node is None:
2408
2720
  details = f"Attempted to deserialize a serialized set of Node Creation commands. Failed to get node '{node_name}'."
@@ -2683,7 +2995,7 @@ class NodeManager:
2683
2995
  node: BaseNode,
2684
2996
  unique_parameter_uuid_to_values: dict[SerializedNodeCommands.UniqueParameterValueUUID, Any],
2685
2997
  serialized_parameter_value_tracker: SerializedParameterValueTracker,
2686
- create_node_request: CreateNodeRequest,
2998
+ create_node_request: CreateNodeRequest | CreateNodeGroupRequest,
2687
2999
  ) -> list[SerializedNodeCommands.IndirectSetParameterValueCommand] | None:
2688
3000
  """Generates code to save a parameter value for a node in a Griptape workflow.
2689
3001
 
@@ -2735,8 +3047,9 @@ class NodeManager:
2735
3047
  if internal_command is None:
2736
3048
  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
3049
  logger.warning(details)
2738
- # Set node to unresolved when serialization fails
2739
- create_node_request.resolution = NodeResolutionState.UNRESOLVED.value
3050
+ # Set node to unresolved when serialization fails (only for CreateNodeRequest)
3051
+ if isinstance(create_node_request, CreateNodeRequest):
3052
+ create_node_request.resolution = NodeResolutionState.UNRESOLVED.value
2740
3053
  else:
2741
3054
  commands.append(internal_command)
2742
3055
  if output_value is not None:
@@ -2752,8 +3065,9 @@ class NodeManager:
2752
3065
  if output_command is None:
2753
3066
  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
3067
  logger.warning(details)
2755
- # Set node to unresolved when serialization fails
2756
- create_node_request.resolution = NodeResolutionState.UNRESOLVED.value
3068
+ # Set node to unresolved when serialization fails (only for CreateNodeRequest)
3069
+ if isinstance(create_node_request, CreateNodeRequest):
3070
+ create_node_request.resolution = NodeResolutionState.UNRESOLVED.value
2757
3071
  else:
2758
3072
  commands.append(output_command)
2759
3073
  return commands if commands else None