griptape-nodes 0.65.5__py3-none-any.whl → 0.66.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 (60) hide show
  1. griptape_nodes/common/node_executor.py +352 -27
  2. griptape_nodes/drivers/storage/base_storage_driver.py +12 -3
  3. griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +18 -2
  4. griptape_nodes/drivers/storage/local_storage_driver.py +42 -5
  5. griptape_nodes/exe_types/base_iterative_nodes.py +0 -1
  6. griptape_nodes/exe_types/connections.py +42 -0
  7. griptape_nodes/exe_types/core_types.py +2 -2
  8. griptape_nodes/exe_types/node_groups/__init__.py +2 -1
  9. griptape_nodes/exe_types/node_groups/base_iterative_node_group.py +177 -0
  10. griptape_nodes/exe_types/node_groups/base_node_group.py +1 -0
  11. griptape_nodes/exe_types/node_groups/subflow_node_group.py +35 -2
  12. griptape_nodes/exe_types/param_types/parameter_audio.py +1 -1
  13. griptape_nodes/exe_types/param_types/parameter_bool.py +1 -1
  14. griptape_nodes/exe_types/param_types/parameter_button.py +1 -1
  15. griptape_nodes/exe_types/param_types/parameter_float.py +1 -1
  16. griptape_nodes/exe_types/param_types/parameter_image.py +1 -1
  17. griptape_nodes/exe_types/param_types/parameter_int.py +1 -1
  18. griptape_nodes/exe_types/param_types/parameter_number.py +1 -1
  19. griptape_nodes/exe_types/param_types/parameter_string.py +1 -1
  20. griptape_nodes/exe_types/param_types/parameter_three_d.py +1 -1
  21. griptape_nodes/exe_types/param_types/parameter_video.py +1 -1
  22. griptape_nodes/machines/control_flow.py +5 -4
  23. griptape_nodes/machines/dag_builder.py +121 -55
  24. griptape_nodes/machines/fsm.py +10 -0
  25. griptape_nodes/machines/parallel_resolution.py +39 -38
  26. griptape_nodes/machines/sequential_resolution.py +29 -3
  27. griptape_nodes/node_library/library_registry.py +41 -2
  28. griptape_nodes/retained_mode/events/library_events.py +147 -8
  29. griptape_nodes/retained_mode/events/os_events.py +12 -4
  30. griptape_nodes/retained_mode/managers/fitness_problems/libraries/__init__.py +2 -0
  31. griptape_nodes/retained_mode/managers/fitness_problems/libraries/incompatible_requirements_problem.py +34 -0
  32. griptape_nodes/retained_mode/managers/flow_manager.py +133 -20
  33. griptape_nodes/retained_mode/managers/library_manager.py +1324 -564
  34. griptape_nodes/retained_mode/managers/node_manager.py +9 -3
  35. griptape_nodes/retained_mode/managers/os_manager.py +429 -65
  36. griptape_nodes/retained_mode/managers/resource_types/compute_resource.py +82 -0
  37. griptape_nodes/retained_mode/managers/resource_types/os_resource.py +17 -0
  38. griptape_nodes/retained_mode/managers/static_files_manager.py +21 -8
  39. griptape_nodes/retained_mode/managers/version_compatibility_manager.py +3 -3
  40. griptape_nodes/version_compatibility/versions/v0_39_0/modified_parameters_set_removal.py +5 -5
  41. griptape_nodes/version_compatibility/versions/v0_65_4/__init__.py +5 -0
  42. griptape_nodes/version_compatibility/versions/v0_65_4/run_in_parallel_to_run_in_order.py +79 -0
  43. griptape_nodes/version_compatibility/versions/v0_65_5/__init__.py +5 -0
  44. griptape_nodes/version_compatibility/versions/v0_65_5/flux_2_removed_parameters.py +85 -0
  45. {griptape_nodes-0.65.5.dist-info → griptape_nodes-0.66.0.dist-info}/METADATA +1 -1
  46. {griptape_nodes-0.65.5.dist-info → griptape_nodes-0.66.0.dist-info}/RECORD +48 -53
  47. griptape_nodes/retained_mode/managers/library_lifecycle/__init__.py +0 -45
  48. griptape_nodes/retained_mode/managers/library_lifecycle/data_models.py +0 -191
  49. griptape_nodes/retained_mode/managers/library_lifecycle/library_directory.py +0 -346
  50. griptape_nodes/retained_mode/managers/library_lifecycle/library_fsm.py +0 -439
  51. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/__init__.py +0 -17
  52. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/base.py +0 -82
  53. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/github.py +0 -116
  54. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/local_file.py +0 -367
  55. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/package.py +0 -104
  56. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/sandbox.py +0 -155
  57. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance.py +0 -18
  58. griptape_nodes/retained_mode/managers/library_lifecycle/library_status.py +0 -12
  59. {griptape_nodes-0.65.5.dist-info → griptape_nodes-0.66.0.dist-info}/WHEEL +0 -0
  60. {griptape_nodes-0.65.5.dist-info → griptape_nodes-0.66.0.dist-info}/entry_points.txt +0 -0
