griptape-nodes 0.55.1__py3-none-any.whl → 0.56.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/app/app.py +10 -15
- griptape_nodes/app/watch.py +35 -67
- griptape_nodes/bootstrap/utils/__init__.py +1 -0
- griptape_nodes/bootstrap/utils/python_subprocess_executor.py +122 -0
- griptape_nodes/bootstrap/workflow_executors/local_session_workflow_executor.py +418 -0
- griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +37 -8
- griptape_nodes/bootstrap/workflow_executors/subprocess_workflow_executor.py +326 -0
- griptape_nodes/bootstrap/workflow_executors/utils/__init__.py +1 -0
- griptape_nodes/bootstrap/workflow_executors/utils/subprocess_script.py +51 -0
- griptape_nodes/bootstrap/workflow_publishers/__init__.py +1 -0
- griptape_nodes/bootstrap/workflow_publishers/local_workflow_publisher.py +43 -0
- griptape_nodes/bootstrap/workflow_publishers/subprocess_workflow_publisher.py +84 -0
- griptape_nodes/bootstrap/workflow_publishers/utils/__init__.py +1 -0
- griptape_nodes/bootstrap/workflow_publishers/utils/subprocess_script.py +54 -0
- griptape_nodes/cli/commands/engine.py +4 -15
- griptape_nodes/cli/commands/init.py +88 -0
- griptape_nodes/cli/commands/models.py +2 -0
- griptape_nodes/cli/main.py +6 -1
- griptape_nodes/cli/shared.py +1 -0
- griptape_nodes/exe_types/core_types.py +130 -0
- griptape_nodes/exe_types/node_types.py +125 -13
- griptape_nodes/machines/control_flow.py +10 -0
- griptape_nodes/machines/dag_builder.py +21 -2
- griptape_nodes/machines/parallel_resolution.py +25 -10
- griptape_nodes/node_library/workflow_registry.py +73 -3
- griptape_nodes/retained_mode/events/agent_events.py +2 -0
- griptape_nodes/retained_mode/events/base_events.py +18 -17
- griptape_nodes/retained_mode/events/execution_events.py +15 -3
- griptape_nodes/retained_mode/events/flow_events.py +63 -7
- griptape_nodes/retained_mode/events/mcp_events.py +363 -0
- griptape_nodes/retained_mode/events/node_events.py +3 -4
- griptape_nodes/retained_mode/events/resource_events.py +290 -0
- griptape_nodes/retained_mode/events/workflow_events.py +57 -2
- griptape_nodes/retained_mode/griptape_nodes.py +17 -1
- griptape_nodes/retained_mode/managers/agent_manager.py +67 -4
- griptape_nodes/retained_mode/managers/event_manager.py +31 -13
- griptape_nodes/retained_mode/managers/flow_manager.py +731 -33
- griptape_nodes/retained_mode/managers/library_manager.py +15 -23
- griptape_nodes/retained_mode/managers/mcp_manager.py +364 -0
- griptape_nodes/retained_mode/managers/model_manager.py +184 -83
- griptape_nodes/retained_mode/managers/node_manager.py +15 -4
- griptape_nodes/retained_mode/managers/os_manager.py +118 -1
- griptape_nodes/retained_mode/managers/resource_components/__init__.py +1 -0
- griptape_nodes/retained_mode/managers/resource_components/capability_field.py +41 -0
- griptape_nodes/retained_mode/managers/resource_components/comparator.py +18 -0
- griptape_nodes/retained_mode/managers/resource_components/resource_instance.py +236 -0
- griptape_nodes/retained_mode/managers/resource_components/resource_type.py +79 -0
- griptape_nodes/retained_mode/managers/resource_manager.py +306 -0
- griptape_nodes/retained_mode/managers/resource_types/__init__.py +1 -0
- griptape_nodes/retained_mode/managers/resource_types/cpu_resource.py +108 -0
- griptape_nodes/retained_mode/managers/resource_types/os_resource.py +87 -0
- griptape_nodes/retained_mode/managers/settings.py +45 -0
- griptape_nodes/retained_mode/managers/sync_manager.py +10 -3
- griptape_nodes/retained_mode/managers/workflow_manager.py +447 -263
- griptape_nodes/traits/multi_options.py +5 -1
- griptape_nodes/traits/options.py +10 -2
- {griptape_nodes-0.55.1.dist-info → griptape_nodes-0.56.1.dist-info}/METADATA +2 -2
- {griptape_nodes-0.55.1.dist-info → griptape_nodes-0.56.1.dist-info}/RECORD +60 -37
- {griptape_nodes-0.55.1.dist-info → griptape_nodes-0.56.1.dist-info}/WHEEL +1 -1
- {griptape_nodes-0.55.1.dist-info → griptape_nodes-0.56.1.dist-info}/entry_points.txt +0 -0
|
@@ -4,7 +4,6 @@ import ast
|
|
|
4
4
|
import asyncio
|
|
5
5
|
import logging
|
|
6
6
|
import pickle
|
|
7
|
-
import pkgutil
|
|
8
7
|
import re
|
|
9
8
|
from dataclasses import dataclass, field, fields, is_dataclass
|
|
10
9
|
from datetime import UTC, datetime
|
|
@@ -24,7 +23,7 @@ from griptape_nodes.drivers.storage import StorageBackend
|
|
|
24
23
|
from griptape_nodes.exe_types.core_types import ParameterTypeBuiltin
|
|
25
24
|
from griptape_nodes.exe_types.flow import ControlFlow
|
|
26
25
|
from griptape_nodes.exe_types.node_types import BaseNode, EndNode, StartNode
|
|
27
|
-
from griptape_nodes.node_library.workflow_registry import Workflow, WorkflowMetadata, WorkflowRegistry
|
|
26
|
+
from griptape_nodes.node_library.workflow_registry import Workflow, WorkflowMetadata, WorkflowRegistry, WorkflowShape
|
|
28
27
|
from griptape_nodes.retained_mode.events.app_events import (
|
|
29
28
|
GetEngineVersionRequest,
|
|
30
29
|
GetEngineVersionResultSuccess,
|
|
@@ -32,12 +31,12 @@ from griptape_nodes.retained_mode.events.app_events import (
|
|
|
32
31
|
|
|
33
32
|
# Runtime imports for ResultDetails since it's used at runtime
|
|
34
33
|
from griptape_nodes.retained_mode.events.base_events import ResultDetail, ResultDetails
|
|
34
|
+
from griptape_nodes.retained_mode.events.execution_events import StartFlowResultFailure, StartFlowResultSuccess
|
|
35
35
|
from griptape_nodes.retained_mode.events.flow_events import (
|
|
36
36
|
CreateFlowRequest,
|
|
37
37
|
GetTopLevelFlowRequest,
|
|
38
38
|
GetTopLevelFlowResultSuccess,
|
|
39
39
|
SerializedFlowCommands,
|
|
40
|
-
SerializedNodeCommands,
|
|
41
40
|
SerializeFlowToCommandsRequest,
|
|
42
41
|
SerializeFlowToCommandsResultSuccess,
|
|
43
42
|
SetFlowMetadataRequest,
|
|
@@ -100,6 +99,9 @@ from griptape_nodes.retained_mode.events.workflow_events import (
|
|
|
100
99
|
RunWorkflowWithCurrentStateRequest,
|
|
101
100
|
RunWorkflowWithCurrentStateResultFailure,
|
|
102
101
|
RunWorkflowWithCurrentStateResultSuccess,
|
|
102
|
+
SaveWorkflowFileFromSerializedFlowRequest,
|
|
103
|
+
SaveWorkflowFileFromSerializedFlowResultFailure,
|
|
104
|
+
SaveWorkflowFileFromSerializedFlowResultSuccess,
|
|
103
105
|
SaveWorkflowRequest,
|
|
104
106
|
SaveWorkflowResultFailure,
|
|
105
107
|
SaveWorkflowResultSuccess,
|
|
@@ -115,14 +117,17 @@ if TYPE_CHECKING:
|
|
|
115
117
|
from types import TracebackType
|
|
116
118
|
|
|
117
119
|
from griptape_nodes.exe_types.core_types import Parameter
|
|
118
|
-
from griptape_nodes.node_library.library_registry import LibraryNameAndVersion
|
|
119
120
|
from griptape_nodes.retained_mode.events.base_events import ResultPayload
|
|
120
|
-
from griptape_nodes.retained_mode.events.node_events import SetLockNodeStateRequest
|
|
121
|
+
from griptape_nodes.retained_mode.events.node_events import SerializedNodeCommands, SetLockNodeStateRequest
|
|
121
122
|
from griptape_nodes.retained_mode.managers.event_manager import EventManager
|
|
122
123
|
|
|
123
124
|
|
|
124
125
|
T = TypeVar("T")
|
|
125
126
|
|
|
127
|
+
# Type aliases for workflow shape building
|
|
128
|
+
ParameterShapeInfo = dict[str, Any] # Parameter metadata dict from _convert_parameter_to_minimal_dict
|
|
129
|
+
NodeParameterMap = dict[str, ParameterShapeInfo] # {param_name: param_info}
|
|
130
|
+
WorkflowShapeNodes = dict[str, NodeParameterMap] # {node_name: {param_name: param_info}}
|
|
126
131
|
|
|
127
132
|
logger = logging.getLogger("griptape_nodes")
|
|
128
133
|
|
|
@@ -269,6 +274,10 @@ class WorkflowManager:
|
|
|
269
274
|
SaveWorkflowRequest,
|
|
270
275
|
self.on_save_workflow_request,
|
|
271
276
|
)
|
|
277
|
+
event_manager.assign_manager_to_request_type(
|
|
278
|
+
SaveWorkflowFileFromSerializedFlowRequest,
|
|
279
|
+
self.on_save_workflow_file_from_serialized_flow_request,
|
|
280
|
+
)
|
|
272
281
|
event_manager.assign_manager_to_request_type(LoadWorkflowMetadata, self.on_load_workflow_metadata_request)
|
|
273
282
|
event_manager.assign_manager_to_request_type(
|
|
274
283
|
PublishWorkflowRequest,
|
|
@@ -1115,116 +1124,299 @@ class WorkflowManager:
|
|
|
1115
1124
|
|
|
1116
1125
|
return True
|
|
1117
1126
|
|
|
1118
|
-
|
|
1119
|
-
"""
|
|
1120
|
-
import_template = "from {} import *"
|
|
1121
|
-
import_statements = []
|
|
1122
|
-
|
|
1123
|
-
from griptape_nodes.retained_mode import events as events_pkg
|
|
1127
|
+
class WriteWorkflowFileResult(NamedTuple):
|
|
1128
|
+
"""Result of writing a workflow file."""
|
|
1124
1129
|
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
if module_name.endswith("generate_request_payload_schemas"):
|
|
1128
|
-
continue
|
|
1129
|
-
import_statements.append(import_template.format(module_name))
|
|
1130
|
+
success: bool
|
|
1131
|
+
error_details: str
|
|
1130
1132
|
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
def _generate_workflow_file_contents_and_metadata( # noqa: C901, PLR0912, PLR0915
|
|
1134
|
-
self,
|
|
1135
|
-
file_name: str,
|
|
1136
|
-
creation_date: datetime,
|
|
1137
|
-
image_path: str | None = None,
|
|
1138
|
-
prior_workflow: Workflow | None = None,
|
|
1139
|
-
custom_metadata: WorkflowMetadata | None = None,
|
|
1140
|
-
) -> tuple[str, WorkflowMetadata]:
|
|
1141
|
-
"""Generate the contents of a workflow file.
|
|
1133
|
+
def _write_workflow_file(self, file_path: Path, content: str, file_name: str) -> WriteWorkflowFileResult:
|
|
1134
|
+
"""Write workflow content to file with proper validation and error handling.
|
|
1142
1135
|
|
|
1143
1136
|
Args:
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
prior_workflow: Optional; existing workflow to preserve branch info from
|
|
1148
|
-
custom_metadata: Optional; pre-constructed metadata to use instead of generating it
|
|
1149
|
-
from the current workflow state. When provided, this metadata will be
|
|
1150
|
-
used directly, allowing branch/merge operations to pass specific metadata.
|
|
1137
|
+
file_path: Path where to write the file
|
|
1138
|
+
content: Content to write
|
|
1139
|
+
file_name: Name for error messages
|
|
1151
1140
|
|
|
1152
1141
|
Returns:
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
Raises:
|
|
1156
|
-
ValueError, TypeError: If workflow generation fails
|
|
1142
|
+
WriteWorkflowFileResult with success status and error details if failed
|
|
1157
1143
|
"""
|
|
1158
|
-
#
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
if not
|
|
1162
|
-
|
|
1163
|
-
|
|
1144
|
+
# Check disk space before any file system operations
|
|
1145
|
+
config_manager = GriptapeNodes.ConfigManager()
|
|
1146
|
+
min_space_gb = config_manager.get_config_value("minimum_disk_space_gb_workflows")
|
|
1147
|
+
if not OSManager.check_available_disk_space(file_path.parent, min_space_gb):
|
|
1148
|
+
error_msg = OSManager.format_disk_space_error(file_path.parent)
|
|
1149
|
+
details = f"Attempted to save workflow '{file_name}' (requires {min_space_gb:.1f} GB). Failed due to insufficient disk space: {error_msg}"
|
|
1150
|
+
return self.WriteWorkflowFileResult(success=False, error_details=details)
|
|
1151
|
+
|
|
1152
|
+
# Create directory structure
|
|
1164
1153
|
try:
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
)
|
|
1169
|
-
except Exception as err:
|
|
1170
|
-
details = f"Failed getting the engine version for workflow '{file_name}': {err}"
|
|
1171
|
-
raise ValueError(details) from err
|
|
1154
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1155
|
+
except OSError as e:
|
|
1156
|
+
details = f"Attempted to save workflow '{file_name}'. Failed when creating directory structure: {e}"
|
|
1157
|
+
return self.WriteWorkflowFileResult(success=False, error_details=details)
|
|
1172
1158
|
|
|
1173
|
-
#
|
|
1174
|
-
|
|
1159
|
+
# Write the file content
|
|
1160
|
+
try:
|
|
1161
|
+
with file_path.open("w", encoding="utf-8") as file:
|
|
1162
|
+
file.write(content)
|
|
1163
|
+
except OSError as e:
|
|
1164
|
+
details = f"Attempted to save workflow '{file_name}'. Failed when writing file content: {e}"
|
|
1165
|
+
return self.WriteWorkflowFileResult(success=False, error_details=details)
|
|
1175
1166
|
|
|
1176
|
-
|
|
1177
|
-
|
|
1167
|
+
return self.WriteWorkflowFileResult(success=True, error_details="")
|
|
1168
|
+
|
|
1169
|
+
def on_save_workflow_request(self, request: SaveWorkflowRequest) -> ResultPayload: # noqa: C901, PLR0912, PLR0915
|
|
1170
|
+
# Start with the file name provided; we may change it.
|
|
1171
|
+
file_name = request.file_name
|
|
1172
|
+
|
|
1173
|
+
# See if we had an existing workflow for this.
|
|
1174
|
+
prior_workflow = None
|
|
1175
|
+
creation_date = None
|
|
1176
|
+
if file_name and WorkflowRegistry.has_workflow_with_name(file_name):
|
|
1177
|
+
# Get the metadata.
|
|
1178
|
+
prior_workflow = WorkflowRegistry.get_workflow_by_name(file_name)
|
|
1179
|
+
# We'll use its creation date.
|
|
1180
|
+
creation_date = prior_workflow.metadata.creation_date
|
|
1181
|
+
elif file_name:
|
|
1182
|
+
# If no prior workflow exists for the new name, check if there's a current workflow
|
|
1183
|
+
# context (e.g., during rename operations) to preserve metadata from
|
|
1184
|
+
context_manager = GriptapeNodes.ContextManager()
|
|
1185
|
+
if context_manager.has_current_workflow():
|
|
1186
|
+
current_workflow_name = context_manager.get_current_workflow_name()
|
|
1187
|
+
if current_workflow_name and WorkflowRegistry.has_workflow_with_name(current_workflow_name):
|
|
1188
|
+
prior_workflow = WorkflowRegistry.get_workflow_by_name(current_workflow_name)
|
|
1189
|
+
creation_date = prior_workflow.metadata.creation_date
|
|
1190
|
+
|
|
1191
|
+
if (creation_date is None) or (creation_date == WorkflowManager.EPOCH_START):
|
|
1192
|
+
# Either a new workflow, or a backcompat situation.
|
|
1193
|
+
creation_date = datetime.now(tz=UTC)
|
|
1194
|
+
|
|
1195
|
+
# Let's see if this is a template file; if so, re-route it as a copy in the customer's workflow directory.
|
|
1196
|
+
if prior_workflow and prior_workflow.metadata.is_template:
|
|
1197
|
+
# Aha! User is attempting to save a template. Create a differently-named file in their workspace.
|
|
1198
|
+
# Find the first available file name that doesn't conflict.
|
|
1199
|
+
curr_idx = 1
|
|
1200
|
+
free_file_found = False
|
|
1201
|
+
while not free_file_found:
|
|
1202
|
+
# Composite a new candidate file name to test.
|
|
1203
|
+
new_file_name = f"{file_name}_{curr_idx}"
|
|
1204
|
+
new_file_name_with_extension = f"{new_file_name}.py"
|
|
1205
|
+
new_file_full_path = GriptapeNodes.ConfigManager().workspace_path.joinpath(new_file_name_with_extension)
|
|
1206
|
+
if new_file_full_path.exists():
|
|
1207
|
+
# Keep going.
|
|
1208
|
+
curr_idx += 1
|
|
1209
|
+
else:
|
|
1210
|
+
free_file_found = True
|
|
1211
|
+
file_name = new_file_name
|
|
1178
1212
|
|
|
1179
|
-
#
|
|
1213
|
+
# Get file name stuff prepped.
|
|
1214
|
+
# Use the existing registered file path if this is an existing workflow (not a template)
|
|
1215
|
+
if prior_workflow and not prior_workflow.metadata.is_template:
|
|
1216
|
+
# Use the existing registered file path
|
|
1217
|
+
relative_file_path = prior_workflow.file_path
|
|
1218
|
+
file_path = Path(WorkflowRegistry.get_complete_file_path(relative_file_path))
|
|
1219
|
+
# Extract file name from the path for metadata generation
|
|
1220
|
+
if not file_name:
|
|
1221
|
+
file_name = prior_workflow.metadata.name
|
|
1222
|
+
else:
|
|
1223
|
+
# Create new path in workspace for new workflows or templates
|
|
1224
|
+
if not file_name:
|
|
1225
|
+
file_name = datetime.now(tz=UTC).strftime("%d.%m_%H.%M")
|
|
1226
|
+
relative_file_path = f"{file_name}.py"
|
|
1227
|
+
file_path = GriptapeNodes.ConfigManager().workspace_path.joinpath(relative_file_path)
|
|
1228
|
+
|
|
1229
|
+
# First, serialize the current workflow state
|
|
1180
1230
|
top_level_flow_request = GetTopLevelFlowRequest()
|
|
1181
1231
|
top_level_flow_result = GriptapeNodes.handle_request(top_level_flow_request)
|
|
1182
1232
|
if not isinstance(top_level_flow_result, GetTopLevelFlowResultSuccess):
|
|
1183
|
-
details = f"Failed when requesting
|
|
1184
|
-
|
|
1233
|
+
details = f"Attempted to save workflow '{relative_file_path}'. Failed when requesting top level flow."
|
|
1234
|
+
return SaveWorkflowResultFailure(result_details=details)
|
|
1235
|
+
|
|
1185
1236
|
top_level_flow_name = top_level_flow_result.flow_name
|
|
1186
1237
|
serialized_flow_request = SerializeFlowToCommandsRequest(
|
|
1187
1238
|
flow_name=top_level_flow_name, include_create_flow_command=True
|
|
1188
1239
|
)
|
|
1189
1240
|
serialized_flow_result = GriptapeNodes.handle_request(serialized_flow_request)
|
|
1190
1241
|
if not isinstance(serialized_flow_result, SerializeFlowToCommandsResultSuccess):
|
|
1191
|
-
details = f"
|
|
1192
|
-
|
|
1242
|
+
details = f"Attempted to save workflow '{relative_file_path}'. Failed when serializing flow."
|
|
1243
|
+
return SaveWorkflowResultFailure(result_details=details)
|
|
1244
|
+
|
|
1193
1245
|
serialized_flow_commands = serialized_flow_result.serialized_flow_commands
|
|
1194
1246
|
|
|
1195
|
-
#
|
|
1196
|
-
|
|
1197
|
-
|
|
1247
|
+
# Extract branched_from information if it exists
|
|
1248
|
+
branched_from = None
|
|
1249
|
+
if prior_workflow and prior_workflow.metadata.branched_from:
|
|
1250
|
+
branched_from = prior_workflow.metadata.branched_from
|
|
1251
|
+
|
|
1252
|
+
# Extract workflow shape if possible
|
|
1253
|
+
workflow_shape = None
|
|
1254
|
+
try:
|
|
1255
|
+
workflow_shape_dict = self.extract_workflow_shape(workflow_name=file_name)
|
|
1256
|
+
workflow_shape = WorkflowShape(inputs=workflow_shape_dict["input"], outputs=workflow_shape_dict["output"])
|
|
1257
|
+
except ValueError:
|
|
1258
|
+
# If we can't extract workflow shape, continue without it
|
|
1259
|
+
pass
|
|
1260
|
+
|
|
1261
|
+
# Use the standalone request to save the workflow file
|
|
1262
|
+
save_file_request = SaveWorkflowFileFromSerializedFlowRequest(
|
|
1263
|
+
serialized_flow_commands=serialized_flow_commands,
|
|
1264
|
+
file_name=file_name,
|
|
1265
|
+
creation_date=creation_date,
|
|
1266
|
+
image_path=request.image_path,
|
|
1267
|
+
execution_flow_name=top_level_flow_name,
|
|
1268
|
+
branched_from=branched_from,
|
|
1269
|
+
workflow_shape=workflow_shape,
|
|
1270
|
+
file_path=str(file_path),
|
|
1271
|
+
)
|
|
1272
|
+
save_file_result = self.on_save_workflow_file_from_serialized_flow_request(save_file_request)
|
|
1273
|
+
|
|
1274
|
+
if not isinstance(save_file_result, SaveWorkflowFileFromSerializedFlowResultSuccess):
|
|
1275
|
+
details = f"Attempted to save workflow '{relative_file_path}'. Failed during file generation: {save_file_result.result_details}"
|
|
1276
|
+
return SaveWorkflowResultFailure(result_details=details)
|
|
1277
|
+
|
|
1278
|
+
# Use the metadata returned by the save operation
|
|
1279
|
+
workflow_metadata = save_file_result.workflow_metadata
|
|
1280
|
+
|
|
1281
|
+
# save the created workflow as an entry in the JSON config file.
|
|
1282
|
+
registered_workflows = WorkflowRegistry.list_workflows()
|
|
1283
|
+
if file_name not in registered_workflows:
|
|
1284
|
+
# Only add to config if it's in the workspace directory (not an external file)
|
|
1285
|
+
if not Path(relative_file_path).is_absolute():
|
|
1286
|
+
try:
|
|
1287
|
+
GriptapeNodes.ConfigManager().save_user_workflow_json(str(file_path))
|
|
1288
|
+
except OSError as e:
|
|
1289
|
+
details = f"Attempted to save workflow '{file_name}'. Failed when saving configuration: {e}"
|
|
1290
|
+
return SaveWorkflowResultFailure(result_details=details)
|
|
1291
|
+
WorkflowRegistry.generate_new_workflow(metadata=workflow_metadata, file_path=relative_file_path)
|
|
1292
|
+
# Update existing workflow's metadata in the registry
|
|
1293
|
+
existing_workflow = WorkflowRegistry.get_workflow_by_name(file_name)
|
|
1294
|
+
existing_workflow.metadata = workflow_metadata
|
|
1295
|
+
details = f"Successfully saved workflow to: {save_file_result.file_path}"
|
|
1296
|
+
return SaveWorkflowResultSuccess(
|
|
1297
|
+
file_path=save_file_result.file_path, result_details=ResultDetails(message=details, level=logging.INFO)
|
|
1298
|
+
)
|
|
1299
|
+
|
|
1300
|
+
def on_save_workflow_file_from_serialized_flow_request(
|
|
1301
|
+
self, request: SaveWorkflowFileFromSerializedFlowRequest
|
|
1302
|
+
) -> ResultPayload:
|
|
1303
|
+
"""Save a workflow file from serialized flow commands without registry overhead."""
|
|
1304
|
+
# Determine file path
|
|
1305
|
+
if request.file_path:
|
|
1306
|
+
# Use provided file path
|
|
1307
|
+
file_path = Path(request.file_path)
|
|
1198
1308
|
else:
|
|
1199
|
-
#
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1309
|
+
# Default to workspace path
|
|
1310
|
+
relative_file_path = f"{request.file_name}.py"
|
|
1311
|
+
file_path = GriptapeNodes.ConfigManager().workspace_path.joinpath(relative_file_path)
|
|
1312
|
+
|
|
1313
|
+
# Use provided creation date or default to current time
|
|
1314
|
+
creation_date = request.creation_date
|
|
1315
|
+
if creation_date is None:
|
|
1316
|
+
creation_date = datetime.now(tz=UTC)
|
|
1317
|
+
|
|
1318
|
+
# Generate metadata from the serialized commands
|
|
1319
|
+
try:
|
|
1320
|
+
workflow_metadata = self._generate_workflow_metadata_from_commands(
|
|
1321
|
+
serialized_flow_commands=request.serialized_flow_commands,
|
|
1322
|
+
file_name=request.file_name,
|
|
1207
1323
|
creation_date=creation_date,
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1324
|
+
image_path=request.image_path,
|
|
1325
|
+
branched_from=request.branched_from,
|
|
1326
|
+
workflow_shape=request.workflow_shape,
|
|
1327
|
+
)
|
|
1328
|
+
except Exception as err:
|
|
1329
|
+
details = f"Attempted to save workflow file '{request.file_name}' from serialized flow commands. Failed during metadata generation: {err}"
|
|
1330
|
+
return SaveWorkflowFileFromSerializedFlowResultFailure(result_details=details)
|
|
1331
|
+
|
|
1332
|
+
# Use provided execution flow name or default to file name
|
|
1333
|
+
execution_flow_name = request.execution_flow_name
|
|
1334
|
+
if execution_flow_name is None:
|
|
1335
|
+
execution_flow_name = request.file_name
|
|
1336
|
+
|
|
1337
|
+
# Generate the workflow file content
|
|
1338
|
+
try:
|
|
1339
|
+
final_code_output = self._generate_workflow_file_content(
|
|
1340
|
+
serialized_flow_commands=request.serialized_flow_commands,
|
|
1341
|
+
workflow_metadata=workflow_metadata,
|
|
1342
|
+
execution_flow_name=execution_flow_name,
|
|
1211
1343
|
)
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1344
|
+
except Exception as err:
|
|
1345
|
+
details = f"Attempted to save workflow file '{request.file_name}' from serialized flow commands. Failed during content generation: {err}"
|
|
1346
|
+
return SaveWorkflowFileFromSerializedFlowResultFailure(result_details=details)
|
|
1347
|
+
|
|
1348
|
+
# Write the workflow file
|
|
1349
|
+
write_result = self._write_workflow_file(file_path, final_code_output, request.file_name)
|
|
1350
|
+
if not write_result.success:
|
|
1351
|
+
return SaveWorkflowFileFromSerializedFlowResultFailure(result_details=write_result.error_details)
|
|
1352
|
+
|
|
1353
|
+
details = f"Successfully saved workflow file at: {file_path}"
|
|
1354
|
+
return SaveWorkflowFileFromSerializedFlowResultSuccess(
|
|
1355
|
+
file_path=str(file_path),
|
|
1356
|
+
workflow_metadata=workflow_metadata,
|
|
1357
|
+
result_details=ResultDetails(message=details, level=logging.INFO),
|
|
1358
|
+
)
|
|
1359
|
+
|
|
1360
|
+
def _generate_workflow_metadata_from_commands( # noqa: PLR0913
|
|
1361
|
+
self,
|
|
1362
|
+
serialized_flow_commands: SerializedFlowCommands,
|
|
1363
|
+
file_name: str,
|
|
1364
|
+
creation_date: datetime,
|
|
1365
|
+
image_path: str | None = None,
|
|
1366
|
+
branched_from: str | None = None,
|
|
1367
|
+
workflow_shape: WorkflowShape | None = None,
|
|
1368
|
+
) -> WorkflowMetadata:
|
|
1369
|
+
"""Generate workflow metadata from serialized commands."""
|
|
1370
|
+
# Get the engine version
|
|
1371
|
+
engine_version_request = GetEngineVersionRequest()
|
|
1372
|
+
engine_version_result = GriptapeNodes.handle_request(request=engine_version_request)
|
|
1373
|
+
if not isinstance(engine_version_result, GetEngineVersionResultSuccess):
|
|
1374
|
+
details = f"Failed getting the engine version for workflow '{file_name}'."
|
|
1375
|
+
raise TypeError(details)
|
|
1376
|
+
|
|
1377
|
+
engine_version_success = cast("GetEngineVersionResultSuccess", engine_version_result)
|
|
1378
|
+
engine_version = f"{engine_version_success.major}.{engine_version_success.minor}.{engine_version_success.patch}"
|
|
1215
1379
|
|
|
1216
|
-
#
|
|
1217
|
-
|
|
1218
|
-
|
|
1380
|
+
# Create the Workflow Metadata header
|
|
1381
|
+
workflows_referenced = None
|
|
1382
|
+
if serialized_flow_commands.node_dependencies.referenced_workflows:
|
|
1383
|
+
workflows_referenced = list(serialized_flow_commands.node_dependencies.referenced_workflows)
|
|
1219
1384
|
|
|
1385
|
+
return WorkflowMetadata(
|
|
1386
|
+
name=str(file_name),
|
|
1387
|
+
schema_version=WorkflowMetadata.LATEST_SCHEMA_VERSION,
|
|
1388
|
+
engine_version_created_with=engine_version,
|
|
1389
|
+
node_libraries_referenced=list(serialized_flow_commands.node_dependencies.libraries),
|
|
1390
|
+
workflows_referenced=workflows_referenced,
|
|
1391
|
+
creation_date=creation_date,
|
|
1392
|
+
last_modified_date=datetime.now(tz=UTC),
|
|
1393
|
+
branched_from=branched_from,
|
|
1394
|
+
workflow_shape=workflow_shape,
|
|
1395
|
+
image=image_path,
|
|
1396
|
+
)
|
|
1397
|
+
|
|
1398
|
+
def _generate_workflow_file_content( # noqa: PLR0912, PLR0915, C901
|
|
1399
|
+
self,
|
|
1400
|
+
serialized_flow_commands: SerializedFlowCommands,
|
|
1401
|
+
workflow_metadata: WorkflowMetadata,
|
|
1402
|
+
execution_flow_name: str,
|
|
1403
|
+
) -> str:
|
|
1404
|
+
"""Generate workflow file content from serialized commands and metadata."""
|
|
1220
1405
|
metadata_block = self._generate_workflow_metadata_header(workflow_metadata=workflow_metadata)
|
|
1221
1406
|
if metadata_block is None:
|
|
1222
|
-
details = f"Failed to generate metadata block for workflow '{
|
|
1407
|
+
details = f"Failed to generate metadata block for workflow '{workflow_metadata.name}'."
|
|
1223
1408
|
raise ValueError(details)
|
|
1224
1409
|
|
|
1225
1410
|
import_recorder = ImportRecorder()
|
|
1226
1411
|
import_recorder.add_from_import("griptape_nodes.retained_mode.griptape_nodes", "GriptapeNodes")
|
|
1227
1412
|
|
|
1413
|
+
# Add imports from node dependencies
|
|
1414
|
+
for import_dep in serialized_flow_commands.node_dependencies.imports:
|
|
1415
|
+
if import_dep.class_name:
|
|
1416
|
+
import_recorder.add_from_import(import_dep.module, import_dep.class_name)
|
|
1417
|
+
else:
|
|
1418
|
+
import_recorder.add_import(import_dep.module)
|
|
1419
|
+
|
|
1228
1420
|
ast_container = ASTContainer()
|
|
1229
1421
|
|
|
1230
1422
|
prereq_code = self._generate_workflow_run_prerequisite_code(
|
|
@@ -1233,7 +1425,7 @@ class WorkflowManager:
|
|
|
1233
1425
|
for node in prereq_code:
|
|
1234
1426
|
ast_container.add_node(node)
|
|
1235
1427
|
|
|
1236
|
-
# Generate unique values code AST node
|
|
1428
|
+
# Generate unique values code AST node
|
|
1237
1429
|
unique_values_node = self._generate_unique_values_code(
|
|
1238
1430
|
unique_parameter_uuid_to_values=serialized_flow_commands.unique_parameter_uuid_to_values,
|
|
1239
1431
|
prefix="top_level",
|
|
@@ -1241,7 +1433,10 @@ class WorkflowManager:
|
|
|
1241
1433
|
)
|
|
1242
1434
|
ast_container.add_node(unique_values_node)
|
|
1243
1435
|
|
|
1244
|
-
#
|
|
1436
|
+
# Keep track of each flow and node index we've created
|
|
1437
|
+
flow_creation_index = 0
|
|
1438
|
+
|
|
1439
|
+
# See if this serialized flow has a flow initialization command; if it does, we'll need to insert that
|
|
1245
1440
|
flow_initialization_command = serialized_flow_commands.flow_initialization_command
|
|
1246
1441
|
|
|
1247
1442
|
match flow_initialization_command:
|
|
@@ -1263,7 +1458,7 @@ class WorkflowManager:
|
|
|
1263
1458
|
# No initialization command, deserialize into current context
|
|
1264
1459
|
pass
|
|
1265
1460
|
|
|
1266
|
-
# Generate assign flow context AST node, if we have any children commands
|
|
1461
|
+
# Generate assign flow context AST node, if we have any children commands
|
|
1267
1462
|
# Skip content generation for referenced workflows - they should only have the import command
|
|
1268
1463
|
is_referenced_workflow = isinstance(flow_initialization_command, ImportWorkflowAsReferencedSubFlowRequest)
|
|
1269
1464
|
has_content_to_serialize = (
|
|
@@ -1275,12 +1470,15 @@ class WorkflowManager:
|
|
|
1275
1470
|
)
|
|
1276
1471
|
|
|
1277
1472
|
if not is_referenced_workflow and has_content_to_serialize:
|
|
1473
|
+
# Keep track of all of the nodes we create and the generated variable names for them
|
|
1474
|
+
node_uuid_to_node_variable_name: dict[SerializedNodeCommands.NodeUUID, str] = {}
|
|
1475
|
+
|
|
1278
1476
|
# Create the "with..." statement
|
|
1279
1477
|
assign_flow_context_node = self._generate_assign_flow_context(
|
|
1280
1478
|
flow_initialization_command=flow_initialization_command, flow_creation_index=flow_creation_index
|
|
1281
1479
|
)
|
|
1282
1480
|
|
|
1283
|
-
# Generate nodes in flow AST node. This will create the node and apply all element modifiers
|
|
1481
|
+
# Generate nodes in flow AST node. This will create the node and apply all element modifiers
|
|
1284
1482
|
nodes_in_flow = self._generate_nodes_in_flow(
|
|
1285
1483
|
serialized_flow_commands, import_recorder, node_uuid_to_node_variable_name
|
|
1286
1484
|
)
|
|
@@ -1307,7 +1505,7 @@ class WorkflowManager:
|
|
|
1307
1505
|
)
|
|
1308
1506
|
assign_flow_context_node.body.append(cast("ast.stmt", sub_flow_import_node))
|
|
1309
1507
|
|
|
1310
|
-
# Now generate the connection code and add it to the flow context
|
|
1508
|
+
# Now generate the connection code and add it to the flow context
|
|
1311
1509
|
connection_asts = self._generate_connections_code(
|
|
1312
1510
|
serialized_connections=serialized_flow_commands.serialized_connections,
|
|
1313
1511
|
node_uuid_to_node_variable_name=node_uuid_to_node_variable_name,
|
|
@@ -1315,7 +1513,7 @@ class WorkflowManager:
|
|
|
1315
1513
|
)
|
|
1316
1514
|
assign_flow_context_node.body.extend(connection_asts)
|
|
1317
1515
|
|
|
1318
|
-
#
|
|
1516
|
+
# Generate parameter values
|
|
1319
1517
|
set_parameter_value_asts = self._generate_set_parameter_value_code(
|
|
1320
1518
|
set_parameter_value_commands=serialized_flow_commands.set_parameter_value_commands,
|
|
1321
1519
|
lock_commands=serialized_flow_commands.set_lock_commands_per_node,
|
|
@@ -1327,166 +1525,20 @@ class WorkflowManager:
|
|
|
1327
1525
|
|
|
1328
1526
|
ast_container.add_node(assign_flow_context_node)
|
|
1329
1527
|
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
if top_level_flow_name
|
|
1336
|
-
else None
|
|
1528
|
+
# Generate workflow execution code
|
|
1529
|
+
workflow_execution_code = self._generate_workflow_execution(
|
|
1530
|
+
flow_name=execution_flow_name,
|
|
1531
|
+
import_recorder=import_recorder,
|
|
1532
|
+
workflow_metadata=workflow_metadata,
|
|
1337
1533
|
)
|
|
1338
1534
|
if workflow_execution_code is not None:
|
|
1339
1535
|
for node in workflow_execution_code:
|
|
1340
1536
|
ast_container.add_node(node)
|
|
1341
1537
|
|
|
1342
|
-
# TODO: https://github.com/griptape-ai/griptape-nodes/issues/1190 do child workflows
|
|
1343
|
-
|
|
1344
1538
|
# Generate final code from ASTContainer
|
|
1345
1539
|
ast_output = "\n\n".join([ast.unparse(node) for node in ast_container.get_ast()])
|
|
1346
1540
|
import_output = import_recorder.generate_imports()
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
return final_code_output, workflow_metadata
|
|
1350
|
-
|
|
1351
|
-
def on_save_workflow_request(self, request: SaveWorkflowRequest) -> ResultPayload: # noqa: C901, PLR0912, PLR0915
|
|
1352
|
-
# Start with the file name provided; we may change it.
|
|
1353
|
-
file_name = request.file_name
|
|
1354
|
-
|
|
1355
|
-
# See if we had an existing workflow for this.
|
|
1356
|
-
prior_workflow = None
|
|
1357
|
-
creation_date = None
|
|
1358
|
-
if file_name and WorkflowRegistry.has_workflow_with_name(file_name):
|
|
1359
|
-
# Get the metadata.
|
|
1360
|
-
prior_workflow = WorkflowRegistry.get_workflow_by_name(file_name)
|
|
1361
|
-
# We'll use its creation date.
|
|
1362
|
-
creation_date = prior_workflow.metadata.creation_date
|
|
1363
|
-
elif file_name:
|
|
1364
|
-
# If no prior workflow exists for the new name, check if there's a current workflow
|
|
1365
|
-
# context (e.g., during rename operations) to preserve metadata from
|
|
1366
|
-
context_manager = GriptapeNodes.ContextManager()
|
|
1367
|
-
if context_manager.has_current_workflow():
|
|
1368
|
-
current_workflow_name = context_manager.get_current_workflow_name()
|
|
1369
|
-
if current_workflow_name and WorkflowRegistry.has_workflow_with_name(current_workflow_name):
|
|
1370
|
-
prior_workflow = WorkflowRegistry.get_workflow_by_name(current_workflow_name)
|
|
1371
|
-
creation_date = prior_workflow.metadata.creation_date
|
|
1372
|
-
|
|
1373
|
-
if (creation_date is None) or (creation_date == WorkflowManager.EPOCH_START):
|
|
1374
|
-
# Either a new workflow, or a backcompat situation.
|
|
1375
|
-
creation_date = datetime.now(tz=UTC)
|
|
1376
|
-
|
|
1377
|
-
# Let's see if this is a template file; if so, re-route it as a copy in the customer's workflow directory.
|
|
1378
|
-
if prior_workflow and prior_workflow.metadata.is_template:
|
|
1379
|
-
# Aha! User is attempting to save a template. Create a differently-named file in their workspace.
|
|
1380
|
-
# Find the first available file name that doesn't conflict.
|
|
1381
|
-
curr_idx = 1
|
|
1382
|
-
free_file_found = False
|
|
1383
|
-
while not free_file_found:
|
|
1384
|
-
# Composite a new candidate file name to test.
|
|
1385
|
-
new_file_name = f"{file_name}_{curr_idx}"
|
|
1386
|
-
new_file_name_with_extension = f"{new_file_name}.py"
|
|
1387
|
-
new_file_full_path = GriptapeNodes.ConfigManager().workspace_path.joinpath(new_file_name_with_extension)
|
|
1388
|
-
if new_file_full_path.exists():
|
|
1389
|
-
# Keep going.
|
|
1390
|
-
curr_idx += 1
|
|
1391
|
-
else:
|
|
1392
|
-
free_file_found = True
|
|
1393
|
-
file_name = new_file_name
|
|
1394
|
-
|
|
1395
|
-
# Get file name stuff prepped.
|
|
1396
|
-
# Use the existing registered file path if this is an existing workflow (not a template)
|
|
1397
|
-
if prior_workflow and not prior_workflow.metadata.is_template:
|
|
1398
|
-
# Use the existing registered file path
|
|
1399
|
-
relative_file_path = prior_workflow.file_path
|
|
1400
|
-
file_path = Path(WorkflowRegistry.get_complete_file_path(relative_file_path))
|
|
1401
|
-
# Extract file name from the path for metadata generation
|
|
1402
|
-
if not file_name:
|
|
1403
|
-
file_name = prior_workflow.metadata.name
|
|
1404
|
-
else:
|
|
1405
|
-
# Create new path in workspace for new workflows or templates
|
|
1406
|
-
if not file_name:
|
|
1407
|
-
file_name = datetime.now(tz=UTC).strftime("%d.%m_%H.%M")
|
|
1408
|
-
relative_file_path = f"{file_name}.py"
|
|
1409
|
-
file_path = GriptapeNodes.ConfigManager().workspace_path.joinpath(relative_file_path)
|
|
1410
|
-
|
|
1411
|
-
# Generate the workflow file contents
|
|
1412
|
-
try:
|
|
1413
|
-
final_code_output, workflow_metadata = self._generate_workflow_file_contents_and_metadata(
|
|
1414
|
-
file_name=file_name,
|
|
1415
|
-
creation_date=creation_date,
|
|
1416
|
-
image_path=request.image_path,
|
|
1417
|
-
prior_workflow=prior_workflow,
|
|
1418
|
-
)
|
|
1419
|
-
except Exception as err:
|
|
1420
|
-
details = f"Attempted to save workflow '{relative_file_path}', but {err}"
|
|
1421
|
-
return SaveWorkflowResultFailure(result_details=details)
|
|
1422
|
-
|
|
1423
|
-
# Create the pathing and write the file
|
|
1424
|
-
try:
|
|
1425
|
-
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1426
|
-
except OSError as e:
|
|
1427
|
-
details = f"Attempted to save workflow '{file_name}'. Failed when creating directory: {e}"
|
|
1428
|
-
return SaveWorkflowResultFailure(result_details=details)
|
|
1429
|
-
|
|
1430
|
-
# Check disk space before writing
|
|
1431
|
-
config_manager = GriptapeNodes.ConfigManager()
|
|
1432
|
-
min_space_gb = config_manager.get_config_value("minimum_disk_space_gb_workflows")
|
|
1433
|
-
if not OSManager.check_available_disk_space(file_path.parent, min_space_gb):
|
|
1434
|
-
error_msg = OSManager.format_disk_space_error(file_path.parent)
|
|
1435
|
-
details = f"Attempted to save workflow '{file_name}' (requires {min_space_gb:.1f} GB). Failed: {error_msg}"
|
|
1436
|
-
return SaveWorkflowResultFailure(result_details=details)
|
|
1437
|
-
|
|
1438
|
-
try:
|
|
1439
|
-
with file_path.open("w", encoding="utf-8") as file:
|
|
1440
|
-
file.write(final_code_output)
|
|
1441
|
-
except OSError as e:
|
|
1442
|
-
details = f"Attempted to save workflow '{file_name}'. Failed when writing file: {e}"
|
|
1443
|
-
return SaveWorkflowResultFailure(result_details=details)
|
|
1444
|
-
|
|
1445
|
-
# save the created workflow as an entry in the JSON config file.
|
|
1446
|
-
registered_workflows = WorkflowRegistry.list_workflows()
|
|
1447
|
-
if file_name not in registered_workflows:
|
|
1448
|
-
# Only add to config if it's in the workspace directory (not an external file)
|
|
1449
|
-
if not Path(relative_file_path).is_absolute():
|
|
1450
|
-
try:
|
|
1451
|
-
GriptapeNodes.ConfigManager().save_user_workflow_json(str(file_path))
|
|
1452
|
-
except OSError as e:
|
|
1453
|
-
details = f"Attempted to save workflow '{file_name}'. Failed when saving configuration: {e}"
|
|
1454
|
-
return SaveWorkflowResultFailure(result_details=details)
|
|
1455
|
-
WorkflowRegistry.generate_new_workflow(metadata=workflow_metadata, file_path=relative_file_path)
|
|
1456
|
-
# Update existing workflow's metadata in the registry
|
|
1457
|
-
existing_workflow = WorkflowRegistry.get_workflow_by_name(file_name)
|
|
1458
|
-
existing_workflow.metadata = workflow_metadata
|
|
1459
|
-
details = f"Successfully saved workflow to: {file_path}"
|
|
1460
|
-
return SaveWorkflowResultSuccess(
|
|
1461
|
-
file_path=str(file_path), result_details=ResultDetails(message=details, level=logging.INFO)
|
|
1462
|
-
)
|
|
1463
|
-
|
|
1464
|
-
def _generate_workflow_metadata( # noqa: PLR0913
|
|
1465
|
-
self,
|
|
1466
|
-
file_name: str,
|
|
1467
|
-
engine_version: str,
|
|
1468
|
-
creation_date: datetime,
|
|
1469
|
-
node_libraries_referenced: list[LibraryNameAndVersion],
|
|
1470
|
-
workflows_referenced: list[str] | None = None,
|
|
1471
|
-
prior_workflow: Workflow | None = None,
|
|
1472
|
-
) -> WorkflowMetadata | None:
|
|
1473
|
-
# Preserve branched workflow information if it exists
|
|
1474
|
-
branched_from = None
|
|
1475
|
-
if prior_workflow and prior_workflow.metadata.branched_from:
|
|
1476
|
-
branched_from = prior_workflow.metadata.branched_from
|
|
1477
|
-
|
|
1478
|
-
workflow_metadata = WorkflowMetadata(
|
|
1479
|
-
name=str(file_name),
|
|
1480
|
-
schema_version=WorkflowMetadata.LATEST_SCHEMA_VERSION,
|
|
1481
|
-
engine_version_created_with=engine_version,
|
|
1482
|
-
node_libraries_referenced=node_libraries_referenced,
|
|
1483
|
-
workflows_referenced=workflows_referenced,
|
|
1484
|
-
creation_date=creation_date,
|
|
1485
|
-
last_modified_date=datetime.now(tz=UTC),
|
|
1486
|
-
branched_from=branched_from,
|
|
1487
|
-
)
|
|
1488
|
-
|
|
1489
|
-
return workflow_metadata
|
|
1541
|
+
return f"{metadata_block}\n\n{import_output}\n\n{ast_output}\n"
|
|
1490
1542
|
|
|
1491
1543
|
def _replace_workflow_metadata_header(self, workflow_content: str, new_metadata: WorkflowMetadata) -> str | None:
|
|
1492
1544
|
"""Replace the metadata header in a workflow file with new metadata.
|
|
@@ -1516,11 +1568,13 @@ class WorkflowManager:
|
|
|
1516
1568
|
toml_doc = tomlkit.document()
|
|
1517
1569
|
toml_doc.add("dependencies", tomlkit.item([]))
|
|
1518
1570
|
griptape_tool_table = tomlkit.table()
|
|
1519
|
-
|
|
1571
|
+
# Strip out the Nones since TOML doesn't like those
|
|
1572
|
+
# WorkflowShape is now serialized as JSON string by Pydantic field_serializer;
|
|
1573
|
+
# this preserves the nil/null/None values that we WANT, but for all of the
|
|
1574
|
+
# Python-related Nones, TOML will flip out if they are not stripped.
|
|
1575
|
+
metadata_dict = workflow_metadata.model_dump(exclude_none=True)
|
|
1520
1576
|
for key, value in metadata_dict.items():
|
|
1521
|
-
|
|
1522
|
-
if value is not None:
|
|
1523
|
-
griptape_tool_table.add(key=key, value=value)
|
|
1577
|
+
griptape_tool_table.add(key=key, value=value)
|
|
1524
1578
|
toml_doc["tool"] = tomlkit.table()
|
|
1525
1579
|
toml_doc["tool"]["griptape-nodes"] = griptape_tool_table # type: ignore (this is the only way I could find to get tomlkit to do the dotted notation correctly)
|
|
1526
1580
|
except Exception as err:
|
|
@@ -1545,14 +1599,20 @@ class WorkflowManager:
|
|
|
1545
1599
|
self,
|
|
1546
1600
|
flow_name: str,
|
|
1547
1601
|
import_recorder: ImportRecorder,
|
|
1602
|
+
workflow_metadata: WorkflowMetadata,
|
|
1548
1603
|
) -> list[ast.AST] | None:
|
|
1549
1604
|
"""Generates execute_workflow(...) and the __main__ guard."""
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
except ValueError:
|
|
1605
|
+
# Use workflow shape from metadata if available, otherwise skip execution block
|
|
1606
|
+
if workflow_metadata.workflow_shape is None:
|
|
1553
1607
|
logger.info("Workflow shape does not have required Start or End Nodes. Skipping local execution block.")
|
|
1554
1608
|
return None
|
|
1555
1609
|
|
|
1610
|
+
# Convert WorkflowShape to dict format expected by the rest of the method
|
|
1611
|
+
workflow_shape = {
|
|
1612
|
+
"input": workflow_metadata.workflow_shape.inputs,
|
|
1613
|
+
"output": workflow_metadata.workflow_shape.outputs,
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1556
1616
|
# === imports ===
|
|
1557
1617
|
import_recorder.add_import("argparse")
|
|
1558
1618
|
import_recorder.add_import("asyncio")
|
|
@@ -1936,6 +1996,17 @@ class WorkflowManager:
|
|
|
1936
1996
|
]
|
|
1937
1997
|
)
|
|
1938
1998
|
|
|
1999
|
+
# Ensure body is not empty - add pass statement if no input parameters
|
|
2000
|
+
if not build_flow_input_stmts:
|
|
2001
|
+
build_flow_input_stmts = [
|
|
2002
|
+
ast.Expr(
|
|
2003
|
+
value=ast.Constant(
|
|
2004
|
+
value="This workflow has no input parameters defined, so there's nothing necessary to supply"
|
|
2005
|
+
)
|
|
2006
|
+
),
|
|
2007
|
+
ast.Pass(),
|
|
2008
|
+
]
|
|
2009
|
+
|
|
1939
2010
|
# Wrap the individual argument processing in an else clause
|
|
1940
2011
|
individual_args_else = ast.If(
|
|
1941
2012
|
test=ast.Compare(
|
|
@@ -2608,7 +2679,7 @@ class WorkflowManager:
|
|
|
2608
2679
|
)
|
|
2609
2680
|
|
|
2610
2681
|
if flow_initialization_command is None:
|
|
2611
|
-
# Construct AST for "GriptapeNodes.ContextManager().flow(GriptapeNodes.ContextManager().
|
|
2682
|
+
# Construct AST for "GriptapeNodes.ContextManager().flow(GriptapeNodes.ContextManager().get_current_flow().flow_name)"
|
|
2612
2683
|
flow_call = ast.Call(
|
|
2613
2684
|
func=ast.Attribute(
|
|
2614
2685
|
value=ast.Call(func=context_manager, args=[], keywords=[], lineno=1, col_offset=0),
|
|
@@ -2618,16 +2689,22 @@ class WorkflowManager:
|
|
|
2618
2689
|
col_offset=0,
|
|
2619
2690
|
),
|
|
2620
2691
|
args=[
|
|
2621
|
-
ast.
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2692
|
+
ast.Attribute(
|
|
2693
|
+
value=ast.Call(
|
|
2694
|
+
func=ast.Attribute(
|
|
2695
|
+
value=ast.Call(func=context_manager, args=[], keywords=[], lineno=1, col_offset=0),
|
|
2696
|
+
attr="get_current_flow",
|
|
2697
|
+
ctx=ast.Load(),
|
|
2698
|
+
lineno=1,
|
|
2699
|
+
col_offset=0,
|
|
2700
|
+
),
|
|
2701
|
+
args=[],
|
|
2702
|
+
keywords=[],
|
|
2626
2703
|
lineno=1,
|
|
2627
2704
|
col_offset=0,
|
|
2628
2705
|
),
|
|
2629
|
-
|
|
2630
|
-
|
|
2706
|
+
attr="flow_name",
|
|
2707
|
+
ctx=ast.Load(),
|
|
2631
2708
|
lineno=1,
|
|
2632
2709
|
col_offset=0,
|
|
2633
2710
|
)
|
|
@@ -3101,17 +3178,12 @@ class WorkflowManager:
|
|
|
3101
3178
|
for node in nodes:
|
|
3102
3179
|
for param in node.parameters:
|
|
3103
3180
|
# Expose only the parameters that are relevant for workflow input and output.
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
if param.type != ParameterTypeBuiltin.CONTROL_TYPE.value and not param.type.startswith("list"):
|
|
3181
|
+
param_info = self.extract_parameter_shape_info(param, include_control_params=True)
|
|
3182
|
+
if param_info is not None:
|
|
3107
3183
|
if node.name in workflow_shape[workflow_shape_type]:
|
|
3108
|
-
cast("dict", workflow_shape[workflow_shape_type][node.name])[param.name] =
|
|
3109
|
-
self._convert_parameter_to_minimal_dict(param)
|
|
3110
|
-
)
|
|
3184
|
+
cast("dict", workflow_shape[workflow_shape_type][node.name])[param.name] = param_info
|
|
3111
3185
|
else:
|
|
3112
|
-
workflow_shape[workflow_shape_type][node.name] = {
|
|
3113
|
-
param.name: self._convert_parameter_to_minimal_dict(param)
|
|
3114
|
-
}
|
|
3186
|
+
workflow_shape[workflow_shape_type][node.name] = {param.name: param_info}
|
|
3115
3187
|
return workflow_shape
|
|
3116
3188
|
|
|
3117
3189
|
def extract_workflow_shape(self, workflow_name: str) -> dict[str, Any]:
|
|
@@ -3162,6 +3234,50 @@ class WorkflowManager:
|
|
|
3162
3234
|
|
|
3163
3235
|
return workflow_shape
|
|
3164
3236
|
|
|
3237
|
+
def extract_parameter_shape_info(
|
|
3238
|
+
self, parameter: Parameter, *, include_control_params: bool
|
|
3239
|
+
) -> ParameterShapeInfo | None:
|
|
3240
|
+
"""Extract shape information from a parameter for workflow shape building.
|
|
3241
|
+
|
|
3242
|
+
Expose only the parameters that are relevant for workflow input and output.
|
|
3243
|
+
|
|
3244
|
+
Args:
|
|
3245
|
+
parameter: The parameter to extract shape info from
|
|
3246
|
+
include_control_params: Whether to include control type parameters (default: False)
|
|
3247
|
+
|
|
3248
|
+
Returns:
|
|
3249
|
+
Parameter info dict if relevant for workflow shape, None if should be excluded
|
|
3250
|
+
"""
|
|
3251
|
+
# TODO (https://github.com/griptape-ai/griptape-nodes/issues/1090): This is a temporary solution until we know how to handle container types.
|
|
3252
|
+
# Always exclude list types until container type handling is implemented
|
|
3253
|
+
if parameter.type.startswith("list"):
|
|
3254
|
+
logger.warning(
|
|
3255
|
+
"Skipping list parameter '%s' of type '%s' in workflow shape - container types not yet supported",
|
|
3256
|
+
parameter.name,
|
|
3257
|
+
parameter.type,
|
|
3258
|
+
)
|
|
3259
|
+
return None
|
|
3260
|
+
|
|
3261
|
+
# Conditionally exclude control types
|
|
3262
|
+
if not include_control_params and parameter.type == ParameterTypeBuiltin.CONTROL_TYPE.value:
|
|
3263
|
+
return None
|
|
3264
|
+
|
|
3265
|
+
return self._convert_parameter_to_minimal_dict(parameter)
|
|
3266
|
+
|
|
3267
|
+
def build_workflow_shape_from_parameter_info(
|
|
3268
|
+
self, input_node_params: WorkflowShapeNodes, output_node_params: WorkflowShapeNodes
|
|
3269
|
+
) -> WorkflowShape:
|
|
3270
|
+
"""Build a WorkflowShape from collected parameter information.
|
|
3271
|
+
|
|
3272
|
+
Args:
|
|
3273
|
+
input_node_params: Mapping of input node names to their parameter info
|
|
3274
|
+
output_node_params: Mapping of output node names to their parameter info
|
|
3275
|
+
|
|
3276
|
+
Returns:
|
|
3277
|
+
WorkflowShape object with inputs and outputs
|
|
3278
|
+
"""
|
|
3279
|
+
return WorkflowShape(inputs=input_node_params, outputs=output_node_params)
|
|
3280
|
+
|
|
3165
3281
|
async def on_publish_workflow_request(self, request: PublishWorkflowRequest) -> ResultPayload:
|
|
3166
3282
|
try:
|
|
3167
3283
|
publisher_name = request.publisher_name
|
|
@@ -3178,12 +3294,80 @@ class WorkflowManager:
|
|
|
3178
3294
|
if isinstance(result, PublishWorkflowResultSuccess):
|
|
3179
3295
|
workflow_file = Path(result.published_workflow_file_path)
|
|
3180
3296
|
result = self._register_published_workflow_file(workflow_file, result)
|
|
3297
|
+
|
|
3298
|
+
if request.execute_on_publish:
|
|
3299
|
+
await self._handle_execute_on_publish(request, workflow_file)
|
|
3300
|
+
|
|
3181
3301
|
return result # noqa: TRY300
|
|
3182
3302
|
except Exception as e:
|
|
3183
3303
|
details = f"Failed to publish workflow '{request.workflow_name}': {e!s}"
|
|
3184
3304
|
logger.exception(details)
|
|
3185
3305
|
return PublishWorkflowResultFailure(exception=e, result_details=details)
|
|
3186
3306
|
|
|
3307
|
+
async def _handle_execute_on_publish(self, request: PublishWorkflowRequest, workflow_file: Path) -> None:
|
|
3308
|
+
class ExecuteOnPublishError(Exception):
|
|
3309
|
+
pass
|
|
3310
|
+
|
|
3311
|
+
# Create an event to track when we get a start workflow result
|
|
3312
|
+
start_result_received = asyncio.Event()
|
|
3313
|
+
exception: ExecuteOnPublishError | None = None
|
|
3314
|
+
|
|
3315
|
+
def invoke_done_callback(future: asyncio.Future) -> None:
|
|
3316
|
+
nonlocal exception
|
|
3317
|
+
try:
|
|
3318
|
+
future.result()
|
|
3319
|
+
except Exception as e:
|
|
3320
|
+
msg = f"Error during invocation of published workflow '{request.workflow_name}': {e!s}"
|
|
3321
|
+
logger.exception(msg)
|
|
3322
|
+
exception = ExecuteOnPublishError(e)
|
|
3323
|
+
start_result_received.set()
|
|
3324
|
+
|
|
3325
|
+
def on_start_flow_result(start_result: ResultPayload) -> None:
|
|
3326
|
+
nonlocal exception
|
|
3327
|
+
if isinstance(start_result, StartFlowResultSuccess):
|
|
3328
|
+
msg = f"Successfully started published workflow '{request.workflow_name}'"
|
|
3329
|
+
logger.info(msg)
|
|
3330
|
+
elif isinstance(start_result, StartFlowResultFailure):
|
|
3331
|
+
msg = f"Failed to invoke published workflow '{request.workflow_name}': {start_result.exception}"
|
|
3332
|
+
logger.exception(msg)
|
|
3333
|
+
exceptions = start_result.validation_exceptions
|
|
3334
|
+
exceptions.append(start_result.exception) if start_result.exception else None
|
|
3335
|
+
exception = ExecuteOnPublishError(exceptions)
|
|
3336
|
+
|
|
3337
|
+
start_result_received.set()
|
|
3338
|
+
|
|
3339
|
+
invoke_task = asyncio.create_task(
|
|
3340
|
+
self._invoke_published_workflow(request.workflow_name, workflow_file, on_start_flow_result)
|
|
3341
|
+
)
|
|
3342
|
+
invoke_task.add_done_callback(invoke_done_callback)
|
|
3343
|
+
|
|
3344
|
+
# Wait for StartFlow result
|
|
3345
|
+
try:
|
|
3346
|
+
await asyncio.wait_for(start_result_received.wait(), timeout=10.0)
|
|
3347
|
+
if exception is not None:
|
|
3348
|
+
raise exception
|
|
3349
|
+
except TimeoutError:
|
|
3350
|
+
# Timeout occurred - don't block on subprocess workflow execution, just log a warning
|
|
3351
|
+
msg = f"Timeout waiting for workflow '{request.workflow_name}' StartFlow result."
|
|
3352
|
+
logger.warning(msg)
|
|
3353
|
+
|
|
3354
|
+
async def _invoke_published_workflow(
|
|
3355
|
+
self, workflow_name: str, workflow_path: Path, on_start_flow_result: Callable[[ResultPayload], None]
|
|
3356
|
+
) -> None:
|
|
3357
|
+
from griptape_nodes.bootstrap.workflow_executors.subprocess_workflow_executor import SubprocessWorkflowExecutor
|
|
3358
|
+
|
|
3359
|
+
subprocess_executor = SubprocessWorkflowExecutor(
|
|
3360
|
+
workflow_path=str(workflow_path), on_start_flow_result=on_start_flow_result
|
|
3361
|
+
)
|
|
3362
|
+
storage_backend_value = GriptapeNodes.ConfigManager().get_config_value("storage_backend")
|
|
3363
|
+
|
|
3364
|
+
async with subprocess_executor as executor:
|
|
3365
|
+
await executor.arun(
|
|
3366
|
+
workflow_name=workflow_name,
|
|
3367
|
+
flow_input={},
|
|
3368
|
+
storage_backend=StorageBackend(value=storage_backend_value),
|
|
3369
|
+
)
|
|
3370
|
+
|
|
3187
3371
|
def _register_published_workflow_file(
|
|
3188
3372
|
self, workflow_file: Path, result: PublishWorkflowResultSuccess
|
|
3189
3373
|
) -> ResultPayload:
|