griptape-nodes 0.52.1__py3-none-any.whl → 0.53.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. griptape_nodes/__init__.py +6 -943
  2. griptape_nodes/__main__.py +6 -0
  3. griptape_nodes/app/app.py +45 -61
  4. griptape_nodes/cli/__init__.py +1 -0
  5. griptape_nodes/cli/commands/__init__.py +1 -0
  6. griptape_nodes/cli/commands/config.py +71 -0
  7. griptape_nodes/cli/commands/engine.py +80 -0
  8. griptape_nodes/cli/commands/init.py +548 -0
  9. griptape_nodes/cli/commands/libraries.py +90 -0
  10. griptape_nodes/cli/commands/self.py +117 -0
  11. griptape_nodes/cli/main.py +46 -0
  12. griptape_nodes/cli/shared.py +84 -0
  13. griptape_nodes/common/__init__.py +1 -0
  14. griptape_nodes/common/directed_graph.py +55 -0
  15. griptape_nodes/drivers/storage/local_storage_driver.py +7 -2
  16. griptape_nodes/exe_types/core_types.py +60 -2
  17. griptape_nodes/exe_types/node_types.py +38 -24
  18. griptape_nodes/machines/control_flow.py +86 -22
  19. griptape_nodes/machines/fsm.py +10 -1
  20. griptape_nodes/machines/parallel_resolution.py +570 -0
  21. griptape_nodes/machines/{node_resolution.py → sequential_resolution.py} +22 -51
  22. griptape_nodes/retained_mode/events/base_events.py +2 -2
  23. griptape_nodes/retained_mode/events/node_events.py +4 -3
  24. griptape_nodes/retained_mode/griptape_nodes.py +25 -12
  25. griptape_nodes/retained_mode/managers/agent_manager.py +9 -5
  26. griptape_nodes/retained_mode/managers/arbitrary_code_exec_manager.py +3 -1
  27. griptape_nodes/retained_mode/managers/context_manager.py +6 -5
  28. griptape_nodes/retained_mode/managers/flow_manager.py +117 -204
  29. griptape_nodes/retained_mode/managers/library_lifecycle/library_directory.py +1 -1
  30. griptape_nodes/retained_mode/managers/library_manager.py +35 -25
  31. griptape_nodes/retained_mode/managers/node_manager.py +81 -199
  32. griptape_nodes/retained_mode/managers/object_manager.py +11 -5
  33. griptape_nodes/retained_mode/managers/os_manager.py +24 -9
  34. griptape_nodes/retained_mode/managers/secrets_manager.py +8 -4
  35. griptape_nodes/retained_mode/managers/settings.py +32 -1
  36. griptape_nodes/retained_mode/managers/static_files_manager.py +8 -3
  37. griptape_nodes/retained_mode/managers/sync_manager.py +8 -5
  38. griptape_nodes/retained_mode/managers/workflow_manager.py +110 -122
  39. griptape_nodes/traits/add_param_button.py +1 -1
  40. griptape_nodes/traits/button.py +216 -6
  41. griptape_nodes/traits/color_picker.py +66 -0
  42. griptape_nodes/traits/traits.json +4 -0
  43. {griptape_nodes-0.52.1.dist-info → griptape_nodes-0.53.0.dist-info}/METADATA +2 -1
  44. {griptape_nodes-0.52.1.dist-info → griptape_nodes-0.53.0.dist-info}/RECORD +46 -32
  45. {griptape_nodes-0.52.1.dist-info → griptape_nodes-0.53.0.dist-info}/WHEEL +0 -0
  46. {griptape_nodes-0.52.1.dist-info → griptape_nodes-0.53.0.dist-info}/entry_points.txt +0 -0
@@ -29,6 +29,9 @@ from griptape_nodes.retained_mode.events.app_events import (
29
29
  GetEngineVersionRequest,
30
30
  GetEngineVersionResultSuccess,
31
31
  )
