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.
- waldiez/__init__.py +1 -1
- waldiez/_version.py +1 -1
- waldiez/cli.py +18 -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 +9 -10
- 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 +34 -23
- waldiez/exporting/agent/extras/group_member_extras.py +6 -5
- waldiez/exporting/agent/extras/handoffs/after_work.py +1 -1
- waldiez/exporting/agent/extras/handoffs/available.py +1 -1
- waldiez/exporting/agent/extras/handoffs/condition.py +3 -2
- waldiez/exporting/agent/extras/handoffs/handoff.py +1 -1
- waldiez/exporting/agent/extras/handoffs/target.py +6 -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/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/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 +2 -2
- waldiez/exporting/core/protocols.py +6 -5
- waldiez/exporting/core/result.py +25 -28
- waldiez/exporting/core/types.py +10 -10
- waldiez/exporting/core/utils/llm_config.py +2 -2
- waldiez/exporting/core/validation.py +10 -11
- waldiez/exporting/flow/execution_generator.py +98 -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 +5 -5
- waldiez/exporting/flow/utils/importing.py +6 -7
- waldiez/exporting/flow/utils/linting.py +25 -9
- waldiez/exporting/flow/utils/logging.py +2 -2
- 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 +8 -4
- waldiez/io/_ws.py +10 -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 +52 -53
- waldiez/io/utils.py +3 -0
- waldiez/io/ws.py +5 -1
- waldiez/logger.py +16 -3
- waldiez/models/agents/__init__.py +3 -0
- waldiez/models/agents/agent/agent.py +23 -16
- 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 +27 -20
- waldiez/models/chat/chat_data.py +22 -19
- 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/dict_utils.py +8 -6
- waldiez/models/common/handoff.py +18 -17
- waldiez/models/common/method_utils.py +7 -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 +5 -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 +1 -0
- waldiez/models/tool/predefined/_google.py +1 -0
- waldiez/models/tool/predefined/_perplexity.py +1 -0
- waldiez/models/tool/predefined/_searxng.py +1 -0
- waldiez/models/tool/predefined/_tavily.py +1 -0
- waldiez/models/tool/predefined/_wikipedia.py +1 -0
- 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 +117 -264
- 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 +22 -25
- waldiez/running/step_by_step/breakpoints_mixin.py +192 -60
- waldiez/running/step_by_step/command_handler.py +3 -0
- waldiez/running/step_by_step/events_processor.py +194 -14
- waldiez/running/step_by_step/step_by_step_models.py +110 -43
- waldiez/running/step_by_step/step_by_step_runner.py +107 -57
- waldiez/running/subprocess_runner/__base__.py +9 -1
- waldiez/running/subprocess_runner/_async_runner.py +5 -3
- waldiez/running/subprocess_runner/_sync_runner.py +6 -2
- waldiez/running/subprocess_runner/runner.py +39 -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/_file_handler.py +18 -18
- waldiez/ws/_mock.py +2 -1
- waldiez/ws/cli.py +36 -12
- waldiez/ws/client_manager.py +35 -27
- waldiez/ws/errors.py +3 -0
- waldiez/ws/models.py +43 -52
- waldiez/ws/reloader.py +12 -4
- waldiez/ws/server.py +85 -55
- waldiez/ws/session_manager.py +8 -9
- waldiez/ws/session_stats.py +1 -1
- waldiez/ws/utils.py +4 -1
- {waldiez-0.6.0.dist-info → waldiez-0.6.1.dist-info}/METADATA +82 -93
- waldiez-0.6.1.dist-info/RECORD +254 -0
- waldiez/running/post_run.py +0 -186
- waldiez/running/pre_run.py +0 -281
- waldiez/running/run_results.py +0 -14
- waldiez/running/utils.py +0 -625
- waldiez-0.6.0.dist-info/RECORD +0 -251
- {waldiez-0.6.0.dist-info → waldiez-0.6.1.dist-info}/WHEEL +0 -0
- {waldiez-0.6.0.dist-info → waldiez-0.6.1.dist-info}/entry_points.txt +0 -0
- {waldiez-0.6.0.dist-info → waldiez-0.6.1.dist-info}/licenses/LICENSE +0 -0
- {waldiez-0.6.0.dist-info → waldiez-0.6.1.dist-info}/licenses/NOTICE.md +0 -0
waldiez/utils/__init__.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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)
|
waldiez/ws/_file_handler.py
CHANGED
|
@@ -23,7 +23,7 @@ class FileRequestHandler:
|
|
|
23
23
|
"""Handles file-related requests."""
|
|
24
24
|
|
|
25
25
|
@staticmethod
|
|
26
|
-
def
|
|
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
|
-
|
|
50
|
+
path = msg.path or f"waldiez_{client_id}.waldiez"
|
|
51
51
|
try:
|
|
52
52
|
output_path = resolve_output_path(
|
|
53
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
72
|
+
output_path.write_text(msg.data, encoding="utf-8", newline="\n")
|
|
73
73
|
|
|
74
74
|
return SaveFlowResponse.ok(
|
|
75
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
110
|
+
format=target_format,
|
|
111
111
|
).model_dump(mode="json")
|
|
112
112
|
|
|
113
113
|
try:
|
|
114
|
-
waldiez_data = Waldiez.from_dict(json.loads(msg.
|
|
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
|
-
|
|
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
|
-
|
|
123
|
+
path = msg.path or f"waldiez_{client_id}.{target_format}"
|
|
124
124
|
output_path = resolve_output_path(
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
142
|
-
|
|
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),
|
|
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
|
|