waldiez 0.6.0__py3-none-any.whl → 0.6.1__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 (188) hide show
  1. waldiez/__init__.py +1 -1
  2. waldiez/_version.py +1 -1
  3. waldiez/cli.py +18 -7
  4. waldiez/cli_extras/jupyter.py +3 -0
  5. waldiez/cli_extras/runner.py +3 -1
  6. waldiez/cli_extras/studio.py +3 -1
  7. waldiez/exporter.py +9 -3
  8. waldiez/exporting/agent/exporter.py +9 -10
  9. waldiez/exporting/agent/extras/captain_agent_extras.py +6 -6
  10. waldiez/exporting/agent/extras/doc_agent_extras.py +6 -6
  11. waldiez/exporting/agent/extras/group_manager_agent_extas.py +34 -23
  12. waldiez/exporting/agent/extras/group_member_extras.py +6 -5
  13. waldiez/exporting/agent/extras/handoffs/after_work.py +1 -1
  14. waldiez/exporting/agent/extras/handoffs/available.py +1 -1
  15. waldiez/exporting/agent/extras/handoffs/condition.py +3 -2
  16. waldiez/exporting/agent/extras/handoffs/handoff.py +1 -1
  17. waldiez/exporting/agent/extras/handoffs/target.py +6 -4
  18. waldiez/exporting/agent/extras/rag/chroma_extras.py +27 -19
  19. waldiez/exporting/agent/extras/rag/mongo_extras.py +8 -8
  20. waldiez/exporting/agent/extras/rag/pgvector_extras.py +5 -5
  21. waldiez/exporting/agent/extras/rag/qdrant_extras.py +5 -4
  22. waldiez/exporting/agent/extras/rag/vector_db_extras.py +1 -1
  23. waldiez/exporting/agent/extras/rag_user_proxy_agent_extras.py +5 -7
  24. waldiez/exporting/agent/extras/reasoning_agent_extras.py +3 -5
  25. waldiez/exporting/chats/exporter.py +4 -4
  26. waldiez/exporting/chats/processor.py +1 -2
  27. waldiez/exporting/chats/utils/common.py +89 -48
  28. waldiez/exporting/chats/utils/group.py +9 -9
  29. waldiez/exporting/chats/utils/nested.py +7 -7
  30. waldiez/exporting/chats/utils/sequential.py +1 -1
  31. waldiez/exporting/chats/utils/single.py +2 -2
  32. waldiez/exporting/core/content.py +7 -7
  33. waldiez/exporting/core/context.py +5 -3
  34. waldiez/exporting/core/exporter.py +5 -3
  35. waldiez/exporting/core/exporters.py +2 -2
  36. waldiez/exporting/core/extras/agent_extras/captain_extras.py +2 -2
  37. waldiez/exporting/core/extras/agent_extras/group_manager_extras.py +2 -2
  38. waldiez/exporting/core/extras/agent_extras/rag_user_extras.py +2 -2
  39. waldiez/exporting/core/extras/agent_extras/standard_extras.py +3 -8
  40. waldiez/exporting/core/extras/base.py +7 -5
  41. waldiez/exporting/core/extras/flow_extras.py +4 -5
  42. waldiez/exporting/core/extras/model_extras.py +2 -2
  43. waldiez/exporting/core/extras/path_resolver.py +1 -2
  44. waldiez/exporting/core/extras/serializer.py +2 -2
  45. waldiez/exporting/core/protocols.py +6 -5
  46. waldiez/exporting/core/result.py +25 -28
  47. waldiez/exporting/core/types.py +10 -10
  48. waldiez/exporting/core/utils/llm_config.py +2 -2
  49. waldiez/exporting/core/validation.py +10 -11
  50. waldiez/exporting/flow/execution_generator.py +98 -10
  51. waldiez/exporting/flow/exporter.py +2 -2
  52. waldiez/exporting/flow/factory.py +2 -2
  53. waldiez/exporting/flow/file_generator.py +4 -2
  54. waldiez/exporting/flow/merger.py +5 -3
  55. waldiez/exporting/flow/orchestrator.py +72 -2
  56. waldiez/exporting/flow/utils/common.py +5 -5
  57. waldiez/exporting/flow/utils/importing.py +6 -7
  58. waldiez/exporting/flow/utils/linting.py +25 -9
  59. waldiez/exporting/flow/utils/logging.py +2 -2
  60. waldiez/exporting/models/exporter.py +8 -8
  61. waldiez/exporting/models/processor.py +5 -5
  62. waldiez/exporting/tools/exporter.py +2 -2
  63. waldiez/exporting/tools/processor.py +7 -4
  64. waldiez/io/__init__.py +8 -4
  65. waldiez/io/_ws.py +10 -6
  66. waldiez/io/models/constants.py +10 -10
  67. waldiez/io/models/content/audio.py +1 -0
  68. waldiez/io/models/content/base.py +20 -18
  69. waldiez/io/models/content/file.py +1 -0
  70. waldiez/io/models/content/image.py +1 -0
  71. waldiez/io/models/content/text.py +1 -0
  72. waldiez/io/models/content/video.py +1 -0
  73. waldiez/io/models/user_input.py +10 -5
  74. waldiez/io/models/user_response.py +17 -16
  75. waldiez/io/mqtt.py +18 -31
  76. waldiez/io/redis.py +18 -22
  77. waldiez/io/structured.py +52 -53
  78. waldiez/io/utils.py +3 -0
  79. waldiez/io/ws.py +5 -1
  80. waldiez/logger.py +16 -3
  81. waldiez/models/agents/__init__.py +3 -0
  82. waldiez/models/agents/agent/agent.py +23 -16
  83. waldiez/models/agents/agent/agent_data.py +25 -22
  84. waldiez/models/agents/agent/code_execution.py +9 -11
  85. waldiez/models/agents/agent/termination_message.py +10 -12
  86. waldiez/models/agents/agent/update_system_message.py +2 -4
  87. waldiez/models/agents/agents.py +8 -8
  88. waldiez/models/agents/assistant/assistant.py +6 -3
  89. waldiez/models/agents/assistant/assistant_data.py +2 -2
  90. waldiez/models/agents/captain/captain_agent.py +7 -4
  91. waldiez/models/agents/captain/captain_agent_data.py +5 -7
  92. waldiez/models/agents/doc_agent/doc_agent.py +7 -4
  93. waldiez/models/agents/doc_agent/doc_agent_data.py +9 -10
  94. waldiez/models/agents/doc_agent/rag_query_engine.py +10 -12
  95. waldiez/models/agents/extra_requirements.py +3 -3
  96. waldiez/models/agents/group_manager/group_manager.py +12 -7
  97. waldiez/models/agents/group_manager/group_manager_data.py +13 -12
  98. waldiez/models/agents/group_manager/speakers.py +17 -19
  99. waldiez/models/agents/rag_user_proxy/rag_user_proxy.py +7 -4
  100. waldiez/models/agents/rag_user_proxy/rag_user_proxy_data.py +4 -1
  101. waldiez/models/agents/rag_user_proxy/retrieve_config.py +69 -63
  102. waldiez/models/agents/rag_user_proxy/vector_db_config.py +19 -19
  103. waldiez/models/agents/reasoning/reasoning_agent.py +7 -4
  104. waldiez/models/agents/reasoning/reasoning_agent_data.py +3 -2
  105. waldiez/models/agents/reasoning/reasoning_agent_reason_config.py +8 -8
  106. waldiez/models/agents/user_proxy/user_proxy.py +6 -3
  107. waldiez/models/agents/user_proxy/user_proxy_data.py +1 -1
  108. waldiez/models/chat/chat.py +27 -20
  109. waldiez/models/chat/chat_data.py +22 -19
  110. waldiez/models/chat/chat_message.py +9 -9
  111. waldiez/models/chat/chat_nested.py +9 -9
  112. waldiez/models/chat/chat_summary.py +6 -6
  113. waldiez/models/common/__init__.py +2 -0
  114. waldiez/models/common/ag2_version.py +2 -0
  115. waldiez/models/common/dict_utils.py +8 -6
  116. waldiez/models/common/handoff.py +18 -17
  117. waldiez/models/common/method_utils.py +7 -7
  118. waldiez/models/common/naming.py +49 -0
  119. waldiez/models/flow/flow.py +11 -6
  120. waldiez/models/flow/flow_data.py +23 -17
  121. waldiez/models/flow/info.py +3 -3
  122. waldiez/models/flow/naming.py +2 -1
  123. waldiez/models/model/_aws.py +11 -13
  124. waldiez/models/model/_llm.py +5 -0
  125. waldiez/models/model/_price.py +2 -4
  126. waldiez/models/model/extra_requirements.py +1 -3
  127. waldiez/models/model/model.py +2 -2
  128. waldiez/models/model/model_data.py +21 -21
  129. waldiez/models/tool/extra_requirements.py +2 -4
  130. waldiez/models/tool/predefined/_duckduckgo.py +1 -0
  131. waldiez/models/tool/predefined/_email.py +1 -0
  132. waldiez/models/tool/predefined/_google.py +1 -0
  133. waldiez/models/tool/predefined/_perplexity.py +1 -0
  134. waldiez/models/tool/predefined/_searxng.py +1 -0
  135. waldiez/models/tool/predefined/_tavily.py +1 -0
  136. waldiez/models/tool/predefined/_wikipedia.py +1 -0
  137. waldiez/models/tool/predefined/_youtube.py +1 -0
  138. waldiez/models/tool/tool.py +8 -5
  139. waldiez/models/tool/tool_data.py +2 -2
  140. waldiez/models/waldiez.py +152 -4
  141. waldiez/runner.py +11 -5
  142. waldiez/running/async_utils.py +192 -0
  143. waldiez/running/base_runner.py +117 -264
  144. waldiez/running/dir_utils.py +52 -0
  145. waldiez/running/environment.py +10 -44
  146. waldiez/running/events_mixin.py +252 -0
  147. waldiez/running/exceptions.py +20 -0
  148. waldiez/running/gen_seq_diagram.py +18 -15
  149. waldiez/running/io_utils.py +216 -0
  150. waldiez/running/protocol.py +11 -5
  151. waldiez/running/requirements_mixin.py +65 -0
  152. waldiez/running/results_mixin.py +926 -0
  153. waldiez/running/standard_runner.py +22 -25
  154. waldiez/running/step_by_step/breakpoints_mixin.py +192 -60
  155. waldiez/running/step_by_step/command_handler.py +3 -0
  156. waldiez/running/step_by_step/events_processor.py +194 -14
  157. waldiez/running/step_by_step/step_by_step_models.py +110 -43
  158. waldiez/running/step_by_step/step_by_step_runner.py +107 -57
  159. waldiez/running/subprocess_runner/__base__.py +9 -1
  160. waldiez/running/subprocess_runner/_async_runner.py +5 -3
  161. waldiez/running/subprocess_runner/_sync_runner.py +6 -2
  162. waldiez/running/subprocess_runner/runner.py +39 -23
  163. waldiez/running/timeline_processor.py +1 -1
  164. waldiez/utils/__init__.py +2 -0
  165. waldiez/utils/conflict_checker.py +4 -4
  166. waldiez/utils/python_manager.py +415 -0
  167. waldiez/ws/_file_handler.py +18 -18
  168. waldiez/ws/_mock.py +2 -1
  169. waldiez/ws/cli.py +36 -12
  170. waldiez/ws/client_manager.py +35 -27
  171. waldiez/ws/errors.py +3 -0
  172. waldiez/ws/models.py +43 -52
  173. waldiez/ws/reloader.py +12 -4
  174. waldiez/ws/server.py +85 -55
  175. waldiez/ws/session_manager.py +8 -9
  176. waldiez/ws/session_stats.py +1 -1
  177. waldiez/ws/utils.py +4 -1
  178. {waldiez-0.6.0.dist-info → waldiez-0.6.1.dist-info}/METADATA +82 -93
  179. waldiez-0.6.1.dist-info/RECORD +254 -0
  180. waldiez/running/post_run.py +0 -186
  181. waldiez/running/pre_run.py +0 -281
  182. waldiez/running/run_results.py +0 -14
  183. waldiez/running/utils.py +0 -625
  184. waldiez-0.6.0.dist-info/RECORD +0 -251
  185. {waldiez-0.6.0.dist-info → waldiez-0.6.1.dist-info}/WHEEL +0 -0
  186. {waldiez-0.6.0.dist-info → waldiez-0.6.1.dist-info}/entry_points.txt +0 -0
  187. {waldiez-0.6.0.dist-info → waldiez-0.6.1.dist-info}/licenses/LICENSE +0 -0
  188. {waldiez-0.6.0.dist-info → waldiez-0.6.1.dist-info}/licenses/NOTICE.md +0 -0
