griptape-nodes 0.62.3__py3-none-any.whl → 0.63.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 (76) hide show
  1. griptape_nodes/cli/commands/libraries.py +6 -21
  2. griptape_nodes/cli/commands/models.py +21 -4
  3. griptape_nodes/drivers/thread_storage/__init__.py +15 -0
  4. griptape_nodes/drivers/thread_storage/base_thread_storage_driver.py +106 -0
  5. griptape_nodes/drivers/thread_storage/griptape_cloud_thread_storage_driver.py +213 -0
  6. griptape_nodes/drivers/thread_storage/local_thread_storage_driver.py +137 -0
  7. griptape_nodes/drivers/thread_storage/thread_storage_backend.py +10 -0
  8. griptape_nodes/node_library/library_registry.py +16 -9
  9. griptape_nodes/node_library/workflow_registry.py +1 -1
  10. griptape_nodes/retained_mode/events/agent_events.py +232 -9
  11. griptape_nodes/retained_mode/events/library_events.py +32 -3
  12. griptape_nodes/retained_mode/events/model_events.py +4 -4
  13. griptape_nodes/retained_mode/events/os_events.py +101 -1
  14. griptape_nodes/retained_mode/managers/agent_manager.py +335 -135
  15. griptape_nodes/retained_mode/managers/fitness_problems/__init__.py +1 -0
  16. griptape_nodes/retained_mode/managers/fitness_problems/libraries/__init__.py +59 -0
  17. griptape_nodes/retained_mode/managers/fitness_problems/libraries/advanced_library_load_failure_problem.py +33 -0
  18. griptape_nodes/retained_mode/managers/fitness_problems/libraries/after_library_callback_problem.py +32 -0
  19. griptape_nodes/retained_mode/managers/fitness_problems/libraries/before_library_callback_problem.py +32 -0
  20. griptape_nodes/retained_mode/managers/fitness_problems/libraries/create_config_category_problem.py +32 -0
  21. griptape_nodes/retained_mode/managers/fitness_problems/libraries/dependency_installation_failed_problem.py +32 -0
  22. griptape_nodes/retained_mode/managers/fitness_problems/libraries/deprecated_node_warning_problem.py +83 -0
  23. griptape_nodes/retained_mode/managers/fitness_problems/libraries/duplicate_library_problem.py +28 -0
  24. griptape_nodes/retained_mode/managers/fitness_problems/libraries/duplicate_node_registration_problem.py +44 -0
  25. griptape_nodes/retained_mode/managers/fitness_problems/libraries/engine_version_error_problem.py +28 -0
  26. griptape_nodes/retained_mode/managers/fitness_problems/libraries/insufficient_disk_space_problem.py +33 -0
  27. griptape_nodes/retained_mode/managers/fitness_problems/libraries/invalid_version_string_problem.py +32 -0
  28. griptape_nodes/retained_mode/managers/fitness_problems/libraries/library_json_decode_problem.py +28 -0
  29. griptape_nodes/retained_mode/managers/fitness_problems/libraries/library_load_exception_problem.py +32 -0
  30. griptape_nodes/retained_mode/managers/fitness_problems/libraries/library_not_found_problem.py +30 -0
  31. griptape_nodes/retained_mode/managers/fitness_problems/libraries/library_problem.py +20 -0
  32. griptape_nodes/retained_mode/managers/fitness_problems/libraries/library_schema_exception_problem.py +32 -0
  33. griptape_nodes/retained_mode/managers/fitness_problems/libraries/library_schema_validation_problem.py +38 -0
  34. griptape_nodes/retained_mode/managers/fitness_problems/libraries/modified_parameters_set_deprecation_warning_problem.py +44 -0
  35. griptape_nodes/retained_mode/managers/fitness_problems/libraries/modified_parameters_set_removed_problem.py +44 -0
  36. griptape_nodes/retained_mode/managers/fitness_problems/libraries/node_class_not_base_node_problem.py +40 -0
  37. griptape_nodes/retained_mode/managers/fitness_problems/libraries/node_class_not_found_problem.py +38 -0
  38. griptape_nodes/retained_mode/managers/fitness_problems/libraries/node_module_import_problem.py +53 -0
  39. griptape_nodes/retained_mode/managers/fitness_problems/libraries/sandbox_directory_missing_problem.py +28 -0
  40. griptape_nodes/retained_mode/managers/fitness_problems/libraries/ui_options_field_modified_incompatible_problem.py +44 -0
  41. griptape_nodes/retained_mode/managers/fitness_problems/libraries/ui_options_field_modified_warning_problem.py +35 -0
  42. griptape_nodes/retained_mode/managers/fitness_problems/libraries/update_config_category_problem.py +32 -0
  43. griptape_nodes/retained_mode/managers/fitness_problems/libraries/venv_creation_failed_problem.py +32 -0
  44. griptape_nodes/retained_mode/managers/fitness_problems/workflows/__init__.py +75 -0
  45. griptape_nodes/retained_mode/managers/fitness_problems/workflows/deprecated_node_in_workflow_problem.py +83 -0
  46. griptape_nodes/retained_mode/managers/fitness_problems/workflows/invalid_dependency_version_string_problem.py +38 -0
  47. griptape_nodes/retained_mode/managers/fitness_problems/workflows/invalid_library_version_string_problem.py +38 -0
  48. griptape_nodes/retained_mode/managers/fitness_problems/workflows/invalid_metadata_schema_problem.py +31 -0
  49. griptape_nodes/retained_mode/managers/fitness_problems/workflows/invalid_metadata_section_count_problem.py +31 -0
  50. griptape_nodes/retained_mode/managers/fitness_problems/workflows/invalid_toml_format_problem.py +30 -0
  51. griptape_nodes/retained_mode/managers/fitness_problems/workflows/library_not_registered_problem.py +35 -0
  52. griptape_nodes/retained_mode/managers/fitness_problems/workflows/library_version_below_required_problem.py +41 -0
  53. griptape_nodes/retained_mode/managers/fitness_problems/workflows/library_version_large_difference_problem.py +41 -0
  54. griptape_nodes/retained_mode/managers/fitness_problems/workflows/library_version_major_mismatch_problem.py +41 -0
  55. griptape_nodes/retained_mode/managers/fitness_problems/workflows/library_version_minor_difference_problem.py +41 -0
  56. griptape_nodes/retained_mode/managers/fitness_problems/workflows/missing_creation_date_problem.py +30 -0
  57. griptape_nodes/retained_mode/managers/fitness_problems/workflows/missing_last_modified_date_problem.py +30 -0
  58. griptape_nodes/retained_mode/managers/fitness_problems/workflows/missing_toml_section_problem.py +30 -0
  59. griptape_nodes/retained_mode/managers/fitness_problems/workflows/node_type_not_found_problem.py +51 -0
  60. griptape_nodes/retained_mode/managers/fitness_problems/workflows/workflow_not_found_problem.py +27 -0
  61. griptape_nodes/retained_mode/managers/fitness_problems/workflows/workflow_problem.py +20 -0
  62. griptape_nodes/retained_mode/managers/fitness_problems/workflows/workflow_schema_version_problem.py +39 -0
  63. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/local_file.py +17 -3
  64. griptape_nodes/retained_mode/managers/library_manager.py +159 -75
  65. griptape_nodes/retained_mode/managers/model_manager.py +182 -205
  66. griptape_nodes/retained_mode/managers/os_manager.py +172 -1
  67. griptape_nodes/retained_mode/managers/settings.py +5 -0
  68. griptape_nodes/retained_mode/managers/version_compatibility_manager.py +76 -51
  69. griptape_nodes/retained_mode/managers/workflow_manager.py +154 -137
  70. griptape_nodes/servers/static.py +18 -19
  71. griptape_nodes/version_compatibility/versions/v0_39_0/modified_parameters_set_removal.py +16 -12
  72. griptape_nodes/version_compatibility/workflow_versions/v0_7_0/local_executor_argument_addition.py +6 -3
  73. {griptape_nodes-0.62.3.dist-info → griptape_nodes-0.63.1.dist-info}/METADATA +3 -2
  74. {griptape_nodes-0.62.3.dist-info → griptape_nodes-0.63.1.dist-info}/RECORD +76 -23
  75. {griptape_nodes-0.62.3.dist-info → griptape_nodes-0.63.1.dist-info}/WHEEL +0 -0
  76. {griptape_nodes-0.62.3.dist-info → griptape_nodes-0.63.1.dist-info}/entry_points.txt +0 -0
