waldiez 0.5.10__py3-none-any.whl → 0.6.1__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 (192) hide show
  1. waldiez/__init__.py +1 -1
  2. waldiez/_version.py +1 -1
  3. waldiez/cli.py +19 -7
  4. waldiez/cli_extras/jupyter.py +3 -0
  5. waldiez/cli_extras/runner.py +3 -1
  6. waldiez/cli_extras/studio.py +3 -1
  7. waldiez/exporter.py +9 -3
  8. waldiez/exporting/agent/exporter.py +15 -16
  9. waldiez/exporting/agent/extras/captain_agent_extras.py +6 -6
  10. waldiez/exporting/agent/extras/doc_agent_extras.py +6 -6
  11. waldiez/exporting/agent/extras/group_manager_agent_extas.py +40 -24
  12. waldiez/exporting/agent/extras/group_member_extras.py +6 -5
  13. waldiez/exporting/agent/extras/handoffs/after_work.py +2 -1
  14. waldiez/exporting/agent/extras/handoffs/available.py +2 -1
  15. waldiez/exporting/agent/extras/handoffs/condition.py +3 -2
  16. waldiez/exporting/agent/extras/handoffs/handoff.py +2 -1
  17. waldiez/exporting/agent/extras/handoffs/target.py +7 -4
  18. waldiez/exporting/agent/extras/rag/chroma_extras.py +27 -19
  19. waldiez/exporting/agent/extras/rag/mongo_extras.py +8 -8
  20. waldiez/exporting/agent/extras/rag/pgvector_extras.py +5 -5
  21. waldiez/exporting/agent/extras/rag/qdrant_extras.py +5 -4
  22. waldiez/exporting/agent/extras/rag/vector_db_extras.py +1 -1
  23. waldiez/exporting/agent/extras/rag_user_proxy_agent_extras.py +5 -7
  24. waldiez/exporting/agent/extras/reasoning_agent_extras.py +3 -5
  25. waldiez/exporting/agent/termination.py +1 -0
  26. waldiez/exporting/chats/exporter.py +4 -4
  27. waldiez/exporting/chats/processor.py +1 -2
  28. waldiez/exporting/chats/utils/common.py +89 -48
  29. waldiez/exporting/chats/utils/group.py +9 -9
  30. waldiez/exporting/chats/utils/nested.py +7 -7
  31. waldiez/exporting/chats/utils/sequential.py +1 -1
  32. waldiez/exporting/chats/utils/single.py +2 -2
  33. waldiez/exporting/core/constants.py +3 -1
  34. waldiez/exporting/core/content.py +7 -7
  35. waldiez/exporting/core/context.py +5 -3
  36. waldiez/exporting/core/exporter.py +5 -3
  37. waldiez/exporting/core/exporters.py +2 -2
  38. waldiez/exporting/core/extras/agent_extras/captain_extras.py +2 -2
  39. waldiez/exporting/core/extras/agent_extras/group_manager_extras.py +2 -2
  40. waldiez/exporting/core/extras/agent_extras/rag_user_extras.py +2 -2
  41. waldiez/exporting/core/extras/agent_extras/standard_extras.py +3 -8
  42. waldiez/exporting/core/extras/base.py +7 -5
  43. waldiez/exporting/core/extras/flow_extras.py +4 -5
  44. waldiez/exporting/core/extras/model_extras.py +2 -2
  45. waldiez/exporting/core/extras/path_resolver.py +1 -2
  46. waldiez/exporting/core/extras/serializer.py +13 -11
  47. waldiez/exporting/core/protocols.py +6 -5
  48. waldiez/exporting/core/result.py +25 -28
  49. waldiez/exporting/core/types.py +11 -10
  50. waldiez/exporting/core/utils/llm_config.py +4 -4
  51. waldiez/exporting/core/validation.py +10 -11
  52. waldiez/exporting/flow/execution_generator.py +99 -10
  53. waldiez/exporting/flow/exporter.py +2 -2
  54. waldiez/exporting/flow/factory.py +2 -2
  55. waldiez/exporting/flow/file_generator.py +4 -2
  56. waldiez/exporting/flow/merger.py +5 -3
  57. waldiez/exporting/flow/orchestrator.py +72 -2
  58. waldiez/exporting/flow/utils/common.py +6 -6
  59. waldiez/exporting/flow/utils/importing.py +7 -8
  60. waldiez/exporting/flow/utils/linting.py +25 -9
  61. waldiez/exporting/flow/utils/logging.py +5 -77
  62. waldiez/exporting/models/exporter.py +8 -8
  63. waldiez/exporting/models/processor.py +5 -5
  64. waldiez/exporting/tools/exporter.py +2 -2
  65. waldiez/exporting/tools/processor.py +7 -4
  66. waldiez/io/__init__.py +11 -5
  67. waldiez/io/_ws.py +12 -6
  68. waldiez/io/models/constants.py +10 -10
  69. waldiez/io/models/content/audio.py +1 -0
  70. waldiez/io/models/content/base.py +20 -18
  71. waldiez/io/models/content/file.py +1 -0
  72. waldiez/io/models/content/image.py +1 -0
  73. waldiez/io/models/content/text.py +1 -0
  74. waldiez/io/models/content/video.py +1 -0
  75. waldiez/io/models/user_input.py +10 -5
  76. waldiez/io/models/user_response.py +17 -16
  77. waldiez/io/mqtt.py +18 -31
  78. waldiez/io/redis.py +18 -22
  79. waldiez/io/structured.py +122 -70
  80. waldiez/io/utils.py +19 -10
  81. waldiez/io/ws.py +7 -3
  82. waldiez/logger.py +16 -3
  83. waldiez/models/agents/__init__.py +3 -0
  84. waldiez/models/agents/agent/agent.py +25 -17
  85. waldiez/models/agents/agent/agent_data.py +25 -22
  86. waldiez/models/agents/agent/code_execution.py +9 -11
  87. waldiez/models/agents/agent/termination_message.py +10 -12
  88. waldiez/models/agents/agent/update_system_message.py +2 -4
  89. waldiez/models/agents/agents.py +8 -8
  90. waldiez/models/agents/assistant/assistant.py +6 -3
  91. waldiez/models/agents/assistant/assistant_data.py +2 -2
  92. waldiez/models/agents/captain/captain_agent.py +7 -4
  93. waldiez/models/agents/captain/captain_agent_data.py +5 -7
  94. waldiez/models/agents/doc_agent/doc_agent.py +7 -4
  95. waldiez/models/agents/doc_agent/doc_agent_data.py +9 -10
  96. waldiez/models/agents/doc_agent/rag_query_engine.py +10 -12
  97. waldiez/models/agents/extra_requirements.py +3 -3
  98. waldiez/models/agents/group_manager/group_manager.py +12 -7
  99. waldiez/models/agents/group_manager/group_manager_data.py +13 -12
  100. waldiez/models/agents/group_manager/speakers.py +17 -19
  101. waldiez/models/agents/rag_user_proxy/rag_user_proxy.py +7 -4
  102. waldiez/models/agents/rag_user_proxy/rag_user_proxy_data.py +4 -1
  103. waldiez/models/agents/rag_user_proxy/retrieve_config.py +69 -63
  104. waldiez/models/agents/rag_user_proxy/vector_db_config.py +19 -19
  105. waldiez/models/agents/reasoning/reasoning_agent.py +7 -4
  106. waldiez/models/agents/reasoning/reasoning_agent_data.py +3 -2
  107. waldiez/models/agents/reasoning/reasoning_agent_reason_config.py +8 -8
  108. waldiez/models/agents/user_proxy/user_proxy.py +6 -3
  109. waldiez/models/agents/user_proxy/user_proxy_data.py +1 -1
  110. waldiez/models/chat/chat.py +28 -20
  111. waldiez/models/chat/chat_data.py +22 -21
  112. waldiez/models/chat/chat_message.py +9 -9
  113. waldiez/models/chat/chat_nested.py +9 -9
  114. waldiez/models/chat/chat_summary.py +6 -6
  115. waldiez/models/common/__init__.py +2 -0
  116. waldiez/models/common/ag2_version.py +2 -0
  117. waldiez/models/common/base.py +2 -0
  118. waldiez/models/common/dict_utils.py +8 -6
  119. waldiez/models/common/handoff.py +20 -17
  120. waldiez/models/common/method_utils.py +9 -7
  121. waldiez/models/common/naming.py +49 -0
  122. waldiez/models/flow/flow.py +11 -6
  123. waldiez/models/flow/flow_data.py +23 -17
  124. waldiez/models/flow/info.py +3 -3
  125. waldiez/models/flow/naming.py +2 -1
  126. waldiez/models/model/_aws.py +11 -13
  127. waldiez/models/model/_llm.py +8 -0
  128. waldiez/models/model/_price.py +2 -4
  129. waldiez/models/model/extra_requirements.py +1 -3
  130. waldiez/models/model/model.py +2 -2
  131. waldiez/models/model/model_data.py +21 -21
  132. waldiez/models/tool/extra_requirements.py +2 -4
  133. waldiez/models/tool/predefined/_duckduckgo.py +1 -0
  134. waldiez/models/tool/predefined/_email.py +4 -0
  135. waldiez/models/tool/predefined/_google.py +1 -0
  136. waldiez/models/tool/predefined/_perplexity.py +2 -1
  137. waldiez/models/tool/predefined/_searxng.py +2 -1
  138. waldiez/models/tool/predefined/_tavily.py +1 -0
  139. waldiez/models/tool/predefined/_wikipedia.py +2 -1
  140. waldiez/models/tool/predefined/_youtube.py +1 -0
  141. waldiez/models/tool/tool.py +8 -5
  142. waldiez/models/tool/tool_data.py +2 -2
  143. waldiez/models/waldiez.py +152 -4
  144. waldiez/runner.py +11 -5
  145. waldiez/running/async_utils.py +192 -0
  146. waldiez/running/base_runner.py +155 -241
  147. waldiez/running/dir_utils.py +52 -0
  148. waldiez/running/environment.py +10 -44
  149. waldiez/running/events_mixin.py +252 -0
  150. waldiez/running/exceptions.py +20 -0
  151. waldiez/running/gen_seq_diagram.py +18 -15
  152. waldiez/running/io_utils.py +216 -0
  153. waldiez/running/protocol.py +11 -5
  154. waldiez/running/requirements_mixin.py +65 -0
  155. waldiez/running/results_mixin.py +926 -0
  156. waldiez/running/standard_runner.py +24 -27
  157. waldiez/running/step_by_step/breakpoints_mixin.py +503 -47
  158. waldiez/running/step_by_step/command_handler.py +154 -0
  159. waldiez/running/step_by_step/events_processor.py +379 -0
  160. waldiez/running/step_by_step/step_by_step_models.py +425 -41
  161. waldiez/running/step_by_step/step_by_step_runner.py +437 -382
  162. waldiez/running/subprocess_runner/__base__.py +13 -8
  163. waldiez/running/subprocess_runner/_async_runner.py +6 -4
  164. waldiez/running/subprocess_runner/_sync_runner.py +11 -6
  165. waldiez/running/subprocess_runner/runner.py +48 -23
  166. waldiez/running/timeline_processor.py +1 -1
  167. waldiez/utils/__init__.py +2 -0
  168. waldiez/utils/conflict_checker.py +4 -4
  169. waldiez/utils/python_manager.py +415 -0
  170. waldiez/ws/__init__.py +8 -7
  171. waldiez/ws/_file_handler.py +18 -20
  172. waldiez/ws/_mock.py +75 -0
  173. waldiez/ws/cli.py +58 -10
  174. waldiez/ws/client_manager.py +77 -53
  175. waldiez/ws/errors.py +3 -0
  176. waldiez/ws/models.py +61 -53
  177. waldiez/ws/reloader.py +33 -4
  178. waldiez/ws/server.py +121 -52
  179. waldiez/ws/session_manager.py +8 -9
  180. waldiez/ws/session_stats.py +1 -1
  181. waldiez/ws/utils.py +33 -5
  182. {waldiez-0.5.10.dist-info → waldiez-0.6.1.dist-info}/METADATA +107 -109
  183. waldiez-0.6.1.dist-info/RECORD +254 -0
  184. waldiez/running/post_run.py +0 -180
  185. waldiez/running/pre_run.py +0 -159
  186. waldiez/running/run_results.py +0 -14
  187. waldiez/running/utils.py +0 -511
  188. waldiez-0.5.10.dist-info/RECORD +0 -248
  189. {waldiez-0.5.10.dist-info → waldiez-0.6.1.dist-info}/WHEEL +0 -0
  190. {waldiez-0.5.10.dist-info → waldiez-0.6.1.dist-info}/entry_points.txt +0 -0
  191. {waldiez-0.5.10.dist-info → waldiez-0.6.1.dist-info}/licenses/LICENSE +0 -0
  192. {waldiez-0.5.10.dist-info → waldiez-0.6.1.dist-info}/licenses/NOTICE.md +0 -0
