waldiez 0.5.9__py3-none-any.whl → 0.6.0__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 (109) hide show
  1. waldiez/_version.py +1 -1
  2. waldiez/cli.py +113 -24
  3. waldiez/exporting/agent/exporter.py +9 -6
  4. waldiez/exporting/agent/extras/captain_agent_extras.py +44 -7
  5. waldiez/exporting/agent/extras/group_manager_agent_extas.py +6 -1
  6. waldiez/exporting/agent/extras/handoffs/after_work.py +1 -0
  7. waldiez/exporting/agent/extras/handoffs/available.py +1 -0
  8. waldiez/exporting/agent/extras/handoffs/condition.py +3 -1
  9. waldiez/exporting/agent/extras/handoffs/handoff.py +1 -0
  10. waldiez/exporting/agent/extras/handoffs/target.py +1 -0
  11. waldiez/exporting/agent/termination.py +1 -0
  12. waldiez/exporting/chats/utils/common.py +25 -23
  13. waldiez/exporting/core/__init__.py +0 -2
  14. waldiez/exporting/core/constants.py +3 -1
  15. waldiez/exporting/core/context.py +13 -13
  16. waldiez/exporting/core/extras/serializer.py +12 -10
  17. waldiez/exporting/core/protocols.py +0 -141
  18. waldiez/exporting/core/result.py +5 -5
  19. waldiez/exporting/core/types.py +1 -0
  20. waldiez/exporting/core/utils/llm_config.py +2 -2
  21. waldiez/exporting/flow/execution_generator.py +1 -0
  22. waldiez/exporting/flow/merger.py +2 -2
  23. waldiez/exporting/flow/orchestrator.py +1 -0
  24. waldiez/exporting/flow/utils/common.py +3 -3
  25. waldiez/exporting/flow/utils/importing.py +1 -0
  26. waldiez/exporting/flow/utils/logging.py +7 -80
  27. waldiez/exporting/tools/exporter.py +5 -0
  28. waldiez/exporting/tools/factory.py +4 -0
  29. waldiez/exporting/tools/processor.py +5 -1
  30. waldiez/io/__init__.py +3 -1
  31. waldiez/io/_ws.py +15 -5
  32. waldiez/io/models/content/image.py +1 -0
  33. waldiez/io/models/user_input.py +4 -4
  34. waldiez/io/models/user_response.py +1 -0
  35. waldiez/io/mqtt.py +1 -1
  36. waldiez/io/structured.py +98 -45
  37. waldiez/io/utils.py +17 -11
  38. waldiez/io/ws.py +10 -12
  39. waldiez/logger.py +180 -63
  40. waldiez/models/agents/agent/agent.py +2 -1
  41. waldiez/models/agents/agent/update_system_message.py +0 -2
  42. waldiez/models/agents/doc_agent/doc_agent.py +8 -1
  43. waldiez/models/chat/chat.py +1 -0
  44. waldiez/models/chat/chat_data.py +0 -2
  45. waldiez/models/common/base.py +2 -0
  46. waldiez/models/common/dict_utils.py +169 -40
  47. waldiez/models/common/handoff.py +2 -0
  48. waldiez/models/common/method_utils.py +2 -0
  49. waldiez/models/flow/flow.py +6 -6
  50. waldiez/models/flow/info.py +5 -1
  51. waldiez/models/model/_llm.py +31 -14
  52. waldiez/models/model/model.py +4 -1
  53. waldiez/models/model/model_data.py +18 -5
  54. waldiez/models/tool/predefined/_config.py +5 -1
  55. waldiez/models/tool/predefined/_duckduckgo.py +4 -0
  56. waldiez/models/tool/predefined/_email.py +477 -0
  57. waldiez/models/tool/predefined/_google.py +4 -1
  58. waldiez/models/tool/predefined/_perplexity.py +4 -1
  59. waldiez/models/tool/predefined/_searxng.py +4 -1
  60. waldiez/models/tool/predefined/_tavily.py +4 -1
  61. waldiez/models/tool/predefined/_wikipedia.py +5 -2
  62. waldiez/models/tool/predefined/_youtube.py +4 -1
  63. waldiez/models/tool/predefined/protocol.py +3 -0
  64. waldiez/models/tool/tool.py +22 -4
  65. waldiez/models/waldiez.py +12 -0
  66. waldiez/runner.py +37 -54
  67. waldiez/running/__init__.py +6 -0
  68. waldiez/running/base_runner.py +381 -363
  69. waldiez/running/environment.py +1 -0
  70. waldiez/running/exceptions.py +9 -0
  71. waldiez/running/post_run.py +10 -4
  72. waldiez/running/pre_run.py +199 -66
  73. waldiez/running/protocol.py +21 -101
  74. waldiez/running/run_results.py +1 -1
  75. waldiez/running/standard_runner.py +83 -276
  76. waldiez/running/step_by_step/__init__.py +46 -0
  77. waldiez/running/step_by_step/breakpoints_mixin.py +512 -0
  78. waldiez/running/step_by_step/command_handler.py +151 -0
  79. waldiez/running/step_by_step/events_processor.py +199 -0
  80. waldiez/running/step_by_step/step_by_step_models.py +541 -0
  81. waldiez/running/step_by_step/step_by_step_runner.py +750 -0
  82. waldiez/running/subprocess_runner/__base__.py +279 -0
  83. waldiez/running/subprocess_runner/__init__.py +16 -0
  84. waldiez/running/subprocess_runner/_async_runner.py +362 -0
  85. waldiez/running/subprocess_runner/_sync_runner.py +456 -0
  86. waldiez/running/subprocess_runner/runner.py +570 -0
  87. waldiez/running/timeline_processor.py +1 -1
  88. waldiez/running/utils.py +492 -3
  89. waldiez/utils/version.py +2 -6
  90. waldiez/ws/__init__.py +71 -0
  91. waldiez/ws/__main__.py +15 -0
  92. waldiez/ws/_file_handler.py +199 -0
  93. waldiez/ws/_mock.py +74 -0
  94. waldiez/ws/cli.py +235 -0
  95. waldiez/ws/client_manager.py +851 -0
  96. waldiez/ws/errors.py +416 -0
  97. waldiez/ws/models.py +988 -0
  98. waldiez/ws/reloader.py +363 -0
  99. waldiez/ws/server.py +508 -0
  100. waldiez/ws/session_manager.py +393 -0
  101. waldiez/ws/session_stats.py +83 -0
  102. waldiez/ws/utils.py +410 -0
  103. {waldiez-0.5.9.dist-info → waldiez-0.6.0.dist-info}/METADATA +105 -96
  104. {waldiez-0.5.9.dist-info → waldiez-0.6.0.dist-info}/RECORD +108 -83
  105. waldiez/running/patch_io_stream.py +0 -210
  106. {waldiez-0.5.9.dist-info → waldiez-0.6.0.dist-info}/WHEEL +0 -0
  107. {waldiez-0.5.9.dist-info → waldiez-0.6.0.dist-info}/entry_points.txt +0 -0
  108. {waldiez-0.5.9.dist-info → waldiez-0.6.0.dist-info}/licenses/LICENSE +0 -0
  109. {waldiez-0.5.9.dist-info → waldiez-0.6.0.dist-info}/licenses/NOTICE.md +0 -0
