Skip to content

Python: fix(core): keep annotations and metadata when from_dict builds data content - #8701

Merged
Eduard van Valkenburg (eavanvalkenburg) merged 1 commit into
microsoft:mainfrom
anishmehta24:fix/from-dict-data-drops-metadata
Sep 24, 2026
Merged

Eduard van Valkenburg (eavanvalkenburg) merged 1 commit into
microsoft:mainfrom
anishmehta24:fix/from-dict-data-drops-metadata

Conversation

@anishmehta24

Copy link
Copy Markdown
Contributor

Motivation and Context

Content.from_dict pops the three shared fields off the mapping before dispatching:

annotations = remaining.pop("annotations", None)
additional_properties = remaining.pop("additional_properties", None)
raw_representation = remaining.pop("raw_representation", None)

then hands them to the constructor at the bottom. But the data case returns before ever reaching it:

if content_type == "data" and "data" in remaining and "media_type" in remaining:
    return cls.from_data(remaining["data"], remaining["media_type"])

from_data accepts annotations, additional_properties and raw_representation as keyword arguments — they just weren't being passed. So a data content silently lost all three, while every other content type kept them:

Content.from_dict({"type": "data", "data": b"hello", "media_type": "text/plain",
                   "additional_properties": {"origin": "upload"},
                   "raw_representation": "provider-blob"})
# additional_properties == {}     raw_representation is None     annotations is None

Content.from_dict({"type": "uri", "uri": "https://example.com/x.png",
                   "media_type": "image/png",
                   "additional_properties": {"origin": "upload"},
                   "raw_representation": "provider-blob"})
# both preserved

This isn't only a direct-call concern. Content.from_dict is what Message.from_dict, ChatResponse.from_dict and ChatResponseUpdate.from_dict use to rebuild contents, so restoring a stored conversation dropped the metadata on any data content it held, with nothing to indicate it.

Worth noting it doesn't show up in a plain round trip, which is probably why it survived: to_dict() on a data content emits uri rather than data, so from_dict(to_dict(c)) never takes this branch. It's the hand-built or provider-supplied data + media_type mapping — the shape this branch exists to support — that loses the fields.

Description

Pass the three popped fields to from_data. One call, no signature change, no new behaviour — the special case now keeps what the general case already kept.

One thing I left alone: this branch requires data to be bytes and raises TypeError on a base64 string. That looks deliberate — from_data's docstring says "If you have a base64 string, decode it first" — so I didn't touch it, though it does mean the branch can't consume a mapping that came from JSON. Happy to look at that separately if it's worth changing.

Contribution Checklist

  • The code builds clean without any errors or warnings
  • I didn't break anyone 😄

Tests in packages/core/tests/core/test_types.py:

  • test_content_from_dict_data_keeps_annotations_and_metadata — all three survive
  • test_content_from_dict_data_matches_other_types_on_metadata — a data content and a uri content built from the same shape are treated the same
  • test_message_from_dict_keeps_data_content_metadata — the reachable path, through Message.from_dict

All three fail on main. tests/core/test_types.py passes in full and the whole tests/core directory is green. ruff check and ruff format clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WyCUJcDPnCnQMmuDwsnDmD

…s data content

`Content.from_dict` pops `annotations`, `additional_properties` and
`raw_representation` off the mapping before dispatching, then returns early
for a data content:

    return cls.from_data(remaining["data"], remaining["media_type"])

`from_data` takes all three as keyword arguments, but none were passed, so a
data content lost them while every other content type kept them:

    Content.from_dict({"type": "data", "data": b"hello", "media_type": "text/plain",
                       "additional_properties": {"origin": "upload"}})
    # main: additional_properties == {}

This is reachable from `Message.from_dict`, `ChatResponse.from_dict` and
`ChatResponseUpdate.from_dict`, so rebuilding a stored conversation dropped
the metadata on any data content in it, silently.

Pass the three through.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WyCUJcDPnCnQMmuDwsnDmD

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@agent-framework-automation agent-framework-automation Bot added the python Usage: [Issues, PRs], Target: Python label Sep 23, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Anish Mehta (@anishmehta24) The current head is green, and the dedicated full-diff review found no remaining actionable issues with preserving annotations and metadata when Content.from_dict constructs data content.

Merged via the queue into microsoft:main with commit 91fd470 Sep 24, 2026
49 checks passed

This branch was previously deployed

1 inactive deployment
github-app-auth — bdb8762b Deployed Sep 23, 2026 by anishmehta24 via team_check #5180
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants