Files
semantic-kernel/python/tests/conftest.py
T
Evan Mattson 6b01468efb Python: fix for kernel function type_object when not using Annotations. Add more unit tests. (#7338)
### Motivation and Context

The current kernel function decorator class parses function params, but
had a miss related to how the underlying param's type is parsed if not
using Annotations.

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

This PR properly handles the type_object for params that don't using
typing.Annotations.
- Adds unit tests to exercise the code.
- Adds more kernel.py unit tests for near 100% test coverage.
- Adds some planner tests to improve the test coverage.
- Closes #7300 

<!-- 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 😄
2024-07-18 05:12:08 +00:00

393 lines
12 KiB
Python

# Copyright (c) Microsoft. All rights reserved.
import warnings
from collections.abc import Callable
from typing import TYPE_CHECKING
from unittest.mock import MagicMock
import pytest
from semantic_kernel.contents.function_call_content import FunctionCallContent
if TYPE_CHECKING:
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.filters.functions.function_invocation_context import FunctionInvocationContext
from semantic_kernel.functions.kernel_function import KernelFunction
from semantic_kernel.kernel import Kernel
from semantic_kernel.services.ai_service_client_base import AIServiceClientBase
@pytest.fixture(scope="function")
def kernel() -> "Kernel":
from semantic_kernel.kernel import Kernel
return Kernel()
@pytest.fixture(scope="session")
def service() -> "AIServiceClientBase":
from semantic_kernel.services.ai_service_client_base import AIServiceClientBase
return AIServiceClientBase(service_id="service", ai_model_id="ai_model_id")
@pytest.fixture(scope="session")
def default_service() -> "AIServiceClientBase":
from semantic_kernel.services.ai_service_client_base import AIServiceClientBase
return AIServiceClientBase(service_id="default", ai_model_id="ai_model_id")
@pytest.fixture(scope="function")
def kernel_with_service(kernel: "Kernel", service: "AIServiceClientBase") -> "Kernel":
kernel.add_service(service)
return kernel
@pytest.fixture(scope="function")
def kernel_with_default_service(kernel: "Kernel", default_service: "AIServiceClientBase") -> "Kernel":
kernel.add_service(default_service)
return kernel
@pytest.fixture(scope="session")
def not_decorated_native_function() -> Callable:
def not_decorated_native_function(arg1: str) -> str:
return "test"
return not_decorated_native_function
@pytest.fixture(scope="session")
def decorated_native_function() -> Callable:
from semantic_kernel.functions.kernel_function_decorator import kernel_function
@kernel_function(name="getLightStatus")
def decorated_native_function(arg1: str) -> str:
return "test"
return decorated_native_function
@pytest.fixture(scope="session")
def custom_plugin_class():
from semantic_kernel.functions.kernel_function_decorator import kernel_function
class CustomPlugin:
@kernel_function(name="getLightStatus")
def decorated_native_function(self) -> str:
return "test"
return CustomPlugin
@pytest.fixture(scope="session")
def experimental_plugin_class():
from semantic_kernel.functions.kernel_function_decorator import kernel_function
from semantic_kernel.utils.experimental_decorator import experimental_class
@experimental_class
class ExperimentalPlugin:
@kernel_function(name="getLightStatus")
def decorated_native_function(self) -> str:
return "test"
return ExperimentalPlugin
@pytest.fixture(scope="session")
def create_mock_function() -> Callable:
from semantic_kernel.contents.streaming_text_content import StreamingTextContent
from semantic_kernel.functions.function_result import FunctionResult
from semantic_kernel.functions.kernel_function import KernelFunction
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata
async def stream_func(*args, **kwargs):
yield [StreamingTextContent(choice_index=0, text="test", metadata={})]
def create_mock_function(name: str, value: str = "test") -> "KernelFunction":
kernel_function_metadata = KernelFunctionMetadata(
name=name,
plugin_name="TestPlugin",
description="test description",
parameters=[],
is_prompt=True,
is_asynchronous=True,
)
class CustomKernelFunction(KernelFunction):
call_count: int = 0
async def _invoke_internal_stream(
self,
context: "FunctionInvocationContext",
) -> None:
self.call_count += 1
context.result = FunctionResult(
function=kernel_function_metadata,
value=stream_func(),
)
async def _invoke_internal(self, context: "FunctionInvocationContext"):
self.call_count += 1
context.result = FunctionResult(function=kernel_function_metadata, value=value, metadata={})
return CustomKernelFunction(metadata=kernel_function_metadata)
return create_mock_function
@pytest.fixture(scope="function")
def get_tool_call_mock():
tool_call_mock = MagicMock(spec=FunctionCallContent)
tool_call_mock.split_name_dict.return_value = {"arg_name": "arg_value"}
tool_call_mock.to_kernel_arguments.return_value = {"arg_name": "arg_value"}
tool_call_mock.name = "test-function"
tool_call_mock.function_name = "function"
tool_call_mock.plugin_name = "test"
tool_call_mock.arguments = {"arg_name": "arg_value"}
tool_call_mock.ai_model_id = None
tool_call_mock.metadata = {}
tool_call_mock.index = 0
tool_call_mock.parse_arguments.return_value = {"arg_name": "arg_value"}
tool_call_mock.id = "test_id"
return tool_call_mock
@pytest.fixture(scope="function")
def chat_history() -> "ChatHistory":
from semantic_kernel.contents.chat_history import ChatHistory
return ChatHistory()
@pytest.fixture(autouse=True)
def enable_debug_mode():
"""Set `autouse=True` to enable easy debugging for tests.
How to debug:
1. Ensure [snoop](https://github.com/alexmojaki/snoop) is installed
(`pip install snoop`).
2. If you're doing print based debugging, use `pr` instead of `print`.
That is, convert `print(some_var)` to `pr(some_var)`.
3. If you want a trace of a particular functions calls, just add `ss()` as the first
line of the function.
Note:
----
It's completely fine to leave `autouse=True` in the fixture. It doesn't affect
the tests unless you use `pr` or `ss` in any test.
Note:
----
When you use `ss` or `pr` in a test, pylance or mypy will complain. This is
because they don't know that we're adding these functions to the builtins. The
tests will run fine though.
"""
import builtins
try:
import snoop
except ImportError:
warnings.warn(
"Install snoop to enable trace debugging. `pip install snoop`",
ImportWarning,
)
return
builtins.ss = snoop.snoop(depth=4).__enter__
builtins.pr = snoop.pp
@pytest.fixture
def exclude_list(request):
"""Fixture that returns a list of environment variables to exclude."""
return request.param if hasattr(request, "param") else []
@pytest.fixture
def override_env_param_dict(request):
"""Fixture that returns a dict of environment variables to override."""
return request.param if hasattr(request, "param") else {}
@pytest.fixture()
def azure_openai_unit_test_env(monkeypatch, exclude_list, override_env_param_dict):
"""Fixture to set environment variables for AzureOpenAISettings."""
if exclude_list is None:
exclude_list = []
if override_env_param_dict is None:
override_env_param_dict = {}
env_vars = {
"AZURE_OPENAI_CHAT_DEPLOYMENT_NAME": "test_chat_deployment",
"AZURE_OPENAI_TEXT_DEPLOYMENT_NAME": "test_text_deployment",
"AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME": "test_embedding_deployment",
"AZURE_OPENAI_API_KEY": "test_api_key",
"AZURE_OPENAI_ENDPOINT": "https://test-endpoint.com",
"AZURE_OPENAI_API_VERSION": "2023-03-15-preview",
"AZURE_OPENAI_BASE_URL": "https://test_text_deployment.test-base-url.com",
}
env_vars.update(override_env_param_dict)
for key, value in env_vars.items():
if key not in exclude_list:
monkeypatch.setenv(key, value)
else:
monkeypatch.delenv(key, raising=False)
return env_vars
@pytest.fixture()
def openai_unit_test_env(monkeypatch, exclude_list, override_env_param_dict):
"""Fixture to set environment variables for OpenAISettings."""
if exclude_list is None:
exclude_list = []
if override_env_param_dict is None:
override_env_param_dict = {}
env_vars = {
"OPENAI_API_KEY": "test_api_key",
"OPENAI_ORG_ID": "test_org_id",
"OPENAI_CHAT_MODEL_ID": "test_chat_model_id",
"OPENAI_TEXT_MODEL_ID": "test_text_model_id",
"OPENAI_EMBEDDING_MODEL_ID": "test_embedding_model_id",
}
env_vars.update(override_env_param_dict)
for key, value in env_vars.items():
if key not in exclude_list:
monkeypatch.setenv(key, value)
else:
monkeypatch.delenv(key, raising=False)
return env_vars
@pytest.fixture()
def mistralai_unit_test_env(monkeypatch, exclude_list, override_env_param_dict):
"""Fixture to set environment variables for MistralAISettings."""
if exclude_list is None:
exclude_list = []
if override_env_param_dict is None:
override_env_param_dict = {}
env_vars = {"MISTRALAI_CHAT_MODEL_ID": "test_chat_model_id", "MISTRALAI_API_KEY": "test_api_key"}
env_vars.update(override_env_param_dict)
for key, value in env_vars.items():
if key not in exclude_list:
monkeypatch.setenv(key, value)
else:
monkeypatch.delenv(key, raising=False)
return env_vars
@pytest.fixture()
def aca_python_sessions_unit_test_env(monkeypatch, exclude_list, override_env_param_dict):
"""Fixture to set environment variables for ACA Python Unit Tests."""
if exclude_list is None:
exclude_list = []
if override_env_param_dict is None:
override_env_param_dict = {}
env_vars = {
"ACA_POOL_MANAGEMENT_ENDPOINT": "https://test.endpoint/",
}
env_vars.update(override_env_param_dict)
for key, value in env_vars.items():
if key not in exclude_list:
monkeypatch.setenv(key, value)
else:
monkeypatch.delenv(key, raising=False)
return env_vars
@pytest.fixture()
def azure_ai_search_unit_test_env(monkeypatch, exclude_list, override_env_param_dict):
"""Fixture to set environment variables for ACA Python Unit Tests."""
if exclude_list is None:
exclude_list = []
if override_env_param_dict is None:
override_env_param_dict = {}
env_vars = {
"AZURE_AI_SEARCH_API_KEY": "test-api-key",
"AZURE_AI_SEARCH_ENDPOINT": "https://test-endpoint.com",
"AZURE_AI_SEARCH_INDEX_NAME": "test-index-name",
}
env_vars.update(override_env_param_dict)
for key, value in env_vars.items():
if key not in exclude_list:
monkeypatch.setenv(key, value)
else:
monkeypatch.delenv(key, raising=False)
return env_vars
@pytest.fixture()
def bing_unit_test_env(monkeypatch, exclude_list, override_env_param_dict):
"""Fixture to set environment variables for BingConnector."""
if exclude_list is None:
exclude_list = []
if override_env_param_dict is None:
override_env_param_dict = {}
env_vars = {
"BING_API_KEY": "test_api_key",
"BING_CUSTOM_CONFIG": "test_org_id",
}
env_vars.update(override_env_param_dict)
for key, value in env_vars.items():
if key not in exclude_list:
monkeypatch.setenv(key, value)
else:
monkeypatch.delenv(key, raising=False)
return env_vars
@pytest.fixture()
def google_search_unit_test_env(monkeypatch, exclude_list, override_env_param_dict):
"""Fixture to set environment variables for the Google Search Connector."""
if exclude_list is None:
exclude_list = []
if override_env_param_dict is None:
override_env_param_dict = {}
env_vars = {
"GOOGLE_SEARCH_API_KEY": "test_api_key",
"GOOGLE_SEARCH_ENGINE_ID": "test_id",
}
env_vars.update(override_env_param_dict)
for key, value in env_vars.items():
if key not in exclude_list:
monkeypatch.setenv(key, value)
else:
monkeypatch.delenv(key, raising=False)
return env_vars