griptape-nodes 0.34.3__py3-none-any.whl → 0.35.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 (31) hide show
  1. griptape_nodes/__init__.py +56 -41
  2. griptape_nodes/app/app.py +3 -3
  3. griptape_nodes/bootstrap/bootstrap_script.py +5 -3
  4. griptape_nodes/drivers/__init__.py +1 -0
  5. griptape_nodes/drivers/storage/__init__.py +4 -0
  6. griptape_nodes/drivers/storage/base_storage_driver.py +38 -0
  7. griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +85 -0
  8. griptape_nodes/drivers/storage/local_storage_driver.py +49 -0
  9. griptape_nodes/exe_types/core_types.py +57 -1
  10. griptape_nodes/exe_types/flow.py +3 -1
  11. griptape_nodes/exe_types/node_types.py +34 -0
  12. griptape_nodes/machines/node_resolution.py +9 -1
  13. griptape_nodes/node_library/library_registry.py +2 -0
  14. griptape_nodes/retained_mode/events/execution_events.py +2 -0
  15. griptape_nodes/retained_mode/events/generate_request_payload_schemas.py +1 -1
  16. griptape_nodes/retained_mode/events/node_events.py +13 -16
  17. griptape_nodes/retained_mode/events/parameter_events.py +10 -3
  18. griptape_nodes/retained_mode/events/static_file_events.py +26 -0
  19. griptape_nodes/retained_mode/griptape_nodes.py +3 -1
  20. griptape_nodes/retained_mode/managers/context_manager.py +123 -87
  21. griptape_nodes/retained_mode/managers/flow_manager.py +126 -86
  22. griptape_nodes/retained_mode/managers/library_manager.py +279 -95
  23. griptape_nodes/retained_mode/managers/node_manager.py +230 -159
  24. griptape_nodes/retained_mode/managers/settings.py +6 -1
  25. griptape_nodes/retained_mode/managers/static_files_manager.py +77 -54
  26. griptape_nodes/retained_mode/managers/workflow_manager.py +51 -12
  27. {griptape_nodes-0.34.3.dist-info → griptape_nodes-0.35.0.dist-info}/METADATA +1 -1
  28. {griptape_nodes-0.34.3.dist-info → griptape_nodes-0.35.0.dist-info}/RECORD +31 -26
  29. {griptape_nodes-0.34.3.dist-info → griptape_nodes-0.35.0.dist-info}/WHEEL +0 -0
  30. {griptape_nodes-0.34.3.dist-info → griptape_nodes-0.35.0.dist-info}/entry_points.txt +0 -0
  31. {griptape_nodes-0.34.3.dist-info → griptape_nodes-0.35.0.dist-info}/licenses/LICENSE +0 -0
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import importlib.util
4
4
  import json
5
5
  import logging
6
+ import os
6
7
  import platform
7
8
  import subprocess
8
9
  import sys
@@ -24,9 +25,19 @@ from rich.text import Text
24
25
  from xdg_base_dirs import xdg_data_home
25
26
 
26
27
  from griptape_nodes.exe_types.node_types import BaseNode
27
- from griptape_nodes.node_library.library_registry import LibraryRegistry, LibrarySchema
28
+ from griptape_nodes.node_library.library_registry import (
29
+ CategoryDefinition,
30
+ Library,
31
+ LibraryMetadata,
32
+ LibraryRegistry,
33
+ LibrarySchema,
34
+ NodeDefinition,
35
+ NodeMetadata,
36
+ )
28
37
  from griptape_nodes.retained_mode.events.app_events import (
29
38
  AppInitializationComplete,
39
+ GetEngineVersionRequest,
40
+ GetEngineVersionResultSuccess,
30
41
  )
