waldiez 0.5.9__py3-none-any.whl → 0.6.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.

Potentially problematic release.


This version of waldiez might be problematic. Click here for more details.

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