@@ -9,6 +9,7 @@ import platform
9
9
  import subprocess
10
10
  import sys
11
11
  import sysconfig
12
+ from collections import defaultdict
12
13
  from dataclasses import dataclass, field
13
14
  from importlib.resources import files
14
15
  from pathlib import Path
@@ -75,6 +76,9 @@ from griptape_nodes.retained_mode.events.library_events import (
75
76
  ListNodeTypesInLibraryResultSuccess,
76
77
  ListRegisteredLibrariesRequest,
77
78
  ListRegisteredLibrariesResultSuccess,
79
+ LoadLibrariesRequest,
80
+ LoadLibrariesResultFailure,
81
+ LoadLibrariesResultSuccess,
78
82
  LoadLibraryMetadataFromFileRequest,
79
83
  LoadLibraryMetadataFromFileResultFailure,
80
84
  LoadLibraryMetadataFromFileResultSuccess,
@@ -96,6 +100,29 @@ from griptape_nodes.retained_mode.events.library_events import (
96
100
  from griptape_nodes.retained_mode.events.object_events import ClearAllObjectStateRequest
97
101
  from griptape_nodes.retained_mode.events.payload_registry import PayloadRegistry
98
102
  from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
103
+ from griptape_nodes.retained_mode.managers.fitness_problems.libraries import (
104
+ AdvancedLibraryLoadFailureProblem,
105
+ AfterLibraryCallbackProblem,
106
+ BeforeLibraryCallbackProblem,
107
+ CreateConfigCategoryProblem,
108
+ DependencyInstallationFailedProblem,
109
+ DuplicateLibraryProblem,
110
+ EngineVersionErrorProblem,
111
+ InsufficientDiskSpaceProblem,
112
+ InvalidVersionStringProblem,
113
+ LibraryJsonDecodeProblem,
114
+ LibraryLoadExceptionProblem,
115
+ LibraryNotFoundProblem,
116
+ LibraryProblem,
117
+ LibrarySchemaExceptionProblem,
118
+ LibrarySchemaValidationProblem,
119
+ NodeClassNotBaseNodeProblem,
120
+ NodeClassNotFoundProblem,
121
+ NodeModuleImportProblem,
122
+ SandboxDirectoryMissingProblem,
123
+ UpdateConfigCategoryProblem,
124
+ VenvCreationFailedProblem,
125
+ )
99
126
  from griptape_nodes.retained_mode.managers.library_lifecycle.library_directory import LibraryDirectory
100
127
  from griptape_nodes.retained_mode.managers.library_lifecycle.library_provenance.local_file import (
101
128
  LibraryProvenanceLocalFile,
@@ -134,7 +161,7 @@ class LibraryManager:
134
161
  library_path: str
135
162
  library_name: str | None = None
136
163
  library_version: str | None = None
137
- problems: list[str] = field(default_factory=list)
164
+ problems: list[LibraryProblem] = field(default_factory=list)
138
165
 
139
166
  _library_file_path_to_info: dict[str, LibraryInfo]
140
167
 
@@ -216,7 +243,8 @@ class LibraryManager:
216
243
  event_manager.assign_manager_to_request_type(
217
244
  UnloadLibraryFromRegistryRequest, self.unload_library_from_registry_request
218
245
  )
219
- event_manager.assign_manager_to_request_type(ReloadAllLibrariesRequest, self.reload_all_libraries_request)
246
+ event_manager.assign_manager_to_request_type(ReloadAllLibrariesRequest, self.reload_libraries_request)
247
+ event_manager.assign_manager_to_request_type(LoadLibrariesRequest, self.load_libraries_request)
220
248
 
221
249
  event_manager.add_listener_to_app_event(
222
250
  AppInitializationComplete,
@@ -240,14 +268,10 @@ class LibraryManager:
240
268
  console.print(panel)
241
269
  return
242
270
 
243
- # Create a table with three columns and row dividers
244
- # Using SQUARE box style which includes row dividers
271
+ # Create a table with two columns and row dividers
245
272
  table = Table(show_header=True, box=HEAVY_EDGE, show_lines=True, expand=True)
246
- table.add_column("Library Name", style="green")
247
- table.add_column("Status", style="green")
248
- table.add_column("Version", style="green")
249
- table.add_column("File Path", style="cyan")
250
- table.add_column("Problems", style="yellow")
273
+ table.add_column("Library", style="green", ratio=1)
274
+ table.add_column("Problems", style="yellow", ratio=1)
251
275
 
252
276
  # Status emojis mapping
253
277
  status_emoji = {
@@ -257,17 +281,20 @@ class LibraryManager:
257
281
  LibraryStatus.MISSING: "[red]?[/red]",
258
282
  }
259
283
 
284
+ # Status text mapping (colored)
285
+ status_text = {
286
+ LibraryStatus.GOOD: "[green](GOOD)[/green]",
287
+ LibraryStatus.FLAWED: "[yellow](FLAWED)[/yellow]",
288
+ LibraryStatus.UNUSABLE: "[red](UNUSABLE)[/red]",
289
+ LibraryStatus.MISSING: "[red](MISSING)[/red]",
290
+ }
291
+
260
292
  # Add rows for each library info
261
293
  for lib_info in library_infos:
262
- # File path column
263
- file_path = lib_info.library_path
264
- file_path_text = Text(file_path, style="cyan")
265
- file_path_text.overflow = "fold" # Force wrapping
266
-
267
- # Library name column with emoji based on status
294
+ # Library column with emoji, name, version, colored status, and file path underneath
268
295
  emoji = status_emoji.get(lib_info.status, "ERROR: Unknown/Unexpected Library Status")
296
+ colored_status = status_text.get(lib_info.status, "(UNKNOWN)")
269
297
  name = lib_info.library_name if lib_info.library_name else "*UNKNOWN*"
270
- library_name = f"{emoji} - {name}"
271
298
 
272
299
  library_version = lib_info.library_version
273
300
  if library_version:
@@ -275,17 +302,36 @@ class LibraryManager:
275
302
  else:
276
303
  version_str = "*UNKNOWN*"
277
304
 
278
- # Problems column - format with numbers if there's more than one
305
+ file_path = lib_info.library_path
306
+ library_name_with_details = Text.from_markup(
307
+ f"{emoji} - {name} v{version_str} {colored_status}\n[cyan dim]{file_path}[/cyan dim]"
308
+ )
309
+ library_name_with_details.overflow = "fold"
310
+
311
+ # Problems column - collate by type then format
279
312
  if not lib_info.problems:
280
313
  problems = "No problems detected."
281
- elif len(lib_info.problems) == 1:
282
- problems = lib_info.problems[0]
283
314
  else:
284
- # Number the problems when there's more than one
285
- problems = "\n".join([f"{j + 1}. {problem}" for j, problem in enumerate(lib_info.problems)])
315
+ # Group problems by type
316
+ problems_by_type = defaultdict(list)
317
+ for problem in lib_info.problems:
318
+ problems_by_type[type(problem)].append(problem)
319
+
320
+ # Collate each group
321
+ collated_strings = []
322
+ for problem_class, instances in problems_by_type.items():
323
+ collated_display = problem_class.collate_problems_for_display(instances)
324
+ collated_strings.append(collated_display)
325
+
326
+ # Format for display
327
+ if len(collated_strings) == 1:
328
+ problems = collated_strings[0]
329
+ else:
330
+ # Number the problems when there's more than one
331
+ problems = "\n".join([f"{j + 1}. {problem}" for j, problem in enumerate(collated_strings)])
286
332
 
287
333
  # Add the row to the table
288
- table.add_row(library_name, lib_info.status.value, version_str, file_path_text, problems)
334
+ table.add_row(library_name_with_details, problems)
289
335
 
290
336
  # Create a panel containing the table
291
337
  panel = Panel(table, title="Library Information", border_style="blue")
@@ -407,9 +453,7 @@ class LibraryManager:
407
453
  library_path=file_path,
408
454
  library_name=None,
409
455
  status=LibraryStatus.MISSING,
410
- problems=[
411
- "Library could not be found at the file path specified. It will be removed from the configuration."
412
- ],
456
+ problems=[LibraryNotFoundProblem(library_path=str(json_path))],
413
457
  result_details=details,
414
458
  )
415
459
 
@@ -424,7 +468,7 @@ class LibraryManager:
424
468
  library_path=file_path,
425
469
  library_name=None,
426
470
  status=LibraryStatus.UNUSABLE,
427
- problems=["Library file not formatted as proper JSON."],
471
+ problems=[LibraryJsonDecodeProblem()],
428
472
  result_details=details,
429
473
  )
430
474
  except Exception as err:
@@ -434,7 +478,7 @@ class LibraryManager:
434
478
  library_path=file_path,
435
479
  library_name=None,
436
480
  status=LibraryStatus.UNUSABLE,
437
- problems=[f"Exception occurred when attempting to load the library: {err}."],
481
+ problems=[LibraryLoadExceptionProblem(error_message=str(err))],
438
482
  result_details=details,
439
483
  )
440
484
 
@@ -451,7 +495,7 @@ class LibraryManager:
451
495
  loc = " -> ".join(map(str, error["loc"]))
452
496
  msg = error["msg"]
453
497
  error_type = error["type"]
454
- problem = f"Error in section '{loc}': {error_type}, {msg}"
498
+ problem = LibrarySchemaValidationProblem(location=loc, error_type=error_type, message=msg)
455
499
  problems.append(problem)
456
500
  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}"