@@ -1,26 +1,20 @@
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
+ import json
10
11
  import shutil
11
12
  import sys
12
13
  import tempfile
13
14
  import threading
14
15
  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
- )
16
+ from types import ModuleType, TracebackType
17
+ from typing import TYPE_CHECKING, Any, Callable, Coroutine, Type, Union
24
18
 
25
19
  from aiofiles.os import wrap
26
20
  from anyio.from_thread import start_blocking_portal
@@ -31,22 +25,31 @@ from waldiez.logger import WaldiezLogger, get_logger
31
25
  from waldiez.models import Waldiez
32
26
 
33
27
  from .environment import refresh_environment, reset_env_vars, set_env_vars
28
+ from .exceptions import StopRunningException
34
29
  from .post_run import after_run
35
- from .pre_run import (
36
- a_install_requirements,
37
- install_requirements,
38
- )
30
+ from .pre_run import RequirementsMixin, dump_waldiez
39
31
  from .protocol import WaldiezRunnerProtocol
40
32
  from .utils import (
41
33
  a_chdir,
42
34
  chdir,
35
+ input_async,
36
+ input_sync,
37
+ is_async_callable,
38
+ syncify,
43
39
  )
44
40
 
45
41
  if TYPE_CHECKING:
46
- from autogen.events import BaseEvent # type: ignore[import-untyped]
42
+ # pylint: disable=line-too-long
43
+ from autogen.events import ( # type: ignore[import-untyped,import-not-found,unused-ignore] # noqa: E501
44
+ BaseEvent,
45
+ )
46
+ from autogen.messages import ( # type: ignore[import-untyped,import-not-found,unused-ignore] # noqa: E501
47
+ BaseMessage,
48
+ )
47
49
 
48
50
 
49
- class WaldiezBaseRunner(WaldiezRunnerProtocol):
51
+ # pylint: disable=too-many-public-methods
52
+ class WaldiezBaseRunner(WaldiezRunnerProtocol, RequirementsMixin):
50
53
  """Base runner for Waldiez.
51
54
 
52
55
  Initialization parameters:
@@ -54,28 +57,29 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
54
57
  - output_path: Path to save the output file.
55
58
  - uploads_root: Root directory for uploads.
56
59
  - structured_io: Whether to use structured I/O.
57
- - skip_patch_io: Whether to skip patching I/O functions.
58
60
  - dot_env: Path to a .env file for environment variables.
59
61
 
60
- Methods to override:
62
+ Methods to possibly override:
63
+ - prepare: Prepare the environment and paths for running the flow.
61
64
  - _before_run: Actions to perform before running the flow.
65
+ - a_prepare: Async version of the prepare method.
62
66
  - _a_before_run: Async actions to perform before running the flow.
63
67
  - _run: Actual implementation of the run logic.
64
68
  - _a_run: Async implementation of the run logic.
65
69
  - _after_run: Actions to perform after running the flow.
66
70
  - _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
71
  """
72
72
 
73
73
  _structured_io: bool
74
74
  _output_path: str | Path | None
75
75
  _uploads_root: str | Path | None
76
76
  _dot_env_path: str | Path | None
77
- _skip_patch_io: bool
78
77
  _running: bool
78
+ _is_async: bool
79
+ _waldiez_file: Path
80
+ _input: Callable[..., str] | Callable[..., Coroutine[Any, Any, str]]
81
+ _print: Callable[..., None]
82
+ _send: Callable[[Union["BaseEvent", "BaseMessage"]], None]
79
83
 
80
84
  def __init__(
81
85
  self,
@@ -83,30 +87,59 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
83
87
  output_path: str | Path | None,
84
88
  uploads_root: str | Path | None,
85
89
  structured_io: bool,
86
- skip_patch_io: bool = False,
87
90
  dot_env: str | Path | None = None,
91
+ **kwargs: Any,
88
92
  ) -> None:
89
93
  """Initialize the Waldiez manager."""
90
- self._waldiez = waldiez
91
94
  WaldiezBaseRunner._running = False
92
95
  WaldiezBaseRunner._structured_io = structured_io
93
96
  WaldiezBaseRunner._output_path = output_path
94
97
  WaldiezBaseRunner._uploads_root = uploads_root
95
- WaldiezBaseRunner._skip_patch_io = skip_patch_io
96
98
  WaldiezBaseRunner._dot_env_path = dot_env
99
+ WaldiezBaseRunner._input = input
100
+ WaldiezBaseRunner._print = print
101
+ WaldiezBaseRunner._send = print
102
+ WaldiezBaseRunner._is_async = waldiez.is_async
103
+ self._waldiez = waldiez
97
104
  self._called_install_requirements = False
98
105
  self._exporter = WaldiezExporter(waldiez)
99
106
  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
107
  self._last_results: list[dict[str, Any]] = []
107
108
  self._last_exception: Exception | None = None
108
- self._execution_complete_event = threading.Event()
109
- self._execution_thread: Optional[threading.Thread] = None
109
+ self._running_lock = threading.Lock()
110
+ self._loaded_module: ModuleType | None = None
111
+ logger = kwargs.get("logger")
112
+ if isinstance(logger, WaldiezLogger):
113
+ self._logger = logger
114
+ else:
115
+ self._logger = get_logger()
116
+ waldiez_file = kwargs.get("waldiez_file", "")
117
+ if isinstance(waldiez_file, str) and waldiez_file:
118
+ waldiez_file_path = Path(waldiez_file).resolve()
119
+ elif isinstance(waldiez_file, Path):
120
+ waldiez_file_path = waldiez_file.resolve()
121
+ else:
122
+ waldiez_file_path = dump_waldiez(waldiez, output_path=output_path)
123
+ if not waldiez_file_path or not waldiez_file_path.is_file():
124
+ raise ValueError("Could not resolve a waldiez file path")
125
+ WaldiezBaseRunner._waldiez_file = waldiez_file_path
126
+
127
+ @staticmethod
128
+ def print(*args: Any, **kwargs: Any) -> None:
129
+ """Print a message to the console.
130
+
131
+ Parameters
132
+ ----------
133
+ *args : Any
134
+ Positional arguments to print.
135
+ **kwargs : Any
136
+ Keyword arguments to print.
137
+ """
138
+ if len(args) == 1 and isinstance(args[0], dict):
139
+ arg = json.dumps(args[0], default=str, ensure_ascii=False)
140
+ WaldiezBaseRunner._print(arg, **kwargs)
141
+ else:
142
+ WaldiezBaseRunner._print(*args, **kwargs)
110
143
 
111
144
  def is_running(self) -> bool:
112
145
  """Check if the workflow is currently running.
@@ -116,65 +149,119 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
116
149
  bool
117
150
  True if the workflow is running, False otherwise.
118
151
  """
119
- return WaldiezBaseRunner._running
152
+ with self._running_lock:
153
+ return WaldiezBaseRunner._running
120
154
 
121
- def wait_for_completion(self, timeout: Optional[float] = None) -> bool:
122
- """Wait for non-blocking execution to complete.
155
+ @staticmethod
156
+ def get_input_function() -> (
157
+ Callable[..., str] | Callable[..., Coroutine[Any, Any, str]]
158
+ ):
159
+ """Get the input function for user interaction.
123
160
 
124
- This is a base implementation that subclasses can override if needed.
161
+ Returns
162
+ -------
163
+ Callable[[str, bool], str]
164
+ A function that takes a prompt and a password flag,
165
+ returning user input.
166
+ """
167
+ if hasattr(WaldiezBaseRunner, "_input") and callable(
168
+ WaldiezBaseRunner._input
169
+ ):
170
+ return WaldiezBaseRunner._input
171
+ if WaldiezBaseRunner._is_async:
172
+ return input_async
173
+ return input_sync
174
+
175
+ @staticmethod
176
+ async def a_get_user_input(
177
+ prompt: str, *, password: bool = False, **kwargs: Any
178
+ ) -> str:
179
+ """Get user input with an optional password prompt.
125
180
 
126
181
  Parameters
127
182
  ----------
128
- timeout: float | None
129
- The maximum time to wait for completion, in seconds.
130
- If None, wait indefinitely.
183
+ prompt : str
184
+ The prompt to display to the user.
185
+ password : bool, optional
186
+ If True, the input will be hidden (default is False).
187
+ **kwargs : Any
188
+ Additional keyword arguments to pass to the input function.
131
189
 
132
190
  Returns
133
191
  -------
134
- bool
135
- True if the workflow completed successfully, False if it timed out.
192
+ str
193
+ The user input.
136
194
  """
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()
195
+ input_function = WaldiezBaseRunner.get_input_function()
196
+ if is_async_callable(input_function):
197
+ try:
198
+ result = await input_function( # type: ignore
199
+ prompt,
200
+ password=password,
201
+ **kwargs,
202
+ )
203
+ except TypeError:
204
+ result = await input_function(prompt) # type: ignore
205
+ else:
206
+ try:
207
+ result = input_function(prompt, password=password, **kwargs)
208
+ except TypeError:
209
+ result = input_function(prompt)
210
+ return result # pyright: ignore
144
211
 
145
- return self._execution_complete_event.wait(timeout or 0)
212
+ @staticmethod
213
+ def get_user_input(
214
+ prompt: str,
215
+ *,
216
+ password: bool = False,
217
+ **kwargs: Any,
218
+ ) -> str:
219
+ """Get user input with an optional password prompt.
146
220
 
147
- def get_execution_stats(self) -> dict[str, Any]:
148
- """Get basic execution statistics.
221
+ Parameters
222
+ ----------
223
+ prompt : str
224
+ The prompt to display to the user.
225
+ password : bool, optional
226
+ If True, the input will be hidden (default is False).
227
+ **kwargs : Any
228
+ Additional keyword arguments to pass to the input function.
149
229
 
150
230
  Returns
151
231
  -------
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.
232
+ str
233
+ The user input.
158
234
  """
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()
235
+ input_function = WaldiezBaseRunner.get_input_function()
236
+ if inspect.iscoroutinefunction(input_function):
237
+ try:
238
+ return syncify(input_function)(
239
+ prompt, password=password, **kwargs
240
+ )
241
+ except TypeError:
242
+ return syncify(input_function)(prompt)
243
+ try:
244
+ return str(input_function(prompt, password=password, **kwargs))
245
+ except TypeError:
246
+ return str(input_function(prompt))
247
+
248
+ def _load_module(self, output_file: Path, temp_dir: Path) -> ModuleType:
249
+ """Load the module from the waldiez file."""
250
+ file_name = output_file.name
251
+ module_name = file_name.replace(".py", "")
252
+ spec = importlib.util.spec_from_file_location(
253
+ module_name, temp_dir / file_name
254
+ )
255
+ if not spec or not spec.loader:
256
+ raise ImportError("Could not import the flow")
257
+ module = importlib.util.module_from_spec(spec)
258
+ spec.loader.exec_module(module)
259
+ if not hasattr(module, "main"):
260
+ raise ImportError(
261
+ "The waldiez file does not contain a main() function"
262
+ )
263
+ self._loaded_module = module
264
+ return module
178
265
 
