griptape-nodes 0.41.0__py3-none-any.whl → 0.43.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 (133) hide show
  1. griptape_nodes/__init__.py +0 -0
  2. griptape_nodes/app/.python-version +0 -0
  3. griptape_nodes/app/__init__.py +1 -10
  4. griptape_nodes/app/api.py +199 -0
  5. griptape_nodes/app/app.py +140 -222
  6. griptape_nodes/app/watch.py +4 -2
  7. griptape_nodes/bootstrap/__init__.py +0 -0
  8. griptape_nodes/bootstrap/bootstrap_script.py +0 -0
  9. griptape_nodes/bootstrap/register_libraries_script.py +0 -0
  10. griptape_nodes/bootstrap/structure_config.yaml +0 -0
  11. griptape_nodes/bootstrap/workflow_executors/__init__.py +0 -0
  12. griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +0 -0
  13. griptape_nodes/bootstrap/workflow_executors/workflow_executor.py +0 -0
  14. griptape_nodes/bootstrap/workflow_runners/__init__.py +0 -0
  15. griptape_nodes/bootstrap/workflow_runners/bootstrap_workflow_runner.py +0 -0
  16. griptape_nodes/bootstrap/workflow_runners/local_workflow_runner.py +0 -0
  17. griptape_nodes/bootstrap/workflow_runners/subprocess_workflow_runner.py +6 -2
  18. griptape_nodes/bootstrap/workflow_runners/workflow_runner.py +0 -0
  19. griptape_nodes/drivers/__init__.py +0 -0
  20. griptape_nodes/drivers/storage/__init__.py +0 -0
  21. griptape_nodes/drivers/storage/base_storage_driver.py +0 -0
  22. griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +0 -0
  23. griptape_nodes/drivers/storage/local_storage_driver.py +5 -3
  24. griptape_nodes/drivers/storage/storage_backend.py +0 -0
  25. griptape_nodes/exe_types/__init__.py +0 -0
  26. griptape_nodes/exe_types/connections.py +0 -0
  27. griptape_nodes/exe_types/core_types.py +0 -0
  28. griptape_nodes/exe_types/flow.py +68 -368
  29. griptape_nodes/exe_types/node_types.py +17 -1
  30. griptape_nodes/exe_types/type_validator.py +0 -0
  31. griptape_nodes/machines/__init__.py +0 -0
  32. griptape_nodes/machines/control_flow.py +52 -20
  33. griptape_nodes/machines/fsm.py +16 -2
  34. griptape_nodes/machines/node_resolution.py +16 -14
  35. griptape_nodes/mcp_server/__init__.py +1 -0
  36. griptape_nodes/mcp_server/server.py +126 -0
  37. griptape_nodes/mcp_server/ws_request_manager.py +268 -0
  38. griptape_nodes/node_library/__init__.py +0 -0
  39. griptape_nodes/node_library/advanced_node_library.py +0 -0
  40. griptape_nodes/node_library/library_registry.py +0 -0
  41. griptape_nodes/node_library/workflow_registry.py +2 -2
  42. griptape_nodes/py.typed +0 -0
  43. griptape_nodes/retained_mode/__init__.py +0 -0
  44. griptape_nodes/retained_mode/events/__init__.py +0 -0
  45. griptape_nodes/retained_mode/events/agent_events.py +70 -8
  46. griptape_nodes/retained_mode/events/app_events.py +137 -12
  47. griptape_nodes/retained_mode/events/arbitrary_python_events.py +23 -0
  48. griptape_nodes/retained_mode/events/base_events.py +13 -31
  49. griptape_nodes/retained_mode/events/config_events.py +87 -11
  50. griptape_nodes/retained_mode/events/connection_events.py +56 -5
  51. griptape_nodes/retained_mode/events/context_events.py +27 -4
  52. griptape_nodes/retained_mode/events/execution_events.py +99 -14
  53. griptape_nodes/retained_mode/events/flow_events.py +165 -7
  54. griptape_nodes/retained_mode/events/generate_request_payload_schemas.py +0 -0
  55. griptape_nodes/retained_mode/events/library_events.py +195 -17
  56. griptape_nodes/retained_mode/events/logger_events.py +11 -0
  57. griptape_nodes/retained_mode/events/node_events.py +242 -22
  58. griptape_nodes/retained_mode/events/object_events.py +40 -4
  59. griptape_nodes/retained_mode/events/os_events.py +116 -3
  60. griptape_nodes/retained_mode/events/parameter_events.py +212 -8
  61. griptape_nodes/retained_mode/events/payload_registry.py +0 -0
  62. griptape_nodes/retained_mode/events/secrets_events.py +59 -7
  63. griptape_nodes/retained_mode/events/static_file_events.py +57 -4
  64. griptape_nodes/retained_mode/events/validation_events.py +39 -4
  65. griptape_nodes/retained_mode/events/workflow_events.py +188 -17
  66. griptape_nodes/retained_mode/griptape_nodes.py +89 -363
  67. griptape_nodes/retained_mode/managers/__init__.py +0 -0
  68. griptape_nodes/retained_mode/managers/agent_manager.py +49 -23
  69. griptape_nodes/retained_mode/managers/arbitrary_code_exec_manager.py +0 -0
  70. griptape_nodes/retained_mode/managers/config_manager.py +0 -0
  71. griptape_nodes/retained_mode/managers/context_manager.py +0 -0
  72. griptape_nodes/retained_mode/managers/engine_identity_manager.py +146 -0
  73. griptape_nodes/retained_mode/managers/event_manager.py +14 -2
  74. griptape_nodes/retained_mode/managers/flow_manager.py +751 -64
  75. griptape_nodes/retained_mode/managers/library_lifecycle/__init__.py +45 -0
  76. griptape_nodes/retained_mode/managers/library_lifecycle/data_models.py +191 -0
  77. griptape_nodes/retained_mode/managers/library_lifecycle/library_directory.py +346 -0
  78. griptape_nodes/retained_mode/managers/library_lifecycle/library_fsm.py +439 -0
  79. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/__init__.py +17 -0
  80. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/base.py +82 -0
  81. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/github.py +116 -0
  82. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/local_file.py +352 -0
  83. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/package.py +104 -0
  84. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/sandbox.py +155 -0
  85. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance.py +18 -0
  86. griptape_nodes/retained_mode/managers/library_lifecycle/library_status.py +12 -0
  87. griptape_nodes/retained_mode/managers/library_manager.py +255 -40
  88. griptape_nodes/retained_mode/managers/node_manager.py +120 -103
  89. griptape_nodes/retained_mode/managers/object_manager.py +11 -3
  90. griptape_nodes/retained_mode/managers/operation_manager.py +0 -0
  91. griptape_nodes/retained_mode/managers/os_manager.py +582 -8
  92. griptape_nodes/retained_mode/managers/secrets_manager.py +4 -0
  93. griptape_nodes/retained_mode/managers/session_manager.py +328 -0
  94. griptape_nodes/retained_mode/managers/settings.py +7 -0
  95. griptape_nodes/retained_mode/managers/static_files_manager.py +0 -0
  96. griptape_nodes/retained_mode/managers/version_compatibility_manager.py +2 -2
  97. griptape_nodes/retained_mode/managers/workflow_manager.py +722 -456
  98. griptape_nodes/retained_mode/retained_mode.py +44 -0
  99. griptape_nodes/retained_mode/utils/__init__.py +0 -0
  100. griptape_nodes/retained_mode/utils/engine_identity.py +141 -27
  101. griptape_nodes/retained_mode/utils/name_generator.py +0 -0
  102. griptape_nodes/traits/__init__.py +0 -0
  103. griptape_nodes/traits/add_param_button.py +0 -0
  104. griptape_nodes/traits/button.py +0 -0
  105. griptape_nodes/traits/clamp.py +0 -0
  106. griptape_nodes/traits/compare.py +0 -0
  107. griptape_nodes/traits/compare_images.py +0 -0
  108. griptape_nodes/traits/file_system_picker.py +127 -0
  109. griptape_nodes/traits/minmax.py +0 -0
  110. griptape_nodes/traits/options.py +0 -0
  111. griptape_nodes/traits/slider.py +0 -0
  112. griptape_nodes/traits/trait_registry.py +0 -0
  113. griptape_nodes/traits/traits.json +0 -0
  114. griptape_nodes/updater/__init__.py +2 -2
  115. griptape_nodes/updater/__main__.py +0 -0
  116. griptape_nodes/utils/__init__.py +0 -0
  117. griptape_nodes/utils/dict_utils.py +0 -0
  118. griptape_nodes/utils/image_preview.py +128 -0
  119. griptape_nodes/utils/metaclasses.py +0 -0
  120. griptape_nodes/version_compatibility/__init__.py +0 -0
  121. griptape_nodes/version_compatibility/versions/__init__.py +0 -0
  122. griptape_nodes/version_compatibility/versions/v0_39_0/__init__.py +0 -0
  123. griptape_nodes/version_compatibility/versions/v0_39_0/modified_parameters_set_removal.py +5 -5
  124. griptape_nodes-0.43.0.dist-info/METADATA +90 -0
  125. griptape_nodes-0.43.0.dist-info/RECORD +129 -0
  126. griptape_nodes-0.43.0.dist-info/WHEEL +4 -0
  127. {griptape_nodes-0.41.0.dist-info → griptape_nodes-0.43.0.dist-info}/entry_points.txt +1 -0
  128. griptape_nodes/app/app_sessions.py +0 -458
  129. griptape_nodes/retained_mode/utils/session_persistence.py +0 -105
  130. griptape_nodes-0.41.0.dist-info/METADATA +0 -78
  131. griptape_nodes-0.41.0.dist-info/RECORD +0 -112
  132. griptape_nodes-0.41.0.dist-info/WHEEL +0 -4
  133. griptape_nodes-0.41.0.dist-info/licenses/LICENSE +0 -201
@@ -1,40 +1,28 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import ast
4
- import importlib.metadata
5
- import json
6
4
  import logging
7
- import os
8
5
  import pickle
9
6
  import pkgutil
10
7
  import re
11
- import shutil
12
- import subprocess
13
- import tempfile
14
8
  from dataclasses import dataclass, field, fields, is_dataclass
15
9
  from datetime import UTC, datetime
16
10
  from enum import StrEnum
17
- from importlib import resources
18
11
  from inspect import getmodule, isclass
19
12
  from pathlib import Path
20
- from typing import TYPE_CHECKING, Any, ClassVar, Literal, NamedTuple, TypeVar, cast
21
- from urllib.parse import urljoin
13
+ from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple, TypeVar, cast
22
14
 
23
- import httpx
24
15
  import tomlkit
25
- from dotenv import get_key, set_key
26
- from dotenv.main import DotEnv
27
16
  from rich.box import HEAVY_EDGE
