libs/core/tests/unit_tests/test_tools.py PYTHON 3,949 lines View on github.com → Search inside
File is large — showing lines 1–2,000 of 3,949.
1"""Test the base tool implementation."""23import inspect4import json5import logging6import sys7import textwrap8import threading9from collections.abc import Callable10from dataclasses import dataclass11from datetime import datetime12from enum import Enum13from functools import partial14from typing import (15    Annotated,16    Any,17    Generic,18    Literal,19    TypeVar,20    cast,21    get_type_hints,22)2324import pytest25from pydantic import BaseModel, ConfigDict, Field, ValidationError26from pydantic.v1 import BaseModel as BaseModelV127from pydantic.v1 import ValidationError as ValidationErrorV128from typing_extensions import TypedDict, override2930from langchain_core import tools31from langchain_core.callbacks import (32    AsyncCallbackManagerForToolRun,33    CallbackManagerForToolRun,34)35from langchain_core.callbacks.manager import (36    CallbackManagerForRetrieverRun,37)38from langchain_core.documents import Document39from langchain_core.messages import ToolCall, ToolMessage40from langchain_core.messages.tool import ToolOutputMixin41from langchain_core.retrievers import BaseRetriever42from langchain_core.runnables import (43    Runnable,44    RunnableConfig,45    RunnableLambda,46    ensure_config,47)48from langchain_core.tools import (49    BaseTool,50    StructuredTool,51    Tool,52    ToolException,53    convert_runnable_to_tool,54    tool,55)56from langchain_core.tools.base import (57    TOOL_MESSAGE_BLOCK_TYPES,58    ArgsSchema,59    InjectedToolArg,60    InjectedToolCallId,61    SchemaAnnotationError,62    _DirectlyInjectedToolArg,63    _format_output,64    _is_message_content_block,65    _normalize_message_content,66    get_all_basemodel_annotations,67)68from langchain_core.utils.function_calling import (69    convert_to_openai_function,70    convert_to_openai_tool,71)72from langchain_core.utils.pydantic import (73    TypeBaseModel,74    _create_subset_model,75    create_model_v2,76)77from tests.unit_tests.fake.callbacks import FakeCallbackHandler78from tests.unit_tests.pydantic_utils import (79    _normalize_schema,80    _schema,81    skip_if_no_pydantic_v1,82)8384try:85    from langgraph.prebuilt import ToolRuntime  # type: ignore[import-not-found]8687    HAS_LANGGRAPH = True88except ImportError:89    HAS_LANGGRAPH = False909192def _get_tool_call_json_schema(tool: BaseTool) -> dict[str, Any]:93    tool_schema = tool.tool_call_schema94    if isinstance(tool_schema, dict):95        return tool_schema9697    if issubclass(tool_schema, BaseModel):98        return tool_schema.model_json_schema()99    if issubclass(tool_schema, BaseModelV1):100        return tool_schema.schema()101    return {}102103104def test_unnamed_decorator() -> None:105    """Test functionality with unnamed decorator."""106107    @tool108    def search_api(query: str) -> str:109        """Search the API for the query."""110        return "API result"111112    assert isinstance(search_api, BaseTool)113    assert search_api.name == "search_api"114    assert not search_api.return_direct115    assert search_api.invoke("test") == "API result"116117118class _MockSchema(BaseModel):119    """Return the arguments directly."""120121    arg1: int122    arg2: bool123    arg3: dict[str, Any] | None = None124125126class _MockStructuredTool(BaseTool):127    name: str = "structured_api"128    args_schema: type[BaseModel] = _MockSchema129    description: str = "A Structured Tool"130131    @override132    def _run(self, *, arg1: int, arg2: bool, arg3: dict[str, Any] | None = None) -> str:133        return f"{arg1} {arg2} {arg3}"134135    async def _arun(136        self, *, arg1: int, arg2: bool, arg3: dict[str, Any] | None = None137    ) -> str:138        raise NotImplementedError139140141class _FakeOutput(ToolOutputMixin):142    """Minimal ToolOutputMixin subclass used only in tests."""143144    def __init__(self, value: int) -> None:145        self.value = value146147    def __eq__(self, other: object) -> bool:148        return isinstance(other, _FakeOutput) and self.value == other.value149150    def __hash__(self) -> int:151        return hash(self.value)152153    def __repr__(self) -> str:154        return f"_FakeOutput({self.value})"155156157def test_structured_args() -> None:158    """Test functionality with structured arguments."""159    structured_api = _MockStructuredTool()160    assert isinstance(structured_api, BaseTool)161    assert structured_api.name == "structured_api"162    expected_result = "1 True {'foo': 'bar'}"163    args = {"arg1": 1, "arg2": True, "arg3": {"foo": "bar"}}164    assert structured_api.run(args) == expected_result165166167def test_misannotated_base_tool_raises_error() -> None:168    """Test that a BaseTool with the incorrect typehint raises an exception."""169    with pytest.raises(SchemaAnnotationError):170171        class _MisAnnotatedTool(BaseTool):172            name: str = "structured_api"173            # This would silently be ignored without the custom metaclass174            args_schema: BaseModel = _MockSchema  # type: ignore[assignment]175            description: str = "A Structured Tool"176177            @override178            def _run(179                self, *, arg1: int, arg2: bool, arg3: dict[str, Any] | None = None180            ) -> str:181                return f"{arg1} {arg2} {arg3}"182183            async def _arun(184                self, *, arg1: int, arg2: bool, arg3: dict[str, Any] | None = None185            ) -> str:186                raise NotImplementedError187188189def test_forward_ref_annotated_base_tool_accepted() -> None:190    """Test that a using forward ref annotation syntax is accepted."""191192    class _ForwardRefAnnotatedTool(BaseTool):193        name: str = "structured_api"194        args_schema: "type[BaseModel]" = _MockSchema195        description: str = "A Structured Tool"196197        @override198        def _run(199            self, *, arg1: int, arg2: bool, arg3: dict[str, Any] | None = None200        ) -> str:201            return f"{arg1} {arg2} {arg3}"202203        async def _arun(204            self, *, arg1: int, arg2: bool, arg3: dict[str, Any] | None = None205        ) -> str:206            raise NotImplementedError207208209def test_subclass_annotated_base_tool_accepted() -> None:210    """Test BaseTool child w/ custom schema isn't overwritten."""211212    class _ForwardRefAnnotatedTool(BaseTool):213        name: str = "structured_api"214        args_schema: type[_MockSchema] = _MockSchema215        description: str = "A Structured Tool"216217        @override218        def _run(219            self, *, arg1: int, arg2: bool, arg3: dict[str, Any] | None = None220        ) -> str:221            return f"{arg1} {arg2} {arg3}"222223        async def _arun(224            self, *, arg1: int, arg2: bool, arg3: dict[str, Any] | None = None225        ) -> str:226            raise NotImplementedError227228    assert issubclass(_ForwardRefAnnotatedTool, BaseTool)229    tool = _ForwardRefAnnotatedTool()230    assert tool.args_schema == _MockSchema231232233def test_decorator_with_specified_schema() -> None:234    """Test that manually specified schemata are passed through to the tool."""235236    @tool(args_schema=_MockSchema)237    def tool_func(*, arg1: int, arg2: bool, arg3: dict[str, Any] | None = None) -> str:238        return f"{arg1} {arg2} {arg3}"239240    assert isinstance(tool_func, BaseTool)241    assert tool_func.args_schema == _MockSchema242243244@pytest.mark.skipif(245    sys.version_info >= (3, 14),246    reason="pydantic.v1 namespace not supported with Python 3.14+",247)248def test_decorator_with_specified_schema_pydantic_v1() -> None:249    """Test that manually specified schemata are passed through to the tool."""250251    class _MockSchemaV1(BaseModelV1):252        """Return the arguments directly."""253254        arg1: int255        arg2: bool256        arg3: dict[str, Any] | None = None257258    @tool(args_schema=cast("ArgsSchema", _MockSchemaV1))259    def tool_func_v1(260        *, arg1: int, arg2: bool, arg3: dict[str, Any] | None = None261    ) -> str:262        return f"{arg1} {arg2} {arg3}"263264    assert isinstance(tool_func_v1, BaseTool)265    assert tool_func_v1.args_schema == cast("ArgsSchema", _MockSchemaV1)266267268def test_decorated_function_schema_equivalent() -> None:269    """Test that a BaseTool without a schema meets expectations."""270271    @tool272    def structured_tool_input(273        *, arg1: int, arg2: bool, arg3: dict[str, Any] | None = None274    ) -> str:275        """Return the arguments directly."""276        return f"{arg1} {arg2} {arg3}"277278    assert isinstance(structured_tool_input, BaseTool)279    assert structured_tool_input.args_schema is not None280    assert (281        _schema(structured_tool_input.args_schema)["properties"]282        == _schema(_MockSchema)["properties"]283        == _normalize_schema(structured_tool_input.args)284    )285286287def test_args_kwargs_filtered() -> None:288    class _SingleArgToolWithKwargs(BaseTool):289        name: str = "single_arg_tool"290        description: str = "A  single arged tool with kwargs"291292        @override293        def _run(294            self,295            some_arg: str,296            run_manager: CallbackManagerForToolRun | None = None,297            **kwargs: Any,298        ) -> str:299            return "foo"300301        async def _arun(302            self,303            some_arg: str,304            run_manager: AsyncCallbackManagerForToolRun | None = None,305            **kwargs: Any,306        ) -> str:307            raise NotImplementedError308309    tool = _SingleArgToolWithKwargs()310    assert tool.is_single_input311312    class _VarArgToolWithKwargs(BaseTool):313        name: str = "single_arg_tool"314        description: str = "A single arged tool with kwargs"315316        @override317        def _run(318            self,319            *args: Any,320            run_manager: CallbackManagerForToolRun | None = None,321            **kwargs: Any,322        ) -> str:323            return "foo"324325        async def _arun(326            self,327            *args: Any,328            run_manager: AsyncCallbackManagerForToolRun | None = None,329            **kwargs: Any,330        ) -> str:331            raise NotImplementedError332333    tool2 = _VarArgToolWithKwargs()334    assert tool2.is_single_input335336337def test_structured_args_decorator_no_infer_schema() -> None:338    """Test functionality with structured arguments parsed as a decorator."""339340    @tool(infer_schema=False)341    def structured_tool_input(342        arg1: int, arg2: float | datetime, opt_arg: dict[str, Any] | None = None343    ) -> str:344        """Return the arguments directly."""345        return f"{arg1}, {arg2}, {opt_arg}"346347    assert isinstance(structured_tool_input, BaseTool)348    assert structured_tool_input.name == "structured_tool_input"349    args = {"arg1": 1, "arg2": 0.001, "opt_arg": {"foo": "bar"}}350    with pytest.raises(ToolException):351        assert structured_tool_input.run(args)352353354def test_structured_single_str_decorator_no_infer_schema() -> None:355    """Test functionality with structured arguments parsed as a decorator."""356357    @tool(infer_schema=False)358    def unstructured_tool_input(tool_input: str) -> str:359        """Return the arguments directly."""360        assert isinstance(tool_input, str)361        return f"{tool_input}"362363    assert isinstance(unstructured_tool_input, BaseTool)364    assert unstructured_tool_input.args_schema is None365    assert unstructured_tool_input.run("foo") == "foo"366367368def test_structured_tool_types_parsed() -> None:369    """Test the non-primitive types are correctly passed to structured tools."""370371    class SomeEnum(Enum):372        A = "a"373        B = "b"374375    class SomeBaseModel(BaseModel):376        foo: str377378    @tool379    def structured_tool(380        some_enum: SomeEnum,381        some_base_model: SomeBaseModel,382    ) -> dict[str, Any]:383        """Return the arguments directly."""384        return {385            "some_enum": some_enum,386            "some_base_model": some_base_model,387        }388389    assert isinstance(structured_tool, StructuredTool)390    args = {391        "some_enum": SomeEnum.A.value,392        "some_base_model": SomeBaseModel(foo="bar").model_dump(),393    }394    result = structured_tool.run(json.loads(json.dumps(args)))395    expected = {396        "some_enum": SomeEnum.A,397        "some_base_model": SomeBaseModel(foo="bar"),398    }399    assert result == expected400401402@pytest.mark.skipif(403    sys.version_info >= (3, 14),404    reason="pydantic.v1 namespace not supported with Python 3.14+",405)406def test_structured_tool_types_parsed_pydantic_v1() -> None:407    """Test the non-primitive types are correctly passed to structured tools."""408409    class SomeBaseModel(BaseModelV1):410        foo: str411412    class AnotherBaseModel(BaseModelV1):413        bar: str414415    @tool416    def structured_tool(some_base_model: SomeBaseModel) -> AnotherBaseModel:417        """Return the arguments directly."""418        return AnotherBaseModel(bar=some_base_model.foo)419420    assert isinstance(structured_tool, StructuredTool)421422    expected = AnotherBaseModel(bar="baz")423    for arg in [424        SomeBaseModel(foo="baz"),425        SomeBaseModel(foo="baz").dict(),426    ]:427        args = {"some_base_model": arg}428        result = structured_tool.run(args)429        assert result == expected430431432def test_structured_tool_types_parsed_pydantic_mixed() -> None:433    """Test handling of tool with mixed Pydantic version arguments."""434435    class SomeBaseModel(BaseModelV1):436        foo: str437438    class AnotherBaseModel(BaseModel):439        bar: str440441    with pytest.raises(NotImplementedError):442443        @tool444        def structured_tool(445            some_base_model: SomeBaseModel, another_base_model: AnotherBaseModel446        ) -> None:447            """Return the arguments directly."""448449450def test_base_tool_inheritance_base_schema() -> None:451    """Test schema is correctly inferred when inheriting from BaseTool."""452453    class _MockSimpleTool(BaseTool):454        name: str = "simple_tool"455        description: str = "A Simple Tool"456457        @override458        def _run(self, tool_input: str) -> str:459            return f"{tool_input}"460461        @override462        async def _arun(self, tool_input: str) -> str:463            raise NotImplementedError464465    simple_tool = _MockSimpleTool()466    assert simple_tool.args_schema is None467    expected_args = {"tool_input": {"title": "Tool Input", "type": "string"}}468    assert simple_tool.args == expected_args469470471def test_tool_lambda_args_schema() -> None:472    """Test args schema inference when the tool argument is a lambda function."""473    tool = Tool(474        name="tool",475        description="A tool",476        func=lambda tool_input: tool_input,477    )478    assert tool.args_schema is None479    expected_args = {"tool_input": {"type": "string"}}480    assert tool.args == expected_args481482483def test_structured_tool_from_function_docstring() -> None:484    """Test that structured tools can be created from functions."""485486    def foo(bar: int, baz: str) -> str:487        """Docstring.488489        Args:490            bar: the bar value491            baz: the baz value492        """493        raise NotImplementedError494495    structured_tool = StructuredTool.from_function(foo)496    assert structured_tool.name == "foo"497    assert structured_tool.args == {498        "bar": {"title": "Bar", "type": "integer"},499        "baz": {"title": "Baz", "type": "string"},500    }501502    assert _schema(structured_tool.args_schema) == {503        "properties": {504            "bar": {"title": "Bar", "type": "integer"},505            "baz": {"title": "Baz", "type": "string"},506        },507        "description": inspect.getdoc(foo),508        "title": "foo",509        "type": "object",510        "required": ["bar", "baz"],511    }512513    assert foo.__doc__ is not None514    assert structured_tool.description == textwrap.dedent(foo.__doc__.strip())515516517def test_structured_tool_from_function_docstring_complex_args() -> None:518    """Test that structured tools can be created from functions."""519520    def foo(bar: int, baz: list[str]) -> str:521        """Docstring.522523        Args:524            bar: int525            baz: list[str]526        """527        raise NotImplementedError528529    structured_tool = StructuredTool.from_function(foo)530    assert structured_tool.name == "foo"531    assert structured_tool.args == {532        "bar": {"title": "Bar", "type": "integer"},533        "baz": {534            "title": "Baz",535            "type": "array",536            "items": {"type": "string"},537        },538    }539540    assert _schema(structured_tool.args_schema) == {541        "properties": {542            "bar": {"title": "Bar", "type": "integer"},543            "baz": {544                "title": "Baz",545                "type": "array",546                "items": {"type": "string"},547            },548        },549        "description": inspect.getdoc(foo),550        "title": "foo",551        "type": "object",552        "required": ["bar", "baz"],553    }554555    assert foo.__doc__ is not None556    assert structured_tool.description == textwrap.dedent(foo.__doc__).strip()557558559def test_structured_tool_lambda_multi_args_schema() -> None:560    """Test args schema inference when the tool argument is a lambda function."""561    tool = StructuredTool.from_function(562        name="tool",563        description="A tool",564        func=lambda tool_input, other_arg: f"{tool_input}{other_arg}",565    )566    assert tool.args_schema is not None567    expected_args = {568        "tool_input": {"title": "Tool Input"},569        "other_arg": {"title": "Other Arg"},570    }571    assert tool.args == expected_args572573574def test_tool_partial_function_args_schema() -> None:575    """Test args schema inference when the tool argument is a partial function."""576577    def func(tool_input: str, other_arg: str) -> str:578        assert isinstance(tool_input, str)579        assert isinstance(other_arg, str)580        return tool_input + other_arg581582    tool = Tool(583        name="tool",584        description="A tool",585        func=partial(func, other_arg="foo"),586    )587    assert tool.run("bar") == "barfoo"588589590def test_empty_args_decorator() -> None:591    """Test inferred schema of decorated fn with no args."""592593    @tool594    def empty_tool_input() -> str:595        """Return a constant."""596        return "the empty result"597598    assert isinstance(empty_tool_input, BaseTool)599    assert empty_tool_input.name == "empty_tool_input"600    assert empty_tool_input.args == {}601    assert empty_tool_input.run({}) == "the empty result"602603604def test_tool_from_function_with_run_manager() -> None:605    """Test run of tool when using run_manager."""606607    def foo(bar: str, callbacks: CallbackManagerForToolRun | None = None) -> str:  # noqa: D417608        """Docstring.609610        Args:611            bar: str.612        """613        assert callbacks is not None614        return "foo" + bar615616    handler = FakeCallbackHandler()617    tool = Tool.from_function(foo, name="foo", description="Docstring")618619    assert tool.run(tool_input={"bar": "bar"}, run_manager=[handler]) == "foobar"620    assert tool.run("baz", run_manager=[handler]) == "foobaz"621622623def test_structured_tool_from_function_with_run_manager() -> None:624    """Test args and schema of structured tool when using callbacks."""625626    def foo(  # noqa: D417627        bar: int, baz: str, callbacks: CallbackManagerForToolRun | None = None628    ) -> str:629        """Docstring.630631        Args:632            bar: int633            baz: str634        """635        assert callbacks is not None636        return str(bar) + baz637638    handler = FakeCallbackHandler()639    structured_tool = StructuredTool.from_function(foo)640641    assert structured_tool.args == {642        "bar": {"title": "Bar", "type": "integer"},643        "baz": {"title": "Baz", "type": "string"},644    }645646    assert _schema(structured_tool.args_schema) == {647        "properties": {648            "bar": {"title": "Bar", "type": "integer"},649            "baz": {"title": "Baz", "type": "string"},650        },651        "description": inspect.getdoc(foo),652        "title": "foo",653        "type": "object",654        "required": ["bar", "baz"],655    }656657    assert (658        structured_tool.run(659            tool_input={"bar": "10", "baz": "baz"}, run_manger=[handler]660        )661        == "10baz"662    )663664665def test_structured_tool_from_parameterless_function() -> None:666    """Test parameterless function of structured tool."""667668    def foo() -> str:669        """Docstring."""670        return "invoke foo"671672    structured_tool = StructuredTool.from_function(foo)673674    assert structured_tool.run({}) == "invoke foo"675    assert structured_tool.run("") == "invoke foo"676677678def test_named_tool_decorator() -> None:679    """Test functionality when arguments are provided as input to decorator."""680681    @tool("search")682    def search_api(query: str) -> str:683        """Search the API for the query."""684        assert isinstance(query, str)685        return f"API result - {query}"686687    assert isinstance(search_api, BaseTool)688    assert search_api.name == "search"689    assert not search_api.return_direct690    assert search_api.run({"query": "foo"}) == "API result - foo"691692693def test_named_tool_decorator_return_direct() -> None:694    """Test functionality when arguments and return direct are provided as input."""695696    @tool("search", return_direct=True)697    def search_api(query: str, *args: Any) -> str:698        """Search the API for the query."""699        return "API result"700701    assert isinstance(search_api, BaseTool)702    assert search_api.name == "search"703    assert search_api.return_direct704    assert search_api.run({"query": "foo"}) == "API result"705706707def test_unnamed_tool_decorator_return_direct() -> None:708    """Test functionality when only return direct is provided."""709710    @tool(return_direct=True)711    def search_api(query: str) -> str:712        """Search the API for the query."""713        assert isinstance(query, str)714        return "API result"715716    assert isinstance(search_api, BaseTool)717    assert search_api.name == "search_api"718    assert search_api.return_direct719    assert search_api.run({"query": "foo"}) == "API result"720721722def test_tool_with_kwargs() -> None:723    """Test functionality when only return direct is provided."""724725    @tool(return_direct=True)726    def search_api(727        arg_0: str,728        arg_1: float = 4.3,729        ping: str = "hi",730    ) -> str:731        """Search the API for the query."""732        return f"arg_0={arg_0}, arg_1={arg_1}, ping={ping}"733734    assert isinstance(search_api, BaseTool)735    result = search_api.run(736        tool_input={737            "arg_0": "foo",738            "arg_1": 3.2,739            "ping": "pong",740        }741    )742    assert result == "arg_0=foo, arg_1=3.2, ping=pong"743744    result = search_api.run(745        tool_input={746            "arg_0": "foo",747        }748    )749    assert result == "arg_0=foo, arg_1=4.3, ping=hi"750    # For backwards compatibility, we still accept a single str arg751    result = search_api.run("foobar")752    assert result == "arg_0=foobar, arg_1=4.3, ping=hi"753754755def test_missing_docstring() -> None:756    """Test error is raised when docstring is missing."""757    # expect to throw a value error if there's no docstring758    with pytest.raises(ValueError, match="Function must have a docstring"):759760        @tool761        def search_api(query: str) -> str:762            return "API result"763764    @tool765    class MyTool(BaseModel):766        foo: str767768    assert not MyTool.description  # type: ignore[attr-defined]769770771def test_create_tool_positional_args() -> None:772    """Test that positional arguments are allowed."""773    test_tool = Tool("test_name", lambda x: x, "test_description")774    assert test_tool.invoke("foo") == "foo"775    assert test_tool.name == "test_name"776    assert test_tool.description == "test_description"777    assert test_tool.is_single_input778779780def test_create_tool_keyword_args() -> None:781    """Test that keyword arguments are allowed."""782    test_tool = Tool(name="test_name", func=lambda x: x, description="test_description")783    assert test_tool.is_single_input784    assert test_tool.invoke("foo") == "foo"785    assert test_tool.name == "test_name"786    assert test_tool.description == "test_description"787788789async def test_create_async_tool() -> None:790    """Test that async tools are allowed."""791792    async def _test_func(x: str) -> str:793        return x794795    test_tool = Tool(796        name="test_name",797        func=lambda x: x,798        description="test_description",799        coroutine=_test_func,800    )801    assert test_tool.is_single_input802    assert test_tool.invoke("foo") == "foo"803    assert test_tool.name == "test_name"804    assert test_tool.description == "test_description"805    assert test_tool.coroutine is not None806    assert await test_tool.arun("foo") == "foo"807808809class _FakeExceptionTool(BaseTool):810    name: str = "exception"811    description: str = "an exception-throwing tool"812    exception: Exception = ToolException()813814    def _run(self) -> str:815        raise self.exception816817    async def _arun(self) -> str:818        raise self.exception819820821def test_exception_handling_bool() -> None:822    tool_ = _FakeExceptionTool(handle_tool_error=True)823    expected = "Tool execution error"824    actual = tool_.run({})825    assert expected == actual826827828def test_exception_handling_str() -> None:829    expected = "foo bar"830    tool_ = _FakeExceptionTool(handle_tool_error=expected)831    actual = tool_.run({})832    assert expected == actual833834835def test_exception_handling_callable() -> None:836    expected = "foo bar"837838    def handling(e: ToolException) -> str:839        return expected840841    tool_ = _FakeExceptionTool(handle_tool_error=handling)842    actual = tool_.run({})843    assert expected == actual844845846def test_exception_handling_callable_message_content_blocks() -> None:847    expected: list[dict[str, Any]] = [{"type": "text", "text": "handled error"}]848849    def handling(e: ToolException) -> list[dict[str, Any]]:850        return expected851852    tool_ = _FakeExceptionTool(handle_tool_error=handling)853    actual = tool_.invoke(854        {"type": "tool_call", "args": {}, "name": "exception", "id": "call_1"}855    )856857    assert isinstance(actual, ToolMessage)858    assert actual.content == expected859    assert actual.status == "error"860    assert actual.tool_call_id == "call_1"861862863def test_exception_handling_callable_message_content_blocks_sequence() -> None:864    content = ({"type": "text", "text": "handled error"},)865866    def handling(e: ToolException) -> tuple[dict[str, Any], ...]:867        return content868869    tool_ = _FakeExceptionTool(handle_tool_error=handling)870    actual = tool_.invoke(871        {"type": "tool_call", "args": {}, "name": "exception", "id": "call_1"}872    )873874    assert isinstance(actual, ToolMessage)875    assert actual.content == list(content)876    assert actual.status == "error"877    assert actual.tool_call_id == "call_1"878879880def test_exception_handling_callable_invalid_blocks_stringified() -> None:881    # A sequence whose elements are not valid content blocks is not message882    # content, so it falls back to a JSON-stringified ToolMessage.883    def handling(e: ToolException) -> list[dict[str, Any]]:884        return [{"text": "foo"}]  # missing 'type' -> not a valid block885886    tool_ = _FakeExceptionTool(handle_tool_error=handling)887    actual = tool_.invoke(888        {"type": "tool_call", "args": {}, "name": "exception", "id": "call_1"}889    )890891    assert isinstance(actual, ToolMessage)892    assert actual.content == '[{"text": "foo"}]'893    assert actual.status == "error"894    assert actual.tool_call_id == "call_1"895896897def test_exception_handling_non_tool_exception() -> None:898    tool_ = _FakeExceptionTool(exception=ValueError("some error"))899    with pytest.raises(ValueError, match="some error"):900        tool_.run({})901902903async def test_async_exception_handling_bool() -> None:904    tool_ = _FakeExceptionTool(handle_tool_error=True)905    expected = "Tool execution error"906    actual = await tool_.arun({})907    assert expected == actual908909910async def test_async_exception_handling_str() -> None:911    expected = "foo bar"912    tool_ = _FakeExceptionTool(handle_tool_error=expected)913    actual = await tool_.arun({})914    assert expected == actual915916917async def test_async_exception_handling_callable() -> None:918    expected = "foo bar"919920    def handling(e: ToolException) -> str:921        return expected922923    tool_ = _FakeExceptionTool(handle_tool_error=handling)924    actual = await tool_.arun({})925    assert expected == actual926927928async def test_async_exception_handling_callable_message_content_blocks() -> None:929    expected: list[dict[str, Any]] = [{"type": "text", "text": "handled error"}]930931    def handling(e: ToolException) -> list[dict[str, Any]]:932        return expected933934    tool_ = _FakeExceptionTool(handle_tool_error=handling)935    actual = await tool_.ainvoke(936        {"type": "tool_call", "args": {}, "name": "exception", "id": "call_1"}937    )938939    assert isinstance(actual, ToolMessage)940    assert actual.content == expected941    assert actual.status == "error"942    assert actual.tool_call_id == "call_1"943944945async def test_async_exception_handling_callable_message_content_blocks_sequence() -> (946    None947):948    content = ({"type": "text", "text": "handled error"},)949950    def handling(e: ToolException) -> tuple[dict[str, Any], ...]:951        return content952953    tool_ = _FakeExceptionTool(handle_tool_error=handling)954    actual = await tool_.ainvoke(955        {"type": "tool_call", "args": {}, "name": "exception", "id": "call_1"}956    )957958    assert isinstance(actual, ToolMessage)959    assert actual.content == list(content)960    assert actual.status == "error"961    assert actual.tool_call_id == "call_1"962963964async def test_async_exception_handling_non_tool_exception() -> None:965    tool_ = _FakeExceptionTool(exception=ValueError("some error"))966    with pytest.raises(ValueError, match="some error"):967        await tool_.arun({})968969970def test_structured_tool_from_function() -> None:971    """Test that structured tools can be created from functions."""972973    def foo(bar: int, baz: str) -> str:974        """Docstring thing.975976        Args:977            bar: the bar value978            baz: the baz value979        """980        raise NotImplementedError981982    structured_tool = StructuredTool.from_function(foo)983    assert structured_tool.name == "foo"984    assert structured_tool.args == {985        "bar": {"title": "Bar", "type": "integer"},986        "baz": {"title": "Baz", "type": "string"},987    }988989    assert _schema(structured_tool.args_schema) == {990        "title": "foo",991        "type": "object",992        "description": inspect.getdoc(foo),993        "properties": {994            "bar": {"title": "Bar", "type": "integer"},995            "baz": {"title": "Baz", "type": "string"},996        },997        "required": ["bar", "baz"],998    }9991000    assert foo.__doc__ is not None1001    assert structured_tool.description == textwrap.dedent(foo.__doc__.strip())100210031004def test_validation_error_handling_bool() -> None:1005    """Test that validation errors are handled correctly."""1006    expected = "Tool input validation error"1007    tool_ = _MockStructuredTool(handle_validation_error=True)1008    actual = tool_.run({})1009    assert expected == actual101010111012def test_validation_error_handling_str() -> None:1013    """Test that validation errors are handled correctly."""1014    expected = "foo bar"1015    tool_ = _MockStructuredTool(handle_validation_error=expected)1016    actual = tool_.run({})1017    assert expected == actual101810191020def test_validation_error_handling_callable() -> None:1021    """Test that validation errors are handled correctly."""1022    expected = "foo bar"10231024    def handling(e: ValidationError | ValidationErrorV1) -> str:1025        return expected10261027    tool_ = _MockStructuredTool(handle_validation_error=handling)1028    actual = tool_.run({})1029    assert expected == actual103010311032@pytest.mark.parametrize(1033    "handler",1034    [1035        True,1036        "foo bar",1037        lambda _: "foo bar",1038    ],1039)1040def test_validation_error_handling_non_validation_error(1041    *,1042    handler: bool | str | Callable[[ValidationError | ValidationErrorV1], str],1043) -> None:1044    """Test that validation errors are handled correctly."""10451046    class _RaiseNonValidationErrorTool(BaseTool):1047        name: str = "raise_non_validation_error_tool"1048        description: str = "A tool that raises a non-validation error"10491050        def _parse_input(1051            self,1052            tool_input: str | dict[str, Any],1053            tool_call_id: str | None,1054        ) -> str | dict[str, Any]:1055            raise NotImplementedError10561057        @override1058        def _run(self) -> str:1059            return "dummy"10601061        @override1062        async def _arun(self) -> str:1063            return "dummy"10641065    tool_ = _RaiseNonValidationErrorTool(handle_validation_error=handler)1066    with pytest.raises(NotImplementedError):1067        tool_.run({})106810691070async def test_async_validation_error_handling_bool() -> None:1071    """Test that validation errors are handled correctly."""1072    expected = "Tool input validation error"1073    tool_ = _MockStructuredTool(handle_validation_error=True)1074    actual = await tool_.arun({})1075    assert expected == actual107610771078async def test_async_validation_error_handling_str() -> None:1079    """Test that validation errors are handled correctly."""1080    expected = "foo bar"1081    tool_ = _MockStructuredTool(handle_validation_error=expected)1082    actual = await tool_.arun({})1083    assert expected == actual108410851086async def test_async_validation_error_handling_callable() -> None:1087    """Test that validation errors are handled correctly."""1088    expected = "foo bar"10891090    def handling(e: ValidationError | ValidationErrorV1) -> str:1091        return expected10921093    tool_ = _MockStructuredTool(handle_validation_error=handling)1094    actual = await tool_.arun({})1095    assert expected == actual109610971098@pytest.mark.parametrize(1099    "handler",1100    [1101        True,1102        "foo bar",1103        lambda _: "foo bar",1104    ],1105)1106async def test_async_validation_error_handling_non_validation_error(1107    *,1108    handler: bool | str | Callable[[ValidationError | ValidationErrorV1], str],1109) -> None:1110    """Test that validation errors are handled correctly."""11111112    class _RaiseNonValidationErrorTool(BaseTool):1113        name: str = "raise_non_validation_error_tool"1114        description: str = "A tool that raises a non-validation error"11151116        def _parse_input(1117            self,1118            tool_input: str | dict[str, Any],1119            tool_call_id: str | None,1120        ) -> str | dict[str, Any]:1121            raise NotImplementedError11221123        @override1124        def _run(self) -> str:1125            return "dummy"11261127        @override1128        async def _arun(self) -> str:1129            return "dummy"11301131    tool_ = _RaiseNonValidationErrorTool(handle_validation_error=handler)1132    with pytest.raises(NotImplementedError):1133        await tool_.arun({})113411351136def test_optional_subset_model_rewrite() -> None:1137    class MyModel(BaseModel):1138        a: str | None = None1139        b: str1140        c: list[str | None] | None = None11411142    model2 = _create_subset_model("model2", MyModel, ["a", "b", "c"])11431144    assert set(_schema(model2)["required"]) == {"b"}114511461147@pytest.mark.parametrize(1148    ("inputs", "expected"),1149    [1150        # Check not required1151        ({"bar": "bar"}, {"bar": "bar", "baz": 3, "buzz": "buzz"}),1152        # Check overwritten1153        (1154            {"bar": "bar", "baz": 4, "buzz": "not-buzz"},1155            {"bar": "bar", "baz": 4, "buzz": "not-buzz"},1156        ),1157        # Check validation error when missing1158        ({}, None),1159        # Check validation error when wrong type1160        ({"bar": "bar", "baz": "not-an-int"}, None),1161        # Check OK when None explicitly passed1162        ({"bar": "bar", "baz": None}, {"bar": "bar", "baz": None, "buzz": "buzz"}),1163    ],1164)1165def test_tool_invoke_optional_args(1166    inputs: dict[str, Any], expected: dict[str, Any] | None1167) -> None:1168    @tool1169    def foo(bar: str, baz: int | None = 3, buzz: str | None = "buzz") -> dict[str, Any]:1170        """The foo."""1171        return {1172            "bar": bar,1173            "baz": baz,1174            "buzz": buzz,1175        }11761177    if expected is not None:1178        assert foo.invoke(inputs) == expected1179    else:1180        with pytest.raises(ValidationError):1181            foo.invoke(inputs)118211831184def test_tool_pass_context() -> None:1185    @tool1186    def foo(bar: str) -> str:1187        """The foo."""1188        config = ensure_config()1189        assert config["configurable"]["foo"] == "not-bar"1190        assert bar == "baz"1191        return bar11921193    assert foo.invoke({"bar": "baz"}, {"configurable": {"foo": "not-bar"}}) == "baz"119411951196@pytest.mark.skipif(1197    sys.version_info < (3, 11),1198    reason="requires python3.11 or higher",1199)1200async def test_async_tool_pass_context() -> None:1201    @tool1202    async def foo(bar: str) -> str:1203        """The foo."""1204        config = ensure_config()1205        assert config["configurable"]["foo"] == "not-bar"1206        assert bar == "baz"1207        return bar12081209    assert (1210        await foo.ainvoke({"bar": "baz"}, {"configurable": {"foo": "not-bar"}}) == "baz"1211    )121212131214def assert_bar(bar: Any, bar_config: RunnableConfig) -> Any:1215    assert bar_config["configurable"]["foo"] == "not-bar"1216    assert bar == "baz"1217    return bar121812191220@tool1221def foo(bar: Any, bar_config: RunnableConfig) -> Any:1222    """The foo."""1223    return assert_bar(bar, bar_config)122412251226@tool1227async def afoo(bar: Any, bar_config: RunnableConfig) -> Any:1228    """The foo."""1229    return assert_bar(bar, bar_config)123012311232@tool(infer_schema=False)1233def simple_foo(bar: Any, bar_config: RunnableConfig) -> Any:1234    """The foo."""1235    return assert_bar(bar, bar_config)123612371238@tool(infer_schema=False)1239async def asimple_foo(bar: Any, bar_config: RunnableConfig) -> Any:1240    """The foo."""1241    return assert_bar(bar, bar_config)124212431244class FooBase(BaseTool):1245    name: str = "Foo"1246    description: str = "Foo"12471248    @override1249    def _run(self, bar: Any, bar_config: RunnableConfig, **kwargs: Any) -> Any:1250        return assert_bar(bar, bar_config)125112521253class AFooBase(FooBase):1254    @override1255    async def _arun(self, bar: Any, bar_config: RunnableConfig, **kwargs: Any) -> Any:1256        return assert_bar(bar, bar_config)125712581259@pytest.mark.parametrize("tool", [foo, simple_foo, FooBase(), AFooBase()])1260def test_tool_pass_config(tool: BaseTool) -> None:1261    assert tool.invoke({"bar": "baz"}, {"configurable": {"foo": "not-bar"}}) == "baz"12621263    # Test we don't mutate tool calls1264    tool_call = {1265        "name": tool.name,1266        "args": {"bar": "baz"},1267        "id": "abc123",1268        "type": "tool_call",1269    }1270    _ = tool.invoke(tool_call, {"configurable": {"foo": "not-bar"}})1271    assert tool_call["args"] == {"bar": "baz"}127212731274class FooBaseNonPickleable(FooBase):1275    @override1276    def _run(self, bar: Any, bar_config: RunnableConfig, **kwargs: Any) -> Any:1277        return True127812791280def test_tool_pass_config_non_pickleable() -> None:1281    tool = FooBaseNonPickleable()12821283    args = {"bar": threading.Lock()}1284    tool_call = {1285        "name": tool.name,1286        "args": args,1287        "id": "abc123",1288        "type": "tool_call",1289    }1290    _ = tool.invoke(tool_call, {"configurable": {"foo": "not-bar"}})1291    assert tool_call["args"] == args129212931294@pytest.mark.parametrize(1295    "tool", [foo, afoo, simple_foo, asimple_foo, FooBase(), AFooBase()]1296)1297async def test_async_tool_pass_config(tool: BaseTool) -> None:1298    assert (1299        await tool.ainvoke({"bar": "baz"}, {"configurable": {"foo": "not-bar"}})1300        == "baz"1301    )130213031304def test_tool_description() -> None:1305    def foo(bar: str) -> str:1306        """The foo."""1307        return bar13081309    foo1 = tool(foo)1310    assert foo1.description == "The foo."13111312    foo2 = StructuredTool.from_function(foo)1313    assert foo2.description == "The foo."131413151316def test_tool_arg_descriptions() -> None:1317    def foo(bar: str, baz: int) -> str:1318        """The foo.13191320        Args:1321            bar: The bar.1322            baz: The baz.1323        """1324        return bar13251326    foo1 = tool(foo)1327    args_schema = _schema(foo1.args_schema)1328    assert args_schema == {1329        "title": "foo",1330        "type": "object",1331        "description": inspect.getdoc(foo),1332        "properties": {1333            "bar": {"title": "Bar", "type": "string"},1334            "baz": {"title": "Baz", "type": "integer"},1335        },1336        "required": ["bar", "baz"],1337    }13381339    # Test parses docstring1340    foo2 = tool(foo, parse_docstring=True)1341    args_schema = _schema(foo2.args_schema)1342    expected = {1343        "title": "foo",1344        "description": "The foo.",1345        "type": "object",1346        "properties": {1347            "bar": {"title": "Bar", "description": "The bar.", "type": "string"},1348            "baz": {"title": "Baz", "description": "The baz.", "type": "integer"},1349        },1350        "required": ["bar", "baz"],1351    }1352    assert args_schema == expected13531354    # Test parsing with run_manager does not raise error1355    def foo3(  # noqa: D4171356        bar: str, baz: int, run_manager: CallbackManagerForToolRun | None = None1357    ) -> str:1358        """The foo.13591360        Args:1361            bar: The bar.1362            baz: The baz.1363        """1364        return bar13651366    as_tool = tool(foo3, parse_docstring=True)1367    args_schema = _schema(as_tool.args_schema)1368    assert args_schema["description"] == expected["description"]1369    assert args_schema["properties"] == expected["properties"]13701371    # Test parsing with runtime does not raise error1372    def foo3_runtime(bar: str, baz: int, runtime: Any) -> str:  # noqa: D4171373        """The foo.13741375        Args:1376            bar: The bar.1377            baz: The baz.1378        """1379        return bar13801381    _ = tool(foo3_runtime, parse_docstring=True)13821383    # Test parameterless tool does not raise error for missing Args section1384    # in docstring.1385    def foo4() -> str:1386        """The foo."""1387        return "bar"13881389    as_tool = tool(foo4, parse_docstring=True)1390    args_schema = _schema(as_tool.args_schema)1391    assert args_schema["description"] == expected["description"]13921393    def foo5(run_manager: CallbackManagerForToolRun | None = None) -> str:1394        """The foo."""1395        return "bar"13961397    as_tool = tool(foo5, parse_docstring=True)1398    args_schema = _schema(as_tool.args_schema)1399    assert args_schema["description"] == expected["description"]140014011402def test_docstring_parsing() -> None:1403    expected = {1404        "title": "foo",1405        "description": "The foo.",1406        "type": "object",1407        "properties": {1408            "bar": {"title": "Bar", "description": "The bar.", "type": "string"},1409            "baz": {"title": "Baz", "description": "The baz.", "type": "integer"},1410        },1411        "required": ["bar", "baz"],1412    }14131414    # Simple case1415    def foo(bar: str, baz: int) -> str:1416        """The foo.14171418        Args:1419            bar: The bar.1420            baz: The baz.1421        """1422        return bar14231424    as_tool = tool(foo, parse_docstring=True)1425    args_schema = _schema(as_tool.args_schema)1426    assert args_schema["description"] == "The foo."1427    assert args_schema["properties"] == expected["properties"]14281429    # Multi-line description1430    def foo2(bar: str, baz: int) -> str:1431        """The foo.14321433        Additional description here.14341435        Args:1436            bar: The bar.1437            baz: The baz.1438        """1439        return bar14401441    as_tool = tool(foo2, parse_docstring=True)1442    args_schema2 = _schema(as_tool.args_schema)1443    assert args_schema2["description"] == "The foo. Additional description here."1444    assert args_schema2["properties"] == expected["properties"]14451446    # Multi-line with Returns block1447    def foo3(bar: str, baz: int) -> str:1448        """The foo.14491450        Additional description here.14511452        Args:1453            bar: The bar.1454            baz: The baz.14551456        Returns:1457            description of returned value.1458        """1459        return bar14601461    as_tool = tool(foo3, parse_docstring=True)1462    args_schema3 = _schema(as_tool.args_schema)1463    args_schema3["title"] = "foo2"1464    assert args_schema2 == args_schema314651466    # Single argument1467    def foo4(bar: str) -> str:1468        """The foo.14691470        Args:1471            bar: The bar.1472        """1473        return bar14741475    as_tool = tool(foo4, parse_docstring=True)1476    args_schema4 = _schema(as_tool.args_schema)1477    assert args_schema4["description"] == "The foo."1478    assert args_schema4["properties"] == {1479        "bar": {"description": "The bar.", "title": "Bar", "type": "string"}1480    }148114821483def test_tool_invalid_docstrings() -> None:1484    """Test invalid docstrings."""14851486    def foo3(bar: str, baz: int) -> str:1487        """The foo."""1488        return bar14891490    def foo4(bar: str, baz: int) -> str:1491        """The foo.1492        Args:1493            bar: The bar.1494            baz: The baz.1495        """  # noqa: D205,D411  # We're intentionally testing bad formatting.1496        return bar14971498    for func in {foo3, foo4}:1499        with pytest.raises(ValueError, match="Found invalid Google-Style docstring"):1500            _ = tool(func, parse_docstring=True)15011502    def foo5(bar: str, baz: int) -> str:  # noqa: D4171503        """The foo.15041505        Args:1506            banana: The bar.1507            monkey: The baz.1508        """1509        return bar15101511    with pytest.raises(1512        ValueError, match="Arg banana in docstring not found in function signature"1513    ):1514        _ = tool(foo5, parse_docstring=True)151515161517def test_tool_annotated_descriptions() -> None:1518    def foo(1519        bar: Annotated[str, "this is the bar"], baz: Annotated[int, "this is the baz"]1520    ) -> str:1521        """The foo.15221523        Returns:1524            The bar only.1525        """1526        return bar15271528    foo1 = tool(foo)1529    args_schema = _schema(foo1.args_schema)1530    assert args_schema == {1531        "title": "foo",1532        "type": "object",1533        "description": inspect.getdoc(foo),1534        "properties": {1535            "bar": {"title": "Bar", "type": "string", "description": "this is the bar"},1536            "baz": {1537                "title": "Baz",1538                "type": "integer",1539                "description": "this is the baz",1540            },1541        },1542        "required": ["bar", "baz"],1543    }154415451546def test_tool_field_description_preserved() -> None:1547    """Test that `Field(description=...)` is preserved in `@tool` decorator."""15481549    @tool1550    def my_tool(1551        topic: Annotated[str, Field(description="The research topic")],1552        depth: Annotated[int, Field(description="Search depth level")] = 3,1553    ) -> str:1554        """A tool for research."""1555        return f"{topic} at depth {depth}"15561557    args_schema = _schema(my_tool.args_schema)1558    assert args_schema == {1559        "title": "my_tool",1560        "type": "object",1561        "description": "A tool for research.",1562        "properties": {1563            "topic": {1564                "title": "Topic",1565                "type": "string",1566                "description": "The research topic",1567            },1568            "depth": {1569                "title": "Depth",1570                "type": "integer",1571                "description": "Search depth level",1572                "default": 3,1573            },1574        },1575        "required": ["topic"],1576    }157715781579def test_tool_call_input_tool_message_output() -> None:1580    tool_call = {1581        "name": "structured_api",1582        "args": {"arg1": 1, "arg2": True, "arg3": {"img": "base64string..."}},1583        "id": "123",1584        "type": "tool_call",1585    }1586    tool = _MockStructuredTool()1587    expected = ToolMessage(1588        "1 True {'img': 'base64string...'}", tool_call_id="123", name="structured_api"1589    )1590    actual = tool.invoke(tool_call)1591    assert actual == expected15921593    tool_call.pop("type")1594    with pytest.raises(ValidationError):1595        tool.invoke(tool_call)159615971598@pytest.mark.parametrize("block_type", [*TOOL_MESSAGE_BLOCK_TYPES, "bad"])1599def test_tool_content_block_output(block_type: str) -> None:1600    @tool1601    def my_tool(query: str) -> list[dict[str, Any]]:1602        """Test tool."""1603        return [{"type": block_type, "foo": "bar"}]16041605    tool_call = {1606        "type": "tool_call",1607        "name": "my_tool",1608        "args": {"query": "baz"},1609        "id": "call_abc123",1610    }16111612    result = my_tool.invoke(tool_call)1613    assert isinstance(result, ToolMessage)16141615    if block_type in TOOL_MESSAGE_BLOCK_TYPES:1616        assert result.content == [{"type": block_type, "foo": "bar"}]1617    else:1618        assert result.content == '[{"type": "bad", "foo": "bar"}]'161916201621class _MockStructuredToolWithRawOutput(BaseTool):1622    name: str = "structured_api"1623    args_schema: type[BaseModel] = _MockSchema1624    description: str = "A Structured Tool"1625    response_format: Literal["content_and_artifact"] = "content_and_artifact"16261627    @override1628    def _run(1629        self,1630        arg1: int,1631        arg2: bool,1632        arg3: dict[str, Any] | None = None,1633    ) -> tuple[str, dict[str, Any]]:1634        return f"{arg1} {arg2}", {"arg1": arg1, "arg2": arg2, "arg3": arg3}163516361637@tool("structured_api", response_format="content_and_artifact")1638def _mock_structured_tool_with_artifact(1639    *, arg1: int, arg2: bool, arg3: dict[str, str] | None = None1640) -> tuple[str, dict[str, Any]]:1641    """A Structured Tool."""1642    return f"{arg1} {arg2}", {"arg1": arg1, "arg2": arg2, "arg3": arg3}164316441645@pytest.mark.parametrize(1646    "tool", [_MockStructuredToolWithRawOutput(), _mock_structured_tool_with_artifact]1647)1648def test_tool_call_input_tool_message_with_artifact(tool: BaseTool) -> None:1649    tool_call: dict[str, Any] = {1650        "name": "structured_api",1651        "args": {"arg1": 1, "arg2": True, "arg3": {"img": "base64string..."}},1652        "id": "123",1653        "type": "tool_call",1654    }1655    expected = ToolMessage(1656        "1 True", artifact=tool_call["args"], tool_call_id="123", name="structured_api"1657    )1658    actual = tool.invoke(tool_call)1659    assert actual == expected16601661    tool_call.pop("type")1662    with pytest.raises(ValidationError):1663        tool.invoke(tool_call)16641665    actual_content = tool.invoke(tool_call["args"])1666    assert actual_content == expected.content166716681669def test_convert_from_runnable_dict() -> None:1670    # Test with typed dict input1671    class Args(TypedDict):1672        a: int1673        b: list[int]16741675    def f(x: Args) -> str:1676        return str(x["a"] * max(x["b"]))16771678    runnable = RunnableLambda(f)1679    as_tool = runnable.as_tool()1680    args_schema = as_tool.args_schema1681    assert args_schema is not None1682    assert _schema(args_schema) == {1683        "title": "f",1684        "type": "object",1685        "properties": {1686            "a": {"title": "A", "type": "integer"},1687            "b": {"title": "B", "type": "array", "items": {"type": "integer"}},1688        },1689        "required": ["a", "b"],1690    }1691    assert as_tool.description1692    result = as_tool.invoke({"a": 3, "b": [1, 2]})1693    assert result == "6"16941695    as_tool = runnable.as_tool(name="my tool", description="test description")1696    assert as_tool.name == "my tool"1697    assert as_tool.description == "test description"16981699    # Dict without typed input-- must supply schema1700    def g(x: dict[str, Any]) -> str:1701        return str(x["a"] * max(x["b"]))17021703    # Specify via args_schema:1704    class GSchema(BaseModel):1705        """Apply a function to an integer and list of integers."""17061707        a: int = Field(..., description="Integer")1708        b: list[int] = Field(..., description="List of ints")17091710    runnable2 = RunnableLambda(g)1711    as_tool2 = runnable2.as_tool(GSchema)1712    as_tool2.invoke({"a": 3, "b": [1, 2]})17131714    # Specify via arg_types:1715    runnable3 = RunnableLambda(g)1716    as_tool3 = runnable3.as_tool(arg_types={"a": int, "b": list[int]})1717    result = as_tool3.invoke({"a": 3, "b": [1, 2]})1718    assert result == "6"17191720    # Test with config1721    def h(x: dict[str, Any]) -> str:1722        config = ensure_config()1723        assert config["configurable"]["foo"] == "not-bar"1724        return str(x["a"] * max(x["b"]))17251726    runnable4 = RunnableLambda(h)1727    as_tool4 = runnable4.as_tool(arg_types={"a": int, "b": list[int]})1728    result = as_tool4.invoke(1729        {"a": 3, "b": [1, 2]}, config={"configurable": {"foo": "not-bar"}}1730    )1731    assert result == "6"173217331734def test_convert_from_runnable_other() -> None:1735    # String input1736    def f(x: str) -> str:1737        return x + "a"17381739    def g(x: str) -> str:1740        return x + "z"17411742    runnable = RunnableLambda(f) | g1743    as_tool = runnable.as_tool()1744    args_schema = as_tool.args_schema1745    assert args_schema is None1746    assert as_tool.description17471748    result = as_tool.invoke("b")1749    assert result == "baz"17501751    # Test with config1752    def h(x: str) -> str:1753        config = ensure_config()1754        assert config["configurable"]["foo"] == "not-bar"1755        return x + "a"17561757    runnable2 = RunnableLambda(h)1758    as_tool2 = runnable2.as_tool()1759    result2 = as_tool2.invoke("b", config={"configurable": {"foo": "not-bar"}})1760    assert result2 == "ba"176117621763@tool("foo", parse_docstring=True)1764def injected_tool(x: int, y: Annotated[str, InjectedToolArg]) -> str:1765    """Foo.17661767    Args:1768        x: abc1769        y: 1231770    """1771    return y177217731774class InjectedTool(BaseTool):1775    name: str = "foo"1776    description: str = "foo."17771778    @override1779    def _run(self, x: int, y: Annotated[str, InjectedToolArg]) -> Any:1780        """Foo.17811782        Args:1783            x: abc1784            y: 1231785        """1786        return y178717881789class fooSchema(BaseModel):  # noqa: N8011790    """foo."""17911792    x: int = Field(..., description="abc")1793    y: Annotated[str, "foobar comment", InjectedToolArg()] = Field(1794        ..., description="123"1795    )179617971798class InjectedToolWithSchema(BaseTool):1799    name: str = "foo"1800    description: str = "foo."1801    args_schema: type[BaseModel] = fooSchema18021803    @override1804    def _run(self, x: int, y: str) -> Any:1805        return y180618071808@tool("foo", args_schema=fooSchema)1809def injected_tool_with_schema(x: int, y: str) -> str:1810    return y181118121813@pytest.mark.parametrize("tool_", [InjectedTool()])1814def test_tool_injected_arg_without_schema(tool_: BaseTool) -> None:1815    assert _schema(tool_.get_input_schema()) == {1816        "title": "foo",1817        "description": "Foo.\n\nArgs:\n    x: abc\n    y: 123",1818        "type": "object",1819        "properties": {1820            "x": {"title": "X", "type": "integer"},1821            "y": {"title": "Y", "type": "string"},1822        },1823        "required": ["x", "y"],1824    }1825    assert _schema(tool_.tool_call_schema) == {1826        "title": "foo",1827        "description": "foo.",1828        "type": "object",1829        "properties": {"x": {"title": "X", "type": "integer"}},1830        "required": ["x"],1831    }1832    assert tool_.invoke({"x": 5, "y": "bar"}) == "bar"1833    assert tool_.invoke(1834        {1835            "name": "foo",1836            "args": {"x": 5, "y": "bar"},1837            "id": "123",1838            "type": "tool_call",1839        }1840    ) == ToolMessage("bar", tool_call_id="123", name="foo")1841    expected_error = (1842        ValidationError if not isinstance(tool_, InjectedTool) else TypeError1843    )1844    with pytest.raises(expected_error):1845        tool_.invoke({"x": 5})18461847    assert convert_to_openai_function(tool_) == {1848        "name": "foo",1849        "description": "foo.",1850        "parameters": {1851            "type": "object",1852            "properties": {"x": {"type": "integer"}},1853            "required": ["x"],1854        },1855    }185618571858@pytest.mark.parametrize(1859    "tool_",1860    [injected_tool_with_schema, InjectedToolWithSchema()],1861)1862def test_tool_injected_arg_with_schema(tool_: BaseTool) -> None:1863    assert _schema(tool_.get_input_schema()) == {1864        "title": "fooSchema",1865        "description": "foo.",1866        "type": "object",1867        "properties": {1868            "x": {"description": "abc", "title": "X", "type": "integer"},1869            "y": {"description": "123", "title": "Y", "type": "string"},1870        },1871        "required": ["x", "y"],1872    }1873    assert _schema(tool_.tool_call_schema) == {1874        "title": "foo",1875        "description": "foo.",1876        "type": "object",1877        "properties": {"x": {"description": "abc", "title": "X", "type": "integer"}},1878        "required": ["x"],1879    }1880    assert tool_.invoke({"x": 5, "y": "bar"}) == "bar"1881    assert tool_.invoke(1882        {1883            "name": "foo",1884            "args": {"x": 5, "y": "bar"},1885            "id": "123",1886            "type": "tool_call",1887        }1888    ) == ToolMessage("bar", tool_call_id="123", name="foo")1889    expected_error = (1890        ValidationError if not isinstance(tool_, InjectedTool) else TypeError1891    )1892    with pytest.raises(expected_error):1893        tool_.invoke({"x": 5})18941895    assert convert_to_openai_function(tool_) == {1896        "name": "foo",1897        "description": "foo.",1898        "parameters": {1899            "type": "object",1900            "properties": {"x": {"type": "integer", "description": "abc"}},1901            "required": ["x"],1902        },1903    }190419051906def test_tool_injected_arg() -> None:1907    tool_ = injected_tool1908    assert _schema(tool_.get_input_schema()) == {1909        "title": "foo",1910        "description": "Foo.",1911        "type": "object",1912        "properties": {1913            "x": {"description": "abc", "title": "X", "type": "integer"},1914            "y": {"description": "123", "title": "Y", "type": "string"},1915        },1916        "required": ["x", "y"],1917    }1918    assert _schema(tool_.tool_call_schema) == {1919        "title": "foo",1920        "description": "Foo.",1921        "type": "object",1922        "properties": {"x": {"description": "abc", "title": "X", "type": "integer"}},1923        "required": ["x"],1924    }1925    assert tool_.invoke({"x": 5, "y": "bar"}) == "bar"1926    assert tool_.invoke(1927        {1928            "name": "foo",1929            "args": {"x": 5, "y": "bar"},1930            "id": "123",1931            "type": "tool_call",1932        }1933    ) == ToolMessage("bar", tool_call_id="123", name="foo")1934    expected_error = (1935        ValidationError if not isinstance(tool_, InjectedTool) else TypeError1936    )1937    with pytest.raises(expected_error):1938        tool_.invoke({"x": 5})19391940    assert convert_to_openai_function(tool_) == {1941        "name": "foo",1942        "description": "Foo.",1943        "parameters": {1944            "type": "object",1945            "properties": {"x": {"type": "integer", "description": "abc"}},1946            "required": ["x"],1947        },1948    }194919501951def test_tool_inherited_injected_arg() -> None:1952    class BarSchema(BaseModel):1953        """bar."""19541955        y: Annotated[str, "foobar comment", InjectedToolArg()] = Field(1956            ..., description="123"1957        )19581959    class FooSchema(BarSchema):1960        """foo."""19611962        x: int = Field(..., description="abc")19631964    class InheritedInjectedArgTool(BaseTool):1965        name: str = "foo"1966        description: str = "foo."1967        args_schema: type[BaseModel] = FooSchema19681969        @override1970        def _run(self, x: int, y: str) -> Any:1971            return y19721973    tool_ = InheritedInjectedArgTool()1974    assert tool_.get_input_jsonschema() == {1975        "title": "FooSchema",  # Matches the title from the provided schema1976        "description": "foo.",1977        "type": "object",1978        "properties": {1979            "x": {"description": "abc", "title": "X", "type": "integer"},1980            "y": {"description": "123", "title": "Y", "type": "string"},1981        },1982        "required": ["y", "x"],1983    }1984    # Should not include `y` since it's annotated as an injected tool arg1985    assert _get_tool_call_json_schema(tool_) == {1986        "title": "foo",1987        "description": "foo.",1988        "type": "object",1989        "properties": {"x": {"description": "abc", "title": "X", "type": "integer"}},1990        "required": ["x"],1991    }1992    assert tool_.invoke({"x": 5, "y": "bar"}) == "bar"1993    assert tool_.invoke(1994        {1995            "name": "foo",1996            "args": {"x": 5, "y": "bar"},1997            "id": "123",1998            "type": "tool_call",1999        }2000    ) == ToolMessage("bar", tool_call_id="123", name="foo")

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.