griptape-nodes 0.63.10__py3-none-any.whl → 0.64.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. griptape_nodes/common/node_executor.py +95 -171
  2. griptape_nodes/exe_types/connections.py +51 -2
  3. griptape_nodes/exe_types/flow.py +3 -3
  4. griptape_nodes/exe_types/node_types.py +330 -202
  5. griptape_nodes/exe_types/param_components/artifact_url/__init__.py +1 -0
  6. griptape_nodes/exe_types/param_components/artifact_url/public_artifact_url_parameter.py +155 -0
  7. griptape_nodes/exe_types/param_components/progress_bar_component.py +1 -1
  8. griptape_nodes/exe_types/param_types/parameter_string.py +27 -0
  9. griptape_nodes/machines/control_flow.py +64 -203
  10. griptape_nodes/machines/dag_builder.py +85 -238
  11. griptape_nodes/machines/parallel_resolution.py +9 -236
  12. griptape_nodes/machines/sequential_resolution.py +133 -11
  13. griptape_nodes/retained_mode/events/agent_events.py +2 -0
  14. griptape_nodes/retained_mode/events/flow_events.py +5 -6
  15. griptape_nodes/retained_mode/events/node_events.py +151 -1
  16. griptape_nodes/retained_mode/events/workflow_events.py +10 -0
  17. griptape_nodes/retained_mode/managers/agent_manager.py +33 -1
  18. griptape_nodes/retained_mode/managers/flow_manager.py +213 -290
  19. griptape_nodes/retained_mode/managers/library_manager.py +24 -7
  20. griptape_nodes/retained_mode/managers/node_manager.py +400 -77
  21. griptape_nodes/retained_mode/managers/version_compatibility_manager.py +113 -69
  22. griptape_nodes/retained_mode/managers/workflow_manager.py +45 -10
  23. griptape_nodes/servers/mcp.py +32 -0
  24. griptape_nodes/version_compatibility/versions/v0_63_8/__init__.py +1 -0
  25. griptape_nodes/version_compatibility/versions/v0_63_8/deprecated_nodegroup_parameters.py +105 -0
  26. {griptape_nodes-0.63.10.dist-info → griptape_nodes-0.64.1.dist-info}/METADATA +3 -1
  27. {griptape_nodes-0.63.10.dist-info → griptape_nodes-0.64.1.dist-info}/RECORD +31 -28
  28. griptape_nodes/version_compatibility/workflow_versions/__init__.py +0 -1
  29. /griptape_nodes/version_compatibility/{workflow_versions → versions}/v0_7_0/__init__.py +0 -0
  30. /griptape_nodes/version_compatibility/{workflow_versions → versions}/v0_7_0/local_executor_argument_addition.py +0 -0
  31. {griptape_nodes-0.63.10.dist-info → griptape_nodes-0.64.1.dist-info}/WHEEL +0 -0
  32. {griptape_nodes-0.63.10.dist-info → griptape_nodes-0.64.1.dist-info}/entry_points.txt +0 -0
@@ -7,13 +7,13 @@ from typing import TYPE_CHECKING
7
7
 
8
8
  from griptape_nodes.common.directed_graph import DirectedGraph
9
9
  from griptape_nodes.exe_types.core_types import ParameterTypeBuiltin
10
- from griptape_nodes.exe_types.node_types import NodeResolutionState
10
+ from griptape_nodes.exe_types.node_types import LOCAL_EXECUTION, NodeGroupNode, NodeResolutionState
11
11
 
12
12
  if TYPE_CHECKING:
13
13
  import asyncio
14
14
 
15
15
  from griptape_nodes.exe_types.connections import Connections
16
- from griptape_nodes.exe_types.node_types import BaseNode, NodeGroup
16
+ from griptape_nodes.exe_types.node_types import BaseNode
17
17
 
18
18
  logger = logging.getLogger("griptape_nodes")
19
19
 
@@ -50,6 +50,50 @@ class DagBuilder:
50
50
  self.node_to_reference: dict[str, DagNode] = {}
51
51
  self.graph_to_nodes = {}
52
52
 
