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

Support ReST-style docstrings when loading tools from function #9023

Open
LastRemote opened this issue Mar 12, 2025 · 1 comment
Open

Support ReST-style docstrings when loading tools from function #9023

LastRemote opened this issue Mar 12, 2025 · 1 comment
Labels
P3 Low priority, leave it in the backlog

Comments

@LastRemote
Copy link
Contributor

Is your feature request related to a problem? Please describe.
Currently we will need to use Annotated parameters for functions to be smoothly parsed as tools. This is not the standard documentation practice in Python however, and would often need manual labor for tool creators to convert standard python functions to tools. And this manual labor is usually copying from the ReST docstring and pasting to the Annotated description.

Describe the solution you'd like
Make create_tool_from_function support ReST-styled docstrings by default. See #9004

Describe alternatives you've considered

Additional context
This was a small upgrade when I was working on my personal project, but I feel like this can be standardized as a common feature. So here's the PR.

@LastRemote
Copy link
Contributor Author

LastRemote commented Mar 12, 2025

Also, here is the complete picture in my use case if anyone is interested:

class _ToolService(metaclass=abc.ABCMeta):
    """
    A ToolService refers to a deployed tool (or a selection of tools) that can be used for various tasks.

    :param name: The name of the tool service.
    :param api_key: The API key to use for the tool service, defaults to None.
    :param uri: The URI of the tool service.
    :param organization: The organization of the tool service, defaults to None.
    """
    def __init__(...): ...

    @abc.abstractmethod
    def __call__(self, *args, **kwargs) -> Any:
        """
        Perform a call to the tool service.

        :param args: positional arguments to pass to the tool service.
        :param kwargs: keyword arguments to pass to the tool service.
        :return: the result of the call.
        """
        raise NotImplementedError("The __call__ method must be implemented by subclasses.")

    def as_tool(self, name: Optional[str] = None, description: Optional[str] = None) -> Tool:
        """
        Convert the tool service to a Tool instance. This method is especially useful when the tool service is used
        directly by a Large Language Model.

        :param name: The name of the tool, defaults to the name of the tool service.
        :param description: The description of the tool, defaults to the inherited docstring of the __call__ method.
        :returns: A Tool instance representing the tool service.
        :raises ValueError: If the __call__ method accepts *args or **kwargs, and therefore cannot be described for
            tool use scenarios.
        """
        call_method = self.__call__
        sig = inspect.signature(call_method)
        for param in sig.parameters.values():
            # We need to disallow *args and **kwargs in the __call__ method so its parameters can be correctly
            # cast to a JSON schema for tool use scenarios.
            if param.kind in (inspect.Parameter.VAR_KEYWORD, inspect.Parameter.VAR_POSITIONAL):
                raise ValueError(
                    f"The __call__ method of {self.__class__.__name__} must not accept *args or **kwargs"
                    "to be used as a tool."
                )

        # Handle the name
        if name is None:
            class_name = self.__class__.__name__
            # Convert CamelCase to snake_case and remove "ToolService" suffix if present
            name = re.sub(r"(?<!^)(?=[A-Z])", "_", class_name).lower()
            if name.endswith("_tool_service"):
                name = name[:-13]

        # Handle the description by searching the class hierarchy for docstrings
        if description is None:
            # Start with the current class's __call__ docstring
            description = call_method.__doc__

            # If that's empty, search up the inheritance chain for first non-empty docstring
            if not description or not description.strip():
                for cls in self.__class__.__mro__:
                    if hasattr(cls, "__call__") and cls.__call__.__doc__:
                        description = cls.__call__.__doc__
                        break

                # If still no docstring found, use the class docstring
                if not description or not description.strip():
                    description = self.__class__.__doc__ or f"Tool service using {self.__class__.__name__}"

        # Create a wrapper function with the right name and docstring
        def wrapped_call(*args, **kwargs):
            return call_method(*args, **kwargs)

        wrapped_call.__name__ = name
        wrapped_call.__doc__ = description
        setattr(wrapped_call, "__signature__", sig)
        wrapped_call.__annotations__ = getattr(call_method, "__annotations__", {})

        return Tool.from_function(wrapped_call)

class WebSearchService(_ToolService, metaclass=abc.ABCMeta):
    """
    A WebSearchService refers to a deployed web search service that can be used for searching the web.
    """

    def __call__(
        self,
        query: str,
        *,
        num: Optional[int] = None,
        page: Optional[int] = None,
        timeout: Optional[float] = None,
        search_kwargs: Optional[Dict[str, Any]] = None,
    ) -> List[Document]:
        """
        Perform a web search using the web search service.

        :param query: The search query.
        :param num: The number of search results to return.
        :param page: The page number of search results to return.
        :param timeout: The timeout for the request.
        :param search_kwargs: Additional search keyword arguments to include in the request.
        :return: The search results as a list of Document instances. Normally, the Document metadata should at least
            contain the following fields:
            - `position`: The position of the search result in the search results.
            - `title`: The title of the search result.
            - `snippet`: A snippet of the search result content.
            - `url`: The URL of the search result.
        """

if __name__ == "__main__":
    websearch_service = WebSearchService(uri="...", api_key="...")
    websearch_service(query="2025-03-13 Seattle weather")
    websearch_tool = websearch_service.as_tool()  # This will make a tool with the correct description and parameter schema from WebSearchService.__call__

@julian-risch julian-risch added the P3 Low priority, leave it in the backlog label Mar 14, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
P3 Low priority, leave it in the backlog
Projects
None yet
Development

No branches or pull requests

2 participants