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

Potentially problematic release.


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

Files changed (192) hide show
  1. waldiez/__init__.py +1 -1
  2. waldiez/_version.py +1 -1
  3. waldiez/cli.py +19 -7
  4. waldiez/cli_extras/jupyter.py +3 -0
  5. waldiez/cli_extras/runner.py +3 -1
  6. waldiez/cli_extras/studio.py +3 -1
  7. waldiez/exporter.py +9 -3
  8. waldiez/exporting/agent/exporter.py +15 -16
  9. waldiez/exporting/agent/extras/captain_agent_extras.py +6 -6
  10. waldiez/exporting/agent/extras/doc_agent_extras.py +6 -6
  11. waldiez/exporting/agent/extras/group_manager_agent_extas.py +40 -24
  12. waldiez/exporting/agent/extras/group_member_extras.py +6 -5
  13. waldiez/exporting/agent/extras/handoffs/after_work.py +2 -1
  14. waldiez/exporting/agent/extras/handoffs/available.py +2 -1
  15. waldiez/exporting/agent/extras/handoffs/condition.py +3 -2
  16. waldiez/exporting/agent/extras/handoffs/handoff.py +2 -1
  17. waldiez/exporting/agent/extras/handoffs/target.py +7 -4
  18. waldiez/exporting/agent/extras/rag/chroma_extras.py +27 -19
  19. waldiez/exporting/agent/extras/rag/mongo_extras.py +8 -8
  20. waldiez/exporting/agent/extras/rag/pgvector_extras.py +5 -5
  21. waldiez/exporting/agent/extras/rag/qdrant_extras.py +5 -4
  22. waldiez/exporting/agent/extras/rag/vector_db_extras.py +1 -1
  23. waldiez/exporting/agent/extras/rag_user_proxy_agent_extras.py +5 -7
  24. waldiez/exporting/agent/extras/reasoning_agent_extras.py +3 -5
  25. waldiez/exporting/agent/termination.py +1 -0
  26. waldiez/exporting/chats/exporter.py +4 -4
  27. waldiez/exporting/chats/processor.py +1 -2
  28. waldiez/exporting/chats/utils/common.py +89 -48
  29. waldiez/exporting/chats/utils/group.py +9 -9
  30. waldiez/exporting/chats/utils/nested.py +7 -7
  31. waldiez/exporting/chats/utils/sequential.py +1 -1
  32. waldiez/exporting/chats/utils/single.py +2 -2
  33. waldiez/exporting/core/constants.py +3 -1
  34. waldiez/exporting/core/content.py +7 -7
  35. waldiez/exporting/core/context.py +5 -3
  36. waldiez/exporting/core/exporter.py +5 -3
  37. waldiez/exporting/core/exporters.py +2 -2
  38. waldiez/exporting/core/extras/agent_extras/captain_extras.py +2 -2
  39. waldiez/exporting/core/extras/agent_extras/group_manager_extras.py +2 -2
  40. waldiez/exporting/core/extras/agent_extras/rag_user_extras.py +2 -2
  41. waldiez/exporting/core/extras/agent_extras/standard_extras.py +3 -8
  42. waldiez/exporting/core/extras/base.py +7 -5
  43. waldiez/exporting/core/extras/flow_extras.py +4 -5
  44. waldiez/exporting/core/extras/model_extras.py +2 -2
  45. waldiez/exporting/core/extras/path_resolver.py +1 -2
  46. waldiez/exporting/core/extras/serializer.py +13 -11
  47. waldiez/exporting/core/protocols.py +6 -5
  48. waldiez/exporting/core/result.py +25 -28
  49. waldiez/exporting/core/types.py +11 -10
  50. waldiez/exporting/core/utils/llm_config.py +4 -4
  51. waldiez/exporting/core/validation.py +10 -11
  52. waldiez/exporting/flow/execution_generator.py +99 -10
  53. waldiez/exporting/flow/exporter.py +2 -2
  54. waldiez/exporting/flow/factory.py +2 -2
  55. waldiez/exporting/flow/file_generator.py +4 -2
  56. waldiez/exporting/flow/merger.py +5 -3
  57. waldiez/exporting/flow/orchestrator.py +72 -2
  58. waldiez/exporting/flow/utils/common.py +6 -6
  59. waldiez/exporting/flow/utils/importing.py +7 -8
  60. waldiez/exporting/flow/utils/linting.py +25 -9
  61. waldiez/exporting/flow/utils/logging.py +5 -77
  62. waldiez/exporting/models/exporter.py +8 -8
  63. waldiez/exporting/models/processor.py +5 -5
  64. waldiez/exporting/tools/exporter.py +2 -2
  65. waldiez/exporting/tools/processor.py +7 -4
  66. waldiez/io/__init__.py +11 -5
  67. waldiez/io/_ws.py +12 -6
  68. waldiez/io/models/constants.py +10 -10
  69. waldiez/io/models/content/audio.py +1 -0
  70. waldiez/io/models/content/base.py +20 -18
  71. waldiez/io/models/content/file.py +1 -0
  72. waldiez/io/models/content/image.py +1 -0
  73. waldiez/io/models/content/text.py +1 -0
  74. waldiez/io/models/content/video.py +1 -0
  75. waldiez/io/models/user_input.py +10 -5
  76. waldiez/io/models/user_response.py +17 -16
  77. waldiez/io/mqtt.py +18 -31
  78. waldiez/io/redis.py +18 -22
  79. waldiez/io/structured.py +122 -70
  80. waldiez/io/utils.py +19 -10
  81. waldiez/io/ws.py +7 -3
  82. waldiez/logger.py +16 -3
  83. waldiez/models/agents/__init__.py +3 -0
  84. waldiez/models/agents/agent/agent.py +25 -17
  85. waldiez/models/agents/agent/agent_data.py +25 -22
  86. waldiez/models/agents/agent/code_execution.py +9 -11
  87. waldiez/models/agents/agent/termination_message.py +10 -12
  88. waldiez/models/agents/agent/update_system_message.py +2 -4
  89. waldiez/models/agents/agents.py +8 -8
  90. waldiez/models/agents/assistant/assistant.py +6 -3
  91. waldiez/models/agents/assistant/assistant_data.py +2 -2
  92. waldiez/models/agents/captain/captain_agent.py +7 -4
  93. waldiez/models/agents/captain/captain_agent_data.py +5 -7
  94. waldiez/models/agents/doc_agent/doc_agent.py +7 -4
  95. waldiez/models/agents/doc_agent/doc_agent_data.py +9 -10
  96. waldiez/models/agents/doc_agent/rag_query_engine.py +10 -12
  97. waldiez/models/agents/extra_requirements.py +3 -3
  98. waldiez/models/agents/group_manager/group_manager.py +12 -7
  99. waldiez/models/agents/group_manager/group_manager_data.py +13 -12
  100. waldiez/models/agents/group_manager/speakers.py +17 -19
  101. waldiez/models/agents/rag_user_proxy/rag_user_proxy.py +7 -4
  102. waldiez/models/agents/rag_user_proxy/rag_user_proxy_data.py +4 -1
  103. waldiez/models/agents/rag_user_proxy/retrieve_config.py +69 -63
  104. waldiez/models/agents/rag_user_proxy/vector_db_config.py +19 -19
  105. waldiez/models/agents/reasoning/reasoning_agent.py +7 -4
  106. waldiez/models/agents/reasoning/reasoning_agent_data.py +3 -2
  107. waldiez/models/agents/reasoning/reasoning_agent_reason_config.py +8 -8
  108. waldiez/models/agents/user_proxy/user_proxy.py +6 -3
  109. waldiez/models/agents/user_proxy/user_proxy_data.py +1 -1
  110. waldiez/models/chat/chat.py +28 -20
  111. waldiez/models/chat/chat_data.py +22 -21
  112. waldiez/models/chat/chat_message.py +9 -9
  113. waldiez/models/chat/chat_nested.py +9 -9
  114. waldiez/models/chat/chat_summary.py +6 -6
  115. waldiez/models/common/__init__.py +2 -0
  116. waldiez/models/common/ag2_version.py +2 -0
  117. waldiez/models/common/base.py +2 -0
  118. waldiez/models/common/dict_utils.py +8 -6
  119. waldiez/models/common/handoff.py +20 -17
  120. waldiez/models/common/method_utils.py +9 -7
  121. waldiez/models/common/naming.py +49 -0
  122. waldiez/models/flow/flow.py +11 -6
  123. waldiez/models/flow/flow_data.py +23 -17
  124. waldiez/models/flow/info.py +3 -3
  125. waldiez/models/flow/naming.py +2 -1
  126. waldiez/models/model/_aws.py +11 -13
  127. waldiez/models/model/_llm.py +8 -0
  128. waldiez/models/model/_price.py +2 -4
  129. waldiez/models/model/extra_requirements.py +1 -3
  130. waldiez/models/model/model.py +2 -2
  131. waldiez/models/model/model_data.py +21 -21
  132. waldiez/models/tool/extra_requirements.py +2 -4
  133. waldiez/models/tool/predefined/_duckduckgo.py +1 -0
  134. waldiez/models/tool/predefined/_email.py +4 -0
  135. waldiez/models/tool/predefined/_google.py +1 -0
  136. waldiez/models/tool/predefined/_perplexity.py +2 -1
  137. waldiez/models/tool/predefined/_searxng.py +2 -1
  138. waldiez/models/tool/predefined/_tavily.py +1 -0
  139. waldiez/models/tool/predefined/_wikipedia.py +2 -1
  140. waldiez/models/tool/predefined/_youtube.py +1 -0
  141. waldiez/models/tool/tool.py +8 -5
  142. waldiez/models/tool/tool_data.py +2 -2
  143. waldiez/models/waldiez.py +152 -4
  144. waldiez/runner.py +11 -5
  145. waldiez/running/async_utils.py +192 -0
  146. waldiez/running/base_runner.py +155 -241
  147. waldiez/running/dir_utils.py +52 -0
  148. waldiez/running/environment.py +10 -44
  149. waldiez/running/events_mixin.py +252 -0
  150. waldiez/running/exceptions.py +20 -0
  151. waldiez/running/gen_seq_diagram.py +18 -15
  152. waldiez/running/io_utils.py +216 -0
  153. waldiez/running/protocol.py +11 -5
  154. waldiez/running/requirements_mixin.py +65 -0
  155. waldiez/running/results_mixin.py +926 -0
  156. waldiez/running/standard_runner.py +24 -27
  157. waldiez/running/step_by_step/breakpoints_mixin.py +503 -47
  158. waldiez/running/step_by_step/command_handler.py +154 -0
  159. waldiez/running/step_by_step/events_processor.py +379 -0
  160. waldiez/running/step_by_step/step_by_step_models.py +425 -41
  161. waldiez/running/step_by_step/step_by_step_runner.py +437 -382
  162. waldiez/running/subprocess_runner/__base__.py +13 -8
  163. waldiez/running/subprocess_runner/_async_runner.py +6 -4
  164. waldiez/running/subprocess_runner/_sync_runner.py +11 -6
  165. waldiez/running/subprocess_runner/runner.py +48 -23
  166. waldiez/running/timeline_processor.py +1 -1
  167. waldiez/utils/__init__.py +2 -0
  168. waldiez/utils/conflict_checker.py +4 -4
  169. waldiez/utils/python_manager.py +415 -0
  170. waldiez/ws/__init__.py +8 -7
  171. waldiez/ws/_file_handler.py +18 -20
  172. waldiez/ws/_mock.py +75 -0
  173. waldiez/ws/cli.py +58 -10
  174. waldiez/ws/client_manager.py +77 -53
  175. waldiez/ws/errors.py +3 -0
  176. waldiez/ws/models.py +61 -53
  177. waldiez/ws/reloader.py +33 -4
  178. waldiez/ws/server.py +121 -52
  179. waldiez/ws/session_manager.py +8 -9
  180. waldiez/ws/session_stats.py +1 -1
  181. waldiez/ws/utils.py +33 -5
  182. {waldiez-0.5.10.dist-info → waldiez-0.6.1.dist-info}/METADATA +107 -109
  183. waldiez-0.6.1.dist-info/RECORD +254 -0
  184. waldiez/running/post_run.py +0 -180
  185. waldiez/running/pre_run.py +0 -159
  186. waldiez/running/run_results.py +0 -14
  187. waldiez/running/utils.py +0 -511
  188. waldiez-0.5.10.dist-info/RECORD +0 -248
  189. {waldiez-0.5.10.dist-info → waldiez-0.6.1.dist-info}/WHEEL +0 -0
  190. {waldiez-0.5.10.dist-info → waldiez-0.6.1.dist-info}/entry_points.txt +0 -0
  191. {waldiez-0.5.10.dist-info → waldiez-0.6.1.dist-info}/licenses/LICENSE +0 -0
  192. {waldiez-0.5.10.dist-info → waldiez-0.6.1.dist-info}/licenses/NOTICE.md +0 -0
