griptape-nodes 0.38.1__py3-none-any.whl → 0.40.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 (37) hide show
  1. griptape_nodes/__init__.py +13 -9
  2. griptape_nodes/app/__init__.py +10 -1
  3. griptape_nodes/app/app.py +2 -3
  4. griptape_nodes/app/app_sessions.py +458 -0
  5. griptape_nodes/bootstrap/workflow_executors/__init__.py +1 -0
  6. griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +213 -0
  7. griptape_nodes/bootstrap/workflow_executors/workflow_executor.py +13 -0
  8. griptape_nodes/bootstrap/workflow_runners/local_workflow_runner.py +1 -1
  9. griptape_nodes/drivers/storage/__init__.py +4 -0
  10. griptape_nodes/drivers/storage/storage_backend.py +10 -0
  11. griptape_nodes/exe_types/core_types.py +5 -1
  12. griptape_nodes/exe_types/node_types.py +20 -24
  13. griptape_nodes/machines/node_resolution.py +5 -1
  14. griptape_nodes/node_library/advanced_node_library.py +51 -0
  15. griptape_nodes/node_library/library_registry.py +28 -2
  16. griptape_nodes/node_library/workflow_registry.py +1 -1
  17. griptape_nodes/retained_mode/events/agent_events.py +15 -2
  18. griptape_nodes/retained_mode/events/app_events.py +113 -2
  19. griptape_nodes/retained_mode/events/base_events.py +28 -1
  20. griptape_nodes/retained_mode/events/library_events.py +111 -1
  21. griptape_nodes/retained_mode/events/workflow_events.py +1 -0
  22. griptape_nodes/retained_mode/griptape_nodes.py +240 -18
  23. griptape_nodes/retained_mode/managers/agent_manager.py +123 -17
  24. griptape_nodes/retained_mode/managers/flow_manager.py +16 -48
  25. griptape_nodes/retained_mode/managers/library_manager.py +642 -121
  26. griptape_nodes/retained_mode/managers/node_manager.py +1 -1
  27. griptape_nodes/retained_mode/managers/static_files_manager.py +4 -3
  28. griptape_nodes/retained_mode/managers/workflow_manager.py +666 -37
  29. griptape_nodes/retained_mode/utils/__init__.py +1 -0
  30. griptape_nodes/retained_mode/utils/engine_identity.py +131 -0
  31. griptape_nodes/retained_mode/utils/name_generator.py +162 -0
  32. griptape_nodes/retained_mode/utils/session_persistence.py +105 -0
  33. {griptape_nodes-0.38.1.dist-info → griptape_nodes-0.40.0.dist-info}/METADATA +1 -1
  34. {griptape_nodes-0.38.1.dist-info → griptape_nodes-0.40.0.dist-info}/RECORD +37 -27
  35. {griptape_nodes-0.38.1.dist-info → griptape_nodes-0.40.0.dist-info}/WHEEL +0 -0
  36. {griptape_nodes-0.38.1.dist-info → griptape_nodes-0.40.0.dist-info}/entry_points.txt +0 -0
  37. {griptape_nodes-0.38.1.dist-info → griptape_nodes-0.40.0.dist-info}/licenses/LICENSE +0 -0
@@ -15,7 +15,7 @@ from pathlib import Path
15
15
  from typing import TYPE_CHECKING, cast
16
16
 
17
17
  import uv
18
- from packaging.requirements import Requirement
18
+ from packaging.requirements import InvalidRequirement, Requirement
19
19
  from pydantic import ValidationError
20
20
  from rich.box import HEAVY_EDGE
21
21
  from rich.console import Console
