From 6117023177331d81a1ae862a60e494deaae90911 Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Mon, 5 Jan 2026 10:51:33 +0900 Subject: [PATCH 1/3] Fix AzureAIClient failure when conversation history contains assistant messages --- .../agent_framework_azure_ai/_client.py | 49 ++++++++++- .../azure-ai/tests/test_azure_ai_client.py | 83 +++++++++++++++++++ 2 files changed, 131 insertions(+), 1 deletion(-) diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_client.py b/python/packages/azure-ai/agent_framework_azure_ai/_client.py index e10fc19068..ba8cac0931 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_client.py @@ -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 @@ async def _prepare_options( """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 @@ async def _prepare_options( 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. Specifically: + - Input items with 'role' need 'type': 'message' at item level + - Assistant message content items (output_text) need 'annotations' field + + 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 cast(list[Any], 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.""" diff --git a/python/packages/azure-ai/tests/test_azure_ai_client.py b/python/packages/azure-ai/tests/test_azure_ai_client.py index 028e8fbdb8..e5cf3ed3ab 100644 --- a/python/packages/azure-ai/tests/test_azure_ai_client.py +++ b/python/packages/azure-ai/tests/test_azure_ai_client.py @@ -286,6 +286,89 @@ 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 "annotations" not in result[0]["content"][0] # user message - no annotations + 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 handle gracefully 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") From bac6241c71ae1c4cd21f5673f4e516911a74c258 Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Mon, 5 Jan 2026 11:35:26 +0900 Subject: [PATCH 2/3] Address PR review feedback: improve docstring and test assertions --- .../packages/azure-ai/agent_framework_azure_ai/_client.py | 8 ++++---- python/packages/azure-ai/tests/test_azure_ai_client.py | 6 +++++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_client.py b/python/packages/azure-ai/agent_framework_azure_ai/_client.py index ba8cac0931..cc43d37826 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_client.py @@ -405,10 +405,10 @@ async def _prepare_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. Specifically: - - Input items with 'role' need 'type': 'message' at item level - - Assistant message content items (output_text) need 'annotations' field + 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. diff --git a/python/packages/azure-ai/tests/test_azure_ai_client.py b/python/packages/azure-ai/tests/test_azure_ai_client.py index e5cf3ed3ab..2ca49c2033 100644 --- a/python/packages/azure-ai/tests/test_azure_ai_client.py +++ b/python/packages/azure-ai/tests/test_azure_ai_client.py @@ -319,7 +319,9 @@ def test_azure_ai_client_transform_input_for_azure_ai(mock_project_client: Magic 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 @@ -365,7 +367,9 @@ def test_azure_ai_client_transform_input_handles_non_dict_content(mock_project_c result = client._transform_input_for_azure_ai(input_with_string_content) # type: ignore - # Should handle gracefully without modification + # 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"] From 3b689b3fcbfeda519034c2e8df9098d681ef3326 Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Mon, 5 Jan 2026 11:42:11 +0900 Subject: [PATCH 3/3] Remove redundant cast --- python/packages/azure-ai/agent_framework_azure_ai/_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_client.py b/python/packages/azure-ai/agent_framework_azure_ai/_client.py index cc43d37826..b06b7b5df0 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_client.py @@ -425,7 +425,7 @@ def _transform_input_for_azure_ai(self, input_items: list[dict[str, Any]]) -> li # 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 cast(list[Any], new_item["content"]): + 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)