griptape-nodes 0.65.6__py3-none-any.whl → 0.66.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. griptape_nodes/common/node_executor.py +352 -27
  2. griptape_nodes/drivers/storage/base_storage_driver.py +12 -3
  3. griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +18 -2
  4. griptape_nodes/drivers/storage/local_storage_driver.py +42 -5
  5. griptape_nodes/exe_types/base_iterative_nodes.py +0 -1
  6. griptape_nodes/exe_types/connections.py +42 -0
  7. griptape_nodes/exe_types/core_types.py +2 -2
  8. griptape_nodes/exe_types/node_groups/__init__.py +2 -1
  9. griptape_nodes/exe_types/node_groups/base_iterative_node_group.py +177 -0
  10. griptape_nodes/exe_types/node_groups/base_node_group.py +1 -0
  11. griptape_nodes/exe_types/node_groups/subflow_node_group.py +35 -2
  12. griptape_nodes/exe_types/param_types/parameter_audio.py +1 -1
  13. griptape_nodes/exe_types/param_types/parameter_bool.py +1 -1
  14. griptape_nodes/exe_types/param_types/parameter_button.py +1 -1
  15. griptape_nodes/exe_types/param_types/parameter_float.py +1 -1
  16. griptape_nodes/exe_types/param_types/parameter_image.py +1 -1
  17. griptape_nodes/exe_types/param_types/parameter_int.py +1 -1
  18. griptape_nodes/exe_types/param_types/parameter_number.py +1 -1
  19. griptape_nodes/exe_types/param_types/parameter_string.py +1 -1
  20. griptape_nodes/exe_types/param_types/parameter_three_d.py +1 -1
  21. griptape_nodes/exe_types/param_types/parameter_video.py +1 -1
  22. griptape_nodes/machines/control_flow.py +5 -4
  23. griptape_nodes/machines/dag_builder.py +121 -55
  24. griptape_nodes/machines/fsm.py +10 -0
  25. griptape_nodes/machines/parallel_resolution.py +39 -38
  26. griptape_nodes/machines/sequential_resolution.py +29 -3
  27. griptape_nodes/node_library/library_registry.py +41 -2
  28. griptape_nodes/retained_mode/events/library_events.py +147 -8
  29. griptape_nodes/retained_mode/events/os_events.py +12 -4
  30. griptape_nodes/retained_mode/managers/fitness_problems/libraries/__init__.py +2 -0
  31. griptape_nodes/retained_mode/managers/fitness_problems/libraries/incompatible_requirements_problem.py +34 -0
  32. griptape_nodes/retained_mode/managers/flow_manager.py +133 -20
  33. griptape_nodes/retained_mode/managers/library_manager.py +1324 -564
  34. griptape_nodes/retained_mode/managers/node_manager.py +9 -3
  35. griptape_nodes/retained_mode/managers/os_manager.py +429 -65
  36. griptape_nodes/retained_mode/managers/resource_types/compute_resource.py +82 -0
  37. griptape_nodes/retained_mode/managers/resource_types/os_resource.py +17 -0
  38. griptape_nodes/retained_mode/managers/static_files_manager.py +21 -8
  39. griptape_nodes/retained_mode/managers/version_compatibility_manager.py +3 -3
  40. griptape_nodes/version_compatibility/versions/v0_39_0/modified_parameters_set_removal.py +5 -5
  41. griptape_nodes/version_compatibility/versions/v0_65_4/__init__.py +5 -0
  42. griptape_nodes/version_compatibility/versions/v0_65_4/run_in_parallel_to_run_in_order.py +79 -0
  43. griptape_nodes/version_compatibility/versions/v0_65_5/__init__.py +5 -0
  44. griptape_nodes/version_compatibility/versions/v0_65_5/flux_2_removed_parameters.py +85 -0
  45. {griptape_nodes-0.65.6.dist-info → griptape_nodes-0.66.1.dist-info}/METADATA +1 -1
  46. {griptape_nodes-0.65.6.dist-info → griptape_nodes-0.66.1.dist-info}/RECORD +48 -53
  47. griptape_nodes/retained_mode/managers/library_lifecycle/__init__.py +0 -45
  48. griptape_nodes/retained_mode/managers/library_lifecycle/data_models.py +0 -191
  49. griptape_nodes/retained_mode/managers/library_lifecycle/library_directory.py +0 -346
  50. griptape_nodes/retained_mode/managers/library_lifecycle/library_fsm.py +0 -439
  51. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/__init__.py +0 -17
  52. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/base.py +0 -82
  53. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/github.py +0 -116
  54. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/local_file.py +0 -367
  55. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/package.py +0 -104
  56. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/sandbox.py +0 -155
  57. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance.py +0 -18
  58. griptape_nodes/retained_mode/managers/library_lifecycle/library_status.py +0 -12
  59. {griptape_nodes-0.65.6.dist-info → griptape_nodes-0.66.1.dist-info}/WHEEL +0 -0
  60. {griptape_nodes-0.65.6.dist-info → griptape_nodes-0.66.1.dist-info}/entry_points.txt +0 -0
