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.
- waldiez/_version.py +1 -1
- waldiez/cli.py +113 -24
- waldiez/exporting/agent/exporter.py +9 -6
- waldiez/exporting/agent/extras/captain_agent_extras.py +44 -7
- waldiez/exporting/agent/extras/group_manager_agent_extas.py +6 -1
- waldiez/exporting/agent/extras/handoffs/after_work.py +1 -0
- waldiez/exporting/agent/extras/handoffs/available.py +1 -0
- waldiez/exporting/agent/extras/handoffs/condition.py +3 -1
- waldiez/exporting/agent/extras/handoffs/handoff.py +1 -0
- waldiez/exporting/agent/extras/handoffs/target.py +1 -0
- waldiez/exporting/agent/termination.py +1 -0
- waldiez/exporting/chats/utils/common.py +25 -23
- waldiez/exporting/core/__init__.py +0 -2
- waldiez/exporting/core/constants.py +3 -1
- waldiez/exporting/core/context.py +13 -13
- waldiez/exporting/core/extras/serializer.py +12 -10
- waldiez/exporting/core/protocols.py +0 -141
- waldiez/exporting/core/result.py +5 -5
- waldiez/exporting/core/types.py +1 -0
- waldiez/exporting/core/utils/llm_config.py +2 -2
- waldiez/exporting/flow/execution_generator.py +1 -0
- waldiez/exporting/flow/merger.py +2 -2
- waldiez/exporting/flow/orchestrator.py +1 -0
- waldiez/exporting/flow/utils/common.py +3 -3
- waldiez/exporting/flow/utils/importing.py +1 -0
- waldiez/exporting/flow/utils/logging.py +7 -80
- waldiez/exporting/tools/exporter.py +5 -0
- waldiez/exporting/tools/factory.py +4 -0
- waldiez/exporting/tools/processor.py +5 -1
- waldiez/io/__init__.py +3 -1
- waldiez/io/_ws.py +15 -5
- waldiez/io/models/content/image.py +1 -0
- waldiez/io/models/user_input.py +4 -4
- waldiez/io/models/user_response.py +1 -0
- waldiez/io/mqtt.py +1 -1
- waldiez/io/structured.py +98 -45
- waldiez/io/utils.py +17 -11
- waldiez/io/ws.py +10 -12
- waldiez/logger.py +180 -63
- waldiez/models/agents/agent/agent.py +2 -1
- waldiez/models/agents/agent/update_system_message.py +0 -2
- waldiez/models/agents/doc_agent/doc_agent.py +8 -1
- waldiez/models/chat/chat.py +1 -0
- waldiez/models/chat/chat_data.py +0 -2
- waldiez/models/common/base.py +2 -0
- waldiez/models/common/dict_utils.py +169 -40
- waldiez/models/common/handoff.py +2 -0
- waldiez/models/common/method_utils.py +2 -0
- waldiez/models/flow/flow.py +6 -6
- waldiez/models/flow/info.py +5 -1
- waldiez/models/model/_llm.py +31 -14
- waldiez/models/model/model.py +4 -1
- waldiez/models/model/model_data.py +18 -5
- waldiez/models/tool/predefined/_config.py +5 -1
- waldiez/models/tool/predefined/_duckduckgo.py +4 -0
- waldiez/models/tool/predefined/_email.py +477 -0
- waldiez/models/tool/predefined/_google.py +4 -1
- waldiez/models/tool/predefined/_perplexity.py +4 -1
- waldiez/models/tool/predefined/_searxng.py +4 -1
- waldiez/models/tool/predefined/_tavily.py +4 -1
- waldiez/models/tool/predefined/_wikipedia.py +5 -2
- waldiez/models/tool/predefined/_youtube.py +4 -1
- waldiez/models/tool/predefined/protocol.py +3 -0
- waldiez/models/tool/tool.py +22 -4
- waldiez/models/waldiez.py +12 -0
- waldiez/runner.py +37 -54
- waldiez/running/__init__.py +6 -0
- waldiez/running/base_runner.py +381 -363
- waldiez/running/environment.py +1 -0
- waldiez/running/exceptions.py +9 -0
- waldiez/running/post_run.py +10 -4
- waldiez/running/pre_run.py +199 -66
- waldiez/running/protocol.py +21 -101
- waldiez/running/run_results.py +1 -1
- waldiez/running/standard_runner.py +83 -276
- waldiez/running/step_by_step/__init__.py +46 -0
- waldiez/running/step_by_step/breakpoints_mixin.py +512 -0
- waldiez/running/step_by_step/command_handler.py +151 -0
- waldiez/running/step_by_step/events_processor.py +199 -0
- waldiez/running/step_by_step/step_by_step_models.py +541 -0
- waldiez/running/step_by_step/step_by_step_runner.py +750 -0
- waldiez/running/subprocess_runner/__base__.py +279 -0
- waldiez/running/subprocess_runner/__init__.py +16 -0
- waldiez/running/subprocess_runner/_async_runner.py +362 -0
- waldiez/running/subprocess_runner/_sync_runner.py +456 -0
- waldiez/running/subprocess_runner/runner.py +570 -0
- waldiez/running/timeline_processor.py +1 -1
- waldiez/running/utils.py +492 -3
- waldiez/utils/version.py +2 -6
- waldiez/ws/__init__.py +71 -0
- waldiez/ws/__main__.py +15 -0
- waldiez/ws/_file_handler.py +199 -0
- waldiez/ws/_mock.py +74 -0
- waldiez/ws/cli.py +235 -0
- waldiez/ws/client_manager.py +851 -0
- waldiez/ws/errors.py +416 -0
- waldiez/ws/models.py +988 -0
- waldiez/ws/reloader.py +363 -0
- waldiez/ws/server.py +508 -0
- waldiez/ws/session_manager.py +393 -0
- waldiez/ws/session_stats.py +83 -0
- waldiez/ws/utils.py +410 -0
- {waldiez-0.5.9.dist-info → waldiez-0.6.0.dist-info}/METADATA +105 -96
- {waldiez-0.5.9.dist-info → waldiez-0.6.0.dist-info}/RECORD +108 -83
- waldiez/running/patch_io_stream.py +0 -210
- {waldiez-0.5.9.dist-info → waldiez-0.6.0.dist-info}/WHEEL +0 -0
- {waldiez-0.5.9.dist-info → waldiez-0.6.0.dist-info}/entry_points.txt +0 -0
- {waldiez-0.5.9.dist-info → waldiez-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {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
|