waldiez 0.5.8__py3-none-any.whl → 0.5.10__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 +112 -24
- waldiez/exporting/agent/exporter.py +3 -0
- waldiez/exporting/agent/extras/captain_agent_extras.py +44 -7
- waldiez/exporting/agent/extras/handoffs/condition.py +3 -1
- waldiez/exporting/chats/utils/common.py +25 -23
- waldiez/exporting/core/__init__.py +0 -2
- waldiez/exporting/core/context.py +13 -13
- waldiez/exporting/core/protocols.py +0 -141
- waldiez/exporting/core/result.py +5 -5
- waldiez/exporting/flow/merger.py +2 -2
- waldiez/exporting/flow/orchestrator.py +1 -0
- waldiez/exporting/flow/utils/common.py +2 -2
- waldiez/exporting/flow/utils/importing.py +1 -0
- waldiez/exporting/flow/utils/logging.py +6 -7
- waldiez/exporting/tools/exporter.py +5 -0
- waldiez/exporting/tools/factory.py +4 -0
- waldiez/exporting/tools/processor.py +5 -1
- waldiez/io/_ws.py +13 -5
- waldiez/io/models/content/image.py +1 -0
- waldiez/io/models/user_input.py +4 -4
- waldiez/io/models/user_response.py +1 -0
- waldiez/io/mqtt.py +1 -1
- waldiez/io/structured.py +17 -17
- waldiez/io/utils.py +1 -1
- waldiez/io/ws.py +9 -11
- waldiez/logger.py +180 -63
- waldiez/models/agents/agent/update_system_message.py +0 -2
- waldiez/models/agents/doc_agent/doc_agent.py +8 -1
- waldiez/models/common/dict_utils.py +169 -40
- waldiez/models/flow/flow.py +6 -6
- waldiez/models/flow/info.py +5 -1
- waldiez/models/model/_llm.py +28 -14
- waldiez/models/model/model.py +4 -1
- waldiez/models/model/model_data.py +18 -5
- waldiez/models/tool/predefined/_config.py +5 -1
- waldiez/models/tool/predefined/_duckduckgo.py +4 -0
- waldiez/models/tool/predefined/_email.py +474 -0
- waldiez/models/tool/predefined/_google.py +8 -6
- waldiez/models/tool/predefined/_perplexity.py +3 -0
- waldiez/models/tool/predefined/_searxng.py +3 -0
- waldiez/models/tool/predefined/_tavily.py +4 -1
- waldiez/models/tool/predefined/_wikipedia.py +4 -1
- waldiez/models/tool/predefined/_youtube.py +4 -1
- waldiez/models/tool/predefined/protocol.py +3 -0
- waldiez/models/tool/tool.py +22 -4
- waldiez/models/waldiez.py +12 -0
- waldiez/runner.py +37 -54
- waldiez/running/__init__.py +6 -0
- waldiez/running/base_runner.py +310 -353
- waldiez/running/environment.py +1 -0
- waldiez/running/exceptions.py +9 -0
- waldiez/running/post_run.py +4 -4
- waldiez/running/pre_run.py +51 -40
- waldiez/running/protocol.py +21 -101
- waldiez/running/run_results.py +1 -1
- waldiez/running/standard_runner.py +84 -277
- waldiez/running/step_by_step/__init__.py +46 -0
- waldiez/running/step_by_step/breakpoints_mixin.py +188 -0
- waldiez/running/step_by_step/step_by_step_models.py +224 -0
- waldiez/running/step_by_step/step_by_step_runner.py +745 -0
- waldiez/running/subprocess_runner/__base__.py +282 -0
- waldiez/running/subprocess_runner/__init__.py +16 -0
- waldiez/running/subprocess_runner/_async_runner.py +362 -0
- waldiez/running/subprocess_runner/_sync_runner.py +455 -0
- waldiez/running/subprocess_runner/runner.py +561 -0
- waldiez/running/timeline_processor.py +1 -1
- waldiez/running/utils.py +376 -1
- waldiez/utils/version.py +2 -6
- waldiez/ws/__init__.py +70 -0
- waldiez/ws/__main__.py +15 -0
- waldiez/ws/_file_handler.py +201 -0
- waldiez/ws/cli.py +211 -0
- waldiez/ws/client_manager.py +835 -0
- waldiez/ws/errors.py +416 -0
- waldiez/ws/models.py +971 -0
- waldiez/ws/reloader.py +342 -0
- waldiez/ws/server.py +469 -0
- waldiez/ws/session_manager.py +393 -0
- waldiez/ws/session_stats.py +83 -0
- waldiez/ws/utils.py +385 -0
- {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/METADATA +74 -74
- {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/RECORD +87 -65
- waldiez/running/patch_io_stream.py +0 -210
- {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/WHEEL +0 -0
- {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/entry_points.txt +0 -0
- {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/licenses/LICENSE +0 -0
- {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/licenses/NOTICE.md +0 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0.
|
|
2
|
+
# Copyright (c) 2024 - 2025 Waldiez and contributors.
|
|
3
|
+
# pylint: disable=no-self-use,unused-argument
|
|
4
|
+
# flake8: noqa: G004
|
|
5
|
+
"""Base subprocess runner - shared functionality."""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import shlex
|
|
10
|
+
import sys
|
|
11
|
+
import uuid
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Literal
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BaseSubprocessRunner:
|
|
17
|
+
"""Base class with common logic for subprocess runners."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
session_id: str | None = None,
|
|
22
|
+
input_timeout: float = 120.0,
|
|
23
|
+
uploads_root: str | Path | None = None,
|
|
24
|
+
dot_env: str | Path | None = None,
|
|
25
|
+
logger: logging.Logger | None = None,
|
|
26
|
+
**kwargs: Any,
|
|
27
|
+
) -> None:
|
|
28
|
+
"""Initialize base subprocess runner.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
session_id : str | None
|
|
33
|
+
Unique session identifier
|
|
34
|
+
input_timeout : float
|
|
35
|
+
Timeout for user input
|
|
36
|
+
output_path : str | Path | None
|
|
37
|
+
Path to output file
|
|
38
|
+
uploads_root : str | Path | None
|
|
39
|
+
Root directory for uploads
|
|
40
|
+
dot_env : str | Path | None
|
|
41
|
+
Path to .env file
|
|
42
|
+
logger : logging.Logger | None
|
|
43
|
+
Logger instance to use
|
|
44
|
+
**kwargs : Any
|
|
45
|
+
Additional arguments
|
|
46
|
+
"""
|
|
47
|
+
self.session_id = session_id or f"session_{uuid.uuid4().hex}"
|
|
48
|
+
self.input_timeout = input_timeout
|
|
49
|
+
self.uploads_root = uploads_root
|
|
50
|
+
self.dot_env = dot_env
|
|
51
|
+
self.logger = logger or logging.getLogger(self.__class__.__name__)
|
|
52
|
+
self.waiting_for_input = False
|
|
53
|
+
|
|
54
|
+
def build_command(
|
|
55
|
+
self,
|
|
56
|
+
flow_path: Path,
|
|
57
|
+
output_path: str | Path | None = None,
|
|
58
|
+
mode: Literal["debug", "run"] = "run",
|
|
59
|
+
structured: bool = True,
|
|
60
|
+
force: bool = True,
|
|
61
|
+
) -> list[str]:
|
|
62
|
+
"""Build subprocess command.
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
flow_path : Path
|
|
67
|
+
Path to the waldiez flow file
|
|
68
|
+
output_path : str | Path | None
|
|
69
|
+
Path to the output file
|
|
70
|
+
mode : Literal["debug", "run"]
|
|
71
|
+
Execution mode ('debug', 'run', etc.)
|
|
72
|
+
structured : bool
|
|
73
|
+
Whether to use structured I/O
|
|
74
|
+
force : bool
|
|
75
|
+
Whether to force overwrite outputs
|
|
76
|
+
|
|
77
|
+
Returns
|
|
78
|
+
-------
|
|
79
|
+
list[str]
|
|
80
|
+
Command arguments list
|
|
81
|
+
"""
|
|
82
|
+
_output_path = (
|
|
83
|
+
str(output_path)
|
|
84
|
+
if output_path
|
|
85
|
+
else str(flow_path.with_suffix(".py"))
|
|
86
|
+
)
|
|
87
|
+
cmd = [
|
|
88
|
+
sys.executable,
|
|
89
|
+
"-m",
|
|
90
|
+
"waldiez",
|
|
91
|
+
"run",
|
|
92
|
+
"--file",
|
|
93
|
+
str(flow_path),
|
|
94
|
+
"--output",
|
|
95
|
+
_output_path,
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
if mode == "debug":
|
|
99
|
+
cmd.append("--step")
|
|
100
|
+
|
|
101
|
+
if structured:
|
|
102
|
+
cmd.append("--structured")
|
|
103
|
+
|
|
104
|
+
if force:
|
|
105
|
+
cmd.append("--force")
|
|
106
|
+
|
|
107
|
+
if self.uploads_root:
|
|
108
|
+
cmd.extend(["--uploads-root", str(self.uploads_root)])
|
|
109
|
+
|
|
110
|
+
if self.dot_env:
|
|
111
|
+
cmd.extend(["--dot-env", str(self.dot_env)])
|
|
112
|
+
|
|
113
|
+
return cmd
|
|
114
|
+
|
|
115
|
+
def parse_output(
|
|
116
|
+
self,
|
|
117
|
+
line: str,
|
|
118
|
+
stream: Literal["stdout", "stderr"],
|
|
119
|
+
) -> dict[str, Any]:
|
|
120
|
+
"""Parse output from subprocess.
|
|
121
|
+
|
|
122
|
+
Parameters
|
|
123
|
+
----------
|
|
124
|
+
line : str
|
|
125
|
+
Raw output line from subprocess
|
|
126
|
+
stream : Literal["stdout", "stderr"]
|
|
127
|
+
Stream name ('stdout' or 'stderr')
|
|
128
|
+
|
|
129
|
+
Returns
|
|
130
|
+
-------
|
|
131
|
+
dict[str, Any]
|
|
132
|
+
Parsed JSON data if valid, empty dict otherwise
|
|
133
|
+
"""
|
|
134
|
+
line = line.strip()
|
|
135
|
+
if not line:
|
|
136
|
+
return {}
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
data = json.loads(line)
|
|
140
|
+
if isinstance(data, dict):
|
|
141
|
+
return data # pyright: ignore
|
|
142
|
+
except json.JSONDecodeError:
|
|
143
|
+
return self.create_output_message(stream=stream, content=line)
|
|
144
|
+
return self.create_output_message(
|
|
145
|
+
stream=stream, content=line
|
|
146
|
+
) # pragma: no cover
|
|
147
|
+
|
|
148
|
+
# noinspection PyMethodMayBeStatic
|
|
149
|
+
def decode_subprocess_line(self, line: Any) -> str:
|
|
150
|
+
"""Safely decode subprocess output line.
|
|
151
|
+
|
|
152
|
+
Parameters
|
|
153
|
+
----------
|
|
154
|
+
line : Any
|
|
155
|
+
Raw bytes from subprocess
|
|
156
|
+
|
|
157
|
+
Returns
|
|
158
|
+
-------
|
|
159
|
+
str
|
|
160
|
+
Decoded string
|
|
161
|
+
"""
|
|
162
|
+
if isinstance(line, bytes):
|
|
163
|
+
return line.decode("utf-8", errors="replace").strip()
|
|
164
|
+
if isinstance(line, str):
|
|
165
|
+
return line.strip()
|
|
166
|
+
return str(line).strip()
|
|
167
|
+
|
|
168
|
+
def log_subprocess_start(self, cmd: list[str]) -> None:
|
|
169
|
+
"""Log subprocess start with command.
|
|
170
|
+
|
|
171
|
+
Parameters
|
|
172
|
+
----------
|
|
173
|
+
cmd : list[str]
|
|
174
|
+
Command arguments
|
|
175
|
+
"""
|
|
176
|
+
cmd_str = " ".join(shlex.quote(arg) for arg in cmd)
|
|
177
|
+
self.logger.info(f"Starting subprocess: {cmd_str}")
|
|
178
|
+
|
|
179
|
+
def log_subprocess_end(self, exit_code: int) -> None:
|
|
180
|
+
"""Log subprocess completion.
|
|
181
|
+
|
|
182
|
+
Parameters
|
|
183
|
+
----------
|
|
184
|
+
exit_code : int
|
|
185
|
+
Process exit code
|
|
186
|
+
"""
|
|
187
|
+
if exit_code == 0:
|
|
188
|
+
self.logger.info(
|
|
189
|
+
f"Subprocess completed successfully (exit code: {exit_code})"
|
|
190
|
+
)
|
|
191
|
+
else:
|
|
192
|
+
self.logger.error(f"Subprocess failed (exit code: {exit_code})")
|
|
193
|
+
|
|
194
|
+
# noinspection PyMethodMayBeStatic
|
|
195
|
+
def create_input_response(
|
|
196
|
+
self,
|
|
197
|
+
response_type: str,
|
|
198
|
+
user_input: str,
|
|
199
|
+
request_id: str | None = None,
|
|
200
|
+
) -> str:
|
|
201
|
+
"""Create input response for subprocess.
|
|
202
|
+
|
|
203
|
+
Parameters
|
|
204
|
+
----------
|
|
205
|
+
response_type : str
|
|
206
|
+
Type of the response
|
|
207
|
+
user_input : str
|
|
208
|
+
User's input response
|
|
209
|
+
request_id : str | None
|
|
210
|
+
Request ID from the input request
|
|
211
|
+
|
|
212
|
+
Returns
|
|
213
|
+
-------
|
|
214
|
+
str
|
|
215
|
+
JSON-formatted response
|
|
216
|
+
"""
|
|
217
|
+
response = {
|
|
218
|
+
"type": response_type,
|
|
219
|
+
"data": user_input,
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if request_id:
|
|
223
|
+
response["request_id"] = request_id
|
|
224
|
+
|
|
225
|
+
return json.dumps(response) + "\n"
|
|
226
|
+
|
|
227
|
+
def create_output_message(
|
|
228
|
+
self, content: str, stream: str = "stdout", msg_type: str = "output"
|
|
229
|
+
) -> dict[str, Any]:
|
|
230
|
+
"""Create standardized output message.
|
|
231
|
+
|
|
232
|
+
Parameters
|
|
233
|
+
----------
|
|
234
|
+
content : str
|
|
235
|
+
Message content
|
|
236
|
+
stream : str
|
|
237
|
+
Source stream ('stdout', 'stderr')
|
|
238
|
+
msg_type : str
|
|
239
|
+
Message type ('output', 'error', etc.)
|
|
240
|
+
|
|
241
|
+
Returns
|
|
242
|
+
-------
|
|
243
|
+
dict[str, Any]
|
|
244
|
+
The output message
|
|
245
|
+
"""
|
|
246
|
+
return {
|
|
247
|
+
"type": "subprocess_output",
|
|
248
|
+
"session_id": self.session_id,
|
|
249
|
+
"stream": stream,
|
|
250
|
+
"content": content,
|
|
251
|
+
"subprocess_type": msg_type,
|
|
252
|
+
"context": {}, # might add in the future
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
def create_completion_message(
|
|
256
|
+
self, success: bool, exit_code: int, message: str | None = None
|
|
257
|
+
) -> dict[str, Any]:
|
|
258
|
+
"""Create completion message.
|
|
259
|
+
|
|
260
|
+
Parameters
|
|
261
|
+
----------
|
|
262
|
+
success : bool
|
|
263
|
+
Whether subprocess completed successfully
|
|
264
|
+
exit_code : int
|
|
265
|
+
Process exit code
|
|
266
|
+
message : str | None
|
|
267
|
+
Additional message
|
|
268
|
+
|
|
269
|
+
Returns
|
|
270
|
+
-------
|
|
271
|
+
dict
|
|
272
|
+
Completion message
|
|
273
|
+
"""
|
|
274
|
+
data = message or f"Process completed with exit code {exit_code}"
|
|
275
|
+
return {
|
|
276
|
+
"type": "subprocess_completion",
|
|
277
|
+
"session_id": self.session_id,
|
|
278
|
+
"success": success,
|
|
279
|
+
"exit_code": exit_code,
|
|
280
|
+
"message": data,
|
|
281
|
+
"context": {}, # might add in the future
|
|
282
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0.
|
|
2
|
+
# Copyright (c) 2024 - 2025 Waldiez and contributors.
|
|
3
|
+
|
|
4
|
+
"""Subprocess based runner."""
|
|
5
|
+
|
|
6
|
+
from .__base__ import BaseSubprocessRunner
|
|
7
|
+
from ._async_runner import AsyncSubprocessRunner
|
|
8
|
+
from ._sync_runner import SyncSubprocessRunner
|
|
9
|
+
from .runner import WaldiezSubprocessRunner
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"SyncSubprocessRunner",
|
|
13
|
+
"AsyncSubprocessRunner",
|
|
14
|
+
"BaseSubprocessRunner",
|
|
15
|
+
"WaldiezSubprocessRunner",
|
|
16
|
+
]
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0.
|
|
2
|
+
# Copyright (c) 2024 - 2025 Waldiez and contributors.
|
|
3
|
+
# pylint: disable=no-self-use,unused-argument,logging-fstring-interpolation
|
|
4
|
+
# pylint: disable=too-many-try-statements,broad-exception-caught,duplicate-code
|
|
5
|
+
# flake8: noqa: G004
|
|
6
|
+
"""Async subprocess runner for Waldiez workflows."""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
# noinspection PyProtectedMember
|
|
12
|
+
from asyncio.subprocess import Process as AsyncProcess
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Callable, Coroutine, Literal, Optional
|
|
15
|
+
|
|
16
|
+
from .__base__ import BaseSubprocessRunner
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AsyncSubprocessRunner(BaseSubprocessRunner):
|
|
20
|
+
"""Async subprocess runner for Waldiez workflows."""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
on_output: Callable[[dict[str, Any]], Coroutine[Any, Any, None]],
|
|
25
|
+
on_input_request: Callable[[str], Coroutine[Any, Any, None]],
|
|
26
|
+
input_timeout: float = 120.0,
|
|
27
|
+
uploads_root: str | Path | None = None,
|
|
28
|
+
dot_env: str | Path | None = None,
|
|
29
|
+
logger: logging.Logger | None = None,
|
|
30
|
+
**kwargs: Any,
|
|
31
|
+
) -> None:
|
|
32
|
+
"""Initialize async subprocess runner.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
on_output : Callable[[dict[str, Any]], Coroutine[Any, Any, None]]
|
|
37
|
+
Callback for handling output messages
|
|
38
|
+
on_input_request : Callable[[str], Coroutine[Any, Any, None]]
|
|
39
|
+
Callback for handling input requests
|
|
40
|
+
input_timeout : float
|
|
41
|
+
Timeout for user input
|
|
42
|
+
uploads_root : str | Path | None
|
|
43
|
+
Root directory for uploads
|
|
44
|
+
dot_env : str | Path | None
|
|
45
|
+
Path to .env file
|
|
46
|
+
logger : logging.Logger | None
|
|
47
|
+
Logger instance
|
|
48
|
+
**kwargs : Any
|
|
49
|
+
Additional arguments passed to base class
|
|
50
|
+
"""
|
|
51
|
+
super().__init__(
|
|
52
|
+
input_timeout=input_timeout,
|
|
53
|
+
uploads_root=uploads_root,
|
|
54
|
+
dot_env=dot_env,
|
|
55
|
+
logger=logger,
|
|
56
|
+
**kwargs,
|
|
57
|
+
)
|
|
58
|
+
self.on_output = on_output
|
|
59
|
+
self.on_input_request = on_input_request
|
|
60
|
+
self.process: Optional[AsyncProcess] = None
|
|
61
|
+
self.input_queue: asyncio.Queue[str] = asyncio.Queue()
|
|
62
|
+
self.output_queue: asyncio.Queue[dict[str, Any]] = asyncio.Queue()
|
|
63
|
+
self._monitor_tasks: list[asyncio.Task[Any]] = []
|
|
64
|
+
|
|
65
|
+
async def run_subprocess(
|
|
66
|
+
self,
|
|
67
|
+
flow_path: Path,
|
|
68
|
+
mode: Literal["debug", "run"] = "debug",
|
|
69
|
+
) -> bool:
|
|
70
|
+
"""Run subprocess with the given flow data.
|
|
71
|
+
|
|
72
|
+
Parameters
|
|
73
|
+
----------
|
|
74
|
+
flow_path : Path
|
|
75
|
+
Path to the waldiez flow file
|
|
76
|
+
mode : Literal["debug", "run"]
|
|
77
|
+
Execution mode ('debug', 'run')
|
|
78
|
+
|
|
79
|
+
Returns
|
|
80
|
+
-------
|
|
81
|
+
bool
|
|
82
|
+
True if subprocess completed successfully, False otherwise
|
|
83
|
+
"""
|
|
84
|
+
try:
|
|
85
|
+
# Prepare flow file
|
|
86
|
+
# flow_path = self.prepare_flow_file(flow_data, base_dir, filename)
|
|
87
|
+
|
|
88
|
+
# Build command
|
|
89
|
+
cmd = self.build_command(flow_path, mode=mode)
|
|
90
|
+
self.log_subprocess_start(cmd)
|
|
91
|
+
|
|
92
|
+
# Start subprocess
|
|
93
|
+
self.process = await asyncio.create_subprocess_exec(
|
|
94
|
+
*cmd,
|
|
95
|
+
stdin=asyncio.subprocess.PIPE,
|
|
96
|
+
stdout=asyncio.subprocess.PIPE,
|
|
97
|
+
stderr=asyncio.subprocess.PIPE,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Start monitoring tasks
|
|
101
|
+
await self._start_monitoring()
|
|
102
|
+
|
|
103
|
+
# Wait for completion
|
|
104
|
+
exit_code = await self.process.wait()
|
|
105
|
+
self.log_subprocess_end(exit_code)
|
|
106
|
+
|
|
107
|
+
# Send completion message
|
|
108
|
+
completion_msg = self.create_completion_message(
|
|
109
|
+
success=exit_code == 0, exit_code=exit_code
|
|
110
|
+
)
|
|
111
|
+
await self.on_output(completion_msg)
|
|
112
|
+
|
|
113
|
+
return exit_code == 0
|
|
114
|
+
|
|
115
|
+
except Exception as e:
|
|
116
|
+
self.logger.error(f"Error running subprocess: {e}")
|
|
117
|
+
error_msg = self.create_output_message(
|
|
118
|
+
f"Subprocess error: {str(e)}",
|
|
119
|
+
stream="stderr",
|
|
120
|
+
msg_type="error",
|
|
121
|
+
)
|
|
122
|
+
await self.on_output(error_msg)
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
finally:
|
|
126
|
+
await self._cleanup()
|
|
127
|
+
|
|
128
|
+
async def provide_user_input(self, user_input: str) -> None:
|
|
129
|
+
"""Provide user input response.
|
|
130
|
+
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
user_input : str
|
|
134
|
+
User's input response
|
|
135
|
+
"""
|
|
136
|
+
if self.waiting_for_input:
|
|
137
|
+
try:
|
|
138
|
+
await self.input_queue.put(user_input)
|
|
139
|
+
self.logger.debug(f"User input queued: {user_input}")
|
|
140
|
+
except Exception as e:
|
|
141
|
+
self.logger.error(f"Error queuing user input: {e}")
|
|
142
|
+
|
|
143
|
+
async def stop(self) -> None:
|
|
144
|
+
"""Stop the subprocess."""
|
|
145
|
+
await self._cleanup()
|
|
146
|
+
if self.process:
|
|
147
|
+
try:
|
|
148
|
+
self.logger.info("Terminating subprocess")
|
|
149
|
+
self.process.terminate()
|
|
150
|
+
|
|
151
|
+
# Wait a bit for graceful termination
|
|
152
|
+
try:
|
|
153
|
+
await asyncio.wait_for(self.process.wait(), timeout=5.0)
|
|
154
|
+
except asyncio.TimeoutError:
|
|
155
|
+
# Force kill if it doesn't terminate gracefully
|
|
156
|
+
self.logger.warning("Force killing subprocess")
|
|
157
|
+
self.process.kill()
|
|
158
|
+
await self.process.wait()
|
|
159
|
+
|
|
160
|
+
except Exception as e:
|
|
161
|
+
self.logger.error(f"Error stopping subprocess: {e}")
|
|
162
|
+
self.process = None
|
|
163
|
+
|
|
164
|
+
async def _start_monitoring(self) -> None:
|
|
165
|
+
"""Start monitoring tasks for subprocess I/O."""
|
|
166
|
+
if not self.process:
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
# Create monitoring tasks
|
|
170
|
+
self._monitor_tasks = [
|
|
171
|
+
asyncio.create_task(self._read_stdout()),
|
|
172
|
+
asyncio.create_task(self._read_stderr()),
|
|
173
|
+
asyncio.create_task(self._send_queued_messages()),
|
|
174
|
+
]
|
|
175
|
+
|
|
176
|
+
# Wait for all monitoring tasks to complete
|
|
177
|
+
await asyncio.gather(*self._monitor_tasks, return_exceptions=True)
|
|
178
|
+
|
|
179
|
+
async def _read_stdout(self) -> None:
|
|
180
|
+
"""Read and handle stdout from subprocess."""
|
|
181
|
+
if not self.process or not self.process.stdout:
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
while self.process.returncode is None: # pragma: no branch
|
|
186
|
+
try:
|
|
187
|
+
line = await asyncio.wait_for(
|
|
188
|
+
self.process.stdout.readline(), timeout=1.0
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if not line:
|
|
192
|
+
break
|
|
193
|
+
|
|
194
|
+
line_str = self.decode_subprocess_line(line)
|
|
195
|
+
if line_str:
|
|
196
|
+
await self._handle_stdout_line(line_str)
|
|
197
|
+
|
|
198
|
+
except asyncio.TimeoutError:
|
|
199
|
+
continue
|
|
200
|
+
except Exception as e:
|
|
201
|
+
self.logger.error(f"Error reading stdout: {e}")
|
|
202
|
+
break
|
|
203
|
+
|
|
204
|
+
except Exception as e:
|
|
205
|
+
self.logger.error(f"Error in stdout reader: {e}")
|
|
206
|
+
|
|
207
|
+
async def _read_stderr(self) -> None:
|
|
208
|
+
"""Read and handle stderr from subprocess."""
|
|
209
|
+
if not self.process or not self.process.stderr:
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
try:
|
|
213
|
+
while self.process.returncode is None: # pragma: no branch
|
|
214
|
+
try:
|
|
215
|
+
line = await asyncio.wait_for(
|
|
216
|
+
self.process.stderr.readline(), timeout=1.0
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
if not line:
|
|
220
|
+
break
|
|
221
|
+
|
|
222
|
+
line_str = self.decode_subprocess_line(line)
|
|
223
|
+
if line_str:
|
|
224
|
+
error_msg = self.create_output_message(
|
|
225
|
+
line_str, stream="stderr", msg_type="error"
|
|
226
|
+
)
|
|
227
|
+
await self.output_queue.put(error_msg)
|
|
228
|
+
|
|
229
|
+
except asyncio.TimeoutError:
|
|
230
|
+
continue
|
|
231
|
+
except Exception as e:
|
|
232
|
+
self.logger.error(f"Error reading stderr: {e}")
|
|
233
|
+
break
|
|
234
|
+
|
|
235
|
+
except Exception as e:
|
|
236
|
+
self.logger.error(f"Error in stderr reader: {e}")
|
|
237
|
+
|
|
238
|
+
async def _send_queued_messages(self) -> None:
|
|
239
|
+
"""Send queued messages to output callback."""
|
|
240
|
+
try:
|
|
241
|
+
while self.process and self.process.returncode is None:
|
|
242
|
+
try:
|
|
243
|
+
message = await asyncio.wait_for(
|
|
244
|
+
self.output_queue.get(), timeout=1.0
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
try:
|
|
248
|
+
await self.on_output(message)
|
|
249
|
+
except Exception as e:
|
|
250
|
+
self.logger.error(f"Error in output callback: {e}")
|
|
251
|
+
|
|
252
|
+
except asyncio.TimeoutError:
|
|
253
|
+
continue
|
|
254
|
+
except Exception as e:
|
|
255
|
+
self.logger.error(f"Error sending queued message: {e}")
|
|
256
|
+
break
|
|
257
|
+
|
|
258
|
+
except Exception as e:
|
|
259
|
+
self.logger.error(f"Error in message sender: {e}")
|
|
260
|
+
|
|
261
|
+
async def _handle_stdout_line(self, line: str) -> None:
|
|
262
|
+
"""Handle a line from stdout.
|
|
263
|
+
|
|
264
|
+
Parameters
|
|
265
|
+
----------
|
|
266
|
+
line : str
|
|
267
|
+
Decoded line from stdout
|
|
268
|
+
"""
|
|
269
|
+
# Try to parse as structured JSON first
|
|
270
|
+
parsed_data = self.parse_output(line, stream="stdout")
|
|
271
|
+
if not parsed_data:
|
|
272
|
+
return
|
|
273
|
+
if parsed_data.get("type") in ("input_request", "debug_input_request"):
|
|
274
|
+
self.waiting_for_input = True
|
|
275
|
+
await self.on_input_request(parsed_data.get("prompt", "> "))
|
|
276
|
+
await self._handle_input_request(parsed_data)
|
|
277
|
+
else:
|
|
278
|
+
try:
|
|
279
|
+
await self.output_queue.put(parsed_data)
|
|
280
|
+
except Exception as e:
|
|
281
|
+
self.logger.error(f"Error queuing output message: {e}")
|
|
282
|
+
|
|
283
|
+
async def _handle_input_request(self, data: dict[str, Any]) -> None:
|
|
284
|
+
"""Handle input request from subprocess.
|
|
285
|
+
|
|
286
|
+
Parameters
|
|
287
|
+
----------
|
|
288
|
+
data : dict[str, Any]
|
|
289
|
+
Input request data
|
|
290
|
+
"""
|
|
291
|
+
response_type = str(data.get("type", "input_request")).replace(
|
|
292
|
+
"request", "response"
|
|
293
|
+
)
|
|
294
|
+
request_id = data.get("request_id")
|
|
295
|
+
try:
|
|
296
|
+
# Wait for user response
|
|
297
|
+
user_input = await asyncio.wait_for(
|
|
298
|
+
self.input_queue.get(),
|
|
299
|
+
timeout=self.input_timeout,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# Create response
|
|
303
|
+
response = self.create_input_response(
|
|
304
|
+
response_type=response_type,
|
|
305
|
+
user_input=user_input,
|
|
306
|
+
request_id=request_id,
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
# Send response to subprocess
|
|
310
|
+
if self.process and self.process.stdin:
|
|
311
|
+
self.process.stdin.write(response.encode())
|
|
312
|
+
await self.process.stdin.drain()
|
|
313
|
+
|
|
314
|
+
self.logger.debug(f"Sent input response: {user_input}")
|
|
315
|
+
|
|
316
|
+
except asyncio.TimeoutError:
|
|
317
|
+
self.logger.warning("Input request timed out")
|
|
318
|
+
# Send empty response on timeout
|
|
319
|
+
if self.process and self.process.stdin:
|
|
320
|
+
response = self.create_input_response(
|
|
321
|
+
response_type=response_type,
|
|
322
|
+
user_input="",
|
|
323
|
+
request_id=request_id,
|
|
324
|
+
)
|
|
325
|
+
self.process.stdin.write(response.encode())
|
|
326
|
+
await self.process.stdin.drain()
|
|
327
|
+
|
|
328
|
+
except Exception as e:
|
|
329
|
+
self.logger.error(f"Error handling input request: {e}")
|
|
330
|
+
|
|
331
|
+
finally:
|
|
332
|
+
self.waiting_for_input = False
|
|
333
|
+
|
|
334
|
+
async def _cleanup(self) -> None:
|
|
335
|
+
"""Cleanup resources."""
|
|
336
|
+
# Cancel monitoring tasks
|
|
337
|
+
for task in self._monitor_tasks:
|
|
338
|
+
if not task.done():
|
|
339
|
+
task.cancel()
|
|
340
|
+
|
|
341
|
+
# Wait for tasks to complete
|
|
342
|
+
if self._monitor_tasks:
|
|
343
|
+
await asyncio.gather(*self._monitor_tasks, return_exceptions=True)
|
|
344
|
+
|
|
345
|
+
self._monitor_tasks.clear()
|
|
346
|
+
|
|
347
|
+
# Close process streams
|
|
348
|
+
if self.process:
|
|
349
|
+
if self.process.stdin and not self.process.stdin.is_closing():
|
|
350
|
+
self.process.stdin.close()
|
|
351
|
+
await self.process.stdin.wait_closed()
|
|
352
|
+
self.waiting_for_input = False
|
|
353
|
+
|
|
354
|
+
def is_running(self) -> bool:
|
|
355
|
+
"""Check if subprocess is currently running.
|
|
356
|
+
|
|
357
|
+
Returns
|
|
358
|
+
-------
|
|
359
|
+
bool
|
|
360
|
+
True if subprocess is running, False otherwise
|
|
361
|
+
"""
|
|
362
|
+
return self.process is not None and self.process.returncode is None
|