griptape-nodes 0.49.0__py3-none-any.whl → 0.51.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/exe_types/core_types.py +8 -0
- griptape_nodes/exe_types/node_types.py +275 -1
- griptape_nodes/retained_mode/events/node_events.py +97 -0
- griptape_nodes/retained_mode/events/parameter_events.py +2 -0
- griptape_nodes/retained_mode/managers/flow_manager.py +47 -6
- griptape_nodes/retained_mode/managers/node_manager.py +248 -19
- {griptape_nodes-0.49.0.dist-info → griptape_nodes-0.51.0.dist-info}/METADATA +1 -1
- griptape_nodes-0.51.0.dist-info/RECORD +127 -0
- {griptape_nodes-0.49.0.dist-info → griptape_nodes-0.51.0.dist-info}/WHEEL +1 -1
- griptape_nodes-0.49.0.dist-info/RECORD +0 -127
- {griptape_nodes-0.49.0.dist-info → griptape_nodes-0.51.0.dist-info}/entry_points.txt +0 -0
|
@@ -714,6 +714,10 @@ class Parameter(BaseNodeElement, UIOptionsMixin):
|
|
|
714
714
|
# During save/load, this value IS still serialized to save its proper state.
|
|
715
715
|
settable: bool = True
|
|
716
716
|
|
|
717
|
+
# "serializable" controls whether parameter values should be serialized during save/load operations.
|
|
718
|
+
# Set to False for parameters containing non-serializable types (ImageDrivers, PromptDrivers, file handles, etc.)
|
|
719
|
+
serializable: bool = True
|
|
720
|
+
|
|
717
721
|
user_defined: bool = False
|
|
718
722
|
_allowed_modes: set = field(
|
|
719
723
|
default_factory=lambda: {
|
|
@@ -747,6 +751,7 @@ class Parameter(BaseNodeElement, UIOptionsMixin):
|
|
|
747
751
|
ui_options: dict | None = None,
|
|
748
752
|
*,
|
|
749
753
|
settable: bool = True,
|
|
754
|
+
serializable: bool = True,
|
|
750
755
|
user_defined: bool = False,
|
|
751
756
|
element_id: str | None = None,
|
|
752
757
|
element_type: str | None = None,
|
|
@@ -764,6 +769,7 @@ class Parameter(BaseNodeElement, UIOptionsMixin):
|
|
|
764
769
|
self.tooltip_as_property = tooltip_as_property
|
|
765
770
|
self.tooltip_as_output = tooltip_as_output
|
|
766
771
|
self.settable = settable
|
|
772
|
+
self.serializable = serializable
|
|
767
773
|
self.user_defined = user_defined
|
|
768
774
|
if allowed_modes is None:
|
|
769
775
|
self._allowed_modes = {ParameterMode.INPUT, ParameterMode.OUTPUT, ParameterMode.PROPERTY}
|
|
@@ -813,6 +819,8 @@ class Parameter(BaseNodeElement, UIOptionsMixin):
|
|
|
813
819
|
our_dict["tooltip_as_property"] = self.tooltip_as_property
|
|
814
820
|
|
|
815
821
|
our_dict["is_user_defined"] = self.user_defined
|
|
822
|
+
our_dict["settable"] = self.settable
|
|
823
|
+
our_dict["serializable"] = self.serializable
|
|
816
824
|
our_dict["ui_options"] = self.ui_options
|
|
817
825
|
|
|
818
826
|
# Let's bundle up the mode details.
|
|
@@ -4,7 +4,7 @@ import logging
|
|
|
4
4
|
from abc import ABC, abstractmethod
|
|
5
5
|
from collections.abc import Callable, Generator, Iterable
|
|
6
6
|
from enum import StrEnum, auto
|
|
7
|
-
from typing import Any, TypeVar
|
|
7
|
+
from typing import Any, NamedTuple, TypeVar
|
|
8
8
|
|
|
9
9
|
from griptape.events import BaseEvent, EventBus
|
|
10
10
|
|
|
@@ -26,12 +26,14 @@ from griptape_nodes.retained_mode.events.base_events import (
|
|
|
26
26
|
ExecutionEvent,
|
|
27
27
|
ExecutionGriptapeNodeEvent,
|
|
28
28
|
ProgressEvent,
|
|
29
|
+
RequestPayload,
|
|
29
30
|
)
|
|
30
31
|
from griptape_nodes.retained_mode.events.execution_events import (
|
|
31
32
|
NodeUnresolvedEvent,
|
|
32
33
|
ParameterValueUpdateEvent,
|
|
33
34
|
)
|
|
34
35
|
from griptape_nodes.retained_mode.events.parameter_events import (
|
|
36
|
+
AddParameterToNodeRequest,
|
|
35
37
|
RemoveElementEvent,
|
|
36
38
|
RemoveParameterFromNodeRequest,
|
|
37
39
|
)
|
|
@@ -52,6 +54,23 @@ class NodeResolutionState(StrEnum):
|
|
|
52
54
|
RESOLVED = auto()
|
|
53
55
|
|
|
54
56
|
|
|
57
|
+
class NodeMessageResult(NamedTuple):
|
|
58
|
+
"""Result from a node message callback.
|
|
59
|
+
|
|
60
|
+
Attributes:
|
|
61
|
+
success: True if the message was handled successfully, False otherwise
|
|
62
|
+
details: Human-readable description of what happened
|
|
63
|
+
response: Optional response data to return to the sender
|
|
64
|
+
altered_workflow_state: True if the message handling altered workflow state.
|
|
65
|
+
Clients can use this to determine if the workflow needs to be re-saved.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
success: bool
|
|
69
|
+
details: str
|
|
70
|
+
response: Any = None
|
|
71
|
+
altered_workflow_state: bool = True
|
|
72
|
+
|
|
73
|
+
|
|
55
74
|
class BaseNode(ABC):
|
|
56
75
|
# Owned by a flow
|
|
57
76
|
name: str
|
|
@@ -148,6 +167,15 @@ class BaseNode(ABC):
|
|
|
148
167
|
"""Callback to confirm allowing a Connection going OUT of this Node."""
|
|
149
168
|
return True
|
|
150
169
|
|
|
170
|
+
def before_incoming_connection(
|
|
171
|
+
self,
|
|
172
|
+
source_node: BaseNode, # noqa: ARG002
|
|
173
|
+
source_parameter_name: str, # noqa: ARG002
|
|
174
|
+
target_parameter_name: str, # noqa: ARG002
|
|
175
|
+
) -> None:
|
|
176
|
+
"""Callback before validating a Connection coming TO this Node."""
|
|
177
|
+
return
|
|
178
|
+
|
|
151
179
|
def after_incoming_connection(
|
|
152
180
|
self,
|
|
153
181
|
source_node: BaseNode, # noqa: ARG002
|
|
@@ -157,6 +185,15 @@ class BaseNode(ABC):
|
|
|
157
185
|
"""Callback after a Connection has been established TO this Node."""
|
|
158
186
|
return
|
|
159
187
|
|
|
188
|
+
def before_outgoing_connection(
|
|
189
|
+
self,
|
|
190
|
+
source_parameter_name: str, # noqa: ARG002
|
|
191
|
+
target_node: BaseNode, # noqa: ARG002
|
|
192
|
+
target_parameter_name: str, # noqa: ARG002
|
|
193
|
+
) -> None:
|
|
194
|
+
"""Callback before validating a Connection going OUT of this Node."""
|
|
195
|
+
return
|
|
196
|
+
|
|
160
197
|
def after_outgoing_connection(
|
|
161
198
|
self,
|
|
162
199
|
source_parameter: Parameter, # noqa: ARG002
|
|
@@ -251,6 +288,31 @@ class BaseNode(ABC):
|
|
|
251
288
|
"""Callback for when a Griptape Event comes destined for this Node."""
|
|
252
289
|
return
|
|
253
290
|
|
|
291
|
+
def on_node_message_received(
|
|
292
|
+
self,
|
|
293
|
+
optional_element_name: str | None, # noqa: ARG002
|
|
294
|
+
message_type: str,
|
|
295
|
+
message: Any, # noqa: ARG002
|
|
296
|
+
) -> NodeMessageResult:
|
|
297
|
+
"""Callback for when a message is sent directly to this node.
|
|
298
|
+
|
|
299
|
+
Custom nodes may elect to override this method to handle specific message types
|
|
300
|
+
and implement custom communication patterns with external systems.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
optional_element_name: Optional element name this message relates to
|
|
304
|
+
message_type: String indicating the message type for parsing
|
|
305
|
+
message: Message payload of any type
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
NodeMessageResult: Result containing success status, details, and optional response
|
|
309
|
+
"""
|
|
310
|
+
return NodeMessageResult(
|
|
311
|
+
success=False,
|
|
312
|
+
details=f"Node '{self.name}' was sent a message of type '{message_type}'. Failed because no message handler was specified for this node. Implement the on_node_message_received method in this node class in order for it to receive messages.",
|
|
313
|
+
response=None,
|
|
314
|
+
)
|
|
315
|
+
|
|
254
316
|
def does_name_exist(self, param_name: str) -> bool:
|
|
255
317
|
for parameter in self.parameters:
|
|
256
318
|
if parameter.name == param_name:
|
|
@@ -1070,6 +1132,218 @@ class EndLoopNode(BaseNode):
|
|
|
1070
1132
|
"""Creating class for Start Loop Node in order to implement loop functionality in execution."""
|
|
1071
1133
|
|
|
1072
1134
|
|
|
1135
|
+
class ErrorProxyNode(BaseNode):
|
|
1136
|
+
"""A proxy node that substitutes for nodes that failed to create due to missing dependencies or errors.
|
|
1137
|
+
|
|
1138
|
+
This node maintains the original node type information and allows workflows to continue loading
|
|
1139
|
+
even when some node types are unavailable. It generates parameters dynamically as connections
|
|
1140
|
+
and values are assigned to maintain workflow structure.
|
|
1141
|
+
"""
|
|
1142
|
+
|
|
1143
|
+
def __init__(
|
|
1144
|
+
self,
|
|
1145
|
+
name: str,
|
|
1146
|
+
original_node_type: str,
|
|
1147
|
+
original_library_name: str,
|
|
1148
|
+
failure_reason: str,
|
|
1149
|
+
metadata: dict[Any, Any] | None = None,
|
|
1150
|
+
) -> None:
|
|
1151
|
+
super().__init__(name, metadata)
|
|
1152
|
+
|
|
1153
|
+
self.original_node_type = original_node_type
|
|
1154
|
+
self.original_library_name = original_library_name
|
|
1155
|
+
self.failure_reason = failure_reason
|
|
1156
|
+
# Record ALL initial_setup=True requests in order for 1:1 replay
|
|
1157
|
+
self._recorded_initialization_requests: list[RequestPayload] = []
|
|
1158
|
+
|
|
1159
|
+
# Track if user has made connection modifications after initial setup
|
|
1160
|
+
self._has_connection_modifications: bool = False
|
|
1161
|
+
|
|
1162
|
+
# Add error message parameter explaining the failure
|
|
1163
|
+
self._error_message = ParameterMessage(
|
|
1164
|
+
name="error_proxy_message",
|
|
1165
|
+
variant="error",
|
|
1166
|
+
value="", # Will be set by _update_error_message
|
|
1167
|
+
)
|
|
1168
|
+
self.add_node_element(self._error_message)
|
|
1169
|
+
self._update_error_message()
|
|
1170
|
+
|
|
1171
|
+
def _get_base_error_message(self) -> str:
|
|
1172
|
+
"""Generate the base error message for this ErrorProxyNode."""
|
|
1173
|
+
return (
|
|
1174
|
+
f"This is a placeholder for a node of type '{self.original_node_type}'"
|
|
1175
|
+
f"\nfrom the '{self.original_library_name}' library."
|
|
1176
|
+
f"\nIt encountered a problem when loading."
|
|
1177
|
+
f"\nThe technical issue:\n{self.failure_reason}\n\n"
|
|
1178
|
+
f"Your original node will be restored once the issue above is fixed "
|
|
1179
|
+
f"(which may require registering the appropriate library, or getting "
|
|
1180
|
+
f"a code fix from the node author)."
|
|
1181
|
+
)
|
|
1182
|
+
|
|
1183
|
+
def on_attempt_set_parameter_value(self, param_name: str) -> None:
|
|
1184
|
+
"""Public method to attempt setting a parameter value during initial setup.
|
|
1185
|
+
|
|
1186
|
+
Creates a PROPERTY mode parameter if it doesn't exist to support value setting.
|
|
1187
|
+
|
|
1188
|
+
Args:
|
|
1189
|
+
param_name: Name of the parameter to prepare for value setting
|
|
1190
|
+
"""
|
|
1191
|
+
self._ensure_parameter_exists(param_name)
|
|
1192
|
+
|
|
1193
|
+
def _ensure_parameter_exists(self, param_name: str) -> None:
|
|
1194
|
+
"""Ensures a parameter exists on this node.
|
|
1195
|
+
|
|
1196
|
+
Creates a universal parameter with all modes enabled for maximum flexibility.
|
|
1197
|
+
Auto-generated parameters are marked as non-user-defined so they don't get serialized.
|
|
1198
|
+
|
|
1199
|
+
Args:
|
|
1200
|
+
param_name: Name of the parameter to ensure exists
|
|
1201
|
+
"""
|
|
1202
|
+
existing_param = super().get_parameter_by_name(param_name)
|
|
1203
|
+
|
|
1204
|
+
if existing_param is None:
|
|
1205
|
+
# Create new universal parameter with all modes enabled
|
|
1206
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
1207
|
+
|
|
1208
|
+
request = AddParameterToNodeRequest(
|
|
1209
|
+
node_name=self.name,
|
|
1210
|
+
parameter_name=param_name,
|
|
1211
|
+
type=ParameterTypeBuiltin.ANY.value, # ANY = parameter's main type for maximum flexibility
|
|
1212
|
+
input_types=[ParameterTypeBuiltin.ANY.value], # ANY = accepts any single input type
|
|
1213
|
+
output_type=ParameterTypeBuiltin.ALL.value, # ALL = can output any type (passthrough)
|
|
1214
|
+
tooltip="Parameter created for placeholder node to preserve workflow connections",
|
|
1215
|
+
mode_allowed_input=True, # Enable all modes upfront
|
|
1216
|
+
mode_allowed_output=True,
|
|
1217
|
+
mode_allowed_property=True,
|
|
1218
|
+
is_user_defined=False, # Don't serialize this parameter
|
|
1219
|
+
initial_setup=True, # Allows setting non-settable parameters and prevents resolution cascades during workflow loading
|
|
1220
|
+
)
|
|
1221
|
+
result = GriptapeNodes.handle_request(request)
|
|
1222
|
+
|
|
1223
|
+
# Check if parameter creation was successful
|
|
1224
|
+
from griptape_nodes.retained_mode.events.parameter_events import AddParameterToNodeResultSuccess
|
|
1225
|
+
|
|
1226
|
+
if not isinstance(result, AddParameterToNodeResultSuccess):
|
|
1227
|
+
failure_message = f"Failed to create parameter '{param_name}': {result.result_details}"
|
|
1228
|
+
raise RuntimeError(failure_message)
|
|
1229
|
+
# If parameter already exists, nothing to do - it already has all modes
|
|
1230
|
+
|
|
1231
|
+
def allow_incoming_connection(
|
|
1232
|
+
self,
|
|
1233
|
+
source_node: BaseNode, # noqa: ARG002
|
|
1234
|
+
source_parameter: Parameter, # noqa: ARG002
|
|
1235
|
+
target_parameter: Parameter, # noqa: ARG002
|
|
1236
|
+
) -> bool:
|
|
1237
|
+
"""ErrorProxyNode allows connections - it's a shell for maintaining connections."""
|
|
1238
|
+
return True
|
|
1239
|
+
|
|
1240
|
+
def allow_outgoing_connection(
|
|
1241
|
+
self,
|
|
1242
|
+
source_parameter: Parameter, # noqa: ARG002
|
|
1243
|
+
target_node: BaseNode, # noqa: ARG002
|
|
1244
|
+
target_parameter: Parameter, # noqa: ARG002
|
|
1245
|
+
) -> bool:
|
|
1246
|
+
"""ErrorProxyNode allows connections - it's a shell for maintaining connections."""
|
|
1247
|
+
return True
|
|
1248
|
+
|
|
1249
|
+
def before_incoming_connection(
|
|
1250
|
+
self,
|
|
1251
|
+
source_node: BaseNode, # noqa: ARG002
|
|
1252
|
+
source_parameter_name: str, # noqa: ARG002
|
|
1253
|
+
target_parameter_name: str,
|
|
1254
|
+
) -> None:
|
|
1255
|
+
"""Create target parameter before connection validation."""
|
|
1256
|
+
self._ensure_parameter_exists(target_parameter_name)
|
|
1257
|
+
|
|
1258
|
+
def before_outgoing_connection(
|
|
1259
|
+
self,
|
|
1260
|
+
source_parameter_name: str,
|
|
1261
|
+
target_node: BaseNode, # noqa: ARG002
|
|
1262
|
+
target_parameter_name: str, # noqa: ARG002
|
|
1263
|
+
) -> None:
|
|
1264
|
+
"""Create source parameter before connection validation."""
|
|
1265
|
+
self._ensure_parameter_exists(source_parameter_name)
|
|
1266
|
+
|
|
1267
|
+
def set_post_init_connections_modified(self) -> None:
|
|
1268
|
+
"""Mark that user-initiated connections have been modified and update the warning message."""
|
|
1269
|
+
if not self._has_connection_modifications:
|
|
1270
|
+
self._has_connection_modifications = True
|
|
1271
|
+
self._update_error_message()
|
|
1272
|
+
|
|
1273
|
+
def _update_error_message(self) -> None:
|
|
1274
|
+
"""Update the ParameterMessage to include connection modification warning."""
|
|
1275
|
+
# Build the updated message with connection warning
|
|
1276
|
+
base_message = self._get_base_error_message()
|
|
1277
|
+
|
|
1278
|
+
# Add connection modification warning if applicable
|
|
1279
|
+
if self._has_connection_modifications:
|
|
1280
|
+
connection_warning = (
|
|
1281
|
+
"\n\nWARNING: You have modified connections to this placeholder node."
|
|
1282
|
+
"\nThis may require manual fixes when the original node is restored."
|
|
1283
|
+
)
|
|
1284
|
+
final_message = base_message + connection_warning
|
|
1285
|
+
else:
|
|
1286
|
+
# Add the general note only if no modifications have been made
|
|
1287
|
+
general_warning = (
|
|
1288
|
+
"\n\nNote: Making changes to this node may require manual fixes when restored,"
|
|
1289
|
+
"\nas we can't predict how all node authors craft their custom nodes."
|
|
1290
|
+
)
|
|
1291
|
+
final_message = base_message + general_warning
|
|
1292
|
+
|
|
1293
|
+
# Update the error message value
|
|
1294
|
+
self._error_message.value = final_message
|
|
1295
|
+
|
|
1296
|
+
def validate_before_node_run(self) -> list[Exception] | None:
|
|
1297
|
+
"""Prevent ErrorProxy nodes from running - validate at node level only."""
|
|
1298
|
+
error_msg = (
|
|
1299
|
+
f"Cannot run node '{self.name}': This is a placeholder node put in place to preserve your workflow until the breaking issue is fixed.\n\n"
|
|
1300
|
+
f"The original '{self.original_node_type}' from library '{self.original_library_name}' failed to load due to this technical issue:\n\n"
|
|
1301
|
+
f"{self.failure_reason}\n\n"
|
|
1302
|
+
f"Once you resolve the issue above, reload this workflow and the placeholder will be automatically replaced with the original node."
|
|
1303
|
+
)
|
|
1304
|
+
return [RuntimeError(error_msg)]
|
|
1305
|
+
|
|
1306
|
+
def record_initialization_request(self, request: RequestPayload) -> None:
|
|
1307
|
+
"""Record an initialization request for replay during serialization.
|
|
1308
|
+
|
|
1309
|
+
This method captures requests that modify ErrorProxyNode structure during workflow loading,
|
|
1310
|
+
preserving information needed for restoration when the original node becomes available.
|
|
1311
|
+
|
|
1312
|
+
WHAT WE RECORD:
|
|
1313
|
+
- AlterParameterDetailsRequest: Parameter modifications from original node definition
|
|
1314
|
+
- Any request with initial_setup=True that changes node structure in ways that cannot
|
|
1315
|
+
be reconstructed from final state alone
|
|
1316
|
+
|
|
1317
|
+
WHAT WE DO NOT RECORD (and why):
|
|
1318
|
+
- SetParameterValueRequest: Final parameter values are serialized normally via parameter_values
|
|
1319
|
+
- AddParameterToNodeRequest: User-defined parameters are serialized via is_user_defined=True flag
|
|
1320
|
+
- CreateConnectionRequest: Connections are serialized separately and recreated during loading
|
|
1321
|
+
- RenameParameterRequest: Final parameter names are preserved in serialized state
|
|
1322
|
+
- SetNodeMetadataRequest: Final metadata state is preserved in node.metadata
|
|
1323
|
+
- SetLockNodeStateRequest: Final lock state is preserved in node.lock
|
|
1324
|
+
"""
|
|
1325
|
+
self._recorded_initialization_requests.append(request)
|
|
1326
|
+
|
|
1327
|
+
def get_recorded_initialization_requests(self, request_type: type | None = None) -> list[RequestPayload]:
|
|
1328
|
+
"""Get recorded initialization requests for 1:1 serialization replay.
|
|
1329
|
+
|
|
1330
|
+
Args:
|
|
1331
|
+
request_type: Optional class to filter by. If provided, only returns requests
|
|
1332
|
+
of that type. If None, returns all recorded requests.
|
|
1333
|
+
|
|
1334
|
+
Returns:
|
|
1335
|
+
List of recorded requests in the order they were received.
|
|
1336
|
+
"""
|
|
1337
|
+
if request_type is None:
|
|
1338
|
+
return self._recorded_initialization_requests
|
|
1339
|
+
|
|
1340
|
+
return [req for req in self._recorded_initialization_requests if isinstance(req, request_type)]
|
|
1341
|
+
|
|
1342
|
+
def process(self) -> Any:
|
|
1343
|
+
"""No-op process method. Error Proxy nodes do nothing during execution."""
|
|
1344
|
+
return None
|
|
1345
|
+
|
|
1346
|
+
|
|
1073
1347
|
class Connection:
|
|
1074
1348
|
source_node: BaseNode
|
|
1075
1349
|
target_node: BaseNode
|
|
@@ -45,6 +45,7 @@ class CreateNodeRequest(RequestPayload):
|
|
|
45
45
|
resolution: Initial resolution state (defaults to UNRESOLVED)
|
|
46
46
|
initial_setup: Skip setup work when loading from file (defaults to False)
|
|
47
47
|
set_as_new_context: Set this node as current context after creation (defaults to False)
|
|
48
|
+
create_error_proxy_on_failure: Create Error Proxy node if creation fails (defaults to True)
|
|
48
49
|
|
|
49
50
|
Results: CreateNodeResultSuccess (with assigned name) | CreateNodeResultFailure (invalid type, missing library, flow not found)
|
|
50
51
|
"""
|
|
@@ -60,6 +61,8 @@ class CreateNodeRequest(RequestPayload):
|
|
|
60
61
|
initial_setup: bool = False
|
|
61
62
|
# When True, this Node will be pushed as the current Node within the Current Context.
|
|
62
63
|
set_as_new_context: bool = False
|
|
64
|
+
# When True, create an Error Proxy node if the requested node type fails to create
|
|
65
|
+
create_error_proxy_on_failure: bool = True
|
|
63
66
|
|
|
64
67
|
|
|
65
68
|
@dataclass
|
|
@@ -262,6 +265,50 @@ class SetNodeMetadataResultFailure(ResultPayloadFailure):
|
|
|
262
265
|
"""Metadata update failed. Common causes: node not found, no current context, invalid metadata format."""
|
|
263
266
|
|
|
264
267
|
|
|
268
|
+
@dataclass
|
|
269
|
+
@PayloadRegistry.register
|
|
270
|
+
class BatchSetNodeMetadataRequest(RequestPayload):
|
|
271
|
+
"""Update metadata for multiple nodes in a single request.
|
|
272
|
+
|
|
273
|
+
Use when: Updating positions for multiple nodes at once, applying bulk styling changes,
|
|
274
|
+
implementing multi-node selection operations, optimizing performance for UI updates.
|
|
275
|
+
Supports partial updates - only specified metadata fields are updated for each node.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
node_metadata_updates: Dictionary mapping node names to their metadata updates.
|
|
279
|
+
Each node's metadata is merged with existing metadata (partial update).
|
|
280
|
+
If a node name is None, uses the current context node.
|
|
281
|
+
|
|
282
|
+
Results: BatchSetNodeMetadataResultSuccess | BatchSetNodeMetadataResultFailure (some nodes not found, update errors)
|
|
283
|
+
"""
|
|
284
|
+
|
|
285
|
+
node_metadata_updates: dict[str | None, dict[str, Any]]
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@dataclass
|
|
289
|
+
@PayloadRegistry.register
|
|
290
|
+
class BatchSetNodeMetadataResultSuccess(WorkflowAlteredMixin, ResultPayloadSuccess):
|
|
291
|
+
"""Batch node metadata update completed successfully.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
updated_nodes: List of node names that were successfully updated
|
|
295
|
+
failed_nodes: Dictionary mapping failed node names to error descriptions (if any)
|
|
296
|
+
"""
|
|
297
|
+
|
|
298
|
+
updated_nodes: list[str]
|
|
299
|
+
failed_nodes: dict[str, str] = field(default_factory=dict)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
@dataclass
|
|
303
|
+
@PayloadRegistry.register
|
|
304
|
+
class BatchSetNodeMetadataResultFailure(ResultPayloadFailure):
|
|
305
|
+
"""Batch metadata update failed.
|
|
306
|
+
|
|
307
|
+
Common causes: all nodes not found, no current context, invalid metadata format,
|
|
308
|
+
or other systemic errors preventing the batch operation.
|
|
309
|
+
"""
|
|
310
|
+
|
|
311
|
+
|
|
265
312
|
# Get all info via a "jumbo" node event. Batches multiple info requests for, say, a GUI.
|
|
266
313
|
# ...jumbode?
|
|
267
314
|
@dataclass
|
|
@@ -677,3 +724,53 @@ class DuplicateSelectedNodesResultFailure(WorkflowNotAlteredMixin, ResultPayload
|
|
|
677
724
|
Common causes: nodes not found, constraints/conflicts,
|
|
678
725
|
insufficient resources, connection duplication failures.
|
|
679
726
|
"""
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
@dataclass
|
|
730
|
+
@PayloadRegistry.register
|
|
731
|
+
class SendNodeMessageRequest(RequestPayload):
|
|
732
|
+
"""Send a message to a specific node.
|
|
733
|
+
|
|
734
|
+
Use when: External systems need to signal or send data directly to individual nodes,
|
|
735
|
+
implementing custom communication patterns, triggering node-specific behaviors.
|
|
736
|
+
|
|
737
|
+
Args:
|
|
738
|
+
node_name: Name of the target node (None for current context node)
|
|
739
|
+
optional_element_name: Optional element name this message relates to
|
|
740
|
+
message_type: String indicating message type for receiver parsing
|
|
741
|
+
message: Message payload of any type
|
|
742
|
+
|
|
743
|
+
Results: SendNodeMessageResultSuccess (with response) | SendNodeMessageResultFailure (node not found, handler error)
|
|
744
|
+
"""
|
|
745
|
+
|
|
746
|
+
message_type: str
|
|
747
|
+
message: Any
|
|
748
|
+
node_name: str | None = None
|
|
749
|
+
optional_element_name: str | None = None
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
@dataclass
|
|
753
|
+
@PayloadRegistry.register
|
|
754
|
+
class SendNodeMessageResultSuccess(ResultPayloadSuccess):
|
|
755
|
+
"""Node message sent and processed successfully.
|
|
756
|
+
|
|
757
|
+
Args:
|
|
758
|
+
response: Optional response data from the node's message handler
|
|
759
|
+
"""
|
|
760
|
+
|
|
761
|
+
response: Any = None
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
@dataclass
|
|
765
|
+
@PayloadRegistry.register
|
|
766
|
+
class SendNodeMessageResultFailure(ResultPayloadFailure):
|
|
767
|
+
"""Node message sending failed.
|
|
768
|
+
|
|
769
|
+
Common causes: node not found, no current context, message handler error,
|
|
770
|
+
unsupported message type.
|
|
771
|
+
|
|
772
|
+
Args:
|
|
773
|
+
response: Optional response data from the node's message handler (even on failure)
|
|
774
|
+
"""
|
|
775
|
+
|
|
776
|
+
response: Any = None
|
|
@@ -40,6 +40,7 @@ class AddParameterToNodeRequest(RequestPayload):
|
|
|
40
40
|
mode_allowed_input: Whether parameter can be used as input
|
|
41
41
|
mode_allowed_property: Whether parameter can be used as property
|
|
42
42
|
mode_allowed_output: Whether parameter can be used as output
|
|
43
|
+
is_user_defined: Whether this is a user-defined parameter (affects serialization)
|
|
43
44
|
parent_container_name: Name of parent container if nested
|
|
44
45
|
initial_setup: Skip setup work when loading from file
|
|
45
46
|
|
|
@@ -61,6 +62,7 @@ class AddParameterToNodeRequest(RequestPayload):
|
|
|
61
62
|
mode_allowed_input: bool = Field(default=True)
|
|
62
63
|
mode_allowed_property: bool = Field(default=True)
|
|
63
64
|
mode_allowed_output: bool = Field(default=True)
|
|
65
|
+
is_user_defined: bool = Field(default=True)
|
|
64
66
|
parent_container_name: str | None = None
|
|
65
67
|
# initial_setup prevents unnecessary work when we are loading a workflow from a file.
|
|
66
68
|
initial_setup: bool = False
|
|
@@ -14,7 +14,7 @@ from griptape_nodes.exe_types.core_types import (
|
|
|
14
14
|
ParameterTypeBuiltin,
|
|
15
15
|
)
|
|
16
16
|
from griptape_nodes.exe_types.flow import ControlFlow
|
|
17
|
-
from griptape_nodes.exe_types.node_types import BaseNode, NodeResolutionState, StartLoopNode, StartNode
|
|
17
|
+
from griptape_nodes.exe_types.node_types import BaseNode, ErrorProxyNode, NodeResolutionState, StartLoopNode, StartNode
|
|
18
18
|
from griptape_nodes.machines.control_flow import CompleteState, ControlFlowMachine
|
|
19
19
|
from griptape_nodes.retained_mode.events.base_events import (
|
|
20
20
|
ExecutionEvent,
|
|
@@ -681,6 +681,18 @@ class FlowManager:
|
|
|
681
681
|
|
|
682
682
|
# Cross-flow connections are now supported via global connection storage
|
|
683
683
|
|
|
684
|
+
# Call before_connection callbacks to allow nodes to prepare parameters
|
|
685
|
+
source_node.before_outgoing_connection(
|
|
686
|
+
source_parameter_name=request.source_parameter_name,
|
|
687
|
+
target_node=target_node,
|
|
688
|
+
target_parameter_name=request.target_parameter_name,
|
|
689
|
+
)
|
|
690
|
+
target_node.before_incoming_connection(
|
|
691
|
+
source_node=source_node,
|
|
692
|
+
source_parameter_name=request.source_parameter_name,
|
|
693
|
+
target_parameter_name=request.target_parameter_name,
|
|
694
|
+
)
|
|
695
|
+
|
|
684
696
|
# Now validate the parameters.
|
|
685
697
|
source_param = source_node.get_parameter_by_name(request.source_parameter_name)
|
|
686
698
|
if source_param is None:
|
|
@@ -852,6 +864,13 @@ class FlowManager:
|
|
|
852
864
|
)
|
|
853
865
|
)
|
|
854
866
|
|
|
867
|
+
# Check if either node is ErrorProxyNode and mark connection modification if not initial_setup
|
|
868
|
+
if not request.initial_setup:
|
|
869
|
+
if isinstance(source_node, ErrorProxyNode):
|
|
870
|
+
source_node.set_post_init_connections_modified()
|
|
871
|
+
if isinstance(target_node, ErrorProxyNode):
|
|
872
|
+
target_node.set_post_init_connections_modified()
|
|
873
|
+
|
|
855
874
|
result = CreateConnectionResultSuccess()
|
|
856
875
|
|
|
857
876
|
return result
|
|
@@ -995,6 +1014,12 @@ class FlowManager:
|
|
|
995
1014
|
details = f'Connection "{source_node_name}.{request.source_parameter_name}" to "{target_node_name}.{request.target_parameter_name}" deleted.'
|
|
996
1015
|
logger.debug(details)
|
|
997
1016
|
|
|
1017
|
+
# Check if either node is ErrorProxyNode and mark connection modification (deletes are always user-initiated)
|
|
1018
|
+
if isinstance(source_node, ErrorProxyNode):
|
|
1019
|
+
source_node.set_post_init_connections_modified()
|
|
1020
|
+
if isinstance(target_node, ErrorProxyNode):
|
|
1021
|
+
target_node.set_post_init_connections_modified()
|
|
1022
|
+
|
|
998
1023
|
result = DeleteConnectionResultSuccess()
|
|
999
1024
|
return result
|
|
1000
1025
|
|
|
@@ -1880,6 +1905,7 @@ class FlowManager:
|
|
|
1880
1905
|
valid_data_nodes = []
|
|
1881
1906
|
start_nodes = []
|
|
1882
1907
|
control_nodes = []
|
|
1908
|
+
cn_mgr = self.get_connections()
|
|
1883
1909
|
for node in all_nodes:
|
|
1884
1910
|
# if it's a start node, start here! Return the first one!
|
|
1885
1911
|
if isinstance(node, StartNode):
|
|
@@ -1890,15 +1916,21 @@ class FlowManager:
|
|
|
1890
1916
|
control_param = False
|
|
1891
1917
|
for parameter in node.parameters:
|
|
1892
1918
|
if ParameterTypeBuiltin.CONTROL_TYPE.value == parameter.output_type:
|
|
1893
|
-
|
|
1894
|
-
|
|
1919
|
+
# Check if the control parameters are being used at all. If they are not, treat it as a data node.
|
|
1920
|
+
incoming_control = (
|
|
1921
|
+
node.name in cn_mgr.incoming_index and parameter.name in cn_mgr.incoming_index[node.name]
|
|
1922
|
+
)
|
|
1923
|
+
outgoing_control = (
|
|
1924
|
+
node.name in cn_mgr.outgoing_index and parameter.name in cn_mgr.outgoing_index[node.name]
|
|
1925
|
+
)
|
|
1926
|
+
if incoming_control or outgoing_control:
|
|
1927
|
+
control_param = True
|
|
1928
|
+
break
|
|
1895
1929
|
if not control_param:
|
|
1896
1930
|
# saving this for later
|
|
1897
1931
|
data_nodes.append(node)
|
|
1898
1932
|
# If this node doesn't have a control connection..
|
|
1899
1933
|
continue
|
|
1900
|
-
|
|
1901
|
-
cn_mgr = self.get_connections()
|
|
1902
1934
|
# check if it has an incoming connection. If it does, it's not a start node
|
|
1903
1935
|
has_control_connection = False
|
|
1904
1936
|
if node.name in cn_mgr.incoming_index:
|
|
@@ -1906,10 +1938,19 @@ class FlowManager:
|
|
|
1906
1938
|
param = node.get_parameter_by_name(param_name)
|
|
1907
1939
|
if param and ParameterTypeBuiltin.CONTROL_TYPE.value == param.output_type:
|
|
1908
1940
|
# there is a control connection coming in
|
|
1941
|
+
# If the node is a StartLoopNode, it may have an incoming hidden connection from it's EndLoopNode for iteration.
|
|
1942
|
+
if isinstance(node, StartLoopNode):
|
|
1943
|
+
connection_id = cn_mgr.incoming_index[node.name][param_name][0]
|
|
1944
|
+
connection = cn_mgr.connections[connection_id]
|
|
1945
|
+
connected_node = connection.get_source_node()
|
|
1946
|
+
# Check if the source node is the end loop node associated with this StartLoopNode.
|
|
1947
|
+
# If it is, then this could still be the first node in the control flow.
|
|
1948
|
+
if connected_node == node.end_node:
|
|
1949
|
+
continue
|
|
1909
1950
|
has_control_connection = True
|
|
1910
1951
|
break
|
|
1911
1952
|
# if there is a connection coming in, isn't a start.
|
|
1912
|
-
if has_control_connection
|
|
1953
|
+
if has_control_connection:
|
|
1913
1954
|
continue
|
|
1914
1955
|
# Does it have an outgoing connection?
|
|
1915
1956
|
if node.name in cn_mgr.outgoing_index:
|