@@ -3081,9 +3081,15 @@ class NodeManager:
3081
3081
  if parameter.name in node.parameter_output_values:
3082
3082
  # Output values are more important.
3083
3083
  output_value = node.parameter_output_values[parameter.name]
3084
- if parameter.name in node.parameter_values:
3085
- # Check the internal parameter values
3086
- internal_value = node.get_parameter_value(parameter.name)
3084
+ # Get the effective value to check if it matches the default
3085
+ effective_value = node.get_parameter_value(parameter.name)
3086
+ # Save the value if it was explicitly set OR if it equals the default value.
3087
+ # The latter ensures the default is preserved when loading workflows,
3088
+ # even if the code's default value changes later.
3089
+ if parameter.name in node.parameter_values or (
3090
+ parameter.default_value is not None and effective_value == parameter.default_value
3091
+ ):
3092
+ internal_value = effective_value
3087
3093
  # We have a value. Attempt to get a hash for it to see if it matches one
3088
3094
  # we've already indexed.
3089
3095
  commands = []
@@ -21,7 +21,6 @@ from griptape_nodes.common.macro_parser.exceptions import MacroResolutionFailure
21
21
  from griptape_nodes.common.macro_parser.formats import NumericPaddingFormat
22
22
  from griptape_nodes.common.macro_parser.resolution import partial_resolve
23
23
  from griptape_nodes.common.macro_parser.segments import ParsedStaticValue, ParsedVariable
24
- from griptape_nodes.retained_mode.events.app_events import AppInitializationComplete
25
24
  from griptape_nodes.retained_mode.events.base_events import ResultDetails, ResultPayload
26
25
  from griptape_nodes.retained_mode.events.os_events import (
27
26
  CopyFileRequest,
@@ -70,8 +69,9 @@ from griptape_nodes.retained_mode.events.resource_events import (
70
69
  )
71
70
  from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes, logger
72
71
  from griptape_nodes.retained_mode.managers.event_manager import EventManager
72
+ from griptape_nodes.retained_mode.managers.resource_types.compute_resource import ComputeBackend, ComputeResourceType
73
73
  from griptape_nodes.retained_mode.managers.resource_types.cpu_resource import CPUResourceType
74
- from griptape_nodes.retained_mode.managers.resource_types.os_resource import OSResourceType
74
+ from griptape_nodes.retained_mode.managers.resource_types.os_resource import Architecture, OSResourceType, Platform
75
75
 
76
76
  console = Console()
77
77
 
@@ -218,8 +218,13 @@ class OSManager:
218
218
  request_type=GetFileInfoRequest, callback=self.on_get_file_info_request
219
219
  )
220
220
 
221
- # Register for app initialization event to setup system resources
222
- event_manager.add_listener_to_app_event(AppInitializationComplete, self.on_app_initialization_complete)
221
+ # Store event_manager for direct access during resource registration
222
+ self._event_manager = event_manager
223
+
224
+ # Register system resources immediately using the event_manager directly
225
+ # This must happen before libraries are loaded so they can check requirements
226
+ # We use event_manager directly to avoid singleton recursion issues
227
+ self._register_system_resources_direct()
223
228
 
224
229
  def _get_workspace_path(self) -> Path:
225
230
  """Get the workspace path from config."""
@@ -462,8 +467,14 @@ class OSManager:
462
467
  return path_str[1:-1]
463
468
  return path_str
464
469
 
465
- def sanitize_path_string(self, path_str: str) -> str:
466
- r"""Strip surrounding quotes and shell escape characters from paths.
470
+ def sanitize_path_string(self, path: str | Path | Any) -> str | Any:
471
+ r"""Clean path strings by removing newlines, carriage returns, shell escapes, and quotes.
472
+
473
+ This method handles multiple path cleaning concerns:
474
+ 1. Removes newlines/carriage returns that cause WinError 123 on Windows
475
+ (from merge_texts nodes accidentally adding newlines between path components)
476
+ 2. Removes shell escape characters and quotes (from macOS Finder 'Copy as Pathname')
477
+ 3. Strips leading/trailing whitespace
467
478
 
468
479
  Handles macOS Finder's 'Copy as Pathname' format which escapes
