mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
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:
committed by
GitHub
Unverified
parent
0aba02c402
commit
928c9d54ad
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user