457
501
  logger.error(details)
@@ -469,7 +513,7 @@ class LibraryManager:
469
513
  library_path=file_path,
470
514
  library_name=library_name,
471
515
  status=LibraryStatus.UNUSABLE,
472
- problems=[f"Library file did not match the library schema specified due to: {err}"],
516
+ problems=[LibrarySchemaExceptionProblem(error_message=str(err))],
473
517
  result_details=details,
474
518
  )
475
519
 
@@ -540,7 +584,7 @@ class LibraryManager:
540
584
  library_path=sandbox_library_dir_as_posix,
541
585
  library_name=LibraryManager.SANDBOX_LIBRARY_NAME,
542
586
  status=LibraryStatus.MISSING,
543
- problems=[details],
587
+ problems=[SandboxDirectoryMissingProblem()],
544
588
  result_details=ResultDetails(message=details, level=logging.INFO),
545
589
  )
546
590
 
@@ -597,7 +641,7 @@ class LibraryManager:
597
641
  library_path=sandbox_library_dir_as_posix,
598
642
  library_name=LibraryManager.SANDBOX_LIBRARY_NAME,
599
643
  status=LibraryStatus.UNUSABLE,
600
- problems=[details],
644
+ problems=[EngineVersionErrorProblem()],
601
645
  result_details=details,
602
646
  )
603
647
 
@@ -632,8 +676,6 @@ class LibraryManager:
632
676
  library = LibraryRegistry.get_library(name=request.library)
633
677
  except KeyError:
634
678
  details = f"Attempted to get node metadata for a node type '{request.node_type}' in a Library named '{request.library}'. Failed because no Library with that name was registered."
635
- logger.error(details)
636
-
637
679
  result = GetNodeMetadataFromLibraryResultFailure(result_details=details)
638
680
  return result
639
681
 
@@ -642,8 +684,6 @@ class LibraryManager:
642
684
  metadata = library.get_node_metadata(node_type=request.node_type)
643
685
  except KeyError:
644
686
  details = f"Attempted to get node metadata for a node type '{request.node_type}' in a Library named '{request.library}'. Failed because no node type of that name could be found in the Library."
645
- logger.error(details)
646
-
647
687
  result = GetNodeMetadataFromLibraryResultFailure(result_details=details)
648
688
  return result
649
689
 
@@ -683,9 +723,7 @@ class LibraryManager:
683
723
  library_path=file_path,
684
724
  library_name=None,
685
725
  status=LibraryStatus.MISSING,
686
- problems=[
687
- "Library could not be found at the file path specified. It will be removed from the configuration."
688
- ],
726
+ problems=[LibraryNotFoundProblem(library_path=file_path)],
689
727
  )
