griptape-nodes 0.62.2__py3-none-any.whl → 0.63.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 (74) hide show
  1. griptape_nodes/cli/commands/libraries.py +6 -21
  2. griptape_nodes/drivers/thread_storage/__init__.py +15 -0
  3. griptape_nodes/drivers/thread_storage/base_thread_storage_driver.py +106 -0
  4. griptape_nodes/drivers/thread_storage/griptape_cloud_thread_storage_driver.py +213 -0
  5. griptape_nodes/drivers/thread_storage/local_thread_storage_driver.py +137 -0
  6. griptape_nodes/drivers/thread_storage/thread_storage_backend.py +10 -0
  7. griptape_nodes/node_library/library_registry.py +16 -9
  8. griptape_nodes/node_library/workflow_registry.py +1 -1
  9. griptape_nodes/retained_mode/events/agent_events.py +232 -9
  10. griptape_nodes/retained_mode/events/app_events.py +38 -0
  11. griptape_nodes/retained_mode/events/library_events.py +32 -3
  12. griptape_nodes/retained_mode/events/os_events.py +101 -1
  13. griptape_nodes/retained_mode/managers/agent_manager.py +335 -135
  14. griptape_nodes/retained_mode/managers/fitness_problems/__init__.py +1 -0
  15. griptape_nodes/retained_mode/managers/fitness_problems/libraries/__init__.py +59 -0
  16. griptape_nodes/retained_mode/managers/fitness_problems/libraries/advanced_library_load_failure_problem.py +33 -0
  17. griptape_nodes/retained_mode/managers/fitness_problems/libraries/after_library_callback_problem.py +32 -0
  18. griptape_nodes/retained_mode/managers/fitness_problems/libraries/before_library_callback_problem.py +32 -0
  19. griptape_nodes/retained_mode/managers/fitness_problems/libraries/create_config_category_problem.py +32 -0
  20. griptape_nodes/retained_mode/managers/fitness_problems/libraries/dependency_installation_failed_problem.py +32 -0
  21. griptape_nodes/retained_mode/managers/fitness_problems/libraries/deprecated_node_warning_problem.py +83 -0
  22. griptape_nodes/retained_mode/managers/fitness_problems/libraries/duplicate_library_problem.py +28 -0
  23. griptape_nodes/retained_mode/managers/fitness_problems/libraries/duplicate_node_registration_problem.py +44 -0
  24. griptape_nodes/retained_mode/managers/fitness_problems/libraries/engine_version_error_problem.py +28 -0
  25. griptape_nodes/retained_mode/managers/fitness_problems/libraries/insufficient_disk_space_problem.py +33 -0
  26. griptape_nodes/retained_mode/managers/fitness_problems/libraries/invalid_version_string_problem.py +32 -0
  27. griptape_nodes/retained_mode/managers/fitness_problems/libraries/library_json_decode_problem.py +28 -0
  28. griptape_nodes/retained_mode/managers/fitness_problems/libraries/library_load_exception_problem.py +32 -0
  29. griptape_nodes/retained_mode/managers/fitness_problems/libraries/library_not_found_problem.py +30 -0
  30. griptape_nodes/retained_mode/managers/fitness_problems/libraries/library_problem.py +20 -0
  31. griptape_nodes/retained_mode/managers/fitness_problems/libraries/library_schema_exception_problem.py +32 -0
  32. griptape_nodes/retained_mode/managers/fitness_problems/libraries/library_schema_validation_problem.py +38 -0
  33. griptape_nodes/retained_mode/managers/fitness_problems/libraries/modified_parameters_set_deprecation_warning_problem.py +44 -0
  34. griptape_nodes/retained_mode/managers/fitness_problems/libraries/modified_parameters_set_removed_problem.py +44 -0
  35. griptape_nodes/retained_mode/managers/fitness_problems/libraries/node_class_not_base_node_problem.py +40 -0
  36. griptape_nodes/retained_mode/managers/fitness_problems/libraries/node_class_not_found_problem.py +38 -0
  37. griptape_nodes/retained_mode/managers/fitness_problems/libraries/node_module_import_problem.py +53 -0
  38. griptape_nodes/retained_mode/managers/fitness_problems/libraries/sandbox_directory_missing_problem.py +28 -0
  39. griptape_nodes/retained_mode/managers/fitness_problems/libraries/ui_options_field_modified_incompatible_problem.py +44 -0
  40. griptape_nodes/retained_mode/managers/fitness_problems/libraries/ui_options_field_modified_warning_problem.py +35 -0
  41. griptape_nodes/retained_mode/managers/fitness_problems/libraries/update_config_category_problem.py +32 -0
  42. griptape_nodes/retained_mode/managers/fitness_problems/libraries/venv_creation_failed_problem.py +32 -0
  43. griptape_nodes/retained_mode/managers/fitness_problems/workflows/__init__.py +75 -0
  44. griptape_nodes/retained_mode/managers/fitness_problems/workflows/deprecated_node_in_workflow_problem.py +83 -0
  45. griptape_nodes/retained_mode/managers/fitness_problems/workflows/invalid_dependency_version_string_problem.py +38 -0
  46. griptape_nodes/retained_mode/managers/fitness_problems/workflows/invalid_library_version_string_problem.py +38 -0
  47. griptape_nodes/retained_mode/managers/fitness_problems/workflows/invalid_metadata_schema_problem.py +31 -0
  48. griptape_nodes/retained_mode/managers/fitness_problems/workflows/invalid_metadata_section_count_problem.py +31 -0
  49. griptape_nodes/retained_mode/managers/fitness_problems/workflows/invalid_toml_format_problem.py +30 -0
  50. griptape_nodes/retained_mode/managers/fitness_problems/workflows/library_not_registered_problem.py +35 -0
  51. griptape_nodes/retained_mode/managers/fitness_problems/workflows/library_version_below_required_problem.py +41 -0
  52. griptape_nodes/retained_mode/managers/fitness_problems/workflows/library_version_large_difference_problem.py +41 -0
  53. griptape_nodes/retained_mode/managers/fitness_problems/workflows/library_version_major_mismatch_problem.py +41 -0
  54. griptape_nodes/retained_mode/managers/fitness_problems/workflows/library_version_minor_difference_problem.py +41 -0
  55. griptape_nodes/retained_mode/managers/fitness_problems/workflows/missing_creation_date_problem.py +30 -0
  56. griptape_nodes/retained_mode/managers/fitness_problems/workflows/missing_last_modified_date_problem.py +30 -0
  57. griptape_nodes/retained_mode/managers/fitness_problems/workflows/missing_toml_section_problem.py +30 -0
  58. griptape_nodes/retained_mode/managers/fitness_problems/workflows/node_type_not_found_problem.py +51 -0
  59. griptape_nodes/retained_mode/managers/fitness_problems/workflows/workflow_not_found_problem.py +27 -0
  60. griptape_nodes/retained_mode/managers/fitness_problems/workflows/workflow_problem.py +20 -0
  61. griptape_nodes/retained_mode/managers/fitness_problems/workflows/workflow_schema_version_problem.py +39 -0
  62. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/local_file.py +17 -3
  63. griptape_nodes/retained_mode/managers/library_manager.py +226 -77
  64. griptape_nodes/retained_mode/managers/os_manager.py +172 -1
  65. griptape_nodes/retained_mode/managers/settings.py +5 -0
  66. griptape_nodes/retained_mode/managers/version_compatibility_manager.py +76 -51
  67. griptape_nodes/retained_mode/managers/workflow_manager.py +237 -159
  68. griptape_nodes/servers/static.py +18 -19
  69. griptape_nodes/version_compatibility/versions/v0_39_0/modified_parameters_set_removal.py +16 -12
  70. griptape_nodes/version_compatibility/workflow_versions/v0_7_0/local_executor_argument_addition.py +6 -3
  71. {griptape_nodes-0.62.2.dist-info → griptape_nodes-0.63.0.dist-info}/METADATA +2 -1
  72. {griptape_nodes-0.62.2.dist-info → griptape_nodes-0.63.0.dist-info}/RECORD +74 -21
  73. {griptape_nodes-0.62.2.dist-info → griptape_nodes-0.63.0.dist-info}/WHEEL +0 -0
  74. {griptape_nodes-0.62.2.dist-info → griptape_nodes-0.63.0.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
@@ -36,12 +37,15 @@ from griptape_nodes.node_library.library_registry import (
36
37
  )
37
38
  from griptape_nodes.retained_mode.events.app_events import (
38
39
  AppInitializationComplete,
40
+ EngineInitializationProgress,
39
41
  GetEngineVersionRequest,
40
42
  GetEngineVersionResultSuccess,
43
+ InitializationPhase,
44
+ InitializationStatus,
41
45
  )
42
46
 
43
47
  # Runtime imports for ResultDetails since it's used at runtime
44
- from griptape_nodes.retained_mode.events.base_events import ResultDetails
48
+ from griptape_nodes.retained_mode.events.base_events import AppEvent, ResultDetails
45
49
  from griptape_nodes.retained_mode.events.config_events import (
46
50
  GetConfigCategoryRequest,
47
51
  GetConfigCategoryResultSuccess,
@@ -72,6 +76,9 @@ from griptape_nodes.retained_mode.events.library_events import (
72
76
  ListNodeTypesInLibraryResultSuccess,
73
77
  ListRegisteredLibrariesRequest,
74
78
  ListRegisteredLibrariesResultSuccess,
79
+ LoadLibrariesRequest,
80
+ LoadLibrariesResultFailure,
81
+ LoadLibrariesResultSuccess,
75
82
  LoadLibraryMetadataFromFileRequest,
76
83
  LoadLibraryMetadataFromFileResultFailure,
77
84
  LoadLibraryMetadataFromFileResultSuccess,
@@ -93,6 +100,29 @@ from griptape_nodes.retained_mode.events.library_events import (
93
100
  from griptape_nodes.retained_mode.events.object_events import ClearAllObjectStateRequest
94
101
  from griptape_nodes.retained_mode.events.payload_registry import PayloadRegistry
95
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
+ )
96
126
  from griptape_nodes.retained_mode.managers.library_lifecycle.library_directory import LibraryDirectory
97
127
  from griptape_nodes.retained_mode.managers.library_lifecycle.library_provenance.local_file import (
98
128
  LibraryProvenanceLocalFile,
@@ -131,7 +161,7 @@ class LibraryManager:
131
161
  library_path: str
132
162
  library_name: str | None = None
133
163
  library_version: str | None = None
134
- problems: list[str] = field(default_factory=list)
164
+ problems: list[LibraryProblem] = field(default_factory=list)
135
165
 
136
166
  _library_file_path_to_info: dict[str, LibraryInfo]
137
167
 
@@ -213,7 +243,8 @@ class LibraryManager:
213
243
  event_manager.assign_manager_to_request_type(
214
244
  UnloadLibraryFromRegistryRequest, self.unload_library_from_registry_request
215
245
  )
216
- 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)
217
248
 
218
249
  event_manager.add_listener_to_app_event(
219
250
  AppInitializationComplete,
@@ -237,14 +268,10 @@ class LibraryManager:
237
268
  console.print(panel)
238
269
  return
239
270
 
240
- # Create a table with three columns and row dividers
241
- # Using SQUARE box style which includes row dividers
271
+ # Create a table with two columns and row dividers
242
272
  table = Table(show_header=True, box=HEAVY_EDGE, show_lines=True, expand=True)
243
- table.add_column("Library Name", style="green")
244
- table.add_column("Status", style="green")
245
- table.add_column("Version", style="green")
246
- table.add_column("File Path", style="cyan")
247
- table.add_column("Problems", style="yellow")
273
+ table.add_column("Library", style="green", ratio=1)
274
+ table.add_column("Problems", style="yellow", ratio=1)
248
275
 
249
276
  # Status emojis mapping
250
277
  status_emoji = {
@@ -254,17 +281,20 @@ class LibraryManager:
254
281
  LibraryStatus.MISSING: "[red]?[/red]",
255
282
  }
256
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
+
257
292
  # Add rows for each library info
258
293
  for lib_info in library_infos:
259
- # File path column
260
- file_path = lib_info.library_path
261
- file_path_text = Text(file_path, style="cyan")
262
- file_path_text.overflow = "fold" # Force wrapping
263
-
264
- # Library name column with emoji based on status
294
+ # Library column with emoji, name, version, colored status, and file path underneath
265
295
  emoji = status_emoji.get(lib_info.status, "ERROR: Unknown/Unexpected Library Status")
296
+ colored_status = status_text.get(lib_info.status, "(UNKNOWN)")
266
297
  name = lib_info.library_name if lib_info.library_name else "*UNKNOWN*"
267
- library_name = f"{emoji} - {name}"
268
298
 
269
299
  library_version = lib_info.library_version
270
300
  if library_version:
@@ -272,17 +302,36 @@ class LibraryManager:
272
302
  else:
273
303
  version_str = "*UNKNOWN*"
274
304
 
275
- # 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
276
312
  if not lib_info.problems:
277
313
  problems = "No problems detected."
278
- elif len(lib_info.problems) == 1:
279
- problems = lib_info.problems[0]
280
314
  else:
281
- # Number the problems when there's more than one
282
- 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)])
283
332
 
284
333
  # Add the row to the table
285
- table.add_row(library_name, lib_info.status.value, version_str, file_path_text, problems)
334
+ table.add_row(library_name_with_details, problems)
286
335
 
287
336
  # Create a panel containing the table
288
337
  panel = Panel(table, title="Library Information", border_style="blue")
@@ -404,9 +453,7 @@ class LibraryManager:
404
453
  library_path=file_path,
405
454
  library_name=None,
406
455
  status=LibraryStatus.MISSING,
407
- problems=[
408
- "Library could not be found at the file path specified. It will be removed from the configuration."
409
- ],
456
+ problems=[LibraryNotFoundProblem(library_path=str(json_path))],
410
457
  result_details=details,
411
458
  )
