diff --git a/config.template.toml b/config.template.toml index 744dfc7953a4..331663c619f4 100644 --- a/config.template.toml +++ b/config.template.toml @@ -17,6 +17,12 @@ #modal_api_token_id = "" #modal_api_token_secret = "" +# API key for Daytona +#daytona_api_key = "" + +# Daytona Target +#daytona_target = "" + # Base path for the workspace workspace_base = "./workspace" diff --git a/openhands/core/config/app_config.py b/openhands/core/config/app_config.py index 5965f06480c8..7abf0936f86b 100644 --- a/openhands/core/config/app_config.py +++ b/openhands/core/config/app_config.py @@ -75,6 +75,9 @@ class AppConfig(BaseModel): file_uploads_restrict_file_types: bool = Field(default=False) file_uploads_allowed_extensions: list[str] = Field(default_factory=lambda: ['.*']) runloop_api_key: SecretStr | None = Field(default=None) + daytona_api_key: SecretStr | None = Field(default=None) + daytona_api_url: str = Field(default='https://app.daytona.io/api') + daytona_target: str = Field(default='us') cli_multiline_input: bool = Field(default=False) conversation_max_age_seconds: int = Field(default=864000) # 10 days in seconds diff --git a/openhands/core/logger.py b/openhands/core/logger.py index 2f830c655a00..82ce45a1fec3 100644 --- a/openhands/core/logger.py +++ b/openhands/core/logger.py @@ -243,6 +243,7 @@ def filter(self, record): 'modal_api_token_secret', 'llm_api_key', 'sandbox_env_github_token', + 'daytona_api_key', ] # add env var names diff --git a/openhands/runtime/__init__.py b/openhands/runtime/__init__.py index 5ddf881fcfa3..0590d62b7c33 100644 --- a/openhands/runtime/__init__.py +++ b/openhands/runtime/__init__.py @@ -1,4 +1,5 @@ from openhands.core.logger import openhands_logger as logger +from openhands.runtime.impl.daytona.daytona_runtime import DaytonaRuntime from openhands.runtime.impl.docker.docker_runtime import ( DockerRuntime, ) @@ -24,6 +25,8 @@ def get_runtime_cls(name: str): return RunloopRuntime elif name == 'local': return LocalRuntime + elif name == 'daytona': + return DaytonaRuntime else: raise ValueError(f'Runtime {name} not supported') diff --git a/openhands/runtime/impl/daytona/README.md b/openhands/runtime/impl/daytona/README.md new file mode 100644 index 000000000000..dfaa7e3dc02f --- /dev/null +++ b/openhands/runtime/impl/daytona/README.md @@ -0,0 +1,24 @@ +# Daytona Runtime + +[Daytona](https://www.daytona.io/) is a platform that provides a secure and elastic infrastructure for running AI-generated code. It provides all the necessary features for an AI Agent to interact with a codebase. It provides a Daytona SDK with official Python and TypeScript interfaces for interacting with Daytona, enabling you to programmatically manage development environments and execute code. + +## Getting started + +1. Sign in at https://app.daytona.io/ + +1. Generate and copy your API key + +1. Set the following environment variables before running the OpenHands app on your local machine or via a `docker run` command: + +```bash + RUNTIME="daytona" + DAYTONA_API_KEY="" +``` +Optionally, if you don't want your sandboxes to default to the US region, set: + +```bash + DAYTONA_TARGET="eu" +``` + +## Documentation +Read more by visiting our [documentation](https://www.daytona.io/docs/) page. diff --git a/openhands/runtime/impl/daytona/daytona_runtime.py b/openhands/runtime/impl/daytona/daytona_runtime.py new file mode 100644 index 000000000000..437f6eeebf52 --- /dev/null +++ b/openhands/runtime/impl/daytona/daytona_runtime.py @@ -0,0 +1,262 @@ +import json +from typing import Callable + +import tenacity +from daytona_sdk import ( + CreateWorkspaceParams, + Daytona, + DaytonaConfig, + SessionExecuteRequest, + Workspace, +) + +from openhands.core.config.app_config import AppConfig +from openhands.events.stream import EventStream +from openhands.runtime.impl.action_execution.action_execution_client import ( + ActionExecutionClient, +) +from openhands.runtime.plugins.requirement import PluginRequirement +from openhands.runtime.utils.command import get_action_execution_server_startup_command +from openhands.utils.async_utils import call_sync_from_async +from openhands.utils.tenacity_stop import stop_if_should_exit + +WORKSPACE_PREFIX = 'openhands-sandbox-' + + +class DaytonaRuntime(ActionExecutionClient): + """The DaytonaRuntime class is a DockerRuntime that utilizes Daytona workspace as a runtime environment.""" + + _sandbox_port: int = 4444 + _vscode_port: int = 4445 + + def __init__( + self, + config: AppConfig, + event_stream: EventStream, + sid: str = 'default', + plugins: list[PluginRequirement] | None = None, + env_vars: dict[str, str] | None = None, + status_callback: Callable | None = None, + attach_to_existing: bool = False, + headless_mode: bool = True, + ): + assert config.daytona_api_key, 'Daytona API key is required' + + self.config = config + self.sid = sid + self.workspace_id = WORKSPACE_PREFIX + sid + self.workspace: Workspace | None = None + self._vscode_url: str | None = None + + daytona_config = DaytonaConfig( + api_key=config.daytona_api_key.get_secret_value(), + server_url=config.daytona_api_url, + target=config.daytona_target, + ) + self.daytona = Daytona(daytona_config) + + # workspace_base cannot be used because we can't bind mount into a workspace. + if self.config.workspace_base is not None: + self.log( + 'warning', + 'Workspace mounting is not supported in the Daytona runtime.', + ) + + super().__init__( + config, + event_stream, + sid, + plugins, + env_vars, + status_callback, + attach_to_existing, + headless_mode, + ) + + def _get_workspace(self) -> Workspace | None: + try: + workspace = self.daytona.get_current_workspace(self.workspace_id) + self.log( + 'info', f'Attached to existing workspace with id: {self.workspace_id}' + ) + except Exception: + self.log( + 'warning', + f'Failed to attach to existing workspace with id: {self.workspace_id}', + ) + workspace = None + + return workspace + + def _get_creation_env_vars(self) -> dict[str, str]: + env_vars: dict[str, str] = { + 'port': str(self._sandbox_port), + 'PYTHONUNBUFFERED': '1', + 'VSCODE_PORT': str(self._vscode_port), + } + + if self.config.debug: + env_vars['DEBUG'] = 'true' + + return env_vars + + def _create_workspace(self) -> Workspace: + workspace_params = CreateWorkspaceParams( + id=self.workspace_id, + language='python', + image=self.config.sandbox.runtime_container_image, + public=True, + env_vars=self._get_creation_env_vars(), + ) + workspace = self.daytona.create(workspace_params) + return workspace + + def _get_workspace_status(self) -> str: + assert self.workspace is not None, 'Workspace is not initialized' + assert ( + self.workspace.instance.info is not None + ), 'Workspace info is not available' + assert ( + self.workspace.instance.info.provider_metadata is not None + ), 'Provider metadata is not available' + + provider_metadata = json.loads(self.workspace.instance.info.provider_metadata) + return provider_metadata.get('status', 'unknown') + + def _construct_api_url(self, port: int) -> str: + assert self.workspace is not None, 'Workspace is not initialized' + assert ( + self.workspace.instance.info is not None + ), 'Workspace info is not available' + assert ( + self.workspace.instance.info.provider_metadata is not None + ), 'Provider metadata is not available' + + node_domain = json.loads(self.workspace.instance.info.provider_metadata)[ + 'nodeDomain' + ] + return f'https://{port}-{self.workspace.id}.{node_domain}' + + def _get_action_execution_server_host(self) -> str: + return self.api_url + + def _start_action_execution_server(self) -> None: + assert self.workspace is not None, 'Workspace is not initialized' + + self.workspace.process.exec( + f'mkdir -p {self.config.workspace_mount_path_in_sandbox}' + ) + + start_command: list[str] = get_action_execution_server_startup_command( + server_port=self._sandbox_port, + plugins=self.plugins, + app_config=self.config, + override_user_id=1000, + override_username='openhands', + ) + start_command_str: str = ' '.join(start_command) + + self.log( + 'debug', + f'Starting action execution server with command: {start_command_str}', + ) + + exec_session_id = 'action-execution-server' + self.workspace.process.create_session(exec_session_id) + self.workspace.process.execute_session_command( + exec_session_id, + SessionExecuteRequest(command='cd /openhands/code', var_async=True), + ) + + exec_command = self.workspace.process.execute_session_command( + exec_session_id, + SessionExecuteRequest(command=start_command_str, var_async=True), + ) + + self.log('debug', f'exec_command_id: {exec_command.cmd_id}') + + @tenacity.retry( + stop=tenacity.stop_after_delay(120) | stop_if_should_exit(), + wait=tenacity.wait_fixed(1), + reraise=(ConnectionRefusedError,), + ) + def _wait_until_alive(self): + super().check_if_alive() + + async def connect(self): + self.send_status_message('STATUS$STARTING_RUNTIME') + + if self.attach_to_existing: + self.workspace = await call_sync_from_async(self._get_workspace) + + if self.workspace is None: + self.send_status_message('STATUS$PREPARING_CONTAINER') + self.workspace = await call_sync_from_async(self._create_workspace) + self.log('info', f'Created new workspace with id: {self.workspace_id}') + + if self._get_workspace_status() == 'stopped': + self.log('info', 'Starting Daytona workspace...') + await call_sync_from_async(self.workspace.start) + + self.api_url = await call_sync_from_async( + self._construct_api_url, self._sandbox_port + ) + + if not self.attach_to_existing: + await call_sync_from_async(self._start_action_execution_server) + self.log( + 'info', + f'Container started. Action execution server url: {self.api_url}', + ) + + self.log('info', 'Waiting for client to become ready...') + self.send_status_message('STATUS$WAITING_FOR_CLIENT') + await call_sync_from_async(self._wait_until_alive) + + if not self.attach_to_existing: + await call_sync_from_async(self.setup_initial_env) + + self.log( + 'info', + f'Container initialized with plugins: {[plugin.name for plugin in self.plugins]}', + ) + + if not self.attach_to_existing: + self.send_status_message(' ') + self._runtime_initialized = True + + def close(self): + super().close() + + if self.attach_to_existing: + return + + if self.workspace: + self.daytona.remove(self.workspace) + + @property + def vscode_url(self) -> str | None: + if self._vscode_url is not None: # cached value + return self._vscode_url + token = super().get_vscode_token() + if not token: + self.log( + 'warning', 'Failed to get VSCode token while trying to get VSCode URL' + ) + return None + if not self.workspace: + self.log( + 'warning', 'Workspace is not initialized while trying to get VSCode URL' + ) + return None + self._vscode_url = ( + self._construct_api_url(self._vscode_port) + + f'/?tkn={token}&folder={self.config.workspace_mount_path_in_sandbox}' + ) + + self.log( + 'debug', + f'VSCode URL: {self._vscode_url}', + ) + + return self._vscode_url diff --git a/poetry.lock b/poetry.lock index b7b7f8f689d9..c0151ab7545e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1507,6 +1507,47 @@ tests-numpy2 = ["Pillow (>=9.4.0)", "absl-py", "decorator", "elasticsearch (<8.0 torch = ["torch"] vision = ["Pillow (>=9.4.0)"] +[[package]] +name = "daytona-api-client" +version = "0.13.0" +description = "Daytona Workspaces" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "daytona_api_client-0.13.0-py3-none-any.whl", hash = "sha256:c4d0dcb89a328c4d0a97d8f076eaf9a00ccc54a8b9f862f4b3302ae887d03c8f"}, + {file = "daytona_api_client-0.13.0.tar.gz", hash = "sha256:d62b7cb14361b2706df192d2da7dc2b5d02be6fd4259e9433cf2bfdc5807416d"}, +] + +[package.dependencies] +pydantic = ">=2" +python-dateutil = ">=2.8.2" +typing-extensions = ">=4.7.1" +urllib3 = ">=1.25.3,<3.0.0" + +[[package]] +name = "daytona-sdk" +version = "0.9.1" +description = "Python SDK for Daytona" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "daytona_sdk-0.9.1-py3-none-any.whl", hash = "sha256:cce6c90cd3d578747b3c388e24c811cb0b21ad125d34b32836c50059a577a12a"}, + {file = "daytona_sdk-0.9.1.tar.gz", hash = "sha256:1e2f219f55130fc72d2f14a57d008b8d3e236d45294e0ca51e249106be5ca5de"}, +] + +[package.dependencies] +daytona_api_client = ">=0.13.0,<1.0.0" +environs = ">=9.5.0,<10.0.0" +marshmallow = ">=3.19.0,<4.0.0" +pydantic = ">=2.4.2,<3.0.0" +python-dateutil = ">=2.8.2,<3.0.0" +urllib3 = ">=2.0.7,<3.0.0" + +[package.extras] +dev = ["black (>=22.0.0)", "isort (>=5.10.0)", "pydoc-markdown (>=4.8.2)"] + [[package]] name = "debugpy" version = "1.8.12" @@ -1731,6 +1772,28 @@ files = [ {file = "english-words-2.0.1.tar.gz", hash = "sha256:a4105c57493bb757a3d8973fcf8e1dc05e7ca09c836dff467c3fb445f84bc43d"}, ] +[[package]] +name = "environs" +version = "9.5.0" +description = "simplified environment variable parsing" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "environs-9.5.0-py2.py3-none-any.whl", hash = "sha256:1e549569a3de49c05f856f40bce86979e7d5ffbbc4398e7f338574c220189124"}, + {file = "environs-9.5.0.tar.gz", hash = "sha256:a76307b36fbe856bdca7ee9161e6c466fd7fcffc297109a118c59b54e27e30c9"}, +] + +[package.dependencies] +marshmallow = ">=3.0.0" +python-dotenv = "*" + +[package.extras] +dev = ["dj-database-url", "dj-email-url", "django-cache-url", "flake8 (==4.0.1)", "flake8-bugbear (==21.9.2)", "mypy (==0.910)", "pre-commit (>=2.4,<3.0)", "pytest", "tox"] +django = ["dj-database-url", "dj-email-url", "django-cache-url"] +lint = ["flake8 (==4.0.1)", "flake8-bugbear (==21.9.2)", "mypy (==0.910)", "pre-commit (>=2.4,<3.0)"] +tests = ["dj-database-url", "dj-email-url", "django-cache-url", "pytest"] + [[package]] name = "evaluate" version = "0.4.3" @@ -3132,14 +3195,14 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "huggingface-hub" -version = "0.28.1" +version = "0.29.0" description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" optional = false python-versions = ">=3.8.0" groups = ["main", "evaluation", "llama-index"] files = [ - {file = "huggingface_hub-0.28.1-py3-none-any.whl", hash = "sha256:aa6b9a3ffdae939b72c464dbb0d7f99f56e649b55c3d52406f49e0a5a620c0a7"}, - {file = "huggingface_hub-0.28.1.tar.gz", hash = "sha256:893471090c98e3b6efbdfdacafe4052b20b84d59866fb6f54c33d9af18c303ae"}, + {file = "huggingface_hub-0.29.0-py3-none-any.whl", hash = "sha256:c02daa0b6bafbdacb1320fdfd1dc7151d0940825c88c4ef89837fdb1f6ea0afe"}, + {file = "huggingface_hub-0.29.0.tar.gz", hash = "sha256:64034c852be270cac16c5743fe1f659b14515a9de6342d6f42cbb2ede191fc80"}, ] [package.dependencies] @@ -4004,14 +4067,14 @@ files = [ [[package]] name = "kubernetes" -version = "32.0.0" +version = "32.0.1" description = "Kubernetes python client" optional = false python-versions = ">=3.6" groups = ["llama-index"] files = [ - {file = "kubernetes-32.0.0-py2.py3-none-any.whl", hash = "sha256:60fd8c29e8e43d9c553ca4811895a687426717deba9c0a66fb2dcc3f5ef96692"}, - {file = "kubernetes-32.0.0.tar.gz", hash = "sha256:319fa840345a482001ac5d6062222daeb66ec4d1bcb3087402aed685adf0aecb"}, + {file = "kubernetes-32.0.1-py2.py3-none-any.whl", hash = "sha256:35282ab8493b938b08ab5526c7ce66588232df00ef5e1dbe88a419107dc10998"}, + {file = "kubernetes-32.0.1.tar.gz", hash = "sha256:42f43d49abd437ada79a79a16bd48a604d3471a117a8347e87db693f2ba0ba28"}, ] [package.dependencies] @@ -4786,7 +4849,7 @@ version = "3.26.1" description = "A lightweight library for converting complex datatypes to and from native Python datatypes." optional = false python-versions = ">=3.9" -groups = ["evaluation", "llama-index"] +groups = ["main", "evaluation", "llama-index"] files = [ {file = "marshmallow-3.26.1-py3-none-any.whl", hash = "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c"}, {file = "marshmallow-3.26.1.tar.gz", hash = "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6"}, @@ -4933,14 +4996,14 @@ urllib3 = "*" [[package]] name = "mistune" -version = "3.1.1" +version = "3.1.2" description = "A sane and fast Markdown parser with useful plugins and renderers" optional = false python-versions = ">=3.8" groups = ["runtime"] files = [ - {file = "mistune-3.1.1-py3-none-any.whl", hash = "sha256:02106ac2aa4f66e769debbfa028509a275069dcffce0dfa578edd7b991ee700a"}, - {file = "mistune-3.1.1.tar.gz", hash = "sha256:e0740d635f515119f7d1feb6f9b192ee60f0cc649f80a8f944f905706a21654c"}, + {file = "mistune-3.1.2-py3-none-any.whl", hash = "sha256:4b47731332315cdca99e0ded46fc0004001c1299ff773dfb48fbe1fd226de319"}, + {file = "mistune-3.1.2.tar.gz", hash = "sha256:733bf018ba007e8b5f2d3a9eb624034f6ee26c4ea769a98ec533ee111d504dff"}, ] [[package]] @@ -8391,33 +8454,34 @@ pathspec = ">=0.10.1" [[package]] name = "scikit-image" -version = "0.25.1" +version = "0.25.2" description = "Image processing in Python" optional = false python-versions = ">=3.10" groups = ["evaluation"] files = [ - {file = "scikit_image-0.25.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:40763a3a089617e6f00f92d46b3475368b9783588a165c2aa854da95b66bb4ff"}, - {file = "scikit_image-0.25.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:7c6b69f33e5512ee7fc53361b064430f146583f08dc75317667e81d5f8fcd0c6"}, - {file = "scikit_image-0.25.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9187347d115776ff0ddba3e5d2a04638d291b1a62e3c315d17b71eea351cde8"}, - {file = "scikit_image-0.25.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdfca713979ad1873a4b55d94bb1eb4bc713f0c10165b261bf6f7e606f44a00c"}, - {file = "scikit_image-0.25.1-cp310-cp310-win_amd64.whl", hash = "sha256:167fb146de80bb2a1493d1a760a9ac81644a8a5de254c3dd12a95d1b662d819c"}, - {file = "scikit_image-0.25.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c1bde2d5f1dfb23b3c72ef9fcdb2dd5f42fa353e8bd606aea63590eba5e79565"}, - {file = "scikit_image-0.25.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:5112d95cccaa45c434e57efc20c1f721ab439e516e2ed49709ddc2afb7c15c70"}, - {file = "scikit_image-0.25.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f5e313b028f5d7a9f3888ad825ddf4fb78913d7762891abb267b99244b4dd31"}, - {file = "scikit_image-0.25.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39ad76aeff754048dabaff83db752aa0655dee425f006678d14485471bdb459d"}, - {file = "scikit_image-0.25.1-cp311-cp311-win_amd64.whl", hash = "sha256:8dc8b06176c1a2316fa8bc539fd7e96155721628ae5cf51bc1a2c62cb9786581"}, - {file = "scikit_image-0.25.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ebf83699d60134909647395a0bf07db3859646de7192b088e656deda6bc15e95"}, - {file = "scikit_image-0.25.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:408086520eed036340e634ab7e4f648e00238f711bac61ab933efeb11464a238"}, - {file = "scikit_image-0.25.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bd709faa87795869ccd21f32490c37989ca5846571495822f4b9430fb42c34c"}, - {file = "scikit_image-0.25.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b15c0265c072a46ff4720784d756d8f8e5d63567639aa8451f6673994d6846"}, - {file = "scikit_image-0.25.1-cp312-cp312-win_amd64.whl", hash = "sha256:a689a0d091e0bd97d7767309abdeb27c43be210d075abb34e71657add920c22b"}, - {file = "scikit_image-0.25.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f070f899d6572a125ab106c4b26d1a5fb784dc60ba6dea45c7816f08c3a4fb4d"}, - {file = "scikit_image-0.25.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:cc9538d8db7670878aa68ea79c0b1796b6c771085e8d50f5408ee617da3281b6"}, - {file = "scikit_image-0.25.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:caa08d4fa851e1f421fcad8eac24d32f2810971dc61f1d72dc950ca9e9ec39b1"}, - {file = "scikit_image-0.25.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9923aa898b7921fbcf503d32574d48ed937a7cff45ce8587be4868b39676e18"}, - {file = "scikit_image-0.25.1-cp313-cp313-win_amd64.whl", hash = "sha256:6c7bba6773ab8c39ee8b1cbb17c7f98965bacdb8cd8da337942be6acc38fc562"}, - {file = "scikit_image-0.25.1.tar.gz", hash = "sha256:d4ab30540d114d37c35fe5c837f89b94aaba2a7643afae8354aa353319e9bbbb"}, + {file = "scikit_image-0.25.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d3278f586793176599df6a4cf48cb6beadae35c31e58dc01a98023af3dc31c78"}, + {file = "scikit_image-0.25.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:5c311069899ce757d7dbf1d03e32acb38bb06153236ae77fcd820fd62044c063"}, + {file = "scikit_image-0.25.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be455aa7039a6afa54e84f9e38293733a2622b8c2fb3362b822d459cc5605e99"}, + {file = "scikit_image-0.25.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4c464b90e978d137330be433df4e76d92ad3c5f46a22f159520ce0fdbea8a09"}, + {file = "scikit_image-0.25.2-cp310-cp310-win_amd64.whl", hash = "sha256:60516257c5a2d2f74387c502aa2f15a0ef3498fbeaa749f730ab18f0a40fd054"}, + {file = "scikit_image-0.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f4bac9196fb80d37567316581c6060763b0f4893d3aca34a9ede3825bc035b17"}, + {file = "scikit_image-0.25.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:d989d64ff92e0c6c0f2018c7495a5b20e2451839299a018e0e5108b2680f71e0"}, + {file = "scikit_image-0.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2cfc96b27afe9a05bc92f8c6235321d3a66499995675b27415e0d0c76625173"}, + {file = "scikit_image-0.25.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24cc986e1f4187a12aa319f777b36008764e856e5013666a4a83f8df083c2641"}, + {file = "scikit_image-0.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:b4f6b61fc2db6340696afe3db6b26e0356911529f5f6aee8c322aa5157490c9b"}, + {file = "scikit_image-0.25.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8db8dd03663112783221bf01ccfc9512d1cc50ac9b5b0fe8f4023967564719fb"}, + {file = "scikit_image-0.25.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:483bd8cc10c3d8a7a37fae36dfa5b21e239bd4ee121d91cad1f81bba10cfb0ed"}, + {file = "scikit_image-0.25.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d1e80107bcf2bf1291acfc0bf0425dceb8890abe9f38d8e94e23497cbf7ee0d"}, + {file = "scikit_image-0.25.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a17e17eb8562660cc0d31bb55643a4da996a81944b82c54805c91b3fe66f4824"}, + {file = "scikit_image-0.25.2-cp312-cp312-win_amd64.whl", hash = "sha256:bdd2b8c1de0849964dbc54037f36b4e9420157e67e45a8709a80d727f52c7da2"}, + {file = "scikit_image-0.25.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7efa888130f6c548ec0439b1a7ed7295bc10105458a421e9bf739b457730b6da"}, + {file = "scikit_image-0.25.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:dd8011efe69c3641920614d550f5505f83658fe33581e49bed86feab43a180fc"}, + {file = "scikit_image-0.25.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28182a9d3e2ce3c2e251383bdda68f8d88d9fff1a3ebe1eb61206595c9773341"}, + {file = "scikit_image-0.25.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8abd3c805ce6944b941cfed0406d88faeb19bab3ed3d4b50187af55cf24d147"}, + {file = "scikit_image-0.25.2-cp313-cp313-win_amd64.whl", hash = "sha256:64785a8acefee460ec49a354706db0b09d1f325674107d7fa3eadb663fb56d6f"}, + {file = "scikit_image-0.25.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:330d061bd107d12f8d68f1d611ae27b3b813b8cdb0300a71d07b1379178dd4cd"}, + {file = "scikit_image-0.25.2.tar.gz", hash = "sha256:e5a37e6cd4d0c018a7a55b9d601357e3382826d3888c10d0213fc63bff977dde"}, ] [package.dependencies] @@ -8427,16 +8491,16 @@ networkx = ">=3.0" numpy = ">=1.24" packaging = ">=21" pillow = ">=10.1" -scipy = ">=1.11.2" +scipy = ">=1.11.4" tifffile = ">=2022.8.12" [package.extras] -build = ["Cython (>=3.0.8)", "build (>=1.2.1)", "meson-python (>=0.16)", "ninja (>=1.11.1.1)", "numpy (>=2.0)", "pythran (>=0.16)", "setuptools (>=68)", "spin (==0.13)"] +build = ["Cython (>=3.0.8)", "build (>=1.2.1)", "meson-python (>=0.16)", "ninja (>=1.11.1.1)", "numpy (>=2.0)", "pythran (>=0.16)", "spin (==0.13)"] data = ["pooch (>=1.6.0)"] developer = ["ipython", "pre-commit", "tomli"] -docs = ["PyWavelets (>=1.6)", "dask[array] (>=2022.9.2)", "intersphinx-registry (>=0.2411.14)", "ipykernel", "ipywidgets", "kaleido (==0.2.1)", "matplotlib (>=3.7)", "myst-parser", "numpydoc (>=1.7)", "pandas (>=2.0)", "plotly (>=5.20)", "pooch (>=1.6)", "pydata-sphinx-theme (>=0.16)", "pytest-doctestplus", "scikit-learn (>=1.2)", "seaborn (>=0.11)", "sphinx (>=8.0)", "sphinx-copybutton", "sphinx-gallery[parallel] (>=0.18)", "sphinx_design (>=0.5)", "tifffile (>=2022.8.12)"] -optional = ["PyWavelets (>=1.6)", "SimpleITK", "astropy (>=5.0)", "cloudpickle (>=0.2.1)", "dask[array] (>=2021.1.0,!=2024.8.0)", "matplotlib (>=3.7)", "pooch (>=1.6.0)", "pyamg (>=5.2)", "scikit-learn (>=1.2)"] -test = ["asv", "numpydoc (>=1.7)", "pooch (>=1.6.0)", "pytest (>=7.0)", "pytest-cov (>=2.11.0)", "pytest-doctestplus", "pytest-faulthandler", "pytest-localserver"] +docs = ["PyWavelets (>=1.6)", "dask[array] (>=2023.2.0)", "intersphinx-registry (>=0.2411.14)", "ipykernel", "ipywidgets", "kaleido (==0.2.1)", "matplotlib (>=3.7)", "myst-parser", "numpydoc (>=1.7)", "pandas (>=2.0)", "plotly (>=5.20)", "pooch (>=1.6)", "pydata-sphinx-theme (>=0.16)", "pytest-doctestplus", "scikit-learn (>=1.2)", "seaborn (>=0.11)", "sphinx (>=8.0)", "sphinx-copybutton", "sphinx-gallery[parallel] (>=0.18)", "sphinx_design (>=0.5)", "tifffile (>=2022.8.12)"] +optional = ["PyWavelets (>=1.6)", "SimpleITK", "astropy (>=5.0)", "cloudpickle (>=1.1.1)", "dask[array] (>=2023.2.0)", "matplotlib (>=3.7)", "pooch (>=1.6.0)", "pyamg (>=5.2)", "scikit-learn (>=1.2)"] +test = ["asv", "numpydoc (>=1.7)", "pooch (>=1.6.0)", "pytest (>=8)", "pytest-cov (>=2.11.0)", "pytest-doctestplus", "pytest-faulthandler", "pytest-localserver"] [[package]] name = "scikit-learn" @@ -9206,14 +9270,14 @@ files = [ [[package]] name = "tifffile" -version = "2025.1.10" +version = "2025.2.18" description = "Read and write TIFF files" optional = false python-versions = ">=3.10" groups = ["evaluation"] files = [ - {file = "tifffile-2025.1.10-py3-none-any.whl", hash = "sha256:ed24cf4c99fb13b4f5fb29f8a0d5605e60558c950bccbdca2a6470732a27cfb3"}, - {file = "tifffile-2025.1.10.tar.gz", hash = "sha256:baaf0a3b87bf7ec375fa1537503353f70497eabe1bdde590f2e41cc0346e612f"}, + {file = "tifffile-2025.2.18-py3-none-any.whl", hash = "sha256:54b36c4d5e5b8d8920134413edfe5a7cfb1c7617bb50cddf7e2772edb7149043"}, + {file = "tifffile-2025.2.18.tar.gz", hash = "sha256:8d731789e691b468746c1615d989bc550ac93cf753e9210865222e90a5a95d11"}, ] [package.dependencies] @@ -10789,4 +10853,4 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"] [metadata] lock-version = "2.1" python-versions = "^3.12" -content-hash = "14998d54438fedacad9d82422003f46d0d7721bd50c2f8096657c15dce0f3edd" +content-hash = "39e0f069346a4d1e52193899989b79ea3e02f81d67fbb2ac0fdc87e70bd1008f" diff --git a/pyproject.toml b/pyproject.toml index 8cc47ebaeb0a..b8919d936deb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,6 +76,7 @@ stripe = "^11.5.0" ipywidgets = "^8.1.5" qtconsole = "^5.6.1" memory-profiler = "^0.61.0" +daytona-sdk = "0.9.1" [tool.poetry.group.llama-index.dependencies] llama-index = "*" diff --git a/tests/runtime/conftest.py b/tests/runtime/conftest.py index 73b18680a11e..bb0c1eca696b 100644 --- a/tests/runtime/conftest.py +++ b/tests/runtime/conftest.py @@ -11,6 +11,7 @@ from openhands.core.logger import openhands_logger as logger from openhands.events import EventStream from openhands.runtime.base import Runtime +from openhands.runtime.impl.daytona.daytona_runtime import DaytonaRuntime from openhands.runtime.impl.docker.docker_runtime import DockerRuntime from openhands.runtime.impl.local.local_runtime import LocalRuntime from openhands.runtime.impl.remote.remote_runtime import RemoteRuntime @@ -130,6 +131,8 @@ def get_runtime_classes() -> list[type[Runtime]]: return [RemoteRuntime] elif runtime.lower() == 'runloop': return [RunloopRuntime] + elif runtime.lower() == 'daytona': + return [DaytonaRuntime] else: raise ValueError(f'Invalid runtime: {runtime}') diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 10f09447ba6c..0848dda676de 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -686,6 +686,7 @@ def test_api_keys_repr_str(): modal_api_token_id='my_modal_api_token_id', modal_api_token_secret='my_modal_api_token_secret', runloop_api_key='my_runloop_api_key', + daytona_api_key='my_daytona_api_key', ) assert 'my_e2b_api_key' not in repr(app_config) assert 'my_e2b_api_key' not in str(app_config) @@ -697,6 +698,8 @@ def test_api_keys_repr_str(): assert 'my_modal_api_token_secret' not in str(app_config) assert 'my_runloop_api_key' not in repr(app_config) assert 'my_runloop_api_key' not in str(app_config) + assert 'my_daytona_api_key' not in repr(app_config) + assert 'my_daytona_api_key' not in str(app_config) # Check that no other attrs in AppConfig have 'key' or 'token' in their name # This will fail when new attrs are added, and attract attention @@ -705,6 +708,7 @@ def test_api_keys_repr_str(): 'modal_api_token_id', 'modal_api_token_secret', 'runloop_api_key', + 'daytona_api_key', ] for attr_name in AppConfig.model_fields.keys(): if (