griptape-nodes 0.64.11__py3-none-any.whl → 0.65.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/app/app.py +25 -5
- griptape_nodes/cli/commands/init.py +65 -54
- griptape_nodes/cli/commands/libraries.py +92 -85
- griptape_nodes/cli/commands/self.py +121 -0
- griptape_nodes/common/node_executor.py +2142 -101
- griptape_nodes/exe_types/base_iterative_nodes.py +1004 -0
- griptape_nodes/exe_types/connections.py +114 -19
- griptape_nodes/exe_types/core_types.py +225 -7
- griptape_nodes/exe_types/flow.py +3 -3
- griptape_nodes/exe_types/node_types.py +681 -225
- griptape_nodes/exe_types/param_components/README.md +414 -0
- griptape_nodes/exe_types/param_components/api_key_provider_parameter.py +200 -0
- griptape_nodes/exe_types/param_components/huggingface/huggingface_model_parameter.py +2 -0
- griptape_nodes/exe_types/param_components/huggingface/huggingface_repo_file_parameter.py +79 -5
- griptape_nodes/exe_types/param_types/parameter_button.py +443 -0
- griptape_nodes/machines/control_flow.py +77 -38
- griptape_nodes/machines/dag_builder.py +148 -70
- griptape_nodes/machines/parallel_resolution.py +61 -35
- griptape_nodes/machines/sequential_resolution.py +11 -113
- griptape_nodes/retained_mode/events/app_events.py +1 -0
- griptape_nodes/retained_mode/events/base_events.py +16 -13
- griptape_nodes/retained_mode/events/connection_events.py +3 -0
- griptape_nodes/retained_mode/events/execution_events.py +35 -0
- griptape_nodes/retained_mode/events/flow_events.py +15 -2
- griptape_nodes/retained_mode/events/library_events.py +347 -0
- griptape_nodes/retained_mode/events/node_events.py +48 -0
- griptape_nodes/retained_mode/events/os_events.py +86 -3
- griptape_nodes/retained_mode/events/project_events.py +15 -1
- griptape_nodes/retained_mode/events/workflow_events.py +48 -1
- griptape_nodes/retained_mode/griptape_nodes.py +6 -2
- griptape_nodes/retained_mode/managers/config_manager.py +10 -8
- griptape_nodes/retained_mode/managers/event_manager.py +168 -0
- griptape_nodes/retained_mode/managers/fitness_problems/libraries/__init__.py +2 -0
- griptape_nodes/retained_mode/managers/fitness_problems/libraries/old_xdg_location_warning_problem.py +43 -0
- griptape_nodes/retained_mode/managers/flow_manager.py +664 -123
- griptape_nodes/retained_mode/managers/library_manager.py +1134 -138
- griptape_nodes/retained_mode/managers/model_manager.py +2 -3
- griptape_nodes/retained_mode/managers/node_manager.py +148 -25
- griptape_nodes/retained_mode/managers/object_manager.py +3 -1
- griptape_nodes/retained_mode/managers/operation_manager.py +3 -1
- griptape_nodes/retained_mode/managers/os_manager.py +1158 -122
- griptape_nodes/retained_mode/managers/secrets_manager.py +2 -3
- griptape_nodes/retained_mode/managers/settings.py +21 -1
- griptape_nodes/retained_mode/managers/sync_manager.py +2 -3
- griptape_nodes/retained_mode/managers/workflow_manager.py +358 -104
- griptape_nodes/retained_mode/retained_mode.py +3 -3
- griptape_nodes/traits/button.py +44 -2
- griptape_nodes/traits/file_system_picker.py +2 -2
- griptape_nodes/utils/file_utils.py +101 -0
- griptape_nodes/utils/git_utils.py +1226 -0
- griptape_nodes/utils/library_utils.py +122 -0
- {griptape_nodes-0.64.11.dist-info → griptape_nodes-0.65.0.dist-info}/METADATA +2 -1
- {griptape_nodes-0.64.11.dist-info → griptape_nodes-0.65.0.dist-info}/RECORD +55 -47
- {griptape_nodes-0.64.11.dist-info → griptape_nodes-0.65.0.dist-info}/WHEEL +1 -1
- {griptape_nodes-0.64.11.dist-info → griptape_nodes-0.65.0.dist-info}/entry_points.txt +0 -0
|
@@ -14,7 +14,6 @@ from pathlib import Path
|
|
|
14
14
|
from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple, TypeVar, cast
|
|
15
15
|
|
|
16
16
|
import aiofiles
|
|
17
|
-
import portalocker
|
|
18
17
|
import semver
|
|
19
18
|
import tomlkit
|
|
20
19
|
from rich.box import HEAVY_EDGE
|
|
@@ -59,7 +58,14 @@ from griptape_nodes.retained_mode.events.library_events import (
|
|
|
59
58
|
ListRegisteredLibrariesRequest,
|
|
60
59
|
ListRegisteredLibrariesResultSuccess,
|
|
61
60
|
)
|
|
61
|
+
from griptape_nodes.retained_mode.events.node_events import CreateNodeGroupRequest
|
|
62
62
|
from griptape_nodes.retained_mode.events.object_events import ClearAllObjectStateRequest
|
|
63
|
+
from griptape_nodes.retained_mode.events.os_events import (
|
|
64
|
+
ExistingFilePolicy,
|
|
65
|
+
FileIOFailureReason,
|
|
66
|
+
WriteFileRequest,
|
|
67
|
+
WriteFileResultFailure,
|
|
68
|
+
)
|
|
63
69
|
from griptape_nodes.retained_mode.events.workflow_events import (
|
|
64
70
|
BranchWorkflowRequest,
|
|
65
71
|
BranchWorkflowResultFailure,
|
|
@@ -70,6 +76,9 @@ from griptape_nodes.retained_mode.events.workflow_events import (
|
|
|
70
76
|
DeleteWorkflowRequest,
|
|
71
77
|
DeleteWorkflowResultFailure,
|
|
72
78
|
DeleteWorkflowResultSuccess,
|
|
79
|
+
GetWorkflowMetadataRequest,
|
|
80
|
+
GetWorkflowMetadataResultFailure,
|
|
81
|
+
GetWorkflowMetadataResultSuccess,
|
|
73
82
|
ImportWorkflowAsReferencedSubFlowRequest,
|
|
74
83
|
ImportWorkflowAsReferencedSubFlowResultFailure,
|
|
75
84
|
ImportWorkflowAsReferencedSubFlowResultSuccess,
|
|
@@ -118,6 +127,9 @@ from griptape_nodes.retained_mode.events.workflow_events import (
|
|
|
118
127
|
SaveWorkflowRequest,
|
|
119
128
|
SaveWorkflowResultFailure,
|
|
120
129
|
SaveWorkflowResultSuccess,
|
|
130
|
+
SetWorkflowMetadataRequest,
|
|
131
|
+
SetWorkflowMetadataResultFailure,
|
|
132
|
+
SetWorkflowMetadataResultSuccess,
|
|
121
133
|
)
|
|
122
134
|
from griptape_nodes.retained_mode.griptape_nodes import (
|
|
123
135
|
GriptapeNodes,
|
|
@@ -332,6 +344,14 @@ class WorkflowManager:
|
|
|
332
344
|
PublishWorkflowRequest,
|
|
333
345
|
self.on_publish_workflow_request,
|
|
334
346
|
)
|
|
347
|
+
event_manager.assign_manager_to_request_type(
|
|
348
|
+
SetWorkflowMetadataRequest,
|
|
349
|
+
self.on_set_workflow_metadata_request,
|
|
350
|
+
)
|
|
351
|
+
event_manager.assign_manager_to_request_type(
|
|
352
|
+
GetWorkflowMetadataRequest,
|
|
353
|
+
self.on_get_workflow_metadata_request,
|
|
354
|
+
)
|
|
335
355
|
event_manager.assign_manager_to_request_type(
|
|
336
356
|
ImportWorkflowAsReferencedSubFlowRequest,
|
|
337
357
|
self.on_import_workflow_as_referenced_sub_flow_request,
|
|
@@ -833,10 +853,17 @@ class WorkflowManager:
|
|
|
833
853
|
details = f"Attempted to rename workflow '{request.workflow_name}' to '{request.requested_name}'. Failed while attempting to save."
|
|
834
854
|
return RenameWorkflowResultFailure(result_details=details)
|
|
835
855
|
|
|
836
|
-
|
|
837
|
-
if
|
|
838
|
-
|
|
839
|
-
|
|
856
|
+
# If the original workflow isn't registered, treat this as a Save As and skip deletion
|
|
857
|
+
if WorkflowRegistry.has_workflow_with_name(request.workflow_name):
|
|
858
|
+
delete_workflow_result = await GriptapeNodes.ahandle_request(
|
|
859
|
+
DeleteWorkflowRequest(name=request.workflow_name)
|
|
860
|
+
)
|
|
861
|
+
if isinstance(delete_workflow_result, DeleteWorkflowResultFailure):
|
|
862
|
+
details = (
|
|
863
|
+
f"Attempted to rename workflow '{request.workflow_name}' to '{request.requested_name}'. "
|
|
864
|
+
"Failed while attempting to remove the original file name from the registry."
|
|
865
|
+
)
|
|
866
|
+
return RenameWorkflowResultFailure(result_details=details)
|
|
840
867
|
|
|
841
868
|
return RenameWorkflowResultSuccess(
|
|
842
869
|
result_details=ResultDetails(
|
|
@@ -844,6 +871,96 @@ class WorkflowManager:
|
|
|
844
871
|
)
|
|
845
872
|
)
|
|
846
873
|
|
|
874
|
+
def on_get_workflow_metadata_request(self, request: GetWorkflowMetadataRequest) -> ResultPayload:
|
|
875
|
+
try:
|
|
876
|
+
workflow = WorkflowRegistry.get_workflow_by_name(request.workflow_name)
|
|
877
|
+
except KeyError:
|
|
878
|
+
details = f"Failed to get metadata. Workflow '{request.workflow_name}' not found."
|
|
879
|
+
return GetWorkflowMetadataResultFailure(result_details=details)
|
|
880
|
+
|
|
881
|
+
return GetWorkflowMetadataResultSuccess(
|
|
882
|
+
workflow_metadata=workflow.metadata,
|
|
883
|
+
result_details="Successfully retrieved workflow metadata.",
|
|
884
|
+
)
|
|
885
|
+
|
|
886
|
+
class WorkflowPathResolution(NamedTuple):
|
|
887
|
+
"""Resolution result for workflow and its corresponding file path."""
|
|
888
|
+
|
|
889
|
+
workflow: Workflow | None
|
|
890
|
+
file_path: Path | None
|
|
891
|
+
error: str | None
|
|
892
|
+
|
|
893
|
+
def _get_workflow_and_path(self, workflow_name: str) -> WorkflowPathResolution:
|
|
894
|
+
"""Resolve workflow from registry and return absolute file path."""
|
|
895
|
+
try:
|
|
896
|
+
workflow = WorkflowRegistry.get_workflow_by_name(workflow_name)
|
|
897
|
+
except KeyError:
|
|
898
|
+
return WorkflowManager.WorkflowPathResolution(
|
|
899
|
+
workflow=None, file_path=None, error=f"Failed to set metadata. Workflow '{workflow_name}' not found."
|
|
900
|
+
)
|
|
901
|
+
|
|
902
|
+
complete_file_path = WorkflowRegistry.get_complete_file_path(workflow.file_path)
|
|
903
|
+
file_path_obj = Path(complete_file_path)
|
|
904
|
+
if not file_path_obj.is_file():
|
|
905
|
+
return WorkflowManager.WorkflowPathResolution(
|
|
906
|
+
workflow=workflow,
|
|
907
|
+
file_path=None,
|
|
908
|
+
error=f"Failed to set metadata. File path '{complete_file_path}' does not exist.",
|
|
909
|
+
)
|
|
910
|
+
|
|
911
|
+
return WorkflowManager.WorkflowPathResolution(workflow=workflow, file_path=file_path_obj, error=None)
|
|
912
|
+
|
|
913
|
+
async def _write_metadata_header(self, file_path: Path, workflow_metadata: WorkflowMetadata) -> str | None:
|
|
914
|
+
"""Replace the workflow header and persist changes to disk."""
|
|
915
|
+
try:
|
|
916
|
+
existing_content = file_path.read_text(encoding="utf-8")
|
|
917
|
+
except OSError as e:
|
|
918
|
+
return f"Failed to read workflow file '{file_path}': {e!s}"
|
|
919
|
+
|
|
920
|
+
updated_content = self._replace_workflow_metadata_header(existing_content, workflow_metadata)
|
|
921
|
+
if updated_content is None:
|
|
922
|
+
return "Failed to update metadata header."
|
|
923
|
+
|
|
924
|
+
write_result = self._write_workflow_file(
|
|
925
|
+
file_path=file_path, content=updated_content, file_name=workflow_metadata.name
|
|
926
|
+
)
|
|
927
|
+
if not write_result.success:
|
|
928
|
+
return write_result.error_details
|
|
929
|
+
return None
|
|
930
|
+
|
|
931
|
+
async def on_set_workflow_metadata_request(self, request: SetWorkflowMetadataRequest) -> ResultPayload:
|
|
932
|
+
await self._workflows_loading_complete.wait()
|
|
933
|
+
|
|
934
|
+
# Resolve workflow and file path
|
|
935
|
+
resolution = self._get_workflow_and_path(request.workflow_name)
|
|
936
|
+
if resolution.error is not None or resolution.workflow is None or resolution.file_path is None:
|
|
937
|
+
return SetWorkflowMetadataResultFailure(result_details=resolution.error or "Failed to resolve workflow.")
|
|
938
|
+
|
|
939
|
+
# Use provided metadata object as the new metadata
|
|
940
|
+
new_metadata = request.workflow_metadata
|
|
941
|
+
# Allow JSON dicts from frontend by coercing to WorkflowMetadata
|
|
942
|
+
if isinstance(new_metadata, dict):
|
|
943
|
+
try:
|
|
944
|
+
new_metadata = WorkflowMetadata.model_validate(new_metadata)
|
|
945
|
+
except Exception as e:
|
|
946
|
+
return SetWorkflowMetadataResultFailure(result_details=f"Invalid workflow_metadata: {e!s}")
|
|
947
|
+
# Refresh last_modified_date to reflect this change
|
|
948
|
+
new_metadata.last_modified_date = datetime.now(tz=UTC)
|
|
949
|
+
|
|
950
|
+
# Persist header
|
|
951
|
+
write_error = await self._write_metadata_header(file_path=resolution.file_path, workflow_metadata=new_metadata)
|
|
952
|
+
if write_error is not None:
|
|
953
|
+
return SetWorkflowMetadataResultFailure(result_details=write_error)
|
|
954
|
+
|
|
955
|
+
# Update registry
|
|
956
|
+
resolution.workflow.metadata = new_metadata
|
|
957
|
+
|
|
958
|
+
return SetWorkflowMetadataResultSuccess(
|
|
959
|
+
result_details=ResultDetails(
|
|
960
|
+
message=f"Successfully updated metadata for workflow '{request.workflow_name}'.", level=logging.INFO
|
|
961
|
+
)
|
|
962
|
+
)
|
|
963
|
+
|
|
847
964
|
def on_move_workflow_request(self, request: MoveWorkflowRequest) -> ResultPayload: # noqa: PLR0911
|
|
848
965
|
try:
|
|
849
966
|
# Validate source workflow exists
|
|
@@ -1273,7 +1390,7 @@ class WorkflowManager:
|
|
|
1273
1390
|
def _write_workflow_file(self, file_path: Path, content: str, file_name: str) -> WriteWorkflowFileResult:
|
|
1274
1391
|
"""Write workflow content to file with proper validation and error handling.
|
|
1275
1392
|
|
|
1276
|
-
Uses
|
|
1393
|
+
Uses OSManager's WriteFileRequest for file writing with exclusive locking.
|
|
1277
1394
|
First write wins - if another process is writing, this call fails immediately.
|
|
1278
1395
|
|
|
1279
1396
|
Args:
|
|
@@ -1292,41 +1409,36 @@ class WorkflowManager:
|
|
|
1292
1409
|
details = f"Attempted to save workflow '{file_name}' (requires {min_space_gb:.1f} GB). Failed due to insufficient disk space: {error_msg}"
|
|
1293
1410
|
return self.WriteWorkflowFileResult(success=False, error_details=details)
|
|
1294
1411
|
|
|
1295
|
-
#
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
with portalocker.Lock(
|
|
1306
|
-
file_path,
|
|
1307
|
-
mode="w",
|
|
1308
|
-
encoding="utf-8",
|
|
1309
|
-
timeout=0, # Non-blocking: fail immediately if locked
|
|
1310
|
-
flags=portalocker.LockFlags.EXCLUSIVE,
|
|
1311
|
-
) as fh:
|
|
1312
|
-
fh.write(content)
|
|
1313
|
-
except portalocker.LockException:
|
|
1314
|
-
error_details = (
|
|
1315
|
-
f"Attempted to save workflow '{file_name}'. Another process is currently writing to this workflow file"
|
|
1316
|
-
)
|
|
1317
|
-
except PermissionError as e:
|
|
1318
|
-
error_details = f"Attempted to save workflow '{file_name}'. Permission denied: {e}"
|
|
1319
|
-
except IsADirectoryError:
|
|
1320
|
-
error_details = f"Attempted to save workflow '{file_name}'. Path is a directory, not a file"
|
|
1321
|
-
except UnicodeEncodeError as e:
|
|
1322
|
-
error_details = f"Attempted to save workflow '{file_name}'. Content encoding error: {e}"
|
|
1323
|
-
except OSError as e:
|
|
1324
|
-
error_details = f"Attempted to save workflow '{file_name}'. OS error: {e}"
|
|
1325
|
-
except Exception as e:
|
|
1326
|
-
error_details = f"Attempted to save workflow '{file_name}'. Unexpected error: {type(e).__name__}: {e}"
|
|
1412
|
+
# Write file using OSManager's centralized file writing API
|
|
1413
|
+
os_manager = GriptapeNodes.OSManager()
|
|
1414
|
+
write_request = WriteFileRequest(
|
|
1415
|
+
file_path=str(file_path),
|
|
1416
|
+
content=content,
|
|
1417
|
+
encoding="utf-8",
|
|
1418
|
+
append=False,
|
|
1419
|
+
existing_file_policy=ExistingFilePolicy.OVERWRITE,
|
|
1420
|
+
create_parents=True,
|
|
1421
|
+
)
|
|
1327
1422
|
|
|
1328
|
-
|
|
1329
|
-
|
|
1423
|
+
result = os_manager.on_write_file_request(write_request)
|
|
1424
|
+
|
|
1425
|
+
if isinstance(result, WriteFileResultFailure):
|
|
1426
|
+
# Map failure reasons to workflow-specific error messages
|
|
1427
|
+
match result.failure_reason:
|
|
1428
|
+
case FileIOFailureReason.IO_ERROR:
|
|
1429
|
+
# Could be lock exception or other I/O error
|
|
1430
|
+
error_msg = str(result.result_details)
|
|
1431
|
+
case FileIOFailureReason.PERMISSION_DENIED:
|
|
1432
|
+
error_msg = f"Permission denied: {result.result_details}"
|
|
1433
|
+
case FileIOFailureReason.IS_DIRECTORY:
|
|
1434
|
+
error_msg = "Path is a directory, not a file"
|
|
1435
|
+
case FileIOFailureReason.ENCODING_ERROR:
|
|
1436
|
+
error_msg = f"Content encoding error: {result.result_details}"
|
|
1437
|
+
case _:
|
|
1438
|
+
error_msg = str(result.result_details)
|
|
1439
|
+
|
|
1440
|
+
details = f"Attempted to save workflow '{file_name}'. {error_msg}"
|
|
1441
|
+
return self.WriteWorkflowFileResult(success=False, error_details=details)
|
|
1330
1442
|
|
|
1331
1443
|
return self.WriteWorkflowFileResult(success=True, error_details="")
|
|
1332
1444
|
|
|
@@ -1336,7 +1448,6 @@ class WorkflowManager:
|
|
|
1336
1448
|
current_workflow_name = (
|
|
1337
1449
|
context_manager.get_current_workflow_name() if context_manager.has_current_workflow() else None
|
|
1338
1450
|
)
|
|
1339
|
-
|
|
1340
1451
|
try:
|
|
1341
1452
|
save_target = self._determine_save_target(
|
|
1342
1453
|
requested_file_name=request.file_name,
|
|
@@ -1360,70 +1471,68 @@ class WorkflowManager:
|
|
|
1360
1471
|
branched_from if branched_from else "None",
|
|
1361
1472
|
)
|
|
1362
1473
|
|
|
1363
|
-
# Serialize
|
|
1364
|
-
|
|
1365
|
-
top_level_flow_result = await GriptapeNodes.ahandle_request(top_level_flow_request)
|
|
1474
|
+
# Serialize current flow and get shape
|
|
1475
|
+
top_level_flow_result = await GriptapeNodes.ahandle_request(GetTopLevelFlowRequest())
|
|
1366
1476
|
if not isinstance(top_level_flow_result, GetTopLevelFlowResultSuccess):
|
|
1367
1477
|
details = f"Attempted to save workflow '{relative_file_path}'. Failed when requesting top level flow."
|
|
1368
1478
|
return SaveWorkflowResultFailure(result_details=details)
|
|
1369
|
-
|
|
1370
1479
|
top_level_flow_name = top_level_flow_result.flow_name
|
|
1371
|
-
|
|
1372
|
-
|
|
1480
|
+
|
|
1481
|
+
serialized_flow_result = await GriptapeNodes.ahandle_request(
|
|
1482
|
+
SerializeFlowToCommandsRequest(flow_name=top_level_flow_name, include_create_flow_command=True)
|
|
1373
1483
|
)
|
|
1374
|
-
serialized_flow_result = await GriptapeNodes.ahandle_request(serialized_flow_request)
|
|
1375
1484
|
if not isinstance(serialized_flow_result, SerializeFlowToCommandsResultSuccess):
|
|
1376
1485
|
details = f"Attempted to save workflow '{relative_file_path}'. Failed when serializing flow."
|
|
1377
1486
|
return SaveWorkflowResultFailure(result_details=details)
|
|
1487
|
+
commands = serialized_flow_result.serialized_flow_commands
|
|
1378
1488
|
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
# Extract workflow shape if possible
|
|
1382
|
-
workflow_shape = None
|
|
1489
|
+
# Extract workflow shape if available; ignore failures
|
|
1383
1490
|
try:
|
|
1384
1491
|
workflow_shape_dict = self.extract_workflow_shape(workflow_name=file_name)
|
|
1385
|
-
workflow_shape = WorkflowShape(
|
|
1492
|
+
workflow_shape = WorkflowShape(
|
|
1493
|
+
inputs=workflow_shape_dict["input"],
|
|
1494
|
+
outputs=workflow_shape_dict["output"],
|
|
1495
|
+
)
|
|
1386
1496
|
except ValueError:
|
|
1387
|
-
|
|
1388
|
-
|
|
1497
|
+
workflow_shape = None
|
|
1498
|
+
|
|
1499
|
+
# Build save request inline (preserve existing description/image/is_template if present)
|
|
1500
|
+
existing_description, existing_image, existing_is_template = self._get_existing_metadata(file_name)
|
|
1389
1501
|
|
|
1390
|
-
# Use the standalone request to save the workflow file
|
|
1391
|
-
# Use pickle_control_flow_result from request if provided, otherwise use False (default)
|
|
1392
|
-
pickle_control_flow_result = (
|
|
1393
|
-
request.pickle_control_flow_result if request.pickle_control_flow_result is not None else False
|
|
1394
|
-
)
|
|
1395
1502
|
save_file_request = SaveWorkflowFileFromSerializedFlowRequest(
|
|
1396
|
-
serialized_flow_commands=
|
|
1503
|
+
serialized_flow_commands=commands,
|
|
1397
1504
|
file_name=file_name,
|
|
1398
1505
|
creation_date=creation_date,
|
|
1399
|
-
image_path=request.image_path,
|
|
1506
|
+
image_path=request.image_path if request.image_path is not None else existing_image,
|
|
1507
|
+
description=existing_description,
|
|
1508
|
+
is_template=existing_is_template,
|
|
1400
1509
|
execution_flow_name=top_level_flow_name,
|
|
1401
1510
|
branched_from=branched_from,
|
|
1402
1511
|
workflow_shape=workflow_shape,
|
|
1403
1512
|
file_path=str(file_path),
|
|
1404
|
-
pickle_control_flow_result=
|
|
1513
|
+
pickle_control_flow_result=(
|
|
1514
|
+
request.pickle_control_flow_result if request.pickle_control_flow_result is not None else False
|
|
1515
|
+
),
|
|
1405
1516
|
)
|
|
1406
|
-
save_file_result = await self.on_save_workflow_file_from_serialized_flow_request(save_file_request)
|
|
1407
1517
|
|
|
1518
|
+
# Execute save and update registry inline
|
|
1519
|
+
save_file_result = await self.on_save_workflow_file_from_serialized_flow_request(save_file_request)
|
|
1408
1520
|
if not isinstance(save_file_result, SaveWorkflowFileFromSerializedFlowResultSuccess):
|
|
1409
|
-
details =
|
|
1521
|
+
details = (
|
|
1522
|
+
f"Attempted to save workflow '{relative_file_path}'. "
|
|
1523
|
+
f"Failed during file generation: {save_file_result.result_details}"
|
|
1524
|
+
)
|
|
1410
1525
|
return SaveWorkflowResultFailure(result_details=details)
|
|
1411
1526
|
|
|
1412
|
-
# Use the metadata returned by the save operation
|
|
1413
1527
|
workflow_metadata = save_file_result.workflow_metadata
|
|
1414
1528
|
|
|
1415
|
-
# save the created workflow as an entry in the JSON config file.
|
|
1416
1529
|
registered_workflows = WorkflowRegistry.list_workflows()
|
|
1417
1530
|
if file_name not in registered_workflows:
|
|
1418
|
-
|
|
1419
|
-
if
|
|
1420
|
-
|
|
1421
|
-
GriptapeNodes.ConfigManager().save_user_workflow_json(str(file_path))
|
|
1422
|
-
except OSError as e:
|
|
1423
|
-
details = f"Attempted to save workflow '{file_name}'. Failed when saving configuration: {e}"
|
|
1424
|
-
return SaveWorkflowResultFailure(result_details=details)
|
|
1531
|
+
error_details = self._ensure_user_workflow_json_saved(relative_file_path, file_path, file_name)
|
|
1532
|
+
if error_details:
|
|
1533
|
+
return SaveWorkflowResultFailure(result_details=error_details)
|
|
1425
1534
|
WorkflowRegistry.generate_new_workflow(metadata=workflow_metadata, file_path=relative_file_path)
|
|
1426
|
-
|
|
1535
|
+
|
|
1427
1536
|
existing_workflow = WorkflowRegistry.get_workflow_by_name(file_name)
|
|
1428
1537
|
existing_workflow.metadata = workflow_metadata
|
|
1429
1538
|
details = f"Successfully saved workflow to: {save_file_result.file_path}"
|
|
@@ -1431,6 +1540,29 @@ class WorkflowManager:
|
|
|
1431
1540
|
file_path=save_file_result.file_path, result_details=ResultDetails(message=details, level=logging.INFO)
|
|
1432
1541
|
)
|
|
1433
1542
|
|
|
1543
|
+
def _get_existing_metadata(self, file_name: str) -> tuple[str | None, str | None, bool | None]:
|
|
1544
|
+
"""Return (description, image, is_template) for existing workflow if present; otherwise Nones."""
|
|
1545
|
+
if not WorkflowRegistry.has_workflow_with_name(file_name):
|
|
1546
|
+
return None, None, None
|
|
1547
|
+
try:
|
|
1548
|
+
existing = WorkflowRegistry.get_workflow_by_name(file_name)
|
|
1549
|
+
except Exception as err:
|
|
1550
|
+
logger.debug("Preserving existing metadata failed for workflow '%s': %s", file_name, err)
|
|
1551
|
+
return None, None, None
|
|
1552
|
+
else:
|
|
1553
|
+
return existing.metadata.description, existing.metadata.image, existing.metadata.is_template
|
|
1554
|
+
|
|
1555
|
+
def _ensure_user_workflow_json_saved(self, relative_file_path: str, file_path: Path, file_name: str) -> str | None:
|
|
1556
|
+
"""Save user workflow JSON when needed; return error details on failure, else None."""
|
|
1557
|
+
if Path(relative_file_path).is_absolute():
|
|
1558
|
+
return None
|
|
1559
|
+
try:
|
|
1560
|
+
GriptapeNodes.ConfigManager().save_user_workflow_json(str(file_path))
|
|
1561
|
+
except OSError as e:
|
|
1562
|
+
return f"Attempted to save workflow '{file_name}'. Failed when saving configuration: {e}"
|
|
1563
|
+
else:
|
|
1564
|
+
return None
|
|
1565
|
+
|
|
1434
1566
|
def _determine_save_target(
|
|
1435
1567
|
self, requested_file_name: str | None, current_workflow_name: str | None
|
|
1436
1568
|
) -> SaveWorkflowTargetInfo:
|
|
@@ -1497,7 +1629,8 @@ class WorkflowManager:
|
|
|
1497
1629
|
file_name = requested_file_name
|
|
1498
1630
|
creation_date = current_workflow.metadata.creation_date
|
|
1499
1631
|
branched_from = current_workflow.metadata.branched_from
|
|
1500
|
-
|
|
1632
|
+
current_dir = Path(current_workflow.file_path).parent
|
|
1633
|
+
relative_file_path = str(current_dir / f"{file_name}.py")
|
|
1501
1634
|
file_path = GriptapeNodes.ConfigManager().workspace_path.joinpath(relative_file_path)
|
|
1502
1635
|
|
|
1503
1636
|
else:
|
|
@@ -1547,6 +1680,8 @@ class WorkflowManager:
|
|
|
1547
1680
|
file_name=request.file_name,
|
|
1548
1681
|
creation_date=creation_date,
|
|
1549
1682
|
image_path=request.image_path,
|
|
1683
|
+
description=request.description,
|
|
1684
|
+
is_template=request.is_template,
|
|
1550
1685
|
branched_from=request.branched_from,
|
|
1551
1686
|
workflow_shape=request.workflow_shape,
|
|
1552
1687
|
)
|
|
@@ -1571,9 +1706,6 @@ class WorkflowManager:
|
|
|
1571
1706
|
return SaveWorkflowFileFromSerializedFlowResultFailure(result_details=details)
|
|
1572
1707
|
|
|
1573
1708
|
# Write the workflow file
|
|
1574
|
-
# TODO: https://github.com/griptape-ai/griptape-nodes/issues/3169
|
|
1575
|
-
# This is a synchronous call within an async context and may block the event loop.
|
|
1576
|
-
# Consider using asyncio.to_thread() to run in a thread pool for better async performance.
|
|
1577
1709
|
write_result = self._write_workflow_file(file_path, final_code_output, request.file_name)
|
|
1578
1710
|
if not write_result.success:
|
|
1579
1711
|
return SaveWorkflowFileFromSerializedFlowResultFailure(result_details=write_result.error_details)
|
|
@@ -1590,7 +1722,10 @@ class WorkflowManager:
|
|
|
1590
1722
|
serialized_flow_commands: SerializedFlowCommands,
|
|
1591
1723
|
file_name: str,
|
|
1592
1724
|
creation_date: datetime,
|
|
1725
|
+
*,
|
|
1593
1726
|
image_path: str | None = None,
|
|
1727
|
+
description: str | None = None,
|
|
1728
|
+
is_template: bool | None = None,
|
|
1594
1729
|
branched_from: str | None = None,
|
|
1595
1730
|
workflow_shape: WorkflowShape | None = None,
|
|
1596
1731
|
) -> WorkflowMetadata:
|
|
@@ -1622,6 +1757,8 @@ class WorkflowManager:
|
|
|
1622
1757
|
branched_from=branched_from,
|
|
1623
1758
|
workflow_shape=workflow_shape,
|
|
1624
1759
|
image=image_path,
|
|
1760
|
+
description=description,
|
|
1761
|
+
is_template=is_template,
|
|
1625
1762
|
)
|
|
1626
1763
|
|
|
1627
1764
|
def _generate_workflow_file_content( # noqa: PLR0912, PLR0915, C901
|
|
@@ -1708,15 +1845,33 @@ class WorkflowManager:
|
|
|
1708
1845
|
flow_initialization_command=flow_initialization_command, flow_creation_index=flow_creation_index
|
|
1709
1846
|
)
|
|
1710
1847
|
|
|
1711
|
-
#
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1848
|
+
# Separate regular nodes from NodeGroup nodes in main flow
|
|
1849
|
+
from griptape_nodes.retained_mode.events.node_events import CreateNodeGroupRequest
|
|
1850
|
+
|
|
1851
|
+
regular_node_commands = []
|
|
1852
|
+
node_group_commands = []
|
|
1853
|
+
for serialized_node_command in serialized_flow_commands.serialized_node_commands:
|
|
1854
|
+
create_cmd = serialized_node_command.create_node_command
|
|
1855
|
+
if isinstance(create_cmd, CreateNodeGroupRequest):
|
|
1856
|
+
node_group_commands.append(serialized_node_command)
|
|
1857
|
+
else:
|
|
1858
|
+
regular_node_commands.append(serialized_node_command)
|
|
1715
1859
|
|
|
1716
|
-
#
|
|
1717
|
-
|
|
1860
|
+
# Track the running node index across all flows to ensure unique variable names
|
|
1861
|
+
current_node_index = 0
|
|
1718
1862
|
|
|
1719
|
-
#
|
|
1863
|
+
# Generate regular nodes in main flow first (NOT NodeGroups yet)
|
|
1864
|
+
for serialized_node_command in regular_node_commands:
|
|
1865
|
+
node_creation_ast = self._generate_node_creation_code(
|
|
1866
|
+
serialized_node_command,
|
|
1867
|
+
current_node_index,
|
|
1868
|
+
import_recorder,
|
|
1869
|
+
node_uuid_to_node_variable_name=node_uuid_to_node_variable_name,
|
|
1870
|
+
)
|
|
1871
|
+
assign_flow_context_node.body.extend(node_creation_ast)
|
|
1872
|
+
current_node_index += 1
|
|
1873
|
+
|
|
1874
|
+
# Process sub-flows - for each sub-flow, generate its nodes
|
|
1720
1875
|
for sub_flow_index, sub_flow_commands in enumerate(serialized_flow_commands.sub_flows_commands):
|
|
1721
1876
|
sub_flow_creation_index = flow_creation_index + 1 + sub_flow_index
|
|
1722
1877
|
|
|
@@ -1726,7 +1881,10 @@ class WorkflowManager:
|
|
|
1726
1881
|
match sub_flow_initialization_command:
|
|
1727
1882
|
case CreateFlowRequest():
|
|
1728
1883
|
sub_flow_create_node = self._generate_create_flow(
|
|
1729
|
-
sub_flow_initialization_command,
|
|
1884
|
+
sub_flow_initialization_command,
|
|
1885
|
+
import_recorder,
|
|
1886
|
+
sub_flow_creation_index,
|
|
1887
|
+
parent_flow_creation_index=flow_creation_index,
|
|
1730
1888
|
)
|
|
1731
1889
|
assign_flow_context_node.body.append(cast("ast.stmt", sub_flow_create_node))
|
|
1732
1890
|
case ImportWorkflowAsReferencedSubFlowRequest():
|
|
@@ -1735,6 +1893,50 @@ class WorkflowManager:
|
|
|
1735
1893
|
)
|
|
1736
1894
|
assign_flow_context_node.body.append(cast("ast.stmt", sub_flow_import_node))
|
|
1737
1895
|
|
|
1896
|
+
# Generate the nodes in this subflow (just like we do for main flow)
|
|
1897
|
+
if sub_flow_commands.serialized_node_commands:
|
|
1898
|
+
# Create "with" statement for subflow
|
|
1899
|
+
subflow_context_node = self._generate_assign_flow_context(
|
|
1900
|
+
flow_initialization_command=sub_flow_initialization_command,
|
|
1901
|
+
flow_creation_index=sub_flow_creation_index,
|
|
1902
|
+
)
|
|
1903
|
+
# Generate nodes in subflow, passing current index and getting next available
|
|
1904
|
+
subflow_nodes, current_node_index = self._generate_nodes_in_flow(
|
|
1905
|
+
sub_flow_commands, import_recorder, node_uuid_to_node_variable_name, current_node_index
|
|
1906
|
+
)
|
|
1907
|
+
subflow_context_node.body.extend(subflow_nodes)
|
|
1908
|
+
|
|
1909
|
+
# Generate connections for nodes in this subflow (must be in subflow context)
|
|
1910
|
+
subflow_connection_asts = self._generate_connections_code(
|
|
1911
|
+
serialized_connections=sub_flow_commands.serialized_connections,
|
|
1912
|
+
node_uuid_to_node_variable_name=node_uuid_to_node_variable_name,
|
|
1913
|
+
import_recorder=import_recorder,
|
|
1914
|
+
)
|
|
1915
|
+
subflow_context_node.body.extend(subflow_connection_asts)
|
|
1916
|
+
|
|
1917
|
+
# Generate parameter values for nodes in this subflow (must be in subflow context)
|
|
1918
|
+
subflow_parameter_value_asts = self._generate_set_parameter_value_code(
|
|
1919
|
+
set_parameter_value_commands=sub_flow_commands.set_parameter_value_commands,
|
|
1920
|
+
lock_commands=sub_flow_commands.set_lock_commands_per_node,
|
|
1921
|
+
node_uuid_to_node_variable_name=node_uuid_to_node_variable_name,
|
|
1922
|
+
unique_values_dict_name="top_level_unique_values_dict",
|
|
1923
|
+
import_recorder=import_recorder,
|
|
1924
|
+
)
|
|
1925
|
+
subflow_context_node.body.extend(subflow_parameter_value_asts)
|
|
1926
|
+
|
|
1927
|
+
assign_flow_context_node.body.append(subflow_context_node)
|
|
1928
|
+
|
|
1929
|
+
# Generate NodeGroup nodes LAST (after subflows, so child nodes exist)
|
|
1930
|
+
for serialized_node_command in node_group_commands:
|
|
1931
|
+
node_creation_ast = self._generate_node_creation_code(
|
|
1932
|
+
serialized_node_command,
|
|
1933
|
+
current_node_index,
|
|
1934
|
+
import_recorder,
|
|
1935
|
+
node_uuid_to_node_variable_name=node_uuid_to_node_variable_name,
|
|
1936
|
+
)
|
|
1937
|
+
assign_flow_context_node.body.extend(node_creation_ast)
|
|
1938
|
+
current_node_index += 1
|
|
1939
|
+
|
|
1738
1940
|
# Now generate the connection code and add it to the flow context
|
|
1739
1941
|
connection_asts = self._generate_connections_code(
|
|
1740
1942
|
serialized_connections=serialized_flow_commands.serialized_connections,
|
|
@@ -1743,7 +1945,7 @@ class WorkflowManager:
|
|
|
1743
1945
|
)
|
|
1744
1946
|
assign_flow_context_node.body.extend(connection_asts)
|
|
1745
1947
|
|
|
1746
|
-
# Generate parameter values
|
|
1948
|
+
# Generate parameter values for main flow only (subflow parameter values generated inside their contexts)
|
|
1747
1949
|
set_parameter_value_asts = self._generate_set_parameter_value_code(
|
|
1748
1950
|
set_parameter_value_commands=serialized_flow_commands.set_parameter_value_commands,
|
|
1749
1951
|
lock_commands=serialized_flow_commands.set_lock_commands_per_node,
|
|
@@ -2679,7 +2881,11 @@ class WorkflowManager:
|
|
|
2679
2881
|
return full_ast
|
|
2680
2882
|
|
|
2681
2883
|
def _generate_create_flow(
|
|
2682
|
-
self,
|
|
2884
|
+
self,
|
|
2885
|
+
create_flow_command: CreateFlowRequest,
|
|
2886
|
+
import_recorder: ImportRecorder,
|
|
2887
|
+
flow_creation_index: int,
|
|
2888
|
+
parent_flow_creation_index: int | None = None,
|
|
2683
2889
|
) -> ast.Module:
|
|
2684
2890
|
import_recorder.add_from_import("griptape_nodes.retained_mode.events.flow_events", "CreateFlowRequest")
|
|
2685
2891
|
|
|
@@ -2691,9 +2897,19 @@ class WorkflowManager:
|
|
|
2691
2897
|
for field in fields(create_flow_command):
|
|
2692
2898
|
field_value = getattr(create_flow_command, field.name)
|
|
2693
2899
|
if field_value != field.default:
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2900
|
+
# Special handling for parent_flow_name - use variable reference if parent index provided
|
|
2901
|
+
if field.name == "parent_flow_name" and parent_flow_creation_index is not None:
|
|
2902
|
+
parent_flow_variable = f"flow{parent_flow_creation_index}_name"
|
|
2903
|
+
create_flow_request_args.append(
|
|
2904
|
+
ast.keyword(
|
|
2905
|
+
arg=field.name,
|
|
2906
|
+
value=ast.Name(id=parent_flow_variable, ctx=ast.Load(), lineno=1, col_offset=0),
|
|
2907
|
+
)
|
|
2908
|
+
)
|
|
2909
|
+
else:
|
|
2910
|
+
create_flow_request_args.append(
|
|
2911
|
+
ast.keyword(arg=field.name, value=ast.Constant(value=field_value, lineno=1, col_offset=0))
|
|
2912
|
+
)
|
|
2697
2913
|
|
|
2698
2914
|
# Create a comment explaining the behavior
|
|
2699
2915
|
comment_ast = ast.Expr(
|
|
@@ -2904,20 +3120,33 @@ class WorkflowManager:
|
|
|
2904
3120
|
serialized_flow_commands: SerializedFlowCommands,
|
|
2905
3121
|
import_recorder: ImportRecorder,
|
|
2906
3122
|
node_uuid_to_node_variable_name: dict[SerializedNodeCommands.NodeUUID, str],
|
|
2907
|
-
|
|
2908
|
-
|
|
3123
|
+
starting_node_index: int,
|
|
3124
|
+
) -> tuple[list[ast.stmt], int]:
|
|
3125
|
+
"""Generate node creation code for nodes in a flow.
|
|
3126
|
+
|
|
3127
|
+
Args:
|
|
3128
|
+
serialized_flow_commands: Commands for the flow
|
|
3129
|
+
import_recorder: Import recorder for tracking imports
|
|
3130
|
+
node_uuid_to_node_variable_name: Mapping from node UUIDs to variable names
|
|
3131
|
+
starting_node_index: The starting index for node variable names
|
|
3132
|
+
|
|
3133
|
+
Returns:
|
|
3134
|
+
Tuple of (list of AST statements, next available node index)
|
|
3135
|
+
"""
|
|
2909
3136
|
node_creation_asts = []
|
|
2910
|
-
|
|
3137
|
+
current_index = starting_node_index
|
|
3138
|
+
for serialized_node_command in serialized_flow_commands.serialized_node_commands:
|
|
2911
3139
|
node_creation_ast = self._generate_node_creation_code(
|
|
2912
3140
|
serialized_node_command,
|
|
2913
|
-
|
|
3141
|
+
current_index,
|
|
2914
3142
|
import_recorder,
|
|
2915
3143
|
node_uuid_to_node_variable_name=node_uuid_to_node_variable_name,
|
|
2916
3144
|
)
|
|
2917
3145
|
node_creation_asts.extend(node_creation_ast)
|
|
2918
|
-
|
|
3146
|
+
current_index += 1
|
|
3147
|
+
return node_creation_asts, current_index
|
|
2919
3148
|
|
|
2920
|
-
def _generate_node_creation_code(
|
|
3149
|
+
def _generate_node_creation_code( # noqa: C901, PLR0912
|
|
2921
3150
|
self,
|
|
2922
3151
|
serialized_node_command: SerializedNodeCommands,
|
|
2923
3152
|
node_index: int,
|
|
@@ -2951,9 +3180,34 @@ class WorkflowManager:
|
|
|
2951
3180
|
for field in fields(create_node_request):
|
|
2952
3181
|
field_value = getattr(create_node_request, field.name)
|
|
2953
3182
|
if field_value != field.default:
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
3183
|
+
# Special handling for CreateNodeGroupRequest.node_names_to_add - these are now UUIDs, convert to variable references
|
|
3184
|
+
if isinstance(create_node_request, CreateNodeGroupRequest) and field.name == "node_names_to_add":
|
|
3185
|
+
# field_value is now a list of UUIDs (converted in _serialize_package_nodes_for_local_execution)
|
|
3186
|
+
# Convert each UUID to an AST Name node referencing the generated variable
|
|
3187
|
+
node_var_ast_list = []
|
|
3188
|
+
for node_uuid in field_value:
|
|
3189
|
+
if node_uuid in node_uuid_to_node_variable_name:
|
|
3190
|
+
variable_name = node_uuid_to_node_variable_name[node_uuid]
|
|
3191
|
+
node_var_ast_list.append(ast.Name(id=variable_name, ctx=ast.Load()))
|
|
3192
|
+
else:
|
|
3193
|
+
logger.info(
|
|
3194
|
+
"NodeGroup child UUID '%s' not found in node_uuid_to_node_variable_name. Available UUIDs: %s...",
|
|
3195
|
+
node_uuid,
|
|
3196
|
+
list(node_uuid_to_node_variable_name.keys())[:5],
|
|
3197
|
+
)
|
|
3198
|
+
if node_var_ast_list:
|
|
3199
|
+
create_node_request_args.append(
|
|
3200
|
+
ast.keyword(arg=field.name, value=ast.List(elts=node_var_ast_list, ctx=ast.Load()))
|
|
3201
|
+
)
|
|
3202
|
+
else:
|
|
3203
|
+
logger.info(
|
|
3204
|
+
"NodeGroup node_names_to_add resulted in empty variable list. Original UUIDs: %s",
|
|
3205
|
+
field_value,
|
|
3206
|
+
)
|
|
3207
|
+
else:
|
|
3208
|
+
create_node_request_args.append(
|
|
3209
|
+
ast.keyword(arg=field.name, value=ast.Constant(value=field_value, lineno=1, col_offset=0))
|
|
3210
|
+
)
|
|
2957
3211
|
# Get the actual request class name (CreateNodeRequest or CreateNodeGroupRequest)
|
|
2958
3212
|
request_class_name = type(create_node_request).__name__
|
|
2959
3213
|
# Handle the create node command and assign to node name
|
|
@@ -4158,7 +4412,7 @@ class WorkflowManager:
|
|
|
4158
4412
|
failed = []
|
|
4159
4413
|
|
|
4160
4414
|
# First pass: collect all workflow files to determine total count
|
|
4161
|
-
all_workflow_files:
|
|
4415
|
+
all_workflow_files: set[Path] = set()
|
|
4162
4416
|
|
|
4163
4417
|
def collect_workflow_files(path: Path) -> None:
|
|
4164
4418
|
"""Collect workflow files from a path."""
|
|
@@ -4174,7 +4428,7 @@ class WorkflowManager:
|
|
|
4174
4428
|
workflow_file, block_name=WorkflowManager.WORKFLOW_METADATA_HEADER
|
|
4175
4429
|
)
|
|
4176
4430
|
if len(metadata_blocks) == 1:
|
|
4177
|
-
all_workflow_files.
|
|
4431
|
+
all_workflow_files.add(workflow_file)
|
|
4178
4432
|
except Exception as e:
|
|
4179
4433
|
# Skip files that can't be read or parsed
|
|
4180
4434
|
logger.debug("Skipping workflow file %s due to error: %s", workflow_file, e)
|
|
@@ -4185,7 +4439,7 @@ class WorkflowManager:
|
|
|
4185
4439
|
path, block_name=WorkflowManager.WORKFLOW_METADATA_HEADER
|
|
4186
4440
|
)
|
|
4187
4441
|
if len(metadata_blocks) == 1:
|
|
4188
|
-
all_workflow_files.
|
|
4442
|
+
all_workflow_files.add(path)
|
|
4189
4443
|
except Exception as e:
|
|
4190
4444
|
logger.debug("Skipping workflow file %s due to error: %s", path, e)
|
|
4191
4445
|
|