mirror of
https://github.com/microsoft/semantic-kernel.git
synced 2026-10-12 02:19:06 +00:00
### Motivation and Context <!-- Thank you for your contribution to the semantic-kernel repo! Please help reviewers and future users, providing the following information: 1. Why is this change required? 2. What problem does it solve? 3. What scenario does it contribute to? 4. If it fixes an open issue, please link to the issue here. --> The usage of `DefaultAzureCredential` by default is unsafe and raises security issues. Instead, users should provide their own credential types such as `AzureCliCredential`, `ManagedIdentityCredential` and so on. More information here: https://learn.microsoft.com/en-us/python/api/azure-identity/azure.identity.aio?view=azure-python In this PR: 1. Removed the usage of `DefaultAzureCredential` from authentication logic. 2. Replaced `DefaultAzureCredential` with `AzureCliCredential` in all examples. **Note**: this is breaking change for Azure users, who relies on `DefaultAzureCredential`. Users will need to update the code and pass the credential type of their choice by using `credential` parameter when initializing one of the Azure clients. Before: ```python chat_completion = AzureChatCompletion() ``` After: ```python chat_completion = AzureChatCompletion(credential=AzureCliCredential()) ``` ### Contribution Checklist <!-- Before submitting this PR, please make sure: --> - [x] The code builds clean without any errors or warnings - [x] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [x] All unit tests pass, and I have added new tests where possible - [ ] I didn't break anyone 😄
294 lines
12 KiB
Python
294 lines
12 KiB
Python
# Copyright (c) Microsoft. All rights reserved.
|
|
|
|
import logging
|
|
import sys
|
|
from collections.abc import Awaitable, Callable
|
|
from copy import copy
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
from azure.core.credentials import TokenCredential
|
|
from openai import AsyncAzureOpenAI
|
|
from pydantic import ValidationError
|
|
|
|
from semantic_kernel.agents import OpenAIResponsesAgent
|
|
from semantic_kernel.agents.agent import register_agent_type
|
|
from semantic_kernel.connectors.ai.open_ai.settings.azure_open_ai_settings import AzureOpenAISettings
|
|
from semantic_kernel.exceptions.agent_exceptions import (
|
|
AgentInitializationException,
|
|
)
|
|
from semantic_kernel.utils.authentication.entra_id_authentication import get_entra_auth_token
|
|
from semantic_kernel.utils.feature_stage_decorator import experimental
|
|
from semantic_kernel.utils.telemetry.user_agent import APP_INFO, prepend_semantic_kernel_to_user_agent
|
|
|
|
if TYPE_CHECKING:
|
|
from semantic_kernel.kernel_pydantic import KernelBaseSettings
|
|
|
|
logger: logging.Logger = logging.getLogger(__name__)
|
|
|
|
if sys.version_info >= (3, 12):
|
|
from typing import override # pragma: no cover
|
|
else:
|
|
from typing_extensions import override # pragma: no cover
|
|
|
|
if sys.version < "3.11":
|
|
from typing_extensions import Self # pragma: no cover
|
|
else:
|
|
from typing import Self # type: ignore # pragma: no cover
|
|
|
|
if sys.version_info >= (3, 13):
|
|
from warnings import deprecated
|
|
else:
|
|
from typing_extensions import deprecated
|
|
|
|
|
|
@experimental
|
|
@register_agent_type("azure_responses")
|
|
class AzureResponsesAgent(OpenAIResponsesAgent):
|
|
"""Azure Responses Agent class.
|
|
|
|
Provides the ability to interact with Azure's Responses API.
|
|
"""
|
|
|
|
@staticmethod
|
|
@deprecated(
|
|
"setup_resources is deprecated. Use AzureResponsesAgent.create_client() instead. This method will be removed by 2025-06-15." # noqa: E501
|
|
)
|
|
def setup_resources(
|
|
*,
|
|
ad_token: str | None = None,
|
|
ad_token_provider: Callable[[], str | Awaitable[str]] | None = None,
|
|
api_key: str | None = None,
|
|
api_version: str | None = None,
|
|
base_url: str | None = None,
|
|
default_headers: dict[str, str] | None = None,
|
|
deployment_name: str | None = None,
|
|
endpoint: str | None = None,
|
|
env_file_path: str | None = None,
|
|
env_file_encoding: str | None = None,
|
|
token_scope: str | None = None,
|
|
credential: TokenCredential | None = None,
|
|
**kwargs: Any,
|
|
) -> tuple[AsyncAzureOpenAI, str]:
|
|
"""A method to create the Azure OpenAI client and the deployment name/model from the provided arguments.
|
|
|
|
Any arguments provided will override the values in the environment variables/environment file.
|
|
|
|
Args:
|
|
ad_token: The Microsoft Entra (previously Azure AD) token represented as a string
|
|
ad_token_provider: The Microsoft Entra (previously Azure AD) token provider provided as a callback
|
|
api_key: The API key
|
|
api_version: The API version
|
|
base_url: The base URL in the form https://<resource>.azure.openai.com/openai/deployments/<deployment_name>
|
|
default_headers: The default headers to add to the client
|
|
deployment_name: The Responses deployment name
|
|
endpoint: The endpoint in the form https://<resource>.azure.openai.com
|
|
env_file_path: The environment file path
|
|
env_file_encoding: The environment file encoding, defaults to utf-8
|
|
token_scope: The token scope
|
|
credential: The credential to use for authentication.
|
|
kwargs: Additional keyword arguments
|
|
|
|
Returns:
|
|
An Azure OpenAI client instance and the configured deployment name (model)
|
|
"""
|
|
try:
|
|
azure_openai_settings = AzureOpenAISettings(
|
|
api_key=api_key,
|
|
base_url=base_url,
|
|
endpoint=endpoint,
|
|
responses_deployment_name=deployment_name,
|
|
api_version=api_version,
|
|
env_file_path=env_file_path,
|
|
env_file_encoding=env_file_encoding,
|
|
token_endpoint=token_scope,
|
|
)
|
|
except ValidationError as exc:
|
|
raise AgentInitializationException(f"Failed to create Azure OpenAI settings: {exc}") from exc
|
|
|
|
if (
|
|
azure_openai_settings.api_key is None
|
|
and ad_token_provider is None
|
|
and ad_token is None
|
|
and azure_openai_settings.token_endpoint
|
|
and credential
|
|
):
|
|
ad_token = get_entra_auth_token(credential, azure_openai_settings.token_endpoint)
|
|
|
|
# If we still have no credentials, we can't proceed
|
|
if not azure_openai_settings.api_key and not ad_token and not ad_token_provider and not credential:
|
|
raise AgentInitializationException(
|
|
"Please provide either an api_key, ad_token, ad_token_provider or credential for authentication."
|
|
)
|
|
|
|
merged_headers = dict(copy(default_headers)) if default_headers else {}
|
|
if default_headers:
|
|
merged_headers.update(default_headers)
|
|
if APP_INFO:
|
|
merged_headers.update(APP_INFO)
|
|
merged_headers = prepend_semantic_kernel_to_user_agent(merged_headers)
|
|
|
|
if not azure_openai_settings.endpoint:
|
|
raise AgentInitializationException("Please provide an Azure OpenAI endpoint")
|
|
|
|
if not azure_openai_settings.responses_deployment_name:
|
|
raise AgentInitializationException("Please provide an Azure OpenAI Responses deployment name")
|
|
|
|
client = AsyncAzureOpenAI(
|
|
azure_endpoint=str(azure_openai_settings.endpoint),
|
|
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,
|
|
azure_ad_token=ad_token,
|
|
azure_ad_token_provider=ad_token_provider,
|
|
default_headers=merged_headers,
|
|
**kwargs,
|
|
)
|
|
|
|
return client, azure_openai_settings.responses_deployment_name
|
|
|
|
@staticmethod
|
|
def create_client(
|
|
*,
|
|
ad_token: str | None = None,
|
|
ad_token_provider: Callable[[], str | Awaitable[str]] | None = None,
|
|
api_key: str | None = None,
|
|
api_version: str | None = None,
|
|
base_url: str | None = None,
|
|
default_headers: dict[str, str] | None = None,
|
|
deployment_name: str | None = None,
|
|
endpoint: str | None = None,
|
|
env_file_path: str | None = None,
|
|
env_file_encoding: str | None = None,
|
|
token_scope: str | None = None,
|
|
credential: TokenCredential | None = None,
|
|
**kwargs: Any,
|
|
) -> AsyncAzureOpenAI:
|
|
"""A method to create the Azure OpenAI client.
|
|
|
|
Any arguments provided will override the values in the environment variables/environment file.
|
|
|
|
Args:
|
|
ad_token: The Microsoft Entra (previously Azure AD) token represented as a string
|
|
ad_token_provider: The Microsoft Entra (previously Azure AD) token provider provided as a callback
|
|
api_key: The API key
|
|
api_version: The API version
|
|
base_url: The base URL in the form https://<resource>.azure.openai.com/openai/deployments/<deployment_name>
|
|
default_headers: The default headers to add to the client
|
|
deployment_name: The Responses deployment name
|
|
endpoint: The endpoint in the form https://<resource>.azure.openai.com
|
|
env_file_path: The environment file path
|
|
env_file_encoding: The environment file encoding, defaults to utf-8
|
|
token_scope: The token scope
|
|
credential: The credential to use for authentication.
|
|
kwargs: Additional keyword arguments
|
|
|
|
Returns:
|
|
An Azure OpenAI client instance.
|
|
"""
|
|
try:
|
|
azure_openai_settings = AzureOpenAISettings(
|
|
api_key=api_key,
|
|
base_url=base_url,
|
|
endpoint=endpoint,
|
|
responses_deployment_name=deployment_name,
|
|
api_version=api_version,
|
|
env_file_path=env_file_path,
|
|
env_file_encoding=env_file_encoding,
|
|
token_endpoint=token_scope,
|
|
)
|
|
except ValidationError as exc:
|
|
raise AgentInitializationException(f"Failed to create Azure OpenAI settings: {exc}") from exc
|
|
|
|
if (
|
|
azure_openai_settings.api_key is None
|
|
and ad_token_provider is None
|
|
and ad_token is None
|
|
and azure_openai_settings.token_endpoint
|
|
and credential
|
|
):
|
|
ad_token = get_entra_auth_token(credential, azure_openai_settings.token_endpoint)
|
|
|
|
# If we still have no credentials, we can't proceed
|
|
if not azure_openai_settings.api_key and not ad_token and not ad_token_provider and not credential:
|
|
raise AgentInitializationException(
|
|
"Please provide either an api_key, ad_token, ad_token_provider or credential for authentication."
|
|
)
|
|
|
|
merged_headers = dict(copy(default_headers)) if default_headers else {}
|
|
if default_headers:
|
|
merged_headers.update(default_headers)
|
|
if APP_INFO:
|
|
merged_headers.update(APP_INFO)
|
|
merged_headers = prepend_semantic_kernel_to_user_agent(merged_headers)
|
|
|
|
if not azure_openai_settings.endpoint:
|
|
raise AgentInitializationException("Please provide an Azure OpenAI endpoint")
|
|
|
|
if not azure_openai_settings.responses_deployment_name:
|
|
raise AgentInitializationException("Please provide an Azure OpenAI Responses deployment name")
|
|
|
|
return AsyncAzureOpenAI(
|
|
azure_endpoint=str(azure_openai_settings.endpoint),
|
|
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,
|
|
azure_ad_token=ad_token,
|
|
azure_ad_token_provider=ad_token_provider,
|
|
default_headers=merged_headers,
|
|
**kwargs,
|
|
)
|
|
|
|
@override
|
|
@classmethod
|
|
def resolve_placeholders(
|
|
cls: type[Self],
|
|
yaml_str: str,
|
|
settings: "KernelBaseSettings | None" = None,
|
|
extras: dict[str, Any] | None = None,
|
|
) -> str:
|
|
"""Substitute ${AzureOpenAI:Key} placeholders with fields from AzureOpenAIAgentSettings and extras."""
|
|
import re
|
|
|
|
pattern = re.compile(r"\$\{([^}]+)\}")
|
|
|
|
# Build the mapping only if settings is provided and valid
|
|
field_mapping: dict[str, Any] = {}
|
|
|
|
if settings is None:
|
|
settings = AzureOpenAISettings()
|
|
|
|
if not isinstance(settings, AzureOpenAISettings):
|
|
raise AgentInitializationException(f"Expected AzureOpenAISettings, got {type(settings).__name__}")
|
|
|
|
field_mapping.update({
|
|
"ChatModelId": getattr(settings, "responses_deployment_name", None),
|
|
"AgentId": getattr(settings, "agent_id", None),
|
|
"ApiKey": getattr(settings, "api_key", None),
|
|
"ApiVersion": getattr(settings, "api_version", None),
|
|
"BaseUrl": getattr(settings, "base_url", None),
|
|
"Endpoint": getattr(settings, "endpoint", None),
|
|
"TokenEndpoint": getattr(settings, "token_endpoint", None),
|
|
})
|
|
|
|
if extras:
|
|
field_mapping.update(extras)
|
|
|
|
def replacer(match: re.Match[str]) -> str:
|
|
"""Replace the matched placeholder with the corresponding value from field_mapping."""
|
|
full_key = match.group(1) # for example, AzureOpenAI:ApiKey
|
|
section, _, key = full_key.partition(":")
|
|
if section != "AzureOpenAI":
|
|
return match.group(0)
|
|
|
|
# Try short key first (ApiKey), then full (AzureOpenAI:ApiKey)
|
|
return str(field_mapping.get(key) or field_mapping.get(full_key) or match.group(0))
|
|
|
|
result = pattern.sub(replacer, yaml_str)
|
|
|
|
# Safety check for unresolved placeholders
|
|
unresolved = pattern.findall(result)
|
|
if unresolved:
|
|
raise AgentInitializationException(
|
|
f"Unresolved placeholders in spec: {', '.join(f'${{{key}}}' for key in unresolved)}"
|
|
)
|
|
|
|
return result
|