Skip to content
Draft
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
35 changes: 22 additions & 13 deletions sdk/agentserver/azure-ai-agentserver-responses/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
### Breaking Changes

- Removed a-prefixed async convenience generator methods from the sync `ResponseEventStream` and sync builder classes. Use `azure.ai.agentserver.responses.aio.ResponseEventStream` for async streaming convenience methods.
- Replaced generated model classes in `azure.ai.agentserver.responses.models` with dict-native `TypedDict` contracts. Model constructors such as `ItemMessage(...)` and `CreateResponse(...)` now produce plain dictionaries instead of generated model instances.
- Removed runtime model-class behavior from response protocol models. Code should no longer rely on attribute access, `isinstance(..., ModelType)`, `.as_dict()`, or generated model base-class behavior.
- Request payloads now use dict-native `TypedDict` contracts, while response protocol models keep object-style construction, attribute access, and `to_dict()` / `as_dict()` conversion helpers.
- Response protocol model internals are backed by JSON-compatible dictionaries so they can be serialized, streamed, and persisted without conversion.
- Replaced most generated enum classes with string literal type aliases. Use string values directly for protocol fields, for example `"completed"`, `"message"`, or `"function_call_output"`.

### Migration Guide
Expand Down Expand Up @@ -41,7 +41,21 @@ async for event in stream.output_item_message(token_stream()):

Builder async helpers follow the same pattern: use builders from `azure.ai.agentserver.responses.aio.streaming` and drop the `a` prefix. For example, `atext_content(...)` becomes `text_content(...)`, `aarguments(...)` becomes `arguments(...)`, and `asummary_part(...)` becomes `summary_part(...)`.

Protocol models are now dict-native. Construction still works, but the result is a dictionary:
Request payloads such as `CreateResponse` are now dict-native. Previously, request model construction returned generated model objects with attribute access:

```python
request = CreateResponse(model="test-model", input="hello")
model = request.model
```

Now, request construction returns a plain dictionary:

```python
request = CreateResponse(model="test-model", input="hello")
model = request["model"]
```

Response protocol models keep object-style access while remaining wire-serializable:

```python
from azure.ai.agentserver.responses.models import ItemMessage, MessageContentInputTextContent
Expand All @@ -52,19 +66,14 @@ message = ItemMessage(
)
```

Before:
Response model access continues to use attributes:

```python
if isinstance(item, ItemMessage):
if item.type == "message":
text = item.content[0].text
```

After:

```python
if item.get("type") == "message":
text = item.get("content", [{}])[0].get("text")
```
Use `item.to_dict()` or `item.as_dict()` when a plain JSON-compatible dictionary is needed.

Before:

Expand All @@ -80,8 +89,8 @@ status = "completed"

### Other Changes

- Updated response hosting, persistence, streaming, validation, samples, and tests to operate on JSON-compatible wire dictionaries.
- Updated model generation tooling to use TypeSpec Python `models-mode=typeddict` and removed generated model shim files.
- Updated response hosting, persistence, streaming, validation, samples, and tests to operate on JSON-compatible wire dictionaries internally.
- Updated model generation tooling to use TypeSpec Python `models-mode=typeddict` for request payloads and added response object-model compatibility wrappers for public response payloads.

## 1.0.0b9 (2026-07-22)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
get_input_expanded,
get_tool_choice_expanded,
)
from ._object_model import ResponseModel, create_response_model_type
from .runtime import ( # pylint: disable=unused-import
ResponseStatus,
TerminalResponseStatus,
Expand Down Expand Up @@ -53,6 +54,25 @@ def _is_public_generated_export(value: object) -> bool:
]


def _is_request_payload_name(name: str) -> bool:
return name == "CreateResponse" or "Param" in name or name.endswith("Request")


def _install_response_object_models() -> list[str]:
model_names: list[str] = []
for name in _generated_all:
if _is_request_payload_name(name):
continue
value = globals().get(name)
if isinstance(value, type):
globals()[name] = create_response_model_type(name, __name__)
model_names.append(name)
return model_names


_object_model_names = _install_response_object_models()


class ResponseIncompleteReason(str, Enum, metaclass=CaseInsensitiveEnumMeta):
"""Reason a response finished as incomplete."""

Expand All @@ -63,6 +83,7 @@ class ResponseIncompleteReason(str, Enum, metaclass=CaseInsensitiveEnumMeta):


