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
@@ -63,6 +63,7 @@ from griptape_nodes.retained_mode.events.project_events import (
63
63
  SetCurrentProjectResultSuccess,
64
64
  )
65
65
  from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
66
+ from griptape_nodes.utils.path_utils import resolve_workspace_path
66
67
 
67
68
  if TYPE_CHECKING:
68
69
  from griptape_nodes.retained_mode.managers.config_manager import ConfigManager
@@ -522,10 +523,7 @@ class ProjectManager:
522
523
  resolved_path = Path(resolved_string)
523
524
 
524
525
  # Make absolute path by resolving against project base directory
525
- if resolved_path.is_absolute():
526
- absolute_path = resolved_path
527
- else:
528
- absolute_path = project_info.project_base_dir / resolved_path
526
+ absolute_path = resolve_workspace_path(resolved_path, project_info.project_base_dir)
529
527
 
530
528
  return GetPathForMacroResultSuccess(
531
529
  resolved_path=resolved_path,
@@ -964,9 +962,7 @@ class ProjectManager:
964
962
  raise RuntimeError(msg) from e
965
963
 
966
964
  # Make absolute (resolve relative paths against project base directory)
967
- resolved_dir_path = Path(resolved_path_str)
968
- if not resolved_dir_path.is_absolute():
969
- resolved_dir_path = project_base_dir / resolved_dir_path
965
+ resolved_dir_path = resolve_workspace_path(Path(resolved_path_str), project_base_dir)
970
966
  # Normalize for consistent cross-platform comparison
971
967
  resolved_dir_path = os_manager.resolve_path_safely(resolved_dir_path)
972
968
 
@@ -175,6 +175,7 @@ class SessionManager:
175
175
  session_state_file = self._get_session_state_file(engine_id)
176
176
  if session_state_file.exists():
177
177
  try:
178
+ # TODO: Replace with DeleteFileRequest https://github.com/griptape-ai/griptape-nodes/issues/3765
178
179
  session_state_file.unlink()
179
180
  logger.info("Cleared all saved session data for engine: %s", engine_id)
180
181
  except OSError:
@@ -225,6 +225,11 @@ class Settings(BaseModel):
225
225
  description="Maximum number of nodes executing at a time for parallel execution.",
226
226
  )
227
227
  storage_backend: Literal["local", "gtc"] = Field(category=STORAGE, default="local")
