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
@@ -0,0 +1,393 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+ """Manages workflow sessions across WebSocket clients."""
4
+
5
+ import asyncio
6
+ import logging
7
+ import time
8
+ from collections import defaultdict
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from .models import ExecutionMode, SessionState, WorkflowSession, WorkflowStatus
13
+ from .session_stats import SessionStats
14
+
15
+
16
+ # noinspection TryExceptPass,PyBroadException
17
+ class SessionManager:
18
+ """Manage workflow sessions across WebSocket clients."""
19
+
20
+ def __init__(
21
+ self,
22
+ cleanup_interval: float = 300.0,
23
+ max_session_age: float = 3600.0,
24
+ ) -> None:
25
+ """Initialize the session manager.
26
+
27
+ Parameters
28
+ ----------
29
+ cleanup_interval : float
30
+ The interval at which to clean up expired sessions
31
+ max_session_age : float
32
+ The maximum age of a session before it is considered expired
33
+ """
34
+ self._sessions: dict[str, WorkflowSession] = {}
35
+ self._client_sessions: dict[str, list[str]] = defaultdict(list)
36
+ self._stats = SessionStats()
37
+ self._cleanup_interval = cleanup_interval
38
+ self._max_session_age = max_session_age
39
+ self._cleanup_task: asyncio.Task[Any] | None = None
40
+ self._lock = asyncio.Lock()
41
+ self._stop_event = asyncio.Event()
42
+ self._logger = logging.getLogger(__name__)
43
+
44
+ # ---------------- lifecycle ----------------
45
+
46
+ async def start(self) -> None:
47
+ """Start the session manager."""
48
+ self._stop_event.clear()
49
+ if self._cleanup_task is None:
50
+ self._cleanup_task = asyncio.create_task(self._cleanup_loop())
51
+
52
+ async def stop(self) -> None:
53
+ """Stop the session manager."""
54
+ self._stop_event.set()
55
+ if self._cleanup_task:
56
+ self._cleanup_task.cancel()
57
+ try:
58
+ await self._cleanup_task
59
+ except asyncio.CancelledError:
60
+ pass
61
+ self._cleanup_task = None
62
+ await self.cleanup_all_sessions()
63
+
64
+ # ---------------- session ops ----------------
65
+
66
+ async def create_session(
67
+ self,
68
+ session_id: str,
69
+ client_id: str,
70
+ execution_mode: ExecutionMode,
71
+ runner: Any = None,
72
+ temp_file: Path | None = None,
73
+ metadata: dict[str, Any] | None = None,
74
+ ) -> WorkflowSession:
75
+ """Create a new workflow session.
76
+
77
+ Parameters
78
+ ----------
79
+ session_id : str
80
+ The ID of the session to create
81
+ client_id : str
82
+ The ID of the client creating the session
83
+ execution_mode : ExecutionMode
84
+ The execution mode for the session
85
+ runner : Any, optional
86
+ The runner to use for the session
87
+ temp_file : Path | None, optional
88
+ The temporary file to use for the session
89
+ metadata : dict[str, Any] | None, optional
90
+ Metadata to associate with the session
91
+
92
+ Returns
93
+ -------
94
+ WorkflowSession
95
+ The created workflow session
96
+
97
+ Raises
98
+ ------
99
+ ValueError
100
+ If a session with the given ID already exists
101
+ """
102
+ session_state = SessionState(
103
+ session_id=session_id,
104
+ client_id=client_id,
105
+ status=WorkflowStatus.IDLE,
106
+ execution_mode=execution_mode,
107
+ metadata=metadata or {},
108
+ )
109
+ session = WorkflowSession(
110
+ session_state=session_state, runner=runner, temp_file=temp_file
111
+ )
112
+
113
+ async with self._lock:
114
+ if session_id in self._sessions:
115
+ raise ValueError(f"Session {session_id} already exists")
116
+ self._sessions[session_id] = session
117
+ self._client_sessions[client_id].append(session_id)
118
+ self._recompute_stats_locked()
119
+ return session
120
+
121
+ async def get_session(self, session_id: str) -> WorkflowSession | None:
122
+ """Get a workflow session by ID.
123
+
124
+ Parameters
125
+ ----------
126
+ session_id : str
127
+ The ID of the session to retrieve
128
+
129
+ Returns
130
+ -------
131
+ WorkflowSession | None
132
+ The workflow session with the given ID, or None if it does not exist
133
+ """
134
+ async with self._lock:
135
+ return self._sessions.get(session_id)
136
+
137
+ async def get_client_sessions(
138
+ self, client_id: str
139
+ ) -> list[WorkflowSession]:
140
+ """Get all workflow sessions for a client.
141
+
142
+ Parameters
143
+ ----------
144
+ client_id : str
145
+ The ID of the client to retrieve sessions for
146
+
147
+ Returns
148
+ -------
149
+ list[WorkflowSession]
150
+ A list of workflow sessions for the client
151
+ """
152
+ async with self._lock:
153
+ ids = list(self._client_sessions.get(client_id, []))
154
+ return [self._sessions[sid] for sid in ids if sid in self._sessions]
155
+
156
+ async def update_session_status(
157
+ self,
158
+ session_id: str,
159
+ new_status: WorkflowStatus,
160
+ ) -> bool:
161
+ """Update the status of a workflow session.
162
+
163
+ Parameters
164
+ ----------
165
+ session_id : str
166
+ The ID of the session to update
167
+ new_status : WorkflowStatus
168
+ The new status to set for the session
169
+
170
+ Returns
171
+ -------
172
+ bool
173
+ True if the status was updated, False if the session was not found
174
+ """
175
+ async with self._lock:
176
+ session = self._sessions.get(session_id)
177
+ if not session:
178
+ return False
179
+ session.update_status(new_status)
180
+ self._recompute_stats_locked()
181
+ return True
182
+
183
+ async def get_session_execution_mode(
184
+ self, session_id: str
185
+ ) -> ExecutionMode | None:
186
+ """Get the execution mode of a workflow session.
187
+
188
+ Parameters
189
+ ----------
190
+ session_id : str
191
+ The ID of the session to retrieve the execution mode for
192
+
193
+ Returns
194
+ -------
195
+ ExecutionMode | None
196
+ The execution mode of the session, or None if it does not exist
197
+ """
198
+ async with self._lock:
199
+ s = self._sessions.get(session_id)
200
+ return s.execution_mode if s else None
201
+
202
+ async def remove_session(self, session_id: str) -> bool:
203
+ """Remove a workflow session by ID.
204
+
205
+ Parameters
206
+ ----------
207
+ session_id : str
208
+ The ID of the session to remove
209
+
210
+ Returns
211
+ -------
212
+ bool
213
+ True if the session was removed, False if it did not exist
214
+ """
215
+ self._logger.debug("Removing session %s", session_id)
216
+ # Detach under lock
217
+ async with self._lock:
218
+ session = self._sessions.pop(session_id, None)
219
+ if not session:
220
+ return False
221
+ client_id = session.client_id
222
+ if client_id in self._client_sessions:
223
+ try:
224
+ self._client_sessions[client_id].remove(session_id)
225
+ if not self._client_sessions[client_id]:
226
+ del self._client_sessions[client_id]
227
+ except ValueError:
228
+ pass
229
+ # Cleanup outside lock
230
+ try:
231
+ session.cleanup()
232
+ except Exception: # pylint: disable=broad-exception-caught
233
+ pass
234
+ # Update stats under lock
235
+ async with self._lock:
236
+ self._stats.cleanup_count += 1
237
+ self._recompute_stats_locked()
238
+ return True
239
+
240
+ async def remove_client_sessions(self, client_id: str) -> int:
241
+ """Remove all workflow sessions for a client.
242
+
243
+ Parameters
244
+ ----------
245
+ client_id : str
246
+ The ID of the client whose sessions should be removed
247
+
248
+ Returns
249
+ -------
250
+ int
251
+ Number of removed sessions
252
+ """
253
+ async with self._lock:
254
+ sids = list(self._client_sessions.get(client_id, []))
255
+ removed = 0
256
+ for sid in sids:
257
+ if await self.remove_session(sid):
258
+ removed += 1
259
+ return removed
260
+
261
+ # ---------------- stats / status ----------------
262
+
263
+ async def get_stats(self) -> SessionStats:
264
+ """Get session statistics.
265
+
266
+ Returns
267
+ -------
268
+ SessionStats
269
+ Session statistics
270
+ """
271
+ async with self._lock:
272
+ self._recompute_stats_locked()
273
+ return self._stats
274
+
275
+ async def get_session_count(self) -> int:
276
+ """Get total number of sessions.
277
+
278
+ Returns
279
+ -------
280
+ int
281
+ Total number of sessions
282
+ """
283
+ async with self._lock:
284
+ return len(self._sessions)
285
+
286
+ async def get_client_count(self) -> int:
287
+ """Get number of clients with sessions.
288
+
289
+ Returns
290
+ -------
291
+ int
292
+ Number of clients with sessions
293
+ """
294
+ async with self._lock:
295
+ return len(self._client_sessions)
296
+
297
+ async def get_status(self) -> dict[str, Any]:
298
+ """Get detailed status of the session manager.
299
+
300
+ Returns
301
+ -------
302
+ dict[str, Any]
303
+ Detailed status information
304
+ """
305
+ stats = await self.get_stats()
306
+ async with self._lock:
307
+ return {
308
+ "session_manager": {
309
+ "total_sessions": len(self._sessions),
310
+ "total_clients": len(self._client_sessions),
311
+ "cleanup_interval": self._cleanup_interval,
312
+ "max_session_age": self._max_session_age,
313
+ "cleanup_task_running": self._cleanup_task is not None
314
+ and not self._cleanup_task.done(),
315
+ },
316
+ "statistics": stats.model_dump(),
317
+ "timestamp": time.time(),
318
+ }
319
+
320
+ # ---------------- cleanup ----------------
321
+
322
+ async def cleanup_old_sessions(self, max_age: float | None = None) -> int:
323
+ """Cleanup old sessions.
324
+
325
+ Parameters
326
+ ----------
327
+ max_age : float | None
328
+ The maximum age of sessions to clean up
329
+
330
+ Returns
331
+ -------
332
+ int
333
+ The number of sessions removed
334
+ """
335
+ max_age = max_age or self._max_session_age
336
+ now_ns = time.monotonic_ns()
337
+
338
+ async with self._lock:
339
+ to_remove: list[str] = []
340
+ for sid, session in self._sessions.items():
341
+ remove = False
342
+ if session.raw_state.is_completed:
343
+ from_now_ns = now_ns - (
344
+ session.raw_state.end_time
345
+ or session.raw_state.start_time
346
+ )
347
+ age_s = from_now_ns / 1_000_000_000
348
+ remove = age_s > max_age
349
+ elif not session.raw_state.is_active:
350
+ age_ns = now_ns - session.last_accessed
351
+ age_s = age_ns / 1_000_000_000
352
+ remove = age_s > (max_age * 2)
353
+ if remove:
354
+ to_remove.append(sid)
355
+
356
+ removed = 0
357
+ for sid in to_remove:
358
+ if await self.remove_session(sid):
359
+ removed += 1
360
+ return removed
361
+
362
+ async def cleanup_all_sessions(self) -> None:
363
+ """Cleanup all sessions."""
364
+ # Detach under lock
365
+ async with self._lock:
366
+ sessions = list(self._sessions.values())
367
+ self._sessions.clear()
368
+ self._client_sessions.clear()
369
+ self._stats = SessionStats()
370
+ # Cleanup outside lock
371
+ for s in sessions:
372
+ try:
373
+ s.cleanup()
374
+ except Exception: # pylint: disable=broad-exception-caught
375
+ pass
376
+
377
+ async def _cleanup_loop(self) -> None:
378
+ """Cleanup loop for old sessions."""
379
+ while not self._stop_event.is_set():
380
+ try:
381
+ await asyncio.sleep(self._cleanup_interval)
382
+ await self.cleanup_old_sessions()
383
+ except asyncio.CancelledError:
384
+ break
385
+ except Exception: # pylint: disable=broad-exception-caught
386
+ async with self._lock:
387
+ self._stats.error_count += 1
388
+
389
+ # ---------------- internal ----------------
390
+
391
+ def _recompute_stats_locked(self) -> None:
392
+ """Recompute session statistics."""
393
+ self._stats.update_from_sessions(list(self._sessions.values()))
@@ -0,0 +1,83 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+ # pyright: reportUnknownVariableType=false
4
+ # pylint: disable=broad-exception-caught,no-member
5
+
6
+ """Session statistics model."""
7
+
8
+ from pydantic import BaseModel, Field
9
+
10
+ from .models import WorkflowSession, WorkflowStatus
11
+
12
+
13
+ class SessionStats(BaseModel):
14
+ """Statistics for session management."""
15
+
16
+ total_sessions: int = 0
17
+ active_sessions: int = 0
18
+ completed_sessions: int = 0
19
+ failed_sessions: int = 0
20
+ cancelled_sessions: int = 0
21
+
22
+ sessions_by_client: dict[str, int] = Field(default_factory=dict)
23
+ sessions_by_mode: dict[str, int] = Field(default_factory=dict)
24
+ sessions_by_status: dict[str, int] = Field(default_factory=dict)
25
+
26
+ average_duration: float = 0.0
27
+ total_duration: float = 0.0
28
+
29
+ cleanup_count: int = 0
30
+ error_count: int = 0
31
+
32
+ def update_from_sessions(self, sessions: list[WorkflowSession]) -> None:
33
+ """Update stats from current sessions.
34
+
35
+ Parameters
36
+ ----------
37
+ sessions : list[WorkflowSession]
38
+ The list of sessions to update stats from.
39
+ """
40
+ # Reset counters
41
+ self.total_sessions = len(sessions)
42
+ self.active_sessions = 0
43
+ self.completed_sessions = 0
44
+ self.failed_sessions = 0
45
+ self.cancelled_sessions = 0
46
+
47
+ self.sessions_by_client.clear()
48
+ self.sessions_by_mode.clear()
49
+ self.sessions_by_status.clear()
50
+
51
+ total_duration = 0.0
52
+ completed_count = 0
53
+
54
+ for session in sessions:
55
+ state = session.state
56
+
57
+ if state.status == WorkflowStatus.COMPLETED:
58
+ self.completed_sessions += 1
59
+ elif state.status == WorkflowStatus.FAILED:
60
+ self.failed_sessions += 1
61
+ elif state.status == WorkflowStatus.CANCELLED:
62
+ self.cancelled_sessions += 1
63
+ elif state.is_active:
64
+ self.active_sessions += 1
65
+
66
+ client_count = self.sessions_by_client.get(state.client_id, 0)
67
+ self.sessions_by_client[state.client_id] = client_count + 1
68
+ mode_key = state.execution_mode.value
69
+ mode_count = self.sessions_by_mode.get(mode_key, 0)
70
+ self.sessions_by_mode[mode_key] = mode_count + 1
71
+ status_key = state.status.value
72
+ # noinspection PyTypeChecker
73
+ status_count = self.sessions_by_status.get(status_key, 0)
74
+ # noinspection PyTypeChecker
75
+ self.sessions_by_status[status_key] = status_count + 1
76
+ if state.is_completed:
77
+ total_duration += state.duration
78
+ completed_count += 1
79
+
80
+ self.total_duration = total_duration
81
+ self.average_duration = (
82
+ total_duration / completed_count if completed_count > 0 else 0.0
83
+ )