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
@@ -0,0 +1,745 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+
4
+ # pylint: disable=line-too-long
5
+ # pyright: reportUnknownMemberType=false, reportAttributeAccessIssue=false
6
+ # pyright: reportUnknownArgumentType=false, reportOptionalMemberAccess=false
7
+ # pylint: disable=duplicate-code
8
+ # flake8: noqa: E501
9
+
10
+ """Step-by-step Waldiez runner with user interaction capabilities."""
11
+
12
+ import asyncio
13
+ import threading
14
+ import traceback
15
+ import uuid
16
+ from pathlib import Path
17
+ from typing import TYPE_CHECKING, Any, Iterable, Union
18
+
19
+ from pydantic import ValidationError
20
+
21
+ from waldiez.models.waldiez import Waldiez
22
+
23
+ from ..base_runner import WaldiezBaseRunner
24
+ from ..exceptions import StopRunningException
25
+ from ..run_results import WaldiezRunResults
26
+ from .breakpoints_mixin import BreakpointsMixin
27
+ from .step_by_step_models import (
28
+ HELP_MESSAGE,
29
+ VALID_CONTROL_COMMANDS,
30
+ WaldiezDebugError,
31
+ WaldiezDebugEventInfo,
32
+ WaldiezDebugInputRequest,
33
+ WaldiezDebugInputResponse,
34
+ WaldiezDebugMessage,
35
+ WaldiezDebugStats,
36
+ WaldiezDebugStepAction,
37
+ )
38
+
39
+ if TYPE_CHECKING:
40
+ from autogen.events import BaseEvent # type: ignore
41
+ from autogen.messages import BaseMessage # type: ignore
42
+
43
+
44
+ DEBUG_INPUT_PROMPT = (
45
+ "[Step] (c)ontinue, (r)un, (q)uit, (i)nfo, (h)elp, (st)ats: "
46
+ )
47
+ MESSAGES = {
48
+ "workflow_starting": "<Waldiez step-by-step> - Starting workflow...",
49
+ "workflow_finished": "<Waldiez step-by-step> - Workflow finished",
50
+ "workflow_stopped": "<Waldiez step-by-step> - Workflow stopped by user",
51
+ "workflow_failed": (
52
+ "<Waldiez step-by-step> - Workflow execution failed: {error}"
53
+ ),
54
+ }
55
+
56
+
57
+ # pylint: disable=too-many-instance-attributes
58
+ # noinspection DuplicatedCode,StrFormat
59
+ class WaldiezStepByStepRunner(WaldiezBaseRunner, BreakpointsMixin):
60
+ """Step-by-step runner with user interaction and debugging capabilities."""
61
+
62
+ def __init__(
63
+ self,
64
+ waldiez: Waldiez,
65
+ output_path: str | Path | None = None,
66
+ uploads_root: str | Path | None = None,
67
+ structured_io: bool = False,
68
+ dot_env: str | Path | None = None,
69
+ auto_continue: bool = False,
70
+ breakpoints: Iterable[str] | None = None,
71
+ **kwargs: Any,
72
+ ) -> None:
73
+ """Initialize the step-by-step runner."""
74
+ super().__init__(
75
+ waldiez,
76
+ output_path=output_path,
77
+ uploads_root=uploads_root,
78
+ structured_io=structured_io,
79
+ dot_env=dot_env,
80
+ **kwargs,
81
+ )
82
+ BreakpointsMixin.__init__(self)
83
+ self._event_count = 0
84
+ self._processed_events = 0
85
+ self._step_mode = True
86
+ self._auto_continue = auto_continue
87
+ # Initialize breakpoints from the mixin and set initial breakpoints
88
+ if breakpoints:
89
+ self.set_breakpoints(breakpoints)
90
+ self._event_history: list[dict[str, Any]] = []
91
+ self._current_event: Union["BaseEvent", "BaseMessage", None] = None
92
+ self._known_participants = self.waldiez.info.participants
93
+ self._last_sender: str | None = None
94
+ self._last_recipient: str | None = None
95
+
96
+ @property
97
+ def auto_continue(self) -> bool:
98
+ """Get whether auto-continue is enabled."""
99
+ return self._auto_continue
100
+
101
+ @auto_continue.setter
102
+ def auto_continue(self, value: bool) -> None:
103
+ """Set whether auto-continue is enabled.
104
+
105
+ Parameters
106
+ ----------
107
+ value : bool
108
+ Whether to enable auto-continue.
109
+ """
110
+ self._auto_continue = value
111
+
112
+ @property
113
+ def stop_requested(self) -> threading.Event:
114
+ """Get the stop requested event."""
115
+ return self._stop_requested
116
+
117
+ def set_auto_continue(self, auto_continue: bool) -> None:
118
+ """Set whether to automatically continue without user input.
119
+
120
+ Parameters
121
+ ----------
122
+ auto_continue : bool
123
+ Whether to automatically continue execution
124
+ without waiting for user input.
125
+ """
126
+ self._auto_continue = auto_continue
127
+ self.log.info("Auto-continue mode set to: %s", auto_continue)
128
+
129
+ # pylint: disable=no-self-use
130
+ # noinspection PyMethodMayBeStatic
131
+ def print(self, *args: Any, **kwargs: Any) -> None:
132
+ """Print.
133
+
134
+ Parameters
135
+ ----------
136
+ *args : Any
137
+ Positional arguments to print.
138
+ **kwargs : Any
139
+ Keyword arguments to print.
140
+ """
141
+ WaldiezBaseRunner._print(*args, **kwargs)
142
+
143
+ def emit_event(self, event: Union["BaseEvent", "BaseMessage"]) -> None:
144
+ """Emit an event.
145
+
146
+ Parameters
147
+ ----------
148
+ event : Union[BaseEvent, BaseMessage]
149
+ The event to emit.
150
+ """
151
+ event_info = event.model_dump(
152
+ mode="json", exclude_none=True, fallback=str
153
+ )
154
+ event_info["count"] = self._event_count
155
+ self._last_sender = getattr(event, "sender", self._last_sender)
156
+ self._last_recipient = getattr(event, "recipient", self._last_recipient)
157
+ event_info["sender"] = self._last_sender
158
+ event_info["recipient"] = self._last_recipient
159
+ self.emit(WaldiezDebugEventInfo(event=event_info))
160
+
161
+ # noinspection PyTypeHints
162
+ def emit(self, message: WaldiezDebugMessage) -> None:
163
+ """Emit a debug message.
164
+
165
+ Parameters
166
+ ----------
167
+ message : WaldiezDebugMessage
168
+ The debug message to emit.
169
+ """
170
+ message_dump = message.model_dump(
171
+ mode="json", exclude_none=True, fallback=str
172
+ )
173
+ self.print(message_dump)
174
+
175
+ def _show_event_info(self) -> None:
176
+ if not self._current_event:
177
+ return
178
+ event_info = self._current_event.model_dump(
179
+ mode="json", exclude_none=True, fallback=str
180
+ )
181
+ self.emit(WaldiezDebugEventInfo(event=event_info))
182
+
183
+ def _show_stats(self) -> None:
184
+ stats_dict: dict[str, Any] = {
185
+ "events_processed": self._processed_events,
186
+ "total_events": self._event_count,
187
+ "step_mode": self._step_mode,
188
+ "auto_continue": self._auto_continue,
189
+ "breakpoints": sorted(self.get_breakpoints()),
190
+ "event_history_count": len(self._event_history),
191
+ }
192
+ self.emit(WaldiezDebugStats(stats=stats_dict))
193
+
194
+ def _get_user_response(
195
+ self, user_response: str, request_id: str
196
+ ) -> tuple[str | None, bool]:
197
+ """Get user response for step-by-step execution.
198
+
199
+ Parameters
200
+ ----------
201
+ user_response : str
202
+ The user's response.
203
+ request_id : str
204
+ The ID of the input request.
205
+
206
+ Returns
207
+ -------
208
+ tuple[str | None, bool]
209
+ The user's response or None if invalid,
210
+ and a boolean indicating validity.
211
+ """
212
+ try:
213
+ response = WaldiezDebugInputResponse.model_validate_json(
214
+ user_response
215
+ )
216
+ except ValidationError as exc:
217
+ got = user_response.strip().lower()
218
+ # in cli mode, let's see if got raw response
219
+ # instead of a structured one
220
+ if got in VALID_CONTROL_COMMANDS:
221
+ return got, True
222
+ self.emit(WaldiezDebugError(error=f"Invalid input: {exc}"))
223
+ return None, False
224
+
225
+ if response.request_id != request_id:
226
+ self.emit(
227
+ WaldiezDebugError(
228
+ error=(
229
+ "Stale input received: "
230
+ f"{response.request_id} != {request_id}"
231
+ )
232
+ )
233
+ )
234
+ return None, False
235
+ return response.data, True
236
+
237
+ # pylint: disable=too-many-return-statements
238
+ def _parse_user_action( # noqa: C901
239
+ self, user_response: str, request_id: str
240
+ ) -> WaldiezDebugStepAction:
241
+ """Parse user action for step-by-step execution.
242
+
243
+ Parameters
244
+ ----------
245
+ user_response : str
246
+ The user's response.
247
+ request_id : str
248
+ The ID of the input request.
249
+
250
+ Returns
251
+ -------
252
+ WaldiezDebugStepAction
253
+ The action chosen by the user.
254
+ """
255
+ self.log.debug("Parsing user action... '%s'", user_response)
256
+ user_input, is_valid = self._get_user_response(
257
+ user_response, request_id=request_id
258
+ )
259
+ if not is_valid:
260
+ return WaldiezDebugStepAction.UNKNOWN
261
+ if not user_input:
262
+ return WaldiezDebugStepAction.CONTINUE
263
+ match user_input:
264
+ case "c":
265
+ self._step_mode = True
266
+ return WaldiezDebugStepAction.CONTINUE
267
+ case "s":
268
+ self._step_mode = True
269
+ return WaldiezDebugStepAction.STEP
270
+ case "r":
271
+ self._step_mode = False
272
+ return WaldiezDebugStepAction.RUN
273
+ case "q":
274
+ self._stop_requested.set()
275
+ return WaldiezDebugStepAction.QUIT
276
+ case "i":
277
+ self._show_event_info()
278
+ return WaldiezDebugStepAction.INFO
279
+ case "h":
280
+ self.emit(HELP_MESSAGE)
281
+ return WaldiezDebugStepAction.HELP
282
+ case "st":
283
+ self._show_stats()
284
+ return WaldiezDebugStepAction.STATS
285
+ case "ab":
286
+ if self._current_event and hasattr(self._current_event, "type"):
287
+ self.add_breakpoint(self._current_event.type)
288
+ else:
289
+ self.emit(
290
+ WaldiezDebugError(
291
+ error="No current event to add breakpoint for"
292
+ )
293
+ )
294
+ return WaldiezDebugStepAction.ADD_BREAKPOINT
295
+ case "rb":
296
+ if self._current_event and hasattr(self._current_event, "type"):
297
+ self.remove_breakpoint(self._current_event.type)
298
+ else:
299
+ self.emit(
300
+ WaldiezDebugError(
301
+ error="No current event to remove breakpoint for"
302
+ )
303
+ )
304
+ return WaldiezDebugStepAction.REMOVE_BREAKPOINT
305
+ case "lb":
306
+ self.list_breakpoints()
307
+ return WaldiezDebugStepAction.LIST_BREAKPOINTS
308
+ case "cb":
309
+ self.clear_breakpoints()
310
+ return WaldiezDebugStepAction.CLEAR_BREAKPOINTS
311
+ case _:
312
+ self.emit(
313
+ WaldiezDebugError(
314
+ error=f"Unknown command: {user_input}, use 'h' for help"
315
+ )
316
+ )
317
+ return WaldiezDebugStepAction.UNKNOWN
318
+
319
+ def _get_user_action(self) -> WaldiezDebugStepAction:
320
+ """Get user action for step-by-step execution.
321
+
322
+ Returns
323
+ -------
324
+ WaldiezDebugStepAction
325
+ The action chosen by the user.
326
+ """
327
+ if self._auto_continue:
328
+ return WaldiezDebugStepAction.CONTINUE
329
+
330
+ while True:
331
+ # pylint: disable=too-many-try-statements
332
+ request_id = str(uuid.uuid4())
333
+ try:
334
+ self.emit(
335
+ WaldiezDebugInputRequest(
336
+ prompt=DEBUG_INPUT_PROMPT, request_id=request_id
337
+ )
338
+ )
339
+ user_input = (
340
+ WaldiezBaseRunner.get_user_input(DEBUG_INPUT_PROMPT)
341
+ .strip()
342
+ .lower()
343
+ )
344
+ return self._parse_user_action(
345
+ user_input, request_id=request_id
346
+ )
347
+ except (KeyboardInterrupt, EOFError):
348
+ self._stop_requested.set()
349
+ return WaldiezDebugStepAction.QUIT
350
+
351
+ # pylint: disable=too-many-return-statements
352
+ async def _a_get_user_action(self) -> WaldiezDebugStepAction:
353
+ """Get user action for step-by-step execution asynchronously.
354
+
355
+ Returns
356
+ -------
357
+ WaldiezDebugStepAction
358
+ The action chosen by the user.
359
+ """
360
+ if self._auto_continue:
361
+ return WaldiezDebugStepAction.CONTINUE
362
+
363
+ while True:
364
+ # pylint: disable=too-many-try-statements
365
+ request_id = str(uuid.uuid4())
366
+ try:
367
+ self.emit(
368
+ WaldiezDebugInputRequest(
369
+ prompt=DEBUG_INPUT_PROMPT, request_id=request_id
370
+ )
371
+ )
372
+ user_input = await WaldiezBaseRunner.a_get_user_input(
373
+ DEBUG_INPUT_PROMPT
374
+ )
375
+ user_input = user_input.strip().lower()
376
+ return self._parse_user_action(
377
+ user_input, request_id=request_id
378
+ )
379
+ except (KeyboardInterrupt, EOFError):
380
+ return WaldiezDebugStepAction.QUIT
381
+
382
+ def _handle_step_interaction(self) -> bool:
383
+ """Handle step-by-step user interaction.
384
+
385
+ Returns
386
+ -------
387
+ bool
388
+ True to continue execution, False to stop.
389
+ """
390
+ while True: # pragma: no branch
391
+ action = self._get_user_action()
392
+
393
+ if action in (
394
+ WaldiezDebugStepAction.CONTINUE,
395
+ WaldiezDebugStepAction.STEP,
396
+ ):
397
+ return True
398
+ if action == WaldiezDebugStepAction.RUN:
399
+ return True
400
+ if action == WaldiezDebugStepAction.QUIT: # pragma: no branch
401
+ return False
402
+
403
+ async def _a_handle_step_interaction(self) -> bool:
404
+ """Handle step-by-step user interaction asynchronously.
405
+
406
+ Returns
407
+ -------
408
+ bool
409
+ True to continue execution, False to stop.
410
+ """
411
+ while True: # pragma: no branch
412
+ action = await self._a_get_user_action()
413
+
414
+ if action in (
415
+ WaldiezDebugStepAction.CONTINUE,
416
+ WaldiezDebugStepAction.STEP,
417
+ ):
418
+ return True
419
+ if action == WaldiezDebugStepAction.RUN:
420
+ return True
421
+ if action == WaldiezDebugStepAction.QUIT: # pragma: no branch
422
+ return False
423
+
424
+ # pylint: disable=unused-argument
425
+ def _run(
426
+ self,
427
+ temp_dir: Path,
428
+ output_file: Path,
429
+ uploads_root: Path | None,
430
+ skip_mmd: bool,
431
+ skip_timeline: bool,
432
+ **kwargs: Any,
433
+ ) -> list[dict[str, Any]]:
434
+ """Run the Waldiez workflow with step-by-step debugging."""
435
+ # pylint: disable=import-outside-toplevel
436
+ from autogen.io import IOStream # type: ignore
437
+
438
+ from waldiez.io import StructuredIOStream
439
+
440
+ results_container: WaldiezRunResults = {
441
+ "results": [],
442
+ "exception": None,
443
+ "completed": False,
444
+ }
445
+ # pylint: disable=too-many-try-statements,broad-exception-caught
446
+ try:
447
+ loaded_module = self._load_module(output_file, temp_dir)
448
+ if self._stop_requested.is_set(): # pragma: no cover
449
+ self.log.debug(
450
+ "Step-by-step execution stopped before workflow start"
451
+ )
452
+ return []
453
+
454
+ # noinspection DuplicatedCode
455
+ if self.structured_io:
456
+ stream = StructuredIOStream(
457
+ uploads_root=uploads_root, is_async=False
458
+ )
459
+ else:
460
+ stream = IOStream.get_default()
461
+
462
+ WaldiezBaseRunner._print = stream.print
463
+ WaldiezBaseRunner._input = stream.input
464
+ WaldiezBaseRunner._send = stream.send
465
+
466
+ self.print(MESSAGES["workflow_starting"])
467
+ self.print(self.waldiez.info.model_dump_json())
468
+
469
+ results = loaded_module.main(on_event=self._on_event)
470
+ results_container["results"] = results
471
+ self.print(MESSAGES["workflow_finished"])
472
+
473
+ except Exception as e:
474
+ if StopRunningException.reason in str(e):
475
+ raise StopRunningException(StopRunningException.reason) from e
476
+ results_container["exception"] = e
477
+ traceback.print_exc()
478
+ # noinspection StrFormat
479
+ self.print(MESSAGES["workflow_failed"].format(error=str(e)))
480
+ finally:
481
+ results_container["completed"] = True
482
+
483
+ return results_container["results"]
484
+
485
+ def _on_event(self, event: Union["BaseEvent", "BaseMessage"]) -> bool:
486
+ """Process an event with step-by-step debugging.
487
+
488
+ Parameters
489
+ ----------
490
+ event : Union[BaseEvent, BaseMessage]
491
+ The event to process.
492
+
493
+ Returns
494
+ -------
495
+ bool
496
+ True to continue, False to stop.
497
+
498
+ Raises
499
+ ------
500
+ RuntimeError
501
+ If an error occurs while processing the event.
502
+ StopRunningException
503
+ If execution is stopped by user request.
504
+ """
505
+ self._event_count += 1
506
+ self._current_event = event
507
+
508
+ if self._stop_requested.is_set():
509
+ self.log.debug(
510
+ "Step-by-step execution stopped before event processing"
511
+ )
512
+ return False
513
+
514
+ # Store event in history
515
+ event_info = event.model_dump(
516
+ mode="json", exclude_none=True, fallback=str
517
+ )
518
+ event_info["count"] = self._event_count
519
+ self._event_history.append(event_info)
520
+
521
+ # pylint: disable=too-many-try-statements
522
+ try:
523
+ # Show event information if we should break
524
+ if self.should_break_on_event(
525
+ event, self._step_mode
526
+ ): # pragma: no branch
527
+ self.emit_event(event)
528
+ if not self._handle_step_interaction():
529
+ self._stop_requested.set()
530
+ if hasattr(event, "type") and event.type == "input_request":
531
+ event.content.respond("exit")
532
+ return True
533
+ raise StopRunningException(StopRunningException.reason)
534
+
535
+ # Process the event
536
+ WaldiezBaseRunner.process_event(event)
537
+ self._processed_events += 1
538
+
539
+ except Exception as e:
540
+ if not isinstance(e, StopRunningException):
541
+ raise RuntimeError(
542
+ f"Error processing event {event}: "
543
+ f"{e}\n{traceback.format_exc()}"
544
+ ) from e
545
+ raise StopRunningException(StopRunningException.reason) from e
546
+ return not self._stop_requested.is_set()
547
+
548
+ # pylint: disable=too-complex
549
+ async def _a_run(
550
+ self,
551
+ temp_dir: Path,
552
+ output_file: Path,
553
+ uploads_root: Path | None,
554
+ skip_mmd: bool = False,
555
+ skip_timeline: bool = False,
556
+ **kwargs: Any,
557
+ ) -> list[dict[str, Any]]:
558
+ """Run the Waldiez workflow with step-by-step debugging (async)."""
559
+
560
+ async def _execute_workflow() -> list[dict[str, Any]]:
561
+ """Execute the workflow in an async context."""
562
+ # pylint: disable=import-outside-toplevel
563
+ from autogen.io import IOStream # pyright: ignore
564
+
565
+ from waldiez.io import StructuredIOStream
566
+
567
+ results: list[dict[str, Any]]
568
+ # pylint: disable=too-many-try-statements,broad-exception-caught
569
+ try:
570
+ loaded_module = self._load_module(output_file, temp_dir)
571
+ if self._stop_requested.is_set(): # pragma: no cover
572
+ self.log.debug(
573
+ "step-by-step execution stopped before workflow start"
574
+ )
575
+ return []
576
+
577
+ if self.structured_io:
578
+ stream = StructuredIOStream(
579
+ uploads_root=uploads_root, is_async=True
580
+ )
581
+ else:
582
+ stream = IOStream.get_default()
583
+
584
+ WaldiezBaseRunner._print = stream.print
585
+ WaldiezBaseRunner._input = stream.input
586
+ WaldiezBaseRunner._send = stream.send
587
+
588
+ self.print(MESSAGES["workflow_starting"])
589
+ self.print(self.waldiez.info.model_dump_json())
590
+
591
+ results = await loaded_module.main(on_event=self._a_on_event)
592
+ self.print(MESSAGES["workflow_finished"])
593
+
594
+ except Exception as e:
595
+ if StopRunningException.reason in str(e):
596
+ raise StopRunningException(
597
+ StopRunningException.reason
598
+ ) from e
599
+ self.print(MESSAGES["workflow_failed"].format(error=str(e)))
600
+ traceback.print_exc()
601
+ return []
602
+
603
+ return results
604
+
605
+ # Create cancellable task
606
+ task = asyncio.create_task(_execute_workflow())
607
+
608
+ # Monitor for stop requests
609
+ # pylint: disable=too-many-try-statements
610
+ try:
611
+ while not task.done():
612
+ if self._stop_requested.is_set():
613
+ task.cancel()
614
+ self.log.debug("Step-by-step execution stopped by user")
615
+ break
616
+ await asyncio.sleep(0.1)
617
+ return await task
618
+
619
+ except asyncio.CancelledError:
620
+ self.log.debug("Step-by-step execution cancelled")
621
+ return []
622
+
623
+ async def _a_on_event(
624
+ self, event: Union["BaseEvent", "BaseMessage"]
625
+ ) -> bool:
626
+ """Process an event with step-by-step debugging asynchronously.
627
+
628
+ Parameters
629
+ ----------
630
+ event : Union[BaseEvent, BaseMessage]
631
+ The event to process.
632
+
633
+ Returns
634
+ -------
635
+ bool
636
+ True to continue, False to stop.
637
+
638
+ Raises
639
+ ------
640
+ RuntimeError
641
+ If an error occurs while processing the event.
642
+ StopRunningException
643
+ If execution is stopped by user request.
644
+ """
645
+ self._event_count += 1
646
+ self._current_event = event
647
+
648
+ if self._stop_requested.is_set():
649
+ self.log.debug(
650
+ "Async step-by-step execution stopped before event processing"
651
+ )
652
+ return False
653
+
654
+ # Store event in history
655
+ event_info = event.model_dump(
656
+ mode="json", exclude_none=True, fallback=str
657
+ )
658
+ event_info["count"] = self._event_count
659
+ self._event_history.append(event_info)
660
+
661
+ # pylint: disable=too-many-try-statements
662
+ try:
663
+ # Show event information if we should break
664
+ if self.should_break_on_event(
665
+ event, self._step_mode
666
+ ): # pragma: no branch
667
+ self.emit_event(event)
668
+
669
+ # Handle step interaction
670
+ if not await self._a_handle_step_interaction():
671
+ self._stop_requested.set()
672
+ if hasattr(event, "type") and event.type == "input_request":
673
+ event.content.respond("exit")
674
+ return True
675
+ raise StopRunningException(StopRunningException.reason)
676
+
677
+ # Process the event
678
+ await WaldiezBaseRunner.a_process_event(event)
679
+ self._processed_events += 1
680
+
681
+ except Exception as e:
682
+ if not isinstance(e, StopRunningException):
683
+ raise RuntimeError(
684
+ f"Error processing event {event}: "
685
+ f"{e}\n{traceback.format_exc()}"
686
+ ) from e
687
+ raise StopRunningException(StopRunningException.reason) from e
688
+ return not self._stop_requested.is_set()
689
+
690
+ def get_execution_stats(self) -> dict[str, Any]:
691
+ """Get execution statistics for step-by-step runner.
692
+
693
+ Returns
694
+ -------
695
+ dict[str, Any]
696
+ A dictionary containing execution statistics.
697
+ """
698
+ return {
699
+ "total_events": self._event_count,
700
+ "processed_events": self._processed_events,
701
+ "event_processing_rate": (
702
+ self._processed_events / self._event_count
703
+ if self._event_count > 0
704
+ else 0
705
+ ),
706
+ "step_mode": self._step_mode,
707
+ "auto_continue": self._auto_continue,
708
+ "breakpoints": sorted(self.get_breakpoints()),
709
+ "event_history_count": len(self._event_history),
710
+ "last_sender": self._last_sender,
711
+ "last_recipient": self._last_recipient,
712
+ "known_participants": [
713
+ p.model_dump() for p in self._known_participants
714
+ ],
715
+ }
716
+
717
+ def get_event_history(self) -> list[dict[str, Any]]:
718
+ """Get the history of processed events.
719
+
720
+ Returns
721
+ -------
722
+ list[dict[str, Any]]
723
+ List of event information dictionaries.
724
+ """
725
+ return self._event_history.copy()
726
+
727
+ def enable_auto_continue(self, enabled: bool = True) -> None:
728
+ """Enable or disable auto-continue mode.
729
+
730
+ Parameters
731
+ ----------
732
+ enabled : bool, optional
733
+ Whether to enable auto-continue, by default True.
734
+ """
735
+ self._auto_continue = enabled
736
+
737
+ def enable_step_mode(self, enabled: bool = True) -> None:
738
+ """Enable or disable step mode.
739
+
740
+ Parameters
741
+ ----------
742
+ enabled : bool, optional
743
+ Whether to enable step mode, by default True.
744
+ """
745
+ self._step_mode = enabled