Source code for camel.runtime.daytona_runtime

# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========

import inspect
import json
import os
from functools import wraps
from typing import Any, Dict, List, Optional, Union

from pydantic import BaseModel

from camel.logger import get_logger
from camel.runtime import BaseRuntime
from camel.toolkits.function_tool import FunctionTool

logger = get_logger(__name__)


[docs] class DaytonaRuntime(BaseRuntime): r"""A runtime that executes functions in a Daytona sandbox environment. Requires the Daytona server to be running and an API key configured. Args: api_key (Optional[str]): The Daytona API key for authentication. If not provided, it will try to use the DAYTONA_API_KEY environment variable. (default: :obj: `None`) api_url (Optional[str]): The URL of the Daytona server. If not provided, it will try to use the DAYTONA_API_URL environment variable. If none is provided, it will use "http://localhost:8000". (default: :obj: `None`) language (Optional[str]): The programming language for the sandbox. (default: :obj: `"python"`) """ def __init__( self, api_key: Optional[str] = None, api_url: Optional[str] = None, language: Optional[str] = "python", ): from daytona_sdk import Daytona, DaytonaConfig super().__init__() self.api_key = api_key or os.environ.get('DAYTONA_API_KEY') self.api_url = api_url or os.environ.get('DAYTONA_API_URL') self.language = language self.config = DaytonaConfig(api_key=self.api_key, api_url=self.api_url) self.daytona = Daytona(self.config) self.sandbox = None self.entrypoint: Dict[str, str] = dict()
[docs] def build(self) -> "DaytonaRuntime": r"""Create and start a Daytona sandbox. Returns: DaytonaRuntime: The current runtime. """ from daytona_sdk import CreateSandboxParams try: params = CreateSandboxParams(language=self.language) self.sandbox = self.daytona.create(params) if self.sandbox is None: raise RuntimeError("Failed to create sandbox.") logger.info(f"Sandbox created with ID: {self.sandbox.id}") except Exception as e: logger.error(f"Failed to create sandbox: {e!s}") raise RuntimeError(f"Daytona sandbox creation failed: {e!s}") return self
def _cleanup(self): r"""Clean up the sandbox when exiting.""" if self.sandbox: try: self.daytona.remove(self.sandbox) logger.info(f"Sandbox {self.sandbox.id} removed") self.sandbox = None except Exception as e: logger.error(f"Failed to remove sandbox: {e!s}")
[docs] def add( self, funcs: Union[FunctionTool, List[FunctionTool]], entrypoint: str, arguments: Optional[Dict[str, Any]] = None, ) -> "DaytonaRuntime": r"""Add a function or list of functions to the runtime. Args: funcs (Union[FunctionTool, List[FunctionTool]]): The function or list of functions to add. entrypoint (str): The entrypoint for the function. arguments (Optional[Dict[str, Any]]): The arguments for the function. (default: :obj: `None`) Returns: DaytonaRuntime: The current runtime. """ if not isinstance(funcs, list): funcs = [funcs] if arguments is not None: entrypoint += json.dumps(arguments, ensure_ascii=False) def make_wrapper(inner_func, func_name, func_code): r"""Creates a wrapper for a function to execute it in the Daytona sandbox. Args: inner_func (Callable): The function to wrap. func_name (str): The name of the function. func_code (str): The source code of the function. Returns: Callable: A wrapped function that executes in the sandbox. """ @wraps(inner_func) def wrapper(*args, **kwargs): if not self.sandbox: raise RuntimeError( "Sandbox not initialized. Call build() first." ) try: for key, value in kwargs.items(): if isinstance(value, BaseModel): kwargs[key] = value.model_dump() args_str = json.dumps(args, ensure_ascii=True) kwargs_str = json.dumps(kwargs, ensure_ascii=True) except (TypeError, ValueError) as e: logger.error(f"Failed to serialize arguments: {e!s}") return {"error": f"Argument serialization failed: {e!s}"} # Upload function code to the sandbox script_path = f"/home/daytona/{func_name}.py" try: self.sandbox.fs.upload_file( script_path, func_code.encode() ) except Exception as e: logger.error( f"Failed to upload function {func_name}: {e!s}" ) return {"error": f"Upload failed: {e!s}"} exec_code = ( f"import sys\n" f"sys.path.append('/home/daytona')\n" f"import json\n" f"from {func_name} import {func_name}\n" f"args = json.loads('{args_str}')\n" f"kwargs = json.loads('{kwargs_str}')\n" f"result = {func_name}(*args, **kwargs)\n" f"print(json.dumps(result) if result is not " f"None else 'null')" ) # Execute the function in the sandbox try: response = self.sandbox.process.code_run(exec_code) return ( json.loads(response.result) if response.result else None ) except json.JSONDecodeError as e: logger.error( f"Failed to decode JSON response for {func_name}" ) return {"error": f"JSON decoding failed: {e!s}"} except Exception as e: logger.error( f"Failed to execute function {func_name}: {e!s}" ) return {"error": f"Execution failed: {e!s}"} return wrapper for func in funcs: inner_func = func.func func_name = func.get_function_name() func_code = inspect.getsource(inner_func).strip() func.func = make_wrapper(inner_func, func_name, func_code) self.tools_map[func_name] = func self.entrypoint[func_name] = entrypoint return self
[docs] def info(self) -> str: r"""Get information about the current sandbox. Returns: str: Information about the sandbox. Raises: RuntimeError: If the sandbox is not initialized. """ if self.sandbox is None: raise RuntimeError("Failed to create sandbox.") info = self.sandbox.info() return ( f"Sandbox {info.name}:\n" f"State: {info.state}\n" f"Resources: {info.resources.cpu} CPU, {info.resources.memory} RAM" )
def __del__(self): r"""Clean up the sandbox when the object is deleted.""" if hasattr(self, 'sandbox'): self._cleanup()
[docs] def stop(self) -> "DaytonaRuntime": r"""Stop and remove the sandbox. Returns: DaytonaRuntime: The current runtime. """ self._cleanup() return self
[docs] def reset(self) -> "DaytonaRuntime": r"""Reset the sandbox by stopping and rebuilding it. Returns: DaytonaRuntime: The current runtime. """ return self.stop().build()
@property def docs(self) -> str: r"""Get the URL for the Daytona API documentation. Returns: str: The URL for the API documentation. """ return "https://www.daytona.io/docs/python-sdk/daytona/"