griptape-nodes 0.45.1__py3-none-any.whl → 0.47.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 (30) hide show
  1. griptape_nodes/__init__.py +51 -14
  2. griptape_nodes/exe_types/core_types.py +65 -10
  3. griptape_nodes/exe_types/node_types.py +10 -0
  4. griptape_nodes/machines/node_resolution.py +10 -8
  5. griptape_nodes/node_library/workflow_registry.py +1 -1
  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/events/workflow_events.py +4 -2
  9. griptape_nodes/retained_mode/griptape_nodes.py +17 -13
  10. griptape_nodes/retained_mode/managers/agent_manager.py +8 -6
  11. griptape_nodes/retained_mode/managers/arbitrary_code_exec_manager.py +1 -1
  12. griptape_nodes/retained_mode/managers/config_manager.py +36 -45
  13. griptape_nodes/retained_mode/managers/flow_manager.py +98 -98
  14. griptape_nodes/retained_mode/managers/library_manager.py +51 -51
  15. griptape_nodes/retained_mode/managers/node_manager.py +122 -129
  16. griptape_nodes/retained_mode/managers/object_manager.py +9 -10
  17. griptape_nodes/retained_mode/managers/os_manager.py +31 -31
  18. griptape_nodes/retained_mode/managers/secrets_manager.py +5 -5
  19. griptape_nodes/retained_mode/managers/static_files_manager.py +18 -17
  20. griptape_nodes/retained_mode/managers/sync_manager.py +3 -2
  21. griptape_nodes/retained_mode/managers/version_compatibility_manager.py +84 -1
  22. griptape_nodes/retained_mode/managers/workflow_manager.py +221 -163
  23. griptape_nodes/retained_mode/retained_mode.py +22 -44
  24. griptape_nodes/version_compatibility/workflow_versions/__init__.py +1 -0
  25. griptape_nodes/version_compatibility/workflow_versions/v0_7_0/__init__.py +1 -0
  26. griptape_nodes/version_compatibility/workflow_versions/v0_7_0/local_executor_argument_addition.py +42 -0
  27. {griptape_nodes-0.45.1.dist-info → griptape_nodes-0.47.0.dist-info}/METADATA +1 -1
  28. {griptape_nodes-0.45.1.dist-info → griptape_nodes-0.47.0.dist-info}/RECORD +30 -27
  29. {griptape_nodes-0.45.1.dist-info → griptape_nodes-0.47.0.dist-info}/WHEEL +1 -1
  30. {griptape_nodes-0.45.1.dist-info → griptape_nodes-0.47.0.dist-info}/entry_points.txt +0 -0
@@ -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
@@ -959,6 +967,16 @@ class WorkflowManager:
959
967
  status=status,
960
968
  )
961
969
  )
970
+
971
+ # Check for workflow version-based compatibility issues and add to problems
972
+ workflow_version_issues = GriptapeNodes.VersionCompatibilityManager().check_workflow_version_compatibility(
973
+ workflow_metadata
974
+ )
975
+ for issue in workflow_version_issues:
976
+ problems.append(issue.message)
977
+ if issue.severity == WorkflowManager.WorkflowStatus.UNUSABLE:
978
+ had_critical_error = True
979
+
962
980
  # OK, we have all of our dependencies together. Let's look at the overall scenario.
963
981
  if had_critical_error:
964
982
  overall_status = WorkflowManager.WorkflowStatus.UNUSABLE
@@ -1351,14 +1369,15 @@ class WorkflowManager:
1351
1369
  except Exception as err:
1352
1370
  details = f"Attempted to save workflow '{relative_file_path}', but {err}"
1353
1371
  logger.error(details)
1354
- return SaveWorkflowResultFailure()
1372
+ return SaveWorkflowResultFailure(result_details=details)
1355
1373
 
1356
1374
  # Create the pathing and write the file
1357
1375
  try:
1358
1376
  file_path.parent.mkdir(parents=True, exist_ok=True)
