waldiez 0.5.3__py3-none-any.whl → 0.5.5__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 (76) hide show
  1. waldiez/_version.py +1 -1
  2. waldiez/cli.py +3 -27
  3. waldiez/exporter.py +0 -13
  4. waldiez/exporting/agent/exporter.py +38 -0
  5. waldiez/exporting/agent/extras/__init__.py +2 -0
  6. waldiez/exporting/agent/extras/doc_agent_extras.py +366 -0
  7. waldiez/exporting/agent/extras/group_member_extras.py +3 -2
  8. waldiez/exporting/agent/processor.py +113 -15
  9. waldiez/exporting/chats/processor.py +2 -21
  10. waldiez/exporting/chats/utils/common.py +66 -1
  11. waldiez/exporting/chats/utils/group.py +6 -3
  12. waldiez/exporting/chats/utils/nested.py +1 -1
  13. waldiez/exporting/chats/utils/sequential.py +25 -9
  14. waldiez/exporting/chats/utils/single.py +8 -6
  15. waldiez/exporting/core/context.py +0 -12
  16. waldiez/exporting/core/extras/agent_extras/standard_extras.py +3 -1
  17. waldiez/exporting/core/extras/base.py +20 -17
  18. waldiez/exporting/core/extras/path_resolver.py +39 -41
  19. waldiez/exporting/core/extras/serializer.py +16 -1
  20. waldiez/exporting/core/protocols.py +17 -0
  21. waldiez/exporting/core/types.py +6 -9
  22. waldiez/exporting/flow/execution_generator.py +56 -21
  23. waldiez/exporting/flow/exporter.py +1 -4
  24. waldiez/exporting/flow/factory.py +0 -9
  25. waldiez/exporting/flow/file_generator.py +6 -0
  26. waldiez/exporting/flow/orchestrator.py +27 -21
  27. waldiez/exporting/flow/utils/__init__.py +0 -2
  28. waldiez/exporting/flow/utils/common.py +15 -96
  29. waldiez/exporting/flow/utils/importing.py +4 -0
  30. waldiez/io/mqtt.py +33 -14
  31. waldiez/io/redis.py +18 -13
  32. waldiez/io/structured.py +9 -4
  33. waldiez/io/utils.py +32 -0
  34. waldiez/io/ws.py +8 -2
  35. waldiez/models/__init__.py +6 -0
  36. waldiez/models/agents/__init__.py +8 -0
  37. waldiez/models/agents/agent/agent.py +136 -38
  38. waldiez/models/agents/agent/agent_type.py +3 -2
  39. waldiez/models/agents/agents.py +10 -0
  40. waldiez/models/agents/doc_agent/__init__.py +13 -0
  41. waldiez/models/agents/doc_agent/doc_agent.py +126 -0
  42. waldiez/models/agents/doc_agent/doc_agent_data.py +149 -0
  43. waldiez/models/agents/doc_agent/rag_query_engine.py +127 -0
  44. waldiez/models/flow/flow.py +13 -2
  45. waldiez/models/model/__init__.py +2 -2
  46. waldiez/models/model/_aws.py +75 -0
  47. waldiez/models/model/_llm.py +516 -0
  48. waldiez/models/model/_price.py +30 -0
  49. waldiez/models/model/model.py +45 -2
  50. waldiez/models/model/model_data.py +2 -83
  51. waldiez/models/tool/predefined/_duckduckgo.py +123 -0
  52. waldiez/models/tool/predefined/_google.py +31 -9
  53. waldiez/models/tool/predefined/_perplexity.py +161 -0
  54. waldiez/models/tool/predefined/_searxng.py +152 -0
  55. waldiez/models/tool/predefined/_tavily.py +46 -9
  56. waldiez/models/tool/predefined/_wikipedia.py +26 -6
  57. waldiez/models/tool/predefined/_youtube.py +36 -8
  58. waldiez/models/tool/predefined/registry.py +6 -0
  59. waldiez/models/waldiez.py +12 -0
  60. waldiez/runner.py +177 -408
  61. waldiez/running/__init__.py +2 -4
  62. waldiez/running/base_runner.py +100 -112
  63. waldiez/running/environment.py +29 -4
  64. waldiez/running/post_run.py +0 -1
  65. waldiez/running/protocol.py +36 -48
  66. waldiez/running/run_results.py +5 -5
  67. waldiez/running/standard_runner.py +429 -0
  68. waldiez/running/timeline_processor.py +0 -82
  69. {waldiez-0.5.3.dist-info → waldiez-0.5.5.dist-info}/METADATA +59 -62
  70. {waldiez-0.5.3.dist-info → waldiez-0.5.5.dist-info}/RECORD +74 -64
  71. waldiez/running/import_runner.py +0 -437
  72. waldiez/running/subprocess_runner.py +0 -104
  73. {waldiez-0.5.3.dist-info → waldiez-0.5.5.dist-info}/WHEEL +0 -0
  74. {waldiez-0.5.3.dist-info → waldiez-0.5.5.dist-info}/entry_points.txt +0 -0
  75. {waldiez-0.5.3.dist-info → waldiez-0.5.5.dist-info}/licenses/LICENSE +0 -0
  76. {waldiez-0.5.3.dist-info → waldiez-0.5.5.dist-info}/licenses/NOTICE.md +0 -0
@@ -7,7 +7,10 @@ from pathlib import Path
7
7
  from typing import TYPE_CHECKING, Protocol, Union, runtime_checkable
8
8
 
9
9
  if TYPE_CHECKING:
10
- from autogen import ChatResult # type: ignore[import-untyped]
10
+ from autogen.io.run_response import ( # type: ignore[import-untyped]
11
+ AsyncRunResponseProtocol,
12
+ RunResponseProtocol,
13
+ )
11
14
 
12
15
 
13
16
  @runtime_checkable
@@ -59,7 +62,6 @@ class WaldiezRunnerProtocol(Protocol):
59
62
  output_path: str | Path | None,
60
63
  uploads_root: str | Path | None,
61
64
  structured_io: bool | None = None,
62
- skip_patch_io: bool | None = None,
63
65
  skip_mmd: bool = False,
64
66
  ) -> None:
65
67
  """Start running the Waldiez flow in a non-blocking way.
@@ -74,9 +76,6 @@ class WaldiezRunnerProtocol(Protocol):
74
76
  The runtime uploads root.
75
77
  structured_io : bool
76
78
  Whether to use structured IO instead of the default 'input/print'.
77
- skip_patch_io : bool | None
78
- Whether to skip patching I/O, by default None.
79
- If None, it will use the value from the context.
80
79
  skip_mmd : bool
81
80
  Whether to skip generating the mermaid diagram.
82
81
 
@@ -91,7 +90,6 @@ class WaldiezRunnerProtocol(Protocol):
91
90
  output_path: str | Path | None,
92
91
  uploads_root: str | Path | None,
93
92
  structured_io: bool | None = None,
94
- skip_patch_io: bool | None = None,
95
93
  skip_mmd: bool = False,
96
94
  ) -> None:
97
95
  """Asynchronously start running the Waldiez flow in a non-blocking way.
@@ -106,9 +104,6 @@ class WaldiezRunnerProtocol(Protocol):
106
104
  The runtime uploads root.
107
105
  structured_io : bool
108
106
  Whether to use structured IO instead of the default 'input/print'.
109
- skip_patch_io : bool | None
110
- Whether to skip patching I/O, by default None.
111
- If None, it will use the value from the context.
112
107
  skip_mmd : bool
113
108
  Whether to skip generating the mermaid diagram.
114
109
 
@@ -123,13 +118,10 @@ class WaldiezRunnerProtocol(Protocol):
123
118
  output_path: str | Path | None,
124
119
  uploads_root: str | Path | None,
125
120
  structured_io: bool | None = None,
126
- threaded: bool | None = None,
127
- skip_patch_io: bool | None = None,
128
121
  skip_mmd: bool = False,
129
122
  ) -> Union[
130
- "ChatResult",
131
- list["ChatResult"],
132
- dict[int, "ChatResult"],
123
+ list["RunResponseProtocol"],
124
+ list["AsyncRunResponseProtocol"],
133
125
  ]: # pyright: ignore
134
126
  """Run the Waldiez flow in a blocking way.
135
127
 
@@ -141,20 +133,17 @@ class WaldiezRunnerProtocol(Protocol):
141
133
  The runtime uploads root.
142
134
  structured_io : bool
143
135
  Whether to use structured IO instead of the default 'input/print'.
144
- threaded : bool | None
145
- Whether to run the flow in a separate thread.
146
- skip_patch_io : bool
147
- Whether to skip patching I/O, by default None.
148
- If None, it will use the value from the context.
149
136
  skip_mmd : bool
150
137
  Whether to skip generating the mermaid diagram.
151
138
 
152
139
  Returns
153
140
  -------
154
- Union[ChatResult, list[ChatResult], dict[int, ChatResult]]
155
- The result of the run, which can be a single ChatResult,
156
- a list of ChatResults,
157
- or a dictionary mapping indices to ChatResults.
141
+ Union[
142
+ list["RunResponseProtocol"],
143
+ list["AsyncRunResponseProtocol"],
144
+ ]
145
+ The result of the run, which can be a list of RunResponseProtocol
146
+ or a list of AsyncRunResponseProtocol.
158
147
  """
159
148
 
160
149
  async def a_run(
@@ -162,12 +151,10 @@ class WaldiezRunnerProtocol(Protocol):
162
151
  output_path: str | Path | None,
163
152
  uploads_root: str | Path | None,
164
153
  structured_io: bool | None = None,
165
- skip_patch_io: bool | None = None,
166
154
  skip_mmd: bool = False,
167
155
  ) -> Union[
168
- "ChatResult",
169
- list["ChatResult"],
170
- dict[int, "ChatResult"],
156
+ list["RunResponseProtocol"],
157
+ list["AsyncRunResponseProtocol"],
171
158
  ]: # pyright: ignore
172
159
  """Run the Waldiez flow.
173
160
 
@@ -179,26 +166,24 @@ class WaldiezRunnerProtocol(Protocol):
179
166
  The runtime uploads root.
180
167
  structured_io : bool
181
168
  Whether to use structured IO instead of the default 'input/print'.
182
- skip_patch_io : bool
183
- Whether to skip patching I/O, by default None.
184
- If None, it will use the value from the context.
185
169
  skip_mmd : bool
186
170
  Whether to skip generating the mermaid diagram.
187
171
 
188
172
  Returns
189
173
  -------
190
- Union[ChatResult, list[ChatResult], dict[int, ChatResult]]
191
- The result of the run, which can be a single ChatResult,
192
- a list of ChatResults,
193
- or a dictionary mapping indices to ChatResults.
174
+ Union[
175
+ list["RunResponseProtocol"],
176
+ list["AsyncRunResponseProtocol"],
177
+ ]
178
+ The result of the run, which can be a list of RunResponseProtocol
179
+ or a list of AsyncRunResponseProtocol.
194
180
  """
195
181
 
