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,455 @@
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
+
179
+ if not line: # pragma: no cover
180
+ time.sleep(0.1)
181
+ continue
182
+
183
+ line_str = (
184
+ line.strip()
185
+ if isinstance(line, str)
186
+ else str(line).strip()
187
+ )
188
+ if line_str: # pragma: no branch
189
+ self._handle_stdout_line(line_str)
190
+
191
+ except Exception as e:
192
+ self.logger.error(f"Error reading stdout: {e}")
193
+ break
194
+
195
+ time.sleep(0.01) # Small delay to prevent busy waiting
196
+
197
+ except Exception as e:
198
+ self.logger.error(f"Error in stdout reader: {e}")
199
+
200
+ # pylint: disable=too-complex,too-many-nested-blocks
201
+ def _read_stderr(self) -> None: # noqa: C901
202
+ """Read and handle stderr from subprocess."""
203
+ if not self.process or not self.process.stderr: # pragma: no cover
204
+ return
205
+
206
+ try:
207
+ while not self._stop_event.is_set() and self.process.poll() is None:
208
+ try:
209
+ # Use readline with timeout simulation
210
+ if self.process.stderr.readable(): # pragma: no branch
211
+ line = self.process.stderr.readline()
212
+
213
+ if not line:
214
+ time.sleep(0.1)
215
+ continue
216
+
217
+ line_str = (
218
+ line.strip()
219
+ if isinstance(line, str)
220
+ else str(line).strip()
221
+ )
222
+ if line_str:
223
+ error_msg = self.create_output_message(
224
+ line_str,
225
+ stream="stderr",
226
+ msg_type="error",
227
+ )
228
+ try:
229
+ self.output_queue.put(error_msg, timeout=1.0)
230
+ except queue.Full:
231
+ self.logger.warning(
232
+ "Output queue full, dropping stderr message"
233
+ )
234
+
235
+ except Exception as e:
236
+ self.logger.error(f"Error reading stderr: {e}")
237
+ break
238
+
239
+ time.sleep(0.01) # Small delay to prevent busy waiting
240
+
241
+ except Exception as e:
242
+ self.logger.error(f"Error in stderr reader: {e}")
243
+
244
+ def _send_queued_messages(self) -> None:
245
+ """Send queued messages to output callback."""
246
+ try:
247
+ while not self._stop_event.is_set():
248
+ try:
249
+ message = self.output_queue.get(timeout=1.0)
250
+
251
+ try:
252
+ self.on_output(message)
253
+ except Exception as e:
254
+ self.logger.error(f"Error in output callback: {e}")
255
+
256
+ self.output_queue.task_done()
257
+
258
+ except queue.Empty:
259
+ continue
260
+ except Exception as e:
261
+ self.logger.error(f"Error sending queued message: {e}")
262
+ break
263
+
264
+ except Exception as e:
265
+ self.logger.error(f"Error in message sender: {e}")
266
+
267
+ def _handle_stdout_line(self, line: str) -> None:
268
+ """Handle a line from stdout.
269
+
270
+ Parameters
271
+ ----------
272
+ line : str
273
+ Decoded line from stdout
274
+ """
275
+ # Try to parse as structured JSON first
276
+ parsed_data = self.parse_output(line, stream="stdout")
277
+ if not parsed_data:
278
+ return
279
+ if parsed_data.get("type") in ("input_request", "debug_input_request"):
280
+ prompt = parsed_data.get("prompt", "> ")
281
+ self.waiting_for_input = True
282
+ self.on_input_request(prompt)
283
+ self._handle_input_request(parsed_data)
284
+ else:
285
+ # Forward message to output
286
+ try:
287
+ self.output_queue.put(parsed_data, timeout=1.0)
288
+ except queue.Full:
289
+ self.logger.warning(
290
+ "Output queue full, dropping structured message"
291
+ )
292
+
293
+ def _handle_input_request(self, data: dict[str, Any]) -> None:
294
+ """Handle input request from subprocess.
295
+
296
+ Parameters
297
+ ----------
298
+ data : dict[str, Any]
299
+ Input request data
300
+ """
301
+ response_type = str(data.get("type", "input_request")).replace(
302
+ "request", "response"
303
+ )
304
+ request_id = data.get("request_id")
305
+ try:
306
+ # Wait for user response
307
+ user_input = self.input_queue.get(timeout=self.input_timeout)
308
+
309
+ # Create response
310
+ response = self.create_input_response(
311
+ response_type=response_type,
312
+ user_input=user_input,
313
+ request_id=request_id,
314
+ )
315
+ # Send response to subprocess
316
+ if self.process and self.process.stdin:
317
+ self.process.stdin.write(response)
318
+ self.process.stdin.flush()
319
+
320
+ self.logger.debug(f"Sent input response: {user_input}")
321
+
322
+ except queue.Empty:
323
+ self.logger.warning("Input request timed out")
324
+ # Send empty response on timeout
325
+ if self.process and self.process.stdin:
326
+ response = self.create_input_response(
327
+ response_type=response_type,
328
+ user_input="",
329
+ request_id=request_id,
330
+ )
331
+ self.process.stdin.write(response)
332
+ self.process.stdin.flush()
333
+
334
+ except Exception as e:
335
+ self.logger.error(f"Error handling input request: {e}")
336
+
337
+ finally:
338
+ self.waiting_for_input = False
339
+
340
+ def _cleanup_threads(self) -> None:
341
+ """Cleanup threads."""
342
+ self._stop_event.set()
343
+
344
+ # Wait for threads to finish
345
+ for thread in self._monitor_threads:
346
+ if thread.is_alive():
347
+ thread.join(timeout=2.0)
348
+ if thread.is_alive():
349
+ self.logger.warning(
350
+ f"Thread {thread.name} did not stop gracefully"
351
+ )
352
+
353
+ # noinspection TryExceptPass,PyBroadException
354
+ def _cleanup_process(self) -> None:
355
+ """Cleanup process resources."""
356
+ if self.process:
357
+ if self.process.stdin:
358
+ try:
359
+ self.process.stdin.close()
360
+ except Exception:
361
+ pass
362
+
363
+ if self.process.stdout:
364
+ try:
365
+ self.process.stdout.close()
366
+ except Exception:
367
+ pass
368
+
369
+ if self.process.stderr:
370
+ try:
371
+ self.process.stderr.close()
372
+ except Exception:
373
+ pass
374
+ self.logger.debug("Terminating subprocess")
375
+ try:
376
+ self.process.terminate()
377
+ self.process.wait(timeout=2.0)
378
+ except (TimeoutError, subprocess.TimeoutExpired):
379
+ self.logger.warning("Force killing subprocess")
380
+ self.process.kill()
381
+ self.process.wait()
382
+ except BaseException as e:
383
+ self.logger.error(f"Error stopping subprocess: {e}")
384
+ finally:
385
+ self.process = None
386
+
387
+ def _cleanup_queues(self) -> None:
388
+ """Cleanup input and output queues."""
389
+ try:
390
+ while not self.input_queue.empty():
391
+ self.input_queue.get_nowait()
392
+ except queue.Empty:
393
+ pass
394
+
395
+ try:
396
+ while not self.output_queue.empty():
397
+ self.output_queue.get_nowait()
398
+ except queue.Empty:
399
+ pass
400
+
401
+ def _cleanup(self) -> None:
402
+ """Cleanup resources."""
403
+ self._stop_event.set()
404
+ self._cleanup_process()
405
+ self._cleanup_threads()
406
+ self._monitor_threads.clear()
407
+ self.waiting_for_input = False
408
+ self._cleanup_queues()
409
+
410
+ def is_running(self) -> bool:
411
+ """Check if subprocess is currently running.
412
+
413
+ Returns
414
+ -------
415
+ bool
416
+ True if subprocess is running, False otherwise
417
+ """
418
+ return self.process is not None and self.process.poll() is None
419
+
420
+ def get_exit_code(self) -> int | None:
421
+ """Get the exit code of the subprocess.
422
+
423
+ Returns
424
+ -------
425
+ int | None
426
+ Exit code if process has finished, None if still running
427
+ """
428
+ if self.process is None:
429
+ return None
430
+ return self.process.poll()
431
+
432
+ def wait_for_completion(self, timeout: float | None = None) -> int:
433
+ """Wait for subprocess to complete.
434
+
435
+ Parameters
436
+ ----------
437
+ timeout : float | None
438
+ Maximum time to wait in seconds. None means wait indefinitely.
439
+
440
+ Returns
441
+ -------
442
+ int
443
+ Exit code of the subprocess
444
+
445
+ Raises
446
+ ------
447
+ subprocess.TimeoutExpired
448
+ If timeout is reached before subprocess completes
449
+ RuntimeError
450
+ If no subprocess is running
451
+ """
452
+ if not self.process:
453
+ raise RuntimeError("No subprocess is running")
454
+
455
+ return self.process.wait(timeout=timeout)