Skip to content

Commit 44ca407

Browse files
author
Motta Kin
committed
#3621 - Pass s3:// file URLs directly to API in BedrockConverseModel
1 parent 8d111fe commit 44ca407

File tree

5 files changed

+175
-5
lines changed

5 files changed

+175
-5
lines changed

pydantic_ai_slim/pydantic_ai/models/bedrock.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -643,20 +643,25 @@ async def _map_user_prompt(part: UserPromptPart, document_count: Iterator[int])
643643
else:
644644
raise NotImplementedError('Binary content is not supported yet.')
645645
elif isinstance(item, ImageUrl | DocumentUrl | VideoUrl):
646-
downloaded_item = await download_item(item, data_format='bytes', type_format='extension')
647-
format = downloaded_item['data_type']
646+
source: dict[str, Any]
647+
if item.url.startswith('s3://'):
648+
source = {'s3Location': {'uri': item.url}}
649+
else:
650+
downloaded_item = await download_item(item, data_format='bytes', type_format='extension')
651+
source = {'bytes': downloaded_item['data']}
652+
648653
if item.kind == 'image-url':
649654
format = item.media_type.split('/')[1]
650655
assert format in ('jpeg', 'png', 'gif', 'webp'), f'Unsupported image format: {format}'
651-
image: ImageBlockTypeDef = {'format': format, 'source': {'bytes': downloaded_item['data']}}
656+
image: ImageBlockTypeDef = {'format': format, 'source': cast(Any, source)}
652657
content.append({'image': image})
653658

654659
elif item.kind == 'document-url':
655660
name = f'Document {next(document_count)}'
656661
document: DocumentBlockTypeDef = {
657662
'name': name,
658663
'format': item.format,
659-
'source': {'bytes': downloaded_item['data']},
664+
'source': cast(Any, source),
660665
}
661666
content.append({'document': document})
662667

@@ -673,7 +678,7 @@ async def _map_user_prompt(part: UserPromptPart, document_count: Iterator[int])
673678
'wmv',
674679
'three_gp',
675680
), f'Unsupported video format: {format}'
676-
video: VideoBlockTypeDef = {'format': format, 'source': {'bytes': downloaded_item['data']}}
681+
video: VideoBlockTypeDef = {'format': format, 'source': cast(Any, source)}
677682
content.append({'video': video})
678683
elif isinstance(item, AudioUrl): # pragma: no cover
679684
raise NotImplementedError('Audio is not supported yet.')
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
interactions:
2+
- request:
3+
body: '{"messages": [{"role": "user", "content": [{"text": "What is the main content on this document?"}, {"document": {"format": "pdf", "name": "test-doc.pdf", "source": {"s3Location": {"uri": "s3://my-bucket/documents/test-doc.pdf"}}}}]}], "system": [{"text": "You are a helpful chatbot."}], "inferenceConfig": {}}'
4+
headers:
5+
amz-sdk-invocation-id:
6+
- !!binary |
7+
ZGQxNWI1ODItMTk4Yy00NWZhLTllZjYtODFlY2IzZmUxNWM2
8+
amz-sdk-request:
9+
- !!binary |
10+
YXR0ZW1wdD0x
11+
content-length:
12+
- '280'
13+
content-type:
14+
- !!binary |
15+
YXBwbGljYXRpb24vanNvbg==
16+
method: POST
17+
uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-v2/converse
18+
response:
19+
headers:
20+
connection:
21+
- keep-alive
22+
content-length:
23+
- '420'
24+
content-type:
25+
- application/json
26+
parsed_body:
27+
metrics:
28+
latencyMs: 600
29+
output:
30+
message:
31+
content:
32+
- text: Based on the provided document, the main content discusses best practices for cloud storage and data management.
33+
role: assistant
34+
stopReason: end_turn
35+
usage:
36+
inputTokens: 35
37+
outputTokens: 18
38+
totalTokens: 53
39+
status:
40+
code: 200
41+
message: OK
42+
version: 1
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
interactions:
2+
- request:
3+
body: '{"messages": [{"role": "user", "content": [{"text": "What is in this image?"}, {"image": {"format": "jpeg", "source": {"s3Location": {"uri": "s3://my-bucket/images/test-image.jpg"}}}}]}], "system": [{"text": "You are a helpful chatbot."}], "inferenceConfig": {}}'
4+
headers:
5+
amz-sdk-invocation-id:
6+
- !!binary |
7+
ZGQxNWI1ODItMTk4Yy00NWZhLTllZjYtODFlY2IzZmUxNWM2
8+
amz-sdk-request:
9+
- !!binary |
10+
YXR0ZW1wdD0x
11+
content-length:
12+
- '250'
13+
content-type:
14+
- !!binary |
15+
YXBwbGljYXRpb24vanNvbg==
16+
method: POST
17+
uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/us.amazon.nova-pro-v1%3A0/converse
18+
response:
19+
headers:
20+
connection:
21+
- keep-alive
22+
content-length:
23+
- '400'
24+
content-type:
25+
- application/json
26+
parsed_body:
27+
metrics:
28+
latencyMs: 450
29+
output:
30+
message:
31+
content:
32+
- text: The image shows a scenic landscape with mountains in the background and a clear blue sky above.
33+
role: assistant
34+
stopReason: end_turn
35+
usage:
36+
inputTokens: 25
37+
outputTokens: 20
38+
totalTokens: 45
39+
status:
40+
code: 200
41+
message: OK
42+
version: 1
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
interactions:
2+
- request:
3+
body: '{"messages": [{"role": "user", "content": [{"text": "Describe this video"}, {"video": {"format": "mp4", "source": {"s3Location": {"uri": "s3://my-bucket/videos/test-video.mp4"}}}}]}], "system": [{"text": "You are a helpful chatbot."}], "inferenceConfig": {}}'
4+
headers:
5+
amz-sdk-invocation-id:
6+
- !!binary |
7+
ZGQxNWI1ODItMTk4Yy00NWZhLTllZjYtODFlY2IzZmUxNWM2
8+
amz-sdk-request:
9+
- !!binary |
10+
YXR0ZW1wdD0x
11+
content-length:
12+
- '250'
13+
content-type:
14+
- !!binary |
15+
YXBwbGljYXRpb24vanNvbg==
16+
method: POST
17+
uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/us.amazon.nova-pro-v1%3A0/converse
18+
response:
19+
headers:
20+
connection:
21+
- keep-alive
22+
content-length:
23+
- '400'
24+
content-type:
25+
- application/json
26+
parsed_body:
27+
metrics:
28+
latencyMs: 550
29+
output:
30+
message:
31+
content:
32+
- text: The video shows a time-lapse of a sunset over the ocean with waves gently rolling onto the shore.
33+
role: assistant
34+
stopReason: end_turn
35+
usage:
36+
inputTokens: 30
37+
outputTokens: 22
38+
totalTokens: 52
39+
status:
40+
code: 200
41+
message: OK
42+
version: 1