__all__ = [
"ResponseModel",
"ResponseIncompleteReason",
"ResponseStatus",
"TerminalResponseStatus",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.
"""Object-style compatibility models for response payloads."""

from __future__ import annotations

from copy import deepcopy
from typing import Any, Mapping


class ResponseModel(dict[str, Any]):
"""Dictionary-backed response model with attribute access.

The Responses server stores and streams JSON-compatible dictionaries
internally. This wrapper preserves that wire-native representation while
restoring the object-style access pattern used by client SDK response
models.
"""

def __init__(self, mapping: Mapping[str, Any] | None = None, **kwargs: Any) -> None:
values: dict[str, Any] = {}
if mapping is not None:
values.update(mapping)
values.update(kwargs)
super().__init__((key, _wrap_value(value)) for key, value in values.items())

def __getattr__(self, name: str) -> Any:
try:
return self[name]
except KeyError as exc:
raise AttributeError(name) from exc

def __setattr__(self, name: str, value: Any) -> None:
self[name] = _wrap_value(value)

def __delattr__(self, name: str) -> None:
try:
del self[name]
except KeyError as exc:
raise AttributeError(name) from exc

def copy(self) -> "ResponseModel": # type: ignore[override]
"""Return a shallow object-model copy."""
return type(self)(self)

def as_dict(self) -> dict[str, Any]:
"""Return a plain dictionary representation of the model."""
return _unwrap_value(self)

def to_dict(self) -> dict[str, Any]:
"""Return a plain dictionary representation of the model."""
return self.as_dict()


def _wrap_value(value: Any) -> Any:
if isinstance(value, ResponseModel):
return value
if isinstance(value, Mapping):
return ResponseModel(value)
if isinstance(value, list):
return [_wrap_value(item) for item in value]
if isinstance(value, tuple):
return tuple(_wrap_value(item) for item in value)
return value


def _unwrap_value(value: Any) -> Any:
if isinstance(value, ResponseModel):
return {key: _unwrap_value(item) for key, item in value.items()}
if isinstance(value, Mapping):
return {str(key): _unwrap_value(item) for key, item in value.items()}
if isinstance(value, list):
return [_unwrap_value(item) for item in value]
if isinstance(value, tuple):
return [_unwrap_value(item) for item in value]
return deepcopy(value)


def create_response_model_type(name: str, module_name: str) -> type[ResponseModel]:
"""Create a named response model class backed by :class:`ResponseModel`."""

return type(name, (ResponseModel,), {"__module__": module_name})


__all__ = ["ResponseModel", "create_response_model_type"]
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ def get_field(payload: Any, field: str, default: Any = None) -> Any:
"""
if isinstance(payload, Mapping):
return payload.get(field, default)
if hasattr(payload, field):
return getattr(payload, field)
return default


Expand Down Expand Up @@ -52,6 +54,10 @@ def to_wire_dict(value: Any) -> Any:
return value
if isinstance(value, datetime):
return int(value.timestamp())
if hasattr(value, "as_dict") and callable(value.as_dict):
return to_wire_dict(value.as_dict())
if hasattr(value, "to_dict") and callable(value.to_dict):
return to_wire_dict(value.to_dict())
if isinstance(value, Mapping):
return {str(k): to_wire_dict(v) for k, v in value.items()}
if isinstance(value, (list, tuple)):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.
"""Object-model ergonomics for response payloads."""

from __future__ import annotations

import json

from azure.ai.agentserver.responses.models import (
CreateResponse,
ItemMessage,
MessageContentInputTextContent,
ResponseModel,
ResponseObject,
)
from azure.ai.agentserver.responses.models._wire import to_wire_dict


def test_create_response_remains_dict_native_request_payload() -> None:
request = CreateResponse(model="test-model", input="hello")

assert type(request) is dict
assert request["model"] == "test-model"


def test_response_models_support_attribute_access_and_to_dict() -> None:
response = ResponseObject(
id="resp_123",
status="completed",
output=[
ItemMessage(
type="message",
role="assistant",
content=[MessageContentInputTextContent(type="input_text", text="hello")],
)
],
)

assert isinstance(response, ResponseModel)
assert response.output[0].content[0].text == "hello"
assert response.to_dict() == {
"id": "resp_123",
"status": "completed",
"output": [
{
"type": "message",
"role": "assistant",
"content": [{"type": "input_text", "text": "hello"}],
}
],
}


def test_response_models_remain_wire_serializable() -> None:
message = ItemMessage(
type="message",
role="user",
content=[MessageContentInputTextContent(type="input_text", text="hi")],
)

wire = to_wire_dict(message)

assert wire == {"type": "message", "role": "user", "content": [{"type": "input_text", "text": "hi"}]}
assert json.dumps(wire)


def test_response_models_can_be_reused_as_later_request_input() -> None:
response = ResponseObject(
id="resp_123",
status="completed",
output=[
ItemMessage(
type="message",
role="assistant",
content=[MessageContentInputTextContent(type="input_text", text="previous answer")],
)
],
)
request = CreateResponse(model="test-model", input=response.output)

wire = to_wire_dict(request)

assert wire == {
"model": "test-model",
"input": [
{
"type": "message",
"role": "assistant",
"content": [{"type": "input_text", "text": "previous answer"}],
}
],
}
Loading