diff --git a/libs/partners/anthropic/langchain_anthropic/chat_models.py b/libs/partners/anthropic/langchain_anthropic/chat_models.py index b9d410d8288..42a9117ab7d 100644 --- a/libs/partners/anthropic/langchain_anthropic/chat_models.py +++ b/libs/partners/anthropic/langchain_anthropic/chat_models.py @@ -172,50 +172,45 @@ def _format_messages(messages: List[BaseMessage]) -> Tuple[Optional[str], List[D content = [] for item in message.content: if isinstance(item, str): - content.append( - { - "type": "text", - "text": item, - } - ) + content.append({"type": "text", "text": item}) elif isinstance(item, dict): if "type" not in item: raise ValueError("Dict content item must have a type key") elif item["type"] == "image_url": # convert format source = _format_image(item["image_url"]["url"]) - content.append( - { - "type": "image", - "source": source, - } - ) + content.append({"type": "image", "source": source}) elif item["type"] == "tool_use": - item.pop("text", None) - content.append(item) + # If a tool_call with the same id as a tool_use content block + # exists, the tool_call is preferred. + if isinstance(message, AIMessage) and item["id"] in [ + tc["id"] for tc in message.tool_calls + ]: + overlapping = [ + tc + for tc in message.tool_calls + if tc["id"] == item["id"] + ] + content.extend( + _lc_tool_calls_to_anthropic_tool_use_blocks(overlapping) + ) + else: + item.pop("text", None) + content.append(item) elif item["type"] == "text": text = item.get("text", "") # Only add non-empty strings for now as empty ones are not # accepted. # https://github.com/anthropics/anthropic-sdk-python/issues/461 if text.strip(): - content.append( - { - "type": "text", - "text": text, - } - ) + content.append({"type": "text", "text": text}) else: content.append(item) else: raise ValueError( f"Content items must be str or dict, instead was: {type(item)}" ) - elif ( - isinstance(message, AIMessage) - and not isinstance(message.content, list) - and message.tool_calls - ): + elif isinstance(message, AIMessage) and message.tool_calls: content = ( [] if not message.content @@ -228,12 +223,7 @@ def _format_messages(messages: List[BaseMessage]) -> Tuple[Optional[str], List[D else: content = message.content - formatted_messages.append( - { - "role": role, - "content": content, - } - ) + formatted_messages.append({"role": role, "content": content}) return system, formatted_messages diff --git a/libs/partners/anthropic/tests/unit_tests/test_chat_models.py b/libs/partners/anthropic/tests/unit_tests/test_chat_models.py index d9e382b6cf2..3c5c5b2691c 100644 --- a/libs/partners/anthropic/tests/unit_tests/test_chat_models.py +++ b/libs/partners/anthropic/tests/unit_tests/test_chat_models.py @@ -352,10 +352,7 @@ def test__format_messages_with_str_content_and_tool_calls() -> None: "thought", tool_calls=[{"name": "bar", "id": "1", "args": {"baz": "buzz"}}], ) - tool = ToolMessage( - "blurb", - tool_call_id="1", - ) + tool = ToolMessage("blurb", tool_call_id="1") messages = [system, human, ai, tool] expected = ( "fuzz", @@ -364,10 +361,7 @@ def test__format_messages_with_str_content_and_tool_calls() -> None: { "role": "assistant", "content": [ - { - "type": "text", - "text": "thought", - }, + {"type": "text", "text": "thought"}, { "type": "tool_use", "name": "bar", @@ -394,12 +388,7 @@ def test__format_messages_with_list_content_and_tool_calls() -> None: # If content and tool_calls are specified and content is a list, then content is # preferred. ai = AIMessage( - [ - { - "type": "text", - "text": "thought", - } - ], + [{"type": "text", "text": "thought"}], tool_calls=[{"name": "bar", "id": "1", "args": {"baz": "buzz"}}], ) tool = ToolMessage( @@ -413,11 +402,53 @@ def test__format_messages_with_list_content_and_tool_calls() -> None: {"role": "user", "content": "foo"}, { "role": "assistant", + "content": [{"type": "text", "text": "thought"}], + }, + { + "role": "user", "content": [ + {"type": "tool_result", "content": "blurb", "tool_use_id": "1"} + ], + }, + ], + ) + actual = _format_messages(messages) + assert expected == actual + + +def test__format_messages_with_tool_use_blocks_and_tool_calls() -> None: + """Show that tool_calls are preferred to tool_use blocks when both have same id.""" + system = SystemMessage("fuzz") + human = HumanMessage("foo") + # NOTE: tool_use block in contents and tool_calls have different arguments. + ai = AIMessage( + [ + {"type": "text", "text": "thought"}, + { + "type": "tool_use", + "name": "bar", + "id": "1", + "input": {"baz": "NOT_BUZZ"}, + }, + ], + tool_calls=[{"name": "bar", "id": "1", "args": {"baz": "BUZZ"}}], + ) + tool = ToolMessage("blurb", tool_call_id="1") + messages = [system, human, ai, tool] + expected = ( + "fuzz", + [ + {"role": "user", "content": "foo"}, + { + "role": "assistant", + "content": [ + {"type": "text", "text": "thought"}, { - "type": "text", - "text": "thought", - } + "type": "tool_use", + "name": "bar", + "id": "1", + "input": {"baz": "BUZZ"}, # tool_calls value preferred. + }, ], }, {