libs/core/tests/unit_tests/test_tools.py PYTHON 4,880 lines View on github.com → Search inside
File is large — showing lines 1–2,000 of 4,880.
1"""Test the base tool implementation."""23import inspect4import json5import logging6import pickle7import sys8import textwrap9import threading10import warnings11from collections.abc import Callable12from dataclasses import dataclass13from datetime import datetime14from enum import Enum15from functools import partial16from typing import (17    Annotated,18    Any,19    Generic,20    Literal,21    TypeVar,22    cast,23    get_type_hints,24)2526import pytest27from pydantic import (28    AliasChoices,29    BaseModel,30    ConfigDict,31    Field,32    RootModel,33    ValidationError,34    field_serializer,35)36from pydantic.errors import PydanticUndefinedAnnotation37from pydantic.v1 import BaseModel as BaseModelV138from pydantic.v1 import ValidationError as ValidationErrorV139from typing_extensions import TypedDict, override4041from langchain_core import tools42from langchain_core.callbacks import (43    AsyncCallbackManagerForToolRun,44    CallbackManagerForToolRun,45)46from langchain_core.callbacks.manager import (47    CallbackManagerForRetrieverRun,48)49from langchain_core.documents import Document50from langchain_core.messages import ToolCall, ToolMessage51from langchain_core.messages.tool import ToolOutputMixin52from langchain_core.retrievers import BaseRetriever53from langchain_core.runnables import (54    Runnable,55    RunnableConfig,56    RunnableLambda,57    ensure_config,58)59from langchain_core.tools import (60    BaseTool,61    StructuredTool,62    Tool,63    ToolException,64    convert_runnable_to_tool,65    tool,66)67from langchain_core.tools.base import (68    TOOL_MESSAGE_BLOCK_TYPES,69    ArgsSchema,70    InjectedToolArg,71    InjectedToolCallId,72    SchemaAnnotationError,73    _DirectlyInjectedToolArg,74    _format_output,75    _is_message_content_block,76    _normalize_message_content,77    create_schema_from_function,78    get_all_basemodel_annotations,79)80from langchain_core.utils.function_calling import (81    convert_to_openai_function,82    convert_to_openai_tool,83)84from langchain_core.utils.pydantic import (85    TypeBaseModel,86    _create_subset_model,87    create_model_v2,88    model_json_schema,89)90from tests.unit_tests.fake.callbacks import FakeCallbackHandler91from tests.unit_tests.pydantic_utils import (92    _normalize_schema,93    _schema,94    skip_if_no_pydantic_v1,95)9697try:98    from langgraph.prebuilt import ToolRuntime  # type: ignore[import-not-found]99100    HAS_LANGGRAPH = True101except ImportError:102    HAS_LANGGRAPH = False103104105def _get_tool_call_json_schema(tool: BaseTool) -> dict[str, Any]:106    tool_schema = tool.tool_call_schema107    if isinstance(tool_schema, dict):108        return tool_schema109110    if issubclass(tool_schema, BaseModel):111        return tool_schema.model_json_schema()112    if issubclass(tool_schema, BaseModelV1):113        return tool_schema.schema()114    return {}  # type: ignore[unreachable]115116117def test_unnamed_decorator() -> None:118    """Test functionality with unnamed decorator."""119120    @tool121    def search_api(query: str) -> str:122        """Search the API for the query."""123        return "API result"124125    assert isinstance(search_api, BaseTool)126    assert search_api.name == "search_api"127    assert not search_api.return_direct128    assert search_api.invoke("test") == "API result"129130131class _MockSchema(BaseModel):132    """Return the arguments directly."""133134    arg1: int135    arg2: bool136    arg3: dict[str, Any] | None = None137138139class _MockStructuredTool(BaseTool):140    name: str = "structured_api"141    args_schema: type[BaseModel] = _MockSchema142    description: str = "A Structured Tool"143144    @override145    def _run(self, *, arg1: int, arg2: bool, arg3: dict[str, Any] | None = None) -> str:146        return f"{arg1} {arg2} {arg3}"147148    async def _arun(149        self, *, arg1: int, arg2: bool, arg3: dict[str, Any] | None = None150    ) -> str:151        raise NotImplementedError152153154class _FakeOutput(ToolOutputMixin):155    """Minimal ToolOutputMixin subclass used only in tests."""156157    def __init__(self, value: int) -> None:158        self.value = value159160    def __eq__(self, other: object) -> bool:161        return isinstance(other, _FakeOutput) and self.value == other.value162163    def __hash__(self) -> int:164        return hash(self.value)165166    def __repr__(self) -> str:167        return f"_FakeOutput({self.value})"168169170def test_structured_args() -> None:171    """Test functionality with structured arguments."""172    structured_api = _MockStructuredTool()173    assert isinstance(structured_api, BaseTool)174    assert structured_api.name == "structured_api"175    expected_result = "1 True {'foo': 'bar'}"176    args = {"arg1": 1, "arg2": True, "arg3": {"foo": "bar"}}177    assert structured_api.run(args) == expected_result178179180def test_misannotated_base_tool_raises_error() -> None:181    """Test that a BaseTool with the incorrect typehint raises an exception."""182    with pytest.raises(SchemaAnnotationError):183184        class _MisAnnotatedTool(BaseTool):185            name: str = "structured_api"186            # This would silently be ignored without the custom metaclass187            args_schema: BaseModel = _MockSchema  # type: ignore[assignment]188            description: str = "A Structured Tool"189190            @override191            def _run(192                self, *, arg1: int, arg2: bool, arg3: dict[str, Any] | None = None193            ) -> str:194                return f"{arg1} {arg2} {arg3}"195196            async def _arun(197                self, *, arg1: int, arg2: bool, arg3: dict[str, Any] | None = None198            ) -> str:199                raise NotImplementedError200201202def test_forward_ref_annotated_base_tool_accepted() -> None:203    """Test that a using forward ref annotation syntax is accepted."""204205    class _ForwardRefAnnotatedTool(BaseTool):206        name: str = "structured_api"207        args_schema: "type[BaseModel]" = _MockSchema208        description: str = "A Structured Tool"209210        @override211        def _run(212            self, *, arg1: int, arg2: bool, arg3: dict[str, Any] | None = None213        ) -> str:214            return f"{arg1} {arg2} {arg3}"215216        async def _arun(217            self, *, arg1: int, arg2: bool, arg3: dict[str, Any] | None = None218        ) -> str:219            raise NotImplementedError220221222def test_subclass_annotated_base_tool_accepted() -> None:223    """Test BaseTool child w/ custom schema isn't overwritten."""224225    class _ForwardRefAnnotatedTool(BaseTool):226        name: str = "structured_api"227        args_schema: type[_MockSchema] = _MockSchema228        description: str = "A Structured Tool"229230        @override231        def _run(232            self, *, arg1: int, arg2: bool, arg3: dict[str, Any] | None = None233        ) -> str:234            return f"{arg1} {arg2} {arg3}"235236        async def _arun(237            self, *, arg1: int, arg2: bool, arg3: dict[str, Any] | None = None238        ) -> str:239            raise NotImplementedError240241    assert issubclass(_ForwardRefAnnotatedTool, BaseTool)242    tool = _ForwardRefAnnotatedTool()243    assert tool.args_schema == _MockSchema244245246def test_decorator_with_specified_schema() -> None:247    """Test that manually specified schemata are passed through to the tool."""248249    @tool(args_schema=_MockSchema)250    def tool_func(*, arg1: int, arg2: bool, arg3: dict[str, Any] | None = None) -> str:251        return f"{arg1} {arg2} {arg3}"252253    assert isinstance(tool_func, BaseTool)254    assert tool_func.args_schema == _MockSchema255256257@pytest.mark.skipif(258    sys.version_info >= (3, 14),259    reason="pydantic.v1 namespace not supported with Python 3.14+",260)261def test_decorator_with_specified_schema_pydantic_v1() -> None:262    """Test that manually specified schemata are passed through to the tool."""263264    class _MockSchemaV1(BaseModelV1):265        """Return the arguments directly."""266267        arg1: int268        arg2: bool269        arg3: dict[str, Any] | None = None270271    @tool(args_schema=cast("ArgsSchema", _MockSchemaV1))272    def tool_func_v1(273        *, arg1: int, arg2: bool, arg3: dict[str, Any] | None = None274    ) -> str:275        return f"{arg1} {arg2} {arg3}"276277    assert isinstance(tool_func_v1, BaseTool)278    assert tool_func_v1.args_schema == cast("ArgsSchema", _MockSchemaV1)279280281def test_decorated_function_schema_equivalent() -> None:282    """Test that a BaseTool without a schema meets expectations."""283284    @tool285    def structured_tool_input(286        *, arg1: int, arg2: bool, arg3: dict[str, Any] | None = None287    ) -> str:288        """Return the arguments directly."""289        return f"{arg1} {arg2} {arg3}"290291    assert isinstance(structured_tool_input, BaseTool)292    assert structured_tool_input.args_schema is not None293    assert (294        _schema(structured_tool_input.args_schema)["properties"]295        == _schema(_MockSchema)["properties"]296        == _normalize_schema(structured_tool_input.args)297    )298299300def test_args_kwargs_filtered() -> None:301    class _SingleArgToolWithKwargs(BaseTool):302        name: str = "single_arg_tool"303        description: str = "A  single arged tool with kwargs"304305        @override306        def _run(307            self,308            some_arg: str,309            run_manager: CallbackManagerForToolRun | None = None,310            **kwargs: Any,311        ) -> str:312            return "foo"313314        async def _arun(315            self,316            some_arg: str,317            run_manager: AsyncCallbackManagerForToolRun | None = None,318            **kwargs: Any,319        ) -> str:320            raise NotImplementedError321322    tool = _SingleArgToolWithKwargs()323    assert tool.is_single_input324325    class _VarArgToolWithKwargs(BaseTool):326        name: str = "single_arg_tool"327        description: str = "A single arged tool with kwargs"328329        @override330        def _run(331            self,332            *args: Any,333            run_manager: CallbackManagerForToolRun | None = None,334            **kwargs: Any,335        ) -> str:336            return "foo"337338        async def _arun(339            self,340            *args: Any,341            run_manager: AsyncCallbackManagerForToolRun | None = None,342            **kwargs: Any,343        ) -> str:344            raise NotImplementedError345346    tool2 = _VarArgToolWithKwargs()347    assert tool2.is_single_input348349350def test_structured_args_decorator_no_infer_schema() -> None:351    """Test functionality with structured arguments parsed as a decorator."""352353    @tool(infer_schema=False)354    def structured_tool_input(355        arg1: int, arg2: float | datetime, opt_arg: dict[str, Any] | None = None356    ) -> str:357        """Return the arguments directly."""358        return f"{arg1}, {arg2}, {opt_arg}"359360    assert isinstance(structured_tool_input, BaseTool)361    assert structured_tool_input.name == "structured_tool_input"362    args = {"arg1": 1, "arg2": 0.001, "opt_arg": {"foo": "bar"}}363    with pytest.raises(ToolException):364        assert structured_tool_input.run(args)365366367def test_structured_single_str_decorator_no_infer_schema() -> None:368    """Test functionality with structured arguments parsed as a decorator."""369370    @tool(infer_schema=False)371    def unstructured_tool_input(tool_input: str) -> str:372        """Return the arguments directly."""373        assert isinstance(tool_input, str)374        return f"{tool_input}"375376    assert isinstance(unstructured_tool_input, BaseTool)377    assert unstructured_tool_input.args_schema is None378    assert unstructured_tool_input.description == "Return the arguments directly."379    assert unstructured_tool_input.run("foo") == "foo"380381382def test_simple_tool_decorator_no_infer_schema_uses_explicit_description() -> None:383    """Test that a simple tool preserves an explicit description."""384385    @tool(infer_schema=False, description="Echo the supplied input.")386    def echo(tool_input: str) -> str:387        return tool_input388389    assert echo.description == "Echo the supplied input."390391392def test_simple_tool_decorator_no_infer_schema_requires_description_or_docstring() -> (393    None394):395    """Test that a simple tool requires an authored description."""396    with pytest.raises(397        ValueError,398        match="Function must have either a docstring or description",399    ):400401        @tool(infer_schema=False)402        def echo(tool_input: str) -> str:403            return tool_input404405406def test_structured_tool_types_parsed() -> None:407    """Test the non-primitive types are correctly passed to structured tools."""408409    class SomeEnum(Enum):410        A = "a"411        B = "b"412413    class SomeBaseModel(BaseModel):414        foo: str415416    @tool417    def structured_tool(418        some_enum: SomeEnum,419        some_base_model: SomeBaseModel,420    ) -> dict[str, Any]:421        """Return the arguments directly."""422        return {423            "some_enum": some_enum,424            "some_base_model": some_base_model,425        }426427    assert isinstance(structured_tool, StructuredTool)428    args = {429        "some_enum": SomeEnum.A.value,430        "some_base_model": SomeBaseModel(foo="bar").model_dump(),431    }432    result = structured_tool.run(json.loads(json.dumps(args)))433    expected = {434        "some_enum": SomeEnum.A,435        "some_base_model": SomeBaseModel(foo="bar"),436    }437    assert result == expected438439440@pytest.mark.skipif(441    sys.version_info >= (3, 14),442    reason="pydantic.v1 namespace not supported with Python 3.14+",443)444def test_structured_tool_types_parsed_pydantic_v1() -> None:445    """Test the non-primitive types are correctly passed to structured tools."""446447    class SomeBaseModel(BaseModelV1):448        foo: str449450    class AnotherBaseModel(BaseModelV1):451        bar: str452453    @tool454    def structured_tool(some_base_model: SomeBaseModel) -> AnotherBaseModel:455        """Return the arguments directly."""456        return AnotherBaseModel(bar=some_base_model.foo)457458    assert isinstance(structured_tool, StructuredTool)459460    expected = AnotherBaseModel(bar="baz")461    for arg in [462        SomeBaseModel(foo="baz"),463        SomeBaseModel(foo="baz").dict(),464    ]:465        args = {"some_base_model": arg}466        result = structured_tool.run(args)467        assert result == expected468469470def test_structured_tool_types_parsed_pydantic_mixed() -> None:471    """Test handling of tool with mixed Pydantic version arguments."""472473    class SomeBaseModel(BaseModelV1):474        foo: str475476    class AnotherBaseModel(BaseModel):477        bar: str478479    with pytest.raises(NotImplementedError):480481        @tool482        def structured_tool(483            some_base_model: SomeBaseModel, another_base_model: AnotherBaseModel484        ) -> None:485            """Return the arguments directly."""486487488def test_base_tool_inheritance_base_schema() -> None:489    """Test schema is correctly inferred when inheriting from BaseTool."""490491    class _MockSimpleTool(BaseTool):492        name: str = "simple_tool"493        description: str = "A Simple Tool"494495        @override496        def _run(self, tool_input: str) -> str:497            return f"{tool_input}"498499        @override500        async def _arun(self, tool_input: str) -> str:501            raise NotImplementedError502503    simple_tool = _MockSimpleTool()504    assert simple_tool.args_schema is None505    expected_args = {"tool_input": {"title": "Tool Input", "type": "string"}}506    assert simple_tool.args == expected_args507508509def test_tool_lambda_args_schema() -> None:510    """Test args schema inference when the tool argument is a lambda function."""511    tool = Tool(512        name="tool",513        description="A tool",514        func=lambda tool_input: tool_input,515    )516    assert tool.args_schema is None517    expected_args = {"tool_input": {"type": "string"}}518    assert tool.args == expected_args519520521def test_structured_tool_from_function_docstring() -> None:522    """Test that structured tools can be created from functions."""523524    def foo(bar: int, baz: str) -> str:525        """Docstring.526527        Args:528            bar: the bar value529            baz: the baz value530        """531        raise NotImplementedError532533    structured_tool = StructuredTool.from_function(foo)534    assert structured_tool.name == "foo"535    assert structured_tool.args == {536        "bar": {"title": "Bar", "type": "integer"},537        "baz": {"title": "Baz", "type": "string"},538    }539540    assert _schema(structured_tool.args_schema) == {541        "properties": {542            "bar": {"title": "Bar", "type": "integer"},543            "baz": {"title": "Baz", "type": "string"},544        },545        "description": inspect.getdoc(foo),546        "title": "foo",547        "type": "object",548        "required": ["bar", "baz"],549    }550551    assert foo.__doc__ is not None552    assert structured_tool.description == textwrap.dedent(foo.__doc__.strip())553554555def test_structured_tool_from_function_docstring_complex_args() -> None:556    """Test that structured tools can be created from functions."""557558    def foo(bar: int, baz: list[str]) -> str:559        """Docstring.560561        Args:562            bar: int563            baz: list[str]564        """565        raise NotImplementedError566567    structured_tool = StructuredTool.from_function(foo)568    assert structured_tool.name == "foo"569    assert structured_tool.args == {570        "bar": {"title": "Bar", "type": "integer"},571        "baz": {572            "title": "Baz",573            "type": "array",574            "items": {"type": "string"},575        },576    }577578    assert _schema(structured_tool.args_schema) == {579        "properties": {580            "bar": {"title": "Bar", "type": "integer"},581            "baz": {582                "title": "Baz",583                "type": "array",584                "items": {"type": "string"},585            },586        },587        "description": inspect.getdoc(foo),588        "title": "foo",589        "type": "object",590        "required": ["bar", "baz"],591    }592593    assert foo.__doc__ is not None594    assert structured_tool.description == textwrap.dedent(foo.__doc__).strip()595596597def test_structured_tool_lambda_multi_args_schema() -> None:598    """Test args schema inference when the tool argument is a lambda function."""599    tool = StructuredTool.from_function(600        name="tool",601        description="A tool",602        func=lambda tool_input, other_arg: f"{tool_input}{other_arg}",603    )604    assert tool.args_schema is not None605    expected_args = {606        "tool_input": {"title": "Tool Input"},607        "other_arg": {"title": "Other Arg"},608    }609    assert tool.args == expected_args610611612def test_tool_partial_function_args_schema() -> None:613    """Test args schema inference when the tool argument is a partial function."""614615    def func(tool_input: str, other_arg: str) -> str:616        assert isinstance(tool_input, str)617        assert isinstance(other_arg, str)618        return tool_input + other_arg619620    tool = Tool(621        name="tool",622        description="A tool",623        func=partial(func, other_arg="foo"),624    )625    assert tool.run("bar") == "barfoo"626627628def test_empty_args_decorator() -> None:629    """Test inferred schema of decorated fn with no args."""630631    @tool632    def empty_tool_input() -> str:633        """Return a constant."""634        return "the empty result"635636    assert isinstance(empty_tool_input, BaseTool)637    assert empty_tool_input.name == "empty_tool_input"638    assert empty_tool_input.args == {}639    assert empty_tool_input.run({}) == "the empty result"640641642def test_tool_from_function_with_run_manager() -> None:643    """Test run of tool when using run_manager."""644645    def foo(bar: str, callbacks: CallbackManagerForToolRun | None = None) -> str:  # noqa: D417646        """Docstring.647648        Args:649            bar: str.650        """651        assert callbacks is not None652        return "foo" + bar653654    handler = FakeCallbackHandler()655    tool = Tool.from_function(foo, name="foo", description="Docstring")656657    assert tool.run(tool_input={"bar": "bar"}, run_manager=[handler]) == "foobar"658    assert tool.run("baz", run_manager=[handler]) == "foobaz"659660661def test_structured_tool_from_function_with_run_manager() -> None:662    """Test args and schema of structured tool when using callbacks."""663664    def foo(  # noqa: D417665        bar: int, baz: str, callbacks: CallbackManagerForToolRun | None = None666    ) -> str:667        """Docstring.668669        Args:670            bar: int671            baz: str672        """673        assert callbacks is not None674        return str(bar) + baz675676    handler = FakeCallbackHandler()677    structured_tool = StructuredTool.from_function(foo)678679    assert structured_tool.args == {680        "bar": {"title": "Bar", "type": "integer"},681        "baz": {"title": "Baz", "type": "string"},682    }683684    assert _schema(structured_tool.args_schema) == {685        "properties": {686            "bar": {"title": "Bar", "type": "integer"},687            "baz": {"title": "Baz", "type": "string"},688        },689        "description": inspect.getdoc(foo),690        "title": "foo",691        "type": "object",692        "required": ["bar", "baz"],693    }694695    assert (696        structured_tool.run(697            tool_input={"bar": "10", "baz": "baz"}, run_manger=[handler]698        )699        == "10baz"700    )701702703def test_structured_tool_from_parameterless_function() -> None:704    """Test parameterless function of structured tool."""705706    def foo() -> str:707        """Docstring."""708        return "invoke foo"709710    structured_tool = StructuredTool.from_function(foo)711712    assert structured_tool.run({}) == "invoke foo"713    assert structured_tool.run("") == "invoke foo"714715716def test_named_tool_decorator() -> None:717    """Test functionality when arguments are provided as input to decorator."""718719    @tool("search")720    def search_api(query: str) -> str:721        """Search the API for the query."""722        assert isinstance(query, str)723        return f"API result - {query}"724725    assert isinstance(search_api, BaseTool)726    assert search_api.name == "search"727    assert not search_api.return_direct728    assert search_api.run({"query": "foo"}) == "API result - foo"729730731def test_named_tool_decorator_return_direct() -> None:732    """Test functionality when arguments and return direct are provided as input."""733734    @tool("search", return_direct=True)735    def search_api(query: str, *args: Any) -> str:736        """Search the API for the query."""737        return "API result"738739    assert isinstance(search_api, BaseTool)740    assert search_api.name == "search"741    assert search_api.return_direct742    assert search_api.run({"query": "foo"}) == "API result"743744745def test_unnamed_tool_decorator_return_direct() -> None:746    """Test functionality when only return direct is provided."""747748    @tool(return_direct=True)749    def search_api(query: str) -> str:750        """Search the API for the query."""751        assert isinstance(query, str)752        return "API result"753754    assert isinstance(search_api, BaseTool)755    assert search_api.name == "search_api"756    assert search_api.return_direct757    assert search_api.run({"query": "foo"}) == "API result"758759760def test_tool_with_kwargs() -> None:761    """Test functionality when only return direct is provided."""762763    @tool(return_direct=True)764    def search_api(765        arg_0: str,766        arg_1: float = 4.3,767        ping: str = "hi",768    ) -> str:769        """Search the API for the query."""770        return f"arg_0={arg_0}, arg_1={arg_1}, ping={ping}"771772    assert isinstance(search_api, BaseTool)773    result = search_api.run(774        tool_input={775            "arg_0": "foo",776            "arg_1": 3.2,777            "ping": "pong",778        }779    )780    assert result == "arg_0=foo, arg_1=3.2, ping=pong"781782    result = search_api.run(783        tool_input={784            "arg_0": "foo",785        }786    )787    assert result == "arg_0=foo, arg_1=4.3, ping=hi"788    # For backwards compatibility, we still accept a single str arg789    result = search_api.run("foobar")790    assert result == "arg_0=foobar, arg_1=4.3, ping=hi"791792793def test_missing_docstring() -> None:794    """Test error is raised when docstring is missing."""795    # expect to throw a value error if there's no docstring796    with pytest.raises(ValueError, match="Function must have a docstring"):797798        @tool799        def search_api(query: str) -> str:800            return "API result"801802    @tool803    class MyTool(BaseModel):804        foo: str805806    assert not MyTool.description  # type: ignore[attr-defined]807808809def test_create_tool_positional_args() -> None:810    """Test that positional arguments are allowed."""811    test_tool = Tool("test_name", lambda x: x, "test_description")812    assert test_tool.invoke("foo") == "foo"813    assert test_tool.name == "test_name"814    assert test_tool.description == "test_description"815    assert test_tool.is_single_input816817818def test_create_tool_keyword_args() -> None:819    """Test that keyword arguments are allowed."""820    test_tool = Tool(name="test_name", func=lambda x: x, description="test_description")821    assert test_tool.is_single_input822    assert test_tool.invoke("foo") == "foo"823    assert test_tool.name == "test_name"824    assert test_tool.description == "test_description"825826827async def test_create_async_tool() -> None:828    """Test that async tools are allowed."""829830    async def _test_func(x: str) -> str:831        return x832833    test_tool = Tool(834        name="test_name",835        func=lambda x: x,836        description="test_description",837        coroutine=_test_func,838    )839    assert test_tool.is_single_input840    assert test_tool.invoke("foo") == "foo"841    assert test_tool.name == "test_name"842    assert test_tool.description == "test_description"843    assert test_tool.coroutine is not None844    assert await test_tool.arun("foo") == "foo"845846847class _FakeExceptionTool(BaseTool):848    name: str = "exception"849    description: str = "an exception-throwing tool"850    exception: Exception = ToolException()851852    def _run(self) -> str:853        raise self.exception854855    async def _arun(self) -> str:856        raise self.exception857858859def test_exception_handling_bool() -> None:860    tool_ = _FakeExceptionTool(handle_tool_error=True)861    expected = "Tool execution error"862    actual = tool_.run({})863    assert expected == actual864865866def test_exception_handling_str() -> None:867    expected = "foo bar"868    tool_ = _FakeExceptionTool(handle_tool_error=expected)869    actual = tool_.run({})870    assert expected == actual871872873def test_exception_handling_callable() -> None:874    expected = "foo bar"875876    def handling(e: ToolException) -> str:877        return expected878879    tool_ = _FakeExceptionTool(handle_tool_error=handling)880    actual = tool_.run({})881    assert expected == actual882883884def test_exception_handling_callable_message_content_blocks() -> None:885    expected: list[dict[str, Any]] = [{"type": "text", "text": "handled error"}]886887    def handling(e: ToolException) -> list[dict[str, Any]]:888        return expected889890    tool_ = _FakeExceptionTool(handle_tool_error=handling)891    actual = tool_.invoke(892        {893            "type": "tool_call",894            "args": {},895            "name": "exception",896            "id": "call_1",897        }898    )899900    assert isinstance(actual, ToolMessage)901    assert actual.content == expected902    assert actual.status == "error"903    assert actual.tool_call_id == "call_1"904905906def test_exception_handling_callable_message_content_blocks_sequence() -> None:907    content = ({"type": "text", "text": "handled error"},)908909    def handling(e: ToolException) -> tuple[dict[str, Any], ...]:910        return content911912    tool_ = _FakeExceptionTool(handle_tool_error=handling)913    actual = tool_.invoke(914        {915            "type": "tool_call",916            "args": {},917            "name": "exception",918            "id": "call_1",919        }920    )921922    assert isinstance(actual, ToolMessage)923    assert actual.content == list(content)924    assert actual.status == "error"925    assert actual.tool_call_id == "call_1"926927928def test_exception_handling_callable_invalid_blocks_stringified() -> None:929    # A sequence whose elements are not valid content blocks is not message930    # content, so it falls back to a JSON-stringified ToolMessage.931    def handling(e: ToolException) -> list[dict[str, Any]]:932        return [{"text": "foo"}]  # missing 'type' -> not a valid block933934    tool_ = _FakeExceptionTool(handle_tool_error=handling)935    actual = tool_.invoke(936        {937            "type": "tool_call",938            "args": {},939            "name": "exception",940            "id": "call_1",941        }942    )943944    assert isinstance(actual, ToolMessage)945    assert actual.content == '[{"text": "foo"}]'946    assert actual.status == "error"947    assert actual.tool_call_id == "call_1"948949950def test_exception_handling_non_tool_exception() -> None:951    tool_ = _FakeExceptionTool(exception=ValueError("some error"))952    with pytest.raises(ValueError, match="some error"):953        tool_.run({})954955956async def test_async_exception_handling_bool() -> None:957    tool_ = _FakeExceptionTool(handle_tool_error=True)958    expected = "Tool execution error"959    actual = await tool_.arun({})960    assert expected == actual961962963async def test_async_exception_handling_str() -> None:964    expected = "foo bar"965    tool_ = _FakeExceptionTool(handle_tool_error=expected)966    actual = await tool_.arun({})967    assert expected == actual968969970async def test_async_exception_handling_callable() -> None:971    expected = "foo bar"972973    def handling(e: ToolException) -> str:974        return expected975976    tool_ = _FakeExceptionTool(handle_tool_error=handling)977    actual = await tool_.arun({})978    assert expected == actual979980981async def test_async_exception_handling_callable_message_content_blocks() -> None:982    expected: list[dict[str, Any]] = [{"type": "text", "text": "handled error"}]983984    def handling(e: ToolException) -> list[dict[str, Any]]:985        return expected986987    tool_ = _FakeExceptionTool(handle_tool_error=handling)988    actual = await tool_.ainvoke(989        {990            "type": "tool_call",991            "args": {},992            "name": "exception",993            "id": "call_1",994        }995    )996997    assert isinstance(actual, ToolMessage)998    assert actual.content == expected999    assert actual.status == "error"1000    assert actual.tool_call_id == "call_1"100110021003async def test_async_exception_handling_callable_message_content_blocks_sequence() -> (1004    None1005):1006    content = ({"type": "text", "text": "handled error"},)10071008    def handling(e: ToolException) -> tuple[dict[str, Any], ...]:1009        return content10101011    tool_ = _FakeExceptionTool(handle_tool_error=handling)1012    actual = await tool_.ainvoke(1013        {1014            "type": "tool_call",1015            "args": {},1016            "name": "exception",1017            "id": "call_1",1018        }1019    )10201021    assert isinstance(actual, ToolMessage)1022    assert actual.content == list(content)1023    assert actual.status == "error"1024    assert actual.tool_call_id == "call_1"102510261027async def test_async_exception_handling_non_tool_exception() -> None:1028    tool_ = _FakeExceptionTool(exception=ValueError("some error"))1029    with pytest.raises(ValueError, match="some error"):1030        await tool_.arun({})103110321033def test_structured_tool_from_function() -> None:1034    """Test that structured tools can be created from functions."""10351036    def foo(bar: int, baz: str) -> str:1037        """Docstring thing.10381039        Args:1040            bar: the bar value1041            baz: the baz value1042        """1043        raise NotImplementedError10441045    structured_tool = StructuredTool.from_function(foo)1046    assert structured_tool.name == "foo"1047    assert structured_tool.args == {1048        "bar": {"title": "Bar", "type": "integer"},1049        "baz": {"title": "Baz", "type": "string"},1050    }10511052    assert _schema(structured_tool.args_schema) == {1053        "title": "foo",1054        "type": "object",1055        "description": inspect.getdoc(foo),1056        "properties": {1057            "bar": {"title": "Bar", "type": "integer"},1058            "baz": {"title": "Baz", "type": "string"},1059        },1060        "required": ["bar", "baz"],1061    }10621063    assert foo.__doc__ is not None1064    assert structured_tool.description == textwrap.dedent(foo.__doc__.strip())106510661067def test_validation_error_handling_bool() -> None:1068    """Test that validation errors are handled correctly."""1069    expected = "Tool input validation error"1070    tool_ = _MockStructuredTool(handle_validation_error=True)1071    actual = tool_.run({})1072    assert expected == actual107310741075def test_validation_error_handling_str() -> None:1076    """Test that validation errors are handled correctly."""1077    expected = "foo bar"1078    tool_ = _MockStructuredTool(handle_validation_error=expected)1079    actual = tool_.run({})1080    assert expected == actual108110821083def test_validation_error_handling_callable() -> None:1084    """Test that validation errors are handled correctly."""1085    expected = "foo bar"10861087    def handling(e: ValidationError | ValidationErrorV1) -> str:1088        return expected10891090    tool_ = _MockStructuredTool(handle_validation_error=handling)1091    actual = tool_.run({})1092    assert expected == actual109310941095@pytest.mark.parametrize(1096    "handler",1097    [1098        True,1099        "foo bar",1100        lambda _: "foo bar",1101    ],1102)1103def test_validation_error_handling_non_validation_error(1104    *,1105    handler: bool | str | Callable[[ValidationError | ValidationErrorV1], str],1106) -> None:1107    """Test that validation errors are handled correctly."""11081109    class _RaiseNonValidationErrorTool(BaseTool):1110        name: str = "raise_non_validation_error_tool"1111        description: str = "A tool that raises a non-validation error"11121113        def _parse_input(1114            self,1115            tool_input: str | dict[str, Any],1116            tool_call_id: str | None,1117        ) -> str | dict[str, Any]:1118            raise NotImplementedError11191120        @override1121        def _run(self) -> str:1122            return "dummy"11231124        @override1125        async def _arun(self) -> str:1126            return "dummy"11271128    tool_ = _RaiseNonValidationErrorTool(handle_validation_error=handler)1129    with pytest.raises(NotImplementedError):1130        tool_.run({})113111321133async def test_async_validation_error_handling_bool() -> None:1134    """Test that validation errors are handled correctly."""1135    expected = "Tool input validation error"1136    tool_ = _MockStructuredTool(handle_validation_error=True)1137    actual = await tool_.arun({})1138    assert expected == actual113911401141async def test_async_validation_error_handling_str() -> None:1142    """Test that validation errors are handled correctly."""1143    expected = "foo bar"1144    tool_ = _MockStructuredTool(handle_validation_error=expected)1145    actual = await tool_.arun({})1146    assert expected == actual114711481149async def test_async_validation_error_handling_callable() -> None:1150    """Test that validation errors are handled correctly."""1151    expected = "foo bar"11521153    def handling(e: ValidationError | ValidationErrorV1) -> str:1154        return expected11551156    tool_ = _MockStructuredTool(handle_validation_error=handling)1157    actual = await tool_.arun({})1158    assert expected == actual115911601161@pytest.mark.skipif(1162    sys.version_info >= (3, 14),1163    reason="pydantic.v1 namespace not supported with Python 3.14+",1164)1165async def test_async_validation_error_handling_pydantic_v1_schema() -> None:1166    """Test async validation error handling for Pydantic V1 schemas."""11671168    class Args(BaseModelV1):1169        x: int11701171    def foo(x: int) -> str:1172        """Return x as text."""1173        return str(x)11741175    tool_ = StructuredTool.from_function(1176        foo,1177        args_schema=cast("ArgsSchema", Args),1178        handle_validation_error=True,1179    )11801181    assert tool_.run({"x": "not-an-integer"}) == "Tool input validation error"1182    assert await tool_.arun({"x": "not-an-integer"}) == "Tool input validation error"118311841185@pytest.mark.parametrize(1186    "handler",1187    [1188        True,1189        "foo bar",1190        lambda _: "foo bar",1191    ],1192)1193async def test_async_validation_error_handling_non_validation_error(1194    *,1195    handler: bool | str | Callable[[ValidationError | ValidationErrorV1], str],1196) -> None:1197    """Test that validation errors are handled correctly."""11981199    class _RaiseNonValidationErrorTool(BaseTool):1200        name: str = "raise_non_validation_error_tool"1201        description: str = "A tool that raises a non-validation error"12021203        def _parse_input(1204            self,1205            tool_input: str | dict[str, Any],1206            tool_call_id: str | None,1207        ) -> str | dict[str, Any]:1208            raise NotImplementedError12091210        @override1211        def _run(self) -> str:1212            return "dummy"12131214        @override1215        async def _arun(self) -> str:1216            return "dummy"12171218    tool_ = _RaiseNonValidationErrorTool(handle_validation_error=handler)1219    with pytest.raises(NotImplementedError):1220        await tool_.arun({})122112221223def test_optional_subset_model_rewrite() -> None:1224    class MyModel(BaseModel):1225        a: str | None = None1226        b: str1227        c: list[str | None] | None = None12281229    model2 = _create_subset_model("model2", MyModel, ["a", "b", "c"])12301231    assert set(_schema(model2)["required"]) == {"b"}123212331234@pytest.mark.parametrize(1235    ("inputs", "expected"),1236    [1237        # Check not required1238        ({"bar": "bar"}, {"bar": "bar", "baz": 3, "buzz": "buzz"}),1239        # Check overwritten1240        (1241            {"bar": "bar", "baz": 4, "buzz": "not-buzz"},1242            {"bar": "bar", "baz": 4, "buzz": "not-buzz"},1243        ),1244        # Check validation error when missing1245        ({}, None),1246        # Check validation error when wrong type1247        ({"bar": "bar", "baz": "not-an-int"}, None),1248        # Check OK when None explicitly passed1249        ({"bar": "bar", "baz": None}, {"bar": "bar", "baz": None, "buzz": "buzz"}),1250    ],1251)1252def test_tool_invoke_optional_args(1253    inputs: dict[str, Any], expected: dict[str, Any] | None1254) -> None:1255    @tool1256    def foo(bar: str, baz: int | None = 3, buzz: str | None = "buzz") -> dict[str, Any]:1257        """The foo."""1258        return {1259            "bar": bar,1260            "baz": baz,1261            "buzz": buzz,1262        }12631264    if expected is not None:1265        assert foo.invoke(inputs) == expected1266    else:1267        with pytest.raises(ValidationError):1268            foo.invoke(inputs)126912701271def test_tool_pass_context() -> None:1272    @tool1273    def foo(bar: str) -> str:1274        """The foo."""1275        config = ensure_config()1276        assert config["configurable"]["foo"] == "not-bar"1277        assert bar == "baz"1278        return bar12791280    assert foo.invoke({"bar": "baz"}, {"configurable": {"foo": "not-bar"}}) == "baz"128112821283@pytest.mark.skipif(1284    sys.version_info < (3, 11),1285    reason="requires python3.11 or higher",1286)1287async def test_async_tool_pass_context() -> None:1288    @tool1289    async def foo(bar: str) -> str:1290        """The foo."""1291        config = ensure_config()1292        assert config["configurable"]["foo"] == "not-bar"1293        assert bar == "baz"1294        return bar12951296    assert (1297        await foo.ainvoke({"bar": "baz"}, {"configurable": {"foo": "not-bar"}}) == "baz"1298    )129913001301def assert_bar(bar: Any, bar_config: RunnableConfig) -> Any:1302    assert bar_config["configurable"]["foo"] == "not-bar"1303    assert bar == "baz"1304    return bar130513061307@tool1308def foo(bar: Any, bar_config: RunnableConfig) -> Any:1309    """The foo."""1310    return assert_bar(bar, bar_config)131113121313@tool1314async def afoo(bar: Any, bar_config: RunnableConfig) -> Any:1315    """The foo."""1316    return assert_bar(bar, bar_config)131713181319@tool(infer_schema=False)1320def simple_foo(bar: Any, bar_config: RunnableConfig) -> Any:1321    """The foo."""1322    return assert_bar(bar, bar_config)132313241325@tool(infer_schema=False)1326async def asimple_foo(bar: Any, bar_config: RunnableConfig) -> Any:1327    """The foo."""1328    return assert_bar(bar, bar_config)132913301331class FooBase(BaseTool):1332    name: str = "Foo"1333    description: str = "Foo"13341335    @override1336    def _run(self, bar: Any, bar_config: RunnableConfig, **kwargs: Any) -> Any:1337        return assert_bar(bar, bar_config)133813391340class AFooBase(FooBase):1341    @override1342    async def _arun(self, bar: Any, bar_config: RunnableConfig, **kwargs: Any) -> Any:1343        return assert_bar(bar, bar_config)134413451346@pytest.mark.parametrize("tool", [foo, simple_foo, FooBase(), AFooBase()])1347def test_tool_pass_config(tool: BaseTool) -> None:1348    assert tool.invoke({"bar": "baz"}, {"configurable": {"foo": "not-bar"}}) == "baz"13491350    # Test we don't mutate tool calls1351    tool_call = {1352        "name": tool.name,1353        "args": {"bar": "baz"},1354        "id": "abc123",1355        "type": "tool_call",1356    }1357    _ = tool.invoke(tool_call, {"configurable": {"foo": "not-bar"}})1358    assert tool_call["args"] == {"bar": "baz"}135913601361class FooBaseNonPickleable(FooBase):1362    @override1363    def _run(self, bar: Any, bar_config: RunnableConfig, **kwargs: Any) -> Any:1364        return True136513661367def test_tool_pass_config_non_pickleable() -> None:1368    tool = FooBaseNonPickleable()13691370    args = {"bar": threading.Lock()}1371    tool_call = {1372        "name": tool.name,1373        "args": args,1374        "id": "abc123",1375        "type": "tool_call",1376    }1377    _ = tool.invoke(tool_call, {"configurable": {"foo": "not-bar"}})1378    assert tool_call["args"] == args137913801381@pytest.mark.parametrize(1382    "tool", [foo, afoo, simple_foo, asimple_foo, FooBase(), AFooBase()]1383)1384async def test_async_tool_pass_config(tool: BaseTool) -> None:1385    assert (1386        await tool.ainvoke({"bar": "baz"}, {"configurable": {"foo": "not-bar"}})1387        == "baz"1388    )138913901391def test_tool_description() -> None:1392    def foo(bar: str) -> str:1393        """The foo."""1394        return bar13951396    foo1 = tool(foo)1397    assert foo1.description == "The foo."13981399    foo2 = StructuredTool.from_function(foo)1400    assert foo2.description == "The foo."140114021403def test_tool_arg_descriptions() -> None:1404    def foo(bar: str, baz: int) -> str:1405        """The foo.14061407        Args:1408            bar: The bar.1409            baz: The baz.1410        """1411        return bar14121413    foo1 = tool(foo)1414    args_schema = _schema(foo1.args_schema)1415    assert args_schema == {1416        "title": "foo",1417        "type": "object",1418        "description": inspect.getdoc(foo),1419        "properties": {1420            "bar": {"title": "Bar", "type": "string"},1421            "baz": {"title": "Baz", "type": "integer"},1422        },1423        "required": ["bar", "baz"],1424    }14251426    # Test parses docstring1427    foo2 = tool(foo, parse_docstring=True)1428    args_schema = _schema(foo2.args_schema)1429    expected = {1430        "title": "foo",1431        "description": "The foo.",1432        "type": "object",1433        "properties": {1434            "bar": {"title": "Bar", "description": "The bar.", "type": "string"},1435            "baz": {"title": "Baz", "description": "The baz.", "type": "integer"},1436        },1437        "required": ["bar", "baz"],1438    }1439    assert args_schema == expected14401441    # Test parsing with run_manager does not raise error1442    def foo3(  # noqa: D4171443        bar: str, baz: int, run_manager: CallbackManagerForToolRun | None = None1444    ) -> str:1445        """The foo.14461447        Args:1448            bar: The bar.1449            baz: The baz.1450        """1451        return bar14521453    as_tool = tool(foo3, parse_docstring=True)1454    args_schema = _schema(as_tool.args_schema)1455    assert args_schema["description"] == expected["description"]1456    assert args_schema["properties"] == expected["properties"]14571458    # Test parsing with runtime does not raise error1459    def foo3_runtime(bar: str, baz: int, runtime: Any) -> str:  # noqa: D4171460        """The foo.14611462        Args:1463            bar: The bar.1464            baz: The baz.1465        """1466        return bar14671468    _ = tool(foo3_runtime, parse_docstring=True)14691470    # Test parameterless tool does not raise error for missing Args section1471    # in docstring.1472    def foo4() -> str:1473        """The foo."""1474        return "bar"14751476    as_tool = tool(foo4, parse_docstring=True)1477    args_schema = _schema(as_tool.args_schema)1478    assert args_schema["description"] == expected["description"]14791480    def foo5(run_manager: CallbackManagerForToolRun | None = None) -> str:1481        """The foo."""1482        return "bar"14831484    as_tool = tool(foo5, parse_docstring=True)1485    args_schema = _schema(as_tool.args_schema)1486    assert args_schema["description"] == expected["description"]148714881489def test_docstring_parsing() -> None:1490    expected = {1491        "title": "foo",1492        "description": "The foo.",1493        "type": "object",1494        "properties": {1495            "bar": {"title": "Bar", "description": "The bar.", "type": "string"},1496            "baz": {"title": "Baz", "description": "The baz.", "type": "integer"},1497        },1498        "required": ["bar", "baz"],1499    }15001501    # Simple case1502    def foo(bar: str, baz: int) -> str:1503        """The foo.15041505        Args:1506            bar: The bar.1507            baz: The baz.1508        """1509        return bar15101511    as_tool = tool(foo, parse_docstring=True)1512    args_schema = _schema(as_tool.args_schema)1513    assert args_schema["description"] == "The foo."1514    assert args_schema["properties"] == expected["properties"]15151516    # Multi-line description1517    def foo2(bar: str, baz: int) -> str:1518        """The foo.15191520        Additional description here.15211522        Args:1523            bar: The bar.1524            baz: The baz.1525        """1526        return bar15271528    as_tool = tool(foo2, parse_docstring=True)1529    args_schema2 = _schema(as_tool.args_schema)1530    assert args_schema2["description"] == "The foo. Additional description here."1531    assert args_schema2["properties"] == expected["properties"]15321533    # Multi-line with Returns block1534    def foo3(bar: str, baz: int) -> str:1535        """The foo.15361537        Additional description here.15381539        Args:1540            bar: The bar.1541            baz: The baz.15421543        Returns:1544            description of returned value.1545        """1546        return bar15471548    as_tool = tool(foo3, parse_docstring=True)1549    args_schema3 = _schema(as_tool.args_schema)1550    args_schema3["title"] = "foo2"1551    assert args_schema2 == args_schema315521553    # Single argument1554    def foo4(bar: str) -> str:1555        """The foo.15561557        Args:1558            bar: The bar.1559        """1560        return bar15611562    as_tool = tool(foo4, parse_docstring=True)1563    args_schema4 = _schema(as_tool.args_schema)1564    assert args_schema4["description"] == "The foo."1565    assert args_schema4["properties"] == {1566        "bar": {"description": "The bar.", "title": "Bar", "type": "string"}1567    }156815691570def test_tool_invalid_docstrings() -> None:1571    """Test invalid docstrings."""15721573    def foo3(bar: str, baz: int) -> str:1574        """The foo."""1575        return bar15761577    def foo4(bar: str, baz: int) -> str:1578        """The foo.1579        Args:1580            bar: The bar.1581            baz: The baz.1582        """  # noqa: D205,D411  # We're intentionally testing bad formatting.1583        return bar15841585    for func in {foo3, foo4}:1586        with pytest.raises(ValueError, match="Found invalid Google-Style docstring"):1587            _ = tool(func, parse_docstring=True)15881589    def foo5(bar: str, baz: int) -> str:  # noqa: D4171590        """The foo.15911592        Args:1593            banana: The bar.1594            monkey: The baz.1595        """1596        return bar15971598    with pytest.raises(1599        ValueError, match="Arg banana in docstring not found in function signature"1600    ):1601        _ = tool(foo5, parse_docstring=True)160216031604def test_tool_annotated_descriptions() -> None:1605    def foo(1606        bar: Annotated[str, "this is the bar"], baz: Annotated[int, "this is the baz"]1607    ) -> str:1608        """The foo.16091610        Returns:1611            The bar only.1612        """1613        return bar16141615    foo1 = tool(foo)1616    args_schema = _schema(foo1.args_schema)1617    assert args_schema == {1618        "title": "foo",1619        "type": "object",1620        "description": inspect.getdoc(foo),1621        "properties": {1622            "bar": {"title": "Bar", "type": "string", "description": "this is the bar"},1623            "baz": {1624                "title": "Baz",1625                "type": "integer",1626                "description": "this is the baz",1627            },1628        },1629        "required": ["bar", "baz"],1630    }163116321633def test_tool_field_description_preserved() -> None:1634    """Test that `Field(description=...)` is preserved in `@tool` decorator."""16351636    @tool1637    def my_tool(1638        topic: Annotated[str, Field(description="The research topic")],1639        depth: Annotated[int, Field(description="Search depth level")] = 3,1640    ) -> str:1641        """A tool for research."""1642        return f"{topic} at depth {depth}"16431644    args_schema = _schema(my_tool.args_schema)1645    assert args_schema == {1646        "title": "my_tool",1647        "type": "object",1648        "description": "A tool for research.",1649        "properties": {1650            "topic": {1651                "title": "Topic",1652                "type": "string",1653                "description": "The research topic",1654            },1655            "depth": {1656                "title": "Depth",1657                "type": "integer",1658                "description": "Search depth level",1659                "default": 3,1660            },1661        },1662        "required": ["topic"],1663    }166416651666def test_tool_call_input_tool_message_output() -> None:1667    tool_call = {1668        "name": "structured_api",1669        "args": {"arg1": 1, "arg2": True, "arg3": {"img": "base64string..."}},1670        "id": "123",1671        "type": "tool_call",1672    }1673    tool = _MockStructuredTool()1674    expected = ToolMessage(1675        "1 True {'img': 'base64string...'}", tool_call_id="123", name="structured_api"1676    )1677    actual = tool.invoke(tool_call)1678    assert actual == expected16791680    tool_call.pop("type")1681    with pytest.raises(ValidationError):1682        tool.invoke(tool_call)168316841685@pytest.mark.parametrize("block_type", [*TOOL_MESSAGE_BLOCK_TYPES, "bad"])1686def test_tool_content_block_output(block_type: str) -> None:1687    @tool1688    def my_tool(query: str) -> list[dict[str, Any]]:1689        """Test tool."""1690        return [{"type": block_type, "foo": "bar"}]16911692    tool_call = {1693        "type": "tool_call",1694        "name": "my_tool",1695        "args": {"query": "baz"},1696        "id": "call_abc123",1697    }16981699    result = my_tool.invoke(tool_call)1700    assert isinstance(result, ToolMessage)17011702    if block_type in TOOL_MESSAGE_BLOCK_TYPES:1703        assert result.content == [{"type": block_type, "foo": "bar"}]1704    else:1705        assert result.content == '[{"type": "bad", "foo": "bar"}]'170617071708class _MockStructuredToolWithRawOutput(BaseTool):1709    name: str = "structured_api"1710    args_schema: type[BaseModel] = _MockSchema1711    description: str = "A Structured Tool"1712    response_format: Literal["content_and_artifact"] = "content_and_artifact"17131714    @override1715    def _run(1716        self,1717        arg1: int,1718        arg2: bool,1719        arg3: dict[str, Any] | None = None,1720    ) -> tuple[str, dict[str, Any]]:1721        return f"{arg1} {arg2}", {"arg1": arg1, "arg2": arg2, "arg3": arg3}172217231724@tool("structured_api", response_format="content_and_artifact")1725def _mock_structured_tool_with_artifact(1726    *, arg1: int, arg2: bool, arg3: dict[str, str] | None = None1727) -> tuple[str, dict[str, Any]]:1728    """A Structured Tool."""1729    return f"{arg1} {arg2}", {"arg1": arg1, "arg2": arg2, "arg3": arg3}173017311732@pytest.mark.parametrize(1733    "tool", [_MockStructuredToolWithRawOutput(), _mock_structured_tool_with_artifact]1734)1735def test_tool_call_input_tool_message_with_artifact(tool: BaseTool) -> None:1736    tool_call: dict[str, Any] = {1737        "name": "structured_api",1738        "args": {"arg1": 1, "arg2": True, "arg3": {"img": "base64string..."}},1739        "id": "123",1740        "type": "tool_call",1741    }1742    expected = ToolMessage(1743        "1 True", artifact=tool_call["args"], tool_call_id="123", name="structured_api"1744    )1745    actual = tool.invoke(tool_call)1746    assert actual == expected17471748    tool_call.pop("type")1749    with pytest.raises(ValidationError):1750        tool.invoke(tool_call)17511752    actual_content = tool.invoke(tool_call["args"])1753    assert actual_content == expected.content175417551756def test_convert_from_runnable_dict() -> None:1757    # Test with typed dict input1758    class Args(TypedDict):1759        a: int1760        b: list[int]17611762    def f(x: Args) -> str:1763        return str(x["a"] * max(x["b"]))17641765    runnable = RunnableLambda(f)1766    as_tool = runnable.as_tool()1767    args_schema = as_tool.args_schema1768    assert args_schema is not None1769    assert _schema(args_schema) == {1770        "title": "f",1771        "type": "object",1772        "properties": {1773            "a": {"title": "A", "type": "integer"},1774            "b": {"title": "B", "type": "array", "items": {"type": "integer"}},1775        },1776        "required": ["a", "b"],1777    }1778    assert as_tool.description1779    result = as_tool.invoke({"a": 3, "b": [1, 2]})1780    assert result == "6"17811782    as_tool = runnable.as_tool(name="my tool", description="test description")1783    assert as_tool.name == "my tool"1784    assert as_tool.description == "test description"17851786    # Dict without typed input-- must supply schema1787    def g(x: dict[str, Any]) -> str:1788        return str(x["a"] * max(x["b"]))17891790    # Specify via args_schema:1791    class GSchema(BaseModel):1792        """Apply a function to an integer and list of integers."""17931794        a: int = Field(..., description="Integer")1795        b: list[int] = Field(..., description="List of ints")17961797    runnable2 = RunnableLambda(g)1798    as_tool2 = runnable2.as_tool(GSchema)1799    as_tool2.invoke({"a": 3, "b": [1, 2]})18001801    # Specify via arg_types:1802    runnable3 = RunnableLambda(g)1803    as_tool3 = runnable3.as_tool(arg_types={"a": int, "b": list[int]})1804    result = as_tool3.invoke({"a": 3, "b": [1, 2]})1805    assert result == "6"18061807    # Test with config1808    def h(x: dict[str, Any]) -> str:1809        config = ensure_config()1810        assert config["configurable"]["foo"] == "not-bar"1811        return str(x["a"] * max(x["b"]))18121813    runnable4 = RunnableLambda(h)1814    as_tool4 = runnable4.as_tool(arg_types={"a": int, "b": list[int]})1815    result = as_tool4.invoke(1816        {"a": 3, "b": [1, 2]}, config={"configurable": {"foo": "not-bar"}}1817    )1818    assert result == "6"181918201821def test_convert_from_runnable_root_model_input_schema() -> None:1822    """`as_tool` should not advertise a `TypedDict` input nested under `root`.18231824    Some `Runnable`s (e.g. a compiled `langgraph` `StateGraph`) expose a1825    `pydantic.RootModel` as `input_schema` even though `get_input_jsonschema`1826    reports a flat object schema. See:1827    """18281829    class Args(TypedDict):1830        foo: str1831        bar: str18321833    class _RootModelInputRunnable(RunnableLambda[Args, str]):1834        @override1835        def get_input_schema(1836            self, config: RunnableConfig | None = None1837        ) -> TypeBaseModel:1838            return RootModel[Args]18391840        @override1841        def get_input_jsonschema(1842            self, config: RunnableConfig | None = None1843        ) -> dict[str, Any]:1844            return {1845                "type": "object",1846                "properties": {1847                    "foo": {"type": "string"},1848                    "bar": {"type": "string"},1849                },1850                "required": ["foo", "bar"],1851            }18521853    def f(x: Args) -> str:1854        return f"{x['foo']} {x['bar']}"18551856    runnable = _RootModelInputRunnable(f)1857    as_tool = runnable.as_tool(name="my_tool", description="Example tool.")18581859    assert as_tool.args_schema is not None1860    assert isinstance(as_tool.args_schema, type)1861    assert not issubclass(as_tool.args_schema, RootModel)18621863    oai_schema = convert_to_openai_tool(as_tool)1864    parameters = oai_schema["function"]["parameters"]1865    assert parameters["properties"].keys() == {"foo", "bar"}1866    assert "root" not in parameters["properties"]18671868    result = as_tool.invoke({"foo": "hello", "bar": "world"})1869    assert result == "hello world"187018711872def test_convert_from_runnable_other() -> None:1873    # String input1874    def f(x: str) -> str:1875        return x + "a"18761877    def g(x: str) -> str:1878        return x + "z"18791880    runnable = RunnableLambda(f) | g1881    as_tool = runnable.as_tool()1882    args_schema = as_tool.args_schema1883    assert args_schema is None1884    assert as_tool.description18851886    result = as_tool.invoke("b")1887    assert result == "baz"18881889    # Test with config1890    def h(x: str) -> str:1891        config = ensure_config()1892        assert config["configurable"]["foo"] == "not-bar"1893        return x + "a"18941895    runnable2 = RunnableLambda(h)1896    as_tool2 = runnable2.as_tool()1897    result2 = as_tool2.invoke("b", config={"configurable": {"foo": "not-bar"}})1898    assert result2 == "ba"189919001901@tool("foo", parse_docstring=True)1902def injected_tool(x: int, y: Annotated[str, InjectedToolArg]) -> str:1903    """Foo.19041905    Args:1906        x: abc1907        y: 1231908    """1909    return y191019111912class InjectedTool(BaseTool):1913    name: str = "foo"1914    description: str = "foo."19151916    @override1917    def _run(self, x: int, y: Annotated[str, InjectedToolArg]) -> Any:1918        """Foo.19191920        Args:1921            x: abc1922            y: 1231923        """1924        return y192519261927class fooSchema(BaseModel):  # noqa: N8011928    """foo."""19291930    x: int = Field(..., description="abc")1931    y: Annotated[str, "foobar comment", InjectedToolArg()] = Field(1932        ..., description="123"1933    )193419351936class InjectedToolWithSchema(BaseTool):1937    name: str = "foo"1938    description: str = "foo."1939    args_schema: type[BaseModel] = fooSchema19401941    @override1942    def _run(self, x: int, y: str) -> Any:1943        return y194419451946@tool("foo", args_schema=fooSchema)1947def injected_tool_with_schema(x: int, y: str) -> str:1948    return y194919501951@pytest.mark.parametrize("tool_", [InjectedTool()])1952def test_tool_injected_arg_without_schema(tool_: BaseTool) -> None:1953    assert _schema(tool_.get_input_schema()) == {1954        "title": "foo",1955        "description": "Foo.\n\nArgs:\n    x: abc\n    y: 123",1956        "type": "object",1957        "properties": {1958            "x": {"title": "X", "type": "integer"},1959            "y": {"title": "Y", "type": "string"},1960        },1961        "required": ["x", "y"],1962    }1963    assert _schema(tool_.tool_call_schema) == {1964        "title": "foo",1965        "description": "foo.",1966        "type": "object",1967        "properties": {"x": {"title": "X", "type": "integer"}},1968        "required": ["x"],1969    }1970    assert tool_.invoke({"x": 5, "y": "bar"}) == "bar"1971    assert tool_.invoke(1972        {1973            "name": "foo",1974            "args": {"x": 5, "y": "bar"},1975            "id": "123",1976            "type": "tool_call",1977        }1978    ) == ToolMessage("bar", tool_call_id="123", name="foo")1979    expected_error = (1980        ValidationError if not isinstance(tool_, InjectedTool) else TypeError1981    )1982    with pytest.raises(expected_error):1983        tool_.invoke({"x": 5})19841985    assert convert_to_openai_function(tool_) == {1986        "name": "foo",1987        "description": "foo.",1988        "parameters": {1989            "type": "object",1990            "properties": {"x": {"type": "integer"}},1991            "required": ["x"],1992        },1993    }199419951996@pytest.mark.parametrize(1997    "tool_",1998    [injected_tool_with_schema, InjectedToolWithSchema()],1999)2000def test_tool_injected_arg_with_schema(tool_: BaseTool) -> None:

Findings

✓ No findings reported for this file.

Get this view in your editor

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