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
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import tempfile
|
|
7
|
+
import threading
|
|
8
|
+
import uuid
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Self
|
|
11
|
+
|
|
12
|
+
import anyio
|
|
13
|
+
from websockets.exceptions import ConnectionClosed, ConnectionClosedError
|
|
14
|
+
|
|
15
|
+
from griptape_nodes.app.app import _create_websocket_connection, _send_subscribe_command
|
|
16
|
+
from griptape_nodes.bootstrap.utils.python_subprocess_executor import PythonSubprocessExecutor
|
|
17
|
+
from griptape_nodes.bootstrap.workflow_executors.local_session_workflow_executor import LocalSessionWorkflowExecutor
|
|
18
|
+
from griptape_nodes.drivers.storage import StorageBackend
|
|
19
|
+
from griptape_nodes.retained_mode.events.base_events import (
|
|
20
|
+
EventResultFailure,
|
|
21
|
+
EventResultSuccess,
|
|
22
|
+
ExecutionEvent,
|
|
23
|
+
ResultPayload,
|
|
24
|
+
)
|
|
25
|
+
from griptape_nodes.retained_mode.events.execution_events import (
|
|
26
|
+
ControlFlowCancelledEvent,
|
|
27
|
+
ControlFlowResolvedEvent,
|
|
28
|
+
StartFlowRequest,
|
|
29
|
+
)
|
|
30
|
+
from griptape_nodes.retained_mode.events.payload_registry import PayloadRegistry
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from collections.abc import Callable
|
|
34
|
+
from types import TracebackType
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class SubprocessWorkflowExecutorError(Exception):
|
|
40
|
+
"""Exception raised during subprocess workflow execution."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class SubprocessWorkflowExecutor(LocalSessionWorkflowExecutor, PythonSubprocessExecutor):
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
workflow_path: str,
|
|
47
|
+
on_start_flow_result: Callable[[ResultPayload], None] | None = None,
|
|
48
|
+
session_id: str | None = None,
|
|
49
|
+
) -> None:
|
|
50
|
+
PythonSubprocessExecutor.__init__(self)
|
|
51
|
+
self._workflow_path = workflow_path
|
|
52
|
+
self._on_start_flow_result = on_start_flow_result
|
|
53
|
+
# Generate a unique session ID for this execution
|
|
54
|
+
self._session_id = session_id or uuid.uuid4().hex
|
|
55
|
+
self._websocket_thread: threading.Thread | None = None
|
|
56
|
+
self._websocket_event_loop: asyncio.AbstractEventLoop | None = None
|
|
57
|
+
self._websocket_event_loop_ready = threading.Event()
|
|
58
|
+
self._event_handlers: dict[str, list] = {}
|
|
59
|
+
self._shutdown_event: asyncio.Event | None = None
|
|
60
|
+
self._stored_exception: SubprocessWorkflowExecutorError | None = None
|
|
61
|
+
|
|
62
|
+
async def __aenter__(self) -> Self:
|
|
63
|
+
"""Async context manager entry: start WebSocket connection."""
|
|
64
|
+
logger.info("Starting WebSocket listener for session %s", self._session_id)
|
|
65
|
+
await self._start_websocket_listener()
|
|
66
|
+
return self
|
|
67
|
+
|
|
68
|
+
async def __aexit__(
|
|
69
|
+
self,
|
|
70
|
+
exc_type: type[BaseException] | None,
|
|
71
|
+
exc_val: BaseException | None,
|
|
72
|
+
exc_tb: TracebackType | None,
|
|
73
|
+
) -> None:
|
|
74
|
+
"""Async context manager exit: stop WebSocket connection."""
|
|
75
|
+
logger.info("Stopping WebSocket listener for session %s", self._session_id)
|
|
76
|
+
self._stop_websocket_listener()
|
|
77
|
+
|
|
78
|
+
async def arun(
|
|
79
|
+
self,
|
|
80
|
+
workflow_name: str, # noqa: ARG002
|
|
81
|
+
flow_input: Any,
|
|
82
|
+
storage_backend: StorageBackend = StorageBackend.LOCAL,
|
|
83
|
+
**kwargs: Any, # noqa: ARG002
|
|
84
|
+
) -> None:
|
|
85
|
+
"""Execute a workflow in a subprocess and wait for completion."""
|
|
86
|
+
script_path = Path(__file__).parent / "utils" / "subprocess_script.py"
|
|
87
|
+
|
|
88
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
89
|
+
tmp_workflow_path = Path(tmpdir) / "workflow.py"
|
|
90
|
+
tmp_script_path = Path(tmpdir) / "subprocess_script.py"
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
async with (
|
|
94
|
+
await anyio.open_file(self._workflow_path, "rb") as src,
|
|
95
|
+
await anyio.open_file(tmp_workflow_path, "wb") as dst,
|
|
96
|
+
):
|
|
97
|
+
await dst.write(await src.read())
|
|
98
|
+
|
|
99
|
+
async with (
|
|
100
|
+
await anyio.open_file(script_path, "rb") as src,
|
|
101
|
+
await anyio.open_file(tmp_script_path, "wb") as dst,
|
|
102
|
+
):
|
|
103
|
+
await dst.write(await src.read())
|
|
104
|
+
except Exception as e:
|
|
105
|
+
msg = f"Failed to copy workflow or script to temp directory: {e}"
|
|
106
|
+
logger.exception(msg)
|
|
107
|
+
raise SubprocessWorkflowExecutorError(msg) from e
|
|
108
|
+
|
|
109
|
+
args = [
|
|
110
|
+
"--json-input",
|
|
111
|
+
json.dumps(flow_input),
|
|
112
|
+
"--session-id",
|
|
113
|
+
self._session_id,
|
|
114
|
+
"--storage-backend",
|
|
115
|
+
storage_backend.value,
|
|
116
|
+
"--workflow-path",
|
|
117
|
+
str(tmp_workflow_path),
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
await self.execute_python_script(
|
|
122
|
+
script_path=tmp_script_path,
|
|
123
|
+
args=args,
|
|
124
|
+
cwd=Path(tmpdir),
|
|
125
|
+
)
|
|
126
|
+
except Exception as e:
|
|
127
|
+
msg = f"Failed to execute subprocess script: {e}"
|
|
128
|
+
logger.exception(msg)
|
|
129
|
+
raise SubprocessWorkflowExecutorError(msg) from e
|
|
130
|
+
finally:
|
|
131
|
+
# Check if an exception was stored coming from the WebSocket
|
|
132
|
+
if self._stored_exception:
|
|
133
|
+
raise self._stored_exception
|
|
134
|
+
|
|
135
|
+
async def _start_websocket_listener(self) -> None:
|
|
136
|
+
"""Start WebSocket connection to listen for events from the subprocess."""
|
|
137
|
+
logger.info("Starting WebSocket listener for session %s", self._session_id)
|
|
138
|
+
api_key = self._get_api_key()
|
|
139
|
+
if api_key is None:
|
|
140
|
+
logger.warning("No API key found, WebSocket listener will not be started")
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
logger.info("API key found, starting WebSocket listener thread")
|
|
144
|
+
self._websocket_thread = threading.Thread(target=self._start_websocket_thread, args=(api_key,), daemon=True)
|
|
145
|
+
self._websocket_thread.start()
|
|
146
|
+
|
|
147
|
+
if self._websocket_event_loop_ready.wait(timeout=10):
|
|
148
|
+
logger.info("WebSocket listener thread ready")
|
|
149
|
+
else:
|
|
150
|
+
logger.error("Timeout waiting for WebSocket listener thread to start")
|
|
151
|
+
|
|
152
|
+
def _stop_websocket_listener(self) -> None:
|
|
153
|
+
"""Stop the WebSocket listener thread."""
|
|
154
|
+
if self._websocket_thread is None or not self._websocket_thread.is_alive():
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
logger.info("Stopping WebSocket listener thread")
|
|
158
|
+
self._websocket_event_loop_ready.clear()
|
|
159
|
+
|
|
160
|
+
# Signal shutdown to the websocket tasks
|
|
161
|
+
if self._websocket_event_loop and self._websocket_event_loop.is_running() and self._shutdown_event:
|
|
162
|
+
|
|
163
|
+
def signal_shutdown() -> None:
|
|
164
|
+
if self._shutdown_event:
|
|
165
|
+
self._shutdown_event.set()
|
|
166
|
+
|
|
167
|
+
self._websocket_event_loop.call_soon_threadsafe(signal_shutdown)
|
|
168
|
+
|
|
169
|
+
# Wait for thread to finish
|
|
170
|
+
self._websocket_thread.join(timeout=5.0)
|
|
171
|
+
if self._websocket_thread.is_alive():
|
|
172
|
+
logger.warning("WebSocket listener thread did not stop gracefully")
|
|
173
|
+
else:
|
|
174
|
+
logger.info("WebSocket listener thread stopped successfully")
|
|
175
|
+
|
|
176
|
+
def _start_websocket_thread(self, api_key: str) -> None:
|
|
177
|
+
"""Run WebSocket tasks in a separate thread with its own async loop."""
|
|
178
|
+
try:
|
|
179
|
+
# Create a new event loop for this thread
|
|
180
|
+
loop = asyncio.new_event_loop()
|
|
181
|
+
self._websocket_event_loop = loop
|
|
182
|
+
asyncio.set_event_loop(loop)
|
|
183
|
+
|
|
184
|
+
# Create shutdown event
|
|
185
|
+
self._shutdown_event = asyncio.Event()
|
|
186
|
+
|
|
187
|
+
# Signal that websocket_event_loop is ready
|
|
188
|
+
self._websocket_event_loop_ready.set()
|
|
189
|
+
logger.info("WebSocket listener thread started and ready")
|
|
190
|
+
|
|
191
|
+
# Run the async WebSocket listener
|
|
192
|
+
loop.run_until_complete(self._run_websocket_listener(api_key))
|
|
193
|
+
except Exception as e:
|
|
194
|
+
logger.error("WebSocket listener thread error: %s", e)
|
|
195
|
+
finally:
|
|
196
|
+
self._websocket_event_loop = None
|
|
197
|
+
self._websocket_event_loop_ready.clear()
|
|
198
|
+
self._shutdown_event = None
|
|
199
|
+
logger.info("WebSocket listener thread ended")
|
|
200
|
+
|
|
201
|
+
async def _run_websocket_listener(self, api_key: str) -> None:
|
|
202
|
+
"""Run WebSocket listener - establish connection and handle incoming messages."""
|
|
203
|
+
logger.info("Creating WebSocket connection stream for listening")
|
|
204
|
+
connection_stream = _create_websocket_connection(api_key)
|
|
205
|
+
|
|
206
|
+
async for ws_connection in connection_stream:
|
|
207
|
+
logger.info("WebSocket connection established for session %s", self._session_id)
|
|
208
|
+
try:
|
|
209
|
+
# Listen for incoming messages
|
|
210
|
+
await self._listen_for_messages(ws_connection)
|
|
211
|
+
|
|
212
|
+
except (ConnectionClosed, ConnectionClosedError):
|
|
213
|
+
logger.info("WebSocket connection closed, reconnecting...")
|
|
214
|
+
continue
|
|
215
|
+
except asyncio.CancelledError:
|
|
216
|
+
logger.info("WebSocket listener task cancelled, shutting down")
|
|
217
|
+
break
|
|
218
|
+
except Exception:
|
|
219
|
+
logger.exception("WebSocket listener failed")
|
|
220
|
+
await asyncio.sleep(2.0)
|
|
221
|
+
continue
|
|
222
|
+
|
|
223
|
+
# Check if shutdown was requested
|
|
224
|
+
if self._shutdown_event and self._shutdown_event.is_set():
|
|
225
|
+
logger.info("Shutdown requested, ending WebSocket listener connection loop")
|
|
226
|
+
break
|
|
227
|
+
|
|
228
|
+
logger.info("WebSocket listener connection loop ended")
|
|
229
|
+
|
|
230
|
+
async def _listen_for_messages(self, ws_connection: Any) -> None:
|
|
231
|
+
"""Listen for incoming WebSocket messages from the subprocess."""
|
|
232
|
+
logger.info("Starting to listen for WebSocket messages")
|
|
233
|
+
|
|
234
|
+
# Subscribe to the session topic to receive messages
|
|
235
|
+
topic = f"sessions/{self._session_id}/response"
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
await _send_subscribe_command(
|
|
239
|
+
ws_connection=ws_connection,
|
|
240
|
+
topic=topic,
|
|
241
|
+
)
|
|
242
|
+
async for message in ws_connection:
|
|
243
|
+
if self._shutdown_event and self._shutdown_event.is_set():
|
|
244
|
+
logger.info("Shutdown requested, ending message listener")
|
|
245
|
+
break
|
|
246
|
+
|
|
247
|
+
try:
|
|
248
|
+
data = json.loads(message)
|
|
249
|
+
logger.debug("Received WebSocket message: %s", data.get("type"))
|
|
250
|
+
await self._process_event(data)
|
|
251
|
+
|
|
252
|
+
except json.JSONDecodeError:
|
|
253
|
+
logger.warning("Failed to parse WebSocket message: %s", message)
|
|
254
|
+
except Exception:
|
|
255
|
+
logger.exception("Error processing WebSocket message")
|
|
256
|
+
|
|
257
|
+
except Exception as e:
|
|
258
|
+
logger.error("Error in WebSocket message listener: %s", e)
|
|
259
|
+
raise
|
|
260
|
+
|
|
261
|
+
async def _process_event(self, event: dict) -> None:
|
|
262
|
+
"""Process events received from the subprocess via WebSocket."""
|
|
263
|
+
event_type = event.get("type", "unknown")
|
|
264
|
+
if event_type == "execution_event":
|
|
265
|
+
await self._process_execution_event(event)
|
|
266
|
+
elif event_type in ["success_result", "failure_result"]:
|
|
267
|
+
await self._process_result_event(event)
|
|
268
|
+
|
|
269
|
+
async def _process_execution_event(self, event: dict) -> None:
|
|
270
|
+
payload = event.get("payload", {})
|
|
271
|
+
event_type = payload.get("event_type", "")
|
|
272
|
+
payload_type_name = payload.get("payload_type", "")
|
|
273
|
+
payload_type = PayloadRegistry.get_type(payload_type_name)
|
|
274
|
+
|
|
275
|
+
# Focusing on ExecutionEvent types for the workflow executor
|
|
276
|
+
if event_type not in ["ExecutionEvent", "EventResultSuccess", "EventResultFailure"]:
|
|
277
|
+
logger.debug("Ignoring event type: %s", event_type)
|
|
278
|
+
return
|
|
279
|
+
|
|
280
|
+
if payload_type is None:
|
|
281
|
+
logger.warning("Unknown payload type: %s", payload_type_name)
|
|
282
|
+
return
|
|
283
|
+
|
|
284
|
+
ex_event = ExecutionEvent.from_dict(data=payload, payload_type=payload_type)
|
|
285
|
+
|
|
286
|
+
if isinstance(ex_event.payload, ControlFlowResolvedEvent):
|
|
287
|
+
logger.info("Workflow execution completed successfully")
|
|
288
|
+
self.output = {ex_event.payload.end_node_name: ex_event.payload.parameter_output_values}
|
|
289
|
+
|
|
290
|
+
if isinstance(ex_event.payload, ControlFlowCancelledEvent):
|
|
291
|
+
logger.error("Workflow execution cancelled")
|
|
292
|
+
|
|
293
|
+
details = ex_event.payload.result_details or "No details provided"
|
|
294
|
+
msg = f"Workflow execution cancelled: {details}"
|
|
295
|
+
|
|
296
|
+
if ex_event.payload.exception:
|
|
297
|
+
msg = f"Exception running workflow: {ex_event.payload.exception}"
|
|
298
|
+
self._stored_exception = SubprocessWorkflowExecutorError(ex_event.payload.exception)
|
|
299
|
+
else:
|
|
300
|
+
self._stored_exception = SubprocessWorkflowExecutorError(msg)
|
|
301
|
+
|
|
302
|
+
async def _process_result_event(self, event: dict) -> None:
|
|
303
|
+
payload = event.get("payload", {})
|
|
304
|
+
request_type_name = payload.get("request_type", "")
|
|
305
|
+
response_type_name = payload.get("result_type", "")
|
|
306
|
+
request_payload_type = PayloadRegistry.get_type(request_type_name)
|
|
307
|
+
response_payload_type = PayloadRegistry.get_type(response_type_name)
|
|
308
|
+
|
|
309
|
+
if request_payload_type is None or response_payload_type is None:
|
|
310
|
+
logger.warning("Unknown payload types: %s, %s", request_type_name, response_type_name)
|
|
311
|
+
return
|
|
312
|
+
if payload.get("type", "unknown") == "success_result":
|
|
313
|
+
result_event = EventResultSuccess.from_dict(
|
|
314
|
+
data=payload, req_payload_type=request_payload_type, res_payload_type=response_payload_type
|
|
315
|
+
)
|
|
316
|
+
else:
|
|
317
|
+
result_event = EventResultFailure.from_dict(
|
|
318
|
+
data=payload, req_payload_type=request_payload_type, res_payload_type=response_payload_type
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
if isinstance(result_event.request, StartFlowRequest):
|
|
322
|
+
logger.info("Received StartFlowRequest result event")
|
|
323
|
+
if self._on_start_flow_result:
|
|
324
|
+
self._on_start_flow_result(result_event.result)
|
|
325
|
+
else:
|
|
326
|
+
logger.warning("Ignoring result event for request type: %s", request_type_name)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Workflow executors utils package."""
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Subprocess script to execute a Griptape Nodes workflow.
|
|
2
|
+
|
|
3
|
+
This script is intended to be run as a subprocess by the SubprocessWorkflowExecutor.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
from argparse import ArgumentParser
|
|
8
|
+
|
|
9
|
+
from workflow import execute_workflow # type: ignore[attr-defined]
|
|
10
|
+
|
|
11
|
+
from griptape_nodes.bootstrap.workflow_executors.local_session_workflow_executor import LocalSessionWorkflowExecutor
|
|
12
|
+
from griptape_nodes.drivers.storage import StorageBackend
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _main() -> None:
|
|
16
|
+
parser = ArgumentParser()
|
|
17
|
+
parser.add_argument(
|
|
18
|
+
"--json-input",
|
|
19
|
+
default=json.dumps({}),
|
|
20
|
+
help="JSON string representing the flow input",
|
|
21
|
+
)
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--session-id",
|
|
24
|
+
default=None,
|
|
25
|
+
help="ID of the session to use",
|
|
26
|
+
)
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"--storage-backend",
|
|
29
|
+
default="local",
|
|
30
|
+
help="Storage backend to use",
|
|
31
|
+
)
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"--workflow-path",
|
|
34
|
+
default=None,
|
|
35
|
+
help="Path to the Griptape Nodes workflow file",
|
|
36
|
+
)
|
|
37
|
+
args = parser.parse_args()
|
|
38
|
+
flow_input = json.loads(args.json_input)
|
|
39
|
+
|
|
40
|
+
local_session_workflow_executor = LocalSessionWorkflowExecutor(
|
|
41
|
+
session_id=args.session_id, storage_backend=StorageBackend(args.storage_backend)
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
execute_workflow(
|
|
45
|
+
input=flow_input,
|
|
46
|
+
workflow_executor=local_session_workflow_executor,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
_main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Workflow publishers package."""
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from griptape_nodes.bootstrap.workflow_executors.local_workflow_executor import LocalWorkflowExecutor
|
|
7
|
+
from griptape_nodes.retained_mode.events.workflow_events import PublishWorkflowRequest, PublishWorkflowResultSuccess
|
|
8
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LocalPublisherError(Exception):
|
|
14
|
+
"""Exception raised during local workflow publish."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class LocalWorkflowPublisher(LocalWorkflowExecutor):
|
|
18
|
+
def __init__(self) -> None:
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
async def arun(
|
|
22
|
+
self,
|
|
23
|
+
workflow_name: str,
|
|
24
|
+
workflow_path: str,
|
|
25
|
+
publisher_name: str,
|
|
26
|
+
published_workflow_file_name: str,
|
|
27
|
+
**kwargs: Any, # noqa: ARG002
|
|
28
|
+
) -> None:
|
|
29
|
+
# Load the workflow into memory
|
|
30
|
+
await self.aprepare_workflow_for_run(workflow_name=workflow_name, flow_input={}, workflow_path=workflow_path)
|
|
31
|
+
publish_workflow_request = PublishWorkflowRequest(
|
|
32
|
+
workflow_name=workflow_name,
|
|
33
|
+
publisher_name=publisher_name,
|
|
34
|
+
execute_on_publish=False,
|
|
35
|
+
published_workflow_file_name=published_workflow_file_name,
|
|
36
|
+
)
|
|
37
|
+
publish_workflow_result = await GriptapeNodes.ahandle_request(publish_workflow_request)
|
|
38
|
+
|
|
39
|
+
if isinstance(publish_workflow_result, PublishWorkflowResultSuccess):
|
|
40
|
+
logger.info("Published workflow to %s", publish_workflow_result.published_workflow_file_path)
|
|
41
|
+
else:
|
|
42
|
+
msg = f"Failed to publish workflow: {publish_workflow_result}"
|
|
43
|
+
raise LocalPublisherError(msg)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import tempfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Self
|
|
7
|
+
|
|
8
|
+
import anyio
|
|
9
|
+
|
|
10
|
+
from griptape_nodes.bootstrap.utils.python_subprocess_executor import PythonSubprocessExecutor
|
|
11
|
+
from griptape_nodes.bootstrap.workflow_publishers.local_workflow_publisher import LocalWorkflowPublisher
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from types import TracebackType
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SubprocessWorkflowPublisherError(Exception):
|
|
20
|
+
"""Exception raised during subprocess workflow publishing."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SubprocessWorkflowPublisher(LocalWorkflowPublisher, PythonSubprocessExecutor):
|
|
24
|
+
def __init__(self) -> None:
|
|
25
|
+
PythonSubprocessExecutor.__init__(self)
|
|
26
|
+
|
|
27
|
+
async def __aenter__(self) -> Self:
|
|
28
|
+
return self
|
|
29
|
+
|
|
30
|
+
async def __aexit__(
|
|
31
|
+
self,
|
|
32
|
+
exc_type: type[BaseException] | None,
|
|
33
|
+
exc_val: BaseException | None,
|
|
34
|
+
exc_tb: TracebackType | None,
|
|
35
|
+
) -> None:
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
async def arun(
|
|
39
|
+
self,
|
|
40
|
+
workflow_name: str,
|
|
41
|
+
workflow_path: str,
|
|
42
|
+
publisher_name: str,
|
|
43
|
+
published_workflow_file_name: str,
|
|
44
|
+
**kwargs: Any, # noqa: ARG002
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Publish a workflow in a subprocess and wait for completion."""
|
|
47
|
+
script_path = Path(__file__).parent / "utils" / "subprocess_script.py"
|
|
48
|
+
|
|
49
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
50
|
+
tmp_workflow_path = Path(tmpdir) / "workflow.py"
|
|
51
|
+
tmp_script_path = Path(tmpdir) / "subprocess_script.py"
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
async with (
|
|
55
|
+
await anyio.open_file(workflow_path, "rb") as src,
|
|
56
|
+
await anyio.open_file(tmp_workflow_path, "wb") as dst,
|
|
57
|
+
):
|
|
58
|
+
await dst.write(await src.read())
|
|
59
|
+
|
|
60
|
+
async with (
|
|
61
|
+
await anyio.open_file(script_path, "rb") as src,
|
|
62
|
+
await anyio.open_file(tmp_script_path, "wb") as dst,
|
|
63
|
+
):
|
|
64
|
+
await dst.write(await src.read())
|
|
65
|
+
except Exception as e:
|
|
66
|
+
msg = f"Failed to copy workflow or script to temp directory: {e}"
|
|
67
|
+
logger.exception(msg)
|
|
68
|
+
raise SubprocessWorkflowPublisherError(msg) from e
|
|
69
|
+
|
|
70
|
+
args = [
|
|
71
|
+
"--workflow-name",
|
|
72
|
+
workflow_name,
|
|
73
|
+
"--workflow-path",
|
|
74
|
+
str(tmp_workflow_path),
|
|
75
|
+
"--publisher-name",
|
|
76
|
+
publisher_name,
|
|
77
|
+
"--published-workflow-file-name",
|
|
78
|
+
published_workflow_file_name,
|
|
79
|
+
]
|
|
80
|
+
await self.execute_python_script(
|
|
81
|
+
script_path=tmp_script_path,
|
|
82
|
+
args=args,
|
|
83
|
+
cwd=Path(tmpdir),
|
|
84
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Workflow publishers utils package."""
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from argparse import ArgumentParser
|
|
4
|
+
|
|
5
|
+
from griptape_nodes.bootstrap.workflow_publishers.local_workflow_publisher import LocalWorkflowPublisher
|
|
6
|
+
|
|
7
|
+
logging.basicConfig(level=logging.INFO)
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def _main(workflow_name: str, workflow_path: str, publisher_name: str, published_workflow_file_name: str) -> None:
|
|
13
|
+
local_publisher = LocalWorkflowPublisher()
|
|
14
|
+
async with local_publisher as publisher:
|
|
15
|
+
await publisher.arun(
|
|
16
|
+
workflow_name=workflow_name,
|
|
17
|
+
workflow_path=workflow_path,
|
|
18
|
+
publisher_name=publisher_name,
|
|
19
|
+
published_workflow_file_name=published_workflow_file_name,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
msg = f"Published workflow to file: {published_workflow_file_name}"
|
|
23
|
+
logger.info(msg)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
if __name__ == "__main__":
|
|
27
|
+
parser = ArgumentParser()
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
"--workflow-name",
|
|
30
|
+
help="Name of the workflow to publish",
|
|
31
|
+
required=True,
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"--workflow-path",
|
|
35
|
+
help="Path to the workflow file to publish",
|
|
36
|
+
required=True,
|
|
37
|
+
)
|
|
38
|
+
parser.add_argument(
|
|
39
|
+
"--publisher-name",
|
|
40
|
+
help="Name of the publisher to use",
|
|
41
|
+
required=True,
|
|
42
|
+
)
|
|
43
|
+
parser.add_argument(
|
|
44
|
+
"--published-workflow-file-name", help="Name to use for the published workflow file", required=True
|
|
45
|
+
)
|
|
46
|
+
args = parser.parse_args()
|
|
47
|
+
asyncio.run(
|
|
48
|
+
_main(
|
|
49
|
+
workflow_name=args.workflow_name,
|
|
50
|
+
workflow_path=args.workflow_path,
|
|
51
|
+
publisher_name=args.publisher_name,
|
|
52
|
+
published_workflow_file_name=args.published_workflow_file_name,
|
|
53
|
+
)
|
|
54
|
+
)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"""Engine command for Griptape Nodes CLI."""
|
|
2
2
|
|
|
3
|
-
import typer
|
|
4
3
|
from rich.prompt import Confirm
|
|
5
4
|
|
|
6
5
|
from griptape_nodes.app import start_app
|
|
@@ -21,19 +20,13 @@ from griptape_nodes.cli.shared import (
|
|
|
21
20
|
from griptape_nodes.utils.version_utils import get_current_version, get_install_source
|
|
22
21
|
|
|
23
22
|
|
|
24
|
-
def engine_command(
|
|
25
|
-
no_update: bool = typer.Option(False, "--no-update", help="Skip the auto-update check."), # noqa: FBT001
|
|
26
|
-
) -> None:
|
|
23
|
+
def engine_command() -> None:
|
|
27
24
|
"""Run the Griptape Nodes engine."""
|
|
28
|
-
_start_engine(
|
|
25
|
+
_start_engine()
|
|
29
26
|
|
|
30
27
|
|
|
31
|
-
def _start_engine(
|
|
32
|
-
"""Starts the Griptape Nodes engine.
|
|
33
|
-
|
|
34
|
-
Args:
|
|
35
|
-
no_update (bool): If True, skips the auto-update check.
|
|
36
|
-
"""
|
|
28
|
+
def _start_engine() -> None:
|
|
29
|
+
"""Starts the Griptape Nodes engine."""
|
|
37
30
|
if not CONFIG_DIR.exists():
|
|
38
31
|
# Default init flow if there is no config directory
|
|
39
32
|
console.print("[bold green]Config directory not found. Initializing...[/bold green]")
|
|
@@ -51,10 +44,6 @@ def _start_engine(*, no_update: bool = False) -> None:
|
|
|
51
44
|
)
|
|
52
45
|
)
|
|
53
46
|
|
|
54
|
-
# Confusing double negation -- If `no_update` is set, we want to skip the update
|
|
55
|
-
if not no_update:
|
|
56
|
-
_auto_update_self()
|
|
57
|
-
|
|
58
47
|
console.print("[bold green]Starting Griptape Nodes engine...[/bold green]")
|
|
59
48
|
start_app()
|
|
60
49
|
|