@@ -1,15 +1,26 @@
1
1
  # SPDX-License-Identifier: Apache-2.0.
2
2
  # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+
3
4
  # pylint: disable=unused-argument
5
+ # pyright: reportDeprecated=false, reportMissingTypeStubs=false
6
+ # pyright: reportUnusedParameter=false, reportUnnecessaryIsInstance=false
7
+ # pyright: reportUnknownMemberType=false, reportUnknownVariableType=false
8
+ # pyright: reportUnknownArgumentType=false
9
+
4
10
  """Breakpoints management mixin for step-by-step debugging."""
5
11
 
6
- from typing import TYPE_CHECKING, Any, Iterable, Union
12
+ import logging
13
+ from collections.abc import Iterable
14
+ from functools import lru_cache
15
+ from typing import TYPE_CHECKING, Any, Callable, Union
7
16
 
8
17
  from .step_by_step_models import (
18
+ WaldiezBreakpoint,
9
19
  WaldiezDebugBreakpointAdded,
10
20
  WaldiezDebugBreakpointCleared,
11
21
  WaldiezDebugBreakpointRemoved,
12
22
  WaldiezDebugBreakpointsList,
23
+ WaldiezDebugConfig,
13
24
  WaldiezDebugError,
14
25
  WaldiezDebugMessage,
15
26
  )
@@ -19,13 +30,102 @@ if TYPE_CHECKING:
19
30
  from autogen.messages import BaseMessage # type: ignore
20
31
 
21
32
 
33
+ def handle_breakpoint_errors(func: Callable[..., bool]) -> Callable[..., bool]:
34
+ """Handle breakpoint-related errors.
35
+
36
+ Parameters
37
+ ----------
38
+ func : Callable
39
+ The function to decorate.
40
+
41
+ Returns
42
+ -------
43
+ Callable
44
+ The decorated function.
45
+ """
46
+
47
+ def _wrapper(self: "BreakpointsMixin", *args: Any, **kwargs: Any) -> bool:
48
+ try:
49
+ return func(self, *args, **kwargs)
50
+ except ValueError as e:
51
+ self.emit(WaldiezDebugError(error=f"Breakpoint error: {e}"))
52
+ return False
53
+ except Exception as e: # pylint: disable=broad-exception-caught
54
+ self.emit(
55
+ WaldiezDebugError(
56
+ error=f"Unexpected error in {func.__name__}: {e}"
57
+ )
58
+ )
59
+ logging.exception("Error in %s", func.__name__)
60
+ return False
61
+
62
+ return _wrapper
63
+
64
+
22
65
  class BreakpointsMixin:
23
66
  """Mixin class for managing breakpoints in step-by-step debugging."""
