From fb6d5e1917dd28def068821d86356e602aff446a Mon Sep 17 00:00:00 2001 From: Ayaan Gazali Date: Fri, 25 Sep 2026 03:50:09 -0700 Subject: [PATCH] fix(llms/aws_bedrock): iterate Converse content blocks for Anthropic text (#6369) --- mem0/llms/aws_bedrock.py | 15 ++++++++--- tests/llms/test_aws_bedrock.py | 48 ++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/mem0/llms/aws_bedrock.py b/mem0/llms/aws_bedrock.py index 20b7f3883..7fabbd22d 100644 --- a/mem0/llms/aws_bedrock.py +++ b/mem0/llms/aws_bedrock.py @@ -579,11 +579,20 @@ class AWSBedrockLLM(LLMBase): # Use converse API for Anthropic models response = self.client.converse(**converse_params) - # Parse Converse API response + # Parse Converse API response. Claude reasoning models can emit a + # `reasoningContent` block before the `text` block, so iterate to + # find the first block that carries text instead of indexing + # content[0] (same approach as the MiniMax branch below). if hasattr(response, 'output') and hasattr(response.output, 'message'): - return response.output.message.content[0].text + for block in response.output.message.content: + if hasattr(block, 'text'): + return block.text + return "" elif 'output' in response and 'message' in response['output']: - return response['output']['message']['content'][0]['text'] + for block in response['output']['message']['content']: + if 'text' in block: + return block['text'] + return "" else: return str(response) diff --git a/tests/llms/test_aws_bedrock.py b/tests/llms/test_aws_bedrock.py index f683f8c3f..ecb960d09 100644 --- a/tests/llms/test_aws_bedrock.py +++ b/tests/llms/test_aws_bedrock.py @@ -506,3 +506,51 @@ class TestParseResponseLegacy: response = {"body": body} result = llm._parse_response(response, tools=None) assert result == "hello from ai21" + + +class TestAnthropicConverseContentParsing: + """The Anthropic Converse branch must not assume content[0] is the text + block: Claude reasoning models emit a reasoningContent block before the + text block, and some stop conditions produce an empty content array. The + parser iterates for the first block carrying text, like the MiniMax branch. + """ + + def test_text_after_reasoning_content_block(self, mock_boto3): + mock_boto3.converse.return_value = { + "output": { + "message": { + "content": [ + {"reasoningContent": {"reasoningText": {"text": "step by step..."}}}, + {"text": "final answer"}, + ] + } + } + } + llm = _make_llm("anthropic.claude-3-5-sonnet-20240620-v1:0", mock_boto3) + + assert llm.generate_response(MESSAGES) == "final answer" + + def test_empty_content_returns_empty_string(self, mock_boto3): + mock_boto3.converse.return_value = {"output": {"message": {"content": []}}} + llm = _make_llm("anthropic.claude-3-5-sonnet-20240620-v1:0", mock_boto3) + + assert llm.generate_response(MESSAGES) == "" + + def test_plain_text_content_still_returned(self, mock_boto3): + mock_boto3.converse.return_value = _converse_response("plain answer") + llm = _make_llm("anthropic.claude-3-5-sonnet-20240620-v1:0", mock_boto3) + + assert llm.generate_response(MESSAGES) == "plain answer" + + def test_object_style_response_iterates_blocks(self, mock_boto3): + # Defensive attr-style branch: object wrapper with a reasoning block first. + from types import SimpleNamespace + + reasoning_block = SimpleNamespace(reasoningContent={"reasoningText": {"text": "hmm"}}) + text_block = SimpleNamespace(text="object answer") + mock_boto3.converse.return_value = SimpleNamespace( + output=SimpleNamespace(message=SimpleNamespace(content=[reasoning_block, text_block])) + ) + llm = _make_llm("anthropic.claude-3-5-sonnet-20240620-v1:0", mock_boto3) + + assert llm.generate_response(MESSAGES) == "object answer"