690
728
  details = f"Attempted to load Library JSON file. Failed because no file could be found at the specified path: {json_path}"
691
729
  logger.error(details)
@@ -717,9 +755,7 @@ class LibraryManager:
717
755
  library_path=file_path,
718
756
  library_name=library_data.name,
719
757
  status=LibraryStatus.UNUSABLE,
720
- problems=[
721
- f"Library's version string '{library_data.metadata.library_version}' wasn't valid. Must be in major.minor.patch format."
722
- ],
758
+ problems=[InvalidVersionStringProblem(version_string=str(library_data.metadata.library_version))],
723
759
  )
724
760
  details = f"Attempted to load Library '{library_data.name}' JSON file from '{json_path}'. Failed because version string '{library_data.metadata.library_version}' wasn't valid. Must be in major.minor.patch format."
725
761
  logger.error(details)
@@ -745,7 +781,9 @@ class LibraryManager:
745
781
  library_version=library_version,
746
782
  status=LibraryStatus.UNUSABLE,
747
783
  problems=[
748
- f"Failed to load Advanced Library module from '{library_data.advanced_library_path}': {err}"
784
+ AdvancedLibraryLoadFailureProblem(
785
+ advanced_library_path=library_data.advanced_library_path, error_message=str(err)
786
+ )
749
787
  ],
