Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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.debug("Discarding field '%s' while deep-copying Content.", k)
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 at debug level."""
import copy

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

cloned = copy.deepcopy(content)
with caplog.at_level("DEBUG", 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_log_for_empty_shallow_copy_fields(caplog: pytest.LogCaptureFixture) -> None:
"""Test that empty shallow-copy fields do not emit discard logs."""
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("DEBUG", 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