24
67
 
68
+ _breakpoints: set[WaldiezBreakpoint]
69
+ _agent_id_to_name: dict[str, str]
70
+ _config: WaldiezDebugConfig
71
+
25
72
  def __init__(self, *args: Any, **kwargs: Any) -> None:
26
73
  """Initialize breakpoints storage."""
27
- self._breakpoints: set[str] = set()
74
+ self._breakpoints = set()
75
+ self._agent_id_to_name = {}
76
+
77
+ # Statistics for monitoring
78
+ self._breakpoint_stats = {
79
+ "total_matches": 0,
80
+ "cache_hits": 0,
81
+ "cache_misses": 0,
82
+ }
28
83
 
84
+ # Create the cached function with proper binding
85
+ self._check_breakpoint_match_cached = lru_cache(maxsize=1000)(
86
+ self._check_breakpoint_match_impl
87
+ )
88
+ self._config = kwargs.get("config", WaldiezDebugConfig())
89
+
90
+ @staticmethod
91
+ def get_initial_breakpoints(
92
+ items: Iterable[Any],
93
+ ) -> set[WaldiezBreakpoint]:
94
+ """Get initial breakpoints.
95
+
96
+ Parameters
97
+ ----------
98
+ items : Iterable[Any]
99
+ The items to parse for getting the endpoints.
100
+
101
+ Returns
102
+ -------
103
+ set[WaldiezBreakpoint]
104
+ The parsed breakpoints.
105
+ """
106
+ breakpoints: set[WaldiezBreakpoint] = set()
107
+ for item in items:
108
+ if isinstance(item, str):
109
+ try:
110
+ entry = WaldiezBreakpoint.from_string(item)
111
+ breakpoints.add(entry)
112
+ except BaseException: # pylint: disable=broad-exception-caught
113
+ pass
114
+ elif isinstance(item, WaldiezBreakpoint):
115
+ breakpoints.add(item)
116
+ return breakpoints
117
+
118
+ def set_agent_id_to_name(self, mapping: dict[str, str]) -> None:
119
+ """Set the agent id to agent name mapping.
120
+
121
+ Parameters
122
+ ----------
123
+ mapping : dict[str, str]
124
+ The agent id to agent name mapping.
125
+ """
126
+ self._agent_id_to_name = mapping
127
+
128
+ # noinspection PyTypeHints
29
129
  def emit(self, message: WaldiezDebugMessage) -> None:
30
130
  """Emit a debug message. Implemented by the class using this mixin.
31
131
 
@@ -36,31 +136,126 @@ class BreakpointsMixin:
36
136
  """
37
137
  raise NotImplementedError("emit method must be implemented")
38
138
 
39
- def add_breakpoint(self, event_type: str) -> None:
40
- """Add a breakpoint for an event type.
139
+ def _invalidate_cache(self) -> None:
140
+ """Invalidate the event matching cache when breakpoints change."""
141
+ self._check_breakpoint_match_cached.cache_clear()
142
+
143
+ def _get_breakpoints_signature(self) -> frozenset[str]:
144
+ """Get a hashable signature of current breakpoints."""
145
+ return frozenset(str(bp) for bp in self._breakpoints)
146
+
147
+ def _check_breakpoint_match_impl(
148
+ self,
149
+ event_type: str,
150
+ sender: str,
151
+ recipient: str,
152
+ sender_only: bool,
153
+ breakpoints_sig: frozenset[str],
154
+ ) -> bool:
155
+ """Check if the event matches any breakpoints.
41
156
 
42
157
  Parameters
43
158
  ----------
44
159
  event_type : str
