griptape-nodes 0.42.0__py3-none-any.whl → 0.43.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.
Files changed (132) hide show
  1. griptape_nodes/__init__.py +0 -0
  2. griptape_nodes/app/.python-version +0 -0
  3. griptape_nodes/app/__init__.py +1 -6
  4. griptape_nodes/app/api.py +199 -0
  5. griptape_nodes/app/app.py +140 -225
  6. griptape_nodes/app/watch.py +1 -1
  7. griptape_nodes/bootstrap/__init__.py +0 -0
  8. griptape_nodes/bootstrap/bootstrap_script.py +0 -0
  9. griptape_nodes/bootstrap/register_libraries_script.py +0 -0
  10. griptape_nodes/bootstrap/structure_config.yaml +0 -0
  11. griptape_nodes/bootstrap/workflow_executors/__init__.py +0 -0
  12. griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +0 -0
  13. griptape_nodes/bootstrap/workflow_executors/workflow_executor.py +0 -0
  14. griptape_nodes/bootstrap/workflow_runners/__init__.py +0 -0
  15. griptape_nodes/bootstrap/workflow_runners/bootstrap_workflow_runner.py +0 -0
  16. griptape_nodes/bootstrap/workflow_runners/local_workflow_runner.py +0 -0
  17. griptape_nodes/bootstrap/workflow_runners/subprocess_workflow_runner.py +6 -2
  18. griptape_nodes/bootstrap/workflow_runners/workflow_runner.py +0 -0
  19. griptape_nodes/drivers/__init__.py +0 -0
  20. griptape_nodes/drivers/storage/__init__.py +0 -0
  21. griptape_nodes/drivers/storage/base_storage_driver.py +0 -0
  22. griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +0 -0
  23. griptape_nodes/drivers/storage/local_storage_driver.py +2 -1
  24. griptape_nodes/drivers/storage/storage_backend.py +0 -0
  25. griptape_nodes/exe_types/__init__.py +0 -0
  26. griptape_nodes/exe_types/connections.py +0 -0
  27. griptape_nodes/exe_types/core_types.py +0 -0
  28. griptape_nodes/exe_types/flow.py +0 -0
  29. griptape_nodes/exe_types/node_types.py +17 -1
  30. griptape_nodes/exe_types/type_validator.py +0 -0
  31. griptape_nodes/machines/__init__.py +0 -0
  32. griptape_nodes/machines/control_flow.py +41 -12
  33. griptape_nodes/machines/fsm.py +16 -2
  34. griptape_nodes/machines/node_resolution.py +0 -0
  35. griptape_nodes/mcp_server/__init__.py +1 -0
  36. griptape_nodes/mcp_server/server.py +126 -0
  37. griptape_nodes/mcp_server/ws_request_manager.py +268 -0
  38. griptape_nodes/node_library/__init__.py +0 -0
  39. griptape_nodes/node_library/advanced_node_library.py +0 -0
  40. griptape_nodes/node_library/library_registry.py +0 -0
  41. griptape_nodes/node_library/workflow_registry.py +1 -1
  42. griptape_nodes/py.typed +0 -0
  43. griptape_nodes/retained_mode/__init__.py +0 -0
  44. griptape_nodes/retained_mode/events/__init__.py +0 -0
  45. griptape_nodes/retained_mode/events/agent_events.py +0 -0
  46. griptape_nodes/retained_mode/events/app_events.py +6 -2
  47. griptape_nodes/retained_mode/events/arbitrary_python_events.py +0 -0
  48. griptape_nodes/retained_mode/events/base_events.py +6 -6
  49. griptape_nodes/retained_mode/events/config_events.py +0 -0
  50. griptape_nodes/retained_mode/events/connection_events.py +0 -0
  51. griptape_nodes/retained_mode/events/context_events.py +0 -0
  52. griptape_nodes/retained_mode/events/execution_events.py +0 -0
  53. griptape_nodes/retained_mode/events/flow_events.py +0 -0
  54. griptape_nodes/retained_mode/events/generate_request_payload_schemas.py +0 -0
  55. griptape_nodes/retained_mode/events/library_events.py +2 -2
  56. griptape_nodes/retained_mode/events/logger_events.py +0 -0
  57. griptape_nodes/retained_mode/events/node_events.py +0 -0
  58. griptape_nodes/retained_mode/events/object_events.py +0 -0
  59. griptape_nodes/retained_mode/events/os_events.py +104 -2
  60. griptape_nodes/retained_mode/events/parameter_events.py +0 -0
  61. griptape_nodes/retained_mode/events/payload_registry.py +0 -0
  62. griptape_nodes/retained_mode/events/secrets_events.py +0 -0
  63. griptape_nodes/retained_mode/events/static_file_events.py +0 -0
  64. griptape_nodes/retained_mode/events/validation_events.py +0 -0
  65. griptape_nodes/retained_mode/events/workflow_events.py +0 -0
  66. griptape_nodes/retained_mode/griptape_nodes.py +43 -40
  67. griptape_nodes/retained_mode/managers/__init__.py +0 -0
  68. griptape_nodes/retained_mode/managers/agent_manager.py +48 -22
  69. griptape_nodes/retained_mode/managers/arbitrary_code_exec_manager.py +0 -0
  70. griptape_nodes/retained_mode/managers/config_manager.py +0 -0
  71. griptape_nodes/retained_mode/managers/context_manager.py +0 -0
  72. griptape_nodes/retained_mode/managers/engine_identity_manager.py +0 -0
  73. griptape_nodes/retained_mode/managers/event_manager.py +0 -0
  74. griptape_nodes/retained_mode/managers/flow_manager.py +2 -0
  75. griptape_nodes/retained_mode/managers/library_lifecycle/__init__.py +45 -0
  76. griptape_nodes/retained_mode/managers/library_lifecycle/data_models.py +191 -0
  77. griptape_nodes/retained_mode/managers/library_lifecycle/library_directory.py +346 -0
  78. griptape_nodes/retained_mode/managers/library_lifecycle/library_fsm.py +439 -0
  79. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/__init__.py +17 -0
  80. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/base.py +82 -0
  81. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/github.py +116 -0
  82. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/local_file.py +352 -0
  83. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/package.py +104 -0
  84. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/sandbox.py +155 -0
  85. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance.py +18 -0
  86. griptape_nodes/retained_mode/managers/library_lifecycle/library_status.py +12 -0
  87. griptape_nodes/retained_mode/managers/library_manager.py +144 -39
  88. griptape_nodes/retained_mode/managers/node_manager.py +86 -72
  89. griptape_nodes/retained_mode/managers/object_manager.py +0 -0
  90. griptape_nodes/retained_mode/managers/operation_manager.py +0 -0
  91. griptape_nodes/retained_mode/managers/os_manager.py +517 -12
  92. griptape_nodes/retained_mode/managers/secrets_manager.py +0 -0
  93. griptape_nodes/retained_mode/managers/session_manager.py +0 -0
  94. griptape_nodes/retained_mode/managers/settings.py +0 -0
  95. griptape_nodes/retained_mode/managers/static_files_manager.py +0 -0
  96. griptape_nodes/retained_mode/managers/version_compatibility_manager.py +2 -2
  97. griptape_nodes/retained_mode/managers/workflow_manager.py +199 -2
  98. griptape_nodes/retained_mode/retained_mode.py +0 -0
  99. griptape_nodes/retained_mode/utils/__init__.py +0 -0
  100. griptape_nodes/retained_mode/utils/engine_identity.py +0 -0
  101. griptape_nodes/retained_mode/utils/name_generator.py +0 -0
  102. griptape_nodes/traits/__init__.py +0 -0
  103. griptape_nodes/traits/add_param_button.py +0 -0
  104. griptape_nodes/traits/button.py +0 -0
  105. griptape_nodes/traits/clamp.py +0 -0
  106. griptape_nodes/traits/compare.py +0 -0
  107. griptape_nodes/traits/compare_images.py +0 -0
  108. griptape_nodes/traits/file_system_picker.py +127 -0
  109. griptape_nodes/traits/minmax.py +0 -0
  110. griptape_nodes/traits/options.py +0 -0
  111. griptape_nodes/traits/slider.py +0 -0
  112. griptape_nodes/traits/trait_registry.py +0 -0
  113. griptape_nodes/traits/traits.json +0 -0
  114. griptape_nodes/updater/__init__.py +2 -2
  115. griptape_nodes/updater/__main__.py +0 -0
  116. griptape_nodes/utils/__init__.py +0 -0
  117. griptape_nodes/utils/dict_utils.py +0 -0
  118. griptape_nodes/utils/image_preview.py +128 -0
  119. griptape_nodes/utils/metaclasses.py +0 -0
  120. griptape_nodes/version_compatibility/__init__.py +0 -0
  121. griptape_nodes/version_compatibility/versions/__init__.py +0 -0
  122. griptape_nodes/version_compatibility/versions/v0_39_0/__init__.py +0 -0
  123. griptape_nodes/version_compatibility/versions/v0_39_0/modified_parameters_set_removal.py +5 -5
  124. griptape_nodes-0.43.1.dist-info/METADATA +90 -0
  125. griptape_nodes-0.43.1.dist-info/RECORD +129 -0
  126. griptape_nodes-0.43.1.dist-info/WHEEL +4 -0
  127. {griptape_nodes-0.42.0.dist-info → griptape_nodes-0.43.1.dist-info}/entry_points.txt +1 -0
  128. griptape_nodes/app/app_sessions.py +0 -554
  129. griptape_nodes-0.42.0.dist-info/METADATA +0 -78
  130. griptape_nodes-0.42.0.dist-info/RECORD +0 -113
  131. griptape_nodes-0.42.0.dist-info/WHEEL +0 -4
  132. griptape_nodes-0.42.0.dist-info/licenses/LICENSE +0 -201
