griptape-nodes 0.52.1__py3-none-any.whl → 0.54.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 (71) hide show
  1. griptape_nodes/__init__.py +8 -942
  2. griptape_nodes/__main__.py +6 -0
  3. griptape_nodes/app/app.py +48 -86
  4. griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +35 -5
  5. griptape_nodes/bootstrap/workflow_executors/workflow_executor.py +15 -1
  6. griptape_nodes/cli/__init__.py +1 -0
  7. griptape_nodes/cli/commands/__init__.py +1 -0
  8. griptape_nodes/cli/commands/config.py +74 -0
  9. griptape_nodes/cli/commands/engine.py +80 -0
  10. griptape_nodes/cli/commands/init.py +550 -0
  11. griptape_nodes/cli/commands/libraries.py +96 -0
  12. griptape_nodes/cli/commands/models.py +504 -0
  13. griptape_nodes/cli/commands/self.py +120 -0
  14. griptape_nodes/cli/main.py +56 -0
  15. griptape_nodes/cli/shared.py +75 -0
  16. griptape_nodes/common/__init__.py +1 -0
  17. griptape_nodes/common/directed_graph.py +71 -0
  18. griptape_nodes/drivers/storage/base_storage_driver.py +40 -20
  19. griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +24 -29
  20. griptape_nodes/drivers/storage/local_storage_driver.py +23 -14
  21. griptape_nodes/exe_types/core_types.py +60 -2
  22. griptape_nodes/exe_types/node_types.py +257 -38
  23. griptape_nodes/exe_types/param_components/__init__.py +1 -0
  24. griptape_nodes/exe_types/param_components/execution_status_component.py +138 -0
  25. griptape_nodes/machines/control_flow.py +195 -94
  26. griptape_nodes/machines/dag_builder.py +207 -0
  27. griptape_nodes/machines/fsm.py +10 -1
  28. griptape_nodes/machines/parallel_resolution.py +558 -0
  29. griptape_nodes/machines/{node_resolution.py → sequential_resolution.py} +30 -57
  30. griptape_nodes/node_library/library_registry.py +34 -1
  31. griptape_nodes/retained_mode/events/app_events.py +5 -1
  32. griptape_nodes/retained_mode/events/base_events.py +9 -9
  33. griptape_nodes/retained_mode/events/config_events.py +30 -0
  34. griptape_nodes/retained_mode/events/execution_events.py +2 -2
  35. griptape_nodes/retained_mode/events/model_events.py +296 -0
  36. griptape_nodes/retained_mode/events/node_events.py +4 -3
  37. griptape_nodes/retained_mode/griptape_nodes.py +34 -12
  38. griptape_nodes/retained_mode/managers/agent_manager.py +23 -5
  39. griptape_nodes/retained_mode/managers/arbitrary_code_exec_manager.py +3 -1
  40. griptape_nodes/retained_mode/managers/config_manager.py +44 -3
  41. griptape_nodes/retained_mode/managers/context_manager.py +6 -5
  42. griptape_nodes/retained_mode/managers/event_manager.py +8 -2
  43. griptape_nodes/retained_mode/managers/flow_manager.py +150 -206
  44. griptape_nodes/retained_mode/managers/library_lifecycle/library_directory.py +1 -1
  45. griptape_nodes/retained_mode/managers/library_manager.py +35 -25
  46. griptape_nodes/retained_mode/managers/model_manager.py +1107 -0
  47. griptape_nodes/retained_mode/managers/node_manager.py +102 -220
  48. griptape_nodes/retained_mode/managers/object_manager.py +11 -5
  49. griptape_nodes/retained_mode/managers/os_manager.py +28 -13
  50. griptape_nodes/retained_mode/managers/secrets_manager.py +8 -4
  51. griptape_nodes/retained_mode/managers/settings.py +116 -7
  52. griptape_nodes/retained_mode/managers/static_files_manager.py +85 -12
  53. griptape_nodes/retained_mode/managers/sync_manager.py +17 -9
  54. griptape_nodes/retained_mode/managers/workflow_manager.py +186 -192
  55. griptape_nodes/retained_mode/retained_mode.py +19 -0
  56. griptape_nodes/servers/__init__.py +1 -0
  57. griptape_nodes/{mcp_server/server.py → servers/mcp.py} +1 -1
  58. griptape_nodes/{app/api.py → servers/static.py} +43 -40
  59. griptape_nodes/traits/add_param_button.py +1 -1
  60. griptape_nodes/traits/button.py +334 -6
  61. griptape_nodes/traits/color_picker.py +66 -0
  62. griptape_nodes/traits/multi_options.py +188 -0
  63. griptape_nodes/traits/numbers_selector.py +77 -0
  64. griptape_nodes/traits/options.py +93 -2
  65. griptape_nodes/traits/traits.json +4 -0
  66. griptape_nodes/utils/async_utils.py +31 -0
  67. {griptape_nodes-0.52.1.dist-info → griptape_nodes-0.54.0.dist-info}/METADATA +4 -1
  68. {griptape_nodes-0.52.1.dist-info → griptape_nodes-0.54.0.dist-info}/RECORD +71 -48
  69. {griptape_nodes-0.52.1.dist-info → griptape_nodes-0.54.0.dist-info}/WHEEL +1 -1
  70. /griptape_nodes/{mcp_server → servers}/ws_request_manager.py +0 -0
  71. {griptape_nodes-0.52.1.dist-info → griptape_nodes-0.54.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,6 @@
1
+ """Main entry point for the Griptape Nodes CLI when run as a module."""
2
+
3
+ from griptape_nodes import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
griptape_nodes/app/app.py CHANGED
@@ -7,7 +7,6 @@ import os
7
7
  import sys
8
8
  import threading
9
9
  from dataclasses import dataclass
10
- from pathlib import Path
11
10
  from typing import Any
12
11
  from urllib.parse import urljoin
13
12
 
@@ -16,10 +15,8 @@ from rich.console import Console
16
15
  from rich.logging import RichHandler
17
16
  from rich.panel import Panel
18
17
  from websockets.asyncio.client import connect
19
- from websockets.exceptions import ConnectionClosed, WebSocketException
18
+ from websockets.exceptions import ConnectionClosed, ConnectionClosedError, WebSocketException
20
19
 
21
- from griptape_nodes.app.api import start_static_server
22
- from griptape_nodes.mcp_server.server import start_mcp_server
23
20
  from griptape_nodes.retained_mode.events import app_events, execution_events
24
21
 
25
22
  # This import is necessary to register all events, even if not technically used
@@ -63,6 +60,11 @@ class UnsubscribeCommand:
63
60
  topic: str
64
61
 
65
62
 
63
+ # Important to bootstrap singleton here so that we don't
64
+ # get any weird circular import issues from the EventLogHandler
65
+ # initializing it from a log during it's own initialization.
66
+ griptape_nodes: GriptapeNodes = GriptapeNodes()
67
+
66
68
  # WebSocket outgoing queue for messages and commands.
67
69
  # Appears to be fine to create outside event loop
68
70
  # https://discuss.python.org/t/can-asyncio-queue-be-safely-created-outside-of-the-event-loop-thread/49215/8
@@ -75,19 +77,10 @@ websocket_event_loop: asyncio.AbstractEventLoop | None = None
75
77
  websocket_event_loop_ready = threading.Event()
76
78
 
77
79
 
78
- # Whether to enable the static server
79
- STATIC_SERVER_ENABLED = os.getenv("STATIC_SERVER_ENABLED", "true").lower() == "true"
80
-
81
80
  # Semaphore to limit concurrent requests
82
81
  REQUEST_SEMAPHORE = asyncio.Semaphore(100)
83
82
 
84
83
 
85
- # Important to bootstrap singleton here so that we don't
86
- # get any weird circular import issues from the EventLogHandler
87
- # initializing it from a log during it's own initialization.
88
- griptape_nodes: GriptapeNodes = GriptapeNodes()
89
-
90
-
91
84
  class EventLogHandler(logging.Handler):
92
85
  """Custom logging handler that emits log messages as AppEvents.
93
86
 
@@ -105,10 +98,12 @@ class EventLogHandler(logging.Handler):
105
98
  logger = logging.getLogger("griptape_nodes_app")
106
99
 
107
100
  griptape_nodes_logger = logging.getLogger("griptape_nodes")
108
- # When running as an app, we want to forward all log messages to the event queue so they can be sent to the GUI
109
- griptape_nodes_logger.addHandler(EventLogHandler())
110
- griptape_nodes_logger.addHandler(RichHandler(show_time=True, show_path=False, markup=True, rich_tracebacks=True))
111
- griptape_nodes_logger.setLevel(logging.INFO)
101
+ logging.basicConfig(
102
+ level=logging.INFO,
103
+ format="%(message)s",
104
+ datefmt="[%X]",
105
+ handlers=[EventLogHandler(), RichHandler(show_time=True, show_path=False, markup=True, rich_tracebacks=True)],
106
+ )
112
107
 
113
108
  console = Console()
114
109
 
@@ -134,14 +129,6 @@ async def astart_app() -> None:
134
129
  main_loop = asyncio.get_running_loop()
135
130
 
136
131
  try:
137
- # Start MCP server in daemon thread
138
- threading.Thread(target=start_mcp_server, args=(api_key,), daemon=True, name="mcp-server").start()
139
-
140
- # Start static server in daemon thread if enabled
141
- if STATIC_SERVER_ENABLED:
142
- static_dir = _build_static_dir()
143
- threading.Thread(target=start_static_server, args=(static_dir,), daemon=True, name="static-server").start()
144
-
145
132
  # Start WebSocket tasks in daemon thread
146
133
  threading.Thread(
147
134
  target=_start_websocket_connection, args=(api_key, main_loop), daemon=True, name="websocket-tasks"
@@ -186,12 +173,11 @@ async def _run_websocket_tasks(api_key: str, main_loop: asyncio.AbstractEventLoo
186
173
  initialized = False
187
174
 
188
175
  async for ws_connection in connection_stream:
176
+ logger.debug("WebSocket connection established")
189
177
  try:
190
178
  # Emit initialization event only for the first connection
191
179
  if not initialized:
192
- griptape_nodes.EventManager().put_event_threadsafe(
193
- main_loop, AppEvent(payload=app_events.AppInitializationComplete())
194
- )
180
+ griptape_nodes.EventManager().put_event(AppEvent(payload=app_events.AppInitializationComplete()))
195
181
  initialized = True
196
182
 
197
183
  # Emit connection established event for every connection
@@ -202,9 +188,13 @@ async def _run_websocket_tasks(api_key: str, main_loop: asyncio.AbstractEventLoo
202
188
  async with asyncio.TaskGroup() as tg:
203
189
  tg.create_task(_process_incoming_messages(ws_connection, main_loop))
204
190
  tg.create_task(_send_outgoing_messages(ws_connection))
205
- except* Exception as e:
206
- logger.error("WebSocket tasks failed: %s", e.exceptions)
191
+ except (ExceptionGroup, ConnectionClosed, ConnectionClosedError):
192
+ logger.info("WebSocket connection closed, reconnecting...")
193
+ continue
194
+ except Exception:
195
+ logger.exception("WebSocket tasks failed")
207
196
  await asyncio.sleep(2.0) # Wait before retry
197
+ continue
208
198
 
209
199
 
210
200
  def _ensure_api_key() -> str:
@@ -227,36 +217,16 @@ def _ensure_api_key() -> str:
227
217
  return api_key
228
218
 
229
219
 
230
- def _build_static_dir() -> Path:
231
- """Build the static directory path based on the workspace configuration."""
232
- config_manager = griptape_nodes.ConfigManager()
233
- return Path(config_manager.workspace_path) / config_manager.merged_config["static_files_directory"]
234
-
235
-
236
220
  async def _process_incoming_messages(ws_connection: Any, main_loop: asyncio.AbstractEventLoop) -> None:
237
221
  """Process incoming WebSocket requests from Nodes API."""
238
- logger.info("Processing incoming WebSocket requests from WebSocket connection")
222
+ logger.debug("Processing incoming WebSocket requests from WebSocket connection")
239
223
 
240
- try:
241
- async for message in ws_connection:
242
- try:
243
- data = json.loads(message)
244
- await _process_api_event(data, main_loop)
245
- except Exception:
246
- logger.exception("Error processing event, skipping.")
247
-
248
- except ConnectionClosed:
249
- logger.info("WebSocket connection closed, will retry")
250
- except asyncio.CancelledError:
251
- # Clean shutdown when task is cancelled
252
- logger.info("WebSocket listener shutdown complete")
253
- raise
254
- except Exception as e:
255
- logger.error("Error in WebSocket connection. Retrying in 2 seconds... %s", e)
256
- await asyncio.sleep(2.0)
257
- raise
258
- finally:
259
- logger.info("WebSocket listener shutdown complete")
224
+ async for message in ws_connection:
225
+ try:
226
+ data = json.loads(message)
227
+ await _process_api_event(data, main_loop)
228
+ except Exception:
229
+ logger.exception("Error processing event, skipping.")
260
230
 
261
231
 
262
232
  def _create_websocket_connection(api_key: str) -> Any:
@@ -313,33 +283,25 @@ async def _process_api_event(event: dict, main_loop: asyncio.AbstractEventLoop)
313
283
 
314
284
  async def _send_outgoing_messages(ws_connection: Any) -> None:
315
285
  """Send outgoing WebSocket requests from queue on background thread."""
316
- logger.info("Starting outgoing WebSocket request sender")
286
+ logger.debug("Starting outgoing WebSocket request sender")
317
287
 
318
- try:
319
- while True:
320
- # Get message from outgoing queue
321
- message = await ws_outgoing_queue.get()
322
-
323
- try:
324
- if isinstance(message, WebSocketMessage):
325
- await _send_websocket_message(ws_connection, message.event_type, message.payload, message.topic)
326
- elif isinstance(message, SubscribeCommand):
327
- await _send_subscribe_command(ws_connection, message.topic)
328
- elif isinstance(message, UnsubscribeCommand):
329
- await _send_unsubscribe_command(ws_connection, message.topic)
330
- else:
331
- logger.warning("Unknown outgoing message type: %s", type(message))
332
- except Exception as e:
333
- logger.error("Error sending outgoing WebSocket request: %s", e)
334
- finally:
335
- ws_outgoing_queue.task_done()
288
+ while True:
289
+ # Get message from outgoing queue
290
+ message = await ws_outgoing_queue.get()
336
291
 
337
- except asyncio.CancelledError:
338
- logger.info("Outbound request sender shutdown complete")
339
- raise
340
- except Exception as e:
341
- logger.error("Fatal error in outgoing request sender: %s", e)
342
- raise
292
+ try:
293
+ if isinstance(message, WebSocketMessage):
294
+ await _send_websocket_message(ws_connection, message.event_type, message.payload, message.topic)
295
+ elif isinstance(message, SubscribeCommand):
296
+ await _send_subscribe_command(ws_connection, message.topic)
297
+ elif isinstance(message, UnsubscribeCommand):
298
+ await _send_unsubscribe_command(ws_connection, message.topic)
299
+ else:
300
+ logger.warning("Unknown outgoing message type: %s", type(message))
301
+ except Exception as e:
302
+ logger.error("Error sending outgoing WebSocket request: %s", e)
303
+ finally:
304
+ ws_outgoing_queue.task_done()
343
305
 
344
306
 
345
307
  async def _send_websocket_message(ws_connection: Any, event_type: str, payload: str, topic: str | None) -> None:
@@ -361,7 +323,7 @@ async def _send_subscribe_command(ws_connection: Any, topic: str) -> None:
361
323
  try:
362
324
  body = {"type": "subscribe", "topic": topic, "payload": {}}
363
325
  await ws_connection.send(json.dumps(body))
364
- logger.info("Subscribed to topic: %s", topic)
326
+ logger.debug("Subscribed to topic: %s", topic)
365
327
  except WebSocketException as e:
366
328
  logger.error("Error subscribing to topic %s: %s", topic, e)
367
329
  except Exception as e:
@@ -373,7 +335,7 @@ async def _send_unsubscribe_command(ws_connection: Any, topic: str) -> None:
373
335
  try:
374
336
  body = {"type": "unsubscribe", "topic": topic, "payload": {}}
375
337
  await ws_connection.send(json.dumps(body))
376
- logger.info("Unsubscribed from topic: %s", topic)
338
+ logger.debug("Unsubscribed from topic: %s", topic)
377
339
  except WebSocketException as e:
378
340
  logger.error("Error unsubscribing from topic %s: %s", topic, e)
379
341
  except Exception as e:
@@ -382,7 +344,7 @@ async def _send_unsubscribe_command(ws_connection: Any, topic: str) -> None:
382
344
 
383
345
  async def _process_event_queue() -> None:
384
346
  """Process events concurrently - runs on main thread."""
385
- logger.info("Starting event queue processor on main thread")
347
+ logger.debug("Starting event queue processor on main thread")
386
348
  background_tasks = set()
387
349
 
388
350
  def _handle_task_result(task: asyncio.Task) -> None:
@@ -415,7 +377,7 @@ async def _process_event_queue() -> None:
415
377
  task.add_done_callback(_handle_task_result)
416
378
  event_queue.task_done()
417
379
  except asyncio.CancelledError:
418
- logger.info("Event queue processor shutdown complete")
380
+ logger.debug("Event queue processor shutdown complete")
419
381
  raise
420
382
 
421
383
 
@@ -1,9 +1,12 @@
1
+ from __future__ import annotations
2
+
1
3
  import logging
2
- from typing import Any
4
+ from typing import TYPE_CHECKING, Any, Self
3
5
 
4
6
  from griptape_nodes.bootstrap.workflow_executors.workflow_executor import WorkflowExecutor
5
7
  from griptape_nodes.drivers.storage import StorageBackend
6
8
  from griptape_nodes.exe_types.node_types import EndNode, StartNode
9
+ from griptape_nodes.retained_mode.events.app_events import AppInitializationComplete
7
10
  from griptape_nodes.retained_mode.events.base_events import (
8
11
  EventRequest,
9
12
  ExecutionGriptapeNodeEvent,
@@ -15,6 +18,9 @@ from griptape_nodes.retained_mode.events.workflow_events import (
15
18
  )
16
19
  from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
17
20
 
21
+ if TYPE_CHECKING:
22
+ from types import TracebackType
23
+
18
24
  logger = logging.getLogger(__name__)
19
25
 
20
26
 
@@ -23,6 +29,29 @@ class LocalExecutorError(Exception):
23
29
 
24
30
 
25
31
  class LocalWorkflowExecutor(WorkflowExecutor):
32
+ def __init__(
33
+ self,
34
+ storage_backend: StorageBackend = StorageBackend.LOCAL,
35
+ ):
36
+ super().__init__()
37
+ self._set_storage_backend(storage_backend=storage_backend)
38
+
39
+ async def __aenter__(self) -> Self:
40
+ """Async context manager entry: initialize queue and broadcast app initialization."""
41
+ GriptapeNodes.EventManager().initialize_queue()
42
+ await GriptapeNodes.EventManager().broadcast_app_event(AppInitializationComplete())
43
+ return self
44
+
45
+ async def __aexit__(
46
+ self,
47
+ exc_type: type[BaseException] | None,
48
+ exc_val: BaseException | None,
49
+ exc_tb: TracebackType | None,
50
+ ) -> None:
51
+ """Async context manager exit."""
52
+ # TODO: Broadcast shutdown https://github.com/griptape-ai/griptape-nodes/issues/2149
53
+ return
54
+
26
55
  def _load_flow_for_workflow(self) -> str:
27
56
  try:
28
57
  context_manager = GriptapeNodes.ContextManager()
@@ -124,7 +153,7 @@ class LocalWorkflowExecutor(WorkflowExecutor):
124
153
  self,
125
154
  workflow_name: str,
126
155
  flow_input: Any,
127
- storage_backend: StorageBackend = StorageBackend.LOCAL,
156
+ storage_backend: StorageBackend | None = None,
128
157
  **kwargs: Any,
129
158
  ) -> None:
130
159
  """Executes a local workflow.
@@ -140,12 +169,13 @@ class LocalWorkflowExecutor(WorkflowExecutor):
140
169
  Returns:
141
170
  None
142
171
  """
172
+ if storage_backend is not None:
173
+ msg = "The storage_backend parameter is deprecated. Pass `storage_backend` to the constructor instead."
174
+ raise ValueError(msg)
175
+
143
176
  logger.info("Executing workflow: %s", workflow_name)
144
177
  GriptapeNodes.EventManager().initialize_queue()
145
178
 
146
- # Set the storage backend
147
- self._set_storage_backend(storage_backend=storage_backend)
148
-
149
179
  # Load workflow from file if workflow_path is provided
150
180
  workflow_path = kwargs.get("workflow_path")
151
181
  if workflow_path:
@@ -1,7 +1,8 @@
1
1
  import asyncio
2
2
  import logging
3
3
  from abc import abstractmethod
4
- from typing import Any
4
+ from types import TracebackType
5
+ from typing import Any, Self
5
6
 
6
7
  from griptape_nodes.drivers.storage import StorageBackend
7
8
 
@@ -12,6 +13,19 @@ class WorkflowExecutor:
12
13
  def __init__(self) -> None:
13
14
  self.output: dict | None = None
14
15
 
16
+ async def __aenter__(self) -> Self:
17
+ """Async context manager entry."""
18
+ return self
19
+
20
+ async def __aexit__(
21
+ self,
22
+ exc_type: type[BaseException] | None,
23
+ exc_val: BaseException | None,
24
+ exc_tb: TracebackType | None,
25
+ ) -> None:
26
+ """Async context manager exit."""
27
+ return
28
+
15
29
  def run(
16
30
  self,
17
31
  workflow_name: str,
@@ -0,0 +1 @@
1
+ """Griptape Nodes CLI module."""
@@ -0,0 +1 @@
1
+ """Griptape Nodes CLI commands."""
@@ -0,0 +1,74 @@
1
+ """Config command for Griptape Nodes CLI."""
2
+
3
+ import json
4
+ import sys
5
+
6
+ import typer
7
+
8
+ from griptape_nodes.cli.shared import console
9
+ from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
10
+
11
+ config_manager = GriptapeNodes.ConfigManager()
12
+
13
+ app = typer.Typer(help="Manage configuration.")
14
+
15
+
16
+ @app.command()
17
+ def show(
18
+ config_path: str = typer.Argument(
19
+ None,
20
+ help="Optional config path to show specific value (e.g., 'workspace_directory').",
21
+ ),
22
+ ) -> None:
23
+ """Show configuration values."""
24
+ _print_user_config(config_path)
25
+
26
+
27
+ @app.command("list")
28
+ def list_configs() -> None:
29
+ """List configuration values."""
30
+ _list_user_configs()
31
+
32
+
33
+ @app.command()
34
+ def reset() -> None:
35
+ """Reset configuration to defaults."""
36
+ _reset_user_config()
37
+
38
+
39
+ def _print_user_config(config_path: str | None = None) -> None:
40
+ """Prints the user configuration from the config file.
41
+
42
+ Args:
43
+ config_path: Optional path to specific config value. If None, prints entire config.
44
+ """
45
+ if config_path is None:
46
+ config = config_manager.merged_config
47
+ sys.stdout.write(json.dumps(config, indent=2))
48
+ else:
49
+ try:
50
+ value = config_manager.get_config_value(config_path)
51
+ if isinstance(value, (dict, list)):
52
+ sys.stdout.write(json.dumps(value, indent=2))
53
+ else:
54
+ sys.stdout.write(str(value))
55
+ except (KeyError, AttributeError, ValueError):
56
+ console.print(f"[bold red]Config path '{config_path}' not found[/bold red]")
57
+ sys.exit(1)
58
+
59
+
60
+ def _list_user_configs() -> None:
61
+ """Lists user configuration files in ascending precedence."""
62
+ num_config_files = len(config_manager.config_files)
63
+ console.print(
64
+ f"[bold]User Configuration Files (lowest precedence (1.) ⟶ highest precedence ({num_config_files}.)):[/bold]"
65
+ )
66
+ for idx, config in enumerate(config_manager.config_files):
67
+ console.print(f"[green]{idx + 1}. {config}[/green]")
68
+
69
+
70
+ def _reset_user_config() -> None:
71
+ """Resets the user configuration to the default values."""
72
+ console.print("[bold]Resetting user configuration to default values...[/bold]")
73
+ config_manager.reset_user_config()
74
+ console.print("[bold green]User configuration reset complete![/bold green]")
@@ -0,0 +1,80 @@
1
+ """Engine command for Griptape Nodes CLI."""
2
+
3
+ import typer
4
+ from rich.prompt import Confirm
5
+
6
+ from griptape_nodes.app import start_app
7
+ from griptape_nodes.cli.commands.init import _run_init
8
+ from griptape_nodes.cli.commands.self import _get_latest_version, _update_self
9
+ from griptape_nodes.cli.shared import (
10
+ CONFIG_DIR,
11
+ ENV_API_KEY,
12
+ ENV_GTN_BUCKET_NAME,
13
+ ENV_LIBRARIES_SYNC,
14
+ ENV_REGISTER_ADVANCED_LIBRARY,
15
+ ENV_STORAGE_BACKEND,
16
+ ENV_WORKSPACE_DIRECTORY,
17
+ PACKAGE_NAME,
18
+ InitConfig,
19
+ console,
20
+ )
21
+ from griptape_nodes.utils.version_utils import get_current_version, get_install_source
22
+
23
+
24
+ def engine_command(
25
+ no_update: bool = typer.Option(False, "--no-update", help="Skip the auto-update check."), # noqa: FBT001
26
+ ) -> None:
27
+ """Run the Griptape Nodes engine."""
28
+ _start_engine(no_update=no_update)
29
+
30
+
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
+ """
37
+ if not CONFIG_DIR.exists():
38
+ # Default init flow if there is no config directory
39
+ console.print("[bold green]Config directory not found. Initializing...[/bold green]")
40
+ _run_init(
41
+ InitConfig(
42
+ workspace_directory=ENV_WORKSPACE_DIRECTORY,
43
+ api_key=ENV_API_KEY,
44
+ storage_backend=ENV_STORAGE_BACKEND,
45
+ register_advanced_library=ENV_REGISTER_ADVANCED_LIBRARY,
46
+ interactive=True,
47
+ config_values=None,
48
+ secret_values=None,
49
+ libraries_sync=ENV_LIBRARIES_SYNC,
50
+ bucket_name=ENV_GTN_BUCKET_NAME,
51
+ )
52
+ )
53
+
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
+ console.print("[bold green]Starting Griptape Nodes engine...[/bold green]")
59
+ start_app()
60
+
61
+
62
+ def _auto_update_self() -> None:
63
+ """Automatically updates the script to the latest version if the user confirms."""
64
+ console.print("[bold green]Checking for updates...[/bold green]")
65
+ source, commit_id = get_install_source()
66
+ current_version = get_current_version()
67
+ latest_version = _get_latest_version(PACKAGE_NAME, source)
68
+
69
+ if source == "git" and commit_id is not None:
70
+ can_update = commit_id != latest_version
71
+ update_message = f"Your current engine version, {current_version} ({source} - {commit_id}), doesn't match the latest release, {latest_version}. Update now?"
72
+ else:
73
+ can_update = current_version < latest_version
74
+ update_message = f"Your current engine version, {current_version}, is behind the latest release, {latest_version}. Update now?"
75
+
76
+ if can_update:
77
+ update = Confirm.ask(update_message, default=True)
78
+
79
+ if update:
80
+ _update_self()