Skip to content

Commit 7e52b8f

Browse files
committed
Python: Serialize durabletask options response format
1 parent e7c7f74 commit 7e52b8f

2 files changed

Lines changed: 74 additions & 2 deletions

File tree

python/packages/durabletask/agent_framework_durabletask/_models.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,34 @@ def _deserialize_response_format(response_format: Any) -> type[BaseModel] | None
9393
return None
9494

9595

96+
def _serialize_options(options: dict[str, Any]) -> dict[str, Any]:
97+
"""Serialize known durable options without mutating caller-provided options."""
98+
result = dict(options)
99+
response_format = result.get("response_format")
100+
if (
101+
_PydanticBaseModel is not None
102+
and inspect.isclass(response_format)
103+
and issubclass(response_format, _PydanticBaseModel)
104+
):
105+
result["response_format"] = serialize_response_format(response_format)
106+
return result
107+
108+
109+
def _deserialize_options(options: dict[str, Any]) -> dict[str, Any]:
110+
"""Deserialize known durable options without mutating caller-provided options."""
111+
result = dict(options)
112+
if (response_format_value := result.get("response_format")) is not None:
113+
response_format = _deserialize_response_format(response_format_value)
114+
if response_format is not None:
115+
result["response_format"] = response_format
116+
elif (
117+
isinstance(response_format_value, dict)
118+
and response_format_value.get("__response_schema_type__") == "pydantic_model"
119+
):
120+
result.pop("response_format")
121+
return result
122+
123+
96124
@dataclass
97125
class RunRequest:
98126
"""Represents a request to run an agent with a specific message and configuration.
@@ -165,7 +193,7 @@ def to_dict(self) -> dict[str, Any]:
165193
"role": self.role,
166194
"request_response_format": self.request_response_format,
167195
"correlationId": self.correlation_id,
168-
"options": self.options,
196+
"options": _serialize_options(self.options),
169197
}
170198
if self.response_format:
171199
result["response_format"] = serialize_response_format(self.response_format)
@@ -200,6 +228,7 @@ def from_dict(cls, data: dict[str, Any]) -> RunRequest:
200228
raise ValueError("correlationId is required in RunRequest data")
201229

202230
options = data.get("options")
231+
options = _deserialize_options(options) if isinstance(options, dict) else {}
203232

204233
return cls(
205234
message=data.get("message", ""),
@@ -211,7 +240,7 @@ def from_dict(cls, data: dict[str, Any]) -> RunRequest:
211240
enable_tool_calls=data.get("enable_tool_calls", True),
212241
created_at=created_at,
213242
orchestration_id=data.get("orchestrationId"),
214-
options=cast(dict[str, Any], options) if isinstance(options, dict) else {},
243+
options=options,
215244
)
216245

217246

python/packages/durabletask/tests/test_models.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
"""Unit tests for data models (RunRequest)."""
44

5+
import json
6+
57
import pytest
68
from pydantic import BaseModel
79

@@ -204,6 +206,47 @@ def test_round_trip_with_options(self) -> None:
204206
assert restored.options["custom"] == "value"
205207
assert restored.options["response_format"] is ModuleStructuredResponse
206208

209+
def test_to_dict_with_options_response_format_is_json_serializable(self) -> None:
210+
"""Ensure options response_format can cross the durable JSON boundary."""
211+
request = RunRequest(
212+
message="Test",
213+
correlation_id="corr-opts-json",
214+
options={"response_format": ModuleStructuredResponse},
215+
)
216+
217+
data = request.to_dict()
218+
encoded = json.dumps(data)
219+
decoded = json.loads(encoded)
220+
restored = RunRequest.from_dict(decoded)
221+
222+
assert data["options"]["response_format"]["__response_schema_type__"] == "pydantic_model"
223+
assert restored.options["response_format"] is ModuleStructuredResponse
224+
225+
def test_from_dict_drops_unresolved_options_response_format_marker(self) -> None:
226+
"""Ensure unresolved durable markers do not block top-level response_format fallback."""
227+
request = RunRequest.from_dict(
228+
{
229+
"message": "Test",
230+
"correlationId": "corr-opts-missing",
231+
"response_format": {
232+
"__response_schema_type__": "pydantic_model",
233+
"module": ModuleStructuredResponse.__module__,
234+
"qualname": ModuleStructuredResponse.__qualname__,
235+
},
236+
"options": {
237+
"response_format": {
238+
"__response_schema_type__": "pydantic_model",
239+
"module": "missing_module",
240+
"qualname": "MissingModel",
241+
},
242+
"custom": "value",
243+
},
244+
}
245+
)
246+
247+
assert request.response_format is ModuleStructuredResponse
248+
assert request.options == {"custom": "value"}
249+
207250
def test_init_with_correlationId(self) -> None:
208251
"""Test RunRequest initialization with correlationId."""
209252
request = RunRequest(message="Test message", correlation_id="corr-123")

0 commit comments

Comments
 (0)