libs/core/langchain_core/tools/structured.py PYTHON 272 lines View on github.com → Search inside
1"""Structured tool."""23from __future__ import annotations45import functools6import textwrap7from collections.abc import Awaitable, Callable8from inspect import signature9from typing import (10    TYPE_CHECKING,11    Annotated,12    Any,13    Literal,14)1516from pydantic import Field, SkipValidation17from typing_extensions import override1819# Cannot move to TYPE_CHECKING as _run/_arun parameter annotations are needed at runtime20from langchain_core.callbacks import (21    AsyncCallbackManagerForToolRun,  # noqa: TC00122    CallbackManagerForToolRun,  # noqa: TC00123)24from langchain_core.runnables import RunnableConfig, run_in_executor25from langchain_core.tools.base import (26    _EMPTY_SET,27    FILTERED_ARGS,28    ArgsSchema,29    BaseTool,30    _get_runnable_config_param,31    _is_injected_arg_type,32    create_schema_from_function,33)34from langchain_core.utils.pydantic import is_basemodel_subclass3536if TYPE_CHECKING:37    from langchain_core.messages import ToolCall383940class StructuredTool(BaseTool):41    """Tool that can operate on any number of inputs."""4243    description: str = ""4445    args_schema: Annotated[ArgsSchema, SkipValidation()] = Field(46        ..., description="The tool schema."47    )48    """The input arguments' schema."""4950    func: Callable[..., Any] | None = None51    """The function to run when the tool is called."""5253    coroutine: Callable[..., Awaitable[Any]] | None = None54    """The asynchronous version of the function."""5556    # --- Runnable ---5758    # TODO: Is this needed?59    @override60    async def ainvoke(61        self,62        input: str | dict | ToolCall,63        config: RunnableConfig | None = None,64        **kwargs: Any,65    ) -> Any:66        if not self.coroutine:67            # If the tool does not implement async, fall back to default implementation68            return await run_in_executor(config, self.invoke, input, config, **kwargs)6970        return await super().ainvoke(input, config, **kwargs)7172    # --- Tool ---7374    def _run(75        self,76        *args: Any,77        config: RunnableConfig,78        run_manager: CallbackManagerForToolRun | None = None,79        **kwargs: Any,80    ) -> Any:81        """Use the tool.8283        Args:84            *args: Positional arguments to pass to the tool85            config: Configuration for the run86            run_manager: Optional callback manager to use for the run87            **kwargs: Keyword arguments to pass to the tool8889        Returns:90            The result of the tool execution91        """92        if self.func:93            if run_manager and signature(self.func).parameters.get("callbacks"):94                kwargs["callbacks"] = run_manager.get_child()95            if config_param := _get_runnable_config_param(self.func):96                kwargs[config_param] = config97            return self.func(*args, **kwargs)98        msg = "StructuredTool does not support sync invocation."99        raise NotImplementedError(msg)100101    async def _arun(102        self,103        *args: Any,104        config: RunnableConfig,105        run_manager: AsyncCallbackManagerForToolRun | None = None,106        **kwargs: Any,107    ) -> Any:108        """Use the tool asynchronously.109110        Args:111            *args: Positional arguments to pass to the tool112            config: Configuration for the run113            run_manager: Optional callback manager to use for the run114            **kwargs: Keyword arguments to pass to the tool115116        Returns:117            The result of the tool execution118        """119        if self.coroutine:120            if run_manager and signature(self.coroutine).parameters.get("callbacks"):121                kwargs["callbacks"] = run_manager.get_child()122            if config_param := _get_runnable_config_param(self.coroutine):123                kwargs[config_param] = config124            return await self.coroutine(*args, **kwargs)125126        # If self.coroutine is None, then this will delegate to the default127        # implementation which is expected to delegate to _run on a separate thread.128        return await super()._arun(129            *args, config=config, run_manager=run_manager, **kwargs130        )131132    @classmethod133    def from_function(134        cls,135        func: Callable | None = None,136        coroutine: Callable[..., Awaitable[Any]] | None = None,137        name: str | None = None,138        description: str | None = None,139        return_direct: bool = False,  # noqa: FBT001,FBT002140        args_schema: ArgsSchema | None = None,141        infer_schema: bool = True,  # noqa: FBT001,FBT002142        *,143        response_format: Literal["content", "content_and_artifact"] = "content",144        parse_docstring: bool = False,145        error_on_invalid_docstring: bool = False,146        **kwargs: Any,147    ) -> StructuredTool:148        """Create tool from a given function.149150        A classmethod that helps to create a tool from a function.151152        Args:153            func: The function from which to create a tool.154            coroutine: The async function from which to create a tool.155            name: The name of the tool.156157                Defaults to the function name.158            description: The description of the tool.159160                Defaults to the function docstring.161            return_direct: Whether to return the result directly or as a callback.162            args_schema: The schema of the tool's input arguments.163            infer_schema: Whether to infer the schema from the function's signature.164            response_format: The tool response format.165166                If `'content'` then the output of the tool is interpreted as the167                contents of a `ToolMessage`. If `'content_and_artifact'` then the output168                is expected to be a two-tuple corresponding to the `(content, artifact)`169                of a `ToolMessage`.170            parse_docstring: If `infer_schema` and `parse_docstring`, will attempt171                to parse parameter descriptions from Google Style function docstrings.172            error_on_invalid_docstring: if `parse_docstring` is provided, configure173                whether to raise `ValueError` on invalid Google Style docstrings.174            **kwargs: Additional arguments to pass to the tool175176        Returns:177            The tool.178179        Raises:180            ValueError: If the function is not provided.181            ValueError: If the function does not have a docstring and description182                is not provided.183            TypeError: If the `args_schema` is not a `BaseModel` or dict.184185        Examples:186            ```python187            def add(a: int, b: int) -> int:188                \"\"\"Add two numbers\"\"\"189                return a + b190            tool = StructuredTool.from_function(add)191            tool.run(1, 2) # 3192193            ```194        """195        if func is not None:196            source_function = func197        elif coroutine is not None:198            source_function = coroutine199        else:200            msg = "Function and/or coroutine must be provided"201            raise ValueError(msg)202        name = name or source_function.__name__203        if args_schema is None and infer_schema:204            # schema name is appended within function205            args_schema = create_schema_from_function(206                name,207                source_function,208                parse_docstring=parse_docstring,209                error_on_invalid_docstring=error_on_invalid_docstring,210                filter_args=_filter_schema_args(source_function),211            )212        description_ = description213        if description is None and not parse_docstring:214            description_ = source_function.__doc__ or None215        if description_ is None and args_schema:216            if isinstance(args_schema, type) and is_basemodel_subclass(args_schema):217                description_ = args_schema.__doc__218                if (219                    description_220                    and "A base class for creating Pydantic models" in description_221                ):222                    description_ = ""223                elif not description_:224                    description_ = None225            elif isinstance(args_schema, dict):226                description_ = args_schema.get("description")227            else:228                msg = (229                    "Invalid args_schema: expected BaseModel or dict, "230                    f"got {args_schema}"231                )232                raise TypeError(msg)233        if description_ is None:234            msg = "Function must have a docstring if description not provided."235            raise ValueError(msg)236        if description is None:237            # Only apply if using the function's docstring238            description_ = textwrap.dedent(description_).strip()239240        # Description example:241        # search_api(query: str) - Searches the API for the query.242        description_ = f"{description_.strip()}"243        return cls(244            name=name,245            func=func,246            coroutine=coroutine,247            args_schema=args_schema,248            description=description_,249            return_direct=return_direct,250            response_format=response_format,251            **kwargs,252        )253254    @functools.cached_property255    def _injected_args_keys(self) -> frozenset[str]:256        fn = self.func or self.coroutine257        if fn is None:258            return _EMPTY_SET259        return frozenset(260            k261            for k, v in signature(fn).parameters.items()262            if _is_injected_arg_type(v.annotation)263        )264265266def _filter_schema_args(func: Callable) -> list[str]:267    filter_args = list(FILTERED_ARGS)268    if config_param := _get_runnable_config_param(func):269        filter_args.append(config_param)270    # filter_args.extend(_get_non_model_params(type_hints))271    return filter_args

Code quality findings 6

Ensure functions have docstrings for documentation
missing-docstring
async def ainvoke(
Ensure functions have docstrings for documentation
missing-docstring
def from_function(
Ensure functions have docstrings for documentation
missing-docstring
def add(a: int, b: int) -> int:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(args_schema, type) and is_basemodel_subclass(args_schema):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
elif isinstance(args_schema, dict):
Avoid unnecessary list conversions; use generators where possible
unnecessary-list
filter_args = list(FILTERED_ARGS)

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.