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.
Files changed (25) hide show
  1. griptape_nodes/app/app.py +1 -1
  2. griptape_nodes/exe_types/core_types.py +129 -10
  3. griptape_nodes/exe_types/node_types.py +9 -3
  4. griptape_nodes/machines/node_resolution.py +10 -8
  5. griptape_nodes/mcp_server/ws_request_manager.py +6 -6
  6. griptape_nodes/retained_mode/events/base_events.py +74 -1
  7. griptape_nodes/retained_mode/events/secrets_events.py +2 -0
  8. griptape_nodes/retained_mode/griptape_nodes.py +17 -13
  9. griptape_nodes/retained_mode/managers/agent_manager.py +8 -6
  10. griptape_nodes/retained_mode/managers/arbitrary_code_exec_manager.py +1 -1
  11. griptape_nodes/retained_mode/managers/config_manager.py +36 -45
  12. griptape_nodes/retained_mode/managers/flow_manager.py +98 -98
  13. griptape_nodes/retained_mode/managers/library_manager.py +57 -57
  14. griptape_nodes/retained_mode/managers/node_manager.py +121 -124
  15. griptape_nodes/retained_mode/managers/object_manager.py +9 -10
  16. griptape_nodes/retained_mode/managers/os_manager.py +31 -31
  17. griptape_nodes/retained_mode/managers/secrets_manager.py +5 -5
  18. griptape_nodes/retained_mode/managers/static_files_manager.py +19 -21
  19. griptape_nodes/retained_mode/managers/sync_manager.py +3 -2
  20. griptape_nodes/retained_mode/managers/workflow_manager.py +153 -174
  21. griptape_nodes/retained_mode/retained_mode.py +25 -47
  22. {griptape_nodes-0.46.0.dist-info → griptape_nodes-0.48.0.dist-info}/METADATA +1 -1
  23. {griptape_nodes-0.46.0.dist-info → griptape_nodes-0.48.0.dist-info}/RECORD +25 -25
  24. {griptape_nodes-0.46.0.dist-info → griptape_nodes-0.48.0.dist-info}/WHEEL +1 -1
  25. {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 = " UNKNOWN"
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
- logger.error("Failed to get workflow from registry.")
571
- return RunWorkflowFromRegistryResultFailure()
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
- logger.error("Failed to move workflow '%s' because it does not exist", request.workflow_name)
678
- return MoveWorkflowResultFailure()
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
- logger.error(
686
- "Failed to move workflow '%s': File path '%s' does not exist", request.workflow_name, current_file_path
687
+ details = (
688
+ f"Failed to move workflow '{request.workflow_name}': File path '{current_file_path}' does not exist."
687
689
  )
688
- return MoveWorkflowResultFailure()
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
- logger.error("Failed to create target directory '%s': %s", target_dir_path, str(e))
702
- return MoveWorkflowResultFailure()
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
- logger.error(
712
- "Failed to move workflow '%s': Target file '%s' already exists",
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
- return MoveWorkflowResultFailure()
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
- logger.error("Failed to move workflow file '%s' to '%s': %s", current_file_path, new_absolute_path, str(e))
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
- logger.info("Rolled back file move for workflow '%s'", request.workflow_name)
740
+ details = f"Rolled back file move for workflow '{request.workflow_name}'"
741
+ logger.info(details)
737
742
  except OSError:
738
- logger.error("Failed to rollback file move for workflow '%s'", request.workflow_name)
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
- logger.error("Failed to move workflow '%s': %s", request.workflow_name, str(e))
743
- return MoveWorkflowResultFailure()
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
- logger.info("Successfully moved workflow '%s' to '%s'", request.workflow_name, new_relative_path)
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
- library_metadata_result = GriptapeNodes.handle_request(library_metadata_request)
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
- logger.error("Attempted to save workflow '%s'. Failed when creating directory: %s", file_name, str(e))
1371
- return SaveWorkflowResultFailure()
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
- logger.error(
1382
- "Attempted to save workflow '%s' (requires %.1f GB). Failed: %s", file_name, min_space_gb, error_msg
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
- logger.error("Attempted to save workflow '%s'. Failed when writing file: %s", file_name, str(e))
1391
- return SaveWorkflowResultFailure()
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
- logger.error("Attempted to save workflow '%s'. Failed when saving configuration: %s", file_name, str(e))
1400
- return SaveWorkflowResultFailure()
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
- logger.error(
3122
- "Attempted to import workflow '%s' as referenced sub flow. Failed because workflow is not registered",
3123
- request.workflow_name,
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
- logger.error(
3133
- "Attempted to import workflow '%s' as referenced sub flow. Failed because workflow version '%s' 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.",
3134
- request.workflow_name,
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
- logger.error(
3144
- "Attempted to import workflow '%s' into Current Context. Failed because Current Context was empty",
3145
- request.workflow_name,
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
- logger.error(
3155
- "Attempted to import workflow '%s' into flow '%s'. Failed because target flow does not exist",
3156
- request.workflow_name,
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
- logger.error(
3182
- "Attempted to import workflow '%s' as referenced sub flow. Failed because workflow execution failed: %s",
3183
- request.workflow_name,
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
- logger.error(
3194
- "Attempted to import workflow '%s' as referenced sub flow. Failed because no new flow was created",
3195
- request.workflow_name,
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
- logger.error(
3220
- "Attempted to import workflow '%s' as referenced sub flow. Failed because metadata could not be applied to created flow '%s'",
3221
- request.workflow_name,
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
- logger.error("Failed to branch workflow '%s' because it does not exist", request.workflow_name)
3242
- return BranchWorkflowResultFailure()
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
- logger.error(
3257
- "Failed to branch workflow '%s' because branch name '%s' already exists",
3258
- request.workflow_name,
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
- logger.error(
3289
- "Failed to branch workflow '%s': File path '%s' does not exist. "
3290
- "The workflow may have been moved or the workspace configuration may have changed.",
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
- logger.error("Failed to replace metadata header for branch workflow '%s'", branch_name)
3302
- return BranchWorkflowResultFailure()
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
- logger.error("Failed to branch workflow '%s': %s", request.workflow_name, str(e))
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
- logger.error("Failed to merge workflow branch because it does not exist: %s", str(e))
3334
- return MergeWorkflowBranchResultFailure()
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
- logger.error(
3340
- "Failed to merge workflow branch '%s' because it has no source workflow",
3341
- request.workflow_name,
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
- logger.error(
3350
- "Failed to merge workflow branch '%s' because source workflow '%s' does not exist",
3351
- request.workflow_name,
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
- logger.error("Failed to replace metadata header for merged workflow '%s'", source_workflow_name)
3383
- return MergeWorkflowBranchResultFailure()
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
- logger.error(
3414
- "Failed to merge branch workflow '%s' into source workflow '%s': %s",
3415
- request.workflow_name,
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
- logger.error("Failed to reset workflow branch because it does not exist: %s", str(e))
3428
- return ResetWorkflowBranchResultFailure()
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
- logger.error(
3434
- "Failed to reset workflow branch '%s' because it has no source workflow",
3435
- request.workflow_name,
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
- logger.error(
3444
- "Failed to reset workflow branch '%s' because source workflow '%s' does not exist",
3445
- request.workflow_name,
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
- logger.error("Failed to replace metadata header for reset branch workflow '%s'", request.workflow_name)
3477
- return ResetWorkflowBranchResultFailure()
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
- logger.error(
3488
- "Failed to reset branch workflow '%s' to source workflow '%s': %s",
3489
- request.workflow_name,
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
- logger.error("Failed to compare workflow '%s' because it does not exist", request.workflow_name)
3509
- return CompareWorkflowsResultFailure()
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
- logger.error(
3735
- "Failed to register workflows from configuration section '%s': %s", request.config_section, str(e)
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