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.
Files changed (67) hide show
  1. griptape_nodes/api_client/client.py +8 -5
  2. griptape_nodes/app/app.py +4 -0
  3. griptape_nodes/bootstrap/utils/python_subprocess_executor.py +48 -9
  4. griptape_nodes/bootstrap/utils/subprocess_websocket_base.py +88 -0
  5. griptape_nodes/bootstrap/utils/subprocess_websocket_listener.py +126 -0
  6. griptape_nodes/bootstrap/utils/subprocess_websocket_sender.py +121 -0
  7. griptape_nodes/bootstrap/workflow_executors/local_session_workflow_executor.py +17 -170
  8. griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +10 -1
  9. griptape_nodes/bootstrap/workflow_executors/subprocess_workflow_executor.py +13 -117
  10. griptape_nodes/bootstrap/workflow_executors/utils/subprocess_script.py +4 -0
  11. griptape_nodes/bootstrap/workflow_publishers/local_session_workflow_publisher.py +206 -0
  12. griptape_nodes/bootstrap/workflow_publishers/subprocess_workflow_publisher.py +22 -3
  13. griptape_nodes/bootstrap/workflow_publishers/utils/subprocess_script.py +49 -25
  14. griptape_nodes/common/node_executor.py +61 -14
  15. griptape_nodes/drivers/image_metadata/__init__.py +21 -0
  16. griptape_nodes/drivers/image_metadata/base_image_metadata_driver.py +63 -0
  17. griptape_nodes/drivers/image_metadata/exif_metadata_driver.py +218 -0
  18. griptape_nodes/drivers/image_metadata/image_metadata_driver_registry.py +55 -0
  19. griptape_nodes/drivers/image_metadata/png_metadata_driver.py +71 -0
  20. griptape_nodes/drivers/storage/base_storage_driver.py +32 -0
  21. griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +384 -10
  22. griptape_nodes/drivers/storage/local_storage_driver.py +65 -4
  23. griptape_nodes/drivers/thread_storage/local_thread_storage_driver.py +1 -0
  24. griptape_nodes/exe_types/base_iterative_nodes.py +1 -1
  25. griptape_nodes/exe_types/node_groups/base_node_group.py +3 -0
  26. griptape_nodes/exe_types/node_groups/subflow_node_group.py +18 -0
  27. griptape_nodes/exe_types/node_types.py +13 -0
  28. griptape_nodes/exe_types/param_components/log_parameter.py +4 -4
  29. griptape_nodes/exe_types/param_components/subflow_execution_component.py +329 -0
  30. griptape_nodes/exe_types/param_types/parameter_audio.py +17 -2
  31. griptape_nodes/exe_types/param_types/parameter_float.py +4 -4
  32. griptape_nodes/exe_types/param_types/parameter_image.py +14 -1
  33. griptape_nodes/exe_types/param_types/parameter_int.py +4 -4
  34. griptape_nodes/exe_types/param_types/parameter_number.py +12 -14
  35. griptape_nodes/exe_types/param_types/parameter_three_d.py +14 -1
  36. griptape_nodes/exe_types/param_types/parameter_video.py +17 -2
  37. griptape_nodes/node_library/workflow_registry.py +5 -8
  38. griptape_nodes/retained_mode/events/app_events.py +1 -0
  39. griptape_nodes/retained_mode/events/base_events.py +42 -26
  40. griptape_nodes/retained_mode/events/flow_events.py +67 -0
  41. griptape_nodes/retained_mode/events/library_events.py +1 -1
  42. griptape_nodes/retained_mode/events/node_events.py +1 -0
  43. griptape_nodes/retained_mode/events/os_events.py +22 -0
  44. griptape_nodes/retained_mode/events/static_file_events.py +28 -4
  45. griptape_nodes/retained_mode/managers/flow_manager.py +134 -0
  46. griptape_nodes/retained_mode/managers/image_metadata_injector.py +339 -0
  47. griptape_nodes/retained_mode/managers/library_manager.py +71 -41
  48. griptape_nodes/retained_mode/managers/model_manager.py +1 -0
  49. griptape_nodes/retained_mode/managers/node_manager.py +8 -5
  50. griptape_nodes/retained_mode/managers/os_manager.py +270 -33
  51. griptape_nodes/retained_mode/managers/project_manager.py +3 -7
  52. griptape_nodes/retained_mode/managers/session_manager.py +1 -0
  53. griptape_nodes/retained_mode/managers/settings.py +5 -0
  54. griptape_nodes/retained_mode/managers/static_files_manager.py +83 -17
  55. griptape_nodes/retained_mode/managers/workflow_manager.py +71 -41
  56. griptape_nodes/servers/static.py +31 -0
  57. griptape_nodes/utils/__init__.py +9 -1
  58. griptape_nodes/utils/artifact_normalization.py +245 -0
  59. griptape_nodes/utils/file_utils.py +13 -13
  60. griptape_nodes/utils/http_file_patch.py +613 -0
  61. griptape_nodes/utils/image_preview.py +27 -0
  62. griptape_nodes/utils/path_utils.py +58 -0
  63. griptape_nodes/utils/url_utils.py +106 -0
  64. {griptape_nodes-0.70.1.dist-info → griptape_nodes-0.72.0.dist-info}/METADATA +2 -1
  65. {griptape_nodes-0.70.1.dist-info → griptape_nodes-0.72.0.dist-info}/RECORD +67 -52
  66. {griptape_nodes-0.70.1.dist-info → griptape_nodes-0.72.0.dist-info}/WHEEL +1 -1
  67. {griptape_nodes-0.70.1.dist-info → griptape_nodes-0.72.0.dist-info}/entry_points.txt +0 -0
