diff --git a/camel/agents/embodied_agent.py b/camel/agents/embodied_agent.py index 33ae6ec1d8..56cf9dcfea 100644 --- a/camel/agents/embodied_agent.py +++ b/camel/agents/embodied_agent.py @@ -11,15 +11,20 @@ # See the License for the specific language governing permissions and # limitations under the License. # =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== -from typing import Any, Dict, List, Optional +from typing import Any, List, Optional from colorama import Fore -from camel.agents import BaseToolAgent, ChatAgent, HuggingFaceToolAgent +from camel.agents import BaseToolAgent, ChatAgent +from camel.interpreters import ( + BaseInterpreter, + InternalPythonInterpreter, + SubprocessInterpreter, +) from camel.messages import BaseMessage from camel.responses import ChatAgentResponse from camel.types import ModelType -from camel.utils import PythonInterpreter, print_text_animated +from camel.utils import print_text_animated class EmbodiedAgent(ChatAgent): @@ -34,8 +39,13 @@ class EmbodiedAgent(ChatAgent): message_window_size (int, optional): The maximum number of previous messages to include in the context window. If `None`, no windowing is performed. (default: :obj:`None`) - action_space (List[Any], optional): The action space for the embodied - agent. (default: :obj:`None`) + tool_agents (List[BaseToolAgent], optional): The tools agents to use in + the embodied agent. (default: :obj:`None`) + code_interpreter (BaseInterpreter, optional): The code interpreter to + execute codes. If `code_interpreter` and `tool_agent` are both + `None`, default to `SubProcessInterpreter`. If `code_interpreter` + is `None` and `tool_agents` is not `None`, default to + `InternalPythonInterpreter`. (default: :obj:`None`) verbose (bool, optional): Whether to print the critic's messages. logger_color (Any): The color of the logger displayed to the user. (default: :obj:`Fore.MAGENTA`) @@ -47,18 +57,22 @@ def __init__( model_type: ModelType = ModelType.GPT_4, model_config: Optional[Any] = None, message_window_size: Optional[int] = None, - action_space: Optional[List[BaseToolAgent]] = None, + tool_agents: Optional[List[BaseToolAgent]] = None, + code_interpreter: Optional[BaseInterpreter] = None, verbose: bool = False, logger_color: Any = Fore.MAGENTA, ) -> None: - default_action_space = [ - HuggingFaceToolAgent('hugging_face_tool_agent', - model_type=model_type.value), - ] - self.action_space = action_space or default_action_space - action_space_prompt = self.get_action_space_prompt() - system_message.content = system_message.content.format( - action_space=action_space_prompt) + self.tool_agents = tool_agents + self.code_interpreter: BaseInterpreter + if code_interpreter is not None: + self.code_interpreter = code_interpreter + elif self.tool_agents: + self.code_interpreter = InternalPythonInterpreter() + else: + self.code_interpreter = SubprocessInterpreter() + + if self.tool_agents: + system_message = self._set_tool_agents(system_message) self.verbose = verbose self.logger_color = logger_color super().__init__( @@ -68,16 +82,41 @@ def __init__( message_window_size=message_window_size, ) - def get_action_space_prompt(self) -> str: + def _set_tool_agents(self, system_message: BaseMessage) -> BaseMessage: + action_space_prompt = self._get_tool_agents_prompt() + result_message = system_message.create_new_instance( + content=system_message.content.format( + action_space=action_space_prompt)) + if self.tool_agents is not None: + self.code_interpreter.update_action_space( + {tool.name: tool + for tool in self.tool_agents}) + return result_message + + def _get_tool_agents_prompt(self) -> str: r"""Returns the action space prompt. Returns: str: The action space prompt. """ - return "\n".join([ - f"*** {action.name} ***:\n {action.description}" - for action in self.action_space - ]) + if self.tool_agents is not None: + return "\n".join([ + f"*** {tool.name} ***:\n {tool.description}" + for tool in self.tool_agents + ]) + else: + return "" + + def get_tool_agent_names(self) -> List[str]: + r"""Returns the names of tool agents. + + Returns: + List[str]: The names of tool agents. + """ + if self.tool_agents is not None: + return [tool.name for tool in self.tool_agents] + else: + return [] def step( self, @@ -111,28 +150,24 @@ def step( if len(explanations) > len(codes): print_text_animated(self.logger_color + - f"> Explanation:\n{explanations}") + f"> Explanation:\n{explanations[-1]}") content = response.msg.content if codes is not None: - content = "\n> Executed Results:" - action_space: Dict[str, Any] = { - action.name: action - for action in self.action_space - } - action_space.update({"print": print, "enumerate": enumerate}) - interpreter = PythonInterpreter(action_space=action_space) - for block_idx, code in enumerate(codes): - executed_outputs, _ = code.execute(interpreter) - content += (f"Executing code block {block_idx}:\n" - f" - execution output:\n{executed_outputs}\n" - f" - Local variables:\n{interpreter.state}\n") - content += "*" * 50 + "\n" + try: + content = "\n> Executed Results:\n" + for block_idx, code in enumerate(codes): + executed_output = self.code_interpreter.run( + code, code.code_type) + content += (f"Executing code block {block_idx}: {{\n" + + executed_output + "}\n") + except InterruptedError as e: + content = (f"\n> Running code fail: {e}\n" + "Please regenerate the code.") # TODO: Handle errors - content = input_message.content + (Fore.RESET + - f"\n> Embodied Actions:\n{content}") + content = input_message.content + f"\n> Embodied Actions:\n{content}" message = BaseMessage(input_message.role_name, input_message.role_type, input_message.meta_dict, content) return ChatAgentResponse([message], response.terminated, response.info) diff --git a/camel/interpreters/__init__.py b/camel/interpreters/__init__.py new file mode 100644 index 0000000000..428ce1a568 --- /dev/null +++ b/camel/interpreters/__init__.py @@ -0,0 +1,25 @@ +# =========== 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. =========== + +from .base import BaseInterpreter +from .interpreter_error import InterpreterError +from .internal_python_interpreter import InternalPythonInterpreter +from .subprocess_interpreter import SubprocessInterpreter + +__all__ = [ + 'BaseInterpreter', + 'InterpreterError', + 'InternalPythonInterpreter', + 'SubprocessInterpreter', +] diff --git a/camel/interpreters/base.py b/camel/interpreters/base.py new file mode 100644 index 0000000000..725d71ae47 --- /dev/null +++ b/camel/interpreters/base.py @@ -0,0 +1,49 @@ +# =========== 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. =========== +from abc import ABC, abstractmethod +from typing import Any, Dict, List + + +class BaseInterpreter(ABC): + r"""An abstract base class for code interpreters.""" + + @abstractmethod + def run(self, code: str, code_type: str) -> str: + r"""Executes the given code based on its type. + + Args: + code (str): The code to be executed. + code_type (str): The type of the code, which must be one of the + types returned by `supported_code_types()`. + + Returns: + str: The result of the code execution. If the execution fails, this + should include sufficient information to diagnose and correct + the issue. + + Raises: + InterpreterError: If the code execution encounters errors that + could be resolved by modifying or regenerating the code. + """ + pass + + @abstractmethod + def supported_code_types(self) -> List[str]: + r"""Provides supported code types by the interpreter.""" + pass + + @abstractmethod + def update_action_space(self, action_space: Dict[str, Any]) -> None: + r"""Updates action space for *python* interpreter""" + pass diff --git a/camel/utils/python_interpreter.py b/camel/interpreters/internal_python_interpreter.py similarity index 85% rename from camel/utils/python_interpreter.py rename to camel/interpreters/internal_python_interpreter.py index f437fe0125..83e11af56c 100644 --- a/camel/utils/python_interpreter.py +++ b/camel/interpreters/internal_python_interpreter.py @@ -17,20 +17,14 @@ import typing from typing import Any, Dict, List, Optional - -class InterpreterError(ValueError): - r"""An error raised when the interpreter cannot evaluate a Python - expression, due to syntax error or unsupported operations. - """ - - pass +from camel.interpreters import BaseInterpreter, InterpreterError -class PythonInterpreter(): +class InternalPythonInterpreter(BaseInterpreter): r"""A customized python interpreter to control the execution of LLM-generated codes. The interpreter makes sure the code can only execute functions given in action space and import white list. It also supports - fuzzy variable matching to reveive uncertain input variable name. + fuzzy variable matching to retrieve uncertain input variable name. .. highlight:: none @@ -64,35 +58,88 @@ class PythonInterpreter(): Modifications copyright (C) 2023 CAMEL-AI.org Args: - action_space (Dict[str, Any]): A dictionary that maps action names to - their corresponding functions or objects. The interpreter can only - execute functions that are either directly listed in this + action_space (Dict[str, Any], optional): A dictionary that maps action + names to their corresponding functions or objects. The interpreter + can only execute functions that are either directly listed in this dictionary or are member functions of objects listed in this dictionary. The concept of :obj:`action_space` is derived from EmbodiedAgent, representing the actions that an agent is capable of - performing. - import_white_list (Optional[List[str]], optional): A list that stores + performing. If `None`, set to empty dict. (default: :obj:`None`) + import_white_list (List[str], optional): A list that stores the Python modules or functions that can be imported in the code. All submodules and functions of the modules listed in this list are importable. Any other import statements will be rejected. The module and its submodule or function name are separated by a period (:obj:`.`). (default: :obj:`None`) + unsafe_mode (bool, optional): If `True`, the interpreter runs the code + by `eval()` without any security check. (default: :obj:`False`) raise_error (bool, optional): Raise error if the interpreter fails. + (default: :obj:`False`) """ - def __init__(self, action_space: Dict[str, Any], - import_white_list: Optional[List[str]] = None, - raise_error: bool = False) -> None: - self.action_space = action_space + _CODE_TYPES = ["python", "py", "python3", "python2"] + + def __init__( + self, + action_space: Optional[Dict[str, Any]] = None, + import_white_list: Optional[List[str]] = None, + unsafe_mode: bool = False, + raise_error: bool = False, + ) -> None: + self.action_space = action_space or dict() self.state = self.action_space.copy() - self.fuzz_state: Dict[str, Any] = {} - self.import_white_list = import_white_list or [] + self.fuzz_state: Dict[str, Any] = dict() + self.import_white_list = import_white_list or list() self.raise_error = raise_error + self.unsafe_mode = unsafe_mode + + def run(self, code: str, code_type: str) -> str: + r"""Executes the given code with specified code type in the + interpreter. + + This method takes a string of code and its type, checks if the code + type is supported, and then executes the code. If `unsafe_mode` is + set to `False`, the code is executed in a controlled environment using + the `execute` method. If `unsafe_mode` is `True`, the code is executed + using `eval()` with the action space as the global context. An + `InterpreterError` is raised if the code type is unsupported or if any + runtime error occurs during execution. + + Args: + code (str): The python code to be executed. + code_type (str): The type of the code, which should be one of the + supported code types (`python`, `py`, `python3`, `python2`). + + + Returns: + str: The string representation of the output of the executed code. + + Raises: + InterpreterError: If the `code_type` is not supported or if any + runtime error occurs during the execution of the code. + """ + if code_type not in self._CODE_TYPES: + raise InterpreterError( + f"Unsupported code type {code_type}. " + f"`{self.__class__.__name__}` only supports " + f"{', '.join(self._CODE_TYPES)}.") + if not self.unsafe_mode: + return str(self.execute(code)) + else: + return str(eval(code, self.action_space)) + + def update_action_space(self, action_space: Dict[str, Any]) -> None: + r"""Updates action space for *python* interpreter.""" + self.action_space.update(action_space) + + def supported_code_types(self) -> List[str]: + r"""Provides supported code types by the interpreter.""" + return self._CODE_TYPES def execute(self, code: str, state: Optional[Dict[str, Any]] = None, fuzz_state: Optional[Dict[str, Any]] = None, keep_state: bool = True) -> Any: - r""" Execute the input python codes in a security environment. + r"""Execute the input python codes in a security environment. Args: code (str): Generated python code to be executed. @@ -154,7 +201,7 @@ def execute(self, code: str, state: Optional[Dict[str, Any]] = None, return result def clear_state(self) -> None: - r"""Initialize :obj:`state` and :obj:`fuzz_state`""" + r"""Initialize :obj:`state` and :obj:`fuzz_state`.""" self.state = self.action_space.copy() self.fuzz_state = {} diff --git a/camel/interpreters/interpreter_error.py b/camel/interpreters/interpreter_error.py new file mode 100644 index 0000000000..2e7f818b09 --- /dev/null +++ b/camel/interpreters/interpreter_error.py @@ -0,0 +1,18 @@ +# =========== 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. =========== + + +class InterpreterError(Exception): + r"""Exception raised for errors that can be solved by regenerating code """ + pass diff --git a/camel/interpreters/subprocess_interpreter.py b/camel/interpreters/subprocess_interpreter.py new file mode 100644 index 0000000000..623e2d66de --- /dev/null +++ b/camel/interpreters/subprocess_interpreter.py @@ -0,0 +1,180 @@ +# =========== 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 shlex +import subprocess +import tempfile +from pathlib import Path +from typing import Any, Dict, List + +from colorama import Fore + +from camel.interpreters import BaseInterpreter, InterpreterError + + +class SubprocessInterpreter(BaseInterpreter): + r""" SubprocessInterpreter is a class for executing code files or code + strings in a subprocess. + + This class handles the execution of code in different scripting languages + (currently Python and Bash) within a subprocess, 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. (default: :obj:`True`) + print_stdout (bool, optional): If True, print the standard output of + the executed code. (default: :obj:`False`) + print_stderr (bool, optional): If True, print the standard error of the + executed code. (default: :obj:`True`) + """ + + _CODE_EXECUTE_CMD_MAPPING = { + "python": "python {file_name}", + "bash": "bash {file_name}", + } + + _CODE_EXTENSION_MAPPING = { + "python": "py", + "bash": "sh", + } + + _CODE_TYPE_MAPPING = { + "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 + + def run_file( + self, + file: Path, + code_type: str, + ) -> str: + r"""Executes a code file in a subprocess and captures its output. + + Args: + file (Path): The path object of the file to run. + 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: + RuntimeError: If the provided file path does not point to a file. + InterpreterError: If the code type provided is not supported. + """ + if not file.is_file(): + raise RuntimeError(f"{file} is not a file.") + code_type = self._check_code_type(code_type) + cmd = shlex.split(self._CODE_EXECUTE_CMD_MAPPING[code_type].format( + file_name=str(file))) + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, text=True) + stdout, stderr = proc.communicate() + if self.print_stdout and stdout: + print("======stdout======") + print(Fore.GREEN + stdout + Fore.RESET) + print("==================") + if self.print_stderr and stderr: + print("======stderr======") + print(Fore.RED + stderr + Fore.RESET) + print("==================") + exec_result = f"{stdout}" + exec_result += f"(stderr: {stderr})" if stderr else "" + return exec_result + + def run( + self, + code: str, + code_type: str, + ) -> str: + r""" Generates a temporary file with the given code, executes it, and + deletes the file afterward. + + 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 if the + code type is unsupported. + """ + 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 on your computer:") + print(Fore.CYAN + code + Fore.RESET) + while True: + choice = input("Running code? [Y/n]:").lower() + if choice in ["y", "yes", "ye", ""]: + break + elif choice in ["no", "n"]: + raise InterpreterError( + "Execution halted: User opted not to run the code. " + "This choice stops the current operation and any " + "further code execution.") + temp_file_path = self._create_temp_file( + code=code, extension=self._CODE_EXTENSION_MAPPING[code_type]) + + result = self.run_file(temp_file_path, code_type) + + temp_file_path.unlink() + return result + + def _create_temp_file(self, code: str, extension: str) -> Path: + with tempfile.NamedTemporaryFile(mode="w", delete=False, + suffix=f".{extension}") as f: + f.write(code) + name = f.name + return Path(name) + + 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] + + def supported_code_types(self) -> List[str]: + r"""Provides supported code types by the interpreter.""" + return list(self._CODE_EXTENSION_MAPPING.keys()) + + 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`.") diff --git a/camel/prompts/base.py b/camel/prompts/base.py index 4e47a8976f..f5c5c14399 100644 --- a/camel/prompts/base.py +++ b/camel/prompts/base.py @@ -12,20 +12,11 @@ # limitations under the License. # =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== import inspect -from typing import ( - Any, - Callable, - Dict, - List, - Optional, - Set, - Tuple, - TypeVar, - Union, -) +from typing import Any, Callable, Dict, Optional, Set, TypeVar, Union +from camel.interpreters import BaseInterpreter, SubprocessInterpreter from camel.types import RoleType -from camel.utils import PythonInterpreter +from camel.utils import get_system_information T = TypeVar('T') @@ -172,33 +163,38 @@ def set_code_type(self, code_type: str) -> None: self._code_type = code_type def execute( - self, interpreter: Optional[PythonInterpreter] = None, - user_variable: Optional[Dict[str, Any]] = None - ) -> Tuple[Any, PythonInterpreter]: - r"""Executes the code string by a given python interpreter. + self, + interpreter: Optional[BaseInterpreter] = None, + **kwargs: Any, + ) -> str: + r"""Executes the code string using the provided interpreter. + + This method runs a code string through either a specified interpreter + or a default one. It supports additional keyword arguments for + flexibility. Args: - interpreter (PythonInterpreter, optional): interpreter to be used - during code execution. (default: :obj:`None`) - user_variable (Optional[Dict[str, Any]]): variables that can be - used in the code, which applying fuzzy matching, such as images - or documents. (default: :obj:`None`) + interpreter (Optional[BaseInterpreter]): The interpreter instance + to use for execution. If `None`, a default interpreter is used. + (default: :obj:`None`) + **kwargs: Additional keyword arguments passed to the interpreter to + run the code. Returns: - Tuple[Any, PythonInterpreter]: A tuple containing the execution - result and the used interpreter. The execution result - represents the value of the last statement (excluding "import") - in the code. This value could potentially be the desired result - of the LLM-generated code. + str: The result of the code execution. If the execution fails, this + should include sufficient information to diagnose and correct + the issue. + + Raises: + InterpreterError: If the code execution encounters errors that + could be resolved by modifying or regenerating the code. """ - # NOTE: Only supports Python code for now. - if not interpreter: - action_space = {} - action_space.update({"print": print, "enumerate": enumerate}) - interpreter = PythonInterpreter(action_space=action_space) - execution_res = interpreter.execute(self, fuzz_state=user_variable, - keep_state=True) - return execution_res, interpreter + if interpreter is None: + execution_res = SubprocessInterpreter().run( + self, self._code_type, **kwargs) + else: + execution_res = interpreter.run(self, self._code_type, **kwargs) + return execution_res # flake8: noqa :E501 @@ -206,22 +202,24 @@ class TextPromptDict(Dict[Any, TextPrompt]): r"""A dictionary class that maps from key to :obj:`TextPrompt` object. """ EMBODIMENT_PROMPT = TextPrompt( + "System information :" + + "\n".join(f"{key}: {value}" + for key, value in get_system_information().items()) + "\n" + """You are the physical embodiment of the {role} who is working on solving a task: {task}. You can do things in the physical world including browsing the Internet, reading documents, drawing images, creating videos, executing code and so on. Your job is to perform the physical actions necessary to interact with the physical world. You will receive thoughts from the {role} and you will need to perform the actions described in the thoughts. -You can write a series of simple commands in Python to act. -You can perform a set of actions by calling the available Python functions. +You can write a series of simple commands in to act. +You can perform a set of actions by calling the available functions. You should perform actions based on the descriptions of the functions. -Here is your action space: +Here is your action space but it is not limited: {action_space} -You should only perform actions in the action space. You can perform multiple actions. You can perform actions in any order. -First, explain the actions you will perform and your reasons, then write Python code to implement your actions. -If you decide to perform actions, you must write Python code to implement the actions. +First, explain the actions you will perform and your reasons, then write code to implement your actions. +If you decide to perform actions, you must write code to implement the actions. You may print intermediate results if necessary.""") def __init__(self, *args: Any, **kwargs: Any) -> None: diff --git a/camel/utils/__init__.py b/camel/utils/__init__.py index ec91f4ef16..f741005c9c 100644 --- a/camel/utils/__init__.py +++ b/camel/utils/__init__.py @@ -11,7 +11,6 @@ # See the License for the specific language governing permissions and # limitations under the License. # =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== -from .python_interpreter import PythonInterpreter from .commons import ( openai_api_key_required, print_text_animated, @@ -20,6 +19,7 @@ download_tasks, get_task_list, check_server_running, + get_system_information, ) from .token_counting import ( get_model_encoding, @@ -34,9 +34,9 @@ 'get_prompt_template_key_words', 'get_first_int', 'download_tasks', - 'PythonInterpreter', 'get_task_list', 'check_server_running', + 'get_system_information', 'get_model_encoding', 'BaseTokenCounter', 'OpenAITokenCounter', diff --git a/camel/utils/commons.py b/camel/utils/commons.py index 16bca813b8..72c758a659 100644 --- a/camel/utils/commons.py +++ b/camel/utils/commons.py @@ -12,6 +12,7 @@ # limitations under the License. # =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== import os +import platform import re import socket import time @@ -178,3 +179,22 @@ def check_server_running(server_url: str) -> bool: # if the port is open, the result should be 0. return result == 0 + + +def get_system_information(): + r"""Gathers information about the operating system. + + Returns: + dict: A dictionary containing various pieces of OS information. + """ + sys_info = { + "OS Name": os.name, + "System": platform.system(), + "Release": platform.release(), + "Version": platform.version(), + "Machine": platform.machine(), + "Processor": platform.processor(), + "Platform": platform.platform(), + } + + return sys_info diff --git a/docs/get_started/code_prompt.md b/docs/get_started/code_prompt.md index 3b87a7696f..33b4becc5d 100644 --- a/docs/get_started/code_prompt.md +++ b/docs/get_started/code_prompt.md @@ -53,34 +53,13 @@ In this example, we change the code type of the `CodePrompt` instance from `None ## Executing the Code -The `CodePrompt` class provides a method called `execute` that allows you to execute the code string associated with the prompt. It returns a tuple containing the value of the last statement in the code and the interpreter. +The `CodePrompt` class provides a method called `execute` that allows you to execute the code string associated with the prompt. It returns a string containing the stdout and stderr. ```python -code_prompt = CodePrompt("a = 1 + 1\nb = a + 1", code_type="python") -output, interpreter = code_prompt.execute() +code_prompt = CodePrompt("a = 1 + 1\nb = a + 1\nprint(a,b)", code_type="python") +output = code_prompt.execute() +# Running code? [Y/n]: y print(output) -# >>> 3 +# >>> 2 3 -print(interpreter.state['a']) -# >>> 2 - -print(interpreter.state['b']) -# >>> 3 ``` - -In this example, we execute the code prompt and inspect the state of variables `a` and `b`. - -## Handling Execution Errors - -If there is an error during the code execution, the `execute` method catches the error and returns the traceback. - -```python -code_prompt = CodePrompt("print('Hello, World!'") -traceback, _ = code_prompt.execute() -assert "SyntaxError" in traceback -# >>> True -``` - -In this example, the code string has a syntax error where a right bracket `)` is missing, and the `execute` method returns the traceback indicating the error. - -That's it! You have went through the basics of using the `CodePrompt` class. You can now create code prompts, access the code and code type, modify the code type if needed, and execute the code. \ No newline at end of file diff --git a/examples/embodiment/code_execution.py b/examples/embodiment/code_execution.py new file mode 100644 index 0000000000..8df5eb62c1 --- /dev/null +++ b/examples/embodiment/code_execution.py @@ -0,0 +1,47 @@ +# =========== 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. =========== +from camel.agents import EmbodiedAgent +from camel.generators import SystemMessageGenerator +from camel.messages import BaseMessage +from camel.types import RoleType + + +def main(): + # Create an embodied agent + role_name = "Programmer" + meta_dict = dict(role=role_name, task="Programming") + sys_msg = SystemMessageGenerator().from_dict( + meta_dict=meta_dict, + role_tuple=(role_name, RoleType.EMBODIMENT), + ) + embodied_agent = EmbodiedAgent( + sys_msg, + verbose=True, + ) + print(embodied_agent.system_message.content) + user_msg = BaseMessage.make_user_message( + role_name=role_name, + content=( + "Write a bash script to install numpy, " + "then write a python script to compute " + "the dot product of [6.75,3] and [4,5] and print the result, " + "then write a script to open a browser and search today's weather." + ), + ) + response = embodied_agent.step(user_msg) + print(response.msg.content) + + +if __name__ == "__main__": + main() diff --git a/examples/embodiment/hugging_face_tool.py b/examples/embodiment/hugging_face_tool.py index e89f99f98e..414fbcc5e3 100644 --- a/examples/embodiment/hugging_face_tool.py +++ b/examples/embodiment/hugging_face_tool.py @@ -27,18 +27,18 @@ def main(): sys_msg = SystemMessageGenerator().from_dict( meta_dict=meta_dict, role_tuple=(f"{role_name}'s Embodiment", RoleType.EMBODIMENT)) - action_space = [ + tool_agents = [ HuggingFaceToolAgent( 'hugging_face_tool_agent', model_type=ModelType.GPT_4.value, remote=True, ) ] - action_space: List[BaseToolAgent] + tool_agents: List[BaseToolAgent] embodied_agent = EmbodiedAgent( sys_msg, verbose=True, - action_space=action_space, + tool_agents=tool_agents, ) user_msg = BaseMessage.make_user_message( role_name=role_name, diff --git a/test/agents/test_embodied_agent.py b/test/agents/test_embodied_agent.py index 3896c2d550..7482f72d1c 100644 --- a/test/agents/test_embodied_agent.py +++ b/test/agents/test_embodied_agent.py @@ -31,10 +31,8 @@ def test_get_action_space_prompt(): meta_dict=meta_dict, role_tuple=(f"{role_name}'s Embodiment", RoleType.EMBODIMENT)) agent = EmbodiedAgent( - sys_msg, - action_space=[HuggingFaceToolAgent('hugging_face_tool_agent')]) - expected_prompt = "*** hugging_face_tool_agent ***:\n" - assert agent.get_action_space_prompt().startswith(expected_prompt) + sys_msg, tool_agents=[HuggingFaceToolAgent('hugging_face_tool_agent')]) + assert 'hugging_face_tool_agent' in agent.get_tool_agent_names() @pytest.mark.skip(reason="Wait huggingface to update openaiv1") diff --git a/test/utils/test_python_interpreter.py b/test/interpreters/test_python_interpreter.py similarity index 81% rename from test/utils/test_python_interpreter.py rename to test/interpreters/test_python_interpreter.py index f7a815fcec..9ee0871c85 100644 --- a/test/utils/test_python_interpreter.py +++ b/test/interpreters/test_python_interpreter.py @@ -15,8 +15,7 @@ import pytest import torch -from camel.utils import PythonInterpreter -from camel.utils.python_interpreter import InterpreterError +from camel.interpreters import InternalPythonInterpreter, InterpreterError def action_function(): @@ -27,11 +26,14 @@ def action_function(): def interpreter(): action_space = {"action1": action_function, "str": str} white_list = ["torch", "numpy.array", "openai"] - return PythonInterpreter(action_space=action_space, - import_white_list=white_list, raise_error=True) + return InternalPythonInterpreter( + action_space=action_space, + import_white_list=white_list, + raise_error=True, + ) -def test_state_update(interpreter: PythonInterpreter): +def test_state_update(interpreter: InternalPythonInterpreter): code = "x = input_variable" input_variable = 10 execution_res = interpreter.execute( @@ -39,7 +41,7 @@ def test_state_update(interpreter: PythonInterpreter): assert execution_res == input_variable -def test_syntax_error(interpreter: PythonInterpreter): +def test_syntax_error(interpreter: InternalPythonInterpreter): code = "x input_variable" with pytest.raises(InterpreterError) as e: interpreter.execute(code) @@ -47,7 +49,7 @@ def test_syntax_error(interpreter: PythonInterpreter): assert "Syntax error in code: invalid syntax" in exec_msg -def test_import_success0(interpreter: PythonInterpreter): +def test_import_success0(interpreter: InternalPythonInterpreter): code = """import torch as pt, openai a = pt.tensor([[1., -1.], [1., -1.]]) openai.__version__""" @@ -57,21 +59,21 @@ def test_import_success0(interpreter: PythonInterpreter): assert isinstance(execution_res, str) -def test_import_success1(interpreter: PythonInterpreter): +def test_import_success1(interpreter: InternalPythonInterpreter): code = """from torch import tensor a = tensor([[1., -1.], [1., -1.]])""" execution_res = interpreter.execute(code) assert torch.equal(execution_res, torch.tensor([[1., -1.], [1., -1.]])) -def test_import_success2(interpreter: PythonInterpreter): +def test_import_success2(interpreter: InternalPythonInterpreter): code = """from numpy import array x = array([[1, 2, 3], [4, 5, 6]])""" execution_res = interpreter.execute(code) assert np.equal(execution_res, np.array([[1, 2, 3], [4, 5, 6]])).all() -def test_import_fail0(interpreter: PythonInterpreter): +def test_import_fail0(interpreter: InternalPythonInterpreter): code = """import os os.mkdir("/tmp/test")""" with pytest.raises(InterpreterError) as e: @@ -82,7 +84,7 @@ def test_import_fail0(interpreter: PythonInterpreter): " white list (try to import os).") -def test_import_fail1(interpreter: PythonInterpreter): +def test_import_fail1(interpreter: InternalPythonInterpreter): code = """import numpy as np x = np.array([[1, 2, 3], [4, 5, 6]], np.int32)""" with pytest.raises(InterpreterError) as e: @@ -93,13 +95,13 @@ def test_import_fail1(interpreter: PythonInterpreter): " white list (try to import numpy).") -def test_action_space(interpreter: PythonInterpreter): +def test_action_space(interpreter: InternalPythonInterpreter): code = "res = action1()" execution_res = interpreter.execute(code) assert execution_res == "access action function" -def test_fuzz_space(interpreter: PythonInterpreter): +def test_fuzz_space(interpreter: InternalPythonInterpreter): from PIL import Image fuzz_state = {"image": Image.new("RGB", (256, 256))} code = "output_image = input_image.crop((20, 20, 100, 100))" @@ -108,7 +110,7 @@ def test_fuzz_space(interpreter: PythonInterpreter): assert execution_res.height == 80 -def test_keep_state0(interpreter: PythonInterpreter): +def test_keep_state0(interpreter: InternalPythonInterpreter): code1 = "a = 42" code2 = "b = a" code3 = "c = b" @@ -124,7 +126,7 @@ def test_keep_state0(interpreter: PythonInterpreter): "The variable `b` is not defined.") -def test_keep_state1(interpreter: PythonInterpreter): +def test_keep_state1(interpreter: InternalPythonInterpreter): code1 = "from torch import tensor" code2 = "a = tensor([[1., -1.], [1., -1.]])" execution_res = interpreter.execute(code1, keep_state=True) @@ -137,14 +139,14 @@ def test_keep_state1(interpreter: PythonInterpreter): "The variable `tensor` is not defined.") -def test_assign0(interpreter: PythonInterpreter): +def test_assign0(interpreter: InternalPythonInterpreter): code = "a = b = 1" interpreter.execute(code) assert interpreter.state["a"] == 1 assert interpreter.state["b"] == 1 -def test_assign1(interpreter: PythonInterpreter): +def test_assign1(interpreter: InternalPythonInterpreter): code = "a, b = c = 2, 3" interpreter.execute(code) assert interpreter.state["a"] == 2 @@ -152,7 +154,7 @@ def test_assign1(interpreter: PythonInterpreter): assert interpreter.state["c"] == (2, 3) -def test_assign_fail(interpreter: PythonInterpreter): +def test_assign_fail(interpreter: InternalPythonInterpreter): code = "x = a, b, c = 2, 3" with pytest.raises(InterpreterError) as e: interpreter.execute(code, keep_state=False) @@ -161,7 +163,7 @@ def test_assign_fail(interpreter: PythonInterpreter): "Expected 3 values but got 2.") -def test_if0(interpreter: PythonInterpreter): +def test_if0(interpreter: InternalPythonInterpreter): code = """a = 0 b = 1 if a < b: @@ -175,7 +177,7 @@ def test_if0(interpreter: PythonInterpreter): assert interpreter.state["b"] == 0 -def test_if1(interpreter: PythonInterpreter): +def test_if1(interpreter: InternalPythonInterpreter): code = """a = 1 b = 0 if a < b: @@ -189,7 +191,7 @@ def test_if1(interpreter: PythonInterpreter): assert interpreter.state["b"] == 1 -def test_compare(interpreter: PythonInterpreter): +def test_compare(interpreter: InternalPythonInterpreter): assert interpreter.execute("2 > 1") is True assert interpreter.execute("2 >= 1") is True assert interpreter.execute("2 < 1") is False @@ -202,7 +204,7 @@ def test_compare(interpreter: PythonInterpreter): assert interpreter.execute("1 not in [1, 2]") is False -def test_oprators(interpreter: PythonInterpreter): +def test_oprators(interpreter: InternalPythonInterpreter): assert interpreter.execute("1 + 1") == 2 assert interpreter.execute("1 - 1") == 0 assert interpreter.execute("1 * 1") == 1 @@ -217,7 +219,7 @@ def test_oprators(interpreter: PythonInterpreter): assert interpreter.execute("not True") is False -def test_for(interpreter: PythonInterpreter): +def test_for(interpreter: InternalPythonInterpreter): code = """l = [2, 3, 5, 7, 11] sum = 0 for i in l: @@ -226,14 +228,14 @@ def test_for(interpreter: PythonInterpreter): assert execution_res == 28 -def test_subscript_access(interpreter: PythonInterpreter): +def test_subscript_access(interpreter: InternalPythonInterpreter): code = """l = [2, 3, 5, 7, 11] res = l[3]""" execution_res = interpreter.execute(code) assert execution_res == 7 -def test_subscript_assign(interpreter: PythonInterpreter): +def test_subscript_assign(interpreter: InternalPythonInterpreter): code = """l = [2, 3, 5, 7, 11] l[3] = 1""" with pytest.raises(InterpreterError) as e: @@ -244,7 +246,7 @@ def test_subscript_assign(interpreter: PythonInterpreter): "ast.Tuple, got Subscript instead.") -def test_dict(interpreter: PythonInterpreter): +def test_dict(interpreter: InternalPythonInterpreter): code = """x = {1: 10, 2: 20} y = {"number": 30, **x} res = y[1] + y[2] + y["numbers"]""" @@ -252,7 +254,7 @@ def test_dict(interpreter: PythonInterpreter): assert execution_res == 60 -def test_formatted_value(interpreter: PythonInterpreter): +def test_formatted_value(interpreter: InternalPythonInterpreter): code = """x = 3 res = f"x = {x}" """ @@ -260,14 +262,14 @@ def test_formatted_value(interpreter: PythonInterpreter): assert execution_res == "x = 3" -def test_joined_str(interpreter: PythonInterpreter): +def test_joined_str(interpreter: InternalPythonInterpreter): code = """l = ["2", "3", "5", "7", "11"] res = ",".join(l)""" execution_res = interpreter.execute(code) assert execution_res == "2,3,5,7,11" -def test_expression_not_support(interpreter: PythonInterpreter): +def test_expression_not_support(interpreter: InternalPythonInterpreter): code = """x = 1 x += 1""" with pytest.raises(InterpreterError) as e: diff --git a/test/interpreters/test_subprocess_interpreter.py b/test/interpreters/test_subprocess_interpreter.py new file mode 100644 index 0000000000..bcdc36170b --- /dev/null +++ b/test/interpreters/test_subprocess_interpreter.py @@ -0,0 +1,112 @@ +# =========== 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. =========== +from pathlib import Path + +import pytest + +from camel.interpreters import InterpreterError, SubprocessInterpreter + + +@pytest.fixture +def subprocess_interpreter(): + return SubprocessInterpreter( + require_confirm=False, + print_stdout=True, + print_stderr=True, + ) + + +def test_run_python_code(subprocess_interpreter): + python_code = """ +def add(a, b): + return a + b + +result = add(10, 20) +print(result) +""" + result = subprocess_interpreter.run(python_code, "python") + assert "30\n" in result + + +def test_python_stderr(subprocess_interpreter): + python_code = """ +def divide(a, b): + return a / b + +result = divide(10, 0) +print(result) +""" + result = subprocess_interpreter.run(python_code, "python") + assert "ZeroDivisionError: division by zero" in result + + +def test_run_bash_code(subprocess_interpreter): + bash_code = """ +#!/bin/bash + +function add() { + echo $(($1 + $2)) +} + +result=$(add 10 20) +echo $result +""" + result = subprocess_interpreter.run(bash_code, "bash") + assert "30\n" in result + + +def test_bash_stderr(subprocess_interpreter): + bash_code = """ +#!/bin/bash + +echo $(undefined_command) +""" + result = subprocess_interpreter.run(bash_code, "bash") + assert "stderr: " in result + assert "undefined_command: command not found" in result + + +def test_run_file_not_found(subprocess_interpreter): + with pytest.raises(RuntimeError) as exc_info: + subprocess_interpreter.run_file( + Path("/path/to/nonexistent/file"), + "python", + ) + assert "/path/to/nonexistent/file is not a file." in str(exc_info.value) + + +def test_run_unsupported_code_type(subprocess_interpreter): + with pytest.raises(InterpreterError) as exc_info: + subprocess_interpreter.run("print('Hello')", "unsupported_code_type") + assert "Unsupported code type unsupported_code_type." in str( + exc_info.value) + + +def test_require_confirm(subprocess_interpreter, monkeypatch): + subprocess_interpreter.require_confirm = True + python_code = "print('Hello')" + + # Simulate user input 'n' for no + monkeypatch.setattr('builtins.input', lambda _: 'n') + + with pytest.raises(InterpreterError) as exc_info: + subprocess_interpreter.run(python_code, "python") + assert "Execution halted" in str(exc_info.value) + + # Simulate user input 'y' for yes + monkeypatch.setattr('builtins.input', lambda _: 'y') + + # No error should be raised when user inputs 'y' + result = subprocess_interpreter.run(python_code, "python") + assert "Hello\n" in result diff --git a/test/prompts/test_prompt_base.py b/test/prompts/test_prompt_base.py index 58c5076001..30cda09fbe 100644 --- a/test/prompts/test_prompt_base.py +++ b/test/prompts/test_prompt_base.py @@ -11,7 +11,6 @@ # See the License for the specific language governing permissions and # limitations under the License. # =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== -import pytest from camel.prompts.base import ( CodePrompt, @@ -20,8 +19,6 @@ return_prompt_wrapper, wrap_prompt_functions, ) -from camel.utils import PythonInterpreter -from camel.utils.python_interpreter import InterpreterError def test_return_prompt_wrapper(): @@ -139,21 +136,16 @@ def test_code_prompt_set_code_type(): assert code_prompt.code_type == "python" -def test_code_prompt_execute(capsys): +def test_code_prompt_execute(monkeypatch): + monkeypatch.setattr('builtins.input', lambda _: 'Y') code_prompt = CodePrompt("a = 1\nprint('Hello, World!')", code_type="python") - interpreter = PythonInterpreter(action_space={"print": print}) - result, interpreter = code_prompt.execute(interpreter=interpreter) - captured = capsys.readouterr() - assert result == 1 - assert interpreter.state["a"] == 1 - assert captured.out == "Hello, World!\n" + result = code_prompt.execute() + assert result == "Hello, World!\n" -def test_code_prompt_execute_error(): +def test_code_prompt_execute_error(monkeypatch): + monkeypatch.setattr('builtins.input', lambda _: "Y") code_prompt = CodePrompt("print('Hello, World!'", code_type="python") - interpreter = PythonInterpreter(action_space={"print": print}, - raise_error=True) - with pytest.raises(InterpreterError) as e: - _, _ = code_prompt.execute(interpreter=interpreter) - assert e.value.args[0].startswith("Syntax error in code:") + result = code_prompt.execute() + assert "SyntaxError:" in result diff --git a/test/utils/test_commons.py b/test/utils/test_commons.py index 5c1a569bda..55d8f60df7 100644 --- a/test/utils/test_commons.py +++ b/test/utils/test_commons.py @@ -11,7 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== -from camel.utils import get_task_list +from camel.utils import get_system_information, get_task_list def test_get_task_list(): @@ -37,3 +37,28 @@ def test_get_task_list(): assert isinstance(task_list, list) assert isinstance(task_list[0], str) assert len(task_list) == 1 + + +def test_get_system_information(): + # Call the function + sys_info = get_system_information() + + # Check if the result is a dictionary + assert isinstance(sys_info, dict) + + # Define the expected keys + expected_keys = [ + "OS Name", + "System", + "Release", + "Version", + "Machine", + "Processor", + "Platform", + ] + + # Check if all expected keys are in the returned dictionary + assert all(key in sys_info for key in expected_keys) + + # Check if all values are non-empty strings + assert all(isinstance(value, str) and value for value in sys_info.values())