1359
1377
  except OSError as e:
1360
- logger.error("Attempted to save workflow '%s'. Failed when creating directory: %s", file_name, str(e))
1361
- return SaveWorkflowResultFailure()
1378
+ details = f"Attempted to save workflow '{file_name}'. Failed when creating directory: {e}"
1379
+ logger.error(details)
1380
+ return SaveWorkflowResultFailure(result_details=details)
1362
1381
 
1363
1382
  relative_serialized_file_path = f"{file_name}.py"
1364
1383
  serialized_file_path = GriptapeNodes.ConfigManager().workspace_path.joinpath(relative_serialized_file_path)
@@ -1368,17 +1387,17 @@ class WorkflowManager:
1368
1387
  min_space_gb = config_manager.get_config_value("minimum_disk_space_gb_workflows")
1369
1388
  if not OSManager.check_available_disk_space(serialized_file_path.parent, min_space_gb):
1370
1389
  error_msg = OSManager.format_disk_space_error(serialized_file_path.parent)
1371
- logger.error(
1372
- "Attempted to save workflow '%s' (requires %.1f GB). Failed: %s", file_name, min_space_gb, error_msg
1373
- )
1374
- return SaveWorkflowResultFailure()
1390
+ details = f"Attempted to save workflow '{file_name}' (requires {min_space_gb:.1f} GB). Failed: {error_msg}"
1391
+ logger.error(details)
1392
+ return SaveWorkflowResultFailure(result_details=details)
1375
1393
 
1376
1394
  try:
1377
1395
  with serialized_file_path.open("w", encoding="utf-8") as file:
1378
1396
  file.write(final_code_output)
1379
1397
  except OSError as e:
1380
- logger.error("Attempted to save workflow '%s'. Failed when writing file: %s", file_name, str(e))
1381
- return SaveWorkflowResultFailure()
1398
+ details = f"Attempted to save workflow '{file_name}'. Failed when writing file: {e}"
1399
+ logger.error(details)
1400
+ return SaveWorkflowResultFailure(result_details=details)
1382
1401
 
1383
1402
  # save the created workflow as an entry in the JSON config file.
1384
1403
  registered_workflows = WorkflowRegistry.list_workflows()
@@ -1386,8 +1405,9 @@ class WorkflowManager:
1386
1405
  try:
1387
1406
  GriptapeNodes.ConfigManager().save_user_workflow_json(str(file_path))
1388
1407
  except OSError as e:
1389
- logger.error("Attempted to save workflow '%s'. Failed when saving configuration: %s", file_name, str(e))
1390
- return SaveWorkflowResultFailure()
1408
+ details = f"Attempted to save workflow '{file_name}'. Failed when saving configuration: {e}"
1409
+ logger.error(details)
1410
+ return SaveWorkflowResultFailure(result_details=details)
1391
1411
  WorkflowRegistry.generate_new_workflow(metadata=workflow_metadata, file_path=relative_file_path)
1392
1412
  # Update existing workflow's metadata in the registry
1393
1413
  existing_workflow = WorkflowRegistry.get_workflow_by_name(file_name)
@@ -1490,6 +1510,7 @@ class WorkflowManager:
1490
1510
 
1491
1511
  # === imports ===
1492
1512
  import_recorder.add_import("argparse")
1513
+ import_recorder.add_import("json")
1493
1514
  import_recorder.add_from_import(
1494
1515
  "griptape_nodes.bootstrap.workflow_executors.local_workflow_executor", "LocalWorkflowExecutor"
1495
1516
  )
@@ -1629,6 +1650,29 @@ class WorkflowManager:
1629
1650
  )
1630
1651
  )
1631
1652
 
1653
+ # Add json input argument
1654
+ add_arg_calls.append(
1655
+ ast.Expr(
1656
+ value=ast.Call(
1657
+ func=ast.Attribute(
1658
+ value=ast.Name(id="parser", ctx=ast.Load()),
1659
+ attr="add_argument",
1660
+ ctx=ast.Load(),
1661
+ ),
1662
+ args=[ast.Constant("--json-input")],
1663
+ keywords=[
1664
+ ast.keyword(arg="default", value=ast.Constant(None)),
1665
+ ast.keyword(
1666
+ arg="help",
1667
+ value=ast.Constant(
1668
+ "JSON string containing parameter values. Takes precedence over individual parameter arguments if provided."
1669
+ ),
1670
+ ),
1671
+ ],
1672
+ )
1673
+ )
1674
+ )
1675
+
1632
1676
  # Generate individual arguments for each parameter in workflow_shape["input"]
1633
1677
  if "input" in workflow_shape:
1634
1678
  for node_name, node_params in workflow_shape["input"].items():
@@ -1670,13 +1714,47 @@ class WorkflowManager:
1670
1714
  ),
1671
1715
  )
1672
1716
 
1673
- # Build flow_input dictionary from individual CLI arguments
1717
+ # Build flow_input dictionary from JSON input or individual CLI arguments
1674
1718
  flow_input_init = ast.Assign(
1675
1719
  targets=[ast.Name(id="flow_input", ctx=ast.Store())],
1676
1720
  value=ast.Dict(keys=[], values=[]),
1677
1721
  )
1678
1722
 
1679
- # Build the flow_input dict structure from individual arguments
1723
+ # Check if json_input is provided and parse it
1724
+ json_input_if = ast.If(
1725
+ test=ast.Compare(
1726
+ left=ast.Attribute(
1727
+ value=ast.Name(id="args", ctx=ast.Load()),
1728
+ attr="json_input",
1729
+ ctx=ast.Load(),
1730
+ ),
1731
+ ops=[ast.IsNot()],
1732
+ comparators=[ast.Constant(value=None)],
1733
+ ),
1734
+ body=[
1735
+ ast.Assign(
1736
+ targets=[ast.Name(id="flow_input", ctx=ast.Store())],
1737
+ value=ast.Call(
1738
+ func=ast.Attribute(
1739
+ value=ast.Name(id="json", ctx=ast.Load()),
1740
+ attr="loads",
1741
+ ctx=ast.Load(),
1742
+ ),
1743
+ args=[
1744
+ ast.Attribute(
1745
+ value=ast.Name(id="args", ctx=ast.Load()),
1746
+ attr="json_input",
1747
+ ctx=ast.Load(),
1748
+ )
1749
+ ],
1750
+ keywords=[],
1751
+ ),
1752
+ )
1753
+ ],
1754
+ orelse=[],
1755
+ )
1756
+
1757
+ # Build the flow_input dict structure from individual arguments (fallback when no JSON input)
1680
1758
  build_flow_input_stmts = []
1681
1759
 
1682
1760
  # For each node, ensure it exists in flow_input
@@ -1747,6 +1825,21 @@ class WorkflowManager:
1747
1825
  ]
1748
1826
  )
1749
1827
 
1828
+ # Wrap the individual argument processing in an else clause
1829
+ individual_args_else = ast.If(
1830
+ test=ast.Compare(
1831
+ left=ast.Attribute(
1832
+ value=ast.Name(id="args", ctx=ast.Load()),
1833
+ attr="json_input",
1834
+ ctx=ast.Load(),
1835
+ ),
1836
+ ops=[ast.Is()],
1837
+ comparators=[ast.Constant(value=None)],
1838
+ ),
1839
+ body=build_flow_input_stmts,
1840
+ orelse=[],
1841
+ )
1842
+
1750
1843
  workflow_output = ast.Assign(
1751
1844
  targets=[ast.Name(id="workflow_output", ctx=ast.Store())],
1752
1845
  value=ast.Call(
@@ -1780,7 +1873,8 @@ class WorkflowManager:
1780
1873
  *add_arg_calls,
1781
1874
  parse_args,
1782
1875
  flow_input_init,
1783
- *build_flow_input_stmts,
1876
+ json_input_if,
1877
+ individual_args_else,
1784
1878
  workflow_output,
1785
1879
  print_output,
1786
1880
  ],
@@ -2974,7 +3068,7 @@ class WorkflowManager:
2974
3068
  except Exception as e:
2975
3069
  details = f"Failed to publish workflow '{request.workflow_name}': {e!s}"
2976
3070
  logger.exception(details)
2977
- return PublishWorkflowResultFailure(exception=e)
3071
+ return PublishWorkflowResultFailure(exception=e, result_details=details)
2978
3072
 
2979
3073
  def _register_published_workflow_file(self, workflow_file: Path) -> None:
2980
3074
  """Register a published workflow file in the workflow registry."""
@@ -3034,45 +3128,35 @@ class WorkflowManager:
3034
3128
  try:
3035
3129
  workflow = self._get_workflow_by_name(request.workflow_name)
3036
3130
  except KeyError:
3037
- logger.error(
3038
- "Attempted to import workflow '%s' as referenced sub flow. Failed because workflow is not registered",
3039
- request.workflow_name,
3040
- )
3041
- return ImportWorkflowAsReferencedSubFlowResultFailure()
3131
+ details = f"Attempted to import workflow '{request.workflow_name}' as referenced sub flow. Failed because workflow is not registered"
3132
+ logger.error(details)
3133
+ return ImportWorkflowAsReferencedSubFlowResultFailure(result_details=details)
3042
3134
 
3043
3135
  # Check workflow version - Schema version 0.6.0+ required for referenced workflow imports
3044
3136
  # (workflow schema was fixed in 0.6.0 to support importing workflows)
3045
3137
  required_version = Version(major=0, minor=6, patch=0)
3046
3138
  workflow_version = Version.from_string(workflow.metadata.schema_version)
3047
3139
  if workflow_version is None or workflow_version < required_version:
3048
- logger.error(
3049
- "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.",
3050
- request.workflow_name,
3051
- workflow.metadata.schema_version,
3052
- )
3053
- return ImportWorkflowAsReferencedSubFlowResultFailure()
3140
+ 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."
3141
+ logger.error(details)
3142
+ return ImportWorkflowAsReferencedSubFlowResultFailure(result_details=details)
3054
3143
 
3055
3144
  # Check target flow
3056
3145
  flow_name = request.flow_name
3057
3146
  if flow_name is None:
3058
3147
  if not GriptapeNodes.ContextManager().has_current_flow():
3059
- logger.error(
3060
- "Attempted to import workflow '%s' into Current Context. Failed because Current Context was empty",
3061
- request.workflow_name,
3062
- )
3063
- return ImportWorkflowAsReferencedSubFlowResultFailure()
3148
+ details = f"Attempted to import workflow '{request.workflow_name}' into Current Context. Failed because Current Context was empty"
3149
+ logger.error(details)
3150
+ return ImportWorkflowAsReferencedSubFlowResultFailure(result_details=details)
3064
3151
  else:
3065
3152
  # Validate that the specified flow exists
3066
3153
  flow_manager = GriptapeNodes.FlowManager()
3067
3154
  try:
3068
3155
  flow_manager.get_flow_by_name(flow_name)
3069
3156
  except KeyError:
3070
- logger.error(
3071
- "Attempted to import workflow '%s' into flow '%s'. Failed because target flow does not exist",
3072
- request.workflow_name,
3073
- flow_name,
3074
- )
3075
- return ImportWorkflowAsReferencedSubFlowResultFailure()
3157
+ details = f"Attempted to import workflow '{request.workflow_name}' into flow '{flow_name}'. Failed because target flow does not exist"
3158
+ logger.error(details)
3159
+ return ImportWorkflowAsReferencedSubFlowResultFailure(result_details=details)
3076
3160
 
3077
3161
  return None
3078
3162
 
@@ -3094,23 +3178,18 @@ class WorkflowManager:
3094
3178
  workflow_result = self.run_workflow(workflow.file_path)
3095
3179
 
3096
3180
  if not workflow_result.execution_successful:
3097
- logger.error(
3098
- "Attempted to import workflow '%s' as referenced sub flow. Failed because workflow execution failed: %s",
3099
- request.workflow_name,
3100
- workflow_result.execution_details,
3101
- )
3102
- return ImportWorkflowAsReferencedSubFlowResultFailure()
3181
+ details = f"Attempted to import workflow '{request.workflow_name}' as referenced sub flow. Failed because workflow execution failed: {workflow_result.execution_details}"
3182
+ logger.error(details)
3183
+ return ImportWorkflowAsReferencedSubFlowResultFailure(result_details=details)
3103
3184
 
3104
3185
  # Get flows after importing to find the new referenced sub flow
3105
3186
  flows_after = set(obj_manager.get_filtered_subset(type=ControlFlow).keys())
3106
3187
  new_flows = flows_after - flows_before
3107
3188
 
3108
3189
  if not new_flows:
3109
- logger.error(
3110
- "Attempted to import workflow '%s' as referenced sub flow. Failed because no new flow was created",
3111
- request.workflow_name,
3112
- )
3113
- return ImportWorkflowAsReferencedSubFlowResultFailure()
3190
+ details = f"Attempted to import workflow '{request.workflow_name}' as referenced sub flow. Failed because no new flow was created"
3191
+ logger.error(details)
3192
+ return ImportWorkflowAsReferencedSubFlowResultFailure(result_details=details)
3114
3193
 
3115
3194
  # For now, use the first created flow as the main imported flow
3116
3195
  # This handles nested workflows correctly since sub-flows are expected
@@ -3132,12 +3211,9 @@ class WorkflowManager:
3132
3211
  set_metadata_result = GriptapeNodes.handle_request(set_metadata_request)
3133
3212
 
3134
3213
  if not isinstance(set_metadata_result, SetFlowMetadataResultSuccess):
3135
- logger.error(
3136
- "Attempted to import workflow '%s' as referenced sub flow. Failed because metadata could not be applied to created flow '%s'",
3137
- request.workflow_name,
3138
- created_flow_name,
3139
- )
3140
- return ImportWorkflowAsReferencedSubFlowResultFailure()
3214
+ 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}'"
3215
+ logger.error(details)
3216
+ return ImportWorkflowAsReferencedSubFlowResultFailure(result_details=details)
3141
3217
 
3142
3218
  logger.debug(
3143
3219
  "Applied imported flow metadata to '%s': %s", created_flow_name, request.imported_flow_metadata
@@ -3154,8 +3230,9 @@ class WorkflowManager:
3154
3230
  # Validate source workflow exists
3155
3231
  source_workflow = WorkflowRegistry.get_workflow_by_name(request.workflow_name)
3156
3232
  except KeyError:
3157
- logger.error("Failed to branch workflow '%s' because it does not exist", request.workflow_name)
3158
- return BranchWorkflowResultFailure()
3233
+ details = f"Failed to branch workflow '{request.workflow_name}' because it does not exist"
3234
+ logger.error(details)
3235
+ return BranchWorkflowResultFailure(result_details=details)
3159
3236
 
3160
3237
  # Generate branch name if not provided
3161
3238
  branch_name = request.branched_workflow_name
@@ -3169,12 +3246,9 @@ class WorkflowManager:
3169
3246
 
3170
3247
  # Check if branch name already exists
3171
3248
  if WorkflowRegistry.has_workflow_with_name(branch_name):
3172
- logger.error(
3173
- "Failed to branch workflow '%s' because branch name '%s' already exists",
3174
- request.workflow_name,
3175
- branch_name,
3176
- )
3177
- return BranchWorkflowResultFailure()
3249
+ details = f"Failed to branch workflow '{request.workflow_name}' because branch name '{branch_name}' already exists"
3250
+ logger.error(details)
3251
+ return BranchWorkflowResultFailure(result_details=details)
3178
3252
 
3179
3253
  try:
3180
3254
  # Create branch metadata by copying source metadata
@@ -3201,21 +3275,18 @@ class WorkflowManager:
3201
3275
  # Read source workflow content and replace metadata header
3202
3276
  source_file_path = WorkflowRegistry.get_complete_file_path(source_workflow.file_path)
3203
3277
  if not Path(source_file_path).exists():
3204
- logger.error(
3205
- "Failed to branch workflow '%s': File path '%s' does not exist. "
3206
- "The workflow may have been moved or the workspace configuration may have changed.",
3207
- request.workflow_name,
3208
- source_file_path,
3209
- )
3210
- return BranchWorkflowResultFailure()
3278
+ 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."
3279
+ logger.error(details)
3280
+ return BranchWorkflowResultFailure(result_details=details)
3211
3281
 
3212
3282
  source_content = Path(source_file_path).read_text(encoding="utf-8")
3213
3283
 
3214
3284
  # Replace the metadata header with branch metadata
3215
3285
  branch_content = self._replace_workflow_metadata_header(source_content, branch_metadata)
3216
3286
  if branch_content is None:
3217
- logger.error("Failed to replace metadata header for branch workflow '%s'", branch_name)
3218
- return BranchWorkflowResultFailure()
3287
+ details = f"Failed to replace metadata header for branch workflow '{branch_name}'"
3288
+ logger.error(details)
3289
+ return BranchWorkflowResultFailure(result_details=details)
3219
3290
 
3220
3291
  # Write branch workflow file to disk BEFORE registering in registry
3221
3292
  branch_full_path = WorkflowRegistry.get_complete_file_path(branch_file_path)
@@ -3234,11 +3305,12 @@ class WorkflowManager:
3234
3305
  )
