griptape-nodes 0.44.0__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 +5 -1
- griptape_nodes/app/api.py +2 -35
- griptape_nodes/app/app.py +70 -3
- griptape_nodes/app/watch.py +5 -2
- griptape_nodes/drivers/storage/base_storage_driver.py +37 -0
- griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +2 -1
- griptape_nodes/exe_types/core_types.py +109 -9
- griptape_nodes/exe_types/node_types.py +19 -5
- griptape_nodes/node_library/workflow_registry.py +29 -0
- griptape_nodes/retained_mode/events/app_events.py +3 -2
- griptape_nodes/retained_mode/events/base_events.py +9 -0
- griptape_nodes/retained_mode/events/sync_events.py +60 -0
- griptape_nodes/retained_mode/events/workflow_events.py +231 -0
- griptape_nodes/retained_mode/griptape_nodes.py +8 -0
- griptape_nodes/retained_mode/managers/library_manager.py +6 -18
- griptape_nodes/retained_mode/managers/node_manager.py +2 -2
- griptape_nodes/retained_mode/managers/operation_manager.py +7 -0
- griptape_nodes/retained_mode/managers/settings.py +5 -0
- griptape_nodes/retained_mode/managers/sync_manager.py +498 -0
- griptape_nodes/retained_mode/managers/workflow_manager.py +682 -28
- griptape_nodes/retained_mode/retained_mode.py +23 -0
- griptape_nodes/updater/__init__.py +4 -2
- griptape_nodes/utils/uv_utils.py +18 -0
- {griptape_nodes-0.44.0.dist-info → griptape_nodes-0.45.0.dist-info}/METADATA +2 -1
- {griptape_nodes-0.44.0.dist-info → griptape_nodes-0.45.0.dist-info}/RECORD +27 -24
- {griptape_nodes-0.44.0.dist-info → griptape_nodes-0.45.0.dist-info}/WHEEL +0 -0
- {griptape_nodes-0.44.0.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,
|
|
@@ -101,6 +120,13 @@ T = TypeVar("T")
|
|
|
101
120
|
logger = logging.getLogger("griptape_nodes")
|
|
102
121
|
|
|
103
122
|
|
|
123
|
+
class WorkflowRegistrationResult(NamedTuple):
|
|
124
|
+
"""Result of processing workflows for registration."""
|
|
125
|
+
|
|
126
|
+
succeeded: list[str]
|
|
127
|
+
failed: list[str]
|
|
128
|
+
|
|
129
|
+
|
|
104
130
|
class WorkflowManager:
|
|
105
131
|
WORKFLOW_METADATA_HEADER: ClassVar[str] = "script"
|
|
106
132
|
MAX_MINOR_VERSION_DEVIATION: ClassVar[int] = (
|
|
@@ -227,6 +253,10 @@ class WorkflowManager:
|
|
|
227
253
|
RenameWorkflowRequest,
|
|
228
254
|
self.on_rename_workflow_request,
|
|
229
255
|
)
|
|
256
|
+
event_manager.assign_manager_to_request_type(
|
|
257
|
+
MoveWorkflowRequest,
|
|
258
|
+
self.on_move_workflow_request,
|
|
259
|
+
)
|
|
230
260
|
|
|
231
261
|
event_manager.assign_manager_to_request_type(
|
|
232
262
|
SaveWorkflowRequest,
|
|
@@ -241,6 +271,26 @@ class WorkflowManager:
|
|
|
241
271
|
ImportWorkflowAsReferencedSubFlowRequest,
|
|
242
272
|
self.on_import_workflow_as_referenced_sub_flow_request,
|
|
243
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
|
+
)
|
|
244
294
|
|
|
245
295
|
def has_current_referenced_workflow(self) -> bool:
|
|
246
296
|
"""Check if there is currently a referenced workflow context active."""
|
|
@@ -258,7 +308,15 @@ class WorkflowManager:
|
|
|
258
308
|
# All of the libraries have loaded, and any workflows they came with have been registered.
|
|
259
309
|
# See if there are USER workflow JSONs to load.
|
|
260
310
|
default_workflow_section = "app_events.on_app_initialization_complete.workflows_to_register"
|
|
261
|
-
|
|
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")
|
|
262
320
|
|
|
263
321
|
# Print it all out nicely.
|
|
264
322
|
self.print_workflow_load_status()
|
|
@@ -611,6 +669,82 @@ class WorkflowManager:
|
|
|
611
669
|
|
|
612
670
|
return RenameWorkflowResultSuccess()
|
|
613
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
|
+
|
|
614
748
|
def on_load_workflow_metadata_request( # noqa: C901, PLR0912, PLR0915
|
|
615
749
|
self, request: LoadWorkflowMetadata
|
|
616
750
|
) -> ResultPayload:
|
|
@@ -933,7 +1067,12 @@ class WorkflowManager:
|
|
|
933
1067
|
return import_statements
|
|
934
1068
|
|
|
935
1069
|
def _generate_workflow_file_contents_and_metadata( # noqa: C901, PLR0912, PLR0915
|
|
936
|
-
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,
|
|
937
1076
|
) -> tuple[str, WorkflowMetadata]:
|
|
938
1077
|
"""Generate the contents of a workflow file.
|
|
939
1078
|
|
|
@@ -941,6 +1080,10 @@ class WorkflowManager:
|
|
|
941
1080
|
file_name: The name of the workflow file
|
|
942
1081
|
creation_date: The creation date for the workflow
|
|
943
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.
|
|
944
1087
|
|
|
945
1088
|
Returns:
|
|
946
1089
|
A tuple of (workflow_file_contents, workflow_metadata)
|
|
@@ -989,22 +1132,27 @@ class WorkflowManager:
|
|
|
989
1132
|
raise TypeError(details)
|
|
990
1133
|
serialized_flow_commands = serialized_flow_result.serialized_flow_commands
|
|
991
1134
|
|
|
992
|
-
#
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
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)
|
|
1008
1156
|
|
|
1009
1157
|
# Set the image if provided
|
|
1010
1158
|
if image_path:
|
|
@@ -1143,8 +1291,6 @@ class WorkflowManager:
|
|
|
1143
1291
|
return final_code_output, workflow_metadata
|
|
1144
1292
|
|
|
1145
1293
|
def on_save_workflow_request(self, request: SaveWorkflowRequest) -> ResultPayload: # noqa: C901, PLR0912, PLR0915
|
|
1146
|
-
local_tz = datetime.now().astimezone().tzinfo
|
|
1147
|
-
|
|
1148
1294
|
# Start with the file name provided; we may change it.
|
|
1149
1295
|
file_name = request.file_name
|
|
1150
1296
|
|
|
@@ -1156,10 +1302,19 @@ class WorkflowManager:
|
|
|
1156
1302
|
prior_workflow = WorkflowRegistry.get_workflow_by_name(file_name)
|
|
1157
1303
|
# We'll use its creation date.
|
|
1158
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
|
|
1159
1314
|
|
|
1160
1315
|
if (creation_date is None) or (creation_date == WorkflowManager.EPOCH_START):
|
|
1161
1316
|
# Either a new workflow, or a backcompat situation.
|
|
1162
|
-
creation_date = datetime.now(tz=
|
|
1317
|
+
creation_date = datetime.now(tz=UTC)
|
|
1163
1318
|
|
|
1164
1319
|
# Let's see if this is a template file; if so, re-route it as a copy in the customer's workflow directory.
|
|
1165
1320
|
if prior_workflow and prior_workflow.metadata.is_template:
|
|
@@ -1181,14 +1336,17 @@ class WorkflowManager:
|
|
|
1181
1336
|
|
|
1182
1337
|
# Get file name stuff prepped.
|
|
1183
1338
|
if not file_name:
|
|
1184
|
-
file_name = datetime.now(tz=
|
|
1339
|
+
file_name = datetime.now(tz=UTC).strftime("%d.%m_%H.%M")
|
|
1185
1340
|
relative_file_path = f"{file_name}.py"
|
|
1186
1341
|
file_path = GriptapeNodes.ConfigManager().workspace_path.joinpath(relative_file_path)
|
|
1187
1342
|
|
|
1188
1343
|
# Generate the workflow file contents
|
|
1189
1344
|
try:
|
|
1190
1345
|
final_code_output, workflow_metadata = self._generate_workflow_file_contents_and_metadata(
|
|
1191
|
-
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,
|
|
1192
1350
|
)
|
|
1193
1351
|
except Exception as err:
|
|
1194
1352
|
details = f"Attempted to save workflow '{relative_file_path}', but {err}"
|
|
@@ -1231,19 +1389,27 @@ class WorkflowManager:
|
|
|
1231
1389
|
logger.error("Attempted to save workflow '%s'. Failed when saving configuration: %s", file_name, str(e))
|
|
1232
1390
|
return SaveWorkflowResultFailure()
|
|
1233
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
|
|
1234
1395
|
details = f"Successfully saved workflow to: {serialized_file_path}"
|
|
1235
1396
|
logger.info(details)
|
|
1236
1397
|
return SaveWorkflowResultSuccess(file_path=str(serialized_file_path))
|
|
1237
1398
|
|
|
1238
|
-
def _generate_workflow_metadata(
|
|
1399
|
+
def _generate_workflow_metadata( # noqa: PLR0913
|
|
1239
1400
|
self,
|
|
1240
1401
|
file_name: str,
|
|
1241
1402
|
engine_version: str,
|
|
1242
1403
|
creation_date: datetime,
|
|
1243
1404
|
node_libraries_referenced: list[LibraryNameAndVersion],
|
|
1244
1405
|
workflows_referenced: list[str] | None = None,
|
|
1406
|
+
prior_workflow: Workflow | None = None,
|
|
1245
1407
|
) -> WorkflowMetadata | None:
|
|
1246
|
-
|
|
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
|
+
|
|
1247
1413
|
workflow_metadata = WorkflowMetadata(
|
|
1248
1414
|
name=str(file_name),
|
|
1249
1415
|
schema_version=WorkflowMetadata.LATEST_SCHEMA_VERSION,
|
|
@@ -1251,11 +1417,35 @@ class WorkflowManager:
|
|
|
1251
1417
|
node_libraries_referenced=node_libraries_referenced,
|
|
1252
1418
|
workflows_referenced=workflows_referenced,
|
|
1253
1419
|
creation_date=creation_date,
|
|
1254
|
-
last_modified_date=datetime.now(tz=
|
|
1420
|
+
last_modified_date=datetime.now(tz=UTC),
|
|
1421
|
+
branched_from=branched_from,
|
|
1255
1422
|
)
|
|
1256
1423
|
|
|
1257
1424
|
return workflow_metadata
|
|
1258
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
|
+
|
|
1259
1449
|
def _generate_workflow_metadata_header(self, workflow_metadata: WorkflowMetadata) -> str | None:
|
|
1260
1450
|
try:
|
|
1261
1451
|
toml_doc = tomlkit.document()
|
|
@@ -2776,13 +2966,47 @@ class WorkflowManager:
|
|
|
2776
2966
|
msg = f"No publishing handler found for '{publisher_name}' in request type '{type(request).__name__}'."
|
|
2777
2967
|
raise ValueError(msg) # noqa: TRY301
|
|
2778
2968
|
|
|
2779
|
-
|
|
2780
|
-
|
|
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
|
|
2781
2974
|
except Exception as e:
|
|
2782
2975
|
details = f"Failed to publish workflow '{request.workflow_name}': {e!s}"
|
|
2783
2976
|
logger.exception(details)
|
|
2784
2977
|
return PublishWorkflowResultFailure(exception=e)
|
|
2785
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
|
+
|
|
2786
3010
|
def on_import_workflow_as_referenced_sub_flow_request(
|
|
2787
3011
|
self, request: ImportWorkflowAsReferencedSubFlowRequest
|
|
2788
3012
|
) -> ResultPayload:
|
|
@@ -2924,6 +3148,341 @@ class WorkflowManager:
|
|
|
2924
3148
|
)
|
|
2925
3149
|
return ImportWorkflowAsReferencedSubFlowResultSuccess(created_flow_name=created_flow_name)
|
|
2926
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
|
+
|
|
2927
3486
|
def _walk_object_tree(
|
|
2928
3487
|
self, obj: Any, process_class_fn: Callable[[type, Any], None], visited: set[int] | None = None
|
|
2929
3488
|
) -> None:
|
|
@@ -3076,6 +3635,101 @@ class WorkflowManager:
|
|
|
3076
3635
|
|
|
3077
3636
|
self._walk_object_tree(obj, collect_class_import)
|
|
3078
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
|
+
|
|
3079
3733
|
|
|
3080
3734
|
class ASTContainer:
|
|
3081
3735
|
"""ASTContainer is a helper class to keep track of AST nodes and generate final code from them."""
|