waldiez/ws/reloader.py CHANGED
@@ -1,5 +1,10 @@
1
1
  # SPDX-License-Identifier: Apache-2.0.
2
2
  # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+ # pylint: disable=line-too-long,invalid-name
4
+ # pyright: reportUnknownVariableType=false,reportConstantRedefinition=false
5
+ # pyright: reportUntypedBaseClass=false,reportUnknownMemberType=false
6
+ # pyright: reportUnknownParameterType=false,reportUnknownArgumentType=false
7
+ # flake8: noqa: E501
3
8
  """Auto-reload functionality for development."""
4
9
 
5
10
  import logging
@@ -9,14 +14,34 @@ import threading
9
14
  import time
10
15
  from pathlib import Path
11
16
  from types import TracebackType
12
- from typing import Any, Callable
13
-
14
- from watchdog.events import FileSystemEvent, FileSystemEventHandler
15
- from watchdog.observers import Observer
17
+ from typing import Any, Callable, final
18
+
19
+ from typing_extensions import override
20
+
21
+ HAS_WATCHDOG = False
22
+ try:
23
+ from watchdog.events import ( # type: ignore[unused-ignore,unused-import,import-not-found,import-untyped]
24
+ FileSystemEvent,
25
+ FileSystemEventHandler,
26
+ )
27
+ from watchdog.observers import ( # type: ignore[unused-ignore,unused-import,import-not-found,import-untyped]
28
+ Observer,
29
+ )
30
+
31
+ HAS_WATCHDOG = True
32
+ except ImportError as exc:
33
+ _msg = ( # pylint: disable=invalid-name
34
+ "The 'watchdog' package is required for auto-reload functionality. "
35
+ "Please install it using 'pip install watchdog'."
36
+ )
37
+ raise ImportError(_msg) from exc
16
38
 
