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