waldiez 0.5.2__py3-none-any.whl → 0.5.4__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 (79) hide show
  1. waldiez/_version.py +1 -1
  2. waldiez/cli.py +5 -27
  3. waldiez/exporter.py +0 -13
  4. waldiez/exporting/agent/exporter.py +38 -0
  5. waldiez/exporting/agent/extras/__init__.py +2 -0
  6. waldiez/exporting/agent/extras/doc_agent_extras.py +366 -0
  7. waldiez/exporting/agent/extras/group_member_extras.py +3 -2
  8. waldiez/exporting/agent/processor.py +113 -15
  9. waldiez/exporting/chats/processor.py +2 -21
  10. waldiez/exporting/chats/utils/common.py +66 -1
  11. waldiez/exporting/chats/utils/group.py +6 -3
  12. waldiez/exporting/chats/utils/nested.py +1 -1
  13. waldiez/exporting/chats/utils/sequential.py +25 -9
  14. waldiez/exporting/chats/utils/single.py +8 -6
  15. waldiez/exporting/core/context.py +0 -12
  16. waldiez/exporting/core/extras/agent_extras/standard_extras.py +3 -1
  17. waldiez/exporting/core/extras/base.py +20 -17
  18. waldiez/exporting/core/extras/path_resolver.py +39 -41
  19. waldiez/exporting/core/extras/serializer.py +16 -1
  20. waldiez/exporting/core/protocols.py +17 -0
  21. waldiez/exporting/core/types.py +6 -9
  22. waldiez/exporting/flow/execution_generator.py +56 -21
  23. waldiez/exporting/flow/exporter.py +1 -4
  24. waldiez/exporting/flow/factory.py +0 -9
  25. waldiez/exporting/flow/file_generator.py +6 -0
  26. waldiez/exporting/flow/orchestrator.py +27 -21
  27. waldiez/exporting/flow/utils/__init__.py +0 -2
  28. waldiez/exporting/flow/utils/common.py +15 -96
  29. waldiez/exporting/flow/utils/importing.py +4 -0
  30. waldiez/io/mqtt.py +33 -14
  31. waldiez/io/redis.py +18 -13
  32. waldiez/io/structured.py +9 -4
  33. waldiez/io/utils.py +32 -0
  34. waldiez/io/ws.py +8 -2
  35. waldiez/models/__init__.py +6 -0
  36. waldiez/models/agents/__init__.py +8 -0
  37. waldiez/models/agents/agent/agent.py +136 -38
  38. waldiez/models/agents/agent/agent_type.py +3 -2
  39. waldiez/models/agents/agents.py +10 -0
  40. waldiez/models/agents/doc_agent/__init__.py +13 -0
  41. waldiez/models/agents/doc_agent/doc_agent.py +126 -0
  42. waldiez/models/agents/doc_agent/doc_agent_data.py +149 -0
  43. waldiez/models/agents/doc_agent/rag_query_engine.py +127 -0
  44. waldiez/models/chat/chat_message.py +1 -1
  45. waldiez/models/flow/flow.py +13 -2
  46. waldiez/models/model/__init__.py +2 -2
  47. waldiez/models/model/_aws.py +75 -0
  48. waldiez/models/model/_llm.py +516 -0
  49. waldiez/models/model/_price.py +30 -0
  50. waldiez/models/model/model.py +45 -2
  51. waldiez/models/model/model_data.py +2 -83
  52. waldiez/models/tool/predefined/_duckduckgo.py +123 -0
  53. waldiez/models/tool/predefined/_google.py +31 -9
  54. waldiez/models/tool/predefined/_perplexity.py +161 -0
  55. waldiez/models/tool/predefined/_searxng.py +152 -0
  56. waldiez/models/tool/predefined/_tavily.py +46 -9
  57. waldiez/models/tool/predefined/_wikipedia.py +26 -6
  58. waldiez/models/tool/predefined/_youtube.py +36 -8
  59. waldiez/models/tool/predefined/registry.py +6 -0
  60. waldiez/models/waldiez.py +12 -0
  61. waldiez/runner.py +184 -382
  62. waldiez/running/__init__.py +2 -4
  63. waldiez/running/base_runner.py +136 -118
  64. waldiez/running/environment.py +61 -17
  65. waldiez/running/post_run.py +70 -14
  66. waldiez/running/pre_run.py +42 -0
  67. waldiez/running/protocol.py +42 -48
  68. waldiez/running/run_results.py +5 -5
  69. waldiez/running/standard_runner.py +429 -0
  70. waldiez/running/timeline_processor.py +1166 -0
  71. waldiez/utils/version.py +12 -1
  72. {waldiez-0.5.2.dist-info → waldiez-0.5.4.dist-info}/METADATA +61 -63
  73. {waldiez-0.5.2.dist-info → waldiez-0.5.4.dist-info}/RECORD +77 -66
  74. waldiez/running/import_runner.py +0 -424
  75. waldiez/running/subprocess_runner.py +0 -100
  76. {waldiez-0.5.2.dist-info → waldiez-0.5.4.dist-info}/WHEEL +0 -0
  77. {waldiez-0.5.2.dist-info → waldiez-0.5.4.dist-info}/entry_points.txt +0 -0
  78. {waldiez-0.5.2.dist-info → waldiez-0.5.4.dist-info}/licenses/LICENSE +0 -0
  79. {waldiez-0.5.2.dist-info → waldiez-0.5.4.dist-info}/licenses/NOTICE.md +0 -0
@@ -0,0 +1,429 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+
4
+ # flake8: noqa: C901, E501
5
+ # pyright: reportUnknownMemberType=false, reportUnknownVariableType=false
6
+ # pyright: reportAttributeAccessIssue=false,reportUnknownArgumentType=false
7
+ # pylint: disable=too-many-try-statements,import-outside-toplevel,line-too-long,
8
+ # pylint: disable=too-complex,unused-argument,duplicate-code,broad-exception-caught
9
+ """Run a waldiez flow.
10
+
11
+ The flow is first converted to an autogen flow with agents, chats, and tools.
12
+ We then chown to temporary directory, call the flow's `main()` and
13
+ return the results. Before running the flow, any additional environment
14
+ variables specified in the waldiez file are set.
15
+ """
16
+
17
+ import asyncio
18
+ import importlib.util
19
+ import sys
20
+ import threading
21
+ import traceback
22
+ from pathlib import Path
23
+ from types import ModuleType
24
+ from typing import TYPE_CHECKING, Any, Callable, Coroutine, Union
25
+
26
+ from waldiez.models.waldiez import Waldiez
27
+ from waldiez.running.run_results import WaldiezRunResults
28
+
29
+ from .base_runner import WaldiezBaseRunner
30
+ from .utils import chdir
31
+
32
+ if TYPE_CHECKING:
33
+ from autogen.events import BaseEvent # type: ignore
34
+ from autogen.io.run_response import ( # type: ignore
35
+ AsyncRunResponseProtocol,
36
+ RunResponseProtocol,
37
+ )
38
+
39
+
40
+ class WaldiezStandardRunner(WaldiezBaseRunner):
41
+ """Run a waldiez flow in a standard way."""
42
+
43
+ def __init__(
44
+ self,
45
+ waldiez: Waldiez,
46
+ output_path: str | Path | None = None,
47
+ uploads_root: str | Path | None = None,
48
+ structured_io: bool = False,
49
+ ) -> None:
50
+ """Initialize the Waldiez manager."""
51
+ super().__init__(
52
+ waldiez,
53
+ output_path=output_path,
54
+ uploads_root=uploads_root,
55
+ structured_io=structured_io,
56
+ )
57
+ self._execution_thread: threading.Thread | None = None
58
+ self._loaded_module: ModuleType | None = None
59
+ self._event_count = 0
60
+ self._processed_events = 0
61
+
62
+ def _load_module(self, output_file: Path, temp_dir: Path) -> ModuleType:
63
+ """Load the module from the waldiez file."""
64
+ file_name = output_file.name
65
+ module_name = file_name.replace(".py", "")
66
+ spec = importlib.util.spec_from_file_location(
67
+ module_name, temp_dir / file_name
68
+ )
69
+ if not spec or not spec.loader:
70
+ raise ImportError("Could not import the flow")
71
+ module = importlib.util.module_from_spec(spec)
72
+ spec.loader.exec_module(module)
73
+ if not hasattr(module, "main"):
74
+ raise ImportError(
75
+ "The waldiez file does not contain a main() function"
76
+ )
77
+ self._loaded_module = module
78
+ return module
79
+
80
+ def _run(
81
+ self,
82
+ temp_dir: Path,
83
+ output_file: Path,
84
+ uploads_root: Path | None,
85
+ skip_mmd: bool,
86
+ skip_timeline: bool,
87
+ ) -> Union[list["RunResponseProtocol"], list["AsyncRunResponseProtocol"]]:
88
+ """Run the Waldiez workflow."""
89
+ from autogen.io import IOStream # type: ignore
90
+
91
+ from waldiez.io import StructuredIOStream
92
+
93
+ self._print: Callable[..., None] = print
94
+ self._input: (
95
+ Callable[..., str] | Callable[..., Coroutine[Any, Any, str]]
96
+ ) = input
97
+ results_container: WaldiezRunResults = {
98
+ "results": [],
99
+ "exception": None,
100
+ "completed": False,
101
+ }
102
+ try:
103
+ self._loaded_module = self._load_module(output_file, temp_dir)
104
+ if self._stop_requested.is_set():
105
+ self.log.info(
106
+ "Async execution stopped before AG2 workflow start"
107
+ )
108
+ return []
109
+ if self.structured_io:
110
+ stream = StructuredIOStream(
111
+ uploads_root=uploads_root, is_async=False
112
+ )
113
+ else:
114
+ stream = IOStream.get_default()
115
+ self._print = stream.print
116
+ self._input = stream.input
117
+ self._send = stream.send
118
+ self._print("<Waldiez> - Starting workflow...")
119
+ self._print(self.waldiez.info.model_dump_json())
120
+ results = self._loaded_module.main(
121
+ on_event=self._on_event,
122
+ )
123
+ results_container["results"] = results
124
+ self._print("<Waldiez> - Workflow finished")
125
+ except SystemExit:
126
+ self._print("<Waldiez> - Workflow stopped by user")
127
+ except Exception as e: # pylint: disable=broad-exception-caught
128
+ results_container["exception"] = e
129
+ traceback.print_exc()
130
+ self._print(f"<Waldiez> - Workflow execution failed: {e}")
131
+ finally:
132
+ results_container["completed"] = True
133
+ return results_container["results"]
134
+
135
+ def _on_event(
136
+ self,
137
+ event: "BaseEvent",
138
+ ) -> bool:
139
+ """Process an event from the workflow."""
140
+ self._event_count += 1
141
+ if self._stop_requested.is_set():
142
+ self.log.info(
143
+ "Async execution stopped before AG2 workflow event processing"
144
+ )
145
+ return False
146
+ try:
147
+ if hasattr(event, "type"):
148
+ if event.type == "input_request":
149
+ prompt = getattr(
150
+ event,
151
+ "prompt",
152
+ getattr(event.content, "prompt", "> "),
153
+ )
154
+ password = getattr(
155
+ event,
156
+ "password",
157
+ getattr(event.content, "password", False),
158
+ )
159
+ user_input = self._input(
160
+ prompt,
161
+ password=password,
162
+ )
163
+ event.content.respond(user_input)
164
+ else:
165
+ self._send(event)
166
+ self._processed_events += 1
167
+ except Exception as e:
168
+ raise RuntimeError(
169
+ f"Error processing event {event}: {e}\n{traceback.format_exc()}"
170
+ ) from e
171
+ if event.type == "run_completion":
172
+ self._signal_completion()
173
+ WaldiezBaseRunner._running = False
174
+ return not self._stop_requested.is_set()
175
+
176
+ def _start(
177
+ self,
178
+ temp_dir: Path,
179
+ output_file: Path,
180
+ uploads_root: Path | None,
181
+ skip_mmd: bool,
182
+ skip_timeline: bool,
183
+ ) -> None:
184
+ """Start the workflow in a non-blocking way."""
185
+ if self._execution_thread and self._execution_thread.is_alive():
186
+ raise RuntimeError("Non-blocking execution already in progress")
187
+
188
+ # Reset completion state
189
+ self._reset_completion_state()
190
+
191
+ # Create thread with proper integration
192
+ self._execution_thread = threading.Thread(
193
+ target=self._threaded_run,
194
+ args=(temp_dir, output_file, uploads_root, skip_mmd, skip_timeline),
195
+ name=f"WaldiezStandardRunner-{self.waldiez.name}",
196
+ daemon=False, # Not daemon so we can properly join
197
+ )
198
+ self._execution_thread.start()
199
+
200
+ def _threaded_run(
201
+ self,
202
+ temp_dir: Path,
203
+ output_file: Path,
204
+ uploads_root: Path | None,
205
+ skip_mmd: bool = False,
206
+ skip_timeline: bool = False,
207
+ ) -> None:
208
+ """Run in a separate thread with proper lifecycle."""
209
+ try:
210
+ # Change to temp directory and manage sys.path
211
+ with chdir(to=temp_dir):
212
+ sys.path.insert(0, str(temp_dir))
213
+ try:
214
+ results = self._run(
215
+ temp_dir=temp_dir,
216
+ output_file=output_file,
217
+ uploads_root=uploads_root,
218
+ skip_mmd=skip_mmd,
219
+ skip_timeline=skip_timeline,
220
+ )
221
+
222
+ # Store results
223
+ self._last_results = results
224
+
225
+ # Call after_run hooks
226
+ self.after_run(
227
+ results=results,
228
+ output_file=output_file,
229
+ uploads_root=uploads_root,
230
+ temp_dir=temp_dir,
231
+ skip_mmd=skip_mmd,
232
+ skip_timeline=skip_timeline,
233
+ )
234
+
235
+ finally:
236
+ # Clean up sys.path
237
+ if sys.path and sys.path[0] == str(temp_dir):
238
+ sys.path.pop(0)
239
+
240
+ except Exception as e:
241
+ self._last_exception = e
242
+ self.log.error("Threaded execution failed: %s", e)
243
+
244
+ finally:
245
+ # Signal completion and mark as not running
246
+ self._signal_completion()
247
+ WaldiezBaseRunner._running = False
248
+
249
+ async def _a_on_event(
250
+ self,
251
+ event: "BaseEvent",
252
+ ) -> bool:
253
+ """Process an event from the workflow asynchronously."""
254
+ self._event_count += 1
255
+ if self._stop_requested.is_set():
256
+ self.log.info(
257
+ "Async execution stopped before AG2 workflow event processing"
258
+ )
259
+ return False
260
+ try:
261
+ if hasattr(event, "type"):
262
+ if event.type == "input_request":
263
+ prompt = getattr(
264
+ event,
265
+ "prompt",
266
+ getattr(event.content, "prompt", "> "),
267
+ )
268
+ password = getattr(
269
+ event,
270
+ "password",
271
+ getattr(event.content, "password", False),
272
+ )
273
+ user_input = self._input(
274
+ prompt,
275
+ password=password,
276
+ )
277
+ if asyncio.iscoroutine(user_input):
278
+ user_input = await user_input
279
+ await event.content.respond(user_input)
280
+ else:
281
+ self._send(event)
282
+ self._processed_events += 1
283
+ except Exception as e:
284
+ raise RuntimeError(
285
+ f"Error processing event {event}: {e}\n{traceback.format_exc()}"
286
+ ) from e
287
+ return not self._stop_requested.is_set()
288
+
289
+ async def _a_run(
290
+ self,
291
+ temp_dir: Path,
292
+ output_file: Path,
293
+ uploads_root: Path | None,
294
+ skip_mmd: bool = False,
295
+ skip_timeline: bool = False,
296
+ ) -> Union[list["RunResponseProtocol"], list["AsyncRunResponseProtocol"]]:
297
+ """Run the Waldiez workflow asynchronously."""
298
+
299
+ # fmt: off
300
+ async def _execute_workflow() -> Union[list["RunResponseProtocol"], list["AsyncRunResponseProtocol"]]:
301
+ # fmt: on
302
+ """Execute the workflow in an async context."""
303
+ from autogen.io import IOStream # pyright: ignore
304
+
305
+ from waldiez.io import StructuredIOStream
306
+
307
+ results: Union[list["AsyncRunResponseProtocol"], list["RunResponseProtocol"]] = []
308
+ try:
309
+ self._loaded_module = self._load_module(output_file, temp_dir)
310
+ if self._stop_requested.is_set():
311
+ self.log.info(
312
+ "Async execution stopped before AG2 workflow start"
313
+ )
314
+ return []
315
+ if self.structured_io:
316
+ stream = StructuredIOStream(
317
+ uploads_root=uploads_root, is_async=True
318
+ )
319
+ else:
320
+ stream = IOStream.get_default()
321
+ self._print = stream.print
322
+ self._input = stream.input
323
+ self._send = stream.send
324
+ self._print("<Waldiez> - Starting workflow...")
325
+ self._print(self.waldiez.info.model_dump_json())
326
+ results = await self._loaded_module.main(
327
+ on_event=self._a_on_event
328
+ )
329
+ except SystemExit:
330
+ self._print("<Waldiez> - Workflow stopped by user")
331
+ return []
332
+ except Exception as e:
333
+ self._print(
334
+ f"<Waldiez> - Error loading workflow: {e}\n{traceback.format_exc()}"
335
+ )
336
+ raise RuntimeError(
337
+ f"Error loading workflow: {e}\n{traceback.format_exc()}"
338
+ ) from e
339
+ return results
340
+
341
+ # Create cancellable task
342
+ task = asyncio.create_task(_execute_workflow())
343
+
344
+ # Monitor for stop requests
345
+ try:
346
+ while not task.done():
347
+ if self._stop_requested.is_set():
348
+ task.cancel()
349
+ self.log.info("Async execution stopped by user")
350
+ break
351
+ await asyncio.sleep(0.1)
352
+ # Return the task result when completed
353
+ return await task
354
+
355
+ except asyncio.CancelledError:
356
+ self.log.info("Async execution cancelled")
357
+ return []
358
+
359
+ async def _a_start(
360
+ self,
361
+ temp_dir: Path,
362
+ output_file: Path,
363
+ uploads_root: Path | None,
364
+ skip_mmd: bool = False,
365
+ skip_timeline: bool = False,
366
+ ) -> None:
367
+ """Start the Waldiez workflow asynchronously."""
368
+
369
+ async def run_in_background() -> None:
370
+ """Run the Waldiez workflow in a background thread."""
371
+ try:
372
+ results = await self._a_run(
373
+ temp_dir,
374
+ output_file,
375
+ uploads_root,
376
+ skip_mmd=skip_mmd,
377
+ skip_timeline=skip_timeline,
378
+ )
379
+ if results:
380
+ self._print(f"<Waldiez> - Workflow completed: {results}")
381
+ except Exception as e:
382
+ self._print(
383
+ f"<Waldiez> - Error during workflow: {e}\n{traceback.format_exc()}"
384
+ )
385
+
386
+ asyncio.create_task(run_in_background())
387
+
388
+ def _stop(self) -> None:
389
+ """Stop the Waldiez workflow."""
390
+ self.log.info("Stopping workflow execution...")
391
+ self._stop_requested.set()
392
+
393
+ # Wait for graceful shutdown
394
+ if self._execution_thread and self._execution_thread.is_alive():
395
+ self._execution_thread.join(timeout=5.0)
396
+
397
+ if self._execution_thread and self._execution_thread.is_alive():
398
+ self.log.warning("Workflow thread did not stop gracefully")
399
+
400
+ async def _a_stop(self) -> None:
401
+ """Stop the Waldiez workflow asynchronously."""
402
+ self.log.info("Stopping workflow execution (async)...")
403
+ self._stop_requested.set()
404
+
405
+ # For async, we rely on the task cancellation in _a_run
406
+ # Let's give it a moment to respond
407
+ await asyncio.sleep(0.5)
408
+
409
+ def get_execution_stats(self) -> dict[str, Any]:
410
+ """Get execution statistics for standard runner.
411
+
412
+ Returns
413
+ -------
414
+ dict[str, Any]
415
+ A dictionary containing execution statistics such as total events,
416
+ processed events, whether a module was loaded, and event processing rate.
417
+ """
418
+ base_stats = super().get_execution_stats()
419
+ return {
420
+ **base_stats,
421
+ "total_events": self._event_count,
422
+ "processed_events": self._processed_events,
423
+ "has_loaded_module": self._loaded_module is not None,
424
+ "event_processing_rate": (
425
+ self._processed_events / self._event_count
426
+ if self._event_count > 0
427
+ else 0
428
+ ),
429
+ }