Python: Fix AzureAIClient failure when conversation history contains assistant messages (#3076)

* Fix AzureAIClient failure when conversation history contains assistant messages

* Address PR review feedback: improve docstring and test assertions

* Remove redundant cast
This commit is contained in:
Evan Mattson
2026-01-06 07:05:46 +09:00
committed by GitHub
Unverified
parent 0aba02c402
commit 928c9d54ad
2 changed files with 135 additions and 1 deletions
@@ -2,7 +2,7 @@
import sys
from collections.abc import Mapping, MutableSequence
from typing import Any, ClassVar, TypeVar
from typing import Any, ClassVar, TypeVar, cast
from agent_framework import (
AGENT_FRAMEWORK_USER_AGENT,
@@ -379,6 +379,15 @@ class AzureAIClient(OpenAIBaseResponsesClient):
"""Take ChatOptions and create the specific options for Azure AI."""
prepared_messages, instructions = self._prepare_messages_for_azure_ai(messages)
run_options = await super()._prepare_options(prepared_messages, chat_options, **kwargs)
# WORKAROUND: Azure AI Projects 'create responses' API has schema divergence from OpenAI's
# Responses API. Azure requires 'type' at item level and 'annotations' in content items.
# See: https://github.com/Azure/azure-sdk-for-python/issues/44493
# See: https://github.com/microsoft/agent-framework/issues/2926
# TODO(agent-framework#2926): Remove this workaround when Azure SDK aligns with OpenAI schema.
if "input" in run_options and isinstance(run_options["input"], list):
run_options["input"] = self._transform_input_for_azure_ai(cast(list[dict[str, Any]], run_options["input"]))
if not self._is_application_endpoint:
# Application-scoped response APIs do not support "agent" property.
agent_reference = await self._get_agent_reference_or_create(run_options, instructions)
@@ -393,6 +402,44 @@ class AzureAIClient(OpenAIBaseResponsesClient):
return run_options
def _transform_input_for_azure_ai(self, input_items: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Transform input items to match Azure AI Projects expected schema.
WORKAROUND: Azure AI Projects 'create responses' API expects a different schema than OpenAI's
Responses API. Azure requires 'type' at the item level, and requires 'annotations'
only for output_text content items (assistant messages), not for input_text content items
(user messages). This helper adapts the OpenAI-style input to the Azure schema.
See: https://github.com/Azure/azure-sdk-for-python/issues/44493
TODO(agent-framework#2926): Remove when Azure SDK aligns with OpenAI schema.
"""
transformed: list[dict[str, Any]] = []
for item in input_items:
new_item: dict[str, Any] = dict(item)
# Add 'type': 'message' at item level for role-based items
if "role" in new_item and "type" not in new_item:
new_item["type"] = "message"
# Add 'annotations' only to output_text content items (assistant messages)
# User messages (input_text) do NOT support annotations in Azure AI
if "content" in new_item and isinstance(new_item["content"], list):
new_content: list[dict[str, Any] | Any] = []
for content_item in new_item["content"]:
if isinstance(content_item, dict):
new_content_item: dict[str, Any] = dict(content_item)
# Only add annotations to output_text (assistant content)
if new_content_item.get("type") == "output_text" and "annotations" not in new_content_item:
new_content_item["annotations"] = []
new_content.append(new_content_item)
else:
new_content.append(content_item)
new_item["content"] = new_content
transformed.append(new_item)
return transformed
@override
def _get_current_conversation_id(self, chat_options: ChatOptions, **kwargs: Any) -> str | None:
"""Get the current conversation ID from chat options or kwargs."""
@@ -286,6 +286,93 @@ async def test_azure_ai_client_prepare_messages_for_azure_ai_no_system_messages(
assert instructions is None
def test_azure_ai_client_transform_input_for_azure_ai(mock_project_client: MagicMock) -> None:
"""Test _transform_input_for_azure_ai adds required fields for Azure AI schema.
WORKAROUND TEST: Azure AI Projects API requires 'type' at item level and
'annotations' in output_text content items, which OpenAI's Responses API does not require.
See: https://github.com/Azure/azure-sdk-for-python/issues/44493
See: https://github.com/microsoft/agent-framework/issues/2926
"""
client = create_test_azure_ai_client(mock_project_client)
# Input in OpenAI Responses API format (what agent-framework generates)
openai_format_input = [
{
"role": "user",
"content": [
{"type": "input_text", "text": "Hello"},
],
},
{
"role": "assistant",
"content": [
{"type": "output_text", "text": "Hi there!"},
],
},
]
result = client._transform_input_for_azure_ai(openai_format_input) # type: ignore
# Verify 'type': 'message' added at item level
assert result[0]["type"] == "message"
assert result[1]["type"] == "message"
# Verify 'annotations' added ONLY to output_text (assistant) content, NOT input_text (user)
assert result[0]["content"][0]["type"] == "input_text" # user content type preserved
assert "annotations" not in result[0]["content"][0] # user message - no annotations
assert result[1]["content"][0]["type"] == "output_text" # assistant content type preserved
assert result[1]["content"][0]["annotations"] == [] # assistant message - has annotations
# Verify original fields preserved
assert result[0]["role"] == "user"
assert result[0]["content"][0]["text"] == "Hello"
assert result[1]["role"] == "assistant"
assert result[1]["content"][0]["text"] == "Hi there!"
def test_azure_ai_client_transform_input_preserves_existing_fields(mock_project_client: MagicMock) -> None:
"""Test _transform_input_for_azure_ai preserves existing type and annotations."""
client = create_test_azure_ai_client(mock_project_client)
# Input that already has the fields (shouldn't duplicate)
input_with_fields = [
{
"type": "message",
"role": "assistant",
"content": [
{"type": "output_text", "text": "Hello", "annotations": [{"some": "annotation"}]},
],
},
]
result = client._transform_input_for_azure_ai(input_with_fields) # type: ignore
# Should preserve existing values, not overwrite
assert result[0]["type"] == "message"
assert result[0]["content"][0]["annotations"] == [{"some": "annotation"}]
def test_azure_ai_client_transform_input_handles_non_dict_content(mock_project_client: MagicMock) -> None:
"""Test _transform_input_for_azure_ai handles non-dict content items."""
client = create_test_azure_ai_client(mock_project_client)
# Input with string content (edge case)
input_with_string_content = [
{
"role": "user",
"content": ["plain string content"],
},
]
result = client._transform_input_for_azure_ai(input_with_string_content) # type: ignore
# Should add 'type': 'message' at item level even with non-dict content
assert result[0]["type"] == "message"
# Non-dict content items should be preserved without modification
assert result[0]["content"] == ["plain string content"]
async def test_azure_ai_client_prepare_options_basic(mock_project_client: MagicMock) -> None:
"""Test prepare_options basic functionality."""
client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent", agent_version="1.0")