griptape-nodes 0.53.0__py3-none-any.whl → 0.54.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- griptape_nodes/__init__.py +5 -2
- griptape_nodes/app/app.py +4 -26
- griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +35 -5
- griptape_nodes/bootstrap/workflow_executors/workflow_executor.py +15 -1
- griptape_nodes/cli/commands/config.py +4 -1
- griptape_nodes/cli/commands/init.py +5 -3
- griptape_nodes/cli/commands/libraries.py +14 -8
- griptape_nodes/cli/commands/models.py +504 -0
- griptape_nodes/cli/commands/self.py +5 -2
- griptape_nodes/cli/main.py +11 -1
- griptape_nodes/cli/shared.py +0 -9
- griptape_nodes/common/directed_graph.py +17 -1
- 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 +17 -13
- griptape_nodes/exe_types/node_types.py +219 -14
- 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 +129 -92
- griptape_nodes/machines/dag_builder.py +207 -0
- griptape_nodes/machines/parallel_resolution.py +264 -276
- griptape_nodes/machines/sequential_resolution.py +9 -7
- 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 +7 -7
- 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/griptape_nodes.py +10 -1
- griptape_nodes/retained_mode/managers/agent_manager.py +14 -0
- griptape_nodes/retained_mode/managers/config_manager.py +44 -3
- griptape_nodes/retained_mode/managers/event_manager.py +8 -2
- griptape_nodes/retained_mode/managers/flow_manager.py +45 -14
- griptape_nodes/retained_mode/managers/library_manager.py +3 -3
- griptape_nodes/retained_mode/managers/model_manager.py +1107 -0
- griptape_nodes/retained_mode/managers/node_manager.py +26 -26
- griptape_nodes/retained_mode/managers/object_manager.py +1 -1
- griptape_nodes/retained_mode/managers/os_manager.py +6 -6
- griptape_nodes/retained_mode/managers/settings.py +87 -9
- griptape_nodes/retained_mode/managers/static_files_manager.py +77 -9
- griptape_nodes/retained_mode/managers/sync_manager.py +10 -5
- griptape_nodes/retained_mode/managers/workflow_manager.py +101 -92
- 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/button.py +124 -6
- 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/utils/async_utils.py +31 -0
- {griptape_nodes-0.53.0.dist-info → griptape_nodes-0.54.1.dist-info}/METADATA +3 -1
- {griptape_nodes-0.53.0.dist-info → griptape_nodes-0.54.1.dist-info}/RECORD +56 -47
- {griptape_nodes-0.53.0.dist-info → griptape_nodes-0.54.1.dist-info}/WHEEL +1 -1
- /griptape_nodes/{mcp_server → servers}/ws_request_manager.py +0 -0
- {griptape_nodes-0.53.0.dist-info → griptape_nodes-0.54.1.dist-info}/entry_points.txt +0 -0
|
@@ -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,5 +1,6 @@
|
|
|
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
|
|
@@ -12,13 +13,16 @@ logger = logging.getLogger("griptape_nodes")
|
|
|
12
13
|
class LocalStorageDriver(BaseStorageDriver):
|
|
13
14
|
"""Stores files using the engine's local static server."""
|
|
14
15
|
|
|
15
|
-
def __init__(self, base_url: str | None = None) -> None:
|
|
16
|
+
def __init__(self, workspace_directory: Path, base_url: str | None = None) -> None:
|
|
16
17
|
"""Initialize the LocalStorageDriver.
|
|
17
18
|
|
|
18
19
|
Args:
|
|
20
|
+
workspace_directory: The base workspace directory path.
|
|
19
21
|
base_url: The base URL for the static file server. If not provided, it will be constructed
|
|
20
22
|
"""
|
|
21
|
-
|
|
23
|
+
super().__init__(workspace_directory)
|
|
24
|
+
|
|
25
|
+
from griptape_nodes.servers.static import (
|
|
22
26
|
STATIC_SERVER_ENABLED,
|
|
23
27
|
STATIC_SERVER_HOST,
|
|
24
28
|
STATIC_SERVER_PORT,
|
|
@@ -33,46 +37,46 @@ class LocalStorageDriver(BaseStorageDriver):
|
|
|
33
37
|
else:
|
|
34
38
|
self.base_url = base_url
|
|
35
39
|
|
|
36
|
-
def create_signed_upload_url(self,
|
|
40
|
+
def create_signed_upload_url(self, path: Path) -> CreateSignedUploadUrlResponse:
|
|
37
41
|
static_url = urljoin(self.base_url, "/static-upload-urls")
|
|
38
42
|
try:
|
|
39
|
-
response = httpx.post(static_url, json={"
|
|
43
|
+
response = httpx.post(static_url, json={"file_path": str(path)})
|
|
40
44
|
response.raise_for_status()
|
|
41
45
|
except httpx.HTTPStatusError as e:
|
|
42
|
-
msg = f"Failed to create presigned URL for file {
|
|
46
|
+
msg = f"Failed to create presigned URL for file {path}: {e}"
|
|
43
47
|
logger.error(msg)
|
|
44
48
|
raise RuntimeError(msg) from e
|
|
45
49
|
|
|
46
50
|
response_data = response.json()
|
|
47
51
|
url = response_data.get("url")
|
|
48
52
|
if url is None:
|
|
49
|
-
msg = f"Failed to create presigned URL for file {
|
|
53
|
+
msg = f"Failed to create presigned URL for file {path}: {response_data}"
|
|
50
54
|
logger.error(msg)
|
|
51
55
|
raise ValueError(msg)
|
|
52
56
|
|
|
53
57
|
return {"url": url, "headers": response_data.get("headers", {}), "method": "PUT"}
|
|
54
58
|
|
|
55
|
-
def create_signed_download_url(self,
|
|
56
|
-
# The base_url already includes the /static path, so just append the
|
|
57
|
-
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}"
|
|
58
62
|
# Add a cache-busting query parameter to the URL so that the browser always reloads the file
|
|
59
63
|
cache_busted_url = f"{url}?t={int(time.time())}"
|
|
60
64
|
return cache_busted_url
|
|
61
65
|
|
|
62
|
-
def delete_file(self,
|
|
66
|
+
def delete_file(self, path: Path) -> None:
|
|
63
67
|
"""Delete a file from local storage.
|
|
64
68
|
|
|
65
69
|
Args:
|
|
66
|
-
|
|
70
|
+
path: The path of the file to delete.
|
|
67
71
|
"""
|
|
68
72
|
# Use the static server's delete endpoint
|
|
69
|
-
delete_url = urljoin(self.base_url, f"/static-files/{
|
|
73
|
+
delete_url = urljoin(self.base_url, f"/static-files/{path}")
|
|
70
74
|
|
|
71
75
|
try:
|
|
72
76
|
response = httpx.delete(delete_url)
|
|
73
77
|
response.raise_for_status()
|
|
74
78
|
except httpx.HTTPStatusError as e:
|
|
75
|
-
msg = f"Failed to delete file {
|
|
79
|
+
msg = f"Failed to delete file {path}: {e}"
|
|
76
80
|
logger.error(msg)
|
|
77
81
|
raise RuntimeError(msg) from e
|
|
78
82
|
|
|
@@ -22,6 +22,7 @@ from griptape_nodes.exe_types.core_types import (
|
|
|
22
22
|
ParameterMode,
|
|
23
23
|
ParameterTypeBuiltin,
|
|
24
24
|
)
|
|
25
|
+
from griptape_nodes.exe_types.param_components.execution_status_component import ExecutionStatusComponent
|
|
25
26
|
from griptape_nodes.exe_types.type_validator import TypeValidator
|
|
26
27
|
from griptape_nodes.retained_mode.events.base_events import (
|
|
27
28
|
ExecutionEvent,
|
|
@@ -715,6 +716,9 @@ class BaseNode(ABC):
|
|
|
715
716
|
raise KeyError(err)
|
|
716
717
|
|
|
717
718
|
def get_next_control_output(self) -> Parameter | None:
|
|
719
|
+
# The default behavior for nodes is to find the first control output found.
|
|
720
|
+
# Advanced nodes can override this behavior (e.g., nodes that have multiple possible
|
|
721
|
+
# control paths).
|
|
718
722
|
for param in self.parameters:
|
|
719
723
|
if (
|
|
720
724
|
ParameterTypeBuiltin.CONTROL_TYPE.value == param.output_type
|
|
@@ -1150,28 +1154,185 @@ class TrackedParameterOutputValues(dict[str, Any]):
|
|
|
1150
1154
|
|
|
1151
1155
|
class ControlNode(BaseNode):
|
|
1152
1156
|
# Control Nodes may have one Control Input Port and at least one Control Output Port
|
|
1157
|
+
def __init__(
|
|
1158
|
+
self,
|
|
1159
|
+
name: str,
|
|
1160
|
+
metadata: dict[Any, Any] | None = None,
|
|
1161
|
+
input_control_name: str | None = None,
|
|
1162
|
+
output_control_name: str | None = None,
|
|
1163
|
+
) -> None:
|
|
1164
|
+
super().__init__(name, metadata=metadata)
|
|
1165
|
+
self.control_parameter_in = ControlParameterInput(
|
|
1166
|
+
display_name=input_control_name if input_control_name is not None else "Flow In"
|
|
1167
|
+
)
|
|
1168
|
+
self.control_parameter_out = ControlParameterOutput(
|
|
1169
|
+
display_name=output_control_name if output_control_name is not None else "Flow Out"
|
|
1170
|
+
)
|
|
1171
|
+
|
|
1172
|
+
self.add_parameter(self.control_parameter_in)
|
|
1173
|
+
self.add_parameter(self.control_parameter_out)
|
|
1174
|
+
|
|
1175
|
+
|
|
1176
|
+
class DataNode(BaseNode):
|
|
1153
1177
|
def __init__(self, name: str, metadata: dict[Any, Any] | None = None) -> None:
|
|
1154
1178
|
super().__init__(name, metadata=metadata)
|
|
1155
|
-
control_parameter_in = ControlParameterInput()
|
|
1156
|
-
control_parameter_out = ControlParameterOutput()
|
|
1157
1179
|
|
|
1158
|
-
|
|
1159
|
-
|
|
1180
|
+
# Create control parameters like ControlNode, but initialize them as hidden
|
|
1181
|
+
# This allows the user to turn a DataNode "into" a Control Node; useful when
|
|
1182
|
+
# in situations like within a For Loop.
|
|
1183
|
+
self.control_parameter_in = ControlParameterInput()
|
|
1184
|
+
self.control_parameter_out = ControlParameterOutput()
|
|
1160
1185
|
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
ParameterTypeBuiltin.CONTROL_TYPE.value == param.output_type
|
|
1165
|
-
and ParameterMode.OUTPUT in param.allowed_modes
|
|
1166
|
-
):
|
|
1167
|
-
return param
|
|
1168
|
-
return None
|
|
1186
|
+
# Hide the control parameters by default
|
|
1187
|
+
self.control_parameter_in.ui_options["hide"] = True
|
|
1188
|
+
self.control_parameter_out.ui_options["hide"] = True
|
|
1169
1189
|
|
|
1190
|
+
self.add_parameter(self.control_parameter_in)
|
|
1191
|
+
self.add_parameter(self.control_parameter_out)
|
|
1192
|
+
|
|
1193
|
+
|
|
1194
|
+
class SuccessFailureNode(BaseNode):
|
|
1195
|
+
"""Base class for nodes that have success/failure branching with control outputs.
|
|
1196
|
+
|
|
1197
|
+
This class provides:
|
|
1198
|
+
- Control input parameter
|
|
1199
|
+
- Two control outputs: success ("exec_out") and failure ("failure")
|
|
1200
|
+
- Execution state tracking for control flow routing
|
|
1201
|
+
- Helper method to check outgoing connections
|
|
1202
|
+
- Helper method to create standard status output parameters
|
|
1203
|
+
"""
|
|
1170
1204
|
|
|
1171
|
-
class DataNode(BaseNode):
|
|
1172
1205
|
def __init__(self, name: str, metadata: dict[Any, Any] | None = None) -> None:
|
|
1173
1206
|
super().__init__(name, metadata=metadata)
|
|
1174
1207
|
|
|
1208
|
+
# Track execution state for control flow routing
|
|
1209
|
+
self._execution_succeeded: bool | None = None
|
|
1210
|
+
|
|
1211
|
+
# Add control input parameter
|
|
1212
|
+
self.control_parameter_in = ControlParameterInput()
|
|
1213
|
+
self.add_parameter(self.control_parameter_in)
|
|
1214
|
+
|
|
1215
|
+
# Add success control output (uses default "exec_out" name)
|
|
1216
|
+
self.control_parameter_out = ControlParameterOutput(
|
|
1217
|
+
display_name="Succeeded", tooltip="Control path when the operation succeeds"
|
|
1218
|
+
)
|
|
1219
|
+
self.add_parameter(self.control_parameter_out)
|
|
1220
|
+
|
|
1221
|
+
# Add failure control output
|
|
1222
|
+
self.failure_output = ControlParameterOutput(
|
|
1223
|
+
name="failure",
|
|
1224
|
+
display_name="Failed",
|
|
1225
|
+
tooltip="Control path when the operation fails",
|
|
1226
|
+
)
|
|
1227
|
+
self.add_parameter(self.failure_output)
|
|
1228
|
+
|
|
1229
|
+
def get_next_control_output(self) -> Parameter | None:
|
|
1230
|
+
"""Determine which control output to follow based on execution result."""
|
|
1231
|
+
if self._execution_succeeded is None:
|
|
1232
|
+
# Execution hasn't completed yet
|
|
1233
|
+
self.stop_flow = True
|
|
1234
|
+
return None
|
|
1235
|
+
|
|
1236
|
+
if self._execution_succeeded:
|
|
1237
|
+
return self.control_parameter_out
|
|
1238
|
+
return self.failure_output
|
|
1239
|
+
|
|
1240
|
+
def _has_outgoing_connections(self, parameter: Parameter) -> bool:
|
|
1241
|
+
"""Check if a specific parameter has outgoing connections."""
|
|
1242
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
1243
|
+
|
|
1244
|
+
connections = GriptapeNodes.FlowManager().get_connections()
|
|
1245
|
+
|
|
1246
|
+
# Check if node has any outgoing connections
|
|
1247
|
+
node_connections = connections.outgoing_index.get(self.name)
|
|
1248
|
+
if node_connections is None:
|
|
1249
|
+
return False
|
|
1250
|
+
|
|
1251
|
+
# Check if this specific parameter has any outgoing connections
|
|
1252
|
+
param_connections = node_connections.get(parameter.name, [])
|
|
1253
|
+
return len(param_connections) > 0
|
|
1254
|
+
|
|
1255
|
+
def _create_status_parameters(
|
|
1256
|
+
self,
|
|
1257
|
+
result_details_tooltip: str = "Details about the operation result",
|
|
1258
|
+
result_details_placeholder: str = "Details on the operation will be presented here.",
|
|
1259
|
+
) -> None:
|
|
1260
|
+
"""Create and add standard status output parameters in a collapsible group.
|
|
1261
|
+
|
|
1262
|
+
This method creates a "Status" ParameterGroup and immediately adds it to the node.
|
|
1263
|
+
Nodes that use this are responsible for calling this at their desired location
|
|
1264
|
+
in their class constructor.
|
|
1265
|
+
|
|
1266
|
+
Creates and adds:
|
|
1267
|
+
- was_successful: Boolean parameter indicating success/failure
|
|
1268
|
+
- result_details: String parameter with operation details
|
|
1269
|
+
|
|
1270
|
+
Args:
|
|
1271
|
+
result_details_tooltip: Custom tooltip for result_details parameter
|
|
1272
|
+
result_details_placeholder: Custom placeholder text for result_details parameter
|
|
1273
|
+
"""
|
|
1274
|
+
# Create status component with OUTPUT modes for SuccessFailureNode
|
|
1275
|
+
self.status_component = ExecutionStatusComponent(
|
|
1276
|
+
self,
|
|
1277
|
+
was_successful_modes={ParameterMode.OUTPUT},
|
|
1278
|
+
result_details_modes={ParameterMode.OUTPUT},
|
|
1279
|
+
parameter_group_initially_collapsed=True,
|
|
1280
|
+
result_details_tooltip=result_details_tooltip,
|
|
1281
|
+
result_details_placeholder=result_details_placeholder,
|
|
1282
|
+
)
|
|
1283
|
+
|
|
1284
|
+
def _clear_execution_status(self) -> None:
|
|
1285
|
+
"""Clear execution status and reset status parameters.
|
|
1286
|
+
|
|
1287
|
+
This method should be called at the start of process() to reset the node state.
|
|
1288
|
+
"""
|
|
1289
|
+
self._execution_succeeded = None
|
|
1290
|
+
self.status_component.clear_execution_status("Beginning execution...")
|
|
1291
|
+
|
|
1292
|
+
def _set_status_results(self, *, was_successful: bool, result_details: str) -> None:
|
|
1293
|
+
"""Set status results and update execution state.
|
|
1294
|
+
|
|
1295
|
+
This method should be called from the process() method to communicate success or failure.
|
|
1296
|
+
It sets the execution state for control flow routing and updates the status output parameters.
|
|
1297
|
+
|
|
1298
|
+
Args:
|
|
1299
|
+
was_successful: Whether the operation succeeded
|
|
1300
|
+
result_details: Details about the operation result
|
|
1301
|
+
"""
|
|
1302
|
+
self._execution_succeeded = was_successful
|
|
1303
|
+
self.status_component.set_execution_result(was_successful=was_successful, result_details=result_details)
|
|
1304
|
+
|
|
1305
|
+
def _handle_failure_exception(self, exception: Exception) -> None:
|
|
1306
|
+
"""Handle failure exceptions based on whether failure output is connected.
|
|
1307
|
+
|
|
1308
|
+
If the failure output has outgoing connections, logs the error and continues execution
|
|
1309
|
+
to allow graceful failure handling. If no connections exist, raises the exception
|
|
1310
|
+
to crash the flow and provide immediate feedback.
|
|
1311
|
+
|
|
1312
|
+
Args:
|
|
1313
|
+
exception: The exception that caused the failure
|
|
1314
|
+
"""
|
|
1315
|
+
if self._has_outgoing_connections(self.failure_output):
|
|
1316
|
+
# User has connected something to Failed output, they want to handle errors gracefully
|
|
1317
|
+
logger.error(
|
|
1318
|
+
"Error in node '%s': %s. Continuing execution since failure output is connected for graceful handling.",
|
|
1319
|
+
self.name,
|
|
1320
|
+
exception,
|
|
1321
|
+
)
|
|
1322
|
+
else:
|
|
1323
|
+
# No graceful handling, raise the exception to crash the flow
|
|
1324
|
+
raise exception
|
|
1325
|
+
|
|
1326
|
+
def validate_before_workflow_run(self) -> list[Exception] | None:
|
|
1327
|
+
"""Clear result details before workflow runs to avoid confusion from previous sessions."""
|
|
1328
|
+
self._set_status_results(was_successful=False, result_details="<Results will appear when the node executes>")
|
|
1329
|
+
return super().validate_before_workflow_run()
|
|
1330
|
+
|
|
1331
|
+
def validate_before_node_run(self) -> list[Exception] | None:
|
|
1332
|
+
"""Clear result details before node runs to avoid confusion from previous sessions."""
|
|
1333
|
+
self._set_status_results(was_successful=False, result_details="<Results will appear when the node executes>")
|
|
1334
|
+
return super().validate_before_node_run()
|
|
1335
|
+
|
|
1175
1336
|
|
|
1176
1337
|
class StartNode(BaseNode):
|
|
1177
1338
|
def __init__(self, name: str, metadata: dict[Any, Any] | None = None) -> None:
|
|
@@ -1183,7 +1344,51 @@ class EndNode(BaseNode):
|
|
|
1183
1344
|
# TODO: https://github.com/griptape-ai/griptape-nodes/issues/854
|
|
1184
1345
|
def __init__(self, name: str, metadata: dict[Any, Any] | None = None) -> None:
|
|
1185
1346
|
super().__init__(name, metadata)
|
|
1186
|
-
|
|
1347
|
+
|
|
1348
|
+
# Add dual control inputs
|
|
1349
|
+
self.succeeded_control = ControlParameterInput(
|
|
1350
|
+
display_name="Succeeded", tooltip="Control path when the flow completed successfully"
|
|
1351
|
+
)
|
|
1352
|
+
self.failed_control = ControlParameterInput(
|
|
1353
|
+
name="failed", display_name="Failed", tooltip="Control path when the flow failed"
|
|
1354
|
+
)
|
|
1355
|
+
|
|
1356
|
+
self.add_parameter(self.succeeded_control)
|
|
1357
|
+
self.add_parameter(self.failed_control)
|
|
1358
|
+
|
|
1359
|
+
# Create status component with INPUT and PROPERTY modes
|
|
1360
|
+
self.status_component = ExecutionStatusComponent(
|
|
1361
|
+
self,
|
|
1362
|
+
was_successful_modes={ParameterMode.PROPERTY},
|
|
1363
|
+
result_details_modes={ParameterMode.INPUT},
|
|
1364
|
+
parameter_group_initially_collapsed=False,
|
|
1365
|
+
result_details_placeholder="Details about the completion or failure will be shown here.",
|
|
1366
|
+
)
|
|
1367
|
+
|
|
1368
|
+
def process(self) -> None:
|
|
1369
|
+
# Detect which control input was used to enter this node and determine success status
|
|
1370
|
+
match self._entry_control_parameter:
|
|
1371
|
+
case self.succeeded_control:
|
|
1372
|
+
was_successful = True
|
|
1373
|
+
status_prefix = "[SUCCEEDED]"
|
|
1374
|
+
case self.failed_control:
|
|
1375
|
+
was_successful = False
|
|
1376
|
+
status_prefix = "[FAILED]"
|
|
1377
|
+
case _:
|
|
1378
|
+
# No specific success/failure connection provided, assume success
|
|
1379
|
+
was_successful = True
|
|
1380
|
+
status_prefix = "[SUCCEEDED] No connection provided for success or failure, assuming successful"
|
|
1381
|
+
|
|
1382
|
+
# Get result details and format the final message
|
|
1383
|
+
result_details_value = self.get_parameter_value("result_details")
|
|
1384
|
+
if result_details_value and self._entry_control_parameter in (self.succeeded_control, self.failed_control):
|
|
1385
|
+
details = f"{status_prefix}\n{result_details_value}"
|
|
1386
|
+
elif self._entry_control_parameter in (self.succeeded_control, self.failed_control):
|
|
1387
|
+
details = f"{status_prefix}\nNo details supplied by flow"
|
|
1388
|
+
else:
|
|
1389
|
+
details = status_prefix
|
|
1390
|
+
|
|
1391
|
+
self.status_component.set_execution_result(was_successful=was_successful, result_details=details)
|
|
1187
1392
|
|
|
1188
1393
|
|
|
1189
1394
|
class StartLoopNode(BaseNode):
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Parameter components for reusable node functionality."""
|