179
266
  def _before_run(
180
267
  self,
@@ -250,48 +337,11 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
250
337
  "The _a_run method must be implemented in the subclass."
251
338
  )
252
339
 
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
340
  def _after_run(
292
341
  self,
293
342
  results: list[dict[str, Any]],
294
343
  output_file: Path,
344
+ waldiez_file: Path,
295
345
  uploads_root: Path | None,
296
346
  temp_dir: Path,
297
347
  skip_mmd: bool,
@@ -303,20 +353,26 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
303
353
 
304
354
  # Reset stop flag for next run
305
355
  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
- )
356
+ # pylint: disable=broad-exception-caught
357
+ try:
358
+ after_run(
359
+ temp_dir=temp_dir,
360
+ output_file=output_file,
361
+ flow_name=self._waldiez.name,
362
+ waldiez_file=waldiez_file,
363
+ uploads_root=uploads_root,
364
+ skip_mmd=skip_mmd,
365
+ skip_timeline=skip_timeline,
366
+ )
367
+ except BaseException as exc: # pragma: no cover
368
+ self.log.warning("Error occurred during after_run: %s", exc)
314
369
  self.log.info("Cleanup completed")
315
370
 
316
371
  async def _a_after_run(
317
372
  self,
318
373
  results: list[dict[str, Any]],
319
374
  output_file: Path,
375
+ waldiez_file: Path,
320
376
  uploads_root: Path | None,
321
377
  temp_dir: Path,
322
378
  skip_mmd: bool,
@@ -330,23 +386,9 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
330
386
  temp_dir=temp_dir,
331
387
  skip_mmd=skip_mmd,
332
388
  skip_timeline=skip_timeline,
389
+ waldiez_file=waldiez_file,
333
390
  )
334
391
 
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
392
  @staticmethod