412
459
 
@@ -421,7 +468,7 @@ class LibraryManager:
421
468
  library_path=file_path,
422
469
  library_name=None,
423
470
  status=LibraryStatus.UNUSABLE,
424
- problems=["Library file not formatted as proper JSON."],
471
+ problems=[LibraryJsonDecodeProblem()],
425
472
  result_details=details,
426
473
  )
427
474
  except Exception as err:
@@ -431,7 +478,7 @@ class LibraryManager:
431
478
  library_path=file_path,
432
479
  library_name=None,
433
480
  status=LibraryStatus.UNUSABLE,
434
- problems=[f"Exception occurred when attempting to load the library: {err}."],
481
+ problems=[LibraryLoadExceptionProblem(error_message=str(err))],
435
482
  result_details=details,
436
483
  )
437
484
 
@@ -448,7 +495,7 @@ class LibraryManager:
448
495
  loc = " -> ".join(map(str, error["loc"]))
449
496
  msg = error["msg"]
450
497
  error_type = error["type"]
451
- problem = f"Error in section '{loc}': {error_type}, {msg}"
498
+ problem = LibrarySchemaValidationProblem(location=loc, error_type=error_type, message=msg)
452
499
  problems.append(problem)
453
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}"
454
501
  logger.error(details)
@@ -466,7 +513,7 @@ class LibraryManager:
466
513
  library_path=file_path,
