griptape-nodes 0.70.1__py3-none-any.whl → 0.72.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/api_client/client.py +8 -5
- griptape_nodes/app/app.py +4 -0
- griptape_nodes/bootstrap/utils/python_subprocess_executor.py +48 -9
- griptape_nodes/bootstrap/utils/subprocess_websocket_base.py +88 -0
- griptape_nodes/bootstrap/utils/subprocess_websocket_listener.py +126 -0
- griptape_nodes/bootstrap/utils/subprocess_websocket_sender.py +121 -0
- griptape_nodes/bootstrap/workflow_executors/local_session_workflow_executor.py +17 -170
- griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +10 -1
- griptape_nodes/bootstrap/workflow_executors/subprocess_workflow_executor.py +13 -117
- griptape_nodes/bootstrap/workflow_executors/utils/subprocess_script.py +4 -0
- griptape_nodes/bootstrap/workflow_publishers/local_session_workflow_publisher.py +206 -0
- griptape_nodes/bootstrap/workflow_publishers/subprocess_workflow_publisher.py +22 -3
- griptape_nodes/bootstrap/workflow_publishers/utils/subprocess_script.py +49 -25
- griptape_nodes/common/node_executor.py +61 -14
- griptape_nodes/drivers/image_metadata/__init__.py +21 -0
- griptape_nodes/drivers/image_metadata/base_image_metadata_driver.py +63 -0
- griptape_nodes/drivers/image_metadata/exif_metadata_driver.py +218 -0
- griptape_nodes/drivers/image_metadata/image_metadata_driver_registry.py +55 -0
- griptape_nodes/drivers/image_metadata/png_metadata_driver.py +71 -0
- griptape_nodes/drivers/storage/base_storage_driver.py +32 -0
- griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +384 -10
- griptape_nodes/drivers/storage/local_storage_driver.py +65 -4
- griptape_nodes/drivers/thread_storage/local_thread_storage_driver.py +1 -0
- griptape_nodes/exe_types/base_iterative_nodes.py +1 -1
- griptape_nodes/exe_types/node_groups/base_node_group.py +3 -0
- griptape_nodes/exe_types/node_groups/subflow_node_group.py +18 -0
- griptape_nodes/exe_types/node_types.py +13 -0
- griptape_nodes/exe_types/param_components/log_parameter.py +4 -4
- griptape_nodes/exe_types/param_components/subflow_execution_component.py +329 -0
- griptape_nodes/exe_types/param_types/parameter_audio.py +17 -2
- griptape_nodes/exe_types/param_types/parameter_float.py +4 -4
- griptape_nodes/exe_types/param_types/parameter_image.py +14 -1
- griptape_nodes/exe_types/param_types/parameter_int.py +4 -4
- griptape_nodes/exe_types/param_types/parameter_number.py +12 -14
- griptape_nodes/exe_types/param_types/parameter_three_d.py +14 -1
- griptape_nodes/exe_types/param_types/parameter_video.py +17 -2
- griptape_nodes/node_library/workflow_registry.py +5 -8
- griptape_nodes/retained_mode/events/app_events.py +1 -0
- griptape_nodes/retained_mode/events/base_events.py +42 -26
- griptape_nodes/retained_mode/events/flow_events.py +67 -0
- griptape_nodes/retained_mode/events/library_events.py +1 -1
- griptape_nodes/retained_mode/events/node_events.py +1 -0
- griptape_nodes/retained_mode/events/os_events.py +22 -0
- griptape_nodes/retained_mode/events/static_file_events.py +28 -4
- griptape_nodes/retained_mode/managers/flow_manager.py +134 -0
- griptape_nodes/retained_mode/managers/image_metadata_injector.py +339 -0
- griptape_nodes/retained_mode/managers/library_manager.py +71 -41
- griptape_nodes/retained_mode/managers/model_manager.py +1 -0
- griptape_nodes/retained_mode/managers/node_manager.py +8 -5
- griptape_nodes/retained_mode/managers/os_manager.py +270 -33
- griptape_nodes/retained_mode/managers/project_manager.py +3 -7
- griptape_nodes/retained_mode/managers/session_manager.py +1 -0
- griptape_nodes/retained_mode/managers/settings.py +5 -0
- griptape_nodes/retained_mode/managers/static_files_manager.py +83 -17
- griptape_nodes/retained_mode/managers/workflow_manager.py +71 -41
- griptape_nodes/servers/static.py +31 -0
- griptape_nodes/utils/__init__.py +9 -1
- griptape_nodes/utils/artifact_normalization.py +245 -0
- griptape_nodes/utils/file_utils.py +13 -13
- griptape_nodes/utils/http_file_patch.py +613 -0
- griptape_nodes/utils/image_preview.py +27 -0
- griptape_nodes/utils/path_utils.py +58 -0
- griptape_nodes/utils/url_utils.py +106 -0
- {griptape_nodes-0.70.1.dist-info → griptape_nodes-0.72.0.dist-info}/METADATA +2 -1
- {griptape_nodes-0.70.1.dist-info → griptape_nodes-0.72.0.dist-info}/RECORD +67 -52
- {griptape_nodes-0.70.1.dist-info → griptape_nodes-0.72.0.dist-info}/WHEEL +1 -1
- {griptape_nodes-0.70.1.dist-info → griptape_nodes-0.72.0.dist-info}/entry_points.txt +0 -0
|
@@ -6,8 +6,9 @@ from urllib.parse import urljoin
|
|
|
6
6
|
import httpx
|
|
7
7
|
|
|
8
8
|
from griptape_nodes.drivers.storage.base_storage_driver import BaseStorageDriver, CreateSignedUploadUrlResponse
|
|
9
|
-
from griptape_nodes.retained_mode.events.os_events import ExistingFilePolicy, WriteFileRequest
|
|
9
|
+
from griptape_nodes.retained_mode.events.os_events import ExistingFilePolicy, WriteFileRequest, WriteFileResultSuccess
|
|
10
10
|
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
11
|
+
from griptape_nodes.utils import resolve_workspace_path
|
|
11
12
|
|
|
12
13
|
logger = logging.getLogger("griptape_nodes")
|
|
13
14
|
|
|
@@ -44,7 +45,7 @@ class LocalStorageDriver(BaseStorageDriver):
|
|
|
44
45
|
self, path: Path, existing_file_policy: ExistingFilePolicy = ExistingFilePolicy.OVERWRITE
|
|
45
46
|
) -> CreateSignedUploadUrlResponse:
|
|
46
47
|
# on_write_file_request seems to work most reliably with an absolute path.
|
|
47
|
-
absolute_path = path
|
|
48
|
+
absolute_path = resolve_workspace_path(path, self.workspace_directory)
|
|
48
49
|
|
|
49
50
|
# Always delegate to OSManager for file path resolution and policy handling.
|
|
50
51
|
# Creating an empty file before the upload url gives us a chance to claim ownership
|
|
@@ -94,9 +95,55 @@ class LocalStorageDriver(BaseStorageDriver):
|
|
|
94
95
|
"file_path": str(resolved_path),
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
def save_file(
|
|
99
|
+
self, path: Path, file_content: bytes, existing_file_policy: ExistingFilePolicy = ExistingFilePolicy.OVERWRITE
|
|
100
|
+
) -> str:
|
|
101
|
+
"""Save a file to local storage by writing directly to disk.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
path: The path of the file to save.
|
|
105
|
+
file_content: The file content as bytes.
|
|
106
|
+
existing_file_policy: How to handle existing files. Defaults to OVERWRITE.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
The absolute file path where the file was saved.
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
FileExistsError: When existing_file_policy is FAIL and file already exists.
|
|
113
|
+
RuntimeError: If file write fails.
|
|
114
|
+
"""
|
|
115
|
+
absolute_path = resolve_workspace_path(path, self.workspace_directory)
|
|
116
|
+
|
|
117
|
+
result = GriptapeNodes.OSManager().on_write_file_request(
|
|
118
|
+
WriteFileRequest(
|
|
119
|
+
file_path=str(absolute_path),
|
|
120
|
+
content=file_content,
|
|
121
|
+
existing_file_policy=existing_file_policy,
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if not isinstance(result, WriteFileResultSuccess):
|
|
126
|
+
msg = f"Failed to write file {path}: {result.result_details}"
|
|
127
|
+
raise ValueError(msg) # noqa: TRY004
|
|
128
|
+
|
|
129
|
+
return result.final_file_path
|
|
130
|
+
|
|
97
131
|
def create_signed_download_url(self, path: Path) -> str:
|
|
98
|
-
#
|
|
99
|
-
|
|
132
|
+
# Resolve path, treating relative paths as workspace-relative
|
|
133
|
+
absolute_path = resolve_workspace_path(path, self.workspace_directory)
|
|
134
|
+
|
|
135
|
+
# Automatically determine if the file is external to the workspace
|
|
136
|
+
try:
|
|
137
|
+
workspace_relative_path = absolute_path.relative_to(self.workspace_directory.resolve())
|
|
138
|
+
# Internal files: use workspace-relative path
|
|
139
|
+
url = f"{self.base_url}/{workspace_relative_path.as_posix()}"
|
|
140
|
+
except ValueError:
|
|
141
|
+
# For external files, use /external path and strip leading slash from absolute path
|
|
142
|
+
path_str = str(absolute_path).removeprefix("/")
|
|
143
|
+
# Build URL with /external prefix, replacing the /workspace part of base_url
|
|
144
|
+
base_without_workspace = self.base_url.rsplit("/workspace", 1)[0]
|
|
145
|
+
url = f"{base_without_workspace}/external/{path_str}"
|
|
146
|
+
|
|
100
147
|
# Add a cache-busting query parameter to the URL so that the browser always reloads the file
|
|
101
148
|
cache_busted_url = f"{url}?t={int(time.time())}"
|
|
102
149
|
return cache_busted_url
|
|
@@ -137,3 +184,17 @@ class LocalStorageDriver(BaseStorageDriver):
|
|
|
137
184
|
|
|
138
185
|
response_data = response.json()
|
|
139
186
|
return response_data.get("files", [])
|
|
187
|
+
|
|
188
|
+
def get_asset_url(self, path: Path) -> str:
|
|
189
|
+
"""Get the permanent URL for a local asset.
|
|
190
|
+
|
|
191
|
+
Returns the absolute file path.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
path: The path of the file
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Absolute file path as a string
|
|
198
|
+
"""
|
|
199
|
+
absolute_path = resolve_workspace_path(path, self.workspace_directory)
|
|
200
|
+
return str(absolute_path)
|
|
@@ -117,6 +117,7 @@ class LocalThreadStorageDriver(BaseThreadStorageDriver):
|
|
|
117
117
|
msg = f"Cannot delete thread {thread_id}. Archive it first."
|
|
118
118
|
raise ValueError(msg)
|
|
119
119
|
|
|
120
|
+
# TODO: Replace with DeleteFileRequest https://github.com/griptape-ai/griptape-nodes/issues/3765
|
|
120
121
|
thread_file.unlink()
|
|
121
122
|
|
|
122
123
|
def thread_exists(self, thread_id: str) -> bool:
|
|
@@ -139,7 +139,7 @@ class BaseIterativeStartNode(BaseNode):
|
|
|
139
139
|
allowed_modes={ParameterMode.PROPERTY, ParameterMode.OUTPUT},
|
|
140
140
|
settable=False,
|
|
141
141
|
default_value=0,
|
|
142
|
-
|
|
142
|
+
hide_property=True,
|
|
143
143
|
)
|
|
144
144
|
self.add_node_element(group)
|
|
145
145
|
|
|
@@ -80,6 +80,9 @@ class BaseNodeGroup(BaseNode):
|
|
|
80
80
|
if node.name in self.nodes:
|
|
81
81
|
del self.nodes[node.name]
|
|
82
82
|
|
|
83
|
+
node_names_in_group = set(self.nodes.keys())
|
|
84
|
+
self.metadata["node_names_in_group"] = list(node_names_in_group)
|
|
85
|
+
|
|
83
86
|
def _add_nodes_to_group_dict(self, nodes: list[BaseNode]) -> None:
|
|
84
87
|
"""Add nodes to the group's node dictionary."""
|
|
85
88
|
for node in nodes:
|
|
@@ -16,6 +16,7 @@ from griptape_nodes.exe_types.node_types import (
|
|
|
16
16
|
LOCAL_EXECUTION,
|
|
17
17
|
get_library_names_with_publish_handlers,
|
|
18
18
|
)
|
|
19
|
+
from griptape_nodes.exe_types.param_components.subflow_execution_component import SubflowExecutionComponent
|
|
19
20
|
from griptape_nodes.retained_mode.events.connection_events import (
|
|
20
21
|
CreateConnectionRequest,
|
|
21
22
|
DeleteConnectionRequest,
|
|
@@ -84,6 +85,9 @@ class SubflowNodeGroup(BaseNodeGroup, ABC):
|
|
|
84
85
|
# Add parameters from registered StartFlow nodes for each publishing library
|
|
85
86
|
self._add_start_flow_parameters()
|
|
86
87
|
|
|
88
|
+
# Add subprocess execution status component for real-time GUI updates
|
|
89
|
+
self._add_subflow_execution_parameters()
|
|
90
|
+
|
|
87
91
|
def _create_subflow(self) -> None:
|
|
88
92
|
"""Create a dedicated subflow for this NodeGroup's nodes.
|
|
89
93
|
|
|
@@ -144,6 +148,11 @@ class SubflowNodeGroup(BaseNodeGroup, ABC):
|
|
|
144
148
|
for library_name, handler in event_handlers.items():
|
|
145
149
|
self._process_library_start_flow_parameters(library_name, handler)
|
|
146
150
|
|
|
151
|
+
def _add_subflow_execution_parameters(self) -> None:
|
|
152
|
+
"""Add parameters for subflow execution tracking."""
|
|
153
|
+
self._subflow_execution_component = SubflowExecutionComponent(self)
|
|
154
|
+
self._subflow_execution_component.add_output_parameters()
|
|
155
|
+
|
|
147
156
|
def _process_library_start_flow_parameters(self, library_name: str, handler: Any) -> None:
|
|
148
157
|
"""Process and add StartFlow parameters from a single library.
|
|
149
158
|
|
|
@@ -649,6 +658,10 @@ class SubflowNodeGroup(BaseNodeGroup, ABC):
|
|
|
649
658
|
self._cleanup_proxy_parameter(target_parameter, metadata_key)
|
|
650
659
|
return super().after_incoming_connection_removed(source_node, source_parameter, target_parameter)
|
|
651
660
|
|
|
661
|
+
def after_value_set(self, parameter: Parameter, value: Any) -> None:
|
|
662
|
+
super().after_value_set(parameter, value)
|
|
663
|
+
self.subflow_execution_component.after_value_set(parameter, value)
|
|
664
|
+
|
|
652
665
|
def add_nodes_to_group(self, nodes: list[BaseNode]) -> None:
|
|
653
666
|
"""Add nodes to the group and track their connections.
|
|
654
667
|
|
|
@@ -1004,3 +1017,8 @@ class SubflowNodeGroup(BaseNodeGroup, ABC):
|
|
|
1004
1017
|
self.remove_nodes_from_group(nodes_to_remove)
|
|
1005
1018
|
subflow_name = self.metadata.get("subflow_name")
|
|
1006
1019
|
return subflow_name
|
|
1020
|
+
|
|
1021
|
+
@property
|
|
1022
|
+
def subflow_execution_component(self) -> SubflowExecutionComponent:
|
|
1023
|
+
"""Get the subflow execution component for real-time status updates."""
|
|
1024
|
+
return self._subflow_execution_component
|
|
@@ -48,6 +48,7 @@ logger = logging.getLogger("griptape_nodes")
|
|
|
48
48
|
T = TypeVar("T")
|
|
49
49
|
|
|
50
50
|
NODE_GROUP_FLOW = "NodeGroupFlow"
|
|
51
|
+
NODE_DEFAULT_SIZE = {"width": 400, "height": 320}
|
|
51
52
|
|
|
52
53
|
|
|
53
54
|
class TransformedParameterValue(NamedTuple):
|
|
@@ -830,6 +831,18 @@ class BaseNode(ABC):
|
|
|
830
831
|
emit_change=False,
|
|
831
832
|
)
|
|
832
833
|
|
|
834
|
+
def set_initial_node_size(
|
|
835
|
+
self, width: int = NODE_DEFAULT_SIZE["width"], height: int = NODE_DEFAULT_SIZE["height"]
|
|
836
|
+
) -> None:
|
|
837
|
+
"""Set the node's UI size. Node authors can call this to give the node a default or custom size.
|
|
838
|
+
|
|
839
|
+
Args:
|
|
840
|
+
width: Width in pixels.
|
|
841
|
+
height: Height in pixels.
|
|
842
|
+
"""
|
|
843
|
+
if "size" not in self.metadata:
|
|
844
|
+
self.metadata["size"] = {"width": width, "height": height}
|
|
845
|
+
|
|
833
846
|
def kill_parameter_children(self, parameter: Parameter) -> None:
|
|
834
847
|
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
835
848
|
|
|
@@ -4,6 +4,7 @@ import sys
|
|
|
4
4
|
import time
|
|
5
5
|
from collections.abc import Callable, Iterator
|
|
6
6
|
from types import TracebackType
|
|
7
|
+
from typing import Self
|
|
7
8
|
|
|
8
9
|
from griptape_nodes.exe_types.core_types import Parameter, ParameterMode
|
|
9
10
|
from griptape_nodes.exe_types.node_types import BaseNode
|
|
@@ -69,10 +70,9 @@ class StdoutCapture:
|
|
|
69
70
|
self._original_stdout.flush()
|
|
70
71
|
|
|
71
72
|
def isatty(self) -> bool:
|
|
72
|
-
|
|
73
|
-
return False
|
|
73
|
+
return self._original_stdout.isatty()
|
|
74
74
|
|
|
75
|
-
def __enter__(self) ->
|
|
75
|
+
def __enter__(self) -> Self:
|
|
76
76
|
sys.stdout = self
|
|
77
77
|
return self
|
|
78
78
|
|
|
@@ -101,7 +101,7 @@ class LoggerCapture:
|
|
|
101
101
|
self.target_level = level
|
|
102
102
|
self._handler = CallbackHandler(callback)
|
|
103
103
|
|
|
104
|
-
def __enter__(self) ->
|
|
104
|
+
def __enter__(self) -> Self:
|
|
105
105
|
self.original_level = self.logger.level
|
|
106
106
|
self.logger.setLevel(self.target_level)
|
|
107
107
|
self.logger.addHandler(self._handler)
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from griptape_nodes.exe_types.core_types import NodeMessageResult, Parameter, ParameterMode
|
|
7
|
+
from griptape_nodes.exe_types.node_types import LOCAL_EXECUTION, PRIVATE_EXECUTION
|
|
8
|
+
from griptape_nodes.retained_mode.events.base_events import (
|
|
9
|
+
EventResultSuccess,
|
|
10
|
+
ExecutionEvent,
|
|
11
|
+
)
|
|
12
|
+
from griptape_nodes.retained_mode.events.execution_events import (
|
|
13
|
+
ControlFlowCancelledEvent,
|
|
14
|
+
ControlFlowResolvedEvent,
|
|
15
|
+
GriptapeEvent,
|
|
16
|
+
NodeFinishProcessEvent,
|
|
17
|
+
NodeResolvedEvent,
|
|
18
|
+
NodeStartProcessEvent,
|
|
19
|
+
)
|
|
20
|
+
from griptape_nodes.retained_mode.events.payload_registry import PayloadRegistry
|
|
21
|
+
from griptape_nodes.retained_mode.events.workflow_events import (
|
|
22
|
+
PublishWorkflowProgressEvent,
|
|
23
|
+
PublishWorkflowRequest,
|
|
24
|
+
PublishWorkflowResultFailure,
|
|
25
|
+
PublishWorkflowResultSuccess,
|
|
26
|
+
)
|
|
27
|
+
from griptape_nodes.traits.button import Button, ButtonDetailsMessagePayload, OnClickMessageResultPayload
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from griptape_nodes.exe_types.node_types import BaseNode
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SubflowExecutionComponent:
|
|
36
|
+
"""A reusable component for managing subprocess execution event parameters.
|
|
37
|
+
|
|
38
|
+
This component creates and manages parameters that display
|
|
39
|
+
real-time events from subprocess execution in the GUI.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, node: BaseNode) -> None:
|
|
43
|
+
"""Initialize the SubflowExecutionComponent.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
node: The node instance that will own the parameter
|
|
47
|
+
"""
|
|
48
|
+
self._node = node
|
|
49
|
+
|
|
50
|
+
def add_output_parameters(self) -> None:
|
|
51
|
+
"""Add the parameters to the node."""
|
|
52
|
+
self._node.add_parameter(
|
|
53
|
+
Parameter(
|
|
54
|
+
name="publishing_progress",
|
|
55
|
+
output_type="float",
|
|
56
|
+
allowed_modes={ParameterMode.PROPERTY},
|
|
57
|
+
tooltip="Progress bar showing workflow publishing completion (0.0 to 1.0)",
|
|
58
|
+
ui_options={"progress_bar": True},
|
|
59
|
+
settable=False,
|
|
60
|
+
hide=True,
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
self._node.add_parameter(
|
|
64
|
+
Parameter(
|
|
65
|
+
name="publishing_target_link",
|
|
66
|
+
output_type="str",
|
|
67
|
+
tooltip="Click the button to open the published workflow location",
|
|
68
|
+
hide=True,
|
|
69
|
+
allowed_modes={ParameterMode.PROPERTY},
|
|
70
|
+
traits={
|
|
71
|
+
Button(
|
|
72
|
+
icon="link",
|
|
73
|
+
on_click=self._handle_get_publishing_target_link,
|
|
74
|
+
tooltip="Open publishing target link",
|
|
75
|
+
state="normal",
|
|
76
|
+
),
|
|
77
|
+
},
|
|
78
|
+
ui_options={"placeholder_text": "Link will appear after publishing"},
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
self._node.add_parameter(
|
|
82
|
+
Parameter(
|
|
83
|
+
name="execution_events",
|
|
84
|
+
output_type="str",
|
|
85
|
+
allowed_modes={ParameterMode.PROPERTY},
|
|
86
|
+
tooltip="Real-time events from subprocess execution",
|
|
87
|
+
ui_options={"multiline": True},
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
if "execution_panel" not in self._node.metadata:
|
|
91
|
+
self._node.metadata["execution_panel"] = {"params": []}
|
|
92
|
+
self._node.metadata["execution_panel"]["params"].append("execution_events")
|
|
93
|
+
self._node.metadata["execution_panel"]["params"].append("publishing_progress")
|
|
94
|
+
self._node.metadata["execution_panel"]["params"].append("publishing_target_link")
|
|
95
|
+
|
|
96
|
+
def _handle_get_publishing_target_link(
|
|
97
|
+
self,
|
|
98
|
+
button: Button, # noqa: ARG002
|
|
99
|
+
button_details: ButtonDetailsMessagePayload,
|
|
100
|
+
) -> NodeMessageResult | None:
|
|
101
|
+
publishing_target_link = self._node.get_parameter_value("publishing_target_link")
|
|
102
|
+
if publishing_target_link:
|
|
103
|
+
return NodeMessageResult(
|
|
104
|
+
success=True,
|
|
105
|
+
details="Publishing target link retrieved successfully.",
|
|
106
|
+
response=OnClickMessageResultPayload(
|
|
107
|
+
button_details=button_details,
|
|
108
|
+
href=publishing_target_link,
|
|
109
|
+
),
|
|
110
|
+
altered_workflow_state=False,
|
|
111
|
+
)
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
def clear_execution_state(self) -> None:
|
|
115
|
+
"""Clear the component state."""
|
|
116
|
+
self.reset_publishing_progress()
|
|
117
|
+
self.clear_events()
|
|
118
|
+
self.clear_publishing_target_link()
|
|
119
|
+
|
|
120
|
+
def clear_events(self) -> None:
|
|
121
|
+
"""Clear events at start of execution."""
|
|
122
|
+
self._node.publish_update_to_parameter("execution_events", "")
|
|
123
|
+
|
|
124
|
+
def clear_publishing_target_link(self) -> None:
|
|
125
|
+
"""Clear the publishing target link parameter."""
|
|
126
|
+
self._node.set_parameter_value("publishing_target_link", None)
|
|
127
|
+
|
|
128
|
+
def append_event(self, event_str: str) -> None:
|
|
129
|
+
"""Append a stringified event to the parameter.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
event_str: The event string to append
|
|
133
|
+
"""
|
|
134
|
+
self._node.append_value_to_parameter("execution_events", event_str + "\n")
|
|
135
|
+
|
|
136
|
+
def after_value_set(self, parameter: Parameter, value: Any) -> None:
|
|
137
|
+
"""Handle actions after a parameter value is set.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
parameter: The parameter that was set
|
|
141
|
+
value: The new value of the parameter
|
|
142
|
+
"""
|
|
143
|
+
if parameter.name == "execution_environment":
|
|
144
|
+
if value in {LOCAL_EXECUTION, PRIVATE_EXECUTION}:
|
|
145
|
+
self._node.hide_parameter_by_name("publishing_progress")
|
|
146
|
+
self._node.hide_parameter_by_name("publishing_target_link")
|
|
147
|
+
else:
|
|
148
|
+
self._node.show_parameter_by_name("publishing_progress")
|
|
149
|
+
|
|
150
|
+
if parameter.name == "publishing_target_link":
|
|
151
|
+
if value and self._node.get_parameter_value("execution_environment") not in {
|
|
152
|
+
LOCAL_EXECUTION,
|
|
153
|
+
PRIVATE_EXECUTION,
|
|
154
|
+
}:
|
|
155
|
+
self._node.show_parameter_by_name("publishing_target_link")
|
|
156
|
+
else:
|
|
157
|
+
self._node.hide_parameter_by_name("publishing_target_link")
|
|
158
|
+
|
|
159
|
+
def reset_publishing_progress(self) -> None:
|
|
160
|
+
"""Reset the publishing progress bar to 0."""
|
|
161
|
+
self._node.publish_update_to_parameter("publishing_progress", 0.0)
|
|
162
|
+
|
|
163
|
+
def _parse_execution_event(self, event: dict) -> ExecutionEvent | None:
|
|
164
|
+
"""Parse an execution event dictionary into an ExecutionEvent object.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
event: The event dictionary containing the execution event data.
|
|
168
|
+
Expected to have type="execution_event" and a payload with payload_type.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
The parsed ExecutionEvent if successful, None if the event cannot be parsed
|
|
172
|
+
(wrong type, unknown payload type, etc.)
|
|
173
|
+
"""
|
|
174
|
+
event_type = event.get("type", "unknown")
|
|
175
|
+
if event_type != "execution_event":
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
payload = event.get("payload", {})
|
|
179
|
+
payload_type_name = payload.get("payload_type", "")
|
|
180
|
+
payload_type = PayloadRegistry.get_type(payload_type_name)
|
|
181
|
+
|
|
182
|
+
if payload_type is None:
|
|
183
|
+
logger.debug("Unknown payload type: %s", payload_type_name)
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
return ExecutionEvent.from_dict(data=payload, payload_type=payload_type)
|
|
187
|
+
|
|
188
|
+
def handle_publishing_event(self, event: dict) -> None:
|
|
189
|
+
"""Handle events from SubprocessWorkflowPublisher.
|
|
190
|
+
|
|
191
|
+
Processes publishing events and updates the GUI with relevant information.
|
|
192
|
+
Handles PublishWorkflowProgressEvent for progress bar updates, and
|
|
193
|
+
PublishWorkflowResultSuccess/Failure for completion status.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
event: The event dictionary from the subprocess publisher
|
|
197
|
+
"""
|
|
198
|
+
event_type = event.get("type", "unknown")
|
|
199
|
+
|
|
200
|
+
# Handle result events (success/failure)
|
|
201
|
+
if event_type in ("success_result", "failure_result"):
|
|
202
|
+
self._handle_publishing_result_event(event)
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
# Handle execution events (progress updates)
|
|
206
|
+
ex_event = self._parse_execution_event(event)
|
|
207
|
+
if ex_event is None:
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
if isinstance(ex_event.payload, PublishWorkflowProgressEvent):
|
|
211
|
+
# Update progress bar (convert from 0-100 to 0.0-1.0)
|
|
212
|
+
progress_value = min(1.0, max(0.0, ex_event.payload.progress / 100.0))
|
|
213
|
+
self._node.publish_update_to_parameter("publishing_progress", progress_value)
|
|
214
|
+
|
|
215
|
+
# Also append a user-friendly message if provided
|
|
216
|
+
if ex_event.payload.message:
|
|
217
|
+
self.append_event(f"Publishing: {ex_event.payload.message} ({ex_event.payload.progress:.0f}%)")
|
|
218
|
+
|
|
219
|
+
def _handle_publishing_result_event(self, event: dict) -> None:
|
|
220
|
+
"""Handle publishing result events (success/failure).
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
event: The event dictionary containing the result
|
|
224
|
+
"""
|
|
225
|
+
payload = event.get("payload", {})
|
|
226
|
+
result_type_name = payload.get("result_type", "")
|
|
227
|
+
result_payload_type = PayloadRegistry.get_type(result_type_name)
|
|
228
|
+
|
|
229
|
+
if result_payload_type is None:
|
|
230
|
+
logger.debug("Unknown result type: %s", result_type_name)
|
|
231
|
+
return
|
|
232
|
+
|
|
233
|
+
result_data = payload.get("result", {})
|
|
234
|
+
|
|
235
|
+
if result_payload_type == PublishWorkflowResultSuccess:
|
|
236
|
+
event_result = EventResultSuccess.from_dict(
|
|
237
|
+
data=payload, req_payload_type=PublishWorkflowRequest, res_payload_type=PublishWorkflowResultSuccess
|
|
238
|
+
)
|
|
239
|
+
if isinstance(event_result.result, PublishWorkflowResultSuccess):
|
|
240
|
+
publish_workflow_result_success = event_result.result
|
|
241
|
+
target_link = (
|
|
242
|
+
publish_workflow_result_success.metadata.get("publish_target_link")
|
|
243
|
+
if publish_workflow_result_success.metadata
|
|
244
|
+
else None
|
|
245
|
+
)
|
|
246
|
+
if target_link:
|
|
247
|
+
self._node.set_parameter_value("publishing_target_link", target_link)
|
|
248
|
+
|
|
249
|
+
elif result_payload_type == PublishWorkflowResultFailure:
|
|
250
|
+
result_details = result_data.get("result_details", "Unknown error")
|
|
251
|
+
self.append_event(f"Publishing failed: {result_details}")
|
|
252
|
+
|
|
253
|
+
def handle_execution_event(self, event: dict) -> None:
|
|
254
|
+
"""Handle events from SubprocessWorkflowExecutor.
|
|
255
|
+
|
|
256
|
+
Processes execution events and updates the GUI with relevant information.
|
|
257
|
+
Filters to only display relevant events with formatted messages.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
event: The event dictionary from the subprocess executor
|
|
261
|
+
"""
|
|
262
|
+
ex_event = self._parse_execution_event(event)
|
|
263
|
+
|
|
264
|
+
if ex_event is None:
|
|
265
|
+
return
|
|
266
|
+
|
|
267
|
+
formatted_message = self._format_execution_event(ex_event)
|
|
268
|
+
if formatted_message is not None:
|
|
269
|
+
self.append_event(formatted_message)
|
|
270
|
+
|
|
271
|
+
def _format_execution_event(self, ex_event: ExecutionEvent) -> str | None:
|
|
272
|
+
"""Format an execution event into a user-friendly message.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
ex_event: The parsed ExecutionEvent
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
A formatted string message, or None if the event should be filtered out
|
|
279
|
+
"""
|
|
280
|
+
payload = ex_event.payload
|
|
281
|
+
payload_type = type(payload)
|
|
282
|
+
|
|
283
|
+
# Map payload types to their formatting functions
|
|
284
|
+
formatters = {
|
|
285
|
+
NodeStartProcessEvent: self._format_node_start_process,
|
|
286
|
+
NodeFinishProcessEvent: self._format_node_finish_process,
|
|
287
|
+
NodeResolvedEvent: self._format_node_resolved,
|
|
288
|
+
ControlFlowResolvedEvent: self._format_control_flow_resolved,
|
|
289
|
+
ControlFlowCancelledEvent: self._format_control_flow_cancelled,
|
|
290
|
+
GriptapeEvent: self._format_griptape_event,
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
formatter = formatters.get(payload_type)
|
|
294
|
+
if formatter is None:
|
|
295
|
+
# Filter out other event types
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
return formatter(payload)
|
|
299
|
+
|
|
300
|
+
def _format_node_start_process(self, payload: NodeStartProcessEvent) -> str:
|
|
301
|
+
"""Format a NodeStartProcessEvent."""
|
|
302
|
+
return f"Starting: {payload.node_name}"
|
|
303
|
+
|
|
304
|
+
def _format_node_finish_process(self, payload: NodeFinishProcessEvent) -> str:
|
|
305
|
+
"""Format a NodeFinishProcessEvent."""
|
|
306
|
+
return f"Finished: {payload.node_name}"
|
|
307
|
+
|
|
308
|
+
def _format_node_resolved(self, payload: NodeResolvedEvent) -> str:
|
|
309
|
+
"""Format a NodeResolvedEvent."""
|
|
310
|
+
return f"Resolved: {payload.node_name}"
|
|
311
|
+
|
|
312
|
+
def _format_control_flow_resolved(self, payload: ControlFlowResolvedEvent) -> str:
|
|
313
|
+
"""Format a ControlFlowResolvedEvent."""
|
|
314
|
+
return f"Flow completed: {payload.end_node_name}"
|
|
315
|
+
|
|
316
|
+
def _format_control_flow_cancelled(self, payload: ControlFlowCancelledEvent) -> str:
|
|
317
|
+
"""Format a ControlFlowCancelledEvent."""
|
|
318
|
+
details = payload.result_details or "Unknown error"
|
|
319
|
+
return f"Flow cancelled: {details}"
|
|
320
|
+
|
|
321
|
+
def _format_griptape_event(self, payload: GriptapeEvent) -> str | None:
|
|
322
|
+
"""Format a GriptapeEvent (progress event).
|
|
323
|
+
|
|
324
|
+
Only formats events for the 'result_details' parameter, filtering out others.
|
|
325
|
+
"""
|
|
326
|
+
if payload.parameter_name != "result_details":
|
|
327
|
+
return None
|
|
328
|
+
|
|
329
|
+
return f"{payload.node_name}: {payload.value}"
|
|
@@ -4,6 +4,7 @@ from collections.abc import Callable
|
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
6
|
from griptape_nodes.exe_types.core_types import Parameter, ParameterMode, Trait
|
|
7
|
+
from griptape_nodes.utils.artifact_normalization import normalize_artifact_input
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class ParameterAudio(Parameter):
|
|
@@ -24,7 +25,7 @@ class ParameterAudio(Parameter):
|
|
|
24
25
|
param.pulse_on_run = True # Change UI options at runtime
|
|
25
26
|
"""
|
|
26
27
|
|
|
27
|
-
def __init__( # noqa: PLR0913
|
|
28
|
+
def __init__( # noqa: C901, PLR0913
|
|
28
29
|
self,
|
|
29
30
|
name: str,
|
|
30
31
|
tooltip: str | None = None,
|
|
@@ -122,6 +123,20 @@ class ParameterAudio(Parameter):
|
|
|
122
123
|
else:
|
|
123
124
|
final_input_types = ["AudioUrlArtifact"]
|
|
124
125
|
|
|
126
|
+
# Add automatic converter to normalize string inputs to AudioUrlArtifact
|
|
127
|
+
# This allows ParameterAudio to automatically handle file paths and localhost URLs
|
|
128
|
+
audio_converters = list(converters) if converters else []
|
|
129
|
+
if accept_any:
|
|
130
|
+
# Create a converter function that uses normalize_artifact_input with AudioUrlArtifact
|
|
131
|
+
def _normalize_audio(value: Any) -> Any:
|
|
132
|
+
try:
|
|
133
|
+
from griptape.artifacts import AudioUrlArtifact
|
|
134
|
+
except ImportError:
|
|
135
|
+
return value
|
|
136
|
+
return normalize_artifact_input(value, AudioUrlArtifact)
|
|
137
|
+
|
|
138
|
+
audio_converters.insert(0, _normalize_audio)
|
|
139
|
+
|
|
125
140
|
# Call parent with explicit parameters, following ControlParameter pattern
|
|
126
141
|
super().__init__(
|
|
127
142
|
name=name,
|
|
@@ -135,7 +150,7 @@ class ParameterAudio(Parameter):
|
|
|
135
150
|
tooltip_as_output=tooltip_as_output,
|
|
136
151
|
allowed_modes=allowed_modes,
|
|
137
152
|
traits=traits,
|
|
138
|
-
converters=
|
|
153
|
+
converters=audio_converters,
|
|
139
154
|
validators=validators,
|
|
140
155
|
ui_options=ui_options,
|
|
141
156
|
hide=hide,
|
|
@@ -43,8 +43,8 @@ class ParameterFloat(ParameterNumber):
|
|
|
43
43
|
ui_options: dict | None = None,
|
|
44
44
|
step: float | None = None,
|
|
45
45
|
slider: bool = False,
|
|
46
|
-
min_val: float =
|
|
47
|
-
max_val: float =
|
|
46
|
+
min_val: float | None = None,
|
|
47
|
+
max_val: float | None = None,
|
|
48
48
|
validate_min_max: bool = False,
|
|
49
49
|
accept_any: bool = True,
|
|
50
50
|
hide: bool | None = None,
|
|
@@ -77,8 +77,8 @@ class ParameterFloat(ParameterNumber):
|
|
|
77
77
|
ui_options: Dictionary of UI options
|
|
78
78
|
step: Step size for numeric input controls
|
|
79
79
|
slider: Whether to use slider trait
|
|
80
|
-
min_val: Minimum value for constraints
|
|
81
|
-
max_val: Maximum value for constraints
|
|
80
|
+
min_val: Minimum value for constraints (None to disable constraints)
|
|
81
|
+
max_val: Maximum value for constraints (None to disable constraints)
|
|
82
82
|
validate_min_max: Whether to validate min/max with error
|
|
83
83
|
accept_any: Whether to accept any input type and convert to float (default: True)
|
|
84
84
|
hide: Whether to hide the entire parameter
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
from collections.abc import Callable
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
+
from griptape.artifacts import ImageArtifact, ImageUrlArtifact
|
|
7
|
+
|
|
6
8
|
from griptape_nodes.exe_types.core_types import Parameter, ParameterMode, Trait
|
|
9
|
+
from griptape_nodes.utils.artifact_normalization import normalize_artifact_input
|
|
7
10
|
|
|
8
11
|
|
|
9
12
|
class ParameterImage(Parameter):
|
|
@@ -122,6 +125,16 @@ class ParameterImage(Parameter):
|
|
|
122
125
|
else:
|
|
123
126
|
final_input_types = ["ImageUrlArtifact"]
|
|
124
127
|
|
|
128
|
+
# Add automatic converter to normalize string inputs to ImageUrlArtifact
|
|
129
|
+
# This allows ParameterImage to automatically handle file paths and localhost URLs
|
|
130
|
+
image_converters = list(converters) if converters else []
|
|
131
|
+
if accept_any:
|
|
132
|
+
# Create a converter function that uses normalize_artifact_input with ImageUrlArtifact
|
|
133
|
+
def _normalize_image(value: Any) -> Any:
|
|
134
|
+
return normalize_artifact_input(value, ImageUrlArtifact, accepted_types=(ImageArtifact,))
|
|
135
|
+
|
|
136
|
+
image_converters.insert(0, _normalize_image)
|
|
137
|
+
|
|
125
138
|
# Call parent with explicit parameters, following ControlParameter pattern
|
|
126
139
|
super().__init__(
|
|
127
140
|
name=name,
|
|
@@ -135,7 +148,7 @@ class ParameterImage(Parameter):
|
|
|
135
148
|
tooltip_as_output=tooltip_as_output,
|
|
136
149
|
allowed_modes=allowed_modes,
|
|
137
150
|
traits=traits,
|
|
138
|
-
converters=
|
|
151
|
+
converters=image_converters,
|
|
139
152
|
validators=validators,
|
|
140
153
|
ui_options=ui_options,
|
|
141
154
|
hide=hide,
|