From 6fc753e751b8abbe5916f34589243e8d3c8ab9b7 Mon Sep 17 00:00:00 2001 From: Michael Chin Date: Wed, 6 Aug 2025 21:51:48 -0700 Subject: [PATCH 1/2] Support multiple system messages for Anthropic --- libs/aws/langchain_aws/chat_models/bedrock.py | 13 +++++++--- .../chat_models/bedrock_converse.py | 4 +-- .../unit_tests/chat_models/test_bedrock.py | 26 ++++++++++++++++++- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/libs/aws/langchain_aws/chat_models/bedrock.py b/libs/aws/langchain_aws/chat_models/bedrock.py index 914b1800..3b63cbc9 100644 --- a/libs/aws/langchain_aws/chat_models/bedrock.py +++ b/libs/aws/langchain_aws/chat_models/bedrock.py @@ -362,7 +362,10 @@ def _merge_messages( ] ) last = merged[-1] if merged else None - if isinstance(last, HumanMessage) and isinstance(curr, HumanMessage): + if any( + all(isinstance(m, c) for m in (curr, last)) + for c in (SystemMessage, HumanMessage) + ): if isinstance(last.content, str): new_content: List = [{"type": "text", "text": last.content}] else: @@ -387,9 +390,11 @@ def _format_anthropic_messages( merged_messages = _merge_messages(messages) for i, message in enumerate(merged_messages): if message.type == "system": - if i != 0: - raise ValueError("System message must be at beginning of message list.") - if isinstance(message.content, str): + if system is not None: + raise ValueError( + "Received multiple non-consecutive system messages." + ) + elif isinstance(message.content, str): system = message.content elif isinstance(message.content, list): system_blocks = [] diff --git a/libs/aws/langchain_aws/chat_models/bedrock_converse.py b/libs/aws/langchain_aws/chat_models/bedrock_converse.py index 815e188d..1ba55375 100644 --- a/libs/aws/langchain_aws/chat_models/bedrock_converse.py +++ b/libs/aws/langchain_aws/chat_models/bedrock_converse.py @@ -1037,8 +1037,8 @@ def _messages_to_bedrock( """Handle Bedrock converse and Anthropic style content blocks""" bedrock_messages: List[Dict[str, Any]] = [] bedrock_system: List[Dict[str, Any]] = [] - # Merge system, human, ai message runs because Anthropic expects (at most) 1 - # system message then alternating human/ai messages. + # Merge system, human, ai message runs because Anthropic expects + # (optional) system messages first, then alternating human/ai messages. messages = merge_message_runs(messages) for msg in messages: content = _lc_content_to_bedrock(msg.content) diff --git a/libs/aws/tests/unit_tests/chat_models/test_bedrock.py b/libs/aws/tests/unit_tests/chat_models/test_bedrock.py index e184c514..f7a047e9 100644 --- a/libs/aws/tests/unit_tests/chat_models/test_bedrock.py +++ b/libs/aws/tests/unit_tests/chat_models/test_bedrock.py @@ -26,6 +26,7 @@ def test__merge_messages() -> None: messages = [ SystemMessage("foo"), # type: ignore[misc] + SystemMessage("barfoo"), # type: ignore[misc] HumanMessage("bar"), # type: ignore[misc] AIMessage( # type: ignore[misc] [ @@ -52,7 +53,12 @@ def test__merge_messages() -> None: HumanMessage("next thing"), # type: ignore[misc] ] expected = [ - SystemMessage("foo"), # type: ignore[misc] + SystemMessage( + [ + {'type': 'text', 'text': 'foo'}, + {'type': 'text', 'text': 'barfoo'} + ] + ), # type: ignore[misc] HumanMessage("bar"), # type: ignore[misc] AIMessage( # type: ignore[misc] [ @@ -345,6 +351,24 @@ def test__format_anthropic_messages_system_message_list_content() -> None: actual = _format_anthropic_messages(messages) assert expected == actual +def test__format_anthropic_multiple_system_messages() -> None: + """Test that multiple system messages can be passed, and that none of them are required to be at position 0.""" + system1 = SystemMessage("foo") # type: ignore[misc] + system2 = SystemMessage("bar") # type: ignore[misc] + human = HumanMessage("Hello!") + messages = [human, system1, system2] + expected_system = [ + {'text': 'foo', 'type': 'text'}, + {'text': 'bar', 'type': 'text'} + ] + expected_messages = [ + {"role": "user", "content": "Hello!"} + ] + + actual_system, actual_messages = _format_anthropic_messages(messages) + assert expected_system == actual_system + assert expected_messages == actual_messages + @pytest.fixture() def pydantic() -> Type[BaseModel]: From e53214e30ea0f7ed15b00de0fa4b6a2894dc38da Mon Sep 17 00:00:00 2001 From: Michael Chin Date: Mon, 18 Aug 2025 18:46:49 -0700 Subject: [PATCH 2/2] Add test for non-consecutive messages --- libs/aws/tests/unit_tests/chat_models/test_bedrock.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/libs/aws/tests/unit_tests/chat_models/test_bedrock.py b/libs/aws/tests/unit_tests/chat_models/test_bedrock.py index f7a047e9..1becf9a9 100644 --- a/libs/aws/tests/unit_tests/chat_models/test_bedrock.py +++ b/libs/aws/tests/unit_tests/chat_models/test_bedrock.py @@ -369,6 +369,16 @@ def test__format_anthropic_multiple_system_messages() -> None: assert expected_system == actual_system assert expected_messages == actual_messages +def test__format_anthropic_nonconsecutive_system_messages() -> None: + """Test that we fail when non-consecutive system messages are passed.""" + system1 = SystemMessage("foo") # type: ignore[misc] + system2 = SystemMessage("bar") # type: ignore[misc] + human = HumanMessage("Hello!") + messages = [system1, human, system2] + + with pytest.raises(ValueError, match="Received multiple non-consecutive system messages."): + _format_anthropic_messages(messages) + @pytest.fixture() def pydantic() -> Type[BaseModel]: