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.
- waldiez/_version.py +1 -1
- waldiez/cli.py +113 -24
- waldiez/exporting/agent/exporter.py +9 -6
- waldiez/exporting/agent/extras/captain_agent_extras.py +44 -7
- waldiez/exporting/agent/extras/group_manager_agent_extas.py +6 -1
- waldiez/exporting/agent/extras/handoffs/after_work.py +1 -0
- waldiez/exporting/agent/extras/handoffs/available.py +1 -0
- waldiez/exporting/agent/extras/handoffs/condition.py +3 -1
- waldiez/exporting/agent/extras/handoffs/handoff.py +1 -0
- waldiez/exporting/agent/extras/handoffs/target.py +1 -0
- waldiez/exporting/agent/termination.py +1 -0
- waldiez/exporting/chats/utils/common.py +25 -23
- waldiez/exporting/core/__init__.py +0 -2
- waldiez/exporting/core/constants.py +3 -1
- waldiez/exporting/core/context.py +13 -13
- waldiez/exporting/core/extras/serializer.py +12 -10
- waldiez/exporting/core/protocols.py +0 -141
- waldiez/exporting/core/result.py +5 -5
- waldiez/exporting/core/types.py +1 -0
- waldiez/exporting/core/utils/llm_config.py +2 -2
- waldiez/exporting/flow/execution_generator.py +1 -0
- waldiez/exporting/flow/merger.py +2 -2
- waldiez/exporting/flow/orchestrator.py +1 -0
- waldiez/exporting/flow/utils/common.py +3 -3
- waldiez/exporting/flow/utils/importing.py +1 -0
- waldiez/exporting/flow/utils/logging.py +7 -80
- waldiez/exporting/tools/exporter.py +5 -0
- waldiez/exporting/tools/factory.py +4 -0
- waldiez/exporting/tools/processor.py +5 -1
- waldiez/io/__init__.py +3 -1
- waldiez/io/_ws.py +15 -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 +98 -45
- waldiez/io/utils.py +17 -11
- waldiez/io/ws.py +10 -12
- waldiez/logger.py +180 -63
- waldiez/models/agents/agent/agent.py +2 -1
- waldiez/models/agents/agent/update_system_message.py +0 -2
- waldiez/models/agents/doc_agent/doc_agent.py +8 -1
- waldiez/models/chat/chat.py +1 -0
- waldiez/models/chat/chat_data.py +0 -2
- waldiez/models/common/base.py +2 -0
- waldiez/models/common/dict_utils.py +169 -40
- waldiez/models/common/handoff.py +2 -0
- waldiez/models/common/method_utils.py +2 -0
- waldiez/models/flow/flow.py +6 -6
- waldiez/models/flow/info.py +5 -1
- waldiez/models/model/_llm.py +31 -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 +477 -0
- waldiez/models/tool/predefined/_google.py +4 -1
- waldiez/models/tool/predefined/_perplexity.py +4 -1
- waldiez/models/tool/predefined/_searxng.py +4 -1
- waldiez/models/tool/predefined/_tavily.py +4 -1
- waldiez/models/tool/predefined/_wikipedia.py +5 -2
- 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 +381 -363
- waldiez/running/environment.py +1 -0
- waldiez/running/exceptions.py +9 -0
- waldiez/running/post_run.py +10 -4
- waldiez/running/pre_run.py +199 -66
- waldiez/running/protocol.py +21 -101
- waldiez/running/run_results.py +1 -1
- waldiez/running/standard_runner.py +83 -276
- waldiez/running/step_by_step/__init__.py +46 -0
- waldiez/running/step_by_step/breakpoints_mixin.py +512 -0
- waldiez/running/step_by_step/command_handler.py +151 -0
- waldiez/running/step_by_step/events_processor.py +199 -0
- waldiez/running/step_by_step/step_by_step_models.py +541 -0
- waldiez/running/step_by_step/step_by_step_runner.py +750 -0
- waldiez/running/subprocess_runner/__base__.py +279 -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 +456 -0
- waldiez/running/subprocess_runner/runner.py +570 -0
- waldiez/running/timeline_processor.py +1 -1
- waldiez/running/utils.py +492 -3
- waldiez/utils/version.py +2 -6
- waldiez/ws/__init__.py +71 -0
- waldiez/ws/__main__.py +15 -0
- waldiez/ws/_file_handler.py +199 -0
- waldiez/ws/_mock.py +74 -0
- waldiez/ws/cli.py +235 -0
- waldiez/ws/client_manager.py +851 -0
- waldiez/ws/errors.py +416 -0
- waldiez/ws/models.py +988 -0
- waldiez/ws/reloader.py +363 -0
- waldiez/ws/server.py +508 -0
- waldiez/ws/session_manager.py +393 -0
- waldiez/ws/session_stats.py +83 -0
- waldiez/ws/utils.py +410 -0
- {waldiez-0.5.9.dist-info → waldiez-0.6.0.dist-info}/METADATA +105 -96
- {waldiez-0.5.9.dist-info → waldiez-0.6.0.dist-info}/RECORD +108 -83
- waldiez/running/patch_io_stream.py +0 -210
- {waldiez-0.5.9.dist-info → waldiez-0.6.0.dist-info}/WHEEL +0 -0
- {waldiez-0.5.9.dist-info → waldiez-0.6.0.dist-info}/entry_points.txt +0 -0
- {waldiez-0.5.9.dist-info → waldiez-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {waldiez-0.5.9.dist-info → waldiez-0.6.0.dist-info}/licenses/NOTICE.md +0 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0.
|
|
2
|
+
# Copyright (c) 2024 - 2025 Waldiez and contributors.
|
|
3
|
+
|
|
4
|
+
"""Files related request handler."""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from waldiez.exporter import WaldiezExporter
|
|
12
|
+
from waldiez.models import Waldiez
|
|
13
|
+
|
|
14
|
+
from .models import (
|
|
15
|
+
ConvertWorkflowRequest,
|
|
16
|
+
ConvertWorkflowResponse,
|
|
17
|
+
SaveFlowRequest,
|
|
18
|
+
SaveFlowResponse,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FileRequestHandler:
|
|
23
|
+
"""Handles file-related requests."""
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def handle_save_flow_request(
|
|
27
|
+
msg: SaveFlowRequest,
|
|
28
|
+
workspace_dir: Path,
|
|
29
|
+
client_id: str,
|
|
30
|
+
logger: logging.Logger,
|
|
31
|
+
) -> dict[str, Any]:
|
|
32
|
+
"""Handle save flow request.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
msg : SaveFlowRequest
|
|
37
|
+
The save flow request message.
|
|
38
|
+
workspace_dir : Path
|
|
39
|
+
The workspace directory.
|
|
40
|
+
client_id : str
|
|
41
|
+
The client ID.
|
|
42
|
+
logger : logging.Logger
|
|
43
|
+
The logger instance.
|
|
44
|
+
|
|
45
|
+
Returns
|
|
46
|
+
-------
|
|
47
|
+
dict[str, Any]
|
|
48
|
+
The response dictionary.
|
|
49
|
+
"""
|
|
50
|
+
filename = msg.filename or f"waldiez_{client_id}.waldiez"
|
|
51
|
+
try:
|
|
52
|
+
output_path = resolve_output_path(
|
|
53
|
+
filename,
|
|
54
|
+
workspace_dir=workspace_dir,
|
|
55
|
+
expected_ext="waldiez",
|
|
56
|
+
)
|
|
57
|
+
except ValueError as exc:
|
|
58
|
+
logger.error("Error resolving output path: %s", exc)
|
|
59
|
+
return SaveFlowResponse.fail(
|
|
60
|
+
error=f"Invalid output path: {exc}",
|
|
61
|
+
file_path=filename,
|
|
62
|
+
).model_dump(mode="json")
|
|
63
|
+
# pylint: disable=too-many-try-statements
|
|
64
|
+
try:
|
|
65
|
+
if output_path.exists() and not msg.force_overwrite:
|
|
66
|
+
return SaveFlowResponse.fail(
|
|
67
|
+
error=f"File exists: {output_path}",
|
|
68
|
+
file_path=str(output_path.relative_to(workspace_dir)),
|
|
69
|
+
).model_dump(mode="json")
|
|
70
|
+
|
|
71
|
+
# Parent dir already created by resolve_output_path
|
|
72
|
+
output_path.write_text(msg.flow_data, encoding="utf-8")
|
|
73
|
+
|
|
74
|
+
return SaveFlowResponse.ok(
|
|
75
|
+
file_path=str(output_path.relative_to(workspace_dir))
|
|
76
|
+
).model_dump(mode="json")
|
|
77
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
78
|
+
logger.error("Error saving flow: %s", e)
|
|
79
|
+
return SaveFlowResponse.fail(error=str(e)).model_dump(mode="json")
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def handle_convert_workflow_request(
|
|
83
|
+
msg: ConvertWorkflowRequest,
|
|
84
|
+
client_id: str,
|
|
85
|
+
workspace_dir: Path,
|
|
86
|
+
logger: logging.Logger,
|
|
87
|
+
) -> dict[str, Any]:
|
|
88
|
+
"""Handle a convert workflow request.
|
|
89
|
+
|
|
90
|
+
Parameters
|
|
91
|
+
----------
|
|
92
|
+
msg : ConvertWorkflowRequest
|
|
93
|
+
The convert workflow request message.
|
|
94
|
+
client_id : str
|
|
95
|
+
The client ID.
|
|
96
|
+
workspace_dir : Path
|
|
97
|
+
The workspace directory.
|
|
98
|
+
logger : logging.Logger
|
|
99
|
+
The logger instance.
|
|
100
|
+
|
|
101
|
+
Returns
|
|
102
|
+
-------
|
|
103
|
+
dict[str, Any]
|
|
104
|
+
The response dictionary.
|
|
105
|
+
"""
|
|
106
|
+
target_format = (msg.target_format or "").strip().lower()
|
|
107
|
+
if target_format not in {"py", "ipynb"}:
|
|
108
|
+
return ConvertWorkflowResponse.fail(
|
|
109
|
+
error=f"Unsupported target format: {target_format}",
|
|
110
|
+
target_format=target_format,
|
|
111
|
+
).model_dump(mode="json")
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
waldiez_data = Waldiez.from_dict(json.loads(msg.flow_data))
|
|
115
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
116
|
+
return ConvertWorkflowResponse.fail(
|
|
117
|
+
error=f"Invalid flow_data: {e}",
|
|
118
|
+
target_format=target_format,
|
|
119
|
+
).model_dump(mode="json")
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
# Use normalized target_format for default name
|
|
123
|
+
filename = msg.output_path or f"waldiez_{client_id}.{target_format}"
|
|
124
|
+
output_path = resolve_output_path(
|
|
125
|
+
filename,
|
|
126
|
+
workspace_dir=workspace_dir,
|
|
127
|
+
expected_ext=target_format,
|
|
128
|
+
)
|
|
129
|
+
except ValueError as exc:
|
|
130
|
+
logger.error("Error resolving output path: %s", exc)
|
|
131
|
+
return ConvertWorkflowResponse.fail(
|
|
132
|
+
error=f"Invalid output path: {exc}",
|
|
133
|
+
target_format=target_format,
|
|
134
|
+
).model_dump(mode="json")
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
exporter = WaldiezExporter(waldiez_data)
|
|
138
|
+
exporter.export(path=output_path, force=True, structured_io=True)
|
|
139
|
+
|
|
140
|
+
return ConvertWorkflowResponse.ok(
|
|
141
|
+
target_format=target_format,
|
|
142
|
+
output_path=str(output_path.relative_to(workspace_dir)),
|
|
143
|
+
).model_dump(mode="json")
|
|
144
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
145
|
+
logger.error("Error converting workflow: %s", e)
|
|
146
|
+
return ConvertWorkflowResponse.fail(
|
|
147
|
+
error=str(e), target_format=target_format
|
|
148
|
+
).model_dump(mode="json")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def resolve_output_path(
|
|
152
|
+
filename: str,
|
|
153
|
+
workspace_dir: Path,
|
|
154
|
+
expected_ext: str | None = None,
|
|
155
|
+
) -> Path:
|
|
156
|
+
"""
|
|
157
|
+
Resolve output path inside the workspace.
|
|
158
|
+
|
|
159
|
+
Parameters
|
|
160
|
+
----------
|
|
161
|
+
filename : str
|
|
162
|
+
Provided filename (may be relative or absolute).
|
|
163
|
+
workspace_dir : Path
|
|
164
|
+
The workspace directory to resolve the output path against.
|
|
165
|
+
expected_ext : str | None
|
|
166
|
+
If provided, ensure the filename ends with this extension.
|
|
167
|
+
|
|
168
|
+
Returns
|
|
169
|
+
-------
|
|
170
|
+
Path
|
|
171
|
+
Resolved absolute path, with parent directories created.
|
|
172
|
+
|
|
173
|
+
Raises
|
|
174
|
+
------
|
|
175
|
+
ValueError
|
|
176
|
+
If the output path is outside the workspace.
|
|
177
|
+
"""
|
|
178
|
+
# Normalize workspace_dir to an absolute path
|
|
179
|
+
workspace_dir = workspace_dir.resolve()
|
|
180
|
+
|
|
181
|
+
output_path = Path(filename)
|
|
182
|
+
if not output_path.is_absolute():
|
|
183
|
+
output_path = workspace_dir / output_path
|
|
184
|
+
|
|
185
|
+
if expected_ext and output_path.suffix != f".{expected_ext}":
|
|
186
|
+
output_path = output_path.with_suffix(f".{expected_ext}")
|
|
187
|
+
|
|
188
|
+
output_path = output_path.resolve()
|
|
189
|
+
|
|
190
|
+
# Ensure output_path is a subpath of workspace_dir
|
|
191
|
+
try:
|
|
192
|
+
output_path.relative_to(workspace_dir)
|
|
193
|
+
except ValueError as exc:
|
|
194
|
+
raise ValueError(
|
|
195
|
+
f"Output path {output_path} is outside workspace {workspace_dir}"
|
|
196
|
+
) from exc
|
|
197
|
+
|
|
198
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
199
|
+
return output_path
|
waldiez/ws/_mock.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0.
|
|
2
|
+
# Copyright (c) 2024 - 2025 Waldiez and contributors.
|
|
3
|
+
|
|
4
|
+
"""Mock websockets for linters."""
|
|
5
|
+
# pylint: disable=invalid-name,line-too-long,unused-argument,too-few-public-methods,no-self-use
|
|
6
|
+
# pylint: disable=missing-class-docstring,missing-function-docstring,missing-return-doc
|
|
7
|
+
# flake8: noqa: E501, D101, D102, D106
|
|
8
|
+
|
|
9
|
+
from typing import Any # pragma: no cover
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# noinspection PyPep8Naming, PyMethodMayBeStatic,PyUnusedLocal
|
|
13
|
+
class websockets: # pragma: no cover
|
|
14
|
+
# noinspection PyMethodMayBeStatic
|
|
15
|
+
class ClientConnection:
|
|
16
|
+
async def __aenter__(self) -> "websockets.ClientConnection":
|
|
17
|
+
return self
|
|
18
|
+
|
|
19
|
+
async def send(self, data: Any) -> None:
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
async def close(self) -> None:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
async def recv(self) -> str:
|
|
26
|
+
return ""
|
|
27
|
+
|
|
28
|
+
# noinspection PyPep8Naming
|
|
29
|
+
class exceptions:
|
|
30
|
+
class ConnectionClosedError(Exception): ...
|
|
31
|
+
|
|
32
|
+
class ConnectionClosedOK(Exception): ...
|
|
33
|
+
|
|
34
|
+
class Server:
|
|
35
|
+
def close(self) -> None:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
async def wait_closed(self) -> None:
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
class ServerConnection:
|
|
42
|
+
remote_address: str
|
|
43
|
+
|
|
44
|
+
# noinspection PyPep8Naming
|
|
45
|
+
class request:
|
|
46
|
+
headers: dict[str, str]
|
|
47
|
+
|
|
48
|
+
async def close(
|
|
49
|
+
self, code: int = 1000, reason: str = "Normal Closure"
|
|
50
|
+
) -> None:
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
# noinspection PyPep8Naming
|
|
54
|
+
async def ConnectionClosedOK(self, *args: Any, **kwargs: Any) -> None:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
async def send(self, data: Any) -> None:
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
async def serve(*args: Any, **kwargs: Any) -> "websockets.Server":
|
|
62
|
+
return websockets.Server()
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
async def connect(
|
|
66
|
+
*args: Any, **kwargs: Any
|
|
67
|
+
) -> "websockets.ClientConnection":
|
|
68
|
+
return websockets.ClientConnection()
|
|
69
|
+
|
|
70
|
+
class ConnectionClosed(Exception):
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
class WebSocketException(Exception):
|
|
74
|
+
pass
|
waldiez/ws/cli.py
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0.
|
|
2
|
+
# Copyright (c) 2024 - 2025 Waldiez and contributors.
|
|
3
|
+
# pylint: disable=too-many-locals,unused-import
|
|
4
|
+
"""CLI interface for Waldiez WebSocket server."""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import logging
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Annotated, Any, Optional, Set
|
|
12
|
+
|
|
13
|
+
import typer
|
|
14
|
+
|
|
15
|
+
HAS_WATCHDOG = False
|
|
16
|
+
try:
|
|
17
|
+
from .reloader import FileWatcher # pyright: ignore # noqa: F401
|
|
18
|
+
|
|
19
|
+
HAS_WATCHDOG = True # pyright: ignore
|
|
20
|
+
except ImportError:
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
HAS_WEBSOCKETS = False
|
|
24
|
+
try:
|
|
25
|
+
from .server import run_server
|
|
26
|
+
|
|
27
|
+
HAS_WEBSOCKETS = True # pyright: ignore
|
|
28
|
+
except ImportError:
|
|
29
|
+
# pylint: disable=missing-param-doc,missing-raises-doc
|
|
30
|
+
# noinspection PyUnusedLocal
|
|
31
|
+
async def run_server(*args: Any, **kwargs: Any) -> None: # type: ignore
|
|
32
|
+
"""No WebSocket server available."""
|
|
33
|
+
raise NotImplementedError("WebSocket server is not available.")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
DEFAULT_WORKSPACE_DIR = Path.cwd()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def setup_logging(verbose: bool = False) -> None:
|
|
40
|
+
"""Set up logging configuration.
|
|
41
|
+
|
|
42
|
+
Parameters
|
|
43
|
+
----------
|
|
44
|
+
verbose : bool
|
|
45
|
+
Enable verbose logging
|
|
46
|
+
"""
|
|
47
|
+
level = logging.DEBUG if verbose else logging.INFO
|
|
48
|
+
format_str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
49
|
+
|
|
50
|
+
logging.basicConfig(
|
|
51
|
+
level=level,
|
|
52
|
+
format=format_str,
|
|
53
|
+
handlers=[logging.StreamHandler(sys.stdout)],
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Set websockets logger to WARNING to reduce noise
|
|
57
|
+
logging.getLogger("websockets").setLevel(logging.WARNING)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
app = typer.Typer(
|
|
61
|
+
name="waldiez-ws",
|
|
62
|
+
help="Waldiez WebSocket server",
|
|
63
|
+
add_completion=False,
|
|
64
|
+
context_settings={
|
|
65
|
+
"help_option_names": ["-h", "--help"],
|
|
66
|
+
"allow_extra_args": True,
|
|
67
|
+
"ignore_unknown_options": True,
|
|
68
|
+
},
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@app.command()
|
|
73
|
+
def serve(
|
|
74
|
+
host: Annotated[
|
|
75
|
+
str, typer.Option(help="Server host address")
|
|
76
|
+
] = "localhost",
|
|
77
|
+
port: Annotated[int, typer.Option(help="Server port")] = 8765,
|
|
78
|
+
max_clients: Annotated[
|
|
79
|
+
int,
|
|
80
|
+
typer.Option(
|
|
81
|
+
"--max-clients", help="Maximum number of concurrent clients"
|
|
82
|
+
),
|
|
83
|
+
] = 1,
|
|
84
|
+
allowed_origins: Annotated[
|
|
85
|
+
Optional[list[str]],
|
|
86
|
+
typer.Option(
|
|
87
|
+
"--allowed-origin",
|
|
88
|
+
help=(
|
|
89
|
+
"Allowed origins for CORS (can be used multiple times). "
|
|
90
|
+
"Supports regex patterns. "
|
|
91
|
+
"Examples: 'https://example.com', '.*\\.mydomain\\.com'"
|
|
92
|
+
),
|
|
93
|
+
),
|
|
94
|
+
] = None,
|
|
95
|
+
auto_reload: Annotated[
|
|
96
|
+
bool,
|
|
97
|
+
typer.Option(
|
|
98
|
+
"--auto-reload",
|
|
99
|
+
help=(
|
|
100
|
+
"Enable auto-reload on file changes "
|
|
101
|
+
"(requires: pip install watchdog)"
|
|
102
|
+
),
|
|
103
|
+
),
|
|
104
|
+
] = False,
|
|
105
|
+
watch_dir: Annotated[
|
|
106
|
+
Optional[list[Path]],
|
|
107
|
+
typer.Option(
|
|
108
|
+
"--watch-dir",
|
|
109
|
+
help=(
|
|
110
|
+
"Additional directories to watch for auto-reload "
|
|
111
|
+
"(can be used multiple times)"
|
|
112
|
+
),
|
|
113
|
+
),
|
|
114
|
+
] = None,
|
|
115
|
+
workspace_dir: Annotated[
|
|
116
|
+
Path,
|
|
117
|
+
typer.Option(
|
|
118
|
+
"--workspace",
|
|
119
|
+
help="Path to the workspace directory",
|
|
120
|
+
resolve_path=True,
|
|
121
|
+
dir_okay=True,
|
|
122
|
+
file_okay=False,
|
|
123
|
+
),
|
|
124
|
+
] = DEFAULT_WORKSPACE_DIR,
|
|
125
|
+
ping_interval: Annotated[
|
|
126
|
+
float,
|
|
127
|
+
typer.Option(
|
|
128
|
+
"--ping-interval", help="WebSocket ping interval in seconds"
|
|
129
|
+
),
|
|
130
|
+
] = 20.0,
|
|
131
|
+
ping_timeout: Annotated[
|
|
132
|
+
float,
|
|
133
|
+
typer.Option(
|
|
134
|
+
"--ping-timeout", help="WebSocket ping timeout in seconds"
|
|
135
|
+
),
|
|
136
|
+
] = 20.0,
|
|
137
|
+
max_size: Annotated[
|
|
138
|
+
int, typer.Option("--max-size", help="Maximum message size in bytes")
|
|
139
|
+
] = 8388608,
|
|
140
|
+
verbose: Annotated[
|
|
141
|
+
bool, typer.Option("--verbose", "-v", help="Enable verbose logging")
|
|
142
|
+
] = False,
|
|
143
|
+
) -> None:
|
|
144
|
+
"""Start Waldiez WebSocket server.
|
|
145
|
+
|
|
146
|
+
Parameters
|
|
147
|
+
----------
|
|
148
|
+
host : str
|
|
149
|
+
Server host address
|
|
150
|
+
port : int
|
|
151
|
+
Server port
|
|
152
|
+
max_clients : int
|
|
153
|
+
Maximum number of concurrent clients
|
|
154
|
+
allowed_origins : list[str] | None
|
|
155
|
+
List of allowed origins for CORS (default: None)
|
|
156
|
+
auto_reload : bool
|
|
157
|
+
Enable auto-reload on file changes
|
|
158
|
+
watch_dir : tuple[Path, ...]
|
|
159
|
+
Additional directories to watch for auto-reload
|
|
160
|
+
workspace_dir : Path
|
|
161
|
+
Path to the workspace directory
|
|
162
|
+
ping_interval : float
|
|
163
|
+
WebSocket ping interval in seconds
|
|
164
|
+
ping_timeout : float
|
|
165
|
+
WebSocket ping timeout in seconds
|
|
166
|
+
max_size : int
|
|
167
|
+
Maximum message size in bytes
|
|
168
|
+
verbose : bool
|
|
169
|
+
Enable verbose logging
|
|
170
|
+
"""
|
|
171
|
+
setup_logging(verbose)
|
|
172
|
+
|
|
173
|
+
logger = logging.getLogger(__name__)
|
|
174
|
+
|
|
175
|
+
# Convert watch directories to set
|
|
176
|
+
watch_dirs: Optional[Set[Path]] = None
|
|
177
|
+
if watch_dir:
|
|
178
|
+
watch_dirs = set(watch_dir)
|
|
179
|
+
|
|
180
|
+
compiled_origins: list[re.Pattern[str]] | None = None
|
|
181
|
+
if allowed_origins:
|
|
182
|
+
try:
|
|
183
|
+
compiled_origins = [
|
|
184
|
+
re.compile(pattern) for pattern in allowed_origins
|
|
185
|
+
]
|
|
186
|
+
except re.error as e:
|
|
187
|
+
typer.echo(f"Invalid regex pattern in allowed origins: {e}")
|
|
188
|
+
sys.exit(1)
|
|
189
|
+
|
|
190
|
+
# Server configuration
|
|
191
|
+
server_config: dict[str, Any] = {
|
|
192
|
+
"max_clients": max_clients,
|
|
193
|
+
"allowed_origins": compiled_origins,
|
|
194
|
+
"ping_interval": ping_interval,
|
|
195
|
+
"ping_timeout": ping_timeout,
|
|
196
|
+
"max_size": max_size,
|
|
197
|
+
}
|
|
198
|
+
if not HAS_WATCHDOG and auto_reload:
|
|
199
|
+
typer.echo(
|
|
200
|
+
"Auto-reload requires the 'watchdog' package. "
|
|
201
|
+
"Please install it with: pip install watchdog"
|
|
202
|
+
)
|
|
203
|
+
auto_reload = False
|
|
204
|
+
logger.info("Starting Waldiez WebSocket server...")
|
|
205
|
+
logger.info("Configuration:")
|
|
206
|
+
logger.info(" Host: %s", host)
|
|
207
|
+
logger.info(" Port: %d", port)
|
|
208
|
+
logger.info(" Max clients: %d", max_clients)
|
|
209
|
+
logger.info(" Allowed origins: %s", allowed_origins or ["*"])
|
|
210
|
+
logger.info(" Auto-reload: %s", auto_reload)
|
|
211
|
+
logger.info(" Workspace directory: %s", workspace_dir)
|
|
212
|
+
|
|
213
|
+
if watch_dirs:
|
|
214
|
+
logger.info(" Watch directories: %s", watch_dirs)
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
asyncio.run(
|
|
218
|
+
run_server(
|
|
219
|
+
host=host,
|
|
220
|
+
port=port,
|
|
221
|
+
workspace_dir=workspace_dir,
|
|
222
|
+
auto_reload=auto_reload,
|
|
223
|
+
watch_dirs=watch_dirs,
|
|
224
|
+
**server_config,
|
|
225
|
+
)
|
|
226
|
+
)
|
|
227
|
+
except KeyboardInterrupt:
|
|
228
|
+
logger.info("Server stopped by user")
|
|
229
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
230
|
+
logger.error("Server error: %s", e)
|
|
231
|
+
sys.exit(1)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
if __name__ == "__main__":
|
|
235
|
+
app()
|