53
+ @staticmethod
54
+ def _should_use_parent_group(node: BaseNode) -> bool:
55
+ """Check if a node's parent group should be used for DAG edges.
56
+
57
+ Returns True if the node has a parent NodeGroupNode that is NOT in LOCAL_EXECUTION mode.
58
+ In LOCAL_EXECUTION mode, groups are transparent and children are treated as separate nodes.
59
+ """
60
+ if not isinstance(node.parent_group, NodeGroupNode):
61
+ return False
62
+ parent_execution_env = node.parent_group.get_parameter_value(node.parent_group.execution_environment.name)
63
+ return parent_execution_env != LOCAL_EXECUTION
64
+
65
+ def _get_node_for_dag_edge(self, node: BaseNode, graph: DirectedGraph, graph_name: str) -> BaseNode:
66
+ """Get the node to use for DAG edges - either the node itself or its parent group.
67
+
68
+ Args:
69
+ node: The original node
70
+ graph: The graph being built
71
+ graph_name: Name of the graph for tracking
72
+
73
+ Returns:
74
+ The node or parent group to use in DAG edges
75
+ """
76
+ if self._should_use_parent_group(node):
77
+ parent_group = node.parent_group
78
+ if isinstance(parent_group, NodeGroupNode):
79
+ self._ensure_group_node_in_dag(parent_group, graph, graph_name)
80
+ return parent_group
81
+ return node
82
+
83
+ def _ensure_group_node_in_dag(self, group_node: NodeGroupNode, graph: DirectedGraph, graph_name: str) -> None:
84
+ """Ensure a NodeGroupNode is added to the DAG if not already present.
85
+
86
+ Args:
87
+ group_node: The NodeGroupNode to add
88
+ graph: The graph to add it to
89
+ graph_name: Name of the graph for tracking
90
+ """
91
+ if group_node.name not in self.node_to_reference:
92
+ dag_node = DagNode(node_reference=group_node, node_state=NodeState.WAITING)
93
+ self.node_to_reference[group_node.name] = dag_node
94
+ graph.add_node(node_for_adding=group_node.name)
95
+ self.graph_to_nodes[graph_name].add(group_node.name)
96
+
53
97
  # Complex with the inner recursive method, but it needs connections and added_nodes.
54
98
  def add_node_with_dependencies(self, node: BaseNode, graph_name: str = "default") -> list[BaseNode]: # noqa: C901
55
99
  """Add node and all its dependencies to DAG. Returns list of added nodes."""
@@ -64,48 +108,62 @@ class DagBuilder:
64
108
  self.graph_to_nodes[graph_name] = set()
65
109
 
66
110
  def _add_node_recursive(current_node: BaseNode, visited: set[str], graph: DirectedGraph) -> None:
111
+ # Skip if already visited or already in DAG
67
112
  if current_node.name in visited:
68
113
  return
69
114
  visited.add(current_node.name)
70
- # Skip if already in DAG (use DAG membership, not resolved state)
115
+
71
116
  if current_node.name in self.node_to_reference:
72
117
  return
73
- # Process dependencies first (depth-first)
74
- ignore_data_dependencies = False
75
- # This is specifically for output_selector. Overriding 'initialize_spotlight' doesn't work anymore.
76
- if hasattr(current_node, "ignore_dependencies"):
77
- ignore_data_dependencies = True
118
+
119
+ # Check if we should ignore dependencies (for special nodes like output_selector)
120
+ ignore_data_dependencies = hasattr(current_node, "ignore_dependencies")
121
+
122
+ # Process all upstream dependencies first (depth-first traversal)
78
123
  for param in current_node.parameters:
124
+ # Skip control flow parameters
79
125
  if param.type == ParameterTypeBuiltin.CONTROL_TYPE:
80
126
  continue
127
+
128
+ # Skip if node ignores dependencies
81
129
  if ignore_data_dependencies:
82
130
  continue
131
+
83
132
  upstream_connection = connections.get_connected_node(current_node, param)
84
- if upstream_connection:
85
- upstream_node, _ = upstream_connection
86
- # Don't add nodes that have already been resolved.
87
- if upstream_node.state == NodeResolutionState.RESOLVED:
88
- continue
89
- # If upstream is already in DAG, skip creating edge (it's in another graph)
90
- if upstream_node.name in self.node_to_reference:
91
- graph.add_edge(upstream_node.name, current_node.name)
92
- # Otherwise, add it to DAG first then create edge
93
- else:
94
- _add_node_recursive(upstream_node, visited, graph)
95
- graph.add_edge(upstream_node.name, current_node.name)
133
+ if not upstream_connection:
134
+ continue
96
135
 
97
- # Add current node to DAG (but keep original resolution state)
136
+ upstream_node, _ = upstream_connection
98
137
 
99
- dag_node = DagNode(node_reference=current_node, node_state=NodeState.WAITING)
100
- self.node_to_reference[current_node.name] = dag_node
101
- graph.add_node(node_for_adding=current_node.name)
138
+ # Skip already resolved nodes
139
+ if upstream_node.state == NodeResolutionState.RESOLVED:
140
+ continue
141
+
142
+ # Check for internal group connections - traverse but don't add edge
143
+ is_internal_connection = (
144
+ self._should_use_parent_group(current_node)
145
+ and upstream_node.parent_group == current_node.parent_group
146
+ )
147
+
148
+ # Recursively add upstream node
149
+ _add_node_recursive(upstream_node, visited, graph)
102
150
 
