Python: Azure Responses client (#311)

* Azure Responses client

* Fix a change made in the wrong place

* allow api_version and token_endpoint to use env vars

* Add getting started sample

* add responses deployment name env var

* update azure clients to use defaults for api_version and token_endpoint

* make tests more reliable

---------

Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
This commit is contained in:
peterychang
2025-08-06 14:18:38 +00:00
committed by GitHub
co-authored by Chris
parent 5a1de491d4
commit f43939d803
12 changed files with 555 additions and 75 deletions
@@ -5,6 +5,7 @@ import importlib.metadata
from ._assistants_client import AzureAssistantsClient
from ._chat_client import AzureChatClient
from ._entra_id_authentication import get_entra_auth_token
from ._responses_client import AzureResponsesClient
from ._shared import AzureOpenAISettings
try:
@@ -16,6 +17,7 @@ __all__ = [
"AzureAssistantsClient",
"AzureChatClient",
"AzureOpenAISettings",
"AzureResponsesClient",
"__version__",
"get_entra_auth_token",
]
@@ -10,7 +10,6 @@ from pydantic import SecretStr, ValidationError
from pydantic.networks import AnyUrl
from ._shared import (
DEFAULT_AZURE_TOKEN_ENDPOINT,
AzureOpenAISettings,
)
@@ -75,10 +74,11 @@ class AzureAssistantsClient(OpenAIAssistantsClient):
base_url=AnyUrl(base_url) if base_url else None,
endpoint=AnyUrl(endpoint) if endpoint else None,
chat_deployment_name=deployment_name,
api_version=api_version or self.DEFAULT_AZURE_API_VERSION,
api_version=api_version,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
token_endpoint=token_endpoint or DEFAULT_AZURE_TOKEN_ENDPOINT,
token_endpoint=token_endpoint,
default_api_version=self.DEFAULT_AZURE_API_VERSION,
)
except ValidationError as ex:
raise ServiceInitializationError("Failed to create Azure OpenAI settings.", ex) from ex
@@ -26,8 +26,6 @@ from pydantic import SecretStr, ValidationError
from pydantic.networks import AnyUrl
from ._shared import (
DEFAULT_AZURE_API_VERSION,
DEFAULT_AZURE_TOKEN_ENDPOINT,
AzureOpenAIConfigBase,
AzureOpenAISettings,
)
@@ -87,16 +85,18 @@ class AzureChatClient(AzureOpenAIConfigBase, OpenAIChatClientBase):
base_url=AnyUrl(base_url) if base_url else None,
endpoint=AnyUrl(endpoint) if endpoint else None,
chat_deployment_name=deployment_name,
api_version=api_version or DEFAULT_AZURE_API_VERSION,
api_version=api_version,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
token_endpoint=token_endpoint or DEFAULT_AZURE_TOKEN_ENDPOINT,
token_endpoint=token_endpoint,
)
except ValidationError as exc:
raise ServiceInitializationError(f"Failed to validate settings: {exc}") from exc
if not azure_openai_settings.chat_deployment_name:
raise ServiceInitializationError("chat_deployment_name is required.")
if not azure_openai_settings.api_version:
raise ServiceInitializationError("api_version is required.")
super().__init__(
deployment_name=azure_openai_settings.chat_deployment_name,
@@ -0,0 +1,131 @@
# Copyright (c) Microsoft. All rights reserved.
from collections.abc import Mapping
from typing import Any, ClassVar
from urllib.parse import urljoin
from agent_framework import use_tool_calling
from agent_framework.exceptions import ServiceInitializationError
from agent_framework.openai._responses_client import OpenAIResponsesClientBase
from agent_framework.openai._shared import OpenAIModelTypes
from agent_framework.telemetry import use_telemetry
from openai.lib.azure import AsyncAzureADTokenProvider, AsyncAzureOpenAI
from pydantic import SecretStr, ValidationError
from pydantic.networks import AnyUrl
from ._shared import (
AzureOpenAIConfigBase,
AzureOpenAISettings,
)
@use_telemetry
@use_tool_calling
class AzureResponsesClient(AzureOpenAIConfigBase, OpenAIResponsesClientBase):
"""Azure Responses completion class."""
MODEL_PROVIDER_NAME: ClassVar[str] = "azure_openai" # type: ignore[reportIncompatibleVariableOverride, misc]
def __init__(
self,
api_key: str | None = None,
deployment_name: str | None = None,
endpoint: str | None = None,
base_url: str | None = None,
api_version: str | None = None,
ad_token: str | None = None,
ad_token_provider: AsyncAzureADTokenProvider | None = None,
token_endpoint: str | None = None,
default_headers: Mapping[str, str] | None = None,
async_client: AsyncAzureOpenAI | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
instruction_role: str | None = None,
) -> None:
"""Initialize an AzureResponses service.
Args:
api_key: The optional api key. If provided, will override the value in the
env vars or .env file.
deployment_name: The optional deployment. If provided, will override the value
(responses_deployment_name) in the env vars or .env file.
endpoint: The optional deployment endpoint. If provided will override the value
in the env vars or .env file.
base_url: The optional deployment base_url. If provided will override the value
in the env vars or .env file. Currently, the base_url must end with "/openai/v1/"
api_version: The optional deployment api version. If provided will override the value
in the env vars or .env file. Currently, the api_version must be "preview".
ad_token: The Azure Active Directory token. (Optional)
ad_token_provider: The Azure Active Directory token provider. (Optional)
token_endpoint: The token endpoint to request an Azure token. (Optional)
default_headers: The default headers mapping of string keys to
string values for HTTP requests. (Optional)
async_client: An existing client to use. (Optional)
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
prompts could use `developer` or `system`. (Optional)
"""
try:
# Filter out any None values from the arguments
azure_openai_settings = AzureOpenAISettings(
api_key=SecretStr(api_key) if api_key else None,
base_url=AnyUrl(base_url) if base_url else None,
endpoint=AnyUrl(endpoint) if endpoint else None,
responses_deployment_name=deployment_name,
api_version=api_version,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
token_endpoint=token_endpoint,
default_api_version="preview",
)
# TODO(peterychang): This is a temporary hack to ensure that the base_url is set correctly
# while this feature is in preview.
# But we should only do this if we're on azure. Private deployments may not need this.
if (
not azure_openai_settings.base_url
and azure_openai_settings.endpoint
and str(azure_openai_settings.endpoint).rstrip("/").endswith("openai.azure.com")
):
azure_openai_settings.base_url = AnyUrl(urljoin(str(azure_openai_settings.endpoint), "/openai/v1/"))
except ValidationError as exc:
raise ServiceInitializationError(f"Failed to validate settings: {exc}") from exc
if not azure_openai_settings.responses_deployment_name:
raise ServiceInitializationError("responses_deployment_name is required.")
if not azure_openai_settings.api_version:
raise ServiceInitializationError("api_version is required.")
super().__init__(
deployment_name=azure_openai_settings.responses_deployment_name,
endpoint=azure_openai_settings.endpoint,
base_url=azure_openai_settings.base_url,
api_version=azure_openai_settings.api_version,
api_key=azure_openai_settings.api_key.get_secret_value() if azure_openai_settings.api_key else None,
ad_token=ad_token,
ad_token_provider=ad_token_provider,
token_endpoint=azure_openai_settings.token_endpoint,
default_headers=default_headers,
ai_model_type=OpenAIModelTypes.RESPONSE,
client=async_client,
instruction_role=instruction_role,
)
@classmethod
def from_dict(cls, settings: dict[str, Any]) -> "AzureResponsesClient":
"""Initialize an Open AI service from a dictionary of settings.
Args:
settings: A dictionary of settings for the service.
"""
return AzureResponsesClient(
api_key=settings.get("api_key"),
deployment_name=settings.get("deployment_name"),
endpoint=settings.get("endpoint"),
base_url=settings.get("base_url"),
api_version=settings.get("api_version"),
ad_token=settings.get("ad_token"),
ad_token_provider=settings.get("ad_token_provider"),
default_headers=settings.get("default_headers"),
env_file_path=settings.get("env_file_path"),
)
@@ -1,7 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
import logging
import sys
from collections.abc import Awaitable, Callable, Mapping
from copy import copy
from typing import Any, ClassVar, Final
@@ -11,10 +11,15 @@ from agent_framework.exceptions import ServiceInitializationError
from agent_framework.openai._shared import OpenAIHandler, OpenAIModelTypes
from agent_framework.telemetry import USER_AGENT_KEY
from openai.lib.azure import AsyncAzureOpenAI
from pydantic import ConfigDict, SecretStr, validate_call
from pydantic import ConfigDict, SecretStr, model_validator, validate_call
from ._entra_id_authentication import get_entra_auth_token
if sys.version_info >= (3, 11):
from typing import Self # pragma: no cover
else:
from typing_extensions import Self # pragma: no cover
logger: logging.Logger = logging.getLogger(__name__)
@@ -94,11 +99,15 @@ class AzureOpenAISettings(AFBaseSettings):
your resource from the Azure portal, the endpoint should end in openai.azure.com.
If both base_url and endpoint are supplied, base_url will be used.
(Env var AZURE_OPENAI_ENDPOINT)
api_version: The API version to use. The default value is "2024-02-01".
api_version: The API version to use. The default value is `default_api_version`.
(Env var AZURE_OPENAI_API_VERSION)
token_endpoint: The token endpoint to use to retrieve the authentication token.
The default value is "https://cognitiveservices.azure.com/.default".
The default value is `default_token_endpoint`.
(Env var AZURE_OPENAI_TOKEN_ENDPOINT)
default_api_version: The default API version to use if not specified.
The default value is "2024-10-21".
default_token_endpoint: The default token endpoint to use if not specified.
The default value is "https://cognitiveservices.azure.com/.default".
Parameters:
env_file_path: The path to the .env file to load settings from.
@@ -118,8 +127,10 @@ class AzureOpenAISettings(AFBaseSettings):
endpoint: HttpsUrl | None = None
base_url: HttpsUrl | None = None
api_key: SecretStr | None = None
api_version: str = DEFAULT_AZURE_API_VERSION
token_endpoint: str = DEFAULT_AZURE_TOKEN_ENDPOINT
api_version: str | None = None
token_endpoint: str | None = None
default_api_version: str = DEFAULT_AZURE_API_VERSION
default_token_endpoint: str = DEFAULT_AZURE_TOKEN_ENDPOINT
def get_azure_openai_auth_token(self, token_endpoint: str | None = None) -> str | None:
"""Retrieve a Microsoft Entra Auth Token for a given token endpoint for the use with Azure OpenAI.
@@ -143,6 +154,12 @@ class AzureOpenAISettings(AFBaseSettings):
raise ServiceInitializationError("Please provide a token endpoint to retrieve the authentication token.")
return get_entra_auth_token(endpoint_to_use)
@model_validator(mode="after")
def _validate_fields(self) -> Self:
self.api_version = self.api_version or self.default_api_version
self.token_endpoint = self.token_endpoint or self.default_token_endpoint
return self
class AzureOpenAIConfigBase(OpenAIHandler):
"""Internal class for configuring a connection to an Azure OpenAI service."""