Skip to content
Merged
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
49 changes: 48 additions & 1 deletion python/packages/azure-ai/agent_framework_azure_ai/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand All @@ -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. 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."""
Expand Down
87 changes: 87 additions & 0 deletions python/packages/azure-ai/tests/test_azure_ai_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading