waldiez 0.6.0__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 (188) hide show
  1. waldiez/__init__.py +1 -1
  2. waldiez/_version.py +1 -1
  3. waldiez/cli.py +18 -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 +9 -10
  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 +34 -23
  12. waldiez/exporting/agent/extras/group_member_extras.py +6 -5
  13. waldiez/exporting/agent/extras/handoffs/after_work.py +1 -1
  14. waldiez/exporting/agent/extras/handoffs/available.py +1 -1
  15. waldiez/exporting/agent/extras/handoffs/condition.py +3 -2
  16. waldiez/exporting/agent/extras/handoffs/handoff.py +1 -1
  17. waldiez/exporting/agent/extras/handoffs/target.py +6 -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/chats/exporter.py +4 -4
  26. waldiez/exporting/chats/processor.py +1 -2
  27. waldiez/exporting/chats/utils/common.py +89 -48
  28. waldiez/exporting/chats/utils/group.py +9 -9
  29. waldiez/exporting/chats/utils/nested.py +7 -7
  30. waldiez/exporting/chats/utils/sequential.py +1 -1
  31. waldiez/exporting/chats/utils/single.py +2 -2
  32. waldiez/exporting/core/content.py +7 -7
  33. waldiez/exporting/core/context.py +5 -3
  34. waldiez/exporting/core/exporter.py +5 -3
  35. waldiez/exporting/core/exporters.py +2 -2
  36. waldiez/exporting/core/extras/agent_extras/captain_extras.py +2 -2
  37. waldiez/exporting/core/extras/agent_extras/group_manager_extras.py +2 -2
  38. waldiez/exporting/core/extras/agent_extras/rag_user_extras.py +2 -2
  39. waldiez/exporting/core/extras/agent_extras/standard_extras.py +3 -8
  40. waldiez/exporting/core/extras/base.py +7 -5
  41. waldiez/exporting/core/extras/flow_extras.py +4 -5
  42. waldiez/exporting/core/extras/model_extras.py +2 -2
  43. waldiez/exporting/core/extras/path_resolver.py +1 -2
  44. waldiez/exporting/core/extras/serializer.py +2 -2
  45. waldiez/exporting/core/protocols.py +6 -5
  46. waldiez/exporting/core/result.py +25 -28
  47. waldiez/exporting/core/types.py +10 -10
  48. waldiez/exporting/core/utils/llm_config.py +2 -2
  49. waldiez/exporting/core/validation.py +10 -11
  50. waldiez/exporting/flow/execution_generator.py +98 -10
  51. waldiez/exporting/flow/exporter.py +2 -2
  52. waldiez/exporting/flow/factory.py +2 -2
  53. waldiez/exporting/flow/file_generator.py +4 -2
  54. waldiez/exporting/flow/merger.py +5 -3
  55. waldiez/exporting/flow/orchestrator.py +72 -2
  56. waldiez/exporting/flow/utils/common.py +5 -5
  57. waldiez/exporting/flow/utils/importing.py +6 -7
  58. waldiez/exporting/flow/utils/linting.py +25 -9
  59. waldiez/exporting/flow/utils/logging.py +2 -2
  60. waldiez/exporting/models/exporter.py +8 -8
  61. waldiez/exporting/models/processor.py +5 -5
  62. waldiez/exporting/tools/exporter.py +2 -2
  63. waldiez/exporting/tools/processor.py +7 -4
  64. waldiez/io/__init__.py +8 -4
  65. waldiez/io/_ws.py +10 -6
  66. waldiez/io/models/constants.py +10 -10
  67. waldiez/io/models/content/audio.py +1 -0
  68. waldiez/io/models/content/base.py +20 -18
  69. waldiez/io/models/content/file.py +1 -0
  70. waldiez/io/models/content/image.py +1 -0
  71. waldiez/io/models/content/text.py +1 -0
  72. waldiez/io/models/content/video.py +1 -0
  73. waldiez/io/models/user_input.py +10 -5
  74. waldiez/io/models/user_response.py +17 -16
  75. waldiez/io/mqtt.py +18 -31
  76. waldiez/io/redis.py +18 -22
  77. waldiez/io/structured.py +52 -53
  78. waldiez/io/utils.py +3 -0
  79. waldiez/io/ws.py +5 -1
  80. waldiez/logger.py +16 -3
  81. waldiez/models/agents/__init__.py +3 -0
  82. waldiez/models/agents/agent/agent.py +23 -16
  83. waldiez/models/agents/agent/agent_data.py +25 -22
  84. waldiez/models/agents/agent/code_execution.py +9 -11
  85. waldiez/models/agents/agent/termination_message.py +10 -12
  86. waldiez/models/agents/agent/update_system_message.py +2 -4
  87. waldiez/models/agents/agents.py +8 -8
  88. waldiez/models/agents/assistant/assistant.py +6 -3
  89. waldiez/models/agents/assistant/assistant_data.py +2 -2
  90. waldiez/models/agents/captain/captain_agent.py +7 -4
  91. waldiez/models/agents/captain/captain_agent_data.py +5 -7
  92. waldiez/models/agents/doc_agent/doc_agent.py +7 -4
  93. waldiez/models/agents/doc_agent/doc_agent_data.py +9 -10
  94. waldiez/models/agents/doc_agent/rag_query_engine.py +10 -12
  95. waldiez/models/agents/extra_requirements.py +3 -3
  96. waldiez/models/agents/group_manager/group_manager.py +12 -7
  97. waldiez/models/agents/group_manager/group_manager_data.py +13 -12
  98. waldiez/models/agents/group_manager/speakers.py +17 -19
  99. waldiez/models/agents/rag_user_proxy/rag_user_proxy.py +7 -4
  100. waldiez/models/agents/rag_user_proxy/rag_user_proxy_data.py +4 -1
  101. waldiez/models/agents/rag_user_proxy/retrieve_config.py +69 -63
  102. waldiez/models/agents/rag_user_proxy/vector_db_config.py +19 -19
  103. waldiez/models/agents/reasoning/reasoning_agent.py +7 -4
  104. waldiez/models/agents/reasoning/reasoning_agent_data.py +3 -2
  105. waldiez/models/agents/reasoning/reasoning_agent_reason_config.py +8 -8
  106. waldiez/models/agents/user_proxy/user_proxy.py +6 -3
  107. waldiez/models/agents/user_proxy/user_proxy_data.py +1 -1
  108. waldiez/models/chat/chat.py +27 -20
  109. waldiez/models/chat/chat_data.py +22 -19
  110. waldiez/models/chat/chat_message.py +9 -9
  111. waldiez/models/chat/chat_nested.py +9 -9
  112. waldiez/models/chat/chat_summary.py +6 -6
  113. waldiez/models/common/__init__.py +2 -0
  114. waldiez/models/common/ag2_version.py +2 -0
  115. waldiez/models/common/dict_utils.py +8 -6
  116. waldiez/models/common/handoff.py +18 -17
  117. waldiez/models/common/method_utils.py +7 -7
  118. waldiez/models/common/naming.py +49 -0
  119. waldiez/models/flow/flow.py +11 -6
  120. waldiez/models/flow/flow_data.py +23 -17
  121. waldiez/models/flow/info.py +3 -3
  122. waldiez/models/flow/naming.py +2 -1
  123. waldiez/models/model/_aws.py +11 -13
  124. waldiez/models/model/_llm.py +5 -0
  125. waldiez/models/model/_price.py +2 -4
  126. waldiez/models/model/extra_requirements.py +1 -3
  127. waldiez/models/model/model.py +2 -2
  128. waldiez/models/model/model_data.py +21 -21
  129. waldiez/models/tool/extra_requirements.py +2 -4
  130. waldiez/models/tool/predefined/_duckduckgo.py +1 -0
  131. waldiez/models/tool/predefined/_email.py +1 -0
  132. waldiez/models/tool/predefined/_google.py +1 -0
  133. waldiez/models/tool/predefined/_perplexity.py +1 -0
  134. waldiez/models/tool/predefined/_searxng.py +1 -0
  135. waldiez/models/tool/predefined/_tavily.py +1 -0
  136. waldiez/models/tool/predefined/_wikipedia.py +1 -0
  137. waldiez/models/tool/predefined/_youtube.py +1 -0
  138. waldiez/models/tool/tool.py +8 -5
  139. waldiez/models/tool/tool_data.py +2 -2
  140. waldiez/models/waldiez.py +152 -4
  141. waldiez/runner.py +11 -5
  142. waldiez/running/async_utils.py +192 -0
  143. waldiez/running/base_runner.py +117 -264
  144. waldiez/running/dir_utils.py +52 -0
  145. waldiez/running/environment.py +10 -44
  146. waldiez/running/events_mixin.py +252 -0
  147. waldiez/running/exceptions.py +20 -0
  148. waldiez/running/gen_seq_diagram.py +18 -15
  149. waldiez/running/io_utils.py +216 -0
  150. waldiez/running/protocol.py +11 -5
  151. waldiez/running/requirements_mixin.py +65 -0
  152. waldiez/running/results_mixin.py +926 -0
  153. waldiez/running/standard_runner.py +22 -25
  154. waldiez/running/step_by_step/breakpoints_mixin.py +192 -60
  155. waldiez/running/step_by_step/command_handler.py +3 -0
  156. waldiez/running/step_by_step/events_processor.py +194 -14
  157. waldiez/running/step_by_step/step_by_step_models.py +110 -43
  158. waldiez/running/step_by_step/step_by_step_runner.py +107 -57
  159. waldiez/running/subprocess_runner/__base__.py +9 -1
  160. waldiez/running/subprocess_runner/_async_runner.py +5 -3
  161. waldiez/running/subprocess_runner/_sync_runner.py +6 -2
  162. waldiez/running/subprocess_runner/runner.py +39 -23
  163. waldiez/running/timeline_processor.py +1 -1
  164. waldiez/utils/__init__.py +2 -0
  165. waldiez/utils/conflict_checker.py +4 -4
  166. waldiez/utils/python_manager.py +415 -0
  167. waldiez/ws/_file_handler.py +18 -18
  168. waldiez/ws/_mock.py +2 -1
  169. waldiez/ws/cli.py +36 -12
  170. waldiez/ws/client_manager.py +35 -27
  171. waldiez/ws/errors.py +3 -0
  172. waldiez/ws/models.py +43 -52
  173. waldiez/ws/reloader.py +12 -4
  174. waldiez/ws/server.py +85 -55
  175. waldiez/ws/session_manager.py +8 -9
  176. waldiez/ws/session_stats.py +1 -1
  177. waldiez/ws/utils.py +4 -1
  178. {waldiez-0.6.0.dist-info → waldiez-0.6.1.dist-info}/METADATA +82 -93
  179. waldiez-0.6.1.dist-info/RECORD +254 -0
  180. waldiez/running/post_run.py +0 -186
  181. waldiez/running/pre_run.py +0 -281
  182. waldiez/running/run_results.py +0 -14
  183. waldiez/running/utils.py +0 -625
  184. waldiez-0.6.0.dist-info/RECORD +0 -251
  185. {waldiez-0.6.0.dist-info → waldiez-0.6.1.dist-info}/WHEEL +0 -0
  186. {waldiez-0.6.0.dist-info → waldiez-0.6.1.dist-info}/entry_points.txt +0 -0
  187. {waldiez-0.6.0.dist-info → waldiez-0.6.1.dist-info}/licenses/LICENSE +0 -0
  188. {waldiez-0.6.0.dist-info → waldiez-0.6.1.dist-info}/licenses/NOTICE.md +0 -0
