diff --git a/libs/partners/anthropic/langchain_anthropic/chat_models.py b/libs/partners/anthropic/langchain_anthropic/chat_models.py index fd64b824a8d..842d30ae00d 100644 --- a/libs/partners/anthropic/langchain_anthropic/chat_models.py +++ b/libs/partners/anthropic/langchain_anthropic/chat_models.py @@ -819,6 +819,7 @@ class ChatAnthropic(BaseChatModel): tool_choice: Optional[ Union[Dict[str, str], Literal["any", "auto"], str] ] = None, + parallel_tool_calls: Optional[bool] = None, **kwargs: Any, ) -> Runnable[LanguageModelInput, BaseMessage]: r"""Bind tool-like objects to this chat model. @@ -832,6 +833,10 @@ class ChatAnthropic(BaseChatModel): - name of the tool as a string or as dict ``{"type": "tool", "name": "<>"}``: calls corresponding tool; - ``"auto"``, ``{"type: "auto"}``, or None: automatically selects a tool (including no tool); - ``"any"`` or ``{"type: "any"}``: force at least one tool to be called; + parallel_tool_calls: Set to ``False`` to disable parallel tool use. + Defaults to ``None`` (no specification, which allows parallel tool use). + + .. versionadded:: 0.3.2 kwargs: Any additional parameters are passed directly to :meth:`~langchain_anthropic.chat_models.ChatAnthropic.bind`. @@ -968,6 +973,19 @@ class ChatAnthropic(BaseChatModel): f"Unrecognized 'tool_choice' type {tool_choice=}. Expected dict, " f"str, or None." ) + + if parallel_tool_calls is not None: + disable_parallel_tool_use = not parallel_tool_calls + if "tool_choice" in kwargs: + kwargs["tool_choice"]["disable_parallel_tool_use"] = ( + disable_parallel_tool_use + ) + else: + kwargs["tool_choice"] = { + "type": "any", + "disable_parallel_tool_use": disable_parallel_tool_use, + } + return self.bind(tools=formatted_tools, **kwargs) def with_structured_output( diff --git a/libs/partners/anthropic/tests/integration_tests/test_chat_models.py b/libs/partners/anthropic/tests/integration_tests/test_chat_models.py index 9f2eaba4554..3da7bb94f9c 100644 --- a/libs/partners/anthropic/tests/integration_tests/test_chat_models.py +++ b/libs/partners/anthropic/tests/integration_tests/test_chat_models.py @@ -444,6 +444,25 @@ def test_tool_use() -> None: assert len(chunks) > 1 +class GenerateUsername(BaseModel): + "Get a username based on someone's name and hair color." + + name: str + hair_color: str + + +def test_disable_parallel_tool_calling() -> None: + llm = ChatAnthropic(model="claude-3-5-sonnet-20241022") + llm_with_tools = llm.bind_tools([GenerateUsername], parallel_tool_calls=False) + result = llm_with_tools.invoke( + "Use the GenerateUsername tool to generate user names for:\n\n" + "Sally with green hair\n" + "Bob with blue hair" + ) + assert isinstance(result, AIMessage) + assert len(result.tool_calls) == 1 + + def test_anthropic_with_empty_text_block() -> None: """Anthropic SDK can return an empty text block.""" diff --git a/libs/partners/openai/tests/integration_tests/chat_models/test_base.py b/libs/partners/openai/tests/integration_tests/chat_models/test_base.py index 49d894603d7..525020192ba 100644 --- a/libs/partners/openai/tests/integration_tests/chat_models/test_base.py +++ b/libs/partners/openai/tests/integration_tests/chat_models/test_base.py @@ -630,6 +630,18 @@ def test_bind_tools_tool_choice() -> None: assert not msg.tool_calls +def test_disable_parallel_tool_calling() -> None: + llm = ChatOpenAI(model="gpt-4o-mini") + llm_with_tools = llm.bind_tools([GenerateUsername], parallel_tool_calls=False) + result = llm_with_tools.invoke( + "Use the GenerateUsername tool to generate user names for:\n\n" + "Sally with green hair\n" + "Bob with blue hair" + ) + assert isinstance(result, AIMessage) + assert len(result.tool_calls) == 1 + + @pytest.mark.parametrize("model", ["gpt-4o-mini", "o1"]) def test_openai_structured_output(model: str) -> None: class MyModel(BaseModel):