griptape-nodes 0.70.0__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.
Files changed (26) hide show
  1. griptape_nodes/api_client/client.py +8 -5
  2. griptape_nodes/bootstrap/utils/python_subprocess_executor.py +48 -9
  3. griptape_nodes/bootstrap/utils/subprocess_websocket_base.py +88 -0
  4. griptape_nodes/bootstrap/utils/subprocess_websocket_listener.py +126 -0
  5. griptape_nodes/bootstrap/utils/subprocess_websocket_sender.py +121 -0
  6. griptape_nodes/bootstrap/workflow_executors/local_session_workflow_executor.py +17 -170
  7. griptape_nodes/bootstrap/workflow_executors/subprocess_workflow_executor.py +13 -117
  8. griptape_nodes/bootstrap/workflow_publishers/local_session_workflow_publisher.py +206 -0
  9. griptape_nodes/bootstrap/workflow_publishers/subprocess_workflow_publisher.py +22 -3
  10. griptape_nodes/bootstrap/workflow_publishers/utils/subprocess_script.py +45 -25
  11. griptape_nodes/common/node_executor.py +60 -13
  12. griptape_nodes/exe_types/base_iterative_nodes.py +1 -1
  13. griptape_nodes/exe_types/node_groups/subflow_node_group.py +18 -0
  14. griptape_nodes/exe_types/param_components/log_parameter.py +1 -2
  15. griptape_nodes/exe_types/param_components/subflow_execution_component.py +329 -0
  16. griptape_nodes/exe_types/param_types/parameter_audio.py +17 -2
  17. griptape_nodes/exe_types/param_types/parameter_image.py +14 -1
  18. griptape_nodes/exe_types/param_types/parameter_three_d.py +14 -1
  19. griptape_nodes/exe_types/param_types/parameter_video.py +17 -2
  20. griptape_nodes/retained_mode/managers/os_manager.py +1 -1
  21. griptape_nodes/utils/artifact_normalization.py +245 -0
  22. griptape_nodes/utils/image_preview.py +27 -0
  23. {griptape_nodes-0.70.0.dist-info → griptape_nodes-0.71.0.dist-info}/METADATA +1 -1
  24. {griptape_nodes-0.70.0.dist-info → griptape_nodes-0.71.0.dist-info}/RECORD +26 -20
  25. {griptape_nodes-0.70.0.dist-info → griptape_nodes-0.71.0.dist-info}/WHEEL +0 -0
  26. {griptape_nodes-0.70.0.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=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=converters,
151
+ converters=image_converters,
139
152
  validators=validators,
140
153
  ui_options=ui_options,
141
154
  hide=hide,
@@ -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=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=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