waldiez/utils/__init__.py CHANGED
@@ -3,9 +3,11 @@
3
3
  """Utils to call on init."""
4
4
 
5
5
  from .conflict_checker import check_conflicts
6
+ from .python_manager import PythonManager
6
7
  from .version import get_waldiez_version
7
8
 
8
9
  __all__ = [
9
10
  "check_conflicts",
10
11
  "get_waldiez_version",
12
+ "PythonManager",
11
13
  ]
@@ -15,16 +15,16 @@ __waldiez_checked_conflicts = False
15
15
  def _check_autogen_agentchat() -> None: # pragma: no cover
16
16
  try:
17
17
  version("autogen-agentchat")
18
- print(
18
+ msg = (
19
19
  "Conflict detected: 'autogen-agentchat' is installed "
20
20
  "in the current environment, \n"
21
21
  "which conflicts with 'ag2'.\n"
22
22
  "Please uninstall 'autogen-agentchat': \n"
23
- f"{sys.executable} -m pip uninstall -y autogen-agentchat" + "\n"
23
+ f"{sys.executable} -m pip uninstall -y autogen-agentchat\n"
24
24
  "And install 'ag2' (and/or 'waldiez') again: \n"
25
- f"{sys.executable} -m pip install --force ag2 waldiez",
26
- file=sys.stderr,
25
+ f"{sys.executable} -m pip install --force ag2 waldiez"
27
26
  )
27
+ print(msg, file=sys.stderr)
28
28
  sys.exit(1)
29
29
  except PackageNotFoundError:
30
30
  pass
@@ -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)
@@ -23,7 +23,7 @@ class FileRequestHandler:
23
23
  """Handles file-related requests."""
