waldiez 0.5.10__py3-none-any.whl → 0.6.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (192) hide show
  1. waldiez/__init__.py +1 -1
  2. waldiez/_version.py +1 -1
  3. waldiez/cli.py +19 -7
  4. waldiez/cli_extras/jupyter.py +3 -0
  5. waldiez/cli_extras/runner.py +3 -1
  6. waldiez/cli_extras/studio.py +3 -1
  7. waldiez/exporter.py +9 -3
  8. waldiez/exporting/agent/exporter.py +15 -16
  9. waldiez/exporting/agent/extras/captain_agent_extras.py +6 -6
  10. waldiez/exporting/agent/extras/doc_agent_extras.py +6 -6
  11. waldiez/exporting/agent/extras/group_manager_agent_extas.py +40 -24
  12. waldiez/exporting/agent/extras/group_member_extras.py +6 -5
  13. waldiez/exporting/agent/extras/handoffs/after_work.py +2 -1
  14. waldiez/exporting/agent/extras/handoffs/available.py +2 -1
  15. waldiez/exporting/agent/extras/handoffs/condition.py +3 -2
  16. waldiez/exporting/agent/extras/handoffs/handoff.py +2 -1
  17. waldiez/exporting/agent/extras/handoffs/target.py +7 -4
  18. waldiez/exporting/agent/extras/rag/chroma_extras.py +27 -19
  19. waldiez/exporting/agent/extras/rag/mongo_extras.py +8 -8
  20. waldiez/exporting/agent/extras/rag/pgvector_extras.py +5 -5
  21. waldiez/exporting/agent/extras/rag/qdrant_extras.py +5 -4
  22. waldiez/exporting/agent/extras/rag/vector_db_extras.py +1 -1
  23. waldiez/exporting/agent/extras/rag_user_proxy_agent_extras.py +5 -7
  24. waldiez/exporting/agent/extras/reasoning_agent_extras.py +3 -5
  25. waldiez/exporting/agent/termination.py +1 -0
  26. waldiez/exporting/chats/exporter.py +4 -4
  27. waldiez/exporting/chats/processor.py +1 -2
  28. waldiez/exporting/chats/utils/common.py +89 -48
  29. waldiez/exporting/chats/utils/group.py +9 -9
  30. waldiez/exporting/chats/utils/nested.py +7 -7
  31. waldiez/exporting/chats/utils/sequential.py +1 -1
  32. waldiez/exporting/chats/utils/single.py +2 -2
  33. waldiez/exporting/core/constants.py +3 -1
  34. waldiez/exporting/core/content.py +7 -7
  35. waldiez/exporting/core/context.py +5 -3
  36. waldiez/exporting/core/exporter.py +5 -3
  37. waldiez/exporting/core/exporters.py +2 -2
  38. waldiez/exporting/core/extras/agent_extras/captain_extras.py +2 -2
  39. waldiez/exporting/core/extras/agent_extras/group_manager_extras.py +2 -2
  40. waldiez/exporting/core/extras/agent_extras/rag_user_extras.py +2 -2
  41. waldiez/exporting/core/extras/agent_extras/standard_extras.py +3 -8
  42. waldiez/exporting/core/extras/base.py +7 -5
  43. waldiez/exporting/core/extras/flow_extras.py +4 -5
  44. waldiez/exporting/core/extras/model_extras.py +2 -2
  45. waldiez/exporting/core/extras/path_resolver.py +1 -2
  46. waldiez/exporting/core/extras/serializer.py +13 -11
  47. waldiez/exporting/core/protocols.py +6 -5
  48. waldiez/exporting/core/result.py +25 -28
  49. waldiez/exporting/core/types.py +11 -10
  50. waldiez/exporting/core/utils/llm_config.py +4 -4
  51. waldiez/exporting/core/validation.py +10 -11
  52. waldiez/exporting/flow/execution_generator.py +99 -10
  53. waldiez/exporting/flow/exporter.py +2 -2
  54. waldiez/exporting/flow/factory.py +2 -2
  55. waldiez/exporting/flow/file_generator.py +4 -2
  56. waldiez/exporting/flow/merger.py +5 -3
  57. waldiez/exporting/flow/orchestrator.py +72 -2
  58. waldiez/exporting/flow/utils/common.py +6 -6
  59. waldiez/exporting/flow/utils/importing.py +7 -8
  60. waldiez/exporting/flow/utils/linting.py +25 -9
  61. waldiez/exporting/flow/utils/logging.py +5 -77
  62. waldiez/exporting/models/exporter.py +8 -8
  63. waldiez/exporting/models/processor.py +5 -5
  64. waldiez/exporting/tools/exporter.py +2 -2
  65. waldiez/exporting/tools/processor.py +7 -4
  66. waldiez/io/__init__.py +11 -5
  67. waldiez/io/_ws.py +12 -6
  68. waldiez/io/models/constants.py +10 -10
  69. waldiez/io/models/content/audio.py +1 -0
  70. waldiez/io/models/content/base.py +20 -18
  71. waldiez/io/models/content/file.py +1 -0
  72. waldiez/io/models/content/image.py +1 -0
  73. waldiez/io/models/content/text.py +1 -0
  74. waldiez/io/models/content/video.py +1 -0
  75. waldiez/io/models/user_input.py +10 -5
  76. waldiez/io/models/user_response.py +17 -16
  77. waldiez/io/mqtt.py +18 -31
  78. waldiez/io/redis.py +18 -22
  79. waldiez/io/structured.py +122 -70
  80. waldiez/io/utils.py +19 -10
  81. waldiez/io/ws.py +7 -3
  82. waldiez/logger.py +16 -3
  83. waldiez/models/agents/__init__.py +3 -0
  84. waldiez/models/agents/agent/agent.py +25 -17
  85. waldiez/models/agents/agent/agent_data.py +25 -22
  86. waldiez/models/agents/agent/code_execution.py +9 -11
  87. waldiez/models/agents/agent/termination_message.py +10 -12
  88. waldiez/models/agents/agent/update_system_message.py +2 -4
  89. waldiez/models/agents/agents.py +8 -8
  90. waldiez/models/agents/assistant/assistant.py +6 -3
  91. waldiez/models/agents/assistant/assistant_data.py +2 -2
  92. waldiez/models/agents/captain/captain_agent.py +7 -4
  93. waldiez/models/agents/captain/captain_agent_data.py +5 -7
  94. waldiez/models/agents/doc_agent/doc_agent.py +7 -4
  95. waldiez/models/agents/doc_agent/doc_agent_data.py +9 -10
  96. waldiez/models/agents/doc_agent/rag_query_engine.py +10 -12
  97. waldiez/models/agents/extra_requirements.py +3 -3
  98. waldiez/models/agents/group_manager/group_manager.py +12 -7
  99. waldiez/models/agents/group_manager/group_manager_data.py +13 -12
  100. waldiez/models/agents/group_manager/speakers.py +17 -19
  101. waldiez/models/agents/rag_user_proxy/rag_user_proxy.py +7 -4
  102. waldiez/models/agents/rag_user_proxy/rag_user_proxy_data.py +4 -1
  103. waldiez/models/agents/rag_user_proxy/retrieve_config.py +69 -63
  104. waldiez/models/agents/rag_user_proxy/vector_db_config.py +19 -19
  105. waldiez/models/agents/reasoning/reasoning_agent.py +7 -4
  106. waldiez/models/agents/reasoning/reasoning_agent_data.py +3 -2
  107. waldiez/models/agents/reasoning/reasoning_agent_reason_config.py +8 -8
  108. waldiez/models/agents/user_proxy/user_proxy.py +6 -3
  109. waldiez/models/agents/user_proxy/user_proxy_data.py +1 -1
  110. waldiez/models/chat/chat.py +28 -20
  111. waldiez/models/chat/chat_data.py +22 -21
  112. waldiez/models/chat/chat_message.py +9 -9
  113. waldiez/models/chat/chat_nested.py +9 -9
  114. waldiez/models/chat/chat_summary.py +6 -6
  115. waldiez/models/common/__init__.py +2 -0
  116. waldiez/models/common/ag2_version.py +2 -0
  117. waldiez/models/common/base.py +2 -0
  118. waldiez/models/common/dict_utils.py +8 -6
  119. waldiez/models/common/handoff.py +20 -17
  120. waldiez/models/common/method_utils.py +9 -7
  121. waldiez/models/common/naming.py +49 -0
  122. waldiez/models/flow/flow.py +11 -6
  123. waldiez/models/flow/flow_data.py +23 -17
  124. waldiez/models/flow/info.py +3 -3
  125. waldiez/models/flow/naming.py +2 -1
  126. waldiez/models/model/_aws.py +11 -13
  127. waldiez/models/model/_llm.py +8 -0
  128. waldiez/models/model/_price.py +2 -4
  129. waldiez/models/model/extra_requirements.py +1 -3
  130. waldiez/models/model/model.py +2 -2
  131. waldiez/models/model/model_data.py +21 -21
  132. waldiez/models/tool/extra_requirements.py +2 -4
  133. waldiez/models/tool/predefined/_duckduckgo.py +1 -0
  134. waldiez/models/tool/predefined/_email.py +4 -0
  135. waldiez/models/tool/predefined/_google.py +1 -0
  136. waldiez/models/tool/predefined/_perplexity.py +2 -1
  137. waldiez/models/tool/predefined/_searxng.py +2 -1
  138. waldiez/models/tool/predefined/_tavily.py +1 -0
  139. waldiez/models/tool/predefined/_wikipedia.py +2 -1
  140. waldiez/models/tool/predefined/_youtube.py +1 -0
  141. waldiez/models/tool/tool.py +8 -5
  142. waldiez/models/tool/tool_data.py +2 -2
  143. waldiez/models/waldiez.py +152 -4
  144. waldiez/runner.py +11 -5
  145. waldiez/running/async_utils.py +192 -0
  146. waldiez/running/base_runner.py +155 -241
  147. waldiez/running/dir_utils.py +52 -0
  148. waldiez/running/environment.py +10 -44
  149. waldiez/running/events_mixin.py +252 -0
  150. waldiez/running/exceptions.py +20 -0
  151. waldiez/running/gen_seq_diagram.py +18 -15
  152. waldiez/running/io_utils.py +216 -0
  153. waldiez/running/protocol.py +11 -5
  154. waldiez/running/requirements_mixin.py +65 -0
  155. waldiez/running/results_mixin.py +926 -0
  156. waldiez/running/standard_runner.py +24 -27
  157. waldiez/running/step_by_step/breakpoints_mixin.py +503 -47
  158. waldiez/running/step_by_step/command_handler.py +154 -0
  159. waldiez/running/step_by_step/events_processor.py +379 -0
  160. waldiez/running/step_by_step/step_by_step_models.py +425 -41
  161. waldiez/running/step_by_step/step_by_step_runner.py +437 -382
  162. waldiez/running/subprocess_runner/__base__.py +13 -8
  163. waldiez/running/subprocess_runner/_async_runner.py +6 -4
  164. waldiez/running/subprocess_runner/_sync_runner.py +11 -6
  165. waldiez/running/subprocess_runner/runner.py +48 -23
  166. waldiez/running/timeline_processor.py +1 -1
  167. waldiez/utils/__init__.py +2 -0
  168. waldiez/utils/conflict_checker.py +4 -4
  169. waldiez/utils/python_manager.py +415 -0
  170. waldiez/ws/__init__.py +8 -7
  171. waldiez/ws/_file_handler.py +18 -20
  172. waldiez/ws/_mock.py +75 -0
  173. waldiez/ws/cli.py +58 -10
  174. waldiez/ws/client_manager.py +77 -53
  175. waldiez/ws/errors.py +3 -0
  176. waldiez/ws/models.py +61 -53
  177. waldiez/ws/reloader.py +33 -4
  178. waldiez/ws/server.py +121 -52
  179. waldiez/ws/session_manager.py +8 -9
  180. waldiez/ws/session_stats.py +1 -1
  181. waldiez/ws/utils.py +33 -5
  182. {waldiez-0.5.10.dist-info → waldiez-0.6.1.dist-info}/METADATA +107 -109
  183. waldiez-0.6.1.dist-info/RECORD +254 -0
  184. waldiez/running/post_run.py +0 -180
  185. waldiez/running/pre_run.py +0 -159
  186. waldiez/running/run_results.py +0 -14
  187. waldiez/running/utils.py +0 -511
  188. waldiez-0.5.10.dist-info/RECORD +0 -248
  189. {waldiez-0.5.10.dist-info → waldiez-0.6.1.dist-info}/WHEEL +0 -0
  190. {waldiez-0.5.10.dist-info → waldiez-0.6.1.dist-info}/entry_points.txt +0 -0
  191. {waldiez-0.5.10.dist-info → waldiez-0.6.1.dist-info}/licenses/LICENSE +0 -0
  192. {waldiez-0.5.10.dist-info → waldiez-0.6.1.dist-info}/licenses/NOTICE.md +0 -0
