Python: allow default_headers configuration and users to pass in custom AzureOpenAI/OpenAI clients. (#3903)

### Motivation and Context

Currently, users in Python have no ability to configure default_headers
for the AzureOpenAI/OpenAI clients we use. Additionally, if a user wants
to set up their own AzureOpenAI/OpenAI client, this isn't possible. This
PR addresses #2895.

<!-- 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.
-->

### Description

Allow users to set default_headers for Chat/Text/Embedding Completion
classes for both OpenAI and AzureOpenAI. Additionally, allow users to
pass in a customer OpenAI/AzureOpenAI client, if desired. Update tests
accordingly.

<!-- Describe your changes, the overall approach, the underlying design.
These notes will help understanding how your code works. Thanks! -->

### Contribution Checklist

<!-- Before submitting this PR, please make sure: -->

- [ ] The code builds clean without any errors or warnings
- [ ] 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
- [ ] All unit tests pass, and I have added new tests where possible
- [ ] I didn't break anyone 😄

---------

Co-authored-by: Evan Mattson <[email protected]>
This commit is contained in:
Evan Mattson
2023-12-05 17:05:13 +00:00
committed by GitHub
co-authored by Evan Mattson
parent 10e19986a8
commit 15f13b285d
26 changed files with 864 additions and 81 deletions
+6 -1
View File
@@ -91,5 +91,10 @@
},
"java.debug.settings.onBuildFailureProceed": true,
"java.compile.nullAnalysis.mode": "disabled",
"dotnet.defaultSolution": "dotnet\\SK-dotnet.sln"
"dotnet.defaultSolution": "dotnet\\SK-dotnet.sln",
"python.testing.pytestArgs": [
"python/tests"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
}
@@ -3,3 +3,4 @@
from typing import final
DEFAULT_AZURE_API_VERSION: final = "2023-05-15"
USER_AGENT: final = "User-Agent"
@@ -2,8 +2,9 @@
from logging import Logger
from typing import Dict, Optional, Union, overload
from typing import Dict, Mapping, Optional, Union, overload
from openai import AsyncAzureOpenAI
from openai.lib.azure import AsyncAzureADTokenProvider
from semantic_kernel.connectors.ai.open_ai.const import DEFAULT_AZURE_API_VERSION
@@ -36,6 +37,7 @@ class AzureChatCompletion(
api_key: Optional[str] = None,
ad_token: Optional[str] = None,
ad_token_provider: Optional[AsyncAzureADTokenProvider] = None,
default_headers: Optional[Mapping[str, str]] = None,
log: Optional[Logger] = None,
) -> None:
"""
@@ -59,6 +61,8 @@ class AzureChatCompletion(
The default value is "2023-05-15".
ad_auth: Whether to use Azure Active Directory authentication. (Optional)
The default value is False.
default_headers: The default headers mapping of string keys to
string values for HTTP requests. (Optional)
log: The logger instance to use. (Optional)
logger: deprecated, use 'log' instead.
"""
@@ -72,6 +76,7 @@ class AzureChatCompletion(
api_key: Optional[str] = None,
ad_token: Optional[str] = None,
ad_token_provider: Optional[AsyncAzureADTokenProvider] = None,
default_headers: Optional[Mapping[str, str]] = None,
log: Optional[Logger] = None,
) -> None:
"""
@@ -93,10 +98,32 @@ class AzureChatCompletion(
The default value is "2023-05-15".
ad_auth: Whether to use Azure Active Directory authentication. (Optional)
The default value is False.
default_headers: The default headers mapping of string keys to
string values for HTTP requests. (Optional)
log: The logger instance to use. (Optional)
logger: deprecated, use 'log' instead.
"""
@overload
def __init__(
self,
deployment_name: str,
async_client: AsyncAzureOpenAI,
log: Optional[Logger] = None,
) -> None:
"""
Initialize an AzureChatCompletion service.
Arguments:
deployment_name: The name of the Azure deployment. This value
will correspond to the custom name you chose for your deployment
when you deployed a model. This value can be found under
Resource Management > Deployments in the Azure portal or, alternatively,
under Management > Deployments in Azure OpenAI Studio.
async_client {AsyncAzureOpenAI} -- An existing client to use.
log: The logger instance to use. (Optional)
"""
def __init__(
self,
deployment_name: str,
@@ -106,8 +133,10 @@ class AzureChatCompletion(
api_key: Optional[str] = None,
ad_token: Optional[str] = None,
ad_token_provider: Optional[AsyncAzureADTokenProvider] = None,
default_headers: Optional[Mapping[str, str]] = None,
log: Optional[Logger] = None,
logger: Optional[Logger] = None,
async_client: Optional[AsyncAzureOpenAI] = None,
) -> None:
"""
Initialize an AzureChatCompletion service.
@@ -134,8 +163,11 @@ class AzureChatCompletion(
The default value is "2023-05-15".
ad_auth: Whether to use Azure Active Directory authentication. (Optional)
The default value is False.
default_headers: The default headers mapping of string keys to
string values for HTTP requests. (Optional)
log: The logger instance to use. (Optional)
logger: deprecated, use 'log' instead.
async_client {Optional[AsyncAzureOpenAI]} -- An existing client to use. (Optional)
"""
if logger:
logger.warning("The 'logger' argument is deprecated, use 'log' instead.")
@@ -151,8 +183,10 @@ class AzureChatCompletion(
api_key=api_key,
ad_token=ad_token,
ad_token_provider=ad_token_provider,
default_headers=default_headers,
log=log or logger,
ai_model_type=OpenAIModelTypes.CHAT,
async_client=async_client,
)
@classmethod
@@ -163,7 +197,7 @@ class AzureChatCompletion(
Arguments:
settings: A dictionary of settings for the service.
should contains keys: deployment_name, endpoint, api_key
and optionally: api_version, ad_auth, log
and optionally: api_version, ad_auth, default_headers, log
"""
return AzureChatCompletion(
deployment_name=settings.get("deployment_name"),
@@ -173,5 +207,6 @@ class AzureChatCompletion(
api_key=settings.get("api_key"),
ad_token=settings.get("ad_token"),
ad_token_provider=settings.get("ad_token_provider"),
default_headers=settings.get("default_headers"),
log=settings.get("log"),
)
@@ -2,13 +2,16 @@
import json
from logging import Logger
from typing import Any, Awaitable, Callable, Dict, Optional, Union
from typing import Any, Awaitable, Callable, Dict, Mapping, Optional, Union
from openai import AsyncAzureOpenAI
from pydantic import validate_call
from semantic_kernel.connectors.ai.ai_exception import AIException
from semantic_kernel.connectors.ai.open_ai.const import DEFAULT_AZURE_API_VERSION
from semantic_kernel.connectors.ai.open_ai.const import (
DEFAULT_AZURE_API_VERSION,
USER_AGENT,
)
from semantic_kernel.connectors.ai.open_ai.services.open_ai_handler import (
OpenAIHandler,
OpenAIModelTypes,
@@ -31,56 +34,82 @@ class AzureOpenAIConfigBase(OpenAIHandler):
api_key: Optional[str] = None,
ad_token: Optional[str] = None,
ad_token_provider: Optional[Callable[[], Union[str, Awaitable[str]]]] = None,
default_headers: Union[Mapping[str, str], None] = None,
log: Optional[Logger] = None,
async_client: Optional[AsyncAzureOpenAI] = None,
) -> None:
# TODO: add SK user-agent here
if not api_key and not ad_token and not ad_token_provider:
raise AIException(
AIException.ErrorCodes.InvalidConfiguration,
"Please provide either api_key, ad_token or ad_token_provider",
)
if base_url:
client = AsyncAzureOpenAI(
base_url=str(base_url),
api_version=api_version,
api_key=api_key,
azure_ad_token=ad_token,
azure_ad_token_provider=ad_token_provider,
default_headers={"User-Agent": json.dumps(APP_INFO)}
if APP_INFO
else None,
)
else:
if not endpoint:
"""Internal class for configuring a connection to an Azure OpenAI service.
Arguments:
deployment_name {str} -- Name of the deployment.
ai_model_type {OpenAIModelTypes} -- The type of OpenAI model to deploy.
endpoint {Optional[HttpsUrl]} -- The specific endpoint URL for the deployment. (Optional)
base_url {Optional[HttpsUrl]} -- The base URL for Azure services. (Optional)
api_version {str} -- Azure API version. Defaults to the defined DEFAULT_AZURE_API_VERSION.
api_key {Optional[str]} -- API key for Azure services. (Optional)
ad_token {Optional[str]} -- Azure AD token for authentication. (Optional)
ad_token_provider {Optional[Callable[[], Union[str, Awaitable[str]]]]} -- A callable
or coroutine function providing Azure AD tokens. (Optional)
default_headers {Union[Mapping[str, str], None]} -- Default headers for HTTP requests. (Optional)
log {Optional[Logger]} -- Logger instance for logging purposes. (Optional)
async_client {Optional[AsyncAzureOpenAI]} -- An existing client to use. (Optional)
The `validate_call` decorator is used with a configuration that allows arbitrary types.
This is necessary for types like `HttpsUrl` and `OpenAIModelTypes`.
"""
# Merge APP_INFO into the headers if it exists
merged_headers = default_headers.copy() if default_headers else {}
if APP_INFO:
merged_headers[USER_AGENT] = json.dumps(APP_INFO)
if not async_client:
if not api_key and not ad_token and not ad_token_provider:
raise AIException(
AIException.ErrorCodes.InvalidConfiguration,
"Please provide either base_url or endpoint",
"Please provide either api_key, ad_token or ad_token_provider",
)
client = AsyncAzureOpenAI(
azure_endpoint=endpoint,
azure_deployment=deployment_name,
api_version=api_version,
api_key=api_key,
azure_ad_token=ad_token,
azure_ad_token_provider=ad_token_provider,
default_headers={"User-Agent": json.dumps(APP_INFO)}
if APP_INFO
else None,
)
if base_url:
async_client = AsyncAzureOpenAI(
base_url=str(base_url),
api_version=api_version,
api_key=api_key,
azure_ad_token=ad_token,
azure_ad_token_provider=ad_token_provider,
default_headers=merged_headers,
)
else:
if not endpoint:
raise AIException(
AIException.ErrorCodes.InvalidConfiguration,
"Please provide either base_url or endpoint",
)
async_client = AsyncAzureOpenAI(
azure_endpoint=endpoint,
azure_deployment=deployment_name,
api_version=api_version,
api_key=api_key,
azure_ad_token=ad_token,
azure_ad_token_provider=ad_token_provider,
default_headers=merged_headers,
)
super().__init__(
ai_model_id=deployment_name,
log=log,
client=client,
client=async_client,
ai_model_type=ai_model_type,
)
def to_dict(self) -> Dict[str, str]:
client_settings = {
"base_url": self.client.base_url,
"base_url": str(self.client.base_url),
"api_version": self.client._custom_query["api-version"],
"api_key": self.client.api_key,
"ad_token": self.client._azure_ad_token,
"ad_token_provider": self.client._azure_ad_token_provider,
"default_headers": {
k: v for k, v in self.client.default_headers.items() if k != USER_AGENT
},
}
base = self.model_dump(
exclude={
@@ -2,8 +2,9 @@
from logging import Logger
from typing import Dict, Optional, overload
from typing import Dict, Mapping, Optional, overload
from openai import AsyncAzureOpenAI
from openai.lib.azure import AsyncAzureADTokenProvider
from semantic_kernel.connectors.ai.open_ai.const import DEFAULT_AZURE_API_VERSION
@@ -29,6 +30,7 @@ class AzureTextCompletion(AzureOpenAIConfigBase, OpenAITextCompletionBase):
api_key: Optional[str] = None,
ad_token: Optional[str] = None,
ad_token_provider: Optional[AsyncAzureADTokenProvider] = None,
default_headers: Optional[Mapping[str, str]] = None,
log: Optional[Logger] = None,
) -> None:
"""
@@ -50,6 +52,8 @@ class AzureTextCompletion(AzureOpenAIConfigBase, OpenAITextCompletionBase):
The default value is "2023-05-15".
ad_auth: Whether to use Azure Active Directory authentication. (Optional)
The default value is False.
default_headers: The default headers mapping of string keys to
string values for HTTP requests. (Optional)
log: The logger instance to use. (Optional)
logger: deprecated, use 'log' instead.
"""
@@ -63,6 +67,7 @@ class AzureTextCompletion(AzureOpenAIConfigBase, OpenAITextCompletionBase):
api_key: Optional[str] = None,
ad_token: Optional[str] = None,
ad_token_provider: Optional[AsyncAzureADTokenProvider] = None,
default_headers: Optional[Mapping[str, str]] = None,
log: Optional[Logger] = None,
) -> None:
"""
@@ -84,10 +89,32 @@ class AzureTextCompletion(AzureOpenAIConfigBase, OpenAITextCompletionBase):
The default value is "2023-05-15".
ad_auth: Whether to use Azure Active Directory authentication. (Optional)
The default value is False.
default_headers: The default headers mapping of string keys to
string values for HTTP requests. (Optional)
log: The logger instance to use. (Optional)
logger: deprecated, use 'log' instead.
"""
@overload
def __init__(
self,
deployment_name: str,
async_client: AsyncAzureOpenAI,
log: Optional[Logger] = None,
) -> None:
"""
Initialize an AzureChatCompletion service.
Arguments:
deployment_name: The name of the Azure deployment. This value
will correspond to the custom name you chose for your deployment
when you deployed a model. This value can be found under
Resource Management > Deployments in the Azure portal or, alternatively,
under Management > Deployments in Azure OpenAI Studio.
async_client {AsyncAzureOpenAI} -- An existing client to use.
log: The logger instance to use. (Optional)
"""
def __init__(
self,
deployment_name: Optional[str] = None,
@@ -97,8 +124,10 @@ class AzureTextCompletion(AzureOpenAIConfigBase, OpenAITextCompletionBase):
api_key: Optional[str] = None,
ad_token: Optional[str] = None,
ad_token_provider: Optional[AsyncAzureADTokenProvider] = None,
default_headers: Optional[Mapping[str, str]] = None,
log: Optional[Logger] = None,
logger: Optional[Logger] = None,
async_client: Optional[AsyncAzureOpenAI] = None,
) -> None:
"""
Initialize an AzureTextCompletion service.
@@ -119,8 +148,11 @@ class AzureTextCompletion(AzureOpenAIConfigBase, OpenAITextCompletionBase):
The default value is "2023-03-15-preview".
ad_auth: Whether to use Azure Active Directory authentication. (Optional)
The default value is False.
default_headers: The default headers mapping of string keys to
string values for HTTP requests. (Optional)
log: The logger instance to use. (Optional)
logger: deprecated, use 'log' instead.
async_client {Optional[AsyncAzureOpenAI]} -- An existing client to use.
"""
if logger:
logger.warning("The 'logger' argument is deprecated, use 'log' instead.")
@@ -132,8 +164,10 @@ class AzureTextCompletion(AzureOpenAIConfigBase, OpenAITextCompletionBase):
api_key=api_key,
ad_token=ad_token,
ad_token_provider=ad_token_provider,
default_headers=default_headers,
log=log or logger,
ai_model_type=OpenAIModelTypes.TEXT,
async_client=async_client,
)
@classmethod
@@ -155,5 +189,6 @@ class AzureTextCompletion(AzureOpenAIConfigBase, OpenAITextCompletionBase):
api_key=settings["api_key"],
ad_token=settings.get("ad_token"),
ad_token_provider=settings.get("ad_token_provider"),
default_headers=settings.get("default_headers"),
log=settings.get("log"),
)
@@ -2,8 +2,9 @@
from logging import Logger
from typing import Dict, Optional
from typing import Dict, Mapping, Optional, overload
from openai import AsyncAzureOpenAI
from openai.lib.azure import AsyncAzureADTokenProvider
from semantic_kernel.connectors.ai.open_ai.const import DEFAULT_AZURE_API_VERSION
@@ -21,16 +22,38 @@ from semantic_kernel.connectors.ai.open_ai.services.open_ai_text_embedding_base
class AzureTextEmbedding(AzureOpenAIConfigBase, OpenAITextEmbeddingBase):
"""Azure Text Embedding class."""
@overload
def __init__(
self,
deployment_name: str,
endpoint: str,
async_client: AsyncAzureOpenAI,
log: Optional[Logger] = None,
) -> None:
"""
Initialize an AzureChatCompletion service.
Arguments:
deployment_name: The name of the Azure deployment. This value
will correspond to the custom name you chose for your deployment
when you deployed a model. This value can be found under
Resource Management > Deployments in the Azure portal or, alternatively,
under Management > Deployments in Azure OpenAI Studio.
async_client {AsyncAzureOpenAI} -- An existing client to use.
log: The logger instance to use. (Optional)
"""
def __init__(
self,
deployment_name: str,
endpoint: Optional[str] = None,
api_version: str = DEFAULT_AZURE_API_VERSION,
api_key: Optional[str] = None,
ad_token: Optional[str] = None,
ad_token_provider: Optional[AsyncAzureADTokenProvider] = None,
default_headers: Optional[Mapping[str, str]] = None,
log: Optional[Logger] = None,
logger: Optional[Logger] = None,
async_client: Optional[AsyncAzureOpenAI] = None,
) -> None:
"""
Initialize an AzureTextEmbedding service.
@@ -46,15 +69,20 @@ class AzureTextEmbedding(AzureOpenAIConfigBase, OpenAITextEmbeddingBase):
:param endpoint: The endpoint of the Azure deployment. This value
can be found in the Keys & Endpoint section when examining
your resource from the Azure portal.
:param api_version: The API version to use. (Optional)
The default value is "2023-05-15".
:param api_key: The API key for the Azure deployment. This value can be
found in the Keys & Endpoint section when examining your resource in
the Azure portal. You can use either KEY1 or KEY2.
:param api_version: The API version to use. (Optional)
The default value is "2023-05-15".
:param log: The logger instance to use. (Optional)
:param logger: Deprecated, please use log instead. (Optional)
:param ad_token : The Azure AD token for authentication. (Optional)
:param ad_auth: Whether to use Azure Active Directory authentication.
(Optional) The default value is False.
:param default_headers: The default headers mapping of string keys to
string values for HTTP requests. (Optional)
:param log: The logger instance to use. (Optional)
:param logger: Deprecated, please use log instead. (Optional)
:param async_client: An existing client to use. (Optional)
"""
if logger:
logger.warning("The 'logger' argument is deprecated, use 'log' instead.")
@@ -65,8 +93,10 @@ class AzureTextEmbedding(AzureOpenAIConfigBase, OpenAITextEmbeddingBase):
api_key=api_key,
ad_token=ad_token,
ad_token_provider=ad_token_provider,
default_headers=default_headers,
log=log or logger,
ai_model_type=OpenAIModelTypes.EMBEDDING,
async_client=async_client,
)
@classmethod
@@ -86,5 +116,6 @@ class AzureTextEmbedding(AzureOpenAIConfigBase, OpenAITextEmbeddingBase):
api_version=settings.get("api_version", DEFAULT_AZURE_API_VERSION),
ad_token=settings.get("ad_token"),
ad_token_provider=settings.get("ad_token_provider"),
default_headers=settings.get("default_headers"),
log=settings.get("log"),
)
@@ -1,7 +1,9 @@
# Copyright (c) Microsoft. All rights reserved.
from logging import Logger
from typing import Dict, Optional
from typing import Dict, Mapping, Optional, overload
from openai import AsyncOpenAI
from semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion_base import (
OpenAIChatCompletionBase,
@@ -22,11 +24,11 @@ class OpenAIChatCompletion(
):
"""OpenAI Chat completion class."""
@overload
def __init__(
self,
ai_model_id: str,
api_key: str,
org_id: Optional[str] = None,
async_client: AsyncOpenAI,
log: Optional[Logger] = None,
) -> None:
"""
@@ -35,19 +37,89 @@ class OpenAIChatCompletion(
Arguments:
ai_model_id {str} -- OpenAI model name, see
https://platform.openai.com/docs/models
api_key {str} -- OpenAI API key, see
async_client {AsyncOpenAI} -- An existing client to use.
log: The logger instance to use. (Optional)
"""
@overload
def __init__(
self,
ai_model_id: str,
api_key: Optional[str] = None,
org_id: Optional[str] = None,
default_headers: Optional[Mapping[str, str]] = None,
log: Optional[Logger] = None,
) -> None:
"""
Initialize an OpenAIChatCompletion service.
Arguments:
ai_model_id {str} -- OpenAI model name, see
https://platform.openai.com/docs/models
api_key {Optional[str]} -- OpenAI API key, see
https://platform.openai.com/account/api-keys
org_id {Optional[str]} -- OpenAI organization ID.
This is usually optional unless your
account belongs to multiple organizations.
default_headers: The default headers mapping of string keys to
string values for HTTP requests. (Optional)
log {Optional[Logger]} -- The logger instance to use. (Optional)
"""
@overload
def __init__(
self,
ai_model_id: str,
api_key: Optional[str] = None,
default_headers: Optional[Mapping[str, str]] = None,
log: Optional[Logger] = None,
) -> None:
"""
Initialize an OpenAIChatCompletion service.
Arguments:
ai_model_id {str} -- OpenAI model name, see
https://platform.openai.com/docs/models
api_key {Optional[str]} -- OpenAI API key, see
https://platform.openai.com/account/api-keys
default_headers: The default headers mapping of string keys to
string values for HTTP requests. (Optional)
log {Optional[Logger]} -- The logger instance to use. (Optional)
"""
def __init__(
self,
ai_model_id: str,
api_key: Optional[str] = None,
org_id: Optional[str] = None,
default_headers: Optional[Mapping[str, str]] = None,
log: Optional[Logger] = None,
async_client: Optional[AsyncOpenAI] = None,
) -> None:
"""
Initialize an OpenAIChatCompletion service.
Arguments:
ai_model_id {str} -- OpenAI model name, see
https://platform.openai.com/docs/models
api_key {Optional[str]} -- OpenAI API key, see
https://platform.openai.com/account/api-keys
org_id {Optional[str]} -- OpenAI organization ID.
This is usually optional unless your
account belongs to multiple organizations.
default_headers: The default headers mapping of string keys to
string values for HTTP requests. (Optional)
log {Optional[Logger]} -- The logger instance to use. (Optional)
async_client {Optional[AsyncOpenAI]} -- An existing client to use. (Optional)
"""
super().__init__(
ai_model_id=ai_model_id,
api_key=api_key,
org_id=org_id,
ai_model_type=OpenAIModelTypes.CHAT,
default_headers=default_headers,
log=log,
async_client=async_client,
)
@classmethod
@@ -63,5 +135,6 @@ class OpenAIChatCompletion(
ai_model_id=settings["ai_model_id"],
api_key=settings["api_key"],
org_id=settings.get("org_id"),
default_headers=settings.get("default_headers"),
log=settings.get("log"),
)
@@ -1,11 +1,16 @@
# Copyright (c) Microsoft. All rights reserved.
import json
from logging import Logger
from typing import Any, Dict, Optional
from typing import Any, Dict, Mapping, Optional
from openai import AsyncOpenAI
from pydantic import Field, validate_call
from semantic_kernel.connectors.ai.ai_exception import AIException
from semantic_kernel.connectors.ai.open_ai.const import (
USER_AGENT,
)
from semantic_kernel.connectors.ai.open_ai.services.open_ai_handler import (
OpenAIHandler,
)
@@ -20,20 +25,53 @@ class OpenAIConfigBase(OpenAIHandler):
def __init__(
self,
ai_model_id: str = Field(min_length=1),
api_key: str = Field(min_length=1),
api_key: Optional[str] = Field(min_length=1),
ai_model_type: Optional[OpenAIModelTypes] = OpenAIModelTypes.CHAT,
org_id: Optional[str] = None,
default_headers: Optional[Mapping[str, str]] = None,
async_client: Optional[AsyncOpenAI] = None,
log: Optional[Logger] = None,
) -> None:
# TODO: add SK user-agent here
client = AsyncOpenAI(
api_key=api_key,
organization=org_id,
default_headers={"User-Agent": APP_INFO},
)
"""Initialize a client for OpenAI services.
This constructor sets up a client to interact with OpenAI's API, allowing for
different types of AI model interactions, like chat or text completion.
Arguments:
ai_model_id {str} -- OpenAI model identifier. Must be non-empty.
Default to a preset value.
api_key {Optional[str]} -- OpenAI API key for authentication.
Must be non-empty. (Optional)
ai_model_type {Optional[OpenAIModelTypes]} -- The type of OpenAI
model to interact with. Defaults to CHAT.
org_id {Optional[str]} -- OpenAI organization ID. This is optional
unless the account belongs to multiple organizations.
default_headers {Optional[Mapping[str, str]]} -- Default headers
for HTTP requests. (Optional)
log {Optional[Logger]} -- Logger instance for logging purposes. (Optional)
"""
# Merge APP_INFO into the headers if it exists
merged_headers = default_headers.copy() if default_headers else {}
if APP_INFO:
merged_headers[USER_AGENT] = json.dumps(APP_INFO)
if not async_client:
if not api_key:
raise AIException(
AIException.ErrorCodes.InvalidConfiguration,
"Please provide an api_key",
)
async_client = AsyncOpenAI(
api_key=api_key,
organization=org_id,
default_headers=merged_headers,
)
super().__init__(
ai_model_id=ai_model_id,
client=client,
client=async_client,
log=log,
ai_model_type=ai_model_type,
)
@@ -44,6 +82,9 @@ class OpenAIConfigBase(OpenAIHandler):
"""
client_settings = {
"api_key": self.client.api_key,
"default_headers": {
k: v for k, v in self.client.default_headers.items() if k != USER_AGENT
},
}
if self.client.organization:
client_settings["org_id"] = self.client.organization
@@ -1,7 +1,9 @@
# Copyright (c) Microsoft. All rights reserved.
from logging import Logger
from typing import Dict, Optional
from typing import Dict, Mapping, Optional, overload
from openai import AsyncOpenAI
from semantic_kernel.connectors.ai.open_ai.services.open_ai_config_base import (
OpenAIConfigBase,
@@ -17,11 +19,11 @@ from semantic_kernel.connectors.ai.open_ai.services.open_ai_text_completion_base
class OpenAITextCompletion(OpenAITextCompletionBase, OpenAIConfigBase):
"""OpenAI Text Completion class."""
@overload
def __init__(
self,
ai_model_id: str,
api_key: str,
org_id: Optional[str] = None,
async_client: AsyncOpenAI,
log: Optional[Logger] = None,
) -> None:
"""
@@ -30,19 +32,89 @@ class OpenAITextCompletion(OpenAITextCompletionBase, OpenAIConfigBase):
Arguments:
ai_model_id {str} -- OpenAI model name, see
https://platform.openai.com/docs/models
api_key {str} -- OpenAI API key, see
https://platform.openai.com/account/api-keys
async_client {AsyncOpenAI} -- An existing client to use.
log: The logger instance to use. (Optional)
"""
@overload
def __init__(
self,
ai_model_id: str,
api_key: Optional[str] = None,
org_id: Optional[str] = None,
default_headers: Optional[Mapping[str, str]] = None,
log: Optional[Logger] = None,
) -> None:
"""
Initialize an OpenAITextCompletion service.
Arguments:
ai_model_id {str} -- OpenAI model name, see
https://platform.openai.com/docs/models
api_key {Optional[str]} -- OpenAI API key, see
https://platform.openai.com/account/api-keys (Optional)
org_id {Optional[str]} -- OpenAI organization ID.
This is usually optional unless your
account belongs to multiple organizations.
default_headers: The default headers mapping of string keys to
string values for HTTP requests. (Optional)
log {Optional[Logger]} -- The logger instance to use. (Optional)
"""
@overload
def __init__(
self,
ai_model_id: str,
api_key: Optional[str] = None,
default_headers: Optional[Mapping[str, str]] = None,
log: Optional[Logger] = None,
) -> None:
"""
Initialize an OpenAITextCompletion service.
Arguments:
ai_model_id {str} -- OpenAI model name, see
https://platform.openai.com/docs/models
api_key {Optional[str]} -- OpenAI API key, see
https://platform.openai.com/account/api-keys (Optional)
default_headers: The default headers mapping of string keys to
string values for HTTP requests. (Optional)
log {Optional[Logger]} -- The logger instance to use. (Optional)
"""
def __init__(
self,
ai_model_id: str,
api_key: Optional[str] = None,
org_id: Optional[str] = None,
default_headers: Optional[Mapping[str, str]] = None,
log: Optional[Logger] = None,
async_client: Optional[AsyncOpenAI] = None,
) -> None:
"""
Initialize an OpenAITextCompletion service.
Arguments:
ai_model_id {str} -- OpenAI model name, see
https://platform.openai.com/docs/models
api_key {Optional[str]} -- OpenAI API key, see
https://platform.openai.com/account/api-keys (Optional)
org_id {Optional[str]} -- OpenAI organization ID.
This is usually optional unless your
account belongs to multiple organizations.
default_headers: The default headers mapping of string keys to
string values for HTTP requests. (Optional)
log {Optional[Logger]} -- The logger instance to use. (Optional)
async_client {Optional[AsyncOpenAI]} -- An existing client to use. (Optional)
"""
super().__init__(
ai_model_id=ai_model_id,
api_key=api_key,
org_id=org_id,
log=log,
ai_model_type=OpenAIModelTypes.TEXT,
default_headers=default_headers,
async_client=async_client,
)
@classmethod
@@ -58,5 +130,6 @@ class OpenAITextCompletion(OpenAITextCompletionBase, OpenAIConfigBase):
ai_model_id=settings["ai_model_id"],
api_key=settings["api_key"],
org_id=settings.get("org_id"),
default_headers=settings.get("default_headers"),
log=settings.get("log"),
)
@@ -1,7 +1,9 @@
# Copyright (c) Microsoft. All rights reserved.
from logging import Logger
from typing import Dict, Optional
from typing import Dict, Mapping, Optional, overload
from openai import AsyncOpenAI
from semantic_kernel.connectors.ai.open_ai.services.open_ai_config_base import (
OpenAIConfigBase,
@@ -17,12 +19,31 @@ from semantic_kernel.connectors.ai.open_ai.services.open_ai_text_embedding_base
class OpenAITextEmbedding(OpenAIConfigBase, OpenAITextEmbeddingBase):
"""OpenAI Text Embedding class."""
@overload
def __init__(
self,
ai_model_id: str,
api_key: str,
org_id: Optional[str] = None,
async_client: AsyncOpenAI,
log: Optional[Logger] = None,
) -> None:
"""
Initialize an OpenAITextEmbedding service.
Arguments:
ai_model_id {str} -- OpenAI model name, see
https://platform.openai.com/docs/models
async_client {AsyncOpenAI} -- An existing client to use.
log: The logger instance to use. (Optional)
"""
def __init__(
self,
ai_model_id: str,
api_key: Optional[str] = None,
org_id: Optional[str] = None,
default_headers: Optional[Mapping[str, str]] = None,
log: Optional[Logger] = None,
async_client: Optional[AsyncOpenAI] = None,
) -> None:
"""
Initializes a new instance of the OpenAITextCompletion class.
@@ -35,13 +56,19 @@ class OpenAITextEmbedding(OpenAIConfigBase, OpenAITextEmbeddingBase):
org_id {Optional[str]} -- OpenAI organization ID.
This is usually optional unless your
account belongs to multiple organizations.
default_headers {Optional[Mapping[str,str]]}: The default headers mapping of string keys to
string values for HTTP requests. (Optional)
log {Optional[Logger]} -- The logger instance to use. (Optional)
async_client {Optional[AsyncOpenAI]} -- An existing client to use. (Optional)
"""
super().__init__(
ai_model_id=ai_model_id,
api_key=api_key,
ai_model_type=OpenAIModelTypes.EMBEDDING,
org_id=org_id,
default_headers=default_headers,
log=log,
async_client=async_client,
)
@classmethod
@@ -57,5 +84,6 @@ class OpenAITextEmbedding(OpenAIConfigBase, OpenAITextEmbeddingBase):
ai_model_id=settings["ai_model_id"],
api_key=settings["api_key"],
org_id=settings.get("org_id"),
default_headers=settings.get("default_headers"),
log=settings.get("log"),
)
@@ -1,3 +1,5 @@
# Copyright (c) Microsoft. All rights reserved.
from logging import Logger
from typing import Any, Dict, List, Optional, Tuple
@@ -1,5 +1,6 @@
# Copyright (c) Microsoft. All rights reserved.
"""Base class for text completion AI services."""
from abc import ABC, abstractmethod
from logging import Logger
from typing import TYPE_CHECKING, AsyncGenerator, List, Optional, Union
@@ -13,6 +13,9 @@ from azure.search.documents.indexes.models import (
)
from dotenv import load_dotenv
from semantic_kernel.connectors.ai.open_ai.const import (
USER_AGENT,
)
from semantic_kernel.memory.memory_record import MemoryRecord
SEARCH_FIELD_ID = "Id"
@@ -83,7 +86,7 @@ def get_search_index_async_client(
if azure_credential is None and token_credential is None:
raise ValueError("Error: Azure Cognitive Search credentials not set.")
sk_headers = {"User-Agent": "Semantic-Kernel"}
sk_headers = {USER_AGENT: "Semantic-Kernel"}
if azure_credential:
return SearchIndexClient(
@@ -11,6 +11,9 @@ from openapi_core.exceptions import OpenAPIError
from prance import ResolvingParser
from semantic_kernel import Kernel, SKContext
from semantic_kernel.connectors.ai.open_ai.const import (
USER_AGENT,
)
from semantic_kernel.connectors.telemetry import HTTP_USER_AGENT
from semantic_kernel.orchestration.sk_function_base import SKFunctionBase
from semantic_kernel.skill_definition import sk_function, sk_function_context_parameter
@@ -128,8 +131,8 @@ class RestApiOperation:
processed_headers["Content-Type"] = content_type
processed_payload = request_body
processed_headers["User-Agent"] = " ".join(
(HTTP_USER_AGENT, processed_headers.get("User-Agent", ""))
processed_headers[USER_AGENT] = " ".join(
(HTTP_USER_AGENT, processed_headers.get(USER_AGENT, ""))
).rstrip()
req = PreparedRestApiRequest(
@@ -3,6 +3,7 @@
import os
import pytest
from openai import AsyncAzureOpenAI
from test_utils import retry
import semantic_kernel.connectors.ai.open_ai as sk_oai
@@ -49,6 +50,56 @@ async def test_azure_e2e_chat_completion_with_skill(
assert len(output) < 100
@pytest.mark.asyncio
async def test_azure_e2e_chat_completion_with_skill_and_provided_client(
setup_tldr_function_for_oai_models, get_aoai_config
):
kernel, sk_prompt, text_to_summarize = setup_tldr_function_for_oai_models
_, api_key, endpoint = get_aoai_config
if "Python_Integration_Tests" in os.environ:
deployment_name = os.environ["AzureOpenAIChat__DeploymentName"]
else:
deployment_name = "gpt-35-turbo"
print("* Service: Azure OpenAI Chat Completion")
print(f"* Endpoint: {endpoint}")
print(f"* Deployment: {deployment_name}")
client = AsyncAzureOpenAI(
azure_endpoint=endpoint,
azure_deployment=deployment_name,
api_key=api_key,
api_version="2023-05-15",
default_headers={"Test-User-X-ID": "test"},
)
# Configure LLM service
kernel.add_chat_service(
"chat_completion",
sk_oai.AzureChatCompletion(
deployment_name=deployment_name,
async_client=client,
),
)
# Create the semantic function
tldr_function = kernel.create_semantic_function(
sk_prompt, max_tokens=200, temperature=0, top_p=0.5
)
summary = await retry(
lambda: kernel.run_async(tldr_function, input_str=text_to_summarize)
)
output = str(summary).strip()
print(f"TLDR using input string: '{output}'")
assert "First Law" not in output and (
"human" in output or "Human" in output or "preserve" in output
)
assert len(output) < 100
@pytest.mark.asyncio
async def test_oai_chat_stream_service_with_skills(
setup_tldr_function_for_oai_models, get_aoai_config
@@ -3,6 +3,7 @@
import os
import pytest
from openai import AsyncAzureOpenAI
from test_utils import retry
import semantic_kernel.connectors.ai.open_ai as sk_oai
@@ -51,6 +52,56 @@ async def test_azure_e2e_text_completion_with_skill(
assert len(output) < 100
@pytest.mark.asyncio
async def test_azure_e2e_text_completion_with_skill_with_provided_client(
setup_tldr_function_for_oai_models, get_aoai_config
):
kernel, sk_prompt, text_to_summarize = setup_tldr_function_for_oai_models
_, api_key, endpoint = get_aoai_config
if "Python_Integration_Tests" in os.environ:
deployment_name = os.environ["AzureOpenAI__DeploymentName"]
else:
deployment_name = "text-davinci-003"
print("* Service: Azure OpenAI Text Completion")
print(f"* Endpoint: {endpoint}")
print(f"* Deployment: {deployment_name}")
client = AsyncAzureOpenAI(
azure_endpoint=endpoint,
azure_deployment=deployment_name,
api_key=api_key,
api_version="2023-05-15",
default_headers={"Test-User-X-ID": "test"},
)
# Configure LLM service
kernel.add_text_completion_service(
"text_completion",
sk_oai.AzureTextCompletion(
deployment_name=deployment_name,
async_client=client,
),
)
# Create the semantic function
tldr_function = kernel.create_semantic_function(
sk_prompt, max_tokens=200, temperature=0, top_p=0.5
)
summary = await retry(
lambda: kernel.run_async(tldr_function, input_str=text_to_summarize)
)
output = str(summary).strip()
print(f"TLDR using input string: '{output}'")
assert "First Law" not in output and (
"human" in output or "Human" in output or "preserve" in output
)
assert len(output) < 100
@pytest.mark.asyncio
async def test_oai_text_stream_completion_with_skills(
setup_tldr_function_for_oai_models, get_aoai_config
@@ -1,6 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from openai import AsyncOpenAI
from test_utils import retry
import semantic_kernel.connectors.ai.open_ai as sk_oai
@@ -39,3 +40,44 @@ async def test_oai_chat_service_with_skills(
"human" in output or "Human" in output or "preserve" in output
)
assert len(output) < 100
@pytest.mark.asyncio
async def test_oai_chat_service_with_skills_with_provided_client(
setup_tldr_function_for_oai_models, get_oai_config
):
kernel, sk_prompt, text_to_summarize = setup_tldr_function_for_oai_models
api_key, org_id = get_oai_config
print("* Service: OpenAI Chat Completion")
print("* Endpoint: OpenAI")
print("* Model: gpt-3.5-turbo")
client = AsyncOpenAI(
api_key=api_key,
organization=org_id,
)
kernel.add_chat_service(
"chat-gpt",
sk_oai.OpenAIChatCompletion(
ai_model_id="gpt-3.5-turbo",
async_client=client,
),
)
# Create the semantic function
tldr_function = kernel.create_semantic_function(
sk_prompt, max_tokens=200, temperature=0, top_p=0.5
)
summary = await retry(
lambda: kernel.run_async(tldr_function, input_str=text_to_summarize)
)
output = str(summary).strip()
print(f"TLDR using input string: '{output}'")
assert "First Law" not in output and (
"human" in output or "Human" in output or "preserve" in output
)
assert len(output) < 100
@@ -1,6 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from openai import AsyncOpenAI
from test_utils import retry
import semantic_kernel.connectors.ai.open_ai as sk_oai
@@ -39,3 +40,44 @@ async def test_oai_text_completion_with_skills(
"human" in output or "Human" in output or "preserve" in output
)
assert len(output) < 100
@pytest.mark.asyncio
async def test_oai_text_completion_with_skills_with_provided_client(
setup_tldr_function_for_oai_models, get_oai_config
):
kernel, sk_prompt, text_to_summarize = setup_tldr_function_for_oai_models
api_key, org_id = get_oai_config
print("* Service: OpenAI Text Completion")
print("* Endpoint: OpenAI")
print("* Model: text-davinci-003")
client = AsyncOpenAI(
api_key=api_key,
organization=org_id,
)
kernel.add_chat_service(
"text-davinci-003",
sk_oai.OpenAITextCompletion(
ai_model_id="text-davinci-003",
async_client=client,
),
)
# Create the semantic function
tldr_function = kernel.create_semantic_function(
sk_prompt, max_tokens=200, temperature=0, top_p=0.5
)
summary = await retry(
lambda: kernel.run_async(tldr_function, input_str=text_to_summarize)
)
output = str(summary).strip()
print(f"TLDR using input string: '{output}'")
assert "First Law" not in output and (
"human" in output or "Human" in output or "preserve" in output
)
assert len(output) < 100
@@ -3,6 +3,7 @@
import os
import pytest
from openai import AsyncAzureOpenAI
import semantic_kernel as sk
import semantic_kernel.connectors.ai.open_ai as sk_oai
@@ -40,6 +41,47 @@ async def test_azure_text_embedding_service(create_kernel, get_aoai_config):
)
@pytest.mark.asyncio
async def test_azure_text_embedding_service_with_provided_client(
create_kernel, get_aoai_config
):
kernel = create_kernel
_, api_key, endpoint = get_aoai_config
if "Python_Integration_Tests" in os.environ:
deployment_name = os.environ["AzureOpenAIEmbeddings__DeploymentName"]
else:
deployment_name = "text-embedding-ada-002"
client = AsyncAzureOpenAI(
azure_endpoint=endpoint,
azure_deployment=deployment_name,
api_key=api_key,
api_version="2023-05-15",
default_headers={"Test-User-X-ID": "test"},
)
kernel.add_text_embedding_generation_service(
"aoai-ada-2",
sk_oai.AzureTextEmbedding(
deployment_name=deployment_name,
async_client=client,
),
)
kernel.register_memory_store(memory_store=sk.memory.VolatileMemoryStore())
await kernel.memory.save_information_async(
"test", id="info1", text="this is a test"
)
await kernel.memory.save_reference_async(
"test",
external_id="info1",
text="this is a test",
external_source_name="external source",
)
@pytest.mark.asyncio
async def test_batch_azure_embeddings(get_aoai_config):
# Configure LLM service
@@ -49,7 +91,7 @@ async def test_batch_azure_embeddings(get_aoai_config):
deployment_name = os.environ["AzureOpenAIEmbeddings__DeploymentName"]
else:
deployment_name = "text-embedding-ada-002"
deployment_name = "ada-002"
embeddings_service = sk_oai.AzureTextEmbedding(
deployment_name=deployment_name,
@@ -1,6 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from openai import AsyncOpenAI
import semantic_kernel as sk
import semantic_kernel.connectors.ai.open_ai as sk_oai
@@ -27,3 +28,36 @@ async def test_oai_embedding_service(create_kernel, get_oai_config):
text="this is a test",
external_source_name="external source",
)
@pytest.mark.asyncio
async def test_oai_embedding_service_with_provided_client(
create_kernel, get_oai_config
):
kernel = create_kernel
api_key, org_id = get_oai_config
client = AsyncOpenAI(
api_key=api_key,
organization=org_id,
)
kernel.add_text_embedding_generation_service(
"oai-ada-2",
sk_oai.OpenAITextEmbedding(
ai_model_id="text-embedding-ada-002",
async_client=client,
),
)
kernel.register_memory_store(memory_store=sk.memory.VolatileMemoryStore())
await kernel.memory.save_information_async(
"test", id="info1", text="this is a test"
)
await kernel.memory.save_reference_async(
"test",
external_id="info1",
text="this is a test",
external_source_name="external source",
)
@@ -13,6 +13,9 @@ from semantic_kernel.connectors.ai.chat_completion_client_base import (
ChatCompletionClientBase,
)
from semantic_kernel.connectors.ai.chat_request_settings import ChatRequestSettings
from semantic_kernel.connectors.ai.open_ai.const import (
USER_AGENT,
)
from semantic_kernel.connectors.ai.open_ai.services.azure_chat_completion import (
AzureChatCompletion,
)
@@ -47,19 +50,25 @@ def test_azure_chat_completion_init_base_url() -> None:
api_version = "2023-03-15-preview"
logger = Logger("test_logger")
# Test successful initialization
# Custom header for testing
default_headers = {"X-Unit-Test": "test-guid"}
azure_chat_completion = AzureChatCompletion(
deployment_name=deployment_name,
base_url=base_url,
api_key=api_key,
api_version=api_version,
log=logger,
default_headers=default_headers,
)
assert azure_chat_completion.client is not None
assert isinstance(azure_chat_completion.client, AsyncAzureOpenAI)
assert azure_chat_completion.ai_model_id == deployment_name
assert isinstance(azure_chat_completion, ChatCompletionClientBase)
for key, value in default_headers.items():
assert key in azure_chat_completion.client.default_headers
assert azure_chat_completion.client.default_headers[key] == value
def test_azure_chat_completion_init_with_empty_deployment_name() -> None:
@@ -276,6 +285,7 @@ def test_azure_chat_completion_serialize() -> None:
endpoint = "https://test-endpoint.com"
api_key = "test_api_key"
api_version = "2023-03-15-preview"
default_headers = {"X-Test": "test"}
logger = Logger("test_logger")
settings = {
@@ -284,6 +294,7 @@ def test_azure_chat_completion_serialize() -> None:
"api_key": api_key,
"api_version": api_version,
"log": logger,
"default_headers": default_headers,
}
azure_chat_completion = AzureChatCompletion.from_dict(settings)
@@ -292,3 +303,12 @@ def test_azure_chat_completion_serialize() -> None:
assert settings["endpoint"] in str(dumped_settings["base_url"])
assert settings["deployment_name"] in str(dumped_settings["base_url"])
assert settings["api_key"] == dumped_settings["api_key"]
assert settings["api_version"] == dumped_settings["api_version"]
# Assert that the default header we added is present in the dumped_settings default headers
for key, value in default_headers.items():
assert key in dumped_settings["default_headers"]
assert dumped_settings["default_headers"][key] == value
# Assert that the 'User-agent' header is not present in the dumped_settings default headers
assert USER_AGENT not in dumped_settings["default_headers"]
@@ -40,6 +40,35 @@ def test_azure_text_completion_init() -> None:
assert isinstance(azure_text_completion, TextCompletionClientBase)
def test_azure_text_completion_init_with_custom_header() -> None:
deployment_name = "test_deployment"
endpoint = "https://test-endpoint.com"
api_key = "test_api_key"
api_version = "2023-03-15-preview"
logger = Logger("test_logger")
# Custom header for testing
default_headers = {"X-Unit-Test": "test-guid"}
# Test successful initialization
azure_text_completion = AzureTextCompletion(
deployment_name=deployment_name,
endpoint=endpoint,
api_key=api_key,
api_version=api_version,
log=logger,
default_headers=default_headers,
)
assert azure_text_completion.client is not None
assert isinstance(azure_text_completion.client, AsyncAzureOpenAI)
assert azure_text_completion.ai_model_id == deployment_name
assert isinstance(azure_text_completion, TextCompletionClientBase)
for key, value in default_headers.items():
assert key in azure_text_completion.client.default_headers
assert azure_text_completion.client.default_headers[key] == value
def test_azure_text_completion_init_with_empty_deployment_name() -> None:
# deployment_name = "test_deployment"
endpoint = "https://test-endpoint.com"
@@ -191,6 +220,7 @@ def test_azure_text_completion_serialize() -> None:
endpoint = "https://test-endpoint.com"
api_key = "test_api_key"
api_version = "2023-03-15-preview"
default_headers = {"X-Test": "test"}
logger = Logger("test_logger")
settings = {
@@ -199,6 +229,7 @@ def test_azure_text_completion_serialize() -> None:
"api_key": api_key,
"api_version": api_version,
"log": logger,
"default_headers": default_headers,
}
azure_text_completion = AzureTextCompletion.from_dict(settings)
@@ -207,3 +238,9 @@ def test_azure_text_completion_serialize() -> None:
assert settings["endpoint"] in str(dumped_settings["base_url"])
assert settings["deployment_name"] in str(dumped_settings["base_url"])
assert settings["api_key"] == dumped_settings["api_key"]
assert settings["api_version"] == dumped_settings["api_version"]
# Assert that the default header we added is present in the dumped_settings default headers
for key, value in default_headers.items():
assert key in dumped_settings["default_headers"]
assert dumped_settings["default_headers"][key] == value
@@ -107,6 +107,38 @@ def test_azure_text_embedding_init_with_invalid_endpoint() -> None:
)
def test_azure_text_embedding_init_with_from_dict() -> None:
deployment_name = "test_deployment"
endpoint = "https://test-endpoint.com"
api_key = "test_api_key"
api_version = "2023-03-15-preview"
logger = Logger("test_logger")
default_headers = {"test_header": "test_value"}
settings = {
"deployment_name": deployment_name,
"endpoint": endpoint,
"api_key": api_key,
"api_version": api_version,
"log": logger,
"default_headers": default_headers,
}
azure_text_embedding = AzureTextEmbedding.from_dict(settings=settings)
assert azure_text_embedding.client is not None
assert isinstance(azure_text_embedding.client, AsyncAzureOpenAI)
assert azure_text_embedding.ai_model_id == deployment_name
assert isinstance(azure_text_embedding, EmbeddingGeneratorBase)
assert endpoint in str(azure_text_embedding.client.base_url)
assert azure_text_embedding.client.api_key == api_key
# Assert that the default header we added is present in the client's default headers
for key, value in default_headers.items():
assert key in azure_text_embedding.client.default_headers
assert azure_text_embedding.client.default_headers[key] == value
@pytest.mark.asyncio
@patch.object(AsyncEmbeddings, "create", new_callable=AsyncMock)
async def test_azure_text_embedding_calls_with_parameters(mock_create) -> None:
@@ -6,6 +6,9 @@ import pytest
from pydantic import ValidationError
from semantic_kernel.connectors.ai import ChatCompletionClientBase
from semantic_kernel.connectors.ai.open_ai.const import (
USER_AGENT,
)
from semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion import (
OpenAIChatCompletion,
)
@@ -27,6 +30,29 @@ def test_open_ai_chat_completion_init() -> None:
assert isinstance(open_ai_chat_completion, ChatCompletionClientBase)
def test_open_ai_chat_completion_init_with_default_header() -> None:
ai_model_id = "test_model_id"
api_key = "test_api_key"
logger = Logger("test_logger")
default_headers = {"X-Unit-Test": "test-guid"}
# Test successful initialization
open_ai_chat_completion = OpenAIChatCompletion(
ai_model_id=ai_model_id,
api_key=api_key,
log=logger,
default_headers=default_headers,
)
assert open_ai_chat_completion.ai_model_id == ai_model_id
assert isinstance(open_ai_chat_completion, ChatCompletionClientBase)
# Assert that the default header we added is present in the client's default headers
for key, value in default_headers.items():
assert key in open_ai_chat_completion.client.default_headers
assert open_ai_chat_completion.client.default_headers[key] == value
def test_open_ai_chat_completion_init_with_empty_model_id() -> None:
# ai_model_id = "test_model_id"
api_key = "test_api_key"
@@ -57,16 +83,25 @@ def test_open_ai_chat_completion_serialize() -> None:
ai_model_id = "test_model_id"
api_key = "test_api_key"
logger = Logger("test_logger")
default_headers = {"X-Unit-Test": "test-guid"}
settings = {
"ai_model_id": ai_model_id,
"api_key": api_key,
"log": logger,
"default_headers": default_headers,
}
open_ai_chat_completion = OpenAIChatCompletion.from_dict(settings)
dumped_settings = open_ai_chat_completion.to_dict()
assert dumped_settings == settings
assert dumped_settings["ai_model_id"] == ai_model_id
assert dumped_settings["api_key"] == api_key
# Assert that the default header we added is present in the dumped_settings default headers
for key, value in default_headers.items():
assert key in dumped_settings["default_headers"]
assert dumped_settings["default_headers"][key] == value
# Assert that the 'User-agent' header is not present in the dumped_settings default headers
assert USER_AGENT not in dumped_settings["default_headers"]
def test_open_ai_chat_completion_serialize_with_org_id() -> None:
@@ -84,4 +119,8 @@ def test_open_ai_chat_completion_serialize_with_org_id() -> None:
open_ai_chat_completion = OpenAIChatCompletion.from_dict(settings)
dumped_settings = open_ai_chat_completion.to_dict()
assert dumped_settings == settings
assert dumped_settings["ai_model_id"] == ai_model_id
assert dumped_settings["api_key"] == api_key
assert dumped_settings["org_id"] == org_id
# Assert that the 'User-agent' header is not present in the dumped_settings default headers
assert USER_AGENT not in dumped_settings["default_headers"]
@@ -27,6 +27,27 @@ def test_open_ai_text_completion_init() -> None:
assert isinstance(open_ai_text_completion, TextCompletionClientBase)
def test_open_ai_text_completion_init_with_default_header() -> None:
ai_model_id = "test_model_id"
api_key = "test_api_key"
logger = Logger("test_logger")
default_headers = {"X-Unit-Test": "test-guid"}
# Test successful initialization
open_ai_text_completion = OpenAITextCompletion(
ai_model_id=ai_model_id,
api_key=api_key,
log=logger,
default_headers=default_headers,
)
assert open_ai_text_completion.ai_model_id == ai_model_id
assert isinstance(open_ai_text_completion, TextCompletionClientBase)
for key, value in default_headers.items():
assert key in open_ai_text_completion.client.default_headers
assert open_ai_text_completion.client.default_headers[key] == value
def test_open_ai_text_completion_init_with_empty_model_id() -> None:
# ai_model_id = "test_model_id"
api_key = "test_api_key"
@@ -57,16 +78,23 @@ def test_open_ai_text_completion_serialize() -> None:
ai_model_id = "test_model_id"
api_key = "test_api_key"
logger = Logger("test_logger")
default_headers = {"X-Unit-Test": "test-guid"}
settings = {
"ai_model_id": ai_model_id,
"api_key": api_key,
"log": logger,
"default_headers": default_headers,
}
open_ai_text_completion = OpenAITextCompletion.from_dict(settings)
dumped_settings = open_ai_text_completion.to_dict()
assert dumped_settings == settings
assert dumped_settings["ai_model_id"] == ai_model_id
assert dumped_settings["api_key"] == api_key
# Assert that the default header we added is present in the dumped_settings default headers
for key, value in default_headers.items():
assert key in dumped_settings["default_headers"]
assert dumped_settings["default_headers"][key] == value
def test_open_ai_text_completion_serialize_with_org_id() -> None:
@@ -84,4 +112,6 @@ def test_open_ai_text_completion_serialize_with_org_id() -> None:
open_ai_text_completion = OpenAITextCompletion.from_dict(settings)
dumped_settings = open_ai_text_completion.to_dict()
assert dumped_settings == settings
assert dumped_settings["ai_model_id"] == ai_model_id
assert dumped_settings["api_key"] == api_key
assert dumped_settings["org_id"] == org_id
+8 -5
View File
@@ -5,6 +5,9 @@ import pytest
import yaml
from openapi_core import Spec
from semantic_kernel.connectors.ai.open_ai.const import (
USER_AGENT,
)
from semantic_kernel.connectors.openapi.sk_openapi import (
OpenApiParser,
OpenApiRunner,
@@ -95,7 +98,7 @@ def test_prepare_request_with_path_params():
headers={
"Authorization": "Bearer abc123",
"Content-Type": "application/json",
"User-Agent": "Semantic-Kernel",
USER_AGENT: "Semantic-Kernel",
},
request_body={"title": "Buy milk", "completed": False},
)
@@ -134,7 +137,7 @@ def test_prepare_request_with_default_query_param():
headers={
"Authorization": "Bearer abc123",
"Content-Type": "application/json",
"User-Agent": "Semantic-Kernel",
USER_AGENT: "Semantic-Kernel",
},
request_body={"title": "Buy milk", "completed": False},
)
@@ -156,7 +159,7 @@ def test_prepare_request_with_default_header():
method="PUT",
url="http://example.com/todos/1",
params={"completed": False},
headers={"Content-Type": "application/json", "User-Agent": "Semantic-Kernel"},
headers={"Content-Type": "application/json", USER_AGENT: "Semantic-Kernel"},
request_body={"title": "Buy milk", "completed": False},
)
actual_request = put_operation.prepare_request(
@@ -171,14 +174,14 @@ def test_prepare_request_with_default_header():
def test_prepare_request_with_existing_user_agent():
path_params = {"id": 1}
query_params = {"completed": False}
headers = {"User-Agent": "API/1.0 PythonBindings"}
headers = {USER_AGENT: "API/1.0 PythonBindings"}
request_body = {"title": "Buy milk", "completed": False}
expected_request = PreparedRestApiRequest(
method="PUT",
url="http://example.com/todos/1",
params={"completed": False},
headers={
"User-Agent": "Semantic-Kernel API/1.0 PythonBindings",
USER_AGENT: "Semantic-Kernel API/1.0 PythonBindings",
"Content-Type": "application/json",
},
request_body={"title": "Buy milk", "completed": False},