469
480
  spaces, apostrophes, and other special characters with backslashes.
@@ -471,7 +482,7 @@ class OSManager:
471
482
  breaking Windows paths like C:\Users\file.txt.
472
483
 
473
484
  Examples:
474
- macOS Finder paths (the reason this exists!):
485
+ macOS Finder paths:
475
486
  "/Downloads/Dragon\'s\ Curse/screenshot.jpg"
476
487
  -> "/Downloads/Dragon's Curse/screenshot.jpg"
477
488
 
@@ -482,22 +493,33 @@ class OSManager:
482
493
  '"/path/with spaces/file.txt"'
483
494
  -> "/path/with spaces/file.txt"
484
495
 
485
- Windows paths (preserved correctly):
486
- "C:\Users\Desktop\file.txt"
487
- -> "C:\Users\Desktop\file.txt"
496
+ Windows paths with newlines:
497
+ "C:\\Users\\file\\n\\n.txt"
498
+ -> "C:\\Users\\file.txt"
488
499
 
489
500
  Windows extended-length paths:
490
501
  r"\\?\C:\Very\ Long\ Path\file.txt"
491
502
  -> r"\\?\C:\Very Long Path\file.txt"
492
503
 
504
+ Path objects:
505
+ Path("/path/to/file")
506
+ -> "/path/to/file"
507
+
493
508
  Args:
494
- path_str: The path string to sanitize
509
+ path: Path string, Path object, or any other type to sanitize
495
510
 
496
511
  Returns:
497
- Sanitized path string
512
+ Sanitized path string, or original value if not a string/Path
498
513
  """
514
+ # Convert Path objects to strings
515
+ if isinstance(path, Path):
516
+ path = str(path)
517
+
518
+ if not isinstance(path, str):
519
+ return path
520
+
499
521
  # First, strip surrounding quotes
500
- path_str = OSManager.strip_surrounding_quotes(path_str)
522
+ path_str = OSManager.strip_surrounding_quotes(path)
501
523
 
502
524
  # Handle Windows extended-length paths (\\?\...) specially
503
525
  # These are used for paths longer than 260 characters on Windows
@@ -512,6 +534,12 @@ class OSManager:
512
534
  # Does NOT match: \U \t \f etc in Windows paths like C:\Users
513
535
  path_str = re.sub(r"\\([ '\"(){}[\]&|;<>$`!*?/])", r"\1", path_str)
514
536
 
537
+ # Remove newlines and carriage returns from anywhere in the path
538
+ path_str = path_str.replace("\n", "").replace("\r", "")
539
+
540
+ # Strip leading/trailing whitespace
541
+ path_str = path_str.strip()
542
+
515
543
  # Restore extended-length prefix if it was present
516
544
  if extended_length_prefix:
517
545
  path_str = extended_length_prefix + path_str
@@ -525,6 +553,8 @@ class OSManager:
525
553
  need the \\?\ prefix to work correctly. This method transparently adds
526
554
  the prefix when needed on Windows.
527
555
 
556
+ Also cleans paths to remove newlines/carriage returns that cause Windows errors.
557
+
528
558
  Note: This method assumes the path exists or will exist. For non-existent
529
559
  paths that need cross-platform normalization, use resolve_path_safely() first.
530
560
 
@@ -532,10 +562,15 @@ class OSManager:
532
562
  path: Path object to convert to string
533
563
 
534
564
  Returns:
535
- String representation of path, with Windows long path prefix if needed
565
+ String representation of path, cleaned of newlines/carriage returns,
566
+ with Windows long path prefix if needed
536
567
  """
537
568
  path_str = str(path.resolve())
538
569
 
570
+ # Clean path to remove newlines/carriage returns, shell escapes, and quotes
571
+ # This handles cases where merge_texts nodes accidentally add newlines between path components
572
+ path_str = self.sanitize_path_string(path_str)
573
+
539
574
  # Windows long path handling (paths > WINDOWS_MAX_PATH chars need \\?\ prefix)
540
575
  if self.is_windows() and len(path_str) >= WINDOWS_MAX_PATH and not path_str.startswith("\\\\?\\"):
541
576
  # UNC paths (\\server\share) need \\?\UNC\ prefix
@@ -1110,22 +1145,55 @@ class OSManager:
1110
1145
  logger.error(details)
1111
1146
  return OpenAssociatedFileResultFailure(failure_reason=FileIOFailureReason.UNKNOWN, result_details=details)
1112
1147
 
