griptape-nodes 0.70.1__py3-none-any.whl → 0.71.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/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/subprocess_workflow_executor.py +13 -117
- 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 +45 -25
- griptape_nodes/common/node_executor.py +60 -13
- griptape_nodes/exe_types/base_iterative_nodes.py +1 -1
- griptape_nodes/exe_types/node_groups/subflow_node_group.py +18 -0
- griptape_nodes/exe_types/param_components/log_parameter.py +1 -2
- 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_image.py +14 -1
- griptape_nodes/exe_types/param_types/parameter_number.py +16 -22
- 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/retained_mode/managers/os_manager.py +1 -1
- griptape_nodes/traits/clamp.py +9 -52
- griptape_nodes/utils/artifact_normalization.py +245 -0
- griptape_nodes/utils/image_preview.py +27 -0
- {griptape_nodes-0.70.1.dist-info → griptape_nodes-0.71.0.dist-info}/METADATA +1 -1
- {griptape_nodes-0.70.1.dist-info → griptape_nodes-0.71.0.dist-info}/RECORD +28 -22
- {griptape_nodes-0.70.1.dist-info → griptape_nodes-0.71.0.dist-info}/WHEEL +1 -1
- {griptape_nodes-0.70.1.dist-info → griptape_nodes-0.71.0.dist-info}/entry_points.txt +0 -0
|
@@ -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,
|
|
@@ -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,
|
|
@@ -235,6 +235,12 @@ class ParameterNumber(Parameter):
|
|
|
235
235
|
validate_min_max: Whether to validate min/max with error
|
|
236
236
|
"""
|
|
237
237
|
# Validation rules
|
|
238
|
+
if min_val is not None and max_val is None:
|
|
239
|
+
msg = f"{name}: If min_val is provided, max_val must also be provided"
|
|
240
|
+
raise ValueError(msg)
|
|
241
|
+
if max_val is not None and min_val is None:
|
|
242
|
+
msg = f"{name}: If max_val is provided, min_val must also be provided"
|
|
243
|
+
raise ValueError(msg)
|
|
238
244
|
if slider and (min_val is None or max_val is None):
|
|
239
245
|
msg = f"{name}: If slider is True, both min_val and max_val must be provided"
|
|
240
246
|
raise ValueError(msg)
|
|
@@ -253,7 +259,7 @@ class ParameterNumber(Parameter):
|
|
|
253
259
|
traits.add(Slider(min_val=min_val, max_val=max_val))
|
|
254
260
|
elif validate_min_max and min_val is not None and max_val is not None:
|
|
255
261
|
traits.add(MinMax(min_val=min_val, max_val=max_val))
|
|
256
|
-
elif min_val is not None
|
|
262
|
+
elif min_val is not None and max_val is not None:
|
|
257
263
|
traits.add(Clamp(min_val=min_val, max_val=max_val))
|
|
258
264
|
|
|
259
265
|
# Store traits for later use
|
|
@@ -298,6 +304,9 @@ class ParameterNumber(Parameter):
|
|
|
298
304
|
@min_val.setter
|
|
299
305
|
def min_val(self, value: float | None) -> None:
|
|
300
306
|
"""Set minimum value and update constraint traits."""
|
|
307
|
+
if value is not None and self.max_val is None:
|
|
308
|
+
msg = f"{self.name}: Cannot set min_val without max_val"
|
|
309
|
+
raise ValueError(msg)
|
|
301
310
|
self._min_val = value
|
|
302
311
|
self._update_constraint_traits()
|
|
303
312
|
|
|
@@ -312,6 +321,9 @@ class ParameterNumber(Parameter):
|
|
|
312
321
|
@max_val.setter
|
|
313
322
|
def max_val(self, value: float | None) -> None:
|
|
314
323
|
"""Set maximum value and update constraint traits."""
|
|
324
|
+
if value is not None and self.min_val is None:
|
|
325
|
+
msg = f"{self.name}: Cannot set max_val without min_val"
|
|
326
|
+
raise ValueError(msg)
|
|
315
327
|
self._max_val = value
|
|
316
328
|
self._update_constraint_traits()
|
|
317
329
|
|
|
@@ -355,35 +367,17 @@ class ParameterNumber(Parameter):
|
|
|
355
367
|
min_val = getattr(self, "_min_val", None)
|
|
356
368
|
max_val = getattr(self, "_max_val", None)
|
|
357
369
|
|
|
358
|
-
if min_val is None
|
|
370
|
+
if min_val is None or max_val is None:
|
|
359
371
|
self._remove_constraint_traits()
|
|
360
372
|
return
|
|
361
373
|
|
|
362
|
-
if self.slider and (min_val is None or max_val is None):
|
|
363
|
-
msg = f"{self.name}: Cannot enable slider without min_val and max_val"
|
|
364
|
-
raise ValueError(msg)
|
|
365
|
-
|
|
366
|
-
if self.validate_min_max and (min_val is None or max_val is None):
|
|
367
|
-
msg = f"{self.name}: Cannot enable validate_min_max without min_val and max_val"
|
|
368
|
-
raise ValueError(msg)
|
|
369
|
-
|
|
370
374
|
# Determine which trait to use based on current state
|
|
371
375
|
if self.slider:
|
|
372
|
-
# Checked above: both min_val and max_val are available when slider is enabled.
|
|
373
|
-
# Type narrowing: assign to variables after None check so type checker understands
|
|
374
|
-
min_val_float: float = min_val # type: ignore[assignment]
|
|
375
|
-
max_val_float: float = max_val # type: ignore[assignment]
|
|
376
|
-
# Python will naturally coerce int to float if needed (no precision loss for reasonable values)
|
|
377
376
|
self._remove_constraint_traits()
|
|
378
|
-
self.add_trait(Slider(min_val=
|
|
377
|
+
self.add_trait(Slider(min_val=min_val, max_val=max_val))
|
|
379
378
|
elif self.validate_min_max:
|
|
380
|
-
# Checked above: both min_val and max_val are available when validate_min_max is enabled.
|
|
381
|
-
# Type narrowing: assign to variables after None check so type checker understands
|
|
382
|
-
min_val_float: float = min_val # type: ignore[assignment]
|
|
383
|
-
max_val_float: float = max_val # type: ignore[assignment]
|
|
384
|
-
# Python will naturally coerce int to float if needed (no precision loss for reasonable values)
|
|
385
379
|
self._remove_constraint_traits()
|
|
386
|
-
self.add_trait(MinMax(min_val=
|
|
380
|
+
self.add_trait(MinMax(min_val=min_val, max_val=max_val))
|
|
387
381
|
else:
|
|
388
382
|
self._remove_constraint_traits()
|
|
389
383
|
self.add_trait(Clamp(min_val=min_val, max_val=max_val))
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
from collections.abc import Callable
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
+
from griptape_nodes_library.three_d.three_d_artifact import ThreeDUrlArtifact # pyright: ignore[reportMissingImports]
|
|
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 Parameter3D(Parameter):
|
|
@@ -117,6 +120,16 @@ class Parameter3D(Parameter):
|
|
|
117
120
|
else:
|
|
118
121
|
final_input_types = ["ThreeDUrlArtifact"]
|
|
119
122
|
|
|
123
|
+
# Add automatic converter to normalize string inputs to ThreeDUrlArtifact
|
|
124
|
+
# This allows Parameter3D to automatically handle file paths and localhost URLs
|
|
125
|
+
three_d_converters = list(converters) if converters else []
|
|
126
|
+
if accept_any:
|
|
127
|
+
# Create a converter function that uses normalize_artifact_input with ThreeDUrlArtifact
|
|
128
|
+
def _normalize_three_d(value: Any) -> Any:
|
|
129
|
+
return normalize_artifact_input(value, ThreeDUrlArtifact)
|
|
130
|
+
|
|
131
|
+
three_d_converters.insert(0, _normalize_three_d)
|
|
132
|
+
|
|
120
133
|
# Call parent with explicit parameters, following ControlParameter pattern
|
|
121
134
|
super().__init__(
|
|
122
135
|
name=name,
|
|
@@ -130,7 +143,7 @@ class Parameter3D(Parameter):
|
|
|
130
143
|
tooltip_as_output=tooltip_as_output,
|
|
131
144
|
allowed_modes=allowed_modes,
|
|
132
145
|
traits=traits,
|
|
133
|
-
converters=
|
|
146
|
+
converters=three_d_converters,
|
|
134
147
|
validators=validators,
|
|
135
148
|
ui_options=ui_options,
|
|
136
149
|
hide=hide,
|
|
@@ -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 ParameterVideo(Parameter):
|
|
@@ -24,7 +25,7 @@ class ParameterVideo(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 ParameterVideo(Parameter):
|
|
|
122
123
|
else:
|
|
123
124
|
final_input_types = ["VideoUrlArtifact"]
|
|
124
125
|
|
|
126
|
+
# Add automatic converter to normalize string inputs to VideoUrlArtifact
|
|
127
|
+
# This allows ParameterVideo to automatically handle file paths and localhost URLs
|
|
128
|
+
video_converters = list(converters) if converters else []
|
|
129
|
+
if accept_any:
|
|
130
|
+
# Create a converter function that uses normalize_artifact_input with VideoUrlArtifact
|
|
131
|
+
def _normalize_video(value: Any) -> Any:
|
|
132
|
+
try:
|
|
133
|
+
from griptape.artifacts import VideoUrlArtifact
|
|
134
|
+
except ImportError:
|
|
135
|
+
return value
|
|
136
|
+
return normalize_artifact_input(value, VideoUrlArtifact)
|
|
137
|
+
|
|
138
|
+
video_converters.insert(0, _normalize_video)
|
|
139
|
+
|
|
125
140
|
# Call parent with explicit parameters, following ControlParameter pattern
|
|
126
141
|
super().__init__(
|
|
127
142
|
name=name,
|
|
@@ -135,7 +150,7 @@ class ParameterVideo(Parameter):
|
|
|
135
150
|
tooltip_as_output=tooltip_as_output,
|
|
136
151
|
allowed_modes=allowed_modes,
|
|
137
152
|
traits=traits,
|
|
138
|
-
converters=
|
|
153
|
+
converters=video_converters,
|
|
139
154
|
validators=validators,
|
|
140
155
|
ui_options=ui_options,
|
|
141
156
|
hide=hide,
|
|
@@ -2033,7 +2033,7 @@ class OSManager:
|
|
|
2033
2033
|
mode=file_mode, # type: ignore[arg-type]
|
|
2034
2034
|
encoding=encoding if isinstance(content, str) else None,
|
|
2035
2035
|
timeout=0, # Non-blocking
|
|
2036
|
-
flags=portalocker.LockFlags.EXCLUSIVE,
|
|
2036
|
+
flags=portalocker.LockFlags.EXCLUSIVE | portalocker.LockFlags.NON_BLOCKING,
|
|
2037
2037
|
) as fh:
|
|
2038
2038
|
fh.write(content)
|
|
2039
2039
|
|
griptape_nodes/traits/clamp.py
CHANGED
|
@@ -11,7 +11,7 @@ class Clamp(Trait):
|
|
|
11
11
|
max: Any = 10
|
|
12
12
|
element_id: str = field(default_factory=lambda: "ClampTrait")
|
|
13
13
|
|
|
14
|
-
def __init__(self, min_val: float
|
|
14
|
+
def __init__(self, min_val: float, max_val: float) -> None:
|
|
15
15
|
super().__init__()
|
|
16
16
|
self.min = min_val
|
|
17
17
|
self.max = max_val
|
|
@@ -20,60 +20,17 @@ class Clamp(Trait):
|
|
|
20
20
|
def get_trait_keys(cls) -> list[str]:
|
|
21
21
|
return ["clamp"]
|
|
22
22
|
|
|
23
|
-
def _clamp_number(self, value: float) -> float:
|
|
24
|
-
# Keep this as a tiny helper so the converter stays readable and so we can
|
|
25
|
-
# consistently apply one-sided bounds (min-only or max-only) everywhere.
|
|
26
|
-
if self.max is not None and value > self.max:
|
|
27
|
-
value = self.max
|
|
28
|
-
if self.min is not None and value < self.min:
|
|
29
|
-
value = self.min
|
|
30
|
-
return value
|
|
31
|
-
|
|
32
|
-
def _clamp_sequence(self, value: list[Any]) -> list[Any]:
|
|
33
|
-
# Clamp historically applied to strings/lists by max length; we keep list handling
|
|
34
|
-
# explicit here so bounds semantics remain obvious.
|
|
35
|
-
if self.max is None:
|
|
36
|
-
return value
|
|
37
|
-
if len(value) <= self.max:
|
|
38
|
-
return value
|
|
39
|
-
return value[: self.max]
|
|
40
|
-
|
|
41
|
-
def _try_parse_numeric_string(self, value: str) -> float | None:
|
|
42
|
-
# Trait converters run BEFORE parameter-level converters (e.g. ParameterInt/Float
|
|
43
|
-
# parsing). That means UI inputs often arrive as strings here; to make min/max
|
|
44
|
-
# clamping actually work with typical UI inputs, we parse numeric strings when
|
|
45
|
-
# bounds are configured.
|
|
46
|
-
if self.min is None and self.max is None:
|
|
47
|
-
return None
|
|
48
|
-
|
|
49
|
-
stripped = value.strip()
|
|
50
|
-
if not stripped:
|
|
51
|
-
return None
|
|
52
|
-
|
|
53
|
-
try:
|
|
54
|
-
return float(stripped)
|
|
55
|
-
except ValueError:
|
|
56
|
-
return None
|
|
57
|
-
|
|
58
23
|
def converters_for_trait(self) -> list[Callable]:
|
|
59
24
|
def clamp(value: Any) -> Any:
|
|
60
|
-
if isinstance(value, list):
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
parsed = self._try_parse_numeric_string(value)
|
|
65
|
-
if parsed is not None:
|
|
66
|
-
return self._clamp_number(parsed)
|
|
67
|
-
|
|
68
|
-
# If it's not a numeric string (or no bounds configured), preserve the
|
|
69
|
-
# historical string behavior: max length clamping only.
|
|
70
|
-
if self.max is None or len(value) <= self.max:
|
|
71
|
-
return value
|
|
72
|
-
return value[: self.max]
|
|
73
|
-
|
|
25
|
+
if isinstance(value, (str, list)):
|
|
26
|
+
if len(value) > self.max:
|
|
27
|
+
return value[: self.max]
|
|
28
|
+
return value
|
|
74
29
|
if isinstance(value, (int, float)):
|
|
75
|
-
|
|
76
|
-
|
|
30
|
+
if value > self.max:
|
|
31
|
+
return self.max
|
|
32
|
+
if value < self.min:
|
|
33
|
+
return self.min
|
|
77
34
|
return value
|
|
78
35
|
|
|
79
36
|
return [clamp]
|