mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: [Feature Branch] Structured Outputs and more examples for AzureAIClient (#1987)
* Small updates * Added support for structured outputs * Added code interpreter example * More examples and fixes * Added more examples and README * Small fix * Addressed PR feedback
This commit is contained in:
committed by
GitHub
Unverified
parent
915c749e41
commit
9423c1763c
@@ -17,7 +17,11 @@ from agent_framework.exceptions import ServiceInitializationError
|
||||
from agent_framework.observability import use_observability
|
||||
from agent_framework.openai._responses_client import OpenAIBaseResponsesClient
|
||||
from azure.ai.projects.aio import AIProjectClient
|
||||
from azure.ai.projects.models import PromptAgentDefinition
|
||||
from azure.ai.projects.models import (
|
||||
PromptAgentDefinition,
|
||||
PromptAgentDefinitionText,
|
||||
ResponseTextFormatConfigurationJsonSchema,
|
||||
)
|
||||
from azure.core.credentials_async import AsyncTokenCredential
|
||||
from azure.core.exceptions import ResourceNotFoundError
|
||||
from openai.types.responses.parsed_response import (
|
||||
@@ -86,24 +90,24 @@ class AzureAIClient(OpenAIBaseResponsesClient):
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework.azure import AzureAIAgentClient
|
||||
from agent_framework.azure import AzureAIClient
|
||||
from azure.identity.aio import DefaultAzureCredential
|
||||
|
||||
# Using environment variables
|
||||
# Set AZURE_AI_PROJECT_ENDPOINT=https://your-project.cognitiveservices.azure.com
|
||||
# Set AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4
|
||||
credential = DefaultAzureCredential()
|
||||
client = AzureAIAgentClient(async_credential=credential)
|
||||
client = AzureAIClient(async_credential=credential)
|
||||
|
||||
# Or passing parameters directly
|
||||
client = AzureAIAgentClient(
|
||||
client = AzureAIClient(
|
||||
project_endpoint="https://your-project.cognitiveservices.azure.com",
|
||||
model_deployment_name="gpt-4",
|
||||
async_credential=credential,
|
||||
)
|
||||
|
||||
# Or loading from a .env file
|
||||
client = AzureAIAgentClient(async_credential=credential, env_file_path="path/to/.env")
|
||||
client = AzureAIClient(async_credential=credential, env_file_path="path/to/.env")
|
||||
"""
|
||||
try:
|
||||
azure_ai_settings = AzureAISettings(
|
||||
@@ -211,12 +215,20 @@ class AzureAIClient(OpenAIBaseResponsesClient):
|
||||
"can also be passed to the get_response methods."
|
||||
)
|
||||
|
||||
args: dict[str, Any] = {
|
||||
"model": run_options["model"],
|
||||
}
|
||||
args: dict[str, Any] = {"model": run_options["model"]}
|
||||
|
||||
if "tools" in run_options:
|
||||
args["tools"] = run_options["tools"]
|
||||
|
||||
if "response_format" in run_options:
|
||||
response_format = run_options["response_format"]
|
||||
args["text"] = PromptAgentDefinitionText(
|
||||
format=ResponseTextFormatConfigurationJsonSchema(
|
||||
name=response_format.__name__,
|
||||
schema=response_format.model_json_schema(),
|
||||
)
|
||||
)
|
||||
|
||||
# Combine instructions from messages and options
|
||||
combined_instructions = [
|
||||
instructions
|
||||
@@ -226,8 +238,6 @@ class AzureAIClient(OpenAIBaseResponsesClient):
|
||||
if combined_instructions:
|
||||
args["instructions"] = "".join(combined_instructions)
|
||||
|
||||
# TODO (dmytrostruk): Add response format
|
||||
|
||||
created_agent = await self.project_client.agents.create_version(
|
||||
agent_name=agent_name, definition=PromptAgentDefinition(**args)
|
||||
)
|
||||
@@ -288,13 +298,12 @@ class AzureAIClient(OpenAIBaseResponsesClient):
|
||||
|
||||
run_options["extra_body"] = {"agent": agent_reference}
|
||||
|
||||
# Remove properties that are not supported
|
||||
# Model and tools captured in the agent setup
|
||||
if "model" in run_options:
|
||||
run_options.pop("model", None)
|
||||
# Remove properties that are not supported on request level
|
||||
# but were configured on agent level
|
||||
exclude = ["model", "tools", "response_format"]
|
||||
|
||||
if "tools" in run_options:
|
||||
run_options.pop("tools", None)
|
||||
for property in exclude:
|
||||
run_options.pop(property, None)
|
||||
|
||||
return run_options
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ classifiers = [
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core",
|
||||
"azure-ai-projects >= 2.0.0a20251103001",
|
||||
"azure-ai-projects >= 2.0.0a20251105001",
|
||||
"azure-ai-agents == 1.2.0b5",
|
||||
"aiohttp",
|
||||
]
|
||||
|
||||
@@ -11,7 +11,10 @@ from agent_framework import (
|
||||
TextContent,
|
||||
)
|
||||
from agent_framework.exceptions import ServiceInitializationError
|
||||
from pydantic import ValidationError
|
||||
from azure.ai.projects.models import (
|
||||
ResponseTextFormatConfigurationJsonSchema,
|
||||
)
|
||||
from pydantic import BaseModel, ConfigDict, ValidationError
|
||||
|
||||
from agent_framework_azure_ai import AzureAIClient, AzureAISettings
|
||||
|
||||
@@ -531,6 +534,86 @@ async def test_azure_ai_client_use_latest_version_with_existing_agent_version(
|
||||
assert agent_ref == {"name": "test-agent", "version": "3.0", "type": "agent_reference"}
|
||||
|
||||
|
||||
class ResponseFormatModel(BaseModel):
|
||||
"""Test Pydantic model for response format testing."""
|
||||
|
||||
name: str
|
||||
value: int
|
||||
description: str
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
async def test_azure_ai_client_agent_creation_with_response_format(
|
||||
mock_project_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test agent creation with response_format configuration."""
|
||||
client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent")
|
||||
|
||||
# Mock agent creation response
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.name = "test-agent"
|
||||
mock_agent.version = "1.0"
|
||||
mock_project_client.agents.create_version = AsyncMock(return_value=mock_agent)
|
||||
|
||||
run_options = {"model": "test-model", "response_format": ResponseFormatModel}
|
||||
|
||||
await client._get_agent_reference_or_create(run_options, None) # type: ignore
|
||||
|
||||
# Verify agent was created with response format configuration
|
||||
call_args = mock_project_client.agents.create_version.call_args
|
||||
created_definition = call_args[1]["definition"]
|
||||
|
||||
# Check that text format configuration was set
|
||||
assert hasattr(created_definition, "text")
|
||||
assert created_definition.text is not None
|
||||
|
||||
# Check that the format is a ResponseTextFormatConfigurationJsonSchema
|
||||
assert hasattr(created_definition.text, "format")
|
||||
format_config = created_definition.text.format
|
||||
assert isinstance(format_config, ResponseTextFormatConfigurationJsonSchema)
|
||||
|
||||
# Check the schema name matches the model class name
|
||||
assert format_config.name == "ResponseFormatModel"
|
||||
|
||||
# Check that schema was generated correctly
|
||||
assert format_config.schema is not None
|
||||
schema = format_config.schema
|
||||
assert "properties" in schema
|
||||
assert "name" in schema["properties"]
|
||||
assert "value" in schema["properties"]
|
||||
assert "description" in schema["properties"]
|
||||
|
||||
|
||||
async def test_azure_ai_client_prepare_options_excludes_response_format(
|
||||
mock_project_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test that prepare_options excludes response_format from final run options."""
|
||||
client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent", agent_version="1.0")
|
||||
|
||||
messages = [ChatMessage(role=Role.USER, contents=[TextContent(text="Hello")])]
|
||||
chat_options = ChatOptions()
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
client.__class__.__bases__[0],
|
||||
"prepare_options",
|
||||
return_value={"model": "test-model", "response_format": ResponseFormatModel},
|
||||
),
|
||||
patch.object(
|
||||
client,
|
||||
"_get_agent_reference_or_create",
|
||||
return_value={"name": "test-agent", "version": "1.0", "type": "agent_reference"},
|
||||
),
|
||||
):
|
||||
run_options = await client.prepare_options(messages, chat_options)
|
||||
|
||||
# response_format should be excluded from final run options
|
||||
assert "response_format" not in run_options
|
||||
# But extra_body should contain agent reference
|
||||
assert "extra_body" in run_options
|
||||
assert run_options["extra_body"]["agent"]["name"] == "test-agent"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_project_client() -> MagicMock:
|
||||
"""Fixture that provides a mock AIProjectClient."""
|
||||
|
||||
@@ -92,7 +92,8 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
client = await self.ensure_client()
|
||||
run_options = await self.prepare_options(messages, chat_options)
|
||||
try:
|
||||
if not chat_options.response_format:
|
||||
response_format = run_options.pop("response_format", None)
|
||||
if not response_format:
|
||||
response = await client.responses.create(
|
||||
stream=False,
|
||||
**run_options,
|
||||
@@ -100,9 +101,8 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
chat_options.conversation_id = self.get_conversation_id(response, chat_options.store)
|
||||
return self._create_response_content(response, chat_options=chat_options)
|
||||
# create call does not support response_format, so we need to handle it via parse call
|
||||
resp_format = chat_options.response_format
|
||||
parsed_response: ParsedResponse[BaseModel] = await client.responses.parse(
|
||||
text_format=resp_format,
|
||||
text_format=response_format,
|
||||
stream=False,
|
||||
**run_options,
|
||||
)
|
||||
@@ -135,7 +135,8 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
run_options = await self.prepare_options(messages, chat_options)
|
||||
function_call_ids: dict[int, tuple[str, str]] = {} # output_index: (call_id, name)
|
||||
try:
|
||||
if not chat_options.response_format:
|
||||
response_format = run_options.pop("response_format", None)
|
||||
if not response_format:
|
||||
response = await client.responses.create(
|
||||
stream=True,
|
||||
**run_options,
|
||||
@@ -148,7 +149,7 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
return
|
||||
# create call does not support response_format, so we need to handle it via stream call
|
||||
async with client.responses.stream(
|
||||
text_format=chat_options.response_format,
|
||||
text_format=response_format,
|
||||
**run_options,
|
||||
) as response:
|
||||
async for chunk in response:
|
||||
@@ -311,7 +312,6 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
run_options: dict[str, Any] = chat_options.to_dict(
|
||||
exclude={
|
||||
"type",
|
||||
"response_format", # handled in inner get methods
|
||||
"presence_penalty", # not supported
|
||||
"frequency_penalty", # not supported
|
||||
"logit_bias", # not supported
|
||||
@@ -320,6 +320,10 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
"instructions", # already added as system message
|
||||
}
|
||||
)
|
||||
|
||||
if chat_options.response_format:
|
||||
run_options["response_format"] = chat_options.response_format
|
||||
|
||||
translations = {
|
||||
"model_id": "model",
|
||||
"allow_multiple_tool_calls": "parallel_tool_calls",
|
||||
|
||||
Reference in New Issue
Block a user