From c765a34cbf5f551c010becbdad6b4990866a9b4a Mon Sep 17 00:00:00 2001 From: Leonardo Pinheiro Date: Sat, 12 Oct 2024 02:28:15 +1000 Subject: [PATCH] Move docker code exec to autogen-ext (#3733) * move docker code exec to autogen-ext * fix test * rename docker subpackage * add missing renamed package --------- Co-authored-by: Leonardo Pinheiro --- .../guides/code-execution.ipynb | 2 +- .../agentchat-user-guide/stocksnippet.md | 2 +- .../cookbook/tool-use-with-intervention.ipynb | 6 +- .../command-line-code-executors.ipynb | 7 +- .../core-user-guide/framework/tools.ipynb | 4 +- .../core-user-guide/quickstart.ipynb | 2 +- python/packages/autogen-core/pyproject.toml | 1 - .../autogen-core/samples/coding_pub_sub.py | 2 +- .../components/code_executor/__init__.py | 6 +- .../test_commandline_code_executor.py | 67 +------ python/packages/autogen-ext/pyproject.toml | 6 + .../code_executor/docker_executor/__init__.py | 3 + .../code_executor/docker_executor/_impl.py} | 19 +- .../test_docker_commandline_code_executor.py | 166 ++++++++++++++++++ .../examples/example_coder.py | 3 +- python/uv.lock | 6 +- 16 files changed, 217 insertions(+), 85 deletions(-) create mode 100644 python/packages/autogen-ext/src/autogen_ext/code_executor/docker_executor/__init__.py rename python/packages/{autogen-core/src/autogen_core/components/code_executor/_impl/docker_command_line_code_executor.py => autogen-ext/src/autogen_ext/code_executor/docker_executor/_impl.py} (96%) create mode 100644 python/packages/autogen-ext/tests/code_executors/test_docker_commandline_code_executor.py diff --git a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/guides/code-execution.ipynb b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/guides/code-execution.ipynb index a9334e2b0df3..eb9841b2f685 100644 --- a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/guides/code-execution.ipynb +++ b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/guides/code-execution.ipynb @@ -315,8 +315,8 @@ "source": [ "from autogen_agentchat.agents import CodeExecutorAgent, CodingAssistantAgent\n", "from autogen_agentchat.teams import RoundRobinGroupChat, StopMessageTermination\n", - "from autogen_core.components.code_executor import DockerCommandLineCodeExecutor\n", "from autogen_core.components.models import OpenAIChatCompletionClient\n", + "from autogen_ext.code_executor.docker_executor import DockerCommandLineCodeExecutor\n", "\n", "async with DockerCommandLineCodeExecutor(work_dir=\"coding\") as code_executor: # type: ignore[syntax]\n", " code_executor_agent = CodeExecutorAgent(\"code_executor\", code_executor=code_executor)\n", diff --git a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/stocksnippet.md b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/stocksnippet.md index 9175ebd7ac09..689aae889587 100644 --- a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/stocksnippet.md +++ b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/stocksnippet.md @@ -8,7 +8,7 @@ from autogen_agentchat import EVENT_LOGGER_NAME from autogen_agentchat.agents import CodeExecutorAgent, CodingAssistantAgent from autogen_agentchat.logging import ConsoleLogHandler from autogen_agentchat.teams import RoundRobinGroupChat, StopMessageTermination -from autogen_core.components.code_executor import DockerCommandLineCodeExecutor +from autogen_ext.code_executor.docker_executor import DockerCommandLineCodeExecutor from autogen_core.components.models import OpenAIChatCompletionClient logger = logging.getLogger(EVENT_LOGGER_NAME) diff --git a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/cookbook/tool-use-with-intervention.ipynb b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/cookbook/tool-use-with-intervention.ipynb index 1ddee2a0ed15..84a45f84ee8f 100644 --- a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/cookbook/tool-use-with-intervention.ipynb +++ b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/cookbook/tool-use-with-intervention.ipynb @@ -23,7 +23,6 @@ "from autogen_core.base import AgentId, AgentType, MessageContext\n", "from autogen_core.base.intervention import DefaultInterventionHandler, DropMessage\n", "from autogen_core.components import FunctionCall, RoutedAgent, message_handler\n", - "from autogen_core.components.code_executor import DockerCommandLineCodeExecutor\n", "from autogen_core.components.models import (\n", " ChatCompletionClient,\n", " LLMMessage,\n", @@ -32,7 +31,8 @@ " UserMessage,\n", ")\n", "from autogen_core.components.tool_agent import ToolAgent, ToolException, tool_agent_caller_loop\n", - "from autogen_core.components.tools import PythonCodeExecutionTool, ToolSchema" + "from autogen_core.components.tools import PythonCodeExecutionTool, ToolSchema\n", + "from autogen_ext.code_executor.docker_executor import DockerCommandLineCodeExecutor" ] }, { @@ -157,7 +157,7 @@ "source": [ "In this example, we will use a tool for Python code execution.\n", "First, we create a Docker-based command-line code executor\n", - "using {py:class}`~autogen_core.components.code_executor.DockerCommandLineCodeExecutor`,\n", + "using {py:class}`~autogen_core.components.code_executor.docker_executorCommandLineCodeExecutor`,\n", "and then use it to instantiate a built-in Python code execution tool\n", "{py:class}`~autogen_core.components.tools.PythonCodeExecutionTool`\n", "that runs code in a Docker container." diff --git a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/framework/command-line-code-executors.ipynb b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/framework/command-line-code-executors.ipynb index e6e8a3be3095..8cdb529e2120 100644 --- a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/framework/command-line-code-executors.ipynb +++ b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/framework/command-line-code-executors.ipynb @@ -10,12 +10,12 @@ "Generally speaking, it will save each code block to a file and the execute that file.\n", "This means that each code block is executed in a new process. There are two forms of this executor:\n", "\n", - "- Docker ({py:class}`~autogen_core.components.code_executor.DockerCommandLineCodeExecutor`) - this is where all commands are executed in a Docker container\n", + "- Docker ({py:class}`~autogen_ext.code_executor.docker_executor.DockerCommandLineCodeExecutor`) - this is where all commands are executed in a Docker container\n", "- Local ({py:class}`~autogen_core.components.code_executor.LocalCommandLineCodeExecutor`) - this is where all commands are executed on the host machine\n", "\n", "## Docker\n", "\n", - "The {py:class}`~autogen_core.components.code_executor.DockerCommandLineCodeExecutor` will create a Docker container and run all commands within that container. \n", + "The {py:class}`~autogen_ext.code_executor.docker_executor.DockerCommandLineCodeExecutor` will create a Docker container and run all commands within that container. \n", "The default image that is used is `python:3-slim`, this can be customized by passing the `image` parameter to the constructor. \n", "If the image is not found locally then the class will try to pull it. \n", "Therefore, having built the image locally is enough. The only thing required for this image to be compatible with the executor is to have `sh` and `python` installed. \n", @@ -50,7 +50,8 @@ "from pathlib import Path\n", "\n", "from autogen_core.base import CancellationToken\n", - "from autogen_core.components.code_executor import CodeBlock, DockerCommandLineCodeExecutor\n", + "from autogen_core.components.code_executor import CodeBlock\n", + "from autogen_ext.code_executor.docker_executor import DockerCommandLineCodeExecutor\n", "\n", "work_dir = Path(\"coding\")\n", "work_dir.mkdir(exist_ok=True)\n", diff --git a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/framework/tools.ipynb b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/framework/tools.ipynb index dd0d0769af6e..bce62e45e7ce 100644 --- a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/framework/tools.ipynb +++ b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/framework/tools.ipynb @@ -44,8 +44,8 @@ ], "source": [ "from autogen_core.base import CancellationToken\n", - "from autogen_core.components.code_executor import DockerCommandLineCodeExecutor\n", "from autogen_core.components.tools import PythonCodeExecutionTool\n", + "from autogen_ext.code_executor.docker_executor import DockerCommandLineCodeExecutor\n", "\n", "# Create the tool.\n", "code_executor = DockerCommandLineCodeExecutor()\n", @@ -63,7 +63,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The {py:class}`~autogen_core.components.code_executor.DockerCommandLineCodeExecutor`\n", + "The {py:class}`~autogen_core.components.code_executor.docker_executorCommandLineCodeExecutor`\n", "class is a built-in code executor that runs Python code snippets in a subprocess\n", "in the local command line environment.\n", "The {py:class}`~autogen_core.components.tools.PythonCodeExecutionTool` class wraps the code executor\n", diff --git a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/quickstart.ipynb b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/quickstart.ipynb index cc489f6f76ea..034504eb326a 100644 --- a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/quickstart.ipynb +++ b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/quickstart.ipynb @@ -312,8 +312,8 @@ "import tempfile\n", "\n", "from autogen_core.application import SingleThreadedAgentRuntime\n", - "from autogen_core.components.code_executor import DockerCommandLineCodeExecutor\n", "from autogen_core.components.models import OpenAIChatCompletionClient\n", + "from autogen_ext.code_executor.docker_executor import DockerCommandLineCodeExecutor\n", "\n", "work_dir = tempfile.mkdtemp()\n", "\n", diff --git a/python/packages/autogen-core/pyproject.toml b/python/packages/autogen-core/pyproject.toml index 614842c85110..a83f3e4a01d5 100644 --- a/python/packages/autogen-core/pyproject.toml +++ b/python/packages/autogen-core/pyproject.toml @@ -23,7 +23,6 @@ dependencies = [ "grpcio~=1.62.0", "protobuf~=4.25.1", "tiktoken", - "docker~=7.0", "opentelemetry-api~=1.27.0", "asyncio_atexit" ] diff --git a/python/packages/autogen-core/samples/coding_pub_sub.py b/python/packages/autogen-core/samples/coding_pub_sub.py index f7ba249768de..68090d556a6f 100644 --- a/python/packages/autogen-core/samples/coding_pub_sub.py +++ b/python/packages/autogen-core/samples/coding_pub_sub.py @@ -20,7 +20,6 @@ from autogen_core.application import SingleThreadedAgentRuntime from autogen_core.base import MessageContext from autogen_core.components import DefaultSubscription, DefaultTopicId, FunctionCall, RoutedAgent, message_handler -from autogen_core.components.code_executor import DockerCommandLineCodeExecutor from autogen_core.components.models import ( AssistantMessage, ChatCompletionClient, @@ -31,6 +30,7 @@ UserMessage, ) from autogen_core.components.tools import PythonCodeExecutionTool, Tool +from autogen_ext.code_executor.docker_executor import DockerCommandLineCodeExecutor from common.utils import get_chat_completion_client_from_envs diff --git a/python/packages/autogen-core/src/autogen_core/components/code_executor/__init__.py b/python/packages/autogen-core/src/autogen_core/components/code_executor/__init__.py index c75bb080d612..c09ee8b799f9 100644 --- a/python/packages/autogen-core/src/autogen_core/components/code_executor/__init__.py +++ b/python/packages/autogen-core/src/autogen_core/components/code_executor/__init__.py @@ -10,9 +10,8 @@ with_requirements, ) from ._impl.command_line_code_result import CommandLineCodeResult -from ._impl.docker_command_line_code_executor import DockerCommandLineCodeExecutor from ._impl.local_commandline_code_executor import LocalCommandLineCodeExecutor -from ._impl.utils import get_required_packages, lang_to_cmd +from ._impl.utils import get_file_name_from_content, get_required_packages, lang_to_cmd, silence_pip from ._utils import extract_markdown_code_blocks __all__ = [ @@ -31,7 +30,8 @@ "extract_markdown_code_blocks", "get_required_packages", "build_python_functions_file", - "DockerCommandLineCodeExecutor", "get_required_packages", "lang_to_cmd", + "get_file_name_from_content", + "silence_pip", ] diff --git a/python/packages/autogen-core/tests/execution/test_commandline_code_executor.py b/python/packages/autogen-core/tests/execution/test_commandline_code_executor.py index 16582ff5ed27..bb3ff2830958 100644 --- a/python/packages/autogen-core/tests/execution/test_commandline_code_executor.py +++ b/python/packages/autogen-core/tests/execution/test_commandline_code_executor.py @@ -2,7 +2,6 @@ # Credit to original authors import asyncio -import os import sys import tempfile from pathlib import Path @@ -12,48 +11,22 @@ import pytest_asyncio from aiofiles import open from autogen_core.base import CancellationToken -from autogen_core.components.code_executor import CodeBlock, DockerCommandLineCodeExecutor, LocalCommandLineCodeExecutor - - -def docker_tests_enabled() -> bool: - if os.environ.get("SKIP_DOCKER", "unset").lower() == "true": - return False - - try: - import docker - from docker.errors import DockerException - except ImportError: - return False - - try: - client = docker.from_env() - client.ping() # type: ignore - return True - except DockerException: - return False +from autogen_core.components.code_executor import CodeBlock, LocalCommandLineCodeExecutor @pytest_asyncio.fixture(scope="function") # type: ignore async def executor_and_temp_dir( request: pytest.FixtureRequest, -) -> AsyncGenerator[tuple[LocalCommandLineCodeExecutor | DockerCommandLineCodeExecutor, str], None]: - if request.param == "local": - with tempfile.TemporaryDirectory() as temp_dir: - yield LocalCommandLineCodeExecutor(work_dir=temp_dir), temp_dir - elif request.param == "docker": - if not docker_tests_enabled(): - pytest.skip("Docker tests are disabled") - - with tempfile.TemporaryDirectory() as temp_dir: - async with DockerCommandLineCodeExecutor(work_dir=temp_dir) as executor: - yield executor, temp_dir +) -> AsyncGenerator[tuple[LocalCommandLineCodeExecutor, str], None]: + with tempfile.TemporaryDirectory() as temp_dir: + yield LocalCommandLineCodeExecutor(work_dir=temp_dir), temp_dir -ExecutorFixture: TypeAlias = tuple[LocalCommandLineCodeExecutor | DockerCommandLineCodeExecutor, str] +ExecutorFixture: TypeAlias = tuple[LocalCommandLineCodeExecutor, str] @pytest.mark.asyncio -@pytest.mark.parametrize("executor_and_temp_dir", ["local", "docker"], indirect=True) +@pytest.mark.parametrize("executor_and_temp_dir", ["local"], indirect=True) async def test_execute_code(executor_and_temp_dir: ExecutorFixture) -> None: executor, _temp_dir = executor_and_temp_dir cancellation_token = CancellationToken() @@ -101,7 +74,7 @@ async def test_execute_code(executor_and_temp_dir: ExecutorFixture) -> None: @pytest.mark.asyncio -@pytest.mark.parametrize("executor_and_temp_dir", ["local", "docker"], indirect=True) +@pytest.mark.parametrize("executor_and_temp_dir", ["local"], indirect=True) async def test_commandline_code_executor_timeout(executor_and_temp_dir: ExecutorFixture) -> None: executor, temp_dir = executor_and_temp_dir cancellation_token = CancellationToken() @@ -111,7 +84,6 @@ async def test_commandline_code_executor_timeout(executor_and_temp_dir: Executor assert code_result.exit_code and "Timeout" in code_result.output -# TODO: add docker when cancellation is supported @pytest.mark.asyncio async def test_commandline_code_executor_cancellation() -> None: with tempfile.TemporaryDirectory() as temp_dir: @@ -136,7 +108,7 @@ async def test_local_commandline_code_executor_restart() -> None: @pytest.mark.asyncio -@pytest.mark.parametrize("executor_and_temp_dir", ["local", "docker"], indirect=True) +@pytest.mark.parametrize("executor_and_temp_dir", ["local"], indirect=True) async def test_invalid_relative_path(executor_and_temp_dir: ExecutorFixture) -> None: executor, _temp_dir = executor_and_temp_dir cancellation_token = CancellationToken() @@ -151,7 +123,7 @@ async def test_invalid_relative_path(executor_and_temp_dir: ExecutorFixture) -> @pytest.mark.asyncio -@pytest.mark.parametrize("executor_and_temp_dir", ["local", "docker"], indirect=True) +@pytest.mark.parametrize("executor_and_temp_dir", ["local"], indirect=True) async def test_valid_relative_path(executor_and_temp_dir: ExecutorFixture) -> None: executor, temp_dir_str = executor_and_temp_dir @@ -171,24 +143,3 @@ async def test_valid_relative_path(executor_and_temp_dir: ExecutorFixture) -> No assert "test.py" in result.code_file assert (temp_dir / Path("test.py")).resolve() == Path(result.code_file).resolve() assert (temp_dir / Path("test.py")).exists() - - -@pytest.mark.asyncio -async def test_docker_commandline_code_executor_start_stop() -> None: - if not docker_tests_enabled(): - pytest.skip("Docker tests are disabled") - - with tempfile.TemporaryDirectory() as temp_dir: - executor = DockerCommandLineCodeExecutor(work_dir=temp_dir) - await executor.start() - await executor.stop() - - -@pytest.mark.asyncio -async def test_docker_commandline_code_executor_start_stop_context_manager() -> None: - if not docker_tests_enabled(): - pytest.skip("Docker tests are disabled") - - with tempfile.TemporaryDirectory() as temp_dir: - async with DockerCommandLineCodeExecutor(work_dir=temp_dir) as _exec: - pass diff --git a/python/packages/autogen-ext/pyproject.toml b/python/packages/autogen-ext/pyproject.toml index ee6bcc5e3af6..a290a03eee10 100644 --- a/python/packages/autogen-ext/pyproject.toml +++ b/python/packages/autogen-ext/pyproject.toml @@ -22,6 +22,7 @@ dependencies = [ [project.optional-dependencies] langchain-tools = ["langchain >= 0.3.1"] azure-code-executor = ["azure-core"] +docker-code-executor = ["docker~=7.0"] [tool.hatch.build.targets.wheel] packages = ["src/autogen_ext"] @@ -47,3 +48,8 @@ include = "../../shared_tasks.toml" [tool.poe.tasks] test = "pytest -n auto" + +[tool.mypy] +[[tool.mypy.overrides]] +module = "docker.*" +ignore_missing_imports = true \ No newline at end of file diff --git a/python/packages/autogen-ext/src/autogen_ext/code_executor/docker_executor/__init__.py b/python/packages/autogen-ext/src/autogen_ext/code_executor/docker_executor/__init__.py new file mode 100644 index 000000000000..23c859ffe63b --- /dev/null +++ b/python/packages/autogen-ext/src/autogen_ext/code_executor/docker_executor/__init__.py @@ -0,0 +1,3 @@ +from ._impl import DockerCommandLineCodeExecutor + +__all__ = ["DockerCommandLineCodeExecutor"] diff --git a/python/packages/autogen-core/src/autogen_core/components/code_executor/_impl/docker_command_line_code_executor.py b/python/packages/autogen-ext/src/autogen_ext/code_executor/docker_executor/_impl.py similarity index 96% rename from python/packages/autogen-core/src/autogen_core/components/code_executor/_impl/docker_command_line_code_executor.py rename to python/packages/autogen-ext/src/autogen_ext/code_executor/docker_executor/_impl.py index 346d7c070aaf..7b3a194e6f6a 100644 --- a/python/packages/autogen-core/src/autogen_core/components/code_executor/_impl/docker_command_line_code_executor.py +++ b/python/packages/autogen-ext/src/autogen_ext/code_executor/docker_executor/_impl.py @@ -18,16 +18,19 @@ import docker import docker.models import docker.models.containers -from docker.errors import ImageNotFound, NotFound - -from ....base._cancellation_token import CancellationToken -from ....components.code_executor._base import CodeBlock, CodeExecutor -from ....components.code_executor._func_with_reqs import FunctionWithRequirements, FunctionWithRequirementsStr -from ....components.code_executor._impl.command_line_code_result import CommandLineCodeResult -from .._func_with_reqs import ( +from autogen_core.base import CancellationToken +from autogen_core.components.code_executor import ( + CodeBlock, + CodeExecutor, + CommandLineCodeResult, + FunctionWithRequirements, + FunctionWithRequirementsStr, build_python_functions_file, + get_file_name_from_content, + lang_to_cmd, + silence_pip, ) -from .utils import get_file_name_from_content, lang_to_cmd, silence_pip +from docker.errors import ImageNotFound, NotFound if sys.version_info >= (3, 11): from typing import Self diff --git a/python/packages/autogen-ext/tests/code_executors/test_docker_commandline_code_executor.py b/python/packages/autogen-ext/tests/code_executors/test_docker_commandline_code_executor.py new file mode 100644 index 000000000000..641e5e703316 --- /dev/null +++ b/python/packages/autogen-ext/tests/code_executors/test_docker_commandline_code_executor.py @@ -0,0 +1,166 @@ +# mypy: disable-error-code="no-any-unimported" +import os +import sys +import tempfile +from pathlib import Path +from typing import AsyncGenerator, TypeAlias + +import pytest +import pytest_asyncio +from aiofiles import open +from autogen_core.base import CancellationToken +from autogen_core.components.code_executor import CodeBlock +from autogen_ext.code_executor.docker_executor import DockerCommandLineCodeExecutor + + +def docker_tests_enabled() -> bool: + if os.environ.get("SKIP_DOCKER", "unset").lower() == "true": + return False + + try: + import docker + from docker.errors import DockerException + except ImportError: + return False + + try: + client = docker.from_env() + client.ping() # type: ignore + return True + except DockerException: + return False + + +@pytest_asyncio.fixture(scope="function") # type: ignore +async def executor_and_temp_dir( + request: pytest.FixtureRequest, +) -> AsyncGenerator[tuple[DockerCommandLineCodeExecutor, str], None]: + if not docker_tests_enabled(): + pytest.skip("Docker tests are disabled") + + with tempfile.TemporaryDirectory() as temp_dir: + async with DockerCommandLineCodeExecutor(work_dir=temp_dir) as executor: + yield executor, temp_dir + + +ExecutorFixture: TypeAlias = tuple[DockerCommandLineCodeExecutor, str] + + +@pytest.mark.asyncio +@pytest.mark.parametrize("executor_and_temp_dir", ["docker"], indirect=True) +async def test_execute_code(executor_and_temp_dir: ExecutorFixture) -> None: + executor, _temp_dir = executor_and_temp_dir + cancellation_token = CancellationToken() + + # Test single code block. + code_blocks = [CodeBlock(code="import sys; print('hello world!')", language="python")] + code_result = await executor.execute_code_blocks(code_blocks, cancellation_token) + assert code_result.exit_code == 0 and "hello world!" in code_result.output and code_result.code_file is not None + + # Test multiple code blocks. + code_blocks = [ + CodeBlock(code="import sys; print('hello world!')", language="python"), + CodeBlock(code="a = 100 + 100; print(a)", language="python"), + ] + code_result = await executor.execute_code_blocks(code_blocks, cancellation_token) + assert ( + code_result.exit_code == 0 + and "hello world!" in code_result.output + and "200" in code_result.output + and code_result.code_file is not None + ) + + # Test bash script. + if sys.platform not in ["win32"]: + code_blocks = [CodeBlock(code="echo 'hello world!'", language="bash")] + code_result = await executor.execute_code_blocks(code_blocks, cancellation_token) + assert code_result.exit_code == 0 and "hello world!" in code_result.output and code_result.code_file is not None + + # Test running code. + file_lines = ["import sys", "print('hello world!')", "a = 100 + 100", "print(a)"] + code_blocks = [CodeBlock(code="\n".join(file_lines), language="python")] + code_result = await executor.execute_code_blocks(code_blocks, cancellation_token) + assert ( + code_result.exit_code == 0 + and "hello world!" in code_result.output + and "200" in code_result.output + and code_result.code_file is not None + ) + + # Check saved code file. + async with open(code_result.code_file) as f: + code_lines = await f.readlines() + for file_line, code_line in zip(file_lines, code_lines, strict=False): + assert file_line.strip() == code_line.strip() + + +@pytest.mark.asyncio +@pytest.mark.parametrize("executor_and_temp_dir", ["docker"], indirect=True) +async def test_commandline_code_executor_timeout(executor_and_temp_dir: ExecutorFixture) -> None: + _executor, temp_dir = executor_and_temp_dir + cancellation_token = CancellationToken() + code_blocks = [CodeBlock(code="import time; time.sleep(10); print('hello world!')", language="python")] + + async with DockerCommandLineCodeExecutor(timeout=1, work_dir=temp_dir) as executor: + code_result = await executor.execute_code_blocks(code_blocks, cancellation_token) + + assert code_result.exit_code and "Timeout" in code_result.output + + +@pytest.mark.asyncio +@pytest.mark.parametrize("executor_and_temp_dir", ["docker"], indirect=True) +async def test_invalid_relative_path(executor_and_temp_dir: ExecutorFixture) -> None: + executor, _temp_dir = executor_and_temp_dir + cancellation_token = CancellationToken() + code = """# filename: /tmp/test.py + +print("hello world") +""" + result = await executor.execute_code_blocks( + [CodeBlock(code=code, language="python")], cancellation_token=cancellation_token + ) + assert result.exit_code == 1 and "Filename is not in the workspace" in result.output + + +@pytest.mark.asyncio +@pytest.mark.parametrize("executor_and_temp_dir", ["docker"], indirect=True) +async def test_valid_relative_path(executor_and_temp_dir: ExecutorFixture) -> None: + executor, temp_dir_str = executor_and_temp_dir + + cancellation_token = CancellationToken() + temp_dir = Path(temp_dir_str) + + code = """# filename: test.py + +print("hello world") +""" + result = await executor.execute_code_blocks( + [CodeBlock(code=code, language="python")], cancellation_token=cancellation_token + ) + assert result.exit_code == 0 + assert "hello world" in result.output + assert result.code_file is not None + assert "test.py" in result.code_file + assert (temp_dir / Path("test.py")).resolve() == Path(result.code_file).resolve() + assert (temp_dir / Path("test.py")).exists() + + +@pytest.mark.asyncio +async def test_docker_commandline_code_executor_start_stop() -> None: + if not docker_tests_enabled(): + pytest.skip("Docker tests are disabled") + + with tempfile.TemporaryDirectory() as temp_dir: + executor = DockerCommandLineCodeExecutor(work_dir=temp_dir) + await executor.start() + await executor.stop() + + +@pytest.mark.asyncio +async def test_docker_commandline_code_executor_start_stop_context_manager() -> None: + if not docker_tests_enabled(): + pytest.skip("Docker tests are disabled") + + with tempfile.TemporaryDirectory() as temp_dir: + async with DockerCommandLineCodeExecutor(work_dir=temp_dir) as _exec: + pass diff --git a/python/packages/autogen-magentic-one/examples/example_coder.py b/python/packages/autogen-magentic-one/examples/example_coder.py index c7721a88f996..b1978a13d2eb 100644 --- a/python/packages/autogen-magentic-one/examples/example_coder.py +++ b/python/packages/autogen-magentic-one/examples/example_coder.py @@ -10,7 +10,8 @@ from autogen_core.application import SingleThreadedAgentRuntime from autogen_core.application.logging import EVENT_LOGGER_NAME from autogen_core.base import AgentId, AgentProxy -from autogen_core.components.code_executor import CodeBlock, DockerCommandLineCodeExecutor +from autogen_core.components.code_executor import CodeBlock +from autogen_ext.code_executor.docker_executor import DockerCommandLineCodeExecutor from autogen_magentic_one.agents.coder import Coder, Executor from autogen_magentic_one.agents.orchestrator import RoundRobinOrchestrator from autogen_magentic_one.agents.user_proxy import UserProxy diff --git a/python/uv.lock b/python/uv.lock index bff3697d5c3f..dcee6ef33035 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -359,7 +359,6 @@ source = { editable = "packages/autogen-core" } dependencies = [ { name = "aiohttp" }, { name = "asyncio-atexit" }, - { name = "docker" }, { name = "grpcio" }, { name = "openai" }, { name = "opentelemetry-api" }, @@ -415,7 +414,6 @@ dev = [ requires-dist = [ { name = "aiohttp" }, { name = "asyncio-atexit" }, - { name = "docker", specifier = "~=7.0" }, { name = "grpcio", specifier = "~=1.62.0" }, { name = "openai", specifier = ">=1.3" }, { name = "opentelemetry-api", specifier = "~=1.27.0" }, @@ -479,6 +477,9 @@ dependencies = [ azure-code-executor = [ { name = "azure-core" }, ] +docker-code-executor = [ + { name = "docker" }, +] langchain-tools = [ { name = "langchain" }, ] @@ -487,6 +488,7 @@ langchain-tools = [ requires-dist = [ { name = "autogen-core", editable = "packages/autogen-core" }, { name = "azure-core", marker = "extra == 'azure-code-executor'" }, + { name = "docker", marker = "extra == 'docker-code-executor'", specifier = "~=7.0" }, { name = "langchain", marker = "extra == 'langchain-tools'", specifier = ">=0.3.1" }, ]