196
182
  def after_run(
197
183
  self,
198
184
  results: Union[
199
- "ChatResult",
200
- list["ChatResult"],
201
- dict[int, "ChatResult"],
185
+ list["RunResponseProtocol"],
186
+ list["AsyncRunResponseProtocol"],
202
187
  ],
203
188
  output_file: Path,
204
189
  uploads_root: Path | None,
@@ -210,10 +195,12 @@ class WaldiezRunnerProtocol(Protocol):
210
195
 
211
196
  Parameters
212
197
  ----------
213
- results : Union[ChatResult, list[ChatResult], dict[int, ChatResult]]
214
- The results of the run, which can be a single ChatResult,
215
- a list of ChatResults,
216
- or a dictionary mapping indices to ChatResults.
198
+ results : Union[
199
+ list["RunResponseProtocol"],
200
+ list["AsyncRunResponseProtocol"],
201
+ ]
202
+ The results of the run, which can be a list of RunResponseProtocol
203
+ or a list of AsyncRunResponseProtocol.
217
204
  output_file : Path
218
205
  The path to the output file.
219
206
  uploads_root : Path | None
@@ -229,9 +216,8 @@ class WaldiezRunnerProtocol(Protocol):
229
216
  async def a_after_run(
230
217
  self,
231
218
  results: Union[
232
- "ChatResult",
233
- list["ChatResult"],
234
- dict[int, "ChatResult"],
219
+ list["RunResponseProtocol"],
220
+ list["AsyncRunResponseProtocol"],
235
221
  ],
236
222
  output_file: Path,
237
223
  uploads_root: Path | None,
@@ -243,10 +229,12 @@ class WaldiezRunnerProtocol(Protocol):
243
229
 
244
230
  Parameters
245
231
  ----------
246
- results : Union[ChatResult, list[ChatResult], dict[int, ChatResult]]
247
- The results of the run, which can be a single ChatResult,
248
- a list of ChatResults,
249
- or a dictionary mapping indices to ChatResults.
232
+ results : Union[
233
+ list["RunResponseProtocol"],
234
+ list["AsyncRunResponseProtocol"]
235
+ ]
236
+ The results of the run, which can be a list of RunResponseProtocol
237
+ or a list of AsyncRunResponseProtocol.
250
238
  output_file : Path
251
239
  The path to the output file.
252
240
  uploads_root : Path | None
@@ -6,17 +6,17 @@
6
6
  from typing import TYPE_CHECKING, TypedDict, Union
7
7
 
8
8
  if TYPE_CHECKING:
9
- from autogen import ChatResult # type: ignore[import-untyped]
9
+ from autogen.io.run_response import ( # type: ignore[import-untyped]
10
+ AsyncRunResponseProtocol,
11
+ RunResponseProtocol,
12
+ )
10
13
 
11
14
 
12
15
  class WaldiezRunResults(TypedDict):
13
16
  """Results of the Waldiez run."""
14
17
 
15
18
  results: Union[
16
- "ChatResult",
17
- list["ChatResult"],
18
- dict[int, "ChatResult"],
19
- None,
19
+ list["RunResponseProtocol"], list["AsyncRunResponseProtocol"]
20
20
  ]
21
21
  exception: Exception | None
22
22
  completed: bool
@@ -0,0 +1,429 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+
4
+ # flake8: noqa: C901, E501
5
+ # pyright: reportUnknownMemberType=false, reportUnknownVariableType=false
6
+ # pyright: reportAttributeAccessIssue=false,reportUnknownArgumentType=false
7
+ # pylint: disable=too-many-try-statements,import-outside-toplevel,line-too-long,
8
+ # pylint: disable=too-complex,unused-argument,duplicate-code,broad-exception-caught
9
+ """Run a waldiez flow.
10
+
11
+ The flow is first converted to an autogen flow with agents, chats, and tools.
12
+ We then chown to temporary directory, call the flow's `main()` and
13
+ return the results. Before running the flow, any additional environment
14
+ variables specified in the waldiez file are set.
15
+ """
16
+
17
+ import asyncio
18
+ import importlib.util
19
+ import sys
20
+ import threading
21
+ import traceback
22
+ from pathlib import Path
23
+ from types import ModuleType
24
+ from typing import TYPE_CHECKING, Any, Callable, Coroutine, Union
25
+
26
+ from waldiez.models.waldiez import Waldiez
27
+ from waldiez.running.run_results import WaldiezRunResults
28
+
29
+ from .base_runner import WaldiezBaseRunner
30
+ from .utils import chdir
31
+
32
+ if TYPE_CHECKING:
33
+ from autogen.events import BaseEvent # type: ignore
34
+ from autogen.io.run_response import ( # type: ignore
35
+ AsyncRunResponseProtocol,
36
+ RunResponseProtocol,
37
+ )
38
+
39
+
40
+ class WaldiezStandardRunner(WaldiezBaseRunner):
41
+ """Run a waldiez flow in a standard way."""
42
+
43
+ def __init__(
44
+ self,
45
+ waldiez: Waldiez,
46
+ output_path: str | Path | None = None,
47
+ uploads_root: str | Path | None = None,
48
+ structured_io: bool = False,
49
+ ) -> None:
50
+ """Initialize the Waldiez manager."""
51
+ super().__init__(
52
+ waldiez,
53
+ output_path=output_path,
54
+ uploads_root=uploads_root,
55
+ structured_io=structured_io,
56
+ )
57
+ self._execution_thread: threading.Thread | None = None
58
+ self._loaded_module: ModuleType | None = None
59
+ self._event_count = 0
60
+ self._processed_events = 0
61
+
62
+ def _load_module(self, output_file: Path, temp_dir: Path) -> ModuleType:
63
+ """Load the module from the waldiez file."""
64
+ file_name = output_file.name
65
+ module_name = file_name.replace(".py", "")
66
+ spec = importlib.util.spec_from_file_location(
67
+ module_name, temp_dir / file_name
68
+ )
69
+ if not spec or not spec.loader:
70
+ raise ImportError("Could not import the flow")
71
+ module = importlib.util.module_from_spec(spec)
72
+ spec.loader.exec_module(module)
73
+ if not hasattr(module, "main"):
74
+ raise ImportError(
75
+ "The waldiez file does not contain a main() function"
76
+ )
77
+ self._loaded_module = module
78
+ return module
79
+
80
+ def _run(
81
+ self,
82
+ temp_dir: Path,
83
+ output_file: Path,
84
+ uploads_root: Path | None,
85
+ skip_mmd: bool,
86
+ skip_timeline: bool,
87
+ ) -> Union[list["RunResponseProtocol"], list["AsyncRunResponseProtocol"]]:
88
+ """Run the Waldiez workflow."""
89
+ from autogen.io import IOStream # type: ignore
90
+
91
+ from waldiez.io import StructuredIOStream
92
+
93
+ self._print: Callable[..., None] = print
94
+ self._input: (
95
+ Callable[..., str] | Callable[..., Coroutine[Any, Any, str]]
96
+ ) = input
97
+ results_container: WaldiezRunResults = {
98
+ "results": [],
99
+ "exception": None,
100
+ "completed": False,
101
+ }
102
+ try:
103
+ self._loaded_module = self._load_module(output_file, temp_dir)
104
+ if self._stop_requested.is_set():
105
+ self.log.info(
106
+ "Async execution stopped before AG2 workflow start"
107
+ )
108
+ return []
109
+ if self.structured_io:
110
+ stream = StructuredIOStream(
111
+ uploads_root=uploads_root, is_async=False
112
+ )
113
+ else:
114
+ stream = IOStream.get_default()
115
+ self._print = stream.print
116
+ self._input = stream.input
117
+ self._send = stream.send
118
+ self._print("<Waldiez> - Starting workflow...")
119
+ self._print(self.waldiez.info.model_dump_json())
120
+ results = self._loaded_module.main(
121
+ on_event=self._on_event,
122
+ )
123
+ results_container["results"] = results
124
+ self._print("<Waldiez> - Workflow finished")
125
+ except SystemExit:
126
+ self._print("<Waldiez> - Workflow stopped by user")
127
+ except Exception as e: # pylint: disable=broad-exception-caught
128
+ results_container["exception"] = e
129
+ traceback.print_exc()
130
+ self._print(f"<Waldiez> - Workflow execution failed: {e}")
131
+ finally:
132
+ results_container["completed"] = True
133
+ return results_container["results"]
134
+
135
+ def _on_event(
136
+ self,
137
+ event: "BaseEvent",
138
+ ) -> bool:
139
+ """Process an event from the workflow."""
140
+ self._event_count += 1
141
+ if self._stop_requested.is_set():
142
+ self.log.info(
143
+ "Async execution stopped before AG2 workflow event processing"
144
+ )
145
+ return False
146
+ try:
147
+ if hasattr(event, "type"):
148
+ if event.type == "input_request":
149
+ prompt = getattr(
150
+ event,
151
+ "prompt",
152
+ getattr(event.content, "prompt", "> "),
153
+ )
154
+ password = getattr(
155
+ event,
156
+ "password",
157
+ getattr(event.content, "password", False),
158
+ )
159
+ user_input = self._input(
160
+ prompt,
161
+ password=password,
162
+ )
163
+ event.content.respond(user_input)
164
+ else:
165
+ self._send(event)
166
+ self._processed_events += 1
167
+ except Exception as e:
168
+ raise RuntimeError(
169
+ f"Error processing event {event}: {e}\n{traceback.format_exc()}"
170
+ ) from e
171
+ if event.type == "run_completion":
172
+ self._signal_completion()
173
+ WaldiezBaseRunner._running = False
174
+ return not self._stop_requested.is_set()
175
+
176
+ def _start(
177
+ self,
178
+ temp_dir: Path,
179
+ output_file: Path,
180
+ uploads_root: Path | None,
181
+ skip_mmd: bool,
182
+ skip_timeline: bool,
183
+ ) -> None:
184
+ """Start the workflow in a non-blocking way."""
185
+ if self._execution_thread and self._execution_thread.is_alive():
186
+ raise RuntimeError("Non-blocking execution already in progress")
187
+
188
+ # Reset completion state
189
+ self._reset_completion_state()
190
+
191
+ # Create thread with proper integration
192
+ self._execution_thread = threading.Thread(
193
+ target=self._threaded_run,
194
+ args=(temp_dir, output_file, uploads_root, skip_mmd, skip_timeline),
195
+ name=f"WaldiezStandardRunner-{self.waldiez.name}",
196
+ daemon=False, # Not daemon so we can properly join
197
+ )
198
+ self._execution_thread.start()
199
+
200
+ def _threaded_run(
201
+ self,
202
+ temp_dir: Path,
203
+ output_file: Path,
204
+ uploads_root: Path | None,
205
+ skip_mmd: bool = False,
206
+ skip_timeline: bool = False,
207
+ ) -> None:
208
+ """Run in a separate thread with proper lifecycle."""
209
+ try:
210
+ # Change to temp directory and manage sys.path
211
+ with chdir(to=temp_dir):
212
+ sys.path.insert(0, str(temp_dir))
213
+ try:
214
+ results = self._run(
215
+ temp_dir=temp_dir,
216
+ output_file=output_file,
217
+ uploads_root=uploads_root,
218
+ skip_mmd=skip_mmd,
219
+ skip_timeline=skip_timeline,
220
+ )
221
+
222
+ # Store results
223
+ self._last_results = results
224
+
225
+ # Call after_run hooks
226
+ self.after_run(
227
+ results=results,
228
+ output_file=output_file,
229
+ uploads_root=uploads_root,
230
+ temp_dir=temp_dir,
231
+ skip_mmd=skip_mmd,
232
+ skip_timeline=skip_timeline,
233
+ )
234
+
235
+ finally:
236
+ # Clean up sys.path
237
+ if sys.path and sys.path[0] == str(temp_dir):
238
+ sys.path.pop(0)
239
+
240
+ except Exception as e:
241
+ self._last_exception = e
242
+ self.log.error("Threaded execution failed: %s", e)
243
+
244
+ finally:
245
+ # Signal completion and mark as not running
246
+ self._signal_completion()
247
+ WaldiezBaseRunner._running = False
248
+
249
+ async def _a_on_event(
250
+ self,
251
+ event: "BaseEvent",
252
+ ) -> bool:
253
+ """Process an event from the workflow asynchronously."""
254
+ self._event_count += 1
255
+ if self._stop_requested.is_set():
256
+ self.log.info(
257
+ "Async execution stopped before AG2 workflow event processing"
258
+ )
259
+ return False
260
+ try:
261
+ if hasattr(event, "type"):
262
+ if event.type == "input_request":
263
+ prompt = getattr(
264
+ event,
265
+ "prompt",
266
+ getattr(event.content, "prompt", "> "),
267
+ )
268
+ password = getattr(
269
+ event,
270
+ "password",
271
+ getattr(event.content, "password", False),
272
+ )
273
+ user_input = self._input(
274
+ prompt,
275
+ password=password,
276
+ )
277
+ if asyncio.iscoroutine(user_input):
278
+ user_input = await user_input
279
+ await event.content.respond(user_input)
280
+ else:
281
+ self._send(event)
282
+ self._processed_events += 1
283
+ except Exception as e:
284
+ raise RuntimeError(
285
+ f"Error processing event {event}: {e}\n{traceback.format_exc()}"
286
+ ) from e
287
+ return not self._stop_requested.is_set()
288
+
289
+ async def _a_run(
290
+ self,
291
+ temp_dir: Path,
292
+ output_file: Path,
293
+ uploads_root: Path | None,
294
+ skip_mmd: bool = False,
295
+ skip_timeline: bool = False,
296
+ ) -> Union[list["RunResponseProtocol"], list["AsyncRunResponseProtocol"]]:
297
+ """Run the Waldiez workflow asynchronously."""
298
+
299
+ # fmt: off
300
+ async def _execute_workflow() -> Union[list["RunResponseProtocol"], list["AsyncRunResponseProtocol"]]:
301
+ # fmt: on
302
+ """Execute the workflow in an async context."""
303
+ from autogen.io import IOStream # pyright: ignore
304
+
305
+ from waldiez.io import StructuredIOStream
306
+
307
+ results: Union[list["AsyncRunResponseProtocol"], list["RunResponseProtocol"]] = []
308
+ try:
309
+ self._loaded_module = self._load_module(output_file, temp_dir)
310
+ if self._stop_requested.is_set():
311
+ self.log.info(
312
+ "Async execution stopped before AG2 workflow start"
313
+ )
314
+ return []
315
+ if self.structured_io:
316
+ stream = StructuredIOStream(
317
+ uploads_root=uploads_root, is_async=True
318
+ )
319
+ else:
320
+ stream = IOStream.get_default()
321
+ self._print = stream.print
322
+ self._input = stream.input
323
+ self._send = stream.send
324
+ self._print("<Waldiez> - Starting workflow...")
325
+ self._print(self.waldiez.info.model_dump_json())
326
+ results = await self._loaded_module.main(
327
+ on_event=self._a_on_event
328
+ )
329
+ except SystemExit:
330
+ self._print("<Waldiez> - Workflow stopped by user")
331
+ return []
332
+ except Exception as e:
333
+ self._print(
334
+ f"<Waldiez> - Error loading workflow: {e}\n{traceback.format_exc()}"
335
+ )
336
+ raise RuntimeError(
337
+ f"Error loading workflow: {e}\n{traceback.format_exc()}"
338
+ ) from e
339
+ return results
340
+
341
+ # Create cancellable task
342
+ task = asyncio.create_task(_execute_workflow())
343
+
344
+ # Monitor for stop requests
345
+ try:
346
+ while not task.done():
347
+ if self._stop_requested.is_set():
348
+ task.cancel()
349
+ self.log.info("Async execution stopped by user")
350
+ break
351
+ await asyncio.sleep(0.1)
352
+ # Return the task result when completed
353
+ return await task
354
+
355
+ except asyncio.CancelledError:
356
+ self.log.info("Async execution cancelled")
357
+ return []
358
+
359
+ async def _a_start(
360
+ self,
361
+ temp_dir: Path,
362
+ output_file: Path,
363
+ uploads_root: Path | None,
364
+ skip_mmd: bool = False,
365
+ skip_timeline: bool = False,
366
+ ) -> None:
367
+ """Start the Waldiez workflow asynchronously."""
368
+
369
+ async def run_in_background() -> None:
370
+ """Run the Waldiez workflow in a background thread."""
371
+ try:
372
+ results = await self._a_run(
373
+ temp_dir,
374
+ output_file,
375
+ uploads_root,
376
+ skip_mmd=skip_mmd,
377
+ skip_timeline=skip_timeline,
378
+ )
379
+ if results:
380
+ self._print(f"<Waldiez> - Workflow completed: {results}")
381
+ except Exception as e:
382
+ self._print(
383
+ f"<Waldiez> - Error during workflow: {e}\n{traceback.format_exc()}"
384
+ )
385
+
386
+ asyncio.create_task(run_in_background())
387
+
388
+ def _stop(self) -> None:
389
+ """Stop the Waldiez workflow."""
390
+ self.log.info("Stopping workflow execution...")
391
+ self._stop_requested.set()
392
+
393
+ # Wait for graceful shutdown
394
+ if self._execution_thread and self._execution_thread.is_alive():
395
+ self._execution_thread.join(timeout=5.0)
396
+
397
+ if self._execution_thread and self._execution_thread.is_alive():
398
+ self.log.warning("Workflow thread did not stop gracefully")
399
+
400
+ async def _a_stop(self) -> None:
401
+ """Stop the Waldiez workflow asynchronously."""
402
+ self.log.info("Stopping workflow execution (async)...")
403
+ self._stop_requested.set()
404
+
405
+ # For async, we rely on the task cancellation in _a_run
406
+ # Let's give it a moment to respond
407
+ await asyncio.sleep(0.5)
408
+
409
+ def get_execution_stats(self) -> dict[str, Any]:
410
+ """Get execution statistics for standard runner.
411
+
412
+ Returns
413
+ -------
414
+ dict[str, Any]
415
+ A dictionary containing execution statistics such as total events,
416
+ processed events, whether a module was loaded, and event processing rate.
417
+ """
418
+ base_stats = super().get_execution_stats()
419
+ return {
420
+ **base_stats,
421
+ "total_events": self._event_count,
422
+ "processed_events": self._processed_events,
423
+ "has_loaded_module": self._loaded_module is not None,
424
+ "event_processing_rate": (
425
+ self._processed_events / self._event_count
426
+ if self._event_count > 0
427
+ else 0
428
+ ),
429
+ }