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 <lpinheiro@microsoft.com>
This commit is contained in:
Leonardo Pinheiro 2024-10-12 02:28:15 +10:00 committed by GitHub
parent e1e9d19cb4
commit c765a34cbf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 217 additions and 85 deletions

View File

@ -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",

View File

@ -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)

View File

@ -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."

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -23,7 +23,6 @@ dependencies = [
"grpcio~=1.62.0",
"protobuf~=4.25.1",
"tiktoken",
"docker~=7.0",
"opentelemetry-api~=1.27.0",
"asyncio_atexit"
]

View File

@ -20,7 +20,6 @@ from typing import Dict, List
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 @@ from autogen_core.components.models import (
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

View File

@ -10,9 +10,8 @@ from ._func_with_reqs import (
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 @@ __all__ = [
"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",
]

View File

@ -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
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 @@ print("hello world")
@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 @@ print("hello world")
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

View File

@ -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

View File

@ -0,0 +1,3 @@
from ._impl import DockerCommandLineCodeExecutor
__all__ = ["DockerCommandLineCodeExecutor"]

View File

@ -18,16 +18,19 @@ import asyncio_atexit
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

View File

@ -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

View File

@ -10,7 +10,8 @@ import logging
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

View File

@ -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" },
]