32
+
33
+ # Runtime imports for ResultDetails since it's used at runtime
34
+ from griptape_nodes.retained_mode.events.base_events import ResultDetail, ResultDetails
32
35
  from griptape_nodes.retained_mode.events.flow_events import (
33
36
  CreateFlowRequest,
34
37
  GetTopLevelFlowRequest,
@@ -535,7 +538,6 @@ class WorkflowManager:
535
538
  complete_file_path = WorkflowRegistry.get_complete_file_path(relative_file_path=relative_file_path)
536
539
  if not Path(complete_file_path).is_file():
537
540
  details = f"Failed to find file. Path '{complete_file_path}' doesn't exist."
538
- logger.error(details)
539
541
  return RunWorkflowFromScratchResultFailure(result_details=details)
540
542
 
541
543
  # Start with a clean slate.
@@ -543,14 +545,12 @@ class WorkflowManager:
543
545
  clear_all_result = GriptapeNodes.handle_request(clear_all_request)
544
546
  if not clear_all_result.succeeded():
545
547
  details = f"Failed to clear the existing object state when trying to run '{complete_file_path}'."
546
- logger.error(details)
547
548
  return RunWorkflowFromScratchResultFailure(result_details=details)
548
549
 
549
550
  # Run the file, goddamn it
550
551
  execution_result = self.run_workflow(relative_file_path=relative_file_path)
551
552
  if execution_result.execution_successful:
552
- logger.debug(execution_result.execution_details)
553
- return RunWorkflowFromScratchResultSuccess()
553
+ return RunWorkflowFromScratchResultSuccess(result_details=execution_result.execution_details)
554
554
 
555
555
  logger.error(execution_result.execution_details)
556
556
  return RunWorkflowFromScratchResultFailure(result_details=execution_result.execution_details)
@@ -560,13 +560,11 @@ class WorkflowManager:
560
560
  complete_file_path = WorkflowRegistry.get_complete_file_path(relative_file_path=relative_file_path)
561
561
  if not Path(complete_file_path).is_file():
562
562
  details = f"Failed to find file. Path '{complete_file_path}' doesn't exist."
563
- logger.error(details)
564
563
  return RunWorkflowWithCurrentStateResultFailure(result_details=details)
565
564
  execution_result = self.run_workflow(relative_file_path=relative_file_path)
566
565
 
567
566
  if execution_result.execution_successful:
568
- logger.debug(execution_result.execution_details)
569
- return RunWorkflowWithCurrentStateResultSuccess()
567
+ return RunWorkflowWithCurrentStateResultSuccess(result_details=execution_result.execution_details)
570
568
  logger.error(execution_result.execution_details)
571
569
  return RunWorkflowWithCurrentStateResultFailure(result_details=execution_result.execution_details)
572
570
 
@@ -576,13 +574,12 @@ class WorkflowManager:
576
574
  workflow = WorkflowRegistry.get_workflow_by_name(request.workflow_name)
577
575
  except KeyError:
578
576
  details = f"Failed to get workflow '{request.workflow_name}' from registry."
579
- logger.error(details)
580
577
  return RunWorkflowFromRegistryResultFailure(result_details=details)
581
578
 
582
579
  # Update current context for workflow.
580
+ context_warning = None
583
581
  if GriptapeNodes.ContextManager().has_current_workflow():
584
- details = f"Started a new workflow '{request.workflow_name}' but a workflow '{GriptapeNodes.ContextManager().get_current_workflow_name()}' was already in the Current Context. Replacing the old with the new."
585
- logger.warning(details)
582
+ context_warning = f"Started a new workflow '{request.workflow_name}' but a workflow '{GriptapeNodes.ContextManager().get_current_workflow_name()}' was already in the Current Context. Replacing the old with the new."
586
583
 
587
584
  # get file_path from workflow
588
585
  relative_file_path = workflow.file_path
@@ -595,7 +592,6 @@ class WorkflowManager:
595
592
  clear_all_result = GriptapeNodes.handle_request(clear_all_request)
596
593
  if not clear_all_result.succeeded():
597
594
  details = f"Failed to clear the existing object state when preparing to run workflow '{request.workflow_name}'."
598
- logger.error(details)
599
595
  return RunWorkflowFromRegistryResultFailure(result_details=details)
600
596
 
601
597
  # Let's run under the assumption that this Workflow will become our Current Context; if we fail, it will revert.
@@ -604,18 +600,24 @@ class WorkflowManager:
604
600
  execution_result = self.run_workflow(relative_file_path=relative_file_path)
605
601
 
606
602
  if not execution_result.execution_successful:
607
- logger.error(execution_result.execution_details)
603
+ result_messages = []
604
+ if context_warning:
605
+ result_messages.append(ResultDetail(message=context_warning, level="WARNING"))
606
+ result_messages.append(ResultDetail(message=execution_result.execution_details, level="ERROR"))
608
607
 
609
608
  # Attempt to clear everything out, as we modified the engine state getting here.
610
609
  clear_all_request = ClearAllObjectStateRequest(i_know_what_im_doing=True)
611
610
  clear_all_result = GriptapeNodes.handle_request(clear_all_request)
612
611
 
613
612
  # The clear-all above here wipes the ContextManager, so no need to do a pop_workflow().
614
- return RunWorkflowFromRegistryResultFailure(result_details=execution_result.execution_details)
613
+ return RunWorkflowFromRegistryResultFailure(result_details=ResultDetails(*result_messages))
615
614
 
616
615
  # Success!
617
- logger.debug(execution_result.execution_details)
618
- return RunWorkflowFromRegistryResultSuccess()
616
+ result_messages = []
617
+ if context_warning:
618
+ result_messages.append(ResultDetail(message=context_warning, level="WARNING"))
619
+ result_messages.append(ResultDetail(message=execution_result.execution_details, level="DEBUG"))
620
+ return RunWorkflowFromRegistryResultSuccess(result_details=ResultDetails(*result_messages))
619
621
 
620
622
  def on_register_workflow_request(self, request: RegisterWorkflowRequest) -> ResultPayload:
621
623
  try:
@@ -625,9 +627,13 @@ class WorkflowManager:
625
627
  workflow = WorkflowRegistry.generate_new_workflow(metadata=request.metadata, file_path=request.file_name)
626
628
  except Exception as e:
627
629
  details = f"Failed to register workflow with name '{request.metadata.name}'. Error: {e}"
628
- logger.error(details)
629
630
  return RegisterWorkflowResultFailure(result_details=details)
630
- return RegisterWorkflowResultSuccess(workflow_name=workflow.metadata.name)
631
+ return RegisterWorkflowResultSuccess(
632
+ workflow_name=workflow.metadata.name,
633
+ result_details=ResultDetails(
634
+ message=f"Successfully registered workflow: {workflow.metadata.name}", level="INFO"
635
+ ),
636
+ )
631
637
 
632
638
  def on_import_workflow_request(self, request: ImportWorkflowRequest) -> ResultPayload:
633
639
  # First, attempt to load metadata from the file
@@ -641,7 +647,10 @@ class WorkflowManager:
641
647
  workflow_name = load_metadata_result.metadata.name
642
648
  if WorkflowRegistry.has_workflow_with_name(workflow_name):
643
649
  # Workflow already exists - no need to re-register
644
- return ImportWorkflowResultSuccess(workflow_name=workflow_name)
650
+ return ImportWorkflowResultSuccess(
651
+ workflow_name=workflow_name,
652
+ result_details=f"Workflow '{workflow_name}' already exists - no need to re-import.",
653
+ )
645
654
 
646
655
  # Now register the workflow with the extracted metadata
647
656
  register_request = RegisterWorkflowRequest(metadata=load_metadata_result.metadata, file_name=request.file_path)
@@ -659,30 +668,34 @@ class WorkflowManager:
659
668
 
660
669
  return ImportWorkflowResultFailure(result_details=details)
661
670
 
662
- return ImportWorkflowResultSuccess(workflow_name=register_result.workflow_name)
671
+ return ImportWorkflowResultSuccess(
672
+ workflow_name=register_result.workflow_name,
673
+ result_details=ResultDetails(
674
+ message=f"Successfully imported workflow: {register_result.workflow_name}", level="INFO"
675
+ ),
676
+ )
663
677
 
664
678
  def on_list_all_workflows_request(self, _request: ListAllWorkflowsRequest) -> ResultPayload:
665
679
  try:
666
680
  workflows = WorkflowRegistry.list_workflows()
667
681
  except Exception:
668
682
  details = "Failed to list all workflows."
669
- logger.error(details)
670
683
  return ListAllWorkflowsResultFailure(result_details=details)
671
- return ListAllWorkflowsResultSuccess(workflows=workflows)
684
+ return ListAllWorkflowsResultSuccess(
685
+ workflows=workflows, result_details=f"Successfully retrieved {len(workflows)} workflows."
686
+ )
672
687
 
673
688
  def on_delete_workflows_request(self, request: DeleteWorkflowRequest) -> ResultPayload:
674
689
  try:
675
690
  workflow = WorkflowRegistry.delete_workflow_by_name(request.name)
676
691
  except Exception as e:
677
692
  details = f"Failed to remove workflow from registry with name '{request.name}'. Exception: {e}"
678
- logger.error(details)
679
693
  return DeleteWorkflowResultFailure(result_details=details)
680
694
  config_manager = GriptapeNodes.ConfigManager()
681
695
  try:
682
696
  config_manager.delete_user_workflow(workflow.file_path)
683
697
  except Exception as e:
684
698
  details = f"Failed to remove workflow from user config with name '{request.name}'. Exception: {e}"
685
- logger.error(details)
686
699
  return DeleteWorkflowResultFailure(result_details=details)
687
700
  # delete the actual file
688
701
  full_path = config_manager.workspace_path.joinpath(workflow.file_path)
@@ -690,25 +703,28 @@ class WorkflowManager:
690
703
  full_path.unlink()
691
704
  except Exception as e:
692
705
  details = f"Failed to delete workflow file with path '{workflow.file_path}'. Exception: {e}"
693
- logger.error(details)
694
706
  return DeleteWorkflowResultFailure(result_details=details)
695
- return DeleteWorkflowResultSuccess()
707
+ return DeleteWorkflowResultSuccess(
708
+ result_details=ResultDetails(message=f"Successfully deleted workflow: {request.name}", level="INFO")
709
+ )
696
710
 
697
711
  def on_rename_workflow_request(self, request: RenameWorkflowRequest) -> ResultPayload:
698
712
  save_workflow_request = GriptapeNodes.handle_request(SaveWorkflowRequest(file_name=request.requested_name))
699
713
 
700
714
  if isinstance(save_workflow_request, SaveWorkflowResultFailure):
701
715
  details = f"Attempted to rename workflow '{request.workflow_name}' to '{request.requested_name}'. Failed while attempting to save."
702
- logger.error(details)
703
716
  return RenameWorkflowResultFailure(result_details=details)
704
717
 
705
718
  delete_workflow_result = GriptapeNodes.handle_request(DeleteWorkflowRequest(name=request.workflow_name))
706
719
  if isinstance(delete_workflow_result, DeleteWorkflowResultFailure):
707
720
  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."
708
- logger.error(details)
709
721
  return RenameWorkflowResultFailure(result_details=details)
710
722
 
711
- return RenameWorkflowResultSuccess()
723
+ return RenameWorkflowResultSuccess(
724
+ result_details=ResultDetails(
725
+ message=f"Successfully renamed workflow to: {request.requested_name}", level="INFO"
726
+ )
727
+ )
712
728
 
713
729
  def on_move_workflow_request(self, request: MoveWorkflowRequest) -> ResultPayload: # noqa: PLR0911
714
730
  try:
@@ -716,7 +732,6 @@ class WorkflowManager:
716
732
  workflow = WorkflowRegistry.get_workflow_by_name(request.workflow_name)
717
733
  except KeyError:
718
734
  details = f"Failed to move workflow '{request.workflow_name}' because it does not exist."
719
- logger.error(details)
720
735
  return MoveWorkflowResultFailure(result_details=details)
721
736
 
722
737
  config_manager = GriptapeNodes.ConfigManager()
@@ -727,7 +742,6 @@ class WorkflowManager:
727
742
  details = (
728
743
  f"Failed to move workflow '{request.workflow_name}': File path '{current_file_path}' does not exist."
729
744
  )
730
- logger.error(details)
731
745
  return MoveWorkflowResultFailure(result_details=details)
732
746
 
733
747
  # Clean and validate target directory
@@ -742,7 +756,6 @@ class WorkflowManager:
742
756
  target_dir_path.mkdir(parents=True, exist_ok=True)
743
757
  except OSError as e:
744
758
  details = f"Failed to create target directory '{target_dir_path}': {e!s}"
745
- logger.error(details)
746
759
  return MoveWorkflowResultFailure(result_details=details)
747
760
 
748
761
  # Create new file path
@@ -755,7 +768,6 @@ class WorkflowManager:
755
768
  details = (
756
769
  f"Failed to move workflow '{request.workflow_name}': Target file '{new_absolute_path}' already exists."
757
770
  )
758
- logger.error(details)
759
771
  return MoveWorkflowResultFailure(result_details=details)
760
772
 
761
773
  try:
@@ -770,28 +782,29 @@ class WorkflowManager:
770
782
  config_manager.save_user_workflow_json(str(new_absolute_path))
771
783
 
772
784
  except OSError as e:
773
- details = f"Failed to move workflow file '{current_file_path}' to '{new_absolute_path}': {e!s}"
774
- logger.error(details)
785
+ error_messages = []
786
+ main_error = f"Failed to move workflow file '{current_file_path}' to '{new_absolute_path}': {e!s}"
787
+ error_messages.append(ResultDetail(message=main_error, level="ERROR"))
775
788
 
776
789
  # Attempt to rollback if file was moved but registry update failed
777
790
  if new_absolute_path.exists() and not Path(current_file_path).exists():
778
791
  try:
779
792
  new_absolute_path.rename(current_file_path)
780
- details = f"Rolled back file move for workflow '{request.workflow_name}'"
781
- logger.info(details)
793
+ rollback_message = f"Rolled back file move for workflow '{request.workflow_name}'"
794
+ error_messages.append(ResultDetail(message=rollback_message, level="INFO"))
782
795
  except OSError:
783
- details = f"Failed to rollback file move for workflow '{request.workflow_name}'"
784
- logger.error(details)
796
+ rollback_failure = f"Failed to rollback file move for workflow '{request.workflow_name}'"
797
+ error_messages.append(ResultDetail(message=rollback_failure, level="ERROR"))
785
798
 
786
- return MoveWorkflowResultFailure(result_details=details)
799
+ return MoveWorkflowResultFailure(result_details=ResultDetails(*error_messages))
787
800
  except Exception as e:
788
801
  details = f"Failed to move workflow '{request.workflow_name}': {e!s}"
789
- logger.error(details)
790
802
  return MoveWorkflowResultFailure(result_details=details)
791
803
  else:
792
804
  details = f"Successfully moved workflow '{request.workflow_name}' to '{new_relative_path}'"
793
- logger.info(details)
794
- return MoveWorkflowResultSuccess(moved_file_path=new_relative_path)
805
+ return MoveWorkflowResultSuccess(
806
+ moved_file_path=new_relative_path, result_details=ResultDetails(message=details, level="INFO")
807
+ )
795
808
 
796
809
  def on_load_workflow_metadata_request( # noqa: C901, PLR0912, PLR0915
797
810
  self, request: LoadWorkflowMetadata
@@ -810,7 +823,6 @@ class WorkflowManager:
810
823
  ],
811
824
  )
