griptape-nodes 0.66.2__py3-none-any.whl → 0.68.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 (34) hide show
  1. griptape_nodes/bootstrap/utils/python_subprocess_executor.py +17 -4
  2. griptape_nodes/common/node_executor.py +295 -18
  3. griptape_nodes/exe_types/core_types.py +28 -1
  4. griptape_nodes/exe_types/node_groups/__init__.py +2 -2
  5. griptape_nodes/exe_types/node_groups/base_iterative_node_group.py +81 -10
  6. griptape_nodes/exe_types/node_groups/base_node_group.py +64 -1
  7. griptape_nodes/exe_types/node_groups/subflow_node_group.py +0 -34
  8. griptape_nodes/exe_types/param_components/huggingface/huggingface_repo_variant_parameter.py +152 -0
  9. griptape_nodes/exe_types/param_components/seed_parameter.py +3 -2
  10. griptape_nodes/exe_types/param_types/parameter_audio.py +3 -0
  11. griptape_nodes/exe_types/param_types/parameter_bool.py +3 -0
  12. griptape_nodes/exe_types/param_types/parameter_button.py +3 -0
  13. griptape_nodes/exe_types/param_types/parameter_dict.py +151 -0
  14. griptape_nodes/exe_types/param_types/parameter_float.py +3 -0
  15. griptape_nodes/exe_types/param_types/parameter_image.py +3 -0
  16. griptape_nodes/exe_types/param_types/parameter_int.py +3 -0
  17. griptape_nodes/exe_types/param_types/parameter_json.py +268 -0
  18. griptape_nodes/exe_types/param_types/parameter_number.py +3 -0
  19. griptape_nodes/exe_types/param_types/parameter_range.py +393 -0
  20. griptape_nodes/exe_types/param_types/parameter_string.py +3 -0
  21. griptape_nodes/exe_types/param_types/parameter_three_d.py +3 -0
  22. griptape_nodes/exe_types/param_types/parameter_video.py +3 -0
  23. griptape_nodes/retained_mode/events/library_events.py +2 -0
  24. griptape_nodes/retained_mode/events/parameter_events.py +89 -1
  25. griptape_nodes/retained_mode/managers/event_manager.py +176 -10
  26. griptape_nodes/retained_mode/managers/flow_manager.py +2 -1
  27. griptape_nodes/retained_mode/managers/library_manager.py +14 -4
  28. griptape_nodes/retained_mode/managers/node_manager.py +187 -7
  29. griptape_nodes/retained_mode/managers/workflow_manager.py +58 -16
  30. griptape_nodes/utils/file_utils.py +58 -0
  31. {griptape_nodes-0.66.2.dist-info → griptape_nodes-0.68.0.dist-info}/METADATA +1 -1
  32. {griptape_nodes-0.66.2.dist-info → griptape_nodes-0.68.0.dist-info}/RECORD +34 -30
  33. {griptape_nodes-0.66.2.dist-info → griptape_nodes-0.68.0.dist-info}/WHEEL +1 -1
  34. {griptape_nodes-0.66.2.dist-info → griptape_nodes-0.68.0.dist-info}/entry_points.txt +0 -0
@@ -2,16 +2,24 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import logging
5
+ import os
5
6
  import sys
6
7
  from typing import TYPE_CHECKING, Any
7
8
 
8
9
  if TYPE_CHECKING:
9
10
  from pathlib import Path
10
- from subprocess import _ENV
11
11
 
12
12
  logger = logging.getLogger(__name__)
13
13
 
14
14
 
15
+ def _create_subprocess_env(extra_env: dict[str, str] | None = None) -> dict[str, str]:
16
+ """Create environment for subprocess, inheriting parent env with optional overrides."""
17
+ env = os.environ.copy()
18
+ if extra_env:
19
+ env.update(extra_env)
20
+ return env
21
+
22
+
15
23
  class PythonSubprocessExecutorError(Exception):
16
24
  """Exception raised during Python subprocess execution."""
17
25
 
@@ -22,7 +30,11 @@ class PythonSubprocessExecutor:
22
30
  self._is_running = False
23
31
 
