griptape-nodes 0.52.0__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.
- griptape_nodes/__init__.py +6 -943
- griptape_nodes/__main__.py +6 -0
- griptape_nodes/app/api.py +1 -12
- griptape_nodes/app/app.py +256 -209
- griptape_nodes/cli/__init__.py +1 -0
- griptape_nodes/cli/commands/__init__.py +1 -0
- griptape_nodes/cli/commands/config.py +71 -0
- griptape_nodes/cli/commands/engine.py +80 -0
- griptape_nodes/cli/commands/init.py +548 -0
- griptape_nodes/cli/commands/libraries.py +90 -0
- griptape_nodes/cli/commands/self.py +117 -0
- griptape_nodes/cli/main.py +46 -0
- griptape_nodes/cli/shared.py +84 -0
- griptape_nodes/common/__init__.py +1 -0
- griptape_nodes/common/directed_graph.py +55 -0
- griptape_nodes/drivers/storage/local_storage_driver.py +7 -2
- griptape_nodes/exe_types/core_types.py +60 -2
- griptape_nodes/exe_types/node_types.py +38 -24
- griptape_nodes/machines/control_flow.py +86 -22
- griptape_nodes/machines/fsm.py +10 -1
- griptape_nodes/machines/parallel_resolution.py +570 -0
- griptape_nodes/machines/{node_resolution.py → sequential_resolution.py} +22 -51
- griptape_nodes/mcp_server/server.py +1 -1
- griptape_nodes/retained_mode/events/base_events.py +2 -2
- griptape_nodes/retained_mode/events/node_events.py +4 -3
- griptape_nodes/retained_mode/griptape_nodes.py +25 -12
- griptape_nodes/retained_mode/managers/agent_manager.py +9 -5
- griptape_nodes/retained_mode/managers/arbitrary_code_exec_manager.py +3 -1
- griptape_nodes/retained_mode/managers/context_manager.py +6 -5
- griptape_nodes/retained_mode/managers/flow_manager.py +117 -204
- griptape_nodes/retained_mode/managers/library_lifecycle/library_directory.py +1 -1
- griptape_nodes/retained_mode/managers/library_manager.py +35 -25
- griptape_nodes/retained_mode/managers/node_manager.py +81 -199
- griptape_nodes/retained_mode/managers/object_manager.py +11 -5
- griptape_nodes/retained_mode/managers/os_manager.py +24 -9
- griptape_nodes/retained_mode/managers/secrets_manager.py +8 -4
- griptape_nodes/retained_mode/managers/settings.py +32 -1
- griptape_nodes/retained_mode/managers/static_files_manager.py +8 -3
- griptape_nodes/retained_mode/managers/sync_manager.py +8 -5
- griptape_nodes/retained_mode/managers/workflow_manager.py +110 -122
- griptape_nodes/traits/add_param_button.py +1 -1
- griptape_nodes/traits/button.py +216 -6
- griptape_nodes/traits/color_picker.py +66 -0
- griptape_nodes/traits/traits.json +4 -0
- {griptape_nodes-0.52.0.dist-info → griptape_nodes-0.53.0.dist-info}/METADATA +2 -1
- {griptape_nodes-0.52.0.dist-info → griptape_nodes-0.53.0.dist-info}/RECORD +48 -34
- {griptape_nodes-0.52.0.dist-info → griptape_nodes-0.53.0.dist-info}/WHEEL +0 -0
- {griptape_nodes-0.52.0.dist-info → griptape_nodes-0.53.0.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
|
|
@@ -16,8 +16,10 @@ 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
|
+
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,18 +38,29 @@ 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."""
|
|
44
46
|
|
|
47
|
+
event_type: str
|
|
48
|
+
payload: str
|
|
49
|
+
topic: str | None = None
|
|
45
50
|
|
|
46
|
-
# Whether to enable the static server
|
|
47
|
-
STATIC_SERVER_ENABLED = os.getenv("STATIC_SERVER_ENABLED", "true").lower() == "true"
|
|
48
51
|
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
51
64
|
|
|
52
65
|
|
|
53
66
|
# Important to bootstrap singleton here so that we don't
|
|
@@ -55,6 +68,24 @@ REQUEST_SEMAPHORE = asyncio.Semaphore(100)
|
|
|
55
68
|
# initializing it from a log during it's own initialization.
|
|
56
69
|
griptape_nodes: GriptapeNodes = GriptapeNodes()
|
|
57
70
|
|
|
71
|
+
# WebSocket outgoing queue for messages and commands.
|
|
72
|
+
# Appears to be fine to create outside event loop
|
|
73
|
+
# https://discuss.python.org/t/can-asyncio-queue-be-safely-created-outside-of-the-event-loop-thread/49215/8
|
|
74
|
+
ws_outgoing_queue: asyncio.Queue = asyncio.Queue()
|
|
75
|
+
|
|
76
|
+
# Background WebSocket event loop reference for cross-thread communication
|
|
77
|
+
websocket_event_loop: asyncio.AbstractEventLoop | None = None
|
|
78
|
+
|
|
79
|
+
# Threading event to signal when websocket_event_loop is ready
|
|
80
|
+
websocket_event_loop_ready = threading.Event()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# Whether to enable the static server
|
|
84
|
+
STATIC_SERVER_ENABLED = os.getenv("STATIC_SERVER_ENABLED", "true").lower() == "true"
|
|
85
|
+
|
|
86
|
+
# Semaphore to limit concurrent requests
|
|
87
|
+
REQUEST_SEMAPHORE = asyncio.Semaphore(100)
|
|
88
|
+
|
|
58
89
|
|
|
59
90
|
class EventLogHandler(logging.Handler):
|
|
60
91
|
"""Custom logging handler that emits log messages as AppEvents.
|
|
@@ -73,10 +104,12 @@ class EventLogHandler(logging.Handler):
|
|
|
73
104
|
logger = logging.getLogger("griptape_nodes_app")
|
|
74
105
|
|
|
75
106
|
griptape_nodes_logger = logging.getLogger("griptape_nodes")
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
+
)
|
|
80
113
|
|
|
81
114
|
console = Console()
|
|
82
115
|
|
|
@@ -95,62 +128,89 @@ async def astart_app() -> None:
|
|
|
95
128
|
"""New async app entry point."""
|
|
96
129
|
api_key = _ensure_api_key()
|
|
97
130
|
|
|
131
|
+
# Initialize event queue in main thread
|
|
98
132
|
griptape_nodes.EventManager().initialize_queue()
|
|
99
133
|
|
|
100
|
-
#
|
|
101
|
-
|
|
134
|
+
# Get main loop reference
|
|
135
|
+
main_loop = asyncio.get_running_loop()
|
|
102
136
|
|
|
103
137
|
try:
|
|
104
|
-
#
|
|
105
|
-
|
|
106
|
-
server_tasks = []
|
|
107
|
-
|
|
108
|
-
# Start MCP server in thread
|
|
109
|
-
server_tasks.append(asyncio.to_thread(_run_mcp_server_sync, api_key))
|
|
138
|
+
# Start MCP server in daemon thread
|
|
139
|
+
threading.Thread(target=start_mcp_server, args=(api_key,), daemon=True, name="mcp-server").start()
|
|
110
140
|
|
|
111
|
-
# Start static server in thread if enabled
|
|
141
|
+
# Start static server in daemon thread if enabled
|
|
112
142
|
if STATIC_SERVER_ENABLED:
|
|
113
143
|
static_dir = _build_static_dir()
|
|
114
|
-
|
|
144
|
+
threading.Thread(target=start_static_server, args=(static_dir,), daemon=True, name="static-server").start()
|
|
115
145
|
|
|
116
|
-
#
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
]
|
|
146
|
+
# Start WebSocket tasks in daemon thread
|
|
147
|
+
threading.Thread(
|
|
148
|
+
target=_start_websocket_connection, args=(api_key, main_loop), daemon=True, name="websocket-tasks"
|
|
149
|
+
).start()
|
|
121
150
|
|
|
122
|
-
#
|
|
123
|
-
|
|
151
|
+
# Run event processing on main thread
|
|
152
|
+
await _process_event_queue()
|
|
124
153
|
|
|
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
154
|
except Exception as e:
|
|
130
155
|
logger.error("Application startup failed: %s", e)
|
|
131
156
|
raise
|
|
132
157
|
|
|
133
158
|
|
|
134
|
-
def
|
|
135
|
-
"""Run
|
|
159
|
+
def _start_websocket_connection(api_key: str, main_loop: asyncio.AbstractEventLoop) -> None:
|
|
160
|
+
"""Run WebSocket tasks in a separate thread with its own async loop."""
|
|
161
|
+
global websocket_event_loop # noqa: PLW0603
|
|
136
162
|
try:
|
|
137
|
-
|
|
163
|
+
# Create a new event loop for this thread
|
|
164
|
+
loop = asyncio.new_event_loop()
|
|
165
|
+
websocket_event_loop = loop
|
|
166
|
+
asyncio.set_event_loop(loop)
|
|
167
|
+
|
|
168
|
+
# Signal that websocket_event_loop is ready
|
|
169
|
+
websocket_event_loop_ready.set()
|
|
138
170
|
|
|
139
|
-
|
|
171
|
+
# Run the async WebSocket tasks
|
|
172
|
+
loop.run_until_complete(_run_websocket_tasks(api_key, main_loop))
|
|
140
173
|
except Exception as e:
|
|
141
|
-
logger.error("
|
|
174
|
+
logger.error("WebSocket thread error: %s", e)
|
|
142
175
|
raise
|
|
176
|
+
finally:
|
|
177
|
+
websocket_event_loop = None
|
|
178
|
+
websocket_event_loop_ready.clear()
|
|
143
179
|
|
|
144
180
|
|
|
145
|
-
def
|
|
146
|
-
"""Run
|
|
147
|
-
|
|
148
|
-
|
|
181
|
+
async def _run_websocket_tasks(api_key: str, main_loop: asyncio.AbstractEventLoop) -> None:
|
|
182
|
+
"""Run WebSocket tasks - async version."""
|
|
183
|
+
# Create WebSocket connection for this thread
|
|
184
|
+
connection_stream = _create_websocket_connection(api_key)
|
|
149
185
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
186
|
+
# Track if this is the first connection
|
|
187
|
+
initialized = False
|
|
188
|
+
|
|
189
|
+
async for ws_connection in connection_stream:
|
|
190
|
+
logger.info("WebSocket connection established")
|
|
191
|
+
try:
|
|
192
|
+
# Emit initialization event only for the first connection
|
|
193
|
+
if not initialized:
|
|
194
|
+
griptape_nodes.EventManager().put_event_threadsafe(
|
|
195
|
+
main_loop, AppEvent(payload=app_events.AppInitializationComplete())
|
|
196
|
+
)
|
|
197
|
+
initialized = True
|
|
198
|
+
|
|
199
|
+
# Emit connection established event for every connection
|
|
200
|
+
griptape_nodes.EventManager().put_event_threadsafe(
|
|
201
|
+
main_loop, AppEvent(payload=app_events.AppConnectionEstablished())
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
async with asyncio.TaskGroup() as tg:
|
|
205
|
+
tg.create_task(_process_incoming_messages(ws_connection, main_loop))
|
|
206
|
+
tg.create_task(_send_outgoing_messages(ws_connection))
|
|
207
|
+
except (ExceptionGroup, ConnectionClosed, ConnectionClosedError):
|
|
208
|
+
logger.info("WebSocket connection closed, reconnecting...")
|
|
209
|
+
continue
|
|
210
|
+
except Exception:
|
|
211
|
+
logger.exception("WebSocket tasks failed")
|
|
212
|
+
await asyncio.sleep(2.0) # Wait before retry
|
|
213
|
+
continue
|
|
154
214
|
|
|
155
215
|
|
|
156
216
|
def _ensure_api_key() -> str:
|
|
@@ -179,70 +239,134 @@ def _build_static_dir() -> Path:
|
|
|
179
239
|
return Path(config_manager.workspace_path) / config_manager.merged_config["static_files_directory"]
|
|
180
240
|
|
|
181
241
|
|
|
182
|
-
async def
|
|
183
|
-
"""
|
|
184
|
-
logger.
|
|
242
|
+
async def _process_incoming_messages(ws_connection: Any, main_loop: asyncio.AbstractEventLoop) -> None:
|
|
243
|
+
"""Process incoming WebSocket requests from Nodes API."""
|
|
244
|
+
logger.debug("Processing incoming WebSocket requests from WebSocket connection")
|
|
185
245
|
|
|
186
|
-
|
|
187
|
-
|
|
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.")
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _create_websocket_connection(api_key: str) -> Any:
|
|
255
|
+
"""Create an async WebSocket connection to the Nodes API."""
|
|
256
|
+
endpoint = urljoin(
|
|
257
|
+
os.getenv("GRIPTAPE_NODES_API_BASE_URL", "https://api.nodes.griptape.ai").replace("http", "ws"),
|
|
258
|
+
"/ws/engines/events?version=v2",
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
return connect(
|
|
262
|
+
endpoint,
|
|
263
|
+
additional_headers={"Authorization": f"Bearer {api_key}"},
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
async def _process_api_event(event: dict, main_loop: asyncio.AbstractEventLoop) -> None:
|
|
268
|
+
"""Process API events and add to async queue."""
|
|
269
|
+
payload = event.get("payload", {})
|
|
188
270
|
|
|
189
271
|
try:
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
272
|
+
payload["request"]
|
|
273
|
+
except KeyError:
|
|
274
|
+
msg = "Error: 'request' was expected but not found."
|
|
275
|
+
raise RuntimeError(msg) from None
|
|
193
276
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
277
|
+
try:
|
|
278
|
+
event_type = payload["event_type"]
|
|
279
|
+
if event_type != "EventRequest":
|
|
280
|
+
msg = "Error: 'event_type' was found on request, but did not match 'EventRequest' as expected."
|
|
281
|
+
raise RuntimeError(msg) from None
|
|
282
|
+
except KeyError:
|
|
283
|
+
msg = "Error: 'event_type' not found in request."
|
|
284
|
+
raise RuntimeError(msg) from None
|
|
285
|
+
|
|
286
|
+
# Now attempt to convert it into an EventRequest.
|
|
287
|
+
try:
|
|
288
|
+
request_event = deserialize_event(json_data=payload)
|
|
198
289
|
except Exception as e:
|
|
199
|
-
|
|
200
|
-
raise
|
|
201
|
-
finally:
|
|
202
|
-
await _cleanup_websocket_connection()
|
|
290
|
+
msg = f"Unable to convert request JSON into a valid EventRequest object. Error Message: '{e}'"
|
|
291
|
+
raise RuntimeError(msg) from None
|
|
203
292
|
|
|
293
|
+
if not isinstance(request_event, EventRequest):
|
|
294
|
+
msg = f"Deserialized event is not an EventRequest: {type(request_event)}"
|
|
295
|
+
raise TypeError(msg)
|
|
204
296
|
|
|
205
|
-
|
|
206
|
-
|
|
297
|
+
# Check if the event implements SkipTheLineMixin for priority processing
|
|
298
|
+
if isinstance(request_event.request, SkipTheLineMixin):
|
|
299
|
+
# Handle the event immediately without queuing
|
|
300
|
+
await _process_event_request(request_event)
|
|
301
|
+
else:
|
|
302
|
+
# Add the event to the main thread event queue for processing
|
|
303
|
+
griptape_nodes.EventManager().put_event_threadsafe(main_loop, request_event)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
async def _send_outgoing_messages(ws_connection: Any) -> None:
|
|
307
|
+
"""Send outgoing WebSocket requests from queue on background thread."""
|
|
308
|
+
logger.debug("Starting outgoing WebSocket request sender")
|
|
309
|
+
|
|
310
|
+
while True:
|
|
311
|
+
# Get message from outgoing queue
|
|
312
|
+
message = await ws_outgoing_queue.get()
|
|
313
|
+
|
|
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()
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
async def _send_websocket_message(ws_connection: Any, event_type: str, payload: str, topic: str | None) -> None:
|
|
330
|
+
"""Send a message via WebSocket."""
|
|
207
331
|
try:
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if not initialized:
|
|
212
|
-
await griptape_nodes.EventManager().aput_event(AppEvent(payload=app_events.AppInitializationComplete()))
|
|
332
|
+
if topic is None:
|
|
333
|
+
topic = determine_response_topic()
|
|
213
334
|
|
|
214
|
-
|
|
335
|
+
body = {"type": event_type, "payload": json.loads(payload), "topic": topic}
|
|
336
|
+
await ws_connection.send(json.dumps(body))
|
|
337
|
+
except WebSocketException as e:
|
|
338
|
+
logger.error("Error sending WebSocket message: %s", e)
|
|
339
|
+
except Exception as e:
|
|
340
|
+
logger.error("Unexpected error sending WebSocket message: %s", e)
|
|
215
341
|
|
|
216
|
-
async for message in ws_connection:
|
|
217
|
-
try:
|
|
218
|
-
data = json.loads(message)
|
|
219
|
-
await _process_api_event(data)
|
|
220
|
-
except Exception:
|
|
221
|
-
logger.exception("Error processing event, skipping.")
|
|
222
342
|
|
|
223
|
-
|
|
224
|
-
|
|
343
|
+
async def _send_subscribe_command(ws_connection: Any, topic: str) -> None:
|
|
344
|
+
"""Send subscribe command via WebSocket."""
|
|
345
|
+
try:
|
|
346
|
+
body = {"type": "subscribe", "topic": topic, "payload": {}}
|
|
347
|
+
await ws_connection.send(json.dumps(body))
|
|
348
|
+
logger.debug("Subscribed to topic: %s", topic)
|
|
349
|
+
except WebSocketException as e:
|
|
350
|
+
logger.error("Error subscribing to topic %s: %s", topic, e)
|
|
225
351
|
except Exception as e:
|
|
226
|
-
logger.error("
|
|
227
|
-
await asyncio.sleep(2.0)
|
|
228
|
-
finally:
|
|
229
|
-
ws_connection_context.set(None)
|
|
230
|
-
ws_ready_event.clear()
|
|
352
|
+
logger.error("Unexpected error subscribing to topic %s: %s", topic, e)
|
|
231
353
|
|
|
232
354
|
|
|
233
|
-
async def
|
|
234
|
-
"""
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
355
|
+
async def _send_unsubscribe_command(ws_connection: Any, topic: str) -> None:
|
|
356
|
+
"""Send unsubscribe command via WebSocket."""
|
|
357
|
+
try:
|
|
358
|
+
body = {"type": "unsubscribe", "topic": topic, "payload": {}}
|
|
359
|
+
await ws_connection.send(json.dumps(body))
|
|
360
|
+
logger.debug("Unsubscribed from topic: %s", topic)
|
|
361
|
+
except WebSocketException as e:
|
|
362
|
+
logger.error("Error unsubscribing from topic %s: %s", topic, e)
|
|
363
|
+
except Exception as e:
|
|
364
|
+
logger.error("Unexpected error unsubscribing from topic %s: %s", topic, e)
|
|
240
365
|
|
|
241
366
|
|
|
242
367
|
async def _process_event_queue() -> None:
|
|
243
|
-
"""Process events concurrently -
|
|
244
|
-
|
|
245
|
-
await _await_websocket_ready()
|
|
368
|
+
"""Process events concurrently - runs on main thread."""
|
|
369
|
+
logger.info("Starting event queue processor on main thread")
|
|
246
370
|
background_tasks = set()
|
|
247
371
|
|
|
248
372
|
def _handle_task_result(task: asyncio.Task) -> None:
|
|
@@ -291,17 +415,7 @@ async def _process_event_request(event: EventRequest) -> None:
|
|
|
291
415
|
else:
|
|
292
416
|
dest_socket = "failure_result"
|
|
293
417
|
|
|
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
|
|
418
|
+
await _send_message(dest_socket, result_event.json(), topic=result_event.response_topic)
|
|
305
419
|
|
|
306
420
|
|
|
307
421
|
async def _process_app_event(event: AppEvent) -> None:
|
|
@@ -309,7 +423,7 @@ async def _process_app_event(event: AppEvent) -> None:
|
|
|
309
423
|
# Let Griptape Nodes broadcast it.
|
|
310
424
|
await griptape_nodes.broadcast_app_event(event.payload)
|
|
311
425
|
|
|
312
|
-
await
|
|
426
|
+
await _send_message("app_event", event.json())
|
|
313
427
|
|
|
314
428
|
|
|
315
429
|
async def _process_node_event(event: GriptapeNodeEvent) -> None:
|
|
@@ -324,12 +438,12 @@ async def _process_node_event(event: GriptapeNodeEvent) -> None:
|
|
|
324
438
|
msg = f"Unknown/unsupported result event type encountered: '{type(result_event)}'."
|
|
325
439
|
raise TypeError(msg) from None
|
|
326
440
|
|
|
327
|
-
await
|
|
441
|
+
await _send_message(dest_socket, result_event.json(), topic=result_event.response_topic)
|
|
328
442
|
|
|
329
443
|
|
|
330
444
|
async def _process_execution_node_event(event: ExecutionGriptapeNodeEvent) -> None:
|
|
331
445
|
"""Process ExecutionGriptapeNodeEvents and send them to the API (async version)."""
|
|
332
|
-
await
|
|
446
|
+
await _send_message("execution_event", event.wrapped_event.json())
|
|
333
447
|
|
|
334
448
|
|
|
335
449
|
async def _process_progress_event(gt_event: ProgressEvent) -> None:
|
|
@@ -341,41 +455,47 @@ async def _process_progress_event(gt_event: ProgressEvent) -> None:
|
|
|
341
455
|
node_name=node_name, parameter_name=gt_event.parameter_name, type=type(gt_event).__name__, value=value
|
|
342
456
|
)
|
|
343
457
|
event_to_emit = ExecutionEvent(payload=payload)
|
|
344
|
-
await
|
|
458
|
+
await _send_message("execution_event", event_to_emit.json())
|
|
345
459
|
|
|
346
460
|
|
|
347
|
-
def
|
|
348
|
-
"""
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
461
|
+
async def _send_message(event_type: str, payload: str, topic: str | None = None) -> None:
|
|
462
|
+
"""Queue a message to be sent via WebSocket using run_coroutine_threadsafe."""
|
|
463
|
+
# Wait for websocket event loop to be ready
|
|
464
|
+
websocket_event_loop_ready.wait()
|
|
465
|
+
|
|
466
|
+
# Use run_coroutine_threadsafe to put message into WebSocket background thread queue
|
|
467
|
+
if websocket_event_loop is None:
|
|
468
|
+
logger.error("WebSocket event loop not available for message")
|
|
469
|
+
return
|
|
470
|
+
|
|
471
|
+
# Determine topic based on session_id and engine_id in the payload
|
|
472
|
+
if topic is None:
|
|
473
|
+
topic = determine_response_topic()
|
|
474
|
+
|
|
475
|
+
message = WebSocketMessage(event_type, payload, topic)
|
|
476
|
+
|
|
477
|
+
asyncio.run_coroutine_threadsafe(ws_outgoing_queue.put(message), websocket_event_loop)
|
|
353
478
|
|
|
354
|
-
return connect(
|
|
355
|
-
endpoint,
|
|
356
|
-
additional_headers={"Authorization": f"Bearer {api_key}"},
|
|
357
|
-
)
|
|
358
479
|
|
|
480
|
+
async def subscribe_to_topic(topic: str) -> None:
|
|
481
|
+
"""Queue a subscribe command for WebSocket using run_coroutine_threadsafe."""
|
|
482
|
+
# Wait for websocket event loop to be ready
|
|
483
|
+
websocket_event_loop_ready.wait()
|
|
359
484
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
ws_connection = ws_connection_context.get()
|
|
363
|
-
if ws_connection is None:
|
|
364
|
-
logger.warning("WebSocket connection not available for sending message")
|
|
485
|
+
if websocket_event_loop is None:
|
|
486
|
+
logger.error("WebSocket event loop not available for subscribe")
|
|
365
487
|
return
|
|
366
488
|
|
|
367
|
-
|
|
368
|
-
# Determine topic based on session_id and engine_id in the payload
|
|
369
|
-
if topic is None:
|
|
370
|
-
topic = determine_response_topic()
|
|
489
|
+
asyncio.run_coroutine_threadsafe(ws_outgoing_queue.put(SubscribeCommand(topic)), websocket_event_loop)
|
|
371
490
|
|
|
372
|
-
body = {"type": event_type, "payload": json.loads(payload), "topic": topic}
|
|
373
491
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
492
|
+
async def unsubscribe_from_topic(topic: str) -> None:
|
|
493
|
+
"""Queue an unsubscribe command for WebSocket using run_coroutine_threadsafe."""
|
|
494
|
+
if websocket_event_loop is None:
|
|
495
|
+
logger.error("WebSocket event loop not available for unsubscribe")
|
|
496
|
+
return
|
|
497
|
+
|
|
498
|
+
asyncio.run_coroutine_threadsafe(ws_outgoing_queue.put(UnsubscribeCommand(topic)), websocket_event_loop)
|
|
379
499
|
|
|
380
500
|
|
|
381
501
|
def determine_response_topic() -> str | None:
|
|
@@ -412,76 +532,3 @@ def determine_request_topic() -> str | None:
|
|
|
412
532
|
|
|
413
533
|
# Default to generic request topic
|
|
414
534
|
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)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Griptape Nodes CLI module."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Griptape Nodes CLI commands."""
|