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
waldiez/ws/reloader.py ADDED
@@ -0,0 +1,342 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+ """Auto-reload functionality for development."""
4
+
5
+ import logging
6
+ import os
7
+ import sys
8
+ import threading
9
+ import time
10
+ from pathlib import Path
11
+ from types import TracebackType
12
+ from typing import Any, Callable
13
+
14
+ from watchdog.events import FileSystemEvent, FileSystemEventHandler
15
+ from watchdog.observers import Observer
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class ReloadHandler(FileSystemEventHandler):
21
+ """Handler for file system events that triggers server reload."""
22
+
23
+ _cwd: str = os.getcwd()
24
+
25
+ def __init__(
26
+ self,
27
+ patterns: set[str] | None = None,
28
+ ignore_patterns: set[str] | None = None,
29
+ debounce_delay: float = 0.5,
30
+ restart_callback: Callable[[], None] | None = None,
31
+ ):
32
+ """Initialize the reload handler.
33
+
34
+ Parameters
35
+ ----------
36
+ patterns : set[str] | None
37
+ File patterns to watch (e.g., {'.py', '.json'})
38
+ ignore_patterns : set[str] | None
39
+ File patterns to ignore
40
+ debounce_delay : float
41
+ Delay before triggering restart to debounce rapid changes
42
+ restart_callback : Callable[[], None] | None
43
+ Custom restart callback (defaults to os.execv)
44
+ """
45
+ super().__init__()
46
+ if patterns is None:
47
+ patterns = {".py", ".json", ".yaml", ".yml"}
48
+ self.patterns = patterns
49
+ self.ignore_patterns = ignore_patterns or {
50
+ ".pyc",
51
+ ".pyo",
52
+ ".pyd",
53
+ "__pycache__",
54
+ ".git",
55
+ ".pytest_cache",
56
+ ".mypy_cache",
57
+ ".ruff_cache",
58
+ }
59
+ self.debounce_delay = debounce_delay
60
+ self.restart_callback = restart_callback or self._default_restart
61
+ self.debounce_timer: threading.Timer | None = None
62
+ self.last_restart_time = 0.0
63
+ self.min_restart_interval = 2.0 # Minimum seconds between restarts
64
+
65
+ def should_watch_file(self, file_path: str) -> bool:
66
+ """Check if a file should trigger a reload.
67
+
68
+ Parameters
69
+ ----------
70
+ file_path : str
71
+ Path to the file
72
+
73
+ Returns
74
+ -------
75
+ bool
76
+ True if file should be watched
77
+ """
78
+ if not self.patterns:
79
+ return False
80
+ path = Path(file_path)
81
+
82
+ # Check file extension
83
+ if not any(path.name.endswith(pattern) for pattern in self.patterns):
84
+ return False
85
+
86
+ # Check ignore patterns
87
+ if any(path.name.endswith(ignore) for ignore in self.ignore_patterns):
88
+ return False
89
+
90
+ # Also check if ignore patterns are in path parts (for directories)
91
+ if any(ignore in path.parts for ignore in self.ignore_patterns):
92
+ return False
93
+
94
+ return True
95
+
96
+ @staticmethod
97
+ def get_src_path(event: FileSystemEvent) -> str:
98
+ """Get the source path from the event.
99
+
100
+ Parameters
101
+ ----------
102
+ event : FileSystemEvent
103
+ The file system event
104
+
105
+ Returns
106
+ -------
107
+ str
108
+ The source path as a string
109
+ """
110
+ return (
111
+ event.src_path
112
+ if isinstance(event.src_path, str)
113
+ else str(event.src_path)
114
+ )
115
+
116
+ def on_modified(self, event: FileSystemEvent) -> None:
117
+ """Handle file modification events.
118
+
119
+ Parameters
120
+ ----------
121
+ event : FileSystemEvent
122
+ The file system event
123
+ """
124
+ if event.is_directory:
125
+ return
126
+ src_path = self.get_src_path(event)
127
+ if not self.should_watch_file(src_path):
128
+ return
129
+ logger.info("File changed: %s", src_path)
130
+ self._schedule_restart()
131
+
132
+ def on_created(self, event: FileSystemEvent) -> None:
133
+ """Handle file creation events.
134
+
135
+ Parameters
136
+ ----------
137
+ event : FileSystemEvent
138
+ The file system event
139
+ """
140
+ src_path = self.get_src_path(event)
141
+ if not event.is_directory and self.should_watch_file(src_path):
142
+ logger.info("File created: %s", src_path)
143
+ self._schedule_restart()
144
+
145
+ def on_deleted(self, event: FileSystemEvent) -> None:
146
+ """Handle file deletion events.
147
+
148
+ Parameters
149
+ ----------
150
+ event : FileSystemEvent
151
+ The file system event
152
+ """
153
+ src_path = self.get_src_path(event)
154
+ if not event.is_directory and self.should_watch_file(
155
+ src_path
156
+ ): # pragma: no branch
157
+ logger.info("File deleted: %s", src_path)
158
+ self._schedule_restart()
159
+
160
+ def _schedule_restart(self) -> None:
161
+ """Schedule a restart with debouncing."""
162
+ current_time = time.time()
163
+
164
+ # Prevent too frequent restarts
165
+ if current_time - self.last_restart_time < self.min_restart_interval:
166
+ logger.debug("Restart throttled due to recent restart")
167
+ return
168
+
169
+ # Cancel previous timer
170
+ if self.debounce_timer:
171
+ self.debounce_timer.cancel()
172
+
173
+ # Schedule new restart
174
+ self.debounce_timer = threading.Timer(
175
+ self.debounce_delay, self._trigger_restart
176
+ )
177
+ self.debounce_timer.start()
178
+ logger.debug("Restart scheduled in %ss", self.debounce_delay)
179
+
180
+ def _trigger_restart(self) -> None:
181
+ """Trigger the actual restart."""
182
+ self.last_restart_time = time.time()
183
+ logger.info("Triggering server restart...")
184
+ try:
185
+ self.restart_callback()
186
+ except Exception as e: # pylint: disable=broad-exception-caught
187
+ logger.error("Error during restart: %s", e)
188
+
189
+ @staticmethod
190
+ def _default_restart() -> None:
191
+ """Restart implementation using os.execv."""
192
+ # Save current working directory
193
+ # pylint: disable=too-many-try-statements,broad-exception-caught
194
+ try:
195
+ # Save current working directory
196
+ os.chdir(ReloadHandler._cwd)
197
+
198
+ # Give time for cleanup
199
+ time.sleep(0.1)
200
+
201
+ # Restart the process with same arguments
202
+ python_path = sys.executable
203
+ args = [python_path] + sys.argv
204
+
205
+ logger.info("Restarting with: %s", " ".join(args))
206
+
207
+ # Use os.execv to replace current process
208
+ # noinspection SpawnShellInjection
209
+ os.execv(python_path, args) # nosemgrep # nosec
210
+
211
+ except Exception as e:
212
+ logger.error("Failed to restart: %s", e)
213
+ # Force exit if restart fails
214
+ os._exit(1) # nosec
215
+
216
+
217
+ class FileWatcher:
218
+ """File watcher with auto-reload functionality."""
219
+
220
+ def __init__(
221
+ self,
222
+ watch_dirs: list[Path],
223
+ patterns: set[str] | None = None,
224
+ ignore_patterns: set[str] | None = None,
225
+ debounce_delay: float = 0.5,
226
+ restart_callback: Callable[[], None] | None = None,
227
+ ):
228
+ """Initialize the file watcher.
229
+
230
+ Parameters
231
+ ----------
232
+ watch_dirs : list[Path]
233
+ Directories to watch for changes
234
+ patterns : set[str] | None
235
+ File patterns to watch
236
+ ignore_patterns : set[str] | None
237
+ File patterns to ignore
238
+ debounce_delay : float
239
+ Debounce delay for restart
240
+ restart_callback : Callable[[], None] | None
241
+ Custom restart callback
242
+ """
243
+ self.watch_dirs = watch_dirs
244
+ self.handler = ReloadHandler(
245
+ patterns=patterns,
246
+ ignore_patterns=ignore_patterns,
247
+ debounce_delay=debounce_delay,
248
+ restart_callback=restart_callback,
249
+ )
250
+ self.observer = Observer()
251
+ self._is_watching = False
252
+
253
+ def start(self) -> None:
254
+ """Start watching for file changes."""
255
+ if self._is_watching:
256
+ logger.warning("File watcher is already running")
257
+ return
258
+
259
+ for watch_dir in self.watch_dirs:
260
+ if watch_dir.exists():
261
+ self.observer.schedule(
262
+ self.handler, str(watch_dir), recursive=True
263
+ )
264
+ logger.info("Watching directory: %s", watch_dir)
265
+ else:
266
+ logger.warning("Watch directory does not exist: %s", watch_dir)
267
+
268
+ self.observer.start()
269
+ self._is_watching = True
270
+ logger.info("File watcher started")
271
+
272
+ def stop(self) -> None:
273
+ """Stop watching for file changes."""
274
+ if not self._is_watching:
275
+ return
276
+
277
+ self.observer.stop()
278
+ self.observer.join(timeout=5.0)
279
+ self._is_watching = False
280
+ logger.info("File watcher stopped")
281
+
282
+ def __enter__(self) -> "FileWatcher":
283
+ """Context manager entry.
284
+
285
+ Returns
286
+ -------
287
+ FileWatcher
288
+ The file watcher instance
289
+ """
290
+ self.start()
291
+ return self
292
+
293
+ def __exit__(
294
+ self,
295
+ exc_type: type[BaseException] | None,
296
+ exc_val: BaseException | None,
297
+ exc_tb: TracebackType | None,
298
+ ) -> None:
299
+ """Context manager exit.
300
+
301
+ Parameters
302
+ ----------
303
+ exc_type : type[BaseException] | None
304
+ The type of the exception
305
+ exc_val : BaseException | None
306
+ The exception instance
307
+ exc_tb : TracebackType | None
308
+ The traceback object
309
+ """
310
+ self.stop()
311
+
312
+
313
+ def create_file_watcher(
314
+ root_dir: Path,
315
+ additional_dirs: list[Path] | None = None,
316
+ **kwargs: Any,
317
+ ) -> FileWatcher:
318
+ """Create a file watcher for typical Waldiez development setup.
319
+
320
+ Parameters
321
+ ----------
322
+ root_dir : Path
323
+ Root directory of the project
324
+ additional_dirs : list[Path] | None
325
+ Additional directories to watch
326
+ **kwargs
327
+ Additional arguments for FileWatcher
328
+
329
+ Returns
330
+ -------
331
+ FileWatcher
332
+ Configured file watcher
333
+ """
334
+ watch_dirs = [root_dir / "waldiez"]
335
+
336
+ if additional_dirs:
337
+ watch_dirs.extend(additional_dirs)
338
+
339
+ # Filter to existing directories
340
+ watch_dirs = [d for d in watch_dirs if d.exists()]
341
+
342
+ return FileWatcher(watch_dirs, **kwargs)