Python: Fix missing methods on the Content class in durable tasks (#4738)

* Fix Content serialization in DurableAgentStateUnknownContent (#4719)

DurableAgentStateUnknownContent.from_unknown_content() stored raw Content
objects without converting them to dicts, causing json.dumps to fail in
Azure Durable Functions' entity state serialization. This affected content
types not explicitly handled (e.g., mcp_server_tool_call/result).

The fix converts Content objects to dicts via to_dict() when storing in
DurableAgentStateUnknownContent, and restores them via Content.from_dict()
in to_ai_content().

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add to_json and from_json methods to Content class (#4719)

Add to_json() and from_json() methods to the Content class to match the
serialization interface provided by SerializationMixin on other model classes.
Also fix pre-existing pyright type errors in durabletask's
DurableAgentStateUnknownContent.to_ai_content().

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR review: add type guard, remove to_json, add fallback, and tests

- Remove Content.to_json() per reviewer request (comment 3)
- Add type guard in Content.from_json() for non-dict JSON (comments 1, 4)
- Wrap json.JSONDecodeError as ValueError for consistent exception contract
- Add try/except fallback in to_ai_content() for invalid Content dicts (comment 5)
- Add test_content_to_dict_exclude_none and test_content_to_dict_exclude_fields (comment 2)
- Add test_unknown_content_to_ai_content_fallback_on_invalid_type_dict (comment 5)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Apply pre-commit auto-fixes

* Address review feedback for #4719: review comment fixes

* Remove Content.from_json, move logic to consuming code (#4719)

Remove the from_json convenience method from Content class per review
feedback. This is the same trivial json.loads + from_dict wrapper as
to_json which was already removed. Consumers should call json.loads
and Content.from_dict directly.

Update tests to use Content.from_dict(json.loads(...)) pattern and
remove from_json-specific error handling tests (those errors are
already covered by json.loads and Content.from_dict).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Eduard van Valkenburg
2026-03-18 09:08:44 +01:00
committed by GitHub
Unverified
parent 192a283c9a
commit 705ed47a0b
3 changed files with 166 additions and 1 deletions
@@ -1,6 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
import base64
import json
from collections.abc import AsyncIterable, Sequence
from dataclasses import dataclass
from datetime import datetime, timezone
@@ -1710,6 +1711,47 @@ def test_content_roundtrip_preserves_compaction_annotation_dict() -> None:
assert annotation[GROUP_TOKEN_COUNT_KEY] is None
def test_content_from_dict_via_json() -> None:
"""Test Content.from_dict with data parsed from a JSON string."""
data = json.loads(json.dumps({"type": "text", "text": "Hello world"}))
content = Content.from_dict(data)
assert content.type == "text"
assert content.text == "Hello world"
def test_content_from_dict_roundtrip_via_json() -> None:
"""Test Content.from_dict roundtrip via to_dict and json.dumps."""
original = Content.from_function_call(call_id="call1", name="my_func", arguments={"key": "value"})
data = json.loads(json.dumps(original.to_dict()))
restored = Content.from_dict(data)
assert restored.type == "function_call"
assert restored.call_id == "call1"
assert restored.name == "my_func"
assert restored.arguments == {"key": "value"}
def test_content_to_dict_exclude_none() -> None:
"""Test Content.to_dict excludes None fields by default."""
content = Content.from_text("Hello")
d = content.to_dict()
parsed = json.loads(json.dumps(d))
assert "uri" not in parsed
d_with_none = content.to_dict(exclude_none=False)
parsed_with_none = json.loads(json.dumps(d_with_none))
assert "uri" in parsed_with_none
assert parsed_with_none["uri"] is None
def test_content_to_dict_exclude_fields() -> None:
"""Test Content.to_dict with explicit field exclusion."""
content = Content.from_text("Hello")
d = content.to_dict(exclude={"text"})
parsed = json.loads(json.dumps(d))
assert "text" not in parsed
assert parsed["type"] == "text"
def test_chat_response_roundtrip_preserves_compaction_annotation_dict() -> None:
response = ChatResponse(
messages=[