24
32
  async def execute_python_script(
25
- self, script_path: Path, args: list[str] | None = None, cwd: Path | None = None, env: _ENV | None = None
33
+ self,
34
+ script_path: Path,
35
+ args: list[str] | None = None,
36
+ cwd: Path | None = None,
37
+ env: dict[str, str] | None = None,
26
38
  ) -> None:
27
39
  """Execute a Python script in a subprocess and wait for completion.
28
40
 
@@ -30,7 +42,7 @@ class PythonSubprocessExecutor:
30
42
  script_path: Path to the Python script to execute
31
43
  args: Additional command line arguments
32
44
  cwd: Working directory for the subprocess
33
- env: Environment variables for the subprocess
45
+ env: Extra environment variables to add or override in the subprocess
34
46
  """
35
47
  if self.is_running():
36
48
  logger.warning("Another subprocess is already running. Terminating it first.")
@@ -38,6 +50,7 @@ class PythonSubprocessExecutor:
38
50
 
39
51
  args = args or []
40
52
  command = [sys.executable, str(script_path), *args]
53
+ subprocess_env = _create_subprocess_env(env)
41
54
 
42
55
  try:
43
56
  logger.info("Starting subprocess: %s", " ".join(command))
@@ -46,7 +59,7 @@ class PythonSubprocessExecutor:
46
59
  self._process = await asyncio.create_subprocess_exec(
47
60
  *command,
48
61
  cwd=cwd,
49
- env=env,
62
+ env=subprocess_env,
50
63
  stdout=asyncio.subprocess.PIPE,
51
64
  stderr=asyncio.subprocess.PIPE,
52
65
  )
@@ -5,6 +5,7 @@ import asyncio
5
5
  import logging
6
6
  import pickle
7
7
  from dataclasses import dataclass
8
+ from enum import StrEnum
8
9
  from pathlib import Path
9
10
  from typing import TYPE_CHECKING, Any, NamedTuple
10
11
 
@@ -16,7 +17,7 @@ from griptape_nodes.exe_types.base_iterative_nodes import (
16
17
  BaseIterativeStartNode,
17
18
  )
18
19
  from griptape_nodes.exe_types.core_types import ParameterTypeBuiltin
