griptape-nodes 0.51.1__py3-none-any.whl → 0.52.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 +5 -4
- griptape_nodes/app/api.py +27 -24
- griptape_nodes/app/app.py +243 -221
- griptape_nodes/app/watch.py +17 -2
- griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +66 -103
- griptape_nodes/bootstrap/workflow_executors/workflow_executor.py +16 -4
- griptape_nodes/exe_types/core_types.py +16 -4
- griptape_nodes/exe_types/node_types.py +74 -16
- griptape_nodes/machines/control_flow.py +21 -26
- griptape_nodes/machines/fsm.py +16 -16
- griptape_nodes/machines/node_resolution.py +28 -119
- griptape_nodes/mcp_server/server.py +14 -10
- griptape_nodes/mcp_server/ws_request_manager.py +2 -2
- griptape_nodes/node_library/workflow_registry.py +5 -0
- griptape_nodes/retained_mode/events/base_events.py +12 -7
- griptape_nodes/retained_mode/events/execution_events.py +0 -6
- griptape_nodes/retained_mode/events/node_events.py +38 -0
- griptape_nodes/retained_mode/events/parameter_events.py +11 -0
- griptape_nodes/retained_mode/events/variable_events.py +361 -0
- griptape_nodes/retained_mode/events/workflow_events.py +35 -0
- griptape_nodes/retained_mode/griptape_nodes.py +61 -26
- griptape_nodes/retained_mode/managers/agent_manager.py +8 -9
- griptape_nodes/retained_mode/managers/event_manager.py +215 -74
- griptape_nodes/retained_mode/managers/flow_manager.py +39 -33
- griptape_nodes/retained_mode/managers/library_lifecycle/library_directory.py +14 -14
- griptape_nodes/retained_mode/managers/library_lifecycle/library_fsm.py +20 -20
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/base.py +1 -1
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/github.py +1 -1
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/local_file.py +4 -3
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/package.py +1 -1
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/sandbox.py +1 -1
- griptape_nodes/retained_mode/managers/library_manager.py +20 -19
- griptape_nodes/retained_mode/managers/node_manager.py +81 -8
- griptape_nodes/retained_mode/managers/object_manager.py +4 -0
- griptape_nodes/retained_mode/managers/settings.py +1 -0
- griptape_nodes/retained_mode/managers/sync_manager.py +3 -9
- griptape_nodes/retained_mode/managers/variable_manager.py +529 -0
- griptape_nodes/retained_mode/managers/workflow_manager.py +156 -50
- griptape_nodes/retained_mode/variable_types.py +18 -0
- griptape_nodes/utils/__init__.py +4 -0
- griptape_nodes/utils/async_utils.py +89 -0
- {griptape_nodes-0.51.1.dist-info → griptape_nodes-0.52.0.dist-info}/METADATA +2 -3
- {griptape_nodes-0.51.1.dist-info → griptape_nodes-0.52.0.dist-info}/RECORD +45 -42
- {griptape_nodes-0.51.1.dist-info → griptape_nodes-0.52.0.dist-info}/WHEEL +1 -1
- griptape_nodes/bootstrap/workflow_executors/subprocess_workflow_executor.py +0 -90
- {griptape_nodes-0.51.1.dist-info → griptape_nodes-0.52.0.dist-info}/entry_points.txt +0 -0
griptape_nodes/app/app.py
CHANGED
|
@@ -1,21 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import contextlib
|
|
5
|
+
import contextvars
|
|
4
6
|
import json
|
|
5
7
|
import logging
|
|
6
8
|
import os
|
|
7
|
-
import signal
|
|
8
9
|
import sys
|
|
9
|
-
import threading
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from
|
|
12
|
-
from typing import Any, cast
|
|
11
|
+
from typing import Any
|
|
13
12
|
from urllib.parse import urljoin
|
|
14
13
|
|
|
15
|
-
from griptape.events import (
|
|
16
|
-
EventBus,
|
|
17
|
-
EventListener,
|
|
18
|
-
)
|
|
19
14
|
from rich.align import Align
|
|
20
15
|
from rich.console import Console
|
|
21
16
|
from rich.logging import RichHandler
|
|
@@ -23,7 +18,6 @@ from rich.panel import Panel
|
|
|
23
18
|
from websockets.asyncio.client import connect
|
|
24
19
|
from websockets.exceptions import ConnectionClosed, WebSocketException
|
|
25
20
|
|
|
26
|
-
from griptape_nodes.mcp_server.server import main as mcp_server
|
|
27
21
|
from griptape_nodes.retained_mode.events import app_events, execution_events
|
|
28
22
|
|
|
29
23
|
# This import is necessary to register all events, even if not technically used
|
|
@@ -36,29 +30,31 @@ from griptape_nodes.retained_mode.events.base_events import (
|
|
|
36
30
|
ExecutionGriptapeNodeEvent,
|
|
37
31
|
GriptapeNodeEvent,
|
|
38
32
|
ProgressEvent,
|
|
39
|
-
RequestPayload,
|
|
40
33
|
SkipTheLineMixin,
|
|
41
34
|
deserialize_event,
|
|
42
35
|
)
|
|
43
36
|
from griptape_nodes.retained_mode.events.logger_events import LogHandlerEvent
|
|
44
37
|
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
45
38
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
# This is a global event queue that will be used to pass events between threads
|
|
49
|
-
event_queue = Queue()
|
|
50
|
-
|
|
51
|
-
# Global WebSocket connection for sending events
|
|
52
|
-
ws_connection_for_sending = None
|
|
53
|
-
event_loop = None
|
|
39
|
+
# Context variable for WebSocket connection - avoids global state
|
|
40
|
+
ws_connection_context: contextvars.ContextVar[Any | None] = contextvars.ContextVar("ws_connection", default=None)
|
|
54
41
|
|
|
55
42
|
# Event to signal when WebSocket connection is ready
|
|
56
|
-
ws_ready_event =
|
|
43
|
+
ws_ready_event = asyncio.Event()
|
|
57
44
|
|
|
58
45
|
|
|
59
46
|
# Whether to enable the static server
|
|
60
47
|
STATIC_SERVER_ENABLED = os.getenv("STATIC_SERVER_ENABLED", "true").lower() == "true"
|
|
61
48
|
|
|
49
|
+
# Semaphore to limit concurrent requests
|
|
50
|
+
REQUEST_SEMAPHORE = asyncio.Semaphore(100)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# Important to bootstrap singleton here so that we don't
|
|
54
|
+
# get any weird circular import issues from the EventLogHandler
|
|
55
|
+
# initializing it from a log during it's own initialization.
|
|
56
|
+
griptape_nodes: GriptapeNodes = GriptapeNodes()
|
|
57
|
+
|
|
62
58
|
|
|
63
59
|
class EventLogHandler(logging.Handler):
|
|
64
60
|
"""Custom logging handler that emits log messages as AppEvents.
|
|
@@ -67,11 +63,10 @@ class EventLogHandler(logging.Handler):
|
|
|
67
63
|
"""
|
|
68
64
|
|
|
69
65
|
def emit(self, record: logging.LogRecord) -> None:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
payload=LogHandlerEvent(message=record.getMessage(), levelname=record.levelname, created=record.created)
|
|
73
|
-
)
|
|
66
|
+
log_event = AppEvent(
|
|
67
|
+
payload=LogHandlerEvent(message=record.getMessage(), levelname=record.levelname, created=record.created)
|
|
74
68
|
)
|
|
69
|
+
griptape_nodes.EventManager().put_event(log_event)
|
|
75
70
|
|
|
76
71
|
|
|
77
72
|
# Logger for this module. Important that this is not the same as the griptape_nodes logger or else we'll have infinite log events.
|
|
@@ -87,26 +82,79 @@ console = Console()
|
|
|
87
82
|
|
|
88
83
|
|
|
89
84
|
def start_app() -> None:
|
|
90
|
-
"""
|
|
85
|
+
"""Legacy sync entry point - runs async app."""
|
|
86
|
+
try:
|
|
87
|
+
asyncio.run(astart_app())
|
|
88
|
+
except KeyboardInterrupt:
|
|
89
|
+
logger.info("Application stopped by user")
|
|
90
|
+
except Exception as e:
|
|
91
|
+
logger.error("Application error: %s", e)
|
|
91
92
|
|
|
92
|
-
Starts the event loop and listens for events from the Nodes API.
|
|
93
|
-
"""
|
|
94
|
-
_init_event_listeners()
|
|
95
|
-
# Listen for any signals to exit the app
|
|
96
|
-
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
97
|
-
signal.signal(sig, lambda *_: sys.exit(0))
|
|
98
93
|
|
|
94
|
+
async def astart_app() -> None:
|
|
95
|
+
"""New async app entry point."""
|
|
99
96
|
api_key = _ensure_api_key()
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
97
|
+
|
|
98
|
+
griptape_nodes.EventManager().initialize_queue()
|
|
99
|
+
|
|
100
|
+
# Create shared context for all tasks to inherit WebSocket connection
|
|
101
|
+
shared_context = contextvars.copy_context()
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
# We need to run the servers in a separate thread otherwise
|
|
105
|
+
# blocking requests to them in the main thread would deadlock the event loop.
|
|
106
|
+
server_tasks = []
|
|
107
|
+
|
|
108
|
+
# Start MCP server in thread
|
|
109
|
+
server_tasks.append(asyncio.to_thread(_run_mcp_server_sync, api_key))
|
|
110
|
+
|
|
111
|
+
# Start static server in thread if enabled
|
|
112
|
+
if STATIC_SERVER_ENABLED:
|
|
113
|
+
static_dir = _build_static_dir()
|
|
114
|
+
server_tasks.append(asyncio.to_thread(_run_static_server_sync, static_dir))
|
|
115
|
+
|
|
116
|
+
# Run main event loop tasks
|
|
117
|
+
main_tasks = [
|
|
118
|
+
_listen_for_api_requests(api_key),
|
|
119
|
+
_process_event_queue(),
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
# Combine server tasks and main tasks
|
|
123
|
+
all_tasks = server_tasks + main_tasks
|
|
124
|
+
|
|
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
|
+
except Exception as e:
|
|
130
|
+
logger.error("Application startup failed: %s", e)
|
|
131
|
+
raise
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _run_mcp_server_sync(api_key: str) -> None:
|
|
135
|
+
"""Run MCP server in a separate thread."""
|
|
136
|
+
try:
|
|
137
|
+
from griptape_nodes.mcp_server.server import main_sync
|
|
138
|
+
|
|
139
|
+
main_sync(api_key)
|
|
140
|
+
except Exception as e:
|
|
141
|
+
logger.error("MCP server thread error: %s", e)
|
|
142
|
+
raise
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _run_static_server_sync(static_dir: Path) -> None:
|
|
146
|
+
"""Run static server in a separate thread."""
|
|
147
|
+
try:
|
|
148
|
+
from .api import start_api
|
|
149
|
+
|
|
150
|
+
start_api(static_dir)
|
|
151
|
+
except Exception as e:
|
|
152
|
+
logger.error("Static server thread error: %s", e)
|
|
153
|
+
raise
|
|
106
154
|
|
|
107
155
|
|
|
108
156
|
def _ensure_api_key() -> str:
|
|
109
|
-
secrets_manager =
|
|
157
|
+
secrets_manager = griptape_nodes.SecretsManager()
|
|
110
158
|
api_key = secrets_manager.get_secret("GT_CLOUD_API_KEY")
|
|
111
159
|
if api_key is None:
|
|
112
160
|
message = Panel(
|
|
@@ -127,79 +175,145 @@ def _ensure_api_key() -> str:
|
|
|
127
175
|
|
|
128
176
|
def _build_static_dir() -> Path:
|
|
129
177
|
"""Build the static directory path based on the workspace configuration."""
|
|
130
|
-
config_manager =
|
|
178
|
+
config_manager = griptape_nodes.ConfigManager()
|
|
131
179
|
return Path(config_manager.workspace_path) / config_manager.merged_config["static_files_directory"]
|
|
132
180
|
|
|
133
181
|
|
|
134
|
-
def
|
|
135
|
-
"""
|
|
136
|
-
|
|
137
|
-
event_listener=EventListener(on_event=__process_node_event, event_types=[GriptapeNodeEvent])
|
|
138
|
-
)
|
|
182
|
+
async def _listen_for_api_requests(api_key: str) -> None:
|
|
183
|
+
"""Listen for events and add to async queue."""
|
|
184
|
+
logger.info("Listening for events from Nodes API via async WebSocket")
|
|
139
185
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
on_event=__process_execution_node_event,
|
|
143
|
-
event_types=[ExecutionGriptapeNodeEvent],
|
|
144
|
-
)
|
|
145
|
-
)
|
|
186
|
+
connection_stream = _create_websocket_connection(api_key)
|
|
187
|
+
initialized = False
|
|
146
188
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
189
|
+
try:
|
|
190
|
+
async for ws_connection in connection_stream:
|
|
191
|
+
await _handle_websocket_connection(ws_connection, initialized=initialized)
|
|
192
|
+
initialized = True
|
|
193
|
+
|
|
194
|
+
except asyncio.CancelledError:
|
|
195
|
+
# Clean shutdown when task is cancelled
|
|
196
|
+
logger.info("WebSocket listener shutdown complete")
|
|
197
|
+
raise
|
|
198
|
+
except Exception as e:
|
|
199
|
+
logger.error("Fatal error in WebSocket listener: %s", e)
|
|
200
|
+
raise
|
|
201
|
+
finally:
|
|
202
|
+
await _cleanup_websocket_connection()
|
|
153
203
|
|
|
154
|
-
EventBus.add_event_listener(
|
|
155
|
-
event_listener=EventListener(
|
|
156
|
-
on_event=__process_app_event, # pyright: ignore[reportArgumentType] TODO: https://github.com/griptape-ai/griptape-nodes/issues/868
|
|
157
|
-
event_types=[AppEvent], # pyright: ignore[reportArgumentType] TODO: https://github.com/griptape-ai/griptape-nodes/issues/868
|
|
158
|
-
)
|
|
159
|
-
)
|
|
160
204
|
|
|
205
|
+
async def _handle_websocket_connection(ws_connection: Any, *, initialized: bool) -> None:
|
|
206
|
+
"""Handle a single WebSocket connection."""
|
|
207
|
+
try:
|
|
208
|
+
ws_connection_context.set(ws_connection)
|
|
209
|
+
ws_ready_event.set()
|
|
161
210
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
global ws_connection_for_sending, event_loop # noqa: PLW0603
|
|
165
|
-
event_loop = asyncio.get_running_loop() # Store the event loop reference
|
|
166
|
-
logger.info("Listening for events from Nodes API via async WebSocket")
|
|
211
|
+
if not initialized:
|
|
212
|
+
await griptape_nodes.EventManager().aput_event(AppEvent(payload=app_events.AppInitializationComplete()))
|
|
167
213
|
|
|
168
|
-
|
|
169
|
-
connection_stream = _create_websocket_connection(api_key)
|
|
170
|
-
initialized = False
|
|
171
|
-
async for ws_connection in connection_stream:
|
|
172
|
-
try:
|
|
173
|
-
ws_connection_for_sending = ws_connection # Store for sending events
|
|
174
|
-
ws_ready_event.set() # Signal that WebSocket is ready for sending
|
|
214
|
+
await griptape_nodes.EventManager().aput_event(AppEvent(payload=app_events.AppConnectionEstablished()))
|
|
175
215
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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.")
|
|
179
222
|
|
|
180
|
-
|
|
223
|
+
except ConnectionClosed:
|
|
224
|
+
logger.info("WebSocket connection closed, will retry")
|
|
225
|
+
except Exception as e:
|
|
226
|
+
logger.error("Error in WebSocket connection. Retrying in 2 seconds... %s", e)
|
|
227
|
+
await asyncio.sleep(2.0)
|
|
228
|
+
finally:
|
|
229
|
+
ws_connection_context.set(None)
|
|
230
|
+
ws_ready_event.clear()
|
|
181
231
|
|
|
182
|
-
async for message in ws_connection:
|
|
183
|
-
try:
|
|
184
|
-
data = json.loads(message)
|
|
185
232
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
await asyncio.sleep(2)
|
|
233
|
+
async def _cleanup_websocket_connection() -> None:
|
|
234
|
+
"""Clean up WebSocket connection on shutdown."""
|
|
235
|
+
ws_connection = ws_connection_context.get()
|
|
236
|
+
if ws_connection:
|
|
237
|
+
with contextlib.suppress(Exception):
|
|
238
|
+
await ws_connection.close()
|
|
239
|
+
logger.info("WebSocket listener shutdown complete")
|
|
194
240
|
|
|
195
241
|
|
|
196
|
-
def
|
|
197
|
-
"""
|
|
198
|
-
|
|
242
|
+
async def _process_event_queue() -> None:
|
|
243
|
+
"""Process events concurrently - all events can run simultaneously."""
|
|
244
|
+
# Wait for WebSocket connection (convert to async)
|
|
245
|
+
await _await_websocket_ready()
|
|
246
|
+
background_tasks = set()
|
|
199
247
|
|
|
248
|
+
def _handle_task_result(task: asyncio.Task) -> None:
|
|
249
|
+
background_tasks.discard(task)
|
|
250
|
+
if task.exception() and not task.cancelled():
|
|
251
|
+
logger.exception("Background task failed", exc_info=task.exception())
|
|
200
252
|
|
|
201
|
-
|
|
202
|
-
|
|
253
|
+
try:
|
|
254
|
+
event_queue = griptape_nodes.EventManager().event_queue
|
|
255
|
+
while True:
|
|
256
|
+
event = await event_queue.get()
|
|
257
|
+
|
|
258
|
+
async with REQUEST_SEMAPHORE:
|
|
259
|
+
if isinstance(event, EventRequest):
|
|
260
|
+
task = asyncio.create_task(_process_event_request(event))
|
|
261
|
+
elif isinstance(event, AppEvent):
|
|
262
|
+
task = asyncio.create_task(_process_app_event(event))
|
|
263
|
+
elif isinstance(event, GriptapeNodeEvent):
|
|
264
|
+
task = asyncio.create_task(_process_node_event(event))
|
|
265
|
+
elif isinstance(event, ExecutionGriptapeNodeEvent):
|
|
266
|
+
task = asyncio.create_task(_process_execution_node_event(event))
|
|
267
|
+
elif isinstance(event, ProgressEvent):
|
|
268
|
+
task = asyncio.create_task(_process_progress_event(event))
|
|
269
|
+
else:
|
|
270
|
+
logger.warning("Unknown event type: %s", type(event))
|
|
271
|
+
event_queue.task_done()
|
|
272
|
+
continue
|
|
273
|
+
|
|
274
|
+
background_tasks.add(task)
|
|
275
|
+
task.add_done_callback(_handle_task_result)
|
|
276
|
+
event_queue.task_done()
|
|
277
|
+
except asyncio.CancelledError:
|
|
278
|
+
logger.info("Event queue processor shutdown complete")
|
|
279
|
+
raise
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
async def _process_event_request(event: EventRequest) -> None:
|
|
283
|
+
"""Handle request and emit success/failure events based on result."""
|
|
284
|
+
result_event = await griptape_nodes.EventManager().ahandle_request(
|
|
285
|
+
event.request,
|
|
286
|
+
result_context={"response_topic": event.response_topic, "request_id": event.request_id},
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
if result_event.result.succeeded():
|
|
290
|
+
dest_socket = "success_result"
|
|
291
|
+
else:
|
|
292
|
+
dest_socket = "failure_result"
|
|
293
|
+
|
|
294
|
+
await __emit_message(dest_socket, result_event.json(), topic=result_event.response_topic)
|
|
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
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
async def _process_app_event(event: AppEvent) -> None:
|
|
308
|
+
"""Process AppEvents and send them to the API (async version)."""
|
|
309
|
+
# Let Griptape Nodes broadcast it.
|
|
310
|
+
await griptape_nodes.broadcast_app_event(event.payload)
|
|
311
|
+
|
|
312
|
+
await __emit_message("app_event", event.json())
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
async def _process_node_event(event: GriptapeNodeEvent) -> None:
|
|
316
|
+
"""Process GriptapeNodeEvents and send them to the API (async version)."""
|
|
203
317
|
# Emit the result back to the GUI
|
|
204
318
|
result_event = event.wrapped_event
|
|
205
319
|
if isinstance(result_event, EventResultSuccess):
|
|
@@ -210,32 +324,16 @@ def __process_node_event(event: GriptapeNodeEvent) -> None:
|
|
|
210
324
|
msg = f"Unknown/unsupported result event type encountered: '{type(result_event)}'."
|
|
211
325
|
raise TypeError(msg) from None
|
|
212
326
|
|
|
213
|
-
|
|
327
|
+
await __emit_message(dest_socket, result_event.json(), topic=result_event.response_topic)
|
|
214
328
|
|
|
215
329
|
|
|
216
|
-
def
|
|
217
|
-
"""Process ExecutionGriptapeNodeEvents and send them to the API."""
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
node_name = result_event.payload.node_name
|
|
224
|
-
logger.info("Resuming Node '%s'", node_name)
|
|
225
|
-
flow_name = GriptapeNodes.NodeManager().get_node_parent_flow_by_name(node_name)
|
|
226
|
-
request = EventRequest(request=execution_events.SingleExecutionStepRequest(flow_name=flow_name))
|
|
227
|
-
event_queue.put(request)
|
|
228
|
-
|
|
229
|
-
if type(result_event.payload).__name__ == "NodeFinishProcessEvent":
|
|
230
|
-
if result_event.payload.node_name != GriptapeNodes.EventManager().current_active_node:
|
|
231
|
-
msg = "Node start and finish do not match."
|
|
232
|
-
raise KeyError(msg) from None
|
|
233
|
-
GriptapeNodes.EventManager().current_active_node = None
|
|
234
|
-
__schedule_async_task(__emit_message("execution_event", result_event.json()))
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
def __process_progress_event(gt_event: ProgressEvent) -> None:
|
|
238
|
-
"""Process Griptape framework events and send them to the API."""
|
|
330
|
+
async def _process_execution_node_event(event: ExecutionGriptapeNodeEvent) -> None:
|
|
331
|
+
"""Process ExecutionGriptapeNodeEvents and send them to the API (async version)."""
|
|
332
|
+
await __emit_message("execution_event", event.wrapped_event.json())
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
async def _process_progress_event(gt_event: ProgressEvent) -> None:
|
|
336
|
+
"""Process Griptape framework events and send them to the API (async version)."""
|
|
239
337
|
node_name = gt_event.node_name
|
|
240
338
|
if node_name:
|
|
241
339
|
value = gt_event.value
|
|
@@ -243,42 +341,7 @@ def __process_progress_event(gt_event: ProgressEvent) -> None:
|
|
|
243
341
|
node_name=node_name, parameter_name=gt_event.parameter_name, type=type(gt_event).__name__, value=value
|
|
244
342
|
)
|
|
245
343
|
event_to_emit = ExecutionEvent(payload=payload)
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
def __process_app_event(event: AppEvent) -> None:
|
|
250
|
-
"""Process AppEvents and send them to the API."""
|
|
251
|
-
# Let Griptape Nodes broadcast it.
|
|
252
|
-
GriptapeNodes.broadcast_app_event(event.payload)
|
|
253
|
-
|
|
254
|
-
__schedule_async_task(__emit_message("app_event", event.json()))
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
def _process_event_queue() -> None:
|
|
258
|
-
"""Listen for events in the event queue and process them.
|
|
259
|
-
|
|
260
|
-
Event queue will be populated by background threads listening for events from the Nodes API.
|
|
261
|
-
"""
|
|
262
|
-
# Wait for WebSocket connection to be established before processing events
|
|
263
|
-
timed_out = ws_ready_event.wait(timeout=15)
|
|
264
|
-
if not timed_out:
|
|
265
|
-
console.print(
|
|
266
|
-
"[red] The connection to the websocket timed out. Please check your internet connection or the status of Griptape Nodes API.[/red]"
|
|
267
|
-
)
|
|
268
|
-
sys.exit(1)
|
|
269
|
-
while True:
|
|
270
|
-
event = event_queue.get(block=True)
|
|
271
|
-
if isinstance(event, EventRequest):
|
|
272
|
-
request_payload = event.request
|
|
273
|
-
GriptapeNodes.handle_request(
|
|
274
|
-
request_payload, response_topic=event.response_topic, request_id=event.request_id
|
|
275
|
-
)
|
|
276
|
-
elif isinstance(event, AppEvent):
|
|
277
|
-
__process_app_event(event)
|
|
278
|
-
else:
|
|
279
|
-
logger.warning("Unknown event type encountered: '%s'.", type(event))
|
|
280
|
-
|
|
281
|
-
event_queue.task_done()
|
|
344
|
+
await __emit_message("execution_event", event_to_emit.json())
|
|
282
345
|
|
|
283
346
|
|
|
284
347
|
def _create_websocket_connection(api_key: str) -> Any:
|
|
@@ -296,29 +359,29 @@ def _create_websocket_connection(api_key: str) -> Any:
|
|
|
296
359
|
|
|
297
360
|
async def __emit_message(event_type: str, payload: str, topic: str | None = None) -> None:
|
|
298
361
|
"""Send a message via WebSocket asynchronously."""
|
|
299
|
-
|
|
300
|
-
if
|
|
362
|
+
ws_connection = ws_connection_context.get()
|
|
363
|
+
if ws_connection is None:
|
|
301
364
|
logger.warning("WebSocket connection not available for sending message")
|
|
302
365
|
return
|
|
303
366
|
|
|
304
367
|
try:
|
|
305
368
|
# Determine topic based on session_id and engine_id in the payload
|
|
306
369
|
if topic is None:
|
|
307
|
-
topic =
|
|
370
|
+
topic = determine_response_topic()
|
|
308
371
|
|
|
309
372
|
body = {"type": event_type, "payload": json.loads(payload), "topic": topic}
|
|
310
373
|
|
|
311
|
-
await
|
|
374
|
+
await ws_connection.send(json.dumps(body))
|
|
312
375
|
except WebSocketException as e:
|
|
313
376
|
logger.error("Error sending event to Nodes API: %s", e)
|
|
314
377
|
except Exception as e:
|
|
315
378
|
logger.error("Unexpected error while sending event to Nodes API: %s", e)
|
|
316
379
|
|
|
317
380
|
|
|
318
|
-
def
|
|
381
|
+
def determine_response_topic() -> str | None:
|
|
319
382
|
"""Determine the response topic based on session_id and engine_id in the payload."""
|
|
320
|
-
engine_id =
|
|
321
|
-
session_id =
|
|
383
|
+
engine_id = griptape_nodes.get_engine_id()
|
|
384
|
+
session_id = griptape_nodes.get_session_id()
|
|
322
385
|
|
|
323
386
|
# Normal topic determination logic
|
|
324
387
|
# Check for session_id first (highest priority)
|
|
@@ -333,10 +396,10 @@ def _determine_response_topic() -> str | None:
|
|
|
333
396
|
return "response"
|
|
334
397
|
|
|
335
398
|
|
|
336
|
-
def
|
|
399
|
+
def determine_request_topic() -> str | None:
|
|
337
400
|
"""Determine the request topic based on session_id and engine_id in the payload."""
|
|
338
|
-
engine_id =
|
|
339
|
-
session_id =
|
|
401
|
+
engine_id = griptape_nodes.get_engine_id()
|
|
402
|
+
session_id = griptape_nodes.get_session_id()
|
|
340
403
|
|
|
341
404
|
# Normal topic determination logic
|
|
342
405
|
# Check for session_id first (highest priority)
|
|
@@ -351,25 +414,16 @@ def _determine_request_topic() -> str | None:
|
|
|
351
414
|
return "request"
|
|
352
415
|
|
|
353
416
|
|
|
354
|
-
def subscribe_to_topic(topic: str) -> None:
|
|
355
|
-
"""Subscribe to a specific topic in the message bus."""
|
|
356
|
-
__schedule_async_task(_asubscribe_to_topic(topic))
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
def unsubscribe_from_topic(topic: str) -> None:
|
|
360
|
-
"""Unsubscribe from a specific topic in the message bus."""
|
|
361
|
-
__schedule_async_task(_aunsubscribe_from_topic(topic))
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
async def _asubscribe_to_topic(topic: str) -> None:
|
|
417
|
+
async def subscribe_to_topic(topic: str) -> None:
|
|
365
418
|
"""Subscribe to a specific topic in the message bus."""
|
|
366
|
-
|
|
419
|
+
ws_connection = ws_connection_context.get()
|
|
420
|
+
if ws_connection is None:
|
|
367
421
|
logger.warning("WebSocket connection not available for subscribing to topic")
|
|
368
422
|
return
|
|
369
423
|
|
|
370
424
|
try:
|
|
371
425
|
body = {"type": "subscribe", "topic": topic, "payload": {}}
|
|
372
|
-
await
|
|
426
|
+
await ws_connection.send(json.dumps(body))
|
|
373
427
|
logger.info("Subscribed to topic: %s", topic)
|
|
374
428
|
except WebSocketException as e:
|
|
375
429
|
logger.error("Error subscribing to topic %s: %s", topic, e)
|
|
@@ -377,15 +431,16 @@ async def _asubscribe_to_topic(topic: str) -> None:
|
|
|
377
431
|
logger.error("Unexpected error while subscribing to topic %s: %s", topic, e)
|
|
378
432
|
|
|
379
433
|
|
|
380
|
-
async def
|
|
434
|
+
async def unsubscribe_from_topic(topic: str) -> None:
|
|
381
435
|
"""Unsubscribe from a specific topic in the message bus."""
|
|
382
|
-
|
|
436
|
+
ws_connection = ws_connection_context.get()
|
|
437
|
+
if ws_connection is None:
|
|
383
438
|
logger.warning("WebSocket connection not available for unsubscribing from topic")
|
|
384
439
|
return
|
|
385
440
|
|
|
386
441
|
try:
|
|
387
442
|
body = {"type": "unsubscribe", "topic": topic, "payload": {}}
|
|
388
|
-
await
|
|
443
|
+
await ws_connection.send(json.dumps(body))
|
|
389
444
|
logger.info("Unsubscribed from topic: %s", topic)
|
|
390
445
|
except WebSocketException as e:
|
|
391
446
|
logger.error("Error unsubscribing from topic %s: %s", topic, e)
|
|
@@ -393,16 +448,8 @@ async def _aunsubscribe_from_topic(topic: str) -> None:
|
|
|
393
448
|
logger.error("Unexpected error while unsubscribing from topic %s: %s", topic, e)
|
|
394
449
|
|
|
395
450
|
|
|
396
|
-
def
|
|
397
|
-
"""
|
|
398
|
-
if event_loop and event_loop.is_running():
|
|
399
|
-
asyncio.run_coroutine_threadsafe(coro, event_loop)
|
|
400
|
-
else:
|
|
401
|
-
logger.warning("Event loop not available for scheduling async task")
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
def _process_api_event(event: dict, event_queue: Queue) -> None:
|
|
405
|
-
"""Process API events and send them to the event queue."""
|
|
451
|
+
async def _process_api_event(event: dict) -> None:
|
|
452
|
+
"""Process API events and add to async queue."""
|
|
406
453
|
payload = event.get("payload", {})
|
|
407
454
|
|
|
408
455
|
try:
|
|
@@ -423,43 +470,18 @@ def _process_api_event(event: dict, event_queue: Queue) -> None:
|
|
|
423
470
|
# Now attempt to convert it into an EventRequest.
|
|
424
471
|
try:
|
|
425
472
|
request_event = deserialize_event(json_data=payload)
|
|
426
|
-
if not isinstance(request_event, EventRequest):
|
|
427
|
-
msg = f"Deserialized event is not an EventRequest: {type(request_event)}"
|
|
428
|
-
raise TypeError(msg) # noqa: TRY301
|
|
429
473
|
except Exception as e:
|
|
430
474
|
msg = f"Unable to convert request JSON into a valid EventRequest object. Error Message: '{e}'"
|
|
431
475
|
raise RuntimeError(msg) from None
|
|
432
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
|
+
|
|
433
481
|
# Check if the event implements SkipTheLineMixin for priority processing
|
|
434
482
|
if isinstance(request_event.request, SkipTheLineMixin):
|
|
435
483
|
# Handle the event immediately without queuing
|
|
436
|
-
|
|
437
|
-
result_payload = GriptapeNodes.handle_request(
|
|
438
|
-
cast("RequestPayload", request_event.request),
|
|
439
|
-
response_topic=request_event.response_topic,
|
|
440
|
-
request_id=request_event.request_id,
|
|
441
|
-
)
|
|
442
|
-
|
|
443
|
-
# Create the result event and emit response immediately
|
|
444
|
-
if result_payload.succeeded():
|
|
445
|
-
result_event = EventResultSuccess(
|
|
446
|
-
request=cast("RequestPayload", request_event.request),
|
|
447
|
-
request_id=request_event.request_id,
|
|
448
|
-
result=result_payload,
|
|
449
|
-
response_topic=request_event.response_topic,
|
|
450
|
-
)
|
|
451
|
-
dest_socket = "success_result"
|
|
452
|
-
else:
|
|
453
|
-
result_event = EventResultFailure(
|
|
454
|
-
request=cast("RequestPayload", request_event.request),
|
|
455
|
-
request_id=request_event.request_id,
|
|
456
|
-
result=result_payload,
|
|
457
|
-
response_topic=request_event.response_topic,
|
|
458
|
-
)
|
|
459
|
-
dest_socket = "failure_result"
|
|
460
|
-
|
|
461
|
-
# Emit the response immediately
|
|
462
|
-
__schedule_async_task(__emit_message(dest_socket, result_event.json(), topic=result_event.response_topic))
|
|
484
|
+
await _process_event_request(request_event)
|
|
463
485
|
else:
|
|
464
|
-
# Add the event to the queue for normal processing
|
|
465
|
-
|
|
486
|
+
# Add the event to the async queue for normal processing
|
|
487
|
+
await griptape_nodes.EventManager().aput_event(request_event)
|
griptape_nodes/app/watch.py
CHANGED
|
@@ -31,7 +31,7 @@ class ReloadHandler(PatternMatchingEventHandler):
|
|
|
31
31
|
|
|
32
32
|
def start_process(self) -> None:
|
|
33
33
|
if self.process:
|
|
34
|
-
self.process
|
|
34
|
+
self._terminate_process(self.process)
|
|
35
35
|
uv_path = find_uv_bin()
|
|
36
36
|
self.process = subprocess.Popen( # noqa: S603
|
|
37
37
|
[uv_path, "run", "gtn"],
|
|
@@ -39,6 +39,21 @@ class ReloadHandler(PatternMatchingEventHandler):
|
|
|
39
39
|
stderr=sys.stderr,
|
|
40
40
|
)
|
|
41
41
|
|
|
42
|
+
def _terminate_process(self, process: subprocess.Popen) -> None:
|
|
43
|
+
"""Gracefully terminate a process with timeout."""
|
|
44
|
+
if process.poll() is not None:
|
|
45
|
+
return # Process already terminated
|
|
46
|
+
|
|
47
|
+
# First try graceful termination
|
|
48
|
+
process.terminate()
|
|
49
|
+
try:
|
|
50
|
+
# Wait up to 5 seconds for graceful shutdown
|
|
51
|
+
process.wait(timeout=5)
|
|
52
|
+
except subprocess.TimeoutExpired:
|
|
53
|
+
# Force kill if it doesn't shut down gracefully
|
|
54
|
+
process.kill()
|
|
55
|
+
process.wait()
|
|
56
|
+
|
|
42
57
|
def on_modified(self, event: Any) -> None:
|
|
43
58
|
"""Called on any file event in the watched directory (create, modify, delete, move)."""
|
|
44
59
|
# Don't reload if the event is on a directory
|
|
@@ -65,7 +80,7 @@ if __name__ == "__main__":
|
|
|
65
80
|
time.sleep(1)
|
|
66
81
|
except KeyboardInterrupt:
|
|
67
82
|
if event_handler.process:
|
|
68
|
-
event_handler.process
|
|
83
|
+
event_handler._terminate_process(event_handler.process)
|
|
69
84
|
finally:
|
|
70
85
|
observer.stop()
|
|
71
86
|
observer.join()
|