@@ -6,6 +6,8 @@ from urllib.parse import urljoin
6
6
  import httpx
7
7
 
8
8
  from griptape_nodes.drivers.storage.base_storage_driver import BaseStorageDriver, CreateSignedUploadUrlResponse
9
+ from griptape_nodes.retained_mode.events.os_events import ExistingFilePolicy, WriteFileRequest
10
+ from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
9
11
 
10
12
  logger = logging.getLogger("griptape_nodes")
11
13
 
@@ -38,24 +40,59 @@ class LocalStorageDriver(BaseStorageDriver):
38
40
  else:
39
41
  self.base_url = base_url
40
42
 
41
- def create_signed_upload_url(self, path: Path) -> CreateSignedUploadUrlResponse:
43
+ def create_signed_upload_url(
44
+ self, path: Path, existing_file_policy: ExistingFilePolicy = ExistingFilePolicy.OVERWRITE
45
+ ) -> CreateSignedUploadUrlResponse:
46
+ # on_write_file_request seems to work most reliably with an absolute path.
47
+ absolute_path = path if path.is_absolute() else self.workspace_directory / path
48
+
49
+ # Always delegate to OSManager for file path resolution and policy handling.
50
+ # Creating an empty file before the upload url gives us a chance to claim ownership
51
+ # of that particular file when creating the upload url. The file policy is not
52
+ # checked when actually uploading the file, it will always overwrite.
53
+ os_manager = GriptapeNodes.OSManager()
54
+ write_request = WriteFileRequest(
55
+ file_path=str(absolute_path),
56
+ content=b"", # Empty content for URL generation
57
+ existing_file_policy=existing_file_policy,
58
+ )
59
+ result = os_manager.on_write_file_request(write_request)
60
+
61
+ if not result.succeeded():
62
+ msg = f"WriteFileRequest failed: {result.result_details}"
63
+ raise FileExistsError(msg)
64
+
65
+ # Use the resolved filename from OSManager
66
+ # Type checker: result is WriteFileResultSuccess when succeeded() is True
67
+ resolved_path = Path(result.final_file_path) # type: ignore[attr-defined]
68
+
69
+ # WriteFileRequest always returns an absolute path, but the url needs
70
+ # to be workspace relative if passed in path was workspace relative.
71
+ if not path.is_absolute():
72
+ resolved_path = resolved_path.relative_to(self.workspace_directory)
73
+
42
74
  static_url = urljoin(self.base_url, "/static-upload-urls")
43
75
  try:
44
- response = httpx.post(static_url, json={"file_path": str(path)})
76
+ response = httpx.post(static_url, json={"file_path": str(resolved_path)})
45
77
  response.raise_for_status()
46
78
  except httpx.HTTPStatusError as e:
47
- msg = f"Failed to create upload URL for file {path}: {e}"
79
+ msg = f"Failed to create upload URL for file {resolved_path}: {e}"
48
80
  logger.error(msg)
49
81
  raise RuntimeError(msg) from e
50
82
 
51
83
  response_data = response.json()
52
84
  url = response_data.get("url")
53
85
  if url is None:
54
- msg = f"Failed to get upload URL for file {path}: {response_data}"
86
+ msg = f"Failed to get upload URL for file {resolved_path}: {response_data}"
55
87
  logger.error(msg)
56
88
  raise ValueError(msg)
57
89
 
58
- return {"url": url, "headers": response_data.get("headers", {}), "method": "PUT"}
90
+ return {
91
+ "url": url,
92
+ "headers": response_data.get("headers", {}),
93
+ "method": "PUT",
94
+ "file_path": str(resolved_path),
95
+ }
59
96
 
60
97
  def create_signed_download_url(self, path: Path) -> str:
61
98
  # The base_url already includes the /static path, so just append the path
@@ -108,7 +108,6 @@ class BaseIterativeStartNode(BaseNode):
108
108
  _current_iteration_count: int
109
109
  _total_iterations: int
110
110
  _flow: ControlFlow | None = None
111
- is_parallel: bool = False # Sequential by default
112
111
 
113
112
  def __init__(self, name: str, metadata: dict[Any, Any] | None = None) -> None:
114
113
  super().__init__(name, metadata)
@@ -403,3 +403,45 @@ class Connections:
403
403
  for conn_id in connection_ids:
404
404
  connections.append(self.connections[conn_id]) # noqa: PERF401, Keeping loop for understanding.
405
405
  return connections
406
+
407
+ def is_node_in_forward_control_path(
408
+ self, start_node: BaseNode, target_node: BaseNode, visited: set[str] | None = None
409
+ ) -> bool:
410
+ """Check if target_node is reachable from start_node through control flow connections.
411
+
412
+ Args:
413
+ start_node: The node to start traversal from
414
+ target_node: The node to check if reachable
415
+ visited: Set of already visited node names (used internally for recursion)
416
+
417
+ Returns:
418
+ True if target_node is in the forward control path from start_node, False otherwise
419
+ """
420
+ if visited is None:
421
+ visited = set()
422
+
423
+ if start_node.name in visited:
424
+ return False
425
+ visited.add(start_node.name)
426
+
427
+ # Check ALL outgoing control connections
428
+ # This handles IfElse nodes that have multiple possible control outputs
429
+ if start_node.name in self.outgoing_index:
430
+ for param_name, connection_ids in self.outgoing_index[start_node.name].items():
431
+ # Find the parameter to check if it's a control type
432
+ param = start_node.get_parameter_by_name(param_name)
433
+ if param and param.output_type == ParameterTypeBuiltin.CONTROL_TYPE.value:
434
+ # This is a control parameter - check all its connections
435
+ for connection_id in connection_ids:
436
+ if connection_id in self.connections:
437
+ connection = self.connections[connection_id]
438
+ next_node = connection.target_node
439
+
440
+ if next_node.name == target_node.name:
441
+ return True
442
+
443
+ # Recursively check the forward path
444
+ if self.is_node_in_forward_control_path(next_node, target_node, visited):
445
+ return True
446
+
447
+ return False
@@ -1967,7 +1967,7 @@ class ParameterContainer(Parameter, ABC):
1967
1967
  converters: list[Callable[[Any], Any]] | None = None,
1968
1968
  validators: list[Callable[[Parameter, Any], None]] | None = None,
1969
1969
  *,
1970
- hide: bool = False,
1970
+ hide: bool | None = None,
1971
1971
  settable: bool = True,
1972
1972
  user_defined: bool = False,
1973
1973
  element_id: str | None = None,
@@ -2037,7 +2037,7 @@ class ParameterList(ParameterContainer):
2037
2037
  converters: list[Callable[[Any], Any]] | None = None,
2038
2038
  validators: list[Callable[[Parameter, Any], None]] | None = None,
2039
2039
  *,
2040
- hide: bool = False,
2040
+ hide: bool | None = None,
2041
2041
  settable: bool = True,
2042
2042
  user_defined: bool = False,
2043
2043
  element_id: str | None = None,
@@ -1,6 +1,7 @@
1
1
  """Node group implementations for managing collections of nodes."""
2
2
 
3
+ from .base_iterative_node_group import BaseIterativeNodeGroup
3
4
  from .base_node_group import BaseNodeGroup
