griptape-nodes 0.60.4__py3-none-any.whl → 0.61.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 (47) hide show
  1. griptape_nodes/bootstrap/workflow_publishers/local_workflow_publisher.py +0 -1
  2. griptape_nodes/common/macro_parser/__init__.py +16 -1
  3. griptape_nodes/common/macro_parser/core.py +15 -3
  4. griptape_nodes/common/macro_parser/exceptions.py +99 -0
  5. griptape_nodes/common/macro_parser/formats.py +13 -4
  6. griptape_nodes/common/macro_parser/matching.py +5 -2
  7. griptape_nodes/common/macro_parser/parsing.py +48 -8
  8. griptape_nodes/common/macro_parser/resolution.py +23 -5
  9. griptape_nodes/common/project_templates/__init__.py +49 -0
  10. griptape_nodes/common/project_templates/default_project_template.py +92 -0
  11. griptape_nodes/common/project_templates/defaults/README.md +36 -0
  12. griptape_nodes/common/project_templates/defaults/project_template.yml +89 -0
  13. griptape_nodes/common/project_templates/directory.py +67 -0
  14. griptape_nodes/common/project_templates/loader.py +341 -0
  15. griptape_nodes/common/project_templates/project.py +252 -0
  16. griptape_nodes/common/project_templates/situation.py +155 -0
  17. griptape_nodes/common/project_templates/validation.py +140 -0
  18. griptape_nodes/exe_types/core_types.py +36 -3
  19. griptape_nodes/exe_types/node_types.py +4 -2
  20. griptape_nodes/exe_types/param_components/progress_bar_component.py +57 -0
  21. griptape_nodes/exe_types/param_types/parameter_audio.py +243 -0
  22. griptape_nodes/exe_types/param_types/parameter_image.py +243 -0
  23. griptape_nodes/exe_types/param_types/parameter_three_d.py +215 -0
  24. griptape_nodes/exe_types/param_types/parameter_video.py +243 -0
  25. griptape_nodes/node_library/workflow_registry.py +1 -1
  26. griptape_nodes/retained_mode/events/execution_events.py +41 -0
  27. griptape_nodes/retained_mode/events/node_events.py +90 -1
  28. griptape_nodes/retained_mode/events/os_events.py +108 -0
  29. griptape_nodes/retained_mode/events/parameter_events.py +1 -1
  30. griptape_nodes/retained_mode/events/project_events.py +413 -0
  31. griptape_nodes/retained_mode/events/workflow_events.py +19 -1
  32. griptape_nodes/retained_mode/griptape_nodes.py +9 -1
  33. griptape_nodes/retained_mode/managers/agent_manager.py +18 -24
  34. griptape_nodes/retained_mode/managers/event_manager.py +6 -9
  35. griptape_nodes/retained_mode/managers/flow_manager.py +63 -0
  36. griptape_nodes/retained_mode/managers/library_manager.py +55 -42
  37. griptape_nodes/retained_mode/managers/mcp_manager.py +14 -6
  38. griptape_nodes/retained_mode/managers/node_manager.py +232 -0
  39. griptape_nodes/retained_mode/managers/os_manager.py +345 -0
  40. griptape_nodes/retained_mode/managers/project_manager.py +617 -0
  41. griptape_nodes/retained_mode/managers/settings.py +6 -0
  42. griptape_nodes/retained_mode/managers/workflow_manager.py +6 -69
  43. griptape_nodes/traits/button.py +18 -0
  44. {griptape_nodes-0.60.4.dist-info → griptape_nodes-0.61.0.dist-info}/METADATA +5 -3
  45. {griptape_nodes-0.60.4.dist-info → griptape_nodes-0.61.0.dist-info}/RECORD +47 -31
  46. {griptape_nodes-0.60.4.dist-info → griptape_nodes-0.61.0.dist-info}/WHEEL +1 -1
  47. {griptape_nodes-0.60.4.dist-info → griptape_nodes-0.61.0.dist-info}/entry_points.txt +0 -0