228
+ auto_inject_workflow_metadata: bool = Field(
229
+ category=STORAGE,
230
+ default=True,
231
+ description="Automatically inject workflow metadata into saved images (JPEG, PNG, TIFF, MPO)",
232
+ )
228
233
  minimum_disk_space_gb_libraries: float = Field(
229
234
  category=SYSTEM_REQUIREMENTS,
230
235
  default=10.0,
@@ -10,9 +10,11 @@ from xdg_base_dirs import xdg_config_home
10
10
  from griptape_nodes.drivers.storage import StorageBackend
11
11
  from griptape_nodes.drivers.storage.griptape_cloud_storage_driver import GriptapeCloudStorageDriver
12
12
  from griptape_nodes.drivers.storage.local_storage_driver import LocalStorageDriver
13
+ from griptape_nodes.node_library.workflow_registry import WorkflowRegistry
13
14
  from griptape_nodes.retained_mode.events.app_events import AppInitializationComplete
14
15
  from griptape_nodes.retained_mode.events.os_events import ExistingFilePolicy
15
16
  from griptape_nodes.retained_mode.events.static_file_events import (
17
+ CreateStaticFileDownloadUrlFromPathRequest,
16
18
  CreateStaticFileDownloadUrlRequest,
17
19
  CreateStaticFileDownloadUrlResultFailure,
18
20
  CreateStaticFileDownloadUrlResultSuccess,
@@ -23,10 +25,13 @@ from griptape_nodes.retained_mode.events.static_file_events import (
23
25
  CreateStaticFileUploadUrlResultFailure,
24
26
  CreateStaticFileUploadUrlResultSuccess,
25
27
  )
28
+ from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
26
29
  from griptape_nodes.retained_mode.managers.config_manager import ConfigManager
27
30
  from griptape_nodes.retained_mode.managers.event_manager import EventManager
31
+ from griptape_nodes.retained_mode.managers.image_metadata_injector import inject_workflow_metadata_if_image
28
32
  from griptape_nodes.retained_mode.managers.secrets_manager import SecretsManager
29
- from griptape_nodes.servers.static import start_static_server
33
+ from griptape_nodes.servers.static import STATIC_SERVER_URL, start_static_server
34
+ from griptape_nodes.utils.url_utils import uri_to_path
30
35
 
31
36
  logger = logging.getLogger("griptape_nodes")
32
37
 
@@ -55,8 +60,6 @@ class StaticFilesManager:
55
60
  workspace_directory = Path(config_manager.get_config_value("workspace_directory"))
56
61
 
57
62
  # Build base URL for LocalStorageDriver from configured base URL
58
- from griptape_nodes.servers.static import STATIC_SERVER_URL
59
-
60
63
  base_url_config = config_manager.get_config_value("static_server_base_url")
61
64
  base_url = f"{base_url_config}{STATIC_SERVER_URL}"
62
65
 
@@ -95,6 +98,10 @@ class StaticFilesManager:
95
98
  event_manager.assign_manager_to_request_type(
96
99
  CreateStaticFileDownloadUrlRequest, self.on_handle_create_static_file_download_url_request
97
100
  )
101
+ event_manager.assign_manager_to_request_type(
102
+ CreateStaticFileDownloadUrlFromPathRequest,
103
+ self.on_handle_create_static_file_download_url_from_path_request,
104
+ )
98
105
  event_manager.add_listener_to_app_event(
99
106
  AppInitializationComplete,
100
107
  self.on_app_initialization_complete,
@@ -148,6 +155,7 @@ class StaticFilesManager:
148
155
  url=response["url"],
149
156
  headers=response["headers"],
150
157
  method=response["method"],
158
+ file_url=self.storage_driver.get_asset_url(Path(response["file_path"])),
151
159
  result_details="Successfully created static file upload URL",
152
160
  )
153
161
 
@@ -155,7 +163,7 @@ class StaticFilesManager:
155
163
  self,
156
164
  request: CreateStaticFileDownloadUrlRequest,
157
165
  ) -> CreateStaticFileDownloadUrlResultSuccess | CreateStaticFileDownloadUrlResultFailure:
158
- """Handle the request to create a presigned URL for downloading a static file.
166
+ """Handle the request to create a presigned URL for downloading a static file from the staticfiles directory.
159
167
 
160
168
  Args:
161
169
  request: The request object containing the file name.
@@ -163,19 +171,46 @@ class StaticFilesManager:
163
171
  Returns:
164
172
  A result object indicating success or failure.
165
173
  """
166
- file_name = request.file_name
167
-
168
174
  resolved_directory = self._get_static_files_directory()
169
- full_file_path = Path(resolved_directory) / file_name
175
+ full_file_path = Path(resolved_directory) / request.file_name
170
176
 
171
177
  try:
172
178
  url = self.storage_driver.create_signed_download_url(full_file_path)
173
179
  except Exception as e:
174
- msg = f"Failed to create presigned URL for file {file_name}: {e}"
180
+ msg = f"Failed to create presigned URL for file {request.file_name}: {e}"
175
181
  return CreateStaticFileDownloadUrlResultFailure(error=msg, result_details=msg)
176
182
 
177
183
  return CreateStaticFileDownloadUrlResultSuccess(
178
- url=url, result_details="Successfully created static file download URL"
184
+ url=url,
185
+ file_url=self.storage_driver.get_asset_url(full_file_path),
186
+ result_details="Successfully created static file download URL",
187
+ )
188
+
189
+ def on_handle_create_static_file_download_url_from_path_request(
190
+ self,
191
+ request: CreateStaticFileDownloadUrlFromPathRequest,
192
+ ) -> CreateStaticFileDownloadUrlResultSuccess | CreateStaticFileDownloadUrlResultFailure:
193
+ """Handle request to create download URL from arbitrary file path.
194
+
195
+ Args:
196
+ request: Request containing file_path parameter.
197
+
198
+ Returns:
199
+ Result with download URL or failure message.
200
+ """
201
+ full_file_path = Path(uri_to_path(request.file_path))
202
+
203
+ try:
204
+ # TODO: use the driver appropriate for the file format. i.e. If local path use LocalStorageDriver, if GTC path use GriptapeCloudStorageDriver https://github.com/griptape-ai/griptape-nodes/issues/3739
205
+ url = self.storage_driver.create_signed_download_url(full_file_path)
206
+ except Exception as e:
207
+ msg = f"Failed to create presigned URL for file {request.file_path}: {e}"
208
+ return CreateStaticFileDownloadUrlResultFailure(error=msg, result_details=msg)
209
+
210
+ return CreateStaticFileDownloadUrlResultSuccess(
211
+ url=url,
212
+ file_url=self.storage_driver.get_asset_url(full_file_path),
213
+ result_details="Successfully created static file download URL",
179
214
  )
180
215
 
181
216
  def on_app_initialization_complete(self, _payload: AppInitializationComplete) -> None:
@@ -184,7 +219,13 @@ class StaticFilesManager:
184
219
  threading.Thread(target=start_static_server, daemon=True, name="static-server").start()
185
220
 
186
221
  def save_static_file(
187
- self, data: bytes, file_name: str, existing_file_policy: ExistingFilePolicy = ExistingFilePolicy.OVERWRITE
222
+ self,
223
+ data: bytes,
224
+ file_name: str,
225
+ existing_file_policy: ExistingFilePolicy = ExistingFilePolicy.OVERWRITE,
226
+ *,
227
+ use_direct_save: bool = False,
228
+ skip_metadata_injection: bool = False,
188
229
  ) -> str:
189
230
  """Saves a static file to the workspace directory.
190
231
 
@@ -197,20 +238,48 @@ class StaticFilesManager:
197
238
  - OVERWRITE: Replace existing file content (default)
198
239
  - CREATE_NEW: Auto-generate unique filename (e.g., file_1.txt, file_2.txt)
199
240
  - FAIL: Raise FileExistsError if file exists
241
+ use_direct_save: If True, use direct storage driver save (new behavior).
242
+ If False, use presigned URL upload (legacy behavior). Defaults to False for backward compatibility.
243
+ skip_metadata_injection: If True, skip automatic workflow metadata injection.
244
+ Defaults to False. Used by nodes that handle metadata explicitly (e.g., WriteImageMetadataNode).
200
245
 
201
246
  Returns:
202
- The URL of the saved file. Note: the actual filename may differ from the requested
203
- file_name when using CREATE_NEW policy.
247
+ The URL of the saved file for UI display (with cache-busting). Note: the actual filename
248
+ may differ from the requested file_name when using CREATE_NEW policy.
204
249
 
205
250
  Raises:
206
251
  FileExistsError: When existing_file_policy is FAIL and file already exists.
252
+ RuntimeError: If file write fails (new behavior).
253
+ ValueError: If file upload fails (legacy behavior).
207
254
  """
208
255
  resolved_directory = self._get_static_files_directory()
209
256
  file_path = Path(resolved_directory) / file_name
210
257
 
211
- # Pass the existing_file_policy to the storage driver
212
- response = self.storage_driver.create_signed_upload_url(file_path, existing_file_policy)
258
+ # Inject workflow metadata if enabled (only when not using direct save)
259
+ if (
260
+ not use_direct_save
261
+ and self.config_manager.get_config_value("auto_inject_workflow_metadata", default=True)
262
+ and not skip_metadata_injection
263
+ ):
264
+ try:
265
+ data = inject_workflow_metadata_if_image(data, file_name)
266
+ except Exception as e:
267
+ logger.warning("Failed to inject workflow metadata into %s: %s", file_name, e)
213
268
 
269
+ # NEW BEHAVIOR: Direct save via storage driver
270
+ if use_direct_save:
271
+ try:
272
+ saved_path = self.storage_driver.save_file(file_path, data, existing_file_policy)
273
+ except FileExistsError:
274
+ raise
275
+ except Exception as e:
276
+ msg = f"Failed to save static file {file_name}: {e}"
277
+ logger.error(msg)
278
+ raise RuntimeError(msg) from e
279
+ return saved_path
280
+
281
+ # OLD BEHAVIOR: Presigned URL upload
282
+ response = self.storage_driver.create_signed_upload_url(file_path, existing_file_policy)
214
283
  resolved_file_path = Path(response["file_path"])
215
284
 
216
285
  try:
@@ -235,9 +304,6 @@ class StaticFilesManager:
235
304
  workflow's directory relative to workspace. Otherwise, returns the staticfiles
236
305
  subdirectory relative to workspace.
237
306
  """
238
- from griptape_nodes.node_library.workflow_registry import WorkflowRegistry
239
- from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
240
-
241
307
  workspace_path = self.config_manager.workspace_path
242
308
  static_files_subdir = self.config_manager.get_config_value("static_files_directory", default="staticfiles")
243
309
 
@@ -60,6 +60,9 @@ from griptape_nodes.retained_mode.events.library_events import (
60
60
  )
61
61
  from griptape_nodes.retained_mode.events.object_events import ClearAllObjectStateRequest
62
62
  from griptape_nodes.retained_mode.events.os_events import (
63
+ DeleteFileRequest,
64
+ DeleteFileResultFailure,
65
+ DeletionBehavior,
63
66
  ExistingFilePolicy,
64
67
  FileIOFailureReason,
65
68
  WriteFileRequest,
@@ -150,6 +153,7 @@ from griptape_nodes.retained_mode.managers.fitness_problems.workflows import (
150
153
  WorkflowNotFoundProblem,
151
154
  )
152
155
  from griptape_nodes.retained_mode.managers.os_manager import OSManager
156
+ from griptape_nodes.utils.path_utils import resolve_workspace_path
153
157
 
154
158
  if TYPE_CHECKING:
155
159
  from collections.abc import Callable, Sequence
@@ -392,7 +396,7 @@ class WorkflowManager:
392
396
  """
393
397
  return self._referenced_workflow_stack[-1]
394
398
 
395
- def on_libraries_initialization_complete(self) -> None:
399
+ def on_libraries_initialization_complete(self, workflows_to_register: list[str] | None = None) -> None:
396
400
  # All of the libraries have loaded, and any workflows they came with have been registered.
397
401
  # Discover workflows from both config and workspace.
398
402
  self._workflows_loading_complete.clear()
@@ -401,15 +405,16 @@ class WorkflowManager:
401
405
  default_workflow_section = "app_events.on_app_initialization_complete.workflows_to_register"
402
406
  config_mgr = GriptapeNodes.ConfigManager()
403
407
 
404
- workflows_to_register = []
408
+ if workflows_to_register is None:
409
+ workflows_to_register = []
405
410
 
406
- # Add from config
407
- config_workflows = config_mgr.get_config_value(default_workflow_section, default=[])
408
- workflows_to_register.extend(config_workflows)
411
+ # Add from config
412
+ config_workflows = config_mgr.get_config_value(default_workflow_section, default=[])
413
+ workflows_to_register.extend(config_workflows)
409
414
 
410
- # Add from workspace (avoiding duplicates)
411
- workspace_path = config_mgr.workspace_path
412
- workflows_to_register.extend([workspace_path])
415
+ # Add from workspace (avoiding duplicates)
416
+ workspace_path = config_mgr.workspace_path
417
+ workflows_to_register.extend([str(workspace_path)])
413
418
 
414
419
  # Register all discovered workflows at once if any were found
415
420
  self._process_workflows_for_registration(workflows_to_register)
@@ -631,11 +636,9 @@ class WorkflowManager:
631
636
  raise RuntimeError(error_message)
632
637
 
633
638
  async def run_workflow(self, relative_file_path: str) -> WorkflowExecutionResult:
634
- relative_file_path_obj = Path(relative_file_path)
635
- if relative_file_path_obj.is_absolute():
636
- complete_file_path = relative_file_path_obj
637
- else:
638
- complete_file_path = WorkflowRegistry.get_complete_file_path(relative_file_path=relative_file_path)
639
+ # Resolve path using utility function
640
+ workspace_path = GriptapeNodes.ConfigManager().workspace_path
641
+ complete_file_path = resolve_workspace_path(Path(relative_file_path), workspace_path)
639
642
  try:
640
643
  # Libraries are now loaded only on app initialization and explicit reload requests
641
644
  # Now execute the workflow.
@@ -820,7 +823,7 @@ class WorkflowManager:
820
823
  workflows=workflows, result_details=f"Successfully retrieved {len(workflows)} workflows."
821
824
  )
822
825
 
823
- def on_delete_workflows_request(self, request: DeleteWorkflowRequest) -> ResultPayload:
826
+ async def on_delete_workflows_request(self, request: DeleteWorkflowRequest) -> ResultPayload:
824
827
  try:
825
828
  workflow = WorkflowRegistry.delete_workflow_by_name(request.name)
826
829
  except Exception as e:
@@ -834,10 +837,15 @@ class WorkflowManager:
834
837
  return DeleteWorkflowResultFailure(result_details=details)
835
838
  # delete the actual file
836
839
  full_path = config_manager.workspace_path.joinpath(workflow.file_path)
837
- try:
838
- full_path.unlink()
839
- except Exception as e:
840
- details = f"Failed to delete workflow file with path '{workflow.file_path}'. Exception: {e}"
840
+
841
+ delete_request = DeleteFileRequest(
842
+ path=str(full_path),
843
+ workspace_only=False,
844
+ deletion_behavior=DeletionBehavior.PREFER_RECYCLE_BIN,
845
+ )
846
+ delete_result = await GriptapeNodes.ahandle_request(delete_request)
847
+ if isinstance(delete_result, DeleteFileResultFailure):
848
+ details = f"Failed to delete workflow file with path '{workflow.file_path}'. {delete_result.result_details}"
841
849
  return DeleteWorkflowResultFailure(result_details=details)
842
850
  return DeleteWorkflowResultSuccess(
843
851
  result_details=ResultDetails(message=f"Successfully deleted workflow: {request.name}", level=logging.INFO)
@@ -1826,8 +1834,11 @@ class WorkflowManager:
1826
1834
 
1827
1835
  ast_container = ASTContainer()
1828
1836
 
1837
+ # Extract library names from workflow metadata
1838
+ library_names = [lib.library_name for lib in workflow_metadata.node_libraries_referenced]
1839
+
1829
1840
  prereq_code = self._generate_workflow_run_prerequisite_code(
1830
- workflow_name=workflow_metadata.name, import_recorder=import_recorder
1841
+ workflow_name=workflow_metadata.name, import_recorder=import_recorder, library_names=library_names
1831
1842
  )
1832
1843
  for node in prereq_code:
1833
1844
  ast_container.add_node(node)
@@ -2145,7 +2156,8 @@ class WorkflowManager:
2145
2156
  ),
2146
2157
  )
2147
2158
 
2148
- # Create conditional logic: workflow_executor = workflow_executor or LocalWorkflowExecutor(storage_backend=storage_backend_enum)
2159
+ # Create conditional logic: workflow_executor = workflow_executor or LocalWorkflowExecutor(storage_backend=storage_backend_enum, skip_library_loading=True, workflows_to_register=[__file__])
2160
+ # TODO: https://github.com/griptape-ai/griptape-nodes/issues/3771 Update for workflows that call other workflows - need to include referenced workflows in the list
2149
2161
  executor_assign = ast.Assign(
2150
2162
  targets=[ast.Name(id="workflow_executor", ctx=ast.Store())],
2151
2163
  value=ast.BoolOp(
@@ -2159,6 +2171,11 @@ class WorkflowManager:
2159
2171
  ast.keyword(
2160
2172
  arg="storage_backend", value=ast.Name(id="storage_backend_enum", ctx=ast.Load())
2161
2173
  ),
2174
+ ast.keyword(arg="skip_library_loading", value=ast.Constant(value=True)),
2175
+ ast.keyword(
2176
+ arg="workflows_to_register",
2177
+ value=ast.List(elts=[ast.Name(id="__file__", ctx=ast.Load())], ctx=ast.Load()),
2178
+ ),
2162
2179
  ],
2163
2180
  ),
2164
2181
  ],
@@ -2750,32 +2767,44 @@ class WorkflowManager:
2750
2767
  self,
2751
2768
  workflow_name: str,
2752
2769
  import_recorder: ImportRecorder,
2770
+ library_names: list[str],
2753
2771
  ) -> list[ast.AST]:
2754
- import_recorder.add_from_import("griptape_nodes.retained_mode.events.library_events", "LoadLibrariesRequest")
2772
+ import_recorder.add_from_import(
2773
+ "griptape_nodes.retained_mode.events.library_events", "RegisterLibraryFromFileRequest"
2774
+ )
2755
2775
 
2756
2776
  code_blocks: list[ast.AST] = []
2757
2777
 
2758
- # Generate load libraries request call
2759
- # TODO (https://github.com/griptape-ai/griptape-nodes/issues/1615): Generate requests to load ONLY the libraries used in this workflow
2760
- load_call = ast.Expr(
2761
- value=ast.Call(
2762
- func=ast.Attribute(
2763
- value=ast.Name(id="GriptapeNodes", ctx=ast.Load()),
2764
- attr="handle_request",
2765
- ctx=ast.Load(),
2766
- ),
2767
- args=[
2768
- ast.Call(
2769
- func=ast.Name(id="LoadLibrariesRequest", ctx=ast.Load()),
2770
- args=[],
2771
- keywords=[],
2772
- )
2773
- ],
2774
- keywords=[],
2778
+ # Generate one RegisterLibraryFromFileRequest call per library
2779
+ for library_name in library_names:
2780
+ register_call = ast.Expr(
2781
+ value=ast.Call(
2782
+ func=ast.Attribute(
2783
+ value=ast.Name(id="GriptapeNodes", ctx=ast.Load()),
2784
+ attr="handle_request",
2785
+ ctx=ast.Load(),
2786
+ ),
2787
+ args=[
2788
+ ast.Call(
2789
+ func=ast.Name(id="RegisterLibraryFromFileRequest", ctx=ast.Load()),
2790
+ args=[],
2791
+ keywords=[
2792
+ ast.keyword(
2793
+ arg="library_name",
2794
+ value=ast.Constant(value=library_name),
2795
+ ),
2796
+ ast.keyword(
2797
+ arg="perform_discovery_if_not_found",
2798
+ value=ast.Constant(value=True),
2799
+ ),
2800
+ ],
2801
+ )
2802
+ ],
2803
+ keywords=[],
2804
+ )
2775
2805
  )
2776
- )
2777
- ast.fix_missing_locations(load_call)
2778
- code_blocks.append(load_call)
2806
+ ast.fix_missing_locations(register_call)
2807
+ code_blocks.append(register_call)
2779
2808
 
2780
2809
  # Generate context manager assignment
2781
2810
  assign_context_manager = ast.Assign(
@@ -4100,6 +4129,7 @@ class WorkflowManager:
4100
4129
  result_messages = []
4101
4130
  try:
4102
4131
  WorkflowRegistry.delete_workflow_by_name(request.workflow_name)
4132
+ # TODO: Replace with DeleteFileRequest https://github.com/griptape-ai/griptape-nodes/issues/3765
4103
4133
  Path(branch_content_file_path).unlink()
4104
4134
  cleanup_message = f"Deleted branch workflow file and registry entry for '{request.workflow_name}'"
4105
4135
  result_messages.append(ResultDetail(message=cleanup_message, level=logging.INFO))
@@ -9,6 +9,7 @@ from urllib.parse import urljoin
9
9
  import uvicorn
10
10
  from fastapi import FastAPI, HTTPException, Request
11
11
  from fastapi.middleware.cors import CORSMiddleware
12
+ from fastapi.responses import FileResponse
12
13
  from fastapi.staticfiles import StaticFiles
13
14
  from rich.logging import RichHandler
14
15
 
@@ -122,6 +123,7 @@ async def _delete_static_file(file_path: str) -> dict:
122
123
  raise HTTPException(status_code=400, detail=msg)
123
124
 
124
125
  try:
126
+ # TODO: Replace with DeleteFileRequest https://github.com/griptape-ai/griptape-nodes/issues/3765
125
127
  file_full_path.unlink()
126
128
  except (OSError, PermissionError) as e:
127
129
  msg = f"Failed to delete file {file_path}: {e}"
@@ -132,6 +134,34 @@ async def _delete_static_file(file_path: str) -> dict:
132
134
  return {"message": f"File {file_path} deleted successfully"}
133
135
 
134
136
 
137
+ async def _serve_external_file(file_path: str) -> FileResponse:
138
+ """Serve a file from outside the workspace.
139
+
140
+ Args:
141
+ file_path: The file path without leading slash (e.g., "tmp/video.mp4" for "/tmp/video.mp4")
142
+ """
143
+ if not STATIC_SERVER_ENABLED:
144
+ msg = "Static server is not enabled. Please set STATIC_SERVER_ENABLED to True."
145
+ raise HTTPException(status_code=500, detail=msg)
146
+
147
+ # Reconstruct absolute path by adding leading slash
148
+ absolute_path = Path(f"/{file_path}")
149
+
150
+ # Check if file exists
151
+ if not absolute_path.exists():
152
+ logger.warning("External file not found: %s", absolute_path)
153
+ raise HTTPException(status_code=404, detail=f"File {absolute_path} not found")
154
+
155
+ # Check if it's actually a file (not a directory)
156
+ if not absolute_path.is_file():
157
+ msg = f"Path {absolute_path} is not a file"
158
+ logger.error(msg)
159
+ raise HTTPException(status_code=400, detail=msg)
160
+
161
+ # Serve the file
162
+ return FileResponse(absolute_path)
163
+
164
+
135
165
  def start_static_server() -> None:
136
166
  """Run uvicorn server synchronously using uvicorn.run."""
137
167
  logger.debug("Starting static server...")
@@ -145,6 +175,7 @@ def start_static_server() -> None:
145
175
  app.add_api_route("/static-uploads/{file_path_prefix:path}", _list_static_files, methods=["GET"])
146
176
  app.add_api_route("/static-uploads/", _list_static_files, methods=["GET"])
147
177
  app.add_api_route("/static-files/{file_path:path}", _delete_static_file, methods=["DELETE"])
178
+ app.add_api_route("/external/{file_path:path}", _serve_external_file, methods=["GET"])
148
179
 
149
180
  # Build CORS allowed origins list
150
181
  allowed_origins = [
@@ -1,5 +1,13 @@
1
1
  """Various utility functions."""
2
2
 
3
3
  from griptape_nodes.utils.async_utils import call_function
4
+ from griptape_nodes.utils.http_file_patch import install_file_url_support
5
+ from griptape_nodes.utils.path_utils import resolve_workspace_path
6
+ from griptape_nodes.utils.url_utils import get_content_type_from_extension
4
7
 
5
- __all__ = ["call_function"]
8
+ __all__ = [
9
+ "call_function",
10
+ "get_content_type_from_extension",
11
+ "install_file_url_support",
12
+ "resolve_workspace_path",
13
+ ]