@@ -43,8 +43,8 @@ class ParameterInt(ParameterNumber):
43
43
  ui_options: dict | None = None,
44
44
  step: int | None = None,
45
45
  slider: bool = False,
46
- min_val: float = 0,
47
- max_val: float = 100,
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 ParameterInt(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 integer (default: True)
84
84
  hide: Whether to hide the entire parameter
@@ -37,8 +37,8 @@ class ParameterNumber(Parameter):
37
37
  ui_options: dict | None = None,
38
38
  step: float | None = None,
39
39
  slider: bool = False,
40
- min_val: float = 0,
41
- max_val: float = 100,
40
+ min_val: float | None = None,
41
+ max_val: float | None = None,
42
42
  validate_min_max: bool = False,
43
43
  accept_any: bool = True,
44
44
  hide: bool | None = None,
@@ -74,8 +74,8 @@ class ParameterNumber(Parameter):
74
74
  ui_options: Dictionary of UI options
75
75
  step: Step size for numeric input controls
76
76
  slider: Whether to use slider trait
77
- min_val: Minimum value for constraints
78
- max_val: Maximum value for constraints
77
+ min_val: Minimum value for constraints (None to disable constraints)
78
+ max_val: Maximum value for constraints (None to disable constraints)
79
79
  validate_min_max: Whether to validate min/max with error
80
80
  accept_any: Whether to accept any input type and convert to number (default: True)
81
81
  hide: Whether to hide the entire parameter
@@ -220,8 +220,8 @@ class ParameterNumber(Parameter):
220
220
  traits: set[type[Trait] | Trait] | None,
221
221
  *,
222
222
  slider: bool,
223
- min_val: float,
224
- max_val: float,
223
+ min_val: float | None,
224
+ max_val: float | None,
225
225
  validate_min_max: bool,
226
226
  ) -> None:
227
227
  """Set up constraint traits based on parameters.
@@ -272,14 +272,14 @@ class ParameterNumber(Parameter):
272
272
  def slider(self, value: bool) -> None:
273
273
  """Set slider trait."""
274
274
  if value:
275
- if not hasattr(self, "_constraint_traits") or not self._constraint_traits:
275
+ # Get min/max from stored values
276
+ min_val = getattr(self, "_min_val", None)
277
+ max_val = getattr(self, "_max_val", None)
278
+ if min_val is None or max_val is None:
276
279
  msg = f"{self.name}: Cannot enable slider without min_val and max_val"
277
280
  raise ValueError(msg)
278
281
  # Find existing constraint traits and replace with slider
279
282
  self._remove_constraint_traits()
280
- # Get min/max from existing traits or use defaults
281
- min_val = getattr(self, "_min_val", 0)
282
- max_val = getattr(self, "_max_val", 100)
283
283
  self.add_trait(Slider(min_val=min_val, max_val=max_val))
284
284
  else:
285
285
  # Remove slider trait
@@ -325,14 +325,12 @@ class ParameterNumber(Parameter):
325
325
  """Set MinMax validation."""
326
326
  if value:
327
327
  # Check if we have stored min/max values
328
- min_val = getattr(self, "_min_val", None)
329
- max_val = getattr(self, "_max_val", None)
330
- if min_val is None or max_val is None:
328
+ if self.min_val is None or self.max_val is None:
331
329
  msg = f"{self.name}: Cannot enable validate_min_max without min_val and max_val"
332
330
  raise ValueError(msg)
333
331
  # Replace existing constraint traits with MinMax
334
332
  self._remove_constraint_traits()
335
- self.add_trait(MinMax(min_val=min_val, max_val=max_val))
333
+ self.add_trait(MinMax(min_val=self.min_val, max_val=self.max_val))
336
334
  else:
337
335
  # Remove MinMax trait and replace with Clamp if we have min/max
338
336
  min_max_traits = self.find_elements_by_type(MinMax)
@@ -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,
@@ -12,6 +12,7 @@ from griptape_nodes.node_library.library_registry import (
12
12
  LibraryNameAndVersion, # noqa: TC001 (putting this into type checking causes it to not be defined)
13
13
  )
14
14
  from griptape_nodes.utils.metaclasses import SingletonMeta
15
+ from griptape_nodes.utils.path_utils import resolve_workspace_path
15
16
 
16
17
  logger = logging.getLogger("griptape_nodes")
17
18
 
@@ -45,7 +46,7 @@ class WorkflowShape(BaseModel):
45
46
 
46
47
 
47
48
  class WorkflowMetadata(BaseModel):
48
- LATEST_SCHEMA_VERSION: ClassVar[str] = "0.14.0"
49
+ LATEST_SCHEMA_VERSION: ClassVar[str] = "0.15.0"
49
50
 
50
51
  name: str
51
52
  schema_version: str
@@ -175,15 +176,11 @@ class WorkflowRegistry(metaclass=SingletonMeta):
175
176
  def get_complete_file_path(cls, relative_file_path: str) -> str:
176
177
  from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
177
178
 
178
- # If the path is already absolute, return it as-is
179
- if Path(relative_file_path).is_absolute():
180
- return relative_file_path
181
-
182
- # Otherwise, resolve it relative to the workspace
179
+ # Resolve path using utility function
183
180
  config_mgr = GriptapeNodes.ConfigManager()
184
181
  workspace_path = config_mgr.workspace_path
185
- complete_file_path = workspace_path / relative_file_path
186
- return str(complete_file_path)
182
+ resolved_path = resolve_workspace_path(Path(relative_file_path), workspace_path)
183
+ return str(resolved_path)
187
184
 
188
185
  @classmethod
189
186
  def delete_workflow_by_name(cls, name: str) -> Workflow:
@@ -113,6 +113,7 @@ class AppInitializationComplete(AppPayload):
113
113
  libraries_to_register: list[str] = field(default_factory=list)
114
114
  workflows_to_register: list[str] = field(default_factory=list)
115
115
  models_to_download: list[str] = field(default_factory=list)
116
+ skip_library_loading: bool = False
116
117
 
117
118
 
118
119
  @dataclass
@@ -16,6 +16,31 @@ if TYPE_CHECKING:
16
16
  import builtins
17
17
 
18
18
 
19
+ def default_json_encoder(obj: Any) -> Any:
20
+ """Custom JSON encoder for various object types.
21
+
22
+ Attempts the following encodings in order:
23
+ 1. If the object is a SerializableMixin, call to_dict()
24
+ 2. If the object is a Pydantic model, call model_dump()
25
+ 3. Attempt to use the default JSON encoder
26
+ 4. If all else fails, return the string representation of the object
27
+
28
+ Args:
29
+ obj: The object to encode
30
+
31
+ Returns:
32
+ JSON-serializable representation of the object
33
+ """
34
+ if isinstance(obj, SerializableMixin):
35
+ return obj.to_dict()
36
+ if isinstance(obj, BaseModel):
37
+ return obj.model_dump()
38
+ try:
39
+ return json.JSONEncoder().default(obj)
40
+ except TypeError:
41
+ return str(obj)
42
+
43
+
19
44
  @dataclass
20
45
  class ResultDetail:
21
46
  """A single detail about an operation result, including logging level and human readable message."""
@@ -71,6 +96,22 @@ class ResultDetails:
71
96
  class Payload(ABC): # noqa: B024
72
97
  """Base class for all payload types. Customers will derive from this."""
73
98
 
99
+ def to_json(self, **kwargs) -> str:
100
+ """Serialize this payload to JSON string.
101
+
102
+ Returns:
103
+ JSON string representation of the payload
104
+ """
105
+ # Convert payload to dict
106
+ if is_dataclass(self):
107
+ payload_dict = asdict(self)
108
+ elif hasattr(self, "__dict__"):
109
+ payload_dict = self.__dict__
110
+ else:
111
+ payload_dict = str(self)
112
+
113
+ return json.dumps(payload_dict, default=default_json_encoder, **kwargs)
114
+
74
115
 
75
116
  # Request payload base class with optional request ID
76
117
  @dataclass(kw_only=True)
@@ -228,32 +269,7 @@ class BaseEvent(BaseModel, ABC):
228
269
 
229
270
  def json(self, **kwargs) -> str:
230
271
  """Serialize to JSON string."""
231
-
232
- # TODO: https://github.com/griptape-ai/griptape-nodes/issues/906
233
- def default_encoder(obj: Any) -> Any:
234
- """Custom JSON encoder for various object types.
235
-
236
- Attempts the following encodings in order:
237
- 1. If the object is a SerializableMixin, call to_dict()
238
- 2. If the object is a Pydantic model, call model_dump()
239
- 3. Attempt to use the default JSON encoder
240
- 4. If all else fails, return the string representation of the object
241
-
242
- Args:
243
- obj: The object to encode
244
-
245
-
246
- """
247
- if isinstance(obj, SerializableMixin):
248
- return obj.to_dict()
249
- if isinstance(obj, BaseModel):
250
- return obj.model_dump()
251
- try:
252
- return json.JSONEncoder().default(obj)
253
- except TypeError:
254
- return str(obj)
255
-
256
- return json.dumps(self.dict(), default=default_encoder, **kwargs)
272
+ return json.dumps(self.dict(), default=default_json_encoder, **kwargs)
257
273
 
258
274
  @abstractmethod
259
275
  def get_request(self) -> Payload:
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass, field
4
+ from enum import StrEnum
4
5
  from typing import TYPE_CHECKING, Any, NamedTuple
5
6
 
6
7
  if TYPE_CHECKING:
@@ -19,6 +20,18 @@ from griptape_nodes.retained_mode.events.base_events import (
19
20
  from griptape_nodes.retained_mode.events.payload_registry import PayloadRegistry
20
21
 
21
22
 
23
+ class FlowMetadataExtractionFailureReason(StrEnum):
24
+ """Reasons why flow metadata extraction from image failed."""
25
+
26
+ FILE_NOT_FOUND = "file_not_found"
27
+ FILE_READ_ERROR = "file_read_error"
28
+ INVALID_IMAGE_FORMAT = "invalid_image_format"
29
+ NO_FLOW_METADATA = "no_flow_metadata"
30
+ INVALID_BASE64 = "invalid_base64"
31
+ INVALID_PICKLE = "invalid_pickle"
32
+ DESERIALIZATION_FAILED = "deserialization_failed"
33
+
34
+
22
35
  @dataclass(kw_only=True)
23
36
  @PayloadRegistry.register
24
37
  class CreateFlowRequest(RequestPayload):
@@ -288,6 +301,60 @@ class DeserializeFlowFromCommandsResultFailure(ResultPayloadFailure):
288
301
  pass
289
302
 
290
303
 
304
+ @dataclass
305
+ @PayloadRegistry.register
306
+ class ExtractFlowCommandsFromImageMetadataRequest(RequestPayload):
307
+ """Extract flow commands from PNG image metadata.
308
+
309
+ This request reads a PNG image file, extracts the embedded workflow metadata
310
+ (specifically the gtn_flow_commands field), decodes it from base64, unpickles it,
311
+ and returns the SerializedFlowCommands object. Optionally, it can automatically
312
+ deserialize the flow by calling DeserializeFlowFromCommandsRequest.
313
+
314
+ Use when: Loading flow commands from an exported image, inspecting workflow
315
+ structure before deserialization, extracting flows shared as PNG files.
316
+
317
+ Args:
318
+ file_url_or_path: Path to the PNG image file (absolute or relative) or URL
319
+ deserialize: If True, automatically deserialize the flow after extraction (default: False)
320
+
321
+ Results:
322
+ ExtractFlowCommandsFromImageMetadataResultSuccess - Commands extracted (and optionally deserialized)
323
+ ExtractFlowCommandsFromImageMetadataResultFailure - Extraction or deserialization failed
324
+ """
325
+
326
+ file_url_or_path: str
327
+ deserialize: bool = False
328
+
329
+
330
+ @dataclass(kw_only=True)
331
+ @PayloadRegistry.register
332
+ class ExtractFlowCommandsFromImageMetadataResultSuccess(ResultPayloadSuccess):
333
+ """Flow commands extracted successfully from image metadata.
334
+
335
+ Args:
336
+ serialized_flow_commands: The extracted and decoded SerializedFlowCommands object
337
+ flow_name: Name of the deserialized flow (only present if deserialize=True)
338
+ node_name_mappings: Mapping from original to deserialized node names (only present if deserialize=True)
339
+ """
340
+
341
+ serialized_flow_commands: SerializedFlowCommands
342
+ flow_name: str | None = None
343
+ node_name_mappings: dict[str, str] = field(default_factory=dict)
344
+
345
+
346
+ @dataclass
347
+ @PayloadRegistry.register
348
+ class ExtractFlowCommandsFromImageMetadataResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
349
+ """Flow commands extraction failed.
350
+
351
+ Args:
352
+ file_path: The file path that was attempted (for error context)
353
+ """
354
+
355
+ file_path: str
356
+
357
+
291
358
  @dataclass
292
359
  @PayloadRegistry.register
293
360
  class GetFlowDetailsRequest(RequestPayload):
@@ -607,7 +607,7 @@ class DiscoverLibrariesRequest(RequestPayload):
607
607
  class DiscoverLibrariesResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
608
608
  """Libraries discovered successfully."""
609
609
 
610
- libraries_discovered: set[DiscoveredLibrary] # Discovered libraries with type info
610
+ libraries_discovered: list[DiscoveredLibrary] # Discovered libraries in config order
611
611
 
612
612
 
613
613
  @dataclass
@@ -565,6 +565,7 @@ class SerializeNodeToCommandsRequest(RequestPayload):
565
565
  default_factory=SerializedParameterValueTracker
566
566
  )
567
567
  use_pickling: bool = False
568
+ serialize_all_parameter_values: bool = False
568
569
 
569
570
 
570
571
  @dataclass
@@ -54,6 +54,24 @@ class FileIOFailureReason(StrEnum):
54
54
  IO_ERROR = "io_error" # Generic I/O error
55
55
  UNKNOWN = "unknown" # Unexpected error
56
56
 
57
+ # Recycle bin errors
58
+ RECYCLE_BIN_UNAVAILABLE = "recycle_bin_unavailable" # Recycle bin unavailable and behavior was RECYCLE_BIN_ONLY
59
+
60
+
61
+ class DeletionBehavior(StrEnum):
62
+ """How to handle file/directory deletion."""
63
+
64
+ PERMANENTLY_DELETE = "permanently_delete" # Permanently delete (default, current behavior)
65
+ RECYCLE_BIN_ONLY = "recycle_bin_only" # Send to recycle bin; fail if unavailable
66
+ PREFER_RECYCLE_BIN = "prefer_recycle_bin" # Try recycle bin; fall back to permanent deletion
67
+
68
+
69
+ class DeletionOutcome(StrEnum):
70
+ """The actual outcome of a deletion operation."""
71
+
72
+ PERMANENTLY_DELETED = "permanently_deleted"
73
+ SENT_TO_RECYCLE_BIN = "sent_to_recycle_bin"
74
+
57
75
 
58
76
  @dataclass
59
77
  class FileSystemEntry:
@@ -585,6 +603,7 @@ class DeleteFileRequest(RequestPayload):
585
603
  path: Path to file/directory to delete (mutually exclusive with file_entry)
586
604
  file_entry: FileSystemEntry from directory listing (mutually exclusive with path)
587
605
  workspace_only: If True, constrain to workspace directory
606
+ deletion_behavior: How to handle deletion (permanent, recycle bin only, or prefer recycle bin)
588
607
 
589
608
  Results: DeleteFileResultSuccess | DeleteFileResultFailure
590
609
  """
