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
41 changes: 25 additions & 16 deletions python/packages/azure-ai/agent_framework_azure_ai/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@
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 (
Expand Down Expand Up @@ -86,24 +90,24 @@ def __init__(
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(
Expand Down Expand Up @@ -211,12 +215,20 @@ async def _get_agent_reference_or_create(
"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
Expand All @@ -226,8 +238,6 @@ async def _get_agent_reference_or_create(
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)
)
Expand Down Expand Up @@ -288,13 +298,12 @@ async def prepare_options(

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

Expand Down
2 changes: 1 addition & 1 deletion python/packages/azure-ai/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
Expand Down
85 changes: 84 additions & 1 deletion python/packages/azure-ai/tests/test_azure_ai_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
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

Expand Down Expand Up @@ -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."""
Expand Down
16 changes: 10 additions & 6 deletions python/packages/core/agent_framework/openai/_responses_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,17 +92,17 @@ async def _inner_get_response(
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,
)
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,
)
Expand Down Expand Up @@ -135,7 +135,8 @@ async def _inner_get_streaming_response(
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,
Expand All @@ -148,7 +149,7 @@ async def _inner_get_streaming_response(
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:
Expand Down Expand Up @@ -311,7 +312,6 @@ async def prepare_options(
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
Expand All @@ -320,6 +320,10 @@ async def prepare_options(
"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",
Expand Down
71 changes: 71 additions & 0 deletions python/samples/getting_started/agents/azure_ai/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Azure AI Agent Examples

This folder contains examples demonstrating different ways to create and use agents with the Azure AI client from the `agent_framework.azure` package.

## Examples

| File | Description |
|------|-------------|
| [`azure_ai_basic.py`](azure_ai_basic.py) | The simplest way to create an agent using `AzureAIClient`. Demonstrates both streaming and non-streaming responses with function tools. Shows automatic agent creation and basic weather functionality. |
| [`azure_ai_use_latest_version.py`](azure_ai_use_latest_version.py) | Demonstrates how to reuse the latest version of an existing agent instead of creating a new agent version on each instantiation using the `use_latest_version=True` parameter. |
| [`azure_ai_with_code_interpreter.py`](azure_ai_with_code_interpreter.py) | Shows how to use the `HostedCodeInterpreterTool` with Azure AI agents to write and execute Python code for mathematical problem solving and data analysis. |
| [`azure_ai_with_existing_agent.py`](azure_ai_with_existing_agent.py) | Shows how to work with a pre-existing agent by providing the agent name and version to the Azure AI client. Demonstrates agent reuse patterns for production scenarios. |
| [`azure_ai_with_existing_conversation.py`](azure_ai_with_existing_conversation.py) | Shows how to work with a pre-existing conversation by providing the conversation ID to continue existing chat sessions. |
| [`azure_ai_with_explicit_settings.py`](azure_ai_with_explicit_settings.py) | Shows how to create an agent with explicitly configured `AzureAIClient` settings, including project endpoint, model deployment, and credentials rather than relying on environment variable defaults. |
| [`azure_ai_with_response_format.py`](azure_ai_with_response_format.py) | Shows how to use structured outputs (response format) with Azure AI agents using Pydantic models to enforce specific response schemas. |
| [`azure_ai_with_thread.py`](azure_ai_with_thread.py) | Demonstrates thread management with Azure AI agents, including automatic thread creation for stateless conversations and explicit thread management for maintaining conversation context across multiple interactions. |

## Environment Variables

Before running the examples, you need to set up your environment variables. You can do this in one of two ways:

### Option 1: Using a .env file (Recommended)

1. Copy the `.env.example` file from the `python` directory to create a `.env` file:

```bash
cp ../../../../.env.example ../../../../.env
```

2. Edit the `.env` file and add your values:

```env
AZURE_AI_PROJECT_ENDPOINT="your-project-endpoint"
AZURE_AI_MODEL_DEPLOYMENT_NAME="your-model-deployment-name"
```

### Option 2: Using environment variables directly

Set the environment variables in your shell:

```bash
export AZURE_AI_PROJECT_ENDPOINT="your-project-endpoint"
export AZURE_AI_MODEL_DEPLOYMENT_NAME="your-model-deployment-name"
```

### Required Variables

- `AZURE_AI_PROJECT_ENDPOINT`: Your Azure AI project endpoint (required for all examples)
- `AZURE_AI_MODEL_DEPLOYMENT_NAME`: The name of your model deployment (required for all examples)

## Authentication

All examples use `AzureCliCredential` for authentication by default. Before running the examples:

1. Install the Azure CLI
2. Run `az login` to authenticate with your Azure account
3. Ensure you have appropriate permissions to the Azure AI project

Alternatively, you can replace `AzureCliCredential` with other authentication options like `DefaultAzureCredential` or environment-based credentials.

## Running the Examples

Each example can be run independently. Navigate to this directory and run any example:

```bash
python azure_ai_basic.py
python azure_ai_with_code_interpreter.py
# ... etc
```

The examples demonstrate various patterns for working with Azure AI agents, from basic usage to advanced scenarios like thread management and structured outputs.
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"""
Azure AI Agent Basic Example

This sample demonstrates basic usage of AzureAIAgentClient.
This sample demonstrates basic usage of AzureAIClient.
Shows both streaming and non-streaming responses with function tools.
"""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
from pydantic import Field

"""
Azure AI Agent Basic Example
Azure AI Agent Latest Version Example

This sample demonstrates how to reuse the latest version of an existing agent instead of creating a new agent version on each instantiation.
The first call creates a new agent, while subsequent calls with `use_latest_version=True` reuse the latest agent version.
This sample demonstrates how to reuse the latest version of an existing agent
instead of creating a new agent version on each instantiation. The first call creates a new agent,
while subsequent calls with `use_latest_version=True` reuse the latest agent version.
"""


Expand Down
Loading