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.

Files changed (62) hide show
  1. waldiez/_version.py +1 -1
  2. waldiez/cli.py +1 -0
  3. waldiez/exporting/agent/exporter.py +6 -6
  4. waldiez/exporting/agent/extras/group_manager_agent_extas.py +6 -1
  5. waldiez/exporting/agent/extras/handoffs/after_work.py +1 -0
  6. waldiez/exporting/agent/extras/handoffs/available.py +1 -0
  7. waldiez/exporting/agent/extras/handoffs/handoff.py +1 -0
  8. waldiez/exporting/agent/extras/handoffs/target.py +1 -0
  9. waldiez/exporting/agent/termination.py +1 -0
  10. waldiez/exporting/core/constants.py +3 -1
  11. waldiez/exporting/core/extras/serializer.py +12 -10
  12. waldiez/exporting/core/types.py +1 -0
  13. waldiez/exporting/core/utils/llm_config.py +2 -2
  14. waldiez/exporting/flow/execution_generator.py +1 -0
  15. waldiez/exporting/flow/utils/common.py +1 -1
  16. waldiez/exporting/flow/utils/importing.py +1 -1
  17. waldiez/exporting/flow/utils/logging.py +3 -75
  18. waldiez/io/__init__.py +3 -1
  19. waldiez/io/_ws.py +2 -0
  20. waldiez/io/structured.py +81 -28
  21. waldiez/io/utils.py +16 -10
  22. waldiez/io/ws.py +2 -2
  23. waldiez/models/agents/agent/agent.py +2 -1
  24. waldiez/models/chat/chat.py +1 -0
  25. waldiez/models/chat/chat_data.py +0 -2
  26. waldiez/models/common/base.py +2 -0
  27. waldiez/models/common/handoff.py +2 -0
  28. waldiez/models/common/method_utils.py +2 -0
  29. waldiez/models/model/_llm.py +3 -0
  30. waldiez/models/tool/predefined/_email.py +3 -0
  31. waldiez/models/tool/predefined/_perplexity.py +1 -1
  32. waldiez/models/tool/predefined/_searxng.py +1 -1
  33. waldiez/models/tool/predefined/_wikipedia.py +1 -1
  34. waldiez/running/base_runner.py +81 -20
  35. waldiez/running/post_run.py +6 -0
  36. waldiez/running/pre_run.py +167 -45
  37. waldiez/running/standard_runner.py +5 -5
  38. waldiez/running/step_by_step/breakpoints_mixin.py +368 -44
  39. waldiez/running/step_by_step/command_handler.py +151 -0
  40. waldiez/running/step_by_step/events_processor.py +199 -0
  41. waldiez/running/step_by_step/step_by_step_models.py +358 -41
  42. waldiez/running/step_by_step/step_by_step_runner.py +358 -353
  43. waldiez/running/subprocess_runner/__base__.py +4 -7
  44. waldiez/running/subprocess_runner/_async_runner.py +1 -1
  45. waldiez/running/subprocess_runner/_sync_runner.py +5 -4
  46. waldiez/running/subprocess_runner/runner.py +9 -0
  47. waldiez/running/utils.py +116 -2
  48. waldiez/ws/__init__.py +8 -7
  49. waldiez/ws/_file_handler.py +0 -2
  50. waldiez/ws/_mock.py +74 -0
  51. waldiez/ws/cli.py +27 -3
  52. waldiez/ws/client_manager.py +45 -29
  53. waldiez/ws/models.py +18 -1
  54. waldiez/ws/reloader.py +23 -2
  55. waldiez/ws/server.py +47 -8
  56. waldiez/ws/utils.py +29 -4
  57. {waldiez-0.5.10.dist-info → waldiez-0.6.0.dist-info}/METADATA +53 -44
  58. {waldiez-0.5.10.dist-info → waldiez-0.6.0.dist-info}/RECORD +62 -59
  59. {waldiez-0.5.10.dist-info → waldiez-0.6.0.dist-info}/WHEEL +0 -0
  60. {waldiez-0.5.10.dist-info → waldiez-0.6.0.dist-info}/entry_points.txt +0 -0
  61. {waldiez-0.5.10.dist-info → waldiez-0.6.0.dist-info}/licenses/LICENSE +0 -0
  62. {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: str,
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 : str
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 input response: {user_input}")
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
- # Try to parse as structured JSON first
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 input response: {user_input}")
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
- [sys.executable, "-u", str(setup.file_path)],
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
- sys.executable,
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
- app.registered_commands.append(
42
- CommandInfo(
43
- name="ws",
44
- help="Start the Waldiez WebSocket server.",
45
- callback=serve,
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__ = [
@@ -3,8 +3,6 @@
3
3
 
4
4
  """Files related request handler."""
5
5
 
6
- from __future__ import annotations
7
-
8
6
  import json
9
7
  import logging
10
8
  from pathlib import 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 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
- from .server import run_server
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)
@@ -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
- import websockets
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 (websockets.ConnectionClosed, ConnectionResetError) as e:
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
- code = {
479
- "": "c",
480
- "continue": "c",
481
- "step": "s",
482
- "run": "r",
483
- "quit": "q",
484
- "info": "i",
485
- "help": "h",
486
- "stats": "st",
487
- }.get(msg.action)
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(session_id, data)
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, session_id: str, data: dict[str, Any]
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
- await self.send_message(
661
- UserInputRequestNotification(
662
- session_id=session_id,
663
- request_id=request_id,
664
- prompt=prompt,
665
- password=bool(data.get("password", False)),
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["continue", "step", "run", "quit", "info", "help", "stats"]
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
- from watchdog.events import FileSystemEvent, FileSystemEventHandler
15
- from watchdog.observers import Observer
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):