467
514
  library_name=library_name,
468
515
  status=LibraryStatus.UNUSABLE,
469
- problems=[f"Library file did not match the library schema specified due to: {err}"],
516
+ problems=[LibrarySchemaExceptionProblem(error_message=str(err))],
470
517
  result_details=details,
471
518
  )
472
519
 
@@ -537,7 +584,7 @@ class LibraryManager:
537
584
  library_path=sandbox_library_dir_as_posix,
538
585
  library_name=LibraryManager.SANDBOX_LIBRARY_NAME,
539
586
  status=LibraryStatus.MISSING,
540
- problems=[details],
587
+ problems=[SandboxDirectoryMissingProblem()],
541
588
  result_details=ResultDetails(message=details, level=logging.INFO),
542
589
  )
543
590
 
@@ -594,7 +641,7 @@ class LibraryManager:
594
641
  library_path=sandbox_library_dir_as_posix,
595
642
  library_name=LibraryManager.SANDBOX_LIBRARY_NAME,
596
643
  status=LibraryStatus.UNUSABLE,
597
- problems=[details],
644
+ problems=[EngineVersionErrorProblem()],
598
645
  result_details=details,
599
646
  )
600
647
 
@@ -629,8 +676,6 @@ class LibraryManager:
629
676
  library = LibraryRegistry.get_library(name=request.library)