1148
+ def _is_hidden(self, dir_entry: os.DirEntry, stat_result: os.stat_result | None = None) -> bool:
1149
+ """Check if a directory entry is hidden in an OS-independent way.
1150
+
1151
+ On Unix/Linux/macOS: Files are considered hidden if their name starts with a dot (.).
1152
+ On Windows: Files have a special "hidden" file attribute (FILE_ATTRIBUTE_HIDDEN).
1153
+
1154
+ Args:
1155
+ dir_entry: The directory entry to check
1156
+ stat_result: Optional pre-fetched stat result (to avoid redundant stat() calls on Windows)
1157
+
1158
+ Returns:
1159
+ True if the entry is hidden, False otherwise
1160
+ """
1161
+ if sys.platform == "win32":
1162
+ # Windows: Check name prefix first (fast heuristic for most hidden files)
1163
+ # Most hidden files on Windows have dot prefix, so this avoids many stat() calls
1164
+ if dir_entry.name.startswith("."):
1165
+ return True
1166
+ # For files without dot prefix, check FILE_ATTRIBUTE_HIDDEN via stat()
1167
+ if stat_result is None:
1168
+ stat_result = dir_entry.stat(follow_symlinks=False)
1169
+ return bool(stat_result.st_file_attributes & stat.FILE_ATTRIBUTE_HIDDEN)
1170
+ # Unix/Linux/macOS: Files are hidden if name starts with dot
1171
+ return dir_entry.name.startswith(".")
1172
+
1113
1173
  def _detect_mime_type(self, file_path: Path) -> str | None:
1114
- """Detect MIME type for a file. Returns None for directories or if detection fails."""
1174
+ """Detect MIME type for a file. Returns None for directories or if detection fails.
1175
+
1176
+ Args:
1177
+ file_path: Original file path (used for is_dir() check and filename extraction)
1178
+ """
1115
1179
  if file_path.is_dir():
1116
1180
  return None
1117
1181
 
1182
+ # mimetypes.guess_type() only needs the filename, not the full path
1183
+ # Using just the filename is ~2x faster and avoids path normalization overhead
1184
+ filename = file_path.name
1118
1185
  try:
1119
- mime_type, _ = mimetypes.guess_type(self.normalize_path_for_platform(file_path), strict=True)
1120
- if mime_type is None:
1121
- mime_type = "text/plain"
1122
- return mime_type # noqa: TRY300
1186
+ mime_type, _ = mimetypes.guess_type(filename, strict=True)
1123
1187
  except Exception as e:
1124
- msg = f"MIME type detection failed for {file_path}: {e}"
1188
+ msg = f"MIME type detection failed for {file_path} (filename: {filename}): {e}"
1125
1189
  logger.warning(msg)
1126
1190
  return "text/plain"
1127
1191
 
1128
- def on_list_directory_request(self, request: ListDirectoryRequest) -> ResultPayload: # noqa: C901, PLR0911, PLR0912
1192
+ if mime_type is None:
1193
+ mime_type = "text/plain"
1194
+ return mime_type
1195
+
1196
+ def on_list_directory_request(self, request: ListDirectoryRequest) -> ResultPayload: # noqa: C901, PLR0911, PLR0912, PLR0915
1129
1197
  """Handle a request to list directory contents."""
1130
1198
  try:
1131
1199
  # Get the directory path to list
@@ -1156,40 +1224,112 @@ class OSManager:
1156
1224
  logger.error(msg)
1157
1225
  return ListDirectoryResultFailure(failure_reason=FileIOFailureReason.INVALID_PATH, result_details=msg)
1158
1226
 
1227
+ # Cache workspace path and resolved workspace to avoid repeated lookups/resolutions
1228
+ # Only resolve workspace if we need it for relative paths or absolute paths
1229
+ need_relative_paths = request.workspace_only is True
1230
+ workspace_path = GriptapeNodes.ConfigManager().workspace_path
1231
+ if need_relative_paths or request.include_absolute_path:
1232
+ resolved_workspace = workspace_path.resolve()
1233
+ else:
1234
+ resolved_workspace = None
1235
+
1159
1236
  entries = []
1160
1237
  try:
