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
@@ -0,0 +1,750 @@
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
+ from collections import deque
16
+ from pathlib import Path
17
+ from typing import TYPE_CHECKING, Any, Iterable, Union
18
+
19
+ from pydantic import ValidationError
20
+
21
+ from waldiez.io.utils import DEBUG_INPUT_PROMPT, gen_id
22
+ from waldiez.models.waldiez import Waldiez
23
+ from waldiez.running.step_by_step.command_handler import CommandHandler
24
+ from waldiez.running.step_by_step.events_processor import EventProcessor
25
+
26
+ from ..base_runner import WaldiezBaseRunner
27
+ from ..exceptions import StopRunningException
28
+ from ..run_results import WaldiezRunResults
29
+ from .breakpoints_mixin import BreakpointsMixin
30
+ from .step_by_step_models import (
31
+ VALID_CONTROL_COMMANDS,
32
+ WaldiezDebugConfig,
33
+ WaldiezDebugError,
34
+ WaldiezDebugEventInfo,
35
+ WaldiezDebugInputRequest,
36
+ WaldiezDebugInputResponse,
37
+ WaldiezDebugMessage,
38
+ WaldiezDebugStats,
39
+ WaldiezDebugStepAction,
40
+ )
41
+
42
+ if TYPE_CHECKING:
43
+ from autogen.events import BaseEvent # type: ignore
44
+ from autogen.messages import BaseMessage # type: ignore
45
+
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
+ """Refactored step-by-step runner with improved architecture."""
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
+ config: WaldiezDebugConfig | None = None,
72
+ **kwargs: Any,
73
+ ) -> None:
74
+ """Initialize the step-by-step runner."""
75
+ super().__init__(
76
+ waldiez,
77
+ output_path=output_path,
78
+ uploads_root=uploads_root,
79
+ structured_io=structured_io,
80
+ dot_env=dot_env,
81
+ **kwargs,
82
+ )
83
+ BreakpointsMixin.__init__(self)
84
+
85
+ # Configuration
86
+ self._config = config or WaldiezDebugConfig()
87
+ self._config.auto_continue = auto_continue
88
+
89
+ # Core state
90
+ self._event_count = 0
91
+ self._processed_events = 0
92
+ self._step_mode = self._config.step_mode
93
+
94
+ # Use deque for efficient FIFO operations on event history
95
+ self._event_history: deque[dict[str, Any]] = deque(
96
+ maxlen=self._config.max_event_history
97
+ )
98
+ self._current_event: Union["BaseEvent", "BaseMessage", None] = None
99
+
100
+ # Participant tracking
101
+ self._known_participants = self.waldiez.info.participants
102
+ self._last_sender: str | None = None
103
+ self._last_recipient: str | None = None
104
+
105
+ # Initialize breakpoints
106
+ if breakpoints:
107
+ _, errors = self.import_breakpoints(list(breakpoints))
108
+ if errors:
109
+ for error in errors:
110
+ self.log.warning("Breakpoint import error: %s", error)
111
+
112
+ # Command handling
113
+ self._command_handler = CommandHandler(self)
114
+ self._event_processor = EventProcessor(self)
115
+
116
+ @property
117
+ def auto_continue(self) -> bool:
118
+ """Get whether auto-continue is enabled."""
119
+ return self._config.auto_continue
120
+
121
+ @auto_continue.setter
122
+ def auto_continue(self, value: bool) -> None:
123
+ """Set whether auto-continue is enabled.
124
+
125
+ Parameters
126
+ ----------
127
+ value : bool
128
+ Whether to enable auto-continue.
129
+ """
130
+ self._config.auto_continue = value
131
+ self.log.debug("Auto-continue mode set to: %s", value)
132
+
133
+ @property
134
+ def step_mode(self) -> bool:
135
+ """Get the step mode.
136
+
137
+ Returns
138
+ -------
139
+ bool
140
+ Whether the step mode is enabled.
141
+ """
142
+ return self._step_mode
143
+
144
+ @step_mode.setter
145
+ def step_mode(self, value: bool) -> None:
146
+ """Set the step mode.
147
+
148
+ Parameters
149
+ ----------
150
+ value : bool
151
+ Whether to enable step mode.
152
+ """
153
+ self._step_mode = value
154
+ self.log.debug("Step mode set to: %s", value)
155
+
156
+ @property
157
+ def last_sender(self) -> str | None:
158
+ """Get the last sender.
159
+
160
+ Returns
161
+ -------
162
+ str | None
163
+ The last sender, if available.
164
+ """
165
+ return self._last_sender
166
+
167
+ @last_sender.setter
168
+ def last_sender(self, value: str | None) -> None:
169
+ """Set the last sender.
170
+
171
+ Parameters
172
+ ----------
173
+ value : str | None
174
+ The last sender to set.
175
+ """
176
+ self._last_sender = value
177
+
178
+ @property
179
+ def last_recipient(self) -> str | None:
180
+ """Get the last recipient.
181
+
182
+ Returns
183
+ -------
184
+ str | None
185
+ The last recipient, if available.
186
+ """
187
+ return self._last_recipient
188
+
189
+ @last_recipient.setter
190
+ def last_recipient(self, value: str | None) -> None:
191
+ """Set the last recipient.
192
+
193
+ Parameters
194
+ ----------
195
+ value : str | None
196
+ The last recipient to set.
197
+ """
198
+ self._last_recipient = value
199
+
200
+ @property
201
+ def stop_requested(self) -> threading.Event:
202
+ """Get the stop requested event."""
203
+ return self._stop_requested
204
+
205
+ @property
206
+ def max_event_history(self) -> int:
207
+ """Get the maximum event history size."""
208
+ return self._config.max_event_history
209
+
210
+ @staticmethod
211
+ def print(*args: Any, **kwargs: Any) -> None:
212
+ """Print method.
213
+
214
+ Parameters
215
+ ----------
216
+ *args : Any
217
+ Positional arguments to print.
218
+ **kwargs : Any
219
+ Keyword arguments to print.
220
+ """
221
+ WaldiezBaseRunner.print(*args, **kwargs)
222
+
223
+ def add_to_history(self, event_info: dict[str, Any]) -> None:
224
+ """Add an event to the history.
225
+
226
+ Parameters
227
+ ----------
228
+ event_info : dict[str, Any]
229
+ The event information to add to the history.
230
+ """
231
+ self._event_history.append(event_info)
232
+
233
+ def pop_event(self) -> None:
234
+ """Pop event from the history."""
235
+ if self._event_history:
236
+ self._event_history.popleft()
237
+
238
+ def emit_event(
239
+ self, event: Union["BaseEvent", "BaseMessage", dict[str, Any]]
240
+ ) -> None:
241
+ """Emit an event.
242
+
243
+ Parameters
244
+ ----------
245
+ event : BaseEvent | BaseMessage | dict[str, Any]
246
+ The event to emit.
247
+ """
248
+ if not isinstance(event, dict):
249
+ event_info = event.model_dump(
250
+ mode="json", exclude_none=True, fallback=str
251
+ )
252
+ event_info["count"] = self._event_count
253
+ event_info["sender"] = getattr(event, "sender", self._last_sender)
254
+ event_info["recipient"] = getattr(
255
+ event, "recipient", self._last_recipient
256
+ )
257
+ else:
258
+ event_info = event
259
+ self.emit(WaldiezDebugEventInfo(event=event_info))
260
+
261
+ # noinspection PyTypeHints
262
+ def emit(self, message: WaldiezDebugMessage) -> None:
263
+ """Emit a debug message.
264
+
265
+ Parameters
266
+ ----------
267
+ message : WaldiezDebugMessage
268
+ The message to emit.
269
+ """
270
+ message_dump = message.model_dump(
271
+ mode="json", exclude_none=True, fallback=str
272
+ )
273
+ self.print(message_dump)
274
+
275
+ @property
276
+ def current_event(self) -> Union["BaseEvent", "BaseMessage", None]:
277
+ """Get the current event.
278
+
279
+ Returns
280
+ -------
281
+ Union["BaseEvent", "BaseMessage", None]
282
+ The current event, if available.
283
+ """
284
+ return self._current_event
285
+
286
+ @current_event.setter
287
+ def current_event(
288
+ self, value: Union["BaseEvent", "BaseMessage", None]
289
+ ) -> None:
290
+ """Set the current event.
291
+
292
+ Parameters
293
+ ----------
294
+ value : Union["BaseEvent", "BaseMessage", None]
295
+ The event to set as the current event.
296
+ """
297
+ self._current_event = value
298
+
299
+ @property
300
+ def event_count(self) -> int:
301
+ """Get the current event count.
302
+
303
+ Returns
304
+ -------
305
+ int
306
+ The current event count.
307
+ """
308
+ return self._event_count
309
+
310
+ def event_plus_one(self) -> None:
311
+ """Increment the current event count."""
312
+ self._event_count += 1
313
+
314
+ def show_event_info(self) -> None:
315
+ """Show detailed information about the current event."""
316
+ if not self._current_event:
317
+ self.emit(WaldiezDebugError(error="No current event to display"))
318
+ return
319
+
320
+ event_info = self._current_event.model_dump(
321
+ mode="json", exclude_none=True, fallback=str
322
+ )
323
+ # Add additional context
324
+ event_info["_meta"] = {
325
+ "event_number": self._event_count,
326
+ "processed_events": self._processed_events,
327
+ "step_mode": self._step_mode,
328
+ "has_breakpoints": len(self._breakpoints) > 0,
329
+ }
330
+ self.emit(WaldiezDebugEventInfo(event=event_info))
331
+
332
+ def show_stats(self) -> None:
333
+ """Show comprehensive execution statistics."""
334
+ base_stats: dict[str, Any] = {
335
+ "execution": {
336
+ "events_processed": self._processed_events,
337
+ "total_events": self._event_count,
338
+ "processing_rate": (
339
+ f"{(self._processed_events / self._event_count * 100):.1f}%"
340
+ if self._event_count > 0
341
+ else "0%"
342
+ ),
343
+ },
344
+ "mode": {
345
+ "step_mode": self._step_mode,
346
+ "auto_continue": self._config.auto_continue,
347
+ },
348
+ "history": {
349
+ "event_history_count": len(self._event_history),
350
+ "max_history_size": self._config.max_event_history,
351
+ "memory_usage": f"{len(self._event_history) * 200}B (est.)",
352
+ },
353
+ "participants": {
354
+ "last_sender": self._last_sender,
355
+ "last_recipient": self._last_recipient,
356
+ "known_participants": len(self._known_participants),
357
+ },
358
+ }
359
+
360
+ # Merge with breakpoint stats
361
+ breakpoint_stats = self.get_breakpoint_stats()
362
+ stats_dict: dict[str, Any] = {
363
+ **base_stats,
364
+ "breakpoints": breakpoint_stats,
365
+ }
366
+
367
+ self.emit(WaldiezDebugStats(stats=stats_dict))
368
+
369
+ @property
370
+ def execution_stats(self) -> dict[str, Any]:
371
+ """Get comprehensive execution statistics.
372
+
373
+ Returns
374
+ -------
375
+ dict[str, Any]
376
+ A dictionary containing execution statistics.
377
+ """
378
+ base_stats: dict[str, Any] = {
379
+ "total_events": self._event_count,
380
+ "processed_events": self._processed_events,
381
+ "event_processing_rate": (
382
+ self._processed_events / self._event_count
383
+ if self._event_count > 0
384
+ else 0
385
+ ),
386
+ "step_mode": self._step_mode,
387
+ "auto_continue": self._config.auto_continue,
388
+ "event_history_count": len(self._event_history),
389
+ "last_sender": self._last_sender,
390
+ "last_recipient": self._last_recipient,
391
+ "known_participants": [
392
+ p.model_dump() for p in self._known_participants
393
+ ],
394
+ "config": self._config.model_dump(),
395
+ }
396
+
397
+ return {**base_stats, "breakpoints": self.get_breakpoint_stats()}
398
+
399
+ @property
400
+ def event_history(self) -> list[dict[str, Any]]:
401
+ """Get the history of processed events.
402
+
403
+ Returns
404
+ -------
405
+ list[dict[str, Any]]
406
+ A list of dictionaries containing event history.
407
+ """
408
+ return list(self._event_history)
409
+
410
+ def reset_session(self) -> None:
411
+ """Reset the debugging session state."""
412
+ self._event_count = 0
413
+ self._processed_events = 0
414
+ self._event_history.clear()
415
+ self._current_event = None
416
+ self._last_sender = None
417
+ self._last_recipient = None
418
+ self.reset_stats()
419
+ self.log.info("Debug session reset")
420
+
421
+ def _get_user_response(
422
+ self,
423
+ user_response: str,
424
+ request_id: str,
425
+ skip_id_check: bool = False,
426
+ ) -> tuple[str | None, bool]:
427
+ """Get and validate user response."""
428
+ try:
429
+ response = WaldiezDebugInputResponse.model_validate_json(
430
+ user_response
431
+ )
432
+ except ValidationError as exc:
433
+ # Handle raw CLI input
434
+ got = user_response.strip().lower()
435
+ if got in VALID_CONTROL_COMMANDS:
436
+ return got, True
437
+ self.emit(WaldiezDebugError(error=f"Invalid input: {exc}"))
438
+ return None, False
439
+
440
+ if not skip_id_check and response.request_id != request_id:
441
+ self.emit(
442
+ WaldiezDebugError(
443
+ error=f"Stale input received: {response.request_id} != {request_id}"
444
+ )
445
+ )
446
+ return None, False
447
+
448
+ return response.data, True
449
+
450
+ def _parse_user_action(
451
+ self, user_response: str, request_id: str
452
+ ) -> WaldiezDebugStepAction:
453
+ """Parse user action using the command handler."""
454
+ self.log.debug("Parsing user action... '%s'", user_response)
455
+
456
+ user_input, is_valid = self._get_user_response(
457
+ user_response,
458
+ request_id=request_id,
459
+ skip_id_check=True,
460
+ )
461
+ if not is_valid:
462
+ return WaldiezDebugStepAction.UNKNOWN
463
+
464
+ return self._command_handler.handle_command(user_input or "")
465
+
466
+ def _get_user_action(self) -> WaldiezDebugStepAction:
467
+ """Get user action with timeout support."""
468
+ if self._config.auto_continue:
469
+ self.step_mode = True
470
+ return WaldiezDebugStepAction.CONTINUE
471
+
472
+ while True:
473
+ request_id = gen_id()
474
+ try:
475
+ if not self.structured_io:
476
+ self.emit(
477
+ WaldiezDebugInputRequest(
478
+ prompt=DEBUG_INPUT_PROMPT, request_id=request_id
479
+ )
480
+ )
481
+ except Exception as e: # pylint: disable=broad-exception-caught
482
+ self.log.warning("Failed to emit input request: %s", e)
483
+ try:
484
+ user_input = WaldiezBaseRunner.get_user_input(
485
+ DEBUG_INPUT_PROMPT
486
+ ).strip()
487
+ return self._parse_user_action(
488
+ user_input, request_id=request_id
489
+ )
490
+
491
+ except (KeyboardInterrupt, EOFError):
492
+ self._stop_requested.set()
493
+ return WaldiezDebugStepAction.QUIT
494
+
495
+ async def _a_get_user_action(self) -> WaldiezDebugStepAction:
496
+ """Get user action asynchronously."""
497
+ if self._config.auto_continue:
498
+ self.step_mode = True
499
+ return WaldiezDebugStepAction.CONTINUE
500
+
501
+ while True:
502
+ request_id = gen_id()
503
+ # pylint: disable=too-many-try-statements
504
+ try:
505
+ self.emit(
506
+ WaldiezDebugInputRequest(
507
+ prompt=DEBUG_INPUT_PROMPT, request_id=request_id
508
+ )
509
+ )
510
+
511
+ user_input = await WaldiezBaseRunner.a_get_user_input(
512
+ DEBUG_INPUT_PROMPT
513
+ )
514
+ user_input = user_input.strip()
515
+ return self._parse_user_action(
516
+ user_input, request_id=request_id
517
+ )
518
+
519
+ except (KeyboardInterrupt, EOFError):
520
+ return WaldiezDebugStepAction.QUIT
521
+
522
+ def _handle_step_interaction(self) -> bool:
523
+ """Handle step-by-step user interaction."""
524
+ while True:
525
+ action = self._get_user_action()
526
+ if action in (
527
+ WaldiezDebugStepAction.CONTINUE,
528
+ WaldiezDebugStepAction.STEP,
529
+ ):
530
+ return True
531
+ if action == WaldiezDebugStepAction.RUN:
532
+ return True
533
+ if action == WaldiezDebugStepAction.QUIT:
534
+ return False
535
+ # For other actions (info, help, etc.), continue the loop
536
+
537
+ async def _a_handle_step_interaction(self) -> bool:
538
+ """Handle step-by-step user interaction asynchronously."""
539
+ while True:
540
+ action = await self._a_get_user_action()
541
+ if action in (
542
+ WaldiezDebugStepAction.CONTINUE,
543
+ WaldiezDebugStepAction.STEP,
544
+ ):
545
+ return True
546
+ if action == WaldiezDebugStepAction.RUN:
547
+ return True
548
+ if action == WaldiezDebugStepAction.QUIT:
549
+ return False
550
+ # For other actions (info, help, etc.), continue the loop
551
+
552
+ def _run(
553
+ self,
554
+ temp_dir: Path,
555
+ output_file: Path,
556
+ uploads_root: Path | None,
557
+ skip_mmd: bool,
558
+ skip_timeline: bool,
559
+ **kwargs: Any,
560
+ ) -> list[dict[str, Any]]:
561
+ """Run the Waldiez workflow with step-by-step debugging."""
562
+ # pylint: disable=import-outside-toplevel
563
+ from autogen.io import IOStream # type: ignore
564
+
565
+ from waldiez.io import StructuredIOStream
566
+
567
+ results_container: WaldiezRunResults = {
568
+ "results": [],
569
+ "exception": None,
570
+ "completed": False,
571
+ }
572
+ # pylint: disable=too-many-try-statements,broad-exception-caught
573
+ try:
574
+ loaded_module = self._load_module(output_file, temp_dir)
575
+ if self._stop_requested.is_set():
576
+ self.log.debug(
577
+ "Step-by-step execution stopped before workflow start"
578
+ )
579
+ return []
580
+
581
+ # Setup I/O
582
+ if self.structured_io:
583
+ stream = StructuredIOStream(
584
+ uploads_root=uploads_root, is_async=False
585
+ )
586
+ else:
587
+ stream = IOStream.get_default()
588
+
589
+ WaldiezBaseRunner._print = stream.print
590
+ WaldiezBaseRunner._input = stream.input
591
+ WaldiezBaseRunner._send = stream.send
592
+
593
+ self.print(MESSAGES["workflow_starting"])
594
+ self.print(self.waldiez.info.model_dump_json())
595
+
596
+ results = loaded_module.main(on_event=self._on_event)
597
+ results_container["results"] = results
598
+ self.print(MESSAGES["workflow_finished"])
599
+
600
+ except Exception as e:
601
+ if StopRunningException.reason in str(e):
602
+ raise StopRunningException(StopRunningException.reason) from e
603
+ results_container["exception"] = e
604
+ traceback.print_exc()
605
+ self.print(MESSAGES["workflow_failed"].format(error=str(e)))
606
+ finally:
607
+ results_container["completed"] = True
608
+
609
+ return results_container["results"]
610
+
611
+ def _on_event(self, event: Union["BaseEvent", "BaseMessage"]) -> bool:
612
+ """Process an event with step-by-step debugging."""
613
+ # pylint: disable=too-many-try-statements,broad-exception-caught
614
+ try:
615
+ # Use the event processor for core logic
616
+ result = self._event_processor.process_event(event)
617
+
618
+ if result["action"] == "stop":
619
+ self.log.debug(
620
+ "Step-by-step execution stopped before event processing"
621
+ )
622
+ return False
623
+ self.emit_event(result["event_info"])
624
+ # Handle breakpoint logic
625
+ if result["should_break"]:
626
+ if not self._handle_step_interaction():
627
+ self._stop_requested.set()
628
+ if hasattr(event, "type") and event.type == "input_request":
629
+ event.content.respond("exit")
630
+ return True
631
+ raise StopRunningException(StopRunningException.reason)
632
+
633
+ # Process the actual event
634
+ WaldiezBaseRunner.process_event(event, skip_send=True)
635
+ self._processed_events += 1
636
+
637
+ except Exception as e:
638
+ if not isinstance(e, StopRunningException):
639
+ raise RuntimeError(
640
+ f"Error processing event {event}: {e}\n{traceback.format_exc()}"
641
+ ) from e
642
+ raise StopRunningException(StopRunningException.reason) from e
643
+
644
+ return not self._stop_requested.is_set()
645
+
646
+ # pylint: disable=too-complex
647
+ async def _a_run(
648
+ self,
649
+ temp_dir: Path,
650
+ output_file: Path,
651
+ uploads_root: Path | None,
652
+ skip_mmd: bool = False,
653
+ skip_timeline: bool = False,
654
+ **kwargs: Any,
655
+ ) -> list[dict[str, Any]]:
656
+ """Run the Waldiez workflow with step-by-step debugging (async)."""
657
+
658
+ async def _execute_workflow() -> list[dict[str, Any]]:
659
+ # pylint: disable=import-outside-toplevel
660
+ from autogen.io import IOStream # pyright: ignore
661
+
662
+ from waldiez.io import StructuredIOStream
663
+
664
+ # pylint: disable=too-many-try-statements,broad-exception-caught
665
+ try:
666
+ loaded_module = self._load_module(output_file, temp_dir)
667
+ if self._stop_requested.is_set():
668
+ self.log.debug(
669
+ "Step-by-step execution stopped before workflow start"
670
+ )
671
+ return []
672
+
673
+ if self.structured_io:
674
+ stream = StructuredIOStream(
675
+ uploads_root=uploads_root, is_async=True
676
+ )
677
+ else:
678
+ stream = IOStream.get_default()
679
+
680
+ WaldiezBaseRunner._print = stream.print
681
+ WaldiezBaseRunner._input = stream.input
682
+ WaldiezBaseRunner._send = stream.send
683
+
684
+ self.print(MESSAGES["workflow_starting"])
685
+ self.print(self.waldiez.info.model_dump_json())
686
+
687
+ results = await loaded_module.main(on_event=self._a_on_event)
688
+ self.print(MESSAGES["workflow_finished"])
689
+ return results
690
+
691
+ except Exception as e:
692
+ if StopRunningException.reason in str(e):
693
+ raise StopRunningException(
694
+ StopRunningException.reason
695
+ ) from e
696
+ self.print(MESSAGES["workflow_failed"].format(error=str(e)))
697
+ traceback.print_exc()
698
+ return []
699
+
700
+ # Create and monitor cancellable task
701
+ task = asyncio.create_task(_execute_workflow())
702
+ # pylint: disable=too-many-try-statements,broad-exception-caught
703
+ try:
704
+ while not task.done():
705
+ if self._stop_requested.is_set():
706
+ task.cancel()
707
+ self.log.debug("Step-by-step execution stopped by user")
708
+ break
709
+ await asyncio.sleep(0.1)
710
+ return await task
711
+ except asyncio.CancelledError:
712
+ self.log.debug("Step-by-step execution cancelled")
713
+ return []
714
+
715
+ async def _a_on_event(
716
+ self, event: Union["BaseEvent", "BaseMessage"]
717
+ ) -> bool:
718
+ """Process an event with step-by-step debugging asynchronously."""
719
+ # pylint: disable=too-many-try-statements,broad-exception-caught
720
+ try:
721
+ # Use the event processor for core logic
722
+ result = self._event_processor.process_event(event)
723
+
724
+ if result["action"] == "stop":
725
+ self.log.debug(
726
+ "Async step-by-step execution stopped before event processing"
727
+ )
728
+ return False
729
+ self.emit_event(result["event_info"])
730
+ # Handle breakpoint logic
731
+ if result["should_break"]:
732
+ if not await self._a_handle_step_interaction():
733
+ self._stop_requested.set()
734
+ if hasattr(event, "type") and event.type == "input_request":
735
+ event.content.respond("exit")
736
+ return True
737
+ raise StopRunningException(StopRunningException.reason)
738
+
739
+ # Process the actual event
740
+ await WaldiezBaseRunner.a_process_event(event, skip_send=True)
741
+ self._processed_events += 1
742
+
743
+ except Exception as e:
744
+ if not isinstance(e, StopRunningException):
745
+ raise RuntimeError(
746
+ f"Error processing event {event}: {e}\n{traceback.format_exc()}"
747
+ ) from e
748
+ raise StopRunningException(StopRunningException.reason) from e
749
+
750
+ return not self._stop_requested.is_set()