From 480d826d7de0d23adc8d0e1611df374fad5a6e81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20C=C3=A9l=C3=A9rier?= Date: Tue, 13 May 2025 17:28:14 +0200 Subject: [PATCH 01/10] fix: fix empty content block with pydantic ai --- pydantic_ai_slim/pydantic_ai/models/bedrock.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pydantic_ai_slim/pydantic_ai/models/bedrock.py b/pydantic_ai_slim/pydantic_ai/models/bedrock.py index 3908f2812..6776a0b80 100644 --- a/pydantic_ai_slim/pydantic_ai/models/bedrock.py +++ b/pydantic_ai_slim/pydantic_ai/models/bedrock.py @@ -423,6 +423,8 @@ async def _map_messages( content: list[ContentBlockOutputTypeDef] = [] for item in message.parts: if isinstance(item, TextPart): + if not item.content.strip(): + continue content.append({'text': item.content}) else: assert isinstance(item, ToolCallPart) From df9cca87df94fa70421616e30ea332ac7588536d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20C=C3=A9l=C3=A9rier?= Date: Tue, 13 May 2025 17:39:45 +0200 Subject: [PATCH 02/10] fix: run pre-commit From 385c44c73917de808409e47fd2aece96c1b835cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20C=C3=A9l=C3=A9rier?= Date: Fri, 16 May 2025 15:22:33 +0200 Subject: [PATCH 03/10] fix: fix the ci --- .../pydantic_ai/models/bedrock.py | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/pydantic_ai_slim/pydantic_ai/models/bedrock.py b/pydantic_ai_slim/pydantic_ai/models/bedrock.py index 6776a0b80..d3fb56c10 100644 --- a/pydantic_ai_slim/pydantic_ai/models/bedrock.py +++ b/pydantic_ai_slim/pydantic_ai/models/bedrock.py @@ -366,6 +366,22 @@ def _map_inference_config( return inference_config + def _map_model_response_messages(self, message: ModelResponse) -> list[ContentBlockOutputTypeDef]: + """Maps a `pydantic_ai.ModelResponse` to the Bedrock `ContentBlockOutputTypeDef`. + + This is used to map the model response to the Bedrock format. + """ + items: list[ContentBlockOutputTypeDef] = [] + for item in message.parts: + if isinstance(item, TextPart): + if not item.content.strip(): + continue + items.append({'text': item.content}) + else: + assert isinstance(item, ToolCallPart) + items.append(self._map_tool_call(item)) + return items + async def _map_messages( self, messages: list[ModelMessage] ) -> tuple[list[SystemContentBlockTypeDef], list[MessageUnionTypeDef]]: @@ -420,15 +436,7 @@ async def _map_messages( } ) elif isinstance(message, ModelResponse): - content: list[ContentBlockOutputTypeDef] = [] - for item in message.parts: - if isinstance(item, TextPart): - if not item.content.strip(): - continue - content.append({'text': item.content}) - else: - assert isinstance(item, ToolCallPart) - content.append(self._map_tool_call(item)) + content: list[ContentBlockOutputTypeDef] = self._map_model_response_messages(message) bedrock_messages.append({'role': 'assistant', 'content': content}) else: assert_never(message) From 909d6edee06c8ea799ce39b6a3aba45ee657b5f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20C=C3=A9l=C3=A9rier?= Date: Fri, 16 May 2025 15:23:40 +0200 Subject: [PATCH 04/10] fix: rename method --- pydantic_ai_slim/pydantic_ai/models/bedrock.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pydantic_ai_slim/pydantic_ai/models/bedrock.py b/pydantic_ai_slim/pydantic_ai/models/bedrock.py index d3fb56c10..a7aa6df66 100644 --- a/pydantic_ai_slim/pydantic_ai/models/bedrock.py +++ b/pydantic_ai_slim/pydantic_ai/models/bedrock.py @@ -366,7 +366,7 @@ def _map_inference_config( return inference_config - def _map_model_response_messages(self, message: ModelResponse) -> list[ContentBlockOutputTypeDef]: + def _map_model_response(self, message: ModelResponse) -> list[ContentBlockOutputTypeDef]: """Maps a `pydantic_ai.ModelResponse` to the Bedrock `ContentBlockOutputTypeDef`. This is used to map the model response to the Bedrock format. @@ -436,7 +436,7 @@ async def _map_messages( } ) elif isinstance(message, ModelResponse): - content: list[ContentBlockOutputTypeDef] = self._map_model_response_messages(message) + content: list[ContentBlockOutputTypeDef] = self._map_model_response(message) bedrock_messages.append({'role': 'assistant', 'content': content}) else: assert_never(message) From f13b7cfafd3a237a396587fc189f6b8a6863e1cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20C=C3=A9l=C3=A9rier?= Date: Tue, 13 May 2025 17:39:45 +0200 Subject: [PATCH 05/10] fix: run pre-commit From 0917adc2f9737f5b7e656008f371571d0e55508b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20C=C3=A9l=C3=A9rier?= Date: Fri, 16 May 2025 15:28:50 +0200 Subject: [PATCH 06/10] fix: rename variables --- pydantic_ai_slim/pydantic_ai/models/bedrock.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pydantic_ai_slim/pydantic_ai/models/bedrock.py b/pydantic_ai_slim/pydantic_ai/models/bedrock.py index a7aa6df66..b6cd3f0dc 100644 --- a/pydantic_ai_slim/pydantic_ai/models/bedrock.py +++ b/pydantic_ai_slim/pydantic_ai/models/bedrock.py @@ -371,16 +371,16 @@ def _map_model_response(self, message: ModelResponse) -> list[ContentBlockOutput This is used to map the model response to the Bedrock format. """ - items: list[ContentBlockOutputTypeDef] = [] + content: list[ContentBlockOutputTypeDef] = [] for item in message.parts: if isinstance(item, TextPart): if not item.content.strip(): continue - items.append({'text': item.content}) + content.append({'text': item.content}) else: assert isinstance(item, ToolCallPart) - items.append(self._map_tool_call(item)) - return items + content.append(self._map_tool_call(item)) + return content async def _map_messages( self, messages: list[ModelMessage] From 168bd72082466aa31d932e0bab0486d4c2ed7047 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20C=C3=A9l=C3=A9rier?= Date: Fri, 16 May 2025 15:57:13 +0200 Subject: [PATCH 07/10] feat: add new test --- tests/models/test_bedrock.py | 57 ++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/models/test_bedrock.py b/tests/models/test_bedrock.py index 6ef3f177c..008d645f6 100644 --- a/tests/models/test_bedrock.py +++ b/tests/models/test_bedrock.py @@ -629,3 +629,60 @@ async def test_bedrock_group_consecutive_tool_return_parts(bedrock_provider: Bed }, ] ) + + +async def test_bedrock_handles_empty_content_blocks(bedrock_provider: BedrockProvider): + """ + Test that the Bedrock provider correctly handles empty content blocks without errors. + This test verifies that empty content in UserPromptPart, TextPart, and ToolReturnPart + are processed properly without causing issues. + """ + model = BedrockConverseModel('us.amazon.nova-micro-v1:0', provider=bedrock_provider) + now = datetime.datetime.now() + + req = [ + ModelRequest(parts=[UserPromptPart(content=['Please use the tool'])]), + ModelResponse( + parts=[ + TextPart(content=""" """), + ToolCallPart( + tool_name='normal_tool', + args={'param': 'value'}, + tool_call_id='normal1', + ), + ] + ), + ModelRequest( + parts=[ + ToolReturnPart(tool_name='normal_tool', content='result', tool_call_id='normal1', timestamp=now), + ] + ), + ] + + _, bedrock_messages = await model._map_messages(req) # type: ignore[reportPrivateUsage] + + assert bedrock_messages == snapshot( + [ + {'role': 'user', 'content': [{'text': 'Please use the tool'}]}, + { + 'role': 'assistant', + 'content': [ + { + 'tool_calls': [ + { + 'id': 'normal1', + 'type': 'function', + 'function': {'name': 'normal_tool', 'arguments': {'param': 'value'}}, + } + ] + } + ], + }, + { + 'role': 'user', + 'content': [ + {'toolResult': {'toolUseId': 'normal1', 'content': [{'text': 'result'}], 'status': 'success'}} + ], + }, + ] + ) From 518e84a8904787b5b05c76b1266ede8c1a36c713 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20C=C3=A9l=C3=A9rier?= Date: Fri, 16 May 2025 16:16:49 +0200 Subject: [PATCH 08/10] feat: add new test --- tests/models/test_bedrock.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/tests/models/test_bedrock.py b/tests/models/test_bedrock.py index 008d645f6..c50e1d478 100644 --- a/tests/models/test_bedrock.py +++ b/tests/models/test_bedrock.py @@ -668,14 +668,19 @@ async def test_bedrock_handles_empty_content_blocks(bedrock_provider: BedrockPro 'role': 'assistant', 'content': [ { - 'tool_calls': [ - { + 'toolUse': { + 'input': { + 'function': { + 'arguments': {'param': 'value'}, + 'name': 'normal_tool', + }, 'id': 'normal1', 'type': 'function', - 'function': {'name': 'normal_tool', 'arguments': {'param': 'value'}}, - } - ] - } + }, + 'name': 'normal_tool', + 'toolUseId': 'normal1', + }, + }, ], }, { From ac248dfc77bdeae04ad550fa693012241f73a468 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20C=C3=A9l=C3=A9rier?= Date: Fri, 16 May 2025 16:20:50 +0200 Subject: [PATCH 09/10] feat: fix test --- tests/models/test_bedrock.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/models/test_bedrock.py b/tests/models/test_bedrock.py index c50e1d478..452106473 100644 --- a/tests/models/test_bedrock.py +++ b/tests/models/test_bedrock.py @@ -671,7 +671,9 @@ async def test_bedrock_handles_empty_content_blocks(bedrock_provider: BedrockPro 'toolUse': { 'input': { 'function': { - 'arguments': {'param': 'value'}, + 'arguments': { + 'param': 'value', + }, 'name': 'normal_tool', }, 'id': 'normal1', From ebe5483cb2ac5e546f67fa897a9038b2ce27c5ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20C=C3=A9l=C3=A9rier?= Date: Fri, 16 May 2025 16:33:07 +0200 Subject: [PATCH 10/10] feat: fix test --- tests/models/test_bedrock.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/tests/models/test_bedrock.py b/tests/models/test_bedrock.py index 452106473..03813bc8e 100644 --- a/tests/models/test_bedrock.py +++ b/tests/models/test_bedrock.py @@ -670,14 +670,7 @@ async def test_bedrock_handles_empty_content_blocks(bedrock_provider: BedrockPro { 'toolUse': { 'input': { - 'function': { - 'arguments': { - 'param': 'value', - }, - 'name': 'normal_tool', - }, - 'id': 'normal1', - 'type': 'function', + 'param': 'value', }, 'name': 'normal_tool', 'toolUseId': 'normal1',