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