750
788
  )
751
789
  details = f"Attempted to load Library '{library_data.name}' from '{json_path}'. Failed to load Advanced Library module: {err}"
@@ -768,9 +806,7 @@ class LibraryManager:
768
806
  library_name=library_data.name,
769
807
  library_version=library_version,
770
808
  status=LibraryStatus.UNUSABLE,
771
- problems=[
772
- "Failed because a library with this name was already registered. Check the Settings to ensure duplicate libraries are not being loaded."
773
- ],
809
+ problems=[DuplicateLibraryProblem()],
774
810
  )
775
811
 
776
812
  details = f"Attempted to load Library JSON file from '{json_path}'. Failed because a Library '{library_data.name}' already exists. Error: {err}."
@@ -797,7 +833,7 @@ class LibraryManager:
797
833
  library_name=library_data.name,
798
834
  library_version=library_version,
799
835
  status=LibraryStatus.UNUSABLE,
800
- problems=[str(e)],
836
+ problems=[VenvCreationFailedProblem(error_message=str(e))],
801
837
  )
802
838
  details = f"Attempted to load Library JSON file from '{json_path}'. Failed when creating the virtual environment: {e}."
803
839
  logger.error(details)
@@ -815,9 +851,7 @@ class LibraryManager:
815
851
  library_name=library_data.name,
816
852
  library_version=library_version,
817
853
  status=LibraryStatus.UNUSABLE,
818
- problems=[
819
- f"Insufficient disk space for dependencies (requires {min_space_gb} GB): {error_msg}"
820
- ],
854
+ problems=[InsufficientDiskSpaceProblem(min_space_gb=min_space_gb, error_message=error_msg)],
821
855
  )
822
856
  return RegisterLibraryFromFileResultFailure(result_details=details)
823
857
 
@@ -857,7 +891,7 @@ class LibraryManager:
857
891
  library_name=library_data.name,
858
892
  library_version=library_version,
859
893
  status=LibraryStatus.UNUSABLE,
860
- problems=[f"Dependency installation failed: {error_details}"],
894
+ problems=[DependencyInstallationFailedProblem(error_details=error_details)],
861
895
  )
862
896
  details = f"Attempted to load Library JSON file from '{json_path}'. Failed when installing dependencies: {error_details}"
863
897
  logger.error(details)
@@ -881,7 +915,7 @@ class LibraryManager:
881
915
  )
882
916
  create_new_category_result = GriptapeNodes.handle_request(create_new_category_request)
883
917
  if not isinstance(create_new_category_result, SetConfigCategoryResultSuccess):
884
- problems.append(f"Failed to create new config category '{library_data_setting.category}'.")
918
+ problems.append(CreateConfigCategoryProblem(category_name=library_data_setting.category))
885
919
  details = f"Failed attempting to create new config category '{library_data_setting.category}' for library '{library_data.name}'."
886
920
  logger.error(details)
887
921
  continue # SKIP IT
@@ -895,7 +929,7 @@ class LibraryManager:
895
929
  )
896
930
  set_category_result = GriptapeNodes.handle_request(set_category_request)
897
931
  if not isinstance(set_category_result, SetConfigCategoryResultSuccess):
898
- problems.append(f"Failed to update config category '{library_data_setting.category}'.")
932
+ problems.append(UpdateConfigCategoryProblem(category_name=library_data_setting.category))
899
933
  details = f"Failed attempting to update config category '{library_data_setting.category}' for library '{library_data.name}'."
900
934
  logger.error(details)
901
935
  continue # SKIP IT
@@ -1397,6 +1431,21 @@ class LibraryManager:
1397
1431
  """
1398
1432
  return module_name.startswith("gtn_dynamic_module_")
1399
1433
 
1434
+ @staticmethod
1435
+ def _get_root_cause_from_exception(exception: BaseException) -> BaseException:
1436
+ """Walk the exception chain to find the root cause.
1437
+
1438
+ Args:
1439
+ exception: The exception to walk
1440
+
1441
+ Returns:
1442
+ The root cause exception (the innermost exception in the chain)
1443
+ """
1444
+ current = exception
1445
+ while current.__cause__ is not None:
1446
+ current = current.__cause__
1447
+ return current
1448
+
1400
1449
  def _load_module_from_file(self, file_path: Path | str, library_name: str) -> ModuleType:
1401
1450
  """Dynamically load a module from a Python file with support for hot reloading.
