Skip to content

Commit 11a2efe

Browse files
authored
fix(anthropic): handle empty AIMessage (#33390)
1 parent d8a680e commit 11a2efe

File tree

3 files changed

+83
-0
lines changed

3 files changed

+83
-0
lines changed

libs/partners/anthropic/langchain_anthropic/chat_models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,10 @@ def _format_messages(
549549
_lc_tool_calls_to_anthropic_tool_use_blocks(missing_tool_calls),
550550
)
551551

552+
if not content and role == "assistant" and _i < len(merged_messages) - 1:
553+
# anthropic.BadRequestError: Error code: 400: all messages must have
554+
# non-empty content except for the optional final assistant message
555+
continue
552556
formatted_messages.append({"role": role, "content": content})
553557
return system, formatted_messages
554558

libs/partners/anthropic/tests/integration_tests/test_chat_models.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,53 @@ def test_system_invoke() -> None:
278278
assert isinstance(result.content, str)
279279

280280

281+
def test_handle_empty_aimessage() -> None:
282+
# Anthropic can generate empty AIMessages, which are not valid unless in the last
283+
# message in a sequence.
284+
llm = ChatAnthropic(model=MODEL_NAME)
285+
messages = [
286+
HumanMessage("Hello"),
287+
AIMessage([]),
288+
HumanMessage("My name is Bob."),
289+
]
290+
_ = llm.invoke(messages)
291+
292+
# Test tool call sequence
293+
llm_with_tools = llm.bind_tools(
294+
[
295+
{
296+
"name": "get_weather",
297+
"description": "Get weather report for a city",
298+
"input_schema": {
299+
"type": "object",
300+
"properties": {"location": {"type": "string"}},
301+
},
302+
},
303+
],
304+
)
305+
_ = llm_with_tools.invoke(
306+
[
307+
HumanMessage("What's the weather in Boston?"),
308+
AIMessage(
309+
content=[],
310+
tool_calls=[
311+
{
312+
"name": "get_weather",
313+
"args": {"location": "Boston"},
314+
"id": "toolu_01V6d6W32QGGSmQm4BT98EKk",
315+
"type": "tool_call",
316+
},
317+
],
318+
),
319+
ToolMessage(
320+
content="It's sunny.", tool_call_id="toolu_01V6d6W32QGGSmQm4BT98EKk"
321+
),
322+
AIMessage([]),
323+
HumanMessage("Thanks!"),
324+
]
325+
)
326+
327+
281328
def test_anthropic_call() -> None:
282329
"""Test valid call to anthropic."""
283330
chat = ChatAnthropic(model=MODEL_NAME) # type: ignore[call-arg]

libs/partners/anthropic/tests/unit_tests/test_chat_models.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,38 @@ def test__format_messages_with_tool_calls() -> None:
642642
actual = _format_messages(messages)
643643
assert expected == actual
644644

645+
# Check handling of empty AIMessage
646+
empty_contents: list[str | list[str | dict]] = ["", []]
647+
for empty_content in empty_contents:
648+
## Permit message in final position
649+
_, anthropic_messages = _format_messages([human, AIMessage(empty_content)])
650+
expected_messages = [
651+
{"role": "user", "content": "foo"},
652+
{"role": "assistant", "content": empty_content},
653+
]
654+
assert expected_messages == anthropic_messages
655+
656+
## Remove message otherwise
657+
_, anthropic_messages = _format_messages(
658+
[human, AIMessage(empty_content), human]
659+
)
660+
expected_messages = [
661+
{"role": "user", "content": "foo"},
662+
{"role": "user", "content": "foo"},
663+
]
664+
assert expected_messages == anthropic_messages
665+
666+
actual = _format_messages(
667+
[system, human, ai, tool, AIMessage(empty_content), human]
668+
)
669+
assert actual[0] == "fuzz"
670+
assert [message["role"] for message in actual[1]] == [
671+
"user",
672+
"assistant",
673+
"user",
674+
"user",
675+
]
676+
645677

646678
def test__format_tool_use_block() -> None:
647679
# Test we correctly format tool_use blocks when there is no corresponding tool_call.

0 commit comments

Comments
 (0)