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.

Files changed (88) hide show
  1. waldiez/_version.py +1 -1
  2. waldiez/cli.py +112 -24
  3. waldiez/exporting/agent/exporter.py +3 -0
  4. waldiez/exporting/agent/extras/captain_agent_extras.py +44 -7
  5. waldiez/exporting/agent/extras/handoffs/condition.py +3 -1
  6. waldiez/exporting/chats/utils/common.py +25 -23
  7. waldiez/exporting/core/__init__.py +0 -2
  8. waldiez/exporting/core/context.py +13 -13
  9. waldiez/exporting/core/protocols.py +0 -141
  10. waldiez/exporting/core/result.py +5 -5
  11. waldiez/exporting/flow/merger.py +2 -2
  12. waldiez/exporting/flow/orchestrator.py +1 -0
  13. waldiez/exporting/flow/utils/common.py +2 -2
  14. waldiez/exporting/flow/utils/importing.py +1 -0
  15. waldiez/exporting/flow/utils/logging.py +6 -7
  16. waldiez/exporting/tools/exporter.py +5 -0
  17. waldiez/exporting/tools/factory.py +4 -0
  18. waldiez/exporting/tools/processor.py +5 -1
  19. waldiez/io/_ws.py +13 -5
  20. waldiez/io/models/content/image.py +1 -0
  21. waldiez/io/models/user_input.py +4 -4
  22. waldiez/io/models/user_response.py +1 -0
  23. waldiez/io/mqtt.py +1 -1
  24. waldiez/io/structured.py +17 -17
  25. waldiez/io/utils.py +1 -1
  26. waldiez/io/ws.py +9 -11
  27. waldiez/logger.py +180 -63
  28. waldiez/models/agents/agent/update_system_message.py +0 -2
  29. waldiez/models/agents/doc_agent/doc_agent.py +8 -1
  30. waldiez/models/common/dict_utils.py +169 -40
  31. waldiez/models/flow/flow.py +6 -6
  32. waldiez/models/flow/info.py +5 -1
  33. waldiez/models/model/_llm.py +28 -14
  34. waldiez/models/model/model.py +4 -1
  35. waldiez/models/model/model_data.py +18 -5
  36. waldiez/models/tool/predefined/_config.py +5 -1
  37. waldiez/models/tool/predefined/_duckduckgo.py +4 -0
  38. waldiez/models/tool/predefined/_email.py +474 -0
  39. waldiez/models/tool/predefined/_google.py +8 -6
  40. waldiez/models/tool/predefined/_perplexity.py +3 -0
  41. waldiez/models/tool/predefined/_searxng.py +3 -0
  42. waldiez/models/tool/predefined/_tavily.py +4 -1
  43. waldiez/models/tool/predefined/_wikipedia.py +4 -1
  44. waldiez/models/tool/predefined/_youtube.py +4 -1
  45. waldiez/models/tool/predefined/protocol.py +3 -0
  46. waldiez/models/tool/tool.py +22 -4
  47. waldiez/models/waldiez.py +12 -0
  48. waldiez/runner.py +37 -54
  49. waldiez/running/__init__.py +6 -0
  50. waldiez/running/base_runner.py +310 -353
  51. waldiez/running/environment.py +1 -0
  52. waldiez/running/exceptions.py +9 -0
  53. waldiez/running/post_run.py +4 -4
  54. waldiez/running/pre_run.py +51 -40
  55. waldiez/running/protocol.py +21 -101
  56. waldiez/running/run_results.py +1 -1
  57. waldiez/running/standard_runner.py +84 -277
  58. waldiez/running/step_by_step/__init__.py +46 -0
  59. waldiez/running/step_by_step/breakpoints_mixin.py +188 -0
  60. waldiez/running/step_by_step/step_by_step_models.py +224 -0
  61. waldiez/running/step_by_step/step_by_step_runner.py +745 -0
  62. waldiez/running/subprocess_runner/__base__.py +282 -0
  63. waldiez/running/subprocess_runner/__init__.py +16 -0
  64. waldiez/running/subprocess_runner/_async_runner.py +362 -0
  65. waldiez/running/subprocess_runner/_sync_runner.py +455 -0
  66. waldiez/running/subprocess_runner/runner.py +561 -0
  67. waldiez/running/timeline_processor.py +1 -1
  68. waldiez/running/utils.py +376 -1
  69. waldiez/utils/version.py +2 -6
  70. waldiez/ws/__init__.py +70 -0
  71. waldiez/ws/__main__.py +15 -0
  72. waldiez/ws/_file_handler.py +201 -0
  73. waldiez/ws/cli.py +211 -0
  74. waldiez/ws/client_manager.py +835 -0
  75. waldiez/ws/errors.py +416 -0
  76. waldiez/ws/models.py +971 -0
  77. waldiez/ws/reloader.py +342 -0
  78. waldiez/ws/server.py +469 -0
  79. waldiez/ws/session_manager.py +393 -0
  80. waldiez/ws/session_stats.py +83 -0
  81. waldiez/ws/utils.py +385 -0
  82. {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/METADATA +74 -74
  83. {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/RECORD +87 -65
  84. waldiez/running/patch_io_stream.py +0 -210
  85. {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/WHEEL +0 -0
  86. {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/entry_points.txt +0 -0
  87. {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/licenses/LICENSE +0 -0
  88. {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/licenses/NOTICE.md +0 -0
@@ -1,26 +1,19 @@
1
1
  # SPDX-License-Identifier: Apache-2.0.
2
2
  # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
3
 
4
- # pylint: disable=too-many-instance-attributes,unused-argument
5
- # pylint: disable=too-many-arguments,too-many-positional-arguments
6
- # pylint: disable=too-many-public-methods,too-many-locals
7
-
4
+ # pyright: reportUnknownMemberType=false, reportAttributeAccessIssue=false
5
+ # pyright: reportUnknownArgumentType=false
8
6
  """Base runner for Waldiez workflows."""
9
7
 
8
+ import importlib.util
9
+ import inspect
10
10
  import shutil
11
11
  import sys
12
12
  import tempfile
13
13
  import threading
14
14
  from pathlib import Path
15
- from types import TracebackType
16
- from typing import (
17
- TYPE_CHECKING,
18
- Any,
19
- Callable,
20
- Coroutine,
21
- Optional,
22
- Type,
23
- )
15
+ from types import ModuleType, TracebackType
16
+ from typing import TYPE_CHECKING, Any, Callable, Coroutine, Type, Union
24
17
 
25
18
  from aiofiles.os import wrap
26
19
  from anyio.from_thread import start_blocking_portal
@@ -31,22 +24,26 @@ from waldiez.logger import WaldiezLogger, get_logger
31
24
  from waldiez.models import Waldiez
32
25
 
33
26
  from .environment import refresh_environment, reset_env_vars, set_env_vars
27
+ from .exceptions import StopRunningException
34
28
  from .post_run import after_run
35
- from .pre_run import (
36
- a_install_requirements,
37
- install_requirements,
38
- )
29
+ from .pre_run import RequirementsMixin
39
30
  from .protocol import WaldiezRunnerProtocol
40
31
  from .utils import (
41
32
  a_chdir,
42
33
  chdir,
34
+ input_async,
35
+ input_sync,
36
+ is_async_callable,
37
+ syncify,
43
38
  )
44
39
 
45
40
  if TYPE_CHECKING:
46
41
  from autogen.events import BaseEvent # type: ignore[import-untyped]
42
+ from autogen.messages import BaseMessage # type: ignore[import-untyped]
47
43
 
48
44
 
49
- class WaldiezBaseRunner(WaldiezRunnerProtocol):
45
+ # pylint: disable=too-many-public-methods
46
+ class WaldiezBaseRunner(WaldiezRunnerProtocol, RequirementsMixin):
50
47
  """Base runner for Waldiez.
51
48
 
52
49
  Initialization parameters:
@@ -57,17 +54,15 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
57
54
  - skip_patch_io: Whether to skip patching I/O functions.
58
55
  - dot_env: Path to a .env file for environment variables.
59
56
 
60
- Methods to override:
57
+ Methods to possibly override:
58
+ - prepare: Prepare the environment and paths for running the flow.
61
59
  - _before_run: Actions to perform before running the flow.
60
+ - a_prepare: Async version of the prepare method.
62
61
  - _a_before_run: Async actions to perform before running the flow.
63
62
  - _run: Actual implementation of the run logic.
64
63
  - _a_run: Async implementation of the run logic.
65
64
  - _after_run: Actions to perform after running the flow.
66
65
  - _a_after_run: Async actions to perform after running the flow.
67
- - _start: Implementation of non-blocking start logic.
68
- - _a_start: Async implementation of non-blocking start logic.
69
- - _stop: Actions to perform when stopping the flow.
70
- - _a_stop: Async actions to perform when stopping the flow.
71
66
  """
72
67
 
73
68
  _structured_io: bool
@@ -76,6 +71,10 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
76
71
  _dot_env_path: str | Path | None
77
72
  _skip_patch_io: bool
78
73
  _running: bool
74
+ _is_async: bool
75
+ _input: Callable[..., str] | Callable[..., Coroutine[Any, Any, str]]
76
+ _print: Callable[..., None]
77
+ _send: Callable[[Union["BaseEvent", "BaseMessage"]], None]
79
78
 
80
79
  def __init__(
81
80
  self,
@@ -85,28 +84,32 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
85
84
  structured_io: bool,
86
85
  skip_patch_io: bool = False,
87
86
  dot_env: str | Path | None = None,
87
+ **kwargs: Any,
88
88
  ) -> None:
89
89
  """Initialize the Waldiez manager."""
90
- self._waldiez = waldiez
91
90
  WaldiezBaseRunner._running = False
92
91
  WaldiezBaseRunner._structured_io = structured_io
93
92
  WaldiezBaseRunner._output_path = output_path
94
93
  WaldiezBaseRunner._uploads_root = uploads_root
95
94
  WaldiezBaseRunner._skip_patch_io = skip_patch_io
96
95
  WaldiezBaseRunner._dot_env_path = dot_env
96
+ WaldiezBaseRunner._input = input
97
+ WaldiezBaseRunner._print = print
98
+ WaldiezBaseRunner._send = print
99
+ WaldiezBaseRunner._is_async = waldiez.is_async
100
+ self._waldiez = waldiez
97
101
  self._called_install_requirements = False
98
102
  self._exporter = WaldiezExporter(waldiez)
99
103
  self._stop_requested = threading.Event()
100
- self._logger = get_logger()
101
- self._print: Callable[..., None] = print
102
- self._send: Callable[["BaseEvent"], None] = self._print
103
- self._input: (
104
- Callable[..., str] | Callable[..., Coroutine[Any, Any, str]]
105
- ) = input
106
104
  self._last_results: list[dict[str, Any]] = []
107
105
  self._last_exception: Exception | None = None
108
- self._execution_complete_event = threading.Event()
109
- self._execution_thread: Optional[threading.Thread] = None
106
+ self._running_lock = threading.Lock()
107
+ self._loaded_module: ModuleType | None = None
108
+ logger = kwargs.get("logger")
109
+ if isinstance(logger, WaldiezLogger):
110
+ self._logger = logger
111
+ else:
112
+ self._logger = get_logger()
110
113
 
111
114
  def is_running(self) -> bool:
112
115
  """Check if the workflow is currently running.
@@ -116,65 +119,109 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
116
119
  bool
117
120
  True if the workflow is running, False otherwise.
118
121
  """
119
- return WaldiezBaseRunner._running
122
+ with self._running_lock:
123
+ return WaldiezBaseRunner._running
124
+
125
+ @staticmethod
126
+ def get_input_function() -> (
127
+ Callable[..., str] | Callable[..., Coroutine[Any, Any, str]]
128
+ ):
129
+ """Get the input function for user interaction.
120
130
 
121
- def wait_for_completion(self, timeout: Optional[float] = None) -> bool:
122
- """Wait for non-blocking execution to complete.
131
+ Returns
132
+ -------
133
+ Callable[[str, bool], str]
134
+ A function that takes a prompt and a password flag,
135
+ returning user input.
136
+ """
137
+ if hasattr(WaldiezBaseRunner, "_input") and callable(
138
+ WaldiezBaseRunner._input
139
+ ):
140
+ return WaldiezBaseRunner._input
141
+ if WaldiezBaseRunner._is_async:
142
+ return input_async
143
+ return input_sync
123
144
 
124
- This is a base implementation that subclasses can override if needed.
145
+ @staticmethod
146
+ async def a_get_user_input(prompt: str, *, password: bool = False) -> str:
147
+ """Get user input with an optional password prompt.
125
148
 
126
149
  Parameters
127
150
  ----------
128
- timeout: float | None
129
- The maximum time to wait for completion, in seconds.
130
- If None, wait indefinitely.
151
+ prompt : str
152
+ The prompt to display to the user.
153
+ password : bool, optional
154
+ If True, the input will be hidden (default is False).
131
155
 
132
156
  Returns
133
157
  -------
134
- bool
135
- True if the workflow completed successfully, False if it timed out.
158
+ str
159
+ The user input.
136
160
  """
137
- # Default implementation - subclasses can override
138
- if not self.is_running():
139
- return True
140
-
141
- if self._execution_thread and self._execution_thread.is_alive():
142
- self._execution_thread.join(timeout)
143
- return not self._execution_thread.is_alive()
161
+ input_function = WaldiezBaseRunner.get_input_function()
162
+ if is_async_callable(input_function):
163
+ try:
164
+ result = await input_function( # type: ignore
165
+ prompt,
166
+ password=password,
167
+ )
168
+ except TypeError:
169
+ result = await input_function(prompt) # type: ignore
170
+ else:
171
+ try:
172
+ result = input_function(prompt, password=password)
173
+ except TypeError:
174
+ result = input_function(prompt)
175
+ return result # pyright: ignore
144
176
 
145
- return self._execution_complete_event.wait(timeout or 0)
177
+ @staticmethod
178
+ def get_user_input(
179
+ prompt: str,
180
+ *,
181
+ password: bool = False,
182
+ ) -> str:
183
+ """Get user input with an optional password prompt.
146
184
 
147
- def get_execution_stats(self) -> dict[str, Any]:
148
- """Get basic execution statistics.
185
+ Parameters
186
+ ----------
187
+ prompt : str
188
+ The prompt to display to the user.
189
+ password : bool, optional
190
+ If True, the input will be hidden (default is False).
149
191
 
150
192
  Returns
151
193
  -------
152
- dict[str, Any]
153
- A dictionary containing execution statistics.
154
- - is_running: Whether the runner is currently running.
155
- - has_thread: Whether there is an execution thread.
156
- - thread_alive: Whether the execution thread is alive.
157
- - has_error: Whether there was an error during execution.
194
+ str
195
+ The user input.
158
196
  """
159
- return {
160
- "is_running": self.is_running(),
161
- "has_thread": self._execution_thread is not None,
162
- "thread_alive": (
163
- self._execution_thread.is_alive()
164
- if self._execution_thread
165
- else False
166
- ),
167
- "has_error": self._last_exception is not None,
168
- }
169
-
170
- # Helper for subclasses
171
- def _signal_completion(self) -> None:
172
- """Signal that execution has completed."""
173
- self._execution_complete_event.set()
174
-
175
- def _reset_completion_state(self) -> None:
176
- """Reset completion state for new execution."""
177
- self._execution_complete_event.clear()
197
+ input_function = WaldiezBaseRunner.get_input_function()
198
+ if inspect.iscoroutinefunction(input_function):
199
+ try:
200
+ return syncify(input_function)(prompt, password=password)
201
+ except TypeError:
202
+ return syncify(input_function)(prompt)
203
+ try:
204
+ return str(input_function(prompt, password=password))
205
+ except TypeError:
206
+ return str(input_function(prompt))
207
+
208
+ def _load_module(self, output_file: Path, temp_dir: Path) -> ModuleType:
209
+ """Load the module from the waldiez file."""
210
+ file_name = output_file.name
211
+ module_name = file_name.replace(".py", "")
212
+ spec = importlib.util.spec_from_file_location(
213
+ module_name, temp_dir / file_name
214
+ )
215
+ if not spec or not spec.loader:
216
+ raise ImportError("Could not import the flow")
217
+ module = importlib.util.module_from_spec(spec)
218
+ spec.loader.exec_module(module)
219
+ if not hasattr(module, "main"):
220
+ raise ImportError(
221
+ "The waldiez file does not contain a main() function"
222
+ )
223
+ self._loaded_module = module
224
+ return module
178
225
 
179
226
  def _before_run(
180
227
  self,
@@ -250,44 +297,6 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
250
297
  "The _a_run method must be implemented in the subclass."
251
298
  )
252
299
 
253
- def _start(
254
- self,
255
- temp_dir: Path,
256
- output_file: Path,
257
- uploads_root: Path | None,
258
- skip_mmd: bool,
259
- skip_timeline: bool,
260
- ) -> None:
261
- """Start running the Waldiez flow in a non-blocking way."""
262
- raise NotImplementedError(
263
- "The _start method must be implemented in the subclass."
264
- )
265
-
266
- async def _a_start(
267
- self,
268
- temp_dir: Path,
269
- output_file: Path,
270
- uploads_root: Path | None,
271
- skip_mmd: bool,
272
- skip_timeline: bool,
273
- ) -> None:
274
- """Start running the Waldiez flow in a non-blocking way asynchronously.
275
-
276
- Parameters
277
- ----------
278
- temp_dir : Path
279
- The path to the temporary directory created for the run.
280
- output_file : Path
281
- The path to the output file.
282
- uploads_root : Path | None
283
- The root path for uploads, if any.
284
- skip_mmd : bool
285
- Whether to skip generating the mermaid diagram.
286
- """
287
- raise NotImplementedError(
288
- "The _a_start method must be implemented in the subclass."
289
- )
290
-
291
300
  def _after_run(
292
301
  self,
293
302
  results: list[dict[str, Any]],
@@ -303,14 +312,18 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
303
312
 
304
313
  # Reset stop flag for next run
305
314
  self._stop_requested.clear()
306
- after_run(
307
- temp_dir=temp_dir,
308
- output_file=output_file,
309
- flow_name=self.waldiez.name,
310
- uploads_root=uploads_root,
311
- skip_mmd=skip_mmd,
312
- skip_timeline=skip_timeline,
313
- )
315
+ # pylint: disable=broad-exception-caught
316
+ try:
317
+ after_run(
318
+ temp_dir=temp_dir,
319
+ output_file=output_file,
320
+ flow_name=self._waldiez.name,
321
+ uploads_root=uploads_root,
322
+ skip_mmd=skip_mmd,
323
+ skip_timeline=skip_timeline,
324
+ )
325
+ except BaseException as exc: # pragma: no cover
326
+ self.log.warning("Error occurred during after_run: %s", exc)
314
327
  self.log.info("Cleanup completed")
315
328
 
316
329
  async def _a_after_run(
@@ -332,21 +345,6 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
332
345
  skip_timeline=skip_timeline,
333
346
  )
334
347
 
335
- def _stop(self) -> None:
336
- """Actions to perform when stopping the flow."""
337
- raise NotImplementedError(
338
- "The _stop method must be implemented in the subclass."
339
- )
340
-
341
- async def _a_stop(self) -> None:
342
- """Asynchronously perform actions when stopping the flow."""
343
- raise NotImplementedError(
344
- "The _a_stop method must be implemented in the subclass."
345
- )
346
-
347
- # ===================================================================
348
- # HELPER METHODS
349
- # ===================================================================
350
348
  @staticmethod
351
349
  def _prepare_paths(
352
350
  output_path: str | Path | None = None,
@@ -366,43 +364,57 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
366
364
  output_file: Path = Path(WaldiezBaseRunner._output_path)
367
365
  return output_file, uploads_root_path
368
366
 
369
- def gather_requirements(self) -> set[str]:
370
- """Gather extra requirements to install before running the flow.
367
+ @staticmethod
368
+ async def a_process_event(event: Union["BaseEvent", "BaseMessage"]) -> None:
369
+ """Process an event or message asynchronously.
371
370
 
372
- Returns
373
- -------
374
- set[str]
375
- A set of requirements that are not already installed and do not
376
- include 'waldiez' in their name.
371
+ Parameters
372
+ ----------
373
+ event : Union[BaseEvent, BaseMessage]
374
+ The event or message to process.
375
+ """
376
+ if hasattr(event, "type"): # pragma: no branch
377
+ if event.type == "input_request":
378
+ prompt = getattr(
379
+ event, "prompt", getattr(event.content, "prompt", "> ")
380
+ )
381
+ password = getattr(
382
+ event,
383
+ "password",
384
+ getattr(event.content, "password", False),
385
+ )
386
+ user_input = await WaldiezBaseRunner.a_get_user_input(
387
+ prompt, password=password
388
+ )
389
+ await event.content.respond(user_input)
390
+ else:
391
+ WaldiezBaseRunner._send(event)
392
+
393
+ @staticmethod
394
+ def process_event(event: Union["BaseEvent", "BaseMessage"]) -> None:
395
+ """Process an event or message synchronously.
396
+
397
+ Parameters
398
+ ----------
399
+ event : Union[BaseEvent, BaseMessage]
400
+ The event or message to process.
377
401
  """
378
- extra_requirements = {
379
- req
380
- for req in self.waldiez.requirements
381
- if req not in sys.modules and "waldiez" not in req
382
- }
383
- if "python-dotenv" not in extra_requirements:
384
- extra_requirements.add("python-dotenv")
385
- return extra_requirements
386
-
387
- def install_requirements(self) -> None:
388
- """Install the requirements for the flow."""
389
- if not self._called_install_requirements:
390
- self._called_install_requirements = True
391
- extra_requirements = self.gather_requirements()
392
- if extra_requirements:
393
- install_requirements(extra_requirements)
394
-
395
- async def a_install_requirements(self) -> None:
396
- """Install the requirements for the flow asynchronously."""
397
- if not self._called_install_requirements:
398
- self._called_install_requirements = True
399
- extra_requirements = self.gather_requirements()
400
- if extra_requirements:
401
- await a_install_requirements(extra_requirements)
402
-
403
- # ===================================================================
404
- # PUBLIC PROTOCOL IMPLEMENTATION
405
- # ===================================================================
402
+ if hasattr(event, "type"): # pragma: no branch
403
+ if event.type == "input_request":
404
+ prompt = getattr(
405
+ event, "prompt", getattr(event.content, "prompt", "> ")
406
+ )
407
+ password = getattr(
408
+ event,
409
+ "password",
410
+ getattr(event.content, "password", False),
411
+ )
412
+ user_input = WaldiezBaseRunner.get_user_input(
413
+ prompt, password=password
414
+ )
415
+ event.content.respond(user_input)
416
+ else:
417
+ WaldiezBaseRunner._send(event)
406
418
 
407
419
  def before_run(
408
420
  self,
@@ -452,6 +464,42 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
452
464
  uploads_root=uploads_root,
453
465
  )
454
466
 
467
+ def prepare(
468
+ self,
469
+ output_path: str | Path | None,
470
+ uploads_root: str | Path | None,
471
+ ) -> tuple[Path, Path, Path | None]:
472
+ """Prepare the paths and environment for running the flow.
473
+
474
+ Parameters
475
+ ----------
476
+ output_path : str | Path | None
477
+ The output path for the flow, by default None.
478
+ uploads_root : str | Path | None
479
+ The root path for uploads, by default None.
480
+
481
+ Returns
482
+ -------
483
+ tuple[Path, Path, Path | None]
484
+ A tuple containing:
485
+ - The path to the output file.
486
+ - The path to the temporary directory created for the run.
487
+ - The root path for uploads, if specified, otherwise None.
488
+ """
489
+ output_file, uploads_root_path = self._prepare_paths(
490
+ output_path=output_path,
491
+ uploads_root=uploads_root,
492
+ )
493
+ temp_dir = self.before_run(
494
+ output_file=output_file,
495
+ uploads_root=uploads_root_path,
496
+ )
497
+ self.install_requirements()
498
+ refresh_environment()
499
+ return temp_dir, output_file, uploads_root_path
500
+
501
+ # noinspection PyProtocol
502
+ # pylint: disable=too-many-locals,unused-argument
455
503
  def run(
456
504
  self,
457
505
  output_path: str | Path | None = None,
@@ -462,7 +510,7 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
462
510
  dot_env: str | Path | None = None,
463
511
  **kwargs: Any,
464
512
  ) -> list[dict[str, Any]]:
465
- """Run the Waldiez flow in blocking mode.
513
+ """Run the Waldiez flow.
466
514
 
467
515
  Parameters
468
516
  ----------
@@ -471,10 +519,9 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
471
519
  uploads_root : str | Path | None
472
520
  The runtime uploads root, by default None.
473
521
  structured_io : bool
474
- Whether to use structured IO instead of the default 'input/print',
475
- by default False.
522
+ Whether to use structured IO instead of the default 'input/print'.
476
523
  skip_mmd : bool
477
- Whether to skip generating the mermaid diagram, by default False.
524
+ Whether to skip generating the mermaid diagram.
478
525
  skip_timeline : bool
479
526
  Whether to skip generating the timeline JSON.
480
527
  dot_env : str | Path | None
@@ -485,12 +532,15 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
485
532
  Returns
486
533
  -------
487
534
  list[dict[str, Any]]
488
- The result of the run.
535
+ The results of the run.
489
536
 
490
537
  Raises
491
538
  ------
492
539
  RuntimeError
493
- If the runner is already running.
540
+ If the runner is already running, the workflow is not async,
541
+ or an error occurs during the run.
542
+ StopRunningException
543
+ If the run is stopped by the user.
494
544
  """
495
545
  if dot_env is not None:
496
546
  resolved = Path(dot_env).resolve()
@@ -500,7 +550,7 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
500
550
  WaldiezBaseRunner._structured_io = structured_io
501
551
  if self.is_running():
502
552
  raise RuntimeError("Workflow already running")
503
- if self.waldiez.is_async:
553
+ if self.is_async:
504
554
  with start_blocking_portal(backend="asyncio") as portal:
505
555
  return portal.call(
506
556
  self.a_run,
@@ -509,19 +559,13 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
509
559
  structured_io,
510
560
  skip_mmd,
511
561
  )
512
- output_file, uploads_root_path = self._prepare_paths(
562
+ temp_dir, output_file, uploads_root_path = self.prepare(
513
563
  output_path=output_path,
514
564
  uploads_root=uploads_root,
515
565
  )
516
- temp_dir = self.before_run(
517
- output_file=output_file,
518
- uploads_root=uploads_root_path,
519
- )
520
- self.install_requirements()
521
- refresh_environment()
522
566
  WaldiezBaseRunner._running = True
523
- results: list[dict[str, Any]] = []
524
- old_env_vars = set_env_vars(self.waldiez.get_flow_env_vars())
567
+ results: list[dict[str, Any]]
568
+ old_env_vars = set_env_vars(self._waldiez.get_flow_env_vars())
525
569
  try:
526
570
  with chdir(to=temp_dir):
527
571
  sys.path.insert(0, str(temp_dir))
@@ -532,6 +576,11 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
532
576
  skip_mmd=skip_mmd,
533
577
  skip_timeline=skip_timeline,
534
578
  )
579
+ except (SystemExit, StopRunningException, KeyboardInterrupt) as exc:
580
+ raise StopRunningException(StopRunningException.reason) from exc
581
+ except BaseException as exc: # pylint: disable=broad-exception-caught
582
+ self.log.error("Error occurred while running workflow: %s", exc)
583
+ results = [{"error": str(exc)}]
535
584
  finally:
536
585
  WaldiezBaseRunner._running = False
537
586
  reset_env_vars(old_env_vars)
@@ -548,7 +597,39 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
548
597
  sys.path.pop(0)
549
598
  return results
550
599
 
600
+ async def a_prepare(
601
+ self,
602
+ output_path: str | Path | None,
603
+ uploads_root: str | Path | None,
604
+ ) -> tuple[Path, Path, Path | None]:
605
+ """Prepare the paths for the async run.
606
+
607
+ Parameters
608
+ ----------
609
+ output_path : str | Path | None
610
+ The output path, by default None.
611
+ uploads_root : str | Path | None
612
+ The uploads root path, by default None.
613
+
614
+ Returns
615
+ -------
616
+ tuple[Path, Path, Path | None]
617
+ The temporary directory, output file, and uploads root path.
618
+ """
619
+ output_file, uploads_root_path = self._prepare_paths(
620
+ output_path=output_path,
621
+ uploads_root=uploads_root,
622
+ )
623
+ temp_dir = await self._a_before_run(
624
+ output_file=output_file,
625
+ uploads_root=uploads_root_path,
626
+ )
627
+ await self.a_install_requirements()
628
+ refresh_environment()
629
+ return temp_dir, output_file, uploads_root_path
630
+
551
631
  # noinspection DuplicatedCode
632
+ # noinspection PyProtocol
552
633
  async def a_run(
553
634
  self,
554
635
  output_path: str | Path | None = None,
@@ -566,28 +647,30 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
566
647
  output_path : str | Path | None
567
648
  The output path, by default None.
568
649
  uploads_root : str | Path | None
569
- The runtime uploads root, by default None.
650
+ The runtime uploads root.
570
651
  structured_io : bool
571
- Whether to use structured IO instead of the default 'input/print',
572
- by default False.
652
+ Whether to use structured IO instead of the default 'input/print'.
573
653
  skip_mmd : bool
574
- Whether to skip generating the mermaid diagram, by default False.
654
+ Whether to skip generating the mermaid diagram.
575
655
  skip_timeline : bool
576
- Whether to skip generating the timeline JSON, by default False.
656
+ Whether to skip generating the timeline JSON.
577
657
  dot_env : str | Path | None
578
658
  The path to the .env file, if any.
579
659
  **kwargs : Any
580
- Additional keyword arguments for the run method.
660
+ Additional keyword arguments for the a_run method.
581
661
 
582
662
  Returns
583
663
  -------
584
664
  list[dict[str, Any]]
585
- The result of the run.
665
+ The results of the run.
586
666
 
587
667
  Raises
588
668
  ------
589
669
  RuntimeError
590
- If the runner is already running.
670
+ If the runner is already running, the workflow is not async
671
+ or an error occurs during the run.
672
+ StopRunningException
673
+ If the run is stopped by the user.
591
674
  """
592
675
  if dot_env is not None:
593
676
  resolved = Path(dot_env).resolve()
@@ -597,19 +680,13 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
597
680
  WaldiezBaseRunner._structured_io = structured_io
598
681
  if self.is_running():
599
682
  raise RuntimeError("Workflow already running")
600
- output_file, uploads_root_path = self._prepare_paths(
683
+ temp_dir, output_file, uploads_root_path = await self.a_prepare(
601
684
  output_path=output_path,
602
685
  uploads_root=uploads_root,
603
686
  )
604
- temp_dir = await self._a_before_run(
605
- output_file=output_file,
606
- uploads_root=uploads_root_path,
607
- )
608
- await self.a_install_requirements()
609
- refresh_environment()
610
687
  WaldiezBaseRunner._running = True
611
- results: list[dict[str, Any]] = []
612
- old_env_vars = set_env_vars(self.waldiez.get_flow_env_vars())
688
+ results: list[dict[str, Any]]
689
+ old_env_vars = set_env_vars(self._waldiez.get_flow_env_vars())
613
690
  try:
614
691
  async with a_chdir(to=temp_dir):
615
692
  sys.path.insert(0, str(temp_dir))
@@ -620,6 +697,10 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
620
697
  skip_mmd=skip_mmd,
621
698
  skip_timeline=skip_timeline,
622
699
  )
700
+ except (SystemExit, StopRunningException, KeyboardInterrupt) as exc:
701
+ raise StopRunningException(StopRunningException.reason) from exc
702
+ except BaseException as exc: # pylint: disable=broad-exception-caught
703
+ results = [{"error": str(exc)}]
623
704
  finally:
624
705
  WaldiezBaseRunner._running = False
625
706
  reset_env_vars(old_env_vars)
@@ -635,129 +716,6 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
635
716
  sys.path.pop(0)
636
717
  return results
637
718
 
638
- def start(
639
- self,
640
- output_path: str | Path | None,
641
- uploads_root: str | Path | None,
642
- structured_io: bool | None = None,
643
- skip_mmd: bool = False,
644
- skip_timeline: bool = False,
645
- dot_env: str | Path | None = None,
646
- **kwargs: Any,
647
- ) -> None:
648
- """Start running the Waldiez flow in a non-blocking way.
649
-
650
- Parameters
651
- ----------
652
- output_path : str | Path | None
653
- The output path.
654
- uploads_root : str | Path | None
655
- The runtime uploads root.
656
- structured_io : bool | None
657
- Whether to use structured IO instead of the default 'input/print'.
658
- skip_mmd : bool
659
- Whether to skip generating the mermaid diagram, by default False.
660
- skip_timeline : bool
661
- Whether to skip generating the timeline JSON, by default False.
662
- dot_env : str | Path | None
663
- The path to the .env file, if any.
664
- **kwargs : Any
665
- Additional keyword arguments for the start method.
666
-
667
- Raises
668
- ------
669
- RuntimeError
670
- If the runner is already running.
671
- """
672
- if dot_env is not None:
673
- resolved = Path(dot_env).resolve()
674
- if resolved.is_file():
675
- WaldiezBaseRunner._dot_env_path = resolved
676
- if structured_io is not None:
677
- WaldiezBaseRunner._structured_io = structured_io
678
- if self.is_running():
679
- raise RuntimeError("Workflow already running")
680
- output_file, uploads_root_path = self._prepare_paths(
681
- output_path=output_path,
682
- uploads_root=uploads_root,
683
- )
684
- temp_dir = self.before_run(
685
- output_file=output_file,
686
- uploads_root=uploads_root_path,
687
- )
688
- self.install_requirements()
689
- refresh_environment()
690
- WaldiezBaseRunner._running = True
691
- self._start(
692
- temp_dir=temp_dir,
693
- output_file=output_file,
694
- uploads_root=uploads_root_path,
695
- skip_mmd=skip_mmd,
696
- skip_timeline=skip_timeline,
697
- )
698
-
699
- # noinspection DuplicatedCode
700
- async def a_start(
701
- self,
702
- output_path: str | Path | None,
703
- uploads_root: str | Path | None,
704
- structured_io: bool | None = None,
705
- skip_mmd: bool = False,
706
- skip_timeline: bool = False,
707
- dot_env: str | Path | None = None,
708
- **kwargs: Any,
709
- ) -> None:
710
- """Asynchronously start running the Waldiez flow in a non-blocking way.
711
-
712
- Parameters
713
- ----------
714
- output_path : str | Path | None
715
- The output path.
716
- uploads_root : str | Path | None
717
- The runtime uploads root.
718
- structured_io : bool | None = None
719
- Whether to use structured IO instead of the default 'input/print'.
720
- skip_mmd : bool = False
721
- Whether to skip generating the mermaid diagram, by default False.
722
- skip_timeline : bool = False
723
- Whether to skip generating the timeline JSON, by default False.
724
- dot_env : str | Path | None = None
725
- The path to the .env file, if any.
726
- **kwargs : Any
727
- Additional keyword arguments for the start method.
728
-
729
- Raises
730
- ------
731
- RuntimeError
732
- If the runner is already running.
733
- """
734
- if dot_env is not None:
735
- resolved = Path(dot_env).resolve()
736
- if resolved.is_file():
737
- WaldiezBaseRunner._dot_env_path = resolved
738
- if structured_io is not None:
739
- WaldiezBaseRunner._structured_io = structured_io
740
- if self.is_running():
741
- raise RuntimeError("Workflow already running")
742
- output_file, uploads_root_path = self._prepare_paths(
743
- output_path=output_path,
744
- uploads_root=uploads_root,
745
- )
746
- temp_dir = await self._a_before_run(
747
- output_file=output_file,
748
- uploads_root=uploads_root_path,
749
- )
750
- await self.a_install_requirements()
751
- refresh_environment()
752
- WaldiezBaseRunner._running = True
753
- await self._a_start(
754
- temp_dir=temp_dir,
755
- output_file=output_file,
756
- uploads_root=uploads_root_path,
757
- skip_mmd=skip_mmd,
758
- skip_timeline=skip_timeline,
759
- )
760
-
761
719
  def after_run(
762
720
  self,
763
721
  results: list[dict[str, Any]],
@@ -828,28 +786,6 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
828
786
  skip_timeline=skip_timeline,
829
787
  )
830
788
 
831
- def stop(self) -> None:
832
- """Stop the runner if it is running."""
833
- if not self.is_running():
834
- return
835
- try:
836
- self._stop()
837
- finally:
838
- WaldiezBaseRunner._running = False
839
-
840
- async def a_stop(self) -> None:
841
- """Asynchronously stop the runner if it is running."""
842
- if not self.is_running():
843
- return
844
- try:
845
- await self._a_stop()
846
- finally:
847
- WaldiezBaseRunner._running = False
848
-
849
- # ===================================================================
850
- # PROPERTIES AND CONTEXT MANAGERS
851
- # ===================================================================
852
-
853
789
  @property
854
790
  def waldiez(self) -> Waldiez:
855
791
  """Get the Waldiez instance."""
@@ -858,7 +794,7 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
858
794
  @property
859
795
  def is_async(self) -> bool:
860
796
  """Check if the workflow is async."""
861
- return self.waldiez.is_async
797
+ return self._waldiez.is_async
862
798
 
863
799
  @property
864
800
  def running(self) -> bool:
@@ -952,6 +888,27 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
952
888
  dot_env=dot_env,
953
889
  )
954
890
 
891
+ def stop(self) -> None:
892
+ """Stop the workflow execution.
893
+
894
+ This method sets the stop flag that will be checked by the event
895
+ handlers and other parts of the workflow execution to gracefully
896
+ terminate the workflow execution.
897
+ Note: Stopping will occur at the "next" AutoGen event, not immediately.
898
+ """
899
+ self.log.info("Stop requested - setting stop flag")
900
+ self._stop_requested.set()
901
+
902
+ def is_stop_requested(self) -> bool:
903
+ """Check if a stop has been requested.
904
+
905
+ Returns
906
+ -------
907
+ bool
908
+ True if stop has been requested, False otherwise.
909
+ """
910
+ return self._stop_requested.is_set()
911
+
955
912
  def __enter__(self) -> Self:
956
913
  """Enter the context manager."""
957
914
  return self
@@ -968,7 +925,7 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
968
925
  ) -> None:
969
926
  """Exit the context manager."""
970
927
  if self.is_running():
971
- self.stop()
928
+ self._stop_requested.set()
972
929
 
973
930
  async def __aexit__(
974
931
  self,
@@ -978,4 +935,4 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
978
935
  ) -> None:
979
936
  """Exit the context manager asynchronously."""
980
937
  if self.is_running():
981
- await self.a_stop()
938
+ self._stop_requested.set()