griptape-nodes 0.40.0__py3-none-any.whl → 0.42.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 (49) hide show
  1. griptape_nodes/app/__init__.py +1 -5
  2. griptape_nodes/app/app.py +12 -9
  3. griptape_nodes/app/app_sessions.py +132 -36
  4. griptape_nodes/app/watch.py +3 -1
  5. griptape_nodes/drivers/storage/local_storage_driver.py +3 -2
  6. griptape_nodes/exe_types/flow.py +68 -368
  7. griptape_nodes/machines/control_flow.py +16 -13
  8. griptape_nodes/machines/node_resolution.py +16 -14
  9. griptape_nodes/node_library/workflow_registry.py +2 -2
  10. griptape_nodes/retained_mode/events/agent_events.py +70 -8
  11. griptape_nodes/retained_mode/events/app_events.py +132 -11
  12. griptape_nodes/retained_mode/events/arbitrary_python_events.py +23 -0
  13. griptape_nodes/retained_mode/events/base_events.py +7 -25
  14. griptape_nodes/retained_mode/events/config_events.py +87 -11
  15. griptape_nodes/retained_mode/events/connection_events.py +56 -5
  16. griptape_nodes/retained_mode/events/context_events.py +27 -4
  17. griptape_nodes/retained_mode/events/execution_events.py +99 -14
  18. griptape_nodes/retained_mode/events/flow_events.py +165 -7
  19. griptape_nodes/retained_mode/events/library_events.py +193 -15
  20. griptape_nodes/retained_mode/events/logger_events.py +11 -0
  21. griptape_nodes/retained_mode/events/node_events.py +243 -22
  22. griptape_nodes/retained_mode/events/object_events.py +40 -4
  23. griptape_nodes/retained_mode/events/os_events.py +13 -2
  24. griptape_nodes/retained_mode/events/parameter_events.py +212 -8
  25. griptape_nodes/retained_mode/events/secrets_events.py +59 -7
  26. griptape_nodes/retained_mode/events/static_file_events.py +57 -4
  27. griptape_nodes/retained_mode/events/validation_events.py +39 -4
  28. griptape_nodes/retained_mode/events/workflow_events.py +188 -17
  29. griptape_nodes/retained_mode/griptape_nodes.py +46 -323
  30. griptape_nodes/retained_mode/managers/agent_manager.py +1 -1
  31. griptape_nodes/retained_mode/managers/engine_identity_manager.py +146 -0
  32. griptape_nodes/retained_mode/managers/event_manager.py +14 -2
  33. griptape_nodes/retained_mode/managers/flow_manager.py +749 -64
  34. griptape_nodes/retained_mode/managers/library_manager.py +112 -2
  35. griptape_nodes/retained_mode/managers/node_manager.py +35 -32
  36. griptape_nodes/retained_mode/managers/object_manager.py +11 -3
  37. griptape_nodes/retained_mode/managers/os_manager.py +70 -1
  38. griptape_nodes/retained_mode/managers/secrets_manager.py +4 -0
  39. griptape_nodes/retained_mode/managers/session_manager.py +328 -0
  40. griptape_nodes/retained_mode/managers/settings.py +7 -0
  41. griptape_nodes/retained_mode/managers/workflow_manager.py +523 -454
  42. griptape_nodes/retained_mode/retained_mode.py +44 -0
  43. griptape_nodes/retained_mode/utils/engine_identity.py +141 -27
  44. {griptape_nodes-0.40.0.dist-info → griptape_nodes-0.42.0.dist-info}/METADATA +2 -2
  45. {griptape_nodes-0.40.0.dist-info → griptape_nodes-0.42.0.dist-info}/RECORD +48 -47
  46. griptape_nodes/retained_mode/utils/session_persistence.py +0 -105
  47. {griptape_nodes-0.40.0.dist-info → griptape_nodes-0.42.0.dist-info}/WHEEL +0 -0
  48. {griptape_nodes-0.40.0.dist-info → griptape_nodes-0.42.0.dist-info}/entry_points.txt +0 -0
  49. {griptape_nodes-0.40.0.dist-info → griptape_nodes-0.42.0.dist-info}/licenses/LICENSE +0 -0
