Spaces:
Build error
Build error
| import json | |
| from typing import Callable | |
| import httpx | |
| import tenacity | |
| from daytona_sdk import ( | |
| CreateWorkspaceParams, | |
| Daytona, | |
| DaytonaConfig, | |
| SessionExecuteRequest, | |
| Workspace, | |
| ) | |
| from openhands.core.config.openhands_config import OpenHandsConfig | |
| 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.runtime.utils.request import RequestHTTPError | |
| 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: OpenHandsConfig, | |
| 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 _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 action_execution_server_url(self) -> str: | |
| return self.api_url | |
| def _start_action_execution_server(self) -> None: | |
| assert self.workspace is not None, 'Workspace is not initialized' | |
| 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 = ( | |
| f'mkdir -p {self.config.workspace_mount_path_in_sandbox} && cd /openhands/code && ' | |
| + ' '.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) | |
| 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}') | |
| def _wait_until_alive(self): | |
| super().check_if_alive() | |
| async def connect(self): | |
| self.send_status_message('STATUS$STARTING_RUNTIME') | |
| should_start_action_execution_server = False | |
| if self.attach_to_existing: | |
| self.workspace = await call_sync_from_async(self._get_workspace) | |
| else: | |
| should_start_action_execution_server = True | |
| 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}') | |
| self.api_url = self._construct_api_url(self._sandbox_port) | |
| state = self.workspace.instance.state | |
| if state == 'stopping': | |
| self.log('info', 'Waiting for Daytona workspace to stop...') | |
| await call_sync_from_async(self.workspace.wait_for_workspace_stop) | |
| state = 'stopped' | |
| if state == 'stopped': | |
| self.log('info', 'Starting Daytona workspace...') | |
| await call_sync_from_async(self.workspace.start) | |
| should_start_action_execution_server = True | |
| if should_start_action_execution_server: | |
| 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 should_start_action_execution_server: | |
| 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 should_start_action_execution_server: | |
| self.send_status_message(' ') | |
| self._runtime_initialized = True | |
| def _send_action_server_request(self, method, url, **kwargs): | |
| return super()._send_action_server_request(method, url, **kwargs) | |
| def close(self): | |
| super().close() | |
| if self.attach_to_existing: | |
| return | |
| if self.workspace: | |
| self.daytona.remove(self.workspace) | |
| 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 | |
| def additional_agent_instructions(self) -> str: | |
| return f'When showing endpoints to access applications for any port, e.g. port 3000, instead of localhost:3000, use this format: {self._construct_api_url(3000)}.' | |