Source code for camel.interpreters.docker_interpreter

# =========== Copyright 2023 @ 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 @ CAMEL-AI.org. All Rights Reserved. ===========

import io
import shlex
import tarfile
import uuid
from pathlib import Path
from typing import TYPE_CHECKING, Any, ClassVar, Dict, List, Optional

from colorama import Fore

from camel.interpreters.base import BaseInterpreter
from camel.interpreters.interpreter_error import InterpreterError
from camel.utils import is_docker_running

if TYPE_CHECKING:
    from docker.models.containers import Container


[docs] class DockerInterpreter(BaseInterpreter): r"""A class for executing code files or code strings in a docker container. This class handles the execution of code in different scripting languages (currently Python and Bash) within a docker container, capturing their stdout and stderr streams, and allowing user checking before executing code strings. Args: require_confirm (bool, optional): If `True`, prompt user before running code strings for security. Defaults to `True`. print_stdout (bool, optional): If `True`, print the standard output of the executed code. Defaults to `False`. print_stderr (bool, optional): If `True`, print the standard error of the executed code. Defaults to `True`. """ _CODE_EXECUTE_CMD_MAPPING: ClassVar[Dict[str, str]] = { "python": "python {file_name}", "bash": "bash {file_name}", } _CODE_EXTENSION_MAPPING: ClassVar[Dict[str, str]] = { "python": "py", "bash": "sh", } _CODE_TYPE_MAPPING: ClassVar[Dict[str, str]] = { "python": "python", "py3": "python", "python3": "python", "py": "python", "shell": "bash", "bash": "bash", "sh": "bash", } def __init__( self, require_confirm: bool = True, print_stdout: bool = False, print_stderr: bool = True, ) -> None: self.require_confirm = require_confirm self.print_stdout = print_stdout self.print_stderr = print_stderr # lazy initialization of container self._container: Optional[Container] = None def __del__(self) -> None: r"""Destructor for the DockerInterpreter class. This method ensures that the Docker container is removed when the interpreter is deleted. """ if self._container is not None: self._container.remove(force=True) def _initialize_if_needed(self) -> None: if self._container is not None: return if not is_docker_running(): raise InterpreterError( "Docker daemon is not running. Please install/start docker " "and try again." ) import docker client = docker.from_env() self._container = client.containers.run( "python:3.10", detach=True, name=f"camel-interpreter-{uuid.uuid4()}", command="tail -f /dev/null", ) def _create_file_in_container(self, content: str) -> Path: # get a random name for the file filename = str(uuid.uuid4()) # create a tar in memory tar_stream = io.BytesIO() with tarfile.open(fileobj=tar_stream, mode='w') as tar: tarinfo = tarfile.TarInfo(name=filename) tarinfo.size = len(content) tar.addfile(tarinfo, io.BytesIO(content.encode('utf-8'))) tar_stream.seek(0) # copy the tar into the container if self._container is None: raise InterpreterError( "Container is not initialized. Try running the code again." ) self._container.put_archive("/tmp", tar_stream) return Path(f"/tmp/{filename}") def _run_file_in_container( self, file: Path, code_type: str, ) -> str: code_type = self._check_code_type(code_type) commands = shlex.split( self._CODE_EXECUTE_CMD_MAPPING[code_type].format( file_name=file.as_posix() ) ) if self._container is None: raise InterpreterError( "Container is not initialized. Try running the code again." ) stdout, stderr = self._container.exec_run( commands, demux=True, ).output if self.print_stdout and stdout: print("======stdout======") print(Fore.GREEN + stdout.decode() + Fore.RESET) print("==================") if self.print_stderr and stderr: print("======stderr======") print(Fore.RED + stderr.decode() + Fore.RESET) print("==================") exec_result = f"{stdout.decode()}" if stdout else "" exec_result += f"(stderr: {stderr.decode()})" if stderr else "" return exec_result
[docs] def run( self, code: str, code_type: str, ) -> str: r"""Executes the given code in the conatiner attached to the interpreter, and captures the stdout and stderr streams. Args: code (str): The code string to execute. code_type (str): The type of code to execute (e.g., 'python', 'bash'). Returns: str: A string containing the captured stdout and stderr of the executed code. Raises: InterpreterError: If the user declines to run the code, or the code type is unsupported, or there is an error in the docker API/container """ import docker.errors code_type = self._check_code_type(code_type) # Print code for security checking if self.require_confirm: print(f"The following {code_type} code will run in container:") print(Fore.CYAN + code + Fore.RESET) while True: choice = input("Running code? [Y/n]:").lower() if choice in ["y", "yes", "ye", ""]: break elif choice not in ["no", "n"]: continue raise InterpreterError( "Execution halted: User opted not to run the code. " "This choice stops the current operation and any " "further code execution." ) self._initialize_if_needed() try: temp_file_path = self._create_file_in_container(code) result = self._run_file_in_container(temp_file_path, code_type) except docker.errors.APIError as e: raise InterpreterError( f"Execution halted due to docker API error: {e.explanation}. " "This choice stops the current operation and any " "further code execution." ) from e except docker.errors.DockerException as e: raise InterpreterError( f"Execution halted due to docker exceptoin: {e}. " "This choice stops the current operation and any " "further code execution." ) from e return result
def _check_code_type(self, code_type: str) -> str: if code_type not in self._CODE_TYPE_MAPPING: raise InterpreterError( f"Unsupported code type {code_type}. Currently " f"`{self.__class__.__name__}` only supports " f"{', '.join(self._CODE_EXTENSION_MAPPING.keys())}." ) return self._CODE_TYPE_MAPPING[code_type]
[docs] def supported_code_types(self) -> List[str]: r"""Provides supported code types by the interpreter.""" return list(self._CODE_EXTENSION_MAPPING.keys())
[docs] def update_action_space(self, action_space: Dict[str, Any]) -> None: r"""Updates action space for *python* interpreter""" raise RuntimeError( "SubprocessInterpreter doesn't support " "`action_space`." )