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
waldiez/ws/utils.py ADDED
@@ -0,0 +1,410 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+ # pylint: disable=import-error,line-too-long
4
+ # pyright: reportUnknownMemberType=false,reportUnknownVariableType=false
5
+ # pyright: reportUnknownArgumentType=false,reportAttributeAccessIssue=false
6
+ # flake8: noqa: E501
7
+ """Utilities for WebSocket server management."""
8
+
9
+ import asyncio
10
+ import json
11
+ import logging
12
+ import socket
13
+ import time
14
+ from contextlib import closing
15
+ from dataclasses import asdict, dataclass
16
+ from typing import TYPE_CHECKING, Any
17
+
18
+ try:
19
+ import websockets # type: ignore[unused-ignore, unused-import, import-not-found, import-untyped] # noqa
20
+ except ImportError: # pragma: no cover
21
+ from ._mock import websockets # type: ignore[no-redef,unused-ignore]
22
+
23
+
24
+ if TYPE_CHECKING:
25
+ from .server import WaldiezWsServer
26
+
27
+
28
+ @dataclass
29
+ class ErrorStats:
30
+ """Error stats."""
31
+
32
+ total_errors: int
33
+ error_counts: dict[str, int]
34
+ most_common_error: str | None = None
35
+
36
+ def to_dict(self) -> dict[str, Any]:
37
+ """Convert to dictionary.
38
+
39
+ Returns
40
+ -------
41
+ dict[str, Any]
42
+ Dictionary representation of the error stats
43
+ """
44
+ return asdict(self)
45
+
46
+
47
+ @dataclass
48
+ class ServerHealth:
49
+ """Server health status."""
50
+
51
+ status: str # "healthy", "degraded", "unhealthy"
52
+ uptime_seconds: float
53
+ active_connections: int
54
+ total_connections: int
55
+ messages_received: int
56
+ messages_sent: int
57
+ memory_usage_mb: float | None = None
58
+ timestamp: float = 0.0
59
+ error_stats: ErrorStats | None = None
60
+
61
+ def __post_init__(self) -> None:
62
+ """Set timestamp after initialization."""
63
+ if self.timestamp == 0.0:
64
+ self.timestamp = time.time()
65
+ if not self.error_stats:
66
+ self.error_stats = ErrorStats(
67
+ total_errors=0, error_counts={}, most_common_error=None
68
+ )
69
+
70
+ def to_dict(self) -> dict[str, Any]:
71
+ """Convert to dictionary.
72
+
73
+ Returns
74
+ -------
75
+ dict[str, Any]
76
+ Dictionary representation of the health status
77
+ """
78
+ my_dict = asdict(self)
79
+ my_dict["error_stats"] = (
80
+ self.error_stats.to_dict() if self.error_stats else None
81
+ )
82
+ return my_dict
83
+
84
+
85
+ class HealthChecker:
86
+ """Health checker for WebSocket server."""
87
+
88
+ def __init__(self, server: "WaldiezWsServer"):
89
+ """Initialize health checker.
90
+
91
+ Parameters
92
+ ----------
93
+ server : WaldiezWsServer
94
+ Server instance to monitor
95
+ """
96
+ self.server = server
97
+ self.check_interval = 30.0 # seconds
98
+ self.task: asyncio.Task[Any] | None = None
99
+ self.last_health: ServerHealth | None = None
100
+
101
+ def start(self) -> None:
102
+ """Start health monitoring."""
103
+ if self.task and not self.task.done():
104
+ return
105
+
106
+ self.task = asyncio.create_task(self._monitor_loop())
107
+
108
+ def stop(self) -> None:
109
+ """Stop health monitoring."""
110
+ if self.task and not self.task.done(): # pragma: no branch
111
+ self.task.cancel()
112
+
113
+ async def _monitor_loop(self) -> None:
114
+ """Health monitoring loop."""
115
+ while True:
116
+ # pylint: disable=too-many-try-statements,broad-exception-caught
117
+ try:
118
+ await asyncio.sleep(self.check_interval)
119
+ health = await self.check_health()
120
+ self.last_health = health
121
+
122
+ # Log health status
123
+ if health.status != "healthy":
124
+ logger = logging.getLogger(__name__)
125
+ logger.warning("Server health: %s", health.status)
126
+
127
+ except asyncio.CancelledError:
128
+ break
129
+ except Exception as e:
130
+ logger = logging.getLogger(__name__)
131
+ logger.error("Health check error: %s", e)
132
+
133
+ async def check_health(self) -> ServerHealth:
134
+ """Check server health.
135
+
136
+ Returns
137
+ -------
138
+ ServerHealth
139
+ Current health status
140
+ """
141
+ stats = self.server.get_stats()
142
+ # error_handler = self.server.error_handler
143
+
144
+ # Determine health status
145
+ status = "healthy"
146
+
147
+ # Check for error rate
148
+ total_messages = stats["messages_received"] + stats["messages_sent"]
149
+ if total_messages > 0:
150
+ error_rate = stats["errors_total"] / total_messages
151
+ if error_rate > 0.1: # More than 10% errors
152
+ status = "unhealthy"
153
+ elif error_rate > 0.05: # More than 5% errors
154
+ status = "degraded"
155
+
156
+ # Check if server is running
157
+ if not stats["is_running"]:
158
+ status = "unhealthy"
159
+
160
+ # Get memory usage (optional)
161
+ memory_usage: float | None = None
162
+ try:
163
+ # pylint: disable=import-outside-toplevel
164
+ # noinspection PyUnusedImports
165
+ import psutil
166
+
167
+ process = psutil.Process()
168
+ memory_usage = process.memory_info().rss / 1024 / 1024 # MB
169
+ except ImportError:
170
+ pass
171
+
172
+ return ServerHealth(
173
+ status=status,
174
+ uptime_seconds=stats["uptime_seconds"],
175
+ active_connections=stats["connections_active"],
176
+ total_connections=stats["connections_total"],
177
+ messages_received=stats["messages_received"],
178
+ messages_sent=stats["messages_sent"],
179
+ memory_usage_mb=memory_usage,
180
+ error_stats=stats["error_stats"],
181
+ )
182
+
183
+ def get_last_health(self) -> ServerHealth | None:
184
+ """Get last health check result.
185
+
186
+ Returns
187
+ -------
188
+ Optional[ServerHealth]
189
+ Last health status or None if no check performed
190
+ """
191
+ return self.last_health
192
+
193
+
194
+ # noinspection PyBroadException
195
+ class ConnectionManager:
196
+ """Manages WebSocket connections and provides utilities."""
197
+
198
+ def __init__(self, server: "WaldiezWsServer"):
199
+ """Initialize connection manager.
200
+
201
+ Parameters
202
+ ----------
203
+ server : WaldiezWsServer
204
+ Server instance
205
+ """
206
+ self.server = server
207
+
208
+ async def ping_all_clients(self) -> dict[str, bool]:
209
+ """Ping all connected clients.
210
+
211
+ Returns
212
+ -------
213
+ dict[str, bool]
214
+ Map of client_id to ping success status
215
+ """
216
+ results: dict[str, bool] = {}
217
+ ping_message: dict[str, Any] = {
218
+ "type": "pong",
219
+ "timestamp": time.time(),
220
+ }
221
+
222
+ for client_id, client in self.server.clients.items():
223
+ try:
224
+ success = await client.send_message(ping_message)
225
+ results[client_id] = success
226
+ except Exception: # pylint: disable=broad-exception-caught
227
+ results[client_id] = False
228
+
229
+ return results
230
+
231
+ async def disconnect_client(
232
+ self, client_id: str, reason: str = "Server initiated"
233
+ ) -> bool:
234
+ """Disconnect a specific client.
235
+
236
+ Parameters
237
+ ----------
238
+ client_id : str
239
+ Client to disconnect
240
+ reason : str
241
+ Disconnection reason
242
+
243
+ Returns
244
+ -------
245
+ bool
246
+ True if client was disconnected successfully
247
+ """
248
+ if client_id not in self.server.clients:
249
+ return False
250
+
251
+ client = self.server.clients[client_id]
252
+ try:
253
+ await client.send_message(
254
+ {
255
+ "type": "disconnect",
256
+ "reason": reason,
257
+ "timestamp": time.time(),
258
+ }
259
+ )
260
+ await client.websocket.close(code=1000, reason=reason)
261
+ return True
262
+ except Exception: # pylint: disable=broad-exception-caught
263
+ return False
264
+
265
+ def get_client_info(self, client_id: str) -> dict[str, Any] | None:
266
+ """Get information about a specific client.
267
+
268
+ Parameters
269
+ ----------
270
+ client_id : str
271
+ Client ID
272
+
273
+ Returns
274
+ -------
275
+ dict[str, Any] | None
276
+ Client information or None if not found
277
+ """
278
+ if client_id not in self.server.clients:
279
+ return None
280
+
281
+ client = self.server.clients[client_id]
282
+ return {
283
+ "client_id": client_id,
284
+ "remote_address": client.remote_address,
285
+ "user_agent": client.user_agent,
286
+ "is_active": client.is_active,
287
+ "connection_time": client.connection_time,
288
+ "connection_duration": client.connection_duration,
289
+ }
290
+
291
+ def list_clients(self) -> dict[str, dict[str, Any]]:
292
+ """List all connected clients.
293
+
294
+ Returns
295
+ -------
296
+ dict[str, dict[str, Any]]
297
+ Map of client_id to client information
298
+ """
299
+ clients = {
300
+ client_id: self.get_client_info(client_id)
301
+ for client_id in self.server.clients
302
+ }
303
+ return {
304
+ client_id: info
305
+ for client_id, info in clients.items()
306
+ if info is not None
307
+ }
308
+
309
+
310
+ async def test_server_connection(
311
+ host: str = "localhost", port: int = 8765, timeout: float = 5.0
312
+ ) -> dict[str, Any]:
313
+ """Test connection to WebSocket server.
314
+
315
+ Parameters
316
+ ----------
317
+ host : str
318
+ Server host
319
+ port : int
320
+ Server port
321
+ timeout : float
322
+ Connection timeout
323
+
324
+ Returns
325
+ -------
326
+ dict[str, Any]
327
+ Test results
328
+ """
329
+ start_time = time.time()
330
+ result: dict[str, Any] = {
331
+ "success": False,
332
+ "error": None,
333
+ "response_time_ms": 0.0,
334
+ "server_response": None,
335
+ }
336
+ # pylint: disable=too-many-try-statements,broad-exception-caught
337
+ try:
338
+ uri = f"ws://{host}:{port}"
339
+
340
+ async with websockets.connect(
341
+ uri,
342
+ ping_interval=None,
343
+ ) as websocket:
344
+ # Send ping message
345
+ ping_msg = {"action": "ping"}
346
+ await websocket.send(json.dumps(ping_msg))
347
+
348
+ # Wait for response
349
+ response = await asyncio.wait_for(websocket.recv(), timeout=timeout)
350
+ result["server_response"] = json.loads(response)
351
+ result["success"] = True
352
+
353
+ except asyncio.TimeoutError:
354
+ result["error"] = "Connection timeout"
355
+ except ConnectionRefusedError:
356
+ result["error"] = "Connection refused - server may not be running"
357
+ except Exception as e:
358
+ result["error"] = str(e)
359
+
360
+ result["response_time_ms"] = (time.time() - start_time) * 1000
361
+ return result
362
+
363
+
364
+ # noinspection PyBroadException
365
+ def is_port_available(port: int) -> bool:
366
+ """Check if the port is available.
367
+
368
+ Parameters
369
+ ----------
370
+ port : int
371
+ Port number
372
+
373
+ Returns
374
+ -------
375
+ bool
376
+ True if port is available
377
+ """
378
+ # Check IPv4
379
+ try:
380
+ with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
381
+ sock.bind(("", port))
382
+ except BaseException: # pylint: disable=broad-exception-caught
383
+ return False
384
+
385
+ # Check IPv6
386
+ try: # pragma: no cover
387
+ with closing(
388
+ socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
389
+ ) as sock:
390
+ # Disable dual-stack to only check IPv6
391
+ sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
392
+ sock.bind(("", port))
393
+ except BaseException: # pylint: disable=broad-exception-caught
394
+ return False
395
+
396
+ return True
397
+
398
+
399
+ def get_available_port() -> int:
400
+ """Get an available port.
401
+
402
+ Returns
403
+ -------
404
+ int
405
+ An available port number
406
+ """
407
+ with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as soc:
408
+ soc.bind(("", 0))
409
+ soc.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
410
+ return soc.getsockname()[1]