mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
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:
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user