1402
1451
 
@@ -1847,7 +1896,7 @@ class LibraryManager:
1847
1896
  base_dir: Path,
1848
1897
  library_file_path: str,
1849
1898
  library_version: str | None,
1850
- problems: list[str],
1899
+ problems: list[LibraryProblem],
1851
1900
  ) -> LibraryManager.LibraryInfo:
1852
1901
  any_nodes_loaded_successfully = False
1853
1902
 
@@ -1855,7 +1904,7 @@ class LibraryManager:
1855
1904
  version_issues = GriptapeNodes.VersionCompatibilityManager().check_library_version_compatibility(library_data)
1856
1905
  has_disqualifying_issues = False
1857
1906
  for issue in version_issues:
1858
- problems.append(issue.message)
1907
+ problems.append(issue.problem)
1859
1908
  if issue.severity == LibraryStatus.UNUSABLE:
1860
1909
  has_disqualifying_issues = True
1861
1910
 
@@ -1877,8 +1926,7 @@ class LibraryManager:
1877
1926
  details = f"Successfully called before_library_nodes_loaded callback for library '{library_data.name}'"
1878
1927
  logger.debug(details)
1879
1928
  except Exception as err:
1880
- problem = f"Error calling before_library_nodes_loaded callback: {err}"
1881
- problems.append(problem)
1929
+ problems.append(BeforeLibraryCallbackProblem(error_message=str(err)))
1882
1930
  details = (
1883
1931
  f"Failed to call before_library_nodes_loaded callback for library '{library_data.name}': {err}"
1884
1932
  )
@@ -1894,27 +1942,39 @@ class LibraryManager:
1894
1942
  try:
1895
1943
  # Dynamically load the module containing the node class
1896
1944
  node_class = self._load_class_from_file(node_file_path, node_definition.class_name, library_data.name)
1897
- except Exception as err:
1945
+ except ImportError as err:
1946
+ root_cause = self._get_root_cause_from_exception(err)
1898
1947
  problems.append(
1899
- f"Failed to load node '{node_definition.class_name}' from '{node_file_path}' with error: {err}"
1948
+ NodeModuleImportProblem(
1949
+ class_name=node_definition.class_name,
1950
+ file_path=str(node_file_path),
1951
+ error_message=str(err),
1952
+ root_cause=str(root_cause),
1953
+ )
1900
1954
  )
1901
- details = f"Attempted to load node '{node_definition.class_name}' from '{node_file_path}'. Failed because an exception occurred: {err}"
1955
+ details = f"Attempted to load node '{node_definition.class_name}' from '{node_file_path}'. Failed because module could not be imported: {err}"
1902
1956
  logger.error(details)
1903
1957
  continue # SKIP IT
1904
-
1905
- try:
1906
- # Register the node type with the library
1907
- forensics_string = library.register_new_node_type(node_class, metadata=node_definition.metadata)
1908
- if forensics_string is not None:
1909
- problems.append(forensics_string)
1910
- except Exception as err:
1958
+ except AttributeError:
1911
1959
  problems.append(
1912
- f"Failed to register node '{node_definition.class_name}' from '{node_file_path}' with error: {err}"
1960
+ NodeClassNotFoundProblem(class_name=node_definition.class_name, file_path=str(node_file_path))
1913
1961
  )
1914
- details = f"Attempted to load node '{node_definition.class_name}' from '{node_file_path}'. Failed because an exception occurred: {err}"
1962
+ details = f"Attempted to load node '{node_definition.class_name}' from '{node_file_path}'. Failed because class not found in module"
1963
+ logger.error(details)
1964
+ continue # SKIP IT
1965
+ except TypeError:
1966
+ problems.append(
1967
+ NodeClassNotBaseNodeProblem(class_name=node_definition.class_name, file_path=str(node_file_path))
1968
+ )
1969
+ details = f"Attempted to load node '{node_definition.class_name}' from '{node_file_path}'. Failed because class doesn't inherit from BaseNode"
1915
1970
  logger.error(details)
1916
1971
  continue # SKIP IT
1917
1972
 
1973
+ # Register the node type with the library
1974
+ library_problem = library.register_new_node_type(node_class, metadata=node_definition.metadata)
1975
+ if library_problem is not None:
1976
+ problems.append(library_problem)
1977
+
1918
1978
  # If we got here, at least one node came in.
1919
1979
  any_nodes_loaded_successfully = True
1920
1980
 