17
39
  logger = logging.getLogger(__name__)
40
+ fsevents_logger = logging.getLogger("fsevents")
41
+ fsevents_logger.setLevel(logging.WARNING) # Reduce noise from fsevents
18
42
 
19
43
 
44
+ @final
20
45
  class ReloadHandler(FileSystemEventHandler):
21
46
  """Handler for file system events that triggers server reload."""
22
47
 
@@ -113,6 +138,7 @@ class ReloadHandler(FileSystemEventHandler):
113
138
  else str(event.src_path)
114
139
  )
115
140
 
141
+ @override
116
142
  def on_modified(self, event: FileSystemEvent) -> None:
117
143
  """Handle file modification events.
118
144
 
@@ -129,6 +155,7 @@ class ReloadHandler(FileSystemEventHandler):
129
155
  logger.info("File changed: %s", src_path)
130
156
  self._schedule_restart()
131
157
 
158
+ @override
132
159
  def on_created(self, event: FileSystemEvent) -> None:
133
160
  """Handle file creation events.
134
161
 
@@ -142,6 +169,7 @@ class ReloadHandler(FileSystemEventHandler):
142
169
  logger.info("File created: %s", src_path)
143
170
  self._schedule_restart()
144
171
 
172
+ @override
145
173
  def on_deleted(self, event: FileSystemEvent) -> None:
146
174
  """Handle file deletion events.
147
175
 
@@ -214,6 +242,7 @@ class ReloadHandler(FileSystemEventHandler):
214
242
  os._exit(1) # nosec
215
243
 
216
244
 
245
+ @final
217
246
  class FileWatcher:
218
247
  """File watcher with auto-reload functionality."""
219
248
 
waldiez/ws/server.py CHANGED
@@ -1,5 +1,14 @@
1
1
  # SPDX-License-Identifier: Apache-2.0.
2
2
  # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+
4
+ # pyright: reportMissingImports=false,reportUnknownVariableType=false
5
+ # pyright: reportPossiblyUnboundVariable=false,reportUnknownMemberType=false
6
+ # pyright: reportUnknownParameterType=false,reportUnknownArgumentType=false
7
+ # pyright: reportAttributeAccessIssue=false,reportAny=false
8
+ # pyright: reportConstantRedefinition=false,reportAssignmentType=false
9
+ # pyright: reportImportCycles=false,reportGeneralTypeIssues=false
10
+ # pylint: disable=import-error,line-too-long,invalid-name
11
+ # flake8: noqa: E501
3
12
  """WebSocket server implementation for Waldiez."""
4
13
 
5
14
  import asyncio
@@ -9,25 +18,63 @@ import signal
9
18
  import time
10
19
  import traceback
11
20
  import uuid
21
+ from collections.abc import Sequence
12
22
  from pathlib import Path
13
- from typing import Any, Sequence
14
-
15
- import websockets
16
- from websockets.exceptions import ConnectionClosed, WebSocketException
23
+ from typing import Any, final
17
24
 
18
25
  from .client_manager import ClientManager
19
26
  from .errors import ErrorHandler, MessageParsingError, ServerOverloadError
20
27
  from .models import ConnectionNotification
21
- from .reloader import create_file_watcher
22
28
  from .session_manager import SessionManager
23
29
  from .utils import get_available_port, is_port_available
24
30
 
31
+ HAS_WATCHDOG = False
32
+ try:
33
+ from .reloader import FileWatcher, create_file_watcher
34
+
35
+ HAS_WATCHDOG = True
36
+ except ImportError:
37
+
38
+ class FileWatcher: # type: ignore[no-redef]
39
+ """Mock fileWatcher for typing."""
40
+
41
+ def start(self) -> None:
42
+ """Start the watcher."""
43
+
44
+ def stop(self) -> None:
45
+ """Stop the watcher."""
46
+
47
+ # pylint: disable=missing-param-doc,missing-return-doc
48
+ def create_file_watcher(*args: Any, **kwargs: Any) -> FileWatcher: # type: ignore
49
+ """No file watcher available."""
50
+ return FileWatcher(*args, **kwargs)
51
+
52
+
53
+ HAS_WEBSOCKETS = False
54
+ try:
55
+ import websockets # type: ignore[unused-ignore, unused-import, import-not-found, import-untyped]
56
+ from websockets.exceptions import ( # type: ignore[unused-ignore, unused-import, import-not-found, import-untyped]
57
+ ConnectionClosed,
58
+ WebSocketException,
59
+ )
60
+
61
+ HAS_WEBSOCKETS = True
62
+ except ImportError:
63
+ from ._mock import ( # type: ignore[no-redef, unused-ignore, unused-import, import-not-found, import-untyped]
64
+ websockets,
65
+ )
66
+
67
+ ConnectionClosed = websockets.ConnectionClosed # type: ignore[no-redef,unused-ignore,unused-import,import-not-found,import-untyped,misc]
68
+ WebSocketException = websockets.WebSocketException # type: ignore[no-redef,unused-ignore,unused-import,import-not-found,import-untyped,misc]
69
+
70
+
25
71
  logger = logging.getLogger(__name__)
26
72
 
27
73
  CWD = Path.cwd()
28
74
 
29
75
 
30
76
  # pylint: disable=too-many-instance-attributes
77
+ @final
31
78
  class WaldiezWsServer:
32
79
  """WebSocket server for Waldiez."""
33
80
 
@@ -35,6 +82,7 @@ class WaldiezWsServer:
35
82
  self,
36
83
  host: str = "localhost",
37
84
  port: int = 8765,
85
+ auto_reload: bool = False,
38
86
  workspace_dir: Path = CWD,
39
87
  max_clients: int = 1,
40
88
  allowed_origins: Sequence[re.Pattern[str]] | None = None,
@@ -48,11 +96,13 @@ class WaldiezWsServer:
48
96
  Server host address
49
97
  port : int
50
98
  Server port
99
+ auto_reload : bool
100
+ Enable automatic reloading of the server on code changes
51
101
  workspace_dir : Path
52
102
  Path to the workspace directory
53
103
  max_clients : int
54
104
  Maximum number of concurrent clients (default: 1)
55
- allowed_origins : list[str] | None
105
+ allowed_origins : Sequence[re.Pattern[str]] | None
56
106
  List of allowed origins for CORS (default: None)
57
107
  ping_interval : float | None
58
108
  Ping interval in seconds
@@ -69,6 +119,7 @@ class WaldiezWsServer:
69
119
  """
