Skip to content

Conversation

@kyuam32
Copy link
Contributor

@kyuam32 kyuam32 commented Aug 21, 2025

This PR implements the feature requested in issue #2635 myself - adding an optional identifier field to FileUrl and its subclasses to enable file url tracking across conversations.

Changes Made

  1. Updated messages.py
  • Added identifier at FileUrl
  • Updated constructors for all subclasses (ImageUrl, VideoUrl, AudioUrl, DocumentUrl)
  1. Updated _agent_graph.py
  • Modified multimodal content processing to use custom identifier when available
  1. Added test coverage
  • Added test_tool_returning_file_url_with_identifier to verify the feature works correctly for all FileUrl subclasses

Testing

  • All existing and new test added checked with makefile.

Note: I've implemented this as a proposal from what i requested as a issue. I understand feature request hasn't been approved yet, so please feel free to close this PR if the approach doesn't align with the project's direction.
I'm happy to adjust the implementation based on your feedback or explore alternative solutions.
This is my first contribution to opensource. I've tried to follow the existing patterns closely, similar implementation #2231.
Thank you for your time and consideration!

self,
url: str,
force_download: bool = False,
identifier: str | None = None,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this not to be a breaking change, this needs to added at the end so that previous initializations that didn't use keyword arguments still work.

identifier = content.identifier or multi_modal_content_identifier(content.data)
else:
identifier = multi_modal_content_identifier(content.url)
identifier = content.identifier or multi_modal_content_identifier(content.url)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can move this to an if content.identifier: branch before the current 2, so we can assume we need to generate it here

self.url = url
self.vendor_metadata = vendor_metadata
self.force_download = force_download
self.identifier = identifier
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you see if we can move the multi_modal_content_identifier function that's used in _agent_graph.py to here, so we can use it if no identifier was explicitly provided? That way we may be able to define the field as always being str (so no None) and always use it without having to check if it's set, similar to media_type.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DouweM I've updated the implementation based on your feedback. The identifier property now always
returns a str for FileUrl classes. Please see my detailed response below.

@kyuam32
Copy link
Contributor Author

kyuam32 commented Aug 26, 2025

We can move this to an if content.identifier: branch before the current 2, so we can assume we need to generate it here

Thank you for review @DouweM,

I've implement your suggestion to use identifier property for FileUrl (similar to media_type)
that always returns a str. I think it's a good solution! However, I've encountered type checking issues.

The problem occurs trying to implement mentioned quote style branch:

  if content.identifier:
      identifier = content.identifier
  else:
      identifier = multi_modal_content_identifier(content.data)

The type checker cannot infer the identifier property of FileUrl and try to check attribute data.
Since only BinaryContent has a data attribute (while FileUrl subclasses don't), pyright reports errors.

If refactor BinaryContent to also use a constructor with a property (or maybe post init) that always returns str for
identifier, we could:

  1. Eliminate the branching entirely (since identifier would always have a str value)
  2. Move the multi_modal_content_identifier() logic into BinaryContent's property

But i'm not sure is this the right way.

For now, I've kept the existing branching logic and only moved the FileUrl.identifier generation logic
from multi_modal_content_identifier() into the FileUrl property.

if isinstance(content, _messages.BinaryContent):
  identifier = content.identifier or multi_modal_content_identifier(content.data)
else:
  identifier = content.identifier

What do you think about this approach? Would you prefer to keep it this way, or should we consider making
BinaryContent.identifier also always return a str for consistency?

@kyuam32 kyuam32 requested a review from DouweM August 26, 2025 15:53
@DouweM
Copy link
Collaborator

DouweM commented Aug 26, 2025

should we consider making
BinaryContent.identifier also always return a str for consistency?

@kyuam32 Yep, let's give that a try. The function can live as a private function in that same file and be used by both classes

@kyuam32
Copy link
Contributor Author

kyuam32 commented Aug 27, 2025

@DouweM Thank you for your patience and detailed feedback throughout this PR!

Identifier support for MultiModalContentTypes

  • New parameter(identifier) placed at the end of constructors (for backward compatibility)
  • identifier always returns a str (auto-generated if not provided)
  • Moved identifier generation logic multi_modal_content_identifier() to messages.py
  • Simplify branch when tool calling processes contents at _angent_graph.py

Design Considerations

  • Initially tried using private _identifier field with @property for FileUrl (similar to media_type)
  • However, BinaryContent already had identifier as a public field in the existing codebase
  • This caused serialization inconsistency:
    • BinaryContent serialized as {“identifier”: “...“}
    • FileUrl serialized as {“_identifier”: “...“}
  • Aligned FileUrl implementation with BinaryContent for consistency
class FileUrl:
    identifier: str
    def __init__(self, url: str, ..., identifier: str | None = None):
        ...
        self.identifier = identifier or _multi_modal_content_identifier(url)
class BinaryContent:
    identifier: str
    def __init__(self, data: bytes, ..., identifier: str | None = None):
        ...
        self.identifier = identifier or _multi_modal_content_identifier(data)

Testing Updates

  • Modified FileUrl, BinaryContent serialization tests to focus on verify auto-generated identifiers
  • Modified FileUrl, BinaryContent identifier tests to focus on verify custom identifier handling

@kyuam32
Copy link
Contributor Author

kyuam32 commented Aug 28, 2025

@DouweM
fixed test failure after merging main branch

@kyuam32
Copy link
Contributor Author

kyuam32 commented Sep 1, 2025

Hi @DouweM.
Would appreciate a review on this PR when convenient.
Happy to address any feedback.
Thanks for your time!

url: str
"""The URL of the file."""

identifier: str
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make this the final field, like it is on the constructor

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DouweM Thank you for the review!
Moved identifier field to be the final field in the dataclass definition

media_type: str | None = None,
kind: Literal['video-url'] = 'video-url',
identifier: str | None = None,
*,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move the * ahead of force_download so all arguments other than url need to be keywords arguments -- same for the other subclasses

Copy link
Contributor Author

@kyuam32 kyuam32 Sep 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactored with same rules for other MultiModalContent types

e.g. "This is file <identifier>:" preceding the `BinaryContent`.
"""

_: KW_ONLY
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move this ahead of media_type

Copy link
Contributor Author

@kyuam32 kyuam32 Sep 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved _: KW_ONLY ahead of media_type, after data field

@kyuam32 kyuam32 requested a review from DouweM September 2, 2025 00:26
@DouweM DouweM merged commit 46ba28f into pydantic:main Sep 2, 2025
41 checks passed
@DouweM
Copy link
Collaborator

DouweM commented Sep 2, 2025

@kyuam32 Thank you!

"""The media type of the binary data."""

This identifier can be provided to the model in a message to allow it to refer to this file in a tool call argument, and the tool can look up the file in question by iterating over the message history and finding the matching `BinaryContent`.
identifier: str
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DouweM @kyuam32 -- was this intended to go to str vs keeping str | None? we store old json to rehydrate later into chags and now ModelMessageAdapter.validate_json throws because it expects identifier to have a value for those old dicts

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kousun12 Ay that was unintentional, I was relying on the fact that we always set an identifier in __init__, but that wouldn't work for validation. Can you create a new issue please?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related issue has been filed: #3103

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants