griptape-nodes 0.56.0__py3-none-any.whl → 0.57.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. griptape_nodes/app/app.py +10 -15
  2. griptape_nodes/app/watch.py +35 -67
  3. griptape_nodes/bootstrap/utils/__init__.py +1 -0
  4. griptape_nodes/bootstrap/utils/python_subprocess_executor.py +122 -0
  5. griptape_nodes/bootstrap/workflow_executors/local_session_workflow_executor.py +418 -0
  6. griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +37 -8
  7. griptape_nodes/bootstrap/workflow_executors/subprocess_workflow_executor.py +326 -0
  8. griptape_nodes/bootstrap/workflow_executors/utils/__init__.py +1 -0
  9. griptape_nodes/bootstrap/workflow_executors/utils/subprocess_script.py +51 -0
  10. griptape_nodes/bootstrap/workflow_publishers/__init__.py +1 -0
  11. griptape_nodes/bootstrap/workflow_publishers/local_workflow_publisher.py +43 -0
  12. griptape_nodes/bootstrap/workflow_publishers/subprocess_workflow_publisher.py +84 -0
  13. griptape_nodes/bootstrap/workflow_publishers/utils/__init__.py +1 -0
  14. griptape_nodes/bootstrap/workflow_publishers/utils/subprocess_script.py +54 -0
  15. griptape_nodes/cli/commands/engine.py +4 -15
  16. griptape_nodes/cli/main.py +6 -1
  17. griptape_nodes/exe_types/core_types.py +26 -0
  18. griptape_nodes/exe_types/node_types.py +116 -1
  19. griptape_nodes/retained_mode/events/agent_events.py +2 -0
  20. griptape_nodes/retained_mode/events/base_events.py +18 -17
  21. griptape_nodes/retained_mode/events/execution_events.py +3 -1
  22. griptape_nodes/retained_mode/events/flow_events.py +5 -7
  23. griptape_nodes/retained_mode/events/mcp_events.py +363 -0
  24. griptape_nodes/retained_mode/events/node_events.py +3 -4
  25. griptape_nodes/retained_mode/griptape_nodes.py +8 -0
  26. griptape_nodes/retained_mode/managers/agent_manager.py +67 -4
  27. griptape_nodes/retained_mode/managers/event_manager.py +31 -13
  28. griptape_nodes/retained_mode/managers/flow_manager.py +76 -44
  29. griptape_nodes/retained_mode/managers/library_manager.py +7 -9
  30. griptape_nodes/retained_mode/managers/mcp_manager.py +364 -0
  31. griptape_nodes/retained_mode/managers/node_manager.py +12 -1
  32. griptape_nodes/retained_mode/managers/settings.py +40 -0
  33. griptape_nodes/retained_mode/managers/workflow_manager.py +94 -8
  34. griptape_nodes/traits/multi_options.py +5 -1
  35. griptape_nodes/traits/options.py +10 -2
  36. {griptape_nodes-0.56.0.dist-info → griptape_nodes-0.57.0.dist-info}/METADATA +2 -2
  37. {griptape_nodes-0.56.0.dist-info → griptape_nodes-0.57.0.dist-info}/RECORD +39 -26
  38. {griptape_nodes-0.56.0.dist-info → griptape_nodes-0.57.0.dist-info}/WHEEL +0 -0
  39. {griptape_nodes-0.56.0.dist-info → griptape_nodes-0.57.0.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(no_update=no_update)
25
+ _start_engine()
29
26
 
30
27
 
31
- def _start_engine(*, no_update: bool = False) -> None:
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
 
@@ -10,6 +10,7 @@ from rich.console import Console
10
10
  sys.path.append(str(Path.cwd()))
11
11
 
12
12
  from griptape_nodes.cli.commands import config, engine, init, libraries, models, self
13
+ from griptape_nodes.cli.commands.engine import _auto_update_self
13
14
  from griptape_nodes.utils.version_utils import get_complete_version_string
14
15
 
15
16
  console = Console()
@@ -47,9 +48,13 @@ def main(
47
48
  console.print(f"[bold green]{version_string}[/bold green]")
48
49
  raise typer.Exit
49
50
 
51
+ # Run auto-update check for any command (unless disabled)
52
+ if not no_update:
53
+ _auto_update_self()
54
+
50
55
  if ctx.invoked_subcommand is None:
51
56
  # Default to engine command when no subcommand is specified
52
- engine.engine_command(no_update=no_update)
57
+ engine.engine_command()
53
58
 
54
59
 
55
60
  if __name__ == "__main__":