@@ -1925,8 +1985,7 @@ class LibraryManager:
1925
1985
  details = f"Successfully called after_library_nodes_loaded callback for library '{library_data.name}'"
1926
1986
  logger.debug(details)
1927
1987
  except Exception as err:
1928
- problem = f"Error calling after_library_nodes_loaded callback: {err}"
1929
- problems.append(problem)
1988
+ problems.append(AfterLibraryCallbackProblem(error_message=str(err)))
1930
1989
  details = f"Failed to call after_library_nodes_loaded callback for library '{library_data.name}': {err}"
1931
1990
  logger.error(details)
1932
1991
 
@@ -1965,7 +2024,15 @@ class LibraryManager:
1965
2024
  try:
1966
2025
  module = self._load_module_from_file(candidate_path, LibraryManager.SANDBOX_LIBRARY_NAME)
1967
2026
  except Exception as err:
1968
- problems.append(f"Could not load module in sandbox library '{candidate_path}': {err}")
2027
+ root_cause = self._get_root_cause_from_exception(err)
2028
+ problems.append(
2029
+ NodeModuleImportProblem(
2030
+ class_name=node_def.class_name,
2031
+ file_path=str(candidate_path),
2032
+ error_message=str(err),
2033
+ root_cause=str(root_cause),
2034
+ )
2035
+ )
1969
2036
  details = f"Attempted to load module in sandbox library '{candidate_path}'. Failed because an exception occurred: {err}."
1970
2037
  logger.warning(details)
1971
2038
  continue # SKIP IT
@@ -2031,9 +2098,7 @@ class LibraryManager:
2031
2098
  library_name=library_data.name,
2032
2099
  library_version=library_data.metadata.library_version,
2033
2100
  status=LibraryStatus.UNUSABLE,
2034
- problems=[
2035
- "Failed because a library with this name was already registered. Check the Settings to ensure duplicate libraries are not being loaded."
2036
- ],
2101
+ problems=[DuplicateLibraryProblem()],
2037
2102
  )
2038
2103
 
2039
2104
  details = f"Attempted to load Library JSON file from '{sandbox_library_dir}'. Failed because a Library '{library_data.name}' already exists. Error: {err}."
@@ -2078,7 +2143,7 @@ class LibraryManager:
2078
2143
  ]
2079
2144
  config_mgr.set_config_value(config_category, libraries_to_register_category)
2080
2145
 
2081
- async def reload_all_libraries_request(self, request: ReloadAllLibrariesRequest) -> ResultPayload: # noqa: ARG002
2146
+ async def reload_libraries_request(self, request: ReloadAllLibrariesRequest) -> ResultPayload: # noqa: ARG002
2082
2147
  # Start with a clean slate.
2083
2148
  clear_all_request = ClearAllObjectStateRequest(i_know_what_im_doing=True)
2084
2149
  clear_all_result = await GriptapeNodes.ahandle_request(clear_all_request)
@@ -2111,6 +2176,25 @@ class LibraryManager:
2111
2176
  )
2112
2177
  return ReloadAllLibrariesResultSuccess(result_details=ResultDetails(message=details, level=logging.INFO))
2113
2178
 
2179
+ async def load_libraries_request(self, request: LoadLibrariesRequest) -> ResultPayload: # noqa: ARG002
2180
+ # Check if there are any new libraries in config that haven't been loaded yet
2181
+ discovered_libraries = {str(path) for path in self._discover_library_files()}
2182
+ loaded_libraries = set(self._library_file_path_to_info.keys())
2183
+ unloaded_libraries = discovered_libraries - loaded_libraries
2184
+
2185
+ if not unloaded_libraries:
2186
+ details = "All configured libraries are already loaded, no action needed."
2187
+ return LoadLibrariesResultSuccess(result_details=ResultDetails(message=details, level=logging.INFO))
2188
+
2189
+ try:
2190
+ await self.load_all_libraries_from_config()
2191
+ details = "Successfully loaded all libraries from configuration."
2192
+ return LoadLibrariesResultSuccess(result_details=ResultDetails(message=details, level=logging.INFO))
2193
+ except Exception as e:
2194
+ details = f"Failed to load libraries from configuration: {e}"
2195
+ logger.error(details)
2196
+ return LoadLibrariesResultFailure(result_details=details)
2197
+
2114
2198
  def _discover_library_files(self) -> list[Path]:
2115
2199
  """Discover library JSON files from config and workspace recursively.
2116
2200