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/models.py ADDED
@@ -0,0 +1,988 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+ # pylint: disable=broad-exception-caught,no-member
4
+ # pyright: reportUnknownVariableType=false
5
+ """Session management models for WebSocket workflow execution."""
6
+
7
+ import json
8
+ import time
9
+ import uuid
10
+ from enum import Enum
11
+ from pathlib import Path
12
+ from typing import Annotated, Any, Literal, Union
13
+
14
+ from pydantic import BaseModel, ConfigDict, Field, ValidationError
15
+
16
+
17
+ class WorkflowStatus(str, Enum):
18
+ """Workflow execution status."""
19
+
20
+ IDLE = "idle"
21
+ STARTING = "starting"
22
+ RUNNING = "running"
23
+ PAUSED = "paused"
24
+ STEP_WAITING = "step_waiting"
25
+ INPUT_WAITING = "input_waiting"
26
+ STOPPING = "stopping"
27
+ COMPLETED = "completed"
28
+ FAILED = "failed"
29
+ CANCELLED = "cancelled"
30
+
31
+
32
+ class ExecutionMode(str, Enum):
33
+ """Workflow execution modes."""
34
+
35
+ STANDARD = "standard"
36
+ STEP_BY_STEP = "step_by_step"
37
+ SUBPROCESS = "subprocess"
38
+
39
+
40
+ class BaseWaldiezMessage(BaseModel):
41
+ """Base class for all Waldiez WebSocket messages."""
42
+
43
+ message_id: str = Field(
44
+ default_factory=lambda: f"msg_{time.monotonic_ns()}",
45
+ )
46
+ timestamp: float = Field(default_factory=time.time)
47
+
48
+ model_config = ConfigDict(
49
+ arbitrary_types_allowed=True,
50
+ extra="ignore",
51
+ json_encoders={
52
+ Path: str,
53
+ Enum: lambda v: v.value,
54
+ },
55
+ )
56
+
57
+
58
+ class BaseRequest(BaseWaldiezMessage):
59
+ """Base class for client requests."""
60
+
61
+
62
+ class BaseResponse(BaseWaldiezMessage):
63
+ """Base class for server responses."""
64
+
65
+ request_id: str | None = None
66
+ success: bool = True
67
+ error: str | None = None
68
+
69
+ @classmethod
70
+ def ok(cls, **kwargs: Any) -> "BaseResponse":
71
+ """Create a successful response.
72
+
73
+ Parameters
74
+ ----------
75
+ kwargs : Any
76
+ Additional keyword arguments to include in the response.
77
+
78
+ Returns
79
+ -------
80
+ BaseResponse
81
+ The created successful response.
82
+
83
+ """
84
+ kwargs.setdefault("success", True)
85
+ kwargs.setdefault("error", None)
86
+ return cls(**kwargs)
87
+
88
+ @classmethod
89
+ def fail(cls, error: str, **kwargs: Any) -> "BaseResponse":
90
+ """
91
+ Create a failed response.
92
+
93
+ Parameters
94
+ ----------
95
+ error : str
96
+ The error message.
97
+ kwargs : Any
98
+ Additional keyword arguments to include in the response.
99
+
100
+ Returns
101
+ -------
102
+ BaseResponse
103
+ The created failed response.
104
+
105
+ """
106
+ kwargs.update({"success": False, "error": error})
107
+ return cls(**kwargs)
108
+
109
+
110
+ class BaseNotification(BaseWaldiezMessage):
111
+ """Base class for server notifications (no response expected)."""
112
+
113
+
114
+ # ========================================
115
+ # CLIENT-TO-SERVER MESSAGES (REQUESTS)
116
+ # ========================================
117
+
118
+
119
+ class SaveFlowRequest(BaseRequest):
120
+ """Request to save a workflow."""
121
+
122
+ type: Literal["save_flow"] = "save_flow"
123
+ flow_data: str # JSON string of workflow
124
+ filename: str | None = None
125
+ force_overwrite: bool = False
126
+
127
+
128
+ class RunWorkflowRequest(BaseRequest):
129
+ """Request to run a workflow in standard mode."""
130
+
131
+ type: Literal["run_workflow"] = "run_workflow"
132
+ flow_data: str # JSON string of workflow
133
+ execution_mode: ExecutionMode = ExecutionMode.STANDARD
134
+ structured_io: bool = True
135
+ uploads_root: str | None = None
136
+ dot_env_path: str | None = None
137
+ output_path: str | None = None
138
+
139
+
140
+ class StepRunWorkflowRequest(BaseRequest):
141
+ """Request to run a workflow in step-by-step mode."""
142
+
143
+ type: Literal["step_run_workflow"] = "step_run_workflow"
144
+ flow_data: str # JSON string of workflow
145
+ auto_continue: bool = False
146
+ breakpoints: list[str] = Field(default_factory=list)
147
+ structured_io: bool = True
148
+ uploads_root: str | None = None
149
+ dot_env_path: str | None = None
150
+ output_path: str | None = None
151
+
152
+
153
+ class StepControlRequest(BaseRequest):
154
+ """Request to control step-by-step execution."""
155
+
156
+ type: Literal["step_control"] = "step_control"
157
+ action: Literal[
158
+ "continue",
159
+ "step",
160
+ "run",
161
+ "quit",
162
+ "info",
163
+ "help",
164
+ "stats",
165
+ "",
166
+ "c",
167
+ "s",
168
+ "r",
169
+ "q",
170
+ "i",
171
+ "h",
172
+ "?",
173
+ "st",
174
+ ]
175
+ session_id: str
176
+
177
+
178
+ class BreakpointRequest(BaseRequest):
179
+ """Request to manage breakpoints."""
180
+
181
+ type: Literal["breakpoint_control"] = "breakpoint_control"
182
+ action: Literal["add", "remove", "list", "clear"]
183
+ event_type: str | None = None # Required for add/remove
184
+ session_id: str
185
+
186
+ @property
187
+ def response(self) -> Literal["added", "removed", "list", "cleared"]:
188
+ """Get the response message for the breakpoint action."""
189
+ if self.action == "remove":
190
+ return "removed"
191
+ if self.action == "list":
192
+ return "list"
193
+ if self.action == "clear":
194
+ return "cleared"
195
+ return "added"
196
+
197
+
198
+ class UserInputResponse(BaseRequest):
199
+ """User input response for workflow execution."""
200
+
201
+ type: Literal["user_input"] = "user_input"
202
+ request_id: str
203
+ data: Any
204
+ session_id: str
205
+
206
+
207
+ class StopWorkflowRequest(BaseRequest):
208
+ """Request to stop workflow execution."""
209
+
210
+ type: Literal["stop_workflow"] = "stop_workflow"
211
+ session_id: str
212
+ force: bool = False
213
+
214
+
215
+ class StopWorkflowResponse(BaseResponse):
216
+ """Response to stop workflow request."""
217
+
218
+ session_id: str
219
+ type: Literal["stop_workflow_response"] = "stop_workflow_response"
220
+ error: str | None = None
221
+ forced: bool = False
222
+
223
+
224
+ class ConvertWorkflowRequest(BaseRequest):
225
+ """Request to convert workflow to different format."""
226
+
227
+ type: Literal["convert_workflow"] = "convert_workflow"
228
+ flow_data: str
229
+ target_format: Literal["py", "ipynb"]
230
+ output_path: str | None = None
231
+
232
+
233
+ class UploadFileRequest(BaseRequest):
234
+ """Request to upload a file."""
235
+
236
+ type: Literal["upload_file"] = "upload_file"
237
+ filename: str
238
+ file_data: str # Base64 encoded
239
+ file_size: int
240
+ mime_type: str | None = None
241
+
242
+
243
+ class PingRequest(BaseRequest):
244
+ """Ping request for connection testing."""
245
+
246
+ type: Literal["ping"] = "ping"
247
+ echo_data: dict[str, Any] = Field(default_factory=dict)
248
+
249
+
250
+ class GetStatusRequest(BaseRequest):
251
+ """Request current server/workflow status."""
252
+
253
+ type: Literal["get_status"] = "get_status"
254
+ session_id: str | None = None
255
+
256
+
257
+ # ========================================
258
+ # SERVER-TO-CLIENT MESSAGES (RESPONSES)
259
+ # ========================================
260
+
261
+
262
+ class SaveFlowResponse(BaseResponse):
263
+ """Response to save flow request."""
264
+
265
+ type: Literal["save_flow_response"] = "save_flow_response"
266
+ file_path: str | None = None
267
+ error: str | None = None
268
+
269
+
270
+ class RunWorkflowResponse(BaseResponse):
271
+ """Response to run workflow request."""
272
+
273
+ type: Literal["run_workflow_response"] = "run_workflow_response"
274
+ session_id: str
275
+ execution_mode: ExecutionMode
276
+ error: str | None = None
277
+
278
+
279
+ class StepRunWorkflowResponse(BaseResponse):
280
+ """Response to step run workflow request."""
281
+
282
+ type: Literal["step_run_workflow_response"] = "step_run_workflow_response"
283
+ session_id: str
284
+ auto_continue: bool
285
+ breakpoints: list[str]
286
+ error: str | None = None
287
+
288
+
289
+ class StepControlResponse(BaseResponse):
290
+ """Response to step control request."""
291
+
292
+ type: Literal["step_control_response"] = "step_control_response"
293
+ action: str
294
+ result: str
295
+ session_id: str
296
+
297
+
298
+ class BreakpointResponse(BaseResponse):
299
+ """Response to breakpoint management."""
300
+
301
+ type: Literal["breakpoint_response"] = "breakpoint_response"
302
+ action: str
303
+ breakpoints: list[str] = Field(default_factory=list)
304
+ session_id: str
305
+ error: str | None = None
306
+
307
+
308
+ class ConvertWorkflowResponse(BaseResponse):
309
+ """Response to convert workflow request."""
310
+
311
+ type: Literal["convert_workflow_response"] = "convert_workflow_response"
312
+ target_format: str
313
+ output_path: str | None = None
314
+ error: str | None = None
315
+
316
+
317
+ class UploadFileResponse(BaseResponse):
318
+ """Response to file upload."""
319
+
320
+ type: Literal["upload_file_response"] = "upload_file_response"
321
+ file_path: str | None = None
322
+ file_size: int | None = None
323
+ error: str | None = None
324
+
325
+
326
+ class PongResponse(BaseResponse):
327
+ """Response to ping."""
328
+
329
+ type: Literal["pong"] = "pong"
330
+ echo_data: dict[str, Any] = Field(default_factory=dict)
331
+ server_time: float = Field(default_factory=time.time)
332
+
333
+
334
+ class StatusResponse(BaseResponse):
335
+ """Response with current status."""
336
+
337
+ type: Literal["status_response"] = "status_response"
338
+ server_status: dict[str, Any]
339
+ workflow_status: WorkflowStatus | None = None
340
+ session_id: str | None = None
341
+
342
+
343
+ class ErrorResponse(BaseResponse):
344
+ """Generic error response."""
345
+
346
+ type: Literal["error"] = "error"
347
+ error_code: int
348
+ error_type: str
349
+ details: dict[str, Any] = Field(default_factory=dict)
350
+ success: bool = False
351
+
352
+
353
+ # ========================================
354
+ # SERVER-TO-CLIENT NOTIFICATIONS
355
+ # ========================================
356
+
357
+
358
+ class WorkflowStatusNotification(BaseNotification):
359
+ """Notification of workflow status change."""
360
+
361
+ type: Literal["workflow_status"] = "workflow_status"
362
+ session_id: str
363
+ status: WorkflowStatus
364
+ execution_mode: ExecutionMode
365
+ details: str | None = None
366
+
367
+ @classmethod
368
+ def make(
369
+ cls,
370
+ session_id: str,
371
+ status: WorkflowStatus,
372
+ mode: ExecutionMode,
373
+ details: str | None = None,
374
+ ) -> "WorkflowStatusNotification":
375
+ """Create a workflow status notification.
376
+
377
+ Parameters
378
+ ----------
379
+ session_id : str
380
+ The session ID.
381
+ status : WorkflowStatus
382
+ The workflow status.
383
+ mode : ExecutionMode
384
+ The execution mode.
385
+ details : str | None
386
+ Additional details about the status.
387
+
388
+ Returns
389
+ -------
390
+ WorkflowStatusNotification
391
+ The created workflow status notification.
392
+ """
393
+ return cls(
394
+ session_id=session_id,
395
+ status=status,
396
+ execution_mode=mode,
397
+ details=details,
398
+ )
399
+
400
+
401
+ class WorkflowOutputNotification(BaseNotification):
402
+ """Notification of workflow output."""
403
+
404
+ type: Literal["workflow_output"] = "workflow_output"
405
+ session_id: str
406
+ stream: Literal["stdout", "stderr"]
407
+ content: str
408
+ output_type: Literal["text", "structured", "debug"] = "text"
409
+
410
+ @classmethod
411
+ def stdout(cls, session_id: str, text: str) -> "WorkflowOutputNotification":
412
+ """Create a workflow output notification for stdout.
413
+
414
+ Parameters
415
+ ----------
416
+ session_id : str
417
+ The session ID.
418
+ text : str
419
+ The output text.
420
+
421
+ Returns
422
+ -------
423
+ WorkflowOutputNotification
424
+ The created workflow output notification.
425
+ """
426
+ return cls(session_id=session_id, stream="stdout", content=text)
427
+
428
+ @classmethod
429
+ def stderr(cls, session_id: str, text: str) -> "WorkflowOutputNotification":
430
+ """Create a workflow output notification for stderr.
431
+
432
+ Parameters
433
+ ----------
434
+ session_id : str
435
+ The session ID.
436
+ text : str
437
+ The output text.
438
+
439
+ Returns
440
+ -------
441
+ WorkflowOutputNotification
442
+ The created workflow output notification.
443
+ """
444
+ return cls(session_id=session_id, stream="stderr", content=text)
445
+
446
+
447
+ class WorkflowEventNotification(BaseNotification):
448
+ """Notification of workflow event (for step-by-step)."""
449
+
450
+ type: Literal["workflow_event"] = "workflow_event"
451
+ session_id: str
452
+ event_data: dict[str, Any]
453
+ event_count: int
454
+ should_break: bool = False
455
+
456
+
457
+ class UserInputRequestNotification(BaseNotification):
458
+ """Notification requesting user input."""
459
+
460
+ type: Literal["input_request"] = "input_request"
461
+ session_id: str
462
+ request_id: str
463
+ prompt: str
464
+ password: bool = False
465
+ timeout: float = 120.0
466
+
467
+
468
+ class BreakpointNotification(BaseNotification):
469
+ """Notification about breakpoint changes."""
470
+
471
+ type: Literal["breakpoint_notification"] = "breakpoint_notification"
472
+ session_id: str
473
+ action: Literal["added", "removed", "cleared", "list"]
474
+ event_type: str | None = None
475
+ breakpoints: list[str] = Field(default_factory=list)
476
+ message: str | None = None
477
+
478
+
479
+ class WorkflowCompletionNotification(BaseNotification):
480
+ """Notification of workflow completion."""
481
+
482
+ type: Literal["workflow_completion"] = "workflow_completion"
483
+ session_id: str
484
+ success: bool
485
+ exit_code: int | None = None
486
+ results: list[dict[str, Any]] = Field(default_factory=list)
487
+ execution_time: float | None = None
488
+ error: str | None = None
489
+
490
+
491
+ class StepDebugNotification(BaseNotification):
492
+ """Notification for step-by-step debug information."""
493
+
494
+ type: Literal["step_debug"] = "step_debug"
495
+ session_id: str
496
+ debug_type: Literal["stats", "help", "error", "info"]
497
+ data: dict[str, Any]
498
+
499
+
500
+ class ConnectionNotification(BaseNotification):
501
+ """Notification about connection status."""
502
+
503
+ type: Literal["connection"] = "connection"
504
+ status: Literal["connected", "disconnected", "error"]
505
+ client_id: str
506
+ server_time: float = Field(default_factory=time.time)
507
+ message: str | None = None
508
+
509
+
510
+ # ========================================
511
+ # SUBPROCESS-SPECIFIC MODELS
512
+ # ========================================
513
+
514
+
515
+ class SubprocessOutputNotification(BaseNotification):
516
+ """Notification from subprocess execution."""
517
+
518
+ type: Literal["subprocess_output"] = "subprocess_output"
519
+ session_id: str
520
+ stream: Literal["stdout", "stderr"]
521
+ content: str
522
+ subprocess_type: Literal["output", "error", "debug"] = "output"
523
+ context: dict[str, Any] = Field(default_factory=dict)
524
+
525
+
526
+ class SubprocessInputRequestNotification(BaseNotification):
527
+ """Input request from subprocess."""
528
+
529
+ type: Literal["subprocess_input_request"] = "subprocess_input_request"
530
+ session_id: str
531
+ request_id: str
532
+ prompt: str
533
+ timeout: float = 120.0
534
+ password: bool = False
535
+ context: dict[str, Any] = Field(default_factory=dict)
536
+
537
+
538
+ class SubprocessCompletionNotification(BaseNotification):
539
+ """Subprocess execution completion."""
540
+
541
+ type: Literal["subprocess_completion"] = "subprocess_completion"
542
+ session_id: str
543
+ success: bool
544
+ exit_code: int
545
+ message: str
546
+ context: dict[str, Any] = Field(default_factory=dict)
547
+
548
+
549
+ # ========================================
550
+ # UNION TYPES FOR MESSAGE PARSING
551
+ # ========================================
552
+
553
+ # Client-to-Server Messages
554
+ ClientMessage = Annotated[
555
+ Union[
556
+ SaveFlowRequest,
557
+ RunWorkflowRequest,
558
+ StepRunWorkflowRequest,
559
+ StepControlRequest,
560
+ BreakpointRequest,
561
+ UserInputResponse,
562
+ StopWorkflowRequest,
563
+ ConvertWorkflowRequest,
564
+ UploadFileRequest,
565
+ PingRequest,
566
+ GetStatusRequest,
567
+ ],
568
+ Field(discriminator="type"),
569
+ ]
570
+
571
+ # Server-to-Client Messages
572
+ ServerMessage = Annotated[
573
+ Union[
574
+ # Responses
575
+ SaveFlowResponse,
576
+ RunWorkflowResponse,
577
+ StepRunWorkflowResponse,
578
+ StopWorkflowResponse,
579
+ StepControlResponse,
580
+ BreakpointResponse,
581
+ BreakpointNotification,
582
+ ConvertWorkflowResponse,
583
+ UploadFileResponse,
584
+ PongResponse,
585
+ StatusResponse,
586
+ ErrorResponse,
587
+ # Notifications
588
+ WorkflowStatusNotification,
589
+ WorkflowOutputNotification,
590
+ WorkflowEventNotification,
591
+ UserInputRequestNotification,
592
+ WorkflowCompletionNotification,
593
+ StepDebugNotification,
594
+ ConnectionNotification,
595
+ SubprocessOutputNotification,
596
+ SubprocessInputRequestNotification,
597
+ SubprocessCompletionNotification,
598
+ ],
599
+ Field(discriminator="type"),
600
+ ]
601
+
602
+
603
+ class _ServerMessageWrapper(BaseModel):
604
+ """Wrapper for server messages to handle discriminators."""
605
+
606
+ # noinspection PyTypeHints
607
+ message: ServerMessage
608
+
609
+
610
+ class _ClientMessageWrapper(BaseModel):
611
+ """Wrapper for client messages to handle discriminators."""
612
+
613
+ # noinspection PyTypeHints
614
+ message: ClientMessage
615
+
616
+
617
+ # All messages
618
+ WaldiezWsMessage = Annotated[
619
+ Union[ClientMessage, ServerMessage],
620
+ Field(discriminator="type"),
621
+ ]
622
+
623
+
624
+ # ========================================
625
+ # UTILITY FUNCTIONS
626
+ # ========================================
627
+
628
+
629
+ # noinspection PyTypeHints,DuplicatedCode
630
+ def parse_client_message(data: str | dict[str, Any]) -> ClientMessage:
631
+ """Parse client message from JSON string or dict.
632
+
633
+ Parameters
634
+ ----------
635
+ data : str | dict[str, Any]
636
+ Message data
637
+
638
+ Returns
639
+ -------
640
+ ClientMessage
641
+ Parsed client message
642
+
643
+ Raises
644
+ ------
645
+ ValueError
646
+ If message cannot be parsed
647
+ """
648
+ if isinstance(data, str):
649
+ try:
650
+ data = json.loads(data)
651
+ except json.JSONDecodeError as e:
652
+ raise ValueError(f"Invalid message format: {e}") from e
653
+ try:
654
+ return _ClientMessageWrapper.model_validate({"message": data}).message
655
+ except ValidationError as e:
656
+ raise ValueError(f"Invalid client message: {e}") from e
657
+
658
+
659
+ # noinspection PyTypeHints,DuplicatedCode
660
+ def parse_server_message(data: str | dict[str, Any]) -> ServerMessage:
661
+ """Parse server message from JSON string or dict.
662
+
663
+ Parameters
664
+ ----------
665
+ data : str | dict[str, Any]
666
+ Message data
667
+
668
+ Returns
669
+ -------
670
+ ServerMessage
671
+ Parsed server message
672
+
673
+ Raises
674
+ ------
675
+ ValueError
676
+ If message cannot be parsed
677
+ """
678
+ if isinstance(data, str):
679
+ try:
680
+ data = json.loads(data)
681
+ except json.JSONDecodeError as e:
682
+ raise ValueError(f"Invalid message format: {e}") from e
683
+ try:
684
+ return _ServerMessageWrapper.model_validate({"message": data}).message
685
+ except ValidationError as e:
686
+ raise ValueError(f"Invalid server message: {e}") from e
687
+
688
+
689
+ def create_error_response(
690
+ error_message: str,
691
+ error_code: int = 500,
692
+ error_type: str = "InternalError",
693
+ request_id: str | None = None,
694
+ details: dict[str, Any] | None = None,
695
+ ) -> ErrorResponse:
696
+ """Create standardized error response.
697
+
698
+ Parameters
699
+ ----------
700
+ error_message : str
701
+ Error message
702
+ error_code : int
703
+ Error code
704
+ error_type : str
705
+ Error type
706
+ request_id : str | None
707
+ Request ID if this is response to a request
708
+ details : dict[str, Any] | None
709
+ Additional error details
710
+
711
+ Returns
712
+ -------
713
+ ErrorResponse
714
+ Standardized error response
715
+ """
716
+ return ErrorResponse(
717
+ request_id=request_id,
718
+ error_code=error_code,
719
+ error_type=error_type,
720
+ error=error_message,
721
+ details=details or {},
722
+ )
723
+
724
+
725
+ def create_session_id() -> str:
726
+ """Create a new session ID.
727
+
728
+ Returns
729
+ -------
730
+ str
731
+ New session ID
732
+ """
733
+ return f"session_{uuid.uuid4().hex[:12]}"
734
+
735
+
736
+ # ========================================
737
+ # MESSAGE TYPE CONSTANTS
738
+ # ========================================
739
+
740
+ CLIENT_MESSAGE_TYPES = {
741
+ "save_flow",
742
+ "run_workflow",
743
+ "step_run_workflow",
744
+ "step_control",
745
+ "breakpoint_control",
746
+ "user_input",
747
+ "stop_workflow",
748
+ "convert_workflow",
749
+ "upload_file",
750
+ "ping",
751
+ "get_status",
752
+ }
753
+
754
+ SERVER_MESSAGE_TYPES = {
755
+ # Responses
756
+ "save_flow_response",
757
+ "run_workflow_response",
758
+ "step_run_workflow_response",
759
+ "step_control_response",
760
+ "breakpoint_response",
761
+ "convert_workflow_response",
762
+ "upload_file_response",
763
+ "pong",
764
+ "status_response",
765
+ "error",
766
+ # Notifications
767
+ "workflow_status",
768
+ "workflow_output",
769
+ "workflow_event",
770
+ "input_request",
771
+ "breakpoint_notification",
772
+ "workflow_completion",
773
+ "step_debug",
774
+ "connection",
775
+ "subprocess_output",
776
+ "subprocess_input_request",
777
+ "subprocess_completion",
778
+ }
779
+
780
+ ALL_MESSAGE_TYPES = CLIENT_MESSAGE_TYPES | SERVER_MESSAGE_TYPES
781
+
782
+
783
+ class SessionState(BaseModel):
784
+ """State information for a workflow session."""
785
+
786
+ session_id: str
787
+ client_id: str
788
+ status: WorkflowStatus
789
+ execution_mode: ExecutionMode
790
+ start_time: int = Field(default_factory=time.monotonic_ns)
791
+ end_time: int | None = None
792
+ metadata: dict[str, Any] = Field(default_factory=dict)
793
+
794
+ # Runtime fields (not serialized)
795
+ runner: Any = Field(default=None, exclude=True)
796
+ temp_file: Path | None = Field(default=None, exclude=True)
797
+
798
+ @property
799
+ def duration(self) -> float:
800
+ """Get session duration in seconds."""
801
+ end = self.end_time or time.monotonic_ns()
802
+ return (end - self.start_time) / 1_000_000_000
803
+
804
+ @property
805
+ def is_active(self) -> bool:
806
+ """Check if session is currently active."""
807
+ return self.status in {
808
+ WorkflowStatus.STARTING,
809
+ WorkflowStatus.RUNNING,
810
+ WorkflowStatus.PAUSED,
811
+ WorkflowStatus.STEP_WAITING,
812
+ WorkflowStatus.INPUT_WAITING,
813
+ }
814
+
815
+ @property
816
+ def is_completed(self) -> bool:
817
+ """Check if session has completed (successfully or not)."""
818
+ return self.status in {
819
+ WorkflowStatus.COMPLETED,
820
+ WorkflowStatus.FAILED,
821
+ WorkflowStatus.CANCELLED,
822
+ }
823
+
824
+ def update_status(self, new_status: WorkflowStatus) -> None:
825
+ """Update session status and set end time if completed.
826
+
827
+ Parameters
828
+ ----------
829
+ new_status : WorkflowStatus
830
+ The new status to set.
831
+ """
832
+ self.status = new_status
833
+ if self.is_completed and not self.end_time:
834
+ self.end_time = time.monotonic_ns()
835
+
836
+ def get_execution_summary(self) -> dict[str, Any]:
837
+ """Get a summary of session execution.
838
+
839
+ Returns
840
+ -------
841
+ dict[str, Any]
842
+ A dictionary containing session execution summary.
843
+ """
844
+ return {
845
+ "session_id": self.session_id,
846
+ "client_id": self.client_id,
847
+ "status": self.status.value,
848
+ "execution_mode": self.execution_mode.value,
849
+ "duration_seconds": self.duration,
850
+ "start_time": self.start_time,
851
+ "end_time": self.end_time,
852
+ "is_active": self.is_active,
853
+ "is_completed": self.is_completed,
854
+ }
855
+
856
+
857
+ # noinspection TryExceptPass,PyBroadException
858
+ class WorkflowSession:
859
+ """Enhanced session wrapper with runtime management capabilities."""
860
+
861
+ def __init__(
862
+ self,
863
+ session_state: SessionState,
864
+ runner: Any = None,
865
+ temp_file: Path | None = None,
866
+ ):
867
+ """Initialize workflow session.
868
+
869
+ Parameters
870
+ ----------
871
+ session_state : SessionState
872
+ The session state data
873
+ runner : Any, optional
874
+ The workflow runner instance
875
+ temp_file : Path, optional
876
+ Temporary file path for cleanup
877
+ """
878
+ self._state = session_state
879
+ self.runner = runner
880
+ self._temp_file = temp_file
881
+ self._created_at = time.monotonic_ns()
882
+ self._last_accessed = time.monotonic_ns()
883
+ self._access_count = 0
884
+
885
+ @property
886
+ def start_time(self) -> int:
887
+ """Get the start time of the session."""
888
+ return self._state.start_time
889
+
890
+ @property
891
+ def state(self) -> SessionState:
892
+ """Get the session state."""
893
+ self._last_accessed = time.monotonic_ns()
894
+ self._access_count += 1
895
+ return self._state
896
+
897
+ @property
898
+ def raw_state(self) -> SessionState:
899
+ """Get the raw session state."""
900
+ return self._state
901
+
902
+ @property
903
+ def session_id(self) -> str:
904
+ """Get session ID."""
905
+ return self._state.session_id
906
+
907
+ @property
908
+ def client_id(self) -> str:
909
+ """Get client ID."""
910
+ return self._state.client_id
911
+
912
+ @property
913
+ def status(self) -> WorkflowStatus:
914
+ """Get current status."""
915
+ return self._state.status
916
+
917
+ @property
918
+ def execution_mode(self) -> ExecutionMode:
919
+ """Get execution mode."""
920
+ return self._state.execution_mode
921
+
922
+ @property
923
+ def temp_file(self) -> Path | None:
924
+ """Get temporary file path."""
925
+ return self._temp_file
926
+
927
+ @property
928
+ def metadata(self) -> dict[str, Any]:
929
+ """Get session metadata."""
930
+ return self._state.metadata
931
+
932
+ @property
933
+ def last_accessed(self) -> float:
934
+ """Get last access time."""
935
+ return self._last_accessed
936
+
937
+ @property
938
+ def access_count(self) -> int:
939
+ """Get access count."""
940
+ return self._access_count
941
+
942
+ def update_status(self, new_status: WorkflowStatus) -> None:
943
+ """Update session status.
944
+
945
+ Parameters
946
+ ----------
947
+ new_status : WorkflowStatus
948
+ The new status to set.
949
+ """
950
+ self._state.update_status(new_status)
951
+ self._access_count += 1
952
+ self._last_accessed = time.monotonic_ns()
953
+
954
+ def cleanup(self) -> None:
955
+ """Cleanup session resources."""
956
+ # Stop runner if still running
957
+ if self.runner and hasattr(self.runner, "stop"):
958
+ try:
959
+ self.runner.stop()
960
+ except Exception:
961
+ pass # Best effort cleanup
962
+
963
+ # Remove temporary file
964
+ if self._temp_file and self._temp_file.exists():
965
+ try:
966
+ self._temp_file.unlink()
967
+ except Exception:
968
+ pass # Best effort cleanup
969
+
970
+ def to_dict(self) -> dict[str, Any]:
971
+ """Convert session to dictionary representation.
972
+
973
+ Returns
974
+ -------
975
+ dict[str, Any]
976
+ The dictionary representation of the session.
977
+ """
978
+ base_dict = self._state.get_execution_summary()
979
+ base_dict.update(
980
+ {
981
+ "created_at": self._created_at,
982
+ "last_accessed": self._last_accessed,
983
+ "access_count": self._access_count,
984
+ "has_runner": self.runner is not None,
985
+ "has_temp_file": self._temp_file is not None,
986
+ }
987
+ )
988
+ return base_dict