351
393
  def _prepare_paths(
352
394
  output_path: str | Path | None = None,
@@ -366,43 +408,67 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
366
408
  output_file: Path = Path(WaldiezBaseRunner._output_path)
367
409
  return output_file, uploads_root_path
368
410
 
369
- def gather_requirements(self) -> set[str]:
370
- """Gather extra requirements to install before running the flow.
411
+ @staticmethod
412
+ async def a_process_event(
413
+ event: Union["BaseEvent", "BaseMessage"],
414
+ skip_send: bool = False,
415
+ ) -> None:
416
+ """Process an event or message asynchronously.
371
417
 
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.
418
+ Parameters
419
+ ----------
420
+ event : Union[BaseEvent, BaseMessage]
421
+ The event or message to process.
422
+ skip_send : bool
423
+ Skip sending the event.
424
+ """
425
+ if hasattr(event, "type"): # pragma: no branch
426
+ if event.type == "input_request":
427
+ prompt = getattr(
428
+ event, "prompt", getattr(event.content, "prompt", "> ")
429
+ )
430
+ password = getattr(
431
+ event,
432
+ "password",
433
+ getattr(event.content, "password", False),
434
+ )
435
+ user_input = await WaldiezBaseRunner.a_get_user_input(
436
+ prompt, password=password
437
+ )
438
+ await event.content.respond(user_input)
439
+ elif not skip_send:
440
+ WaldiezBaseRunner._send(event)
441
+
442
+ @staticmethod
443
+ def process_event(
444
+ event: Union["BaseEvent", "BaseMessage"],
445
+ skip_send: bool = False,
446
+ ) -> None:
447
+ """Process an event or message synchronously.
448
+
449
+ Parameters
450
+ ----------
451
+ event : Union[BaseEvent, BaseMessage]
452
+ The event or message to process.
453
+ skip_send : bool
454
+ Skip sending the event.
377
455
  """
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
- # ===================================================================
456
+ if hasattr(event, "type"): # pragma: no branch
457
+ if event.type == "input_request":
458
+ prompt = getattr(
459
+ event, "prompt", getattr(event.content, "prompt", "> ")
460
+ )
461
+ password = getattr(
462
+ event,
463
+ "password",
464
+ getattr(event.content, "password", False),
465
+ )
466
+ user_input = WaldiezBaseRunner.get_user_input(
467
+ prompt, password=password
468
+ )
469
+ event.content.respond(user_input)
470
+ elif not skip_send:
471
+ WaldiezBaseRunner._send(event)
406
472
 
407
473
  def before_run(
408
474
  self,
@@ -452,6 +518,42 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
452
518
  uploads_root=uploads_root,
453
519
  )
454
520
 
521
+ def prepare(
522
+ self,
523
+ output_path: str | Path | None,
524
+ uploads_root: str | Path | None,
525
+ ) -> tuple[Path, Path, Path | None]:
526
+ """Prepare the paths and environment for running the flow.
527
+
528
+ Parameters
529
+ ----------
530
+ output_path : str | Path | None
531
+ The output path for the flow, by default None.
532
+ uploads_root : str | Path | None
533
+ The root path for uploads, by default None.
534
+
535
+ Returns
536
+ -------
537
+ tuple[Path, Path, Path | None]
538
+ A tuple containing:
539
+ - The path to the output file.
540
+ - The path to the temporary directory created for the run.
541
+ - The root path for uploads, if specified, otherwise None.
542
+ """
543
+ output_file, uploads_root_path = self._prepare_paths(
544
+ output_path=output_path,
545
+ uploads_root=uploads_root,
546
+ )
547
+ temp_dir = self.before_run(
548
+ output_file=output_file,
549
+ uploads_root=uploads_root_path,
550
+ )
551
+ self.install_requirements()
552
+ refresh_environment()
553
+ return temp_dir, output_file, uploads_root_path
554
+
555
+ # noinspection PyProtocol
556
+ # pylint: disable=too-many-locals,unused-argument
455
557
  def run(
456
558
  self,
457
559
  output_path: str | Path | None = None,
@@ -462,7 +564,7 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
462
564
  dot_env: str | Path | None = None,
463
565
  **kwargs: Any,
464
566
  ) -> list[dict[str, Any]]:
465
- """Run the Waldiez flow in blocking mode.
567
+ """Run the Waldiez flow.
466
568
 
467
569
  Parameters
468
570
  ----------
@@ -471,10 +573,9 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
471
573
  uploads_root : str | Path | None
472
574
  The runtime uploads root, by default None.
473
575
  structured_io : bool
474
- Whether to use structured IO instead of the default 'input/print',
475
- by default False.
576
+ Whether to use structured IO instead of the default 'input/print'.
476
577
  skip_mmd : bool
477
- Whether to skip generating the mermaid diagram, by default False.
578
+ Whether to skip generating the mermaid diagram.
478
579
  skip_timeline : bool
479
580
  Whether to skip generating the timeline JSON.
480
581
  dot_env : str | Path | None
@@ -485,12 +586,15 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
485
586
  Returns
486
587
  -------
487
588
  list[dict[str, Any]]
488
- The result of the run.
589
+ The results of the run.
489
590
 
490
591
  Raises
491
592
  ------
492
593
  RuntimeError
493
- If the runner is already running.
594
+ If the runner is already running, the workflow is not async,
595
+ or an error occurs during the run.
596
+ StopRunningException
597
+ If the run is stopped by the user.
494
598
  """
495
599
  if dot_env is not None:
496
600
  resolved = Path(dot_env).resolve()
@@ -500,7 +604,7 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
500
604
  WaldiezBaseRunner._structured_io = structured_io
501
605
  if self.is_running():
502
606
  raise RuntimeError("Workflow already running")
503
- if self.waldiez.is_async:
607
+ if self.is_async:
504
608
  with start_blocking_portal(backend="asyncio") as portal:
505
609
  return portal.call(
506
610
  self.a_run,
@@ -509,19 +613,13 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
509
613
  structured_io,
510
614
  skip_mmd,
511
615
  )
512
- output_file, uploads_root_path = self._prepare_paths(
616
+ temp_dir, output_file, uploads_root_path = self.prepare(
513
617
  output_path=output_path,
514
618
  uploads_root=uploads_root,
515
619
  )
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
620
  WaldiezBaseRunner._running = True
523
- results: list[dict[str, Any]] = []
524
- old_env_vars = set_env_vars(self.waldiez.get_flow_env_vars())
621
+ results: list[dict[str, Any]]
622
+ old_env_vars = set_env_vars(self._waldiez.get_flow_env_vars())
525
623
  try:
526
624
  with chdir(to=temp_dir):
527
625
  sys.path.insert(0, str(temp_dir))
@@ -532,6 +630,11 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
532
630
  skip_mmd=skip_mmd,
533
631
  skip_timeline=skip_timeline,
534
632
  )
633
+ except (SystemExit, StopRunningException, KeyboardInterrupt) as exc:
634
+ raise StopRunningException(StopRunningException.reason) from exc
635
+ except BaseException as exc: # pylint: disable=broad-exception-caught
636
+ self.log.error("Error occurred while running workflow: %s", exc)
637
+ results = [{"error": str(exc)}]
535
638
  finally:
536
639
  WaldiezBaseRunner._running = False
537
640
  reset_env_vars(old_env_vars)
@@ -548,7 +651,39 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
548
651
  sys.path.pop(0)
549
652
  return results
550
653
 
654
+ async def a_prepare(
655
+ self,
656
+ output_path: str | Path | None,
657
+ uploads_root: str | Path | None,
658
+ ) -> tuple[Path, Path, Path | None]:
659
+ """Prepare the paths for the async run.
660
+
661
+ Parameters
662
+ ----------
663
+ output_path : str | Path | None
664
+ The output path, by default None.
665
+ uploads_root : str | Path | None
666
+ The uploads root path, by default None.
667
+
668
+ Returns
669
+ -------
670
+ tuple[Path, Path, Path | None]
671
+ The temporary directory, output file, and uploads root path.
672
+ """
673
+ output_file, uploads_root_path = self._prepare_paths(
674
+ output_path=output_path,
675
+ uploads_root=uploads_root,
676
+ )
677
+ temp_dir = await self._a_before_run(
678
+ output_file=output_file,
679
+ uploads_root=uploads_root_path,
680
+ )
681
+ await self.a_install_requirements()
682
+ refresh_environment()
683
+ return temp_dir, output_file, uploads_root_path
684
+
551
685
  # noinspection DuplicatedCode
686
+ # noinspection PyProtocol
552
687
  async def a_run(
553
688
  self,
554
689
  output_path: str | Path | None = None,
@@ -566,28 +701,30 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
566
701
  output_path : str | Path | None
567
702
  The output path, by default None.
568
703
  uploads_root : str | Path | None
569
- The runtime uploads root, by default None.
704
+ The runtime uploads root.
570
705
  structured_io : bool
571
- Whether to use structured IO instead of the default 'input/print',
572
- by default False.
706
+ Whether to use structured IO instead of the default 'input/print'.
573
707
  skip_mmd : bool
574
- Whether to skip generating the mermaid diagram, by default False.
708
+ Whether to skip generating the mermaid diagram.
575
709
  skip_timeline : bool
576
- Whether to skip generating the timeline JSON, by default False.
710
+ Whether to skip generating the timeline JSON.
577
711
  dot_env : str | Path | None
578
712
  The path to the .env file, if any.
579
713
  **kwargs : Any
580
- Additional keyword arguments for the run method.
714
+ Additional keyword arguments for the a_run method.
581
715
 
582
716
  Returns
583
717
  -------
584
718
  list[dict[str, Any]]
585
- The result of the run.
719
+ The results of the run.
586
720
 
587
721
  Raises
588
722
  ------
589
723
  RuntimeError
590
- If the runner is already running.
724
+ If the runner is already running, the workflow is not async
725
+ or an error occurs during the run.
726
+ StopRunningException
727
+ If the run is stopped by the user.
591
728
  """
592
729
  if dot_env is not None:
593
730
  resolved = Path(dot_env).resolve()
@@ -597,19 +734,13 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
597
734
  WaldiezBaseRunner._structured_io = structured_io
598
735
  if self.is_running():
599
736
  raise RuntimeError("Workflow already running")
600
- output_file, uploads_root_path = self._prepare_paths(
737
+ temp_dir, output_file, uploads_root_path = await self.a_prepare(
601
738
  output_path=output_path,
602
739
  uploads_root=uploads_root,
603
740
  )
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
741
  WaldiezBaseRunner._running = True
611
- results: list[dict[str, Any]] = []
612
- old_env_vars = set_env_vars(self.waldiez.get_flow_env_vars())
742
+ results: list[dict[str, Any]]
743
+ old_env_vars = set_env_vars(self._waldiez.get_flow_env_vars())
613
744
  try:
614
745
  async with a_chdir(to=temp_dir):
615
746
  sys.path.insert(0, str(temp_dir))
@@ -620,6 +751,10 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
620
751
  skip_mmd=skip_mmd,
621
752
  skip_timeline=skip_timeline,
622
753
  )
754
+ except (SystemExit, StopRunningException, KeyboardInterrupt) as exc:
755
+ raise StopRunningException(StopRunningException.reason) from exc
756
+ except BaseException as exc: # pylint: disable=broad-exception-caught
757
+ results = [{"error": str(exc)}]
623
758
  finally:
624
759
  WaldiezBaseRunner._running = False
625
760
  reset_env_vars(old_env_vars)
@@ -627,6 +762,7 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
627
762
  results=results,
628
763
  output_file=output_file,
629
764
  uploads_root=uploads_root_path,
765
+ waldiez_file=WaldiezBaseRunner._waldiez_file,
630
766
  temp_dir=temp_dir,
631
767
  skip_mmd=skip_mmd,
632
768
  skip_timeline=skip_timeline,
@@ -635,129 +771,6 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
635
771
  sys.path.pop(0)
636
772
  return results
637
773
 
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
774
  def after_run(
762
775
  self,
763
776
  results: list[dict[str, Any]],
@@ -787,6 +800,7 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
787
800
  self._after_run(
788
801
  results=results,
789
802
  output_file=output_file,
803
+ waldiez_file=WaldiezBaseRunner._waldiez_file,
790
804
  uploads_root=uploads_root,
791
805
  temp_dir=temp_dir,
792
806
  skip_mmd=skip_mmd,
@@ -826,39 +840,23 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
826
840
  temp_dir=temp_dir,
827
841
  skip_mmd=skip_mmd,
828
842
  skip_timeline=skip_timeline,
843
+ waldiez_file=WaldiezBaseRunner._waldiez_file,
829
844
  )
830
845
 
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
846
  @property
854
847
  def waldiez(self) -> Waldiez:
855
848
  """Get the Waldiez instance."""
856
849
  return self._waldiez
857
850
 
851
+ @property
852
+ def waldiez_file(self) -> Path:
853
+ """Get the path to the waldiez file."""
854
+ return self._waldiez_file
855
+
858
856
  @property
859
857
  def is_async(self) -> bool:
860
858
  """Check if the workflow is async."""
861
- return self.waldiez.is_async
859
+ return self._waldiez.is_async
862
860
 
863
861
  @property
864
862
  def running(self) -> bool:
@@ -890,11 +888,6 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
890
888
  """Get the uploads root path for the runner."""
891
889
  return WaldiezBaseRunner._uploads_root
892
890
 
893
- @property
894
- def skip_patch_io(self) -> bool:
895
- """Check if the runner is skipping patching IO."""
896
- return WaldiezBaseRunner._skip_patch_io
897
-
898
891
  @classmethod
899
892
  def load(
900
893
  cls,
@@ -952,6 +945,31 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
952
945
  dot_env=dot_env,
953
946
  )
954
947
 
948
+ def stop(self) -> None:
949
+ """Stop the workflow execution.
950
+
951
+ This method sets the stop flag that will be checked by the event
952
+ handlers and other parts of the workflow execution to gracefully
953
+ terminate the workflow execution.
954
+ Note: Stopping will occur at the "next" AutoGen event, not immediately.
955
+ """
956
+ self.log.info("Stop requested - setting stop flag")
957
+ self._stop_requested.set()
958
+
959
+ def is_stop_requested(self) -> bool:
960
+ """Check if a stop has been requested.
961
+
962
+ Returns
963
+ -------
964
+ bool
965
+ True if stop has been requested, False otherwise.
966
+ """
967
+ return self._stop_requested.is_set()
968
+
969
+ def set_stop_requested(self) -> None:
970
+ """Set the stop requested flag."""
971
+ self._stop_requested.set()
972
+
955
973
  def __enter__(self) -> Self:
956
974
  """Enter the context manager."""
957
975
  return self
@@ -968,7 +986,7 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
968
986
  ) -> None:
969
987
  """Exit the context manager."""
970
988
  if self.is_running():
971
- self.stop()
989
+ self._stop_requested.set()
972
990
 
973
991
  async def __aexit__(
974
992
  self,
@@ -978,4 +996,4 @@ class WaldiezBaseRunner(WaldiezRunnerProtocol):
978
996
  ) -> None:
979
997
  """Exit the context manager asynchronously."""
980
998
  if self.is_running():
981
- await self.a_stop()
999
+ self._stop_requested.set()