@@ -146,6 +146,7 @@ def {self.name}(
146
146
  return content
147
147
 
148
148
 
149
+ # pylint: disable=invalid-name
149
150
  SearxNGSearchTool = SearxNGSearchToolImpl()
150
151
  SearxNGSearchConfig = PredefinedToolConfig(
151
152
  name=SearxNGSearchTool.name,
@@ -146,6 +146,7 @@ def {self.name}(
146
146
  return content
147
147
 
148
148
 
149
+ # pylint: disable=invalid-name
149
150
  TavilySearchTool = TavilySearchToolImpl()
150
151
  TavilySearchConfig = PredefinedToolConfig(
151
152
  name=TavilySearchTool.name,
@@ -145,6 +145,7 @@ def {self.name}(query: str, language: str = "en", top_k: int = 3, verbose: bool
145
145
  return content
146
146
 
147
147
 
148
+ # pylint: disable=invalid-name
148
149
  WikipediaSearchTool = WikipediaSearchToolImpl()
149
150
  WikipediaSearchConfig = PredefinedToolConfig(
150
151
  name=WikipediaSearchTool.name,
@@ -138,6 +138,7 @@ def {self.name}(
138
138
  return content
139
139
 
140
140
 
141
+ # pylint: disable=invalid-name
141
142
  YouTubeSearchTool = YouTubeSearchToolImpl()
142
143
  YouTubeSearchConfig = PredefinedToolConfig(
143
144
  name=YouTubeSearchTool.name,
@@ -81,7 +81,7 @@ class WaldiezTool(WaldiezBase):
81
81
  description="The tags of the tool.",
82
82
  default_factory=list,
83
83
  ),
84
- ] = []
84
+ ]
85
85
  requirements: Annotated[
86
86
  list[str],
87
87
  Field(
@@ -89,7 +89,7 @@ class WaldiezTool(WaldiezBase):
89
89
  description="The requirements of the tool.",
90
90
  default_factory=list,
91
91
  ),
92
- ] = []
92
+ ]
93
93
  data: Annotated[
94
94
  WaldiezToolData,
95
95
  Field(..., title="Data", description="The data of the tool."),
@@ -324,22 +324,25 @@ class WaldiezTool(WaldiezBase):
324
324
  config = get_predefined_tool_config(self.name)
325
325
  if not config:
326
326
  available_tools = list_predefined_tools()
327
- raise ValueError(
327
+ msg = (
328
328
  f"Unknown predefined tool: {self.name}. "
329
329
  f"Available tools: {available_tools}"
330
330
  )
331
+ raise ValueError(msg)
331
332
  missing_secrets = config.validate_secrets(self.data.secrets)
332
333
  if missing_secrets:
333
- raise ValueError(
334
+ msg = (
334
335
  f"Missing required secrets for {self.name}: "
335
336
  f"{missing_secrets}"
336
337
  )
338
+ raise ValueError(msg)
337
339
  invalid_kwargs = config.validate_kwargs(self.data.kwargs)
338
340
  if invalid_kwargs:
339
- raise ValueError(
341
+ msg = (
340
342
  f"Invalid keyword arguments for {self.name}: "
341
343
  f"{invalid_kwargs}"
342
344
  )
345
+ raise ValueError(msg)
343
346
  # Update tool metadata from predefined config
344
347
  if not self.description:
345
348
  self.description = config.description
@@ -34,7 +34,7 @@ class WaldiezToolData(WaldiezBase):
34
34
  "The type of the tool: shared, custom, langchain, crewai."
35
35
  ),
36
36
  ),
37
- ] = "custom"
37
+ ]
38
38
  content: Annotated[
39
39
  str,
40
40
  Field(
@@ -60,7 +60,7 @@ class WaldiezToolData(WaldiezBase):
60
60
  "Keyword arguments for the tool, used for initialization."
61
61
  ),
62
62
  ),
63
- ] = {}
63
+ ]
64
64
 
65
65
  _raw_content: str = ""
66
66
 
waldiez/models/waldiez.py CHANGED
@@ -8,10 +8,15 @@ and run an autogen workflow. It has the model/LLM configurations, the agent
8
8
  definitions and their optional additional tools to be used.
9
9
  """
10
10
 
11
+ import asyncio
11
12
  import json
13
+ import tempfile
14
+ from collections.abc import Iterator
12
15
  from dataclasses import dataclass
13
16
  from pathlib import Path
14
- from typing import Any, Iterator
17
+ from typing import Any
18
+
19
+ import aiofiles
15
20
 
16
21
  from .agents import (
17
22
  WaldiezAgent,
@@ -19,7 +24,7 @@ from .agents import (
19
24
  get_captain_agent_extra_requirements,
20
25
  get_retrievechat_extra_requirements,
21
26
  )
22
- from .common import get_autogen_version
27
+ from .common import get_autogen_version, safe_filename
23
28
  from .flow import (
24
29
  WaldiezAgentConnection,
25
30
  WaldiezFlow,
@@ -92,7 +97,7 @@ class Waldiez:
92
97
  requirements=requirements,
93
98
  )
94
99
  validated = WaldiezFlow.model_validate(flow)
95
- return cls(flow=validated) # pyright: ignore
100
+ return cls(flow=validated)
96
101
 
97
102
  @classmethod
98
103
  def load(
@@ -131,7 +136,7 @@ class Waldiez:
131
136
  data: dict[str, Any] = {}
132
137
  if not Path(waldiez_file).exists():
133
138
  raise ValueError(f"File not found: {waldiez_file}")
134
- with open(waldiez_file, "r", encoding="utf-8") as file:
139
+ with open(waldiez_file, "r", encoding="utf-8", newline="\n") as file:
135
140
  try:
136
141
  data = json.load(file)
137
142
  except json.decoder.JSONDecodeError as error:
@@ -144,6 +149,59 @@ class Waldiez:
144
149
  requirements=requirements,
145
150
  )
146
151
 
152
+ @classmethod
153
+ async def a_load(
154
+ cls,
155
+ waldiez_file: str | Path,
156
+ name: str | None = None,
157
+ description: str | None = None,
158
+ tags: list[str] | None = None,
159
+ requirements: list[str] | None = None,
160
+ ) -> "Waldiez":
161
+ """Load a Waldiez from a file.
162
+
163
+ Parameters
164
+ ----------
165
+ waldiez_file : Union[str, Path]
166
+ The Waldiez file.
167
+ name: str | None, optional
168
+ The name, by default None (retrieved from data).
169
+ description : str | None, optional
170
+ The description, by default None (retrieved from data).
171
+ tags: list[str] | None, optional
172
+ The tags, by default None (retrieved from data).
173
+ requirements: list[str] | None, optional
174
+ The requirements, by default None (retrieved from data).
175
+
176
+ Returns
177
+ -------
178
+ Waldiez
179
+ The Waldiez.
180
+
181
+ Raises
182
+ ------
183
+ ValueError
184
+ If the file is not found or invalid JSON.
185
+ """
186
+ data: dict[str, Any] = {}
187
+ if not Path(waldiez_file).exists():
188
+ raise ValueError(f"File not found: {waldiez_file}")
189
+ async with aiofiles.open(
190
+ waldiez_file, "r", encoding="utf-8", newline="\n"
191
+ ) as file:
192
+ try:
193
+ contents = await file.read()
194
+ data = json.loads(contents)
195
+ except json.decoder.JSONDecodeError as error:
196
+ raise ValueError(f"Invalid JSON: {waldiez_file}") from error
197
+ return cls.from_dict(
198
+ data,
199
+ name=name,
200
+ description=description,
201
+ tags=tags,
202
+ requirements=requirements,
203
+ )
204
+
147
205
  def model_dump_json(
148
206
  self, by_alias: bool = True, indent: int | None = None
149
207
  ) -> str:
@@ -200,6 +258,26 @@ class Waldiez:
200
258
  """Get the chats."""
201
259
  return self.flow.ordered_flow
202
260
 
261
+ @property
262
+ def is_group_pattern_based(
263
+ self,
264
+ ) -> bool:
265
+ """Check if the group manager should use pattern strategy.
266
+
267
+ Returns
268
+ -------
269
+ bool
270
+ True if pattern strategy should be used, False otherwise.
271
+ """
272
+ if not self.initial_chats:
273
+ return True
274
+
275
+ first_chat = self.initial_chats[0]["chat"]
276
+ return (
277
+ isinstance(first_chat.data.message, str)
278
+ or not first_chat.data.message.is_method()
279
+ )
280
+
203
281
  @property
204
282
  def agents(self) -> Iterator[WaldiezAgent]:
205
283
  """Get the agents.
@@ -367,3 +445,73 @@ class Waldiez:
367
445
  if not agent.is_group_manager:
368
446
  return []
369
447
  return self.flow.get_group_chat_members(agent.id)
448
+
449
+ def dump(self, to: str | Path | None = None) -> Path:
450
+ """Dump waldiez flow to a file.
451
+
452
+ Parameters
453
+ ----------
454
+ to : str | Path | None
455
+ Optional output path to determine the directory to save the flow to.
456
+
457
+ Returns
458
+ -------
459
+ Path
460
+ The path to the generated file.
461
+ """
462
+ file_path = Path(to) if to else None
463
+ if file_path:
464
+ file_name = file_path.name
465
+ if not file_name.endswith(".waldiez"):
466
+ file_path.with_suffix(".waldiez")
467
+
468
+ else:
469
+ full_name = self.name
470
+ file_name = safe_filename(full_name, "waldiez")
471
+ file_dir: Path
472
+ if file_path:
473
+ file_dir = file_path if file_path.is_dir() else file_path.parent
474
+ else:
475
+ file_dir = Path(tempfile.mkdtemp())
476
+ file_dir.mkdir(parents=True, exist_ok=True)
477
+ output_path = file_dir / file_name
478
+ with output_path.open(
479
+ "w", encoding="utf-8", errors="replace", newline="\n"
480
+ ) as f_open:
481
+ f_open.write(self.model_dump_json())
482
+ return output_path
483
+
484
+ async def a_dump(self, to: str | Path | None = None) -> Path:
485
+ """Dump waldiez flow to a file asynchronously.
486
+
487
+ Parameters
488
+ ----------
489
+ to : str | Path | None
490
+ Optional output path to determine the directory to save the flow to.
491
+
492
+ Returns
493
+ -------
494
+ Path
495
+ The path to the generated file.
496
+ """
497
+ file_path = Path(to) if to else None
498
+ if file_path:
499
+ file_name = file_path.name
500
+ if not file_name.endswith(".waldiez"):
501
+ file_path.with_suffix(".waldiez")
502
+ else:
503
+ full_name = self.name
504
+ file_name = safe_filename(full_name, "waldiez")
505
+ file_dir: Path
506
+ if file_path:
507
+ file_dir = file_path if file_path.is_dir() else file_path.parent
508
+ else:
509
+ tmp_dir = await asyncio.to_thread(tempfile.mkdtemp)
510
+ file_dir = Path(tmp_dir)
511
+ file_dir.mkdir(parents=True, exist_ok=True)
512
+ output_path = file_dir / file_name
513
+ async with aiofiles.open(
514
+ output_path, "w", encoding="utf-8", errors="replace", newline="\n"
515
+ ) as f_open:
516
+ await f_open.write(self.model_dump_json())
517
+ return output_path
waldiez/runner.py CHANGED
@@ -20,7 +20,7 @@ during the flow execution.
20
20
  from pathlib import Path
21
21
  from typing import Any
22
22
 
23
- from typing_extensions import Literal
23
+ from typing_extensions import Literal, override
24
24
 
25
25
  from .models.waldiez import Waldiez
26
26
  from .running import (
@@ -81,12 +81,12 @@ def create_runner(
81
81
  f"Unknown runner mode '{mode}'. Available: {available}"
82
82
  )
83
83
 
84
- runner_class = runners[mode]
84
+ runner_cls = runners[mode]
85
85
  if mode == "subprocess":
86
86
  subprocess_mode = kwargs.pop("subprocess_mode", "run")
87
87
  if subprocess_mode not in ["run", "debug"]:
88
88
  subprocess_mode = "run"
89
- return runner_class(
89
+ return runner_cls(
90
90
  waldiez=waldiez,
91
91
  output_path=output_path,
92
92
  uploads_root=uploads_root,
@@ -95,7 +95,9 @@ def create_runner(
95
95
  mode=subprocess_mode,
96
96
  **kwargs,
97
97
  )
98
- return runner_class(
98
+ if mode != "debug" and "breakpoints" in kwargs: # pragma: no cover
99
+ kwargs.pop("breakpoints", None)
100
+ return runner_cls(
99
101
  waldiez=waldiez,
100
102
  output_path=output_path,
101
103
  uploads_root=uploads_root,
@@ -111,7 +113,7 @@ class WaldiezRunner(WaldiezBaseRunner):
111
113
 
112
114
  # pylint: disable=super-init-not-called
113
115
  # noinspection PyMissingConstructor
114
- def __init__(
116
+ def __init__( # pyright: ignore[reportMissingSuperCall]
115
117
  self,
116
118
  waldiez: Waldiez,
117
119
  mode: Literal["standard", "debug"] = "standard",
@@ -150,6 +152,7 @@ class WaldiezRunner(WaldiezBaseRunner):
150
152
  **kwargs,
151
153
  )
152
154
 
155
+ @override
153
156
  def __repr__(self) -> str: # pragma: no cover
154
157
  """Get the string representation of the runner.
155
158
 
@@ -179,6 +182,7 @@ class WaldiezRunner(WaldiezBaseRunner):
179
182
  f"{type(self).__name__} has no attribute '{name}'"
180
183
  ) # pragma: no cover
181
184
 
185
+ @override
182
186
  def _run(
183
187
  self,
184
188
  temp_dir: Path,
@@ -197,6 +201,7 @@ class WaldiezRunner(WaldiezBaseRunner):
197
201
  **kwargs,
198
202
  )
199
203
 
204
+ @override
200
205
  async def _a_run(
201
206
  self,
202
207
  temp_dir: Path,
@@ -217,6 +222,7 @@ class WaldiezRunner(WaldiezBaseRunner):
217
222
  **kwargs,
218
223
  )
219
224
 
225
+ @override
220
226
  @classmethod
221
227
  def load(
222
228
  cls,
@@ -0,0 +1,192 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+ """Common utilities for the waldiez runner."""
4
+
5
+ import asyncio
6
+ import functools
7
+ import inspect
8
+ import logging
9
+ import threading
10
+
11
+ # noinspection PyProtectedMember
12
+ from collections.abc import Coroutine
13
+ from dataclasses import dataclass
14
+ from typing import (
15
+ Any,
16
+ Callable,
17
+ Generic,
18
+ TypeVar,
19
+ cast,
20
+ )
21
+
22
+ T = TypeVar("T")
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ @dataclass
28
+ class _ResultContainer(Generic[T]):
29
+ """Container for thread execution results with proper typing."""
30
+
31
+ result: T | None = None
32
+ exception: BaseException | None = None
33
+
34
+
35
+ def is_async_callable(fn: Any) -> bool:
36
+ """Check if a function is async callable, including partials/callables.
37
+
38
+ Parameters
39
+ ----------
40
+ fn : Any
41
+ The function to check.
42
+
43
+ Returns
44
+ -------
45
+ bool
46
+ True if the function is async callable, False otherwise.
47
+ """
48
+ if isinstance(fn, functools.partial):
49
+ fn = fn.func
50
+ unwrapped = inspect.unwrap(fn)
51
+ return inspect.iscoroutinefunction(
52
+ unwrapped
53
+ ) or inspect.iscoroutinefunction(
54
+ getattr(unwrapped, "__call__", None), # noqa: B004
55
+ )
56
+
57
+
58
+ def syncify(
59
+ async_func: Callable[..., Coroutine[Any, Any, T]],
60
+ timeout: float | None = None,
61
+ ) -> Callable[..., T]:
62
+ """Convert an async function to a sync function.
63
+
64
+ This function handles the conversion of async functions to sync functions,
65
+ properly managing event loops and thread execution contexts.
66
+
67
+ Parameters
68
+ ----------
69
+ async_func : Callable[..., Coroutine[Any, Any, T]]
70
+ The async function to convert.
71
+ timeout : float | None, optional
72
+ The timeout for the sync function. Defaults to None.
73
+
74
+ Returns
75
+ -------
76
+ Callable[..., T]
77
+ The converted sync function.
78
+
79
+ Raises
80
+ ------
81
+ TimeoutError
82
+ If the async function times out.
83
+ RuntimeError
84
+ If there are issues with event loop management.
85
+ """
86
+
87
+ def _sync_wrapper(*args: Any, **kwargs: Any) -> T:
88
+ """Get the result of the async function."""
89
+ # pylint: disable=too-many-try-statements
90
+ try:
91
+ # Check if we're already in an event loop
92
+ asyncio.get_running_loop()
93
+ return _run_in_thread(async_func, args, kwargs, timeout)
94
+ except RuntimeError:
95
+ # No event loop running, we can use asyncio.run directly
96
+ logger.debug("No event loop running, using asyncio.run")
97
+
98
+ # Create a new event loop and run the coroutine
99
+ try:
100
+ if timeout is not None:
101
+ # Need to run with asyncio.run and wait_for inside
102
+ async def _with_timeout() -> T:
103
+ return await asyncio.wait_for(
104
+ async_func(*args, **kwargs), timeout=timeout
105
+ )
106
+
107
+ return asyncio.run(_with_timeout())
108
+ return asyncio.run(async_func(*args, **kwargs))
109
+ except (asyncio.TimeoutError, TimeoutError) as e:
110
+ raise TimeoutError(
111
+ f"Async function timed out after {timeout} seconds"
112
+ ) from e
113
+
114
+ return _sync_wrapper
115
+
116
+
117
+ def _run_in_thread(
118
+ async_func: Callable[..., Coroutine[Any, Any, T]],
119
+ args: tuple[Any, ...],
120
+ kwargs: dict[str, Any],
121
+ timeout: float | None,
122
+ ) -> T:
123
+ """Run async function in a separate thread.
124
+
125
+ Parameters
126
+ ----------
127
+ async_func : Callable[..., Coroutine[Any, Any, T]]
128
+ The async function to run.
129
+ args : tuple[Any, ...]
130
+ Positional arguments for the function.
131
+ kwargs : dict[str, Any]
132
+ Keyword arguments for the function.
133
+ timeout : float | None
134
+ Timeout in seconds.
135
+
136
+ Returns
137
+ -------
138
+ T
139
+ The result of the async function.
140
+
141
+ Raises
142
+ ------
143
+ TimeoutError
144
+ If the function execution times out.
145
+ RuntimeError
146
+ If thread execution fails unexpectedly.
147
+ """
148
+ result_container: _ResultContainer[T] = _ResultContainer()
149
+ finished_event = threading.Event()
150
+
151
+ def _thread_target() -> None:
152
+ """Target function for the thread."""
153
+ # pylint: disable=too-many-try-statements, broad-exception-caught
154
+ try:
155
+ if timeout is not None:
156
+
157
+ async def _with_timeout() -> T:
158
+ return await asyncio.wait_for(
159
+ async_func(*args, **kwargs), timeout=timeout
160
+ )
161
+
162
+ result_container.result = asyncio.run(_with_timeout())
163
+ else:
164
+ result_container.result = asyncio.run(
165
+ async_func(*args, **kwargs)
166
+ )
167
+ except (
168
+ BaseException
169
+ ) as e: # Catch BaseException to propagate cancellations
170
+ result_container.exception = e
171
+ finally:
172
+ finished_event.set()
173
+
174
+ thread = threading.Thread(target=_thread_target, daemon=True)
175
+ thread.start()
176
+
177
+ # Wait for completion with timeout
178
+ timeout_buffer = 1.0 # 1 second buffer for cleanup
179
+ wait_timeout = timeout + timeout_buffer if timeout is not None else None
180
+
181
+ if not finished_event.wait(timeout=wait_timeout): # pragma: no cover
182
+ raise TimeoutError(
183
+ f"Function execution timed out after {timeout} seconds"
184
+ )
185
+
186
+ thread.join(timeout=timeout_buffer) # Give thread time to clean up
187
+
188
+ if result_container.exception is not None:
189
+ raise result_container.exception
190
+
191
+ # Use cast since we know the result should be T if no exception occurred
192
+ return cast(T, result_container.result)