630
677
  except KeyError:
631
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."
632
- logger.error(details)
633
-
634
679
  result = GetNodeMetadataFromLibraryResultFailure(result_details=details)
635
680
  return result
636
681
 
@@ -639,8 +684,6 @@ class LibraryManager:
639
684
  metadata = library.get_node_metadata(node_type=request.node_type)
640
685
  except KeyError:
641
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."
642
- logger.error(details)
643
-
644
687
  result = GetNodeMetadataFromLibraryResultFailure(result_details=details)
645
688
  return result
646
689
 
@@ -680,9 +723,7 @@ class LibraryManager:
680
723
  library_path=file_path,
681
724
  library_name=None,
682
725
  status=LibraryStatus.MISSING,
683
- problems=[
684
- "Library could not be found at the file path specified. It will be removed from the configuration."
685
- ],
726
+ problems=[LibraryNotFoundProblem(library_path=file_path)],
686
727
  )
687
728
  details = f"Attempted to load Library JSON file. Failed because no file could be found at the specified path: {json_path}"
688
729
  logger.error(details)
@@ -714,9 +755,7 @@ class LibraryManager:
714
755
  library_path=file_path,
715
756
  library_name=library_data.name,
716
757
  status=LibraryStatus.UNUSABLE,
717
- problems=[
718
- f"Library's version string '{library_data.metadata.library_version}' wasn't valid. Must be in major.minor.patch format."
719
- ],
758
+ problems=[InvalidVersionStringProblem(version_string=str(library_data.metadata.library_version))],
720
759
  )
