waldiez 0.5.10__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 +1 -0
- waldiez/exporting/agent/exporter.py +6 -6
- 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/handoff.py +1 -0
- waldiez/exporting/agent/extras/handoffs/target.py +1 -0
- waldiez/exporting/agent/termination.py +1 -0
- waldiez/exporting/core/constants.py +3 -1
- waldiez/exporting/core/extras/serializer.py +12 -10
- 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/utils/common.py +1 -1
- waldiez/exporting/flow/utils/importing.py +1 -1
- waldiez/exporting/flow/utils/logging.py +3 -75
- waldiez/io/__init__.py +3 -1
- waldiez/io/_ws.py +2 -0
- waldiez/io/structured.py +81 -28
- waldiez/io/utils.py +16 -10
- waldiez/io/ws.py +2 -2
- waldiez/models/agents/agent/agent.py +2 -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/handoff.py +2 -0
- waldiez/models/common/method_utils.py +2 -0
- waldiez/models/model/_llm.py +3 -0
- waldiez/models/tool/predefined/_email.py +3 -0
- waldiez/models/tool/predefined/_perplexity.py +1 -1
- waldiez/models/tool/predefined/_searxng.py +1 -1
- waldiez/models/tool/predefined/_wikipedia.py +1 -1
- waldiez/running/base_runner.py +81 -20
- waldiez/running/post_run.py +6 -0
- waldiez/running/pre_run.py +167 -45
- waldiez/running/standard_runner.py +5 -5
- waldiez/running/step_by_step/breakpoints_mixin.py +368 -44
- 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 +358 -41
- waldiez/running/step_by_step/step_by_step_runner.py +358 -353
- waldiez/running/subprocess_runner/__base__.py +4 -7
- waldiez/running/subprocess_runner/_async_runner.py +1 -1
- waldiez/running/subprocess_runner/_sync_runner.py +5 -4
- waldiez/running/subprocess_runner/runner.py +9 -0
- waldiez/running/utils.py +116 -2
- waldiez/ws/__init__.py +8 -7
- waldiez/ws/_file_handler.py +0 -2
- waldiez/ws/_mock.py +74 -0
- waldiez/ws/cli.py +27 -3
- waldiez/ws/client_manager.py +45 -29
- waldiez/ws/models.py +18 -1
- waldiez/ws/reloader.py +23 -2
- waldiez/ws/server.py +47 -8
- waldiez/ws/utils.py +29 -4
- {waldiez-0.5.10.dist-info → waldiez-0.6.0.dist-info}/METADATA +53 -44
- {waldiez-0.5.10.dist-info → waldiez-0.6.0.dist-info}/RECORD +62 -59
- {waldiez-0.5.10.dist-info → waldiez-0.6.0.dist-info}/WHEEL +0 -0
- {waldiez-0.5.10.dist-info → waldiez-0.6.0.dist-info}/entry_points.txt +0 -0
- {waldiez-0.5.10.dist-info → waldiez-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {waldiez-0.5.10.dist-info → waldiez-0.6.0.dist-info}/licenses/NOTICE.md +0 -0
|
@@ -195,7 +195,7 @@ class BaseSubprocessRunner:
|
|
|
195
195
|
def create_input_response(
|
|
196
196
|
self,
|
|
197
197
|
response_type: str,
|
|
198
|
-
user_input:
|
|
198
|
+
user_input: Any,
|
|
199
199
|
request_id: str | None = None,
|
|
200
200
|
) -> str:
|
|
201
201
|
"""Create input response for subprocess.
|
|
@@ -204,7 +204,7 @@ class BaseSubprocessRunner:
|
|
|
204
204
|
----------
|
|
205
205
|
response_type : str
|
|
206
206
|
Type of the response
|
|
207
|
-
user_input :
|
|
207
|
+
user_input : Any
|
|
208
208
|
User's input response
|
|
209
209
|
request_id : str | None
|
|
210
210
|
Request ID from the input request
|
|
@@ -214,15 +214,12 @@ class BaseSubprocessRunner:
|
|
|
214
214
|
str
|
|
215
215
|
JSON-formatted response
|
|
216
216
|
"""
|
|
217
|
-
response = {
|
|
218
|
-
"type": response_type,
|
|
219
|
-
"data": user_input,
|
|
220
|
-
}
|
|
217
|
+
response: dict[str, Any] = {"type": response_type, "data": user_input}
|
|
221
218
|
|
|
222
219
|
if request_id:
|
|
223
220
|
response["request_id"] = request_id
|
|
224
221
|
|
|
225
|
-
return json.dumps(response) + "\n"
|
|
222
|
+
return json.dumps(response, default=str, ensure_ascii=False) + "\n"
|
|
226
223
|
|
|
227
224
|
def create_output_message(
|
|
228
225
|
self, content: str, stream: str = "stdout", msg_type: str = "output"
|
|
@@ -311,7 +311,7 @@ class AsyncSubprocessRunner(BaseSubprocessRunner):
|
|
|
311
311
|
self.process.stdin.write(response.encode())
|
|
312
312
|
await self.process.stdin.drain()
|
|
313
313
|
|
|
314
|
-
self.logger.debug(f"Sent
|
|
314
|
+
self.logger.debug(f"Sent {response_type}: {user_input}")
|
|
315
315
|
|
|
316
316
|
except asyncio.TimeoutError:
|
|
317
317
|
self.logger.warning("Input request timed out")
|
|
@@ -175,7 +175,6 @@ class SyncSubprocessRunner(BaseSubprocessRunner):
|
|
|
175
175
|
# Use readline with timeout simulation
|
|
176
176
|
if self.process.stdout.readable(): # pragma: no branch
|
|
177
177
|
line = self.process.stdout.readline()
|
|
178
|
-
|
|
179
178
|
if not line: # pragma: no cover
|
|
180
179
|
time.sleep(0.1)
|
|
181
180
|
continue
|
|
@@ -209,7 +208,7 @@ class SyncSubprocessRunner(BaseSubprocessRunner):
|
|
|
209
208
|
# Use readline with timeout simulation
|
|
210
209
|
if self.process.stderr.readable(): # pragma: no branch
|
|
211
210
|
line = self.process.stderr.readline()
|
|
212
|
-
|
|
211
|
+
self.logger.debug("Stderr line: %s", line)
|
|
213
212
|
if not line:
|
|
214
213
|
time.sleep(0.1)
|
|
215
214
|
continue
|
|
@@ -272,9 +271,11 @@ class SyncSubprocessRunner(BaseSubprocessRunner):
|
|
|
272
271
|
line : str
|
|
273
272
|
Decoded line from stdout
|
|
274
273
|
"""
|
|
275
|
-
|
|
274
|
+
self.logger.debug(f"Stdout line: {line}")
|
|
276
275
|
parsed_data = self.parse_output(line, stream="stdout")
|
|
277
276
|
if not parsed_data:
|
|
277
|
+
self.logger.debug("Non-structured output, forwarding as is")
|
|
278
|
+
self.output_queue.put({"type": "print", "data": line}, timeout=1.0)
|
|
278
279
|
return
|
|
279
280
|
if parsed_data.get("type") in ("input_request", "debug_input_request"):
|
|
280
281
|
prompt = parsed_data.get("prompt", "> ")
|
|
@@ -317,7 +318,7 @@ class SyncSubprocessRunner(BaseSubprocessRunner):
|
|
|
317
318
|
self.process.stdin.write(response)
|
|
318
319
|
self.process.stdin.flush()
|
|
319
320
|
|
|
320
|
-
self.logger.debug(f"Sent
|
|
321
|
+
self.logger.debug(f"Sent {response_type}: {user_input}")
|
|
321
322
|
|
|
322
323
|
except queue.Empty:
|
|
323
324
|
self.logger.warning("Input request timed out")
|
|
@@ -18,6 +18,7 @@ from ._sync_runner import SyncSubprocessRunner
|
|
|
18
18
|
# in self._run and self._a_run
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
# noinspection PyUnusedLocal
|
|
21
22
|
class WaldiezSubprocessRunner(WaldiezBaseRunner):
|
|
22
23
|
"""Waldiez runner that uses subprocess execution via standalone runners."""
|
|
23
24
|
|
|
@@ -89,6 +90,7 @@ class WaldiezSubprocessRunner(WaldiezBaseRunner):
|
|
|
89
90
|
mode = kwargs.get("mode", "run")
|
|
90
91
|
if mode not in ["run", "debug"]:
|
|
91
92
|
raise ValueError(f"Invalid mode: {mode}")
|
|
93
|
+
# noinspection PyTypeChecker
|
|
92
94
|
self.mode: Literal["run", "debug"] = mode
|
|
93
95
|
waldiez_file = kwargs.get("waldiez_file")
|
|
94
96
|
self._waldiez_file = self._ensure_waldiez_file(waldiez_file)
|
|
@@ -101,6 +103,7 @@ class WaldiezSubprocessRunner(WaldiezBaseRunner):
|
|
|
101
103
|
return waldiez_file.resolve()
|
|
102
104
|
file_name = self.waldiez.name
|
|
103
105
|
# sanitize file name
|
|
106
|
+
# noinspection RegExpRedundantEscape
|
|
104
107
|
file_name = re.sub(r"[^a-zA-Z0-9_\-\.]", "_", file_name)[:30]
|
|
105
108
|
file_name = f"{file_name}.waldiez"
|
|
106
109
|
with open(file_name, "w", encoding="utf-8") as f:
|
|
@@ -462,6 +465,7 @@ class WaldiezSubprocessRunner(WaldiezBaseRunner):
|
|
|
462
465
|
self,
|
|
463
466
|
results: list[dict[str, Any]],
|
|
464
467
|
output_file: Path,
|
|
468
|
+
waldiez_file: Path,
|
|
465
469
|
uploads_root: Path | None,
|
|
466
470
|
temp_dir: Path,
|
|
467
471
|
skip_mmd: bool,
|
|
@@ -475,6 +479,8 @@ class WaldiezSubprocessRunner(WaldiezBaseRunner):
|
|
|
475
479
|
Results from the workflow execution
|
|
476
480
|
output_file : Path
|
|
477
481
|
Output file path
|
|
482
|
+
waldiez_file : Path
|
|
483
|
+
The waldiez file used/dumped for the run.
|
|
478
484
|
uploads_root : Path | None
|
|
479
485
|
Uploads root directory
|
|
480
486
|
temp_dir : Path
|
|
@@ -491,6 +497,7 @@ class WaldiezSubprocessRunner(WaldiezBaseRunner):
|
|
|
491
497
|
self,
|
|
492
498
|
results: list[dict[str, Any]],
|
|
493
499
|
output_file: Path,
|
|
500
|
+
waldiez_file: Path,
|
|
494
501
|
uploads_root: Path | None,
|
|
495
502
|
temp_dir: Path,
|
|
496
503
|
skip_mmd: bool,
|
|
@@ -504,6 +511,8 @@ class WaldiezSubprocessRunner(WaldiezBaseRunner):
|
|
|
504
511
|
Results from the workflow execution
|
|
505
512
|
output_file : Path
|
|
506
513
|
Output file path
|
|
514
|
+
waldiez_file : Path
|
|
515
|
+
The waldiez file used/dumped for the run.
|
|
507
516
|
uploads_root : Path | None
|
|
508
517
|
Uploads root directory
|
|
509
518
|
temp_dir : Path
|
waldiez/running/utils.py
CHANGED
|
@@ -97,6 +97,71 @@ async def a_chdir(to: Union[str, Path]) -> AsyncIterator[None]:
|
|
|
97
97
|
os.chdir(old_cwd)
|
|
98
98
|
|
|
99
99
|
|
|
100
|
+
def get_python_executable() -> str:
|
|
101
|
+
"""Get the appropriate Python executable path.
|
|
102
|
+
|
|
103
|
+
For bundled applications, this might be different from sys.executable.
|
|
104
|
+
|
|
105
|
+
Returns
|
|
106
|
+
-------
|
|
107
|
+
str
|
|
108
|
+
Path to the Python executable to use for pip operations.
|
|
109
|
+
"""
|
|
110
|
+
# Check if we're in a bundled application (e.g., PyInstaller)
|
|
111
|
+
if getattr(sys, "frozen", False): # pragma: no cover
|
|
112
|
+
# We're in a bundled app
|
|
113
|
+
if hasattr(sys, "_MEIPASS"):
|
|
114
|
+
sys_meipass = getattr(
|
|
115
|
+
sys, "_MEIPASS", str(Path.home() / ".waldiez" / "bin")
|
|
116
|
+
)
|
|
117
|
+
bundled = Path(sys_meipass) / "python"
|
|
118
|
+
if bundled.exists():
|
|
119
|
+
return str(bundled)
|
|
120
|
+
return sys.executable
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# noinspection TryExceptPass,PyBroadException
|
|
124
|
+
def ensure_pip() -> None: # pragma: no cover
|
|
125
|
+
"""Make sure `python -m pip` works (bootstrap via ensurepip if needed)."""
|
|
126
|
+
# pylint: disable=import-outside-toplevel
|
|
127
|
+
# pylint: disable=unused-import,broad-exception-caught
|
|
128
|
+
try:
|
|
129
|
+
import pip # noqa: F401 # pyright: ignore
|
|
130
|
+
|
|
131
|
+
return
|
|
132
|
+
except Exception:
|
|
133
|
+
pass
|
|
134
|
+
try:
|
|
135
|
+
import ensurepip
|
|
136
|
+
|
|
137
|
+
ensurepip.bootstrap(upgrade=True)
|
|
138
|
+
except Exception:
|
|
139
|
+
# If bootstrap fails, we'll still attempt `-m pip` and surface errors.
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def get_pip_install_location() -> str | None:
|
|
144
|
+
"""Determine the best location to install packages.
|
|
145
|
+
|
|
146
|
+
Returns
|
|
147
|
+
-------
|
|
148
|
+
Optional[str]
|
|
149
|
+
The installation target directory, or None for default.
|
|
150
|
+
"""
|
|
151
|
+
if getattr(sys, "frozen", False): # pragma: no cover
|
|
152
|
+
# For bundled apps, try to install to a user-writable location
|
|
153
|
+
if hasattr(sys, "_MEIPASS"):
|
|
154
|
+
app_data = Path.home() / ".waldiez" / "site-packages"
|
|
155
|
+
app_data.mkdir(parents=True, exist_ok=True)
|
|
156
|
+
# Add to sys.path if not already there
|
|
157
|
+
app_data_str = str(app_data)
|
|
158
|
+
if app_data_str not in sys.path:
|
|
159
|
+
# after stdlib
|
|
160
|
+
sys.path.insert(1, app_data_str)
|
|
161
|
+
return app_data_str
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
|
|
100
165
|
def strip_ansi(text: str) -> str:
|
|
101
166
|
"""Remove ANSI escape sequences from text.
|
|
102
167
|
|
|
@@ -128,7 +193,7 @@ def create_sync_subprocess(setup: ProcessSetup) -> subprocess.Popen[bytes]:
|
|
|
128
193
|
The created subprocess.
|
|
129
194
|
"""
|
|
130
195
|
return subprocess.Popen(
|
|
131
|
-
[
|
|
196
|
+
[get_python_executable(), "-u", str(setup.file_path)],
|
|
132
197
|
stdout=subprocess.PIPE,
|
|
133
198
|
stderr=subprocess.PIPE,
|
|
134
199
|
stdin=subprocess.PIPE,
|
|
@@ -153,7 +218,7 @@ async def create_async_subprocess(setup: ProcessSetup) -> Process:
|
|
|
153
218
|
The created asynchronous subprocess.
|
|
154
219
|
"""
|
|
155
220
|
return await asyncio.create_subprocess_exec(
|
|
156
|
-
|
|
221
|
+
get_python_executable(),
|
|
157
222
|
"-u",
|
|
158
223
|
str(setup.file_path),
|
|
159
224
|
# stdout=asyncio.subprocess.PIPE,
|
|
@@ -509,3 +574,52 @@ def _run_in_thread(
|
|
|
509
574
|
|
|
510
575
|
# Use cast since we know the result should be T if no exception occurred
|
|
511
576
|
return cast(T, result_container.result)
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def safe_filename(name: str, ext: str = "") -> str:
|
|
580
|
+
"""
|
|
581
|
+
Make a safe cross-platform filename from an arbitrary string.
|
|
582
|
+
|
|
583
|
+
Parameters
|
|
584
|
+
----------
|
|
585
|
+
name : str
|
|
586
|
+
The string to turn into a safe filename.
|
|
587
|
+
ext : str
|
|
588
|
+
Optional extension (with or without leading dot).
|
|
589
|
+
|
|
590
|
+
Returns
|
|
591
|
+
-------
|
|
592
|
+
str
|
|
593
|
+
A safe filename string.
|
|
594
|
+
"""
|
|
595
|
+
# Normalize extension
|
|
596
|
+
# pylint: disable=inconsistent-quotes
|
|
597
|
+
ext = f".{ext.lstrip('.')}" if ext else ""
|
|
598
|
+
|
|
599
|
+
# Forbidden characters on Windows (also bad on Unix)
|
|
600
|
+
forbidden = r'[<>:"/\\|?*\x00-\x1F]'
|
|
601
|
+
name = re.sub(forbidden, "_", name)
|
|
602
|
+
|
|
603
|
+
# Trim trailing dots/spaces (illegal on Windows)
|
|
604
|
+
name = name.rstrip(". ")
|
|
605
|
+
|
|
606
|
+
# Collapse multiple underscores
|
|
607
|
+
name = re.sub(r"_+", "_", name)
|
|
608
|
+
|
|
609
|
+
# Reserved Windows device names
|
|
610
|
+
reserved = re.compile(
|
|
611
|
+
r"^(con|prn|aux|nul|com[1-9]|lpt[1-9])$", re.IGNORECASE
|
|
612
|
+
)
|
|
613
|
+
if reserved.match(name):
|
|
614
|
+
name = f"_{name}"
|
|
615
|
+
|
|
616
|
+
# Fallback if empty
|
|
617
|
+
if not name:
|
|
618
|
+
name = "file"
|
|
619
|
+
|
|
620
|
+
# Ensure length limit (NTFS max filename length = 255 bytes)
|
|
621
|
+
max_len = 255 - len(ext)
|
|
622
|
+
if len(name) > max_len:
|
|
623
|
+
name = name[:max_len]
|
|
624
|
+
|
|
625
|
+
return f"{name}{ext}"
|
waldiez/ws/__init__.py
CHANGED
|
@@ -18,7 +18,7 @@ from .errors import (
|
|
|
18
18
|
UnsupportedActionError,
|
|
19
19
|
WaldiezServerError,
|
|
20
20
|
)
|
|
21
|
-
from .server import WaldiezWsServer, run_server
|
|
21
|
+
from .server import HAS_WEBSOCKETS, WaldiezWsServer, run_server
|
|
22
22
|
from .session_manager import SessionManager
|
|
23
23
|
from .utils import (
|
|
24
24
|
ConnectionManager,
|
|
@@ -38,13 +38,14 @@ def add_ws_app(app: typer.Typer) -> None:
|
|
|
38
38
|
app : typer.Typer
|
|
39
39
|
The Typer application instance.
|
|
40
40
|
"""
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
if HAS_WEBSOCKETS:
|
|
42
|
+
app.registered_commands.append(
|
|
43
|
+
CommandInfo(
|
|
44
|
+
name="ws",
|
|
45
|
+
help="Start the Waldiez WebSocket server.",
|
|
46
|
+
callback=serve,
|
|
47
|
+
)
|
|
46
48
|
)
|
|
47
|
-
)
|
|
48
49
|
|
|
49
50
|
|
|
50
51
|
__all__ = [
|
waldiez/ws/_file_handler.py
CHANGED
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# SPDX-License-Identifier: Apache-2.0.
|
|
2
2
|
# Copyright (c) 2024 - 2025 Waldiez and contributors.
|
|
3
|
-
# pylint: disable=too-many-locals
|
|
3
|
+
# pylint: disable=too-many-locals,unused-import
|
|
4
4
|
"""CLI interface for Waldiez WebSocket server."""
|
|
5
5
|
|
|
6
6
|
import asyncio
|
|
@@ -12,7 +12,26 @@ from typing import Annotated, Any, Optional, Set
|
|
|
12
12
|
|
|
13
13
|
import typer
|
|
14
14
|
|
|
15
|
-
|
|
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
|
+
|
|
16
35
|
|
|
17
36
|
DEFAULT_WORKSPACE_DIR = Path.cwd()
|
|
18
37
|
|
|
@@ -176,7 +195,12 @@ def serve(
|
|
|
176
195
|
"ping_timeout": ping_timeout,
|
|
177
196
|
"max_size": max_size,
|
|
178
197
|
}
|
|
179
|
-
|
|
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
|
|
180
204
|
logger.info("Starting Waldiez WebSocket server...")
|
|
181
205
|
logger.info("Configuration:")
|
|
182
206
|
logger.info(" Host: %s", host)
|
waldiez/ws/client_manager.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# SPDX-License-Identifier: Apache-2.0.
|
|
2
2
|
# Copyright (c) 2024 - 2025 Waldiez and contributors.
|
|
3
|
-
# pylint: disable=too-many-try-statements,broad-exception-caught
|
|
4
|
-
# pylint: disable=too-complex,too-many-return-statements
|
|
3
|
+
# pylint: disable=too-many-try-statements,broad-exception-caught,line-too-long
|
|
4
|
+
# pylint: disable=too-complex,too-many-return-statements,import-error
|
|
5
5
|
# pyright: reportUnknownMemberType=false,reportAttributeAccessIssue=false
|
|
6
6
|
# pyright: reportUnknownVariableType=false,reportUnknownArgumentType=false
|
|
7
|
-
# pyright: reportAssignmentType=false
|
|
7
|
+
# pyright: reportAssignmentType=false,reportUnknownParameterType=false
|
|
8
8
|
# flake8: noqa: C901
|
|
9
9
|
"""WebSocket client manager: bridges WS <-> subprocess runner."""
|
|
10
10
|
|
|
@@ -15,7 +15,11 @@ import time
|
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
from typing import Any, Callable, Literal
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
try:
|
|
19
|
+
import websockets # type: ignore[unused-ignore, unused-import, import-not-found, import-untyped] # noqa
|
|
20
|
+
except ImportError: # pragma: no cover
|
|
21
|
+
from ._mock import websockets # type: ignore[no-redef,unused-ignore]
|
|
22
|
+
|
|
19
23
|
|
|
20
24
|
from waldiez.models import Waldiez
|
|
21
25
|
from waldiez.running.subprocess_runner.runner import WaldiezSubprocessRunner
|
|
@@ -67,7 +71,7 @@ class ClientManager:
|
|
|
67
71
|
|
|
68
72
|
def __init__(
|
|
69
73
|
self,
|
|
70
|
-
websocket: websockets.ServerConnection,
|
|
74
|
+
websocket: websockets.ServerConnection, # pyright: ignore
|
|
71
75
|
client_id: str,
|
|
72
76
|
session_manager: SessionManager,
|
|
73
77
|
workspace_dir: Path = CWD,
|
|
@@ -141,7 +145,10 @@ class ClientManager:
|
|
|
141
145
|
data = json.loads(json.dumps(payload, default=str))
|
|
142
146
|
await self.websocket.send(json.dumps(data))
|
|
143
147
|
return True
|
|
144
|
-
except (
|
|
148
|
+
except (
|
|
149
|
+
websockets.ConnectionClosed,
|
|
150
|
+
ConnectionResetError,
|
|
151
|
+
) as e: # pyright: ignore
|
|
145
152
|
self.logger.info("Client %s disconnected: %s", self.client_id, e)
|
|
146
153
|
await self.cleanup()
|
|
147
154
|
return False
|
|
@@ -383,8 +390,6 @@ class ClientManager:
|
|
|
383
390
|
ExecutionMode.STEP_BY_STEP,
|
|
384
391
|
session_id=session_id,
|
|
385
392
|
)
|
|
386
|
-
|
|
387
|
-
# Persist desired flags (optional)
|
|
388
393
|
sess = await self.session_manager.get_session(session_id)
|
|
389
394
|
if sess:
|
|
390
395
|
sess.state.metadata.update(
|
|
@@ -474,17 +479,20 @@ class ClientManager:
|
|
|
474
479
|
result="",
|
|
475
480
|
session_id=msg.session_id,
|
|
476
481
|
).model_dump(mode="json")
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
482
|
+
code: str | None
|
|
483
|
+
if msg.action in {"", "c", "s", "r", "q", "i", "h", "st"}:
|
|
484
|
+
code = msg.action if msg.action else "c"
|
|
485
|
+
else:
|
|
486
|
+
code = {
|
|
487
|
+
"": "c",
|
|
488
|
+
"continue": "c",
|
|
489
|
+
"step": "s",
|
|
490
|
+
"run": "r",
|
|
491
|
+
"quit": "q",
|
|
492
|
+
"info": "i",
|
|
493
|
+
"help": "h",
|
|
494
|
+
"stats": "st",
|
|
495
|
+
}.get(msg.action)
|
|
488
496
|
|
|
489
497
|
if not code:
|
|
490
498
|
return StepControlResponse.fail(
|
|
@@ -596,6 +604,7 @@ class ClientManager:
|
|
|
596
604
|
- anything else
|
|
597
605
|
-> fallback stdout SubprocessOutputNotification
|
|
598
606
|
"""
|
|
607
|
+
# self.logger.debug("Handling runner output: %s", data)
|
|
599
608
|
try:
|
|
600
609
|
msg_type = str(data.get("type", "")).lower()
|
|
601
610
|
session_id_raw = data.get("session_id")
|
|
@@ -606,7 +615,9 @@ class ClientManager:
|
|
|
606
615
|
)
|
|
607
616
|
|
|
608
617
|
if msg_type in ("input_request", "debug_input_request"):
|
|
609
|
-
await self._handle_runner_input_request(
|
|
618
|
+
await self._handle_runner_input_request(
|
|
619
|
+
session_id, data, is_debug=msg_type == "debug_input_request"
|
|
620
|
+
)
|
|
610
621
|
return
|
|
611
622
|
|
|
612
623
|
if msg_type.startswith("debug_"):
|
|
@@ -646,7 +657,10 @@ class ClientManager:
|
|
|
646
657
|
)
|
|
647
658
|
|
|
648
659
|
async def _handle_runner_input_request(
|
|
649
|
-
self,
|
|
660
|
+
self,
|
|
661
|
+
session_id: str,
|
|
662
|
+
data: dict[str, Any],
|
|
663
|
+
is_debug: bool,
|
|
650
664
|
) -> None:
|
|
651
665
|
"""Handle an input request from the runner."""
|
|
652
666
|
request_id = str(data.get("request_id", ""))
|
|
@@ -657,15 +671,17 @@ class ClientManager:
|
|
|
657
671
|
await self.session_manager.update_session_status(
|
|
658
672
|
session_id, WorkflowStatus.INPUT_WAITING
|
|
659
673
|
)
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
timeout=float(data.get("timeout", 120.0)),
|
|
667
|
-
)
|
|
674
|
+
notification = UserInputRequestNotification(
|
|
675
|
+
session_id=session_id,
|
|
676
|
+
request_id=request_id,
|
|
677
|
+
prompt=prompt,
|
|
678
|
+
password=bool(data.get("password", False)),
|
|
679
|
+
timeout=float(data.get("timeout", 120.0)),
|
|
668
680
|
)
|
|
681
|
+
msg_dump = notification.model_dump(mode="json", fallback=str)
|
|
682
|
+
if is_debug:
|
|
683
|
+
msg_dump["type"] = "debug_input_request"
|
|
684
|
+
await self.send_message(msg_dump)
|
|
669
685
|
|
|
670
686
|
# pylint: disable=line-too-long
|
|
671
687
|
async def _handle_runner_debug(
|
waldiez/ws/models.py
CHANGED
|
@@ -154,7 +154,24 @@ class StepControlRequest(BaseRequest):
|
|
|
154
154
|
"""Request to control step-by-step execution."""
|
|
155
155
|
|
|
156
156
|
type: Literal["step_control"] = "step_control"
|
|
157
|
-
action: Literal[
|
|
157
|
+
action: Literal[
|
|
158
|
+
"continue",
|
|
159
|
+
"step",
|
|
160
|
+
"run",
|
|
161
|
+
"quit",
|
|
162
|
+
"info",
|
|
163
|
+
"help",
|
|
164
|
+
"stats",
|
|
165
|
+
"",
|
|
166
|
+
"c",
|
|
167
|
+
"s",
|
|
168
|
+
"r",
|
|
169
|
+
"q",
|
|
170
|
+
"i",
|
|
171
|
+
"h",
|
|
172
|
+
"?",
|
|
173
|
+
"st",
|
|
174
|
+
]
|
|
158
175
|
session_id: str
|
|
159
176
|
|
|
160
177
|
|
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
|
|
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
|
|
@@ -11,10 +16,26 @@ from pathlib import Path
|
|
|
11
16
|
from types import TracebackType
|
|
12
17
|
from typing import Any, Callable
|
|
13
18
|
|
|
14
|
-
|
|
15
|
-
|
|
19
|
+
HAS_WATCHDOG = False
|
|
20
|
+
try:
|
|
21
|
+
from watchdog.events import ( # type: ignore[unused-ignore,unused-import,import-not-found,import-untyped]
|
|
22
|
+
FileSystemEvent,
|
|
23
|
+
FileSystemEventHandler,
|
|
24
|
+
)
|
|
25
|
+
from watchdog.observers import ( # type: ignore[unused-ignore,unused-import,import-not-found,import-untyped]
|
|
26
|
+
Observer,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
HAS_WATCHDOG = True
|
|
30
|
+
except ImportError as exc:
|
|
31
|
+
raise ImportError(
|
|
32
|
+
"The 'watchdog' package is required for auto-reload functionality. "
|
|
33
|
+
"Please install it using 'pip install watchdog'."
|
|
34
|
+
) from exc
|
|
16
35
|
|
|
17
36
|
logger = logging.getLogger(__name__)
|
|
37
|
+
fsevents_logger = logging.getLogger("fsevents")
|
|
38
|
+
fsevents_logger.setLevel(logging.WARNING) # Reduce noise from fsevents
|
|
18
39
|
|
|
19
40
|
|
|
20
41
|
class ReloadHandler(FileSystemEventHandler):
|