Skip to content

Commit

Permalink
Python: Updates to user agent. Add unit tests. (#6824)
Browse files Browse the repository at this point in the history
### Motivation and Context

Updates to the User-Agent are required so that we can align with all
three SK languages. Examples:

User agent
- semantic-kernel-csharp/1.1.1 [existing user agent]
- semantic-kernel-java/1.1.1 [existing user agent]
- semantic-kernel-python/1.1.1 [existing user agent]

semantic-kernel-version header
- csharp/1.1.1 
- java/1.1.1
- python/1.1.1

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

Updates to the user agent to reflect the desired changes/standardization
across all SK languages. Adding unit tests.
- Closes #6827 
- Moves the `USER_AGENT` const from its previous home in the openai
const file to a more generic const file.
- Updates unit tests to be able to run properly when a valid `.env` file
exists in the current working directory. The unit tests patch the env
vars and our current implementation of Pydantic Settings loads the
`.env` file if not specified.

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

- [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
- [X] I didn't break anyone 😄
  • Loading branch information
moonbox3 committed Jun 20, 2024
1 parent a214e12 commit 148e8de
Show file tree
Hide file tree
Showing 19 changed files with 158 additions and 47 deletions.
1 change: 0 additions & 1 deletion python/semantic_kernel/connectors/ai/open_ai/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,3 @@
from typing import Final

DEFAULT_AZURE_API_VERSION: Final[str] = "2024-02-01"
USER_AGENT: Final[str] = "User-Agent"
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
from openai import AsyncAzureOpenAI
from pydantic import ConfigDict, validate_call

from semantic_kernel.connectors.ai.open_ai.const import DEFAULT_AZURE_API_VERSION, USER_AGENT
from semantic_kernel.connectors.ai.open_ai.const import DEFAULT_AZURE_API_VERSION
from semantic_kernel.connectors.ai.open_ai.services.open_ai_handler import OpenAIHandler, OpenAIModelTypes
from semantic_kernel.connectors.telemetry import APP_INFO, prepend_semantic_kernel_to_user_agent
from semantic_kernel.const import USER_AGENT
from semantic_kernel.exceptions import ServiceInitializationError
from semantic_kernel.kernel_pydantic import HttpsUrl

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
from openai import AsyncOpenAI
from pydantic import ConfigDict, Field, validate_call

from semantic_kernel.connectors.ai.open_ai.const import USER_AGENT
from semantic_kernel.connectors.ai.open_ai.services.open_ai_handler import OpenAIHandler
from semantic_kernel.connectors.ai.open_ai.services.open_ai_model_types import OpenAIModelTypes
from semantic_kernel.connectors.telemetry import APP_INFO, prepend_semantic_kernel_to_user_agent
from semantic_kernel.const import USER_AGENT
from semantic_kernel.exceptions import ServiceInitializationError

logger: logging.Logger = logging.getLogger(__name__)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from azure.search.documents.indexes.models import SearchableField, SearchField, SearchFieldDataType, SimpleField
from dotenv import load_dotenv

from semantic_kernel.connectors.ai.open_ai.const import USER_AGENT
from semantic_kernel.const import USER_AGENT
from semantic_kernel.exceptions import ServiceInitializationError
from semantic_kernel.memory.memory_record import MemoryRecord

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@
import httpx
from openapi_core import Spec

from semantic_kernel.connectors.ai.open_ai.const import USER_AGENT
from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation import RestApiOperation
from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_expected_response import (
RestApiOperationExpectedResponse,
)
from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_payload import RestApiOperationPayload
from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_run_options import RestApiOperationRunOptions
from semantic_kernel.connectors.telemetry import APP_INFO, prepend_semantic_kernel_to_user_agent
from semantic_kernel.exceptions.function_exceptions import FunctionExecutionException
from semantic_kernel.functions.kernel_arguments import KernelArguments
from semantic_kernel.utils.experimental_decorator import experimental_class
Expand Down Expand Up @@ -124,8 +124,6 @@ async def run_operation(
options: RestApiOperationRunOptions | None = None,
) -> str:
"""Run the operation."""
from semantic_kernel.connectors.telemetry import HTTP_USER_AGENT

url = self.build_operation_url(
operation=operation,
arguments=arguments,
Expand All @@ -143,7 +141,9 @@ async def run_operation(
headers_update = await self.auth_callback(headers=headers)
headers.update(headers_update)

headers[USER_AGENT] = " ".join((HTTP_USER_AGENT, headers.get(USER_AGENT, ""))).rstrip()
if APP_INFO:
headers.update(APP_INFO)
headers = prepend_semantic_kernel_to_user_agent(headers)

if "Content-Type" not in headers:
headers["Content-Type"] = self._get_first_response_media_type(operation.responses)
Expand Down
16 changes: 10 additions & 6 deletions python/semantic_kernel/connectors/telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
from importlib.metadata import PackageNotFoundError, version
from typing import Any

from semantic_kernel.connectors.ai.open_ai.const import USER_AGENT
from semantic_kernel.const import USER_AGENT

TELEMETRY_DISABLED_ENV_VAR = "AZURE_TELEMETRY_DISABLED"

IS_TELEMETRY_ENABLED = os.environ.get(TELEMETRY_DISABLED_ENV_VAR, "false").lower() not in ["true", "1"]

HTTP_USER_AGENT = "Semantic-Kernel"
HTTP_USER_AGENT = "semantic-kernel-python"

try:
version_info = version("semantic-kernel")
Expand All @@ -19,22 +19,26 @@

APP_INFO = (
{
"Semantic-Kernel-Version": f"python-{version_info}",
"semantic-kernel-version": f"python/{version_info}",
}
if IS_TELEMETRY_ENABLED
else None
)


def prepend_semantic_kernel_to_user_agent(headers: dict[str, Any]):
"""Prepend "Semantic-Kernel" to the User-Agent in the headers.
"""Prepend "semantic-kernel" to the User-Agent in the headers.
Args:
headers: The existing headers dictionary.
Returns:
The modified headers dictionary with "Semantic-Kernel" prepended to the User-Agent.
The modified headers dictionary with "semantic-kernel" prepended to the User-Agent.
"""
headers[USER_AGENT] = f"{HTTP_USER_AGENT} {headers[USER_AGENT]}" if USER_AGENT in headers else f"{HTTP_USER_AGENT}"
headers[USER_AGENT] = (
f"{HTTP_USER_AGENT}/{version_info} {headers[USER_AGENT]}"
if USER_AGENT in headers
else f"{HTTP_USER_AGENT}/{version_info}"
)

return headers
1 change: 1 addition & 0 deletions python/semantic_kernel/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@

METADATA_EXCEPTION_KEY: Final[str] = "exception"
DEFAULT_SERVICE_NAME: Final[str] = "default"
USER_AGENT: Final[str] = "User-Agent"
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
import httpx
from pydantic import ValidationError

from semantic_kernel.connectors.ai.open_ai.const import USER_AGENT
from semantic_kernel.connectors.telemetry import HTTP_USER_AGENT, version_info
from semantic_kernel.const import USER_AGENT
from semantic_kernel.core_plugins.sessions_python_tool.sessions_python_settings import (
ACASessionsSettings,
SessionsPythonSettings,
Expand Down Expand Up @@ -93,7 +93,7 @@ def _sanitize_input(self, code: str) -> str:
code = re.sub(r"^(\s|`)*(?i:python)?\s*", "", code)
# Removes whitespace & ` from end
return re.sub(r"(\s|`)*$", "", code)

def _construct_remote_file_path(self, remote_file_path: str) -> str:
"""Construct the remote file path.
Expand All @@ -109,8 +109,8 @@ def _construct_remote_file_path(self, remote_file_path: str) -> str:

def _build_url_with_version(self, base_url, endpoint, params):
"""Builds a URL with the provided base URL, endpoint, and query parameters."""
params['api-version'] = SESSIONS_API_VERSION
query_string = '&'.join([f"{key}={value}" for key, value in params.items()])
params["api-version"] = SESSIONS_API_VERSION
query_string = "&".join([f"{key}={value}" for key, value in params.items()])
return f"{base_url}{endpoint}?{query_string}"

@kernel_function(
Expand Down Expand Up @@ -178,7 +178,9 @@ async def upload_file(
self,
*,
local_file_path: Annotated[str, "The path to the local file on the machine"],
remote_file_path: Annotated[str | None, "The remote path to the file in the session. Defaults to /mnt/data"] = None, # noqa: E501
remote_file_path: Annotated[
str | None, "The remote path to the file in the session. Defaults to /mnt/data"
] = None,
) -> Annotated[SessionsRemoteFileMetadata, "The metadata of the uploaded file"]:
"""Upload a file to the session pool.
Expand Down Expand Up @@ -222,7 +224,7 @@ async def upload_file(
response.raise_for_status()

response_json = response.json()
return SessionsRemoteFileMetadata.from_dict(response_json['$values'][0])
return SessionsRemoteFileMetadata.from_dict(response_json["$values"][0])

@kernel_function(name="list_files", description="Lists all files in the provided Session ID")
async def list_files(self) -> list[SessionsRemoteFileMetadata]:
Expand All @@ -242,7 +244,7 @@ async def list_files(self) -> list[SessionsRemoteFileMetadata]:
url = self._build_url_with_version(
base_url=self.pool_management_endpoint,
endpoint="python/files",
params={"identifier": self.settings.session_id}
params={"identifier": self.settings.session_id},
)

response = await self.http_client.get(
Expand Down Expand Up @@ -275,10 +277,7 @@ async def download_file(self, *, remote_file_path: str, local_file_path: str | N
url = self._build_url_with_version(
base_url=self.pool_management_endpoint,
endpoint="python/downloadFile",
params={
"identifier": self.settings.session_id,
"filename": remote_file_path
}
params={"identifier": self.settings.session_id, "filename": remote_file_path},
)

response = await self.http_client.get(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def test_google_palm_chat_completion_init_with_empty_api_key(google_palm_unit_te
with pytest.raises(ServiceInitializationError):
GooglePalmChatCompletion(
ai_model_id=ai_model_id,
env_file_path="test.env",
)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def test_google_palm_text_completion_init_with_empty_api_key(google_palm_unit_te
with pytest.raises(ServiceInitializationError):
GooglePalmTextCompletion(
ai_model_id=ai_model_id,
env_file_path="test.env",
)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def test_google_palm_text_embedding_init_with_empty_api_key(google_palm_unit_tes
with pytest.raises(ServiceInitializationError):
GooglePalmTextEmbedding(
ai_model_id=ai_model_id,
env_file_path="test.env",
)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase
from semantic_kernel.connectors.ai.function_call_behavior import FunctionCallBehavior
from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion
from semantic_kernel.connectors.ai.open_ai.const import USER_AGENT
from semantic_kernel.connectors.ai.open_ai.exceptions.content_filter_ai_exception import (
ContentFilterAIException,
ContentFilterResultSeverity,
Expand All @@ -22,6 +21,7 @@
AzureChatPromptExecutionSettings,
ExtraBody,
)
from semantic_kernel.const import USER_AGENT
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.exceptions import ServiceInitializationError, ServiceInvalidExecutionSettingsError
from semantic_kernel.exceptions.service_exceptions import ServiceResponseException
Expand Down Expand Up @@ -58,19 +58,25 @@ def test_azure_chat_completion_init_base_url(azure_openai_unit_test_env) -> None
@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"]], indirect=True)
def test_azure_chat_completion_init_with_empty_deployment_name(azure_openai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError):
AzureChatCompletion()
AzureChatCompletion(
env_file_path="test.env",
)


@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_API_KEY"]], indirect=True)
def test_azure_chat_completion_init_with_empty_api_key(azure_openai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError):
AzureChatCompletion()
AzureChatCompletion(
env_file_path="test.env",
)


@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_BASE_URL"]], indirect=True)
def test_azure_chat_completion_init_with_empty_endpoint_and_base_url(azure_openai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError):
AzureChatCompletion()
AzureChatCompletion(
env_file_path="test.env",
)


@pytest.mark.parametrize("override_env_param_dict", [{"AZURE_OPENAI_ENDPOINT": "http://test.com"}], indirect=True)
Expand Down Expand Up @@ -450,9 +456,7 @@ async def test_azure_chat_completion_auto_invoke_false_no_kernel_provided_throws
prompt = "some prompt that would trigger the content filtering"
chat_history.add_user_message(prompt)
complete_prompt_execution_settings = AzureChatPromptExecutionSettings(
function_call_behavior=FunctionCallBehavior.EnableFunctions(
auto_invoke=False, filters={}
)
function_call_behavior=FunctionCallBehavior.EnableFunctions(auto_invoke=False, filters={})
)

test_endpoint = os.getenv("AZURE_OPENAI_ENDPOINT")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,21 +43,28 @@ def test_azure_text_completion_init_with_custom_header(azure_openai_unit_test_en


@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_TEXT_DEPLOYMENT_NAME"]], indirect=True)
def test_azure_text_completion_init_with_empty_deployment_name(azure_openai_unit_test_env) -> None:
def test_azure_text_completion_init_with_empty_deployment_name(monkeypatch, azure_openai_unit_test_env) -> None:
monkeypatch.delenv("AZURE_OPENAI_TEXT_DEPLOYMENT_NAME", raising=False)
with pytest.raises(ServiceInitializationError):
AzureTextCompletion()
AzureTextCompletion(
env_file_path="test.env",
)


@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_API_KEY"]], indirect=True)
def test_azure_text_completion_init_with_empty_api_key(azure_openai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError):
AzureTextCompletion()
AzureTextCompletion(
env_file_path="test.env",
)


@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_BASE_URL"]], indirect=True)
def test_azure_text_completion_init_with_empty_endpoint_and_base_url(azure_openai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError):
AzureTextCompletion()
AzureTextCompletion(
env_file_path="test.env",
)


@pytest.mark.parametrize("override_env_param_dict", [{"AZURE_OPENAI_ENDPOINT": "http://test.com"}], indirect=True)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,25 @@ def test_azure_text_embedding_init(azure_openai_unit_test_env) -> None:
@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME"]], indirect=True)
def test_azure_text_embedding_init_with_empty_deployment_name(azure_openai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError):
AzureTextEmbedding()
AzureTextEmbedding(
env_file_path="test.env",
)


@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_API_KEY"]], indirect=True)
def test_azure_text_embedding_init_with_empty_api_key(azure_openai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError):
AzureTextEmbedding()
AzureTextEmbedding(
env_file_path="test.env",
)


@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_BASE_URL"]], indirect=True)
def test_azure_text_embedding_init_with_empty_endpoint_and_base_url(azure_openai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError):
AzureTextEmbedding()
AzureTextEmbedding(
env_file_path="test.env",
)


@pytest.mark.parametrize("override_env_param_dict", [{"AZURE_OPENAI_ENDPOINT": "http://test.com"}], indirect=True)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
import pytest

from semantic_kernel.connectors.ai.chat_completion_client_base 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
from semantic_kernel.const import USER_AGENT
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError


Expand Down Expand Up @@ -46,7 +46,9 @@ def test_open_ai_chat_completion_init_with_default_header(openai_unit_test_env)
@pytest.mark.parametrize("exclude_list", [["OPENAI_API_KEY"]], indirect=True)
def test_open_ai_chat_completion_init_with_empty_model_id(openai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError):
OpenAIChatCompletion()
OpenAIChatCompletion(
env_file_path="test.env",
)


@pytest.mark.parametrize("exclude_list", [["OPENAI_API_KEY"]], indirect=True)
Expand All @@ -56,6 +58,7 @@ def test_open_ai_chat_completion_init_with_empty_api_key(openai_unit_test_env) -
with pytest.raises(ServiceInitializationError):
OpenAIChatCompletion(
ai_model_id=ai_model_id,
env_file_path="test.env",
)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ def test_open_ai_text_completion_init_with_default_header(openai_unit_test_env)
@pytest.mark.parametrize("exclude_list", [["OPENAI_API_KEY"]], indirect=True)
def test_open_ai_text_completion_init_with_empty_api_key(openai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError):
OpenAITextCompletion()
OpenAITextCompletion(
env_file_path="test.env",
)


def test_open_ai_text_completion_serialize(openai_unit_test_env) -> None:
Expand Down
Loading

0 comments on commit 148e8de

Please sign in to comment.