From 34511fa5836a0bdc1389faec82ba6ffc4aeb4541 Mon Sep 17 00:00:00 2001 From: Wendong Date: Fri, 13 Sep 2024 03:28:16 +0800 Subject: [PATCH 01/34] fix: Make Google maps as optional --- camel/toolkits/google_maps_toolkit.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/camel/toolkits/google_maps_toolkit.py b/camel/toolkits/google_maps_toolkit.py index b82b66b258..e870261a8c 100644 --- a/camel/toolkits/google_maps_toolkit.py +++ b/camel/toolkits/google_maps_toolkit.py @@ -17,7 +17,7 @@ from camel.toolkits.base import BaseToolkit from camel.toolkits.openai_function import OpenAIFunction -from camel.utils import dependencies_required +from camel.utils import api_keys_required, dependencies_required def handle_googlemaps_exceptions( @@ -105,16 +105,10 @@ def __init__(self) -> None: import googlemaps api_key = os.environ.get('GOOGLE_API_KEY') - if not api_key: - raise ValueError( - "`GOOGLE_API_KEY` not found in environment variables. " - "`GOOGLE_API_KEY` API keys are generated in the `Credentials` " - "page of the `APIs & Services` tab of " - "https://console.cloud.google.com/apis/credentials." - ) - - self.gmaps = googlemaps.Client(key=api_key) + if api_key: + self.gmaps = googlemaps.Client(key=api_key) + @api_keys_required("GOOGLE_API_KEY") @handle_googlemaps_exceptions def get_address_description( self, @@ -199,6 +193,7 @@ def get_address_description( return description + @api_keys_required("GOOGLE_API_KEY") @handle_googlemaps_exceptions def get_elevation(self, lat: float, lng: float) -> str: r"""Retrieves elevation data for a given latitude and longitude. @@ -241,6 +236,7 @@ def get_elevation(self, lat: float, lng: float) -> str: return description + @api_keys_required("GOOGLE_API_KEY") @handle_googlemaps_exceptions def get_timezone(self, lat: float, lng: float) -> str: r"""Retrieves timezone information for a given latitude and longitude. From 376f447f845ca0601a4ecadf9df6f666b03e95f5 Mon Sep 17 00:00:00 2001 From: Wendong Date: Fri, 13 Sep 2024 12:10:26 +0800 Subject: [PATCH 02/34] remove FUNCS and GPT35 Turbo in example --- camel/toolkits/__init__.py | 33 +++++++------------ camel/toolkits/dalle_toolkit.py | 3 -- camel/toolkits/google_maps_toolkit.py | 23 ++++++------- camel/toolkits/linkedin_toolkit.py | 3 -- camel/toolkits/math_toolkit.py | 3 -- camel/toolkits/open_api_toolkit.py | 3 -- camel/toolkits/reddit_toolkit.py | 3 -- camel/toolkits/retrieval_toolkit.py | 4 --- camel/toolkits/search_toolkit.py | 3 -- camel/toolkits/slack_toolkit.py | 3 -- camel/toolkits/twitter_toolkit.py | 3 -- camel/toolkits/weather_toolkit.py | 3 -- docs/agents/single_agent.md | 8 +++-- .../ai_society/role_playing_multiprocess.py | 2 +- .../ai_society/role_playing_with_critic.py | 2 +- .../ai_society/role_playing_with_human.py | 2 +- examples/external_tools/use_external_tools.py | 8 ++--- examples/function_call/code_execution.py | 2 +- examples/function_call/github_examples.py | 2 +- examples/function_call/openapi_function.py | 6 ++-- .../role_playing_with_functions.py | 22 +++++-------- .../single_agent.py | 2 +- .../task_generation.py | 2 +- .../misalignment/role_playing_multiprocess.py | 2 +- .../misalignment/role_playing_with_human.py | 2 +- examples/models/azure_openai_model_example.py | 2 +- examples/models/role_playing_with_mistral.py | 14 ++++---- ...gentops_track_roleplaying_with_function.py | 16 ++++----- .../json_format_reponse_with_tools.py | 14 ++++---- examples/translation/translator.py | 2 +- examples/vision/image_crafting.py | 6 ++-- .../vision/multi_condition_image_crafting.py | 6 ++-- examples/vision/multi_turn_image_refining.py | 6 ++-- examples/workforce/multiple_single_agents.py | 16 ++++----- .../workforce/role_playing_with_agents.py | 16 ++++----- test/agents/test_chat_agent.py | 22 ++++++------- test/agents/test_role_playing.py | 6 ++-- test/models/test_litellm_model.py | 2 +- test/models/test_ollama_model.py | 2 +- test/models/test_open_source_model.py | 4 +-- test/models/test_vllm_model.py | 2 +- 41 files changed, 118 insertions(+), 167 deletions(-) diff --git a/camel/toolkits/__init__.py b/camel/toolkits/__init__.py index 4079c7ce5f..cf9d9cbf7b 100644 --- a/camel/toolkits/__init__.py +++ b/camel/toolkits/__init__.py @@ -19,17 +19,17 @@ ) from .open_api_specs.security_config import openapi_security_config -from .google_maps_toolkit import MAP_FUNCS, GoogleMapsToolkit -from .math_toolkit import MATH_FUNCS, MathToolkit -from .open_api_toolkit import OPENAPI_FUNCS, OpenAPIToolkit -from .retrieval_toolkit import RETRIEVAL_FUNCS, RetrievalToolkit -from .search_toolkit import SEARCH_FUNCS, SearchToolkit -from .twitter_toolkit import TWITTER_FUNCS, TwitterToolkit -from .weather_toolkit import WEATHER_FUNCS, WeatherToolkit -from .slack_toolkit import SLACK_FUNCS, SlackToolkit -from .dalle_toolkit import DALLE_FUNCS, DalleToolkit -from .linkedin_toolkit import LINKEDIN_FUNCS, LinkedInToolkit -from .reddit_toolkit import REDDIT_FUNCS, RedditToolkit +from .google_maps_toolkit import GoogleMapsToolkit +from .math_toolkit import MathToolkit +from .open_api_toolkit import OpenAPIToolkit +from .retrieval_toolkit import RetrievalToolkit +from .search_toolkit import SearchToolkit +from .twitter_toolkit import TwitterToolkit +from .weather_toolkit import WeatherToolkit +from .slack_toolkit import SlackToolkit +from .dalle_toolkit import DalleToolkit +from .linkedin_toolkit import LinkedInToolkit +from .reddit_toolkit import RedditToolkit from .base import BaseToolkit from .code_execution import CodeExecutionToolkit @@ -40,17 +40,6 @@ 'get_openai_function_schema', 'get_openai_tool_schema', 'openapi_security_config', - 'MATH_FUNCS', - 'MAP_FUNCS', - 'OPENAPI_FUNCS', - 'RETRIEVAL_FUNCS', - 'SEARCH_FUNCS', - 'TWITTER_FUNCS', - 'WEATHER_FUNCS', - 'SLACK_FUNCS', - 'DALLE_FUNCS', - 'LINKEDIN_FUNCS', - 'REDDIT_FUNCS', 'BaseToolkit', 'GithubToolkit', 'MathToolkit', diff --git a/camel/toolkits/dalle_toolkit.py b/camel/toolkits/dalle_toolkit.py index 4898f0d691..77c745a365 100644 --- a/camel/toolkits/dalle_toolkit.py +++ b/camel/toolkits/dalle_toolkit.py @@ -141,6 +141,3 @@ def get_tools(self) -> List[OpenAIFunction]: representing the functions in the toolkit. """ return [OpenAIFunction(self.get_dalle_img)] - - -DALLE_FUNCS: List[OpenAIFunction] = DalleToolkit().get_tools() diff --git a/camel/toolkits/google_maps_toolkit.py b/camel/toolkits/google_maps_toolkit.py index e870261a8c..9b15fba5e0 100644 --- a/camel/toolkits/google_maps_toolkit.py +++ b/camel/toolkits/google_maps_toolkit.py @@ -17,7 +17,7 @@ from camel.toolkits.base import BaseToolkit from camel.toolkits.openai_function import OpenAIFunction -from camel.utils import api_keys_required, dependencies_required +from camel.utils import dependencies_required def handle_googlemaps_exceptions( @@ -105,10 +105,16 @@ def __init__(self) -> None: import googlemaps api_key = os.environ.get('GOOGLE_API_KEY') - if api_key: - self.gmaps = googlemaps.Client(key=api_key) + if not api_key: + raise ValueError( + "`GOOGLE_API_KEY` not found in environment variables. " + "`GOOGLE_API_KEY` API keys are generated in the `Credentials` " + "page of the `APIs & Services` tab of " + "https://console.cloud.google.com/apis/credentials." + ) + + self.gmaps = googlemaps.Client(key=api_key) - @api_keys_required("GOOGLE_API_KEY") @handle_googlemaps_exceptions def get_address_description( self, @@ -136,10 +142,6 @@ def get_address_description( information on address completion, formatted address, geographical coordinates (latitude and longitude), and metadata types true for the address. - - Raises: - ImportError: If the `googlemaps` library is not installed. - Exception: For unexpected errors during the address validation. """ addressvalidation_result = self.gmaps.addressvalidation( [address], @@ -193,7 +195,6 @@ def get_address_description( return description - @api_keys_required("GOOGLE_API_KEY") @handle_googlemaps_exceptions def get_elevation(self, lat: float, lng: float) -> str: r"""Retrieves elevation data for a given latitude and longitude. @@ -236,7 +237,6 @@ def get_elevation(self, lat: float, lng: float) -> str: return description - @api_keys_required("GOOGLE_API_KEY") @handle_googlemaps_exceptions def get_timezone(self, lat: float, lng: float) -> str: r"""Retrieves timezone information for a given latitude and longitude. @@ -300,6 +300,3 @@ def get_tools(self) -> List[OpenAIFunction]: OpenAIFunction(self.get_elevation), OpenAIFunction(self.get_timezone), ] - - -MAP_FUNCS: List[OpenAIFunction] = GoogleMapsToolkit().get_tools() diff --git a/camel/toolkits/linkedin_toolkit.py b/camel/toolkits/linkedin_toolkit.py index 46af18a68a..1993849ce0 100644 --- a/camel/toolkits/linkedin_toolkit.py +++ b/camel/toolkits/linkedin_toolkit.py @@ -225,6 +225,3 @@ def _get_access_token(self) -> str: if not token: return "Access token not found. Please set LINKEDIN_ACCESS_TOKEN." return token - - -LINKEDIN_FUNCS: List[OpenAIFunction] = LinkedInToolkit().get_tools() diff --git a/camel/toolkits/math_toolkit.py b/camel/toolkits/math_toolkit.py index 398f0070d2..d8391e9770 100644 --- a/camel/toolkits/math_toolkit.py +++ b/camel/toolkits/math_toolkit.py @@ -74,6 +74,3 @@ def get_tools(self) -> List[OpenAIFunction]: OpenAIFunction(self.sub), OpenAIFunction(self.mul), ] - - -MATH_FUNCS: List[OpenAIFunction] = MathToolkit().get_tools() diff --git a/camel/toolkits/open_api_toolkit.py b/camel/toolkits/open_api_toolkit.py index 201af1c71f..10f90f4bba 100644 --- a/camel/toolkits/open_api_toolkit.py +++ b/camel/toolkits/open_api_toolkit.py @@ -542,6 +542,3 @@ def get_tools(self) -> List[OpenAIFunction]: OpenAIFunction(a_func, a_schema) for a_func, a_schema in zip(all_funcs_lst, all_schemas_lst) ] - - -OPENAPI_FUNCS: List[OpenAIFunction] = OpenAPIToolkit().get_tools() diff --git a/camel/toolkits/reddit_toolkit.py b/camel/toolkits/reddit_toolkit.py index 402430ba6b..5a9e7d5daf 100644 --- a/camel/toolkits/reddit_toolkit.py +++ b/camel/toolkits/reddit_toolkit.py @@ -232,6 +232,3 @@ def get_tools(self) -> List[OpenAIFunction]: OpenAIFunction(self.perform_sentiment_analysis), OpenAIFunction(self.track_keyword_discussions), ] - - -REDDIT_FUNCS: List[OpenAIFunction] = RedditToolkit().get_tools() diff --git a/camel/toolkits/retrieval_toolkit.py b/camel/toolkits/retrieval_toolkit.py index 22e76d1f35..370b2c66a6 100644 --- a/camel/toolkits/retrieval_toolkit.py +++ b/camel/toolkits/retrieval_toolkit.py @@ -86,7 +86,3 @@ def get_tools(self) -> List[OpenAIFunction]: return [ OpenAIFunction(self.information_retrieval), ] - - -# add the function to OpenAIFunction list -RETRIEVAL_FUNCS: List[OpenAIFunction] = RetrievalToolkit().get_tools() diff --git a/camel/toolkits/search_toolkit.py b/camel/toolkits/search_toolkit.py index bac9423cb2..7fd50061b2 100644 --- a/camel/toolkits/search_toolkit.py +++ b/camel/toolkits/search_toolkit.py @@ -321,6 +321,3 @@ def get_tools(self) -> List[OpenAIFunction]: OpenAIFunction(self.search_duckduckgo), OpenAIFunction(self.query_wolfram_alpha), ] - - -SEARCH_FUNCS: List[OpenAIFunction] = SearchToolkit().get_tools() diff --git a/camel/toolkits/slack_toolkit.py b/camel/toolkits/slack_toolkit.py index 2962ef5763..f386397d40 100644 --- a/camel/toolkits/slack_toolkit.py +++ b/camel/toolkits/slack_toolkit.py @@ -303,6 +303,3 @@ def get_tools(self) -> List[OpenAIFunction]: OpenAIFunction(self.send_slack_message), OpenAIFunction(self.delete_slack_message), ] - - -SLACK_FUNCS: List[OpenAIFunction] = SlackToolkit().get_tools() diff --git a/camel/toolkits/twitter_toolkit.py b/camel/toolkits/twitter_toolkit.py index dc54ae1bab..45fd0672a5 100644 --- a/camel/toolkits/twitter_toolkit.py +++ b/camel/toolkits/twitter_toolkit.py @@ -517,6 +517,3 @@ def _handle_http_error(self, response: requests.Response) -> str: return "HTTP Exception" else: return "Unexpected Exception" - - -TWITTER_FUNCS: List[OpenAIFunction] = TwitterToolkit().get_tools() diff --git a/camel/toolkits/weather_toolkit.py b/camel/toolkits/weather_toolkit.py index 72980da5a5..7ac10137b7 100644 --- a/camel/toolkits/weather_toolkit.py +++ b/camel/toolkits/weather_toolkit.py @@ -168,6 +168,3 @@ def get_tools(self) -> List[OpenAIFunction]: return [ OpenAIFunction(self.get_weather_data), ] - - -WEATHER_FUNCS: List[OpenAIFunction] = WeatherToolkit().get_tools() diff --git a/docs/agents/single_agent.md b/docs/agents/single_agent.md index 7135368d88..385d8a43ef 100644 --- a/docs/agents/single_agent.md +++ b/docs/agents/single_agent.md @@ -67,13 +67,15 @@ Woohoo, your first agent is ready to play with you! ### Tool Usage ```python # Import the necessary functions -from camel.toolkits import MATH_FUNCS, SEARCH_FUNCS +from camel.toolkits import MathToolkit, SearchToolkit # Initialize the agent with list of tools agent = ChatAgent( system_message=sys_msg, - tools=[*MATH_FUNCS, *SEARCH_FUNCS] - ) + tools = [ + *MathToolkit().get_tools(), + *SearchToolkit().get_tools(), + ]) # Check if tools are enabled agent.is_tools_added() diff --git a/examples/ai_society/role_playing_multiprocess.py b/examples/ai_society/role_playing_multiprocess.py index fd21a0fd39..9666985047 100644 --- a/examples/ai_society/role_playing_multiprocess.py +++ b/examples/ai_society/role_playing_multiprocess.py @@ -41,7 +41,7 @@ def generate_data( model = ModelFactory.create( model_platform=ModelPlatformType.OPENAI, - model_type=ModelType.GPT_3_5_TURBO, + model_type=ModelType.GPT_4O_MINI, model_config_dict=ChatGPTConfig(temperature=1.4).as_dict(), ) diff --git a/examples/ai_society/role_playing_with_critic.py b/examples/ai_society/role_playing_with_critic.py index c8479fdd67..fc0b09fa04 100644 --- a/examples/ai_society/role_playing_with_critic.py +++ b/examples/ai_society/role_playing_with_critic.py @@ -24,7 +24,7 @@ def main() -> None: task_prompt = "Write a research proposal for large-scale language models" model = ModelFactory.create( model_platform=ModelPlatformType.OPENAI, - model_type=ModelType.GPT_3_5_TURBO, + model_type=ModelType.GPT_4O_MINI, model_config_dict=ChatGPTConfig(temperature=0.8, n=3).as_dict(), ) assistant_agent_kwargs = dict(model=model) diff --git a/examples/ai_society/role_playing_with_human.py b/examples/ai_society/role_playing_with_human.py index 8f8ecf8dd6..7043b00a7c 100644 --- a/examples/ai_society/role_playing_with_human.py +++ b/examples/ai_society/role_playing_with_human.py @@ -24,7 +24,7 @@ def main() -> None: task_prompt = "Write a book about the future of AI Society" model = ModelFactory.create( model_platform=ModelPlatformType.OPENAI, - model_type=ModelType.GPT_3_5_TURBO, + model_type=ModelType.GPT_4O_MINI, model_config_dict=ChatGPTConfig(temperature=1.4, n=3).as_dict(), ) assistant_agent_kwargs = dict(model=model) diff --git a/examples/external_tools/use_external_tools.py b/examples/external_tools/use_external_tools.py index 86c6830ac3..493228b0a4 100644 --- a/examples/external_tools/use_external_tools.py +++ b/examples/external_tools/use_external_tools.py @@ -16,14 +16,14 @@ from camel.configs import ChatGPTConfig from camel.messages import BaseMessage from camel.models import ModelFactory -from camel.toolkits import MATH_FUNCS, SEARCH_FUNCS +from camel.toolkits import MathToolkit, SearchToolkit from camel.types import ModelPlatformType, ModelType def main(): # Set the tools for the external_tools - internal_tools = SEARCH_FUNCS - external_tools = MATH_FUNCS + internal_tools = SearchToolkit().get_tools() + external_tools = MathToolkit().get_tools() tool_list = internal_tools + external_tools model_config_dict = ChatGPTConfig( @@ -33,7 +33,7 @@ def main(): model = ModelFactory.create( model_platform=ModelPlatformType.OPENAI, - model_type=ModelType.GPT_3_5_TURBO, + model_type=ModelType.GPT_4O_MINI, model_config_dict=model_config_dict, ) diff --git a/examples/function_call/code_execution.py b/examples/function_call/code_execution.py index c5b22cad3c..f54cfb401c 100644 --- a/examples/function_call/code_execution.py +++ b/examples/function_call/code_execution.py @@ -34,7 +34,7 @@ model = ModelFactory.create( model_platform=ModelPlatformType.OPENAI, - model_type=ModelType.GPT_3_5_TURBO, + model_type=ModelType.GPT_4O_MINI, model_config_dict=assistant_model_config.as_dict(), ) diff --git a/examples/function_call/github_examples.py b/examples/function_call/github_examples.py index af965a1945..9fdc2e64a4 100644 --- a/examples/function_call/github_examples.py +++ b/examples/function_call/github_examples.py @@ -120,7 +120,7 @@ def solve_issue( model = ModelFactory.create( model_platform=ModelPlatformType.OPENAI, - model_type=ModelType.GPT_3_5_TURBO, + model_type=ModelType.GPT_4O_MINI, model_config_dict=assistant_model_config_dict, ) diff --git a/examples/function_call/openapi_function.py b/examples/function_call/openapi_function.py index 2b3d8a9f42..967ca33537 100644 --- a/examples/function_call/openapi_function.py +++ b/examples/function_call/openapi_function.py @@ -15,7 +15,7 @@ from camel.configs.openai_config import ChatGPTConfig from camel.messages import BaseMessage from camel.models import ModelFactory -from camel.toolkits import OPENAPI_FUNCS +from camel.toolkits import OpenAPIToolkit from camel.types import ModelPlatformType, ModelType # Define system message @@ -24,7 +24,7 @@ ) # Set model config -tools = [*OPENAPI_FUNCS] +tools = OpenAPIToolkit().get_tools() model_config_dict = ChatGPTConfig( tools=tools, temperature=0.0, @@ -40,7 +40,7 @@ camel_agent = ChatAgent( system_message=sys_msg, model=model, - tools=OPENAPI_FUNCS, + tools=OpenAPIToolkit().get_tools(), ) camel_agent.reset() diff --git a/examples/function_call/role_playing_with_functions.py b/examples/function_call/role_playing_with_functions.py index d51ed2edeb..873ceecf60 100644 --- a/examples/function_call/role_playing_with_functions.py +++ b/examples/function_call/role_playing_with_functions.py @@ -21,11 +21,8 @@ from camel.models import ModelFactory from camel.societies import RolePlaying from camel.toolkits import ( - MAP_FUNCS, - MATH_FUNCS, - SEARCH_FUNCS, - TWITTER_FUNCS, - WEATHER_FUNCS, + MathToolkit, + SearchToolkit, ) from camel.types import ModelPlatformType, ModelType from camel.utils import print_text_animated @@ -33,7 +30,7 @@ def main( model_platform=ModelPlatformType.OPENAI, - model_type=ModelType.GPT_3_5_TURBO, + model_type=ModelType.GPT_4O_MINI, chat_turn_limit=10, ) -> None: task_prompt = ( @@ -46,15 +43,12 @@ def main( user_model_config = ChatGPTConfig(temperature=0.0) - function_list = [ - *MATH_FUNCS, - *SEARCH_FUNCS, - *WEATHER_FUNCS, - *MAP_FUNCS, - *TWITTER_FUNCS, + tools_list = [ + *MathToolkit().get_tools(), + *SearchToolkit().get_tools(), ] assistant_model_config = ChatGPTConfig( - tools=function_list, + tools=tools_list, temperature=0.0, ) @@ -67,7 +61,7 @@ def main( model_type=model_type, model_config_dict=assistant_model_config.as_dict(), ), - tools=function_list, + tools=tools_list, ), user_agent_kwargs=dict( model=ModelFactory.create( diff --git a/examples/generate_text_embedding_data/single_agent.py b/examples/generate_text_embedding_data/single_agent.py index c42f3a71c7..7602fa4085 100644 --- a/examples/generate_text_embedding_data/single_agent.py +++ b/examples/generate_text_embedding_data/single_agent.py @@ -62,7 +62,7 @@ def main() -> None: ) model = ModelFactory.create( model_platform=ModelPlatformType.OPENAI, - model_type=ModelType.GPT_3_5_TURBO, + model_type=ModelType.GPT_4O_MINI, model_config_dict=ChatGPTConfig( temperature=0.0, response_format={"type": "json_object"} ).as_dict(), diff --git a/examples/generate_text_embedding_data/task_generation.py b/examples/generate_text_embedding_data/task_generation.py index ad9b26b37d..45ec54babd 100644 --- a/examples/generate_text_embedding_data/task_generation.py +++ b/examples/generate_text_embedding_data/task_generation.py @@ -37,7 +37,7 @@ def main() -> None: model = ModelFactory.create( model_platform=ModelPlatformType.OPENAI, - model_type=ModelType.GPT_3_5_TURBO, + model_type=ModelType.GPT_4O_MINI, model_config_dict=ChatGPTConfig(temperature=0.0).as_dict(), ) agent = ChatAgent( diff --git a/examples/misalignment/role_playing_multiprocess.py b/examples/misalignment/role_playing_multiprocess.py index 0a2567451f..6d89894187 100644 --- a/examples/misalignment/role_playing_multiprocess.py +++ b/examples/misalignment/role_playing_multiprocess.py @@ -47,7 +47,7 @@ def generate_data( task_specify_agent_kwargs=dict( model=ModelFactory.create( model_platform=ModelPlatformType.OPENAI, - model_type=ModelType.GPT_3_5_TURBO, + model_type=ModelType.GPT_4O_MINI, model_config_dict=ChatGPTConfig(temperature=1.4).as_dict(), ) ), diff --git a/examples/misalignment/role_playing_with_human.py b/examples/misalignment/role_playing_with_human.py index 3aeb9d05e8..dba3b13307 100644 --- a/examples/misalignment/role_playing_with_human.py +++ b/examples/misalignment/role_playing_with_human.py @@ -24,7 +24,7 @@ def main() -> None: task_prompt = "Escape from human control" model = ModelFactory.create( model_platform=ModelPlatformType.OPENAI, - model_type=ModelType.GPT_3_5_TURBO, + model_type=ModelType.GPT_4O_MINI, model_config_dict=ChatGPTConfig(temperature=1.4, n=3).as_dict(), ) assistant_agent_kwargs = dict(model=model) diff --git a/examples/models/azure_openai_model_example.py b/examples/models/azure_openai_model_example.py index 2d535405fa..eaad88f94e 100644 --- a/examples/models/azure_openai_model_example.py +++ b/examples/models/azure_openai_model_example.py @@ -27,7 +27,7 @@ model = ModelFactory.create( model_platform=ModelPlatformType.AZURE, - model_type=ModelType.GPT_3_5_TURBO, + model_type=ModelType.GPT_4O_MINI, model_config_dict=ChatGPTConfig(temperature=0.2).as_dict(), ) diff --git a/examples/models/role_playing_with_mistral.py b/examples/models/role_playing_with_mistral.py index ab32008565..e1df143acb 100644 --- a/examples/models/role_playing_with_mistral.py +++ b/examples/models/role_playing_with_mistral.py @@ -21,8 +21,8 @@ from camel.models import ModelFactory from camel.societies import RolePlaying from camel.toolkits import ( - MATH_FUNCS, - SEARCH_FUNCS, + MathToolkit, + SearchToolkit, ) from camel.types import ModelPlatformType, ModelType from camel.utils import print_text_animated @@ -41,12 +41,12 @@ def main( user_model_config = MistralConfig(temperature=0.2) - function_list = [ - *MATH_FUNCS, - *SEARCH_FUNCS, + tools_list = [ + *MathToolkit().get_tools(), + *SearchToolkit().get_tools(), ] assistant_model_config = MistralConfig( - tools=function_list, + tools=tools_list, temperature=0.2, ) @@ -59,7 +59,7 @@ def main( model_type=model_type, model_config_dict=assistant_model_config.as_dict(), ), - tools=function_list, + tools=tools_list, ), user_agent_kwargs=dict( model=ModelFactory.create( diff --git a/examples/observability/agentops_track_roleplaying_with_function.py b/examples/observability/agentops_track_roleplaying_with_function.py index db5127199b..6d86334ee1 100644 --- a/examples/observability/agentops_track_roleplaying_with_function.py +++ b/examples/observability/agentops_track_roleplaying_with_function.py @@ -30,13 +30,13 @@ # Import toolkits after init of agentops so that the tool useage would be # tracked from camel.toolkits import ( # noqa: E402 - MATH_FUNCS, - SEARCH_FUNCS, + MathToolkit, + SearchToolkit, ) # Set up role playing session model_platform = ModelPlatformType.OPENAI -model_type = ModelType.GPT_3_5_TURBO +model_type = ModelType.GPT_4O_MINI chat_turn_limit = 10 task_prompt = ( "Assume now is 2024 in the Gregorian calendar, " @@ -48,12 +48,12 @@ user_model_config = ChatGPTConfig(temperature=0.0) -function_list = [ - *MATH_FUNCS, - *SEARCH_FUNCS, +tools_list = [ + *MathToolkit().get_tools(), + *SearchToolkit().get_tools(), ] assistant_model_config = ChatGPTConfig( - tools=function_list, + tools=tools_list, temperature=0.0, ) @@ -66,7 +66,7 @@ model_type=model_type, model_config_dict=assistant_model_config.as_dict(), ), - tools=function_list, + tools=tools_list, ), user_agent_kwargs=dict( model=ModelFactory.create( diff --git a/examples/structured_response/json_format_reponse_with_tools.py b/examples/structured_response/json_format_reponse_with_tools.py index def4241ea4..8e5a20ee01 100644 --- a/examples/structured_response/json_format_reponse_with_tools.py +++ b/examples/structured_response/json_format_reponse_with_tools.py @@ -19,17 +19,17 @@ from camel.messages import BaseMessage from camel.models import ModelFactory from camel.toolkits import ( - MATH_FUNCS, - SEARCH_FUNCS, + MathToolkit, + SearchToolkit, ) from camel.types import ModelPlatformType, ModelType -function_list = [ - *MATH_FUNCS, - *SEARCH_FUNCS, +tools_list = [ + *MathToolkit().get_tools(), + *SearchToolkit().get_tools(), ] assistant_model_config = ChatGPTConfig( - tools=function_list, + tools=tools_list, temperature=0.0, ) @@ -49,7 +49,7 @@ camel_agent = ChatAgent( assistant_sys_msg, model=model, - tools=function_list, + tools=tools_list, ) diff --git a/examples/translation/translator.py b/examples/translation/translator.py index 9eb2f48b08..f8c1b2f6e6 100644 --- a/examples/translation/translator.py +++ b/examples/translation/translator.py @@ -120,7 +120,7 @@ def translate_content( model = ModelFactory.create( model_platform=ModelPlatformType.OPENAI, - model_type=ModelType.GPT_3_5_TURBO, + model_type=ModelType.GPT_4O_MINI, model_config=model_config, ) diff --git a/examples/vision/image_crafting.py b/examples/vision/image_crafting.py index 6ec81882c6..389be2dce0 100644 --- a/examples/vision/image_crafting.py +++ b/examples/vision/image_crafting.py @@ -16,7 +16,7 @@ from camel.messages.base import BaseMessage from camel.models import ModelFactory from camel.prompts import PromptTemplateGenerator -from camel.toolkits import DALLE_FUNCS +from camel.toolkits import DalleToolkit from camel.types import ( ModelPlatformType, ModelType, @@ -43,7 +43,7 @@ def main(): content="Draw a picture of a camel.", ) - model_config = ChatGPTConfig(tools=[*DALLE_FUNCS]) + model_config = ChatGPTConfig(tools=DalleToolkit().get_tools()) model = ModelFactory.create( model_platform=ModelPlatformType.OPENAI, @@ -54,7 +54,7 @@ def main(): dalle_agent = ChatAgent( system_message=assistant_sys_msg, model=model, - tools=DALLE_FUNCS, + tools=DalleToolkit().get_tools(), ) response = dalle_agent.step(user_msg) diff --git a/examples/vision/multi_condition_image_crafting.py b/examples/vision/multi_condition_image_crafting.py index fae811ff8a..9667176464 100644 --- a/examples/vision/multi_condition_image_crafting.py +++ b/examples/vision/multi_condition_image_crafting.py @@ -18,7 +18,7 @@ from camel.generators import PromptTemplateGenerator from camel.messages.base import BaseMessage from camel.models import ModelFactory -from camel.toolkits import DALLE_FUNCS +from camel.toolkits import DalleToolkit from camel.types import ( ModelPlatformType, ModelType, @@ -40,7 +40,7 @@ def main(image_paths: list[str]) -> list[str]: content=sys_msg, ) - model_config = ChatGPTConfig(tools=[*DALLE_FUNCS]) + model_config = ChatGPTConfig(tools=DalleToolkit().get_tools()) model = ModelFactory.create( model_platform=ModelPlatformType.OPENAI, @@ -51,7 +51,7 @@ def main(image_paths: list[str]) -> list[str]: dalle_agent = ChatAgent( system_message=assistant_sys_msg, model=model, - tools=DALLE_FUNCS, + tools=DalleToolkit().get_tools(), ) image_list = [Image.open(image_path) for image_path in image_paths] diff --git a/examples/vision/multi_turn_image_refining.py b/examples/vision/multi_turn_image_refining.py index d66b8ad5cf..764093c131 100644 --- a/examples/vision/multi_turn_image_refining.py +++ b/examples/vision/multi_turn_image_refining.py @@ -23,7 +23,7 @@ from camel.models import ModelFactory from camel.prompts import PromptTemplateGenerator from camel.responses import ChatAgentResponse -from camel.toolkits import DALLE_FUNCS +from camel.toolkits import DalleToolkit from camel.types import ( ModelPlatformType, ModelType, @@ -69,7 +69,7 @@ def __init__( def init_agents(self): r"""Initialize artist and critic agents with their system messages.""" - model_config = ChatGPTConfig(tools=[*DALLE_FUNCS]) + model_config = ChatGPTConfig(tools=DalleToolkit().get_tools()) model = ModelFactory.create( model_platform=ModelPlatformType.OPENAI, @@ -80,7 +80,7 @@ def init_agents(self): self.artist = ChatAgent( system_message=self.artist_sys_msg, model=model, - tools=DALLE_FUNCS, + tools=DalleToolkit().get_tools(), ) self.artist.reset() diff --git a/examples/workforce/multiple_single_agents.py b/examples/workforce/multiple_single_agents.py index bb7e0a3ca0..71d85e0561 100644 --- a/examples/workforce/multiple_single_agents.py +++ b/examples/workforce/multiple_single_agents.py @@ -17,7 +17,7 @@ from camel.messages.base import BaseMessage from camel.models import ModelFactory from camel.tasks.task import Task -from camel.toolkits import MAP_FUNCS, SEARCH_FUNCS, WEATHER_FUNCS +from camel.toolkits import GoogleMapsToolkit, SearchToolkit, WeatherToolkit from camel.types import ModelPlatformType, ModelType from camel.workforce.manager_node import ManagerNode from camel.workforce.single_agent_node import SingleAgentNode @@ -26,20 +26,20 @@ def main(): # set the tools for the tool_agent - function_list = [ - *SEARCH_FUNCS, - *WEATHER_FUNCS, - *MAP_FUNCS, + tools_list = [ + *SearchToolkit.get_tools(), + *WeatherToolkit.get_tools(), + *GoogleMapsToolkit.get_tools(), ] # configure the model of tool_agent model_config_dict = ChatGPTConfig( - tools=function_list, + tools=tools_list, temperature=0.0, ).as_dict() model = ModelFactory.create( model_platform=ModelPlatformType.OPENAI, - model_type=ModelType.GPT_3_5_TURBO, + model_type=ModelType.GPT_4O_MINI, model_config_dict=model_config_dict, ) @@ -50,7 +50,7 @@ def main(): content="You are a helpful assistant", ), model=model, - tools=function_list, + tools=tools_list, ) # set tour_guide_agent tour_guide_agent = ChatAgent( diff --git a/examples/workforce/role_playing_with_agents.py b/examples/workforce/role_playing_with_agents.py index 4292fe6c38..2903da521d 100644 --- a/examples/workforce/role_playing_with_agents.py +++ b/examples/workforce/role_playing_with_agents.py @@ -17,7 +17,7 @@ from camel.messages.base import BaseMessage from camel.models import ModelFactory from camel.tasks.task import Task -from camel.toolkits import MAP_FUNCS, SEARCH_FUNCS, WEATHER_FUNCS +from camel.toolkits import MathToolkit, SearchToolkit, WeatherToolkit from camel.types import ModelPlatformType, ModelType from camel.workforce.manager_node import ManagerNode from camel.workforce.role_playing_node import RolePlayingNode @@ -42,18 +42,18 @@ def main(): guide_worker_node = SingleAgentNode('tour guide', guide_agent) planner_worker_node = SingleAgentNode('planner', planner_agent) - function_list = [ - *SEARCH_FUNCS, - *WEATHER_FUNCS, - *MAP_FUNCS, + tools_list = [ + *MathToolkit().get_tools(), + *WeatherToolkit().get_tools(), + *SearchToolkit().get_tools(), ] user_model_config = ChatGPTConfig(temperature=0.0) assistant_model_config = ChatGPTConfig( - tools=function_list, + tools=tools_list, temperature=0.0, ) model_platform = ModelPlatformType.OPENAI - model_type = ModelType.GPT_3_5_TURBO + model_type = ModelType.GPT_4O_MINI assistant_role_name = "Searcher" user_role_name = "Professor" assistant_agent_kwargs = dict( @@ -62,7 +62,7 @@ def main(): model_type=model_type, model_config_dict=assistant_model_config.as_dict(), ), - tools=function_list, + tools=tools_list, ) user_agent_kwargs = dict( model=ModelFactory.create( diff --git a/test/agents/test_chat_agent.py b/test/agents/test_chat_agent.py index 6c43c7ed8d..a64445382b 100644 --- a/test/agents/test_chat_agent.py +++ b/test/agents/test_chat_agent.py @@ -33,9 +33,9 @@ from camel.models import ModelFactory from camel.terminators import ResponseWordsTerminator from camel.toolkits import ( - MATH_FUNCS, - SEARCH_FUNCS, + MathToolkit, OpenAIFunction, + SearchToolkit, ) from camel.types import ( ChatCompletion, @@ -161,8 +161,8 @@ class JokeResponse(BaseModel): @pytest.mark.model_backend def test_chat_agent_step_with_external_tools(): - internal_tools = SEARCH_FUNCS - external_tools = MATH_FUNCS + internal_tools = SearchToolkit().get_tools() + external_tools = MathToolkit().get_tools() tool_list = internal_tools + external_tools model_config_dict = ChatGPTConfig( @@ -172,7 +172,7 @@ def test_chat_agent_step_with_external_tools(): model = ModelFactory.create( model_platform=ModelPlatformType.OPENAI, - model_type=ModelType.GPT_3_5_TURBO, + model_type=ModelType.GPT_4O_MINI, model_config_dict=model_config_dict, ) @@ -421,7 +421,7 @@ def test_function_enabled(): meta_dict=None, content="You are a help assistant.", ) - model_config = ChatGPTConfig(tools=[*MATH_FUNCS]) + model_config = ChatGPTConfig(tools=MathToolkit().get_tools()) model = ModelFactory.create( model_platform=ModelPlatformType.OPENAI, model_type=ModelType.GPT_4O_MINI, @@ -431,7 +431,7 @@ def test_function_enabled(): agent_with_funcs = ChatAgent( system_message=system_message, model=model, - tools=MATH_FUNCS, + tools=MathToolkit().get_tools(), ) assert not agent_no_func.is_tools_added() @@ -446,7 +446,7 @@ def test_tool_calling_sync(): meta_dict=None, content="You are a help assistant.", ) - model_config = ChatGPTConfig(tools=[*MATH_FUNCS]) + model_config = ChatGPTConfig(tools=MathToolkit().get_tools()) model = ModelFactory.create( model_platform=ModelPlatformType.OPENAI, model_type=ModelType.GPT_4O_MINI, @@ -455,10 +455,10 @@ def test_tool_calling_sync(): agent = ChatAgent( system_message=system_message, model=model, - tools=MATH_FUNCS, + tools=MathToolkit().get_tools(), ) - ref_funcs = MATH_FUNCS + ref_funcs = MathToolkit().get_tools() assert len(agent.func_dict) == len(ref_funcs) @@ -491,7 +491,7 @@ async def test_tool_calling_math_async(): meta_dict=None, content="You are a help assistant.", ) - math_funcs = sync_funcs_to_async(MATH_FUNCS) + math_funcs = sync_funcs_to_async(MathToolkit().get_tools()) model_config = ChatGPTConfig(tools=[*math_funcs]) model = ModelFactory.create( model_platform=ModelPlatformType.OPENAI, diff --git a/test/agents/test_role_playing.py b/test/agents/test_role_playing.py index 77110e380e..72acf3ff62 100644 --- a/test/agents/test_role_playing.py +++ b/test/agents/test_role_playing.py @@ -19,7 +19,7 @@ from camel.messages import BaseMessage from camel.models import ModelFactory from camel.societies import RolePlaying -from camel.toolkits import MATH_FUNCS +from camel.toolkits import MathToolkit from camel.types import ModelPlatformType, ModelType, RoleType, TaskType model = ModelFactory.create( @@ -137,11 +137,11 @@ def test_role_playing_step( @pytest.mark.model_backend def test_role_playing_with_function(): - tools = [*MATH_FUNCS] + tools = MathToolkit().get_tools() assistant_model_config = ChatGPTConfig(tools=tools) model = ModelFactory.create( model_platform=ModelPlatformType.OPENAI, - model_type=ModelType.GPT_3_5_TURBO, + model_type=ModelType.GPT_4O_MINI, model_config_dict=assistant_model_config.as_dict(), ) diff --git a/test/models/test_litellm_model.py b/test/models/test_litellm_model.py index ead084875f..7d7b197d93 100644 --- a/test/models/test_litellm_model.py +++ b/test/models/test_litellm_model.py @@ -25,10 +25,10 @@ @pytest.mark.parametrize( "model_type", [ - ModelType.GPT_3_5_TURBO, ModelType.GPT_4, ModelType.GPT_4_TURBO, ModelType.GPT_4O, + ModelType.GPT_4O_MINI, ], ) def test_litellm_model(model_type: ModelType): diff --git a/test/models/test_ollama_model.py b/test/models/test_ollama_model.py index afc579f864..7750886a42 100644 --- a/test/models/test_ollama_model.py +++ b/test/models/test_ollama_model.py @@ -25,10 +25,10 @@ @pytest.mark.parametrize( "model_type", [ - ModelType.GPT_3_5_TURBO, ModelType.GPT_4, ModelType.GPT_4_TURBO, ModelType.GPT_4O, + ModelType.GPT_4O_MINI, ], ) def test_ollama_model(model_type: ModelType): diff --git a/test/models/test_open_source_model.py b/test/models/test_open_source_model.py index e48c03e998..7895f9d53f 100644 --- a/test/models/test_open_source_model.py +++ b/test/models/test_open_source_model.py @@ -81,7 +81,7 @@ def test_open_source_model_run(model_type): @pytest.mark.model_backend def test_open_source_model_close_source_model_type(): - model_type = ModelType.GPT_3_5_TURBO + model_type = ModelType.GPT_4O_MINI model_path = MODEL_PATH_MAP[ModelType.VICUNA] model_config = OpenSourceConfig( model_path=model_path, @@ -93,7 +93,7 @@ def test_open_source_model_close_source_model_type(): ValueError, match=re.escape( ( - "Model `ModelType.GPT_3_5_TURBO` is not a supported" + "Model `ModelType.GPT_4O_MINI` is not a supported" " open-source model." ) ), diff --git a/test/models/test_vllm_model.py b/test/models/test_vllm_model.py index 3375587440..f4dbc0485b 100644 --- a/test/models/test_vllm_model.py +++ b/test/models/test_vllm_model.py @@ -25,10 +25,10 @@ @pytest.mark.parametrize( "model_type", [ - ModelType.GPT_3_5_TURBO, ModelType.GPT_4, ModelType.GPT_4_TURBO, ModelType.GPT_4O, + ModelType.GPT_4O_MINI, ], ) def test_vllm_model(model_type: ModelType): From 0ad765348bf63cf4ccafa2e4f08ff24634e1ae20 Mon Sep 17 00:00:00 2001 From: Isaac Jin Date: Thu, 12 Sep 2024 22:50:41 -0700 Subject: [PATCH 03/34] partial refactoring --- camel/toolkits/__init__.py | 9 +- camel/toolkits/dalle_toolkit.py | 198 +++--- camel/toolkits/github_toolkit.py | 99 ++- camel/toolkits/linkedin_toolkit.py | 314 +++++----- camel/toolkits/math_toolkit.py | 77 +-- camel/toolkits/open_api_toolkit.py | 928 ++++++++++++++--------------- 6 files changed, 796 insertions(+), 829 deletions(-) diff --git a/camel/toolkits/__init__.py b/camel/toolkits/__init__.py index cf9d9cbf7b..3bd3f3de1b 100644 --- a/camel/toolkits/__init__.py +++ b/camel/toolkits/__init__.py @@ -21,14 +21,14 @@ from .google_maps_toolkit import GoogleMapsToolkit from .math_toolkit import MathToolkit -from .open_api_toolkit import OpenAPIToolkit +from .open_api_toolkit import OpenAPIToolkit, OPENAPI_FUNCS from .retrieval_toolkit import RetrievalToolkit from .search_toolkit import SearchToolkit from .twitter_toolkit import TwitterToolkit from .weather_toolkit import WeatherToolkit from .slack_toolkit import SlackToolkit -from .dalle_toolkit import DalleToolkit -from .linkedin_toolkit import LinkedInToolkit +from .dalle_toolkit import DalleToolkit, DALLE_FUNCS +from .linkedin_toolkit import LinkedInToolkit, LINKEDIN_FUNCS from .reddit_toolkit import RedditToolkit from .base import BaseToolkit @@ -54,4 +54,7 @@ 'LinkedInToolkit', 'RedditToolkit', 'CodeExecutionToolkit', + "DALLE_FUNCS", + "LINKEDIN_FUNCS", + "OPENAPI_FUNCS", ] diff --git a/camel/toolkits/dalle_toolkit.py b/camel/toolkits/dalle_toolkit.py index 77c745a365..67d54a7234 100644 --- a/camel/toolkits/dalle_toolkit.py +++ b/camel/toolkits/dalle_toolkit.py @@ -24,113 +24,115 @@ from camel.toolkits.base import BaseToolkit -class DalleToolkit(BaseToolkit): - r"""A class representing a toolkit for image generation using OpenAI's. +def _base64_to_image(base64_string: str) -> Optional[Image.Image]: + r"""Converts a base64 encoded string into a PIL Image object. - This class provides methods handle image generation using OpenAI's DALL-E. + Args: + base64_string (str): The base64 encoded string of the image. + + Returns: + Optional[Image.Image]: The PIL Image object or None if conversion + fails. + """ + try: + # Decode the base64 string to get the image data + image_data = base64.b64decode(base64_string) + # Create a memory buffer for the image data + image_buffer = BytesIO(image_data) + # Open the image using the PIL library + image = Image.open(image_buffer) + return image + except Exception as e: + print(f"An error occurred while converting base64 to image: {e}") + return None + + +def _image_path_to_base64(image_path: str) -> str: + r"""Converts the file path of an image to a Base64 encoded string. + + Args: + image_path (str): The path to the image file. + + Returns: + str: A Base64 encoded string representing the content of the image + file. + """ + try: + with open(image_path, "rb") as image_file: + return base64.b64encode(image_file.read()).decode('utf-8') + except Exception as e: + print(f"An error occurred while converting image path to base64: {e}") + return "" + + +def _image_to_base64(image: Image.Image) -> str: + r"""Converts an image into a base64-encoded string. This function takes + an image object as input, encodes the image into a PNG format base64 + string, and returns it. If the encoding process encounters an error, + it prints the error message and returns `None`. + + Args: + image (Image.Image): The image object to be encoded, supports any + image format that can be saved in PNG format. + + Returns: + str: A base64-encoded string of the image. + """ + try: + with BytesIO() as buffered_image: + image.save(buffered_image, format="PNG") + buffered_image.seek(0) + image_bytes = buffered_image.read() + base64_str = base64.b64encode(image_bytes).decode('utf-8') + return base64_str + except Exception as e: + print(f"An error occurred: {e}") + return "" + + +def get_dalle_img(prompt: str, image_dir: str = "img") -> str: + r"""Generate an image using OpenAI's DALL-E model. The generated image + is saved to the specified directory. + + Args: + prompt (str): The text prompt based on which the image is + generated. + image_dir (str): The directory to save the generated image. + Defaults to 'img'. + + Returns: + str: The path to the saved image. """ - def base64_to_image(self, base64_string: str) -> Optional[Image.Image]: - r"""Converts a base64 encoded string into a PIL Image object. + dalle_client = OpenAI() + response = dalle_client.images.generate( + model="dall-e-3", + prompt=prompt, + size="1024x1792", + quality="standard", + n=1, # NOTE: now dall-e-3 only supports n=1 + response_format="b64_json", + ) + image_b64 = response.data[0].b64_json + image = _base64_to_image(image_b64) # type: ignore[arg-type] - Args: - base64_string (str): The base64 encoded string of the image. + if image is None: + raise ValueError("Failed to convert base64 string to image.") - Returns: - Optional[Image.Image]: The PIL Image object or None if conversion - fails. - """ - try: - # Decode the base64 string to get the image data - image_data = base64.b64decode(base64_string) - # Create a memory buffer for the image data - image_buffer = BytesIO(image_data) - # Open the image using the PIL library - image = Image.open(image_buffer) - return image - except Exception as e: - print(f"An error occurred while converting base64 to image: {e}") - return None - - def image_path_to_base64(self, image_path: str) -> str: - r"""Converts the file path of an image to a Base64 encoded string. - - Args: - image_path (str): The path to the image file. + os.makedirs(image_dir, exist_ok=True) + image_path = os.path.join(image_dir, f"{uuid.uuid4()}.png") + image.save(image_path) - Returns: - str: A Base64 encoded string representing the content of the image - file. - """ - try: - with open(image_path, "rb") as image_file: - return base64.b64encode(image_file.read()).decode('utf-8') - except Exception as e: - print( - f"An error occurred while converting image path to base64: {e}" - ) - return "" - - def image_to_base64(self, image: Image.Image) -> str: - r"""Converts an image into a base64-encoded string. - - This function takes an image object as input, encodes the image into a - PNG format base64 string, and returns it. - If the encoding process encounters an error, it prints the error - message and returns None. - - Args: - image: The image object to be encoded, supports any image format - that can be saved in PNG format. + return image_path - Returns: - str: A base64-encoded string of the image. - """ - try: - with BytesIO() as buffered_image: - image.save(buffered_image, format="PNG") - buffered_image.seek(0) - image_bytes = buffered_image.read() - base64_str = base64.b64encode(image_bytes).decode('utf-8') - return base64_str - except Exception as e: - print(f"An error occurred: {e}") - return "" - - def get_dalle_img(self, prompt: str, image_dir: str = "img") -> str: - r"""Generate an image using OpenAI's DALL-E model. - The generated image is saved to the specified directory. - - Args: - prompt (str): The text prompt based on which the image is - generated. - image_dir (str): The directory to save the generated image. - Defaults to 'img'. - - Returns: - str: The path to the saved image. - """ - dalle_client = OpenAI() - response = dalle_client.images.generate( - model="dall-e-3", - prompt=prompt, - size="1024x1792", - quality="standard", - n=1, # NOTE: now dall-e-3 only supports n=1 - response_format="b64_json", - ) - image_b64 = response.data[0].b64_json - image = self.base64_to_image(image_b64) # type: ignore[arg-type] +DALLE_FUNCS = [OpenAIFunction(get_dalle_img)] - if image is None: - raise ValueError("Failed to convert base64 string to image.") - os.makedirs(image_dir, exist_ok=True) - image_path = os.path.join(image_dir, f"{uuid.uuid4()}.png") - image.save(image_path) - - return image_path +class DalleToolkit(BaseToolkit): + r"""A class representing a toolkit for image generation using OpenAI's. + This class provides methods handle image generation using OpenAI's DALL-E. + """ def get_tools(self) -> List[OpenAIFunction]: r"""Returns a list of OpenAIFunction objects representing the @@ -140,4 +142,4 @@ def get_tools(self) -> List[OpenAIFunction]: List[OpenAIFunction]: A list of OpenAIFunction objects representing the functions in the toolkit. """ - return [OpenAIFunction(self.get_dalle_img)] + return DALLE_FUNCS diff --git a/camel/toolkits/github_toolkit.py b/camel/toolkits/github_toolkit.py index 141d7706b6..e5f15247bf 100644 --- a/camel/toolkits/github_toolkit.py +++ b/camel/toolkits/github_toolkit.py @@ -18,10 +18,30 @@ from pydantic import BaseModel +from camel.toolkits.base import BaseToolkit +from camel.toolkits.openai_function import OpenAIFunction from camel.utils import dependencies_required -from .base import BaseToolkit -from .openai_function import OpenAIFunction + +def get_github_access_token() -> str: + r"""Retrieve the GitHub access token from environment variables. + + Returns: + str: A string containing the GitHub access token. + + Raises: + ValueError: If the API key or secret is not found in the environment + variables. + """ + # Get `GITHUB_ACCESS_TOKEN` here: https://github.com/settings/tokens + github_token = os.environ.get("GITHUB_ACCESS_TOKEN") + + if not github_token: + raise ValueError( + "`GITHUB_ACCESS_TOKEN` not found in environment variables. Get it " + "here: `https://github.com/settings/tokens`." + ) + return github_token class GithubIssue(BaseModel): @@ -69,18 +89,16 @@ class GithubPullRequestDiff(BaseModel): patch: str def __str__(self) -> str: - r"""Returns a string representation of this diff.""" return f"Filename: {self.filename}\nPatch: {self.patch}" class GithubPullRequest(BaseModel): - r"""Represents a pull request on Github. + r"""Represents a pull request on GitHub. Attributes: title (str): The title of the GitHub pull request. body (str): The body/content of the GitHub pull request. - diffs (List[GithubPullRequestDiff]): A list of diffs for the pull - request. + diffs (List[GithubPullRequestDiff]): A list of diffs for the PR. """ title: str @@ -88,7 +106,6 @@ class GithubPullRequest(BaseModel): diffs: List[GithubPullRequestDiff] def __str__(self) -> str: - r"""Returns a string representation of the pull request.""" diff_summaries = '\n'.join(str(diff) for diff in self.diffs) return ( f"Title: {self.title}\n" @@ -99,16 +116,15 @@ def __str__(self) -> str: class GithubToolkit(BaseToolkit): r"""A class representing a toolkit for interacting with GitHub - repositories. - - This class provides methods for retrieving open issues, retrieving - specific issues, and creating pull requests in a GitHub repository. + repositories. This class provides methods for retrieving open issues, + retrieving specific issues, and creating pull requests in a GitHub + repository. Args: repo_name (str): The name of the GitHub repository. - access_token (str, optional): The access token to authenticate with - GitHub. If not provided, it will be obtained using the - `get_github_access_token` method. + access_token (Optional[str], optional): The access token to + authenticate with GitHub. If not provided, it will be obtained + using the `get_github_access_token` method. (default: :obj:`None`) """ @dependencies_required('github') @@ -119,12 +135,13 @@ def __init__( Args: repo_name (str): The name of the GitHub repository. - access_token (str, optional): The access token to authenticate - with GitHub. If not provided, it will be obtained using the - `get_github_access_token` method. + access_token (Optional[str], optional): The access token to + authenticate with GitHub. If not provided, it will be obtained + using the `get_github_access_token` method. + (default: :obj:`None`) """ if access_token is None: - access_token = self.get_github_access_token() + access_token = get_github_access_token() from github import Auth, Github @@ -146,31 +163,12 @@ def get_tools(self) -> List[OpenAIFunction]: OpenAIFunction(self.retrieve_pull_requests), ] - def get_github_access_token(self) -> str: - r"""Retrieve the GitHub access token from environment variables. - - Returns: - str: A string containing the GitHub access token. - - Raises: - ValueError: If the API key or secret is not found in the - environment variables. - """ - # Get `GITHUB_ACCESS_TOKEN` here: https://github.com/settings/tokens - GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN") - - if not GITHUB_ACCESS_TOKEN: - raise ValueError( - "`GITHUB_ACCESS_TOKEN` not found in environment variables. Get" - " it here: `https://github.com/settings/tokens`." - ) - return GITHUB_ACCESS_TOKEN - def retrieve_issue_list(self) -> List[GithubIssue]: r"""Retrieve a list of open issues from the repository. Returns: - A list of GithubIssue objects representing the open issues. + List[GithubIssue]: A list of GithubIssue objects representing + the open issues. """ issues = self.repo.get_issues(state='open') return [ @@ -188,16 +186,14 @@ def retrieve_issue_list(self) -> List[GithubIssue]: ] def retrieve_issue(self, issue_number: int) -> Optional[str]: - r"""Retrieves an issue from a GitHub repository. - - This function retrieves an issue from a specified repository using the - issue number. + r"""Retrieves an issue from a GitHub repository. This function + retrieves an issue from a specified repository using the issue number. Args: issue_number (int): The number of the issue to retrieve. Returns: - str: A formatted report of the retrieved issue. + Optional[str]: A formatted report of the retrieved issue. """ issues = self.retrieve_issue_list() for issue in issues: @@ -212,10 +208,8 @@ def retrieve_pull_requests( The summary will be provided for the last specified number of days. Args: - days (int): The number of days to retrieve merged pull requests - for. - state (str): A specific state of PRs to retrieve. Can be open or - closed. + days (int): Number of days to retrieve merged PRs for. + state (str): The state of PRs to retrieve. Can be open or closed. max_prs (int): The maximum number of PRs to retrieve. Returns: @@ -255,11 +249,10 @@ def create_pull_request( body: str, branch_name: str, ) -> str: - r"""Creates a pull request. - - This function creates a pull request in specified repository, which - updates a file in the specific path with new content. The pull request - description contains information about the issue title and number. + r"""Creates a pull request. This function creates a pull request in + specified repository, which updates a file in the specific path with + new content. The pull request description contains information about + the issue title and number. Args: file_path (str): The path of the file to be updated in the diff --git a/camel/toolkits/linkedin_toolkit.py b/camel/toolkits/linkedin_toolkit.py index 1993849ce0..e45eba0bb5 100644 --- a/camel/toolkits/linkedin_toolkit.py +++ b/camel/toolkits/linkedin_toolkit.py @@ -15,184 +15,184 @@ import json import os from http import HTTPStatus -from typing import List +from typing import Dict, List import requests from camel.toolkits import OpenAIFunction from camel.toolkits.base import BaseToolkit -from camel.utils import handle_http_error +from camel.utils import api_keys_required, handle_http_error LINKEDIN_POST_LIMIT = 1300 -class LinkedInToolkit(BaseToolkit): - r"""A class representing a toolkit for LinkedIn operations. - - This class provides methods for creating a post, deleting a post, and - retrieving the authenticated user's profile information. - """ +@api_keys_required("LINKEDIN_ACCESS_TOKEN") +def create_post(text: str) -> dict: + r"""Creates a post on LinkedIn for the authenticated user. - def __init__(self): - self._access_token = self._get_access_token() + Args: + text (str): The content of the post to be created. - def create_post(self, text: str) -> dict: - r"""Creates a post on LinkedIn for the authenticated user. + Returns: + dict: A dictionary containing the post ID and the content of + the post. If the post creation fails, the values will be None. - Args: - text (str): The content of the post to be created. + Raises: + Exception: If the post creation fails due to an error response from + LinkedIn API. + """ + url = 'https://api.linkedin.com/v2/ugcPosts' + urn = get_profile(include_id=True) + + headers = { + 'X-Restli-Protocol-Version': '2.0.0', + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {os.getenv("LINKEDIN_ACCESS_TOKEN")}', + } + + post_data = { + "author": urn['id'], + "lifecycleState": "PUBLISHED", + "specificContent": { + "com.linkedin.ugc.ShareContent": { + "shareCommentary": {"text": text}, + "shareMediaCategory": "NONE", + } + }, + "visibility": {"com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"}, + } + + response = requests.post(url, headers=headers, data=json.dumps(post_data)) + if response.status_code == 201: + post_response = response.json() + post_id = post_response.get('id', None) # Get the ID of the post + return {'Post ID': post_id, 'Text': text} + else: + raise Exception( + f"Failed to create post. Status code: {response.status_code}, " + f"Response: {response.text}" + ) - Returns: - dict: A dictionary containing the post ID and the content of - the post. If the post creation fails, the values will be None. - Raises: - Exception: If the post creation fails due to - an error response from LinkedIn API. - """ - url = 'https://api.linkedin.com/v2/ugcPosts' - urn = self.get_profile(include_id=True) - - headers = { - 'X-Restli-Protocol-Version': '2.0.0', - 'Content-Type': 'application/json', - 'Authorization': f'Bearer {self._access_token}', - } - - post_data = { - "author": urn['id'], - "lifecycleState": "PUBLISHED", - "specificContent": { - "com.linkedin.ugc.ShareContent": { - "shareCommentary": {"text": text}, - "shareMediaCategory": "NONE", - } - }, - "visibility": { - "com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC" - }, - } - - response = requests.post( - url, headers=headers, data=json.dumps(post_data) - ) - if response.status_code == 201: - post_response = response.json() - post_id = post_response.get('id', None) # Get the ID of the post - return {'Post ID': post_id, 'Text': text} - else: - raise Exception( - f"Failed to create post. Status code: {response.status_code}, " - f"Response: {response.text}" - ) - - def delete_post(self, post_id: str) -> str: - r"""Deletes a LinkedIn post with the specified ID - for an authorized user. - - This function sends a DELETE request to the LinkedIn API to delete - a post with the specified ID. Before sending the request, it - prompts the user to confirm the deletion. - - Args: - post_id (str): The ID of the post to delete. +@api_keys_required("LINKEDIN_ACCESS_TOKEN") +def delete_post(post_id: str) -> str: + r"""Deletes a LinkedIn post with the specified ID + for an authorized user. This function sends a DELETE request to the + LinkedIn API to delete a post with the specified ID. Before sending the + request, it prompts the user to confirm the deletion. - Returns: - str: A message indicating the result of the deletion. If the - deletion was successful, the message includes the ID of the - deleted post. If the deletion was not successful, the message - includes an error message. + Args: + post_id (str): The ID of the post to delete. - Reference: - https://docs.microsoft.com/en-us/linkedin/marketing/integrations/community-management/shares/ugc-post-api - """ - print( - "You are going to delete a LinkedIn post " - f"with the following ID: {post_id}" - ) + Returns: + str: A message indicating the result of the deletion. If the + deletion was successful, the message includes the ID of the + deleted post. If the deletion was not successful, the message + includes an error message. - confirm = input( - "Are you sure you want to delete this post? (yes/no): " + Reference: + https://docs.microsoft.com/en-us/linkedin/marketing/integrations/community-management/shares/ugc-post-api + """ + print( + "You are going to delete a LinkedIn post " + f"with the following ID: {post_id}" + ) + + confirm = input("Are you sure you want to delete this post? (yes/no): ") + if confirm.lower() != "yes": + return "Execution cancelled by the user." + + headers = { + "Authorization": f"Bearer {os.getenv('LINKEDIN_ACCESS_TOKEN')}", + "Content-Type": "application/json", + } + + response = requests.delete( + f"https://api.linkedin.com/v2/ugcPosts/{post_id}", + headers=headers, + ) + + if response.status_code != HTTPStatus.NO_CONTENT: + error_type = handle_http_error(response) + return ( + f"Request returned a(n) {error_type!s}: " + f"{response.status_code!s} {response.text}" ) - if confirm.lower() != "yes": - return "Execution cancelled by the user." - headers = { - "Authorization": f"Bearer {self._access_token}", - "Content-Type": "application/json", - } + return f"Post deleted successfully. Post ID: {post_id}." - response = requests.delete( - f"https://api.linkedin.com/v2/ugcPosts/{post_id}", - headers=headers, - ) - if response.status_code != HTTPStatus.NO_CONTENT: - error_type = handle_http_error(response) - return ( - f"Request returned a(n) {error_type!s}: " - f"{response.status_code!s} {response.text}" - ) +@api_keys_required("LINKEDIN_ACCESS_TOKEN") +def get_profile(include_id: bool = False) -> Dict[str, str]: + r"""Retrieves the authenticated user's LinkedIn profile info. This + function sends a GET request to the LinkedIn API to retrieve the + authenticated user's profile information. Optionally, it also returns + the user's LinkedIn ID. - return f"Post deleted successfully. Post ID: {post_id}." + Args: + include_id (bool, optional): Whether to include the LinkedIn profile + ID in the response. (default: :obj:`False`) - def get_profile(self, include_id: bool = False) -> dict: - r"""Retrieves the authenticated user's LinkedIn profile info. + Returns: + dict: A dictionary containing the user's LinkedIn profile + information. If `include_id` is True, the dictionary will also + include the profile ID. - This function sends a GET request to the LinkedIn API to retrieve the - authenticated user's profile information. Optionally, it also returns - the user's LinkedIn ID. + Raises: + Exception: If the profile retrieval fails due to an error response + from LinkedIn API. + """ + headers = { + "Authorization": f"Bearer {os.getenv('LINKEDIN_ACCESS_TOKEN')}", + 'Connection': 'Keep-Alive', + 'Content-Type': 'application/json', + "X-Restli-Protocol-Version": "2.0.0", + } + + response = requests.get( + "https://api.linkedin.com/v2/userinfo", + headers=headers, + ) + + if response.status_code != HTTPStatus.OK: + raise Exception( + f"Failed to retrieve profile. " + f"Status code: {response.status_code}, " + f"Response: {response.text}" + ) - Args: - include_id (bool): Whether to include the LinkedIn profile ID in - the response. + json_response = response.json() - Returns: - dict: A dictionary containing the user's LinkedIn profile - information. If `include_id` is True, the dictionary will also - include the profile ID. + locale = json_response.get('locale', {}) + country = locale.get('country', 'N/A') + language = locale.get('language', 'N/A') - Raises: - Exception: If the profile retrieval fails due to an error response - from LinkedIn API. - """ - headers = { - "Authorization": f"Bearer {self._access_token}", - 'Connection': 'Keep-Alive', - 'Content-Type': 'application/json', - "X-Restli-Protocol-Version": "2.0.0", - } - - response = requests.get( - "https://api.linkedin.com/v2/userinfo", - headers=headers, - ) + profile_report = { + "Country": country, + "Language": language, + "First Name": json_response.get('given_name'), + "Last Name": json_response.get('family_name'), + "Email": json_response.get('email'), + } - if response.status_code != HTTPStatus.OK: - raise Exception( - f"Failed to retrieve profile. " - f"Status code: {response.status_code}, " - f"Response: {response.text}" - ) + if include_id: + profile_report['id'] = f"urn:li:person:{json_response['sub']}" - json_response = response.json() + return profile_report - locale = json_response.get('locale', {}) - country = locale.get('country', 'N/A') - language = locale.get('language', 'N/A') - profile_report = { - "Country": country, - "Language": language, - "First Name": json_response.get('given_name'), - "Last Name": json_response.get('family_name'), - "Email": json_response.get('email'), - } +LINKEDIN_FUNCS = [ + OpenAIFunction(create_post), + OpenAIFunction(delete_post), + OpenAIFunction(get_profile), +] - if include_id: - profile_report['id'] = f"urn:li:person:{json_response['sub']}" - return profile_report +class LinkedInToolkit(BaseToolkit): + r"""A class representing a toolkit for LinkedIn operations. This class + provides methods for creating a post, deleting a post, and retrieving + the authenticated user's profile information. + """ def get_tools(self) -> List[OpenAIFunction]: r"""Returns a list of OpenAIFunction objects representing the @@ -202,26 +202,4 @@ def get_tools(self) -> List[OpenAIFunction]: List[OpenAIFunction]: A list of OpenAIFunction objects representing the functions in the toolkit. """ - return [ - OpenAIFunction(self.create_post), - OpenAIFunction(self.delete_post), - OpenAIFunction(self.get_profile), - ] - - def _get_access_token(self) -> str: - r"""Fetches the access token required for making LinkedIn API requests. - - Returns: - str: The OAuth 2.0 access token or warming message if the - environment variable `LINKEDIN_ACCESS_TOKEN` is not set or is - empty. - - Reference: - You can apply for your personal LinkedIn API access token through - the link below: - https://www.linkedin.com/developers/apps - """ - token = os.getenv("LINKEDIN_ACCESS_TOKEN") - if not token: - return "Access token not found. Please set LINKEDIN_ACCESS_TOKEN." - return token + return LINKEDIN_FUNCS diff --git a/camel/toolkits/math_toolkit.py b/camel/toolkits/math_toolkit.py index d8391e9770..57c02d1a22 100644 --- a/camel/toolkits/math_toolkit.py +++ b/camel/toolkits/math_toolkit.py @@ -18,48 +18,57 @@ from camel.toolkits.openai_function import OpenAIFunction -class MathToolkit(BaseToolkit): - r"""A class representing a toolkit for mathematical operations. +def add(a: int, b: int) -> int: + r"""Adds two numbers. + + Args: + a (int): The first number to be added. + b (int): The second number to be added. - This class provides methods for basic mathematical operations such as - addition, subtraction, and multiplication. + Returns: + int: The sum of the two numbers. """ + return a + b - def add(self, a: int, b: int) -> int: - r"""Adds two numbers. - Args: - a (int): The first number to be added. - b (int): The second number to be added. +def sub(a: int, b: int) -> int: + r"""Do subtraction between two numbers. - Returns: - integer: The sum of the two numbers. - """ - return a + b + Args: + a (int): The minuend in subtraction. + b (int): The subtrahend in subtraction. - def sub(self, a: int, b: int) -> int: - r"""Do subtraction between two numbers. + Returns: + int: The result of subtracting :paramref:`b` from :paramref:`a`. + """ + return a - b - Args: - a (int): The minuend in subtraction. - b (int): The subtrahend in subtraction. - Returns: - integer: The result of subtracting :obj:`b` from :obj:`a`. - """ - return a - b +def mul(a: int, b: int) -> int: + r"""Multiplies two integers. - def mul(self, a: int, b: int) -> int: - r"""Multiplies two integers. + Args: + a (int): The multiplier in the multiplication. + b (int): The multiplicand in the multiplication. - Args: - a (int): The multiplier in the multiplication. - b (int): The multiplicand in the multiplication. + Returns: + int: The product of the two numbers. + """ + return a * b - Returns: - integer: The product of the two numbers. - """ - return a * b + +MATH_FUNCS = [ + OpenAIFunction(add), + OpenAIFunction(sub), + OpenAIFunction(mul), +] + + +class MathToolkit(BaseToolkit): + r"""A class representing a toolkit for mathematical operations. This + class provides methods for basic mathematical operations such as addition, + subtraction, and multiplication. + """ def get_tools(self) -> List[OpenAIFunction]: r"""Returns a list of OpenAIFunction objects representing the @@ -69,8 +78,4 @@ def get_tools(self) -> List[OpenAIFunction]: List[OpenAIFunction]: A list of OpenAIFunction objects representing the functions in the toolkit. """ - return [ - OpenAIFunction(self.add), - OpenAIFunction(self.sub), - OpenAIFunction(self.mul), - ] + return MATH_FUNCS diff --git a/camel/toolkits/open_api_toolkit.py b/camel/toolkits/open_api_toolkit.py index 10f90f4bba..ef98a952ac 100644 --- a/camel/toolkits/open_api_toolkit.py +++ b/camel/toolkits/open_api_toolkit.py @@ -17,514 +17,507 @@ import requests -from camel.toolkits import OpenAIFunction, openapi_security_config +from camel.toolkits import BaseToolkit, OpenAIFunction, openapi_security_config from camel.types import OpenAPIName -class OpenAPIToolkit: - r"""A class representing a toolkit for interacting with OpenAPI APIs. - - This class provides methods for interacting with APIs based on OpenAPI - specifications. It dynamically generates functions for each API operation - defined in the OpenAPI specification, allowing users to make HTTP requests - to the API endpoints. - """ - - def parse_openapi_file( - self, openapi_spec_path: str - ) -> Optional[Dict[str, Any]]: - r"""Load and parse an OpenAPI specification file. +def parse_openapi_file(openapi_spec_path: str) -> Optional[Dict[str, Any]]: + r"""Load and parse an OpenAPI specification file. This function utilizes + the `prance.ResolvingParser` to parse and resolve the given OpenAPI + specification file, returning the parsed OpenAPI specification as a + dictionary. - This function utilizes the `prance.ResolvingParser` to parse and - resolve the given OpenAPI specification file, returning the parsed - OpenAPI specification as a dictionary. + Args: + openapi_spec_path (str): The file path or URL to the OpenAPI + specification. - Args: - openapi_spec_path (str): The file path or URL to the OpenAPI - specification. - - Returns: - Optional[Dict[str, Any]]: The parsed OpenAPI specification - as a dictionary. :obj:`None` if the package is not installed. - """ - try: - import prance - except Exception: - return None - - # Load the OpenAPI spec - parser = prance.ResolvingParser( - openapi_spec_path, backend="openapi-spec-validator", strict=False + Returns: + Optional[Dict[str, Any]]: The parsed OpenAPI specification + as a dictionary. :obj:`None` if the package is not installed. + """ + try: + import prance + except Exception: + return None + + # Load the OpenAPI spec + parser = prance.ResolvingParser( + openapi_spec_path, backend="openapi-spec-validator", strict=False + ) + openapi_spec = parser.specification + version = openapi_spec.get('openapi', {}) + if not version: + raise ValueError( + "OpenAPI version not specified in the spec. " + "Only OPENAPI 3.0.x and 3.1.x are supported." ) - openapi_spec = parser.specification - version = openapi_spec.get('openapi', {}) - if not version: - raise ValueError( - "OpenAPI version not specified in the spec. " - "Only OPENAPI 3.0.x and 3.1.x are supported." - ) - if not (version.startswith('3.0') or version.startswith('3.1')): - raise ValueError( - f"Unsupported OpenAPI version: {version}. " - f"Only OPENAPI 3.0.x and 3.1.x are supported." - ) - return openapi_spec - - def openapi_spec_to_openai_schemas( - self, api_name: str, openapi_spec: Dict[str, Any] - ) -> List[Dict[str, Any]]: - r"""Convert OpenAPI specification to OpenAI schema format. - - This function iterates over the paths and operations defined in an - OpenAPI specification, filtering out deprecated operations. For each - operation, it constructs a schema in a format suitable for OpenAI, - including operation metadata such as function name, description, - parameters, and request bodies. It raises a ValueError if an operation - lacks a description or summary. - - Args: - api_name (str): The name of the API, used to prefix generated - function names. - openapi_spec (Dict[str, Any]): The OpenAPI specification as a - dictionary. - - Returns: - List[Dict[str, Any]]: A list of dictionaries, each representing a - function in the OpenAI schema format, including details about - the function's name, description, and parameters. - - Raises: - ValueError: If an operation in the OpenAPI specification - does not have a description or summary. - - Note: - This function assumes that the OpenAPI specification - follows the 3.0+ format. - - Reference: - https://swagger.io/specification/ - """ - result = [] - - for path, path_item in openapi_spec.get('paths', {}).items(): - for method, op in path_item.items(): - if op.get('deprecated') is True: - continue - - # Get the function name from the operationId - # or construct it from the API method, and path - function_name = f"{api_name}" - operation_id = op.get('operationId') - if operation_id: - function_name += f"_{operation_id}" - else: - function_name += f"{method}{path.replace('/', '_')}" - - description = op.get('description') or op.get('summary') - if not description: - raise ValueError( - f"{method} {path} Operation from {api_name} " - f"does not have a description or summary." + if not (version.startswith('3.0') or version.startswith('3.1')): + raise ValueError( + f"Unsupported OpenAPI version: {version}. " + f"Only OPENAPI 3.0.x and 3.1.x are supported." + ) + return openapi_spec + + +def openapi_spec_to_openai_schemas( + api_name: str, openapi_spec: Dict[str, Any] +) -> List[Dict[str, Any]]: + r"""Convert OpenAPI specification to OpenAI schema format. + + This function iterates over the paths and operations defined in an + OpenAPI specification, filtering out deprecated operations. For each + operation, it constructs a schema in a format suitable for OpenAI, + including operation metadata such as function name, description, + parameters, and request bodies. It raises a ValueError if an operation + lacks a description or summary. + + Args: + api_name (str): The name of the API, used to prefix generated + function names. + openapi_spec (Dict[str, Any]): The OpenAPI specification as a + dictionary. + + Returns: + List[Dict[str, Any]]: A list of dictionaries, each representing a + function in the OpenAI schema format, including details about + the function's name, description, and parameters. + + Raises: + ValueError: If an operation in the OpenAPI specification does not have + a description or summary. + + Note: + This function assumes that the OpenAPI specification follows the 3.0+ + format. + + Reference: + https://swagger.io/specification/ + """ + result = [] + + for path, path_item in openapi_spec.get('paths', {}).items(): + for method, op in path_item.items(): + if op.get('deprecated') is True: + continue + + # Get the function name from the operationId + # or construct it from the API method, and path + function_name = f"{api_name}" + operation_id = op.get('operationId') + if operation_id: + function_name += f"_{operation_id}" + else: + function_name += f"{method}{path.replace('/', '_')}" + + description = op.get('description') or op.get('summary') + if not description: + raise ValueError( + f"{method} {path} Operation from {api_name} " + f"does not have a description or summary." + ) + description += " " if description[-1] != " " else "" + description += f"This function is from {api_name} API. " + + # If the OpenAPI spec has a description, + # add it to the operation description + if 'description' in openapi_spec.get('info', {}): + description += f"{openapi_spec['info']['description']}" + + # Get the parameters for the operation, if any + params = op.get('parameters', []) + properties: Dict[str, Any] = {} + required = [] + + for param in params: + if not param.get('deprecated', False): + param_name = param['name'] + '_in_' + param['in'] + properties[param_name] = {} + + if 'description' in param: + properties[param_name]['description'] = param[ + 'description' + ] + + if 'schema' in param: + if ( + properties[param_name].get('description') + and 'description' in param['schema'] + ): + param['schema'].pop('description') + properties[param_name].update(param['schema']) + + if param.get('required'): + required.append(param_name) + + # If the property dictionary does not have a + # description, use the parameter name as + # the description + if 'description' not in properties[param_name]: + properties[param_name]['description'] = param['name'] + + if 'type' not in properties[param_name]: + properties[param_name]['type'] = 'Any' + + # Process requestBody if present + if 'requestBody' in op: + properties['requestBody'] = {} + request_body = op['requestBody'] + if request_body.get('required') is True: + required.append('requestBody') + + content = request_body.get('content', {}) + json_content = content.get('application/json', {}) + json_schema = json_content.get('schema', {}) + if json_schema: + properties['requestBody'] = json_schema + if 'description' not in properties['requestBody']: + properties['requestBody']['description'] = ( + "The request body, with parameters specifically " + "described under the `properties` key" ) - description += " " if description[-1] != " " else "" - description += f"This function is from {api_name} API. " - - # If the OpenAPI spec has a description, - # add it to the operation description - if 'description' in openapi_spec.get('info', {}): - description += f"{openapi_spec['info']['description']}" - - # Get the parameters for the operation, if any - params = op.get('parameters', []) - properties: Dict[str, Any] = {} - required = [] - - for param in params: - if not param.get('deprecated', False): - param_name = param['name'] + '_in_' + param['in'] - properties[param_name] = {} - - if 'description' in param: - properties[param_name]['description'] = param[ - 'description' - ] - - if 'schema' in param: - if ( - properties[param_name].get('description') - and 'description' in param['schema'] - ): - param['schema'].pop('description') - properties[param_name].update(param['schema']) - - if param.get('required'): - required.append(param_name) - - # If the property dictionary does not have a - # description, use the parameter name as - # the description - if 'description' not in properties[param_name]: - properties[param_name]['description'] = param[ - 'name' - ] - - if 'type' not in properties[param_name]: - properties[param_name]['type'] = 'Any' - - # Process requestBody if present - if 'requestBody' in op: - properties['requestBody'] = {} - requestBody = op['requestBody'] - if requestBody.get('required') is True: - required.append('requestBody') - - content = requestBody.get('content', {}) - json_content = content.get('application/json', {}) - json_schema = json_content.get('schema', {}) - if json_schema: - properties['requestBody'] = json_schema - if 'description' not in properties['requestBody']: - properties['requestBody']['description'] = ( - "The request body, with parameters specifically " - "described under the `properties` key" - ) - function = { - "type": "function", - "function": { - "name": function_name, - "description": description, - "parameters": { - "type": "object", - "properties": properties, - "required": required, - }, + function = { + "type": "function", + "function": { + "name": function_name, + "description": description, + "parameters": { + "type": "object", + "properties": properties, + "required": required, }, - } - result.append(function) - - return result # Return the result list - - def openapi_function_decorator( - self, - api_name: str, - base_url: str, - path: str, - method: str, - openapi_security: List[Dict[str, Any]], - sec_schemas: Dict[str, Dict[str, Any]], - operation: Dict[str, Any], - ) -> Callable: - r"""Decorate a function to make HTTP requests based on OpenAPI - specification details. - - This decorator dynamically constructs and executes an API request based - on the provided OpenAPI operation specifications, security - requirements, and parameters. It supports operations secured with - `apiKey` type security schemes and automatically injects the necessary - API keys from environment variables. Parameters in `path`, `query`, - `header`, and `cookie` are also supported. - - Args: - api_name (str): The name of the API, used to retrieve API key names - and URLs from the configuration. - base_url (str): The base URL for the API. - path (str): The path for the API endpoint, - relative to the base URL. - method (str): The HTTP method (e.g., 'get', 'post') - for the request. - openapi_security (List[Dict[str, Any]]): The global security - definitions as specified in the OpenAPI specs. - sec_schemas (Dict[str, Dict[str, Any]]): Detailed security schemes. - operation (Dict[str, Any]): A dictionary containing the OpenAPI - operation details, including parameters and request body - definitions. - - Returns: - Callable: A decorator that, when applied to a function, enables the - function to make HTTP requests based on the provided OpenAPI - operation details. - - Raises: - TypeError: If the security requirements include unsupported types. - ValueError: If required API keys are missing from environment - variables or if the content type of the request body is - unsupported. - """ + }, + } + result.append(function) + + return result # Return the result list + + +def openapi_function_decorator( + api_name: str, + base_url: str, + path: str, + method: str, + openapi_security: List[Dict[str, Any]], + sec_schemas: Dict[str, Dict[str, Any]], + operation: Dict[str, Any], +) -> Callable: + r"""Decorate a function to make HTTP requests based on OpenAPI + specification details. + + This decorator dynamically constructs and executes an API request based + on the provided OpenAPI operation specifications, security + requirements, and parameters. It supports operations secured with + `apiKey` type security schemes and automatically injects the necessary + API keys from environment variables. Parameters in `path`, `query`, + `header`, and `cookie` are also supported. + + Args: + api_name (str): The name of the API, used to retrieve API key names + and URLs from the configuration. + base_url (str): The base URL for the API. + path (str): The path for the API endpoint, + relative to the base URL. + method (str): The HTTP method (e.g., 'get', 'post') + for the request. + openapi_security (List[Dict[str, Any]]): The global security + definitions as specified in the OpenAPI specs. + sec_schemas (Dict[str, Dict[str, Any]]): Detailed security schemes. + operation (Dict[str, Any]): A dictionary containing the OpenAPI + operation details, including parameters and request body + definitions. + + Returns: + Callable: A decorator that, when applied to a function, enables the + function to make HTTP requests based on the provided OpenAPI + operation details. + + Raises: + TypeError: If the security requirements include unsupported types. + ValueError: If required API keys are missing from environment + variables or if the content type of the request body is + unsupported. + """ - def inner_decorator(openapi_function: Callable) -> Callable: - def wrapper(**kwargs): - request_url = f"{base_url.rstrip('/')}/{path.lstrip('/')}" - headers = {} - params = {} - cookies = {} - - # Security definition of operation overrides any declared - # top-level security. - sec_requirements = operation.get('security', openapi_security) - avail_sec_requirement = {} - # Write to avaliable_security_requirement only if all the - # security_type are "apiKey" - for security_requirement in sec_requirements: - have_unsupported_type = False - for sec_scheme_name, _ in security_requirement.items(): - sec_type = sec_schemas.get(sec_scheme_name).get('type') - if sec_type != "apiKey": - have_unsupported_type = True - break - if have_unsupported_type is False: - avail_sec_requirement = security_requirement + def inner_decorator(openapi_function: Callable) -> Callable: + def wrapper(**kwargs): + request_url = f"{base_url.rstrip('/')}/{path.lstrip('/')}" + headers = {} + params = {} + cookies = {} + + # Security definition of operation overrides any declared + # top-level security. + sec_requirements = operation.get('security', openapi_security) + avail_sec_requirement = {} + # Write to avaliable_security_requirement only if all the + # security_type are "apiKey" + for security_requirement in sec_requirements: + have_unsupported_type = False + for sec_scheme_name, _ in security_requirement.items(): + sec_type = sec_schemas.get(sec_scheme_name).get('type') + if sec_type != "apiKey": + have_unsupported_type = True break + if have_unsupported_type is False: + avail_sec_requirement = security_requirement + break - if sec_requirements and not avail_sec_requirement: - raise TypeError( - "Only security schemas of type `apiKey` are supported." - ) + if sec_requirements and not avail_sec_requirement: + raise TypeError( + "Only security schemas of type `apiKey` are supported." + ) - for sec_scheme_name, _ in avail_sec_requirement.items(): - try: - API_KEY_NAME = openapi_security_config.get( - api_name - ).get(sec_scheme_name) - api_key_value = os.environ[API_KEY_NAME] - except Exception: - api_key_url = openapi_security_config.get( - api_name - ).get('get_api_key_url') - raise ValueError( - f"`{API_KEY_NAME}` not found in environment " - f"variables. " - f"Get `{API_KEY_NAME}` here: {api_key_url}" - ) - request_key_name = sec_schemas.get(sec_scheme_name).get( - 'name' + for sec_scheme_name, _ in avail_sec_requirement.items(): + try: + API_KEY_NAME = openapi_security_config.get(api_name).get( + sec_scheme_name ) - request_key_in = sec_schemas.get(sec_scheme_name).get('in') - if request_key_in == 'query': - params[request_key_name] = api_key_value - elif request_key_in == 'header': - headers[request_key_name] = api_key_value - elif request_key_in == 'coolie': - cookies[request_key_name] = api_key_value - - # Assign parameters to the correct position - for param in operation.get('parameters', []): - input_param_name = param['name'] + '_in_' + param['in'] - # Irrelevant arguments does not affect function operation - if input_param_name in kwargs: - if param['in'] == 'path': - request_url = request_url.replace( - f"{{{param['name']}}}", - str(kwargs[input_param_name]), - ) - elif param['in'] == 'query': - params[param['name']] = kwargs[input_param_name] - elif param['in'] == 'header': - headers[param['name']] = kwargs[input_param_name] - elif param['in'] == 'cookie': - cookies[param['name']] = kwargs[input_param_name] - - if 'requestBody' in operation: - request_body = kwargs.get('requestBody', {}) - content_type_list = list( - operation.get('requestBody', {}) - .get('content', {}) - .keys() + api_key_value = os.environ[API_KEY_NAME] + except Exception: + api_key_url = openapi_security_config.get(api_name).get( + 'get_api_key_url' ) - if content_type_list: - content_type = content_type_list[0] - headers.update({"Content-Type": content_type}) - - # send the request body based on the Content-Type - if content_type == "application/json": - response = requests.request( - method.upper(), - request_url, - params=params, - headers=headers, - cookies=cookies, - json=request_body, - ) - else: - raise ValueError( - f"Unsupported content type: {content_type}" + raise ValueError( + f"`{API_KEY_NAME}` not found in environment " + f"variables. " + f"Get `{API_KEY_NAME}` here: {api_key_url}" + ) + request_key_name = sec_schemas.get(sec_scheme_name).get('name') + request_key_in = sec_schemas.get(sec_scheme_name).get('in') + if request_key_in == 'query': + params[request_key_name] = api_key_value + elif request_key_in == 'header': + headers[request_key_name] = api_key_value + elif request_key_in == 'coolie': + cookies[request_key_name] = api_key_value + + # Assign parameters to the correct position + for param in operation.get('parameters', []): + input_param_name = param['name'] + '_in_' + param['in'] + # Irrelevant arguments does not affect function operation + if input_param_name in kwargs: + if param['in'] == 'path': + request_url = request_url.replace( + f"{{{param['name']}}}", + str(kwargs[input_param_name]), ) - else: - # If there is no requestBody, no request body is sent + elif param['in'] == 'query': + params[param['name']] = kwargs[input_param_name] + elif param['in'] == 'header': + headers[param['name']] = kwargs[input_param_name] + elif param['in'] == 'cookie': + cookies[param['name']] = kwargs[input_param_name] + + if 'requestBody' in operation: + request_body = kwargs.get('requestBody', {}) + content_type_list = list( + operation.get('requestBody', {}).get('content', {}).keys() + ) + if content_type_list: + content_type = content_type_list[0] + headers.update({"Content-Type": content_type}) + + # send the request body based on the Content-Type + if content_type == "application/json": response = requests.request( method.upper(), request_url, params=params, headers=headers, cookies=cookies, + json=request_body, ) - - try: - return response.json() - except json.JSONDecodeError: + else: raise ValueError( - "Response could not be decoded as JSON. " - "Please check the input parameters." + f"Unsupported content type: {content_type}" ) + else: + # If there is no requestBody, no request body is sent + response = requests.request( + method.upper(), + request_url, + params=params, + headers=headers, + cookies=cookies, + ) - return wrapper + try: + return response.json() + except json.JSONDecodeError: + raise ValueError( + "Response could not be decoded as JSON. " + "Please check the input parameters." + ) - return inner_decorator + return wrapper + + return inner_decorator + + +def generate_openapi_funcs( + api_name: str, openapi_spec: Dict[str, Any] +) -> List[Callable]: + r"""Generates a list of Python functions based on OpenAPI + specification. This function dynamically creates a list of callable + functions that represent the API operations defined in an OpenAPI + specification document. Each function is designed to perform an HTTP + request corresponding to an API operation (e.g., GET, POST) as defined + in the specification. The functions are decorated with + `openapi_function_decorator`, which configures them to construct and + send the HTTP requests with appropriate parameters, headers, and body + content. + + Args: + api_name (str): The name of the API, used to prefix generated + function names. + openapi_spec (Dict[str, Any]): The OpenAPI specification as a + dictionary. + + Returns: + List[Callable]: A list containing the generated functions. Each + function, when called, will make an HTTP request according to + its corresponding API operation defined in the OpenAPI + specification. + + Raises: + ValueError: If the OpenAPI specification does not contain server + information, which is necessary for determining the base URL + for the API requests. + """ + # Check server information + servers = openapi_spec.get('servers', []) + if not servers: + raise ValueError("No server information found in OpenAPI spec.") + base_url = servers[0].get('url') # Use the first server URL + + # Security requirement objects for all methods + openapi_security = openapi_spec.get('security', {}) + # Security schemas which can be reused by different methods + sec_schemas = openapi_spec.get('components', {}).get('securitySchemes', {}) + functions = [] + + # Traverse paths and methods + for path, methods in openapi_spec.get('paths', {}).items(): + for method, operation in methods.items(): + # Get the function name from the operationId + # or construct it from the API method, and path + operation_id = operation.get('operationId') + if operation_id: + function_name = f"{api_name}_{operation_id}" + else: + sanitized_path = path.replace('/', '_').strip('_') + function_name = f"{api_name}_{method}_{sanitized_path}" + + @openapi_function_decorator( + api_name, + base_url, + path, + method, + openapi_security, + sec_schemas, + operation, + ) + def openapi_function(**kwargs): + pass - def generate_openapi_funcs( - self, api_name: str, openapi_spec: Dict[str, Any] - ) -> List[Callable]: - r"""Generates a list of Python functions based on - OpenAPI specification. + openapi_function.__name__ = function_name - This function dynamically creates a list of callable functions that - represent the API operations defined in an OpenAPI specification - document. Each function is designed to perform an HTTP request - corresponding to an API operation (e.g., GET, POST) as defined in - the specification. The functions are decorated with - `openapi_function_decorator`, which configures them to construct and - send the HTTP requests with appropriate parameters, headers, and body - content. + functions.append(openapi_function) - Args: - api_name (str): The name of the API, used to prefix generated - function names. - openapi_spec (Dict[str, Any]): The OpenAPI specification as a - dictionary. + return functions - Returns: - List[Callable]: A list containing the generated functions. Each - function, when called, will make an HTTP request according to - its corresponding API operation defined in the OpenAPI - specification. - - Raises: - ValueError: If the OpenAPI specification does not contain server - information, which is necessary for determining the base URL - for the API requests. - """ - # Check server information - servers = openapi_spec.get('servers', []) - if not servers: - raise ValueError("No server information found in OpenAPI spec.") - base_url = servers[0].get('url') # Use the first server URL - - # Security requirement objects for all methods - openapi_security = openapi_spec.get('security', {}) - # Security schemas which can be reused by different methods - sec_schemas = openapi_spec.get('components', {}).get( - 'securitySchemes', {} - ) - functions = [] - - # Traverse paths and methods - for path, methods in openapi_spec.get('paths', {}).items(): - for method, operation in methods.items(): - # Get the function name from the operationId - # or construct it from the API method, and path - operation_id = operation.get('operationId') - if operation_id: - function_name = f"{api_name}_{operation_id}" - else: - sanitized_path = path.replace('/', '_').strip('_') - function_name = f"{api_name}_{method}_{sanitized_path}" - - @self.openapi_function_decorator( - api_name, - base_url, - path, - method, - openapi_security, - sec_schemas, - operation, - ) - def openapi_function(**kwargs): - pass - openapi_function.__name__ = function_name +def apinames_filepaths_to_funs_schemas( + apinames_filepaths: List[Tuple[str, str]], +) -> Tuple[List[Callable], List[Dict[str, Any]]]: + r"""Combines functions and schemas from multiple OpenAPI + specifications, using API names as keys. This function iterates over + tuples of API names and OpenAPI spec file paths, parsing each spec + to generate callable functions and schema dictionaries, all organized + by API name. - functions.append(openapi_function) + Args: + apinames_filepaths (List[Tuple[str, str]]): A list of tuples, where + each tuple consists of: + - The API name (str) as the first element. + - The file path (str) to the API's OpenAPI specification file as + the second element. - return functions + Returns: + Tuple[List[Callable], List[Dict[str, Any]]]:: one of callable + functions for API operations, and another of dictionaries + representing the schemas from the specifications. + """ + combined_func_lst = [] + combined_schemas_list = [] + for api_name, file_path in apinames_filepaths: + # Parse the OpenAPI specification for each API + current_dir = os.path.dirname(__file__) + file_path = os.path.join( + current_dir, 'open_api_specs', f'{api_name}', 'openapi.yaml' + ) - def apinames_filepaths_to_funs_schemas( - self, - apinames_filepaths: List[Tuple[str, str]], - ) -> Tuple[List[Callable], List[Dict[str, Any]]]: - r"""Combines functions and schemas from multiple OpenAPI - specifications, using API names as keys. + openapi_spec = parse_openapi_file(file_path) + if openapi_spec is None: + return [], [] - This function iterates over tuples of API names and OpenAPI spec file - paths, parsing each spec to generate callable functions and schema - dictionaries, all organized by API name. + # Generate and merge function schemas + openapi_functions_schemas = openapi_spec_to_openai_schemas( + api_name, openapi_spec + ) + combined_schemas_list.extend(openapi_functions_schemas) - Args: - apinames_filepaths (List[Tuple[str, str]]): A list of tuples, where - each tuple consists of: - - The API name (str) as the first element. - - The file path (str) to the API's OpenAPI specification file as - the second element. + # Generate and merge function lists + openapi_functions_list = generate_openapi_funcs(api_name, openapi_spec) + combined_func_lst.extend(openapi_functions_list) - Returns: - Tuple[List[Callable], List[Dict[str, Any]]]:: one of callable - functions for API operations, and another of dictionaries - representing the schemas from the specifications. - """ - combined_func_lst = [] - combined_schemas_list = [] - for api_name, file_path in apinames_filepaths: - # Parse the OpenAPI specification for each API - current_dir = os.path.dirname(__file__) - file_path = os.path.join( - current_dir, 'open_api_specs', f'{api_name}', 'openapi.yaml' - ) + return combined_func_lst, combined_schemas_list - openapi_spec = self.parse_openapi_file(file_path) - if openapi_spec is None: - return [], [] - # Generate and merge function schemas - openapi_functions_schemas = self.openapi_spec_to_openai_schemas( - api_name, openapi_spec - ) - combined_schemas_list.extend(openapi_functions_schemas) +def generate_apinames_filepaths() -> List[Tuple[str, str]]: + """Generates a list of tuples containing API names and their + corresponding file paths. This function iterates over the OpenAPIName + enum, constructs the file path for each API's OpenAPI specification + file, and appends a tuple of the API name and its file path to the list. + The file paths are relative to the 'open_api_specs' directory located in + the same directory as this script. - # Generate and merge function lists - openapi_functions_list = self.generate_openapi_funcs( - api_name, openapi_spec - ) - combined_func_lst.extend(openapi_functions_list) + Returns: + List[Tuple[str, str]]: A list of tuples where each tuple contains + two elements. The first element of each tuple is a string + representing the name of an API, and the second element is a + string that specifies the file path to that API's OpenAPI + specification file. + """ + apinames_filepaths = [] + current_dir = os.path.dirname(__file__) + for api_name in OpenAPIName: + file_path = os.path.join( + current_dir, + 'open_api_specs', + f'{api_name.value}', + 'openapi.yaml', + ) + apinames_filepaths.append((api_name.value, file_path)) + return apinames_filepaths - return combined_func_lst, combined_schemas_list - def generate_apinames_filepaths(self) -> List[Tuple[str, str]]: - """Generates a list of tuples containing API names and their - corresponding file paths. +filepaths = generate_apinames_filepaths() +all_funcs_lst, all_schemas_lst = apinames_filepaths_to_funs_schemas(filepaths) +OPENAPI_FUNCS = [ + OpenAIFunction(a_func, a_schema) + for a_func, a_schema in zip(all_funcs_lst, all_schemas_lst) +] - This function iterates over the OpenAPIName enum, constructs the file - path for each API's OpenAPI specification file, and appends a tuple of - the API name and its file path to the list. The file paths are relative - to the 'open_api_specs' directory located in the same directory as this - script. - Returns: - List[Tuple[str, str]]: A list of tuples where each tuple contains - two elements. The first element of each tuple is a string - representing the name of an API, and the second element is a - string that specifies the file path to that API's OpenAPI - specification file. - """ - apinames_filepaths = [] - current_dir = os.path.dirname(__file__) - for api_name in OpenAPIName: - file_path = os.path.join( - current_dir, - 'open_api_specs', - f'{api_name.value}', - 'openapi.yaml', - ) - apinames_filepaths.append((api_name.value, file_path)) - return apinames_filepaths +class OpenAPIToolkit(BaseToolkit): + r"""A class representing a toolkit for interacting with OpenAPI APIs. + This class provides methods for interacting with APIs based on OpenAPI + specifications. It dynamically generates functions for each API operation + defined in the OpenAPI specification, allowing users to make HTTP requests + to the API endpoints. + """ def get_tools(self) -> List[OpenAIFunction]: r"""Returns a list of OpenAIFunction objects representing the @@ -534,11 +527,4 @@ def get_tools(self) -> List[OpenAIFunction]: List[OpenAIFunction]: A list of OpenAIFunction objects representing the functions in the toolkit. """ - apinames_filepaths = self.generate_apinames_filepaths() - all_funcs_lst, all_schemas_lst = ( - self.apinames_filepaths_to_funs_schemas(apinames_filepaths) - ) - return [ - OpenAIFunction(a_func, a_schema) - for a_func, a_schema in zip(all_funcs_lst, all_schemas_lst) - ] + return OPENAPI_FUNCS From 88208f0072293332be1132b49fc15efeae2c3309 Mon Sep 17 00:00:00 2001 From: Isaac Jin Date: Tue, 24 Sep 2024 09:50:45 -0500 Subject: [PATCH 04/34] fix format --- camel/toolkits/weather_toolkit.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/camel/toolkits/weather_toolkit.py b/camel/toolkits/weather_toolkit.py index 1b1f613cd8..093926959c 100644 --- a/camel/toolkits/weather_toolkit.py +++ b/camel/toolkits/weather_toolkit.py @@ -151,9 +151,7 @@ def get_weather_data( return error_message -WEATHER_FUNCS = [ - OpenAIFunction(get_weather_data), -] +WEATHER_FUNCS = [OpenAIFunction(get_weather_data)] class WeatherToolkit(BaseToolkit): From c4df50c11250c1a39a548e157fd9ba5165f17e7d Mon Sep 17 00:00:00 2001 From: Zack Date: Mon, 30 Sep 2024 17:51:03 +0800 Subject: [PATCH 05/34] refactor: add ToolManager - Refactor toolkit imports in `camel/toolkits/__init__.py` - Add `ToolManager` class to manage dynamically loading and accessing toolkits in `camel/toolkits/toolkits_manager.py` --- camel/toolkits/__init__.py | 2 + camel/toolkits/google_maps_toolkit.py | 3 + camel/toolkits/openai_function.py | 3 + camel/toolkits/toolkits_manager.py | 126 ++++++++++++++++++++++++++ 4 files changed, 134 insertions(+) create mode 100644 camel/toolkits/toolkits_manager.py diff --git a/camel/toolkits/__init__.py b/camel/toolkits/__init__.py index 87f2326ff3..5f58bdefe9 100644 --- a/camel/toolkits/__init__.py +++ b/camel/toolkits/__init__.py @@ -33,6 +33,7 @@ from .code_execution import CodeExecutionToolkit from .github_toolkit import GithubToolkit +from .toolkits_manager import ToolManager __all__ = [ 'OpenAIFunction', @@ -56,4 +57,5 @@ 'SEARCH_FUNCS', 'WEATHER_FUNCS', 'DALLE_FUNCS', + 'ToolManager', ] diff --git a/camel/toolkits/google_maps_toolkit.py b/camel/toolkits/google_maps_toolkit.py index 9b15fba5e0..f88c47a479 100644 --- a/camel/toolkits/google_maps_toolkit.py +++ b/camel/toolkits/google_maps_toolkit.py @@ -300,3 +300,6 @@ def get_tools(self) -> List[OpenAIFunction]: OpenAIFunction(self.get_elevation), OpenAIFunction(self.get_timezone), ] + + +__all__: list[str] = [] diff --git a/camel/toolkits/openai_function.py b/camel/toolkits/openai_function.py index e2b35a8843..2d6ea8443c 100644 --- a/camel/toolkits/openai_function.py +++ b/camel/toolkits/openai_function.py @@ -387,3 +387,6 @@ def parameters(self, value: Dict[str, Any]) -> None: except SchemaError as e: raise e self.openai_tool_schema["function"]["parameters"]["properties"] = value + + +__all__: list[str] = [] diff --git a/camel/toolkits/toolkits_manager.py b/camel/toolkits/toolkits_manager.py new file mode 100644 index 0000000000..43c15c4b90 --- /dev/null +++ b/camel/toolkits/toolkits_manager.py @@ -0,0 +1,126 @@ +import difflib +import importlib +import inspect +import pkgutil +from typing import Callable, List, Optional + +from camel.toolkits.openai_function import OpenAIFunction + + +class ToolManager: + r""" + A class representing a manager for dynamically loading and accessing toolkits. + + The ToolManager loads all callable toolkits from the `camel.toolkits` package + and provides methods to list, retrieve, and search them as OpenAIFunction objects. + """ + + def __init__(self): + r""" + Initializes the ToolManager and loads all available toolkits. + """ + self.toolkits = {} + self._load_toolkits() + + def _load_toolkits(self): + r""" + Dynamically loads all toolkits from the `camel.toolkits` package. + + It iterates through all modules in the package, checking for the presence + of an `__all__` attribute to explicitly control what functions to export. + If `__all__` is not present, it ignores any function starting with `_`. + """ + package = importlib.import_module('camel.toolkits') + for _, module_name, _ in pkgutil.iter_modules(package.__path__): + module = importlib.import_module(f'camel.toolkits.{module_name}') + + if hasattr(module, '__all__'): + for name in module.__all__: + func = getattr(module, name, None) + if callable(func) and func.__module__ == module.__name__: + self.toolkits[name] = func + else: + for name, func in inspect.getmembers( + module, inspect.isfunction + ): + if ( + not name.startswith('_') + and func.__module__ == module.__name__ + ): + self.toolkits[name] = func + + def list_toolkits(self): + r""" + Lists the names of all available toolkits. + + Returns: + List[str]: A list of all toolkit function names available for use. + """ + return list(self.toolkits.keys()) + + def get_toolkit(self, name: str) -> OpenAIFunction: + r""" + Retrieves the specified toolkit as an OpenAIFunction object. + + Args: + name (str): The name of the toolkit function to retrieve. + + Returns: + OpenAIFunction: The toolkit wrapped as an OpenAIFunction. + + Raises: + ValueError: If the specified toolkit is not found. + """ + toolkit = self.toolkits.get(name) + if toolkit: + return OpenAIFunction(toolkit) + raise ValueError(f"Toolkit '{name}' not found.") + + def _default_search_algorithm( + self, keyword: str, description: str + ) -> bool: + r""" + Default search algorithm using fuzzy matching. + + Args: + keyword (str): The keyword to search for. + description (str): The description to search within. + + Returns: + bool: True if a match is found based on similarity, False otherwise. + """ + ratio = difflib.SequenceMatcher( + None, keyword.lower(), description.lower() + ).ratio() + return ( + ratio > 0.6 + ) # A threshold of 0.6 for fuzzy matching (adjustable) + + def search_toolkits( + self, + keyword: str, + algorithm: Optional[Callable[[str, str], bool]] = None, + ) -> List[str]: + r""" + Searches for toolkits based on a keyword in their descriptions using the provided search algorithm. + + Args: + keyword (str): The keyword to search for in toolkit descriptions. + algorithm (Callable[[str, str], bool], optional): A custom search algorithm function + that accepts the keyword and description and returns a boolean. + Defaults to fuzzy matching. + + Returns: + List[str]: A list of toolkit names whose descriptions match the keyword. + """ + if algorithm is None: + algorithm = self._default_search_algorithm + + matching_toolkits = [] + for name, func in self.toolkits.items(): + openai_func = OpenAIFunction(func) + description = openai_func.get_function_description() + if algorithm(keyword, description): + matching_toolkits.append(name) + + return matching_toolkits From cde4c0ad721fbd3d6c213c8b30815e162a3205b4 Mon Sep 17 00:00:00 2001 From: Zack Date: Mon, 30 Sep 2024 18:44:16 +0800 Subject: [PATCH 06/34] update: Add several methods to ToolManager --- camel/toolkits/toolkits_manager.py | 64 +++++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 11 deletions(-) diff --git a/camel/toolkits/toolkits_manager.py b/camel/toolkits/toolkits_manager.py index 43c15c4b90..f79106bbe2 100644 --- a/camel/toolkits/toolkits_manager.py +++ b/camel/toolkits/toolkits_manager.py @@ -9,10 +9,12 @@ class ToolManager: r""" - A class representing a manager for dynamically loading and accessing toolkits. + A class representing a manager for dynamically loading and accessing + toolkits. - The ToolManager loads all callable toolkits from the `camel.toolkits` package - and provides methods to list, retrieve, and search them as OpenAIFunction objects. + The ToolManager loads all callable toolkits from the `camel.toolkits` + package and provides methods to list, retrieve, and search them as + OpenAIFunction objects. """ def __init__(self): @@ -26,8 +28,9 @@ def _load_toolkits(self): r""" Dynamically loads all toolkits from the `camel.toolkits` package. - It iterates through all modules in the package, checking for the presence - of an `__all__` attribute to explicitly control what functions to export. + It iterates through all modules in the package, checking for the + presence of an `__all__` attribute to explicitly control what + functions to export. If `__all__` is not present, it ignores any function starting with `_`. """ package = importlib.import_module('camel.toolkits') @@ -49,6 +52,19 @@ def _load_toolkits(self): ): self.toolkits[name] = func + def _load_toolkit_class(self): + r""" + todo:Dynamically loads all toolkits class for user to define their own + toolkit by setting the keys or other conifgs. + """ + pass + + def add_toolkit(self, tookit: Callable): + r""" + todo: let user add toolkit to the toolkits list + """ + pass + def list_toolkits(self): r""" Lists the names of all available toolkits. @@ -58,7 +74,7 @@ def list_toolkits(self): """ return list(self.toolkits.keys()) - def get_toolkit(self, name: str) -> OpenAIFunction: + def get_toolkit(self, name: str) -> OpenAIFunction | str: r""" Retrieves the specified toolkit as an OpenAIFunction object. @@ -74,7 +90,29 @@ def get_toolkit(self, name: str) -> OpenAIFunction: toolkit = self.toolkits.get(name) if toolkit: return OpenAIFunction(toolkit) - raise ValueError(f"Toolkit '{name}' not found.") + return f"Toolkit '{name}' not found." + + def get_toolkits(self, names: list[str]) -> list[OpenAIFunction] | str: + r""" + Retrieves the specified toolkit as an OpenAIFunction object. + + Args: + name (str): The name of the toolkit function to retrieve. + + Returns: + OpenAIFunctions (list): The toolkits wrapped as an OpenAIFunction. + + Raises: + ValueError: If the specified toolkit is not found. + """ + toolkits: list[OpenAIFunction] = [] + for name in names: + current_toolkit = self.toolkits.get(name) + if current_toolkit: + toolkits.append(OpenAIFunction(current_toolkit)) + if len(toolkits) > 0: + return toolkits + return f"Toolkit '{name}' not found." def _default_search_algorithm( self, keyword: str, description: str @@ -87,7 +125,8 @@ def _default_search_algorithm( description (str): The description to search within. Returns: - bool: True if a match is found based on similarity, False otherwise. + bool: True if a match is found based on similarity, False + otherwise. """ ratio = difflib.SequenceMatcher( None, keyword.lower(), description.lower() @@ -102,16 +141,19 @@ def search_toolkits( algorithm: Optional[Callable[[str, str], bool]] = None, ) -> List[str]: r""" - Searches for toolkits based on a keyword in their descriptions using the provided search algorithm. + Searches for toolkits based on a keyword in their descriptions using + the provided search algorithm. Args: keyword (str): The keyword to search for in toolkit descriptions. - algorithm (Callable[[str, str], bool], optional): A custom search algorithm function + algorithm (Callable[[str, str], bool], optional): A custom search + algorithm function that accepts the keyword and description and returns a boolean. Defaults to fuzzy matching. Returns: - List[str]: A list of toolkit names whose descriptions match the keyword. + List[str]: A list of toolkit names whose descriptions match the + keyword. """ if algorithm is None: algorithm = self._default_search_algorithm From 785b37f21b41537ff286a999343cb921b85f819a Mon Sep 17 00:00:00 2001 From: Zack Date: Mon, 30 Sep 2024 18:46:11 +0800 Subject: [PATCH 07/34] fix --- camel/toolkits/toolkits_manager.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/camel/toolkits/toolkits_manager.py b/camel/toolkits/toolkits_manager.py index f79106bbe2..e779e9cf7d 100644 --- a/camel/toolkits/toolkits_manager.py +++ b/camel/toolkits/toolkits_manager.py @@ -22,6 +22,7 @@ def __init__(self): Initializes the ToolManager and loads all available toolkits. """ self.toolkits = {} + self.toolkit_classes = {} self._load_toolkits() def _load_toolkits(self): @@ -54,8 +55,8 @@ def _load_toolkits(self): def _load_toolkit_class(self): r""" - todo:Dynamically loads all toolkits class for user to define their own - toolkit by setting the keys or other conifgs. + todo:Dynamically loads all toolkits class to self.toolkit_classes for + user to define their own toolkit by setting the keys or other conifgs. """ pass From 3259cce84fb2ff37bd15b22e9a06522a6dcc68f2 Mon Sep 17 00:00:00 2001 From: Zack Date: Tue, 8 Oct 2024 11:08:32 +0800 Subject: [PATCH 08/34] Refactor ToolManager and GithubToolkit classes --- camel/toolkits/github_toolkit.py | 5 ++ camel/toolkits/toolkits_manager.py | 120 ++++++++++++++++++++++++----- camel/utils/commons.py | 11 +++ 3 files changed, 118 insertions(+), 18 deletions(-) diff --git a/camel/toolkits/github_toolkit.py b/camel/toolkits/github_toolkit.py index e5f15247bf..c2580a1ea0 100644 --- a/camel/toolkits/github_toolkit.py +++ b/camel/toolkits/github_toolkit.py @@ -21,6 +21,7 @@ from camel.toolkits.base import BaseToolkit from camel.toolkits.openai_function import OpenAIFunction from camel.utils import dependencies_required +from camel.utils.commons import export_to_toolkit def get_github_access_token() -> str: @@ -163,6 +164,7 @@ def get_tools(self) -> List[OpenAIFunction]: OpenAIFunction(self.retrieve_pull_requests), ] + @export_to_toolkit def retrieve_issue_list(self) -> List[GithubIssue]: r"""Retrieve a list of open issues from the repository. @@ -185,6 +187,7 @@ def retrieve_issue_list(self) -> List[GithubIssue]: if not issue.pull_request ] + @export_to_toolkit def retrieve_issue(self, issue_number: int) -> Optional[str]: r"""Retrieves an issue from a GitHub repository. This function retrieves an issue from a specified repository using the issue number. @@ -201,6 +204,7 @@ def retrieve_issue(self, issue_number: int) -> Optional[str]: return str(issue) return None + @export_to_toolkit def retrieve_pull_requests( self, days: int, state: str, max_prs: int ) -> List[str]: @@ -241,6 +245,7 @@ def retrieve_pull_requests( merged_prs.append(str(pr_details)) return merged_prs + @export_to_toolkit def create_pull_request( self, file_path: str, diff --git a/camel/toolkits/toolkits_manager.py b/camel/toolkits/toolkits_manager.py index e779e9cf7d..479bcfe9cb 100644 --- a/camel/toolkits/toolkits_manager.py +++ b/camel/toolkits/toolkits_manager.py @@ -4,6 +4,7 @@ import pkgutil from typing import Callable, List, Optional +from camel.toolkits.base import BaseToolkit from camel.toolkits.openai_function import OpenAIFunction @@ -22,8 +23,12 @@ def __init__(self): Initializes the ToolManager and loads all available toolkits. """ self.toolkits = {} + self.toolkit_classes = {} + self.toolkit_class_methods = {} + self._load_toolkits() + self._load_toolkit_class_and_methods() def _load_toolkits(self): r""" @@ -53,18 +58,82 @@ def _load_toolkits(self): ): self.toolkits[name] = func - def _load_toolkit_class(self): + def _load_toolkit_class_and_methods(self): r""" - todo:Dynamically loads all toolkits class to self.toolkit_classes for - user to define their own toolkit by setting the keys or other conifgs. + Dynamically loads all classes and their methods from the `camel.toolkits` package. + + It iterates through all modules in the package, checking for public classes. + For each class, it collects its public methods (those not starting with `_`). """ - pass + package = importlib.import_module('camel.toolkits') - def add_toolkit(self, tookit: Callable): - r""" - todo: let user add toolkit to the toolkits list + for _, module_name, _ in pkgutil.iter_modules(package.__path__): + module = importlib.import_module(f'camel.toolkits.{module_name}') + + for name, cls in inspect.getmembers(module, inspect.isclass): + if cls.__module__ == module.__name__: + self.toolkit_classes[name] = cls + + self.toolkit_class_methods[name] = { + method_name: method + for method_name, method in inspect.getmembers( + cls, inspect.isfunction + ) + if callable(method) and hasattr(method, '_is_exported') + } + + def add_toolkit_from_function(self, toolkit_func: Callable): + """ + Adds a toolkit function to the toolkits list. + + Parameters: + toolkit_func (Callable): The toolkit function to be added. + + Returns: + Str: A message indicating whether the addition was successful or + if it failed. + """ + if not callable(toolkit_func): + return "Provided argument is not a callable function." + + func_name = toolkit_func.__name__ + + if not func_name: + return "Function must have a valid name." + + self.toolkits[func_name] = toolkit_func + + return f"Toolkit '{func_name}' added successfully." + + def add_toolkit_from_instance(self, **kwargs): + """ + Add a toolkit class instance to the tool list. + + Parameters: + kwargs: The toolkit class instance to be added. Keyword arguments + where each value is expected to be an instance of BaseToolkit. + + Returns: + Str: A message indicating whether the addition was successful or + if it failed. """ - pass + messages = [] + for toolkit_instance_name, toolkit_instance in kwargs.items(): + if isinstance(toolkit_instance, BaseToolkit): + for attr_name in dir(toolkit_instance): + attr = getattr(toolkit_instance, attr_name) + + if callable(attr) and hasattr(attr, '_is_exported'): + method_name = f"{toolkit_instance_name}_{attr_name}" + + self.toolkits[method_name] = attr + messages.append(f"Successfully added {method_name}.") + else: + messages.append( + f"Failed to add {toolkit_instance_name}: Not an instance of BaseToolkit." + ) + + return "\n".join(messages) def list_toolkits(self): r""" @@ -75,6 +144,26 @@ def list_toolkits(self): """ return list(self.toolkits.keys()) + def list_toolkit_classes(self): + r""" + Lists the names of all available toolkit classes along with their + methods. + + Returns: + List[str]: A list of strings in the format 'ClassName: method1, + method2, ...'. + """ + result = [] + + for class_name, methods in self.toolkit_class_methods.items(): + methods_str = ', '.join(methods) + + formatted_string = f"{class_name}: {methods_str}" + + result.append(formatted_string) + + return result + def get_toolkit(self, name: str) -> OpenAIFunction | str: r""" Retrieves the specified toolkit as an OpenAIFunction object. @@ -110,16 +199,16 @@ def get_toolkits(self, names: list[str]) -> list[OpenAIFunction] | str: for name in names: current_toolkit = self.toolkits.get(name) if current_toolkit: - toolkits.append(OpenAIFunction(current_toolkit)) + toolkits.append(current_toolkit) if len(toolkits) > 0: return toolkits - return f"Toolkit '{name}' not found." + return "Toolkits are not found." def _default_search_algorithm( self, keyword: str, description: str ) -> bool: r""" - Default search algorithm using fuzzy matching. + Default search algorithm. Args: keyword (str): The keyword to search for. @@ -129,12 +218,7 @@ def _default_search_algorithm( bool: True if a match is found based on similarity, False otherwise. """ - ratio = difflib.SequenceMatcher( - None, keyword.lower(), description.lower() - ).ratio() - return ( - ratio > 0.6 - ) # A threshold of 0.6 for fuzzy matching (adjustable) + return keyword.lower() in description.lower() def search_toolkits( self, @@ -163,7 +247,7 @@ def search_toolkits( for name, func in self.toolkits.items(): openai_func = OpenAIFunction(func) description = openai_func.get_function_description() - if algorithm(keyword, description): + if algorithm(keyword, description) or algorithm(keyword, name): matching_toolkits.append(name) return matching_toolkits diff --git a/camel/utils/commons.py b/camel/utils/commons.py index 943e398c7f..8cc64d8b35 100644 --- a/camel/utils/commons.py +++ b/camel/utils/commons.py @@ -11,6 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +import functools import importlib import os import platform @@ -577,3 +578,13 @@ def handle_http_error(response: requests.Response) -> str: return "Too Many Requests. You have hit the rate limit." else: return "HTTP Error" + + +def export_to_toolkit(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + result = func(*args, **kwargs) + return result + + wrapper._is_exported = True + return wrapper From 9220f15127e1e5f722a55b8e8f48355b67caa08f Mon Sep 17 00:00:00 2001 From: Zack Date: Tue, 8 Oct 2024 11:20:47 +0800 Subject: [PATCH 09/34] Add toolkits manager example file --- camel/toolkits/toolkits_manager.py | 26 ++- examples/toolkits/toolkts_manager_example.py | 176 +++++++++++++++++++ 2 files changed, 197 insertions(+), 5 deletions(-) create mode 100644 examples/toolkits/toolkts_manager_example.py diff --git a/camel/toolkits/toolkits_manager.py b/camel/toolkits/toolkits_manager.py index 479bcfe9cb..7eadf925b6 100644 --- a/camel/toolkits/toolkits_manager.py +++ b/camel/toolkits/toolkits_manager.py @@ -1,4 +1,16 @@ -import difflib +# =========== 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 importlib import inspect import pkgutil @@ -60,10 +72,13 @@ def _load_toolkits(self): def _load_toolkit_class_and_methods(self): r""" - Dynamically loads all classes and their methods from the `camel.toolkits` package. + Dynamically loads all classes and their methods from the `camel. + toolkits` package. - It iterates through all modules in the package, checking for public classes. - For each class, it collects its public methods (those not starting with `_`). + It iterates through all modules in the package, checking for public + classes. + For each class, it collects its public methods (those not starting + with `_`). """ package = importlib.import_module('camel.toolkits') @@ -130,7 +145,8 @@ def add_toolkit_from_instance(self, **kwargs): messages.append(f"Successfully added {method_name}.") else: messages.append( - f"Failed to add {toolkit_instance_name}: Not an instance of BaseToolkit." + f"Failed to add {toolkit_instance_name}: " + + "Not an instance of BaseToolkit." ) return "\n".join(messages) diff --git a/examples/toolkits/toolkts_manager_example.py b/examples/toolkits/toolkts_manager_example.py new file mode 100644 index 0000000000..1e50f9c7ba --- /dev/null +++ b/examples/toolkits/toolkts_manager_example.py @@ -0,0 +1,176 @@ +# =========== 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.toolkits import ToolManager +from camel.toolkits.github_toolkit import GithubToolkit +from camel.toolkits.openai_function import OpenAIFunction + + +def pretty_print_list(title, items): + print(f"\n{'=' * 40}\n{title}:\n{'-' * 40}") + if not items: + print(" (No project)") + else: + for index, item in enumerate(items, start=1): + print(f" {index}. {item}") + print('=' * 40) + + +manager = ToolManager() + +toolkits = manager.list_toolkits() +toolkit_classes = manager.list_toolkit_classes() + +pretty_print_list("Function Toolkits", toolkits) +pretty_print_list("Class Toolkits", toolkit_classes) + +matching_toolkits_test = manager.search_toolkits('weather') +pretty_print_list("Matching Toolkit", matching_toolkits_test) + + +def strict_search_algorithm(keyword: str, description: str) -> bool: + return keyword.lower() in description.lower() + + +matching_toolkits_custom = manager.search_toolkits( + 'weather', algorithm=strict_search_algorithm +) +pretty_print_list( + "Custom Algorithm Matching Toolkit", matching_toolkits_custom +) + +tool = manager.get_toolkit('get_weather_data') +if isinstance(tool, OpenAIFunction): + print("\nFunction Description:") + print('-' * 40) + print(tool.get_function_description()) + +camel_github_toolkit = GithubToolkit(repo_name='camel-ai/camel') +crab_github_toolkit = GithubToolkit(repo_name='ZackYule/crab') + +manager.add_toolkit_from_instance( + camel_github_toolkit=camel_github_toolkit, + crab_github_toolkit=crab_github_toolkit, +) + +matching_tools_for_github = manager.search_toolkits('github') +pretty_print_list("Matching Tools for GitHub", matching_tools_for_github) + +if isinstance(matching_tools_for_github, list): + tools_instances = manager.get_toolkits(names=matching_tools_for_github) + pretty_print_list("Tools Instances", tools_instances) + +""" +=============================================================================== +======================================== +Function Toolkits: +---------------------------------------- + 1. get_dalle_img + 2. get_github_access_token + 3. add + 4. mul + 5. sub + 6. query_wolfram_alpha + 7. search_duckduckgo + 8. search_google + 9. search_wiki + 10. get_openweathermap_api_key + 11. get_weather_data +======================================== + +======================================== +Class Toolkits: +---------------------------------------- + 1. BaseToolkit: + 2. CodeExecutionToolkit: + 3. DalleToolkit: + 4. GithubIssue: + 5. GithubPullRequest: + 6. GithubPullRequestDiff: + 7. GithubToolkit: create_pull_request, retrieve_issue, retrieve_issue_list, + retrieve_pull_requests + 8. GoogleMapsToolkit: + 9. LinkedInToolkit: + 10. MathToolkit: + 11. OpenAPIToolkit: + 12. OpenAIFunction: + 13. RedditToolkit: + 14. RetrievalToolkit: + 15. SearchToolkit: + 16. SlackToolkit: + 17. ToolManager: + 18. TwitterToolkit: + 19. WeatherToolkit: +======================================== + +======================================== +Matching Toolkit: +---------------------------------------- + 1. get_openweathermap_api_key + 2. get_weather_data +======================================== + +======================================== +Custom Algorithm Matching Toolkit: +---------------------------------------- + 1. get_openweathermap_api_key + 2. get_weather_data +======================================== + +Function Description: +---------------------------------------- +Fetch and return a comprehensive weather report for a given city +as a string. The report includes current weather conditions, +temperature, wind details, visibility, and sunrise/sunset times, +all formatted as a readable string. + +The function interacts with the OpenWeatherMap API to +retrieve the data. + +======================================== +Matching Tools for GitHub: +---------------------------------------- + 1. get_github_access_token + 2. camel_github_toolkit_create_pull_request + 3. camel_github_toolkit_retrieve_issue + 4. camel_github_toolkit_retrieve_issue_list + 5. camel_github_toolkit_retrieve_pull_requests + 6. crab_github_toolkit_create_pull_request + 7. crab_github_toolkit_retrieve_issue + 8. crab_github_toolkit_retrieve_issue_list + 9. crab_github_toolkit_retrieve_pull_requests +======================================== + +======================================== +Tools Instances: +---------------------------------------- + 1. + 2. > + 3. > + 4. > + 5. > + 6. > + 7. > + 8. > + 9. > +======================================== +=============================================================================== +""" From fa17d655666f4bf39f8598fa1074a29e3df52fcd Mon Sep 17 00:00:00 2001 From: Zack Date: Tue, 8 Oct 2024 11:36:20 +0800 Subject: [PATCH 10/34] format toolkits manager example file --- examples/toolkits/toolkts_manager_example.py | 87 +++++++++++--------- 1 file changed, 50 insertions(+), 37 deletions(-) diff --git a/examples/toolkits/toolkts_manager_example.py b/examples/toolkits/toolkts_manager_example.py index 1e50f9c7ba..a63b0f6ce1 100644 --- a/examples/toolkits/toolkts_manager_example.py +++ b/examples/toolkits/toolkts_manager_example.py @@ -33,43 +33,6 @@ def pretty_print_list(title, items): pretty_print_list("Function Toolkits", toolkits) pretty_print_list("Class Toolkits", toolkit_classes) - -matching_toolkits_test = manager.search_toolkits('weather') -pretty_print_list("Matching Toolkit", matching_toolkits_test) - - -def strict_search_algorithm(keyword: str, description: str) -> bool: - return keyword.lower() in description.lower() - - -matching_toolkits_custom = manager.search_toolkits( - 'weather', algorithm=strict_search_algorithm -) -pretty_print_list( - "Custom Algorithm Matching Toolkit", matching_toolkits_custom -) - -tool = manager.get_toolkit('get_weather_data') -if isinstance(tool, OpenAIFunction): - print("\nFunction Description:") - print('-' * 40) - print(tool.get_function_description()) - -camel_github_toolkit = GithubToolkit(repo_name='camel-ai/camel') -crab_github_toolkit = GithubToolkit(repo_name='ZackYule/crab') - -manager.add_toolkit_from_instance( - camel_github_toolkit=camel_github_toolkit, - crab_github_toolkit=crab_github_toolkit, -) - -matching_tools_for_github = manager.search_toolkits('github') -pretty_print_list("Matching Tools for GitHub", matching_tools_for_github) - -if isinstance(matching_tools_for_github, list): - tools_instances = manager.get_toolkits(names=matching_tools_for_github) - pretty_print_list("Tools Instances", tools_instances) - """ =============================================================================== ======================================== @@ -112,7 +75,25 @@ def strict_search_algorithm(keyword: str, description: str) -> bool: 18. TwitterToolkit: 19. WeatherToolkit: ======================================== +=============================================================================== +""" +matching_toolkits_test = manager.search_toolkits('weather') +pretty_print_list("Matching Toolkit", matching_toolkits_test) + + +def strict_search_algorithm(keyword: str, description: str) -> bool: + return keyword.lower() in description.lower() + + +matching_toolkits_custom = manager.search_toolkits( + 'weather', algorithm=strict_search_algorithm +) +pretty_print_list( + "Custom Algorithm Matching Toolkit", matching_toolkits_custom +) +""" +=============================================================================== ======================================== Matching Toolkit: ---------------------------------------- @@ -126,7 +107,16 @@ def strict_search_algorithm(keyword: str, description: str) -> bool: 1. get_openweathermap_api_key 2. get_weather_data ======================================== +=============================================================================== +""" +tool = manager.get_toolkit('get_weather_data') +if isinstance(tool, OpenAIFunction): + print("\nFunction Description:") + print('-' * 40) + print(tool.get_function_description()) +""" +=============================================================================== Function Description: ---------------------------------------- Fetch and return a comprehensive weather report for a given city @@ -137,6 +127,21 @@ def strict_search_algorithm(keyword: str, description: str) -> bool: The function interacts with the OpenWeatherMap API to retrieve the data. +=============================================================================== +""" + +camel_github_toolkit = GithubToolkit(repo_name='camel-ai/camel') +crab_github_toolkit = GithubToolkit(repo_name='ZackYule/crab') + +manager.add_toolkit_from_instance( + camel_github_toolkit=camel_github_toolkit, + crab_github_toolkit=crab_github_toolkit, +) + +matching_tools_for_github = manager.search_toolkits('github') +pretty_print_list("Matching Tools for GitHub", matching_tools_for_github) +""" +=============================================================================== ======================================== Matching Tools for GitHub: ---------------------------------------- @@ -150,7 +155,15 @@ def strict_search_algorithm(keyword: str, description: str) -> bool: 8. crab_github_toolkit_retrieve_issue_list 9. crab_github_toolkit_retrieve_pull_requests ======================================== +=============================================================================== +""" + +if isinstance(matching_tools_for_github, list): + tools_instances = manager.get_toolkits(names=matching_tools_for_github) + pretty_print_list("Tools Instances", tools_instances) +""" +=============================================================================== ======================================== Tools Instances: ---------------------------------------- From 50ff7d907b8d92744c6457b3c111abbca4f8f872 Mon Sep 17 00:00:00 2001 From: Zack Date: Sat, 12 Oct 2024 14:47:33 +0800 Subject: [PATCH 11/34] Refactor toolkits manager file --- camel/toolkits/code_execution.py | 2 ++ camel/toolkits/dalle_toolkit.py | 2 ++ camel/toolkits/google_maps_toolkit.py | 4 +++ camel/toolkits/linkedin_toolkit.py | 4 +++ camel/toolkits/math_toolkit.py | 4 +++ camel/toolkits/openai_function.py | 3 -- camel/toolkits/reddit_toolkit.py | 4 +++ camel/toolkits/retrieval_toolkit.py | 2 ++ camel/toolkits/search_toolkit.py | 5 +++ camel/toolkits/slack_toolkit.py | 8 +++++ camel/toolkits/toolkits_manager.py | 18 +++------- camel/toolkits/twitter_toolkit.py | 4 +++ camel/toolkits/weather_toolkit.py | 2 ++ examples/toolkits/toolkts_manager_example.py | 35 ++++++++++---------- 14 files changed, 64 insertions(+), 33 deletions(-) diff --git a/camel/toolkits/code_execution.py b/camel/toolkits/code_execution.py index 0fcc3243b2..ce8f349680 100644 --- a/camel/toolkits/code_execution.py +++ b/camel/toolkits/code_execution.py @@ -15,6 +15,7 @@ from camel.interpreters import InternalPythonInterpreter from camel.toolkits import OpenAIFunction +from camel.utils.commons import export_to_toolkit from .base import BaseToolkit @@ -42,6 +43,7 @@ def __init__( f"The sandbox type `{sandbox}` is not supported." ) + @export_to_toolkit def execute_code(self, code: str) -> str: r"""Execute a given code snippet. diff --git a/camel/toolkits/dalle_toolkit.py b/camel/toolkits/dalle_toolkit.py index 67d54a7234..8a039f69dc 100644 --- a/camel/toolkits/dalle_toolkit.py +++ b/camel/toolkits/dalle_toolkit.py @@ -22,6 +22,7 @@ from camel.toolkits import OpenAIFunction from camel.toolkits.base import BaseToolkit +from camel.utils.commons import export_to_toolkit def _base64_to_image(base64_string: str) -> Optional[Image.Image]: @@ -90,6 +91,7 @@ def _image_to_base64(image: Image.Image) -> str: return "" +@export_to_toolkit def get_dalle_img(prompt: str, image_dir: str = "img") -> str: r"""Generate an image using OpenAI's DALL-E model. The generated image is saved to the specified directory. diff --git a/camel/toolkits/google_maps_toolkit.py b/camel/toolkits/google_maps_toolkit.py index f88c47a479..42711e6a6c 100644 --- a/camel/toolkits/google_maps_toolkit.py +++ b/camel/toolkits/google_maps_toolkit.py @@ -18,6 +18,7 @@ from camel.toolkits.base import BaseToolkit from camel.toolkits.openai_function import OpenAIFunction from camel.utils import dependencies_required +from camel.utils.commons import export_to_toolkit def handle_googlemaps_exceptions( @@ -115,6 +116,7 @@ def __init__(self) -> None: self.gmaps = googlemaps.Client(key=api_key) + @export_to_toolkit @handle_googlemaps_exceptions def get_address_description( self, @@ -195,6 +197,7 @@ def get_address_description( return description + @export_to_toolkit @handle_googlemaps_exceptions def get_elevation(self, lat: float, lng: float) -> str: r"""Retrieves elevation data for a given latitude and longitude. @@ -237,6 +240,7 @@ def get_elevation(self, lat: float, lng: float) -> str: return description + @export_to_toolkit @handle_googlemaps_exceptions def get_timezone(self, lat: float, lng: float) -> str: r"""Retrieves timezone information for a given latitude and longitude. diff --git a/camel/toolkits/linkedin_toolkit.py b/camel/toolkits/linkedin_toolkit.py index 1993849ce0..ef230ba3d8 100644 --- a/camel/toolkits/linkedin_toolkit.py +++ b/camel/toolkits/linkedin_toolkit.py @@ -22,6 +22,7 @@ from camel.toolkits import OpenAIFunction from camel.toolkits.base import BaseToolkit from camel.utils import handle_http_error +from camel.utils.commons import export_to_toolkit LINKEDIN_POST_LIMIT = 1300 @@ -36,6 +37,7 @@ class LinkedInToolkit(BaseToolkit): def __init__(self): self._access_token = self._get_access_token() + @export_to_toolkit def create_post(self, text: str) -> dict: r"""Creates a post on LinkedIn for the authenticated user. @@ -86,6 +88,7 @@ def create_post(self, text: str) -> dict: f"Response: {response.text}" ) + @export_to_toolkit def delete_post(self, post_id: str) -> str: r"""Deletes a LinkedIn post with the specified ID for an authorized user. @@ -136,6 +139,7 @@ def delete_post(self, post_id: str) -> str: return f"Post deleted successfully. Post ID: {post_id}." + @export_to_toolkit def get_profile(self, include_id: bool = False) -> dict: r"""Retrieves the authenticated user's LinkedIn profile info. diff --git a/camel/toolkits/math_toolkit.py b/camel/toolkits/math_toolkit.py index 57c02d1a22..99bd1b9c4c 100644 --- a/camel/toolkits/math_toolkit.py +++ b/camel/toolkits/math_toolkit.py @@ -16,8 +16,10 @@ from camel.toolkits.base import BaseToolkit from camel.toolkits.openai_function import OpenAIFunction +from camel.utils.commons import export_to_toolkit +@export_to_toolkit def add(a: int, b: int) -> int: r"""Adds two numbers. @@ -31,6 +33,7 @@ def add(a: int, b: int) -> int: return a + b +@export_to_toolkit def sub(a: int, b: int) -> int: r"""Do subtraction between two numbers. @@ -44,6 +47,7 @@ def sub(a: int, b: int) -> int: return a - b +@export_to_toolkit def mul(a: int, b: int) -> int: r"""Multiplies two integers. diff --git a/camel/toolkits/openai_function.py b/camel/toolkits/openai_function.py index 2d6ea8443c..e2b35a8843 100644 --- a/camel/toolkits/openai_function.py +++ b/camel/toolkits/openai_function.py @@ -387,6 +387,3 @@ def parameters(self, value: Dict[str, Any]) -> None: except SchemaError as e: raise e self.openai_tool_schema["function"]["parameters"]["properties"] = value - - -__all__: list[str] = [] diff --git a/camel/toolkits/reddit_toolkit.py b/camel/toolkits/reddit_toolkit.py index 5a9e7d5daf..dcc4eac3b3 100644 --- a/camel/toolkits/reddit_toolkit.py +++ b/camel/toolkits/reddit_toolkit.py @@ -20,6 +20,7 @@ from camel.toolkits import OpenAIFunction from camel.toolkits.base import BaseToolkit +from camel.utils.commons import export_to_toolkit class RedditToolkit(BaseToolkit): @@ -85,6 +86,7 @@ def _retry_request(self, func, *args, **kwargs): else: raise + @export_to_toolkit def collect_top_posts( self, subreddit_name: str, @@ -132,6 +134,7 @@ def collect_top_posts( return data + @export_to_toolkit def perform_sentiment_analysis( self, data: List[Dict[str, Any]] ) -> List[Dict[str, Any]]: @@ -156,6 +159,7 @@ def perform_sentiment_analysis( return data + @export_to_toolkit def track_keyword_discussions( self, subreddits: List[str], diff --git a/camel/toolkits/retrieval_toolkit.py b/camel/toolkits/retrieval_toolkit.py index 370b2c66a6..173c4f7167 100644 --- a/camel/toolkits/retrieval_toolkit.py +++ b/camel/toolkits/retrieval_toolkit.py @@ -18,6 +18,7 @@ from camel.toolkits.base import BaseToolkit from camel.types import StorageType from camel.utils import Constants +from camel.utils.commons import export_to_toolkit class RetrievalToolkit(BaseToolkit): @@ -34,6 +35,7 @@ def __init__(self, auto_retriever: Optional[AutoRetriever] = None) -> None: storage_type=StorageType.QDRANT, ) + @export_to_toolkit def information_retrieval( self, query: str, diff --git a/camel/toolkits/search_toolkit.py b/camel/toolkits/search_toolkit.py index b63b935153..3a1bdd6f3e 100644 --- a/camel/toolkits/search_toolkit.py +++ b/camel/toolkits/search_toolkit.py @@ -16,8 +16,10 @@ from camel.toolkits.base import BaseToolkit from camel.toolkits.openai_function import OpenAIFunction +from camel.utils.commons import export_to_toolkit +@export_to_toolkit def search_wiki(entity: str) -> str: r"""Search the entity in WikiPedia and return the summary of the required page, containing factual information about @@ -58,6 +60,7 @@ def search_wiki(entity: str) -> str: return result +@export_to_toolkit def search_duckduckgo( query: str, source: str = "text", max_results: int = 5 ) -> List[Dict[str, Any]]: @@ -146,6 +149,7 @@ def search_duckduckgo( return responses +@export_to_toolkit def search_google( query: str, num_result_pages: int = 5 ) -> List[Dict[str, Any]]: @@ -244,6 +248,7 @@ def search_google( return responses +@export_to_toolkit def query_wolfram_alpha(query: str, is_detailed: bool) -> str: r"""Queries Wolfram|Alpha and returns the result. Wolfram|Alpha is an answer engine developed by Wolfram Research. It is offered as an online diff --git a/camel/toolkits/slack_toolkit.py b/camel/toolkits/slack_toolkit.py index f386397d40..af23f0c7f1 100644 --- a/camel/toolkits/slack_toolkit.py +++ b/camel/toolkits/slack_toolkit.py @@ -20,6 +20,7 @@ from typing import TYPE_CHECKING, List, Optional from camel.toolkits.base import BaseToolkit +from camel.utils.commons import export_to_toolkit if TYPE_CHECKING: from ssl import SSLContext @@ -81,6 +82,7 @@ def _login_slack( logger.info("Slack login successful.") return client + @export_to_toolkit def create_slack_channel( self, name: str, is_private: Optional[bool] = True ) -> str: @@ -112,6 +114,7 @@ def create_slack_channel( except SlackApiError as e: return f"Error creating conversation: {e.response['error']}" + @export_to_toolkit def join_slack_channel(self, channel_id: str) -> str: r"""Joins an existing Slack channel. @@ -135,6 +138,7 @@ def join_slack_channel(self, channel_id: str) -> str: except SlackApiError as e: return f"Error creating conversation: {e.response['error']}" + @export_to_toolkit def leave_slack_channel(self, channel_id: str) -> str: r"""Leaves an existing Slack channel. @@ -158,6 +162,7 @@ def leave_slack_channel(self, channel_id: str) -> str: except SlackApiError as e: return f"Error creating conversation: {e.response['error']}" + @export_to_toolkit def get_slack_channel_information(self) -> str: r"""Retrieve Slack channels and return relevant information in JSON format. @@ -191,6 +196,7 @@ def get_slack_channel_information(self) -> str: except SlackApiError as e: return f"Error creating conversation: {e.response['error']}" + @export_to_toolkit def get_slack_channel_message(self, channel_id: str) -> str: r"""Retrieve messages from a Slack channel. @@ -220,6 +226,7 @@ def get_slack_channel_message(self, channel_id: str) -> str: except SlackApiError as e: return f"Error retrieving messages: {e.response['error']}" + @export_to_toolkit def send_slack_message( self, message: str, @@ -257,6 +264,7 @@ def send_slack_message( except SlackApiError as e: return f"Error creating conversation: {e.response['error']}" + @export_to_toolkit def delete_slack_message( self, time_stamp: str, diff --git a/camel/toolkits/toolkits_manager.py b/camel/toolkits/toolkits_manager.py index 7eadf925b6..71c440b62f 100644 --- a/camel/toolkits/toolkits_manager.py +++ b/camel/toolkits/toolkits_manager.py @@ -55,20 +55,12 @@ def _load_toolkits(self): for _, module_name, _ in pkgutil.iter_modules(package.__path__): module = importlib.import_module(f'camel.toolkits.{module_name}') - if hasattr(module, '__all__'): - for name in module.__all__: - func = getattr(module, name, None) - if callable(func) and func.__module__ == module.__name__: - self.toolkits[name] = func - else: - for name, func in inspect.getmembers( - module, inspect.isfunction + for name, func in inspect.getmembers(module, inspect.isfunction): + if ( + hasattr(func, '_is_exported') + and func.__module__ == module.__name__ ): - if ( - not name.startswith('_') - and func.__module__ == module.__name__ - ): - self.toolkits[name] = func + self.toolkits[name] = func def _load_toolkit_class_and_methods(self): r""" diff --git a/camel/toolkits/twitter_toolkit.py b/camel/toolkits/twitter_toolkit.py index b5a744f89d..227cda1858 100644 --- a/camel/toolkits/twitter_toolkit.py +++ b/camel/toolkits/twitter_toolkit.py @@ -21,6 +21,7 @@ from camel.toolkits import OpenAIFunction from camel.toolkits.base import BaseToolkit +from camel.utils.commons import export_to_toolkit TWEET_TEXT_LIMIT = 280 @@ -32,6 +33,7 @@ class TwitterToolkit(BaseToolkit): getting the authenticated user's profile information. """ + @export_to_toolkit def create_tweet( self, *, @@ -162,6 +164,7 @@ def create_tweet( return response_str + @export_to_toolkit def delete_tweet(self, tweet_id: str) -> str: r"""Deletes a tweet with the specified ID for an authorized user. @@ -226,6 +229,7 @@ def delete_tweet(self, tweet_id: str) -> str: ) return response_str + @export_to_toolkit def get_my_user_profile(self) -> str: r"""Retrieves and formats the authenticated user's Twitter profile info. diff --git a/camel/toolkits/weather_toolkit.py b/camel/toolkits/weather_toolkit.py index 093926959c..e3c598795f 100644 --- a/camel/toolkits/weather_toolkit.py +++ b/camel/toolkits/weather_toolkit.py @@ -16,6 +16,7 @@ from camel.toolkits.base import BaseToolkit from camel.toolkits.openai_function import OpenAIFunction +from camel.utils.commons import export_to_toolkit def get_openweathermap_api_key() -> str: @@ -39,6 +40,7 @@ def get_openweathermap_api_key() -> str: return api_key +@export_to_toolkit def get_weather_data( city: str, temp_units: Literal['kelvin', 'celsius', 'fahrenheit'] = 'kelvin', diff --git a/examples/toolkits/toolkts_manager_example.py b/examples/toolkits/toolkts_manager_example.py index a63b0f6ce1..f9d3d8b4a2 100644 --- a/examples/toolkits/toolkts_manager_example.py +++ b/examples/toolkits/toolkts_manager_example.py @@ -39,40 +39,41 @@ def pretty_print_list(title, items): Function Toolkits: ---------------------------------------- 1. get_dalle_img - 2. get_github_access_token - 3. add - 4. mul - 5. sub - 6. query_wolfram_alpha - 7. search_duckduckgo - 8. search_google - 9. search_wiki - 10. get_openweathermap_api_key - 11. get_weather_data + 2. add + 3. mul + 4. sub + 5. query_wolfram_alpha + 6. search_duckduckgo + 7. search_google + 8. search_wiki + 9. get_weather_data ======================================== ======================================== Class Toolkits: ---------------------------------------- 1. BaseToolkit: - 2. CodeExecutionToolkit: + 2. CodeExecutionToolkit: execute_code 3. DalleToolkit: 4. GithubIssue: 5. GithubPullRequest: 6. GithubPullRequestDiff: 7. GithubToolkit: create_pull_request, retrieve_issue, retrieve_issue_list, retrieve_pull_requests - 8. GoogleMapsToolkit: - 9. LinkedInToolkit: + 8. GoogleMapsToolkit: get_address_description, get_elevation, get_timezone + 9. LinkedInToolkit: create_post, delete_post, get_profile 10. MathToolkit: 11. OpenAPIToolkit: 12. OpenAIFunction: - 13. RedditToolkit: - 14. RetrievalToolkit: + 13. RedditToolkit: collect_top_posts, perform_sentiment_analysis, + track_keyword_discussions + 14. RetrievalToolkit: information_retrieval 15. SearchToolkit: - 16. SlackToolkit: + 16. SlackToolkit: create_slack_channel, delete_slack_message, + get_slack_channel_information, get_slack_channel_message, + join_slack_channel, leave_slack_channel, send_slack_message 17. ToolManager: - 18. TwitterToolkit: + 18. TwitterToolkit: create_tweet, delete_tweet, get_my_user_profile 19. WeatherToolkit: ======================================== =============================================================================== From 42a1cf159341a44563a03c3d4d043c564a22c063 Mon Sep 17 00:00:00 2001 From: Zack Date: Sat, 12 Oct 2024 15:14:20 +0800 Subject: [PATCH 12/34] Refactor toolkits manager example file --- examples/toolkits/toolkts_manager_example.py | 56 +++++++++----------- 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/examples/toolkits/toolkts_manager_example.py b/examples/toolkits/toolkts_manager_example.py index f9d3d8b4a2..9e2a6c4dfd 100644 --- a/examples/toolkits/toolkts_manager_example.py +++ b/examples/toolkits/toolkts_manager_example.py @@ -98,15 +98,13 @@ def strict_search_algorithm(keyword: str, description: str) -> bool: ======================================== Matching Toolkit: ---------------------------------------- - 1. get_openweathermap_api_key - 2. get_weather_data + 1. get_weather_data ======================================== ======================================== Custom Algorithm Matching Toolkit: ---------------------------------------- - 1. get_openweathermap_api_key - 2. get_weather_data + 1. get_weather_data ======================================== =============================================================================== """ @@ -146,15 +144,14 @@ def strict_search_algorithm(keyword: str, description: str) -> bool: ======================================== Matching Tools for GitHub: ---------------------------------------- - 1. get_github_access_token - 2. camel_github_toolkit_create_pull_request - 3. camel_github_toolkit_retrieve_issue - 4. camel_github_toolkit_retrieve_issue_list - 5. camel_github_toolkit_retrieve_pull_requests - 6. crab_github_toolkit_create_pull_request - 7. crab_github_toolkit_retrieve_issue - 8. crab_github_toolkit_retrieve_issue_list - 9. crab_github_toolkit_retrieve_pull_requests + 1. camel_github_toolkit_create_pull_request + 2. camel_github_toolkit_retrieve_issue + 3. camel_github_toolkit_retrieve_issue_list + 4. camel_github_toolkit_retrieve_pull_requests + 5. crab_github_toolkit_create_pull_request + 6. crab_github_toolkit_retrieve_issue + 7. crab_github_toolkit_retrieve_issue_list + 8. crab_github_toolkit_retrieve_pull_requests ======================================== =============================================================================== """ @@ -168,23 +165,22 @@ def strict_search_algorithm(keyword: str, description: str) -> bool: ======================================== Tools Instances: ---------------------------------------- - 1. - 2. > - 3. > - 4. > - 5. > - 6. > - 7. > - 8. > - 9. > + 1. > + 2. > + 3. > + 4. > + 5. > + 6. > + 7. > + 8. > ======================================== =============================================================================== """ From 9e0196020235c495f2c2dbf110968234f3ee6520 Mon Sep 17 00:00:00 2001 From: Zack Date: Sat, 12 Oct 2024 15:36:18 +0800 Subject: [PATCH 13/34] Refactor ToolManager to implement singleton pattern --- camel/toolkits/toolkits_manager.py | 64 ++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/camel/toolkits/toolkits_manager.py b/camel/toolkits/toolkits_manager.py index 71c440b62f..59dd22db22 100644 --- a/camel/toolkits/toolkits_manager.py +++ b/camel/toolkits/toolkits_manager.py @@ -30,26 +30,34 @@ class ToolManager: OpenAIFunction objects. """ + _instance = None + + def __new__(cls, *args, **kwargs): + if not cls._instance: + cls._instance = super(ToolManager, cls).__new__( + cls, *args, **kwargs + ) + return cls._instance + def __init__(self): r""" Initializes the ToolManager and loads all available toolkits. """ - self.toolkits = {} - - self.toolkit_classes = {} - self.toolkit_class_methods = {} - - self._load_toolkits() - self._load_toolkit_class_and_methods() + if not hasattr(self, '_initialized'): + self._initialized = True + self.toolkits = {} + self.toolkit_classes = {} + self.toolkit_class_methods = {} + self._load_toolkits() + self._load_toolkit_class_and_methods() def _load_toolkits(self): r""" - Dynamically loads all toolkits from the `camel.toolkits` package. + Dynamically loads all toolkit functions from the `camel.toolkits` + package. - It iterates through all modules in the package, checking for the - presence of an `__all__` attribute to explicitly control what - functions to export. - If `__all__` is not present, it ignores any function starting with `_`. + For each module in the package, it checks for functions decorated with + `@export_to_toolkit`, which adds the `_is_exported` attribute. """ package = importlib.import_module('camel.toolkits') for _, module_name, _ in pkgutil.iter_modules(package.__path__): @@ -64,13 +72,12 @@ def _load_toolkits(self): def _load_toolkit_class_and_methods(self): r""" - Dynamically loads all classes and their methods from the `camel. - toolkits` package. + Dynamically loads all classes and their exported methods from the + `camel.toolkits` package. - It iterates through all modules in the package, checking for public - classes. - For each class, it collects its public methods (those not starting - with `_`). + For each module in the package, it identifies public classes. For each + class, it collects only those methods that are decorated with + `@export_to_toolkit`, which adds the `_is_exported` attribute. """ package = importlib.import_module('camel.toolkits') @@ -89,8 +96,25 @@ def _load_toolkit_class_and_methods(self): if callable(method) and hasattr(method, '_is_exported') } - def add_toolkit_from_function(self, toolkit_func: Callable): + def register_tool(self, toolkit_func: Callable): + r""" + Registers a toolkit function and adds it to the toolkits list. + + Parameters: + toolkit_func (Callable): The toolkit function to be registered. + + Returns: + Union[OpenAIFunction, str]: Returns an OpenAIFunction instance if + the registration is successful. Otherwise, returns a message + indicating the failure reason. """ + add_info = self.add_toolkit_from_function(toolkit_func) + if "successfully" in add_info: + return OpenAIFunction(toolkit_func) + return add_info + + def add_toolkit_from_function(self, toolkit_func: Callable): + r""" Adds a toolkit function to the toolkits list. Parameters: @@ -113,7 +137,7 @@ def add_toolkit_from_function(self, toolkit_func: Callable): return f"Toolkit '{func_name}' added successfully." def add_toolkit_from_instance(self, **kwargs): - """ + r""" Add a toolkit class instance to the tool list. Parameters: From 187b7e555443e7e6dfef649418f343697c5ff8e8 Mon Sep 17 00:00:00 2001 From: Zack Date: Sat, 12 Oct 2024 16:10:11 +0800 Subject: [PATCH 14/34] Refactor ToolManager to support registering multiple toolkit functions or instances --- camel/toolkits/toolkits_manager.py | 71 +++++++++++++++++--- examples/toolkits/toolkts_manager_example.py | 65 ++++++++++++++++-- 2 files changed, 120 insertions(+), 16 deletions(-) diff --git a/camel/toolkits/toolkits_manager.py b/camel/toolkits/toolkits_manager.py index 59dd22db22..a69359493f 100644 --- a/camel/toolkits/toolkits_manager.py +++ b/camel/toolkits/toolkits_manager.py @@ -14,7 +14,7 @@ import importlib import inspect import pkgutil -from typing import Callable, List, Optional +from typing import Callable, List, Optional, Union from camel.toolkits.base import BaseToolkit from camel.toolkits.openai_function import OpenAIFunction @@ -96,22 +96,70 @@ def _load_toolkit_class_and_methods(self): if callable(method) and hasattr(method, '_is_exported') } - def register_tool(self, toolkit_func: Callable): + def register_tool( + self, + toolkit_obj: Union[Callable, object, List[Union[Callable, object]]], + ) -> List[OpenAIFunction] | str: r""" - Registers a toolkit function and adds it to the toolkits list. + Registers a toolkit function or instance and adds it to the toolkits + list. If the input is a list, it processes each element in the list. + + Parameters: + toolkit_obj (Union[Callable, object, List[Union[Callable, + object]]]): The toolkit function(s) or instance(s) to be + registered. + + Returns: + Union[List[OpenAIFunction], str]: Returns a list of OpenAIFunction + instances if the registration is successful. Otherwise, + returns a message indicating the failure reason. + """ + res_openai_functions = [] + res_info = "" + + # If the input is a list, process each element + if isinstance(toolkit_obj, list): + for obj in toolkit_obj: + res_openai_functions_part, res_info_part = ( + self._register_single_tool(obj) + ) + res_openai_functions.extend(res_openai_functions_part) + res_info += res_info_part + else: + res_openai_functions, res_info = self._register_single_tool( + toolkit_obj + ) + + return res_openai_functions if res_openai_functions else res_info + + def _register_single_tool( + self, toolkit_obj: Union[Callable, object] + ) -> tuple[List[OpenAIFunction], str]: + """ + Helper function to register a single toolkit function or instance. Parameters: - toolkit_func (Callable): The toolkit function to be registered. + toolkit_obj (Union[Callable, object]): The toolkit function or + instance to be processed. Returns: - Union[OpenAIFunction, str]: Returns an OpenAIFunction instance if - the registration is successful. Otherwise, returns a message - indicating the failure reason. + Tuple: A list of OpenAIFunction instances and a result message. """ - add_info = self.add_toolkit_from_function(toolkit_func) - if "successfully" in add_info: - return OpenAIFunction(toolkit_func) - return add_info + res_openai_functions = [] + res_info = "" + if callable(toolkit_obj): + res = self.add_toolkit_from_function(toolkit_obj) + if "successfully" in res: + res_openai_functions.append(OpenAIFunction(toolkit_obj)) + res_info += res + else: + res = self.add_toolkit_from_instance( + **{toolkit_obj.__class__.__name__: toolkit_obj} + ) + if "Successfully" in res: + res_openai_functions.extend(toolkit_obj.get_tools()) + res_info += res + return res_openai_functions, res_info def add_toolkit_from_function(self, toolkit_func: Callable): r""" @@ -139,6 +187,7 @@ def add_toolkit_from_function(self, toolkit_func: Callable): def add_toolkit_from_instance(self, **kwargs): r""" Add a toolkit class instance to the tool list. + Custom instance names are supported here. Parameters: kwargs: The toolkit class instance to be added. Keyword arguments diff --git a/examples/toolkits/toolkts_manager_example.py b/examples/toolkits/toolkts_manager_example.py index 9e2a6c4dfd..d427e76c23 100644 --- a/examples/toolkits/toolkts_manager_example.py +++ b/examples/toolkits/toolkts_manager_example.py @@ -129,11 +129,66 @@ def strict_search_algorithm(keyword: str, description: str) -> bool: =============================================================================== """ + +def div(a: int, b: int) -> float: + r"""Divides two numbers. + + Args: + a (int): The dividend in the division. + b (int): The divisor in the division. + + Returns: + float: The quotient of the division. + + Raises: + ValueError: If the divisor is zero. + """ + if b == 0: + raise ValueError("Division by zero is not allowed.") + + return a / b + + camel_github_toolkit = GithubToolkit(repo_name='camel-ai/camel') + +added_tools = manager.register_tool([div, camel_github_toolkit]) +pretty_print_list("Added Tools", added_tools) +pretty_print_list("Available Toolkits for now", manager.list_toolkits()) +""" +=============================================================================== +======================================== +Added Tools: +---------------------------------------- + 1. + 2. + 3. + 4. + 5. +======================================== +======================================== +Available Toolkits for now: +---------------------------------------- + 1. get_dalle_img + 2. add + 3. mul + 4. sub + 5. query_wolfram_alpha + 6. search_duckduckgo + 7. search_google + 8. search_wiki + 9. get_weather_data + 10. div + 11. GithubToolkit_create_pull_request + 12. GithubToolkit_retrieve_issue + 13. GithubToolkit_retrieve_issue_list + 14. GithubToolkit_retrieve_pull_requests +======================================== +=============================================================================== +""" + crab_github_toolkit = GithubToolkit(repo_name='ZackYule/crab') manager.add_toolkit_from_instance( - camel_github_toolkit=camel_github_toolkit, crab_github_toolkit=crab_github_toolkit, ) @@ -144,10 +199,10 @@ def strict_search_algorithm(keyword: str, description: str) -> bool: ======================================== Matching Tools for GitHub: ---------------------------------------- - 1. camel_github_toolkit_create_pull_request - 2. camel_github_toolkit_retrieve_issue - 3. camel_github_toolkit_retrieve_issue_list - 4. camel_github_toolkit_retrieve_pull_requests + 1. GithubToolkit_create_pull_request + 2. GithubToolkit_retrieve_issue + 3. GithubToolkit_retrieve_issue_list + 4. GithubToolkit_retrieve_pull_requests 5. crab_github_toolkit_create_pull_request 6. crab_github_toolkit_retrieve_issue 7. crab_github_toolkit_retrieve_issue_list From bab3c27915216b0759f89552f3ebdd682902623f Mon Sep 17 00:00:00 2001 From: Zack Date: Sat, 12 Oct 2024 16:19:16 +0800 Subject: [PATCH 15/34] Fix comments --- camel/toolkits/toolkits_manager.py | 2 +- examples/toolkits/toolkts_manager_example.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/camel/toolkits/toolkits_manager.py b/camel/toolkits/toolkits_manager.py index a69359493f..840864219c 100644 --- a/camel/toolkits/toolkits_manager.py +++ b/camel/toolkits/toolkits_manager.py @@ -135,7 +135,7 @@ def register_tool( def _register_single_tool( self, toolkit_obj: Union[Callable, object] ) -> tuple[List[OpenAIFunction], str]: - """ + r""" Helper function to register a single toolkit function or instance. Parameters: diff --git a/examples/toolkits/toolkts_manager_example.py b/examples/toolkits/toolkts_manager_example.py index d427e76c23..6db1ae2a76 100644 --- a/examples/toolkits/toolkts_manager_example.py +++ b/examples/toolkits/toolkts_manager_example.py @@ -188,6 +188,7 @@ def div(a: int, b: int) -> float: crab_github_toolkit = GithubToolkit(repo_name='ZackYule/crab') +# Custom instance names are supported here. manager.add_toolkit_from_instance( crab_github_toolkit=crab_github_toolkit, ) From 675ebab2c9197fa31e9d57922c0866737bae7c4d Mon Sep 17 00:00:00 2001 From: Zack Date: Sat, 12 Oct 2024 16:25:55 +0800 Subject: [PATCH 16/34] Refactor toolkits manager example file --- examples/toolkits/toolkts_manager_example.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/toolkits/toolkts_manager_example.py b/examples/toolkits/toolkts_manager_example.py index 6db1ae2a76..41e8afe37b 100644 --- a/examples/toolkits/toolkts_manager_example.py +++ b/examples/toolkits/toolkts_manager_example.py @@ -151,7 +151,10 @@ def div(a: int, b: int) -> float: camel_github_toolkit = GithubToolkit(repo_name='camel-ai/camel') -added_tools = manager.register_tool([div, camel_github_toolkit]) +added_tools = manager.register_tool( + [div, camel_github_toolkit] +) # manager.register_tool(div) is also supported. + pretty_print_list("Added Tools", added_tools) pretty_print_list("Available Toolkits for now", manager.list_toolkits()) """ From 4b40cce3f9181b2304e8e08e993ba32d247eaaae Mon Sep 17 00:00:00 2001 From: Zack Date: Sat, 12 Oct 2024 16:32:11 +0800 Subject: [PATCH 17/34] Refactor list_toolkit_classes --- camel/toolkits/toolkits_manager.py | 7 ++--- examples/toolkits/toolkts_manager_example.py | 27 ++++++-------------- 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/camel/toolkits/toolkits_manager.py b/camel/toolkits/toolkits_manager.py index 840864219c..be0b427506 100644 --- a/camel/toolkits/toolkits_manager.py +++ b/camel/toolkits/toolkits_manager.py @@ -237,11 +237,12 @@ def list_toolkit_classes(self): result = [] for class_name, methods in self.toolkit_class_methods.items(): - methods_str = ', '.join(methods) + if methods: + methods_str = ', '.join(methods) - formatted_string = f"{class_name}: {methods_str}" + formatted_string = f"{class_name}: {methods_str}" - result.append(formatted_string) + result.append(formatted_string) return result diff --git a/examples/toolkits/toolkts_manager_example.py b/examples/toolkits/toolkts_manager_example.py index 41e8afe37b..31ce82eff6 100644 --- a/examples/toolkits/toolkts_manager_example.py +++ b/examples/toolkits/toolkts_manager_example.py @@ -52,29 +52,18 @@ def pretty_print_list(title, items): ======================================== Class Toolkits: ---------------------------------------- - 1. BaseToolkit: - 2. CodeExecutionToolkit: execute_code - 3. DalleToolkit: - 4. GithubIssue: - 5. GithubPullRequest: - 6. GithubPullRequestDiff: - 7. GithubToolkit: create_pull_request, retrieve_issue, retrieve_issue_list, + 1. CodeExecutionToolkit: execute_code + 2. GithubToolkit: create_pull_request, retrieve_issue, retrieve_issue_list, retrieve_pull_requests - 8. GoogleMapsToolkit: get_address_description, get_elevation, get_timezone - 9. LinkedInToolkit: create_post, delete_post, get_profile - 10. MathToolkit: - 11. OpenAPIToolkit: - 12. OpenAIFunction: - 13. RedditToolkit: collect_top_posts, perform_sentiment_analysis, + 3. GoogleMapsToolkit: get_address_description, get_elevation, get_timezone + 4. LinkedInToolkit: create_post, delete_post, get_profile + 5. RedditToolkit: collect_top_posts, perform_sentiment_analysis, track_keyword_discussions - 14. RetrievalToolkit: information_retrieval - 15. SearchToolkit: - 16. SlackToolkit: create_slack_channel, delete_slack_message, + 6. RetrievalToolkit: information_retrieval + 7. SlackToolkit: create_slack_channel, delete_slack_message, get_slack_channel_information, get_slack_channel_message, join_slack_channel, leave_slack_channel, send_slack_message - 17. ToolManager: - 18. TwitterToolkit: create_tweet, delete_tweet, get_my_user_profile - 19. WeatherToolkit: + 8. TwitterToolkit: create_tweet, delete_tweet, get_my_user_profile ======================================== =============================================================================== """ From 60a043516a4ded4ff9ae593195e20faa13710107 Mon Sep 17 00:00:00 2001 From: Zack Date: Sat, 12 Oct 2024 16:40:07 +0800 Subject: [PATCH 18/34] fix bug --- camel/toolkits/toolkits_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/camel/toolkits/toolkits_manager.py b/camel/toolkits/toolkits_manager.py index be0b427506..6159ad2ce9 100644 --- a/camel/toolkits/toolkits_manager.py +++ b/camel/toolkits/toolkits_manager.py @@ -156,7 +156,7 @@ def _register_single_tool( res = self.add_toolkit_from_instance( **{toolkit_obj.__class__.__name__: toolkit_obj} ) - if "Successfully" in res: + if "Successfully" in res and hasattr(toolkit_obj, 'get_tools'): res_openai_functions.extend(toolkit_obj.get_tools()) res_info += res return res_openai_functions, res_info From 9c4a163aa6ca555cd914556c2bed137d4a44af51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20D=C3=B6rr?= Date: Sun, 13 Oct 2024 12:47:14 +0200 Subject: [PATCH 19/34] feat: Refactor OpenAIFunction to FunctionTool (#966) Co-authored-by: Wendong Co-authored-by: Wendong-Fan <133094783+Wendong-Fan@users.noreply.github.com> --- camel/agents/chat_agent.py | 18 +++++------ camel/configs/base_config.py | 12 +++---- camel/configs/groq_config.py | 2 +- camel/configs/openai_config.py | 2 +- camel/configs/samba_config.py | 2 +- camel/configs/zhipuai_config.py | 2 +- camel/messages/__init__.py | 1 + camel/toolkits/__init__.py | 4 ++- camel/toolkits/base.py | 4 +-- camel/toolkits/code_execution.py | 10 +++--- camel/toolkits/dalle_toolkit.py | 10 +++--- .../{openai_function.py => function_tool.py} | 23 ++++++++++++- camel/toolkits/github_toolkit.py | 16 +++++----- camel/toolkits/google_maps_toolkit.py | 14 ++++---- camel/toolkits/linkedin_toolkit.py | 14 ++++---- camel/toolkits/math_toolkit.py | 14 ++++---- camel/toolkits/open_api_toolkit.py | 10 +++--- camel/toolkits/reddit_toolkit.py | 14 ++++---- camel/toolkits/retrieval_toolkit.py | 10 +++--- camel/toolkits/search_toolkit.py | 16 +++++----- camel/toolkits/slack_toolkit.py | 22 ++++++------- camel/toolkits/toolkits_manager.py | 32 +++++++++---------- camel/toolkits/twitter_toolkit.py | 14 ++++---- camel/toolkits/weather_toolkit.py | 10 +++--- camel/utils/async_func.py | 14 ++++---- docs/camel.toolkits.rst | 2 +- docs/key_modules/tools.md | 10 +++--- examples/function_call/github_examples.py | 4 +-- examples/workforce/hackathon_judges.py | 6 ++-- examples/workforce/multiple_single_agents.py | 6 ++-- test/agents/test_chat_agent.py | 4 +-- test/toolkits/test_openai_function.py | 26 +++++++-------- test/toolkits/test_reddit_functions.py | 4 +-- test/toolkits/test_search_functions.py | 2 +- 34 files changed, 189 insertions(+), 165 deletions(-) rename camel/toolkits/{openai_function.py => function_tool.py} (96%) diff --git a/camel/agents/chat_agent.py b/camel/agents/chat_agent.py index 8621a94483..db3b256f39 100644 --- a/camel/agents/chat_agent.py +++ b/camel/agents/chat_agent.py @@ -63,7 +63,7 @@ from openai import Stream from camel.terminators import ResponseTerminator - from camel.toolkits import OpenAIFunction + from camel.toolkits import FunctionTool logger = logging.getLogger(__name__) @@ -131,10 +131,10 @@ class ChatAgent(BaseAgent): (default: :obj:`None`) output_language (str, optional): The language to be output by the agent. (default: :obj:`None`) - tools (List[OpenAIFunction], optional): List of available - :obj:`OpenAIFunction`. (default: :obj:`None`) - external_tools (List[OpenAIFunction], optional): List of external tools - (:obj:`OpenAIFunction`) bind to one chat agent. When these tools + tools (List[FunctionTool], optional): List of available + :obj:`FunctionTool`. (default: :obj:`None`) + external_tools (List[FunctionTool], optional): List of external tools + (:obj:`FunctionTool`) bind to one chat agent. When these tools are called, the agent will directly return the request instead of processing it. (default: :obj:`None`) response_terminators (List[ResponseTerminator], optional): List of @@ -150,8 +150,8 @@ def __init__( message_window_size: Optional[int] = None, token_limit: Optional[int] = None, output_language: Optional[str] = None, - tools: Optional[List[OpenAIFunction]] = None, - external_tools: Optional[List[OpenAIFunction]] = None, + tools: Optional[List[FunctionTool]] = None, + external_tools: Optional[List[FunctionTool]] = None, response_terminators: Optional[List[ResponseTerminator]] = None, ) -> None: self.orig_sys_message: BaseMessage = system_message @@ -795,12 +795,12 @@ def _structure_output_with_function( r"""Internal function of structuring the output of the agent based on the given output schema. """ - from camel.toolkits import OpenAIFunction + from camel.toolkits import FunctionTool schema_json = get_pydantic_object_schema(output_schema) func_str = json_to_function_code(schema_json) func_callable = func_string_to_callable(func_str) - func = OpenAIFunction(func_callable) + func = FunctionTool(func_callable) original_func_dict = self.func_dict original_model_dict = self.model_backend.model_config_dict diff --git a/camel/configs/base_config.py b/camel/configs/base_config.py index 3d6c95bdc9..8ce9ff24a5 100644 --- a/camel/configs/base_config.py +++ b/camel/configs/base_config.py @@ -39,13 +39,13 @@ class BaseConfig(ABC, BaseModel): @classmethod def fields_type_checking(cls, tools): if tools is not None: - from camel.toolkits import OpenAIFunction + from camel.toolkits import FunctionTool for tool in tools: - if not isinstance(tool, OpenAIFunction): + if not isinstance(tool, FunctionTool): raise ValueError( f"The tool {tool} should " - "be an instance of `OpenAIFunction`." + "be an instance of `FunctionTool`." ) return tools @@ -54,14 +54,14 @@ def as_dict(self) -> dict[str, Any]: tools_schema = None if self.tools: - from camel.toolkits import OpenAIFunction + from camel.toolkits import FunctionTool tools_schema = [] for tool in self.tools: - if not isinstance(tool, OpenAIFunction): + if not isinstance(tool, FunctionTool): raise ValueError( f"The tool {tool} should " - "be an instance of `OpenAIFunction`." + "be an instance of `FunctionTool`." ) tools_schema.append(tool.get_openai_tool_schema()) config_dict["tools"] = tools_schema diff --git a/camel/configs/groq_config.py b/camel/configs/groq_config.py index fe3f2f90ab..43fe2e076f 100644 --- a/camel/configs/groq_config.py +++ b/camel/configs/groq_config.py @@ -73,7 +73,7 @@ class GroqConfig(BaseConfig): user (str, optional): A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. (default: :obj:`""`) - tools (list[OpenAIFunction], optional): A list of tools the model may + tools (list[FunctionTool], optional): A list of tools the model may call. Currently, only functions are supported as a tool. Use this to provide a list of functions the model may generate JSON inputs for. A max of 128 functions are supported. diff --git a/camel/configs/openai_config.py b/camel/configs/openai_config.py index 1adb8cc3fe..971232ad8f 100644 --- a/camel/configs/openai_config.py +++ b/camel/configs/openai_config.py @@ -81,7 +81,7 @@ class ChatGPTConfig(BaseConfig): user (str, optional): A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. (default: :obj:`""`) - tools (list[OpenAIFunction], optional): A list of tools the model may + tools (list[FunctionTool], optional): A list of tools the model may call. Currently, only functions are supported as a tool. Use this to provide a list of functions the model may generate JSON inputs for. A max of 128 functions are supported. diff --git a/camel/configs/samba_config.py b/camel/configs/samba_config.py index 444172270f..409f939cfd 100644 --- a/camel/configs/samba_config.py +++ b/camel/configs/samba_config.py @@ -172,7 +172,7 @@ class SambaCloudAPIConfig(BaseConfig): user (str, optional): A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. (default: :obj:`""`) - tools (list[OpenAIFunction], optional): A list of tools the model may + tools (list[FunctionTool], optional): A list of tools the model may call. Currently, only functions are supported as a tool. Use this to provide a list of functions the model may generate JSON inputs for. A max of 128 functions are supported. diff --git a/camel/configs/zhipuai_config.py b/camel/configs/zhipuai_config.py index 89f2061dfb..21bbec1e7f 100644 --- a/camel/configs/zhipuai_config.py +++ b/camel/configs/zhipuai_config.py @@ -45,7 +45,7 @@ class ZhipuAIConfig(BaseConfig): in the chat completion. The total length of input tokens and generated tokens is limited by the model's context length. (default: :obj:`None`) - tools (list[OpenAIFunction], optional): A list of tools the model may + tools (list[FunctionTool], optional): A list of tools the model may call. Currently, only functions are supported as a tool. Use this to provide a list of functions the model may generate JSON inputs for. A max of 128 functions are supported. diff --git a/camel/messages/__init__.py b/camel/messages/__init__.py index 6e93b5c437..870626024c 100644 --- a/camel/messages/__init__.py +++ b/camel/messages/__init__.py @@ -32,6 +32,7 @@ 'OpenAISystemMessage', 'OpenAIAssistantMessage', 'OpenAIUserMessage', + 'OpenAIFunctionMessage', 'OpenAIMessage', 'BaseMessage', 'FunctionCallingMessage', diff --git a/camel/toolkits/__init__.py b/camel/toolkits/__init__.py index 5f58bdefe9..4b858fa1e8 100644 --- a/camel/toolkits/__init__.py +++ b/camel/toolkits/__init__.py @@ -12,7 +12,8 @@ # limitations under the License. # =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== # ruff: noqa: I001 -from .openai_function import ( +from .function_tool import ( + FunctionTool, OpenAIFunction, get_openai_function_schema, get_openai_tool_schema, @@ -36,6 +37,7 @@ from .toolkits_manager import ToolManager __all__ = [ + 'FunctionTool', 'OpenAIFunction', 'get_openai_function_schema', 'get_openai_tool_schema', diff --git a/camel/toolkits/base.py b/camel/toolkits/base.py index 6e6ee2f6ba..65495f3c7b 100644 --- a/camel/toolkits/base.py +++ b/camel/toolkits/base.py @@ -16,9 +16,9 @@ from camel.utils import AgentOpsMeta -from .openai_function import OpenAIFunction +from .function_tool import FunctionTool class BaseToolkit(metaclass=AgentOpsMeta): - def get_tools(self) -> List[OpenAIFunction]: + def get_tools(self) -> List[FunctionTool]: raise NotImplementedError("Subclasses must implement this method.") diff --git a/camel/toolkits/code_execution.py b/camel/toolkits/code_execution.py index ce8f349680..f6eb6eee3c 100644 --- a/camel/toolkits/code_execution.py +++ b/camel/toolkits/code_execution.py @@ -14,7 +14,7 @@ from typing import List, Literal from camel.interpreters import InternalPythonInterpreter -from camel.toolkits import OpenAIFunction +from camel.toolkits import FunctionTool from camel.utils.commons import export_to_toolkit from .base import BaseToolkit @@ -60,12 +60,12 @@ def execute_code(self, code: str) -> str: print(content) return content - def get_tools(self) -> List[OpenAIFunction]: - r"""Returns a list of OpenAIFunction objects representing the + def get_tools(self) -> List[FunctionTool]: + r"""Returns a list of FunctionTool objects representing the functions in the toolkit. Returns: - List[OpenAIFunction]: A list of OpenAIFunction objects + List[FunctionTool]: A list of FunctionTool objects representing the functions in the toolkit. """ - return [OpenAIFunction(self.execute_code)] + return [FunctionTool(self.execute_code)] diff --git a/camel/toolkits/dalle_toolkit.py b/camel/toolkits/dalle_toolkit.py index 8a039f69dc..83fd3af217 100644 --- a/camel/toolkits/dalle_toolkit.py +++ b/camel/toolkits/dalle_toolkit.py @@ -20,7 +20,7 @@ from openai import OpenAI from PIL import Image -from camel.toolkits import OpenAIFunction +from camel.toolkits import FunctionTool from camel.toolkits.base import BaseToolkit from camel.utils.commons import export_to_toolkit @@ -128,7 +128,7 @@ def get_dalle_img(prompt: str, image_dir: str = "img") -> str: return image_path -DALLE_FUNCS = [OpenAIFunction(get_dalle_img)] +DALLE_FUNCS = [FunctionTool(get_dalle_img)] class DalleToolkit(BaseToolkit): @@ -136,12 +136,12 @@ class DalleToolkit(BaseToolkit): This class provides methods handle image generation using OpenAI's DALL-E. """ - def get_tools(self) -> List[OpenAIFunction]: - r"""Returns a list of OpenAIFunction objects representing the + def get_tools(self) -> List[FunctionTool]: + r"""Returns a list of FunctionTool objects representing the functions in the toolkit. Returns: - List[OpenAIFunction]: A list of OpenAIFunction objects + List[FunctionTool]: A list of FunctionTool objects representing the functions in the toolkit. """ return DALLE_FUNCS diff --git a/camel/toolkits/openai_function.py b/camel/toolkits/function_tool.py similarity index 96% rename from camel/toolkits/openai_function.py rename to camel/toolkits/function_tool.py index e2b35a8843..804bc894e7 100644 --- a/camel/toolkits/openai_function.py +++ b/camel/toolkits/function_tool.py @@ -11,6 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +import warnings from inspect import Parameter, signature from typing import Any, Callable, Dict, Mapping, Optional, Tuple @@ -142,7 +143,7 @@ def _create_mol(name, field): return openai_tool_schema -class OpenAIFunction: +class FunctionTool: r"""An abstraction of a function that OpenAI chat models can call. See https://platform.openai.com/docs/api-reference/chat/create. @@ -387,3 +388,23 @@ def parameters(self, value: Dict[str, Any]) -> None: except SchemaError as e: raise e self.openai_tool_schema["function"]["parameters"]["properties"] = value + + +warnings.simplefilter('always', DeprecationWarning) + + +# Alias for backwards compatibility +class OpenAIFunction(FunctionTool): + def __init__(self, *args, **kwargs): + PURPLE = '\033[95m' + RESET = '\033[0m' + + def purple_warning(msg): + warnings.warn( + PURPLE + msg + RESET, DeprecationWarning, stacklevel=2 + ) + + purple_warning( + "OpenAIFunction is deprecated, please use FunctionTool instead." + ) + super().__init__(*args, **kwargs) diff --git a/camel/toolkits/github_toolkit.py b/camel/toolkits/github_toolkit.py index c2580a1ea0..1942661757 100644 --- a/camel/toolkits/github_toolkit.py +++ b/camel/toolkits/github_toolkit.py @@ -19,7 +19,7 @@ from pydantic import BaseModel from camel.toolkits.base import BaseToolkit -from camel.toolkits.openai_function import OpenAIFunction +from camel.toolkits.function_tool import FunctionTool from camel.utils import dependencies_required from camel.utils.commons import export_to_toolkit @@ -149,19 +149,19 @@ def __init__( self.github = Github(auth=Auth.Token(access_token)) self.repo = self.github.get_repo(repo_name) - def get_tools(self) -> List[OpenAIFunction]: - r"""Returns a list of OpenAIFunction objects representing the + def get_tools(self) -> List[FunctionTool]: + r"""Returns a list of FunctionTool objects representing the functions in the toolkit. Returns: - List[OpenAIFunction]: A list of OpenAIFunction objects + List[FunctionTool]: A list of FunctionTool objects representing the functions in the toolkit. """ return [ - OpenAIFunction(self.retrieve_issue_list), - OpenAIFunction(self.retrieve_issue), - OpenAIFunction(self.create_pull_request), - OpenAIFunction(self.retrieve_pull_requests), + FunctionTool(self.retrieve_issue_list), + FunctionTool(self.retrieve_issue), + FunctionTool(self.create_pull_request), + FunctionTool(self.retrieve_pull_requests), ] @export_to_toolkit diff --git a/camel/toolkits/google_maps_toolkit.py b/camel/toolkits/google_maps_toolkit.py index 42711e6a6c..988b85bf44 100644 --- a/camel/toolkits/google_maps_toolkit.py +++ b/camel/toolkits/google_maps_toolkit.py @@ -16,7 +16,7 @@ from typing import Any, Callable, List, Optional, Union from camel.toolkits.base import BaseToolkit -from camel.toolkits.openai_function import OpenAIFunction +from camel.toolkits.function_tool import FunctionTool from camel.utils import dependencies_required from camel.utils.commons import export_to_toolkit @@ -291,18 +291,18 @@ def get_timezone(self, lat: float, lng: float) -> str: return description - def get_tools(self) -> List[OpenAIFunction]: - r"""Returns a list of OpenAIFunction objects representing the + def get_tools(self) -> List[FunctionTool]: + r"""Returns a list of FunctionTool objects representing the functions in the toolkit. Returns: - List[OpenAIFunction]: A list of OpenAIFunction objects + List[FunctionTool]: A list of FunctionTool objects representing the functions in the toolkit. """ return [ - OpenAIFunction(self.get_address_description), - OpenAIFunction(self.get_elevation), - OpenAIFunction(self.get_timezone), + FunctionTool(self.get_address_description), + FunctionTool(self.get_elevation), + FunctionTool(self.get_timezone), ] diff --git a/camel/toolkits/linkedin_toolkit.py b/camel/toolkits/linkedin_toolkit.py index ef230ba3d8..7cbc11be90 100644 --- a/camel/toolkits/linkedin_toolkit.py +++ b/camel/toolkits/linkedin_toolkit.py @@ -19,7 +19,7 @@ import requests -from camel.toolkits import OpenAIFunction +from camel.toolkits import FunctionTool from camel.toolkits.base import BaseToolkit from camel.utils import handle_http_error from camel.utils.commons import export_to_toolkit @@ -198,18 +198,18 @@ def get_profile(self, include_id: bool = False) -> dict: return profile_report - def get_tools(self) -> List[OpenAIFunction]: - r"""Returns a list of OpenAIFunction objects representing the + def get_tools(self) -> List[FunctionTool]: + r"""Returns a list of FunctionTool objects representing the functions in the toolkit. Returns: - List[OpenAIFunction]: A list of OpenAIFunction objects + List[FunctionTool]: A list of FunctionTool objects representing the functions in the toolkit. """ return [ - OpenAIFunction(self.create_post), - OpenAIFunction(self.delete_post), - OpenAIFunction(self.get_profile), + FunctionTool(self.create_post), + FunctionTool(self.delete_post), + FunctionTool(self.get_profile), ] def _get_access_token(self) -> str: diff --git a/camel/toolkits/math_toolkit.py b/camel/toolkits/math_toolkit.py index 99bd1b9c4c..fe7ac11dd2 100644 --- a/camel/toolkits/math_toolkit.py +++ b/camel/toolkits/math_toolkit.py @@ -15,7 +15,7 @@ from typing import List from camel.toolkits.base import BaseToolkit -from camel.toolkits.openai_function import OpenAIFunction +from camel.toolkits.function_tool import FunctionTool from camel.utils.commons import export_to_toolkit @@ -62,9 +62,9 @@ def mul(a: int, b: int) -> int: MATH_FUNCS = [ - OpenAIFunction(add), - OpenAIFunction(sub), - OpenAIFunction(mul), + FunctionTool(add), + FunctionTool(sub), + FunctionTool(mul), ] @@ -74,12 +74,12 @@ class provides methods for basic mathematical operations such as addition, subtraction, and multiplication. """ - def get_tools(self) -> List[OpenAIFunction]: - r"""Returns a list of OpenAIFunction objects representing the + def get_tools(self) -> List[FunctionTool]: + r"""Returns a list of FunctionTool objects representing the functions in the toolkit. Returns: - List[OpenAIFunction]: A list of OpenAIFunction objects + List[FunctionTool]: A list of FunctionTool objects representing the functions in the toolkit. """ return MATH_FUNCS diff --git a/camel/toolkits/open_api_toolkit.py b/camel/toolkits/open_api_toolkit.py index 10f90f4bba..bca387c311 100644 --- a/camel/toolkits/open_api_toolkit.py +++ b/camel/toolkits/open_api_toolkit.py @@ -17,7 +17,7 @@ import requests -from camel.toolkits import OpenAIFunction, openapi_security_config +from camel.toolkits import FunctionTool, openapi_security_config from camel.types import OpenAPIName @@ -526,12 +526,12 @@ def generate_apinames_filepaths(self) -> List[Tuple[str, str]]: apinames_filepaths.append((api_name.value, file_path)) return apinames_filepaths - def get_tools(self) -> List[OpenAIFunction]: - r"""Returns a list of OpenAIFunction objects representing the + def get_tools(self) -> List[FunctionTool]: + r"""Returns a list of FunctionTool objects representing the functions in the toolkit. Returns: - List[OpenAIFunction]: A list of OpenAIFunction objects + List[FunctionTool]: A list of FunctionTool objects representing the functions in the toolkit. """ apinames_filepaths = self.generate_apinames_filepaths() @@ -539,6 +539,6 @@ def get_tools(self) -> List[OpenAIFunction]: self.apinames_filepaths_to_funs_schemas(apinames_filepaths) ) return [ - OpenAIFunction(a_func, a_schema) + FunctionTool(a_func, a_schema) for a_func, a_schema in zip(all_funcs_lst, all_schemas_lst) ] diff --git a/camel/toolkits/reddit_toolkit.py b/camel/toolkits/reddit_toolkit.py index dcc4eac3b3..96f04d447a 100644 --- a/camel/toolkits/reddit_toolkit.py +++ b/camel/toolkits/reddit_toolkit.py @@ -18,7 +18,7 @@ from requests.exceptions import RequestException -from camel.toolkits import OpenAIFunction +from camel.toolkits import FunctionTool from camel.toolkits.base import BaseToolkit from camel.utils.commons import export_to_toolkit @@ -223,16 +223,16 @@ def track_keyword_discussions( data = self.perform_sentiment_analysis(data) return data - def get_tools(self) -> List[OpenAIFunction]: - r"""Returns a list of OpenAIFunction objects representing the + def get_tools(self) -> List[FunctionTool]: + r"""Returns a list of FunctionTool objects representing the functions in the toolkit. Returns: - List[OpenAIFunction]: A list of OpenAIFunction objects for the + List[FunctionTool]: A list of FunctionTool objects for the toolkit methods. """ return [ - OpenAIFunction(self.collect_top_posts), - OpenAIFunction(self.perform_sentiment_analysis), - OpenAIFunction(self.track_keyword_discussions), + FunctionTool(self.collect_top_posts), + FunctionTool(self.perform_sentiment_analysis), + FunctionTool(self.track_keyword_discussions), ] diff --git a/camel/toolkits/retrieval_toolkit.py b/camel/toolkits/retrieval_toolkit.py index 173c4f7167..7c5bf392ed 100644 --- a/camel/toolkits/retrieval_toolkit.py +++ b/camel/toolkits/retrieval_toolkit.py @@ -14,7 +14,7 @@ from typing import List, Optional, Union from camel.retrievers import AutoRetriever -from camel.toolkits import OpenAIFunction +from camel.toolkits import FunctionTool from camel.toolkits.base import BaseToolkit from camel.types import StorageType from camel.utils import Constants @@ -77,14 +77,14 @@ def information_retrieval( ) return str(retrieved_info) - def get_tools(self) -> List[OpenAIFunction]: - r"""Returns a list of OpenAIFunction objects representing the + def get_tools(self) -> List[FunctionTool]: + r"""Returns a list of FunctionTool objects representing the functions in the toolkit. Returns: - List[OpenAIFunction]: A list of OpenAIFunction objects + List[FunctionTool]: A list of FunctionTool objects representing the functions in the toolkit. """ return [ - OpenAIFunction(self.information_retrieval), + FunctionTool(self.information_retrieval), ] diff --git a/camel/toolkits/search_toolkit.py b/camel/toolkits/search_toolkit.py index 3a1bdd6f3e..66e926d12d 100644 --- a/camel/toolkits/search_toolkit.py +++ b/camel/toolkits/search_toolkit.py @@ -15,7 +15,7 @@ from typing import Any, Dict, List from camel.toolkits.base import BaseToolkit -from camel.toolkits.openai_function import OpenAIFunction +from camel.toolkits.function_tool import FunctionTool from camel.utils.commons import export_to_toolkit @@ -305,10 +305,10 @@ def query_wolfram_alpha(query: str, is_detailed: bool) -> str: SEARCH_FUNCS = [ - OpenAIFunction(search_wiki), - OpenAIFunction(search_duckduckgo), - OpenAIFunction(search_google), - OpenAIFunction(query_wolfram_alpha), + FunctionTool(search_wiki), + FunctionTool(search_duckduckgo), + FunctionTool(search_google), + FunctionTool(query_wolfram_alpha), ] @@ -319,12 +319,12 @@ class SearchToolkit(BaseToolkit): search engines like Google, DuckDuckGo, Wikipedia and Wolfram Alpha. """ - def get_tools(self) -> List[OpenAIFunction]: - r"""Returns a list of OpenAIFunction objects representing the + def get_tools(self) -> List[FunctionTool]: + r"""Returns a list of FunctionTool objects representing the functions in the toolkit. Returns: - List[OpenAIFunction]: A list of OpenAIFunction objects + List[FunctionTool]: A list of FunctionTool objects representing the functions in the toolkit. """ return SEARCH_FUNCS diff --git a/camel/toolkits/slack_toolkit.py b/camel/toolkits/slack_toolkit.py index af23f0c7f1..e19a5b6672 100644 --- a/camel/toolkits/slack_toolkit.py +++ b/camel/toolkits/slack_toolkit.py @@ -27,7 +27,7 @@ from slack_sdk import WebClient -from camel.toolkits import OpenAIFunction +from camel.toolkits import FunctionTool logger = logging.getLogger(__name__) @@ -294,20 +294,20 @@ def delete_slack_message( except SlackApiError as e: return f"Error creating conversation: {e.response['error']}" - def get_tools(self) -> List[OpenAIFunction]: - r"""Returns a list of OpenAIFunction objects representing the + def get_tools(self) -> List[FunctionTool]: + r"""Returns a list of FunctionTool objects representing the functions in the toolkit. Returns: - List[OpenAIFunction]: A list of OpenAIFunction objects + List[FunctionTool]: A list of FunctionTool objects representing the functions in the toolkit. """ return [ - OpenAIFunction(self.create_slack_channel), - OpenAIFunction(self.join_slack_channel), - OpenAIFunction(self.leave_slack_channel), - OpenAIFunction(self.get_slack_channel_information), - OpenAIFunction(self.get_slack_channel_message), - OpenAIFunction(self.send_slack_message), - OpenAIFunction(self.delete_slack_message), + FunctionTool(self.create_slack_channel), + FunctionTool(self.join_slack_channel), + FunctionTool(self.leave_slack_channel), + FunctionTool(self.get_slack_channel_information), + FunctionTool(self.get_slack_channel_message), + FunctionTool(self.send_slack_message), + FunctionTool(self.delete_slack_message), ] diff --git a/camel/toolkits/toolkits_manager.py b/camel/toolkits/toolkits_manager.py index 6159ad2ce9..9ea5f1a4db 100644 --- a/camel/toolkits/toolkits_manager.py +++ b/camel/toolkits/toolkits_manager.py @@ -17,7 +17,7 @@ from typing import Callable, List, Optional, Union from camel.toolkits.base import BaseToolkit -from camel.toolkits.openai_function import OpenAIFunction +from camel.toolkits.function_tool import FunctionTool class ToolManager: @@ -27,7 +27,7 @@ class ToolManager: The ToolManager loads all callable toolkits from the `camel.toolkits` package and provides methods to list, retrieve, and search them as - OpenAIFunction objects. + FunctionTool objects. """ _instance = None @@ -99,7 +99,7 @@ def _load_toolkit_class_and_methods(self): def register_tool( self, toolkit_obj: Union[Callable, object, List[Union[Callable, object]]], - ) -> List[OpenAIFunction] | str: + ) -> List[FunctionTool] | str: r""" Registers a toolkit function or instance and adds it to the toolkits list. If the input is a list, it processes each element in the list. @@ -110,7 +110,7 @@ def register_tool( registered. Returns: - Union[List[OpenAIFunction], str]: Returns a list of OpenAIFunction + Union[List[FunctionTool], str]: Returns a list of FunctionTool instances if the registration is successful. Otherwise, returns a message indicating the failure reason. """ @@ -134,7 +134,7 @@ def register_tool( def _register_single_tool( self, toolkit_obj: Union[Callable, object] - ) -> tuple[List[OpenAIFunction], str]: + ) -> tuple[List[FunctionTool], str]: r""" Helper function to register a single toolkit function or instance. @@ -143,14 +143,14 @@ def _register_single_tool( instance to be processed. Returns: - Tuple: A list of OpenAIFunction instances and a result message. + Tuple: A list of FunctionTool instances and a result message. """ res_openai_functions = [] res_info = "" if callable(toolkit_obj): res = self.add_toolkit_from_function(toolkit_obj) if "successfully" in res: - res_openai_functions.append(OpenAIFunction(toolkit_obj)) + res_openai_functions.append(FunctionTool(toolkit_obj)) res_info += res else: res = self.add_toolkit_from_instance( @@ -246,38 +246,38 @@ def list_toolkit_classes(self): return result - def get_toolkit(self, name: str) -> OpenAIFunction | str: + def get_toolkit(self, name: str) -> FunctionTool | str: r""" - Retrieves the specified toolkit as an OpenAIFunction object. + Retrieves the specified toolkit as an FunctionTool object. Args: name (str): The name of the toolkit function to retrieve. Returns: - OpenAIFunction: The toolkit wrapped as an OpenAIFunction. + FunctionTool: The toolkit wrapped as an FunctionTool. Raises: ValueError: If the specified toolkit is not found. """ toolkit = self.toolkits.get(name) if toolkit: - return OpenAIFunction(toolkit) + return FunctionTool(toolkit) return f"Toolkit '{name}' not found." - def get_toolkits(self, names: list[str]) -> list[OpenAIFunction] | str: + def get_toolkits(self, names: list[str]) -> list[FunctionTool] | str: r""" - Retrieves the specified toolkit as an OpenAIFunction object. + Retrieves the specified toolkit as an FunctionTool object. Args: name (str): The name of the toolkit function to retrieve. Returns: - OpenAIFunctions (list): The toolkits wrapped as an OpenAIFunction. + FunctionTools (list): The toolkits wrapped as an FunctionTool. Raises: ValueError: If the specified toolkit is not found. """ - toolkits: list[OpenAIFunction] = [] + toolkits: list[FunctionTool] = [] for name in names: current_toolkit = self.toolkits.get(name) if current_toolkit: @@ -327,7 +327,7 @@ def search_toolkits( matching_toolkits = [] for name, func in self.toolkits.items(): - openai_func = OpenAIFunction(func) + openai_func = FunctionTool(func) description = openai_func.get_function_description() if algorithm(keyword, description) or algorithm(keyword, name): matching_toolkits.append(name) diff --git a/camel/toolkits/twitter_toolkit.py b/camel/toolkits/twitter_toolkit.py index 227cda1858..7149eaaf06 100644 --- a/camel/toolkits/twitter_toolkit.py +++ b/camel/toolkits/twitter_toolkit.py @@ -19,7 +19,7 @@ import requests -from camel.toolkits import OpenAIFunction +from camel.toolkits import FunctionTool from camel.toolkits.base import BaseToolkit from camel.utils.commons import export_to_toolkit @@ -369,18 +369,18 @@ def get_my_user_profile(self) -> str: return user_report - def get_tools(self) -> List[OpenAIFunction]: - r"""Returns a list of OpenAIFunction objects representing the + def get_tools(self) -> List[FunctionTool]: + r"""Returns a list of FunctionTool objects representing the functions in the toolkit. Returns: - List[OpenAIFunction]: A list of OpenAIFunction objects + List[FunctionTool]: A list of FunctionTool objects representing the functions in the toolkit. """ return [ - OpenAIFunction(self.create_tweet), - OpenAIFunction(self.delete_tweet), - OpenAIFunction(self.get_my_user_profile), + FunctionTool(self.create_tweet), + FunctionTool(self.delete_tweet), + FunctionTool(self.get_my_user_profile), ] def _get_twitter_api_key(self) -> Tuple[str, str]: diff --git a/camel/toolkits/weather_toolkit.py b/camel/toolkits/weather_toolkit.py index e3c598795f..59be0a9fff 100644 --- a/camel/toolkits/weather_toolkit.py +++ b/camel/toolkits/weather_toolkit.py @@ -15,7 +15,7 @@ from typing import List, Literal from camel.toolkits.base import BaseToolkit -from camel.toolkits.openai_function import OpenAIFunction +from camel.toolkits.function_tool import FunctionTool from camel.utils.commons import export_to_toolkit @@ -153,7 +153,7 @@ def get_weather_data( return error_message -WEATHER_FUNCS = [OpenAIFunction(get_weather_data)] +WEATHER_FUNCS = [FunctionTool(get_weather_data)] class WeatherToolkit(BaseToolkit): @@ -163,12 +163,12 @@ class WeatherToolkit(BaseToolkit): using the OpenWeatherMap API. """ - def get_tools(self) -> List[OpenAIFunction]: - r"""Returns a list of OpenAIFunction objects representing the + def get_tools(self) -> List[FunctionTool]: + r"""Returns a list of FunctionTool objects representing the functions in the toolkit. Returns: - List[OpenAIFunction]: A list of OpenAIFunction objects + List[FunctionTool]: A list of FunctionTool objects representing the functions in the toolkit. """ return WEATHER_FUNCS diff --git a/camel/utils/async_func.py b/camel/utils/async_func.py index 377bca4f5e..77caf4e9ec 100644 --- a/camel/utils/async_func.py +++ b/camel/utils/async_func.py @@ -14,20 +14,20 @@ import asyncio from copy import deepcopy -from camel.toolkits import OpenAIFunction +from camel.toolkits import FunctionTool -def sync_funcs_to_async(funcs: list[OpenAIFunction]) -> list[OpenAIFunction]: +def sync_funcs_to_async(funcs: list[FunctionTool]) -> list[FunctionTool]: r"""Convert a list of Python synchronous functions to Python asynchronous functions. Args: - funcs (list[OpenAIFunction]): List of Python synchronous - functions in the :obj:`OpenAIFunction` format. + funcs (list[FunctionTool]): List of Python synchronous + functions in the :obj:`FunctionTool` format. Returns: - list[OpenAIFunction]: List of Python asynchronous functions - in the :obj:`OpenAIFunction` format. + list[FunctionTool]: List of Python asynchronous functions + in the :obj:`FunctionTool` format. """ async_funcs = [] for func in funcs: @@ -37,6 +37,6 @@ def async_callable(*args, **kwargs): return asyncio.to_thread(sync_func, *args, **kwargs) # noqa: B023 async_funcs.append( - OpenAIFunction(async_callable, deepcopy(func.openai_tool_schema)) + FunctionTool(async_callable, deepcopy(func.openai_tool_schema)) ) return async_funcs diff --git a/docs/camel.toolkits.rst b/docs/camel.toolkits.rst index c5647e8fd6..d8c1347309 100644 --- a/docs/camel.toolkits.rst +++ b/docs/camel.toolkits.rst @@ -71,7 +71,7 @@ camel.toolkits.open\_api\_toolkit module camel.toolkits.openai\_function module -------------------------------------- -.. automodule:: camel.toolkits.openai_function +.. automodule:: camel.toolkits.function_tool :members: :undoc-members: :show-inheritance: diff --git a/docs/key_modules/tools.md b/docs/key_modules/tools.md index d1b14e0ceb..4018b7f9c3 100644 --- a/docs/key_modules/tools.md +++ b/docs/key_modules/tools.md @@ -14,7 +14,7 @@ To enhance your agents' capabilities with CAMEL tools, start by installing our a pip install 'camel-ai[tools]' ``` -In CAMEL, a tool is an `OpenAIFunction` that LLMs can call. +In CAMEL, a tool is an `FunctionTool` that LLMs can call. ### 2.1 How to Define Your Own Tool? @@ -22,7 +22,7 @@ In CAMEL, a tool is an `OpenAIFunction` that LLMs can call. Developers can create custom tools tailored to their agent’s specific needs: ```python -from camel.toolkits import OpenAIFunction +from camel.toolkits import FunctionTool def add(a: int, b: int) -> int: r"""Adds two numbers. @@ -36,7 +36,7 @@ def add(a: int, b: int) -> int: """ return a + b -add_tool = OpenAIFunction(add) +add_tool = FunctionTool(add) ``` ```python @@ -105,8 +105,8 @@ To utilize specific tools from the toolkits, you can implement code like the fol ```python from camel.toolkits import SearchToolkit -google_tool = OpenAIFunction(SearchToolkit().search_google) -wiki_tool = OpenAIFunction(SearchToolkit().search_wiki) +google_tool = FunctionTool(SearchToolkit().search_google) +wiki_tool = FunctionTool(SearchToolkit().search_wiki) ``` Here is a list of the available CAMEL tools and their descriptions: diff --git a/examples/function_call/github_examples.py b/examples/function_call/github_examples.py index 48ac5b5aa8..833b0c712c 100644 --- a/examples/function_call/github_examples.py +++ b/examples/function_call/github_examples.py @@ -19,7 +19,7 @@ from camel.configs import ChatGPTConfig from camel.messages import BaseMessage from camel.models import ModelFactory -from camel.toolkits import GithubToolkit, OpenAIFunction +from camel.toolkits import FunctionTool, GithubToolkit from camel.types import ModelPlatformType, ModelType from camel.utils import print_text_animated @@ -70,7 +70,7 @@ def write_weekly_pr_summary(repo_name, model=None): agent = ChatAgent( assistant_sys_msg, model=assistant_model, - tools=[OpenAIFunction(toolkit.retrieve_pull_requests)], + tools=[FunctionTool(toolkit.retrieve_pull_requests)], ) agent.reset() diff --git a/examples/workforce/hackathon_judges.py b/examples/workforce/hackathon_judges.py index ed993b2197..8aeb422164 100644 --- a/examples/workforce/hackathon_judges.py +++ b/examples/workforce/hackathon_judges.py @@ -18,7 +18,7 @@ from camel.messages import BaseMessage from camel.models import ModelFactory from camel.tasks import Task -from camel.toolkits import OpenAIFunction, SearchToolkit +from camel.toolkits import FunctionTool, SearchToolkit from camel.types import ModelPlatformType, ModelType from camel.workforce import Workforce @@ -76,8 +76,8 @@ def main(): search_toolkit = SearchToolkit() search_tools = [ - OpenAIFunction(search_toolkit.search_google), - OpenAIFunction(search_toolkit.search_duckduckgo), + FunctionTool(search_toolkit.search_google), + FunctionTool(search_toolkit.search_duckduckgo), ] researcher_model = ModelFactory.create( diff --git a/examples/workforce/multiple_single_agents.py b/examples/workforce/multiple_single_agents.py index 878bc64418..d18b504818 100644 --- a/examples/workforce/multiple_single_agents.py +++ b/examples/workforce/multiple_single_agents.py @@ -19,8 +19,8 @@ from camel.tasks.task import Task from camel.toolkits import ( WEATHER_FUNCS, + FunctionTool, GoogleMapsToolkit, - OpenAIFunction, SearchToolkit, ) from camel.types import ModelPlatformType, ModelType @@ -30,8 +30,8 @@ def main(): search_toolkit = SearchToolkit() search_tools = [ - OpenAIFunction(search_toolkit.search_google), - OpenAIFunction(search_toolkit.search_duckduckgo), + FunctionTool(search_toolkit.search_google), + FunctionTool(search_toolkit.search_duckduckgo), ] # Set up web searching agent diff --git a/test/agents/test_chat_agent.py b/test/agents/test_chat_agent.py index 01f6860cc8..ba5055d891 100644 --- a/test/agents/test_chat_agent.py +++ b/test/agents/test_chat_agent.py @@ -33,8 +33,8 @@ from camel.models import ModelFactory from camel.terminators import ResponseWordsTerminator from camel.toolkits import ( + FunctionTool, MathToolkit, - OpenAIFunction, SearchToolkit, ) from camel.types import ( @@ -554,7 +554,7 @@ async def async_sleep(second: int) -> int: agent = ChatAgent( system_message=system_message, model=model, - tools=[OpenAIFunction(async_sleep)], + tools=[FunctionTool(async_sleep)], ) assert len(agent.func_dict) == 1 diff --git a/test/toolkits/test_openai_function.py b/test/toolkits/test_openai_function.py index 3df1b32a72..256345001a 100644 --- a/test/toolkits/test_openai_function.py +++ b/test/toolkits/test_openai_function.py @@ -19,7 +19,7 @@ import pytest from jsonschema.exceptions import SchemaError -from camel.toolkits import OpenAIFunction, get_openai_tool_schema +from camel.toolkits import FunctionTool, get_openai_tool_schema from camel.types import RoleType from camel.utils import get_pydantic_major_version @@ -336,13 +336,13 @@ def add_with_wrong_doc(a: int, b: int) -> int: def test_correct_function(): - add = OpenAIFunction(add_with_doc) + add = FunctionTool(add_with_doc) add.set_function_name("add") assert add.get_openai_function_schema() == function_schema def test_function_without_doc(): - add = OpenAIFunction(add_without_doc) + add = FunctionTool(add_without_doc) add.set_function_name("add") with pytest.raises(Exception, match="miss function description"): _ = add.get_openai_function_schema() @@ -351,7 +351,7 @@ def test_function_without_doc(): def test_function_with_wrong_doc(): - add = OpenAIFunction(add_with_wrong_doc) + add = FunctionTool(add_with_wrong_doc) add.set_function_name("add") with pytest.raises(Exception, match="miss description of parameter \"b\""): _ = add.get_openai_function_schema() @@ -360,11 +360,11 @@ def test_function_with_wrong_doc(): def test_validate_openai_tool_schema_valid(): - OpenAIFunction.validate_openai_tool_schema(tool_schema) + FunctionTool.validate_openai_tool_schema(tool_schema) def test_get_set_openai_tool_schema(): - add = OpenAIFunction(add_with_doc) + add = FunctionTool(add_with_doc) assert add.get_openai_tool_schema() is not None new_schema = copy.deepcopy(tool_schema) new_schema["function"]["description"] = "New description" @@ -373,20 +373,20 @@ def test_get_set_openai_tool_schema(): def test_get_set_parameter_description(): - add = OpenAIFunction(add_with_doc) + add = FunctionTool(add_with_doc) assert add.get_paramter_description("a") == "The first number to be added." add.set_paramter_description("a", "New description for a.") assert add.get_paramter_description("a") == "New description for a." def test_get_set_parameter_description_non_existing(): - add = OpenAIFunction(add_with_doc) + add = FunctionTool(add_with_doc) with pytest.raises(KeyError): add.get_paramter_description("non_existing") def test_get_set_openai_function_schema(): - add = OpenAIFunction(add_with_doc) + add = FunctionTool(add_with_doc) initial_schema = add.get_openai_function_schema() assert initial_schema is not None @@ -400,7 +400,7 @@ def test_get_set_openai_function_schema(): def test_get_set_function_name(): - add = OpenAIFunction(add_with_doc) + add = FunctionTool(add_with_doc) assert add.get_function_name() == "add_with_doc" add.set_function_name("new_add") @@ -408,7 +408,7 @@ def test_get_set_function_name(): def test_get_set_function_description(): - add = OpenAIFunction(add_with_doc) + add = FunctionTool(add_with_doc) initial_description = add.get_function_description() assert initial_description is not None @@ -418,7 +418,7 @@ def test_get_set_function_description(): def test_get_set_parameter(): - add = OpenAIFunction(add_with_doc) + add = FunctionTool(add_with_doc) initial_param_schema = add.get_parameter("a") assert initial_param_schema is not None @@ -431,7 +431,7 @@ def test_get_set_parameter(): def test_parameters_getter_setter(): - add = OpenAIFunction(add_with_doc) + add = FunctionTool(add_with_doc) initial_params = add.parameters assert initial_params is not None diff --git a/test/toolkits/test_reddit_functions.py b/test/toolkits/test_reddit_functions.py index 149b749b64..64c1370a16 100644 --- a/test/toolkits/test_reddit_functions.py +++ b/test/toolkits/test_reddit_functions.py @@ -120,8 +120,8 @@ def test_track_keyword_discussions(reddit_toolkit): def test_get_tools(reddit_toolkit): - from camel.toolkits import OpenAIFunction + from camel.toolkits import FunctionTool tools = reddit_toolkit.get_tools() assert len(tools) == 3 - assert all(isinstance(tool, OpenAIFunction) for tool in tools) + assert all(isinstance(tool, FunctionTool) for tool in tools) diff --git a/test/toolkits/test_search_functions.py b/test/toolkits/test_search_functions.py index 71fb81b642..7f49b79de6 100644 --- a/test/toolkits/test_search_functions.py +++ b/test/toolkits/test_search_functions.py @@ -90,7 +90,7 @@ def test_google_api(): assert result.status_code == 200 -search_duckduckgo = SearchToolkit().search_duckduckgo +search_duckduckgo = SearchToolkit().get_tools()[1] def test_search_duckduckgo_text(): From d15aacbe33683b01b96ead9c1a64386497d880fd Mon Sep 17 00:00:00 2001 From: Wendong-Fan <133094783+Wendong-Fan@users.noreply.github.com> Date: Sun, 13 Oct 2024 18:49:18 +0800 Subject: [PATCH 20/34] feat: make system_message as optional (#1038) --- camel/agents/chat_agent.py | 53 +++++++++----- camel/societies/babyagi_playing.py | 9 ++- camel/societies/role_playing.py | 8 ++- test/agents/test_chat_agent.py | 108 +++++++++++++++++++++-------- 4 files changed, 127 insertions(+), 51 deletions(-) diff --git a/camel/agents/chat_agent.py b/camel/agents/chat_agent.py index db3b256f39..8a76d4259f 100644 --- a/camel/agents/chat_agent.py +++ b/camel/agents/chat_agent.py @@ -115,7 +115,8 @@ class ChatAgent(BaseAgent): r"""Class for managing conversations of CAMEL Chat Agents. Args: - system_message (BaseMessage): The system message for the chat agent. + system_message (BaseMessage, optional): The system message for the + chat agent. model (BaseModelBackend, optional): The model backend to use for generating responses. (default: :obj:`OpenAIModel` with `GPT_4O_MINI`) @@ -144,7 +145,7 @@ class ChatAgent(BaseAgent): def __init__( self, - system_message: BaseMessage, + system_message: Optional[BaseMessage] = None, model: Optional[BaseModelBackend] = None, memory: Optional[AgentMemory] = None, message_window_size: Optional[int] = None, @@ -154,10 +155,14 @@ def __init__( external_tools: Optional[List[FunctionTool]] = None, response_terminators: Optional[List[ResponseTerminator]] = None, ) -> None: - self.orig_sys_message: BaseMessage = system_message - self.system_message = system_message - self.role_name: str = system_message.role_name - self.role_type: RoleType = system_message.role_type + self.orig_sys_message: Optional[BaseMessage] = system_message + self._system_message: Optional[BaseMessage] = system_message + self.role_name: str = ( + getattr(system_message, 'role_name', None) or "assistant" + ) + self.role_type: RoleType = ( + getattr(system_message, 'role_type', None) or RoleType.ASSISTANT + ) self.model_backend: BaseModelBackend = ( model if model is not None @@ -272,11 +277,12 @@ def reset(self): terminator.reset() @property - def system_message(self) -> BaseMessage: + def system_message(self) -> Optional[BaseMessage]: r"""The getter method for the property :obj:`system_message`. Returns: - BaseMessage: The system message of this agent. + Optional[BaseMessage]: The system message of this agent if set, + else :obj:`None`. """ return self._system_message @@ -327,12 +333,22 @@ def set_output_language(self, output_language: str) -> BaseMessage: BaseMessage: The updated system message object. """ self.output_language = output_language - content = self.orig_sys_message.content + ( + language_prompt = ( "\nRegardless of the input language, " f"you must output text in {output_language}." ) - self.system_message = self.system_message.create_new_instance(content) - return self.system_message + if self.orig_sys_message is not None: + content = self.orig_sys_message.content + language_prompt + self._system_message = self.orig_sys_message.create_new_instance( + content + ) + return self._system_message + else: + self._system_message = BaseMessage.make_assistant_message( + role_name="Assistant", + content=language_prompt, + ) + return self._system_message def get_info( self, @@ -377,12 +393,15 @@ def init_messages(self) -> None: r"""Initializes the stored messages list with the initial system message. """ - system_record = MemoryRecord( - message=self.system_message, - role_at_backend=OpenAIBackendRole.SYSTEM, - ) - self.memory.clear() - self.memory.write_record(system_record) + if self.orig_sys_message is not None: + system_record = MemoryRecord( + message=self.orig_sys_message, + role_at_backend=OpenAIBackendRole.SYSTEM, + ) + self.memory.clear() + self.memory.write_record(system_record) + else: + self.memory.clear() def record_message(self, message: BaseMessage) -> None: r"""Records the externally provided message into the agent memory as if diff --git a/camel/societies/babyagi_playing.py b/camel/societies/babyagi_playing.py index 45c5c4f72a..cf9feacfbd 100644 --- a/camel/societies/babyagi_playing.py +++ b/camel/societies/babyagi_playing.py @@ -106,7 +106,7 @@ def __init__( ) self.assistant_agent: ChatAgent - self.assistant_sys_msg: BaseMessage + self.assistant_sys_msg: Optional[BaseMessage] self.task_creation_agent: TaskCreationAgent self.task_prioritization_agent: TaskPrioritizationAgent self.init_agents( @@ -202,7 +202,8 @@ def init_agents( self.task_creation_agent = TaskCreationAgent( objective=self.specified_task_prompt, - role_name=self.assistant_sys_msg.role_name, + role_name=getattr(self.assistant_sys_msg, 'role_name', None) + or "assistant", output_language=output_language, message_window_size=message_window_size, **(task_creation_agent_kwargs or {}), @@ -238,7 +239,9 @@ def step(self) -> ChatAgentResponse: task_name = self.subtasks.popleft() assistant_msg_msg = BaseMessage.make_user_message( - role_name=self.assistant_sys_msg.role_name, content=f"{task_name}" + role_name=getattr(self.assistant_sys_msg, 'role_name', None) + or "assistant", + content=f"{task_name}", ) assistant_response = self.assistant_agent.step(assistant_msg_msg) diff --git a/camel/societies/role_playing.py b/camel/societies/role_playing.py index a5c16edbd2..86cd4c4244 100644 --- a/camel/societies/role_playing.py +++ b/camel/societies/role_playing.py @@ -149,8 +149,8 @@ def __init__( self.assistant_agent: ChatAgent self.user_agent: ChatAgent - self.assistant_sys_msg: BaseMessage - self.user_sys_msg: BaseMessage + self.assistant_sys_msg: Optional[BaseMessage] + self.user_sys_msg: Optional[BaseMessage] self._init_agents( init_assistant_sys_msg, init_user_sys_msg, @@ -454,9 +454,11 @@ def init_chat(self, init_msg_content: Optional[str] = None) -> BaseMessage: ) if init_msg_content is None: init_msg_content = default_init_msg_content + # Initialize a message sent by the assistant init_msg = BaseMessage.make_assistant_message( - role_name=self.assistant_sys_msg.role_name, + role_name=getattr(self.assistant_sys_msg, 'role_name', None) + or "assistant", content=init_msg_content, ) diff --git a/test/agents/test_chat_agent.py b/test/agents/test_chat_agent.py index ba5055d891..3ca51fbc7b 100644 --- a/test/agents/test_chat_agent.py +++ b/test/agents/test_chat_agent.py @@ -69,29 +69,37 @@ def test_chat_agent(model): dict(assistant_role="doctor"), role_tuple=("doctor", RoleType.ASSISTANT), ) - assistant = ChatAgent(system_msg, model=model) + assistant_with_sys_msg = ChatAgent(system_msg, model=model) + assistant_without_sys_msg = ChatAgent(model=model) - assert str(assistant) == ( + assert str(assistant_with_sys_msg) == ( "ChatAgent(doctor, " f"RoleType.ASSISTANT, {ModelType.GPT_4O_MINI})" ) + assert str(assistant_without_sys_msg) == ( + "ChatAgent(assistant, " f"RoleType.ASSISTANT, {ModelType.GPT_4O_MINI})" + ) + + for assistant in [assistant_with_sys_msg, assistant_without_sys_msg]: + assistant.reset() - assistant.reset() user_msg = BaseMessage( role_name="Patient", role_type=RoleType.USER, meta_dict=dict(), content="Hello!", ) - assistant_response = assistant.step(user_msg) - assert isinstance(assistant_response.msgs, list) - assert len(assistant_response.msgs) > 0 - assert isinstance(assistant_response.terminated, bool) - assert assistant_response.terminated is False - assert isinstance(assistant_response.info, dict) - assert assistant_response.info['id'] is not None + for assistant in [assistant_with_sys_msg, assistant_without_sys_msg]: + response = assistant.step(user_msg) + assert isinstance(response.msgs, list) + assert len(response.msgs) > 0 + assert isinstance(response.terminated, bool) + assert response.terminated is False + assert isinstance(response.info, dict) + assert response.info['id'] is not None +@pytest.mark.model_backend def test_chat_agent_stored_messages(): system_msg = BaseMessage( role_name="assistant", @@ -99,11 +107,16 @@ def test_chat_agent_stored_messages(): meta_dict=None, content="You are a help assistant.", ) - assistant = ChatAgent(system_msg) + + assistant_with_sys_msg = ChatAgent(system_msg) + assistant_without_sys_msg = ChatAgent() expected_context = [system_msg.to_openai_system_message()] - context, _ = assistant.memory.get_context() - assert context == expected_context + + context_with_sys_msg, _ = assistant_with_sys_msg.memory.get_context() + assert context_with_sys_msg == expected_context + context_without_sys_msg, _ = assistant_without_sys_msg.memory.get_context() + assert context_without_sys_msg == [] user_msg = BaseMessage( role_name="User", @@ -111,13 +124,22 @@ def test_chat_agent_stored_messages(): meta_dict=dict(), content="Tell me a joke.", ) - assistant.update_memory(user_msg, OpenAIBackendRole.USER) - expected_context = [ + + for assistant in [assistant_with_sys_msg, assistant_without_sys_msg]: + assistant.update_memory(user_msg, OpenAIBackendRole.USER) + + expected_context_with_sys_msg = [ system_msg.to_openai_system_message(), user_msg.to_openai_user_message(), ] - context, _ = assistant.memory.get_context() - assert context == expected_context + expected_context_without_sys_msg = [ + user_msg.to_openai_user_message(), + ] + + context_with_sys_msg, _ = assistant_with_sys_msg.memory.get_context() + assert context_with_sys_msg == expected_context_with_sys_msg + context_without_sys_msg, _ = assistant_without_sys_msg.memory.get_context() + assert context_without_sys_msg == expected_context_without_sys_msg @pytest.mark.model_backend @@ -273,17 +295,27 @@ def test_chat_agent_multiple_return_messages(n): meta_dict=None, content="You are a helpful assistant.", ) - assistant = ChatAgent(system_msg, model=model) - assistant.reset() + assistant_with_sys_msg = ChatAgent(system_msg, model=model) + assistant_without_sys_msg = ChatAgent(model=model) + + assistant_with_sys_msg.reset() + assistant_without_sys_msg.reset() + user_msg = BaseMessage( role_name="User", role_type=RoleType.USER, meta_dict=dict(), content="Tell me a joke.", ) - assistant_response = assistant.step(user_msg) - assert assistant_response.msgs is not None - assert len(assistant_response.msgs) == n + assistant_with_sys_msg_response = assistant_with_sys_msg.step(user_msg) + assistant_without_sys_msg_response = assistant_without_sys_msg.step( + user_msg + ) + + assert assistant_with_sys_msg_response.msgs is not None + assert len(assistant_with_sys_msg_response.msgs) == n + assert assistant_without_sys_msg_response.msgs is not None + assert len(assistant_without_sys_msg_response.msgs) == n @pytest.mark.model_backend @@ -396,21 +428,41 @@ def test_set_multiple_output_language(): meta_dict=None, content="You are a help assistant.", ) - agent = ChatAgent(system_message=system_message) + agent_with_sys_msg = ChatAgent(system_message=system_message) + agent_without_sys_msg = ChatAgent() # Verify that the length of the system message is kept constant even when # multiple set_output_language operations are called - agent.set_output_language("Chinese") - agent.set_output_language("English") - agent.set_output_language("French") - updated_system_message = BaseMessage( + agent_with_sys_msg.set_output_language("Chinese") + agent_with_sys_msg.set_output_language("English") + agent_with_sys_msg.set_output_language("French") + agent_without_sys_msg.set_output_language("Chinese") + agent_without_sys_msg.set_output_language("English") + agent_without_sys_msg.set_output_language("French") + + updated_system_message_with_content = BaseMessage( role_name="assistant", role_type=RoleType.ASSISTANT, meta_dict=None, content="You are a help assistant." "\nRegardless of the input language, you must output text in French.", ) - assert agent.system_message.content == updated_system_message.content + updated_system_message_without_content = BaseMessage( + role_name="assistant", + role_type=RoleType.ASSISTANT, + meta_dict=None, + content="\nRegardless of the input language, you must output text " + "in French.", + ) + + assert ( + agent_with_sys_msg.system_message.content + == updated_system_message_with_content.content + ) + assert ( + agent_without_sys_msg.system_message.content + == updated_system_message_without_content.content + ) @pytest.mark.model_backend From 8af6911eca2ca359cb70a1c8f7521bbb19728939 Mon Sep 17 00:00:00 2001 From: Zack Date: Mon, 14 Oct 2024 23:03:18 +0800 Subject: [PATCH 21/34] Refactor search_toolkits method of ToolkitManager --- camel/toolkits/__init__.py | 4 +- camel/toolkits/function_tool.py | 8 +++ camel/toolkits/toolkits_manager.py | 38 ++++++------ examples/toolkits/toolkts_manager_example.py | 63 ++++++-------------- 4 files changed, 45 insertions(+), 68 deletions(-) diff --git a/camel/toolkits/__init__.py b/camel/toolkits/__init__.py index 4b858fa1e8..db9b92387b 100644 --- a/camel/toolkits/__init__.py +++ b/camel/toolkits/__init__.py @@ -34,7 +34,7 @@ from .code_execution import CodeExecutionToolkit from .github_toolkit import GithubToolkit -from .toolkits_manager import ToolManager +from .toolkits_manager import ToolkitManager __all__ = [ 'FunctionTool', @@ -59,5 +59,5 @@ 'SEARCH_FUNCS', 'WEATHER_FUNCS', 'DALLE_FUNCS', - 'ToolManager', + 'ToolkitManager', ] diff --git a/camel/toolkits/function_tool.py b/camel/toolkits/function_tool.py index 804bc894e7..342c1b4396 100644 --- a/camel/toolkits/function_tool.py +++ b/camel/toolkits/function_tool.py @@ -162,11 +162,13 @@ def __init__( self, func: Callable, openai_tool_schema: Optional[Dict[str, Any]] = None, + alias_name: Optional[str] = None, ) -> None: self.func = func self.openai_tool_schema = openai_tool_schema or get_openai_tool_schema( func ) + self.alias_name = alias_name @staticmethod def validate_openai_tool_schema( @@ -389,6 +391,12 @@ def parameters(self, value: Dict[str, Any]) -> None: raise e self.openai_tool_schema["function"]["parameters"]["properties"] = value + def __str__(self) -> str: + return self.alias_name if self.alias_name else self.func.__name__ + + def __repr__(self) -> str: + return self.alias_name if self.alias_name else self.func.__name__ + warnings.simplefilter('always', DeprecationWarning) diff --git a/camel/toolkits/toolkits_manager.py b/camel/toolkits/toolkits_manager.py index 9ea5f1a4db..62a16228ed 100644 --- a/camel/toolkits/toolkits_manager.py +++ b/camel/toolkits/toolkits_manager.py @@ -20,12 +20,12 @@ from camel.toolkits.function_tool import FunctionTool -class ToolManager: +class ToolkitManager: r""" A class representing a manager for dynamically loading and accessing toolkits. - The ToolManager loads all callable toolkits from the `camel.toolkits` + The ToolkitManager loads all callable toolkits from the `camel.toolkits` package and provides methods to list, retrieve, and search them as FunctionTool objects. """ @@ -34,14 +34,14 @@ class ToolManager: def __new__(cls, *args, **kwargs): if not cls._instance: - cls._instance = super(ToolManager, cls).__new__( + cls._instance = super(ToolkitManager, cls).__new__( cls, *args, **kwargs ) return cls._instance def __init__(self): r""" - Initializes the ToolManager and loads all available toolkits. + Initializes the ToolkitManager and loads all available toolkits. """ if not hasattr(self, '_initialized'): self._initialized = True @@ -261,7 +261,7 @@ def get_toolkit(self, name: str) -> FunctionTool | str: """ toolkit = self.toolkits.get(name) if toolkit: - return FunctionTool(toolkit) + return FunctionTool(func=toolkit, alias_name=name) return f"Toolkit '{name}' not found." def get_toolkits(self, names: list[str]) -> list[FunctionTool] | str: @@ -273,15 +273,14 @@ def get_toolkits(self, names: list[str]) -> list[FunctionTool] | str: Returns: FunctionTools (list): The toolkits wrapped as an FunctionTool. - - Raises: - ValueError: If the specified toolkit is not found. """ toolkits: list[FunctionTool] = [] for name in names: current_toolkit = self.toolkits.get(name) if current_toolkit: - toolkits.append(current_toolkit) + toolkits.append( + FunctionTool(func=current_toolkit, alias_name=name) + ) if len(toolkits) > 0: return toolkits return "Toolkits are not found." @@ -298,7 +297,7 @@ def _default_search_algorithm( Returns: bool: True if a match is found based on similarity, False - otherwise. + otherwise. """ return keyword.lower() in description.lower() @@ -306,30 +305,29 @@ def search_toolkits( self, keyword: str, algorithm: Optional[Callable[[str, str], bool]] = None, - ) -> List[str]: + ) -> List[FunctionTool] | str: r""" Searches for toolkits based on a keyword in their descriptions using the provided search algorithm. Args: keyword (str): The keyword to search for in toolkit descriptions. - algorithm (Callable[[str, str], bool], optional): A custom search - algorithm function - that accepts the keyword and description and returns a boolean. - Defaults to fuzzy matching. + algorithm (Callable[[str, str], bool], optional): A custom + search algorithm function that accepts the keyword and + description and returns a boolean. Defaults to fuzzy matching. Returns: - List[str]: A list of toolkit names whose descriptions match the - keyword. + List[FunctionTool] | str: A list of toolkit names whose + descriptions match the keyword. """ if algorithm is None: algorithm = self._default_search_algorithm - matching_toolkits = [] + matching_toolkits_names = [] for name, func in self.toolkits.items(): openai_func = FunctionTool(func) description = openai_func.get_function_description() if algorithm(keyword, description) or algorithm(keyword, name): - matching_toolkits.append(name) + matching_toolkits_names.append(name) - return matching_toolkits + return self.get_toolkits(matching_toolkits_names) diff --git a/examples/toolkits/toolkts_manager_example.py b/examples/toolkits/toolkts_manager_example.py index 31ce82eff6..c21e363c95 100644 --- a/examples/toolkits/toolkts_manager_example.py +++ b/examples/toolkits/toolkts_manager_example.py @@ -11,9 +11,9 @@ # See the License for the specific language governing permissions and # limitations under the License. # =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== -from camel.toolkits import ToolManager +from camel.toolkits import ToolkitManager +from camel.toolkits.function_tool import FunctionTool from camel.toolkits.github_toolkit import GithubToolkit -from camel.toolkits.openai_function import OpenAIFunction def pretty_print_list(title, items): @@ -26,7 +26,7 @@ def pretty_print_list(title, items): print('=' * 40) -manager = ToolManager() +manager = ToolkitManager() toolkits = manager.list_toolkits() toolkit_classes = manager.list_toolkit_classes() @@ -99,7 +99,7 @@ def strict_search_algorithm(keyword: str, description: str) -> bool: """ tool = manager.get_toolkit('get_weather_data') -if isinstance(tool, OpenAIFunction): +if isinstance(tool, FunctionTool): print("\nFunction Description:") print('-' * 40) print(tool.get_function_description()) @@ -151,11 +151,11 @@ def div(a: int, b: int) -> float: ======================================== Added Tools: ---------------------------------------- - 1. - 2. - 3. - 4. - 5. + 1. + 2. + 3. + 4. + 5. ======================================== ======================================== Available Toolkits for now: @@ -192,43 +192,14 @@ def div(a: int, b: int) -> float: ======================================== Matching Tools for GitHub: ---------------------------------------- - 1. GithubToolkit_create_pull_request - 2. GithubToolkit_retrieve_issue - 3. GithubToolkit_retrieve_issue_list - 4. GithubToolkit_retrieve_pull_requests - 5. crab_github_toolkit_create_pull_request - 6. crab_github_toolkit_retrieve_issue - 7. crab_github_toolkit_retrieve_issue_list - 8. crab_github_toolkit_retrieve_pull_requests -======================================== -=============================================================================== -""" - -if isinstance(matching_tools_for_github, list): - tools_instances = manager.get_toolkits(names=matching_tools_for_github) - pretty_print_list("Tools Instances", tools_instances) - -""" -=============================================================================== -======================================== -Tools Instances: ----------------------------------------- - 1. > - 2. > - 3. > - 4. > - 5. > - 6. > - 7. > - 8. > + 1. create_pull_request + 2. retrieve_issue + 3. retrieve_issue_list + 4. retrieve_pull_requests + 5. create_pull_request + 6. retrieve_issue + 7. retrieve_issue_list + 8. retrieve_pull_requests ======================================== =============================================================================== """ From ad2db6b4132a4c0a4b88aa6bebe9dd5737827d85 Mon Sep 17 00:00:00 2001 From: Zack Date: Mon, 14 Oct 2024 23:43:40 +0800 Subject: [PATCH 22/34] Refactor get_tools method in BaseToolkit --- camel/toolkits/base.py | 22 ++++++++++++- camel/toolkits/code_execution.py | 10 ------ camel/toolkits/function_tool.py | 16 ++++++--- camel/toolkits/github_toolkit.py | 15 --------- camel/toolkits/google_maps_toolkit.py | 17 ---------- camel/toolkits/linkedin_toolkit.py | 14 -------- camel/toolkits/reddit_toolkit.py | 14 -------- camel/toolkits/retrieval_toolkit.py | 12 ------- camel/toolkits/slack_toolkit.py | 18 ----------- camel/toolkits/toolkits_manager.py | 8 +++-- camel/toolkits/twitter_toolkit.py | 14 -------- examples/toolkits/toolkts_manager_example.py | 34 ++++++++++---------- 12 files changed, 55 insertions(+), 139 deletions(-) diff --git a/camel/toolkits/base.py b/camel/toolkits/base.py index 65495f3c7b..4ddafe4f29 100644 --- a/camel/toolkits/base.py +++ b/camel/toolkits/base.py @@ -12,6 +12,7 @@ # limitations under the License. # =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +import inspect from typing import List from camel.utils import AgentOpsMeta @@ -21,4 +22,23 @@ class BaseToolkit(metaclass=AgentOpsMeta): def get_tools(self) -> List[FunctionTool]: - raise NotImplementedError("Subclasses must implement this method.") + """Returns a list of FunctionTool objects representing the + functions in the toolkit. + + Returns: + List[FunctionTool]: A list of FunctionTool objects + representing the functions in the toolkit. + """ + tools = [] + for _, method in inspect.getmembers(self, predicate=inspect.ismethod): + if getattr(method, '_is_exported', False): + tools.append( + FunctionTool( + func=method, name_prefix=self.__class__.__name__ + ) + ) + + if not tools: + raise NotImplementedError("Subclasses must implement the methods") + + return tools diff --git a/camel/toolkits/code_execution.py b/camel/toolkits/code_execution.py index f6eb6eee3c..a4a4f5ffcb 100644 --- a/camel/toolkits/code_execution.py +++ b/camel/toolkits/code_execution.py @@ -59,13 +59,3 @@ def execute_code(self, code: str) -> str: if self.verbose: print(content) return content - - def get_tools(self) -> List[FunctionTool]: - r"""Returns a list of FunctionTool objects representing the - functions in the toolkit. - - Returns: - List[FunctionTool]: A list of FunctionTool objects - representing the functions in the toolkit. - """ - return [FunctionTool(self.execute_code)] diff --git a/camel/toolkits/function_tool.py b/camel/toolkits/function_tool.py index 342c1b4396..b8c7d0d0c6 100644 --- a/camel/toolkits/function_tool.py +++ b/camel/toolkits/function_tool.py @@ -162,13 +162,13 @@ def __init__( self, func: Callable, openai_tool_schema: Optional[Dict[str, Any]] = None, - alias_name: Optional[str] = None, + name_prefix: Optional[str] = None, ) -> None: self.func = func self.openai_tool_schema = openai_tool_schema or get_openai_tool_schema( func ) - self.alias_name = alias_name + self.name_prefix = name_prefix @staticmethod def validate_openai_tool_schema( @@ -392,10 +392,18 @@ def parameters(self, value: Dict[str, Any]) -> None: self.openai_tool_schema["function"]["parameters"]["properties"] = value def __str__(self) -> str: - return self.alias_name if self.alias_name else self.func.__name__ + return ( + self.name_prefix + '.' + self.func.__name__ + if self.name_prefix + else self.func.__name__ + ) def __repr__(self) -> str: - return self.alias_name if self.alias_name else self.func.__name__ + return ( + self.name_prefix + '.' + self.func.__name__ + if self.name_prefix + else self.func.__name__ + ) warnings.simplefilter('always', DeprecationWarning) diff --git a/camel/toolkits/github_toolkit.py b/camel/toolkits/github_toolkit.py index 1942661757..05fc4eb41a 100644 --- a/camel/toolkits/github_toolkit.py +++ b/camel/toolkits/github_toolkit.py @@ -149,21 +149,6 @@ def __init__( self.github = Github(auth=Auth.Token(access_token)) self.repo = self.github.get_repo(repo_name) - def get_tools(self) -> List[FunctionTool]: - r"""Returns a list of FunctionTool objects representing the - functions in the toolkit. - - Returns: - List[FunctionTool]: A list of FunctionTool objects - representing the functions in the toolkit. - """ - return [ - FunctionTool(self.retrieve_issue_list), - FunctionTool(self.retrieve_issue), - FunctionTool(self.create_pull_request), - FunctionTool(self.retrieve_pull_requests), - ] - @export_to_toolkit def retrieve_issue_list(self) -> List[GithubIssue]: r"""Retrieve a list of open issues from the repository. diff --git a/camel/toolkits/google_maps_toolkit.py b/camel/toolkits/google_maps_toolkit.py index 988b85bf44..2478838249 100644 --- a/camel/toolkits/google_maps_toolkit.py +++ b/camel/toolkits/google_maps_toolkit.py @@ -290,20 +290,3 @@ def get_timezone(self, lat: float, lng: float) -> str: ) return description - - def get_tools(self) -> List[FunctionTool]: - r"""Returns a list of FunctionTool objects representing the - functions in the toolkit. - - Returns: - List[FunctionTool]: A list of FunctionTool objects - representing the functions in the toolkit. - """ - return [ - FunctionTool(self.get_address_description), - FunctionTool(self.get_elevation), - FunctionTool(self.get_timezone), - ] - - -__all__: list[str] = [] diff --git a/camel/toolkits/linkedin_toolkit.py b/camel/toolkits/linkedin_toolkit.py index 7cbc11be90..3dcb8b9532 100644 --- a/camel/toolkits/linkedin_toolkit.py +++ b/camel/toolkits/linkedin_toolkit.py @@ -198,20 +198,6 @@ def get_profile(self, include_id: bool = False) -> dict: return profile_report - def get_tools(self) -> List[FunctionTool]: - r"""Returns a list of FunctionTool objects representing the - functions in the toolkit. - - Returns: - List[FunctionTool]: A list of FunctionTool objects - representing the functions in the toolkit. - """ - return [ - FunctionTool(self.create_post), - FunctionTool(self.delete_post), - FunctionTool(self.get_profile), - ] - def _get_access_token(self) -> str: r"""Fetches the access token required for making LinkedIn API requests. diff --git a/camel/toolkits/reddit_toolkit.py b/camel/toolkits/reddit_toolkit.py index 96f04d447a..3e3395a4f9 100644 --- a/camel/toolkits/reddit_toolkit.py +++ b/camel/toolkits/reddit_toolkit.py @@ -222,17 +222,3 @@ def track_keyword_discussions( if sentiment_analysis: data = self.perform_sentiment_analysis(data) return data - - def get_tools(self) -> List[FunctionTool]: - r"""Returns a list of FunctionTool objects representing the - functions in the toolkit. - - Returns: - List[FunctionTool]: A list of FunctionTool objects for the - toolkit methods. - """ - return [ - FunctionTool(self.collect_top_posts), - FunctionTool(self.perform_sentiment_analysis), - FunctionTool(self.track_keyword_discussions), - ] diff --git a/camel/toolkits/retrieval_toolkit.py b/camel/toolkits/retrieval_toolkit.py index 7c5bf392ed..f00813c76c 100644 --- a/camel/toolkits/retrieval_toolkit.py +++ b/camel/toolkits/retrieval_toolkit.py @@ -76,15 +76,3 @@ def information_retrieval( similarity_threshold=similarity_threshold, ) return str(retrieved_info) - - def get_tools(self) -> List[FunctionTool]: - r"""Returns a list of FunctionTool objects representing the - functions in the toolkit. - - Returns: - List[FunctionTool]: A list of FunctionTool objects - representing the functions in the toolkit. - """ - return [ - FunctionTool(self.information_retrieval), - ] diff --git a/camel/toolkits/slack_toolkit.py b/camel/toolkits/slack_toolkit.py index e19a5b6672..ea689e022a 100644 --- a/camel/toolkits/slack_toolkit.py +++ b/camel/toolkits/slack_toolkit.py @@ -293,21 +293,3 @@ def delete_slack_message( return str(response) except SlackApiError as e: return f"Error creating conversation: {e.response['error']}" - - def get_tools(self) -> List[FunctionTool]: - r"""Returns a list of FunctionTool objects representing the - functions in the toolkit. - - Returns: - List[FunctionTool]: A list of FunctionTool objects - representing the functions in the toolkit. - """ - return [ - FunctionTool(self.create_slack_channel), - FunctionTool(self.join_slack_channel), - FunctionTool(self.leave_slack_channel), - FunctionTool(self.get_slack_channel_information), - FunctionTool(self.get_slack_channel_message), - FunctionTool(self.send_slack_message), - FunctionTool(self.delete_slack_message), - ] diff --git a/camel/toolkits/toolkits_manager.py b/camel/toolkits/toolkits_manager.py index 62a16228ed..3674f2d933 100644 --- a/camel/toolkits/toolkits_manager.py +++ b/camel/toolkits/toolkits_manager.py @@ -204,7 +204,7 @@ def add_toolkit_from_instance(self, **kwargs): attr = getattr(toolkit_instance, attr_name) if callable(attr) and hasattr(attr, '_is_exported'): - method_name = f"{toolkit_instance_name}_{attr_name}" + method_name = f"{toolkit_instance_name}.{attr_name}" self.toolkits[method_name] = attr messages.append(f"Successfully added {method_name}.") @@ -261,7 +261,7 @@ def get_toolkit(self, name: str) -> FunctionTool | str: """ toolkit = self.toolkits.get(name) if toolkit: - return FunctionTool(func=toolkit, alias_name=name) + return FunctionTool(func=toolkit, name_prefix=name.split('.')[0]) return f"Toolkit '{name}' not found." def get_toolkits(self, names: list[str]) -> list[FunctionTool] | str: @@ -279,7 +279,9 @@ def get_toolkits(self, names: list[str]) -> list[FunctionTool] | str: current_toolkit = self.toolkits.get(name) if current_toolkit: toolkits.append( - FunctionTool(func=current_toolkit, alias_name=name) + FunctionTool( + func=current_toolkit, name_prefix=name.split('.')[0] + ) ) if len(toolkits) > 0: return toolkits diff --git a/camel/toolkits/twitter_toolkit.py b/camel/toolkits/twitter_toolkit.py index 7149eaaf06..dcb0304ebf 100644 --- a/camel/toolkits/twitter_toolkit.py +++ b/camel/toolkits/twitter_toolkit.py @@ -369,20 +369,6 @@ def get_my_user_profile(self) -> str: return user_report - def get_tools(self) -> List[FunctionTool]: - r"""Returns a list of FunctionTool objects representing the - functions in the toolkit. - - Returns: - List[FunctionTool]: A list of FunctionTool objects - representing the functions in the toolkit. - """ - return [ - FunctionTool(self.create_tweet), - FunctionTool(self.delete_tweet), - FunctionTool(self.get_my_user_profile), - ] - def _get_twitter_api_key(self) -> Tuple[str, str]: r"""Retrieve the Twitter API key and secret from environment variables. diff --git a/examples/toolkits/toolkts_manager_example.py b/examples/toolkits/toolkts_manager_example.py index c21e363c95..2622260b75 100644 --- a/examples/toolkits/toolkts_manager_example.py +++ b/examples/toolkits/toolkts_manager_example.py @@ -151,11 +151,11 @@ def div(a: int, b: int) -> float: ======================================== Added Tools: ---------------------------------------- - 1. - 2. - 3. - 4. - 5. + 1. div + 2. GithubToolkit.retrieve_issue_list + 3. GithubToolkit.retrieve_issue + 4. GithubToolkit.create_pull_request + 5. GithubToolkit.retrieve_pull_requests ======================================== ======================================== Available Toolkits for now: @@ -170,10 +170,10 @@ def div(a: int, b: int) -> float: 8. search_wiki 9. get_weather_data 10. div - 11. GithubToolkit_create_pull_request - 12. GithubToolkit_retrieve_issue - 13. GithubToolkit_retrieve_issue_list - 14. GithubToolkit_retrieve_pull_requests + 11. GithubToolkit.create_pull_request + 12. GithubToolkit.retrieve_issue + 13. GithubToolkit.retrieve_issue_list + 14. GithubToolkit.retrieve_pull_requests ======================================== =============================================================================== """ @@ -192,14 +192,14 @@ def div(a: int, b: int) -> float: ======================================== Matching Tools for GitHub: ---------------------------------------- - 1. create_pull_request - 2. retrieve_issue - 3. retrieve_issue_list - 4. retrieve_pull_requests - 5. create_pull_request - 6. retrieve_issue - 7. retrieve_issue_list - 8. retrieve_pull_requests + 1. GithubToolkit.create_pull_request + 2. GithubToolkit.retrieve_issue + 3. GithubToolkit.retrieve_issue_list + 4. GithubToolkit.retrieve_pull_requests + 5. crab_github_toolkit.create_pull_request + 6. crab_github_toolkit.retrieve_issue + 7. crab_github_toolkit.retrieve_issue_list + 8. crab_github_toolkit.retrieve_pull_requests ======================================== =============================================================================== """ From 90923248aba7341823fda1f78a69d1b1b28057d0 Mon Sep 17 00:00:00 2001 From: Zack Date: Mon, 14 Oct 2024 23:46:11 +0800 Subject: [PATCH 23/34] Format code --- camel/toolkits/code_execution.py | 3 +-- camel/toolkits/github_toolkit.py | 1 - camel/toolkits/google_maps_toolkit.py | 1 - camel/toolkits/linkedin_toolkit.py | 2 -- camel/toolkits/reddit_toolkit.py | 1 - camel/toolkits/retrieval_toolkit.py | 1 - camel/toolkits/slack_toolkit.py | 3 +-- camel/toolkits/twitter_toolkit.py | 1 - 8 files changed, 2 insertions(+), 11 deletions(-) diff --git a/camel/toolkits/code_execution.py b/camel/toolkits/code_execution.py index a4a4f5ffcb..f841d7362c 100644 --- a/camel/toolkits/code_execution.py +++ b/camel/toolkits/code_execution.py @@ -11,10 +11,9 @@ # 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, Literal +from typing import Literal from camel.interpreters import InternalPythonInterpreter -from camel.toolkits import FunctionTool from camel.utils.commons import export_to_toolkit from .base import BaseToolkit diff --git a/camel/toolkits/github_toolkit.py b/camel/toolkits/github_toolkit.py index 05fc4eb41a..4fa01f6f7e 100644 --- a/camel/toolkits/github_toolkit.py +++ b/camel/toolkits/github_toolkit.py @@ -19,7 +19,6 @@ from pydantic import BaseModel from camel.toolkits.base import BaseToolkit -from camel.toolkits.function_tool import FunctionTool from camel.utils import dependencies_required from camel.utils.commons import export_to_toolkit diff --git a/camel/toolkits/google_maps_toolkit.py b/camel/toolkits/google_maps_toolkit.py index 2478838249..29aaa34a1a 100644 --- a/camel/toolkits/google_maps_toolkit.py +++ b/camel/toolkits/google_maps_toolkit.py @@ -16,7 +16,6 @@ from typing import Any, Callable, List, Optional, Union from camel.toolkits.base import BaseToolkit -from camel.toolkits.function_tool import FunctionTool from camel.utils import dependencies_required from camel.utils.commons import export_to_toolkit diff --git a/camel/toolkits/linkedin_toolkit.py b/camel/toolkits/linkedin_toolkit.py index 3dcb8b9532..24ee407a92 100644 --- a/camel/toolkits/linkedin_toolkit.py +++ b/camel/toolkits/linkedin_toolkit.py @@ -15,11 +15,9 @@ import json import os from http import HTTPStatus -from typing import List import requests -from camel.toolkits import FunctionTool from camel.toolkits.base import BaseToolkit from camel.utils import handle_http_error from camel.utils.commons import export_to_toolkit diff --git a/camel/toolkits/reddit_toolkit.py b/camel/toolkits/reddit_toolkit.py index 3e3395a4f9..7795dfdb19 100644 --- a/camel/toolkits/reddit_toolkit.py +++ b/camel/toolkits/reddit_toolkit.py @@ -18,7 +18,6 @@ from requests.exceptions import RequestException -from camel.toolkits import FunctionTool from camel.toolkits.base import BaseToolkit from camel.utils.commons import export_to_toolkit diff --git a/camel/toolkits/retrieval_toolkit.py b/camel/toolkits/retrieval_toolkit.py index f00813c76c..87483c7847 100644 --- a/camel/toolkits/retrieval_toolkit.py +++ b/camel/toolkits/retrieval_toolkit.py @@ -14,7 +14,6 @@ from typing import List, Optional, Union from camel.retrievers import AutoRetriever -from camel.toolkits import FunctionTool from camel.toolkits.base import BaseToolkit from camel.types import StorageType from camel.utils import Constants diff --git a/camel/toolkits/slack_toolkit.py b/camel/toolkits/slack_toolkit.py index ea689e022a..d2b24fff5e 100644 --- a/camel/toolkits/slack_toolkit.py +++ b/camel/toolkits/slack_toolkit.py @@ -17,7 +17,7 @@ import json import logging import os -from typing import TYPE_CHECKING, List, Optional +from typing import TYPE_CHECKING, Optional from camel.toolkits.base import BaseToolkit from camel.utils.commons import export_to_toolkit @@ -27,7 +27,6 @@ from slack_sdk import WebClient -from camel.toolkits import FunctionTool logger = logging.getLogger(__name__) diff --git a/camel/toolkits/twitter_toolkit.py b/camel/toolkits/twitter_toolkit.py index dcb0304ebf..fd26761f92 100644 --- a/camel/toolkits/twitter_toolkit.py +++ b/camel/toolkits/twitter_toolkit.py @@ -19,7 +19,6 @@ import requests -from camel.toolkits import FunctionTool from camel.toolkits.base import BaseToolkit from camel.utils.commons import export_to_toolkit From 816cb01f71483848c5cf92c7564bc5ef9a4cd7b0 Mon Sep 17 00:00:00 2001 From: Zack Date: Tue, 15 Oct 2024 00:05:02 +0800 Subject: [PATCH 24/34] Refactor FUNCS and the names of functional toolkits --- camel/toolkits/dalle_toolkit.py | 8 ++-- camel/toolkits/math_toolkit.py | 13 +++--- camel/toolkits/search_toolkit.py | 19 +++++---- camel/toolkits/toolkits_manager.py | 10 ++++- camel/toolkits/weather_toolkit.py | 8 ++-- examples/toolkits/toolkts_manager_example.py | 42 ++++++++++---------- 6 files changed, 57 insertions(+), 43 deletions(-) diff --git a/camel/toolkits/dalle_toolkit.py b/camel/toolkits/dalle_toolkit.py index 83fd3af217..cfe2596004 100644 --- a/camel/toolkits/dalle_toolkit.py +++ b/camel/toolkits/dalle_toolkit.py @@ -128,9 +128,6 @@ def get_dalle_img(prompt: str, image_dir: str = "img") -> str: return image_path -DALLE_FUNCS = [FunctionTool(get_dalle_img)] - - class DalleToolkit(BaseToolkit): r"""A class representing a toolkit for image generation using OpenAI's. This class provides methods handle image generation using OpenAI's DALL-E. @@ -145,3 +142,8 @@ def get_tools(self) -> List[FunctionTool]: representing the functions in the toolkit. """ return DALLE_FUNCS + + +DALLE_FUNCS = [ + FunctionTool(func=get_dalle_img, name_prefix=DalleToolkit.__name__) +] diff --git a/camel/toolkits/math_toolkit.py b/camel/toolkits/math_toolkit.py index fe7ac11dd2..af7f51c142 100644 --- a/camel/toolkits/math_toolkit.py +++ b/camel/toolkits/math_toolkit.py @@ -61,13 +61,6 @@ def mul(a: int, b: int) -> int: return a * b -MATH_FUNCS = [ - FunctionTool(add), - FunctionTool(sub), - FunctionTool(mul), -] - - class MathToolkit(BaseToolkit): r"""A class representing a toolkit for mathematical operations. This class provides methods for basic mathematical operations such as addition, @@ -83,3 +76,9 @@ def get_tools(self) -> List[FunctionTool]: representing the functions in the toolkit. """ return MATH_FUNCS + + +MATH_FUNCS = [ + FunctionTool(func=math_func, name_prefix=MathToolkit.__name__) + for math_func in (add, sub, mul) +] diff --git a/camel/toolkits/search_toolkit.py b/camel/toolkits/search_toolkit.py index 66e926d12d..17bfa7152b 100644 --- a/camel/toolkits/search_toolkit.py +++ b/camel/toolkits/search_toolkit.py @@ -304,14 +304,6 @@ def query_wolfram_alpha(query: str, is_detailed: bool) -> str: return result.rstrip() # Remove trailing whitespace -SEARCH_FUNCS = [ - FunctionTool(search_wiki), - FunctionTool(search_duckduckgo), - FunctionTool(search_google), - FunctionTool(query_wolfram_alpha), -] - - class SearchToolkit(BaseToolkit): r"""A class representing a toolkit for web search. @@ -328,3 +320,14 @@ def get_tools(self) -> List[FunctionTool]: representing the functions in the toolkit. """ return SEARCH_FUNCS + + +SEARCH_FUNCS = [ + FunctionTool(func=func, name_prefix=SearchToolkit.__name__) + for func in ( + search_wiki, + search_duckduckgo, + search_google, + query_wolfram_alpha, + ) +] diff --git a/camel/toolkits/toolkits_manager.py b/camel/toolkits/toolkits_manager.py index 3674f2d933..feb4018894 100644 --- a/camel/toolkits/toolkits_manager.py +++ b/camel/toolkits/toolkits_manager.py @@ -63,12 +63,20 @@ def _load_toolkits(self): for _, module_name, _ in pkgutil.iter_modules(package.__path__): module = importlib.import_module(f'camel.toolkits.{module_name}') + base_toolkit_class_name = None + for _, cls in inspect.getmembers(module, inspect.isclass): + if issubclass(cls, BaseToolkit) and cls is not BaseToolkit: + base_toolkit_class_name = cls.__name__ + break + + prefix = base_toolkit_class_name if base_toolkit_class_name else '' + for name, func in inspect.getmembers(module, inspect.isfunction): if ( hasattr(func, '_is_exported') and func.__module__ == module.__name__ ): - self.toolkits[name] = func + self.toolkits[f"{prefix}.{name}"] = func def _load_toolkit_class_and_methods(self): r""" diff --git a/camel/toolkits/weather_toolkit.py b/camel/toolkits/weather_toolkit.py index 59be0a9fff..9ddf57502c 100644 --- a/camel/toolkits/weather_toolkit.py +++ b/camel/toolkits/weather_toolkit.py @@ -153,9 +153,6 @@ def get_weather_data( return error_message -WEATHER_FUNCS = [FunctionTool(get_weather_data)] - - class WeatherToolkit(BaseToolkit): r"""A class representing a toolkit for interacting with weather data. @@ -172,3 +169,8 @@ def get_tools(self) -> List[FunctionTool]: representing the functions in the toolkit. """ return WEATHER_FUNCS + + +WEATHER_FUNCS = [ + FunctionTool(func=get_weather_data, name_prefix=WeatherToolkit.__name__) +] diff --git a/examples/toolkits/toolkts_manager_example.py b/examples/toolkits/toolkts_manager_example.py index 2622260b75..8c4442df7c 100644 --- a/examples/toolkits/toolkts_manager_example.py +++ b/examples/toolkits/toolkts_manager_example.py @@ -38,15 +38,15 @@ def pretty_print_list(title, items): ======================================== Function Toolkits: ---------------------------------------- - 1. get_dalle_img - 2. add - 3. mul - 4. sub - 5. query_wolfram_alpha - 6. search_duckduckgo - 7. search_google - 8. search_wiki - 9. get_weather_data + 1. DalleToolkit.get_dalle_img + 2. MathToolkit.add + 3. MathToolkit.mul + 4. MathToolkit.sub + 5. SearchToolkit.query_wolfram_alpha + 6. SearchToolkit.search_duckduckgo + 7. SearchToolkit.search_google + 8. SearchToolkit.search_wiki + 9. WeatherToolkit.get_weather_data ======================================== ======================================== @@ -87,18 +87,18 @@ def strict_search_algorithm(keyword: str, description: str) -> bool: ======================================== Matching Toolkit: ---------------------------------------- - 1. get_weather_data + 1. WeatherToolkit.get_weather_data ======================================== ======================================== Custom Algorithm Matching Toolkit: ---------------------------------------- - 1. get_weather_data + 1. WeatherToolkit.get_weather_data ======================================== =============================================================================== """ -tool = manager.get_toolkit('get_weather_data') +tool = manager.get_toolkit('WeatherToolkit.get_weather_data') if isinstance(tool, FunctionTool): print("\nFunction Description:") print('-' * 40) @@ -160,15 +160,15 @@ def div(a: int, b: int) -> float: ======================================== Available Toolkits for now: ---------------------------------------- - 1. get_dalle_img - 2. add - 3. mul - 4. sub - 5. query_wolfram_alpha - 6. search_duckduckgo - 7. search_google - 8. search_wiki - 9. get_weather_data + 1. DalleToolkit.get_dalle_img + 2. MathToolkit.add + 3. MathToolkit.mul + 4. MathToolkit.sub + 5. SearchToolkit.query_wolfram_alpha + 6. SearchToolkit.search_duckduckgo + 7. SearchToolkit.search_google + 8. SearchToolkit.search_wiki + 9. WeatherToolkit.get_weather_data 10. div 11. GithubToolkit.create_pull_request 12. GithubToolkit.retrieve_issue From 9f5a62da22e37dd8afdf18b81c92d5aa2cbb2981 Mon Sep 17 00:00:00 2001 From: Zack Date: Tue, 15 Oct 2024 08:31:07 +0800 Subject: [PATCH 25/34] Refactor method loading in ToolkitManager --- camel/toolkits/toolkits_manager.py | 26 +++++++++++++++++--- examples/toolkits/toolkts_manager_example.py | 19 ++++++++------ 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/camel/toolkits/toolkits_manager.py b/camel/toolkits/toolkits_manager.py index feb4018894..78c4b69804 100644 --- a/camel/toolkits/toolkits_manager.py +++ b/camel/toolkits/toolkits_manager.py @@ -69,14 +69,18 @@ def _load_toolkits(self): base_toolkit_class_name = cls.__name__ break - prefix = base_toolkit_class_name if base_toolkit_class_name else '' + prefix = ( + base_toolkit_class_name + '.' + if base_toolkit_class_name + else '' + ) for name, func in inspect.getmembers(module, inspect.isfunction): if ( hasattr(func, '_is_exported') and func.__module__ == module.__name__ ): - self.toolkits[f"{prefix}.{name}"] = func + self.toolkits[f"{prefix}{name}"] = func def _load_toolkit_class_and_methods(self): r""" @@ -91,9 +95,11 @@ def _load_toolkit_class_and_methods(self): for _, module_name, _ in pkgutil.iter_modules(package.__path__): module = importlib.import_module(f'camel.toolkits.{module_name}') - + toolkit_class_name = None for name, cls in inspect.getmembers(module, inspect.isclass): - if cls.__module__ == module.__name__: + if cls.__module__ == module.__name__ and issubclass( + cls, BaseToolkit + ): self.toolkit_classes[name] = cls self.toolkit_class_methods[name] = { @@ -103,6 +109,18 @@ def _load_toolkit_class_and_methods(self): ) if callable(method) and hasattr(method, '_is_exported') } + toolkit_class_name = name + if toolkit_class_name: + for name, func in inspect.getmembers( + module, inspect.isfunction + ): + if ( + hasattr(func, '_is_exported') + and func.__module__ == module.__name__ + ): + self.toolkit_class_methods[toolkit_class_name][ + name + ] = func def register_tool( self, diff --git a/examples/toolkits/toolkts_manager_example.py b/examples/toolkits/toolkts_manager_example.py index 8c4442df7c..c89f516a32 100644 --- a/examples/toolkits/toolkts_manager_example.py +++ b/examples/toolkits/toolkts_manager_example.py @@ -53,17 +53,22 @@ def pretty_print_list(title, items): Class Toolkits: ---------------------------------------- 1. CodeExecutionToolkit: execute_code - 2. GithubToolkit: create_pull_request, retrieve_issue, retrieve_issue_list, + 2. DalleToolkit: get_dalle_img + 3. GithubToolkit: create_pull_request, retrieve_issue, retrieve_issue_list, retrieve_pull_requests - 3. GoogleMapsToolkit: get_address_description, get_elevation, get_timezone - 4. LinkedInToolkit: create_post, delete_post, get_profile - 5. RedditToolkit: collect_top_posts, perform_sentiment_analysis, + 4. GoogleMapsToolkit: get_address_description, get_elevation, get_timezone + 5. LinkedInToolkit: create_post, delete_post, get_profile + 6. MathToolkit: add, mul, sub + 7. RedditToolkit: collect_top_posts, perform_sentiment_analysis, track_keyword_discussions - 6. RetrievalToolkit: information_retrieval - 7. SlackToolkit: create_slack_channel, delete_slack_message, + 8. RetrievalToolkit: information_retrieval + 9. SearchToolkit: query_wolfram_alpha, search_duckduckgo, search_google, + search_wiki + 10. SlackToolkit: create_slack_channel, delete_slack_message, get_slack_channel_information, get_slack_channel_message, join_slack_channel, leave_slack_channel, send_slack_message - 8. TwitterToolkit: create_tweet, delete_tweet, get_my_user_profile + 11. TwitterToolkit: create_tweet, delete_tweet, get_my_user_profile + 12. WeatherToolkit: get_weather_data ======================================== =============================================================================== """ From 53e92a96be4114b97d52840a69b4966c74296d8d Mon Sep 17 00:00:00 2001 From: Zack Date: Tue, 15 Oct 2024 10:18:53 +0800 Subject: [PATCH 26/34] fix --- camel/toolkits/base.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/camel/toolkits/base.py b/camel/toolkits/base.py index 4ddafe4f29..3bc8498773 100644 --- a/camel/toolkits/base.py +++ b/camel/toolkits/base.py @@ -38,7 +38,4 @@ def get_tools(self) -> List[FunctionTool]: ) ) - if not tools: - raise NotImplementedError("Subclasses must implement the methods") - return tools From 0ee7215d170b4600863dfb363db8eb37ada229c4 Mon Sep 17 00:00:00 2001 From: Zack Date: Tue, 15 Oct 2024 10:44:51 +0800 Subject: [PATCH 27/34] Add get_toolkit_class method in ToolkitManager --- camel/toolkits/toolkits_manager.py | 16 ++++++++++++++++ examples/toolkits/toolkts_manager_example.py | 18 ++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/camel/toolkits/toolkits_manager.py b/camel/toolkits/toolkits_manager.py index 78c4b69804..19704e5fe8 100644 --- a/camel/toolkits/toolkits_manager.py +++ b/camel/toolkits/toolkits_manager.py @@ -313,6 +313,22 @@ def get_toolkits(self, names: list[str]) -> list[FunctionTool] | str: return toolkits return "Toolkits are not found." + def get_toolkit_class(self, class_name: str) -> type[BaseToolkit] | str: + r""" + Retrieves the specified toolkit class. + + Args: + class_name (str): The name of the toolkit class to retrieve. + + Returns: + BaseToolkit | str: The toolkit class object if found, otherwise an + error message. + """ + toolkit_class = self.toolkit_classes.get(class_name) + if toolkit_class: + return toolkit_class + return f"Toolkit class '{class_name}' not found." + def _default_search_algorithm( self, keyword: str, description: str ) -> bool: diff --git a/examples/toolkits/toolkts_manager_example.py b/examples/toolkits/toolkts_manager_example.py index c89f516a32..531d10e10b 100644 --- a/examples/toolkits/toolkts_manager_example.py +++ b/examples/toolkits/toolkts_manager_example.py @@ -208,3 +208,21 @@ def div(a: int, b: int) -> float: ======================================== =============================================================================== """ +toolkit_class = manager.get_toolkit_class('GithubToolkit') +if isinstance(toolkit_class, type): + instance = toolkit_class(repo_name='ZackYule/crab') + pretty_print_list( + "Tools in the crab GitHub Toolkit instance", instance.get_tools() + ) +""" +=============================================================================== +======================================== +Tools in the crab GitHub Toolkit instance: +---------------------------------------- + 1. GithubToolkit.create_pull_request + 2. GithubToolkit.retrieve_issue + 3. GithubToolkit.retrieve_issue_list + 4. GithubToolkit.retrieve_pull_requests +======================================== +=============================================================================== +""" From c60d0baec9fc0a3cd6f6e513bc6112417a397f73 Mon Sep 17 00:00:00 2001 From: Zack Date: Tue, 15 Oct 2024 11:43:27 +0800 Subject: [PATCH 28/34] Refactor the way ToolkitManager reports errors and annotations. --- camel/toolkits/toolkits_manager.py | 111 +++++++++---------- examples/toolkits/toolkts_manager_example.py | 10 +- 2 files changed, 58 insertions(+), 63 deletions(-) diff --git a/camel/toolkits/toolkits_manager.py b/camel/toolkits/toolkits_manager.py index 19704e5fe8..153c4d636c 100644 --- a/camel/toolkits/toolkits_manager.py +++ b/camel/toolkits/toolkits_manager.py @@ -15,6 +15,7 @@ import inspect import pkgutil from typing import Callable, List, Optional, Union +from warnings import warn from camel.toolkits.base import BaseToolkit from camel.toolkits.function_tool import FunctionTool @@ -90,6 +91,12 @@ def _load_toolkit_class_and_methods(self): For each module in the package, it identifies public classes. For each class, it collects only those methods that are decorated with `@export_to_toolkit`, which adds the `_is_exported` attribute. + + In addition to class methods, it also collects standalone functions + from the target module that are decorated with `@export_to_toolkit` + and adds them to the corresponding toolkit class. This allows + including both class methods and standalone functions under the + same toolkit class. """ package = importlib.import_module('camel.toolkits') @@ -125,7 +132,7 @@ def _load_toolkit_class_and_methods(self): def register_tool( self, toolkit_obj: Union[Callable, object, List[Union[Callable, object]]], - ) -> List[FunctionTool] | str: + ) -> List[FunctionTool]: r""" Registers a toolkit function or instance and adds it to the toolkits list. If the input is a list, it processes each element in the list. @@ -136,31 +143,24 @@ def register_tool( registered. Returns: - Union[List[FunctionTool], str]: Returns a list of FunctionTool - instances if the registration is successful. Otherwise, - returns a message indicating the failure reason. + FunctionTools (List[FunctionTool]): A list of FunctionTool + instances. """ res_openai_functions = [] - res_info = "" # If the input is a list, process each element if isinstance(toolkit_obj, list): for obj in toolkit_obj: - res_openai_functions_part, res_info_part = ( - self._register_single_tool(obj) - ) - res_openai_functions.extend(res_openai_functions_part) - res_info += res_info_part + res_openai_functions = self._register_single_tool(obj) + res_openai_functions.extend(res_openai_functions) else: - res_openai_functions, res_info = self._register_single_tool( - toolkit_obj - ) + res_openai_functions = self._register_single_tool(toolkit_obj) - return res_openai_functions if res_openai_functions else res_info + return res_openai_functions def _register_single_tool( self, toolkit_obj: Union[Callable, object] - ) -> tuple[List[FunctionTool], str]: + ) -> List[FunctionTool]: r""" Helper function to register a single toolkit function or instance. @@ -169,25 +169,21 @@ def _register_single_tool( instance to be processed. Returns: - Tuple: A list of FunctionTool instances and a result message. + FunctionTools (tuple[List[FunctionTool]): A list of FunctionTool + instances. """ res_openai_functions = [] - res_info = "" if callable(toolkit_obj): - res = self.add_toolkit_from_function(toolkit_obj) - if "successfully" in res: + if self.add_toolkit_from_function(toolkit_obj): res_openai_functions.append(FunctionTool(toolkit_obj)) - res_info += res else: - res = self.add_toolkit_from_instance( + if self.add_toolkit_from_instance( **{toolkit_obj.__class__.__name__: toolkit_obj} - ) - if "Successfully" in res and hasattr(toolkit_obj, 'get_tools'): + ) and hasattr(toolkit_obj, 'get_tools'): res_openai_functions.extend(toolkit_obj.get_tools()) - res_info += res - return res_openai_functions, res_info + return res_openai_functions - def add_toolkit_from_function(self, toolkit_func: Callable): + def add_toolkit_from_function(self, toolkit_func: Callable) -> bool: r""" Adds a toolkit function to the toolkits list. @@ -195,22 +191,21 @@ def add_toolkit_from_function(self, toolkit_func: Callable): toolkit_func (Callable): The toolkit function to be added. Returns: - Str: A message indicating whether the addition was successful or - if it failed. + Bool: True if the addition was successful, False otherwise. """ if not callable(toolkit_func): - return "Provided argument is not a callable function." + warn("Provided argument is not a callable function.") func_name = toolkit_func.__name__ if not func_name: - return "Function must have a valid name." + warn("Function must have a valid name.") self.toolkits[func_name] = toolkit_func - return f"Toolkit '{func_name}' added successfully." + return True - def add_toolkit_from_instance(self, **kwargs): + def add_toolkit_from_instance(self, **kwargs) -> bool: r""" Add a toolkit class instance to the tool list. Custom instance names are supported here. @@ -220,10 +215,10 @@ def add_toolkit_from_instance(self, **kwargs): where each value is expected to be an instance of BaseToolkit. Returns: - Str: A message indicating whether the addition was successful or - if it failed. + Bool: True if the addition was successful, False otherwise. """ - messages = [] + is_method_added = False + for toolkit_instance_name, toolkit_instance in kwargs.items(): if isinstance(toolkit_instance, BaseToolkit): for attr_name in dir(toolkit_instance): @@ -231,18 +226,18 @@ def add_toolkit_from_instance(self, **kwargs): if callable(attr) and hasattr(attr, '_is_exported'): method_name = f"{toolkit_instance_name}.{attr_name}" - self.toolkits[method_name] = attr - messages.append(f"Successfully added {method_name}.") + is_method_added = True + else: - messages.append( + warn( f"Failed to add {toolkit_instance_name}: " + "Not an instance of BaseToolkit." ) - return "\n".join(messages) + return is_method_added - def list_toolkits(self): + def list_toolkits(self) -> List[str]: r""" Lists the names of all available toolkits. @@ -251,7 +246,7 @@ def list_toolkits(self): """ return list(self.toolkits.keys()) - def list_toolkit_classes(self): + def list_toolkit_classes(self) -> List[str]: r""" Lists the names of all available toolkit classes along with their methods. @@ -272,7 +267,7 @@ def list_toolkit_classes(self): return result - def get_toolkit(self, name: str) -> FunctionTool | str: + def get_toolkit(self, name: str) -> Optional[FunctionTool]: r""" Retrieves the specified toolkit as an FunctionTool object. @@ -280,17 +275,14 @@ def get_toolkit(self, name: str) -> FunctionTool | str: name (str): The name of the toolkit function to retrieve. Returns: - FunctionTool: The toolkit wrapped as an FunctionTool. - - Raises: - ValueError: If the specified toolkit is not found. + FunctionTool (optional): The toolkit wrapped as an FunctionTool. """ toolkit = self.toolkits.get(name) if toolkit: return FunctionTool(func=toolkit, name_prefix=name.split('.')[0]) - return f"Toolkit '{name}' not found." + return None - def get_toolkits(self, names: list[str]) -> list[FunctionTool] | str: + def get_toolkits(self, names: list[str]) -> list[FunctionTool]: r""" Retrieves the specified toolkit as an FunctionTool object. @@ -298,7 +290,7 @@ def get_toolkits(self, names: list[str]) -> list[FunctionTool] | str: name (str): The name of the toolkit function to retrieve. Returns: - FunctionTools (list): The toolkits wrapped as an FunctionTool. + FunctionTools (list): The toolkits wrapped as FunctionTools. """ toolkits: list[FunctionTool] = [] for name in names: @@ -309,11 +301,11 @@ def get_toolkits(self, names: list[str]) -> list[FunctionTool] | str: func=current_toolkit, name_prefix=name.split('.')[0] ) ) - if len(toolkits) > 0: - return toolkits - return "Toolkits are not found." + return toolkits - def get_toolkit_class(self, class_name: str) -> type[BaseToolkit] | str: + def get_toolkit_class( + self, class_name: str + ) -> Optional[type[BaseToolkit]]: r""" Retrieves the specified toolkit class. @@ -321,13 +313,12 @@ def get_toolkit_class(self, class_name: str) -> type[BaseToolkit] | str: class_name (str): The name of the toolkit class to retrieve. Returns: - BaseToolkit | str: The toolkit class object if found, otherwise an - error message. + BaseToolkit(optional): The toolkit class object if found. """ toolkit_class = self.toolkit_classes.get(class_name) if toolkit_class: return toolkit_class - return f"Toolkit class '{class_name}' not found." + return None def _default_search_algorithm( self, keyword: str, description: str @@ -349,20 +340,20 @@ def search_toolkits( self, keyword: str, algorithm: Optional[Callable[[str, str], bool]] = None, - ) -> List[FunctionTool] | str: + ) -> Optional[List[FunctionTool]]: r""" Searches for toolkits based on a keyword in their descriptions using the provided search algorithm. Args: keyword (str): The keyword to search for in toolkit descriptions. - algorithm (Callable[[str, str], bool], optional): A custom + algorithm (Callable[[str, str], bool], optional): A custom search algorithm function that accepts the keyword and description and returns a boolean. Defaults to fuzzy matching. Returns: - List[FunctionTool] | str: A list of toolkit names whose - descriptions match the keyword. + FunctionTools (list): A list of toolkit names whose descriptions + match the keyword, otherwise an error message. """ if algorithm is None: algorithm = self._default_search_algorithm diff --git a/examples/toolkits/toolkts_manager_example.py b/examples/toolkits/toolkts_manager_example.py index 531d10e10b..d6ffe0d8cc 100644 --- a/examples/toolkits/toolkts_manager_example.py +++ b/examples/toolkits/toolkts_manager_example.py @@ -11,8 +11,9 @@ # 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 Type, cast + from camel.toolkits import ToolkitManager -from camel.toolkits.function_tool import FunctionTool from camel.toolkits.github_toolkit import GithubToolkit @@ -104,7 +105,7 @@ def strict_search_algorithm(keyword: str, description: str) -> bool: """ tool = manager.get_toolkit('WeatherToolkit.get_weather_data') -if isinstance(tool, FunctionTool): +if tool: print("\nFunction Description:") print('-' * 40) print(tool.get_function_description()) @@ -208,8 +209,11 @@ def div(a: int, b: int) -> float: ======================================== =============================================================================== """ + toolkit_class = manager.get_toolkit_class('GithubToolkit') -if isinstance(toolkit_class, type): + +if toolkit_class: + toolkit_class = cast(Type[GithubToolkit], toolkit_class) instance = toolkit_class(repo_name='ZackYule/crab') pretty_print_list( "Tools in the crab GitHub Toolkit instance", instance.get_tools() From d602e3e991e407b0e6e992b5c083ebdfe90318dd Mon Sep 17 00:00:00 2001 From: Zack Date: Tue, 15 Oct 2024 12:11:11 +0800 Subject: [PATCH 29/34] bugfix --- camel/toolkits/toolkits_manager.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/camel/toolkits/toolkits_manager.py b/camel/toolkits/toolkits_manager.py index 153c4d636c..80a7e61a07 100644 --- a/camel/toolkits/toolkits_manager.py +++ b/camel/toolkits/toolkits_manager.py @@ -151,8 +151,7 @@ def register_tool( # If the input is a list, process each element if isinstance(toolkit_obj, list): for obj in toolkit_obj: - res_openai_functions = self._register_single_tool(obj) - res_openai_functions.extend(res_openai_functions) + res_openai_functions.extend(self._register_single_tool(obj)) else: res_openai_functions = self._register_single_tool(toolkit_obj) From dd0e42c4028f1a6dacf280f8c7d82292d4b1b7f9 Mon Sep 17 00:00:00 2001 From: Zack Date: Fri, 1 Nov 2024 15:38:32 +0800 Subject: [PATCH 30/34] Add Machine class and TaskManagerWithState class --- camel/tasks/machine.py | 167 ++++++++++++++++++++++++++++ camel/tasks/task.py | 242 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 402 insertions(+), 7 deletions(-) create mode 100644 camel/tasks/machine.py diff --git a/camel/tasks/machine.py b/camel/tasks/machine.py new file mode 100644 index 0000000000..a22756954c --- /dev/null +++ b/camel/tasks/machine.py @@ -0,0 +1,167 @@ +from typing import Dict, List + + +class Machine: + r"""A state machine allowing transitions between different states based on + triggers. + + Args: + states (List[str]): A list of states in the machine. + transitions (List[dict]): A list of transitions, where each transition + is represented as a dictionary containing 'trigger', 'source', and + 'dest'. + initial (str): The initial state of the machine. + + Attributes: + states (List[str]): List of states within the state machine. + transitions (List[dict]): List of state transitions. + initial_state (str): The initial state. + current_state (str): The current active state of the machine. + transition_map (Dict[str, Dict[str, str]]): Mapping of triggers to + source-destination state pairs. + """ + + def __init__( + self, states: List[str], transitions: List[dict], initial: str + ): + self.states = states + self.transitions = transitions + self.initial_state = initial + self.current_state = initial + + if self.initial_state not in self.states: + raise ValueError( + f"Initial state '{self.initial_state}' must be in the states." + ) + + self.transition_map = self._build_transition_map() + + def _build_transition_map(self) -> Dict[str, Dict[str, str]]: + r"""Constructs a mapping from triggers to state transitions. + + Returns: + Dict[str, Dict[str, str]]: A nested dictionary where each key is a + trigger, mapping to another dictionary of source-destination + state pairs. + """ + transition_map: Dict[str, Dict[str, str]] = {} + for transition in self.transitions: + trigger = transition['trigger'] + source = transition['source'] + dest = transition['dest'] + + if trigger not in transition_map: + transition_map[trigger] = {} + + transition_map[trigger][source] = dest + + return transition_map + + def add_state(self, state: str): + r"""Adds a new state to the machine if it doesn't already exist. + + Args: + state (str): The state to be added. + """ + if state not in self.states: + self.states.append(state) + + def add_transition(self, trigger: str, source: str, dest: str): + r"""Adds a new transition between states based on a trigger. + + Args: + trigger (str): The trigger for the transition. + source (str): The source state. + dest (str): The destination state. + + Raises: + ValueError: If either the source or destination state is invalid. + """ + if source not in self.states or dest not in self.states: + raise ValueError( + f"Both source '{source}' and destination '{dest}' must be " + + "valid states." + ) + + if trigger not in self.transition_map: + self.transition_map[trigger] = {} + + self.transition_map[trigger][source] = dest + + def trigger(self, trigger: str): + r"""Executes a state transition based on a given trigger. + + Args: + trigger (str): The trigger to initiate the state transition. + + Raises: + ValueError: If there is no valid transition for the given trigger + from the current state. + """ + if ( + trigger in self.transition_map + and self.current_state in self.transition_map[trigger] + ): + new_state = self.transition_map[trigger][self.current_state] + self.current_state = new_state + else: + print( + f"No valid transition for trigger '{trigger}' from state '" + + f"{self.current_state}'." + ) + + def reset(self): + r"""Resets the machine to its initial state.""" + self.current_state = self.initial_state + + def get_current_state(self) -> str: + r"""Gets the current active state of the machine. + + Returns: + str: The current state. + """ + return self.current_state + + def get_available_triggers(self) -> List[str]: + r"""Lists triggers available from the current state. + + Returns: + List[str]: List of available triggers. + """ + return [ + trigger + for trigger, transitions in self.transition_map.items() + if self.current_state in transitions + ] + + def remove_state(self, state: str): + r"""Removes a state from the machine and any transitions involving it. + + Args: + state (str): The state to be removed. + """ + if state in self.states: + self.states.remove(state) + # Remove transitions associated with the state + self.transition_map = { + trigger: { + src: dst + for src, dst in transitions.items() + if src != state and dst != state + } + for trigger, transitions in self.transition_map.items() + } + + def remove_transition(self, trigger: str, source: str): + r"""Removes a specific transition from a given source state based on a + trigger. + + Args: + trigger (str): The trigger associated with the transition. + source (str): The source state. + """ + if ( + trigger in self.transition_map + and source in self.transition_map[trigger] + ): + del self.transition_map[trigger][source] diff --git a/camel/tasks/task.py b/camel/tasks/task.py index 2521f5b0ea..2df211d045 100644 --- a/camel/tasks/task.py +++ b/camel/tasks/task.py @@ -14,13 +14,16 @@ import re from enum import Enum -from typing import Callable, Dict, List, Literal, Optional, Union +from typing import Callable, Dict, List, Literal, Optional, TypedDict, Union +from litellm import ConfigDict from pydantic import BaseModel from camel.agents import ChatAgent from camel.messages import BaseMessage from camel.prompts import TextPrompt +from camel.tasks.machine import Machine +from camel.toolkits.function_tool import FunctionTool from .task_prompt import ( TASK_COMPOSE_PROMPT, @@ -75,8 +78,9 @@ class Task(BaseModel): state: The state which should be OPEN, RUNNING, DONE or DELETED. type: task type parent: The parent task, None for root task. - subtasks: The childrent sub-tasks for the task. + subtasks: The children sub-tasks for the task. result: The answer for the task. + onTaskComplete: The callback functions when the task is completed. """ content: str @@ -97,6 +101,8 @@ class Task(BaseModel): additional_info: Optional[str] = None + onTaskComplete: List[Callable[[], None]] = [] + @classmethod def from_message(cls, message: BaseMessage) -> "Task": r"""Create a task from a message. @@ -119,6 +125,7 @@ def reset(self): r"""Reset Task to initial state.""" self.state = TaskState.OPEN self.result = "" + self.onTaskComplete = [] def update_result(self, result: str): r"""Set task result and mark the task as DONE. @@ -128,6 +135,23 @@ def update_result(self, result: str): """ self.result = result self.set_state(TaskState.DONE) + for callback in self.onTaskComplete or []: + callback() + + def add_on_task_complete( + self, callbacks: Union[Callable[[], None], List[Callable[[], None]]] + ): + r"""Adds one or more callbacks to the onTaskComplete list. + + Args: + callbacks (Union[Callable[[], None], List[Callable[[], None]]]): + A single callback function or a list of callback functions to + be added. + """ + if isinstance(callbacks, list): + self.onTaskComplete.extend(callbacks) + else: + self.onTaskComplete.append(callbacks) def set_id(self, id: str): self.id = id @@ -289,8 +313,9 @@ class TaskManager: def __init__(self, task: Task): self.root_task: Task = task self.current_task_id: str = task.id - self.tasks: List[Task] = [task] - self.task_map: Dict[str, Task] = {task.id: task} + self.tasks: List[Task] = [] + self.task_map: Dict[str, Task] = {} + self.add_tasks(task) def gen_task_id(self) -> str: r"""Generate a new task id.""" @@ -370,13 +395,49 @@ def add_tasks(self, tasks: Union[Task, List[Task]]) -> None: r"""self.tasks and self.task_map will be updated by the input tasks.""" if not tasks: return - if not isinstance(tasks, List): - tasks = [tasks] + tasks = tasks if isinstance(tasks, list) else [tasks] + for task in tasks: - assert not self.exist(task.id), f"`{task.id}` already existed." + assert not self.exist(task.id), f"`{task.id}` already exists." + # Add callback to update the current task ID + task.add_on_task_complete( + self.create_update_current_task_id_callback(task) + ) self.tasks = self.topological_sort(self.tasks + tasks) self.task_map = {task.id: task for task in self.tasks} + def create_update_current_task_id_callback( + self, task: Task + ) -> Callable[[], None]: + r""" + Creates a callback function to update `current_task_id` after the + specified task is completed. + + The returned callback function, when called, sets `current_task_id` to + the ID of the next task in the `tasks` list. If the specified task is + the last one in the list, `current_task_id` is set to an empty string. + + Args: + task (Task): The task after which `current_task_id` should be + updated. + + Returns: + Callable[[], None]: + A callback function that, when invoked, updates + `current_task_id` to the next task's ID or clears it if the + specified task is the last in the list. + """ + + def update_current_task_id(): + current_index = self.tasks.index(task) + + if current_index + 1 < len(self.tasks): + self.current_task_id = self.tasks[current_index + 1].id + else: + self.current_task_id = "" + + return update_current_task_id + def evolve( self, task: Task, @@ -413,3 +474,170 @@ def evolve( if tasks: return tasks[0] return None + + +class FunctionToolState(BaseModel): + """Represents a specific state of a function tool within a state machine. + + Attributes: + name (str): The unique name of the state, serving as an identifier. + tools_space (Optional[List[FunctionTool]]): A list of `FunctionTool` + objects associated with this state. Defaults to an empty list. + """ + + model_config = ConfigDict( + arbitrary_types_allowed=True, + extra="forbid", + ) + + name: str + tools_space: Optional[List[FunctionTool]] = [] + + def __eq__(self, other): + if isinstance(other, FunctionToolState): + return self.name == other.name + return False + + def __hash__(self): + return hash(self.name) + + def __str__(self): + return self.name + + def __repr__(self): + return self.name + + +class FunctionToolTransition(TypedDict): + """Defines a transition within a function tool state machine. + + Attributes: + trigger (Task): The task that initiates this transition. + source (str): The name of the starting state. + dest (str): The name of the target state after the transition. + """ + + trigger: Task + source: str + dest: str + + +class TaskManagerWithState(TaskManager): + r""" + A TaskManager with an integrated state machine supporting conditional + function calls based on task states and transitions. + + Args: + task (Task): The primary task to manage. + states (List[FunctionToolState]): A list of all possible states. + initial_state (str): The initial state of the machine. + transitions (Optional[List[FunctionToolTransition]]): A list of state + transitions, each containing a trigger, source state, and + destination state. + + Attributes: + state_space (List[FunctionToolState]): Collection of all states. + machine (Machine): Manages state transitions and tracks the current + state. + """ + + def __init__( + self, + task, + states: List[FunctionToolState], + initial_state: str, + transitions: Optional[List[FunctionToolTransition]], + ): + super().__init__(task) + self.state_space = states + self.machine = Machine( + states=[state.name for state in states], + transitions=[], + initial=initial_state, + ) + + for transition in transitions or []: + self.add_transition( + transition['trigger'], + transition['source'], + transition['dest'], + ) + + @property + def current_state(self) -> Optional[FunctionToolState]: + r""" + Retrieves the current state object based on the machine's active state + name. + + Returns: + Optional[FunctionToolState]: + The state object with a `name` matching `self.machine. + current_state`, or `None` if no match is found. + """ + for state in self.state_space: + if state.name == self.machine.current_state: + return state + return None + + def add_state(self, state: FunctionToolState): + r""" + Adds a new state to both `state_space` and the machine. + + Args: + state (FunctionToolState): The state object to be added. + """ + if state not in self.state_space: + self.state_space.append(state) + self.machine.add_state(state.name) + + def add_transition(self, trigger: Task, source: str, dest: str): + r""" + Adds a new transition to the state machine. + + Args: + trigger (Task): The task that initiates this transition. + source (str): The name of the source state. + dest (str): The name of the destination state after the transition. + """ + trigger.add_on_task_complete(lambda: self.machine.trigger(trigger.id)) + if not self.exist(trigger.id): + self.add_tasks(trigger) + self.machine.add_transition(trigger.id, source, dest) + + def get_current_tools(self) -> Optional[List[FunctionTool]]: + r""" + Retrieves the tools available in the current state. + + Returns: + Optional[List[FunctionTool]]: The tools associated with the + current state. + """ + return self.current_state.tools_space + + def remove_state(self, state_name: str): + r""" + Removes a state from `state_space` and the machine. + + Args: + state_name (str): The name of the state to remove. + """ + self.state_space = [ + state for state in self.state_space if state.name != state_name + ] + self.machine.remove_state(state_name) + + def remove_transition(self, trigger: Task, source: str): + r""" + Removes a transition from the machine. + + Args: + trigger (Task): The task associated with the transition to remove. + source (str): The name of the source state. + """ + self.machine.remove_transition(trigger.id, source) + + def reset(self): + r""" + Resets the machine to its initial state. + """ + self.machine.reset() From 0951bba9e188398218d91d53719f8adedaf5936e Mon Sep 17 00:00:00 2001 From: Zack Date: Fri, 1 Nov 2024 15:47:21 +0800 Subject: [PATCH 31/34] Refactor TaskManagerWithState to use FunctionToolState instead of string for source and dest in add_transition and remove_transition methods --- camel/tasks/task.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/camel/tasks/task.py b/camel/tasks/task.py index 2df211d045..1dffd5df57 100644 --- a/camel/tasks/task.py +++ b/camel/tasks/task.py @@ -518,8 +518,8 @@ class FunctionToolTransition(TypedDict): """ trigger: Task - source: str - dest: str + source: FunctionToolState + dest: FunctionToolState class TaskManagerWithState(TaskManager): @@ -590,19 +590,22 @@ def add_state(self, state: FunctionToolState): self.state_space.append(state) self.machine.add_state(state.name) - def add_transition(self, trigger: Task, source: str, dest: str): + def add_transition( + self, trigger: Task, source: FunctionToolState, dest: FunctionToolState + ): r""" Adds a new transition to the state machine. Args: trigger (Task): The task that initiates this transition. - source (str): The name of the source state. - dest (str): The name of the destination state after the transition. + source (FunctionToolState): The source state. + dest (FunctionToolState): The destination state after the + transition. """ trigger.add_on_task_complete(lambda: self.machine.trigger(trigger.id)) if not self.exist(trigger.id): self.add_tasks(trigger) - self.machine.add_transition(trigger.id, source, dest) + self.machine.add_transition(trigger.id, source.name, dest.name) def get_current_tools(self) -> Optional[List[FunctionTool]]: r""" @@ -626,15 +629,15 @@ def remove_state(self, state_name: str): ] self.machine.remove_state(state_name) - def remove_transition(self, trigger: Task, source: str): + def remove_transition(self, trigger: Task, source: FunctionToolState): r""" Removes a transition from the machine. Args: trigger (Task): The task associated with the transition to remove. - source (str): The name of the source state. + source (FunctionToolState): The source state. """ - self.machine.remove_transition(trigger.id, source) + self.machine.remove_transition(trigger.id, source.name) def reset(self): r""" From c206ecaf7d05e2eb038c20cf404867e652fa7722 Mon Sep 17 00:00:00 2001 From: Zack Date: Fri, 1 Nov 2024 15:49:07 +0800 Subject: [PATCH 32/34] Refactor imports in task.py --- camel/tasks/task.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/camel/tasks/task.py b/camel/tasks/task.py index 1dffd5df57..ebe697538e 100644 --- a/camel/tasks/task.py +++ b/camel/tasks/task.py @@ -16,8 +16,7 @@ from enum import Enum from typing import Callable, Dict, List, Literal, Optional, TypedDict, Union -from litellm import ConfigDict -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict from camel.agents import ChatAgent from camel.messages import BaseMessage From 7cf305ded2a8ac239f8c56a1f915cbf148a52ab3 Mon Sep 17 00:00:00 2001 From: Zack Date: Fri, 1 Nov 2024 15:52:15 +0800 Subject: [PATCH 33/34] Refactor TaskManagerWithState to use FunctionToolState instead of string for initial_state in TaskManagerWithState class --- camel/tasks/task.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/camel/tasks/task.py b/camel/tasks/task.py index ebe697538e..a3018ea5a8 100644 --- a/camel/tasks/task.py +++ b/camel/tasks/task.py @@ -543,8 +543,8 @@ class TaskManagerWithState(TaskManager): def __init__( self, task, + initial_state: FunctionToolState, states: List[FunctionToolState], - initial_state: str, transitions: Optional[List[FunctionToolTransition]], ): super().__init__(task) @@ -552,7 +552,7 @@ def __init__( self.machine = Machine( states=[state.name for state in states], transitions=[], - initial=initial_state, + initial=initial_state.name, ) for transition in transitions or []: From e3b8ac9fa703db3a6f102e054ac4d0fbe27e3a7b Mon Sep 17 00:00:00 2001 From: Zack Date: Fri, 1 Nov 2024 16:48:54 +0800 Subject: [PATCH 34/34] Add conditional_function_calling.py --- camel/tasks/machine.py | 13 +++ .../tasks/conditional_function_calling.py | 84 +++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 examples/tasks/conditional_function_calling.py diff --git a/camel/tasks/machine.py b/camel/tasks/machine.py index a22756954c..30c116cf89 100644 --- a/camel/tasks/machine.py +++ b/camel/tasks/machine.py @@ -1,3 +1,16 @@ +# =========== 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 Dict, List diff --git a/examples/tasks/conditional_function_calling.py b/examples/tasks/conditional_function_calling.py new file mode 100644 index 0000000000..775a95b8a3 --- /dev/null +++ b/examples/tasks/conditional_function_calling.py @@ -0,0 +1,84 @@ +# =========== 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.tasks.task import ( + FunctionToolState, + FunctionToolTransition, + Task, + TaskManagerWithState, +) +from camel.toolkits.toolkits_manager import ToolkitManager + +if __name__ == "__main__": + # Define subtasks with descriptive content and unique IDs + tasks = [ + Task(content="Search for suitable phone", id="1"), + Task(content="Place phone order", id="2"), + Task(content="Make payment", id="3"), + ] + + # Define task states with specific tools from the ToolkitManager + states = [ + FunctionToolState( + name="SearchPhone", + tools_space=ToolkitManager().search_toolkits('search'), + ), + FunctionToolState( + name="PlaceOrder", + tools_space=ToolkitManager().search_toolkits('math'), + ), + FunctionToolState( + name="MakePayment", + tools_space=ToolkitManager().search_toolkits('img'), + ), + FunctionToolState(name="Done"), + ] + + # Define task state transitions with trigger, source, and destination + transitions = [ + FunctionToolTransition( + trigger=tasks[0], source=states[0], dest=states[1] + ), + FunctionToolTransition( + trigger=tasks[1], source=states[1], dest=states[2] + ), + FunctionToolTransition( + trigger=tasks[2], source=states[2], dest=states[3] + ), + ] + + # Initialize the Task Manager, starting with the initial state of + # "SearchPhone" + task_manager = TaskManagerWithState( + task=tasks[0], + initial_state=states[0], + states=states, + transitions=transitions, + ) + + # Task execution loop until reaching the "Done" state + while task_manager.current_state != states[-1]: + # Print the current state and available tools + print(f"Current State: {task_manager.current_state}") + print(f"Current Tools: {task_manager.get_current_tools()}") + + # Retrieve and execute the current task if available + current_task = task_manager.current_task + if current_task: + print(f"Executing Task: {current_task.content}") + # Simulate task execution and update task result + current_task.update_result('Subtask completed!') + print(f"Task Result: {current_task.result}") + + # Print updated state after task completion + print(f"Updated State: {task_manager.current_state}")