812
825
  details = f"Attempted to load workflow metadata for a file at '{complete_file_path}. Failed because no file could be found at that path."
813
- logger.error(details)
814
826
  return LoadWorkflowMetadataResultFailure(result_details=details)
815
827
 
816
828
  # Find the metadata block.
@@ -827,7 +839,6 @@ class WorkflowManager:
827
839
  ],
828
840
  )
829
841
  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."
830
- logger.error(details)
831
842
  return LoadWorkflowMetadataResultFailure(result_details=details)
832
843
 
833
844
  # Now attempt to parse out the metadata section, stripped of comment prefixes.
@@ -847,7 +858,6 @@ class WorkflowManager:
847
858
  problems=[f"Failed because the metadata was not valid TOML: {err}"],
848
859
  )
849
860
  details = f"Attempted to load workflow metadata for a file at '{complete_file_path}'. Failed because the metadata was not valid TOML: {err}"
850
- logger.error(details)
851
861
  return LoadWorkflowMetadataResultFailure(result_details=details)
852
862
 
853
863
  tool_header = "tool"
@@ -863,7 +873,6 @@ class WorkflowManager:
863
873
  problems=[f"Failed because the '[{tool_header}.{griptape_nodes_header}]' section could not be found."],
864
874
  )
865
875
  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}"
