griptape-nodes 0.52.1__py3-none-any.whl → 0.54.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 (71) hide show
  1. griptape_nodes/__init__.py +8 -942
  2. griptape_nodes/__main__.py +6 -0
  3. griptape_nodes/app/app.py +48 -86
  4. griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +35 -5
  5. griptape_nodes/bootstrap/workflow_executors/workflow_executor.py +15 -1
  6. griptape_nodes/cli/__init__.py +1 -0
  7. griptape_nodes/cli/commands/__init__.py +1 -0
  8. griptape_nodes/cli/commands/config.py +74 -0
  9. griptape_nodes/cli/commands/engine.py +80 -0
  10. griptape_nodes/cli/commands/init.py +550 -0
  11. griptape_nodes/cli/commands/libraries.py +96 -0
  12. griptape_nodes/cli/commands/models.py +504 -0
  13. griptape_nodes/cli/commands/self.py +120 -0
  14. griptape_nodes/cli/main.py +56 -0
  15. griptape_nodes/cli/shared.py +75 -0
  16. griptape_nodes/common/__init__.py +1 -0
  17. griptape_nodes/common/directed_graph.py +71 -0
  18. griptape_nodes/drivers/storage/base_storage_driver.py +40 -20
  19. griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +24 -29
  20. griptape_nodes/drivers/storage/local_storage_driver.py +23 -14
  21. griptape_nodes/exe_types/core_types.py +60 -2
  22. griptape_nodes/exe_types/node_types.py +257 -38
  23. griptape_nodes/exe_types/param_components/__init__.py +1 -0
  24. griptape_nodes/exe_types/param_components/execution_status_component.py +138 -0
  25. griptape_nodes/machines/control_flow.py +195 -94
  26. griptape_nodes/machines/dag_builder.py +207 -0
  27. griptape_nodes/machines/fsm.py +10 -1
  28. griptape_nodes/machines/parallel_resolution.py +558 -0
  29. griptape_nodes/machines/{node_resolution.py → sequential_resolution.py} +30 -57
  30. griptape_nodes/node_library/library_registry.py +34 -1
  31. griptape_nodes/retained_mode/events/app_events.py +5 -1
  32. griptape_nodes/retained_mode/events/base_events.py +9 -9
  33. griptape_nodes/retained_mode/events/config_events.py +30 -0
  34. griptape_nodes/retained_mode/events/execution_events.py +2 -2
  35. griptape_nodes/retained_mode/events/model_events.py +296 -0
  36. griptape_nodes/retained_mode/events/node_events.py +4 -3
  37. griptape_nodes/retained_mode/griptape_nodes.py +34 -12
  38. griptape_nodes/retained_mode/managers/agent_manager.py +23 -5
  39. griptape_nodes/retained_mode/managers/arbitrary_code_exec_manager.py +3 -1
  40. griptape_nodes/retained_mode/managers/config_manager.py +44 -3
  41. griptape_nodes/retained_mode/managers/context_manager.py +6 -5
  42. griptape_nodes/retained_mode/managers/event_manager.py +8 -2
  43. griptape_nodes/retained_mode/managers/flow_manager.py +150 -206
  44. griptape_nodes/retained_mode/managers/library_lifecycle/library_directory.py +1 -1
  45. griptape_nodes/retained_mode/managers/library_manager.py +35 -25
  46. griptape_nodes/retained_mode/managers/model_manager.py +1107 -0
  47. griptape_nodes/retained_mode/managers/node_manager.py +102 -220
  48. griptape_nodes/retained_mode/managers/object_manager.py +11 -5
  49. griptape_nodes/retained_mode/managers/os_manager.py +28 -13
  50. griptape_nodes/retained_mode/managers/secrets_manager.py +8 -4
  51. griptape_nodes/retained_mode/managers/settings.py +116 -7
  52. griptape_nodes/retained_mode/managers/static_files_manager.py +85 -12
  53. griptape_nodes/retained_mode/managers/sync_manager.py +17 -9
  54. griptape_nodes/retained_mode/managers/workflow_manager.py +186 -192
  55. griptape_nodes/retained_mode/retained_mode.py +19 -0
  56. griptape_nodes/servers/__init__.py +1 -0
  57. griptape_nodes/{mcp_server/server.py → servers/mcp.py} +1 -1
  58. griptape_nodes/{app/api.py → servers/static.py} +43 -40
  59. griptape_nodes/traits/add_param_button.py +1 -1
  60. griptape_nodes/traits/button.py +334 -6
  61. griptape_nodes/traits/color_picker.py +66 -0
  62. griptape_nodes/traits/multi_options.py +188 -0
  63. griptape_nodes/traits/numbers_selector.py +77 -0
  64. griptape_nodes/traits/options.py +93 -2
  65. griptape_nodes/traits/traits.json +4 -0
  66. griptape_nodes/utils/async_utils.py +31 -0
  67. {griptape_nodes-0.52.1.dist-info → griptape_nodes-0.54.0.dist-info}/METADATA +4 -1
  68. {griptape_nodes-0.52.1.dist-info → griptape_nodes-0.54.0.dist-info}/RECORD +71 -48
  69. {griptape_nodes-0.52.1.dist-info → griptape_nodes-0.54.0.dist-info}/WHEEL +1 -1
  70. /griptape_nodes/{mcp_server → servers}/ws_request_manager.py +0 -0
  71. {griptape_nodes-0.52.1.dist-info → griptape_nodes-0.54.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,75 @@
1
+ """Shared constants and managers for CLI commands."""
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from rich.console import Console
9
+ from xdg_base_dirs import xdg_config_home, xdg_data_home
10
+
11
+
12
+ @dataclass
13
+ class InitConfig:
14
+ """Configuration for initialization."""
15
+
16
+ interactive: bool = True
17
+ workspace_directory: str | None = None
18
+ api_key: str | None = None
19
+ storage_backend: str | None = None
20
+ register_advanced_library: bool | None = None
21
+ config_values: dict[str, Any] | None = None
22
+ secret_values: dict[str, str] | None = None
23
+ libraries_sync: bool | None = None
24
+ bucket_name: str | None = None
25
+
26
+
27
+ # Initialize console
28
+ console = Console()
29
+
30
+ # Directory paths
31
+ CONFIG_DIR = xdg_config_home() / "griptape_nodes"
32
+ DATA_DIR = xdg_data_home() / "griptape_nodes"
33
+ ENV_FILE = CONFIG_DIR / ".env"
34
+ CONFIG_FILE = CONFIG_DIR / "griptape_nodes_config.json"
35
+
36
+ # URLs and constants
37
+ LATEST_TAG = "latest"
38
+ PACKAGE_NAME = "griptape-nodes"
39
+ NODES_APP_URL = "https://nodes.griptape.ai"
40
+ NODES_TARBALL_URL = "https://github.com/griptape-ai/griptape-nodes/archive/refs/tags/{tag}.tar.gz"
41
+ PYPI_UPDATE_URL = "https://pypi.org/pypi/{package}/json"
42
+ GITHUB_UPDATE_URL = "https://api.github.com/repos/griptape-ai/{package}/git/refs/tags/{revision}"
43
+ GT_CLOUD_BASE_URL = os.getenv("GT_CLOUD_BASE_URL", "https://cloud.griptape.ai")
44
+
45
+ # Environment variable defaults for init configuration
46
+ ENV_WORKSPACE_DIRECTORY = os.getenv("GTN_WORKSPACE_DIRECTORY")
47
+ ENV_API_KEY = os.getenv("GTN_API_KEY")
48
+ ENV_STORAGE_BACKEND = os.getenv("GTN_STORAGE_BACKEND")
49
+ ENV_REGISTER_ADVANCED_LIBRARY = (
50
+ os.getenv("GTN_REGISTER_ADVANCED_LIBRARY", "false").lower() == "true"
51
+ if os.getenv("GTN_REGISTER_ADVANCED_LIBRARY") is not None
52
+ else None
53
+ )
54
+ ENV_LIBRARIES_SYNC = (
55
+ os.getenv("GTN_LIBRARIES_SYNC", "false").lower() == "true" if os.getenv("GTN_LIBRARIES_SYNC") is not None else None
56
+ )
57
+ ENV_GTN_BUCKET_NAME = os.getenv("GTN_BUCKET_NAME")
58
+ ENV_LIBRARIES_BASE_DIR = os.getenv("GTN_LIBRARIES_BASE_DIR", str(DATA_DIR / "libraries"))
59
+
60
+
61
+ def init_system_config() -> None:
62
+ """Initializes the system config directory if it doesn't exist."""
63
+ if not CONFIG_DIR.exists():
64
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
65
+
66
+ files_to_create = [
67
+ (ENV_FILE, ""),
68
+ (CONFIG_FILE, "{}"),
69
+ ]
70
+
71
+ for file_name in files_to_create:
72
+ file_path = CONFIG_DIR / file_name[0]
73
+ if not file_path.exists():
74
+ with Path.open(file_path, "w", encoding="utf-8") as file:
75
+ file.write(file_name[1])
@@ -0,0 +1 @@
1
+ """Common package."""
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+
5
+ logger = logging.getLogger("griptape_nodes")
6
+
7
+
8
+ class DirectedGraph:
9
+ """Directed graph implementation using Python's graphlib for DAG operations."""
10
+
11
+ def __init__(self) -> None:
12
+ self._nodes: set[str] = set()
13
+ self._predecessors: dict[str, set[str]] = {}
14
+
15
+ def __len__(self) -> int:
16
+ """Return the number of nodes in the graph."""
17
+ return len(self._nodes)
18
+
19
+ def add_node(self, node_for_adding: str) -> None:
20
+ """Add a node to the graph."""
21
+ self._nodes.add(node_for_adding)
22
+ if node_for_adding not in self._predecessors:
23
+ self._predecessors[node_for_adding] = set()
24
+
25
+ def add_edge(self, from_node: str, to_node: str) -> None:
26
+ """Add a directed edge from from_node to to_node."""
27
+ self.add_node(from_node)
28
+ self.add_node(to_node)
29
+ self._predecessors[to_node].add(from_node)
30
+
31
+ def nodes(self) -> set[str]:
32
+ """Return all nodes in the graph."""
33
+ return self._nodes.copy()
34
+
35
+ def in_degree(self, node: str) -> int:
36
+ """Return the in-degree of a node (number of incoming edges)."""
37
+ if node not in self._nodes:
38
+ msg = f"Node {node} not found in graph"
39
+ raise KeyError(msg)
40
+ return len(self._predecessors.get(node, set()))
41
+
42
+ def out_degree(self, node: str) -> int:
43
+ """Return the out-degree of a node (number of outgoing edges)."""
44
+ if node not in self._nodes:
45
+ msg = f"Node {node} not found in graph"
46
+ raise KeyError(msg)
47
+ count = 0
48
+ for predecessors in self._predecessors.values():
49
+ if node in predecessors:
50
+ count += 1
51
+ return count
52
+
53
+ def remove_node(self, node: str) -> None:
54
+ """Remove a node and all its edges from the graph."""
55
+ if node not in self._nodes:
56
+ return
57
+
58
+ self._nodes.remove(node)
59
+
60
+ # Remove this node from all predecessor lists
61
+ for predecessors in self._predecessors.values():
62
+ predecessors.discard(node)
63
+
64
+ # Remove this node's predecessor entry
65
+ if node in self._predecessors:
66
+ del self._predecessors[node]
67
+
68
+ def clear(self) -> None:
69
+ """Clear all nodes and edges from the graph."""
70
+ self._nodes.clear()
71
+ self._predecessors.clear()
@@ -1,5 +1,6 @@
1
1
  import logging
2
2
  from abc import ABC, abstractmethod
3
+ from pathlib import Path
3
4
  from typing import TypedDict
4
5
 
5
6
  import httpx
@@ -18,12 +19,31 @@ class CreateSignedUploadUrlResponse(TypedDict):
18
19
  class BaseStorageDriver(ABC):
19
20
  """Base class for storage drivers."""
20
21
 
22
+ def __init__(self, workspace_directory: Path) -> None:
23
+ """Initialize the storage driver with a workspace directory.
24
+
25
+ Args:
26
+ workspace_directory: The base workspace directory path.
27
+ """
28
+ self.workspace_directory = workspace_directory
29
+
30
+ def _get_full_path(self, path: Path) -> Path:
31
+ """Get the full path by joining workspace directory with the given path.
32
+
33
+ Args:
34
+ path: The relative path to join with workspace directory.
35
+
36
+ Returns:
37
+ The full path as workspace_directory / path.
38
+ """
39
+ return self.workspace_directory / path
40
+
21
41
  @abstractmethod
22
- def create_signed_upload_url(self, file_name: str) -> CreateSignedUploadUrlResponse:
23
- """Create a signed upload URL for the given file name.
42
+ def create_signed_upload_url(self, path: Path) -> CreateSignedUploadUrlResponse:
43
+ """Create a signed upload URL for the given path.
24
44
 
25
45
  Args:
26
- file_name: The name of the file to create a signed URL for.
46
+ path: The path of the file to create a signed URL for.
27
47
 
28
48
  Returns:
29
49
  CreateSignedUploadUrlResponse: A dictionary containing the signed URL, headers, and operation type.
@@ -31,11 +51,11 @@ class BaseStorageDriver(ABC):
31
51
  ...
32
52
 
33
53
  @abstractmethod
34
- def create_signed_download_url(self, file_name: str) -> str:
35
- """Create a signed download URL for the given file name.
54
+ def create_signed_download_url(self, path: Path) -> str:
55
+ """Create a signed download URL for the given path.
36
56
 
37
57
  Args:
38
- file_name: The name of the file to create a signed URL for.
58
+ path: The path of the file to create a signed URL for.
39
59
 
40
60
  Returns:
41
61
  str: The signed URL for downloading the file.
@@ -43,11 +63,11 @@ class BaseStorageDriver(ABC):
43
63
  ...
44
64
 
45
65
  @abstractmethod
46
- def delete_file(self, file_name: str) -> None:
66
+ def delete_file(self, path: Path) -> None:
47
67
  """Delete a file from storage.
48
68
 
49
69
  Args:
50
- file_name: The name of the file to delete.
70
+ path: The path of the file to delete.
51
71
  """
52
72
  ...
53
73
 
@@ -60,11 +80,11 @@ class BaseStorageDriver(ABC):
60
80
  """
61
81
  ...
62
82
 
63
- def upload_file(self, file_name: str, file_content: bytes) -> str:
83
+ def upload_file(self, path: Path, file_content: bytes) -> str:
64
84
  """Upload a file to storage.
65
85
 
66
86
  Args:
67
- file_name: The name of the file to upload.
87
+ path: The path of the file to upload.
68
88
  file_content: The file content as bytes.
69
89
 
70
90
  Returns:
@@ -75,7 +95,7 @@ class BaseStorageDriver(ABC):
75
95
  """
76
96
  try:
77
97
  # Get signed upload URL
78
- upload_response = self.create_signed_upload_url(file_name)
98
+ upload_response = self.create_signed_upload_url(path)
79
99
 
80
100
  # Upload the file using the signed URL
81
101
  response = httpx.request(
@@ -87,21 +107,21 @@ class BaseStorageDriver(ABC):
87
107
  response.raise_for_status()
88
108
 
89
109
  # Return the download URL
90
- return self.create_signed_download_url(file_name)
110
+ return self.create_signed_download_url(path)
91
111
  except httpx.HTTPStatusError as e:
92
- msg = f"Failed to upload file {file_name}: {e}"
112
+ msg = f"Failed to upload file {path}: {e}"
93
113
  logger.error(msg)
94
114
  raise RuntimeError(msg) from e
95
115
  except Exception as e:
96
- msg = f"Unexpected error uploading file {file_name}: {e}"
116
+ msg = f"Unexpected error uploading file {path}: {e}"
97
117
  logger.error(msg)
98
118
  raise RuntimeError(msg) from e
99
119
 
100
- def download_file(self, file_name: str) -> bytes:
101
- """Download a file from the bucket.
120
+ def download_file(self, path: Path) -> bytes:
121
+ """Download a file from storage.
102
122
 
103
123
  Args:
104
- file_name: The name of the file to download.
124
+ path: The path of the file to download.
105
125
 
106
126
  Returns:
107
127
  The file content as bytes.
@@ -111,17 +131,17 @@ class BaseStorageDriver(ABC):
111
131
  """
112
132
  try:
113
133
  # Get signed download URL
114
- download_url = self.create_signed_download_url(file_name)
134
+ download_url = self.create_signed_download_url(path)
115
135
 
116
136
  # Download the file
117
137
  response = httpx.get(download_url)
118
138
  response.raise_for_status()
119
139
  except httpx.HTTPStatusError as e:
120
- msg = f"Failed to download file {file_name}: {e}"
140
+ msg = f"Failed to download file {path}: {e}"
121
141
  logger.error(msg)
122
142
  raise RuntimeError(msg) from e
123
143
  except Exception as e:
124
- msg = f"Unexpected error downloading file {file_name}: {e}"
144
+ msg = f"Unexpected error downloading file {path}: {e}"
125
145
  logger.error(msg)
126
146
  raise RuntimeError(msg) from e
127
147
  else:
@@ -1,5 +1,6 @@
1
1
  import logging
2
2
  import os
3
+ from pathlib import Path
3
4
  from urllib.parse import urljoin
4
5
 
5
6
  import httpx
@@ -14,52 +15,46 @@ class GriptapeCloudStorageDriver(BaseStorageDriver):
14
15
 
15
16
  def __init__(
16
17
  self,
18
+ workspace_directory: Path,
17
19
  *,
18
20
  bucket_id: str,
19
- base_url: str | None = None,
20
21
  api_key: str | None = None,
21
- headers: dict | None = None,
22
22
  static_files_directory: str | None = None,
23
+ **kwargs,
23
24
  ) -> None:
24
25
  """Initialize the GriptapeCloudStorageDriver.
25
26
 
26
27
  Args:
28
+ workspace_directory: The base workspace directory path.
27
29
  bucket_id: The ID of the bucket to use. Required.
28
- base_url: The base URL for the Griptape Cloud API. If not provided, it will be retrieved from the environment variable "GT_CLOUD_BASE_URL" or default to "https://cloud.griptape.ai".
29
30
  api_key: The API key for authentication. If not provided, it will be retrieved from the environment variable "GT_CLOUD_API_KEY".
30
- headers: Additional headers to include in the requests. If not provided, the default headers will be used.
31
31
  static_files_directory: The directory path prefix for static files. If provided, file names will be prefixed with this path.
32
+ **kwargs: Additional keyword arguments including base_url and headers.
32
33
  """
33
- self.base_url = (
34
- base_url if base_url is not None else os.environ.get("GT_CLOUD_BASE_URL", "https://cloud.griptape.ai")
35
- )
34
+ super().__init__(workspace_directory)
35
+
36
+ self.base_url = kwargs.get("base_url") or os.environ.get("GT_CLOUD_BASE_URL", "https://cloud.griptape.ai")
36
37
  self.api_key = api_key if api_key is not None else os.environ.get("GT_CLOUD_API_KEY")
37
- self.headers = (
38
- headers
39
- if headers is not None
40
- else {
41
- "Authorization": f"Bearer {self.api_key}",
42
- }
43
- )
38
+ self.headers = kwargs.get("headers") or {"Authorization": f"Bearer {self.api_key}"}
44
39
 
45
40
  self.bucket_id = bucket_id
46
41
  self.static_files_directory = static_files_directory
47
42
 
48
- def _get_full_file_path(self, file_name: str) -> str:
49
- """Get the full file path including the static files directory prefix.
43
+ def _get_full_file_path(self, path: Path) -> str:
44
+ """Get the full file path including workspace directory and static files directory prefix.
50
45
 
51
46
  Args:
52
- file_name: The base file name.
47
+ path: The relative path from the workspace directory.
53
48
 
54
49
  Returns:
55
50
  The full file path with static files directory prefix if configured.
56
51
  """
57
52
  if self.static_files_directory:
58
- return f"{self.static_files_directory}/{file_name}"
59
- return file_name
53
+ return f"{self.static_files_directory}/{path}"
54
+ return str(path)
60
55
 
61
- def create_signed_upload_url(self, file_name: str) -> CreateSignedUploadUrlResponse:
62
- full_file_path = self._get_full_file_path(file_name)
56
+ def create_signed_upload_url(self, path: Path) -> CreateSignedUploadUrlResponse:
57
+ full_file_path = self._get_full_file_path(path)
63
58
  self._create_asset(full_file_path)
64
59
 
65
60
  url = urljoin(self.base_url, f"/api/buckets/{self.bucket_id}/asset-urls/{full_file_path}")
@@ -67,7 +62,7 @@ class GriptapeCloudStorageDriver(BaseStorageDriver):
67
62
  response = httpx.post(url, json={"operation": "PUT"}, headers=self.headers)
68
63
  response.raise_for_status()
69
64
  except httpx.HTTPStatusError as e:
70
- msg = f"Failed to create presigned URL for file {file_name}: {e}"
65
+ msg = f"Failed to create presigned URL for file {path}: {e}"
71
66
  logger.error(msg)
72
67
  raise RuntimeError(msg) from e
73
68
 
@@ -75,14 +70,14 @@ class GriptapeCloudStorageDriver(BaseStorageDriver):
75
70
 
76
71
  return {"url": response_data["url"], "headers": response_data.get("headers", {}), "method": "PUT"}
77
72
 
78
- def create_signed_download_url(self, file_name: str) -> str:
79
- full_file_path = self._get_full_file_path(file_name)
73
+ def create_signed_download_url(self, path: Path) -> str:
74
+ full_file_path = self._get_full_file_path(path)
80
75
  url = urljoin(self.base_url, f"/api/buckets/{self.bucket_id}/asset-urls/{full_file_path}")
81
76
  try:
82
77
  response = httpx.post(url, json={"method": "GET"}, headers=self.headers)
83
78
  response.raise_for_status()
84
79
  except httpx.HTTPStatusError as e:
85
- msg = f"Failed to create presigned URL for file {file_name}: {e}"
80
+ msg = f"Failed to create presigned URL for file {path}: {e}"
86
81
  logger.error(msg)
87
82
  raise RuntimeError(msg) from e
88
83
 
@@ -190,19 +185,19 @@ class GriptapeCloudStorageDriver(BaseStorageDriver):
190
185
 
191
186
  return response.json().get("buckets", [])
192
187
 
193
- def delete_file(self, file_name: str) -> None:
188
+ def delete_file(self, path: Path) -> None:
194
189
  """Delete a file from the bucket.
195
190
 
196
191
  Args:
197
- file_name: The name of the file to delete.
192
+ path: The path of the file to delete.
198
193
  """
199
- full_file_path = self._get_full_file_path(file_name)
194
+ full_file_path = self._get_full_file_path(path)
200
195
  url = urljoin(self.base_url, f"/api/buckets/{self.bucket_id}/assets/{full_file_path}")
201
196
 
202
197
  try:
203
198
  response = httpx.delete(url, headers=self.headers)
204
199
  response.raise_for_status()
205
200
  except httpx.HTTPStatusError as e:
206
- msg = f"Failed to delete file {file_name}: {e}"
201
+ msg = f"Failed to delete file {path}: {e}"
207
202
  logger.error(msg)
208
203
  raise RuntimeError(msg) from e
@@ -1,11 +1,10 @@
1
1
  import logging
2
2
  import time
3
+ from pathlib import Path
3
4
  from urllib.parse import urljoin
4
5
 
5
6
  import httpx
6
7
 
7
- from griptape_nodes.app.api import STATIC_SERVER_HOST, STATIC_SERVER_PORT, STATIC_SERVER_URL
8
- from griptape_nodes.app.app import STATIC_SERVER_ENABLED
9
8
  from griptape_nodes.drivers.storage.base_storage_driver import BaseStorageDriver, CreateSignedUploadUrlResponse
10
9
 
11
10
  logger = logging.getLogger("griptape_nodes")
@@ -14,12 +13,22 @@ logger = logging.getLogger("griptape_nodes")
14
13
  class LocalStorageDriver(BaseStorageDriver):
15
14
  """Stores files using the engine's local static server."""
16
15
 
17
- def __init__(self, base_url: str | None = None) -> None:
16
+ def __init__(self, workspace_directory: Path, base_url: str | None = None) -> None:
18
17
  """Initialize the LocalStorageDriver.
19
18
 
20
19
  Args:
20
+ workspace_directory: The base workspace directory path.
21
21
  base_url: The base URL for the static file server. If not provided, it will be constructed
22
22
  """
23
+ super().__init__(workspace_directory)
24
+
25
+ from griptape_nodes.servers.static import (
26
+ STATIC_SERVER_ENABLED,
27
+ STATIC_SERVER_HOST,
28
+ STATIC_SERVER_PORT,
29
+ STATIC_SERVER_URL,
30
+ )
31
+
23
32
  if not STATIC_SERVER_ENABLED:
24
33
  msg = "Static server is not enabled. Please set STATIC_SERVER_ENABLED to True."
25
34
  raise ValueError(msg)
@@ -28,46 +37,46 @@ class LocalStorageDriver(BaseStorageDriver):
28
37
  else:
29
38
  self.base_url = base_url
30
39
 
31
- def create_signed_upload_url(self, file_name: str) -> CreateSignedUploadUrlResponse:
40
+ def create_signed_upload_url(self, path: Path) -> CreateSignedUploadUrlResponse:
32
41
  static_url = urljoin(self.base_url, "/static-upload-urls")
33
42
  try:
34
- response = httpx.post(static_url, json={"file_name": file_name})
43
+ response = httpx.post(static_url, json={"file_path": str(path)})
35
44
  response.raise_for_status()
36
45
  except httpx.HTTPStatusError as e:
37
- msg = f"Failed to create presigned URL for file {file_name}: {e}"
46
+ msg = f"Failed to create presigned URL for file {path}: {e}"
38
47
  logger.error(msg)
39
48
  raise RuntimeError(msg) from e
40
49
 
41
50
  response_data = response.json()
42
51
  url = response_data.get("url")
43
52
  if url is None:
44
- msg = f"Failed to create presigned URL for file {file_name}: {response_data}"
53
+ msg = f"Failed to create presigned URL for file {path}: {response_data}"
45
54
  logger.error(msg)
46
55
  raise ValueError(msg)
47
56
 
48
57
  return {"url": url, "headers": response_data.get("headers", {}), "method": "PUT"}
49
58
 
50
- def create_signed_download_url(self, file_name: str) -> str:
51
- # The base_url already includes the /static path, so just append the filename
52
- url = f"{self.base_url}/{file_name}"
59
+ def create_signed_download_url(self, path: Path) -> str:
60
+ # The base_url already includes the /static path, so just append the path
61
+ url = f"{self.base_url}/{path}"
53
62
  # Add a cache-busting query parameter to the URL so that the browser always reloads the file
54
63
  cache_busted_url = f"{url}?t={int(time.time())}"
55
64
  return cache_busted_url
56
65
 
57
- def delete_file(self, file_name: str) -> None:
66
+ def delete_file(self, path: Path) -> None:
58
67
  """Delete a file from local storage.
59
68
 
60
69
  Args:
61
- file_name: The name of the file to delete.
70
+ path: The path of the file to delete.
62
71
  """
63
72
  # Use the static server's delete endpoint
64
- delete_url = urljoin(self.base_url, f"/static-files/{file_name}")
73
+ delete_url = urljoin(self.base_url, f"/static-files/{path}")
65
74
 
66
75
  try:
67
76
  response = httpx.delete(delete_url)
68
77
  response.raise_for_status()
69
78
  except httpx.HTTPStatusError as e:
70
- msg = f"Failed to delete file {file_name}: {e}"
79
+ msg = f"Failed to delete file {path}: {e}"
71
80
  logger.error(msg)
72
81
  raise RuntimeError(msg) from e
73
82
 
@@ -4,15 +4,48 @@ import uuid
4
4
  from abc import ABC, abstractmethod
5
5
  from copy import deepcopy
6
6
  from dataclasses import dataclass, field
7
- from enum import Enum, auto
7
+ from enum import Enum, StrEnum, auto
8
8
  from typing import TYPE_CHECKING, Any, ClassVar, Literal, NamedTuple, Self, TypeVar
9
9
 
10
+ from pydantic import BaseModel
11
+
12
+
13
+ class NodeMessagePayload(BaseModel):
14
+ """Structured payload for node messages.
15
+
16
+ This replaces the use of Any in message payloads, providing
17
+ better type safety and validation for node message handling.
18
+ """
19
+
20
+ data: Any = None
21
+
22
+
23
+ class NodeMessageResult(BaseModel):
24
+ """Result from a node message callback.
25
+
26
+ Attributes:
27
+ success: True if the message was handled successfully, False otherwise
28
+ details: Human-readable description of what happened
29
+ response: Optional response data to return to the sender
30
+ altered_workflow_state: True if the message handling altered workflow state.
31
+ Clients can use this to determine if the workflow needs to be re-saved.
32
+ """
33
+
34
+ success: bool
35
+ details: str
36
+ response: NodeMessagePayload | None = None
37
+ altered_workflow_state: bool = True
38
+
39
+
10
40
  if TYPE_CHECKING:
11
41
  from collections.abc import Callable
12
42
  from types import TracebackType
13
43
 
14
44
  from griptape_nodes.exe_types.node_types import BaseNode
15
45
 
46
+ # Type alias for element message callback functions
47
+ type ElementMessageCallback = Callable[[str, "NodeMessagePayload | None"], "NodeMessageResult"]
48
+
16
49
  T = TypeVar("T", bound="Parameter")
17
50
  N = TypeVar("N", bound="BaseNodeElement")
18
51
 
@@ -24,7 +57,7 @@ class ParameterMode(Enum):
24
57
  PROPERTY = auto()
25
58
 
26
59
 
27
- class ParameterTypeBuiltin(Enum):
60
+ class ParameterTypeBuiltin(StrEnum):
28
61
  STR = "str"
29
62
  BOOL = "bool"
30
63
  INT = "int"
@@ -416,6 +449,31 @@ class BaseNodeElement:
416
449
  }
417
450
  return event_data
418
451
 
452
+ def on_message_received(self, message_type: str, message: NodeMessagePayload | None) -> NodeMessageResult | None:
453
+ """Virtual method for handling messages sent to this element.
454
+
455
+ Attempts to delegate to child elements first. If any child handles the message
456
+ (returns non-None), that result is returned immediately. Otherwise, falls back
457
+ to default behavior (return None).
458
+
459
+ Args:
460
+ message_type: String indicating the message type for parsing
461
+ message: Message payload as NodeMessagePayload or None
462
+
463
+ Returns:
464
+ NodeMessageResult | None: Result if handled, None if no handler available
465
+ """
466
+ # Try to delegate to all children first
467
+ # NOTE: This returns immediately on the first child that accepts the message (returns non-None).
468
+ # In the future, we may need to expand this to handle multiple children processing the same message.
469
+ for child in self._children:
470
+ result = child.on_message_received(message_type, message)
471
+ if result is not None:
472
+ return result
473
+
474
+ # No child handled it, return None (indicating no handler)
475
+ return None
476
+
419
477
 
420
478
  class UIOptionsMixin:
421
479
  """Mixin providing UI options update functionality for classes with ui_options."""