3235
3306
 
3236
3307
  except Exception as e:
3237
- logger.error("Failed to branch workflow '%s': %s", request.workflow_name, str(e))
3308
+ details = f"Failed to branch workflow '{request.workflow_name}': {e!s}"
3309
+ logger.error(details)
3238
3310
  import traceback
3239
3311
 
3240
3312
  traceback.print_exc()
3241
- return BranchWorkflowResultFailure()
3313
+ return BranchWorkflowResultFailure(result_details=details)
3242
3314
 
3243
3315
  def on_merge_workflow_branch_request(self, request: MergeWorkflowBranchRequest) -> ResultPayload:
3244
3316
  """Merge a branch back into its source workflow, removing the branch when complete."""
@@ -3246,28 +3318,24 @@ class WorkflowManager:
3246
3318
  # Validate branch workflow exists
3247
3319
  branch_workflow = WorkflowRegistry.get_workflow_by_name(request.workflow_name)
3248
3320
  except KeyError as e:
3249
- logger.error("Failed to merge workflow branch because it does not exist: %s", str(e))
3250
- return MergeWorkflowBranchResultFailure()
3321
+ details = f"Failed to merge workflow branch because it does not exist: {e!s}"
3322
+ logger.error(details)
3323
+ return MergeWorkflowBranchResultFailure(result_details=details)
3251
3324
 
3252
3325
  # Get source workflow name from branch metadata
3253
3326
  source_workflow_name = branch_workflow.metadata.branched_from
3254
3327
  if not source_workflow_name:
3255
- logger.error(
3256
- "Failed to merge workflow branch '%s' because it has no source workflow",
3257
- request.workflow_name,
3258
- )
3259
- return MergeWorkflowBranchResultFailure()
3328
+ details = f"Failed to merge workflow branch '{request.workflow_name}' because it has no source workflow"
3329
+ logger.error(details)
3330
+ return MergeWorkflowBranchResultFailure(result_details=details)
3260
3331
 
3261
3332
  # Validate source workflow exists
3262
3333
  try:
3263
3334
  source_workflow = WorkflowRegistry.get_workflow_by_name(source_workflow_name)
3264
3335
  except KeyError:
3265
- logger.error(
3266
- "Failed to merge workflow branch '%s' because source workflow '%s' does not exist",
3267
- request.workflow_name,
3268
- source_workflow_name,
3269
- )
3270
- return MergeWorkflowBranchResultFailure()
3336
+ details = f"Failed to merge workflow branch '{request.workflow_name}' because source workflow '{source_workflow_name}' does not exist"
3337
+ logger.error(details)
3338
+ return MergeWorkflowBranchResultFailure(result_details=details)
3271
3339
 
3272
3340
  try:
3273
3341
  # Create updated metadata for source workflow - update timestamp
@@ -3295,8 +3363,9 @@ class WorkflowManager:
3295
3363
  # Replace the metadata header with merged metadata
3296
3364
  merged_content = self._replace_workflow_metadata_header(branch_content, merged_metadata)
3297
3365
  if merged_content is None:
3298
- logger.error("Failed to replace metadata header for merged workflow '%s'", source_workflow_name)
3299
- return MergeWorkflowBranchResultFailure()
3366
+ details = f"Failed to replace metadata header for merged workflow '{source_workflow_name}'"
3367
+ logger.error(details)
3368
+ return MergeWorkflowBranchResultFailure(result_details=details)
3300
3369
 
3301
3370
  # Write the updated content to the source workflow file
3302
3371
  source_file_path = WorkflowRegistry.get_complete_file_path(source_workflow.file_path)
@@ -3326,13 +3395,9 @@ class WorkflowManager:
3326
3395
  return MergeWorkflowBranchResultSuccess(merged_workflow_name=source_workflow_name)
3327
3396
 
3328
3397
  except Exception as e:
3329
- logger.error(
3330
- "Failed to merge branch workflow '%s' into source workflow '%s': %s",
3331
- request.workflow_name,
3332
- source_workflow_name,
3333
- str(e),
3334
- )
3335
- return MergeWorkflowBranchResultFailure()
3398
+ details = f"Failed to merge branch workflow '{request.workflow_name}' into source workflow '{source_workflow_name}': {e!s}"
3399
+ logger.error(details)
3400
+ return MergeWorkflowBranchResultFailure(result_details=details)
3336
3401
 
3337
3402
  def on_reset_workflow_branch_request(self, request: ResetWorkflowBranchRequest) -> ResultPayload:
3338
3403
  """Reset a branch to match its source workflow, discarding branch changes."""
@@ -3340,28 +3405,24 @@ class WorkflowManager:
3340
3405
  # Validate branch workflow exists
3341
3406
  branch_workflow = WorkflowRegistry.get_workflow_by_name(request.workflow_name)
3342
3407
  except KeyError as e:
3343
- logger.error("Failed to reset workflow branch because it does not exist: %s", str(e))
3344
- return ResetWorkflowBranchResultFailure()
3408
+ details = f"Failed to reset workflow branch because it does not exist: {e!s}"
3409
+ logger.error(details)
3410
+ return ResetWorkflowBranchResultFailure(result_details=details)
3345
3411
 
3346
3412
  # Get source workflow name from branch metadata
3347
3413
  source_workflow_name = branch_workflow.metadata.branched_from
3348
3414
  if not source_workflow_name:
3349
- logger.error(
3350
- "Failed to reset workflow branch '%s' because it has no source workflow",
3351
- request.workflow_name,
3352
- )
3353
- return ResetWorkflowBranchResultFailure()
3415
+ details = f"Failed to reset workflow branch '{request.workflow_name}' because it has no source workflow"
3416
+ logger.error(details)
3417
+ return ResetWorkflowBranchResultFailure(result_details=details)
3354
3418
 
3355
3419
  # Validate source workflow exists
3356
3420
  try:
3357
3421
  source_workflow = WorkflowRegistry.get_workflow_by_name(source_workflow_name)
