waldiez 0.5.8__py3-none-any.whl → 0.5.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of waldiez might be problematic. Click here for more details.

Files changed (88) hide show
  1. waldiez/_version.py +1 -1
  2. waldiez/cli.py +112 -24
  3. waldiez/exporting/agent/exporter.py +3 -0
  4. waldiez/exporting/agent/extras/captain_agent_extras.py +44 -7
  5. waldiez/exporting/agent/extras/handoffs/condition.py +3 -1
  6. waldiez/exporting/chats/utils/common.py +25 -23
  7. waldiez/exporting/core/__init__.py +0 -2
  8. waldiez/exporting/core/context.py +13 -13
  9. waldiez/exporting/core/protocols.py +0 -141
  10. waldiez/exporting/core/result.py +5 -5
  11. waldiez/exporting/flow/merger.py +2 -2
  12. waldiez/exporting/flow/orchestrator.py +1 -0
  13. waldiez/exporting/flow/utils/common.py +2 -2
  14. waldiez/exporting/flow/utils/importing.py +1 -0
  15. waldiez/exporting/flow/utils/logging.py +6 -7
  16. waldiez/exporting/tools/exporter.py +5 -0
  17. waldiez/exporting/tools/factory.py +4 -0
  18. waldiez/exporting/tools/processor.py +5 -1
  19. waldiez/io/_ws.py +13 -5
  20. waldiez/io/models/content/image.py +1 -0
  21. waldiez/io/models/user_input.py +4 -4
  22. waldiez/io/models/user_response.py +1 -0
  23. waldiez/io/mqtt.py +1 -1
  24. waldiez/io/structured.py +17 -17
  25. waldiez/io/utils.py +1 -1
  26. waldiez/io/ws.py +9 -11
  27. waldiez/logger.py +180 -63
  28. waldiez/models/agents/agent/update_system_message.py +0 -2
  29. waldiez/models/agents/doc_agent/doc_agent.py +8 -1
  30. waldiez/models/common/dict_utils.py +169 -40
  31. waldiez/models/flow/flow.py +6 -6
  32. waldiez/models/flow/info.py +5 -1
  33. waldiez/models/model/_llm.py +28 -14
  34. waldiez/models/model/model.py +4 -1
  35. waldiez/models/model/model_data.py +18 -5
  36. waldiez/models/tool/predefined/_config.py +5 -1
  37. waldiez/models/tool/predefined/_duckduckgo.py +4 -0
  38. waldiez/models/tool/predefined/_email.py +474 -0
  39. waldiez/models/tool/predefined/_google.py +8 -6
  40. waldiez/models/tool/predefined/_perplexity.py +3 -0
  41. waldiez/models/tool/predefined/_searxng.py +3 -0
  42. waldiez/models/tool/predefined/_tavily.py +4 -1
  43. waldiez/models/tool/predefined/_wikipedia.py +4 -1
  44. waldiez/models/tool/predefined/_youtube.py +4 -1
  45. waldiez/models/tool/predefined/protocol.py +3 -0
  46. waldiez/models/tool/tool.py +22 -4
  47. waldiez/models/waldiez.py +12 -0
  48. waldiez/runner.py +37 -54
  49. waldiez/running/__init__.py +6 -0
  50. waldiez/running/base_runner.py +310 -353
  51. waldiez/running/environment.py +1 -0
  52. waldiez/running/exceptions.py +9 -0
  53. waldiez/running/post_run.py +4 -4
  54. waldiez/running/pre_run.py +51 -40
  55. waldiez/running/protocol.py +21 -101
  56. waldiez/running/run_results.py +1 -1
  57. waldiez/running/standard_runner.py +84 -277
  58. waldiez/running/step_by_step/__init__.py +46 -0
  59. waldiez/running/step_by_step/breakpoints_mixin.py +188 -0
  60. waldiez/running/step_by_step/step_by_step_models.py +224 -0
  61. waldiez/running/step_by_step/step_by_step_runner.py +745 -0
  62. waldiez/running/subprocess_runner/__base__.py +282 -0
  63. waldiez/running/subprocess_runner/__init__.py +16 -0
  64. waldiez/running/subprocess_runner/_async_runner.py +362 -0
  65. waldiez/running/subprocess_runner/_sync_runner.py +455 -0
  66. waldiez/running/subprocess_runner/runner.py +561 -0
  67. waldiez/running/timeline_processor.py +1 -1
  68. waldiez/running/utils.py +376 -1
  69. waldiez/utils/version.py +2 -6
  70. waldiez/ws/__init__.py +70 -0
  71. waldiez/ws/__main__.py +15 -0
  72. waldiez/ws/_file_handler.py +201 -0
  73. waldiez/ws/cli.py +211 -0
  74. waldiez/ws/client_manager.py +835 -0
  75. waldiez/ws/errors.py +416 -0
  76. waldiez/ws/models.py +971 -0
  77. waldiez/ws/reloader.py +342 -0
  78. waldiez/ws/server.py +469 -0
  79. waldiez/ws/session_manager.py +393 -0
  80. waldiez/ws/session_stats.py +83 -0
  81. waldiez/ws/utils.py +385 -0
  82. {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/METADATA +74 -74
  83. {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/RECORD +87 -65
  84. waldiez/running/patch_io_stream.py +0 -210
  85. {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/WHEEL +0 -0
  86. {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/entry_points.txt +0 -0
  87. {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/licenses/LICENSE +0 -0
  88. {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/licenses/NOTICE.md +0 -0
waldiez/ws/errors.py ADDED
@@ -0,0 +1,416 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+ # pylint: disable=unused-argument
4
+ """Error handling and exceptions for Waldiez WebSocket server."""
5
+
6
+ import logging
7
+ from enum import IntEnum
8
+ from typing import Any
9
+
10
+
11
+ class ErrorCode(IntEnum):
12
+ """WebSocket error codes."""
13
+
14
+ # Standard WebSocket close codes
15
+ NORMAL_CLOSURE = 1000
16
+ GOING_AWAY = 1001
17
+ PROTOCOL_ERROR = 1002
18
+ UNSUPPORTED_DATA = 1003
19
+ INTERNAL_ERROR = 1011
20
+ TRY_AGAIN_LATER = 1013
21
+
22
+ # Custom application error codes (4000-4999 range)
23
+ INVALID_MESSAGE_FORMAT = 4000
24
+ UNSUPPORTED_ACTION = 4001
25
+ SERVER_OVERLOADED = 4002
26
+ INVALID_REQUEST_DATA = 4003
27
+ OPERATION_FAILED = 4004
28
+ NO_INPUT_REQUESTED = 4005
29
+ STALE_INPUT_REQUEST = 4006
30
+ SESSION_NOT_FOUND = 4007
31
+ TIMEOUT = 4008
32
+
33
+ @property
34
+ def string(self) -> str:
35
+ """Get the string representation of the error code."""
36
+ # to upper camel case
37
+ return self.name.replace("_", " ").title().replace(" ", "")
38
+
39
+
40
+ class WaldiezServerError(Exception):
41
+ """Base exception for Waldiez server errors."""
42
+
43
+ def __init__(
44
+ self,
45
+ message: str,
46
+ error_code: ErrorCode = ErrorCode.INTERNAL_ERROR,
47
+ details: dict[str, Any] | None = None,
48
+ ):
49
+ """Initialize server error.
50
+
51
+ Parameters
52
+ ----------
53
+ message : str
54
+ Error message
55
+ error_code : ErrorCode
56
+ Error code
57
+ details : dict[str, Any] | None
58
+ Additional error details
59
+ """
60
+ super().__init__(message)
61
+ self.message = message
62
+ self.error_code = error_code
63
+ self.details = details or {}
64
+
65
+ def to_dict(self) -> dict[str, Any]:
66
+ """Convert error to dictionary.
67
+
68
+ Returns
69
+ -------
70
+ Dict[str, Any]
71
+ Error dictionary
72
+ """
73
+ return {
74
+ "type": "error",
75
+ "message": self.message,
76
+ "code": int(self.error_code.value),
77
+ "error_type": self.error_code.string,
78
+ "details": self.details,
79
+ }
80
+
81
+
82
+ class MessageParsingError(WaldiezServerError):
83
+ """Error parsing WebSocket message."""
84
+
85
+ def __init__(self, message: str, raw_data: str | None = None):
86
+ """Initialize message parsing error.
87
+
88
+ Parameters
89
+ ----------
90
+ message : str
91
+ Error message
92
+ raw_data : str | None
93
+ Raw message data that failed to parse
94
+ """
95
+ details: dict[str, Any] = {}
96
+ if raw_data:
97
+ details["raw_data"] = raw_data[:500] # Limit size
98
+
99
+ super().__init__(
100
+ message,
101
+ ErrorCode.INVALID_MESSAGE_FORMAT,
102
+ details,
103
+ )
104
+
105
+
106
+ class UnsupportedActionError(WaldiezServerError):
107
+ """Error for unsupported message actions."""
108
+
109
+ def __init__(self, action: str):
110
+ """Initialize unsupported action error.
111
+
112
+ Parameters
113
+ ----------
114
+ action : str
115
+ Unsupported action
116
+ """
117
+ super().__init__(
118
+ f"Unsupported action: {action}",
119
+ ErrorCode.UNSUPPORTED_ACTION,
120
+ {"action": action},
121
+ )
122
+
123
+
124
+ class ServerOverloadError(WaldiezServerError):
125
+ """Error when server is overloaded."""
126
+
127
+ def __init__(self, current_clients: int, max_clients: int):
128
+ """Initialize server overload error.
129
+
130
+ Parameters
131
+ ----------
132
+ current_clients : int
133
+ Current number of clients
134
+ max_clients : int
135
+ Maximum allowed clients
136
+ """
137
+ super().__init__(
138
+ f"Server overloaded: {current_clients}/{max_clients} clients",
139
+ ErrorCode.SERVER_OVERLOADED,
140
+ {
141
+ "current_clients": current_clients,
142
+ "max_clients": max_clients,
143
+ },
144
+ )
145
+
146
+
147
+ class OperationTimeoutError(WaldiezServerError):
148
+ """Error when operation times out."""
149
+
150
+ def __init__(self, operation: str, timeout: float):
151
+ """Initialize operation timeout error.
152
+
153
+ Parameters
154
+ ----------
155
+ operation : str
156
+ Operation that timed out
157
+ timeout : float
158
+ Timeout duration in seconds
159
+ """
160
+ super().__init__(
161
+ f"Operation '{operation}' timed out after {timeout} seconds",
162
+ ErrorCode.TIMEOUT,
163
+ {
164
+ "operation": operation,
165
+ "timeout": timeout,
166
+ },
167
+ )
168
+
169
+
170
+ class MessageHandlingError(WaldiezServerError):
171
+ """Error during message processing/business logic."""
172
+
173
+ def __init__(
174
+ self,
175
+ operation: str,
176
+ details: str,
177
+ original_error: Exception | None = None,
178
+ ):
179
+ """Initialize message handling error.
180
+
181
+ Parameters
182
+ ----------
183
+ operation : str
184
+ The operation that failed (e.g., "save", "run", "convert")
185
+ details : str
186
+ Details about the failure
187
+ original_error : Exception | None
188
+ The original exception that caused this error
189
+ """
190
+ error_details = {"operation": operation, "details": details}
191
+ if original_error:
192
+ error_details["original_error"] = str(original_error)
193
+
194
+ super().__init__(
195
+ f"Failed to handle {operation}: {details}",
196
+ ErrorCode.OPERATION_FAILED,
197
+ error_details,
198
+ )
199
+
200
+
201
+ class SessionNotFoundError(WaldiezServerError):
202
+ """Error when a session is not found."""
203
+
204
+ def __init__(self, session_id: str):
205
+ """Initialize session not found error.
206
+
207
+ Parameters
208
+ ----------
209
+ session_id : str
210
+ The ID of the session that was not found
211
+ """
212
+ super().__init__(
213
+ f"Session not found: {session_id}",
214
+ ErrorCode.SESSION_NOT_FOUND,
215
+ {"session_id": session_id},
216
+ )
217
+
218
+
219
+ class NoInputRequestedError(WaldiezServerError):
220
+ """Error when no user input was requested."""
221
+
222
+ def __init__(self) -> None:
223
+ """Initialize no input requested error."""
224
+ super().__init__(
225
+ "No user input requested",
226
+ ErrorCode.NO_INPUT_REQUESTED,
227
+ {},
228
+ )
229
+
230
+
231
+ class StaleInputRequestError(WaldiezServerError):
232
+ """Error when an input request is stale."""
233
+
234
+ def __init__(self, request_id: str, expected_id: str):
235
+ """Initialize stale input request error.
236
+
237
+ Parameters
238
+ ----------
239
+ request_id : str
240
+ The ID of the stale request
241
+ expected_id : str
242
+ The ID of the expected request
243
+ """
244
+ super().__init__(
245
+ f"Stale input request. Expected {expected_id}, got {request_id}",
246
+ ErrorCode.STALE_INPUT_REQUEST,
247
+ {"request_id": request_id, "expected_id": expected_id},
248
+ )
249
+
250
+
251
+ class ErrorHandler:
252
+ """Centralized error handler for WebSocket server."""
253
+
254
+ def __init__(self, logger: logging.Logger | None = None):
255
+ """Initialize error handler.
256
+
257
+ Parameters
258
+ ----------
259
+ logger : logging.Logger | None
260
+ Logger instance
261
+ """
262
+ self.logger = logger or logging.getLogger(__name__)
263
+ self.error_counts: dict[str, int] = {}
264
+
265
+ # noinspection PyUnusedLocal
266
+ def handle_error(
267
+ self,
268
+ error: Exception,
269
+ context: dict[str, Any] | None = None,
270
+ client_id: str | None = None,
271
+ ) -> dict[str, Any]:
272
+ """Handle an error and return appropriate response.
273
+
274
+ Parameters
275
+ ----------
276
+ error : Exception
277
+ The error to handle
278
+ context : dict[str, Any] | None
279
+ Additional context information
280
+ client_id : str | None
281
+ Client ID if error is client-specific
282
+
283
+ Returns
284
+ -------
285
+ dict[str, Any]
286
+ Error response to send to client
287
+ """
288
+ # Increment error count
289
+ error_type = type(error).__name__
290
+ self.error_counts[error_type] = self.error_counts.get(error_type, 0) + 1
291
+
292
+ # Handle known Waldiez errors
293
+ if isinstance(error, WaldiezServerError):
294
+ self.logger.warning(
295
+ "Waldiez error%s: %s",
296
+ f" (client: {client_id})" if client_id else "",
297
+ error.message,
298
+ )
299
+ return error.to_dict()
300
+
301
+ # Handle specific common errors
302
+ if isinstance(error, ValueError):
303
+ self.logger.warning(
304
+ "Validation error%s: %s",
305
+ f" (client: {client_id})" if client_id else "",
306
+ str(error),
307
+ )
308
+ return {
309
+ "type": "error",
310
+ "message": str(error),
311
+ "code": int(ErrorCode.INVALID_REQUEST_DATA),
312
+ "error_type": "ValidationError",
313
+ }
314
+
315
+ if isinstance(error, TimeoutError):
316
+ self.logger.warning(
317
+ "Timeout error%s: %s",
318
+ f" (client: {client_id})" if client_id else "",
319
+ str(error),
320
+ )
321
+ return {
322
+ "type": "error",
323
+ "message": "Operation timed out",
324
+ "code": int(ErrorCode.TIMEOUT),
325
+ "error_type": "TimeoutError",
326
+ }
327
+
328
+ # Handle unexpected errors
329
+ self.logger.error(
330
+ "Unexpected error%s: %s",
331
+ f" (client: {client_id})" if client_id else "",
332
+ str(error),
333
+ exc_info=True,
334
+ )
335
+
336
+ # Return generic error response (don't expose internal details)
337
+ return {
338
+ "type": "error",
339
+ "message": "An internal server error occurred",
340
+ "code": int(ErrorCode.INTERNAL_ERROR),
341
+ "error_type": "InternalError",
342
+ }
343
+
344
+ def record_operational_error(
345
+ self, error_type: str, details: str | None = None
346
+ ) -> None:
347
+ """Record an operational error that isn't an exception.
348
+
349
+ Parameters
350
+ ----------
351
+ error_type : str
352
+ Type of operational error
353
+ (e.g., "MessageSendFailure", "ConnectionDropped")
354
+ details : str | None
355
+ Optional details about the error
356
+ """
357
+ self.error_counts[error_type] = self.error_counts.get(error_type, 0) + 1
358
+
359
+ # Log the operational error
360
+ if details:
361
+ self.logger.warning(
362
+ "Operational error [%s]: %s", error_type, details
363
+ )
364
+ else:
365
+ self.logger.warning("Operational error: %s", error_type)
366
+
367
+ def record_send_failure(self, client_id: str | None = None) -> None:
368
+ """Handle message send failures.
369
+
370
+ Parameters
371
+ ----------
372
+ client_id : str | None
373
+ Client ID if available
374
+ """
375
+ details = f"client: {client_id}" if client_id else None
376
+ self.record_operational_error("MessageSendFailure", details)
377
+
378
+ def record_connection_drop(
379
+ self, client_id: str, reason: str | None = None
380
+ ) -> None:
381
+ """Handle connection drops.
382
+
383
+ Parameters
384
+ ----------
385
+ client_id : str
386
+ Client ID
387
+ reason : str | None
388
+ Reason for connection drop
389
+ """
390
+ details = f"client: {client_id}"
391
+ if reason:
392
+ details += f", reason: {reason}"
393
+ self.record_operational_error("ConnectionDropped", details)
394
+
395
+ def get_error_stats(self) -> dict[str, Any]:
396
+ """Get error statistics.
397
+
398
+ Returns
399
+ -------
400
+ dict[str, Any]
401
+ Error statistics
402
+ """
403
+ total_errors = sum(self.error_counts.values())
404
+ return {
405
+ "total_errors": total_errors,
406
+ "error_counts": self.error_counts.copy(),
407
+ "most_common_error": (
408
+ max(self.error_counts, key=lambda x: self.error_counts[x])
409
+ if self.error_counts
410
+ else None
411
+ ),
412
+ }
413
+
414
+ def reset_stats(self) -> None:
415
+ """Reset error statistics."""
416
+ self.error_counts.clear()