24
24
 
25
25
  @staticmethod
26
- def handle_save_flow_request(
26
+ def handle_save_request(
27
27
  msg: SaveFlowRequest,
28
28
  workspace_dir: Path,
29
29
  client_id: str,
@@ -47,10 +47,10 @@ class FileRequestHandler:
47
47
  dict[str, Any]
48
48
  The response dictionary.
49
49
  """
50
- filename = msg.filename or f"waldiez_{client_id}.waldiez"
50
+ path = msg.path or f"waldiez_{client_id}.waldiez"
51
51
  try:
52
52
  output_path = resolve_output_path(
53
- filename,
53
+ path,
54
54
  workspace_dir=workspace_dir,
55
55
  expected_ext="waldiez",
56
56
  )
@@ -58,28 +58,28 @@ class FileRequestHandler:
58
58
  logger.error("Error resolving output path: %s", exc)
59
59
  return SaveFlowResponse.fail(
60
60
  error=f"Invalid output path: {exc}",
61
- file_path=filename,
61
+ path=path,
62
62
  ).model_dump(mode="json")
63
63
  # pylint: disable=too-many-try-statements
64
64
  try:
65
- if output_path.exists() and not msg.force_overwrite:
65
+ if output_path.exists() and not msg.force:
66
66
  return SaveFlowResponse.fail(
67
67
  error=f"File exists: {output_path}",
68
68
  file_path=str(output_path.relative_to(workspace_dir)),
69
69
  ).model_dump(mode="json")
70
70
 
71
71
  # Parent dir already created by resolve_output_path
72
- output_path.write_text(msg.flow_data, encoding="utf-8")
72
+ output_path.write_text(msg.data, encoding="utf-8", newline="\n")
73
73
 
74
74
  return SaveFlowResponse.ok(
75
- file_path=str(output_path.relative_to(workspace_dir))
75
+ path=str(output_path.relative_to(workspace_dir))
76
76
  ).model_dump(mode="json")
77
77
  except Exception as e: # pylint: disable=broad-exception-caught
78
78
  logger.error("Error saving flow: %s", e)
79
79
  return SaveFlowResponse.fail(error=str(e)).model_dump(mode="json")
80
80
 
81
81
  @staticmethod
82
- def handle_convert_workflow_request(
82
+ def handle_convert_request(
83
83
  msg: ConvertWorkflowRequest,
84
84
  client_id: str,
85
85
  workspace_dir: Path,
@@ -103,26 +103,26 @@ class FileRequestHandler:
103
103
  dict[str, Any]
104
104
  The response dictionary.
105
105
  """
106
- target_format = (msg.target_format or "").strip().lower()
106
+ target_format = (msg.format or "").strip().lower()
107
107
  if target_format not in {"py", "ipynb"}:
108
108
  return ConvertWorkflowResponse.fail(
109
109
  error=f"Unsupported target format: {target_format}",
110
- target_format=target_format,
110
+ format=target_format,
111
111
  ).model_dump(mode="json")
112
112
 
113
113
  try:
114
- waldiez_data = Waldiez.from_dict(json.loads(msg.flow_data))
114
+ waldiez_data = Waldiez.from_dict(json.loads(msg.data))
115
115
  except Exception as e: # pylint: disable=broad-exception-caught
116
116
  return ConvertWorkflowResponse.fail(
117
117
  error=f"Invalid flow_data: {e}",
118
- target_format=target_format,
118
+ format=target_format,
119
119
  ).model_dump(mode="json")
120
120
 
121
121
  try:
122
122
  # Use normalized target_format for default name
123
- filename = msg.output_path or f"waldiez_{client_id}.{target_format}"
123
+ path = msg.path or f"waldiez_{client_id}.{target_format}"
124
124
  output_path = resolve_output_path(
125
- filename,
125
+ path,
126
126
  workspace_dir=workspace_dir,
127
127
  expected_ext=target_format,
128
128
  )
@@ -130,7 +130,7 @@ class FileRequestHandler:
130
130
  logger.error("Error resolving output path: %s", exc)
131
131
  return ConvertWorkflowResponse.fail(
132
132
  error=f"Invalid output path: {exc}",
133
- target_format=target_format,
133
+ format=target_format,
134
134
  ).model_dump(mode="json")
135
135
 
136
136
  try:
@@ -138,13 +138,13 @@ class FileRequestHandler:
138
138
  exporter.export(path=output_path, force=True, structured_io=True)
139
139
 
140
140
  return ConvertWorkflowResponse.ok(
141
- target_format=target_format,
142
- output_path=str(output_path.relative_to(workspace_dir)),
141
+ format=target_format,
142
+ path=str(output_path.relative_to(workspace_dir)),
143
143
  ).model_dump(mode="json")
144
144
  except Exception as e: # pylint: disable=broad-exception-caught
145
145
  logger.error("Error converting workflow: %s", e)
146
146
  return ConvertWorkflowResponse.fail(
147
- error=str(e), target_format=target_format
147
+ error=str(e), format=target_format
148
148
  ).model_dump(mode="json")
149
149
 
150
150
 
waldiez/ws/_mock.py CHANGED
@@ -1,10 +1,11 @@
1
1
  # SPDX-License-Identifier: Apache-2.0.
2
2
  # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
3
 
4
- """Mock websockets for linters."""
5
4
  # pylint: disable=invalid-name,line-too-long,unused-argument,too-few-public-methods,no-self-use
6
5
  # pylint: disable=missing-class-docstring,missing-function-docstring,missing-return-doc
7
6
  # flake8: noqa: E501, D101, D102, D106
7
+ # pyright: reportUnusedParameter=false, reportUninitializedInstanceVariable=false
8
+ """Mock websockets for linters."""
8
9
 
9
10
  from typing import Any # pragma: no cover
10
11