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.
- griptape_nodes/__init__.py +8 -942
- griptape_nodes/__main__.py +6 -0
- griptape_nodes/app/app.py +48 -86
- griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +35 -5
- griptape_nodes/bootstrap/workflow_executors/workflow_executor.py +15 -1
- griptape_nodes/cli/__init__.py +1 -0
- griptape_nodes/cli/commands/__init__.py +1 -0
- griptape_nodes/cli/commands/config.py +74 -0
- griptape_nodes/cli/commands/engine.py +80 -0
- griptape_nodes/cli/commands/init.py +550 -0
- griptape_nodes/cli/commands/libraries.py +96 -0
- griptape_nodes/cli/commands/models.py +504 -0
- griptape_nodes/cli/commands/self.py +120 -0
- griptape_nodes/cli/main.py +56 -0
- griptape_nodes/cli/shared.py +75 -0
- griptape_nodes/common/__init__.py +1 -0
- griptape_nodes/common/directed_graph.py +71 -0
- griptape_nodes/drivers/storage/base_storage_driver.py +40 -20
- griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +24 -29
- griptape_nodes/drivers/storage/local_storage_driver.py +23 -14
- griptape_nodes/exe_types/core_types.py +60 -2
- griptape_nodes/exe_types/node_types.py +257 -38
- griptape_nodes/exe_types/param_components/__init__.py +1 -0
- griptape_nodes/exe_types/param_components/execution_status_component.py +138 -0
- griptape_nodes/machines/control_flow.py +195 -94
- griptape_nodes/machines/dag_builder.py +207 -0
- griptape_nodes/machines/fsm.py +10 -1
- griptape_nodes/machines/parallel_resolution.py +558 -0
- griptape_nodes/machines/{node_resolution.py → sequential_resolution.py} +30 -57
- griptape_nodes/node_library/library_registry.py +34 -1
- griptape_nodes/retained_mode/events/app_events.py +5 -1
- griptape_nodes/retained_mode/events/base_events.py +9 -9
- griptape_nodes/retained_mode/events/config_events.py +30 -0
- griptape_nodes/retained_mode/events/execution_events.py +2 -2
- griptape_nodes/retained_mode/events/model_events.py +296 -0
- griptape_nodes/retained_mode/events/node_events.py +4 -3
- griptape_nodes/retained_mode/griptape_nodes.py +34 -12
- griptape_nodes/retained_mode/managers/agent_manager.py +23 -5
- griptape_nodes/retained_mode/managers/arbitrary_code_exec_manager.py +3 -1
- griptape_nodes/retained_mode/managers/config_manager.py +44 -3
- griptape_nodes/retained_mode/managers/context_manager.py +6 -5
- griptape_nodes/retained_mode/managers/event_manager.py +8 -2
- griptape_nodes/retained_mode/managers/flow_manager.py +150 -206
- griptape_nodes/retained_mode/managers/library_lifecycle/library_directory.py +1 -1
- griptape_nodes/retained_mode/managers/library_manager.py +35 -25
- griptape_nodes/retained_mode/managers/model_manager.py +1107 -0
- griptape_nodes/retained_mode/managers/node_manager.py +102 -220
- griptape_nodes/retained_mode/managers/object_manager.py +11 -5
- griptape_nodes/retained_mode/managers/os_manager.py +28 -13
- griptape_nodes/retained_mode/managers/secrets_manager.py +8 -4
- griptape_nodes/retained_mode/managers/settings.py +116 -7
- griptape_nodes/retained_mode/managers/static_files_manager.py +85 -12
- griptape_nodes/retained_mode/managers/sync_manager.py +17 -9
- griptape_nodes/retained_mode/managers/workflow_manager.py +186 -192
- griptape_nodes/retained_mode/retained_mode.py +19 -0
- griptape_nodes/servers/__init__.py +1 -0
- griptape_nodes/{mcp_server/server.py → servers/mcp.py} +1 -1
- griptape_nodes/{app/api.py → servers/static.py} +43 -40
- griptape_nodes/traits/add_param_button.py +1 -1
- griptape_nodes/traits/button.py +334 -6
- griptape_nodes/traits/color_picker.py +66 -0
- griptape_nodes/traits/multi_options.py +188 -0
- griptape_nodes/traits/numbers_selector.py +77 -0
- griptape_nodes/traits/options.py +93 -2
- griptape_nodes/traits/traits.json +4 -0
- griptape_nodes/utils/async_utils.py +31 -0
- {griptape_nodes-0.52.1.dist-info → griptape_nodes-0.54.0.dist-info}/METADATA +4 -1
- {griptape_nodes-0.52.1.dist-info → griptape_nodes-0.54.0.dist-info}/RECORD +71 -48
- {griptape_nodes-0.52.1.dist-info → griptape_nodes-0.54.0.dist-info}/WHEEL +1 -1
- /griptape_nodes/{mcp_server → servers}/ws_request_manager.py +0 -0
- {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,
|
|
23
|
-
"""Create a signed upload URL for the given
|
|
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
|
-
|
|
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,
|
|
35
|
-
"""Create a signed download URL for the given
|
|
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
|
-
|
|
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,
|
|
66
|
+
def delete_file(self, path: Path) -> None:
|
|
47
67
|
"""Delete a file from storage.
|
|
48
68
|
|
|
49
69
|
Args:
|
|
50
|
-
|
|
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,
|
|
83
|
+
def upload_file(self, path: Path, file_content: bytes) -> str:
|
|
64
84
|
"""Upload a file to storage.
|
|
65
85
|
|
|
66
86
|
Args:
|
|
67
|
-
|
|
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(
|
|
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(
|
|
110
|
+
return self.create_signed_download_url(path)
|
|
91
111
|
except httpx.HTTPStatusError as e:
|
|
92
|
-
msg = f"Failed to upload file {
|
|
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 {
|
|
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,
|
|
101
|
-
"""Download a file from
|
|
120
|
+
def download_file(self, path: Path) -> bytes:
|
|
121
|
+
"""Download a file from storage.
|
|
102
122
|
|
|
103
123
|
Args:
|
|
104
|
-
|
|
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(
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
34
|
-
|
|
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,
|
|
49
|
-
"""Get the full file path including
|
|
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
|
-
|
|
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}/{
|
|
59
|
-
return
|
|
53
|
+
return f"{self.static_files_directory}/{path}"
|
|
54
|
+
return str(path)
|
|
60
55
|
|
|
61
|
-
def create_signed_upload_url(self,
|
|
62
|
-
full_file_path = self._get_full_file_path(
|
|
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 {
|
|
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,
|
|
79
|
-
full_file_path = self._get_full_file_path(
|
|
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 {
|
|
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,
|
|
188
|
+
def delete_file(self, path: Path) -> None:
|
|
194
189
|
"""Delete a file from the bucket.
|
|
195
190
|
|
|
196
191
|
Args:
|
|
197
|
-
|
|
192
|
+
path: The path of the file to delete.
|
|
198
193
|
"""
|
|
199
|
-
full_file_path = self._get_full_file_path(
|
|
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 {
|
|
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,
|
|
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={"
|
|
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 {
|
|
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 {
|
|
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,
|
|
51
|
-
# The base_url already includes the /static path, so just append the
|
|
52
|
-
url = f"{self.base_url}/{
|
|
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,
|
|
66
|
+
def delete_file(self, path: Path) -> None:
|
|
58
67
|
"""Delete a file from local storage.
|
|
59
68
|
|
|
60
69
|
Args:
|
|
61
|
-
|
|
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/{
|
|
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 {
|
|
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(
|
|
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."""
|