griptape-nodes 0.53.0__py3-none-any.whl → 0.54.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/__init__.py +5 -2
- griptape_nodes/app/app.py +4 -26
- griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +35 -5
- griptape_nodes/bootstrap/workflow_executors/workflow_executor.py +15 -1
- griptape_nodes/cli/commands/config.py +4 -1
- griptape_nodes/cli/commands/init.py +5 -3
- griptape_nodes/cli/commands/libraries.py +14 -8
- griptape_nodes/cli/commands/models.py +504 -0
- griptape_nodes/cli/commands/self.py +5 -2
- griptape_nodes/cli/main.py +11 -1
- griptape_nodes/cli/shared.py +0 -9
- griptape_nodes/common/directed_graph.py +17 -1
- griptape_nodes/drivers/storage/base_storage_driver.py +40 -20
- griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +24 -29
- griptape_nodes/drivers/storage/local_storage_driver.py +17 -13
- griptape_nodes/exe_types/node_types.py +219 -14
- griptape_nodes/exe_types/param_components/__init__.py +1 -0
- griptape_nodes/exe_types/param_components/execution_status_component.py +138 -0
- griptape_nodes/machines/control_flow.py +129 -92
- griptape_nodes/machines/dag_builder.py +207 -0
- griptape_nodes/machines/parallel_resolution.py +264 -276
- griptape_nodes/machines/sequential_resolution.py +9 -7
- griptape_nodes/node_library/library_registry.py +34 -1
- griptape_nodes/retained_mode/events/app_events.py +5 -1
- griptape_nodes/retained_mode/events/base_events.py +7 -7
- griptape_nodes/retained_mode/events/config_events.py +30 -0
- griptape_nodes/retained_mode/events/execution_events.py +2 -2
- griptape_nodes/retained_mode/events/model_events.py +296 -0
- griptape_nodes/retained_mode/griptape_nodes.py +10 -1
- griptape_nodes/retained_mode/managers/agent_manager.py +14 -0
- griptape_nodes/retained_mode/managers/config_manager.py +44 -3
- griptape_nodes/retained_mode/managers/event_manager.py +8 -2
- griptape_nodes/retained_mode/managers/flow_manager.py +45 -14
- griptape_nodes/retained_mode/managers/library_manager.py +3 -3
- griptape_nodes/retained_mode/managers/model_manager.py +1107 -0
- griptape_nodes/retained_mode/managers/node_manager.py +26 -26
- griptape_nodes/retained_mode/managers/object_manager.py +1 -1
- griptape_nodes/retained_mode/managers/os_manager.py +6 -6
- griptape_nodes/retained_mode/managers/settings.py +87 -9
- griptape_nodes/retained_mode/managers/static_files_manager.py +77 -9
- griptape_nodes/retained_mode/managers/sync_manager.py +10 -5
- griptape_nodes/retained_mode/managers/workflow_manager.py +101 -92
- griptape_nodes/retained_mode/retained_mode.py +19 -0
- griptape_nodes/servers/__init__.py +1 -0
- griptape_nodes/{mcp_server/server.py → servers/mcp.py} +1 -1
- griptape_nodes/{app/api.py → servers/static.py} +43 -40
- griptape_nodes/traits/button.py +124 -6
- griptape_nodes/traits/multi_options.py +188 -0
- griptape_nodes/traits/numbers_selector.py +77 -0
- griptape_nodes/traits/options.py +93 -2
- griptape_nodes/utils/async_utils.py +31 -0
- {griptape_nodes-0.53.0.dist-info → griptape_nodes-0.54.1.dist-info}/METADATA +3 -1
- {griptape_nodes-0.53.0.dist-info → griptape_nodes-0.54.1.dist-info}/RECORD +56 -47
- {griptape_nodes-0.53.0.dist-info → griptape_nodes-0.54.1.dist-info}/WHEEL +1 -1
- /griptape_nodes/{mcp_server → servers}/ws_request_manager.py +0 -0
- {griptape_nodes-0.53.0.dist-info → griptape_nodes-0.54.1.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from griptape_nodes.exe_types.core_types import (
|
|
4
|
+
Parameter,
|
|
5
|
+
ParameterGroup,
|
|
6
|
+
ParameterMode,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ExecutionStatusComponent:
|
|
11
|
+
"""A reusable component for managing execution status parameters.
|
|
12
|
+
|
|
13
|
+
This component creates and manages a "Status" ParameterGroup containing:
|
|
14
|
+
- was_successful: Boolean parameter indicating success/failure
|
|
15
|
+
- result_details: String parameter with operation details
|
|
16
|
+
|
|
17
|
+
The component can be customized for different parameter modes to support
|
|
18
|
+
various node types (EndNode uses INPUT/PROPERTY, SuccessFailureNode uses OUTPUT).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__( # noqa: PLR0913
|
|
22
|
+
self,
|
|
23
|
+
node: Any, # BaseNode type, but avoiding circular import
|
|
24
|
+
*,
|
|
25
|
+
was_successful_modes: set[ParameterMode],
|
|
26
|
+
result_details_modes: set[ParameterMode],
|
|
27
|
+
parameter_group_initially_collapsed: bool = True,
|
|
28
|
+
result_details_tooltip: str = "Details about the operation result",
|
|
29
|
+
result_details_placeholder: str = "Details on the operation will be presented here.",
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Initialize the ExecutionStatusComponent and create the parameters immediately.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
node: The node instance that will own these parameters
|
|
35
|
+
was_successful_modes: Set of ParameterModes for was_successful parameter
|
|
36
|
+
result_details_modes: Set of ParameterModes for result_details parameter
|
|
37
|
+
parameter_group_initially_collapsed: Whether the Status group should start collapsed
|
|
38
|
+
result_details_tooltip: Custom tooltip for result_details parameter
|
|
39
|
+
result_details_placeholder: Custom placeholder text for result_details parameter
|
|
40
|
+
"""
|
|
41
|
+
self._node = node
|
|
42
|
+
|
|
43
|
+
# Create the Status ParameterGroup
|
|
44
|
+
self._status_group = ParameterGroup(name="Status")
|
|
45
|
+
self._status_group.ui_options = {"collapsed": parameter_group_initially_collapsed}
|
|
46
|
+
|
|
47
|
+
# Boolean parameter to indicate success/failure
|
|
48
|
+
self._was_successful = Parameter(
|
|
49
|
+
name="was_successful",
|
|
50
|
+
tooltip="Indicates whether it completed without errors.",
|
|
51
|
+
type="bool",
|
|
52
|
+
default_value=False,
|
|
53
|
+
settable=False,
|
|
54
|
+
allowed_modes=was_successful_modes,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Result details parameter with multiline option
|
|
58
|
+
self._result_details = Parameter(
|
|
59
|
+
name="result_details",
|
|
60
|
+
tooltip=result_details_tooltip,
|
|
61
|
+
type="str",
|
|
62
|
+
default_value=None,
|
|
63
|
+
allowed_modes=result_details_modes,
|
|
64
|
+
settable=False,
|
|
65
|
+
ui_options={
|
|
66
|
+
"multiline": True,
|
|
67
|
+
"placeholder_text": result_details_placeholder,
|
|
68
|
+
},
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Add parameters to the group
|
|
72
|
+
self._status_group.add_child(self._was_successful)
|
|
73
|
+
self._status_group.add_child(self._result_details)
|
|
74
|
+
|
|
75
|
+
# Add the group to the node
|
|
76
|
+
self._node.add_node_element(self._status_group)
|
|
77
|
+
|
|
78
|
+
def get_parameter_group(self) -> ParameterGroup:
|
|
79
|
+
"""Get the Status ParameterGroup.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
ParameterGroup: The Status group containing was_successful and result_details
|
|
83
|
+
"""
|
|
84
|
+
return self._status_group
|
|
85
|
+
|
|
86
|
+
def set_execution_result(self, *, was_successful: bool, result_details: str) -> None:
|
|
87
|
+
"""Set the execution result values.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
was_successful: Whether the operation succeeded
|
|
91
|
+
result_details: Details about the operation result
|
|
92
|
+
"""
|
|
93
|
+
self._update_parameter_value(self._was_successful, was_successful)
|
|
94
|
+
self._update_parameter_value(self._result_details, result_details)
|
|
95
|
+
|
|
96
|
+
def clear_execution_status(self, initial_message: str | None = None) -> None:
|
|
97
|
+
"""Clear execution status and reset parameters.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
initial_message: Initial message to set in result_details. If None, clears result_details entirely.
|
|
101
|
+
"""
|
|
102
|
+
if initial_message is None:
|
|
103
|
+
initial_message = ""
|
|
104
|
+
self.set_execution_result(was_successful=False, result_details=initial_message)
|
|
105
|
+
|
|
106
|
+
def append_to_result_details(self, additional_text: str, separator: str = "\n") -> None:
|
|
107
|
+
"""Append text to the existing result_details.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
additional_text: Text to append to the current result_details
|
|
111
|
+
separator: Separator to use between existing and new text (default: newline)
|
|
112
|
+
"""
|
|
113
|
+
# Get current result_details value
|
|
114
|
+
current_details = self._node.get_parameter_value(self._result_details.name)
|
|
115
|
+
|
|
116
|
+
# Append the new text
|
|
117
|
+
if current_details:
|
|
118
|
+
updated_details = f"{current_details}{separator}{additional_text}"
|
|
119
|
+
else:
|
|
120
|
+
updated_details = additional_text
|
|
121
|
+
|
|
122
|
+
# Use consolidated update method
|
|
123
|
+
self._update_parameter_value(self._result_details, updated_details)
|
|
124
|
+
|
|
125
|
+
def _update_parameter_value(self, parameter: Parameter, value: Any) -> None:
|
|
126
|
+
"""Update a parameter value with all necessary operations.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
parameter: The parameter to update
|
|
130
|
+
value: The new value to set
|
|
131
|
+
"""
|
|
132
|
+
# ALWAYS set parameter value and publish update
|
|
133
|
+
self._node.set_parameter_value(parameter.name, value)
|
|
134
|
+
self._node.publish_update_to_parameter(parameter.name, value)
|
|
135
|
+
|
|
136
|
+
# ONLY set output values if the parameter mode is OUTPUT
|
|
137
|
+
if ParameterMode.OUTPUT in parameter.get_mode():
|
|
138
|
+
self._node.parameter_output_values[parameter.name] = value
|
|
@@ -39,7 +39,7 @@ logger = logging.getLogger("griptape_nodes")
|
|
|
39
39
|
# This is the control flow context. Owns the Resolution Machine
|
|
40
40
|
class ControlFlowContext:
|
|
41
41
|
flow: ControlFlow
|
|
42
|
-
|
|
42
|
+
current_nodes: list[BaseNode]
|
|
43
43
|
resolution_machine: ParallelResolutionMachine | SequentialResolutionMachine
|
|
44
44
|
selected_output: Parameter | None
|
|
45
45
|
paused: bool = False
|
|
@@ -54,35 +54,57 @@ class ControlFlowContext:
|
|
|
54
54
|
) -> None:
|
|
55
55
|
self.flow_name = flow_name
|
|
56
56
|
if execution_type == WorkflowExecutionMode.PARALLEL:
|
|
57
|
-
|
|
57
|
+
# Get the global DagBuilder from FlowManager
|
|
58
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
59
|
+
|
|
60
|
+
dag_builder = GriptapeNodes.FlowManager().global_dag_builder
|
|
61
|
+
self.resolution_machine = ParallelResolutionMachine(
|
|
62
|
+
flow_name, max_nodes_in_parallel, dag_builder=dag_builder
|
|
63
|
+
)
|
|
58
64
|
else:
|
|
59
65
|
self.resolution_machine = SequentialResolutionMachine()
|
|
60
|
-
self.
|
|
66
|
+
self.current_nodes = []
|
|
61
67
|
|
|
62
|
-
def
|
|
63
|
-
"""Get
|
|
68
|
+
def get_next_nodes(self, output_parameter: Parameter | None = None) -> list[NextNodeInfo]:
|
|
69
|
+
"""Get all next nodes from the current nodes.
|
|
64
70
|
|
|
65
71
|
Returns:
|
|
66
|
-
NextNodeInfo
|
|
72
|
+
list[NextNodeInfo]: List of next nodes to process
|
|
67
73
|
"""
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
next_nodes = []
|
|
75
|
+
for current_node in self.current_nodes:
|
|
76
|
+
if output_parameter is not None:
|
|
77
|
+
# Get connected node from control flow
|
|
78
|
+
node_connection = (
|
|
79
|
+
GriptapeNodes.FlowManager().get_connections().get_connected_node(current_node, output_parameter)
|
|
80
|
+
)
|
|
81
|
+
if node_connection is not None:
|
|
82
|
+
node, entry_parameter = node_connection
|
|
83
|
+
next_nodes.append(NextNodeInfo(node=node, entry_parameter=entry_parameter))
|
|
84
|
+
else:
|
|
85
|
+
# Get next control output for this node
|
|
86
|
+
next_output = current_node.get_next_control_output()
|
|
87
|
+
if next_output is not None:
|
|
88
|
+
node_connection = (
|
|
89
|
+
GriptapeNodes.FlowManager().get_connections().get_connected_node(current_node, next_output)
|
|
90
|
+
)
|
|
91
|
+
if node_connection is not None:
|
|
92
|
+
node, entry_parameter = node_connection
|
|
93
|
+
next_nodes.append(NextNodeInfo(node=node, entry_parameter=entry_parameter))
|
|
94
|
+
|
|
95
|
+
# If no connections found, check execution queue
|
|
96
|
+
if not next_nodes:
|
|
77
97
|
node = GriptapeNodes.FlowManager().get_next_node_from_execution_queue()
|
|
78
98
|
if node is not None:
|
|
79
|
-
|
|
80
|
-
|
|
99
|
+
next_nodes.append(NextNodeInfo(node=node, entry_parameter=None))
|
|
100
|
+
|
|
101
|
+
return next_nodes
|
|
81
102
|
|
|
82
103
|
def reset(self, *, cancel: bool = False) -> None:
|
|
83
|
-
if self.
|
|
84
|
-
self.
|
|
85
|
-
|
|
104
|
+
if self.current_nodes is not None:
|
|
105
|
+
for node in self.current_nodes:
|
|
106
|
+
node.clear_node()
|
|
107
|
+
self.current_nodes = []
|
|
86
108
|
self.resolution_machine.reset_machine(cancel=cancel)
|
|
87
109
|
self.selected_output = None
|
|
88
110
|
self.paused = False
|
|
@@ -93,24 +115,25 @@ class ResolveNodeState(State):
|
|
|
93
115
|
@staticmethod
|
|
94
116
|
async def on_enter(context: ControlFlowContext) -> type[State] | None:
|
|
95
117
|
# The state machine has started, but it hasn't began to execute yet.
|
|
96
|
-
if context.
|
|
118
|
+
if len(context.current_nodes) == 0:
|
|
97
119
|
# We don't have anything else to do. Move back to Complete State so it has to restart.
|
|
98
120
|
return CompleteState
|
|
99
121
|
|
|
100
|
-
# Mark
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
122
|
+
# Mark all current nodes unresolved and broadcast events
|
|
123
|
+
for current_node in context.current_nodes:
|
|
124
|
+
if not current_node.lock:
|
|
125
|
+
current_node.make_node_unresolved(
|
|
126
|
+
current_states_to_trigger_change_event=set(
|
|
127
|
+
{NodeResolutionState.UNRESOLVED, NodeResolutionState.RESOLVED, NodeResolutionState.RESOLVING}
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
# Now broadcast that we have a current control node.
|
|
131
|
+
GriptapeNodes.EventManager().put_event(
|
|
132
|
+
ExecutionGriptapeNodeEvent(
|
|
133
|
+
wrapped_event=ExecutionEvent(payload=CurrentControlNodeEvent(node_name=current_node.name))
|
|
105
134
|
)
|
|
106
135
|
)
|
|
107
|
-
|
|
108
|
-
GriptapeNodes.EventManager().put_event(
|
|
109
|
-
ExecutionGriptapeNodeEvent(
|
|
110
|
-
wrapped_event=ExecutionEvent(payload=CurrentControlNodeEvent(node_name=context.current_node.name))
|
|
111
|
-
)
|
|
112
|
-
)
|
|
113
|
-
logger.info("Resolving %s", context.current_node.name)
|
|
136
|
+
logger.info("Resolving %s", current_node.name)
|
|
114
137
|
if not context.paused:
|
|
115
138
|
# Call the update. Otherwise wait
|
|
116
139
|
return ResolveNodeState
|
|
@@ -119,13 +142,17 @@ class ResolveNodeState(State):
|
|
|
119
142
|
# This is necessary to transition to the next step.
|
|
120
143
|
@staticmethod
|
|
121
144
|
async def on_update(context: ControlFlowContext) -> type[State] | None:
|
|
122
|
-
# If
|
|
123
|
-
if context.
|
|
145
|
+
# If no current nodes, we're done
|
|
146
|
+
if len(context.current_nodes) == 0:
|
|
124
147
|
return CompleteState
|
|
125
|
-
|
|
126
|
-
|
|
148
|
+
|
|
149
|
+
# Resolve nodes - pass first node for sequential resolution
|
|
150
|
+
current_node = context.current_nodes[0] if context.current_nodes else None
|
|
151
|
+
await context.resolution_machine.resolve_node(current_node)
|
|
127
152
|
|
|
128
153
|
if context.resolution_machine.is_complete():
|
|
154
|
+
if isinstance(context.resolution_machine, ParallelResolutionMachine):
|
|
155
|
+
return CompleteState
|
|
129
156
|
return NextNodeState
|
|
130
157
|
return None
|
|
131
158
|
|
|
@@ -133,44 +160,49 @@ class ResolveNodeState(State):
|
|
|
133
160
|
class NextNodeState(State):
|
|
134
161
|
@staticmethod
|
|
135
162
|
async def on_enter(context: ControlFlowContext) -> type[State] | None:
|
|
136
|
-
if context.
|
|
163
|
+
if len(context.current_nodes) == 0:
|
|
137
164
|
return CompleteState
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
165
|
+
|
|
166
|
+
# Check for stop_flow on any current nodes
|
|
167
|
+
for current_node in context.current_nodes[:]:
|
|
168
|
+
if current_node.stop_flow:
|
|
169
|
+
current_node.stop_flow = False
|
|
170
|
+
context.current_nodes.remove(current_node)
|
|
171
|
+
|
|
172
|
+
# If all nodes stopped flow, complete
|
|
173
|
+
if len(context.current_nodes) == 0:
|
|
142
174
|
return CompleteState
|
|
143
|
-
next_output = context.current_node.get_next_control_output()
|
|
144
|
-
next_node_info = None
|
|
145
175
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
176
|
+
# Get all next nodes from current nodes
|
|
177
|
+
next_node_infos = context.get_next_nodes()
|
|
178
|
+
|
|
179
|
+
# Broadcast selected control output events for nodes with outputs
|
|
180
|
+
for current_node in context.current_nodes:
|
|
181
|
+
next_output = current_node.get_next_control_output()
|
|
182
|
+
if next_output is not None:
|
|
183
|
+
context.selected_output = next_output
|
|
184
|
+
GriptapeNodes.EventManager().put_event(
|
|
185
|
+
ExecutionGriptapeNodeEvent(
|
|
186
|
+
wrapped_event=ExecutionEvent(
|
|
187
|
+
payload=SelectedControlOutputEvent(
|
|
188
|
+
node_name=current_node.name,
|
|
189
|
+
selected_output_parameter_name=next_output.name,
|
|
190
|
+
)
|
|
155
191
|
)
|
|
156
192
|
)
|
|
157
193
|
)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
next_node = GriptapeNodes.FlowManager().get_next_node_from_execution_queue()
|
|
162
|
-
if next_node is not None:
|
|
163
|
-
next_node_info = NextNodeInfo(node=next_node, entry_parameter=None)
|
|
164
|
-
|
|
165
|
-
# The parameter that will be evaluated next
|
|
166
|
-
if next_node_info is None:
|
|
167
|
-
# If no node attached
|
|
194
|
+
|
|
195
|
+
# If no next nodes, we're complete
|
|
196
|
+
if not next_node_infos:
|
|
168
197
|
return CompleteState
|
|
169
198
|
|
|
170
|
-
#
|
|
171
|
-
|
|
199
|
+
# Set up next nodes as current nodes
|
|
200
|
+
next_nodes = []
|
|
201
|
+
for next_node_info in next_node_infos:
|
|
202
|
+
next_node_info.node.set_entry_control_parameter(next_node_info.entry_parameter)
|
|
203
|
+
next_nodes.append(next_node_info.node)
|
|
172
204
|
|
|
173
|
-
context.
|
|
205
|
+
context.current_nodes = next_nodes
|
|
174
206
|
context.selected_output = None
|
|
175
207
|
if not context.paused:
|
|
176
208
|
return ResolveNodeState
|
|
@@ -184,15 +216,14 @@ class NextNodeState(State):
|
|
|
184
216
|
class CompleteState(State):
|
|
185
217
|
@staticmethod
|
|
186
218
|
async def on_enter(context: ControlFlowContext) -> type[State] | None:
|
|
187
|
-
|
|
219
|
+
# Broadcast completion events for any remaining current nodes
|
|
220
|
+
for current_node in context.current_nodes:
|
|
188
221
|
GriptapeNodes.EventManager().put_event(
|
|
189
222
|
ExecutionGriptapeNodeEvent(
|
|
190
223
|
wrapped_event=ExecutionEvent(
|
|
191
224
|
payload=ControlFlowResolvedEvent(
|
|
192
|
-
end_node_name=
|
|
193
|
-
parameter_output_values=TypeValidator.safe_serialize(
|
|
194
|
-
context.current_node.parameter_output_values
|
|
195
|
-
),
|
|
225
|
+
end_node_name=current_node.name,
|
|
226
|
+
parameter_output_values=TypeValidator.safe_serialize(current_node.parameter_output_values),
|
|
196
227
|
)
|
|
197
228
|
)
|
|
198
229
|
)
|
|
@@ -218,10 +249,13 @@ class ControlFlowMachine(FSM[ControlFlowContext]):
|
|
|
218
249
|
async def start_flow(self, start_node: BaseNode, debug_mode: bool = False) -> None: # noqa: FBT001, FBT002
|
|
219
250
|
# If using DAG resolution, process data_nodes from queue first
|
|
220
251
|
if isinstance(self._context.resolution_machine, ParallelResolutionMachine):
|
|
221
|
-
await self.
|
|
222
|
-
|
|
252
|
+
current_nodes = await self._process_nodes_for_dag(start_node)
|
|
253
|
+
else:
|
|
254
|
+
current_nodes = [start_node]
|
|
255
|
+
self._context.current_nodes = current_nodes
|
|
223
256
|
# Set entry control parameter for initial node (None for workflow start)
|
|
224
|
-
|
|
257
|
+
for node in current_nodes:
|
|
258
|
+
node.set_entry_control_parameter(None)
|
|
225
259
|
# Set up to debug
|
|
226
260
|
self._context.paused = debug_mode
|
|
227
261
|
await self.start(ResolveNodeState) # Begins the flow
|
|
@@ -266,38 +300,41 @@ class ControlFlowMachine(FSM[ControlFlowContext]):
|
|
|
266
300
|
):
|
|
267
301
|
await self.update()
|
|
268
302
|
|
|
269
|
-
async def
|
|
303
|
+
async def _process_nodes_for_dag(self, start_node: BaseNode) -> list[BaseNode]:
|
|
270
304
|
"""Process data_nodes from the global queue to build unified DAG.
|
|
271
305
|
|
|
272
306
|
This method identifies data_nodes in the execution queue and processes
|
|
273
307
|
their dependencies into the DAG resolution machine.
|
|
274
308
|
"""
|
|
275
309
|
if not isinstance(self._context.resolution_machine, ParallelResolutionMachine):
|
|
276
|
-
return
|
|
310
|
+
return []
|
|
277
311
|
# Get the global flow queue
|
|
278
312
|
flow_manager = GriptapeNodes.FlowManager()
|
|
313
|
+
dag_builder = flow_manager.global_dag_builder
|
|
314
|
+
if dag_builder is None:
|
|
315
|
+
msg = "DAG builder is not initialized."
|
|
316
|
+
raise ValueError(msg)
|
|
317
|
+
# Build with the first node:
|
|
318
|
+
dag_builder.add_node_with_dependencies(start_node, start_node.name)
|
|
279
319
|
queue_items = list(flow_manager.global_flow_queue.queue)
|
|
280
|
-
|
|
320
|
+
start_nodes = [start_node]
|
|
281
321
|
# Find data_nodes and remove them from queue
|
|
282
|
-
data_nodes = []
|
|
283
322
|
for item in queue_items:
|
|
284
323
|
from griptape_nodes.retained_mode.managers.flow_manager import DagExecutionType
|
|
285
324
|
|
|
286
|
-
if item.dag_execution_type
|
|
287
|
-
|
|
325
|
+
if item.dag_execution_type in (DagExecutionType.CONTROL_NODE, DagExecutionType.START_NODE):
|
|
326
|
+
node = item.node
|
|
327
|
+
node.state = NodeResolutionState.UNRESOLVED
|
|
328
|
+
dag_builder.add_node_with_dependencies(node, node.name)
|
|
288
329
|
flow_manager.global_flow_queue.queue.remove(item)
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
await self._context.resolution_machine.update()
|
|
298
|
-
|
|
299
|
-
# Reset the machine state to allow adding more nodes
|
|
300
|
-
self._context.resolution_machine.current_state = None
|
|
330
|
+
start_nodes.append(node)
|
|
331
|
+
elif item.dag_execution_type == DagExecutionType.DATA_NODE:
|
|
332
|
+
node = item.node
|
|
333
|
+
node.state = NodeResolutionState.UNRESOLVED
|
|
334
|
+
# Build here.
|
|
335
|
+
dag_builder.add_node_with_dependencies(node, node.name)
|
|
336
|
+
flow_manager.global_flow_queue.queue.remove(item)
|
|
337
|
+
return start_nodes
|
|
301
338
|
|
|
302
339
|
def reset_machine(self, *, cancel: bool = False) -> None:
|
|
303
340
|
self._context.reset(cancel=cancel)
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from griptape_nodes.common.directed_graph import DirectedGraph
|
|
9
|
+
from griptape_nodes.exe_types.core_types import ParameterTypeBuiltin
|
|
10
|
+
from griptape_nodes.exe_types.node_types import NodeResolutionState
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
import asyncio
|
|
14
|
+
|
|
15
|
+
from griptape_nodes.exe_types.connections import Connections
|
|
16
|
+
from griptape_nodes.exe_types.node_types import BaseNode
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger("griptape_nodes")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class NodeState(StrEnum):
|
|
22
|
+
"""Individual node execution states."""
|
|
23
|
+
|
|
24
|
+
QUEUED = "queued"
|
|
25
|
+
PROCESSING = "processing"
|
|
26
|
+
DONE = "done"
|
|
27
|
+
CANCELED = "canceled"
|
|
28
|
+
ERRORED = "errored"
|
|
29
|
+
WAITING = "waiting"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(kw_only=True)
|
|
33
|
+
class DagNode:
|
|
34
|
+
"""Represents a node in the DAG with runtime references."""
|
|
35
|
+
|
|
36
|
+
task_reference: asyncio.Task | None = field(default=None)
|
|
37
|
+
node_state: NodeState = field(default=NodeState.WAITING)
|
|
38
|
+
node_reference: BaseNode
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class DagBuilder:
|
|
42
|
+
"""Handles DAG construction independently of execution state machine."""
|
|
43
|
+
|
|
44
|
+
graphs: dict[str, DirectedGraph] # Str is the name of the start node associated here.
|
|
45
|
+
node_to_reference: dict[str, DagNode]
|
|
46
|
+
|
|
47
|
+
def __init__(self) -> None:
|
|
48
|
+
self.graphs = {}
|
|
49
|
+
self.node_to_reference: dict[str, DagNode] = {}
|
|
50
|
+
|
|
51
|
+
# Complex with the inner recursive method, but it needs connections and added_nodes.
|
|
52
|
+
def add_node_with_dependencies(self, node: BaseNode, graph_name: str = "default") -> list[BaseNode]: # noqa: C901
|
|
53
|
+
"""Add node and all its dependencies to DAG. Returns list of added nodes."""
|
|
54
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
55
|
+
|
|
56
|
+
connections = GriptapeNodes.FlowManager().get_connections()
|
|
57
|
+
added_nodes = []
|
|
58
|
+
graph = self.graphs.get(graph_name, None)
|
|
59
|
+
if graph is None:
|
|
60
|
+
graph = DirectedGraph()
|
|
61
|
+
self.graphs[graph_name] = graph
|
|
62
|
+
|
|
63
|
+
def _add_node_recursive(current_node: BaseNode, visited: set[str], graph: DirectedGraph) -> None:
|
|
64
|
+
if current_node.name in visited:
|
|
65
|
+
return
|
|
66
|
+
visited.add(current_node.name)
|
|
67
|
+
|
|
68
|
+
# Skip if already in DAG (use DAG membership, not resolved state)
|
|
69
|
+
if current_node.name in self.node_to_reference:
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
# Process dependencies first (depth-first)
|
|
73
|
+
ignore_data_dependencies = False
|
|
74
|
+
# This is specifically for output_selector. Overriding 'initialize_spotlight' doesn't work anymore.
|
|
75
|
+
if hasattr(current_node, "ignore_dependencies"):
|
|
76
|
+
ignore_data_dependencies = True
|
|
77
|
+
for param in current_node.parameters:
|
|
78
|
+
if param.type == ParameterTypeBuiltin.CONTROL_TYPE:
|
|
79
|
+
continue
|
|
80
|
+
if ignore_data_dependencies:
|
|
81
|
+
continue
|
|
82
|
+
upstream_connection = connections.get_connected_node(current_node, param)
|
|
83
|
+
if upstream_connection:
|
|
84
|
+
upstream_node, _ = upstream_connection
|
|
85
|
+
# Don't add nodes that have already been resolved.
|
|
86
|
+
if upstream_node.state == NodeResolutionState.RESOLVED:
|
|
87
|
+
continue
|
|
88
|
+
# If upstream is already in DAG, skip creating edge (it's in another graph)
|
|
89
|
+
if upstream_node.name in self.node_to_reference:
|
|
90
|
+
graph.add_edge(upstream_node.name, current_node.name)
|
|
91
|
+
# Otherwise, add it to DAG first then create edge
|
|
92
|
+
else:
|
|
93
|
+
_add_node_recursive(upstream_node, visited, graph)
|
|
94
|
+
graph.add_edge(upstream_node.name, current_node.name)
|
|
95
|
+
|
|
96
|
+
# Add current node to DAG (but keep original resolution state)
|
|
97
|
+
|
|
98
|
+
dag_node = DagNode(node_reference=current_node, node_state=NodeState.WAITING)
|
|
99
|
+
self.node_to_reference[current_node.name] = dag_node
|
|
100
|
+
graph.add_node(node_for_adding=current_node.name)
|
|
101
|
+
# DON'T mark as resolved - that happens during actual execution
|
|
102
|
+
added_nodes.append(current_node)
|
|
103
|
+
|
|
104
|
+
_add_node_recursive(node, set(), graph)
|
|
105
|
+
|
|
106
|
+
return added_nodes
|
|
107
|
+
|
|
108
|
+
def add_node(self, node: BaseNode, graph_name: str = "default") -> DagNode:
|
|
109
|
+
"""Add just one node to DAG without dependencies (assumes dependencies already exist)."""
|
|
110
|
+
if node.name in self.node_to_reference:
|
|
111
|
+
return self.node_to_reference[node.name]
|
|
112
|
+
|
|
113
|
+
dag_node = DagNode(node_reference=node, node_state=NodeState.WAITING)
|
|
114
|
+
self.node_to_reference[node.name] = dag_node
|
|
115
|
+
graph = self.graphs.get(graph_name, None)
|
|
116
|
+
if graph is None:
|
|
117
|
+
graph = DirectedGraph()
|
|
118
|
+
self.graphs[graph_name] = graph
|
|
119
|
+
graph.add_node(node_for_adding=node.name)
|
|
120
|
+
return dag_node
|
|
121
|
+
|
|
122
|
+
def clear(self) -> None:
|
|
123
|
+
"""Clear all nodes and references from the DAG builder."""
|
|
124
|
+
self.graphs.clear()
|
|
125
|
+
self.node_to_reference.clear()
|
|
126
|
+
|
|
127
|
+
def can_queue_control_node(self, node: DagNode) -> bool:
|
|
128
|
+
if len(self.graphs) == 1:
|
|
129
|
+
return True
|
|
130
|
+
|
|
131
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
132
|
+
|
|
133
|
+
connections = GriptapeNodes.FlowManager().get_connections()
|
|
134
|
+
|
|
135
|
+
control_connections = self.get_number_incoming_control_connections(node.node_reference, connections)
|
|
136
|
+
if control_connections <= 1:
|
|
137
|
+
return True
|
|
138
|
+
|
|
139
|
+
for graph in self.graphs.values():
|
|
140
|
+
# If the length of the graph is 0, skip it. it's either reached it or it's a dead end.
|
|
141
|
+
if len(graph.nodes()) == 0:
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
# If graph has nodes, the root node (not the leaf, the root), check forward path from that
|
|
145
|
+
root_nodes = [n for n in graph.nodes() if graph.out_degree(n) == 0]
|
|
146
|
+
for root_node_name in root_nodes:
|
|
147
|
+
if root_node_name in self.node_to_reference:
|
|
148
|
+
root_node = self.node_to_reference[root_node_name].node_reference
|
|
149
|
+
|
|
150
|
+
# Skip if the root node is the same as the target node - it can't reach itself
|
|
151
|
+
if root_node == node.node_reference:
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
# Check if the target node is in the forward path from this root
|
|
155
|
+
if self._is_node_in_forward_path(root_node, node.node_reference, connections):
|
|
156
|
+
return False # This graph could still reach the target node
|
|
157
|
+
|
|
158
|
+
# Otherwise, return true at the end of the function
|
|
159
|
+
return True
|
|
160
|
+
|
|
161
|
+
def get_number_incoming_control_connections(self, node: BaseNode, connections: Connections) -> int:
|
|
162
|
+
if node.name not in connections.incoming_index:
|
|
163
|
+
return 0
|
|
164
|
+
|
|
165
|
+
control_connection_count = 0
|
|
166
|
+
node_connections = connections.incoming_index[node.name]
|
|
167
|
+
|
|
168
|
+
for param_name, connection_ids in node_connections.items():
|
|
169
|
+
# Find the parameter to check if it's a control type
|
|
170
|
+
param = node.get_parameter_by_name(param_name)
|
|
171
|
+
if param and ParameterTypeBuiltin.CONTROL_TYPE.value in param.input_types:
|
|
172
|
+
control_connection_count += len(connection_ids)
|
|
173
|
+
|
|
174
|
+
return control_connection_count
|
|
175
|
+
|
|
176
|
+
def _is_node_in_forward_path(
|
|
177
|
+
self, start_node: BaseNode, target_node: BaseNode, connections: Connections, visited: set[str] | None = None
|
|
178
|
+
) -> bool:
|
|
179
|
+
"""Check if target_node is reachable from start_node through control flow connections."""
|
|
180
|
+
if visited is None:
|
|
181
|
+
visited = set()
|
|
182
|
+
|
|
183
|
+
if start_node.name in visited:
|
|
184
|
+
return False
|
|
185
|
+
visited.add(start_node.name)
|
|
186
|
+
|
|
187
|
+
# Check ALL outgoing control connections, not just get_next_control_output()
|
|
188
|
+
# This handles IfElse nodes that have multiple possible control outputs
|
|
189
|
+
if start_node.name in connections.outgoing_index:
|
|
190
|
+
for param_name, connection_ids in connections.outgoing_index[start_node.name].items():
|
|
191
|
+
# Find the parameter to check if it's a control type
|
|
192
|
+
param = start_node.get_parameter_by_name(param_name)
|
|
193
|
+
if param and param.output_type == ParameterTypeBuiltin.CONTROL_TYPE.value:
|
|
194
|
+
# This is a control parameter - check all its connections
|
|
195
|
+
for connection_id in connection_ids:
|
|
196
|
+
if connection_id in connections.connections:
|
|
197
|
+
connection = connections.connections[connection_id]
|
|
198
|
+
next_node = connection.target_node
|
|
199
|
+
|
|
200
|
+
if next_node.name == target_node.name:
|
|
201
|
+
return True
|
|
202
|
+
|
|
203
|
+
# Recursively check the forward path
|
|
204
|
+
if self._is_node_in_forward_path(next_node, target_node, connections, visited):
|
|
205
|
+
return True
|
|
206
|
+
|
|
207
|
+
return False
|