19
- from griptape_nodes.exe_types.node_groups import BaseIterativeNodeGroup, SubflowNodeGroup
20
+ from griptape_nodes.exe_types.node_groups import BaseIterativeNodeGroup, IterationControlParam, SubflowNodeGroup
20
21
  from griptape_nodes.exe_types.node_types import (
21
22
  CONTROL_INPUT_PARAMETER,
22
23
  LOCAL_EXECUTION,
@@ -106,6 +107,15 @@ if TYPE_CHECKING:
106
107
 
107
108
  logger = logging.getLogger("griptape_nodes")
108
109
 
110
+
111
+ class IterationControlAction(StrEnum):
112
+ """Enum for iterative group control actions."""
113
+
114
+ ADD = "add" # Normal path - add result to list
115
+ SKIP = "skip" # Skip this iteration, don't add result
116
+ BREAK = "break" # Break out of loop immediately
117
+
118
+
109
119
  LOOP_EVENTS_TO_SUPPRESS = {
110
120
  CreateFlowResultSuccess,
111
121
  CreateFlowResultFailure,
@@ -816,13 +826,153 @@ class NodeExecutor:
816
826
  # Check if it's the break signal
817
827
  return next_control_output == deserialized_end_node.break_loop_signal_output
818
828
 
819
- async def _execute_loop_iterations_sequentially( # noqa: PLR0915
829
+ def _get_iteration_control_action_for_group(
830
+ self,
831
+ end_loop_node: BaseIterativeNodeGroup,
832
+ node_name_mappings: dict[str, str],
833
+ ) -> IterationControlAction:
834
+ """Determine which control action was taken during iteration for an iterative group.
835
+
836
+ Checks if any internal nodes have executed their control outputs that connect to the
837
+ group's control parameters (loop_complete, skip_iteration, break_loop). This is done by
838
+ checking the source node's parameter_output_values for CONTROL_INPUT_PARAMETER.
839
+
840
+ Args:
841
+ end_loop_node: The BaseIterativeNodeGroup being executed
842
+ node_name_mappings: Mapping from original to deserialized node names
843
+
844
+ Returns:
845
+ IterationControlAction indicating which control path was taken
846
+ """
847
+ # Get incoming connections to the end_loop_node (the iterative group)
848
+ list_connections_request = ListConnectionsForNodeRequest(node_name=end_loop_node.name)
849
+ list_connections_result = GriptapeNodes.handle_request(list_connections_request)
850
+ if not isinstance(list_connections_result, ListConnectionsForNodeResultSuccess):
851
+ logger.warning("Failed to list connections for node %s", end_loop_node.name)
852
+ return IterationControlAction.ADD
853
+
854
+ incoming_connections = list_connections_result.incoming_connections
855
+
856
+ # Check each control parameter to see if its source node has fired
857
+ # Priority: BREAK > SKIP > LOOP_COMPLETE > default ADD
858
+ break_source = self._find_source_for_control_param(incoming_connections, IterationControlParam.BREAK_LOOP)
859
+ skip_source = self._find_source_for_control_param(incoming_connections, IterationControlParam.SKIP_ITERATION)
860
+ loop_complete_source = self._find_source_for_control_param(
861
+ incoming_connections, IterationControlParam.LOOP_COMPLETE
862
+ )
863
+
864
+ # Check if break was triggered
865
+ if self._check_control_source_fired(break_source, node_name_mappings):
866
+ return IterationControlAction.BREAK
867
+
868
+ # Check if skip was triggered
869
+ if self._check_control_source_fired(skip_source, node_name_mappings):
870
+ return IterationControlAction.SKIP
871
+
872
+ # Check if loop_complete was triggered
873
+ if self._check_control_source_fired(loop_complete_source, node_name_mappings):
874
+ return IterationControlAction.ADD
875
+
876
+ # If none of the control parameters were triggered, default to ADD
877
+ # This preserves backward compatibility for workflows without explicit loop_complete connections
878
+ return IterationControlAction.ADD
879
+
880
+ def _check_control_source_fired(
881
+ self,
882
+ source: tuple[str, str] | None,
883
+ node_name_mappings: dict[str, str],
884
+ ) -> bool:
885
+ """Check if a control source node has fired its control output.
886
+
887
+ Args:
888
+ source: Tuple of (source_node_name, source_parameter_name) or None
889
+ node_name_mappings: Mapping from original to deserialized node names
890
+
891
+ Returns:
892
+ True if the source node's next control output matches the specified parameter
893
+ """
894
+ if source is None:
895
+ return False
896
+
897
+ source_node_name, source_param_name = source
898
+ deserialized_source_name = node_name_mappings.get(source_node_name)
899
+ if deserialized_source_name is None:
900
+ return False
901
+
902
+ node_manager = GriptapeNodes.NodeManager()
903
+ try:
904
+ deserialized_source_node = node_manager.get_node_by_name(deserialized_source_name)
905
+ except ValueError:
906
+ return False
907
+
908
+ if deserialized_source_node is None:
909
+ return False
910
+
911
+ # Check if the node's next control output matches the source parameter
912
+ next_control_output = deserialized_source_node.get_next_control_output()
913
+ if next_control_output is None:
914
+ return False
915
+
916
+ # Get the parameter object to compare
917
+ source_param = deserialized_source_node.get_parameter_by_name(source_param_name)
918
+ return next_control_output == source_param
919
+
920
+ def _find_source_for_control_param(
921
+ self,
922
+ incoming_connections: list,
923
+ control_param_name: str,
924
+ ) -> tuple[str, str] | None:
925
+ """Find the source node and parameter that connects to a control parameter on the iterative group.
926
+
927
+ Args:
928
+ incoming_connections: List of incoming connections to the iterative group
929
+ control_param_name: Name of the control parameter to find (e.g., "break_loop")
930
+
931
+ Returns:
932
+ Tuple of (source_node_name, source_parameter_name), or None if not found
933
+ """
934
+ flow_manager = GriptapeNodes.FlowManager()
935
+ connections = flow_manager.get_connections()
936
+
937
+ for conn in incoming_connections:
938
+ if conn.target_parameter_name != control_param_name:
939
+ continue
940
+
941
+ source_node_name = conn.source_node_name
942
+ source_param_name = conn.source_parameter_name
943
+
944
+ # If source is a SubflowNodeGroup, follow the internal connection to get the actual source
945
+ node_manager = GriptapeNodes.NodeManager()
946
+ try:
947
+ source_node = node_manager.get_node_by_name(source_node_name)
948
+ except ValueError:
949
+ continue
950
+
951
+ if isinstance(source_node, SubflowNodeGroup):
952
+ # Get connections to this proxy parameter to find the actual internal source
953
+ proxy_param = source_node.get_parameter_by_name(source_param_name)
954
+ if proxy_param:
955
+ internal_connections = connections.get_all_incoming_connections(source_node)
956
+ for internal_conn in internal_connections:
957
+ if (
958
+ internal_conn.target_parameter.name == source_param_name
959
+ and internal_conn.is_node_group_internal
960
+ ):
961
+ source_node_name = internal_conn.source_node.name
962
+ source_param_name = internal_conn.source_parameter.name
963
+ break
964
+
965
+ return (source_node_name, source_param_name)
966
+
967
+ return None
968
+
969
+ async def _execute_loop_iterations_sequentially( # noqa: PLR0915, C901, PLR0912
820
970
  self,
821
971
  package_result: PackageNodesAsSerializedFlowResultSuccess,
822
972
  total_iterations: int,
823
973
  parameter_values_per_iteration: dict[int, dict[str, Any]],
824
974
  end_loop_node: BaseIterativeEndNode | BaseIterativeNodeGroup,
825
- ) -> tuple[dict[int, Any], list[int], dict[str, Any]]:
975
+ ) -> tuple[dict[int, Any], list[int], dict[str, Any], int, bool]:
826
976
  """Execute loop iterations sequentially by running one flow instance N times.
827
977
 
828
978
  Args:
@@ -834,8 +984,10 @@ class NodeExecutor:
834
984
  Returns:
835
985
  Tuple of:
836
986
  - iteration_results: Dict mapping iteration_index -> result value
837
- - successful_iterations: List of iteration indices that succeeded
987
+ - successful_iterations: List of iteration indices that executed without error
838
988
  - last_iteration_values: Dict mapping parameter names -> values from last iteration
989
+ - skipped_count: Number of iterations that were skipped via skip control signal
990
+ - break_occurred: True if the loop exited early due to a break signal
839
991
  """
840
992
  # Deserialize flow once
841
993
  context_manager = GriptapeNodes.ContextManager()
@@ -868,6 +1020,8 @@ class NodeExecutor:
868
1020
 
869
1021
  iteration_results: dict[int, Any] = {}
870
1022
  successful_iterations: list[int] = []
1023
+ skipped_count = 0
1024
+ break_occurred = False
871
1025
 
872
1026
  # Build reverse mapping: packaged_name → original_name for event translation
873
1027
  reverse_node_mapping = {
@@ -927,6 +1081,39 @@ class NodeExecutor:
927
1081
 
928
1082
  successful_iterations.append(iteration_index)
929
1083
 
1084
+ # For BaseIterativeNodeGroup, check control action to handle skip/break
1085
+ if isinstance(end_loop_node, BaseIterativeNodeGroup):
1086
+ control_action = self._get_iteration_control_action_for_group(end_loop_node, node_name_mappings)
1087
+
1088
+ if control_action == IterationControlAction.SKIP:
1089
+ logger.info(
1090
+ "Skip detected at iteration %d/%d - skipping result collection",
1091
+ iteration_index + 1,
1092
+ total_iterations,
1093
+ )
1094
+ skipped_count += 1
1095
+ continue
1096
+
1097
+ if control_action == IterationControlAction.BREAK:
1098
+ logger.info(
1099
+ "Break detected at iteration %d/%d - collecting result then stopping",
1100
+ iteration_index + 1,
1101
+ total_iterations,
1102
+ )
1103
+ # Extract result from this iteration before breaking
1104
+ # (the work was done, so we should collect the result)
1105
+ deserialized_flows = [(iteration_index, flow_name, node_name_mappings)]
1106
+ single_iteration_results = self.get_parameter_values_from_iterations(
1107
+ end_loop_node=end_loop_node,
1108
+ deserialized_flows=deserialized_flows,
1109
+ package_flow_result_success=package_result,
1110
+ )
1111
+ iteration_results.update(single_iteration_results)
1112
+ break_occurred = True
1113
+ break
1114
+
1115
+ # control_action == ADD: fall through to extract result
1116
+
930
1117
  # Extract result from this iteration
931
1118
  deserialized_flows = [(iteration_index, flow_name, node_name_mappings)]
932
1119
  single_iteration_results = self.get_parameter_values_from_iterations(
@@ -938,13 +1125,14 @@ class NodeExecutor:
938
1125
 
939
1126
  logger.info("Completed sequential iteration %d/%d", iteration_index + 1, total_iterations)
940
1127
 
941
- # Check if the end node signaled a break
1128
+ # Check if the end node signaled a break (for BaseIterativeEndNode)
942
1129
  if self._should_break_loop(node_name_mappings, package_result):
943
1130
  logger.info(
944
1131
  "Loop break detected at iteration %d/%d - stopping execution early",
945
1132
  iteration_index + 1,
946
1133
  total_iterations,
947
1134
  )
1135
+ break_occurred = True
948
1136
  break
949
1137
 
950
1138
  # Extract last iteration values from the last successful iteration
@@ -956,7 +1144,7 @@ class NodeExecutor:
956
1144
  total_iterations=len(successful_iterations),
957
1145
  )
958
1146
 
959
- return iteration_results, successful_iterations, last_iteration_values
1147
+ return iteration_results, successful_iterations, last_iteration_values, skipped_count, break_occurred
960
1148
 
961
1149
  finally:
962
1150
  # Cleanup - delete the flow
@@ -1018,6 +1206,8 @@ class NodeExecutor:
1018
1206
  iteration_results,
1019
1207
  successful_iterations,
1020
1208
  last_iteration_values,
1209
+ _skipped_count,
1210
+ _break_occurred,
1021
1211
  ) = await self._execute_loop_iterations_sequentially(
1022
1212
  package_result=package_result,
1023
1213
  total_iterations=total_iterations,
@@ -1383,12 +1573,16 @@ class NodeExecutor:
1383
1573
  parameter_values_per_iteration = self._get_merged_parameter_values_for_iterative_group(node, package_result)
1384
1574
 
1385
1575
  # Execute iterations sequentially based on execution environment
1576
+ skipped_count = 0
1577
+ break_occurred = False
1386
1578
  match execution_type:
1387
1579
  case node_types.LOCAL_EXECUTION:
1388
1580
  (
1389
1581
  iteration_results,
1390
1582
  successful_iterations,
1391
1583
  last_iteration_values,
1584
+ skipped_count,
1585
+ break_occurred,
1392
1586
  ) = await self._execute_loop_iterations_sequentially(
1393
1587
  package_result=package_result,
1394
1588
  total_iterations=total_iterations,
@@ -1420,19 +1614,25 @@ class NodeExecutor:
1420
1614
  execution_type=execution_type,
1421
1615
  )
1422
1616
 
1423
- # Check if execution stopped early due to break (not failure)
1617
+ # Check if execution stopped early
1424
1618
  if len(successful_iterations) < total_iterations:
1425
- expected_count = len(successful_iterations)
1426
- actual_count = len(iteration_results)
1427
- if expected_count != actual_count:
1428
- failed_count = expected_count - actual_count
1429
- msg = f"Iterative group execution failed: {failed_count} of {expected_count} iterations failed"
1430
- raise RuntimeError(msg)
1431
- logger.info(
1432
- "Iterative group execution stopped early at %d of %d iterations (break signal)",
1433
- len(successful_iterations),
1434
- total_iterations,
1435
- )
1619
+ if break_occurred:
1620
+ # Early exit due to break signal - this is expected behavior
1621
+ logger.info(
1622
+ "Iterative group execution stopped early at %d of %d iterations (break signal)",
1623
+ len(successful_iterations),
1624
+ total_iterations,
1625
+ )
1626
+ else:
1627
+ # Early exit not due to break - check for failures
1628
+ # successful_iterations includes skipped iterations, but iteration_results does not
1629
+ # So we need to account for skipped iterations when checking for failures
1630
+ expected_result_count = len(successful_iterations) - skipped_count
1631
+ actual_result_count = len(iteration_results)
1632
+ if expected_result_count != actual_result_count:
1633
+ failed_count = expected_result_count - actual_result_count
1634
+ msg = f"Iterative group execution failed: {failed_count} of {len(successful_iterations)} iterations failed"
1635
+ raise RuntimeError(msg)
1436
1636
 
1437
1637
  # Build results list in iteration order
1438
1638
  node._results_list = []
@@ -1772,6 +1972,12 @@ class NodeExecutor:
1772
1972
  Returns:
1773
1973
  The upstream value if criteria met, None otherwise
1774
1974
  """
1975
+ # If upstream is a BaseIterativeNodeGroup (e.g., ForEach Group) that's currently executing,
1976
+ # we need to trace through its proxy parameter to find the actual resolved source.
1977
+ # This handles the case where: ExternalNode -> ForEachGroup.proxy_param -> InternalNode
1978
+ if isinstance(upstream_node, BaseIterativeNodeGroup) and upstream_node.state != NodeResolutionState.RESOLVED:
1979
+ return self._get_value_through_iterative_group_proxy(upstream_node, upstream_param, packaged_node_names)
1980
+
1775
1981
  if upstream_node.state != NodeResolutionState.RESOLVED:
1776
1982
  return None
1777
1983
 
@@ -1783,6 +1989,77 @@ class NodeExecutor:
1783
1989
 
1784
1990
  return upstream_node.get_parameter_value(upstream_param.name)
1785
1991
 
1992
+ def _get_value_through_iterative_group_proxy(
1993
+ self,
1994
+ iterative_group: BaseIterativeNodeGroup,
1995
+ proxy_param: Any,
1996
+ packaged_node_names: list[str],
1997
+ ) -> Any | None:
1998
+ """Trace through an iterative group's proxy parameter to get value from the actual resolved source.
1999
+
2000
+ When a packaged node inside a ForEach Group has an incoming connection from the group's
2001
+ proxy parameter, we need to find the external node that connects TO that proxy parameter
2002
+ and get the value from there.
2003
+
2004
+ Connection chain: ResolvedExternalNode -> IterativeGroup.proxy_param -> PackagedInternalNode
2005
+ We want to get the value from ResolvedExternalNode.
2006
+
2007
+ Args:
2008
+ iterative_group: The BaseIterativeNodeGroup with the proxy parameter
2009
+ proxy_param: The proxy parameter on the iterative group
2010
+ packaged_node_names: List of packaged node names to exclude
2011
+
2012
+ Returns:
2013
+ The value from the resolved external source, or None if not found
2014
+ """
2015
+ flow_manager = GriptapeNodes.FlowManager()
2016
+ connections = flow_manager.get_connections()
2017
+
2018
+ # Find the incoming connection TO the proxy parameter on the iterative group
2019
+ # This will give us the actual external source node
2020
+ incoming_to_proxy = connections.get_incoming_connections_to_parameter(iterative_group, proxy_param)
2021
+
2022
+ for conn in incoming_to_proxy:
2023
+ # Skip internal connections (from nodes inside the group)
2024
+ if conn.is_node_group_internal:
2025
+ continue
2026
+
2027
+ source_node = conn.source_node
2028
+ source_param = conn.source_parameter
2029
+
2030
+ # Skip if the source is also inside the packaged nodes
2031
+ if source_node.name in packaged_node_names:
2032
+ continue
2033
+
2034
+ # The source must be resolved for us to get its value
2035
+ if source_node.state != NodeResolutionState.RESOLVED:
2036
+ logger.debug(
2037
+ "Source node '%s' for proxy param '%s.%s' is not resolved (state: %s)",
2038
+ source_node.name,
2039
+ iterative_group.name,
2040
+ proxy_param.name,
2041
+ source_node.state,
2042
+ )
2043
+ continue
2044
+
2045
+ # Get the value from the resolved source node
2046
+ if source_param.name in source_node.parameter_output_values:
2047
+ value = source_node.parameter_output_values[source_param.name]
2048
+ else:
2049
+ value = source_node.get_parameter_value(source_param.name)
2050
+
2051
+ logger.debug(
2052
+ "Traced through proxy: %s.%s -> %s.%s (value type: %s)",
2053
+ source_node.name,
2054
+ source_param.name,
2055
+ iterative_group.name,
2056
+ proxy_param.name,
2057
+ type(value).__name__ if value is not None else "None",
2058
+ )
2059
+ return value
2060
+
2061
+ return None
2062
+
1786
2063
  def _map_to_startflow_parameter(
1787
2064
  self,
1788
2065
  packaged_node_name: str,
@@ -908,7 +908,15 @@ class DeprecationMessage(ParameterMessage):
908
908
  class ParameterGroup(BaseNodeElement, UIOptionsMixin):
909
909
  """UI element for a group of parameters."""
910
910
 
911
- def __init__(self, name: str, ui_options: dict | None = None, *, collapsed: bool = False, **kwargs):
911
+ def __init__(
912
+ self,
913
+ name: str,
914
+ ui_options: dict | None = None,
915
+ *,
916
+ collapsed: bool = False,
917
+ user_defined: bool = False,
918
+ **kwargs,
919
+ ):
912
920
  super().__init__(name=name, **kwargs)
913
921
  if ui_options is None:
914
922
  ui_options = {}
@@ -920,6 +928,7 @@ class ParameterGroup(BaseNodeElement, UIOptionsMixin):
920
928
  ui_options["collapsed"] = collapsed
921
929
 
922
930
  self._ui_options = ui_options
931
+ self.user_defined = user_defined
923
932
 
924
933
  @property
925
934
  def ui_options(self) -> dict:
@@ -1181,6 +1190,7 @@ class Parameter(BaseNodeElement, UIOptionsMixin):
1181
1190
  serializable: bool = True
1182
1191
 
1183
1192
  user_defined: bool = False
1193
+ private: bool = False
1184
1194
  _allowed_modes: set = field(
1185
1195
  default_factory=lambda: {
1186
1196
  ParameterMode.OUTPUT,
@@ -1222,6 +1232,7 @@ class Parameter(BaseNodeElement, UIOptionsMixin):
1222
1232
  settable: bool = True,
1223
1233
  serializable: bool = True,
1224
1234
  user_defined: bool = False,
1235
+ private: bool = False,
1225
1236
  element_id: str | None = None,
1226
1237
  element_type: str | None = None,
1227
1238
  parent_container_name: str | None = None,
@@ -1246,6 +1257,7 @@ class Parameter(BaseNodeElement, UIOptionsMixin):
1246
1257
  self._settable = settable
1247
1258
  self.serializable = serializable
1248
1259
  self.user_defined = user_defined
1260
+ self.private = private
1249
1261
 
1250
1262
  # Process allowed_modes - use convenience parameters if allowed_modes not explicitly set
1251
1263
  if allowed_modes is None:
@@ -1401,6 +1413,7 @@ class Parameter(BaseNodeElement, UIOptionsMixin):
1401
1413
  our_dict["is_user_defined"] = self.user_defined
1402
1414
  our_dict["settable"] = self.settable
1403
1415
  our_dict["serializable"] = self.serializable
1416
+ our_dict["private"] = self.private
1404
1417
  our_dict["ui_options"] = self.ui_options
1405
1418
 
1406
1419
  # Let's bundle up the mode details.
@@ -1838,6 +1851,7 @@ class ControlParameter(Parameter, ABC):
1838
1851
  ui_options: dict | None = None,
1839
1852
  *,
1840
1853
  user_defined: bool = False,
1854
+ private: bool = False,
1841
1855
  ):
1842
1856
  # Call parent with a few explicit tweaks.
1843
1857
  super().__init__(
@@ -1857,6 +1871,7 @@ class ControlParameter(Parameter, ABC):
1857
1871
  validators=validators,
1858
1872
  ui_options=ui_options,
1859
1873
  user_defined=user_defined,
1874
+ private=private,
1860
1875
  element_type=self.__class__.__name__,
1861
1876
  )
1862
1877
 
@@ -1875,6 +1890,7 @@ class ControlParameterInput(ControlParameter):
1875
1890
  validators: list[Callable[[Parameter, Any], None]] | None = None,
1876
1891
  *,
1877
1892
  user_defined: bool = False,
1893
+ private: bool = False,
1878
1894
  ):
1879
1895
  allowed_modes = {ParameterMode.INPUT}
1880
1896
  input_types = [ParameterTypeBuiltin.CONTROL_TYPE.value]
@@ -1899,6 +1915,7 @@ class ControlParameterInput(ControlParameter):
1899
1915
  validators=validators,
1900
1916
  ui_options=ui_options,
1901
1917
  user_defined=user_defined,
1918
+ private=private,
1902
1919
  )
1903
1920
 
1904
1921
 
@@ -1916,6 +1933,7 @@ class ControlParameterOutput(ControlParameter):
1916
1933
  validators: list[Callable[[Parameter, Any], None]] | None = None,
1917
1934
  *,
1918
1935
  user_defined: bool = False,
1936
+ private: bool = False,
1919
1937
  ):
1920
1938
  allowed_modes = {ParameterMode.OUTPUT}
1921
1939
  output_type = ParameterTypeBuiltin.CONTROL_TYPE.value
@@ -1940,6 +1958,7 @@ class ControlParameterOutput(ControlParameter):
1940
1958
  validators=validators,
1941
1959
  ui_options=ui_options,
1942
1960
  user_defined=user_defined,
1961
+ private=private,
1943
1962
  )
1944
1963
 
1945
1964
 
@@ -1970,6 +1989,7 @@ class ParameterContainer(Parameter, ABC):
1970
1989
  hide: bool | None = None,
1971
1990
  settable: bool = True,
1972
1991
  user_defined: bool = False,
1992
+ private: bool = False,
1973
1993
  element_id: str | None = None,
1974
1994
  element_type: str | None = None,
1975
1995
  ):
@@ -1991,6 +2011,7 @@ class ParameterContainer(Parameter, ABC):
1991
2011
  hide=hide,
1992
2012
  settable=settable,
1993
2013
  user_defined=user_defined,
2014
+ private=private,
1994
2015
  element_id=element_id,
1995
2016
  element_type=element_type,
1996
2017
  )
@@ -2040,6 +2061,7 @@ class ParameterList(ParameterContainer):
2040
2061
  hide: bool | None = None,
2041
2062
  settable: bool = True,
2042
2063
  user_defined: bool = False,
2064
+ private: bool = False,
2043
2065
  element_id: str | None = None,
2044
2066
  element_type: str | None = None,
2045
2067
  max_items: int | None = None,
@@ -2080,6 +2102,7 @@ class ParameterList(ParameterContainer):
2080
2102
  hide=hide,
2081
2103
  settable=settable,
2082
2104
  user_defined=user_defined,
2105
+ private=private,
2083
2106
  element_id=element_id,
2084
2107
  element_type=element_type,
2085
2108
  )
@@ -2418,6 +2441,7 @@ class ParameterKeyValuePair(Parameter):
2418
2441
  *,
2419
2442
  settable: bool = True,
2420
2443
  user_defined: bool = False,
2444
+ private: bool = False,
2421
2445
  element_id: str | None = None,
2422
2446
  element_type: str | None = None,
2423
2447
  ):
@@ -2437,6 +2461,7 @@ class ParameterKeyValuePair(Parameter):
2437
2461
  validators=validators,
2438
2462
  settable=settable,
2439
2463
  user_defined=user_defined,
2464
+ private=private,
2440
2465
  element_id=element_id,
2441
2466
  element_type=element_type,
2442
2467
  )
@@ -2538,6 +2563,7 @@ class ParameterDictionary(ParameterContainer):
2538
2563
  *,
2539
2564
  settable: bool = True,
2540
2565
  user_defined: bool = False,
2566
+ private: bool = False,
2541
2567
  element_id: str | None = None,
2542
2568
  element_type: str | None = None,
2543
2569
  ):
@@ -2557,6 +2583,7 @@ class ParameterDictionary(ParameterContainer):
2557
2583
  validators=validators,
2558
2584
  settable=settable,
2559
2585
  user_defined=user_defined,
2586
+ private=private,
2560
2587
  element_id=element_id,
2561
2588
  element_type=element_type,
2562
2589
  )
@@ -1,7 +1,7 @@
1
1
  """Node group implementations for managing collections of nodes."""
2
2
 
3
- from .base_iterative_node_group import BaseIterativeNodeGroup
3
+ from .base_iterative_node_group import BaseIterativeNodeGroup, IterationControlParam
4
4
  from .base_node_group import BaseNodeGroup
5
5
  from .subflow_node_group import SubflowNodeGroup
6
6
 
7
- __all__ = ["BaseIterativeNodeGroup", "BaseNodeGroup", "SubflowNodeGroup"]
7
+ __all__ = ["BaseIterativeNodeGroup", "BaseNodeGroup", "IterationControlParam", "SubflowNodeGroup"]