@@ -72,6 +72,9 @@ from griptape_nodes.retained_mode.events.execution_events import (
72
72
  SingleNodeStepRequest,
73
73
  SingleNodeStepResultFailure,
74
74
  SingleNodeStepResultSuccess,
75
+ StartFlowFromNodeRequest,
76
+ StartFlowFromNodeResultFailure,
77
+ StartFlowFromNodeResultSuccess,
75
78
  StartFlowRequest,
76
79
  StartFlowResultFailure,
77
80
  StartFlowResultSuccess,
@@ -239,6 +242,7 @@ class FlowManager:
239
242
  event_manager.assign_manager_to_request_type(CreateConnectionRequest, self.on_create_connection_request)
240
243
  event_manager.assign_manager_to_request_type(DeleteConnectionRequest, self.on_delete_connection_request)
241
244
  event_manager.assign_manager_to_request_type(StartFlowRequest, self.on_start_flow_request)
245
+ event_manager.assign_manager_to_request_type(StartFlowFromNodeRequest, self.on_start_flow_from_node_request)
242
246
  event_manager.assign_manager_to_request_type(SingleNodeStepRequest, self.on_single_node_step_request)
243
247
  event_manager.assign_manager_to_request_type(SingleExecutionStepRequest, self.on_single_execution_step_request)
244
248
  event_manager.assign_manager_to_request_type(
@@ -2451,6 +2455,65 @@ class FlowManager:
2451
2455
 
2452
2456
  return StartFlowResultSuccess(result_details=details)
2453
2457
 
2458
+ async def on_start_flow_from_node_request(self, request: StartFlowFromNodeRequest) -> ResultPayload: # noqa: C901, PLR0911
2459
+ # which flow
2460
+ flow_name = request.flow_name
2461
+ if not flow_name:
2462
+ details = "Must provide flow name to start a flow."
2463
+
2464
+ return StartFlowResultFailure(validation_exceptions=[], result_details=details)
2465
+ # get the flow by ID
2466
+ try:
2467
+ flow = self.get_flow_by_name(flow_name)
2468
+ except KeyError as err:
2469
+ details = f"Cannot start flow. Error: {err}"
2470
+ return StartFlowFromNodeResultFailure(validation_exceptions=[err], result_details=details)
2471
+ # Check to see if the flow is already running.
2472
+ if self.check_for_existing_running_flow():
2473
+ details = "Cannot start flow. Flow is already running."
2474
+ return StartFlowFromNodeResultFailure(validation_exceptions=[], result_details=details)
2475
+ node_name = request.node_name
2476
+ if node_name is None:
2477
+ details = "Must provide node name to start a flow."
2478
+ return StartFlowFromNodeResultFailure(validation_exceptions=[], result_details=details)
2479
+ start_node = GriptapeNodes.ObjectManager().attempt_get_object_by_name_as_type(node_name, BaseNode)
2480
+ if not start_node:
2481
+ details = f"Provided node with name {node_name} does not exist"
2482
+ return StartFlowResultFailure(validation_exceptions=[], result_details=details)
2483
+ result = await self.on_validate_flow_dependencies_request(
2484
+ ValidateFlowDependenciesRequest(flow_name=flow_name, flow_node_name=start_node.name if start_node else None)
2485
+ )
2486
+ try:
2487
+ if not result.succeeded():
2488
+ details = f"Couldn't start flow with name {flow_name}. Flow Validation Failed"
2489
+ return StartFlowFromNodeResultFailure(validation_exceptions=[], result_details=details)
2490
+ result = cast("ValidateFlowDependenciesResultSuccess", result)
2491
+
2492
+ if not result.validation_succeeded:
2493
+ details = f"Couldn't start flow with name {flow_name}. Flow Validation Failed."
2494
+ if len(result.exceptions) > 0:
2495
+ for exception in result.exceptions:
2496
+ details = f"{details}\n\t{exception}"
2497
+ return StartFlowFromNodeResultFailure(validation_exceptions=result.exceptions, result_details=details)
2498
+ except Exception as e:
2499
+ details = f"Couldn't start flow with name {flow_name}. Flow Validation Failed: {e}"
2500
+ return StartFlowFromNodeResultFailure(validation_exceptions=[e], result_details=details)
2501
+ # By now, it has been validated with no exceptions.
2502
+ try:
2503
+ await self.start_flow(
2504
+ flow,
2505
+ start_node,
2506
+ debug_mode=request.debug_mode,
2507
+ pickle_control_flow_result=request.pickle_control_flow_result,
2508
+ )
2509
+ except Exception as e:
2510
+ details = f"Failed to kick off flow with name {flow_name}. Exception occurred: {e} "
2511
+ return StartFlowFromNodeResultFailure(validation_exceptions=[e], result_details=details)
2512
+
2513
+ details = f"Successfully kicked off flow with name {flow_name}"
2514
+
2515
+ return StartFlowFromNodeResultSuccess(result_details=details)
2516
+
2454
2517
  def on_get_flow_state_request(self, event: GetFlowStateRequest) -> ResultPayload:
2455
2518
  flow_name = event.flow_name
2456
2519
  if not flow_name:
@@ -169,6 +169,7 @@ class LibraryManager:
169
169
  self._library_event_handler_mappings: dict[type[Payload], dict[str, LibraryManager.RegisteredEventHandler]] = {}
170
170
  # LibraryDirectory owns the FSMs and manages library lifecycle
171
171
  self._library_directory = LibraryDirectory()
172
+ self._libraries_loading_complete = asyncio.Event()
172
173
 
173
174
  event_manager.assign_manager_to_request_type(
174
175
  ListRegisteredLibrariesRequest, self.on_list_registered_libraries_request
@@ -204,7 +205,7 @@ class LibraryManager:
204
205
  )
205
206
  event_manager.assign_manager_to_request_type(GetAllInfoForLibraryRequest, self.get_all_info_for_library_request)
206
207
  event_manager.assign_manager_to_request_type(
207
- GetAllInfoForAllLibrariesRequest, self.get_all_info_for_all_libraries_request
208
+ GetAllInfoForAllLibrariesRequest, self.on_get_all_info_for_all_libraries_request
208
209
  )
209
210
  event_manager.assign_manager_to_request_type(
210
211
  LoadMetadataForAllLibrariesRequest, self.load_metadata_for_all_libraries_request
@@ -531,13 +532,13 @@ class LibraryManager:
531
532
  sandbox_library_dir_as_posix = sandbox_library_dir.as_posix()
532
533
 
533
534
  if not sandbox_library_dir.exists():
534
- details = "Sandbox directory does not exist."
535
+ details = "Sandbox directory does not exist. If you wish to create a Sandbox directory to develop custom nodes: in the Griptape Nodes editor, go to Settings -> Libraries and navigate to the Sandbox Settings."
535
536
  return LoadLibraryMetadataFromFileResultFailure(
536
537
  library_path=sandbox_library_dir_as_posix,
537
538
  library_name=LibraryManager.SANDBOX_LIBRARY_NAME,
538
539
  status=LibraryStatus.MISSING,
539
540
  problems=[details],
540
- result_details=details,
541
+ result_details=ResultDetails(message=details, level=logging.INFO),
541
542
  )
542
543
 
543
544
  sandbox_node_candidates = self._find_files_in_dir(directory=sandbox_library_dir, extension=".py")
@@ -1170,6 +1171,13 @@ class LibraryManager:
1170
1171
  )
1171
1172
  return result
1172
1173
 
1174
+ async def on_get_all_info_for_all_libraries_request(
1175
+ self, request: GetAllInfoForAllLibrariesRequest
1176
+ ) -> ResultPayload:
1177
+ """Async handler for GetAllInfoForAllLibrariesRequest that waits for library loading to complete."""
1178
+ await self._libraries_loading_complete.wait()
1179
+ return await asyncio.to_thread(self.get_all_info_for_all_libraries_request, request)
1180
+
1173
1181
  def get_all_info_for_library_request(self, request: GetAllInfoForLibraryRequest) -> ResultPayload: # noqa: PLR0911
1174
1182
  # Does this library exist?
1175
1183
  try:
@@ -1500,48 +1508,53 @@ class LibraryManager:
1500
1508
  return node_class
1501
1509
 
1502
1510
  async def load_all_libraries_from_config(self) -> None:
1503
- # Load metadata for all libraries to determine which ones can be safely loaded
1504
- metadata_request = LoadMetadataForAllLibrariesRequest()
1505
- metadata_result = self.load_metadata_for_all_libraries_request(metadata_request)
1506
-
1507
- # Check if metadata loading succeeded
1508
- if not isinstance(metadata_result, LoadMetadataForAllLibrariesResultSuccess):
1509
- logger.error("Failed to load metadata for all libraries, skipping library registration")
1510
- return
1511
-
1512
- # Record all failed libraries in our tracking immediately
1513
- for failed_library in metadata_result.failed_libraries:
1514
- self._library_file_path_to_info[failed_library.library_path] = LibraryManager.LibraryInfo(
1515
- library_path=failed_library.library_path,
1516
- library_name=failed_library.library_name,
1517
- status=failed_library.status,
1518
- problems=failed_library.problems,
1519
- )
1511
+ self._libraries_loading_complete.clear()
1520
1512
 
1521
- # Use metadata results to selectively load libraries
1522
- for library_result in metadata_result.successful_libraries:
1523
- if library_result.library_schema.name == LibraryManager.SANDBOX_LIBRARY_NAME:
1524
- # Handle sandbox library - use the schema we already have
1525
- await self._attempt_generate_sandbox_library_from_schema(
1526
- library_schema=library_result.library_schema, sandbox_directory=library_result.file_path
1527
- )
1528
- else:
1529
- # Handle config-based library - register it directly using the file path
1530
- register_request = RegisterLibraryFromFileRequest(
1531
- file_path=library_result.file_path, load_as_default_library=False
1513
+ try:
1514
+ # Load metadata for all libraries to determine which ones can be safely loaded
1515
+ metadata_request = LoadMetadataForAllLibrariesRequest()
1516
+ metadata_result = self.load_metadata_for_all_libraries_request(metadata_request)
1517
+
1518
+ # Check if metadata loading succeeded
1519
+ if not isinstance(metadata_result, LoadMetadataForAllLibrariesResultSuccess):
1520
+ logger.error("Failed to load metadata for all libraries, skipping library registration")
1521
+ return
1522
+
1523
+ # Record all failed libraries in our tracking immediately
1524
+ for failed_library in metadata_result.failed_libraries:
1525
+ self._library_file_path_to_info[failed_library.library_path] = LibraryManager.LibraryInfo(
1526
+ library_path=failed_library.library_path,
1527
+ library_name=failed_library.library_name,
1528
+ status=failed_library.status,
1529
+ problems=failed_library.problems,
1532
1530
  )
1533
- register_result = await self.register_library_from_file_request(register_request)
1534
- if isinstance(register_result, RegisterLibraryFromFileResultFailure):
1535
- # Registration failed - the failure info is already recorded in _library_file_path_to_info
1536
- # by register_library_from_file_request, so we just log it here for visibility
1537
- logger.warning("Failed to register library from %s", library_result.file_path)
1538
1531
 
1539
- # Print 'em all pretty
1540
- self.print_library_load_status()
1541
-
1542
- # Remove any missing libraries AFTER we've printed them for the user.
1543
- user_libraries_section = "app_events.on_app_initialization_complete.libraries_to_register"
1544
- self._remove_missing_libraries_from_config(config_category=user_libraries_section)
1532
+ # Use metadata results to selectively load libraries
1533
+ for library_result in metadata_result.successful_libraries:
1534
+ if library_result.library_schema.name == LibraryManager.SANDBOX_LIBRARY_NAME:
1535
+ # Handle sandbox library - use the schema we already have
1536
+ await self._attempt_generate_sandbox_library_from_schema(
1537
+ library_schema=library_result.library_schema, sandbox_directory=library_result.file_path
1538
+ )
1539
+ else:
1540
+ # Handle config-based library - register it directly using the file path
1541
+ register_request = RegisterLibraryFromFileRequest(
1542
+ file_path=library_result.file_path, load_as_default_library=False
1543
+ )
1544
+ register_result = await self.register_library_from_file_request(register_request)
1545
+ if isinstance(register_result, RegisterLibraryFromFileResultFailure):
1546
+ # Registration failed - the failure info is already recorded in _library_file_path_to_info
1547
+ # by register_library_from_file_request, so we just log it here for visibility
1548
+ logger.warning("Failed to register library from %s", library_result.file_path)
1549
+
1550
+ # Print 'em all pretty
1551
+ self.print_library_load_status()
1552
+
1553
+ # Remove any missing libraries AFTER we've printed them for the user.
1554
+ user_libraries_section = "app_events.on_app_initialization_complete.libraries_to_register"
1555
+ self._remove_missing_libraries_from_config(config_category=user_libraries_section)
1556
+ finally:
1557
+ self._libraries_loading_complete.set()
1545
1558
 
1546
1559
  async def on_app_initialization_complete(self, _payload: AppInitializationComplete) -> None:
1547
1560
  # App just got init'd. See if there are library JSONs to load!
@@ -232,19 +232,27 @@ class MCPManager:
232
232
  self, request: UpdateMCPServerRequest
233
233
  ) -> UpdateMCPServerResultSuccess | UpdateMCPServerResultFailure:
234
234
  """Handle update MCP server request."""
235
- servers = self._get_mcp_servers(filter_by={"name": request.name})
236
- server_config = servers[0] if servers else None
235
+ servers = self._get_mcp_servers()
237
236
 
238
- if server_config is None:
237
+ # Find the server to update
238
+ server_index = next((i for i, server in enumerate(servers) if server.name == request.name), None)
239
+
240
+ if server_index is None:
239
241
  return UpdateMCPServerResultFailure(
240
242
  result_details=f"Failed to update MCP server '{request.name}' - not found"
241
243
  )
242
244
 
243
- # Update only provided fields
244
- self._update_server_fields(server_config, request)
245
+ # Create a backup of the original server and update a copy
246
+ original_server = servers[server_index]
247
+ updated_server = original_server.model_copy()
248
+ self._update_server_fields(updated_server, request)
249
+
250
+ # Create a copy of the servers list with the updated server
251
+ updated_servers = servers.copy()
252
+ updated_servers[server_index] = updated_server
245
253
 
246
254
  try:
247
- self._save_mcp_servers(servers)
255
+ self._save_mcp_servers(updated_servers)
248
256
  except Exception as e:
249
257
  logger.error("Failed to save MCP server '%s': %s", request.name, e)
250
258
  return UpdateMCPServerResultFailure(result_details=f"Failed to save MCP server '{request.name}': {e}")
@@ -63,6 +63,9 @@ from griptape_nodes.retained_mode.events.node_events import (
63
63
  BatchSetNodeMetadataRequest,
64
64
  BatchSetNodeMetadataResultFailure,
65
65
  BatchSetNodeMetadataResultSuccess,
66
+ CanResetNodeToDefaultsRequest,
67
+ CanResetNodeToDefaultsResultFailure,
68
+ CanResetNodeToDefaultsResultSuccess,
66
69
  CreateNodeRequest,
67
70
  CreateNodeResultFailure,
68
71
  CreateNodeResultSuccess,
@@ -93,6 +96,9 @@ from griptape_nodes.retained_mode.events.node_events import (
93
96
  ListParametersOnNodeRequest,
94
97
  ListParametersOnNodeResultFailure,
95
98
  ListParametersOnNodeResultSuccess,
99
+ ResetNodeToDefaultsRequest,
100
+ ResetNodeToDefaultsResultFailure,
101
+ ResetNodeToDefaultsResultSuccess,
96
102
  SendNodeMessageRequest,
97
103
  SendNodeMessageResultFailure,
98
104
  SendNodeMessageResultSuccess,
@@ -111,6 +117,10 @@ from griptape_nodes.retained_mode.events.node_events import (
111
117
  SetNodeMetadataResultFailure,
112
118
  SetNodeMetadataResultSuccess,
113
119
  )
120
+ from griptape_nodes.retained_mode.events.object_events import (
121
+ RenameObjectRequest,
122
+ RenameObjectResultSuccess,
123
+ )
114
124
  from griptape_nodes.retained_mode.events.parameter_events import (
115
125
  AddParameterToNodeRequest,
116
126
  AddParameterToNodeResultFailure,
@@ -171,6 +181,18 @@ class SerializedParameterValues(NamedTuple):
171
181
  unique_parameter_uuid_to_values: dict[Any, Any] | None
172
182
 
173
183
 
184
+ class CanResetResult(NamedTuple):
185
+ """Result of checking if a node can be reset to defaults.
186
+
187
+ Attributes:
188
+ can_reset: True if the node can be reset to defaults, False otherwise
189
+ editor_tooltip_reason: Optional explanation if node cannot be reset
190
+ """
191
+
192
+ can_reset: bool
193
+ editor_tooltip_reason: str | None
194
+
195
+
174
196
  class NodeManager:
175
197
  _name_to_parent_flow_name: dict[str, str]
176
198
 
@@ -233,6 +255,10 @@ class NodeManager:
233
255
  event_manager.assign_manager_to_request_type(SetLockNodeStateRequest, self.on_toggle_lock_node_request)
234
256
  event_manager.assign_manager_to_request_type(GetFlowForNodeRequest, self.on_get_flow_for_node_request)
235
257
  event_manager.assign_manager_to_request_type(SendNodeMessageRequest, self.on_send_node_message_request)
258
+ event_manager.assign_manager_to_request_type(
259
+ CanResetNodeToDefaultsRequest, self.on_can_reset_node_to_defaults_request
260
+ )
261
+ event_manager.assign_manager_to_request_type(ResetNodeToDefaultsRequest, self.on_reset_node_to_defaults_request)
236
262
 
237
263
  def handle_node_rename(self, old_name: str, new_name: str) -> None:
238
264
  # Get the node itself
@@ -3414,3 +3440,209 @@ class NodeManager:
3414
3440
  )
3415
3441
 
3416
3442
  return None
3443
+
3444
+ def _check_can_reset_node(self, node: BaseNode) -> CanResetResult:
3445
+ """Check if a node can be reset to defaults.
3446
+
3447
+ Args:
3448
+ node: The node to check
3449
+
3450
+ Returns:
3451
+ CanResetResult with can_reset flag and optional tooltip reason
3452
+ """
3453
+ if node.lock:
3454
+ return CanResetResult(
3455
+ can_reset=False,
3456
+ editor_tooltip_reason="Node is locked. Unlock the node in order to reset it.",
3457
+ )
3458
+
3459
+ return CanResetResult(can_reset=True, editor_tooltip_reason=None)
3460
+
3461
+ def on_can_reset_node_to_defaults_request(self, request: CanResetNodeToDefaultsRequest) -> ResultPayload:
3462
+ """Check if a node can be reset to its default state."""
3463
+ node_name = request.node_name
3464
+ node = None
3465
+
3466
+ # FAILURE CHECK: Validate node_name
3467
+ if node_name is None:
3468
+ if not GriptapeNodes.ContextManager().has_current_node():
3469
+ details = (
3470
+ "Attempted to check reset eligibility for a Node from the Current Context. "
3471
+ "Failed because the Current Context is empty."
3472
+ )
3473
+ return CanResetNodeToDefaultsResultFailure(result_details=details)
3474
+ node = GriptapeNodes.ContextManager().get_current_node()
3475
+ node_name = node.name
3476
+
3477
+ # FAILURE CHECK: Get source node
3478
+ if node is None:
3479
+ node = GriptapeNodes.ObjectManager().attempt_get_object_by_name_as_type(node_name, BaseNode)
3480
+ if node is None:
3481
+ details = f"Attempted to check reset eligibility for Node '{node_name}', but no such Node was found."
3482
+ return CanResetNodeToDefaultsResultFailure(result_details=details)
3483
+
3484
+ # FAILURE CHECK: Get node type and library
3485
+ if "library" not in node.metadata:
3486
+ details = (
3487
+ f"Attempted to check reset eligibility for Node '{node_name}'. "
3488
+ f"Failed because node has no library information in metadata."
3489
+ )
3490
+ return CanResetNodeToDefaultsResultFailure(result_details=details)
3491
+
3492
+ # Check if node can be reset
3493
+ can_reset_result = self._check_can_reset_node(node)
3494
+ if not can_reset_result.can_reset:
3495
+ details = f"Node '{node_name}' cannot be reset: {can_reset_result.editor_tooltip_reason}"
3496
+ return CanResetNodeToDefaultsResultSuccess(
3497
+ can_reset=False,
3498
+ editor_tooltip_reason=can_reset_result.editor_tooltip_reason,
3499
+ result_details=details,
3500
+ )
3501
+
3502
+ # SUCCESS PATH: Node can be reset
3503
+ details = f"Node '{node_name}' can be reset to defaults."
3504
+ return CanResetNodeToDefaultsResultSuccess(
3505
+ can_reset=True,
3506
+ editor_tooltip_reason=None,
3507
+ result_details=details,
3508
+ )
3509
+
3510
+ def on_reset_node_to_defaults_request(self, request: ResetNodeToDefaultsRequest) -> ResultPayload: # noqa: C901, PLR0911, PLR0912, PLR0915
3511
+ """Reset a node to its default state while preserving connections where possible."""
3512
+ node_name = request.node_name
3513
+ node = None
3514
+
3515
+ # FAILURE CHECK: Validate node_name
3516
+ if node_name is None:
3517
+ if not GriptapeNodes.ContextManager().has_current_node():
3518
+ details = (
3519
+ "Attempted to reset a Node from the Current Context. Failed because the Current Context is empty."
3520
+ )
3521
+ return ResetNodeToDefaultsResultFailure(result_details=details)
3522
+ node = GriptapeNodes.ContextManager().get_current_node()
3523
+ node_name = node.name
3524
+
3525
+ # FAILURE CHECK: Get source node
3526
+ if node is None:
3527
+ node = GriptapeNodes.ObjectManager().attempt_get_object_by_name_as_type(node_name, BaseNode)
3528
+ if node is None:
3529
+ details = f"Attempted to reset Node '{node_name}', but no such Node was found."
3530
+ return ResetNodeToDefaultsResultFailure(result_details=details)
3531
+
3532
+ # FAILURE CHECK: Get node type and library
3533
+ node_type = node.__class__.__name__
3534
+ if "library" not in node.metadata:
3535
+ details = (
3536
+ f"Attempted to reset Node '{node_name}'. Failed because node has no library information in metadata."
3537
+ )
3538
+ return ResetNodeToDefaultsResultFailure(result_details=details)
3539
+ library_name = node.metadata["library"]
3540
+
3541
+ # FAILURE CHECK: Check if node can be reset
3542
+ can_reset_result = self._check_can_reset_node(node)
3543
+ if not can_reset_result.can_reset:
3544
+ details = f"Attempted to reset Node '{node_name}'. Failed because: {can_reset_result.editor_tooltip_reason}"
3545
+ return ResetNodeToDefaultsResultFailure(result_details=details)
3546
+
3547
+ # FAILURE CHECK: Gather node information
3548
+ all_info_request = GetAllNodeInfoRequest(node_name=node_name)
3549
+ all_info_result = self.on_get_all_node_info_request(all_info_request)
3550
+ if not isinstance(all_info_result, GetAllNodeInfoResultSuccess):
3551
+ details = f"Attempted to reset Node '{node_name}'. Failed to get node information."
3552
+ return ResetNodeToDefaultsResultFailure(result_details=details)
3553
+
3554
+ connections = all_info_result.connections
3555
+
3556
+ # FAILURE CHECK: Get parent flow name
3557
+ if node_name not in self._name_to_parent_flow_name:
3558
+ details = f"Attempted to reset Node '{node_name}'. Failed to find parent flow name."
3559
+ return ResetNodeToDefaultsResultFailure(result_details=details)
3560
+ parent_flow_name = self._name_to_parent_flow_name[node_name]
3561
+
3562
+ # FAILURE CHECK: Create new node with temporary name
3563
+ temp_node_name = f"{node_name}_temp"
3564
+ create_node_request = CreateNodeRequest(
3565
+ node_type=node_type,
3566
+ specific_library_name=library_name,
3567
+ node_name=temp_node_name,
3568
+ override_parent_flow_name=parent_flow_name,
3569
+ create_error_proxy_on_failure=False,
3570
+ )
3571
+ create_result = self.on_create_node_request(create_node_request)
3572
+ if not isinstance(create_result, CreateNodeResultSuccess):
3573
+ details = f"Attempted to reset Node '{node_name}'. Failed to create new node of type '{node_type}'."
3574
+ return ResetNodeToDefaultsResultFailure(result_details=details)
3575
+ new_node_name = create_result.node_name
3576
+
3577
+ # TODO: (griptape-nodes) Don't rely on manually copying metadata fields. https://github.com/griptape-ai/griptape-nodes/issues/2862
3578
+ # Copy only position and size from original node's metadata to preserve layout.
3579
+ # We don't copy the full metadata because it contains instance-specific data that shouldn't be transferred.
3580
+ original_metadata = all_info_result.metadata
3581
+ new_node = self.get_node_by_name(new_node_name)
3582
+ if "position" in original_metadata:
3583
+ new_node.metadata["position"] = copy.deepcopy(original_metadata["position"])
3584
+ if "size" in original_metadata:
3585
+ new_node.metadata["size"] = copy.deepcopy(original_metadata["size"])
3586
+
3587
+ # NON-FATAL: Attempt to reconnect connections
3588
+ failed_incoming: list[IncomingConnection] = []
3589
+ failed_outgoing: list[OutgoingConnection] = []
3590
+
3591
+ for incoming_connection in connections.incoming_connections:
3592
+ connection_request = CreateConnectionRequest(
3593
+ source_node_name=incoming_connection.source_node_name,
3594
+ source_parameter_name=incoming_connection.source_parameter_name,
3595
+ target_node_name=new_node_name,
3596
+ target_parameter_name=incoming_connection.target_parameter_name,
3597
+ )
3598
+ connection_result = GriptapeNodes.FlowManager().on_create_connection_request(connection_request)
3599
+ if not isinstance(connection_result, CreateConnectionResultSuccess):
3600
+ failed_incoming.append(incoming_connection)
3601
+
3602
+ for outgoing_connection in connections.outgoing_connections:
3603
+ connection_request = CreateConnectionRequest(
3604
+ source_node_name=new_node_name,
3605
+ source_parameter_name=outgoing_connection.source_parameter_name,
3606
+ target_node_name=outgoing_connection.target_node_name,
3607
+ target_parameter_name=outgoing_connection.target_parameter_name,
3608
+ )
3609
+ connection_result = GriptapeNodes.FlowManager().on_create_connection_request(connection_request)
3610
+ if not isinstance(connection_result, CreateConnectionResultSuccess):
3611
+ failed_outgoing.append(outgoing_connection)
3612
+
3613
+ # FAILURE CHECK: Delete source node
3614
+ delete_request = DeleteNodeRequest(node_name=node_name)
3615
+ delete_result = self.on_delete_node_request(delete_request)
3616
+ if not isinstance(delete_result, DeleteNodeResultSuccess):
3617
+ details = f"Attempted to reset Node '{node_name}'. Failed to delete original node."
3618
+ return ResetNodeToDefaultsResultFailure(result_details=details)
3619
+
3620
+ # FAILURE CHECK: Rename new node to original name
3621
+ rename_request = RenameObjectRequest(
3622
+ object_name=new_node_name, requested_name=node_name, allow_next_closest_name_available=False
3623
+ )
3624
+ rename_result = GriptapeNodes.ObjectManager().on_rename_object_request(rename_request)
3625
+ if not isinstance(rename_result, RenameObjectResultSuccess):
3626
+ details = f"Attempted to reset Node '{node_name}'. Failed to rename new node to original name."
3627
+ return ResetNodeToDefaultsResultFailure(result_details=details)
3628
+
3629
+ # SUCCESS PATH
3630
+ if not failed_incoming and not failed_outgoing:
3631
+ details = f"Successfully reset node '{node_name}' to defaults."
3632
+ log_level = logging.DEBUG
3633
+ else:
3634
+ details = f"Successfully reset node '{node_name}' but one or more connections could not be restored."
3635
+ if failed_incoming:
3636
+ source_node_names = {conn.source_node_name for conn in failed_incoming}
3637
+ details += f" Connections FROM the following nodes were not restored: {source_node_names}."
3638
+ if failed_outgoing:
3639
+ target_node_names = {conn.target_node_name for conn in failed_outgoing}
3640
+ details += f" Connections TO the following nodes were not restored: {target_node_names}."
3641
+ log_level = logging.WARNING
3642
+
3643
+ return ResetNodeToDefaultsResultSuccess(
3644
+ node_name=node_name,
3645
+ failed_incoming_connections=failed_incoming,
3646
+ failed_outgoing_connections=failed_outgoing,
3647
+ result_details=ResultDetails(message=details, level=log_level),
3648
+ )