45
- The event type to add a breakpoint for.
160
+ The event type to check.
161
+ sender : str
162
+ The event sender.
163
+ recipient : str
164
+ The event recipient.
165
+ sender_only : bool
166
+ Only check for event's sender agent.
167
+ breakpoints_sig : frozenset[str]
168
+ Signature of current breakpoints for cache invalidation.
169
+
170
+ Returns
171
+ -------
172
+ bool
173
+ True if any breakpoint matches, False otherwise.
46
174
  """
47
- if not event_type or not isinstance(event_type, str): # pyright: ignore
175
+ event_dict = {
176
+ "type": event_type,
177
+ "sender": self._agent_id_to_name.get(sender, sender),
178
+ "recipient": self._agent_id_to_name.get(recipient, recipient),
179
+ }
180
+
181
+ # Reconstruct breakpoints from signature for cache safety
182
+ # noinspection PyBroadException
183
+ try:
184
+ breakpoints = {
185
+ WaldiezBreakpoint.from_string(bp_str)
186
+ for bp_str in breakpoints_sig
187
+ }
188
+ return any(
189
+ bp.matches(
190
+ event_dict,
191
+ self._agent_id_to_name,
192
+ sender_only=sender_only,
193
+ )
194
+ for bp in breakpoints
195
+ )
196
+ except Exception: # pylint: disable=broad-exception-caught
197
+ # Fallback to current breakpoints if signature is malformed
198
+ return any(
199
+ bp.matches(
200
+ event_dict,
201
+ self._agent_id_to_name,
202
+ sender_only=sender_only,
203
+ )
204
+ for bp in self._breakpoints
205
+ )
206
+
207
+ @handle_breakpoint_errors
208
+ def add_breakpoint(self, spec: str) -> bool:
209
+ """Add a breakpoint for an event type.
210
+
211
+ Parameters
212
+ ----------
213
+ spec : str
214
+ The event type specification to add a breakpoint for.
215
+
216
+ Returns
217
+ -------
218
+ bool
219
+ True if the breakpoint was added successfully, False otherwise.
220
+ """
221
+ if not spec or not isinstance(spec, str):
48
222
  self.emit(
49
223
  WaldiezDebugError(
50
224
  error="Invalid event type: must be a non-empty string"
51
225
  )
52
226
  )
53
- return
227
+ return False
228
+ # pylint: disable=too-many-try-statements
229
+ try:
230
+ breakpoint_obj = WaldiezBreakpoint.from_string(spec)
231
+
232
+ # Check if breakpoint already exists
233
+ if breakpoint_obj in self._breakpoints:
234
+ self.emit(
235
+ WaldiezDebugError(
236
+ error=f"Breakpoint for '{spec}' already exists"
237
+ )
238
+ )
239
+ return False
240
+
241
+ self._breakpoints.add(breakpoint_obj)
242
+ self._invalidate_cache()
243
+ self.emit(WaldiezDebugBreakpointAdded(breakpoint=spec))
244
+ return True
54
245
 
55
- self._breakpoints.add(event_type)
56
- self.emit(WaldiezDebugBreakpointAdded(breakpoint=event_type))
246
+ except ValueError as e:
247
+ self.emit(
248
+ WaldiezDebugError(error=f"Invalid breakpoint format: {e}")
249
+ )
250
+ return False
57
251
 
58
- def remove_breakpoint(self, event_type: str) -> bool:
59
- """Remove a breakpoint for an event type.
252
+ @handle_breakpoint_errors
253
+ def remove_breakpoint(self, spec: str | WaldiezBreakpoint) -> bool:
254
+ """Remove a breakpoint based on its specification.
60
255
 
61
256
  Parameters
62
257
  ----------
63
- event_type : str
258
+ spec : str | WaldiezBreakpoint
64
259
  The event type to remove the breakpoint for.
65
260
 
66
261
  Returns
@@ -68,79 +263,209 @@ class BreakpointsMixin:
68
263
  bool
69
264
  True if the breakpoint was removed, False if it didn't exist.
70
265
  """