griptape_nodes/app/app.py CHANGED
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
- import binascii
5
4
  import json
6
5
  import logging
7
6
  import os
@@ -10,13 +9,9 @@ import sys
10
9
  import threading
11
10
  from pathlib import Path
12
11
  from queue import Queue
13
- from typing import Any, cast
12
+ from typing import Any
14
13
  from urllib.parse import urljoin
15
14
 
16
- import uvicorn
17
- from fastapi import FastAPI, HTTPException, Request
18
- from fastapi.middleware.cors import CORSMiddleware
19
- from fastapi.staticfiles import StaticFiles
20
15
  from griptape.events import (
21
16
  EventBus,
22
17
  EventListener,
@@ -28,6 +23,8 @@ from rich.panel import Panel
28
23
  from websockets.asyncio.client import connect
29
24
  from websockets.exceptions import ConnectionClosed, WebSocketException
30
25
 
26
+ from griptape_nodes.mcp_server.server import main as mcp_server
27
+
31
28
  # This import is necessary to register all events, even if not technically used
32
29
  from griptape_nodes.retained_mode.events import app_events, execution_events
33
30
  from griptape_nodes.retained_mode.events.base_events import (
@@ -39,11 +36,12 @@ from griptape_nodes.retained_mode.events.base_events import (
39
36
  ExecutionGriptapeNodeEvent,
40
37
  GriptapeNodeEvent,
41
38
  ProgressEvent,
42
- deserialize_event,
43
39
  )
44
40
  from griptape_nodes.retained_mode.events.logger_events import LogHandlerEvent
45
41
  from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
46
42
 
43
+ from .api import _process_api_event, start_api
44
+
47
45
  # This is a global event queue that will be used to pass events between threads
48
46
  event_queue = Queue()
49
47
 
@@ -51,16 +49,12 @@ event_queue = Queue()
51
49
  ws_connection_for_sending = None
52
50
  event_loop = None
53
51
 
52
+ # Event to signal when WebSocket connection is ready
53
+ ws_ready_event = threading.Event()
54
+
55
+
54
56
  # Whether to enable the static server
55
57
  STATIC_SERVER_ENABLED = os.getenv("STATIC_SERVER_ENABLED", "true").lower() == "true"
56
- # Host of the static server
57
- STATIC_SERVER_HOST = os.getenv("STATIC_SERVER_HOST", "localhost")
58
- # Port of the static server
59
- STATIC_SERVER_PORT = int(os.getenv("STATIC_SERVER_PORT", "8124"))
60
- # URL path for the static server
61
- STATIC_SERVER_URL = os.getenv("STATIC_SERVER_URL", "/static")
62
- # Log level for the static server
63
- STATIC_SERVER_LOG_LEVEL = os.getenv("STATIC_SERVER_LOG_LEVEL", "info").lower()
64
58
 
65
59
 
66
60
  class EventLogHandler(logging.Handler):
@@ -79,6 +73,13 @@ class EventLogHandler(logging.Handler):
79
73
 
80
74
  # Logger for this module. Important that this is not the same as the griptape_nodes logger or else we'll have infinite log events.
81
75
  logger = logging.getLogger("griptape_nodes_app")
76
+
77
+ griptape_nodes_logger = logging.getLogger("griptape_nodes")
78
+ # When running as an app, we want to forward all log messages to the event queue so they can be sent to the GUI
79
+ griptape_nodes_logger.addHandler(EventLogHandler())
80
+ griptape_nodes_logger.addHandler(RichHandler(show_time=True, show_path=False, markup=True, rich_tracebacks=True))
81
+ griptape_nodes_logger.setLevel(logging.INFO)
82
+
82
83
  console = Console()
83
84
 
84
85
 
@@ -89,106 +90,45 @@ def start_app() -> None:
89
90
  """
90
91
  _init_event_listeners()
91
92
 
92
- griptape_nodes_logger = logging.getLogger("griptape_nodes")
93
- # When running as an app, we want to forward all log messages to the event queue so they can be sent to the GUI
94
- griptape_nodes_logger.addHandler(EventLogHandler())
95
- griptape_nodes_logger.addHandler(RichHandler(show_time=True, show_path=False, markup=True, rich_tracebacks=True))
96
- griptape_nodes_logger.setLevel(logging.INFO)
97
-
98
93
  # Listen for any signals to exit the app
99
94
  for sig in (signal.SIGINT, signal.SIGTERM):
100
95
  signal.signal(sig, lambda *_: sys.exit(0))
101
96
 
102
- # SSE subscription pushes events into event_queue
103
- threading.Thread(target=_listen_for_api_events, daemon=True).start()
97
+ api_key = _ensure_api_key()
98
+ threading.Thread(target=mcp_server, args=(api_key,), daemon=True).start()
99
+ threading.Thread(target=_listen_for_api_events, args=(api_key,), daemon=True).start()
104
100
 
105
101
  if STATIC_SERVER_ENABLED:
106
- threading.Thread(target=_serve_static_server, daemon=True).start()
102
+ static_dir = _build_static_dir()
103
+ threading.Thread(target=start_api, args=(static_dir, event_queue), daemon=True).start()
107
104
 
108
105
  _process_event_queue()
109
106
 
110
107
 
111
- def _serve_static_server() -> None:
112
- """Run FastAPI with Uvicorn in order to serve static files produced by nodes."""
113
- config_manager = GriptapeNodes.ConfigManager()
114
- app = FastAPI()
115
-
116
- static_dir = config_manager.workspace_path / config_manager.merged_config["static_files_directory"]
117
-
118
- if not static_dir.exists():
119
- static_dir.mkdir(parents=True, exist_ok=True)
120
-
121
- app.add_middleware(
122
- CORSMiddleware,
123
- allow_origins=[
124
- os.getenv("GRIPTAPE_NODES_UI_BASE_URL", "https://app.nodes.griptape.ai"),
125
- "https://app.nodes-staging.griptape.ai",
126
- "http://localhost:5173",
127
- ],
128
- allow_credentials=True,
129
- allow_methods=["OPTIONS", "GET", "POST", "PUT"],
130
- allow_headers=["*"],
131
- )
132
-
133
- app.mount(
134
- STATIC_SERVER_URL,
135
- StaticFiles(directory=static_dir),
136
- name="static",
137
- )
138
-
139
- @app.post("/static-upload-urls")
140
- async def create_static_file_upload_url(request: Request) -> dict:
141
- """Create a URL for uploading a static file.
142
-
143
- Similar to a presigned URL, but for uploading files to the static server.
144
- """
145
- base_url = request.base_url
146
- body = await request.json()
147
- file_name = body["file_name"]
148
- url = urljoin(str(base_url), f"/static-uploads/{file_name}")
149
-
150
- return {"url": url}
151
-
152
- @app.put("/static-uploads/{file_path:path}")
153
- async def create_static_file(request: Request, file_path: str) -> dict:
154
- """Upload a static file to the static server."""
155
- if not STATIC_SERVER_ENABLED:
156
- msg = "Static server is not enabled. Please set STATIC_SERVER_ENABLED to True."
157
- raise ValueError(msg)
158
-
159
- file_full_path = Path(static_dir / file_path)
108
+ def _ensure_api_key() -> str:
109
+ secrets_manager = GriptapeNodes.SecretsManager()
110
+ api_key = secrets_manager.get_secret("GT_CLOUD_API_KEY")
111
+ if api_key is None:
112
+ message = Panel(
113
+ Align.center(
114
+ "[bold red]Nodes API key is not set, please run [code]gtn init[/code] with a valid key: [/bold red]"
115
+ "[code]gtn init --api-key <your key>[/code]\n"
116
+ "[bold red]You can generate a new key from [/bold red][bold blue][link=https://nodes.griptape.ai]https://nodes.griptape.ai[/link][/bold blue]",
117
+ ),
118
+ title="🔑 ❌ Missing Nodes API Key",
119
+ border_style="red",
120
+ padding=(1, 4),
121
+ )
122
+ console.print(message)
123
+ sys.exit(1)
160
124
 
161
- # Create parent directories if they don't exist
162
- file_full_path.parent.mkdir(parents=True, exist_ok=True)
125
+ return api_key
163
126
 
164
- data = await request.body()
165
- try:
166
- file_full_path.write_bytes(data)
167
- except binascii.Error as e:
168
- msg = f"Invalid base64 encoding for file {file_path}."
169
- logger.error(msg)
170
- raise HTTPException(status_code=400, detail=msg) from e
171
- except (OSError, PermissionError) as e:
172
- msg = f"Failed to write file {file_path} to {config_manager.workspace_path}: {e}"
173
- logger.error(msg)
174
- raise HTTPException(status_code=500, detail=msg) from e
175
-
176
- static_url = f"http://{STATIC_SERVER_HOST}:{STATIC_SERVER_PORT}{STATIC_SERVER_URL}/{file_path}"
177
- return {"url": static_url}
178
-
179
- @app.post("/engines/request")
180
- async def create_event(request: Request) -> None:
181
- body = await request.json()
182
- if "payload" in body:
183
- __process_api_event(body["payload"])
184
-
185
- logging.getLogger("uvicorn").addHandler(
186
- RichHandler(show_time=True, show_path=False, markup=True, rich_tracebacks=True)
187
- )
188
127
 
189
- uvicorn.run(
190
- app, host=STATIC_SERVER_HOST, port=STATIC_SERVER_PORT, log_level=STATIC_SERVER_LOG_LEVEL, log_config=None
191
- )
128
+ def _build_static_dir() -> Path:
129
+ """Build the static directory path based on the workspace configuration."""
130
+ config_manager = GriptapeNodes.ConfigManager()
131
+ return Path(config_manager.workspace_path) / config_manager.merged_config["static_files_directory"]
192
132
 
193
133
 
194
134
  def _init_event_listeners() -> None:
@@ -219,38 +159,31 @@ def _init_event_listeners() -> None:
219
159
  )
220
160
 
221
161
 
222
- async def _alisten_for_api_requests() -> None:
162
+ async def _alisten_for_api_requests(api_key: str) -> None:
223
163
  """Listen for events from the Nodes API and process them asynchronously."""
224
164
  global ws_connection_for_sending, event_loop # noqa: PLW0603
225
165
  event_loop = asyncio.get_running_loop() # Store the event loop reference
226
- nodes_app_url = os.getenv("GRIPTAPE_NODES_UI_BASE_URL", "https://nodes.griptape.ai")
227
166
  logger.info("Listening for events from Nodes API via async WebSocket")
228
167
 
229
168
  # Auto reconnect https://websockets.readthedocs.io/en/stable/reference/asyncio/client.html#opening-a-connection
230
- connection_stream = __create_async_websocket_connection()
169
+ connection_stream = _create_websocket_connection(api_key)
231
170
  initialized = False
232
171
  async for ws_connection in connection_stream:
233
172
  try:
234
173
  ws_connection_for_sending = ws_connection # Store for sending events
174
+ ws_ready_event.set() # Signal that WebSocket is ready for sending
175
+
235
176
  if not initialized:
236
- __broadcast_app_initialization_complete(nodes_app_url)
177
+ event_queue.put(AppEvent(payload=app_events.AppInitializationComplete()))
237
178
  initialized = True
238
179
 
180
+ event_queue.put(AppEvent(payload=app_events.AppConnectionEstablished()))
181
+
239
182
  async for message in ws_connection:
240
183
  try:
241
184
  data = json.loads(message)
242
185
 
243
- payload = data.get("payload", {})
244
- # With heartbeat events, we skip the regular processing and just send the heartbeat
245
- # Technically no longer needed since https://github.com/griptape-ai/griptape-nodes/pull/369
246
- # but we don't have a proper EventRequest for it yet.
247
- if payload.get("request_type") == "Heartbeat":
248
- session_id = GriptapeNodes.get_session_id()
249
- await __send_heartbeat(
250
- session_id=session_id, request=payload["request"], ws_connection=ws_connection
251
- )
252
- else:
253
- __process_api_event(payload)
186
+ _process_api_event(data, event_queue)
254
187
  except Exception:
255
188
  logger.exception("Error processing event, skipping.")
256
189
  except ConnectionClosed:
@@ -260,9 +193,9 @@ async def _alisten_for_api_requests() -> None:
260
193
  await asyncio.sleep(2)
261
194
 
262
195
 
263
- def _listen_for_api_events() -> None:
196
+ def _listen_for_api_events(api_key: str) -> None:
264
197
  """Run the async WebSocket listener in an event loop."""
265
- asyncio.run(_alisten_for_api_requests())
198
+ asyncio.run(_alisten_for_api_requests(api_key))
266
199
 
267
200
 
268
201
  def __process_node_event(event: GriptapeNodeEvent) -> None:
@@ -276,9 +209,8 @@ def __process_node_event(event: GriptapeNodeEvent) -> None:
276
209
  else:
277
210
  msg = f"Unknown/unsupported result event type encountered: '{type(result_event)}'."
278
211
  raise TypeError(msg) from None
279
- # Don't send events over the wire that don't have a request_id set (e.g. engine-internal events)
280
- event_json = result_event.json()
281
- __schedule_async_task(__emit_message(dest_socket, event_json))
212
+
213
+ __schedule_async_task(__emit_message(dest_socket, result_event.json(), topic=result_event.response_topic))
282
214
 
283
215
 
284
216
  def __process_execution_node_event(event: ExecutionGriptapeNodeEvent) -> None:
@@ -286,7 +218,6 @@ def __process_execution_node_event(event: ExecutionGriptapeNodeEvent) -> None:
286
218
  result_event = event.wrapped_event
287
219
  if type(result_event.payload).__name__ == "NodeStartProcessEvent":
288
220
  GriptapeNodes.EventManager().current_active_node = result_event.payload.node_name
289
- event_json = result_event.json()
290
221
 
291
222
  if type(result_event.payload).__name__ == "ResumeNodeProcessingEvent":
292
223
  node_name = result_event.payload.node_name
@@ -300,7 +231,7 @@ def __process_execution_node_event(event: ExecutionGriptapeNodeEvent) -> None:
300
231
  msg = "Node start and finish do not match."
301
232
  raise KeyError(msg) from None
302
233
  GriptapeNodes.EventManager().current_active_node = None
303
- __schedule_async_task(__emit_message("execution_event", event_json))
234
+ __schedule_async_task(__emit_message("execution_event", result_event.json()))
304
235
 
305
236
 
306
237
  def __process_progress_event(gt_event: ProgressEvent) -> None:
@@ -328,11 +259,16 @@ def _process_event_queue() -> None:
328
259
 
329
260
  Event queue will be populated by background threads listening for events from the Nodes API.
330
261
  """
262
+ # Wait for WebSocket connection to be established before processing events
263
+ ws_ready_event.wait()
264
+
331
265
  while True:
332
266
  event = event_queue.get(block=True)
333
267
  if isinstance(event, EventRequest):
334
268
  request_payload = event.request
335
- GriptapeNodes.handle_request(request_payload)
269
+ GriptapeNodes.handle_request(
270
+ request_payload, response_topic=event.response_topic, request_id=event.request_id
271
+ )
336
272
  elif isinstance(event, AppEvent):
337
273
  __process_app_event(event)
338
274
  else:
@@ -341,27 +277,11 @@ def _process_event_queue() -> None:
341
277
  event_queue.task_done()
342
278
 
343
279
 
344
- def __create_async_websocket_connection() -> Any:
280
+ def _create_websocket_connection(api_key: str) -> Any:
345
281
  """Create an async WebSocket connection to the Nodes API."""
346
- secrets_manager = GriptapeNodes.SecretsManager()
347
- api_key = secrets_manager.get_secret("GT_CLOUD_API_KEY")
348
- if api_key is None:
349
- message = Panel(
350
- Align.center(
351
- "[bold red]Nodes API key is not set, please run [code]gtn init[/code] with a valid key: [/bold red]"
352
- "[code]gtn init --api-key <your key>[/code]\n"
353
- "[bold red]You can generate a new key from [/bold red][bold blue][link=https://nodes.griptape.ai]https://nodes.griptape.ai[/link][/bold blue]",
354
- ),
355
- title="🔑 ❌ Missing Nodes API Key",
356
- border_style="red",
357
- padding=(1, 4),
358
- )
359
- console.print(message)
360
- sys.exit(1)
361
-
362
282
  endpoint = urljoin(
363
283
  os.getenv("GRIPTAPE_NODES_API_BASE_URL", "https://api.nodes.griptape.ai").replace("http", "ws"),
364
- "/ws/engines/events?publish_channel=responses&subscribe_channel=requests",
284
+ "/ws/engines/events?version=v2",
365
285
  )
366
286
 
367
287
  return connect(
@@ -370,7 +290,7 @@ def __create_async_websocket_connection() -> Any:
370
290
  )
371
291
 
372
292
 
373
- async def __emit_message(event_type: str, payload: str) -> None:
293
+ async def __emit_message(event_type: str, payload: str, topic: str | None = None) -> None:
374
294
  """Send a message via WebSocket asynchronously."""
375
295
  global ws_connection_for_sending # noqa: PLW0602
376
296
  if ws_connection_for_sending is None:
@@ -378,7 +298,12 @@ async def __emit_message(event_type: str, payload: str) -> None:
378
298
  return
379
299
 
380
300
  try:
381
- body = {"type": event_type, "payload": json.loads(payload) if payload else {}}
301
+ # Determine topic based on session_id and engine_id in the payload
302
+ if topic is None:
303
+ topic = _determine_response_topic()
304
+
305
+ body = {"type": event_type, "payload": json.loads(payload), "topic": topic}
306
+
382
307
  await ws_connection_for_sending.send(json.dumps(body))
383
308
  except WebSocketException as e:
384
309
  logger.error("Error sending event to Nodes API: %s", e)
@@ -386,97 +311,87 @@ async def __emit_message(event_type: str, payload: str) -> None:
386
311
  logger.error("Unexpected error while sending event to Nodes API: %s", e)
387
312
 
388
313
 
389
- async def __send_heartbeat(*, session_id: str | None, request: dict, ws_connection: Any) -> None:
390
- """Send a heartbeat response via WebSocket."""
391
- heartbeat_response = {
392
- "request": request,
393
- "result": {},
394
- "request_type": "Heartbeat",
395
- "event_type": "EventResultSuccess",
396
- "result_type": "HeartbeatSuccess",
397
- **({"session_id": session_id} if session_id is not None else {}),
398
- }
314
+ def _determine_response_topic() -> str | None:
315
+ """Determine the response topic based on session_id and engine_id in the payload."""
316
+ engine_id = GriptapeNodes.get_engine_id()
317
+ session_id = GriptapeNodes.get_session_id()
399
318
 
400
- body = {"type": "success_result", "payload": heartbeat_response}
401
- try:
402
- await ws_connection.send(json.dumps(body))
403
- logger.debug(
404
- "Responded to heartbeat request with session: %s and request: %s", session_id, request.get("request_id")
405
- )
406
- except WebSocketException as e:
407
- logger.error("Error sending heartbeat response: %s", e)
408
- except Exception as e:
409
- logger.error("Unexpected error while sending heartbeat response: %s", e)
319
+ # Normal topic determination logic
320
+ # Check for session_id first (highest priority)
321
+ if session_id:
322
+ return f"sessions/{session_id}/response"
410
323
 
324
+ # Check for engine_id if no session_id
325
+ if engine_id:
326
+ return f"engines/{engine_id}/response"
411
327
 
412
- def __schedule_async_task(coro: Any) -> None:
413
- """Schedule an async coroutine to run in the event loop from a sync context."""
414
- if event_loop and event_loop.is_running():
415
- asyncio.run_coroutine_threadsafe(coro, event_loop)
416
- else:
417
- logger.warning("Event loop not available for scheduling async task")
328
+ # Default to generic response topic
329
+ return "response"
418
330
 
419
331
 
420
- def __broadcast_app_initialization_complete(nodes_app_url: str) -> None:
421
- """Broadcast the AppInitializationComplete event to all listeners.
332
+ def _determine_request_topic() -> str | None:
333
+ """Determine the request topic based on session_id and engine_id in the payload."""
334
+ engine_id = GriptapeNodes.get_engine_id()
335
+ session_id = GriptapeNodes.get_session_id()
422
336
 
423
- This is used to notify the GUI that the app is ready to receive events.
424
- """
425
- # Broadcast this to anybody who wants a callback on "hey, the app's ready to roll"
426
- payload = app_events.AppInitializationComplete()
427
- app_event = AppEvent(payload=payload)
428
- event_queue.put(app_event)
429
-
430
- engine_version_request = app_events.GetEngineVersionRequest()
431
- engine_version_result = GriptapeNodes.get_instance().handle_engine_version_request(engine_version_request)
432
- if isinstance(engine_version_result, app_events.GetEngineVersionResultSuccess):
433
- engine_version = f"v{engine_version_result.major}.{engine_version_result.minor}.{engine_version_result.patch}"
434
- else:
435
- engine_version = "<UNKNOWN ENGINE VERSION>"
436
-
437
- message = Panel(
438
- Align.center(
439
- f"[bold green]Engine is ready to receive events[/bold green]\n"
440
- f"[bold blue]Return to: [link={nodes_app_url}]{nodes_app_url}[/link] to access the Workflow Editor[/bold blue]",
441
- vertical="middle",
442
- ),
443
- title="🚀 Griptape Nodes Engine Started",
444
- subtitle=f"[green]{engine_version}[/green]",
445
- border_style="green",
446
- padding=(1, 4),
447
- )
448
- console.print(message)
337
+ # Normal topic determination logic
338
+ # Check for session_id first (highest priority)
339
+ if session_id:
340
+ return f"sessions/{session_id}/request"
449
341
 
342
+ # Check for engine_id if no session_id
343
+ if engine_id:
344
+ return f"engines/{engine_id}/request"
450
345
 
451
- def __process_api_event(data: dict) -> None:
452
- """Process API events and send them to the event queue."""
453
- try:
454
- data["request"]
455
- except KeyError:
456
- msg = "Error: 'request' was expected but not found."
457
- raise RuntimeError(msg) from None
346
+ # Default to generic request topic
347
+ return "request"
348
+
349
+
350
+ def subscribe_to_topic(topic: str) -> None:
351
+ """Subscribe to a specific topic in the message bus."""
352
+ __schedule_async_task(_asubscribe_to_topic(topic))
353
+
354
+
355
+ def unsubscribe_from_topic(topic: str) -> None:
356
+ """Unsubscribe from a specific topic in the message bus."""
357
+ __schedule_async_task(_aunsubscribe_from_topic(topic))
358
+
359
+
360
+ async def _asubscribe_to_topic(topic: str) -> None:
361
+ """Subscribe to a specific topic in the message bus."""
362
+ if ws_connection_for_sending is None:
363
+ logger.warning("WebSocket connection not available for subscribing to topic")
364
+ return
458
365
 
459
366
  try:
460
- event_type = data["event_type"]
461
- if event_type != "EventRequest":
462
- msg = "Error: 'event_type' was found on request, but did not match 'EventRequest' as expected."
463
- raise RuntimeError(msg) from None
464
- except KeyError:
465
- msg = "Error: 'event_type' not found in request."
466
- raise RuntimeError(msg) from None
467
-
468
- # Now attempt to convert it into an EventRequest.
469
- try:
470
- request_event: EventRequest = cast("EventRequest", deserialize_event(json_data=data))
367
+ body = {"type": "subscribe", "topic": topic, "payload": {}}
368
+ await ws_connection_for_sending.send(json.dumps(body))
369
+ logger.info("Subscribed to topic: %s", topic)
370
+ except WebSocketException as e:
371
+ logger.error("Error subscribing to topic %s: %s", topic, e)
471
372
  except Exception as e:
472
- msg = f"Unable to convert request JSON into a valid EventRequest object. Error Message: '{e}'"
473
- raise RuntimeError(msg) from None
373
+ logger.error("Unexpected error while subscribing to topic %s: %s", topic, e)
374
+
375
+
376
+ async def _aunsubscribe_from_topic(topic: str) -> None:
377
+ """Unsubscribe from a specific topic in the message bus."""
378
+ if ws_connection_for_sending is None:
379
+ logger.warning("WebSocket connection not available for unsubscribing from topic")
380
+ return
474
381
 
475
- # Add a request_id to the payload
476
- request_id = request_event.request.request_id
477
- request_event.request.request_id = request_id
382
+ try:
383
+ body = {"type": "unsubscribe", "topic": topic, "payload": {}}
384
+ await ws_connection_for_sending.send(json.dumps(body))
385
+ logger.info("Unsubscribed from topic: %s", topic)
386
+ except WebSocketException as e:
387
+ logger.error("Error unsubscribing from topic %s: %s", topic, e)
388
+ except Exception as e:
389
+ logger.error("Unexpected error while unsubscribing from topic %s: %s", topic, e)
478
390
 
479
- # Add the event to the queue
480
- event_queue.put(request_event)
481
391
 
482
- return request_id
392
+ def __schedule_async_task(coro: Any) -> None:
393
+ """Schedule an async coroutine to run in the event loop from a sync context."""
394
+ if event_loop and event_loop.is_running():
395
+ asyncio.run_coroutine_threadsafe(coro, event_loop)
396
+ else:
397
+ logger.warning("Event loop not available for scheduling async task")
@@ -30,7 +30,7 @@ class ReloadHandler(PatternMatchingEventHandler):
30
30
  def start_process(self) -> None:
31
31
  if self.process:
32
32
  self.process.terminate()
33
- self.process = subprocess.Popen( # noqa: S603
33
+ self.process = subprocess.Popen(
34
34
  ["uv", "run", "gtn"], # noqa: S607
35
35
  stdout=sys.stdout,
36
36
  stderr=sys.stderr,
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -1,10 +1,12 @@
1
1
  import logging
2
2
  import threading
3
3
  from multiprocessing import Process, Queue
4
+ from multiprocessing import Queue as ProcessQueue
4
5
  from pathlib import Path
5
6
  from typing import Any
6
7
 
7
- from griptape_nodes.app.app import _serve_static_server
8
+ from griptape_nodes.app.api import start_api
9
+ from griptape_nodes.app.app import _build_static_dir
8
10
  from griptape_nodes.bootstrap.workflow_runners.local_workflow_runner import LocalWorkflowRunner
9
11
  from griptape_nodes.bootstrap.workflow_runners.workflow_runner import WorkflowRunner
10
12
 
@@ -30,7 +32,9 @@ class SubprocessWorkflowRunner(WorkflowRunner):
30
32
  logger.setLevel(logging.NOTSET)
31
33
 
32
34
  try:
33
- threading.Thread(target=_serve_static_server, daemon=True).start()
35
+ static_dir = _build_static_dir()
36
+ event_queue = ProcessQueue()
37
+ threading.Thread(target=start_api, args=(static_dir, event_queue), daemon=True).start()
34
38
  workflow_runner = LocalWorkflowRunner(libraries)
35
39
  workflow_runner.run(workflow_path, workflow_name, flow_input, "local")
36
40
  except Exception as e:
File without changes
File without changes
File without changes
@@ -4,7 +4,8 @@ from urllib.parse import urljoin
4
4
 
5
5
  import httpx
6
6
 
7
- from griptape_nodes.app.app import STATIC_SERVER_ENABLED, STATIC_SERVER_HOST, STATIC_SERVER_PORT, STATIC_SERVER_URL
7
+ from griptape_nodes.app.api import STATIC_SERVER_HOST, STATIC_SERVER_PORT, STATIC_SERVER_URL
8
+ from griptape_nodes.app.app import STATIC_SERVER_ENABLED
8
9
  from griptape_nodes.drivers.storage.base_storage_driver import BaseStorageDriver, CreateSignedUploadUrlResponse
9
10
 
10
11
  logger = logging.getLogger("griptape_nodes")
File without changes
File without changes
File without changes
File without changes
File without changes