griptape-nodes 0.52.1__py3-none-any.whl → 0.53.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 (46) hide show
  1. griptape_nodes/__init__.py +6 -943
  2. griptape_nodes/__main__.py +6 -0
  3. griptape_nodes/app/app.py +45 -61
  4. griptape_nodes/cli/__init__.py +1 -0
  5. griptape_nodes/cli/commands/__init__.py +1 -0
  6. griptape_nodes/cli/commands/config.py +71 -0
  7. griptape_nodes/cli/commands/engine.py +80 -0
  8. griptape_nodes/cli/commands/init.py +548 -0
  9. griptape_nodes/cli/commands/libraries.py +90 -0
  10. griptape_nodes/cli/commands/self.py +117 -0
  11. griptape_nodes/cli/main.py +46 -0
  12. griptape_nodes/cli/shared.py +84 -0
  13. griptape_nodes/common/__init__.py +1 -0
  14. griptape_nodes/common/directed_graph.py +55 -0
  15. griptape_nodes/drivers/storage/local_storage_driver.py +7 -2
  16. griptape_nodes/exe_types/core_types.py +60 -2
  17. griptape_nodes/exe_types/node_types.py +38 -24
  18. griptape_nodes/machines/control_flow.py +86 -22
  19. griptape_nodes/machines/fsm.py +10 -1
  20. griptape_nodes/machines/parallel_resolution.py +570 -0
  21. griptape_nodes/machines/{node_resolution.py → sequential_resolution.py} +22 -51
  22. griptape_nodes/retained_mode/events/base_events.py +2 -2
  23. griptape_nodes/retained_mode/events/node_events.py +4 -3
  24. griptape_nodes/retained_mode/griptape_nodes.py +25 -12
  25. griptape_nodes/retained_mode/managers/agent_manager.py +9 -5
  26. griptape_nodes/retained_mode/managers/arbitrary_code_exec_manager.py +3 -1
  27. griptape_nodes/retained_mode/managers/context_manager.py +6 -5
  28. griptape_nodes/retained_mode/managers/flow_manager.py +117 -204
  29. griptape_nodes/retained_mode/managers/library_lifecycle/library_directory.py +1 -1
  30. griptape_nodes/retained_mode/managers/library_manager.py +35 -25
  31. griptape_nodes/retained_mode/managers/node_manager.py +81 -199
  32. griptape_nodes/retained_mode/managers/object_manager.py +11 -5
  33. griptape_nodes/retained_mode/managers/os_manager.py +24 -9
  34. griptape_nodes/retained_mode/managers/secrets_manager.py +8 -4
  35. griptape_nodes/retained_mode/managers/settings.py +32 -1
  36. griptape_nodes/retained_mode/managers/static_files_manager.py +8 -3
  37. griptape_nodes/retained_mode/managers/sync_manager.py +8 -5
  38. griptape_nodes/retained_mode/managers/workflow_manager.py +110 -122
  39. griptape_nodes/traits/add_param_button.py +1 -1
  40. griptape_nodes/traits/button.py +216 -6
  41. griptape_nodes/traits/color_picker.py +66 -0
  42. griptape_nodes/traits/traits.json +4 -0
  43. {griptape_nodes-0.52.1.dist-info → griptape_nodes-0.53.0.dist-info}/METADATA +2 -1
  44. {griptape_nodes-0.52.1.dist-info → griptape_nodes-0.53.0.dist-info}/RECORD +46 -32
  45. {griptape_nodes-0.52.1.dist-info → griptape_nodes-0.53.0.dist-info}/WHEEL +0 -0
  46. {griptape_nodes-0.52.1.dist-info → griptape_nodes-0.53.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
@@ -16,7 +16,7 @@ from rich.console import Console
16
16
  from rich.logging import RichHandler
17
17
  from rich.panel import Panel
18
18
  from websockets.asyncio.client import connect
19
- from websockets.exceptions import ConnectionClosed, WebSocketException
19
+ from websockets.exceptions import ConnectionClosed, ConnectionClosedError, WebSocketException
20
20
 
21
21
  from griptape_nodes.app.api import start_static_server
22
22
  from griptape_nodes.mcp_server.server import start_mcp_server
@@ -63,6 +63,11 @@ class UnsubscribeCommand:
63
63
  topic: str
64
64
 
65
65
 
66
+ # Important to bootstrap singleton here so that we don't
67
+ # get any weird circular import issues from the EventLogHandler
68
+ # initializing it from a log during it's own initialization.
69
+ griptape_nodes: GriptapeNodes = GriptapeNodes()
70
+
66
71
  # WebSocket outgoing queue for messages and commands.
67
72
  # Appears to be fine to create outside event loop
68
73
  # https://discuss.python.org/t/can-asyncio-queue-be-safely-created-outside-of-the-event-loop-thread/49215/8
@@ -82,12 +87,6 @@ STATIC_SERVER_ENABLED = os.getenv("STATIC_SERVER_ENABLED", "true").lower() == "t
82
87
  REQUEST_SEMAPHORE = asyncio.Semaphore(100)
83
88
 
84
89
 
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
90
  class EventLogHandler(logging.Handler):
92
91
  """Custom logging handler that emits log messages as AppEvents.
93
92
 
@@ -105,10 +104,12 @@ class EventLogHandler(logging.Handler):
105
104
  logger = logging.getLogger("griptape_nodes_app")
106
105
 
107
106
  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)
107
+ logging.basicConfig(
108
+ level=logging.INFO,
109
+ format="%(message)s",
110
+ datefmt="[%X]",
111
+ handlers=[EventLogHandler(), RichHandler(show_time=True, show_path=False, markup=True, rich_tracebacks=True)],
112
+ )
112
113
 
113
114
  console = Console()
114
115
 
@@ -186,6 +187,7 @@ async def _run_websocket_tasks(api_key: str, main_loop: asyncio.AbstractEventLoo
186
187
  initialized = False
187
188
 
188
189
  async for ws_connection in connection_stream:
190
+ logger.info("WebSocket connection established")
189
191
  try:
190
192
  # Emit initialization event only for the first connection
191
193
  if not initialized:
@@ -202,9 +204,13 @@ async def _run_websocket_tasks(api_key: str, main_loop: asyncio.AbstractEventLoo
202
204
  async with asyncio.TaskGroup() as tg:
203
205
  tg.create_task(_process_incoming_messages(ws_connection, main_loop))
204
206
  tg.create_task(_send_outgoing_messages(ws_connection))
205
- except* Exception as e:
206
- logger.error("WebSocket tasks failed: %s", e.exceptions)
207
+ except (ExceptionGroup, ConnectionClosed, ConnectionClosedError):
208
+ logger.info("WebSocket connection closed, reconnecting...")
209
+ continue
210
+ except Exception:
211
+ logger.exception("WebSocket tasks failed")
207
212
  await asyncio.sleep(2.0) # Wait before retry
213
+ continue
208
214
 
209
215
 
210
216
  def _ensure_api_key() -> str:
@@ -235,28 +241,14 @@ def _build_static_dir() -> Path:
235
241
 
236
242
  async def _process_incoming_messages(ws_connection: Any, main_loop: asyncio.AbstractEventLoop) -> None:
237
243
  """Process incoming WebSocket requests from Nodes API."""
238
- logger.info("Processing incoming WebSocket requests from WebSocket connection")
244
+ logger.debug("Processing incoming WebSocket requests from WebSocket connection")
239
245
 
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")
246
+ async for message in ws_connection:
247
+ try:
248
+ data = json.loads(message)
249
+ await _process_api_event(data, main_loop)
250
+ except Exception:
251
+ logger.exception("Error processing event, skipping.")
260
252
 
261
253
 
262
254
  def _create_websocket_connection(api_key: str) -> Any:
@@ -313,33 +305,25 @@ async def _process_api_event(event: dict, main_loop: asyncio.AbstractEventLoop)
313
305
 
314
306
  async def _send_outgoing_messages(ws_connection: Any) -> None:
315
307
  """Send outgoing WebSocket requests from queue on background thread."""
316
- logger.info("Starting outgoing WebSocket request sender")
308
+ logger.debug("Starting outgoing WebSocket request sender")
317
309
 
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()
310
+ while True:
311
+ # Get message from outgoing queue
312
+ message = await ws_outgoing_queue.get()
336
313
 
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
314
+ try:
315
+ if isinstance(message, WebSocketMessage):
316
+ await _send_websocket_message(ws_connection, message.event_type, message.payload, message.topic)
317
+ elif isinstance(message, SubscribeCommand):
318
+ await _send_subscribe_command(ws_connection, message.topic)
319
+ elif isinstance(message, UnsubscribeCommand):
320
+ await _send_unsubscribe_command(ws_connection, message.topic)
321
+ else:
322
+ logger.warning("Unknown outgoing message type: %s", type(message))
323
+ except Exception as e:
324
+ logger.error("Error sending outgoing WebSocket request: %s", e)
325
+ finally:
326
+ ws_outgoing_queue.task_done()
343
327
 
344
328
 
345
329
  async def _send_websocket_message(ws_connection: Any, event_type: str, payload: str, topic: str | None) -> None:
@@ -361,7 +345,7 @@ async def _send_subscribe_command(ws_connection: Any, topic: str) -> None:
361
345
  try:
362
346
  body = {"type": "subscribe", "topic": topic, "payload": {}}
363
347
  await ws_connection.send(json.dumps(body))
364
- logger.info("Subscribed to topic: %s", topic)
348
+ logger.debug("Subscribed to topic: %s", topic)
365
349
  except WebSocketException as e:
366
350
  logger.error("Error subscribing to topic %s: %s", topic, e)
367
351
  except Exception as e:
@@ -373,7 +357,7 @@ async def _send_unsubscribe_command(ws_connection: Any, topic: str) -> None:
373
357
  try:
374
358
  body = {"type": "unsubscribe", "topic": topic, "payload": {}}
375
359
  await ws_connection.send(json.dumps(body))
376
- logger.info("Unsubscribed from topic: %s", topic)
360
+ logger.debug("Unsubscribed from topic: %s", topic)
377
361
  except WebSocketException as e:
378
362
  logger.error("Error unsubscribing from topic %s: %s", topic, e)
379
363
  except Exception as e:
@@ -0,0 +1 @@
1
+ """Griptape Nodes CLI module."""
@@ -0,0 +1 @@
1
+ """Griptape Nodes CLI commands."""
@@ -0,0 +1,71 @@
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 config_manager, console
9
+
10
+ app = typer.Typer(help="Manage configuration.")
11
+
12
+
13
+ @app.command()
14
+ def show(
15
+ config_path: str = typer.Argument(
16
+ None,
17
+ help="Optional config path to show specific value (e.g., 'workspace_directory').",
18
+ ),
19
+ ) -> None:
20
+ """Show configuration values."""
21
+ _print_user_config(config_path)
22
+
23
+
24
+ @app.command("list")
25
+ def list_configs() -> None:
26
+ """List configuration values."""
27
+ _list_user_configs()
28
+
29
+
30
+ @app.command()
31
+ def reset() -> None:
32
+ """Reset configuration to defaults."""
33
+ _reset_user_config()
34
+
35
+
36
+ def _print_user_config(config_path: str | None = None) -> None:
37
+ """Prints the user configuration from the config file.
38
+
39
+ Args:
40
+ config_path: Optional path to specific config value. If None, prints entire config.
41
+ """
42
+ if config_path is None:
43
+ config = config_manager.merged_config
44
+ sys.stdout.write(json.dumps(config, indent=2))
45
+ else:
46
+ try:
47
+ value = config_manager.get_config_value(config_path)
48
+ if isinstance(value, (dict, list)):
49
+ sys.stdout.write(json.dumps(value, indent=2))
50
+ else:
51
+ sys.stdout.write(str(value))
52
+ except (KeyError, AttributeError, ValueError):
53
+ console.print(f"[bold red]Config path '{config_path}' not found[/bold red]")
54
+ sys.exit(1)
55
+
56
+
57
+ def _list_user_configs() -> None:
58
+ """Lists user configuration files in ascending precedence."""
59
+ num_config_files = len(config_manager.config_files)
60
+ console.print(
61
+ f"[bold]User Configuration Files (lowest precedence (1.) ⟶ highest precedence ({num_config_files}.)):[/bold]"
62
+ )
63
+ for idx, config in enumerate(config_manager.config_files):
64
+ console.print(f"[green]{idx + 1}. {config}[/green]")
65
+
66
+
67
+ def _reset_user_config() -> None:
68
+ """Resets the user configuration to the default values."""
69
+ console.print("[bold]Resetting user configuration to default values...[/bold]")
70
+ config_manager.reset_user_config()
71
+ 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()