libs/core/tests/unit_tests/test_tools.py PYTHON 4,066 lines View on github.com → Search inside
File is large — showing lines 1–2,000 of 4,066.
1"""Test the base tool implementation."""23import inspect4import json5import logging6import pickle7import sys8import textwrap9import threading10from collections.abc import Callable11from dataclasses import dataclass12from datetime import datetime13from enum import Enum14from functools import partial15from typing import (16    Annotated,17    Any,18    Generic,19    Literal,20    TypeVar,21    cast,22    get_type_hints,23)2425import pytest26from pydantic import BaseModel, ConfigDict, Field, ValidationError27from pydantic.v1 import BaseModel as BaseModelV128from pydantic.v1 import ValidationError as ValidationErrorV129from typing_extensions import TypedDict, override3031from langchain_core import tools32from langchain_core.callbacks import (33    AsyncCallbackManagerForToolRun,34    CallbackManagerForToolRun,35)36from langchain_core.callbacks.manager import (37    CallbackManagerForRetrieverRun,38)39from langchain_core.documents import Document40from langchain_core.messages import ToolCall, ToolMessage41from langchain_core.messages.tool import ToolOutputMixin42from langchain_core.retrievers import BaseRetriever43from langchain_core.runnables import (44    Runnable,45    RunnableConfig,46    RunnableLambda,47    ensure_config,48)49from langchain_core.tools import (50    BaseTool,51    StructuredTool,52    Tool,53    ToolException,54    convert_runnable_to_tool,55    tool,56)57from langchain_core.tools.base import (58    TOOL_MESSAGE_BLOCK_TYPES,59    ArgsSchema,60    InjectedToolArg,61    InjectedToolCallId,62    SchemaAnnotationError,63    _DirectlyInjectedToolArg,64    _format_output,65    _is_message_content_block,66    _normalize_message_content,67    get_all_basemodel_annotations,68)69from langchain_core.utils.function_calling import (70    convert_to_openai_function,71    convert_to_openai_tool,72)73from langchain_core.utils.pydantic import (74    TypeBaseModel,75    _create_subset_model,76    create_model_v2,77    model_json_schema,78)79from tests.unit_tests.fake.callbacks import FakeCallbackHandler80from tests.unit_tests.pydantic_utils import (81    _normalize_schema,82    _schema,83    skip_if_no_pydantic_v1,84)8586try:87    from langgraph.prebuilt import ToolRuntime  # type: ignore[import-not-found]8889    HAS_LANGGRAPH = True90except ImportError:91    HAS_LANGGRAPH = False929394def _get_tool_call_json_schema(tool: BaseTool) -> dict[str, Any]:95    tool_schema = tool.tool_call_schema96    if isinstance(tool_schema, dict):97        return tool_schema9899    if issubclass(tool_schema, BaseModel):100        return tool_schema.model_json_schema()101    if issubclass(tool_schema, BaseModelV1):102        return tool_schema.schema()103    return {}  # type: ignore[unreachable]104105106def test_unnamed_decorator() -> None:107    """Test functionality with unnamed decorator."""108109    @tool110    def search_api(query: str) -> str:111        """Search the API for the query."""112        return "API result"113114    assert isinstance(search_api, BaseTool)115    assert search_api.name == "search_api"116    assert not search_api.return_direct117    assert search_api.invoke("test") == "API result"118119120class _MockSchema(BaseModel):121    """Return the arguments directly."""122123    arg1: int124    arg2: bool125    arg3: dict[str, Any] | None = None126127128class _MockStructuredTool(BaseTool):129    name: str = "structured_api"130    args_schema: type[BaseModel] = _MockSchema131    description: str = "A Structured Tool"132133    @override134    def _run(self, *, arg1: int, arg2: bool, arg3: dict[str, Any] | None = None) -> str:135        return f"{arg1} {arg2} {arg3}"136137    async def _arun(138        self, *, arg1: int, arg2: bool, arg3: dict[str, Any] | None = None139    ) -> str:140        raise NotImplementedError141142143class _FakeOutput(ToolOutputMixin):144    """Minimal ToolOutputMixin subclass used only in tests."""145146    def __init__(self, value: int) -> None:147        self.value = value148149    def __eq__(self, other: object) -> bool:150        return isinstance(other, _FakeOutput) and self.value == other.value151152    def __hash__(self) -> int:153        return hash(self.value)154155    def __repr__(self) -> str:156        return f"_FakeOutput({self.value})"157158159def test_structured_args() -> None:160    """Test functionality with structured arguments."""161    structured_api = _MockStructuredTool()162    assert isinstance(structured_api, BaseTool)163    assert structured_api.name == "structured_api"164    expected_result = "1 True {'foo': 'bar'}"165    args = {"arg1": 1, "arg2": True, "arg3": {"foo": "bar"}}166    assert structured_api.run(args) == expected_result167168169def test_misannotated_base_tool_raises_error() -> None:170    """Test that a BaseTool with the incorrect typehint raises an exception."""171    with pytest.raises(SchemaAnnotationError):172173        class _MisAnnotatedTool(BaseTool):174            name: str = "structured_api"175            # This would silently be ignored without the custom metaclass176            args_schema: BaseModel = _MockSchema  # type: ignore[assignment]177            description: str = "A Structured Tool"178179            @override180            def _run(181                self, *, arg1: int, arg2: bool, arg3: dict[str, Any] | None = None182            ) -> str:183                return f"{arg1} {arg2} {arg3}"184185            async def _arun(186                self, *, arg1: int, arg2: bool, arg3: dict[str, Any] | None = None187            ) -> str:188                raise NotImplementedError189190191def test_forward_ref_annotated_base_tool_accepted() -> None:192    """Test that a using forward ref annotation syntax is accepted."""193194    class _ForwardRefAnnotatedTool(BaseTool):195        name: str = "structured_api"196        args_schema: "type[BaseModel]" = _MockSchema197        description: str = "A Structured Tool"198199        @override200        def _run(201            self, *, arg1: int, arg2: bool, arg3: dict[str, Any] | None = None202        ) -> str:203            return f"{arg1} {arg2} {arg3}"204205        async def _arun(206            self, *, arg1: int, arg2: bool, arg3: dict[str, Any] | None = None207        ) -> str:208            raise NotImplementedError209210211def test_subclass_annotated_base_tool_accepted() -> None:212    """Test BaseTool child w/ custom schema isn't overwritten."""213214    class _ForwardRefAnnotatedTool(BaseTool):215        name: str = "structured_api"216        args_schema: type[_MockSchema] = _MockSchema217        description: str = "A Structured Tool"218219        @override220        def _run(221            self, *, arg1: int, arg2: bool, arg3: dict[str, Any] | None = None222        ) -> str:223            return f"{arg1} {arg2} {arg3}"224225        async def _arun(226            self, *, arg1: int, arg2: bool, arg3: dict[str, Any] | None = None227        ) -> str:228            raise NotImplementedError229230    assert issubclass(_ForwardRefAnnotatedTool, BaseTool)231    tool = _ForwardRefAnnotatedTool()232    assert tool.args_schema == _MockSchema233234235def test_decorator_with_specified_schema() -> None:236    """Test that manually specified schemata are passed through to the tool."""237238    @tool(args_schema=_MockSchema)239    def tool_func(*, arg1: int, arg2: bool, arg3: dict[str, Any] | None = None) -> str:240        return f"{arg1} {arg2} {arg3}"241242    assert isinstance(tool_func, BaseTool)243    assert tool_func.args_schema == _MockSchema244245246@pytest.mark.skipif(247    sys.version_info >= (3, 14),248    reason="pydantic.v1 namespace not supported with Python 3.14+",249)250def test_decorator_with_specified_schema_pydantic_v1() -> None:251    """Test that manually specified schemata are passed through to the tool."""252253    class _MockSchemaV1(BaseModelV1):254        """Return the arguments directly."""255256        arg1: int257        arg2: bool258        arg3: dict[str, Any] | None = None259260    @tool(args_schema=cast("ArgsSchema", _MockSchemaV1))261    def tool_func_v1(262        *, arg1: int, arg2: bool, arg3: dict[str, Any] | None = None263    ) -> str:264        return f"{arg1} {arg2} {arg3}"265266    assert isinstance(tool_func_v1, BaseTool)267    assert tool_func_v1.args_schema == cast("ArgsSchema", _MockSchemaV1)268269270def test_decorated_function_schema_equivalent() -> None:271    """Test that a BaseTool without a schema meets expectations."""272273    @tool274    def structured_tool_input(275        *, arg1: int, arg2: bool, arg3: dict[str, Any] | None = None276    ) -> str:277        """Return the arguments directly."""278        return f"{arg1} {arg2} {arg3}"279280    assert isinstance(structured_tool_input, BaseTool)281    assert structured_tool_input.args_schema is not None282    assert (283        _schema(structured_tool_input.args_schema)["properties"]284        == _schema(_MockSchema)["properties"]285        == _normalize_schema(structured_tool_input.args)286    )287288289def test_args_kwargs_filtered() -> None:290    class _SingleArgToolWithKwargs(BaseTool):291        name: str = "single_arg_tool"292        description: str = "A  single arged tool with kwargs"293294        @override295        def _run(296            self,297            some_arg: str,298            run_manager: CallbackManagerForToolRun | None = None,299            **kwargs: Any,300        ) -> str:301            return "foo"302303        async def _arun(304            self,305            some_arg: str,306            run_manager: AsyncCallbackManagerForToolRun | None = None,307            **kwargs: Any,308        ) -> str:309            raise NotImplementedError310311    tool = _SingleArgToolWithKwargs()312    assert tool.is_single_input313314    class _VarArgToolWithKwargs(BaseTool):315        name: str = "single_arg_tool"316        description: str = "A single arged tool with kwargs"317318        @override319        def _run(320            self,321            *args: Any,322            run_manager: CallbackManagerForToolRun | None = None,323            **kwargs: Any,324        ) -> str:325            return "foo"326327        async def _arun(328            self,329            *args: Any,330            run_manager: AsyncCallbackManagerForToolRun | None = None,331            **kwargs: Any,332        ) -> str:333            raise NotImplementedError334335    tool2 = _VarArgToolWithKwargs()336    assert tool2.is_single_input337338339def test_structured_args_decorator_no_infer_schema() -> None:340    """Test functionality with structured arguments parsed as a decorator."""341342    @tool(infer_schema=False)343    def structured_tool_input(344        arg1: int, arg2: float | datetime, opt_arg: dict[str, Any] | None = None345    ) -> str:346        """Return the arguments directly."""347        return f"{arg1}, {arg2}, {opt_arg}"348349    assert isinstance(structured_tool_input, BaseTool)350    assert structured_tool_input.name == "structured_tool_input"351    args = {"arg1": 1, "arg2": 0.001, "opt_arg": {"foo": "bar"}}352    with pytest.raises(ToolException):353        assert structured_tool_input.run(args)354355356def test_structured_single_str_decorator_no_infer_schema() -> None:357    """Test functionality with structured arguments parsed as a decorator."""358359    @tool(infer_schema=False)360    def unstructured_tool_input(tool_input: str) -> str:361        """Return the arguments directly."""362        assert isinstance(tool_input, str)363        return f"{tool_input}"364365    assert isinstance(unstructured_tool_input, BaseTool)366    assert unstructured_tool_input.args_schema is None367    assert unstructured_tool_input.run("foo") == "foo"368369370def test_structured_tool_types_parsed() -> None:371    """Test the non-primitive types are correctly passed to structured tools."""372373    class SomeEnum(Enum):374        A = "a"375        B = "b"376377    class SomeBaseModel(BaseModel):378        foo: str379380    @tool381    def structured_tool(382        some_enum: SomeEnum,383        some_base_model: SomeBaseModel,384    ) -> dict[str, Any]:385        """Return the arguments directly."""386        return {387            "some_enum": some_enum,388            "some_base_model": some_base_model,389        }390391    assert isinstance(structured_tool, StructuredTool)392    args = {393        "some_enum": SomeEnum.A.value,394        "some_base_model": SomeBaseModel(foo="bar").model_dump(),395    }396    result = structured_tool.run(json.loads(json.dumps(args)))397    expected = {398        "some_enum": SomeEnum.A,399        "some_base_model": SomeBaseModel(foo="bar"),400    }401    assert result == expected402403404@pytest.mark.skipif(405    sys.version_info >= (3, 14),406    reason="pydantic.v1 namespace not supported with Python 3.14+",407)408def test_structured_tool_types_parsed_pydantic_v1() -> None:409    """Test the non-primitive types are correctly passed to structured tools."""410411    class SomeBaseModel(BaseModelV1):412        foo: str413414    class AnotherBaseModel(BaseModelV1):415        bar: str416417    @tool418    def structured_tool(some_base_model: SomeBaseModel) -> AnotherBaseModel:419        """Return the arguments directly."""420        return AnotherBaseModel(bar=some_base_model.foo)421422    assert isinstance(structured_tool, StructuredTool)423424    expected = AnotherBaseModel(bar="baz")425    for arg in [426        SomeBaseModel(foo="baz"),427        SomeBaseModel(foo="baz").dict(),428    ]:429        args = {"some_base_model": arg}430        result = structured_tool.run(args)431        assert result == expected432433434def test_structured_tool_types_parsed_pydantic_mixed() -> None:435    """Test handling of tool with mixed Pydantic version arguments."""436437    class SomeBaseModel(BaseModelV1):438        foo: str439440    class AnotherBaseModel(BaseModel):441        bar: str442443    with pytest.raises(NotImplementedError):444445        @tool446        def structured_tool(447            some_base_model: SomeBaseModel, another_base_model: AnotherBaseModel448        ) -> None:449            """Return the arguments directly."""450451452def test_base_tool_inheritance_base_schema() -> None:453    """Test schema is correctly inferred when inheriting from BaseTool."""454455    class _MockSimpleTool(BaseTool):456        name: str = "simple_tool"457        description: str = "A Simple Tool"458459        @override460        def _run(self, tool_input: str) -> str:461            return f"{tool_input}"462463        @override464        async def _arun(self, tool_input: str) -> str:465            raise NotImplementedError466467    simple_tool = _MockSimpleTool()468    assert simple_tool.args_schema is None469    expected_args = {"tool_input": {"title": "Tool Input", "type": "string"}}470    assert simple_tool.args == expected_args471472473def test_tool_lambda_args_schema() -> None:474    """Test args schema inference when the tool argument is a lambda function."""475    tool = Tool(476        name="tool",477        description="A tool",478        func=lambda tool_input: tool_input,479    )480    assert tool.args_schema is None481    expected_args = {"tool_input": {"type": "string"}}482    assert tool.args == expected_args483484485def test_structured_tool_from_function_docstring() -> None:486    """Test that structured tools can be created from functions."""487488    def foo(bar: int, baz: str) -> str:489        """Docstring.490491        Args:492            bar: the bar value493            baz: the baz value494        """495        raise NotImplementedError496497    structured_tool = StructuredTool.from_function(foo)498    assert structured_tool.name == "foo"499    assert structured_tool.args == {500        "bar": {"title": "Bar", "type": "integer"},501        "baz": {"title": "Baz", "type": "string"},502    }503504    assert _schema(structured_tool.args_schema) == {505        "properties": {506            "bar": {"title": "Bar", "type": "integer"},507            "baz": {"title": "Baz", "type": "string"},508        },509        "description": inspect.getdoc(foo),510        "title": "foo",511        "type": "object",512        "required": ["bar", "baz"],513    }514515    assert foo.__doc__ is not None516    assert structured_tool.description == textwrap.dedent(foo.__doc__.strip())517518519def test_structured_tool_from_function_docstring_complex_args() -> None:520    """Test that structured tools can be created from functions."""521522    def foo(bar: int, baz: list[str]) -> str:523        """Docstring.524525        Args:526            bar: int527            baz: list[str]528        """529        raise NotImplementedError530531    structured_tool = StructuredTool.from_function(foo)532    assert structured_tool.name == "foo"533    assert structured_tool.args == {534        "bar": {"title": "Bar", "type": "integer"},535        "baz": {536            "title": "Baz",537            "type": "array",538            "items": {"type": "string"},539        },540    }541542    assert _schema(structured_tool.args_schema) == {543        "properties": {544            "bar": {"title": "Bar", "type": "integer"},545            "baz": {546                "title": "Baz",547                "type": "array",548                "items": {"type": "string"},549            },550        },551        "description": inspect.getdoc(foo),552        "title": "foo",553        "type": "object",554        "required": ["bar", "baz"],555    }556557    assert foo.__doc__ is not None558    assert structured_tool.description == textwrap.dedent(foo.__doc__).strip()559560561def test_structured_tool_lambda_multi_args_schema() -> None:562    """Test args schema inference when the tool argument is a lambda function."""563    tool = StructuredTool.from_function(564        name="tool",565        description="A tool",566        func=lambda tool_input, other_arg: f"{tool_input}{other_arg}",567    )568    assert tool.args_schema is not None569    expected_args = {570        "tool_input": {"title": "Tool Input"},571        "other_arg": {"title": "Other Arg"},572    }573    assert tool.args == expected_args574575576def test_tool_partial_function_args_schema() -> None:577    """Test args schema inference when the tool argument is a partial function."""578579    def func(tool_input: str, other_arg: str) -> str:580        assert isinstance(tool_input, str)581        assert isinstance(other_arg, str)582        return tool_input + other_arg583584    tool = Tool(585        name="tool",586        description="A tool",587        func=partial(func, other_arg="foo"),588    )589    assert tool.run("bar") == "barfoo"590591592def test_empty_args_decorator() -> None:593    """Test inferred schema of decorated fn with no args."""594595    @tool596    def empty_tool_input() -> str:597        """Return a constant."""598        return "the empty result"599600    assert isinstance(empty_tool_input, BaseTool)601    assert empty_tool_input.name == "empty_tool_input"602    assert empty_tool_input.args == {}603    assert empty_tool_input.run({}) == "the empty result"604605606def test_tool_from_function_with_run_manager() -> None:607    """Test run of tool when using run_manager."""608609    def foo(bar: str, callbacks: CallbackManagerForToolRun | None = None) -> str:  # noqa: D417610        """Docstring.611612        Args:613            bar: str.614        """615        assert callbacks is not None616        return "foo" + bar617618    handler = FakeCallbackHandler()619    tool = Tool.from_function(foo, name="foo", description="Docstring")620621    assert tool.run(tool_input={"bar": "bar"}, run_manager=[handler]) == "foobar"622    assert tool.run("baz", run_manager=[handler]) == "foobaz"623624625def test_structured_tool_from_function_with_run_manager() -> None:626    """Test args and schema of structured tool when using callbacks."""627628    def foo(  # noqa: D417629        bar: int, baz: str, callbacks: CallbackManagerForToolRun | None = None630    ) -> str:631        """Docstring.632633        Args:634            bar: int635            baz: str636        """637        assert callbacks is not None638        return str(bar) + baz639640    handler = FakeCallbackHandler()641    structured_tool = StructuredTool.from_function(foo)642643    assert structured_tool.args == {644        "bar": {"title": "Bar", "type": "integer"},645        "baz": {"title": "Baz", "type": "string"},646    }647648    assert _schema(structured_tool.args_schema) == {649        "properties": {650            "bar": {"title": "Bar", "type": "integer"},651            "baz": {"title": "Baz", "type": "string"},652        },653        "description": inspect.getdoc(foo),654        "title": "foo",655        "type": "object",656        "required": ["bar", "baz"],657    }658659    assert (660        structured_tool.run(661            tool_input={"bar": "10", "baz": "baz"}, run_manger=[handler]662        )663        == "10baz"664    )665666667def test_structured_tool_from_parameterless_function() -> None:668    """Test parameterless function of structured tool."""669670    def foo() -> str:671        """Docstring."""672        return "invoke foo"673674    structured_tool = StructuredTool.from_function(foo)675676    assert structured_tool.run({}) == "invoke foo"677    assert structured_tool.run("") == "invoke foo"678679680def test_named_tool_decorator() -> None:681    """Test functionality when arguments are provided as input to decorator."""682683    @tool("search")684    def search_api(query: str) -> str:685        """Search the API for the query."""686        assert isinstance(query, str)687        return f"API result - {query}"688689    assert isinstance(search_api, BaseTool)690    assert search_api.name == "search"691    assert not search_api.return_direct692    assert search_api.run({"query": "foo"}) == "API result - foo"693694695def test_named_tool_decorator_return_direct() -> None:696    """Test functionality when arguments and return direct are provided as input."""697698    @tool("search", return_direct=True)699    def search_api(query: str, *args: Any) -> str:700        """Search the API for the query."""701        return "API result"702703    assert isinstance(search_api, BaseTool)704    assert search_api.name == "search"705    assert search_api.return_direct706    assert search_api.run({"query": "foo"}) == "API result"707708709def test_unnamed_tool_decorator_return_direct() -> None:710    """Test functionality when only return direct is provided."""711712    @tool(return_direct=True)713    def search_api(query: str) -> str:714        """Search the API for the query."""715        assert isinstance(query, str)716        return "API result"717718    assert isinstance(search_api, BaseTool)719    assert search_api.name == "search_api"720    assert search_api.return_direct721    assert search_api.run({"query": "foo"}) == "API result"722723724def test_tool_with_kwargs() -> None:725    """Test functionality when only return direct is provided."""726727    @tool(return_direct=True)728    def search_api(729        arg_0: str,730        arg_1: float = 4.3,731        ping: str = "hi",732    ) -> str:733        """Search the API for the query."""734        return f"arg_0={arg_0}, arg_1={arg_1}, ping={ping}"735736    assert isinstance(search_api, BaseTool)737    result = search_api.run(738        tool_input={739            "arg_0": "foo",740            "arg_1": 3.2,741            "ping": "pong",742        }743    )744    assert result == "arg_0=foo, arg_1=3.2, ping=pong"745746    result = search_api.run(747        tool_input={748            "arg_0": "foo",749        }750    )751    assert result == "arg_0=foo, arg_1=4.3, ping=hi"752    # For backwards compatibility, we still accept a single str arg753    result = search_api.run("foobar")754    assert result == "arg_0=foobar, arg_1=4.3, ping=hi"755756757def test_missing_docstring() -> None:758    """Test error is raised when docstring is missing."""759    # expect to throw a value error if there's no docstring760    with pytest.raises(ValueError, match="Function must have a docstring"):761762        @tool763        def search_api(query: str) -> str:764            return "API result"765766    @tool767    class MyTool(BaseModel):768        foo: str769770    assert not MyTool.description  # type: ignore[attr-defined]771772773def test_create_tool_positional_args() -> None:774    """Test that positional arguments are allowed."""775    test_tool = Tool("test_name", lambda x: x, "test_description")776    assert test_tool.invoke("foo") == "foo"777    assert test_tool.name == "test_name"778    assert test_tool.description == "test_description"779    assert test_tool.is_single_input780781782def test_create_tool_keyword_args() -> None:783    """Test that keyword arguments are allowed."""784    test_tool = Tool(name="test_name", func=lambda x: x, description="test_description")785    assert test_tool.is_single_input786    assert test_tool.invoke("foo") == "foo"787    assert test_tool.name == "test_name"788    assert test_tool.description == "test_description"789790791async def test_create_async_tool() -> None:792    """Test that async tools are allowed."""793794    async def _test_func(x: str) -> str:795        return x796797    test_tool = Tool(798        name="test_name",799        func=lambda x: x,800        description="test_description",801        coroutine=_test_func,802    )803    assert test_tool.is_single_input804    assert test_tool.invoke("foo") == "foo"805    assert test_tool.name == "test_name"806    assert test_tool.description == "test_description"807    assert test_tool.coroutine is not None808    assert await test_tool.arun("foo") == "foo"809810811class _FakeExceptionTool(BaseTool):812    name: str = "exception"813    description: str = "an exception-throwing tool"814    exception: Exception = ToolException()815816    def _run(self) -> str:817        raise self.exception818819    async def _arun(self) -> str:820        raise self.exception821822823def test_exception_handling_bool() -> None:824    tool_ = _FakeExceptionTool(handle_tool_error=True)825    expected = "Tool execution error"826    actual = tool_.run({})827    assert expected == actual828829830def test_exception_handling_str() -> None:831    expected = "foo bar"832    tool_ = _FakeExceptionTool(handle_tool_error=expected)833    actual = tool_.run({})834    assert expected == actual835836837def test_exception_handling_callable() -> None:838    expected = "foo bar"839840    def handling(e: ToolException) -> str:841        return expected842843    tool_ = _FakeExceptionTool(handle_tool_error=handling)844    actual = tool_.run({})845    assert expected == actual846847848def test_exception_handling_callable_message_content_blocks() -> None:849    expected: list[dict[str, Any]] = [{"type": "text", "text": "handled error"}]850851    def handling(e: ToolException) -> list[dict[str, Any]]:852        return expected853854    tool_ = _FakeExceptionTool(handle_tool_error=handling)855    actual = tool_.invoke(856        {857            "type": "tool_call",858            "args": {},859            "name": "exception",860            "id": "call_1",861        }862    )863864    assert isinstance(actual, ToolMessage)865    assert actual.content == expected866    assert actual.status == "error"867    assert actual.tool_call_id == "call_1"868869870def test_exception_handling_callable_message_content_blocks_sequence() -> None:871    content = ({"type": "text", "text": "handled error"},)872873    def handling(e: ToolException) -> tuple[dict[str, Any], ...]:874        return content875876    tool_ = _FakeExceptionTool(handle_tool_error=handling)877    actual = tool_.invoke(878        {879            "type": "tool_call",880            "args": {},881            "name": "exception",882            "id": "call_1",883        }884    )885886    assert isinstance(actual, ToolMessage)887    assert actual.content == list(content)888    assert actual.status == "error"889    assert actual.tool_call_id == "call_1"890891892def test_exception_handling_callable_invalid_blocks_stringified() -> None:893    # A sequence whose elements are not valid content blocks is not message894    # content, so it falls back to a JSON-stringified ToolMessage.895    def handling(e: ToolException) -> list[dict[str, Any]]:896        return [{"text": "foo"}]  # missing 'type' -> not a valid block897898    tool_ = _FakeExceptionTool(handle_tool_error=handling)899    actual = tool_.invoke(900        {901            "type": "tool_call",902            "args": {},903            "name": "exception",904            "id": "call_1",905        }906    )907908    assert isinstance(actual, ToolMessage)909    assert actual.content == '[{"text": "foo"}]'910    assert actual.status == "error"911    assert actual.tool_call_id == "call_1"912913914def test_exception_handling_non_tool_exception() -> None:915    tool_ = _FakeExceptionTool(exception=ValueError("some error"))916    with pytest.raises(ValueError, match="some error"):917        tool_.run({})918919920async def test_async_exception_handling_bool() -> None:921    tool_ = _FakeExceptionTool(handle_tool_error=True)922    expected = "Tool execution error"923    actual = await tool_.arun({})924    assert expected == actual925926927async def test_async_exception_handling_str() -> None:928    expected = "foo bar"929    tool_ = _FakeExceptionTool(handle_tool_error=expected)930    actual = await tool_.arun({})931    assert expected == actual932933934async def test_async_exception_handling_callable() -> None:935    expected = "foo bar"936937    def handling(e: ToolException) -> str:938        return expected939940    tool_ = _FakeExceptionTool(handle_tool_error=handling)941    actual = await tool_.arun({})942    assert expected == actual943944945async def test_async_exception_handling_callable_message_content_blocks() -> None:946    expected: list[dict[str, Any]] = [{"type": "text", "text": "handled error"}]947948    def handling(e: ToolException) -> list[dict[str, Any]]:949        return expected950951    tool_ = _FakeExceptionTool(handle_tool_error=handling)952    actual = await tool_.ainvoke(953        {954            "type": "tool_call",955            "args": {},956            "name": "exception",957            "id": "call_1",958        }959    )960961    assert isinstance(actual, ToolMessage)962    assert actual.content == expected963    assert actual.status == "error"964    assert actual.tool_call_id == "call_1"965966967async def test_async_exception_handling_callable_message_content_blocks_sequence() -> (968    None969):970    content = ({"type": "text", "text": "handled error"},)971972    def handling(e: ToolException) -> tuple[dict[str, Any], ...]:973        return content974975    tool_ = _FakeExceptionTool(handle_tool_error=handling)976    actual = await tool_.ainvoke(977        {978            "type": "tool_call",979            "args": {},980            "name": "exception",981            "id": "call_1",982        }983    )984985    assert isinstance(actual, ToolMessage)986    assert actual.content == list(content)987    assert actual.status == "error"988    assert actual.tool_call_id == "call_1"989990991async def test_async_exception_handling_non_tool_exception() -> None:992    tool_ = _FakeExceptionTool(exception=ValueError("some error"))993    with pytest.raises(ValueError, match="some error"):994        await tool_.arun({})995996997def test_structured_tool_from_function() -> None:998    """Test that structured tools can be created from functions."""9991000    def foo(bar: int, baz: str) -> str:1001        """Docstring thing.10021003        Args:1004            bar: the bar value1005            baz: the baz value1006        """1007        raise NotImplementedError10081009    structured_tool = StructuredTool.from_function(foo)1010    assert structured_tool.name == "foo"1011    assert structured_tool.args == {1012        "bar": {"title": "Bar", "type": "integer"},1013        "baz": {"title": "Baz", "type": "string"},1014    }10151016    assert _schema(structured_tool.args_schema) == {1017        "title": "foo",1018        "type": "object",1019        "description": inspect.getdoc(foo),1020        "properties": {1021            "bar": {"title": "Bar", "type": "integer"},1022            "baz": {"title": "Baz", "type": "string"},1023        },1024        "required": ["bar", "baz"],1025    }10261027    assert foo.__doc__ is not None1028    assert structured_tool.description == textwrap.dedent(foo.__doc__.strip())102910301031def test_validation_error_handling_bool() -> None:1032    """Test that validation errors are handled correctly."""1033    expected = "Tool input validation error"1034    tool_ = _MockStructuredTool(handle_validation_error=True)1035    actual = tool_.run({})1036    assert expected == actual103710381039def test_validation_error_handling_str() -> None:1040    """Test that validation errors are handled correctly."""1041    expected = "foo bar"1042    tool_ = _MockStructuredTool(handle_validation_error=expected)1043    actual = tool_.run({})1044    assert expected == actual104510461047def test_validation_error_handling_callable() -> None:1048    """Test that validation errors are handled correctly."""1049    expected = "foo bar"10501051    def handling(e: ValidationError | ValidationErrorV1) -> str:1052        return expected10531054    tool_ = _MockStructuredTool(handle_validation_error=handling)1055    actual = tool_.run({})1056    assert expected == actual105710581059@pytest.mark.parametrize(1060    "handler",1061    [1062        True,1063        "foo bar",1064        lambda _: "foo bar",1065    ],1066)1067def test_validation_error_handling_non_validation_error(1068    *,1069    handler: bool | str | Callable[[ValidationError | ValidationErrorV1], str],1070) -> None:1071    """Test that validation errors are handled correctly."""10721073    class _RaiseNonValidationErrorTool(BaseTool):1074        name: str = "raise_non_validation_error_tool"1075        description: str = "A tool that raises a non-validation error"10761077        def _parse_input(1078            self,1079            tool_input: str | dict[str, Any],1080            tool_call_id: str | None,1081        ) -> str | dict[str, Any]:1082            raise NotImplementedError10831084        @override1085        def _run(self) -> str:1086            return "dummy"10871088        @override1089        async def _arun(self) -> str:1090            return "dummy"10911092    tool_ = _RaiseNonValidationErrorTool(handle_validation_error=handler)1093    with pytest.raises(NotImplementedError):1094        tool_.run({})109510961097async def test_async_validation_error_handling_bool() -> None:1098    """Test that validation errors are handled correctly."""1099    expected = "Tool input validation error"1100    tool_ = _MockStructuredTool(handle_validation_error=True)1101    actual = await tool_.arun({})1102    assert expected == actual110311041105async def test_async_validation_error_handling_str() -> None:1106    """Test that validation errors are handled correctly."""1107    expected = "foo bar"1108    tool_ = _MockStructuredTool(handle_validation_error=expected)1109    actual = await tool_.arun({})1110    assert expected == actual111111121113async def test_async_validation_error_handling_callable() -> None:1114    """Test that validation errors are handled correctly."""1115    expected = "foo bar"11161117    def handling(e: ValidationError | ValidationErrorV1) -> str:1118        return expected11191120    tool_ = _MockStructuredTool(handle_validation_error=handling)1121    actual = await tool_.arun({})1122    assert expected == actual112311241125@pytest.mark.parametrize(1126    "handler",1127    [1128        True,1129        "foo bar",1130        lambda _: "foo bar",1131    ],1132)1133async def test_async_validation_error_handling_non_validation_error(1134    *,1135    handler: bool | str | Callable[[ValidationError | ValidationErrorV1], str],1136) -> None:1137    """Test that validation errors are handled correctly."""11381139    class _RaiseNonValidationErrorTool(BaseTool):1140        name: str = "raise_non_validation_error_tool"1141        description: str = "A tool that raises a non-validation error"11421143        def _parse_input(1144            self,1145            tool_input: str | dict[str, Any],1146            tool_call_id: str | None,1147        ) -> str | dict[str, Any]:1148            raise NotImplementedError11491150        @override1151        def _run(self) -> str:1152            return "dummy"11531154        @override1155        async def _arun(self) -> str:1156            return "dummy"11571158    tool_ = _RaiseNonValidationErrorTool(handle_validation_error=handler)1159    with pytest.raises(NotImplementedError):1160        await tool_.arun({})116111621163def test_optional_subset_model_rewrite() -> None:1164    class MyModel(BaseModel):1165        a: str | None = None1166        b: str1167        c: list[str | None] | None = None11681169    model2 = _create_subset_model("model2", MyModel, ["a", "b", "c"])11701171    assert set(_schema(model2)["required"]) == {"b"}117211731174@pytest.mark.parametrize(1175    ("inputs", "expected"),1176    [1177        # Check not required1178        ({"bar": "bar"}, {"bar": "bar", "baz": 3, "buzz": "buzz"}),1179        # Check overwritten1180        (1181            {"bar": "bar", "baz": 4, "buzz": "not-buzz"},1182            {"bar": "bar", "baz": 4, "buzz": "not-buzz"},1183        ),1184        # Check validation error when missing1185        ({}, None),1186        # Check validation error when wrong type1187        ({"bar": "bar", "baz": "not-an-int"}, None),1188        # Check OK when None explicitly passed1189        ({"bar": "bar", "baz": None}, {"bar": "bar", "baz": None, "buzz": "buzz"}),1190    ],1191)1192def test_tool_invoke_optional_args(1193    inputs: dict[str, Any], expected: dict[str, Any] | None1194) -> None:1195    @tool1196    def foo(bar: str, baz: int | None = 3, buzz: str | None = "buzz") -> dict[str, Any]:1197        """The foo."""1198        return {1199            "bar": bar,1200            "baz": baz,1201            "buzz": buzz,1202        }12031204    if expected is not None:1205        assert foo.invoke(inputs) == expected1206    else:1207        with pytest.raises(ValidationError):1208            foo.invoke(inputs)120912101211def test_tool_pass_context() -> None:1212    @tool1213    def foo(bar: str) -> str:1214        """The foo."""1215        config = ensure_config()1216        assert config["configurable"]["foo"] == "not-bar"1217        assert bar == "baz"1218        return bar12191220    assert foo.invoke({"bar": "baz"}, {"configurable": {"foo": "not-bar"}}) == "baz"122112221223@pytest.mark.skipif(1224    sys.version_info < (3, 11),1225    reason="requires python3.11 or higher",1226)1227async def test_async_tool_pass_context() -> None:1228    @tool1229    async def foo(bar: str) -> str:1230        """The foo."""1231        config = ensure_config()1232        assert config["configurable"]["foo"] == "not-bar"1233        assert bar == "baz"1234        return bar12351236    assert (1237        await foo.ainvoke({"bar": "baz"}, {"configurable": {"foo": "not-bar"}}) == "baz"1238    )123912401241def assert_bar(bar: Any, bar_config: RunnableConfig) -> Any:1242    assert bar_config["configurable"]["foo"] == "not-bar"1243    assert bar == "baz"1244    return bar124512461247@tool1248def foo(bar: Any, bar_config: RunnableConfig) -> Any:1249    """The foo."""1250    return assert_bar(bar, bar_config)125112521253@tool1254async def afoo(bar: Any, bar_config: RunnableConfig) -> Any:1255    """The foo."""1256    return assert_bar(bar, bar_config)125712581259@tool(infer_schema=False)1260def simple_foo(bar: Any, bar_config: RunnableConfig) -> Any:1261    """The foo."""1262    return assert_bar(bar, bar_config)126312641265@tool(infer_schema=False)1266async def asimple_foo(bar: Any, bar_config: RunnableConfig) -> Any:1267    """The foo."""1268    return assert_bar(bar, bar_config)126912701271class FooBase(BaseTool):1272    name: str = "Foo"1273    description: str = "Foo"12741275    @override1276    def _run(self, bar: Any, bar_config: RunnableConfig, **kwargs: Any) -> Any:1277        return assert_bar(bar, bar_config)127812791280class AFooBase(FooBase):1281    @override1282    async def _arun(self, bar: Any, bar_config: RunnableConfig, **kwargs: Any) -> Any:1283        return assert_bar(bar, bar_config)128412851286@pytest.mark.parametrize("tool", [foo, simple_foo, FooBase(), AFooBase()])1287def test_tool_pass_config(tool: BaseTool) -> None:1288    assert tool.invoke({"bar": "baz"}, {"configurable": {"foo": "not-bar"}}) == "baz"12891290    # Test we don't mutate tool calls1291    tool_call = {1292        "name": tool.name,1293        "args": {"bar": "baz"},1294        "id": "abc123",1295        "type": "tool_call",1296    }1297    _ = tool.invoke(tool_call, {"configurable": {"foo": "not-bar"}})1298    assert tool_call["args"] == {"bar": "baz"}129913001301class FooBaseNonPickleable(FooBase):1302    @override1303    def _run(self, bar: Any, bar_config: RunnableConfig, **kwargs: Any) -> Any:1304        return True130513061307def test_tool_pass_config_non_pickleable() -> None:1308    tool = FooBaseNonPickleable()13091310    args = {"bar": threading.Lock()}1311    tool_call = {1312        "name": tool.name,1313        "args": args,1314        "id": "abc123",1315        "type": "tool_call",1316    }1317    _ = tool.invoke(tool_call, {"configurable": {"foo": "not-bar"}})1318    assert tool_call["args"] == args131913201321@pytest.mark.parametrize(1322    "tool", [foo, afoo, simple_foo, asimple_foo, FooBase(), AFooBase()]1323)1324async def test_async_tool_pass_config(tool: BaseTool) -> None:1325    assert (1326        await tool.ainvoke({"bar": "baz"}, {"configurable": {"foo": "not-bar"}})1327        == "baz"1328    )132913301331def test_tool_description() -> None:1332    def foo(bar: str) -> str:1333        """The foo."""1334        return bar13351336    foo1 = tool(foo)1337    assert foo1.description == "The foo."13381339    foo2 = StructuredTool.from_function(foo)1340    assert foo2.description == "The foo."134113421343def test_tool_arg_descriptions() -> None:1344    def foo(bar: str, baz: int) -> str:1345        """The foo.13461347        Args:1348            bar: The bar.1349            baz: The baz.1350        """1351        return bar13521353    foo1 = tool(foo)1354    args_schema = _schema(foo1.args_schema)1355    assert args_schema == {1356        "title": "foo",1357        "type": "object",1358        "description": inspect.getdoc(foo),1359        "properties": {1360            "bar": {"title": "Bar", "type": "string"},1361            "baz": {"title": "Baz", "type": "integer"},1362        },1363        "required": ["bar", "baz"],1364    }13651366    # Test parses docstring1367    foo2 = tool(foo, parse_docstring=True)1368    args_schema = _schema(foo2.args_schema)1369    expected = {1370        "title": "foo",1371        "description": "The foo.",1372        "type": "object",1373        "properties": {1374            "bar": {"title": "Bar", "description": "The bar.", "type": "string"},1375            "baz": {"title": "Baz", "description": "The baz.", "type": "integer"},1376        },1377        "required": ["bar", "baz"],1378    }1379    assert args_schema == expected13801381    # Test parsing with run_manager does not raise error1382    def foo3(  # noqa: D4171383        bar: str, baz: int, run_manager: CallbackManagerForToolRun | None = None1384    ) -> str:1385        """The foo.13861387        Args:1388            bar: The bar.1389            baz: The baz.1390        """1391        return bar13921393    as_tool = tool(foo3, parse_docstring=True)1394    args_schema = _schema(as_tool.args_schema)1395    assert args_schema["description"] == expected["description"]1396    assert args_schema["properties"] == expected["properties"]13971398    # Test parsing with runtime does not raise error1399    def foo3_runtime(bar: str, baz: int, runtime: Any) -> str:  # noqa: D4171400        """The foo.14011402        Args:1403            bar: The bar.1404            baz: The baz.1405        """1406        return bar14071408    _ = tool(foo3_runtime, parse_docstring=True)14091410    # Test parameterless tool does not raise error for missing Args section1411    # in docstring.1412    def foo4() -> str:1413        """The foo."""1414        return "bar"14151416    as_tool = tool(foo4, parse_docstring=True)1417    args_schema = _schema(as_tool.args_schema)1418    assert args_schema["description"] == expected["description"]14191420    def foo5(run_manager: CallbackManagerForToolRun | None = None) -> str:1421        """The foo."""1422        return "bar"14231424    as_tool = tool(foo5, parse_docstring=True)1425    args_schema = _schema(as_tool.args_schema)1426    assert args_schema["description"] == expected["description"]142714281429def test_docstring_parsing() -> None:1430    expected = {1431        "title": "foo",1432        "description": "The foo.",1433        "type": "object",1434        "properties": {1435            "bar": {"title": "Bar", "description": "The bar.", "type": "string"},1436            "baz": {"title": "Baz", "description": "The baz.", "type": "integer"},1437        },1438        "required": ["bar", "baz"],1439    }14401441    # Simple case1442    def foo(bar: str, baz: int) -> str:1443        """The foo.14441445        Args:1446            bar: The bar.1447            baz: The baz.1448        """1449        return bar14501451    as_tool = tool(foo, parse_docstring=True)1452    args_schema = _schema(as_tool.args_schema)1453    assert args_schema["description"] == "The foo."1454    assert args_schema["properties"] == expected["properties"]14551456    # Multi-line description1457    def foo2(bar: str, baz: int) -> str:1458        """The foo.14591460        Additional description here.14611462        Args:1463            bar: The bar.1464            baz: The baz.1465        """1466        return bar14671468    as_tool = tool(foo2, parse_docstring=True)1469    args_schema2 = _schema(as_tool.args_schema)1470    assert args_schema2["description"] == "The foo. Additional description here."1471    assert args_schema2["properties"] == expected["properties"]14721473    # Multi-line with Returns block1474    def foo3(bar: str, baz: int) -> str:1475        """The foo.14761477        Additional description here.14781479        Args:1480            bar: The bar.1481            baz: The baz.14821483        Returns:1484            description of returned value.1485        """1486        return bar14871488    as_tool = tool(foo3, parse_docstring=True)1489    args_schema3 = _schema(as_tool.args_schema)1490    args_schema3["title"] = "foo2"1491    assert args_schema2 == args_schema314921493    # Single argument1494    def foo4(bar: str) -> str:1495        """The foo.14961497        Args:1498            bar: The bar.1499        """1500        return bar15011502    as_tool = tool(foo4, parse_docstring=True)1503    args_schema4 = _schema(as_tool.args_schema)1504    assert args_schema4["description"] == "The foo."1505    assert args_schema4["properties"] == {1506        "bar": {"description": "The bar.", "title": "Bar", "type": "string"}1507    }150815091510def test_tool_invalid_docstrings() -> None:1511    """Test invalid docstrings."""15121513    def foo3(bar: str, baz: int) -> str:1514        """The foo."""1515        return bar15161517    def foo4(bar: str, baz: int) -> str:1518        """The foo.1519        Args:1520            bar: The bar.1521            baz: The baz.1522        """  # noqa: D205,D411  # We're intentionally testing bad formatting.1523        return bar15241525    for func in {foo3, foo4}:1526        with pytest.raises(ValueError, match="Found invalid Google-Style docstring"):1527            _ = tool(func, parse_docstring=True)15281529    def foo5(bar: str, baz: int) -> str:  # noqa: D4171530        """The foo.15311532        Args:1533            banana: The bar.1534            monkey: The baz.1535        """1536        return bar15371538    with pytest.raises(1539        ValueError, match="Arg banana in docstring not found in function signature"1540    ):1541        _ = tool(foo5, parse_docstring=True)154215431544def test_tool_annotated_descriptions() -> None:1545    def foo(1546        bar: Annotated[str, "this is the bar"], baz: Annotated[int, "this is the baz"]1547    ) -> str:1548        """The foo.15491550        Returns:1551            The bar only.1552        """1553        return bar15541555    foo1 = tool(foo)1556    args_schema = _schema(foo1.args_schema)1557    assert args_schema == {1558        "title": "foo",1559        "type": "object",1560        "description": inspect.getdoc(foo),1561        "properties": {1562            "bar": {"title": "Bar", "type": "string", "description": "this is the bar"},1563            "baz": {1564                "title": "Baz",1565                "type": "integer",1566                "description": "this is the baz",1567            },1568        },1569        "required": ["bar", "baz"],1570    }157115721573def test_tool_field_description_preserved() -> None:1574    """Test that `Field(description=...)` is preserved in `@tool` decorator."""15751576    @tool1577    def my_tool(1578        topic: Annotated[str, Field(description="The research topic")],1579        depth: Annotated[int, Field(description="Search depth level")] = 3,1580    ) -> str:1581        """A tool for research."""1582        return f"{topic} at depth {depth}"15831584    args_schema = _schema(my_tool.args_schema)1585    assert args_schema == {1586        "title": "my_tool",1587        "type": "object",1588        "description": "A tool for research.",1589        "properties": {1590            "topic": {1591                "title": "Topic",1592                "type": "string",1593                "description": "The research topic",1594            },1595            "depth": {1596                "title": "Depth",1597                "type": "integer",1598                "description": "Search depth level",1599                "default": 3,1600            },1601        },1602        "required": ["topic"],1603    }160416051606def test_tool_call_input_tool_message_output() -> None:1607    tool_call = {1608        "name": "structured_api",1609        "args": {"arg1": 1, "arg2": True, "arg3": {"img": "base64string..."}},1610        "id": "123",1611        "type": "tool_call",1612    }1613    tool = _MockStructuredTool()1614    expected = ToolMessage(1615        "1 True {'img': 'base64string...'}", tool_call_id="123", name="structured_api"1616    )1617    actual = tool.invoke(tool_call)1618    assert actual == expected16191620    tool_call.pop("type")1621    with pytest.raises(ValidationError):1622        tool.invoke(tool_call)162316241625@pytest.mark.parametrize("block_type", [*TOOL_MESSAGE_BLOCK_TYPES, "bad"])1626def test_tool_content_block_output(block_type: str) -> None:1627    @tool1628    def my_tool(query: str) -> list[dict[str, Any]]:1629        """Test tool."""1630        return [{"type": block_type, "foo": "bar"}]16311632    tool_call = {1633        "type": "tool_call",1634        "name": "my_tool",1635        "args": {"query": "baz"},1636        "id": "call_abc123",1637    }16381639    result = my_tool.invoke(tool_call)1640    assert isinstance(result, ToolMessage)16411642    if block_type in TOOL_MESSAGE_BLOCK_TYPES:1643        assert result.content == [{"type": block_type, "foo": "bar"}]1644    else:1645        assert result.content == '[{"type": "bad", "foo": "bar"}]'164616471648class _MockStructuredToolWithRawOutput(BaseTool):1649    name: str = "structured_api"1650    args_schema: type[BaseModel] = _MockSchema1651    description: str = "A Structured Tool"1652    response_format: Literal["content_and_artifact"] = "content_and_artifact"16531654    @override1655    def _run(1656        self,1657        arg1: int,1658        arg2: bool,1659        arg3: dict[str, Any] | None = None,1660    ) -> tuple[str, dict[str, Any]]:1661        return f"{arg1} {arg2}", {"arg1": arg1, "arg2": arg2, "arg3": arg3}166216631664@tool("structured_api", response_format="content_and_artifact")1665def _mock_structured_tool_with_artifact(1666    *, arg1: int, arg2: bool, arg3: dict[str, str] | None = None1667) -> tuple[str, dict[str, Any]]:1668    """A Structured Tool."""1669    return f"{arg1} {arg2}", {"arg1": arg1, "arg2": arg2, "arg3": arg3}167016711672@pytest.mark.parametrize(1673    "tool", [_MockStructuredToolWithRawOutput(), _mock_structured_tool_with_artifact]1674)1675def test_tool_call_input_tool_message_with_artifact(tool: BaseTool) -> None:1676    tool_call: dict[str, Any] = {1677        "name": "structured_api",1678        "args": {"arg1": 1, "arg2": True, "arg3": {"img": "base64string..."}},1679        "id": "123",1680        "type": "tool_call",1681    }1682    expected = ToolMessage(1683        "1 True", artifact=tool_call["args"], tool_call_id="123", name="structured_api"1684    )1685    actual = tool.invoke(tool_call)1686    assert actual == expected16871688    tool_call.pop("type")1689    with pytest.raises(ValidationError):1690        tool.invoke(tool_call)16911692    actual_content = tool.invoke(tool_call["args"])1693    assert actual_content == expected.content169416951696def test_convert_from_runnable_dict() -> None:1697    # Test with typed dict input1698    class Args(TypedDict):1699        a: int1700        b: list[int]17011702    def f(x: Args) -> str:1703        return str(x["a"] * max(x["b"]))17041705    runnable = RunnableLambda(f)1706    as_tool = runnable.as_tool()1707    args_schema = as_tool.args_schema1708    assert args_schema is not None1709    assert _schema(args_schema) == {1710        "title": "f",1711        "type": "object",1712        "properties": {1713            "a": {"title": "A", "type": "integer"},1714            "b": {"title": "B", "type": "array", "items": {"type": "integer"}},1715        },1716        "required": ["a", "b"],1717    }1718    assert as_tool.description1719    result = as_tool.invoke({"a": 3, "b": [1, 2]})1720    assert result == "6"17211722    as_tool = runnable.as_tool(name="my tool", description="test description")1723    assert as_tool.name == "my tool"1724    assert as_tool.description == "test description"17251726    # Dict without typed input-- must supply schema1727    def g(x: dict[str, Any]) -> str:1728        return str(x["a"] * max(x["b"]))17291730    # Specify via args_schema:1731    class GSchema(BaseModel):1732        """Apply a function to an integer and list of integers."""17331734        a: int = Field(..., description="Integer")1735        b: list[int] = Field(..., description="List of ints")17361737    runnable2 = RunnableLambda(g)1738    as_tool2 = runnable2.as_tool(GSchema)1739    as_tool2.invoke({"a": 3, "b": [1, 2]})17401741    # Specify via arg_types:1742    runnable3 = RunnableLambda(g)1743    as_tool3 = runnable3.as_tool(arg_types={"a": int, "b": list[int]})1744    result = as_tool3.invoke({"a": 3, "b": [1, 2]})1745    assert result == "6"17461747    # Test with config1748    def h(x: dict[str, Any]) -> str:1749        config = ensure_config()1750        assert config["configurable"]["foo"] == "not-bar"1751        return str(x["a"] * max(x["b"]))17521753    runnable4 = RunnableLambda(h)1754    as_tool4 = runnable4.as_tool(arg_types={"a": int, "b": list[int]})1755    result = as_tool4.invoke(1756        {"a": 3, "b": [1, 2]}, config={"configurable": {"foo": "not-bar"}}1757    )1758    assert result == "6"175917601761def test_convert_from_runnable_other() -> None:1762    # String input1763    def f(x: str) -> str:1764        return x + "a"17651766    def g(x: str) -> str:1767        return x + "z"17681769    runnable = RunnableLambda(f) | g1770    as_tool = runnable.as_tool()1771    args_schema = as_tool.args_schema1772    assert args_schema is None1773    assert as_tool.description17741775    result = as_tool.invoke("b")1776    assert result == "baz"17771778    # Test with config1779    def h(x: str) -> str:1780        config = ensure_config()1781        assert config["configurable"]["foo"] == "not-bar"1782        return x + "a"17831784    runnable2 = RunnableLambda(h)1785    as_tool2 = runnable2.as_tool()1786    result2 = as_tool2.invoke("b", config={"configurable": {"foo": "not-bar"}})1787    assert result2 == "ba"178817891790@tool("foo", parse_docstring=True)1791def injected_tool(x: int, y: Annotated[str, InjectedToolArg]) -> str:1792    """Foo.17931794    Args:1795        x: abc1796        y: 1231797    """1798    return y179918001801class InjectedTool(BaseTool):1802    name: str = "foo"1803    description: str = "foo."18041805    @override1806    def _run(self, x: int, y: Annotated[str, InjectedToolArg]) -> Any:1807        """Foo.18081809        Args:1810            x: abc1811            y: 1231812        """1813        return y181418151816class fooSchema(BaseModel):  # noqa: N8011817    """foo."""18181819    x: int = Field(..., description="abc")1820    y: Annotated[str, "foobar comment", InjectedToolArg()] = Field(1821        ..., description="123"1822    )182318241825class InjectedToolWithSchema(BaseTool):1826    name: str = "foo"1827    description: str = "foo."1828    args_schema: type[BaseModel] = fooSchema18291830    @override1831    def _run(self, x: int, y: str) -> Any:1832        return y183318341835@tool("foo", args_schema=fooSchema)1836def injected_tool_with_schema(x: int, y: str) -> str:1837    return y183818391840@pytest.mark.parametrize("tool_", [InjectedTool()])1841def test_tool_injected_arg_without_schema(tool_: BaseTool) -> None:1842    assert _schema(tool_.get_input_schema()) == {1843        "title": "foo",1844        "description": "Foo.\n\nArgs:\n    x: abc\n    y: 123",1845        "type": "object",1846        "properties": {1847            "x": {"title": "X", "type": "integer"},1848            "y": {"title": "Y", "type": "string"},1849        },1850        "required": ["x", "y"],1851    }1852    assert _schema(tool_.tool_call_schema) == {1853        "title": "foo",1854        "description": "foo.",1855        "type": "object",1856        "properties": {"x": {"title": "X", "type": "integer"}},1857        "required": ["x"],1858    }1859    assert tool_.invoke({"x": 5, "y": "bar"}) == "bar"1860    assert tool_.invoke(1861        {1862            "name": "foo",1863            "args": {"x": 5, "y": "bar"},1864            "id": "123",1865            "type": "tool_call",1866        }1867    ) == ToolMessage("bar", tool_call_id="123", name="foo")1868    expected_error = (1869        ValidationError if not isinstance(tool_, InjectedTool) else TypeError1870    )1871    with pytest.raises(expected_error):1872        tool_.invoke({"x": 5})18731874    assert convert_to_openai_function(tool_) == {1875        "name": "foo",1876        "description": "foo.",1877        "parameters": {1878            "type": "object",1879            "properties": {"x": {"type": "integer"}},1880            "required": ["x"],1881        },1882    }188318841885@pytest.mark.parametrize(1886    "tool_",1887    [injected_tool_with_schema, InjectedToolWithSchema()],1888)1889def test_tool_injected_arg_with_schema(tool_: BaseTool) -> None:1890    assert _schema(tool_.get_input_schema()) == {1891        "title": "fooSchema",1892        "description": "foo.",1893        "type": "object",1894        "properties": {1895            "x": {"description": "abc", "title": "X", "type": "integer"},1896            "y": {"description": "123", "title": "Y", "type": "string"},1897        },1898        "required": ["x", "y"],1899    }1900    assert _schema(tool_.tool_call_schema) == {1901        "title": "foo",1902        "description": "foo.",1903        "type": "object",1904        "properties": {"x": {"description": "abc", "title": "X", "type": "integer"}},1905        "required": ["x"],1906    }1907    assert tool_.invoke({"x": 5, "y": "bar"}) == "bar"1908    assert tool_.invoke(1909        {1910            "name": "foo",1911            "args": {"x": 5, "y": "bar"},1912            "id": "123",1913            "type": "tool_call",1914        }1915    ) == ToolMessage("bar", tool_call_id="123", name="foo")1916    expected_error = (1917        ValidationError if not isinstance(tool_, InjectedTool) else TypeError1918    )1919    with pytest.raises(expected_error):1920        tool_.invoke({"x": 5})19211922    assert convert_to_openai_function(tool_) == {1923        "name": "foo",1924        "description": "foo.",1925        "parameters": {1926            "type": "object",1927            "properties": {"x": {"type": "integer", "description": "abc"}},1928            "required": ["x"],1929        },1930    }193119321933def test_tool_injected_arg() -> None:1934    tool_ = injected_tool1935    assert _schema(tool_.get_input_schema()) == {1936        "title": "foo",1937        "description": "Foo.",1938        "type": "object",1939        "properties": {1940            "x": {"description": "abc", "title": "X", "type": "integer"},1941            "y": {"description": "123", "title": "Y", "type": "string"},1942        },1943        "required": ["x", "y"],1944    }1945    assert _schema(tool_.tool_call_schema) == {1946        "title": "foo",1947        "description": "Foo.",1948        "type": "object",1949        "properties": {"x": {"description": "abc", "title": "X", "type": "integer"}},1950        "required": ["x"],1951    }1952    assert tool_.invoke({"x": 5, "y": "bar"}) == "bar"1953    assert tool_.invoke(1954        {1955            "name": "foo",1956            "args": {"x": 5, "y": "bar"},1957            "id": "123",1958            "type": "tool_call",1959        }1960    ) == ToolMessage("bar", tool_call_id="123", name="foo")1961    expected_error = (1962        ValidationError if not isinstance(tool_, InjectedTool) else TypeError1963    )1964    with pytest.raises(expected_error):1965        tool_.invoke({"x": 5})19661967    assert convert_to_openai_function(tool_) == {1968        "name": "foo",1969        "description": "Foo.",1970        "parameters": {1971            "type": "object",1972            "properties": {"x": {"type": "integer", "description": "abc"}},1973            "required": ["x"],1974        },1975    }197619771978def test_tool_inherited_injected_arg() -> None:1979    class BarSchema(BaseModel):1980        """bar."""19811982        y: Annotated[str, "foobar comment", InjectedToolArg()] = Field(1983            ..., description="123"1984        )19851986    class FooSchema(BarSchema):1987        """foo."""19881989        x: int = Field(..., description="abc")19901991    class InheritedInjectedArgTool(BaseTool):1992        name: str = "foo"1993        description: str = "foo."1994        args_schema: type[BaseModel] = FooSchema19951996        @override1997        def _run(self, x: int, y: str) -> Any:1998            return y19992000    tool_ = InheritedInjectedArgTool()

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.