@@ -592,6 +611,7 @@ class DeleteFileRequest(RequestPayload):
592
611
  path: str | None = None
593
612
  file_entry: FileSystemEntry | None = None
594
613
  workspace_only: bool | None = True
614
+ deletion_behavior: DeletionBehavior = DeletionBehavior.PREFER_RECYCLE_BIN
595
615
 
596
616
 
597
617
  @dataclass
@@ -603,11 +623,13 @@ class DeleteFileResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
603
623
  deleted_path: The absolute path that was deleted (primary path)
604
624
  was_directory: Whether the deleted item was a directory
605
625
  deleted_paths: List of all paths that were deleted (for recursive deletes, includes all files/dirs)
626
+ outcome: The actual outcome of the deletion (permanently deleted or sent to recycle bin)
606
627
  """
607
628
 
608
629
  deleted_path: str
609
630
  was_directory: bool
610
631
  deleted_paths: list[str]
632
+ outcome: DeletionOutcome
611
633
 
612
634
 
613
635
  @dataclass
@@ -78,11 +78,13 @@ class CreateStaticFileUploadUrlResultSuccess(WorkflowNotAlteredMixin, ResultPayl
78
78
  url: Presigned URL for uploading the file
79
79
  headers: HTTP headers required for the upload request
80
80
  method: HTTP method to use for upload (typically PUT)
81
+ file_url: File URI (file://) for the absolute path where the file will be accessible after upload
81
82
  """
