waldiez 0.5.3__py3-none-any.whl → 0.5.5__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/_version.py +1 -1
- waldiez/cli.py +3 -27
- waldiez/exporter.py +0 -13
- waldiez/exporting/agent/exporter.py +38 -0
- waldiez/exporting/agent/extras/__init__.py +2 -0
- waldiez/exporting/agent/extras/doc_agent_extras.py +366 -0
- waldiez/exporting/agent/extras/group_member_extras.py +3 -2
- waldiez/exporting/agent/processor.py +113 -15
- waldiez/exporting/chats/processor.py +2 -21
- waldiez/exporting/chats/utils/common.py +66 -1
- waldiez/exporting/chats/utils/group.py +6 -3
- waldiez/exporting/chats/utils/nested.py +1 -1
- waldiez/exporting/chats/utils/sequential.py +25 -9
- waldiez/exporting/chats/utils/single.py +8 -6
- waldiez/exporting/core/context.py +0 -12
- waldiez/exporting/core/extras/agent_extras/standard_extras.py +3 -1
- waldiez/exporting/core/extras/base.py +20 -17
- waldiez/exporting/core/extras/path_resolver.py +39 -41
- waldiez/exporting/core/extras/serializer.py +16 -1
- waldiez/exporting/core/protocols.py +17 -0
- waldiez/exporting/core/types.py +6 -9
- waldiez/exporting/flow/execution_generator.py +56 -21
- waldiez/exporting/flow/exporter.py +1 -4
- waldiez/exporting/flow/factory.py +0 -9
- waldiez/exporting/flow/file_generator.py +6 -0
- waldiez/exporting/flow/orchestrator.py +27 -21
- waldiez/exporting/flow/utils/__init__.py +0 -2
- waldiez/exporting/flow/utils/common.py +15 -96
- waldiez/exporting/flow/utils/importing.py +4 -0
- waldiez/io/mqtt.py +33 -14
- waldiez/io/redis.py +18 -13
- waldiez/io/structured.py +9 -4
- waldiez/io/utils.py +32 -0
- waldiez/io/ws.py +8 -2
- waldiez/models/__init__.py +6 -0
- waldiez/models/agents/__init__.py +8 -0
- waldiez/models/agents/agent/agent.py +136 -38
- waldiez/models/agents/agent/agent_type.py +3 -2
- waldiez/models/agents/agents.py +10 -0
- waldiez/models/agents/doc_agent/__init__.py +13 -0
- waldiez/models/agents/doc_agent/doc_agent.py +126 -0
- waldiez/models/agents/doc_agent/doc_agent_data.py +149 -0
- waldiez/models/agents/doc_agent/rag_query_engine.py +127 -0
- waldiez/models/flow/flow.py +13 -2
- waldiez/models/model/__init__.py +2 -2
- waldiez/models/model/_aws.py +75 -0
- waldiez/models/model/_llm.py +516 -0
- waldiez/models/model/_price.py +30 -0
- waldiez/models/model/model.py +45 -2
- waldiez/models/model/model_data.py +2 -83
- waldiez/models/tool/predefined/_duckduckgo.py +123 -0
- waldiez/models/tool/predefined/_google.py +31 -9
- waldiez/models/tool/predefined/_perplexity.py +161 -0
- waldiez/models/tool/predefined/_searxng.py +152 -0
- waldiez/models/tool/predefined/_tavily.py +46 -9
- waldiez/models/tool/predefined/_wikipedia.py +26 -6
- waldiez/models/tool/predefined/_youtube.py +36 -8
- waldiez/models/tool/predefined/registry.py +6 -0
- waldiez/models/waldiez.py +12 -0
- waldiez/runner.py +177 -408
- waldiez/running/__init__.py +2 -4
- waldiez/running/base_runner.py +100 -112
- waldiez/running/environment.py +29 -4
- waldiez/running/post_run.py +0 -1
- waldiez/running/protocol.py +36 -48
- waldiez/running/run_results.py +5 -5
- waldiez/running/standard_runner.py +429 -0
- waldiez/running/timeline_processor.py +0 -82
- {waldiez-0.5.3.dist-info → waldiez-0.5.5.dist-info}/METADATA +59 -62
- {waldiez-0.5.3.dist-info → waldiez-0.5.5.dist-info}/RECORD +74 -64
- waldiez/running/import_runner.py +0 -437
- waldiez/running/subprocess_runner.py +0 -104
- {waldiez-0.5.3.dist-info → waldiez-0.5.5.dist-info}/WHEEL +0 -0
- {waldiez-0.5.3.dist-info → waldiez-0.5.5.dist-info}/entry_points.txt +0 -0
- {waldiez-0.5.3.dist-info → waldiez-0.5.5.dist-info}/licenses/LICENSE +0 -0
- {waldiez-0.5.3.dist-info → waldiez-0.5.5.dist-info}/licenses/NOTICE.md +0 -0
waldiez/running/protocol.py
CHANGED
|
@@ -7,7 +7,10 @@ from pathlib import Path
|
|
|
7
7
|
from typing import TYPE_CHECKING, Protocol, Union, runtime_checkable
|
|
8
8
|
|
|
9
9
|
if TYPE_CHECKING:
|
|
10
|
-
from autogen import
|
|
10
|
+
from autogen.io.run_response import ( # type: ignore[import-untyped]
|
|
11
|
+
AsyncRunResponseProtocol,
|
|
12
|
+
RunResponseProtocol,
|
|
13
|
+
)
|
|
11
14
|
|
|
12
15
|
|
|
13
16
|
@runtime_checkable
|
|
@@ -59,7 +62,6 @@ class WaldiezRunnerProtocol(Protocol):
|
|
|
59
62
|
output_path: str | Path | None,
|
|
60
63
|
uploads_root: str | Path | None,
|
|
61
64
|
structured_io: bool | None = None,
|
|
62
|
-
skip_patch_io: bool | None = None,
|
|
63
65
|
skip_mmd: bool = False,
|
|
64
66
|
) -> None:
|
|
65
67
|
"""Start running the Waldiez flow in a non-blocking way.
|
|
@@ -74,9 +76,6 @@ class WaldiezRunnerProtocol(Protocol):
|
|
|
74
76
|
The runtime uploads root.
|
|
75
77
|
structured_io : bool
|
|
76
78
|
Whether to use structured IO instead of the default 'input/print'.
|
|
77
|
-
skip_patch_io : bool | None
|
|
78
|
-
Whether to skip patching I/O, by default None.
|
|
79
|
-
If None, it will use the value from the context.
|
|
80
79
|
skip_mmd : bool
|
|
81
80
|
Whether to skip generating the mermaid diagram.
|
|
82
81
|
|
|
@@ -91,7 +90,6 @@ class WaldiezRunnerProtocol(Protocol):
|
|
|
91
90
|
output_path: str | Path | None,
|
|
92
91
|
uploads_root: str | Path | None,
|
|
93
92
|
structured_io: bool | None = None,
|
|
94
|
-
skip_patch_io: bool | None = None,
|
|
95
93
|
skip_mmd: bool = False,
|
|
96
94
|
) -> None:
|
|
97
95
|
"""Asynchronously start running the Waldiez flow in a non-blocking way.
|
|
@@ -106,9 +104,6 @@ class WaldiezRunnerProtocol(Protocol):
|
|
|
106
104
|
The runtime uploads root.
|
|
107
105
|
structured_io : bool
|
|
108
106
|
Whether to use structured IO instead of the default 'input/print'.
|
|
109
|
-
skip_patch_io : bool | None
|
|
110
|
-
Whether to skip patching I/O, by default None.
|
|
111
|
-
If None, it will use the value from the context.
|
|
112
107
|
skip_mmd : bool
|
|
113
108
|
Whether to skip generating the mermaid diagram.
|
|
114
109
|
|
|
@@ -123,13 +118,10 @@ class WaldiezRunnerProtocol(Protocol):
|
|
|
123
118
|
output_path: str | Path | None,
|
|
124
119
|
uploads_root: str | Path | None,
|
|
125
120
|
structured_io: bool | None = None,
|
|
126
|
-
threaded: bool | None = None,
|
|
127
|
-
skip_patch_io: bool | None = None,
|
|
128
121
|
skip_mmd: bool = False,
|
|
129
122
|
) -> Union[
|
|
130
|
-
"
|
|
131
|
-
list["
|
|
132
|
-
dict[int, "ChatResult"],
|
|
123
|
+
list["RunResponseProtocol"],
|
|
124
|
+
list["AsyncRunResponseProtocol"],
|
|
133
125
|
]: # pyright: ignore
|
|
134
126
|
"""Run the Waldiez flow in a blocking way.
|
|
135
127
|
|
|
@@ -141,20 +133,17 @@ class WaldiezRunnerProtocol(Protocol):
|
|
|
141
133
|
The runtime uploads root.
|
|
142
134
|
structured_io : bool
|
|
143
135
|
Whether to use structured IO instead of the default 'input/print'.
|
|
144
|
-
threaded : bool | None
|
|
145
|
-
Whether to run the flow in a separate thread.
|
|
146
|
-
skip_patch_io : bool
|
|
147
|
-
Whether to skip patching I/O, by default None.
|
|
148
|
-
If None, it will use the value from the context.
|
|
149
136
|
skip_mmd : bool
|
|
150
137
|
Whether to skip generating the mermaid diagram.
|
|
151
138
|
|
|
152
139
|
Returns
|
|
153
140
|
-------
|
|
154
|
-
Union[
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
141
|
+
Union[
|
|
142
|
+
list["RunResponseProtocol"],
|
|
143
|
+
list["AsyncRunResponseProtocol"],
|
|
144
|
+
]
|
|
145
|
+
The result of the run, which can be a list of RunResponseProtocol
|
|
146
|
+
or a list of AsyncRunResponseProtocol.
|
|
158
147
|
"""
|
|
159
148
|
|
|
160
149
|
async def a_run(
|
|
@@ -162,12 +151,10 @@ class WaldiezRunnerProtocol(Protocol):
|
|
|
162
151
|
output_path: str | Path | None,
|
|
163
152
|
uploads_root: str | Path | None,
|
|
164
153
|
structured_io: bool | None = None,
|
|
165
|
-
skip_patch_io: bool | None = None,
|
|
166
154
|
skip_mmd: bool = False,
|
|
167
155
|
) -> Union[
|
|
168
|
-
"
|
|
169
|
-
list["
|
|
170
|
-
dict[int, "ChatResult"],
|
|
156
|
+
list["RunResponseProtocol"],
|
|
157
|
+
list["AsyncRunResponseProtocol"],
|
|
171
158
|
]: # pyright: ignore
|
|
172
159
|
"""Run the Waldiez flow.
|
|
173
160
|
|
|
@@ -179,26 +166,24 @@ class WaldiezRunnerProtocol(Protocol):
|
|
|
179
166
|
The runtime uploads root.
|
|
180
167
|
structured_io : bool
|
|
181
168
|
Whether to use structured IO instead of the default 'input/print'.
|
|
182
|
-
skip_patch_io : bool
|
|
183
|
-
Whether to skip patching I/O, by default None.
|
|
184
|
-
If None, it will use the value from the context.
|
|
185
169
|
skip_mmd : bool
|
|
186
170
|
Whether to skip generating the mermaid diagram.
|
|
187
171
|
|
|
188
172
|
Returns
|
|
189
173
|
-------
|
|
190
|
-
Union[
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
174
|
+
Union[
|
|
175
|
+
list["RunResponseProtocol"],
|
|
176
|
+
list["AsyncRunResponseProtocol"],
|
|
177
|
+
]
|
|
178
|
+
The result of the run, which can be a list of RunResponseProtocol
|
|
179
|
+
or a list of AsyncRunResponseProtocol.
|
|
194
180
|
"""
|
|
195
181
|
|
|
196
182
|
def after_run(
|
|
197
183
|
self,
|
|
198
184
|
results: Union[
|
|
199
|
-
"
|
|
200
|
-
list["
|
|
201
|
-
dict[int, "ChatResult"],
|
|
185
|
+
list["RunResponseProtocol"],
|
|
186
|
+
list["AsyncRunResponseProtocol"],
|
|
202
187
|
],
|
|
203
188
|
output_file: Path,
|
|
204
189
|
uploads_root: Path | None,
|
|
@@ -210,10 +195,12 @@ class WaldiezRunnerProtocol(Protocol):
|
|
|
210
195
|
|
|
211
196
|
Parameters
|
|
212
197
|
----------
|
|
213
|
-
results : Union[
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
198
|
+
results : Union[
|
|
199
|
+
list["RunResponseProtocol"],
|
|
200
|
+
list["AsyncRunResponseProtocol"],
|
|
201
|
+
]
|
|
202
|
+
The results of the run, which can be a list of RunResponseProtocol
|
|
203
|
+
or a list of AsyncRunResponseProtocol.
|
|
217
204
|
output_file : Path
|
|
218
205
|
The path to the output file.
|
|
219
206
|
uploads_root : Path | None
|
|
@@ -229,9 +216,8 @@ class WaldiezRunnerProtocol(Protocol):
|
|
|
229
216
|
async def a_after_run(
|
|
230
217
|
self,
|
|
231
218
|
results: Union[
|
|
232
|
-
"
|
|
233
|
-
list["
|
|
234
|
-
dict[int, "ChatResult"],
|
|
219
|
+
list["RunResponseProtocol"],
|
|
220
|
+
list["AsyncRunResponseProtocol"],
|
|
235
221
|
],
|
|
236
222
|
output_file: Path,
|
|
237
223
|
uploads_root: Path | None,
|
|
@@ -243,10 +229,12 @@ class WaldiezRunnerProtocol(Protocol):
|
|
|
243
229
|
|
|
244
230
|
Parameters
|
|
245
231
|
----------
|
|
246
|
-
results : Union[
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
232
|
+
results : Union[
|
|
233
|
+
list["RunResponseProtocol"],
|
|
234
|
+
list["AsyncRunResponseProtocol"]
|
|
235
|
+
]
|
|
236
|
+
The results of the run, which can be a list of RunResponseProtocol
|
|
237
|
+
or a list of AsyncRunResponseProtocol.
|
|
250
238
|
output_file : Path
|
|
251
239
|
The path to the output file.
|
|
252
240
|
uploads_root : Path | None
|
waldiez/running/run_results.py
CHANGED
|
@@ -6,17 +6,17 @@
|
|
|
6
6
|
from typing import TYPE_CHECKING, TypedDict, Union
|
|
7
7
|
|
|
8
8
|
if TYPE_CHECKING:
|
|
9
|
-
from autogen import
|
|
9
|
+
from autogen.io.run_response import ( # type: ignore[import-untyped]
|
|
10
|
+
AsyncRunResponseProtocol,
|
|
11
|
+
RunResponseProtocol,
|
|
12
|
+
)
|
|
10
13
|
|
|
11
14
|
|
|
12
15
|
class WaldiezRunResults(TypedDict):
|
|
13
16
|
"""Results of the Waldiez run."""
|
|
14
17
|
|
|
15
18
|
results: Union[
|
|
16
|
-
"
|
|
17
|
-
list["ChatResult"],
|
|
18
|
-
dict[int, "ChatResult"],
|
|
19
|
-
None,
|
|
19
|
+
list["RunResponseProtocol"], list["AsyncRunResponseProtocol"]
|
|
20
20
|
]
|
|
21
21
|
exception: Exception | None
|
|
22
22
|
completed: bool
|
|
@@ -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
|
+
}
|