tests/models/test_bedrock.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,45 @@ async def test_text_document_url_input(allow_model_requests: None, bedrock_provi
729729
""")
730730

731731

732+
@pytest.mark.vcr()
733+
async def test_s3_image_url_input(allow_model_requests: None, bedrock_provider: BedrockProvider):
734+
"""Test that s3:// image URLs are passed directly to Bedrock API without downloading."""
735+
m = BedrockConverseModel('us.amazon.nova-pro-v1:0', provider=bedrock_provider)
736+
agent = Agent(m, system_prompt='You are a helpful chatbot.')
737+
image_url = ImageUrl(url='s3://my-bucket/images/test-image.jpg', media_type='image/jpeg')
738+
739+
result = await agent.run(['What is in this image?', image_url])
740+
assert result.output == snapshot(
741+
'The image shows a scenic landscape with mountains in the background and a clear blue sky above.'
742+
)
743+
744+
745+
@pytest.mark.vcr()
746+
async def test_s3_video_url_input(allow_model_requests: None, bedrock_provider: BedrockProvider):
747+
"""Test that s3:// video URLs are passed directly to Bedrock API."""
748+
m = BedrockConverseModel('us.amazon.nova-pro-v1:0', provider=bedrock_provider)
749+
agent = Agent(m, system_prompt='You are a helpful chatbot.')
750+
video_url = VideoUrl(url='s3://my-bucket/videos/test-video.mp4', media_type='video/mp4')
751+
752+
result = await agent.run(['Describe this video', video_url])
753+
assert result.output == snapshot(
754+
'The video shows a time-lapse of a sunset over the ocean with waves gently rolling onto the shore.'
755+
)
756+
757+
758+
@pytest.mark.vcr()
759+
async def test_s3_document_url_input(allow_model_requests: None, bedrock_provider: BedrockProvider):
760+
"""Test that s3:// document URLs are passed directly to Bedrock API."""
761+
m = BedrockConverseModel('anthropic.claude-v2', provider=bedrock_provider)
762+
agent = Agent(m, system_prompt='You are a helpful chatbot.')
763+
document_url = DocumentUrl(url='s3://my-bucket/documents/test-doc.pdf', media_type='application/pdf')
764+
765+
result = await agent.run(['What is the main content on this document?', document_url])
766+
assert result.output == snapshot(
767+
'Based on the provided document, the main content discusses best practices for cloud storage and data management.'
768+
)
769+
770+
732771
@pytest.mark.vcr()
733772
async def test_text_as_binary_content_input(allow_model_requests: None, bedrock_provider: BedrockProvider):
734773
m = BedrockConverseModel('us.amazon.nova-pro-v1:0', provider=bedrock_provider)

0 commit comments

Comments
 (0)