721
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."
722
761
  logger.error(details)
@@ -742,7 +781,9 @@ class LibraryManager:
742
781
  library_version=library_version,
743
782
  status=LibraryStatus.UNUSABLE,
744
783
  problems=[
745
- 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
+ )
746
787
  ],
747
788
  )
748
789
  details = f"Attempted to load Library '{library_data.name}' from '{json_path}'. Failed to load Advanced Library module: {err}"
@@ -765,9 +806,7 @@ class LibraryManager:
765
806
  library_name=library_data.name,
766
807
  library_version=library_version,
767
808
  status=LibraryStatus.UNUSABLE,
768
- problems=[
769
- "Failed because a library with this name was already registered. Check the Settings to ensure duplicate libraries are not being loaded."
770
- ],
809
+ problems=[DuplicateLibraryProblem()],
771
810
  )
772
811
 
773
812
  details = f"Attempted to load Library JSON file from '{json_path}'. Failed because a Library '{library_data.name}' already exists. Error: {err}."
@@ -794,7 +833,7 @@ class LibraryManager:
794
833
  library_name=library_data.name,
795
834
  library_version=library_version,
796
835
  status=LibraryStatus.UNUSABLE,
797
- problems=[str(e)],
836
+ problems=[VenvCreationFailedProblem(error_message=str(e))],
798
837
  )
799
838
  details = f"Attempted to load Library JSON file from '{json_path}'. Failed when creating the virtual environment: {e}."
800
839
  logger.error(details)
@@ -812,9 +851,7 @@ class LibraryManager:
812
851
  library_name=library_data.name,
813
852
  library_version=library_version,
814
853
  status=LibraryStatus.UNUSABLE,
815
- problems=[
816
- f"Insufficient disk space for dependencies (requires {min_space_gb} GB): {error_msg}"
817
- ],
854
+ problems=[InsufficientDiskSpaceProblem(min_space_gb=min_space_gb, error_message=error_msg)],
818
855
  )
