griptape-nodes 0.64.10__py3-none-any.whl → 0.65.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.
- griptape_nodes/app/app.py +25 -5
- griptape_nodes/cli/commands/init.py +65 -54
- griptape_nodes/cli/commands/libraries.py +92 -85
- griptape_nodes/cli/commands/self.py +121 -0
- griptape_nodes/common/node_executor.py +2142 -101
- griptape_nodes/exe_types/base_iterative_nodes.py +1004 -0
- griptape_nodes/exe_types/connections.py +114 -19
- griptape_nodes/exe_types/core_types.py +225 -7
- griptape_nodes/exe_types/flow.py +3 -3
- griptape_nodes/exe_types/node_types.py +681 -225
- griptape_nodes/exe_types/param_components/README.md +414 -0
- griptape_nodes/exe_types/param_components/api_key_provider_parameter.py +200 -0
- griptape_nodes/exe_types/param_components/huggingface/huggingface_model_parameter.py +2 -0
- griptape_nodes/exe_types/param_components/huggingface/huggingface_repo_file_parameter.py +79 -5
- griptape_nodes/exe_types/param_types/parameter_button.py +443 -0
- griptape_nodes/machines/control_flow.py +77 -38
- griptape_nodes/machines/dag_builder.py +148 -70
- griptape_nodes/machines/parallel_resolution.py +61 -35
- griptape_nodes/machines/sequential_resolution.py +11 -113
- griptape_nodes/retained_mode/events/app_events.py +1 -0
- griptape_nodes/retained_mode/events/base_events.py +16 -13
- griptape_nodes/retained_mode/events/connection_events.py +3 -0
- griptape_nodes/retained_mode/events/execution_events.py +35 -0
- griptape_nodes/retained_mode/events/flow_events.py +15 -2
- griptape_nodes/retained_mode/events/library_events.py +347 -0
- griptape_nodes/retained_mode/events/node_events.py +48 -0
- griptape_nodes/retained_mode/events/os_events.py +86 -3
- griptape_nodes/retained_mode/events/project_events.py +15 -1
- griptape_nodes/retained_mode/events/workflow_events.py +48 -1
- griptape_nodes/retained_mode/griptape_nodes.py +6 -2
- griptape_nodes/retained_mode/managers/config_manager.py +10 -8
- griptape_nodes/retained_mode/managers/event_manager.py +168 -0
- griptape_nodes/retained_mode/managers/fitness_problems/libraries/__init__.py +2 -0
- griptape_nodes/retained_mode/managers/fitness_problems/libraries/old_xdg_location_warning_problem.py +43 -0
- griptape_nodes/retained_mode/managers/flow_manager.py +664 -123
- griptape_nodes/retained_mode/managers/library_manager.py +1143 -139
- griptape_nodes/retained_mode/managers/model_manager.py +2 -3
- griptape_nodes/retained_mode/managers/node_manager.py +148 -25
- griptape_nodes/retained_mode/managers/object_manager.py +3 -1
- griptape_nodes/retained_mode/managers/operation_manager.py +3 -1
- griptape_nodes/retained_mode/managers/os_manager.py +1158 -122
- griptape_nodes/retained_mode/managers/secrets_manager.py +2 -3
- griptape_nodes/retained_mode/managers/settings.py +21 -1
- griptape_nodes/retained_mode/managers/sync_manager.py +2 -3
- griptape_nodes/retained_mode/managers/workflow_manager.py +358 -104
- griptape_nodes/retained_mode/retained_mode.py +3 -3
- griptape_nodes/traits/button.py +44 -2
- griptape_nodes/traits/file_system_picker.py +2 -2
- griptape_nodes/utils/file_utils.py +101 -0
- griptape_nodes/utils/git_utils.py +1226 -0
- griptape_nodes/utils/library_utils.py +122 -0
- {griptape_nodes-0.64.10.dist-info → griptape_nodes-0.65.0.dist-info}/METADATA +2 -1
- {griptape_nodes-0.64.10.dist-info → griptape_nodes-0.65.0.dist-info}/RECORD +55 -47
- {griptape_nodes-0.64.10.dist-info → griptape_nodes-0.65.0.dist-info}/WHEEL +1 -1
- {griptape_nodes-0.64.10.dist-info → griptape_nodes-0.65.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,1004 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import logging
|
|
3
|
+
from abc import abstractmethod
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
from typing import Any, NamedTuple
|
|
6
|
+
|
|
7
|
+
from griptape_nodes.exe_types.core_types import (
|
|
8
|
+
ControlParameterInput,
|
|
9
|
+
ControlParameterOutput,
|
|
10
|
+
Parameter,
|
|
11
|
+
ParameterGroup,
|
|
12
|
+
ParameterMessage,
|
|
13
|
+
ParameterMode,
|
|
14
|
+
ParameterTypeBuiltin,
|
|
15
|
+
)
|
|
16
|
+
from griptape_nodes.exe_types.flow import ControlFlow
|
|
17
|
+
from griptape_nodes.exe_types.node_types import BaseNode
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _outgoing_connection_exists(source_node: str, source_param: str) -> bool:
|
|
21
|
+
"""Check if a source node/parameter has any outgoing connections.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
source_node: Name of the node that would be sending the connection
|
|
25
|
+
source_param: Name of the parameter on that node
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
True if the parameter has at least one outgoing connection, False otherwise
|
|
29
|
+
|
|
30
|
+
Logic: Look in connections.outgoing_index[source_node][source_param]
|
|
31
|
+
"""
|
|
32
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
33
|
+
|
|
34
|
+
connections = GriptapeNodes.FlowManager().get_connections()
|
|
35
|
+
|
|
36
|
+
# Check if source_node has any outgoing connections at all
|
|
37
|
+
source_connections = connections.outgoing_index.get(source_node)
|
|
38
|
+
if source_connections is None:
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
# Check if source_param has any outgoing connections
|
|
42
|
+
param_connections = source_connections.get(source_param)
|
|
43
|
+
if param_connections is None:
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
# Return True if connections list is populated
|
|
47
|
+
return bool(param_connections)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _incoming_connection_exists(target_node: str, target_param: str) -> bool:
|
|
51
|
+
"""Check if a target node/parameter has any incoming connections.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
target_node: Name of the node that would be receiving the connection
|
|
55
|
+
target_param: Name of the parameter on that node
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
True if the parameter has at least one incoming connection, False otherwise
|
|
59
|
+
|
|
60
|
+
Logic: Look in connections.incoming_index[target_node][target_param]
|
|
61
|
+
"""
|
|
62
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
63
|
+
|
|
64
|
+
connections = GriptapeNodes.FlowManager().get_connections()
|
|
65
|
+
|
|
66
|
+
# Check if target_node has any incoming connections at all
|
|
67
|
+
target_connections = connections.incoming_index.get(target_node)
|
|
68
|
+
if target_connections is None:
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
# Check if target_param has any incoming connections
|
|
72
|
+
param_connections = target_connections.get(target_param)
|
|
73
|
+
if param_connections is None:
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
# Return True if connections list is populated
|
|
77
|
+
return bool(param_connections)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class StatusType(StrEnum):
|
|
81
|
+
"""Enum for iterative loop status types."""
|
|
82
|
+
|
|
83
|
+
NORMAL = "normal"
|
|
84
|
+
BREAK = "break"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class NodeParameterPair(NamedTuple):
|
|
88
|
+
"""A named tuple for storing a pair of node and parameters for connections.
|
|
89
|
+
|
|
90
|
+
Fields:
|
|
91
|
+
node: The node the parameter lives on
|
|
92
|
+
parameter: The parameter connected
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
node: BaseNode
|
|
96
|
+
parameter: Parameter
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class BaseIterativeStartNode(BaseNode):
|
|
100
|
+
"""Base class for all iterative start nodes (ForEach, ForLoop, etc.).
|
|
101
|
+
|
|
102
|
+
This class consolidates all shared signal logic, connection management,
|
|
103
|
+
state tracking, and validation logic used by iterative loop start nodes.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
end_node: "BaseIterativeEndNode | None" = None
|
|
107
|
+
exec_out: ControlParameterOutput
|
|
108
|
+
_current_iteration_count: int
|
|
109
|
+
_total_iterations: int
|
|
110
|
+
_flow: ControlFlow | None = None
|
|
111
|
+
is_parallel: bool = False # Sequential by default
|
|
112
|
+
|
|
113
|
+
def __init__(self, name: str, metadata: dict[Any, Any] | None = None) -> None:
|
|
114
|
+
super().__init__(name, metadata)
|
|
115
|
+
self._current_iteration_count = 0
|
|
116
|
+
|
|
117
|
+
# This is the total number of iterations that WILL be run (calculated during init)
|
|
118
|
+
self._total_iterations = 0
|
|
119
|
+
|
|
120
|
+
# Connection tracking for validation
|
|
121
|
+
self._connected_parameters: set[str] = set()
|
|
122
|
+
|
|
123
|
+
# Main control flow
|
|
124
|
+
self.exec_in = ControlParameterInput(tooltip="Start Loop", name="exec_in")
|
|
125
|
+
self.exec_in.ui_options = {"display_name": "Start Loop"}
|
|
126
|
+
self.add_parameter(self.exec_in)
|
|
127
|
+
|
|
128
|
+
# On Each Item control output - moved outside group for proper rendering
|
|
129
|
+
self.exec_out = ControlParameterOutput(tooltip=self._get_exec_out_tooltip(), name="exec_out")
|
|
130
|
+
self.exec_out.ui_options = {"display_name": self._get_exec_out_display_name()}
|
|
131
|
+
self.add_parameter(self.exec_out)
|
|
132
|
+
|
|
133
|
+
# Create parameter group for iteration data
|
|
134
|
+
with ParameterGroup(name=self._get_parameter_group_name()) as group:
|
|
135
|
+
# Add index parameter that all iterative nodes have
|
|
136
|
+
self.index_count = Parameter(
|
|
137
|
+
name="index",
|
|
138
|
+
tooltip="Current index of the iteration",
|
|
139
|
+
type=ParameterTypeBuiltin.INT.value,
|
|
140
|
+
allowed_modes={ParameterMode.PROPERTY, ParameterMode.OUTPUT},
|
|
141
|
+
settable=False,
|
|
142
|
+
default_value=0,
|
|
143
|
+
ui_options={"hide_property": True},
|
|
144
|
+
)
|
|
145
|
+
self.add_node_element(group)
|
|
146
|
+
|
|
147
|
+
# Explicit tethering to corresponding End node (hidden)
|
|
148
|
+
self.loop = Parameter(
|
|
149
|
+
name="loop",
|
|
150
|
+
tooltip="Connected Loop End Node",
|
|
151
|
+
output_type=ParameterTypeBuiltin.ALL.value,
|
|
152
|
+
allowed_modes={ParameterMode.OUTPUT},
|
|
153
|
+
)
|
|
154
|
+
self.loop.ui_options = {"hide": True, "display_name": "Loop End Node"}
|
|
155
|
+
self.add_parameter(self.loop)
|
|
156
|
+
|
|
157
|
+
# Hidden signal inputs from End node
|
|
158
|
+
self.trigger_next_iteration_signal = ControlParameterInput(
|
|
159
|
+
tooltip="Signal from End to continue to next iteration", name="trigger_next_iteration_signal"
|
|
160
|
+
)
|
|
161
|
+
self.trigger_next_iteration_signal.ui_options = {"hide": True, "display_name": "Next Iteration Signal"}
|
|
162
|
+
self.trigger_next_iteration_signal.settable = False
|
|
163
|
+
|
|
164
|
+
self.break_loop_signal = ControlParameterInput(
|
|
165
|
+
tooltip="Signal from End to break out of loop", name="break_loop_signal"
|
|
166
|
+
)
|
|
167
|
+
self.break_loop_signal.ui_options = {"hide": True, "display_name": "Break Loop Signal"}
|
|
168
|
+
self.break_loop_signal.settable = False
|
|
169
|
+
|
|
170
|
+
# Hidden control output - loop end condition
|
|
171
|
+
self.loop_end_condition_met_signal = ControlParameterOutput(
|
|
172
|
+
tooltip="Signal to End when loop should end", name="loop_end_condition_met_signal"
|
|
173
|
+
)
|
|
174
|
+
self.loop_end_condition_met_signal.ui_options = {"hide": True, "display_name": "Loop End Signal"}
|
|
175
|
+
self.loop_end_condition_met_signal.settable = False
|
|
176
|
+
|
|
177
|
+
# Add hidden parameters
|
|
178
|
+
self.add_parameter(self.trigger_next_iteration_signal)
|
|
179
|
+
self.add_parameter(self.break_loop_signal)
|
|
180
|
+
self.add_parameter(self.loop_end_condition_met_signal)
|
|
181
|
+
|
|
182
|
+
# Control output tracking
|
|
183
|
+
self.next_control_output: Parameter | None = None
|
|
184
|
+
self._logger = logging.getLogger(f"{__name__}.{self.name}")
|
|
185
|
+
|
|
186
|
+
# Status message parameter - moved to bottom
|
|
187
|
+
self.status_message = ParameterMessage(
|
|
188
|
+
name="status_message",
|
|
189
|
+
variant="info",
|
|
190
|
+
value="",
|
|
191
|
+
)
|
|
192
|
+
self.add_node_element(self.status_message)
|
|
193
|
+
|
|
194
|
+
# Initialize status message
|
|
195
|
+
self._update_status_message()
|
|
196
|
+
|
|
197
|
+
def _get_base_node_type_name(self) -> str:
|
|
198
|
+
"""Get the base node type name (e.g., 'ForLoop' from 'ForLoopStartNode')."""
|
|
199
|
+
return self.__class__.__name__.replace("StartNode", "")
|
|
200
|
+
|
|
201
|
+
@abstractmethod
|
|
202
|
+
def _get_compatible_end_classes(self) -> set[type]:
|
|
203
|
+
"""Return the set of End node classes that this Start node can connect to."""
|
|
204
|
+
|
|
205
|
+
@abstractmethod
|
|
206
|
+
def _get_parameter_group_name(self) -> str:
|
|
207
|
+
"""Return the name for the parameter group containing iteration data."""
|
|
208
|
+
|
|
209
|
+
@abstractmethod
|
|
210
|
+
def _get_exec_out_display_name(self) -> str:
|
|
211
|
+
"""Return the display name for the exec_out parameter."""
|
|
212
|
+
|
|
213
|
+
@abstractmethod
|
|
214
|
+
def _get_exec_out_tooltip(self) -> str:
|
|
215
|
+
"""Return the tooltip for the exec_out parameter."""
|
|
216
|
+
|
|
217
|
+
@abstractmethod
|
|
218
|
+
def _get_iteration_items(self) -> list[Any]:
|
|
219
|
+
"""Get the list of items to iterate over."""
|
|
220
|
+
|
|
221
|
+
@abstractmethod
|
|
222
|
+
def _initialize_iteration_data(self) -> None:
|
|
223
|
+
"""Initialize iteration-specific data and state."""
|
|
224
|
+
|
|
225
|
+
@abstractmethod
|
|
226
|
+
def _get_current_item_value(self) -> Any:
|
|
227
|
+
"""Get the current iteration value."""
|
|
228
|
+
|
|
229
|
+
@abstractmethod
|
|
230
|
+
def is_loop_finished(self) -> bool:
|
|
231
|
+
"""Return True if the loop has completed all iterations.
|
|
232
|
+
|
|
233
|
+
This method must be implemented by subclasses to define when
|
|
234
|
+
the loop should terminate.
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
@abstractmethod
|
|
238
|
+
def _get_total_iterations(self) -> int:
|
|
239
|
+
"""Return the total number of iterations for this loop."""
|
|
240
|
+
|
|
241
|
+
@abstractmethod
|
|
242
|
+
def _get_current_iteration_count(self) -> int:
|
|
243
|
+
"""Return the current iteration count (0-based)."""
|
|
244
|
+
|
|
245
|
+
@abstractmethod
|
|
246
|
+
def get_current_index(self) -> int:
|
|
247
|
+
"""Return the current index value for this iteration type.
|
|
248
|
+
|
|
249
|
+
For ForEach: returns array position (0, 1, 2, ...)
|
|
250
|
+
For ForLoop: returns actual loop value (start, start+step, start+2*step, ...)
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
@abstractmethod
|
|
254
|
+
def _advance_to_next_iteration(self) -> None:
|
|
255
|
+
"""Advance to the next iteration.
|
|
256
|
+
|
|
257
|
+
For ForEach: increment index by 1
|
|
258
|
+
For ForLoop: increment current value by step, increment index by 1
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
def get_all_iteration_values(self) -> list[int]:
|
|
262
|
+
"""Calculate and return all iteration values for this loop.
|
|
263
|
+
|
|
264
|
+
For ForEach nodes, this returns indices 0, 1, 2, ...
|
|
265
|
+
For ForLoop nodes, this returns actual loop values (start, start+step, start+2*step, ...).
|
|
266
|
+
|
|
267
|
+
This is used by parallel execution to set correct parameter values for each iteration.
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
List of integer values for each iteration
|
|
271
|
+
"""
|
|
272
|
+
# Default implementation for ForEach: return 0-based indices
|
|
273
|
+
return list(range(self._get_total_iterations()))
|
|
274
|
+
|
|
275
|
+
def process(self) -> None:
|
|
276
|
+
if self._flow is None:
|
|
277
|
+
return
|
|
278
|
+
|
|
279
|
+
# Handle different control entry points with direct logic
|
|
280
|
+
match self._entry_control_parameter:
|
|
281
|
+
case self.exec_in | None:
|
|
282
|
+
# Starting the loop (initialization)
|
|
283
|
+
self._initialize_loop()
|
|
284
|
+
self._check_completion_and_set_output()
|
|
285
|
+
case self.trigger_next_iteration_signal:
|
|
286
|
+
# Next iteration signal from End - advance to next iteration
|
|
287
|
+
self._advance_to_next_iteration()
|
|
288
|
+
self._check_completion_and_set_output()
|
|
289
|
+
case self.break_loop_signal:
|
|
290
|
+
# Break signal from End - halt loop immediately
|
|
291
|
+
self._complete_loop(StatusType.BREAK)
|
|
292
|
+
case _:
|
|
293
|
+
# Unexpected control entry point - log error for debugging
|
|
294
|
+
err_str = f"Iterative Start node '{self.name}' received unexpected control parameter: {self._entry_control_parameter}. "
|
|
295
|
+
"Expected: exec_in, trigger_next_iteration_signal, break_loop_signal, or None."
|
|
296
|
+
self._logger.error(err_str)
|
|
297
|
+
return
|
|
298
|
+
|
|
299
|
+
def _validate_start_node(self) -> list[Exception] | None:
|
|
300
|
+
"""Common validation logic for both workflow and node run validation."""
|
|
301
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
302
|
+
|
|
303
|
+
exceptions = []
|
|
304
|
+
|
|
305
|
+
# Validate end node connection
|
|
306
|
+
if self.end_node is None:
|
|
307
|
+
msg = f"{self.name}: End node not found or connected."
|
|
308
|
+
exceptions.append(Exception(msg))
|
|
309
|
+
|
|
310
|
+
# Validate all required connections exist
|
|
311
|
+
validation_errors = self._validate_iterative_connections()
|
|
312
|
+
if validation_errors:
|
|
313
|
+
exceptions.extend(validation_errors)
|
|
314
|
+
|
|
315
|
+
try:
|
|
316
|
+
flow = GriptapeNodes.ObjectManager().get_object_by_name(
|
|
317
|
+
GriptapeNodes.NodeManager().get_node_parent_flow_by_name(self.name)
|
|
318
|
+
)
|
|
319
|
+
if isinstance(flow, ControlFlow):
|
|
320
|
+
self._flow = flow
|
|
321
|
+
except Exception as e:
|
|
322
|
+
exceptions.append(e)
|
|
323
|
+
return exceptions
|
|
324
|
+
|
|
325
|
+
def validate_before_workflow_run(self) -> list[Exception] | None:
|
|
326
|
+
return self._validate_start_node()
|
|
327
|
+
|
|
328
|
+
def validate_before_node_run(self) -> list[Exception] | None:
|
|
329
|
+
return self._validate_start_node()
|
|
330
|
+
|
|
331
|
+
def get_next_control_output(self) -> Parameter | None:
|
|
332
|
+
return self.next_control_output
|
|
333
|
+
|
|
334
|
+
def allow_outgoing_connection(
|
|
335
|
+
self,
|
|
336
|
+
source_parameter: Parameter,
|
|
337
|
+
target_node: BaseNode,
|
|
338
|
+
target_parameter: Parameter,
|
|
339
|
+
) -> bool:
|
|
340
|
+
"""Validate outgoing connections for type safety."""
|
|
341
|
+
# Check if this is a loop tethering connection
|
|
342
|
+
if source_parameter == self.loop:
|
|
343
|
+
# Ensure target node is compatible
|
|
344
|
+
compatible_end_classes = self._get_compatible_end_classes()
|
|
345
|
+
compatible_class_names = {cls.__name__ for cls in compatible_end_classes}
|
|
346
|
+
target_class_name = target_node.__class__.__name__
|
|
347
|
+
if target_class_name not in compatible_class_names:
|
|
348
|
+
self._logger.warning(
|
|
349
|
+
"Incompatible connection: %s can only connect to %s, but attempted to connect to %s",
|
|
350
|
+
self.__class__.__name__,
|
|
351
|
+
list(compatible_class_names),
|
|
352
|
+
target_class_name,
|
|
353
|
+
)
|
|
354
|
+
return False
|
|
355
|
+
return super().allow_outgoing_connection(source_parameter, target_node, target_parameter)
|
|
356
|
+
|
|
357
|
+
def allow_incoming_connection(
|
|
358
|
+
self,
|
|
359
|
+
source_node: BaseNode,
|
|
360
|
+
source_parameter: Parameter,
|
|
361
|
+
target_parameter: Parameter,
|
|
362
|
+
) -> bool:
|
|
363
|
+
"""Validate incoming connections for type safety."""
|
|
364
|
+
# Check signal connections from End nodes
|
|
365
|
+
if target_parameter in (self.trigger_next_iteration_signal, self.break_loop_signal):
|
|
366
|
+
# Ensure source node is compatible
|
|
367
|
+
compatible_end_classes = self._get_compatible_end_classes()
|
|
368
|
+
compatible_class_names = {cls.__name__ for cls in compatible_end_classes}
|
|
369
|
+
source_class_name = source_node.__class__.__name__
|
|
370
|
+
if source_class_name not in compatible_class_names:
|
|
371
|
+
self._logger.warning(
|
|
372
|
+
"Incompatible connection: %s can only receive signals from %s, but %s attempted to connect",
|
|
373
|
+
self.__class__.__name__,
|
|
374
|
+
list(compatible_class_names),
|
|
375
|
+
source_class_name,
|
|
376
|
+
)
|
|
377
|
+
return False
|
|
378
|
+
return super().allow_incoming_connection(source_node, source_parameter, target_parameter)
|
|
379
|
+
|
|
380
|
+
def _update_status_message(self, status_type: StatusType = StatusType.NORMAL) -> None:
|
|
381
|
+
"""Update the status message parameter based on current loop state."""
|
|
382
|
+
if self._total_iterations == 0:
|
|
383
|
+
# Handle the case where loop terminates immediately without iterations
|
|
384
|
+
status = "Completed 0 (of 0)"
|
|
385
|
+
elif status_type == StatusType.BREAK:
|
|
386
|
+
status = f"Stopped at {self._current_iteration_count} (of {self._total_iterations}) - Break"
|
|
387
|
+
elif self.is_loop_finished():
|
|
388
|
+
status = f"Completed {self._total_iterations} (of {self._total_iterations})"
|
|
389
|
+
else:
|
|
390
|
+
status = f"Processing {self._current_iteration_count} (of {self._total_iterations})"
|
|
391
|
+
|
|
392
|
+
self.status_message.value = status
|
|
393
|
+
|
|
394
|
+
def _initialize_loop(self) -> None:
|
|
395
|
+
"""Initialize the loop with fresh parameter values."""
|
|
396
|
+
# Reset all state for fresh loop execution
|
|
397
|
+
self._current_iteration_count = 0
|
|
398
|
+
self.next_control_output = None
|
|
399
|
+
|
|
400
|
+
# Reset the coupled End node's state for fresh loop runs
|
|
401
|
+
if self.end_node and isinstance(self.end_node, BaseIterativeEndNode):
|
|
402
|
+
self.end_node.reset_for_workflow_run()
|
|
403
|
+
|
|
404
|
+
# Initialize iteration-specific data and set total iterations
|
|
405
|
+
self._initialize_iteration_data()
|
|
406
|
+
self._total_iterations = self._get_total_iterations()
|
|
407
|
+
|
|
408
|
+
def _check_completion_and_set_output(self) -> None:
|
|
409
|
+
"""Check if loop should end or continue and set appropriate control output."""
|
|
410
|
+
if self.is_loop_finished():
|
|
411
|
+
self._complete_loop()
|
|
412
|
+
return
|
|
413
|
+
|
|
414
|
+
# Continue with current item - unresolve future nodes for fresh evaluation
|
|
415
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
416
|
+
|
|
417
|
+
connections = GriptapeNodes.FlowManager().get_connections()
|
|
418
|
+
connections.unresolve_future_nodes(self)
|
|
419
|
+
|
|
420
|
+
# Always set the index output in base class
|
|
421
|
+
current_index = self.get_current_index()
|
|
422
|
+
self.parameter_output_values["index"] = current_index
|
|
423
|
+
self.publish_update_to_parameter("index", current_index)
|
|
424
|
+
|
|
425
|
+
# Get current item value from subclass (subclasses handle their own logic)
|
|
426
|
+
current_item_value = self._get_current_item_value()
|
|
427
|
+
if current_item_value is not None:
|
|
428
|
+
# Subclasses can handle their own current_item logic
|
|
429
|
+
pass
|
|
430
|
+
|
|
431
|
+
# Update status message and continue with execution
|
|
432
|
+
self._update_status_message()
|
|
433
|
+
self.next_control_output = self.exec_out
|
|
434
|
+
|
|
435
|
+
def _complete_loop(self, status_type: StatusType = StatusType.NORMAL) -> None:
|
|
436
|
+
"""Complete the loop and set final state."""
|
|
437
|
+
self._update_status_message(status_type)
|
|
438
|
+
self._current_iteration_count = 0
|
|
439
|
+
self._total_iterations = 0
|
|
440
|
+
self.next_control_output = self.loop_end_condition_met_signal
|
|
441
|
+
|
|
442
|
+
def _validate_iterative_connections(self) -> list[Exception]:
|
|
443
|
+
"""Validate that all required iterative connections are properly established."""
|
|
444
|
+
errors = []
|
|
445
|
+
node_type = self._get_base_node_type_name()
|
|
446
|
+
|
|
447
|
+
# Check if exec_out has outgoing connections
|
|
448
|
+
if not _outgoing_connection_exists(self.name, self.exec_out.name):
|
|
449
|
+
exec_out_display_name = self._get_exec_out_display_name()
|
|
450
|
+
errors.append(
|
|
451
|
+
Exception(
|
|
452
|
+
f"{self.name}: Missing required connection from '{exec_out_display_name}'. "
|
|
453
|
+
f"REQUIRED ACTION: Connect {node_type} Start '{exec_out_display_name}' to interior loop nodes. "
|
|
454
|
+
"The start node must connect to other nodes to execute the loop body."
|
|
455
|
+
)
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
# Check if loop has outgoing connection to End
|
|
459
|
+
if self.end_node is None:
|
|
460
|
+
errors.append(
|
|
461
|
+
Exception(
|
|
462
|
+
f"{self.name}: Missing required tethering connection. "
|
|
463
|
+
f"REQUIRED ACTION: Connect {node_type} Start 'Loop End Node' to {node_type} End 'Loop Start Node'. "
|
|
464
|
+
"This establishes the explicit relationship between start and end nodes."
|
|
465
|
+
)
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
# Check if all hidden signal connections exist (only if end_node is connected)
|
|
469
|
+
if self.end_node:
|
|
470
|
+
# Check trigger_next_iteration_signal connection
|
|
471
|
+
if not _incoming_connection_exists(self.name, self.trigger_next_iteration_signal.name):
|
|
472
|
+
errors.append(
|
|
473
|
+
Exception(
|
|
474
|
+
f"{self.name}: Missing hidden signal connection. "
|
|
475
|
+
f"REQUIRED ACTION: Connect {node_type} End 'Next Iteration Signal Output' to {node_type} Start 'Next Iteration Signal'. "
|
|
476
|
+
"This signal tells the start node to continue to the next item."
|
|
477
|
+
)
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
# Check break_loop_signal connection
|
|
481
|
+
if not _incoming_connection_exists(self.name, self.break_loop_signal.name):
|
|
482
|
+
errors.append(
|
|
483
|
+
Exception(
|
|
484
|
+
f"{self.name}: Missing hidden signal connection. "
|
|
485
|
+
f"REQUIRED ACTION: Connect {node_type} End 'Break Loop Signal Output' to {node_type} Start 'Break Loop Signal'. "
|
|
486
|
+
"This signal tells the start node to break out of the loop early."
|
|
487
|
+
)
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
return errors
|
|
491
|
+
|
|
492
|
+
def after_incoming_connection(
|
|
493
|
+
self,
|
|
494
|
+
source_node: BaseNode,
|
|
495
|
+
source_parameter: Parameter,
|
|
496
|
+
target_parameter: Parameter,
|
|
497
|
+
) -> None:
|
|
498
|
+
# Track incoming connections for validation
|
|
499
|
+
self._connected_parameters.add(target_parameter.name)
|
|
500
|
+
return super().after_incoming_connection(source_node, source_parameter, target_parameter)
|
|
501
|
+
|
|
502
|
+
def after_incoming_connection_removed(
|
|
503
|
+
self,
|
|
504
|
+
source_node: BaseNode,
|
|
505
|
+
source_parameter: Parameter,
|
|
506
|
+
target_parameter: Parameter,
|
|
507
|
+
) -> None:
|
|
508
|
+
# Remove from tracking when connection is removed
|
|
509
|
+
self._connected_parameters.discard(target_parameter.name)
|
|
510
|
+
return super().after_incoming_connection_removed(source_node, source_parameter, target_parameter)
|
|
511
|
+
|
|
512
|
+
def after_outgoing_connection(
|
|
513
|
+
self,
|
|
514
|
+
source_parameter: Parameter,
|
|
515
|
+
target_node: BaseNode,
|
|
516
|
+
target_parameter: Parameter,
|
|
517
|
+
) -> None:
|
|
518
|
+
if source_parameter == self.loop and isinstance(target_node, BaseIterativeEndNode):
|
|
519
|
+
self.end_node = target_node
|
|
520
|
+
return super().after_outgoing_connection(source_parameter, target_node, target_parameter)
|
|
521
|
+
|
|
522
|
+
def after_outgoing_connection_removed(
|
|
523
|
+
self,
|
|
524
|
+
source_parameter: Parameter,
|
|
525
|
+
target_node: BaseNode,
|
|
526
|
+
target_parameter: Parameter,
|
|
527
|
+
) -> None:
|
|
528
|
+
if source_parameter == self.loop and isinstance(target_node, BaseIterativeEndNode):
|
|
529
|
+
self.end_node = None
|
|
530
|
+
return super().after_outgoing_connection_removed(source_parameter, target_node, target_parameter)
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
class BaseIterativeEndNode(BaseNode):
|
|
534
|
+
"""Base class for all iterative end nodes (ForEach, ForLoop, etc.).
|
|
535
|
+
|
|
536
|
+
This class consolidates all shared signal logic, connection management,
|
|
537
|
+
conditional evaluation, and result accumulation logic used by iterative loop end nodes.
|
|
538
|
+
"""
|
|
539
|
+
|
|
540
|
+
start_node: "BaseIterativeStartNode | None" = None
|
|
541
|
+
|
|
542
|
+
def __init__(self, name: str, metadata: dict[Any, Any] | None = None) -> None:
|
|
543
|
+
super().__init__(name, metadata)
|
|
544
|
+
self.start_node = None
|
|
545
|
+
|
|
546
|
+
# End node manages its own results list
|
|
547
|
+
self._results_list: list[Any] = []
|
|
548
|
+
|
|
549
|
+
# Connection tracking for validation
|
|
550
|
+
self._connected_parameters: set[str] = set()
|
|
551
|
+
|
|
552
|
+
# Explicit tethering to Start node
|
|
553
|
+
self.from_start = Parameter(
|
|
554
|
+
name="from_start",
|
|
555
|
+
tooltip="Connected Loop Start Node",
|
|
556
|
+
input_types=[ParameterTypeBuiltin.ALL.value],
|
|
557
|
+
allowed_modes={ParameterMode.INPUT},
|
|
558
|
+
)
|
|
559
|
+
self.from_start.ui_options = {"hide": True, "display_name": "Loop Start Node"}
|
|
560
|
+
|
|
561
|
+
# Main control input and data parameter
|
|
562
|
+
self.add_item_control = ControlParameterInput(
|
|
563
|
+
tooltip="Add current item to output and continue loop", name="add_item"
|
|
564
|
+
)
|
|
565
|
+
self.add_item_control.ui_options = {"display_name": "Add Item to Output"}
|
|
566
|
+
|
|
567
|
+
# Data input for the item to add - positioned right under Add Item control
|
|
568
|
+
self.new_item_to_add = Parameter(
|
|
569
|
+
name="new_item_to_add",
|
|
570
|
+
tooltip="Item to add to results list",
|
|
571
|
+
type=ParameterTypeBuiltin.ANY.value,
|
|
572
|
+
allowed_modes={ParameterMode.INPUT},
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
# Loop completion output
|
|
576
|
+
self.exec_out = ControlParameterOutput(tooltip="Triggered when loop completes", name="exec_out")
|
|
577
|
+
self.exec_out.ui_options = {"display_name": "On Loop Complete"}
|
|
578
|
+
|
|
579
|
+
# Results output - positioned below On Loop Complete
|
|
580
|
+
self.results = Parameter(
|
|
581
|
+
name="results",
|
|
582
|
+
tooltip="Collected loop results",
|
|
583
|
+
output_type="list",
|
|
584
|
+
allowed_modes={ParameterMode.OUTPUT},
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
# Advanced control options for skip and break
|
|
588
|
+
self.skip_control = ControlParameterInput(
|
|
589
|
+
tooltip="Skip current item and continue to next iteration", name="skip_iteration"
|
|
590
|
+
)
|
|
591
|
+
self.skip_control.ui_options = {"display_name": "Skip to Next Iteration"}
|
|
592
|
+
|
|
593
|
+
self.break_control = ControlParameterInput(tooltip="Break out of loop immediately", name="break_loop")
|
|
594
|
+
self.break_control.ui_options = {"display_name": "Break Out of Loop"}
|
|
595
|
+
|
|
596
|
+
# Hidden inputs from Start
|
|
597
|
+
self.loop_end_condition_met_signal_input = ControlParameterInput(
|
|
598
|
+
tooltip="Signal from Start when loop should end", name="loop_end_condition_met_signal_input"
|
|
599
|
+
)
|
|
600
|
+
self.loop_end_condition_met_signal_input.ui_options = {"hide": True, "display_name": "Loop End Signal Input"}
|
|
601
|
+
self.loop_end_condition_met_signal_input.settable = False
|
|
602
|
+
|
|
603
|
+
# Hidden outputs to Start
|
|
604
|
+
self.trigger_next_iteration_signal_output = ControlParameterOutput(
|
|
605
|
+
tooltip="Signal to Start to continue to next iteration", name="trigger_next_iteration_signal_output"
|
|
606
|
+
)
|
|
607
|
+
self.trigger_next_iteration_signal_output.ui_options = {
|
|
608
|
+
"hide": True,
|
|
609
|
+
"display_name": "Next Iteration Signal Output",
|
|
610
|
+
}
|
|
611
|
+
self.trigger_next_iteration_signal_output.settable = False
|
|
612
|
+
|
|
613
|
+
self.break_loop_signal_output = ControlParameterOutput(
|
|
614
|
+
tooltip="Signal to Start to break out of loop", name="break_loop_signal_output"
|
|
615
|
+
)
|
|
616
|
+
self.break_loop_signal_output.ui_options = {"hide": True, "display_name": "Break Loop Signal Output"}
|
|
617
|
+
self.break_loop_signal_output.settable = False
|
|
618
|
+
|
|
619
|
+
# Output to iteratively update results
|
|
620
|
+
self.results_output = None
|
|
621
|
+
|
|
622
|
+
# Add main workflow parameters first
|
|
623
|
+
self.add_parameter(self.add_item_control)
|
|
624
|
+
self.add_parameter(self.new_item_to_add)
|
|
625
|
+
self.add_parameter(self.exec_out)
|
|
626
|
+
self.add_parameter(self.results)
|
|
627
|
+
|
|
628
|
+
# Add advanced control options before tethering connection
|
|
629
|
+
self.add_parameter(self.skip_control)
|
|
630
|
+
self.add_parameter(self.break_control)
|
|
631
|
+
|
|
632
|
+
# Add hidden parameters
|
|
633
|
+
self.add_parameter(self.from_start)
|
|
634
|
+
self.add_parameter(self.loop_end_condition_met_signal_input)
|
|
635
|
+
self.add_parameter(self.trigger_next_iteration_signal_output)
|
|
636
|
+
self.add_parameter(self.break_loop_signal_output)
|
|
637
|
+
|
|
638
|
+
def _get_base_node_type_name(self) -> str:
|
|
639
|
+
"""Get the base node type name (e.g., 'ForLoop' from 'ForLoopEndNode')."""
|
|
640
|
+
return self.__class__.__name__.replace("EndNode", "")
|
|
641
|
+
|
|
642
|
+
@abstractmethod
|
|
643
|
+
def _get_compatible_start_classes(self) -> set[type]:
|
|
644
|
+
"""Return the set of Start node classes that this End node can connect to."""
|
|
645
|
+
|
|
646
|
+
def _output_results_list(self) -> None:
|
|
647
|
+
"""Output the current results list to the results parameter.
|
|
648
|
+
|
|
649
|
+
Uses deep copy to ensure nested objects (like dictionaries) are properly copied
|
|
650
|
+
and won't have unintended side effects if modified later.
|
|
651
|
+
"""
|
|
652
|
+
self.parameter_output_values["results"] = copy.deepcopy(self._results_list)
|
|
653
|
+
|
|
654
|
+
def _validate_end_node(self) -> list[Exception] | None:
|
|
655
|
+
"""Common validation logic for both workflow and node run validation."""
|
|
656
|
+
exceptions = []
|
|
657
|
+
if self.start_node is None:
|
|
658
|
+
exceptions.append(Exception("Start node is not set on End Node."))
|
|
659
|
+
|
|
660
|
+
# Validate all required connections exist
|
|
661
|
+
validation_errors = self._validate_iterative_connections()
|
|
662
|
+
if validation_errors:
|
|
663
|
+
exceptions.extend(validation_errors)
|
|
664
|
+
|
|
665
|
+
if exceptions:
|
|
666
|
+
return exceptions
|
|
667
|
+
return super().validate_before_node_run()
|
|
668
|
+
|
|
669
|
+
def validate_before_node_run(self) -> list[Exception] | None:
|
|
670
|
+
return self._validate_end_node()
|
|
671
|
+
|
|
672
|
+
def validate_before_workflow_run(self) -> list[Exception] | None:
|
|
673
|
+
return self._validate_end_node()
|
|
674
|
+
|
|
675
|
+
def process(self) -> None:
|
|
676
|
+
"""Process the end node based on the control path taken."""
|
|
677
|
+
match self._entry_control_parameter:
|
|
678
|
+
case self.add_item_control:
|
|
679
|
+
# Only evaluate new_item_to_add parameter when adding to output
|
|
680
|
+
new_item_value = self.get_parameter_value("new_item_to_add")
|
|
681
|
+
self._results_list.append(new_item_value)
|
|
682
|
+
if self.results_output is not None:
|
|
683
|
+
node, param = self.results_output
|
|
684
|
+
# Set the parameter value on the node. This should trigger after_value_set.
|
|
685
|
+
node.set_parameter_value(param.name, self._results_list)
|
|
686
|
+
case self.skip_control:
|
|
687
|
+
# Skip - don't add anything to output, just continue loop
|
|
688
|
+
pass
|
|
689
|
+
case self.break_control:
|
|
690
|
+
# Break - emit current results and trigger break signal in get_next_control_output
|
|
691
|
+
self._output_results_list()
|
|
692
|
+
case self.loop_end_condition_met_signal_input:
|
|
693
|
+
# Loop has ended naturally, output final results as standard parameter
|
|
694
|
+
self._output_results_list()
|
|
695
|
+
return
|
|
696
|
+
|
|
697
|
+
def get_next_control_output(self) -> Parameter | None:
|
|
698
|
+
"""Return the appropriate signal output based on the control path taken."""
|
|
699
|
+
match self._entry_control_parameter:
|
|
700
|
+
case self.add_item_control | self.skip_control:
|
|
701
|
+
# Both add and skip trigger next iteration
|
|
702
|
+
return self.trigger_next_iteration_signal_output
|
|
703
|
+
case self.break_control:
|
|
704
|
+
# Break triggers break loop signal
|
|
705
|
+
return self.break_loop_signal_output
|
|
706
|
+
case self.loop_end_condition_met_signal_input:
|
|
707
|
+
# Loop end condition triggers normal completion
|
|
708
|
+
return self.exec_out
|
|
709
|
+
case _:
|
|
710
|
+
# Default fallback - should not happen
|
|
711
|
+
return self.exec_out
|
|
712
|
+
|
|
713
|
+
def allow_outgoing_connection(
|
|
714
|
+
self,
|
|
715
|
+
source_parameter: Parameter,
|
|
716
|
+
target_node: BaseNode,
|
|
717
|
+
target_parameter: Parameter,
|
|
718
|
+
) -> bool:
|
|
719
|
+
"""Validate outgoing connections for type safety."""
|
|
720
|
+
# Check signal connections to Start nodes
|
|
721
|
+
if source_parameter in (self.trigger_next_iteration_signal_output, self.break_loop_signal_output):
|
|
722
|
+
# Ensure target node is compatible
|
|
723
|
+
compatible_start_classes = self._get_compatible_start_classes()
|
|
724
|
+
compatible_class_names = {cls.__name__ for cls in compatible_start_classes}
|
|
725
|
+
target_class_name = target_node.__class__.__name__
|
|
726
|
+
if target_class_name not in compatible_class_names:
|
|
727
|
+
logger = logging.getLogger(__name__ + "." + self.name)
|
|
728
|
+
logger.warning(
|
|
729
|
+
"Incompatible connection: %s can only connect to %s, but attempted to connect to %s",
|
|
730
|
+
self.__class__.__name__,
|
|
731
|
+
list(compatible_class_names),
|
|
732
|
+
target_class_name,
|
|
733
|
+
)
|
|
734
|
+
return False
|
|
735
|
+
return super().allow_outgoing_connection(source_parameter, target_node, target_parameter)
|
|
736
|
+
|
|
737
|
+
def allow_incoming_connection(
|
|
738
|
+
self,
|
|
739
|
+
source_node: BaseNode,
|
|
740
|
+
source_parameter: Parameter,
|
|
741
|
+
target_parameter: Parameter,
|
|
742
|
+
) -> bool:
|
|
743
|
+
"""Validate incoming connections for type safety."""
|
|
744
|
+
# Check if this is a loop tethering connection
|
|
745
|
+
if target_parameter == self.from_start:
|
|
746
|
+
# Ensure source node is compatible
|
|
747
|
+
compatible_start_classes = self._get_compatible_start_classes()
|
|
748
|
+
compatible_class_names = {cls.__name__ for cls in compatible_start_classes}
|
|
749
|
+
source_class_name = source_node.__class__.__name__
|
|
750
|
+
if source_class_name not in compatible_class_names:
|
|
751
|
+
logger = logging.getLogger(__name__ + "." + self.name)
|
|
752
|
+
logger.warning(
|
|
753
|
+
"Incompatible connection: %s can only receive connections from %s, but %s attempted to connect",
|
|
754
|
+
self.__class__.__name__,
|
|
755
|
+
list(compatible_class_names),
|
|
756
|
+
source_class_name,
|
|
757
|
+
)
|
|
758
|
+
return False
|
|
759
|
+
return super().allow_incoming_connection(source_node, source_parameter, target_parameter)
|
|
760
|
+
|
|
761
|
+
def _validate_iterative_connections(self) -> list[Exception]:
|
|
762
|
+
"""Validate that all required iterative connections are properly established."""
|
|
763
|
+
errors = []
|
|
764
|
+
node_type = self._get_base_node_type_name()
|
|
765
|
+
|
|
766
|
+
# Check if from_start has incoming connection from Start
|
|
767
|
+
if self.start_node is None:
|
|
768
|
+
errors.append(
|
|
769
|
+
Exception(
|
|
770
|
+
f"{self.name}: Missing required tethering connection. "
|
|
771
|
+
f"REQUIRED ACTION: Connect {node_type} Start 'Loop End Node' to {node_type} End 'Loop Start Node'. "
|
|
772
|
+
"This establishes the explicit relationship between start and end nodes."
|
|
773
|
+
)
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
# Check if all hidden signal connections exist (only if start_node is connected)
|
|
777
|
+
if self.start_node and not _incoming_connection_exists(self.name, "loop_end_condition_met_signal_input"):
|
|
778
|
+
errors.append(
|
|
779
|
+
Exception(
|
|
780
|
+
f"{self.name}: Missing hidden signal connection. "
|
|
781
|
+
f"REQUIRED ACTION: Connect {node_type} Start 'Loop End Signal' to {node_type} End 'Loop End Signal Input'. "
|
|
782
|
+
"This receives the signal when the loop has completed naturally."
|
|
783
|
+
)
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
# Check if control inputs have at least one connection
|
|
787
|
+
control_names = ["add_item", "skip_iteration", "break_loop"]
|
|
788
|
+
connected_controls = []
|
|
789
|
+
|
|
790
|
+
for control_name in control_names:
|
|
791
|
+
if _incoming_connection_exists(self.name, control_name):
|
|
792
|
+
connected_controls.append(control_name) # noqa: PERF401
|
|
793
|
+
|
|
794
|
+
if not connected_controls:
|
|
795
|
+
errors.append(
|
|
796
|
+
Exception(
|
|
797
|
+
f"{self.name}: No control flow connections found. "
|
|
798
|
+
f"REQUIRED ACTION: Connect at least one control flow to {node_type} End. "
|
|
799
|
+
"Options: 'Add Item to Output', 'Skip to Next Iteration', or 'Break Out of Loop'. "
|
|
800
|
+
"The End node needs to receive control flow from your loop body logic."
|
|
801
|
+
)
|
|
802
|
+
)
|
|
803
|
+
|
|
804
|
+
return errors
|
|
805
|
+
|
|
806
|
+
def initialize_spotlight(self) -> None:
|
|
807
|
+
"""Custom spotlight initialization for conditional dependency resolution."""
|
|
808
|
+
match self._entry_control_parameter:
|
|
809
|
+
case self.add_item_control:
|
|
810
|
+
# Only resolve new_item_to_add dependency if we're actually going to use it
|
|
811
|
+
new_item_param = self.get_parameter_by_name("new_item_to_add")
|
|
812
|
+
if new_item_param and ParameterMode.INPUT in new_item_param.get_mode():
|
|
813
|
+
self.current_spotlight_parameter = new_item_param
|
|
814
|
+
case _:
|
|
815
|
+
# For skip or break paths, don't resolve any input dependencies
|
|
816
|
+
self.current_spotlight_parameter = None
|
|
817
|
+
|
|
818
|
+
def advance_parameter(self) -> bool:
|
|
819
|
+
"""Custom parameter advancement with conditional dependency resolution."""
|
|
820
|
+
if self.current_spotlight_parameter is None:
|
|
821
|
+
return False
|
|
822
|
+
|
|
823
|
+
# Use default advancement behavior for the new_item_to_add parameter
|
|
824
|
+
if self.current_spotlight_parameter.next is not None:
|
|
825
|
+
self.current_spotlight_parameter = self.current_spotlight_parameter.next
|
|
826
|
+
return True
|
|
827
|
+
|
|
828
|
+
self.current_spotlight_parameter = None
|
|
829
|
+
return False
|
|
830
|
+
|
|
831
|
+
def reset_for_workflow_run(self) -> None:
|
|
832
|
+
"""Reset End state for a fresh workflow run."""
|
|
833
|
+
self._results_list = []
|
|
834
|
+
self._output_results_list()
|
|
835
|
+
|
|
836
|
+
def after_incoming_connection(
|
|
837
|
+
self,
|
|
838
|
+
source_node: BaseNode,
|
|
839
|
+
source_parameter: Parameter,
|
|
840
|
+
target_parameter: Parameter,
|
|
841
|
+
) -> None:
|
|
842
|
+
# Track incoming connections for validation
|
|
843
|
+
self._connected_parameters.add(target_parameter.name)
|
|
844
|
+
|
|
845
|
+
if target_parameter is self.from_start and isinstance(source_node, BaseIterativeStartNode):
|
|
846
|
+
self.start_node = source_node
|
|
847
|
+
# Auto-create all hidden signal connections when main tethering connection is made
|
|
848
|
+
self._create_hidden_signal_connections(source_node)
|
|
849
|
+
return super().after_incoming_connection(source_node, source_parameter, target_parameter)
|
|
850
|
+
|
|
851
|
+
def after_incoming_connection_removed(
|
|
852
|
+
self,
|
|
853
|
+
source_node: BaseNode,
|
|
854
|
+
source_parameter: Parameter,
|
|
855
|
+
target_parameter: Parameter,
|
|
856
|
+
) -> None:
|
|
857
|
+
# Remove from tracking when connection is removed
|
|
858
|
+
self._connected_parameters.discard(target_parameter.name)
|
|
859
|
+
|
|
860
|
+
if target_parameter is self.from_start and isinstance(source_node, BaseIterativeStartNode):
|
|
861
|
+
self.start_node = None
|
|
862
|
+
# Clean up hidden signal connections when main tethering connection is removed
|
|
863
|
+
self._remove_hidden_signal_connections(source_node)
|
|
864
|
+
return super().after_incoming_connection_removed(source_node, source_parameter, target_parameter)
|
|
865
|
+
|
|
866
|
+
def _create_hidden_signal_connections(self, start_node: BaseNode) -> None:
|
|
867
|
+
"""Automatically create all hidden signal connections between Start and End nodes."""
|
|
868
|
+
from griptape_nodes.retained_mode.events.connection_events import CreateConnectionRequest
|
|
869
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
870
|
+
|
|
871
|
+
# Create the hidden signal connections and default control flow:
|
|
872
|
+
|
|
873
|
+
# 1. Start → End: loop_end_condition_met_signal → loop_end_condition_met_signal_input
|
|
874
|
+
GriptapeNodes.handle_request(
|
|
875
|
+
CreateConnectionRequest(
|
|
876
|
+
source_node_name=start_node.name,
|
|
877
|
+
source_parameter_name="loop_end_condition_met_signal",
|
|
878
|
+
target_node_name=self.name,
|
|
879
|
+
target_parameter_name="loop_end_condition_met_signal_input",
|
|
880
|
+
)
|
|
881
|
+
)
|
|
882
|
+
|
|
883
|
+
# 2. End → Start: trigger_next_iteration_signal_output → trigger_next_iteration_signal
|
|
884
|
+
GriptapeNodes.handle_request(
|
|
885
|
+
CreateConnectionRequest(
|
|
886
|
+
source_node_name=self.name,
|
|
887
|
+
source_parameter_name="trigger_next_iteration_signal_output",
|
|
888
|
+
target_node_name=start_node.name,
|
|
889
|
+
target_parameter_name="trigger_next_iteration_signal",
|
|
890
|
+
)
|
|
891
|
+
)
|
|
892
|
+
|
|
893
|
+
# 3. End → Start: break_loop_signal_output → break_loop_signal
|
|
894
|
+
GriptapeNodes.handle_request(
|
|
895
|
+
CreateConnectionRequest(
|
|
896
|
+
source_node_name=self.name,
|
|
897
|
+
source_parameter_name="break_loop_signal_output",
|
|
898
|
+
target_node_name=start_node.name,
|
|
899
|
+
target_parameter_name="break_loop_signal",
|
|
900
|
+
)
|
|
901
|
+
)
|
|
902
|
+
|
|
903
|
+
# 4. Default control flow: Start → End: exec_out → add_item (default "happy path")
|
|
904
|
+
# Only create this connection if the exec_out parameter doesn't already have a connection
|
|
905
|
+
if not _outgoing_connection_exists(start_node.name, "exec_out"):
|
|
906
|
+
GriptapeNodes.handle_request(
|
|
907
|
+
CreateConnectionRequest(
|
|
908
|
+
source_node_name=start_node.name,
|
|
909
|
+
source_parameter_name="exec_out",
|
|
910
|
+
target_node_name=self.name,
|
|
911
|
+
target_parameter_name="add_item",
|
|
912
|
+
)
|
|
913
|
+
)
|
|
914
|
+
|
|
915
|
+
def _remove_hidden_signal_connections(self, start_node: BaseNode) -> None:
|
|
916
|
+
"""Remove all hidden signal connections when the main tethering connection is removed."""
|
|
917
|
+
from griptape_nodes.retained_mode.events.connection_events import (
|
|
918
|
+
DeleteConnectionRequest,
|
|
919
|
+
ListConnectionsForNodeRequest,
|
|
920
|
+
ListConnectionsForNodeResultSuccess,
|
|
921
|
+
)
|
|
922
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
923
|
+
|
|
924
|
+
# Get current connections for start node to check what still exists
|
|
925
|
+
list_connections_result = GriptapeNodes.handle_request(ListConnectionsForNodeRequest(node_name=start_node.name))
|
|
926
|
+
if not isinstance(list_connections_result, ListConnectionsForNodeResultSuccess):
|
|
927
|
+
return # Cannot determine what connections exist, exit gracefully
|
|
928
|
+
|
|
929
|
+
# Helper function to check if a connection exists
|
|
930
|
+
def connection_exists(
|
|
931
|
+
source_node_name: str, source_param: str, target_node_name: str, target_param: str
|
|
932
|
+
) -> bool:
|
|
933
|
+
# Check in outgoing connections from source node
|
|
934
|
+
for conn in list_connections_result.outgoing_connections:
|
|
935
|
+
if (
|
|
936
|
+
conn.source_parameter_name == source_param
|
|
937
|
+
and conn.target_node_name == target_node_name
|
|
938
|
+
and conn.target_parameter_name == target_param
|
|
939
|
+
):
|
|
940
|
+
return True
|
|
941
|
+
# Check in incoming connections to source node
|
|
942
|
+
for conn in list_connections_result.incoming_connections:
|
|
943
|
+
if (
|
|
944
|
+
conn.source_node_name == source_node_name
|
|
945
|
+
and conn.source_parameter_name == source_param
|
|
946
|
+
and conn.target_parameter_name == target_param
|
|
947
|
+
):
|
|
948
|
+
return True
|
|
949
|
+
return False
|
|
950
|
+
|
|
951
|
+
# Remove the hidden signal connections:
|
|
952
|
+
|
|
953
|
+
# 1. Start → End: loop_end_condition_met_signal → loop_end_condition_met_signal_input
|
|
954
|
+
if connection_exists(
|
|
955
|
+
start_node.name, "loop_end_condition_met_signal", self.name, "loop_end_condition_met_signal_input"
|
|
956
|
+
):
|
|
957
|
+
GriptapeNodes.handle_request(
|
|
958
|
+
DeleteConnectionRequest(
|
|
959
|
+
source_node_name=start_node.name,
|
|
960
|
+
source_parameter_name="loop_end_condition_met_signal",
|
|
961
|
+
target_node_name=self.name,
|
|
962
|
+
target_parameter_name="loop_end_condition_met_signal_input",
|
|
963
|
+
)
|
|
964
|
+
)
|
|
965
|
+
|
|
966
|
+
# 2. End → Start: trigger_next_iteration_signal_output → trigger_next_iteration_signal
|
|
967
|
+
if connection_exists(
|
|
968
|
+
self.name, "trigger_next_iteration_signal_output", start_node.name, "trigger_next_iteration_signal"
|
|
969
|
+
):
|
|
970
|
+
GriptapeNodes.handle_request(
|
|
971
|
+
DeleteConnectionRequest(
|
|
972
|
+
source_node_name=self.name,
|
|
973
|
+
source_parameter_name="trigger_next_iteration_signal_output",
|
|
974
|
+
target_node_name=start_node.name,
|
|
975
|
+
target_parameter_name="trigger_next_iteration_signal",
|
|
976
|
+
)
|
|
977
|
+
)
|
|
978
|
+
|
|
979
|
+
# 3. End → Start: break_loop_signal_output → break_loop_signal
|
|
980
|
+
if connection_exists(self.name, "break_loop_signal_output", start_node.name, "break_loop_signal"):
|
|
981
|
+
GriptapeNodes.handle_request(
|
|
982
|
+
DeleteConnectionRequest(
|
|
983
|
+
source_node_name=self.name,
|
|
984
|
+
source_parameter_name="break_loop_signal_output",
|
|
985
|
+
target_node_name=start_node.name,
|
|
986
|
+
target_parameter_name="break_loop_signal",
|
|
987
|
+
)
|
|
988
|
+
)
|
|
989
|
+
|
|
990
|
+
def after_outgoing_connection(
|
|
991
|
+
self, source_parameter: Parameter, target_node: BaseNode, target_parameter: Parameter
|
|
992
|
+
) -> None:
|
|
993
|
+
if source_parameter == self.results:
|
|
994
|
+
# Update value on each iteration
|
|
995
|
+
self.results_output = NodeParameterPair(node=target_node, parameter=target_parameter)
|
|
996
|
+
return super().after_outgoing_connection(source_parameter, target_node, target_parameter)
|
|
997
|
+
|
|
998
|
+
def after_outgoing_connection_removed(
|
|
999
|
+
self, source_parameter: Parameter, target_node: BaseNode, target_parameter: Parameter
|
|
1000
|
+
) -> None:
|
|
1001
|
+
if source_parameter == self.results:
|
|
1002
|
+
# Update value on each iteration
|
|
1003
|
+
self.results_output = None
|
|
1004
|
+
return super().after_outgoing_connection_removed(source_parameter, target_node, target_parameter)
|