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.
- waldiez/__init__.py +1 -1
- waldiez/_version.py +1 -1
- waldiez/cli.py +18 -7
- waldiez/cli_extras/jupyter.py +3 -0
- waldiez/cli_extras/runner.py +3 -1
- waldiez/cli_extras/studio.py +3 -1
- waldiez/exporter.py +9 -3
- waldiez/exporting/agent/exporter.py +9 -10
- waldiez/exporting/agent/extras/captain_agent_extras.py +6 -6
- waldiez/exporting/agent/extras/doc_agent_extras.py +6 -6
- waldiez/exporting/agent/extras/group_manager_agent_extas.py +34 -23
- waldiez/exporting/agent/extras/group_member_extras.py +6 -5
- waldiez/exporting/agent/extras/handoffs/after_work.py +1 -1
- waldiez/exporting/agent/extras/handoffs/available.py +1 -1
- waldiez/exporting/agent/extras/handoffs/condition.py +3 -2
- waldiez/exporting/agent/extras/handoffs/handoff.py +1 -1
- waldiez/exporting/agent/extras/handoffs/target.py +6 -4
- waldiez/exporting/agent/extras/rag/chroma_extras.py +27 -19
- waldiez/exporting/agent/extras/rag/mongo_extras.py +8 -8
- waldiez/exporting/agent/extras/rag/pgvector_extras.py +5 -5
- waldiez/exporting/agent/extras/rag/qdrant_extras.py +5 -4
- waldiez/exporting/agent/extras/rag/vector_db_extras.py +1 -1
- waldiez/exporting/agent/extras/rag_user_proxy_agent_extras.py +5 -7
- waldiez/exporting/agent/extras/reasoning_agent_extras.py +3 -5
- waldiez/exporting/chats/exporter.py +4 -4
- waldiez/exporting/chats/processor.py +1 -2
- waldiez/exporting/chats/utils/common.py +89 -48
- waldiez/exporting/chats/utils/group.py +9 -9
- waldiez/exporting/chats/utils/nested.py +7 -7
- waldiez/exporting/chats/utils/sequential.py +1 -1
- waldiez/exporting/chats/utils/single.py +2 -2
- waldiez/exporting/core/content.py +7 -7
- waldiez/exporting/core/context.py +5 -3
- waldiez/exporting/core/exporter.py +5 -3
- waldiez/exporting/core/exporters.py +2 -2
- waldiez/exporting/core/extras/agent_extras/captain_extras.py +2 -2
- waldiez/exporting/core/extras/agent_extras/group_manager_extras.py +2 -2
- waldiez/exporting/core/extras/agent_extras/rag_user_extras.py +2 -2
- waldiez/exporting/core/extras/agent_extras/standard_extras.py +3 -8
- waldiez/exporting/core/extras/base.py +7 -5
- waldiez/exporting/core/extras/flow_extras.py +4 -5
- waldiez/exporting/core/extras/model_extras.py +2 -2
- waldiez/exporting/core/extras/path_resolver.py +1 -2
- waldiez/exporting/core/extras/serializer.py +2 -2
- waldiez/exporting/core/protocols.py +6 -5
- waldiez/exporting/core/result.py +25 -28
- waldiez/exporting/core/types.py +10 -10
- waldiez/exporting/core/utils/llm_config.py +2 -2
- waldiez/exporting/core/validation.py +10 -11
- waldiez/exporting/flow/execution_generator.py +98 -10
- waldiez/exporting/flow/exporter.py +2 -2
- waldiez/exporting/flow/factory.py +2 -2
- waldiez/exporting/flow/file_generator.py +4 -2
- waldiez/exporting/flow/merger.py +5 -3
- waldiez/exporting/flow/orchestrator.py +72 -2
- waldiez/exporting/flow/utils/common.py +5 -5
- waldiez/exporting/flow/utils/importing.py +6 -7
- waldiez/exporting/flow/utils/linting.py +25 -9
- waldiez/exporting/flow/utils/logging.py +2 -2
- waldiez/exporting/models/exporter.py +8 -8
- waldiez/exporting/models/processor.py +5 -5
- waldiez/exporting/tools/exporter.py +2 -2
- waldiez/exporting/tools/processor.py +7 -4
- waldiez/io/__init__.py +8 -4
- waldiez/io/_ws.py +10 -6
- waldiez/io/models/constants.py +10 -10
- waldiez/io/models/content/audio.py +1 -0
- waldiez/io/models/content/base.py +20 -18
- waldiez/io/models/content/file.py +1 -0
- waldiez/io/models/content/image.py +1 -0
- waldiez/io/models/content/text.py +1 -0
- waldiez/io/models/content/video.py +1 -0
- waldiez/io/models/user_input.py +10 -5
- waldiez/io/models/user_response.py +17 -16
- waldiez/io/mqtt.py +18 -31
- waldiez/io/redis.py +18 -22
- waldiez/io/structured.py +52 -53
- waldiez/io/utils.py +3 -0
- waldiez/io/ws.py +5 -1
- waldiez/logger.py +16 -3
- waldiez/models/agents/__init__.py +3 -0
- waldiez/models/agents/agent/agent.py +23 -16
- waldiez/models/agents/agent/agent_data.py +25 -22
- waldiez/models/agents/agent/code_execution.py +9 -11
- waldiez/models/agents/agent/termination_message.py +10 -12
- waldiez/models/agents/agent/update_system_message.py +2 -4
- waldiez/models/agents/agents.py +8 -8
- waldiez/models/agents/assistant/assistant.py +6 -3
- waldiez/models/agents/assistant/assistant_data.py +2 -2
- waldiez/models/agents/captain/captain_agent.py +7 -4
- waldiez/models/agents/captain/captain_agent_data.py +5 -7
- waldiez/models/agents/doc_agent/doc_agent.py +7 -4
- waldiez/models/agents/doc_agent/doc_agent_data.py +9 -10
- waldiez/models/agents/doc_agent/rag_query_engine.py +10 -12
- waldiez/models/agents/extra_requirements.py +3 -3
- waldiez/models/agents/group_manager/group_manager.py +12 -7
- waldiez/models/agents/group_manager/group_manager_data.py +13 -12
- waldiez/models/agents/group_manager/speakers.py +17 -19
- waldiez/models/agents/rag_user_proxy/rag_user_proxy.py +7 -4
- waldiez/models/agents/rag_user_proxy/rag_user_proxy_data.py +4 -1
- waldiez/models/agents/rag_user_proxy/retrieve_config.py +69 -63
- waldiez/models/agents/rag_user_proxy/vector_db_config.py +19 -19
- waldiez/models/agents/reasoning/reasoning_agent.py +7 -4
- waldiez/models/agents/reasoning/reasoning_agent_data.py +3 -2
- waldiez/models/agents/reasoning/reasoning_agent_reason_config.py +8 -8
- waldiez/models/agents/user_proxy/user_proxy.py +6 -3
- waldiez/models/agents/user_proxy/user_proxy_data.py +1 -1
- waldiez/models/chat/chat.py +27 -20
- waldiez/models/chat/chat_data.py +22 -19
- waldiez/models/chat/chat_message.py +9 -9
- waldiez/models/chat/chat_nested.py +9 -9
- waldiez/models/chat/chat_summary.py +6 -6
- waldiez/models/common/__init__.py +2 -0
- waldiez/models/common/ag2_version.py +2 -0
- waldiez/models/common/dict_utils.py +8 -6
- waldiez/models/common/handoff.py +18 -17
- waldiez/models/common/method_utils.py +7 -7
- waldiez/models/common/naming.py +49 -0
- waldiez/models/flow/flow.py +11 -6
- waldiez/models/flow/flow_data.py +23 -17
- waldiez/models/flow/info.py +3 -3
- waldiez/models/flow/naming.py +2 -1
- waldiez/models/model/_aws.py +11 -13
- waldiez/models/model/_llm.py +5 -0
- waldiez/models/model/_price.py +2 -4
- waldiez/models/model/extra_requirements.py +1 -3
- waldiez/models/model/model.py +2 -2
- waldiez/models/model/model_data.py +21 -21
- waldiez/models/tool/extra_requirements.py +2 -4
- waldiez/models/tool/predefined/_duckduckgo.py +1 -0
- waldiez/models/tool/predefined/_email.py +1 -0
- waldiez/models/tool/predefined/_google.py +1 -0
- waldiez/models/tool/predefined/_perplexity.py +1 -0
- waldiez/models/tool/predefined/_searxng.py +1 -0
- waldiez/models/tool/predefined/_tavily.py +1 -0
- waldiez/models/tool/predefined/_wikipedia.py +1 -0
- waldiez/models/tool/predefined/_youtube.py +1 -0
- waldiez/models/tool/tool.py +8 -5
- waldiez/models/tool/tool_data.py +2 -2
- waldiez/models/waldiez.py +152 -4
- waldiez/runner.py +11 -5
- waldiez/running/async_utils.py +192 -0
- waldiez/running/base_runner.py +117 -264
- waldiez/running/dir_utils.py +52 -0
- waldiez/running/environment.py +10 -44
- waldiez/running/events_mixin.py +252 -0
- waldiez/running/exceptions.py +20 -0
- waldiez/running/gen_seq_diagram.py +18 -15
- waldiez/running/io_utils.py +216 -0
- waldiez/running/protocol.py +11 -5
- waldiez/running/requirements_mixin.py +65 -0
- waldiez/running/results_mixin.py +926 -0
- waldiez/running/standard_runner.py +22 -25
- waldiez/running/step_by_step/breakpoints_mixin.py +192 -60
- waldiez/running/step_by_step/command_handler.py +3 -0
- waldiez/running/step_by_step/events_processor.py +194 -14
- waldiez/running/step_by_step/step_by_step_models.py +110 -43
- waldiez/running/step_by_step/step_by_step_runner.py +107 -57
- waldiez/running/subprocess_runner/__base__.py +9 -1
- waldiez/running/subprocess_runner/_async_runner.py +5 -3
- waldiez/running/subprocess_runner/_sync_runner.py +6 -2
- waldiez/running/subprocess_runner/runner.py +39 -23
- waldiez/running/timeline_processor.py +1 -1
- waldiez/utils/__init__.py +2 -0
- waldiez/utils/conflict_checker.py +4 -4
- waldiez/utils/python_manager.py +415 -0
- waldiez/ws/_file_handler.py +18 -18
- waldiez/ws/_mock.py +2 -1
- waldiez/ws/cli.py +36 -12
- waldiez/ws/client_manager.py +35 -27
- waldiez/ws/errors.py +3 -0
- waldiez/ws/models.py +43 -52
- waldiez/ws/reloader.py +12 -4
- waldiez/ws/server.py +85 -55
- waldiez/ws/session_manager.py +8 -9
- waldiez/ws/session_stats.py +1 -1
- waldiez/ws/utils.py +4 -1
- {waldiez-0.6.0.dist-info → waldiez-0.6.1.dist-info}/METADATA +82 -93
- waldiez-0.6.1.dist-info/RECORD +254 -0
- waldiez/running/post_run.py +0 -186
- waldiez/running/pre_run.py +0 -281
- waldiez/running/run_results.py +0 -14
- waldiez/running/utils.py +0 -625
- waldiez-0.6.0.dist-info/RECORD +0 -251
- {waldiez-0.6.0.dist-info → waldiez-0.6.1.dist-info}/WHEEL +0 -0
- {waldiez-0.6.0.dist-info → waldiez-0.6.1.dist-info}/entry_points.txt +0 -0
- {waldiez-0.6.0.dist-info → waldiez-0.6.1.dist-info}/licenses/LICENSE +0 -0
- {waldiez-0.6.0.dist-info → waldiez-0.6.1.dist-info}/licenses/NOTICE.md +0 -0
|
@@ -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,
|
waldiez/models/tool/tool.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
waldiez/models/tool/tool_data.py
CHANGED
|
@@ -34,7 +34,7 @@ class WaldiezToolData(WaldiezBase):
|
|
|
34
34
|
"The type of the tool: shared, custom, langchain, crewai."
|
|
35
35
|
),
|
|
36
36
|
),
|
|
37
|
-
]
|
|
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
|
|
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)
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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)
|