70
120
  self.host = host
71
121
  self.port = port
122
+ self.auto_reload = auto_reload and HAS_WATCHDOG
72
123
  self.workspace_dir = workspace_dir
73
124
  self.max_clients = max_clients
74
125
  self.allowed_origins = allowed_origins
@@ -148,11 +199,14 @@ class WaldiezWsServer:
148
199
  )
149
200
 
150
201
  # Message handling loop
202
+ # noinspection PyTypeChecker
151
203
  async for raw_message in websocket:
152
204
  try:
153
205
  # Parse message
154
206
  if isinstance(raw_message, bytes):
155
- message_str = raw_message.decode("utf-8")
207
+ message_str = raw_message.decode(
208
+ "utf-8", errors="replace"
209
+ )
156
210
  else:
157
211
  # noinspection PyUnreachableCode
158
212
  message_str = (
@@ -230,7 +284,7 @@ class WaldiezWsServer:
230
284
 
231
285
  await self.session_manager.start()
232
286
  # Check port availability
233
- if not is_port_available(self.port):
287
+ if not self.auto_reload and not is_port_available(self.port):
234
288
  logger.warning("Port %d is not available", self.port)
235
289
  self.port = get_available_port()
236
290
  logger.info("Using port %d", self.port)
@@ -239,6 +293,7 @@ class WaldiezWsServer:
239
293
  # pylint: disable=too-many-try-statements,broad-exception-caught
240
294
  try:
241
295
  # Create server
296
+ # noinspection PyTypeChecker
242
297
  self.server = await websockets.serve(
243
298
  self._handle_client,
244
299
  self.host,
@@ -373,6 +428,52 @@ class WaldiezWsServer:
373
428
  return successful
374
429
 
375
430
 
431
+ def _get_file_watcher(
432
+ auto_reload: bool, watch_dirs: set[Path] | None
433
+ ) -> FileWatcher | None:
434
+ file_watcher: FileWatcher | None = None
435
+ if auto_reload and HAS_WATCHDOG:
436
+ # pylint: disable=import-outside-toplevel,too-many-try-statements
437
+ try:
438
+ # Determine watch directories
439
+ if watch_dirs is None:
440
+ project_root = Path(__file__).parents[2]
441
+
442
+ # Watch the actual waldiez package directory
443
+ waldiez_dir = project_root / "waldiez"
444
+ if waldiez_dir.exists():
445
+ watch_dirs = {waldiez_dir}
446
+ logger.info(
447
+ "Auto-reload: watching waldiez package at %s",
448
+ waldiez_dir,
449
+ )
450
+ else:
451
+ # Fallback: watch current directory
452
+ watch_dirs = {Path.cwd()}
453
+ logger.warning(
454
+ "Auto-reload: fallback to current directory %s",
455
+ Path.cwd(),
456
+ )
457
+
458
+ # Create file watcher with restart callback
459
+ file_watcher = create_file_watcher(
460
+ root_dir=Path(__file__).parents[2],
461
+ additional_dirs=list(watch_dirs),
462
+ restart_callback=None,
463
+ )
464
+ file_watcher.start()
465
+ logger.info(
466
+ "Auto-reload enabled for directories: %s",
467
+ {str(dir_) for dir_ in watch_dirs},
468
+ )
469
+
470
+ except ImportError as e:
471
+ logger.warning("Auto-reload not available: %s", e)
472
+ except Exception as e: # pylint: disable=broad-exception-caught
473
+ logger.error("Failed to set up auto-reload: %s", e)
474
+ return file_watcher
475
+
476
+
376
477
  async def run_server(
377
478
  host: str = "localhost",
378
479
  port: int = 8765,
@@ -401,6 +502,7 @@ async def run_server(
401
502
  server = WaldiezWsServer(
402
503
  host=host,
403
504
  port=port,
505
+ auto_reload=auto_reload,
404
506
  workspace_dir=workspace_dir,
405
507
  **server_kwargs,
406
508
  )
@@ -414,52 +516,19 @@ async def run_server(
414
516
 
415
517
  # Register signal handlers
416
518
  loop = asyncio.get_running_loop()
417
- for sig in (signal.SIGTERM, signal.SIGINT):
418
- # noinspection PyTypeChecker
419
- loop.add_signal_handler(sig, signal_handler)
519
+ try:
520
+ for sig in (signal.SIGTERM, signal.SIGINT):
521
+ # noinspection PyTypeChecker
522
+ loop.add_signal_handler(sig, signal_handler)
523
+ except NotImplementedError:
524
+ # Fallback for Windows
525
+ signal.signal(signal.SIGINT, lambda s, f: signal_handler())
526
+ signal.signal(signal.SIGTERM, lambda s, f: signal_handler())
420
527
 
421
528
  # 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
-
529
+ file_watcher = _get_file_watcher(
530
+ auto_reload=auto_reload, watch_dirs=watch_dirs
531
+ )
463
532
  try:
464
533
  # Start server
465
534
  await server.start()
@@ -7,13 +7,14 @@ import logging
7
7
  import time
8
8
  from collections import defaultdict
9
9
  from pathlib import Path
10
- from typing import Any
10
+ from typing import Any, final
11
11
 
12
12
  from .models import ExecutionMode, SessionState, WorkflowSession, WorkflowStatus
13
13
  from .session_stats import SessionStats
14
14
 
15
15
 
16
16
  # noinspection TryExceptPass,PyBroadException
17
+ @final
17
18
  class SessionManager:
18
19
  """Manage workflow sessions across WebSocket clients."""
19
20
 
@@ -67,7 +68,7 @@ class SessionManager:
67
68
  self,
68
69
  session_id: str,
69
70
  client_id: str,
70
- execution_mode: ExecutionMode,
71
+ mode: ExecutionMode,
71
72
  runner: Any = None,
72
73
  temp_file: Path | None = None,
73
74
  metadata: dict[str, Any] | None = None,
@@ -80,7 +81,7 @@ class SessionManager:
80
81
  The ID of the session to create
81
82
  client_id : str
82
83
  The ID of the client creating the session
83
- execution_mode : ExecutionMode
84
+ mode : ExecutionMode
84
85
  The execution mode for the session
85
86
  runner : Any, optional
86
87
  The runner to use for the session
@@ -103,7 +104,7 @@ class SessionManager:
103
104
  session_id=session_id,
104
105
  client_id=client_id,
105
106
  status=WorkflowStatus.IDLE,
106
- execution_mode=execution_mode,
107
+ mode=mode,
107
108
  metadata=metadata or {},
108
109
  )
109
110
  session = WorkflowSession(
@@ -180,9 +181,7 @@ class SessionManager:
180
181
  self._recompute_stats_locked()
181
182
  return True
182
183
 
183
- async def get_session_execution_mode(
184
- self, session_id: str
185
- ) -> ExecutionMode | None:
184
+ async def get_session_mode(self, session_id: str) -> ExecutionMode | None:
186
185
  """Get the execution mode of a workflow session.
187
186
 
188
187
  Parameters
@@ -196,8 +195,8 @@ class SessionManager:
196
195
  The execution mode of the session, or None if it does not exist
197
196
  """
198
197
  async with self._lock:
199
- s = self._sessions.get(session_id)
200
- return s.execution_mode if s else None
198
+ session = self._sessions.get(session_id)
199
+ return session.mode if session else None
201
200
 
202
201
  async def remove_session(self, session_id: str) -> bool:
203
202
  """Remove a workflow session by ID.
@@ -65,7 +65,7 @@ class SessionStats(BaseModel):
65
65
 
66
66
  client_count = self.sessions_by_client.get(state.client_id, 0)
67
67
  self.sessions_by_client[state.client_id] = client_count + 1
68
- mode_key = state.execution_mode.value
68
+ mode_key = state.mode.value
69
69
  mode_count = self.sessions_by_mode.get(mode_key, 0)
70
70
  self.sessions_by_mode[mode_key] = mode_count + 1
71
71
  status_key = state.status.value
waldiez/ws/utils.py CHANGED
@@ -1,5 +1,10 @@
1
1
  # SPDX-License-Identifier: Apache-2.0.
2
2
  # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+ # pylint: disable=import-error,line-too-long
4
+ # pyright: reportUnknownMemberType=false,reportUnknownVariableType=false
5
+ # pyright: reportUnknownArgumentType=false,reportAttributeAccessIssue=false
6
+ # pyright: reportGeneralTypeIssues=false,reportAny=false
7
+ # flake8: noqa: E501
3
8
  """Utilities for WebSocket server management."""
4
9
 
5
10
  import asyncio
@@ -9,9 +14,13 @@ import socket
9
14
  import time
10
15
  from contextlib import closing
11
16
  from dataclasses import asdict, dataclass
12
- from typing import TYPE_CHECKING, Any
17
+ from typing import TYPE_CHECKING, Any, final
18
+
19
+ try:
20
+ import websockets # type: ignore[unused-ignore, unused-import, import-not-found, import-untyped] # noqa
21
+ except ImportError: # pragma: no cover
22
+ from ._mock import websockets # type: ignore[no-redef,unused-ignore]
13
23
 
14
- import websockets
15
24
 
16
25
  if TYPE_CHECKING:
17
26
  from .server import WaldiezWsServer
@@ -74,6 +83,7 @@ class ServerHealth:
74
83
  return my_dict
75
84
 
76
85
 
86
+ @final
77
87
  class HealthChecker:
78
88
  """Health checker for WebSocket server."""
79
89
 
@@ -184,6 +194,7 @@ class HealthChecker:
184
194
 
185
195
 
186
196
  # noinspection PyBroadException
197
+ @final
187
198
  class ConnectionManager:
188
199
  """Manages WebSocket connections and provides utilities."""
189
200
 
@@ -329,7 +340,10 @@ async def test_server_connection(
329
340
  try:
330
341
  uri = f"ws://{host}:{port}"
331
342
 
332
- async with websockets.connect(uri, ping_interval=None) as websocket:
343
+ async with websockets.connect(
344
+ uri,
345
+ ping_interval=None,
346
+ ) as websocket:
333
347
  # Send ping message
334
348
  ping_msg = {"action": "ping"}
335
349
  await websocket.send(json.dumps(ping_msg))
@@ -350,6 +364,7 @@ async def test_server_connection(
350
364
  return result
351
365
 
352
366
 
367
+ # noinspection PyBroadException
353
368
  def is_port_available(port: int) -> bool:
354
369
  """Check if the port is available.
355
370
 
@@ -363,13 +378,26 @@ def is_port_available(port: int) -> bool:
363
378
  bool
364
379
  True if port is available
365
380
  """
381
+ # Check IPv4
366
382
  try:
367
383
  with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
368
384
  sock.bind(("", port))
369
- return True
370
- except OSError:
385
+ except BaseException: # pylint: disable=broad-exception-caught
371
386
  return False
372
387
 
388
+ # Check IPv6
389
+ try: # pragma: no cover
390
+ with closing(
391
+ socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
392
+ ) as sock:
393
+ # Disable dual-stack to only check IPv6
394
+ sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
395
+ sock.bind(("", port))
396
+ except BaseException: # pylint: disable=broad-exception-caught
397
+ return False
398
+
399
+ return True
400
+
373
401
 
374
402
  def get_available_port() -> int:
375
403
  """Get an available port.