Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ServiceFactory to support quick setup for tools model&agent. #26

Merged
merged 8 commits into from
Feb 26, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion docs/sphinx_doc/source/agentscope.service.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"expiringdict",
]

service_requires = ["docker", "pymongo", "pymysql"]
service_requires = ["docstring_parser", "docker", "pymongo", "pymysql"]

doc_requires = [
"sphinx",
Expand Down
11 changes: 8 additions & 3 deletions src/agentscope/pipelines/functional.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
7 changes: 4 additions & 3 deletions src/agentscope/service/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
""" Execute python code functions. """
"""Service to execute python code."""
import builtins
import contextlib
import inspect
Expand Down Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions src/agentscope/service/retrieval/retrieval_from_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,18 @@ 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,
we use the embedding model to embed the query.

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]`):
Expand Down
176 changes: 176 additions & 0 deletions src/agentscope/service/service_factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# -*- coding: utf-8 -*-
"""Service factory for model prompt."""
import collections.abc
from functools import partial
import inspect
from typing import (
Callable,
Any,
Tuple,
Union,
Optional,
Literal,
get_args,
get_origin,
)

try:
from docstring_parser import parse
except ImportError:
parse = None
from loguru import logger


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:
type_str = [_get_type_str(_) for _ in get_args(cls)]
elif cls.__origin__ is collections.abc.Sequence:
type_str = "array"
else:
type_str = str(cls.__origin__)
else:
# Normal class
if cls is str:
type_str = "string"
elif cls in [float, int, complex]:
type_str = "number"
elif cls is bool:
type_str = "boolean"
elif cls is collections.abc.Sequence:
type_str = "array"
elif cls is None.__class__:
type_str = "null"
else:
type_str = cls.__name__

return type_str # type: ignore[return-value]


class ServiceFactory:
"""A service factory class that turns service function into string
prompt format."""

@classmethod
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
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 = dict(
zip(
reversed(argsspec.args),
reversed(argsspec.defaults), # type: ignore
),
)

args_required = sorted(
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 = {}
for key in args_agent:
arg_property = {}
# type
if key in args_types:
try:
required_type = _get_type_str(args_types[key])
arg_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:
arg_property["enum"] = list(args_types[key].__args__)

# description
if key in args_description:
arg_property["description"] = args_description[key]

# default
if key in args_defaults and args_defaults[key] is not None:
arg_property["default"] = args_defaults[key]

properties_field[key] = arg_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
4 changes: 2 additions & 2 deletions src/agentscope/service/sql_query/mongodb.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ 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`):
The name of the database to use.
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`):
Expand Down
3 changes: 2 additions & 1 deletion src/agentscope/service/sql_query/mysql.py
Original file line number Diff line number Diff line change
Expand Up @@ -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`):
Expand Down
2 changes: 1 addition & 1 deletion src/agentscope/service/sql_query/sqlite.py
Original file line number Diff line number Diff line change
Expand Up @@ -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`):
Expand Down
4 changes: 3 additions & 1 deletion src/agentscope/service/text_processing/summarization.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading
Loading