ag2 0.9.5__py3-none-any.whl → 0.9.6__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 ag2 might be problematic. Click here for more details.
- {ag2-0.9.5.dist-info → ag2-0.9.6.dist-info}/METADATA +1 -1
- {ag2-0.9.5.dist-info → ag2-0.9.6.dist-info}/RECORD +19 -11
- autogen/coding/docker_commandline_code_executor.py +29 -9
- autogen/environments/__init__.py +10 -0
- autogen/environments/docker_python_environment.py +375 -0
- autogen/environments/python_environment.py +134 -0
- autogen/environments/system_python_environment.py +86 -0
- autogen/environments/venv_python_environment.py +224 -0
- autogen/environments/working_directory.py +75 -0
- autogen/llm_config.py +6 -3
- autogen/mcp/mcp_proxy/mcp_proxy.py +4 -4
- autogen/oai/client.py +28 -7
- autogen/tools/experimental/__init__.py +2 -0
- autogen/tools/experimental/code_execution/__init__.py +7 -0
- autogen/tools/experimental/code_execution/python_code_execution.py +88 -0
- autogen/version.py +1 -1
- {ag2-0.9.5.dist-info → ag2-0.9.6.dist-info}/WHEEL +0 -0
- {ag2-0.9.5.dist-info → ag2-0.9.6.dist-info}/licenses/LICENSE +0 -0
- {ag2-0.9.5.dist-info → ag2-0.9.6.dist-info}/licenses/NOTICE.md +0 -0
|
@@ -8,13 +8,13 @@ autogen/function_utils.py,sha256=YnzwNFA49Jbbe4riAY1sinYcKphg5lrHFCXx0POdYbw,481
|
|
|
8
8
|
autogen/graph_utils.py,sha256=2dfGUHZCCF629vh0vMK9WMXIX-Zi-if9NbdC0KFcuw4,7904
|
|
9
9
|
autogen/import_utils.py,sha256=byPlexRvu1bkGzEAtwwtJ2SQIm1IzModK9B5ONGkqJw,17634
|
|
10
10
|
autogen/json_utils.py,sha256=_61k__3SS1-ffE2K5Mm9ZGHQJBRUbOL9kHGQGTAWRZk,1378
|
|
11
|
-
autogen/llm_config.py,sha256=
|
|
11
|
+
autogen/llm_config.py,sha256=lQPV3zXbLKL_7rJHi1fJAJn1NpNK7wZtqGUxHeapIZQ,14437
|
|
12
12
|
autogen/math_utils.py,sha256=Ew9-I5chny9eAOnphGxKf6XCeW6Pepz5S-5touWrbYU,9546
|
|
13
13
|
autogen/retrieve_utils.py,sha256=R3Yp5d8dH4o9ayLZrGn4rCjIaY4glOHIiyQjwClmdi8,20087
|
|
14
14
|
autogen/runtime_logging.py,sha256=yCmZODvwqYR91m8lX3Q4SoPcY-DK48NF4m56CP6Om3c,4692
|
|
15
15
|
autogen/token_count_utils.py,sha256=n4wTFVNHwrfjZkrErFr8kNig2K-YCGgMLWsjDRS9D6g,10797
|
|
16
16
|
autogen/types.py,sha256=qu-7eywhakW2AxQ5lYisLLeIg45UoOW-b3ErIuyRTuw,1000
|
|
17
|
-
autogen/version.py,sha256=
|
|
17
|
+
autogen/version.py,sha256=Gx2IBExx_OQsL98JzXZ9G56t2UljGjVfvrmaV4UzVp4,193
|
|
18
18
|
autogen/_website/__init__.py,sha256=c8B9TpO07x9neD0zsJWj6AaEdlcP-WvxrvVOGWLtamk,143
|
|
19
19
|
autogen/_website/generate_api_references.py,sha256=yKqyeSP_NE27wwLYWsZbTYRceEoxzNxPXqn6vsIzEvk,14789
|
|
20
20
|
autogen/_website/generate_mkdocs.py,sha256=TkmLnUDv1Ms5cGClXPmenA8nxmwg4kR0E-FHCVjw_og,43246
|
|
@@ -212,7 +212,7 @@ autogen/cache/in_memory_cache.py,sha256=qeECs4UnN5FzuCSgiF-Ma11eLGSjJBgGlqLraWhD
|
|
|
212
212
|
autogen/cache/redis_cache.py,sha256=XqUVObZRbD_kDL5vDD6H6xu_U-xcL8qKmsUq2Ix8xVo,4248
|
|
213
213
|
autogen/coding/__init__.py,sha256=fN8UCm3RzJZ4OFMwlp3a0jic0ZzOege6TNVAk5t6J0U,825
|
|
214
214
|
autogen/coding/base.py,sha256=hj70EwB-b0MVzHXACw-Oj3CiHo-GUC5kGZGniDTMOnw,3801
|
|
215
|
-
autogen/coding/docker_commandline_code_executor.py,sha256=
|
|
215
|
+
autogen/coding/docker_commandline_code_executor.py,sha256=8sr6bBpk1JtWEb1YQNgmj5z_n2mL_-cAP3EzSrn6SLU,10872
|
|
216
216
|
autogen/coding/factory.py,sha256=bC3kkIBNExAPt8KZvCQ1PsLpyJWLq1jpw1JcvIbKR04,1978
|
|
217
217
|
autogen/coding/func_with_reqs.py,sha256=dPcjRs4gOjoVO3m5cQYWtFm7FKO94gBYPwr3O7gVj7g,6372
|
|
218
218
|
autogen/coding/local_commandline_code_executor.py,sha256=XKkT141IYZf_HngNJjMcJ7uZXe3k056wMdXZC2hfAts,16671
|
|
@@ -226,6 +226,12 @@ autogen/coding/jupyter/import_utils.py,sha256=7yzEAYmNmJsODsqz3XCNOORKfvp0t6xnaf
|
|
|
226
226
|
autogen/coding/jupyter/jupyter_client.py,sha256=ROXAWOKG_EJ_oFNuyqUd_3uOBPUTRoTh6_-8bSpXBdU,8674
|
|
227
227
|
autogen/coding/jupyter/jupyter_code_executor.py,sha256=Z2vZvou6QzpMBg0IgOzVRoCADswd15mvfkktIjGhUMY,6374
|
|
228
228
|
autogen/coding/jupyter/local_jupyter_server.py,sha256=7b8yi5qK8ms2e5-PRCrzmXKGp1iC5KgpMU8xiqQ9u8o,6589
|
|
229
|
+
autogen/environments/__init__.py,sha256=SNh4NFcBgySNW--H1YUZPEf7F7QLpBQA3Qc8EvyM-gk,488
|
|
230
|
+
autogen/environments/docker_python_environment.py,sha256=oKdtr2IN2UDoYi8V_Qjvut5VGfwUNzNoj8J1aMRP9GU,15312
|
|
231
|
+
autogen/environments/python_environment.py,sha256=CXY0h3SWJPYSZtcR_nqY5VmVvGItIJwAnBqSx7YvECg,4265
|
|
232
|
+
autogen/environments/system_python_environment.py,sha256=jk1U4CSDPsF1igqdv-GQOKsH_NTxitRzN35-UWng2e8,3081
|
|
233
|
+
autogen/environments/venv_python_environment.py,sha256=i7dgd8lFuxfZwkysnFXuOn_B26CFYCzNHHBfCtExLkk,10076
|
|
234
|
+
autogen/environments/working_directory.py,sha256=MevUpN1lzmJLEKM_o9JHZod9cy9pQgI5AsE0ic6fvrE,2505
|
|
229
235
|
autogen/events/__init__.py,sha256=XwCA6Rsq9AyjgeecGuiwHcAvDQMmKXgGomw5iLDIU5Q,358
|
|
230
236
|
autogen/events/agent_events.py,sha256=LSCOMwA-nlBSboE5jOh-Da2Pm7hghiwjfzYTz_bS30k,31112
|
|
231
237
|
autogen/events/base_event.py,sha256=5K1wzDBAio9wLxahErSvE0-UbFfMuSTxBp6EI8SbPGU,3475
|
|
@@ -281,7 +287,7 @@ autogen/mcp/helpers.py,sha256=J5_J6n3jMJUEJH5K8k9BeUb6ymQgRUI0hC1gbY7rlwM,1533
|
|
|
281
287
|
autogen/mcp/mcp_client.py,sha256=7c_lHgBJEs77TFYjLcTlVrEu_0z4EafPPY3PgteY87c,7400
|
|
282
288
|
autogen/mcp/mcp_proxy/__init__.py,sha256=3HTU-TqHLk4XSXeBV1UFd9XkQ1B0yOuXXyGseXvDVec,518
|
|
283
289
|
autogen/mcp/mcp_proxy/fastapi_code_generator_helpers.py,sha256=dx-w2tGVMnh8pzY2NuXlMD7oIF7_5Gvc5oSbHIySEpc,2110
|
|
284
|
-
autogen/mcp/mcp_proxy/mcp_proxy.py,sha256=
|
|
290
|
+
autogen/mcp/mcp_proxy/mcp_proxy.py,sha256=1eWwZatPgPxqkhcbEFCW-ErmkrWUF9F_cLkFWVtLkug,22171
|
|
285
291
|
autogen/mcp/mcp_proxy/operation_grouping.py,sha256=1n4o5qhkQd2hVr7OaOMsRhInDveDGkCozLudsBVusC4,6349
|
|
286
292
|
autogen/mcp/mcp_proxy/operation_renaming.py,sha256=G5J4VdxUAwSvOo-DsqIUbCfV9TnH3riaHLI6IpctDF8,3956
|
|
287
293
|
autogen/mcp/mcp_proxy/patch_fastapi_code_generator.py,sha256=vBr8P890nsFvK4iuFicrdNskooHlBQeTQ6jbb5kcPAk,3490
|
|
@@ -296,7 +302,7 @@ autogen/oai/__init__.py,sha256=BIwnV6wtHmKgIM4IUdymfPfpdNqos5P7BfRv-7_QL9A,1680
|
|
|
296
302
|
autogen/oai/anthropic.py,sha256=AK_Bbcc5I-PqAvoiwB0LaO1ZLugh_wQNAOIFcj7eTtk,29174
|
|
297
303
|
autogen/oai/bedrock.py,sha256=8AYWZVsDkJS2HmQ0ggoUqlKV_a4H_ON8rks4VW2Jrxk,25719
|
|
298
304
|
autogen/oai/cerebras.py,sha256=8hiSBq88l2yTXUJPV7AvGXRCtwvW0Y9hIYUnYK2S2os,12462
|
|
299
|
-
autogen/oai/client.py,sha256=
|
|
305
|
+
autogen/oai/client.py,sha256=ZJfax-XdV92xfmIvn-qmJhSwjP-MusU4oDc9_88o-1k,68206
|
|
300
306
|
autogen/oai/client_utils.py,sha256=lVbHyff7OnpdM-tXskC23xLdFccj2AalTdWA4DxzxS4,7543
|
|
301
307
|
autogen/oai/cohere.py,sha256=pRcQWjbzKbZ1RfC1vk9WGjgndwjHbIaOVoKEYdV2L6c,19421
|
|
302
308
|
autogen/oai/gemini.py,sha256=bc_RQwtoGi7DnwQwPie7ZdvsqpD1AjYwzjVwnIKP39U,43168
|
|
@@ -323,9 +329,11 @@ autogen/tools/toolkit.py,sha256=1tOmTGJ96RhkJrrtAViKUyEcwacA6ztoIbYbnf8NgTU,2558
|
|
|
323
329
|
autogen/tools/contrib/__init__.py,sha256=DWEjPK6xCR2ihAXXdquQZmiuqRLA3Pqb8QV8W1RtS3k,202
|
|
324
330
|
autogen/tools/contrib/time/__init__.py,sha256=dplie5aBJZ8VoKy6EKcQMLTtSgcCkNDYzpdsC2I0YWk,195
|
|
325
331
|
autogen/tools/contrib/time/time.py,sha256=tPi49vOUwfvujbYA-zS00CWcLW-y18CPyQ1gnJG6iRg,1271
|
|
326
|
-
autogen/tools/experimental/__init__.py,sha256=
|
|
332
|
+
autogen/tools/experimental/__init__.py,sha256=OPJlrK_tJCPbeBT-qsIWwJBdLuaCMskpshduA8Haj7E,1671
|
|
327
333
|
autogen/tools/experimental/browser_use/__init__.py,sha256=kfxCajXcVMDH6CZq-lWh2p8PKxOwT9yjC_Za0jr4zUg,290
|
|
328
334
|
autogen/tools/experimental/browser_use/browser_use.py,sha256=KfU4MI_BWaHepv0bDMf9HTDUaHTJThuBJI8R_BPpjmg,5561
|
|
335
|
+
autogen/tools/experimental/code_execution/__init__.py,sha256=KFUku2awEgsnkSa48t2iMMPC-QCxpqAG7lb8fB4Fr-c,242
|
|
336
|
+
autogen/tools/experimental/code_execution/python_code_execution.py,sha256=Fj1Wxsn6z0HuXWpqa-ahedkCtyPYBShUnbiyvSw_mus,3705
|
|
329
337
|
autogen/tools/experimental/crawl4ai/__init__.py,sha256=UjFJLSZ9P5xT6WCV0RDPtwt4MHuwPdK90TU7ByXhLWs,207
|
|
330
338
|
autogen/tools/experimental/crawl4ai/crawl4ai.py,sha256=MsOLtbPHRpRrCnRJPQVVVNwmsBcgsWLSHNXDOF-47ws,6088
|
|
331
339
|
autogen/tools/experimental/deep_research/__init__.py,sha256=9SFcDEj2OHxNSlXP11lf1uHENlfUeO47ROcOSD9GCDs,220
|
|
@@ -406,8 +414,8 @@ autogen/agentchat/contrib/captainagent/tools/math/modular_inverse_sum.py,sha256=
|
|
|
406
414
|
autogen/agentchat/contrib/captainagent/tools/math/simplify_mixed_numbers.py,sha256=iqgpFJdyBHPPNCqkehSIbeuV8Rabr2eDMilT23Wx7PI,1687
|
|
407
415
|
autogen/agentchat/contrib/captainagent/tools/math/sum_of_digit_factorials.py,sha256=-6T5r6Er4mONPldRxv3F9tLoE7Og3qmeSeTC7Du_tTg,596
|
|
408
416
|
autogen/agentchat/contrib/captainagent/tools/math/sum_of_primes_below.py,sha256=Xig7K3A3DRnbv-UXfyo5bybGZUQYAQsltthfTYW5eV8,509
|
|
409
|
-
ag2-0.9.
|
|
410
|
-
ag2-0.9.
|
|
411
|
-
ag2-0.9.
|
|
412
|
-
ag2-0.9.
|
|
413
|
-
ag2-0.9.
|
|
417
|
+
ag2-0.9.6.dist-info/METADATA,sha256=Syw0Wb_vvJEdWJ52b0UjycmcDQj0PJtCt8ZRUa-lLeM,35268
|
|
418
|
+
ag2-0.9.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
419
|
+
ag2-0.9.6.dist-info/licenses/LICENSE,sha256=GEFQVNayAR-S_rQD5l8hPdgvgyktVdy4Bx5-v90IfRI,11384
|
|
420
|
+
ag2-0.9.6.dist-info/licenses/NOTICE.md,sha256=07iCPQGbth4pQrgkSgZinJGT5nXddkZ6_MGYcBd2oiY,1134
|
|
421
|
+
ag2-0.9.6.dist-info/RECORD,,
|
|
@@ -71,6 +71,8 @@ class DockerCommandLineCodeExecutor(CodeExecutor):
|
|
|
71
71
|
auto_remove: bool = True,
|
|
72
72
|
stop_container: bool = True,
|
|
73
73
|
execution_policies: Optional[dict[str, bool]] = None,
|
|
74
|
+
*,
|
|
75
|
+
container_create_kwargs: Optional[dict[str, Any]] = None,
|
|
74
76
|
):
|
|
75
77
|
"""(Experimental) A code executor class that executes code through
|
|
76
78
|
a command line environment in a Docker container.
|
|
@@ -98,6 +100,11 @@ class DockerCommandLineCodeExecutor(CodeExecutor):
|
|
|
98
100
|
whether code in that language should be executed. True means code in that language
|
|
99
101
|
will be executed, False means it will only be saved to a file. This overrides the
|
|
100
102
|
default execution policies. Defaults to None.
|
|
103
|
+
container_create_kwargs: Optional dict forwarded verbatim to
|
|
104
|
+
"docker.client.containers.create". Use it to set advanced Docker
|
|
105
|
+
options (environment variables, GPU device_requests, port mappings, etc.).
|
|
106
|
+
Values here override the class defaults when keys collide. Defaults to None.
|
|
107
|
+
|
|
101
108
|
|
|
102
109
|
Raises:
|
|
103
110
|
ValueError: On argument error, or if the container fails to start.
|
|
@@ -128,16 +135,29 @@ class DockerCommandLineCodeExecutor(CodeExecutor):
|
|
|
128
135
|
if container_name is None:
|
|
129
136
|
container_name = f"autogen-code-exec-{uuid.uuid4()}"
|
|
130
137
|
|
|
138
|
+
# build kwargs for docker.create
|
|
139
|
+
base_kwargs: dict[str, Any] = {
|
|
140
|
+
"image": image,
|
|
141
|
+
"name": container_name,
|
|
142
|
+
"entrypoint": "/bin/sh",
|
|
143
|
+
"tty": True,
|
|
144
|
+
"auto_remove": auto_remove,
|
|
145
|
+
"volumes": {str(bind_dir.resolve()): {"bind": "/workspace", "mode": "rw"}},
|
|
146
|
+
"working_dir": "/workspace",
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if container_create_kwargs:
|
|
150
|
+
for k in ("entrypoint", "volumes", "working_dir", "tty"):
|
|
151
|
+
if k in container_create_kwargs:
|
|
152
|
+
logging.warning(
|
|
153
|
+
"DockerCommandLineCodeExecutor: overriding default %s=%s",
|
|
154
|
+
k,
|
|
155
|
+
container_create_kwargs[k],
|
|
156
|
+
)
|
|
157
|
+
base_kwargs.update(container_create_kwargs)
|
|
158
|
+
|
|
131
159
|
# Start a container from the image, read to exec commands later
|
|
132
|
-
self._container = client.containers.create(
|
|
133
|
-
image,
|
|
134
|
-
name=container_name,
|
|
135
|
-
entrypoint="/bin/sh",
|
|
136
|
-
tty=True,
|
|
137
|
-
auto_remove=auto_remove,
|
|
138
|
-
volumes={str(bind_dir.resolve()): {"bind": "/workspace", "mode": "rw"}},
|
|
139
|
-
working_dir="/workspace",
|
|
140
|
-
)
|
|
160
|
+
self._container = client.containers.create(**base_kwargs)
|
|
141
161
|
self._container.start()
|
|
142
162
|
|
|
143
163
|
_wait_for_ready(self._container)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from .docker_python_environment import DockerPythonEnvironment
|
|
6
|
+
from .system_python_environment import SystemPythonEnvironment
|
|
7
|
+
from .venv_python_environment import VenvPythonEnvironment
|
|
8
|
+
from .working_directory import WorkingDirectory
|
|
9
|
+
|
|
10
|
+
__all__ = ["DockerPythonEnvironment", "SystemPythonEnvironment", "VenvPythonEnvironment", "WorkingDirectory"]
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
# Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
import tempfile
|
|
10
|
+
import uuid
|
|
11
|
+
from typing import Any, Optional, Tuple
|
|
12
|
+
|
|
13
|
+
from asyncer import asyncify
|
|
14
|
+
|
|
15
|
+
from .python_environment import PythonEnvironment
|
|
16
|
+
|
|
17
|
+
__all__ = ["DockerPythonEnvironment"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DockerPythonEnvironment(PythonEnvironment):
|
|
21
|
+
"""A Python environment using Docker containers for isolated execution."""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
image: str = "python:3.11-slim",
|
|
26
|
+
container_name_prefix: str = "ag2_docker_env_",
|
|
27
|
+
volumes: Optional[dict[str, str]] = None,
|
|
28
|
+
environment: Optional[dict[str, str]] = None,
|
|
29
|
+
network: Optional[str] = None,
|
|
30
|
+
pip_packages: Optional[list[str]] = None,
|
|
31
|
+
requirements_file: Optional[str] = None,
|
|
32
|
+
dockerfile: Optional[str] = None,
|
|
33
|
+
build_args: Optional[dict[str, str]] = None,
|
|
34
|
+
cleanup_container: bool = True,
|
|
35
|
+
keep_container_running: bool = False,
|
|
36
|
+
container_startup_timeout: int = 30,
|
|
37
|
+
):
|
|
38
|
+
"""
|
|
39
|
+
Initialize a Docker Python environment.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
image: Docker image to use (ignored if dockerfile is provided)
|
|
43
|
+
container_name_prefix: Prefix for container names
|
|
44
|
+
volumes: Dictionary mapping host paths to container paths for mounting
|
|
45
|
+
environment: Dictionary of environment variables to set in the container
|
|
46
|
+
network: Docker network to attach the container to
|
|
47
|
+
pip_packages: List of pip packages to install in the container
|
|
48
|
+
requirements_file: Path to requirements.txt file to install in the container
|
|
49
|
+
dockerfile: Optional path to a Dockerfile to build and use instead of pulling an image
|
|
50
|
+
build_args: Optional build arguments for the Dockerfile
|
|
51
|
+
cleanup_container: Whether to remove the container after use
|
|
52
|
+
keep_container_running: Whether to keep the container running after execution
|
|
53
|
+
container_startup_timeout: Timeout in seconds for container startup
|
|
54
|
+
"""
|
|
55
|
+
self.image = image
|
|
56
|
+
self.container_name_prefix = container_name_prefix
|
|
57
|
+
self.volumes = volumes or {}
|
|
58
|
+
self.environment = environment or {}
|
|
59
|
+
self.network = network
|
|
60
|
+
self.pip_packages = pip_packages or []
|
|
61
|
+
self.requirements_file = requirements_file
|
|
62
|
+
self.dockerfile = dockerfile
|
|
63
|
+
self.build_args = build_args or {}
|
|
64
|
+
self.cleanup_container = cleanup_container
|
|
65
|
+
self.keep_container_running = keep_container_running
|
|
66
|
+
self.container_startup_timeout = container_startup_timeout
|
|
67
|
+
|
|
68
|
+
# Internal state
|
|
69
|
+
self._container_id = None
|
|
70
|
+
self._container_name = None
|
|
71
|
+
self._custom_image_name = None
|
|
72
|
+
self._temp_dir = None
|
|
73
|
+
|
|
74
|
+
super().__init__()
|
|
75
|
+
|
|
76
|
+
def _setup_environment(self) -> None:
|
|
77
|
+
"""Set up the Docker environment."""
|
|
78
|
+
# Verify Docker is installed and accessible
|
|
79
|
+
try:
|
|
80
|
+
result = subprocess.run(["docker", "--version"], capture_output=True, text=True, check=True)
|
|
81
|
+
logging.info(f"Docker version: {result.stdout.strip()}")
|
|
82
|
+
except (subprocess.SubprocessError, FileNotFoundError) as e:
|
|
83
|
+
raise RuntimeError(
|
|
84
|
+
"Docker not found or not accessible. Please ensure Docker is installed and running."
|
|
85
|
+
) from e
|
|
86
|
+
|
|
87
|
+
# Create a temporary directory for file operations
|
|
88
|
+
self._temp_dir = tempfile.mkdtemp(prefix="ag2_docker_")
|
|
89
|
+
|
|
90
|
+
# Generate a unique container name
|
|
91
|
+
self._container_name = f"{self.container_name_prefix}{uuid.uuid4().hex[:8]}"
|
|
92
|
+
|
|
93
|
+
# Build custom image if Dockerfile is provided
|
|
94
|
+
if self.dockerfile:
|
|
95
|
+
self._build_custom_image()
|
|
96
|
+
else:
|
|
97
|
+
# Pull the specified image
|
|
98
|
+
try:
|
|
99
|
+
subprocess.run(
|
|
100
|
+
["docker", "pull", self.image],
|
|
101
|
+
check=True,
|
|
102
|
+
stdout=subprocess.PIPE,
|
|
103
|
+
stderr=subprocess.PIPE,
|
|
104
|
+
text=True,
|
|
105
|
+
)
|
|
106
|
+
logging.info(f"Pulled Docker image: {self.image}")
|
|
107
|
+
except subprocess.CalledProcessError as e:
|
|
108
|
+
raise RuntimeError(f"Failed to pull Docker image: {e.stderr}") from e
|
|
109
|
+
|
|
110
|
+
# Start the container
|
|
111
|
+
self._start_container()
|
|
112
|
+
|
|
113
|
+
def _build_custom_image(self) -> None:
|
|
114
|
+
"""Build a custom Docker image from the provided Dockerfile."""
|
|
115
|
+
if not os.path.exists(self.dockerfile):
|
|
116
|
+
raise RuntimeError(f"Dockerfile not found at: {self.dockerfile}")
|
|
117
|
+
|
|
118
|
+
# Create a unique image name
|
|
119
|
+
self._custom_image_name = f"ag2-custom-python-{uuid.uuid4().hex[:8]}"
|
|
120
|
+
|
|
121
|
+
# Build command
|
|
122
|
+
build_cmd = ["docker", "build", "-t", self._custom_image_name]
|
|
123
|
+
|
|
124
|
+
# Add build args
|
|
125
|
+
for arg_name, arg_value in self.build_args.items():
|
|
126
|
+
build_cmd.extend(["--build-arg", f"{arg_name}={arg_value}"])
|
|
127
|
+
|
|
128
|
+
# Add Dockerfile path
|
|
129
|
+
build_cmd.extend(["-f", self.dockerfile, os.path.dirname(self.dockerfile)])
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
logging.info(f"Building custom Docker image: {self._custom_image_name}")
|
|
133
|
+
_ = subprocess.run(
|
|
134
|
+
build_cmd,
|
|
135
|
+
check=True,
|
|
136
|
+
stdout=subprocess.PIPE,
|
|
137
|
+
stderr=subprocess.PIPE,
|
|
138
|
+
text=True,
|
|
139
|
+
)
|
|
140
|
+
logging.info(f"Built custom Docker image: {self._custom_image_name}")
|
|
141
|
+
except subprocess.CalledProcessError as e:
|
|
142
|
+
raise RuntimeError(f"Failed to build Docker image: {e.stderr}") from e
|
|
143
|
+
|
|
144
|
+
# Use the custom image
|
|
145
|
+
self.image = self._custom_image_name
|
|
146
|
+
|
|
147
|
+
def _start_container(self) -> None:
|
|
148
|
+
"""Start the Docker container."""
|
|
149
|
+
# Basic container run command
|
|
150
|
+
run_cmd = ["docker", "run", "--name", self._container_name]
|
|
151
|
+
|
|
152
|
+
# Add detached mode flag to run container in background
|
|
153
|
+
run_cmd.append("-d")
|
|
154
|
+
|
|
155
|
+
# Add network if specified
|
|
156
|
+
if self.network:
|
|
157
|
+
run_cmd.extend(["--network", self.network])
|
|
158
|
+
|
|
159
|
+
# Add environment variables
|
|
160
|
+
for env_name, env_value in self.environment.items():
|
|
161
|
+
run_cmd.extend(["-e", f"{env_name}={env_value}"])
|
|
162
|
+
|
|
163
|
+
# Add volume mounts including temp directory
|
|
164
|
+
work_dir_mount = f"{self._temp_dir}:/workspace"
|
|
165
|
+
run_cmd.extend(["-v", work_dir_mount])
|
|
166
|
+
|
|
167
|
+
for host_path, container_path in self.volumes.items():
|
|
168
|
+
run_cmd.extend(["-v", f"{host_path}:{container_path}"])
|
|
169
|
+
|
|
170
|
+
# Set workspace as working directory
|
|
171
|
+
run_cmd.extend(["-w", "/workspace"])
|
|
172
|
+
|
|
173
|
+
# Add tty to keep container running
|
|
174
|
+
run_cmd.append("-t")
|
|
175
|
+
|
|
176
|
+
# Add image name
|
|
177
|
+
run_cmd.append(self.image)
|
|
178
|
+
|
|
179
|
+
# Initial command to keep container running
|
|
180
|
+
run_cmd.extend(["tail", "-f", "/dev/null"])
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
# Start the container
|
|
184
|
+
logging.info(f"Starting Docker container: {self._container_name}")
|
|
185
|
+
result = subprocess.run(
|
|
186
|
+
run_cmd,
|
|
187
|
+
check=True,
|
|
188
|
+
stdout=subprocess.PIPE,
|
|
189
|
+
stderr=subprocess.PIPE,
|
|
190
|
+
text=True,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# Get container ID
|
|
194
|
+
self._container_id = result.stdout.strip()
|
|
195
|
+
logging.info(f"Started Docker container: {self._container_name} ({self._container_id})")
|
|
196
|
+
|
|
197
|
+
# Install pip packages if specified
|
|
198
|
+
if self.pip_packages or self.requirements_file:
|
|
199
|
+
self._install_packages()
|
|
200
|
+
|
|
201
|
+
except subprocess.CalledProcessError as e:
|
|
202
|
+
raise RuntimeError(f"Failed to start Docker container: {e.stderr}") from e
|
|
203
|
+
|
|
204
|
+
def _install_packages(self) -> None:
|
|
205
|
+
"""Install Python packages in the running container."""
|
|
206
|
+
# Install pip packages
|
|
207
|
+
if self.pip_packages:
|
|
208
|
+
packages_str = " ".join(self.pip_packages)
|
|
209
|
+
try:
|
|
210
|
+
logging.info(f"Installing pip packages: {packages_str}")
|
|
211
|
+
_ = subprocess.run(
|
|
212
|
+
["docker", "exec", self._container_name, "pip", "install", "--no-cache-dir"] + self.pip_packages,
|
|
213
|
+
check=True,
|
|
214
|
+
stdout=subprocess.PIPE,
|
|
215
|
+
stderr=subprocess.PIPE,
|
|
216
|
+
text=True,
|
|
217
|
+
)
|
|
218
|
+
logging.info("Successfully installed pip packages")
|
|
219
|
+
except subprocess.CalledProcessError as e:
|
|
220
|
+
logging.warning(f"Failed to install pip packages: {e.stderr}")
|
|
221
|
+
|
|
222
|
+
# Install from requirements file
|
|
223
|
+
if self.requirements_file:
|
|
224
|
+
if os.path.exists(self.requirements_file):
|
|
225
|
+
# Copy requirements file to temp directory
|
|
226
|
+
req_filename = os.path.basename(self.requirements_file)
|
|
227
|
+
temp_req_path = os.path.join(self._temp_dir, req_filename)
|
|
228
|
+
shutil.copy(self.requirements_file, temp_req_path)
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
logging.info(f"Installing requirements from: {req_filename}")
|
|
232
|
+
_ = subprocess.run(
|
|
233
|
+
[
|
|
234
|
+
"docker",
|
|
235
|
+
"exec",
|
|
236
|
+
self._container_name,
|
|
237
|
+
"pip",
|
|
238
|
+
"install",
|
|
239
|
+
"--no-cache-dir",
|
|
240
|
+
"-r",
|
|
241
|
+
f"/workspace/{req_filename}",
|
|
242
|
+
],
|
|
243
|
+
check=True,
|
|
244
|
+
stdout=subprocess.PIPE,
|
|
245
|
+
stderr=subprocess.PIPE,
|
|
246
|
+
text=True,
|
|
247
|
+
)
|
|
248
|
+
logging.info("Successfully installed requirements")
|
|
249
|
+
except subprocess.CalledProcessError as e:
|
|
250
|
+
logging.warning(f"Failed to install requirements: {e.stderr}")
|
|
251
|
+
else:
|
|
252
|
+
logging.warning(f"Requirements file not found: {self.requirements_file}")
|
|
253
|
+
|
|
254
|
+
def _cleanup_environment(self) -> None:
|
|
255
|
+
"""Clean up the Docker environment."""
|
|
256
|
+
if self._container_id:
|
|
257
|
+
# Stop the container if it's running and we want to clean it up
|
|
258
|
+
if not self.keep_container_running:
|
|
259
|
+
try:
|
|
260
|
+
logging.info(f"Stopping Docker container: {self._container_name}")
|
|
261
|
+
subprocess.run(
|
|
262
|
+
["docker", "stop", self._container_name],
|
|
263
|
+
check=True,
|
|
264
|
+
stdout=subprocess.PIPE,
|
|
265
|
+
stderr=subprocess.PIPE,
|
|
266
|
+
text=True,
|
|
267
|
+
)
|
|
268
|
+
except subprocess.CalledProcessError:
|
|
269
|
+
logging.warning(f"Failed to stop Docker container: {self._container_name}")
|
|
270
|
+
|
|
271
|
+
# Remove the container if cleanup is enabled
|
|
272
|
+
if self.cleanup_container and not self.keep_container_running:
|
|
273
|
+
try:
|
|
274
|
+
logging.info(f"Removing Docker container: {self._container_name}")
|
|
275
|
+
subprocess.run(
|
|
276
|
+
["docker", "rm", "-f", self._container_name],
|
|
277
|
+
check=True,
|
|
278
|
+
stdout=subprocess.PIPE,
|
|
279
|
+
stderr=subprocess.PIPE,
|
|
280
|
+
text=True,
|
|
281
|
+
)
|
|
282
|
+
except subprocess.CalledProcessError:
|
|
283
|
+
logging.warning(f"Failed to remove Docker container: {self._container_name}")
|
|
284
|
+
|
|
285
|
+
# Remove the custom image if it was created
|
|
286
|
+
if self._custom_image_name and self.cleanup_container:
|
|
287
|
+
try:
|
|
288
|
+
logging.info(f"Removing custom Docker image: {self._custom_image_name}")
|
|
289
|
+
subprocess.run(
|
|
290
|
+
["docker", "rmi", self._custom_image_name],
|
|
291
|
+
check=True,
|
|
292
|
+
stdout=subprocess.PIPE,
|
|
293
|
+
stderr=subprocess.PIPE,
|
|
294
|
+
text=True,
|
|
295
|
+
)
|
|
296
|
+
except subprocess.CalledProcessError:
|
|
297
|
+
logging.warning(f"Failed to remove custom Docker image: {self._custom_image_name}")
|
|
298
|
+
|
|
299
|
+
# Clean up the temporary directory
|
|
300
|
+
if self._temp_dir and os.path.exists(self._temp_dir):
|
|
301
|
+
try:
|
|
302
|
+
shutil.rmtree(self._temp_dir)
|
|
303
|
+
except Exception as e:
|
|
304
|
+
logging.warning(f"Failed to remove temporary directory: {e}")
|
|
305
|
+
|
|
306
|
+
def get_executable(self) -> str:
|
|
307
|
+
"""Get the path to the Python executable in the Docker container."""
|
|
308
|
+
# This is a virtual path in the container
|
|
309
|
+
return "python"
|
|
310
|
+
|
|
311
|
+
async def execute_code(self, code: str, script_path: str, timeout: int = 30) -> dict[str, Any]:
|
|
312
|
+
"""Execute code in the Docker container."""
|
|
313
|
+
# Ensure the container is running
|
|
314
|
+
if not self._container_id:
|
|
315
|
+
return {"success": False, "error": "Docker container not started"}
|
|
316
|
+
|
|
317
|
+
try:
|
|
318
|
+
# Calculate the relative path within the temp directory
|
|
319
|
+
if os.path.isabs(script_path):
|
|
320
|
+
rel_path = os.path.basename(script_path)
|
|
321
|
+
host_script_path = os.path.join(self._temp_dir, rel_path)
|
|
322
|
+
else:
|
|
323
|
+
rel_path = script_path
|
|
324
|
+
host_script_path = os.path.join(self._temp_dir, rel_path)
|
|
325
|
+
|
|
326
|
+
# Ensure the directory for the script exists
|
|
327
|
+
script_dir = os.path.dirname(host_script_path)
|
|
328
|
+
if script_dir:
|
|
329
|
+
os.makedirs(script_dir, exist_ok=True)
|
|
330
|
+
|
|
331
|
+
# Write the code to the script file on the host
|
|
332
|
+
await asyncify(self._write_to_file)(host_script_path, code)
|
|
333
|
+
|
|
334
|
+
# Path to the script in the container
|
|
335
|
+
container_script_path = f"/workspace/{rel_path}"
|
|
336
|
+
|
|
337
|
+
# Execute the script in the container
|
|
338
|
+
exec_cmd = ["docker", "exec", self._container_name, "python", container_script_path]
|
|
339
|
+
|
|
340
|
+
# Run the command with a timeout
|
|
341
|
+
result = await asyncify(self._run_subprocess_with_timeout)(exec_cmd, timeout)
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
"success": result[0],
|
|
345
|
+
"stdout": result[1],
|
|
346
|
+
"stderr": result[2],
|
|
347
|
+
"returncode": result[3] if result[0] else 1,
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
except Exception as e:
|
|
351
|
+
return {"success": False, "error": f"Execution error: {str(e)}"}
|
|
352
|
+
|
|
353
|
+
def _run_subprocess_with_timeout(self, cmd: list[str], timeout: int) -> Tuple[bool, str, str, int]:
|
|
354
|
+
"""
|
|
355
|
+
Run a subprocess with timeout and return status, stdout, stderr, and return code.
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
cmd: Command to run as a list of strings
|
|
359
|
+
timeout: Maximum execution time in seconds
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
Tuple of (success, stdout, stderr, return_code)
|
|
363
|
+
"""
|
|
364
|
+
try:
|
|
365
|
+
result = subprocess.run(
|
|
366
|
+
cmd,
|
|
367
|
+
capture_output=True,
|
|
368
|
+
text=True,
|
|
369
|
+
timeout=timeout,
|
|
370
|
+
)
|
|
371
|
+
return (result.returncode == 0, result.stdout, result.stderr, result.returncode)
|
|
372
|
+
except subprocess.TimeoutExpired:
|
|
373
|
+
return (False, "", f"Execution timed out after {timeout} seconds", -1)
|
|
374
|
+
except Exception as e:
|
|
375
|
+
return (False, "", str(e), -1)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from contextvars import ContextVar
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
__all__ = ["PythonEnvironment"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PythonEnvironment(ABC):
|
|
14
|
+
"""Python execution environments base class"""
|
|
15
|
+
|
|
16
|
+
# Shared context variable for tracking the current environment
|
|
17
|
+
_current_python_environment: ContextVar["PythonEnvironment"] = ContextVar("_current_python_environment")
|
|
18
|
+
|
|
19
|
+
def __init__(self):
|
|
20
|
+
"""
|
|
21
|
+
Initialize the Python environment.
|
|
22
|
+
"""
|
|
23
|
+
self._token = None
|
|
24
|
+
|
|
25
|
+
# Set up the environment
|
|
26
|
+
self._setup_environment()
|
|
27
|
+
|
|
28
|
+
def __enter__(self):
|
|
29
|
+
"""
|
|
30
|
+
Enter the environment context.
|
|
31
|
+
Sets this environment as the current one.
|
|
32
|
+
"""
|
|
33
|
+
# Set this as the current Python environment in the context
|
|
34
|
+
self._token = PythonEnvironment._current_python_environment.set(self)
|
|
35
|
+
|
|
36
|
+
return self
|
|
37
|
+
|
|
38
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
39
|
+
"""
|
|
40
|
+
Exit the environment context.
|
|
41
|
+
Resets the current environment and performs cleanup.
|
|
42
|
+
"""
|
|
43
|
+
# Reset the context variable if this was the active environment
|
|
44
|
+
if self._token is not None:
|
|
45
|
+
PythonEnvironment._current_python_environment.reset(self._token)
|
|
46
|
+
self._token = None
|
|
47
|
+
|
|
48
|
+
# Clean up resources
|
|
49
|
+
self._cleanup_environment()
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def _setup_environment(self) -> None:
|
|
53
|
+
"""Set up the Python environment. Called by __enter__."""
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
@abstractmethod
|
|
57
|
+
def _cleanup_environment(self) -> None:
|
|
58
|
+
"""Clean up the Python environment. Called by __exit__."""
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
@abstractmethod
|
|
62
|
+
def get_executable(self) -> str:
|
|
63
|
+
"""
|
|
64
|
+
Get the path to the Python executable in this environment.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
The full path to the Python executable.
|
|
68
|
+
"""
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
@abstractmethod
|
|
72
|
+
async def execute_code(self, code: str, script_path: str, timeout: int = 30) -> dict[str, Any]:
|
|
73
|
+
"""
|
|
74
|
+
Execute the given code in this environment.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
code: The Python code to execute.
|
|
78
|
+
script_path: Path where the code should be saved before execution.
|
|
79
|
+
timeout: Maximum execution time in seconds.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
dict with execution results including stdout, stderr, and success status.
|
|
83
|
+
"""
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
# Utility method for subclasses to wrap (for async support)
|
|
87
|
+
def _write_to_file(self, script_path: str, content: str) -> None:
|
|
88
|
+
"""
|
|
89
|
+
Write content to a file (blocking operation).
|
|
90
|
+
|
|
91
|
+
This is a helper method for use with asyncify in async contexts.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
script_path: Path to the file to write.
|
|
95
|
+
content: Content to write to the file.
|
|
96
|
+
"""
|
|
97
|
+
with open(script_path, "w") as f:
|
|
98
|
+
f.write(content)
|
|
99
|
+
|
|
100
|
+
# Utility method for subclasses to wrap (for async support)
|
|
101
|
+
def _run_subprocess(self, cmd: list[str], timeout: int) -> subprocess.CompletedProcess:
|
|
102
|
+
"""
|
|
103
|
+
Run a subprocess (blocking operation).
|
|
104
|
+
|
|
105
|
+
This is a helper method for use with asyncify in async contexts.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
cmd: Command to run as a list of strings.
|
|
109
|
+
timeout: Maximum execution time in seconds.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
CompletedProcess instance with results of the subprocess.
|
|
113
|
+
"""
|
|
114
|
+
return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def get_current_python_environment(
|
|
118
|
+
cls, python_environment: Optional["PythonEnvironment"] = None
|
|
119
|
+
) -> Optional["PythonEnvironment"]:
|
|
120
|
+
"""
|
|
121
|
+
Get the current Python environment or the specified one if provided.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
python_environment: Optional environment to return if specified.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
The current Python environment or None if none is active.
|
|
128
|
+
"""
|
|
129
|
+
if python_environment is not None:
|
|
130
|
+
return python_environment
|
|
131
|
+
try:
|
|
132
|
+
return cls._current_python_environment.get()
|
|
133
|
+
except LookupError:
|
|
134
|
+
return None
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
|
|
11
|
+
from asyncer import asyncify
|
|
12
|
+
|
|
13
|
+
from .python_environment import PythonEnvironment
|
|
14
|
+
|
|
15
|
+
__all__ = ["SystemPythonEnvironment"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SystemPythonEnvironment(PythonEnvironment):
|
|
19
|
+
"""A Python environment using the system's Python installation."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
executable: Optional[str] = None,
|
|
24
|
+
):
|
|
25
|
+
"""
|
|
26
|
+
Initialize a system Python environment.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
executable: Optional path to a specific Python executable. If None, uses the current Python executable.
|
|
30
|
+
"""
|
|
31
|
+
self._executable = executable or sys.executable
|
|
32
|
+
super().__init__()
|
|
33
|
+
|
|
34
|
+
def _setup_environment(self) -> None:
|
|
35
|
+
"""Set up the system Python environment."""
|
|
36
|
+
# Verify the Python executable exists
|
|
37
|
+
if not os.path.exists(self._executable):
|
|
38
|
+
raise RuntimeError(f"Python executable not found at: {self._executable}")
|
|
39
|
+
|
|
40
|
+
logging.info(f"Using system Python at: {self._executable}")
|
|
41
|
+
|
|
42
|
+
def _cleanup_environment(self) -> None:
|
|
43
|
+
"""Clean up the system Python environment."""
|
|
44
|
+
# No cleanup needed for system Python
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
def get_executable(self) -> str:
|
|
48
|
+
"""Get the path to the Python executable."""
|
|
49
|
+
return self._executable
|
|
50
|
+
|
|
51
|
+
async def execute_code(self, code: str, script_path: str, timeout: int = 30) -> dict[str, Any]:
|
|
52
|
+
"""Execute code using the system Python."""
|
|
53
|
+
try:
|
|
54
|
+
# Get the Python executable
|
|
55
|
+
python_executable = self.get_executable()
|
|
56
|
+
|
|
57
|
+
# Verify the executable exists
|
|
58
|
+
if not os.path.exists(python_executable):
|
|
59
|
+
return {"success": False, "error": f"Python executable not found at {python_executable}"}
|
|
60
|
+
|
|
61
|
+
# Ensure the directory for the script exists
|
|
62
|
+
script_dir = os.path.dirname(script_path)
|
|
63
|
+
if script_dir:
|
|
64
|
+
os.makedirs(script_dir, exist_ok=True)
|
|
65
|
+
|
|
66
|
+
# Write the code to the script file using asyncify (from base class)
|
|
67
|
+
await asyncify(self._write_to_file)(script_path, code)
|
|
68
|
+
|
|
69
|
+
logging.info(f"Wrote code to {script_path}")
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
# Execute directly with subprocess using asyncify for better reliability
|
|
73
|
+
result = await asyncify(self._run_subprocess)([python_executable, script_path], timeout)
|
|
74
|
+
|
|
75
|
+
# Main execution result
|
|
76
|
+
return {
|
|
77
|
+
"success": result.returncode == 0,
|
|
78
|
+
"stdout": result.stdout,
|
|
79
|
+
"stderr": result.stderr,
|
|
80
|
+
"returncode": result.returncode,
|
|
81
|
+
}
|
|
82
|
+
except subprocess.TimeoutExpired:
|
|
83
|
+
return {"success": False, "error": f"Execution timed out after {timeout} seconds"}
|
|
84
|
+
|
|
85
|
+
except Exception as e:
|
|
86
|
+
return {"success": False, "error": f"Execution error: {str(e)}"}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
from typing import Any, Optional
|
|
11
|
+
|
|
12
|
+
from asyncer import asyncify
|
|
13
|
+
|
|
14
|
+
from .python_environment import PythonEnvironment
|
|
15
|
+
|
|
16
|
+
__all__ = ["VenvPythonEnvironment"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class VenvPythonEnvironment(PythonEnvironment):
|
|
20
|
+
"""A Python environment using a virtual environment (venv)."""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
python_version: Optional[str] = None,
|
|
25
|
+
python_path: Optional[str] = None,
|
|
26
|
+
venv_path: Optional[str] = None,
|
|
27
|
+
):
|
|
28
|
+
"""
|
|
29
|
+
Initialize a virtual environment for Python execution.
|
|
30
|
+
|
|
31
|
+
If you pass in a venv_path the path will be checked for a valid venv. If the venv doesn't exist it will be created using the python_version or python_path provided.
|
|
32
|
+
|
|
33
|
+
If the python_version or python_path is provided and the venv_path is not, a temporary directory will be created for venv and it will be setup with the provided python version.
|
|
34
|
+
|
|
35
|
+
If python_path is provided, it will take precedence over python_version.
|
|
36
|
+
|
|
37
|
+
The python version will not be installed if it doesn't exist and a RuntimeError will be raised.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
python_version: The Python version to use (e.g., "3.11"), otherwise defaults to the current executing Python version. Ignored if venv_path is provided and has a valid environment already.
|
|
41
|
+
python_path: Optional direct path to a Python executable to use (must include the executable). Takes precedence over python_version if both are provided.
|
|
42
|
+
venv_path: Optional path for the virtual environment, will create it if it doesn't exist. If None, creates a temp directory.
|
|
43
|
+
"""
|
|
44
|
+
self.python_version = python_version
|
|
45
|
+
self.python_path = python_path
|
|
46
|
+
self.venv_path = venv_path
|
|
47
|
+
self.created_venv = False
|
|
48
|
+
self._executable = None
|
|
49
|
+
super().__init__()
|
|
50
|
+
|
|
51
|
+
def _setup_environment(self) -> None:
|
|
52
|
+
"""Set up the virtual environment."""
|
|
53
|
+
# Create a venv directory if not provided
|
|
54
|
+
if self.venv_path is None:
|
|
55
|
+
self.venv_path = tempfile.mkdtemp(prefix="ag2_python_env_")
|
|
56
|
+
self.created_venv = True
|
|
57
|
+
|
|
58
|
+
# Determine the python version, getting it from the venv if it already has one
|
|
59
|
+
base_python = self._get_python_executable_for_version()
|
|
60
|
+
needs_creation = True
|
|
61
|
+
else:
|
|
62
|
+
# If venv_path is provided, check if it's already a valid venv
|
|
63
|
+
if os.name == "nt": # Windows
|
|
64
|
+
venv_python = os.path.join(self.venv_path, "Scripts", "python.exe")
|
|
65
|
+
else: # Unix-like (Mac/Linux)
|
|
66
|
+
venv_python = os.path.join(self.venv_path, "bin", "python")
|
|
67
|
+
|
|
68
|
+
if os.path.exists(venv_python) and os.access(venv_python, os.X_OK):
|
|
69
|
+
# Valid venv already exists, just use it
|
|
70
|
+
self._executable = venv_python
|
|
71
|
+
logging.info(f"Using existing virtual environment at {self.venv_path}")
|
|
72
|
+
needs_creation = False
|
|
73
|
+
else:
|
|
74
|
+
# Path exists but not a valid venv, or doesn't exist
|
|
75
|
+
if not os.path.exists(self.venv_path):
|
|
76
|
+
os.makedirs(self.venv_path, exist_ok=True)
|
|
77
|
+
self.created_venv = True
|
|
78
|
+
base_python = sys.executable
|
|
79
|
+
needs_creation = True
|
|
80
|
+
|
|
81
|
+
# Only create the venv if needed
|
|
82
|
+
if needs_creation:
|
|
83
|
+
logging.info(f"Creating virtual environment at {self.venv_path} using {base_python}")
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
# Create the virtual environment
|
|
87
|
+
_ = subprocess.run(
|
|
88
|
+
[base_python, "-m", "venv", "--system-site-packages", self.venv_path],
|
|
89
|
+
check=True,
|
|
90
|
+
stdout=subprocess.PIPE,
|
|
91
|
+
stderr=subprocess.PIPE,
|
|
92
|
+
text=True,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Determine the Python executable path
|
|
96
|
+
if os.name == "nt": # Windows
|
|
97
|
+
self._executable = os.path.join(self.venv_path, "Scripts", "python.exe")
|
|
98
|
+
else: # Unix-like (Mac/Linux)
|
|
99
|
+
self._executable = os.path.join(self.venv_path, "bin", "python")
|
|
100
|
+
|
|
101
|
+
# Verify the executable exists
|
|
102
|
+
if not os.path.exists(self._executable):
|
|
103
|
+
raise RuntimeError(
|
|
104
|
+
f"Virtual environment created but Python executable not found at {self._executable}"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
except subprocess.CalledProcessError as e:
|
|
108
|
+
raise RuntimeError(f"Failed to create virtual environment: {e.stderr}") from e
|
|
109
|
+
|
|
110
|
+
def _cleanup_environment(self) -> None:
|
|
111
|
+
"""Clean up the virtual environment."""
|
|
112
|
+
# Note: We intentionally don't clean up the venv here to allow
|
|
113
|
+
# tools to continue using it after the context exits.
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
def get_executable(self) -> str:
|
|
117
|
+
"""Get the path to the Python executable in the virtual environment."""
|
|
118
|
+
if not self._executable or not os.path.exists(self._executable):
|
|
119
|
+
raise RuntimeError("Virtual environment Python executable not found")
|
|
120
|
+
return self._executable
|
|
121
|
+
|
|
122
|
+
async def execute_code(self, code: str, script_path: str, timeout: int = 30) -> dict[str, Any]:
|
|
123
|
+
"""Execute code in the virtual environment."""
|
|
124
|
+
try:
|
|
125
|
+
# Get the Python executable
|
|
126
|
+
python_executable = self.get_executable()
|
|
127
|
+
|
|
128
|
+
# Verify the executable exists
|
|
129
|
+
if not os.path.exists(python_executable):
|
|
130
|
+
return {"success": False, "error": f"Python executable not found at {python_executable}"}
|
|
131
|
+
|
|
132
|
+
# Ensure the directory for the script exists
|
|
133
|
+
script_dir = os.path.dirname(script_path)
|
|
134
|
+
if script_dir:
|
|
135
|
+
os.makedirs(script_dir, exist_ok=True)
|
|
136
|
+
|
|
137
|
+
# Write the code to the script file using asyncify (from base class)
|
|
138
|
+
await asyncify(self._write_to_file)(script_path, code)
|
|
139
|
+
|
|
140
|
+
logging.info(f"Wrote code to {script_path}")
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
# Execute directly with subprocess using asyncify for better reliability
|
|
144
|
+
result = await asyncify(self._run_subprocess)([python_executable, script_path], timeout)
|
|
145
|
+
|
|
146
|
+
# Main execution result
|
|
147
|
+
return {
|
|
148
|
+
"success": result.returncode == 0,
|
|
149
|
+
"stdout": result.stdout,
|
|
150
|
+
"stderr": result.stderr,
|
|
151
|
+
"returncode": result.returncode,
|
|
152
|
+
}
|
|
153
|
+
except subprocess.TimeoutExpired:
|
|
154
|
+
return {"success": False, "error": f"Execution timed out after {timeout} seconds"}
|
|
155
|
+
|
|
156
|
+
except Exception as e:
|
|
157
|
+
return {"success": False, "error": f"Execution error: {str(e)}"}
|
|
158
|
+
|
|
159
|
+
def _get_python_executable_for_version(self) -> str:
|
|
160
|
+
"""Get the Python executable for the specified version and verify it can create a venv."""
|
|
161
|
+
# If a specific path is provided, use it directly
|
|
162
|
+
if self.python_path:
|
|
163
|
+
if not os.path.exists(self.python_path) or not os.access(self.python_path, os.X_OK):
|
|
164
|
+
raise RuntimeError(f"Python executable not found at {self.python_path}")
|
|
165
|
+
return self.python_path
|
|
166
|
+
|
|
167
|
+
# If no specific version is requested, use the current Python
|
|
168
|
+
if not self.python_version:
|
|
169
|
+
return sys.executable
|
|
170
|
+
|
|
171
|
+
potential_executables = []
|
|
172
|
+
|
|
173
|
+
# Try to find a specific Python version using pyenv if available
|
|
174
|
+
try:
|
|
175
|
+
pyenv_result = subprocess.run(
|
|
176
|
+
["pyenv", "which", f"python{self.python_version}"],
|
|
177
|
+
check=True,
|
|
178
|
+
stdout=subprocess.PIPE,
|
|
179
|
+
stderr=subprocess.PIPE,
|
|
180
|
+
text=True,
|
|
181
|
+
)
|
|
182
|
+
potential_executables.append(pyenv_result.stdout.strip())
|
|
183
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
184
|
+
pass
|
|
185
|
+
|
|
186
|
+
# Try common system paths based on platform
|
|
187
|
+
if os.name == "nt": # Windows
|
|
188
|
+
potential_executables.extend([
|
|
189
|
+
f"C:\\Python{self.python_version.replace('.', '')}\\python.exe",
|
|
190
|
+
f"C:\\Program Files\\Python{self.python_version.replace('.', '')}\\python.exe",
|
|
191
|
+
f"C:\\Program Files (x86)\\Python{self.python_version.replace('.', '')}\\python.exe",
|
|
192
|
+
])
|
|
193
|
+
else: # Unix-like (Mac and Linux)
|
|
194
|
+
# Add more paths that might exist on macOS
|
|
195
|
+
potential_executables.extend([
|
|
196
|
+
f"/usr/bin/python{self.python_version}",
|
|
197
|
+
f"/usr/local/bin/python{self.python_version}",
|
|
198
|
+
f"/opt/homebrew/bin/python{self.python_version}", # Homebrew on Apple Silicon
|
|
199
|
+
f"/opt/python/bin/python{self.python_version}",
|
|
200
|
+
])
|
|
201
|
+
|
|
202
|
+
# Try each potential path and verify it can create a venv
|
|
203
|
+
for path in potential_executables:
|
|
204
|
+
if os.path.exists(path) and os.access(path, os.X_OK):
|
|
205
|
+
# Verify this Python can create a venv
|
|
206
|
+
try:
|
|
207
|
+
test_result = subprocess.run(
|
|
208
|
+
[path, "-m", "venv", "--help"],
|
|
209
|
+
check=False, # Don't raise exception
|
|
210
|
+
stdout=subprocess.PIPE,
|
|
211
|
+
stderr=subprocess.PIPE,
|
|
212
|
+
text=True,
|
|
213
|
+
timeout=5, # Add timeout for safety
|
|
214
|
+
)
|
|
215
|
+
if test_result.returncode == 0:
|
|
216
|
+
# Successfully found a valid Python executable
|
|
217
|
+
return path
|
|
218
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
219
|
+
continue
|
|
220
|
+
|
|
221
|
+
# If we couldn't find the specified version, raise an exception
|
|
222
|
+
raise RuntimeError(
|
|
223
|
+
f"Python {self.python_version} not found or cannot create virtual environments. Provide a python_path to use a specific Python executable."
|
|
224
|
+
)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import tempfile
|
|
9
|
+
from contextvars import ContextVar
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
__all__ = ["WorkingDirectory"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class WorkingDirectory:
|
|
16
|
+
"""Context manager for changing the current working directory."""
|
|
17
|
+
|
|
18
|
+
_current_working_directory: ContextVar["WorkingDirectory"] = ContextVar("_current_working_directory")
|
|
19
|
+
|
|
20
|
+
def __init__(self, path: str):
|
|
21
|
+
"""
|
|
22
|
+
Initialize with a directory path.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
path: The directory path to change to.
|
|
26
|
+
"""
|
|
27
|
+
self.path = path
|
|
28
|
+
self.original_path = None
|
|
29
|
+
self.created_tmp = False
|
|
30
|
+
self._token = None
|
|
31
|
+
|
|
32
|
+
def __enter__(self):
|
|
33
|
+
"""Change to the specified directory and return self."""
|
|
34
|
+
self.original_path = os.getcwd()
|
|
35
|
+
if self.path:
|
|
36
|
+
os.makedirs(self.path, exist_ok=True)
|
|
37
|
+
os.chdir(self.path)
|
|
38
|
+
|
|
39
|
+
# Set this as the current working directory in the context
|
|
40
|
+
self._token = WorkingDirectory._current_working_directory.set(self)
|
|
41
|
+
|
|
42
|
+
return self
|
|
43
|
+
|
|
44
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
45
|
+
"""Change back to the original directory and clean up if necessary."""
|
|
46
|
+
# Reset the context variable if this was the active working directory
|
|
47
|
+
if self._token is not None:
|
|
48
|
+
WorkingDirectory._current_working_directory.reset(self._token)
|
|
49
|
+
self._token = None
|
|
50
|
+
|
|
51
|
+
if self.original_path:
|
|
52
|
+
os.chdir(self.original_path)
|
|
53
|
+
if self.created_tmp and self.path and os.path.exists(self.path):
|
|
54
|
+
with contextlib.suppress(Exception):
|
|
55
|
+
shutil.rmtree(self.path)
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def create_tmp(cls):
|
|
59
|
+
"""Create a temporary directory and return a WorkingDirectory instance for it."""
|
|
60
|
+
tmp_dir = tempfile.mkdtemp(prefix="ag2_work_dir_")
|
|
61
|
+
instance = cls(tmp_dir)
|
|
62
|
+
instance.created_tmp = True
|
|
63
|
+
return instance
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def get_current_working_directory(
|
|
67
|
+
cls, working_directory: Optional["WorkingDirectory"] = None
|
|
68
|
+
) -> Optional["WorkingDirectory"]:
|
|
69
|
+
"""Get the current working directory or the specified one if provided."""
|
|
70
|
+
if working_directory is not None:
|
|
71
|
+
return working_directory
|
|
72
|
+
try:
|
|
73
|
+
return cls._current_working_directory.get()
|
|
74
|
+
except LookupError:
|
|
75
|
+
return None
|
autogen/llm_config.py
CHANGED
|
@@ -9,7 +9,7 @@ from abc import ABC, abstractmethod
|
|
|
9
9
|
from collections.abc import Iterable
|
|
10
10
|
from contextvars import ContextVar
|
|
11
11
|
from pathlib import Path
|
|
12
|
-
from typing import TYPE_CHECKING, Annotated, Any, Mapping, Optional, Type, TypeVar, Union
|
|
12
|
+
from typing import TYPE_CHECKING, Annotated, Any, Literal, Mapping, Optional, Type, TypeVar, Union
|
|
13
13
|
|
|
14
14
|
from httpx import Client as httpxClient
|
|
15
15
|
from pydantic import BaseModel, ConfigDict, Field, HttpUrl, SecretStr, ValidationInfo, field_serializer, field_validator
|
|
@@ -268,6 +268,7 @@ class LLMConfig(metaclass=MetaLLMConfig):
|
|
|
268
268
|
list[Annotated[Union[llm_config_classes], Field(discriminator="api_type")]],
|
|
269
269
|
Field(default_factory=list, min_length=1),
|
|
270
270
|
]
|
|
271
|
+
routing_method: Optional[Literal["fixed_order", "round_robin"]] = None
|
|
271
272
|
|
|
272
273
|
# Following field is configuration for pydantic to disallow extra fields
|
|
273
274
|
model_config = ConfigDict(extra="forbid")
|
|
@@ -302,13 +303,15 @@ class LLMConfigEntry(BaseModel, ABC):
|
|
|
302
303
|
@field_validator("base_url", mode="before")
|
|
303
304
|
@classmethod
|
|
304
305
|
def check_base_url(cls, v: Any, info: ValidationInfo) -> Any:
|
|
306
|
+
if v is None: # Handle None case explicitly
|
|
307
|
+
return None
|
|
305
308
|
if not str(v).startswith("https://") and not str(v).startswith("http://"):
|
|
306
309
|
v = f"http://{str(v)}"
|
|
307
310
|
return v
|
|
308
311
|
|
|
309
|
-
@field_serializer("base_url")
|
|
312
|
+
@field_serializer("base_url", when_used="unless-none") # Ensure serializer also respects None
|
|
310
313
|
def serialize_base_url(self, v: Any) -> Any:
|
|
311
|
-
return str(v)
|
|
314
|
+
return str(v) if v is not None else None
|
|
312
315
|
|
|
313
316
|
@field_serializer("api_key", when_used="unless-none")
|
|
314
317
|
def serialize_api_key(self, v: SecretStr) -> Any:
|
|
@@ -129,12 +129,12 @@ class MCPProxy:
|
|
|
129
129
|
return mcp
|
|
130
130
|
|
|
131
131
|
def _process_params(
|
|
132
|
-
self,
|
|
132
|
+
self, process_path: str, func: Callable[[Any], Any], **kwargs: Any
|
|
133
133
|
) -> tuple[str, dict[str, Any], dict[str, Any]]:
|
|
134
|
-
|
|
135
|
-
q_params, path_params, body, security = MCPProxy._get_params(
|
|
134
|
+
process_path = MCPProxy._convert_camel_case_within_braces_to_snake(process_path)
|
|
135
|
+
q_params, path_params, body, security = MCPProxy._get_params(process_path, func)
|
|
136
136
|
|
|
137
|
-
expanded_path =
|
|
137
|
+
expanded_path = process_path.format(**{p: kwargs[p] for p in path_params})
|
|
138
138
|
|
|
139
139
|
url = self._servers[0]["url"] + expanded_path
|
|
140
140
|
|
autogen/oai/client.py
CHANGED
|
@@ -806,17 +806,29 @@ class OpenAIWrapper:
|
|
|
806
806
|
self._clients: list[ModelClient] = []
|
|
807
807
|
self._config_list: list[dict[str, Any]] = []
|
|
808
808
|
|
|
809
|
+
# Determine routing_method from base_config only.
|
|
810
|
+
self.routing_method = base_config.get("routing_method") or "fixed_order"
|
|
811
|
+
self._round_robin_index = 0
|
|
812
|
+
|
|
813
|
+
# Remove routing_method from extra_kwargs after it has been used to set self.routing_method
|
|
814
|
+
# This ensures it's not part of the individual client configurations that are based on extra_kwargs.
|
|
815
|
+
extra_kwargs.pop("routing_method", None)
|
|
816
|
+
|
|
809
817
|
if config_list:
|
|
810
818
|
config_list = [config.copy() for config in config_list] # make a copy before modifying
|
|
811
|
-
for
|
|
812
|
-
self._register_default_client(
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
}
|
|
819
|
+
for config_item in config_list:
|
|
820
|
+
self._register_default_client(config_item, openai_config)
|
|
821
|
+
# Construct current_config_extra_kwargs using the cleaned extra_kwargs
|
|
822
|
+
# (which doesn't have routing_method from base_config)
|
|
823
|
+
# and specific non-openai kwargs from config_item.
|
|
824
|
+
config_item_specific_extras = {k: v for k, v in config_item.items() if k not in self.openai_kwargs}
|
|
825
|
+
self._config_list.append({**extra_kwargs, **config_item_specific_extras})
|
|
817
826
|
else:
|
|
827
|
+
# For a single config passed via base_config (already in extra_kwargs)
|
|
818
828
|
self._register_default_client(extra_kwargs, openai_config)
|
|
829
|
+
# extra_kwargs has already had routing_method popped.
|
|
819
830
|
self._config_list = [extra_kwargs]
|
|
831
|
+
|
|
820
832
|
self.wrapper_id = id(self)
|
|
821
833
|
|
|
822
834
|
def _separate_openai_config(self, config: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
@@ -1074,7 +1086,16 @@ class OpenAIWrapper:
|
|
|
1074
1086
|
raise RuntimeError(
|
|
1075
1087
|
f"Model client(s) {non_activated} are not activated. Please register the custom model clients using `register_model_client` or filter them out form the config list."
|
|
1076
1088
|
)
|
|
1077
|
-
|
|
1089
|
+
|
|
1090
|
+
ordered_clients_indices = list(range(len(self._clients)))
|
|
1091
|
+
if self.routing_method == "round_robin" and len(self._clients) > 0:
|
|
1092
|
+
ordered_clients_indices = (
|
|
1093
|
+
ordered_clients_indices[self._round_robin_index :] + ordered_clients_indices[: self._round_robin_index]
|
|
1094
|
+
)
|
|
1095
|
+
self._round_robin_index = (self._round_robin_index + 1) % len(self._clients)
|
|
1096
|
+
|
|
1097
|
+
for i in ordered_clients_indices:
|
|
1098
|
+
client = self._clients[i]
|
|
1078
1099
|
# merge the input config with the i-th config in the config list
|
|
1079
1100
|
full_config = {**config, **self._config_list[i]}
|
|
1080
1101
|
# separate the config into create_config and extra_kwargs
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
|
|
5
5
|
from .browser_use import BrowserUseTool
|
|
6
|
+
from .code_execution import PythonCodeExecutionTool
|
|
6
7
|
from .crawl4ai import Crawl4AITool
|
|
7
8
|
from .deep_research import DeepResearchTool
|
|
8
9
|
from .duckduckgo import DuckDuckGoSearchTool
|
|
@@ -34,6 +35,7 @@ __all__ = [
|
|
|
34
35
|
"FirecrawlTool",
|
|
35
36
|
"GoogleSearchTool",
|
|
36
37
|
"PerplexitySearchTool",
|
|
38
|
+
"PythonCodeExecutionTool",
|
|
37
39
|
"ReliableTool",
|
|
38
40
|
"ReliableToolError",
|
|
39
41
|
"SearxngSearchTool",
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import tempfile
|
|
7
|
+
from typing import Annotated, Any, Optional
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
from ....doc_utils import export_module
|
|
12
|
+
from ....environments import WorkingDirectory
|
|
13
|
+
from ....environments.python_environment import PythonEnvironment
|
|
14
|
+
from ... import Tool
|
|
15
|
+
|
|
16
|
+
__all__ = ["PythonCodeExecutionTool"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@export_module("autogen.tools.experimental")
|
|
20
|
+
class PythonCodeExecutionTool(Tool):
|
|
21
|
+
"""Executes Python code in a given environment and returns the result."""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
*,
|
|
26
|
+
timeout: int = 30,
|
|
27
|
+
working_directory: Optional[WorkingDirectory] = None,
|
|
28
|
+
python_environment: Optional[PythonEnvironment] = None,
|
|
29
|
+
) -> None:
|
|
30
|
+
"""
|
|
31
|
+
Initialize the PythonCodeExecutionTool.
|
|
32
|
+
|
|
33
|
+
**CAUTION**: If provided a local environment, this tool will execute code in your local environment, which can be dangerous if the code is untrusted.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
timeout: Maximum execution time allowed in seconds, will raise a TimeoutError exception if exceeded.
|
|
37
|
+
working_directory: Optional WorkingDirectory context manager to use.
|
|
38
|
+
python_environment: Optional PythonEnvironment to use. If None, will auto-detect or create based on other parameters.
|
|
39
|
+
"""
|
|
40
|
+
# Store configuration parameters
|
|
41
|
+
self.timeout = timeout
|
|
42
|
+
self.working_directory = WorkingDirectory.get_current_working_directory(working_directory)
|
|
43
|
+
tool_python_environment = PythonEnvironment.get_current_python_environment(python_environment)
|
|
44
|
+
|
|
45
|
+
assert self.working_directory, "No Working directory found"
|
|
46
|
+
assert tool_python_environment, "No Python environment found"
|
|
47
|
+
|
|
48
|
+
self.python_environment = tool_python_environment
|
|
49
|
+
|
|
50
|
+
# Pydantic model to contain the code and list of libraries to execute
|
|
51
|
+
class CodeExecutionRequest(BaseModel):
|
|
52
|
+
code: Annotated[str, Field(description="Python code to execute")]
|
|
53
|
+
libraries: Annotated[list[str], Field(description="List of libraries to install before execution")]
|
|
54
|
+
|
|
55
|
+
# The tool function, this is what goes to the LLM
|
|
56
|
+
async def execute_python_code(
|
|
57
|
+
code_execution_request: Annotated[CodeExecutionRequest, "Python code and the libraries required"],
|
|
58
|
+
) -> dict[str, Any]:
|
|
59
|
+
"""
|
|
60
|
+
Executes Python code in the attached environment and returns the result.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
code_execution_request (CodeExecutionRequest): The Python code and libraries to execute
|
|
64
|
+
"""
|
|
65
|
+
code = code_execution_request.code
|
|
66
|
+
|
|
67
|
+
# NOTE: Libraries are not installed (something to consider for future versions)
|
|
68
|
+
|
|
69
|
+
# Prepare a script file path
|
|
70
|
+
script_dir = self._get_script_directory()
|
|
71
|
+
script_path = os.path.join(script_dir, "script.py")
|
|
72
|
+
|
|
73
|
+
# Execute the code
|
|
74
|
+
return await self.python_environment.execute_code(code=code, script_path=script_path, timeout=self.timeout)
|
|
75
|
+
|
|
76
|
+
super().__init__(
|
|
77
|
+
name="python_execute_code",
|
|
78
|
+
description="Executes Python code and returns the result.",
|
|
79
|
+
func_or_tool=execute_python_code,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def _get_script_directory(self) -> str:
|
|
83
|
+
"""Get the directory to use for scripts."""
|
|
84
|
+
if self.working_directory and hasattr(self.working_directory, "path") and self.working_directory.path:
|
|
85
|
+
path = self.working_directory.path
|
|
86
|
+
os.makedirs(path, exist_ok=True)
|
|
87
|
+
return path
|
|
88
|
+
return tempfile.mkdtemp(prefix="ag2_script_dir_")
|
autogen/version.py
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|