griptape-nodes 0.65.6__py3-none-any.whl → 0.66.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.
- griptape_nodes/common/node_executor.py +352 -27
- griptape_nodes/drivers/storage/base_storage_driver.py +12 -3
- griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +18 -2
- griptape_nodes/drivers/storage/local_storage_driver.py +42 -5
- griptape_nodes/exe_types/base_iterative_nodes.py +0 -1
- griptape_nodes/exe_types/connections.py +42 -0
- griptape_nodes/exe_types/core_types.py +2 -2
- griptape_nodes/exe_types/node_groups/__init__.py +2 -1
- griptape_nodes/exe_types/node_groups/base_iterative_node_group.py +177 -0
- griptape_nodes/exe_types/node_groups/base_node_group.py +1 -0
- griptape_nodes/exe_types/node_groups/subflow_node_group.py +35 -2
- griptape_nodes/exe_types/param_types/parameter_audio.py +1 -1
- griptape_nodes/exe_types/param_types/parameter_bool.py +1 -1
- griptape_nodes/exe_types/param_types/parameter_button.py +1 -1
- griptape_nodes/exe_types/param_types/parameter_float.py +1 -1
- griptape_nodes/exe_types/param_types/parameter_image.py +1 -1
- griptape_nodes/exe_types/param_types/parameter_int.py +1 -1
- griptape_nodes/exe_types/param_types/parameter_number.py +1 -1
- griptape_nodes/exe_types/param_types/parameter_string.py +1 -1
- griptape_nodes/exe_types/param_types/parameter_three_d.py +1 -1
- griptape_nodes/exe_types/param_types/parameter_video.py +1 -1
- griptape_nodes/machines/control_flow.py +5 -4
- griptape_nodes/machines/dag_builder.py +121 -55
- griptape_nodes/machines/fsm.py +10 -0
- griptape_nodes/machines/parallel_resolution.py +39 -38
- griptape_nodes/machines/sequential_resolution.py +29 -3
- griptape_nodes/node_library/library_registry.py +41 -2
- griptape_nodes/retained_mode/events/library_events.py +147 -8
- griptape_nodes/retained_mode/events/os_events.py +12 -4
- griptape_nodes/retained_mode/managers/fitness_problems/libraries/__init__.py +2 -0
- griptape_nodes/retained_mode/managers/fitness_problems/libraries/incompatible_requirements_problem.py +34 -0
- griptape_nodes/retained_mode/managers/flow_manager.py +133 -20
- griptape_nodes/retained_mode/managers/library_manager.py +1324 -564
- griptape_nodes/retained_mode/managers/node_manager.py +9 -3
- griptape_nodes/retained_mode/managers/os_manager.py +429 -65
- griptape_nodes/retained_mode/managers/resource_types/compute_resource.py +82 -0
- griptape_nodes/retained_mode/managers/resource_types/os_resource.py +17 -0
- griptape_nodes/retained_mode/managers/static_files_manager.py +21 -8
- griptape_nodes/retained_mode/managers/version_compatibility_manager.py +3 -3
- griptape_nodes/version_compatibility/versions/v0_39_0/modified_parameters_set_removal.py +5 -5
- griptape_nodes/version_compatibility/versions/v0_65_4/__init__.py +5 -0
- griptape_nodes/version_compatibility/versions/v0_65_4/run_in_parallel_to_run_in_order.py +79 -0
- griptape_nodes/version_compatibility/versions/v0_65_5/__init__.py +5 -0
- griptape_nodes/version_compatibility/versions/v0_65_5/flux_2_removed_parameters.py +85 -0
- {griptape_nodes-0.65.6.dist-info → griptape_nodes-0.66.1.dist-info}/METADATA +1 -1
- {griptape_nodes-0.65.6.dist-info → griptape_nodes-0.66.1.dist-info}/RECORD +48 -53
- griptape_nodes/retained_mode/managers/library_lifecycle/__init__.py +0 -45
- griptape_nodes/retained_mode/managers/library_lifecycle/data_models.py +0 -191
- griptape_nodes/retained_mode/managers/library_lifecycle/library_directory.py +0 -346
- griptape_nodes/retained_mode/managers/library_lifecycle/library_fsm.py +0 -439
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/__init__.py +0 -17
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/base.py +0 -82
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/github.py +0 -116
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/local_file.py +0 -367
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/package.py +0 -104
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/sandbox.py +0 -155
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance.py +0 -18
- griptape_nodes/retained_mode/managers/library_lifecycle/library_status.py +0 -12
- {griptape_nodes-0.65.6.dist-info → griptape_nodes-0.66.1.dist-info}/WHEEL +0 -0
- {griptape_nodes-0.65.6.dist-info → griptape_nodes-0.66.1.dist-info}/entry_points.txt +0 -0
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import copy
|
|
3
4
|
import logging
|
|
4
5
|
from dataclasses import dataclass, field
|
|
5
6
|
from enum import StrEnum
|
|
6
7
|
from typing import TYPE_CHECKING
|
|
7
8
|
|
|
8
9
|
from griptape_nodes.common.directed_graph import DirectedGraph
|
|
9
|
-
from griptape_nodes.exe_types.base_iterative_nodes import BaseIterativeStartNode
|
|
10
|
+
from griptape_nodes.exe_types.base_iterative_nodes import BaseIterativeEndNode, BaseIterativeStartNode
|
|
11
|
+
from griptape_nodes.exe_types.connections import Direction
|
|
10
12
|
from griptape_nodes.exe_types.core_types import ParameterTypeBuiltin
|
|
11
13
|
from griptape_nodes.exe_types.node_types import NodeResolutionState
|
|
12
14
|
|
|
@@ -45,7 +47,7 @@ class DagBuilder:
|
|
|
45
47
|
graphs: dict[str, DirectedGraph] # Str is the name of the start node associated here.
|
|
46
48
|
node_to_reference: dict[str, DagNode]
|
|
47
49
|
graph_to_nodes: dict[str, set[str]] # Track which nodes belong to which graph
|
|
48
|
-
start_node_candidates: dict[str, set[str]]
|
|
50
|
+
start_node_candidates: dict[str, dict[str, set[str]]] # {data_node: {graph: {boundary_nodes}}}
|
|
49
51
|
|
|
50
52
|
def __init__(self) -> None:
|
|
51
53
|
self.graphs = {}
|
|
@@ -53,8 +55,31 @@ class DagBuilder:
|
|
|
53
55
|
self.graph_to_nodes = {}
|
|
54
56
|
self.start_node_candidates = {}
|
|
55
57
|
|
|
58
|
+
def _get_nodes_to_exclude_for_iterative_end(self, node: BaseNode, connections: Connections) -> set[str]:
|
|
59
|
+
"""Get nodes to exclude when collecting dependencies for BaseIterativeEndNode.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
node: The node being processed
|
|
63
|
+
connections: The connections manager
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Set of node names to exclude (empty if not BaseIterativeEndNode)
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
ValueError: If node is BaseIterativeEndNode but start_node is None
|
|
70
|
+
"""
|
|
71
|
+
if not isinstance(node, BaseIterativeEndNode):
|
|
72
|
+
return set()
|
|
73
|
+
|
|
74
|
+
if node.start_node is None:
|
|
75
|
+
error_msg = f"Error: {node.name} is not properly connected to a start node"
|
|
76
|
+
logger.error(error_msg)
|
|
77
|
+
raise ValueError(error_msg)
|
|
78
|
+
|
|
79
|
+
return self.collect_loop_body_nodes(node.start_node, node, connections)
|
|
80
|
+
|
|
56
81
|
# Complex with the inner recursive method, but it needs connections and added_nodes.
|
|
57
|
-
def add_node_with_dependencies(self, node: BaseNode, graph_name: str = "default") -> list[BaseNode]:
|
|
82
|
+
def add_node_with_dependencies(self, node: BaseNode, graph_name: str = "default") -> list[BaseNode]: # noqa: C901
|
|
58
83
|
"""Add node and all its dependencies to DAG. Returns list of added nodes."""
|
|
59
84
|
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
60
85
|
|
|
@@ -66,6 +91,9 @@ class DagBuilder:
|
|
|
66
91
|
self.graphs[graph_name] = graph
|
|
67
92
|
self.graph_to_nodes[graph_name] = set()
|
|
68
93
|
|
|
94
|
+
# Get nodes to exclude for BaseIterativeEndNode (loop body nodes)
|
|
95
|
+
nodes_to_exclude = self._get_nodes_to_exclude_for_iterative_end(node, connections)
|
|
96
|
+
|
|
69
97
|
def _add_node_recursive(current_node: BaseNode, visited: set[str], graph: DirectedGraph) -> None:
|
|
70
98
|
# Skip if already visited or already in DAG
|
|
71
99
|
if current_node.name in visited:
|
|
@@ -102,6 +130,10 @@ class DagBuilder:
|
|
|
102
130
|
|
|
103
131
|
upstream_node, _ = upstream_connection
|
|
104
132
|
|
|
133
|
+
# Skip nodes in exclusion set (for BaseIterativeEndNode loop body)
|
|
134
|
+
if upstream_node.name in nodes_to_exclude:
|
|
135
|
+
continue
|
|
136
|
+
|
|
105
137
|
# Skip already resolved nodes
|
|
106
138
|
if upstream_node.state == NodeResolutionState.RESOLVED:
|
|
107
139
|
continue
|
|
@@ -114,6 +146,14 @@ class DagBuilder:
|
|
|
114
146
|
|
|
115
147
|
_add_node_recursive(node, set(), graph)
|
|
116
148
|
|
|
149
|
+
# Special handling for BaseIterativeEndNode: add start_node as dependency
|
|
150
|
+
if isinstance(node, BaseIterativeEndNode) and node.start_node is not None:
|
|
151
|
+
# Add start_node and its dependencies
|
|
152
|
+
_add_node_recursive(node.start_node, set(), graph)
|
|
153
|
+
# Add edge from start_node to end_node
|
|
154
|
+
if node.start_node.name in graph.nodes() and node.name in graph.nodes():
|
|
155
|
+
graph.add_edge(node.start_node.name, node.name)
|
|
156
|
+
|
|
117
157
|
return added_nodes
|
|
118
158
|
|
|
119
159
|
def add_node(self, node: BaseNode, graph_name: str = "default") -> DagNode:
|
|
@@ -171,8 +211,15 @@ class DagBuilder:
|
|
|
171
211
|
if root_node == node.node_reference:
|
|
172
212
|
continue
|
|
173
213
|
|
|
214
|
+
# Skip if the root node is the end node of a start node. It's technically not downstream.
|
|
215
|
+
if (
|
|
216
|
+
isinstance(node.node_reference, BaseIterativeStartNode)
|
|
217
|
+
and root_node == node.node_reference.end_node
|
|
218
|
+
):
|
|
219
|
+
continue
|
|
220
|
+
|
|
174
221
|
# Check if the target node is in the forward path from this root
|
|
175
|
-
if
|
|
222
|
+
if connections.is_node_in_forward_control_path(root_node, node.node_reference):
|
|
176
223
|
return False # This graph could still reach the target node
|
|
177
224
|
|
|
178
225
|
# Otherwise, return true at the end of the function
|
|
@@ -250,6 +297,49 @@ class DagBuilder:
|
|
|
250
297
|
|
|
251
298
|
return nodes_in_path
|
|
252
299
|
|
|
300
|
+
@staticmethod
|
|
301
|
+
def collect_loop_body_nodes(
|
|
302
|
+
start_node: BaseIterativeStartNode, end_node: BaseIterativeEndNode, connections: Connections
|
|
303
|
+
) -> set[str]:
|
|
304
|
+
"""Collect all nodes in the loop body between start_node and end_node.
|
|
305
|
+
|
|
306
|
+
This walks through all outgoing connections from start_node and stops when
|
|
307
|
+
reaching end_node, collecting all intermediate nodes.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
start_node: The iterative start node
|
|
311
|
+
end_node: The iterative end node
|
|
312
|
+
connections: The connections manager
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
Set of node names in the loop body (between start and end, exclusive)
|
|
316
|
+
"""
|
|
317
|
+
loop_body_nodes: set[str] = set()
|
|
318
|
+
to_visit = []
|
|
319
|
+
to_visit.append(start_node)
|
|
320
|
+
visited = set()
|
|
321
|
+
while to_visit:
|
|
322
|
+
current_node = to_visit.pop(0)
|
|
323
|
+
|
|
324
|
+
if current_node.name in visited:
|
|
325
|
+
continue
|
|
326
|
+
visited.add(current_node.name)
|
|
327
|
+
# Don't add start or end nodes themselves
|
|
328
|
+
if current_node not in (start_node, end_node):
|
|
329
|
+
loop_body_nodes.add(current_node.name)
|
|
330
|
+
# Stop traversal if we've reached the end node
|
|
331
|
+
if current_node == end_node:
|
|
332
|
+
continue
|
|
333
|
+
for param in current_node.parameters:
|
|
334
|
+
downstream_connection = connections.get_connected_node(
|
|
335
|
+
current_node, param, direction=Direction.DOWNSTREAM, include_internal=False
|
|
336
|
+
)
|
|
337
|
+
if downstream_connection:
|
|
338
|
+
next_node = downstream_connection.node
|
|
339
|
+
if next_node.name not in visited:
|
|
340
|
+
to_visit.append(next_node)
|
|
341
|
+
return loop_body_nodes
|
|
342
|
+
|
|
253
343
|
@staticmethod
|
|
254
344
|
def collect_data_dependencies_for_node(
|
|
255
345
|
node: BaseNode, connections: Connections, nodes_to_exclude: set[str], visited: set[str]
|
|
@@ -302,39 +392,6 @@ class DagBuilder:
|
|
|
302
392
|
|
|
303
393
|
return dependencies
|
|
304
394
|
|
|
305
|
-
def _is_node_in_forward_path(
|
|
306
|
-
self, start_node: BaseNode, target_node: BaseNode, connections: Connections, visited: set[str] | None = None
|
|
307
|
-
) -> bool:
|
|
308
|
-
"""Check if target_node is reachable from start_node through control flow connections."""
|
|
309
|
-
if visited is None:
|
|
310
|
-
visited = set()
|
|
311
|
-
|
|
312
|
-
if start_node.name in visited:
|
|
313
|
-
return False
|
|
314
|
-
visited.add(start_node.name)
|
|
315
|
-
|
|
316
|
-
# Check ALL outgoing control connections, not just get_next_control_output()
|
|
317
|
-
# This handles IfElse nodes that have multiple possible control outputs
|
|
318
|
-
if start_node.name in connections.outgoing_index:
|
|
319
|
-
for param_name, connection_ids in connections.outgoing_index[start_node.name].items():
|
|
320
|
-
# Find the parameter to check if it's a control type
|
|
321
|
-
param = start_node.get_parameter_by_name(param_name)
|
|
322
|
-
if param and param.output_type == ParameterTypeBuiltin.CONTROL_TYPE.value:
|
|
323
|
-
# This is a control parameter - check all its connections
|
|
324
|
-
for connection_id in connection_ids:
|
|
325
|
-
if connection_id in connections.connections:
|
|
326
|
-
connection = connections.connections[connection_id]
|
|
327
|
-
next_node = connection.target_node
|
|
328
|
-
|
|
329
|
-
if next_node.name == target_node.name:
|
|
330
|
-
return True
|
|
331
|
-
|
|
332
|
-
# Recursively check the forward path
|
|
333
|
-
if self._is_node_in_forward_path(next_node, target_node, connections, visited):
|
|
334
|
-
return True
|
|
335
|
-
|
|
336
|
-
return False
|
|
337
|
-
|
|
338
395
|
def cleanup_empty_graph_nodes(self, graph_name: str) -> None:
|
|
339
396
|
"""Remove nodes from node_to_reference when their graph becomes empty (only in single node resolution)."""
|
|
340
397
|
if graph_name in self.graph_to_nodes:
|
|
@@ -342,21 +399,30 @@ class DagBuilder:
|
|
|
342
399
|
self.node_to_reference.pop(node_name, None)
|
|
343
400
|
self.graph_to_nodes.pop(graph_name, None)
|
|
344
401
|
|
|
345
|
-
def
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
402
|
+
def remove_node_from_dependencies(self, completed_node: str, graph_name: str) -> list[str]:
|
|
403
|
+
"""Remove completed node from all dependencies, return nodes ready to execute.
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
completed_node: Name of the node that just completed
|
|
407
|
+
graph_name: Name of the graph the node completed in
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
List of data node names that are now ready to execute
|
|
411
|
+
"""
|
|
412
|
+
newly_available = []
|
|
413
|
+
|
|
414
|
+
for data_node, graph_deps in copy.deepcopy(list(self.start_node_candidates.items())):
|
|
415
|
+
if graph_name in graph_deps:
|
|
416
|
+
# Remove the completed node from this graph's boundary nodes
|
|
417
|
+
graph_deps[graph_name].discard(completed_node)
|
|
418
|
+
|
|
419
|
+
# If all boundary nodes from this graph have completed, remove the graph dependency
|
|
420
|
+
if len(graph_deps[graph_name]) == 0:
|
|
421
|
+
del graph_deps[graph_name]
|
|
422
|
+
|
|
423
|
+
# If all graph dependencies are satisfied, the data node is ready
|
|
424
|
+
if len(graph_deps) == 0:
|
|
425
|
+
del self.start_node_candidates[data_node]
|
|
426
|
+
newly_available.append(data_node)
|
|
427
|
+
|
|
428
|
+
return newly_available
|
griptape_nodes/machines/fsm.py
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
|
+
from enum import StrEnum
|
|
1
2
|
from typing import Any, TypeVar
|
|
2
3
|
|
|
3
4
|
T = TypeVar("T")
|
|
4
5
|
|
|
5
6
|
|
|
7
|
+
class WorkflowState(StrEnum):
|
|
8
|
+
"""Workflow execution states."""
|
|
9
|
+
|
|
10
|
+
NO_ERROR = "no_error"
|
|
11
|
+
WORKFLOW_COMPLETE = "workflow_complete"
|
|
12
|
+
ERRORED = "errored"
|
|
13
|
+
CANCELED = "canceled"
|
|
14
|
+
|
|
15
|
+
|
|
6
16
|
class State:
|
|
7
17
|
@staticmethod
|
|
8
18
|
async def on_enter(context: Any) -> type["State"] | None: # noqa: ARG004
|
|
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import logging
|
|
5
|
-
from enum import StrEnum
|
|
6
5
|
from typing import TYPE_CHECKING
|
|
7
6
|
|
|
8
7
|
from griptape_nodes.exe_types.base_iterative_nodes import BaseIterativeEndNode, BaseIterativeStartNode
|
|
@@ -14,7 +13,7 @@ from griptape_nodes.exe_types.node_types import (
|
|
|
14
13
|
)
|
|
15
14
|
from griptape_nodes.exe_types.type_validator import TypeValidator
|
|
16
15
|
from griptape_nodes.machines.dag_builder import NodeState
|
|
17
|
-
from griptape_nodes.machines.fsm import FSM, State
|
|
16
|
+
from griptape_nodes.machines.fsm import FSM, State, WorkflowState
|
|
18
17
|
from griptape_nodes.node_library.library_registry import LibraryRegistry
|
|
19
18
|
from griptape_nodes.retained_mode.events.base_events import (
|
|
20
19
|
ExecutionEvent,
|
|
@@ -41,15 +40,6 @@ if TYPE_CHECKING:
|
|
|
41
40
|
logger = logging.getLogger("griptape_nodes")
|
|
42
41
|
|
|
43
42
|
|
|
44
|
-
class WorkflowState(StrEnum):
|
|
45
|
-
"""Workflow execution states."""
|
|
46
|
-
|
|
47
|
-
NO_ERROR = "no_error"
|
|
48
|
-
WORKFLOW_COMPLETE = "workflow_complete"
|
|
49
|
-
ERRORED = "errored"
|
|
50
|
-
CANCELED = "canceled"
|
|
51
|
-
|
|
52
|
-
|
|
53
43
|
class ParallelResolutionContext:
|
|
54
44
|
paused: bool
|
|
55
45
|
flow_name: str
|
|
@@ -60,7 +50,6 @@ class ParallelResolutionContext:
|
|
|
60
50
|
task_to_node: dict[asyncio.Task, DagNode]
|
|
61
51
|
dag_builder: DagBuilder | None
|
|
62
52
|
last_resolved_node: BaseNode | None # Track the last node that was resolved
|
|
63
|
-
last_resolved_node: BaseNode | None # Track the last node that was resolved
|
|
64
53
|
|
|
65
54
|
def __init__(
|
|
66
55
|
self, flow_name: str, max_nodes_in_parallel: int | None = None, dag_builder: DagBuilder | None = None
|
|
@@ -116,7 +105,21 @@ class ParallelResolutionContext:
|
|
|
116
105
|
|
|
117
106
|
class ExecuteDagState(State):
|
|
118
107
|
@staticmethod
|
|
119
|
-
|
|
108
|
+
def check_for_new_start_nodes(
|
|
109
|
+
context: ParallelResolutionContext, current_node_name: str, network_name: str
|
|
110
|
+
) -> None:
|
|
111
|
+
# Remove this node from dependencies and get newly available nodes
|
|
112
|
+
if context.dag_builder is not None:
|
|
113
|
+
newly_available = context.dag_builder.remove_node_from_dependencies(current_node_name, network_name)
|
|
114
|
+
for data_node_name in newly_available:
|
|
115
|
+
data_node = GriptapeNodes.NodeManager().get_node_by_name(data_node_name)
|
|
116
|
+
added_nodes = context.dag_builder.add_node_with_dependencies(data_node, data_node_name)
|
|
117
|
+
if added_nodes:
|
|
118
|
+
for added_node in added_nodes:
|
|
119
|
+
ExecuteDagState._try_queue_waiting_node(context, added_node.name)
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
async def handle_done_nodes(context: ParallelResolutionContext, done_node: DagNode, network_name: str) -> None:
|
|
120
123
|
current_node = done_node.node_reference
|
|
121
124
|
|
|
122
125
|
# Check if node was already resolved (shouldn't happen)
|
|
@@ -194,13 +197,7 @@ class ExecuteDagState(State):
|
|
|
194
197
|
)
|
|
195
198
|
# Now the final thing to do, is to take their directed graph and update it.
|
|
196
199
|
ExecuteDagState.get_next_control_graph(context, current_node, network_name)
|
|
197
|
-
|
|
198
|
-
if len(graph.nodes()) == 0 and context.dag_builder is not None:
|
|
199
|
-
# remove from dependencies. This is so we can potentially queue the data node.
|
|
200
|
-
data_start_nodes = context.dag_builder.remove_graph_from_dependencies()
|
|
201
|
-
for data_start_node_name in data_start_nodes:
|
|
202
|
-
data_start_node = GriptapeNodes.NodeManager().get_node_by_name(data_start_node_name)
|
|
203
|
-
context.dag_builder.add_node_with_dependencies(data_start_node, network_name)
|
|
200
|
+
ExecuteDagState.check_for_new_start_nodes(context, current_node.name, network_name)
|
|
204
201
|
|
|
205
202
|
@staticmethod
|
|
206
203
|
def get_next_control_graph(context: ParallelResolutionContext, node: BaseNode, network_name: str) -> None:
|
|
@@ -227,7 +224,8 @@ class ExecuteDagState(State):
|
|
|
227
224
|
if network is None:
|
|
228
225
|
msg = f"Network {network_name} not found in DAG builder"
|
|
229
226
|
raise ValueError(msg)
|
|
230
|
-
|
|
227
|
+
is_isolated = context.dag_builder is not flow_manager.global_dag_builder
|
|
228
|
+
if flow_manager.global_single_node_resolution and not is_isolated:
|
|
231
229
|
# Clean up nodes from emptied graphs in single node resolution mode
|
|
232
230
|
if len(network) == 0 and context.dag_builder is not None:
|
|
233
231
|
context.dag_builder.cleanup_empty_graph_nodes(network_name)
|
|
@@ -354,7 +352,7 @@ class ExecuteDagState(State):
|
|
|
354
352
|
)
|
|
355
353
|
)
|
|
356
354
|
if isinstance(result, SetParameterValueResultFailure):
|
|
357
|
-
msg = f"Failed to set value for
|
|
355
|
+
msg = f"Failed to set parameter value for node '{current_node.name}' and parameter '{parameter.name}'. Details: {result.result_details}"
|
|
358
356
|
logger.error(msg)
|
|
359
357
|
raise RuntimeError(msg)
|
|
360
358
|
|
|
@@ -390,7 +388,9 @@ class ExecuteDagState(State):
|
|
|
390
388
|
networks = context.networks
|
|
391
389
|
handled_nodes = set() # Track nodes we've already processed to avoid duplicates
|
|
392
390
|
|
|
393
|
-
|
|
391
|
+
# Create a copy of items to avoid "dictionary changed size during iteration" error
|
|
392
|
+
# This is necessary because handle_done_nodes can add new networks via the DAG builder
|
|
393
|
+
for network_name, network in list(networks.items()):
|
|
394
394
|
# Check and see if there are leaf nodes that are cancelled.
|
|
395
395
|
# Reinitialize leaf nodes since maybe we changed things up.
|
|
396
396
|
# We removed nodes from the network. There may be new leaf nodes.
|
|
@@ -481,8 +481,10 @@ class ExecuteDagState(State):
|
|
|
481
481
|
node_reference.node_reference.parameter_output_values.silent_clear()
|
|
482
482
|
exceptions = node_reference.node_reference.validate_before_node_run()
|
|
483
483
|
if exceptions:
|
|
484
|
-
msg = f"
|
|
485
|
-
logger.error(msg)
|
|
484
|
+
msg = f"Node '{node_reference.node_reference.name}' encountered problems: {exceptions}"
|
|
485
|
+
logger.error("Canceling flow run. %s", msg)
|
|
486
|
+
context.error_message = msg
|
|
487
|
+
context.workflow_state = WorkflowState.ERRORED
|
|
486
488
|
return ErrorState
|
|
487
489
|
|
|
488
490
|
# We've set up the node for success completely. Now we check and handle accordingly if it's a for-each-start node
|
|
@@ -497,6 +499,8 @@ class ExecuteDagState(State):
|
|
|
497
499
|
f"Cannot have a Start Loop Node without an End Loop Node: {node_reference.node_reference.name}"
|
|
498
500
|
)
|
|
499
501
|
logger.error(msg)
|
|
502
|
+
context.error_message = msg
|
|
503
|
+
context.workflow_state = WorkflowState.ERRORED
|
|
500
504
|
return ErrorState
|
|
501
505
|
# We're going to skip straight to the end node here instead.
|
|
502
506
|
# Set end node to node reference
|
|
@@ -551,18 +555,12 @@ class ExecuteDagState(State):
|
|
|
551
555
|
exc = task.exception()
|
|
552
556
|
dag_node = context.task_to_node.get(task)
|
|
553
557
|
node_name = dag_node.node_reference.name if dag_node else "Unknown"
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
"Task execution failed for node '%s' (type: %s) in flow '%s'. Exception: %s",
|
|
558
|
-
node_name,
|
|
559
|
-
node_type,
|
|
560
|
-
context.flow_name,
|
|
561
|
-
exc,
|
|
562
|
-
)
|
|
558
|
+
|
|
559
|
+
logger.exception("Error processing node '%s'", node_name)
|
|
560
|
+
msg = f"Node '{node_name}' encountered a problem: {exc}"
|
|
563
561
|
|
|
564
562
|
context.task_to_node.pop(task)
|
|
565
|
-
context.error_message =
|
|
563
|
+
context.error_message = msg
|
|
566
564
|
context.workflow_state = WorkflowState.ERRORED
|
|
567
565
|
return ErrorState
|
|
568
566
|
context.task_to_node.pop(task)
|
|
@@ -578,9 +576,6 @@ class ExecuteDagState(State):
|
|
|
578
576
|
class ErrorState(State):
|
|
579
577
|
@staticmethod
|
|
580
578
|
async def on_enter(context: ParallelResolutionContext) -> type[State] | None:
|
|
581
|
-
if context.error_message:
|
|
582
|
-
logger.error("DAG execution error: %s", context.error_message)
|
|
583
|
-
|
|
584
579
|
for node in context.node_to_reference.values():
|
|
585
580
|
# Cancel all nodes that haven't yet begun processing.
|
|
586
581
|
if node.node_state == NodeState.QUEUED:
|
|
@@ -676,3 +671,9 @@ class ParallelResolutionMachine(FSM[ParallelResolutionContext]):
|
|
|
676
671
|
def get_last_resolved_node(self) -> BaseNode | None:
|
|
677
672
|
"""Get the last node that was resolved in the DAG execution."""
|
|
678
673
|
return self._context.last_resolved_node
|
|
674
|
+
|
|
675
|
+
def is_errored(self) -> bool:
|
|
676
|
+
return self._context.workflow_state == WorkflowState.ERRORED
|
|
677
|
+
|
|
678
|
+
def get_error_message(self) -> str | None:
|
|
679
|
+
return self._context.error_message
|
|
@@ -9,7 +9,7 @@ from griptape_nodes.exe_types.connections import Direction
|
|
|
9
9
|
from griptape_nodes.exe_types.core_types import ParameterTypeBuiltin
|
|
10
10
|
from griptape_nodes.exe_types.node_types import BaseNode, NodeResolutionState
|
|
11
11
|
from griptape_nodes.exe_types.type_validator import TypeValidator
|
|
12
|
-
from griptape_nodes.machines.fsm import FSM, State
|
|
12
|
+
from griptape_nodes.machines.fsm import FSM, State, WorkflowState
|
|
13
13
|
from griptape_nodes.node_library.library_registry import LibraryRegistry
|
|
14
14
|
from griptape_nodes.retained_mode.events.base_events import (
|
|
15
15
|
ExecutionEvent,
|
|
@@ -42,10 +42,14 @@ class Focus:
|
|
|
42
42
|
class ResolutionContext:
|
|
43
43
|
focus_stack: list[Focus]
|
|
44
44
|
paused: bool
|
|
45
|
+
error_message: str | None
|
|
46
|
+
workflow_state: WorkflowState
|
|
45
47
|
|
|
46
48
|
def __init__(self) -> None:
|
|
47
49
|
self.focus_stack = []
|
|
48
50
|
self.paused = False
|
|
51
|
+
self.error_message = None
|
|
52
|
+
self.workflow_state = WorkflowState.NO_ERROR
|
|
49
53
|
|
|
50
54
|
@property
|
|
51
55
|
def current_node(self) -> BaseNode:
|
|
@@ -62,6 +66,8 @@ class ResolutionContext:
|
|
|
62
66
|
node.clear_node()
|
|
63
67
|
self.focus_stack.clear()
|
|
64
68
|
self.paused = False
|
|
69
|
+
self.error_message = None
|
|
70
|
+
self.workflow_state = WorkflowState.NO_ERROR
|
|
65
71
|
|
|
66
72
|
|
|
67
73
|
class InitializeSpotlightState(State):
|
|
@@ -260,7 +266,10 @@ class ExecuteNodeState(State):
|
|
|
260
266
|
|
|
261
267
|
exceptions = current_node.validate_before_node_run()
|
|
262
268
|
if exceptions:
|
|
263
|
-
msg = f"
|
|
269
|
+
msg = f"Node '{current_node.name}' encountered problems: {exceptions}"
|
|
270
|
+
logger.error("Canceling flow run. %s", msg)
|
|
271
|
+
context.error_message = msg
|
|
272
|
+
context.workflow_state = WorkflowState.ERRORED
|
|
264
273
|
# Mark the node as unresolved, broadcasting to everyone.
|
|
265
274
|
raise RuntimeError(msg)
|
|
266
275
|
if not context.paused:
|
|
@@ -268,7 +277,7 @@ class ExecuteNodeState(State):
|
|
|
268
277
|
return None
|
|
269
278
|
|
|
270
279
|
@staticmethod
|
|
271
|
-
async def on_update(context: ResolutionContext) -> type[State] | None:
|
|
280
|
+
async def on_update(context: ResolutionContext) -> type[State] | None: # noqa: PLR0915
|
|
272
281
|
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
273
282
|
|
|
274
283
|
# Once everything has been set
|
|
@@ -305,10 +314,21 @@ class ExecuteNodeState(State):
|
|
|
305
314
|
wrapped_event=ExecutionEvent(payload=NodeFinishProcessEvent(node_name=current_node.name))
|
|
306
315
|
)
|
|
307
316
|
)
|
|
317
|
+
msg = f"Node '{current_node.name}' encountered a problem that cancelled the task."
|
|
318
|
+
context.error_message = msg
|
|
319
|
+
context.workflow_state = WorkflowState.ERRORED
|
|
320
|
+
# Mark the node as unresolved, broadcasting to everyone.
|
|
321
|
+
current_node.make_node_unresolved(
|
|
322
|
+
current_states_to_trigger_change_event=set(
|
|
323
|
+
{NodeResolutionState.UNRESOLVED, NodeResolutionState.RESOLVED, NodeResolutionState.RESOLVING}
|
|
324
|
+
)
|
|
325
|
+
)
|
|
308
326
|
return CompleteState
|
|
309
327
|
except Exception as e:
|
|
310
328
|
logger.exception("Error processing node '%s", current_node.name)
|
|
311
329
|
msg = f"Node '{current_node.name}' encountered a problem: {e}"
|
|
330
|
+
context.error_message = msg
|
|
331
|
+
context.workflow_state = WorkflowState.ERRORED
|
|
312
332
|
# Mark the node as unresolved, broadcasting to everyone.
|
|
313
333
|
current_node.make_node_unresolved(
|
|
314
334
|
current_states_to_trigger_change_event=set(
|
|
@@ -449,6 +469,12 @@ class SequentialResolutionMachine(FSM[ResolutionContext]):
|
|
|
449
469
|
def is_started(self) -> bool:
|
|
450
470
|
return self._current_state is not None
|
|
451
471
|
|
|
472
|
+
def is_errored(self) -> bool:
|
|
473
|
+
return self._context.workflow_state == WorkflowState.ERRORED
|
|
474
|
+
|
|
475
|
+
def get_error_message(self) -> str | None:
|
|
476
|
+
return self._context.error_message
|
|
477
|
+
|
|
452
478
|
# Unused argument but necessary for parallel_resolution because of futures ending during cancel but not reset.
|
|
453
479
|
def reset_machine(self, *, cancel: bool = False) -> None: # noqa: ARG002
|
|
454
480
|
self._context.reset()
|
|
@@ -3,11 +3,14 @@ from __future__ import annotations
|
|
|
3
3
|
import logging
|
|
4
4
|
from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple
|
|
5
5
|
|
|
6
|
-
from pydantic import BaseModel, Field
|
|
6
|
+
from pydantic import BaseModel, Field, field_validator
|
|
7
7
|
|
|
8
8
|
from griptape_nodes.retained_mode.managers.fitness_problems.libraries.duplicate_node_registration_problem import (
|
|
9
9
|
DuplicateNodeRegistrationProblem,
|
|
10
10
|
)
|
|
11
|
+
from griptape_nodes.retained_mode.managers.resource_components.resource_instance import (
|
|
12
|
+
Requirements, # noqa: TC001 (putting this into type checking causes it to not be defined for Pydantic field_validator)
|
|
13
|
+
)
|
|
11
14
|
from griptape_nodes.utils.metaclasses import SingletonMeta
|
|
12
15
|
|
|
13
16
|
if TYPE_CHECKING:
|
|
@@ -34,6 +37,40 @@ class Dependencies(BaseModel):
|
|
|
34
37
|
pip_install_flags: list[str] | None = None
|
|
35
38
|
|
|
36
39
|
|
|
40
|
+
class ResourceRequirements(BaseModel):
|
|
41
|
+
"""Resource requirements for a library.
|
|
42
|
+
|
|
43
|
+
Specifies what system resources (OS, compute backends) the library needs.
|
|
44
|
+
Example: {"platform": (["linux", "windows"], "has_any"), "arch": "x86_64", "compute": (["cuda", "cpu"], "has_all")}
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
required: Requirements | None = None
|
|
48
|
+
|
|
49
|
+
@field_validator("required", mode="before")
|
|
50
|
+
@classmethod
|
|
51
|
+
def convert_lists_to_tuples(cls, v: Any) -> Any:
|
|
52
|
+
"""Convert list values to tuples for requirements loaded from JSON.
|
|
53
|
+
|
|
54
|
+
JSON arrays become Python lists, but the Requirements type expects tuples
|
|
55
|
+
for (value, comparator) pairs.
|
|
56
|
+
"""
|
|
57
|
+
if v is None:
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
if not isinstance(v, dict):
|
|
61
|
+
return v
|
|
62
|
+
|
|
63
|
+
converted = {}
|
|
64
|
+
comparator_tuple_length = 2
|
|
65
|
+
for key, value in v.items():
|
|
66
|
+
# Check if value is a list with exactly 2 elements where second is a string (comparator)
|
|
67
|
+
if isinstance(value, list) and len(value) == comparator_tuple_length and isinstance(value[1], str):
|
|
68
|
+
converted[key] = tuple(value)
|
|
69
|
+
else:
|
|
70
|
+
converted[key] = value
|
|
71
|
+
return converted
|
|
72
|
+
|
|
73
|
+
|
|
37
74
|
class LibraryMetadata(BaseModel):
|
|
38
75
|
"""Metadata that explains details about the library, including versioning and search details."""
|
|
39
76
|
|
|
@@ -45,6 +82,8 @@ class LibraryMetadata(BaseModel):
|
|
|
45
82
|
dependencies: Dependencies | None = None
|
|
46
83
|
# If True, this library will be surfaced to Griptape Nodes customers when listing Node Libraries available to them.
|
|
47
84
|
is_griptape_nodes_searchable: bool = True
|
|
85
|
+
# Resource requirements for this library. If None, library is assumed to work on any platform.
|
|
86
|
+
resources: ResourceRequirements | None = None
|
|
48
87
|
|
|
49
88
|
|
|
50
89
|
class IconVariant(BaseModel):
|
|
@@ -112,7 +151,7 @@ class LibrarySchema(BaseModel):
|
|
|
112
151
|
library itself.
|
|
113
152
|
"""
|
|
114
153
|
|
|
115
|
-
LATEST_SCHEMA_VERSION: ClassVar[str] = "0.
|
|
154
|
+
LATEST_SCHEMA_VERSION: ClassVar[str] = "0.4.0"
|
|
116
155
|
|
|
117
156
|
name: str
|
|
118
157
|
library_schema_version: str
|