griptape-nodes 0.43.1__py3-none-any.whl → 0.45.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- griptape_nodes/__init__.py +46 -52
- griptape_nodes/app/.python-version +0 -0
- griptape_nodes/app/__init__.py +0 -0
- griptape_nodes/app/api.py +37 -41
- griptape_nodes/app/app.py +70 -3
- griptape_nodes/app/watch.py +5 -2
- griptape_nodes/bootstrap/__init__.py +0 -0
- griptape_nodes/bootstrap/workflow_executors/__init__.py +0 -0
- griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +7 -1
- griptape_nodes/bootstrap/workflow_executors/subprocess_workflow_executor.py +90 -0
- griptape_nodes/bootstrap/workflow_executors/workflow_executor.py +7 -1
- griptape_nodes/drivers/__init__.py +0 -0
- griptape_nodes/drivers/storage/__init__.py +0 -0
- griptape_nodes/drivers/storage/base_storage_driver.py +90 -0
- griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +48 -0
- griptape_nodes/drivers/storage/local_storage_driver.py +37 -0
- griptape_nodes/drivers/storage/storage_backend.py +0 -0
- griptape_nodes/exe_types/__init__.py +0 -0
- griptape_nodes/exe_types/connections.py +0 -0
- griptape_nodes/exe_types/core_types.py +222 -17
- griptape_nodes/exe_types/flow.py +0 -0
- griptape_nodes/exe_types/node_types.py +20 -5
- griptape_nodes/exe_types/type_validator.py +0 -0
- griptape_nodes/machines/__init__.py +0 -0
- griptape_nodes/machines/control_flow.py +5 -4
- griptape_nodes/machines/fsm.py +0 -0
- griptape_nodes/machines/node_resolution.py +110 -74
- griptape_nodes/mcp_server/__init__.py +0 -0
- griptape_nodes/mcp_server/server.py +16 -8
- griptape_nodes/mcp_server/ws_request_manager.py +0 -0
- griptape_nodes/node_library/__init__.py +0 -0
- griptape_nodes/node_library/advanced_node_library.py +0 -0
- griptape_nodes/node_library/library_registry.py +0 -0
- griptape_nodes/node_library/workflow_registry.py +29 -0
- griptape_nodes/py.typed +0 -0
- griptape_nodes/retained_mode/__init__.py +0 -0
- griptape_nodes/retained_mode/events/__init__.py +0 -0
- griptape_nodes/retained_mode/events/agent_events.py +0 -0
- griptape_nodes/retained_mode/events/app_events.py +3 -8
- griptape_nodes/retained_mode/events/arbitrary_python_events.py +0 -0
- griptape_nodes/retained_mode/events/base_events.py +15 -7
- griptape_nodes/retained_mode/events/config_events.py +0 -0
- griptape_nodes/retained_mode/events/connection_events.py +0 -0
- griptape_nodes/retained_mode/events/context_events.py +0 -0
- griptape_nodes/retained_mode/events/execution_events.py +0 -0
- griptape_nodes/retained_mode/events/flow_events.py +2 -1
- griptape_nodes/retained_mode/events/generate_request_payload_schemas.py +0 -0
- griptape_nodes/retained_mode/events/library_events.py +0 -0
- griptape_nodes/retained_mode/events/logger_events.py +0 -0
- griptape_nodes/retained_mode/events/node_events.py +36 -0
- griptape_nodes/retained_mode/events/object_events.py +0 -0
- griptape_nodes/retained_mode/events/os_events.py +98 -6
- griptape_nodes/retained_mode/events/parameter_events.py +0 -0
- griptape_nodes/retained_mode/events/payload_registry.py +0 -0
- griptape_nodes/retained_mode/events/secrets_events.py +0 -0
- griptape_nodes/retained_mode/events/static_file_events.py +0 -0
- griptape_nodes/retained_mode/events/sync_events.py +60 -0
- griptape_nodes/retained_mode/events/validation_events.py +0 -0
- griptape_nodes/retained_mode/events/workflow_events.py +231 -0
- griptape_nodes/retained_mode/griptape_nodes.py +9 -4
- griptape_nodes/retained_mode/managers/__init__.py +0 -0
- griptape_nodes/retained_mode/managers/agent_manager.py +0 -0
- griptape_nodes/retained_mode/managers/arbitrary_code_exec_manager.py +0 -0
- griptape_nodes/retained_mode/managers/config_manager.py +1 -1
- griptape_nodes/retained_mode/managers/context_manager.py +0 -0
- griptape_nodes/retained_mode/managers/engine_identity_manager.py +0 -0
- griptape_nodes/retained_mode/managers/event_manager.py +0 -0
- griptape_nodes/retained_mode/managers/flow_manager.py +6 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/__init__.py +0 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/data_models.py +0 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_directory.py +0 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_fsm.py +0 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/__init__.py +0 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/base.py +0 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/github.py +0 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/local_file.py +0 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/package.py +0 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/sandbox.py +0 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance.py +0 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_status.py +0 -0
- griptape_nodes/retained_mode/managers/library_manager.py +8 -26
- griptape_nodes/retained_mode/managers/node_manager.py +78 -7
- griptape_nodes/retained_mode/managers/object_manager.py +0 -0
- griptape_nodes/retained_mode/managers/operation_manager.py +7 -0
- griptape_nodes/retained_mode/managers/os_manager.py +133 -8
- griptape_nodes/retained_mode/managers/secrets_manager.py +0 -0
- griptape_nodes/retained_mode/managers/session_manager.py +0 -0
- griptape_nodes/retained_mode/managers/settings.py +5 -0
- griptape_nodes/retained_mode/managers/static_files_manager.py +0 -0
- griptape_nodes/retained_mode/managers/sync_manager.py +498 -0
- griptape_nodes/retained_mode/managers/version_compatibility_manager.py +0 -0
- griptape_nodes/retained_mode/managers/workflow_manager.py +736 -33
- griptape_nodes/retained_mode/retained_mode.py +23 -0
- griptape_nodes/retained_mode/utils/__init__.py +0 -0
- griptape_nodes/retained_mode/utils/engine_identity.py +0 -0
- griptape_nodes/retained_mode/utils/name_generator.py +0 -0
- griptape_nodes/traits/__init__.py +0 -0
- griptape_nodes/traits/add_param_button.py +0 -0
- griptape_nodes/traits/button.py +0 -0
- griptape_nodes/traits/clamp.py +0 -0
- griptape_nodes/traits/compare.py +0 -0
- griptape_nodes/traits/compare_images.py +0 -0
- griptape_nodes/traits/file_system_picker.py +18 -0
- griptape_nodes/traits/minmax.py +0 -0
- griptape_nodes/traits/options.py +0 -0
- griptape_nodes/traits/slider.py +0 -0
- griptape_nodes/traits/trait_registry.py +0 -0
- griptape_nodes/traits/traits.json +0 -0
- griptape_nodes/updater/__init__.py +4 -2
- griptape_nodes/updater/__main__.py +0 -0
- griptape_nodes/utils/__init__.py +0 -0
- griptape_nodes/utils/dict_utils.py +0 -0
- griptape_nodes/utils/image_preview.py +0 -0
- griptape_nodes/utils/metaclasses.py +0 -0
- griptape_nodes/utils/uv_utils.py +18 -0
- griptape_nodes/utils/version_utils.py +51 -0
- griptape_nodes/version_compatibility/__init__.py +0 -0
- griptape_nodes/version_compatibility/versions/__init__.py +0 -0
- griptape_nodes/version_compatibility/versions/v0_39_0/__init__.py +0 -0
- griptape_nodes/version_compatibility/versions/v0_39_0/modified_parameters_set_removal.py +0 -0
- {griptape_nodes-0.43.1.dist-info → griptape_nodes-0.45.0.dist-info}/METADATA +2 -1
- {griptape_nodes-0.43.1.dist-info → griptape_nodes-0.45.0.dist-info}/RECORD +42 -47
- {griptape_nodes-0.43.1.dist-info → griptape_nodes-0.45.0.dist-info}/WHEEL +1 -1
- griptape_nodes/bootstrap/bootstrap_script.py +0 -54
- griptape_nodes/bootstrap/post_build_install_script.sh +0 -3
- griptape_nodes/bootstrap/pre_build_install_script.sh +0 -4
- griptape_nodes/bootstrap/register_libraries_script.py +0 -32
- griptape_nodes/bootstrap/structure_config.yaml +0 -15
- griptape_nodes/bootstrap/workflow_runners/__init__.py +0 -1
- griptape_nodes/bootstrap/workflow_runners/bootstrap_workflow_runner.py +0 -28
- griptape_nodes/bootstrap/workflow_runners/local_workflow_runner.py +0 -237
- griptape_nodes/bootstrap/workflow_runners/subprocess_workflow_runner.py +0 -62
- griptape_nodes/bootstrap/workflow_runners/workflow_runner.py +0 -11
- {griptape_nodes-0.43.1.dist-info → griptape_nodes-0.45.0.dist-info}/entry_points.txt +0 -0
|
@@ -45,6 +45,12 @@ from griptape_nodes.retained_mode.events.library_events import (
|
|
|
45
45
|
)
|
|
46
46
|
from griptape_nodes.retained_mode.events.object_events import ClearAllObjectStateRequest
|
|
47
47
|
from griptape_nodes.retained_mode.events.workflow_events import (
|
|
48
|
+
BranchWorkflowRequest,
|
|
49
|
+
BranchWorkflowResultFailure,
|
|
50
|
+
BranchWorkflowResultSuccess,
|
|
51
|
+
CompareWorkflowsRequest,
|
|
52
|
+
CompareWorkflowsResultFailure,
|
|
53
|
+
CompareWorkflowsResultSuccess,
|
|
48
54
|
DeleteWorkflowRequest,
|
|
49
55
|
DeleteWorkflowResultFailure,
|
|
50
56
|
DeleteWorkflowResultSuccess,
|
|
@@ -57,14 +63,27 @@ from griptape_nodes.retained_mode.events.workflow_events import (
|
|
|
57
63
|
LoadWorkflowMetadata,
|
|
58
64
|
LoadWorkflowMetadataResultFailure,
|
|
59
65
|
LoadWorkflowMetadataResultSuccess,
|
|
66
|
+
MergeWorkflowBranchRequest,
|
|
67
|
+
MergeWorkflowBranchResultFailure,
|
|
68
|
+
MergeWorkflowBranchResultSuccess,
|
|
69
|
+
MoveWorkflowRequest,
|
|
70
|
+
MoveWorkflowResultFailure,
|
|
71
|
+
MoveWorkflowResultSuccess,
|
|
60
72
|
PublishWorkflowRequest,
|
|
61
73
|
PublishWorkflowResultFailure,
|
|
74
|
+
PublishWorkflowResultSuccess,
|
|
62
75
|
RegisterWorkflowRequest,
|
|
63
76
|
RegisterWorkflowResultFailure,
|
|
64
77
|
RegisterWorkflowResultSuccess,
|
|
78
|
+
RegisterWorkflowsFromConfigRequest,
|
|
79
|
+
RegisterWorkflowsFromConfigResultFailure,
|
|
80
|
+
RegisterWorkflowsFromConfigResultSuccess,
|
|
65
81
|
RenameWorkflowRequest,
|
|
66
82
|
RenameWorkflowResultFailure,
|
|
67
83
|
RenameWorkflowResultSuccess,
|
|
84
|
+
ResetWorkflowBranchRequest,
|
|
85
|
+
ResetWorkflowBranchResultFailure,
|
|
86
|
+
ResetWorkflowBranchResultSuccess,
|
|
68
87
|
RunWorkflowFromRegistryRequest,
|
|
69
88
|
RunWorkflowFromRegistryResultFailure,
|
|
70
89
|
RunWorkflowFromRegistryResultSuccess,
|
|
@@ -91,6 +110,7 @@ if TYPE_CHECKING:
|
|
|
91
110
|
from griptape_nodes.exe_types.core_types import Parameter
|
|
92
111
|
from griptape_nodes.node_library.library_registry import LibraryNameAndVersion
|
|
93
112
|
from griptape_nodes.retained_mode.events.base_events import ResultPayload
|
|
113
|
+
from griptape_nodes.retained_mode.events.node_events import SetLockNodeStateRequest
|
|
94
114
|
from griptape_nodes.retained_mode.managers.event_manager import EventManager
|
|
95
115
|
|
|
96
116
|
|
|
@@ -100,6 +120,13 @@ T = TypeVar("T")
|
|
|
100
120
|
logger = logging.getLogger("griptape_nodes")
|
|
101
121
|
|
|
102
122
|
|
|
123
|
+
class WorkflowRegistrationResult(NamedTuple):
|
|
124
|
+
"""Result of processing workflows for registration."""
|
|
125
|
+
|
|
126
|
+
succeeded: list[str]
|
|
127
|
+
failed: list[str]
|
|
128
|
+
|
|
129
|
+
|
|
103
130
|
class WorkflowManager:
|
|
104
131
|
WORKFLOW_METADATA_HEADER: ClassVar[str] = "script"
|
|
105
132
|
MAX_MINOR_VERSION_DEVIATION: ClassVar[int] = (
|
|
@@ -226,6 +253,10 @@ class WorkflowManager:
|
|
|
226
253
|
RenameWorkflowRequest,
|
|
227
254
|
self.on_rename_workflow_request,
|
|
228
255
|
)
|
|
256
|
+
event_manager.assign_manager_to_request_type(
|
|
257
|
+
MoveWorkflowRequest,
|
|
258
|
+
self.on_move_workflow_request,
|
|
259
|
+
)
|
|
229
260
|
|
|
230
261
|
event_manager.assign_manager_to_request_type(
|
|
231
262
|
SaveWorkflowRequest,
|
|
@@ -240,6 +271,26 @@ class WorkflowManager:
|
|
|
240
271
|
ImportWorkflowAsReferencedSubFlowRequest,
|
|
241
272
|
self.on_import_workflow_as_referenced_sub_flow_request,
|
|
242
273
|
)
|
|
274
|
+
event_manager.assign_manager_to_request_type(
|
|
275
|
+
BranchWorkflowRequest,
|
|
276
|
+
self.on_branch_workflow_request,
|
|
277
|
+
)
|
|
278
|
+
event_manager.assign_manager_to_request_type(
|
|
279
|
+
MergeWorkflowBranchRequest,
|
|
280
|
+
self.on_merge_workflow_branch_request,
|
|
281
|
+
)
|
|
282
|
+
event_manager.assign_manager_to_request_type(
|
|
283
|
+
ResetWorkflowBranchRequest,
|
|
284
|
+
self.on_reset_workflow_branch_request,
|
|
285
|
+
)
|
|
286
|
+
event_manager.assign_manager_to_request_type(
|
|
287
|
+
CompareWorkflowsRequest,
|
|
288
|
+
self.on_compare_workflows_request,
|
|
289
|
+
)
|
|
290
|
+
event_manager.assign_manager_to_request_type(
|
|
291
|
+
RegisterWorkflowsFromConfigRequest,
|
|
292
|
+
self.on_register_workflows_from_config_request,
|
|
293
|
+
)
|
|
243
294
|
|
|
244
295
|
def has_current_referenced_workflow(self) -> bool:
|
|
245
296
|
"""Check if there is currently a referenced workflow context active."""
|
|
@@ -257,7 +308,15 @@ class WorkflowManager:
|
|
|
257
308
|
# All of the libraries have loaded, and any workflows they came with have been registered.
|
|
258
309
|
# See if there are USER workflow JSONs to load.
|
|
259
310
|
default_workflow_section = "app_events.on_app_initialization_complete.workflows_to_register"
|
|
260
|
-
|
|
311
|
+
|
|
312
|
+
# Use the request/response pattern for workflow registration
|
|
313
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
314
|
+
|
|
315
|
+
register_request = RegisterWorkflowsFromConfigRequest(config_section=default_workflow_section)
|
|
316
|
+
register_result = GriptapeNodes.handle_request(register_request)
|
|
317
|
+
|
|
318
|
+
if not isinstance(register_result, RegisterWorkflowsFromConfigResultSuccess):
|
|
319
|
+
logger.warning("Failed to register workflows from configuration during library initialization")
|
|
261
320
|
|
|
262
321
|
# Print it all out nicely.
|
|
263
322
|
self.print_workflow_load_status()
|
|
@@ -610,6 +669,82 @@ class WorkflowManager:
|
|
|
610
669
|
|
|
611
670
|
return RenameWorkflowResultSuccess()
|
|
612
671
|
|
|
672
|
+
def on_move_workflow_request(self, request: MoveWorkflowRequest) -> ResultPayload: # noqa: PLR0911
|
|
673
|
+
try:
|
|
674
|
+
# Validate source workflow exists
|
|
675
|
+
workflow = WorkflowRegistry.get_workflow_by_name(request.workflow_name)
|
|
676
|
+
except KeyError:
|
|
677
|
+
logger.error("Failed to move workflow '%s' because it does not exist", request.workflow_name)
|
|
678
|
+
return MoveWorkflowResultFailure()
|
|
679
|
+
|
|
680
|
+
config_manager = GriptapeNodes.ConfigManager()
|
|
681
|
+
|
|
682
|
+
# Get current file path
|
|
683
|
+
current_file_path = WorkflowRegistry.get_complete_file_path(workflow.file_path)
|
|
684
|
+
if not Path(current_file_path).exists():
|
|
685
|
+
logger.error(
|
|
686
|
+
"Failed to move workflow '%s': File path '%s' does not exist", request.workflow_name, current_file_path
|
|
687
|
+
)
|
|
688
|
+
return MoveWorkflowResultFailure()
|
|
689
|
+
|
|
690
|
+
# Clean and validate target directory
|
|
691
|
+
target_directory = request.target_directory.strip().replace("\\", "/")
|
|
692
|
+
target_directory = target_directory.removeprefix("/") # Remove leading slash
|
|
693
|
+
|
|
694
|
+
# Create target directory path
|
|
695
|
+
target_dir_path = config_manager.workspace_path / target_directory
|
|
696
|
+
|
|
697
|
+
try:
|
|
698
|
+
# Create target directory if it doesn't exist
|
|
699
|
+
target_dir_path.mkdir(parents=True, exist_ok=True)
|
|
700
|
+
except OSError as e:
|
|
701
|
+
logger.error("Failed to create target directory '%s': %s", target_dir_path, str(e))
|
|
702
|
+
return MoveWorkflowResultFailure()
|
|
703
|
+
|
|
704
|
+
# Create new file path
|
|
705
|
+
workflow_filename = Path(workflow.file_path).name
|
|
706
|
+
new_relative_path = str(Path(target_directory) / workflow_filename)
|
|
707
|
+
new_absolute_path = config_manager.workspace_path / new_relative_path
|
|
708
|
+
|
|
709
|
+
# Check if target file already exists
|
|
710
|
+
if new_absolute_path.exists():
|
|
711
|
+
logger.error(
|
|
712
|
+
"Failed to move workflow '%s': Target file '%s' already exists",
|
|
713
|
+
request.workflow_name,
|
|
714
|
+
new_absolute_path,
|
|
715
|
+
)
|
|
716
|
+
return MoveWorkflowResultFailure()
|
|
717
|
+
|
|
718
|
+
try:
|
|
719
|
+
# Move the file
|
|
720
|
+
Path(current_file_path).rename(new_absolute_path)
|
|
721
|
+
|
|
722
|
+
# Update workflow registry with new file path
|
|
723
|
+
workflow.file_path = new_relative_path
|
|
724
|
+
|
|
725
|
+
# Update configuration - remove old path and add new path
|
|
726
|
+
config_manager.delete_user_workflow(workflow.file_path)
|
|
727
|
+
config_manager.save_user_workflow_json(str(new_absolute_path))
|
|
728
|
+
|
|
729
|
+
except OSError as e:
|
|
730
|
+
logger.error("Failed to move workflow file '%s' to '%s': %s", current_file_path, new_absolute_path, str(e))
|
|
731
|
+
|
|
732
|
+
# Attempt to rollback if file was moved but registry update failed
|
|
733
|
+
if new_absolute_path.exists() and not Path(current_file_path).exists():
|
|
734
|
+
try:
|
|
735
|
+
new_absolute_path.rename(current_file_path)
|
|
736
|
+
logger.info("Rolled back file move for workflow '%s'", request.workflow_name)
|
|
737
|
+
except OSError:
|
|
738
|
+
logger.error("Failed to rollback file move for workflow '%s'", request.workflow_name)
|
|
739
|
+
|
|
740
|
+
return MoveWorkflowResultFailure()
|
|
741
|
+
except Exception as e:
|
|
742
|
+
logger.error("Failed to move workflow '%s': %s", request.workflow_name, str(e))
|
|
743
|
+
return MoveWorkflowResultFailure()
|
|
744
|
+
else:
|
|
745
|
+
logger.info("Successfully moved workflow '%s' to '%s'", request.workflow_name, new_relative_path)
|
|
746
|
+
return MoveWorkflowResultSuccess(moved_file_path=new_relative_path)
|
|
747
|
+
|
|
613
748
|
def on_load_workflow_metadata_request( # noqa: C901, PLR0912, PLR0915
|
|
614
749
|
self, request: LoadWorkflowMetadata
|
|
615
750
|
) -> ResultPayload:
|
|
@@ -932,7 +1067,12 @@ class WorkflowManager:
|
|
|
932
1067
|
return import_statements
|
|
933
1068
|
|
|
934
1069
|
def _generate_workflow_file_contents_and_metadata( # noqa: C901, PLR0912, PLR0915
|
|
935
|
-
self,
|
|
1070
|
+
self,
|
|
1071
|
+
file_name: str,
|
|
1072
|
+
creation_date: datetime,
|
|
1073
|
+
image_path: str | None = None,
|
|
1074
|
+
prior_workflow: Workflow | None = None,
|
|
1075
|
+
custom_metadata: WorkflowMetadata | None = None,
|
|
936
1076
|
) -> tuple[str, WorkflowMetadata]:
|
|
937
1077
|
"""Generate the contents of a workflow file.
|
|
938
1078
|
|
|
@@ -940,6 +1080,10 @@ class WorkflowManager:
|
|
|
940
1080
|
file_name: The name of the workflow file
|
|
941
1081
|
creation_date: The creation date for the workflow
|
|
942
1082
|
image_path: Optional; the path to an image to include in the workflow metadata
|
|
1083
|
+
prior_workflow: Optional; existing workflow to preserve branch info from
|
|
1084
|
+
custom_metadata: Optional; pre-constructed metadata to use instead of generating it
|
|
1085
|
+
from the current workflow state. When provided, this metadata will be
|
|
1086
|
+
used directly, allowing branch/merge operations to pass specific metadata.
|
|
943
1087
|
|
|
944
1088
|
Returns:
|
|
945
1089
|
A tuple of (workflow_file_contents, workflow_metadata)
|
|
@@ -988,22 +1132,27 @@ class WorkflowManager:
|
|
|
988
1132
|
raise TypeError(details)
|
|
989
1133
|
serialized_flow_commands = serialized_flow_result.serialized_flow_commands
|
|
990
1134
|
|
|
991
|
-
#
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1135
|
+
# Use custom metadata if provided, otherwise generate it
|
|
1136
|
+
if custom_metadata is not None:
|
|
1137
|
+
workflow_metadata = custom_metadata
|
|
1138
|
+
else:
|
|
1139
|
+
# Create the Workflow Metadata header.
|
|
1140
|
+
workflows_referenced = None
|
|
1141
|
+
if serialized_flow_commands.referenced_workflows:
|
|
1142
|
+
workflows_referenced = list(serialized_flow_commands.referenced_workflows)
|
|
1143
|
+
|
|
1144
|
+
workflow_metadata = self._generate_workflow_metadata(
|
|
1145
|
+
file_name=file_name,
|
|
1146
|
+
engine_version=engine_version,
|
|
1147
|
+
creation_date=creation_date,
|
|
1148
|
+
node_libraries_referenced=list(serialized_flow_commands.node_libraries_used),
|
|
1149
|
+
workflows_referenced=workflows_referenced,
|
|
1150
|
+
prior_workflow=prior_workflow,
|
|
1151
|
+
)
|
|
1152
|
+
if workflow_metadata is None:
|
|
1153
|
+
details = f"Failed to generate metadata for workflow '{file_name}'."
|
|
1154
|
+
logger.error(details)
|
|
1155
|
+
raise ValueError(details)
|
|
1007
1156
|
|
|
1008
1157
|
# Set the image if provided
|
|
1009
1158
|
if image_path:
|
|
@@ -1064,6 +1213,7 @@ class WorkflowManager:
|
|
|
1064
1213
|
or len(serialized_flow_commands.serialized_connections) > 0
|
|
1065
1214
|
or len(serialized_flow_commands.set_parameter_value_commands) > 0
|
|
1066
1215
|
or len(serialized_flow_commands.sub_flows_commands) > 0
|
|
1216
|
+
or len(serialized_flow_commands.set_lock_commands_per_node) > 0
|
|
1067
1217
|
)
|
|
1068
1218
|
|
|
1069
1219
|
if not is_referenced_workflow and has_content_to_serialize:
|
|
@@ -1110,6 +1260,7 @@ class WorkflowManager:
|
|
|
1110
1260
|
# Now generate all the set parameter value code and add it to the flow context.
|
|
1111
1261
|
set_parameter_value_asts = self._generate_set_parameter_value_code(
|
|
1112
1262
|
set_parameter_value_commands=serialized_flow_commands.set_parameter_value_commands,
|
|
1263
|
+
lock_commands=serialized_flow_commands.set_lock_commands_per_node,
|
|
1113
1264
|
node_uuid_to_node_variable_name=node_uuid_to_node_variable_name,
|
|
1114
1265
|
unique_values_dict_name="top_level_unique_values_dict",
|
|
1115
1266
|
import_recorder=import_recorder,
|
|
@@ -1140,8 +1291,6 @@ class WorkflowManager:
|
|
|
1140
1291
|
return final_code_output, workflow_metadata
|
|
1141
1292
|
|
|
1142
1293
|
def on_save_workflow_request(self, request: SaveWorkflowRequest) -> ResultPayload: # noqa: C901, PLR0912, PLR0915
|
|
1143
|
-
local_tz = datetime.now().astimezone().tzinfo
|
|
1144
|
-
|
|
1145
1294
|
# Start with the file name provided; we may change it.
|
|
1146
1295
|
file_name = request.file_name
|
|
1147
1296
|
|
|
@@ -1153,10 +1302,19 @@ class WorkflowManager:
|
|
|
1153
1302
|
prior_workflow = WorkflowRegistry.get_workflow_by_name(file_name)
|
|
1154
1303
|
# We'll use its creation date.
|
|
1155
1304
|
creation_date = prior_workflow.metadata.creation_date
|
|
1305
|
+
elif file_name:
|
|
1306
|
+
# If no prior workflow exists for the new name, check if there's a current workflow
|
|
1307
|
+
# context (e.g., during rename operations) to preserve metadata from
|
|
1308
|
+
context_manager = GriptapeNodes.ContextManager()
|
|
1309
|
+
if context_manager.has_current_workflow():
|
|
1310
|
+
current_workflow_name = context_manager.get_current_workflow_name()
|
|
1311
|
+
if current_workflow_name and WorkflowRegistry.has_workflow_with_name(current_workflow_name):
|
|
1312
|
+
prior_workflow = WorkflowRegistry.get_workflow_by_name(current_workflow_name)
|
|
1313
|
+
creation_date = prior_workflow.metadata.creation_date
|
|
1156
1314
|
|
|
1157
1315
|
if (creation_date is None) or (creation_date == WorkflowManager.EPOCH_START):
|
|
1158
1316
|
# Either a new workflow, or a backcompat situation.
|
|
1159
|
-
creation_date = datetime.now(tz=
|
|
1317
|
+
creation_date = datetime.now(tz=UTC)
|
|
1160
1318
|
|
|
1161
1319
|
# Let's see if this is a template file; if so, re-route it as a copy in the customer's workflow directory.
|
|
1162
1320
|
if prior_workflow and prior_workflow.metadata.is_template:
|
|
@@ -1178,14 +1336,17 @@ class WorkflowManager:
|
|
|
1178
1336
|
|
|
1179
1337
|
# Get file name stuff prepped.
|
|
1180
1338
|
if not file_name:
|
|
1181
|
-
file_name = datetime.now(tz=
|
|
1339
|
+
file_name = datetime.now(tz=UTC).strftime("%d.%m_%H.%M")
|
|
1182
1340
|
relative_file_path = f"{file_name}.py"
|
|
1183
1341
|
file_path = GriptapeNodes.ConfigManager().workspace_path.joinpath(relative_file_path)
|
|
1184
1342
|
|
|
1185
1343
|
# Generate the workflow file contents
|
|
1186
1344
|
try:
|
|
1187
1345
|
final_code_output, workflow_metadata = self._generate_workflow_file_contents_and_metadata(
|
|
1188
|
-
file_name=file_name,
|
|
1346
|
+
file_name=file_name,
|
|
1347
|
+
creation_date=creation_date,
|
|
1348
|
+
image_path=request.image_path,
|
|
1349
|
+
prior_workflow=prior_workflow,
|
|
1189
1350
|
)
|
|
1190
1351
|
except Exception as err:
|
|
1191
1352
|
details = f"Attempted to save workflow '{relative_file_path}', but {err}"
|
|
@@ -1228,19 +1389,27 @@ class WorkflowManager:
|
|
|
1228
1389
|
logger.error("Attempted to save workflow '%s'. Failed when saving configuration: %s", file_name, str(e))
|
|
1229
1390
|
return SaveWorkflowResultFailure()
|
|
1230
1391
|
WorkflowRegistry.generate_new_workflow(metadata=workflow_metadata, file_path=relative_file_path)
|
|
1392
|
+
# Update existing workflow's metadata in the registry
|
|
1393
|
+
existing_workflow = WorkflowRegistry.get_workflow_by_name(file_name)
|
|
1394
|
+
existing_workflow.metadata = workflow_metadata
|
|
1231
1395
|
details = f"Successfully saved workflow to: {serialized_file_path}"
|
|
1232
1396
|
logger.info(details)
|
|
1233
1397
|
return SaveWorkflowResultSuccess(file_path=str(serialized_file_path))
|
|
1234
1398
|
|
|
1235
|
-
def _generate_workflow_metadata(
|
|
1399
|
+
def _generate_workflow_metadata( # noqa: PLR0913
|
|
1236
1400
|
self,
|
|
1237
1401
|
file_name: str,
|
|
1238
1402
|
engine_version: str,
|
|
1239
1403
|
creation_date: datetime,
|
|
1240
1404
|
node_libraries_referenced: list[LibraryNameAndVersion],
|
|
1241
1405
|
workflows_referenced: list[str] | None = None,
|
|
1406
|
+
prior_workflow: Workflow | None = None,
|
|
1242
1407
|
) -> WorkflowMetadata | None:
|
|
1243
|
-
|
|
1408
|
+
# Preserve branched workflow information if it exists
|
|
1409
|
+
branched_from = None
|
|
1410
|
+
if prior_workflow and prior_workflow.metadata.branched_from:
|
|
1411
|
+
branched_from = prior_workflow.metadata.branched_from
|
|
1412
|
+
|
|
1244
1413
|
workflow_metadata = WorkflowMetadata(
|
|
1245
1414
|
name=str(file_name),
|
|
1246
1415
|
schema_version=WorkflowMetadata.LATEST_SCHEMA_VERSION,
|
|
@@ -1248,11 +1417,35 @@ class WorkflowManager:
|
|
|
1248
1417
|
node_libraries_referenced=node_libraries_referenced,
|
|
1249
1418
|
workflows_referenced=workflows_referenced,
|
|
1250
1419
|
creation_date=creation_date,
|
|
1251
|
-
last_modified_date=datetime.now(tz=
|
|
1420
|
+
last_modified_date=datetime.now(tz=UTC),
|
|
1421
|
+
branched_from=branched_from,
|
|
1252
1422
|
)
|
|
1253
1423
|
|
|
1254
1424
|
return workflow_metadata
|
|
1255
1425
|
|
|
1426
|
+
def _replace_workflow_metadata_header(self, workflow_content: str, new_metadata: WorkflowMetadata) -> str | None:
|
|
1427
|
+
"""Replace the metadata header in a workflow file with new metadata.
|
|
1428
|
+
|
|
1429
|
+
Args:
|
|
1430
|
+
workflow_content: The full content of the workflow file
|
|
1431
|
+
new_metadata: The new metadata to replace the existing header with
|
|
1432
|
+
|
|
1433
|
+
Returns:
|
|
1434
|
+
The workflow content with updated metadata header, or None if replacement failed
|
|
1435
|
+
"""
|
|
1436
|
+
import re
|
|
1437
|
+
|
|
1438
|
+
# Generate the new metadata header
|
|
1439
|
+
new_metadata_header = self._generate_workflow_metadata_header(new_metadata)
|
|
1440
|
+
if new_metadata_header is None:
|
|
1441
|
+
return None
|
|
1442
|
+
|
|
1443
|
+
# Replace the metadata block using regex
|
|
1444
|
+
metadata_pattern = r"(# /// script\n)(.*?)(# ///)"
|
|
1445
|
+
updated_content = re.sub(metadata_pattern, new_metadata_header, workflow_content, flags=re.DOTALL)
|
|
1446
|
+
|
|
1447
|
+
return updated_content
|
|
1448
|
+
|
|
1256
1449
|
def _generate_workflow_metadata_header(self, workflow_metadata: WorkflowMetadata) -> str | None:
|
|
1257
1450
|
try:
|
|
1258
1451
|
toml_doc = tomlkit.document()
|
|
@@ -2503,6 +2696,7 @@ class WorkflowManager:
|
|
|
2503
2696
|
set_parameter_value_commands: dict[
|
|
2504
2697
|
SerializedNodeCommands.NodeUUID, list[SerializedNodeCommands.IndirectSetParameterValueCommand]
|
|
2505
2698
|
],
|
|
2699
|
+
lock_commands: dict[SerializedNodeCommands.NodeUUID, SetLockNodeStateRequest],
|
|
2506
2700
|
node_uuid_to_node_variable_name: dict[SerializedNodeCommands.NodeUUID, str],
|
|
2507
2701
|
unique_values_dict_name: str,
|
|
2508
2702
|
import_recorder: ImportRecorder,
|
|
@@ -2510,9 +2704,14 @@ class WorkflowManager:
|
|
|
2510
2704
|
parameter_value_asts = []
|
|
2511
2705
|
for node_uuid, indirect_set_parameter_value_commands in set_parameter_value_commands.items():
|
|
2512
2706
|
node_variable_name = node_uuid_to_node_variable_name[node_uuid]
|
|
2707
|
+
lock_node_command = lock_commands.get(node_uuid)
|
|
2513
2708
|
parameter_value_asts.extend(
|
|
2514
2709
|
self._generate_set_parameter_value_for_node(
|
|
2515
|
-
node_variable_name,
|
|
2710
|
+
node_variable_name,
|
|
2711
|
+
indirect_set_parameter_value_commands,
|
|
2712
|
+
unique_values_dict_name,
|
|
2713
|
+
import_recorder,
|
|
2714
|
+
lock_node_command,
|
|
2516
2715
|
)
|
|
2517
2716
|
)
|
|
2518
2717
|
return parameter_value_asts
|
|
@@ -2523,13 +2722,15 @@ class WorkflowManager:
|
|
|
2523
2722
|
indirect_set_parameter_value_commands: list[SerializedNodeCommands.IndirectSetParameterValueCommand],
|
|
2524
2723
|
unique_values_dict_name: str,
|
|
2525
2724
|
import_recorder: ImportRecorder,
|
|
2725
|
+
lock_node_command: SetLockNodeStateRequest | None = None,
|
|
2526
2726
|
) -> list[ast.stmt]:
|
|
2527
|
-
if not indirect_set_parameter_value_commands:
|
|
2727
|
+
if not indirect_set_parameter_value_commands and lock_node_command is None:
|
|
2528
2728
|
return []
|
|
2529
2729
|
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2730
|
+
if indirect_set_parameter_value_commands:
|
|
2731
|
+
import_recorder.add_from_import(
|
|
2732
|
+
"griptape_nodes.retained_mode.events.parameter_events", "SetParameterValueRequest"
|
|
2733
|
+
)
|
|
2533
2734
|
|
|
2534
2735
|
set_parameter_value_asts = []
|
|
2535
2736
|
with_node_context = ast.With(
|
|
@@ -2613,6 +2814,44 @@ class WorkflowManager:
|
|
|
2613
2814
|
)
|
|
2614
2815
|
with_node_context.body.append(set_parameter_value_request_call)
|
|
2615
2816
|
|
|
2817
|
+
# Add lock command as the LAST command in the with context
|
|
2818
|
+
if lock_node_command is not None:
|
|
2819
|
+
import_recorder.add_from_import(
|
|
2820
|
+
"griptape_nodes.retained_mode.events.node_events", "SetLockNodeStateRequest"
|
|
2821
|
+
)
|
|
2822
|
+
|
|
2823
|
+
lock_node_call_ast = ast.Expr(
|
|
2824
|
+
value=ast.Call(
|
|
2825
|
+
func=ast.Attribute(
|
|
2826
|
+
value=ast.Name(id="GriptapeNodes", ctx=ast.Load(), lineno=1, col_offset=0),
|
|
2827
|
+
attr="handle_request",
|
|
2828
|
+
ctx=ast.Load(),
|
|
2829
|
+
lineno=1,
|
|
2830
|
+
col_offset=0,
|
|
2831
|
+
),
|
|
2832
|
+
args=[
|
|
2833
|
+
ast.Call(
|
|
2834
|
+
func=ast.Name(id="SetLockNodeStateRequest", ctx=ast.Load(), lineno=1, col_offset=0),
|
|
2835
|
+
args=[],
|
|
2836
|
+
keywords=[
|
|
2837
|
+
ast.keyword(arg="node_name", value=ast.Constant(value=None, lineno=1, col_offset=0)),
|
|
2838
|
+
ast.keyword(
|
|
2839
|
+
arg="lock", value=ast.Constant(value=lock_node_command.lock, lineno=1, col_offset=0)
|
|
2840
|
+
),
|
|
2841
|
+
],
|
|
2842
|
+
lineno=1,
|
|
2843
|
+
col_offset=0,
|
|
2844
|
+
)
|
|
2845
|
+
],
|
|
2846
|
+
keywords=[],
|
|
2847
|
+
lineno=1,
|
|
2848
|
+
col_offset=0,
|
|
2849
|
+
),
|
|
2850
|
+
lineno=1,
|
|
2851
|
+
col_offset=0,
|
|
2852
|
+
)
|
|
2853
|
+
with_node_context.body.append(lock_node_call_ast)
|
|
2854
|
+
|
|
2616
2855
|
set_parameter_value_asts.append(with_node_context)
|
|
2617
2856
|
return set_parameter_value_asts
|
|
2618
2857
|
|
|
@@ -2727,13 +2966,47 @@ class WorkflowManager:
|
|
|
2727
2966
|
msg = f"No publishing handler found for '{publisher_name}' in request type '{type(request).__name__}'."
|
|
2728
2967
|
raise ValueError(msg) # noqa: TRY301
|
|
2729
2968
|
|
|
2730
|
-
|
|
2731
|
-
|
|
2969
|
+
result = publishing_handler.handler(request)
|
|
2970
|
+
if isinstance(result, PublishWorkflowResultSuccess):
|
|
2971
|
+
file = Path(result.published_workflow_file_path)
|
|
2972
|
+
self._register_published_workflow_file(file)
|
|
2973
|
+
return result # noqa: TRY300
|
|
2732
2974
|
except Exception as e:
|
|
2733
2975
|
details = f"Failed to publish workflow '{request.workflow_name}': {e!s}"
|
|
2734
2976
|
logger.exception(details)
|
|
2735
2977
|
return PublishWorkflowResultFailure(exception=e)
|
|
2736
2978
|
|
|
2979
|
+
def _register_published_workflow_file(self, workflow_file: Path) -> None:
|
|
2980
|
+
"""Register a published workflow file in the workflow registry."""
|
|
2981
|
+
if workflow_file.exists() and workflow_file.is_file():
|
|
2982
|
+
load_workflow_metadata_request = LoadWorkflowMetadata(
|
|
2983
|
+
file_name=workflow_file.name,
|
|
2984
|
+
)
|
|
2985
|
+
load_metadata_result = self.on_load_workflow_metadata_request(load_workflow_metadata_request)
|
|
2986
|
+
if isinstance(load_metadata_result, LoadWorkflowMetadataResultSuccess):
|
|
2987
|
+
register_workflow_result = self.on_register_workflow_request(
|
|
2988
|
+
RegisterWorkflowRequest(
|
|
2989
|
+
metadata=load_metadata_result.metadata,
|
|
2990
|
+
file_name=workflow_file.name,
|
|
2991
|
+
)
|
|
2992
|
+
)
|
|
2993
|
+
if isinstance(register_workflow_result, RegisterWorkflowResultSuccess):
|
|
2994
|
+
logger.info(
|
|
2995
|
+
"Successfully registered new workflow with file '%s'.",
|
|
2996
|
+
workflow_file.name,
|
|
2997
|
+
)
|
|
2998
|
+
else:
|
|
2999
|
+
logger.warning(
|
|
3000
|
+
"Failed to register workflow with file '%s': %s",
|
|
3001
|
+
workflow_file.name,
|
|
3002
|
+
cast("RegisterWorkflowResultFailure", register_workflow_result).exception,
|
|
3003
|
+
)
|
|
3004
|
+
else:
|
|
3005
|
+
logger.warning(
|
|
3006
|
+
"Failed to load metadata for workflow file '%s'. Not registering workflow.",
|
|
3007
|
+
workflow_file.name,
|
|
3008
|
+
)
|
|
3009
|
+
|
|
2737
3010
|
def on_import_workflow_as_referenced_sub_flow_request(
|
|
2738
3011
|
self, request: ImportWorkflowAsReferencedSubFlowRequest
|
|
2739
3012
|
) -> ResultPayload:
|
|
@@ -2875,6 +3148,341 @@ class WorkflowManager:
|
|
|
2875
3148
|
)
|
|
2876
3149
|
return ImportWorkflowAsReferencedSubFlowResultSuccess(created_flow_name=created_flow_name)
|
|
2877
3150
|
|
|
3151
|
+
def on_branch_workflow_request(self, request: BranchWorkflowRequest) -> ResultPayload:
|
|
3152
|
+
"""Create a branch (copy) of an existing workflow with branch tracking."""
|
|
3153
|
+
try:
|
|
3154
|
+
# Validate source workflow exists
|
|
3155
|
+
source_workflow = WorkflowRegistry.get_workflow_by_name(request.workflow_name)
|
|
3156
|
+
except KeyError:
|
|
3157
|
+
logger.error("Failed to branch workflow '%s' because it does not exist", request.workflow_name)
|
|
3158
|
+
return BranchWorkflowResultFailure()
|
|
3159
|
+
|
|
3160
|
+
# Generate branch name if not provided
|
|
3161
|
+
branch_name = request.branched_workflow_name
|
|
3162
|
+
if branch_name is None:
|
|
3163
|
+
base_name = request.workflow_name
|
|
3164
|
+
counter = 1
|
|
3165
|
+
branch_name = f"{base_name}_branch_{counter}"
|
|
3166
|
+
while WorkflowRegistry.has_workflow_with_name(branch_name):
|
|
3167
|
+
counter += 1
|
|
3168
|
+
branch_name = f"{base_name}_branch_{counter}"
|
|
3169
|
+
|
|
3170
|
+
# Check if branch name already exists
|
|
3171
|
+
if WorkflowRegistry.has_workflow_with_name(branch_name):
|
|
3172
|
+
logger.error(
|
|
3173
|
+
"Failed to branch workflow '%s' because branch name '%s' already exists",
|
|
3174
|
+
request.workflow_name,
|
|
3175
|
+
branch_name,
|
|
3176
|
+
)
|
|
3177
|
+
return BranchWorkflowResultFailure()
|
|
3178
|
+
|
|
3179
|
+
try:
|
|
3180
|
+
# Create branch metadata by copying source metadata
|
|
3181
|
+
branch_metadata = WorkflowMetadata(
|
|
3182
|
+
name=branch_name,
|
|
3183
|
+
schema_version=source_workflow.metadata.schema_version,
|
|
3184
|
+
engine_version_created_with=source_workflow.metadata.engine_version_created_with,
|
|
3185
|
+
node_libraries_referenced=source_workflow.metadata.node_libraries_referenced.copy(),
|
|
3186
|
+
workflows_referenced=source_workflow.metadata.workflows_referenced.copy()
|
|
3187
|
+
if source_workflow.metadata.workflows_referenced
|
|
3188
|
+
else None,
|
|
3189
|
+
description=source_workflow.metadata.description,
|
|
3190
|
+
image=source_workflow.metadata.image,
|
|
3191
|
+
is_griptape_provided=False, # Branches are always user-created
|
|
3192
|
+
is_template=False,
|
|
3193
|
+
creation_date=datetime.now(tz=UTC),
|
|
3194
|
+
last_modified_date=source_workflow.metadata.last_modified_date,
|
|
3195
|
+
branched_from=request.workflow_name,
|
|
3196
|
+
)
|
|
3197
|
+
|
|
3198
|
+
# Prepare branch file path
|
|
3199
|
+
branch_file_path = f"{branch_name}.py"
|
|
3200
|
+
|
|
3201
|
+
# Read source workflow content and replace metadata header
|
|
3202
|
+
source_file_path = WorkflowRegistry.get_complete_file_path(source_workflow.file_path)
|
|
3203
|
+
if not Path(source_file_path).exists():
|
|
3204
|
+
logger.error(
|
|
3205
|
+
"Failed to branch workflow '%s': File path '%s' does not exist. "
|
|
3206
|
+
"The workflow may have been moved or the workspace configuration may have changed.",
|
|
3207
|
+
request.workflow_name,
|
|
3208
|
+
source_file_path,
|
|
3209
|
+
)
|
|
3210
|
+
return BranchWorkflowResultFailure()
|
|
3211
|
+
|
|
3212
|
+
source_content = Path(source_file_path).read_text(encoding="utf-8")
|
|
3213
|
+
|
|
3214
|
+
# Replace the metadata header with branch metadata
|
|
3215
|
+
branch_content = self._replace_workflow_metadata_header(source_content, branch_metadata)
|
|
3216
|
+
if branch_content is None:
|
|
3217
|
+
logger.error("Failed to replace metadata header for branch workflow '%s'", branch_name)
|
|
3218
|
+
return BranchWorkflowResultFailure()
|
|
3219
|
+
|
|
3220
|
+
# Write branch workflow file to disk BEFORE registering in registry
|
|
3221
|
+
branch_full_path = WorkflowRegistry.get_complete_file_path(branch_file_path)
|
|
3222
|
+
Path(branch_full_path).write_text(branch_content, encoding="utf-8")
|
|
3223
|
+
|
|
3224
|
+
# Now create the branch workflow in registry (file must exist on disk first)
|
|
3225
|
+
WorkflowRegistry.generate_new_workflow(metadata=branch_metadata, file_path=branch_file_path)
|
|
3226
|
+
|
|
3227
|
+
# Register the branch in user configuration
|
|
3228
|
+
config_manager = GriptapeNodes.ConfigManager()
|
|
3229
|
+
config_manager.save_user_workflow_json(branch_full_path)
|
|
3230
|
+
|
|
3231
|
+
logger.info("Successfully branched workflow '%s' as '%s'", request.workflow_name, branch_name)
|
|
3232
|
+
return BranchWorkflowResultSuccess(
|
|
3233
|
+
branched_workflow_name=branch_name, original_workflow_name=request.workflow_name
|
|
3234
|
+
)
|
|
3235
|
+
|
|
3236
|
+
except Exception as e:
|
|
3237
|
+
logger.error("Failed to branch workflow '%s': %s", request.workflow_name, str(e))
|
|
3238
|
+
import traceback
|
|
3239
|
+
|
|
3240
|
+
traceback.print_exc()
|
|
3241
|
+
return BranchWorkflowResultFailure()
|
|
3242
|
+
|
|
3243
|
+
def on_merge_workflow_branch_request(self, request: MergeWorkflowBranchRequest) -> ResultPayload:
|
|
3244
|
+
"""Merge a branch back into its source workflow, removing the branch when complete."""
|
|
3245
|
+
try:
|
|
3246
|
+
# Validate branch workflow exists
|
|
3247
|
+
branch_workflow = WorkflowRegistry.get_workflow_by_name(request.workflow_name)
|
|
3248
|
+
except KeyError as e:
|
|
3249
|
+
logger.error("Failed to merge workflow branch because it does not exist: %s", str(e))
|
|
3250
|
+
return MergeWorkflowBranchResultFailure()
|
|
3251
|
+
|
|
3252
|
+
# Get source workflow name from branch metadata
|
|
3253
|
+
source_workflow_name = branch_workflow.metadata.branched_from
|
|
3254
|
+
if not source_workflow_name:
|
|
3255
|
+
logger.error(
|
|
3256
|
+
"Failed to merge workflow branch '%s' because it has no source workflow",
|
|
3257
|
+
request.workflow_name,
|
|
3258
|
+
)
|
|
3259
|
+
return MergeWorkflowBranchResultFailure()
|
|
3260
|
+
|
|
3261
|
+
# Validate source workflow exists
|
|
3262
|
+
try:
|
|
3263
|
+
source_workflow = WorkflowRegistry.get_workflow_by_name(source_workflow_name)
|
|
3264
|
+
except KeyError:
|
|
3265
|
+
logger.error(
|
|
3266
|
+
"Failed to merge workflow branch '%s' because source workflow '%s' does not exist",
|
|
3267
|
+
request.workflow_name,
|
|
3268
|
+
source_workflow_name,
|
|
3269
|
+
)
|
|
3270
|
+
return MergeWorkflowBranchResultFailure()
|
|
3271
|
+
|
|
3272
|
+
try:
|
|
3273
|
+
# Create updated metadata for source workflow - update timestamp
|
|
3274
|
+
merged_metadata = WorkflowMetadata(
|
|
3275
|
+
name=source_workflow_name,
|
|
3276
|
+
schema_version=source_workflow.metadata.schema_version,
|
|
3277
|
+
engine_version_created_with=source_workflow.metadata.engine_version_created_with,
|
|
3278
|
+
node_libraries_referenced=source_workflow.metadata.node_libraries_referenced.copy(),
|
|
3279
|
+
workflows_referenced=source_workflow.metadata.workflows_referenced.copy()
|
|
3280
|
+
if source_workflow.metadata.workflows_referenced
|
|
3281
|
+
else None,
|
|
3282
|
+
description=source_workflow.metadata.description,
|
|
3283
|
+
image=source_workflow.metadata.image,
|
|
3284
|
+
is_griptape_provided=source_workflow.metadata.is_griptape_provided,
|
|
3285
|
+
is_template=source_workflow.metadata.is_template,
|
|
3286
|
+
creation_date=source_workflow.metadata.creation_date,
|
|
3287
|
+
last_modified_date=datetime.now(tz=UTC),
|
|
3288
|
+
branched_from=source_workflow.metadata.branched_from, # Preserve original source chain
|
|
3289
|
+
)
|
|
3290
|
+
|
|
3291
|
+
# Read branch content and replace metadata header with merged metadata
|
|
3292
|
+
branch_content_file_path = WorkflowRegistry.get_complete_file_path(branch_workflow.file_path)
|
|
3293
|
+
branch_content = Path(branch_content_file_path).read_text(encoding="utf-8")
|
|
3294
|
+
|
|
3295
|
+
# Replace the metadata header with merged metadata
|
|
3296
|
+
merged_content = self._replace_workflow_metadata_header(branch_content, merged_metadata)
|
|
3297
|
+
if merged_content is None:
|
|
3298
|
+
logger.error("Failed to replace metadata header for merged workflow '%s'", source_workflow_name)
|
|
3299
|
+
return MergeWorkflowBranchResultFailure()
|
|
3300
|
+
|
|
3301
|
+
# Write the updated content to the source workflow file
|
|
3302
|
+
source_file_path = WorkflowRegistry.get_complete_file_path(source_workflow.file_path)
|
|
3303
|
+
Path(source_file_path).write_text(merged_content, encoding="utf-8")
|
|
3304
|
+
|
|
3305
|
+
# Update the registry with new metadata for the source workflow
|
|
3306
|
+
source_workflow.metadata = merged_metadata
|
|
3307
|
+
|
|
3308
|
+
# Remove the branch workflow from registry and delete file
|
|
3309
|
+
try:
|
|
3310
|
+
WorkflowRegistry.delete_workflow_by_name(request.workflow_name)
|
|
3311
|
+
Path(branch_content_file_path).unlink()
|
|
3312
|
+
logger.info("Deleted branch workflow file and registry entry for '%s'", request.workflow_name)
|
|
3313
|
+
except Exception as delete_error:
|
|
3314
|
+
logger.warning(
|
|
3315
|
+
"Failed to fully clean up branch workflow '%s': %s",
|
|
3316
|
+
request.workflow_name,
|
|
3317
|
+
str(delete_error),
|
|
3318
|
+
)
|
|
3319
|
+
# Continue anyway - the merge was successful even if cleanup failed
|
|
3320
|
+
|
|
3321
|
+
logger.info(
|
|
3322
|
+
"Successfully merged branch workflow '%s' into source workflow '%s'",
|
|
3323
|
+
request.workflow_name,
|
|
3324
|
+
source_workflow_name,
|
|
3325
|
+
)
|
|
3326
|
+
return MergeWorkflowBranchResultSuccess(merged_workflow_name=source_workflow_name)
|
|
3327
|
+
|
|
3328
|
+
except Exception as e:
|
|
3329
|
+
logger.error(
|
|
3330
|
+
"Failed to merge branch workflow '%s' into source workflow '%s': %s",
|
|
3331
|
+
request.workflow_name,
|
|
3332
|
+
source_workflow_name,
|
|
3333
|
+
str(e),
|
|
3334
|
+
)
|
|
3335
|
+
return MergeWorkflowBranchResultFailure()
|
|
3336
|
+
|
|
3337
|
+
def on_reset_workflow_branch_request(self, request: ResetWorkflowBranchRequest) -> ResultPayload:
|
|
3338
|
+
"""Reset a branch to match its source workflow, discarding branch changes."""
|
|
3339
|
+
try:
|
|
3340
|
+
# Validate branch workflow exists
|
|
3341
|
+
branch_workflow = WorkflowRegistry.get_workflow_by_name(request.workflow_name)
|
|
3342
|
+
except KeyError as e:
|
|
3343
|
+
logger.error("Failed to reset workflow branch because it does not exist: %s", str(e))
|
|
3344
|
+
return ResetWorkflowBranchResultFailure()
|
|
3345
|
+
|
|
3346
|
+
# Get source workflow name from branch metadata
|
|
3347
|
+
source_workflow_name = branch_workflow.metadata.branched_from
|
|
3348
|
+
if not source_workflow_name:
|
|
3349
|
+
logger.error(
|
|
3350
|
+
"Failed to reset workflow branch '%s' because it has no source workflow",
|
|
3351
|
+
request.workflow_name,
|
|
3352
|
+
)
|
|
3353
|
+
return ResetWorkflowBranchResultFailure()
|
|
3354
|
+
|
|
3355
|
+
# Validate source workflow exists
|
|
3356
|
+
try:
|
|
3357
|
+
source_workflow = WorkflowRegistry.get_workflow_by_name(source_workflow_name)
|
|
3358
|
+
except KeyError:
|
|
3359
|
+
logger.error(
|
|
3360
|
+
"Failed to reset workflow branch '%s' because source workflow '%s' does not exist",
|
|
3361
|
+
request.workflow_name,
|
|
3362
|
+
source_workflow_name,
|
|
3363
|
+
)
|
|
3364
|
+
return ResetWorkflowBranchResultFailure()
|
|
3365
|
+
|
|
3366
|
+
try:
|
|
3367
|
+
# Read content from the source workflow (what we're resetting the branch to)
|
|
3368
|
+
source_content_file_path = WorkflowRegistry.get_complete_file_path(source_workflow.file_path)
|
|
3369
|
+
source_content = Path(source_content_file_path).read_text(encoding="utf-8")
|
|
3370
|
+
|
|
3371
|
+
# Create updated metadata for branch workflow - preserve branch relationship and source timestamp
|
|
3372
|
+
reset_metadata = WorkflowMetadata(
|
|
3373
|
+
name=request.workflow_name,
|
|
3374
|
+
schema_version=source_workflow.metadata.schema_version,
|
|
3375
|
+
engine_version_created_with=source_workflow.metadata.engine_version_created_with,
|
|
3376
|
+
node_libraries_referenced=source_workflow.metadata.node_libraries_referenced.copy(),
|
|
3377
|
+
workflows_referenced=source_workflow.metadata.workflows_referenced.copy()
|
|
3378
|
+
if source_workflow.metadata.workflows_referenced
|
|
3379
|
+
else None,
|
|
3380
|
+
description=source_workflow.metadata.description,
|
|
3381
|
+
image=source_workflow.metadata.image,
|
|
3382
|
+
is_griptape_provided=branch_workflow.metadata.is_griptape_provided,
|
|
3383
|
+
is_template=branch_workflow.metadata.is_template,
|
|
3384
|
+
creation_date=branch_workflow.metadata.creation_date,
|
|
3385
|
+
last_modified_date=source_workflow.metadata.last_modified_date,
|
|
3386
|
+
branched_from=source_workflow_name, # Preserve branch relationship
|
|
3387
|
+
)
|
|
3388
|
+
|
|
3389
|
+
# Replace the metadata header with reset metadata
|
|
3390
|
+
reset_content = self._replace_workflow_metadata_header(source_content, reset_metadata)
|
|
3391
|
+
if reset_content is None:
|
|
3392
|
+
logger.error("Failed to replace metadata header for reset branch workflow '%s'", request.workflow_name)
|
|
3393
|
+
return ResetWorkflowBranchResultFailure()
|
|
3394
|
+
|
|
3395
|
+
# Write the updated content to the branch workflow file
|
|
3396
|
+
branch_content_file_path = WorkflowRegistry.get_complete_file_path(branch_workflow.file_path)
|
|
3397
|
+
Path(branch_content_file_path).write_text(reset_content, encoding="utf-8")
|
|
3398
|
+
|
|
3399
|
+
# Update the registry with new metadata for the branch workflow
|
|
3400
|
+
branch_workflow.metadata = reset_metadata
|
|
3401
|
+
|
|
3402
|
+
except Exception as e:
|
|
3403
|
+
logger.error(
|
|
3404
|
+
"Failed to reset branch workflow '%s' to source workflow '%s': %s",
|
|
3405
|
+
request.workflow_name,
|
|
3406
|
+
source_workflow_name,
|
|
3407
|
+
str(e),
|
|
3408
|
+
)
|
|
3409
|
+
return ResetWorkflowBranchResultFailure()
|
|
3410
|
+
else:
|
|
3411
|
+
logger.info(
|
|
3412
|
+
"Successfully reset branch workflow '%s' to match source workflow '%s'",
|
|
3413
|
+
request.workflow_name,
|
|
3414
|
+
source_workflow_name,
|
|
3415
|
+
)
|
|
3416
|
+
return ResetWorkflowBranchResultSuccess(reset_workflow_name=request.workflow_name)
|
|
3417
|
+
|
|
3418
|
+
def on_compare_workflows_request(self, request: CompareWorkflowsRequest) -> ResultPayload:
|
|
3419
|
+
"""Compare two workflows to determine if one is ahead, behind, or up-to-date relative to the other."""
|
|
3420
|
+
try:
|
|
3421
|
+
# Get the workflow to evaluate
|
|
3422
|
+
workflow = WorkflowRegistry.get_workflow_by_name(request.workflow_name)
|
|
3423
|
+
except KeyError:
|
|
3424
|
+
logger.error("Failed to compare workflow '%s' because it does not exist", request.workflow_name)
|
|
3425
|
+
return CompareWorkflowsResultFailure()
|
|
3426
|
+
|
|
3427
|
+
# Use the provided compare_workflow_name
|
|
3428
|
+
compare_workflow_name = request.compare_workflow_name
|
|
3429
|
+
|
|
3430
|
+
# Try to get the source workflow
|
|
3431
|
+
try:
|
|
3432
|
+
source_workflow = WorkflowRegistry.get_workflow_by_name(compare_workflow_name)
|
|
3433
|
+
except KeyError:
|
|
3434
|
+
# Source workflow no longer exists
|
|
3435
|
+
details = f"Source workflow '{compare_workflow_name}' for '{request.workflow_name}' no longer exists"
|
|
3436
|
+
return CompareWorkflowsResultSuccess(
|
|
3437
|
+
workflow_name=request.workflow_name,
|
|
3438
|
+
compare_workflow_name=compare_workflow_name,
|
|
3439
|
+
status="no_source",
|
|
3440
|
+
workflow_last_modified=workflow.metadata.last_modified_date.isoformat()
|
|
3441
|
+
if workflow.metadata.last_modified_date
|
|
3442
|
+
else None,
|
|
3443
|
+
source_last_modified=None,
|
|
3444
|
+
details=details,
|
|
3445
|
+
)
|
|
3446
|
+
|
|
3447
|
+
# Compare last modified dates
|
|
3448
|
+
workflow_last_modified = workflow.metadata.last_modified_date
|
|
3449
|
+
source_last_modified = source_workflow.metadata.last_modified_date
|
|
3450
|
+
|
|
3451
|
+
# Handle missing timestamps
|
|
3452
|
+
if workflow_last_modified is None or source_last_modified is None:
|
|
3453
|
+
details = f"Cannot compare timestamps - workflow: {workflow_last_modified}, source: {source_last_modified}"
|
|
3454
|
+
logger.warning(details)
|
|
3455
|
+
return CompareWorkflowsResultSuccess(
|
|
3456
|
+
workflow_name=request.workflow_name,
|
|
3457
|
+
compare_workflow_name=compare_workflow_name,
|
|
3458
|
+
status="diverged",
|
|
3459
|
+
workflow_last_modified=workflow_last_modified.isoformat() if workflow_last_modified else None,
|
|
3460
|
+
source_last_modified=source_last_modified.isoformat() if source_last_modified else None,
|
|
3461
|
+
details=details,
|
|
3462
|
+
)
|
|
3463
|
+
|
|
3464
|
+
# Compare timestamps to determine status
|
|
3465
|
+
if workflow_last_modified == source_last_modified:
|
|
3466
|
+
status = "up_to_date"
|
|
3467
|
+
details = f"Workflow '{request.workflow_name}' is up-to-date with source '{compare_workflow_name}'"
|
|
3468
|
+
elif workflow_last_modified > source_last_modified:
|
|
3469
|
+
status = "ahead"
|
|
3470
|
+
details = f"Workflow '{request.workflow_name}' is ahead of source '{compare_workflow_name}' (local changes)"
|
|
3471
|
+
else:
|
|
3472
|
+
status = "behind"
|
|
3473
|
+
details = (
|
|
3474
|
+
f"Workflow '{request.workflow_name}' is behind source '{compare_workflow_name}' (source has updates)"
|
|
3475
|
+
)
|
|
3476
|
+
|
|
3477
|
+
return CompareWorkflowsResultSuccess(
|
|
3478
|
+
workflow_name=request.workflow_name,
|
|
3479
|
+
compare_workflow_name=compare_workflow_name,
|
|
3480
|
+
status=status,
|
|
3481
|
+
workflow_last_modified=workflow_last_modified.isoformat(),
|
|
3482
|
+
source_last_modified=source_last_modified.isoformat(),
|
|
3483
|
+
details=details,
|
|
3484
|
+
)
|
|
3485
|
+
|
|
2878
3486
|
def _walk_object_tree(
|
|
2879
3487
|
self, obj: Any, process_class_fn: Callable[[type, Any], None], visited: set[int] | None = None
|
|
2880
3488
|
) -> None:
|
|
@@ -3027,6 +3635,101 @@ class WorkflowManager:
|
|
|
3027
3635
|
|
|
3028
3636
|
self._walk_object_tree(obj, collect_class_import)
|
|
3029
3637
|
|
|
3638
|
+
def on_register_workflows_from_config_request(self, request: RegisterWorkflowsFromConfigRequest) -> ResultPayload:
|
|
3639
|
+
"""Register workflows from a configuration section."""
|
|
3640
|
+
try:
|
|
3641
|
+
workflows_to_register = GriptapeNodes.ConfigManager().get_config_value(request.config_section)
|
|
3642
|
+
if not workflows_to_register:
|
|
3643
|
+
logger.info("No workflows found in configuration section '%s'", request.config_section)
|
|
3644
|
+
return RegisterWorkflowsFromConfigResultSuccess(succeeded_workflows=[], failed_workflows=[])
|
|
3645
|
+
|
|
3646
|
+
# Process all workflows and track results
|
|
3647
|
+
succeeded, failed = self._process_workflows_for_registration(workflows_to_register)
|
|
3648
|
+
|
|
3649
|
+
except Exception as e:
|
|
3650
|
+
logger.error(
|
|
3651
|
+
"Failed to register workflows from configuration section '%s': %s", request.config_section, str(e)
|
|
3652
|
+
)
|
|
3653
|
+
return RegisterWorkflowsFromConfigResultFailure()
|
|
3654
|
+
else:
|
|
3655
|
+
return RegisterWorkflowsFromConfigResultSuccess(succeeded_workflows=succeeded, failed_workflows=failed)
|
|
3656
|
+
|
|
3657
|
+
def _process_workflows_for_registration(self, workflows_to_register: list[str]) -> WorkflowRegistrationResult:
|
|
3658
|
+
"""Process a list of workflow paths for registration.
|
|
3659
|
+
|
|
3660
|
+
Returns:
|
|
3661
|
+
WorkflowRegistrationResult with succeeded and failed workflow names
|
|
3662
|
+
"""
|
|
3663
|
+
succeeded = []
|
|
3664
|
+
failed = []
|
|
3665
|
+
|
|
3666
|
+
for workflow_to_register in workflows_to_register:
|
|
3667
|
+
path = Path(workflow_to_register)
|
|
3668
|
+
|
|
3669
|
+
if path.is_dir():
|
|
3670
|
+
dir_result = self._process_workflow_directory(path)
|
|
3671
|
+
succeeded.extend(dir_result.succeeded)
|
|
3672
|
+
failed.extend(dir_result.failed)
|
|
3673
|
+
elif path.suffix == ".py":
|
|
3674
|
+
workflow_name = self._process_single_workflow_file(path)
|
|
3675
|
+
if workflow_name:
|
|
3676
|
+
succeeded.append(workflow_name)
|
|
3677
|
+
else:
|
|
3678
|
+
failed.append(str(path))
|
|
3679
|
+
|
|
3680
|
+
return WorkflowRegistrationResult(succeeded=succeeded, failed=failed)
|
|
3681
|
+
|
|
3682
|
+
def _process_workflow_directory(self, directory_path: Path) -> WorkflowRegistrationResult:
|
|
3683
|
+
"""Process all workflow files in a directory.
|
|
3684
|
+
|
|
3685
|
+
Returns:
|
|
3686
|
+
WorkflowRegistrationResult with succeeded and failed workflow names
|
|
3687
|
+
"""
|
|
3688
|
+
succeeded = []
|
|
3689
|
+
failed = []
|
|
3690
|
+
|
|
3691
|
+
for workflow_file in directory_path.glob("*.py"):
|
|
3692
|
+
# Check that the python file has script metadata
|
|
3693
|
+
metadata_blocks = self.get_workflow_metadata(
|
|
3694
|
+
workflow_file, block_name=WorkflowManager.WORKFLOW_METADATA_HEADER
|
|
3695
|
+
)
|
|
3696
|
+
if len(metadata_blocks) == 1:
|
|
3697
|
+
workflow_name = self._process_single_workflow_file(workflow_file)
|
|
3698
|
+
if workflow_name:
|
|
3699
|
+
succeeded.append(workflow_name)
|
|
3700
|
+
else:
|
|
3701
|
+
failed.append(str(workflow_file))
|
|
3702
|
+
|
|
3703
|
+
return WorkflowRegistrationResult(succeeded=succeeded, failed=failed)
|
|
3704
|
+
|
|
3705
|
+
def _process_single_workflow_file(self, workflow_file: Path) -> str | None:
|
|
3706
|
+
"""Process a single workflow file for registration.
|
|
3707
|
+
|
|
3708
|
+
Returns:
|
|
3709
|
+
Workflow name if registered successfully, None if failed or skipped
|
|
3710
|
+
"""
|
|
3711
|
+
# Parse metadata once and use it for both registration check and actual registration
|
|
3712
|
+
load_metadata_request = LoadWorkflowMetadata(file_name=str(workflow_file))
|
|
3713
|
+
load_metadata_result = self.on_load_workflow_metadata_request(load_metadata_request)
|
|
3714
|
+
|
|
3715
|
+
if not isinstance(load_metadata_result, LoadWorkflowMetadataResultSuccess):
|
|
3716
|
+
logger.debug("Skipping workflow with invalid metadata: %s", workflow_file)
|
|
3717
|
+
return None
|
|
3718
|
+
|
|
3719
|
+
workflow_metadata = load_metadata_result.metadata
|
|
3720
|
+
|
|
3721
|
+
# Check if workflow is already registered using the parsed metadata
|
|
3722
|
+
if WorkflowRegistry.has_workflow_with_name(workflow_metadata.name):
|
|
3723
|
+
logger.debug("Skipping already registered workflow: %s", workflow_file)
|
|
3724
|
+
return None
|
|
3725
|
+
|
|
3726
|
+
# Register workflow using existing method with parsed metadata available
|
|
3727
|
+
# The _register_workflow method will re-parse metadata, but this is acceptable
|
|
3728
|
+
# since we've already validated it's parseable and the duplicate work is minimal
|
|
3729
|
+
if self._register_workflow(str(workflow_file)):
|
|
3730
|
+
return workflow_metadata.name
|
|
3731
|
+
return None
|
|
3732
|
+
|
|
3030
3733
|
|
|
3031
3734
|
class ASTContainer:
|
|
3032
3735
|
"""ASTContainer is a helper class to keep track of AST nodes and generate final code from them."""
|