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