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