From 0e38ce80db4fff9b658a5ebf12edd124244e094f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 16:26:59 +0000 Subject: [PATCH 1/7] Initial plan From 0c44fc2aafdca2cb3356caa5520d99c3ec47859f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 16:34:51 +0000 Subject: [PATCH 2/7] feat: extend AzureOpenAIResponsesClient to support Foundry project endpoints Add project_client and project_endpoint parameters to allow creating the client via an Azure AI Foundry project. When provided, the client uses AIProjectClient.get_openai_client() to obtain the OpenAI client. The azure-ai-projects package is imported lazily and only required when using the project endpoint path. Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com> --- .../azure/_responses_client.py | 102 ++++++++++++++ .../azure/test_azure_responses_client.py | 128 ++++++++++++++++++ 2 files changed, 230 insertions(+) diff --git a/python/packages/core/agent_framework/azure/_responses_client.py b/python/packages/core/agent_framework/azure/_responses_client.py index cc57beb57c..d61a15073c 100644 --- a/python/packages/core/agent_framework/azure/_responses_client.py +++ b/python/packages/core/agent_framework/azure/_responses_client.py @@ -12,6 +12,7 @@ from pydantic import ValidationError from .._middleware import ChatMiddlewareLayer +from .._telemetry import AGENT_FRAMEWORK_USER_AGENT from .._tools import FunctionInvocationConfiguration, FunctionInvocationLayer from ..exceptions import ServiceInitializationError from ..observability import ChatTelemetryLayer @@ -73,6 +74,8 @@ def __init__( credential: TokenCredential | None = None, default_headers: Mapping[str, str] | None = None, async_client: AsyncAzureOpenAI | None = None, + project_client: Any | None = None, + project_endpoint: str | None = None, env_file_path: str | None = None, env_file_encoding: str | None = None, instruction_role: str | None = None, @@ -82,6 +85,14 @@ def __init__( ) -> None: """Initialize an Azure OpenAI Responses client. + The client can be created in two ways: + + 1. **Direct Azure OpenAI** (default): Provide endpoint, api_key, or credential + to connect directly to an Azure OpenAI deployment. + 2. **Foundry project endpoint**: Provide a ``project_client`` or ``project_endpoint`` + (with ``credential``) to create the client via an Azure AI Foundry project. + This requires the ``azure-ai-projects`` package to be installed. + Keyword Args: api_key: The API key. If provided, will override the value in the env vars or .env file. Can also be set via environment variable AZURE_OPENAI_API_KEY. @@ -105,6 +116,12 @@ def __init__( default_headers: The default headers mapping of string keys to string values for HTTP requests. async_client: An existing client to use. + project_client: An existing ``AIProjectClient`` (from ``azure.ai.projects.aio``) to use. + The OpenAI client will be obtained via ``project_client.get_openai_client()``. + Requires the ``azure-ai-projects`` package. + project_endpoint: The Azure AI Foundry project endpoint URL. + When provided with ``credential``, an ``AIProjectClient`` will be created + and used to obtain the OpenAI client. Requires the ``azure-ai-projects`` package. env_file_path: Use the environment settings file as a fallback to using env vars. env_file_encoding: The encoding of the environment settings file, defaults to 'utf-8'. instruction_role: The role to use for 'instruction' messages, for example, summarization @@ -132,6 +149,27 @@ def __init__( # Or loading from a .env file client = AzureOpenAIResponsesClient(env_file_path="path/to/.env") + # Using a Foundry project endpoint + from azure.identity import DefaultAzureCredential + + client = AzureOpenAIResponsesClient( + project_endpoint="https://your-project.services.ai.azure.com", + deployment_name="gpt-4o", + credential=DefaultAzureCredential(), + ) + + # Or using an existing AIProjectClient + from azure.ai.projects.aio import AIProjectClient + + project_client = AIProjectClient( + endpoint="https://your-project.services.ai.azure.com", + credential=DefaultAzureCredential(), + ) + client = AzureOpenAIResponsesClient( + project_client=project_client, + deployment_name="gpt-4o", + ) + # Using custom ChatOptions with type safety: from typing import TypedDict from agent_framework.azure import AzureOpenAIResponsesOptions @@ -146,6 +184,18 @@ class MyOptions(AzureOpenAIResponsesOptions, total=False): """ if model_id := kwargs.pop("model_id", None) and not deployment_name: deployment_name = str(model_id) + + # Project client path: create OpenAI client from an Azure AI Foundry project + if project_client is not None or project_endpoint is not None: + async_client = self._create_client_from_project( + project_client=project_client, + project_endpoint=project_endpoint, + credential=credential, + deployment_name=deployment_name, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + ) + try: azure_openai_settings = AzureOpenAISettings( # pydantic settings will see if there is a value, if not, will try the env var or .env file @@ -195,6 +245,58 @@ class MyOptions(AzureOpenAIResponsesOptions, total=False): function_invocation_configuration=function_invocation_configuration, ) + @staticmethod + def _create_client_from_project( + *, + project_client: Any | None, + project_endpoint: str | None, + credential: TokenCredential | None, + deployment_name: str | None, + env_file_path: str | None, + env_file_encoding: str | None, + ) -> AsyncAzureOpenAI: + """Create an AsyncOpenAI client from an Azure AI Foundry project. + + Args: + project_client: An existing AIProjectClient to use. + project_endpoint: The Azure AI Foundry project endpoint URL. + credential: Azure credential for authentication. + deployment_name: The deployment name (used as model_id). + env_file_path: Path to environment file. + env_file_encoding: Encoding of the environment file. + + Returns: + An AsyncAzureOpenAI client obtained from the project client. + + Raises: + ServiceInitializationError: If required parameters are missing or + the azure-ai-projects package is not installed. + """ + try: + from azure.ai.projects.aio import AIProjectClient + except ImportError as exc: + raise ServiceInitializationError( + "The 'azure-ai-projects' package is required to use project_client or project_endpoint. " + "Please install it with: pip install azure-ai-projects" + ) from exc + + if project_client is None: + if not project_endpoint: + raise ServiceInitializationError( + "Azure AI project endpoint is required when project_client is not provided." + ) + if not credential: + raise ServiceInitializationError( + "Azure credential is required when using project_endpoint without a project_client." + ) + project_client = AIProjectClient( + endpoint=project_endpoint, + credential=credential, # type: ignore[arg-type] + user_agent=AGENT_FRAMEWORK_USER_AGENT, + ) + + return project_client.get_openai_client() # type: ignore[return-value] + @override def _check_model_presence(self, run_options: dict[str, Any]) -> None: if not run_options.get("model"): diff --git a/python/packages/core/tests/azure/test_azure_responses_client.py b/python/packages/core/tests/azure/test_azure_responses_client.py index 434674d50c..a0bd071686 100644 --- a/python/packages/core/tests/azure/test_azure_responses_client.py +++ b/python/packages/core/tests/azure/test_azure_responses_client.py @@ -3,6 +3,7 @@ import json import os from typing import Annotated, Any +from unittest.mock import MagicMock import pytest from azure.identity import AzureCliCredential @@ -115,6 +116,133 @@ def test_init_with_empty_model_id(azure_openai_unit_test_env: dict[str, str]) -> ) +def test_init_with_project_client(azure_openai_unit_test_env: dict[str, str]) -> None: + """Test initialization with an existing AIProjectClient.""" + from unittest.mock import MagicMock, patch + + from openai import AsyncOpenAI + + # Create a mock AIProjectClient that returns a mock AsyncOpenAI client + mock_openai_client = MagicMock(spec=AsyncOpenAI) + mock_openai_client.default_headers = {} + + mock_project_client = MagicMock() + mock_project_client.get_openai_client.return_value = mock_openai_client + + with patch( + "agent_framework.azure._responses_client.AzureOpenAIResponsesClient._create_client_from_project", + return_value=mock_openai_client, + ): + azure_responses_client = AzureOpenAIResponsesClient( + project_client=mock_project_client, + deployment_name="gpt-4o", + ) + + assert azure_responses_client.model_id == "gpt-4o" + assert azure_responses_client.client is mock_openai_client + assert isinstance(azure_responses_client, ChatClientProtocol) + + +def test_init_with_project_endpoint(azure_openai_unit_test_env: dict[str, str]) -> None: + """Test initialization with a project endpoint and credential.""" + from unittest.mock import MagicMock, patch + + from openai import AsyncOpenAI + + mock_openai_client = MagicMock(spec=AsyncOpenAI) + mock_openai_client.default_headers = {} + + with patch( + "agent_framework.azure._responses_client.AzureOpenAIResponsesClient._create_client_from_project", + return_value=mock_openai_client, + ): + azure_responses_client = AzureOpenAIResponsesClient( + project_endpoint="https://test-project.services.ai.azure.com", + deployment_name="gpt-4o", + credential=AzureCliCredential(), + ) + + assert azure_responses_client.model_id == "gpt-4o" + assert azure_responses_client.client is mock_openai_client + assert isinstance(azure_responses_client, ChatClientProtocol) + + +def test_create_client_from_project_with_project_client() -> None: + """Test _create_client_from_project with an existing project client.""" + from unittest.mock import MagicMock + + from openai import AsyncOpenAI + + mock_openai_client = MagicMock(spec=AsyncOpenAI) + mock_project_client = MagicMock() + mock_project_client.get_openai_client.return_value = mock_openai_client + + result = AzureOpenAIResponsesClient._create_client_from_project( + project_client=mock_project_client, + project_endpoint=None, + credential=None, + deployment_name="gpt-4o", + env_file_path=None, + env_file_encoding=None, + ) + + assert result is mock_openai_client + mock_project_client.get_openai_client.assert_called_once() + + +def test_create_client_from_project_with_endpoint() -> None: + """Test _create_client_from_project with a project endpoint.""" + from unittest.mock import MagicMock, patch + + from openai import AsyncOpenAI + + mock_openai_client = MagicMock(spec=AsyncOpenAI) + mock_credential = MagicMock() + + with patch("azure.ai.projects.aio.AIProjectClient") as MockAIProjectClient: + mock_instance = MockAIProjectClient.return_value + mock_instance.get_openai_client.return_value = mock_openai_client + + result = AzureOpenAIResponsesClient._create_client_from_project( + project_client=None, + project_endpoint="https://test-project.services.ai.azure.com", + credential=mock_credential, + deployment_name="gpt-4o", + env_file_path=None, + env_file_encoding=None, + ) + + assert result is mock_openai_client + MockAIProjectClient.assert_called_once() + mock_instance.get_openai_client.assert_called_once() + + +def test_create_client_from_project_missing_endpoint() -> None: + """Test _create_client_from_project raises error when endpoint is missing.""" + with pytest.raises(ServiceInitializationError, match="project endpoint is required"): + AzureOpenAIResponsesClient._create_client_from_project( + project_client=None, + project_endpoint=None, + credential=MagicMock(), + deployment_name="gpt-4o", + env_file_path=None, + env_file_encoding=None, + ) + + +def test_create_client_from_project_missing_credential() -> None: + """Test _create_client_from_project raises error when credential is missing.""" + with pytest.raises(ServiceInitializationError, match="credential is required"): + AzureOpenAIResponsesClient._create_client_from_project( + project_client=None, + project_endpoint="https://test-project.services.ai.azure.com", + credential=None, + deployment_name="gpt-4o", + env_file_path=None, + env_file_encoding=None, + ) + + def test_serialize(azure_openai_unit_test_env: dict[str, str]) -> None: default_headers = {"X-Unit-Test": "test-guid"} From e188df882c611eccded86188c7f8c8e62224acf7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 16:35:55 +0000 Subject: [PATCH 3/7] fix: address code review - remove duplicate MagicMock imports in tests Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com> --- .../core/tests/azure/test_azure_responses_client.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/python/packages/core/tests/azure/test_azure_responses_client.py b/python/packages/core/tests/azure/test_azure_responses_client.py index a0bd071686..997854c1a5 100644 --- a/python/packages/core/tests/azure/test_azure_responses_client.py +++ b/python/packages/core/tests/azure/test_azure_responses_client.py @@ -118,7 +118,7 @@ def test_init_with_empty_model_id(azure_openai_unit_test_env: dict[str, str]) -> def test_init_with_project_client(azure_openai_unit_test_env: dict[str, str]) -> None: """Test initialization with an existing AIProjectClient.""" - from unittest.mock import MagicMock, patch + from unittest.mock import patch from openai import AsyncOpenAI @@ -145,7 +145,7 @@ def test_init_with_project_client(azure_openai_unit_test_env: dict[str, str]) -> def test_init_with_project_endpoint(azure_openai_unit_test_env: dict[str, str]) -> None: """Test initialization with a project endpoint and credential.""" - from unittest.mock import MagicMock, patch + from unittest.mock import patch from openai import AsyncOpenAI @@ -169,8 +169,6 @@ def test_init_with_project_endpoint(azure_openai_unit_test_env: dict[str, str]) def test_create_client_from_project_with_project_client() -> None: """Test _create_client_from_project with an existing project client.""" - from unittest.mock import MagicMock - from openai import AsyncOpenAI mock_openai_client = MagicMock(spec=AsyncOpenAI) @@ -192,7 +190,7 @@ def test_create_client_from_project_with_project_client() -> None: def test_create_client_from_project_with_endpoint() -> None: """Test _create_client_from_project with a project endpoint.""" - from unittest.mock import MagicMock, patch + from unittest.mock import patch from openai import AsyncOpenAI From cfea811da3674d764bc7433e5eec217c59c37ae4 Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Wed, 11 Feb 2026 13:46:49 +0100 Subject: [PATCH 4/7] fix: add type field to Responses API input items and add Foundry sample - Add 'type: message' to input items in _prepare_message_for_openai to comply with the Responses API schema requirement - Filter out empty dicts from unsupported content types to prevent sending items with invalid empty type values - Add azure_responses_client_with_foundry.py sample demonstrating AzureOpenAIResponsesClient with project_endpoint - Update README and pyrightconfig.samples.json accordingly --- .../openai/_responses_client.py | 19 ++- .../openai/test_openai_responses_client.py | 16 +-- python/pyrightconfig.samples.json | 3 +- .../agents/azure_openai/README.md | 1 + .../azure_responses_client_with_foundry.py | 113 ++++++++++++++++++ 5 files changed, 133 insertions(+), 19 deletions(-) create mode 100644 python/samples/getting_started/agents/azure_openai/azure_responses_client_with_foundry.py diff --git a/python/packages/core/agent_framework/openai/_responses_client.py b/python/packages/core/agent_framework/openai/_responses_client.py index 5902ad0e46..77bf89df69 100644 --- a/python/packages/core/agent_framework/openai/_responses_client.py +++ b/python/packages/core/agent_framework/openai/_responses_client.py @@ -901,6 +901,7 @@ def _prepare_message_for_openai( """Prepare a chat message for the OpenAI Responses API format.""" all_messages: list[dict[str, Any]] = [] args: dict[str, Any] = { + "type": "message", "role": message.role, } for content in message.contents: @@ -911,16 +912,22 @@ def _prepare_message_for_openai( case "function_result": new_args: dict[str, Any] = {} new_args.update(self._prepare_content_for_openai(message.role, content, call_id_to_id)) # type: ignore[arg-type] - all_messages.append(new_args) + if new_args: + all_messages.append(new_args) case "function_call": function_call = self._prepare_content_for_openai(message.role, content, call_id_to_id) # type: ignore[arg-type] - all_messages.append(function_call) # type: ignore + if function_call: + all_messages.append(function_call) # type: ignore case "function_approval_response" | "function_approval_request": - all_messages.append(self._prepare_content_for_openai(message.role, content, call_id_to_id)) # type: ignore + prepared = self._prepare_content_for_openai(message.role, content, call_id_to_id) + if prepared: + all_messages.append(prepared) # type: ignore case _: - if "content" not in args: - args["content"] = [] - args["content"].append(self._prepare_content_for_openai(message.role, content, call_id_to_id)) # type: ignore + prepared_content = self._prepare_content_for_openai(message.role, content, call_id_to_id) # type: ignore + if prepared_content: + if "content" not in args: + args["content"] = [] + args["content"].append(prepared_content) # type: ignore if "content" in args or "tool_calls" in args: all_messages.append(args) return all_messages diff --git a/python/packages/core/tests/openai/test_openai_responses_client.py b/python/packages/core/tests/openai/test_openai_responses_client.py index e51ed4e989..e3f982d826 100644 --- a/python/packages/core/tests/openai/test_openai_responses_client.py +++ b/python/packages/core/tests/openai/test_openai_responses_client.py @@ -798,12 +798,8 @@ def test_chat_message_with_error_content() -> None: result = client._prepare_message_for_openai(message, call_id_to_id) - # Message should be prepared with empty content list since ErrorContent returns {} - assert len(result) == 1 - prepared_message = result[0] - assert prepared_message["role"] == "assistant" - # Content should be a list with empty dict since ErrorContent returns {} - assert prepared_message.get("content") == [{}] + # Message should be empty since ErrorContent is filtered out + assert len(result) == 0 def test_chat_message_with_usage_content() -> None: @@ -823,12 +819,8 @@ def test_chat_message_with_usage_content() -> None: result = client._prepare_message_for_openai(message, call_id_to_id) - # Message should be prepared with empty content list since UsageContent returns {} - assert len(result) == 1 - prepared_message = result[0] - assert prepared_message["role"] == "assistant" - # Content should be a list with empty dict since UsageContent returns {} - assert prepared_message.get("content") == [{}] + # Message should be empty since UsageContent is filtered out + assert len(result) == 0 def test_hosted_file_content_preparation() -> None: diff --git a/python/pyrightconfig.samples.json b/python/pyrightconfig.samples.json index a74e252474..5dae59f141 100644 --- a/python/pyrightconfig.samples.json +++ b/python/pyrightconfig.samples.json @@ -5,7 +5,8 @@ "**/autogen-migration/**", "**/semantic-kernel-migration/**", "**/demos/**", - "**/agent_with_foundry_tracing.py" + "**/agent_with_foundry_tracing.py", + "**/azure_responses_client_with_foundry.py" ], "typeCheckingMode": "off", "reportMissingImports": "error", diff --git a/python/samples/getting_started/agents/azure_openai/README.md b/python/samples/getting_started/agents/azure_openai/README.md index fea029c209..1bab25bfee 100644 --- a/python/samples/getting_started/agents/azure_openai/README.md +++ b/python/samples/getting_started/agents/azure_openai/README.md @@ -22,6 +22,7 @@ This folder contains examples demonstrating different ways to create and use age | [`azure_responses_client_with_code_interpreter.py`](azure_responses_client_with_code_interpreter.py) | Shows how to use `AzureOpenAIResponsesClient.get_code_interpreter_tool()` with Azure agents to write and execute Python code. Includes helper methods for accessing code interpreter data from response chunks. | | [`azure_responses_client_with_explicit_settings.py`](azure_responses_client_with_explicit_settings.py) | Shows how to initialize an agent with a specific responses client, configuring settings explicitly including endpoint and deployment name. | | [`azure_responses_client_with_file_search.py`](azure_responses_client_with_file_search.py) | Demonstrates using `AzureOpenAIResponsesClient.get_file_search_tool()` with Azure OpenAI Responses Client for direct document-based question answering and information retrieval from vector stores. | +| [`azure_responses_client_with_foundry.py`](azure_responses_client_with_foundry.py) | Shows how to create an agent using an Azure AI Foundry project endpoint instead of a direct Azure OpenAI endpoint. Requires the `azure-ai-projects` package. | | [`azure_responses_client_with_function_tools.py`](azure_responses_client_with_function_tools.py) | Demonstrates how to use function tools with agents. Shows both agent-level tools (defined when creating the agent) and query-level tools (provided with specific queries). | | [`azure_responses_client_with_hosted_mcp.py`](azure_responses_client_with_hosted_mcp.py) | Shows how to integrate Azure OpenAI Responses Client with hosted Model Context Protocol (MCP) servers using `AzureOpenAIResponsesClient.get_mcp_tool()` for extended functionality. | | [`azure_responses_client_with_local_mcp.py`](azure_responses_client_with_local_mcp.py) | Shows how to integrate Azure OpenAI Responses Client with local Model Context Protocol (MCP) servers using MCPStreamableHTTPTool for extended functionality. | diff --git a/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_foundry.py b/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_foundry.py new file mode 100644 index 0000000000..7020121db9 --- /dev/null +++ b/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_foundry.py @@ -0,0 +1,113 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +import os +from random import randint +from typing import Annotated + +from agent_framework import tool +from agent_framework.azure import AzureOpenAIResponsesClient +from azure.identity import AzureCliCredential +from dotenv import load_dotenv +from pydantic import Field + +""" +Azure OpenAI Responses Client with Foundry Project Example + +This sample demonstrates how to create an AzureOpenAIResponsesClient using an +Azure AI Foundry project endpoint. Instead of providing an Azure OpenAI endpoint +directly, you provide a Foundry project endpoint and the client is created via +the Azure AI Foundry project SDK. + +This requires: +- The `azure-ai-projects` package to be installed. +- The `AZURE_AI_PROJECT_ENDPOINT` environment variable set to your Foundry project endpoint. +- The `AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME` environment variable set to the model deployment name. +""" + +load_dotenv() # Load environment variables from .env file if present + + +# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. +@tool(approval_mode="never_require") +def get_weather( + location: Annotated[str, Field(description="The location to get the weather for.")], +) -> str: + """Get the weather for a given location.""" + conditions = ["sunny", "cloudy", "rainy", "stormy"] + return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C." + + +async def non_streaming_example() -> None: + """Example of non-streaming response (get the complete result at once).""" + print("=== Non-streaming Response Example ===") + + # 1. Create the AzureOpenAIResponsesClient using a Foundry project endpoint. + # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred + # authentication option. + credential = AzureCliCredential() + agent = AzureOpenAIResponsesClient( + project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], + deployment_name=os.environ["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"], + credential=credential, + ).as_agent( + instructions="You are a helpful weather agent.", + tools=get_weather, + ) + + # 2. Run a query and print the result. + query = "What's the weather like in Seattle?" + print(f"User: {query}") + result = await agent.run(query) + print(f"Result: {result}\n") + + +async def streaming_example() -> None: + """Example of streaming response (get results as they are generated).""" + print("=== Streaming Response Example ===") + + # 1. Create the AzureOpenAIResponsesClient using a Foundry project endpoint. + # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred + # authentication option. + credential = AzureCliCredential() + agent = AzureOpenAIResponsesClient( + project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], + deployment_name=os.environ["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"], + credential=credential, + ).as_agent( + instructions="You are a helpful weather agent.", + tools=get_weather, + ) + + # 2. Stream the response and print each chunk as it arrives. + query = "What's the weather like in Portland?" + print(f"User: {query}") + print("Agent: ", end="", flush=True) + async for chunk in agent.run(query, stream=True): + if chunk.text: + print(chunk.text, end="", flush=True) + print("\n") + + +async def main() -> None: + print("=== Azure OpenAI Responses Client with Foundry Project Example ===") + + await non_streaming_example() + await streaming_example() + + +if __name__ == "__main__": + asyncio.run(main()) + + +""" +Sample output: +=== Azure OpenAI Responses Client with Foundry Project Example === +=== Non-streaming Response Example === +User: What's the weather like in Seattle? +Result: The weather in Seattle is cloudy with a high of 18°C. + +=== Streaming Response Example === +User: What's the weather like in Portland? +Agent: The weather in Portland is sunny with a high of 25°C. +""" From 3f523ae42fee6e8ad3551384fe0cc3761f7d4837 Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Wed, 11 Feb 2026 14:03:44 +0100 Subject: [PATCH 5/7] updates to response format and setup --- python/packages/azure-ai/pyproject.toml | 1 - .../azure/_responses_client.py | 64 ++++++++----------- .../core/agent_framework/azure/_shared.py | 3 +- .../openai/_responses_client.py | 2 +- python/packages/core/pyproject.toml | 1 + .../azure/test_azure_responses_client.py | 12 ---- python/uv.lock | 4 +- 7 files changed, 31 insertions(+), 56 deletions(-) diff --git a/python/packages/azure-ai/pyproject.toml b/python/packages/azure-ai/pyproject.toml index 86e0d3342f..0ad16c4362 100644 --- a/python/packages/azure-ai/pyproject.toml +++ b/python/packages/azure-ai/pyproject.toml @@ -24,7 +24,6 @@ classifiers = [ ] dependencies = [ "agent-framework-core>=1.0.0b260210", - "azure-ai-projects >= 2.0.0b3", "azure-ai-agents == 1.2.0b5", "aiohttp", ] diff --git a/python/packages/core/agent_framework/azure/_responses_client.py b/python/packages/core/agent_framework/azure/_responses_client.py index d61a15073c..0049979a6a 100644 --- a/python/packages/core/agent_framework/azure/_responses_client.py +++ b/python/packages/core/agent_framework/azure/_responses_client.py @@ -7,8 +7,10 @@ from typing import TYPE_CHECKING, Any, Generic from urllib.parse import urljoin +from azure.ai.projects.aio import AIProjectClient from azure.core.credentials import TokenCredential -from openai.lib.azure import AsyncAzureADTokenProvider, AsyncAzureOpenAI +from openai import AsyncOpenAI +from openai.lib.azure import AsyncAzureADTokenProvider from pydantic import ValidationError from .._middleware import ChatMiddlewareLayer @@ -73,7 +75,7 @@ def __init__( token_endpoint: str | None = None, credential: TokenCredential | None = None, default_headers: Mapping[str, str] | None = None, - async_client: AsyncAzureOpenAI | None = None, + async_client: AsyncOpenAI | None = None, project_client: Any | None = None, project_endpoint: str | None = None, env_file_path: str | None = None, @@ -186,14 +188,11 @@ class MyOptions(AzureOpenAIResponsesOptions, total=False): deployment_name = str(model_id) # Project client path: create OpenAI client from an Azure AI Foundry project - if project_client is not None or project_endpoint is not None: + if async_client is None and (project_client is not None or project_endpoint is not None): async_client = self._create_client_from_project( project_client=project_client, project_endpoint=project_endpoint, credential=credential, - deployment_name=deployment_name, - env_file_path=env_file_path, - env_file_encoding=env_file_encoding, ) try: @@ -248,22 +247,16 @@ class MyOptions(AzureOpenAIResponsesOptions, total=False): @staticmethod def _create_client_from_project( *, - project_client: Any | None, + project_client: AIProjectClient | None, project_endpoint: str | None, credential: TokenCredential | None, - deployment_name: str | None, - env_file_path: str | None, - env_file_encoding: str | None, - ) -> AsyncAzureOpenAI: + ) -> AsyncOpenAI: """Create an AsyncOpenAI client from an Azure AI Foundry project. Args: project_client: An existing AIProjectClient to use. project_endpoint: The Azure AI Foundry project endpoint URL. credential: Azure credential for authentication. - deployment_name: The deployment name (used as model_id). - env_file_path: Path to environment file. - env_file_encoding: Encoding of the environment file. Returns: An AsyncAzureOpenAI client obtained from the project client. @@ -272,34 +265,27 @@ def _create_client_from_project( ServiceInitializationError: If required parameters are missing or the azure-ai-projects package is not installed. """ - try: - from azure.ai.projects.aio import AIProjectClient - except ImportError as exc: + if project_client is not None: + return project_client.get_openai_client() + + if not project_endpoint: raise ServiceInitializationError( - "The 'azure-ai-projects' package is required to use project_client or project_endpoint. " - "Please install it with: pip install azure-ai-projects" - ) from exc - - if project_client is None: - if not project_endpoint: - raise ServiceInitializationError( - "Azure AI project endpoint is required when project_client is not provided." - ) - if not credential: - raise ServiceInitializationError( - "Azure credential is required when using project_endpoint without a project_client." - ) - project_client = AIProjectClient( - endpoint=project_endpoint, - credential=credential, # type: ignore[arg-type] - user_agent=AGENT_FRAMEWORK_USER_AGENT, + "Azure AI project endpoint is required when project_client is not provided." ) - - return project_client.get_openai_client() # type: ignore[return-value] + if not credential: + raise ServiceInitializationError( + "Azure credential is required when using project_endpoint without a project_client." + ) + project_client = AIProjectClient( + endpoint=project_endpoint, + credential=credential, # type: ignore[arg-type] + user_agent=AGENT_FRAMEWORK_USER_AGENT, + ) + return project_client.get_openai_client() @override - def _check_model_presence(self, run_options: dict[str, Any]) -> None: - if not run_options.get("model"): + def _check_model_presence(self, options: dict[str, Any]) -> None: + if not options.get("model"): if not self.model_id: raise ValueError("deployment_name must be a non-empty string") - run_options["model"] = self.model_id + options["model"] = self.model_id diff --git a/python/packages/core/agent_framework/azure/_shared.py b/python/packages/core/agent_framework/azure/_shared.py index 8e90002a75..5ef0585f96 100644 --- a/python/packages/core/agent_framework/azure/_shared.py +++ b/python/packages/core/agent_framework/azure/_shared.py @@ -9,6 +9,7 @@ from typing import Any, ClassVar, Final from azure.core.credentials import TokenCredential +from openai import AsyncOpenAI from openai.lib.azure import AsyncAzureOpenAI from pydantic import SecretStr, model_validator @@ -162,7 +163,7 @@ def __init__( token_endpoint: str | None = None, credential: TokenCredential | None = None, default_headers: Mapping[str, str] | None = None, - client: AsyncAzureOpenAI | None = None, + client: AsyncOpenAI | None = None, instruction_role: str | None = None, **kwargs: Any, ) -> None: diff --git a/python/packages/core/agent_framework/openai/_responses_client.py b/python/packages/core/agent_framework/openai/_responses_client.py index 77bf89df69..f239221c49 100644 --- a/python/packages/core/agent_framework/openai/_responses_client.py +++ b/python/packages/core/agent_framework/openai/_responses_client.py @@ -919,7 +919,7 @@ def _prepare_message_for_openai( if function_call: all_messages.append(function_call) # type: ignore case "function_approval_response" | "function_approval_request": - prepared = self._prepare_content_for_openai(message.role, content, call_id_to_id) + prepared = self._prepare_content_for_openai(Role(message.role), content, call_id_to_id) if prepared: all_messages.append(prepared) # type: ignore case _: diff --git a/python/packages/core/pyproject.toml b/python/packages/core/pyproject.toml index f4f28c898a..5a90b479e7 100644 --- a/python/packages/core/pyproject.toml +++ b/python/packages/core/pyproject.toml @@ -34,6 +34,7 @@ dependencies = [ # connectors and functions "openai>=1.99.0", "azure-identity>=1,<2", + "azure-ai-projects >= 2.0.0b3", "mcp[ws]>=1.24.0,<2", "packaging>=24.1", ] diff --git a/python/packages/core/tests/azure/test_azure_responses_client.py b/python/packages/core/tests/azure/test_azure_responses_client.py index 997854c1a5..9ab31f0a23 100644 --- a/python/packages/core/tests/azure/test_azure_responses_client.py +++ b/python/packages/core/tests/azure/test_azure_responses_client.py @@ -179,9 +179,6 @@ def test_create_client_from_project_with_project_client() -> None: project_client=mock_project_client, project_endpoint=None, credential=None, - deployment_name="gpt-4o", - env_file_path=None, - env_file_encoding=None, ) assert result is mock_openai_client @@ -205,9 +202,6 @@ def test_create_client_from_project_with_endpoint() -> None: project_client=None, project_endpoint="https://test-project.services.ai.azure.com", credential=mock_credential, - deployment_name="gpt-4o", - env_file_path=None, - env_file_encoding=None, ) assert result is mock_openai_client @@ -222,9 +216,6 @@ def test_create_client_from_project_missing_endpoint() -> None: project_client=None, project_endpoint=None, credential=MagicMock(), - deployment_name="gpt-4o", - env_file_path=None, - env_file_encoding=None, ) @@ -235,9 +226,6 @@ def test_create_client_from_project_missing_credential() -> None: project_client=None, project_endpoint="https://test-project.services.ai.azure.com", credential=None, - deployment_name="gpt-4o", - env_file_path=None, - env_file_encoding=None, ) diff --git a/python/uv.lock b/python/uv.lock index fac55c8e21..24396ef396 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -209,7 +209,6 @@ dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "aiohttp", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "azure-ai-agents", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, - { name = "azure-ai-projects", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] [package.metadata] @@ -217,7 +216,6 @@ requires-dist = [ { name = "agent-framework-core", editable = "packages/core" }, { name = "aiohttp" }, { name = "azure-ai-agents", specifier = "==1.2.0b5" }, - { name = "azure-ai-projects", specifier = ">=2.0.0b3" }, ] [[package]] @@ -324,6 +322,7 @@ name = "agent-framework-core" version = "1.0.0b260210" source = { editable = "packages/core" } dependencies = [ + { name = "azure-ai-projects", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "azure-identity", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "mcp", extra = ["ws"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "openai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -378,6 +377,7 @@ requires-dist = [ { name = "agent-framework-orchestrations", marker = "extra == 'all'", editable = "packages/orchestrations" }, { name = "agent-framework-purview", marker = "extra == 'all'", editable = "packages/purview" }, { name = "agent-framework-redis", marker = "extra == 'all'", editable = "packages/redis" }, + { name = "azure-ai-projects", specifier = ">=2.0.0b3" }, { name = "azure-identity", specifier = ">=1,<2" }, { name = "mcp", extras = ["ws"], specifier = ">=1.24.0,<2" }, { name = "openai", specifier = ">=1.99.0" }, From 42495cea679f022196fa8d82ac42f671b15f9ef3 Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Wed, 11 Feb 2026 15:08:15 +0100 Subject: [PATCH 6/7] fix: patch AIProjectClient at correct module path in test Patch agent_framework.azure._responses_client.AIProjectClient instead of azure.ai.projects.aio.AIProjectClient since the import is at module level. --- .../core/tests/azure/test_azure_responses_client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/python/packages/core/tests/azure/test_azure_responses_client.py b/python/packages/core/tests/azure/test_azure_responses_client.py index 9ab31f0a23..1d40c769db 100644 --- a/python/packages/core/tests/azure/test_azure_responses_client.py +++ b/python/packages/core/tests/azure/test_azure_responses_client.py @@ -140,7 +140,7 @@ def test_init_with_project_client(azure_openai_unit_test_env: dict[str, str]) -> assert azure_responses_client.model_id == "gpt-4o" assert azure_responses_client.client is mock_openai_client - assert isinstance(azure_responses_client, ChatClientProtocol) + assert isinstance(azure_responses_client, SupportsChatGetResponse) def test_init_with_project_endpoint(azure_openai_unit_test_env: dict[str, str]) -> None: @@ -164,7 +164,7 @@ def test_init_with_project_endpoint(azure_openai_unit_test_env: dict[str, str]) assert azure_responses_client.model_id == "gpt-4o" assert azure_responses_client.client is mock_openai_client - assert isinstance(azure_responses_client, ChatClientProtocol) + assert isinstance(azure_responses_client, SupportsChatGetResponse) def test_create_client_from_project_with_project_client() -> None: @@ -194,7 +194,7 @@ def test_create_client_from_project_with_endpoint() -> None: mock_openai_client = MagicMock(spec=AsyncOpenAI) mock_credential = MagicMock() - with patch("azure.ai.projects.aio.AIProjectClient") as MockAIProjectClient: + with patch("agent_framework.azure._responses_client.AIProjectClient") as MockAIProjectClient: mock_instance = MockAIProjectClient.return_value mock_instance.get_openai_client.return_value = mock_openai_client From 7e519d8d982c8b0f910d0500af447c27a13747b4 Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Wed, 11 Feb 2026 16:15:36 +0100 Subject: [PATCH 7/7] docs: add Foundry sample to READMEs and document AZURE_AI_PROJECT_ENDPOINT env var --- python/samples/README.md | 1 + python/samples/getting_started/agents/azure_openai/README.md | 3 +++ 2 files changed, 4 insertions(+) diff --git a/python/samples/README.md b/python/samples/README.md index fc64dced52..eb234cdc3e 100644 --- a/python/samples/README.md +++ b/python/samples/README.md @@ -78,6 +78,7 @@ This directory contains samples demonstrating the capabilities of Microsoft Agen | [`getting_started/agents/azure_openai/azure_responses_client_image_analysis.py`](./getting_started/agents/azure_openai/azure_responses_client_image_analysis.py) | Azure OpenAI Responses Client with Image Analysis Example | | [`getting_started/agents/azure_openai/azure_responses_client_with_code_interpreter.py`](./getting_started/agents/azure_openai/azure_responses_client_with_code_interpreter.py) | Azure OpenAI Responses Client with Code Interpreter Example | | [`getting_started/agents/azure_openai/azure_responses_client_with_explicit_settings.py`](./getting_started/agents/azure_openai/azure_responses_client_with_explicit_settings.py) | Azure OpenAI Responses Client with Explicit Settings Example | +| [`getting_started/agents/azure_openai/azure_responses_client_with_foundry.py`](./getting_started/agents/azure_openai/azure_responses_client_with_foundry.py) | Azure OpenAI Responses Client with Foundry Project Example | | [`getting_started/agents/azure_openai/azure_responses_client_with_function_tools.py`](./getting_started/agents/azure_openai/azure_responses_client_with_function_tools.py) | Azure OpenAI Responses Client with Function Tools Example | | [`getting_started/agents/azure_openai/azure_responses_client_with_hosted_mcp.py`](./getting_started/agents/azure_openai/azure_responses_client_with_hosted_mcp.py) | Azure OpenAI Responses Client with Hosted Model Context Protocol (MCP) Example | | [`getting_started/agents/azure_openai/azure_responses_client_with_local_mcp.py`](./getting_started/agents/azure_openai/azure_responses_client_with_local_mcp.py) | Azure OpenAI Responses Client with local Model Context Protocol (MCP) Example | diff --git a/python/samples/getting_started/agents/azure_openai/README.md b/python/samples/getting_started/agents/azure_openai/README.md index 1bab25bfee..614e60b14d 100644 --- a/python/samples/getting_started/agents/azure_openai/README.md +++ b/python/samples/getting_started/agents/azure_openai/README.md @@ -36,6 +36,9 @@ Make sure to set the following environment variables before running the examples - `AZURE_OPENAI_CHAT_DEPLOYMENT_NAME`: The name of your Azure OpenAI chat model deployment - `AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME`: The name of your Azure OpenAI Responses deployment +For the Foundry project sample (`azure_responses_client_with_foundry.py`), also set: +- `AZURE_AI_PROJECT_ENDPOINT`: Your Azure AI Foundry project endpoint + Optionally, you can set: - `AZURE_OPENAI_API_VERSION`: The API version to use (default is `2024-02-15-preview`) - `AZURE_OPENAI_API_KEY`: Your Azure OpenAI API key (if not using `AzureCliCredential`)