4
5
  from .subflow_node_group import SubflowNodeGroup
5
6
 
6
- __all__ = ["BaseNodeGroup", "SubflowNodeGroup"]
7
+ __all__ = ["BaseIterativeNodeGroup", "BaseNodeGroup", "SubflowNodeGroup"]
@@ -0,0 +1,177 @@
1
+ """Base class for iterative node groups (ForEach, ForLoop, etc.)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from abc import abstractmethod
7
+ from typing import Any
8
+
9
+ from griptape_nodes.exe_types.core_types import (
10
+ Parameter,
11
+ ParameterMode,
12
+ ParameterTypeBuiltin,
13
+ )
14
+ from griptape_nodes.exe_types.node_groups.subflow_node_group import SubflowNodeGroup
15
+
16
+ logger = logging.getLogger("griptape_nodes")
17
+
18
+
19
+ class BaseIterativeNodeGroup(SubflowNodeGroup):
20
+ """Base class for iterative node groups (ForEach, ForLoop, etc.).
21
+
22
+ Combines the functionality of BaseIterativeStartNode and BaseIterativeEndNode
23
+ into a single group node that encapsulates the loop body as child nodes.
24
+
25
+ This provides a simpler user experience than separate start/end nodes while
26
+ maintaining the same execution capabilities (sequential/parallel, local/private/cloud).
27
+
28
+ The NodeExecutor detects instances of this class and handles iteration execution
29
+ via handle_iterative_group_execution(), similar to how it handles BaseIterativeEndNode.
30
+
31
+ Subclasses must implement:
32
+ - _get_iteration_items(): Return the list of items to iterate over
33
+ - _get_current_item_value(iteration_index): Get the value for current iteration
34
+ """
35
+
36
+ # Iteration state
37
+ _items: list[Any]
38
+ _current_iteration_count: int
39
+ _total_iterations: int
40
+ is_parallel: bool
41
+
42
+ # Results storage
43
+ _results_list: list[Any]
44
+
45
+ def __init__(
46
+ self,
47
+ name: str,
48
+ metadata: dict[Any, Any] | None = None,
49
+ ) -> None:
50
+ super().__init__(name, metadata)
51
+
52
+ # Initialize iteration state
53
+ self._items = []
54
+ self._current_iteration_count = 0
55
+ self._total_iterations = 0
56
+ self.is_parallel = False
57
+ self._results_list = []
58
+
59
+ # Add parallel execution control parameter
60
+ self.run_in_order = Parameter(
61
+ name="run_in_order",
62
+ tooltip="Execute all iterations in order or concurrently",
63
+ type=ParameterTypeBuiltin.BOOL.value,
64
+ allowed_modes={ParameterMode.PROPERTY},
65
+ default_value=True,
66
+ ui_options={"display_name": "Run in Order"},
67
+ )
68
+ self.add_parameter(self.run_in_order)
69
+
70
+ # Index parameter - available in all iterative nodes (left side - feeds into group)
71
+ self.index_param = Parameter(
72
+ name="index",
73
+ tooltip="Current index of the iteration",
74
+ type=ParameterTypeBuiltin.INT.value,
75
+ allowed_modes={ParameterMode.OUTPUT},
76
+ settable=False,
77
+ default_value=0,
78
+ )
79
+ self.add_parameter(self.index_param)
80
+
81
+ # Track left parameters for UI layout
82
+ if "left_parameters" not in self.metadata:
83
+ self.metadata["left_parameters"] = []
84
+ self.metadata["left_parameters"].append("index")
85
+
86
+ # Results collection parameters (right side - collects from group)
87
+ self.new_item_to_add = Parameter(
88
+ name="new_item_to_add",
89
+ tooltip="Item to add to results list for each iteration",
90
+ type=ParameterTypeBuiltin.ANY.value,
91
+ allowed_modes={ParameterMode.INPUT},
92
+ )
93
+ self.add_parameter(self.new_item_to_add)
94
+
95
+ self.results = Parameter(
96
+ name="results",
97
+ tooltip="Collected results from all iterations",
98
+ output_type="list",
99
+ allowed_modes={ParameterMode.OUTPUT},
100
+ )
101
+ self.add_parameter(self.results)
102
+
103
+ # Track right parameters for UI layout
104
+ if "right_parameters" not in self.metadata:
105
+ self.metadata["right_parameters"] = []
106
+ self.metadata["right_parameters"].extend(["new_item_to_add", "results"])
107
+
108
+ def after_value_set(self, parameter: Parameter, value: Any) -> None:
109
+ """Handle parameter value changes."""
110
+ super().after_value_set(parameter, value)
111
+ if parameter == self.run_in_order:
112
+ self.is_parallel = not value
113
+
114
+ @abstractmethod
115
+ def _get_iteration_items(self) -> list[Any]:
116
+ """Get the list of items to iterate over.
117
+
118
+ Returns:
119
+ List of items for iteration. Empty list if no items.
120
+ """
121
+
122
+ @abstractmethod
123
+ def _get_current_item_value(self, iteration_index: int) -> Any:
124
+ """Get the value for a specific iteration.
125
+
126
+ Args:
127
+ iteration_index: 0-based iteration index
128
+
129
+ Returns:
130
+ The value to use for this iteration
131
+ """
132
+
133
+ def _initialize_iteration_data(self) -> None:
134
+ """Initialize iteration-specific data and state."""
135
+ self._items = self._get_iteration_items()
136
+ self._total_iterations = len(self._items) if self._items else 0
137
+ self._current_iteration_count = 0
138
+ self._results_list = []
139
+
140
+ def _get_total_iterations(self) -> int:
141
+ """Return the total number of iterations for this loop."""
142
+ return self._total_iterations
143
+
144
+ def get_all_iteration_values(self) -> list[int]:
145
+ """Calculate and return all iteration index values.
146
+
147
+ For ForEach nodes, this returns indices 0, 1, 2, ...
148
+ For ForLoop nodes, this could return actual loop values.
149
+
150
+ Returns:
151
+ List of integer values for each iteration
152
+ """
153
+ return list(range(self._get_total_iterations()))
154
+
155
+ def _output_results_list(self) -> None:
156
+ """Output the current results list to the results parameter."""
157
+ import copy
158
+
159
+ self.parameter_output_values["results"] = copy.deepcopy(self._results_list)
160
+
161
+ def reset_for_workflow_run(self) -> None:
162
+ """Reset state for a fresh workflow run."""
163
+ self._results_list = []
164
+ self._current_iteration_count = 0
165
+ self._total_iterations = 0
166
+ self._output_results_list()
167
+
168
+ async def aprocess(self) -> None:
169
+ """Execute the iterative node group.
170
+
171
+ Note: This method is typically not called directly. The NodeExecutor
172
+ detects BaseIterativeNodeGroup instances and calls handle_iterative_group_execution()
173
+ instead. This implementation exists as a fallback for direct local execution.
174
+ """
175
+ # For direct local execution (when NodeExecutor doesn't intercept),
176
+ # just execute the subflow once. The NodeExecutor handles iteration logic.
177
+ await self.execute_subflow()
@@ -29,3 +29,4 @@ class BaseNodeGroup(BaseNode):
29
29
  super().__init__(name, metadata)
30
30
  self.nodes = {}
31
31
  self.metadata["is_node_group"] = True
32
+ self.metadata["executable"] = False
@@ -14,6 +14,7 @@ from griptape_nodes.exe_types.core_types import (
14
14
  from griptape_nodes.exe_types.node_groups.base_node_group import BaseNodeGroup
15
15
  from griptape_nodes.exe_types.node_types import (
16
16
  LOCAL_EXECUTION,
17
+ PRIVATE_EXECUTION,
17
18
  get_library_names_with_publish_handlers,
18
19
  )
19
20
  from griptape_nodes.retained_mode.events.connection_events import (
@@ -76,6 +77,7 @@ class SubflowNodeGroup(BaseNodeGroup, ABC):
76
77
  "start_flow_node": "StartFlow",
77
78
  "parameter_names": {},
78
79
  }
80
+ self.metadata["executable"] = True
79
81
 
80
82
  # Don't create subflow in __init__ - it will be created on-demand when nodes are added
81
83
  # or restored during deserialization
@@ -298,6 +300,26 @@ class SubflowNodeGroup(BaseNodeGroup, ABC):
298
300
 
299
301
  return proxy_param
300
302
 
303
+ def add_parameter_to_group_settings(self, parameter: Parameter) -> None:
304
+ """Add a parameter to the Group settings panel.
305
+
306
+ Args:
307
+ parameter: The parameter to add to settings
308
+ """
309
+ if ParameterMode.PROPERTY not in parameter.allowed_modes:
310
+ msg = f"Parameter '{parameter.name}' must allow PROPERTY mode to be added to settings."
311
+ raise ValueError(msg)
312
+
313
+ execution_environment: dict = self.metadata.get("execution_environment", {})
314
+ if LOCAL_EXECUTION not in execution_environment:
315
+ execution_environment[LOCAL_EXECUTION] = {"parameter_names": []}
316
+ if PRIVATE_EXECUTION not in execution_environment:
317
+ execution_environment[PRIVATE_EXECUTION] = {"parameter_names": []}
318
+
319
+ for library in execution_environment:
320
+ parameter_names = self.metadata["execution_environment"][library].get("parameter_names", [])
321
+ self.metadata["execution_environment"][library]["parameter_names"] = [parameter.name, *parameter_names]
322
+
301
323
  def get_all_nodes(self) -> dict[str, BaseNode]:
302
324
  all_nodes = {}
303
325
  for node_name, node in self.nodes.items():
@@ -945,15 +967,26 @@ class SubflowNodeGroup(BaseNodeGroup, ABC):
945
967
 
946
968
  Can be called by concrete subclasses in their aprocess() implementation.
947
969
  """