3358
3422
  except KeyError:
3359
- logger.error(
3360
- "Failed to reset workflow branch '%s' because source workflow '%s' does not exist",
3361
- request.workflow_name,
3362
- source_workflow_name,
3363
- )
3364
- return ResetWorkflowBranchResultFailure()
3423
+ details = f"Failed to reset workflow branch '{request.workflow_name}' because source workflow '{source_workflow_name}' does not exist"
3424
+ logger.error(details)
3425
+ return ResetWorkflowBranchResultFailure(result_details=details)
3365
3426
 
3366
3427
  try:
3367
3428
  # Read content from the source workflow (what we're resetting the branch to)
@@ -3389,8 +3450,9 @@ class WorkflowManager:
3389
3450
  # Replace the metadata header with reset metadata
3390
3451
  reset_content = self._replace_workflow_metadata_header(source_content, reset_metadata)
3391
3452
  if reset_content is None:
3392
- logger.error("Failed to replace metadata header for reset branch workflow '%s'", request.workflow_name)
3393
- return ResetWorkflowBranchResultFailure()
3453
+ details = f"Failed to replace metadata header for reset branch workflow '{request.workflow_name}'"
3454
+ logger.error(details)
3455
+ return ResetWorkflowBranchResultFailure(result_details=details)
3394
3456
 
3395
3457
  # Write the updated content to the branch workflow file
3396
3458
  branch_content_file_path = WorkflowRegistry.get_complete_file_path(branch_workflow.file_path)
@@ -3400,13 +3462,9 @@ class WorkflowManager:
3400
3462
  branch_workflow.metadata = reset_metadata
3401
3463
 
3402
3464
  except Exception as e:
3403
- logger.error(
3404
- "Failed to reset branch workflow '%s' to source workflow '%s': %s",
3405
- request.workflow_name,
3406
- source_workflow_name,
3407
- str(e),
3408
- )
3409
- return ResetWorkflowBranchResultFailure()
3465
+ details = f"Failed to reset branch workflow '{request.workflow_name}' to source workflow '{source_workflow_name}': {e!s}"
3466
+ logger.error(details)
3467
+ return ResetWorkflowBranchResultFailure(result_details=details)
3410
3468
  else:
3411
3469
  logger.info(
3412
3470
  "Successfully reset branch workflow '%s' to match source workflow '%s'",
@@ -3421,8 +3479,9 @@ class WorkflowManager:
3421
3479
  # Get the workflow to evaluate
3422
3480
  workflow = WorkflowRegistry.get_workflow_by_name(request.workflow_name)
3423
3481
  except KeyError:
3424
- logger.error("Failed to compare workflow '%s' because it does not exist", request.workflow_name)
3425
- return CompareWorkflowsResultFailure()
3482
+ details = f"Failed to compare workflow '{request.workflow_name}' because it does not exist"
3483
+ logger.error(details)
3484
+ return CompareWorkflowsResultFailure(result_details=details)
3426
3485
 
3427
3486
  # Use the provided compare_workflow_name
3428
3487
  compare_workflow_name = request.compare_workflow_name
@@ -3647,10 +3706,9 @@ class WorkflowManager:
3647
3706
  succeeded, failed = self._process_workflows_for_registration(workflows_to_register)
3648
3707
 
3649
3708
  except Exception as e:
3650
- logger.error(
3651
- "Failed to register workflows from configuration section '%s': %s", request.config_section, str(e)
3652
- )
3653
- return RegisterWorkflowsFromConfigResultFailure()
3709
+ details = f"Failed to register workflows from configuration section '{request.config_section}': {e!s}"
3710
+ logger.error(details)
3711
+ return RegisterWorkflowsFromConfigResultFailure(result_details=details)
3654
3712
  else:
3655
3713
  return RegisterWorkflowsFromConfigResultSuccess(succeeded_workflows=succeeded, failed_workflows=failed)
3656
3714