103
- # Track which nodes belong to this graph
104
- self.graph_to_nodes[graph_name].add(current_node.name)
151
+ # Add edge unless it's an internal group connection
152
+ if not is_internal_connection:
153
+ upstream_for_edge = self._get_node_for_dag_edge(upstream_node, graph, graph_name)
154
+ current_for_edge = self._get_node_for_dag_edge(current_node, graph, graph_name)
155
+ graph.add_edge(upstream_for_edge.name, current_for_edge.name)
105
156
 
106
- # DON'T mark as resolved - that happens during actual execution
157
+ # Always add current node to tracking (even if parent group is used for edges)
158
+ dag_node = DagNode(node_reference=current_node, node_state=NodeState.WAITING)
159
+ self.node_to_reference[current_node.name] = dag_node
107
160
  added_nodes.append(current_node)
108
161
 
162
+ # Add to graph if not using parent group
163
+ if not self._should_use_parent_group(current_node):
164
+ graph.add_node(node_for_adding=current_node.name)
165
+ self.graph_to_nodes[graph_name].add(current_node.name)
166
+
109
167
  _add_node_recursive(node, set(), graph)
110
168
 
111
169
  return added_nodes
@@ -224,214 +282,3 @@ class DagBuilder:
224
282
  for node_name in self.graph_to_nodes[graph_name]:
225
283
  self.node_to_reference.pop(node_name, None)
226
284
  self.graph_to_nodes.pop(graph_name, None)
