Skip to content

Commit ed65fb5

Browse files
mwildehahnDouweM
andauthored
Fix serialization / deserialization issues with FileUrl subclasses (#2677)
Co-authored-by: Douwe Maan <[email protected]>
1 parent 0198f48 commit ed65fb5

File tree

2 files changed

+69
-1
lines changed

2 files changed

+69
-1
lines changed

pydantic_ai_slim/pydantic_ai/messages.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,9 @@ class FileUrl(ABC):
110110
- `GoogleModel`: `VideoUrl.vendor_metadata` is used as `video_metadata`: https://ai.google.dev/gemini-api/docs/video-understanding#customize-video-processing
111111
"""
112112

113-
_media_type: str | None = field(init=False, repr=False, compare=False)
113+
_media_type: Annotated[str | None, pydantic.Field(alias='media_type', default=None, exclude=True)] = field(
114+
compare=False
115+
)
114116

115117
def __init__(
116118
self,
@@ -124,6 +126,7 @@ def __init__(
124126
self.force_download = force_download
125127
self._media_type = media_type
126128

129+
@pydantic.computed_field
127130
@property
128131
def media_type(self) -> str:
129132
"""Return the media type of the file, based on the URL or the provided `media_type`."""

tests/test_agent.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3076,6 +3076,65 @@ def test_binary_content_serializable():
30763076
assert messages == result.all_messages()
30773077

30783078

3079+
def test_image_url_serializable_missing_media_type():
3080+
agent = Agent('test')
3081+
content = ImageUrl('https://example.com/chart.jpeg')
3082+
result = agent.run_sync(['Hello', content])
3083+
serialized = result.all_messages_json()
3084+
assert json.loads(serialized) == snapshot(
3085+
[
3086+
{
3087+
'parts': [
3088+
{
3089+
'content': [
3090+
'Hello',
3091+
{
3092+
'url': 'https://example.com/chart.jpeg',
3093+
'force_download': False,
3094+
'vendor_metadata': None,
3095+
'kind': 'image-url',
3096+
'media_type': 'image/jpeg',
3097+
},
3098+
],
3099+
'timestamp': IsStr(),
3100+
'part_kind': 'user-prompt',
3101+
}
3102+
],
3103+
'instructions': None,
3104+
'kind': 'request',
3105+
},
3106+
{
3107+
'parts': [{'content': 'success (no tool calls)', 'part_kind': 'text'}],
3108+
'usage': {
3109+
'input_tokens': 51,
3110+
'cache_write_tokens': 0,
3111+
'cache_read_tokens': 0,
3112+
'output_tokens': 4,
3113+
'input_audio_tokens': 0,
3114+
'cache_audio_read_tokens': 0,
3115+
'output_audio_tokens': 0,
3116+
'details': {},
3117+
},
3118+
'model_name': 'test',
3119+
'timestamp': IsStr(),
3120+
'provider_name': None,
3121+
'provider_details': None,
3122+
'provider_request_id': None,
3123+
'kind': 'response',
3124+
},
3125+
]
3126+
)
3127+
3128+
# We also need to be able to round trip the serialized messages.
3129+
messages = ModelMessagesTypeAdapter.validate_json(serialized)
3130+
part = messages[0].parts[0]
3131+
assert isinstance(part, UserPromptPart)
3132+
content = part.content[1]
3133+
assert isinstance(content, ImageUrl)
3134+
assert content.media_type == 'image/jpeg'
3135+
assert messages == result.all_messages()
3136+
3137+
30793138
def test_image_url_serializable():
30803139
agent = Agent('test')
30813140

@@ -3095,6 +3154,7 @@ def test_image_url_serializable():
30953154
'force_download': False,
30963155
'vendor_metadata': None,
30973156
'kind': 'image-url',
3157+
'media_type': 'image/jpeg',
30983158
},
30993159
],
31003160
'timestamp': IsStr(),
@@ -3128,6 +3188,11 @@ def test_image_url_serializable():
31283188

31293189
# We also need to be able to round trip the serialized messages.
31303190
messages = ModelMessagesTypeAdapter.validate_json(serialized)
3191+
part = messages[0].parts[0]
3192+
assert isinstance(part, UserPromptPart)
3193+
content = part.content[1]
3194+
assert isinstance(content, ImageUrl)
3195+
assert content.media_type == 'image/jpeg'
31313196
assert messages == result.all_messages()
31323197

31333198

0 commit comments

Comments
 (0)