1161
- # List directory contents
1162
- for entry in directory.iterdir():
1163
- # Skip hidden files if not requested
1164
- if not request.show_hidden and entry.name.startswith("."):
1165
- continue
1166
-
1167
- # Apply pattern filter if specified
1168
- if request.pattern is not None and not entry.match(request.pattern):
1169
- continue
1238
+ # Pre-compute whether we need stat() calls (constant for all entries)
1239
+ need_stat_for_metadata = request.include_size or request.include_modified_time
1240
+ # On Windows, we need stat() to check FILE_ATTRIBUTE_HIDDEN when filtering hidden files
1241
+ # (only for files without dot prefix, since dot-prefix files are handled by name check)
1242
+ need_stat_for_hidden = not request.show_hidden and sys.platform == "win32"
1243
+
1244
+ # Use os.scandir() instead of Path.iterdir() for better performance
1245
+ # os.scandir() is ~3.7x faster and provides cached stat info
1246
+ with os.scandir(str(directory)) as scan_iter:
1247
+ for dir_entry in scan_iter:
1248
+ # Initialize stat - we'll get it once if needed for hidden check and/or metadata
1249
+ stat = None
1250
+
1251
+ # Skip hidden files if not requested (OS-independent check)
1252
+ if not request.show_hidden:
1253
+ # On Windows, files without dot prefix need stat() to check FILE_ATTRIBUTE_HIDDEN
1254
+ # Get stat() once if needed (for hidden check and/or metadata)
1255
+ if need_stat_for_hidden and not dir_entry.name.startswith("."):
1256
+ stat = dir_entry.stat(follow_symlinks=False)
1257
+
1258
+ if self._is_hidden(dir_entry, stat_result=stat):
1259
+ continue
1260
+
1261
+ # Apply pattern filter if specified, or create Path object if needed
1262
+ if request.pattern is not None:
1263
+ # Convert DirEntry to Path for pattern matching
1264
+ entry_path_obj = Path(dir_entry.path)
1265
+ if not entry_path_obj.match(request.pattern):
1266
+ continue
1267
+ elif request.include_absolute_path or request.include_mime_type or need_relative_paths:
1268
+ # Only create Path object if we need it
1269
+ entry_path_obj = Path(dir_entry.path)
1270
+ else:
1271
+ entry_path_obj = None
1170
1272
 
1171
- try:
1172
- stat = entry.stat()
1173
- # Get path relative to workspace if within workspace
1174
- _, entry_path = self._validate_workspace_path(entry)
1175
- # Also get absolute resolved path
1176
- absolute_resolved_path = str(entry.resolve())
1177
- mime_type = self._detect_mime_type(entry)
1178
- entries.append(
1179
- FileSystemEntry(
1180
- name=entry.name,
1181
- path=str(entry_path),
1182
- is_dir=entry.is_dir(),
1183
- size=stat.st_size,
1184
- modified_time=stat.st_mtime,
1185
- mime_type=mime_type,
1186
- absolute_path=absolute_resolved_path,
1273
+ try:
1274
+ # Get stat() if needed for metadata (reuse if we already have it from hidden check)
1275
+ if need_stat_for_metadata and stat is None:
1276
+ stat = dir_entry.stat(follow_symlinks=False)
1277
+
1278
+ # Only resolve entry path if we need absolute_path or relative paths
1279
+ resolved_entry = None
1280
+ absolute_resolved_path = ""
1281
+ if request.include_absolute_path or need_relative_paths:
1282
+ if entry_path_obj is None:
1283
+ entry_path_obj = Path(dir_entry.path)
1284
+ resolved_entry = entry_path_obj.resolve()
1285
+ absolute_resolved_path = str(resolved_entry) if request.include_absolute_path else ""
1286
+
1287
+ # Determine entry_path based on what we need
1288
+ if need_relative_paths and resolved_entry is not None and resolved_workspace is not None:
1289
+ try:
1290
+ relative = resolved_entry.relative_to(resolved_workspace)
1291
+ entry_path = relative
1292
+ except ValueError:
1293
+ # Entry is outside workspace
1294
+ entry_path = resolved_entry
1295
+ elif request.include_absolute_path and resolved_entry is not None:
1296
+ entry_path = resolved_entry
1297
+ else:
1298
+ # Use the path from dir_entry (may be relative or absolute depending on system)
1299
+ entry_path = dir_entry.path
1300
+
1301
+ # Only detect MIME type if requested
1302
+ mime_type = None
1303
+ if request.include_mime_type:
1304
+ if entry_path_obj is None:
1305
+ entry_path_obj = Path(dir_entry.path)
1306
+ # Use resolved_entry if available, otherwise just entry_path_obj
1307
+ mime_type = self._detect_mime_type(entry_path_obj)
1308
+
1309
+ # Determine size and modified_time values
1310
+ entry_size = 0
1311
+ if stat and request.include_size:
1312
+ entry_size = stat.st_size
1313
+
1314
+ entry_modified_time = 0.0
1315
+ if stat and request.include_modified_time:
1316
+ entry_modified_time = stat.st_mtime
1317
+
1318
+ entries.append(
1319
+ FileSystemEntry(
1320
+ name=dir_entry.name,
1321
+ path=str(entry_path),
1322
+ is_dir=dir_entry.is_dir(),
1323
+ size=entry_size,
1324
+ modified_time=entry_modified_time,
1325
+ mime_type=mime_type,
1326
+ absolute_path=absolute_resolved_path,
1327
+ )
1187
1328
  )
1188
- )
1189
- except (OSError, PermissionError) as e:
1190
- msg = f"Could not stat entry {entry}: {e}"
1191
- logger.warning(msg)
1192
- continue
1329
+ except (OSError, PermissionError) as e:
1330
+ msg = f"Could not process entry {dir_entry.name}: {e}"
1331
+ logger.warning(msg)
1332
+ continue
1193
1333
 
1194
1334
  except PermissionError as e:
1195
1335
  msg = f"Permission denied listing directory {directory}: {e}"
@@ -2735,15 +2875,132 @@ class OSManager:
2735
2875
  result_details=f"Directory tree copied successfully: {source_path} -> {destination_path}",
2736
2876
  )