227
-
228
- def identify_and_process_node_groups(self) -> dict[str, BaseNode]:
229
- """Identify node groups, validate them, and replace with proxy nodes.
230
-
231
- Scans all nodes in the DAG for non-empty node_group parameter values,
232
- creates NodeGroup instances, validates they have no intermediate ungrouped nodes,
233
- and replaces them with NodeGroupProxyNode instances in the DAG.
234
-
235
- Returns:
236
- Dictionary mapping group IDs to their proxy nodes
237
-
238
- Raises:
239
- ValueError: If validation fails (e.g., ungrouped nodes between grouped nodes)
240
- """
241
- from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
242
-
243
- connections = GriptapeNodes.FlowManager().get_connections()
244
-
245
- # Step 1: Identify groups by scanning all nodes
246
- groups = self._identify_node_groups(connections)
247
-
248
- if not groups:
249
- return {}
250
-
251
- # Step 2: Validate each group
252
- for group in groups.values():
253
- group.validate_no_intermediate_nodes(connections.connections)
254
-
255
- # Step 3: Create proxy nodes and replace groups in DAG
256
- proxy_nodes = {}
257
- for group_id, group in groups.items():
258
- proxy_node = self._create_and_install_proxy_node(group_id, group, connections)
259
- proxy_nodes[group_id] = proxy_node
260
-
261
- return proxy_nodes
262
-
263
- def _identify_node_groups(self, connections: Connections) -> dict[str, NodeGroup]:
264
- """Identify and build NodeGroup instances from nodes in the DAG.
265
-
266
- Args:
267
- connections: Connections object for analyzing connection topology
268
-
269
- Returns:
270
- Dictionary mapping group IDs to NodeGroup instances
271
- """
272
- from griptape_nodes.exe_types.node_types import NodeGroup
273
-
274
- groups: dict[str, NodeGroup] = {}
275
-
276
- # Scan all nodes in DAG for group membership
277
- for dag_node in self.node_to_reference.values():
278
- node = dag_node.node_reference
279
- group_id = node.get_parameter_value("job_group")
280
-
281
- # Skip nodes without group assignment or empty group ID
282
- if not group_id or group_id == "":
283
- continue
284
-
285
- # Create group if it doesn't exist
286
- if group_id not in groups:
287
- groups[group_id] = NodeGroup(group_id=group_id)
288
-
289
- # Add node to group
290
- groups[group_id].add_node(node)
291
-
292
- # Analyze connections for each group
293
- for group in groups.values():
294
- self._analyze_group_connections(group, connections)
295
-
296
- return groups
297
-
298
- def _analyze_group_connections(self, group: NodeGroup, connections: Connections) -> None:
299
- """Analyze and categorize connections for a node group.
300
-
301
- Categorizes all connections involving group nodes as either:
302
- - Internal: Both endpoints within the group
303
- - External incoming: From outside node to group node
304
- - External outgoing: From group node to outside node
305
-
306
- Args:
307
- group: NodeGroup to analyze
308
- connections: Connections object containing all flow connections
309
- """
310
- node_names_in_group = set(group.nodes.keys())
311
-
312
- # Analyze all connections in the flow
313
- for conn in connections.connections.values():
314
- source_in_group = conn.source_node.name in node_names_in_group
315
- target_in_group = conn.target_node.name in node_names_in_group
316
-
317
- if source_in_group and target_in_group:
318
- # Both endpoints in group - internal connection
319
- group.internal_connections.append(conn)
320
- elif source_in_group and not target_in_group:
321
- # From group to outside - external outgoing
322
- group.external_outgoing_connections.append(conn)
323
- elif not source_in_group and target_in_group:
324
- # From outside to group - external incoming
325
- group.external_incoming_connections.append(conn)
326
-
327
- def _create_and_install_proxy_node(self, group_id: str, group: NodeGroup, connections: Connections) -> BaseNode:
328
- """Create a proxy node for a group and install it in the DAG.
329
-
330
- Creates a NodeGroupProxyNode, adds it to the DAG, remaps all external
331
- connections to point to the proxy, and removes the original grouped
332
- nodes from the DAG.
333
-
334
- Args:
335
- group_id: Unique identifier for the group
336
- group: NodeGroup instance to replace
337
- connections: Connections object for remapping connections
338
-
339
- Returns:
340
- The created NodeGroupProxyNode
341
- """
342
- from griptape_nodes.exe_types.node_types import NodeGroupProxyNode
343
-
344
- # Create proxy node with unique name
345
- proxy_name = f"__group_proxy_{group_id}"
346
- proxy_node = NodeGroupProxyNode(name=proxy_name, node_group=group)
347
-
348
- # Determine which graph to add proxy to (use first grouped node's graph)
349
- target_graph_name = None
350
- for graph_name, node_set in self.graph_to_nodes.items():
351
- if any(node_name in node_set for node_name in group.nodes):
352
- target_graph_name = graph_name
353
- break
354
-
355
- if target_graph_name is None:
356
- target_graph_name = "default"
357
-
358
- # Add proxy node to DAG
359
- self.add_node(proxy_node, target_graph_name)
360
-
361
- # Remap external connections to proxy
362
- self._remap_connections_to_proxy(group, proxy_node, connections)
363
-
364
- # Remove grouped nodes from DAG
365
- self._remove_grouped_nodes_from_dag(group)
366
-
367
- return proxy_node
368
-
369
- def _remap_connections_to_proxy(self, group: NodeGroup, proxy_node: BaseNode, connections: Connections) -> None:
370
- """Remap external connections from group nodes to the proxy node.
371
-
372
- Updates the connection indices and Connection objects to redirect
373
- external connections through the proxy node instead of the original
374
- grouped nodes.
375
-
376
- Args:
377
- group: NodeGroup being replaced
378
- proxy_node: Proxy node that will handle external connections
379
- connections: Connections object to update
380
- """
381
- # Remap external incoming connections (from outside -> group becomes outside -> proxy)
382
- for conn in group.external_incoming_connections:
383
- conn_id = id(conn)
384
-
385
- # Remove old incoming index entry
386
- if (
387
- conn.target_node.name in connections.incoming_index
388
- and conn.target_parameter.name in connections.incoming_index[conn.target_node.name]
389
- ):
390
- connections.incoming_index[conn.target_node.name][conn.target_parameter.name].remove(conn_id)
391
-
392
- # Update connection target to proxy
393
- conn.target_node = proxy_node
394
-
395
- # Add new incoming index entry
396
- connections.incoming_index.setdefault(proxy_node.name, {}).setdefault(
397
- conn.target_parameter.name, []
398
- ).append(conn_id)
399
-
400
- # Remap external outgoing connections (group -> outside becomes proxy -> outside)
401
- for conn in group.external_outgoing_connections:
402
- conn_id = id(conn)
403
-
404
- # Remove old outgoing index entry
405
- if (
406
- conn.source_node.name in connections.outgoing_index
407
- and conn.source_parameter.name in connections.outgoing_index[conn.source_node.name]
408
- ):
409
- connections.outgoing_index[conn.source_node.name][conn.source_parameter.name].remove(conn_id)
410
-
411
- # Update connection source to proxy
412
- conn.source_node = proxy_node
413
-
414
- # Add new outgoing index entry
415
- connections.outgoing_index.setdefault(proxy_node.name, {}).setdefault(
416
- conn.source_parameter.name, []
417
- ).append(conn_id)
418
-
419
- def _remove_grouped_nodes_from_dag(self, group: NodeGroup) -> None:
420
- """Remove all nodes in a group from the DAG graphs and references.
421
-
422
- Args:
423
- group: NodeGroup whose nodes should be removed from the DAG
424
- """
425
- for node_name in group.nodes:
426
- # Remove from node_to_reference
427
- if node_name in self.node_to_reference:
428
- del self.node_to_reference[node_name]
429
-
430
- # Remove from all graphs
431
- for graph in self.graphs.values():
432
- if node_name in graph.nodes():
433
- graph.remove_node(node_name)
434
-
435
- # Remove from graph_to_nodes tracking
436
- for node_set in self.graph_to_nodes.values():
437
- node_set.discard(node_name)