From 307f5faab175d5dc20f27cd635e4051cc3b45976 Mon Sep 17 00:00:00 2001 From: DavdGao Date: Sun, 4 Feb 2024 14:08:24 +0800 Subject: [PATCH 1/8] 1. Move code to code_ to avoid bug during debug; 2. Split web_search into two functions to make the description more clear for model; 3. Rewrite partial docstring for model; 4. Add `ServiceFactory` for developers to extract tools from function and its docstring. --- src/agentscope/pipelines/functional.py | 11 +- src/agentscope/service/__init__.py | 7 +- .../service/{code => code_}/__init__.py | 0 .../service/{code => code_}/exec_python.py | 11 +- .../service/retrieval/retrieval_from_list.py | 7 +- src/agentscope/service/service_factory.py | 158 +++++++++++++ src/agentscope/service/sql_query/mysql.py | 3 +- .../service/text_processing/summarization.py | 4 +- src/agentscope/service/web_search/search.py | 77 +------ tests/service_factory_test.py | 212 ++++++++++++++++++ 10 files changed, 406 insertions(+), 84 deletions(-) rename src/agentscope/service/{code => code_}/__init__.py (100%) rename src/agentscope/service/{code => code_}/exec_python.py (97%) create mode 100644 src/agentscope/service/service_factory.py create mode 100644 tests/service_factory_test.py diff --git a/src/agentscope/pipelines/functional.py b/src/agentscope/pipelines/functional.py index 042d1b6c4..2cb09b608 100644 --- a/src/agentscope/pipelines/functional.py +++ b/src/agentscope/pipelines/functional.py @@ -1,8 +1,13 @@ # -*- coding: utf-8 -*- """ Functional counterpart for Pipeline """ -from typing import Callable, Sequence, Optional, Union -from typing import Any -from typing import Mapping +from typing import ( + Callable, + Sequence, + Optional, + Union, + Any, + Mapping +) from ..agents.operator import Operator # A single Operator or a Sequence of Operators diff --git a/src/agentscope/service/__init__.py b/src/agentscope/service/__init__.py index f466189e3..836032a19 100644 --- a/src/agentscope/service/__init__.py +++ b/src/agentscope/service/__init__.py @@ -2,7 +2,7 @@ """ Import all service-related modules in the package.""" from loguru import logger -from .code.exec_python import execute_python_code +from .code_.exec_python import execute_python_code from .file.common import ( create_file, delete_file, @@ -16,7 +16,7 @@ from .sql_query.mysql import query_mysql from .sql_query.sqlite import query_sqlite from .sql_query.mongodb import query_mongodb -from .web_search.search import web_search +from .web_search.search import bing_search, google_search from .service_response import ServiceResponse from .retrieval.similarity import cos_sim from .text_processing.summarization import summarization @@ -42,7 +42,8 @@ def get_help() -> None: "write_text_file", "read_json_file", "write_json_file", - "web_search", + "bing_search", + "google_search" "query_mysql", "query_sqlite", "query_mongodb", diff --git a/src/agentscope/service/code/__init__.py b/src/agentscope/service/code_/__init__.py similarity index 100% rename from src/agentscope/service/code/__init__.py rename to src/agentscope/service/code_/__init__.py diff --git a/src/agentscope/service/code/exec_python.py b/src/agentscope/service/code_/exec_python.py similarity index 97% rename from src/agentscope/service/code/exec_python.py rename to src/agentscope/service/code_/exec_python.py index 8032cb4f1..a5f72fd71 100644 --- a/src/agentscope/service/code/exec_python.py +++ b/src/agentscope/service/code_/exec_python.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -""" Execute python code functions. """ +"""Service to execute python code.""" import builtins import contextlib import inspect @@ -34,28 +34,27 @@ def execute_python_code( - code: str = "", + code: str, timeout: Optional[Union[int, float]] = 300, use_docker: Optional[Union[bool, str]] = None, maximum_memory_bytes: Optional[int] = None, ) -> ServiceResponse: """ - Execute a string of Python code, optionally inside a Docker container. + Execute a piece of python code. This function can run Python code provided in string format. It has the option to execute the code within a Docker container to provide an additional layer of security, especially important when running untrusted code. - WARNING: If `use_docker` is set to `False`, the code will be run + WARNING: If `use_docker` is set to `False`, the `code` will be run directly in the host system's environment. This poses a potential security risk if the code is untrusted. Only disable Docker if you are confident in the safety of the code being executed. Args: code (`str`, optional): - The Python code to execute, provided as a string. Default is an - empty string. + The Python code to be executed. timeout (`Optional[Union[int, float]]`, defaults to `300`): The maximum time (in seconds) allowed for the code to run. If diff --git a/src/agentscope/service/retrieval/retrieval_from_list.py b/src/agentscope/service/retrieval/retrieval_from_list.py index 4b1af94dd..c290d92a0 100644 --- a/src/agentscope/service/retrieval/retrieval_from_list.py +++ b/src/agentscope/service/retrieval/retrieval_from_list.py @@ -16,7 +16,10 @@ def retrieve_from_list( embedding_model: Optional[ModelWrapperBase] = None, preserve_order: bool = True, ) -> ServiceResponse: - """Memory retrieval with user-defined score function. The score function is + """ + Retrieve data in a list. + + Memory retrieval with user-defined score function. The score function is expected to take the `query` and one of the element in 'knowledge' (a list). This function retrieves top-k elements in 'knowledge' with HIGHEST scores. If the 'query' is a dict but has no embedding, @@ -24,7 +27,7 @@ def retrieve_from_list( Args: query (`Any`): - A provided message, based on which we retrieve. + A message to be retrieved. knowledge (`Sequence`): Data/knowledge to be retrieved from. score_func (`Callable[[Any, Any], float]`): diff --git a/src/agentscope/service/service_factory.py b/src/agentscope/service/service_factory.py new file mode 100644 index 000000000..cd81f9347 --- /dev/null +++ b/src/agentscope/service/service_factory.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- +"""Service factory for model prompt.""" +import collections.abc +from functools import partial +import inspect +from typing import ( + Callable, + Any, + Tuple, + Union, + Literal, + get_args, + get_origin +) + +from docstring_parser import parse +from loguru import logger + +from agentscope.service import bing_search + + +def _get_type_str(cls): + """Get the type string.""" + if hasattr(cls, "__origin__"): + # Typing class + if cls.__origin__ is Union: + return [_get_type_str(_) for _ in get_args(cls)] + elif cls.__origin__ is collections.abc.Sequence: + return "array" + else: + return str(cls.__origin__) + else: + # Normal class + if cls is str: + return "string" + elif cls in [float, int, complex]: + return "number" + elif cls is bool: + return "boolean" + elif cls is collections.abc.Sequence: + return "array" + elif cls is None.__class__: + return "null" + else: + return cls.__name__ + + + +class ServiceFactory: + """A service factory class that turns service function into string + prompt format.""" + + @classmethod + def get(self, service_func: Callable[..., Any], **kwargs: Any) -> Tuple[ + Callable[..., Any], dict]: + """Covnert a service function into a tool function that agent can + use, and generate a dictionary in JSON Schema format that can be + used in OpenAI API directly. While for open-source model, developers + should handle the conversation from json dictionary to prompt. + + Args: + service_func (`Callable[..., Any]`): + The service function to be called. + kwargs (`Any`): + The arguments to be passed to the service function. + + Returns: + `Tuple(Callable[..., Any], dict)`: A tuple of tool function and + a dict in JSON Schema format to describe the function. + + Note: + The description of the function and arguments are extracted from + its docstring automatically, which should be well-formatted in + **Google style**. Otherwise, their descriptions in the returned + dictionary will be empty. + + Suggestions: + 1. The name of the service function should be self-explanatory, + so that the agent can understand the function and use it properly. + 2. The typing of the arguments should be provided when defining + the function (e.g. `def func(a: int, b: str, c: bool)`), so that + the agent can specify the arguments properly. + + Example: + + """ + # Get the function for agent to use + tool_func = partial(service_func, **kwargs) + + # Obtain all arguments of the service function + argsspec = inspect.getfullargspec(service_func) + + # Construct the mapping from arguments to their typings + docstring = parse(service_func.__doc__) + + # Function description + func_description = (docstring.short_description or + docstring.long_description) + + # The arguments that requires the agent to specify + args_agent = set(argsspec.args) - set(kwargs.keys()) + + # Check if the arguments from agent have descriptions in docstring + args_description = {_.arg_name: _.description for _ in + docstring.params} + + # Prepare default values + args_defaults = {k: v for k, v in zip(reversed(argsspec.args), + reversed(argsspec.defaults))} + args_required = list(set(args_agent) - set(args_defaults.keys())) + + # Prepare types of the arguments, remove the return type + args_types = {k: v for k, v in argsspec.annotations.items() if k != + "return"} + + # Prepare argument dictionary + properties_field = dict() + for key in args_agent: + property = dict() + # type + if key in args_types: + try: + required_type = _get_type_str(args_types[key]) + property["type"] = required_type + except Exception: + logger.warning(f"Fail and skip to get the type of the " + f"argument `{key}`.") + + + # For Literal type, add enum field + if get_origin(args_types[key]) is Literal: + property["enum"] = list(args_types[key].__args__) + + # description + if key in args_description: + property["description"] = args_description[key] + + # default + if key in args_defaults and args_defaults[key] is not None: + property["default"] = args_defaults[key] + + properties_field[key] = property + + # Construct the JSON Schema for the service function + func_dict = { + "type": "function", + "function": { + "name": service_func.__name__, + "description": func_description, + "parameters": { + "type": "object", + "properties": properties_field, + "required": args_required + } + } + } + + return tool_func, func_dict diff --git a/src/agentscope/service/sql_query/mysql.py b/src/agentscope/service/sql_query/mysql.py index da3394fbb..40089adef 100644 --- a/src/agentscope/service/sql_query/mysql.py +++ b/src/agentscope/service/sql_query/mysql.py @@ -24,7 +24,8 @@ def query_mysql( maxcount_results: Optional[int] = None, **kwargs: Any, ) -> ServiceResponse: - """Executes a query on a MySQL database and returns the results. + """ + Execute query within MySQL database. Args: database (`str`): diff --git a/src/agentscope/service/text_processing/summarization.py b/src/agentscope/service/text_processing/summarization.py index e6b61830b..1a2426c09 100644 --- a/src/agentscope/service/text_processing/summarization.py +++ b/src/agentscope/service/text_processing/summarization.py @@ -20,7 +20,9 @@ def summarization( max_return_token: int = -1, token_limit_prompt: str = _DEFAULT_TOKEN_LIMIT_PROMPT, ) -> ServiceResponse: - """Summarization function (Notice: curent version of token limitation is + """Summarize the input text. + + Summarization function (Notice: curent version of token limitation is built with Open AI API) Args: diff --git a/src/agentscope/service/web_search/search.py b/src/agentscope/service/web_search/search.py index 5df5ae9b2..828041d01 100644 --- a/src/agentscope/service/web_search/search.py +++ b/src/agentscope/service/web_search/search.py @@ -1,78 +1,20 @@ # -*- coding: utf-8 -*- """Search question in the web""" -from typing import Optional +from typing import Optional, Any from agentscope.service.service_response import ServiceResponse from agentscope.utils.common import requests_get from agentscope.service.service_status import ServiceExecStatus -def web_search( - engine: str, - question: str, - api_key: str, - cse_id: Optional[str] = None, - num_results: int = 10, - **kwargs: Optional[dict], -) -> ServiceResponse: - """ - Perform a web search using a specified search engine (currently supports - Google and Bing). - - This function abstracts the details of using the Google Custom Search JSON - API and the Bing Search API. It formulates the correct query based on the - search engine, handles the API request, and returns the results in a - uniform format. - - Args: - engine (`str`): - The search engine to use. Supported values are 'google' and 'bing'. - question (`str`): - The search query string. - api_key (`str`): - The API key for authenticating with the chosen search engine's API. - cse_id (`Optional[str]`, defaults to `None`): - The unique identifier for a specific Google Custom Search - Engine. Required only when using Google search. - num_results (`int`, defaults to `10`): - The maximum number of search results to return. - **kwargs (`Optional[dict]`): - Additional keyword arguments to pass to the search engine API. - These can include search-specific parameters such as language, - region, and safe search settings. - - Returns: - `ServiceResponse`: A dictionary containing the status of the search ( - 'success' or 'fail') and the search results. The 'content' key - within the dictionary contains a list of search results, each result - is a dictionary with 'title', 'link', and 'snippet', or the error - information. - - Raises: - `ValueError`: If an unsupported search engine is specified. - """ - if engine.lower() == "google": - if not cse_id: - raise ValueError( - "Google Custom Search Engine ID (cse_id) must be " - "provided for Google search.", - ) - return _search_google(question, api_key, cse_id, num_results, **kwargs) - elif engine.lower() == "bing": - return _search_bing(question, api_key, num_results, **kwargs) - else: - raise ValueError(f"Unsupported search engine: {engine}") - - -def _search_bing( +def bing_search( question: str, bing_api_key: str, num_results: int = 10, - **kwargs: Optional[dict], + **kwargs: Any, ) -> ServiceResponse: """ - Performs a query search using the Bing Search API and returns searching - results. + Search question in Bing Search API and return the searching results Args: question (`str`): @@ -81,7 +23,7 @@ def _search_bing( The API key provided for authenticating with the Bing Search API. num_results (`int`, defaults to `10`): The number of search results to return. - **kwargs (`Optional[dict]`): + **kwargs (`Any`): Additional keyword arguments to be included in the search query. For more details, please refer to https://learn.microsoft.com/en-us/bing/search-apis/bing-web-search/reference/query-parameters @@ -172,16 +114,15 @@ def _search_bing( ) -def _search_google( +def google_search( question: str, google_api_key: str, google_cse_id: str, num_results: int = 10, - **kwargs: Optional[dict], + **kwargs: Any, ) -> ServiceResponse: """ - Performs a query search using the Google Custom Search JSON API and - returns searching results. + Search question in Google Search API and return the searching results Args: question (`str`): @@ -193,7 +134,7 @@ def _search_google( The unique identifier of a programmable search engine to use. num_results (`int`, defaults to `10`): The number of search results to return. - **kwargs (`Optional[dict]`): + **kwargs (`Any`): Additional keyword arguments to be included in the search query. For more details, please refer to https://developers.google.com/custom-search/v1/reference/rest/v1/cse/list diff --git a/tests/service_factory_test.py b/tests/service_factory_test.py new file mode 100644 index 000000000..231f0a83a --- /dev/null +++ b/tests/service_factory_test.py @@ -0,0 +1,212 @@ +# -*- coding: utf-8 -*- +""" Unit test for service factory. """ +import json +import unittest +from typing import Literal + +from agentscope.models import ModelWrapperBase +from agentscope.service import bing_search, execute_python_code, \ + retrieve_from_list, query_mysql, summarization +from agentscope.service.service_factory import ServiceFactory + + +class ServiceFactoryTest(unittest.TestCase): + """ + Unit test for service factory. + """ + + def setUp(self) -> None: + """Init for ExampleTest.""" + pass + + def test_bing_search(self) -> None: + """Test bing_search.""" + # api_key is specified by developer, while question and num_results + # are specified by model + _, doc_dict = ServiceFactory.get(bing_search, bing_api_key="xxx") + + self.assertDictEqual(doc_dict, { + "type": "function", + "function": { + "name": "bing_search", + "description": "Search question in Bing Search API and return the searching results", + "parameters": { + "type": "object", + "properties": { + "num_results": { + "type": "number", + "description": "The number of search results to return.", + "default": 10 + }, + "question": { + "type": "string", + "description": "The search query string." + } + }, + "required": ["question"] + } + } + }) + + # Set num_results by developer rather than model + _, doc_dict = ServiceFactory.get(bing_search, + num_results=3, bing_api_key="xxx") + + self.assertEquals(doc_dict, { + "type": "function", + "function": { + "name": "bing_search", + "description": "Search question in Bing Search API and return the searching results", + "parameters": { + "type": "object", + "properties": { + "question": { + "type": "string", + "description": "The search query string." + } + }, + "required": ["question"] + } + } + }) + + def test_enum(self): + def func(a: str, b, c="test", d: Literal[1, "abc", "d"] = 1) -> int: + pass + + _, doc_dict = ServiceFactory.get(func) + + self.assertDictEqual(doc_dict, { + "type": "function", + "function": { + "name": "func", + "description": None, + "parameters": { + "type": "object", + "properties": { + "c": {"default": "test"}, + "d": { + "type": "typing.Literal", + "enum": [1, "abc", "d"], + "default": 1 + }, + "b": {}, + "a": {"type": "string"} + }, + "required": ["b", "a"] + } + } + }) + + def test_exec_python_code(self): + _, doc_dict = ServiceFactory.get(execute_python_code, + timeout=300, + use_docker=True, + maximum_memory_bytes=None) + + self.assertDictEqual(doc_dict, { + "type": "function", + "function": { + "name": "execute_python_code", + "description": "Execute a piece of python code.", + "parameters": { + "type": "object", + "properties": { + "code": { + "type": "string", + "description": "The Python code to be executed." + } + }, + "required": ["code"] + } + } + }) + + def test_retrieval(self): + _, doc_dict = ServiceFactory.get(retrieve_from_list, + knowledge=[1, 2, 3], + score_func=lambda x, y: 1.0, + top_k=10, + embedding_model=10, + preserve_order=True) + + self.assertDictEqual(doc_dict, { + "type": "function", + "function": { + "name": "retrieve_from_list", + "description": "Retrieve data in a list.", + "parameters": { + "type": "object", + "properties": { + "query": { + "description": "A message to be retrieved." + } + }, + "required": [ + "query" + ] + } + } + }) + + def test_sql_query(self): + _, doc_dict = ServiceFactory.get(query_mysql, database="test", + host="localhost", + user="root", password="xxx", + port=3306, + allow_change_data=False, + maxcount_results=None) + + self.assertDictEqual(doc_dict, { + "type": "function", + "function": { + "name": "query_mysql", + "description": "Execute query within MySQL database.", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "SQL query to execute." + } + }, + "required": [ + "query" + ] + } + } + }) + + def test_summary(self): + _, doc_dict = ServiceFactory.get(summarization, + model=ModelWrapperBase("abc"), + system_prompt="", + summarization_prompt="", + max_return_token=-1, + token_limit_prompt="") + + print(json.dumps(doc_dict, indent=4)) + + self.assertDictEqual(doc_dict, { + "type": "function", + "function": { + "name": "summarization", + "description": "Summarize the input text.", + "parameters": { + "type": "object", + "properties": { + "text": { + "type": "string", + "description": "Text to be summarized by the model." + } + }, + "required": [ + "text" + ] + } + } + }) + + +if __name__ == "__main__": + unittest.main() From fec2543022403c1f5c8d3c1cb21a10c5aca17aae Mon Sep 17 00:00:00 2001 From: DavdGao Date: Sun, 4 Feb 2024 15:13:35 +0800 Subject: [PATCH 2/8] 1. format correction; 2. modify unit test for web search --- src/agentscope/pipelines/functional.py | 2 +- src/agentscope/service/__init__.py | 2 +- src/agentscope/service/service_factory.py | 85 +++-- src/agentscope/service/web_search/search.py | 20 +- tests/service_factory_test.py | 394 +++++++++++--------- tests/web_search_test.py | 11 +- 6 files changed, 293 insertions(+), 221 deletions(-) diff --git a/src/agentscope/pipelines/functional.py b/src/agentscope/pipelines/functional.py index 2cb09b608..d04a4596c 100644 --- a/src/agentscope/pipelines/functional.py +++ b/src/agentscope/pipelines/functional.py @@ -6,7 +6,7 @@ Optional, Union, Any, - Mapping + Mapping, ) from ..agents.operator import Operator diff --git a/src/agentscope/service/__init__.py b/src/agentscope/service/__init__.py index 836032a19..e5a07b7e8 100644 --- a/src/agentscope/service/__init__.py +++ b/src/agentscope/service/__init__.py @@ -43,7 +43,7 @@ def get_help() -> None: "read_json_file", "write_json_file", "bing_search", - "google_search" + "google_search", "query_mysql", "query_sqlite", "query_mongodb", diff --git a/src/agentscope/service/service_factory.py b/src/agentscope/service/service_factory.py index cd81f9347..b1b9e9d03 100644 --- a/src/agentscope/service/service_factory.py +++ b/src/agentscope/service/service_factory.py @@ -8,42 +8,43 @@ Any, Tuple, Union, + Optional, Literal, get_args, - get_origin + get_origin, ) from docstring_parser import parse from loguru import logger -from agentscope.service import bing_search - -def _get_type_str(cls): +def _get_type_str(cls: Any) -> Optional[Union[str, list]]: """Get the type string.""" + type_str = None if hasattr(cls, "__origin__"): # Typing class if cls.__origin__ is Union: - return [_get_type_str(_) for _ in get_args(cls)] + type_str = [_get_type_str(_) for _ in get_args(cls)] elif cls.__origin__ is collections.abc.Sequence: - return "array" + type_str = "array" else: - return str(cls.__origin__) + type_str = str(cls.__origin__) else: # Normal class if cls is str: - return "string" + type_str = "string" elif cls in [float, int, complex]: - return "number" + type_str = "number" elif cls is bool: - return "boolean" + type_str = "boolean" elif cls is collections.abc.Sequence: - return "array" + type_str = "array" elif cls is None.__class__: - return "null" + type_str = "null" else: - return cls.__name__ + type_str = cls.__name__ + return type_str # type: ignore[return-value] class ServiceFactory: @@ -51,8 +52,11 @@ class ServiceFactory: prompt format.""" @classmethod - def get(self, service_func: Callable[..., Any], **kwargs: Any) -> Tuple[ - Callable[..., Any], dict]: + def get( + cls, + service_func: Callable[..., Any], + **kwargs: Any, + ) -> Tuple[Callable[..., Any], dict]: """Covnert a service function into a tool function that agent can use, and generate a dictionary in JSON Schema format that can be used in OpenAI API directly. While for open-source model, developers @@ -94,52 +98,61 @@ def get(self, service_func: Callable[..., Any], **kwargs: Any) -> Tuple[ docstring = parse(service_func.__doc__) # Function description - func_description = (docstring.short_description or - docstring.long_description) + func_description = ( + docstring.short_description or docstring.long_description + ) # The arguments that requires the agent to specify args_agent = set(argsspec.args) - set(kwargs.keys()) # Check if the arguments from agent have descriptions in docstring - args_description = {_.arg_name: _.description for _ in - docstring.params} + args_description = { + _.arg_name: _.description for _ in docstring.params + } # Prepare default values - args_defaults = {k: v for k, v in zip(reversed(argsspec.args), - reversed(argsspec.defaults))} + args_defaults = dict( + zip( + reversed(argsspec.args), + reversed(argsspec.defaults), # type: ignore + ), + ) + args_required = list(set(args_agent) - set(args_defaults.keys())) # Prepare types of the arguments, remove the return type - args_types = {k: v for k, v in argsspec.annotations.items() if k != - "return"} + args_types = { + k: v for k, v in argsspec.annotations.items() if k != "return" + } # Prepare argument dictionary - properties_field = dict() + properties_field = {} for key in args_agent: - property = dict() + arg_property = {} # type if key in args_types: try: required_type = _get_type_str(args_types[key]) - property["type"] = required_type + arg_property["type"] = required_type except Exception: - logger.warning(f"Fail and skip to get the type of the " - f"argument `{key}`.") - + logger.warning( + f"Fail and skip to get the type of the " + f"argument `{key}`.", + ) # For Literal type, add enum field if get_origin(args_types[key]) is Literal: - property["enum"] = list(args_types[key].__args__) + arg_property["enum"] = list(args_types[key].__args__) # description if key in args_description: - property["description"] = args_description[key] + arg_property["description"] = args_description[key] # default if key in args_defaults and args_defaults[key] is not None: - property["default"] = args_defaults[key] + arg_property["default"] = args_defaults[key] - properties_field[key] = property + properties_field[key] = arg_property # Construct the JSON Schema for the service function func_dict = { @@ -150,9 +163,9 @@ def get(self, service_func: Callable[..., Any], **kwargs: Any) -> Tuple[ "parameters": { "type": "object", "properties": properties_field, - "required": args_required - } - } + "required": args_required, + }, + }, } return tool_func, func_dict diff --git a/src/agentscope/service/web_search/search.py b/src/agentscope/service/web_search/search.py index 828041d01..9d26837db 100644 --- a/src/agentscope/service/web_search/search.py +++ b/src/agentscope/service/web_search/search.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """Search question in the web""" -from typing import Optional, Any +from typing import Any from agentscope.service.service_response import ServiceResponse from agentscope.utils.common import requests_get @@ -9,7 +9,7 @@ def bing_search( question: str, - bing_api_key: str, + api_key: str, num_results: int = 10, **kwargs: Any, ) -> ServiceResponse: @@ -19,7 +19,7 @@ def bing_search( Args: question (`str`): The search query string. - bing_api_key (`str`): + api_key (`str`): The API key provided for authenticating with the Bing Search API. num_results (`int`, defaults to `10`): The number of search results to return. @@ -84,7 +84,7 @@ def bing_search( if kwargs: params.update(**kwargs) - headers = {"Ocp-Apim-Subscription-Key": bing_api_key} + headers = {"Ocp-Apim-Subscription-Key": api_key} search_results = requests_get( bing_search_url, @@ -116,8 +116,8 @@ def bing_search( def google_search( question: str, - google_api_key: str, - google_cse_id: str, + api_key: str, + cse_id: str, num_results: int = 10, **kwargs: Any, ) -> ServiceResponse: @@ -127,10 +127,10 @@ def google_search( Args: question (`str`): The search query string. - google_api_key (`str`): + api_key (`str`): The API key provided for authenticating with the Google Custom Search JSON API. - google_cse_id (`str`): + cse_id (`str`): The unique identifier of a programmable search engine to use. num_results (`int`, defaults to `10`): The number of search results to return. @@ -167,8 +167,8 @@ def google_search( # Define the query parameters params = { "q": question, - "key": google_api_key, - "cx": google_cse_id, + "key": api_key, + "cx": cse_id, "num": num_results, } if kwargs: diff --git a/tests/service_factory_test.py b/tests/service_factory_test.py index 231f0a83a..9df195de9 100644 --- a/tests/service_factory_test.py +++ b/tests/service_factory_test.py @@ -5,8 +5,13 @@ from typing import Literal from agentscope.models import ModelWrapperBase -from agentscope.service import bing_search, execute_python_code, \ - retrieve_from_list, query_mysql, summarization +from agentscope.service import ( + bing_search, + execute_python_code, + retrieve_from_list, + query_mysql, + summarization, +) from agentscope.service.service_factory import ServiceFactory @@ -17,7 +22,6 @@ class ServiceFactoryTest(unittest.TestCase): def setUp(self) -> None: """Init for ExampleTest.""" - pass def test_bing_search(self) -> None: """Test bing_search.""" @@ -25,187 +29,245 @@ def test_bing_search(self) -> None: # are specified by model _, doc_dict = ServiceFactory.get(bing_search, bing_api_key="xxx") - self.assertDictEqual(doc_dict, { - "type": "function", - "function": { - "name": "bing_search", - "description": "Search question in Bing Search API and return the searching results", - "parameters": { - "type": "object", - "properties": { - "num_results": { - "type": "number", - "description": "The number of search results to return.", - "default": 10 + self.assertDictEqual( + doc_dict, + { + "type": "function", + "function": { + "name": "bing_search", + "description": ( + "Search question in Bing Search API and " + "return the searching results" + ), + "parameters": { + "type": "object", + "properties": { + "num_results": { + "type": "number", + "description": ( + "The number of search " + "results to return." + ), + "default": 10, + }, + "question": { + "type": "string", + "description": "The search query string.", + }, }, - "question": { - "type": "string", - "description": "The search query string." - } + "required": ["question"], }, - "required": ["question"] - } - } - }) + }, + }, + ) # Set num_results by developer rather than model - _, doc_dict = ServiceFactory.get(bing_search, - num_results=3, bing_api_key="xxx") - - self.assertEquals(doc_dict, { - "type": "function", - "function": { - "name": "bing_search", - "description": "Search question in Bing Search API and return the searching results", - "parameters": { - "type": "object", - "properties": { - "question": { - "type": "string", - "description": "The search query string." - } + _, doc_dict = ServiceFactory.get( + bing_search, + num_results=3, + bing_api_key="xxx", + ) + + self.assertDictEqual( + doc_dict, + { + "type": "function", + "function": { + "name": "bing_search", + "description": ( + "Search question in Bing Search API and " + "return the searching results" + ), + "parameters": { + "type": "object", + "properties": { + "question": { + "type": "string", + "description": "The search query string.", + }, + }, + "required": ["question"], }, - "required": ["question"] - } - } - }) + }, + }, + ) - def test_enum(self): - def func(a: str, b, c="test", d: Literal[1, "abc", "d"] = 1) -> int: - pass + def test_enum(self) -> None: + """Test enum in service factory.""" + + def func( # type: ignore + a: str, + b, + c="test", + d: Literal[1, "abc", "d"] = 1, + ) -> None: + print(a, b, c, d) _, doc_dict = ServiceFactory.get(func) - self.assertDictEqual(doc_dict, { - "type": "function", - "function": { - "name": "func", - "description": None, - "parameters": { - "type": "object", - "properties": { - "c": {"default": "test"}, - "d": { - "type": "typing.Literal", - "enum": [1, "abc", "d"], - "default": 1 + self.assertDictEqual( + doc_dict, + { + "type": "function", + "function": { + "name": "func", + "description": None, + "parameters": { + "type": "object", + "properties": { + "c": {"default": "test"}, + "d": { + "type": "typing.Literal", + "enum": [1, "abc", "d"], + "default": 1, + }, + "b": {}, + "a": {"type": "string"}, }, - "b": {}, - "a": {"type": "string"} + "required": ["b", "a"], }, - "required": ["b", "a"] - } - } - }) - - def test_exec_python_code(self): - _, doc_dict = ServiceFactory.get(execute_python_code, - timeout=300, - use_docker=True, - maximum_memory_bytes=None) - - self.assertDictEqual(doc_dict, { - "type": "function", - "function": { - "name": "execute_python_code", - "description": "Execute a piece of python code.", - "parameters": { - "type": "object", - "properties": { - "code": { - "type": "string", - "description": "The Python code to be executed." - } + }, + }, + ) + + def test_exec_python_code(self) -> None: + """Test execute_python_code in service factory.""" + _, doc_dict = ServiceFactory.get( + execute_python_code, + timeout=300, + use_docker=True, + maximum_memory_bytes=None, + ) + + self.assertDictEqual( + doc_dict, + { + "type": "function", + "function": { + "name": "execute_python_code", + "description": "Execute a piece of python code.", + "parameters": { + "type": "object", + "properties": { + "code": { + "type": "string", + "description": ( + "The Python code to be " "executed." + ), + }, + }, + "required": ["code"], }, - "required": ["code"] - } - } - }) - - def test_retrieval(self): - _, doc_dict = ServiceFactory.get(retrieve_from_list, - knowledge=[1, 2, 3], - score_func=lambda x, y: 1.0, - top_k=10, - embedding_model=10, - preserve_order=True) - - self.assertDictEqual(doc_dict, { - "type": "function", - "function": { - "name": "retrieve_from_list", - "description": "Retrieve data in a list.", - "parameters": { - "type": "object", - "properties": { - "query": { - "description": "A message to be retrieved." - } + }, + }, + ) + + def test_retrieval(self) -> None: + """Test retrieval in service factory.""" + _, doc_dict = ServiceFactory.get( + retrieve_from_list, + knowledge=[1, 2, 3], + score_func=lambda x, y: 1.0, + top_k=10, + embedding_model=10, + preserve_order=True, + ) + + self.assertDictEqual( + doc_dict, + { + "type": "function", + "function": { + "name": "retrieve_from_list", + "description": "Retrieve data in a list.", + "parameters": { + "type": "object", + "properties": { + "query": { + "description": "A message to be retrieved.", + }, + }, + "required": [ + "query", + ], }, - "required": [ - "query" - ] - } - } - }) - - def test_sql_query(self): - _, doc_dict = ServiceFactory.get(query_mysql, database="test", - host="localhost", - user="root", password="xxx", - port=3306, - allow_change_data=False, - maxcount_results=None) - - self.assertDictEqual(doc_dict, { - "type": "function", - "function": { - "name": "query_mysql", - "description": "Execute query within MySQL database.", - "parameters": { - "type": "object", - "properties": { - "query": { - "type": "string", - "description": "SQL query to execute." - } + }, + }, + ) + + def test_sql_query(self) -> None: + """Test sql_query in service factory.""" + _, doc_dict = ServiceFactory.get( + query_mysql, + database="test", + host="localhost", + user="root", + password="xxx", + port=3306, + allow_change_data=False, + maxcount_results=None, + ) + + self.assertDictEqual( + doc_dict, + { + "type": "function", + "function": { + "name": "query_mysql", + "description": "Execute query within MySQL database.", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "SQL query to execute.", + }, + }, + "required": [ + "query", + ], }, - "required": [ - "query" - ] - } - } - }) - - def test_summary(self): - _, doc_dict = ServiceFactory.get(summarization, - model=ModelWrapperBase("abc"), - system_prompt="", - summarization_prompt="", - max_return_token=-1, - token_limit_prompt="") + }, + }, + ) + + def test_summary(self) -> None: + """Test summarization in service factory.""" + _, doc_dict = ServiceFactory.get( + summarization, + model=ModelWrapperBase("abc"), + system_prompt="", + summarization_prompt="", + max_return_token=-1, + token_limit_prompt="", + ) print(json.dumps(doc_dict, indent=4)) - self.assertDictEqual(doc_dict, { - "type": "function", - "function": { - "name": "summarization", - "description": "Summarize the input text.", - "parameters": { - "type": "object", - "properties": { - "text": { - "type": "string", - "description": "Text to be summarized by the model." - } + self.assertDictEqual( + doc_dict, + { + "type": "function", + "function": { + "name": "summarization", + "description": "Summarize the input text.", + "parameters": { + "type": "object", + "properties": { + "text": { + "type": "string", + "description": ( + "Text to be summarized by " "the model." + ), + }, + }, + "required": [ + "text", + ], }, - "required": [ - "text" - ] - } - } - }) + }, + }, + ) if __name__ == "__main__": diff --git a/tests/web_search_test.py b/tests/web_search_test.py index b5e3ca1e0..6f7e38860 100644 --- a/tests/web_search_test.py +++ b/tests/web_search_test.py @@ -4,7 +4,7 @@ from unittest.mock import Mock, patch, MagicMock from agentscope.service import ServiceResponse -from agentscope.service import web_search +from agentscope.service import bing_search, google_search from agentscope.service.service_status import ServiceExecStatus @@ -43,7 +43,6 @@ def test_search_bing(self, mock_get: MagicMock) -> None: mock_get.return_value = mock_response # set parameters - engine = "Bing" bing_api_key = "fake-bing-api-key" test_question = "test test_question" num_results = 1 @@ -51,8 +50,7 @@ def test_search_bing(self, mock_get: MagicMock) -> None: headers = {"Ocp-Apim-Subscription-Key": bing_api_key} # Call the function - results = web_search( - engine, + results = bing_search( test_question, api_key=bing_api_key, num_results=num_results, @@ -99,7 +97,6 @@ def test_search_google(self, mock_get: MagicMock) -> None: mock_get.return_value = mock_response # set parameter - engine = "Google" test_question = "test test_question" google_api_key = "fake-google-api-key" google_cse_id = "fake-google-cse-id" @@ -113,13 +110,13 @@ def test_search_google(self, mock_get: MagicMock) -> None: } # Call the function - results = web_search( - engine, + results = google_search( test_question, api_key=google_api_key, cse_id=google_cse_id, num_results=num_results, ) + # Assertions mock_get.assert_called_once_with( "https://www.googleapis.com/customsearch/v1", From 39b301d9dd947ef99139e38da985ef426cc68550 Mon Sep 17 00:00:00 2001 From: DavdGao Date: Sun, 4 Feb 2024 15:43:58 +0800 Subject: [PATCH 3/8] Add required package `docstring_parser` to setup.py --- setup.py | 2 +- src/agentscope/service/service_factory.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index ae2189fa1..1cbc55a3a 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ "expiringdict", ] -service_requires = ["docker", "pymongo", "pymysql"] +service_requires = ["docstring_parser", "docker", "pymongo", "pymysql"] doc_requires = [ "sphinx", diff --git a/src/agentscope/service/service_factory.py b/src/agentscope/service/service_factory.py index b1b9e9d03..d717c6960 100644 --- a/src/agentscope/service/service_factory.py +++ b/src/agentscope/service/service_factory.py @@ -14,7 +14,10 @@ get_origin, ) -from docstring_parser import parse +try: + from docstring_parser import parse +except ImportError: + parse = None from loguru import logger From 43eca62f750d7a9d313e43a9397771871dcb233e Mon Sep 17 00:00:00 2001 From: DavdGao Date: Sun, 4 Feb 2024 15:59:11 +0800 Subject: [PATCH 4/8] Add required package `docstring_parser` to setup.py --- src/agentscope/service/sql_query/mongodb.py | 4 +-- src/agentscope/service/sql_query/sqlite.py | 2 +- tests/service_factory_test.py | 30 +++++++++------------ 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/src/agentscope/service/sql_query/mongodb.py b/src/agentscope/service/sql_query/mongodb.py index 09ff4f3c4..a1d1ec2d6 100644 --- a/src/agentscope/service/sql_query/mongodb.py +++ b/src/agentscope/service/sql_query/mongodb.py @@ -20,7 +20,7 @@ def query_mongodb( maxcount_results: Optional[int] = None, **kwargs: Any, ) -> ServiceResponse: - """Searches the MongoDB database for documents matching the query. + """Execute query within MongoDB database. Args: database (`str`): @@ -28,7 +28,7 @@ def query_mongodb( collection (`str`): The name of the collection to use in mongodb. query (`dict`): - The mongodb query to execute. Note that the query is a dictionary. + The mongodb query to execute. host (`str`): The hostname or IP address of the MongoDB server. port (`int`): diff --git a/src/agentscope/service/sql_query/sqlite.py b/src/agentscope/service/sql_query/sqlite.py index d921f2af1..52bed0f39 100644 --- a/src/agentscope/service/sql_query/sqlite.py +++ b/src/agentscope/service/sql_query/sqlite.py @@ -20,7 +20,7 @@ def query_sqlite( maxcount_results: Optional[int] = None, **kwargs: Any, ) -> ServiceResponse: - """Executes a query on a sqlite database and returns the results. + """Executes query within sqlite database. Args: database (`str`): diff --git a/tests/service_factory_test.py b/tests/service_factory_test.py index 9df195de9..85fcb3b63 100644 --- a/tests/service_factory_test.py +++ b/tests/service_factory_test.py @@ -27,45 +27,41 @@ def test_bing_search(self) -> None: """Test bing_search.""" # api_key is specified by developer, while question and num_results # are specified by model - _, doc_dict = ServiceFactory.get(bing_search, bing_api_key="xxx") - + _, doc_dict = ServiceFactory.get(bing_search, api_key="xxx") + print(json.dumps(doc_dict, indent=4)) self.assertDictEqual( doc_dict, { "type": "function", "function": { "name": "bing_search", - "description": ( - "Search question in Bing Search API and " - "return the searching results" - ), + "description": "Search question in Bing Search API and return the searching results", "parameters": { "type": "object", "properties": { "num_results": { "type": "number", - "description": ( - "The number of search " - "results to return." - ), - "default": 10, + "description": "The number of search results to return.", + "default": 10 }, "question": { "type": "string", - "description": "The search query string.", + "description": "The search query string." }, }, - "required": ["question"], - }, - }, - }, + "required": [ + "question" + ] + } + } + } ) # Set num_results by developer rather than model _, doc_dict = ServiceFactory.get( bing_search, num_results=3, - bing_api_key="xxx", + api_key="xxx", ) self.assertDictEqual( From 5c086b0f5da290d0d206e9586dec253903b607d0 Mon Sep 17 00:00:00 2001 From: DavdGao Date: Sun, 4 Feb 2024 16:00:19 +0800 Subject: [PATCH 5/8] Improve format accordingly. --- tests/service_factory_test.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/tests/service_factory_test.py b/tests/service_factory_test.py index 85fcb3b63..de8c529f6 100644 --- a/tests/service_factory_test.py +++ b/tests/service_factory_test.py @@ -35,26 +35,32 @@ def test_bing_search(self) -> None: "type": "function", "function": { "name": "bing_search", - "description": "Search question in Bing Search API and return the searching results", + "description": ( + "Search question in Bing Search API and " + "return the searching results" + ), "parameters": { "type": "object", "properties": { "num_results": { "type": "number", - "description": "The number of search results to return.", - "default": 10 + "description": ( + "The number of search " + "results to return." + ), + "default": 10, }, "question": { "type": "string", - "description": "The search query string." + "description": "The search query string.", }, }, "required": [ - "question" - ] - } - } - } + "question", + ], + }, + }, + }, ) # Set num_results by developer rather than model From f34f38599ac4a9db9d91e50649b34c2c9fe90e99 Mon Sep 17 00:00:00 2001 From: DavdGao Date: Mon, 26 Feb 2024 11:11:46 +0800 Subject: [PATCH 6/8] Rename code_ into execute_code and update tutorial --- ...ope.service.code.rst => agentscope.service.execute_code.rst} | 2 +- docs/sphinx_doc/source/agentscope.service.rst | 2 +- src/agentscope/service/__init__.py | 2 +- src/agentscope/service/{code_ => execute_code}/__init__.py | 0 src/agentscope/service/{code_ => execute_code}/exec_python.py | 0 5 files changed, 3 insertions(+), 3 deletions(-) rename docs/sphinx_doc/source/{agentscope.service.code.rst => agentscope.service.execute_code.rst} (73%) rename src/agentscope/service/{code_ => execute_code}/__init__.py (100%) rename src/agentscope/service/{code_ => execute_code}/exec_python.py (100%) diff --git a/docs/sphinx_doc/source/agentscope.service.code.rst b/docs/sphinx_doc/source/agentscope.service.execute_code.rst similarity index 73% rename from docs/sphinx_doc/source/agentscope.service.code.rst rename to docs/sphinx_doc/source/agentscope.service.execute_code.rst index 280ff48a5..9019a59aa 100644 --- a/docs/sphinx_doc/source/agentscope.service.code.rst +++ b/docs/sphinx_doc/source/agentscope.service.execute_code.rst @@ -4,7 +4,7 @@ Code package exec\_python module -------------------------------------------- -.. automodule:: agentscope.service.code.exec_python +.. automodule:: agentscope.service.execute_code.exec_python :members: :undoc-members: :show-inheritance: diff --git a/docs/sphinx_doc/source/agentscope.service.rst b/docs/sphinx_doc/source/agentscope.service.rst index cb029eaf8..6f8e7df1c 100644 --- a/docs/sphinx_doc/source/agentscope.service.rst +++ b/docs/sphinx_doc/source/agentscope.service.rst @@ -5,7 +5,7 @@ Service package .. toctree:: :maxdepth: 4 - agentscope.service.code + agentscope.service.execute_code agentscope.service.file agentscope.service.retrieval agentscope.service.sql_query diff --git a/src/agentscope/service/__init__.py b/src/agentscope/service/__init__.py index e5a07b7e8..0ab2560ac 100644 --- a/src/agentscope/service/__init__.py +++ b/src/agentscope/service/__init__.py @@ -2,7 +2,7 @@ """ Import all service-related modules in the package.""" from loguru import logger -from .code_.exec_python import execute_python_code +from .execute_code.exec_python import execute_python_code from .file.common import ( create_file, delete_file, diff --git a/src/agentscope/service/code_/__init__.py b/src/agentscope/service/execute_code/__init__.py similarity index 100% rename from src/agentscope/service/code_/__init__.py rename to src/agentscope/service/execute_code/__init__.py diff --git a/src/agentscope/service/code_/exec_python.py b/src/agentscope/service/execute_code/exec_python.py similarity index 100% rename from src/agentscope/service/code_/exec_python.py rename to src/agentscope/service/execute_code/exec_python.py From 4d4899b7bfe68e82b94938e9bda91a9bcb24cafd Mon Sep 17 00:00:00 2001 From: DavdGao Date: Mon, 26 Feb 2024 11:37:53 +0800 Subject: [PATCH 7/8] Re-correct format --- src/agentscope/service/web_search/search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agentscope/service/web_search/search.py b/src/agentscope/service/web_search/search.py index 9d26837db..49b29a619 100644 --- a/src/agentscope/service/web_search/search.py +++ b/src/agentscope/service/web_search/search.py @@ -48,6 +48,7 @@ def bing_search( It returns the following dict. .. code-block:: python + { 'status': , 'content': [ @@ -74,7 +75,6 @@ def bing_search( } ] } - ``` """ # Bing Search API endpoint From 8e8ea2c9b9b3c06692ebbd12058820ac6a2d6190 Mon Sep 17 00:00:00 2001 From: DavdGao Date: Mon, 26 Feb 2024 13:03:59 +0800 Subject: [PATCH 8/8] Add sorted function in required fields to avoid randomness --- src/agentscope/service/service_factory.py | 4 +++- tests/service_factory_test.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/agentscope/service/service_factory.py b/src/agentscope/service/service_factory.py index d717c6960..e9e2ad891 100644 --- a/src/agentscope/service/service_factory.py +++ b/src/agentscope/service/service_factory.py @@ -121,7 +121,9 @@ def get( ), ) - args_required = list(set(args_agent) - set(args_defaults.keys())) + args_required = sorted( + list(set(args_agent) - set(args_defaults.keys())), + ) # Prepare types of the arguments, remove the return type args_types = { diff --git a/tests/service_factory_test.py b/tests/service_factory_test.py index de8c529f6..385f4c457 100644 --- a/tests/service_factory_test.py +++ b/tests/service_factory_test.py @@ -126,7 +126,7 @@ def func( # type: ignore "b": {}, "a": {"type": "string"}, }, - "required": ["b", "a"], + "required": ["a", "b"], }, }, },