819
856
  return RegisterLibraryFromFileResultFailure(result_details=details)
820
857
 
@@ -854,7 +891,7 @@ class LibraryManager:
854
891
  library_name=library_data.name,
855
892
  library_version=library_version,
856
893
  status=LibraryStatus.UNUSABLE,
857
- problems=[f"Dependency installation failed: {error_details}"],
894
+ problems=[DependencyInstallationFailedProblem(error_details=error_details)],
858
895
  )
859
896
  details = f"Attempted to load Library JSON file from '{json_path}'. Failed when installing dependencies: {error_details}"
860
897
  logger.error(details)
@@ -878,7 +915,7 @@ class LibraryManager:
878
915
  )
879
916
  create_new_category_result = GriptapeNodes.handle_request(create_new_category_request)
880
917
  if not isinstance(create_new_category_result, SetConfigCategoryResultSuccess):
881
- problems.append(f"Failed to create new config category '{library_data_setting.category}'.")
918
+ problems.append(CreateConfigCategoryProblem(category_name=library_data_setting.category))
882
919
  details = f"Failed attempting to create new config category '{library_data_setting.category}' for library '{library_data.name}'."
883
920
  logger.error(details)
884
921
  continue # SKIP IT
@@ -892,7 +929,7 @@ class LibraryManager:
892
929
  )
893
930
  set_category_result = GriptapeNodes.handle_request(set_category_request)
894
931
  if not isinstance(set_category_result, SetConfigCategoryResultSuccess):
895
- problems.append(f"Failed to update config category '{library_data_setting.category}'.")
932
+ problems.append(UpdateConfigCategoryProblem(category_name=library_data_setting.category))
896
933
  details = f"Failed attempting to update config category '{library_data_setting.category}' for library '{library_data.name}'."
897
934
  logger.error(details)
898
935
  continue # SKIP IT
@@ -1394,6 +1431,21 @@ class LibraryManager:
1394
1431
  """
1395
1432
  return module_name.startswith("gtn_dynamic_module_")
1396
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
+
1397
1449
  def _load_module_from_file(self, file_path: Path | str, library_name: str) -> ModuleType:
1398
1450
  """Dynamically load a module from a Python file with support for hot reloading.
1399
1451
 
@@ -1532,13 +1584,44 @@ class LibraryManager:
1532
1584
  problems=failed_library.problems,
1533
1585
  )
1534
1586
 
1587
+ # Calculate total libraries for progress tracking
1588
+ total_libraries = len(metadata_result.successful_libraries)
1589
+
1535
1590
  # Use metadata results to selectively load libraries
1536
- for library_result in metadata_result.successful_libraries:
1591
+ for current_library_index, library_result in enumerate(metadata_result.successful_libraries, start=1):
1592
+ library_name = library_result.library_schema.name
1593
+
1594
+ # Emit loading event
1595
+ GriptapeNodes.EventManager().put_event(
1596
+ AppEvent(
1597
+ payload=EngineInitializationProgress(
1598
+ phase=InitializationPhase.LIBRARIES,
1599
+ item_name=library_name,
1600
+ status=InitializationStatus.LOADING,
1601
+ current=current_library_index,
1602
+ total=total_libraries,
1603
+ )
1604
+ )
1605
+ )
1606
+
1607
+ # Register the library
1537
1608
  if library_result.library_schema.name == LibraryManager.SANDBOX_LIBRARY_NAME:
1538
1609
  # Handle sandbox library - use the schema we already have
1539
1610
  await self._attempt_generate_sandbox_library_from_schema(
1540
1611
  library_schema=library_result.library_schema, sandbox_directory=library_result.file_path
1541
1612
  )
1613
+ # Emit success event for sandbox library
1614
+ GriptapeNodes.EventManager().put_event(
1615
+ AppEvent(
1616
+ payload=EngineInitializationProgress(
1617
+ phase=InitializationPhase.LIBRARIES,
1618
+ item_name=library_name,
1619
+ status=InitializationStatus.COMPLETE,
1620
+ current=current_library_index,
1621
+ total=total_libraries,
1622
+ )
1623
+ )
1624
+ )
1542
1625
  else:
1543
1626
  # Handle config-based library - register it directly using the file path
1544
1627
  register_request = RegisterLibraryFromFileRequest(
@@ -1549,6 +1632,37 @@ class LibraryManager:
1549
1632
  # Registration failed - the failure info is already recorded in _library_file_path_to_info
1550
1633
  # by register_library_from_file_request, so we just log it here for visibility
1551
1634
  logger.warning("Failed to register library from %s", library_result.file_path)
1635
+ # Emit failure event
1636
+ error_message = (
1637
+ register_result.result_details.result_details[0].message
1638
+ if isinstance(register_result.result_details, ResultDetails)
1639
+ else register_result.result_details
1640
+ )
1641
+ GriptapeNodes.EventManager().put_event(
1642
+ AppEvent(
1643
+ payload=EngineInitializationProgress(
1644
+ phase=InitializationPhase.LIBRARIES,
1645
+ item_name=library_name,
1646
+ status=InitializationStatus.FAILED,
1647
+ current=current_library_index,
1648
+ total=total_libraries,
1649
+ error=error_message,
1650
+ )
1651
+ )
1652
+ )
1653
+ else:
1654
+ # Emit success event
1655
+ GriptapeNodes.EventManager().put_event(
1656
+ AppEvent(
1657
+ payload=EngineInitializationProgress(
1658
+ phase=InitializationPhase.LIBRARIES,
1659
+ item_name=library_name,
1660
+ status=InitializationStatus.COMPLETE,
1661
+ current=current_library_index,
1662
+ total=total_libraries,
1663
+ )
1664
+ )
1665
+ )
1552
1666
 
1553
1667
  # Print 'em all pretty
1554
1668
  self.print_library_load_status()
@@ -1782,7 +1896,7 @@ class LibraryManager:
1782
1896
  base_dir: Path,
1783
1897
  library_file_path: str,
1784
1898
  library_version: str | None,
1785
- problems: list[str],
1899
+ problems: list[LibraryProblem],
1786
1900
  ) -> LibraryManager.LibraryInfo:
1787
1901
  any_nodes_loaded_successfully = False
1788
1902
 
@@ -1790,7 +1904,7 @@ class LibraryManager:
1790
1904
  version_issues = GriptapeNodes.VersionCompatibilityManager().check_library_version_compatibility(library_data)
1791
1905
  has_disqualifying_issues = False
1792
1906
  for issue in version_issues:
1793
- problems.append(issue.message)
1907
+ problems.append(issue.problem)
1794
1908
  if issue.severity == LibraryStatus.UNUSABLE:
1795
1909
  has_disqualifying_issues = True
1796
1910
 
@@ -1812,8 +1926,7 @@ class LibraryManager:
1812
1926
  details = f"Successfully called before_library_nodes_loaded callback for library '{library_data.name}'"
1813
1927
  logger.debug(details)
1814
1928
  except Exception as err:
1815
- problem = f"Error calling before_library_nodes_loaded callback: {err}"
1816
- problems.append(problem)
1929
+ problems.append(BeforeLibraryCallbackProblem(error_message=str(err)))
1817
1930
  details = (
1818
1931
  f"Failed to call before_library_nodes_loaded callback for library '{library_data.name}': {err}"
1819
1932
  )
@@ -1829,27 +1942,39 @@ class LibraryManager:
1829
1942
  try:
1830
1943
  # Dynamically load the module containing the node class
1831
1944
  node_class = self._load_class_from_file(node_file_path, node_definition.class_name, library_data.name)
1832
- except Exception as err:
1945
+ except ImportError as err:
1946
+ root_cause = self._get_root_cause_from_exception(err)
1833
1947
  problems.append(
1834
- 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
+ )
1835
1954
  )
1836
- 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}"
1837
1956
  logger.error(details)
1838
1957
  continue # SKIP IT
