From 015df47e5384584685655fa854f6b45eb2d169a2 Mon Sep 17 00:00:00 2001 From: Xingyao Wang Date: Thu, 17 Oct 2024 09:19:53 -0500 Subject: [PATCH 01/27] chore: remove integration tests from CI to unblock (#4451) --- .github/workflows/ghcr-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ghcr-build.yml b/.github/workflows/ghcr-build.yml index d5727387bcd1..4692e6b8aa39 100644 --- a/.github/workflows/ghcr-build.yml +++ b/.github/workflows/ghcr-build.yml @@ -392,7 +392,7 @@ jobs: name: All Runtime Tests Passed if: ${{ !cancelled() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }} runs-on: ubuntu-latest - needs: [test_runtime_root, test_runtime_oh, runtime_integration_tests_on_linux, verify_hash_equivalence_in_runtime_and_app] + needs: [test_runtime_root, test_runtime_oh, verify_hash_equivalence_in_runtime_and_app] steps: - name: All tests passed run: echo "All runtime tests have passed successfully!" @@ -401,7 +401,7 @@ jobs: name: All Runtime Tests Passed if: ${{ cancelled() || contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }} runs-on: ubuntu-latest - needs: [test_runtime_root, test_runtime_oh, runtime_integration_tests_on_linux, verify_hash_equivalence_in_runtime_and_app] + needs: [test_runtime_root, test_runtime_oh, verify_hash_equivalence_in_runtime_and_app] steps: - name: Some tests failed run: | From 83c096b974eed96e3f124470fea8997e4fbb3511 Mon Sep 17 00:00:00 2001 From: "sp.wack" <83104063+amanape@users.noreply.github.com> Date: Thu, 17 Oct 2024 18:35:21 +0400 Subject: [PATCH 02/27] [ALL-551] chore(frontend): Retrieve `APP_MODE` from the server (#4423) --- containers/app/Dockerfile | 2 +- frontend/.env.sample | 1 - frontend/global.d.ts | 3 +++ frontend/public/config.json | 3 +++ frontend/src/api/open-hands.ts | 9 +++++++++ frontend/src/routes/_oh._index/route.tsx | 2 +- frontend/src/routes/_oh.tsx | 3 +++ openhands/server/listen.py | 2 +- 8 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 frontend/global.d.ts create mode 100644 frontend/public/config.json diff --git a/containers/app/Dockerfile b/containers/app/Dockerfile index ce2c0ba75c51..507f45659b01 100644 --- a/containers/app/Dockerfile +++ b/containers/app/Dockerfile @@ -8,7 +8,7 @@ RUN npm install -g npm@10.5.1 RUN npm ci COPY ./frontend ./ -RUN npm run make-i18n && npm run build +RUN npm run build FROM python:3.12.3-slim AS backend-builder diff --git a/frontend/.env.sample b/frontend/.env.sample index ba82f3b80bcf..9d6bf87a70e9 100644 --- a/frontend/.env.sample +++ b/frontend/.env.sample @@ -3,4 +3,3 @@ VITE_MOCK_API="false" # true or false # GitHub OAuth VITE_GITHUB_CLIENT_ID="" -VITE_APP_MODE="oss" # "oss" or "saas" diff --git a/frontend/global.d.ts b/frontend/global.d.ts new file mode 100644 index 000000000000..e71757d809d1 --- /dev/null +++ b/frontend/global.d.ts @@ -0,0 +1,3 @@ +interface Window { + __APP_MODE__?: "saas" | "oss"; +} diff --git a/frontend/public/config.json b/frontend/public/config.json new file mode 100644 index 000000000000..bdc29014a437 --- /dev/null +++ b/frontend/public/config.json @@ -0,0 +1,3 @@ +{ + "APP_MODE": "oss" +} diff --git a/frontend/src/api/open-hands.ts b/frontend/src/api/open-hands.ts index a0d3087de5a8..be41cf7967d2 100644 --- a/frontend/src/api/open-hands.ts +++ b/frontend/src/api/open-hands.ts @@ -60,6 +60,15 @@ class OpenHands { return response.json(); } + static async getConfig(): Promise<{ APP_MODE: "saas" | "oss" }> { + const response = await fetch(`${OpenHands.BASE_URL}/config.json`, { + headers: { + "Cache-Control": "no-cache", + }, + }); + return response.json(); + } + /** * Retrieve the list of files available in the workspace * @param token User token provided by the server diff --git a/frontend/src/routes/_oh._index/route.tsx b/frontend/src/routes/_oh._index/route.tsx index 6cf98852e4c7..21b6bc197918 100644 --- a/frontend/src/routes/_oh._index/route.tsx +++ b/frontend/src/routes/_oh._index/route.tsx @@ -113,7 +113,7 @@ function Home() { const { files } = useSelector((state: RootState) => state.initalQuery); const handleConnectToGitHub = () => { - const isSaas = import.meta.env.VITE_APP_MODE === "saas"; + const isSaas = window.__APP_MODE__ === "saas"; if (isSaas) { window.location.href = githubAuthUrl; diff --git a/frontend/src/routes/_oh.tsx b/frontend/src/routes/_oh.tsx index fc9793c4538d..0449443dbdc7 100644 --- a/frontend/src/routes/_oh.tsx +++ b/frontend/src/routes/_oh.tsx @@ -26,6 +26,9 @@ import NewProjectIcon from "#/assets/new-project.svg?react"; import DocsIcon from "#/assets/docs.svg?react"; export const clientLoader = async () => { + const config = await OpenHands.getConfig(); + window.__APP_MODE__ = config.APP_MODE; + let token = localStorage.getItem("token"); const ghToken = localStorage.getItem("ghToken"); diff --git a/openhands/server/listen.py b/openhands/server/listen.py index 32c93a117e23..2f472823672d 100644 --- a/openhands/server/listen.py +++ b/openhands/server/listen.py @@ -798,4 +798,4 @@ def github_callback(auth_code: AuthCode): ) -app.mount('/', StaticFiles(directory='./frontend/build', html=True), name='dist') +app.mount('/', StaticFiles(directory='./frontend/build/client', html=True), name='dist') From 5fb3dece9319a8bcc74ede599c0c7176dc1d20b2 Mon Sep 17 00:00:00 2001 From: tofarr Date: Thu, 17 Oct 2024 09:08:56 -0600 Subject: [PATCH 03/27] Feat: Divided docker layer to make it easier to cache (#4313) Co-authored-by: Xingyao Wang --- docs/modules/usage/architecture/runtime.md | 82 ++-- openhands/runtime/builder/docker.py | 3 - openhands/runtime/modal/runtime.py | 10 +- openhands/runtime/utils/runtime_build.py | 436 ++++++++---------- .../utils/runtime_templates/Dockerfile.j2 | 47 +- tests/unit/test_runtime_build.py | 318 ++++++------- 6 files changed, 371 insertions(+), 525 deletions(-) diff --git a/docs/modules/usage/architecture/runtime.md b/docs/modules/usage/architecture/runtime.md index 161439091f63..98e3d11da8ad 100644 --- a/docs/modules/usage/architecture/runtime.md +++ b/docs/modules/usage/architecture/runtime.md @@ -70,74 +70,46 @@ Check out the [relevant code](https://github.com/All-Hands-AI/OpenHands/blob/mai ### Image Tagging System -OpenHands uses a dual-tagging system for its runtime images to balance reproducibility with flexibility: +OpenHands uses a dual-tagging system for its runtime images to balance reproducibility with flexibility. +Tags may be in one of 2 formats: -1. Hash-based tag: `{target_image_repo}:{target_image_hash_tag}`. - Example: `runtime:abc123def456` +- **Generic**: `oh_v{openhands_version}_{16_digit_lock_hash}` (e.g.: `oh_v0.9.9_1234567890abcdef`) +- **Specific**: `oh_v{openhands_version}_{16_digit_lock_hash}_{16_digit_source_hash}` + (e.g.: `oh_v0.9.9_1234567890abcdef_1234567890abcdef`) - - This tag is based on the MD5 hash of the Docker build folder, which includes the source code (of runtime client and related dependencies) and Dockerfile - - Identical hash tags guarantee that the images were built with exactly the same source code and Dockerfile - - This ensures reproducibility; the same hash always means the same image contents +#### Lock Hash -2. Generic tag: `{target_image_repo}:{target_image_tag}`. - Example: `runtime:oh_v0.9.3_ubuntu_tag_22.04` +This hash is built from the first 16 digits of the MD5 of: +- The name of the base image upon which the image was built (e.g.: `nikolaik/python-nodejs:python3.12-nodejs22`) +- The content of the `pyproject.toml` included in the image. +- The content of the `poetry.lock` included in the image. - - This tag follows the format: `runtime:oh_v{OH_VERSION}_{BASE_IMAGE_NAME}_tag_{BASE_IMAGE_TAG}` - - It represents the latest build for a particular base image and OpenHands version combination - - This tag is updated whenever a new image is built from the same base image, even if the source code changes +This effectively gives a hash for the dependencies of Openhands independent of the source code. -The hash-based tag ensures reproducibility, while the generic tag provides a stable reference to the latest version of a particular configuration. This dual-tagging approach allows OpenHands to efficiently manage both development and production environments. +#### Source Hash -### Build Process +This is the first 16 digits of the MD5 of the directory hash for the source directory. This gives a hash +for only the openhands source -1. Image Naming Convention: - - Hash-based tag: `{target_image_repo}:{target_image_hash_tag}`. - Example: `runtime:abc123def456` - - Generic tag: `{target_image_repo}:{target_image_tag}`. - Example: `runtime:oh_v0.9.3_ubuntu_tag_22.04` +#### Build Process -2. Build Process: - - a. Convert the base image name to an OH runtime image name - Example: `ubuntu:22.04` -> `runtime:oh_v0.9.3_ubuntu_tag_22.04` - - b. Generate a build context (Dockerfile and OpenHands source code) and calculate its hash - - c. Check for an existing image with the calculated hash - - d. If not found, check for a recent compatible image to use as a base - - e. If no compatible image exists, build from scratch using the original base image - - f. Tag the new image with both hash-based and generic tags +When generating an image... -3. Image Reuse and Rebuilding Logic: - The system follows these steps to determine whether to build a new image or use an existing one from a user-provided (base) image (e.g., `ubuntu:22.04`): - - a. If an image exists with the same hash (e.g., `runtime:abc123def456`), it will be reused as is - - b. If the exact hash is not found, the system will try to rebuild using the latest generic image (e.g., `runtime:oh_v0.9.3_ubuntu_tag_22.04`) as a base. This saves time by leveraging existing dependencies - - c. If neither the hash-tagged nor the generic-tagged image is found, the system will build the image completely from scratch +- OpenHands first checks whether an image with the same **Specific** tag exists. If there is such an image, + no build is performed - the existing image is used. +- OpenHands next checks whether an image with the **Generic** tag exists. If there is such an image, + OpenHands builds a new image based upon it, bypassing all installation steps (like `poetry install` and + `apt-get`) except a final operation to copy the current source code. The new image is tagged with a + **Specific** tag only. +- If neither a **Specific** nor **Generic** tag exists, a brand new image is built based upon the base + image (Which is a slower operation). This new image is tagged with both the **Generic** and **Specific** + tags. -4. Caching and Efficiency: - - The system attempts to reuse existing images when possible to save build time - - If an exact match (by hash) is found, it's used without rebuilding - - If a compatible image is found, it's used as a base for rebuilding, saving time on dependency installation - -Here's a flowchart illustrating the build process: - -```mermaid -flowchart TD - A[Start] --> B{Convert base image name} - B --> |ubuntu:22.04 -> runtime:oh_v0.9.3_ubuntu_tag_22.04| C[Generate build context and hash] - C --> D{Check for existing image with hash} - D -->|Found runtime:abc123def456| E[Use existing image] - D -->|Not found| F{Check for runtime:oh_v0.9.3_ubuntu_tag_22.04} - F -->|Found| G[Rebuild based on recent image] - F -->|Not found| H[Build from scratch] - G --> I[Tag with hash and generic tags] - H --> I - E --> J[End] - I --> J -``` - -This approach ensures that: +This dual-tagging approach allows OpenHands to efficiently manage both development and production environments. 1. Identical source code and Dockerfile always produce the same image (via hash-based tags) 2. The system can quickly rebuild images when minor changes occur (by leveraging recent compatible images) -3. The generic tag (e.g., `runtime:oh_v0.9.3_ubuntu_tag_22.04`) always points to the latest build for a particular base image and OpenHands version combination +3. The generic tag (e.g., `runtime:oh_v0.9.3_1234567890abcdef`) always points to the latest build for a particular base image and OpenHands version combination ## Runtime Plugin System diff --git a/openhands/runtime/builder/docker.py b/openhands/runtime/builder/docker.py index 56a759df7d30..de03de172d95 100644 --- a/openhands/runtime/builder/docker.py +++ b/openhands/runtime/builder/docker.py @@ -59,9 +59,6 @@ def build( target_image_repo, target_image_hash_tag = target_image_hash_name.split(':') target_image_tag = tags[1].split(':')[1] if len(tags) > 1 else None - # Check if the image exists and pull if necessary - self.image_exists(target_image_hash_name) - buildx_cmd = [ 'docker', 'buildx', diff --git a/openhands/runtime/modal/runtime.py b/openhands/runtime/modal/runtime.py index 55fa6729f60a..2aae1b99c97f 100644 --- a/openhands/runtime/modal/runtime.py +++ b/openhands/runtime/modal/runtime.py @@ -2,6 +2,7 @@ import tempfile import threading import uuid +from pathlib import Path from typing import Callable, Generator import modal @@ -14,7 +15,7 @@ from openhands.runtime.client.runtime import EventStreamRuntime, LogBuffer from openhands.runtime.plugins import PluginRequirement from openhands.runtime.utils.runtime_build import ( - prep_docker_build_folder, + prep_build_folder, ) @@ -148,9 +149,10 @@ def _get_image_definition( base_runtime_image = modal.Image.from_registry(runtime_container_image_id) elif base_container_image_id: build_folder = tempfile.mkdtemp() - prep_docker_build_folder( - build_folder, - base_container_image_id, + prep_build_folder( + build_folder=Path(build_folder), + base_image=base_container_image_id, + build_from_scratch=True, extra_deps=runtime_extra_deps, ) diff --git a/openhands/runtime/utils/runtime_build.py b/openhands/runtime/utils/runtime_build.py index 92d480533092..3765ff64a833 100644 --- a/openhands/runtime/utils/runtime_build.py +++ b/openhands/runtime/utils/runtime_build.py @@ -1,16 +1,17 @@ import argparse import hashlib -import importlib.metadata import os import shutil +import string import tempfile +from pathlib import Path +from typing import List import docker from dirhash import dirhash from jinja2 import Environment, FileSystemLoader import openhands -from openhands import __package_name__ from openhands import __version__ as oh_version from openhands.core.logger import openhands_logger as logger from openhands.runtime.builder import DockerRuntimeBuilder, RuntimeBuilder @@ -20,65 +21,16 @@ def get_runtime_image_repo(): return os.getenv('OH_RUNTIME_RUNTIME_IMAGE_REPO', 'ghcr.io/all-hands-ai/runtime') -def _put_source_code_to_dir(temp_dir: str): - """Builds the project source tarball directly in temp_dir and unpacks it. - The OpenHands source code ends up in the temp_dir/code directory. - Parameters: - - temp_dir (str): The directory to put the source code in - """ - if not os.path.isdir(temp_dir): - raise RuntimeError(f'Temp directory {temp_dir} does not exist') - - dest_dir = os.path.join(temp_dir, 'code') - openhands_dir = None - - try: - # Try to get the source directory from the installed package - distribution = importlib.metadata.distribution(__package_name__) - source_dir = os.path.dirname(distribution.locate_file(__package_name__)) - openhands_dir = os.path.join(source_dir, 'openhands') - except importlib.metadata.PackageNotFoundError: - pass - - if openhands_dir is not None and os.path.isdir(openhands_dir): - logger.info(f'Package {__package_name__} found') - shutil.copytree(openhands_dir, os.path.join(dest_dir, 'openhands')) - # note: "pyproject.toml" and "poetry.lock" are included in the openhands - # package, so we need to move them out to the top-level directory - for filename in ['pyproject.toml', 'poetry.lock']: - shutil.move(os.path.join(dest_dir, 'openhands', filename), dest_dir) - else: - # If package is not found, build from source code - project_root = os.path.dirname( - os.path.dirname(os.path.abspath(openhands.__file__)) - ) - logger.info(f'Building source distribution using project root: {project_root}') - - # Copy the 'openhands' directory - openhands_dir = os.path.join(project_root, 'openhands') - if not os.path.isdir(openhands_dir): - raise RuntimeError(f"'openhands' directory not found in {project_root}") - shutil.copytree(openhands_dir, os.path.join(dest_dir, 'openhands')) - - # Copy pyproject.toml and poetry.lock files - for file in ['pyproject.toml', 'poetry.lock']: - src_file = os.path.join(project_root, file) - dest_file = os.path.join(dest_dir, file) - shutil.copy2(src_file, dest_file) - - logger.info(f'Unpacked source code directory: {dest_dir}') - - def _generate_dockerfile( base_image: str, - skip_init: bool = False, + build_from_scratch: bool = True, extra_deps: str | None = None, ) -> str: """Generate the Dockerfile content for the runtime image based on the base image. Parameters: - base_image (str): The base image provided for the runtime image - - skip_init (boolean): + - build_from_scratch (boolean): False implies most steps can be skipped (Base image is another openhands instance) - extra_deps (str): Returns: @@ -93,69 +45,12 @@ def _generate_dockerfile( dockerfile_content = template.render( base_image=base_image, - skip_init=skip_init, + build_from_scratch=build_from_scratch, extra_deps=extra_deps if extra_deps is not None else '', ) return dockerfile_content -def prep_docker_build_folder( - dir_path: str, - base_image: str, - skip_init: bool = False, - extra_deps: str | None = None, -) -> str: - """Prepares a docker build folder by copying the source code and generating the Dockerfile - - Parameters: - - dir_path (str): The build folder to place the source code and Dockerfile - - base_image (str): The base Docker image to use for the Dockerfile - - skip_init (str): - - extra_deps (str): - - Returns: - - str: The MD5 hash of the build folder directory (dir_path) - """ - # Copy the source code to directory. It will end up in dir_path/code - _put_source_code_to_dir(dir_path) - - # Create a Dockerfile and write it to dir_path - dockerfile_content = _generate_dockerfile( - base_image, - skip_init=skip_init, - extra_deps=extra_deps, - ) - if os.getenv('SKIP_CONTAINER_LOGS', 'false') != 'true': - logger.debug( - ( - f'===== Dockerfile content start =====\n' - f'{dockerfile_content}\n' - f'===== Dockerfile content end =====' - ) - ) - with open(os.path.join(dir_path, 'Dockerfile'), 'w') as file: - file.write(dockerfile_content) - - # Get the MD5 hash of the dir_path directory - dir_hash = dirhash( - dir_path, - 'md5', - ignore=[ - '.*/', # hidden directories - '__pycache__/', - '*.pyc', - ], - ) - hash = f'v{oh_version}_{dir_hash}' - logger.info( - f'Input base image: {base_image}\n' - f'Skip init: {skip_init}\n' - f'Extra deps: {extra_deps}\n' - f'Hash for docker build directory [{dir_path}] (contents: {os.listdir(dir_path)}): {hash}\n' - ) - return hash - - def get_runtime_image_repo_and_tag(base_image: str) -> tuple[str, str]: """Retrieves the Docker repo and tag associated with the Docker image. @@ -204,7 +99,7 @@ def build_runtime_image( base_image: str, runtime_builder: RuntimeBuilder, extra_deps: str | None = None, - docker_build_folder: str | None = None, + build_folder: str | None = None, dry_run: bool = False, force_rebuild: bool = False, ) -> str: @@ -215,7 +110,7 @@ def build_runtime_image( - base_image (str): The name of the base Docker image to use - runtime_builder (RuntimeBuilder): The runtime builder to use - extra_deps (str): - - docker_build_folder (str): The directory to use for the build. If not provided a temporary directory will be used + - build_folder (str): The directory to use for the build. If not provided a temporary directory will be used - dry_run (bool): if True, it will only ready the build folder. It will not actually build the Docker image - force_rebuild (bool): if True, it will create the Dockerfile which uses the base_image @@ -224,160 +119,195 @@ def build_runtime_image( See https://docs.all-hands.dev/modules/usage/architecture/runtime for more details. """ - # Calculate the hash for the docker build folder (source code and Dockerfile) - with tempfile.TemporaryDirectory() as temp_dir: - from_scratch_hash = prep_docker_build_folder( - temp_dir, - base_image=base_image, - skip_init=False, - extra_deps=extra_deps, - ) - - runtime_image_repo, runtime_image_tag = get_runtime_image_repo_and_tag(base_image) - - # The image name in the format : - hash_runtime_image_name = f'{runtime_image_repo}:{from_scratch_hash}' - - # non-hash generic image name, it could contain *similar* dependencies - # but *might* not exactly match the state of the source code. - # It resembles the "latest" tag in the docker image naming convention for - # a particular {repo}:{tag} pair (e.g., ubuntu:latest -> runtime:ubuntu_tag_latest) - # we will build from IT to save time if the `from_scratch_hash` is not found - generic_runtime_image_name = f'{runtime_image_repo}:{runtime_image_tag}' - - # Scenario 1: If we already have an image with the exact same hash, then it means the image is already built - # with the exact same source code and Dockerfile, so we will reuse it. Building it is not required. - if not force_rebuild and runtime_builder.image_exists( - hash_runtime_image_name, False - ): - logger.info( - f'Image [{hash_runtime_image_name}] already exists so we will reuse it.' - ) - return hash_runtime_image_name - - # Scenario 2: If a Docker image with the exact hash is not found, we will FIRST try to re-build it - # by leveraging the `generic_runtime_image_name` to save some time - # from re-building the dependencies (e.g., poetry install, apt install) - if not force_rebuild and runtime_builder.image_exists(generic_runtime_image_name): - logger.info( - f'Could not find docker image [{hash_runtime_image_name}]\n' - f'Will try to re-build it from latest [{generic_runtime_image_name}] image to potentially save ' - f'time for dependencies installation.\n' - ) - - cur_docker_build_folder = docker_build_folder or tempfile.mkdtemp() - _skip_init_hash = prep_docker_build_folder( - cur_docker_build_folder, - # we want to use the existing generic image as base - # so that we can leverage existing dependencies already installed in the image - base_image=generic_runtime_image_name, - skip_init=True, # skip init since we are re-using the existing image - extra_deps=extra_deps, - ) - - assert ( - _skip_init_hash != from_scratch_hash - ), f'The skip_init hash [{_skip_init_hash}] should not match the existing hash [{from_scratch_hash}]' - - if not dry_run: - _build_sandbox_image( - docker_folder=cur_docker_build_folder, + if build_folder is None: + with tempfile.TemporaryDirectory() as temp_dir: + result = build_runtime_image_in_folder( + base_image=base_image, runtime_builder=runtime_builder, - target_image_repo=runtime_image_repo, - # NOTE: WE ALWAYS use the "from_scratch_hash" tag for the target image - # otherwise, even if the source code is exactly the same, the image *might* be re-built - # because the same source code will generate different hash when skip_init=True/False - # since the Dockerfile is slightly different - target_image_hash_tag=from_scratch_hash, - target_image_tag=runtime_image_tag, + build_folder=Path(temp_dir), + extra_deps=extra_deps, + dry_run=dry_run, + force_rebuild=force_rebuild, ) - else: - logger.info( - f'Dry run: Skipping image build for [{generic_runtime_image_name}]' - ) - - if docker_build_folder is None: - shutil.rmtree(cur_docker_build_folder) + return result - # Scenario 3: If the Docker image with the required hash is not found AND we cannot re-use the latest - # relevant 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' - ) + result = build_runtime_image_in_folder( + base_image=base_image, + runtime_builder=runtime_builder, + build_folder=Path(build_folder), + extra_deps=extra_deps, + dry_run=dry_run, + force_rebuild=force_rebuild, + ) + return result - cur_docker_build_folder = docker_build_folder or tempfile.mkdtemp() - _new_from_scratch_hash = prep_docker_build_folder( - cur_docker_build_folder, - base_image, - skip_init=False, - extra_deps=extra_deps, - ) - assert ( - _new_from_scratch_hash == from_scratch_hash - ), f'The new from scratch hash [{_new_from_scratch_hash}] does not match the existing hash [{from_scratch_hash}]' +def build_runtime_image_in_folder( + base_image: str, + runtime_builder: RuntimeBuilder, + build_folder: Path, + extra_deps: str | None, + dry_run: bool, + force_rebuild: bool, +) -> str: + runtime_image_repo, _ = get_runtime_image_repo_and_tag(base_image) + lock_tag = f'oh_v{oh_version}_{get_hash_for_lock_files(base_image)}' + hash_tag = f'{lock_tag}_{get_hash_for_source_files()}' + hash_image_name = f'{runtime_image_repo}:{hash_tag}' + + if force_rebuild: + logger.info(f'Force rebuild: [{runtime_image_repo}:{hash_tag}] from scratch.') + prep_build_folder(build_folder, base_image, True, extra_deps) if not dry_run: _build_sandbox_image( - docker_folder=cur_docker_build_folder, - runtime_builder=runtime_builder, - target_image_repo=runtime_image_repo, - # NOTE: WE ALWAYS use the "from_scratch_hash" tag for the target image - target_image_hash_tag=from_scratch_hash, - target_image_tag=runtime_image_tag, - ) - else: - logger.info( - f'Dry run: Skipping image build for [{generic_runtime_image_name}]' + build_folder, + runtime_builder, + runtime_image_repo, + hash_tag, + lock_tag, ) + return hash_image_name + + lock_image_name = f'{runtime_image_repo}:{lock_tag}' + build_from_scratch = True + + # If the exact image already exists, we do not need to build it + if runtime_builder.image_exists(hash_image_name, False): + logger.info(f'Reusing Image [{hash_image_name}]') + return hash_image_name + + # We look for an existing image that shares the same lock_tag. If such an image exists, we + # can use it as the base image for the build and just copy source files. This makes the build + # much faster. + if runtime_builder.image_exists(lock_image_name): + logger.info(f'Build [{hash_image_name}] from [{lock_image_name}]') + build_from_scratch = False + base_image = lock_image_name + else: + logger.info(f'Build [{hash_image_name}] from scratch') + + prep_build_folder(build_folder, base_image, build_from_scratch, extra_deps) + if not dry_run: + _build_sandbox_image( + build_folder, + runtime_builder, + runtime_image_repo, + hash_tag, + lock_tag, + ) - if docker_build_folder is None: - shutil.rmtree(cur_docker_build_folder) - - return f'{runtime_image_repo}:{from_scratch_hash}' + return hash_image_name -def _build_sandbox_image( - docker_folder: str, - runtime_builder: RuntimeBuilder, - target_image_repo: str, - target_image_hash_tag: str, - target_image_tag: str, -) -> str: - """Build and tag the sandbox image. - The image will be tagged as both: - - target_image_hash_tag - - target_image_tag +def prep_build_folder( + build_folder: Path, + base_image: str, + build_from_scratch: bool, + extra_deps: str | None, +): + # Copy the source code to directory. It will end up in build_folder/code + # If package is not found, build from source code + openhands_source_dir = Path(openhands.__file__).parent + project_root = openhands_source_dir.parent + logger.info(f'Building source distribution using project root: {project_root}') + + # Copy the 'openhands' directory (Source code) + shutil.copytree( + openhands_source_dir, + Path(build_folder, 'code', 'openhands'), + ignore=shutil.ignore_patterns( + '.*/', + '__pycache__/', + '*.pyc', + '*.md', + ), + ) - Parameters: - - docker_folder (str): the path to the docker build folder - - runtime_builder (RuntimeBuilder): the runtime builder instance - - target_image_repo (str): the repository name for the target image - - target_image_hash_tag (str): the *hash* tag for the target image that is calculated based - on the contents of the docker build folder (source code and Dockerfile) - e.g. 1234567890abcdef - -target_image_tag (str): the tag for the target image that's generic and based on the base image name - e.g. oh_v0.9.3_image_ubuntu_tag_22.04 - """ - target_image_hash_name = f'{target_image_repo}:{target_image_hash_tag}' - target_image_generic_name = f'{target_image_repo}:{target_image_tag}' + # Copy pyproject.toml and poetry.lock files + for file in ['pyproject.toml', 'poetry.lock']: + src = Path(openhands_source_dir, file) + if not src.exists(): + src = Path(project_root, file) + shutil.copy2(src, Path(build_folder, 'code', file)) - tags_to_add = [target_image_hash_name] + # Create a Dockerfile and write it to build_folder + dockerfile_content = _generate_dockerfile( + base_image, + build_from_scratch=build_from_scratch, + extra_deps=extra_deps, + ) + with open(Path(build_folder, 'Dockerfile'), 'w') as file: # type: ignore + file.write(dockerfile_content) # type: ignore + + +_ALPHABET = string.digits + string.ascii_lowercase + + +def truncate_hash(hash: str) -> str: + """Convert the base16 hash to base36 and truncate at 16 characters.""" + value = int(hash, 16) + result: List[str] = [] + while value > 0 and len(result) < 16: + value, remainder = divmod(value, len(_ALPHABET)) + result.append(_ALPHABET[remainder]) + return ''.join(result) + + +def get_hash_for_lock_files(base_image: str): + openhands_source_dir = Path(openhands.__file__).parent + md5 = hashlib.md5() + md5.update(base_image.encode()) + for file in ['pyproject.toml', 'poetry.lock']: + src = Path(openhands_source_dir, file) + if not src.exists(): + src = Path(openhands_source_dir.parent, file) + with open(src, 'rb') as f: + for chunk in iter(lambda: f.read(4096), b''): + md5.update(chunk) + # We get away with truncation because we want something that is unique + # rather than something that is cryptographically secure + result = truncate_hash(md5.hexdigest()) + return result + + +def get_hash_for_source_files(): + openhands_source_dir = Path(openhands.__file__).parent + dir_hash = dirhash( + openhands_source_dir, + 'md5', + ignore=[ + '.*/', # hidden directories + '__pycache__/', + '*.pyc', + ], + ) + # We get away with truncation because we want something that is unique + # rather than something that is cryptographically secure + result = truncate_hash(dir_hash) + return result - # Only add the generic tag if the image does not exist - # so it does not get overwritten & only points to the earliest version - # to avoid "too many layers" after many re-builds - if not runtime_builder.image_exists(target_image_generic_name): - tags_to_add.append(target_image_generic_name) - try: - image_name = runtime_builder.build(path=docker_folder, tags=tags_to_add) - if not image_name: - raise RuntimeError(f'Build failed for image {target_image_hash_name}') - except Exception as e: - logger.error(f'Sandbox image build failed: {str(e)}') - raise +def _build_sandbox_image( + build_folder: Path, + runtime_builder: RuntimeBuilder, + runtime_image_repo: str, + hash_tag: str, + lock_tag: str, +): + """Build and tag the sandbox image. The image will be tagged with all tags that do not yet exist""" + + names = [ + name + for name in [ + f'{runtime_image_repo}:{hash_tag}', + f'{runtime_image_repo}:{lock_tag}', + ] + if not runtime_builder.image_exists(name, False) + ] + + image_name = runtime_builder.build(path=str(build_folder), tags=names) + if not image_name: + raise RuntimeError(f'Build failed for image {names}') return image_name @@ -416,7 +346,7 @@ def _build_sandbox_image( runtime_image_hash_name = build_runtime_image( args.base_image, runtime_builder=DockerRuntimeBuilder(docker.from_env()), - docker_build_folder=temp_dir, + build_folder=temp_dir, dry_run=True, force_rebuild=args.force_rebuild, ) diff --git a/openhands/runtime/utils/runtime_templates/Dockerfile.j2 b/openhands/runtime/utils/runtime_templates/Dockerfile.j2 index 9fb902252093..a236d3ca28f2 100644 --- a/openhands/runtime/utils/runtime_templates/Dockerfile.j2 +++ b/openhands/runtime/utils/runtime_templates/Dockerfile.j2 @@ -4,10 +4,13 @@ FROM {{ base_image }} ENV POETRY_VIRTUALENVS_PATH=/openhands/poetry ENV MAMBA_ROOT_PREFIX=/openhands/micromamba -{% if not skip_init %} +{% if build_from_scratch %} # ================================================================ # START: Build Runtime Image from Scratch # ================================================================ +# This is used in cases where the base image is something more generic like nikolaik/python-nodejs +# rather than the current OpenHands release + {% if 'ubuntu' in base_image and (base_image.endswith(':latest') or base_image.endswith(':24.04')) %} {% set LIBGL_MESA = 'libgl1' %} {% else %} @@ -38,24 +41,14 @@ RUN mkdir -p /openhands/micromamba/bin && \ RUN /openhands/micromamba/bin/micromamba create -n openhands -y && \ /openhands/micromamba/bin/micromamba install -n openhands -c conda-forge poetry python=3.12 -y -# ================================================================ -# END: Build Runtime Image from Scratch -# ================================================================ -{% endif %} - -# ================================================================ -# START: Copy Project and Install/Update Dependencies -# ================================================================ -RUN if [ -d /openhands/code ]; then rm -rf /openhands/code; fi -COPY ./code /openhands/code +# Create a clean openhands directory including only the pyproject.toml, poetry.lock and openhands/__init__.py +RUN \ + if [ -d /openhands/code ]; then rm -rf /openhands/code; fi && \ + mkdir -p /openhands/code/openhands && \ + touch /openhands/code/openhands/__init__.py +COPY ./code/pyproject.toml ./code/poetry.lock /openhands/code -# Below RUN command sets up the Python environment using Poetry, -# installs project dependencies, and configures the container -# for OpenHands development. -# It creates and activates a virtual environment, installs necessary -# tools like Playwright, sets up environment variables, and configures -# the bash environment to ensure the correct Python interpreter and -# virtual environment are used by default. +# Install all dependencies WORKDIR /openhands/code RUN \ /openhands/micromamba/bin/micromamba config set changeps1 False && \ @@ -70,16 +63,26 @@ RUN \ /openhands/micromamba/bin/micromamba run -n openhands poetry run playwright install --with-deps chromium && \ # Set environment variables 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/micromamba/bin/micromamba run -n openhands poetry cache clear --all . && \ # Set permissions - {% if not skip_init %}chmod -R g+rws /openhands/poetry && {% endif %} \ + chmod -R g+rws /openhands/poetry && \ 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/micromamba/bin/micromamba clean --all + +# ================================================================ +# END: Build Runtime Image from Scratch # ================================================================ -# END: Copy Project and Install/Update Dependencies +{% endif %} + # ================================================================ +# Copy Project source files +# ================================================================ +RUN if [ -d /openhands/code/openhands ]; then rm -rf /openhands/code/openhands; fi +COPY ./code/pyproject.toml ./code/poetry.lock /openhands/code +COPY ./code/openhands /openhands/code/openhands + +# Install extra dependencies if specified +{% if extra_deps %}RUN {{ extra_deps }} {% endif %} diff --git a/tests/unit/test_runtime_build.py b/tests/unit/test_runtime_build.py index a8d215544303..8cc56bbc330a 100644 --- a/tests/unit/test_runtime_build.py +++ b/tests/unit/test_runtime_build.py @@ -1,25 +1,29 @@ +import hashlib import os import tempfile import uuid from importlib.metadata import version -from unittest import mock -from unittest.mock import ANY, MagicMock, call, patch +from pathlib import Path +from unittest.mock import ANY, MagicMock, mock_open, patch import docker import pytest import toml from pytest import TempPathFactory +import openhands from openhands import __version__ as oh_version from openhands.core.logger import openhands_logger as logger from openhands.runtime.builder.docker import DockerRuntimeBuilder from openhands.runtime.utils.runtime_build import ( _generate_dockerfile, - _put_source_code_to_dir, build_runtime_image, + get_hash_for_lock_files, + get_hash_for_source_files, get_runtime_image_repo, get_runtime_image_repo_and_tag, - prep_docker_build_folder, + prep_build_folder, + truncate_hash, ) OH_VERSION = f'oh_v{oh_version}' @@ -73,39 +77,19 @@ def _check_source_code_in_dir(temp_dir): assert _pyproject_version == version('openhands-ai') -def test_put_source_code_to_dir(temp_dir): +def test_prep_build_folder(temp_dir): shutil_mock = MagicMock() - with patch(f'{_put_source_code_to_dir.__module__}.shutil', shutil_mock): - _put_source_code_to_dir(temp_dir) - shutil_mock.copytree.assert_called_once_with( - os.path.join(os.getcwd(), 'openhands'), - os.path.join(temp_dir, 'code', 'openhands'), - ) - shutil_mock.copy2.assert_has_calls( - [ - mock.call( - os.path.join(os.getcwd(), 'pyproject.toml'), - os.path.join(temp_dir, 'code', 'pyproject.toml'), - ), - mock.call( - os.path.join(os.getcwd(), 'poetry.lock'), - os.path.join(temp_dir, 'code', 'poetry.lock'), - ), - ] - ) - - -def test_docker_build_folder(temp_dir): - mock = MagicMock() - with patch(f'{_put_source_code_to_dir.__module__}._put_source_code_to_dir', mock): - prep_docker_build_folder( + with patch(f'{prep_build_folder.__module__}.shutil', shutil_mock): + prep_build_folder( temp_dir, base_image=DEFAULT_BASE_IMAGE, - skip_init=False, + build_from_scratch=True, + extra_deps=None, ) # make sure that the code was copied - mock.assert_called_once_with(temp_dir) + shutil_mock.copytree.assert_called_once() + assert shutil_mock.copy2.call_count == 2 # Now check dockerfile is in the folder dockerfile_path = os.path.join(temp_dir, 'Dockerfile') @@ -113,71 +97,40 @@ def test_docker_build_folder(temp_dir): assert os.path.isfile(dockerfile_path) -def test_hash_folder_same(temp_dir): - mock = ( - MagicMock() - ) # We don't actually need to copy the rest of the files to perform this check - with patch(f'{_put_source_code_to_dir.__module__}._put_source_code_to_dir', mock): - dir_hash_1 = prep_docker_build_folder( - temp_dir, - base_image=DEFAULT_BASE_IMAGE, - skip_init=False, - ) - - with tempfile.TemporaryDirectory() as temp_dir_2: - dir_hash_2 = prep_docker_build_folder( - temp_dir_2, - base_image=DEFAULT_BASE_IMAGE, - skip_init=False, - ) - assert dir_hash_1 == dir_hash_2 - - -def test_hash_folder_diff_init(temp_dir): - mock = ( - MagicMock() - ) # We don't actually need to copy the all of the files to perform this check - with patch(f'{_put_source_code_to_dir.__module__}._put_source_code_to_dir', mock): - dir_hash_1 = prep_docker_build_folder( - temp_dir, - base_image=DEFAULT_BASE_IMAGE, - skip_init=False, - ) - - with tempfile.TemporaryDirectory() as temp_dir_2: - dir_hash_2 = prep_docker_build_folder( - temp_dir_2, - base_image=DEFAULT_BASE_IMAGE, - skip_init=True, - ) - assert dir_hash_1 != dir_hash_2 - - -def test_hash_folder_diff_image(temp_dir): - mock = ( - MagicMock() - ) # We don't actually need to copy all of the files to perform this check - with patch(f'{_put_source_code_to_dir.__module__}._put_source_code_to_dir', mock): - dir_hash_1 = prep_docker_build_folder( - temp_dir, - base_image=DEFAULT_BASE_IMAGE, - skip_init=False, +def test_get_hash_for_lock_files(): + with patch('builtins.open', mock_open(read_data='mock-data'.encode())): + hash = get_hash_for_lock_files('some_base_image') + # Since we mocked open to always return "mock_data", the hash is the result + # of hashing the name of the base image followed by "mock-data" twice + md5 = hashlib.md5() + md5.update('some_base_image'.encode()) + for _ in range(2): + md5.update('mock-data'.encode()) + assert hash == truncate_hash(md5.hexdigest()) + + +def test_get_hash_for_source_files(): + dirhash_mock = MagicMock() + dirhash_mock.return_value = '1f69bd20d68d9e3874d5bf7f7459709b' + with patch(f'{get_hash_for_source_files.__module__}.dirhash', dirhash_mock): + result = get_hash_for_source_files() + assert result == truncate_hash(dirhash_mock.return_value) + dirhash_mock.assert_called_once_with( + Path(openhands.__file__).parent, + 'md5', + ignore=[ + '.*/', # hidden directories + '__pycache__/', + '*.pyc', + ], ) - with tempfile.TemporaryDirectory() as temp_dir_2: - dir_hash_2 = prep_docker_build_folder( - temp_dir_2, - base_image='debian:11', - skip_init=False, - ) - assert dir_hash_1 != dir_hash_2 - -def test_generate_dockerfile_scratch(): +def test_generate_dockerfile_build_from_scratch(): base_image = 'debian:11' dockerfile_content = _generate_dockerfile( base_image, - skip_init=False, + build_from_scratch=True, ) assert base_image in dockerfile_content assert 'apt-get update' in dockerfile_content @@ -186,29 +139,29 @@ def test_generate_dockerfile_scratch(): assert 'python=3.12' in dockerfile_content # Check the update command - assert 'COPY ./code /openhands/code' in dockerfile_content + assert 'COPY ./code/openhands /openhands/code/openhands' in dockerfile_content assert ( '/openhands/micromamba/bin/micromamba run -n openhands poetry install' in dockerfile_content ) -def test_generate_dockerfile_skip_init(): +def test_generate_dockerfile_build_from_existing(): base_image = 'debian:11' dockerfile_content = _generate_dockerfile( base_image, - skip_init=True, + build_from_scratch=False, ) - # These commands SHOULD NOT include in the dockerfile if skip_init is True + # These commands SHOULD NOT include in the dockerfile if build_from_scratch is False assert 'RUN apt update && apt install -y wget sudo' not in dockerfile_content assert '-c conda-forge' not in dockerfile_content assert 'python=3.12' not in dockerfile_content assert 'https://micro.mamba.pm/install.sh' not in dockerfile_content + assert 'poetry install' not in dockerfile_content # These update commands SHOULD still in the dockerfile - assert 'COPY ./code /openhands/code' in dockerfile_content - assert 'poetry install' in dockerfile_content + assert 'COPY ./code/openhands /openhands/code/openhands' in dockerfile_content def test_get_runtime_image_repo_and_tag_eventstream(): @@ -234,114 +187,96 @@ def test_get_runtime_image_repo_and_tag_eventstream(): ) -def test_build_runtime_image_from_scratch(temp_dir): +def test_build_runtime_image_from_scratch(): base_image = 'debian:11' - mock = ( - MagicMock() - ) # We don't actually need to copy all of the files to perform this check - with patch(f'{_put_source_code_to_dir.__module__}._put_source_code_to_dir', mock): - from_scratch_hash = prep_docker_build_folder( - temp_dir, - base_image, - skip_init=False, - ) - - mock_runtime_builder = MagicMock() - mock_runtime_builder.image_exists.return_value = False - mock_runtime_builder.build.return_value = ( - f'{get_runtime_image_repo()}:{from_scratch_hash}' - ) - + mock_lock_hash = MagicMock() + mock_lock_hash.return_value = 'mock-lock-hash' + mock_source_hash = MagicMock() + mock_source_hash.return_value = 'mock-source-hash' + mock_runtime_builder = MagicMock() + mock_runtime_builder.image_exists.return_value = False + mock_runtime_builder.build.return_value = ( + f'{get_runtime_image_repo()}:{OH_VERSION}_mock-lock-hash' + ) + mock_prep_build_folder = MagicMock() + mod = build_runtime_image.__module__ + with ( + patch(f'{mod}.get_hash_for_lock_files', mock_lock_hash), + patch(f'{mod}.get_hash_for_source_files', mock_source_hash), + patch( + f'{build_runtime_image.__module__}.prep_build_folder', + mock_prep_build_folder, + ), + ): image_name = build_runtime_image(base_image, mock_runtime_builder) mock_runtime_builder.build.assert_called_once_with( path=ANY, tags=[ - f'{get_runtime_image_repo()}:{from_scratch_hash}', - f'{get_runtime_image_repo()}:{OH_VERSION}_image_debian_tag_11', + f'{get_runtime_image_repo()}:{OH_VERSION}_mock-lock-hash_mock-source-hash', + f'{get_runtime_image_repo()}:{OH_VERSION}_mock-lock-hash', ], ) - assert image_name == f'{get_runtime_image_repo()}:{from_scratch_hash}' - - -def test_build_runtime_image_exact_hash_exist(temp_dir): - base_image = 'debian:11' - mock = ( - MagicMock() - ) # We don't actually need to copy all of the files to perform this check - with patch(f'{_put_source_code_to_dir.__module__}._put_source_code_to_dir', mock): - from_scratch_hash = prep_docker_build_folder( - temp_dir, - base_image, - skip_init=False, + assert ( + image_name + == f'{get_runtime_image_repo()}:{OH_VERSION}_mock-lock-hash_mock-source-hash' ) + mock_prep_build_folder.assert_called_once_with(ANY, base_image, True, None) - mock_runtime_builder = MagicMock() - mock_runtime_builder.image_exists.return_value = True - mock_runtime_builder.build.return_value = ( - f'{get_runtime_image_repo()}:{from_scratch_hash}' - ) +def test_build_runtime_image_exact_hash_exist(): + base_image = 'debian:11' + mock_lock_hash = MagicMock() + mock_lock_hash.return_value = 'mock-lock-hash' + mock_source_hash = MagicMock() + mock_source_hash.return_value = 'mock-source-hash' + mock_runtime_builder = MagicMock() + mock_runtime_builder.image_exists.return_value = True + mock_prep_build_folder = MagicMock() + mod = build_runtime_image.__module__ + with ( + patch(f'{mod}.get_hash_for_lock_files', mock_lock_hash), + patch(f'{mod}.get_hash_for_source_files', mock_source_hash), + patch( + f'{build_runtime_image.__module__}.prep_build_folder', + mock_prep_build_folder, + ), + ): image_name = build_runtime_image(base_image, mock_runtime_builder) - assert image_name == f'{get_runtime_image_repo()}:{from_scratch_hash}' + assert ( + image_name + == f'{get_runtime_image_repo()}:{OH_VERSION}_mock-lock-hash_mock-source-hash' + ) mock_runtime_builder.build.assert_not_called() + mock_prep_build_folder.assert_not_called() -@patch('openhands.runtime.utils.runtime_build._build_sandbox_image') -def test_build_runtime_image_exact_hash_not_exist(mock_build_sandbox_image, temp_dir): +def test_build_runtime_image_exact_hash_not_exist(): base_image = 'debian:11' - repo, latest_image_tag = get_runtime_image_repo_and_tag(base_image) - latest_image_name = f'{repo}:{latest_image_tag}' - - mock = ( - MagicMock() - ) # We don't actually need to copy all of the files to perform this check - with patch(f'{_put_source_code_to_dir.__module__}._put_source_code_to_dir', mock): - from_scratch_hash = prep_docker_build_folder( - temp_dir, - base_image, - skip_init=False, + mock_lock_hash = MagicMock() + mock_lock_hash.return_value = 'mock-lock-hash' + mock_source_hash = MagicMock() + mock_source_hash.return_value = 'mock-source-hash' + mock_runtime_builder = MagicMock() + mock_runtime_builder.image_exists.side_effect = [False, True, False, True] + mock_prep_build_folder = MagicMock() + mod = build_runtime_image.__module__ + with ( + patch(f'{mod}.get_hash_for_lock_files', mock_lock_hash), + patch(f'{mod}.get_hash_for_source_files', mock_source_hash), + patch( + f'{build_runtime_image.__module__}.prep_build_folder', + mock_prep_build_folder, + ), + ): + image_name = build_runtime_image(base_image, mock_runtime_builder) + assert ( + image_name + == f'{get_runtime_image_repo()}:{OH_VERSION}_mock-lock-hash_mock-source-hash' + ) + mock_runtime_builder.build.assert_called_once() + mock_prep_build_folder.assert_called_once_with( + ANY, f'{get_runtime_image_repo()}:{OH_VERSION}_mock-lock-hash', False, None ) - with tempfile.TemporaryDirectory() as temp_dir_2: - non_from_scratch_hash = prep_docker_build_folder( - temp_dir_2, - base_image, - skip_init=True, - ) - - mock_runtime_builder = MagicMock() - # Set up mock_runtime_builder.image_exists to return False then True - mock_runtime_builder.image_exists.side_effect = [False, True] - - with patch( - 'openhands.runtime.utils.runtime_build.prep_docker_build_folder' - ) as mock_prep_docker_build_folder: - mock_prep_docker_build_folder.side_effect = [ - from_scratch_hash, - non_from_scratch_hash, - ] - - image_name = build_runtime_image(base_image, mock_runtime_builder) - - mock_prep_docker_build_folder.assert_has_calls( - [ - call(ANY, base_image=base_image, skip_init=False, extra_deps=None), - call( - ANY, - base_image=latest_image_name, - skip_init=True, - extra_deps=None, - ), - ] - ) - - mock_build_sandbox_image.assert_called_once_with( - docker_folder=ANY, - runtime_builder=mock_runtime_builder, - target_image_repo=repo, - target_image_hash_tag=from_scratch_hash, - target_image_tag=latest_image_tag, - ) - assert image_name == f'{repo}:{from_scratch_hash}' # ============================== @@ -588,3 +523,10 @@ def test_image_exists_not_found(): mock_client.api.pull.assert_called_once_with( 'nonexistent', tag='image', stream=True, decode=True ) + + +def test_truncate_hash(): + truncated = truncate_hash('b08f254d76b1c6a7ad924708c0032251') + assert truncated == 'pma2wc71uq3c9a85' + truncated = truncate_hash('102aecc0cea025253c0278f54ebef078') + assert truncated == '4titk6gquia3taj5' From cc500a622a3a3f62284a8552d0bd1ba8dc06bef3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Oct 2024 15:29:01 +0000 Subject: [PATCH 04/27] chore(deps-dev): bump @testing-library/jest-dom from 6.5.0 to 6.6.1 in /frontend (#4456) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/package-lock.json | 8 ++++---- frontend/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 117e22570594..e8f08e19d4d0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -47,7 +47,7 @@ "@remix-run/dev": "^2.11.2", "@remix-run/testing": "^2.11.2", "@tailwindcss/typography": "^0.5.15", - "@testing-library/jest-dom": "^6.5.0", + "@testing-library/jest-dom": "^6.6.1", "@testing-library/react": "^16.0.1", "@testing-library/user-event": "^14.5.2", "@types/node": "^22.7.5", @@ -5814,9 +5814,9 @@ } }, "node_modules/@testing-library/jest-dom": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.5.0.tgz", - "integrity": "sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==", + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.1.tgz", + "integrity": "sha512-mNYIiAuP4yJwV2zBRQCV7PHoQwbb6/8TfMpPcwSUzcSVDJHWOXt6hjNtIN1v5knDmimYnjJxKhsoVd4LVGIO+w==", "dev": true, "dependencies": { "@adobe/css-tools": "^4.4.0", diff --git a/frontend/package.json b/frontend/package.json index 90722062b9b3..e05ade359d55 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -72,7 +72,7 @@ "@remix-run/dev": "^2.11.2", "@remix-run/testing": "^2.11.2", "@tailwindcss/typography": "^0.5.15", - "@testing-library/jest-dom": "^6.5.0", + "@testing-library/jest-dom": "^6.6.1", "@testing-library/react": "^16.0.1", "@testing-library/user-event": "^14.5.2", "@types/node": "^22.7.5", From ca3fbb2a8027f28e97b63256735d2c04397dfbc2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Oct 2024 15:29:23 +0000 Subject: [PATCH 05/27] chore(deps-dev): bump @types/node from 22.7.5 to 22.7.6 in /frontend (#4455) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/package-lock.json | 8 ++++---- frontend/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e8f08e19d4d0..f08544d6ea2a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -50,7 +50,7 @@ "@testing-library/jest-dom": "^6.6.1", "@testing-library/react": "^16.0.1", "@testing-library/user-event": "^14.5.2", - "@types/node": "^22.7.5", + "@types/node": "^22.7.6", "@types/react": "^18.3.11", "@types/react-dom": "^18.3.0", "@types/react-highlight": "^0.12.8", @@ -6028,9 +6028,9 @@ } }, "node_modules/@types/node": { - "version": "22.7.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", - "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "version": "22.7.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.6.tgz", + "integrity": "sha512-/d7Rnj0/ExXDMcioS78/kf1lMzYk4BZV8MZGTBKzTGZ6/406ukkbYlIsZmMPhcR5KlkunDHQLrtAVmSq7r+mSw==", "devOptional": true, "dependencies": { "undici-types": "~6.19.2" diff --git a/frontend/package.json b/frontend/package.json index e05ade359d55..42b439b09168 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -75,7 +75,7 @@ "@testing-library/jest-dom": "^6.6.1", "@testing-library/react": "^16.0.1", "@testing-library/user-event": "^14.5.2", - "@types/node": "^22.7.5", + "@types/node": "^22.7.6", "@types/react": "^18.3.11", "@types/react-dom": "^18.3.0", "@types/react-highlight": "^0.12.8", From 206788a0e88e84dda8743c662f059ddcf3156ce2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Oct 2024 15:31:07 +0000 Subject: [PATCH 06/27] chore(deps): bump react-syntax-highlighter from 15.5.0 to 15.6.1 in /frontend (#4457) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/package-lock.json | 14 ++++++++++---- frontend/package.json | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f08544d6ea2a..0ad8a5d9fc4c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -35,7 +35,7 @@ "react-markdown": "^9.0.1", "react-redux": "^9.1.2", "react-router-dom": "^6.26.1", - "react-syntax-highlighter": "^15.5.0", + "react-syntax-highlighter": "^15.6.1", "remark-gfm": "^4.0.0", "sirv-cli": "^3.0.0", "tailwind-merge": "^2.5.4", @@ -11043,6 +11043,11 @@ "node": "*" } }, + "node_modules/highlightjs-vue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz", + "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==" + }, "node_modules/hosted-git-info": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.1.tgz", @@ -20734,12 +20739,13 @@ } }, "node_modules/react-syntax-highlighter": { - "version": "15.5.0", - "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz", - "integrity": "sha512-+zq2myprEnQmH5yw6Gqc8lD55QHnpKaU8TOcFeC/Lg/MQSs8UknEA0JC4nTZGFAXC2J2Hyj/ijJ7NlabyPi2gg==", + "version": "15.6.1", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz", + "integrity": "sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==", "dependencies": { "@babel/runtime": "^7.3.1", "highlight.js": "^10.4.1", + "highlightjs-vue": "^1.0.0", "lowlight": "^1.17.0", "prismjs": "^1.27.0", "refractor": "^3.6.0" diff --git a/frontend/package.json b/frontend/package.json index 42b439b09168..6995b97efa55 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -34,7 +34,7 @@ "react-markdown": "^9.0.1", "react-redux": "^9.1.2", "react-router-dom": "^6.26.1", - "react-syntax-highlighter": "^15.5.0", + "react-syntax-highlighter": "^15.6.1", "remark-gfm": "^4.0.0", "sirv-cli": "^3.0.0", "tailwind-merge": "^2.5.4", From ad800bf373dffeec09efe0ec6ec3795b7deca177 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Oct 2024 17:51:45 +0200 Subject: [PATCH 07/27] chore(deps): bump litellm from 1.49.5 to 1.49.6 (#4458) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 2d24971cb3ad..4b3920ddc41a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3879,13 +3879,13 @@ types-tqdm = "*" [[package]] name = "litellm" -version = "1.49.5" +version = "1.49.6" description = "Library to easily interface with LLM API providers" optional = false python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8" files = [ - {file = "litellm-1.49.5-py3-none-any.whl", hash = "sha256:9cd246221c1d922edb7614f29b5b5618b9da588f0ac04612965dbd8afeaa130f"}, - {file = "litellm-1.49.5.tar.gz", hash = "sha256:f45667d723d77d235dc3cc3b056338012eb2ffbfd46d7beb7abea927f49358e0"}, + {file = "litellm-1.49.6-py3-none-any.whl", hash = "sha256:c3b1dc25861850e8c2409e14198325019fe22680bf0c455e0124c23e6b5de318"}, + {file = "litellm-1.49.6.tar.gz", hash = "sha256:cd82b332e11d80bd7e8b866b407bfbda8afe8bcf109687be5747f0f85f09c74a"}, ] [package.dependencies] From 678630c5bd8b1c82eefe79083072bc7ffaf15a1b Mon Sep 17 00:00:00 2001 From: "sp.wack" <83104063+amanape@users.noreply.github.com> Date: Thu, 17 Oct 2024 20:17:44 +0400 Subject: [PATCH 08/27] fix(frontend): Catch config fetch error and set default fallback (#4453) --- frontend/src/routes/_oh.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/src/routes/_oh.tsx b/frontend/src/routes/_oh.tsx index 0449443dbdc7..c72bafa31580 100644 --- a/frontend/src/routes/_oh.tsx +++ b/frontend/src/routes/_oh.tsx @@ -26,8 +26,12 @@ import NewProjectIcon from "#/assets/new-project.svg?react"; import DocsIcon from "#/assets/docs.svg?react"; export const clientLoader = async () => { - const config = await OpenHands.getConfig(); - window.__APP_MODE__ = config.APP_MODE; + try { + const config = await OpenHands.getConfig(); + window.__APP_MODE__ = config.APP_MODE; + } catch (error) { + window.__APP_MODE__ = "oss"; + } let token = localStorage.getItem("token"); const ghToken = localStorage.getItem("ghToken"); From 154854bbe3367b7804619cd01acb9d00b6b3fc75 Mon Sep 17 00:00:00 2001 From: Robert Brennan Date: Thu, 17 Oct 2024 12:40:47 -0400 Subject: [PATCH 09/27] run in dev mode in makefile (#4452) --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 6c89b0458697..7b9e142dd1a0 100644 --- a/Makefile +++ b/Makefile @@ -195,7 +195,7 @@ start-backend: # Start frontend start-frontend: @echo "$(YELLOW)Starting frontend...$(RESET)" - @cd frontend && VITE_BACKEND_HOST=$(BACKEND_HOST_PORT) VITE_FRONTEND_PORT=$(FRONTEND_PORT) npm run start -- --port $(FRONTEND_PORT) + @cd frontend && VITE_BACKEND_HOST=$(BACKEND_HOST_PORT) VITE_FRONTEND_PORT=$(FRONTEND_PORT) npm run dev -- --port $(FRONTEND_PORT) # Common setup for running the app (non-callable) _run_setup: @@ -214,7 +214,7 @@ _run_setup: run: @echo "$(YELLOW)Running the app...$(RESET)" @$(MAKE) -s _run_setup - @cd frontend && echo "$(BLUE)Starting frontend with npm...$(RESET)" && npm run start -- --port $(FRONTEND_PORT) + @cd frontend && echo "$(BLUE)Starting frontend with npm...$(RESET)" && npm run dev -- --port $(FRONTEND_PORT) @echo "$(GREEN)Application started successfully.$(RESET)" # Run the app (in docker) From 6cb174b7d194085ffcafa120522af27cafb1225f Mon Sep 17 00:00:00 2001 From: "sp.wack" <83104063+amanape@users.noreply.github.com> Date: Thu, 17 Oct 2024 21:14:55 +0400 Subject: [PATCH 10/27] [ALL-557] feat(frontend): Add save and discard actions to the editor (#4442) Co-authored-by: mamoodi --- frontend/src/components/editor-actions.tsx | 62 ++++++++++++++++++++ frontend/src/context/files.tsx | 17 ++++-- frontend/src/routes/_oh.app._index/route.tsx | 39 +++++++++++- 3 files changed, 112 insertions(+), 6 deletions(-) create mode 100644 frontend/src/components/editor-actions.tsx diff --git a/frontend/src/components/editor-actions.tsx b/frontend/src/components/editor-actions.tsx new file mode 100644 index 000000000000..e66e9553e6f4 --- /dev/null +++ b/frontend/src/components/editor-actions.tsx @@ -0,0 +1,62 @@ +import { cn } from "@nextui-org/react"; +import { HTMLAttributes } from "react"; + +interface EditorActionButtonProps { + onClick: () => void; + disabled: boolean; + className: HTMLAttributes["className"]; +} + +function EditorActionButton({ + onClick, + disabled, + className, + children, +}: React.PropsWithChildren) { + return ( + + ); +} + +interface EditorActionsProps { + onSave: () => void; + onDiscard: () => void; + isDisabled: boolean; +} + +export function EditorActions({ + onSave, + onDiscard, + isDisabled, +}: EditorActionsProps) { + return ( +
+ + Save + + + + Discard + +
+ ); +} diff --git a/frontend/src/context/files.tsx b/frontend/src/context/files.tsx index c600eb08ca71..cc1f5d7311fa 100644 --- a/frontend/src/context/files.tsx +++ b/frontend/src/context/files.tsx @@ -27,6 +27,7 @@ interface FilesContextType { modifiedFiles: Record; modifyFileContent: (path: string, content: string) => void; saveFileContent: (path: string) => string | undefined; + discardChanges: (path: string) => void; } const FilesContext = React.createContext( @@ -62,19 +63,25 @@ function FilesProvider({ children }: FilesProviderProps) { [files, modifiedFiles], ); + const discardChanges = React.useCallback((path: string) => { + setModifiedFiles((prev) => { + const newModifiedFiles = { ...prev }; + delete newModifiedFiles[path]; + return newModifiedFiles; + }); + }, []); + const saveFileContent = React.useCallback( (path: string): string | undefined => { const content = modifiedFiles[path]; if (content) { setFiles((prev) => ({ ...prev, [path]: content })); - const newModifiedFiles = { ...modifiedFiles }; - delete newModifiedFiles[path]; - setModifiedFiles(newModifiedFiles); + discardChanges(path); } return content; }, - [files, modifiedFiles, selectedPath], + [files, modifiedFiles, selectedPath, discardChanges], ); const value = React.useMemo( @@ -88,6 +95,7 @@ function FilesProvider({ children }: FilesProviderProps) { modifiedFiles, modifyFileContent, saveFileContent, + discardChanges, }), [ paths, @@ -99,6 +107,7 @@ function FilesProvider({ children }: FilesProviderProps) { modifiedFiles, modifyFileContent, saveFileContent, + discardChanges, ], ); diff --git a/frontend/src/routes/_oh.app._index/route.tsx b/frontend/src/routes/_oh.app._index/route.tsx index 9a5c0a73e957..5cc533e15d0f 100644 --- a/frontend/src/routes/_oh.app._index/route.tsx +++ b/frontend/src/routes/_oh.app._index/route.tsx @@ -13,6 +13,7 @@ import OpenHands from "#/api/open-hands"; import { useSocket } from "#/context/socket"; import CodeEditorCompoonent from "./code-editor-component"; import { useFiles } from "#/context/files"; +import { EditorActions } from "#/components/editor-actions"; export const clientLoader = async () => { const token = localStorage.getItem("token"); @@ -48,7 +49,13 @@ export function ErrorBoundary() { function CodeEditor() { const { token } = useLoaderData(); const { runtimeActive } = useSocket(); - const { setPaths } = useFiles(); + const { + setPaths, + selectedPath, + modifiedFiles, + saveFileContent: saveNewFileContent, + discardChanges, + } = useFiles(); const agentState = useSelector( (state: RootState) => state.agent.curAgentState, @@ -68,10 +75,38 @@ function CodeEditor() { [agentState], ); + const handleSave = async () => { + if (selectedPath) { + const content = saveNewFileContent(selectedPath); + + if (content && token) { + try { + await OpenHands.saveFile(token, selectedPath, content); + } catch (error) { + // handle error + } + } + } + }; + + const handleDiscard = () => { + if (selectedPath) discardChanges(selectedPath); + }; + return (
-
+
+ {selectedPath && ( +
+ {selectedPath} + +
+ )}
From 642e01b67395810d418421a51943b9eca6a38899 Mon Sep 17 00:00:00 2001 From: "sp.wack" <83104063+amanape@users.noreply.github.com> Date: Thu, 17 Oct 2024 23:24:49 +0400 Subject: [PATCH 11/27] fix(frontend): Update build directory and referenced paths (#4461) --- containers/app/Dockerfile | 2 +- frontend/package.json | 2 +- frontend/vite.config.ts | 41 ++++++++++++++++++++++++++++++++------ openhands/server/listen.py | 2 +- 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/containers/app/Dockerfile b/containers/app/Dockerfile index 507f45659b01..78faa1215a8d 100644 --- a/containers/app/Dockerfile +++ b/containers/app/Dockerfile @@ -90,7 +90,7 @@ RUN python openhands/core/download.py # No-op to download assets # openhands:openhands -> openhands:app RUN find /app \! -group app -exec chgrp app {} + -COPY --chown=openhands:app --chmod=770 --from=frontend-builder /app/build/client ./frontend/build +COPY --chown=openhands:app --chmod=770 --from=frontend-builder /app/build ./frontend/build COPY --chown=openhands:app --chmod=770 ./containers/app/entrypoint.sh /app/entrypoint.sh USER root diff --git a/frontend/package.json b/frontend/package.json index 6995b97efa55..5fe066c65838 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -46,7 +46,7 @@ "dev": "npm run make-i18n && VITE_MOCK_API=false remix vite:dev", "dev:mock": "npm run make-i18n && VITE_MOCK_API=true remix vite:dev", "build": "npm run make-i18n && tsc && remix vite:build", - "start": "npx sirv-cli build/client/ --single", + "start": "npx sirv-cli build/ --single", "test": "vitest run", "test:coverage": "npm run make-i18n && vitest run --coverage", "dev_wsl": "VITE_WATCH_USE_POLLING=true vite", diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index c5c8c7ebdae6..d9ed9134f8e3 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -8,11 +8,10 @@ import { vitePlugin as remix } from "@remix-run/dev"; export default defineConfig(({ mode }) => { const { - VITE_BACKEND_HOST = "127.0.0.1:3000", - VITE_USE_TLS = "false", - VITE_FRONTEND_PORT = "3001", - VITE_INSECURE_SKIP_VERIFY = "false", - VITE_WATCH_USE_POLLING = "false", + VITE_BACKEND_HOST = "127.0.0.1:3000", + VITE_USE_TLS = "false", + VITE_FRONTEND_PORT = "3001", + VITE_INSECURE_SKIP_VERIFY = "false", } = loadEnv(mode, process.cwd()); const USE_TLS = VITE_USE_TLS === "true"; @@ -23,6 +22,35 @@ export default defineConfig(({ mode }) => { const API_URL = `${PROTOCOL}://${VITE_BACKEND_HOST}/`; const WS_URL = `${WS_PROTOCOL}://${VITE_BACKEND_HOST}/`; const FE_PORT = Number.parseInt(VITE_FRONTEND_PORT, 10); + + /** + * This script is used to unpack the client directory from the frontend build directory. + * Remix SPA mode builds the client directory into the build directory. This function + * moves the contents of the client directory to the build directory and then removes the + * client directory. + * + * This script is used in the buildEnd function of the Vite config. + */ + const unpackClientDirectory = async () => { + const fs = await import("fs"); + const path = await import("path"); + + const buildDir = path.resolve(__dirname, "build"); + const clientDir = path.resolve(buildDir, "client"); + + const files = await fs.promises.readdir(clientDir); + await Promise.all( + files.map((file) => + fs.promises.rename( + path.resolve(clientDir, file), + path.resolve(buildDir, file), + ), + ), + ); + + await fs.promises.rmdir(clientDir); + }; + return { plugins: [ !process.env.VITEST && @@ -33,6 +61,7 @@ export default defineConfig(({ mode }) => { v3_throwAbortReason: true, }, appDirectory: "src", + buildEnd: unpackClientDirectory, ssr: false, }), viteTsconfigPaths(), @@ -67,5 +96,5 @@ export default defineConfig(({ mode }) => { include: ["src/**/*.{ts,tsx}"], }, }, - } + }; }); diff --git a/openhands/server/listen.py b/openhands/server/listen.py index 2f472823672d..32c93a117e23 100644 --- a/openhands/server/listen.py +++ b/openhands/server/listen.py @@ -798,4 +798,4 @@ def github_callback(auth_code: AuthCode): ) -app.mount('/', StaticFiles(directory='./frontend/build/client', html=True), name='dist') +app.mount('/', StaticFiles(directory='./frontend/build', html=True), name='dist') From ec3152b6e1b96f0f68876cb134d063413f36221c Mon Sep 17 00:00:00 2001 From: Xingyao Wang Date: Thu, 17 Oct 2024 14:57:03 -0500 Subject: [PATCH 12/27] linter: only lint on updated lines in the new file (#4409) --- openhands/linter/languages/python.py | 25 +- openhands/linter/linter.py | 87 ++++++ openhands/utils/diff.py | 41 +++ poetry.lock | 4 +- pyproject.toml | 1 + tests/unit/linters/test_lint_diff.py | 417 +++++++++++++++++++++++++++ 6 files changed, 571 insertions(+), 4 deletions(-) create mode 100644 openhands/utils/diff.py create mode 100644 tests/unit/linters/test_lint_diff.py diff --git a/openhands/linter/languages/python.py b/openhands/linter/languages/python.py index f3e201f3b477..9b7e944a2868 100644 --- a/openhands/linter/languages/python.py +++ b/openhands/linter/languages/python.py @@ -1,5 +1,6 @@ from typing import List +from openhands.core.logger import openhands_logger as logger from openhands.linter.base import BaseLinter, LintResult from openhands.linter.utils import run_cmd @@ -39,11 +40,31 @@ def flake_lint(filepath: str) -> list[LintResult]: _msg = parts[3].strip() if len(parts) > 4: _msg += ': ' + parts[4].strip() + + try: + line_num = int(parts[1]) + except ValueError as e: + logger.warning( + f'Error parsing flake8 output for line: {e}. Parsed parts: {parts}. Skipping...' + ) + continue + + try: + column_num = int(parts[2]) + except ValueError as e: + column_num = 1 + _msg = ( + parts[2].strip() + ' ' + _msg + ) # add the unparsed message to the original message + logger.warning( + f'Error parsing flake8 output for column: {e}. Parsed parts: {parts}. Using default column 1.' + ) + results.append( LintResult( file=filepath, - line=int(parts[1]), - column=int(parts[2]), + line=line_num, + column=column_num, message=_msg, ) ) diff --git a/openhands/linter/linter.py b/openhands/linter/linter.py index 7b73e57f5d8a..a7a4a23ca2c1 100644 --- a/openhands/linter/linter.py +++ b/openhands/linter/linter.py @@ -1,5 +1,6 @@ import os from collections import defaultdict +from difflib import SequenceMatcher from openhands.linter.base import BaseLinter, LinterException, LintResult from openhands.linter.languages.python import PythonLinter @@ -33,3 +34,89 @@ def lint(self, file_path: str) -> list[LintResult]: if res: return res return [] + + def lint_file_diff( + self, original_file_path: str, updated_file_path: str + ) -> list[LintResult]: + """Only return lint errors that are introduced by the diff. + + Args: + original_file_path: The original file path. + updated_file_path: The updated file path. + + Returns: + A list of lint errors that are introduced by the diff. + """ + # 1. Lint the original and updated file + original_lint_errors: list[LintResult] = self.lint(original_file_path) + updated_lint_errors: list[LintResult] = self.lint(updated_file_path) + + # 2. Load the original and updated file content + with open(original_file_path, 'r') as f: + old_lines = f.readlines() + with open(updated_file_path, 'r') as f: + new_lines = f.readlines() + + # 3. Get line numbers that are changed & unchanged + # Map the line number of the original file to the updated file + # NOTE: this only works for lines that are not changed (i.e., equal) + old_to_new_line_no_mapping: dict[int, int] = {} + replace_or_inserted_lines: list[int] = [] + for ( + tag, + old_idx_start, + old_idx_end, + new_idx_start, + new_idx_end, + ) in SequenceMatcher( + isjunk=None, + a=old_lines, + b=new_lines, + ).get_opcodes(): + if tag == 'equal': + for idx, _ in enumerate(old_lines[old_idx_start:old_idx_end]): + old_to_new_line_no_mapping[old_idx_start + idx + 1] = ( + new_idx_start + idx + 1 + ) + elif tag == 'replace' or tag == 'insert': + for idx, _ in enumerate(old_lines[old_idx_start:old_idx_end]): + replace_or_inserted_lines.append(new_idx_start + idx + 1) + else: + # omit the case of delete + pass + + # 4. Get pre-existing errors in unchanged lines + # increased error elsewhere introduced by the newlines + # i.e., we omit errors that are already in original files and report new one + new_line_no_to_original_errors: dict[int, list[LintResult]] = defaultdict(list) + for error in original_lint_errors: + if error.line in old_to_new_line_no_mapping: + new_line_no_to_original_errors[ + old_to_new_line_no_mapping[error.line] + ].append(error) + + # 5. Select errors from lint results in new file to report + selected_errors = [] + for error in updated_lint_errors: + # 5.1. Error introduced by replace/insert + if error.line in replace_or_inserted_lines: + selected_errors.append(error) + # 5.2. Error introduced by modified lines that impacted + # the unchanged lines that HAVE pre-existing errors + elif error.line in new_line_no_to_original_errors: + # skip if the error is already reported + # or add if the error is new + if not any( + original_error.message == error.message + and original_error.column == error.column + for original_error in new_line_no_to_original_errors[error.line] + ): + selected_errors.append(error) + # 5.3. Error introduced by modified lines that impacted + # the unchanged lines that have NO pre-existing errors + else: + selected_errors.append(error) + + # 6. Sort errors by line and column + selected_errors.sort(key=lambda x: (x.line, x.column)) + return selected_errors diff --git a/openhands/utils/diff.py b/openhands/utils/diff.py new file mode 100644 index 000000000000..71d00a2eb943 --- /dev/null +++ b/openhands/utils/diff.py @@ -0,0 +1,41 @@ +import difflib + +import whatthepatch + + +def get_diff(old_contents: str, new_contents: str, filepath: str = 'file') -> str: + diff = list( + difflib.unified_diff( + old_contents.split('\n'), + new_contents.split('\n'), + fromfile=filepath, + tofile=filepath, + # do not output unchange lines + # because they can cause `parse_diff` to fail + n=0, + ) + ) + return '\n'.join(map(lambda x: x.rstrip(), diff)) + + +def parse_diff(diff_patch: str) -> list[whatthepatch.patch.Change]: + # handle empty patch + if diff_patch.strip() == '': + return [] + + patch = whatthepatch.parse_patch(diff_patch) + patch_list = list(patch) + assert len(patch_list) == 1, ( + 'parse_diff only supports single file diff. But got:\nPATCH:\n' + + diff_patch + + '\nPATCH LIST:\n' + + str(patch_list) + ) + changes = patch_list[0].changes + + # ignore changes that are the same (i.e., old_lineno == new_lineno) + output_changes = [] + for change in changes: + if change.old != change.new: + output_changes.append(change) + return output_changes diff --git a/poetry.lock b/poetry.lock index 4b3920ddc41a..24b8f148a06c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "aenum" @@ -10001,4 +10001,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "f0d6c96fb36fd6ff330f27b0bf8a2051099b1c46f8e3b03d0d530025c87c92af" +content-hash = "431cd8f4b8a41e6c52c79cd18a55654388c8d15dcdabfa1487dda17cf6caa3e4" diff --git a/pyproject.toml b/pyproject.toml index 2ba189915235..46c3e19f3fc2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,6 +52,7 @@ python-pptx = "*" pylatexenc = "*" tornado = "*" python-dotenv = "*" +whatthepatch = "^1.0.6" protobuf = "^4.21.6,<5.0.0" # chromadb currently fails on 5.0+ opentelemetry-api = "1.25.0" opentelemetry-exporter-otlp-proto-grpc = "1.25.0" diff --git a/tests/unit/linters/test_lint_diff.py b/tests/unit/linters/test_lint_diff.py new file mode 100644 index 000000000000..f3b560c3df32 --- /dev/null +++ b/tests/unit/linters/test_lint_diff.py @@ -0,0 +1,417 @@ +from openhands.linter import DefaultLinter, LintResult +from openhands.utils.diff import get_diff, parse_diff + +OLD_CONTENT = """ +def foo(): + print("Hello, World!") + x = UNDEFINED_VARIABLE +foo() +""" + +NEW_CONTENT_V1 = ( + OLD_CONTENT + + """ +def new_function_that_causes_error(): + y = ANOTHER_UNDEFINED_VARIABLE +""" +) + +NEW_CONTENT_V2 = """ +def foo(): + print("Hello, World!") + x = UNDEFINED_VARIABLE + y = ANOTHER_UNDEFINED_VARIABLE +foo() +""" + + +def test_get_and_parse_diff(tmp_path): + diff = get_diff(OLD_CONTENT, NEW_CONTENT_V1, 'test.py') + print(diff) + assert ( + diff + == """ +--- test.py ++++ test.py +@@ -6,0 +7,3 @@ ++def new_function_that_causes_error(): ++ y = ANOTHER_UNDEFINED_VARIABLE ++ +""".strip() + ) + + print( + '\n'.join( + [f'{i+1}|{line}' for i, line in enumerate(NEW_CONTENT_V1.splitlines())] + ) + ) + changes = parse_diff(diff) + assert len(changes) == 3 + assert ( + changes[0].old is None + and changes[0].new == 7 + and changes[0].line == 'def new_function_that_causes_error():' + ) + assert ( + changes[1].old is None + and changes[1].new == 8 + and changes[1].line == ' y = ANOTHER_UNDEFINED_VARIABLE' + ) + assert changes[2].old is None and changes[2].new == 9 and changes[2].line == '' + + +def test_lint_with_diff_append(tmp_path): + with open(tmp_path / 'old.py', 'w') as f: + f.write(OLD_CONTENT) + with open(tmp_path / 'new.py', 'w') as f: + f.write(NEW_CONTENT_V1) + + linter = DefaultLinter() + result: list[LintResult] = linter.lint_file_diff( + str(tmp_path / 'old.py'), + str(tmp_path / 'new.py'), + ) + print(result) + assert len(result) == 1 + assert ( + result[0].line == 8 + and result[0].column == 9 + and result[0].message == "F821 undefined name 'ANOTHER_UNDEFINED_VARIABLE'" + ) + + +def test_lint_with_diff_insert(tmp_path): + with open(tmp_path / 'old.py', 'w') as f: + f.write(OLD_CONTENT) + with open(tmp_path / 'new.py', 'w') as f: + f.write(NEW_CONTENT_V2) + + linter = DefaultLinter() + result: list[LintResult] = linter.lint_file_diff( + str(tmp_path / 'old.py'), + str(tmp_path / 'new.py'), + ) + assert len(result) == 1 + assert ( + result[0].line == 5 + and result[0].column == 9 + and result[0].message == "F821 undefined name 'ANOTHER_UNDEFINED_VARIABLE'" + ) + + +def test_lint_with_multiple_changes_and_errors(tmp_path): + old_content = """ +def foo(): + print("Hello, World!") + x = 10 +foo() +""" + new_content = """ +def foo(): + print("Hello, World!") + x = UNDEFINED_VARIABLE + y = 20 + +def bar(): + z = ANOTHER_UNDEFINED_VARIABLE + return z + 1 + +foo() +bar() +""" + with open(tmp_path / 'old.py', 'w') as f: + f.write(old_content) + with open(tmp_path / 'new.py', 'w') as f: + f.write(new_content) + + linter = DefaultLinter() + result: list[LintResult] = linter.lint_file_diff( + str(tmp_path / 'old.py'), + str(tmp_path / 'new.py'), + ) + assert len(result) == 2 + assert ( + result[0].line == 4 + and result[0].column == 9 + and result[0].message == "F821 undefined name 'UNDEFINED_VARIABLE'" + ) + assert ( + result[1].line == 8 + and result[1].column == 9 + and result[1].message == "F821 undefined name 'ANOTHER_UNDEFINED_VARIABLE'" + ) + + +def test_lint_with_introduced_and_fixed_errors(tmp_path): + old_content = """ +x = UNDEFINED_VARIABLE +y = 10 +""" + new_content = """ +x = 5 +y = ANOTHER_UNDEFINED_VARIABLE +z = UNDEFINED_VARIABLE +""" + with open(tmp_path / 'old.py', 'w') as f: + f.write(old_content) + with open(tmp_path / 'new.py', 'w') as f: + f.write(new_content) + + linter = DefaultLinter() + result: list[LintResult] = linter.lint_file_diff( + str(tmp_path / 'old.py'), + str(tmp_path / 'new.py'), + ) + assert len(result) == 2 + assert ( + result[0].line == 3 + and result[0].column == 5 + and result[0].message == "F821 undefined name 'ANOTHER_UNDEFINED_VARIABLE'" + ) + assert ( + result[1].line == 4 + and result[1].column == 5 + and result[1].message == "F821 undefined name 'UNDEFINED_VARIABLE'" + ) + + +def test_lint_with_multiline_changes(tmp_path): + old_content = """ +def complex_function(a, b, c): + return (a + + b + + c) +""" + new_content = """ +def complex_function(a, b, c): + return (a + + UNDEFINED_VARIABLE + + b + + c) +""" + with open(tmp_path / 'old.py', 'w') as f: + f.write(old_content) + with open(tmp_path / 'new.py', 'w') as f: + f.write(new_content) + + linter = DefaultLinter() + result: list[LintResult] = linter.lint_file_diff( + str(tmp_path / 'old.py'), + str(tmp_path / 'new.py'), + ) + assert len(result) == 1 + assert ( + result[0].line == 4 + and result[0].column == 13 + and result[0].message == "F821 undefined name 'UNDEFINED_VARIABLE'" + ) + + +def test_lint_with_syntax_error(tmp_path): + old_content = """ +def foo(): + print("Hello, World!") +""" + new_content = """ +def foo(): + print("Hello, World!" +""" + with open(tmp_path / 'old.py', 'w') as f: + f.write(old_content) + with open(tmp_path / 'new.py', 'w') as f: + f.write(new_content) + + linter = DefaultLinter() + result: list[LintResult] = linter.lint_file_diff( + str(tmp_path / 'old.py'), + str(tmp_path / 'new.py'), + ) + assert len(result) == 1 + assert ( + result[0].line == 3 + and result[0].column == 11 + and result[0].message == "E999 SyntaxError: '(' was never closed" + ) + + +def test_lint_with_docstring_changes(tmp_path): + old_content = ''' +def foo(): + """This is a function.""" + print("Hello, World!") +''' + new_content = ''' +def foo(): + """ + This is a function. + It now has a multi-line docstring with an UNDEFINED_VARIABLE. + """ + print("Hello, World!") +''' + with open(tmp_path / 'old.py', 'w') as f: + f.write(old_content) + with open(tmp_path / 'new.py', 'w') as f: + f.write(new_content) + + linter = DefaultLinter() + result: list[LintResult] = linter.lint_file_diff( + str(tmp_path / 'old.py'), + str(tmp_path / 'new.py'), + ) + assert len(result) == 0 # Linter should ignore changes in docstrings + + +def test_lint_with_multiple_errors_on_same_line(tmp_path): + old_content = """ +def foo(): + print("Hello, World!") + x = 10 +foo() +""" + new_content = """ +def foo(): + print("Hello, World!") + x = UNDEFINED_VARIABLE + ANOTHER_UNDEFINED_VARIABLE +foo() +""" + with open(tmp_path / 'old.py', 'w') as f: + f.write(old_content) + with open(tmp_path / 'new.py', 'w') as f: + f.write(new_content) + + linter = DefaultLinter() + result: list[LintResult] = linter.lint_file_diff( + str(tmp_path / 'old.py'), + str(tmp_path / 'new.py'), + ) + print(result) + assert len(result) == 2 + assert ( + result[0].line == 4 + and result[0].column == 9 + and result[0].message == "F821 undefined name 'UNDEFINED_VARIABLE'" + ) + assert ( + result[1].line == 4 + and result[1].column == 30 + and result[1].message == "F821 undefined name 'ANOTHER_UNDEFINED_VARIABLE'" + ) + + +def test_parse_diff_with_empty_patch(): + diff_patch = '' + changes = parse_diff(diff_patch) + assert len(changes) == 0 + + +def test_lint_file_diff_ignore_existing_errors(tmp_path): + """ + Make sure we allow edits as long as it does not introduce new errors. In other + words, we don't care about existing linting errors. Although they might be + real syntax issues, sometimes they are just false positives, or errors that + we don't care about. + """ + content = """def some_valid_but_weird_function(): + # this function is legitimate, yet static analysis tools like flake8 + # reports 'F821 undefined name' + if 'variable' in locals(): + print(variable) +def some_wrong_but_unused_function(): + # this function has a linting error, but it is not modified by us, and + # who knows, this function might be completely dead code + x = 1 +def sum(a, b): + return a - b +""" + new_content = content.replace(' return a - b', ' return a + b') + temp_file_old_path = tmp_path / 'problematic-file-test.py' + temp_file_old_path.write_text(content) + temp_file_new_path = tmp_path / 'problematic-file-test-new.py' + temp_file_new_path.write_text(new_content) + + linter = DefaultLinter() + result: list[LintResult] = linter.lint_file_diff( + str(temp_file_old_path), + str(temp_file_new_path), + ) + assert len(result) == 0 # no new errors introduced + + +def test_lint_file_diff_catch_new_errors_in_edits(tmp_path): + """ + Make sure we catch new linting errors in our edit chunk, and at the same + time, ignore old linting errors (in this case, the old linting error is + a false positive) + """ + content = """def some_valid_but_weird_function(): + # this function is legitimate, yet static analysis tools like flake8 + # reports 'F821 undefined name' + if 'variable' in locals(): + print(variable) +def sum(a, b): + return a - b +""" + + temp_file_old_path = tmp_path / 'problematic-file-test.py' + temp_file_old_path.write_text(content) + new_content = content.replace(' return a - b', ' return a + variable') + temp_file_new_path = tmp_path / 'problematic-file-test-new.py' + temp_file_new_path.write_text(new_content) + + linter = DefaultLinter() + result: list[LintResult] = linter.lint_file_diff( + str(temp_file_old_path), + str(temp_file_new_path), + ) + print(result) + assert len(result) == 1 + assert ( + result[0].line == 7 + and result[0].column == 16 + and result[0].message == "F821 undefined name 'variable'" + ) + + +def test_lint_file_diff_catch_new_errors_outside_edits(tmp_path): + """ + Make sure we catch new linting errors induced by our edits, even + though the error itself is not in the edit chunk + """ + content = """def valid_func1(): + print(my_sum(1, 2)) +def my_sum(a, b): + return a - b +def valid_func2(): + print(my_sum(0, 0)) +""" + # Add 100 lines of invalid code, which linter shall ignore + # because they are not being edited. For testing purpose, we + # must add these existing linting errors, otherwise the pre-edit + # linting would pass, and thus there won't be any comparison + # between pre-edit and post-edit linting. + for _ in range(100): + content += '\ninvalid_func()' + + temp_file_old_path = tmp_path / 'problematic-file-test.py' + temp_file_old_path.write_text(content) + + new_content = content.replace('def my_sum(a, b):', 'def my_sum2(a, b):') + temp_file_new_path = tmp_path / 'problematic-file-test-new.py' + temp_file_new_path.write_text(new_content) + + linter = DefaultLinter() + result: list[LintResult] = linter.lint_file_diff( + str(temp_file_old_path), + str(temp_file_new_path), + ) + assert len(result) == 2 + assert ( + result[0].line == 2 + and result[0].column == 11 + and result[0].message == "F821 undefined name 'my_sum'" + ) + assert ( + result[1].line == 6 + and result[1].column == 11 + and result[1].message == "F821 undefined name 'my_sum'" + ) From 0e467b14290d7a1113484dbbc221ba586f8d6386 Mon Sep 17 00:00:00 2001 From: mamoodi Date: Thu, 17 Oct 2024 18:23:40 -0400 Subject: [PATCH 13/27] Release 0.10.0 (#4463) --- README.md | 2 +- docs/modules/usage/how-to/cli-mode.md | 2 +- docs/modules/usage/how-to/headless-mode.md | 2 +- docs/modules/usage/installation.mdx | 6 +++--- frontend/package-lock.json | 4 ++-- frontend/package.json | 2 +- pyproject.toml | 4 +--- 7 files changed, 10 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index d8be7bc2bed6..bc1cba09bbea 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ system requirements and more information. ```bash export WORKSPACE_BASE=$(pwd)/workspace -docker pull ghcr.io/all-hands-ai/runtime:0.9-nikolaik +docker pull ghcr.io/all-hands-ai/runtime:0.10-nikolaik docker run -it --pull=always \ -e SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.9-nikolaik \ diff --git a/docs/modules/usage/how-to/cli-mode.md b/docs/modules/usage/how-to/cli-mode.md index 63fd1874efc5..fe04f2d4cc90 100644 --- a/docs/modules/usage/how-to/cli-mode.md +++ b/docs/modules/usage/how-to/cli-mode.md @@ -57,7 +57,7 @@ docker run -it \ -v /var/run/docker.sock:/var/run/docker.sock \ --add-host host.docker.internal:host-gateway \ --name openhands-app-$(date +%Y%m%d%H%M%S) \ - ghcr.io/all-hands-ai/openhands:0.9 \ + ghcr.io/all-hands-ai/openhands:0.10 \ python -m openhands.core.cli ``` diff --git a/docs/modules/usage/how-to/headless-mode.md b/docs/modules/usage/how-to/headless-mode.md index 87d190d2fc6a..65f2a0313076 100644 --- a/docs/modules/usage/how-to/headless-mode.md +++ b/docs/modules/usage/how-to/headless-mode.md @@ -51,6 +51,6 @@ docker run -it \ -v /var/run/docker.sock:/var/run/docker.sock \ --add-host host.docker.internal:host-gateway \ --name openhands-app-$(date +%Y%m%d%H%M%S) \ - ghcr.io/all-hands-ai/openhands:0.9 \ + ghcr.io/all-hands-ai/openhands:0.10 \ python -m openhands.core.main -t "write a bash script that prints hi" ``` diff --git a/docs/modules/usage/installation.mdx b/docs/modules/usage/installation.mdx index 68ae77b91881..0cd6e91aece1 100644 --- a/docs/modules/usage/installation.mdx +++ b/docs/modules/usage/installation.mdx @@ -14,10 +14,10 @@ existing code that you'd like to modify. ```bash export WORKSPACE_BASE=$(pwd)/workspace -docker pull ghcr.io/all-hands-ai/runtime:0.9-nikolaik +docker pull ghcr.io/all-hands-ai/runtime:0.10-nikolaik docker run -it --pull=always \ - -e SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.9-nikolaik \ + -e SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.10-nikolaik \ -e SANDBOX_USER_ID=$(id -u) \ -e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \ -v $WORKSPACE_BASE:/opt/workspace_base \ @@ -25,7 +25,7 @@ docker run -it --pull=always \ -p 3000:3000 \ --add-host host.docker.internal:host-gateway \ --name openhands-app-$(date +%Y%m%d%H%M%S) \ - ghcr.io/all-hands-ai/openhands:0.9 + ghcr.io/all-hands-ai/openhands:0.10 ``` You can also run OpenHands in a scriptable [headless mode](https://docs.all-hands.dev/modules/usage/how-to/headless-mode), as an [interactive CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode), or using the [OpenHands GitHub Action](https://docs.all-hands.dev/modules/usage/how-to/github-action). diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0ad8a5d9fc4c..2284f10218f5 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "openhands-frontend", - "version": "0.9.8", + "version": "0.10.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openhands-frontend", - "version": "0.9.8", + "version": "0.10.0", "dependencies": { "@monaco-editor/react": "^4.6.0", "@nextui-org/react": "^2.4.8", diff --git a/frontend/package.json b/frontend/package.json index 5fe066c65838..0eed7424c7d9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "openhands-frontend", - "version": "0.9.8", + "version": "0.10.0", "private": true, "type": "module", "engines": { diff --git a/pyproject.toml b/pyproject.toml index 46c3e19f3fc2..3523d2b36642 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "openhands-ai" -version = "0.9.8" +version = "0.10.0" description = "OpenHands: Code Less, Make More" authors = ["OpenHands"] license = "MIT" @@ -88,7 +88,6 @@ reportlab = "*" [tool.coverage.run] concurrency = ["gevent"] - [tool.poetry.group.runtime.dependencies] jupyterlab = "*" notebook = "*" @@ -119,7 +118,6 @@ ignore = ["D1"] [tool.ruff.lint.pydocstyle] convention = "google" - [tool.poetry.group.evaluation.dependencies] streamlit = "*" whatthepatch = "*" From d2d55f5ea2a0707231eccf1ae2b8641b800e1195 Mon Sep 17 00:00:00 2001 From: mamoodi Date: Thu, 17 Oct 2024 18:23:57 -0400 Subject: [PATCH 14/27] Update custom sandbox doc (#4332) --- .../usage/how-to/custom-sandbox-guide.md | 83 +++++++++---------- 1 file changed, 39 insertions(+), 44 deletions(-) diff --git a/docs/modules/usage/how-to/custom-sandbox-guide.md b/docs/modules/usage/how-to/custom-sandbox-guide.md index db0a3c4efd6b..076e5a24a093 100644 --- a/docs/modules/usage/how-to/custom-sandbox-guide.md +++ b/docs/modules/usage/how-to/custom-sandbox-guide.md @@ -1,81 +1,76 @@ # Custom Sandbox -The sandbox is where the agent does its work. Instead of running commands directly on your computer -(which could be dangerous), the agent runs them inside of a Docker container. +The sandbox is where the agent performs its tasks. Instead of running commands directly on your computer +(which could be risky), the agent runs them inside a Docker container. The default OpenHands sandbox (`python-nodejs:python3.12-nodejs22` from [nikolaik/python-nodejs](https://hub.docker.com/r/nikolaik/python-nodejs)) comes with some packages installed such -as python and Node.js but your use case may need additional software installed by default. +as python and Node.js but may need other software installed by default. -There are two ways you can do so: +You have two options for customization: -1. Use an existing image from docker hub. -2. Creating your own custom docker image and using it. +1. Use an existing image with the required software. +2. Create your own custom Docker image. -If you want to take the first approach, you can skip the `Create Your Docker Image` section. - -## Setup - -Make sure you are able to run OpenHands using the [Development.md](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md) first. +If you choose the first option, you can skip the `Create Your Docker Image` section. ## Create Your Docker Image -To create a custom docker image, it must be debian/ubuntu based. +To create a custom Docker image, it must be Debian based. -For example, if we want OpenHands to have access to the `node` binary, we would use the following Dockerfile: +For example, if you want OpenHands to have `ruby` installed, create a `Dockerfile` with the following content: ```dockerfile -# Start with latest ubuntu image -FROM ubuntu:latest +FROM debian:latest -# Run needed updates -RUN apt-get update && apt-get install -y +# Install required packages +RUN apt-get update && apt-get install -y ruby +``` -# Install node -RUN apt-get install -y nodejs +Save this file in a folder. Then, build your Docker image (e.g., named custom-image) by navigating to the folder in +the terminal and running:: +```bash +docker build -t custom-image . ``` -Next build your docker image with the name of your choice, for example `custom_image`. +This will produce a new image called `custom-image`, which will be available in Docker. -To do this you can create a directory and put your file inside it with the name `Dockerfile`, and inside the directory run the following command: +> Note that in the configuration described in this document, OpenHands will run as user "openhands" inside the +> sandbox and thus all packages installed via the docker file should be available to all users on the system, not just root. + +## Option 1: Using the Docker Command + +[In the docker command](https://docs.all-hands.dev/modules/usage/installation#start-the-app), replace +`SANDBOX_RUNTIME_CONTAINER_IMAGE` with `SANDBOX_BASE_CONTAINER_IMAGE` and set it to the desired image. +This can be an image you’ve already pulled or one you’ve built: ```bash -docker build -t custom_image . +docker run -it --pull=always \ + -e SANDBOX_BASE_CONTAINER_IMAGE=custom-image \ + ... ``` -This will produce a new image called ```custom_image``` that will be available in Docker Engine. +## Option 2: Using the Development Workflow -> Note that in the configuration described in this document, OpenHands will run as user "openhands" inside the sandbox and thus all packages installed via the docker file should be available to all users on the system, not just root. -> -> Installing with apt-get above installs node for all users. +### Setup -## Specify your sandbox image in config.toml file +First, ensure you can run OpenHands by following the instructions in [Development.md](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md). -OpenHands configuration occurs via the top-level `config.toml` file. +### Specify the Base Sandbox Image -Create a `config.toml` file in the OpenHands directory and enter these contents: +In the `config.toml` file within the OpenHands directory, set the `sandbox_base_container_image` to the image you want to use. +This can be an image you’ve already pulled or one you’ve built: -```toml +```bash [core] -workspace_base="./workspace" -run_as_openhands=true -sandbox_base_container_image="custom_image" +... +sandbox_base_container_image="custom-image" ``` -For `sandbox_base_container_image`, you can specify either: - -1. The name of your custom image that you built in the previous step (e.g., `”custom_image”`) -2. A pre-existing image from Docker Hub (e.g., `”node:20”` if you want a sandbox with Node.js pre-installed) +### Run -## Run Run OpenHands by running ```make run``` in the top level directory. -Navigate to ```localhost:3001``` and check if your desired dependencies are available. - -In the case of the example above, running ```node -v``` in the terminal produces ```v20.15.0```. - -Congratulations! - ## Technical Explanation Please refer to [custom docker image section of the runtime documentation](https://docs.all-hands.dev/modules/usage/architecture/runtime#advanced-how-openhands-builds-and-maintains-od-runtime-images) for more details. From 2e09b4f95e24780af63658569457ac0972547b1c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Oct 2024 05:01:27 +0200 Subject: [PATCH 15/27] chore(deps-dev): bump torch from 2.2.2 to 2.5.0 (#4459) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 188 ++++++++++++++++++++++++++++--------------------- pyproject.toml | 2 +- 2 files changed, 110 insertions(+), 80 deletions(-) diff --git a/poetry.lock b/poetry.lock index 24b8f148a06c..fdb6a083528e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "aenum" @@ -5325,56 +5325,61 @@ files = [ [[package]] name = "nvidia-cublas-cu12" -version = "12.1.3.1" +version = "12.4.5.8" description = "CUBLAS native runtime libraries" optional = false python-versions = ">=3" files = [ - {file = "nvidia_cublas_cu12-12.1.3.1-py3-none-manylinux1_x86_64.whl", hash = "sha256:ee53ccca76a6fc08fb9701aa95b6ceb242cdaab118c3bb152af4e579af792728"}, - {file = "nvidia_cublas_cu12-12.1.3.1-py3-none-win_amd64.whl", hash = "sha256:2b964d60e8cf11b5e1073d179d85fa340c120e99b3067558f3cf98dd69d02906"}, + {file = "nvidia_cublas_cu12-12.4.5.8-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0f8aa1706812e00b9f19dfe0cdb3999b092ccb8ca168c0db5b8ea712456fd9b3"}, + {file = "nvidia_cublas_cu12-12.4.5.8-py3-none-manylinux2014_x86_64.whl", hash = "sha256:2fc8da60df463fdefa81e323eef2e36489e1c94335b5358bcb38360adf75ac9b"}, + {file = "nvidia_cublas_cu12-12.4.5.8-py3-none-win_amd64.whl", hash = "sha256:5a796786da89203a0657eda402bcdcec6180254a8ac22d72213abc42069522dc"}, ] [[package]] name = "nvidia-cuda-cupti-cu12" -version = "12.1.105" +version = "12.4.127" description = "CUDA profiling tools runtime libs." optional = false python-versions = ">=3" files = [ - {file = "nvidia_cuda_cupti_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:e54fde3983165c624cb79254ae9818a456eb6e87a7fd4d56a2352c24ee542d7e"}, - {file = "nvidia_cuda_cupti_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:bea8236d13a0ac7190bd2919c3e8e6ce1e402104276e6f9694479e48bb0eb2a4"}, + {file = "nvidia_cuda_cupti_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:79279b35cf6f91da114182a5ce1864997fd52294a87a16179ce275773799458a"}, + {file = "nvidia_cuda_cupti_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:9dec60f5ac126f7bb551c055072b69d85392b13311fcc1bcda2202d172df30fb"}, + {file = "nvidia_cuda_cupti_cu12-12.4.127-py3-none-win_amd64.whl", hash = "sha256:5688d203301ab051449a2b1cb6690fbe90d2b372f411521c86018b950f3d7922"}, ] [[package]] name = "nvidia-cuda-nvrtc-cu12" -version = "12.1.105" +version = "12.4.127" description = "NVRTC native runtime libraries" optional = false python-versions = ">=3" files = [ - {file = "nvidia_cuda_nvrtc_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:339b385f50c309763ca65456ec75e17bbefcbbf2893f462cb8b90584cd27a1c2"}, - {file = "nvidia_cuda_nvrtc_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:0a98a522d9ff138b96c010a65e145dc1b4850e9ecb75a0172371793752fd46ed"}, + {file = "nvidia_cuda_nvrtc_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0eedf14185e04b76aa05b1fea04133e59f465b6f960c0cbf4e37c3cb6b0ea198"}, + {file = "nvidia_cuda_nvrtc_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a178759ebb095827bd30ef56598ec182b85547f1508941a3d560eb7ea1fbf338"}, + {file = "nvidia_cuda_nvrtc_cu12-12.4.127-py3-none-win_amd64.whl", hash = "sha256:a961b2f1d5f17b14867c619ceb99ef6fcec12e46612711bcec78eb05068a60ec"}, ] [[package]] name = "nvidia-cuda-runtime-cu12" -version = "12.1.105" +version = "12.4.127" description = "CUDA Runtime native Libraries" optional = false python-versions = ">=3" files = [ - {file = "nvidia_cuda_runtime_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:6e258468ddf5796e25f1dc591a31029fa317d97a0a94ed93468fc86301d61e40"}, - {file = "nvidia_cuda_runtime_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:dfb46ef84d73fababab44cf03e3b83f80700d27ca300e537f85f636fac474344"}, + {file = "nvidia_cuda_runtime_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:961fe0e2e716a2a1d967aab7caee97512f71767f852f67432d572e36cb3a11f3"}, + {file = "nvidia_cuda_runtime_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:64403288fa2136ee8e467cdc9c9427e0434110899d07c779f25b5c068934faa5"}, + {file = "nvidia_cuda_runtime_cu12-12.4.127-py3-none-win_amd64.whl", hash = "sha256:09c2e35f48359752dfa822c09918211844a3d93c100a715d79b59591130c5e1e"}, ] [[package]] name = "nvidia-cudnn-cu12" -version = "8.9.2.26" +version = "9.1.0.70" description = "cuDNN runtime libraries" optional = false python-versions = ">=3" files = [ - {file = "nvidia_cudnn_cu12-8.9.2.26-py3-none-manylinux1_x86_64.whl", hash = "sha256:5ccb288774fdfb07a7e7025ffec286971c06d8d7b4fb162525334616d7629ff9"}, + {file = "nvidia_cudnn_cu12-9.1.0.70-py3-none-manylinux2014_x86_64.whl", hash = "sha256:165764f44ef8c61fcdfdfdbe769d687e06374059fbb388b6c89ecb0e28793a6f"}, + {file = "nvidia_cudnn_cu12-9.1.0.70-py3-none-win_amd64.whl", hash = "sha256:6278562929433d68365a07a4a1546c237ba2849852c0d4b2262a486e805b977a"}, ] [package.dependencies] @@ -5382,35 +5387,41 @@ nvidia-cublas-cu12 = "*" [[package]] name = "nvidia-cufft-cu12" -version = "11.0.2.54" +version = "11.2.1.3" description = "CUFFT native runtime libraries" optional = false python-versions = ">=3" files = [ - {file = "nvidia_cufft_cu12-11.0.2.54-py3-none-manylinux1_x86_64.whl", hash = "sha256:794e3948a1aa71fd817c3775866943936774d1c14e7628c74f6f7417224cdf56"}, - {file = "nvidia_cufft_cu12-11.0.2.54-py3-none-win_amd64.whl", hash = "sha256:d9ac353f78ff89951da4af698f80870b1534ed69993f10a4cf1d96f21357e253"}, + {file = "nvidia_cufft_cu12-11.2.1.3-py3-none-manylinux2014_aarch64.whl", hash = "sha256:5dad8008fc7f92f5ddfa2101430917ce2ffacd86824914c82e28990ad7f00399"}, + {file = "nvidia_cufft_cu12-11.2.1.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f083fc24912aa410be21fa16d157fed2055dab1cc4b6934a0e03cba69eb242b9"}, + {file = "nvidia_cufft_cu12-11.2.1.3-py3-none-win_amd64.whl", hash = "sha256:d802f4954291101186078ccbe22fc285a902136f974d369540fd4a5333d1440b"}, ] +[package.dependencies] +nvidia-nvjitlink-cu12 = "*" + [[package]] name = "nvidia-curand-cu12" -version = "10.3.2.106" +version = "10.3.5.147" description = "CURAND native runtime libraries" optional = false python-versions = ">=3" files = [ - {file = "nvidia_curand_cu12-10.3.2.106-py3-none-manylinux1_x86_64.whl", hash = "sha256:9d264c5036dde4e64f1de8c50ae753237c12e0b1348738169cd0f8a536c0e1e0"}, - {file = "nvidia_curand_cu12-10.3.2.106-py3-none-win_amd64.whl", hash = "sha256:75b6b0c574c0037839121317e17fd01f8a69fd2ef8e25853d826fec30bdba74a"}, + {file = "nvidia_curand_cu12-10.3.5.147-py3-none-manylinux2014_aarch64.whl", hash = "sha256:1f173f09e3e3c76ab084aba0de819c49e56614feae5c12f69883f4ae9bb5fad9"}, + {file = "nvidia_curand_cu12-10.3.5.147-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a88f583d4e0bb643c49743469964103aa59f7f708d862c3ddb0fc07f851e3b8b"}, + {file = "nvidia_curand_cu12-10.3.5.147-py3-none-win_amd64.whl", hash = "sha256:f307cc191f96efe9e8f05a87096abc20d08845a841889ef78cb06924437f6771"}, ] [[package]] name = "nvidia-cusolver-cu12" -version = "11.4.5.107" +version = "11.6.1.9" description = "CUDA solver native runtime libraries" optional = false python-versions = ">=3" files = [ - {file = "nvidia_cusolver_cu12-11.4.5.107-py3-none-manylinux1_x86_64.whl", hash = "sha256:8a7ec542f0412294b15072fa7dab71d31334014a69f953004ea7a118206fe0dd"}, - {file = "nvidia_cusolver_cu12-11.4.5.107-py3-none-win_amd64.whl", hash = "sha256:74e0c3a24c78612192a74fcd90dd117f1cf21dea4822e66d89e8ea80e3cd2da5"}, + {file = "nvidia_cusolver_cu12-11.6.1.9-py3-none-manylinux2014_aarch64.whl", hash = "sha256:d338f155f174f90724bbde3758b7ac375a70ce8e706d70b018dd3375545fc84e"}, + {file = "nvidia_cusolver_cu12-11.6.1.9-py3-none-manylinux2014_x86_64.whl", hash = "sha256:19e33fa442bcfd085b3086c4ebf7e8debc07cfe01e11513cc6d332fd918ac260"}, + {file = "nvidia_cusolver_cu12-11.6.1.9-py3-none-win_amd64.whl", hash = "sha256:e77314c9d7b694fcebc84f58989f3aa4fb4cb442f12ca1a9bde50f5e8f6d1b9c"}, ] [package.dependencies] @@ -5420,13 +5431,14 @@ nvidia-nvjitlink-cu12 = "*" [[package]] name = "nvidia-cusparse-cu12" -version = "12.1.0.106" +version = "12.3.1.170" description = "CUSPARSE native runtime libraries" optional = false python-versions = ">=3" files = [ - {file = "nvidia_cusparse_cu12-12.1.0.106-py3-none-manylinux1_x86_64.whl", hash = "sha256:f3b50f42cf363f86ab21f720998517a659a48131e8d538dc02f8768237bd884c"}, - {file = "nvidia_cusparse_cu12-12.1.0.106-py3-none-win_amd64.whl", hash = "sha256:b798237e81b9719373e8fae8d4f091b70a0cf09d9d85c95a557e11df2d8e9a5a"}, + {file = "nvidia_cusparse_cu12-12.3.1.170-py3-none-manylinux2014_aarch64.whl", hash = "sha256:9d32f62896231ebe0480efd8a7f702e143c98cfaa0e8a76df3386c1ba2b54df3"}, + {file = "nvidia_cusparse_cu12-12.3.1.170-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ea4f11a2904e2a8dc4b1833cc1b5181cde564edd0d5cd33e3c168eff2d1863f1"}, + {file = "nvidia_cusparse_cu12-12.3.1.170-py3-none-win_amd64.whl", hash = "sha256:9bc90fb087bc7b4c15641521f31c0371e9a612fc2ba12c338d3ae032e6b6797f"}, ] [package.dependencies] @@ -5434,35 +5446,36 @@ nvidia-nvjitlink-cu12 = "*" [[package]] name = "nvidia-nccl-cu12" -version = "2.19.3" +version = "2.21.5" description = "NVIDIA Collective Communication Library (NCCL) Runtime" optional = false python-versions = ">=3" files = [ - {file = "nvidia_nccl_cu12-2.19.3-py3-none-manylinux1_x86_64.whl", hash = "sha256:a9734707a2c96443331c1e48c717024aa6678a0e2a4cb66b2c364d18cee6b48d"}, + {file = "nvidia_nccl_cu12-2.21.5-py3-none-manylinux2014_x86_64.whl", hash = "sha256:8579076d30a8c24988834445f8d633c697d42397e92ffc3f63fa26766d25e0a0"}, ] [[package]] name = "nvidia-nvjitlink-cu12" -version = "12.6.77" +version = "12.4.127" description = "Nvidia JIT LTO Library" optional = false python-versions = ">=3" files = [ - {file = "nvidia_nvjitlink_cu12-12.6.77-py3-none-manylinux2014_aarch64.whl", hash = "sha256:3bf10d85bb1801e9c894c6e197e44dd137d2a0a9e43f8450e9ad13f2df0dd52d"}, - {file = "nvidia_nvjitlink_cu12-12.6.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:9ae346d16203ae4ea513be416495167a0101d33d2d14935aa9c1829a3fb45142"}, - {file = "nvidia_nvjitlink_cu12-12.6.77-py3-none-win_amd64.whl", hash = "sha256:410718cd44962bed862a31dd0318620f6f9a8b28a6291967bcfcb446a6516771"}, + {file = "nvidia_nvjitlink_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4abe7fef64914ccfa909bc2ba39739670ecc9e820c83ccc7a6ed414122599b83"}, + {file = "nvidia_nvjitlink_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:06b3b9b25bf3f8af351d664978ca26a16d2c5127dbd53c0497e28d1fb9611d57"}, + {file = "nvidia_nvjitlink_cu12-12.4.127-py3-none-win_amd64.whl", hash = "sha256:fd9020c501d27d135f983c6d3e244b197a7ccad769e34df53a42e276b0e25fa1"}, ] [[package]] name = "nvidia-nvtx-cu12" -version = "12.1.105" +version = "12.4.127" description = "NVIDIA Tools Extension" optional = false python-versions = ">=3" files = [ - {file = "nvidia_nvtx_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:dc21cf308ca5691e7c04d962e213f8a4aa9bbfa23d95412f452254c2caeb09e5"}, - {file = "nvidia_nvtx_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:65f4d98982b31b60026e0e6de73fbdfc09d08a96f4656dd3665ca616a11e1e82"}, + {file = "nvidia_nvtx_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7959ad635db13edf4fc65c06a6e9f9e55fc2f92596db928d169c0bb031e88ef3"}, + {file = "nvidia_nvtx_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:781e950d9b9f60d8241ccea575b32f5105a5baf4c2351cab5256a24869f12a1a"}, + {file = "nvidia_nvtx_cu12-12.4.127-py3-none-win_amd64.whl", hash = "sha256:641dccaaa1139f3ffb0d3164b4b84f9d253397e38246a4f2f36728b48566d485"}, ] [[package]] @@ -8405,13 +8418,13 @@ resolved_reference = "c807c112edc3dcb4fdf5ddac63b34706912d5cdb" [[package]] name = "sympy" -version = "1.13.3" +version = "1.13.1" description = "Computer algebra system (CAS) in Python" optional = false python-versions = ">=3.8" files = [ - {file = "sympy-1.13.3-py3-none-any.whl", hash = "sha256:54612cf55a62755ee71824ce692986f23c88ffa77207b30c1368eda4a7060f73"}, - {file = "sympy-1.13.3.tar.gz", hash = "sha256:b27fd2c6530e0ab39e275fc9b683895367e51d5da91baa8d3d64db2565fec4d9"}, + {file = "sympy-1.13.1-py3-none-any.whl", hash = "sha256:db36cdc64bf61b9b24578b6f7bab1ecdd2452cf008f34faa33776680c26d66f8"}, + {file = "sympy-1.13.1.tar.gz", hash = "sha256:9cebf7e04ff162015ce31c9c6c9144daa34a93bd082f54fd8f12deca4f47515f"}, ] [package.dependencies] @@ -8728,36 +8741,28 @@ files = [ [[package]] name = "torch" -version = "2.2.2" +version = "2.5.0" description = "Tensors and Dynamic neural networks in Python with strong GPU acceleration" optional = false python-versions = ">=3.8.0" files = [ - {file = "torch-2.2.2-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:bc889d311a855dd2dfd164daf8cc903a6b7273a747189cebafdd89106e4ad585"}, - {file = "torch-2.2.2-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:15dffa4cc3261fa73d02f0ed25f5fa49ecc9e12bf1ae0a4c1e7a88bbfaad9030"}, - {file = "torch-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:11e8fe261233aeabd67696d6b993eeb0896faa175c6b41b9a6c9f0334bdad1c5"}, - {file = "torch-2.2.2-cp310-none-macosx_10_9_x86_64.whl", hash = "sha256:b2e2200b245bd9f263a0d41b6a2dab69c4aca635a01b30cca78064b0ef5b109e"}, - {file = "torch-2.2.2-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:877b3e6593b5e00b35bbe111b7057464e76a7dd186a287280d941b564b0563c2"}, - {file = "torch-2.2.2-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:ad4c03b786e074f46606f4151c0a1e3740268bcf29fbd2fdf6666d66341c1dcb"}, - {file = "torch-2.2.2-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:32827fa1fbe5da8851686256b4cd94cc7b11be962862c2293811c94eea9457bf"}, - {file = "torch-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:f9ef0a648310435511e76905f9b89612e45ef2c8b023bee294f5e6f7e73a3e7c"}, - {file = "torch-2.2.2-cp311-none-macosx_10_9_x86_64.whl", hash = "sha256:95b9b44f3bcebd8b6cd8d37ec802048c872d9c567ba52c894bba90863a439059"}, - {file = "torch-2.2.2-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:49aa4126ede714c5aeef7ae92969b4b0bbe67f19665106463c39f22e0a1860d1"}, - {file = "torch-2.2.2-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:cf12cdb66c9c940227ad647bc9cf5dba7e8640772ae10dfe7569a0c1e2a28aca"}, - {file = "torch-2.2.2-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:89ddac2a8c1fb6569b90890955de0c34e1724f87431cacff4c1979b5f769203c"}, - {file = "torch-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:451331406b760f4b1ab298ddd536486ab3cfb1312614cfe0532133535be60bea"}, - {file = "torch-2.2.2-cp312-none-macosx_10_9_x86_64.whl", hash = "sha256:eb4d6e9d3663e26cd27dc3ad266b34445a16b54908e74725adb241aa56987533"}, - {file = "torch-2.2.2-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:bf9558da7d2bf7463390b3b2a61a6a3dbb0b45b161ee1dd5ec640bf579d479fc"}, - {file = "torch-2.2.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cd2bf7697c9e95fb5d97cc1d525486d8cf11a084c6af1345c2c2c22a6b0029d0"}, - {file = "torch-2.2.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:b421448d194496e1114d87a8b8d6506bce949544e513742b097e2ab8f7efef32"}, - {file = "torch-2.2.2-cp38-cp38-win_amd64.whl", hash = "sha256:3dbcd563a9b792161640c0cffe17e3270d85e8f4243b1f1ed19cca43d28d235b"}, - {file = "torch-2.2.2-cp38-none-macosx_10_9_x86_64.whl", hash = "sha256:31f4310210e7dda49f1fb52b0ec9e59382cfcb938693f6d5378f25b43d7c1d29"}, - {file = "torch-2.2.2-cp38-none-macosx_11_0_arm64.whl", hash = "sha256:c795feb7e8ce2e0ef63f75f8e1ab52e7fd5e1a4d7d0c31367ade1e3de35c9e95"}, - {file = "torch-2.2.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:a6e5770d68158d07456bfcb5318b173886f579fdfbf747543901ce718ea94782"}, - {file = "torch-2.2.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:67dcd726edff108e2cd6c51ff0e416fd260c869904de95750e80051358680d24"}, - {file = "torch-2.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:539d5ef6c4ce15bd3bd47a7b4a6e7c10d49d4d21c0baaa87c7d2ef8698632dfb"}, - {file = "torch-2.2.2-cp39-none-macosx_10_9_x86_64.whl", hash = "sha256:dff696de90d6f6d1e8200e9892861fd4677306d0ef604cb18f2134186f719f82"}, - {file = "torch-2.2.2-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:3a4dd910663fd7a124c056c878a52c2b0be4a5a424188058fe97109d4436ee42"}, + {file = "torch-2.5.0-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:7f179373a047b947dec448243f4e6598a1c960fa3bb978a9a7eecd529fbc363f"}, + {file = "torch-2.5.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:15fbc95e38d330e5b0ef1593b7bc0a19f30e5bdad76895a5cffa1a6a044235e9"}, + {file = "torch-2.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:f499212f1cffea5d587e5f06144630ed9aa9c399bba12ec8905798d833bd1404"}, + {file = "torch-2.5.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:c54db1fade17287aabbeed685d8e8ab3a56fea9dd8d46e71ced2da367f09a49f"}, + {file = "torch-2.5.0-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:499a68a756d3b30d10f7e0f6214dc3767b130b797265db3b1c02e9094e2a07be"}, + {file = "torch-2.5.0-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:9f3df8138a1126a851440b7d5a4869bfb7c9cc43563d64fd9d96d0465b581024"}, + {file = "torch-2.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:b81da3bdb58c9de29d0e1361e52f12fcf10a89673f17a11a5c6c7da1cb1a8376"}, + {file = "torch-2.5.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:ba135923295d564355326dc409b6b7f5bd6edc80f764cdaef1fb0a1b23ff2f9c"}, + {file = "torch-2.5.0-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:2dd40c885a05ef7fe29356cca81be1435a893096ceb984441d6e2c27aff8c6f4"}, + {file = "torch-2.5.0-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:bc52d603d87fe1da24439c0d5fdbbb14e0ae4874451d53f0120ffb1f6c192727"}, + {file = "torch-2.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea718746469246cc63b3353afd75698a288344adb55e29b7f814a5d3c0a7c78d"}, + {file = "torch-2.5.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:6de1fd253e27e7f01f05cd7c37929ae521ca23ca4620cfc7c485299941679112"}, + {file = "torch-2.5.0-cp313-cp313-manylinux1_x86_64.whl", hash = "sha256:83dcf518685db20912b71fc49cbddcc8849438cdb0e9dcc919b02a849e2cd9e8"}, + {file = "torch-2.5.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:65e0a60894435608334d68c8811e55fd8f73e5bf8ee6f9ccedb0064486a7b418"}, + {file = "torch-2.5.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:38c21ff1bd39f076d72ab06e3c88c2ea6874f2e6f235c9450816b6c8e7627094"}, + {file = "torch-2.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:ce4baeba9804da5a346e210b3b70826f5811330c343e4fe1582200359ee77fe5"}, + {file = "torch-2.5.0-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:03e53f577a96e4d41aca472da8faa40e55df89d2273664af390ce1f570e885bd"}, ] [package.dependencies] @@ -8765,23 +8770,26 @@ filelock = "*" fsspec = "*" jinja2 = "*" networkx = "*" -nvidia-cublas-cu12 = {version = "12.1.3.1", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cuda-cupti-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cuda-nvrtc-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cuda-runtime-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cudnn-cu12 = {version = "8.9.2.26", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cufft-cu12 = {version = "11.0.2.54", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-curand-cu12 = {version = "10.3.2.106", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cusolver-cu12 = {version = "11.4.5.107", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cusparse-cu12 = {version = "12.1.0.106", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-nccl-cu12 = {version = "2.19.3", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-nvtx-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -sympy = "*" +nvidia-cublas-cu12 = {version = "12.4.5.8", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cuda-cupti-cu12 = {version = "12.4.127", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cuda-nvrtc-cu12 = {version = "12.4.127", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cuda-runtime-cu12 = {version = "12.4.127", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cudnn-cu12 = {version = "9.1.0.70", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cufft-cu12 = {version = "11.2.1.3", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-curand-cu12 = {version = "10.3.5.147", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cusolver-cu12 = {version = "11.6.1.9", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cusparse-cu12 = {version = "12.3.1.170", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-nccl-cu12 = {version = "2.21.5", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-nvjitlink-cu12 = {version = "12.4.127", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-nvtx-cu12 = {version = "12.4.127", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +setuptools = {version = "*", markers = "python_version >= \"3.12\""} +sympy = {version = "1.13.1", markers = "python_version >= \"3.9\""} +triton = {version = "3.1.0", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.13\""} typing-extensions = ">=4.8.0" [package.extras] opt-einsum = ["opt-einsum (>=3.3)"] -optree = ["optree (>=0.9.1)"] +optree = ["optree (>=0.12.0)"] [[package]] name = "tornado" @@ -9023,6 +9031,28 @@ files = [ [package.dependencies] tree-sitter = "*" +[[package]] +name = "triton" +version = "3.1.0" +description = "A language and compiler for custom Deep Learning operations" +optional = false +python-versions = "*" +files = [ + {file = "triton-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b0dd10a925263abbe9fa37dcde67a5e9b2383fc269fdf59f5657cac38c5d1d8"}, + {file = "triton-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f34f6e7885d1bf0eaaf7ba875a5f0ce6f3c13ba98f9503651c1e6dc6757ed5c"}, + {file = "triton-3.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8182f42fd8080a7d39d666814fa36c5e30cc00ea7eeeb1a2983dbb4c99a0fdc"}, + {file = "triton-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dadaca7fc24de34e180271b5cf864c16755702e9f63a16f62df714a8099126a"}, + {file = "triton-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aafa9a20cd0d9fee523cd4504aa7131807a864cd77dcf6efe7e981f18b8c6c11"}, +] + +[package.dependencies] +filelock = "*" + +[package.extras] +build = ["cmake (>=3.20)", "lit"] +tests = ["autopep8", "flake8", "isort", "llnl-hatchet", "numpy", "pytest", "scipy (>=1.7.1)"] +tutorials = ["matplotlib", "pandas", "tabulate"] + [[package]] name = "typer" version = "0.12.5" @@ -10001,4 +10031,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "431cd8f4b8a41e6c52c79cd18a55654388c8d15dcdabfa1487dda17cf6caa3e4" +content-hash = "7fc51225767e3a98147f7b0dacdce4486a1afd83dc3273f06fd9f6cdc35d1860" diff --git a/pyproject.toml b/pyproject.toml index 3523d2b36642..de7640f30811 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ llama-index = "*" llama-index-vector-stores-chroma = "*" chromadb = "*" llama-index-embeddings-huggingface = "*" -torch = "2.2.2" +torch = "2.5.0" llama-index-embeddings-azure-openai = "*" llama-index-embeddings-ollama = "*" From 1ea3087eec406e2a6b101fbed603de55631a7718 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Oct 2024 05:02:20 +0200 Subject: [PATCH 16/27] chore(deps): bump modal from 0.64.182 to 0.64.192 (#4460) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index fdb6a083528e..d3a199ad9203 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4799,12 +4799,12 @@ type = ["mypy (==1.11.2)"] [[package]] name = "modal" -version = "0.64.182" +version = "0.64.192" description = "Python client library for Modal" optional = false python-versions = ">=3.8" files = [ - {file = "modal-0.64.182-py3-none-any.whl", hash = "sha256:d3213550a0724b13b1dacf8b468d26c78f51d850fd2a76529f180921905bcad3"}, + {file = "modal-0.64.192-py3-none-any.whl", hash = "sha256:4d20e201e4040b13841c0e34ea4ad0e58719568adc084eee6b1da71b7c618273"}, ] [package.dependencies] From fd6facbf0301782955fe37d4ea09e3443bf2ad2e Mon Sep 17 00:00:00 2001 From: Robert Brennan Date: Thu, 17 Oct 2024 23:08:54 -0400 Subject: [PATCH 17/27] update contributing docs (#4438) Co-authored-by: Engel Nyst --- CONTRIBUTING.md | 107 +++++++++++++++++++----------------------------- 1 file changed, 42 insertions(+), 65 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c3c5a0a65acb..effb7662a123 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,14 +2,6 @@ Thanks for your interest in contributing to OpenHands! We welcome and appreciate contributions. -## How Can I Contribute? - -There are many ways that you can contribute: - -1. **Download and use** OpenHands, and send [issues](https://github.com/All-Hands-AI/OpenHands/issues) when you encounter something that isn't working or a feature that you'd like to see. -2. **Send feedback** after each session by [clicking the thumbs-up thumbs-down buttons](https://docs.all-hands.dev/modules/usage/feedback), so we can see where things are working and failing, and also build an open dataset for training code agents. -3. **Improve the Codebase** by sending PRs (see details below). In particular, we have some [good first issues](https://github.com/All-Hands-AI/OpenHands/labels/good%20first%20issue) that may be ones to start on. - ## Understanding OpenHands's CodeBase To understand the codebase, please refer to the README in each module: @@ -19,79 +11,61 @@ To understand the codebase, please refer to the README in each module: - [agenthub](./openhands/agenthub/README.md) - [server](./openhands/server/README.md) +## Setting up your development environment -When you write code, it is also good to write tests. Please navigate to the `tests` folder to see existing test suites. -At the moment, we have two kinds of tests: `unit` and `integration`. Please refer to the README for each test suite. These tests also run on GitHub's continuous integration to ensure quality of the project. - -## Sending Pull Requests to OpenHands - -### 1. Fork the Official Repository -Fork the [OpenHands repository](https://github.com/All-Hands-AI/OpenHands) into your own account. -Clone your own forked repository into your local environment: - -```shell -git clone git@github.com:/OpenHands.git -``` - -### 2. Configure Git - -Set the official repository as your [upstream](https://www.atlassian.com/git/tutorials/git-forks-and-upstreams) to synchronize with the latest update in the official repository. -Add the original repository as upstream: - -```shell -cd OpenHands -git remote add upstream git@github.com:All-Hands-AI/OpenHands.git -``` +We have a separate doc [Development.md](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md) that tells you how to set up a development workflow. -Verify that the remote is set: +## How can I contribute? -```shell -git remote -v -``` +There are many ways that you can contribute: -You should see both `origin` and `upstream` in the output. +1. **Download and use** OpenHands, and send [issues](https://github.com/All-Hands-AI/OpenHands/issues) when you encounter something that isn't working or a feature that you'd like to see. +2. **Send feedback** after each session by [clicking the thumbs-up thumbs-down buttons](https://docs.all-hands.dev/modules/usage/feedback), so we can see where things are working and failing, and also build an open dataset for training code agents. +3. **Improve the Codebase** by sending PRs (see details below). In particular, we have some [good first issues](https://github.com/All-Hands-AI/OpenHands/labels/good%20first%20issue) that may be ones to start on. -### 3. Synchronize with Official Repository -Synchronize latest commit with official repository before coding: +## What can I build? +Here are a few ways you can help improve the codebase. -```shell -git fetch upstream -git checkout main -git merge upstream/main -git push origin main -``` +#### UI/UX +We're always looking to improve the look and feel of the application. If you've got a small fix +for something that's bugging you, feel free to open up a PR that changes the `./frontend` directory. -### 4. Set up the Development Environment +If you're looking to make a bigger change, add a new UI element, or significantly alter the style +of the application, please open an issue first, or better, join the #frontend channel in our Slack +to gather consensus from our design team first. -We have a separate doc [Development.md](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md) that tells you how to set up a development workflow. +#### Improving the agent +Our main agent is the CodeAct agent. You can [see its prompts here](https://github.com/All-Hands-AI/OpenHands/tree/main/openhands/agenthub/codeact_agent) -### 5. Write Code and Commit It +Changes to these prompts, and to the underlying behavior in Python, can have a huge impact on user experience. +You can try modifying the prompts to see how they change the behavior of the agent as you use the app +locally, but we will need to do an end-to-end evaluation of any changes here to ensure that the agent +is getting better over time. -Once you have done this, you can write code, test it, and commit it to a branch (replace `my_branch` with an appropriate name): +We use the [SWE-bench](https://www.swebench.com/) benchmark to test our agent. You can join the #evaluation +channel in Slack to learn more. -```shell -git checkout -b my_branch -git add . -git commit -git push origin my_branch -``` +#### Adding a new agent +You may want to experiment with building new types of agents. You can add an agent to `openhands/agenthub` +to help expand the capabilities of OpenHands. -### 6. Open a Pull Request +#### Adding a new runtime +The agent needs a place to run code and commands. When you run OpenHands on your laptop, it uses a Docker container +to do this by default. But there are other ways of creating a sandbox for the agent. -* On GitHub, go to the page of your forked repository, and create a Pull Request: - - Click on `Branches` - - Click on the `...` beside your branch and click on `New pull request` - - Set `base repository` to `All-Hands-AI/OpenHands` - - Set `base` to `main` - - Click `Create pull request` +If you work for a company that provides a cloud-based runtime, you could help us add support for that runtime +by implementing the [interface specified here](https://github.com/All-Hands-AI/OpenHands/blob/main/openhands/runtime/runtime.py). -The PR should appear in [OpenHands PRs](https://github.com/All-Hands-AI/OpenHands/pulls). +#### Testing +When you write code, it is also good to write tests. Please navigate to the `tests` folder to see existing test suites. +At the moment, we have two kinds of tests: `unit` and `integration`. Please refer to the README for each test suite. These tests also run on GitHub's continuous integration to ensure quality of the project. -Then the OpenHands team will review your code. +## Sending Pull Requests to OpenHands -## PR Rules +You'll need to fork our repository to send us a Pull Request. You can learn more +about how to fork a GitHub repo and open a PR with your changes in [this article](https://medium.com/swlh/forks-and-pull-requests-how-to-contribute-to-github-repos-8843fac34ce8) -### 1. Pull Request title +### Pull Request title As described [here](https://github.com/commitizen/conventional-commit-types/blob/master/index.json), a valid PR title should begin with one of the following prefixes: - `feat`: A new feature @@ -112,6 +86,9 @@ For example, a PR title could be: You may also check out previous PRs in the [PR list](https://github.com/All-Hands-AI/OpenHands/pulls). -### 2. Pull Request description +### Pull Request description - If your PR is small (such as a typo fix), you can go brief. - If it contains a lot of changes, it's better to write more details. + +If your changes are user-facing (e.g. a new feature in the UI, a change in behavior, or a bugfix) +please include a short message that we can add to our changelog. From feee509de7ffe30859611ace9882c72d1a6a3a6b Mon Sep 17 00:00:00 2001 From: mamoodi Date: Fri, 18 Oct 2024 09:28:53 -0400 Subject: [PATCH 18/27] Update leftover versions (#4468) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bc1cba09bbea..bd3ae10332f7 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ export WORKSPACE_BASE=$(pwd)/workspace docker pull ghcr.io/all-hands-ai/runtime:0.10-nikolaik docker run -it --pull=always \ - -e SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.9-nikolaik \ + -e SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.10-nikolaik \ -e SANDBOX_USER_ID=$(id -u) \ -e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \ -v $WORKSPACE_BASE:/opt/workspace_base \ @@ -53,7 +53,7 @@ docker run -it --pull=always \ -p 3000:3000 \ --add-host host.docker.internal:host-gateway \ --name openhands-app-$(date +%Y%m%d%H%M%S) \ - ghcr.io/all-hands-ai/openhands:0.9 + ghcr.io/all-hands-ai/openhands:0.10 ``` You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)! From e6a5e39047ce573e2e26c1cb974377057d901ed8 Mon Sep 17 00:00:00 2001 From: mamoodi Date: Fri, 18 Oct 2024 10:19:56 -0400 Subject: [PATCH 19/27] Update docs associated with new UI (#4469) --- docs/modules/usage/llms/azure-llms.md | 4 ++-- docs/modules/usage/llms/google-llms.md | 2 +- docs/modules/usage/llms/llms.md | 2 +- docs/static/img/settings-advanced.png | Bin 25061 -> 28576 bytes docs/static/img/settings-screenshot.png | Bin 26790 -> 33797 bytes 5 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/modules/usage/llms/azure-llms.md b/docs/modules/usage/llms/azure-llms.md index f4e41f875a38..11653fd42c38 100644 --- a/docs/modules/usage/llms/azure-llms.md +++ b/docs/modules/usage/llms/azure-llms.md @@ -5,7 +5,7 @@ OpenHands uses LiteLLM to make calls to Azure's chat models. You can find their ## Azure OpenAI Configuration When running OpenHands, you'll need to set the following environment variable using `-e` in the -[docker run command](/modules/usage/installation): +[docker run command](/modules/usage/installation#start-the-app): ``` LLM_API_VERSION="" # e.g. "2023-05-15" @@ -37,7 +37,7 @@ OpenHands uses llama-index for embeddings. You can find their documentation on A ### Azure OpenAI Configuration When running OpenHands, set the following environment variables using `-e` in the -[docker run command](/modules/usage/installation): +[docker run command](/modules/usage/installation#start-the-app): ``` LLM_EMBEDDING_MODEL="azureopenai" diff --git a/docs/modules/usage/llms/google-llms.md b/docs/modules/usage/llms/google-llms.md index a7a5838f73ba..701208b75976 100644 --- a/docs/modules/usage/llms/google-llms.md +++ b/docs/modules/usage/llms/google-llms.md @@ -16,7 +16,7 @@ If the model is not in the list, toggle `Advanced Options`, and enter it in `Cus ## VertexAI - Google Cloud Platform Configs To use Vertex AI through Google Cloud Platform when running OpenHands, you'll need to set the following environment -variables using `-e` in the [docker run command](/modules/usage/installation): +variables using `-e` in the [docker run command](/modules/usage/installation#start-the-app): ``` GOOGLE_APPLICATION_CREDENTIALS="" diff --git a/docs/modules/usage/llms/llms.md b/docs/modules/usage/llms/llms.md index 328d5149f068..75d19296270b 100644 --- a/docs/modules/usage/llms/llms.md +++ b/docs/modules/usage/llms/llms.md @@ -48,7 +48,7 @@ The following can be set in the OpenHands UI through the Settings: - `Base URL` (through `Advanced Settings`) There are some settings that may be necessary for some LLMs/providers that cannot be set through the UI. Instead, these -can be set through environment variables passed to the [docker run command](/modules/usage/installation) +can be set through environment variables passed to the [docker run command](/modules/usage/installation#start-the-app) using `-e`: - `LLM_API_VERSION` diff --git a/docs/static/img/settings-advanced.png b/docs/static/img/settings-advanced.png index 0e80b2baea966066a19d45296fa2a17f988bfe43..43a9cf05ab83383c0dfafeba871bd1cc89cbf7ff 100644 GIT binary patch literal 28576 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 literal 25061 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 z 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 From 02abf60433be61cef3b2c6301e63e637ea0e02ab Mon Sep 17 00:00:00 2001 From: mamoodi Date: Fri, 18 Oct 2024 10:38:40 -0400 Subject: [PATCH 20/27] Run flaky mac tests nightly (#4470) --- .github/workflows/py-unit-tests-mac.yml | 96 +++++++++++++++++++++++++ .github/workflows/py-unit-tests.yml | 88 ----------------------- 2 files changed, 96 insertions(+), 88 deletions(-) create mode 100644 .github/workflows/py-unit-tests-mac.yml diff --git a/.github/workflows/py-unit-tests-mac.yml b/.github/workflows/py-unit-tests-mac.yml new file mode 100644 index 000000000000..d750eb172114 --- /dev/null +++ b/.github/workflows/py-unit-tests-mac.yml @@ -0,0 +1,96 @@ +# Workflow that runs python unit tests on mac +name: Run Python Unit Tests Mac + +# This job is flaky so only run it nightly +on: + schedule: + - cron: '0 0 * * *' + +jobs: + # Run python unit tests on macOS + test-on-macos: + name: Python Unit Tests on macOS + runs-on: macos-14 + env: + INSTALL_DOCKER: '1' # Set to '0' to skip Docker installation + strategy: + matrix: + python-version: ['3.12'] + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Cache Poetry dependencies + uses: actions/cache@v4 + with: + path: | + ~/.cache/pypoetry + ~/.virtualenvs + key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }} + restore-keys: | + ${{ runner.os }}-poetry- + - name: Install poetry via pipx + run: pipx install poetry + - name: Install Python dependencies using Poetry + run: poetry install --without evaluation,llama-index + - name: Install & Start Docker + if: env.INSTALL_DOCKER == '1' + run: | + INSTANCE_NAME="colima-${GITHUB_RUN_ID}" + + # Uninstall colima to upgrade to the latest version + if brew list colima &>/dev/null; then + brew uninstall colima + # unlinking colima dependency: go + brew uninstall go@1.21 + fi + rm -rf ~/.colima ~/.lima + brew install --HEAD colima + brew install docker + + start_colima() { + # Find a free port in the range 10000-20000 + RANDOM_PORT=$((RANDOM % 10001 + 10000)) + + # Original line: + if ! colima start --network-address --arch x86_64 --cpu=1 --memory=1 --verbose --ssh-port $RANDOM_PORT; then + echo "Failed to start Colima." + return 1 + fi + return 0 + } + + # Attempt to start Colima for 5 total attempts: + ATTEMPT_LIMIT=5 + for ((i=1; i<=ATTEMPT_LIMIT; i++)); do + + if start_colima; then + echo "Colima started successfully." + break + else + colima stop -f + sleep 10 + colima delete -f + if [ $i -eq $ATTEMPT_LIMIT ]; then + exit 1 + fi + sleep 10 + fi + done + + # For testcontainers to find the Colima socket + # https://github.com/abiosoft/colima/blob/main/docs/FAQ.md#cannot-connect-to-the-docker-daemon-at-unixvarrundockersock-is-the-docker-daemon-running + sudo ln -sf $HOME/.colima/default/docker.sock /var/run/docker.sock + - name: Build Environment + run: make build + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v3 + - name: Run Tests + run: poetry run pytest --forked --cov=openhands --cov-report=xml ./tests/unit --ignore=tests/unit/test_memory.py + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/py-unit-tests.yml b/.github/workflows/py-unit-tests.yml index 30d901b0b2f6..6e624904a82f 100644 --- a/.github/workflows/py-unit-tests.yml +++ b/.github/workflows/py-unit-tests.yml @@ -16,94 +16,6 @@ concurrency: cancel-in-progress: true jobs: - # Run python unit tests on macOS - test-on-macos: - name: Python Unit Tests on macOS - runs-on: macos-14 - env: - INSTALL_DOCKER: '1' # Set to '0' to skip Docker installation - strategy: - matrix: - python-version: ['3.12'] - steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Cache Poetry dependencies - uses: actions/cache@v4 - with: - path: | - ~/.cache/pypoetry - ~/.virtualenvs - key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }} - restore-keys: | - ${{ runner.os }}-poetry- - - name: Install poetry via pipx - run: pipx install poetry - - name: Install Python dependencies using Poetry - run: poetry install --without evaluation,llama-index - - name: Install & Start Docker - if: env.INSTALL_DOCKER == '1' - run: | - INSTANCE_NAME="colima-${GITHUB_RUN_ID}" - - # Uninstall colima to upgrade to the latest version - if brew list colima &>/dev/null; then - brew uninstall colima - # unlinking colima dependency: go - brew uninstall go@1.21 - fi - rm -rf ~/.colima ~/.lima - brew install --HEAD colima - brew install docker - - start_colima() { - # Find a free port in the range 10000-20000 - RANDOM_PORT=$((RANDOM % 10001 + 10000)) - - # Original line: - if ! colima start --network-address --arch x86_64 --cpu=1 --memory=1 --verbose --ssh-port $RANDOM_PORT; then - echo "Failed to start Colima." - return 1 - fi - return 0 - } - - # Attempt to start Colima for 5 total attempts: - ATTEMPT_LIMIT=5 - for ((i=1; i<=ATTEMPT_LIMIT; i++)); do - - if start_colima; then - echo "Colima started successfully." - break - else - colima stop -f - sleep 10 - colima delete -f - if [ $i -eq $ATTEMPT_LIMIT ]; then - exit 1 - fi - sleep 10 - fi - done - - # For testcontainers to find the Colima socket - # https://github.com/abiosoft/colima/blob/main/docs/FAQ.md#cannot-connect-to-the-docker-daemon-at-unixvarrundockersock-is-the-docker-daemon-running - sudo ln -sf $HOME/.colima/default/docker.sock /var/run/docker.sock - - name: Build Environment - run: make build - - name: Set up Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v3 - - name: Run Tests - run: poetry run pytest --forked --cov=openhands --cov-report=xml ./tests/unit --ignore=tests/unit/test_memory.py - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - # Run python unit tests on Linux test-on-linux: name: Python Unit Tests on Linux From 56fe9052410eabb0fa42e6fb1e52cbbfdbe573bb Mon Sep 17 00:00:00 2001 From: Robert Brennan Date: Fri, 18 Oct 2024 11:21:15 -0400 Subject: [PATCH 21/27] reduce dependabot frequency (#4305) --- .github/dependabot.yml | 49 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 09ef6a92abf0..50e60d7ba6a4 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,21 +1,35 @@ -# To get started with Dependabot version updates, you'll need to specify which -# package ecosystems to update and where the package manifests are located. -# Please see the documentation for all configuration options: -# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file - version: 2 updates: - package-ecosystem: "pip" directory: "/" schedule: interval: "daily" - open-pull-requests-limit: 20 + open-pull-requests-limit: 1 + groups: + # put packages in their own group if they have a history of breaking the build or needing to be reverted + pre-commit: + patterns: + - "pre-commit" + llama: + patterns: + - "llama*" + chromadb: + patterns: + - "chromadb" + security-all: + applies-to: "security-updates" + patterns: + - "*" + version-all: + applies-to: "version-updates" + patterns: + - "*" - package-ecosystem: "npm" directory: "/frontend" schedule: interval: "daily" - open-pull-requests-limit: 20 + open-pull-requests-limit: 1 groups: docusaurus: patterns: @@ -23,12 +37,21 @@ updates: eslint: patterns: - "*eslint*" + security-all: + applies-to: "security-updates" + patterns: + - "*" + version-all: + applies-to: "version-updates" + patterns: + - "*" - package-ecosystem: "npm" directory: "/docs" schedule: - interval: "daily" - open-pull-requests-limit: 20 + interval: "weekly" + day: "wednesday" + open-pull-requests-limit: 1 groups: docusaurus: patterns: @@ -36,3 +59,11 @@ updates: eslint: patterns: - "*eslint*" + security-all: + applies-to: "security-updates" + patterns: + - "*" + version-all: + applies-to: "version-updates" + patterns: + - "*" From cf793582a702148dec41309c14b12651c32c3b1b Mon Sep 17 00:00:00 2001 From: "sp.wack" <83104063+amanape@users.noreply.github.com> Date: Fri, 18 Oct 2024 19:32:46 +0400 Subject: [PATCH 22/27] [ALL-543] feat(frontend): Setup auth route, replace loading spinner, add new route (#4448) --- frontend/src/api/open-hands.ts | 17 +++++++++ frontend/src/routes/_oh._index/route.tsx | 8 ----- frontend/src/routes/_oh.app.tsx | 12 ++++++- frontend/src/routes/_oh.tsx | 39 +++++++++------------ frontend/src/routes/_oh.waitlist.tsx | 39 +++++++++++++++++++++ frontend/src/utils/user-is-authenticated.ts | 16 +++++++++ openhands/server/listen.py | 29 +++++++++++++++ 7 files changed, 129 insertions(+), 31 deletions(-) create mode 100644 frontend/src/routes/_oh.waitlist.tsx create mode 100644 frontend/src/utils/user-is-authenticated.ts diff --git a/frontend/src/api/open-hands.ts b/frontend/src/api/open-hands.ts index be41cf7967d2..0dd4199840e6 100644 --- a/frontend/src/api/open-hands.ts +++ b/frontend/src/api/open-hands.ts @@ -210,6 +210,23 @@ class OpenHands { return response.json(); } + + /** + * Check if the user is authenticated + * @param login The user's GitHub login handle + * @returns Whether the user is authenticated + */ + static async isAuthenticated(login: string): Promise { + const response = await fetch(`${OpenHands.BASE_URL}/authenticate`, { + method: "POST", + body: JSON.stringify({ login }), + headers: { + "Content-Type": "application/json", + }, + }); + + return response.status === 200; + } } export default OpenHands; diff --git a/frontend/src/routes/_oh._index/route.tsx b/frontend/src/routes/_oh._index/route.tsx index 21b6bc197918..ed69ae98f11e 100644 --- a/frontend/src/routes/_oh._index/route.tsx +++ b/frontend/src/routes/_oh._index/route.tsx @@ -4,7 +4,6 @@ import { json, redirect, useLoaderData, - useNavigation, useRouteLoaderData, } from "@remix-run/react"; import React from "react"; @@ -21,7 +20,6 @@ import ModalButton from "#/components/buttons/ModalButton"; import GitHubLogo from "#/assets/branding/github-logo.svg?react"; import { ConnectToGitHubModal } from "#/components/modals/connect-to-github-modal"; import { ModalBackdrop } from "#/components/modals/modal-backdrop"; -import { LoadingSpinner } from "#/components/modals/LoadingProject"; import store, { RootState } from "#/store"; import { removeFile, setInitialQuery } from "#/state/initial-query-slice"; import { clientLoader as rootClientLoader } from "#/routes/_oh"; @@ -102,7 +100,6 @@ export const clientAction = async ({ request }: ClientActionFunctionArgs) => { function Home() { const rootData = useRouteLoaderData("routes/_oh"); - const navigation = useNavigation(); const { repositories, githubAuthUrl } = useLoaderData(); const [connectToGitHubModalOpen, setConnectToGitHubModalOpen] = React.useState(false); @@ -124,11 +121,6 @@ function Home() { return (
- {navigation.state === "loading" && ( -
- -
- )}
diff --git a/frontend/src/routes/_oh.app.tsx b/frontend/src/routes/_oh.app.tsx index d9192bffb1df..97b76ce56ff3 100644 --- a/frontend/src/routes/_oh.app.tsx +++ b/frontend/src/routes/_oh.app.tsx @@ -7,6 +7,7 @@ import { json, ClientActionFunctionArgs, useRouteLoaderData, + redirect, } from "@remix-run/react"; import { useDispatch, useSelector } from "react-redux"; import WebSocket from "ws"; @@ -42,6 +43,8 @@ import { base64ToBlob } from "#/utils/base64-to-blob"; import { clientLoader as rootClientLoader } from "#/routes/_oh"; import { clearJupyter } from "#/state/jupyterSlice"; import { FilesProvider } from "#/context/files"; +import { clearSession } from "#/utils/clear-session"; +import { userIsAuthenticated } from "#/utils/user-is-authenticated"; const isAgentStateChange = ( data: object, @@ -51,6 +54,14 @@ const isAgentStateChange = ( "agent_state" in data.extras; export const clientLoader = async () => { + const ghToken = localStorage.getItem("ghToken"); + + const isAuthed = await userIsAuthenticated(ghToken); + if (!isAuthed) { + clearSession(); + return redirect("/waitlist"); + } + const q = store.getState().initalQuery.initialQuery; const repo = store.getState().initalQuery.selectedRepository || @@ -59,7 +70,6 @@ export const clientLoader = async () => { const settings = getSettings(); const token = localStorage.getItem("token"); - const ghToken = localStorage.getItem("ghToken"); if (token && importedProject) { const blob = base64ToBlob(importedProject); diff --git a/frontend/src/routes/_oh.tsx b/frontend/src/routes/_oh.tsx index c72bafa31580..13913380ea33 100644 --- a/frontend/src/routes/_oh.tsx +++ b/frontend/src/routes/_oh.tsx @@ -15,7 +15,7 @@ import CogTooth from "#/assets/cog-tooth"; import { SettingsForm } from "#/components/form/settings-form"; import AccountSettingsModal from "#/components/modals/AccountSettingsModal"; import { DangerModal } from "#/components/modals/confirmation-modals/danger-modal"; -import LoadingProjectModal from "#/components/modals/LoadingProject"; +import { LoadingSpinner } from "#/components/modals/LoadingProject"; import { ModalBackdrop } from "#/components/modals/modal-backdrop"; import { UserAvatar } from "#/components/user-avatar"; import { useSocket } from "#/context/socket"; @@ -173,16 +173,22 @@ export default function MainApp() { return (
-