Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions python/packages/core/agent_framework/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -589,18 +589,20 @@ def __init__(
self.consent_link = consent_link

def __deepcopy__(self, memo: dict[int, Any]) -> Content:
"""Create a deep copy, preserving ``_SHALLOW_COPY_FIELDS`` by reference.
"""Create a deep copy, discarding non-``None`` ``_SHALLOW_COPY_FIELDS``.

Fields listed in ``_SHALLOW_COPY_FIELDS`` may contain LLM SDK objects
(e.g., proto/gRPC responses) that are not safe to deep-copy.
(e.g., proto/gRPC responses) that are not safe to deep-copy or share.
"""
cls = type(self)
result = cls.__new__(cls)
memo[id(self)] = result
shallow = cls._SHALLOW_COPY_FIELDS
for k, v in self.__dict__.items():
if k in shallow:
object.__setattr__(result, k, v)
if v is not None:
logger.warning("Discarding field '%s' while deep-copying Content.", k)
Comment thread
Sweetteabittersugar marked this conversation as resolved.
Outdated
object.__setattr__(result, k, None)
else:
object.__setattr__(result, k, deepcopy(v, memo))
return result
Expand Down
33 changes: 17 additions & 16 deletions python/packages/core/tests/core/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -1197,7 +1197,7 @@ def test_chat_response_updates_to_chat_response_multiple_multiple():
assert len(chat_response.messages) == 1
assert isinstance(chat_response.messages[0], Message)
assert chat_response.messages[0].message_id == "1"
assert chat_response.messages[0].contents[0].raw_representation is not None
assert chat_response.messages[0].contents[0].raw_representation is None

assert len(chat_response.messages[0].contents) == 3
assert chat_response.messages[0].contents[0].type == "text"
Expand Down Expand Up @@ -1796,17 +1796,17 @@ def test_response_update_propagates_fields_and_metadata():
assert resp.messages[0].message_id == "mid"


def test_text_coalescing_preserves_first_properties():
def test_text_coalescing_preserves_first_serializable_properties():
t1 = Content.from_text("A", raw_representation={"r": 1}, additional_properties={"p": 1})
t2 = Content.from_text("B")
upd1 = ChatResponseUpdate(contents=[t1], message_id="x")
upd2 = ChatResponseUpdate(contents=[t2], message_id="x")
resp = ChatResponse.from_updates([upd1, upd2])
# After coalescing there should be a single TextContent with merged text and preserved props from first
# After coalescing there should be a single TextContent with merged text and serializable props from first.
items = [c for c in resp.messages[0].contents if c.type == "text"]
assert len(items) >= 1
assert items[0].text == "AB"
assert items[0].raw_representation == {"r": 1}
assert items[0].raw_representation is None
assert items[0].additional_properties == {"p": 1}


Expand Down Expand Up @@ -2500,18 +2500,20 @@ def __deepcopy__(self, memo: dict) -> Any:
raise TypeError("Cannot deepcopy this object")


def test_content_deepcopy_preserves_raw_representation():
"""Test that deepcopy of Content keeps raw_representation by reference."""
def test_content_deepcopy_discards_raw_representation(caplog: pytest.LogCaptureFixture) -> None:
"""Test that deepcopy of Content discards raw_representation and logs a warning."""
import copy

raw = _NonCopyableRaw()
content = Content.from_text("hello", raw_representation=raw)

cloned = copy.deepcopy(content)
with caplog.at_level("WARNING", logger="agent_framework"):
cloned = copy.deepcopy(content)

assert cloned.text == "hello"
assert cloned.raw_representation is raw
assert cloned.raw_representation is None
assert cloned.additional_properties is not content.additional_properties
assert caplog.messages == ["Discarding field 'raw_representation' while deep-copying Content."]


def test_message_deepcopy_preserves_raw_representation():
Expand Down Expand Up @@ -2617,21 +2619,20 @@ def test_nested_deepcopy_preserves_raw_representation():
assert cloned.text == "hello"


def test_content_deepcopy_shallow_copy_fields_identity():
"""Test that Content._SHALLOW_COPY_FIELDS fields are identity-preserved while others are deep-copied."""
def test_content_deepcopy_does_not_warn_for_empty_shallow_copy_fields(caplog: pytest.LogCaptureFixture) -> None:
"""Test that empty shallow-copy fields do not emit discard warnings."""
import copy

raw = _NonCopyableRaw()
content = Content.from_text("hello", raw_representation=raw)
content = Content.from_text("hello")
content.additional_properties["key"] = "value"

cloned = copy.deepcopy(content)
with caplog.at_level("WARNING", logger="agent_framework"):
cloned = copy.deepcopy(content)

# _SHALLOW_COPY_FIELDS (raw_representation) should be same object
assert cloned.raw_representation is raw
# Non-shallow fields should be independent deep copies
assert cloned.raw_representation is None
assert cloned.additional_properties is not content.additional_properties
assert cloned.additional_properties == {"key": "value"}
assert caplog.messages == []


def test_chat_response_deepcopy_deep_copies_additional_properties():
Expand Down
Loading