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
@@ -0,0 +1,456 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+ # pylint: disable=no-self-use,unused-argument,logging-fstring-interpolation
4
+ # pylint: disable=too-many-try-statements,broad-exception-caught,duplicate-code
5
+ # flake8: noqa: G004
6
+ """Sync subprocess runner for Waldiez workflows."""
7
+
8
+ import logging
9
+ import queue
10
+ import subprocess
11
+ import threading
12
+ import time
13
+ from pathlib import Path
14
+ from typing import Any, Callable, Literal
15
+
16
+ from .__base__ import BaseSubprocessRunner
17
+
18
+
19
+ # noinspection PyUnusedLocal
20
+ class SyncSubprocessRunner(BaseSubprocessRunner):
21
+ """Sync subprocess runner for Waldiez workflows using threading."""
22
+
23
+ def __init__(
24
+ self,
25
+ on_output: Callable[[dict[str, Any]], None],
26
+ on_input_request: Callable[[str], None],
27
+ input_timeout: float = 120.0,
28
+ uploads_root: str | Path | None = None,
29
+ dot_env: str | Path | None = None,
30
+ logger: logging.Logger | None = None,
31
+ **kwargs: Any,
32
+ ) -> None:
33
+ """Initialize sync subprocess runner.
34
+
35
+ Parameters
36
+ ----------
37
+ on_output : Callable[[dict], None]
38
+ Callback for handling output messages
39
+ on_input_request : Callable[[str], None]
40
+ Callback for handling input requests
41
+ input_timeout : float
42
+ Timeout for user input
43
+ uploads_root : str | Path | None
44
+ Root directory for uploads
45
+ dot_env : str | Path | None
46
+ Path to .env file
47
+ logger : logging.Logger | None
48
+ Logger instance
49
+ **kwargs : Any
50
+ Additional arguments passed to base class
51
+ """
52
+ super().__init__(
53
+ input_timeout=input_timeout,
54
+ **kwargs,
55
+ )
56
+ self.waiting_for_input = False
57
+ self.on_output = on_output
58
+ self.on_input_request = on_input_request
59
+ self.process: subprocess.Popen[Any] | None = None
60
+ self.input_queue: queue.Queue[str] = queue.Queue()
61
+ self.output_queue: queue.Queue[dict[str, Any]] = queue.Queue()
62
+ self._stop_event = threading.Event()
63
+ self._monitor_threads: list[threading.Thread] = []
64
+
65
+ def run_subprocess(
66
+ self,
67
+ flow_path: Path,
68
+ mode: Literal["debug", "run"],
69
+ ) -> bool:
70
+ """Run subprocess with the given flow data.
71
+
72
+ Parameters
73
+ ----------
74
+ flow_path : Path
75
+ Path to the waldiez flow file
76
+ mode : Literal["debug", "run"]
77
+ Execution mode ('debug', 'run')
78
+
79
+ Returns
80
+ -------
81
+ bool
82
+ True if subprocess completed successfully, False otherwise
83
+ """
84
+ try:
85
+ # Build command
86
+ cmd = self.build_command(flow_path, mode=mode)
87
+ self.log_subprocess_start(cmd)
88
+
89
+ # Start subprocess
90
+ # pylint: disable=consider-using-with
91
+ self.process = subprocess.Popen(
92
+ cmd,
93
+ stdin=subprocess.PIPE,
94
+ stdout=subprocess.PIPE,
95
+ stderr=subprocess.PIPE,
96
+ text=True,
97
+ bufsize=1, # Line buffered
98
+ )
99
+
100
+ # Start monitoring threads
101
+ self._start_monitoring()
102
+
103
+ # Wait for completion
104
+ exit_code = self.process.wait()
105
+ self.log_subprocess_end(exit_code)
106
+
107
+ # Send completion message
108
+ completion_msg = self.create_completion_message(
109
+ success=exit_code == 0, exit_code=exit_code
110
+ )
111
+ self.on_output(completion_msg)
112
+
113
+ return exit_code == 0
114
+
115
+ except Exception as e:
116
+ self.logger.error(f"Error running subprocess: {e}")
117
+ error_msg = self.create_output_message(
118
+ f"Subprocess error: {str(e)}",
119
+ stream="stderr",
120
+ msg_type="error",
121
+ )
122
+ self.on_output(error_msg)
123
+ return False
124
+
125
+ finally:
126
+ self._cleanup()
127
+
128
+ def provide_user_input(self, user_input: str) -> None:
129
+ """Provide user input response.
130
+
131
+ Parameters
132
+ ----------
133
+ user_input : str
134
+ User's input response
135
+ """
136
+ if self.waiting_for_input:
137
+ try:
138
+ self.input_queue.put(user_input, timeout=1.0)
139
+ self.logger.debug(f"User input queued: {user_input}")
140
+ except queue.Full:
141
+ self.logger.warning("Input queue is full, dropping input")
142
+ except Exception as e: # pragma: no cover
143
+ self.logger.error(f"Error queuing user input: {e}")
144
+
145
+ def stop(self) -> None:
146
+ """Stop the subprocess."""
147
+ self._cleanup()
148
+
149
+ def _start_monitoring(self) -> None:
150
+ """Start monitoring threads for subprocess I/O."""
151
+ if not self.process: # pragma: no cover
152
+ return
153
+
154
+ # Reset stop event
155
+ self._stop_event.clear()
156
+
157
+ # Create and start monitoring threads
158
+ self._monitor_threads = [
159
+ threading.Thread(target=self._read_stdout, daemon=True),
160
+ threading.Thread(target=self._read_stderr, daemon=True),
161
+ threading.Thread(target=self._send_queued_messages, daemon=True),
162
+ ]
163
+
164
+ for thread in self._monitor_threads:
165
+ thread.start()
166
+
167
+ def _read_stdout(self) -> None:
168
+ """Read and handle stdout from subprocess."""
169
+ if not self.process or not self.process.stdout: # pragma: no cover
170
+ return
171
+
172
+ try:
173
+ while not self._stop_event.is_set() and self.process.poll() is None:
174
+ try:
175
+ # Use readline with timeout simulation
176
+ if self.process.stdout.readable(): # pragma: no branch
177
+ line = self.process.stdout.readline()
178
+ if not line: # pragma: no cover
179
+ time.sleep(0.1)
180
+ continue
181
+
182
+ line_str = (
183
+ line.strip()
184
+ if isinstance(line, str)
185
+ else str(line).strip()
186
+ )
187
+ if line_str: # pragma: no branch
188
+ self._handle_stdout_line(line_str)
189
+
190
+ except Exception as e:
191
+ self.logger.error(f"Error reading stdout: {e}")
192
+ break
193
+
194
+ time.sleep(0.01) # Small delay to prevent busy waiting
195
+
196
+ except Exception as e:
197
+ self.logger.error(f"Error in stdout reader: {e}")
198
+
199
+ # pylint: disable=too-complex,too-many-nested-blocks
200
+ def _read_stderr(self) -> None: # noqa: C901
201
+ """Read and handle stderr from subprocess."""
202
+ if not self.process or not self.process.stderr: # pragma: no cover
203
+ return
204
+
205
+ try:
206
+ while not self._stop_event.is_set() and self.process.poll() is None:
207
+ try:
208
+ # Use readline with timeout simulation
209
+ if self.process.stderr.readable(): # pragma: no branch
210
+ line = self.process.stderr.readline()
211
+ self.logger.debug("Stderr line: %s", line)
212
+ if not line:
213
+ time.sleep(0.1)
214
+ continue
215
+
216
+ line_str = (
217
+ line.strip()
218
+ if isinstance(line, str)
219
+ else str(line).strip()
220
+ )
221
+ if line_str:
222
+ error_msg = self.create_output_message(
223
+ line_str,
224
+ stream="stderr",
225
+ msg_type="error",
226
+ )
227
+ try:
228
+ self.output_queue.put(error_msg, timeout=1.0)
229
+ except queue.Full:
230
+ self.logger.warning(
231
+ "Output queue full, dropping stderr message"
232
+ )
233
+
234
+ except Exception as e:
235
+ self.logger.error(f"Error reading stderr: {e}")
236
+ break
237
+
238
+ time.sleep(0.01) # Small delay to prevent busy waiting
239
+
240
+ except Exception as e:
241
+ self.logger.error(f"Error in stderr reader: {e}")
242
+
243
+ def _send_queued_messages(self) -> None:
244
+ """Send queued messages to output callback."""
245
+ try:
246
+ while not self._stop_event.is_set():
247
+ try:
248
+ message = self.output_queue.get(timeout=1.0)
249
+
250
+ try:
251
+ self.on_output(message)
252
+ except Exception as e:
253
+ self.logger.error(f"Error in output callback: {e}")
254
+
255
+ self.output_queue.task_done()
256
+
257
+ except queue.Empty:
258
+ continue
259
+ except Exception as e:
260
+ self.logger.error(f"Error sending queued message: {e}")
261
+ break
262
+
263
+ except Exception as e:
264
+ self.logger.error(f"Error in message sender: {e}")
265
+
266
+ def _handle_stdout_line(self, line: str) -> None:
267
+ """Handle a line from stdout.
268
+
269
+ Parameters
270
+ ----------
271
+ line : str
272
+ Decoded line from stdout
273
+ """
274
+ self.logger.debug(f"Stdout line: {line}")
275
+ parsed_data = self.parse_output(line, stream="stdout")
276
+ if not parsed_data:
277
+ self.logger.debug("Non-structured output, forwarding as is")
278
+ self.output_queue.put({"type": "print", "data": line}, timeout=1.0)
279
+ return
280
+ if parsed_data.get("type") in ("input_request", "debug_input_request"):
281
+ prompt = parsed_data.get("prompt", "> ")
282
+ self.waiting_for_input = True
283
+ self.on_input_request(prompt)
284
+ self._handle_input_request(parsed_data)
285
+ else:
286
+ # Forward message to output
287
+ try:
288
+ self.output_queue.put(parsed_data, timeout=1.0)
289
+ except queue.Full:
290
+ self.logger.warning(
291
+ "Output queue full, dropping structured message"
292
+ )
293
+
294
+ def _handle_input_request(self, data: dict[str, Any]) -> None:
295
+ """Handle input request from subprocess.
296
+
297
+ Parameters
298
+ ----------
299
+ data : dict[str, Any]
300
+ Input request data
301
+ """
302
+ response_type = str(data.get("type", "input_request")).replace(
303
+ "request", "response"
304
+ )
305
+ request_id = data.get("request_id")
306
+ try:
307
+ # Wait for user response
308
+ user_input = self.input_queue.get(timeout=self.input_timeout)
309
+
310
+ # Create response
311
+ response = self.create_input_response(
312
+ response_type=response_type,
313
+ user_input=user_input,
314
+ request_id=request_id,
315
+ )
316
+ # Send response to subprocess
317
+ if self.process and self.process.stdin:
318
+ self.process.stdin.write(response)
319
+ self.process.stdin.flush()
320
+
321
+ self.logger.debug(f"Sent {response_type}: {user_input}")
322
+
323
+ except queue.Empty:
324
+ self.logger.warning("Input request timed out")
325
+ # Send empty response on timeout
326
+ if self.process and self.process.stdin:
327
+ response = self.create_input_response(
328
+ response_type=response_type,
329
+ user_input="",
330
+ request_id=request_id,
331
+ )
332
+ self.process.stdin.write(response)
333
+ self.process.stdin.flush()
334
+
335
+ except Exception as e:
336
+ self.logger.error(f"Error handling input request: {e}")
337
+
338
+ finally:
339
+ self.waiting_for_input = False
340
+
341
+ def _cleanup_threads(self) -> None:
342
+ """Cleanup threads."""
343
+ self._stop_event.set()
344
+
345
+ # Wait for threads to finish
346
+ for thread in self._monitor_threads:
347
+ if thread.is_alive():
348
+ thread.join(timeout=2.0)
349
+ if thread.is_alive():
350
+ self.logger.warning(
351
+ f"Thread {thread.name} did not stop gracefully"
352
+ )
353
+
354
+ # noinspection TryExceptPass,PyBroadException
355
+ def _cleanup_process(self) -> None:
356
+ """Cleanup process resources."""
357
+ if self.process:
358
+ if self.process.stdin:
359
+ try:
360
+ self.process.stdin.close()
361
+ except Exception:
362
+ pass
363
+
364
+ if self.process.stdout:
365
+ try:
366
+ self.process.stdout.close()
367
+ except Exception:
368
+ pass
369
+
370
+ if self.process.stderr:
371
+ try:
372
+ self.process.stderr.close()
373
+ except Exception:
374
+ pass
375
+ self.logger.debug("Terminating subprocess")
376
+ try:
377
+ self.process.terminate()
378
+ self.process.wait(timeout=2.0)
379
+ except (TimeoutError, subprocess.TimeoutExpired):
380
+ self.logger.warning("Force killing subprocess")
381
+ self.process.kill()
382
+ self.process.wait()
383
+ except BaseException as e:
384
+ self.logger.error(f"Error stopping subprocess: {e}")
385
+ finally:
386
+ self.process = None
387
+
388
+ def _cleanup_queues(self) -> None:
389
+ """Cleanup input and output queues."""
390
+ try:
391
+ while not self.input_queue.empty():
392
+ self.input_queue.get_nowait()
393
+ except queue.Empty:
394
+ pass
395
+
396
+ try:
397
+ while not self.output_queue.empty():
398
+ self.output_queue.get_nowait()
399
+ except queue.Empty:
400
+ pass
401
+
402
+ def _cleanup(self) -> None:
403
+ """Cleanup resources."""
404
+ self._stop_event.set()
405
+ self._cleanup_process()
406
+ self._cleanup_threads()
407
+ self._monitor_threads.clear()
408
+ self.waiting_for_input = False
409
+ self._cleanup_queues()
410
+
411
+ def is_running(self) -> bool:
412
+ """Check if subprocess is currently running.
413
+
414
+ Returns
415
+ -------
416
+ bool
417
+ True if subprocess is running, False otherwise
418
+ """
419
+ return self.process is not None and self.process.poll() is None
420
+
421
+ def get_exit_code(self) -> int | None:
422
+ """Get the exit code of the subprocess.
423
+
424
+ Returns
425
+ -------
426
+ int | None
427
+ Exit code if process has finished, None if still running
428
+ """
429
+ if self.process is None:
430
+ return None
431
+ return self.process.poll()
432
+
433
+ def wait_for_completion(self, timeout: float | None = None) -> int:
434
+ """Wait for subprocess to complete.
435
+
436
+ Parameters
437
+ ----------
438
+ timeout : float | None
439
+ Maximum time to wait in seconds. None means wait indefinitely.
440
+
441
+ Returns
442
+ -------
443
+ int
444
+ Exit code of the subprocess
445
+
446
+ Raises
447
+ ------
448
+ subprocess.TimeoutExpired
449
+ If timeout is reached before subprocess completes
450
+ RuntimeError
451
+ If no subprocess is running
452
+ """
453
+ if not self.process:
454
+ raise RuntimeError("No subprocess is running")
455
+
456
+ return self.process.wait(timeout=timeout)