71
- if not event_type or not isinstance(event_type, str): # pyright: ignore
72
- self.emit(
73
- WaldiezDebugError(
74
- error="Invalid event type: must be a non-empty string"
266
+ if isinstance(spec, WaldiezBreakpoint):
267
+ breakpoint_obj = spec
268
+ spec_str = str(spec)
269
+ elif isinstance(spec, str) and spec:
270
+ try:
271
+ breakpoint_obj = WaldiezBreakpoint.from_string(spec)
272
+ spec_str = spec
273
+ except ValueError as e:
274
+ self.emit(
275
+ WaldiezDebugError(error=f"Invalid breakpoint format: {e}")
75
276
  )
277
+ return False
278
+ else:
279
+ self.emit(
280
+ WaldiezDebugError(error="Invalid breakpoint specification")
76
281
  )
77
282
  return False
78
283
 
79
- if event_type in self._breakpoints:
80
- self._breakpoints.remove(event_type)
81
- self.emit(WaldiezDebugBreakpointRemoved(breakpoint=event_type))
284
+ if breakpoint_obj in self._breakpoints:
285
+ self._breakpoints.remove(breakpoint_obj)
286
+ self._invalidate_cache()
287
+ self.emit(WaldiezDebugBreakpointRemoved(breakpoint=spec_str))
82
288
  return True
289
+
83
290
  self.emit(
84
291
  WaldiezDebugError(
85
- error=f"Breakpoint for '{event_type}' does not exist"
292
+ error=f"Breakpoint for '{spec_str}' does not exist"
86
293
  )
87
294
  )
88
295
  return False
89
296
 
90
297
  def list_breakpoints(self) -> None:
91
298
  """List all current breakpoints."""
92
- self.emit(
93
- WaldiezDebugBreakpointsList(breakpoints=sorted(self._breakpoints))
94
- )
299
+ breakpoints_list = sorted(self._breakpoints, key=str)
300
+ self.emit(WaldiezDebugBreakpointsList(breakpoints=breakpoints_list))
95
301
 
96
302
  def clear_breakpoints(self) -> None:
97
303
  """Clear all breakpoints."""
98
304
  count = len(self._breakpoints)
99
305
  self._breakpoints.clear()
306
+ self._invalidate_cache()
307
+
100
308
  if count > 0:
101
309
  self.emit(
102
310
  WaldiezDebugBreakpointCleared(
103
311
  message=f"Cleared {count} breakpoint(s)"
104
312
  )
105
313
  )
314
+ else:
315
+ self.emit(
316
+ WaldiezDebugBreakpointCleared(message="No breakpoints to clear")
317
+ )
106
318
 
107
- def set_breakpoints(self, event_types: Iterable[str]) -> None:
108
- """Set which event types to break on.
319
+ @handle_breakpoint_errors
320
+ def set_breakpoints(self, specs: Iterable[str | WaldiezBreakpoint]) -> bool:
321
+ """Set which breakpoints to activate.
109
322
 
110
323
  Parameters
111
324
  ----------
112
- event_types : Iterable[str]
113
- Iterable of event types to break on. Empty means break on all.
325
+ specs : Iterable[str | WaldiezBreakpoint]
326
+ Iterable of event types to break on. Empty means no breakpoints.
327
+
328
+ Returns
329
+ -------
330
+ bool
331
+ True if all breakpoints were set successfully, False if any failed.
114
332
  """
115
- self._breakpoints = set(event_types)
333
+ new_breakpoints: set[WaldiezBreakpoint] = set()
334
+ errors: list[str] = []
335
+
336
+ for spec in specs:
337
+ try:
338
+ if isinstance(spec, WaldiezBreakpoint):
339
+ new_breakpoints.add(spec)
340
+ else:
341
+ new_breakpoints.add(WaldiezBreakpoint.from_string(spec))
342
+ except ValueError as e:
343
+ errors.append(f"Invalid breakpoint '{spec}': {e}")
344
+
345
+ if errors:
346
+ for error in errors:
347
+ self.emit(WaldiezDebugError(error=error))
348
+ return False
349
+
350
+ old_count = len(self._breakpoints)
351
+ self._breakpoints = new_breakpoints
352
+ new_count = len(self._breakpoints)
353
+ self._invalidate_cache()
354
+
355
+ self.emit(
356
+ WaldiezDebugBreakpointCleared(
357
+ message=f"Updated breakpoints: {old_count} -> {new_count}"
358
+ )
359
+ )
360
+ return True
116
361
 
117
- def get_breakpoints(self) -> set[str]:
362
+ def get_breakpoints(self) -> set[WaldiezBreakpoint]:
118
363
  """Get current breakpoints.
119
364
 
120
365
  Returns
121
366
  -------
122
- set[str]
367
+ set[WaldiezBreakpoint]
123
368
  Set of current breakpoint event types.
124
369
  """
125
370
  return self._breakpoints.copy()
126
371
 
127
- def has_breakpoint(self, event_type: str) -> bool:
128
- """Check if a breakpoint exists for an event type.
372
+ def has_breakpoint(self, spec: str | WaldiezBreakpoint) -> bool:
373
+ """Check if a breakpoint exists.
129
374
 
130
375
  Parameters
131
376
  ----------
132
- event_type : str
133
- The event type to check.
377
+ spec : str | WaldiezBreakpoint
378
+ The breakpoint specification to check.
134
379
 
135
380
  Returns
136
381
  -------
137
382
  bool
138
383
  True if a breakpoint exists for this event type.
139
384
  """
140
- return event_type in self._breakpoints
385
+ try:
386
+ if isinstance(spec, WaldiezBreakpoint):
387
+ return spec in self._breakpoints
388
+ return WaldiezBreakpoint.from_string(spec) in self._breakpoints
389
+ except ValueError:
390
+ return False
391
+
392
+ @staticmethod
393
+ def _get_event_core(event_dict: dict[str, Any]) -> tuple[str, str, str]:
394
+ event_type = event_dict.get("type", "unknown")
395
+ sender = event_dict.get("sender", "")
396
+ if not sender:
397
+ event_content = event_dict.get("content", {})
398
+ if isinstance(event_content, dict):
399
+ sender = event_content.get(
400
+ "sender",
401
+ event_content.get("speaker", ""),
402
+ )
403
+ if not isinstance(sender, str):
404
+ sender = ""
405
+ recipient = event_dict.get("recipient", "")
406
+ if not recipient:
407
+ event_content = event_dict.get("content", {})
408
+ if isinstance(event_content, dict):
409
+ recipient = event_content.get("recipient")
410
+ if not isinstance(recipient, str):
411
+ recipient = ""
412
+ return event_type, sender, recipient
413
+
414
+ def _got_breakpoint_match(
415
+ self, event_dump: dict[str, Any], sender_only: bool
416
+ ) -> bool:
417
+ event_type, sender, recipient = BreakpointsMixin._get_event_core(
418
+ event_dump
419
+ )
420
+ event_dict = {
421
+ "type": event_type,
422
+ "sender": sender,
423
+ "recipient": recipient,
424
+ }
425
+
426
+ has_agent_breakpoints = any(
427
+ bp.agent is not None for bp in self._breakpoints
428
+ )
429
+ if has_agent_breakpoints:
430
+ # Don't use cache for agent-based breakpoints
431
+ # (we might have a mix of agent ids and names)
432
+ matches_breakpoint = any(
433
+ bp.matches(
434
+ event_dict,
435
+ self._agent_id_to_name,
436
+ sender_only=sender_only,
437
+ )
438
+ for bp in self._breakpoints
439
+ )
440
+ else:
441
+ # Get current breakpoints signature for cache invalidation
442
+ breakpoints_sig = self._get_breakpoints_signature()
443
+
444
+ # Check cached result
445
+ # noinspection PyBroadException
446
+ try:
447
+ matches_breakpoint = self._check_breakpoint_match_cached(
448
+ event_type, sender, recipient, sender_only, breakpoints_sig
449
+ )
450
+ self._breakpoint_stats["cache_hits"] += 1
451
+ except Exception: # pylint: disable=broad-exception-caught
452
+ # Fallback to non-cached check
453
+ matches_breakpoint = any(
454
+ bp.matches(
455
+ event_dict,
456
+ self._agent_id_to_name,
457
+ sender_only=sender_only,
458
+ )
459
+ for bp in self._breakpoints
460
+ )
461
+ self._breakpoint_stats["cache_misses"] += 1
462
+
463
+ return matches_breakpoint
141
464
 
142
465
  def should_break_on_event(
143
- self, event: Union["BaseEvent", "BaseMessage"], step_mode: bool = True
466
+ self,
467
+ event: Union["BaseEvent", "BaseMessage"],
468
+ sender_only: bool,
144
469
  ) -> bool:
145
470
  """Determine if we should break on this event.
146
471
 
@@ -148,17 +473,14 @@ class BreakpointsMixin:
148
473
  ----------
149
474
  event : Union[BaseEvent, BaseMessage]
150
475
  The event to check.
151
- step_mode : bool, optional
152
- Whether step mode is enabled, by default True.
476
+ sender_only : bool
477
+ Only check for event's sender agent.
153
478
 
154
479
  Returns
155
480
  -------
156
481
  bool
157
482
  True if we should break, False otherwise.
158
483
  """
159
- if not step_mode:
160
- return False
161
-
162
484
  # Get event type
163
485
  event_type = getattr(event, "type", "unknown")
164
486
 
@@ -166,23 +488,157 @@ class BreakpointsMixin:
166
488
  if event_type == "input_request":
167
489
  return False
168
490
 
169
- # If no specific breakpoints set, break on all events
170
491
  if not self._breakpoints:
171
- return True
492
+ return not bool(self._config.auto_continue)
493
+
494
+ # Check if this event matches any breakpoint using caching
495
+ if not hasattr(event, "model_dump"):
496
+ return not bool(self._config.auto_continue)
497
+ # pylint: disable=too-many-try-statements,broad-exception-caught
498
+ try:
499
+ event_dump = event.model_dump(
500
+ mode="python", exclude_none=True, fallback=str
501
+ )
502
+ matches_breakpoint = self._got_breakpoint_match(
503
+ event_dump, sender_only=sender_only
504
+ )
505
+ # If any breakpoint matches: break regardless of step_mode
506
+ if matches_breakpoint:
507
+ self._breakpoint_stats["total_matches"] += 1
508
+ return True
172
509
 
173
- # Check if this event type has a breakpoint
174
- return event_type in self._breakpoints
510
+ except Exception as e: # pylint: disable=broad-exception-caught
511
+ logging.warning("Error processing event for breakpoints: %s", e)
512
+ self._breakpoint_stats["cache_misses"] += 1
513
+
514
+ return not bool(self._config.auto_continue)
175
515
 
176
516
  def get_breakpoint_stats(self) -> dict[str, Any]:
177
- """Get breakpoint statistics.
517
+ """Get breakpoint statistics including performance metrics.
178
518
 
179
519
  Returns
180
520
  -------
181
521
  dict[str, Any]
182
522
  Dictionary containing breakpoint statistics.
183
523
  """
524
+ breakpoints: list[dict[str, Any]] = [
525
+ {
526
+ "type": bp.type.value,
527
+ "event_type": bp.event_type,
528
+ "agent": bp.agent,
529
+ "description": bp.description,
530
+ "string_repr": str(bp),
531
+ }
532
+ for bp in self._breakpoints
533
+ ]
534
+
535
+ # Calculate cache efficiency
536
+ total_checks = (
537
+ self._breakpoint_stats["cache_hits"]
538
+ + self._breakpoint_stats["cache_misses"]
539
+ )
540
+ cache_hit_rate = (
541
+ self._breakpoint_stats["cache_hits"] / total_checks
542
+ if total_checks > 0
543
+ else 0
544
+ )
545
+
546
+ # Get cache info from lru_cache
547
+ cache_info = self._check_breakpoint_match_cached.cache_info()
548
+
184
549
  return {
185
550
  "total_breakpoints": len(self._breakpoints),
186
- "breakpoints": sorted(self._breakpoints),
551
+ "breakpoints": breakpoints,
187
552
  "has_breakpoints": len(self._breakpoints) > 0,
553
+ "cache_stats": {
554
+ "cache_hit_rate": f"{cache_hit_rate:.2%}",
555
+ "cache_size": cache_info.currsize,
556
+ "cache_maxsize": cache_info.maxsize,
557
+ "total_matches": self._breakpoint_stats["total_matches"],
558
+ "cache_hits": cache_info.hits,
559
+ "cache_misses": cache_info.misses,
560
+ },
561
+ "performance": {
562
+ "lru_cache_info": {
563
+ "hits": cache_info.hits,
564
+ "misses": cache_info.misses,
565
+ "maxsize": cache_info.maxsize,
566
+ "currsize": cache_info.currsize,
567
+ }
568
+ },
569
+ }
570
+
571
+ def reset_stats(self) -> None:
572
+ """Reset breakpoint statistics."""
573
+ self._breakpoint_stats = {
574
+ "total_matches": 0,
575
+ "cache_hits": 0,
576
+ "cache_misses": 0,
188
577
  }
578
+ self._invalidate_cache()
579
+
580
+ def optimize_cache(self) -> None:
581
+ """Manually optimize the cache by clearing it."""
582
+ self._invalidate_cache()
583
+
584
+ def export_breakpoints(self) -> list[str]:
585
+ """Export breakpoints as a list of strings for persistence.
586
+
587
+ Returns
588
+ -------
589
+ list[str]
590
+ List of breakpoint specifications as strings.
591
+ """
592
+ return [str(bp) for bp in sorted(self._breakpoints, key=str)]
593
+
594
+ def is_auto_run(self) -> bool:
595
+ """Check if we are in auto-run mode.
596
+
597
+ Returns
598
+ -------
599
+ bool
600
+ False if we don't have any breakpoints and
601
+ we don't have any 'all' breakpoints, True otherwise.
602
+ """
603
+ if not self._breakpoints:
604
+ return False
605
+ if any(bp.type.value == "all" for bp in self._breakpoints):
606
+ self._breakpoints.clear()
607
+ self._invalidate_cache()
608
+ return False
609
+ return True
610
+
611
+ def import_breakpoints(
612
+ self, breakpoint_specs: list[str]
613
+ ) -> tuple[int, list[str]]:
614
+ """Import breakpoints from a list of string specifications.
615
+
616
+ Parameters
617
+ ----------
618
+ breakpoint_specs : list[str]
619
+ List of breakpoint specifications as strings.
620
+
621
+ Returns
622
+ -------
623
+ tuple[int, list[str]]
624
+ Tuple of (successful_imports, error_messages).
625
+ """
626
+ successful = 0
627
+ errors: list[str] = []
628
+
629
+ for spec in breakpoint_specs:
630
+ # pylint: disable=too-many-try-statements
631
+ try:
632
+ breakpoint_obj = WaldiezBreakpoint.from_string(spec)
633
+ if breakpoint_obj not in self._breakpoints:
634
+ self._breakpoints.add(breakpoint_obj)
635
+ successful += 1
636
+ else:
637
+ errors.append(f"Breakpoint '{spec}' already exists")
638
+ except ValueError as e:
639
+ errors.append(f"Invalid breakpoint '{spec}': {e}")
640
+
641
+ if successful > 0:
642
+ self._invalidate_cache()
643
+
644
+ return successful, errors