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.
- waldiez/__init__.py +1 -1
- waldiez/_version.py +1 -1
- waldiez/cli.py +19 -7
- waldiez/cli_extras/jupyter.py +3 -0
- waldiez/cli_extras/runner.py +3 -1
- waldiez/cli_extras/studio.py +3 -1
- waldiez/exporter.py +9 -3
- waldiez/exporting/agent/exporter.py +15 -16
- waldiez/exporting/agent/extras/captain_agent_extras.py +6 -6
- waldiez/exporting/agent/extras/doc_agent_extras.py +6 -6
- waldiez/exporting/agent/extras/group_manager_agent_extas.py +40 -24
- waldiez/exporting/agent/extras/group_member_extras.py +6 -5
- waldiez/exporting/agent/extras/handoffs/after_work.py +2 -1
- waldiez/exporting/agent/extras/handoffs/available.py +2 -1
- waldiez/exporting/agent/extras/handoffs/condition.py +3 -2
- waldiez/exporting/agent/extras/handoffs/handoff.py +2 -1
- waldiez/exporting/agent/extras/handoffs/target.py +7 -4
- waldiez/exporting/agent/extras/rag/chroma_extras.py +27 -19
- waldiez/exporting/agent/extras/rag/mongo_extras.py +8 -8
- waldiez/exporting/agent/extras/rag/pgvector_extras.py +5 -5
- waldiez/exporting/agent/extras/rag/qdrant_extras.py +5 -4
- waldiez/exporting/agent/extras/rag/vector_db_extras.py +1 -1
- waldiez/exporting/agent/extras/rag_user_proxy_agent_extras.py +5 -7
- waldiez/exporting/agent/extras/reasoning_agent_extras.py +3 -5
- waldiez/exporting/agent/termination.py +1 -0
- waldiez/exporting/chats/exporter.py +4 -4
- waldiez/exporting/chats/processor.py +1 -2
- waldiez/exporting/chats/utils/common.py +89 -48
- waldiez/exporting/chats/utils/group.py +9 -9
- waldiez/exporting/chats/utils/nested.py +7 -7
- waldiez/exporting/chats/utils/sequential.py +1 -1
- waldiez/exporting/chats/utils/single.py +2 -2
- waldiez/exporting/core/constants.py +3 -1
- waldiez/exporting/core/content.py +7 -7
- waldiez/exporting/core/context.py +5 -3
- waldiez/exporting/core/exporter.py +5 -3
- waldiez/exporting/core/exporters.py +2 -2
- waldiez/exporting/core/extras/agent_extras/captain_extras.py +2 -2
- waldiez/exporting/core/extras/agent_extras/group_manager_extras.py +2 -2
- waldiez/exporting/core/extras/agent_extras/rag_user_extras.py +2 -2
- waldiez/exporting/core/extras/agent_extras/standard_extras.py +3 -8
- waldiez/exporting/core/extras/base.py +7 -5
- waldiez/exporting/core/extras/flow_extras.py +4 -5
- waldiez/exporting/core/extras/model_extras.py +2 -2
- waldiez/exporting/core/extras/path_resolver.py +1 -2
- waldiez/exporting/core/extras/serializer.py +13 -11
- waldiez/exporting/core/protocols.py +6 -5
- waldiez/exporting/core/result.py +25 -28
- waldiez/exporting/core/types.py +11 -10
- waldiez/exporting/core/utils/llm_config.py +4 -4
- waldiez/exporting/core/validation.py +10 -11
- waldiez/exporting/flow/execution_generator.py +99 -10
- waldiez/exporting/flow/exporter.py +2 -2
- waldiez/exporting/flow/factory.py +2 -2
- waldiez/exporting/flow/file_generator.py +4 -2
- waldiez/exporting/flow/merger.py +5 -3
- waldiez/exporting/flow/orchestrator.py +72 -2
- waldiez/exporting/flow/utils/common.py +6 -6
- waldiez/exporting/flow/utils/importing.py +7 -8
- waldiez/exporting/flow/utils/linting.py +25 -9
- waldiez/exporting/flow/utils/logging.py +5 -77
- waldiez/exporting/models/exporter.py +8 -8
- waldiez/exporting/models/processor.py +5 -5
- waldiez/exporting/tools/exporter.py +2 -2
- waldiez/exporting/tools/processor.py +7 -4
- waldiez/io/__init__.py +11 -5
- waldiez/io/_ws.py +12 -6
- waldiez/io/models/constants.py +10 -10
- waldiez/io/models/content/audio.py +1 -0
- waldiez/io/models/content/base.py +20 -18
- waldiez/io/models/content/file.py +1 -0
- waldiez/io/models/content/image.py +1 -0
- waldiez/io/models/content/text.py +1 -0
- waldiez/io/models/content/video.py +1 -0
- waldiez/io/models/user_input.py +10 -5
- waldiez/io/models/user_response.py +17 -16
- waldiez/io/mqtt.py +18 -31
- waldiez/io/redis.py +18 -22
- waldiez/io/structured.py +122 -70
- waldiez/io/utils.py +19 -10
- waldiez/io/ws.py +7 -3
- waldiez/logger.py +16 -3
- waldiez/models/agents/__init__.py +3 -0
- waldiez/models/agents/agent/agent.py +25 -17
- waldiez/models/agents/agent/agent_data.py +25 -22
- waldiez/models/agents/agent/code_execution.py +9 -11
- waldiez/models/agents/agent/termination_message.py +10 -12
- waldiez/models/agents/agent/update_system_message.py +2 -4
- waldiez/models/agents/agents.py +8 -8
- waldiez/models/agents/assistant/assistant.py +6 -3
- waldiez/models/agents/assistant/assistant_data.py +2 -2
- waldiez/models/agents/captain/captain_agent.py +7 -4
- waldiez/models/agents/captain/captain_agent_data.py +5 -7
- waldiez/models/agents/doc_agent/doc_agent.py +7 -4
- waldiez/models/agents/doc_agent/doc_agent_data.py +9 -10
- waldiez/models/agents/doc_agent/rag_query_engine.py +10 -12
- waldiez/models/agents/extra_requirements.py +3 -3
- waldiez/models/agents/group_manager/group_manager.py +12 -7
- waldiez/models/agents/group_manager/group_manager_data.py +13 -12
- waldiez/models/agents/group_manager/speakers.py +17 -19
- waldiez/models/agents/rag_user_proxy/rag_user_proxy.py +7 -4
- waldiez/models/agents/rag_user_proxy/rag_user_proxy_data.py +4 -1
- waldiez/models/agents/rag_user_proxy/retrieve_config.py +69 -63
- waldiez/models/agents/rag_user_proxy/vector_db_config.py +19 -19
- waldiez/models/agents/reasoning/reasoning_agent.py +7 -4
- waldiez/models/agents/reasoning/reasoning_agent_data.py +3 -2
- waldiez/models/agents/reasoning/reasoning_agent_reason_config.py +8 -8
- waldiez/models/agents/user_proxy/user_proxy.py +6 -3
- waldiez/models/agents/user_proxy/user_proxy_data.py +1 -1
- waldiez/models/chat/chat.py +28 -20
- waldiez/models/chat/chat_data.py +22 -21
- waldiez/models/chat/chat_message.py +9 -9
- waldiez/models/chat/chat_nested.py +9 -9
- waldiez/models/chat/chat_summary.py +6 -6
- waldiez/models/common/__init__.py +2 -0
- waldiez/models/common/ag2_version.py +2 -0
- waldiez/models/common/base.py +2 -0
- waldiez/models/common/dict_utils.py +8 -6
- waldiez/models/common/handoff.py +20 -17
- waldiez/models/common/method_utils.py +9 -7
- waldiez/models/common/naming.py +49 -0
- waldiez/models/flow/flow.py +11 -6
- waldiez/models/flow/flow_data.py +23 -17
- waldiez/models/flow/info.py +3 -3
- waldiez/models/flow/naming.py +2 -1
- waldiez/models/model/_aws.py +11 -13
- waldiez/models/model/_llm.py +8 -0
- waldiez/models/model/_price.py +2 -4
- waldiez/models/model/extra_requirements.py +1 -3
- waldiez/models/model/model.py +2 -2
- waldiez/models/model/model_data.py +21 -21
- waldiez/models/tool/extra_requirements.py +2 -4
- waldiez/models/tool/predefined/_duckduckgo.py +1 -0
- waldiez/models/tool/predefined/_email.py +4 -0
- waldiez/models/tool/predefined/_google.py +1 -0
- waldiez/models/tool/predefined/_perplexity.py +2 -1
- waldiez/models/tool/predefined/_searxng.py +2 -1
- waldiez/models/tool/predefined/_tavily.py +1 -0
- waldiez/models/tool/predefined/_wikipedia.py +2 -1
- waldiez/models/tool/predefined/_youtube.py +1 -0
- waldiez/models/tool/tool.py +8 -5
- waldiez/models/tool/tool_data.py +2 -2
- waldiez/models/waldiez.py +152 -4
- waldiez/runner.py +11 -5
- waldiez/running/async_utils.py +192 -0
- waldiez/running/base_runner.py +155 -241
- waldiez/running/dir_utils.py +52 -0
- waldiez/running/environment.py +10 -44
- waldiez/running/events_mixin.py +252 -0
- waldiez/running/exceptions.py +20 -0
- waldiez/running/gen_seq_diagram.py +18 -15
- waldiez/running/io_utils.py +216 -0
- waldiez/running/protocol.py +11 -5
- waldiez/running/requirements_mixin.py +65 -0
- waldiez/running/results_mixin.py +926 -0
- waldiez/running/standard_runner.py +24 -27
- waldiez/running/step_by_step/breakpoints_mixin.py +503 -47
- waldiez/running/step_by_step/command_handler.py +154 -0
- waldiez/running/step_by_step/events_processor.py +379 -0
- waldiez/running/step_by_step/step_by_step_models.py +425 -41
- waldiez/running/step_by_step/step_by_step_runner.py +437 -382
- waldiez/running/subprocess_runner/__base__.py +13 -8
- waldiez/running/subprocess_runner/_async_runner.py +6 -4
- waldiez/running/subprocess_runner/_sync_runner.py +11 -6
- waldiez/running/subprocess_runner/runner.py +48 -23
- waldiez/running/timeline_processor.py +1 -1
- waldiez/utils/__init__.py +2 -0
- waldiez/utils/conflict_checker.py +4 -4
- waldiez/utils/python_manager.py +415 -0
- waldiez/ws/__init__.py +8 -7
- waldiez/ws/_file_handler.py +18 -20
- waldiez/ws/_mock.py +75 -0
- waldiez/ws/cli.py +58 -10
- waldiez/ws/client_manager.py +77 -53
- waldiez/ws/errors.py +3 -0
- waldiez/ws/models.py +61 -53
- waldiez/ws/reloader.py +33 -4
- waldiez/ws/server.py +121 -52
- waldiez/ws/session_manager.py +8 -9
- waldiez/ws/session_stats.py +1 -1
- waldiez/ws/utils.py +33 -5
- {waldiez-0.5.10.dist-info → waldiez-0.6.1.dist-info}/METADATA +107 -109
- waldiez-0.6.1.dist-info/RECORD +254 -0
- waldiez/running/post_run.py +0 -180
- waldiez/running/pre_run.py +0 -159
- waldiez/running/run_results.py +0 -14
- waldiez/running/utils.py +0 -511
- waldiez-0.5.10.dist-info/RECORD +0 -248
- {waldiez-0.5.10.dist-info → waldiez-0.6.1.dist-info}/WHEEL +0 -0
- {waldiez-0.5.10.dist-info → waldiez-0.6.1.dist-info}/entry_points.txt +0 -0
- {waldiez-0.5.10.dist-info → waldiez-0.6.1.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
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
|
@@ -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
|
|
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
|
-
|
|
50
|
+
path = msg.path or f"waldiez_{client_id}.waldiez"
|
|
53
51
|
try:
|
|
54
52
|
output_path = resolve_output_path(
|
|
55
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
72
|
+
output_path.write_text(msg.data, encoding="utf-8", newline="\n")
|
|
75
73
|
|
|
76
74
|
return SaveFlowResponse.ok(
|
|
77
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
110
|
+
format=target_format,
|
|
113
111
|
).model_dump(mode="json")
|
|
114
112
|
|
|
115
113
|
try:
|
|
116
|
-
waldiez_data = Waldiez.from_dict(json.loads(msg.
|
|
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
|
-
|
|
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
|
-
|
|
123
|
+
path = msg.path or f"waldiez_{client_id}.{target_format}"
|
|
126
124
|
output_path = resolve_output_path(
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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),
|
|
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
|