1839
-
1840
- try:
1841
- # Register the node type with the library
1842
- forensics_string = library.register_new_node_type(node_class, metadata=node_definition.metadata)
1843
- if forensics_string is not None:
1844
- problems.append(forensics_string)
1845
- except Exception as err:
1958
+ except AttributeError:
1959
+ problems.append(
1960
+ NodeClassNotFoundProblem(class_name=node_definition.class_name, file_path=str(node_file_path))
1961
+ )
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:
1846
1966
  problems.append(
1847
- f"Failed to register node '{node_definition.class_name}' from '{node_file_path}' with error: {err}"
1967
+ NodeClassNotBaseNodeProblem(class_name=node_definition.class_name, file_path=str(node_file_path))
1848
1968
  )
1849
- details = f"Attempted to load node '{node_definition.class_name}' from '{node_file_path}'. Failed because an exception occurred: {err}"
1969
+ details = f"Attempted to load node '{node_definition.class_name}' from '{node_file_path}'. Failed because class doesn't inherit from BaseNode"
1850
1970
  logger.error(details)
1851
1971
  continue # SKIP IT
1852
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
+
1853
1978
  # If we got here, at least one node came in.
1854
1979
  any_nodes_loaded_successfully = True
1855
1980
 
@@ -1860,8 +1985,7 @@ class LibraryManager:
1860
1985
  details = f"Successfully called after_library_nodes_loaded callback for library '{library_data.name}'"
1861
1986
  logger.debug(details)
1862
1987
  except Exception as err:
1863
- problem = f"Error calling after_library_nodes_loaded callback: {err}"
1864
- problems.append(problem)
1988
+ problems.append(AfterLibraryCallbackProblem(error_message=str(err)))
1865
1989
  details = f"Failed to call after_library_nodes_loaded callback for library '{library_data.name}': {err}"
1866
1990
  logger.error(details)
1867
1991
 
@@ -1900,7 +2024,15 @@ class LibraryManager:
1900
2024
  try:
1901
2025
  module = self._load_module_from_file(candidate_path, LibraryManager.SANDBOX_LIBRARY_NAME)
1902
2026
  except Exception as err:
1903
- 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
+ )
1904
2036
  details = f"Attempted to load module in sandbox library '{candidate_path}'. Failed because an exception occurred: {err}."
1905
2037
  logger.warning(details)
1906
2038
  continue # SKIP IT
@@ -1966,9 +2098,7 @@ class LibraryManager:
1966
2098
  library_name=library_data.name,
1967
2099
  library_version=library_data.metadata.library_version,
1968
2100
  status=LibraryStatus.UNUSABLE,
1969
- problems=[
1970
- "Failed because a library with this name was already registered. Check the Settings to ensure duplicate libraries are not being loaded."
1971
- ],
2101
+ problems=[DuplicateLibraryProblem()],
1972
2102
  )
1973
2103
 
1974
2104
  details = f"Attempted to load Library JSON file from '{sandbox_library_dir}'. Failed because a Library '{library_data.name}' already exists. Error: {err}."
@@ -2013,7 +2143,7 @@ class LibraryManager:
2013
2143
  ]
2014
2144
  config_mgr.set_config_value(config_category, libraries_to_register_category)
2015
2145
 
2016
- async def reload_all_libraries_request(self, request: ReloadAllLibrariesRequest) -> ResultPayload: # noqa: ARG002
2146
+ async def reload_libraries_request(self, request: ReloadAllLibrariesRequest) -> ResultPayload: # noqa: ARG002
2017
2147
  # Start with a clean slate.
2018
2148
  clear_all_request = ClearAllObjectStateRequest(i_know_what_im_doing=True)
2019
2149
  clear_all_result = await GriptapeNodes.ahandle_request(clear_all_request)
@@ -2046,6 +2176,25 @@ class LibraryManager:
2046
2176
  )
2047
2177
  return ReloadAllLibrariesResultSuccess(result_details=ResultDetails(message=details, level=logging.INFO))
2048
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
+
2049
2198
  def _discover_library_files(self) -> list[Path]:
2050
2199
  """Discover library JSON files from config and workspace recursively.
2051
2200