28
17
  from rich.console import Console
29
18
  from rich.panel import Panel
30
19
  from rich.table import Table
31
20
  from rich.text import Text
32
- from xdg_base_dirs import xdg_config_home
33
21
 
34
22
  from griptape_nodes.drivers.storage import StorageBackend
35
23
  from griptape_nodes.exe_types.core_types import ParameterTypeBuiltin
24
+ from griptape_nodes.exe_types.flow import ControlFlow
36
25
  from griptape_nodes.exe_types.node_types import BaseNode, EndNode, StartNode
37
- from griptape_nodes.node_library.library_registry import LibraryNameAndVersion, LibraryRegistry
38
26
  from griptape_nodes.node_library.workflow_registry import Workflow, WorkflowMetadata, WorkflowRegistry
39
27
  from griptape_nodes.retained_mode.events.app_events import (
40
28
  GetEngineVersionRequest,
@@ -48,20 +36,21 @@ from griptape_nodes.retained_mode.events.flow_events import (
48
36
  SerializedNodeCommands,
49
37
  SerializeFlowToCommandsRequest,
50
38
  SerializeFlowToCommandsResultSuccess,
39
+ SetFlowMetadataRequest,
40
+ SetFlowMetadataResultSuccess,
51
41
  )
52
42
  from griptape_nodes.retained_mode.events.library_events import (
53
43
  GetLibraryMetadataRequest,
54
44
  GetLibraryMetadataResultSuccess,
55
45
  )
56
46
  from griptape_nodes.retained_mode.events.object_events import ClearAllObjectStateRequest
57
- from griptape_nodes.retained_mode.events.secrets_events import (
58
- GetAllSecretValuesRequest,
59
- GetAllSecretValuesResultSuccess,
60
- )
61
47
  from griptape_nodes.retained_mode.events.workflow_events import (
62
48
  DeleteWorkflowRequest,
63
49
  DeleteWorkflowResultFailure,
64
50
  DeleteWorkflowResultSuccess,
51
+ ImportWorkflowAsReferencedSubFlowRequest,
52
+ ImportWorkflowAsReferencedSubFlowResultFailure,
53
+ ImportWorkflowAsReferencedSubFlowResultSuccess,
65
54
  ListAllWorkflowsRequest,
66
55
  ListAllWorkflowsResultFailure,
67
56
  ListAllWorkflowsResultSuccess,
@@ -70,7 +59,6 @@ from griptape_nodes.retained_mode.events.workflow_events import (
70
59
  LoadWorkflowMetadataResultSuccess,
71
60
  PublishWorkflowRequest,
72
61
  PublishWorkflowResultFailure,
73
- PublishWorkflowResultSuccess,
74
62
  RegisterWorkflowRequest,
75
63
  RegisterWorkflowResultFailure,
76
64
  RegisterWorkflowResultSuccess,
@@ -94,12 +82,14 @@ from griptape_nodes.retained_mode.griptape_nodes import (
94
82
  GriptapeNodes,
95
83
  Version,
96
84
  )
85
+ from griptape_nodes.retained_mode.managers.os_manager import OSManager
97
86
 
98
87
  if TYPE_CHECKING:
99
88
  from collections.abc import Callable, Sequence
100
89
  from types import TracebackType
101
90
 
102
91
  from griptape_nodes.exe_types.core_types import Parameter
92
+ from griptape_nodes.node_library.library_registry import LibraryNameAndVersion
103
93
  from griptape_nodes.retained_mode.events.base_events import ResultPayload
104
94
  from griptape_nodes.retained_mode.managers.event_manager import EventManager
105
95
 
@@ -176,6 +166,28 @@ class WorkflowManager:
176
166
 
177
167
  _squelch_workflow_altered_count: int = 0
178
168
 
169
+ # Track referenced workflow import context stack
170
+ class ReferencedWorkflowContext:
171
+ """Context manager for tracking workflow import operations."""
172
+
173
+ def __init__(self, manager: WorkflowManager, workflow_name: str):
174
+ self.manager = manager
175
+ self.workflow_name = workflow_name
176
+
177
+ def __enter__(self) -> WorkflowManager.ReferencedWorkflowContext:
178
+ self.manager._referenced_workflow_stack.append(self.workflow_name)
179
+ return self
180
+
181
+ def __exit__(
182
+ self,
183
+ exc_type: type[BaseException] | None,
184
+ exc_value: BaseException | None,
185
+ exc_traceback: TracebackType | None,
186
+ ) -> None:
187
+ self.manager._referenced_workflow_stack.pop()
188
+
189
+ _referenced_workflow_stack: list[str] = field(default_factory=list)
190
+
179
191
  class WorkflowExecutionResult(NamedTuple):
180
192
  """Result of a workflow execution."""
181
193
 
@@ -185,6 +197,7 @@ class WorkflowManager:
185
197
  def __init__(self, event_manager: EventManager) -> None:
186
198
  self._workflow_file_path_to_info = {}
187
199
  self._squelch_workflow_altered_count = 0
200
+ self._referenced_workflow_stack = []
188
201
 
189
202
  event_manager.assign_manager_to_request_type(
190
203
  RunWorkflowFromScratchRequest, self.on_run_workflow_from_scratch_request
@@ -223,6 +236,22 @@ class WorkflowManager:
223
236
  PublishWorkflowRequest,
224
237
  self.on_publish_workflow_request,
225
238
  )
239
+ event_manager.assign_manager_to_request_type(
240
+ ImportWorkflowAsReferencedSubFlowRequest,
241
+ self.on_import_workflow_as_referenced_sub_flow_request,
242
+ )
243
+
244
+ def has_current_referenced_workflow(self) -> bool:
245
+ """Check if there is currently a referenced workflow context active."""
246
+ return len(self._referenced_workflow_stack) > 0
247
+
248
+ def get_current_referenced_workflow(self) -> str:
249
+ """Get the current workflow source path from the context stack.
250
+
251
+ Raises:
252
+ IndexError: If no referenced workflow context is active.
253
+ """
254
+ return self._referenced_workflow_stack[-1]
226
255
 
227
256
  def on_libraries_initialization_complete(self) -> None:
228
257
  # All of the libraries have loaded, and any workflows they came with have been registered.
@@ -367,6 +396,42 @@ class WorkflowManager:
367
396
  def should_squelch_workflow_altered(self) -> bool:
368
397
  return self._squelch_workflow_altered_count > 0
369
398
 
399
+ def _ensure_workflow_context_established(self) -> None:
400
+ """Ensure there's a current workflow and flow context after workflow execution."""
401
+ context_manager = GriptapeNodes.ContextManager()
402
+
403
+ # First check: Do we have a current workflow? If not, that's a critical failure.
404
+ if not context_manager.has_current_workflow():
405
+ error_message = "Workflow execution completed but no current workflow is established in context"
406
+ raise RuntimeError(error_message)
407
+
408
+ # Second check: Do we have a current flow? If not, try to establish one.
409
+ if not context_manager.has_current_flow():
410
+ # Use the proper request to get the top-level flow
411
+ from griptape_nodes.retained_mode.events.flow_events import (
412
+ GetTopLevelFlowRequest,
413
+ GetTopLevelFlowResultSuccess,
414
+ )
415
+
416
+ top_level_flow_request = GetTopLevelFlowRequest()
417
+ top_level_flow_result = GriptapeNodes.handle_request(top_level_flow_request)
418
+
419
+ if (
420
+ isinstance(top_level_flow_result, GetTopLevelFlowResultSuccess)
421
+ and top_level_flow_result.flow_name is not None
422
+ ):
423
+ # Push the flow to the context stack permanently using FlowManager
424
+ flow_manager = GriptapeNodes.FlowManager()
425
+ flow_obj = flow_manager.get_flow_by_name(top_level_flow_result.flow_name)
426
+ context_manager.push_flow(flow_obj)
427
+ details = f"Workflow execution completed. Set '{top_level_flow_result.flow_name}' as current context."
428
+ logger.debug(details)
429
+
430
+ # If we still don't have a flow, that's a critical error
431
+ if not context_manager.has_current_flow():
432
+ error_message = "Workflow execution completed but no current flow context could be established"
433
+ raise RuntimeError(error_message)
434
+
370
435
  def run_workflow(self, relative_file_path: str) -> WorkflowExecutionResult:
371
436
  relative_file_path_obj = Path(relative_file_path)
372
437
  if relative_file_path_obj.is_absolute():
@@ -379,6 +444,12 @@ class WorkflowManager:
379
444
  with Path(complete_file_path).open(encoding="utf-8") as file:
380
445
  workflow_content = file.read()
381
446
  exec(workflow_content) # noqa: S102
447
+
448
+ # After workflow execution, ensure there's always a current context by pushing
449
+ # the top-level flow if the context is empty. This fixes regressions where
450
+ # with Workflow Schema version 0.6.0+ workflows expect context to be established.
451
+ self._ensure_workflow_context_established()
452
+
382
453
  except Exception as e:
383
454
  return WorkflowManager.WorkflowExecutionResult(
384
455
  execution_successful=False,
@@ -829,9 +900,9 @@ class WorkflowManager:
829
900
  if workflow_metadata.is_griptape_provided:
830
901
  workflow_metadata.image = workflow_metadata.image
831
902
  else:
832
- workflow_metadata.image = str(
833
- GriptapeNodes.ConfigManager().workspace_path.joinpath(workflow_metadata.image)
834
- )
903
+ # For user workflows, the image should be just the filename, not a full path
904
+ # The frontend now sends just filenames, so we don't need to prepend the workspace path
905
+ workflow_metadata.image = workflow_metadata.image
835
906
 
836
907
  # Register it as a success.
837
908
  workflow_register_request = RegisterWorkflowRequest(
@@ -860,66 +931,38 @@ class WorkflowManager:
860
931
 
861
932
  return import_statements
862
933
 
863
- def on_save_workflow_request(self, request: SaveWorkflowRequest) -> ResultPayload: # noqa: C901, PLR0911, PLR0912, PLR0915
864
- logger.debug("SaveWorkflowRequest payload: %r (image_path: %r)", request, getattr(request, "image_path", None))
865
- local_tz = datetime.now().astimezone().tzinfo
934
+ def _generate_workflow_file_contents_and_metadata( # noqa: C901, PLR0912, PLR0915
935
+ self, file_name: str, creation_date: datetime, image_path: str | None = None
936
+ ) -> tuple[str, WorkflowMetadata]:
937
+ """Generate the contents of a workflow file.
866
938
 
867
- # Start with the file name provided; we may change it.
868
- file_name = request.file_name
869
-
870
- # See if we had an existing workflow for this.
871
- prior_workflow = None
872
- creation_date = None
873
- if file_name and WorkflowRegistry.has_workflow_with_name(file_name):
874
- # Get the metadata.
875
- prior_workflow = WorkflowRegistry.get_workflow_by_name(file_name)
876
- # We'll use its creation date.
877
- creation_date = prior_workflow.metadata.creation_date
878
-
879
- if (creation_date is None) or (creation_date == WorkflowManager.EPOCH_START):
880
- # Either a new workflow, or a backcompat situation.
881
- creation_date = datetime.now(tz=local_tz)
882
-
883
- # Let's see if this is a template file; if so, re-route it as a copy in the customer's workflow directory.
884
- if prior_workflow and prior_workflow.metadata.is_template:
885
- # Aha! User is attempting to save a template. Create a differently-named file in their workspace.
886
- # Find the first available file name that doesn't conflict.
887
- curr_idx = 1
888
- free_file_found = False
889
- while not free_file_found:
890
- # Composite a new candidate file name to test.
891
- new_file_name = f"{file_name}_{curr_idx}"
892
- new_file_name_with_extension = f"{new_file_name}.py"
893
- new_file_full_path = GriptapeNodes.ConfigManager().workspace_path.joinpath(new_file_name_with_extension)
894
- if new_file_full_path.exists():
895
- # Keep going.
896
- curr_idx += 1
897
- else:
898
- free_file_found = True
899
- file_name = new_file_name
939
+ Args:
940
+ file_name: The name of the workflow file
941
+ creation_date: The creation date for the workflow
942
+ image_path: Optional; the path to an image to include in the workflow metadata
900
943
 
901
- # Get file name stuff prepped.
902
- if not file_name:
903
- file_name = datetime.now(tz=local_tz).strftime("%d.%m_%H.%M")
904
- relative_file_path = f"{file_name}.py"
905
- file_path = GriptapeNodes.ConfigManager().workspace_path.joinpath(relative_file_path)
944
+ Returns:
945
+ A tuple of (workflow_file_contents, workflow_metadata)
906
946
 
947
+ Raises:
948
+ ValueError, TypeError: If workflow generation fails
949
+ """
907
950
  # Get the engine version.
908
951
  engine_version_request = GetEngineVersionRequest()
909
952
  engine_version_result = GriptapeNodes.handle_request(request=engine_version_request)
910
953
  if not isinstance(engine_version_result, GetEngineVersionResultSuccess):
911
- details = f"Attempted to save workflow '{relative_file_path}', but failed getting the engine version."
954
+ details = f"Failed getting the engine version for workflow '{file_name}'."
912
955
  logger.error(details)
913
- return SaveWorkflowResultFailure()
956
+ raise TypeError(details)
914
957
  try:
915
958
  engine_version_success = cast("GetEngineVersionResultSuccess", engine_version_result)
916
959
  engine_version = (
917
960
  f"{engine_version_success.major}.{engine_version_success.minor}.{engine_version_success.patch}"
918
961
  )
919
962
  except Exception as err:
920
- details = f"Attempted to save workflow '{relative_file_path}', but failed getting the engine version: {err}"
963
+ details = f"Failed getting the engine version for workflow '{file_name}': {err}"
921
964
  logger.error(details)
922
- return SaveWorkflowResultFailure()
965
+ raise ValueError(details) from err
923
966
 
924
967
  # Keep track of all of the nodes we create and the generated variable names for them.
925
968
  node_uuid_to_node_variable_name: dict[SerializedNodeCommands.NodeUUID, str] = {}
@@ -931,44 +974,46 @@ class WorkflowManager:
931
974
  top_level_flow_request = GetTopLevelFlowRequest()
932
975
  top_level_flow_result = GriptapeNodes.handle_request(top_level_flow_request)
933
976
  if not isinstance(top_level_flow_result, GetTopLevelFlowResultSuccess):
934
- details = (
935
- f"Attempted to save workflow '{relative_file_path}'. Failed when requesting to get top level flow."
936
- )
977
+ details = f"Failed when requesting to get top level flow for workflow '{file_name}'."
937
978
  logger.error(details)
938
- return SaveWorkflowResultFailure()
979
+ raise TypeError(details)
939
980
  top_level_flow_name = top_level_flow_result.flow_name
940
981
  serialized_flow_request = SerializeFlowToCommandsRequest(
941
982
  flow_name=top_level_flow_name, include_create_flow_command=True
942
983
  )
943
984
  serialized_flow_result = GriptapeNodes.handle_request(serialized_flow_request)
944
985
  if not isinstance(serialized_flow_result, SerializeFlowToCommandsResultSuccess):
945
- details = f"Attempted to save workflow '{relative_file_path}'. Failed when serializing flow."
986
+ details = f"Failed when serializing flow for workflow '{file_name}'."
946
987
  logger.error(details)
947
- return SaveWorkflowResultFailure()
988
+ raise TypeError(details)
948
989
  serialized_flow_commands = serialized_flow_result.serialized_flow_commands
949
990
 
950
991
  # Create the Workflow Metadata header.
992
+ workflows_referenced = None
993
+ if serialized_flow_commands.referenced_workflows:
994
+ workflows_referenced = list(serialized_flow_commands.referenced_workflows)
995
+
951
996
  workflow_metadata = self._generate_workflow_metadata(
952
997
  file_name=file_name,
953
998
  engine_version=engine_version,
954
999
  creation_date=creation_date,
955
1000
  node_libraries_referenced=list(serialized_flow_commands.node_libraries_used),
956
- published_workflow_id=prior_workflow.metadata.published_workflow_id if prior_workflow else None,
1001
+ workflows_referenced=workflows_referenced,
957
1002
  )
958
1003
  if workflow_metadata is None:
959
- details = f"Attempted to save workflow '{relative_file_path}'. Failed to generate metadata."
1004
+ details = f"Failed to generate metadata for workflow '{file_name}'."
960
1005
  logger.error(details)
961
- return SaveWorkflowResultFailure()
1006
+ raise ValueError(details)
962
1007
 
963
1008
  # Set the image if provided
964
- if request.image_path:
965
- workflow_metadata.image = request.image_path
1009
+ if image_path:
1010
+ workflow_metadata.image = image_path
966
1011
 
967
1012
  metadata_block = self._generate_workflow_metadata_header(workflow_metadata=workflow_metadata)
968
1013
  if metadata_block is None:
969
- details = f"Attempted to save workflow '{relative_file_path}'. Failed to generate metadata block."
1014
+ details = f"Failed to generate metadata block for workflow '{file_name}'."
970
1015
  logger.error(details)
971
- return SaveWorkflowResultFailure()
1016
+ raise ValueError(details)
972
1017
 
973
1018
  import_recorder = ImportRecorder()
974
1019
  import_recorder.add_from_import("griptape_nodes.retained_mode.griptape_nodes", "GriptapeNodes")
@@ -989,26 +1034,42 @@ class WorkflowManager:
989
1034
  )
990
1035
  ast_container.add_node(unique_values_node)
991
1036
 
992
- # See if this serialized flow has a create flow command; if it does, we'll need to insert that.
993
- create_flow_command = serialized_flow_commands.create_flow_command
1037
+ # See if this serialized flow has a flow initialization command; if it does, we'll need to insert that.
1038
+ flow_initialization_command = serialized_flow_commands.flow_initialization_command
994
1039
 
995
- if create_flow_command is not None:
996
- # Generate create flow context AST node
997
- create_flow_context_node = self._generate_create_flow(
998
- create_flow_command, import_recorder, flow_creation_index
999
- )
1000
- ast_container.add_node(create_flow_context_node)
1040
+ match flow_initialization_command:
1041
+ case CreateFlowRequest():
1042
+ # Generate create flow context AST module
1043
+ create_flow_context_module = self._generate_create_flow(
1044
+ flow_initialization_command, import_recorder, flow_creation_index
1045
+ )
1046
+ for node in create_flow_context_module.body:
1047
+ ast_container.add_node(node)
1048
+ case ImportWorkflowAsReferencedSubFlowRequest():
1049
+ # Generate import workflow context AST module
1050
+ import_workflow_context_module = self._generate_import_workflow(
1051
+ flow_initialization_command, import_recorder, flow_creation_index
1052
+ )
1053
+ for node in import_workflow_context_module.body:
1054
+ ast_container.add_node(node)
1055
+ case None:
1056
+ # No initialization command, deserialize into current context
1057
+ pass
1001
1058
 
1002
1059
  # Generate assign flow context AST node, if we have any children commands.
1003
- if (
1060
+ # Skip content generation for referenced workflows - they should only have the import command
1061
+ is_referenced_workflow = isinstance(flow_initialization_command, ImportWorkflowAsReferencedSubFlowRequest)
1062
+ has_content_to_serialize = (
1004
1063
  len(serialized_flow_commands.serialized_node_commands) > 0
1005
1064
  or len(serialized_flow_commands.serialized_connections) > 0
1006
1065
  or len(serialized_flow_commands.set_parameter_value_commands) > 0
1007
1066
  or len(serialized_flow_commands.sub_flows_commands) > 0
1008
- ):
1067
+ )
1068
+
1069
+ if not is_referenced_workflow and has_content_to_serialize:
1009
1070
  # Create the "with..." statement
1010
1071
  assign_flow_context_node = self._generate_assign_flow_context(
1011
- create_flow_command=create_flow_command, flow_creation_index=flow_creation_index
1072
+ flow_initialization_command=flow_initialization_command, flow_creation_index=flow_creation_index
1012
1073
  )
1013
1074
 
1014
1075
  # Generate nodes in flow AST node. This will create the node and apply all element modifiers.
@@ -1018,24 +1079,44 @@ class WorkflowManager:
1018
1079
 
1019
1080
  # Add the nodes to the body of the Current Context flow's "with" statement
1020
1081
  assign_flow_context_node.body.extend(nodes_in_flow)
1021
- ast_container.add_node(assign_flow_context_node)
1022
1082
 
1023
- # Now generate the connection code.
1083
+ # Process sub-flows - for each sub-flow, generate its initialization command
1084
+ for sub_flow_index, sub_flow_commands in enumerate(serialized_flow_commands.sub_flows_commands):
1085
+ sub_flow_creation_index = flow_creation_index + 1 + sub_flow_index
1086
+
1087
+ # Generate initialization command for the sub-flow
1088
+ sub_flow_initialization_command = sub_flow_commands.flow_initialization_command
1089
+ if sub_flow_initialization_command is not None:
1090
+ match sub_flow_initialization_command:
1091
+ case CreateFlowRequest():
1092
+ sub_flow_create_node = self._generate_create_flow(
1093
+ sub_flow_initialization_command, import_recorder, sub_flow_creation_index
1094
+ )
1095
+ assign_flow_context_node.body.append(cast("ast.stmt", sub_flow_create_node))
1096
+ case ImportWorkflowAsReferencedSubFlowRequest():
1097
+ sub_flow_import_node = self._generate_import_workflow(
1098
+ sub_flow_initialization_command, import_recorder, sub_flow_creation_index
1099
+ )
1100
+ assign_flow_context_node.body.append(cast("ast.stmt", sub_flow_import_node))
1101
+
1102
+ # Now generate the connection code and add it to the flow context.
1024
1103
  connection_asts = self._generate_connections_code(
1025
1104
  serialized_connections=serialized_flow_commands.serialized_connections,
1026
1105
  node_uuid_to_node_variable_name=node_uuid_to_node_variable_name,
1027
1106
  import_recorder=import_recorder,
1028
1107
  )
1029
- ast_container.nodes.extend(connection_asts)
1108
+ assign_flow_context_node.body.extend(connection_asts)
1030
1109
 
1031
- # Now generate all the set parameter value code.
1110
+ # Now generate all the set parameter value code and add it to the flow context.
1032
1111
  set_parameter_value_asts = self._generate_set_parameter_value_code(
1033
1112
  set_parameter_value_commands=serialized_flow_commands.set_parameter_value_commands,
1034
1113
  node_uuid_to_node_variable_name=node_uuid_to_node_variable_name,
1035
1114
  unique_values_dict_name="top_level_unique_values_dict",
1036
1115
  import_recorder=import_recorder,
1037
1116
  )
1038
- ast_container.nodes.extend(set_parameter_value_asts)
1117
+ assign_flow_context_node.body.extend(set_parameter_value_asts)
1118
+
1119
+ ast_container.add_node(assign_flow_context_node)
1039
1120
 
1040
1121
  workflow_execution_code = (
1041
1122
  self._generate_workflow_execution(
@@ -1056,18 +1137,96 @@ class WorkflowManager:
1056
1137
  import_output = import_recorder.generate_imports()
1057
1138
  final_code_output = f"{metadata_block}\n\n{import_output}\n\n{ast_output}\n"
1058
1139
 
1140
+ return final_code_output, workflow_metadata
1141
+
1142
+ def on_save_workflow_request(self, request: SaveWorkflowRequest) -> ResultPayload: # noqa: C901, PLR0912, PLR0915
1143
+ local_tz = datetime.now().astimezone().tzinfo
1144
+
1145
+ # Start with the file name provided; we may change it.
1146
+ file_name = request.file_name
1147
+
1148
+ # See if we had an existing workflow for this.
1149
+ prior_workflow = None
1150
+ creation_date = None
1151
+ if file_name and WorkflowRegistry.has_workflow_with_name(file_name):
1152
+ # Get the metadata.
1153
+ prior_workflow = WorkflowRegistry.get_workflow_by_name(file_name)
1154
+ # We'll use its creation date.
1155
+ creation_date = prior_workflow.metadata.creation_date
1156
+
1157
+ if (creation_date is None) or (creation_date == WorkflowManager.EPOCH_START):
1158
+ # Either a new workflow, or a backcompat situation.
1159
+ creation_date = datetime.now(tz=local_tz)
1160
+
1161
+ # Let's see if this is a template file; if so, re-route it as a copy in the customer's workflow directory.
1162
+ if prior_workflow and prior_workflow.metadata.is_template:
1163
+ # Aha! User is attempting to save a template. Create a differently-named file in their workspace.
1164
+ # Find the first available file name that doesn't conflict.
1165
+ curr_idx = 1
1166
+ free_file_found = False
1167
+ while not free_file_found:
1168
+ # Composite a new candidate file name to test.
1169
+ new_file_name = f"{file_name}_{curr_idx}"
1170
+ new_file_name_with_extension = f"{new_file_name}.py"
1171
+ new_file_full_path = GriptapeNodes.ConfigManager().workspace_path.joinpath(new_file_name_with_extension)
1172
+ if new_file_full_path.exists():
1173
+ # Keep going.
1174
+ curr_idx += 1
1175
+ else:
1176
+ free_file_found = True
1177
+ file_name = new_file_name
1178
+
1179
+ # Get file name stuff prepped.
1180
+ if not file_name:
1181
+ file_name = datetime.now(tz=local_tz).strftime("%d.%m_%H.%M")
1182
+ relative_file_path = f"{file_name}.py"
1183
+ file_path = GriptapeNodes.ConfigManager().workspace_path.joinpath(relative_file_path)
1184
+
1185
+ # Generate the workflow file contents
1186
+ try:
1187
+ final_code_output, workflow_metadata = self._generate_workflow_file_contents_and_metadata(
1188
+ file_name=file_name, creation_date=creation_date, image_path=request.image_path
1189
+ )
1190
+ except Exception as err:
1191
+ details = f"Attempted to save workflow '{relative_file_path}', but {err}"
1192
+ logger.error(details)
1193
+ return SaveWorkflowResultFailure()
1194
+
1059
1195
  # Create the pathing and write the file
1060
- file_path.parent.mkdir(parents=True, exist_ok=True)
1196
+ try:
1197
+ file_path.parent.mkdir(parents=True, exist_ok=True)
1198
+ except OSError as e:
1199
+ logger.error("Attempted to save workflow '%s'. Failed when creating directory: %s", file_name, str(e))
1200
+ return SaveWorkflowResultFailure()
1061
1201
 
1062
1202
  relative_serialized_file_path = f"{file_name}.py"
1063
1203
  serialized_file_path = GriptapeNodes.ConfigManager().workspace_path.joinpath(relative_serialized_file_path)
1064
- with serialized_file_path.open("w", encoding="utf-8") as file:
1065
- file.write(final_code_output)
1204
+
1205
+ # Check disk space before writing
1206
+ config_manager = GriptapeNodes.ConfigManager()
1207
+ min_space_gb = config_manager.get_config_value("minimum_disk_space_gb_workflows")
1208
+ if not OSManager.check_available_disk_space(serialized_file_path.parent, min_space_gb):
1209
+ error_msg = OSManager.format_disk_space_error(serialized_file_path.parent)
1210
+ logger.error(
1211
+ "Attempted to save workflow '%s' (requires %.1f GB). Failed: %s", file_name, min_space_gb, error_msg
1212
+ )
1213
+ return SaveWorkflowResultFailure()
1214
+
1215
+ try:
1216
+ with serialized_file_path.open("w", encoding="utf-8") as file:
1217
+ file.write(final_code_output)
1218
+ except OSError as e:
1219
+ logger.error("Attempted to save workflow '%s'. Failed when writing file: %s", file_name, str(e))
1220
+ return SaveWorkflowResultFailure()
1066
1221
 
1067
1222
  # save the created workflow as an entry in the JSON config file.
1068
1223
  registered_workflows = WorkflowRegistry.list_workflows()
1069
1224
  if file_name not in registered_workflows:
1070
- GriptapeNodes.ConfigManager().save_user_workflow_json(str(file_path))
1225
+ try:
1226
+ GriptapeNodes.ConfigManager().save_user_workflow_json(str(file_path))
1227
+ except OSError as e:
1228
+ logger.error("Attempted to save workflow '%s'. Failed when saving configuration: %s", file_name, str(e))
1229
+ return SaveWorkflowResultFailure()
1071
1230
  WorkflowRegistry.generate_new_workflow(metadata=workflow_metadata, file_path=relative_file_path)
1072
1231
  details = f"Successfully saved workflow to: {serialized_file_path}"
1073
1232
  logger.info(details)
@@ -1079,7 +1238,7 @@ class WorkflowManager:
1079
1238
  engine_version: str,
1080
1239
  creation_date: datetime,
1081
1240
  node_libraries_referenced: list[LibraryNameAndVersion],
1082
- published_workflow_id: str | None,
1241
+ workflows_referenced: list[str] | None = None,
1083
1242
  ) -> WorkflowMetadata | None:
1084
1243
  local_tz = datetime.now().astimezone().tzinfo
1085
1244
  workflow_metadata = WorkflowMetadata(
@@ -1087,9 +1246,9 @@ class WorkflowManager:
1087
1246
  schema_version=WorkflowMetadata.LATEST_SCHEMA_VERSION,
1088
1247
  engine_version_created_with=engine_version,
1089
1248
  node_libraries_referenced=node_libraries_referenced,
1249
+ workflows_referenced=workflows_referenced,
1090
1250
  creation_date=creation_date,
1091
1251
  last_modified_date=datetime.now(tz=local_tz),
1092
- published_workflow_id=published_workflow_id,
1093
1252
  )
1094
1253
 
1095
1254
  return workflow_metadata
@@ -1131,30 +1290,40 @@ class WorkflowManager:
1131
1290
  ) -> list[ast.AST] | None:
1132
1291
  """Generates execute_workflow(...) and the __main__ guard."""
1133
1292
  try:
1134
- workflow_shape = self._extract_workflow_shape(flow_name)
1293
+ workflow_shape = self.extract_workflow_shape(flow_name)
1135
1294
  except ValueError:
1136
1295
  logger.info("Workflow shape does not have required Start or End Nodes. Skipping local execution block.")
1137
1296
  return None
1138
1297
 
1139
1298
  # === imports ===
1140
1299
  import_recorder.add_import("argparse")
1141
- import_recorder.add_import("json")
1142
1300
  import_recorder.add_from_import(
1143
1301
  "griptape_nodes.bootstrap.workflow_executors.local_workflow_executor", "LocalWorkflowExecutor"
1144
1302
  )
1303
+ import_recorder.add_from_import(
1304
+ "griptape_nodes.bootstrap.workflow_executors.workflow_executor", "WorkflowExecutor"
1305
+ )
1145
1306
 
1146
- # === 1) build the `def execute_workflow(input: dict, storage_backend: str = StorageBackend.LOCAL) -> dict | None:` ===
1307
+ # === 1) build the `def execute_workflow(input: dict, storage_backend: str = StorageBackend.LOCAL, workflow_executor: WorkflowExecutor | None = None) -> dict | None:` ===
1147
1308
  # args
1148
1309
  arg_input = ast.arg(arg="input", annotation=ast.Name(id="dict", ctx=ast.Load()))
1149
1310
  arg_storage_backend = ast.arg(arg="storage_backend", annotation=ast.Name(id="str", ctx=ast.Load()))
1311
+ arg_workflow_executor = ast.arg(
1312
+ arg="workflow_executor",
1313
+ annotation=ast.BinOp(
1314
+ left=ast.Name(id="WorkflowExecutor", ctx=ast.Load()),
1315
+ op=ast.BitOr(),
1316
+ right=ast.Constant(value=None),
1317
+ ),
1318
+ )
1150
1319
  args = ast.arguments(
1151
1320
  posonlyargs=[],
1152
- args=[arg_input, arg_storage_backend],
1321
+ args=[arg_input, arg_storage_backend, arg_workflow_executor],
1153
1322
  vararg=None,
1154
1323
  kwonlyargs=[],
1155
1324
  kw_defaults=[],
1156
1325
  kwarg=None,
1157
- defaults=[ast.Constant(StorageBackend.LOCAL.value)],
1326
+ defaults=[ast.Constant(StorageBackend.LOCAL.value), ast.Constant(value=None)],
1158
1327
  )
1159
1328
  # return annotation: dict | None
1160
1329
  return_annotation = ast.BinOp(
@@ -1163,12 +1332,22 @@ class WorkflowManager:
1163
1332
  right=ast.Constant(value=None),
1164
1333
  )
1165
1334
 
1335
+ # Generate the ensure flow context function call
1336
+ ensure_context_call = self._generate_ensure_flow_context_call()
1337
+
1338
+ # Create conditional logic: workflow_executor = workflow_executor or LocalWorkflowExecutor()
1166
1339
  executor_assign = ast.Assign(
1167
1340
  targets=[ast.Name(id="workflow_executor", ctx=ast.Store())],
1168
- value=ast.Call(
1169
- func=ast.Name(id="LocalWorkflowExecutor", ctx=ast.Load()),
1170
- args=[],
1171
- keywords=[],
1341
+ value=ast.BoolOp(
1342
+ op=ast.Or(),
1343
+ values=[
1344
+ ast.Name(id="workflow_executor", ctx=ast.Load()),
1345
+ ast.Call(
1346
+ func=ast.Name(id="LocalWorkflowExecutor", ctx=ast.Load()),
1347
+ args=[],
1348
+ keywords=[],
1349
+ ),
1350
+ ],
1172
1351
  ),
1173
1352
  )
1174
1353
  run_call = ast.Expr(
@@ -1197,7 +1376,7 @@ class WorkflowManager:
1197
1376
  func_def = ast.FunctionDef(
1198
1377
  name="execute_workflow",
1199
1378
  args=args,
1200
- body=[executor_assign, run_call, return_stmt],
1379
+ body=[ensure_context_call, executor_assign, run_call, return_stmt],
1201
1380
  decorator_list=[],
1202
1381
  returns=return_annotation,
1203
1382
  type_params=[],
@@ -1416,7 +1595,201 @@ class WorkflowManager:
1416
1595
  )
1417
1596
  ast.fix_missing_locations(if_node)
1418
1597
 
1419
- return [func_def, if_node]
1598
+ # Generate the ensure flow context function
1599
+ ensure_context_func = self._generate_ensure_flow_context_function(import_recorder)
1600
+
1601
+ return [ensure_context_func, func_def, if_node]
1602
+
1603
+ def _generate_ensure_flow_context_function(
1604
+ self,
1605
+ import_recorder: ImportRecorder,
1606
+ ) -> ast.FunctionDef:
1607
+ """Generates the _ensure_workflow_context function for the serialized workflow file."""
1608
+ import_recorder.add_from_import("griptape_nodes.retained_mode.events.flow_events", "GetTopLevelFlowRequest")
1609
+ import_recorder.add_from_import(
1610
+ "griptape_nodes.retained_mode.events.flow_events", "GetTopLevelFlowResultSuccess"
1611
+ )
1612
+
1613
+ # Function signature: def _ensure_workflow_context():
1614
+ func_def = ast.FunctionDef(
1615
+ name="_ensure_workflow_context",
1616
+ args=ast.arguments(
1617
+ posonlyargs=[],
1618
+ args=[],
1619
+ vararg=None,
1620
+ kwonlyargs=[],
1621
+ kw_defaults=[],
1622
+ kwarg=None,
1623
+ defaults=[],
1624
+ ),
1625
+ body=[],
1626
+ decorator_list=[],
1627
+ returns=None,
1628
+ type_params=[],
1629
+ )
1630
+
1631
+ context_manager_assign = ast.Assign(
1632
+ targets=[ast.Name(id="context_manager", ctx=ast.Store())],
1633
+ value=ast.Call(
1634
+ func=ast.Attribute(
1635
+ value=ast.Name(id="GriptapeNodes", ctx=ast.Load()),
1636
+ attr="ContextManager",
1637
+ ctx=ast.Load(),
1638
+ ),
1639
+ args=[],
1640
+ keywords=[],
1641
+ ),
1642
+ )
1643
+
1644
+ # if not context_manager.has_current_flow():
1645
+ has_flow_check = ast.UnaryOp(
1646
+ op=ast.Not(),
1647
+ operand=ast.Call(
1648
+ func=ast.Attribute(
1649
+ value=ast.Name(id="context_manager", ctx=ast.Load()),
1650
+ attr="has_current_flow",
1651
+ ctx=ast.Load(),
1652
+ ),
1653
+ args=[],
1654
+ keywords=[],
1655
+ ),
1656
+ )
1657
+
1658
+ # top_level_flow_request = GetTopLevelFlowRequest() # noqa: ERA001
1659
+ flow_request_assign = ast.Assign(
1660
+ targets=[ast.Name(id="top_level_flow_request", ctx=ast.Store())],
1661
+ value=ast.Call(
1662
+ func=ast.Name(id="GetTopLevelFlowRequest", ctx=ast.Load()),
1663
+ args=[],
1664
+ keywords=[],
1665
+ ),
1666
+ )
1667
+
1668
+ # top_level_flow_result = GriptapeNodes.handle_request(top_level_flow_request) # noqa: ERA001
1669
+ flow_result_assign = ast.Assign(
1670
+ targets=[ast.Name(id="top_level_flow_result", ctx=ast.Store())],
1671
+ value=ast.Call(
1672
+ func=ast.Attribute(
1673
+ value=ast.Name(id="GriptapeNodes", ctx=ast.Load()),
1674
+ attr="handle_request",
1675
+ ctx=ast.Load(),
1676
+ ),
1677
+ args=[ast.Name(id="top_level_flow_request", ctx=ast.Load())],
1678
+ keywords=[],
1679
+ ),
1680
+ )
1681
+
1682
+ # isinstance check and flow_name is not None
1683
+ isinstance_check = ast.Call(
1684
+ func=ast.Name(id="isinstance", ctx=ast.Load()),
1685
+ args=[
1686
+ ast.Name(id="top_level_flow_result", ctx=ast.Load()),
1687
+ ast.Name(id="GetTopLevelFlowResultSuccess", ctx=ast.Load()),
1688
+ ],
1689
+ keywords=[],
1690
+ )
1691
+
1692
+ flow_name_check = ast.Compare(
1693
+ left=ast.Attribute(
1694
+ value=ast.Name(id="top_level_flow_result", ctx=ast.Load()),
1695
+ attr="flow_name",
1696
+ ctx=ast.Load(),
1697
+ ),
1698
+ ops=[ast.IsNot()],
1699
+ comparators=[ast.Constant(value=None)],
1700
+ )
1701
+
1702
+ success_condition = ast.BoolOp(
1703
+ op=ast.And(),
1704
+ values=[isinstance_check, flow_name_check],
1705
+ )
1706
+
1707
+ # flow_manager = GriptapeNodes.FlowManager() # noqa: ERA001
1708
+ flow_manager_assign = ast.Assign(
1709
+ targets=[ast.Name(id="flow_manager", ctx=ast.Store())],
1710
+ value=ast.Call(
1711
+ func=ast.Attribute(
1712
+ value=ast.Name(id="GriptapeNodes", ctx=ast.Load()),
1713
+ attr="FlowManager",
1714
+ ctx=ast.Load(),
1715
+ ),
1716
+ args=[],
1717
+ keywords=[],
1718
+ ),
1719
+ )
1720
+
1721
+ # flow_obj = flow_manager.get_flow_by_name(top_level_flow_result.flow_name) # noqa: ERA001
1722
+ flow_obj_assign = ast.Assign(
1723
+ targets=[ast.Name(id="flow_obj", ctx=ast.Store())],
1724
+ value=ast.Call(
1725
+ func=ast.Attribute(
1726
+ value=ast.Name(id="flow_manager", ctx=ast.Load()),
1727
+ attr="get_flow_by_name",
1728
+ ctx=ast.Load(),
1729
+ ),
1730
+ args=[
1731
+ ast.Attribute(
1732
+ value=ast.Name(id="top_level_flow_result", ctx=ast.Load()),
1733
+ attr="flow_name",
1734
+ ctx=ast.Load(),
1735
+ )
1736
+ ],
1737
+ keywords=[],
1738
+ ),
1739
+ )
1740
+
1741
+ # context_manager.push_flow(flow_obj) # noqa: ERA001
1742
+ push_flow_call = ast.Expr(
1743
+ value=ast.Call(
1744
+ func=ast.Attribute(
1745
+ value=ast.Name(id="context_manager", ctx=ast.Load()),
1746
+ attr="push_flow",
1747
+ ctx=ast.Load(),
1748
+ ),
1749
+ args=[ast.Name(id="flow_obj", ctx=ast.Load())],
1750
+ keywords=[],
1751
+ ),
1752
+ )
1753
+
1754
+ # Build the inner if statement for success condition
1755
+ success_if = ast.If(
1756
+ test=success_condition,
1757
+ body=[
1758
+ flow_manager_assign,
1759
+ flow_obj_assign,
1760
+ push_flow_call,
1761
+ ],
1762
+ orelse=[],
1763
+ )
1764
+
1765
+ # Build the main if statement
1766
+ main_if = ast.If(
1767
+ test=has_flow_check,
1768
+ body=[
1769
+ flow_request_assign,
1770
+ flow_result_assign,
1771
+ success_if,
1772
+ ],
1773
+ orelse=[],
1774
+ )
1775
+
1776
+ # Set the function body
1777
+ func_def.body = [context_manager_assign, main_if]
1778
+ ast.fix_missing_locations(func_def)
1779
+
1780
+ return func_def
1781
+
1782
+ def _generate_ensure_flow_context_call(
1783
+ self,
1784
+ ) -> ast.Expr:
1785
+ """Generates the call to _ensure_workflow_context() function."""
1786
+ return ast.Expr(
1787
+ value=ast.Call(
1788
+ func=ast.Name(id="_ensure_workflow_context", ctx=ast.Load()),
1789
+ args=[],
1790
+ keywords=[],
1791
+ )
1792
+ )
1420
1793
 
1421
1794
  def _generate_workflow_run_prerequisite_code(
1422
1795
  self,
@@ -1679,7 +2052,7 @@ class WorkflowManager:
1679
2052
 
1680
2053
  def _generate_create_flow(
1681
2054
  self, create_flow_command: CreateFlowRequest, import_recorder: ImportRecorder, flow_creation_index: int
1682
- ) -> ast.AST:
2055
+ ) -> ast.Module:
1683
2056
  import_recorder.add_from_import("griptape_nodes.retained_mode.events.flow_events", "CreateFlowRequest")
1684
2057
 
1685
2058
  # Prepare arguments for CreateFlowRequest
@@ -1694,6 +2067,17 @@ class WorkflowManager:
1694
2067
  ast.keyword(arg=field.name, value=ast.Constant(value=field_value, lineno=1, col_offset=0))
1695
2068
  )
1696
2069
 
2070
+ # Create a comment explaining the behavior
2071
+ comment_ast = ast.Expr(
2072
+ value=ast.Constant(
2073
+ value="# Create the Flow, then do work within it as context.",
2074
+ lineno=1,
2075
+ col_offset=0,
2076
+ ),
2077
+ lineno=1,
2078
+ col_offset=0,
2079
+ )
2080
+
1697
2081
  # Construct the AST for creating the flow
1698
2082
  flow_variable_name = f"flow{flow_creation_index}_name"
1699
2083
  create_flow_result = ast.Assign(
@@ -1729,10 +2113,92 @@ class WorkflowManager:
1729
2113
  col_offset=0,
1730
2114
  )
1731
2115
 
1732
- return create_flow_result
2116
+ # Return both the comment and the assignment as a module
2117
+ return ast.Module(body=[comment_ast, create_flow_result], type_ignores=[])
2118
+
2119
+ def _generate_import_workflow(
2120
+ self,
2121
+ import_workflow_command: ImportWorkflowAsReferencedSubFlowRequest,
2122
+ import_recorder: ImportRecorder,
2123
+ flow_creation_index: int,
2124
+ ) -> ast.Module:
2125
+ """Generate AST code for importing a referenced workflow.
2126
+
2127
+ Creates an assignment statement that executes an ImportWorkflowAsReferencedSubFlowRequest
2128
+ and stores the resulting flow name in a variable.
2129
+
2130
+ Args:
2131
+ import_workflow_command: The import request containing the workflow file path
2132
+ import_recorder: Tracks imports needed for the generated code
2133
+ flow_creation_index: Index used to generate unique variable names
2134
+
2135
+ Returns:
2136
+ AST assignment node representing the import workflow command
2137
+
2138
+ Example output:
2139
+ flow1_name = GriptapeNodes.handle_request(ImportWorkflowAsReferencedSubFlowRequest(
2140
+ file_path='/path/to/workflow.py'
2141
+ )).created_flow_name
2142
+ """
2143
+ import_recorder.add_from_import(
2144
+ "griptape_nodes.retained_mode.events.workflow_events", "ImportWorkflowAsReferencedSubFlowRequest"
2145
+ )
2146
+
2147
+ # Prepare arguments for ImportWorkflowAsReferencedSubFlowRequest
2148
+ import_workflow_request_args = []
2149
+
2150
+ # Omit values that match default values.
2151
+ if is_dataclass(import_workflow_command):
2152
+ for field in fields(import_workflow_command):
2153
+ field_value = getattr(import_workflow_command, field.name)
2154
+ if field_value != field.default:
2155
+ import_workflow_request_args.append(
2156
+ ast.keyword(arg=field.name, value=ast.Constant(value=field_value, lineno=1, col_offset=0))
2157
+ )
2158
+
2159
+ # Construct the AST for importing the workflow
2160
+ flow_variable_name = f"flow{flow_creation_index}_name"
2161
+ import_workflow_result = ast.Assign(
2162
+ targets=[ast.Name(id=flow_variable_name, ctx=ast.Store(), lineno=1, col_offset=0)],
2163
+ value=ast.Attribute(
2164
+ value=ast.Call(
2165
+ func=ast.Attribute(
2166
+ value=ast.Name(id="GriptapeNodes", ctx=ast.Load(), lineno=1, col_offset=0),
2167
+ attr="handle_request",
2168
+ ctx=ast.Load(),
2169
+ lineno=1,
2170
+ col_offset=0,
2171
+ ),
2172
+ args=[
2173
+ ast.Call(
2174
+ func=ast.Name(
2175
+ id="ImportWorkflowAsReferencedSubFlowRequest", ctx=ast.Load(), lineno=1, col_offset=0
2176
+ ),
2177
+ args=[],
2178
+ keywords=import_workflow_request_args,
2179
+ lineno=1,
2180
+ col_offset=0,
2181
+ )
2182
+ ],
2183
+ keywords=[],
2184
+ lineno=1,
2185
+ col_offset=0,
2186
+ ),
2187
+ attr="created_flow_name",
2188
+ ctx=ast.Load(),
2189
+ lineno=1,
2190
+ col_offset=0,
2191
+ ),
2192
+ lineno=1,
2193
+ col_offset=0,
2194
+ )
2195
+
2196
+ return ast.Module(body=[import_workflow_result], type_ignores=[])
1733
2197
 
1734
2198
  def _generate_assign_flow_context(
1735
- self, create_flow_command: CreateFlowRequest | None, flow_creation_index: int
2199
+ self,
2200
+ flow_initialization_command: CreateFlowRequest | ImportWorkflowAsReferencedSubFlowRequest | None,
2201
+ flow_creation_index: int,
1736
2202
  ) -> ast.With:
1737
2203
  context_manager = ast.Attribute(
1738
2204
  value=ast.Name(id="GriptapeNodes", ctx=ast.Load(), lineno=1, col_offset=0),
@@ -1742,7 +2208,7 @@ class WorkflowManager:
1742
2208
  col_offset=0,
1743
2209
  )
1744
2210
 
1745
- if create_flow_command is None:
2211
+ if flow_initialization_command is None:
1746
2212
  # Construct AST for "GriptapeNodes.ContextManager().flow(GriptapeNodes.ContextManager().get_current_flow_name())"
1747
2213
  flow_call = ast.Call(
1748
2214
  func=ast.Attribute(
@@ -2199,7 +2665,7 @@ class WorkflowManager:
2199
2665
  }
2200
2666
  return workflow_shape
2201
2667
 
2202
- def _extract_workflow_shape(self, workflow_name: str) -> dict[str, Any]:
2668
+ def extract_workflow_shape(self, workflow_name: str) -> dict[str, Any]:
2203
2669
  """Extracts the input and output shape for a workflow.
2204
2670
 
2205
2671
  Here we gather information about the Workflow's exposed input and output Parameters
@@ -2249,365 +2715,165 @@ class WorkflowManager:
2249
2715
 
2250
2716
  return workflow_shape
2251
2717
 
2252
- def _copy_libraries_to_path_for_workflow(
2253
- self,
2254
- node_libraries: list[LibraryNameAndVersion],
2255
- destination_path: Path,
2256
- runtime_env_path: Path,
2257
- workflow: Workflow,
2258
- ) -> list[str]:
2259
- """Copies the libraries to the specified path for the workflow, returning the list of library paths.
2260
-
2261
- This is used to package the workflow for publishing.
2262
- """
2263
- library_paths: list[str] = []
2264
-
2265
- for library_ref in node_libraries:
2266
- library = GriptapeNodes.LibraryManager().get_library_info_by_library_name(library_ref.library_name)
2267
-
2268
- if library is None:
2269
- details = f"Attempted to publish workflow '{workflow.metadata.name}', but failed gathering library info for library '{library_ref.library_name}'."
2270
- logger.error(details)
2271
- raise ValueError(details)
2272
-
2273
- library_data = LibraryRegistry.get_library(library_ref.library_name).get_library_data()
2274
-
2275
- library_path = Path(library.library_path)
2276
- absolute_library_path = library_path.resolve()
2277
- abs_paths = [absolute_library_path]
2278
- for node in library_data.nodes:
2279
- p = (library_path.parent / Path(node.file_path)).resolve()
2280
- abs_paths.append(p)
2281
- common_root = Path(os.path.commonpath([str(p) for p in abs_paths]))
2282
- dest = destination_path / common_root.name
2283
- shutil.copytree(common_root, dest, dirs_exist_ok=True)
2284
- library_path_relative_to_common_root = absolute_library_path.relative_to(common_root)
2285
- library_paths.append(str(runtime_env_path / common_root.name / library_path_relative_to_common_root))
2718
+ def on_publish_workflow_request(self, request: PublishWorkflowRequest) -> ResultPayload:
2719
+ try:
2720
+ publisher_name = request.publisher_name
2721
+ event_handler_mappings = GriptapeNodes.LibraryManager().get_registered_event_handlers(
2722
+ request_type=type(request)
2723
+ )
2724
+ publishing_handler = event_handler_mappings.get(publisher_name)
2286
2725
 
2287
- return library_paths
2726
+ if publishing_handler is None:
2727
+ msg = f"No publishing handler found for '{publisher_name}' in request type '{type(request).__name__}'."
2728
+ raise ValueError(msg) # noqa: TRY301
2288
2729
 
2289
- def __get_install_source(self) -> tuple[Literal["git", "file", "pypi"], str | None]:
2290
- """Determines the install source of the Griptape Nodes package.
2730
+ return publishing_handler.handler(request)
2291
2731
 
2292
- Returns:
2293
- tuple: A tuple containing the install source and commit ID (if applicable).
2294
- """
2295
- dist = importlib.metadata.distribution("griptape_nodes")
2296
- direct_url_text = dist.read_text("direct_url.json")
2297
- # installing from pypi doesn't have a direct_url.json file
2298
- if direct_url_text is None:
2299
- logger.debug("No direct_url.json file found, assuming pypi install")
2300
- return "pypi", None
2301
-
2302
- direct_url_info = json.loads(direct_url_text)
2303
- url = direct_url_info.get("url")
2304
- if url.startswith("file://"):
2305
- try:
2306
- pkg_dir = Path(str(dist.locate_file(""))).resolve()
2307
- git_root = next(p for p in (pkg_dir, *pkg_dir.parents) if (p / ".git").is_dir())
2308
- commit = (
2309
- subprocess.check_output( # noqa: S603
2310
- ["git", "rev-parse", "--short", "HEAD"], # noqa: S607
2311
- cwd=git_root,
2312
- stderr=subprocess.DEVNULL,
2313
- )
2314
- .decode()
2315
- .strip()
2316
- )
2317
- except (StopIteration, subprocess.CalledProcessError):
2318
- logger.debug("File URL but no git repo → file")
2319
- return "file", None
2320
- else:
2321
- logger.debug("Detected git repo at %s (commit %s)", git_root, commit)
2322
- return "git", commit
2323
- if "vcs_info" in direct_url_info:
2324
- logger.debug("Detected git repo at %s", url)
2325
- return "git", direct_url_info["vcs_info"].get("commit_id")[:7]
2326
- # Fall back to pypi if no other source is found
2327
- logger.debug("Failed to detect install source, assuming pypi")
2328
- return "pypi", None
2329
-
2330
- def _get_merged_env_file_mapping(self, workspace_env_file_path: Path) -> dict[str, Any]:
2331
- """Merges the secrets from the workspace env file with the secrets from the GriptapeNodes SecretsManager.
2332
-
2333
- This is used to create a single .env file for the workflow. We can gather all secrets explicitly defined in the .env file
2334
- and by the settings/SecretsManager, but we will not gather all secrets from the OS env for the purpose of publishing.
2335
- """
2336
- env_file_dict = {}
2337
- if workspace_env_file_path.exists():
2338
- env_file = DotEnv(workspace_env_file_path)
2339
- env_file_dict = env_file.dict()
2340
-
2341
- get_all_secrets_request = GetAllSecretValuesRequest()
2342
- get_all_secrets_result = GriptapeNodes.handle_request(request=get_all_secrets_request)
2343
- if not isinstance(get_all_secrets_result, GetAllSecretValuesResultSuccess):
2344
- details = "Failed to get all secret values."
2345
- logger.error(details)
2346
- raise TypeError(details)
2347
-
2348
- secret_values = get_all_secrets_result.values
2349
- for secret_name, secret_value in secret_values.items():
2350
- if secret_name not in env_file_dict:
2351
- env_file_dict[secret_name] = secret_value
2352
-
2353
- return env_file_dict
2732
+ except Exception as e:
2733
+ details = f"Failed to publish workflow '{request.workflow_name}': {e!s}"
2734
+ logger.exception(details)
2735
+ return PublishWorkflowResultFailure(exception=e)
2354
2736
 
2355
- def _write_env_file(self, env_file_path: Path, env_file_dict: dict[str, Any]) -> None:
2356
- env_file_path.touch(exist_ok=True)
2357
- for key, val in env_file_dict.items():
2358
- set_key(env_file_path, key, str(val))
2737
+ def on_import_workflow_as_referenced_sub_flow_request(
2738
+ self, request: ImportWorkflowAsReferencedSubFlowRequest
2739
+ ) -> ResultPayload:
2740
+ """Import a registered workflow as a new referenced sub flow in the current context."""
2741
+ # Validate prerequisites
2742
+ validation_error = self._validate_import_prerequisites(request)
2743
+ if validation_error:
2744
+ return validation_error
2745
+
2746
+ # Get the workflow (validation passed, so we know it exists)
2747
+ workflow = self._get_workflow_by_name(request.workflow_name)
2748
+
2749
+ # Determine target flow name
2750
+ if request.flow_name is not None:
2751
+ flow_name = request.flow_name
2752
+ else:
2753
+ flow_name = GriptapeNodes.ContextManager().get_current_flow().name
2359
2754
 
2360
- def _package_workflow(self, workflow_name: str) -> str: # noqa: PLR0915
2361
- config_manager = GriptapeNodes.get_instance()._config_manager
2362
- secrets_manager = GriptapeNodes.get_instance()._secrets_manager
2363
- workflow = WorkflowRegistry.get_workflow_by_name(workflow_name)
2755
+ # Execute the import
2756
+ return self._execute_workflow_import(request, workflow, flow_name)
2364
2757
 
2365
- engine_version: str = ""
2366
- engine_version_request = GetEngineVersionRequest()
2367
- engine_version_result = GriptapeNodes.handle_request(request=engine_version_request)
2368
- if not engine_version_result.succeeded():
2369
- details = (
2370
- f"Attempted to publish workflow '{workflow.metadata.name}', but failed getting the engine version."
2758
+ def _validate_import_prerequisites(self, request: ImportWorkflowAsReferencedSubFlowRequest) -> ResultPayload | None:
2759
+ """Validate all prerequisites for import. Returns error result or None if valid."""
2760
+ # Check workflow exists and get it
2761
+ try:
2762
+ workflow = self._get_workflow_by_name(request.workflow_name)
2763
+ except KeyError:
2764
+ logger.error(
2765
+ "Attempted to import workflow '%s' as referenced sub flow. Failed because workflow is not registered",
2766
+ request.workflow_name,
2371
2767
  )
2372
- logger.error(details)
2373
- raise ValueError(details)
2374
- engine_version_success = cast("GetEngineVersionResultSuccess", engine_version_result)
2375
- engine_version = (
2376
- f"v{engine_version_success.major}.{engine_version_success.minor}.{engine_version_success.patch}"
2377
- )
2378
-
2379
- # This is the path where the full workflow will be packaged to in the runtime environment.
2380
- packaged_top_level_dir = "/structure"
2381
-
2382
- # Gather the paths to the files we need to copy.
2383
- bootstrap_pkg = resources.files("griptape_nodes.bootstrap")
2384
- bootstrap_script_traversable = bootstrap_pkg.joinpath("bootstrap_script.py")
2385
- with resources.as_file(bootstrap_script_traversable) as script_path:
2386
- root_griptape_nodes_path = Path(script_path).parent.parent
2387
-
2388
- structure_file_path = root_griptape_nodes_path / "bootstrap" / "bootstrap_script.py"
2389
- structure_config_file_path = root_griptape_nodes_path / "bootstrap" / "structure_config.yaml"
2390
- pre_build_install_script_path = root_griptape_nodes_path / "bootstrap" / "pre_build_install_script.sh"
2391
- post_build_install_script_path = root_griptape_nodes_path / "bootstrap" / "post_build_install_script.sh"
2392
- register_libraries_script_path = root_griptape_nodes_path / "bootstrap" / "register_libraries_script.py"
2393
- full_workflow_file_path = WorkflowRegistry.get_complete_file_path(workflow.file_path)
2394
-
2395
- env_file_mapping = self._get_merged_env_file_mapping(secrets_manager.workspace_env_path)
2396
-
2397
- config = config_manager.user_config
2398
- config["workspace_directory"] = packaged_top_level_dir
2399
-
2400
- # Create a temporary directory to perform the packaging
2401
- with tempfile.TemporaryDirectory() as tmp_dir:
2402
- tmp_dir_path = Path(tmp_dir)
2403
- temp_workflow_file_path = tmp_dir_path / "workflow.py"
2404
- temp_structure_path = tmp_dir_path / "structure.py"
2405
- temp_pre_build_install_script_path = tmp_dir_path / "pre_build_install_script.sh"
2406
- temp_post_build_install_script_path = tmp_dir_path / "post_build_install_script.sh"
2407
- temp_register_libraries_script_path = tmp_dir_path / "register_libraries_script.py"
2408
- config_file_path = tmp_dir_path / "GriptapeNodes" / "griptape_nodes_config.json"
2409
- init_file_path = tmp_dir_path / "__init__.py"
2768
+ return ImportWorkflowAsReferencedSubFlowResultFailure()
2769
+
2770
+ # Check workflow version - Schema version 0.6.0+ required for referenced workflow imports
2771
+ # (workflow schema was fixed in 0.6.0 to support importing workflows)
2772
+ required_version = Version(major=0, minor=6, patch=0)
2773
+ workflow_version = Version.from_string(workflow.metadata.schema_version)
2774
+ if workflow_version is None or workflow_version < required_version:
2775
+ logger.error(
2776
+ "Attempted to import workflow '%s' as referenced sub flow. Failed because workflow version '%s' is less than required version '0.6.0'. To remedy, open the workflow you are attempting to import and save it again to upgrade it to the latest version.",
2777
+ request.workflow_name,
2778
+ workflow.metadata.schema_version,
2779
+ )
2780
+ return ImportWorkflowAsReferencedSubFlowResultFailure()
2410
2781
 
2411
- try:
2412
- # Copy the workflow file, libraries, and structure files to the temporary directory
2413
- shutil.copyfile(full_workflow_file_path, temp_workflow_file_path)
2414
- shutil.copyfile(structure_file_path, temp_structure_path)
2415
- shutil.copyfile(pre_build_install_script_path, temp_pre_build_install_script_path)
2416
- shutil.copyfile(post_build_install_script_path, temp_post_build_install_script_path)
2417
- shutil.copyfile(register_libraries_script_path, temp_register_libraries_script_path)
2418
- shutil.copyfile(structure_config_file_path, tmp_dir_path / "structure_config.yaml")
2419
-
2420
- # Write the environment variables to the .env file
2421
- self._write_env_file(tmp_dir_path / ".env", env_file_mapping)
2422
-
2423
- # Get the library paths
2424
- library_paths = self._copy_libraries_to_path_for_workflow(
2425
- node_libraries=workflow.metadata.node_libraries_referenced,
2426
- destination_path=tmp_dir_path / "libraries",
2427
- runtime_env_path=Path(packaged_top_level_dir) / "libraries",
2428
- workflow=workflow,
2782
+ # Check target flow
2783
+ flow_name = request.flow_name
2784
+ if flow_name is None:
2785
+ if not GriptapeNodes.ContextManager().has_current_flow():
2786
+ logger.error(
2787
+ "Attempted to import workflow '%s' into Current Context. Failed because Current Context was empty",
2788
+ request.workflow_name,
2429
2789
  )
2430
-
2431
- with register_libraries_script_path.open("r", encoding="utf-8") as register_libraries_script_file:
2432
- register_libraries_script_contents = register_libraries_script_file.read()
2433
- library_paths = [f'"{library_path}"' for library_path in library_paths]
2434
- register_libraries_script_contents = register_libraries_script_contents.replace(
2435
- '["REPLACE_LIBRARY_PATHS"]',
2436
- f"[{', '.join(library_paths)}]",
2437
- )
2438
- with temp_register_libraries_script_path.open("w", encoding="utf-8") as register_libraries_script_file:
2439
- register_libraries_script_file.write(register_libraries_script_contents)
2440
-
2441
- config_file_path.parent.mkdir(parents=True, exist_ok=True)
2442
- with config_file_path.open("w", encoding="utf-8") as config_file:
2443
- config_file.write(json.dumps(config, indent=4))
2444
-
2445
- init_file_path.parent.mkdir(parents=True, exist_ok=True)
2446
- with init_file_path.open("w", encoding="utf-8") as init_file:
2447
- init_file.write('"""This is a temporary __init__.py file for the structure."""\n')
2448
-
2449
- shutil.copyfile(config_file_path, tmp_dir_path / "griptape_nodes_config.json")
2450
-
2451
- except Exception as e:
2452
- details = f"Failed to copy files to temporary directory. Error: {e}"
2453
- logger.exception(details)
2454
- raise
2455
-
2456
- # Create the requirements.txt file using the correct engine version
2457
- source, commit_id = self.__get_install_source()
2458
- if source == "git" and commit_id is not None:
2459
- engine_version = commit_id
2460
- requirements_file_path = tmp_dir_path / "requirements.txt"
2461
- with requirements_file_path.open("w", encoding="utf-8") as requirements_file:
2462
- requirements_file.write(
2463
- f"griptape-nodes @ git+https://github.com/griptape-ai/griptape-nodes.git@{engine_version}\n"
2790
+ return ImportWorkflowAsReferencedSubFlowResultFailure()
2791
+ else:
2792
+ # Validate that the specified flow exists
2793
+ flow_manager = GriptapeNodes.FlowManager()
2794
+ try:
2795
+ flow_manager.get_flow_by_name(flow_name)
2796
+ except KeyError:
2797
+ logger.error(
2798
+ "Attempted to import workflow '%s' into flow '%s'. Failed because target flow does not exist",
2799
+ request.workflow_name,
2800
+ flow_name,
2464
2801
  )
2802
+ return ImportWorkflowAsReferencedSubFlowResultFailure()
2465
2803
 
2466
- archive_base_name = config_manager.workspace_path / workflow_name
2467
- shutil.make_archive(str(archive_base_name), "zip", tmp_dir)
2468
- return str(archive_base_name) + ".zip"
2469
-
2470
- def _get_publish_workflow_request(self, base_url: str, files: httpx._types.RequestFiles) -> httpx.Request:
2471
- endpoint = urljoin(
2472
- base_url,
2473
- "/api/workflows",
2474
- )
2475
- return httpx.Request(
2476
- method="post",
2477
- url=endpoint,
2478
- files=files,
2479
- )
2480
-
2481
- def _get_update_workflow_request(
2482
- self, base_url: str, files: httpx._types.RequestFiles, workflow_id: str
2483
- ) -> httpx.Request:
2484
- endpoint = urljoin(
2485
- base_url,
2486
- f"/api/workflows/{workflow_id}",
2487
- )
2488
- return httpx.Request(
2489
- method="patch",
2490
- url=endpoint,
2491
- files=files,
2492
- )
2804
+ return None
2493
2805
 
2494
- def _does_published_workflow_exist(self, api_key: str, base_url: str, workflow_id: str) -> bool:
2495
- endpoint = urljoin(
2496
- base_url,
2497
- f"/api/workflows/{workflow_id}",
2498
- )
2499
- request = httpx.Request(
2500
- method="get",
2501
- url=endpoint,
2502
- )
2503
- request.headers["Authorization"] = f"Bearer {api_key}"
2504
- request.headers["Accept"] = "application/json"
2806
+ def _get_workflow_by_name(self, workflow_name: str) -> Workflow:
2807
+ """Get workflow by name from the registry."""
2808
+ return WorkflowRegistry.get_workflow_by_name(workflow_name)
2505
2809
 
2506
- with httpx.Client() as client:
2507
- try:
2508
- response = client.send(request)
2509
- response.raise_for_status()
2510
- except httpx.HTTPStatusError as e:
2511
- if e.response.status_code == httpx.codes.NOT_FOUND:
2512
- return False
2513
- raise
2514
- else:
2515
- return True
2516
-
2517
- def _deploy_workflow_to_cloud(self, workflow_name: str, package_path: str, input_data: dict) -> Any:
2518
- workflow = WorkflowRegistry.get_workflow_by_name(workflow_name)
2519
- workflow_id: str | None = workflow.metadata.published_workflow_id
2810
+ def _execute_workflow_import(
2811
+ self, request: ImportWorkflowAsReferencedSubFlowRequest, workflow: Workflow, flow_name: str
2812
+ ) -> ResultPayload:
2813
+ """Execute the actual workflow import."""
2814
+ # Get current flows before importing
2815
+ obj_manager = GriptapeNodes.ObjectManager()
2816
+ flows_before = set(obj_manager.get_filtered_subset(type=ControlFlow).keys())
2817
+
2818
+ # Execute the workflow within the target flow context and referenced context
2819
+ with GriptapeNodes.ContextManager().flow(flow_name): # noqa: SIM117
2820
+ with self.ReferencedWorkflowContext(self, request.workflow_name):
2821
+ workflow_result = self.run_workflow(workflow.file_path)
2822
+
2823
+ if not workflow_result.execution_successful:
2824
+ logger.error(
2825
+ "Attempted to import workflow '%s' as referenced sub flow. Failed because workflow execution failed: %s",
2826
+ request.workflow_name,
2827
+ workflow_result.execution_details,
2828
+ )
2829
+ return ImportWorkflowAsReferencedSubFlowResultFailure()
2520
2830
 
2521
- # Create http request to upload the package
2522
- base_url = os.getenv("GRIPTAPE_NODES_API_BASE_URL", "https://api.nodes.griptape.ai")
2523
- api_key = get_key(xdg_config_home() / "griptape_nodes" / ".env", "GT_CLOUD_API_KEY")
2524
- if not api_key:
2525
- details = "Failed to get API key from environment variables."
2526
- logger.error(details)
2527
- raise ValueError(details)
2831
+ # Get flows after importing to find the new referenced sub flow
2832
+ flows_after = set(obj_manager.get_filtered_subset(type=ControlFlow).keys())
2833
+ new_flows = flows_after - flows_before
2528
2834
 
2529
- input_key = "publish_workflow_input" if workflow_id is None else "update_workflow_input"
2530
- with Path(package_path).open("rb") as file:
2531
- parts = {
2532
- input_key: (None, json.dumps(input_data)),
2533
- "file": ("workflow.zip", file.read()),
2534
- }
2535
-
2536
- request: httpx.Request = (
2537
- self._get_update_workflow_request(
2538
- base_url=base_url,
2539
- files=parts,
2540
- workflow_id=workflow_id,
2835
+ if not new_flows:
2836
+ logger.error(
2837
+ "Attempted to import workflow '%s' as referenced sub flow. Failed because no new flow was created",
2838
+ request.workflow_name,
2541
2839
  )
2542
- if workflow_id and self._does_published_workflow_exist(api_key, base_url, workflow_id)
2543
- else self._get_publish_workflow_request(
2544
- base_url=base_url,
2545
- files=parts,
2840
+ return ImportWorkflowAsReferencedSubFlowResultFailure()
2841
+
2842
+ # For now, use the first created flow as the main imported flow
2843
+ # This handles nested workflows correctly since sub-flows are expected
2844
+ created_flow_name = next(iter(new_flows))
2845
+
2846
+ if len(new_flows) > 1:
2847
+ logger.debug(
2848
+ "Multiple flows created during import of '%s'. Main flow: %s, Sub-flows: %s",
2849
+ request.workflow_name,
2850
+ created_flow_name,
2851
+ [flow for flow in new_flows if flow != created_flow_name],
2546
2852
  )
2547
- )
2548
- request.headers["Authorization"] = f"Bearer {api_key}"
2549
- request.headers["Accept"] = "application/json"
2550
2853
 
2551
- response = None
2552
- with httpx.Client() as client:
2553
- try:
2554
- response = client.send(request)
2555
- response.raise_for_status()
2556
- return response.json()
2557
- except Exception:
2558
- status_code = response.status_code if response else "Unknown"
2559
- response_text = response.text if response else "No response text"
2560
- details = f"Failed to publish workflow. Status code: {status_code}, Response: {response_text}"
2561
- logger.error(details)
2562
- raise
2563
-
2564
- def _update_workflow_metadata_with_published_id(self, workflow_name: str, published_workflow_id: str) -> None:
2565
- workflow = WorkflowRegistry.get_workflow_by_name(workflow_name)
2566
- if workflow.metadata.published_workflow_id != published_workflow_id:
2567
- workflow.metadata.published_workflow_id = published_workflow_id
2568
-
2569
- file_name = Path(workflow.file_path).name
2570
- file_name = file_name.replace(".py", "") if workflow.file_path.endswith(".py") else workflow.file_path
2571
- save_workflow_request = SaveWorkflowRequest(file_name=file_name)
2572
- save_workflow_result = self.on_save_workflow_request(save_workflow_request)
2573
- if save_workflow_result.failed():
2574
- details = f"Failed to update workflow metadata with published ID for workflow '{workflow_name}'."
2575
- logger.error(details)
2576
- raise ValueError(details)
2577
-
2578
- def on_publish_workflow_request(self, request: PublishWorkflowRequest) -> ResultPayload:
2579
- try:
2580
- # Get the workflow shape
2581
- workflow_shape = self._extract_workflow_shape(request.workflow_name)
2582
- logger.info("Workflow shape: %s", workflow_shape)
2583
-
2584
- # Package the workflow
2585
- package_path = self._package_workflow(request.workflow_name)
2586
- logger.info("Workflow packaged to path: %s", package_path)
2587
-
2588
- input_data = {
2589
- "name": request.workflow_name,
2590
- }
2591
- session_id = GriptapeNodes.get_session_id()
2592
- if session_id is not None:
2593
- input_data["session_id"] = session_id
2594
- input_data.update(workflow_shape)
2595
- response = self._deploy_workflow_to_cloud(request.workflow_name, package_path, input_data)
2596
- logger.info("Workflow '%s' published successfully: %s", request.workflow_name, response)
2597
-
2598
- self._update_workflow_metadata_with_published_id(
2599
- workflow_name=request.workflow_name,
2600
- published_workflow_id=response["id"],
2854
+ # Apply imported flow metadata if provided
2855
+ if request.imported_flow_metadata:
2856
+ set_metadata_request = SetFlowMetadataRequest(
2857
+ flow_name=created_flow_name, metadata=request.imported_flow_metadata
2601
2858
  )
2602
- logger.info("Workflow '%s' metadata updated with published ID: %s", request.workflow_name, response["id"])
2859
+ set_metadata_result = GriptapeNodes.handle_request(set_metadata_request)
2603
2860
 
2604
- return PublishWorkflowResultSuccess(
2605
- workflow_id=response["id"],
2861
+ if not isinstance(set_metadata_result, SetFlowMetadataResultSuccess):
2862
+ logger.error(
2863
+ "Attempted to import workflow '%s' as referenced sub flow. Failed because metadata could not be applied to created flow '%s'",
2864
+ request.workflow_name,
2865
+ created_flow_name,
2866
+ )
2867
+ return ImportWorkflowAsReferencedSubFlowResultFailure()
2868
+
2869
+ logger.debug(
2870
+ "Applied imported flow metadata to '%s': %s", created_flow_name, request.imported_flow_metadata
2606
2871
  )
2607
- except Exception as e:
2608
- details = f"Failed to publish workflow '{request.workflow_name}'. Error: {e}"
2609
- logger.error(details)
2610
- return PublishWorkflowResultFailure()
2872
+
2873
+ logger.info(
2874
+ "Successfully imported workflow '%s' as referenced sub flow '%s'", request.workflow_name, created_flow_name
2875
+ )
2876
+ return ImportWorkflowAsReferencedSubFlowResultSuccess(created_flow_name=created_flow_name)
2611
2877
 
2612
2878
  def _walk_object_tree(
2613
2879
  self, obj: Any, process_class_fn: Callable[[type, Any], None], visited: set[int] | None = None