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.
- waldiez/__init__.py +1 -1
- waldiez/_version.py +1 -1
- waldiez/cli.py +19 -7
- waldiez/cli_extras/jupyter.py +3 -0
- waldiez/cli_extras/runner.py +3 -1
- waldiez/cli_extras/studio.py +3 -1
- waldiez/exporter.py +9 -3
- waldiez/exporting/agent/exporter.py +15 -16
- waldiez/exporting/agent/extras/captain_agent_extras.py +6 -6
- waldiez/exporting/agent/extras/doc_agent_extras.py +6 -6
- waldiez/exporting/agent/extras/group_manager_agent_extas.py +40 -24
- waldiez/exporting/agent/extras/group_member_extras.py +6 -5
- waldiez/exporting/agent/extras/handoffs/after_work.py +2 -1
- waldiez/exporting/agent/extras/handoffs/available.py +2 -1
- waldiez/exporting/agent/extras/handoffs/condition.py +3 -2
- waldiez/exporting/agent/extras/handoffs/handoff.py +2 -1
- waldiez/exporting/agent/extras/handoffs/target.py +7 -4
- waldiez/exporting/agent/extras/rag/chroma_extras.py +27 -19
- waldiez/exporting/agent/extras/rag/mongo_extras.py +8 -8
- waldiez/exporting/agent/extras/rag/pgvector_extras.py +5 -5
- waldiez/exporting/agent/extras/rag/qdrant_extras.py +5 -4
- waldiez/exporting/agent/extras/rag/vector_db_extras.py +1 -1
- waldiez/exporting/agent/extras/rag_user_proxy_agent_extras.py +5 -7
- waldiez/exporting/agent/extras/reasoning_agent_extras.py +3 -5
- waldiez/exporting/agent/termination.py +1 -0
- waldiez/exporting/chats/exporter.py +4 -4
- waldiez/exporting/chats/processor.py +1 -2
- waldiez/exporting/chats/utils/common.py +89 -48
- waldiez/exporting/chats/utils/group.py +9 -9
- waldiez/exporting/chats/utils/nested.py +7 -7
- waldiez/exporting/chats/utils/sequential.py +1 -1
- waldiez/exporting/chats/utils/single.py +2 -2
- waldiez/exporting/core/constants.py +3 -1
- waldiez/exporting/core/content.py +7 -7
- waldiez/exporting/core/context.py +5 -3
- waldiez/exporting/core/exporter.py +5 -3
- waldiez/exporting/core/exporters.py +2 -2
- waldiez/exporting/core/extras/agent_extras/captain_extras.py +2 -2
- waldiez/exporting/core/extras/agent_extras/group_manager_extras.py +2 -2
- waldiez/exporting/core/extras/agent_extras/rag_user_extras.py +2 -2
- waldiez/exporting/core/extras/agent_extras/standard_extras.py +3 -8
- waldiez/exporting/core/extras/base.py +7 -5
- waldiez/exporting/core/extras/flow_extras.py +4 -5
- waldiez/exporting/core/extras/model_extras.py +2 -2
- waldiez/exporting/core/extras/path_resolver.py +1 -2
- waldiez/exporting/core/extras/serializer.py +13 -11
- waldiez/exporting/core/protocols.py +6 -5
- waldiez/exporting/core/result.py +25 -28
- waldiez/exporting/core/types.py +11 -10
- waldiez/exporting/core/utils/llm_config.py +4 -4
- waldiez/exporting/core/validation.py +10 -11
- waldiez/exporting/flow/execution_generator.py +99 -10
- waldiez/exporting/flow/exporter.py +2 -2
- waldiez/exporting/flow/factory.py +2 -2
- waldiez/exporting/flow/file_generator.py +4 -2
- waldiez/exporting/flow/merger.py +5 -3
- waldiez/exporting/flow/orchestrator.py +72 -2
- waldiez/exporting/flow/utils/common.py +6 -6
- waldiez/exporting/flow/utils/importing.py +7 -8
- waldiez/exporting/flow/utils/linting.py +25 -9
- waldiez/exporting/flow/utils/logging.py +5 -77
- waldiez/exporting/models/exporter.py +8 -8
- waldiez/exporting/models/processor.py +5 -5
- waldiez/exporting/tools/exporter.py +2 -2
- waldiez/exporting/tools/processor.py +7 -4
- waldiez/io/__init__.py +11 -5
- waldiez/io/_ws.py +12 -6
- waldiez/io/models/constants.py +10 -10
- waldiez/io/models/content/audio.py +1 -0
- waldiez/io/models/content/base.py +20 -18
- waldiez/io/models/content/file.py +1 -0
- waldiez/io/models/content/image.py +1 -0
- waldiez/io/models/content/text.py +1 -0
- waldiez/io/models/content/video.py +1 -0
- waldiez/io/models/user_input.py +10 -5
- waldiez/io/models/user_response.py +17 -16
- waldiez/io/mqtt.py +18 -31
- waldiez/io/redis.py +18 -22
- waldiez/io/structured.py +122 -70
- waldiez/io/utils.py +19 -10
- waldiez/io/ws.py +7 -3
- waldiez/logger.py +16 -3
- waldiez/models/agents/__init__.py +3 -0
- waldiez/models/agents/agent/agent.py +25 -17
- waldiez/models/agents/agent/agent_data.py +25 -22
- waldiez/models/agents/agent/code_execution.py +9 -11
- waldiez/models/agents/agent/termination_message.py +10 -12
- waldiez/models/agents/agent/update_system_message.py +2 -4
- waldiez/models/agents/agents.py +8 -8
- waldiez/models/agents/assistant/assistant.py +6 -3
- waldiez/models/agents/assistant/assistant_data.py +2 -2
- waldiez/models/agents/captain/captain_agent.py +7 -4
- waldiez/models/agents/captain/captain_agent_data.py +5 -7
- waldiez/models/agents/doc_agent/doc_agent.py +7 -4
- waldiez/models/agents/doc_agent/doc_agent_data.py +9 -10
- waldiez/models/agents/doc_agent/rag_query_engine.py +10 -12
- waldiez/models/agents/extra_requirements.py +3 -3
- waldiez/models/agents/group_manager/group_manager.py +12 -7
- waldiez/models/agents/group_manager/group_manager_data.py +13 -12
- waldiez/models/agents/group_manager/speakers.py +17 -19
- waldiez/models/agents/rag_user_proxy/rag_user_proxy.py +7 -4
- waldiez/models/agents/rag_user_proxy/rag_user_proxy_data.py +4 -1
- waldiez/models/agents/rag_user_proxy/retrieve_config.py +69 -63
- waldiez/models/agents/rag_user_proxy/vector_db_config.py +19 -19
- waldiez/models/agents/reasoning/reasoning_agent.py +7 -4
- waldiez/models/agents/reasoning/reasoning_agent_data.py +3 -2
- waldiez/models/agents/reasoning/reasoning_agent_reason_config.py +8 -8
- waldiez/models/agents/user_proxy/user_proxy.py +6 -3
- waldiez/models/agents/user_proxy/user_proxy_data.py +1 -1
- waldiez/models/chat/chat.py +28 -20
- waldiez/models/chat/chat_data.py +22 -21
- waldiez/models/chat/chat_message.py +9 -9
- waldiez/models/chat/chat_nested.py +9 -9
- waldiez/models/chat/chat_summary.py +6 -6
- waldiez/models/common/__init__.py +2 -0
- waldiez/models/common/ag2_version.py +2 -0
- waldiez/models/common/base.py +2 -0
- waldiez/models/common/dict_utils.py +8 -6
- waldiez/models/common/handoff.py +20 -17
- waldiez/models/common/method_utils.py +9 -7
- waldiez/models/common/naming.py +49 -0
- waldiez/models/flow/flow.py +11 -6
- waldiez/models/flow/flow_data.py +23 -17
- waldiez/models/flow/info.py +3 -3
- waldiez/models/flow/naming.py +2 -1
- waldiez/models/model/_aws.py +11 -13
- waldiez/models/model/_llm.py +8 -0
- waldiez/models/model/_price.py +2 -4
- waldiez/models/model/extra_requirements.py +1 -3
- waldiez/models/model/model.py +2 -2
- waldiez/models/model/model_data.py +21 -21
- waldiez/models/tool/extra_requirements.py +2 -4
- waldiez/models/tool/predefined/_duckduckgo.py +1 -0
- waldiez/models/tool/predefined/_email.py +4 -0
- waldiez/models/tool/predefined/_google.py +1 -0
- waldiez/models/tool/predefined/_perplexity.py +2 -1
- waldiez/models/tool/predefined/_searxng.py +2 -1
- waldiez/models/tool/predefined/_tavily.py +1 -0
- waldiez/models/tool/predefined/_wikipedia.py +2 -1
- waldiez/models/tool/predefined/_youtube.py +1 -0
- waldiez/models/tool/tool.py +8 -5
- waldiez/models/tool/tool_data.py +2 -2
- waldiez/models/waldiez.py +152 -4
- waldiez/runner.py +11 -5
- waldiez/running/async_utils.py +192 -0
- waldiez/running/base_runner.py +155 -241
- waldiez/running/dir_utils.py +52 -0
- waldiez/running/environment.py +10 -44
- waldiez/running/events_mixin.py +252 -0
- waldiez/running/exceptions.py +20 -0
- waldiez/running/gen_seq_diagram.py +18 -15
- waldiez/running/io_utils.py +216 -0
- waldiez/running/protocol.py +11 -5
- waldiez/running/requirements_mixin.py +65 -0
- waldiez/running/results_mixin.py +926 -0
- waldiez/running/standard_runner.py +24 -27
- waldiez/running/step_by_step/breakpoints_mixin.py +503 -47
- waldiez/running/step_by_step/command_handler.py +154 -0
- waldiez/running/step_by_step/events_processor.py +379 -0
- waldiez/running/step_by_step/step_by_step_models.py +425 -41
- waldiez/running/step_by_step/step_by_step_runner.py +437 -382
- waldiez/running/subprocess_runner/__base__.py +13 -8
- waldiez/running/subprocess_runner/_async_runner.py +6 -4
- waldiez/running/subprocess_runner/_sync_runner.py +11 -6
- waldiez/running/subprocess_runner/runner.py +48 -23
- waldiez/running/timeline_processor.py +1 -1
- waldiez/utils/__init__.py +2 -0
- waldiez/utils/conflict_checker.py +4 -4
- waldiez/utils/python_manager.py +415 -0
- waldiez/ws/__init__.py +8 -7
- waldiez/ws/_file_handler.py +18 -20
- waldiez/ws/_mock.py +75 -0
- waldiez/ws/cli.py +58 -10
- waldiez/ws/client_manager.py +77 -53
- waldiez/ws/errors.py +3 -0
- waldiez/ws/models.py +61 -53
- waldiez/ws/reloader.py +33 -4
- waldiez/ws/server.py +121 -52
- waldiez/ws/session_manager.py +8 -9
- waldiez/ws/session_stats.py +1 -1
- waldiez/ws/utils.py +33 -5
- {waldiez-0.5.10.dist-info → waldiez-0.6.1.dist-info}/METADATA +107 -109
- waldiez-0.6.1.dist-info/RECORD +254 -0
- waldiez/running/post_run.py +0 -180
- waldiez/running/pre_run.py +0 -159
- waldiez/running/run_results.py +0 -14
- waldiez/running/utils.py +0 -511
- waldiez-0.5.10.dist-info/RECORD +0 -248
- {waldiez-0.5.10.dist-info → waldiez-0.6.1.dist-info}/WHEEL +0 -0
- {waldiez-0.5.10.dist-info → waldiez-0.6.1.dist-info}/entry_points.txt +0 -0
- {waldiez-0.5.10.dist-info → waldiez-0.6.1.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
15
|
-
|
|
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,
|
|
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 :
|
|
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(
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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 =
|
|
423
|
-
|
|
424
|
-
|
|
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()
|
waldiez/ws/session_manager.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
200
|
-
return
|
|
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.
|
waldiez/ws/session_stats.py
CHANGED
|
@@ -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.
|
|
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(
|
|
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
|
-
|
|
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.
|