griptape-nodes 0.70.1__py3-none-any.whl → 0.72.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/api_client/client.py +8 -5
- griptape_nodes/app/app.py +4 -0
- griptape_nodes/bootstrap/utils/python_subprocess_executor.py +48 -9
- griptape_nodes/bootstrap/utils/subprocess_websocket_base.py +88 -0
- griptape_nodes/bootstrap/utils/subprocess_websocket_listener.py +126 -0
- griptape_nodes/bootstrap/utils/subprocess_websocket_sender.py +121 -0
- griptape_nodes/bootstrap/workflow_executors/local_session_workflow_executor.py +17 -170
- griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +10 -1
- griptape_nodes/bootstrap/workflow_executors/subprocess_workflow_executor.py +13 -117
- griptape_nodes/bootstrap/workflow_executors/utils/subprocess_script.py +4 -0
- griptape_nodes/bootstrap/workflow_publishers/local_session_workflow_publisher.py +206 -0
- griptape_nodes/bootstrap/workflow_publishers/subprocess_workflow_publisher.py +22 -3
- griptape_nodes/bootstrap/workflow_publishers/utils/subprocess_script.py +49 -25
- griptape_nodes/common/node_executor.py +61 -14
- griptape_nodes/drivers/image_metadata/__init__.py +21 -0
- griptape_nodes/drivers/image_metadata/base_image_metadata_driver.py +63 -0
- griptape_nodes/drivers/image_metadata/exif_metadata_driver.py +218 -0
- griptape_nodes/drivers/image_metadata/image_metadata_driver_registry.py +55 -0
- griptape_nodes/drivers/image_metadata/png_metadata_driver.py +71 -0
- griptape_nodes/drivers/storage/base_storage_driver.py +32 -0
- griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +384 -10
- griptape_nodes/drivers/storage/local_storage_driver.py +65 -4
- griptape_nodes/drivers/thread_storage/local_thread_storage_driver.py +1 -0
- griptape_nodes/exe_types/base_iterative_nodes.py +1 -1
- griptape_nodes/exe_types/node_groups/base_node_group.py +3 -0
- griptape_nodes/exe_types/node_groups/subflow_node_group.py +18 -0
- griptape_nodes/exe_types/node_types.py +13 -0
- griptape_nodes/exe_types/param_components/log_parameter.py +4 -4
- griptape_nodes/exe_types/param_components/subflow_execution_component.py +329 -0
- griptape_nodes/exe_types/param_types/parameter_audio.py +17 -2
- griptape_nodes/exe_types/param_types/parameter_float.py +4 -4
- griptape_nodes/exe_types/param_types/parameter_image.py +14 -1
- griptape_nodes/exe_types/param_types/parameter_int.py +4 -4
- griptape_nodes/exe_types/param_types/parameter_number.py +12 -14
- griptape_nodes/exe_types/param_types/parameter_three_d.py +14 -1
- griptape_nodes/exe_types/param_types/parameter_video.py +17 -2
- griptape_nodes/node_library/workflow_registry.py +5 -8
- griptape_nodes/retained_mode/events/app_events.py +1 -0
- griptape_nodes/retained_mode/events/base_events.py +42 -26
- griptape_nodes/retained_mode/events/flow_events.py +67 -0
- griptape_nodes/retained_mode/events/library_events.py +1 -1
- griptape_nodes/retained_mode/events/node_events.py +1 -0
- griptape_nodes/retained_mode/events/os_events.py +22 -0
- griptape_nodes/retained_mode/events/static_file_events.py +28 -4
- griptape_nodes/retained_mode/managers/flow_manager.py +134 -0
- griptape_nodes/retained_mode/managers/image_metadata_injector.py +339 -0
- griptape_nodes/retained_mode/managers/library_manager.py +71 -41
- griptape_nodes/retained_mode/managers/model_manager.py +1 -0
- griptape_nodes/retained_mode/managers/node_manager.py +8 -5
- griptape_nodes/retained_mode/managers/os_manager.py +270 -33
- griptape_nodes/retained_mode/managers/project_manager.py +3 -7
- griptape_nodes/retained_mode/managers/session_manager.py +1 -0
- griptape_nodes/retained_mode/managers/settings.py +5 -0
- griptape_nodes/retained_mode/managers/static_files_manager.py +83 -17
- griptape_nodes/retained_mode/managers/workflow_manager.py +71 -41
- griptape_nodes/servers/static.py +31 -0
- griptape_nodes/utils/__init__.py +9 -1
- griptape_nodes/utils/artifact_normalization.py +245 -0
- griptape_nodes/utils/file_utils.py +13 -13
- griptape_nodes/utils/http_file_patch.py +613 -0
- griptape_nodes/utils/image_preview.py +27 -0
- griptape_nodes/utils/path_utils.py +58 -0
- griptape_nodes/utils/url_utils.py +106 -0
- {griptape_nodes-0.70.1.dist-info → griptape_nodes-0.72.0.dist-info}/METADATA +2 -1
- {griptape_nodes-0.70.1.dist-info → griptape_nodes-0.72.0.dist-info}/RECORD +67 -52
- {griptape_nodes-0.70.1.dist-info → griptape_nodes-0.72.0.dist-info}/WHEEL +1 -1
- {griptape_nodes-0.70.1.dist-info → griptape_nodes-0.72.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
-
import json
|
|
5
4
|
import logging
|
|
6
|
-
import threading
|
|
7
5
|
from typing import TYPE_CHECKING, Any, Self
|
|
8
6
|
|
|
9
|
-
from griptape_nodes.
|
|
10
|
-
from griptape_nodes.app.app import WebSocketMessage
|
|
7
|
+
from griptape_nodes.bootstrap.utils.subprocess_websocket_sender import SubprocessWebSocketSenderMixin
|
|
11
8
|
from griptape_nodes.bootstrap.workflow_executors.local_workflow_executor import (
|
|
12
9
|
LocalExecutorError,
|
|
13
10
|
LocalWorkflowExecutor,
|
|
@@ -20,10 +17,12 @@ from griptape_nodes.retained_mode.events.base_events import (
|
|
|
20
17
|
EventResultSuccess,
|
|
21
18
|
ExecutionEvent,
|
|
22
19
|
ExecutionGriptapeNodeEvent,
|
|
20
|
+
ProgressEvent,
|
|
23
21
|
ResultPayload,
|
|
24
22
|
)
|
|
25
23
|
from griptape_nodes.retained_mode.events.execution_events import (
|
|
26
24
|
ControlFlowCancelledEvent,
|
|
25
|
+
GriptapeEvent,
|
|
27
26
|
StartFlowRequest,
|
|
28
27
|
StartFlowResultFailure,
|
|
29
28
|
)
|
|
@@ -36,7 +35,7 @@ if TYPE_CHECKING:
|
|
|
36
35
|
logger = logging.getLogger(__name__)
|
|
37
36
|
|
|
38
37
|
|
|
39
|
-
class LocalSessionWorkflowExecutor(LocalWorkflowExecutor):
|
|
38
|
+
class LocalSessionWorkflowExecutor(LocalWorkflowExecutor, SubprocessWebSocketSenderMixin):
|
|
40
39
|
def __init__(
|
|
41
40
|
self,
|
|
42
41
|
session_id: str,
|
|
@@ -44,13 +43,8 @@ class LocalSessionWorkflowExecutor(LocalWorkflowExecutor):
|
|
|
44
43
|
on_start_flow_result: Callable[[ResultPayload], None] | None = None,
|
|
45
44
|
):
|
|
46
45
|
super().__init__(storage_backend=storage_backend)
|
|
47
|
-
self.
|
|
46
|
+
self._init_websocket_sender(session_id)
|
|
48
47
|
self._on_start_flow_result = on_start_flow_result
|
|
49
|
-
self._websocket_thread: threading.Thread | None = None
|
|
50
|
-
self._websocket_event_loop: asyncio.AbstractEventLoop | None = None
|
|
51
|
-
self._websocket_event_loop_ready = threading.Event()
|
|
52
|
-
self._ws_outgoing_queue: asyncio.Queue | None = None
|
|
53
|
-
self._shutdown_event: asyncio.Event | None = None
|
|
54
48
|
|
|
55
49
|
async def __aenter__(self) -> Self:
|
|
56
50
|
"""Async context manager entry: initialize queue and broadcast app initialization."""
|
|
@@ -71,37 +65,12 @@ class LocalSessionWorkflowExecutor(LocalWorkflowExecutor):
|
|
|
71
65
|
exc_tb: TracebackType | None,
|
|
72
66
|
) -> None:
|
|
73
67
|
"""Async context manager exit."""
|
|
74
|
-
self.
|
|
68
|
+
await self._stop_websocket_connection()
|
|
75
69
|
|
|
76
70
|
GriptapeNodes.SessionManager().remove_session(self._session_id)
|
|
77
71
|
|
|
78
72
|
# TODO: Broadcast shutdown https://github.com/griptape-ai/griptape-nodes/issues/2149
|
|
79
73
|
|
|
80
|
-
def _stop_websocket_thread(self) -> None:
|
|
81
|
-
"""Stop the websocket thread."""
|
|
82
|
-
if self._websocket_thread is None or not self._websocket_thread.is_alive():
|
|
83
|
-
logger.debug("No websocket thread to stop")
|
|
84
|
-
return
|
|
85
|
-
|
|
86
|
-
logger.debug("Stopping websocket thread")
|
|
87
|
-
self._websocket_event_loop_ready.clear()
|
|
88
|
-
|
|
89
|
-
# Signal shutdown to the websocket tasks
|
|
90
|
-
if self._websocket_event_loop and self._websocket_event_loop.is_running() and self._shutdown_event:
|
|
91
|
-
|
|
92
|
-
def signal_shutdown() -> None:
|
|
93
|
-
if self._shutdown_event:
|
|
94
|
-
self._shutdown_event.set()
|
|
95
|
-
|
|
96
|
-
self._websocket_event_loop.call_soon_threadsafe(signal_shutdown)
|
|
97
|
-
|
|
98
|
-
# Wait for thread to finish
|
|
99
|
-
self._websocket_thread.join(timeout=5.0)
|
|
100
|
-
if self._websocket_thread.is_alive():
|
|
101
|
-
logger.warning("Websocket thread did not stop gracefully")
|
|
102
|
-
else:
|
|
103
|
-
logger.info("Websocket thread stopped successfully")
|
|
104
|
-
|
|
105
74
|
async def _process_execution_event_async(self, event: ExecutionGriptapeNodeEvent) -> None:
|
|
106
75
|
"""Process execution events asynchronously for real-time websocket emission."""
|
|
107
76
|
logger.debug("REAL-TIME: Processing execution event for session %s", self._session_id)
|
|
@@ -144,7 +113,7 @@ class LocalSessionWorkflowExecutor(LocalWorkflowExecutor):
|
|
|
144
113
|
await asyncio.sleep(1)
|
|
145
114
|
raise LocalExecutorError(msg) from e
|
|
146
115
|
finally:
|
|
147
|
-
self.
|
|
116
|
+
await self._stop_websocket_connection()
|
|
148
117
|
|
|
149
118
|
async def _arun( # noqa: C901, PLR0915
|
|
150
119
|
self,
|
|
@@ -234,6 +203,16 @@ class LocalSessionWorkflowExecutor(LocalWorkflowExecutor):
|
|
|
234
203
|
background_tasks.add(task)
|
|
235
204
|
task.add_done_callback(_handle_task_done)
|
|
236
205
|
is_flow_finished, error = await self._handle_execution_event(event, flow_name)
|
|
206
|
+
elif isinstance(event, ProgressEvent):
|
|
207
|
+
# Convert ProgressEvent to GriptapeEvent and emit via WebSocket
|
|
208
|
+
payload = GriptapeEvent(
|
|
209
|
+
node_name=event.node_name,
|
|
210
|
+
parameter_name=event.parameter_name,
|
|
211
|
+
type=type(event).__name__,
|
|
212
|
+
value=event.value,
|
|
213
|
+
)
|
|
214
|
+
execution_event = ExecutionEvent(payload=payload)
|
|
215
|
+
self.send_event("execution_event", execution_event.json())
|
|
237
216
|
|
|
238
217
|
event_queue.task_done()
|
|
239
218
|
|
|
@@ -251,135 +230,3 @@ class LocalSessionWorkflowExecutor(LocalWorkflowExecutor):
|
|
|
251
230
|
|
|
252
231
|
if error is not None:
|
|
253
232
|
raise error
|
|
254
|
-
|
|
255
|
-
async def _start_websocket_connection(self) -> None:
|
|
256
|
-
"""Start websocket connection in a background thread for event emission."""
|
|
257
|
-
logger.info("Starting websocket connection for session %s", self._session_id)
|
|
258
|
-
self._websocket_thread = threading.Thread(target=self._start_websocket_thread, daemon=True)
|
|
259
|
-
self._websocket_thread.start()
|
|
260
|
-
|
|
261
|
-
if self._websocket_event_loop_ready.wait(timeout=10):
|
|
262
|
-
logger.info("Websocket thread ready")
|
|
263
|
-
await asyncio.sleep(1) # Brief wait for connection to establish
|
|
264
|
-
else:
|
|
265
|
-
logger.error("Timeout waiting for websocket thread to start")
|
|
266
|
-
|
|
267
|
-
def _start_websocket_thread(self) -> None:
|
|
268
|
-
"""Run WebSocket tasks in a separate thread with its own async loop."""
|
|
269
|
-
try:
|
|
270
|
-
# Create a new event loop for this thread
|
|
271
|
-
loop = asyncio.new_event_loop()
|
|
272
|
-
self._websocket_event_loop = loop
|
|
273
|
-
asyncio.set_event_loop(loop)
|
|
274
|
-
|
|
275
|
-
# Create the outgoing queue and shutdown event
|
|
276
|
-
self._ws_outgoing_queue = asyncio.Queue()
|
|
277
|
-
self._shutdown_event = asyncio.Event()
|
|
278
|
-
|
|
279
|
-
# Signal that websocket_event_loop is ready
|
|
280
|
-
self._websocket_event_loop_ready.set()
|
|
281
|
-
logger.info("Websocket thread started and ready")
|
|
282
|
-
|
|
283
|
-
# Run the async WebSocket tasks
|
|
284
|
-
loop.run_until_complete(self._run_websocket_tasks())
|
|
285
|
-
except Exception as e:
|
|
286
|
-
logger.error("WebSocket thread error: %s", e)
|
|
287
|
-
finally:
|
|
288
|
-
self._websocket_event_loop = None
|
|
289
|
-
self._websocket_event_loop_ready.clear()
|
|
290
|
-
self._shutdown_event = None
|
|
291
|
-
logger.info("Websocket thread ended")
|
|
292
|
-
|
|
293
|
-
async def _run_websocket_tasks(self) -> None:
|
|
294
|
-
"""Run websocket tasks - establish connection and handle outgoing messages."""
|
|
295
|
-
logger.info("Creating Client for session %s", self._session_id)
|
|
296
|
-
|
|
297
|
-
async with Client() as client:
|
|
298
|
-
logger.info("WebSocket connection established for session %s", self._session_id)
|
|
299
|
-
|
|
300
|
-
try:
|
|
301
|
-
await self._send_outgoing_messages(client)
|
|
302
|
-
except Exception:
|
|
303
|
-
logger.exception("WebSocket tasks failed")
|
|
304
|
-
finally:
|
|
305
|
-
logger.info("WebSocket connection loop ended")
|
|
306
|
-
|
|
307
|
-
async def _send_outgoing_messages(self, client: Client) -> None:
|
|
308
|
-
"""Send outgoing WebSocket messages from queue - matches app.py pattern exactly."""
|
|
309
|
-
if self._ws_outgoing_queue is None:
|
|
310
|
-
logger.error("No outgoing queue available")
|
|
311
|
-
return
|
|
312
|
-
|
|
313
|
-
logger.debug("Starting outgoing WebSocket request sender")
|
|
314
|
-
|
|
315
|
-
while True:
|
|
316
|
-
# Check if shutdown was requested
|
|
317
|
-
if self._shutdown_event and self._shutdown_event.is_set():
|
|
318
|
-
logger.info("Shutdown requested, ending message sender")
|
|
319
|
-
break
|
|
320
|
-
|
|
321
|
-
try:
|
|
322
|
-
# Get message from outgoing queue with timeout to allow shutdown checks
|
|
323
|
-
message = await asyncio.wait_for(self._ws_outgoing_queue.get(), timeout=1.0)
|
|
324
|
-
except TimeoutError:
|
|
325
|
-
# No message in queue, continue to check for shutdown
|
|
326
|
-
continue
|
|
327
|
-
|
|
328
|
-
try:
|
|
329
|
-
if isinstance(message, WebSocketMessage):
|
|
330
|
-
topic = message.topic if message.topic else f"sessions/{self._session_id}/response"
|
|
331
|
-
payload_dict = json.loads(message.payload)
|
|
332
|
-
await client.publish(message.event_type, payload_dict, topic)
|
|
333
|
-
logger.debug("DELIVERED: %s event", message.event_type)
|
|
334
|
-
else:
|
|
335
|
-
logger.warning("Unknown outgoing message type: %s", type(message))
|
|
336
|
-
except Exception as e:
|
|
337
|
-
logger.error("Error sending outgoing WebSocket request: %s", e)
|
|
338
|
-
finally:
|
|
339
|
-
self._ws_outgoing_queue.task_done()
|
|
340
|
-
|
|
341
|
-
def send_event(self, event_type: str, payload: str) -> None:
|
|
342
|
-
"""Send an event via websocket if connected - thread-safe version."""
|
|
343
|
-
# Wait for websocket event loop to be ready
|
|
344
|
-
if not self._websocket_event_loop_ready.wait(timeout=1.0):
|
|
345
|
-
logger.debug("Websocket not ready, event not sent: %s", event_type)
|
|
346
|
-
return
|
|
347
|
-
|
|
348
|
-
# Use run_coroutine_threadsafe to put message into WebSocket background thread queue
|
|
349
|
-
if self._websocket_event_loop is None:
|
|
350
|
-
logger.debug("WebSocket event loop not available for message: %s", event_type)
|
|
351
|
-
return
|
|
352
|
-
|
|
353
|
-
topic = f"sessions/{self._session_id}/response"
|
|
354
|
-
message = WebSocketMessage(event_type, payload, topic)
|
|
355
|
-
|
|
356
|
-
if self._ws_outgoing_queue is None:
|
|
357
|
-
logger.debug("No websocket queue available for event: %s", event_type)
|
|
358
|
-
return
|
|
359
|
-
|
|
360
|
-
try:
|
|
361
|
-
asyncio.run_coroutine_threadsafe(self._ws_outgoing_queue.put(message), self._websocket_event_loop)
|
|
362
|
-
logger.debug("SENT: %s event via websocket", event_type)
|
|
363
|
-
except Exception as e:
|
|
364
|
-
logger.error("Failed to queue event %s: %s", event_type, e)
|
|
365
|
-
|
|
366
|
-
async def _wait_for_websocket_queue_flush(self, timeout_seconds: float = 5.0) -> None:
|
|
367
|
-
"""Wait for all websocket messages to be sent."""
|
|
368
|
-
if self._ws_outgoing_queue is None or self._websocket_event_loop is None:
|
|
369
|
-
return
|
|
370
|
-
|
|
371
|
-
async def _check_queue_empty() -> bool:
|
|
372
|
-
return self._ws_outgoing_queue.empty() if self._ws_outgoing_queue else True
|
|
373
|
-
|
|
374
|
-
start_time = asyncio.get_event_loop().time()
|
|
375
|
-
while asyncio.get_event_loop().time() - start_time < timeout_seconds:
|
|
376
|
-
future = asyncio.run_coroutine_threadsafe(_check_queue_empty(), self._websocket_event_loop)
|
|
377
|
-
try:
|
|
378
|
-
is_empty = future.result(timeout=0.1)
|
|
379
|
-
if is_empty:
|
|
380
|
-
return
|
|
381
|
-
except Exception as e:
|
|
382
|
-
logger.debug("Error checking queue status: %s", e)
|
|
383
|
-
await asyncio.sleep(0.1)
|
|
384
|
-
|
|
385
|
-
logger.warning("Timeout waiting for websocket queue to flush")
|
|
@@ -32,14 +32,23 @@ class LocalWorkflowExecutor(WorkflowExecutor):
|
|
|
32
32
|
def __init__(
|
|
33
33
|
self,
|
|
34
34
|
storage_backend: StorageBackend = StorageBackend.LOCAL,
|
|
35
|
+
*,
|
|
36
|
+
skip_library_loading: bool = False,
|
|
37
|
+
workflows_to_register: list[str] | None = None,
|
|
35
38
|
):
|
|
36
39
|
super().__init__()
|
|
37
40
|
self._set_storage_backend(storage_backend=storage_backend)
|
|
41
|
+
self._skip_library_loading = skip_library_loading
|
|
42
|
+
self._workflows_to_register = workflows_to_register or []
|
|
38
43
|
|
|
39
44
|
async def __aenter__(self) -> Self:
|
|
40
45
|
"""Async context manager entry: initialize queue and broadcast app initialization."""
|
|
41
46
|
GriptapeNodes.EventManager().initialize_queue()
|
|
42
|
-
await GriptapeNodes.EventManager().broadcast_app_event(
|
|
47
|
+
await GriptapeNodes.EventManager().broadcast_app_event(
|
|
48
|
+
AppInitializationComplete(
|
|
49
|
+
skip_library_loading=self._skip_library_loading, workflows_to_register=self._workflows_to_register
|
|
50
|
+
)
|
|
51
|
+
)
|
|
43
52
|
return self
|
|
44
53
|
|
|
45
54
|
async def __aexit__(
|
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import asyncio
|
|
4
3
|
import json
|
|
5
4
|
import logging
|
|
6
5
|
import tempfile
|
|
7
|
-
import threading
|
|
8
|
-
import uuid
|
|
9
6
|
from pathlib import Path
|
|
10
7
|
from typing import TYPE_CHECKING, Any, Self
|
|
11
8
|
|
|
12
9
|
import anyio
|
|
13
10
|
|
|
14
|
-
from griptape_nodes.api_client import Client
|
|
15
11
|
from griptape_nodes.bootstrap.utils.python_subprocess_executor import PythonSubprocessExecutor
|
|
16
|
-
from griptape_nodes.bootstrap.
|
|
12
|
+
from griptape_nodes.bootstrap.utils.subprocess_websocket_listener import SubprocessWebSocketListenerMixin
|
|
13
|
+
from griptape_nodes.bootstrap.workflow_executors.workflow_executor import WorkflowExecutor
|
|
17
14
|
from griptape_nodes.drivers.storage import StorageBackend
|
|
18
15
|
from griptape_nodes.retained_mode.events.base_events import (
|
|
19
16
|
EventResultFailure,
|
|
@@ -39,28 +36,23 @@ class SubprocessWorkflowExecutorError(Exception):
|
|
|
39
36
|
"""Exception raised during subprocess workflow execution."""
|
|
40
37
|
|
|
41
38
|
|
|
42
|
-
class SubprocessWorkflowExecutor(
|
|
39
|
+
class SubprocessWorkflowExecutor(WorkflowExecutor, PythonSubprocessExecutor, SubprocessWebSocketListenerMixin):
|
|
43
40
|
def __init__(
|
|
44
41
|
self,
|
|
45
42
|
workflow_path: str,
|
|
46
43
|
on_start_flow_result: Callable[[ResultPayload], None] | None = None,
|
|
44
|
+
on_event: Callable[[dict], None] | None = None,
|
|
47
45
|
session_id: str | None = None,
|
|
48
46
|
) -> None:
|
|
47
|
+
WorkflowExecutor.__init__(self)
|
|
49
48
|
PythonSubprocessExecutor.__init__(self)
|
|
49
|
+
self._init_websocket_listener(session_id=session_id, on_event=on_event)
|
|
50
50
|
self._workflow_path = workflow_path
|
|
51
51
|
self._on_start_flow_result = on_start_flow_result
|
|
52
|
-
# Generate a unique session ID for this execution
|
|
53
|
-
self._session_id = session_id or uuid.uuid4().hex
|
|
54
|
-
self._websocket_thread: threading.Thread | None = None
|
|
55
|
-
self._websocket_event_loop: asyncio.AbstractEventLoop | None = None
|
|
56
|
-
self._websocket_event_loop_ready = threading.Event()
|
|
57
|
-
self._event_handlers: dict[str, list] = {}
|
|
58
|
-
self._shutdown_event: asyncio.Event | None = None
|
|
59
52
|
self._stored_exception: SubprocessWorkflowExecutorError | None = None
|
|
60
53
|
|
|
61
54
|
async def __aenter__(self) -> Self:
|
|
62
|
-
"""Async context manager entry: start WebSocket
|
|
63
|
-
logger.info("Starting WebSocket listener for session %s", self._session_id)
|
|
55
|
+
"""Async context manager entry: start WebSocket listener."""
|
|
64
56
|
await self._start_websocket_listener()
|
|
65
57
|
return self
|
|
66
58
|
|
|
@@ -70,9 +62,8 @@ class SubprocessWorkflowExecutor(LocalSessionWorkflowExecutor, PythonSubprocessE
|
|
|
70
62
|
exc_val: BaseException | None,
|
|
71
63
|
exc_tb: TracebackType | None,
|
|
72
64
|
) -> None:
|
|
73
|
-
"""Async context manager exit: stop WebSocket
|
|
74
|
-
|
|
75
|
-
self._stop_websocket_listener()
|
|
65
|
+
"""Async context manager exit: stop WebSocket listener."""
|
|
66
|
+
await self._stop_websocket_listener()
|
|
76
67
|
|
|
77
68
|
async def arun(
|
|
78
69
|
self,
|
|
@@ -138,106 +129,11 @@ class SubprocessWorkflowExecutor(LocalSessionWorkflowExecutor, PythonSubprocessE
|
|
|
138
129
|
if self._stored_exception:
|
|
139
130
|
raise self._stored_exception
|
|
140
131
|
|
|
141
|
-
async def
|
|
142
|
-
"""
|
|
143
|
-
logger.info("Starting WebSocket listener for session %s", self._session_id)
|
|
144
|
-
self._websocket_thread = threading.Thread(target=self._start_websocket_thread, daemon=True)
|
|
145
|
-
self._websocket_thread.start()
|
|
132
|
+
async def _handle_subprocess_event(self, event: dict) -> None:
|
|
133
|
+
"""Handle executor-specific events from the subprocess.
|
|
146
134
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
else:
|
|
150
|
-
logger.error("Timeout waiting for WebSocket listener thread to start")
|
|
151
|
-
|
|
152
|
-
def _stop_websocket_listener(self) -> None:
|
|
153
|
-
"""Stop the WebSocket listener thread."""
|
|
154
|
-
if self._websocket_thread is None or not self._websocket_thread.is_alive():
|
|
155
|
-
return
|
|
156
|
-
|
|
157
|
-
logger.info("Stopping WebSocket listener thread")
|
|
158
|
-
self._websocket_event_loop_ready.clear()
|
|
159
|
-
|
|
160
|
-
# Signal shutdown to the websocket tasks
|
|
161
|
-
if self._websocket_event_loop and self._websocket_event_loop.is_running() and self._shutdown_event:
|
|
162
|
-
|
|
163
|
-
def signal_shutdown() -> None:
|
|
164
|
-
if self._shutdown_event:
|
|
165
|
-
self._shutdown_event.set()
|
|
166
|
-
|
|
167
|
-
self._websocket_event_loop.call_soon_threadsafe(signal_shutdown)
|
|
168
|
-
|
|
169
|
-
# Wait for thread to finish
|
|
170
|
-
self._websocket_thread.join(timeout=5.0)
|
|
171
|
-
if self._websocket_thread.is_alive():
|
|
172
|
-
logger.warning("WebSocket listener thread did not stop gracefully")
|
|
173
|
-
else:
|
|
174
|
-
logger.info("WebSocket listener thread stopped successfully")
|
|
175
|
-
|
|
176
|
-
def _start_websocket_thread(self) -> None:
|
|
177
|
-
"""Run WebSocket tasks in a separate thread with its own async loop."""
|
|
178
|
-
try:
|
|
179
|
-
# Create a new event loop for this thread
|
|
180
|
-
loop = asyncio.new_event_loop()
|
|
181
|
-
self._websocket_event_loop = loop
|
|
182
|
-
asyncio.set_event_loop(loop)
|
|
183
|
-
|
|
184
|
-
# Create shutdown event
|
|
185
|
-
self._shutdown_event = asyncio.Event()
|
|
186
|
-
|
|
187
|
-
# Signal that websocket_event_loop is ready
|
|
188
|
-
self._websocket_event_loop_ready.set()
|
|
189
|
-
logger.info("WebSocket listener thread started and ready")
|
|
190
|
-
|
|
191
|
-
# Run the async WebSocket listener
|
|
192
|
-
loop.run_until_complete(self._run_websocket_listener())
|
|
193
|
-
except Exception as e:
|
|
194
|
-
logger.error("WebSocket listener thread error: %s", e)
|
|
195
|
-
finally:
|
|
196
|
-
self._websocket_event_loop = None
|
|
197
|
-
self._websocket_event_loop_ready.clear()
|
|
198
|
-
self._shutdown_event = None
|
|
199
|
-
logger.info("WebSocket listener thread ended")
|
|
200
|
-
|
|
201
|
-
async def _run_websocket_listener(self) -> None:
|
|
202
|
-
"""Run WebSocket listener - establish connection and handle incoming messages."""
|
|
203
|
-
logger.info("Creating Client for listening on session %s", self._session_id)
|
|
204
|
-
|
|
205
|
-
async with Client() as client:
|
|
206
|
-
logger.info("WebSocket connection established for session %s", self._session_id)
|
|
207
|
-
|
|
208
|
-
try:
|
|
209
|
-
await self._listen_for_messages(client)
|
|
210
|
-
except Exception:
|
|
211
|
-
logger.exception("WebSocket listener failed")
|
|
212
|
-
finally:
|
|
213
|
-
logger.info("WebSocket listener connection loop ended")
|
|
214
|
-
|
|
215
|
-
async def _listen_for_messages(self, client: Client) -> None:
|
|
216
|
-
"""Listen for incoming WebSocket messages from the subprocess."""
|
|
217
|
-
logger.info("Starting to listen for WebSocket messages")
|
|
218
|
-
|
|
219
|
-
topic = f"sessions/{self._session_id}/response"
|
|
220
|
-
await client.subscribe(topic)
|
|
221
|
-
|
|
222
|
-
try:
|
|
223
|
-
async for message in client.messages:
|
|
224
|
-
if self._shutdown_event and self._shutdown_event.is_set():
|
|
225
|
-
logger.info("Shutdown requested, ending message listener")
|
|
226
|
-
break
|
|
227
|
-
|
|
228
|
-
try:
|
|
229
|
-
logger.debug("Received WebSocket message: %s", message.get("type"))
|
|
230
|
-
await self._process_event(message)
|
|
231
|
-
|
|
232
|
-
except Exception:
|
|
233
|
-
logger.exception("Error processing WebSocket message")
|
|
234
|
-
|
|
235
|
-
except Exception as e:
|
|
236
|
-
logger.error("Error in WebSocket message listener: %s", e)
|
|
237
|
-
raise
|
|
238
|
-
|
|
239
|
-
async def _process_event(self, event: dict) -> None:
|
|
240
|
-
"""Process events received from the subprocess via WebSocket."""
|
|
135
|
+
Processes execution events and result events.
|
|
136
|
+
"""
|
|
241
137
|
event_type = event.get("type", "unknown")
|
|
242
138
|
if event_type == "execution_event":
|
|
243
139
|
await self._process_execution_event(event)
|
|
@@ -10,6 +10,10 @@ from workflow import execute_workflow # type: ignore[attr-defined]
|
|
|
10
10
|
|
|
11
11
|
from griptape_nodes.bootstrap.workflow_executors.local_session_workflow_executor import LocalSessionWorkflowExecutor
|
|
12
12
|
from griptape_nodes.drivers.storage import StorageBackend
|
|
13
|
+
from griptape_nodes.utils import install_file_url_support
|
|
14
|
+
|
|
15
|
+
# Install file:// URL support for httpx/requests in subprocess
|
|
16
|
+
install_file_url_support()
|
|
13
17
|
|
|
14
18
|
|
|
15
19
|
def _main() -> None:
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Local session workflow publisher with WebSocket event emission support.
|
|
2
|
+
|
|
3
|
+
This module provides a workflow publisher that emits events over WebSocket
|
|
4
|
+
for real-time progress updates during the publishing process.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import logging
|
|
11
|
+
from typing import TYPE_CHECKING, Any, Self
|
|
12
|
+
|
|
13
|
+
from griptape_nodes.bootstrap.utils.subprocess_websocket_sender import SubprocessWebSocketSenderMixin
|
|
14
|
+
from griptape_nodes.bootstrap.workflow_publishers.local_workflow_publisher import (
|
|
15
|
+
LocalPublisherError,
|
|
16
|
+
LocalWorkflowPublisher,
|
|
17
|
+
)
|
|
18
|
+
from griptape_nodes.retained_mode.events.app_events import AppInitializationComplete
|
|
19
|
+
from griptape_nodes.retained_mode.events.base_events import (
|
|
20
|
+
EventResultFailure,
|
|
21
|
+
EventResultSuccess,
|
|
22
|
+
ExecutionEvent,
|
|
23
|
+
ExecutionGriptapeNodeEvent,
|
|
24
|
+
ResultPayload,
|
|
25
|
+
)
|
|
26
|
+
from griptape_nodes.retained_mode.events.workflow_events import (
|
|
27
|
+
PublishWorkflowProgressEvent,
|
|
28
|
+
PublishWorkflowRequest,
|
|
29
|
+
PublishWorkflowResultFailure,
|
|
30
|
+
)
|
|
31
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from types import TracebackType
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class LocalSessionWorkflowPublisher(LocalWorkflowPublisher, SubprocessWebSocketSenderMixin):
|
|
40
|
+
"""Publisher with WebSocket support for sending events to parent process.
|
|
41
|
+
|
|
42
|
+
This publisher is used inside subprocesses to emit publishing progress events
|
|
43
|
+
over WebSocket back to the parent process.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, session_id: str) -> None:
|
|
47
|
+
super().__init__()
|
|
48
|
+
self._init_websocket_sender(session_id)
|
|
49
|
+
|
|
50
|
+
async def __aenter__(self) -> Self:
|
|
51
|
+
"""Async context manager entry: initialize queue and start WebSocket connection."""
|
|
52
|
+
GriptapeNodes.EventManager().initialize_queue()
|
|
53
|
+
await GriptapeNodes.EventManager().broadcast_app_event(AppInitializationComplete())
|
|
54
|
+
|
|
55
|
+
logger.info("Setting up publishing session %s", self._session_id)
|
|
56
|
+
GriptapeNodes.SessionManager().save_session(self._session_id)
|
|
57
|
+
GriptapeNodes.SessionManager().active_session_id = self._session_id
|
|
58
|
+
await self._start_websocket_connection()
|
|
59
|
+
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
async def __aexit__(
|
|
63
|
+
self,
|
|
64
|
+
exc_type: type[BaseException] | None,
|
|
65
|
+
exc_val: BaseException | None,
|
|
66
|
+
exc_tb: TracebackType | None,
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Async context manager exit."""
|
|
69
|
+
await self._stop_websocket_connection()
|
|
70
|
+
GriptapeNodes.SessionManager().remove_session(self._session_id)
|
|
71
|
+
|
|
72
|
+
async def arun(
|
|
73
|
+
self,
|
|
74
|
+
workflow_name: str,
|
|
75
|
+
workflow_path: str,
|
|
76
|
+
publisher_name: str,
|
|
77
|
+
published_workflow_file_name: str,
|
|
78
|
+
**kwargs: Any,
|
|
79
|
+
) -> None:
|
|
80
|
+
"""Run the publish operation with WebSocket event emission enabled.
|
|
81
|
+
|
|
82
|
+
Progress events are emitted by the Library handling the publishing process,
|
|
83
|
+
which uses the GriptapeNodes event infrastructure to emit events that will
|
|
84
|
+
be sent over WebSocket to the parent process.
|
|
85
|
+
"""
|
|
86
|
+
try:
|
|
87
|
+
await self._arun(
|
|
88
|
+
workflow_name=workflow_name,
|
|
89
|
+
workflow_path=workflow_path,
|
|
90
|
+
publisher_name=publisher_name,
|
|
91
|
+
published_workflow_file_name=published_workflow_file_name,
|
|
92
|
+
**kwargs,
|
|
93
|
+
)
|
|
94
|
+
except Exception as e:
|
|
95
|
+
msg = f"Unexpected error during publish: {e}"
|
|
96
|
+
logger.exception(msg)
|
|
97
|
+
raise LocalPublisherError(msg) from e
|
|
98
|
+
finally:
|
|
99
|
+
await self._stop_websocket_connection()
|
|
100
|
+
|
|
101
|
+
async def _arun( # noqa: C901, PLR0915
|
|
102
|
+
self,
|
|
103
|
+
workflow_name: str,
|
|
104
|
+
workflow_path: str,
|
|
105
|
+
publisher_name: str,
|
|
106
|
+
published_workflow_file_name: str,
|
|
107
|
+
**kwargs: Any,
|
|
108
|
+
) -> None:
|
|
109
|
+
"""Internal async run method with event queue monitoring and websocket integration."""
|
|
110
|
+
# Load the workflow into memory
|
|
111
|
+
await self.aprepare_workflow_for_run(flow_input={}, workflow_path=workflow_path)
|
|
112
|
+
|
|
113
|
+
pickle_control_flow_result = kwargs.get("pickle_control_flow_result", False)
|
|
114
|
+
publish_workflow_request = PublishWorkflowRequest(
|
|
115
|
+
workflow_name=workflow_name,
|
|
116
|
+
publisher_name=publisher_name,
|
|
117
|
+
published_workflow_file_name=published_workflow_file_name,
|
|
118
|
+
pickle_control_flow_result=pickle_control_flow_result,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Send the publish request async (fire and forget pattern)
|
|
122
|
+
publish_task = asyncio.create_task(GriptapeNodes.ahandle_request(publish_workflow_request))
|
|
123
|
+
|
|
124
|
+
is_publish_finished = False
|
|
125
|
+
error: Exception | None = None
|
|
126
|
+
background_tasks: set[asyncio.Task] = set()
|
|
127
|
+
|
|
128
|
+
def _handle_publish_result(task: asyncio.Task[ResultPayload]) -> None:
|
|
129
|
+
nonlocal is_publish_finished, error
|
|
130
|
+
try:
|
|
131
|
+
publish_result = task.result()
|
|
132
|
+
|
|
133
|
+
if isinstance(publish_result, PublishWorkflowResultFailure):
|
|
134
|
+
msg = f"Failed to publish workflow: {publish_result.result_details}"
|
|
135
|
+
logger.error(msg)
|
|
136
|
+
event_result_failure = EventResultFailure(request=publish_workflow_request, result=publish_result)
|
|
137
|
+
self.send_event("failure_result", event_result_failure.json())
|
|
138
|
+
is_publish_finished = True
|
|
139
|
+
error = LocalPublisherError(msg)
|
|
140
|
+
else:
|
|
141
|
+
logger.info("Published workflow successfully")
|
|
142
|
+
event_result_success = EventResultSuccess(request=publish_workflow_request, result=publish_result)
|
|
143
|
+
self.send_event("success_result", event_result_success.json())
|
|
144
|
+
is_publish_finished = True
|
|
145
|
+
|
|
146
|
+
except Exception as e:
|
|
147
|
+
msg = "Error during publish workflow"
|
|
148
|
+
logger.exception(msg)
|
|
149
|
+
is_publish_finished = True
|
|
150
|
+
error = e
|
|
151
|
+
# Add a dummy event to wake up the loop in failure cases
|
|
152
|
+
event_queue = GriptapeNodes.EventManager().event_queue
|
|
153
|
+
queue_event_task = asyncio.create_task(event_queue.put(None))
|
|
154
|
+
background_tasks.add(queue_event_task)
|
|
155
|
+
queue_event_task.add_done_callback(background_tasks.discard)
|
|
156
|
+
|
|
157
|
+
publish_task.add_done_callback(_handle_publish_result)
|
|
158
|
+
|
|
159
|
+
logger.info("Publish workflow request sent! Processing events...")
|
|
160
|
+
|
|
161
|
+
def _handle_task_done(task: asyncio.Task) -> None:
|
|
162
|
+
background_tasks.discard(task)
|
|
163
|
+
if task.exception() and not task.cancelled():
|
|
164
|
+
logger.exception("Background task failed", exc_info=task.exception())
|
|
165
|
+
|
|
166
|
+
event_queue = GriptapeNodes.EventManager().event_queue
|
|
167
|
+
while not is_publish_finished:
|
|
168
|
+
try:
|
|
169
|
+
event = await event_queue.get()
|
|
170
|
+
|
|
171
|
+
# Handle the dummy wake up event (None)
|
|
172
|
+
if event is None:
|
|
173
|
+
event_queue.task_done()
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
logger.debug("Processing publish event: %s", type(event).__name__)
|
|
177
|
+
|
|
178
|
+
if isinstance(event, ExecutionGriptapeNodeEvent):
|
|
179
|
+
# Unwrap the event and check if it contains a PublishWorkflowProgressEvent
|
|
180
|
+
wrapped_event = event.wrapped_event
|
|
181
|
+
if isinstance(wrapped_event, ExecutionEvent) and isinstance(
|
|
182
|
+
wrapped_event.payload, PublishWorkflowProgressEvent
|
|
183
|
+
):
|
|
184
|
+
self.send_event("execution_event", wrapped_event.json())
|
|
185
|
+
logger.debug(
|
|
186
|
+
"Emitted progress event: %.1f%% - %s",
|
|
187
|
+
wrapped_event.payload.progress,
|
|
188
|
+
wrapped_event.payload.message,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
event_queue.task_done()
|
|
192
|
+
|
|
193
|
+
except Exception as e:
|
|
194
|
+
msg = f"Error handling queue event: {e}"
|
|
195
|
+
logger.exception(msg)
|
|
196
|
+
error = LocalPublisherError(msg)
|
|
197
|
+
break
|
|
198
|
+
|
|
199
|
+
if background_tasks:
|
|
200
|
+
logger.info("Waiting for %d background tasks to complete", len(background_tasks))
|
|
201
|
+
await asyncio.gather(*background_tasks, return_exceptions=True)
|
|
202
|
+
|
|
203
|
+
await self._wait_for_websocket_queue_flush()
|
|
204
|
+
|
|
205
|
+
if error is not None:
|
|
206
|
+
raise error
|