griptape-nodes 0.57.1__py3-none-any.whl → 0.58.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/api_client/__init__.py +9 -0
- griptape_nodes/api_client/client.py +279 -0
- griptape_nodes/api_client/request_client.py +273 -0
- griptape_nodes/app/app.py +57 -150
- griptape_nodes/bootstrap/utils/python_subprocess_executor.py +1 -1
- griptape_nodes/bootstrap/workflow_executors/local_session_workflow_executor.py +22 -50
- griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +6 -1
- griptape_nodes/bootstrap/workflow_executors/subprocess_workflow_executor.py +27 -46
- griptape_nodes/bootstrap/workflow_executors/utils/subprocess_script.py +7 -0
- griptape_nodes/bootstrap/workflow_publishers/local_workflow_publisher.py +3 -1
- griptape_nodes/bootstrap/workflow_publishers/subprocess_workflow_publisher.py +3 -1
- griptape_nodes/bootstrap/workflow_publishers/utils/subprocess_script.py +16 -1
- griptape_nodes/common/node_executor.py +466 -0
- griptape_nodes/drivers/storage/base_storage_driver.py +0 -11
- griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +7 -25
- griptape_nodes/drivers/storage/local_storage_driver.py +2 -2
- griptape_nodes/exe_types/connections.py +37 -9
- griptape_nodes/exe_types/core_types.py +1 -1
- griptape_nodes/exe_types/node_types.py +115 -22
- griptape_nodes/machines/control_flow.py +48 -7
- griptape_nodes/machines/parallel_resolution.py +98 -29
- griptape_nodes/machines/sequential_resolution.py +61 -22
- griptape_nodes/node_library/library_registry.py +24 -1
- griptape_nodes/node_library/workflow_registry.py +38 -2
- griptape_nodes/retained_mode/events/execution_events.py +8 -1
- griptape_nodes/retained_mode/events/flow_events.py +90 -3
- griptape_nodes/retained_mode/events/node_events.py +17 -10
- griptape_nodes/retained_mode/events/workflow_events.py +5 -0
- griptape_nodes/retained_mode/griptape_nodes.py +16 -219
- griptape_nodes/retained_mode/managers/config_manager.py +0 -46
- griptape_nodes/retained_mode/managers/engine_identity_manager.py +225 -74
- griptape_nodes/retained_mode/managers/flow_manager.py +1276 -230
- griptape_nodes/retained_mode/managers/library_manager.py +7 -8
- griptape_nodes/retained_mode/managers/node_manager.py +197 -9
- griptape_nodes/retained_mode/managers/secrets_manager.py +26 -0
- griptape_nodes/retained_mode/managers/session_manager.py +264 -227
- griptape_nodes/retained_mode/managers/settings.py +4 -38
- griptape_nodes/retained_mode/managers/static_files_manager.py +3 -3
- griptape_nodes/retained_mode/managers/version_compatibility_manager.py +135 -6
- griptape_nodes/retained_mode/managers/workflow_manager.py +206 -78
- griptape_nodes/servers/mcp.py +23 -15
- griptape_nodes/utils/async_utils.py +36 -0
- griptape_nodes/utils/dict_utils.py +8 -2
- griptape_nodes/version_compatibility/versions/v0_39_0/modified_parameters_set_removal.py +11 -6
- griptape_nodes/version_compatibility/workflow_versions/v0_7_0/local_executor_argument_addition.py +12 -5
- {griptape_nodes-0.57.1.dist-info → griptape_nodes-0.58.1.dist-info}/METADATA +4 -3
- {griptape_nodes-0.57.1.dist-info → griptape_nodes-0.58.1.dist-info}/RECORD +49 -47
- {griptape_nodes-0.57.1.dist-info → griptape_nodes-0.58.1.dist-info}/WHEEL +1 -1
- griptape_nodes/retained_mode/utils/engine_identity.py +0 -245
- griptape_nodes/servers/ws_request_manager.py +0 -268
- {griptape_nodes-0.57.1.dist-info → griptape_nodes-0.58.1.dist-info}/entry_points.txt +0 -0
griptape_nodes/app/app.py
CHANGED
|
@@ -3,20 +3,13 @@ from __future__ import annotations
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import json
|
|
5
5
|
import logging
|
|
6
|
-
import os
|
|
7
|
-
import sys
|
|
8
6
|
import threading
|
|
9
7
|
from dataclasses import dataclass
|
|
10
|
-
from typing import Any
|
|
11
|
-
from urllib.parse import urljoin
|
|
12
8
|
|
|
13
|
-
from rich.align import Align
|
|
14
9
|
from rich.console import Console
|
|
15
10
|
from rich.logging import RichHandler
|
|
16
|
-
from rich.panel import Panel
|
|
17
|
-
from websockets.asyncio.client import connect
|
|
18
|
-
from websockets.exceptions import ConnectionClosed, ConnectionClosedError, WebSocketException
|
|
19
11
|
|
|
12
|
+
from griptape_nodes.api_client import Client
|
|
20
13
|
from griptape_nodes.retained_mode.events import app_events, execution_events
|
|
21
14
|
|
|
22
15
|
# This import is necessary to register all events, even if not technically used
|
|
@@ -98,11 +91,15 @@ class EventLogHandler(logging.Handler):
|
|
|
98
91
|
logger = logging.getLogger("griptape_nodes_app")
|
|
99
92
|
|
|
100
93
|
griptape_nodes_logger = logging.getLogger("griptape_nodes")
|
|
94
|
+
griptape_nodes_logger.addHandler(EventLogHandler())
|
|
95
|
+
griptape_nodes_logger.setLevel(logging.INFO)
|
|
96
|
+
|
|
97
|
+
# Root logger only gets RichHandler for console output
|
|
101
98
|
logging.basicConfig(
|
|
102
99
|
level=logging.INFO,
|
|
103
100
|
format="%(message)s",
|
|
104
101
|
datefmt="[%X]",
|
|
105
|
-
handlers=[
|
|
102
|
+
handlers=[RichHandler(show_time=True, show_path=False, markup=True, rich_tracebacks=True)],
|
|
106
103
|
)
|
|
107
104
|
|
|
108
105
|
console = Console()
|
|
@@ -120,16 +117,12 @@ def start_app() -> None:
|
|
|
120
117
|
|
|
121
118
|
async def astart_app() -> None:
|
|
122
119
|
"""New async app entry point."""
|
|
123
|
-
api_key = _ensure_api_key()
|
|
124
|
-
|
|
125
120
|
# Initialize event queue in main thread
|
|
126
121
|
griptape_nodes.EventManager().initialize_queue()
|
|
127
122
|
|
|
128
123
|
try:
|
|
129
124
|
# Start WebSocket tasks in daemon thread
|
|
130
|
-
threading.Thread(
|
|
131
|
-
target=_start_websocket_connection, args=(api_key,), daemon=True, name="websocket-tasks"
|
|
132
|
-
).start()
|
|
125
|
+
threading.Thread(target=_start_websocket_connection, daemon=True, name="websocket-tasks").start()
|
|
133
126
|
|
|
134
127
|
# Run event processing on main thread
|
|
135
128
|
await _process_event_queue()
|
|
@@ -139,7 +132,7 @@ async def astart_app() -> None:
|
|
|
139
132
|
raise
|
|
140
133
|
|
|
141
134
|
|
|
142
|
-
def _start_websocket_connection(
|
|
135
|
+
def _start_websocket_connection() -> None:
|
|
143
136
|
"""Run WebSocket tasks in a separate thread with its own async loop."""
|
|
144
137
|
global websocket_event_loop # noqa: PLW0603
|
|
145
138
|
try:
|
|
@@ -152,7 +145,7 @@ def _start_websocket_connection(api_key: str) -> None:
|
|
|
152
145
|
websocket_event_loop_ready.set()
|
|
153
146
|
|
|
154
147
|
# Run the async WebSocket tasks
|
|
155
|
-
loop.run_until_complete(_run_websocket_tasks(
|
|
148
|
+
loop.run_until_complete(_run_websocket_tasks())
|
|
156
149
|
except Exception as e:
|
|
157
150
|
logger.error("WebSocket thread error: %s", e)
|
|
158
151
|
raise
|
|
@@ -161,80 +154,40 @@ def _start_websocket_connection(api_key: str) -> None:
|
|
|
161
154
|
websocket_event_loop_ready.clear()
|
|
162
155
|
|
|
163
156
|
|
|
164
|
-
async def _run_websocket_tasks(
|
|
157
|
+
async def _run_websocket_tasks() -> None:
|
|
165
158
|
"""Run WebSocket tasks - async version."""
|
|
166
|
-
|
|
167
|
-
connection_stream = _create_websocket_connection(api_key)
|
|
168
|
-
|
|
169
|
-
# Track if this is the first connection
|
|
170
|
-
initialized = False
|
|
171
|
-
|
|
172
|
-
async for ws_connection in connection_stream:
|
|
159
|
+
async with Client() as client:
|
|
173
160
|
logger.debug("WebSocket connection established")
|
|
174
|
-
try:
|
|
175
|
-
# Emit initialization event only for the first connection
|
|
176
|
-
if not initialized:
|
|
177
|
-
griptape_nodes.EventManager().put_event(AppEvent(payload=app_events.AppInitializationComplete()))
|
|
178
|
-
initialized = True
|
|
179
|
-
|
|
180
|
-
# Emit connection established event for every connection
|
|
181
|
-
griptape_nodes.EventManager().put_event(AppEvent(payload=app_events.AppConnectionEstablished()))
|
|
182
|
-
|
|
183
|
-
async with asyncio.TaskGroup() as tg:
|
|
184
|
-
tg.create_task(_process_incoming_messages(ws_connection))
|
|
185
|
-
tg.create_task(_send_outgoing_messages(ws_connection))
|
|
186
|
-
except (ExceptionGroup, ConnectionClosed, ConnectionClosedError):
|
|
187
|
-
logger.info("WebSocket connection closed, reconnecting...")
|
|
188
|
-
continue
|
|
189
|
-
except Exception:
|
|
190
|
-
logger.exception("WebSocket tasks failed")
|
|
191
|
-
await asyncio.sleep(2.0) # Wait before retry
|
|
192
|
-
continue
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
def _ensure_api_key() -> str:
|
|
196
|
-
secrets_manager = griptape_nodes.SecretsManager()
|
|
197
|
-
api_key = secrets_manager.get_secret("GT_CLOUD_API_KEY")
|
|
198
|
-
if api_key is None:
|
|
199
|
-
message = Panel(
|
|
200
|
-
Align.center(
|
|
201
|
-
"[bold red]Nodes API key is not set, please run [code]gtn init[/code] with a valid key: [/bold red]"
|
|
202
|
-
"[code]gtn init --api-key <your key>[/code]\n"
|
|
203
|
-
"[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]",
|
|
204
|
-
),
|
|
205
|
-
title="[red]X[/red] Missing Nodes API Key",
|
|
206
|
-
border_style="red",
|
|
207
|
-
padding=(1, 4),
|
|
208
|
-
)
|
|
209
|
-
console.print(message)
|
|
210
|
-
sys.exit(1)
|
|
211
161
|
|
|
212
|
-
|
|
162
|
+
griptape_nodes.EventManager().put_event(AppEvent(payload=app_events.AppInitializationComplete()))
|
|
163
|
+
griptape_nodes.EventManager().put_event(AppEvent(payload=app_events.AppConnectionEstablished()))
|
|
213
164
|
|
|
165
|
+
async with asyncio.TaskGroup() as tg:
|
|
166
|
+
tg.create_task(_process_incoming_messages(client))
|
|
167
|
+
tg.create_task(_send_outgoing_messages(client))
|
|
214
168
|
|
|
215
|
-
|
|
169
|
+
|
|
170
|
+
async def _process_incoming_messages(client: Client) -> None:
|
|
216
171
|
"""Process incoming WebSocket requests from Nodes API."""
|
|
217
172
|
logger.debug("Processing incoming WebSocket requests from WebSocket connection")
|
|
218
173
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
except Exception:
|
|
224
|
-
logger.exception("Error processing event, skipping.")
|
|
174
|
+
topics = ["request"]
|
|
175
|
+
engine_id = griptape_nodes.get_engine_id()
|
|
176
|
+
if engine_id:
|
|
177
|
+
topics.append(f"engines/{engine_id}/request")
|
|
225
178
|
|
|
179
|
+
session_id = griptape_nodes.get_session_id()
|
|
180
|
+
if session_id:
|
|
181
|
+
topics.append(f"sessions/{session_id}/request")
|
|
226
182
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
endpoint = urljoin(
|
|
230
|
-
os.getenv("GRIPTAPE_NODES_API_BASE_URL", "https://api.nodes.griptape.ai").replace("http", "ws"),
|
|
231
|
-
"/ws/engines/events?version=v2",
|
|
232
|
-
)
|
|
183
|
+
for topic in topics:
|
|
184
|
+
await client.subscribe(topic)
|
|
233
185
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
186
|
+
async for message in client.messages:
|
|
187
|
+
try:
|
|
188
|
+
await _process_api_event(message)
|
|
189
|
+
except Exception:
|
|
190
|
+
logger.exception("Error processing event, skipping.")
|
|
238
191
|
|
|
239
192
|
|
|
240
193
|
async def _process_api_event(event: dict) -> None:
|
|
@@ -276,7 +229,7 @@ async def _process_api_event(event: dict) -> None:
|
|
|
276
229
|
griptape_nodes.EventManager().put_event(request_event)
|
|
277
230
|
|
|
278
231
|
|
|
279
|
-
async def _send_outgoing_messages(
|
|
232
|
+
async def _send_outgoing_messages(client: Client) -> None:
|
|
280
233
|
"""Send outgoing WebSocket requests from queue on background thread."""
|
|
281
234
|
logger.debug("Starting outgoing WebSocket request sender")
|
|
282
235
|
|
|
@@ -286,11 +239,14 @@ async def _send_outgoing_messages(ws_connection: Any) -> None:
|
|
|
286
239
|
|
|
287
240
|
try:
|
|
288
241
|
if isinstance(message, WebSocketMessage):
|
|
289
|
-
|
|
242
|
+
# Use client to publish message
|
|
243
|
+
topic = message.topic if message.topic else _determine_response_topic()
|
|
244
|
+
payload_dict = json.loads(message.payload)
|
|
245
|
+
await client.publish(message.event_type, payload_dict, topic)
|
|
290
246
|
elif isinstance(message, SubscribeCommand):
|
|
291
|
-
await
|
|
247
|
+
await client.subscribe(message.topic)
|
|
292
248
|
elif isinstance(message, UnsubscribeCommand):
|
|
293
|
-
await
|
|
249
|
+
await client.unsubscribe(message.topic)
|
|
294
250
|
else:
|
|
295
251
|
logger.warning("Unknown outgoing message type: %s", type(message))
|
|
296
252
|
except Exception as e:
|
|
@@ -299,44 +255,6 @@ async def _send_outgoing_messages(ws_connection: Any) -> None:
|
|
|
299
255
|
ws_outgoing_queue.task_done()
|
|
300
256
|
|
|
301
257
|
|
|
302
|
-
async def _send_websocket_message(ws_connection: Any, event_type: str, payload: str, topic: str | None) -> None:
|
|
303
|
-
"""Send a message via WebSocket."""
|
|
304
|
-
try:
|
|
305
|
-
if topic is None:
|
|
306
|
-
topic = determine_response_topic()
|
|
307
|
-
|
|
308
|
-
body = {"type": event_type, "payload": json.loads(payload), "topic": topic}
|
|
309
|
-
await ws_connection.send(json.dumps(body))
|
|
310
|
-
except WebSocketException as e:
|
|
311
|
-
logger.error("Error sending WebSocket message: %s", e)
|
|
312
|
-
except Exception as e:
|
|
313
|
-
logger.error("Unexpected error sending WebSocket message: %s", e)
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
async def _send_subscribe_command(ws_connection: Any, topic: str) -> None:
|
|
317
|
-
"""Send subscribe command via WebSocket."""
|
|
318
|
-
try:
|
|
319
|
-
body = {"type": "subscribe", "topic": topic, "payload": {}}
|
|
320
|
-
await ws_connection.send(json.dumps(body))
|
|
321
|
-
logger.debug("Subscribed to topic: %s", topic)
|
|
322
|
-
except WebSocketException as e:
|
|
323
|
-
logger.error("Error subscribing to topic %s: %s", topic, e)
|
|
324
|
-
except Exception as e:
|
|
325
|
-
logger.error("Unexpected error subscribing to topic %s: %s", topic, e)
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
async def _send_unsubscribe_command(ws_connection: Any, topic: str) -> None:
|
|
329
|
-
"""Send unsubscribe command via WebSocket."""
|
|
330
|
-
try:
|
|
331
|
-
body = {"type": "unsubscribe", "topic": topic, "payload": {}}
|
|
332
|
-
await ws_connection.send(json.dumps(body))
|
|
333
|
-
logger.debug("Unsubscribed from topic: %s", topic)
|
|
334
|
-
except WebSocketException as e:
|
|
335
|
-
logger.error("Error unsubscribing from topic %s: %s", topic, e)
|
|
336
|
-
except Exception as e:
|
|
337
|
-
logger.error("Unexpected error unsubscribing from topic %s: %s", topic, e)
|
|
338
|
-
|
|
339
|
-
|
|
340
258
|
async def _process_event_queue() -> None:
|
|
341
259
|
"""Process events concurrently - runs on main thread."""
|
|
342
260
|
logger.debug("Starting event queue processor on main thread")
|
|
@@ -382,13 +300,7 @@ async def _process_event_request(event: EventRequest) -> None:
|
|
|
382
300
|
event.request,
|
|
383
301
|
result_context={"response_topic": event.response_topic, "request_id": event.request_id},
|
|
384
302
|
)
|
|
385
|
-
|
|
386
|
-
if result_event.result.succeeded():
|
|
387
|
-
dest_socket = "success_result"
|
|
388
|
-
else:
|
|
389
|
-
dest_socket = "failure_result"
|
|
390
|
-
|
|
391
|
-
await _send_message(dest_socket, result_event.json(), topic=result_event.response_topic)
|
|
303
|
+
await _process_node_event(GriptapeNodeEvent(wrapped_event=result_event))
|
|
392
304
|
|
|
393
305
|
|
|
394
306
|
async def _process_app_event(event: AppEvent) -> None:
|
|
@@ -403,8 +315,21 @@ async def _process_node_event(event: GriptapeNodeEvent) -> None:
|
|
|
403
315
|
"""Process GriptapeNodeEvents and send them to the API (async version)."""
|
|
404
316
|
# Emit the result back to the GUI
|
|
405
317
|
result_event = event.wrapped_event
|
|
318
|
+
|
|
406
319
|
if isinstance(result_event, EventResultSuccess):
|
|
407
320
|
dest_socket = "success_result"
|
|
321
|
+
# Handle session-specific topic subscriptions
|
|
322
|
+
if isinstance(result_event.result, app_events.AppStartSessionResultSuccess):
|
|
323
|
+
session_id = result_event.result.session_id
|
|
324
|
+
topic = f"sessions/{session_id}/request"
|
|
325
|
+
await _subscribe_to_topic(topic)
|
|
326
|
+
logger.info("Subscribed to session topic: %s", topic)
|
|
327
|
+
elif isinstance(result_event.result, app_events.AppEndSessionResultSuccess):
|
|
328
|
+
session_id = result_event.result.session_id
|
|
329
|
+
if session_id is not None:
|
|
330
|
+
topic = f"sessions/{session_id}/request"
|
|
331
|
+
await _unsubscribe_from_topic(topic)
|
|
332
|
+
logger.info("Unsubscribed from session topic: %s", topic)
|
|
408
333
|
elif isinstance(result_event, EventResultFailure):
|
|
409
334
|
dest_socket = "failure_result"
|
|
410
335
|
else:
|
|
@@ -443,14 +368,14 @@ async def _send_message(event_type: str, payload: str, topic: str | None = None)
|
|
|
443
368
|
|
|
444
369
|
# Determine topic based on session_id and engine_id in the payload
|
|
445
370
|
if topic is None:
|
|
446
|
-
topic =
|
|
371
|
+
topic = _determine_response_topic()
|
|
447
372
|
|
|
448
373
|
message = WebSocketMessage(event_type, payload, topic)
|
|
449
374
|
|
|
450
375
|
asyncio.run_coroutine_threadsafe(ws_outgoing_queue.put(message), websocket_event_loop)
|
|
451
376
|
|
|
452
377
|
|
|
453
|
-
async def
|
|
378
|
+
async def _subscribe_to_topic(topic: str) -> None:
|
|
454
379
|
"""Queue a subscribe command for WebSocket using run_coroutine_threadsafe."""
|
|
455
380
|
# Wait for websocket event loop to be ready
|
|
456
381
|
websocket_event_loop_ready.wait()
|
|
@@ -462,7 +387,7 @@ async def subscribe_to_topic(topic: str) -> None:
|
|
|
462
387
|
asyncio.run_coroutine_threadsafe(ws_outgoing_queue.put(SubscribeCommand(topic)), websocket_event_loop)
|
|
463
388
|
|
|
464
389
|
|
|
465
|
-
async def
|
|
390
|
+
async def _unsubscribe_from_topic(topic: str) -> None:
|
|
466
391
|
"""Queue an unsubscribe command for WebSocket using run_coroutine_threadsafe."""
|
|
467
392
|
if websocket_event_loop is None:
|
|
468
393
|
logger.error("WebSocket event loop not available for unsubscribe")
|
|
@@ -471,7 +396,7 @@ async def unsubscribe_from_topic(topic: str) -> None:
|
|
|
471
396
|
asyncio.run_coroutine_threadsafe(ws_outgoing_queue.put(UnsubscribeCommand(topic)), websocket_event_loop)
|
|
472
397
|
|
|
473
398
|
|
|
474
|
-
def
|
|
399
|
+
def _determine_response_topic() -> str:
|
|
475
400
|
"""Determine the response topic based on session_id and engine_id in the payload."""
|
|
476
401
|
engine_id = griptape_nodes.get_engine_id()
|
|
477
402
|
session_id = griptape_nodes.get_session_id()
|
|
@@ -487,21 +412,3 @@ def determine_response_topic() -> str | None:
|
|
|
487
412
|
|
|
488
413
|
# Default to generic response topic
|
|
489
414
|
return "response"
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
def determine_request_topic() -> str | None:
|
|
493
|
-
"""Determine the request topic based on session_id and engine_id in the payload."""
|
|
494
|
-
engine_id = griptape_nodes.get_engine_id()
|
|
495
|
-
session_id = griptape_nodes.get_session_id()
|
|
496
|
-
|
|
497
|
-
# Normal topic determination logic
|
|
498
|
-
# Check for session_id first (highest priority)
|
|
499
|
-
if session_id:
|
|
500
|
-
return f"sessions/{session_id}/request"
|
|
501
|
-
|
|
502
|
-
# Check for engine_id if no session_id
|
|
503
|
-
if engine_id:
|
|
504
|
-
return f"engines/{engine_id}/request"
|
|
505
|
-
|
|
506
|
-
# Default to generic request topic
|
|
507
|
-
return "request"
|
|
@@ -69,7 +69,7 @@ class PythonSubprocessExecutor:
|
|
|
69
69
|
raise RuntimeError(msg) # noqa: TRY301
|
|
70
70
|
|
|
71
71
|
except Exception as e:
|
|
72
|
-
msg = "Error running subprocess"
|
|
72
|
+
msg = f"Error running subprocess: {e}"
|
|
73
73
|
logger.exception(msg)
|
|
74
74
|
raise PythonSubprocessExecutorError(msg) from e
|
|
75
75
|
finally:
|
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import json
|
|
4
5
|
import logging
|
|
5
6
|
import threading
|
|
6
7
|
from typing import TYPE_CHECKING, Any, Self
|
|
7
8
|
|
|
8
|
-
from
|
|
9
|
-
|
|
10
|
-
from griptape_nodes.app.app import (
|
|
11
|
-
WebSocketMessage,
|
|
12
|
-
_create_websocket_connection,
|
|
13
|
-
_send_websocket_message,
|
|
14
|
-
)
|
|
9
|
+
from griptape_nodes.api_client import Client
|
|
10
|
+
from griptape_nodes.app.app import WebSocketMessage
|
|
15
11
|
from griptape_nodes.bootstrap.workflow_executors.local_workflow_executor import (
|
|
16
12
|
LocalExecutorError,
|
|
17
13
|
LocalWorkflowExecutor,
|
|
@@ -63,7 +59,7 @@ class LocalSessionWorkflowExecutor(LocalWorkflowExecutor):
|
|
|
63
59
|
|
|
64
60
|
logger.info("Setting up session %s", self._session_id)
|
|
65
61
|
GriptapeNodes.SessionManager().save_session(self._session_id)
|
|
66
|
-
GriptapeNodes.SessionManager().
|
|
62
|
+
GriptapeNodes.SessionManager().active_session_id = self._session_id
|
|
67
63
|
await self._start_websocket_connection()
|
|
68
64
|
|
|
69
65
|
return self
|
|
@@ -169,7 +165,10 @@ class LocalSessionWorkflowExecutor(LocalWorkflowExecutor):
|
|
|
169
165
|
)
|
|
170
166
|
|
|
171
167
|
# Send the run command to actually execute it (fire and forget)
|
|
172
|
-
|
|
168
|
+
pickle_control_flow_result = kwargs.get("pickle_control_flow_result", False)
|
|
169
|
+
start_flow_request = StartFlowRequest(
|
|
170
|
+
flow_name=flow_name, pickle_control_flow_result=pickle_control_flow_result
|
|
171
|
+
)
|
|
173
172
|
start_flow_task = asyncio.create_task(GriptapeNodes.ahandle_request(start_flow_request))
|
|
174
173
|
|
|
175
174
|
is_flow_finished = False
|
|
@@ -261,13 +260,7 @@ class LocalSessionWorkflowExecutor(LocalWorkflowExecutor):
|
|
|
261
260
|
async def _start_websocket_connection(self) -> None:
|
|
262
261
|
"""Start websocket connection in a background thread for event emission."""
|
|
263
262
|
logger.info("Starting websocket connection for session %s", self._session_id)
|
|
264
|
-
|
|
265
|
-
if api_key is None:
|
|
266
|
-
logger.warning("No API key found, websocket connection will not be established")
|
|
267
|
-
return
|
|
268
|
-
|
|
269
|
-
logger.info("API key found, starting websocket thread")
|
|
270
|
-
self._websocket_thread = threading.Thread(target=self._start_websocket_thread, args=(api_key,), daemon=True)
|
|
263
|
+
self._websocket_thread = threading.Thread(target=self._start_websocket_thread, daemon=True)
|
|
271
264
|
self._websocket_thread.start()
|
|
272
265
|
|
|
273
266
|
if self._websocket_event_loop_ready.wait(timeout=10):
|
|
@@ -276,16 +269,7 @@ class LocalSessionWorkflowExecutor(LocalWorkflowExecutor):
|
|
|
276
269
|
else:
|
|
277
270
|
logger.error("Timeout waiting for websocket thread to start")
|
|
278
271
|
|
|
279
|
-
def
|
|
280
|
-
"""Get API key from secrets manager."""
|
|
281
|
-
try:
|
|
282
|
-
secrets_manager = GriptapeNodes.SecretsManager()
|
|
283
|
-
return secrets_manager.get_secret("GT_CLOUD_API_KEY")
|
|
284
|
-
except Exception:
|
|
285
|
-
logger.exception("Failed to get API key")
|
|
286
|
-
return None
|
|
287
|
-
|
|
288
|
-
def _start_websocket_thread(self, api_key: str) -> None:
|
|
272
|
+
def _start_websocket_thread(self) -> None:
|
|
289
273
|
"""Run WebSocket tasks in a separate thread with its own async loop."""
|
|
290
274
|
try:
|
|
291
275
|
# Create a new event loop for this thread
|
|
@@ -302,7 +286,7 @@ class LocalSessionWorkflowExecutor(LocalWorkflowExecutor):
|
|
|
302
286
|
logger.info("Websocket thread started and ready")
|
|
303
287
|
|
|
304
288
|
# Run the async WebSocket tasks
|
|
305
|
-
loop.run_until_complete(self._run_websocket_tasks(
|
|
289
|
+
loop.run_until_complete(self._run_websocket_tasks())
|
|
306
290
|
except Exception as e:
|
|
307
291
|
logger.error("WebSocket thread error: %s", e)
|
|
308
292
|
finally:
|
|
@@ -311,35 +295,21 @@ class LocalSessionWorkflowExecutor(LocalWorkflowExecutor):
|
|
|
311
295
|
self._shutdown_event = None
|
|
312
296
|
logger.info("Websocket thread ended")
|
|
313
297
|
|
|
314
|
-
async def _run_websocket_tasks(self
|
|
298
|
+
async def _run_websocket_tasks(self) -> None:
|
|
315
299
|
"""Run websocket tasks - establish connection and handle outgoing messages."""
|
|
316
|
-
logger.info("Creating
|
|
317
|
-
connection_stream = _create_websocket_connection(api_key)
|
|
300
|
+
logger.info("Creating Client for session %s", self._session_id)
|
|
318
301
|
|
|
319
|
-
async
|
|
302
|
+
async with Client() as client:
|
|
320
303
|
logger.info("WebSocket connection established for session %s", self._session_id)
|
|
321
|
-
try:
|
|
322
|
-
# Use our own version that works with our local queue
|
|
323
|
-
await self._send_outgoing_messages(ws_connection)
|
|
324
304
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
continue
|
|
328
|
-
except asyncio.CancelledError:
|
|
329
|
-
logger.info("WebSocket task cancelled, shutting down")
|
|
330
|
-
break
|
|
305
|
+
try:
|
|
306
|
+
await self._send_outgoing_messages(client)
|
|
331
307
|
except Exception:
|
|
332
308
|
logger.exception("WebSocket tasks failed")
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
if self._shutdown_event and self._shutdown_event.is_set():
|
|
337
|
-
logger.info("Shutdown requested, ending WebSocket connection loop")
|
|
338
|
-
break
|
|
339
|
-
|
|
340
|
-
logger.info("WebSocket connection loop ended")
|
|
309
|
+
finally:
|
|
310
|
+
logger.info("WebSocket connection loop ended")
|
|
341
311
|
|
|
342
|
-
async def _send_outgoing_messages(self,
|
|
312
|
+
async def _send_outgoing_messages(self, client: Client) -> None:
|
|
343
313
|
"""Send outgoing WebSocket messages from queue - matches app.py pattern exactly."""
|
|
344
314
|
if self._ws_outgoing_queue is None:
|
|
345
315
|
logger.error("No outgoing queue available")
|
|
@@ -362,7 +332,9 @@ class LocalSessionWorkflowExecutor(LocalWorkflowExecutor):
|
|
|
362
332
|
|
|
363
333
|
try:
|
|
364
334
|
if isinstance(message, WebSocketMessage):
|
|
365
|
-
|
|
335
|
+
topic = message.topic if message.topic else f"sessions/{self._session_id}/response"
|
|
336
|
+
payload_dict = json.loads(message.payload)
|
|
337
|
+
await client.publish(message.event_type, payload_dict, topic)
|
|
366
338
|
logger.debug("DELIVERED: %s event", message.event_type)
|
|
367
339
|
else:
|
|
368
340
|
logger.warning("Unknown outgoing message type: %s", type(message))
|
|
@@ -104,6 +104,8 @@ class LocalWorkflowExecutor(WorkflowExecutor):
|
|
|
104
104
|
for node_name, node in nodes.items():
|
|
105
105
|
if isinstance(node, EndNode):
|
|
106
106
|
output[node_name] = node.parameter_values
|
|
107
|
+
# Parameter_output_values should also be included, and should take priority over parameter_values
|
|
108
|
+
output[node_name].update(node.parameter_output_values)
|
|
107
109
|
|
|
108
110
|
return output
|
|
109
111
|
|
|
@@ -216,7 +218,10 @@ class LocalWorkflowExecutor(WorkflowExecutor):
|
|
|
216
218
|
)
|
|
217
219
|
|
|
218
220
|
# Now send the run command to actually execute it
|
|
219
|
-
|
|
221
|
+
pickle_control_flow_result = kwargs.get("pickle_control_flow_result", False)
|
|
222
|
+
start_flow_request = StartFlowRequest(
|
|
223
|
+
flow_name=flow_name, pickle_control_flow_result=pickle_control_flow_result
|
|
224
|
+
)
|
|
220
225
|
start_flow_result = await GriptapeNodes.ahandle_request(start_flow_request)
|
|
221
226
|
|
|
222
227
|
if start_flow_result.failed():
|
|
@@ -10,9 +10,8 @@ from pathlib import Path
|
|
|
10
10
|
from typing import TYPE_CHECKING, Any, Self
|
|
11
11
|
|
|
12
12
|
import anyio
|
|
13
|
-
from websockets.exceptions import ConnectionClosed, ConnectionClosedError
|
|
14
13
|
|
|
15
|
-
from griptape_nodes.
|
|
14
|
+
from griptape_nodes.api_client import Client
|
|
16
15
|
from griptape_nodes.bootstrap.utils.python_subprocess_executor import PythonSubprocessExecutor
|
|
17
16
|
from griptape_nodes.bootstrap.workflow_executors.local_session_workflow_executor import LocalSessionWorkflowExecutor
|
|
18
17
|
from griptape_nodes.drivers.storage import StorageBackend
|
|
@@ -80,6 +79,8 @@ class SubprocessWorkflowExecutor(LocalSessionWorkflowExecutor, PythonSubprocessE
|
|
|
80
79
|
workflow_name: str, # noqa: ARG002
|
|
81
80
|
flow_input: Any,
|
|
82
81
|
storage_backend: StorageBackend = StorageBackend.LOCAL,
|
|
82
|
+
*,
|
|
83
|
+
pickle_control_flow_result: bool = False,
|
|
83
84
|
**kwargs: Any, # noqa: ARG002
|
|
84
85
|
) -> None:
|
|
85
86
|
"""Execute a workflow in a subprocess and wait for completion."""
|
|
@@ -117,6 +118,9 @@ class SubprocessWorkflowExecutor(LocalSessionWorkflowExecutor, PythonSubprocessE
|
|
|
117
118
|
str(tmp_workflow_path),
|
|
118
119
|
]
|
|
119
120
|
|
|
121
|
+
if pickle_control_flow_result:
|
|
122
|
+
args.append("--pickle-control-flow-result")
|
|
123
|
+
|
|
120
124
|
try:
|
|
121
125
|
await self.execute_python_script(
|
|
122
126
|
script_path=tmp_script_path,
|
|
@@ -135,13 +139,7 @@ class SubprocessWorkflowExecutor(LocalSessionWorkflowExecutor, PythonSubprocessE
|
|
|
135
139
|
async def _start_websocket_listener(self) -> None:
|
|
136
140
|
"""Start WebSocket connection to listen for events from the subprocess."""
|
|
137
141
|
logger.info("Starting WebSocket listener for session %s", self._session_id)
|
|
138
|
-
|
|
139
|
-
if api_key is None:
|
|
140
|
-
logger.warning("No API key found, WebSocket listener will not be started")
|
|
141
|
-
return
|
|
142
|
-
|
|
143
|
-
logger.info("API key found, starting WebSocket listener thread")
|
|
144
|
-
self._websocket_thread = threading.Thread(target=self._start_websocket_thread, args=(api_key,), daemon=True)
|
|
142
|
+
self._websocket_thread = threading.Thread(target=self._start_websocket_thread, daemon=True)
|
|
145
143
|
self._websocket_thread.start()
|
|
146
144
|
|
|
147
145
|
if self._websocket_event_loop_ready.wait(timeout=10):
|
|
@@ -173,7 +171,7 @@ class SubprocessWorkflowExecutor(LocalSessionWorkflowExecutor, PythonSubprocessE
|
|
|
173
171
|
else:
|
|
174
172
|
logger.info("WebSocket listener thread stopped successfully")
|
|
175
173
|
|
|
176
|
-
def _start_websocket_thread(self
|
|
174
|
+
def _start_websocket_thread(self) -> None:
|
|
177
175
|
"""Run WebSocket tasks in a separate thread with its own async loop."""
|
|
178
176
|
try:
|
|
179
177
|
# Create a new event loop for this thread
|
|
@@ -189,7 +187,7 @@ class SubprocessWorkflowExecutor(LocalSessionWorkflowExecutor, PythonSubprocessE
|
|
|
189
187
|
logger.info("WebSocket listener thread started and ready")
|
|
190
188
|
|
|
191
189
|
# Run the async WebSocket listener
|
|
192
|
-
loop.run_until_complete(self._run_websocket_listener(
|
|
190
|
+
loop.run_until_complete(self._run_websocket_listener())
|
|
193
191
|
except Exception as e:
|
|
194
192
|
logger.error("WebSocket listener thread error: %s", e)
|
|
195
193
|
finally:
|
|
@@ -198,59 +196,37 @@ class SubprocessWorkflowExecutor(LocalSessionWorkflowExecutor, PythonSubprocessE
|
|
|
198
196
|
self._shutdown_event = None
|
|
199
197
|
logger.info("WebSocket listener thread ended")
|
|
200
198
|
|
|
201
|
-
async def _run_websocket_listener(self
|
|
199
|
+
async def _run_websocket_listener(self) -> None:
|
|
202
200
|
"""Run WebSocket listener - establish connection and handle incoming messages."""
|
|
203
|
-
logger.info("Creating
|
|
204
|
-
connection_stream = _create_websocket_connection(api_key)
|
|
201
|
+
logger.info("Creating Client for listening on session %s", self._session_id)
|
|
205
202
|
|
|
206
|
-
async
|
|
203
|
+
async with Client() as client:
|
|
207
204
|
logger.info("WebSocket connection established for session %s", self._session_id)
|
|
205
|
+
|
|
208
206
|
try:
|
|
209
|
-
|
|
210
|
-
await self._listen_for_messages(ws_connection)
|
|
211
|
-
|
|
212
|
-
except (ConnectionClosed, ConnectionClosedError):
|
|
213
|
-
logger.info("WebSocket connection closed, reconnecting...")
|
|
214
|
-
continue
|
|
215
|
-
except asyncio.CancelledError:
|
|
216
|
-
logger.info("WebSocket listener task cancelled, shutting down")
|
|
217
|
-
break
|
|
207
|
+
await self._listen_for_messages(client)
|
|
218
208
|
except Exception:
|
|
219
209
|
logger.exception("WebSocket listener failed")
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
# Check if shutdown was requested
|
|
224
|
-
if self._shutdown_event and self._shutdown_event.is_set():
|
|
225
|
-
logger.info("Shutdown requested, ending WebSocket listener connection loop")
|
|
226
|
-
break
|
|
227
|
-
|
|
228
|
-
logger.info("WebSocket listener connection loop ended")
|
|
210
|
+
finally:
|
|
211
|
+
logger.info("WebSocket listener connection loop ended")
|
|
229
212
|
|
|
230
|
-
async def _listen_for_messages(self,
|
|
213
|
+
async def _listen_for_messages(self, client: Client) -> None:
|
|
231
214
|
"""Listen for incoming WebSocket messages from the subprocess."""
|
|
232
215
|
logger.info("Starting to listen for WebSocket messages")
|
|
233
216
|
|
|
234
|
-
# Subscribe to the session topic to receive messages
|
|
235
217
|
topic = f"sessions/{self._session_id}/response"
|
|
218
|
+
await client.subscribe(topic)
|
|
236
219
|
|
|
237
220
|
try:
|
|
238
|
-
|
|
239
|
-
ws_connection=ws_connection,
|
|
240
|
-
topic=topic,
|
|
241
|
-
)
|
|
242
|
-
async for message in ws_connection:
|
|
221
|
+
async for message in client.messages:
|
|
243
222
|
if self._shutdown_event and self._shutdown_event.is_set():
|
|
244
223
|
logger.info("Shutdown requested, ending message listener")
|
|
245
224
|
break
|
|
246
225
|
|
|
247
226
|
try:
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
await self._process_event(data)
|
|
227
|
+
logger.debug("Received WebSocket message: %s", message.get("type"))
|
|
228
|
+
await self._process_event(message)
|
|
251
229
|
|
|
252
|
-
except json.JSONDecodeError:
|
|
253
|
-
logger.warning("Failed to parse WebSocket message: %s", message)
|
|
254
230
|
except Exception:
|
|
255
231
|
logger.exception("Error processing WebSocket message")
|
|
256
232
|
|
|
@@ -285,7 +261,12 @@ class SubprocessWorkflowExecutor(LocalSessionWorkflowExecutor, PythonSubprocessE
|
|
|
285
261
|
|
|
286
262
|
if isinstance(ex_event.payload, ControlFlowResolvedEvent):
|
|
287
263
|
logger.info("Workflow execution completed successfully")
|
|
288
|
-
|
|
264
|
+
# Store both parameter output values and unique UUID values for deserialization
|
|
265
|
+
result = {
|
|
266
|
+
"parameter_output_values": ex_event.payload.parameter_output_values,
|
|
267
|
+
"unique_parameter_uuid_to_values": ex_event.payload.unique_parameter_uuid_to_values,
|
|
268
|
+
}
|
|
269
|
+
self.output = {ex_event.payload.end_node_name: result}
|
|
289
270
|
|
|
290
271
|
if isinstance(ex_event.payload, ControlFlowCancelledEvent):
|
|
291
272
|
logger.error("Workflow execution cancelled")
|