waldiez 0.5.8__py3-none-any.whl → 0.5.10__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.
Potentially problematic release.
This version of waldiez might be problematic. Click here for more details.
- waldiez/_version.py +1 -1
- waldiez/cli.py +112 -24
- waldiez/exporting/agent/exporter.py +3 -0
- waldiez/exporting/agent/extras/captain_agent_extras.py +44 -7
- waldiez/exporting/agent/extras/handoffs/condition.py +3 -1
- waldiez/exporting/chats/utils/common.py +25 -23
- waldiez/exporting/core/__init__.py +0 -2
- waldiez/exporting/core/context.py +13 -13
- waldiez/exporting/core/protocols.py +0 -141
- waldiez/exporting/core/result.py +5 -5
- waldiez/exporting/flow/merger.py +2 -2
- waldiez/exporting/flow/orchestrator.py +1 -0
- waldiez/exporting/flow/utils/common.py +2 -2
- waldiez/exporting/flow/utils/importing.py +1 -0
- waldiez/exporting/flow/utils/logging.py +6 -7
- waldiez/exporting/tools/exporter.py +5 -0
- waldiez/exporting/tools/factory.py +4 -0
- waldiez/exporting/tools/processor.py +5 -1
- waldiez/io/_ws.py +13 -5
- waldiez/io/models/content/image.py +1 -0
- waldiez/io/models/user_input.py +4 -4
- waldiez/io/models/user_response.py +1 -0
- waldiez/io/mqtt.py +1 -1
- waldiez/io/structured.py +17 -17
- waldiez/io/utils.py +1 -1
- waldiez/io/ws.py +9 -11
- waldiez/logger.py +180 -63
- waldiez/models/agents/agent/update_system_message.py +0 -2
- waldiez/models/agents/doc_agent/doc_agent.py +8 -1
- waldiez/models/common/dict_utils.py +169 -40
- waldiez/models/flow/flow.py +6 -6
- waldiez/models/flow/info.py +5 -1
- waldiez/models/model/_llm.py +28 -14
- waldiez/models/model/model.py +4 -1
- waldiez/models/model/model_data.py +18 -5
- waldiez/models/tool/predefined/_config.py +5 -1
- waldiez/models/tool/predefined/_duckduckgo.py +4 -0
- waldiez/models/tool/predefined/_email.py +474 -0
- waldiez/models/tool/predefined/_google.py +8 -6
- waldiez/models/tool/predefined/_perplexity.py +3 -0
- waldiez/models/tool/predefined/_searxng.py +3 -0
- waldiez/models/tool/predefined/_tavily.py +4 -1
- waldiez/models/tool/predefined/_wikipedia.py +4 -1
- waldiez/models/tool/predefined/_youtube.py +4 -1
- waldiez/models/tool/predefined/protocol.py +3 -0
- waldiez/models/tool/tool.py +22 -4
- waldiez/models/waldiez.py +12 -0
- waldiez/runner.py +37 -54
- waldiez/running/__init__.py +6 -0
- waldiez/running/base_runner.py +310 -353
- waldiez/running/environment.py +1 -0
- waldiez/running/exceptions.py +9 -0
- waldiez/running/post_run.py +4 -4
- waldiez/running/pre_run.py +51 -40
- waldiez/running/protocol.py +21 -101
- waldiez/running/run_results.py +1 -1
- waldiez/running/standard_runner.py +84 -277
- waldiez/running/step_by_step/__init__.py +46 -0
- waldiez/running/step_by_step/breakpoints_mixin.py +188 -0
- waldiez/running/step_by_step/step_by_step_models.py +224 -0
- waldiez/running/step_by_step/step_by_step_runner.py +745 -0
- waldiez/running/subprocess_runner/__base__.py +282 -0
- waldiez/running/subprocess_runner/__init__.py +16 -0
- waldiez/running/subprocess_runner/_async_runner.py +362 -0
- waldiez/running/subprocess_runner/_sync_runner.py +455 -0
- waldiez/running/subprocess_runner/runner.py +561 -0
- waldiez/running/timeline_processor.py +1 -1
- waldiez/running/utils.py +376 -1
- waldiez/utils/version.py +2 -6
- waldiez/ws/__init__.py +70 -0
- waldiez/ws/__main__.py +15 -0
- waldiez/ws/_file_handler.py +201 -0
- waldiez/ws/cli.py +211 -0
- waldiez/ws/client_manager.py +835 -0
- waldiez/ws/errors.py +416 -0
- waldiez/ws/models.py +971 -0
- waldiez/ws/reloader.py +342 -0
- waldiez/ws/server.py +469 -0
- waldiez/ws/session_manager.py +393 -0
- waldiez/ws/session_stats.py +83 -0
- waldiez/ws/utils.py +385 -0
- {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/METADATA +74 -74
- {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/RECORD +87 -65
- waldiez/running/patch_io_stream.py +0 -210
- {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/WHEEL +0 -0
- {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/entry_points.txt +0 -0
- {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/licenses/LICENSE +0 -0
- {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/licenses/NOTICE.md +0 -0
waldiez/ws/server.py
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0.
|
|
2
|
+
# Copyright (c) 2024 - 2025 Waldiez and contributors.
|
|
3
|
+
"""WebSocket server implementation for Waldiez."""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import re
|
|
8
|
+
import signal
|
|
9
|
+
import time
|
|
10
|
+
import traceback
|
|
11
|
+
import uuid
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Sequence
|
|
14
|
+
|
|
15
|
+
import websockets
|
|
16
|
+
from websockets.exceptions import ConnectionClosed, WebSocketException
|
|
17
|
+
|
|
18
|
+
from .client_manager import ClientManager
|
|
19
|
+
from .errors import ErrorHandler, MessageParsingError, ServerOverloadError
|
|
20
|
+
from .models import ConnectionNotification
|
|
21
|
+
from .reloader import create_file_watcher
|
|
22
|
+
from .session_manager import SessionManager
|
|
23
|
+
from .utils import get_available_port, is_port_available
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
CWD = Path.cwd()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# pylint: disable=too-many-instance-attributes
|
|
31
|
+
class WaldiezWsServer:
|
|
32
|
+
"""WebSocket server for Waldiez."""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
host: str = "localhost",
|
|
37
|
+
port: int = 8765,
|
|
38
|
+
workspace_dir: Path = CWD,
|
|
39
|
+
max_clients: int = 1,
|
|
40
|
+
allowed_origins: Sequence[re.Pattern[str]] | None = None,
|
|
41
|
+
**kwargs: Any,
|
|
42
|
+
):
|
|
43
|
+
"""Initialize WebSocket server.
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
host : str
|
|
48
|
+
Server host address
|
|
49
|
+
port : int
|
|
50
|
+
Server port
|
|
51
|
+
workspace_dir : Path
|
|
52
|
+
Path to the workspace directory
|
|
53
|
+
max_clients : int
|
|
54
|
+
Maximum number of concurrent clients (default: 1)
|
|
55
|
+
allowed_origins : list[str] | None
|
|
56
|
+
List of allowed origins for CORS (default: None)
|
|
57
|
+
ping_interval : float | None
|
|
58
|
+
Ping interval in seconds
|
|
59
|
+
ping_timeout : float | None
|
|
60
|
+
Ping timeout in seconds
|
|
61
|
+
close_timeout : float | None
|
|
62
|
+
Close timeout in seconds
|
|
63
|
+
max_size : int | None
|
|
64
|
+
Maximum message size in bytes
|
|
65
|
+
max_queue : int | None
|
|
66
|
+
Maximum queue size
|
|
67
|
+
write_limit : int
|
|
68
|
+
Write buffer limit
|
|
69
|
+
"""
|
|
70
|
+
self.host = host
|
|
71
|
+
self.port = port
|
|
72
|
+
self.workspace_dir = workspace_dir
|
|
73
|
+
self.max_clients = max_clients
|
|
74
|
+
self.allowed_origins = allowed_origins
|
|
75
|
+
|
|
76
|
+
# WebSocket configuration
|
|
77
|
+
self.ping_interval = kwargs.get("ping_interval", 20.0)
|
|
78
|
+
self.ping_timeout = kwargs.get("ping_timeout", 20.0)
|
|
79
|
+
self.close_timeout = kwargs.get("close_timeout", 10.0)
|
|
80
|
+
self.max_size = kwargs.get("max_size", 2**23) # 8MB
|
|
81
|
+
self.max_queue = kwargs.get("max_queue", 32)
|
|
82
|
+
self.write_limit = kwargs.get("write_limit", 2**16) # 64KB
|
|
83
|
+
|
|
84
|
+
# Server state
|
|
85
|
+
self.server: websockets.Server | None = None
|
|
86
|
+
self.session_manager = SessionManager()
|
|
87
|
+
self.clients: dict[str, ClientManager] = {}
|
|
88
|
+
self.is_running = False
|
|
89
|
+
self.start_time = 0.0
|
|
90
|
+
self.error_handler = ErrorHandler()
|
|
91
|
+
|
|
92
|
+
# Shutdown event
|
|
93
|
+
self.shutdown_event = asyncio.Event()
|
|
94
|
+
|
|
95
|
+
# Statistics
|
|
96
|
+
self.stats = {
|
|
97
|
+
"connections_total": 0,
|
|
98
|
+
"connections_active": 0,
|
|
99
|
+
"messages_received": 0,
|
|
100
|
+
"messages_sent": 0,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# pylint: disable=too-complex,too-many-branches,too-many-statements
|
|
104
|
+
async def _handle_client( # noqa: C901
|
|
105
|
+
self,
|
|
106
|
+
websocket: websockets.ServerConnection,
|
|
107
|
+
) -> None:
|
|
108
|
+
"""Handle individual client connections.
|
|
109
|
+
|
|
110
|
+
Parameters
|
|
111
|
+
----------
|
|
112
|
+
websocket : websockets.WebSocketServerProtocol
|
|
113
|
+
WebSocket connection
|
|
114
|
+
"""
|
|
115
|
+
client_id = str(uuid.uuid4())
|
|
116
|
+
|
|
117
|
+
# Check client limit
|
|
118
|
+
if len(self.clients) >= self.max_clients:
|
|
119
|
+
logger.warning(
|
|
120
|
+
"Client limit exceeded (%d/%d), rejecting connection from %s",
|
|
121
|
+
len(self.clients),
|
|
122
|
+
self.max_clients,
|
|
123
|
+
websocket.remote_address,
|
|
124
|
+
)
|
|
125
|
+
error = ServerOverloadError(len(self.clients), self.max_clients)
|
|
126
|
+
await websocket.close(code=error.error_code, reason=error.message)
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
# Create client handler
|
|
130
|
+
client_manager = ClientManager(
|
|
131
|
+
websocket,
|
|
132
|
+
client_id,
|
|
133
|
+
self.session_manager,
|
|
134
|
+
workspace_dir=self.workspace_dir,
|
|
135
|
+
error_handler=self.error_handler,
|
|
136
|
+
)
|
|
137
|
+
self.clients[client_id] = client_manager
|
|
138
|
+
self.stats["connections_total"] += 1
|
|
139
|
+
self.stats["connections_active"] = len(self.clients)
|
|
140
|
+
# pylint: disable=too-many-try-statements,broad-exception-caught
|
|
141
|
+
try:
|
|
142
|
+
await client_manager.send_message(
|
|
143
|
+
ConnectionNotification(
|
|
144
|
+
status="connected",
|
|
145
|
+
client_id=client_id,
|
|
146
|
+
server_time=time.time(),
|
|
147
|
+
).model_dump(mode="json", fallback=str)
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Message handling loop
|
|
151
|
+
async for raw_message in websocket:
|
|
152
|
+
try:
|
|
153
|
+
# Parse message
|
|
154
|
+
if isinstance(raw_message, bytes):
|
|
155
|
+
message_str = raw_message.decode("utf-8")
|
|
156
|
+
else:
|
|
157
|
+
# noinspection PyUnreachableCode
|
|
158
|
+
message_str = (
|
|
159
|
+
raw_message
|
|
160
|
+
if isinstance(raw_message, str)
|
|
161
|
+
else str(raw_message)
|
|
162
|
+
)
|
|
163
|
+
response = await client_manager.handle_message(message_str)
|
|
164
|
+
self.stats["messages_received"] += 1
|
|
165
|
+
|
|
166
|
+
# Send response if available
|
|
167
|
+
if response: # pragma: no branch
|
|
168
|
+
success = await client_manager.send_message(response)
|
|
169
|
+
if success:
|
|
170
|
+
self.stats["messages_sent"] += 1
|
|
171
|
+
else:
|
|
172
|
+
self.error_handler.record_send_failure(client_id)
|
|
173
|
+
|
|
174
|
+
except ValueError as e:
|
|
175
|
+
logger.warning("Invalid message from %s: %s", client_id, e)
|
|
176
|
+
error_response = self.error_handler.handle_error(
|
|
177
|
+
MessageParsingError(str(e), str(raw_message)),
|
|
178
|
+
client_id=client_id,
|
|
179
|
+
)
|
|
180
|
+
await client_manager.send_message(error_response)
|
|
181
|
+
|
|
182
|
+
except Exception as e:
|
|
183
|
+
traceback.print_exc()
|
|
184
|
+
logger.error(
|
|
185
|
+
"Error handling message from %s: %s", client_id, e
|
|
186
|
+
)
|
|
187
|
+
error_response = self.error_handler.handle_error(
|
|
188
|
+
e, client_id=client_id
|
|
189
|
+
)
|
|
190
|
+
await client_manager.send_message(error_response)
|
|
191
|
+
|
|
192
|
+
except ConnectionClosed:
|
|
193
|
+
logger.info("Client %s disconnected normally", client_id)
|
|
194
|
+
|
|
195
|
+
except WebSocketException as e:
|
|
196
|
+
logger.warning("WebSocket error for client %s: %s", client_id, e)
|
|
197
|
+
self.error_handler.record_operational_error(
|
|
198
|
+
"WebSocketException", str(e)
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
except Exception as e:
|
|
202
|
+
logger.error(
|
|
203
|
+
"Unexpected error handling client %s: %s", client_id, e
|
|
204
|
+
)
|
|
205
|
+
self.error_handler.record_operational_error(
|
|
206
|
+
"UnexpectedError", str(e)
|
|
207
|
+
)
|
|
208
|
+
raise
|
|
209
|
+
|
|
210
|
+
finally:
|
|
211
|
+
# Clean up client
|
|
212
|
+
if client_id in self.clients: # pragma: no branch
|
|
213
|
+
self.clients[client_id].close_connection()
|
|
214
|
+
del self.clients[client_id]
|
|
215
|
+
self.stats["connections_active"] = len(self.clients)
|
|
216
|
+
|
|
217
|
+
async def start(self) -> None:
|
|
218
|
+
"""Start the WebSocket server.
|
|
219
|
+
|
|
220
|
+
Raises
|
|
221
|
+
------
|
|
222
|
+
RuntimeError
|
|
223
|
+
If the port is already in use
|
|
224
|
+
Exception
|
|
225
|
+
For any other errors
|
|
226
|
+
"""
|
|
227
|
+
if self.is_running:
|
|
228
|
+
logger.warning("Server is already running")
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
await self.session_manager.start()
|
|
232
|
+
# Check port availability
|
|
233
|
+
if not is_port_available(self.port):
|
|
234
|
+
logger.warning("Port %d is not available", self.port)
|
|
235
|
+
self.port = get_available_port()
|
|
236
|
+
logger.info("Using port %d", self.port)
|
|
237
|
+
|
|
238
|
+
logger.info("Starting WebSocket server on %s:%d", self.host, self.port)
|
|
239
|
+
# pylint: disable=too-many-try-statements,broad-exception-caught
|
|
240
|
+
try:
|
|
241
|
+
# Create server
|
|
242
|
+
self.server = await websockets.serve(
|
|
243
|
+
self._handle_client,
|
|
244
|
+
self.host,
|
|
245
|
+
self.port,
|
|
246
|
+
ping_interval=self.ping_interval,
|
|
247
|
+
ping_timeout=self.ping_timeout,
|
|
248
|
+
close_timeout=self.close_timeout,
|
|
249
|
+
max_size=self.max_size,
|
|
250
|
+
max_queue=self.max_queue,
|
|
251
|
+
write_limit=self.write_limit,
|
|
252
|
+
origins=self.allowed_origins,
|
|
253
|
+
# Additional settings
|
|
254
|
+
compression=None, # Disable compression for lower latency
|
|
255
|
+
logger=logger,
|
|
256
|
+
server_header="Waldiez/ws",
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
self.is_running = True
|
|
260
|
+
self.start_time = time.time()
|
|
261
|
+
|
|
262
|
+
logger.info("WebSocket server started successfully")
|
|
263
|
+
logger.info("Server configuration:")
|
|
264
|
+
logger.info(" - Host: %s", self.host)
|
|
265
|
+
logger.info(" - Port: %d", self.port)
|
|
266
|
+
logger.info(" - Max clients: %d", self.max_clients)
|
|
267
|
+
logger.info(" - Ping interval: %s", self.ping_interval)
|
|
268
|
+
logger.info(" - Max message size: %s", self.max_size)
|
|
269
|
+
|
|
270
|
+
# Wait for shutdown
|
|
271
|
+
await self.shutdown_event.wait()
|
|
272
|
+
|
|
273
|
+
except Exception as e:
|
|
274
|
+
logger.error("Failed to start server: %s", e)
|
|
275
|
+
raise
|
|
276
|
+
|
|
277
|
+
finally:
|
|
278
|
+
await self.stop()
|
|
279
|
+
|
|
280
|
+
async def stop(self) -> None:
|
|
281
|
+
"""Stop the WebSocket server."""
|
|
282
|
+
await self.session_manager.stop()
|
|
283
|
+
if not self.is_running:
|
|
284
|
+
logger.warning("Server is not running")
|
|
285
|
+
return
|
|
286
|
+
|
|
287
|
+
logger.info("Stopping WebSocket server...")
|
|
288
|
+
|
|
289
|
+
# Close all client connections
|
|
290
|
+
if self.clients:
|
|
291
|
+
logger.info(
|
|
292
|
+
"Closing %d active client connections", len(self.clients)
|
|
293
|
+
)
|
|
294
|
+
close_tasks: list[Any] = []
|
|
295
|
+
for client in self.clients.values():
|
|
296
|
+
if client.is_active: # pragma: no branch
|
|
297
|
+
close_tasks.append(client.websocket.close())
|
|
298
|
+
|
|
299
|
+
if close_tasks: # pragma: no branch
|
|
300
|
+
await asyncio.gather(*close_tasks, return_exceptions=True)
|
|
301
|
+
|
|
302
|
+
# Stop server
|
|
303
|
+
if self.server:
|
|
304
|
+
self.server.close()
|
|
305
|
+
await self.server.wait_closed()
|
|
306
|
+
|
|
307
|
+
self.is_running = False
|
|
308
|
+
self.clients.clear()
|
|
309
|
+
self.stats["connections_active"] = 0
|
|
310
|
+
|
|
311
|
+
uptime = time.time() - self.start_time
|
|
312
|
+
logger.info("WebSocket server stopped (uptime: %.1f seconds)", uptime)
|
|
313
|
+
|
|
314
|
+
def shutdown(self) -> None:
|
|
315
|
+
"""Trigger server shutdown."""
|
|
316
|
+
self.shutdown_event.set()
|
|
317
|
+
|
|
318
|
+
def get_stats(self) -> dict[str, Any]:
|
|
319
|
+
"""Get server statistics.
|
|
320
|
+
|
|
321
|
+
Returns
|
|
322
|
+
-------
|
|
323
|
+
dict[str, Any]
|
|
324
|
+
Server statistics
|
|
325
|
+
"""
|
|
326
|
+
uptime = time.time() - self.start_time if self.is_running else 0
|
|
327
|
+
return {
|
|
328
|
+
**self.stats,
|
|
329
|
+
"uptime_seconds": uptime,
|
|
330
|
+
"is_running": self.is_running,
|
|
331
|
+
"server_config": {
|
|
332
|
+
"host": self.host,
|
|
333
|
+
"port": self.port,
|
|
334
|
+
"max_clients": self.max_clients,
|
|
335
|
+
"ping_interval": self.ping_interval,
|
|
336
|
+
"max_size": self.max_size,
|
|
337
|
+
},
|
|
338
|
+
"error_stats": self.error_handler.get_error_stats(),
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async def broadcast(
|
|
342
|
+
self, message: dict[str, Any], exclude_client: str | None = None
|
|
343
|
+
) -> int:
|
|
344
|
+
"""Broadcast message to all connected clients.
|
|
345
|
+
|
|
346
|
+
Parameters
|
|
347
|
+
----------
|
|
348
|
+
message : Dict[str, Any]
|
|
349
|
+
Message to broadcast
|
|
350
|
+
exclude_client : Optional[str]
|
|
351
|
+
Client ID to exclude from broadcast
|
|
352
|
+
|
|
353
|
+
Returns
|
|
354
|
+
-------
|
|
355
|
+
int
|
|
356
|
+
Number of clients that received the message
|
|
357
|
+
"""
|
|
358
|
+
if not self.clients:
|
|
359
|
+
return 0
|
|
360
|
+
|
|
361
|
+
send_tasks: list[Any] = []
|
|
362
|
+
for client_id, client in self.clients.items():
|
|
363
|
+
if client_id != exclude_client and client.is_active:
|
|
364
|
+
send_tasks.append(client.send_message(message))
|
|
365
|
+
|
|
366
|
+
if not send_tasks: # pragma: no cover
|
|
367
|
+
return 0
|
|
368
|
+
|
|
369
|
+
results = await asyncio.gather(*send_tasks, return_exceptions=True)
|
|
370
|
+
successful = sum(1 for result in results if result is True)
|
|
371
|
+
|
|
372
|
+
self.stats["messages_sent"] += successful
|
|
373
|
+
return successful
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
async def run_server(
|
|
377
|
+
host: str = "localhost",
|
|
378
|
+
port: int = 8765,
|
|
379
|
+
workspace_dir: Path = CWD,
|
|
380
|
+
auto_reload: bool = False,
|
|
381
|
+
watch_dirs: set[Path] | None = None,
|
|
382
|
+
**server_kwargs: Any,
|
|
383
|
+
) -> None:
|
|
384
|
+
"""Run the WebSocket server with optional auto-reload.
|
|
385
|
+
|
|
386
|
+
Parameters
|
|
387
|
+
----------
|
|
388
|
+
host : str
|
|
389
|
+
Server host
|
|
390
|
+
port : int
|
|
391
|
+
Server port
|
|
392
|
+
workspace_dir : Path
|
|
393
|
+
Path to the workspace directory
|
|
394
|
+
auto_reload : bool
|
|
395
|
+
Enable auto-reload on file changes
|
|
396
|
+
watch_dirs : Optional[Set[Path]]
|
|
397
|
+
Directories to watch for auto-reload
|
|
398
|
+
**server_kwargs
|
|
399
|
+
Additional server configuration
|
|
400
|
+
"""
|
|
401
|
+
server = WaldiezWsServer(
|
|
402
|
+
host=host,
|
|
403
|
+
port=port,
|
|
404
|
+
workspace_dir=workspace_dir,
|
|
405
|
+
**server_kwargs,
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
# Set up signal handlers
|
|
409
|
+
def signal_handler() -> None:
|
|
410
|
+
"""Handle shutdown signals."""
|
|
411
|
+
logger.info("Received shutdown signal")
|
|
412
|
+
server.shutdown()
|
|
413
|
+
logger.info("Shutdown event set, stopping server...")
|
|
414
|
+
|
|
415
|
+
# Register signal handlers
|
|
416
|
+
loop = asyncio.get_running_loop()
|
|
417
|
+
for sig in (signal.SIGTERM, signal.SIGINT):
|
|
418
|
+
# noinspection PyTypeChecker
|
|
419
|
+
loop.add_signal_handler(sig, signal_handler)
|
|
420
|
+
|
|
421
|
+
# Set up auto-reload if requested
|
|
422
|
+
file_watcher = None
|
|
423
|
+
if auto_reload:
|
|
424
|
+
# pylint: disable=import-outside-toplevel,too-many-try-statements
|
|
425
|
+
try:
|
|
426
|
+
# Determine watch directories
|
|
427
|
+
if watch_dirs is None:
|
|
428
|
+
project_root = Path(__file__).parents[2]
|
|
429
|
+
|
|
430
|
+
# Watch the actual waldiez package directory
|
|
431
|
+
waldiez_dir = project_root / "waldiez"
|
|
432
|
+
if waldiez_dir.exists():
|
|
433
|
+
watch_dirs = {waldiez_dir}
|
|
434
|
+
logger.info(
|
|
435
|
+
"Auto-reload: watching waldiez package at %s",
|
|
436
|
+
waldiez_dir,
|
|
437
|
+
)
|
|
438
|
+
else:
|
|
439
|
+
# Fallback: watch current directory
|
|
440
|
+
watch_dirs = {Path.cwd()}
|
|
441
|
+
logger.warning(
|
|
442
|
+
"Auto-reload: fallback to current directory %s",
|
|
443
|
+
Path.cwd(),
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
# Create file watcher with restart callback
|
|
447
|
+
file_watcher = create_file_watcher(
|
|
448
|
+
root_dir=Path(__file__).parents[2],
|
|
449
|
+
additional_dirs=list(watch_dirs),
|
|
450
|
+
restart_callback=None,
|
|
451
|
+
)
|
|
452
|
+
file_watcher.start()
|
|
453
|
+
logger.info(
|
|
454
|
+
"Auto-reload enabled for directories: %s",
|
|
455
|
+
{str(dir_) for dir_ in watch_dirs},
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
except ImportError as e:
|
|
459
|
+
logger.warning("Auto-reload not available: %s", e)
|
|
460
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
461
|
+
logger.error("Failed to set up auto-reload: %s", e)
|
|
462
|
+
|
|
463
|
+
try:
|
|
464
|
+
# Start server
|
|
465
|
+
await server.start()
|
|
466
|
+
finally:
|
|
467
|
+
# Clean up file watcher
|
|
468
|
+
if file_watcher:
|
|
469
|
+
file_watcher.stop()
|