2737
2877
 
2738
- def on_app_initialization_complete(self, _payload: AppInitializationComplete) -> None:
2739
- """Handle app initialization complete event by registering system resources."""
2740
- self._register_system_resources()
2878
+ # Resource Management Methods
2879
+ def _register_system_resources_direct(self) -> None:
2880
+ """Register OS, CPU, and Compute resource types directly during initialization.
2881
+
2882
+ This method is called during __init__ and uses the event_manager directly
2883
+ to avoid singleton recursion issues with GriptapeNodes.handle_request.
2884
+ """
2885
+ self._attempt_generate_os_resources_direct()
2886
+ self._attempt_generate_cpu_resources_direct()
2887
+ self._attempt_generate_compute_resources_direct()
2888
+
2889
+ def _handle_request_direct(self, request: Any) -> Any:
2890
+ """Handle a request directly through the event_manager during initialization.
2891
+
2892
+ This bypasses GriptapeNodes.handle_request to avoid singleton recursion.
2893
+ """
2894
+ request_type = type(request)
2895
+ callback = self._event_manager._request_type_to_manager.get(request_type)
2896
+ if not callback:
2897
+ msg = f"No manager found to handle request of type '{request_type.__name__}'."
2898
+ raise TypeError(msg)
2899
+ return callback(request)
2741
2900
 
2742
- # NEW Resource Management Methods
2743
2901
  def _register_system_resources(self) -> None:
2744
- """Register OS and CPU resource types with ResourceManager and create system instances."""
2902
+ """Register OS, CPU, and Compute resource types with ResourceManager and create system instances."""
2745
2903
  self._attempt_generate_os_resources()
2746
2904
  self._attempt_generate_cpu_resources()
