mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
fix: pip not available in runtime (#3306)
* try to fix pip unavailable * update test case for pip * force rebuild in CI * remove extra symlink * fix newline * added semi-colon to line 31 * Dockerfile.j2: activate env at the end * Revert "Dockerfile.j2: activate env at the end" This reverts commit cf2f5651021fe80d4ab69a35a85f0a35b29dc3d7. * cleanup Dockerfile * switch default python image * remove image agnostic (no longer used) * fix tests * switch to nikolaik/python-nodejs:python3.11-nodejs22 * fix test * fix test * revert docker * update template --------- Co-authored-by: tobitege <tobitege@gmx.de> Co-authored-by: Graham Neubig <neubig@gmail.com>
This commit is contained in:
parent
00bc68642f
commit
bdf6df12c3
4
.github/workflows/ghcr.yml
vendored
4
.github/workflows/ghcr.yml
vendored
@ -81,7 +81,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
image: ['od_runtime']
|
||||
base_image: ['ubuntu:22.04']
|
||||
base_image: ['nikolaik/python-nodejs:python3.11-nodejs22']
|
||||
platform: ['amd64', 'arm64']
|
||||
steps:
|
||||
- name: Checkout
|
||||
@ -115,7 +115,7 @@ jobs:
|
||||
- name: Install Python dependencies using Poetry
|
||||
run: make install-python-dependencies
|
||||
- name: Create source distribution and Dockerfile
|
||||
run: poetry run python3 opendevin/runtime/utils/runtime_build.py --base_image ${{ matrix.base_image }} --build_folder containers/runtime
|
||||
run: poetry run python3 opendevin/runtime/utils/runtime_build.py --base_image ${{ matrix.base_image }} --build_folder containers/runtime --force_rebuild
|
||||
- name: Build and export image
|
||||
id: build
|
||||
run: |
|
||||
|
||||
@ -174,7 +174,7 @@ llm_config = 'gpt3'
|
||||
#user_id = 1000
|
||||
|
||||
# Container image to use for the sandbox
|
||||
#container_image = "ghcr.io/opendevin/sandbox:main"
|
||||
#container_image = "nikolaik/python-nodejs:python3.11-nodejs22"
|
||||
|
||||
# Use host network
|
||||
#use_host_network = false
|
||||
|
||||
@ -62,7 +62,7 @@ def get_config(
|
||||
runtime='eventstream',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
container_image='ubuntu:22.04',
|
||||
container_image='python:3.11-bookworm',
|
||||
enable_auto_lint=False,
|
||||
use_host_network=False,
|
||||
),
|
||||
|
||||
@ -44,7 +44,7 @@ def get_config(
|
||||
runtime='eventstream',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
container_image='ubuntu:22.04',
|
||||
container_image='python:3.11-bookworm',
|
||||
enable_auto_lint=True,
|
||||
use_host_network=False,
|
||||
),
|
||||
|
||||
@ -75,7 +75,7 @@ def get_config(
|
||||
runtime='eventstream',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
container_image='ubuntu:22.04',
|
||||
container_image='python:3.11-bookworm',
|
||||
enable_auto_lint=True,
|
||||
use_host_network=False,
|
||||
),
|
||||
|
||||
@ -40,7 +40,7 @@ def get_config(
|
||||
runtime='eventstream',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
container_image='ubuntu:22.04',
|
||||
container_image='python:3.11-bookworm',
|
||||
enable_auto_lint=False,
|
||||
use_host_network=False,
|
||||
),
|
||||
|
||||
@ -51,7 +51,7 @@ def get_config(
|
||||
runtime='eventstream',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
container_image='ubuntu:22.04',
|
||||
container_image='python:3.11-bookworm',
|
||||
enable_auto_lint=True,
|
||||
use_host_network=False,
|
||||
),
|
||||
|
||||
@ -42,7 +42,7 @@ def get_config(
|
||||
runtime='eventstream',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
container_image='ubuntu:22.04',
|
||||
container_image='python:3.11-bookworm',
|
||||
enable_auto_lint=True,
|
||||
use_host_network=False,
|
||||
),
|
||||
|
||||
@ -65,7 +65,7 @@ def get_config(
|
||||
runtime='eventstream',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
container_image='ubuntu:22.04',
|
||||
container_image='python:3.11-bookworm',
|
||||
enable_auto_lint=True,
|
||||
use_host_network=False,
|
||||
),
|
||||
|
||||
@ -86,7 +86,7 @@ def get_config(
|
||||
runtime='eventstream',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
container_image='ubuntu:22.04',
|
||||
container_image='python:3.11-bookworm',
|
||||
enable_auto_lint=True,
|
||||
use_host_network=False,
|
||||
),
|
||||
|
||||
@ -45,7 +45,7 @@ def get_config(
|
||||
runtime='eventstream',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
container_image='ubuntu:22.04',
|
||||
container_image='python:3.11-bookworm',
|
||||
enable_auto_lint=True,
|
||||
use_host_network=False,
|
||||
),
|
||||
|
||||
@ -54,7 +54,7 @@ def get_config(
|
||||
runtime='eventstream',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
container_image='ubuntu:22.04',
|
||||
container_image='python:3.11-bookworm',
|
||||
enable_auto_lint=True,
|
||||
use_host_network=False,
|
||||
browsergym_eval_env=env_id,
|
||||
|
||||
@ -166,9 +166,7 @@ class SandboxConfig(metaclass=Singleton):
|
||||
"""
|
||||
|
||||
api_hostname: str = 'localhost'
|
||||
container_image: str = (
|
||||
'ubuntu:22.04' # default to ubuntu:22.04 for eventstream runtime
|
||||
)
|
||||
container_image: str = 'nikolaik/python-nodejs:python3.11-nodejs22' # default to nikolaik/python-nodejs:python3.11-nodejs22 for eventstream runtime
|
||||
user_id: int = os.getuid() if hasattr(os, 'getuid') else 1000
|
||||
timeout: int = 120
|
||||
enable_auto_lint: bool = (
|
||||
|
||||
@ -1,113 +0,0 @@
|
||||
"""This module contains functions for building and managing the agnostic sandbox image.
|
||||
|
||||
This WILL BE DEPRECATED when EventStreamRuntime is fully implemented and adopted.
|
||||
"""
|
||||
|
||||
import tempfile
|
||||
|
||||
import docker
|
||||
|
||||
from opendevin.core.logger import opendevin_logger as logger
|
||||
|
||||
|
||||
def generate_dockerfile(base_image: str) -> str:
|
||||
"""Generate the Dockerfile content for the agnostic sandbox image based on user-provided base image.
|
||||
|
||||
NOTE: This is only tested on debian yet.
|
||||
"""
|
||||
# FIXME: Remove the requirement of ssh in future version
|
||||
dockerfile_content = (
|
||||
f'FROM {base_image}\n'
|
||||
'RUN apt update && apt install -y openssh-server wget sudo\n'
|
||||
'RUN mkdir -p -m0755 /var/run/sshd\n'
|
||||
'RUN mkdir -p /opendevin && mkdir -p /opendevin/logs && chmod 777 /opendevin/logs\n'
|
||||
'RUN echo "" > /opendevin/bash.bashrc\n'
|
||||
'RUN if [ ! -d /opendevin/miniforge3 ]; then \\\n'
|
||||
' wget --progress=bar:force -O Miniforge3.sh "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh" && \\\n'
|
||||
' bash Miniforge3.sh -b -p /opendevin/miniforge3 && \\\n'
|
||||
' rm Miniforge3.sh && \\\n'
|
||||
' chmod -R g+w /opendevin/miniforge3 && \\\n'
|
||||
' bash -c ". /opendevin/miniforge3/etc/profile.d/conda.sh && conda config --set changeps1 False && conda config --append channels conda-forge"; \\\n'
|
||||
' fi\n'
|
||||
'RUN /opendevin/miniforge3/bin/pip install --upgrade pip\n'
|
||||
'RUN /opendevin/miniforge3/bin/pip install jupyterlab notebook jupyter_kernel_gateway flake8\n'
|
||||
'RUN /opendevin/miniforge3/bin/pip install python-docx PyPDF2 python-pptx pylatexenc openai\n'
|
||||
).strip()
|
||||
return dockerfile_content
|
||||
|
||||
|
||||
def _build_sandbox_image(
|
||||
base_image: str, target_image_name: str, docker_client: docker.DockerClient
|
||||
):
|
||||
try:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
dockerfile_content = generate_dockerfile(base_image)
|
||||
|
||||
logger.info(f'Building agnostic sandbox image: {target_image_name}')
|
||||
logger.info(
|
||||
(
|
||||
f'===== Dockerfile content =====\n'
|
||||
f'{dockerfile_content}\n'
|
||||
f'==============================='
|
||||
)
|
||||
)
|
||||
with open(f'{temp_dir}/Dockerfile', 'w') as file:
|
||||
file.write(dockerfile_content)
|
||||
|
||||
api_client = docker_client.api
|
||||
build_logs = api_client.build(
|
||||
path=temp_dir, tag=target_image_name, rm=True, decode=True
|
||||
)
|
||||
|
||||
for log in build_logs:
|
||||
if 'stream' in log:
|
||||
print(log['stream'].strip())
|
||||
elif 'error' in log:
|
||||
logger.error(log['error'].strip())
|
||||
else:
|
||||
logger.info(str(log))
|
||||
|
||||
logger.info(f'Image {target_image_name} built successfully')
|
||||
except docker.errors.BuildError as e:
|
||||
logger.error(f'Sandbox image build failed: {e}')
|
||||
raise e
|
||||
except Exception as e:
|
||||
logger.error(f'An error occurred during sandbox image build: {e}')
|
||||
raise e
|
||||
|
||||
|
||||
def _get_new_image_name(base_image: str) -> str:
|
||||
prefix = 'od_sandbox'
|
||||
if ':' not in base_image:
|
||||
base_image = base_image + ':latest'
|
||||
|
||||
[repo, tag] = base_image.split(':')
|
||||
repo = repo.replace('/', '___')
|
||||
return f'{prefix}:{repo}__{tag}'
|
||||
|
||||
|
||||
def get_od_sandbox_image(base_image: str, docker_client: docker.DockerClient) -> str:
|
||||
"""Return the sandbox image name based on user-provided base image.
|
||||
|
||||
The returned sandbox image is assumed to contains all the required dependencies for OpenDevin.
|
||||
If the sandbox image is not found, it will be built.
|
||||
"""
|
||||
# OpenDevin's offcial sandbox already contains the required dependencies for OpenDevin.
|
||||
if 'ghcr.io/opendevin/sandbox' in base_image:
|
||||
return base_image
|
||||
|
||||
new_image_name = _get_new_image_name(base_image)
|
||||
|
||||
# Detect if the sandbox image is built
|
||||
images = docker_client.images.list()
|
||||
for image in images:
|
||||
if new_image_name in image.tags:
|
||||
logger.info('Found existing od_sandbox image, reuse:' + new_image_name)
|
||||
return new_image_name
|
||||
|
||||
# If the sandbox image is not found, build it
|
||||
logger.info(
|
||||
f'od_sandbox image is not found for {base_image}, will build: {new_image_name}'
|
||||
)
|
||||
_build_sandbox_image(base_image, new_image_name, docker_client)
|
||||
return new_image_name
|
||||
@ -239,6 +239,7 @@ def build_runtime_image(
|
||||
extra_deps: str | None = None,
|
||||
docker_build_folder: str | None = None,
|
||||
dry_run: bool = False,
|
||||
force_rebuild: bool = False,
|
||||
) -> str:
|
||||
"""Build the runtime image for the OpenDevin runtime.
|
||||
|
||||
@ -277,7 +278,10 @@ def build_runtime_image(
|
||||
# 2. If the exact hash is not found, we will FIRST try to re-build it
|
||||
# by leveraging the non-hash `generic_runtime_image_name` to save some time
|
||||
# from re-building the dependencies (e.g., poetry install, apt install)
|
||||
elif _check_image_exists(generic_runtime_image_name, docker_client):
|
||||
elif (
|
||||
_check_image_exists(generic_runtime_image_name, docker_client)
|
||||
and not force_rebuild
|
||||
):
|
||||
logger.info(
|
||||
f'Cannot find matched hash for image [{hash_runtime_image_name}]\n'
|
||||
f'Will try to re-build it from latest [{generic_runtime_image_name}] image to potentially save '
|
||||
@ -319,6 +323,10 @@ def build_runtime_image(
|
||||
# 3. If the image is not found AND we cannot re-use the non-hash latest relavant image,
|
||||
# we will build it completely from scratch
|
||||
else:
|
||||
if force_rebuild:
|
||||
logger.info(
|
||||
f'Force re-build: Will try to re-build image [{generic_runtime_image_name}] from scratch.\n'
|
||||
)
|
||||
cur_docker_build_folder = docker_build_folder or tempfile.mkdtemp()
|
||||
_new_from_scratch_hash = prep_docker_build_folder(
|
||||
cur_docker_build_folder,
|
||||
@ -352,8 +360,11 @@ def build_runtime_image(
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--base_image', type=str, default='ubuntu:22.04')
|
||||
parser.add_argument(
|
||||
'--base_image', type=str, default='nikolaik/python-nodejs:python3.11-nodejs22'
|
||||
)
|
||||
parser.add_argument('--build_folder', type=str, default=None)
|
||||
parser.add_argument('--force_rebuild', action='store_true', default=False)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.build_folder is not None:
|
||||
@ -373,6 +384,7 @@ if __name__ == '__main__':
|
||||
docker_client=docker.from_env(),
|
||||
docker_build_folder=temp_dir,
|
||||
dry_run=True,
|
||||
force_rebuild=args.force_rebuild,
|
||||
)
|
||||
_runtime_image_repo, runtime_image_hash_tag = runtime_image_hash_name.split(
|
||||
':'
|
||||
|
||||
@ -18,13 +18,6 @@ RUN apt-get update && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Python if not already installed
|
||||
RUN if [ ! -e /usr/bin/python ]; then \
|
||||
apt-get update && \
|
||||
apt-get install -y python3 && \
|
||||
ln -s /usr/bin/python3 /usr/bin/python; \
|
||||
fi
|
||||
|
||||
# Create necessary directories
|
||||
RUN mkdir -p /opendevin && \
|
||||
mkdir -p /opendevin/logs && \
|
||||
|
||||
@ -59,7 +59,7 @@ mkdir -p $WORKSPACE_BASE
|
||||
TEST_RUNTIME="${TEST_RUNTIME:-eventstream}" # can be server or eventstream
|
||||
# TODO: set this as default after ServerRuntime is deprecated
|
||||
if [ "$TEST_RUNTIME" == "eventstream" ] && [ -z "$SANDBOX_CONTAINER_IMAGE" ]; then
|
||||
SANDBOX_CONTAINER_IMAGE="ubuntu:22.04"
|
||||
SANDBOX_CONTAINER_IMAGE="nikolaik/python-nodejs:python3.11-nodejs22"
|
||||
fi
|
||||
|
||||
MAX_ITERATIONS=15
|
||||
|
||||
@ -358,7 +358,7 @@ def test_defaults_dict_after_updates(default_config):
|
||||
assert defaults_after_updates['sandbox']['timeout']['default'] == 120
|
||||
assert (
|
||||
defaults_after_updates['sandbox']['container_image']['default']
|
||||
== 'ubuntu:22.04'
|
||||
== 'nikolaik/python-nodejs:python3.11-nodejs22'
|
||||
)
|
||||
assert defaults_after_updates == initial_defaults
|
||||
|
||||
|
||||
@ -1,51 +0,0 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from opendevin.runtime.utils.image_agnostic import (
|
||||
_get_new_image_name,
|
||||
generate_dockerfile,
|
||||
get_od_sandbox_image,
|
||||
)
|
||||
|
||||
|
||||
def test_generate_dockerfile():
|
||||
base_image = 'debian:11'
|
||||
dockerfile_content = generate_dockerfile(base_image)
|
||||
assert base_image in dockerfile_content
|
||||
assert (
|
||||
'RUN apt update && apt install -y openssh-server wget sudo'
|
||||
in dockerfile_content
|
||||
)
|
||||
|
||||
|
||||
def test_get_new_image_name_legacy():
|
||||
# test non-eventstream runtime (sandbox-based)
|
||||
base_image = 'debian:11'
|
||||
new_image_name = _get_new_image_name(base_image)
|
||||
assert new_image_name == 'od_sandbox:debian__11'
|
||||
|
||||
base_image = 'ubuntu:22.04'
|
||||
new_image_name = _get_new_image_name(base_image)
|
||||
assert new_image_name == 'od_sandbox:ubuntu__22.04'
|
||||
|
||||
base_image = 'ubuntu'
|
||||
new_image_name = _get_new_image_name(base_image)
|
||||
assert new_image_name == 'od_sandbox:ubuntu__latest'
|
||||
|
||||
|
||||
@patch('opendevin.runtime.utils.image_agnostic._build_sandbox_image')
|
||||
@patch('opendevin.runtime.utils.image_agnostic.docker.DockerClient')
|
||||
def test_get_od_sandbox_image(mock_docker_client, mock_build_sandbox_image):
|
||||
base_image = 'debian:11'
|
||||
mock_docker_client.images.list.return_value = [
|
||||
MagicMock(tags=['od_sandbox:debian__11'])
|
||||
]
|
||||
|
||||
image_name = get_od_sandbox_image(base_image, mock_docker_client)
|
||||
assert image_name == 'od_sandbox:debian__11'
|
||||
|
||||
mock_docker_client.images.list.return_value = []
|
||||
image_name = get_od_sandbox_image(base_image, mock_docker_client)
|
||||
assert image_name == 'od_sandbox:debian__11'
|
||||
mock_build_sandbox_image.assert_called_once_with(
|
||||
base_image, 'od_sandbox:debian__11', mock_docker_client
|
||||
)
|
||||
@ -83,7 +83,9 @@ def enable_auto_lint(request):
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture(scope='module', params=['ubuntu:22.04', 'debian:11'])
|
||||
@pytest.fixture(
|
||||
scope='module', params=['nikolaik/python-nodejs:python3.11-nodejs22', 'debian:11']
|
||||
)
|
||||
def container_image(request):
|
||||
time.sleep(1)
|
||||
return request.param
|
||||
@ -127,7 +129,7 @@ async def _load_runtime(
|
||||
if 'od_runtime' not in cur_container_image and cur_container_image not in {
|
||||
'xingyaoww/od-eval-miniwob:v1.0'
|
||||
}: # a special exception list
|
||||
cur_container_image = 'ubuntu:22.04'
|
||||
cur_container_image = 'nikolaik/python-nodejs:python3.11-nodejs22'
|
||||
logger.warning(
|
||||
f'`{config.sandbox.container_image}` is not an od_runtime image. Will use `{cur_container_image}` as the container image for testing.'
|
||||
)
|
||||
@ -1033,6 +1035,13 @@ async def test_bash_python_version(temp_dir, box_class):
|
||||
assert obs.exit_code == 0
|
||||
# Should not error out
|
||||
|
||||
action = CmdRunAction(command='pip --version')
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = await runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert obs.exit_code == 0
|
||||
# Should not error out
|
||||
|
||||
await runtime.close()
|
||||
await asyncio.sleep(1)
|
||||
|
||||
|
||||
@ -62,7 +62,7 @@ def test_put_source_code_to_dir(temp_dir):
|
||||
def test_docker_build_folder(temp_dir):
|
||||
prep_docker_build_folder(
|
||||
temp_dir,
|
||||
base_image='ubuntu:22.04',
|
||||
base_image='nikolaik/python-nodejs:python3.11-nodejs22',
|
||||
skip_init=False,
|
||||
)
|
||||
|
||||
@ -81,14 +81,14 @@ def test_docker_build_folder(temp_dir):
|
||||
def test_hash_folder_same(temp_dir):
|
||||
dir_hash_1 = prep_docker_build_folder(
|
||||
temp_dir,
|
||||
base_image='ubuntu:22.04',
|
||||
base_image='nikolaik/python-nodejs:python3.11-nodejs22',
|
||||
skip_init=False,
|
||||
)
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir_2:
|
||||
dir_hash_2 = prep_docker_build_folder(
|
||||
temp_dir_2,
|
||||
base_image='ubuntu:22.04',
|
||||
base_image='nikolaik/python-nodejs:python3.11-nodejs22',
|
||||
skip_init=False,
|
||||
)
|
||||
assert dir_hash_1 == dir_hash_2
|
||||
@ -97,14 +97,14 @@ def test_hash_folder_same(temp_dir):
|
||||
def test_hash_folder_diff_init(temp_dir):
|
||||
dir_hash_1 = prep_docker_build_folder(
|
||||
temp_dir,
|
||||
base_image='ubuntu:22.04',
|
||||
base_image='nikolaik/python-nodejs:python3.11-nodejs22',
|
||||
skip_init=False,
|
||||
)
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir_2:
|
||||
dir_hash_2 = prep_docker_build_folder(
|
||||
temp_dir_2,
|
||||
base_image='ubuntu:22.04',
|
||||
base_image='nikolaik/python-nodejs:python3.11-nodejs22',
|
||||
skip_init=True,
|
||||
)
|
||||
assert dir_hash_1 != dir_hash_2
|
||||
@ -113,7 +113,7 @@ def test_hash_folder_diff_init(temp_dir):
|
||||
def test_hash_folder_diff_image(temp_dir):
|
||||
dir_hash_1 = prep_docker_build_folder(
|
||||
temp_dir,
|
||||
base_image='ubuntu:22.04',
|
||||
base_image='nikolaik/python-nodejs:python3.11-nodejs22',
|
||||
skip_init=False,
|
||||
)
|
||||
|
||||
@ -178,11 +178,12 @@ def test_get_runtime_image_repo_and_tag_eventstream():
|
||||
and img_tag == f'{OD_VERSION}_image_debian_tag_11'
|
||||
)
|
||||
|
||||
base_image = 'ubuntu:22.04'
|
||||
base_image = 'nikolaik/python-nodejs:python3.11-nodejs22'
|
||||
img_repo, img_tag = get_runtime_image_repo_and_tag(base_image)
|
||||
assert (
|
||||
img_repo == f'{RUNTIME_IMAGE_REPO}'
|
||||
and img_tag == f'{OD_VERSION}_image_ubuntu_tag_22.04'
|
||||
and img_tag
|
||||
== f'{OD_VERSION}_image_nikolaik___python-nodejs_tag_python3.11-nodejs22'
|
||||
)
|
||||
|
||||
base_image = 'ubuntu'
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user