griptape-nodes 0.65.1__py3-none-any.whl → 0.65.3__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/common/node_executor.py +14 -14
- griptape_nodes/exe_types/node_groups/__init__.py +6 -0
- griptape_nodes/exe_types/node_groups/base_node_group.py +31 -0
- griptape_nodes/exe_types/node_groups/subflow_node_group.py +1007 -0
- griptape_nodes/exe_types/node_types.py +0 -805
- griptape_nodes/machines/control_flow.py +4 -4
- griptape_nodes/node_library/library_registry.py +1 -0
- griptape_nodes/node_library/workflow_registry.py +1 -1
- griptape_nodes/retained_mode/events/flow_events.py +1 -1
- griptape_nodes/retained_mode/events/node_events.py +11 -77
- griptape_nodes/retained_mode/managers/flow_manager.py +72 -79
- griptape_nodes/retained_mode/managers/node_manager.py +113 -193
- griptape_nodes/retained_mode/managers/workflow_manager.py +5 -8
- griptape_nodes/version_compatibility/versions/v0_63_8/deprecated_nodegroup_parameters.py +2 -2
- {griptape_nodes-0.65.1.dist-info → griptape_nodes-0.65.3.dist-info}/METADATA +1 -1
- {griptape_nodes-0.65.1.dist-info → griptape_nodes-0.65.3.dist-info}/RECORD +18 -15
- {griptape_nodes-0.65.1.dist-info → griptape_nodes-0.65.3.dist-info}/WHEEL +1 -1
- {griptape_nodes-0.65.1.dist-info → griptape_nodes-0.65.3.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,1007 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from griptape_nodes.exe_types.core_types import (
|
|
8
|
+
ControlParameter,
|
|
9
|
+
Parameter,
|
|
10
|
+
ParameterMode,
|
|
11
|
+
ParameterTypeBuiltin,
|
|
12
|
+
Trait,
|
|
13
|
+
)
|
|
14
|
+
from griptape_nodes.exe_types.node_groups.base_node_group import BaseNodeGroup
|
|
15
|
+
from griptape_nodes.exe_types.node_types import (
|
|
16
|
+
LOCAL_EXECUTION,
|
|
17
|
+
get_library_names_with_publish_handlers,
|
|
18
|
+
)
|
|
19
|
+
from griptape_nodes.retained_mode.events.connection_events import (
|
|
20
|
+
CreateConnectionRequest,
|
|
21
|
+
DeleteConnectionRequest,
|
|
22
|
+
DeleteConnectionResultSuccess,
|
|
23
|
+
)
|
|
24
|
+
from griptape_nodes.retained_mode.events.parameter_events import (
|
|
25
|
+
AddParameterToNodeRequest,
|
|
26
|
+
AddParameterToNodeResultSuccess,
|
|
27
|
+
RemoveParameterFromNodeRequest,
|
|
28
|
+
)
|
|
29
|
+
from griptape_nodes.traits.options import Options
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from griptape_nodes.exe_types.connections import Connections
|
|
33
|
+
from griptape_nodes.exe_types.node_types import BaseNode, Connection
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger("griptape_nodes")
|
|
36
|
+
|
|
37
|
+
NODE_GROUP_FLOW = "NodeGroupFlow"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SubflowNodeGroup(BaseNodeGroup, ABC):
|
|
41
|
+
"""Abstract base class for subflow node groups.
|
|
42
|
+
|
|
43
|
+
Proxy node that represents a group of nodes during DAG execution.
|
|
44
|
+
|
|
45
|
+
This node acts as a single execution unit for a group of nodes that should
|
|
46
|
+
be executed in parallel. When the DAG executor encounters this proxy node,
|
|
47
|
+
it passes the entire NodeGroup to the NodeExecutor which handles parallel
|
|
48
|
+
execution of all grouped nodes.
|
|
49
|
+
|
|
50
|
+
The proxy node has parameters that mirror the external connections to/from
|
|
51
|
+
the group, allowing it to seamlessly integrate into the DAG structure.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
_proxy_param_to_connections: dict[str, int]
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
name: str,
|
|
59
|
+
metadata: dict[Any, Any] | None = None,
|
|
60
|
+
) -> None:
|
|
61
|
+
super().__init__(name, metadata)
|
|
62
|
+
self.execution_environment = Parameter(
|
|
63
|
+
name="execution_environment",
|
|
64
|
+
tooltip="Environment that the group should execute in",
|
|
65
|
+
type=ParameterTypeBuiltin.STR,
|
|
66
|
+
allowed_modes={ParameterMode.PROPERTY},
|
|
67
|
+
default_value=LOCAL_EXECUTION,
|
|
68
|
+
traits={Options(choices=get_library_names_with_publish_handlers())},
|
|
69
|
+
)
|
|
70
|
+
self.add_parameter(self.execution_environment)
|
|
71
|
+
# Track mapping from proxy parameter name to (original_node, original_param_name)
|
|
72
|
+
self._proxy_param_to_connections = {}
|
|
73
|
+
if "execution_environment" not in self.metadata:
|
|
74
|
+
self.metadata["execution_environment"] = {}
|
|
75
|
+
self.metadata["execution_environment"]["Griptape Nodes Library"] = {
|
|
76
|
+
"start_flow_node": "StartFlow",
|
|
77
|
+
"parameter_names": {},
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# Don't create subflow in __init__ - it will be created on-demand when nodes are added
|
|
81
|
+
# or restored during deserialization
|
|
82
|
+
|
|
83
|
+
# Add parameters from registered StartFlow nodes for each publishing library
|
|
84
|
+
self._add_start_flow_parameters()
|
|
85
|
+
|
|
86
|
+
def _create_subflow(self) -> None:
|
|
87
|
+
"""Create a dedicated subflow for this NodeGroup's nodes.
|
|
88
|
+
|
|
89
|
+
Note: This is called during __init__, so the node may not yet be added to a flow.
|
|
90
|
+
The subflow will be created without a parent initially, and can be reparented later.
|
|
91
|
+
"""
|
|
92
|
+
from griptape_nodes.retained_mode.events.flow_events import (
|
|
93
|
+
CreateFlowRequest,
|
|
94
|
+
CreateFlowResultSuccess,
|
|
95
|
+
)
|
|
96
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
97
|
+
|
|
98
|
+
subflow_name = f"{self.name}_subflow"
|
|
99
|
+
self.metadata["subflow_name"] = subflow_name
|
|
100
|
+
|
|
101
|
+
# Get current flow to set as parent so subflow will be serialized with parent
|
|
102
|
+
current_flow = GriptapeNodes.ContextManager().get_current_flow()
|
|
103
|
+
parent_flow_name = current_flow.name if current_flow else None
|
|
104
|
+
|
|
105
|
+
# Create metadata with flow_type
|
|
106
|
+
subflow_metadata = {"flow_type": NODE_GROUP_FLOW}
|
|
107
|
+
|
|
108
|
+
request = CreateFlowRequest(
|
|
109
|
+
flow_name=subflow_name,
|
|
110
|
+
parent_flow_name=parent_flow_name,
|
|
111
|
+
set_as_new_context=False,
|
|
112
|
+
metadata=subflow_metadata,
|
|
113
|
+
)
|
|
114
|
+
result = GriptapeNodes.handle_request(request)
|
|
115
|
+
|
|
116
|
+
if not isinstance(result, CreateFlowResultSuccess):
|
|
117
|
+
logger.warning("%s failed to create subflow '%s': %s", self.name, subflow_name, result.result_details)
|
|
118
|
+
|
|
119
|
+
def _add_start_flow_parameters(self) -> None:
|
|
120
|
+
"""Add parameters from all registered StartFlow nodes to this SubflowNodeGroup.
|
|
121
|
+
|
|
122
|
+
For each library that has registered a PublishWorkflowRequest handler with
|
|
123
|
+
a StartFlow node, this method:
|
|
124
|
+
1. Creates a temporary instance of that StartFlow node
|
|
125
|
+
2. Extracts all its parameters
|
|
126
|
+
3. Adds them to this SubflowNodeGroup with a prefix based on the class name
|
|
127
|
+
4. Stores metadata mapping execution environments to their parameters
|
|
128
|
+
"""
|
|
129
|
+
from griptape_nodes.retained_mode.events.workflow_events import PublishWorkflowRequest
|
|
130
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
131
|
+
|
|
132
|
+
# Initialize metadata structure for execution environment mappings
|
|
133
|
+
if self.metadata is None:
|
|
134
|
+
self.metadata = {}
|
|
135
|
+
if "execution_environment" not in self.metadata:
|
|
136
|
+
self.metadata["execution_environment"] = {}
|
|
137
|
+
|
|
138
|
+
# Get all libraries that have registered PublishWorkflowRequest handlers
|
|
139
|
+
library_manager = GriptapeNodes.LibraryManager()
|
|
140
|
+
event_handlers = library_manager.get_registered_event_handlers(PublishWorkflowRequest)
|
|
141
|
+
|
|
142
|
+
# Process each registered library
|
|
143
|
+
for library_name, handler in event_handlers.items():
|
|
144
|
+
self._process_library_start_flow_parameters(library_name, handler)
|
|
145
|
+
|
|
146
|
+
def _process_library_start_flow_parameters(self, library_name: str, handler: Any) -> None:
|
|
147
|
+
"""Process and add StartFlow parameters from a single library.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
library_name: Name of the library
|
|
151
|
+
handler: The registered event handler containing event data
|
|
152
|
+
"""
|
|
153
|
+
import logging
|
|
154
|
+
|
|
155
|
+
from griptape_nodes.node_library.library_registry import LibraryRegistry
|
|
156
|
+
from griptape_nodes.retained_mode.events.workflow_events import PublishWorkflowRegisteredEventData
|
|
157
|
+
|
|
158
|
+
logger = logging.getLogger(__name__)
|
|
159
|
+
|
|
160
|
+
registered_event_data = handler.event_data
|
|
161
|
+
|
|
162
|
+
if registered_event_data is None:
|
|
163
|
+
return
|
|
164
|
+
if not isinstance(registered_event_data, PublishWorkflowRegisteredEventData):
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
# Get the StartFlow node information
|
|
168
|
+
start_flow_node_type = registered_event_data.start_flow_node_type
|
|
169
|
+
start_flow_library_name = registered_event_data.start_flow_node_library_name
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
# Get the library that contains the StartFlow node
|
|
173
|
+
library = LibraryRegistry.get_library(name=start_flow_library_name)
|
|
174
|
+
except KeyError:
|
|
175
|
+
logger.debug(
|
|
176
|
+
"Library '%s' not found when adding StartFlow parameters for '%s'",
|
|
177
|
+
start_flow_library_name,
|
|
178
|
+
library_name,
|
|
179
|
+
)
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
# Create a temporary instance of the StartFlow node to inspect its parameters
|
|
184
|
+
temp_start_flow_node = library.create_node(
|
|
185
|
+
node_type=start_flow_node_type,
|
|
186
|
+
name=f"temp_{start_flow_node_type}",
|
|
187
|
+
)
|
|
188
|
+
except Exception as e:
|
|
189
|
+
logger.debug(
|
|
190
|
+
"Failed to create temporary StartFlow node '%s' from library '%s': %s",
|
|
191
|
+
start_flow_node_type,
|
|
192
|
+
start_flow_library_name,
|
|
193
|
+
e,
|
|
194
|
+
)
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
# Get the class name for prefixing (convert to lowercase for parameter naming)
|
|
198
|
+
class_name_prefix = start_flow_node_type.lower()
|
|
199
|
+
|
|
200
|
+
# Store metadata for this execution environment
|
|
201
|
+
parameter_names = []
|
|
202
|
+
|
|
203
|
+
# Add each parameter from the StartFlow node to this SubflowNodeGroup
|
|
204
|
+
for param in temp_start_flow_node.parameters:
|
|
205
|
+
if isinstance(param, ControlParameter):
|
|
206
|
+
continue
|
|
207
|
+
|
|
208
|
+
# Create prefixed parameter name
|
|
209
|
+
prefixed_param_name = f"{class_name_prefix}_{param.name}"
|
|
210
|
+
parameter_names.append(prefixed_param_name)
|
|
211
|
+
|
|
212
|
+
# Clone and add the parameter
|
|
213
|
+
self._clone_and_add_parameter(param, prefixed_param_name)
|
|
214
|
+
|
|
215
|
+
# Store the mapping in metadata
|
|
216
|
+
self.metadata["execution_environment"][library_name] = {
|
|
217
|
+
"start_flow_node": start_flow_node_type,
|
|
218
|
+
"parameter_names": parameter_names,
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
def _clone_and_add_parameter(self, param: Parameter, new_name: str) -> None:
|
|
222
|
+
"""Clone a parameter with a new name and add it to this node.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
param: The parameter to clone
|
|
226
|
+
new_name: The new name for the cloned parameter
|
|
227
|
+
"""
|
|
228
|
+
# Extract traits from parameter children (traits are stored as children of type Trait)
|
|
229
|
+
traits_set: set[type[Trait] | Trait] | None = {child for child in param.children if isinstance(child, Trait)}
|
|
230
|
+
if not traits_set:
|
|
231
|
+
traits_set = None
|
|
232
|
+
|
|
233
|
+
# Clone the parameter with the new name
|
|
234
|
+
cloned_param = Parameter(
|
|
235
|
+
name=new_name,
|
|
236
|
+
tooltip=param.tooltip,
|
|
237
|
+
type=param.type,
|
|
238
|
+
allowed_modes=param.allowed_modes,
|
|
239
|
+
default_value=param.default_value,
|
|
240
|
+
traits=traits_set,
|
|
241
|
+
parent_container_name=param.parent_container_name,
|
|
242
|
+
parent_element_name=param.parent_element_name,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# Add the parameter to this node
|
|
246
|
+
self.add_parameter(cloned_param)
|
|
247
|
+
|
|
248
|
+
def _create_proxy_parameter_for_connection(self, original_param: Parameter, *, is_incoming: bool) -> Parameter:
|
|
249
|
+
"""Create a proxy parameter on this SubflowNodeGroup for an external connection.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
original_param: The parameter from the grouped node
|
|
253
|
+
grouped_node: The node within the group that has the original parameter
|
|
254
|
+
conn_id: The connection ID for uniqueness
|
|
255
|
+
is_incoming: True if this is an incoming connection to the group
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
The newly created proxy parameter
|
|
259
|
+
"""
|
|
260
|
+
# Clone the parameter with the new name
|
|
261
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
262
|
+
|
|
263
|
+
input_types = None
|
|
264
|
+
output_type = None
|
|
265
|
+
if is_incoming:
|
|
266
|
+
input_types = original_param.input_types
|
|
267
|
+
else:
|
|
268
|
+
output_type = original_param.output_type
|
|
269
|
+
|
|
270
|
+
request = AddParameterToNodeRequest(
|
|
271
|
+
node_name=self.name,
|
|
272
|
+
parameter_name=original_param.name,
|
|
273
|
+
input_types=input_types,
|
|
274
|
+
output_type=output_type,
|
|
275
|
+
tooltip="",
|
|
276
|
+
mode_allowed_input=True,
|
|
277
|
+
mode_allowed_output=True,
|
|
278
|
+
)
|
|
279
|
+
# Add with a request, because this will handle naming for us.
|
|
280
|
+
result = GriptapeNodes.handle_request(request)
|
|
281
|
+
if not isinstance(result, AddParameterToNodeResultSuccess):
|
|
282
|
+
msg = "Failed to add parameter to node."
|
|
283
|
+
raise TypeError(msg)
|
|
284
|
+
# Retrieve and return the newly created parameter
|
|
285
|
+
proxy_param = self.get_parameter_by_name(result.parameter_name)
|
|
286
|
+
if proxy_param is None:
|
|
287
|
+
msg = f"{self.name} failed to create proxy parameter '{result.parameter_name}'"
|
|
288
|
+
raise RuntimeError(msg)
|
|
289
|
+
if is_incoming:
|
|
290
|
+
if "left_parameters" in self.metadata:
|
|
291
|
+
self.metadata["left_parameters"].append(proxy_param.name)
|
|
292
|
+
else:
|
|
293
|
+
self.metadata["left_parameters"] = [proxy_param.name]
|
|
294
|
+
elif "right_parameters" in self.metadata:
|
|
295
|
+
self.metadata["right_parameters"].append(proxy_param.name)
|
|
296
|
+
else:
|
|
297
|
+
self.metadata["right_parameters"] = [proxy_param.name]
|
|
298
|
+
|
|
299
|
+
return proxy_param
|
|
300
|
+
|
|
301
|
+
def get_all_nodes(self) -> dict[str, BaseNode]:
|
|
302
|
+
all_nodes = {}
|
|
303
|
+
for node_name, node in self.nodes.items():
|
|
304
|
+
all_nodes[node_name] = node
|
|
305
|
+
if isinstance(node, SubflowNodeGroup):
|
|
306
|
+
all_nodes.update(node.nodes)
|
|
307
|
+
return all_nodes
|
|
308
|
+
|
|
309
|
+
def map_external_connection(self, conn: Connection, *, is_incoming: bool) -> bool:
|
|
310
|
+
"""Track a connection to/from a node in the group and rewire it through a proxy parameter.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
conn: The external connection to track
|
|
314
|
+
conn_id: ID of the connection
|
|
315
|
+
is_incoming: True if connection is coming INTO the group
|
|
316
|
+
"""
|
|
317
|
+
if is_incoming:
|
|
318
|
+
grouped_parameter = conn.target_parameter
|
|
319
|
+
# Store the existing connection so it can be recreated if needed.
|
|
320
|
+
else:
|
|
321
|
+
grouped_parameter = conn.source_parameter
|
|
322
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
323
|
+
|
|
324
|
+
request = DeleteConnectionRequest(
|
|
325
|
+
conn.source_parameter.name,
|
|
326
|
+
conn.target_parameter.name,
|
|
327
|
+
conn.source_node.name,
|
|
328
|
+
conn.target_node.name,
|
|
329
|
+
)
|
|
330
|
+
result = GriptapeNodes.handle_request(request)
|
|
331
|
+
if not isinstance(result, DeleteConnectionResultSuccess):
|
|
332
|
+
return False
|
|
333
|
+
proxy_parameter = self._create_proxy_parameter_for_connection(grouped_parameter, is_incoming=is_incoming)
|
|
334
|
+
# Create connections for proxy parameter
|
|
335
|
+
self.create_connections_for_proxy(proxy_parameter, conn, is_incoming=is_incoming)
|
|
336
|
+
return True
|
|
337
|
+
|
|
338
|
+
def create_connections_for_proxy(
|
|
339
|
+
self, proxy_parameter: Parameter, old_connection: Connection, *, is_incoming: bool
|
|
340
|
+
) -> None:
|
|
341
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
342
|
+
|
|
343
|
+
create_first_connection = CreateConnectionRequest(
|
|
344
|
+
source_parameter_name=old_connection.source_parameter.name,
|
|
345
|
+
target_parameter_name=proxy_parameter.name,
|
|
346
|
+
source_node_name=old_connection.source_node.name,
|
|
347
|
+
target_node_name=self.name,
|
|
348
|
+
is_node_group_internal=not is_incoming,
|
|
349
|
+
)
|
|
350
|
+
create_second_connection = CreateConnectionRequest(
|
|
351
|
+
source_parameter_name=proxy_parameter.name,
|
|
352
|
+
target_parameter_name=old_connection.target_parameter.name,
|
|
353
|
+
source_node_name=self.name,
|
|
354
|
+
target_node_name=old_connection.target_node.name,
|
|
355
|
+
is_node_group_internal=is_incoming,
|
|
356
|
+
)
|
|
357
|
+
# Store the mapping from proxy parameter to original node/parameter
|
|
358
|
+
# only increment by 1, even though we're making two connections.
|
|
359
|
+
if proxy_parameter.name not in self._proxy_param_to_connections:
|
|
360
|
+
self._proxy_param_to_connections[proxy_parameter.name] = 2
|
|
361
|
+
else:
|
|
362
|
+
self._proxy_param_to_connections[proxy_parameter.name] += 2
|
|
363
|
+
GriptapeNodes.handle_request(create_first_connection)
|
|
364
|
+
GriptapeNodes.handle_request(create_second_connection)
|
|
365
|
+
|
|
366
|
+
def unmap_node_connections(self, node: BaseNode, connections: Connections) -> None: # noqa: C901
|
|
367
|
+
"""Remove tracking of an external connection, restore original connection, and clean up proxy parameter.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
node: The node to unmap
|
|
371
|
+
connections: The connections object
|
|
372
|
+
"""
|
|
373
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
374
|
+
|
|
375
|
+
# For the node being removed - We need to figure out all of it's connections TO the node group. These connections need to be remapped.
|
|
376
|
+
# If we delete connections from a proxy parameter, and it has no more connections, then the proxy parameter should be deleted unless it's user defined.
|
|
377
|
+
# It will 1. not be in the proxy map. and 2. it will have a value of > 0
|
|
378
|
+
# Get all outgoing connections
|
|
379
|
+
outgoing_connections = connections.get_outgoing_connections_to_node(node, to_node=self)
|
|
380
|
+
# Delete outgoing connections
|
|
381
|
+
for parameter_name, outgoing_connection_list in outgoing_connections.items():
|
|
382
|
+
for outgoing_connection in outgoing_connection_list:
|
|
383
|
+
proxy_parameter = outgoing_connection.target_parameter
|
|
384
|
+
# get old connections first, since this will delete the proxy
|
|
385
|
+
remap_connections = connections.get_outgoing_connections_from_parameter(self, proxy_parameter)
|
|
386
|
+
# Delete the internal connection
|
|
387
|
+
delete_result = GriptapeNodes.FlowManager().on_delete_connection_request(
|
|
388
|
+
DeleteConnectionRequest(
|
|
389
|
+
source_parameter_name=parameter_name,
|
|
390
|
+
target_parameter_name=proxy_parameter.name,
|
|
391
|
+
source_node_name=node.name,
|
|
392
|
+
target_node_name=self.name,
|
|
393
|
+
)
|
|
394
|
+
)
|
|
395
|
+
if delete_result.failed():
|
|
396
|
+
msg = f"{self.name}: Failed to delete internal outgoing connection from {node.name}.{parameter_name} to proxy {proxy_parameter.name}: {delete_result.result_details}"
|
|
397
|
+
raise RuntimeError(msg)
|
|
398
|
+
|
|
399
|
+
# Now create the new connection! We need to get the connections from the proxy parameter
|
|
400
|
+
for connection in remap_connections:
|
|
401
|
+
create_result = GriptapeNodes.FlowManager().on_create_connection_request(
|
|
402
|
+
CreateConnectionRequest(
|
|
403
|
+
source_parameter_name=parameter_name,
|
|
404
|
+
target_parameter_name=connection.target_parameter.name,
|
|
405
|
+
source_node_name=node.name,
|
|
406
|
+
target_node_name=connection.target_node.name,
|
|
407
|
+
)
|
|
408
|
+
)
|
|
409
|
+
if create_result.failed():
|
|
410
|
+
msg = f"{self.name}: Failed to create direct outgoing connection from {node.name}.{parameter_name} to {connection.target_node.name}.{connection.target_parameter.name}: {create_result.result_details}"
|
|
411
|
+
raise RuntimeError(msg)
|
|
412
|
+
|
|
413
|
+
# Get all incoming connections
|
|
414
|
+
incoming_connections = connections.get_incoming_connections_from_node(node, from_node=self)
|
|
415
|
+
# Delete incoming connections
|
|
416
|
+
for parameter_name, incoming_connection_list in incoming_connections.items():
|
|
417
|
+
for incoming_connection in incoming_connection_list:
|
|
418
|
+
proxy_parameter = incoming_connection.source_parameter
|
|
419
|
+
# Get the incoming connections to the proxy parameter
|
|
420
|
+
remap_connections = connections.get_incoming_connections_to_parameter(self, proxy_parameter)
|
|
421
|
+
# Delete the internal connection
|
|
422
|
+
delete_result = GriptapeNodes.FlowManager().on_delete_connection_request(
|
|
423
|
+
DeleteConnectionRequest(
|
|
424
|
+
source_parameter_name=proxy_parameter.name,
|
|
425
|
+
target_parameter_name=parameter_name,
|
|
426
|
+
source_node_name=self.name,
|
|
427
|
+
target_node_name=node.name,
|
|
428
|
+
)
|
|
429
|
+
)
|
|
430
|
+
if delete_result.failed():
|
|
431
|
+
msg = f"{self.name}: Failed to delete internal incoming connection from proxy {proxy_parameter.name} to {node.name}.{parameter_name}: {delete_result.result_details}"
|
|
432
|
+
raise RuntimeError(msg)
|
|
433
|
+
|
|
434
|
+
# Now create the new connection! We need to get the connections to the proxy parameter
|
|
435
|
+
for connection in remap_connections:
|
|
436
|
+
create_result = GriptapeNodes.FlowManager().on_create_connection_request(
|
|
437
|
+
CreateConnectionRequest(
|
|
438
|
+
source_parameter_name=connection.source_parameter.name,
|
|
439
|
+
target_parameter_name=parameter_name,
|
|
440
|
+
source_node_name=connection.source_node.name,
|
|
441
|
+
target_node_name=node.name,
|
|
442
|
+
)
|
|
443
|
+
)
|
|
444
|
+
if create_result.failed():
|
|
445
|
+
msg = f"{self.name}: Failed to create direct incoming connection from {connection.source_node.name}.{connection.source_parameter.name} to {node.name}.{parameter_name}: {create_result.result_details}"
|
|
446
|
+
raise RuntimeError(msg)
|
|
447
|
+
|
|
448
|
+
def _remove_nodes_from_existing_parents(self, nodes: list[BaseNode]) -> None:
|
|
449
|
+
"""Remove nodes from their existing parent groups."""
|
|
450
|
+
child_nodes = {}
|
|
451
|
+
for node in nodes:
|
|
452
|
+
if node.parent_group is not None:
|
|
453
|
+
existing_parent_group = node.parent_group
|
|
454
|
+
if isinstance(existing_parent_group, SubflowNodeGroup):
|
|
455
|
+
child_nodes.setdefault(existing_parent_group, []).append(node)
|
|
456
|
+
for parent_group, node_list in child_nodes.items():
|
|
457
|
+
parent_group.remove_nodes_from_group(node_list)
|
|
458
|
+
|
|
459
|
+
def _add_nodes_to_group_dict(self, nodes: list[BaseNode]) -> None:
|
|
460
|
+
"""Add nodes to the group's node dictionary."""
|
|
461
|
+
for node in nodes:
|
|
462
|
+
node.parent_group = self
|
|
463
|
+
self.nodes[node.name] = node
|
|
464
|
+
|
|
465
|
+
def _cleanup_proxy_parameter(self, proxy_parameter: Parameter, metadata_key: str) -> None:
|
|
466
|
+
"""Clean up proxy parameter if it has no more connections.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
proxy_parameter: The proxy parameter to potentially clean up
|
|
470
|
+
metadata_key: The metadata key ('left_parameters' or 'right_parameters')
|
|
471
|
+
"""
|
|
472
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
473
|
+
|
|
474
|
+
if proxy_parameter.name not in self._proxy_param_to_connections:
|
|
475
|
+
return
|
|
476
|
+
|
|
477
|
+
self._proxy_param_to_connections[proxy_parameter.name] -= 1
|
|
478
|
+
if self._proxy_param_to_connections[proxy_parameter.name] == 0:
|
|
479
|
+
GriptapeNodes.NodeManager().on_remove_parameter_from_node_request(
|
|
480
|
+
request=RemoveParameterFromNodeRequest(node_name=self.name, parameter_name=proxy_parameter.name)
|
|
481
|
+
)
|
|
482
|
+
del self._proxy_param_to_connections[proxy_parameter.name]
|
|
483
|
+
if metadata_key in self.metadata and proxy_parameter.name in self.metadata[metadata_key]:
|
|
484
|
+
self.metadata[metadata_key].remove(proxy_parameter.name)
|
|
485
|
+
|
|
486
|
+
def _remap_outgoing_connections(self, node: BaseNode, connections: Connections) -> None:
|
|
487
|
+
"""Remap outgoing connections that go through proxy parameters.
|
|
488
|
+
|
|
489
|
+
Args:
|
|
490
|
+
node: The node being added to the group
|
|
491
|
+
connections: Connections object from FlowManager
|
|
492
|
+
"""
|
|
493
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
494
|
+
|
|
495
|
+
outgoing_connections = connections.get_outgoing_connections_to_node(node, to_node=self)
|
|
496
|
+
for parameter_name, outgoing_connection_list in outgoing_connections.items():
|
|
497
|
+
for outgoing_connection in outgoing_connection_list:
|
|
498
|
+
proxy_parameter = outgoing_connection.target_parameter
|
|
499
|
+
remap_connections = connections.get_outgoing_connections_from_parameter(self, proxy_parameter)
|
|
500
|
+
|
|
501
|
+
# Check if proxy has other incoming connections besides this one
|
|
502
|
+
# If so, we should keep the proxy and its outgoing connections
|
|
503
|
+
incoming_to_proxy = connections.get_incoming_connections_to_parameter(self, proxy_parameter)
|
|
504
|
+
other_incoming_exists = any(
|
|
505
|
+
conn.source_node.name != node.name or conn.source_parameter.name != parameter_name
|
|
506
|
+
for conn in incoming_to_proxy
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
# Delete the connection from this node to proxy
|
|
510
|
+
delete_result = GriptapeNodes.FlowManager().on_delete_connection_request(
|
|
511
|
+
DeleteConnectionRequest(
|
|
512
|
+
source_parameter_name=parameter_name,
|
|
513
|
+
target_parameter_name=proxy_parameter.name,
|
|
514
|
+
source_node_name=node.name,
|
|
515
|
+
target_node_name=self.name,
|
|
516
|
+
)
|
|
517
|
+
)
|
|
518
|
+
if delete_result.failed():
|
|
519
|
+
msg = f"{self.name}: Failed to delete internal outgoing connection from {node.name}.{parameter_name} to proxy {proxy_parameter.name}: {delete_result.result_details}"
|
|
520
|
+
raise RuntimeError(msg)
|
|
521
|
+
|
|
522
|
+
# Create direct connections from this node to target nodes
|
|
523
|
+
for connection in remap_connections:
|
|
524
|
+
create_result = GriptapeNodes.FlowManager().on_create_connection_request(
|
|
525
|
+
CreateConnectionRequest(
|
|
526
|
+
source_parameter_name=parameter_name,
|
|
527
|
+
target_parameter_name=connection.target_parameter.name,
|
|
528
|
+
source_node_name=node.name,
|
|
529
|
+
target_node_name=connection.target_node.name,
|
|
530
|
+
)
|
|
531
|
+
)
|
|
532
|
+
if create_result.failed():
|
|
533
|
+
msg = f"{self.name}: Failed to create direct outgoing connection from {node.name}.{parameter_name} to {connection.target_node.name}.{connection.target_parameter.name}: {create_result.result_details}"
|
|
534
|
+
raise RuntimeError(msg)
|
|
535
|
+
|
|
536
|
+
# Only delete outgoing connections from proxy and clean up if no other incoming connections exist
|
|
537
|
+
if not other_incoming_exists:
|
|
538
|
+
for connection in remap_connections:
|
|
539
|
+
delete_result = GriptapeNodes.FlowManager().on_delete_connection_request(
|
|
540
|
+
DeleteConnectionRequest(
|
|
541
|
+
source_parameter_name=connection.source_parameter.name,
|
|
542
|
+
target_parameter_name=connection.target_parameter.name,
|
|
543
|
+
source_node_name=connection.source_node.name,
|
|
544
|
+
target_node_name=connection.target_node.name,
|
|
545
|
+
)
|
|
546
|
+
)
|
|
547
|
+
if delete_result.failed():
|
|
548
|
+
msg = f"{self.name}: Failed to delete external connection from proxy {proxy_parameter.name} to {connection.target_node.name}.{connection.target_parameter.name}: {delete_result.result_details}"
|
|
549
|
+
raise RuntimeError(msg)
|
|
550
|
+
|
|
551
|
+
self._cleanup_proxy_parameter(proxy_parameter, "right_parameters")
|
|
552
|
+
|
|
553
|
+
def _remap_incoming_connections(self, node: BaseNode, connections: Connections) -> None:
|
|
554
|
+
"""Remap incoming connections that go through proxy parameters.
|
|
555
|
+
|
|
556
|
+
Args:
|
|
557
|
+
node: The node being added to the group
|
|
558
|
+
connections: Connections object from FlowManager
|
|
559
|
+
"""
|
|
560
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
561
|
+
|
|
562
|
+
incoming_connections = connections.get_incoming_connections_from_node(node, from_node=self)
|
|
563
|
+
for parameter_name, incoming_connection_list in incoming_connections.items():
|
|
564
|
+
for incoming_connection in incoming_connection_list:
|
|
565
|
+
proxy_parameter = incoming_connection.source_parameter
|
|
566
|
+
remap_connections = connections.get_incoming_connections_to_parameter(self, proxy_parameter)
|
|
567
|
+
|
|
568
|
+
# Check if proxy has other outgoing connections besides this one
|
|
569
|
+
# If so, we should keep the proxy and its incoming connections
|
|
570
|
+
outgoing_from_proxy = connections.get_outgoing_connections_from_parameter(self, proxy_parameter)
|
|
571
|
+
other_outgoing_exists = any(
|
|
572
|
+
conn.target_node.name != node.name or conn.target_parameter.name != parameter_name
|
|
573
|
+
for conn in outgoing_from_proxy
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
# Delete the connection from proxy to this node
|
|
577
|
+
delete_result = GriptapeNodes.FlowManager().on_delete_connection_request(
|
|
578
|
+
DeleteConnectionRequest(
|
|
579
|
+
source_parameter_name=proxy_parameter.name,
|
|
580
|
+
target_parameter_name=parameter_name,
|
|
581
|
+
source_node_name=self.name,
|
|
582
|
+
target_node_name=node.name,
|
|
583
|
+
)
|
|
584
|
+
)
|
|
585
|
+
if delete_result.failed():
|
|
586
|
+
msg = f"{self.name}: Failed to delete internal incoming connection from proxy {proxy_parameter.name} to {node.name}.{parameter_name}: {delete_result.result_details}"
|
|
587
|
+
raise RuntimeError(msg)
|
|
588
|
+
|
|
589
|
+
# Create direct connections from source nodes to this node
|
|
590
|
+
for connection in remap_connections:
|
|
591
|
+
create_result = GriptapeNodes.FlowManager().on_create_connection_request(
|
|
592
|
+
CreateConnectionRequest(
|
|
593
|
+
source_parameter_name=connection.source_parameter.name,
|
|
594
|
+
target_parameter_name=parameter_name,
|
|
595
|
+
source_node_name=connection.source_node.name,
|
|
596
|
+
target_node_name=node.name,
|
|
597
|
+
)
|
|
598
|
+
)
|
|
599
|
+
if create_result.failed():
|
|
600
|
+
msg = f"{self.name}: Failed to create direct incoming connection from {connection.source_node.name}.{connection.source_parameter.name} to {node.name}.{parameter_name}: {create_result.result_details}"
|
|
601
|
+
raise RuntimeError(msg)
|
|
602
|
+
|
|
603
|
+
# Only delete incoming connections to proxy and clean up if no other outgoing connections exist
|
|
604
|
+
if not other_outgoing_exists:
|
|
605
|
+
for connection in remap_connections:
|
|
606
|
+
delete_result = GriptapeNodes.FlowManager().on_delete_connection_request(
|
|
607
|
+
DeleteConnectionRequest(
|
|
608
|
+
source_parameter_name=connection.source_parameter.name,
|
|
609
|
+
target_parameter_name=proxy_parameter.name,
|
|
610
|
+
source_node_name=connection.source_node.name,
|
|
611
|
+
target_node_name=self.name,
|
|
612
|
+
)
|
|
613
|
+
)
|
|
614
|
+
if delete_result.failed():
|
|
615
|
+
msg = f"{self.name}: Failed to delete external connection from {connection.source_node.name}.{connection.source_parameter.name} to proxy {proxy_parameter.name}: {delete_result.result_details}"
|
|
616
|
+
raise RuntimeError(msg)
|
|
617
|
+
|
|
618
|
+
self._cleanup_proxy_parameter(proxy_parameter, "left_parameters")
|
|
619
|
+
|
|
620
|
+
def remap_to_internal(self, nodes: list[BaseNode], connections: Connections) -> None:
|
|
621
|
+
"""Remap connections that are now internal after adding nodes to the group.
|
|
622
|
+
|
|
623
|
+
When nodes are added to a group, some connections that previously went through
|
|
624
|
+
proxy parameters may now be internal. This method identifies such connections
|
|
625
|
+
and restores direct connections between the nodes.
|
|
626
|
+
|
|
627
|
+
Args:
|
|
628
|
+
nodes: List of nodes being added to the group
|
|
629
|
+
connections: Connections object from FlowManager
|
|
630
|
+
"""
|
|
631
|
+
for node in nodes:
|
|
632
|
+
self._remap_outgoing_connections(node, connections)
|
|
633
|
+
self._remap_incoming_connections(node, connections)
|
|
634
|
+
|
|
635
|
+
def after_outgoing_connection_removed(
|
|
636
|
+
self, source_parameter: Parameter, target_node: BaseNode, target_parameter: Parameter
|
|
637
|
+
) -> None:
|
|
638
|
+
# Instead of right_parameters, we should check the internal connections
|
|
639
|
+
if target_node.parent_group == self:
|
|
640
|
+
metadata_key = "left_parameters"
|
|
641
|
+
else:
|
|
642
|
+
metadata_key = "right_parameters"
|
|
643
|
+
self._cleanup_proxy_parameter(source_parameter, metadata_key)
|
|
644
|
+
return super().after_outgoing_connection_removed(source_parameter, target_node, target_parameter)
|
|
645
|
+
|
|
646
|
+
def after_incoming_connection_removed(
|
|
647
|
+
self, source_node: BaseNode, source_parameter: Parameter, target_parameter: Parameter
|
|
648
|
+
) -> None:
|
|
649
|
+
# Instead of left_parameters, we should check the internal connections.
|
|
650
|
+
if source_node.parent_group == self:
|
|
651
|
+
metadata_key = "right_parameters"
|
|
652
|
+
else:
|
|
653
|
+
metadata_key = "left_parameters"
|
|
654
|
+
self._cleanup_proxy_parameter(target_parameter, metadata_key)
|
|
655
|
+
return super().after_incoming_connection_removed(source_node, source_parameter, target_parameter)
|
|
656
|
+
|
|
657
|
+
def add_nodes_to_group(self, nodes: list[BaseNode]) -> None:
|
|
658
|
+
"""Add nodes to the group and track their connections.
|
|
659
|
+
|
|
660
|
+
Args:
|
|
661
|
+
nodes: List of nodes to add to the group
|
|
662
|
+
"""
|
|
663
|
+
from griptape_nodes.retained_mode.events.node_events import (
|
|
664
|
+
MoveNodeToNewFlowRequest,
|
|
665
|
+
MoveNodeToNewFlowResultSuccess,
|
|
666
|
+
)
|
|
667
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
668
|
+
|
|
669
|
+
self._remove_nodes_from_existing_parents(nodes)
|
|
670
|
+
self._add_nodes_to_group_dict(nodes)
|
|
671
|
+
|
|
672
|
+
# Create subflow on-demand if it doesn't exist
|
|
673
|
+
subflow_name = self.metadata.get("subflow_name")
|
|
674
|
+
if subflow_name is None:
|
|
675
|
+
self._create_subflow()
|
|
676
|
+
subflow_name = self.metadata.get("subflow_name")
|
|
677
|
+
|
|
678
|
+
if subflow_name is not None:
|
|
679
|
+
for node in nodes:
|
|
680
|
+
move_request = MoveNodeToNewFlowRequest(node_name=node.name, target_flow_name=subflow_name)
|
|
681
|
+
move_result = GriptapeNodes.handle_request(move_request)
|
|
682
|
+
if not isinstance(move_result, MoveNodeToNewFlowResultSuccess):
|
|
683
|
+
msg = "%s failed to move node '%s' to subflow: %s", self.name, node.name, move_result.result_details
|
|
684
|
+
logger.error(msg)
|
|
685
|
+
raise RuntimeError(msg) # noqa: TRY004
|
|
686
|
+
|
|
687
|
+
connections = GriptapeNodes.FlowManager().get_connections()
|
|
688
|
+
node_names_in_group = set(self.nodes.keys())
|
|
689
|
+
self.metadata["node_names_in_group"] = list(node_names_in_group)
|
|
690
|
+
self.remap_to_internal(nodes, connections)
|
|
691
|
+
self._map_external_connections_for_nodes(nodes, connections, node_names_in_group)
|
|
692
|
+
|
|
693
|
+
def _map_external_connections_for_nodes(
|
|
694
|
+
self, nodes: list[BaseNode], connections: Connections, node_names_in_group: set[str]
|
|
695
|
+
) -> None:
|
|
696
|
+
"""Map external connections for nodes being added to the group.
|
|
697
|
+
|
|
698
|
+
Args:
|
|
699
|
+
nodes: List of nodes being added
|
|
700
|
+
connections: Connections object from FlowManager
|
|
701
|
+
node_names_in_group: Set of all node names currently in the group
|
|
702
|
+
"""
|
|
703
|
+
# Group outgoing connections by (source_node, source_parameter) to reuse proxy parameters
|
|
704
|
+
# Skip connections that already go to the NodeGroup itself (existing proxy parameters)
|
|
705
|
+
outgoing_by_source: dict[tuple[str, str], list[Connection]] = {}
|
|
706
|
+
for node in nodes:
|
|
707
|
+
outgoing_connections = connections.get_all_outgoing_connections(node)
|
|
708
|
+
for conn in outgoing_connections:
|
|
709
|
+
if conn.target_node.name not in node_names_in_group and conn.target_node.name != self.name:
|
|
710
|
+
key = (conn.source_node.name, conn.source_parameter.name)
|
|
711
|
+
outgoing_by_source.setdefault(key, []).append(conn)
|
|
712
|
+
|
|
713
|
+
# Group incoming connections by (source_node, source_parameter) to reuse proxy parameters
|
|
714
|
+
# This ensures that when an external node connects to multiple internal nodes,
|
|
715
|
+
# they share a single proxy parameter
|
|
716
|
+
# Skip connections that already come from the NodeGroup itself (existing proxy parameters)
|
|
717
|
+
incoming_by_source: dict[tuple[str, str], list[Connection]] = {}
|
|
718
|
+
for node in nodes:
|
|
719
|
+
incoming_connections = connections.get_all_incoming_connections(node)
|
|
720
|
+
for conn in incoming_connections:
|
|
721
|
+
if conn.source_node.name not in node_names_in_group and conn.source_node.name != self.name:
|
|
722
|
+
key = (conn.source_node.name, conn.source_parameter.name)
|
|
723
|
+
incoming_by_source.setdefault(key, []).append(conn)
|
|
724
|
+
|
|
725
|
+
# Map outgoing connections - one proxy parameter per source parameter
|
|
726
|
+
for conn_list in outgoing_by_source.values():
|
|
727
|
+
self._map_external_connections_group(conn_list, is_incoming=False)
|
|
728
|
+
|
|
729
|
+
# Map incoming connections - one proxy parameter per source parameter
|
|
730
|
+
for conn_list in incoming_by_source.values():
|
|
731
|
+
self._map_external_connections_group(conn_list, is_incoming=True)
|
|
732
|
+
|
|
733
|
+
def _map_external_connections_group(self, conn_list: list[Connection], *, is_incoming: bool) -> None:
|
|
734
|
+
"""Map a group of external connections that share the same external parameter.
|
|
735
|
+
|
|
736
|
+
Creates a single proxy parameter and connects all nodes through it.
|
|
737
|
+
If an existing proxy parameter already handles the same internal source,
|
|
738
|
+
it will be reused instead of creating a new one.
|
|
739
|
+
|
|
740
|
+
Args:
|
|
741
|
+
conn_list: List of connections sharing the same external parameter
|
|
742
|
+
is_incoming: True if these are incoming connections to the group
|
|
743
|
+
"""
|
|
744
|
+
if not conn_list:
|
|
745
|
+
return
|
|
746
|
+
|
|
747
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
748
|
+
|
|
749
|
+
# All connections share the same external parameter
|
|
750
|
+
# For outgoing: the internal (source) parameter is shared
|
|
751
|
+
# For incoming: the external (source) parameter is shared
|
|
752
|
+
first_conn = conn_list[0]
|
|
753
|
+
# Use source_parameter in both cases since we group by source
|
|
754
|
+
grouped_parameter = first_conn.source_parameter
|
|
755
|
+
|
|
756
|
+
# Check if there's an existing proxy parameter we can reuse
|
|
757
|
+
existing_proxy = self._find_existing_proxy_for_source(
|
|
758
|
+
first_conn.source_node, first_conn.source_parameter, is_incoming=is_incoming
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
# Delete all original connections first
|
|
762
|
+
for conn in conn_list:
|
|
763
|
+
request = DeleteConnectionRequest(
|
|
764
|
+
conn.source_parameter.name,
|
|
765
|
+
conn.target_parameter.name,
|
|
766
|
+
conn.source_node.name,
|
|
767
|
+
conn.target_node.name,
|
|
768
|
+
)
|
|
769
|
+
result = GriptapeNodes.handle_request(request)
|
|
770
|
+
if not isinstance(result, DeleteConnectionResultSuccess):
|
|
771
|
+
logger.warning(
|
|
772
|
+
"%s failed to delete connection from %s.%s to %s.%s",
|
|
773
|
+
self.name,
|
|
774
|
+
conn.source_node.name,
|
|
775
|
+
conn.source_parameter.name,
|
|
776
|
+
conn.target_node.name,
|
|
777
|
+
conn.target_parameter.name,
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
# Use existing proxy or create a new one
|
|
781
|
+
if existing_proxy is not None:
|
|
782
|
+
proxy_parameter = existing_proxy
|
|
783
|
+
else:
|
|
784
|
+
proxy_parameter = self._create_proxy_parameter_for_connection(grouped_parameter, is_incoming=is_incoming)
|
|
785
|
+
|
|
786
|
+
# Create connections for all external nodes through the single proxy
|
|
787
|
+
for conn in conn_list:
|
|
788
|
+
self._create_connections_for_proxy_single(proxy_parameter, conn, is_incoming=is_incoming)
|
|
789
|
+
|
|
790
|
+
def _find_existing_proxy_for_source(
|
|
791
|
+
self, source_node: BaseNode, source_parameter: Parameter, *, is_incoming: bool
|
|
792
|
+
) -> Parameter | None:
|
|
793
|
+
"""Find an existing proxy parameter that already handles the given source.
|
|
794
|
+
|
|
795
|
+
For outgoing connections (is_incoming=False):
|
|
796
|
+
Looks for a right-side proxy that has an incoming connection from the
|
|
797
|
+
same internal source node/parameter.
|
|
798
|
+
|
|
799
|
+
For incoming connections (is_incoming=True):
|
|
800
|
+
Looks for a left-side proxy that has an incoming connection from the
|
|
801
|
+
same external source node/parameter.
|
|
802
|
+
|
|
803
|
+
Args:
|
|
804
|
+
source_node: The source node of the connection
|
|
805
|
+
source_parameter: The source parameter of the connection
|
|
806
|
+
is_incoming: True if looking for incoming connection proxies
|
|
807
|
+
|
|
808
|
+
Returns:
|
|
809
|
+
The existing proxy parameter if found, None otherwise
|
|
810
|
+
"""
|
|
811
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
812
|
+
|
|
813
|
+
connections = GriptapeNodes.FlowManager().get_connections()
|
|
814
|
+
|
|
815
|
+
# Determine which proxy parameters to check based on direction
|
|
816
|
+
if is_incoming:
|
|
817
|
+
proxy_param_names = self.metadata.get("left_parameters", [])
|
|
818
|
+
else:
|
|
819
|
+
proxy_param_names = self.metadata.get("right_parameters", [])
|
|
820
|
+
|
|
821
|
+
for proxy_name in proxy_param_names:
|
|
822
|
+
proxy_param = self.get_parameter_by_name(proxy_name)
|
|
823
|
+
if proxy_param is None:
|
|
824
|
+
continue
|
|
825
|
+
|
|
826
|
+
# Check incoming connections to this proxy parameter
|
|
827
|
+
incoming_to_proxy = connections.get_incoming_connections_to_parameter(self, proxy_param)
|
|
828
|
+
for conn in incoming_to_proxy:
|
|
829
|
+
if conn.source_node.name == source_node.name and conn.source_parameter.name == source_parameter.name:
|
|
830
|
+
return proxy_param
|
|
831
|
+
|
|
832
|
+
return None
|
|
833
|
+
|
|
834
|
+
def _create_connections_for_proxy_single(
|
|
835
|
+
self, proxy_parameter: Parameter, old_connection: Connection, *, is_incoming: bool
|
|
836
|
+
) -> None:
|
|
837
|
+
"""Create connections for a single external connection through a proxy parameter.
|
|
838
|
+
|
|
839
|
+
Unlike create_connections_for_proxy, this assumes the proxy parameter already exists
|
|
840
|
+
and is being shared by multiple connections.
|
|
841
|
+
|
|
842
|
+
Args:
|
|
843
|
+
proxy_parameter: The proxy parameter to connect through
|
|
844
|
+
old_connection: The original connection being remapped
|
|
845
|
+
is_incoming: True if this is an incoming connection to the group
|
|
846
|
+
"""
|
|
847
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
848
|
+
|
|
849
|
+
create_first_connection = CreateConnectionRequest(
|
|
850
|
+
source_parameter_name=old_connection.source_parameter.name,
|
|
851
|
+
target_parameter_name=proxy_parameter.name,
|
|
852
|
+
source_node_name=old_connection.source_node.name,
|
|
853
|
+
target_node_name=self.name,
|
|
854
|
+
is_node_group_internal=not is_incoming,
|
|
855
|
+
)
|
|
856
|
+
create_second_connection = CreateConnectionRequest(
|
|
857
|
+
source_parameter_name=proxy_parameter.name,
|
|
858
|
+
target_parameter_name=old_connection.target_parameter.name,
|
|
859
|
+
source_node_name=self.name,
|
|
860
|
+
target_node_name=old_connection.target_node.name,
|
|
861
|
+
is_node_group_internal=is_incoming,
|
|
862
|
+
)
|
|
863
|
+
|
|
864
|
+
# Track connections for cleanup
|
|
865
|
+
if proxy_parameter.name not in self._proxy_param_to_connections:
|
|
866
|
+
self._proxy_param_to_connections[proxy_parameter.name] = 2
|
|
867
|
+
else:
|
|
868
|
+
self._proxy_param_to_connections[proxy_parameter.name] += 2
|
|
869
|
+
|
|
870
|
+
GriptapeNodes.handle_request(create_first_connection)
|
|
871
|
+
GriptapeNodes.handle_request(create_second_connection)
|
|
872
|
+
|
|
873
|
+
def _validate_nodes_in_group(self, nodes: list[BaseNode]) -> None:
|
|
874
|
+
"""Validate that all nodes are in the group."""
|
|
875
|
+
for node in nodes:
|
|
876
|
+
if node.name not in self.nodes:
|
|
877
|
+
msg = f"Node {node.name} is not in node group {self.name}"
|
|
878
|
+
raise ValueError(msg)
|
|
879
|
+
|
|
880
|
+
def delete_nodes_from_group(self, nodes: list[BaseNode]) -> None:
|
|
881
|
+
"""Delete nodes from the group and untrack their connections.
|
|
882
|
+
|
|
883
|
+
Args:
|
|
884
|
+
nodes: List of nodes to delete from the group
|
|
885
|
+
"""
|
|
886
|
+
for node in nodes:
|
|
887
|
+
self.nodes.pop(node.name)
|
|
888
|
+
self.metadata["node_names_in_group"] = list(self.nodes.keys())
|
|
889
|
+
|
|
890
|
+
def remove_nodes_from_group(self, nodes: list[BaseNode]) -> None:
|
|
891
|
+
"""Remove nodes from the group and untrack their connections.
|
|
892
|
+
|
|
893
|
+
Args:
|
|
894
|
+
nodes: List of nodes to remove from the group
|
|
895
|
+
"""
|
|
896
|
+
from griptape_nodes.retained_mode.events.node_events import (
|
|
897
|
+
MoveNodeToNewFlowRequest,
|
|
898
|
+
MoveNodeToNewFlowResultSuccess,
|
|
899
|
+
)
|
|
900
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
901
|
+
|
|
902
|
+
self._validate_nodes_in_group(nodes)
|
|
903
|
+
|
|
904
|
+
parent_flow_name = None
|
|
905
|
+
try:
|
|
906
|
+
parent_flow_name = GriptapeNodes.NodeManager().get_node_parent_flow_by_name(self.name)
|
|
907
|
+
except KeyError:
|
|
908
|
+
logger.warning("%s has no parent flow, cannot move nodes back", self.name)
|
|
909
|
+
|
|
910
|
+
connections = GriptapeNodes.FlowManager().get_connections()
|
|
911
|
+
for node in nodes:
|
|
912
|
+
node.parent_group = None
|
|
913
|
+
self.nodes.pop(node.name)
|
|
914
|
+
|
|
915
|
+
if parent_flow_name is not None:
|
|
916
|
+
move_request = MoveNodeToNewFlowRequest(node_name=node.name, target_flow_name=parent_flow_name)
|
|
917
|
+
move_result = GriptapeNodes.handle_request(move_request)
|
|
918
|
+
if not isinstance(move_result, MoveNodeToNewFlowResultSuccess):
|
|
919
|
+
msg = (
|
|
920
|
+
"%s failed to move node '%s' back to parent flow: %s",
|
|
921
|
+
self.name,
|
|
922
|
+
node.name,
|
|
923
|
+
move_result.result_details,
|
|
924
|
+
)
|
|
925
|
+
logger.error(msg)
|
|
926
|
+
raise RuntimeError(msg)
|
|
927
|
+
|
|
928
|
+
for node in nodes:
|
|
929
|
+
self.unmap_node_connections(node, connections)
|
|
930
|
+
|
|
931
|
+
self.metadata["node_names_in_group"] = list(self.nodes.keys())
|
|
932
|
+
|
|
933
|
+
remaining_nodes = list(self.nodes.values())
|
|
934
|
+
if remaining_nodes:
|
|
935
|
+
node_names_in_group = set(self.nodes.keys())
|
|
936
|
+
self._map_external_connections_for_nodes(remaining_nodes, connections, node_names_in_group)
|
|
937
|
+
|
|
938
|
+
async def execute_subflow(self) -> None:
|
|
939
|
+
"""Execute the subflow and propagate output values.
|
|
940
|
+
|
|
941
|
+
This helper method:
|
|
942
|
+
1. Starts the local subflow execution
|
|
943
|
+
2. Collects output values from internal nodes
|
|
944
|
+
3. Sets them on the NodeGroup's output (right) proxy parameters
|
|
945
|
+
|
|
946
|
+
Can be called by concrete subclasses in their aprocess() implementation.
|
|
947
|
+
"""
|
|
948
|
+
from griptape_nodes.retained_mode.events.execution_events import StartLocalSubflowRequest
|
|
949
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
950
|
+
|
|
951
|
+
subflow = self.metadata.get("subflow_name")
|
|
952
|
+
if subflow is not None and isinstance(subflow, str):
|
|
953
|
+
await GriptapeNodes.FlowManager().on_start_local_subflow_request(
|
|
954
|
+
StartLocalSubflowRequest(flow_name=subflow)
|
|
955
|
+
)
|
|
956
|
+
|
|
957
|
+
# After subflow execution, collect output values from internal nodes
|
|
958
|
+
# and set them on the NodeGroup's output (right) proxy parameters
|
|
959
|
+
connections = GriptapeNodes.FlowManager().get_connections()
|
|
960
|
+
|
|
961
|
+
# Get all right parameters (output parameters)
|
|
962
|
+
right_params = self.metadata.get("right_parameters", [])
|
|
963
|
+
for proxy_param_name in right_params:
|
|
964
|
+
proxy_param = self.get_parameter_by_name(proxy_param_name)
|
|
965
|
+
if proxy_param is None:
|
|
966
|
+
continue
|
|
967
|
+
|
|
968
|
+
# Find the internal node connected to this proxy parameter
|
|
969
|
+
# The internal connection goes: InternalNode -> ProxyParameter
|
|
970
|
+
incoming_connections = connections.get_incoming_connections_to_parameter(self, proxy_param)
|
|
971
|
+
if not incoming_connections:
|
|
972
|
+
continue
|
|
973
|
+
|
|
974
|
+
# Get the first connection (there should only be one internal connection)
|
|
975
|
+
for connection in incoming_connections:
|
|
976
|
+
if not connection.is_node_group_internal:
|
|
977
|
+
continue
|
|
978
|
+
|
|
979
|
+
# Get the value from the internal node's output parameter
|
|
980
|
+
internal_node = connection.source_node
|
|
981
|
+
internal_param = connection.source_parameter
|
|
982
|
+
|
|
983
|
+
if internal_param.name in internal_node.parameter_output_values:
|
|
984
|
+
value = internal_node.parameter_output_values[internal_param.name]
|
|
985
|
+
else:
|
|
986
|
+
value = internal_node.get_parameter_value(internal_param.name)
|
|
987
|
+
|
|
988
|
+
# Set the value on the NodeGroup's proxy parameter output
|
|
989
|
+
if value is not None:
|
|
990
|
+
self.parameter_output_values[proxy_param_name] = value
|
|
991
|
+
break
|
|
992
|
+
|
|
993
|
+
@abstractmethod
|
|
994
|
+
async def aprocess(self) -> None:
|
|
995
|
+
"""Execute all nodes in the group.
|
|
996
|
+
|
|
997
|
+
Must be implemented by concrete subclasses to define execution behavior.
|
|
998
|
+
"""
|
|
999
|
+
|
|
1000
|
+
def process(self) -> Any:
|
|
1001
|
+
"""Synchronous process method - not used for proxy nodes."""
|
|
1002
|
+
|
|
1003
|
+
def delete_group(self) -> str | None:
|
|
1004
|
+
nodes_to_remove = list(self.nodes.values())
|
|
1005
|
+
self.remove_nodes_from_group(nodes_to_remove)
|
|
1006
|
+
subflow_name = self.metadata.get("subflow_name")
|
|
1007
|
+
return subflow_name
|