31
42
  from griptape_nodes.retained_mode.events.config_events import (
32
43
  GetConfigCategoryRequest,
@@ -69,6 +80,8 @@ from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
69
80
  from griptape_nodes.retained_mode.managers.os_manager import OSManager
70
81
 
71
82
  if TYPE_CHECKING:
83
+ from types import ModuleType
84
+
72
85
  from griptape_nodes.retained_mode.events.base_events import ResultPayload
73
86
  from griptape_nodes.retained_mode.managers.event_manager import EventManager
74
87
 
@@ -342,7 +355,7 @@ class LibraryManager:
342
355
 
343
356
  # Load the JSON
344
357
  try:
345
- with json_path.open("r") as f:
358
+ with json_path.open("r", encoding="utf-8") as f:
346
359
  library_json = json.load(f)
347
360
  except json.JSONDecodeError:
348
361
  self._library_file_path_to_info[file_path] = LibraryManager.LibraryInfo(
@@ -444,13 +457,11 @@ class LibraryManager:
444
457
  # Install node library dependencies
445
458
  try:
446
459
  site_packages = None
447
- if library_data.metadata.dependencies:
460
+ if library_data.metadata.dependencies and library_data.metadata.dependencies.pip_dependencies:
448
461
  pip_install_flags = library_data.metadata.dependencies.pip_install_flags
449
462
  if pip_install_flags is None:
450
463
  pip_install_flags = []
451
464
  pip_dependencies = library_data.metadata.dependencies.pip_dependencies
452
- if pip_dependencies is None:
453
- pip_dependencies = []
454
465
 
455
466
  # Create a virtual environment for the library
456
467
  python_version = platform.python_version()
@@ -553,77 +564,33 @@ class LibraryManager:
553
564
  continue # SKIP IT
554
565
 
555
566
  # Attempt to load nodes from the library.
556
- any_nodes_loaded_successesfully = False
557
- # Process each node in the metadata
558
- for node_definition in library_data.nodes:
559
- # Resolve relative path to absolute path
560
- node_file_path = Path(node_definition.file_path)
561
- if not node_file_path.is_absolute():
562
- node_file_path = base_dir / node_file_path
563
-
564
- try:
565
- # Dynamically load the module containing the node class
566
- node_class = self._load_class_from_file(node_file_path, node_definition.class_name)
567
- except Exception as err:
568
- problems.append(
569
- f"Failed to load node '{node_definition.class_name}' from '{node_file_path}' with error: {err}"
570
- )
571
- details = f"Attempted to load node '{node_definition.class_name}' from '{node_file_path}'. Failed because an exception occurred: {err}"
567
+ library_load_results = self._attempt_load_nodes_from_library(
568
+ library_data=library_data,
569
+ library=library,
570
+ base_dir=base_dir,
571
+ library_file_path=file_path,
572
+ library_version=library_version,
573
+ problems=problems,
574
+ )
575
+ self._library_file_path_to_info[file_path] = library_load_results
576
+
577
+ match library_load_results.status:
578
+ case LibraryManager.LibraryStatus.GOOD:
579
+ details = f"Successfully loaded Library '{library_data.name}' from JSON file at {json_path}"
580
+ logger.info(details)
581
+ return RegisterLibraryFromFileResultSuccess(library_name=library_data.name)
582
+ case LibraryManager.LibraryStatus.FLAWED:
583
+ details = f"Successfully loaded Library JSON file from '{json_path}', but one or more nodes failed to load. Check the log for more details."
584
+ logger.warning(details)
585
+ return RegisterLibraryFromFileResultSuccess(library_name=library_data.name)
586
+ case LibraryManager.LibraryStatus.UNUSABLE:
587
+ details = f"Attempted to load Library JSON file from '{json_path}'. Failed because no nodes were loaded. Check the log for more details."
572
588
  logger.error(details)
573
- continue # SKIP IT
574
-
575
- try:
576
- # Register the node type with the library
577
- forensics_string = library.register_new_node_type(node_class, metadata=node_definition.metadata)
578
- if forensics_string is not None:
579
- problems.append(forensics_string)
580
- except Exception as err:
581
- problems.append(
582
- f"Failed to register node '{node_definition.class_name}' from '{node_file_path}' with error: {err}"
583
- )
584
- details = f"Attempted to load node '{node_definition.class_name}' from '{node_file_path}'. Failed because an exception occurred: {err}"
589
+ return RegisterLibraryFromFileResultFailure()
590
+ case _:
591
+ details = f"Attempted to load Library JSON file from '{json_path}'. Failed because an unknown/unexpected status '{library_load_results.status}' was returned."
585
592
  logger.error(details)
586
- continue # SKIP IT
587
-
588
- # If we got here, at least one node came in.
589
- any_nodes_loaded_successesfully = True
590
-
591
- if not any_nodes_loaded_successesfully:
592
- self._library_file_path_to_info[file_path] = LibraryManager.LibraryInfo(
593
- library_path=file_path,
594
- library_name=library_data.name,
595
- library_version=library_version,
596
- status=LibraryManager.LibraryStatus.UNUSABLE,
597
- problems=problems,
598
- )
599
- details = f"Attempted to load Library JSON file from '{json_path}'. Failed because no nodes were loaded. Check the log for more details."
600
- logger.error(details)
601
- return RegisterLibraryFromFileResultFailure()
602
-
603
- # Successes, but errors.
604
- if problems:
605
- self._library_file_path_to_info[file_path] = LibraryManager.LibraryInfo(
606
- library_path=file_path,
607
- library_name=library_data.name,
608
- library_version=library_version,
609
- status=LibraryManager.LibraryStatus.FLAWED,
610
- problems=problems,
611
- )
612
- details = f"Successfully loaded Library JSON file from '{json_path}', but one or more nodes failed to load. Check the log for more details."
613
- logger.warning(details)
614
- else:
615
- # Flawless victory.
616
- self._library_file_path_to_info[file_path] = LibraryManager.LibraryInfo(
617
- library_path=file_path,
618
- library_name=library_data.name,
619
- library_version=library_version,
620
- status=LibraryManager.LibraryStatus.GOOD,
621
- problems=problems,
622
- )
623
- details = f"Successfully loaded Library '{library_data.name}' from JSON file at {json_path}"
624
- logger.info(details)
625
-
626
- return RegisterLibraryFromFileResultSuccess(library_name=library_data.name)
593
+ return RegisterLibraryFromFileResultFailure()
627
594
 
628
595
  def register_library_from_requirement_specifier_request(
629
596
  self, request: RegisterLibraryFromRequirementSpecifierRequest
@@ -779,20 +746,17 @@ class LibraryManager:
779
746
  )
780
747
  return result
781
748
 
782
- def _load_class_from_file(self, file_path: Path | str, class_name: str) -> type[BaseNode]:
783
- """Dynamically load a class from a Python file with support for hot reloading.
749
+ def _load_module_from_file(self, file_path: Path | str) -> ModuleType:
750
+ """Dynamically load a module from a Python file with support for hot reloading.
784
751
 
785
752
  Args:
786
753
  file_path: Path to the Python file
787
- class_name: Name of the class to load
788
754
 
789
755
  Returns:
790
- The loaded class
756
+ The loaded module
791
757
 
792
758
  Raises:
793
759
  ImportError: If the module cannot be imported
794
- AttributeError: If the class doesn't exist in the module
795
- TypeError: If the loaded class isn't a BaseNode-derived class
796
760
  """
797
761
  # Ensure file_path is a Path object
798
762
  file_path = Path(file_path)
@@ -846,9 +810,32 @@ class LibraryManager:
846
810
  try:
847
811
  spec.loader.exec_module(module)
848
812
  except Exception as err:
849
- msg = f"Class '{class_name}' from module '{file_path}' failed to load with error: {err}"
813
+ msg = f"Module at '{file_path}' failed to load with error: {err}"
850
814
  raise ImportError(msg) from err
851
815
 
816
+ return module
817
+
818
+ def _load_class_from_file(self, file_path: Path | str, class_name: str) -> type[BaseNode]:
819
+ """Dynamically load a class from a Python file with support for hot reloading.
820
+
821
+ Args:
822
+ file_path: Path to the Python file
823
+ class_name: Name of the class to load
824
+
825
+ Returns:
826
+ The loaded class
827
+
828
+ Raises:
829
+ ImportError: If the module cannot be imported
830
+ AttributeError: If the class doesn't exist in the module
831
+ TypeError: If the loaded class isn't a BaseNode-derived class
832
+ """
833
+ try:
834
+ module = self._load_module_from_file(file_path)
835
+ except ImportError as err:
836
+ msg = f"Attempted to load class '{class_name}'. Error: {err}"
837
+ raise ImportError(msg) from err
838
+
852
839
  # Get the class
853
840
  try:
854
841
  node_class = getattr(module, class_name)
@@ -867,7 +854,13 @@ class LibraryManager:
867
854
  user_libraries_section = "app_events.on_app_initialization_complete.libraries_to_register"
868
855
  self._load_libraries_from_config_category(config_category=user_libraries_section, load_as_default_library=False)
869
856
 
870
- # Remove any missing libraries
857
+ sandbox_library_section = "sandbox_library_directory"
858
+ self._attempt_generate_sandbox_library(config_category=sandbox_library_section)
859
+
860
+ # Print 'em all pretty
861
+ self.print_library_load_status()
862
+
863
+ # Remove any missing libraries AFTER we've printed them for the user.
871
864
  self._remove_missing_libraries_from_config(config_category=user_libraries_section)
872
865
 
873
866
  def on_app_initialization_complete(self, _payload: AppInitializationComplete) -> None:
@@ -909,25 +902,216 @@ class LibraryManager:
909
902
  # Go tell the Workflow Manager that it's turn is now.
910
903
  GriptapeNodes.WorkflowManager().on_libraries_initialization_complete()
911
904
 
905
+ def _attempt_load_nodes_from_library( # noqa: PLR0913
906
+ self,
907
+ library_data: LibrarySchema,
908
+ library: Library,
909
+ base_dir: Path,
910
+ library_file_path: str,
911
+ library_version: str | None,
912
+ problems: list[str],
913
+ ) -> LibraryManager.LibraryInfo:
914
+ any_nodes_loaded_successfully = False
915
+ # Process each node in the metadata
916
+ for node_definition in library_data.nodes:
917
+ # Resolve relative path to absolute path
918
+ node_file_path = Path(node_definition.file_path)
919
+ if not node_file_path.is_absolute():
920
+ node_file_path = base_dir / node_file_path
921
+
922
+ try:
923
+ # Dynamically load the module containing the node class
924
+ node_class = self._load_class_from_file(node_file_path, node_definition.class_name)
925
+ except Exception as err:
926
+ problems.append(
927
+ f"Failed to load node '{node_definition.class_name}' from '{node_file_path}' with error: {err}"
928
+ )
929
+ details = f"Attempted to load node '{node_definition.class_name}' from '{node_file_path}'. Failed because an exception occurred: {err}"
930
+ logger.error(details)
931
+ continue # SKIP IT
932
+
933
+ try:
934
+ # Register the node type with the library
935
+ forensics_string = library.register_new_node_type(node_class, metadata=node_definition.metadata)
936
+ if forensics_string is not None:
937
+ problems.append(forensics_string)
938
+ except Exception as err:
939
+ problems.append(
940
+ f"Failed to register node '{node_definition.class_name}' from '{node_file_path}' with error: {err}"
941
+ )
942
+ details = f"Attempted to load node '{node_definition.class_name}' from '{node_file_path}'. Failed because an exception occurred: {err}"
943
+ logger.error(details)
944
+ continue # SKIP IT
945
+
946
+ # If we got here, at least one node came in.
947
+ any_nodes_loaded_successfully = True
948
+
949
+ # Create a LibraryInfo object based on load successes and problem count.
950
+ if not any_nodes_loaded_successfully:
951
+ status = LibraryManager.LibraryStatus.UNUSABLE
952
+ elif problems:
953
+ # Success, but errors.
954
+ status = LibraryManager.LibraryStatus.FLAWED
955
+ else:
956
+ # Flawless victory.
957
+ status = LibraryManager.LibraryStatus.GOOD
958
+
959
+ # Create a LibraryInfo object based on load successes and problem count.
960
+ return LibraryManager.LibraryInfo(
961
+ library_path=library_file_path,
962
+ library_name=library_data.name,
963
+ library_version=library_version,
964
+ status=status,
965
+ problems=problems,
966
+ )
967
+
968
+ def _attempt_generate_sandbox_library(self, config_category: str) -> None:
969
+ config_mgr = GriptapeNodes.ConfigManager()
970
+ sandbox_library_subdir = config_mgr.get_config_value(config_category)
971
+ if not sandbox_library_subdir:
972
+ logger.debug("No sandbox directory specified in config at key '%s'. Skipping.", config_category)
973
+ return
974
+
975
+ # Prepend the workflow directory; if the sandbox dir starts with a slash, the workflow dir will be ignored.
976
+ sandbox_library_dir = config_mgr.workspace_path / sandbox_library_subdir
977
+ sandbox_library_dir_as_posix = sandbox_library_dir.as_posix()
978
+
979
+ sandbox_node_candidates = self._find_files_in_dir(directory=sandbox_library_dir, extension=".py")
980
+ if not sandbox_node_candidates:
981
+ logger.debug("No candidate files found in sandbox directory '%s'. Skipping.", sandbox_library_dir)
982
+ return
983
+
984
+ sandbox_category = CategoryDefinition(
985
+ title="Sandbox",
986
+ description="Nodes loaded from the Sandbox Library.",
987
+ color="#ff0000",
988
+ icon="Folder",
989
+ )
990
+
991
+ problems = []
992
+
993
+ # Trawl through the Python files and find those that are nodes.
994
+ node_definitions = []
995
+ for candidate in sandbox_node_candidates:
996
+ try:
997
+ module = self._load_module_from_file(candidate)
998
+ except Exception as err:
999
+ problems.append(f"Could not load module in sandbox library '{candidate}': {err}")
1000
+ details = f"Attempted to load module in sandbox library '{candidate}'. Failed because an exception occurred: {err}."
1001
+ logger.warning(details)
1002
+ continue # SKIP IT
1003
+
1004
+ # Peek inside for any BaseNodes.
1005
+ for class_name, obj in vars(module).items():
1006
+ if (
1007
+ isinstance(obj, type)
1008
+ and issubclass(obj, BaseNode)
1009
+ and type(obj) is not BaseNode
1010
+ and obj.__module__ == module.__name__
1011
+ ):
1012
+ details = f"Found node '{class_name}' in sandbox library '{candidate}'."
1013
+ logger.debug(details)
1014
+ node_metadata = NodeMetadata(
1015
+ category="Griptape Nodes Sandbox",
1016
+ description=f"'{class_name}' (loaded from the Sandbox Library).",
1017
+ display_name=class_name,
1018
+ )
1019
+ node_definition = NodeDefinition(
1020
+ class_name=class_name,
1021
+ file_path=str(candidate),
1022
+ metadata=node_metadata,
1023
+ )
1024
+ node_definitions.append(node_definition)
1025
+
1026
+ if not node_definitions:
1027
+ logger.info("No nodes found in sandbox library '%s'. Skipping.", sandbox_library_dir)
1028
+ return
1029
+
1030
+ # Create the library schema and metadata.
1031
+ engine_version = GriptapeNodes().handle_engine_version_request(request=GetEngineVersionRequest())
1032
+ if not isinstance(engine_version, GetEngineVersionResultSuccess):
1033
+ logger.error("Could not get engine version. Skipping sandbox library.")
1034
+ return
1035
+ engine_version_str = f"{engine_version.major}.{engine_version.minor}.{engine_version.patch}"
1036
+ library_metadata = LibraryMetadata(
1037
+ author="Author needs to be specified when library is published.",
1038
+ description="Nodes loaded from the sandbox library.",
1039
+ library_version=engine_version_str,
1040
+ engine_version=engine_version_str,
1041
+ tags=["sandbox"],
1042
+ is_griptape_nodes_searchable=False,
1043
+ )
1044
+ categories = [
1045
+ {"Griptape Nodes Sandbox": sandbox_category},
1046
+ ]
1047
+ library_data = LibrarySchema(
1048
+ name="Sandbox Library",
1049
+ library_schema_version=LibrarySchema.LATEST_SCHEMA_VERSION,
1050
+ metadata=library_metadata,
1051
+ categories=categories,
1052
+ nodes=node_definitions,
1053
+ )
1054
+
1055
+ # Register the library.
1056
+ # Create or get the library
1057
+ try:
1058
+ # Try to create a new library
1059
+ library = LibraryRegistry.generate_new_library(
1060
+ library_data=library_data,
1061
+ mark_as_default_library=True,
1062
+ )
1063
+
1064
+ except KeyError as err:
1065
+ # Library already exists
1066
+ self._library_file_path_to_info[sandbox_library_dir_as_posix] = LibraryManager.LibraryInfo(
1067
+ library_path=sandbox_library_dir_as_posix,
1068
+ library_name=library_data.name,
1069
+ library_version=engine_version_str,
1070
+ status=LibraryManager.LibraryStatus.UNUSABLE,
1071
+ problems=["Failed because a library with this name was already registered."],
1072
+ )
1073
+
1074
+ details = f"Attempted to load Library JSON file from '{sandbox_library_dir}'. Failed because a Library '{library_data.name}' already exists. Error: {err}."
1075
+ logger.error(details)
1076
+ return
1077
+
1078
+ # Attempt to load nodes from the library.
1079
+ library_load_results = self._attempt_load_nodes_from_library(
1080
+ library_data=library_data,
1081
+ library=library,
1082
+ base_dir=sandbox_library_dir_as_posix,
1083
+ library_file_path=sandbox_library_dir_as_posix,
1084
+ library_version=engine_version_str,
1085
+ problems=problems,
1086
+ )
1087
+ self._library_file_path_to_info[sandbox_library_dir_as_posix] = library_load_results
1088
+
1089
+ def _find_files_in_dir(self, directory: Path, extension: str) -> list[Path]:
1090
+ ret_val = []
1091
+ for root, _, files_found in os.walk(directory):
1092
+ for file in files_found:
1093
+ if file.endswith(extension):
1094
+ file_path = Path(root) / file
1095
+ ret_val.append(file_path)
1096
+ return ret_val
1097
+
912
1098
  def _load_libraries_from_config_category(self, config_category: str, *, load_as_default_library: bool) -> None:
913
1099
  config_mgr = GriptapeNodes.ConfigManager()
914
1100
  libraries_to_register_category: list[str] = config_mgr.get_config_value(config_category)
915
1101
 
916
1102
  if libraries_to_register_category is not None:
917
1103
  for library_to_register in libraries_to_register_category:
918
- if library_to_register.endswith(".json"):
919
- library_load_request = RegisterLibraryFromFileRequest(
920
- file_path=library_to_register,
921
- load_as_default_library=load_as_default_library,
922
- )
923
- else:
924
- library_load_request = RegisterLibraryFromRequirementSpecifierRequest(
925
- requirement_specifier=library_to_register
926
- )
927
- GriptapeNodes.handle_request(library_load_request)
928
-
929
- # Print 'em all pretty
930
- self.print_library_load_status()
1104
+ if library_to_register:
1105
+ if library_to_register.endswith(".json"):
1106
+ library_load_request = RegisterLibraryFromFileRequest(
1107
+ file_path=library_to_register,
1108
+ load_as_default_library=load_as_default_library,
1109
+ )
1110
+ else:
1111
+ library_load_request = RegisterLibraryFromRequirementSpecifierRequest(
1112
+ requirement_specifier=library_to_register
1113
+ )
1114
+ GriptapeNodes.handle_request(library_load_request)
931
1115
 
932
1116
  def _remove_missing_libraries_from_config(self, config_category: str) -> None:
933
1117
  # Now remove all libraries that were missing from the user's config.