2905
+ self._attempt_generate_compute_resources()
2906
+
2907
+ def _attempt_generate_os_resources_direct(self) -> None:
2908
+ """Register OS resource type and create system OS instance (direct version for init)."""
2909
+ os_resource_type = OSResourceType()
2910
+ register_request = RegisterResourceTypeRequest(resource_type=os_resource_type)
2911
+ result = self._handle_request_direct(register_request)
2912
+
2913
+ if not isinstance(result, RegisterResourceTypeResultSuccess):
2914
+ logger.error("Attempted to register OS resource type. Failed due to resource type registration failure")
2915
+ return
2916
+
2917
+ logger.debug("Successfully registered OS resource type")
2918
+ self._create_system_os_instance_direct()
2919
+
2920
+ def _attempt_generate_cpu_resources_direct(self) -> None:
2921
+ """Register CPU resource type and create system CPU instance (direct version for init)."""
2922
+ cpu_resource_type = CPUResourceType()
2923
+ register_request = RegisterResourceTypeRequest(resource_type=cpu_resource_type)
2924
+ result = self._handle_request_direct(register_request)
2925
+
2926
+ if not isinstance(result, RegisterResourceTypeResultSuccess):
2927
+ logger.error("Attempted to register CPU resource type. Failed due to resource type registration failure")
2928
+ return
2929
+
2930
+ logger.debug("Successfully registered CPU resource type")
2931
+ self._create_system_cpu_instance_direct()
2932
+
2933
+ def _attempt_generate_compute_resources_direct(self) -> None:
2934
+ """Register Compute resource type and create system compute instance (direct version for init)."""
2935
+ compute_resource_type = ComputeResourceType()
2936
+ register_request = RegisterResourceTypeRequest(resource_type=compute_resource_type)
2937
+ result = self._handle_request_direct(register_request)
2938
+
2939
+ if not isinstance(result, RegisterResourceTypeResultSuccess):
2940
+ logger.error(
2941
+ "Attempted to register Compute resource type. Failed due to resource type registration failure"
2942
+ )
2943
+ return
2944
+
2945
+ logger.debug("Successfully registered Compute resource type")
2946
+ self._create_system_compute_instance_direct()
2947
+
2948
+ def _create_system_os_instance_direct(self) -> None:
2949
+ """Create system OS instance (direct version for init)."""
2950
+ os_capabilities = {
2951
+ "platform": self._get_platform_name(),
2952
+ "arch": self._get_architecture(),
2953
+ "version": self._get_platform_version(),
2954
+ }
2955
+ create_request = CreateResourceInstanceRequest(
2956
+ resource_type_name="OSResourceType", capabilities=os_capabilities
2957
+ )
2958
+ result = self._handle_request_direct(create_request)
2959
+
2960
+ if not isinstance(result, CreateResourceInstanceResultSuccess):
2961
+ logger.error(
2962
+ "Attempted to create system OS resource instance. Failed due to resource instance creation failure"
2963
+ )
2964
+ return
2965
+
2966
+ logger.debug("Successfully created system OS instance: %s", result.instance_id)
2967
+
2968
+ def _create_system_cpu_instance_direct(self) -> None:
2969
+ """Create system CPU instance (direct version for init)."""
2970
+ cpu_capabilities = {
2971
+ "cores": os.cpu_count() or 1,
2972
+ "architecture": self._get_architecture(),
2973
+ }
2974
+ create_request = CreateResourceInstanceRequest(
2975
+ resource_type_name="CPUResourceType", capabilities=cpu_capabilities
2976
+ )
2977
+ result = self._handle_request_direct(create_request)
2978
+
2979
+ if not isinstance(result, CreateResourceInstanceResultSuccess):
2980
+ logger.error(
2981
+ "Attempted to create system CPU resource instance. Failed due to resource instance creation failure"
2982
+ )
2983
+ return
2984
+
2985
+ logger.debug("Successfully created system CPU instance: %s", result.instance_id)
2986
+
2987
+ def _create_system_compute_instance_direct(self) -> None:
2988
+ """Create system compute instance with detected backends (direct version for init)."""
2989
+ compute_capabilities = {
2990
+ "compute": self._get_available_compute_backends(),
2991
+ }
2992
+ create_request = CreateResourceInstanceRequest(
2993
+ resource_type_name="ComputeResourceType", capabilities=compute_capabilities
2994
+ )
2995
+ result = self._handle_request_direct(create_request)
2996
+
2997
+ if not isinstance(result, CreateResourceInstanceResultSuccess):
2998
+ logger.error(
2999
+ "Attempted to create system Compute resource instance. Failed due to resource instance creation failure"
3000
+ )
3001
+ return
3002
+
3003
+ logger.debug("Successfully created system Compute instance: %s", result.instance_id)
2747
3004
 
2748
3005
  def _attempt_generate_os_resources(self) -> None:
2749
3006
  """Register OS resource type and create system OS instance if successful."""
@@ -2814,23 +3071,130 @@ class OSManager:
2814
3071
 
2815
3072
  logger.debug("Successfully created system CPU instance: %s", result.instance_id)
2816
3073
 
