griptape-nodes 0.56.0__py3-none-any.whl → 0.56.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- griptape_nodes/app/app.py +10 -15
- griptape_nodes/app/watch.py +35 -67
- griptape_nodes/bootstrap/utils/__init__.py +1 -0
- griptape_nodes/bootstrap/utils/python_subprocess_executor.py +122 -0
- griptape_nodes/bootstrap/workflow_executors/local_session_workflow_executor.py +418 -0
- griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +37 -8
- griptape_nodes/bootstrap/workflow_executors/subprocess_workflow_executor.py +326 -0
- griptape_nodes/bootstrap/workflow_executors/utils/__init__.py +1 -0
- griptape_nodes/bootstrap/workflow_executors/utils/subprocess_script.py +51 -0
- griptape_nodes/bootstrap/workflow_publishers/__init__.py +1 -0
- griptape_nodes/bootstrap/workflow_publishers/local_workflow_publisher.py +43 -0
- griptape_nodes/bootstrap/workflow_publishers/subprocess_workflow_publisher.py +84 -0
- griptape_nodes/bootstrap/workflow_publishers/utils/__init__.py +1 -0
- griptape_nodes/bootstrap/workflow_publishers/utils/subprocess_script.py +54 -0
- griptape_nodes/cli/commands/engine.py +4 -15
- griptape_nodes/cli/main.py +6 -1
- griptape_nodes/exe_types/core_types.py +26 -0
- griptape_nodes/exe_types/node_types.py +116 -1
- griptape_nodes/retained_mode/events/agent_events.py +2 -0
- griptape_nodes/retained_mode/events/base_events.py +18 -17
- griptape_nodes/retained_mode/events/execution_events.py +3 -1
- griptape_nodes/retained_mode/events/flow_events.py +5 -7
- griptape_nodes/retained_mode/events/mcp_events.py +363 -0
- griptape_nodes/retained_mode/events/node_events.py +3 -4
- griptape_nodes/retained_mode/griptape_nodes.py +8 -0
- griptape_nodes/retained_mode/managers/agent_manager.py +67 -4
- griptape_nodes/retained_mode/managers/event_manager.py +31 -13
- griptape_nodes/retained_mode/managers/flow_manager.py +76 -44
- griptape_nodes/retained_mode/managers/library_manager.py +7 -9
- griptape_nodes/retained_mode/managers/mcp_manager.py +364 -0
- griptape_nodes/retained_mode/managers/node_manager.py +12 -1
- griptape_nodes/retained_mode/managers/settings.py +40 -0
- griptape_nodes/retained_mode/managers/workflow_manager.py +94 -8
- griptape_nodes/traits/multi_options.py +5 -1
- griptape_nodes/traits/options.py +10 -2
- {griptape_nodes-0.56.0.dist-info → griptape_nodes-0.56.1.dist-info}/METADATA +2 -2
- {griptape_nodes-0.56.0.dist-info → griptape_nodes-0.56.1.dist-info}/RECORD +39 -26
- {griptape_nodes-0.56.0.dist-info → griptape_nodes-0.56.1.dist-info}/WHEEL +0 -0
- {griptape_nodes-0.56.0.dist-info → griptape_nodes-0.56.1.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import threading
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Self
|
|
7
|
+
|
|
8
|
+
from websockets.exceptions import ConnectionClosed, ConnectionClosedError
|
|
9
|
+
|
|
10
|
+
from griptape_nodes.app.app import (
|
|
11
|
+
WebSocketMessage,
|
|
12
|
+
_create_websocket_connection,
|
|
13
|
+
_send_websocket_message,
|
|
14
|
+
)
|
|
15
|
+
from griptape_nodes.bootstrap.workflow_executors.local_workflow_executor import (
|
|
16
|
+
LocalExecutorError,
|
|
17
|
+
LocalWorkflowExecutor,
|
|
18
|
+
)
|
|
19
|
+
from griptape_nodes.drivers.storage import StorageBackend
|
|
20
|
+
from griptape_nodes.retained_mode.events.app_events import AppInitializationComplete
|
|
21
|
+
from griptape_nodes.retained_mode.events.base_events import (
|
|
22
|
+
EventRequest,
|
|
23
|
+
EventResultFailure,
|
|
24
|
+
EventResultSuccess,
|
|
25
|
+
ExecutionEvent,
|
|
26
|
+
ExecutionGriptapeNodeEvent,
|
|
27
|
+
ResultPayload,
|
|
28
|
+
)
|
|
29
|
+
from griptape_nodes.retained_mode.events.execution_events import (
|
|
30
|
+
ControlFlowCancelledEvent,
|
|
31
|
+
StartFlowRequest,
|
|
32
|
+
StartFlowResultFailure,
|
|
33
|
+
)
|
|
34
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from collections.abc import Callable
|
|
38
|
+
from types import TracebackType
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class LocalSessionWorkflowExecutor(LocalWorkflowExecutor):
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
session_id: str,
|
|
47
|
+
storage_backend: StorageBackend = StorageBackend.LOCAL,
|
|
48
|
+
on_start_flow_result: Callable[[ResultPayload], None] | None = None,
|
|
49
|
+
):
|
|
50
|
+
super().__init__(storage_backend=storage_backend)
|
|
51
|
+
self._session_id = session_id
|
|
52
|
+
self._on_start_flow_result = on_start_flow_result
|
|
53
|
+
self._websocket_thread: threading.Thread | None = None
|
|
54
|
+
self._websocket_event_loop: asyncio.AbstractEventLoop | None = None
|
|
55
|
+
self._websocket_event_loop_ready = threading.Event()
|
|
56
|
+
self._ws_outgoing_queue: asyncio.Queue | None = None
|
|
57
|
+
self._shutdown_event: asyncio.Event | None = None
|
|
58
|
+
|
|
59
|
+
async def __aenter__(self) -> Self:
|
|
60
|
+
"""Async context manager entry: initialize queue and broadcast app initialization."""
|
|
61
|
+
GriptapeNodes.EventManager().initialize_queue()
|
|
62
|
+
await GriptapeNodes.EventManager().broadcast_app_event(AppInitializationComplete())
|
|
63
|
+
|
|
64
|
+
logger.info("Setting up session %s", self._session_id)
|
|
65
|
+
GriptapeNodes.SessionManager().save_session(self._session_id)
|
|
66
|
+
GriptapeNodes.SessionManager().set_active_session_id(self._session_id)
|
|
67
|
+
await self._start_websocket_connection()
|
|
68
|
+
|
|
69
|
+
return self
|
|
70
|
+
|
|
71
|
+
async def __aexit__(
|
|
72
|
+
self,
|
|
73
|
+
exc_type: type[BaseException] | None,
|
|
74
|
+
exc_val: BaseException | None,
|
|
75
|
+
exc_tb: TracebackType | None,
|
|
76
|
+
) -> None:
|
|
77
|
+
"""Async context manager exit."""
|
|
78
|
+
self._stop_websocket_thread()
|
|
79
|
+
|
|
80
|
+
GriptapeNodes.SessionManager().remove_session(self._session_id)
|
|
81
|
+
|
|
82
|
+
# TODO: Broadcast shutdown https://github.com/griptape-ai/griptape-nodes/issues/2149
|
|
83
|
+
|
|
84
|
+
def _stop_websocket_thread(self) -> None:
|
|
85
|
+
"""Stop the websocket thread."""
|
|
86
|
+
if self._websocket_thread is None or not self._websocket_thread.is_alive():
|
|
87
|
+
logger.debug("No websocket thread to stop")
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
logger.debug("Stopping websocket thread")
|
|
91
|
+
self._websocket_event_loop_ready.clear()
|
|
92
|
+
|
|
93
|
+
# Signal shutdown to the websocket tasks
|
|
94
|
+
if self._websocket_event_loop and self._websocket_event_loop.is_running() and self._shutdown_event:
|
|
95
|
+
|
|
96
|
+
def signal_shutdown() -> None:
|
|
97
|
+
if self._shutdown_event:
|
|
98
|
+
self._shutdown_event.set()
|
|
99
|
+
|
|
100
|
+
self._websocket_event_loop.call_soon_threadsafe(signal_shutdown)
|
|
101
|
+
|
|
102
|
+
# Wait for thread to finish
|
|
103
|
+
self._websocket_thread.join(timeout=5.0)
|
|
104
|
+
if self._websocket_thread.is_alive():
|
|
105
|
+
logger.warning("Websocket thread did not stop gracefully")
|
|
106
|
+
else:
|
|
107
|
+
logger.info("Websocket thread stopped successfully")
|
|
108
|
+
|
|
109
|
+
async def _process_execution_event_async(self, event: ExecutionGriptapeNodeEvent) -> None:
|
|
110
|
+
"""Process execution events asynchronously for real-time websocket emission."""
|
|
111
|
+
logger.debug("REAL-TIME: Processing execution event for session %s", self._session_id)
|
|
112
|
+
self.send_event("execution_event", event.wrapped_event.json())
|
|
113
|
+
|
|
114
|
+
async def arun(
|
|
115
|
+
self,
|
|
116
|
+
workflow_name: str,
|
|
117
|
+
flow_input: Any,
|
|
118
|
+
storage_backend: StorageBackend | None = None,
|
|
119
|
+
**kwargs: Any,
|
|
120
|
+
) -> None:
|
|
121
|
+
"""Executes a local workflow.
|
|
122
|
+
|
|
123
|
+
Executes a workflow by setting up event listeners, registering libraries,
|
|
124
|
+
loading the user-defined workflow, and running the specified workflow.
|
|
125
|
+
|
|
126
|
+
Parameters:
|
|
127
|
+
workflow_name: The name of the workflow to execute.
|
|
128
|
+
flow_input: Input data for the flow, typically a dictionary.
|
|
129
|
+
storage_backend: The storage backend to use for the workflow execution.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
None
|
|
133
|
+
"""
|
|
134
|
+
try:
|
|
135
|
+
await self._arun(
|
|
136
|
+
workflow_name=workflow_name,
|
|
137
|
+
flow_input=flow_input,
|
|
138
|
+
storage_backend=storage_backend,
|
|
139
|
+
**kwargs,
|
|
140
|
+
)
|
|
141
|
+
except Exception as e:
|
|
142
|
+
msg = f"Workflow execution failed: {e}"
|
|
143
|
+
logger.exception(msg)
|
|
144
|
+
control_flow_cancelled_event = ControlFlowCancelledEvent(
|
|
145
|
+
result_details="Encountered an error during workflow execution",
|
|
146
|
+
exception=e,
|
|
147
|
+
)
|
|
148
|
+
execution_event = ExecutionEvent(payload=control_flow_cancelled_event)
|
|
149
|
+
self.send_event("execution_event", execution_event.json())
|
|
150
|
+
await self._wait_for_websocket_queue_flush()
|
|
151
|
+
await asyncio.sleep(1)
|
|
152
|
+
raise LocalExecutorError(msg) from e
|
|
153
|
+
finally:
|
|
154
|
+
self._stop_websocket_thread()
|
|
155
|
+
|
|
156
|
+
async def _arun( # noqa: C901, PLR0915
|
|
157
|
+
self,
|
|
158
|
+
workflow_name: str,
|
|
159
|
+
flow_input: Any,
|
|
160
|
+
storage_backend: StorageBackend | None = None,
|
|
161
|
+
**kwargs: Any,
|
|
162
|
+
) -> None:
|
|
163
|
+
"""Internal async run method with detailed event handling and websocket integration."""
|
|
164
|
+
flow_name = await self.aprepare_workflow_for_run(
|
|
165
|
+
workflow_name=workflow_name,
|
|
166
|
+
flow_input=flow_input,
|
|
167
|
+
storage_backend=storage_backend,
|
|
168
|
+
**kwargs,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Send the run command to actually execute it (fire and forget)
|
|
172
|
+
start_flow_request = StartFlowRequest(flow_name=flow_name)
|
|
173
|
+
start_flow_task = asyncio.create_task(GriptapeNodes.ahandle_request(start_flow_request))
|
|
174
|
+
|
|
175
|
+
is_flow_finished = False
|
|
176
|
+
error: Exception | None = None
|
|
177
|
+
|
|
178
|
+
def _handle_start_flow_result(task: asyncio.Task[ResultPayload]) -> None:
|
|
179
|
+
nonlocal is_flow_finished, error, start_flow_request
|
|
180
|
+
try:
|
|
181
|
+
start_flow_result = task.result()
|
|
182
|
+
self._on_start_flow_result(start_flow_result) if self._on_start_flow_result is not None else None
|
|
183
|
+
|
|
184
|
+
if isinstance(start_flow_result, StartFlowResultFailure):
|
|
185
|
+
msg = f"Failed to start flow {flow_name}"
|
|
186
|
+
logger.error(msg)
|
|
187
|
+
event_result_failure = EventResultFailure(request=start_flow_request, result=start_flow_result)
|
|
188
|
+
self.send_event("failure_result", event_result_failure.json())
|
|
189
|
+
raise LocalExecutorError(msg) from start_flow_result.exception # noqa: TRY301
|
|
190
|
+
|
|
191
|
+
event_result_success = EventResultSuccess(request=start_flow_request, result=start_flow_result)
|
|
192
|
+
self.send_event("success_result", event_result_success.json())
|
|
193
|
+
|
|
194
|
+
except Exception as e:
|
|
195
|
+
msg = "Error starting workflow"
|
|
196
|
+
logger.exception(msg)
|
|
197
|
+
is_flow_finished = True
|
|
198
|
+
error = e
|
|
199
|
+
# The StartFlowRequest is sent asynchronously to enable real-time event emission via WebSocket.
|
|
200
|
+
# The main while loop below then waits for events from the queue. However, if StartFlowRequest fails
|
|
201
|
+
# immediately, then no events are ever added to the queue, causing the loop to hang indefinitely
|
|
202
|
+
# on event_queue.get(). This fix adds a dummy event to wake up the loop in failure cases.
|
|
203
|
+
event_queue = GriptapeNodes.EventManager().event_queue
|
|
204
|
+
queue_event_task = asyncio.create_task(event_queue.put(None))
|
|
205
|
+
background_tasks.add(queue_event_task)
|
|
206
|
+
queue_event_task.add_done_callback(background_tasks.discard)
|
|
207
|
+
|
|
208
|
+
start_flow_task.add_done_callback(_handle_start_flow_result)
|
|
209
|
+
|
|
210
|
+
logger.info("Workflow start request sent! Processing events...")
|
|
211
|
+
|
|
212
|
+
background_tasks: set[asyncio.Task] = set()
|
|
213
|
+
|
|
214
|
+
def _handle_task_done(task: asyncio.Task) -> None:
|
|
215
|
+
background_tasks.discard(task)
|
|
216
|
+
if task.exception() and not task.cancelled():
|
|
217
|
+
logger.exception("Background task failed", exc_info=task.exception())
|
|
218
|
+
|
|
219
|
+
event_queue = GriptapeNodes.EventManager().event_queue
|
|
220
|
+
while not is_flow_finished:
|
|
221
|
+
try:
|
|
222
|
+
event = await event_queue.get()
|
|
223
|
+
|
|
224
|
+
# Handle the dummy wake up event (None)
|
|
225
|
+
if event is None:
|
|
226
|
+
event_queue.task_done()
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
logger.debug("Processing event: %s", type(event).__name__)
|
|
230
|
+
|
|
231
|
+
if isinstance(event, EventRequest):
|
|
232
|
+
self.send_event("event_request", event.json())
|
|
233
|
+
task = asyncio.create_task(self._handle_event_request(event))
|
|
234
|
+
background_tasks.add(task)
|
|
235
|
+
task.add_done_callback(_handle_task_done)
|
|
236
|
+
elif isinstance(event, ExecutionGriptapeNodeEvent):
|
|
237
|
+
# Emit execution event via WebSocket
|
|
238
|
+
self.send_event("execution_event", event.wrapped_event.json())
|
|
239
|
+
task = asyncio.create_task(self._process_execution_event_async(event))
|
|
240
|
+
background_tasks.add(task)
|
|
241
|
+
task.add_done_callback(_handle_task_done)
|
|
242
|
+
is_flow_finished, error = await self._handle_execution_event(event, flow_name)
|
|
243
|
+
|
|
244
|
+
event_queue.task_done()
|
|
245
|
+
|
|
246
|
+
except Exception as e:
|
|
247
|
+
msg = f"Error handling queue event: {e}"
|
|
248
|
+
logger.exception(msg)
|
|
249
|
+
error = LocalExecutorError(msg)
|
|
250
|
+
break
|
|
251
|
+
|
|
252
|
+
if background_tasks:
|
|
253
|
+
logger.info("Waiting for %d background tasks to complete", len(background_tasks))
|
|
254
|
+
await asyncio.gather(*background_tasks, return_exceptions=True)
|
|
255
|
+
|
|
256
|
+
await self._wait_for_websocket_queue_flush()
|
|
257
|
+
|
|
258
|
+
if error is not None:
|
|
259
|
+
raise error
|
|
260
|
+
|
|
261
|
+
async def _start_websocket_connection(self) -> None:
|
|
262
|
+
"""Start websocket connection in a background thread for event emission."""
|
|
263
|
+
logger.info("Starting websocket connection for session %s", self._session_id)
|
|
264
|
+
api_key = self._get_api_key()
|
|
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)
|
|
271
|
+
self._websocket_thread.start()
|
|
272
|
+
|
|
273
|
+
if self._websocket_event_loop_ready.wait(timeout=10):
|
|
274
|
+
logger.info("Websocket thread ready")
|
|
275
|
+
await asyncio.sleep(1) # Brief wait for connection to establish
|
|
276
|
+
else:
|
|
277
|
+
logger.error("Timeout waiting for websocket thread to start")
|
|
278
|
+
|
|
279
|
+
def _get_api_key(self) -> str | None:
|
|
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:
|
|
289
|
+
"""Run WebSocket tasks in a separate thread with its own async loop."""
|
|
290
|
+
try:
|
|
291
|
+
# Create a new event loop for this thread
|
|
292
|
+
loop = asyncio.new_event_loop()
|
|
293
|
+
self._websocket_event_loop = loop
|
|
294
|
+
asyncio.set_event_loop(loop)
|
|
295
|
+
|
|
296
|
+
# Create the outgoing queue and shutdown event
|
|
297
|
+
self._ws_outgoing_queue = asyncio.Queue()
|
|
298
|
+
self._shutdown_event = asyncio.Event()
|
|
299
|
+
|
|
300
|
+
# Signal that websocket_event_loop is ready
|
|
301
|
+
self._websocket_event_loop_ready.set()
|
|
302
|
+
logger.info("Websocket thread started and ready")
|
|
303
|
+
|
|
304
|
+
# Run the async WebSocket tasks
|
|
305
|
+
loop.run_until_complete(self._run_websocket_tasks(api_key))
|
|
306
|
+
except Exception as e:
|
|
307
|
+
logger.error("WebSocket thread error: %s", e)
|
|
308
|
+
finally:
|
|
309
|
+
self._websocket_event_loop = None
|
|
310
|
+
self._websocket_event_loop_ready.clear()
|
|
311
|
+
self._shutdown_event = None
|
|
312
|
+
logger.info("Websocket thread ended")
|
|
313
|
+
|
|
314
|
+
async def _run_websocket_tasks(self, api_key: str) -> None:
|
|
315
|
+
"""Run websocket tasks - establish connection and handle outgoing messages."""
|
|
316
|
+
logger.info("Creating websocket connection stream")
|
|
317
|
+
connection_stream = _create_websocket_connection(api_key)
|
|
318
|
+
|
|
319
|
+
async for ws_connection in connection_stream:
|
|
320
|
+
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
|
+
|
|
325
|
+
except (ConnectionClosed, ConnectionClosedError):
|
|
326
|
+
logger.info("WebSocket connection closed, reconnecting...")
|
|
327
|
+
continue
|
|
328
|
+
except asyncio.CancelledError:
|
|
329
|
+
logger.info("WebSocket task cancelled, shutting down")
|
|
330
|
+
break
|
|
331
|
+
except Exception:
|
|
332
|
+
logger.exception("WebSocket tasks failed")
|
|
333
|
+
await asyncio.sleep(2.0)
|
|
334
|
+
continue
|
|
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")
|
|
341
|
+
|
|
342
|
+
async def _send_outgoing_messages(self, ws_connection: Any) -> None:
|
|
343
|
+
"""Send outgoing WebSocket messages from queue - matches app.py pattern exactly."""
|
|
344
|
+
if self._ws_outgoing_queue is None:
|
|
345
|
+
logger.error("No outgoing queue available")
|
|
346
|
+
return
|
|
347
|
+
|
|
348
|
+
logger.debug("Starting outgoing WebSocket request sender")
|
|
349
|
+
|
|
350
|
+
while True:
|
|
351
|
+
# Check if shutdown was requested
|
|
352
|
+
if self._shutdown_event and self._shutdown_event.is_set():
|
|
353
|
+
logger.info("Shutdown requested, ending message sender")
|
|
354
|
+
break
|
|
355
|
+
|
|
356
|
+
try:
|
|
357
|
+
# Get message from outgoing queue with timeout to allow shutdown checks
|
|
358
|
+
message = await asyncio.wait_for(self._ws_outgoing_queue.get(), timeout=1.0)
|
|
359
|
+
except TimeoutError:
|
|
360
|
+
# No message in queue, continue to check for shutdown
|
|
361
|
+
continue
|
|
362
|
+
|
|
363
|
+
try:
|
|
364
|
+
if isinstance(message, WebSocketMessage):
|
|
365
|
+
await _send_websocket_message(ws_connection, message.event_type, message.payload, message.topic)
|
|
366
|
+
logger.debug("DELIVERED: %s event", message.event_type)
|
|
367
|
+
else:
|
|
368
|
+
logger.warning("Unknown outgoing message type: %s", type(message))
|
|
369
|
+
except Exception as e:
|
|
370
|
+
logger.error("Error sending outgoing WebSocket request: %s", e)
|
|
371
|
+
finally:
|
|
372
|
+
self._ws_outgoing_queue.task_done()
|
|
373
|
+
|
|
374
|
+
def send_event(self, event_type: str, payload: str) -> None:
|
|
375
|
+
"""Send an event via websocket if connected - thread-safe version."""
|
|
376
|
+
# Wait for websocket event loop to be ready
|
|
377
|
+
if not self._websocket_event_loop_ready.wait(timeout=1.0):
|
|
378
|
+
logger.debug("Websocket not ready, event not sent: %s", event_type)
|
|
379
|
+
return
|
|
380
|
+
|
|
381
|
+
# Use run_coroutine_threadsafe to put message into WebSocket background thread queue
|
|
382
|
+
if self._websocket_event_loop is None:
|
|
383
|
+
logger.debug("WebSocket event loop not available for message: %s", event_type)
|
|
384
|
+
return
|
|
385
|
+
|
|
386
|
+
topic = f"sessions/{self._session_id}/response"
|
|
387
|
+
message = WebSocketMessage(event_type, payload, topic)
|
|
388
|
+
|
|
389
|
+
if self._ws_outgoing_queue is None:
|
|
390
|
+
logger.debug("No websocket queue available for event: %s", event_type)
|
|
391
|
+
return
|
|
392
|
+
|
|
393
|
+
try:
|
|
394
|
+
asyncio.run_coroutine_threadsafe(self._ws_outgoing_queue.put(message), self._websocket_event_loop)
|
|
395
|
+
logger.debug("SENT: %s event via websocket", event_type)
|
|
396
|
+
except Exception as e:
|
|
397
|
+
logger.error("Failed to queue event %s: %s", event_type, e)
|
|
398
|
+
|
|
399
|
+
async def _wait_for_websocket_queue_flush(self, timeout_seconds: float = 5.0) -> None:
|
|
400
|
+
"""Wait for all websocket messages to be sent."""
|
|
401
|
+
if self._ws_outgoing_queue is None or self._websocket_event_loop is None:
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
async def _check_queue_empty() -> bool:
|
|
405
|
+
return self._ws_outgoing_queue.empty() if self._ws_outgoing_queue else True
|
|
406
|
+
|
|
407
|
+
start_time = asyncio.get_event_loop().time()
|
|
408
|
+
while asyncio.get_event_loop().time() - start_time < timeout_seconds:
|
|
409
|
+
future = asyncio.run_coroutine_threadsafe(_check_queue_empty(), self._websocket_event_loop)
|
|
410
|
+
try:
|
|
411
|
+
is_empty = future.result(timeout=0.1)
|
|
412
|
+
if is_empty:
|
|
413
|
+
return
|
|
414
|
+
except Exception as e:
|
|
415
|
+
logger.debug("Error checking queue status: %s", e)
|
|
416
|
+
await asyncio.sleep(0.1)
|
|
417
|
+
|
|
418
|
+
logger.warning("Timeout waiting for websocket queue to flush")
|
|
@@ -149,25 +149,25 @@ class LocalWorkflowExecutor(WorkflowExecutor):
|
|
|
149
149
|
|
|
150
150
|
return False, None
|
|
151
151
|
|
|
152
|
-
async def
|
|
152
|
+
async def aprepare_workflow_for_run(
|
|
153
153
|
self,
|
|
154
154
|
workflow_name: str,
|
|
155
155
|
flow_input: Any,
|
|
156
156
|
storage_backend: StorageBackend | None = None,
|
|
157
157
|
**kwargs: Any,
|
|
158
|
-
) ->
|
|
159
|
-
"""
|
|
160
|
-
|
|
161
|
-
Executes a workflow by setting up event listeners, registering libraries,
|
|
162
|
-
loading the user-defined workflow, and running the specified workflow.
|
|
158
|
+
) -> str:
|
|
159
|
+
"""Prepares a local workflow for execution.
|
|
163
160
|
|
|
161
|
+
This method sets up the environment for executing a workflow, including
|
|
162
|
+
initializing event listeners, registering libraries, loading the user-defined
|
|
163
|
+
workflow, and preparing the specified workflow for execution.
|
|
164
164
|
Parameters:
|
|
165
|
-
workflow_name: The name of the workflow to
|
|
165
|
+
workflow_name: The name of the workflow to prepare.
|
|
166
166
|
flow_input: Input data for the flow, typically a dictionary.
|
|
167
167
|
storage_backend: The storage backend to use for the workflow execution.
|
|
168
168
|
|
|
169
169
|
Returns:
|
|
170
|
-
|
|
170
|
+
str: The name of the prepared flow.
|
|
171
171
|
"""
|
|
172
172
|
if storage_backend is not None:
|
|
173
173
|
msg = "The storage_backend parameter is deprecated. Pass `storage_backend` to the constructor instead."
|
|
@@ -186,6 +186,35 @@ class LocalWorkflowExecutor(WorkflowExecutor):
|
|
|
186
186
|
# Now let's set the input to the flow
|
|
187
187
|
await self._set_input_for_flow(flow_name=flow_name, flow_input=flow_input)
|
|
188
188
|
|
|
189
|
+
return flow_name
|
|
190
|
+
|
|
191
|
+
async def arun(
|
|
192
|
+
self,
|
|
193
|
+
workflow_name: str,
|
|
194
|
+
flow_input: Any,
|
|
195
|
+
storage_backend: StorageBackend | None = None,
|
|
196
|
+
**kwargs: Any,
|
|
197
|
+
) -> None:
|
|
198
|
+
"""Executes a local workflow.
|
|
199
|
+
|
|
200
|
+
Executes a workflow by setting up event listeners, registering libraries,
|
|
201
|
+
loading the user-defined workflow, and running the specified workflow.
|
|
202
|
+
|
|
203
|
+
Parameters:
|
|
204
|
+
workflow_name: The name of the workflow to execute.
|
|
205
|
+
flow_input: Input data for the flow, typically a dictionary.
|
|
206
|
+
storage_backend: The storage backend to use for the workflow execution.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
None
|
|
210
|
+
"""
|
|
211
|
+
flow_name = await self.aprepare_workflow_for_run(
|
|
212
|
+
workflow_name=workflow_name,
|
|
213
|
+
flow_input=flow_input,
|
|
214
|
+
storage_backend=storage_backend,
|
|
215
|
+
**kwargs,
|
|
216
|
+
)
|
|
217
|
+
|
|
189
218
|
# Now send the run command to actually execute it
|
|
190
219
|
start_flow_request = StartFlowRequest(flow_name=flow_name)
|
|
191
220
|
start_flow_result = await GriptapeNodes.ahandle_request(start_flow_request)
|