82
83
 
83
84
  url: str
84
85
  headers: dict = field(default_factory=dict)
85
86
  method: str = "PUT"
87
+ file_url: str = ""
86
88
 
87
89
 
88
90
  @dataclass
@@ -100,13 +102,13 @@ class CreateStaticFileUploadUrlResultFailure(WorkflowNotAlteredMixin, ResultPayl
100
102
  @dataclass
101
103
  @PayloadRegistry.register
102
104
  class CreateStaticFileDownloadUrlRequest(RequestPayload):
103
- """Create a presigned URL for downloading a static file via HTTP GET.
105
+ """Create a presigned URL for downloading a static file from the staticfiles directory via HTTP GET.
104
106
 
105
- Use when: Providing secure file access, implementing file sharing,
106
- enabling temporary download links, controlling file access permissions.
107
+ Use when: Providing secure file access to files in the staticfiles directory,
108
+ implementing file sharing, enabling temporary download links, controlling file access permissions.
107
109
 
108
110
  Args:
109
- file_name: Name of the file to be downloaded
111
+ file_name: Name of the file to be downloaded from the staticfiles directory
110
112
 
111
113
  Results: CreateStaticFileDownloadUrlResultSuccess (with URL) | CreateStaticFileDownloadUrlResultFailure (URL creation error)
112
114
  """
@@ -114,6 +116,26 @@ class CreateStaticFileDownloadUrlRequest(RequestPayload):
114
116
  file_name: str
115
117
 
116
118
 
119
+ @dataclass
120
+ @PayloadRegistry.register
121
+ class CreateStaticFileDownloadUrlFromPathRequest(RequestPayload):
122
+ """Create a presigned URL for downloading a file from an arbitrary path.
123
+
124
+ Use when: Need to create download URLs for files outside the staticfiles directory,
125
+ working with absolute paths, file:// URLs, or workspace-relative paths.
126
+
127
+ Args:
128
+ file_path: File path or URL. Accepts:
129
+ - file:// URLs (e.g., "file:///absolute/path/to/file.jpg")
130
+ - Absolute paths (e.g., "/absolute/path/to/file.jpg")
131
+ - Workspace-relative paths (e.g., "relative/path/to/file.jpg")
132
+
133
+ Results: CreateStaticFileDownloadUrlResultSuccess (with URL) | CreateStaticFileDownloadUrlResultFailure (URL creation error)
134
+ """
135
+
136
+ file_path: str
137
+
138
+
117
139
  @dataclass
118
140
  @PayloadRegistry.register
119
141
  class CreateStaticFileDownloadUrlResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
@@ -121,9 +143,11 @@ class CreateStaticFileDownloadUrlResultSuccess(WorkflowNotAlteredMixin, ResultPa
121
143
 
122
144
  Args:
123
145
  url: Presigned URL for downloading the file
146
+ file_url: File URI (file://) for the absolute path to the file that was used to create the download URL
124
147
  """
125
148
 
126
149
  url: str
150
+ file_url: str = ""
127
151
 
128
152
 
129
153
  @dataclass