griptape-nodes 0.65.5__py3-none-any.whl → 0.66.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.
- griptape_nodes/common/node_executor.py +352 -27
- griptape_nodes/drivers/storage/base_storage_driver.py +12 -3
- griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +18 -2
- griptape_nodes/drivers/storage/local_storage_driver.py +42 -5
- griptape_nodes/exe_types/base_iterative_nodes.py +0 -1
- griptape_nodes/exe_types/connections.py +42 -0
- griptape_nodes/exe_types/core_types.py +2 -2
- griptape_nodes/exe_types/node_groups/__init__.py +2 -1
- griptape_nodes/exe_types/node_groups/base_iterative_node_group.py +177 -0
- griptape_nodes/exe_types/node_groups/base_node_group.py +1 -0
- griptape_nodes/exe_types/node_groups/subflow_node_group.py +35 -2
- griptape_nodes/exe_types/param_types/parameter_audio.py +1 -1
- griptape_nodes/exe_types/param_types/parameter_bool.py +1 -1
- griptape_nodes/exe_types/param_types/parameter_button.py +1 -1
- griptape_nodes/exe_types/param_types/parameter_float.py +1 -1
- griptape_nodes/exe_types/param_types/parameter_image.py +1 -1
- griptape_nodes/exe_types/param_types/parameter_int.py +1 -1
- griptape_nodes/exe_types/param_types/parameter_number.py +1 -1
- griptape_nodes/exe_types/param_types/parameter_string.py +1 -1
- griptape_nodes/exe_types/param_types/parameter_three_d.py +1 -1
- griptape_nodes/exe_types/param_types/parameter_video.py +1 -1
- griptape_nodes/machines/control_flow.py +5 -4
- griptape_nodes/machines/dag_builder.py +121 -55
- griptape_nodes/machines/fsm.py +10 -0
- griptape_nodes/machines/parallel_resolution.py +39 -38
- griptape_nodes/machines/sequential_resolution.py +29 -3
- griptape_nodes/node_library/library_registry.py +41 -2
- griptape_nodes/retained_mode/events/library_events.py +147 -8
- griptape_nodes/retained_mode/events/os_events.py +12 -4
- griptape_nodes/retained_mode/managers/fitness_problems/libraries/__init__.py +2 -0
- griptape_nodes/retained_mode/managers/fitness_problems/libraries/incompatible_requirements_problem.py +34 -0
- griptape_nodes/retained_mode/managers/flow_manager.py +133 -20
- griptape_nodes/retained_mode/managers/library_manager.py +1324 -564
- griptape_nodes/retained_mode/managers/node_manager.py +9 -3
- griptape_nodes/retained_mode/managers/os_manager.py +429 -65
- griptape_nodes/retained_mode/managers/resource_types/compute_resource.py +82 -0
- griptape_nodes/retained_mode/managers/resource_types/os_resource.py +17 -0
- griptape_nodes/retained_mode/managers/static_files_manager.py +21 -8
- griptape_nodes/retained_mode/managers/version_compatibility_manager.py +3 -3
- griptape_nodes/version_compatibility/versions/v0_39_0/modified_parameters_set_removal.py +5 -5
- griptape_nodes/version_compatibility/versions/v0_65_4/__init__.py +5 -0
- griptape_nodes/version_compatibility/versions/v0_65_4/run_in_parallel_to_run_in_order.py +79 -0
- griptape_nodes/version_compatibility/versions/v0_65_5/__init__.py +5 -0
- griptape_nodes/version_compatibility/versions/v0_65_5/flux_2_removed_parameters.py +85 -0
- {griptape_nodes-0.65.5.dist-info → griptape_nodes-0.66.0.dist-info}/METADATA +1 -1
- {griptape_nodes-0.65.5.dist-info → griptape_nodes-0.66.0.dist-info}/RECORD +48 -53
- griptape_nodes/retained_mode/managers/library_lifecycle/__init__.py +0 -45
- griptape_nodes/retained_mode/managers/library_lifecycle/data_models.py +0 -191
- griptape_nodes/retained_mode/managers/library_lifecycle/library_directory.py +0 -346
- griptape_nodes/retained_mode/managers/library_lifecycle/library_fsm.py +0 -439
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/__init__.py +0 -17
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/base.py +0 -82
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/github.py +0 -116
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/local_file.py +0 -367
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/package.py +0 -104
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/sandbox.py +0 -155
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance.py +0 -18
- griptape_nodes/retained_mode/managers/library_lifecycle/library_status.py +0 -12
- {griptape_nodes-0.65.5.dist-info → griptape_nodes-0.66.0.dist-info}/WHEEL +0 -0
- {griptape_nodes-0.65.5.dist-info → griptape_nodes-0.66.0.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
|
|
3085
|
-
|
|
3086
|
-
|
|
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
|
-
#
|
|
222
|
-
|
|
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,
|
|
466
|
-
r"""
|
|
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
|
|
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
|
|
486
|
-
"C
|
|
487
|
-
-> "C
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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
|
-
#
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
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
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
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
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
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
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
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
|
|
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
|
|
3172
|
+
return Platform.WINDOWS
|
|
2821
3173
|
if self.is_mac():
|
|
2822
|
-
return
|
|
3174
|
+
return Platform.DARWIN
|
|
2823
3175
|
if self.is_linux():
|
|
2824
|
-
return
|
|
3176
|
+
return Platform.LINUX
|
|
2825
3177
|
return sys.platform
|
|
2826
3178
|
|
|
2827
3179
|
def _get_architecture(self) -> str:
|
|
2828
|
-
"""Get system architecture."""
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
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."""
|