@@ -0,0 +1,415 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+ # pylint: disable=too-many-try-statements,broad-exception-caught
4
+ # pylint: disable=import-error
5
+ """Python manager class."""
6
+
7
+ import asyncio
8
+ import io
9
+ import json
10
+ import os
11
+ import platform
12
+ import re
13
+ import subprocess
14
+ import sys
15
+ from pathlib import Path
16
+ from typing import Any, Callable
17
+
18
+ WALDIEZ_SITE_PACKAGES = "WALDIEZ_SITE_PACKAGES"
19
+ WALDIEZ_APP_ROOT = "WALDIEZ_APP_ROOT"
20
+
21
+
22
+ # noinspection PyBroadException
23
+ class PythonManager:
24
+ """Python manager."""
25
+
26
+ _app_dir: Path | None
27
+
28
+ def __init__(self) -> None:
29
+ self.system = platform.system().lower()
30
+ self.is_frozen = bool(getattr(sys, "frozen", False))
31
+ self._app_dir = None
32
+
33
+ @property
34
+ def app_dir(self) -> Path:
35
+ """Directory to read packaged resources from.
36
+
37
+ Returns
38
+ -------
39
+ Path
40
+ The path to the application installation directory.
41
+ """
42
+ return self._get_app_dir()
43
+
44
+ @property
45
+ def site_packages_directory(self) -> Path | None:
46
+ """Determine the best location to install packages.
47
+
48
+ Returns
49
+ -------
50
+ str | None
51
+ The installation target directory, or None for default.
52
+ """
53
+ return self._get_site_packages_path()
54
+
55
+ def get_python_executable(self) -> str:
56
+ """Get the appropriate Python executable path.
57
+
58
+ Returns
59
+ -------
60
+ str
61
+ The path to the appropriate Python executable path.
62
+ """
63
+ if not self.is_frozen:
64
+ return sys.executable
65
+ # Check for bundled Python in installation directory
66
+ app_dir = self.app_dir
67
+ if self.system == "windows":
68
+ candidates = [
69
+ app_dir / "bundled_python" / "Scripts" / "python.exe",
70
+ app_dir / "bundled_python" / "Scripts" / "python3.exe",
71
+ app_dir / "bundled_python" / "python.exe",
72
+ ]
73
+ else:
74
+ candidates = [
75
+ app_dir / "bundled_python" / "bin" / "python3",
76
+ app_dir / "bundled_python" / "bin" / "python",
77
+ app_dir / "bundled_python" / "python3",
78
+ ]
79
+
80
+ for candidate in candidates:
81
+ if candidate.exists():
82
+ return str(candidate)
83
+ # Fallback to system Python
84
+ return sys.executable
85
+
86
+ def list_installed_packages(self) -> list[dict[str, str]]:
87
+ """List packages in our managed environment.
88
+
89
+ Returns
90
+ -------
91
+ list[dict[str,str]]
92
+ The locally installed packages.
93
+ """
94
+ python_exe = self.get_python_executable()
95
+
96
+ env = os.environ.copy()
97
+ if self.site_packages_directory:
98
+ env["PYTHONPATH"] = str(self.site_packages_directory)
99
+
100
+ cmd = [python_exe, "-m", "pip", "list", "--format=json"]
101
+ try:
102
+ result = subprocess.run(
103
+ cmd,
104
+ capture_output=True,
105
+ text=True,
106
+ env=env,
107
+ check=True,
108
+ )
109
+ if result.returncode == 0:
110
+ return json.loads(result.stdout)
111
+ except Exception:
112
+ pass
113
+
114
+ return []
115
+
116
+ def get_debug_info(self) -> dict[str, Any]:
117
+ """Get comprehensive debug information.
118
+
119
+ Returns
120
+ -------
121
+ dict[str, Any]
122
+ Debug info about the paths and the packages.
123
+ """
124
+ return {
125
+ "system": self.system,
126
+ "is_frozen": self.is_frozen,
127
+ "site_packages_directory": (
128
+ str(self.site_packages_directory)
129
+ if self.site_packages_directory
130
+ else None
131
+ ),
132
+ "python_executable": self.get_python_executable(),
133
+ "python_version": sys.version,
134
+ "in_virtualenv": self.in_virtualenv(),
135
+ "sys_path": sys.path,
136
+ "pythonpath": os.environ.get("PYTHONPATH", ""),
137
+ }
138
+
139
+ def pip_install(
140
+ self,
141
+ packages: set[str],
142
+ upgrade: bool = False,
143
+ printer: Callable[..., None] = print,
144
+ ) -> None:
145
+ """Install packages.
146
+
147
+ Parameters
148
+ ----------
149
+ packages : set[str]
150
+ The packages to install.
151
+ upgrade : bool
152
+ Upgrade existing or not.
153
+ printer : Callable[..., None]
154
+ The callable to use for printing the process' output
155
+ """
156
+ pip_install_cmd, break_system_packages = self._before_pip(
157
+ packages, upgrade
158
+ )
159
+ try:
160
+ with subprocess.Popen(
161
+ pip_install_cmd,
162
+ stdout=subprocess.PIPE,
163
+ stderr=subprocess.PIPE,
164
+ ) as proc:
165
+ if proc.stdout: # pragma: no branch
166
+ for line in io.TextIOWrapper(proc.stdout, encoding="utf-8"):
167
+ stripped_line = strip_ansi(line.strip())
168
+ if stripped_line: # Only print non-empty lines
169
+ printer(stripped_line)
170
+ if proc.stderr: # pragma: no branch
171
+ for line in io.TextIOWrapper(proc.stderr, encoding="utf-8"):
172
+ stripped_line = strip_ansi(line.strip())
173
+ if stripped_line: # Only print non-empty lines
174
+ printer(stripped_line)
175
+
176
+ # Wait for process to complete and check return code
177
+ return_code = proc.wait()
178
+ if return_code != 0: # pragma: no cover
179
+ msg = (
180
+ "Package installation failed "
181
+ f"with exit code {return_code}"
182
+ )
183
+ printer(msg)
184
+
185
+ except Exception as e: # pragma: no cover
186
+ printer(f"Failed to install requirements: {e}")
187
+ finally:
188
+ self._after_pip(break_system_packages)
189
+
190
+ async def a_pip_install(
191
+ self,
192
+ packages: set[str],
193
+ upgrade: bool = False,
194
+ printer: Callable[..., None] = print,
195
+ ) -> None:
196
+ """Install packages asynchronously.
197
+
198
+ Parameters
199
+ ----------
200
+ packages : set[str]
201
+ The packages to install.
202
+ upgrade : bool
203
+ Upgrade existing or not.
204
+ printer : Callable[..., None]
205
+ The callable to use for printing the process' output
206
+ """
207
+ pip_install, break_system_packages = self._before_pip(
208
+ packages, upgrade=upgrade
209
+ )
210
+ requirements_string = ", ".join(packages)
211
+ printer(f"Installing requirements: {requirements_string}")
212
+ try:
213
+ proc = await asyncio.create_subprocess_exec(
214
+ *pip_install,
215
+ stdout=asyncio.subprocess.PIPE,
216
+ stderr=asyncio.subprocess.PIPE,
217
+ )
218
+
219
+ async def _pump_stream(stream: asyncio.StreamReader | None) -> None:
220
+ if not stream: # pragma: no cover
221
+ return
222
+ async for raw in stream:
223
+ text = strip_ansi(raw.decode(errors="replace").rstrip())
224
+ if text:
225
+ printer(text)
226
+
227
+ # Create tasks for concurrent execution
228
+ tasks: list[asyncio.Task[int | None]] = [
229
+ asyncio.create_task(_pump_stream(proc.stdout)),
230
+ asyncio.create_task(_pump_stream(proc.stderr)),
231
+ asyncio.create_task(proc.wait()),
232
+ ]
233
+ await asyncio.gather(*tasks, return_exceptions=True)
234
+ if proc.returncode != 0: # pragma: no cover
235
+ msg = (
236
+ "Package installation failed "
237
+ f"with exit code {proc.returncode}"
238
+ )
239
+ printer(msg)
240
+
241
+ except Exception as e:
242
+ printer(f"Failed to install requirements: {e}")
243
+ finally:
244
+ self._after_pip(break_system_packages)
245
+
246
+ # noinspection TryExceptPass
247
+ @staticmethod
248
+ def _ensure_pip() -> None: # pragma: no cover
249
+ """Make sure `python -m pip` works (bootstrap if needed)."""
250
+ # pylint: disable=import-outside-toplevel,unused-import
251
+ try:
252
+ import pip # noqa: F401 # pyright: ignore[reportUnusedImport]
253
+
254
+ return
255
+ except Exception:
256
+ pass
257
+ try:
258
+ import ensurepip
259
+
260
+ ensurepip.bootstrap(upgrade=True)
261
+ except Exception:
262
+ # If bootstrap fails,
263
+ # we'll still attempt `-m pip` and surface errors.
264
+ pass
265
+
266
+ def _get_app_dir(self) -> Path:
267
+ if self._app_dir is None:
268
+ from_env_str = os.environ.get(WALDIEZ_APP_ROOT, "")
269
+ if from_env_str:
270
+ from_env_path = Path(from_env_str).resolve()
271
+ if from_env_path.is_dir():
272
+ self._app_dir = from_env_path
273
+ return self._app_dir
274
+ if self.is_frozen and hasattr(sys, "_MEIPASS"): # PyInstaller
275
+ self._app_dir = Path(
276
+ getattr(sys, "_MEIPASS", Path(sys.executable).parent)
277
+ )
278
+ return self._app_dir
279
+ # dev: package root
280
+ self._app_dir = Path(__file__).parent.parent
281
+ return self._app_dir
282
+
283
+ def _get_site_packages_path(self) -> Path | None:
284
+ from_env_str = os.environ.get(WALDIEZ_SITE_PACKAGES, "")
285
+ if from_env_str:
286
+ from_env_path = Path(from_env_str).resolve()
287
+ if from_env_path.is_dir():
288
+ return from_env_path
289
+ if self.is_frozen:
290
+ # Use the bundled site-packages if available
291
+ bundled_sp = self.app_dir / "bundled_python" / "site-packages"
292
+ if bundled_sp.exists():
293
+ return bundled_sp
294
+ return None
295
+
296
+ def _before_pip(
297
+ self,
298
+ packages: set[str],
299
+ upgrade: bool,
300
+ ) -> tuple[list[str], str]:
301
+ """Gather the pip command for installing requirements.
302
+
303
+ Parameters
304
+ ----------
305
+ packages : set[str]
306
+ The packages to install.
307
+ upgrade : bool
308
+ Whether to upgrade the packages.
309
+
310
+ Returns
311
+ -------
312
+ tuple[list[str], str]
313
+ The pip command, and the break_system_packages flag.
314
+ """
315
+ self._ensure_pip()
316
+ pip_install = [
317
+ self.get_python_executable(),
318
+ "-m",
319
+ "pip",
320
+ "install",
321
+ "--disable-pip-version-check",
322
+ "--no-input",
323
+ ]
324
+ install_location = self.site_packages_directory
325
+ break_system_packages = ""
326
+
327
+ if install_location:
328
+ pip_install += ["--target", str(install_location)]
329
+ elif not self.in_virtualenv(): # pragma: no cover
330
+ # it should, if not, let's try to install as user
331
+ if not is_root():
332
+ pip_install.append("--user")
333
+ break_system_packages = os.environ.get(
334
+ "PIP_BREAK_SYSTEM_PACKAGES", ""
335
+ )
336
+ os.environ["PIP_BREAK_SYSTEM_PACKAGES"] = "1"
337
+
338
+ if upgrade: # pragma: no cover
339
+ pip_install.append("--upgrade")
340
+
341
+ pip_install.extend(sorted(packages))
342
+ return pip_install, break_system_packages
343
+
344
+ def _after_pip(
345
+ self,
346
+ break_system_packages: str,
347
+ ) -> None:
348
+ """Restore environment variables after pip installation.
349
+
350
+ Parameters
351
+ ----------
352
+ break_system_packages : str
353
+ The original value of PIP_BREAK_SYSTEM_PACKAGES.
354
+ """
355
+ if (
356
+ not self.site_packages_directory and not self.in_virtualenv()
357
+ ): # pragma: no cover
358
+ # restore the old env var
359
+ if break_system_packages:
360
+ os.environ["PIP_BREAK_SYSTEM_PACKAGES"] = break_system_packages
361
+ else:
362
+ # Use pop to avoid KeyError if the key doesn't exist
363
+ os.environ.pop("PIP_BREAK_SYSTEM_PACKAGES", None)
364
+
365
+ @staticmethod
366
+ def in_virtualenv() -> bool:
367
+ """Check if we are inside a virtualenv.
368
+
369
+ Returns
370
+ -------
371
+ bool
372
+ True if inside a virtualenv, False otherwise.
373
+ """
374
+ return hasattr(sys, "real_prefix") or (
375
+ hasattr(sys, "base_prefix")
376
+ and os.path.realpath(sys.base_prefix)
377
+ != os.path.realpath(sys.prefix)
378
+ )
379
+
380
+
381
+ def is_root() -> bool: # pragma: no cover # os specific
382
+ """Check if the script is running as root/administrator.
383
+
384
+ Returns
385
+ -------
386
+ bool
387
+ True if running as root/administrator, False otherwise.
388
+ """
389
+ # pylint: disable=import-outside-toplevel,line-too-long,no-member
390
+ if os.name == "nt":
391
+ try:
392
+ import ctypes
393
+
394
+ return ctypes.windll.shell32.IsUserAnAdmin() != 0 # type: ignore[unused-ignore,attr-defined] # noqa: E501
395
+ except Exception:
396
+ return False
397
+ else:
398
+ return os.getuid() == 0
399
+
400
+
401
+ def strip_ansi(text: str) -> str:
402
+ """Remove ANSI escape sequences from text.
403
+
404
+ Parameters
405
+ ----------
406
+ text : str
407
+ The text to strip.
408
+
409
+ Returns
410
+ -------
411
+ str
412
+ The text without ANSI escape sequences.
413
+ """
414
+ ansi_pattern = re.compile(r"\x1b\[[0-9;]*m|\x1b\[.*?[@-~]")
415
+ return ansi_pattern.sub("", text)
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
@@ -25,7 +23,7 @@ class FileRequestHandler:
25
23
  """Handles file-related requests."""
