Skip to content

Commit 6e6f667

Browse files
Motta Kinmochow13
authored andcommitted
#3621 - Pass s3:// file URLs directly to API in BedrockConverseModel
1 parent ea2d9d3 commit 6e6f667

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
@@ -733,20 +733,25 @@ async def _map_user_prompt( # noqa: C901
733733
else:
734734
raise NotImplementedError('Binary content is not supported yet.')
735735
elif isinstance(item, ImageUrl | DocumentUrl | VideoUrl):
736-
downloaded_item = await download_item(item, data_format='bytes', type_format='extension')
737-
format = downloaded_item['data_type']
736+
source: dict[str, Any]
737+
if item.url.startswith('s3://'):
738+
source = {'s3Location': {'uri': item.url}}
739+
else:
740+
downloaded_item = await download_item(item, data_format='bytes', type_format='extension')
741+
source = {'bytes': downloaded_item['data']}
742+
738743
if item.kind == 'image-url':
739744
format = item.media_type.split('/')[1]
740745
assert format in ('jpeg', 'png', 'gif', 'webp'), f'Unsupported image format: {format}'
741-
image: ImageBlockTypeDef = {'format': format, 'source': {'bytes': downloaded_item['data']}}
746+
image: ImageBlockTypeDef = {'format': format, 'source': cast(Any, source)}
742747
content.append({'image': image})
743748

744749
elif item.kind == 'document-url':
745750
name = f'Document {next(document_count)}'
746751
document: DocumentBlockTypeDef = {
747752
'name': name,
748753
'format': item.format,
749-
'source': {'bytes': downloaded_item['data']},
754+
'source': cast(Any, source),
750755
}
751756
content.append({'document': document})
752757

@@ -763,7 +768,7 @@ async def _map_user_prompt( # noqa: C901
763768
'wmv',
764769
'three_gp',
765770
), f'Unsupported video format: {format}'
766-
video: VideoBlockTypeDef = {'format': format, 'source': {'bytes': downloaded_item['data']}}
771+
video: VideoBlockTypeDef = {'format': format, 'source': cast(Any, source)}
767772
content.append({'video': video})
768773
elif isinstance(item, AudioUrl): # pragma: no cover
769774
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
@@ -739,6 +739,45 @@ async def test_text_document_url_input(allow_model_requests: None, bedrock_provi
739739
)
740740

741741

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

0 commit comments

Comments
 (0)