diff --git a/camel/model_backend.py b/camel/model_backend.py index 2c664ba33c..7ab2afd1a7 100644 --- a/camel/model_backend.py +++ b/camel/model_backend.py @@ -1,198 +1,201 @@ -# =========== 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 - -import openai -import tiktoken - -from camel.typing import ModelType -from chatdev.statistics import prompt_cost -from chatdev.utils import log_visualize - -try: - from openai.types.chat import ChatCompletion - - openai_new_api = True # new openai api version -except ImportError: - openai_new_api = False # old openai api version - -import os - -OPENAI_API_KEY = os.environ['OPENAI_API_KEY'] -if 'BASE_URL' in os.environ: - BASE_URL = os.environ['BASE_URL'] -else: - BASE_URL = None - - -class ModelBackend(ABC): - r"""Base class for different model backends. - May be OpenAI API, a local LLM, a stub for unit tests, etc.""" - - @abstractmethod - def run(self, *args, **kwargs): - r"""Runs the query to the backend model. - - Raises: - RuntimeError: if the return value from OpenAI API - is not a dict that is expected. - - Returns: - Dict[str, Any]: All backends must return a dict in OpenAI format. - """ - pass - - -class OpenAIModel(ModelBackend): - r"""OpenAI API in a unified ModelBackend interface.""" - - def __init__(self, model_type: ModelType, model_config_dict: Dict) -> None: - super().__init__() - self.model_type = model_type - self.model_config_dict = model_config_dict - - def run(self, *args, **kwargs): - string = "\n".join([message["content"] for message in kwargs["messages"]]) - encoding = tiktoken.encoding_for_model(self.model_type.value) - num_prompt_tokens = len(encoding.encode(string)) - gap_between_send_receive = 15 * len(kwargs["messages"]) - num_prompt_tokens += gap_between_send_receive - - if openai_new_api: - # Experimental, add base_url - if BASE_URL: - client = openai.OpenAI( - api_key=OPENAI_API_KEY, - base_url=BASE_URL, - ) - else: - client = openai.OpenAI( - api_key=OPENAI_API_KEY - ) - - num_max_token_map = { - "gpt-3.5-turbo": 4096, - "gpt-3.5-turbo-16k": 16384, - "gpt-3.5-turbo-0613": 4096, - "gpt-3.5-turbo-16k-0613": 16384, - "gpt-4": 8192, - "gpt-4-0613": 8192, - "gpt-4-32k": 32768, - "gpt-4-turbo": 100000, - } - num_max_token = num_max_token_map[self.model_type.value] - num_max_completion_tokens = num_max_token - num_prompt_tokens - self.model_config_dict['max_tokens'] = num_max_completion_tokens - - response = client.chat.completions.create(*args, **kwargs, model=self.model_type.value, - **self.model_config_dict) - - cost = prompt_cost( - self.model_type.value, - num_prompt_tokens=response.usage.prompt_tokens, - num_completion_tokens=response.usage.completion_tokens - ) - - log_visualize( - "**[OpenAI_Usage_Info Receive]**\nprompt_tokens: {}\ncompletion_tokens: {}\ntotal_tokens: {}\ncost: ${:.6f}\n".format( - response.usage.prompt_tokens, response.usage.completion_tokens, - response.usage.total_tokens, cost)) - if not isinstance(response, ChatCompletion): - raise RuntimeError("Unexpected return from OpenAI API") - return response - else: - num_max_token_map = { - "gpt-3.5-turbo": 4096, - "gpt-3.5-turbo-16k": 16384, - "gpt-3.5-turbo-0613": 4096, - "gpt-3.5-turbo-16k-0613": 16384, - "gpt-4": 8192, - "gpt-4-0613": 8192, - "gpt-4-32k": 32768, - "gpt-4-turbo": 100000, - } - num_max_token = num_max_token_map[self.model_type.value] - num_max_completion_tokens = num_max_token - num_prompt_tokens - self.model_config_dict['max_tokens'] = num_max_completion_tokens - - response = openai.ChatCompletion.create(*args, **kwargs, model=self.model_type.value, - **self.model_config_dict) - - cost = prompt_cost( - self.model_type.value, - num_prompt_tokens=response["usage"]["prompt_tokens"], - num_completion_tokens=response["usage"]["completion_tokens"] - ) - - log_visualize( - "**[OpenAI_Usage_Info Receive]**\nprompt_tokens: {}\ncompletion_tokens: {}\ntotal_tokens: {}\ncost: ${:.6f}\n".format( - response["usage"]["prompt_tokens"], response["usage"]["completion_tokens"], - response["usage"]["total_tokens"], cost)) - if not isinstance(response, Dict): - raise RuntimeError("Unexpected return from OpenAI API") - return response - - -class StubModel(ModelBackend): - r"""A dummy model used for unit tests.""" - - def __init__(self, *args, **kwargs) -> None: - super().__init__() - - def run(self, *args, **kwargs) -> Dict[str, Any]: - ARBITRARY_STRING = "Lorem Ipsum" - - return dict( - id="stub_model_id", - usage=dict(), - choices=[ - dict(finish_reason="stop", - message=dict(content=ARBITRARY_STRING, role="assistant")) - ], - ) - - -class ModelFactory: - r"""Factory of backend models. - - Raises: - ValueError: in case the provided model type is unknown. - """ - - @staticmethod - def create(model_type: ModelType, model_config_dict: Dict) -> ModelBackend: - default_model_type = ModelType.GPT_3_5_TURBO - - if model_type in { - ModelType.GPT_3_5_TURBO, - ModelType.GPT_3_5_TURBO_NEW, - ModelType.GPT_4, - ModelType.GPT_4_32k, - ModelType.GPT_4_TURBO, - ModelType.GPT_4_TURBO_V, - None - }: - model_class = OpenAIModel - elif model_type == ModelType.STUB: - model_class = StubModel - else: - raise ValueError("Unknown model") - - if model_type is None: - model_type = default_model_type - - # log_visualize("Model Type: {}".format(model_type)) - inst = model_class(model_type, model_config_dict) - return inst +# =========== 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 + +import openai +import tiktoken + +from camel.typing import ModelType +from chatdev.statistics import prompt_cost +from chatdev.utils import log_visualize + +try: + from openai.types.chat import ChatCompletion + + openai_new_api = True # new openai api version +except ImportError: + openai_new_api = False # old openai api version + +import os + +OPENAI_API_KEY = os.environ['OPENAI_API_KEY'] +if 'BASE_URL' in os.environ: + BASE_URL = os.environ['BASE_URL'] +else: + BASE_URL = None + + +class ModelBackend(ABC): + r"""Base class for different model backends. + May be OpenAI API, a local LLM, a stub for unit tests, etc.""" + + @abstractmethod + def run(self, *args, **kwargs): + r"""Runs the query to the backend model. + + Raises: + RuntimeError: if the return value from OpenAI API + is not a dict that is expected. + + Returns: + Dict[str, Any]: All backends must return a dict in OpenAI format. + """ + pass + + +class OpenAIModel(ModelBackend): + r"""OpenAI API in a unified ModelBackend interface.""" + + def __init__(self, model_type: ModelType, model_config_dict: Dict) -> None: + super().__init__() + self.model_type = model_type + self.model_config_dict = model_config_dict + + def run(self, *args, **kwargs): + string = "\n".join([message["content"] for message in kwargs["messages"]]) + encoding = tiktoken.encoding_for_model(self.model_type.value) + num_prompt_tokens = len(encoding.encode(string)) + gap_between_send_receive = 15 * len(kwargs["messages"]) + num_prompt_tokens += gap_between_send_receive + + if openai_new_api: + # Experimental, add base_url + if BASE_URL: + client = openai.OpenAI( + api_key=OPENAI_API_KEY, + base_url=BASE_URL, + ) + else: + client = openai.OpenAI( + api_key=OPENAI_API_KEY + ) + + num_max_token_map = { + "gpt-3.5-turbo": 4096, + "gpt-3.5-turbo-16k": 16384, + "gpt-3.5-turbo-0613": 4096, + "gpt-3.5-turbo-16k-0613": 16384, + "gpt-4": 8192, + "gpt-4-0613": 8192, + "gpt-4-32k": 32768, + "gpt-4-1106-preview": 4096, + "gpt-4-1106-vision-preview": 4096, + "gpt-4o": 4096, + } + num_max_token = num_max_token_map[self.model_type.value] + num_max_completion_tokens = num_max_token - num_prompt_tokens + self.model_config_dict['max_tokens'] = num_max_completion_tokens + + response = client.chat.completions.create(*args, **kwargs, model=self.model_type.value, + **self.model_config_dict) + + cost = prompt_cost( + self.model_type.value, + num_prompt_tokens=response.usage.prompt_tokens, + num_completion_tokens=response.usage.completion_tokens + ) + + log_visualize( + "**[OpenAI_Usage_Info Receive]**\nprompt_tokens: {}\ncompletion_tokens: {}\ntotal_tokens: {}\ncost: ${:.6f}\n".format( + response.usage.prompt_tokens, response.usage.completion_tokens, + response.usage.total_tokens, cost)) + if not isinstance(response, ChatCompletion): + raise RuntimeError("Unexpected return from OpenAI API") + return response + else: + num_max_token_map = { + "gpt-3.5-turbo": 4096, + "gpt-3.5-turbo-16k": 16384, + "gpt-3.5-turbo-0613": 4096, + "gpt-3.5-turbo-16k-0613": 16384, + "gpt-4": 8192, + "gpt-4-0613": 8192, + "gpt-4-32k": 32768, + "gpt-4o": 4096, + } + num_max_token = num_max_token_map[self.model_type.value] + num_max_completion_tokens = num_max_token - num_prompt_tokens + self.model_config_dict['max_tokens'] = num_max_completion_tokens + + response = openai.ChatCompletion.create(*args, **kwargs, model=self.model_type.value, + **self.model_config_dict) + + cost = prompt_cost( + self.model_type.value, + num_prompt_tokens=response["usage"]["prompt_tokens"], + num_completion_tokens=response["usage"]["completion_tokens"] + ) + + log_visualize( + "**[OpenAI_Usage_Info Receive]**\nprompt_tokens: {}\ncompletion_tokens: {}\ntotal_tokens: {}\ncost: ${:.6f}\n".format( + response["usage"]["prompt_tokens"], response["usage"]["completion_tokens"], + response["usage"]["total_tokens"], cost)) + if not isinstance(response, Dict): + raise RuntimeError("Unexpected return from OpenAI API") + return response + + +class StubModel(ModelBackend): + r"""A dummy model used for unit tests.""" + + def __init__(self, *args, **kwargs) -> None: + super().__init__() + + def run(self, *args, **kwargs) -> Dict[str, Any]: + ARBITRARY_STRING = "Lorem Ipsum" + + return dict( + id="stub_model_id", + usage=dict(), + choices=[ + dict(finish_reason="stop", + message=dict(content=ARBITRARY_STRING, role="assistant")) + ], + ) + + +class ModelFactory: + r"""Factory of backend models. + + Raises: + ValueError: in case the provided model type is unknown. + """ + + @staticmethod + def create(model_type: ModelType, model_config_dict: Dict) -> ModelBackend: + default_model_type = ModelType.GPT_3_5_TURBO + + if model_type in { + ModelType.GPT_3_5_TURBO, + ModelType.GPT_3_5_TURBO_NEW, + ModelType.GPT_4, + ModelType.GPT_4_32k, + ModelType.GPT_4_TURBO, + ModelType.GPT_4_TURBO_V, + ModelType.GPT_4o, + None + }: + model_class = OpenAIModel + elif model_type == ModelType.STUB: + model_class = StubModel + else: + raise ValueError("Unknown model") + + if model_type is None: + model_type = default_model_type + + # log_visualize("Model Type: {}".format(model_type)) + inst = model_class(model_type, model_config_dict) + return inst diff --git a/camel/typing.py b/camel/typing.py index fa0caa9fbc..ee4918efe9 100644 --- a/camel/typing.py +++ b/camel/typing.py @@ -1,85 +1,86 @@ -# =========== 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 enum import Enum - - -class TaskType(Enum): - AI_SOCIETY = "ai_society" - CODE = "code" - MISALIGNMENT = "misalignment" - TRANSLATION = "translation" - EVALUATION = "evaluation" - SOLUTION_EXTRACTION = "solution_extraction" - CHATDEV = "chat_dev" - DEFAULT = "default" - - -class RoleType(Enum): - ASSISTANT = "assistant" - USER = "user" - CRITIC = "critic" - EMBODIMENT = "embodiment" - DEFAULT = "default" - CHATDEV = "AgentTech" - CHATDEV_COUNSELOR = "counselor" - CHATDEV_CEO = "chief executive officer (CEO)" - CHATDEV_CHRO = "chief human resource officer (CHRO)" - CHATDEV_CPO = "chief product officer (CPO)" - CHATDEV_CTO = "chief technology officer (CTO)" - CHATDEV_PROGRAMMER = "programmer" - CHATDEV_REVIEWER = "code reviewer" - CHATDEV_TESTER = "software test engineer" - CHATDEV_CCO = "chief creative officer (CCO)" - - -class ModelType(Enum): - GPT_3_5_TURBO = "gpt-3.5-turbo-16k-0613" - GPT_3_5_TURBO_NEW = "gpt-3.5-turbo-16k" - GPT_4 = "gpt-4" - GPT_4_32k = "gpt-4-32k" - GPT_4_TURBO = "gpt-4-turbo" - GPT_4_TURBO_V = "gpt-4-turbo" - - STUB = "stub" - - @property - def value_for_tiktoken(self): - return self.value if self.name != "STUB" else "gpt-3.5-turbo-16k-0613" - - -class PhaseType(Enum): - REFLECTION = "reflection" - RECRUITING_CHRO = "recruiting CHRO" - RECRUITING_CPO = "recruiting CPO" - RECRUITING_CTO = "recruiting CTO" - DEMAND_ANALYSIS = "demand analysis" - CHOOSING_LANGUAGE = "choosing language" - RECRUITING_PROGRAMMER = "recruiting programmer" - RECRUITING_REVIEWER = "recruiting reviewer" - RECRUITING_TESTER = "recruiting software test engineer" - RECRUITING_CCO = "recruiting chief creative officer" - CODING = "coding" - CODING_COMPLETION = "coding completion" - CODING_AUTOMODE = "coding auto mode" - REVIEWING_COMMENT = "review comment" - REVIEWING_MODIFICATION = "code modification after reviewing" - ERROR_SUMMARY = "error summary" - MODIFICATION = "code modification" - ART_ELEMENT_ABSTRACTION = "art element abstraction" - ART_ELEMENT_INTEGRATION = "art element integration" - CREATING_ENVIRONMENT_DOCUMENT = "environment document" - CREATING_USER_MANUAL = "user manual" - - -__all__ = ["TaskType", "RoleType", "ModelType", "PhaseType"] +# =========== 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 enum import Enum + + +class TaskType(Enum): + AI_SOCIETY = "ai_society" + CODE = "code" + MISALIGNMENT = "misalignment" + TRANSLATION = "translation" + EVALUATION = "evaluation" + SOLUTION_EXTRACTION = "solution_extraction" + CHATDEV = "chat_dev" + DEFAULT = "default" + + +class RoleType(Enum): + ASSISTANT = "assistant" + USER = "user" + CRITIC = "critic" + EMBODIMENT = "embodiment" + DEFAULT = "default" + CHATDEV = "AgentTech" + CHATDEV_COUNSELOR = "counselor" + CHATDEV_CEO = "chief executive officer (CEO)" + CHATDEV_CHRO = "chief human resource officer (CHRO)" + CHATDEV_CPO = "chief product officer (CPO)" + CHATDEV_CTO = "chief technology officer (CTO)" + CHATDEV_PROGRAMMER = "programmer" + CHATDEV_REVIEWER = "code reviewer" + CHATDEV_TESTER = "software test engineer" + CHATDEV_CCO = "chief creative officer (CCO)" + + +class ModelType(Enum): + GPT_3_5_TURBO = "gpt-3.5-turbo-16k-0613" + GPT_3_5_TURBO_NEW = "gpt-3.5-turbo-16k" + GPT_4 = "gpt-4" + GPT_4_32k = "gpt-4-32k" + GPT_4_TURBO = "gpt-4-1106-preview" + GPT_4_TURBO_V = "gpt-4-1106-vision-preview" + GPT_4o = "gpt-4o" + + STUB = "stub" + + @property + def value_for_tiktoken(self): + return self.value if self.name != "STUB" else "gpt-3.5-turbo-16k-0613" + + +class PhaseType(Enum): + REFLECTION = "reflection" + RECRUITING_CHRO = "recruiting CHRO" + RECRUITING_CPO = "recruiting CPO" + RECRUITING_CTO = "recruiting CTO" + DEMAND_ANALYSIS = "demand analysis" + CHOOSING_LANGUAGE = "choosing language" + RECRUITING_PROGRAMMER = "recruiting programmer" + RECRUITING_REVIEWER = "recruiting reviewer" + RECRUITING_TESTER = "recruiting software test engineer" + RECRUITING_CCO = "recruiting chief creative officer" + CODING = "coding" + CODING_COMPLETION = "coding completion" + CODING_AUTOMODE = "coding auto mode" + REVIEWING_COMMENT = "review comment" + REVIEWING_MODIFICATION = "code modification after reviewing" + ERROR_SUMMARY = "error summary" + MODIFICATION = "code modification" + ART_ELEMENT_ABSTRACTION = "art element abstraction" + ART_ELEMENT_INTEGRATION = "art element integration" + CREATING_ENVIRONMENT_DOCUMENT = "environment document" + CREATING_USER_MANUAL = "user manual" + + +__all__ = ["TaskType", "RoleType", "ModelType", "PhaseType"] diff --git a/camel/utils.py b/camel/utils.py index a2713af344..4aebbb60af 100644 --- a/camel/utils.py +++ b/camel/utils.py @@ -1,229 +1,232 @@ -# =========== 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 os -import re -import zipfile -from functools import wraps -from typing import Any, Callable, List, Optional, Set, TypeVar - -import requests -import tiktoken - -from camel.messages import OpenAIMessage -from camel.typing import ModelType, TaskType - -F = TypeVar('F', bound=Callable[..., Any]) - -import time - - -def count_tokens_openai_chat_models( - messages: List[OpenAIMessage], - encoding: Any, -) -> int: - r"""Counts the number of tokens required to generate an OpenAI chat based - on a given list of messages. - - Args: - messages (List[OpenAIMessage]): The list of messages. - encoding (Any): The encoding method to use. - - Returns: - int: The number of tokens required. - """ - num_tokens = 0 - for message in messages: - # message follows {role/name}\n{content}\n - num_tokens += 4 - for key, value in message.items(): - num_tokens += len(encoding.encode(value)) - if key == "name": # if there's a name, the role is omitted - num_tokens += -1 # role is always 1 token - num_tokens += 2 # every reply is primed with assistant - return num_tokens - - -def num_tokens_from_messages( - messages: List[OpenAIMessage], - model: ModelType, -) -> int: - r"""Returns the number of tokens used by a list of messages. - - Args: - messages (List[OpenAIMessage]): The list of messages to count the - number of tokens for. - model (ModelType): The OpenAI model used to encode the messages. - - Returns: - int: The total number of tokens used by the messages. - - Raises: - NotImplementedError: If the specified `model` is not implemented. - - References: - - https://github.com/openai/openai-python/blob/main/chatml.md - - https://platform.openai.com/docs/models/gpt-4 - - https://platform.openai.com/docs/models/gpt-3-5 - """ - try: - value_for_tiktoken = model.value_for_tiktoken - encoding = tiktoken.encoding_for_model(value_for_tiktoken) - except KeyError: - encoding = tiktoken.get_encoding("cl100k_base") - - if model in { - ModelType.GPT_3_5_TURBO, - ModelType.GPT_3_5_TURBO_NEW, - ModelType.GPT_4, - ModelType.GPT_4_32k, - ModelType.GPT_4_TURBO, - ModelType.GPT_4_TURBO_V, - ModelType.STUB - }: - return count_tokens_openai_chat_models(messages, encoding) - else: - raise NotImplementedError( - f"`num_tokens_from_messages`` is not presently implemented " - f"for model {model}. " - f"See https://github.com/openai/openai-python/blob/main/chatml.md " - f"for information on how messages are converted to tokens. " - f"See https://platform.openai.com/docs/models/gpt-4" - f"or https://platform.openai.com/docs/models/gpt-3-5" - f"for information about openai chat models.") - - -def get_model_token_limit(model: ModelType) -> int: - r"""Returns the maximum token limit for a given model. - - Args: - model (ModelType): The type of the model. - - Returns: - int: The maximum token limit for the given model. - """ - if model == ModelType.GPT_3_5_TURBO: - return 16384 - elif model == ModelType.GPT_3_5_TURBO_NEW: - return 16384 - elif model == ModelType.GPT_4: - return 8192 - elif model == ModelType.GPT_4_32k: - return 32768 - elif model == ModelType.GPT_4_TURBO: - return 128000 - elif model == ModelType.STUB: - return 4096 - else: - raise ValueError("Unknown model type") - - -def openai_api_key_required(func: F) -> F: - r"""Decorator that checks if the OpenAI API key is available in the - environment variables. - - Args: - func (callable): The function to be wrapped. - - Returns: - callable: The decorated function. - - Raises: - ValueError: If the OpenAI API key is not found in the environment - variables. - """ - - @wraps(func) - def wrapper(self, *args, **kwargs): - from camel.agents.chat_agent import ChatAgent - if not isinstance(self, ChatAgent): - raise ValueError("Expected ChatAgent") - if self.model == ModelType.STUB: - return func(self, *args, **kwargs) - elif 'OPENAI_API_KEY' in os.environ: - return func(self, *args, **kwargs) - else: - raise ValueError('OpenAI API key not found.') - - return wrapper - - -def print_text_animated(text, delay: float = 0.005, end: str = ""): - r"""Prints the given text with an animated effect. - - Args: - text (str): The text to print. - delay (float, optional): The delay between each character printed. - (default: :obj:`0.02`) - end (str, optional): The end character to print after the text. - (default: :obj:`""`) - """ - for char in text: - print(char, end=end, flush=True) - time.sleep(delay) - print('\n') - - -def get_prompt_template_key_words(template: str) -> Set[str]: - r"""Given a string template containing curly braces {}, return a set of - the words inside the braces. - - Args: - template (str): A string containing curly braces. - - Returns: - List[str]: A list of the words inside the curly braces. - - Example: - >>> get_prompt_template_key_words('Hi, {name}! How are you {status}?') - {'name', 'status'} - """ - return set(re.findall(r'{([^}]*)}', template)) - - -def get_first_int(string: str) -> Optional[int]: - r"""Returns the first integer number found in the given string. - - If no integer number is found, returns None. - - Args: - string (str): The input string. - - Returns: - int or None: The first integer number found in the string, or None if - no integer number is found. - """ - match = re.search(r'\d+', string) - if match: - return int(match.group()) - else: - return None - - -def download_tasks(task: TaskType, folder_path: str) -> None: - # Define the path to save the zip file - zip_file_path = os.path.join(folder_path, "tasks.zip") - - # Download the zip file from the Google Drive link - response = requests.get("https://huggingface.co/datasets/camel-ai/" - f"metadata/resolve/main/{task.value}_tasks.zip") - - # Save the zip file - with open(zip_file_path, "wb") as f: - f.write(response.content) - - with zipfile.ZipFile(zip_file_path, "r") as zip_ref: - zip_ref.extractall(folder_path) - - # Delete the zip file - os.remove(zip_file_path) +# =========== 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 os +import re +import zipfile +from functools import wraps +from typing import Any, Callable, List, Optional, Set, TypeVar + +import requests +import tiktoken + +from camel.messages import OpenAIMessage +from camel.typing import ModelType, TaskType + +F = TypeVar('F', bound=Callable[..., Any]) + +import time + + +def count_tokens_openai_chat_models( + messages: List[OpenAIMessage], + encoding: Any, +) -> int: + r"""Counts the number of tokens required to generate an OpenAI chat based + on a given list of messages. + + Args: + messages (List[OpenAIMessage]): The list of messages. + encoding (Any): The encoding method to use. + + Returns: + int: The number of tokens required. + """ + num_tokens = 0 + for message in messages: + # message follows {role/name}\n{content}\n + num_tokens += 4 + for key, value in message.items(): + num_tokens += len(encoding.encode(value)) + if key == "name": # if there's a name, the role is omitted + num_tokens += -1 # role is always 1 token + num_tokens += 2 # every reply is primed with assistant + return num_tokens + + +def num_tokens_from_messages( + messages: List[OpenAIMessage], + model: ModelType, +) -> int: + r"""Returns the number of tokens used by a list of messages. + + Args: + messages (List[OpenAIMessage]): The list of messages to count the + number of tokens for. + model (ModelType): The OpenAI model used to encode the messages. + + Returns: + int: The total number of tokens used by the messages. + + Raises: + NotImplementedError: If the specified `model` is not implemented. + + References: + - https://github.com/openai/openai-python/blob/main/chatml.md + - https://platform.openai.com/docs/models/gpt-4 + - https://platform.openai.com/docs/models/gpt-3-5 + """ + try: + value_for_tiktoken = model.value_for_tiktoken + encoding = tiktoken.encoding_for_model(value_for_tiktoken) + except KeyError: + encoding = tiktoken.get_encoding("cl100k_base") + + if model in { + ModelType.GPT_3_5_TURBO, + ModelType.GPT_3_5_TURBO_NEW, + ModelType.GPT_4, + ModelType.GPT_4_32k, + ModelType.GPT_4_TURBO, + ModelType.GPT_4_TURBO_V, + ModelType.GPT_4o, + ModelType.STUB + }: + return count_tokens_openai_chat_models(messages, encoding) + else: + raise NotImplementedError( + f"`num_tokens_from_messages`` is not presently implemented " + f"for model {model}. " + f"See https://github.com/openai/openai-python/blob/main/chatml.md " + f"for information on how messages are converted to tokens. " + f"See https://platform.openai.com/docs/models/gpt-4" + f"or https://platform.openai.com/docs/models/gpt-3-5" + f"for information about openai chat models.") + + +def get_model_token_limit(model: ModelType) -> int: + r"""Returns the maximum token limit for a given model. + + Args: + model (ModelType): The type of the model. + + Returns: + int: The maximum token limit for the given model. + """ + if model == ModelType.GPT_3_5_TURBO: + return 16384 + elif model == ModelType.GPT_3_5_TURBO_NEW: + return 16384 + elif model == ModelType.GPT_4: + return 8192 + elif model == ModelType.GPT_4_32k: + return 32768 + elif model == ModelType.GPT_4_TURBO: + return 128000 + elif model == ModelType.GPT_4o: + return 128000 + elif model == ModelType.STUB: + return 4096 + else: + raise ValueError("Unknown model type") + + +def openai_api_key_required(func: F) -> F: + r"""Decorator that checks if the OpenAI API key is available in the + environment variables. + + Args: + func (callable): The function to be wrapped. + + Returns: + callable: The decorated function. + + Raises: + ValueError: If the OpenAI API key is not found in the environment + variables. + """ + + @wraps(func) + def wrapper(self, *args, **kwargs): + from camel.agents.chat_agent import ChatAgent + if not isinstance(self, ChatAgent): + raise ValueError("Expected ChatAgent") + if self.model == ModelType.STUB: + return func(self, *args, **kwargs) + elif 'OPENAI_API_KEY' in os.environ: + return func(self, *args, **kwargs) + else: + raise ValueError('OpenAI API key not found.') + + return wrapper + + +def print_text_animated(text, delay: float = 0.005, end: str = ""): + r"""Prints the given text with an animated effect. + + Args: + text (str): The text to print. + delay (float, optional): The delay between each character printed. + (default: :obj:`0.02`) + end (str, optional): The end character to print after the text. + (default: :obj:`""`) + """ + for char in text: + print(char, end=end, flush=True) + time.sleep(delay) + print('\n') + + +def get_prompt_template_key_words(template: str) -> Set[str]: + r"""Given a string template containing curly braces {}, return a set of + the words inside the braces. + + Args: + template (str): A string containing curly braces. + + Returns: + List[str]: A list of the words inside the curly braces. + + Example: + >>> get_prompt_template_key_words('Hi, {name}! How are you {status}?') + {'name', 'status'} + """ + return set(re.findall(r'{([^}]*)}', template)) + + +def get_first_int(string: str) -> Optional[int]: + r"""Returns the first integer number found in the given string. + + If no integer number is found, returns None. + + Args: + string (str): The input string. + + Returns: + int or None: The first integer number found in the string, or None if + no integer number is found. + """ + match = re.search(r'\d+', string) + if match: + return int(match.group()) + else: + return None + + +def download_tasks(task: TaskType, folder_path: str) -> None: + # Define the path to save the zip file + zip_file_path = os.path.join(folder_path, "tasks.zip") + + # Download the zip file from the Google Drive link + response = requests.get("https://huggingface.co/datasets/camel-ai/" + f"metadata/resolve/main/{task.value}_tasks.zip") + + # Save the zip file + with open(zip_file_path, "wb") as f: + f.write(response.content) + + with zipfile.ZipFile(zip_file_path, "r") as zip_ref: + zip_ref.extractall(folder_path) + + # Delete the zip file + os.remove(zip_file_path) diff --git a/run.py b/run.py index ca2ea727d5..d95d34a9f5 100644 --- a/run.py +++ b/run.py @@ -1,140 +1,141 @@ -# =========== 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 argparse -import logging -import os -import sys - -from camel.typing import ModelType - -root = os.path.dirname(__file__) -sys.path.append(root) - -from chatdev.chat_chain import ChatChain - -try: - from openai.types.chat.chat_completion_message_tool_call import ChatCompletionMessageToolCall - from openai.types.chat.chat_completion_message import FunctionCall - - openai_new_api = True # new openai api version -except ImportError: - openai_new_api = False # old openai api version - print( - "Warning: Your OpenAI version is outdated. \n " - "Please update as specified in requirement.txt. \n " - "The old API interface is deprecated and will no longer be supported.") - - -def get_config(company): - """ - return configuration json files for ChatChain - user can customize only parts of configuration json files, other files will be left for default - Args: - company: customized configuration name under CompanyConfig/ - - Returns: - path to three configuration jsons: [config_path, config_phase_path, config_role_path] - """ - config_dir = os.path.join(root, "CompanyConfig", company) - default_config_dir = os.path.join(root, "CompanyConfig", "Default") - - config_files = [ - "ChatChainConfig.json", - "PhaseConfig.json", - "RoleConfig.json" - ] - - config_paths = [] - - for config_file in config_files: - company_config_path = os.path.join(config_dir, config_file) - default_config_path = os.path.join(default_config_dir, config_file) - - if os.path.exists(company_config_path): - config_paths.append(company_config_path) - else: - config_paths.append(default_config_path) - - return tuple(config_paths) - - -parser = argparse.ArgumentParser(description='argparse') -parser.add_argument('--config', type=str, default="Default", - help="Name of config, which is used to load configuration under CompanyConfig/") -parser.add_argument('--org', type=str, default="DefaultOrganization", - help="Name of organization, your software will be generated in WareHouse/name_org_timestamp") -parser.add_argument('--task', type=str, default="Develop a basic Gomoku game.", - help="Prompt of software") -parser.add_argument('--name', type=str, default="Gomoku", - help="Name of software, your software will be generated in WareHouse/name_org_timestamp") -parser.add_argument('--model', type=str, default="GPT_3_5_TURBO", - help="GPT Model, choose from {'GPT_3_5_TURBO', 'GPT_4', 'GPT_4_TURBO'}") -parser.add_argument('--path', type=str, default="", - help="Your file directory, ChatDev will build upon your software in the Incremental mode") -args = parser.parse_args() - -# Start ChatDev - -# ---------------------------------------- -# Init ChatChain -# ---------------------------------------- -config_path, config_phase_path, config_role_path = get_config(args.config) -args2type = {'GPT_3_5_TURBO': ModelType.GPT_3_5_TURBO, - 'GPT_4': ModelType.GPT_4, - # 'GPT_4_32K': ModelType.GPT_4_32k, - 'GPT_4_TURBO': ModelType.GPT_4_TURBO, - # 'GPT_4_TURBO_V': ModelType.GPT_4_TURBO_V - } -if openai_new_api: - args2type['GPT_3_5_TURBO'] = ModelType.GPT_3_5_TURBO_NEW - -chat_chain = ChatChain(config_path=config_path, - config_phase_path=config_phase_path, - config_role_path=config_role_path, - task_prompt=args.task, - project_name=args.name, - org_name=args.org, - model_type=args2type[args.model], - code_path=args.path) - -# ---------------------------------------- -# Init Log -# ---------------------------------------- -logging.basicConfig(filename=chat_chain.log_filepath, level=logging.INFO, - format='[%(asctime)s %(levelname)s] %(message)s', - datefmt='%Y-%d-%m %H:%M:%S', encoding="utf-8") - -# ---------------------------------------- -# Pre Processing -# ---------------------------------------- - -chat_chain.pre_processing() - -# ---------------------------------------- -# Personnel Recruitment -# ---------------------------------------- - -chat_chain.make_recruitment() - -# ---------------------------------------- -# Chat Chain -# ---------------------------------------- - -chat_chain.execute_chain() - -# ---------------------------------------- -# Post Processing -# ---------------------------------------- - -chat_chain.post_processing() +# =========== 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 argparse +import logging +import os +import sys + +from camel.typing import ModelType + +root = os.path.dirname(__file__) +sys.path.append(root) + +from chatdev.chat_chain import ChatChain + +try: + from openai.types.chat.chat_completion_message_tool_call import ChatCompletionMessageToolCall + from openai.types.chat.chat_completion_message import FunctionCall + + openai_new_api = True # new openai api version +except ImportError: + openai_new_api = False # old openai api version + print( + "Warning: Your OpenAI version is outdated. \n " + "Please update as specified in requirement.txt. \n " + "The old API interface is deprecated and will no longer be supported.") + + +def get_config(company): + """ + return configuration json files for ChatChain + user can customize only parts of configuration json files, other files will be left for default + Args: + company: customized configuration name under CompanyConfig/ + + Returns: + path to three configuration jsons: [config_path, config_phase_path, config_role_path] + """ + config_dir = os.path.join(root, "CompanyConfig", company) + default_config_dir = os.path.join(root, "CompanyConfig", "Default") + + config_files = [ + "ChatChainConfig.json", + "PhaseConfig.json", + "RoleConfig.json" + ] + + config_paths = [] + + for config_file in config_files: + company_config_path = os.path.join(config_dir, config_file) + default_config_path = os.path.join(default_config_dir, config_file) + + if os.path.exists(company_config_path): + config_paths.append(company_config_path) + else: + config_paths.append(default_config_path) + + return tuple(config_paths) + + +parser = argparse.ArgumentParser(description='argparse') +parser.add_argument('--config', type=str, default="Default", + help="Name of config, which is used to load configuration under CompanyConfig/") +parser.add_argument('--org', type=str, default="DefaultOrganization", + help="Name of organization, your software will be generated in WareHouse/name_org_timestamp") +parser.add_argument('--task', type=str, default="Develop a basic Gomoku game.", + help="Prompt of software") +parser.add_argument('--name', type=str, default="Gomoku", + help="Name of software, your software will be generated in WareHouse/name_org_timestamp") +parser.add_argument('--model', type=str, default="GPT_3_5_TURBO", + help="GPT Model, choose from {'GPT_3_5_TURBO','GPT_4','GPT_4_32K', 'GPT_4_TURBO', 'GPT_4o'}") +parser.add_argument('--path', type=str, default="", + help="Your file directory, ChatDev will build upon your software in the Incremental mode") +args = parser.parse_args() + +# Start ChatDev + +# ---------------------------------------- +# Init ChatChain +# ---------------------------------------- +config_path, config_phase_path, config_role_path = get_config(args.config) +args2type = {'GPT_3_5_TURBO': ModelType.GPT_3_5_TURBO, + 'GPT_4': ModelType.GPT_4, + 'GPT_4_32K': ModelType.GPT_4_32k, + 'GPT_4_TURBO': ModelType.GPT_4_TURBO, + 'GPT_4_TURBO_V': ModelType.GPT_4_TURBO_V, + 'GPT_4o': ModelType.GPT_4o + } +if openai_new_api: + args2type['GPT_3_5_TURBO'] = ModelType.GPT_3_5_TURBO_NEW + +chat_chain = ChatChain(config_path=config_path, + config_phase_path=config_phase_path, + config_role_path=config_role_path, + task_prompt=args.task, + project_name=args.name, + org_name=args.org, + model_type=args2type[args.model], + code_path=args.path) + +# ---------------------------------------- +# Init Log +# ---------------------------------------- +logging.basicConfig(filename=chat_chain.log_filepath, level=logging.INFO, + format='[%(asctime)s %(levelname)s] %(message)s', + datefmt='%Y-%d-%m %H:%M:%S', encoding="utf-8") + +# ---------------------------------------- +# Pre Processing +# ---------------------------------------- + +chat_chain.pre_processing() + +# ---------------------------------------- +# Personnel Recruitment +# ---------------------------------------- + +chat_chain.make_recruitment() + +# ---------------------------------------- +# Chat Chain +# ---------------------------------------- + +chat_chain.execute_chain() + +# ---------------------------------------- +# Post Processing +# ---------------------------------------- + +chat_chain.post_processing()