3074
+ def _attempt_generate_compute_resources(self) -> None:
3075
+ """Register Compute resource type and create system compute instance if successful."""
3076
+ # Register Compute resource type
3077
+ compute_resource_type = ComputeResourceType()
3078
+ register_request = RegisterResourceTypeRequest(resource_type=compute_resource_type)
3079
+ result = GriptapeNodes.handle_request(register_request)
3080
+
3081
+ if not isinstance(result, RegisterResourceTypeResultSuccess):
3082
+ logger.error(
3083
+ "Attempted to register Compute resource type. Failed due to resource type registration failure"
3084
+ )
3085
+ return
3086
+
3087
+ logger.debug("Successfully registered Compute resource type")
3088
+ # Registration successful, now create instance
3089
+ self._create_system_compute_instance()
3090
+
3091
+ def _create_system_compute_instance(self) -> None:
3092
+ """Create system compute instance with detected backends."""
3093
+ compute_capabilities = {
3094
+ "compute": self._get_available_compute_backends(),
3095
+ }
3096
+ create_request = CreateResourceInstanceRequest(
3097
+ resource_type_name="ComputeResourceType", capabilities=compute_capabilities
3098
+ )
3099
+ result = GriptapeNodes.handle_request(create_request)
3100
+
3101
+ if not isinstance(result, CreateResourceInstanceResultSuccess):
3102
+ logger.error(
3103
+ "Attempted to create system Compute resource instance. Failed due to resource instance creation failure"
3104
+ )
3105
+ return
3106
+
3107
+ logger.debug("Successfully created system Compute instance: %s", result.instance_id)
3108
+
3109
+ def _get_available_compute_backends(self) -> list[str]:
3110
+ """Detect available compute backends on the system.
3111
+
3112
+ Returns:
3113
+ List of available backends: always includes 'cpu', plus 'cuda' or 'mps' if available.
3114
+ """
3115
+ backends: list[str] = [ComputeBackend.CPU] # CPU is always available
3116
+
3117
+ # Check for CUDA (NVIDIA GPU)
3118
+ if self._is_cuda_available():
3119
+ backends.append(ComputeBackend.CUDA)
3120
+
3121
+ # Check for MPS (Apple Silicon)
3122
+ if self._is_mps_available():
3123
+ backends.append(ComputeBackend.MPS)
3124
+
3125
+ logger.debug("Detected compute backends: %s", backends)
3126
+ return backends
3127
+
3128
+ def _is_cuda_available(self) -> bool:
3129
+ """Check if CUDA is available by detecting NVIDIA driver.
3130
+
3131
+ Uses nvidia-smi command which is lightweight and doesn't require torch.
3132
+ """
3133
+ nvidia_smi = shutil.which("nvidia-smi")
3134
+ if nvidia_smi is None:
3135
+ return False
3136
+ try:
3137
+ result = subprocess.run( # noqa: S603
3138
+ [nvidia_smi, "--query-gpu=name", "--format=csv,noheader"],
3139
+ check=False,
3140
+ capture_output=True,
3141
+ text=True,
3142
+ timeout=5,
3143
+ )
3144
+ if result.returncode == 0 and result.stdout.strip():
3145
+ logger.debug("CUDA detected via nvidia-smi: %s", result.stdout.strip().split("\n")[0])
3146
+ return True
3147
+ except (subprocess.TimeoutExpired, OSError):
3148
+ pass
3149
+ return False
3150
+
3151
+ def _is_mps_available(self) -> bool:
3152
+ """Check if MPS (Metal Performance Shaders) is available.
3153
+
3154
+ MPS is available on Apple Silicon Macs (arm64 architecture) with macOS 12.3+.
3155
+ """
3156
+ if not self.is_mac():
3157
+ return False
3158
+
3159
+ # Check for Apple Silicon (arm64)
3160
+ arch = self._get_architecture()
3161
+ if arch not in (Architecture.ARM64, Architecture.AARCH64):
3162
+ return False
3163
+
3164
+ # MPS requires macOS 12.3+, but arm64 Macs shipped with 11.0+
3165
+ # and all arm64 Macs can run 12.3+, so if it's arm64 Mac, MPS is available
3166
+ logger.debug("MPS detected: Apple Silicon Mac")
3167
+ return True
3168
+
2817
3169
  def _get_platform_name(self) -> str:
2818
3170
  """Get platform name using existing sys.platform detection."""
2819
3171
  if self.is_windows():
2820
- return "windows"
3172
+ return Platform.WINDOWS
2821
3173
  if self.is_mac():
2822
- return "darwin"
3174
+ return Platform.DARWIN
2823
3175
  if self.is_linux():
2824
- return "linux"
3176
+ return Platform.LINUX
2825
3177
  return sys.platform
2826
3178
 
2827
3179
  def _get_architecture(self) -> str:
2828
- """Get system architecture."""
2829
- try:
2830
- return os.uname().machine.lower()
2831
- except AttributeError:
2832
- # Windows doesn't have os.uname(), fallback to environment variable
2833
- return os.environ.get("PROCESSOR_ARCHITECTURE", "unknown").lower()
3180
+ """Get system architecture, normalized across platforms."""
3181
+ platform = self._get_platform_name()
3182
+ if platform == Platform.WINDOWS:
3183
+ arch = os.environ.get("PROCESSOR_ARCHITECTURE", "unknown").lower()
3184
+ else:
3185
+ arch = os.uname().machine.lower()
3186
+
3187
+ # Normalize architecture names across platforms
3188
+ # Windows reports "amd64", Linux/macOS report "x86_64" - they're the same
3189
+ if arch == "amd64":
3190
+ return Architecture.X86_64
3191
+ if arch == "x86_64":
3192
+ return Architecture.X86_64
3193
+ if arch == "arm64":
3194
+ return Architecture.ARM64
3195
+ if arch == "aarch64":
3196
+ return Architecture.AARCH64
3197
+ return arch
2834
3198
 
2835
3199
  def _get_platform_version(self) -> str:
2836
3200
  """Get platform version."""