948
- from griptape_nodes.retained_mode.events.execution_events import StartLocalSubflowRequest
970
+ from griptape_nodes.retained_mode.events.execution_events import (
971
+ StartLocalSubflowRequest,
972
+ StartLocalSubflowResultFailure,
973
+ )
949
974
  from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
950
975
 
951
976
  subflow = self.metadata.get("subflow_name")
952
977
  if subflow is not None and isinstance(subflow, str):
953
- await GriptapeNodes.FlowManager().on_start_local_subflow_request(
978
+ result = await GriptapeNodes.FlowManager().on_start_local_subflow_request(
954
979
  StartLocalSubflowRequest(flow_name=subflow)
955
980
  )
956
981
 
982
+ if isinstance(result, StartLocalSubflowResultFailure):
983
+ logger.error("%s: %s", self.name, result.result_details)
984
+ # Clear partial outputs to prevent inconsistent state
985
+ self.parameter_output_values.clear()
986
+ # Re-raise the error message directly without wrapping
987
+ msg = result.result_details
988
+ raise RuntimeError(msg)
989
+
957
990
  # After subflow execution, collect output values from internal nodes
958
991
  # and set them on the NodeGroup's output (right) proxy parameters
959
992
  connections = GriptapeNodes.FlowManager().get_connections()
@@ -46,7 +46,7 @@ class ParameterAudio(Parameter):
46
46
  microphone_capture_audio: bool = False,
47
47
  edit_audio: bool = False,
48
48
  accept_any: bool = True,
49
- hide: bool = False,
49
+ hide: bool | None = None,
50
50
  hide_label: bool = False,
51
51
  hide_property: bool = False,
52
52
  allow_input: bool = True,
@@ -42,7 +42,7 @@ class ParameterBool(Parameter):
42
42
  on_label: str | None = None,
43
43
  off_label: str | None = None,
44
44
  accept_any: bool = True,
45
- hide: bool = False,
45
+ hide: bool | None = None,
46
46
  hide_label: bool = False,
47
47
  hide_property: bool = False,
48
48
  allow_input: bool = True,
@@ -88,7 +88,7 @@ class ParameterButton(Parameter):
88
88
  on_click: Button.OnClickCallback | None = None,
89
89
  get_button_state: Button.GetButtonStateCallback | None = None,
90
90
  href: str | None = None,
91
- hide: bool = False,
91
+ hide: bool | None = None,
92
92
  hide_label: bool = False,
93
93
  hide_property: bool = False,
94
94
  allow_input: bool = False,
@@ -47,7 +47,7 @@ class ParameterFloat(ParameterNumber):
47
47
  max_val: float = 100,
48
48
  validate_min_max: bool = False,
49
49
  accept_any: bool = True,
50
- hide: bool = False,
50
+ hide: bool | None = None,
51
51
  hide_label: bool = False,
52
52
  hide_property: bool = False,
53
53
  allow_input: bool = True,
@@ -46,7 +46,7 @@ class ParameterImage(Parameter):
46
46
  webcam_capture_image: bool = False,
47
47
  edit_mask: bool = False,
48
48
  accept_any: bool = True,
49
- hide: bool = False,
49
+ hide: bool | None = None,
50
50
  hide_label: bool = False,
51
51
  hide_property: bool = False,
52
52
  allow_input: bool = True,
@@ -47,7 +47,7 @@ class ParameterInt(ParameterNumber):
47
47
  max_val: float = 100,
48
48
  validate_min_max: bool = False,
49
49
  accept_any: bool = True,
50
- hide: bool = False,
50
+ hide: bool | None = None,
51
51
  hide_label: bool = False,
52
52
  hide_property: bool = False,
53
53
  allow_input: bool = True,
@@ -41,7 +41,7 @@ class ParameterNumber(Parameter):
41
41
  max_val: float = 100,
42
42
  validate_min_max: bool = False,
43
43
  accept_any: bool = True,
44
- hide: bool = False,
44
+ hide: bool | None = None,
45
45
  hide_label: bool = False,
46
46
  hide_property: bool = False,
47
47
  allow_input: bool = True,
@@ -46,7 +46,7 @@ class ParameterString(Parameter):
46
46
  placeholder_text: str | None = None,
47
47
  is_full_width: bool = False,
48
48
  accept_any: bool = True,
49
- hide: bool = False,
49
+ hide: bool | None = None,
50
50
  hide_label: bool = False,
51
51
  hide_property: bool = False,
52
52
  allow_input: bool = True,
@@ -44,7 +44,7 @@ class Parameter3D(Parameter):
44
44
  clickable_file_browser: bool = True,
45
45
  expander: bool = False,
46
46
  accept_any: bool = True,
47
- hide: bool = False,
47
+ hide: bool | None = None,
48
48
  hide_label: bool = False,
49
49
  hide_property: bool = False,
50
50
  allow_input: bool = True,
@@ -46,7 +46,7 @@ class ParameterVideo(Parameter):
46
46
  webcam_capture_video: bool = False,
47
47
  edit_video: bool = False,
48
48
  accept_any: bool = True,
49
- hide: bool = False,
49
+ hide: bool | None = None,
50
50
  hide_label: bool = False,
51
51
  hide_property: bool = False,
52
52
  allow_input: bool = True,
@@ -480,14 +480,15 @@ class ControlFlowMachine(FSM[ControlFlowContext]):
480
480
  # Figure out which graph the data node belongs to, if it belongs to a graph.
481
481
  for graph_start_node_name in dag_builder.graphs:
482
482
  graph_start_node = node_manager.get_node_by_name(graph_start_node_name)
483
- correct_graph = flow_manager.is_node_connected(graph_start_node, node)
483
+ # Get boundary nodes (empty list if not connected)
484
+ boundary_nodes = flow_manager.is_node_connected(graph_start_node, node)
484
485
  # This means this node is in the downstream connection of one of this graph.
485
- if correct_graph:
486
+ if boundary_nodes:
486
487
  # Is the node connected to a graph?
487
488
  disconnected = False
488
489
  if node.name not in dag_builder.start_node_candidates:
489
- dag_builder.start_node_candidates[node.name] = set()
490
- dag_builder.start_node_candidates[node.name].add(graph_start_node_name)
490
+ dag_builder.start_node_candidates[node.name] = {}
491
+ dag_builder.start_node_candidates[node.name][graph_start_node_name] = set(boundary_nodes)
491
492
  if disconnected:
492
493
  # If the node is not connected to any graph, we can add it as it's own graph here.
493
494
  # It will not cause any overlapping confusion with existing graphs.