griptape-nodes 0.46.0__py3-none-any.whl → 0.48.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 +1 -1
- griptape_nodes/exe_types/core_types.py +129 -10
- griptape_nodes/exe_types/node_types.py +9 -3
- griptape_nodes/machines/node_resolution.py +10 -8
- griptape_nodes/mcp_server/ws_request_manager.py +6 -6
- griptape_nodes/retained_mode/events/base_events.py +74 -1
- griptape_nodes/retained_mode/events/secrets_events.py +2 -0
- griptape_nodes/retained_mode/griptape_nodes.py +17 -13
- griptape_nodes/retained_mode/managers/agent_manager.py +8 -6
- griptape_nodes/retained_mode/managers/arbitrary_code_exec_manager.py +1 -1
- griptape_nodes/retained_mode/managers/config_manager.py +36 -45
- griptape_nodes/retained_mode/managers/flow_manager.py +98 -98
- griptape_nodes/retained_mode/managers/library_manager.py +57 -57
- griptape_nodes/retained_mode/managers/node_manager.py +121 -124
- griptape_nodes/retained_mode/managers/object_manager.py +9 -10
- griptape_nodes/retained_mode/managers/os_manager.py +31 -31
- griptape_nodes/retained_mode/managers/secrets_manager.py +5 -5
- griptape_nodes/retained_mode/managers/static_files_manager.py +19 -21
- griptape_nodes/retained_mode/managers/sync_manager.py +3 -2
- griptape_nodes/retained_mode/managers/workflow_manager.py +153 -174
- griptape_nodes/retained_mode/retained_mode.py +25 -47
- {griptape_nodes-0.46.0.dist-info → griptape_nodes-0.48.0.dist-info}/METADATA +1 -1
- {griptape_nodes-0.46.0.dist-info → griptape_nodes-0.48.0.dist-info}/RECORD +25 -25
- {griptape_nodes-0.46.0.dist-info → griptape_nodes-0.48.0.dist-info}/WHEEL +1 -1
- {griptape_nodes-0.46.0.dist-info → griptape_nodes-0.48.0.dist-info}/entry_points.txt +0 -0
|
@@ -389,19 +389,19 @@ class WorkflowManager:
|
|
|
389
389
|
|
|
390
390
|
# Status emojis mapping
|
|
391
391
|
status_emoji = {
|
|
392
|
-
self.WorkflowStatus.GOOD: "
|
|
393
|
-
self.WorkflowStatus.FLAWED: "
|
|
394
|
-
self.WorkflowStatus.UNUSABLE: "
|
|
395
|
-
self.WorkflowStatus.MISSING: "
|
|
392
|
+
self.WorkflowStatus.GOOD: "[green]OK[/green]",
|
|
393
|
+
self.WorkflowStatus.FLAWED: "[yellow]![/yellow]",
|
|
394
|
+
self.WorkflowStatus.UNUSABLE: "[red]X[/red]",
|
|
395
|
+
self.WorkflowStatus.MISSING: "[red]?[/red]",
|
|
396
396
|
}
|
|
397
397
|
|
|
398
398
|
dependency_status_emoji = {
|
|
399
|
-
self.WorkflowDependencyStatus.PERFECT: "
|
|
400
|
-
self.WorkflowDependencyStatus.GOOD: "
|
|
401
|
-
self.WorkflowDependencyStatus.CAUTION: "
|
|
402
|
-
self.WorkflowDependencyStatus.BAD: "
|
|
403
|
-
self.WorkflowDependencyStatus.MISSING: "
|
|
404
|
-
self.WorkflowDependencyStatus.UNKNOWN: "
|
|
399
|
+
self.WorkflowDependencyStatus.PERFECT: "[green]OK[/green]",
|
|
400
|
+
self.WorkflowDependencyStatus.GOOD: "[green]GOOD[/green]",
|
|
401
|
+
self.WorkflowDependencyStatus.CAUTION: "[yellow]CAUTION[/yellow]",
|
|
402
|
+
self.WorkflowDependencyStatus.BAD: "[red]BAD[/red]",
|
|
403
|
+
self.WorkflowDependencyStatus.MISSING: "[red]MISSING[/red]",
|
|
404
|
+
self.WorkflowDependencyStatus.UNKNOWN: "[red]UNKNOWN[/red]",
|
|
405
405
|
}
|
|
406
406
|
|
|
407
407
|
# Add rows for each workflow info
|
|
@@ -414,7 +414,7 @@ class WorkflowManager:
|
|
|
414
414
|
# Workflow name column with emoji based on status
|
|
415
415
|
emoji = status_emoji.get(wf_info.status, "ERR: Unknown/Unexpected Workflow Status")
|
|
416
416
|
name = wf_info.workflow_name if wf_info.workflow_name else "*UNKNOWN*"
|
|
417
|
-
workflow_name = f"{emoji} {name}"
|
|
417
|
+
workflow_name = f"{emoji} - {name}"
|
|
418
418
|
|
|
419
419
|
# Problems column - format with numbers if there's more than one
|
|
420
420
|
problems = "\n".join(wf_info.problems) if wf_info.problems else "No problems detected."
|
|
@@ -423,11 +423,11 @@ class WorkflowManager:
|
|
|
423
423
|
if wf_info.status == self.WorkflowStatus.MISSING or (
|
|
424
424
|
wf_info.status == self.WorkflowStatus.UNUSABLE and not wf_info.workflow_dependencies
|
|
425
425
|
):
|
|
426
|
-
dependencies = "
|
|
426
|
+
dependencies = "[red]?[/red] UNKNOWN"
|
|
427
427
|
else:
|
|
428
428
|
dependencies = (
|
|
429
429
|
"\n".join(
|
|
430
|
-
f"{dependency_status_emoji.get(dep.status, '?')} {dep.library_name} ({dep.version_requested}): {dep.status.value}"
|
|
430
|
+
f"{dependency_status_emoji.get(dep.status, '?')} - {dep.library_name} ({dep.version_requested}): {dep.status.value}"
|
|
431
431
|
for dep in wf_info.workflow_dependencies
|
|
432
432
|
)
|
|
433
433
|
if wf_info.workflow_dependencies
|
|
@@ -528,7 +528,7 @@ class WorkflowManager:
|
|
|
528
528
|
if not Path(complete_file_path).is_file():
|
|
529
529
|
details = f"Failed to find file. Path '{complete_file_path}' doesn't exist."
|
|
530
530
|
logger.error(details)
|
|
531
|
-
return RunWorkflowFromScratchResultFailure()
|
|
531
|
+
return RunWorkflowFromScratchResultFailure(result_details=details)
|
|
532
532
|
|
|
533
533
|
# Start with a clean slate.
|
|
534
534
|
clear_all_request = ClearAllObjectStateRequest(i_know_what_im_doing=True)
|
|
@@ -536,7 +536,7 @@ class WorkflowManager:
|
|
|
536
536
|
if not clear_all_result.succeeded():
|
|
537
537
|
details = f"Failed to clear the existing object state when trying to run '{complete_file_path}'."
|
|
538
538
|
logger.error(details)
|
|
539
|
-
return RunWorkflowFromScratchResultFailure()
|
|
539
|
+
return RunWorkflowFromScratchResultFailure(result_details=details)
|
|
540
540
|
|
|
541
541
|
# Run the file, goddamn it
|
|
542
542
|
execution_result = self.run_workflow(relative_file_path=relative_file_path)
|
|
@@ -545,7 +545,7 @@ class WorkflowManager:
|
|
|
545
545
|
return RunWorkflowFromScratchResultSuccess()
|
|
546
546
|
|
|
547
547
|
logger.error(execution_result.execution_details)
|
|
548
|
-
return RunWorkflowFromScratchResultFailure()
|
|
548
|
+
return RunWorkflowFromScratchResultFailure(result_details=execution_result.execution_details)
|
|
549
549
|
|
|
550
550
|
def on_run_workflow_with_current_state_request(self, request: RunWorkflowWithCurrentStateRequest) -> ResultPayload:
|
|
551
551
|
relative_file_path = request.file_path
|
|
@@ -553,22 +553,23 @@ class WorkflowManager:
|
|
|
553
553
|
if not Path(complete_file_path).is_file():
|
|
554
554
|
details = f"Failed to find file. Path '{complete_file_path}' doesn't exist."
|
|
555
555
|
logger.error(details)
|
|
556
|
-
return RunWorkflowWithCurrentStateResultFailure()
|
|
556
|
+
return RunWorkflowWithCurrentStateResultFailure(result_details=details)
|
|
557
557
|
execution_result = self.run_workflow(relative_file_path=relative_file_path)
|
|
558
558
|
|
|
559
559
|
if execution_result.execution_successful:
|
|
560
560
|
logger.debug(execution_result.execution_details)
|
|
561
561
|
return RunWorkflowWithCurrentStateResultSuccess()
|
|
562
562
|
logger.error(execution_result.execution_details)
|
|
563
|
-
return RunWorkflowWithCurrentStateResultFailure()
|
|
563
|
+
return RunWorkflowWithCurrentStateResultFailure(result_details=execution_result.execution_details)
|
|
564
564
|
|
|
565
565
|
def on_run_workflow_from_registry_request(self, request: RunWorkflowFromRegistryRequest) -> ResultPayload:
|
|
566
566
|
# get workflow from registry
|
|
567
567
|
try:
|
|
568
568
|
workflow = WorkflowRegistry.get_workflow_by_name(request.workflow_name)
|
|
569
569
|
except KeyError:
|
|
570
|
-
|
|
571
|
-
|
|
570
|
+
details = f"Failed to get workflow '{request.workflow_name}' from registry."
|
|
571
|
+
logger.error(details)
|
|
572
|
+
return RunWorkflowFromRegistryResultFailure(result_details=details)
|
|
572
573
|
|
|
573
574
|
# Update current context for workflow.
|
|
574
575
|
if GriptapeNodes.ContextManager().has_current_workflow():
|
|
@@ -587,7 +588,7 @@ class WorkflowManager:
|
|
|
587
588
|
if not clear_all_result.succeeded():
|
|
588
589
|
details = f"Failed to clear the existing object state when preparing to run workflow '{request.workflow_name}'."
|
|
589
590
|
logger.error(details)
|
|
590
|
-
return RunWorkflowFromRegistryResultFailure()
|
|
591
|
+
return RunWorkflowFromRegistryResultFailure(result_details=details)
|
|
591
592
|
|
|
592
593
|
# Let's run under the assumption that this Workflow will become our Current Context; if we fail, it will revert.
|
|
593
594
|
GriptapeNodes.ContextManager().push_workflow(request.workflow_name)
|
|
@@ -602,7 +603,7 @@ class WorkflowManager:
|
|
|
602
603
|
clear_all_result = GriptapeNodes.handle_request(clear_all_request)
|
|
603
604
|
|
|
604
605
|
# The clear-all above here wipes the ContextManager, so no need to do a pop_workflow().
|
|
605
|
-
return RunWorkflowFromRegistryResultFailure()
|
|
606
|
+
return RunWorkflowFromRegistryResultFailure(result_details=execution_result.execution_details)
|
|
606
607
|
|
|
607
608
|
# Success!
|
|
608
609
|
logger.debug(execution_result.execution_details)
|
|
@@ -617,7 +618,7 @@ class WorkflowManager:
|
|
|
617
618
|
except Exception as e:
|
|
618
619
|
details = f"Failed to register workflow with name '{request.metadata.name}'. Error: {e}"
|
|
619
620
|
logger.error(details)
|
|
620
|
-
return RegisterWorkflowResultFailure()
|
|
621
|
+
return RegisterWorkflowResultFailure(result_details=details)
|
|
621
622
|
return RegisterWorkflowResultSuccess(workflow_name=workflow.metadata.name)
|
|
622
623
|
|
|
623
624
|
def on_list_all_workflows_request(self, _request: ListAllWorkflowsRequest) -> ResultPayload:
|
|
@@ -626,7 +627,7 @@ class WorkflowManager:
|
|
|
626
627
|
except Exception:
|
|
627
628
|
details = "Failed to list all workflows."
|
|
628
629
|
logger.error(details)
|
|
629
|
-
return ListAllWorkflowsResultFailure()
|
|
630
|
+
return ListAllWorkflowsResultFailure(result_details=details)
|
|
630
631
|
return ListAllWorkflowsResultSuccess(workflows=workflows)
|
|
631
632
|
|
|
632
633
|
def on_delete_workflows_request(self, request: DeleteWorkflowRequest) -> ResultPayload:
|
|
@@ -635,14 +636,14 @@ class WorkflowManager:
|
|
|
635
636
|
except Exception as e:
|
|
636
637
|
details = f"Failed to remove workflow from registry with name '{request.name}'. Exception: {e}"
|
|
637
638
|
logger.error(details)
|
|
638
|
-
return DeleteWorkflowResultFailure()
|
|
639
|
+
return DeleteWorkflowResultFailure(result_details=details)
|
|
639
640
|
config_manager = GriptapeNodes.ConfigManager()
|
|
640
641
|
try:
|
|
641
642
|
config_manager.delete_user_workflow(workflow.file_path)
|
|
642
643
|
except Exception as e:
|
|
643
644
|
details = f"Failed to remove workflow from user config with name '{request.name}'. Exception: {e}"
|
|
644
645
|
logger.error(details)
|
|
645
|
-
return DeleteWorkflowResultFailure()
|
|
646
|
+
return DeleteWorkflowResultFailure(result_details=details)
|
|
646
647
|
# delete the actual file
|
|
647
648
|
full_path = config_manager.workspace_path.joinpath(workflow.file_path)
|
|
648
649
|
try:
|
|
@@ -650,7 +651,7 @@ class WorkflowManager:
|
|
|
650
651
|
except Exception as e:
|
|
651
652
|
details = f"Failed to delete workflow file with path '{workflow.file_path}'. Exception: {e}"
|
|
652
653
|
logger.error(details)
|
|
653
|
-
return DeleteWorkflowResultFailure()
|
|
654
|
+
return DeleteWorkflowResultFailure(result_details=details)
|
|
654
655
|
return DeleteWorkflowResultSuccess()
|
|
655
656
|
|
|
656
657
|
def on_rename_workflow_request(self, request: RenameWorkflowRequest) -> ResultPayload:
|
|
@@ -659,13 +660,13 @@ class WorkflowManager:
|
|
|
659
660
|
if isinstance(save_workflow_request, SaveWorkflowResultFailure):
|
|
660
661
|
details = f"Attempted to rename workflow '{request.workflow_name}' to '{request.requested_name}'. Failed while attempting to save."
|
|
661
662
|
logger.error(details)
|
|
662
|
-
return RenameWorkflowResultFailure()
|
|
663
|
+
return RenameWorkflowResultFailure(result_details=details)
|
|
663
664
|
|
|
664
665
|
delete_workflow_result = GriptapeNodes.handle_request(DeleteWorkflowRequest(name=request.workflow_name))
|
|
665
666
|
if isinstance(delete_workflow_result, DeleteWorkflowResultFailure):
|
|
666
667
|
details = f"Attempted to rename workflow '{request.workflow_name}' to '{request.requested_name}'. Failed while attempting to remove the original file name from the registry."
|
|
667
668
|
logger.error(details)
|
|
668
|
-
return RenameWorkflowResultFailure()
|
|
669
|
+
return RenameWorkflowResultFailure(result_details=details)
|
|
669
670
|
|
|
670
671
|
return RenameWorkflowResultSuccess()
|
|
671
672
|
|
|
@@ -674,18 +675,20 @@ class WorkflowManager:
|
|
|
674
675
|
# Validate source workflow exists
|
|
675
676
|
workflow = WorkflowRegistry.get_workflow_by_name(request.workflow_name)
|
|
676
677
|
except KeyError:
|
|
677
|
-
|
|
678
|
-
|
|
678
|
+
details = f"Failed to move workflow '{request.workflow_name}' because it does not exist."
|
|
679
|
+
logger.error(details)
|
|
680
|
+
return MoveWorkflowResultFailure(result_details=details)
|
|
679
681
|
|
|
680
682
|
config_manager = GriptapeNodes.ConfigManager()
|
|
681
683
|
|
|
682
684
|
# Get current file path
|
|
683
685
|
current_file_path = WorkflowRegistry.get_complete_file_path(workflow.file_path)
|
|
684
686
|
if not Path(current_file_path).exists():
|
|
685
|
-
|
|
686
|
-
"Failed to move workflow '
|
|
687
|
+
details = (
|
|
688
|
+
f"Failed to move workflow '{request.workflow_name}': File path '{current_file_path}' does not exist."
|
|
687
689
|
)
|
|
688
|
-
|
|
690
|
+
logger.error(details)
|
|
691
|
+
return MoveWorkflowResultFailure(result_details=details)
|
|
689
692
|
|
|
690
693
|
# Clean and validate target directory
|
|
691
694
|
target_directory = request.target_directory.strip().replace("\\", "/")
|
|
@@ -698,8 +701,9 @@ class WorkflowManager:
|
|
|
698
701
|
# Create target directory if it doesn't exist
|
|
699
702
|
target_dir_path.mkdir(parents=True, exist_ok=True)
|
|
700
703
|
except OSError as e:
|
|
701
|
-
|
|
702
|
-
|
|
704
|
+
details = f"Failed to create target directory '{target_dir_path}': {e!s}"
|
|
705
|
+
logger.error(details)
|
|
706
|
+
return MoveWorkflowResultFailure(result_details=details)
|
|
703
707
|
|
|
704
708
|
# Create new file path
|
|
705
709
|
workflow_filename = Path(workflow.file_path).name
|
|
@@ -708,12 +712,11 @@ class WorkflowManager:
|
|
|
708
712
|
|
|
709
713
|
# Check if target file already exists
|
|
710
714
|
if new_absolute_path.exists():
|
|
711
|
-
|
|
712
|
-
"Failed to move workflow '
|
|
713
|
-
request.workflow_name,
|
|
714
|
-
new_absolute_path,
|
|
715
|
+
details = (
|
|
716
|
+
f"Failed to move workflow '{request.workflow_name}': Target file '{new_absolute_path}' already exists."
|
|
715
717
|
)
|
|
716
|
-
|
|
718
|
+
logger.error(details)
|
|
719
|
+
return MoveWorkflowResultFailure(result_details=details)
|
|
717
720
|
|
|
718
721
|
try:
|
|
719
722
|
# Move the file
|
|
@@ -727,22 +730,27 @@ class WorkflowManager:
|
|
|
727
730
|
config_manager.save_user_workflow_json(str(new_absolute_path))
|
|
728
731
|
|
|
729
732
|
except OSError as e:
|
|
730
|
-
|
|
733
|
+
details = f"Failed to move workflow file '{current_file_path}' to '{new_absolute_path}': {e!s}"
|
|
734
|
+
logger.error(details)
|
|
731
735
|
|
|
732
736
|
# Attempt to rollback if file was moved but registry update failed
|
|
733
737
|
if new_absolute_path.exists() and not Path(current_file_path).exists():
|
|
734
738
|
try:
|
|
735
739
|
new_absolute_path.rename(current_file_path)
|
|
736
|
-
|
|
740
|
+
details = f"Rolled back file move for workflow '{request.workflow_name}'"
|
|
741
|
+
logger.info(details)
|
|
737
742
|
except OSError:
|
|
738
|
-
|
|
743
|
+
details = f"Failed to rollback file move for workflow '{request.workflow_name}'"
|
|
744
|
+
logger.error(details)
|
|
739
745
|
|
|
740
|
-
return MoveWorkflowResultFailure()
|
|
746
|
+
return MoveWorkflowResultFailure(result_details=details)
|
|
741
747
|
except Exception as e:
|
|
742
|
-
|
|
743
|
-
|
|
748
|
+
details = f"Failed to move workflow '{request.workflow_name}': {e!s}"
|
|
749
|
+
logger.error(details)
|
|
750
|
+
return MoveWorkflowResultFailure(result_details=details)
|
|
744
751
|
else:
|
|
745
|
-
|
|
752
|
+
details = f"Successfully moved workflow '{request.workflow_name}' to '{new_relative_path}'"
|
|
753
|
+
logger.info(details)
|
|
746
754
|
return MoveWorkflowResultSuccess(moved_file_path=new_relative_path)
|
|
747
755
|
|
|
748
756
|
def on_load_workflow_metadata_request( # noqa: C901, PLR0912, PLR0915
|
|
@@ -763,7 +771,7 @@ class WorkflowManager:
|
|
|
763
771
|
)
|
|
764
772
|
details = f"Attempted to load workflow metadata for a file at '{complete_file_path}. Failed because no file could be found at that path."
|
|
765
773
|
logger.error(details)
|
|
766
|
-
return LoadWorkflowMetadataResultFailure()
|
|
774
|
+
return LoadWorkflowMetadataResultFailure(result_details=details)
|
|
767
775
|
|
|
768
776
|
# Find the metadata block.
|
|
769
777
|
block_name = WorkflowManager.WORKFLOW_METADATA_HEADER
|
|
@@ -780,7 +788,7 @@ class WorkflowManager:
|
|
|
780
788
|
)
|
|
781
789
|
details = f"Attempted to load workflow metadata for a file at '{complete_file_path}'. Failed as it had {len(matches)} sections titled '{block_name}', and we expect exactly 1 such section."
|
|
782
790
|
logger.error(details)
|
|
783
|
-
return LoadWorkflowMetadataResultFailure()
|
|
791
|
+
return LoadWorkflowMetadataResultFailure(result_details=details)
|
|
784
792
|
|
|
785
793
|
# Now attempt to parse out the metadata section, stripped of comment prefixes.
|
|
786
794
|
metadata_content_toml = "".join(
|
|
@@ -800,7 +808,7 @@ class WorkflowManager:
|
|
|
800
808
|
)
|
|
801
809
|
details = f"Attempted to load workflow metadata for a file at '{complete_file_path}'. Failed because the metadata was not valid TOML: {err}"
|
|
802
810
|
logger.error(details)
|
|
803
|
-
return LoadWorkflowMetadataResultFailure()
|
|
811
|
+
return LoadWorkflowMetadataResultFailure(result_details=details)
|
|
804
812
|
|
|
805
813
|
tool_header = "tool"
|
|
806
814
|
griptape_nodes_header = "griptape-nodes"
|
|
@@ -816,7 +824,7 @@ class WorkflowManager:
|
|
|
816
824
|
)
|
|
817
825
|
details = f"Attempted to load workflow metadata for a file at '{complete_file_path}'. Failed because the '[{tool_header}.{griptape_nodes_header}]' section could not be found: {err}"
|
|
818
826
|
logger.error(details)
|
|
819
|
-
return LoadWorkflowMetadataResultFailure()
|
|
827
|
+
return LoadWorkflowMetadataResultFailure(result_details=details)
|
|
820
828
|
|
|
821
829
|
try:
|
|
822
830
|
# Is it kosher?
|
|
@@ -834,7 +842,7 @@ class WorkflowManager:
|
|
|
834
842
|
)
|
|
835
843
|
details = f"Attempted to load workflow metadata for a file at '{complete_file_path}'. Failed because the metadata in the '[{tool_header}.{griptape_nodes_header}]' section did not match the requisite schema with error: {err}"
|
|
836
844
|
logger.error(details)
|
|
837
|
-
return LoadWorkflowMetadataResultFailure()
|
|
845
|
+
return LoadWorkflowMetadataResultFailure(result_details=details)
|
|
838
846
|
|
|
839
847
|
# We have valid dependencies, etc.
|
|
840
848
|
# TODO: validate schema versions, engine versions: https://github.com/griptape-ai/griptape-nodes/issues/617
|
|
@@ -878,7 +886,12 @@ class WorkflowManager:
|
|
|
878
886
|
# See how our desired version compares against the actual library we (may) have.
|
|
879
887
|
# See if the library exists.
|
|
880
888
|
library_metadata_request = GetLibraryMetadataRequest(library=library_name)
|
|
881
|
-
|
|
889
|
+
# NOTE: Per https://github.com/griptape-ai/griptape-vsl-gui/issues/1123, we
|
|
890
|
+
# generate a FLOOD of error messages here that can swamp the GUI. We'll call
|
|
891
|
+
# directly instead of the usual handle_request() path so we don't generate those.
|
|
892
|
+
library_metadata_result = GriptapeNodes.LibraryManager().get_library_metadata_request(
|
|
893
|
+
library_metadata_request
|
|
894
|
+
)
|
|
882
895
|
if not isinstance(library_metadata_result, GetLibraryMetadataResultSuccess):
|
|
883
896
|
# Metadata failed to be found.
|
|
884
897
|
had_critical_error = True
|
|
@@ -1361,14 +1374,15 @@ class WorkflowManager:
|
|
|
1361
1374
|
except Exception as err:
|
|
1362
1375
|
details = f"Attempted to save workflow '{relative_file_path}', but {err}"
|
|
1363
1376
|
logger.error(details)
|
|
1364
|
-
return SaveWorkflowResultFailure()
|
|
1377
|
+
return SaveWorkflowResultFailure(result_details=details)
|
|
1365
1378
|
|
|
1366
1379
|
# Create the pathing and write the file
|
|
1367
1380
|
try:
|
|
1368
1381
|
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1369
1382
|
except OSError as e:
|
|
1370
|
-
|
|
1371
|
-
|
|
1383
|
+
details = f"Attempted to save workflow '{file_name}'. Failed when creating directory: {e}"
|
|
1384
|
+
logger.error(details)
|
|
1385
|
+
return SaveWorkflowResultFailure(result_details=details)
|
|
1372
1386
|
|
|
1373
1387
|
relative_serialized_file_path = f"{file_name}.py"
|
|
1374
1388
|
serialized_file_path = GriptapeNodes.ConfigManager().workspace_path.joinpath(relative_serialized_file_path)
|
|
@@ -1378,17 +1392,17 @@ class WorkflowManager:
|
|
|
1378
1392
|
min_space_gb = config_manager.get_config_value("minimum_disk_space_gb_workflows")
|
|
1379
1393
|
if not OSManager.check_available_disk_space(serialized_file_path.parent, min_space_gb):
|
|
1380
1394
|
error_msg = OSManager.format_disk_space_error(serialized_file_path.parent)
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
)
|
|
1384
|
-
return SaveWorkflowResultFailure()
|
|
1395
|
+
details = f"Attempted to save workflow '{file_name}' (requires {min_space_gb:.1f} GB). Failed: {error_msg}"
|
|
1396
|
+
logger.error(details)
|
|
1397
|
+
return SaveWorkflowResultFailure(result_details=details)
|
|
1385
1398
|
|
|
1386
1399
|
try:
|
|
1387
1400
|
with serialized_file_path.open("w", encoding="utf-8") as file:
|
|
1388
1401
|
file.write(final_code_output)
|
|
1389
1402
|
except OSError as e:
|
|
1390
|
-
|
|
1391
|
-
|
|
1403
|
+
details = f"Attempted to save workflow '{file_name}'. Failed when writing file: {e}"
|
|
1404
|
+
logger.error(details)
|
|
1405
|
+
return SaveWorkflowResultFailure(result_details=details)
|
|
1392
1406
|
|
|
1393
1407
|
# save the created workflow as an entry in the JSON config file.
|
|
1394
1408
|
registered_workflows = WorkflowRegistry.list_workflows()
|
|
@@ -1396,8 +1410,9 @@ class WorkflowManager:
|
|
|
1396
1410
|
try:
|
|
1397
1411
|
GriptapeNodes.ConfigManager().save_user_workflow_json(str(file_path))
|
|
1398
1412
|
except OSError as e:
|
|
1399
|
-
|
|
1400
|
-
|
|
1413
|
+
details = f"Attempted to save workflow '{file_name}'. Failed when saving configuration: {e}"
|
|
1414
|
+
logger.error(details)
|
|
1415
|
+
return SaveWorkflowResultFailure(result_details=details)
|
|
1401
1416
|
WorkflowRegistry.generate_new_workflow(metadata=workflow_metadata, file_path=relative_file_path)
|
|
1402
1417
|
# Update existing workflow's metadata in the registry
|
|
1403
1418
|
existing_workflow = WorkflowRegistry.get_workflow_by_name(file_name)
|
|
@@ -3058,7 +3073,7 @@ class WorkflowManager:
|
|
|
3058
3073
|
except Exception as e:
|
|
3059
3074
|
details = f"Failed to publish workflow '{request.workflow_name}': {e!s}"
|
|
3060
3075
|
logger.exception(details)
|
|
3061
|
-
return PublishWorkflowResultFailure(exception=e)
|
|
3076
|
+
return PublishWorkflowResultFailure(exception=e, result_details=details)
|
|
3062
3077
|
|
|
3063
3078
|
def _register_published_workflow_file(self, workflow_file: Path) -> None:
|
|
3064
3079
|
"""Register a published workflow file in the workflow registry."""
|
|
@@ -3118,45 +3133,35 @@ class WorkflowManager:
|
|
|
3118
3133
|
try:
|
|
3119
3134
|
workflow = self._get_workflow_by_name(request.workflow_name)
|
|
3120
3135
|
except KeyError:
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
)
|
|
3125
|
-
return ImportWorkflowAsReferencedSubFlowResultFailure()
|
|
3136
|
+
details = f"Attempted to import workflow '{request.workflow_name}' as referenced sub flow. Failed because workflow is not registered"
|
|
3137
|
+
logger.error(details)
|
|
3138
|
+
return ImportWorkflowAsReferencedSubFlowResultFailure(result_details=details)
|
|
3126
3139
|
|
|
3127
3140
|
# Check workflow version - Schema version 0.6.0+ required for referenced workflow imports
|
|
3128
3141
|
# (workflow schema was fixed in 0.6.0 to support importing workflows)
|
|
3129
3142
|
required_version = Version(major=0, minor=6, patch=0)
|
|
3130
3143
|
workflow_version = Version.from_string(workflow.metadata.schema_version)
|
|
3131
3144
|
if workflow_version is None or workflow_version < required_version:
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
workflow.metadata.schema_version,
|
|
3136
|
-
)
|
|
3137
|
-
return ImportWorkflowAsReferencedSubFlowResultFailure()
|
|
3145
|
+
details = f"Attempted to import workflow '{request.workflow_name}' as referenced sub flow. Failed because workflow version '{workflow.metadata.schema_version}' is less than required version '0.6.0'. To remedy, open the workflow you are attempting to import and save it again to upgrade it to the latest version."
|
|
3146
|
+
logger.error(details)
|
|
3147
|
+
return ImportWorkflowAsReferencedSubFlowResultFailure(result_details=details)
|
|
3138
3148
|
|
|
3139
3149
|
# Check target flow
|
|
3140
3150
|
flow_name = request.flow_name
|
|
3141
3151
|
if flow_name is None:
|
|
3142
3152
|
if not GriptapeNodes.ContextManager().has_current_flow():
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
)
|
|
3147
|
-
return ImportWorkflowAsReferencedSubFlowResultFailure()
|
|
3153
|
+
details = f"Attempted to import workflow '{request.workflow_name}' into Current Context. Failed because Current Context was empty"
|
|
3154
|
+
logger.error(details)
|
|
3155
|
+
return ImportWorkflowAsReferencedSubFlowResultFailure(result_details=details)
|
|
3148
3156
|
else:
|
|
3149
3157
|
# Validate that the specified flow exists
|
|
3150
3158
|
flow_manager = GriptapeNodes.FlowManager()
|
|
3151
3159
|
try:
|
|
3152
3160
|
flow_manager.get_flow_by_name(flow_name)
|
|
3153
3161
|
except KeyError:
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
flow_name,
|
|
3158
|
-
)
|
|
3159
|
-
return ImportWorkflowAsReferencedSubFlowResultFailure()
|
|
3162
|
+
details = f"Attempted to import workflow '{request.workflow_name}' into flow '{flow_name}'. Failed because target flow does not exist"
|
|
3163
|
+
logger.error(details)
|
|
3164
|
+
return ImportWorkflowAsReferencedSubFlowResultFailure(result_details=details)
|
|
3160
3165
|
|
|
3161
3166
|
return None
|
|
3162
3167
|
|
|
@@ -3178,23 +3183,18 @@ class WorkflowManager:
|
|
|
3178
3183
|
workflow_result = self.run_workflow(workflow.file_path)
|
|
3179
3184
|
|
|
3180
3185
|
if not workflow_result.execution_successful:
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
workflow_result.execution_details,
|
|
3185
|
-
)
|
|
3186
|
-
return ImportWorkflowAsReferencedSubFlowResultFailure()
|
|
3186
|
+
details = f"Attempted to import workflow '{request.workflow_name}' as referenced sub flow. Failed because workflow execution failed: {workflow_result.execution_details}"
|
|
3187
|
+
logger.error(details)
|
|
3188
|
+
return ImportWorkflowAsReferencedSubFlowResultFailure(result_details=details)
|
|
3187
3189
|
|
|
3188
3190
|
# Get flows after importing to find the new referenced sub flow
|
|
3189
3191
|
flows_after = set(obj_manager.get_filtered_subset(type=ControlFlow).keys())
|
|
3190
3192
|
new_flows = flows_after - flows_before
|
|
3191
3193
|
|
|
3192
3194
|
if not new_flows:
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
)
|
|
3197
|
-
return ImportWorkflowAsReferencedSubFlowResultFailure()
|
|
3195
|
+
details = f"Attempted to import workflow '{request.workflow_name}' as referenced sub flow. Failed because no new flow was created"
|
|
3196
|
+
logger.error(details)
|
|
3197
|
+
return ImportWorkflowAsReferencedSubFlowResultFailure(result_details=details)
|
|
3198
3198
|
|
|
3199
3199
|
# For now, use the first created flow as the main imported flow
|
|
3200
3200
|
# This handles nested workflows correctly since sub-flows are expected
|
|
@@ -3216,12 +3216,9 @@ class WorkflowManager:
|
|
|
3216
3216
|
set_metadata_result = GriptapeNodes.handle_request(set_metadata_request)
|
|
3217
3217
|
|
|
3218
3218
|
if not isinstance(set_metadata_result, SetFlowMetadataResultSuccess):
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
created_flow_name,
|
|
3223
|
-
)
|
|
3224
|
-
return ImportWorkflowAsReferencedSubFlowResultFailure()
|
|
3219
|
+
details = f"Attempted to import workflow '{request.workflow_name}' as referenced sub flow. Failed because metadata could not be applied to created flow '{created_flow_name}'"
|
|
3220
|
+
logger.error(details)
|
|
3221
|
+
return ImportWorkflowAsReferencedSubFlowResultFailure(result_details=details)
|
|
3225
3222
|
|
|
3226
3223
|
logger.debug(
|
|
3227
3224
|
"Applied imported flow metadata to '%s': %s", created_flow_name, request.imported_flow_metadata
|
|
@@ -3238,8 +3235,9 @@ class WorkflowManager:
|
|
|
3238
3235
|
# Validate source workflow exists
|
|
3239
3236
|
source_workflow = WorkflowRegistry.get_workflow_by_name(request.workflow_name)
|
|
3240
3237
|
except KeyError:
|
|
3241
|
-
|
|
3242
|
-
|
|
3238
|
+
details = f"Failed to branch workflow '{request.workflow_name}' because it does not exist"
|
|
3239
|
+
logger.error(details)
|
|
3240
|
+
return BranchWorkflowResultFailure(result_details=details)
|
|
3243
3241
|
|
|
3244
3242
|
# Generate branch name if not provided
|
|
3245
3243
|
branch_name = request.branched_workflow_name
|
|
@@ -3253,12 +3251,9 @@ class WorkflowManager:
|
|
|
3253
3251
|
|
|
3254
3252
|
# Check if branch name already exists
|
|
3255
3253
|
if WorkflowRegistry.has_workflow_with_name(branch_name):
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
branch_name,
|
|
3260
|
-
)
|
|
3261
|
-
return BranchWorkflowResultFailure()
|
|
3254
|
+
details = f"Failed to branch workflow '{request.workflow_name}' because branch name '{branch_name}' already exists"
|
|
3255
|
+
logger.error(details)
|
|
3256
|
+
return BranchWorkflowResultFailure(result_details=details)
|
|
3262
3257
|
|
|
3263
3258
|
try:
|
|
3264
3259
|
# Create branch metadata by copying source metadata
|
|
@@ -3285,21 +3280,18 @@ class WorkflowManager:
|
|
|
3285
3280
|
# Read source workflow content and replace metadata header
|
|
3286
3281
|
source_file_path = WorkflowRegistry.get_complete_file_path(source_workflow.file_path)
|
|
3287
3282
|
if not Path(source_file_path).exists():
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
request.workflow_name,
|
|
3292
|
-
source_file_path,
|
|
3293
|
-
)
|
|
3294
|
-
return BranchWorkflowResultFailure()
|
|
3283
|
+
details = f"Failed to branch workflow '{request.workflow_name}': File path '{source_file_path}' does not exist. The workflow may have been moved or the workspace configuration may have changed."
|
|
3284
|
+
logger.error(details)
|
|
3285
|
+
return BranchWorkflowResultFailure(result_details=details)
|
|
3295
3286
|
|
|
3296
3287
|
source_content = Path(source_file_path).read_text(encoding="utf-8")
|
|
3297
3288
|
|
|
3298
3289
|
# Replace the metadata header with branch metadata
|
|
3299
3290
|
branch_content = self._replace_workflow_metadata_header(source_content, branch_metadata)
|
|
3300
3291
|
if branch_content is None:
|
|
3301
|
-
|
|
3302
|
-
|
|
3292
|
+
details = f"Failed to replace metadata header for branch workflow '{branch_name}'"
|
|
3293
|
+
logger.error(details)
|
|
3294
|
+
return BranchWorkflowResultFailure(result_details=details)
|
|
3303
3295
|
|
|
3304
3296
|
# Write branch workflow file to disk BEFORE registering in registry
|
|
3305
3297
|
branch_full_path = WorkflowRegistry.get_complete_file_path(branch_file_path)
|
|
@@ -3318,11 +3310,12 @@ class WorkflowManager:
|
|
|
3318
3310
|
)
|
|
3319
3311
|
|
|
3320
3312
|
except Exception as e:
|
|
3321
|
-
|
|
3313
|
+
details = f"Failed to branch workflow '{request.workflow_name}': {e!s}"
|
|
3314
|
+
logger.error(details)
|
|
3322
3315
|
import traceback
|
|
3323
3316
|
|
|
3324
3317
|
traceback.print_exc()
|
|
3325
|
-
return BranchWorkflowResultFailure()
|
|
3318
|
+
return BranchWorkflowResultFailure(result_details=details)
|
|
3326
3319
|
|
|
3327
3320
|
def on_merge_workflow_branch_request(self, request: MergeWorkflowBranchRequest) -> ResultPayload:
|
|
3328
3321
|
"""Merge a branch back into its source workflow, removing the branch when complete."""
|
|
@@ -3330,28 +3323,24 @@ class WorkflowManager:
|
|
|
3330
3323
|
# Validate branch workflow exists
|
|
3331
3324
|
branch_workflow = WorkflowRegistry.get_workflow_by_name(request.workflow_name)
|
|
3332
3325
|
except KeyError as e:
|
|
3333
|
-
|
|
3334
|
-
|
|
3326
|
+
details = f"Failed to merge workflow branch because it does not exist: {e!s}"
|
|
3327
|
+
logger.error(details)
|
|
3328
|
+
return MergeWorkflowBranchResultFailure(result_details=details)
|
|
3335
3329
|
|
|
3336
3330
|
# Get source workflow name from branch metadata
|
|
3337
3331
|
source_workflow_name = branch_workflow.metadata.branched_from
|
|
3338
3332
|
if not source_workflow_name:
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
)
|
|
3343
|
-
return MergeWorkflowBranchResultFailure()
|
|
3333
|
+
details = f"Failed to merge workflow branch '{request.workflow_name}' because it has no source workflow"
|
|
3334
|
+
logger.error(details)
|
|
3335
|
+
return MergeWorkflowBranchResultFailure(result_details=details)
|
|
3344
3336
|
|
|
3345
3337
|
# Validate source workflow exists
|
|
3346
3338
|
try:
|
|
3347
3339
|
source_workflow = WorkflowRegistry.get_workflow_by_name(source_workflow_name)
|
|
3348
3340
|
except KeyError:
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
source_workflow_name,
|
|
3353
|
-
)
|
|
3354
|
-
return MergeWorkflowBranchResultFailure()
|
|
3341
|
+
details = f"Failed to merge workflow branch '{request.workflow_name}' because source workflow '{source_workflow_name}' does not exist"
|
|
3342
|
+
logger.error(details)
|
|
3343
|
+
return MergeWorkflowBranchResultFailure(result_details=details)
|
|
3355
3344
|
|
|
3356
3345
|
try:
|
|
3357
3346
|
# Create updated metadata for source workflow - update timestamp
|
|
@@ -3379,8 +3368,9 @@ class WorkflowManager:
|
|
|
3379
3368
|
# Replace the metadata header with merged metadata
|
|
3380
3369
|
merged_content = self._replace_workflow_metadata_header(branch_content, merged_metadata)
|
|
3381
3370
|
if merged_content is None:
|
|
3382
|
-
|
|
3383
|
-
|
|
3371
|
+
details = f"Failed to replace metadata header for merged workflow '{source_workflow_name}'"
|
|
3372
|
+
logger.error(details)
|
|
3373
|
+
return MergeWorkflowBranchResultFailure(result_details=details)
|
|
3384
3374
|
|
|
3385
3375
|
# Write the updated content to the source workflow file
|
|
3386
3376
|
source_file_path = WorkflowRegistry.get_complete_file_path(source_workflow.file_path)
|
|
@@ -3410,13 +3400,9 @@ class WorkflowManager:
|
|
|
3410
3400
|
return MergeWorkflowBranchResultSuccess(merged_workflow_name=source_workflow_name)
|
|
3411
3401
|
|
|
3412
3402
|
except Exception as e:
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
source_workflow_name,
|
|
3417
|
-
str(e),
|
|
3418
|
-
)
|
|
3419
|
-
return MergeWorkflowBranchResultFailure()
|
|
3403
|
+
details = f"Failed to merge branch workflow '{request.workflow_name}' into source workflow '{source_workflow_name}': {e!s}"
|
|
3404
|
+
logger.error(details)
|
|
3405
|
+
return MergeWorkflowBranchResultFailure(result_details=details)
|
|
3420
3406
|
|
|
3421
3407
|
def on_reset_workflow_branch_request(self, request: ResetWorkflowBranchRequest) -> ResultPayload:
|
|
3422
3408
|
"""Reset a branch to match its source workflow, discarding branch changes."""
|
|
@@ -3424,28 +3410,24 @@ class WorkflowManager:
|
|
|
3424
3410
|
# Validate branch workflow exists
|
|
3425
3411
|
branch_workflow = WorkflowRegistry.get_workflow_by_name(request.workflow_name)
|
|
3426
3412
|
except KeyError as e:
|
|
3427
|
-
|
|
3428
|
-
|
|
3413
|
+
details = f"Failed to reset workflow branch because it does not exist: {e!s}"
|
|
3414
|
+
logger.error(details)
|
|
3415
|
+
return ResetWorkflowBranchResultFailure(result_details=details)
|
|
3429
3416
|
|
|
3430
3417
|
# Get source workflow name from branch metadata
|
|
3431
3418
|
source_workflow_name = branch_workflow.metadata.branched_from
|
|
3432
3419
|
if not source_workflow_name:
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
)
|
|
3437
|
-
return ResetWorkflowBranchResultFailure()
|
|
3420
|
+
details = f"Failed to reset workflow branch '{request.workflow_name}' because it has no source workflow"
|
|
3421
|
+
logger.error(details)
|
|
3422
|
+
return ResetWorkflowBranchResultFailure(result_details=details)
|
|
3438
3423
|
|
|
3439
3424
|
# Validate source workflow exists
|
|
3440
3425
|
try:
|
|
3441
3426
|
source_workflow = WorkflowRegistry.get_workflow_by_name(source_workflow_name)
|
|
3442
3427
|
except KeyError:
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
source_workflow_name,
|
|
3447
|
-
)
|
|
3448
|
-
return ResetWorkflowBranchResultFailure()
|
|
3428
|
+
details = f"Failed to reset workflow branch '{request.workflow_name}' because source workflow '{source_workflow_name}' does not exist"
|
|
3429
|
+
logger.error(details)
|
|
3430
|
+
return ResetWorkflowBranchResultFailure(result_details=details)
|
|
3449
3431
|
|
|
3450
3432
|
try:
|
|
3451
3433
|
# Read content from the source workflow (what we're resetting the branch to)
|
|
@@ -3473,8 +3455,9 @@ class WorkflowManager:
|
|
|
3473
3455
|
# Replace the metadata header with reset metadata
|
|
3474
3456
|
reset_content = self._replace_workflow_metadata_header(source_content, reset_metadata)
|
|
3475
3457
|
if reset_content is None:
|
|
3476
|
-
|
|
3477
|
-
|
|
3458
|
+
details = f"Failed to replace metadata header for reset branch workflow '{request.workflow_name}'"
|
|
3459
|
+
logger.error(details)
|
|
3460
|
+
return ResetWorkflowBranchResultFailure(result_details=details)
|
|
3478
3461
|
|
|
3479
3462
|
# Write the updated content to the branch workflow file
|
|
3480
3463
|
branch_content_file_path = WorkflowRegistry.get_complete_file_path(branch_workflow.file_path)
|
|
@@ -3484,13 +3467,9 @@ class WorkflowManager:
|
|
|
3484
3467
|
branch_workflow.metadata = reset_metadata
|
|
3485
3468
|
|
|
3486
3469
|
except Exception as e:
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
source_workflow_name,
|
|
3491
|
-
str(e),
|
|
3492
|
-
)
|
|
3493
|
-
return ResetWorkflowBranchResultFailure()
|
|
3470
|
+
details = f"Failed to reset branch workflow '{request.workflow_name}' to source workflow '{source_workflow_name}': {e!s}"
|
|
3471
|
+
logger.error(details)
|
|
3472
|
+
return ResetWorkflowBranchResultFailure(result_details=details)
|
|
3494
3473
|
else:
|
|
3495
3474
|
logger.info(
|
|
3496
3475
|
"Successfully reset branch workflow '%s' to match source workflow '%s'",
|
|
@@ -3505,8 +3484,9 @@ class WorkflowManager:
|
|
|
3505
3484
|
# Get the workflow to evaluate
|
|
3506
3485
|
workflow = WorkflowRegistry.get_workflow_by_name(request.workflow_name)
|
|
3507
3486
|
except KeyError:
|
|
3508
|
-
|
|
3509
|
-
|
|
3487
|
+
details = f"Failed to compare workflow '{request.workflow_name}' because it does not exist"
|
|
3488
|
+
logger.error(details)
|
|
3489
|
+
return CompareWorkflowsResultFailure(result_details=details)
|
|
3510
3490
|
|
|
3511
3491
|
# Use the provided compare_workflow_name
|
|
3512
3492
|
compare_workflow_name = request.compare_workflow_name
|
|
@@ -3731,10 +3711,9 @@ class WorkflowManager:
|
|
|
3731
3711
|
succeeded, failed = self._process_workflows_for_registration(workflows_to_register)
|
|
3732
3712
|
|
|
3733
3713
|
except Exception as e:
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
)
|
|
3737
|
-
return RegisterWorkflowsFromConfigResultFailure()
|
|
3714
|
+
details = f"Failed to register workflows from configuration section '{request.config_section}': {e!s}"
|
|
3715
|
+
logger.error(details)
|
|
3716
|
+
return RegisterWorkflowsFromConfigResultFailure(result_details=details)
|
|
3738
3717
|
else:
|
|
3739
3718
|
return RegisterWorkflowsFromConfigResultSuccess(succeeded_workflows=succeeded, failed_workflows=failed)
|
|
3740
3719
|
|