866
- logger.error(details)
867
876
  return LoadWorkflowMetadataResultFailure(result_details=details)
868
877
 
869
878
  try:
@@ -881,7 +890,6 @@ class WorkflowManager:
881
890
  ],
882
891
  )
883
892
  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}"
884
- logger.error(details)
885
893
  return LoadWorkflowMetadataResultFailure(result_details=details)
886
894
 
887
895
  # We have valid dependencies, etc.
@@ -1037,7 +1045,9 @@ class WorkflowManager:
1037
1045
  workflow_dependencies=dependency_infos,
1038
1046
  problems=problems,
1039
1047
  )
1040
- return LoadWorkflowMetadataResultSuccess(metadata=workflow_metadata)
1048
+ return LoadWorkflowMetadataResultSuccess(
1049
+ metadata=workflow_metadata, result_details="Workflow metadata loaded successfully."
1050
+ )
1041
1051
 
1042
1052
  def register_workflows_from_config(self, config_section: str) -> None:
1043
1053
  workflows_to_register = GriptapeNodes.ConfigManager().get_config_value(config_section)
@@ -1159,7 +1169,6 @@ class WorkflowManager:
1159
1169
  engine_version_result = GriptapeNodes.handle_request(request=engine_version_request)
1160
1170
  if not isinstance(engine_version_result, GetEngineVersionResultSuccess):
1161
1171
  details = f"Failed getting the engine version for workflow '{file_name}'."
1162
- logger.error(details)
1163
1172
  raise TypeError(details)
1164
1173
  try:
1165
1174
  engine_version_success = cast("GetEngineVersionResultSuccess", engine_version_result)
@@ -1168,7 +1177,6 @@ class WorkflowManager:
1168
1177
  )
1169
1178
  except Exception as err:
1170
1179
  details = f"Failed getting the engine version for workflow '{file_name}': {err}"
1171
- logger.error(details)
1172
1180
  raise ValueError(details) from err
1173
1181
 
1174
1182
  # Keep track of all of the nodes we create and the generated variable names for them.
@@ -1182,7 +1190,6 @@ class WorkflowManager:
1182
1190
  top_level_flow_result = GriptapeNodes.handle_request(top_level_flow_request)
1183
1191
  if not isinstance(top_level_flow_result, GetTopLevelFlowResultSuccess):
1184
1192
  details = f"Failed when requesting to get top level flow for workflow '{file_name}'."
1185
- logger.error(details)
1186
1193
  raise TypeError(details)
1187
1194
  top_level_flow_name = top_level_flow_result.flow_name
1188
1195
  serialized_flow_request = SerializeFlowToCommandsRequest(
@@ -1191,7 +1198,6 @@ class WorkflowManager:
1191
1198
  serialized_flow_result = GriptapeNodes.handle_request(serialized_flow_request)
1192
1199
  if not isinstance(serialized_flow_result, SerializeFlowToCommandsResultSuccess):
1193
1200
  details = f"Failed when serializing flow for workflow '{file_name}'."
1194
- logger.error(details)
1195
1201
  raise TypeError(details)
1196
1202
  serialized_flow_commands = serialized_flow_result.serialized_flow_commands
1197
1203
 
@@ -1214,7 +1220,6 @@ class WorkflowManager:
1214
1220
  )
1215
1221
  if workflow_metadata is None:
1216
1222
  details = f"Failed to generate metadata for workflow '{file_name}'."
1217
- logger.error(details)
1218
1223
  raise ValueError(details)