@@ -66,6 +66,11 @@ from griptape_nodes.retained_mode.events.library_events import (
66
66
  ListNodeTypesInLibraryResultSuccess,
67
67
  ListRegisteredLibrariesRequest,
68
68
  ListRegisteredLibrariesResultSuccess,
69
+ LoadLibraryMetadataFromFileRequest,
70
+ LoadLibraryMetadataFromFileResultFailure,
71
+ LoadLibraryMetadataFromFileResultSuccess,
72
+ LoadMetadataForAllLibrariesRequest,
73
+ LoadMetadataForAllLibrariesResultSuccess,
69
74
  RegisterLibraryFromFileRequest,
70
75
  RegisterLibraryFromFileResultFailure,
71
76
  RegisterLibraryFromFileResultSuccess,
@@ -86,6 +91,7 @@ from griptape_nodes.retained_mode.managers.os_manager import OSManager
86
91
  if TYPE_CHECKING:
87
92
  from types import ModuleType
88
93
 
94
+ from griptape_nodes.node_library.advanced_node_library import AdvancedNodeLibrary
89
95
  from griptape_nodes.retained_mode.events.base_events import ResultPayload
90
96
  from griptape_nodes.retained_mode.managers.event_manager import EventManager
91
97
 
@@ -93,6 +99,8 @@ logger = logging.getLogger("griptape_nodes")
93
99
 
94
100
 
95
101
  class LibraryManager:
102
+ SANDBOX_LIBRARY_NAME = "Sandbox Library"
103
+
96
104
  class LibraryStatus(StrEnum):
97
105
  """Status of the library that was attempted to be loaded."""
98
106
 
@@ -116,8 +124,30 @@ class LibraryManager:
116
124
 
117
125
  _library_file_path_to_info: dict[str, LibraryInfo]
118
126
 
127
+ # Stable module namespace mappings for workflow serialization
128
+ # These mappings ensure that dynamically loaded modules can be reliably imported
129
+ # in generated workflow code by providing stable, predictable import paths.
130
+ #
131
+ # Example mappings:
132
+ # dynamic to stable module mapping:
133
+ # "gtn_dynamic_module_image_to_video_py_123456789": "griptape_nodes.node_libraries.runwayml_library.image_to_video"
134
+ #
135
+ # stable to dynamic module mapping:
136
+ # "griptape_nodes.node_libraries.runwayml_library.image_to_video": "gtn_dynamic_module_image_to_video_py_123456789"
137
+ #
138
+ # library to stable modules:
139
+ # "RunwayML Library": {"griptape_nodes.node_libraries.runwayml_library.image_to_video", "griptape_nodes.node_libraries.runwayml_library.text_to_image"},
140
+ # "Sandbox Library": {"griptape_nodes.node_libraries.sandbox.my_custom_node"}
141
+ #
142
+ _dynamic_to_stable_module_mapping: dict[str, str] # dynamic_module_name -> stable_namespace
143
+ _stable_to_dynamic_module_mapping: dict[str, str] # stable_namespace -> dynamic_module_name
144
+ _library_to_stable_modules: dict[str, set[str]] # library_name -> set of stable_namespaces
145
+
119
146
  def __init__(self, event_manager: EventManager) -> None:
120
147
  self._library_file_path_to_info = {}
148
+ self._dynamic_to_stable_module_mapping = {}
149
+ self._stable_to_dynamic_module_mapping = {}
150
+ self._library_to_stable_modules = {}
121
151
 
122
152
  event_manager.assign_manager_to_request_type(
123
153
  ListRegisteredLibrariesRequest, self.on_list_registered_libraries_request
@@ -129,6 +159,10 @@ class LibraryManager:
129
159
  GetNodeMetadataFromLibraryRequest,
130
160
  self.get_node_metadata_from_library_request,
131
161
  )
162
+ event_manager.assign_manager_to_request_type(
163
+ LoadLibraryMetadataFromFileRequest,
164
+ self.load_library_metadata_from_file_request,
165
+ )
132
166
  event_manager.assign_manager_to_request_type(
133
167
  RegisterLibraryFromFileRequest,
134
168
  self.register_library_from_file_request,
@@ -148,6 +182,9 @@ class LibraryManager:
148
182
  event_manager.assign_manager_to_request_type(
149
183
  GetAllInfoForAllLibrariesRequest, self.get_all_info_for_all_libraries_request
150
184
  )
185
+ event_manager.assign_manager_to_request_type(
186
+ LoadMetadataForAllLibrariesRequest, self.load_metadata_for_all_libraries_request
187
+ )
151
188
  event_manager.assign_manager_to_request_type(
152
189
  UnloadLibraryFromRegistryRequest, self.unload_library_from_registry_request
153
190
  )
@@ -295,6 +332,241 @@ class LibraryManager:
295
332
  result = GetLibraryMetadataResultSuccess(metadata=metadata)
296
333
  return result
297
334
 
335
+ def load_library_metadata_from_file_request(self, request: LoadLibraryMetadataFromFileRequest) -> ResultPayload:
336
+ """Load library metadata from a JSON file without loading the actual node modules.
337
+
338
+ This method provides a lightweight way to get library schema information
339
+ without the overhead of dynamically importing Python modules.
340
+ """
341
+ file_path = request.file_path
342
+
343
+ # Convert to Path object if it's a string
344
+ json_path = Path(file_path)
345
+
346
+ # Check if the file exists
347
+ if not json_path.exists():
348
+ details = f"Attempted to load Library JSON file. Failed because no file could be found at the specified path: {json_path}"
349
+ logger.error(details)
350
+ return LoadLibraryMetadataFromFileResultFailure(
351
+ library_path=file_path,
352
+ library_name=None,
353
+ status=LibraryManager.LibraryStatus.MISSING,
354
+ problems=[
355
+ "Library could not be found at the file path specified. It will be removed from the configuration."
356
+ ],
357
+ )
358
+
359
+ # Load the JSON
360
+ try:
361
+ with json_path.open("r", encoding="utf-8") as f:
362
+ library_json = json.load(f)
363
+ except json.JSONDecodeError:
364
+ details = f"Attempted to load Library JSON file. Failed because the file at path '{json_path}' was improperly formatted."
365
+ logger.error(details)
366
+ return LoadLibraryMetadataFromFileResultFailure(
367
+ library_path=file_path,
368
+ library_name=None,
369
+ status=LibraryManager.LibraryStatus.UNUSABLE,
370
+ problems=["Library file not formatted as proper JSON."],
371
+ )
372
+ except Exception as err:
373
+ details = f"Attempted to load Library JSON file from location '{json_path}'. Failed because an exception occurred: {err}"
374
+ logger.error(details)
375
+ return LoadLibraryMetadataFromFileResultFailure(
376
+ library_path=file_path,
377
+ library_name=None,
378
+ status=LibraryManager.LibraryStatus.UNUSABLE,
379
+ problems=[f"Exception occurred when attempting to load the library: {err}."],
380
+ )
381
+
382
+ # Try to extract library name from JSON for better error reporting
383
+ library_name = library_json.get("name") if isinstance(library_json, dict) else None
384
+
385
+ # Do you comport, my dude
386
+ try:
387
+ library_data = LibrarySchema.model_validate(library_json)
388
+ except ValidationError as err:
389
+ # Do some more hardcore error handling.
390
+ problems = []
391
+ for error in err.errors():
392
+ loc = " -> ".join(map(str, error["loc"]))
393
+ msg = error["msg"]
394
+ error_type = error["type"]
395
+ problem = f"Error in section '{loc}': {error_type}, {msg}"
396
+ problems.append(problem)
397
+ details = f"Attempted to load Library JSON file. Failed because the file at path '{json_path}' failed to match the library schema due to: {err}"
398
+ logger.error(details)
399
+ return LoadLibraryMetadataFromFileResultFailure(
400
+ library_path=file_path,
401
+ library_name=library_name,
402
+ status=LibraryManager.LibraryStatus.UNUSABLE,
403
+ problems=problems,
404
+ )
405
+ except Exception as err:
406
+ details = f"Attempted to load Library JSON file. Failed because the file at path '{json_path}' failed to match the library schema due to: {err}"
407
+ logger.error(details)
408
+ return LoadLibraryMetadataFromFileResultFailure(
409
+ library_path=file_path,
410
+ library_name=library_name,
411
+ status=LibraryManager.LibraryStatus.UNUSABLE,
412
+ problems=[f"Library file did not match the library schema specified due to: {err}"],
413
+ )
414
+
415
+ details = f"Successfully loaded library metadata from JSON file at {json_path}"
416
+ logger.debug(details)
417
+ return LoadLibraryMetadataFromFileResultSuccess(library_schema=library_data, file_path=file_path)
418
+
419
+ def load_metadata_for_all_libraries_request(self, request: LoadMetadataForAllLibrariesRequest) -> ResultPayload: # noqa: ARG002
420
+ """Load metadata for all libraries from configuration without loading node modules.
421
+
422
+ This loads metadata from both library JSON files specified in configuration
423
+ and generates sandbox library metadata by scanning Python files without importing them.
424
+ """
425
+ successful_libraries = []
426
+ failed_libraries = []
427
+
428
+ # Load metadata from config libraries
429
+ config_mgr = GriptapeNodes.ConfigManager()
430
+ user_libraries_section = "app_events.on_app_initialization_complete.libraries_to_register"
431
+ libraries_to_register: list[str] = config_mgr.get_config_value(user_libraries_section)
432
+
433
+ if libraries_to_register is not None:
434
+ for library_to_register in libraries_to_register:
435
+ if library_to_register and library_to_register.endswith(".json"):
436
+ # Load metadata for this library file
437
+ metadata_request = LoadLibraryMetadataFromFileRequest(file_path=library_to_register)
438
+ metadata_result = self.load_library_metadata_from_file_request(metadata_request)
439
+
440
+ if isinstance(metadata_result, LoadLibraryMetadataFromFileResultSuccess):
441
+ successful_libraries.append(metadata_result)
442
+ else:
443
+ failed_libraries.append(cast("LoadLibraryMetadataFromFileResultFailure", metadata_result))
444
+ # Note: We skip requirement specifier libraries (non-.json) as they don't have
445
+ # JSON files we can load metadata from without installation
446
+
447
+ # Generate sandbox library metadata
448
+ sandbox_result = self._generate_sandbox_library_metadata()
449
+ if isinstance(sandbox_result, LoadLibraryMetadataFromFileResultSuccess):
450
+ successful_libraries.append(sandbox_result)
451
+ elif isinstance(sandbox_result, LoadLibraryMetadataFromFileResultFailure):
452
+ failed_libraries.append(sandbox_result)
453
+ # If sandbox_result is None, sandbox was not configured or no files found - skip it
454
+
455
+ details = (
456
+ f"Successfully loaded metadata for {len(successful_libraries)} libraries, {len(failed_libraries)} failed"
457
+ )
458
+ logger.debug(details)
459
+ return LoadMetadataForAllLibrariesResultSuccess(
460
+ successful_libraries=successful_libraries,
461
+ failed_libraries=failed_libraries,
462
+ )
463
+
464
+ def _generate_sandbox_library_metadata(
465
+ self,
466
+ ) -> LoadLibraryMetadataFromFileResultSuccess | LoadLibraryMetadataFromFileResultFailure | None:
467
+ """Generate sandbox library metadata by scanning Python files without importing them.
468
+
469
+ Returns None if no sandbox directory is configured or no files are found.
470
+ """
471
+ config_mgr = GriptapeNodes.ConfigManager()
472
+ sandbox_library_subdir = config_mgr.get_config_value("sandbox_library_directory")
473
+ if not sandbox_library_subdir:
474
+ logger.debug("No sandbox directory specified in config. Skipping sandbox library metadata generation.")
475
+ return None
476
+
477
+ # Prepend the workflow directory; if the sandbox dir starts with a slash, the workflow dir will be ignored.
478
+ sandbox_library_dir = config_mgr.workspace_path / sandbox_library_subdir
479
+ sandbox_library_dir_as_posix = sandbox_library_dir.as_posix()
480
+
481
+ if not sandbox_library_dir.exists():
482
+ return LoadLibraryMetadataFromFileResultFailure(
483
+ library_path=sandbox_library_dir_as_posix,
484
+ library_name=LibraryManager.SANDBOX_LIBRARY_NAME,
485
+ status=LibraryManager.LibraryStatus.MISSING,
486
+ problems=["Sandbox directory does not exist."],
487
+ )
488
+
489
+ sandbox_node_candidates = self._find_files_in_dir(directory=sandbox_library_dir, extension=".py")
490
+ if not sandbox_node_candidates:
491
+ logger.debug(
492
+ "No candidate files found in sandbox directory '%s'. Skipping sandbox library metadata generation.",
493
+ sandbox_library_dir,
494
+ )
495
+ return None
496
+
497
+ # For metadata-only generation, we create placeholder node definitions
498
+ # based on file names since we can't inspect the classes without importing
499
+ node_definitions = []
500
+ for candidate in sandbox_node_candidates:
501
+ # Use the full file name (with extension) as a placeholder to make it clear this is a file candidate
502
+ file_name = candidate.name
503
+
504
+ # Create a placeholder node definition - we can't get the actual class metadata
505
+ # without importing, so we use defaults
506
+ node_metadata = NodeMetadata(
507
+ category="Griptape Nodes Sandbox",
508
+ description=f"'{file_name}' may contain one or more nodes defined in this candidate file.",
509
+ display_name=file_name,
510
+ icon="square-dashed",
511
+ color=None,
512
+ )
513
+ node_definition = NodeDefinition(
514
+ class_name=file_name,
515
+ file_path=str(candidate),
516
+ metadata=node_metadata,
517
+ )
518
+ node_definitions.append(node_definition)
519
+
520
+ if not node_definitions:
521
+ logger.debug(
522
+ "No valid node files found in sandbox directory '%s'. Skipping sandbox library metadata generation.",
523
+ sandbox_library_dir,
524
+ )
525
+ return None
526
+
527
+ # Create the library schema
528
+ sandbox_category = CategoryDefinition(
529
+ title="Sandbox",
530
+ description=f"Nodes loaded from the {LibraryManager.SANDBOX_LIBRARY_NAME}.",
531
+ color="#c7621a",
532
+ icon="Folder",
533
+ )
534
+
535
+ engine_version = GriptapeNodes().handle_engine_version_request(request=GetEngineVersionRequest())
536
+ if not isinstance(engine_version, GetEngineVersionResultSuccess):
537
+ return LoadLibraryMetadataFromFileResultFailure(
538
+ library_path=sandbox_library_dir_as_posix,
539
+ library_name=LibraryManager.SANDBOX_LIBRARY_NAME,
540
+ status=LibraryManager.LibraryStatus.UNUSABLE,
541
+ problems=["Could not get engine version for sandbox library generation."],
542
+ )
543
+
544
+ engine_version_str = f"{engine_version.major}.{engine_version.minor}.{engine_version.patch}"
545
+ library_metadata = LibraryMetadata(
546
+ author="Author needs to be specified when library is published.",
547
+ description="Nodes loaded from the sandbox library.",
548
+ library_version=engine_version_str,
549
+ engine_version=engine_version_str,
550
+ tags=["sandbox"],
551
+ is_griptape_nodes_searchable=False,
552
+ )
553
+ categories = [
554
+ {"Griptape Nodes Sandbox": sandbox_category},
555
+ ]
556
+ library_schema = LibrarySchema(
557
+ name=LibraryManager.SANDBOX_LIBRARY_NAME,
558
+ library_schema_version=LibrarySchema.LATEST_SCHEMA_VERSION,
559
+ metadata=library_metadata,
560
+ categories=categories,
561
+ nodes=node_definitions,
562
+ )
563
+
564
+ details = f"Successfully generated sandbox library metadata with {len(node_definitions)} nodes from {sandbox_library_dir}"
565
+ logger.debug(details)
566
+ return LoadLibraryMetadataFromFileResultSuccess(
567
+ library_schema=library_schema, file_path=str(sandbox_library_dir)
568
+ )
569
+
298
570
  def get_node_metadata_from_library_request(self, request: GetNodeMetadataFromLibraryRequest) -> ResultPayload:
299
571
  # Does this library exist?
300
572
  try:
@@ -358,63 +630,25 @@ class LibraryManager:
358
630
  logger.error(details)
359
631
  return RegisterLibraryFromFileResultFailure()
360
632
 
361
- # Load the JSON
362
- try:
363
- with json_path.open("r", encoding="utf-8") as f:
364
- library_json = json.load(f)
365
- except json.JSONDecodeError:
366
- self._library_file_path_to_info[file_path] = LibraryManager.LibraryInfo(
367
- library_path=file_path,
368
- library_name=None,
369
- status=LibraryManager.LibraryStatus.UNUSABLE,
370
- problems=["Library file not formatted as proper JSON."],
371
- )
372
- details = f"Attempted to load Library JSON file. Failed because the file at path '{json_path}' was improperly formatted."
373
- logger.error(details)
374
- return RegisterLibraryFromFileResultFailure()
375
- except Exception as err:
376
- self._library_file_path_to_info[file_path] = LibraryManager.LibraryInfo(
377
- library_path=file_path,
378
- library_name=None,
379
- status=LibraryManager.LibraryStatus.UNUSABLE,
380
- problems=[f"Exception occurred when attempting to load the library: {err}."],
381
- )
382
- details = f"Attempted to load Library JSON file from location '{json_path}'. Failed because an exception occurred: {err}"
383
- logger.error(details)
384
- return RegisterLibraryFromFileResultFailure()
633
+ # Use the new metadata loading functionality
634
+ metadata_request = LoadLibraryMetadataFromFileRequest(file_path=file_path)
635
+ metadata_result = self.load_library_metadata_from_file_request(metadata_request)
636
+
637
+ if not isinstance(metadata_result, LoadLibraryMetadataFromFileResultSuccess):
638
+ # Metadata loading failed, use the detailed error information from the failure result
639
+ failure_result = cast("LoadLibraryMetadataFromFileResultFailure", metadata_result)
385
640
 
386
- # Do you comport, my dude
387
- try:
388
- library_data = LibrarySchema.model_validate(library_json)
389
- except ValidationError as err:
390
- # Do some more hardcore error handling.
391
- problems = []
392
- for error in err.errors():
393
- loc = " -> ".join(map(str, error["loc"]))
394
- msg = error["msg"]
395
- error_type = error["type"]
396
- problem = f"Error in section '{loc}': {error_type}, {msg}"
397
- problems.append(problem)
398
- self._library_file_path_to_info[file_path] = LibraryManager.LibraryInfo(
399
- library_path=file_path,
400
- library_name=None,
401
- status=LibraryManager.LibraryStatus.UNUSABLE,
402
- problems=problems,
403
- )
404
- details = f"Attempted to load Library JSON file. Failed because the file at path '{json_path}' failed to match the library schema due to: {err}"
405
- logger.error(details)
406
- return RegisterLibraryFromFileResultFailure()
407
- except Exception as err:
408
641
  self._library_file_path_to_info[file_path] = LibraryManager.LibraryInfo(
409
642
  library_path=file_path,
410
- library_name=None,
411
- status=LibraryManager.LibraryStatus.UNUSABLE,
412
- problems=[f"Library file did not match the library schema specified due to: {err}"],
643
+ library_name=failure_result.library_name,
644
+ status=failure_result.status,
645
+ problems=failure_result.problems,
413
646
  )
414
- details = f"Attempted to load Library JSON file. Failed because the file at path '{json_path}' failed to match the library schema due to: {err}"
415
- logger.error(details)
416
647
  return RegisterLibraryFromFileResultFailure()
417
648
 
649
+ # Get the validated library data
650
+ library_data = metadata_result.library_schema
651
+
418
652
  # Make sure the version string is copacetic.
419
653
  library_version = library_data.metadata.library_version
420
654
  if library_version is None:
@@ -435,12 +669,35 @@ class LibraryManager:
435
669
  # Add the directory to the Python path to allow for relative imports
436
670
  sys.path.insert(0, str(base_dir))
437
671
 
672
+ # Load the advanced library module if specified
673
+ advanced_library_instance = None
674
+ if library_data.advanced_library_path:
675
+ try:
676
+ advanced_library_instance = self._load_advanced_library_module(
677
+ library_data=library_data,
678
+ base_dir=base_dir,
679
+ )
680
+ except Exception as err:
681
+ self._library_file_path_to_info[file_path] = LibraryManager.LibraryInfo(
682
+ library_path=file_path,
683
+ library_name=library_data.name,
684
+ library_version=library_version,
685
+ status=LibraryManager.LibraryStatus.UNUSABLE,
686
+ problems=[
687
+ f"Failed to load Advanced Library module from '{library_data.advanced_library_path}': {err}"
688
+ ],
689
+ )
690
+ details = f"Attempted to load Library '{library_data.name}' from '{json_path}'. Failed to load Advanced Library module: {err}"
691
+ logger.error(details)
692
+ return RegisterLibraryFromFileResultFailure()
693
+
438
694
  # Create or get the library
439
695
  try:
440
696
  # Try to create a new library
441
697
  library = LibraryRegistry.generate_new_library(
442
698
  library_data=library_data,
443
699
  mark_as_default_library=request.load_as_default_library,
700
+ advanced_library=advanced_library_instance,
444
701
  )
445
702
 
446
703
  except KeyError as err:
@@ -593,8 +850,8 @@ class LibraryManager:
593
850
  def register_library_from_requirement_specifier_request(
594
851
  self, request: RegisterLibraryFromRequirementSpecifierRequest
595
852
  ) -> ResultPayload:
596
- package_name = Requirement(request.requirement_specifier).name
597
853
  try:
854
+ package_name = Requirement(request.requirement_specifier).name
598
855
  # Determine venv path for dependency installation
599
856
  venv_path = self._get_library_venv_path(package_name, None)
600
857
 
@@ -630,6 +887,10 @@ class LibraryManager:
630
887
  details = f"Attempted to install library '{request.requirement_specifier}'. Failed: return code={e.returncode}, stdout={e.stdout}, stderr={e.stderr}"
631
888
  logger.error(details)
632
889
  return RegisterLibraryFromRequirementSpecifierResultFailure()
890
+ except InvalidRequirement as e:
891
+ details = f"Attempted to install library '{request.requirement_specifier}'. Failed due to invalid requirement specifier: {e}"
892
+ logger.error(details)
893
+ return RegisterLibraryFromRequirementSpecifierResultFailure()
633
894
 
634
895
  library_path = str(files(package_name).joinpath(request.library_config_name))
635
896
 
@@ -752,6 +1013,9 @@ class LibraryManager:
752
1013
  logger.error(details)
753
1014
  return UnloadLibraryFromRegistryResultFailure()
754
1015
 
1016
+ # Clean up all stable module aliases for this library
1017
+ self._unregister_all_stable_module_aliases_for_library(request.library_name)
1018
+
755
1019
  # Remove the library from our library info list. This prevents it from still showing
756
1020
  # up in the table of attempted library loads.
757
1021
  lib_info = self.get_library_info_by_library_name(request.library_name)
@@ -876,11 +1140,149 @@ class LibraryManager:
876
1140
  )
877
1141
  return result
878
1142
 
879
- def _load_module_from_file(self, file_path: Path | str) -> ModuleType:
1143
+ def _create_stable_namespace(self, library_name: str, file_path: Path) -> str:
1144
+ """Create a stable namespace for a dynamic module.
1145
+
1146
+ Args:
1147
+ library_name: Name of the library
1148
+ file_path: Path to the Python file
1149
+
1150
+ Returns:
1151
+ Stable namespace string like 'griptape_nodes.node_libraries.runwayml_library.image_to_video'
1152
+ """
1153
+ # Convert library name to safe module name
1154
+ safe_library_name = library_name.lower().replace(" ", "_").replace("-", "_")
1155
+ # Remove invalid characters
1156
+ safe_library_name = "".join(c for c in safe_library_name if c.isalnum() or c == "_")
1157
+
1158
+ # Convert file path to safe module name
1159
+ safe_file_name = file_path.stem.replace("-", "_")
1160
+
1161
+ return f"griptape_nodes.node_libraries.{safe_library_name}.{safe_file_name}"
1162
+
1163
+ def _register_stable_module_alias(
1164
+ self, dynamic_module_name: str, stable_namespace: str, module: ModuleType, library_name: str
1165
+ ) -> None:
1166
+ """Register a stable alias for a dynamic module in sys.modules.
1167
+
1168
+ Args:
1169
+ dynamic_module_name: Original dynamic module name
1170
+ stable_namespace: Stable namespace to alias to
1171
+ module: The loaded module
1172
+ library_name: Name of the library
1173
+ """
1174
+ # Update our mapping
1175
+ self._dynamic_to_stable_module_mapping[dynamic_module_name] = stable_namespace
1176
+ self._stable_to_dynamic_module_mapping[stable_namespace] = dynamic_module_name
1177
+
1178
+ # Track library-to-modules mapping for bulk cleanup
1179
+ library_key = library_name
1180
+ if library_key not in self._library_to_stable_modules:
1181
+ self._library_to_stable_modules[library_key] = set()
1182
+ self._library_to_stable_modules[library_key].add(stable_namespace)
1183
+
1184
+ # Register the stable alias in sys.modules
1185
+ sys.modules[stable_namespace] = module
1186
+
1187
+ details = f"Registered stable alias: {stable_namespace} -> {dynamic_module_name} (library: {library_key})"
1188
+ logger.debug(details)
1189
+
1190
+ def _unregister_stable_module_alias(self, dynamic_module_name: str) -> None:
1191
+ """Unregister a stable alias for a dynamic module during hot reload.
1192
+
1193
+ Args:
1194
+ dynamic_module_name: Original dynamic module name
1195
+ """
1196
+ if dynamic_module_name in self._dynamic_to_stable_module_mapping:
1197
+ stable_namespace = self._dynamic_to_stable_module_mapping[dynamic_module_name]
1198
+
1199
+ # Remove from sys.modules if it exists
1200
+ if stable_namespace in sys.modules:
1201
+ del sys.modules[stable_namespace]
1202
+
1203
+ # Remove from library tracking
1204
+ for library_modules in self._library_to_stable_modules.values():
1205
+ library_modules.discard(stable_namespace)
1206
+
1207
+ # Remove from our mappings
1208
+ del self._dynamic_to_stable_module_mapping[dynamic_module_name]
1209
+ del self._stable_to_dynamic_module_mapping[stable_namespace]
1210
+
1211
+ details = f"Unregistered stable alias: {stable_namespace}"
1212
+ logger.debug(details)
1213
+
1214
+ def _unregister_all_stable_module_aliases_for_library(self, library_name: str) -> None:
1215
+ """Unregister all stable module aliases for a library during library unload/reload.
1216
+
1217
+ Args:
1218
+ library_name: Name of the library to clean up
1219
+ """
1220
+ library_key = library_name
1221
+ if library_key not in self._library_to_stable_modules:
1222
+ return
1223
+
1224
+ stable_namespaces = self._library_to_stable_modules[library_key].copy()
1225
+ details = f"Unregistering {len(stable_namespaces)} stable aliases for library: {library_name}"
1226
+ logger.debug(details)
1227
+
1228
+ for stable_namespace in stable_namespaces:
1229
+ # Remove from sys.modules if it exists
1230
+ if stable_namespace in sys.modules:
1231
+ del sys.modules[stable_namespace]
1232
+
1233
+ # Find and remove from dynamic mapping
1234
+ dynamic_module_name = self._stable_to_dynamic_module_mapping.get(stable_namespace)
1235
+ if dynamic_module_name:
1236
+ self._dynamic_to_stable_module_mapping.pop(dynamic_module_name, None)
1237
+ self._stable_to_dynamic_module_mapping.pop(stable_namespace, None)
1238
+
1239
+ # Clear the library's module set
1240
+ del self._library_to_stable_modules[library_key]
1241
+ details = f"Completed cleanup of stable aliases for library: '{library_name}'."
1242
+ logger.debug(details)
1243
+
1244
+ def get_stable_namespace_for_dynamic_module(self, dynamic_module_name: str) -> str | None:
1245
+ """Get the stable namespace for a dynamic module name.
1246
+
1247
+ This method is used during workflow serialization to convert dynamic module names
1248
+ (like "gtn_dynamic_module_image_to_video_py_123456789") to stable namespace imports
1249
+ (like "griptape_nodes.node_libraries.runwayml_library.image_to_video").
1250
+
1251
+ Args:
1252
+ dynamic_module_name: The dynamic module name to look up
1253
+
1254
+ Returns:
1255
+ The stable namespace string, or None if not found
1256
+
1257
+ Example:
1258
+ >>> manager.get_stable_namespace_for_dynamic_module("gtn_dynamic_module_image_to_video_py_123456789")
1259
+ "griptape_nodes.node_libraries.runwayml_library.image_to_video"
1260
+ """
1261
+ return self._dynamic_to_stable_module_mapping.get(dynamic_module_name)
1262
+
1263
+ def is_dynamic_module(self, module_name: str) -> bool:
1264
+ """Check if a module name represents a dynamically loaded module.
1265
+
1266
+ Args:
1267
+ module_name: The module name to check
1268
+
1269
+ Returns:
1270
+ True if this is a dynamic module name, False otherwise
1271
+
1272
+ Example:
1273
+ >>> manager.is_dynamic_module("gtn_dynamic_module_image_to_video_py_123456789")
1274
+ True
1275
+ >>> manager.is_dynamic_module("griptape.artifacts")
1276
+ False
1277
+ """
1278
+ return module_name.startswith("gtn_dynamic_module_")
1279
+
1280
+ def _load_module_from_file(self, file_path: Path | str, library_name: str) -> ModuleType:
880
1281
  """Dynamically load a module from a Python file with support for hot reloading.
881
1282
 
882
1283
  Args:
883
1284
  file_path: Path to the Python file
1285
+ library_name: Name of the library
884
1286
 
885
1287
  Returns:
886
1288
  The loaded module
@@ -892,13 +1294,19 @@ class LibraryManager:
892
1294
  file_path = Path(file_path)
893
1295
 
894
1296
  # Generate a unique module name
895
- module_name = f"dynamic_module_{file_path.name.replace('.', '_')}_{hash(str(file_path))}"
1297
+ module_name = f"gtn_dynamic_module_{file_path.name.replace('.', '_')}_{hash(str(file_path))}"
1298
+
1299
+ # Create stable namespace
1300
+ stable_namespace = self._create_stable_namespace(library_name, file_path)
896
1301
 
897
1302
  # Check if this module is already loaded
898
1303
  if module_name in sys.modules:
899
1304
  # For dynamically loaded modules, we need to re-create the module
900
1305
  # with a fresh spec rather than using importlib.reload
901
1306
 
1307
+ # Unregister old stable alias
1308
+ self._unregister_stable_module_alias(module_name)
1309
+
902
1310
  # Remove the old module from sys.modules
903
1311
  old_module = sys.modules.pop(module_name)
904
1312
 
@@ -914,6 +1322,8 @@ class LibraryManager:
914
1322
  try:
915
1323
  # Execute the module with the new code
916
1324
  spec.loader.exec_module(module)
1325
+ # Register new stable alias
1326
+ self._register_stable_module_alias(module_name, stable_namespace, module, library_name)
917
1327
  details = f"Hot reloaded module: {module_name} from {file_path}"
918
1328
  logger.debug(details)
919
1329
  except Exception as e:
@@ -939,18 +1349,21 @@ class LibraryManager:
939
1349
  # Execute the module
940
1350
  try:
941
1351
  spec.loader.exec_module(module)
1352
+ # Register stable alias
1353
+ self._register_stable_module_alias(module_name, stable_namespace, module, library_name)
942
1354
  except Exception as err:
943
1355
  msg = f"Module at '{file_path}' failed to load with error: {err}"
944
1356
  raise ImportError(msg) from err
945
1357
 
946
1358
  return module
947
1359
 
948
- def _load_class_from_file(self, file_path: Path | str, class_name: str) -> type[BaseNode]:
1360
+ def _load_class_from_file(self, file_path: Path | str, class_name: str, library_name: str) -> type[BaseNode]:
949
1361
  """Dynamically load a class from a Python file with support for hot reloading.
950
1362
 
951
1363
  Args:
952
1364
  file_path: Path to the Python file
953
1365
  class_name: Name of the class to load
1366
+ library_name: Name of the library
954
1367
 
955
1368
  Returns:
956
1369
  The loaded class
@@ -961,7 +1374,7 @@ class LibraryManager:
961
1374
  TypeError: If the loaded class isn't a BaseNode-derived class
962
1375
  """
963
1376
  try:
964
- module = self._load_module_from_file(file_path)
1377
+ module = self._load_module_from_file(file_path, library_name)
965
1378
  except ImportError as err:
966
1379
  msg = f"Attempted to load class '{class_name}'. Error: {err}"
967
1380
  raise ImportError(msg) from err
@@ -981,16 +1394,50 @@ class LibraryManager:
981
1394
  return node_class
982
1395
 
983
1396
  def load_all_libraries_from_config(self) -> None:
1397
+ # Load metadata for all libraries to determine which ones can be safely loaded
1398
+ metadata_request = LoadMetadataForAllLibrariesRequest()
1399
+ metadata_result = self.load_metadata_for_all_libraries_request(metadata_request)
1400
+
1401
+ # Check if metadata loading succeeded
1402
+ if not isinstance(metadata_result, LoadMetadataForAllLibrariesResultSuccess):
1403
+ logger.error("Failed to load metadata for all libraries, skipping library registration")
1404
+ return
1405
+
1406
+ # Record all failed libraries in our tracking immediately
1407
+ for failed_library in metadata_result.failed_libraries:
1408
+ self._library_file_path_to_info[failed_library.library_path] = LibraryManager.LibraryInfo(
1409
+ library_path=failed_library.library_path,
1410
+ library_name=failed_library.library_name,
1411
+ status=failed_library.status,
1412
+ problems=failed_library.problems,
1413
+ )
1414
+
1415
+ # Use metadata results to selectively load libraries
984
1416
  user_libraries_section = "app_events.on_app_initialization_complete.libraries_to_register"
985
- self._load_libraries_from_config_category(config_category=user_libraries_section, load_as_default_library=False)
986
1417
 
987
- sandbox_library_section = "sandbox_library_directory"
988
- self._attempt_generate_sandbox_library(config_category=sandbox_library_section)
1418
+ # Load libraries that had successful metadata loading
1419
+ for library_result in metadata_result.successful_libraries:
1420
+ if library_result.library_schema.name == LibraryManager.SANDBOX_LIBRARY_NAME:
1421
+ # Handle sandbox library - use the schema we already have
1422
+ self._attempt_generate_sandbox_library_from_schema(
1423
+ library_schema=library_result.library_schema, sandbox_directory=library_result.file_path
1424
+ )
1425
+ else:
1426
+ # Handle config-based library - register it directly using the file path
1427
+ register_request = RegisterLibraryFromFileRequest(
1428
+ file_path=library_result.file_path, load_as_default_library=False
1429
+ )
1430
+ register_result = self.register_library_from_file_request(register_request)
1431
+ if isinstance(register_result, RegisterLibraryFromFileResultFailure):
1432
+ # Registration failed - the failure info is already recorded in _library_file_path_to_info
1433
+ # by register_library_from_file_request, so we just log it here for visibility
1434
+ logger.warning(f"Failed to register library from {library_result.file_path}") # noqa: G004
989
1435
 
990
1436
  # Print 'em all pretty
991
1437
  self.print_library_load_status()
992
1438
 
993
1439
  # Remove any missing libraries AFTER we've printed them for the user.
1440
+ user_libraries_section = "app_events.on_app_initialization_complete.libraries_to_register"
994
1441
  self._remove_missing_libraries_from_config(config_category=user_libraries_section)
995
1442
 
996
1443
  def on_app_initialization_complete(self, _payload: AppInitializationComplete) -> None:
@@ -1032,7 +1479,73 @@ class LibraryManager:
1032
1479
  # Go tell the Workflow Manager that it's turn is now.
1033
1480
  GriptapeNodes.WorkflowManager().on_libraries_initialization_complete()
1034
1481
 
1035
- def _attempt_load_nodes_from_library( # noqa: PLR0913, C901
1482
+ def _load_advanced_library_module(
1483
+ self,
1484
+ library_data: LibrarySchema,
1485
+ base_dir: Path,
1486
+ ) -> AdvancedNodeLibrary | None:
1487
+ """Load the advanced library module and return an instance.
1488
+
1489
+ Args:
1490
+ library_data: The library schema data
1491
+ base_dir: Base directory containing the library files
1492
+
1493
+ Returns:
1494
+ An instance of the AdvancedNodeLibrary class from the module, or None if not specified
1495
+
1496
+ Raises:
1497
+ ImportError: If the module cannot be loaded
1498
+ AttributeError: If no AdvancedNodeLibrary subclass is found
1499
+ TypeError: If the found class cannot be instantiated
1500
+ """
1501
+ from griptape_nodes.node_library.advanced_node_library import AdvancedNodeLibrary
1502
+
1503
+ if not library_data.advanced_library_path:
1504
+ return None
1505
+
1506
+ # Resolve relative path to absolute path
1507
+ advanced_library_module_path = Path(library_data.advanced_library_path)
1508
+ if not advanced_library_module_path.is_absolute():
1509
+ advanced_library_module_path = base_dir / advanced_library_module_path
1510
+
1511
+ # Load the module (supports hot reloading)
1512
+ try:
1513
+ module = self._load_module_from_file(advanced_library_module_path, library_data.name)
1514
+ except Exception as err:
1515
+ msg = f"Failed to load Advanced Library module from '{advanced_library_module_path}': {err}"
1516
+ raise ImportError(msg) from err
1517
+
1518
+ # Find an AdvancedNodeLibrary subclass in the module
1519
+ advanced_library_class = None
1520
+ for obj in vars(module).values():
1521
+ if (
1522
+ isinstance(obj, type)
1523
+ and issubclass(obj, AdvancedNodeLibrary)
1524
+ and obj is not AdvancedNodeLibrary
1525
+ and obj.__module__ == module.__name__
1526
+ ):
1527
+ advanced_library_class = obj
1528
+ break
1529
+
1530
+ if not advanced_library_class:
1531
+ msg = f"No AdvancedNodeLibrary subclass found in Advanced Library module '{advanced_library_module_path}'"
1532
+ raise AttributeError(msg)
1533
+
1534
+ # Create an instance
1535
+ try:
1536
+ advanced_library_instance = advanced_library_class()
1537
+ except Exception as err:
1538
+ msg = f"Failed to instantiate AdvancedNodeLibrary class '{advanced_library_class.__name__}': {err}"
1539
+ raise TypeError(msg) from err
1540
+
1541
+ # Validate the instance
1542
+ if not isinstance(advanced_library_instance, AdvancedNodeLibrary):
1543
+ msg = f"Created instance is not an AdvancedNodeLibrary subclass: {type(advanced_library_instance)}"
1544
+ raise TypeError(msg)
1545
+
1546
+ return advanced_library_instance
1547
+
1548
+ def _attempt_load_nodes_from_library( # noqa: PLR0913, PLR0912, PLR0915, C901
1036
1549
  self,
1037
1550
  library_data: LibrarySchema,
1038
1551
  library: Library,
@@ -1061,6 +1574,21 @@ class LibraryManager:
1061
1574
  problems=problems,
1062
1575
  )
1063
1576
 
1577
+ # Call the before_library_nodes_loaded callback if available
1578
+ advanced_library = library.get_advanced_library()
1579
+ if advanced_library:
1580
+ try:
1581
+ advanced_library.before_library_nodes_loaded(library_data, library)
1582
+ details = f"Successfully called before_library_nodes_loaded callback for library '{library_data.name}'"
1583
+ logger.debug(details)
1584
+ except Exception as err:
1585
+ problem = f"Error calling before_library_nodes_loaded callback: {err}"
1586
+ problems.append(problem)
1587
+ details = (
1588
+ f"Failed to call before_library_nodes_loaded callback for library '{library_data.name}': {err}"
1589
+ )
1590
+ logger.error(details)
1591
+
1064
1592
  # Process each node in the metadata
1065
1593
  for node_definition in library_data.nodes:
1066
1594
  # Resolve relative path to absolute path
@@ -1070,7 +1598,7 @@ class LibraryManager:
1070
1598
 
1071
1599
  try:
1072
1600
  # Dynamically load the module containing the node class
1073
- node_class = self._load_class_from_file(node_file_path, node_definition.class_name)
1601
+ node_class = self._load_class_from_file(node_file_path, node_definition.class_name, library_data.name)
1074
1602
  except Exception as err:
1075
1603
  problems.append(
1076
1604
  f"Failed to load node '{node_definition.class_name}' from '{node_file_path}' with error: {err}"
@@ -1095,6 +1623,18 @@ class LibraryManager:
1095
1623
  # If we got here, at least one node came in.
1096
1624
  any_nodes_loaded_successfully = True
1097
1625
 
1626
+ # Call the after_library_nodes_loaded callback if available
1627
+ if advanced_library:
1628
+ try:
1629
+ advanced_library.after_library_nodes_loaded(library_data, library)
1630
+ details = f"Successfully called after_library_nodes_loaded callback for library '{library_data.name}'"
1631
+ logger.debug(details)
1632
+ except Exception as err:
1633
+ problem = f"Error calling after_library_nodes_loaded callback: {err}"
1634
+ problems.append(problem)
1635
+ details = f"Failed to call after_library_nodes_loaded callback for library '{library_data.name}': {err}"
1636
+ logger.error(details)
1637
+
1098
1638
  # Create a LibraryInfo object based on load successes and problem count.
1099
1639
  if not any_nodes_loaded_successfully:
1100
1640
  status = LibraryManager.LibraryStatus.UNUSABLE
@@ -1114,39 +1654,24 @@ class LibraryManager:
1114
1654
  problems=problems,
1115
1655
  )
1116
1656
 
1117
- def _attempt_generate_sandbox_library(self, config_category: str) -> None:
1118
- config_mgr = GriptapeNodes.ConfigManager()
1119
- sandbox_library_subdir = config_mgr.get_config_value(config_category)
1120
- if not sandbox_library_subdir:
1121
- logger.debug("No sandbox directory specified in config at key '%s'. Skipping.", config_category)
1122
- return
1123
-
1124
- # Prepend the workflow directory; if the sandbox dir starts with a slash, the workflow dir will be ignored.
1125
- sandbox_library_dir = config_mgr.workspace_path / sandbox_library_subdir
1657
+ def _attempt_generate_sandbox_library_from_schema(
1658
+ self, library_schema: LibrarySchema, sandbox_directory: str
1659
+ ) -> None:
1660
+ """Generate sandbox library using an existing schema, loading actual node classes."""
1661
+ sandbox_library_dir = Path(sandbox_directory)
1126
1662
  sandbox_library_dir_as_posix = sandbox_library_dir.as_posix()
1127
1663
 
1128
- sandbox_node_candidates = self._find_files_in_dir(directory=sandbox_library_dir, extension=".py")
1129
- if not sandbox_node_candidates:
1130
- logger.debug("No candidate files found in sandbox directory '%s'. Skipping.", sandbox_library_dir)
1131
- return
1132
-
1133
- sandbox_category = CategoryDefinition(
1134
- title="Sandbox",
1135
- description="Nodes loaded from the Sandbox Library.",
1136
- color="#ff0000",
1137
- icon="Folder",
1138
- )
1139
-
1140
1664
  problems = []
1141
1665
 
1142
- # Trawl through the Python files and find those that are nodes.
1143
- node_definitions = []
1144
- for candidate in sandbox_node_candidates:
1666
+ # Get the file paths from the schema's node definitions to load actual classes
1667
+ actual_node_definitions = []
1668
+ for node_def in library_schema.nodes:
1669
+ candidate_path = Path(node_def.file_path)
1145
1670
  try:
1146
- module = self._load_module_from_file(candidate)
1671
+ module = self._load_module_from_file(candidate_path, LibraryManager.SANDBOX_LIBRARY_NAME)
1147
1672
  except Exception as err:
1148
- problems.append(f"Could not load module in sandbox library '{candidate}': {err}")
1149
- details = f"Attempted to load module in sandbox library '{candidate}'. Failed because an exception occurred: {err}."
1673
+ problems.append(f"Could not load module in sandbox library '{candidate_path}': {err}")
1674
+ details = f"Attempted to load module in sandbox library '{candidate_path}'. Failed because an exception occurred: {err}."
1150
1675
  logger.warning(details)
1151
1676
  continue # SKIP IT
1152
1677
 
@@ -1158,47 +1683,41 @@ class LibraryManager:
1158
1683
  and type(obj) is not BaseNode
1159
1684
  and obj.__module__ == module.__name__
1160
1685
  ):
1161
- details = f"Found node '{class_name}' in sandbox library '{candidate}'."
1686
+ details = f"Found node '{class_name}' in sandbox library '{candidate_path}'."
1162
1687
  logger.debug(details)
1688
+
1689
+ # Get metadata from class attributes if they exist, otherwise use defaults
1690
+ node_icon = getattr(obj, "ICON", "square-dashed")
1691
+ node_description = getattr(
1692
+ obj, "DESCRIPTION", f"'{class_name}' (loaded from the {LibraryManager.SANDBOX_LIBRARY_NAME})."
1693
+ )
1694
+ node_color = getattr(obj, "COLOR", None)
1695
+
1163
1696
  node_metadata = NodeMetadata(
1164
1697
  category="Griptape Nodes Sandbox",
1165
- description=f"'{class_name}' (loaded from the Sandbox Library).",
1698
+ description=node_description,
1166
1699
  display_name=class_name,
1700
+ icon=node_icon,
1701
+ color=node_color,
1167
1702
  )
1168
1703
  node_definition = NodeDefinition(
1169
1704
  class_name=class_name,
1170
- file_path=str(candidate),
1705
+ file_path=str(candidate_path),
1171
1706
  metadata=node_metadata,
1172
1707
  )
1173
- node_definitions.append(node_definition)
1708
+ actual_node_definitions.append(node_definition)
1174
1709
 
1175
- if not node_definitions:
1710
+ if not actual_node_definitions:
1176
1711
  logger.info("No nodes found in sandbox library '%s'. Skipping.", sandbox_library_dir)
1177
1712
  return
1178
1713
 
1179
- # Create the library schema and metadata.
1180
- engine_version = GriptapeNodes().handle_engine_version_request(request=GetEngineVersionRequest())
1181
- if not isinstance(engine_version, GetEngineVersionResultSuccess):
1182
- logger.error("Could not get engine version. Skipping sandbox library.")
1183
- return
1184
- engine_version_str = f"{engine_version.major}.{engine_version.minor}.{engine_version.patch}"
1185
- library_metadata = LibraryMetadata(
1186
- author="Author needs to be specified when library is published.",
1187
- description="Nodes loaded from the sandbox library.",
1188
- library_version=engine_version_str,
1189
- engine_version=engine_version_str,
1190
- tags=["sandbox"],
1191
- is_griptape_nodes_searchable=False,
1192
- )
1193
- categories = [
1194
- {"Griptape Nodes Sandbox": sandbox_category},
1195
- ]
1714
+ # Use the existing schema but replace nodes with actual discovered ones
1196
1715
  library_data = LibrarySchema(
1197
- name="Sandbox Library",
1198
- library_schema_version=LibrarySchema.LATEST_SCHEMA_VERSION,
1199
- metadata=library_metadata,
1200
- categories=categories,
1201
- nodes=node_definitions,
1716
+ name=library_schema.name,
1717
+ library_schema_version=library_schema.library_schema_version,
1718
+ metadata=library_schema.metadata,
1719
+ categories=library_schema.categories,
1720
+ nodes=actual_node_definitions,
1202
1721
  )
1203
1722
 
1204
1723
  # Register the library.
@@ -1215,22 +1734,24 @@ class LibraryManager:
1215
1734
  self._library_file_path_to_info[sandbox_library_dir_as_posix] = LibraryManager.LibraryInfo(
1216
1735
  library_path=sandbox_library_dir_as_posix,
1217
1736
  library_name=library_data.name,
1218
- library_version=engine_version_str,
1737
+ library_version=library_data.metadata.library_version,
1219
1738
  status=LibraryManager.LibraryStatus.UNUSABLE,
1220
- problems=["Failed because a library with this name was already registered."],
1739
+ problems=[
1740
+ "Failed because a library with this name was already registered. Check the Settings to ensure duplicate libraries are not being loaded."
1741
+ ],
1221
1742
  )
1222
1743
 
1223
1744
  details = f"Attempted to load Library JSON file from '{sandbox_library_dir}'. Failed because a Library '{library_data.name}' already exists. Error: {err}."
1224
1745
  logger.error(details)
1225
1746
  return
1226
1747
 
1227
- # Attempt to load nodes from the library.
1748
+ # Load nodes into the library
1228
1749
  library_load_results = self._attempt_load_nodes_from_library(
1229
1750
  library_data=library_data,
1230
1751
  library=library,
1231
- base_dir=sandbox_library_dir_as_posix,
1752
+ base_dir=sandbox_library_dir,
1232
1753
  library_file_path=sandbox_library_dir_as_posix,
1233
- library_version=engine_version_str,
1754
+ library_version=library_data.metadata.library_version,
1234
1755
  problems=problems,
1235
1756
  )
1236
1757
  self._library_file_path_to_info[sandbox_library_dir_as_posix] = library_load_results