griptape-nodes 0.52.0__py3-none-any.whl → 0.52.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- griptape_nodes/app/api.py +1 -12
- griptape_nodes/app/app.py +256 -193
- griptape_nodes/mcp_server/server.py +1 -1
- {griptape_nodes-0.52.0.dist-info → griptape_nodes-0.52.1.dist-info}/METADATA +1 -1
- {griptape_nodes-0.52.0.dist-info → griptape_nodes-0.52.1.dist-info}/RECORD +7 -7
- {griptape_nodes-0.52.0.dist-info → griptape_nodes-0.52.1.dist-info}/WHEEL +0 -0
- {griptape_nodes-0.52.0.dist-info → griptape_nodes-0.52.1.dist-info}/entry_points.txt +0 -0
griptape_nodes/app/api.py
CHANGED
|
@@ -141,17 +141,6 @@ async def _delete_static_file(file_path: str, static_directory: Annotated[Path,
|
|
|
141
141
|
return {"message": f"File {file_path} deleted successfully"}
|
|
142
142
|
|
|
143
143
|
|
|
144
|
-
@app.post("/engines/request")
|
|
145
|
-
async def _create_event(request: Request) -> None:
|
|
146
|
-
"""Create event using centralized event utilities."""
|
|
147
|
-
from .app import _process_api_event
|
|
148
|
-
|
|
149
|
-
body = await request.json()
|
|
150
|
-
|
|
151
|
-
# Use centralized event processing
|
|
152
|
-
await _process_api_event(body)
|
|
153
|
-
|
|
154
|
-
|
|
155
144
|
def _setup_app(static_directory: Path) -> None:
|
|
156
145
|
"""Setup FastAPI app with middleware and static files."""
|
|
157
146
|
global static_dir # noqa: PLW0603
|
|
@@ -179,7 +168,7 @@ def _setup_app(static_directory: Path) -> None:
|
|
|
179
168
|
)
|
|
180
169
|
|
|
181
170
|
|
|
182
|
-
def
|
|
171
|
+
def start_static_server(static_directory: Path) -> None:
|
|
183
172
|
"""Run uvicorn server synchronously using uvicorn.run."""
|
|
184
173
|
# Setup the FastAPI app
|
|
185
174
|
_setup_app(static_directory)
|
griptape_nodes/app/app.py
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
-
import contextlib
|
|
5
|
-
import contextvars
|
|
6
4
|
import json
|
|
7
5
|
import logging
|
|
8
6
|
import os
|
|
9
7
|
import sys
|
|
8
|
+
import threading
|
|
9
|
+
from dataclasses import dataclass
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from typing import Any
|
|
12
12
|
from urllib.parse import urljoin
|
|
@@ -18,6 +18,8 @@ from rich.panel import Panel
|
|
|
18
18
|
from websockets.asyncio.client import connect
|
|
19
19
|
from websockets.exceptions import ConnectionClosed, WebSocketException
|
|
20
20
|
|
|
21
|
+
from griptape_nodes.app.api import start_static_server
|
|
22
|
+
from griptape_nodes.mcp_server.server import start_mcp_server
|
|
21
23
|
from griptape_nodes.retained_mode.events import app_events, execution_events
|
|
22
24
|
|
|
23
25
|
# This import is necessary to register all events, even if not technically used
|
|
@@ -36,11 +38,41 @@ from griptape_nodes.retained_mode.events.base_events import (
|
|
|
36
38
|
from griptape_nodes.retained_mode.events.logger_events import LogHandlerEvent
|
|
37
39
|
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
38
40
|
|
|
39
|
-
# Context variable for WebSocket connection - avoids global state
|
|
40
|
-
ws_connection_context: contextvars.ContextVar[Any | None] = contextvars.ContextVar("ws_connection", default=None)
|
|
41
41
|
|
|
42
|
-
#
|
|
43
|
-
|
|
42
|
+
# WebSocket thread communication message types
|
|
43
|
+
@dataclass
|
|
44
|
+
class WebSocketMessage:
|
|
45
|
+
"""Message to send via WebSocket."""
|
|
46
|
+
|
|
47
|
+
event_type: str
|
|
48
|
+
payload: str
|
|
49
|
+
topic: str | None = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class SubscribeCommand:
|
|
54
|
+
"""Command to subscribe to a topic."""
|
|
55
|
+
|
|
56
|
+
topic: str
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class UnsubscribeCommand:
|
|
61
|
+
"""Command to unsubscribe from a topic."""
|
|
62
|
+
|
|
63
|
+
topic: str
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# WebSocket outgoing queue for messages and commands.
|
|
67
|
+
# Appears to be fine to create outside event loop
|
|
68
|
+
# https://discuss.python.org/t/can-asyncio-queue-be-safely-created-outside-of-the-event-loop-thread/49215/8
|
|
69
|
+
ws_outgoing_queue: asyncio.Queue = asyncio.Queue()
|
|
70
|
+
|
|
71
|
+
# Background WebSocket event loop reference for cross-thread communication
|
|
72
|
+
websocket_event_loop: asyncio.AbstractEventLoop | None = None
|
|
73
|
+
|
|
74
|
+
# Threading event to signal when websocket_event_loop is ready
|
|
75
|
+
websocket_event_loop_ready = threading.Event()
|
|
44
76
|
|
|
45
77
|
|
|
46
78
|
# Whether to enable the static server
|
|
@@ -95,62 +127,84 @@ async def astart_app() -> None:
|
|
|
95
127
|
"""New async app entry point."""
|
|
96
128
|
api_key = _ensure_api_key()
|
|
97
129
|
|
|
130
|
+
# Initialize event queue in main thread
|
|
98
131
|
griptape_nodes.EventManager().initialize_queue()
|
|
99
132
|
|
|
100
|
-
#
|
|
101
|
-
|
|
133
|
+
# Get main loop reference
|
|
134
|
+
main_loop = asyncio.get_running_loop()
|
|
102
135
|
|
|
103
136
|
try:
|
|
104
|
-
#
|
|
105
|
-
|
|
106
|
-
server_tasks = []
|
|
137
|
+
# Start MCP server in daemon thread
|
|
138
|
+
threading.Thread(target=start_mcp_server, args=(api_key,), daemon=True, name="mcp-server").start()
|
|
107
139
|
|
|
108
|
-
# Start
|
|
109
|
-
server_tasks.append(asyncio.to_thread(_run_mcp_server_sync, api_key))
|
|
110
|
-
|
|
111
|
-
# Start static server in thread if enabled
|
|
140
|
+
# Start static server in daemon thread if enabled
|
|
112
141
|
if STATIC_SERVER_ENABLED:
|
|
113
142
|
static_dir = _build_static_dir()
|
|
114
|
-
|
|
143
|
+
threading.Thread(target=start_static_server, args=(static_dir,), daemon=True, name="static-server").start()
|
|
115
144
|
|
|
116
|
-
#
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
]
|
|
145
|
+
# Start WebSocket tasks in daemon thread
|
|
146
|
+
threading.Thread(
|
|
147
|
+
target=_start_websocket_connection, args=(api_key, main_loop), daemon=True, name="websocket-tasks"
|
|
148
|
+
).start()
|
|
121
149
|
|
|
122
|
-
#
|
|
123
|
-
|
|
150
|
+
# Run event processing on main thread
|
|
151
|
+
await _process_event_queue()
|
|
124
152
|
|
|
125
|
-
async with asyncio.TaskGroup() as tg:
|
|
126
|
-
for task in all_tasks:
|
|
127
|
-
# Context is supposed to be copied automatically, but it isn't working for some reason so we do it manually here
|
|
128
|
-
tg.create_task(task, context=shared_context)
|
|
129
153
|
except Exception as e:
|
|
130
154
|
logger.error("Application startup failed: %s", e)
|
|
131
155
|
raise
|
|
132
156
|
|
|
133
157
|
|
|
134
|
-
def
|
|
135
|
-
"""Run
|
|
158
|
+
def _start_websocket_connection(api_key: str, main_loop: asyncio.AbstractEventLoop) -> None:
|
|
159
|
+
"""Run WebSocket tasks in a separate thread with its own async loop."""
|
|
160
|
+
global websocket_event_loop # noqa: PLW0603
|
|
136
161
|
try:
|
|
137
|
-
|
|
162
|
+
# Create a new event loop for this thread
|
|
163
|
+
loop = asyncio.new_event_loop()
|
|
164
|
+
websocket_event_loop = loop
|
|
165
|
+
asyncio.set_event_loop(loop)
|
|
166
|
+
|
|
167
|
+
# Signal that websocket_event_loop is ready
|
|
168
|
+
websocket_event_loop_ready.set()
|
|
138
169
|
|
|
139
|
-
|
|
170
|
+
# Run the async WebSocket tasks
|
|
171
|
+
loop.run_until_complete(_run_websocket_tasks(api_key, main_loop))
|
|
140
172
|
except Exception as e:
|
|
141
|
-
logger.error("
|
|
173
|
+
logger.error("WebSocket thread error: %s", e)
|
|
142
174
|
raise
|
|
175
|
+
finally:
|
|
176
|
+
websocket_event_loop = None
|
|
177
|
+
websocket_event_loop_ready.clear()
|
|
143
178
|
|
|
144
179
|
|
|
145
|
-
def
|
|
146
|
-
"""Run
|
|
147
|
-
|
|
148
|
-
|
|
180
|
+
async def _run_websocket_tasks(api_key: str, main_loop: asyncio.AbstractEventLoop) -> None:
|
|
181
|
+
"""Run WebSocket tasks - async version."""
|
|
182
|
+
# Create WebSocket connection for this thread
|
|
183
|
+
connection_stream = _create_websocket_connection(api_key)
|
|
149
184
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
185
|
+
# Track if this is the first connection
|
|
186
|
+
initialized = False
|
|
187
|
+
|
|
188
|
+
async for ws_connection in connection_stream:
|
|
189
|
+
try:
|
|
190
|
+
# Emit initialization event only for the first connection
|
|
191
|
+
if not initialized:
|
|
192
|
+
griptape_nodes.EventManager().put_event_threadsafe(
|
|
193
|
+
main_loop, AppEvent(payload=app_events.AppInitializationComplete())
|
|
194
|
+
)
|
|
195
|
+
initialized = True
|
|
196
|
+
|
|
197
|
+
# Emit connection established event for every connection
|
|
198
|
+
griptape_nodes.EventManager().put_event_threadsafe(
|
|
199
|
+
main_loop, AppEvent(payload=app_events.AppConnectionEstablished())
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
async with asyncio.TaskGroup() as tg:
|
|
203
|
+
tg.create_task(_process_incoming_messages(ws_connection, main_loop))
|
|
204
|
+
tg.create_task(_send_outgoing_messages(ws_connection))
|
|
205
|
+
except* Exception as e:
|
|
206
|
+
logger.error("WebSocket tasks failed: %s", e.exceptions)
|
|
207
|
+
await asyncio.sleep(2.0) # Wait before retry
|
|
154
208
|
|
|
155
209
|
|
|
156
210
|
def _ensure_api_key() -> str:
|
|
@@ -179,70 +233,156 @@ def _build_static_dir() -> Path:
|
|
|
179
233
|
return Path(config_manager.workspace_path) / config_manager.merged_config["static_files_directory"]
|
|
180
234
|
|
|
181
235
|
|
|
182
|
-
async def
|
|
183
|
-
"""
|
|
184
|
-
logger.info("
|
|
185
|
-
|
|
186
|
-
connection_stream = _create_websocket_connection(api_key)
|
|
187
|
-
initialized = False
|
|
236
|
+
async def _process_incoming_messages(ws_connection: Any, main_loop: asyncio.AbstractEventLoop) -> None:
|
|
237
|
+
"""Process incoming WebSocket requests from Nodes API."""
|
|
238
|
+
logger.info("Processing incoming WebSocket requests from WebSocket connection")
|
|
188
239
|
|
|
189
240
|
try:
|
|
190
|
-
async for
|
|
191
|
-
|
|
192
|
-
|
|
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.")
|
|
193
247
|
|
|
248
|
+
except ConnectionClosed:
|
|
249
|
+
logger.info("WebSocket connection closed, will retry")
|
|
194
250
|
except asyncio.CancelledError:
|
|
195
251
|
# Clean shutdown when task is cancelled
|
|
196
252
|
logger.info("WebSocket listener shutdown complete")
|
|
197
253
|
raise
|
|
198
254
|
except Exception as e:
|
|
199
|
-
logger.error("
|
|
255
|
+
logger.error("Error in WebSocket connection. Retrying in 2 seconds... %s", e)
|
|
256
|
+
await asyncio.sleep(2.0)
|
|
200
257
|
raise
|
|
201
258
|
finally:
|
|
202
|
-
|
|
259
|
+
logger.info("WebSocket listener shutdown complete")
|
|
203
260
|
|
|
204
261
|
|
|
205
|
-
|
|
206
|
-
"""
|
|
262
|
+
def _create_websocket_connection(api_key: str) -> Any:
|
|
263
|
+
"""Create an async WebSocket connection to the Nodes API."""
|
|
264
|
+
endpoint = urljoin(
|
|
265
|
+
os.getenv("GRIPTAPE_NODES_API_BASE_URL", "https://api.nodes.griptape.ai").replace("http", "ws"),
|
|
266
|
+
"/ws/engines/events?version=v2",
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
return connect(
|
|
270
|
+
endpoint,
|
|
271
|
+
additional_headers={"Authorization": f"Bearer {api_key}"},
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
async def _process_api_event(event: dict, main_loop: asyncio.AbstractEventLoop) -> None:
|
|
276
|
+
"""Process API events and add to async queue."""
|
|
277
|
+
payload = event.get("payload", {})
|
|
278
|
+
|
|
207
279
|
try:
|
|
208
|
-
|
|
209
|
-
|
|
280
|
+
payload["request"]
|
|
281
|
+
except KeyError:
|
|
282
|
+
msg = "Error: 'request' was expected but not found."
|
|
283
|
+
raise RuntimeError(msg) from None
|
|
210
284
|
|
|
211
|
-
|
|
212
|
-
|
|
285
|
+
try:
|
|
286
|
+
event_type = payload["event_type"]
|
|
287
|
+
if event_type != "EventRequest":
|
|
288
|
+
msg = "Error: 'event_type' was found on request, but did not match 'EventRequest' as expected."
|
|
289
|
+
raise RuntimeError(msg) from None
|
|
290
|
+
except KeyError:
|
|
291
|
+
msg = "Error: 'event_type' not found in request."
|
|
292
|
+
raise RuntimeError(msg) from None
|
|
213
293
|
|
|
214
|
-
|
|
294
|
+
# Now attempt to convert it into an EventRequest.
|
|
295
|
+
try:
|
|
296
|
+
request_event = deserialize_event(json_data=payload)
|
|
297
|
+
except Exception as e:
|
|
298
|
+
msg = f"Unable to convert request JSON into a valid EventRequest object. Error Message: '{e}'"
|
|
299
|
+
raise RuntimeError(msg) from None
|
|
300
|
+
|
|
301
|
+
if not isinstance(request_event, EventRequest):
|
|
302
|
+
msg = f"Deserialized event is not an EventRequest: {type(request_event)}"
|
|
303
|
+
raise TypeError(msg)
|
|
304
|
+
|
|
305
|
+
# Check if the event implements SkipTheLineMixin for priority processing
|
|
306
|
+
if isinstance(request_event.request, SkipTheLineMixin):
|
|
307
|
+
# Handle the event immediately without queuing
|
|
308
|
+
await _process_event_request(request_event)
|
|
309
|
+
else:
|
|
310
|
+
# Add the event to the main thread event queue for processing
|
|
311
|
+
griptape_nodes.EventManager().put_event_threadsafe(main_loop, request_event)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
async def _send_outgoing_messages(ws_connection: Any) -> None:
|
|
315
|
+
"""Send outgoing WebSocket requests from queue on background thread."""
|
|
316
|
+
logger.info("Starting outgoing WebSocket request sender")
|
|
317
|
+
|
|
318
|
+
try:
|
|
319
|
+
while True:
|
|
320
|
+
# Get message from outgoing queue
|
|
321
|
+
message = await ws_outgoing_queue.get()
|
|
215
322
|
|
|
216
|
-
async for message in ws_connection:
|
|
217
323
|
try:
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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()
|
|
222
336
|
|
|
223
|
-
except
|
|
224
|
-
logger.info("
|
|
337
|
+
except asyncio.CancelledError:
|
|
338
|
+
logger.info("Outbound request sender shutdown complete")
|
|
339
|
+
raise
|
|
225
340
|
except Exception as e:
|
|
226
|
-
logger.error("
|
|
227
|
-
|
|
228
|
-
finally:
|
|
229
|
-
ws_connection_context.set(None)
|
|
230
|
-
ws_ready_event.clear()
|
|
341
|
+
logger.error("Fatal error in outgoing request sender: %s", e)
|
|
342
|
+
raise
|
|
231
343
|
|
|
232
344
|
|
|
233
|
-
async def
|
|
234
|
-
"""
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
345
|
+
async def _send_websocket_message(ws_connection: Any, event_type: str, payload: str, topic: str | None) -> None:
|
|
346
|
+
"""Send a message via WebSocket."""
|
|
347
|
+
try:
|
|
348
|
+
if topic is None:
|
|
349
|
+
topic = determine_response_topic()
|
|
350
|
+
|
|
351
|
+
body = {"type": event_type, "payload": json.loads(payload), "topic": topic}
|
|
352
|
+
await ws_connection.send(json.dumps(body))
|
|
353
|
+
except WebSocketException as e:
|
|
354
|
+
logger.error("Error sending WebSocket message: %s", e)
|
|
355
|
+
except Exception as e:
|
|
356
|
+
logger.error("Unexpected error sending WebSocket message: %s", e)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
async def _send_subscribe_command(ws_connection: Any, topic: str) -> None:
|
|
360
|
+
"""Send subscribe command via WebSocket."""
|
|
361
|
+
try:
|
|
362
|
+
body = {"type": "subscribe", "topic": topic, "payload": {}}
|
|
363
|
+
await ws_connection.send(json.dumps(body))
|
|
364
|
+
logger.info("Subscribed to topic: %s", topic)
|
|
365
|
+
except WebSocketException as e:
|
|
366
|
+
logger.error("Error subscribing to topic %s: %s", topic, e)
|
|
367
|
+
except Exception as e:
|
|
368
|
+
logger.error("Unexpected error subscribing to topic %s: %s", topic, e)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
async def _send_unsubscribe_command(ws_connection: Any, topic: str) -> None:
|
|
372
|
+
"""Send unsubscribe command via WebSocket."""
|
|
373
|
+
try:
|
|
374
|
+
body = {"type": "unsubscribe", "topic": topic, "payload": {}}
|
|
375
|
+
await ws_connection.send(json.dumps(body))
|
|
376
|
+
logger.info("Unsubscribed from topic: %s", topic)
|
|
377
|
+
except WebSocketException as e:
|
|
378
|
+
logger.error("Error unsubscribing from topic %s: %s", topic, e)
|
|
379
|
+
except Exception as e:
|
|
380
|
+
logger.error("Unexpected error unsubscribing from topic %s: %s", topic, e)
|
|
240
381
|
|
|
241
382
|
|
|
242
383
|
async def _process_event_queue() -> None:
|
|
243
|
-
"""Process events concurrently -
|
|
244
|
-
|
|
245
|
-
await _await_websocket_ready()
|
|
384
|
+
"""Process events concurrently - runs on main thread."""
|
|
385
|
+
logger.info("Starting event queue processor on main thread")
|
|
246
386
|
background_tasks = set()
|
|
247
387
|
|
|
248
388
|
def _handle_task_result(task: asyncio.Task) -> None:
|
|
@@ -291,17 +431,7 @@ async def _process_event_request(event: EventRequest) -> None:
|
|
|
291
431
|
else:
|
|
292
432
|
dest_socket = "failure_result"
|
|
293
433
|
|
|
294
|
-
await
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
async def _await_websocket_ready() -> None:
|
|
298
|
-
"""Wait for WebSocket connection to be ready using event coordination."""
|
|
299
|
-
websocket_timeout = 15
|
|
300
|
-
try:
|
|
301
|
-
await asyncio.wait_for(ws_ready_event.wait(), timeout=websocket_timeout)
|
|
302
|
-
except TimeoutError:
|
|
303
|
-
console.print("[red]WebSocket connection timeout[/red]")
|
|
304
|
-
raise
|
|
434
|
+
await _send_message(dest_socket, result_event.json(), topic=result_event.response_topic)
|
|
305
435
|
|
|
306
436
|
|
|
307
437
|
async def _process_app_event(event: AppEvent) -> None:
|
|
@@ -309,7 +439,7 @@ async def _process_app_event(event: AppEvent) -> None:
|
|
|
309
439
|
# Let Griptape Nodes broadcast it.
|
|
310
440
|
await griptape_nodes.broadcast_app_event(event.payload)
|
|
311
441
|
|
|
312
|
-
await
|
|
442
|
+
await _send_message("app_event", event.json())
|
|
313
443
|
|
|
314
444
|
|
|
315
445
|
async def _process_node_event(event: GriptapeNodeEvent) -> None:
|
|
@@ -324,12 +454,12 @@ async def _process_node_event(event: GriptapeNodeEvent) -> None:
|
|
|
324
454
|
msg = f"Unknown/unsupported result event type encountered: '{type(result_event)}'."
|
|
325
455
|
raise TypeError(msg) from None
|
|
326
456
|
|
|
327
|
-
await
|
|
457
|
+
await _send_message(dest_socket, result_event.json(), topic=result_event.response_topic)
|
|
328
458
|
|
|
329
459
|
|
|
330
460
|
async def _process_execution_node_event(event: ExecutionGriptapeNodeEvent) -> None:
|
|
331
461
|
"""Process ExecutionGriptapeNodeEvents and send them to the API (async version)."""
|
|
332
|
-
await
|
|
462
|
+
await _send_message("execution_event", event.wrapped_event.json())
|
|
333
463
|
|
|
334
464
|
|
|
335
465
|
async def _process_progress_event(gt_event: ProgressEvent) -> None:
|
|
@@ -341,41 +471,47 @@ async def _process_progress_event(gt_event: ProgressEvent) -> None:
|
|
|
341
471
|
node_name=node_name, parameter_name=gt_event.parameter_name, type=type(gt_event).__name__, value=value
|
|
342
472
|
)
|
|
343
473
|
event_to_emit = ExecutionEvent(payload=payload)
|
|
344
|
-
await
|
|
474
|
+
await _send_message("execution_event", event_to_emit.json())
|
|
345
475
|
|
|
346
476
|
|
|
347
|
-
def
|
|
348
|
-
"""
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
"/ws/engines/events?version=v2",
|
|
352
|
-
)
|
|
477
|
+
async def _send_message(event_type: str, payload: str, topic: str | None = None) -> None:
|
|
478
|
+
"""Queue a message to be sent via WebSocket using run_coroutine_threadsafe."""
|
|
479
|
+
# Wait for websocket event loop to be ready
|
|
480
|
+
websocket_event_loop_ready.wait()
|
|
353
481
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
482
|
+
# Use run_coroutine_threadsafe to put message into WebSocket background thread queue
|
|
483
|
+
if websocket_event_loop is None:
|
|
484
|
+
logger.error("WebSocket event loop not available for message")
|
|
485
|
+
return
|
|
486
|
+
|
|
487
|
+
# Determine topic based on session_id and engine_id in the payload
|
|
488
|
+
if topic is None:
|
|
489
|
+
topic = determine_response_topic()
|
|
490
|
+
|
|
491
|
+
message = WebSocketMessage(event_type, payload, topic)
|
|
492
|
+
|
|
493
|
+
asyncio.run_coroutine_threadsafe(ws_outgoing_queue.put(message), websocket_event_loop)
|
|
358
494
|
|
|
359
495
|
|
|
360
|
-
async def
|
|
361
|
-
"""
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
496
|
+
async def subscribe_to_topic(topic: str) -> None:
|
|
497
|
+
"""Queue a subscribe command for WebSocket using run_coroutine_threadsafe."""
|
|
498
|
+
# Wait for websocket event loop to be ready
|
|
499
|
+
websocket_event_loop_ready.wait()
|
|
500
|
+
|
|
501
|
+
if websocket_event_loop is None:
|
|
502
|
+
logger.error("WebSocket event loop not available for subscribe")
|
|
365
503
|
return
|
|
366
504
|
|
|
367
|
-
|
|
368
|
-
# Determine topic based on session_id and engine_id in the payload
|
|
369
|
-
if topic is None:
|
|
370
|
-
topic = determine_response_topic()
|
|
505
|
+
asyncio.run_coroutine_threadsafe(ws_outgoing_queue.put(SubscribeCommand(topic)), websocket_event_loop)
|
|
371
506
|
|
|
372
|
-
body = {"type": event_type, "payload": json.loads(payload), "topic": topic}
|
|
373
507
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
508
|
+
async def unsubscribe_from_topic(topic: str) -> None:
|
|
509
|
+
"""Queue an unsubscribe command for WebSocket using run_coroutine_threadsafe."""
|
|
510
|
+
if websocket_event_loop is None:
|
|
511
|
+
logger.error("WebSocket event loop not available for unsubscribe")
|
|
512
|
+
return
|
|
513
|
+
|
|
514
|
+
asyncio.run_coroutine_threadsafe(ws_outgoing_queue.put(UnsubscribeCommand(topic)), websocket_event_loop)
|
|
379
515
|
|
|
380
516
|
|
|
381
517
|
def determine_response_topic() -> str | None:
|
|
@@ -412,76 +548,3 @@ def determine_request_topic() -> str | None:
|
|
|
412
548
|
|
|
413
549
|
# Default to generic request topic
|
|
414
550
|
return "request"
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
async def subscribe_to_topic(topic: str) -> None:
|
|
418
|
-
"""Subscribe to a specific topic in the message bus."""
|
|
419
|
-
ws_connection = ws_connection_context.get()
|
|
420
|
-
if ws_connection is None:
|
|
421
|
-
logger.warning("WebSocket connection not available for subscribing to topic")
|
|
422
|
-
return
|
|
423
|
-
|
|
424
|
-
try:
|
|
425
|
-
body = {"type": "subscribe", "topic": topic, "payload": {}}
|
|
426
|
-
await ws_connection.send(json.dumps(body))
|
|
427
|
-
logger.info("Subscribed to topic: %s", topic)
|
|
428
|
-
except WebSocketException as e:
|
|
429
|
-
logger.error("Error subscribing to topic %s: %s", topic, e)
|
|
430
|
-
except Exception as e:
|
|
431
|
-
logger.error("Unexpected error while subscribing to topic %s: %s", topic, e)
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
async def unsubscribe_from_topic(topic: str) -> None:
|
|
435
|
-
"""Unsubscribe from a specific topic in the message bus."""
|
|
436
|
-
ws_connection = ws_connection_context.get()
|
|
437
|
-
if ws_connection is None:
|
|
438
|
-
logger.warning("WebSocket connection not available for unsubscribing from topic")
|
|
439
|
-
return
|
|
440
|
-
|
|
441
|
-
try:
|
|
442
|
-
body = {"type": "unsubscribe", "topic": topic, "payload": {}}
|
|
443
|
-
await ws_connection.send(json.dumps(body))
|
|
444
|
-
logger.info("Unsubscribed from topic: %s", topic)
|
|
445
|
-
except WebSocketException as e:
|
|
446
|
-
logger.error("Error unsubscribing from topic %s: %s", topic, e)
|
|
447
|
-
except Exception as e:
|
|
448
|
-
logger.error("Unexpected error while unsubscribing from topic %s: %s", topic, e)
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
async def _process_api_event(event: dict) -> None:
|
|
452
|
-
"""Process API events and add to async queue."""
|
|
453
|
-
payload = event.get("payload", {})
|
|
454
|
-
|
|
455
|
-
try:
|
|
456
|
-
payload["request"]
|
|
457
|
-
except KeyError:
|
|
458
|
-
msg = "Error: 'request' was expected but not found."
|
|
459
|
-
raise RuntimeError(msg) from None
|
|
460
|
-
|
|
461
|
-
try:
|
|
462
|
-
event_type = payload["event_type"]
|
|
463
|
-
if event_type != "EventRequest":
|
|
464
|
-
msg = "Error: 'event_type' was found on request, but did not match 'EventRequest' as expected."
|
|
465
|
-
raise RuntimeError(msg) from None
|
|
466
|
-
except KeyError:
|
|
467
|
-
msg = "Error: 'event_type' not found in request."
|
|
468
|
-
raise RuntimeError(msg) from None
|
|
469
|
-
|
|
470
|
-
# Now attempt to convert it into an EventRequest.
|
|
471
|
-
try:
|
|
472
|
-
request_event = deserialize_event(json_data=payload)
|
|
473
|
-
except Exception as e:
|
|
474
|
-
msg = f"Unable to convert request JSON into a valid EventRequest object. Error Message: '{e}'"
|
|
475
|
-
raise RuntimeError(msg) from None
|
|
476
|
-
|
|
477
|
-
if not isinstance(request_event, EventRequest):
|
|
478
|
-
msg = f"Deserialized event is not an EventRequest: {type(request_event)}"
|
|
479
|
-
raise TypeError(msg)
|
|
480
|
-
|
|
481
|
-
# Check if the event implements SkipTheLineMixin for priority processing
|
|
482
|
-
if isinstance(request_event.request, SkipTheLineMixin):
|
|
483
|
-
# Handle the event immediately without queuing
|
|
484
|
-
await _process_event_request(request_event)
|
|
485
|
-
else:
|
|
486
|
-
# Add the event to the async queue for normal processing
|
|
487
|
-
await griptape_nodes.EventManager().aput_event(request_event)
|
|
@@ -69,7 +69,7 @@ mcp_server_logger.addHandler(RichHandler(show_time=True, show_path=False, markup
|
|
|
69
69
|
mcp_server_logger.setLevel(logging.INFO)
|
|
70
70
|
|
|
71
71
|
|
|
72
|
-
def
|
|
72
|
+
def start_mcp_server(api_key: str) -> None:
|
|
73
73
|
"""Synchronous version of main entry point for the Griptape Nodes MCP server."""
|
|
74
74
|
mcp_server_logger.debug("Starting MCP GTN server...")
|
|
75
75
|
# Give these a session ID
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
griptape_nodes/__init__.py,sha256=gKRELLnKTgmejKncdssrWFz7k0Aj_Fx7TatcEJB9H-A,37424
|
|
2
2
|
griptape_nodes/app/.python-version,sha256=e1X45ntWI8S-8_ppEojalDfXnTq6FW3kjUgdsyrH0W0,5
|
|
3
3
|
griptape_nodes/app/__init__.py,sha256=DB-DTsgcNnbmEClXEouwzGhrmo3gHBCWXB9BkPGpdQI,90
|
|
4
|
-
griptape_nodes/app/api.py,sha256=
|
|
5
|
-
griptape_nodes/app/app.py,sha256=
|
|
4
|
+
griptape_nodes/app/api.py,sha256=5IQgyXtcJeppwBpGmLdAQCOc5kh0KX2t1WOqkpo8P30,6579
|
|
5
|
+
griptape_nodes/app/app.py,sha256=OoRVe4QR5DTOVPywZF_ffLsGWt7cBR33VviFGSY_cmA,20727
|
|
6
6
|
griptape_nodes/app/watch.py,sha256=WE3P0Hl_jDezqy_UetNK2K5NTQFYKcoEbcKwzRWY4MU,2648
|
|
7
7
|
griptape_nodes/bootstrap/__init__.py,sha256=ENv3SIzQ9TtlRrg1y4e4CnoBpJaFpFSkNpTFBV8X5Ls,25
|
|
8
8
|
griptape_nodes/bootstrap/workflow_executors/__init__.py,sha256=pyjN81-eLtjyECFYLXOtMCixiiI9qBi5yald86iM7Ek,34
|
|
@@ -25,7 +25,7 @@ griptape_nodes/machines/control_flow.py,sha256=x-GlCjfMp-B1BvElcIOxOHtXADx1eK1JN
|
|
|
25
25
|
griptape_nodes/machines/fsm.py,sha256=JXf4VgaLMEcjDuCxuKyWlAmK5PCOrWHsMau6OPjhL3s,2344
|
|
26
26
|
griptape_nodes/machines/node_resolution.py,sha256=ruvSI5_q6S1O5kNypavxT-feduQpVEvRKrF5ZTPJdL8,18458
|
|
27
27
|
griptape_nodes/mcp_server/__init__.py,sha256=GSpJWqE4lICaryhsQR1okeMH2x6j1bBL0HVxtr52WLg,42
|
|
28
|
-
griptape_nodes/mcp_server/server.py,sha256=
|
|
28
|
+
griptape_nodes/mcp_server/server.py,sha256=X1ulGyHqQCFiRnJ8PgJh4lf83k62a_D9vFQThgKUaMw,5217
|
|
29
29
|
griptape_nodes/mcp_server/ws_request_manager.py,sha256=UpXOKNq_VauQmJ0Rx0C7OrzcwjeCoDfjpx3IE38NgsE,10949
|
|
30
30
|
griptape_nodes/node_library/__init__.py,sha256=U3FcSdmq6UW7qt6E3Up3NWKvUEn5_5lqL-u5qbzfxMQ,28
|
|
31
31
|
griptape_nodes/node_library/advanced_node_library.py,sha256=B1ZaxuFIzQ6tx_3MLIxlsHuahthEC1Hw_t6K_ByIdzs,2104
|
|
@@ -124,7 +124,7 @@ griptape_nodes/version_compatibility/versions/v0_39_0/modified_parameters_set_re
|
|
|
124
124
|
griptape_nodes/version_compatibility/workflow_versions/__init__.py,sha256=z5XDgkizoNByCXpyo34hfsJKFsWlOHbD6hgzfYH9ubc,52
|
|
125
125
|
griptape_nodes/version_compatibility/workflow_versions/v0_7_0/__init__.py,sha256=IzPPmGK86h2swfGGTOHyVcBIlOng6SjgWQzlbf3ngmo,51
|
|
126
126
|
griptape_nodes/version_compatibility/workflow_versions/v0_7_0/local_executor_argument_addition.py,sha256=9PclAp_Mm5IPtd5yj5XSS5-x7QYmifvhTly20CgBZmo,2018
|
|
127
|
-
griptape_nodes-0.52.
|
|
128
|
-
griptape_nodes-0.52.
|
|
129
|
-
griptape_nodes-0.52.
|
|
130
|
-
griptape_nodes-0.52.
|
|
127
|
+
griptape_nodes-0.52.1.dist-info/WHEEL,sha256=Jb20R3Ili4n9P1fcwuLup21eQ5r9WXhs4_qy7VTrgPI,79
|
|
128
|
+
griptape_nodes-0.52.1.dist-info/entry_points.txt,sha256=qvevqd3BVbAV5TcantnAm0ouqaqYKhsRO3pkFymWLWM,82
|
|
129
|
+
griptape_nodes-0.52.1.dist-info/METADATA,sha256=SvH75XQLx75G84IO6Sgb2Q8yq8PRzFKPeJ3dt5uaB3A,4943
|
|
130
|
+
griptape_nodes-0.52.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|