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,512 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+ # pylint: disable=unused-argument
4
+ """Breakpoints management mixin for step-by-step debugging."""
5
+
6
+ import logging
7
+ from functools import lru_cache
8
+ from typing import TYPE_CHECKING, Any, Callable, Iterable, Union
9
+
10
+ from .step_by_step_models import (
11
+ WaldiezBreakpoint,
12
+ WaldiezDebugBreakpointAdded,
13
+ WaldiezDebugBreakpointCleared,
14
+ WaldiezDebugBreakpointRemoved,
15
+ WaldiezDebugBreakpointsList,
16
+ WaldiezDebugError,
17
+ WaldiezDebugMessage,
18
+ )
19
+
20
+ if TYPE_CHECKING:
21
+ from autogen.events import BaseEvent # type: ignore
22
+ from autogen.messages import BaseMessage # type: ignore
23
+
24
+
25
+ def handle_breakpoint_errors(func: Callable[..., bool]) -> Callable[..., bool]:
26
+ """Handle breakpoint-related errors.
27
+
28
+ Parameters
29
+ ----------
30
+ func : Callable
31
+ The function to decorate.
32
+
33
+ Returns
34
+ -------
35
+ Callable
36
+ The decorated function.
37
+ """
38
+
39
+ def _wrapper(self: "BreakpointsMixin", *args: Any, **kwargs: Any) -> bool:
40
+ try:
41
+ return func(self, *args, **kwargs)
42
+ except ValueError as e:
43
+ self.emit(WaldiezDebugError(error=f"Breakpoint error: {e}"))
44
+ return False
45
+ except Exception as e: # pylint: disable=broad-exception-caught
46
+ self.emit(
47
+ WaldiezDebugError(
48
+ error=f"Unexpected error in {func.__name__}: {e}"
49
+ )
50
+ )
51
+ logging.exception("Error in %s", func.__name__)
52
+ return False
53
+
54
+ return _wrapper
55
+
56
+
57
+ class BreakpointsMixin:
58
+ """Mixin class for managing breakpoints in step-by-step debugging."""
59
+
60
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
61
+ """Initialize breakpoints storage."""
62
+ self._breakpoints: set[WaldiezBreakpoint] = set()
63
+
64
+ # Statistics for monitoring
65
+ self._breakpoint_stats = {
66
+ "total_matches": 0,
67
+ "cache_hits": 0,
68
+ "cache_misses": 0,
69
+ }
70
+
71
+ # Create the cached function with proper binding
72
+ self._check_breakpoint_match_cached = lru_cache(maxsize=1000)(
73
+ self._check_breakpoint_match_impl
74
+ )
75
+
76
+ # noinspection PyTypeHints
77
+ def emit(self, message: WaldiezDebugMessage) -> None:
78
+ """Emit a debug message. Implemented by the class using this mixin.
79
+
80
+ Parameters
81
+ ----------
82
+ message : WaldiezDebugMessage
83
+ The debug message to emit.
84
+ """
85
+ raise NotImplementedError("emit method must be implemented")
86
+
87
+ def _invalidate_cache(self) -> None:
88
+ """Invalidate the event matching cache when breakpoints change."""
89
+ self._check_breakpoint_match_cached.cache_clear()
90
+
91
+ def _get_breakpoints_signature(self) -> frozenset[str]:
92
+ """Get a hashable signature of current breakpoints."""
93
+ return frozenset(str(bp) for bp in self._breakpoints)
94
+
95
+ def _check_breakpoint_match_impl(
96
+ self,
97
+ event_type: str,
98
+ sender: str,
99
+ recipient: str,
100
+ breakpoints_sig: frozenset[str],
101
+ ) -> bool:
102
+ """Check if the event matches any breakpoints.
103
+
104
+ Parameters
105
+ ----------
106
+ event_type : str
107
+ The event type to check.
108
+ sender : str
109
+ The event sender.
110
+ recipient : str
111
+ The event recipient.
112
+ breakpoints_sig : frozenset[str]
113
+ Signature of current breakpoints for cache invalidation.
114
+
115
+ Returns
116
+ -------
117
+ bool
118
+ True if any breakpoint matches, False otherwise.
119
+ """
120
+ event_dict = {
121
+ "type": event_type,
122
+ "sender": sender,
123
+ "recipient": recipient,
124
+ }
125
+
126
+ # Reconstruct breakpoints from signature for cache safety
127
+ # noinspection PyBroadException
128
+ try:
129
+ breakpoints = {
130
+ WaldiezBreakpoint.from_string(bp_str)
131
+ for bp_str in breakpoints_sig
132
+ }
133
+ return any(bp.matches(event_dict) for bp in breakpoints)
134
+ except Exception: # pylint: disable=broad-exception-caught
135
+ # Fallback to current breakpoints if signature is malformed
136
+ return any(bp.matches(event_dict) for bp in self._breakpoints)
137
+
138
+ @handle_breakpoint_errors
139
+ def add_breakpoint(self, spec: str) -> bool:
140
+ """Add a breakpoint for an event type.
141
+
142
+ Parameters
143
+ ----------
144
+ spec : str
145
+ The event type specification to add a breakpoint for.
146
+
147
+ Returns
148
+ -------
149
+ bool
150
+ True if the breakpoint was added successfully, False otherwise.
151
+ """
152
+ if not spec or not isinstance(spec, str): # pyright: ignore
153
+ self.emit(
154
+ WaldiezDebugError(
155
+ error="Invalid event type: must be a non-empty string"
156
+ )
157
+ )
158
+ return False
159
+ # pylint: disable=too-many-try-statements
160
+ try:
161
+ breakpoint_obj = WaldiezBreakpoint.from_string(spec)
162
+
163
+ # Check if breakpoint already exists
164
+ if breakpoint_obj in self._breakpoints:
165
+ self.emit(
166
+ WaldiezDebugError(
167
+ error=f"Breakpoint for '{spec}' already exists"
168
+ )
169
+ )
170
+ return False
171
+
172
+ self._breakpoints.add(breakpoint_obj)
173
+ self._invalidate_cache()
174
+ self.emit(WaldiezDebugBreakpointAdded(breakpoint=spec))
175
+ return True
176
+
177
+ except ValueError as e:
178
+ self.emit(
179
+ WaldiezDebugError(error=f"Invalid breakpoint format: {e}")
180
+ )
181
+ return False
182
+
183
+ @handle_breakpoint_errors
184
+ def remove_breakpoint(self, spec: str | WaldiezBreakpoint) -> bool:
185
+ """Remove a breakpoint based on its specification.
186
+
187
+ Parameters
188
+ ----------
189
+ spec : str | WaldiezBreakpoint
190
+ The event type to remove the breakpoint for.
191
+
192
+ Returns
193
+ -------
194
+ bool
195
+ True if the breakpoint was removed, False if it didn't exist.
196
+ """
197
+ if isinstance(spec, WaldiezBreakpoint):
198
+ breakpoint_obj = spec
199
+ spec_str = str(spec)
200
+ elif isinstance(spec, str) and spec: # pyright: ignore
201
+ try:
202
+ breakpoint_obj = WaldiezBreakpoint.from_string(spec)
203
+ spec_str = spec
204
+ except ValueError as e:
205
+ self.emit(
206
+ WaldiezDebugError(error=f"Invalid breakpoint format: {e}")
207
+ )
208
+ return False
209
+ else:
210
+ self.emit(
211
+ WaldiezDebugError(error="Invalid breakpoint specification")
212
+ )
213
+ return False
214
+
215
+ if breakpoint_obj in self._breakpoints:
216
+ self._breakpoints.remove(breakpoint_obj)
217
+ self._invalidate_cache()
218
+ self.emit(WaldiezDebugBreakpointRemoved(breakpoint=spec_str))
219
+ return True
220
+
221
+ self.emit(
222
+ WaldiezDebugError(
223
+ error=f"Breakpoint for '{spec_str}' does not exist"
224
+ )
225
+ )
226
+ return False
227
+
228
+ def list_breakpoints(self) -> None:
229
+ """List all current breakpoints."""
230
+ breakpoints_list = sorted(self._breakpoints, key=str)
231
+ self.emit(WaldiezDebugBreakpointsList(breakpoints=breakpoints_list))
232
+
233
+ def clear_breakpoints(self) -> None:
234
+ """Clear all breakpoints."""
235
+ count = len(self._breakpoints)
236
+ self._breakpoints.clear()
237
+ self._invalidate_cache()
238
+
239
+ if count > 0:
240
+ self.emit(
241
+ WaldiezDebugBreakpointCleared(
242
+ message=f"Cleared {count} breakpoint(s)"
243
+ )
244
+ )
245
+ else:
246
+ self.emit(
247
+ WaldiezDebugBreakpointCleared(message="No breakpoints to clear")
248
+ )
249
+
250
+ @handle_breakpoint_errors
251
+ def set_breakpoints(self, specs: Iterable[str | WaldiezBreakpoint]) -> bool:
252
+ """Set which breakpoints to activate.
253
+
254
+ Parameters
255
+ ----------
256
+ specs : Iterable[str | WaldiezBreakpoint]
257
+ Iterable of event types to break on. Empty means no breakpoints.
258
+
259
+ Returns
260
+ -------
261
+ bool
262
+ True if all breakpoints were set successfully, False if any failed.
263
+ """
264
+ new_breakpoints: set[WaldiezBreakpoint] = set()
265
+ errors: list[str] = []
266
+
267
+ for spec in specs:
268
+ try:
269
+ if isinstance(spec, WaldiezBreakpoint):
270
+ new_breakpoints.add(spec)
271
+ else:
272
+ new_breakpoints.add(WaldiezBreakpoint.from_string(spec))
273
+ except ValueError as e:
274
+ errors.append(f"Invalid breakpoint '{spec}': {e}")
275
+
276
+ if errors:
277
+ for error in errors:
278
+ self.emit(WaldiezDebugError(error=error))
279
+ return False
280
+
281
+ old_count = len(self._breakpoints)
282
+ self._breakpoints = new_breakpoints
283
+ new_count = len(self._breakpoints)
284
+ self._invalidate_cache()
285
+
286
+ self.emit(
287
+ WaldiezDebugBreakpointCleared(
288
+ message=f"Updated breakpoints: {old_count} -> {new_count}"
289
+ )
290
+ )
291
+ return True
292
+
293
+ def get_breakpoints(self) -> set[WaldiezBreakpoint]:
294
+ """Get current breakpoints.
295
+
296
+ Returns
297
+ -------
298
+ set[WaldiezBreakpoint]
299
+ Set of current breakpoint event types.
300
+ """
301
+ return self._breakpoints.copy()
302
+
303
+ def has_breakpoint(self, spec: str | WaldiezBreakpoint) -> bool:
304
+ """Check if a breakpoint exists.
305
+
306
+ Parameters
307
+ ----------
308
+ spec : str | WaldiezBreakpoint
309
+ The breakpoint specification to check.
310
+
311
+ Returns
312
+ -------
313
+ bool
314
+ True if a breakpoint exists for this event type.
315
+ """
316
+ try:
317
+ if isinstance(spec, WaldiezBreakpoint):
318
+ return spec in self._breakpoints
319
+ return WaldiezBreakpoint.from_string(spec) in self._breakpoints
320
+ except ValueError:
321
+ return False
322
+
323
+ def should_break_on_event(
324
+ self, event: Union["BaseEvent", "BaseMessage"], step_mode: bool = True
325
+ ) -> bool:
326
+ """Determine if we should break on this event.
327
+
328
+ Parameters
329
+ ----------
330
+ event : Union[BaseEvent, BaseMessage]
331
+ The event to check.
332
+ step_mode : bool, optional
333
+ Whether step mode is enabled, by default True.
334
+
335
+ Returns
336
+ -------
337
+ bool
338
+ True if we should break, False otherwise.
339
+ """
340
+ # Get event type
341
+ event_type = getattr(event, "type", "unknown")
342
+
343
+ # Don't break on input requests - they're handled separately
344
+ if event_type == "input_request":
345
+ return False
346
+
347
+ # Quick path: if no breakpoints and not in step mode, don't break
348
+ if not self._breakpoints and not step_mode:
349
+ return False
350
+
351
+ # Quick path: if step mode and no specific breakpoints,
352
+ # break on everything
353
+ if step_mode and not self._breakpoints:
354
+ return True
355
+
356
+ # Check if this event matches any breakpoint using caching
357
+ if hasattr(event, "model_dump"):
358
+ # pylint: disable=too-many-try-statements,broad-exception-caught
359
+ try:
360
+ event_dict = event.model_dump(
361
+ mode="python", exclude_none=True, fallback=str
362
+ )
363
+
364
+ # Extract event details for cache key
365
+ event_type_key = event_dict.get("type", "unknown")
366
+ sender = event_dict.get("sender", "")
367
+ recipient = event_dict.get("recipient", "")
368
+
369
+ # Get current breakpoints signature for cache invalidation
370
+ breakpoints_sig = self._get_breakpoints_signature()
371
+
372
+ # Check cached result
373
+ # noinspection PyBroadException
374
+ try:
375
+ matches_breakpoint = self._check_breakpoint_match_cached(
376
+ event_type_key, sender, recipient, breakpoints_sig
377
+ )
378
+ self._breakpoint_stats["cache_hits"] += 1
379
+ except Exception:
380
+ # Fallback to non-cached check
381
+ matches_breakpoint = any(
382
+ bp.matches(event_dict) for bp in self._breakpoints
383
+ )
384
+ self._breakpoint_stats["cache_misses"] += 1
385
+
386
+ self._breakpoint_stats["total_matches"] += 1
387
+
388
+ # If any breakpoint matches: break regardless of step_mode
389
+ if matches_breakpoint:
390
+ return True
391
+
392
+ except Exception as e: # pylint: disable=broad-exception-caught
393
+ logging.warning("Error processing event for breakpoints: %s", e)
394
+ self._breakpoint_stats["cache_misses"] += 1
395
+
396
+ # No specific breakpoints matched:
397
+ # - If step_mode, break on every event (single-step behavior)
398
+ # - If not step_mode, do not break
399
+ return bool(step_mode)
400
+
401
+ def get_breakpoint_stats(self) -> dict[str, Any]:
402
+ """Get breakpoint statistics including performance metrics.
403
+
404
+ Returns
405
+ -------
406
+ dict[str, Any]
407
+ Dictionary containing breakpoint statistics.
408
+ """
409
+ breakpoints: list[dict[str, Any]] = [
410
+ {
411
+ "type": bp.type.value,
412
+ "event_type": bp.event_type,
413
+ "agent_name": bp.agent_name,
414
+ "description": bp.description,
415
+ "string_repr": str(bp),
416
+ }
417
+ for bp in self._breakpoints
418
+ ]
419
+
420
+ # Calculate cache efficiency
421
+ total_checks = (
422
+ self._breakpoint_stats["cache_hits"]
423
+ + self._breakpoint_stats["cache_misses"]
424
+ )
425
+ cache_hit_rate = (
426
+ self._breakpoint_stats["cache_hits"] / total_checks
427
+ if total_checks > 0
428
+ else 0
429
+ )
430
+
431
+ # Get cache info from lru_cache
432
+ cache_info = self._check_breakpoint_match_cached.cache_info()
433
+
434
+ return {
435
+ "total_breakpoints": len(self._breakpoints),
436
+ "breakpoints": breakpoints,
437
+ "has_breakpoints": len(self._breakpoints) > 0,
438
+ "cache_stats": {
439
+ "cache_hit_rate": f"{cache_hit_rate:.2%}",
440
+ "cache_size": cache_info.currsize,
441
+ "cache_maxsize": cache_info.maxsize,
442
+ "total_matches": self._breakpoint_stats["total_matches"],
443
+ "cache_hits": cache_info.hits,
444
+ "cache_misses": cache_info.misses,
445
+ },
446
+ "performance": {
447
+ "lru_cache_info": {
448
+ "hits": cache_info.hits,
449
+ "misses": cache_info.misses,
450
+ "maxsize": cache_info.maxsize,
451
+ "currsize": cache_info.currsize,
452
+ }
453
+ },
454
+ }
455
+
456
+ def reset_stats(self) -> None:
457
+ """Reset breakpoint statistics."""
458
+ self._breakpoint_stats = {
459
+ "total_matches": 0,
460
+ "cache_hits": 0,
461
+ "cache_misses": 0,
462
+ }
463
+ self._invalidate_cache()
464
+
465
+ def optimize_cache(self) -> None:
466
+ """Manually optimize the cache by clearing it."""
467
+ self._invalidate_cache()
468
+
469
+ def export_breakpoints(self) -> list[str]:
470
+ """Export breakpoints as a list of strings for persistence.
471
+
472
+ Returns
473
+ -------
474
+ list[str]
475
+ List of breakpoint specifications as strings.
476
+ """
477
+ return [str(bp) for bp in sorted(self._breakpoints, key=str)]
478
+
479
+ def import_breakpoints(
480
+ self, breakpoint_specs: list[str]
481
+ ) -> tuple[int, list[str]]:
482
+ """Import breakpoints from a list of string specifications.
483
+
484
+ Parameters
485
+ ----------
486
+ breakpoint_specs : list[str]
487
+ List of breakpoint specifications as strings.
488
+
489
+ Returns
490
+ -------
491
+ tuple[int, list[str]]
492
+ Tuple of (successful_imports, error_messages).
493
+ """
494
+ successful = 0
495
+ errors: list[str] = []
496
+
497
+ for spec in breakpoint_specs:
498
+ # pylint: disable=too-many-try-statements
499
+ try:
500
+ breakpoint_obj = WaldiezBreakpoint.from_string(spec)
501
+ if breakpoint_obj not in self._breakpoints:
502
+ self._breakpoints.add(breakpoint_obj)
503
+ successful += 1
504
+ else:
505
+ errors.append(f"Breakpoint '{spec}' already exists")
506
+ except ValueError as e:
507
+ errors.append(f"Invalid breakpoint '{spec}': {e}")
508
+
509
+ if successful > 0:
510
+ self._invalidate_cache()
511
+
512
+ return successful, errors
@@ -0,0 +1,151 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+ # pylint: disable=unused-argument
4
+ """Command handler for step-by-step execution."""
5
+
6
+ from typing import TYPE_CHECKING, Callable
7
+
8
+ from .step_by_step_models import (
9
+ HELP_MESSAGE,
10
+ WaldiezDebugError,
11
+ WaldiezDebugStepAction,
12
+ )
13
+
14
+ if TYPE_CHECKING:
15
+ # noinspection PyUnusedImports
16
+ from .step_by_step_runner import WaldiezStepByStepRunner
17
+
18
+
19
+ # pylint: disable=too-few-public-methods
20
+ class CommandHandler:
21
+ """Handler for debug commands to reduce complexity in main runner."""
22
+
23
+ def __init__(self, runner: "WaldiezStepByStepRunner"):
24
+ self.runner = runner
25
+ self._command_map: dict[
26
+ str, Callable[[str | None], WaldiezDebugStepAction]
27
+ ] = {
28
+ "c": self._handle_continue,
29
+ "s": self._handle_step,
30
+ "r": self._handle_run,
31
+ "q": self._handle_quit,
32
+ "i": self._handle_info,
33
+ "h": self._handle_help,
34
+ "st": self._handle_stats,
35
+ "ab": self._handle_add_breakpoint,
36
+ "rb": self._handle_remove_breakpoint,
37
+ "lb": self._handle_list_breakpoints,
38
+ "cb": self._handle_clear_breakpoints,
39
+ }
40
+
41
+ def handle_command(self, command_line: str) -> WaldiezDebugStepAction:
42
+ """Handle a command line input.
43
+
44
+ Parameters
45
+ ----------
46
+ command_line : str
47
+ The command line input to handle.
48
+
49
+ Returns
50
+ -------
51
+ WaldiezDebugStepAction
52
+ The action to take for the command.
53
+ """
54
+ if not command_line or not command_line.strip():
55
+ return self._handle_step(None) # Enter = step
56
+ parts = command_line.strip().split(maxsplit=1)
57
+ if not parts or not parts[0]:
58
+ return self._handle_unknown(None)
59
+ command = parts[0].lower()
60
+ args = parts[1] if len(parts) > 1 else None
61
+
62
+ handler = self._command_map.get(command, self._handle_unknown)
63
+ return handler(args)
64
+
65
+ def _handle_continue(self, args: str | None) -> WaldiezDebugStepAction:
66
+ self.runner.step_mode = True
67
+ return WaldiezDebugStepAction.CONTINUE
68
+
69
+ def _handle_step(self, args: str | None) -> WaldiezDebugStepAction:
70
+ self.runner.step_mode = True
71
+ return WaldiezDebugStepAction.STEP
72
+
73
+ def _handle_run(self, args: str | None) -> WaldiezDebugStepAction:
74
+ self.runner.step_mode = False
75
+ return WaldiezDebugStepAction.RUN
76
+
77
+ def _handle_quit(self, args: str | None) -> WaldiezDebugStepAction:
78
+ self.runner.set_stop_requested()
79
+ return WaldiezDebugStepAction.QUIT
80
+
81
+ def _handle_info(self, args: str | None) -> WaldiezDebugStepAction:
82
+ self.runner.show_event_info()
83
+ return WaldiezDebugStepAction.INFO
84
+
85
+ def _handle_help(self, args: str | None) -> WaldiezDebugStepAction:
86
+ self.runner.emit(HELP_MESSAGE)
87
+ return WaldiezDebugStepAction.HELP
88
+
89
+ def _handle_stats(self, args: str | None) -> WaldiezDebugStepAction:
90
+ self.runner.show_stats()
91
+ return WaldiezDebugStepAction.STATS
92
+
93
+ def _handle_add_breakpoint(
94
+ self, args: str | None
95
+ ) -> WaldiezDebugStepAction:
96
+ if args:
97
+ self.runner.add_breakpoint(args)
98
+ return WaldiezDebugStepAction.ADD_BREAKPOINT
99
+ current_event = self.runner.current_event
100
+ if current_event and hasattr(current_event, "type"):
101
+ self.runner.add_breakpoint(getattr(current_event, "type", ""))
102
+ return WaldiezDebugStepAction.ADD_BREAKPOINT
103
+ # else:
104
+ self.runner.emit(
105
+ WaldiezDebugError(
106
+ error=(
107
+ "No breakpoint specification provided "
108
+ "and no current event available"
109
+ )
110
+ )
111
+ )
112
+ return WaldiezDebugStepAction.ADD_BREAKPOINT
113
+
114
+ def _handle_remove_breakpoint(
115
+ self, args: str | None
116
+ ) -> WaldiezDebugStepAction:
117
+ if args:
118
+ self.runner.remove_breakpoint(args)
119
+ return WaldiezDebugStepAction.REMOVE_BREAKPOINT
120
+ current_event = self.runner.current_event
121
+ if current_event and hasattr(current_event, "type"):
122
+ self.runner.remove_breakpoint(getattr(current_event, "type", ""))
123
+ return WaldiezDebugStepAction.REMOVE_BREAKPOINT
124
+ # else:
125
+ self.runner.emit(
126
+ WaldiezDebugError(
127
+ error=(
128
+ "No breakpoint specification provided and "
129
+ "no current event available"
130
+ )
131
+ )
132
+ )
133
+ return WaldiezDebugStepAction.REMOVE_BREAKPOINT
134
+
135
+ def _handle_list_breakpoints(
136
+ self, args: str | None
137
+ ) -> WaldiezDebugStepAction:
138
+ self.runner.list_breakpoints()
139
+ return WaldiezDebugStepAction.LIST_BREAKPOINTS
140
+
141
+ def _handle_clear_breakpoints(
142
+ self, args: str | None
143
+ ) -> WaldiezDebugStepAction:
144
+ self.runner.clear_breakpoints()
145
+ return WaldiezDebugStepAction.CLEAR_BREAKPOINTS
146
+
147
+ def _handle_unknown(self, args: str | None) -> WaldiezDebugStepAction:
148
+ self.runner.emit(
149
+ WaldiezDebugError(error="Unknown command. Use 'h' for help")
150
+ )
151
+ return WaldiezDebugStepAction.UNKNOWN