@@ -58,6 +58,9 @@ from griptape_nodes.retained_mode.events.library_events import (
58
58
  GetNodeMetadataFromLibraryRequest,
59
59
  GetNodeMetadataFromLibraryResultFailure,
60
60
  GetNodeMetadataFromLibraryResultSuccess,
61
+ ListCapableLibraryEventHandlersRequest,
62
+ ListCapableLibraryEventHandlersResultFailure,
63
+ ListCapableLibraryEventHandlersResultSuccess,
61
64
  ListCategoriesInLibraryRequest,
62
65
  ListCategoriesInLibraryResultFailure,
63
66
  ListCategoriesInLibraryResultSuccess,
@@ -85,19 +88,36 @@ from griptape_nodes.retained_mode.events.library_events import (
85
88
  UnloadLibraryFromRegistryResultSuccess,
86
89
  )
87
90
  from griptape_nodes.retained_mode.events.object_events import ClearAllObjectStateRequest
91
+ from griptape_nodes.retained_mode.events.payload_registry import PayloadRegistry
88
92
  from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
89
93
  from griptape_nodes.retained_mode.managers.os_manager import OSManager
90
94
 
91
95
  if TYPE_CHECKING:
96
+ from collections.abc import Callable
92
97
  from types import ModuleType
93
98
 
94
99
  from griptape_nodes.node_library.advanced_node_library import AdvancedNodeLibrary
95
- from griptape_nodes.retained_mode.events.base_events import ResultPayload
100
+ from griptape_nodes.retained_mode.events.base_events import Payload, RequestPayload, ResultPayload
96
101
  from griptape_nodes.retained_mode.managers.event_manager import EventManager
97
102
 
98
103
  logger = logging.getLogger("griptape_nodes")
99
104
 
100
105
 
106
+ def _find_griptape_uv_bin() -> str:
107
+ """Find the uv binary, checking dedicated Griptape installation first, then system uv.
108
+
109
+ Returns:
110
+ Path to the uv binary to use
111
+ """
112
+ # Check for dedicated Griptape uv installation first
113
+ dedicated_uv_path = xdg_data_home() / "griptape_nodes" / "bin" / "uv"
114
+ if dedicated_uv_path.exists():
115
+ return str(dedicated_uv_path)
116
+
117
+ # Fall back to system uv installation
118
+ return uv.find_uv_bin()
119
+
120
+
101
121
  class LibraryManager:
102
122
  SANDBOX_LIBRARY_NAME = "Sandbox Library"
103
123
 
@@ -124,6 +144,13 @@ class LibraryManager:
124
144
 
125
145
  _library_file_path_to_info: dict[str, LibraryInfo]
126
146
 
147
+ @dataclass
148
+ class RegisteredEventHandler:
149
+ """Information regarding an event handler from a registered library."""
150
+
151
+ handler: Callable[[RequestPayload], ResultPayload]
152
+ library_data: LibrarySchema
153
+
127
154
  # Stable module namespace mappings for workflow serialization
128
155
  # These mappings ensure that dynamically loaded modules can be reliably imported
129
156
  # in generated workflow code by providing stable, predictable import paths.
@@ -148,10 +175,14 @@ class LibraryManager:
148
175
  self._dynamic_to_stable_module_mapping = {}
149
176
  self._stable_to_dynamic_module_mapping = {}
150
177
  self._library_to_stable_modules = {}
178
+ self._library_event_handler_mappings: dict[type[Payload], dict[str, LibraryManager.RegisteredEventHandler]] = {}
151
179
 
152
180
  event_manager.assign_manager_to_request_type(
153
181
  ListRegisteredLibrariesRequest, self.on_list_registered_libraries_request
154
182
  )
183
+ event_manager.assign_manager_to_request_type(
184
+ ListCapableLibraryEventHandlersRequest, self.on_list_capable_event_handlers
185
+ )
155
186
  event_manager.assign_manager_to_request_type(
156
187
  ListNodeTypesInLibraryRequest, self.on_list_node_types_in_library_request
157
188
  )
@@ -277,6 +308,33 @@ class LibraryManager:
277
308
  return library_info
278
309
  return None
279
310
 
311
+ def on_register_event_handler(
312
+ self,
313
+ request_type: type[RequestPayload],
314
+ handler: Callable[[RequestPayload], ResultPayload],
315
+ library_data: LibrarySchema,
316
+ ) -> None:
317
+ """Register an event handler for a specific request type from a library."""
318
+ if self._library_event_handler_mappings.get(request_type) is None:
319
+ self._library_event_handler_mappings[request_type] = {}
320
+ self._library_event_handler_mappings[request_type][library_data.name] = LibraryManager.RegisteredEventHandler(
321
+ handler=handler, library_data=library_data
322
+ )
323
+
324
+ def get_registered_event_handlers(self, request_type: type[Payload]) -> dict[str, RegisteredEventHandler]:
325
+ """Get all registered event handlers for a specific request type."""
326
+ return self._library_event_handler_mappings.get(request_type, {})
327
+
328
+ def on_list_capable_event_handlers(self, request: ListCapableLibraryEventHandlersRequest) -> ResultPayload:
329
+ """Get all registered event handlers for a specific request type."""
330
+ request_type = PayloadRegistry.get_type(request.request_type)
331
+ if request_type is None:
332
+ return ListCapableLibraryEventHandlersResultFailure(
333
+ exception=KeyError(f"Request type '{request.request_type}' is not registered in the PayloadRegistry.")
334
+ )
335
+ handler_mappings = self.get_registered_event_handlers(request_type)
336
+ return ListCapableLibraryEventHandlersResultSuccess(handlers=list(handler_mappings.keys()))
337
+
280
338
  def on_list_registered_libraries_request(self, _request: ListRegisteredLibrariesRequest) -> ResultPayload:
281
339
  # Make a COPY of the list
282
340
  snapshot_list = LibraryRegistry.list_libraries()
@@ -742,6 +800,28 @@ class LibraryManager:
742
800
  logger.error(details)
743
801
  return RegisterLibraryFromFileResultFailure()
744
802
  if self._can_write_to_venv_location(library_venv_python_path):
803
+ # Check disk space before installing dependencies
804
+ config_manager = GriptapeNodes.ConfigManager()
805
+ min_space_gb = config_manager.get_config_value("minimum_disk_space_gb_libraries")
806
+ if not OSManager.check_available_disk_space(Path(venv_path), min_space_gb):
807
+ error_msg = OSManager.format_disk_space_error(Path(venv_path))
808
+ logger.error(
809
+ "Attempted to load Library JSON from '%s'. Failed when installing dependencies (requires %.1f GB): %s",
810
+ json_path,
811
+ min_space_gb,
812
+ error_msg,
813
+ )
814
+ self._library_file_path_to_info[file_path] = LibraryManager.LibraryInfo(
815
+ library_path=file_path,
816
+ library_name=library_data.name,
817
+ library_version=library_version,
818
+ status=LibraryManager.LibraryStatus.UNUSABLE,
819
+ problems=[
820
+ f"Insufficient disk space for dependencies (requires {min_space_gb} GB): {error_msg}"
821
+ ],
822
+ )
823
+ return RegisterLibraryFromFileResultFailure()
824
+
745
825
  # Grab the python executable from the virtual environment so that we can pip install there
746
826
  logger.info(
747
827
  "Installing dependencies for library '%s' with pip in venv at %s", library_data.name, venv_path
@@ -771,6 +851,7 @@ class LibraryManager:
771
851
  except subprocess.CalledProcessError as e:
772
852
  # Failed to create the library
773
853
  error_details = f"return code={e.returncode}, stdout={e.stdout}, stderr={e.stderr}"
854
+
774
855
  self._library_file_path_to_info[file_path] = LibraryManager.LibraryInfo(
775
856
  library_path=file_path,
776
857
  library_name=library_data.name,
@@ -863,10 +944,23 @@ class LibraryManager:
863
944
  logger.error(details)
864
945
  return RegisterLibraryFromRequirementSpecifierResultFailure()
865
946
  if self._can_write_to_venv_location(library_python_venv_path):
947
+ # Check disk space before installing dependencies
948
+ config_manager = GriptapeNodes.ConfigManager()
949
+ min_space_gb = config_manager.get_config_value("minimum_disk_space_gb_libraries")
950
+ if not OSManager.check_available_disk_space(Path(venv_path), min_space_gb):
951
+ error_msg = OSManager.format_disk_space_error(Path(venv_path))
952
+ logger.error(
953
+ "Attempted to install library '%s'. Failed when installing dependencies (requires %.1f GB): %s",
954
+ request.requirement_specifier,
955
+ min_space_gb,
956
+ error_msg,
957
+ )
958
+ return RegisterLibraryFromRequirementSpecifierResultFailure()
959
+
866
960
  logger.info("Installing dependency '%s' with pip in venv at %s", package_name, venv_path)
867
961
  subprocess.run( # noqa: S603
868
962
  [
869
- uv.find_uv_bin(),
963
+ _find_griptape_uv_bin(),
870
964
  "pip",
871
965
  "install",
872
966
  request.requirement_specifier,
@@ -922,6 +1016,19 @@ class LibraryManager:
922
1016
  if library_venv_path.exists():
923
1017
  logger.debug("Virtual environment already exists at %s", library_venv_path)
924
1018
  else:
1019
+ # Check disk space before creating virtual environment
1020
+ config_manager = GriptapeNodes.ConfigManager()
1021
+ min_space_gb = config_manager.get_config_value("minimum_disk_space_gb_libraries")
1022
+ if not OSManager.check_available_disk_space(library_venv_path.parent, min_space_gb):
1023
+ error_msg = OSManager.format_disk_space_error(library_venv_path.parent)
1024
+ logger.error(
1025
+ "Attempted to create virtual environment (requires %.1f GB). Failed: %s", min_space_gb, error_msg
1026
+ )
1027
+ error_message = (
1028
+ f"Disk space error creating virtual environment (requires {min_space_gb} GB): {error_msg}"
1029
+ )
1030
+ raise RuntimeError(error_message)
1031
+
925
1032
  try:
926
1033
  logger.info("Creating virtual environment at %s with Python %s", library_venv_path, python_version)
927
1034
  subprocess.run( # noqa: S603
@@ -1441,6 +1548,9 @@ class LibraryManager:
1441
1548
  self._remove_missing_libraries_from_config(config_category=user_libraries_section)
1442
1549
 
1443
1550
  def on_app_initialization_complete(self, _payload: AppInitializationComplete) -> None:
1551
+ GriptapeNodes.EngineIdentityManager().initialize_engine_id()
1552
+ GriptapeNodes.SessionManager().get_saved_session_id()
1553
+
1444
1554
  # App just got init'd. See if there are library JSONs to load!
1445
1555
  self.load_all_libraries_from_config()
1446
1556
 
@@ -185,23 +185,24 @@ class NodeManager:
185
185
  # Get all connections for this node and update them.
186
186
  flow_name = self.get_node_parent_flow_by_name(old_name)
187
187
  flow = GriptapeNodes.FlowManager().get_flow_by_name(flow_name)
188
+ connections = GriptapeNodes.FlowManager().get_connections()
188
189
  # Get all incoming and outgoing connections and update them.
189
- if old_name in flow.connections.incoming_index:
190
- incoming_connections = flow.connections.incoming_index[old_name]
190
+ if old_name in connections.incoming_index:
191
+ incoming_connections = connections.incoming_index[old_name]
191
192
  for connection_ids in incoming_connections.values():
192
193
  for connection_id in connection_ids:
193
- connection = flow.connections.connections[connection_id]
194
+ connection = connections.connections[connection_id]
194
195
  connection.target_node.name = new_name
195
- temp = flow.connections.incoming_index.pop(old_name)
196
- flow.connections.incoming_index[new_name] = temp
197
- if old_name in flow.connections.outgoing_index:
198
- outgoing_connections = flow.connections.outgoing_index[old_name]
196
+ temp = connections.incoming_index.pop(old_name)
197
+ connections.incoming_index[new_name] = temp
198
+ if old_name in connections.outgoing_index:
199
+ outgoing_connections = connections.outgoing_index[old_name]
199
200
  for connection_ids in outgoing_connections.values():
200
201
  for connection_id in connection_ids:
201
- connection = flow.connections.connections[connection_id]
202
+ connection = connections.connections[connection_id]
202
203
  connection.source_node.name = new_name
203
- temp = flow.connections.outgoing_index.pop(old_name)
204
- flow.connections.outgoing_index[new_name] = temp
204
+ temp = connections.outgoing_index.pop(old_name)
205
+ connections.outgoing_index[new_name] = temp
205
206
  # update the node in the flow!
206
207
  flow.remove_node(old_name)
207
208
  node.name = new_name
@@ -395,11 +396,11 @@ class NodeManager:
395
396
  This method also clears the flow queue regardless of whether cancellation occurred,
396
397
  to ensure the specified node is not processed in the future.
397
398
  """
398
- if parent_flow.check_for_existing_running_flow():
399
+ if GriptapeNodes.FlowManager().check_for_existing_running_flow():
399
400
  # get the current node executing / resolving
400
401
  # if it's in connected nodes, cancel flow.
401
402
  # otherwise, leave it.
402
- control_node_name, resolving_node_name = parent_flow.flow_state()
403
+ control_node_name, resolving_node_name = GriptapeNodes.FlowManager().flow_state(parent_flow)
403
404
  connected_nodes = parent_flow.get_all_connected_nodes(node)
404
405
  cancelled = False
405
406
  if control_node_name is not None:
@@ -423,8 +424,8 @@ class NodeManager:
423
424
  )
424
425
  logger.error(details)
425
426
  return DeleteNodeResultFailure()
426
- # Clear the queue, because we don't want to his this node eventually.
427
- parent_flow.clear_flow_queue()
427
+ # Clear the execution queue, because we don't want to hit this node eventually.
428
+ parent_flow.clear_execution_queue()
428
429
  return None
429
430
 
430
431
  def on_delete_node_request(self, request: DeleteNodeRequest) -> ResultPayload: # noqa: C901, PLR0911 (complex logic, lots of edge cases)
@@ -640,7 +641,7 @@ class NodeManager:
640
641
 
641
642
  parent_flow_name = self._name_to_parent_flow_name[node_name]
642
643
  try:
643
- parent_flow = GriptapeNodes.FlowManager().get_flow_by_name(parent_flow_name)
644
+ GriptapeNodes.FlowManager().get_flow_by_name(parent_flow_name)
644
645
  except KeyError as err:
645
646
  details = f"Attempted to list Connections for a Node '{node_name}'. Error: {err}"
646
647
  logger.error(details)
@@ -649,7 +650,7 @@ class NodeManager:
649
650
  return result
650
651
 
651
652
  # Kinda gross, but let's do it
652
- connection_mgr = parent_flow.connections
653
+ connection_mgr = GriptapeNodes.FlowManager().get_connections()
653
654
  # get outgoing connections
654
655
  outgoing_connections_list = []
655
656
  if node_name in connection_mgr.outgoing_index:
@@ -1429,7 +1430,7 @@ class NodeManager:
1429
1430
  return SetParameterValueResultFailure()
1430
1431
  if not request.initial_setup and modified:
1431
1432
  try:
1432
- parent_flow.connections.unresolve_future_nodes(node)
1433
+ GriptapeNodes.FlowManager().get_connections().unresolve_future_nodes(node)
1433
1434
  except Exception as err:
1434
1435
  details = f"Attempted to set parameter value for '{node_name}.{request.parameter_name}'. Failed because Exception: {err}"
1435
1436
  logger.error(details)
@@ -1752,12 +1753,12 @@ class NodeManager:
1752
1753
  details = f'Failed to fetch parent flow for "{node_name}"'
1753
1754
  logger.error(details)
1754
1755
  return ResolveNodeResultFailure(validation_exceptions=[])
1755
- if flow.check_for_existing_running_flow():
1756
+ if GriptapeNodes.FlowManager().check_for_existing_running_flow():
1756
1757
  details = f"Failed to resolve from node '{node_name}'. Flow is already running."
1757
1758
  logger.error(details)
1758
1759
  return ResolveNodeResultFailure(validation_exceptions=[])
1759
1760
  try:
1760
- flow.connections.unresolve_future_nodes(node)
1761
+ GriptapeNodes.FlowManager().get_connections().unresolve_future_nodes(node)
1761
1762
  except Exception as e:
1762
1763
  details = f'Failed to mark future nodes dirty. Unable to kick off flow from "{node_name}": {e}'
1763
1764
  logger.error(details)
@@ -1783,7 +1784,7 @@ class NodeManager:
1783
1784
  logger.error(details)
1784
1785
  return StartFlowResultFailure(validation_exceptions=[e])
1785
1786
  try:
1786
- flow.resolve_singular_node(node, debug_mode)
1787
+ GriptapeNodes.FlowManager().resolve_singular_node(flow, node, debug_mode)
1787
1788
  except Exception as e:
1788
1789
  details = f'Failed to resolve "{node_name}". Error: {e}'
1789
1790
  logger.error(details)
@@ -2098,15 +2099,16 @@ class NodeManager:
2098
2099
  parameter_commands[result.serialized_node_commands.node_uuid] = result.set_parameter_value_commands
2099
2100
  try:
2100
2101
  flow_name = self.get_node_parent_flow_by_name(node_name)
2101
- flow = GriptapeNodes.FlowManager().get_flow_by_name(flow_name)
2102
+ GriptapeNodes.FlowManager().get_flow_by_name(flow_name)
2102
2103
  except Exception:
2103
2104
  details = f"Attempted to serialize a selection of Nodes. Failed to get the flow of node {node_name}. Cannot serialize connections for this node."
2104
2105
  logger.warning(details)
2105
2106
  continue
2106
- if node_name in flow.connections.outgoing_index:
2107
+ connections = GriptapeNodes.FlowManager().get_connections()
2108
+ if node_name in connections.outgoing_index:
2107
2109
  node_connections = [
2108
- flow.connections.connections[connection_id]
2109
- for category_dict in flow.connections.outgoing_index[node_name].values()
2110
+ connections.connections[connection_id]
2111
+ for category_dict in connections.outgoing_index[node_name].values()
2110
2112
  for connection_id in category_dict
2111
2113
  ]
2112
2114
  for connection in node_connections:
@@ -2215,7 +2217,7 @@ class NodeManager:
2215
2217
  details = "Failed to serialized selected nodes."
2216
2218
  logger.error(details)
2217
2219
  return DuplicateSelectedNodesResultFailure()
2218
- result = GriptapeNodes.handle_request(DeserializeSelectedNodesFromCommandsRequest(positions=None))
2220
+ result = GriptapeNodes.handle_request(DeserializeSelectedNodesFromCommandsRequest(positions=request.positions))
2219
2221
  if not isinstance(result, DeserializeSelectedNodesFromCommandsResultSuccess):
2220
2222
  details = "Failed to deserialize selected nodes."
2221
2223
  logger.error(details)
@@ -2420,22 +2422,23 @@ class NodeManager:
2420
2422
 
2421
2423
  # Get all connections for this node
2422
2424
  flow_name = self.get_node_parent_flow_by_name(node_name)
2423
- flow = GriptapeNodes.FlowManager().get_flow_by_name(flow_name)
2425
+ GriptapeNodes.FlowManager().get_flow_by_name(flow_name)
2426
+ connections = GriptapeNodes.FlowManager().get_connections()
2424
2427
 
2425
2428
  # Update connections that reference this parameter
2426
- if node_name in flow.connections.incoming_index:
2427
- incoming_connections = flow.connections.incoming_index[node_name]
2429
+ if node_name in connections.incoming_index:
2430
+ incoming_connections = connections.incoming_index[node_name]
2428
2431
  for connection_ids in incoming_connections.values():
2429
2432
  for connection_id in connection_ids:
2430
- connection = flow.connections.connections[connection_id]
2433
+ connection = connections.connections[connection_id]
2431
2434
  if connection.target_parameter.name == request.parameter_name:
2432
2435
  connection.target_parameter.name = request.new_parameter_name
2433
2436
 
2434
- if node_name in flow.connections.outgoing_index:
2435
- outgoing_connections = flow.connections.outgoing_index[node_name]
2437
+ if node_name in connections.outgoing_index:
2438
+ outgoing_connections = connections.outgoing_index[node_name]
2436
2439
  for connection_ids in outgoing_connections.values():
2437
2440
  for connection_id in connection_ids:
2438
- connection = flow.connections.connections[connection_id]
2441
+ connection = connections.connections[connection_id]
2439
2442
  if connection.source_parameter.name == request.parameter_name:
2440
2443
  connection.source_parameter.name = request.new_parameter_name
2441
2444
 
@@ -100,7 +100,7 @@ class ObjectManager:
100
100
  logger.log(level=log_level, msg=details)
101
101
  return RenameObjectResultSuccess(final_name=final_name)
102
102
 
103
- def on_clear_all_object_state_request(self, request: ClearAllObjectStateRequest) -> ResultPayload:
103
+ def on_clear_all_object_state_request(self, request: ClearAllObjectStateRequest) -> ResultPayload: # noqa: C901
104
104
  if not request.i_know_what_im_doing:
105
105
  logger.warning(
106
106
  "Attempted to clear all object state and delete everything. Failed because they didn't know what they were doing."
@@ -109,14 +109,22 @@ class ObjectManager:
109
109
  # Let's try and clear it all.
110
110
  # Cancel any running flows.
111
111
  flows = self.get_filtered_subset(type=ControlFlow)
112
- for flow_name, flow in flows.items():
113
- if flow.check_for_existing_running_flow():
112
+ for flow_name in flows:
113
+ if GriptapeNodes.FlowManager().check_for_existing_running_flow():
114
114
  result = GriptapeNodes.handle_request(CancelFlowRequest(flow_name=flow_name))
115
115
  if not result.succeeded():
116
116
  details = f"Attempted to clear all object state and delete everything. Failed because running flow '{flow_name}' could not cancel."
117
117
  logger.error(details)
118
118
  return ClearAllObjectStateResultFailure()
119
119
 
120
+ try:
121
+ # Reset global execution state first to eliminate all references before deletion
122
+ GriptapeNodes.FlowManager().reset_global_execution_state()
123
+ except Exception as e:
124
+ details = f"Attempted to reset global execution state. Failed with exception: {e}"
125
+ logger.error(details)
126
+ return ClearAllObjectStateResultFailure()
127
+
120
128
  try:
121
129
  # Delete the existing flows, which will clear all nodes and connections.
122
130
  GriptapeNodes.clear_data()
@@ -1,9 +1,10 @@
1
1
  import logging
2
2
  import os
3
+ import shutil
3
4
  import subprocess
4
5
  import sys
5
6
  from pathlib import Path
6
- from typing import Any
7
+ from typing import Any, NamedTuple
7
8
 
8
9
  from rich.console import Console
9
10
 
@@ -19,6 +20,14 @@ console = Console()
19
20
  logger = logging.getLogger("griptape_nodes")
20
21
 
21
22
 
23
+ class DiskSpaceInfo(NamedTuple):
24
+ """Disk space information in bytes."""
25
+
26
+ total: int
27
+ used: int
28
+ free: int
29
+
30
+
22
31
  class OSManager:
23
32
  """A class to manage OS-level scenarios.
24
33
 
@@ -129,3 +138,63 @@ class OSManager:
129
138
  except Exception as e:
130
139
  logger.error("Exception occurred when trying to open file: %s", type(e).__name__)
131
140
  return OpenAssociatedFileResultFailure()
141
+
142
+ @staticmethod
143
+ def get_disk_space_info(path: Path) -> DiskSpaceInfo:
144
+ """Get disk space information for a given path.
145
+
146
+ Args:
147
+ path: The path to check disk space for.
148
+
149
+ Returns:
150
+ DiskSpaceInfo with total, used, and free disk space in bytes.
151
+ """
152
+ stat = shutil.disk_usage(path)
153
+ return DiskSpaceInfo(total=stat.total, used=stat.used, free=stat.free)
154
+
155
+ @staticmethod
156
+ def check_available_disk_space(path: Path, required_gb: float) -> bool:
157
+ """Check if there is sufficient disk space available.
158
+
159
+ Args:
160
+ path: The path to check disk space for.
161
+ required_gb: The minimum disk space required in GB.
162
+
163
+ Returns:
164
+ True if sufficient space is available, False otherwise.
165
+ """
166
+ try:
167
+ disk_info = OSManager.get_disk_space_info(path)
168
+ required_bytes = int(required_gb * 1024 * 1024 * 1024) # Convert GB to bytes
169
+ return disk_info.free >= required_bytes # noqa: TRY300
170
+ except OSError:
171
+ return False
172
+
173
+ @staticmethod
174
+ def format_disk_space_error(path: Path, exception: Exception | None = None) -> str:
175
+ """Format a user-friendly disk space error message.
176
+
177
+ Args:
178
+ path: The path where the disk space issue occurred.
179
+ exception: The original exception, if any.
180
+
181
+ Returns:
182
+ A formatted error message with disk space information.
183
+ """
184
+ try:
185
+ disk_info = OSManager.get_disk_space_info(path)
186
+ free_gb = disk_info.free / (1024**3)
187
+ used_gb = disk_info.used / (1024**3)
188
+ total_gb = disk_info.total / (1024**3)
189
+
190
+ error_msg = f"Insufficient disk space at {path}. "
191
+ error_msg += f"Available: {free_gb:.2f} GB, Used: {used_gb:.2f} GB, Total: {total_gb:.2f} GB. "
192
+
193
+ if exception:
194
+ error_msg += f"Error: {exception}"
195
+ else:
196
+ error_msg += "Please free up disk space and try again."
197
+
198
+ return error_msg # noqa: TRY300
199
+ except OSError:
200
+ return f"Disk space error at {path}. Unable to retrieve disk space information."
@@ -32,6 +32,10 @@ class SecretsManager:
32
32
  def __init__(self, config_manager: ConfigManager, event_manager: EventManager | None = None) -> None:
33
33
  self.config_manager = config_manager
34
34
 
35
+ # So that users can access secrets directly via `os.environ`
36
+ load_dotenv(self.workspace_env_path, override=False)
37
+ load_dotenv(ENV_VAR_PATH, override=False)
38
+
35
39
  # Register all our listeners.
36
40
  if event_manager is not None:
37
41
  event_manager.assign_manager_to_request_type(GetSecretValueRequest, self.on_handle_get_secret_request)