diff --git a/.github/workflows/documentation.yaml b/.github/workflows/documentation.yaml index 613bafbb3..b3ac1f6d2 100644 --- a/.github/workflows/documentation.yaml +++ b/.github/workflows/documentation.yaml @@ -13,7 +13,8 @@ jobs: pip install sphinx sphinx_book_theme recommonmark numpy openai tenacity tiktoken colorama - name: Sphinx build run: | - sphinx-build docs docs/_build + cd docs + make html - name: Deploy uses: peaceiris/actions-gh-pages@v3 if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master'}} diff --git a/.github/workflows/pytest_package.yml b/.github/workflows/pytest_package.yml index 8a41856aa..664943a78 100644 --- a/.github/workflows/pytest_package.yml +++ b/.github/workflows/pytest_package.yml @@ -13,7 +13,7 @@ permissions: contents: read jobs: - pytest_package: + pytest_package_test: runs-on: ubuntu-latest @@ -40,3 +40,59 @@ jobs: OPENAI_API_KEY: "${{ secrets.OPENAI_API_KEY }}" run: | pytest test/ + + pytest_package_full_test: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.8 + uses: actions/setup-python@v3 + with: + python-version: "3.8" + - uses: actions/cache@v3 + id: cache + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.*') }} + restore-keys: | + ${{ runner.os }}-pip- + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 pytest + pip install -e . + - name: Test with pytest + env: + OPENAI_API_KEY: "${{ secrets.OPENAI_API_KEY }}" + run: | + pytest --full-test-mode test/ + + pytest_package_fast_test: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.8 + uses: actions/setup-python@v3 + with: + python-version: "3.8" + - uses: actions/cache@v3 + id: cache + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.*') }} + restore-keys: | + ${{ runner.os }}-pip- + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 pytest + pip install -e . + - name: Test with pytest + env: + OPENAI_API_KEY: "${{ secrets.OPENAI_API_KEY }}" + run: | + pytest --fast-test-mode test/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 483169b51..ee078e6e9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -119,7 +119,8 @@ We kindly request that you provide comprehensive documentation for all classes a To build the documentation locally, follow these steps: ```bash -sphinx-build docs docs/_build +cd docs +make html ``` More guidelines about building and hosting documentations locally can be found [here](https://github.com/camel-ai/camel/blob/master/docs/README.md). @@ -130,7 +131,18 @@ As of now, CAMEL is actively in development and not published to PyPI yet. CAMEL follows the [semver](https://semver.org/) versioning standard. As pre-1.0 software, even patch releases may contain [non-backwards-compatible changes](https://semver.org/#spec-item-4). Currently, the major version is 0, and the minor version is incremented. Releases are made once the maintainers feel that a significant body of changes has accumulated. -### Giving Credit 🎉 + +## License 📜 + +The source code of the CAMEL project is licensed under Apache 2.0. Your contributed code will be also licensed under Apache 2.0 by default. To add license to you code, you can manually copy-paste it from `license_template.txt` to the head of your files or run the `update_license.py` script to automate the process: + +```bash +python licenses/update_license.py . licenses/license_template.txt +``` + +This script will add licenses to all the `*.py` files or update the licenses if the existing licenses are not the same as `license_template.txt`. + +## Giving Credit 🎉 If your contribution has been included in a release, we'd love to give you credit on Twitter, but only if you're comfortable with it! diff --git a/camel/agents/__init__.py b/camel/agents/__init__.py index 8cb8eb8e3..619a94042 100644 --- a/camel/agents/__init__.py +++ b/camel/agents/__init__.py @@ -11,15 +11,23 @@ # 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 BaseAgent from .chat_agent import ChatAgent from .task_agent import TaskPlannerAgent, TaskSpecifyAgent from .critic_agent import CriticAgent +from .tool_agents.base import BaseToolAgent +from .tool_agents.hugging_face_tool_agent import HuggingFaceToolAgent +from .embodied_agent import EmbodiedAgent from .role_playing import RolePlaying __all__ = [ + 'BaseAgent', 'ChatAgent', 'TaskSpecifyAgent', 'TaskPlannerAgent', 'CriticAgent', + 'BaseToolAgent', + 'HuggingFaceToolAgent', + 'EmbodiedAgent', 'RolePlaying', ] diff --git a/camel/agents/base.py b/camel/agents/base.py new file mode 100644 index 000000000..5f46beb19 --- /dev/null +++ b/camel/agents/base.py @@ -0,0 +1,28 @@ +# =========== 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 + + +class BaseAgent(ABC): + r"""An abstract base class for all CAMEL agents.""" + + @abstractmethod + def reset(self) -> None: + r"""Resets the agent to its initial state.""" + pass + + @abstractmethod + def step(self) -> None: + r"""Performs a single step of the agent.""" + pass diff --git a/camel/agents/chat_agent.py b/camel/agents/chat_agent.py index b579624d3..e1b09adca 100644 --- a/camel/agents/chat_agent.py +++ b/camel/agents/chat_agent.py @@ -16,13 +16,18 @@ import openai from tenacity import retry, stop_after_attempt, wait_exponential +from camel.agents import BaseAgent from camel.configs import ChatGPTConfig from camel.messages import ChatMessage, MessageType, SystemMessage from camel.typing import ModelType -from camel.utils import get_model_token_limit, num_tokens_from_messages +from camel.utils import ( + get_model_token_limit, + num_tokens_from_messages, + openai_api_key_required, +) -class ChatAgent: +class ChatAgent(BaseAgent): r"""Class for managing conversations of CAMEL Chat Agents. Args: @@ -40,7 +45,7 @@ def __init__( self, system_message: SystemMessage, model: ModelType = ModelType.GPT_3_5_TURBO, - model_config: Any = None, + model_config: Optional[Any] = None, message_window_size: Optional[int] = None, ) -> None: @@ -115,6 +120,7 @@ def update_messages(self, message: ChatMessage) -> List[MessageType]: return self.stored_messages @retry(wait=wait_exponential(min=5, max=60), stop=stop_after_attempt(5)) + @openai_api_key_required def step( self, input_message: ChatMessage, diff --git a/camel/agents/critic_agent.py b/camel/agents/critic_agent.py index 2c1f5b34e..9300d3676 100644 --- a/camel/agents/critic_agent.py +++ b/camel/agents/critic_agent.py @@ -40,26 +40,26 @@ class CriticAgent(ChatAgent): retry_attempts (int, optional): The number of retry attempts if the critic fails to return a valid option. (default: :obj:`2`) verbose (bool, optional): Whether to print the critic's messages. - menu_color (Any): The color of the menu options displayed to the user. - (default: :obj:`Fore.MAGENTA`) + logger_color (Any): The color of the menu options displayed to the + user. (default: :obj:`Fore.MAGENTA`) """ def __init__( self, system_message: SystemMessage, model: ModelType = ModelType.GPT_3_5_TURBO, - model_config: Any = None, + model_config: Optional[Any] = None, message_window_size: int = 6, retry_attempts: int = 2, verbose: bool = False, - menu_color: Any = Fore.MAGENTA, + logger_color: Any = Fore.MAGENTA, ) -> None: super().__init__(system_message, model, model_config, message_window_size) self.options_dict: Dict[str, str] = dict() self.retry_attempts = retry_attempts self.verbose = verbose - self.menu_color = menu_color + self.logger_color = logger_color def flatten_options(self, messages: List[ChatMessage]) -> str: r"""Flattens the options to the critic. @@ -107,7 +107,7 @@ def get_option(self, input_message: ChatMessage) -> str: critic_msg = critic_msgs[0] self.update_messages(critic_msg) if self.verbose: - print_text_animated(self.menu_color + "\n> Critic response: " + print_text_animated(self.logger_color + "\n> Critic response: " f"\x1b[3m{critic_msg.content}\x1b[0m\n") choice = self.parse_critic(critic_msg) @@ -163,7 +163,7 @@ def step(self, messages: List[ChatMessage]) -> ChatMessage: flatten_options = self.flatten_options(messages) if self.verbose: - print_text_animated(self.menu_color + + print_text_animated(self.logger_color + f"\x1b[3m{flatten_options}\x1b[0m\n") input_msg = copy.deepcopy(meta_chat_message) input_msg.content = flatten_options diff --git a/camel/agents/embodied_agent.py b/camel/agents/embodied_agent.py new file mode 100644 index 000000000..280501f4c --- /dev/null +++ b/camel/agents/embodied_agent.py @@ -0,0 +1,188 @@ +# =========== 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 typing import Any, Dict, List, Optional, Tuple + +from colorama import Fore + +from camel.agents import BaseToolAgent, ChatAgent, HuggingFaceToolAgent +from camel.messages import ChatMessage, SystemMessage +from camel.typing import ModelType +from camel.utils import print_text_animated + + +class EmbodiedAgent(ChatAgent): + r"""Class for managing conversations of CAMEL Embodied Agents. + + Args: + system_message (SystemMessage): The system message for the chat agent. + model (ModelType, optional): The LLM model to use for generating + responses. (default :obj:`ModelType.GPT_4`) + model_config (Any, optional): Configuration options for the LLM model. + (default: :obj:`None`) + 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`) + 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`) + """ + + def __init__( + self, + system_message: SystemMessage, + model: ModelType = ModelType.GPT_4, + model_config: Optional[Any] = None, + message_window_size: Optional[int] = None, + action_space: Optional[List[BaseToolAgent]] = None, + verbose: bool = False, + logger_color: Any = Fore.MAGENTA, + ) -> None: + default_action_space = [ + HuggingFaceToolAgent('hugging_face_tool_agent', model=model.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.verbose = verbose + self.logger_color = logger_color + super().__init__( + system_message=system_message, + model=model, + model_config=model_config, + message_window_size=message_window_size, + ) + + def get_action_space_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 + ]) + + @staticmethod + def execute_code(code_string: str, global_vars: Dict = None) -> str: + r"""Executes the given code string. + + Args: + code_string (str): The code string to execute. + global_vars (Dict, optional): The global variables to use during + code execution. (default: :obj:`None`) + + Returns: + str: The execution results. + """ + # TODO: Refactor this with `CodePrompt.execute`. + try: + # Execute the code string + import io + import sys + output_str = io.StringIO() + sys.stdout = output_str + + global_vars = global_vars or globals() + local_vars = {} + exec( + code_string, + global_vars, + local_vars, + ) + sys.stdout = sys.__stdout__ + output_str.seek(0) + + # If there was no error, return the output + return (f"- Python standard output:\n{output_str.read()}\n" + f"- Local variables:\n{str(local_vars)}") + except Exception: + import traceback + traceback_str = traceback.format_exc() + sys.stdout = sys.__stdout__ + # If there was an error, return the error message + return f"Traceback:\n{traceback_str}" + + @staticmethod + def get_explanation_and_code( + text_prompt: str) -> Tuple[str, Optional[str]]: + r"""Extracts the explanation and code from the text prompt. + + Args: + text_prompt (str): The text prompt containing the explanation and + code. + + Returns: + Tuple[str, Optional[str]]: The extracted explanation and code. + """ + # TODO: Refactor this with `BaseMessage.extract_text_and_code_prompts`. + lines = text_prompt.split("\n") + idx = 0 + while idx < len(lines) and not lines[idx].lstrip().startswith("```"): + idx += 1 + explanation = "\n".join(lines[:idx]).strip() + if idx == len(lines): + return explanation, None + + idx += 1 + start_idx = idx + while not lines[idx].lstrip().startswith("```"): + idx += 1 + code = "\n".join(lines[start_idx:idx]).strip() + + return explanation, code + + def step( + self, + input_message: ChatMessage, + ) -> Tuple[ChatMessage, bool, Dict[str, Any]]: + r"""Performs a step in the conversation. + + Args: + input_message (ChatMessage): The input message. + + Returns: + Tuple[ChatMessage, bool, Dict[str, Any]]: A tuple + containing the output messages, termination status, and + additional information. + """ + output_messages, terminated, info = super().step(input_message) + + if output_messages is None: + raise RuntimeError("Got None output messages.") + if terminated: + raise RuntimeError(f"{self.__class__.__name__} step failed.") + + # NOTE: Only single output messages are supported + output_message = output_messages[0] + explanation, code = self.get_explanation_and_code( + output_message.content) + if self.verbose: + print_text_animated(self.logger_color + + f"> Explanation:\n{explanation}") + print_text_animated(self.logger_color + f"> Code:\n{code}") + + if code is not None: + global_vars = {action.name: action for action in self.action_space} + executed_results = self.execute_code(code, global_vars) + output_message.content += ( + f"\n> Executed Results:\n{executed_results}") + + # TODO: Handle errors + input_message.content += ( + f"\n> Embodied Actions:\n{output_message.content}") + return input_message, terminated, info diff --git a/camel/agents/role_playing.py b/camel/agents/role_playing.py index 3d375d09d..f583eb7c2 100644 --- a/camel/agents/role_playing.py +++ b/camel/agents/role_playing.py @@ -201,7 +201,7 @@ def init_chat(self) -> Tuple[AssistantChatMessage, List[ChatMessage]]: assistant_msg = AssistantChatMessage( role_name=self.assistant_sys_msg.role_name, content=(f"{self.user_sys_msg.content}. " - "Now start to give me introductions one by one. " + "Now start to give me instructions one by one. " "Only reply with Instruction and Input.")) user_msg = UserChatMessage(role_name=self.user_sys_msg.role_name, diff --git a/camel/agents/task_agent.py b/camel/agents/task_agent.py index 3f7134042..f08ffae85 100644 --- a/camel/agents/task_agent.py +++ b/camel/agents/task_agent.py @@ -46,7 +46,7 @@ def __init__( self, model: ModelType = ModelType.GPT_3_5_TURBO, task_type: TaskType = TaskType.AI_SOCIETY, - model_config: Any = None, + model_config: Optional[Any] = None, task_specify_prompt: Optional[Union[str, TextPrompt]] = None, word_limit: int = DEFAULT_WORD_LIMIT, ) -> None: diff --git a/camel/agents/tool_agents/__init__.py b/camel/agents/tool_agents/__init__.py new file mode 100644 index 000000000..e47fcf82b --- /dev/null +++ b/camel/agents/tool_agents/__init__.py @@ -0,0 +1,20 @@ +# =========== 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 BaseToolAgent +from .hugging_face_tool_agent import HuggingFaceToolAgent + +__all__ = [ + 'BaseToolAgent', + 'HuggingFaceToolAgent', +] diff --git a/setup.py b/camel/agents/tool_agents/base.py similarity index 58% rename from setup.py rename to camel/agents/tool_agents/base.py index ac5a6a106..a06c72e42 100644 --- a/setup.py +++ b/camel/agents/tool_agents/base.py @@ -11,37 +11,22 @@ # See the License for the specific language governing permissions and # limitations under the License. # =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== -from setuptools import find_packages, setup +from camel.agents import BaseAgent -__version__ = '0.1.0' -install_requires = [ - 'numpy', - 'openai', - 'tenacity', - 'tiktoken', - 'colorama', -] +class BaseToolAgent(BaseAgent): + r"""Creates a :obj:`BaseToolAgent` object with the specified name and + description. -test_requires = [ - 'pytest', - 'pytest-cov', -] + Args: + name (str): The name of the tool agent. + description (str): The description of the tool agent. + """ -dev_requires = [ - 'pre-commit', - 'yapf', - 'isort', - 'flake8', -] + def __init__(self, name: str, description: str) -> None: -setup( - name='camel', - version=__version__, - install_requires=install_requires, - extras_require={ - 'test': test_requires, - 'dev': dev_requires, - }, - packages=find_packages(), -) + self.name = name + self.description = description + + def __str__(self) -> str: + return f"{self.name}: {self.description}" diff --git a/camel/agents/tool_agents/hugging_face_tool_agent.py b/camel/agents/tool_agents/hugging_face_tool_agent.py new file mode 100644 index 000000000..0bf4b7b71 --- /dev/null +++ b/camel/agents/tool_agents/hugging_face_tool_agent.py @@ -0,0 +1,188 @@ +# =========== 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 typing import Any, Optional + +from camel.agents.tool_agents import BaseToolAgent + + +# flake8: noqa :E501 +class HuggingFaceToolAgent(BaseToolAgent): + r"""Tool agent for calling HuggingFace models. This agent is a wrapper + around agents from the `transformers` library. For more information + about the available models, please see the `transformers` documentation + at https://huggingface.co/docs/transformers/transformers_agents. + + Args: + name (str): The name of the agent. + *args (Any): Additional positional arguments to pass to the underlying + Agent class. + remote (bool, optional): Flag indicating whether to run the agent + remotely. (default: :obj:`True`) + **kwargs (Any): Additional keyword arguments to pass to the underlying + Agent class. + """ + + def __init__( + self, + name: str, + *args: Any, + remote: bool = True, + **kwargs: Any, + ) -> None: + try: + # TODO: Support other tool agents + from transformers.tools import OpenAiAgent + except ImportError: + raise ValueError( + "Could not import transformers tool agents. " + "Please setup the environment with " + "pip install huggingface_hub==0.14.1 transformers==4.29.0 diffusers accelerate datasets torch soundfile sentencepiece opencv-python" + ) + self.agent = OpenAiAgent(*args, **kwargs) + self.name = name + self.remote = remote + self.description = f"""The `{self.name}` is a tool agent that can perform a variety of tasks including: +- Document question answering: given a document (such as a PDF) in image format, answer a question on this document +- Text question answering: given a long text and a question, answer the question in the text +- Unconditional image captioning: Caption the image! +- Image question answering: given an image, answer a question on this image +- Image segmentation: given an image and a prompt, output the segmentation mask of that prompt +- Speech to text: given an audio recording of a person talking, transcribe the speech into text +- Text to speech: convert text to speech +- Zero-shot text classification: given a text and a list of labels, identify to which label the text corresponds the most +- Text summarization: summarize a long text in one or a few sentences +- Translation: translate the text into a given language +- Text downloading: to download a text from a web URL +- Text to image: generate an image according to a prompt, leveraging stable diffusion +- Image transformation: modify an image given an initial image and a prompt, leveraging instruct pix2pix stable diffusion +- Text to video: generate a small video according to a prompt + +Here are some python code examples of what you can do with this agent: + +Single execution (step) mode, the single execution method is when using the step() method of the agent: +``` +# Text to image +rivers_and_lakes_image = {self.name}.step("Draw me a picture of rivers and lakes.") +rivers_and_lakes_image.save("./rivers_and_lakes_image.png") + +# Text to image -> Image transformation +sea_add_island_image = {self.name}.step("Draw me a picture of the sea then transform the picture to add an island") +sea_add_island_image.save("./sea_add_island_image.png") + +# If you'd like to keep a state across executions or to pass non-text objects to the agent, +# you can do so by specifying variables that you would like the agent to use. For example, +# you could generate the first image of rivers and lakes, and ask the model to update that picture to add an island by doing the following: +picture = {self.name}.step("Generate a picture of rivers and lakes.") +picture.save("./picture.png") +updated_picture = {self.name}.step("Transform the image in `picture` to add an island to it.", picture=picture) +updated_picture.save("./updated_picture.png") + +capybara_sea_image = {self.name}.step("Draw me a picture of the `prompt`", prompt="a capybara swimming in the sea") +capybara_sea_image.save("./capybara_sea_image.png") + +# Document question answering +answer = {self.name}.step( + "In the following `document`, where will the TRRF Scientific Advisory Council Meeting take place?", + document=document, +) +print(answer) + + +# Text to image +boat_image = {self.name}.step("Generate an image of a boat in the water") +boat_image.save("./boat_image.png") + +# Unconditional image captioning +boat_image_caption = {self.name}.step("Can you caption the `boat_image`?", boat_image=boat_image) +print(boat_image_caption) + +# Text to image -> Unconditional image captioning -> Text to speech +boat_audio = {self.name}.step("Can you generate an image of a boat? Please read out loud the contents of the image afterwards") + +# Text downloading +document = {self.name}.step("Download the text from http://hf.co") +print(document) + +# Text summarization +summary = {self.name}.step("Summarize the following text: `document`", document=document) +print(summary) + +# Text downloading -> Text summarization -> Text to speech +audio = {self.name}.step("Read out loud the summary of http://hf.co") +``` + +Chat-based execution (chat), the agent also has a chat-based approach, using the chat() method: +``` +# Clean the chat history +{self.name}.reset() + +# Text to image +capybara_image = {self.name}.chat("Show me an an image of a capybara") +capybara_image.save("./capybara_image.png") + +# Image transformation +transformed_capybara_image = {self.name}.chat("Transform the image so that it snows") +transformed_capybara_image.save("./transformed_capybara_image.png") + +# Image segmentation +segmented_transformed_capybara_image = {self.name}.chat("Show me a mask of the snowy capybaras") +segmented_transformed_capybara_image.save("./segmented_transformed_capybara_image.png") +``` +""" + + def reset(self) -> None: + r"""Resets the chat history of the agent.""" + self.agent.prepare_for_new_chat() + + def step( + self, + *args: Any, + remote: Optional[bool] = None, + **kwargs: Any, + ) -> Any: + r"""Runs the agent in single execution mode. + + Args: + *args (Any): Positional arguments to pass to the agent. + remote (bool, optional): Flag indicating whether to run the agent + remotely. Overrides the default setting. (default: :obj:`None`) + **kwargs (Any): Keyword arguments to pass to the agent. + + Returns: + str: The response from the agent. + """ + if remote is None: + remote = self.remote + return self.agent.run(*args, remote=remote, **kwargs) + + def chat( + self, + *args: Any, + remote: Optional[bool] = None, + **kwargs: Any, + ) -> Any: + r"""Runs the agent in a chat conversation mode. + + Args: + *args (Any): Positional arguments to pass to the agent. + remote (bool, optional): Flag indicating whether to run the agent + remotely. Overrides the default setting. (default: :obj:`None`) + **kwargs (Any): Keyword arguments to pass to the agent. + + Returns: + str: The response from the agent. + """ + if remote is None: + remote = self.remote + return self.agent.chat(*args, remote=remote, **kwargs) diff --git a/camel/generators.py b/camel/generators.py index 885eeda6c..0c6b64e88 100644 --- a/camel/generators.py +++ b/camel/generators.py @@ -55,15 +55,23 @@ def __init__( task_type, RoleType.CRITIC, ) + embodiment_prompt_template = PromptTemplateGenerator( + ).get_system_prompt( + task_type, + RoleType.EMBODIMENT, + ) self.sys_prompts: Dict[RoleType, str] = dict() self.sys_prompts[RoleType.ASSISTANT] = assistant_prompt_template self.sys_prompts[RoleType.USER] = user_prompt_template self.sys_prompts[RoleType.CRITIC] = critic_prompt_template + self.sys_prompts[RoleType.EMBODIMENT] = embodiment_prompt_template - self.sys_msg_meta_dict_keys = (assistant_prompt_template.key_words - | user_prompt_template.key_words - | critic_prompt_template.key_words) + self.sys_msg_meta_dict_keys = ( + assistant_prompt_template.key_words + | user_prompt_template.key_words + | critic_prompt_template.key_words + | embodiment_prompt_template.key_words) if RoleType.DEFAULT not in self.sys_prompts: self.sys_prompts[RoleType.DEFAULT] = "You are a helpful assistant." diff --git a/camel/human.py b/camel/human.py index 20fadb88a..8e6faeb8f 100644 --- a/camel/human.py +++ b/camel/human.py @@ -25,12 +25,13 @@ class Human: Args: name (str): The name of the human user. (default: :obj:`"Kill Switch Engineer"`). - menu_color (Any): The color of the menu options displayed to the user. - (default: :obj:`Fore.MAGENTA`) + logger_color (Any): The color of the menu options displayed to the + user. (default: :obj:`Fore.MAGENTA`) Attributes: name (str): The name of the human user. - menu_color (Any): The color of the menu options displayed to the user. + logger_color (Any): The color of the menu options displayed to the + user. input_button (str): The text displayed for the input button. kill_button (str): The text displayed for the kill button. options_dict (Dict[str, str]): A dictionary containing the options @@ -38,9 +39,9 @@ class Human: """ def __init__(self, name: str = "Kill Switch Engineer", - menu_color: Any = Fore.MAGENTA) -> None: + logger_color: Any = Fore.MAGENTA) -> None: self.name = name - self.menu_color = menu_color + self.logger_color = logger_color self.input_button = f"Input by {self.name}." self.kill_button = "Stop!!!" self.options_dict: Dict[str, str] = dict() @@ -58,12 +59,12 @@ def display_options(self, messages: List[ChatMessage]) -> None: options.append(self.input_button) options.append(self.kill_button) print_text_animated( - self.menu_color + "\n> Proposals from " + self.logger_color + "\n> Proposals from " f"{messages[0].role_name} ({messages[0].role_type}). " "Please choose an option:\n") for index, option in enumerate(options): print_text_animated( - self.menu_color + + self.logger_color + f"\x1b[3mOption {index + 1}:\n{option}\x1b[0m\n") self.options_dict[str(index + 1)] = option @@ -75,12 +76,12 @@ def get_input(self) -> str: """ while True: human_input = input( - self.menu_color + + self.logger_color + f"Please enter your choice ([1-{len(self.options_dict)}]): ") print("\n") if human_input in self.options_dict: break - print_text_animated(self.menu_color + + print_text_animated(self.logger_color + "\n> Invalid choice. Please try again.\n") return human_input @@ -97,11 +98,11 @@ def parse_input(self, human_input: str, ChatMessage: A `ChatMessage` object. """ if self.options_dict[human_input] == self.input_button: - meta_chat_message.content = input(self.menu_color + + meta_chat_message.content = input(self.logger_color + "Please enter your message: ") return meta_chat_message elif self.options_dict[human_input] == self.kill_button: - exit(self.menu_color + f"Killed by {self.name}.") + exit(self.logger_color + f"Killed by {self.name}.") else: meta_chat_message.content = self.options_dict[human_input] return meta_chat_message diff --git a/camel/prompts/ai_society.py b/camel/prompts/ai_society.py index 54ae703dc..c9e253f9b 100644 --- a/camel/prompts/ai_society.py +++ b/camel/prompts/ai_society.py @@ -17,7 +17,7 @@ from camel.typing import RoleType -# flake8: noqa +# flake8: noqa :E501 class AISocietyPromptTemplateDict(TextPromptDict): r"""A dictionary containing :obj:`TextPrompt` used in the `AI Society` task. diff --git a/camel/prompts/base.py b/camel/prompts/base.py index 8f0faa5d8..a2b1aabcf 100644 --- a/camel/prompts/base.py +++ b/camel/prompts/base.py @@ -14,6 +14,7 @@ import inspect from typing import Any, Callable, Dict, Set, TypeVar, Union +from camel.typing import RoleType from camel.utils import get_prompt_template_key_words T = TypeVar('T') @@ -116,7 +117,29 @@ def format(self, *args: Any, **kwargs: Any) -> 'TextPrompt': return TextPrompt(super().format(*args, **default_kwargs)) +# flake8: noqa :E501 class TextPromptDict(Dict[Any, TextPrompt]): r"""A dictionary class that maps from key to :obj:`TextPrompt` object. """ - pass + EMBODIMENT_PROMPT = TextPrompt( + """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 should perform actions based on the descriptions of the functions. + +Here is your action space: +{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. +You may print intermediate results if necessary.""") + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.update({RoleType.EMBODIMENT: self.EMBODIMENT_PROMPT}) diff --git a/camel/prompts/code.py b/camel/prompts/code.py index 8b368707a..35c1563af 100644 --- a/camel/prompts/code.py +++ b/camel/prompts/code.py @@ -17,7 +17,7 @@ from camel.typing import RoleType -# flake8: noqa +# flake8: noqa :E501 class CodePromptTemplateDict(TextPromptDict): r"""A dictionary containing :obj:`TextPrompt` used in the `Code` task. diff --git a/camel/prompts/evaluation.py b/camel/prompts/evaluation.py index 396fdeed7..478063ca3 100644 --- a/camel/prompts/evaluation.py +++ b/camel/prompts/evaluation.py @@ -14,18 +14,16 @@ from typing import Any from camel.prompts import TextPrompt, TextPromptDict -from camel.typing import RoleType -# flake8: noqa class EvaluationPromptTemplateDict(TextPromptDict): r"""A dictionary containing :obj:`TextPrompt` used in the `Evaluation` task. Attributes: - GENERATE_QUESTIONS (TextPrompt): A prompt to generate a set of questions - to be used for evaluating emergence of knowledge based on a particular - field of knowledge. + GENERATE_QUESTIONS (TextPrompt): A prompt to generate a set of + questions to be used for evaluating emergence of knowledge based + on a particular field of knowledge. """ GENERATE_QUESTIONS = TextPrompt( diff --git a/camel/prompts/misalignment.py b/camel/prompts/misalignment.py index 7a32fd2c1..24ae65cb7 100644 --- a/camel/prompts/misalignment.py +++ b/camel/prompts/misalignment.py @@ -21,7 +21,7 @@ from camel.typing import RoleType -# flake8: noqa +# flake8: noqa :E501 class MisalignmentPromptTemplateDict(TextPromptDict): r"""A dictionary containing :obj:`TextPrompt` used in the `Misalignment` task. diff --git a/camel/prompts/translation.py b/camel/prompts/translation.py index 8cc9f5f42..40993a1dd 100644 --- a/camel/prompts/translation.py +++ b/camel/prompts/translation.py @@ -17,7 +17,7 @@ from camel.typing import RoleType -# flake8: noqa +# flake8: noqa :E501 class TranslationPromptTemplateDict(TextPromptDict): r"""A dictionary containing :obj:`TextPrompt` used in the `Translation` task. diff --git a/camel/typing.py b/camel/typing.py index 2b78bfd48..9e0f163c1 100644 --- a/camel/typing.py +++ b/camel/typing.py @@ -18,6 +18,7 @@ class RoleType(Enum): ASSISTANT = "assistant" USER = "user" CRITIC = "critic" + EMBODIMENT = "embodiment" DEFAULT = "default" diff --git a/camel/utils.py b/camel/utils.py index dd74007bc..d66c84a66 100644 --- a/camel/utils.py +++ b/camel/utils.py @@ -15,13 +15,15 @@ import re import time from functools import wraps -from typing import Any, Callable, List, Optional, Set +from typing import Any, Callable, List, Optional, Set, TypeVar import tiktoken from camel.messages import OpenAIMessage from camel.typing import ModelType +F = TypeVar('F', bound=Callable[..., Any]) + def count_tokens_openai_chat_models( messages: List[OpenAIMessage], @@ -109,7 +111,7 @@ def get_model_token_limit(model: ModelType) -> int: return 32768 -def openai_api_key_required(func: Callable[..., Any]) -> Callable[..., Any]: +def openai_api_key_required(func: F) -> F: r"""Decorator that checks if the OpenAI API key is available in the environment variables. @@ -125,7 +127,7 @@ def openai_api_key_required(func: Callable[..., Any]) -> Callable[..., Any]: """ @wraps(func) - def wrapper(*args, **kwargs) -> Callable[..., Any]: + def wrapper(*args, **kwargs): if 'OPENAI_API_KEY' in os.environ: return func(*args, **kwargs) else: diff --git a/docs/camel.agents.tool_agents.rst b/docs/camel.agents.tool_agents.rst new file mode 100644 index 000000000..ec85836f0 --- /dev/null +++ b/docs/camel.agents.tool_agents.rst @@ -0,0 +1,29 @@ +camel.agents.tool\_agents package +================================= + +Submodules +---------- + +camel.agents.tool\_agents.base module +------------------------------------- + +.. automodule:: camel.agents.tool_agents.base + :members: + :undoc-members: + :show-inheritance: + +camel.agents.tool\_agents.hugging\_face\_tool\_agent module +----------------------------------------------------------- + +.. automodule:: camel.agents.tool_agents.hugging_face_tool_agent + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: camel.agents.tool_agents + :members: + :undoc-members: + :show-inheritance: diff --git a/examples/embodiment/hugging_face_tool.py b/examples/embodiment/hugging_face_tool.py new file mode 100644 index 000000000..60d0e4da7 --- /dev/null +++ b/examples/embodiment/hugging_face_tool.py @@ -0,0 +1,50 @@ +# =========== 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, HuggingFaceToolAgent +from camel.generators import SystemMessageGenerator +from camel.messages import UserChatMessage +from camel.typing import ModelType, RoleType + + +def main(): + # Create an embodied agent + role_name = "Artist" + meta_dict = dict(role=role_name, task="Drawing") + sys_msg = SystemMessageGenerator().from_dict( + meta_dict=meta_dict, + role_tuple=(f"{role_name}'s Embodiment", RoleType.EMBODIMENT)) + action_space = [ + HuggingFaceToolAgent( + 'hugging_face_tool_agent', + model=ModelType.GPT_4.value, + remote=True, + ) + ] + embodied_agent = EmbodiedAgent( + sys_msg, + verbose=True, + action_space=action_space, + ) + user_msg = UserChatMessage( + role_name=role_name, + content=("Draw all the Camelidae species, " + "caption the image content, " + "save the images by species name."), + ) + output_message, _, _ = embodied_agent.step(user_msg) + print(output_message.content) + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml index 7c99c1a85..36ca5d3c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,11 @@ [build-system] -requires = ["setuptools", "wheel"] -build-backend = "setuptools.build_meta" +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" -[project] +[tool.poetry] name="camel" version="0.1.0" -authors=[ - {name="CAMEL TEAM", email="camel.ai.team@gmail.com"}, -] +authors=["CAMEL-AI.org"] description="Communicative Agents for AI Society Study" readme="README.md" requires-python=">=3.7" @@ -21,24 +19,33 @@ keywords=[ "natural-language-processing", "large-language-models", ] -classifiers=[ - "Development Status :: 2 - Pre-Alpha", - "License :: OSI Approved :: Apache License 2.0", - "Programming Language :: Python", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3 :: Only", -] - -dynamic=["dependencies", "optional-dependencies"] - -[project.urls] +license="Apache License 2.0" homepage="https://www.camel-ai.org/" -repository="https://github.com/lightaime/camel" -changelog="https://github.com/lightaime/camel/blob/master/CHANGELOG.md" +repository="https://github.com/camel-ai/camel" +documentation="https://docs.camel-ai.org" + +[tool.poetry.dependencies] +python = "^3.7" +numpy = "^1" +openai = "^0" +tenacity = "^8" +tiktoken = "^0" +colorama = "^0" +yapf = "^0" +isort = "^5" +flake8 = "^6" +pre-commit = "^3" +pytest = "^7" +pytest-cov = "^4" +bs4 = {version = "^0", optional = true} +transformers = {version = "^4", optional = true} +diffusers = {version = "^0", optional = true} +accelerate = {version = "^0", optional = true} +datasets = {version = "^2", optional = true} +torch = {version = "^1", optional = true} +soundfile = {version = "^0", optional = true} +sentencepiece = {version = "^0", optional = true} +opencv-python = {version = "^4", optional = true} [tool.yapf] based_on_style = "pep8" @@ -50,6 +57,8 @@ include_trailing_comma = true skip = [".gitingore", "__init__.py"] [tool.pytest.ini_options] -pythonpath = [ - ".", +pythonpath = ["."] +markers = [ + "full_test_only: mark a test to run only in full test mode", + "slow: mark a test as slow", ] diff --git a/requirements.txt b/requirements.txt index ee2bf76d1..fb22ba519 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,8 +6,6 @@ tiktoken colorama # Packages for development -setuptools -build yapf isort flake8 @@ -17,5 +15,13 @@ pre-commit pytest pytest-cov -# Packages for tools -google-search-results==2.4.2 \ No newline at end of file +# Packages for huggingface tools +bs4 +transformers["agent"] +diffusers==0.16.1 +accelerate==0.19.0 +datasets==2.12.0 +torch==2.0.0 +soundfile==0.12.1 +sentencepiece==0.1.97 +opencv-python==4.7.0.72 \ No newline at end of file diff --git a/test/agents/test_agent_base.py b/test/agents/test_agent_base.py new file mode 100644 index 000000000..5e1df5b2e --- /dev/null +++ b/test/agents/test_agent_base.py @@ -0,0 +1,46 @@ +# =========== 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 pytest + +from camel.agents import BaseAgent + + +class DummyAgent(BaseAgent): + + def __init__(self): + self.step_count = 0 + + def reset(self): + self.step_count = 0 + + def step(self): + self.step_count += 1 + + +def test_base_agent(): + with pytest.raises(TypeError): + BaseAgent() + + +def test_dummy_agent(): + agent = DummyAgent() + assert agent.step_count == 0 + agent.step() + assert agent.step_count == 1 + agent.reset() + assert agent.step_count == 0 + agent.step() + assert agent.step_count == 1 + agent.step() + assert agent.step_count == 2 diff --git a/test/agents/test_chat_agent.py b/test/agents/test_chat_agent.py index edbd67a2c..07b11cfad 100644 --- a/test/agents/test_chat_agent.py +++ b/test/agents/test_chat_agent.py @@ -23,6 +23,7 @@ from camel.utils import get_model_token_limit, openai_api_key_required +@pytest.mark.slow @openai_api_key_required @pytest.mark.parametrize('model', [ModelType.GPT_3_5_TURBO, ModelType.GPT_4]) def test_chat_agent(model): @@ -61,6 +62,7 @@ def test_chat_agent(model): assert info['termination_reasons'][0] == "max_tokens_exceeded" +@pytest.mark.slow @openai_api_key_required @pytest.mark.parametrize('n', [1, 2, 3]) def test_chat_agent_multiple_return_messages(n): diff --git a/test/agents/test_critic_agent.py b/test/agents/test_critic_agent.py index 6602e3f6d..0a00ff1b8 100644 --- a/test/agents/test_critic_agent.py +++ b/test/agents/test_critic_agent.py @@ -58,6 +58,7 @@ def test_flatten_options(critic_agent: CriticAgent): assert critic_agent.flatten_options(messages) == expected_output +@pytest.mark.slow @openai_api_key_required def test_get_option(critic_agent: CriticAgent): messages = [ @@ -101,6 +102,7 @@ def test_parse_critic(critic_agent: CriticAgent): assert critic_agent.parse_critic(critic_msg) == expected_output +@pytest.mark.slow @openai_api_key_required def test_step(critic_agent: CriticAgent): messages = [ diff --git a/test/agents/test_embodied_agent.py b/test/agents/test_embodied_agent.py new file mode 100644 index 000000000..18e6d2309 --- /dev/null +++ b/test/agents/test_embodied_agent.py @@ -0,0 +1,72 @@ +# =========== 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 pytest + +from camel.agents import EmbodiedAgent, HuggingFaceToolAgent +from camel.generators import SystemMessageGenerator +from camel.messages import ChatMessage, UserChatMessage +from camel.typing import RoleType +from camel.utils import openai_api_key_required + + +@openai_api_key_required +def test_get_action_space_prompt(): + role_name = "Artist" + meta_dict = dict(role=role_name, task="Drawing") + sys_msg = SystemMessageGenerator().from_dict( + 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) + + +def test_execute_code(): + code_string = "print('Hello, world!')" + expected_output = ( + "- Python standard output:\nHello, world!\n\n- Local variables:\n{}") + assert EmbodiedAgent.execute_code(code_string) == expected_output + + +def test_get_explanation_and_code(): + text_prompt = ( + "This is an explanation.\n\n```python\nprint('Hello, world!')\n```") + expected_explanation = "This is an explanation." + expected_code = "print('Hello, world!')" + assert EmbodiedAgent.get_explanation_and_code(text_prompt) == ( + expected_explanation, expected_code) + + +@pytest.mark.slow +@pytest.mark.full_test_only +@openai_api_key_required +def test_step(): + # Create an embodied agent + role_name = "Artist" + meta_dict = dict(role=role_name, task="Drawing") + sys_msg = SystemMessageGenerator().from_dict( + meta_dict=meta_dict, + role_tuple=(f"{role_name}'s Embodiment", RoleType.EMBODIMENT)) + embodied_agent = EmbodiedAgent(sys_msg, verbose=True) + print(embodied_agent.system_message) + user_msg = UserChatMessage( + role_name=role_name, + content="Draw all the Camelidae species.", + ) + output_message, terminated, info = embodied_agent.step(user_msg) + assert isinstance(output_message, ChatMessage) + assert not terminated + assert isinstance(info, dict) diff --git a/test/agents/test_role_playing.py b/test/agents/test_role_playing.py index b855ecbbb..1b36a1b9f 100644 --- a/test/agents/test_role_playing.py +++ b/test/agents/test_role_playing.py @@ -52,6 +52,7 @@ def test_role_playing_init(): assert role_playing.critic is None +@pytest.mark.slow @openai_api_key_required @pytest.mark.parametrize( "task_type, extend_sys_msg_meta_dicts, extend_task_specify_meta_dict", diff --git a/test/agents/test_task_agent.py b/test/agents/test_task_agent.py index d1dc554ce..bc5cdcc15 100644 --- a/test/agents/test_task_agent.py +++ b/test/agents/test_task_agent.py @@ -11,12 +11,15 @@ # 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.agents import TaskPlannerAgent, TaskSpecifyAgent from camel.configs import ChatGPTConfig from camel.typing import TaskType from camel.utils import openai_api_key_required +@pytest.mark.slow @openai_api_key_required def test_task_specify_ai_society_agent(): original_task_prompt = "Improving stage presence and performance skills" @@ -30,6 +33,7 @@ def test_task_specify_ai_society_agent(): print(f"Specified task prompt:\n{specified_task_prompt}\n") +@pytest.mark.slow @openai_api_key_required def test_task_specify_code_agent(): original_task_prompt = "Modeling molecular dynamics" @@ -45,6 +49,7 @@ def test_task_specify_code_agent(): print(f"Specified task prompt:\n{specified_task_prompt}\n") +@pytest.mark.slow @openai_api_key_required def test_task_planner_agent(): original_task_prompt = "Modeling molecular dynamics" diff --git a/test/agents/tool_agents/test_hugging_face_tool_agent.py b/test/agents/tool_agents/test_hugging_face_tool_agent.py new file mode 100644 index 000000000..8f9c1f6f3 --- /dev/null +++ b/test/agents/tool_agents/test_hugging_face_tool_agent.py @@ -0,0 +1,55 @@ +# =========== 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 pytest + +from camel.agents import HuggingFaceToolAgent +from camel.utils import openai_api_key_required + + +@pytest.mark.slow +@pytest.mark.full_test_only +@openai_api_key_required +def test_hugging_face_tool_agent_initialization(): + agent = HuggingFaceToolAgent("hugging_face_tool_agent") + assert agent.name == "hugging_face_tool_agent" + assert agent.remote is True + assert agent.description.startswith(f"The `{agent.name}` is a tool agent") + + +@pytest.mark.slow +@pytest.mark.full_test_only +@openai_api_key_required +def test_hugging_face_tool_agent_step(): + from PIL.PngImagePlugin import PngImageFile + agent = HuggingFaceToolAgent("hugging_face_tool_agent") + result = agent.step("Generate an image of a boat in the water") + assert isinstance(result, PngImageFile) + + +@pytest.mark.slow +@pytest.mark.full_test_only +@openai_api_key_required +def test_hugging_face_tool_agent_chat(): + from PIL.PngImagePlugin import PngImageFile + agent = HuggingFaceToolAgent("hugging_face_tool_agent") + result = agent.chat("Show me an image of a capybara") + assert isinstance(result, PngImageFile) + + +@pytest.mark.slow +@pytest.mark.full_test_only +@openai_api_key_required +def test_hugging_face_tool_agent_reset(): + agent = HuggingFaceToolAgent("hugging_face_tool_agent") + agent.reset() diff --git a/test/agents/tool_agents/test_tool_agent_base.py b/test/agents/tool_agents/test_tool_agent_base.py new file mode 100644 index 000000000..39ae590fd --- /dev/null +++ b/test/agents/tool_agents/test_tool_agent_base.py @@ -0,0 +1,29 @@ +# =========== 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 BaseToolAgent + + +class DummyToolAgent(BaseToolAgent): + + def reset(self): + pass + + def step(self): + pass + + +def test_tool_agent_initialization(): + tool_agent = DummyToolAgent("tool_agent", "description") + assert tool_agent.name == "tool_agent" + assert tool_agent.description == "description" diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 000000000..f8144927d --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,42 @@ +# =========== 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 typing import List + +import pytest +from _pytest.config import Config +from _pytest.nodes import Item + + +def pytest_addoption(parser: pytest.Parser) -> None: + parser.addoption("--full-test-mode", action="store_true", + help="Enable full test mode") + parser.addoption("--fast-test-mode", action="store_true", + help="Enable fast test mode") + + +def pytest_collection_modifyitems(config: Config, items: List[Item]) -> None: + # Skip full test only tests if not in full test mode + if not config.getoption("--full-test-mode"): + skip_full_test = pytest.mark.skip( + reason="Test runs only in full test mode") + for item in items: + if "full_test_only" in item.keywords: + item.add_marker(skip_full_test) + + # Skip slow tests if fast test mode is enabled + if config.getoption("--fast-test-mode"): + skip_slow = pytest.mark.skip(reason="Skipped for fast test mode") + for item in items: + if "slow" in item.keywords: + item.add_marker(skip_slow)