waldiez 0.5.8__py3-none-any.whl → 0.5.10__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/_version.py +1 -1
- waldiez/cli.py +112 -24
- waldiez/exporting/agent/exporter.py +3 -0
- waldiez/exporting/agent/extras/captain_agent_extras.py +44 -7
- waldiez/exporting/agent/extras/handoffs/condition.py +3 -1
- waldiez/exporting/chats/utils/common.py +25 -23
- waldiez/exporting/core/__init__.py +0 -2
- waldiez/exporting/core/context.py +13 -13
- waldiez/exporting/core/protocols.py +0 -141
- waldiez/exporting/core/result.py +5 -5
- waldiez/exporting/flow/merger.py +2 -2
- waldiez/exporting/flow/orchestrator.py +1 -0
- waldiez/exporting/flow/utils/common.py +2 -2
- waldiez/exporting/flow/utils/importing.py +1 -0
- waldiez/exporting/flow/utils/logging.py +6 -7
- waldiez/exporting/tools/exporter.py +5 -0
- waldiez/exporting/tools/factory.py +4 -0
- waldiez/exporting/tools/processor.py +5 -1
- waldiez/io/_ws.py +13 -5
- waldiez/io/models/content/image.py +1 -0
- waldiez/io/models/user_input.py +4 -4
- waldiez/io/models/user_response.py +1 -0
- waldiez/io/mqtt.py +1 -1
- waldiez/io/structured.py +17 -17
- waldiez/io/utils.py +1 -1
- waldiez/io/ws.py +9 -11
- waldiez/logger.py +180 -63
- waldiez/models/agents/agent/update_system_message.py +0 -2
- waldiez/models/agents/doc_agent/doc_agent.py +8 -1
- waldiez/models/common/dict_utils.py +169 -40
- waldiez/models/flow/flow.py +6 -6
- waldiez/models/flow/info.py +5 -1
- waldiez/models/model/_llm.py +28 -14
- waldiez/models/model/model.py +4 -1
- waldiez/models/model/model_data.py +18 -5
- waldiez/models/tool/predefined/_config.py +5 -1
- waldiez/models/tool/predefined/_duckduckgo.py +4 -0
- waldiez/models/tool/predefined/_email.py +474 -0
- waldiez/models/tool/predefined/_google.py +8 -6
- waldiez/models/tool/predefined/_perplexity.py +3 -0
- waldiez/models/tool/predefined/_searxng.py +3 -0
- waldiez/models/tool/predefined/_tavily.py +4 -1
- waldiez/models/tool/predefined/_wikipedia.py +4 -1
- waldiez/models/tool/predefined/_youtube.py +4 -1
- waldiez/models/tool/predefined/protocol.py +3 -0
- waldiez/models/tool/tool.py +22 -4
- waldiez/models/waldiez.py +12 -0
- waldiez/runner.py +37 -54
- waldiez/running/__init__.py +6 -0
- waldiez/running/base_runner.py +310 -353
- waldiez/running/environment.py +1 -0
- waldiez/running/exceptions.py +9 -0
- waldiez/running/post_run.py +4 -4
- waldiez/running/pre_run.py +51 -40
- waldiez/running/protocol.py +21 -101
- waldiez/running/run_results.py +1 -1
- waldiez/running/standard_runner.py +84 -277
- waldiez/running/step_by_step/__init__.py +46 -0
- waldiez/running/step_by_step/breakpoints_mixin.py +188 -0
- waldiez/running/step_by_step/step_by_step_models.py +224 -0
- waldiez/running/step_by_step/step_by_step_runner.py +745 -0
- waldiez/running/subprocess_runner/__base__.py +282 -0
- waldiez/running/subprocess_runner/__init__.py +16 -0
- waldiez/running/subprocess_runner/_async_runner.py +362 -0
- waldiez/running/subprocess_runner/_sync_runner.py +455 -0
- waldiez/running/subprocess_runner/runner.py +561 -0
- waldiez/running/timeline_processor.py +1 -1
- waldiez/running/utils.py +376 -1
- waldiez/utils/version.py +2 -6
- waldiez/ws/__init__.py +70 -0
- waldiez/ws/__main__.py +15 -0
- waldiez/ws/_file_handler.py +201 -0
- waldiez/ws/cli.py +211 -0
- waldiez/ws/client_manager.py +835 -0
- waldiez/ws/errors.py +416 -0
- waldiez/ws/models.py +971 -0
- waldiez/ws/reloader.py +342 -0
- waldiez/ws/server.py +469 -0
- waldiez/ws/session_manager.py +393 -0
- waldiez/ws/session_stats.py +83 -0
- waldiez/ws/utils.py +385 -0
- {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/METADATA +74 -74
- {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/RECORD +87 -65
- waldiez/running/patch_io_stream.py +0 -210
- {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/WHEEL +0 -0
- {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/entry_points.txt +0 -0
- {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/licenses/LICENSE +0 -0
- {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/licenses/NOTICE.md +0 -0
waldiez/running/utils.py
CHANGED
|
@@ -3,17 +3,44 @@
|
|
|
3
3
|
"""Common utilities for the waldiez runner."""
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import functools
|
|
7
|
+
import inspect
|
|
8
|
+
import logging
|
|
6
9
|
import os
|
|
7
10
|
import re
|
|
8
11
|
import subprocess
|
|
9
12
|
import sys
|
|
13
|
+
import threading
|
|
14
|
+
import traceback
|
|
10
15
|
|
|
11
16
|
# noinspection PyProtectedMember
|
|
12
17
|
from asyncio.subprocess import Process
|
|
13
18
|
from contextlib import asynccontextmanager, contextmanager
|
|
14
19
|
from dataclasses import dataclass
|
|
20
|
+
from getpass import getpass
|
|
15
21
|
from pathlib import Path
|
|
16
|
-
from typing import
|
|
22
|
+
from typing import (
|
|
23
|
+
Any,
|
|
24
|
+
AsyncIterator,
|
|
25
|
+
Callable,
|
|
26
|
+
Coroutine,
|
|
27
|
+
Generic,
|
|
28
|
+
Iterator,
|
|
29
|
+
TypeVar,
|
|
30
|
+
Union,
|
|
31
|
+
cast,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
T = TypeVar("T")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class _ResultContainer(Generic[T]):
|
|
40
|
+
"""Container for thread execution results with proper typing."""
|
|
41
|
+
|
|
42
|
+
result: T | None = None
|
|
43
|
+
exception: BaseException | None = None
|
|
17
44
|
|
|
18
45
|
|
|
19
46
|
@dataclass
|
|
@@ -134,3 +161,351 @@ async def create_async_subprocess(setup: ProcessSetup) -> Process:
|
|
|
134
161
|
# stdin=asyncio.subprocess.PIPE,
|
|
135
162
|
env={**os.environ},
|
|
136
163
|
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
async def input_async(prompt: str, *, password: bool = False) -> str:
|
|
167
|
+
"""Asynchronous input function.
|
|
168
|
+
|
|
169
|
+
Parameters
|
|
170
|
+
----------
|
|
171
|
+
prompt : str
|
|
172
|
+
The prompt to display to the user.
|
|
173
|
+
password : bool, optional
|
|
174
|
+
Whether to hide input (password mode), by default False.
|
|
175
|
+
|
|
176
|
+
Returns
|
|
177
|
+
-------
|
|
178
|
+
str
|
|
179
|
+
The user input.
|
|
180
|
+
"""
|
|
181
|
+
if password:
|
|
182
|
+
try:
|
|
183
|
+
return await asyncio.to_thread(getpass, prompt)
|
|
184
|
+
except EOFError:
|
|
185
|
+
return ""
|
|
186
|
+
try:
|
|
187
|
+
return await asyncio.to_thread(input, prompt)
|
|
188
|
+
except EOFError:
|
|
189
|
+
return ""
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def input_sync(prompt: str, *, password: bool = False) -> str:
|
|
193
|
+
"""Input function (synchronous).
|
|
194
|
+
|
|
195
|
+
Parameters
|
|
196
|
+
----------
|
|
197
|
+
prompt : str
|
|
198
|
+
The prompt to display to the user.
|
|
199
|
+
password : bool, optional
|
|
200
|
+
Whether to hide input (password mode), by default False.
|
|
201
|
+
|
|
202
|
+
Returns
|
|
203
|
+
-------
|
|
204
|
+
str
|
|
205
|
+
The user input.
|
|
206
|
+
"""
|
|
207
|
+
if password:
|
|
208
|
+
try:
|
|
209
|
+
return getpass(prompt)
|
|
210
|
+
except EOFError:
|
|
211
|
+
return ""
|
|
212
|
+
try:
|
|
213
|
+
return input(prompt)
|
|
214
|
+
except EOFError:
|
|
215
|
+
return ""
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# pylint: disable=import-outside-toplevel,too-complex
|
|
219
|
+
def get_printer() -> Callable[..., None]: # noqa: C901
|
|
220
|
+
"""Get the printer function.
|
|
221
|
+
|
|
222
|
+
Returns
|
|
223
|
+
-------
|
|
224
|
+
Callable[..., None]
|
|
225
|
+
The printer function that handles Unicode encoding errors gracefully.
|
|
226
|
+
"""
|
|
227
|
+
try:
|
|
228
|
+
# noinspection PyUnresolvedReferences
|
|
229
|
+
from autogen.io import IOStream # type: ignore
|
|
230
|
+
|
|
231
|
+
printer = IOStream.get_default().print
|
|
232
|
+
except ImportError: # pragma: no cover
|
|
233
|
+
# Fallback to standard print if autogen is not available
|
|
234
|
+
printer = print
|
|
235
|
+
|
|
236
|
+
# noinspection PyBroadException,TryExceptPass
|
|
237
|
+
def safe_printer(*args: Any, **kwargs: Any) -> None: # noqa: C901
|
|
238
|
+
"""Safe printer that handles Unicode encoding errors.
|
|
239
|
+
|
|
240
|
+
Parameters
|
|
241
|
+
----------
|
|
242
|
+
*args : Any
|
|
243
|
+
Arguments to pass to the printer
|
|
244
|
+
**kwargs : Any
|
|
245
|
+
Keyword arguments to pass to the printer
|
|
246
|
+
"""
|
|
247
|
+
# pylint: disable=broad-exception-caught,too-many-try-statements
|
|
248
|
+
try:
|
|
249
|
+
printer(*args, **kwargs)
|
|
250
|
+
except (UnicodeEncodeError, UnicodeDecodeError):
|
|
251
|
+
# First fallback: try to get a safe string representation
|
|
252
|
+
try:
|
|
253
|
+
msg, flush = get_what_to_print(*args, **kwargs)
|
|
254
|
+
# Convert problematic characters to safe representations
|
|
255
|
+
safe_msg = msg.encode("utf-8", errors="replace").decode("utf-8")
|
|
256
|
+
printer(safe_msg, end="", flush=flush)
|
|
257
|
+
except (UnicodeEncodeError, UnicodeDecodeError):
|
|
258
|
+
# Second fallback: use built-in print with safe encoding
|
|
259
|
+
try:
|
|
260
|
+
# Convert args to safe string representations
|
|
261
|
+
safe_args: list[str] = []
|
|
262
|
+
for arg in args:
|
|
263
|
+
try:
|
|
264
|
+
safe_args.append(
|
|
265
|
+
str(arg)
|
|
266
|
+
.encode("utf-8", errors="replace")
|
|
267
|
+
.decode("utf-8")
|
|
268
|
+
)
|
|
269
|
+
except (
|
|
270
|
+
UnicodeEncodeError,
|
|
271
|
+
UnicodeDecodeError,
|
|
272
|
+
): # pragma: no cover
|
|
273
|
+
safe_args.append(repr(arg))
|
|
274
|
+
|
|
275
|
+
# Use built-in print instead of the custom printer
|
|
276
|
+
print(*safe_args, **kwargs)
|
|
277
|
+
|
|
278
|
+
except Exception:
|
|
279
|
+
# Final fallback: write directly to stderr buffer
|
|
280
|
+
error_msg = (
|
|
281
|
+
"Could not print the message due to encoding issues.\n"
|
|
282
|
+
)
|
|
283
|
+
to_sys_stderr(error_msg)
|
|
284
|
+
except Exception as e:
|
|
285
|
+
# Handle any other unexpected errors
|
|
286
|
+
traceback.print_exc()
|
|
287
|
+
error_msg = f"Unexpected error in printer: {str(e)}\n"
|
|
288
|
+
to_sys_stderr(error_msg)
|
|
289
|
+
|
|
290
|
+
return safe_printer
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def to_sys_stderr(msg: str) -> None:
|
|
294
|
+
"""Write a message to sys.stderr.
|
|
295
|
+
|
|
296
|
+
Parameters
|
|
297
|
+
----------
|
|
298
|
+
msg : str
|
|
299
|
+
The message to write to stderr.
|
|
300
|
+
"""
|
|
301
|
+
# pylint: disable=broad-exception-caught
|
|
302
|
+
# noinspection TryExceptPass,PyBroadException
|
|
303
|
+
try:
|
|
304
|
+
if hasattr(sys.stderr, "buffer"):
|
|
305
|
+
sys.stderr.buffer.write(msg.encode("utf-8", errors="replace"))
|
|
306
|
+
sys.stderr.buffer.flush()
|
|
307
|
+
else: # pragma: no cover
|
|
308
|
+
sys.stderr.write(msg)
|
|
309
|
+
sys.stderr.flush()
|
|
310
|
+
except Exception: # pragma: no cover
|
|
311
|
+
pass
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def get_what_to_print(*args: Any, **kwargs: Any) -> tuple[str, bool]:
|
|
315
|
+
"""Extract message and flush flag from print arguments.
|
|
316
|
+
|
|
317
|
+
Parameters
|
|
318
|
+
----------
|
|
319
|
+
*args : Any
|
|
320
|
+
Arguments to print
|
|
321
|
+
**kwargs : Any
|
|
322
|
+
Keyword arguments for print function
|
|
323
|
+
|
|
324
|
+
Returns
|
|
325
|
+
-------
|
|
326
|
+
tuple[str, bool]
|
|
327
|
+
Message to print and flush flag
|
|
328
|
+
"""
|
|
329
|
+
# Convert all args to strings and join with spaces (like print does)
|
|
330
|
+
msg = " ".join(str(arg) for arg in args)
|
|
331
|
+
|
|
332
|
+
# Handle sep parameter
|
|
333
|
+
sep = kwargs.get("sep", " ")
|
|
334
|
+
if isinstance(sep, bytes): # pragma: no cover
|
|
335
|
+
sep = sep.decode("utf-8", errors="replace")
|
|
336
|
+
if len(args) > 1:
|
|
337
|
+
msg = sep.join(str(arg) for arg in args)
|
|
338
|
+
|
|
339
|
+
# Handle end parameter
|
|
340
|
+
end = kwargs.get("end", "\n")
|
|
341
|
+
if isinstance(end, bytes): # pragma: no cover
|
|
342
|
+
end = end.decode("utf-8", errors="replace")
|
|
343
|
+
msg += end
|
|
344
|
+
|
|
345
|
+
# Handle flush parameter
|
|
346
|
+
flush = kwargs.get("flush", False)
|
|
347
|
+
# noinspection PyUnreachableCode
|
|
348
|
+
if not isinstance(flush, bool): # pragma: no cover
|
|
349
|
+
flush = False
|
|
350
|
+
|
|
351
|
+
return msg, flush
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def is_async_callable(fn: Any) -> bool:
|
|
355
|
+
"""Check if a function is async callable, including partials/callables.
|
|
356
|
+
|
|
357
|
+
Parameters
|
|
358
|
+
----------
|
|
359
|
+
fn : Any
|
|
360
|
+
The function to check.
|
|
361
|
+
|
|
362
|
+
Returns
|
|
363
|
+
-------
|
|
364
|
+
bool
|
|
365
|
+
True if the function is async callable, False otherwise.
|
|
366
|
+
"""
|
|
367
|
+
if isinstance(fn, functools.partial):
|
|
368
|
+
fn = fn.func
|
|
369
|
+
unwrapped = inspect.unwrap(fn)
|
|
370
|
+
return inspect.iscoroutinefunction(
|
|
371
|
+
unwrapped
|
|
372
|
+
) or inspect.iscoroutinefunction(
|
|
373
|
+
getattr(unwrapped, "__call__", None), # noqa: B004
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def syncify(
|
|
378
|
+
async_func: Callable[..., Coroutine[Any, Any, T]],
|
|
379
|
+
timeout: float | None = None,
|
|
380
|
+
) -> Callable[..., T]:
|
|
381
|
+
"""Convert an async function to a sync function.
|
|
382
|
+
|
|
383
|
+
This function handles the conversion of async functions to sync functions,
|
|
384
|
+
properly managing event loops and thread execution contexts.
|
|
385
|
+
|
|
386
|
+
Parameters
|
|
387
|
+
----------
|
|
388
|
+
async_func : Callable[..., Coroutine[Any, Any, T]]
|
|
389
|
+
The async function to convert.
|
|
390
|
+
timeout : float | None, optional
|
|
391
|
+
The timeout for the sync function. Defaults to None.
|
|
392
|
+
|
|
393
|
+
Returns
|
|
394
|
+
-------
|
|
395
|
+
Callable[..., T]
|
|
396
|
+
The converted sync function.
|
|
397
|
+
|
|
398
|
+
Raises
|
|
399
|
+
------
|
|
400
|
+
TimeoutError
|
|
401
|
+
If the async function times out.
|
|
402
|
+
RuntimeError
|
|
403
|
+
If there are issues with event loop management.
|
|
404
|
+
"""
|
|
405
|
+
|
|
406
|
+
def _sync_wrapper(*args: Any, **kwargs: Any) -> T:
|
|
407
|
+
"""Get the result of the async function."""
|
|
408
|
+
# pylint: disable=too-many-try-statements
|
|
409
|
+
try:
|
|
410
|
+
# Check if we're already in an event loop
|
|
411
|
+
asyncio.get_running_loop()
|
|
412
|
+
return _run_in_thread(async_func, args, kwargs, timeout)
|
|
413
|
+
except RuntimeError:
|
|
414
|
+
# No event loop running, we can use asyncio.run directly
|
|
415
|
+
logger.debug("No event loop running, using asyncio.run")
|
|
416
|
+
|
|
417
|
+
# Create a new event loop and run the coroutine
|
|
418
|
+
try:
|
|
419
|
+
if timeout is not None:
|
|
420
|
+
# Need to run with asyncio.run and wait_for inside
|
|
421
|
+
async def _with_timeout() -> T:
|
|
422
|
+
return await asyncio.wait_for(
|
|
423
|
+
async_func(*args, **kwargs), timeout=timeout
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
return asyncio.run(_with_timeout())
|
|
427
|
+
return asyncio.run(async_func(*args, **kwargs))
|
|
428
|
+
except (asyncio.TimeoutError, TimeoutError) as e:
|
|
429
|
+
raise TimeoutError(
|
|
430
|
+
f"Async function timed out after {timeout} seconds"
|
|
431
|
+
) from e
|
|
432
|
+
|
|
433
|
+
return _sync_wrapper
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _run_in_thread(
|
|
437
|
+
async_func: Callable[..., Coroutine[Any, Any, T]],
|
|
438
|
+
args: tuple[Any, ...],
|
|
439
|
+
kwargs: dict[str, Any],
|
|
440
|
+
timeout: float | None,
|
|
441
|
+
) -> T:
|
|
442
|
+
"""Run async function in a separate thread.
|
|
443
|
+
|
|
444
|
+
Parameters
|
|
445
|
+
----------
|
|
446
|
+
async_func : Callable[..., Coroutine[Any, Any, T]]
|
|
447
|
+
The async function to run.
|
|
448
|
+
args : tuple[Any, ...]
|
|
449
|
+
Positional arguments for the function.
|
|
450
|
+
kwargs : dict[str, Any]
|
|
451
|
+
Keyword arguments for the function.
|
|
452
|
+
timeout : float | None
|
|
453
|
+
Timeout in seconds.
|
|
454
|
+
|
|
455
|
+
Returns
|
|
456
|
+
-------
|
|
457
|
+
T
|
|
458
|
+
The result of the async function.
|
|
459
|
+
|
|
460
|
+
Raises
|
|
461
|
+
------
|
|
462
|
+
TimeoutError
|
|
463
|
+
If the function execution times out.
|
|
464
|
+
RuntimeError
|
|
465
|
+
If thread execution fails unexpectedly.
|
|
466
|
+
"""
|
|
467
|
+
result_container: _ResultContainer[T] = _ResultContainer()
|
|
468
|
+
finished_event = threading.Event()
|
|
469
|
+
|
|
470
|
+
def _thread_target() -> None:
|
|
471
|
+
"""Target function for the thread."""
|
|
472
|
+
# pylint: disable=too-many-try-statements, broad-exception-caught
|
|
473
|
+
try:
|
|
474
|
+
if timeout is not None:
|
|
475
|
+
|
|
476
|
+
async def _with_timeout() -> T:
|
|
477
|
+
return await asyncio.wait_for(
|
|
478
|
+
async_func(*args, **kwargs), timeout=timeout
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
result_container.result = asyncio.run(_with_timeout())
|
|
482
|
+
else:
|
|
483
|
+
result_container.result = asyncio.run(
|
|
484
|
+
async_func(*args, **kwargs)
|
|
485
|
+
)
|
|
486
|
+
except (
|
|
487
|
+
BaseException
|
|
488
|
+
) as e: # Catch BaseException to propagate cancellations
|
|
489
|
+
result_container.exception = e
|
|
490
|
+
finally:
|
|
491
|
+
finished_event.set()
|
|
492
|
+
|
|
493
|
+
thread = threading.Thread(target=_thread_target, daemon=True)
|
|
494
|
+
thread.start()
|
|
495
|
+
|
|
496
|
+
# Wait for completion with timeout
|
|
497
|
+
timeout_buffer = 1.0 # 1 second buffer for cleanup
|
|
498
|
+
wait_timeout = timeout + timeout_buffer if timeout is not None else None
|
|
499
|
+
|
|
500
|
+
if not finished_event.wait(timeout=wait_timeout): # pragma: no cover
|
|
501
|
+
raise TimeoutError(
|
|
502
|
+
f"Function execution timed out after {timeout} seconds"
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
thread.join(timeout=timeout_buffer) # Give thread time to clean up
|
|
506
|
+
|
|
507
|
+
if result_container.exception is not None:
|
|
508
|
+
raise result_container.exception
|
|
509
|
+
|
|
510
|
+
# Use cast since we know the result should be T if no exception occurred
|
|
511
|
+
return cast(T, result_container.result)
|
waldiez/utils/version.py
CHANGED
|
@@ -30,7 +30,7 @@ def _get_waldiez_version_from_package_json() -> str | None:
|
|
|
30
30
|
def _get_waldiez_version_from_version_py() -> str | None:
|
|
31
31
|
"""Get the Waldiez version from _version.py."""
|
|
32
32
|
version_py_path = Path(__file__).parent.parent / "_version.py"
|
|
33
|
-
if version_py_path.exists():
|
|
33
|
+
if version_py_path.exists(): # pragma: no branch
|
|
34
34
|
with open(version_py_path, "r", encoding="utf-8") as f:
|
|
35
35
|
for line in f:
|
|
36
36
|
if line.startswith("__version__"):
|
|
@@ -48,14 +48,10 @@ def get_waldiez_version() -> str:
|
|
|
48
48
|
The Waldiez version, or "dev" if not found.
|
|
49
49
|
"""
|
|
50
50
|
w_version = _get_waldiez_version_from_importlib()
|
|
51
|
-
if not w_version:
|
|
51
|
+
if not w_version: # pragma: no branch
|
|
52
52
|
w_version = _get_waldiez_version_from_version_py()
|
|
53
53
|
if not w_version:
|
|
54
54
|
w_version = _get_waldiez_version_from_package_json()
|
|
55
55
|
if not w_version:
|
|
56
56
|
w_version = "dev"
|
|
57
57
|
return w_version
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if __name__ == "__main__":
|
|
61
|
-
print(get_waldiez_version())
|
waldiez/ws/__init__.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0.
|
|
2
|
+
# Copyright (c) 2024 - 2025 Waldiez and contributors.
|
|
3
|
+
# pylint: disable=duplicate-code
|
|
4
|
+
"""Waldiez WebSocket server module."""
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from typer.models import CommandInfo
|
|
8
|
+
|
|
9
|
+
from .cli import serve
|
|
10
|
+
from .client_manager import ClientManager
|
|
11
|
+
from .errors import (
|
|
12
|
+
ErrorCode,
|
|
13
|
+
ErrorHandler,
|
|
14
|
+
MessageHandlingError,
|
|
15
|
+
MessageParsingError,
|
|
16
|
+
OperationTimeoutError,
|
|
17
|
+
ServerOverloadError,
|
|
18
|
+
UnsupportedActionError,
|
|
19
|
+
WaldiezServerError,
|
|
20
|
+
)
|
|
21
|
+
from .server import WaldiezWsServer, run_server
|
|
22
|
+
from .session_manager import SessionManager
|
|
23
|
+
from .utils import (
|
|
24
|
+
ConnectionManager,
|
|
25
|
+
HealthChecker,
|
|
26
|
+
ServerHealth,
|
|
27
|
+
get_available_port,
|
|
28
|
+
is_port_available,
|
|
29
|
+
test_server_connection,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def add_ws_app(app: typer.Typer) -> None:
|
|
34
|
+
"""Add WebSocket server commands to the CLI.
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
app : typer.Typer
|
|
39
|
+
The Typer application instance.
|
|
40
|
+
"""
|
|
41
|
+
app.registered_commands.append(
|
|
42
|
+
CommandInfo(
|
|
43
|
+
name="ws",
|
|
44
|
+
help="Start the Waldiez WebSocket server.",
|
|
45
|
+
callback=serve,
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
"WaldiezWsServer",
|
|
52
|
+
"run_server",
|
|
53
|
+
"ClientManager",
|
|
54
|
+
"ConnectionManager",
|
|
55
|
+
"HealthChecker",
|
|
56
|
+
"ServerHealth",
|
|
57
|
+
"test_server_connection",
|
|
58
|
+
"ErrorHandler",
|
|
59
|
+
"ErrorCode",
|
|
60
|
+
"MessageParsingError",
|
|
61
|
+
"MessageHandlingError",
|
|
62
|
+
"UnsupportedActionError",
|
|
63
|
+
"ServerOverloadError",
|
|
64
|
+
"SessionManager",
|
|
65
|
+
"OperationTimeoutError",
|
|
66
|
+
"WaldiezServerError",
|
|
67
|
+
"get_available_port",
|
|
68
|
+
"is_port_available",
|
|
69
|
+
"add_ws_app",
|
|
70
|
+
]
|
waldiez/ws/__main__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0.
|
|
2
|
+
# Copyright (c) 2024 - 2025 Waldiez and contributors.
|
|
3
|
+
"""Main entry point for Waldiez WebSocket server."""
|
|
4
|
+
|
|
5
|
+
import sys # pragma: no cover
|
|
6
|
+
from pathlib import Path # pragma: no cover
|
|
7
|
+
|
|
8
|
+
try: # pragma: no cover
|
|
9
|
+
from .cli import app
|
|
10
|
+
except ImportError: # pragma: no cover
|
|
11
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
|
12
|
+
from waldiez.ws.cli import app
|
|
13
|
+
|
|
14
|
+
if __name__ == "__main__":
|
|
15
|
+
app()
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0.
|
|
2
|
+
# Copyright (c) 2024 - 2025 Waldiez and contributors.
|
|
3
|
+
|
|
4
|
+
"""Files related request handler."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from waldiez.exporter import WaldiezExporter
|
|
14
|
+
from waldiez.models import Waldiez
|
|
15
|
+
|
|
16
|
+
from .models import (
|
|
17
|
+
ConvertWorkflowRequest,
|
|
18
|
+
ConvertWorkflowResponse,
|
|
19
|
+
SaveFlowRequest,
|
|
20
|
+
SaveFlowResponse,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class FileRequestHandler:
|
|
25
|
+
"""Handles file-related requests."""
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def handle_save_flow_request(
|
|
29
|
+
msg: SaveFlowRequest,
|
|
30
|
+
workspace_dir: Path,
|
|
31
|
+
client_id: str,
|
|
32
|
+
logger: logging.Logger,
|
|
33
|
+
) -> dict[str, Any]:
|
|
34
|
+
"""Handle save flow request.
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
msg : SaveFlowRequest
|
|
39
|
+
The save flow request message.
|
|
40
|
+
workspace_dir : Path
|
|
41
|
+
The workspace directory.
|
|
42
|
+
client_id : str
|
|
43
|
+
The client ID.
|
|
44
|
+
logger : logging.Logger
|
|
45
|
+
The logger instance.
|
|
46
|
+
|
|
47
|
+
Returns
|
|
48
|
+
-------
|
|
49
|
+
dict[str, Any]
|
|
50
|
+
The response dictionary.
|
|
51
|
+
"""
|
|
52
|
+
filename = msg.filename or f"waldiez_{client_id}.waldiez"
|
|
53
|
+
try:
|
|
54
|
+
output_path = resolve_output_path(
|
|
55
|
+
filename,
|
|
56
|
+
workspace_dir=workspace_dir,
|
|
57
|
+
expected_ext="waldiez",
|
|
58
|
+
)
|
|
59
|
+
except ValueError as exc:
|
|
60
|
+
logger.error("Error resolving output path: %s", exc)
|
|
61
|
+
return SaveFlowResponse.fail(
|
|
62
|
+
error=f"Invalid output path: {exc}",
|
|
63
|
+
file_path=filename,
|
|
64
|
+
).model_dump(mode="json")
|
|
65
|
+
# pylint: disable=too-many-try-statements
|
|
66
|
+
try:
|
|
67
|
+
if output_path.exists() and not msg.force_overwrite:
|
|
68
|
+
return SaveFlowResponse.fail(
|
|
69
|
+
error=f"File exists: {output_path}",
|
|
70
|
+
file_path=str(output_path.relative_to(workspace_dir)),
|
|
71
|
+
).model_dump(mode="json")
|
|
72
|
+
|
|
73
|
+
# Parent dir already created by resolve_output_path
|
|
74
|
+
output_path.write_text(msg.flow_data, encoding="utf-8")
|
|
75
|
+
|
|
76
|
+
return SaveFlowResponse.ok(
|
|
77
|
+
file_path=str(output_path.relative_to(workspace_dir))
|
|
78
|
+
).model_dump(mode="json")
|
|
79
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
80
|
+
logger.error("Error saving flow: %s", e)
|
|
81
|
+
return SaveFlowResponse.fail(error=str(e)).model_dump(mode="json")
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def handle_convert_workflow_request(
|
|
85
|
+
msg: ConvertWorkflowRequest,
|
|
86
|
+
client_id: str,
|
|
87
|
+
workspace_dir: Path,
|
|
88
|
+
logger: logging.Logger,
|
|
89
|
+
) -> dict[str, Any]:
|
|
90
|
+
"""Handle a convert workflow request.
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
msg : ConvertWorkflowRequest
|
|
95
|
+
The convert workflow request message.
|
|
96
|
+
client_id : str
|
|
97
|
+
The client ID.
|
|
98
|
+
workspace_dir : Path
|
|
99
|
+
The workspace directory.
|
|
100
|
+
logger : logging.Logger
|
|
101
|
+
The logger instance.
|
|
102
|
+
|
|
103
|
+
Returns
|
|
104
|
+
-------
|
|
105
|
+
dict[str, Any]
|
|
106
|
+
The response dictionary.
|
|
107
|
+
"""
|
|
108
|
+
target_format = (msg.target_format or "").strip().lower()
|
|
109
|
+
if target_format not in {"py", "ipynb"}:
|
|
110
|
+
return ConvertWorkflowResponse.fail(
|
|
111
|
+
error=f"Unsupported target format: {target_format}",
|
|
112
|
+
target_format=target_format,
|
|
113
|
+
).model_dump(mode="json")
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
waldiez_data = Waldiez.from_dict(json.loads(msg.flow_data))
|
|
117
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
118
|
+
return ConvertWorkflowResponse.fail(
|
|
119
|
+
error=f"Invalid flow_data: {e}",
|
|
120
|
+
target_format=target_format,
|
|
121
|
+
).model_dump(mode="json")
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
# Use normalized target_format for default name
|
|
125
|
+
filename = msg.output_path or f"waldiez_{client_id}.{target_format}"
|
|
126
|
+
output_path = resolve_output_path(
|
|
127
|
+
filename,
|
|
128
|
+
workspace_dir=workspace_dir,
|
|
129
|
+
expected_ext=target_format,
|
|
130
|
+
)
|
|
131
|
+
except ValueError as exc:
|
|
132
|
+
logger.error("Error resolving output path: %s", exc)
|
|
133
|
+
return ConvertWorkflowResponse.fail(
|
|
134
|
+
error=f"Invalid output path: {exc}",
|
|
135
|
+
target_format=target_format,
|
|
136
|
+
).model_dump(mode="json")
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
exporter = WaldiezExporter(waldiez_data)
|
|
140
|
+
exporter.export(path=output_path, force=True, structured_io=True)
|
|
141
|
+
|
|
142
|
+
return ConvertWorkflowResponse.ok(
|
|
143
|
+
target_format=target_format,
|
|
144
|
+
output_path=str(output_path.relative_to(workspace_dir)),
|
|
145
|
+
).model_dump(mode="json")
|
|
146
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
147
|
+
logger.error("Error converting workflow: %s", e)
|
|
148
|
+
return ConvertWorkflowResponse.fail(
|
|
149
|
+
error=str(e), target_format=target_format
|
|
150
|
+
).model_dump(mode="json")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def resolve_output_path(
|
|
154
|
+
filename: str,
|
|
155
|
+
workspace_dir: Path,
|
|
156
|
+
expected_ext: str | None = None,
|
|
157
|
+
) -> Path:
|
|
158
|
+
"""
|
|
159
|
+
Resolve output path inside the workspace.
|
|
160
|
+
|
|
161
|
+
Parameters
|
|
162
|
+
----------
|
|
163
|
+
filename : str
|
|
164
|
+
Provided filename (may be relative or absolute).
|
|
165
|
+
workspace_dir : Path
|
|
166
|
+
The workspace directory to resolve the output path against.
|
|
167
|
+
expected_ext : str | None
|
|
168
|
+
If provided, ensure the filename ends with this extension.
|
|
169
|
+
|
|
170
|
+
Returns
|
|
171
|
+
-------
|
|
172
|
+
Path
|
|
173
|
+
Resolved absolute path, with parent directories created.
|
|
174
|
+
|
|
175
|
+
Raises
|
|
176
|
+
------
|
|
177
|
+
ValueError
|
|
178
|
+
If the output path is outside the workspace.
|
|
179
|
+
"""
|
|
180
|
+
# Normalize workspace_dir to an absolute path
|
|
181
|
+
workspace_dir = workspace_dir.resolve()
|
|
182
|
+
|
|
183
|
+
output_path = Path(filename)
|
|
184
|
+
if not output_path.is_absolute():
|
|
185
|
+
output_path = workspace_dir / output_path
|
|
186
|
+
|
|
187
|
+
if expected_ext and output_path.suffix != f".{expected_ext}":
|
|
188
|
+
output_path = output_path.with_suffix(f".{expected_ext}")
|
|
189
|
+
|
|
190
|
+
output_path = output_path.resolve()
|
|
191
|
+
|
|
192
|
+
# Ensure output_path is a subpath of workspace_dir
|
|
193
|
+
try:
|
|
194
|
+
output_path.relative_to(workspace_dir)
|
|
195
|
+
except ValueError as exc:
|
|
196
|
+
raise ValueError(
|
|
197
|
+
f"Output path {output_path} is outside workspace {workspace_dir}"
|
|
198
|
+
) from exc
|
|
199
|
+
|
|
200
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
201
|
+
return output_path
|