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.
- waldiez/_version.py +1 -1
- waldiez/cli.py +112 -24
- waldiez/exporting/agent/exporter.py +3 -0
- waldiez/exporting/agent/extras/captain_agent_extras.py +44 -7
- waldiez/exporting/agent/extras/handoffs/condition.py +3 -1
- waldiez/exporting/chats/utils/common.py +25 -23
- waldiez/exporting/core/__init__.py +0 -2
- waldiez/exporting/core/context.py +13 -13
- waldiez/exporting/core/protocols.py +0 -141
- waldiez/exporting/core/result.py +5 -5
- waldiez/exporting/flow/merger.py +2 -2
- waldiez/exporting/flow/orchestrator.py +1 -0
- waldiez/exporting/flow/utils/common.py +2 -2
- waldiez/exporting/flow/utils/importing.py +1 -0
- waldiez/exporting/flow/utils/logging.py +6 -7
- waldiez/exporting/tools/exporter.py +5 -0
- waldiez/exporting/tools/factory.py +4 -0
- waldiez/exporting/tools/processor.py +5 -1
- waldiez/io/_ws.py +13 -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 +17 -17
- waldiez/io/utils.py +1 -1
- waldiez/io/ws.py +9 -11
- waldiez/logger.py +180 -63
- waldiez/models/agents/agent/update_system_message.py +0 -2
- waldiez/models/agents/doc_agent/doc_agent.py +8 -1
- waldiez/models/common/dict_utils.py +169 -40
- waldiez/models/flow/flow.py +6 -6
- waldiez/models/flow/info.py +5 -1
- waldiez/models/model/_llm.py +28 -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 +474 -0
- waldiez/models/tool/predefined/_google.py +8 -6
- waldiez/models/tool/predefined/_perplexity.py +3 -0
- waldiez/models/tool/predefined/_searxng.py +3 -0
- waldiez/models/tool/predefined/_tavily.py +4 -1
- waldiez/models/tool/predefined/_wikipedia.py +4 -1
- 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 +310 -353
- waldiez/running/environment.py +1 -0
- waldiez/running/exceptions.py +9 -0
- waldiez/running/post_run.py +4 -4
- waldiez/running/pre_run.py +51 -40
- waldiez/running/protocol.py +21 -101
- waldiez/running/run_results.py +1 -1
- waldiez/running/standard_runner.py +84 -277
- waldiez/running/step_by_step/__init__.py +46 -0
- waldiez/running/step_by_step/breakpoints_mixin.py +188 -0
- waldiez/running/step_by_step/step_by_step_models.py +224 -0
- waldiez/running/step_by_step/step_by_step_runner.py +745 -0
- waldiez/running/subprocess_runner/__base__.py +282 -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 +455 -0
- waldiez/running/subprocess_runner/runner.py +561 -0
- waldiez/running/timeline_processor.py +1 -1
- waldiez/running/utils.py +376 -1
- waldiez/utils/version.py +2 -6
- waldiez/ws/__init__.py +70 -0
- waldiez/ws/__main__.py +15 -0
- waldiez/ws/_file_handler.py +201 -0
- waldiez/ws/cli.py +211 -0
- waldiez/ws/client_manager.py +835 -0
- waldiez/ws/errors.py +416 -0
- waldiez/ws/models.py +971 -0
- waldiez/ws/reloader.py +342 -0
- waldiez/ws/server.py +469 -0
- waldiez/ws/session_manager.py +393 -0
- waldiez/ws/session_stats.py +83 -0
- waldiez/ws/utils.py +385 -0
- {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/METADATA +74 -74
- {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/RECORD +87 -65
- waldiez/running/patch_io_stream.py +0 -210
- {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/WHEEL +0 -0
- {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/entry_points.txt +0 -0
- {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/licenses/LICENSE +0 -0
- {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
|
+
)
|