1219
1224
 
1220
1225
  # Set the image if provided
@@ -1224,7 +1229,6 @@ class WorkflowManager:
1224
1229
  metadata_block = self._generate_workflow_metadata_header(workflow_metadata=workflow_metadata)
1225
1230
  if metadata_block is None:
1226
1231
  details = f"Failed to generate metadata block for workflow '{file_name}'."
1227
- logger.error(details)
1228
1232
  raise ValueError(details)
1229
1233
 
1230
1234
  import_recorder = ImportRecorder()
@@ -1423,7 +1427,6 @@ class WorkflowManager:
1423
1427
  )
1424
1428
  except Exception as err:
1425
1429
  details = f"Attempted to save workflow '{relative_file_path}', but {err}"
1426
- logger.error(details)
1427
1430
  return SaveWorkflowResultFailure(result_details=details)
1428
1431
 
1429
1432
  # Create the pathing and write the file
@@ -1431,7 +1434,6 @@ class WorkflowManager:
1431
1434
  file_path.parent.mkdir(parents=True, exist_ok=True)
1432
1435
  except OSError as e:
1433
1436
  details = f"Attempted to save workflow '{file_name}'. Failed when creating directory: {e}"
1434
- logger.error(details)
1435
1437
  return SaveWorkflowResultFailure(result_details=details)
1436
1438
 
1437
1439
  # Check disk space before writing
@@ -1440,7 +1442,6 @@ class WorkflowManager:
1440
1442
  if not OSManager.check_available_disk_space(file_path.parent, min_space_gb):
1441
1443
  error_msg = OSManager.format_disk_space_error(file_path.parent)
1442
1444
  details = f"Attempted to save workflow '{file_name}' (requires {min_space_gb:.1f} GB). Failed: {error_msg}"
1443
- logger.error(details)
1444
1445
  return SaveWorkflowResultFailure(result_details=details)
1445
1446
 
1446
1447
  try:
@@ -1448,7 +1449,6 @@ class WorkflowManager:
1448
1449
  file.write(final_code_output)
1449
1450
  except OSError as e:
1450
1451
  details = f"Attempted to save workflow '{file_name}'. Failed when writing file: {e}"
1451
- logger.error(details)
1452
1452
  return SaveWorkflowResultFailure(result_details=details)
1453
1453
 
1454
1454
  # save the created workflow as an entry in the JSON config file.
@@ -1460,15 +1460,15 @@ class WorkflowManager:
1460
1460
  GriptapeNodes.ConfigManager().save_user_workflow_json(str(file_path))
1461
1461
  except OSError as e:
1462
1462
  details = f"Attempted to save workflow '{file_name}'. Failed when saving configuration: {e}"
1463
- logger.error(details)
1464
1463
  return SaveWorkflowResultFailure(result_details=details)
1465
1464
  WorkflowRegistry.generate_new_workflow(metadata=workflow_metadata, file_path=relative_file_path)
1466
1465
  # Update existing workflow's metadata in the registry
1467
1466
  existing_workflow = WorkflowRegistry.get_workflow_by_name(file_name)
1468
1467
  existing_workflow.metadata = workflow_metadata
1469
1468
  details = f"Successfully saved workflow to: {file_path}"
1470
- logger.info(details)
1471
- return SaveWorkflowResultSuccess(file_path=str(file_path))
1469
+ return SaveWorkflowResultSuccess(
1470
+ file_path=str(file_path), result_details=ResultDetails(message=details, level="INFO")
1471
+ )
1472
1472
 
1473
1473
  def _generate_workflow_metadata( # noqa: PLR0913
1474
1474
  self,
@@ -3122,12 +3122,10 @@ class WorkflowManager:
3122
3122
  result = flow_manager.on_get_top_level_flow_request(GetTopLevelFlowRequest())
3123
3123
  if result.failed():
3124
3124
  details = f"Workflow '{workflow_name}' does not have a top-level flow."
3125
- logger.error(details)
3126
3125
  raise ValueError(details)
3127
3126
  flow_name = cast("GetTopLevelFlowResultSuccess", result).flow_name
3128
3127
  if flow_name is None:
3129
3128
  details = f"Workflow '{workflow_name}' does not have a top-level flow."
3130
- logger.error(details)
3131
3129
  raise ValueError(details)
3132
3130
 
3133
3131
  control_flow = flow_manager.get_flow_by_name(flow_name)
@@ -3195,22 +3193,21 @@ class WorkflowManager:
3195
3193
  file_name=workflow_file.name,
3196
3194
  )
3197
3195
  )
3196
+ result_messages = []
3198
3197
  if isinstance(register_workflow_result, RegisterWorkflowResultSuccess):
3199
- logger.info(
3200
- "Successfully registered new workflow with file '%s'.",
3201
- workflow_file.name,
3202
- )
3198
+ success_message = f"Successfully registered new workflow with file '{workflow_file.name}'."
3199
+ result_messages.append(ResultDetail(message=success_message, level="INFO"))
3203
3200
  else:
3204
- logger.warning(
3205
- "Failed to register workflow with file '%s': %s",
3206
- workflow_file.name,
3207
- cast("RegisterWorkflowResultFailure", register_workflow_result).exception,
3208
- )
3201
+ failure_message = f"Failed to register workflow with file '{workflow_file.name}': {cast('RegisterWorkflowResultFailure', register_workflow_result).exception}"
3202
+ result_messages.append(ResultDetail(message=failure_message, level="WARNING"))
3209
3203
  else:
3210
- logger.warning(
3211
- "Failed to load metadata for workflow file '%s'. Not registering workflow.",
3212
- workflow_file.name,
3204
+ metadata_failure_message = (
3205
+ f"Failed to load metadata for workflow file '{workflow_file.name}'. Not registering workflow."
3213
3206
  )
3207
+ result_messages = [ResultDetail(message=metadata_failure_message, level="WARNING")]
3208
+
3209
+ # Log all messages through consolidated ResultDetails
3210
+ ResultDetails(*result_messages)
3214
3211
 
3215
3212
  def on_import_workflow_as_referenced_sub_flow_request(
3216
3213
  self, request: ImportWorkflowAsReferencedSubFlowRequest
@@ -3240,7 +3237,6 @@ class WorkflowManager:
3240
3237
  workflow = self._get_workflow_by_name(request.workflow_name)
3241
3238
  except KeyError:
3242
3239
  details = f"Attempted to import workflow '{request.workflow_name}' as referenced sub flow. Failed because workflow is not registered"
3243
- logger.error(details)
3244
3240
  return ImportWorkflowAsReferencedSubFlowResultFailure(result_details=details)
3245
3241
 
3246
3242
  # Check workflow version - Schema version 0.6.0+ required for referenced workflow imports
@@ -3249,7 +3245,6 @@ class WorkflowManager:
3249
3245
  workflow_version = Version.from_string(workflow.metadata.schema_version)
3250
3246
  if workflow_version is None or workflow_version < required_version:
3251
3247
  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."
3252
- logger.error(details)
3253
3248
  return ImportWorkflowAsReferencedSubFlowResultFailure(result_details=details)
3254
3249
 
3255
3250
  # Check target flow
@@ -3257,7 +3252,6 @@ class WorkflowManager:
3257
3252
  if flow_name is None:
3258
3253
  if not GriptapeNodes.ContextManager().has_current_flow():
3259
3254
  details = f"Attempted to import workflow '{request.workflow_name}' into Current Context. Failed because Current Context was empty"
3260
- logger.error(details)
3261
3255
  return ImportWorkflowAsReferencedSubFlowResultFailure(result_details=details)
3262
3256
  else:
3263
3257
  # Validate that the specified flow exists
@@ -3266,7 +3260,6 @@ class WorkflowManager:
3266
3260
  flow_manager.get_flow_by_name(flow_name)
3267
3261
  except KeyError:
3268
3262
  details = f"Attempted to import workflow '{request.workflow_name}' into flow '{flow_name}'. Failed because target flow does not exist"
3269
- logger.error(details)
3270
3263
  return ImportWorkflowAsReferencedSubFlowResultFailure(result_details=details)
3271
3264
 
3272
3265
  return None
@@ -3290,7 +3283,6 @@ class WorkflowManager:
3290
3283
 
3291
3284
  if not workflow_result.execution_successful:
3292
3285
  details = f"Attempted to import workflow '{request.workflow_name}' as referenced sub flow. Failed because workflow execution failed: {workflow_result.execution_details}"
3293
- logger.error(details)
3294
3286
  return ImportWorkflowAsReferencedSubFlowResultFailure(result_details=details)
3295
3287
 
3296
3288
  # Get flows after importing to find the new referenced sub flow
@@ -3299,7 +3291,6 @@ class WorkflowManager:
3299
3291
 
3300
3292
  if not new_flows:
3301
3293
  details = f"Attempted to import workflow '{request.workflow_name}' as referenced sub flow. Failed because no new flow was created"
3302
- logger.error(details)
3303
3294
  return ImportWorkflowAsReferencedSubFlowResultFailure(result_details=details)
3304
3295
 
3305
3296
  # For now, use the first created flow as the main imported flow
@@ -3323,17 +3314,18 @@ class WorkflowManager:
3323
3314
 
3324
3315
  if not isinstance(set_metadata_result, SetFlowMetadataResultSuccess):
3325
3316
  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}'"
3326
- logger.error(details)
3327
3317
  return ImportWorkflowAsReferencedSubFlowResultFailure(result_details=details)
3328
3318
 
3329
3319
  logger.debug(
3330
3320
  "Applied imported flow metadata to '%s': %s", created_flow_name, request.imported_flow_metadata
3331
3321
  )
3332
3322
 
3333
- logger.info(
3334
- "Successfully imported workflow '%s' as referenced sub flow '%s'", request.workflow_name, created_flow_name
3323
+ details = (
3324
+ f"Successfully imported workflow '{request.workflow_name}' as referenced sub flow '{created_flow_name}'"
3325
+ )
3326
+ return ImportWorkflowAsReferencedSubFlowResultSuccess(
3327
+ created_flow_name=created_flow_name, result_details=details
3335
3328
  )
3336
- return ImportWorkflowAsReferencedSubFlowResultSuccess(created_flow_name=created_flow_name)
3337
3329
 
3338
3330
  def on_branch_workflow_request(self, request: BranchWorkflowRequest) -> ResultPayload:
3339
3331
  """Create a branch (copy) of an existing workflow with branch tracking."""
@@ -3342,7 +3334,6 @@ class WorkflowManager:
3342
3334
  source_workflow = WorkflowRegistry.get_workflow_by_name(request.workflow_name)
3343
3335
  except KeyError:
3344
3336
  details = f"Failed to branch workflow '{request.workflow_name}' because it does not exist"
3345
- logger.error(details)
3346
3337
  return BranchWorkflowResultFailure(result_details=details)
3347
3338
 
3348
3339
  # Generate branch name if not provided
@@ -3358,7 +3349,6 @@ class WorkflowManager:
3358
3349
  # Check if branch name already exists
3359
3350
  if WorkflowRegistry.has_workflow_with_name(branch_name):
3360
3351
  details = f"Failed to branch workflow '{request.workflow_name}' because branch name '{branch_name}' already exists"
3361
- logger.error(details)
3362
3352
  return BranchWorkflowResultFailure(result_details=details)
3363
3353
 
3364
3354
  try:
@@ -3387,7 +3377,6 @@ class WorkflowManager:
3387
3377
  source_file_path = WorkflowRegistry.get_complete_file_path(source_workflow.file_path)
3388
3378
  if not Path(source_file_path).exists():
3389
3379
  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."
3390
- logger.error(details)
3391
3380
  return BranchWorkflowResultFailure(result_details=details)
3392
3381
 
3393
3382
  source_content = Path(source_file_path).read_text(encoding="utf-8")
@@ -3396,7 +3385,6 @@ class WorkflowManager:
3396
3385
  branch_content = self._replace_workflow_metadata_header(source_content, branch_metadata)
3397
3386
  if branch_content is None:
3398
3387
  details = f"Failed to replace metadata header for branch workflow '{branch_name}'"
3399
- logger.error(details)
3400
3388
  return BranchWorkflowResultFailure(result_details=details)
3401
3389
 
3402
3390
  # Write branch workflow file to disk BEFORE registering in registry
@@ -3410,14 +3398,15 @@ class WorkflowManager:
3410
3398
  config_manager = GriptapeNodes.ConfigManager()
3411
3399
  config_manager.save_user_workflow_json(branch_full_path)
3412
3400
 
3413
- logger.info("Successfully branched workflow '%s' as '%s'", request.workflow_name, branch_name)
3401
+ details = f"Successfully branched workflow '{request.workflow_name}' as '{branch_name}'"
3414
3402
  return BranchWorkflowResultSuccess(
3415
- branched_workflow_name=branch_name, original_workflow_name=request.workflow_name
3403
+ branched_workflow_name=branch_name,
3404
+ original_workflow_name=request.workflow_name,
3405
+ result_details=ResultDetails(message=details, level="INFO"),
3416
3406
  )
3417
3407
 
3418
3408
  except Exception as e:
3419
3409
  details = f"Failed to branch workflow '{request.workflow_name}': {e!s}"
3420
- logger.error(details)
3421
3410
  import traceback
3422
3411
 
3423
3412
  traceback.print_exc()
@@ -3430,14 +3419,12 @@ class WorkflowManager:
3430
3419
  branch_workflow = WorkflowRegistry.get_workflow_by_name(request.workflow_name)
3431
3420
  except KeyError as e:
3432
3421
  details = f"Failed to merge workflow branch because it does not exist: {e!s}"
3433
- logger.error(details)
3434
3422
  return MergeWorkflowBranchResultFailure(result_details=details)
3435
3423
 
3436
3424
  # Get source workflow name from branch metadata
3437
3425
  source_workflow_name = branch_workflow.metadata.branched_from
3438
3426
  if not source_workflow_name:
3439
3427
  details = f"Failed to merge workflow branch '{request.workflow_name}' because it has no source workflow"
3440
- logger.error(details)
3441
3428
  return MergeWorkflowBranchResultFailure(result_details=details)
3442
3429
 
3443
3430
  # Validate source workflow exists
@@ -3445,7 +3432,6 @@ class WorkflowManager:
3445
3432
  source_workflow = WorkflowRegistry.get_workflow_by_name(source_workflow_name)
3446
3433
  except KeyError:
3447
3434
  details = f"Failed to merge workflow branch '{request.workflow_name}' because source workflow '{source_workflow_name}' does not exist"
3448
- logger.error(details)
3449
3435
  return MergeWorkflowBranchResultFailure(result_details=details)
3450
3436
 
3451
3437
  try:
@@ -3475,7 +3461,6 @@ class WorkflowManager:
3475
3461
  merged_content = self._replace_workflow_metadata_header(branch_content, merged_metadata)
3476
3462
  if merged_content is None:
3477
3463
  details = f"Failed to replace metadata header for merged workflow '{source_workflow_name}'"
3478
- logger.error(details)
3479
3464
  return MergeWorkflowBranchResultFailure(result_details=details)
3480
3465
 
3481
3466
  # Write the updated content to the source workflow file
@@ -3486,28 +3471,28 @@ class WorkflowManager:
3486
3471
  source_workflow.metadata = merged_metadata
3487
3472
 
3488
3473
  # Remove the branch workflow from registry and delete file
3474
+ result_messages = []
3489
3475
  try:
3490
3476
  WorkflowRegistry.delete_workflow_by_name(request.workflow_name)
3491
3477
  Path(branch_content_file_path).unlink()
3492
- logger.info("Deleted branch workflow file and registry entry for '%s'", request.workflow_name)
3478
+ cleanup_message = f"Deleted branch workflow file and registry entry for '{request.workflow_name}'"
3479
+ result_messages.append(ResultDetail(message=cleanup_message, level="INFO"))
3493
3480
  except Exception as delete_error:
3494
- logger.warning(
3495
- "Failed to fully clean up branch workflow '%s': %s",
3496
- request.workflow_name,
3497
- str(delete_error),
3481
+ warning_message = (
3482
+ f"Failed to fully clean up branch workflow '{request.workflow_name}': {delete_error!s}"
3498
3483
  )
3484
+ result_messages.append(ResultDetail(message=warning_message, level="WARNING"))
3499
3485
  # Continue anyway - the merge was successful even if cleanup failed
3500
3486
 
3501
- logger.info(
3502
- "Successfully merged branch workflow '%s' into source workflow '%s'",
3503
- request.workflow_name,
3504
- source_workflow_name,
3487
+ success_message = f"Successfully merged branch workflow '{request.workflow_name}' into source workflow '{source_workflow_name}'"
3488
+ result_messages.append(ResultDetail(message=success_message, level="INFO"))
3489
+
3490
+ return MergeWorkflowBranchResultSuccess(
3491
+ merged_workflow_name=source_workflow_name, result_details=ResultDetails(*result_messages)
3505
3492
  )
3506
- return MergeWorkflowBranchResultSuccess(merged_workflow_name=source_workflow_name)
3507
3493
 
3508
3494
  except Exception as e:
3509
3495
  details = f"Failed to merge branch workflow '{request.workflow_name}' into source workflow '{source_workflow_name}': {e!s}"
3510
- logger.error(details)
3511
3496
  return MergeWorkflowBranchResultFailure(result_details=details)
3512
3497
 
3513
3498
  def on_reset_workflow_branch_request(self, request: ResetWorkflowBranchRequest) -> ResultPayload:
@@ -3517,14 +3502,12 @@ class WorkflowManager:
3517
3502
  branch_workflow = WorkflowRegistry.get_workflow_by_name(request.workflow_name)
3518
3503
  except KeyError as e:
3519
3504
  details = f"Failed to reset workflow branch because it does not exist: {e!s}"
3520
- logger.error(details)
3521
3505
  return ResetWorkflowBranchResultFailure(result_details=details)
3522
3506
 
3523
3507
  # Get source workflow name from branch metadata
3524
3508
  source_workflow_name = branch_workflow.metadata.branched_from
3525
3509
  if not source_workflow_name:
3526
3510
  details = f"Failed to reset workflow branch '{request.workflow_name}' because it has no source workflow"
3527
- logger.error(details)
3528
3511
  return ResetWorkflowBranchResultFailure(result_details=details)
3529
3512
 
3530
3513
  # Validate source workflow exists
@@ -3532,7 +3515,6 @@ class WorkflowManager:
3532
3515
  source_workflow = WorkflowRegistry.get_workflow_by_name(source_workflow_name)
3533
3516
  except KeyError:
3534
3517
  details = f"Failed to reset workflow branch '{request.workflow_name}' because source workflow '{source_workflow_name}' does not exist"
3535
- logger.error(details)
3536
3518
  return ResetWorkflowBranchResultFailure(result_details=details)
3537
3519
 
3538
3520
  try:
@@ -3562,7 +3544,6 @@ class WorkflowManager:
3562
3544
  reset_content = self._replace_workflow_metadata_header(source_content, reset_metadata)
3563
3545
  if reset_content is None:
3564
3546
  details = f"Failed to replace metadata header for reset branch workflow '{request.workflow_name}'"
3565
- logger.error(details)
3566
3547
  return ResetWorkflowBranchResultFailure(result_details=details)
3567
3548
 
3568
3549
  # Write the updated content to the branch workflow file
@@ -3574,15 +3555,12 @@ class WorkflowManager:
3574
3555
 
3575
3556
  except Exception as e:
3576
3557
  details = f"Failed to reset branch workflow '{request.workflow_name}' to source workflow '{source_workflow_name}': {e!s}"
3577
- logger.error(details)
3578
3558
  return ResetWorkflowBranchResultFailure(result_details=details)
3579
3559
  else:
3580
- logger.info(
3581
- "Successfully reset branch workflow '%s' to match source workflow '%s'",
3582
- request.workflow_name,
3583
- source_workflow_name,
3560
+ details = f"Successfully reset branch workflow '{request.workflow_name}' to match source workflow '{source_workflow_name}'"
3561
+ return ResetWorkflowBranchResultSuccess(
3562
+ reset_workflow_name=request.workflow_name, result_details=ResultDetails(message=details, level="INFO")
3584
3563
  )
3585
- return ResetWorkflowBranchResultSuccess(reset_workflow_name=request.workflow_name)
3586
3564
 
3587
3565
  def on_compare_workflows_request(self, request: CompareWorkflowsRequest) -> ResultPayload:
3588
3566
  """Compare two workflows to determine if one is ahead, behind, or up-to-date relative to the other."""
@@ -3591,7 +3569,6 @@ class WorkflowManager:
3591
3569
  workflow = WorkflowRegistry.get_workflow_by_name(request.workflow_name)
3592
3570
  except KeyError:
3593
3571
  details = f"Failed to compare workflow '{request.workflow_name}' because it does not exist"
3594
- logger.error(details)
3595
3572
  return CompareWorkflowsResultFailure(result_details=details)
3596
3573
 
3597
3574
  # Use the provided compare_workflow_name
@@ -3612,6 +3589,7 @@ class WorkflowManager:
3612
3589
  else None,
3613
3590
  source_last_modified=None,
3614
3591
  details=details,
3592
+ result_details="Workflow comparison completed successfully.",
3615
3593
  )
3616
3594
 
3617
3595
  # Compare last modified dates
@@ -3629,6 +3607,7 @@ class WorkflowManager:
3629
3607
  workflow_last_modified=workflow_last_modified.isoformat() if workflow_last_modified else None,
3630
3608
  source_last_modified=source_last_modified.isoformat() if source_last_modified else None,
3631
3609
  details=details,
3610
+ result_details="Workflow comparison completed successfully.",
3632
3611
  )
3633
3612
 
3634
3613
  # Compare timestamps to determine status
@@ -3651,6 +3630,7 @@ class WorkflowManager:
3651
3630
  workflow_last_modified=workflow_last_modified.isoformat(),
3652
3631
  source_last_modified=source_last_modified.isoformat(),
3653
3632
  details=details,
3633
+ result_details="Workflow comparison completed successfully.",
3654
3634
  )
3655
3635
 
3656
3636
  def _walk_object_tree(
@@ -3810,18 +3790,26 @@ class WorkflowManager:
3810
3790
  try:
3811
3791
  workflows_to_register = GriptapeNodes.ConfigManager().get_config_value(request.config_section)
3812
3792
  if not workflows_to_register:
3813
- logger.info("No workflows found in configuration section '%s'", request.config_section)
3814
- return RegisterWorkflowsFromConfigResultSuccess(succeeded_workflows=[], failed_workflows=[])
3793
+ details = f"No workflows found in configuration section '{request.config_section}'"
3794
+ return RegisterWorkflowsFromConfigResultSuccess(
3795
+ succeeded_workflows=[], failed_workflows=[], result_details=details
3796
+ )
3815
3797
 
3816
3798
  # Process all workflows and track results
3817
3799
  succeeded, failed = self._process_workflows_for_registration(workflows_to_register)
3818
3800
 
3819
3801
  except Exception as e:
3820
3802
  details = f"Failed to register workflows from configuration section '{request.config_section}': {e!s}"
3821
- logger.error(details)
3822
3803
  return RegisterWorkflowsFromConfigResultFailure(result_details=details)
3823
3804
  else:
3824
- return RegisterWorkflowsFromConfigResultSuccess(succeeded_workflows=succeeded, failed_workflows=failed)
3805
+ return RegisterWorkflowsFromConfigResultSuccess(
3806
+ succeeded_workflows=succeeded,
3807
+ failed_workflows=failed,
3808
+ result_details=ResultDetails(
3809
+ message=f"Successfully processed workflows: {len(succeeded)} succeeded, {len(failed)} failed.",
3810
+ level="INFO",
3811
+ ),
3812
+ )
3825
3813
 
3826
3814
  def _process_workflows_for_registration(self, workflows_to_register: list[str]) -> WorkflowRegistrationResult:
3827
3815
  """Process a list of workflow paths for registration.