26
24
 
27
25
  @staticmethod
28
- def handle_save_flow_request(
26
+ def handle_save_request(
29
27
  msg: SaveFlowRequest,
30
28
  workspace_dir: Path,
31
29
  client_id: str,
@@ -49,10 +47,10 @@ class FileRequestHandler:
49
47
  dict[str, Any]
50
48
  The response dictionary.
51
49
  """
52
- filename = msg.filename or f"waldiez_{client_id}.waldiez"
50
+ path = msg.path or f"waldiez_{client_id}.waldiez"
53
51
  try:
54
52
  output_path = resolve_output_path(
55
- filename,
53
+ path,
56
54
  workspace_dir=workspace_dir,
57
55
  expected_ext="waldiez",
58
56
  )
@@ -60,28 +58,28 @@ class FileRequestHandler:
60
58
  logger.error("Error resolving output path: %s", exc)
61
59
  return SaveFlowResponse.fail(
62
60
  error=f"Invalid output path: {exc}",
63
- file_path=filename,
61
+ path=path,
64
62
  ).model_dump(mode="json")
65
63
  # pylint: disable=too-many-try-statements
66
64
  try:
67
- if output_path.exists() and not msg.force_overwrite:
65
+ if output_path.exists() and not msg.force:
68
66
  return SaveFlowResponse.fail(
69
67
  error=f"File exists: {output_path}",
70
68
  file_path=str(output_path.relative_to(workspace_dir)),
71
69
  ).model_dump(mode="json")
72
70
 
73
71
  # Parent dir already created by resolve_output_path
74
- output_path.write_text(msg.flow_data, encoding="utf-8")
72
+ output_path.write_text(msg.data, encoding="utf-8", newline="\n")
75
73
 
76
74
  return SaveFlowResponse.ok(
77
- file_path=str(output_path.relative_to(workspace_dir))
75
+ path=str(output_path.relative_to(workspace_dir))
78
76
  ).model_dump(mode="json")
79
77
  except Exception as e: # pylint: disable=broad-exception-caught
80
78
  logger.error("Error saving flow: %s", e)
81
79
  return SaveFlowResponse.fail(error=str(e)).model_dump(mode="json")
82
80
 
83
81
  @staticmethod
84
- def handle_convert_workflow_request(
82
+ def handle_convert_request(
85
83
  msg: ConvertWorkflowRequest,
86
84
  client_id: str,
87
85
  workspace_dir: Path,
@@ -105,26 +103,26 @@ class FileRequestHandler:
105
103
  dict[str, Any]
106
104
  The response dictionary.
107
105
  """
108
- target_format = (msg.target_format or "").strip().lower()
106
+ target_format = (msg.format or "").strip().lower()
109
107
  if target_format not in {"py", "ipynb"}:
110
108
  return ConvertWorkflowResponse.fail(
111
109
  error=f"Unsupported target format: {target_format}",
112
- target_format=target_format,
110
+ format=target_format,
113
111
  ).model_dump(mode="json")
114
112
 
115
113
  try:
116
- waldiez_data = Waldiez.from_dict(json.loads(msg.flow_data))
114
+ waldiez_data = Waldiez.from_dict(json.loads(msg.data))
117
115
  except Exception as e: # pylint: disable=broad-exception-caught
118
116
  return ConvertWorkflowResponse.fail(
119
117
  error=f"Invalid flow_data: {e}",
120
- target_format=target_format,
118
+ format=target_format,
121
119
  ).model_dump(mode="json")
122
120
 
123
121
  try:
124
122
  # Use normalized target_format for default name
125
- filename = msg.output_path or f"waldiez_{client_id}.{target_format}"
123
+ path = msg.path or f"waldiez_{client_id}.{target_format}"
126
124
  output_path = resolve_output_path(
127
- filename,
125
+ path,
128
126
  workspace_dir=workspace_dir,
129
127
  expected_ext=target_format,
130
128
  )
@@ -132,7 +130,7 @@ class FileRequestHandler:
132
130
  logger.error("Error resolving output path: %s", exc)
133
131
  return ConvertWorkflowResponse.fail(
134
132
  error=f"Invalid output path: {exc}",
135
- target_format=target_format,
133
+ format=target_format,
136
134
  ).model_dump(mode="json")
137
135
 
138
136
  try:
@@ -140,13 +138,13 @@ class FileRequestHandler:
140
138
  exporter.export(path=output_path, force=True, structured_io=True)
141
139
 
142
140
  return ConvertWorkflowResponse.ok(
143
- target_format=target_format,
144
- output_path=str(output_path.relative_to(workspace_dir)),
141
+ format=target_format,
142
+ path=str(output_path.relative_to(workspace_dir)),
145
143
  ).model_dump(mode="json")
146
144
  except Exception as e: # pylint: disable=broad-exception-caught
147
145
  logger.error("Error converting workflow: %s", e)
148
146
  return ConvertWorkflowResponse.fail(
149
- error=str(e), target_format=target_format
147
+ error=str(e), format=target_format
150
148
  ).model_dump(mode="json")
151
149
 
152
150
 
waldiez/ws/_mock.py ADDED
@@ -0,0 +1,75 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+
4
+ # pylint: disable=invalid-name,line-too-long,unused-argument,too-few-public-methods,no-self-use
5
+ # pylint: disable=missing-class-docstring,missing-function-docstring,missing-return-doc
6
+ # flake8: noqa: E501, D101, D102, D106
7
+ # pyright: reportUnusedParameter=false, reportUninitializedInstanceVariable=false
8
+ """Mock websockets for linters."""
9
+
10
+ from typing import Any # pragma: no cover
11
+
12
+
13
+ # noinspection PyPep8Naming, PyMethodMayBeStatic,PyUnusedLocal
14
+ class websockets: # pragma: no cover
15
+ # noinspection PyMethodMayBeStatic
16
+ class ClientConnection:
17
+ async def __aenter__(self) -> "websockets.ClientConnection":
18
+ return self
19
+
20
+ async def send(self, data: Any) -> None:
21
+ pass
22
+
23
+ async def close(self) -> None:
24
+ pass
25
+
26
+ async def recv(self) -> str:
27
+ return ""
28
+
29
+ # noinspection PyPep8Naming
30
+ class exceptions:
31
+ class ConnectionClosedError(Exception): ...
32
+
33
+ class ConnectionClosedOK(Exception): ...
34
+
35
+ class Server:
36
+ def close(self) -> None:
37
+ pass
38
+
39
+ async def wait_closed(self) -> None:
40
+ pass
41
+
42
+ class ServerConnection:
43
+ remote_address: str
44
+
45
+ # noinspection PyPep8Naming
46
+ class request:
47
+ headers: dict[str, str]
48
+
49
+ async def close(
50
+ self, code: int = 1000, reason: str = "Normal Closure"
51
+ ) -> None:
52
+ pass
53
+
54
+ # noinspection PyPep8Naming
55
+ async def ConnectionClosedOK(self, *args: Any, **kwargs: Any) -> None:
56
+ pass
57
+
58
+ async def send(self, data: Any) -> None:
59
+ pass
60
+
61
+ @staticmethod
62
+ async def serve(*args: Any, **kwargs: Any) -> "websockets.Server":
63
+ return websockets.Server()
64
+
65
+ @staticmethod
66
+ async def connect(
67
+ *args: Any, **kwargs: Any
68
+ ) -> "websockets.ClientConnection":
69
+ return websockets.ClientConnection()
70
+
71
+ class ConnectionClosed(Exception):
72
+ pass
73
+
74
+ class WebSocketException(Exception):
75
+ pass