Skip to content

Commit

Permalink
Merge branch 'main' into kevin
Browse files Browse the repository at this point in the history
  • Loading branch information
SmartManoj committed Oct 3, 2024
2 parents 055970e + 1abfd3b commit ed22e2d
Show file tree
Hide file tree
Showing 20 changed files with 109 additions and 92 deletions.
4 changes: 1 addition & 3 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
- [ ] Include this change in the Release Notes. If checked, you must provide an **end-user friendly** description for your change below

**End-user friendly description of the problem this fixes or functionality that this introduces**


- [ ] Include this change in the Release Notes. If checked, you must provide an **end-user friendly** description for your change below

---
**Give a summary of what the PR does, explaining any non-trivial design decisions**
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/dummy-agent-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
- name: Run tests
run: |
set -e
poetry run python3 openhands/core/main.py -t "do a flip" -d ./workspace/ -c DummyAgent
SANDBOX_FORCE_REBUILD_RUNTIME=True poetry run python3 openhands/core/main.py -t "do a flip" -d ./workspace/ -c DummyAgent
- name: Check exit code
run: |
if [ $? -ne 0 ]; then
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/ghcr-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ jobs:
SANDBOX_RUNTIME_CONTAINER_IMAGE=$image_name \
TEST_IN_CI=true \
RUN_AS_OPENHANDS=false \
poetry run pytest -n 3 -raR --reruns 1 --reruns-delay 3 --cov=agenthub --cov=openhands --cov-report=xml -s ./tests/runtime
poetry run pytest -n 3 -raRs --reruns 2 --reruns-delay 5 --cov=agenthub --cov=openhands --cov-report=xml -s ./tests/runtime
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
env:
Expand Down Expand Up @@ -371,7 +371,7 @@ jobs:
SANDBOX_RUNTIME_CONTAINER_IMAGE=$image_name \
TEST_IN_CI=true \
RUN_AS_OPENHANDS=true \
poetry run pytest -n 3 -raR --reruns 1 --reruns-delay 3 --cov=agenthub --cov=openhands --cov-report=xml -s ./tests/runtime
poetry run pytest -n 3 -raRs --reruns 2 --reruns-delay 5 --cov=agenthub --cov=openhands --cov-report=xml -s ./tests/runtime
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
env:
Expand Down
2 changes: 1 addition & 1 deletion openhands/core/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ async def main():
return

logger.setLevel(logging.WARNING)
config = load_app_config()
config = load_app_config(config_file=args.config_file)
sid = 'cli'

agent_cls: Type[Agent] = Agent.get_cls(config.default_agent)
Expand Down
2 changes: 2 additions & 0 deletions openhands/core/config/sandbox_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class SandboxConfig:
enable_auto_lint: Whether to enable auto-lint.
use_host_network: Whether to use the host network.
initialize_plugins: Whether to initialize plugins.
force_rebuild_runtime: Whether to force rebuild the runtime image.
runtime_extra_deps: The extra dependencies to install in the runtime image (typically used for evaluation).
This will be rendered into the end of the Dockerfile that builds the runtime image.
It can contain any valid shell commands (e.g., pip install numpy).
Expand Down Expand Up @@ -46,6 +47,7 @@ class SandboxConfig:
)
use_host_network: bool = False
initialize_plugins: bool = True
force_rebuild_runtime: bool = False
runtime_extra_deps: str | None = None
runtime_startup_env_vars: dict[str, str] = field(default_factory=dict)
browsergym_eval_env: str | None = None
Expand Down
15 changes: 12 additions & 3 deletions openhands/core/config/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,12 @@ def get_llm_config_arg(
def get_parser() -> argparse.ArgumentParser:
"""Get the parser for the command line arguments."""
parser = argparse.ArgumentParser(description='Run an agent with a specific task')
parser.add_argument(
'--config-file',
type=str,
default='config.toml',
help='Path to the config file (default: config.toml in the current directory)',
)
parser.add_argument(
'-d',
'--directory',
Expand Down Expand Up @@ -383,14 +389,17 @@ def parse_arguments() -> argparse.Namespace:
return parsed_args


def load_app_config(set_logging_levels: bool = True) -> AppConfig:
"""Load the configuration from the config.toml file and environment variables.
def load_app_config(
set_logging_levels: bool = True, config_file: str = 'config.toml'
) -> AppConfig:
"""Load the configuration from the specified config file and environment variables.
Args:
set_logger_levels: Whether to set the global variables for logging levels.
config_file: Path to the config file. Defaults to 'config.toml' in the current directory.
"""
config = AppConfig()
load_from_toml(config)
load_from_toml(config, config_file)
load_from_env(config, os.environ)
finalize_config(config)
if set_logging_levels:
Expand Down
2 changes: 1 addition & 1 deletion openhands/core/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ def generate_sid(config: AppConfig, session_name: str | None = None) -> str:
# Load the app config
# this will load config from config.toml in the current directory
# as well as from the environment variables
config = load_app_config()
config = load_app_config(config_file=args.config_file)

# Override default LLM configs ([llm] section in config.toml)
if args.llm_config:
Expand Down
2 changes: 2 additions & 0 deletions openhands/llm/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from litellm.caching import Cache
from litellm.exceptions import (
APIConnectionError,
APIError,
InternalServerError,
RateLimitError,
ServiceUnavailableError,
Expand All @@ -39,6 +40,7 @@
# tuple of exceptions to retry on
LLM_RETRY_EXCEPTIONS: tuple[type[Exception], ...] = (
APIConnectionError,
APIError,
InternalServerError,
RateLimitError,
ServiceUnavailableError,
Expand Down
4 changes: 2 additions & 2 deletions openhands/runtime/builder/docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@ def build(
raise subprocess.CalledProcessError(
return_code,
process.args,
output=None,
stderr=None,
output=process.stdout.read() if process.stdout else None,
stderr=process.stderr.read() if process.stderr else None,
)

except subprocess.CalledProcessError as e:
Expand Down
8 changes: 5 additions & 3 deletions openhands/runtime/builder/remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from openhands.runtime.builder import RuntimeBuilder
from openhands.runtime.utils.request import send_request
from openhands.runtime.utils.shutdown_listener import (
should_exit,
should_continue,
sleep_if_should_continue,
)

Expand Down Expand Up @@ -60,8 +60,8 @@ def build(self, path: str, tags: list[str]) -> str:
# Poll /build_status until the build is complete
start_time = time.time()
timeout = 30 * 60 # 20 minutes in seconds
while True:
if should_exit() or time.time() - start_time > timeout:
while should_continue():
if time.time() - start_time > timeout:
logger.error('Build timed out after 30 minutes')
raise RuntimeError('Build timed out after 30 minutes')

Expand Down Expand Up @@ -101,6 +101,8 @@ def build(self, path: str, tags: list[str]) -> str:
# Wait before polling again
sleep_if_should_continue(30)

raise RuntimeError('Build interrupted (likely received SIGTERM or SIGINT).')

def image_exists(self, image_name: str, pull_from_repo: bool = True) -> bool:
"""Checks if an image exists in the remote registry using the /image_exists endpoint."""
params = {'image': image_name}
Expand Down
14 changes: 9 additions & 5 deletions openhands/runtime/client/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,11 +177,13 @@ def __init__(
raise ValueError(
'Neither runtime container image nor base container image is set'
)
logger.info('Preparing container, this might take a few minutes...')
self.send_status_message('STATUS$STARTING_CONTAINER')
self.runtime_container_image = build_runtime_image(
self.base_container_image,
self.runtime_builder,
extra_deps=self.config.sandbox.runtime_extra_deps,
force_rebuild=self.config.sandbox.force_rebuild_runtime,
)
self.container = self._init_container(
sandbox_workspace_dir=self.config.workspace_mount_path_in_sandbox,
Expand Down Expand Up @@ -302,7 +304,7 @@ def _init_container(
container = self.docker_client.containers.run(
self.runtime_container_image,
command=(
f'/openhands/miniforge3/bin/mamba run --no-capture-output -n base '
f'/openhands/micromamba/bin/micromamba run -n openhands '
f'poetry run '
f'python -u -m openhands.runtime.client.client {self._container_port} '
f'--working-dir "{sandbox_workspace_dir}" '
Expand Down Expand Up @@ -450,13 +452,15 @@ def run_action(self, action: Action) -> Observation:
logger.debug(f'response: {response}')
error_message = response.text
logger.error(f'Error from server: {error_message}')
obs = ErrorObservation(f'Command execution failed: {error_message}')
obs = ErrorObservation(f'Action execution failed: {error_message}')
except requests.Timeout:
logger.error('No response received within the timeout period.')
obs = ErrorObservation('Command execution timed out')
obs = ErrorObservation(
f'Action execution timed out after {action.timeout} seconds.'
)
except Exception as e:
logger.error(f'Error during command execution: {e}')
obs = ErrorObservation(f'Command execution failed: {str(e)}')
logger.error(f'Error during action execution: {e}')
obs = ErrorObservation(f'Action execution failed: {str(e)}')
self._refresh_logs()
return obs

Expand Down
3 changes: 2 additions & 1 deletion openhands/runtime/plugins/jupyter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ async def initialize(self, username: str, kernel_id: str = 'openhands-default'):
'cd /openhands/code\n'
'export POETRY_VIRTUALENVS_PATH=/openhands/poetry;\n'
'export PYTHONPATH=/openhands/code:$PYTHONPATH;\n'
'/openhands/miniforge3/bin/mamba run -n base '
'export MAMBA_ROOT_PREFIX=/openhands/micromamba;\n'
'/openhands/micromamba/bin/micromamba run -n openhands '
'poetry run jupyter kernelgateway '
'--KernelGatewayApp.ip=0.0.0.0 '
f'--KernelGatewayApp.port={self.kernel_gateway_port}\n'
Expand Down
5 changes: 3 additions & 2 deletions openhands/runtime/remote/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ def __init__(
self.config.sandbox.base_container_image,
self.runtime_builder,
extra_deps=self.config.sandbox.runtime_extra_deps,
force_rebuild=self.config.sandbox.force_rebuild_runtime,
)

response = send_request(
Expand All @@ -144,8 +145,8 @@ def __init__(
start_request = {
'image': self.container_image,
'command': (
f'/openhands/miniforge3/bin/mamba run --no-capture-output -n base '
'PYTHONUNBUFFERED=1 poetry run '
f'/openhands/micromamba/bin/micromamba run -n openhands '
'poetry run '
f'python -u -m openhands.runtime.client.client {self.port} '
f'--working-dir {self.config.workspace_mount_path_in_sandbox} '
f'{plugin_arg}'
Expand Down
48 changes: 24 additions & 24 deletions openhands/runtime/utils/runtime_templates/Dockerfile.j2
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
{% if skip_init %}
FROM {{ base_image }}
{% else %}

# Shared environment variables (regardless of init or not)
ENV POETRY_VIRTUALENVS_PATH=/openhands/poetry
ENV MAMBA_ROOT_PREFIX=/openhands/micromamba

{% if not skip_init %}
# ================================================================
# START: Build Runtime Image from Scratch
# ================================================================
FROM {{ base_image }}

{% if 'ubuntu' in base_image and (base_image.endswith(':latest') or base_image.endswith(':24.04')) %}
{% set LIBGL_MESA = 'libgl1' %}
{% else %}
Expand All @@ -14,7 +16,7 @@ FROM {{ base_image }}

# Install necessary packages and clean up in one layer
RUN apt-get update && \
apt-get install -y wget sudo apt-utils {{ LIBGL_MESA }} libasound2-plugins git docker.io lsof && \
apt-get install -y wget curl sudo apt-utils {{ LIBGL_MESA }} libasound2-plugins git docker.io lsof && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

Expand All @@ -26,19 +28,16 @@ RUN mkdir -p /openhands && \
mkdir -p /openhands/logs && \
mkdir -p /openhands/poetry

# Directory containing subdirectories for virtual environment.
ENV POETRY_VIRTUALENVS_PATH=/openhands/poetry
# Install micromamba
RUN mkdir -p /openhands/micromamba/bin && \
/bin/bash -c "PREFIX_LOCATION=/openhands/micromamba BIN_FOLDER=/openhands/micromamba/bin INIT_YES=no CONDA_FORGE_YES=yes $(curl -L https://micro.mamba.pm/install.sh)" && \
/openhands/micromamba/bin/micromamba config remove channels defaults && \
/openhands/micromamba/bin/micromamba config list

RUN if [ ! -d /openhands/miniforge3 ]; then \
wget --progress=bar:force -O Miniforge3.sh "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh" && \
bash Miniforge3.sh -b -p /openhands/miniforge3 && \
rm Miniforge3.sh && \
chmod -R g+w /openhands/miniforge3 && \
bash -c ". /openhands/miniforge3/etc/profile.d/conda.sh && conda config --set changeps1 False && conda config --append channels conda-forge"; \
fi
# Create the openhands virtual environment and install poetry and python
RUN /openhands/micromamba/bin/micromamba create -n openhands -y && \
/openhands/micromamba/bin/micromamba install -n openhands -c conda-forge poetry python=3.12 -y

# Install Python and Poetry
RUN /openhands/miniforge3/bin/mamba install conda-forge::poetry python=3.12 -y
# ================================================================
# END: Build Runtime Image from Scratch
# ================================================================
Expand All @@ -59,27 +58,28 @@ COPY ./code /openhands/code
# virtual environment are used by default.
WORKDIR /openhands/code
RUN \
/openhands/micromamba/bin/micromamba config set changeps1 False && \
# Configure Poetry and create virtual environment
/openhands/miniforge3/bin/mamba run -n base poetry config virtualenvs.path /openhands/poetry && \
/openhands/miniforge3/bin/mamba run -n base poetry env use python3.11 && \
/openhands/micromamba/bin/micromamba run -n openhands poetry config virtualenvs.path /openhands/poetry && \
/openhands/micromamba/bin/micromamba run -n openhands poetry env use python3.11 && \
# Install project dependencies
/openhands/miniforge3/bin/mamba run -n base poetry install --only main,runtime --no-interaction --no-root && \
/openhands/micromamba/bin/micromamba run -n openhands poetry install --only main,runtime --no-interaction --no-root && \
# Update and install additional tools
apt-get update && \
/openhands/miniforge3/bin/mamba run -n base poetry run pip install playwright docker && \
/openhands/miniforge3/bin/mamba run -n base poetry run playwright install --with-deps chromium && \
/openhands/micromamba/bin/micromamba run -n openhands poetry run pip install playwright docker&& \
/openhands/micromamba/bin/micromamba run -n openhands poetry run playwright install --with-deps chromium && \
# Set environment variables
echo "OH_INTERPRETER_PATH=$(/openhands/miniforge3/bin/mamba run -n base poetry run python -c "import sys; print(sys.executable)")" >> /etc/environment && \
echo "OH_INTERPRETER_PATH=$(/openhands/micromamba/bin/micromamba run -n openhands poetry run python -c "import sys; print(sys.executable)")" >> /etc/environment && \
# Install extra dependencies if specified
{{ extra_deps }} {% if extra_deps %} && {% endif %} \
# Clear caches
/openhands/miniforge3/bin/mamba run -n base poetry cache clear --all . && \
/openhands/micromamba/bin/micromamba run -n openhands poetry cache clear --all . && \
# Set permissions
{% if not skip_init %}chmod -R g+rws /openhands/poetry && {% endif %} \
mkdir -p /openhands/workspace && chmod -R g+rws,o+rw /openhands/workspace && \
# Clean up
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \
/openhands/miniforge3/bin/mamba clean --all
/openhands/micromamba/bin/micromamba clean --all
# ================================================================
# END: Copy Project and Install/Update Dependencies
# ================================================================
3 changes: 2 additions & 1 deletion openhands/server/listen.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import asyncio
import os
import re
import tempfile
Expand Down Expand Up @@ -257,7 +258,7 @@ async def websocket_endpoint(websocket: WebSocket):
{"action": "finish", "args": {}}
```
"""
await websocket.accept()
await asyncio.wait_for(websocket.accept(), 10)

if websocket.query_params.get('token'):
token = websocket.query_params.get('token')
Expand Down
Loading

0 comments on commit ed22e2d

Please sign in to comment.