chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from semantic_kernel.contents.kernel_content import KernelContent
|
||||
from semantic_kernel.exceptions.function_exceptions import FunctionResultError
|
||||
from semantic_kernel.functions.function_result import FunctionResult
|
||||
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata
|
||||
|
||||
|
||||
def test_function_result_str_with_value():
|
||||
result = FunctionResult(
|
||||
function=KernelFunctionMetadata(name="test_function", is_prompt=False, is_asynchronous=False),
|
||||
value="test_value",
|
||||
)
|
||||
assert str(result) == "test_value"
|
||||
|
||||
|
||||
def test_function_result_str_with_list_value():
|
||||
result = FunctionResult(
|
||||
function=KernelFunctionMetadata(name="test_function", is_prompt=False, is_asynchronous=False),
|
||||
value=["test_value1", "test_value2"],
|
||||
)
|
||||
assert str(result) == "test_value1,test_value2"
|
||||
|
||||
|
||||
def test_function_result_str_with_kernel_content_list():
|
||||
class MockKernelContent(KernelContent):
|
||||
def __str__(self) -> str:
|
||||
return "mock_content"
|
||||
|
||||
def to_element(self) -> Any:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def from_element(cls: type["KernelContent"], element: Any) -> "KernelContent":
|
||||
pass
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
pass
|
||||
|
||||
content = MockKernelContent(inner_content="inner_content")
|
||||
result = FunctionResult(
|
||||
function=KernelFunctionMetadata(name="test_function", is_prompt=False, is_asynchronous=False), value=[content]
|
||||
)
|
||||
assert str(result) == "mock_content"
|
||||
|
||||
|
||||
def test_function_result_str_with_dict_value():
|
||||
result = FunctionResult(
|
||||
function=KernelFunctionMetadata(name="test_function", is_prompt=False, is_asynchronous=False),
|
||||
value={"key1": "value1", "key2": "value2"},
|
||||
)
|
||||
assert str(result) == "value2"
|
||||
|
||||
|
||||
def test_function_result_str_empty_value():
|
||||
result = FunctionResult(
|
||||
function=KernelFunctionMetadata(name="test_function", is_prompt=False, is_asynchronous=False), value=None
|
||||
)
|
||||
assert str(result) == ""
|
||||
|
||||
|
||||
def test_function_result_str_with_conversion_error():
|
||||
class Unconvertible:
|
||||
def __str__(self):
|
||||
raise ValueError("Cannot convert to string")
|
||||
|
||||
result = FunctionResult(
|
||||
function=KernelFunctionMetadata(name="test_function", is_prompt=False, is_asynchronous=False),
|
||||
value=Unconvertible(),
|
||||
)
|
||||
with pytest.raises(FunctionResultError, match="Failed to convert value to string"):
|
||||
str(result)
|
||||
|
||||
|
||||
def test_function_result_get_inner_content_with_list():
|
||||
class MockKernelContent(KernelContent):
|
||||
def __str__(self) -> str:
|
||||
return "mock_content"
|
||||
|
||||
def to_element(self) -> Any:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def from_element(cls: type["KernelContent"], element: Any) -> "KernelContent":
|
||||
pass
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
pass
|
||||
|
||||
content = MockKernelContent(inner_content="inner_content")
|
||||
result = FunctionResult(
|
||||
function=KernelFunctionMetadata(name="test_function", is_prompt=False, is_asynchronous=False), value=[content]
|
||||
)
|
||||
assert result.get_inner_content() == "inner_content"
|
||||
|
||||
|
||||
def test_function_result_get_inner_content_with_kernel_content():
|
||||
class MockKernelContent(KernelContent):
|
||||
def __str__(self) -> str:
|
||||
return "mock_content"
|
||||
|
||||
def to_element(self) -> Any:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def from_element(cls: type["KernelContent"], element: Any) -> "KernelContent":
|
||||
pass
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
pass
|
||||
|
||||
content = MockKernelContent(inner_content="inner_content")
|
||||
result = FunctionResult(
|
||||
function=KernelFunctionMetadata(name="test_function", is_prompt=False, is_asynchronous=False), value=content
|
||||
)
|
||||
assert result.get_inner_content() == "inner_content"
|
||||
|
||||
|
||||
def test_function_result_get_inner_content_no_inner_content():
|
||||
result = FunctionResult(
|
||||
function=KernelFunctionMetadata(name="test_function", is_prompt=False, is_asynchronous=False),
|
||||
value="test_value",
|
||||
)
|
||||
assert result.get_inner_content() is None
|
||||
@@ -0,0 +1,181 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import pytest
|
||||
|
||||
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
|
||||
from semantic_kernel.functions.kernel_arguments import KernelArguments
|
||||
|
||||
|
||||
def test_kernel_arguments():
|
||||
kargs = KernelArguments()
|
||||
assert kargs is not None
|
||||
assert kargs.execution_settings is None
|
||||
assert not kargs.keys()
|
||||
|
||||
|
||||
def test_kernel_arguments_with_input():
|
||||
kargs = KernelArguments(input=10)
|
||||
assert kargs is not None
|
||||
assert kargs["input"] == 10
|
||||
|
||||
|
||||
def test_kernel_arguments_with_input_get():
|
||||
kargs = KernelArguments(input=10)
|
||||
assert kargs is not None
|
||||
assert kargs.get("input", None) == 10
|
||||
assert not kargs.get("input2", None)
|
||||
|
||||
|
||||
def test_kernel_arguments_keys():
|
||||
kargs = KernelArguments(input=10)
|
||||
assert kargs is not None
|
||||
assert list(kargs.keys()) == ["input"]
|
||||
|
||||
|
||||
def test_kernel_arguments_with_execution_settings():
|
||||
test_pes = PromptExecutionSettings(service_id="test")
|
||||
kargs = KernelArguments(settings=[test_pes])
|
||||
assert kargs is not None
|
||||
assert kargs.execution_settings == {"test": test_pes}
|
||||
|
||||
|
||||
def test_kernel_arguments_bool():
|
||||
# An empty KernelArguments object should return False
|
||||
assert not KernelArguments()
|
||||
# An KernelArguments object with keyword arguments should return True
|
||||
assert KernelArguments(input=10)
|
||||
# An KernelArguments object with execution_settings should return True
|
||||
assert KernelArguments(settings=PromptExecutionSettings(service_id="test"))
|
||||
# An KernelArguments object with both keyword arguments and execution_settings should return True
|
||||
assert KernelArguments(input=10, settings=PromptExecutionSettings(service_id="test"))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"lhs, rhs, expected_dict, expected_settings_keys",
|
||||
[
|
||||
# Merging different keys
|
||||
(KernelArguments(a=1), KernelArguments(b=2), {"a": 1, "b": 2}, None),
|
||||
# RHS overwrites when keys duplicate
|
||||
(KernelArguments(a=1), KernelArguments(a=99), {"a": 99}, None),
|
||||
# Merging with a plain dict
|
||||
(KernelArguments(a=1), {"b": 2}, {"a": 1, "b": 2}, None),
|
||||
# Merging execution_settings together
|
||||
(
|
||||
KernelArguments(settings=PromptExecutionSettings(service_id="s1")),
|
||||
KernelArguments(settings=PromptExecutionSettings(service_id="s2")),
|
||||
{},
|
||||
["s1", "s2"],
|
||||
),
|
||||
# Same service_id is overwritten by RHS
|
||||
(
|
||||
KernelArguments(settings=PromptExecutionSettings(service_id="shared")),
|
||||
KernelArguments(settings=PromptExecutionSettings(service_id="shared")),
|
||||
{},
|
||||
["shared"],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_kernel_arguments_or_operator(lhs, rhs, expected_dict, expected_settings_keys):
|
||||
"""Test the __or__ operator (lhs | rhs) with various argument combinations."""
|
||||
result = lhs | rhs
|
||||
assert isinstance(result, KernelArguments)
|
||||
assert dict(result) == expected_dict
|
||||
if expected_settings_keys is None:
|
||||
assert result.execution_settings is None
|
||||
else:
|
||||
assert sorted(result.execution_settings.keys()) == sorted(expected_settings_keys)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("rhs", [42, "foo", None])
|
||||
def test_kernel_arguments_or_operator_with_invalid_type(rhs):
|
||||
"""Test the __or__ operator with an invalid type raises TypeError."""
|
||||
with pytest.raises(TypeError):
|
||||
KernelArguments() | rhs
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"lhs, rhs, expected_dict, expected_settings_keys",
|
||||
[
|
||||
# Dict merge (in-place)
|
||||
(KernelArguments(a=1), {"b": 2}, {"a": 1, "b": 2}, None),
|
||||
# Merging between KernelArguments
|
||||
(KernelArguments(a=1), KernelArguments(b=2), {"a": 1, "b": 2}, None),
|
||||
# Retain existing execution_settings after dict merge
|
||||
(KernelArguments(a=1, settings=PromptExecutionSettings(service_id="s1")), {"b": 2}, {"a": 1, "b": 2}, ["s1"]),
|
||||
# In-place merge of execution_settings
|
||||
(
|
||||
KernelArguments(settings=PromptExecutionSettings(service_id="s1")),
|
||||
KernelArguments(settings=PromptExecutionSettings(service_id="s2")),
|
||||
{},
|
||||
["s1", "s2"],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_kernel_arguments_inplace_merge(lhs, rhs, expected_dict, expected_settings_keys):
|
||||
"""Test the |= operator with various argument combinations without execution_settings."""
|
||||
original_id = id(lhs)
|
||||
lhs |= rhs
|
||||
# Verify this is the same object (in-place)
|
||||
assert id(lhs) == original_id
|
||||
assert dict(lhs) == expected_dict
|
||||
if expected_settings_keys is None:
|
||||
assert lhs.execution_settings is None
|
||||
else:
|
||||
assert sorted(lhs.execution_settings.keys()) == sorted(expected_settings_keys)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"rhs, lhs, expected_dict, expected_settings_keys",
|
||||
[
|
||||
# Merging different keys
|
||||
({"b": 2}, KernelArguments(a=1), {"b": 2, "a": 1}, None),
|
||||
# RHS overwrites when keys duplicate
|
||||
({"a": 1}, KernelArguments(a=99), {"a": 99}, None),
|
||||
# Merging with a KernelArguments
|
||||
({"b": 2}, KernelArguments(a=1), {"b": 2, "a": 1}, None),
|
||||
# Merging execution_settings together
|
||||
(
|
||||
{"test": "value"},
|
||||
KernelArguments(settings=PromptExecutionSettings(service_id="s2")),
|
||||
{"test": "value"},
|
||||
["s2"],
|
||||
),
|
||||
# Plain dict on the left with KernelArguments+settings on the right
|
||||
(
|
||||
{"a": 1},
|
||||
KernelArguments(b=2, settings=PromptExecutionSettings(service_id="shared")),
|
||||
{"a": 1, "b": 2},
|
||||
["shared"],
|
||||
),
|
||||
# KernelArguments on both sides with execution_settings
|
||||
(
|
||||
KernelArguments(a=1, settings=PromptExecutionSettings(service_id="s1")),
|
||||
KernelArguments(b=2, settings=PromptExecutionSettings(service_id="s2")),
|
||||
{"a": 1, "b": 2},
|
||||
["s1", "s2"],
|
||||
),
|
||||
# Same service_id is overwritten by RHS (KernelArguments)
|
||||
(
|
||||
KernelArguments(a=1, settings=PromptExecutionSettings(service_id="shared")),
|
||||
KernelArguments(b=2, settings=PromptExecutionSettings(service_id="shared")),
|
||||
{"a": 1, "b": 2},
|
||||
["shared"],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_kernel_arguments_ror_operator(rhs, lhs, expected_dict, expected_settings_keys):
|
||||
"""Test the __ror__ operator (lhs | rhs) with various argument combinations."""
|
||||
result = rhs | lhs
|
||||
assert isinstance(result, KernelArguments)
|
||||
assert dict(result) == expected_dict
|
||||
if expected_settings_keys is None:
|
||||
assert result.execution_settings is None
|
||||
else:
|
||||
assert sorted(result.execution_settings.keys()) == sorted(expected_settings_keys)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("lhs", [42, "foo", None])
|
||||
def test_kernel_arguments_ror_operator_with_invalid_type(lhs):
|
||||
"""Test the __ror__ operator with an invalid type raises TypeError."""
|
||||
with pytest.raises(TypeError):
|
||||
lhs | KernelArguments()
|
||||
@@ -0,0 +1,282 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from collections.abc import AsyncGenerator, AsyncIterable
|
||||
from inspect import Parameter, Signature
|
||||
from typing import TYPE_CHECKING, Annotated, Any, Union
|
||||
|
||||
import pytest
|
||||
|
||||
from semantic_kernel.functions.kernel_function_decorator import _process_signature, kernel_function
|
||||
from semantic_kernel.kernel_pydantic import KernelBaseModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from semantic_kernel.functions.kernel_arguments import KernelArguments
|
||||
|
||||
|
||||
class InputObject(KernelBaseModel):
|
||||
arg1: str
|
||||
arg2: int
|
||||
|
||||
|
||||
class MiscClass:
|
||||
__test__ = False
|
||||
|
||||
@kernel_function(description="description")
|
||||
def func_with_description(self, input):
|
||||
return input
|
||||
|
||||
@kernel_function(description="description")
|
||||
def func_no_name(self, input):
|
||||
return input
|
||||
|
||||
@kernel_function(description="description", name="my-name")
|
||||
def func_with_name(self, input):
|
||||
return input
|
||||
|
||||
@kernel_function
|
||||
def func_docstring_as_description(self, input):
|
||||
"""Description."""
|
||||
return input
|
||||
|
||||
@kernel_function
|
||||
def func_input_annotated(self, input: Annotated[str, "input description"]):
|
||||
return input
|
||||
|
||||
@kernel_function
|
||||
def func_input_annotated_optional(self, input: Annotated[str | None, "input description"] = "test"):
|
||||
return input
|
||||
|
||||
@kernel_function
|
||||
def func_input_optional(self, input: str | None = "test"):
|
||||
return input
|
||||
|
||||
@kernel_function
|
||||
def func_return_type(self, input: str) -> str:
|
||||
return input
|
||||
|
||||
@kernel_function
|
||||
def func_return_type_optional(self, input: str) -> str | None:
|
||||
return input
|
||||
|
||||
@kernel_function
|
||||
def func_return_type_annotated(self, input: str) -> Annotated[str, "test return"]:
|
||||
return input
|
||||
|
||||
@kernel_function
|
||||
def func_return_type_streaming(self, input: str) -> Annotated[AsyncGenerator[str, Any], "test return"]: # type: ignore
|
||||
yield input
|
||||
|
||||
@kernel_function
|
||||
def func_input_object(self, input: InputObject):
|
||||
return input
|
||||
|
||||
@kernel_function
|
||||
def func_input_object_optional(self, input: InputObject | None = None):
|
||||
return input
|
||||
|
||||
@kernel_function
|
||||
def func_input_object_annotated(self, input: Annotated[InputObject, "input description"]):
|
||||
return input
|
||||
|
||||
@kernel_function
|
||||
def func_input_object_annotated_optional(self, input: Annotated[InputObject | None, "input description"] = None):
|
||||
return input
|
||||
|
||||
@kernel_function
|
||||
def func_input_object_union(self, input: InputObject | str):
|
||||
return input
|
||||
|
||||
@kernel_function
|
||||
def func_no_typing(self, input):
|
||||
return input
|
||||
|
||||
|
||||
def test_func_name_as_name():
|
||||
decorator_test = MiscClass()
|
||||
my_func = getattr(decorator_test, "func_with_description")
|
||||
assert my_func.__kernel_function_name__ == "func_with_description"
|
||||
|
||||
|
||||
def test_description():
|
||||
decorator_test = MiscClass()
|
||||
my_func = getattr(decorator_test, "func_with_description")
|
||||
assert my_func.__kernel_function_description__ == "description"
|
||||
|
||||
|
||||
def test_kernel_function_name_not_specified():
|
||||
decorator_test = MiscClass()
|
||||
my_func = getattr(decorator_test, "func_no_name")
|
||||
assert my_func.__kernel_function_name__ == "func_no_name"
|
||||
|
||||
|
||||
def test_kernel_function_with_name_specified():
|
||||
decorator_test = MiscClass()
|
||||
my_func = getattr(decorator_test, "func_with_name")
|
||||
assert my_func.__kernel_function_name__ == "my-name"
|
||||
|
||||
|
||||
def test_kernel_function_docstring_as_description():
|
||||
decorator_test = MiscClass()
|
||||
my_func = getattr(decorator_test, "func_docstring_as_description")
|
||||
assert my_func.__kernel_function_description__ == "Description."
|
||||
|
||||
|
||||
def test_kernel_function_param_annotated():
|
||||
decorator_test = MiscClass()
|
||||
my_func = getattr(decorator_test, "func_input_annotated")
|
||||
assert my_func.__kernel_function_parameters__[0]["description"] == "input description"
|
||||
assert my_func.__kernel_function_parameters__[0]["type_"] == "str"
|
||||
assert my_func.__kernel_function_parameters__[0]["is_required"]
|
||||
assert my_func.__kernel_function_parameters__[0].get("default_value") is None
|
||||
assert my_func.__kernel_function_parameters__[0]["name"] == "input"
|
||||
|
||||
|
||||
def test_kernel_function_param_optional():
|
||||
decorator_test = MiscClass()
|
||||
my_func = getattr(decorator_test, "func_input_optional")
|
||||
assert my_func.__kernel_function_parameters__[0]["type_"] == "str"
|
||||
assert not my_func.__kernel_function_parameters__[0]["is_required"]
|
||||
assert my_func.__kernel_function_parameters__[0]["default_value"] == "test"
|
||||
assert my_func.__kernel_function_parameters__[0]["name"] == "input"
|
||||
|
||||
|
||||
def test_kernel_function_param_annotated_optional():
|
||||
decorator_test = MiscClass()
|
||||
my_func = getattr(decorator_test, "func_input_annotated_optional")
|
||||
assert my_func.__kernel_function_parameters__[0]["description"] == "input description"
|
||||
assert my_func.__kernel_function_parameters__[0]["type_"] == "str"
|
||||
assert not my_func.__kernel_function_parameters__[0]["is_required"]
|
||||
assert my_func.__kernel_function_parameters__[0]["default_value"] == "test"
|
||||
assert my_func.__kernel_function_parameters__[0]["name"] == "input"
|
||||
|
||||
|
||||
def test_kernel_function_return_type():
|
||||
decorator_test = MiscClass()
|
||||
my_func = getattr(decorator_test, "func_return_type")
|
||||
assert my_func.__kernel_function_return_type__ == "str"
|
||||
assert my_func.__kernel_function_return_required__
|
||||
assert not my_func.__kernel_function_streaming__
|
||||
|
||||
|
||||
def test_kernel_function_return_type_optional():
|
||||
decorator_test = MiscClass()
|
||||
my_func = getattr(decorator_test, "func_return_type_optional")
|
||||
assert my_func.__kernel_function_return_type__ == "str"
|
||||
assert my_func.__kernel_function_return_description__ == ""
|
||||
assert not my_func.__kernel_function_return_required__
|
||||
assert not my_func.__kernel_function_streaming__
|
||||
|
||||
|
||||
def test_kernel_function_return_type_annotated():
|
||||
decorator_test = MiscClass()
|
||||
my_func = getattr(decorator_test, "func_return_type_annotated")
|
||||
assert my_func.__kernel_function_return_type__ == "str"
|
||||
assert my_func.__kernel_function_return_description__ == "test return"
|
||||
assert my_func.__kernel_function_return_required__
|
||||
assert not my_func.__kernel_function_streaming__
|
||||
|
||||
|
||||
def test_kernel_function_return_type_streaming():
|
||||
decorator_test = MiscClass()
|
||||
my_func = getattr(decorator_test, "func_return_type_streaming")
|
||||
assert my_func.__kernel_function_return_type__ in ("str, Any", "str, typing.Any")
|
||||
assert my_func.__kernel_function_return_description__ == "test return"
|
||||
assert my_func.__kernel_function_return_required__
|
||||
assert my_func.__kernel_function_streaming__
|
||||
|
||||
|
||||
def test_kernel_function_input_object():
|
||||
decorator_test = MiscClass()
|
||||
my_func = getattr(decorator_test, "func_input_object")
|
||||
assert my_func.__kernel_function_parameters__[0]["type_"] == "InputObject"
|
||||
assert my_func.__kernel_function_parameters__[0]["is_required"]
|
||||
assert my_func.__kernel_function_parameters__[0].get("default_value") is None
|
||||
assert my_func.__kernel_function_parameters__[0]["name"] == "input"
|
||||
assert my_func.__kernel_function_parameters__[0]["type_object"] == InputObject
|
||||
|
||||
|
||||
def test_kernel_function_input_object_optional():
|
||||
decorator_test = MiscClass()
|
||||
my_func = getattr(decorator_test, "func_input_object_optional")
|
||||
assert my_func.__kernel_function_parameters__[0]["type_"] == "InputObject"
|
||||
assert not my_func.__kernel_function_parameters__[0]["is_required"]
|
||||
assert my_func.__kernel_function_parameters__[0]["default_value"] is None
|
||||
assert my_func.__kernel_function_parameters__[0]["name"] == "input"
|
||||
assert my_func.__kernel_function_parameters__[0]["type_object"] == InputObject
|
||||
|
||||
|
||||
def test_kernel_function_input_object_annotated():
|
||||
decorator_test = MiscClass()
|
||||
my_func = getattr(decorator_test, "func_input_object_annotated")
|
||||
assert my_func.__kernel_function_parameters__[0]["description"] == "input description"
|
||||
assert my_func.__kernel_function_parameters__[0]["type_"] == "InputObject"
|
||||
assert my_func.__kernel_function_parameters__[0]["is_required"]
|
||||
assert my_func.__kernel_function_parameters__[0].get("default_value") is None
|
||||
assert my_func.__kernel_function_parameters__[0]["name"] == "input"
|
||||
assert my_func.__kernel_function_parameters__[0]["type_object"] == InputObject
|
||||
|
||||
|
||||
def test_kernel_function_input_object_annotated_optional():
|
||||
decorator_test = MiscClass()
|
||||
my_func = getattr(decorator_test, "func_input_object_annotated_optional")
|
||||
assert my_func.__kernel_function_parameters__[0]["description"] == "input description"
|
||||
assert my_func.__kernel_function_parameters__[0]["type_"] == "InputObject"
|
||||
assert not my_func.__kernel_function_parameters__[0]["is_required"]
|
||||
assert my_func.__kernel_function_parameters__[0]["default_value"] is None
|
||||
assert my_func.__kernel_function_parameters__[0]["name"] == "input"
|
||||
assert my_func.__kernel_function_parameters__[0]["type_object"] == InputObject
|
||||
|
||||
|
||||
def test_kernel_function_input_object_union():
|
||||
decorator_test = MiscClass()
|
||||
my_func = getattr(decorator_test, "func_input_object_union")
|
||||
assert my_func.__kernel_function_parameters__[0]["type_"] == "InputObject, str"
|
||||
assert my_func.__kernel_function_parameters__[0]["is_required"]
|
||||
assert my_func.__kernel_function_parameters__[0].get("default_value") is None
|
||||
assert my_func.__kernel_function_parameters__[0]["name"] == "input"
|
||||
|
||||
|
||||
def test_kernel_function_no_typing():
|
||||
decorator_test = MiscClass()
|
||||
my_func = getattr(decorator_test, "func_no_typing")
|
||||
assert my_func.__kernel_function_parameters__[0]["type_"] == "Any"
|
||||
assert my_func.__kernel_function_parameters__[0]["is_required"]
|
||||
assert my_func.__kernel_function_parameters__[0].get("default_value") is None
|
||||
assert my_func.__kernel_function_parameters__[0]["name"] == "input"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("name", "annotation", "description", "type_", "is_required"),
|
||||
[
|
||||
("anno_str", Annotated[str, "test"], "test", "str", True),
|
||||
("anno_opt_str", Annotated[str | None, "test"], "test", "str", False),
|
||||
("anno_iter_str", Annotated[AsyncIterable[str], "test"], "test", "str", True),
|
||||
("anno_opt_str_int", Annotated[str | int | None, "test"], "test", "str, int", False),
|
||||
("str", str, None, "str", True),
|
||||
("union", Union[str, int, float, "KernelArguments"], None, "str, int, float, KernelArguments", True),
|
||||
("new_union", "str | int | float | KernelArguments", None, "str, int, float, KernelArguments", True),
|
||||
("opt_str", str | None, None, "str", False),
|
||||
("list_str", list[str], None, "list[str]", True),
|
||||
("dict_str", dict[str, str], None, "dict[str, str]", True),
|
||||
("list_str_opt", list[str] | None, None, "list[str]", False),
|
||||
("anno_dict_str", Annotated[dict[str, str], "description"], "description", "dict[str, str]", True),
|
||||
("anno_opt_dict_str", Annotated[dict | str | None, "description"], "description", "dict, str", False),
|
||||
],
|
||||
)
|
||||
def test_annotation_parsing(name, annotation, description, type_, is_required):
|
||||
param = Parameter(
|
||||
name=name,
|
||||
annotation=annotation,
|
||||
default=Parameter.empty,
|
||||
kind=Parameter.POSITIONAL_OR_KEYWORD,
|
||||
)
|
||||
func_sig = Signature(parameters=[param])
|
||||
|
||||
annotations = _process_signature(func_sig)
|
||||
|
||||
assert len(annotations) == 1
|
||||
annotation_dict = annotations[0]
|
||||
|
||||
assert description == annotation_dict.get("description")
|
||||
assert type_ == annotation_dict["type_"]
|
||||
assert is_required == annotation_dict["is_required"]
|
||||
@@ -0,0 +1,568 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
from collections.abc import AsyncGenerator, Iterable
|
||||
from typing import Annotated, Any
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion import OpenAIChatCompletion
|
||||
from semantic_kernel.exceptions import FunctionExecutionException, FunctionInitializationError
|
||||
from semantic_kernel.filters.functions.function_invocation_context import FunctionInvocationContext
|
||||
from semantic_kernel.filters.kernel_filters_extension import _rebuild_function_invocation_context
|
||||
from semantic_kernel.functions.function_result import FunctionResult
|
||||
from semantic_kernel.functions.kernel_arguments import KernelArguments
|
||||
from semantic_kernel.functions.kernel_function import KernelFunction
|
||||
from semantic_kernel.functions.kernel_function_decorator import kernel_function
|
||||
from semantic_kernel.functions.kernel_function_from_method import KernelFunctionFromMethod
|
||||
from semantic_kernel.functions.kernel_parameter_metadata import KernelParameterMetadata
|
||||
from semantic_kernel.kernel import Kernel
|
||||
from semantic_kernel.kernel_pydantic import KernelBaseModel
|
||||
|
||||
|
||||
class CustomType(KernelBaseModel):
|
||||
id: str
|
||||
name: str
|
||||
|
||||
|
||||
class CustomTypeNonPydantic:
|
||||
id: str
|
||||
name: str
|
||||
|
||||
def __init__(self, id: str, name: str):
|
||||
self.id = id
|
||||
self.name = name
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def get_custom_type_function_pydantic():
|
||||
@kernel_function
|
||||
def func_default(param: list[CustomType]):
|
||||
return input
|
||||
|
||||
return KernelFunction.from_method(func_default, "test")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def get_custom_type_function_nonpydantic():
|
||||
@kernel_function
|
||||
def func_default(param: list[CustomTypeNonPydantic]):
|
||||
return input
|
||||
|
||||
return KernelFunction.from_method(func_default, "test")
|
||||
|
||||
|
||||
def test_init_native_function_with_input_description():
|
||||
@kernel_function(description="Mock description", name="mock_function")
|
||||
def mock_function(input: Annotated[str, "input"], arguments: "KernelArguments") -> None:
|
||||
pass
|
||||
|
||||
mock_method = mock_function
|
||||
|
||||
native_function = KernelFunction.from_method(method=mock_method, plugin_name="MockPlugin")
|
||||
|
||||
assert native_function.method == mock_method
|
||||
assert native_function.parameters[0].name == "input"
|
||||
assert native_function.parameters[0].description == "input"
|
||||
assert not native_function.parameters[0].default_value
|
||||
assert native_function.parameters[0].type_ == "str"
|
||||
assert native_function.parameters[0].is_required is True
|
||||
assert native_function.parameters[1].name == "arguments"
|
||||
assert native_function.parameters[1].description is None
|
||||
assert not native_function.parameters[1].default_value
|
||||
assert native_function.parameters[1].type_ == "KernelArguments"
|
||||
assert native_function.parameters[1].is_required is True
|
||||
|
||||
|
||||
def test_init_native_function_without_input_description():
|
||||
@kernel_function()
|
||||
def mock_function(arguments: "KernelArguments") -> None:
|
||||
pass
|
||||
|
||||
mock_function.__kernel_function__ = True
|
||||
mock_function.__kernel_function_name__ = "mock_function_no_input_desc"
|
||||
mock_function.__kernel_function_description__ = "Mock description no input desc"
|
||||
mock_function.__kernel_function_parameters__ = [
|
||||
{
|
||||
"name": "arguments",
|
||||
"description": "Param 1 description",
|
||||
"default_value": "default_param1_value",
|
||||
"is_required": True,
|
||||
}
|
||||
]
|
||||
|
||||
mock_method = mock_function
|
||||
|
||||
native_function = KernelFunction.from_method(method=mock_method, plugin_name="MockPlugin")
|
||||
|
||||
assert native_function.method == mock_method
|
||||
assert native_function.parameters[0].name == "arguments"
|
||||
assert native_function.parameters[0].description == "Param 1 description"
|
||||
assert native_function.parameters[0].default_value == "default_param1_value"
|
||||
assert native_function.parameters[0].type_ == "str"
|
||||
assert native_function.parameters[0].is_required is True
|
||||
|
||||
|
||||
def test_init_native_function_from_kernel_function_decorator():
|
||||
@kernel_function(
|
||||
description="Test description",
|
||||
name="test_function",
|
||||
)
|
||||
def decorated_function(input: Annotated[str | None, "Test input description"] = "test_default_value") -> None:
|
||||
pass
|
||||
|
||||
assert decorated_function.__kernel_function__ is True
|
||||
assert decorated_function.__kernel_function_description__ == "Test description"
|
||||
assert decorated_function.__kernel_function_name__ == "test_function"
|
||||
|
||||
native_function = KernelFunction.from_method(method=decorated_function, plugin_name="MockPlugin")
|
||||
|
||||
assert native_function.method == decorated_function
|
||||
assert native_function.parameters[0].name == "input"
|
||||
assert native_function.parameters[0].description == "Test input description"
|
||||
assert native_function.parameters[0].default_value == "test_default_value"
|
||||
assert native_function.parameters[0].type_ == "str"
|
||||
assert native_function.parameters[0].is_required is False
|
||||
assert type(native_function.return_parameter) is KernelParameterMetadata
|
||||
|
||||
|
||||
def test_init_native_function_from_kernel_function_decorator_defaults():
|
||||
@kernel_function()
|
||||
def decorated_function() -> None:
|
||||
pass
|
||||
|
||||
assert decorated_function.__kernel_function__ is True
|
||||
assert decorated_function.__kernel_function_description__ is None
|
||||
assert decorated_function.__kernel_function_name__ == "decorated_function"
|
||||
|
||||
native_function = KernelFunction.from_method(method=decorated_function, plugin_name="MockPlugin")
|
||||
|
||||
assert native_function.method == decorated_function
|
||||
assert len(native_function.parameters) == 0
|
||||
|
||||
|
||||
def test_init_method_is_none():
|
||||
with pytest.raises(FunctionInitializationError):
|
||||
KernelFunction.from_method(method=None, plugin_name="MockPlugin")
|
||||
|
||||
|
||||
def test_init_method_is_not_kernel_function():
|
||||
def not_kernel_function():
|
||||
pass
|
||||
|
||||
with pytest.raises(FunctionInitializationError):
|
||||
KernelFunction.from_method(method=not_kernel_function, plugin_name="MockPlugin")
|
||||
|
||||
|
||||
def test_init_invalid_name():
|
||||
@kernel_function(name="invalid name")
|
||||
def invalid_name():
|
||||
pass
|
||||
|
||||
with pytest.raises(FunctionInitializationError):
|
||||
KernelFunction.from_method(method=invalid_name, plugin_name="MockPlugin")
|
||||
|
||||
|
||||
async def test_invoke_non_async(kernel: Kernel):
|
||||
@kernel_function()
|
||||
def non_async_function() -> str:
|
||||
return ""
|
||||
|
||||
native_function = KernelFunction.from_method(method=non_async_function, plugin_name="MockPlugin")
|
||||
|
||||
result = await native_function.invoke(kernel=kernel, arguments=None)
|
||||
assert result.value == ""
|
||||
|
||||
with pytest.raises(NotImplementedError):
|
||||
async for _ in native_function.invoke_stream(kernel=kernel, arguments=None):
|
||||
pass
|
||||
|
||||
|
||||
async def test_invoke_async(kernel: Kernel):
|
||||
@kernel_function()
|
||||
async def async_function() -> str:
|
||||
return ""
|
||||
|
||||
native_function = KernelFunction.from_method(method=async_function, plugin_name="MockPlugin")
|
||||
|
||||
result = await native_function.invoke(kernel=kernel, arguments=None)
|
||||
assert result.value == ""
|
||||
|
||||
with pytest.raises(NotImplementedError):
|
||||
async for _ in native_function.invoke_stream(kernel=kernel, arguments=None):
|
||||
pass
|
||||
|
||||
|
||||
async def test_invoke_gen(kernel: Kernel):
|
||||
@kernel_function()
|
||||
def gen_function() -> Iterable[str]:
|
||||
yield ""
|
||||
|
||||
native_function = KernelFunction.from_method(method=gen_function, plugin_name="MockPlugin")
|
||||
|
||||
result = await native_function.invoke(kernel=kernel, arguments=None)
|
||||
assert result.value == [""]
|
||||
|
||||
async for partial_result in native_function.invoke_stream(kernel=kernel, arguments=None):
|
||||
assert partial_result == ""
|
||||
|
||||
|
||||
async def test_invoke_gen_async(kernel: Kernel):
|
||||
@kernel_function()
|
||||
async def async_gen_function() -> AsyncGenerator[str, Any]:
|
||||
yield ""
|
||||
|
||||
native_function = KernelFunction.from_method(method=async_gen_function, plugin_name="MockPlugin")
|
||||
|
||||
result = await native_function.invoke(kernel=kernel, arguments=None)
|
||||
assert result.value == [""]
|
||||
|
||||
async for partial_result in native_function.invoke_stream(kernel=kernel, arguments=None):
|
||||
assert partial_result == ""
|
||||
|
||||
|
||||
async def test_service_execution(kernel: Kernel, openai_unit_test_env):
|
||||
service = OpenAIChatCompletion(service_id="test", ai_model_id="test")
|
||||
req_settings = service.get_prompt_execution_settings_class()(service_id="test")
|
||||
req_settings.temperature = 0.5
|
||||
kernel.add_service(service)
|
||||
arguments = KernelArguments(settings=req_settings)
|
||||
|
||||
@kernel_function(name="function")
|
||||
def my_function(kernel, service, execution_settings, arguments) -> str:
|
||||
assert kernel is not None
|
||||
assert isinstance(kernel, Kernel)
|
||||
assert service is not None
|
||||
assert isinstance(service, OpenAIChatCompletion)
|
||||
assert execution_settings is not None
|
||||
assert isinstance(execution_settings, req_settings.__class__)
|
||||
assert execution_settings.temperature == 0.5
|
||||
assert arguments is not None
|
||||
assert isinstance(arguments, KernelArguments)
|
||||
return "ok"
|
||||
|
||||
func = KernelFunction.from_method(my_function, "test")
|
||||
|
||||
result = await func.invoke(kernel, arguments)
|
||||
assert result.value == "ok"
|
||||
|
||||
|
||||
async def test_required_param_not_supplied(kernel: Kernel):
|
||||
@kernel_function()
|
||||
def my_function(input: str) -> str:
|
||||
return input
|
||||
|
||||
func = KernelFunction.from_method(my_function, "test")
|
||||
|
||||
with pytest.raises(FunctionExecutionException):
|
||||
await func.invoke(kernel=kernel, arguments=KernelArguments())
|
||||
|
||||
|
||||
async def test_service_execution_with_complex_object(kernel: Kernel):
|
||||
class InputObject(KernelBaseModel):
|
||||
arg1: str
|
||||
arg2: int
|
||||
|
||||
@kernel_function(name="function")
|
||||
def my_function(input_obj: InputObject) -> str:
|
||||
assert input_obj is not None
|
||||
assert isinstance(input_obj, InputObject)
|
||||
assert input_obj.arg1 == "test"
|
||||
assert input_obj.arg2 == 5
|
||||
return f"{input_obj.arg1} {input_obj.arg2}"
|
||||
|
||||
func = KernelFunction.from_method(my_function, "test")
|
||||
|
||||
arguments = KernelArguments(input_obj=InputObject(arg1="test", arg2=5))
|
||||
result = await func.invoke(kernel, arguments)
|
||||
assert result.value == "test 5"
|
||||
|
||||
|
||||
class InputObject(KernelBaseModel):
|
||||
arg1: str
|
||||
arg2: int
|
||||
|
||||
|
||||
async def test_service_execution_with_complex_object_from_str(kernel: Kernel):
|
||||
@kernel_function(name="function")
|
||||
def my_function(input_obj: InputObject) -> str:
|
||||
assert input_obj is not None
|
||||
assert isinstance(input_obj, InputObject)
|
||||
assert input_obj.arg1 == "test"
|
||||
assert input_obj.arg2 == 5
|
||||
return f"{input_obj.arg1} {input_obj.arg2}"
|
||||
|
||||
func = KernelFunction.from_method(my_function, "test")
|
||||
|
||||
arguments = KernelArguments(input_obj={"arg1": "test", "arg2": 5})
|
||||
result = await func.invoke(kernel, arguments)
|
||||
assert result.value == "test 5"
|
||||
|
||||
|
||||
async def test_service_execution_with_complex_object_from_str_mixed(kernel: Kernel):
|
||||
@kernel_function(name="function")
|
||||
def my_function(input_obj: InputObject, input_str: str) -> str:
|
||||
assert input_obj is not None
|
||||
assert isinstance(input_obj, InputObject)
|
||||
assert input_obj.arg1 == "test"
|
||||
assert input_obj.arg2 == 5
|
||||
return f"{input_obj.arg1} {input_str} {input_obj.arg2}"
|
||||
|
||||
func = KernelFunction.from_method(my_function, "test")
|
||||
|
||||
arguments = KernelArguments(input_obj={"arg1": "test", "arg2": 5}, input_str="test2")
|
||||
result = await func.invoke(kernel, arguments)
|
||||
assert result.value == "test test2 5"
|
||||
|
||||
|
||||
async def test_service_execution_with_complex_object_from_str_mixed_multi(kernel: Kernel):
|
||||
@kernel_function(name="function")
|
||||
def my_function(input_obj: InputObject, input_str: str | int) -> str:
|
||||
assert input_obj is not None
|
||||
assert isinstance(input_obj, InputObject)
|
||||
assert input_obj.arg1 == "test"
|
||||
assert input_obj.arg2 == 5
|
||||
return f"{input_obj.arg1} {input_str} {input_obj.arg2}"
|
||||
|
||||
func = KernelFunction.from_method(my_function, "test")
|
||||
|
||||
arguments = KernelArguments(input_obj={"arg1": "test", "arg2": 5}, input_str="test2")
|
||||
result = await func.invoke(kernel, arguments)
|
||||
assert result.value == "test test2 5"
|
||||
|
||||
|
||||
def test_function_from_lambda():
|
||||
func = KernelFunctionFromMethod(method=kernel_function(lambda x: x**2, name="square"), plugin_name="math")
|
||||
assert func is not None
|
||||
|
||||
|
||||
async def test_function_invoke_return_list_type(kernel: Kernel):
|
||||
@kernel_function(name="list_func")
|
||||
def test_list_func() -> list[str]:
|
||||
return ["test1", "test2"]
|
||||
|
||||
func = KernelFunction.from_method(test_list_func, "test")
|
||||
|
||||
result = await kernel.invoke(function=func)
|
||||
assert str(result) == "test1,test2"
|
||||
|
||||
|
||||
async def test_function_invocation_filters(kernel: Kernel):
|
||||
func = KernelFunctionFromMethod(method=kernel_function(lambda input: input**2, name="square"), plugin_name="math")
|
||||
kernel.add_function(plugin_name="math", function=func)
|
||||
|
||||
pre_call_count = 0
|
||||
post_call_count = 0
|
||||
|
||||
async def custom_filter(context, next):
|
||||
nonlocal pre_call_count
|
||||
pre_call_count += 1
|
||||
await next(context)
|
||||
nonlocal post_call_count
|
||||
post_call_count += 1
|
||||
|
||||
kernel.add_filter("function_invocation", custom_filter)
|
||||
result = await kernel.invoke(plugin_name="math", function_name="square", arguments=KernelArguments(input=2))
|
||||
assert result.value == 4
|
||||
assert pre_call_count == 1
|
||||
assert post_call_count == 1
|
||||
|
||||
|
||||
async def test_function_invocation_multiple_filters(kernel: Kernel):
|
||||
call_stack = []
|
||||
|
||||
@kernel_function(name="square")
|
||||
def func(input: int):
|
||||
nonlocal call_stack
|
||||
call_stack.append("func")
|
||||
return input**2
|
||||
|
||||
kernel.add_function(plugin_name="math", function=func)
|
||||
|
||||
async def custom_filter1(context, next):
|
||||
nonlocal call_stack
|
||||
call_stack.append("custom_filter1_pre")
|
||||
await next(context)
|
||||
call_stack.append("custom_filter1_post")
|
||||
|
||||
async def custom_filter2(context, next):
|
||||
nonlocal call_stack
|
||||
call_stack.append("custom_filter2_pre")
|
||||
await next(context)
|
||||
call_stack.append("custom_filter2_post")
|
||||
|
||||
kernel.add_filter("function_invocation", custom_filter1)
|
||||
kernel.add_filter("function_invocation", custom_filter2)
|
||||
result = await kernel.invoke(plugin_name="math", function_name="square", arguments=KernelArguments(input=2))
|
||||
assert result.value == 4
|
||||
assert call_stack == [
|
||||
"custom_filter1_pre",
|
||||
"custom_filter2_pre",
|
||||
"func",
|
||||
"custom_filter2_post",
|
||||
"custom_filter1_post",
|
||||
]
|
||||
|
||||
|
||||
async def test_function_invocation_filters_streaming(kernel: Kernel):
|
||||
call_stack = []
|
||||
|
||||
@kernel_function(name="square")
|
||||
async def func(input: int):
|
||||
nonlocal call_stack
|
||||
call_stack.append("func1")
|
||||
yield input**2
|
||||
call_stack.append("func2")
|
||||
yield input**3
|
||||
|
||||
kernel.add_function(plugin_name="math", function=func)
|
||||
|
||||
async def custom_filter(context, next):
|
||||
nonlocal call_stack
|
||||
call_stack.append("custom_filter_pre")
|
||||
await next(context)
|
||||
|
||||
async def override_stream(stream):
|
||||
nonlocal call_stack
|
||||
async for partial in stream:
|
||||
call_stack.append("overridden_func")
|
||||
yield partial * 2
|
||||
|
||||
stream = context.result.value
|
||||
context.result = FunctionResult(function=context.result.function, value=override_stream(stream))
|
||||
call_stack.append("custom_filter_post")
|
||||
|
||||
kernel.add_filter("function_invocation", custom_filter)
|
||||
index = 0
|
||||
async for partial in kernel.invoke_stream(
|
||||
plugin_name="math", function_name="square", arguments=KernelArguments(input=2)
|
||||
):
|
||||
assert partial == 8 if index == 0 else 16
|
||||
index += 1
|
||||
assert call_stack == [
|
||||
"custom_filter_pre",
|
||||
"custom_filter_post",
|
||||
"func1",
|
||||
"overridden_func",
|
||||
"func2",
|
||||
"overridden_func",
|
||||
]
|
||||
|
||||
|
||||
async def test_default_handling(kernel: Kernel):
|
||||
@kernel_function
|
||||
def func_default(input: str = "test"):
|
||||
return input
|
||||
|
||||
func = kernel.add_function(plugin_name="test", function_name="func_default", function=func_default)
|
||||
|
||||
res = await kernel.invoke(func)
|
||||
assert str(res) == "test"
|
||||
|
||||
|
||||
async def test_default_handling_2(kernel: Kernel):
|
||||
@kernel_function
|
||||
def func_default(base: str, input: str = "test"):
|
||||
return input
|
||||
|
||||
func = kernel.add_function(plugin_name="test", function_name="func_default", function=func_default)
|
||||
|
||||
res = await kernel.invoke(func, base="base")
|
||||
assert str(res) == "test"
|
||||
|
||||
|
||||
def test_parse_list_of_objects(get_custom_type_function_pydantic):
|
||||
func = get_custom_type_function_pydantic
|
||||
|
||||
param_type = list[CustomType]
|
||||
value = [{"id": "1", "name": "John"}, {"id": "2", "name": "Jane"}]
|
||||
result = func._parse_parameter(value, param_type)
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 2
|
||||
assert all(isinstance(item, CustomType) for item in result)
|
||||
|
||||
|
||||
def test_parse_individual_object(get_custom_type_function_pydantic):
|
||||
value = {"id": "2", "name": "Jane"}
|
||||
func = get_custom_type_function_pydantic
|
||||
result = func._parse_parameter(value, CustomType)
|
||||
assert isinstance(result, CustomType)
|
||||
assert result.id == "2"
|
||||
assert result.name == "Jane"
|
||||
|
||||
|
||||
def test_parse_non_list_raises_exception(get_custom_type_function_pydantic):
|
||||
func = get_custom_type_function_pydantic
|
||||
param_type = list[CustomType]
|
||||
value = {"id": "2", "name": "Jane"}
|
||||
with pytest.raises(FunctionExecutionException, match=r"Expected a list for .*"):
|
||||
func._parse_parameter(value, param_type)
|
||||
|
||||
|
||||
def test_parse_invalid_dict_raises_exception(get_custom_type_function_pydantic):
|
||||
func = get_custom_type_function_pydantic
|
||||
value = {"id": "1"}
|
||||
with pytest.raises(FunctionExecutionException, match=r"Parameter is expected to be parsed to .*"):
|
||||
func._parse_parameter(value, CustomType)
|
||||
|
||||
|
||||
def test_parse_invalid_value_raises_exception(get_custom_type_function_pydantic):
|
||||
func = get_custom_type_function_pydantic
|
||||
value = "invalid_value"
|
||||
with pytest.raises(FunctionExecutionException, match=r"Parameter is expected to be parsed to .*"):
|
||||
func._parse_parameter(value, CustomType)
|
||||
|
||||
|
||||
def test_parse_invalid_list_raises_exception(get_custom_type_function_pydantic):
|
||||
func = get_custom_type_function_pydantic
|
||||
param_type = list[CustomType]
|
||||
value = ["invalid_value"]
|
||||
with pytest.raises(FunctionExecutionException, match=r"Parameter is expected to be parsed to .*"):
|
||||
func._parse_parameter(value, param_type)
|
||||
|
||||
|
||||
def test_parse_dict_with_init_non_pydantic(get_custom_type_function_nonpydantic):
|
||||
func = get_custom_type_function_nonpydantic
|
||||
value = {"id": "3", "name": "Alice"}
|
||||
result = func._parse_parameter(value, CustomTypeNonPydantic)
|
||||
assert isinstance(result, CustomTypeNonPydantic)
|
||||
assert result.id == "3"
|
||||
assert result.name == "Alice"
|
||||
|
||||
|
||||
def test_parse_invalid_dict_raises_exception_new(get_custom_type_function_nonpydantic):
|
||||
func = get_custom_type_function_nonpydantic
|
||||
value = {"wrong_key": "3", "name": "Alice"}
|
||||
with pytest.raises(FunctionExecutionException, match=r"Parameter is expected to be parsed to .*"):
|
||||
func._parse_parameter(value, CustomTypeNonPydantic)
|
||||
|
||||
|
||||
def test_gather_function_parameters_exception_handling(get_custom_type_function_pydantic):
|
||||
kernel = Mock(spec=Kernel) # Mock kernel
|
||||
func = get_custom_type_function_pydantic
|
||||
_rebuild_function_invocation_context()
|
||||
context = FunctionInvocationContext(kernel=kernel, function=func, arguments=KernelArguments(param="test"))
|
||||
|
||||
with pytest.raises(FunctionExecutionException, match=r"Parameter param is expected to be parsed to .* but is not."):
|
||||
func.gather_function_parameters(context)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mode"),
|
||||
[
|
||||
("python"),
|
||||
("json"),
|
||||
],
|
||||
)
|
||||
def test_function_model_dump(get_custom_type_function_pydantic, mode):
|
||||
func: KernelFunctionFromMethod = get_custom_type_function_pydantic
|
||||
model_dump = func.model_dump(mode=mode)
|
||||
assert isinstance(model_dump, dict)
|
||||
assert "metadata" in model_dump
|
||||
assert len(model_dump["metadata"]["parameters"]) == 1
|
||||
|
||||
|
||||
def test_function_model_dump_json(get_custom_type_function_pydantic):
|
||||
func = get_custom_type_function_pydantic
|
||||
model_dump = func.model_dump_json()
|
||||
assert isinstance(model_dump, str)
|
||||
assert "metadata" in model_dump
|
||||
@@ -0,0 +1,648 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from copy import deepcopy
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion import OpenAIChatCompletion
|
||||
from semantic_kernel.connectors.ai.open_ai.services.open_ai_text_completion import OpenAITextCompletion
|
||||
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
|
||||
from semantic_kernel.const import METADATA_EXCEPTION_KEY
|
||||
from semantic_kernel.contents import AuthorRole
|
||||
from semantic_kernel.contents.chat_message_content import ChatMessageContent
|
||||
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
|
||||
from semantic_kernel.contents.text_content import TextContent
|
||||
from semantic_kernel.exceptions import FunctionInitializationError
|
||||
from semantic_kernel.filters.functions.function_invocation_context import FunctionInvocationContext
|
||||
from semantic_kernel.filters.kernel_filters_extension import _rebuild_function_invocation_context
|
||||
from semantic_kernel.filters.prompts.prompt_render_context import PromptRenderContext
|
||||
from semantic_kernel.functions.kernel_arguments import KernelArguments
|
||||
from semantic_kernel.functions.kernel_function_from_prompt import KernelFunctionFromPrompt
|
||||
from semantic_kernel.kernel import Kernel
|
||||
from semantic_kernel.prompt_template.input_variable import InputVariable
|
||||
from semantic_kernel.prompt_template.kernel_prompt_template import KernelPromptTemplate
|
||||
from semantic_kernel.prompt_template.prompt_template_config import PromptTemplateConfig
|
||||
|
||||
|
||||
def test_init_minimal_prompt():
|
||||
function = KernelFunctionFromPrompt(
|
||||
function_name="test",
|
||||
plugin_name="test",
|
||||
prompt="test",
|
||||
)
|
||||
|
||||
assert function.name == "test"
|
||||
assert function.plugin_name == "test"
|
||||
assert function.description is None
|
||||
assert function.prompt_template.prompt_template_config.template == "test"
|
||||
|
||||
|
||||
def test_init_minimal_prompt_template():
|
||||
function = KernelFunctionFromPrompt(
|
||||
function_name="test",
|
||||
plugin_name="test",
|
||||
prompt_template=KernelPromptTemplate(prompt_template_config=PromptTemplateConfig(template="test")),
|
||||
)
|
||||
|
||||
assert function.name == "test"
|
||||
assert function.plugin_name == "test"
|
||||
assert function.description is None
|
||||
assert function.prompt_template.prompt_template_config.template == "test"
|
||||
|
||||
|
||||
def test_init_minimal_prompt_template_config():
|
||||
function = KernelFunctionFromPrompt(
|
||||
function_name="test", plugin_name="test", prompt_template_config=PromptTemplateConfig(template="test")
|
||||
)
|
||||
|
||||
assert function.name == "test"
|
||||
assert function.plugin_name == "test"
|
||||
assert function.description is None
|
||||
assert function.prompt_template.prompt_template_config.template == "test"
|
||||
|
||||
|
||||
def test_init_no_prompt():
|
||||
with pytest.raises(FunctionInitializationError):
|
||||
KernelFunctionFromPrompt(
|
||||
function_name="test",
|
||||
plugin_name="test",
|
||||
)
|
||||
|
||||
|
||||
def test_init_invalid_name():
|
||||
with pytest.raises(FunctionInitializationError):
|
||||
KernelFunctionFromPrompt(function_name="test func", plugin_name="test", prompt="test")
|
||||
|
||||
|
||||
def test_init_prompt_execution_settings_none():
|
||||
function = KernelFunctionFromPrompt(
|
||||
function_name="test",
|
||||
plugin_name="test",
|
||||
prompt="test",
|
||||
prompt_execution_settings=None,
|
||||
)
|
||||
|
||||
assert function.name == "test"
|
||||
assert function.plugin_name == "test"
|
||||
assert function.description is None
|
||||
assert function.prompt_template.prompt_template_config.template == "test"
|
||||
|
||||
|
||||
def test_init_prompt_execution_settings_none_with_prompt_template():
|
||||
function = KernelFunctionFromPrompt(
|
||||
function_name="test",
|
||||
plugin_name="test",
|
||||
prompt_template=KernelPromptTemplate(
|
||||
prompt_template_config=PromptTemplateConfig(template="test", execution_settings={})
|
||||
),
|
||||
prompt_execution_settings=None,
|
||||
)
|
||||
|
||||
assert function.name == "test"
|
||||
assert function.plugin_name == "test"
|
||||
assert function.description is None
|
||||
assert function.prompt_template.prompt_template_config.template == "test"
|
||||
|
||||
|
||||
def test_init_prompt_execution_settings():
|
||||
function = KernelFunctionFromPrompt(
|
||||
function_name="test",
|
||||
plugin_name="test",
|
||||
prompt="test",
|
||||
prompt_execution_settings=PromptExecutionSettings(service_id="test"),
|
||||
)
|
||||
|
||||
assert function.name == "test"
|
||||
assert function.plugin_name == "test"
|
||||
assert function.description is None
|
||||
assert function.prompt_template.prompt_template_config.template == "test"
|
||||
|
||||
|
||||
def test_init_prompt_execution_settings_list():
|
||||
function = KernelFunctionFromPrompt(
|
||||
function_name="test",
|
||||
plugin_name="test",
|
||||
prompt="test",
|
||||
prompt_execution_settings=[PromptExecutionSettings(service_id="test")],
|
||||
)
|
||||
|
||||
assert function.name == "test"
|
||||
assert function.plugin_name == "test"
|
||||
assert function.description is None
|
||||
assert function.prompt_template.prompt_template_config.template == "test"
|
||||
|
||||
|
||||
def test_init_prompt_execution_settings_dict():
|
||||
function = KernelFunctionFromPrompt(
|
||||
function_name="test",
|
||||
plugin_name="test",
|
||||
prompt="test",
|
||||
prompt_execution_settings={"test": PromptExecutionSettings(service_id="test")},
|
||||
)
|
||||
|
||||
assert function.name == "test"
|
||||
assert function.plugin_name == "test"
|
||||
assert function.description is None
|
||||
assert function.prompt_template.prompt_template_config.template == "test"
|
||||
|
||||
|
||||
async def test_invoke_chat_stream(openai_unit_test_env):
|
||||
kernel = Kernel()
|
||||
kernel.add_service(OpenAIChatCompletion(service_id="test", ai_model_id="test"))
|
||||
function = KernelFunctionFromPrompt(
|
||||
function_name="test",
|
||||
plugin_name="test",
|
||||
prompt="test",
|
||||
prompt_execution_settings=PromptExecutionSettings(service_id="test"),
|
||||
)
|
||||
|
||||
# This part remains unchanged - for synchronous mocking example
|
||||
with patch(
|
||||
"semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion.OpenAIChatCompletion.get_chat_message_contents"
|
||||
) as mock:
|
||||
mock.return_value = [ChatMessageContent(role=AuthorRole.ASSISTANT, content="test", metadata={})]
|
||||
result = await function.invoke(kernel=kernel)
|
||||
assert str(result) == "test"
|
||||
|
||||
with patch(
|
||||
"semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion.OpenAIChatCompletion.get_streaming_chat_message_contents"
|
||||
) as mock:
|
||||
mock.return_value = [
|
||||
StreamingChatMessageContent(choice_index=0, role=AuthorRole.ASSISTANT, content="test", metadata={})
|
||||
]
|
||||
async for result in function.invoke_stream(kernel=kernel):
|
||||
assert str(result) == "test"
|
||||
|
||||
|
||||
async def test_invoke_exception(openai_unit_test_env):
|
||||
kernel = Kernel()
|
||||
kernel.add_service(OpenAIChatCompletion(service_id="test", ai_model_id="test"))
|
||||
function = KernelFunctionFromPrompt(
|
||||
function_name="test",
|
||||
plugin_name="test",
|
||||
prompt="test",
|
||||
prompt_execution_settings=PromptExecutionSettings(service_id="test"),
|
||||
)
|
||||
with patch(
|
||||
"semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion.OpenAIChatCompletion.get_chat_message_contents",
|
||||
side_effect=Exception,
|
||||
) as mock:
|
||||
mock.return_value = [ChatMessageContent(role=AuthorRole.ASSISTANT, content="test", metadata={})]
|
||||
with pytest.raises(Exception, match="test"):
|
||||
await function.invoke(kernel=kernel)
|
||||
|
||||
with patch(
|
||||
"semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion.OpenAIChatCompletion.get_streaming_chat_message_contents",
|
||||
side_effect=Exception,
|
||||
) as mock:
|
||||
mock.return_value = [
|
||||
StreamingChatMessageContent(choice_index=0, role=AuthorRole.ASSISTANT, content="test", metadata={})
|
||||
]
|
||||
with pytest.raises(Exception):
|
||||
async for result in function.invoke_stream(kernel=kernel):
|
||||
assert isinstance(result.metadata[METADATA_EXCEPTION_KEY], Exception)
|
||||
|
||||
|
||||
async def test_invoke_text(openai_unit_test_env):
|
||||
kernel = Kernel()
|
||||
kernel.add_service(OpenAITextCompletion(service_id="test", ai_model_id="test"))
|
||||
function = KernelFunctionFromPrompt(
|
||||
function_name="test",
|
||||
plugin_name="test",
|
||||
prompt="test",
|
||||
prompt_execution_settings=PromptExecutionSettings(service_id="test"),
|
||||
)
|
||||
with patch(
|
||||
"semantic_kernel.connectors.ai.open_ai.services.open_ai_text_completion.OpenAITextCompletion.get_text_contents",
|
||||
) as mock:
|
||||
mock.return_value = [TextContent(text="test", metadata={})]
|
||||
result = await function.invoke(kernel=kernel)
|
||||
assert str(result) == "test"
|
||||
|
||||
with patch(
|
||||
"semantic_kernel.connectors.ai.open_ai.services.open_ai_text_completion.OpenAITextCompletion.get_streaming_text_contents",
|
||||
) as mock:
|
||||
mock.return_value = [TextContent(text="test", metadata={})]
|
||||
async for result in function.invoke_stream(kernel=kernel):
|
||||
assert str(result) == "test"
|
||||
|
||||
|
||||
async def test_invoke_exception_text(openai_unit_test_env):
|
||||
kernel = Kernel()
|
||||
kernel.add_service(OpenAITextCompletion(service_id="test", ai_model_id="test"))
|
||||
function = KernelFunctionFromPrompt(
|
||||
function_name="test",
|
||||
plugin_name="test",
|
||||
prompt="test",
|
||||
prompt_execution_settings=PromptExecutionSettings(service_id="test"),
|
||||
)
|
||||
with patch(
|
||||
"semantic_kernel.connectors.ai.open_ai.services.open_ai_text_completion.OpenAITextCompletion.get_text_contents",
|
||||
side_effect=Exception,
|
||||
) as mock:
|
||||
mock.return_value = [TextContent(text="test", metadata={})]
|
||||
with pytest.raises(Exception, match="test"):
|
||||
await function.invoke(kernel=kernel)
|
||||
|
||||
with patch(
|
||||
"semantic_kernel.connectors.ai.open_ai.services.open_ai_text_completion.OpenAITextCompletion.get_streaming_text_contents",
|
||||
side_effect=Exception,
|
||||
) as mock:
|
||||
mock.return_value = []
|
||||
with pytest.raises(Exception):
|
||||
async for result in function.invoke_stream(kernel=kernel):
|
||||
assert isinstance(result.metadata[METADATA_EXCEPTION_KEY], Exception)
|
||||
|
||||
|
||||
async def test_invoke_defaults(openai_unit_test_env):
|
||||
kernel = Kernel()
|
||||
kernel.add_service(OpenAIChatCompletion(service_id="test", ai_model_id="test"))
|
||||
function = KernelFunctionFromPrompt(
|
||||
function_name="test",
|
||||
plugin_name="test",
|
||||
prompt_template_config=PromptTemplateConfig(
|
||||
template="{{$input}}",
|
||||
input_variables=[InputVariable(name="input", type="str", default="test", is_required=False)],
|
||||
),
|
||||
prompt_execution_settings=PromptExecutionSettings(service_id="test"),
|
||||
)
|
||||
with patch(
|
||||
"semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion.OpenAIChatCompletion.get_chat_message_contents"
|
||||
) as mock:
|
||||
mock.return_value = [ChatMessageContent(role=AuthorRole.ASSISTANT, content="test", metadata={})]
|
||||
result = await function.invoke(kernel=kernel)
|
||||
assert str(result) == "test"
|
||||
|
||||
|
||||
def test_create_with_multiple_settings():
|
||||
function = KernelFunctionFromPrompt(
|
||||
function_name="test",
|
||||
plugin_name="test",
|
||||
prompt_template_config=PromptTemplateConfig(
|
||||
template="test",
|
||||
execution_settings=[
|
||||
PromptExecutionSettings(service_id="test", temperature=0.0),
|
||||
PromptExecutionSettings(service_id="test2", temperature=1.0),
|
||||
],
|
||||
),
|
||||
)
|
||||
assert (
|
||||
function.prompt_template.prompt_template_config.execution_settings["test"].extension_data["temperature"] == 0.0
|
||||
)
|
||||
assert (
|
||||
function.prompt_template.prompt_template_config.execution_settings["test2"].extension_data["temperature"] == 1.0
|
||||
)
|
||||
|
||||
|
||||
async def test_create_with_multiple_settings_one_service_registered(openai_unit_test_env):
|
||||
kernel = Kernel()
|
||||
kernel.add_service(OpenAIChatCompletion(service_id="test2", ai_model_id="test"))
|
||||
function = KernelFunctionFromPrompt(
|
||||
function_name="test",
|
||||
plugin_name="test",
|
||||
prompt_template_config=PromptTemplateConfig(
|
||||
template="test",
|
||||
execution_settings=[
|
||||
PromptExecutionSettings(service_id="test", temperature=0.0),
|
||||
PromptExecutionSettings(service_id="test2", temperature=1.0),
|
||||
],
|
||||
),
|
||||
)
|
||||
with patch(
|
||||
"semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion.OpenAIChatCompletion.get_chat_message_contents"
|
||||
) as mock:
|
||||
mock.return_value = [ChatMessageContent(role=AuthorRole.ASSISTANT, content="test", metadata={})]
|
||||
result = await function.invoke(kernel=kernel)
|
||||
assert str(result) == "test"
|
||||
|
||||
|
||||
def test_from_yaml_fail():
|
||||
with pytest.raises(FunctionInitializationError):
|
||||
KernelFunctionFromPrompt.from_yaml("template_format: something_else")
|
||||
|
||||
|
||||
def test_from_directory_prompt_only():
|
||||
with pytest.raises(FunctionInitializationError):
|
||||
KernelFunctionFromPrompt.from_directory(
|
||||
path=os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
"../../assets",
|
||||
"test_plugins",
|
||||
"TestPlugin",
|
||||
"TestFunctionPromptOnly",
|
||||
),
|
||||
plugin_name="test",
|
||||
)
|
||||
|
||||
|
||||
def test_from_directory_config_only():
|
||||
with pytest.raises(FunctionInitializationError):
|
||||
KernelFunctionFromPrompt.from_directory(
|
||||
path=os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
"../../assets",
|
||||
"test_plugins",
|
||||
"TestPlugin",
|
||||
"TestFunctionConfigOnly",
|
||||
),
|
||||
plugin_name="test",
|
||||
)
|
||||
|
||||
|
||||
async def test_prompt_render(kernel: Kernel, openai_unit_test_env):
|
||||
kernel.add_service(OpenAIChatCompletion(service_id="default", ai_model_id="test"))
|
||||
function = KernelFunctionFromPrompt(
|
||||
function_name="test",
|
||||
plugin_name="test",
|
||||
prompt="test",
|
||||
template_format="semantic-kernel",
|
||||
)
|
||||
_rebuild_function_invocation_context()
|
||||
context = FunctionInvocationContext(function=function, kernel=kernel, arguments=KernelArguments())
|
||||
prompt_render_result = await function._render_prompt(context)
|
||||
assert prompt_render_result.rendered_prompt == "test"
|
||||
|
||||
|
||||
async def test_prompt_render_with_filter(kernel: Kernel, openai_unit_test_env):
|
||||
kernel.add_service(OpenAIChatCompletion(service_id="default", ai_model_id="test"))
|
||||
|
||||
@kernel.filter("prompt_rendering")
|
||||
async def prompt_rendering_filter(context: PromptRenderContext, next):
|
||||
await next(context)
|
||||
context.rendered_prompt = f"preface {context.rendered_prompt or ''}"
|
||||
|
||||
function = KernelFunctionFromPrompt(
|
||||
function_name="test",
|
||||
plugin_name="test",
|
||||
prompt="test",
|
||||
template_format="semantic-kernel",
|
||||
)
|
||||
_rebuild_function_invocation_context()
|
||||
context = FunctionInvocationContext(function=function, kernel=kernel, arguments=KernelArguments())
|
||||
prompt_render_result = await function._render_prompt(context)
|
||||
assert prompt_render_result.rendered_prompt == "preface test"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mode"),
|
||||
[
|
||||
("python"),
|
||||
("json"),
|
||||
],
|
||||
)
|
||||
def test_function_model_dump(mode: str):
|
||||
function = KernelFunctionFromPrompt(
|
||||
function_name="test",
|
||||
plugin_name="test",
|
||||
prompt="test",
|
||||
template_format="semantic-kernel",
|
||||
prompt_template_config=PromptTemplateConfig(
|
||||
template="test",
|
||||
input_variables=[InputVariable(name="input", type="str", default="test", is_required=False)],
|
||||
),
|
||||
)
|
||||
model_dump = function.model_dump(mode=mode)
|
||||
assert isinstance(model_dump, dict)
|
||||
assert "metadata" in model_dump
|
||||
assert len(model_dump["metadata"]["parameters"]) == 1
|
||||
|
||||
|
||||
def test_function_model_dump_json():
|
||||
function = KernelFunctionFromPrompt(
|
||||
function_name="test",
|
||||
plugin_name="test",
|
||||
prompt="test",
|
||||
template_format="semantic-kernel",
|
||||
)
|
||||
model_dump_json = function.model_dump_json()
|
||||
assert isinstance(model_dump_json, str)
|
||||
assert "test" in model_dump_json
|
||||
|
||||
|
||||
def test_from_directory_utf8_encoding_default():
|
||||
"""Test loading plugin with default UTF-8 encoding."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
prompt_path = os.path.join(temp_dir, "skprompt.txt")
|
||||
config_path = os.path.join(temp_dir, "config.json")
|
||||
|
||||
# UTF-8 content with international characters
|
||||
prompt_content = """Hello! I can help with questions in multiple languages:
|
||||
English: Hello world!
|
||||
Spanish: ¡Hola mundo!
|
||||
Chinese: 你好世界!
|
||||
Japanese: こんにちは世界!
|
||||
|
||||
Question: {{$input}}
|
||||
"""
|
||||
|
||||
config_content = """{
|
||||
"schema": 1,
|
||||
"description": "A multilingual assistant function",
|
||||
"input_variables": [
|
||||
{
|
||||
"name": "input",
|
||||
"description": "User's question",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
}"""
|
||||
|
||||
# Write files with UTF-8 encoding
|
||||
with open(prompt_path, "w", encoding="utf-8") as f:
|
||||
f.write(prompt_content)
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
f.write(config_content)
|
||||
|
||||
# Test default behavior (should use UTF-8)
|
||||
function = KernelFunctionFromPrompt.from_directory(temp_dir)
|
||||
assert function.name == os.path.basename(temp_dir)
|
||||
assert function.description == "A multilingual assistant function"
|
||||
assert "你好世界" in function.prompt_template.prompt_template_config.template
|
||||
assert "こんにちは世界" in function.prompt_template.prompt_template_config.template
|
||||
|
||||
|
||||
def test_from_directory_explicit_utf8_encoding():
|
||||
"""Test loading plugin with explicit UTF-8 encoding."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
prompt_path = os.path.join(temp_dir, "skprompt.txt")
|
||||
config_path = os.path.join(temp_dir, "config.json")
|
||||
|
||||
prompt_content = "Hello with UTF-8 characters: ñáéíóú {{$input}}"
|
||||
config_content = '{"schema": 1, "description": "Test with UTF-8 characters"}'
|
||||
|
||||
with open(prompt_path, "w", encoding="utf-8") as f:
|
||||
f.write(prompt_content)
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
f.write(config_content)
|
||||
|
||||
# Test explicit UTF-8 encoding
|
||||
function = KernelFunctionFromPrompt.from_directory(temp_dir, encoding="utf-8")
|
||||
assert function.description == "Test with UTF-8 characters"
|
||||
assert "ñáéíóú" in function.prompt_template.prompt_template_config.template
|
||||
|
||||
|
||||
def test_from_directory_latin1_encoding():
|
||||
"""Test loading plugin with Latin-1 encoding."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
prompt_path = os.path.join(temp_dir, "skprompt.txt")
|
||||
config_path = os.path.join(temp_dir, "config.json")
|
||||
|
||||
# Content with Latin-1 characters (Western European)
|
||||
prompt_content = """Assistant for Western European languages:
|
||||
French: café, naïve, résumé
|
||||
German: Müller, Größe, weiß
|
||||
Spanish: niño, señora, años
|
||||
|
||||
Question: {{$input}}
|
||||
"""
|
||||
|
||||
config_content = """{
|
||||
"schema": 1,
|
||||
"description": "Western European language assistant",
|
||||
"input_variables": [
|
||||
{
|
||||
"name": "input",
|
||||
"description": "User's question",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
}"""
|
||||
|
||||
# Write files with Latin-1 encoding
|
||||
with open(prompt_path, "w", encoding="latin-1") as f:
|
||||
f.write(prompt_content)
|
||||
with open(config_path, "w", encoding="latin-1") as f:
|
||||
f.write(config_content)
|
||||
|
||||
# Load with Latin-1 encoding
|
||||
function = KernelFunctionFromPrompt.from_directory(temp_dir, encoding="latin-1")
|
||||
assert function.description == "Western European language assistant"
|
||||
assert "café" in function.prompt_template.prompt_template_config.template
|
||||
assert "Müller" in function.prompt_template.prompt_template_config.template
|
||||
assert "niño" in function.prompt_template.prompt_template_config.template
|
||||
|
||||
|
||||
def test_from_directory_cp1252_encoding():
|
||||
"""Test loading plugin with Windows-1252 encoding."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
prompt_path = os.path.join(temp_dir, "skprompt.txt")
|
||||
config_path = os.path.join(temp_dir, "config.json")
|
||||
|
||||
# Content with Windows-1252 specific characters
|
||||
prompt_content = """Windows text processing assistant:
|
||||
Smart quotes: "Hello" and 'world'
|
||||
Em dash: Yes—absolutely!
|
||||
Ellipsis: Wait…
|
||||
|
||||
Question: {{$input}}
|
||||
"""
|
||||
|
||||
config_content = """{
|
||||
"schema": 1,
|
||||
"description": "Windows text processing assistant",
|
||||
"input_variables": [
|
||||
{
|
||||
"name": "input",
|
||||
"description": "User's question about text processing",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
}"""
|
||||
|
||||
# Write files with Windows-1252 encoding
|
||||
with open(prompt_path, "w", encoding="cp1252") as f:
|
||||
f.write(prompt_content)
|
||||
with open(config_path, "w", encoding="cp1252") as f:
|
||||
f.write(config_content)
|
||||
|
||||
# Load with Windows-1252 encoding
|
||||
function = KernelFunctionFromPrompt.from_directory(temp_dir, encoding="cp1252")
|
||||
assert function.description == "Windows text processing assistant"
|
||||
assert '"Hello"' in function.prompt_template.prompt_template_config.template
|
||||
assert "Yes—absolutely" in function.prompt_template.prompt_template_config.template
|
||||
assert "Wait…" in function.prompt_template.prompt_template_config.template
|
||||
|
||||
|
||||
def test_from_directory_with_plugin_name_and_encoding():
|
||||
"""Test loading plugin with both plugin name and encoding specified."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
prompt_path = os.path.join(temp_dir, "skprompt.txt")
|
||||
config_path = os.path.join(temp_dir, "config.json")
|
||||
|
||||
prompt_content = "Simple assistant: {{$input}}"
|
||||
config_content = '{"schema": 1, "description": "Simple assistant"}'
|
||||
|
||||
with open(prompt_path, "w", encoding="utf-8") as f:
|
||||
f.write(prompt_content)
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
f.write(config_content)
|
||||
|
||||
# Load with both plugin name and encoding specified
|
||||
function = KernelFunctionFromPrompt.from_directory(
|
||||
path=temp_dir, plugin_name="MyCustomPlugin", encoding="utf-8"
|
||||
)
|
||||
assert function.metadata.plugin_name == "MyCustomPlugin"
|
||||
assert function.description == "Simple assistant"
|
||||
assert function.prompt_template.prompt_template_config.template == "Simple assistant: {{$input}}"
|
||||
|
||||
|
||||
def test_from_directory_encoding_error_handling():
|
||||
"""Test that incorrect encoding raises appropriate error."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
prompt_path = os.path.join(temp_dir, "skprompt.txt")
|
||||
config_path = os.path.join(temp_dir, "config.json")
|
||||
|
||||
# Write UTF-8 content
|
||||
prompt_content = "Hello with UTF-8: 你好世界 {{$input}}"
|
||||
config_content = '{"schema": 1, "description": "UTF-8 content"}'
|
||||
|
||||
with open(prompt_path, "w", encoding="utf-8") as f:
|
||||
f.write(prompt_content)
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
f.write(config_content)
|
||||
|
||||
# Try to read with ASCII encoding - should fail
|
||||
with pytest.raises(UnicodeDecodeError):
|
||||
KernelFunctionFromPrompt.from_directory(temp_dir, encoding="ascii")
|
||||
|
||||
|
||||
def test_from_directory_backward_compatibility():
|
||||
"""Test that existing code without encoding parameter still works."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
prompt_path = os.path.join(temp_dir, "skprompt.txt")
|
||||
config_path = os.path.join(temp_dir, "config.json")
|
||||
|
||||
prompt_content = "Basic ASCII content: {{$input}}"
|
||||
config_content = '{"schema": 1, "description": "Basic function"}'
|
||||
|
||||
with open(prompt_path, "w", encoding="utf-8") as f:
|
||||
f.write(prompt_content)
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
f.write(config_content)
|
||||
|
||||
# Test that old calling style still works
|
||||
function = KernelFunctionFromPrompt.from_directory(temp_dir)
|
||||
assert function.description == "Basic function"
|
||||
assert function.prompt_template.prompt_template_config.template == "Basic ASCII content: {{$input}}"
|
||||
|
||||
|
||||
def test_kernel_function_from_prompt_deepcopy():
|
||||
"""Test deepcopying a KernelFunctionFromPrompt."""
|
||||
function = KernelFunctionFromPrompt(
|
||||
function_name="test_function",
|
||||
plugin_name="test_plugin",
|
||||
prompt="Hello, world!",
|
||||
description="A test function.",
|
||||
)
|
||||
copied_function = deepcopy(function)
|
||||
assert copied_function is not function
|
||||
assert copied_function.name == function.name
|
||||
assert copied_function.plugin_name == function.plugin_name
|
||||
assert copied_function.description == function.description
|
||||
assert copied_function.prompt_template.prompt_template_config.template == (
|
||||
function.prompt_template.prompt_template_config.template
|
||||
)
|
||||
assert copied_function.prompt_template is not function.prompt_template
|
||||
@@ -0,0 +1,91 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
import pytest
|
||||
|
||||
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata
|
||||
from semantic_kernel.functions.kernel_parameter_metadata import KernelParameterMetadata
|
||||
|
||||
|
||||
def test_kernel_function_metadata():
|
||||
function_metadata = KernelFunctionMetadata(
|
||||
name="function1",
|
||||
plugin_name="plugin1",
|
||||
description="Semantic function",
|
||||
parameters=[],
|
||||
is_prompt=True,
|
||||
is_asynchronous=True,
|
||||
)
|
||||
assert function_metadata.is_prompt
|
||||
|
||||
|
||||
def test_kernel_function_metadata_defaults():
|
||||
function_metadata = KernelFunctionMetadata(
|
||||
name="function1",
|
||||
plugin_name="plugin1",
|
||||
description="Semantic function",
|
||||
is_prompt=True,
|
||||
)
|
||||
assert function_metadata.parameters == []
|
||||
assert function_metadata.is_asynchronous
|
||||
|
||||
|
||||
def test_kernel_function_metadata_name_pattern_error():
|
||||
with pytest.raises(ValueError):
|
||||
KernelFunctionMetadata(
|
||||
name="*",
|
||||
plugin_name="plugin1",
|
||||
description="Semantic function",
|
||||
is_prompt=True,
|
||||
)
|
||||
|
||||
|
||||
def test_kernel_function_metadata_name_empty_error():
|
||||
with pytest.raises(ValueError):
|
||||
KernelFunctionMetadata(
|
||||
name="",
|
||||
plugin_name="plugin1",
|
||||
description="Semantic function",
|
||||
is_prompt=True,
|
||||
)
|
||||
|
||||
|
||||
def test_kernel_function_equals():
|
||||
function_metadata_1 = KernelFunctionMetadata(
|
||||
name="function1",
|
||||
plugin_name="plugin1",
|
||||
description="Semantic function",
|
||||
is_prompt=True,
|
||||
)
|
||||
function_metadata_2 = KernelFunctionMetadata(
|
||||
name="function1",
|
||||
plugin_name="plugin1",
|
||||
description="Semantic function",
|
||||
is_prompt=True,
|
||||
)
|
||||
assert function_metadata_1 == function_metadata_2
|
||||
|
||||
|
||||
def test_kernel_function_not_equals():
|
||||
function_metadata_1 = KernelFunctionMetadata(
|
||||
name="function1",
|
||||
plugin_name="plugin1",
|
||||
description="Semantic function",
|
||||
is_prompt=True,
|
||||
)
|
||||
function_metadata_2 = KernelFunctionMetadata(
|
||||
name="function2",
|
||||
plugin_name="plugin1",
|
||||
description="Semantic function",
|
||||
is_prompt=True,
|
||||
)
|
||||
assert function_metadata_1 != function_metadata_2
|
||||
|
||||
|
||||
def test_kernel_function_not_equals_other_object():
|
||||
function_metadata_1 = KernelFunctionMetadata(
|
||||
name="function1",
|
||||
plugin_name="plugin1",
|
||||
description="Semantic function",
|
||||
is_prompt=True,
|
||||
)
|
||||
function_metadata_2 = KernelParameterMetadata(name="function2", description="Semantic function", default_value="")
|
||||
assert function_metadata_1 != function_metadata_2
|
||||
@@ -0,0 +1,67 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from semantic_kernel.functions.kernel_parameter_metadata import KernelParameterMetadata
|
||||
|
||||
|
||||
def test_kernel_parameter_metadata_init():
|
||||
metadata = KernelParameterMetadata(
|
||||
name="test",
|
||||
description="description",
|
||||
is_required=True,
|
||||
type="str",
|
||||
default_value="default",
|
||||
)
|
||||
|
||||
assert metadata.name == "test"
|
||||
assert metadata.description == "description"
|
||||
assert metadata.is_required is True
|
||||
assert metadata.default_value == "default"
|
||||
|
||||
|
||||
def test_kernel_parameter_metadata_valid():
|
||||
metadata = KernelParameterMetadata(
|
||||
name="param1",
|
||||
description="A test parameter",
|
||||
default_value="default",
|
||||
type_="str",
|
||||
is_required=True,
|
||||
type_object=str,
|
||||
)
|
||||
assert metadata.name == "param1"
|
||||
assert metadata.description == "A test parameter"
|
||||
assert metadata.default_value == "default"
|
||||
assert metadata.type_ == "str"
|
||||
assert metadata.is_required is True
|
||||
assert metadata.type_object is str
|
||||
assert metadata.schema_data == {"type": "string", "description": "A test parameter"}
|
||||
|
||||
|
||||
def test_kernel_parameter_metadata_invalid_name():
|
||||
with pytest.raises(ValidationError):
|
||||
KernelParameterMetadata(
|
||||
name="invalid name!", description="A test parameter", default_value="default", type_="str"
|
||||
)
|
||||
|
||||
|
||||
def test_kernel_parameter_metadata_infer_schema_with_type_object():
|
||||
metadata = KernelParameterMetadata(name="param2", type_object=int, description="An integer parameter")
|
||||
assert metadata.schema_data == {"type": "integer", "description": "An integer parameter"}
|
||||
|
||||
|
||||
def test_kernel_parameter_metadata_infer_schema_with_type_name():
|
||||
metadata = KernelParameterMetadata(name="param3", type_="int", default_value=42, description="An integer parameter")
|
||||
assert metadata.schema_data == {"type": "integer", "description": "An integer parameter (default value: 42)"}
|
||||
|
||||
|
||||
def test_kernel_parameter_metadata_without_schema_data():
|
||||
metadata = KernelParameterMetadata(name="param4", type_="bool")
|
||||
assert metadata.schema_data == {"type": "boolean"}
|
||||
|
||||
|
||||
def test_kernel_parameter_metadata_with_partial_data():
|
||||
metadata = KernelParameterMetadata(name="param5", type_="float", default_value=3.14)
|
||||
assert metadata.schema_data == {"type": "number", "description": "(default value: 3.14)"}
|
||||
@@ -0,0 +1,538 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import os
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from pytest import raises
|
||||
|
||||
from semantic_kernel.connectors.ai import PromptExecutionSettings
|
||||
from semantic_kernel.connectors.openapi_plugin.openapi_parser import OpenApiParser
|
||||
from semantic_kernel.exceptions.function_exceptions import PluginInitializationError
|
||||
from semantic_kernel.functions import kernel_function
|
||||
from semantic_kernel.functions.kernel_function import KernelFunction
|
||||
from semantic_kernel.functions.kernel_function_from_method import KernelFunctionFromMethod
|
||||
from semantic_kernel.functions.kernel_function_from_prompt import KernelFunctionFromPrompt
|
||||
from semantic_kernel.functions.kernel_plugin import KernelPlugin
|
||||
from semantic_kernel.prompt_template.input_variable import InputVariable
|
||||
from semantic_kernel.prompt_template.prompt_template_config import PromptTemplateConfig
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_function() -> Callable[..., Any]:
|
||||
@kernel_function
|
||||
def mock_function(input: str) -> None:
|
||||
pass
|
||||
|
||||
return mock_function
|
||||
|
||||
|
||||
# region Init
|
||||
|
||||
|
||||
def test_init_fail_no_name():
|
||||
with raises(TypeError):
|
||||
KernelPlugin(description="A unit test plugin")
|
||||
|
||||
|
||||
def test_init_with_no_functions():
|
||||
expected_plugin_name = "test_plugin"
|
||||
expected_plugin_description = "A unit test plugin"
|
||||
plugin = KernelPlugin(name=expected_plugin_name, description=expected_plugin_description)
|
||||
assert plugin.name == expected_plugin_name
|
||||
assert plugin.description == expected_plugin_description
|
||||
assert not plugin.functions
|
||||
|
||||
|
||||
def test_init_with_kernel_functions(mock_function):
|
||||
function_plugin_name = "MockPlugin"
|
||||
expected_plugin_name = "test_plugin"
|
||||
expected_plugin_description = "A unit test plugin"
|
||||
|
||||
native_function = KernelFunction.from_method(method=mock_function, plugin_name=function_plugin_name)
|
||||
|
||||
plugin = KernelPlugin(name=expected_plugin_name, description=expected_plugin_description, functions=native_function)
|
||||
assert plugin.name == expected_plugin_name
|
||||
assert plugin.description == expected_plugin_description
|
||||
assert len(plugin.functions) == 1
|
||||
assert plugin["mock_function"].plugin_name == expected_plugin_name
|
||||
assert native_function.plugin_name == function_plugin_name
|
||||
|
||||
|
||||
def test_init_with_kernel_functions_list(mock_function):
|
||||
function_plugin_name = "MockPlugin"
|
||||
expected_plugin_name = "test_plugin"
|
||||
expected_plugin_description = "A unit test plugin"
|
||||
|
||||
native_function = KernelFunction.from_method(method=mock_function, plugin_name=function_plugin_name)
|
||||
|
||||
plugin = KernelPlugin(
|
||||
name=expected_plugin_name, description=expected_plugin_description, functions=[native_function]
|
||||
)
|
||||
assert plugin.name == expected_plugin_name
|
||||
assert plugin.description == expected_plugin_description
|
||||
assert len(plugin.functions) == 1
|
||||
assert plugin["mock_function"].plugin_name == expected_plugin_name
|
||||
assert native_function.plugin_name == function_plugin_name
|
||||
|
||||
|
||||
def test_init_with_list_other_fail():
|
||||
with raises(ValueError):
|
||||
KernelPlugin(name="test_plugin", description="A unit test plugin", functions=["str"])
|
||||
|
||||
|
||||
def test_init_with_other_fail():
|
||||
with raises(ValueError):
|
||||
KernelPlugin(name="test_plugin", description="A unit test plugin", functions="str")
|
||||
|
||||
|
||||
def test_init_with_kernel_functions_dict(mock_function):
|
||||
function_plugin_name = "MockPlugin"
|
||||
expected_plugin_name = "test_plugin"
|
||||
expected_plugin_description = "A unit test plugin"
|
||||
|
||||
native_function = KernelFunction.from_method(method=mock_function, plugin_name=function_plugin_name)
|
||||
|
||||
plugin = KernelPlugin(
|
||||
name=expected_plugin_name,
|
||||
description=expected_plugin_description,
|
||||
functions={native_function.name: native_function},
|
||||
)
|
||||
assert plugin.name == expected_plugin_name
|
||||
assert plugin.description == expected_plugin_description
|
||||
assert len(plugin.functions) == 1
|
||||
assert plugin["mock_function"].plugin_name == expected_plugin_name
|
||||
assert native_function.plugin_name == function_plugin_name
|
||||
|
||||
|
||||
def test_init_with_callable_functions(mock_function):
|
||||
expected_plugin_name = "test_plugin"
|
||||
expected_plugin_description = "A unit test plugin"
|
||||
|
||||
plugin = KernelPlugin(name=expected_plugin_name, description=expected_plugin_description, functions=mock_function)
|
||||
assert plugin.name == expected_plugin_name
|
||||
assert plugin.description == expected_plugin_description
|
||||
assert len(plugin.functions) == 1
|
||||
assert plugin["mock_function"].plugin_name == expected_plugin_name
|
||||
|
||||
|
||||
def test_init_with_callable_functions_list(mock_function):
|
||||
expected_plugin_name = "test_plugin"
|
||||
expected_plugin_description = "A unit test plugin"
|
||||
|
||||
plugin = KernelPlugin(name=expected_plugin_name, description=expected_plugin_description, functions=[mock_function])
|
||||
assert plugin.name == expected_plugin_name
|
||||
assert plugin.description == expected_plugin_description
|
||||
assert len(plugin.functions) == 1
|
||||
assert plugin["mock_function"].plugin_name == expected_plugin_name
|
||||
|
||||
|
||||
def test_init_with_kernel_plugin(mock_function):
|
||||
function_plugin_name = "MockPlugin"
|
||||
expected_plugin_name = "test_plugin"
|
||||
expected_plugin_description = "A unit test plugin"
|
||||
|
||||
native_function = KernelFunction.from_method(method=mock_function, plugin_name=function_plugin_name)
|
||||
first_plugin = KernelPlugin(
|
||||
name=expected_plugin_name, description=expected_plugin_description, functions=native_function
|
||||
)
|
||||
plugin = KernelPlugin(name=expected_plugin_name, description=expected_plugin_description, functions=first_plugin)
|
||||
assert plugin.name == expected_plugin_name
|
||||
assert plugin.description == expected_plugin_description
|
||||
assert len(plugin.functions) == 1
|
||||
assert plugin["mock_function"].plugin_name == expected_plugin_name
|
||||
assert native_function.plugin_name == function_plugin_name
|
||||
|
||||
|
||||
def test_init_with_kernel_plugin_list(mock_function):
|
||||
function_plugin_name = "MockPlugin"
|
||||
expected_plugin_name = "test_plugin"
|
||||
expected_plugin_description = "A unit test plugin"
|
||||
|
||||
native_function = KernelFunction.from_method(method=mock_function, plugin_name=function_plugin_name)
|
||||
first_plugin = KernelPlugin(
|
||||
name=expected_plugin_name, description=expected_plugin_description, functions=native_function
|
||||
)
|
||||
plugin = KernelPlugin(name=expected_plugin_name, description=expected_plugin_description, functions=[first_plugin])
|
||||
assert plugin.name == expected_plugin_name
|
||||
assert plugin.description == expected_plugin_description
|
||||
assert len(plugin.functions) == 1
|
||||
assert plugin["mock_function"].plugin_name == expected_plugin_name
|
||||
assert native_function.plugin_name == function_plugin_name
|
||||
|
||||
|
||||
def test_init_exposes_the_native_function_it_contains(mock_function):
|
||||
expected_plugin_name = "test_plugin"
|
||||
expected_plugin_description = "A unit test plugin"
|
||||
|
||||
native_function = KernelFunction.from_method(method=mock_function, plugin_name="MockPlugin")
|
||||
|
||||
plugin = KernelPlugin(
|
||||
name=expected_plugin_name, description=expected_plugin_description, functions=[native_function]
|
||||
)
|
||||
assert plugin.name == expected_plugin_name
|
||||
assert plugin.description == expected_plugin_description
|
||||
assert len(plugin.functions) == 1
|
||||
assert plugin["mock_function"].name == native_function.name
|
||||
|
||||
|
||||
def test_init_with_prompt_function():
|
||||
req_settings = PromptExecutionSettings(extension_data={"max_tokens": 2000, "temperature": 0.7, "top_p": 0.8})
|
||||
|
||||
prompt = "Use this input: {{$request}}"
|
||||
|
||||
prompt_template_config = PromptTemplateConfig(
|
||||
template=prompt,
|
||||
name="chat",
|
||||
template_format="semantic-kernel",
|
||||
input_variables=[
|
||||
InputVariable(name="request", description="The user input", is_required=True),
|
||||
],
|
||||
execution_settings={"default": req_settings},
|
||||
)
|
||||
|
||||
expected_plugin_name = "test_plugin"
|
||||
expected_function_name = "mock_function"
|
||||
semantic_function = KernelFunctionFromPrompt(
|
||||
prompt=prompt,
|
||||
prompt_template_config=prompt_template_config,
|
||||
plugin_name=expected_plugin_name,
|
||||
function_name=expected_function_name,
|
||||
)
|
||||
|
||||
expected_plugin_description = "A unit test plugin"
|
||||
|
||||
plugin = KernelPlugin(
|
||||
name=expected_plugin_name, description=expected_plugin_description, functions=[semantic_function]
|
||||
)
|
||||
|
||||
assert plugin.name == expected_plugin_name
|
||||
assert plugin.description == expected_plugin_description
|
||||
assert len(plugin.functions) == 1
|
||||
assert plugin["mock_function"] == semantic_function
|
||||
|
||||
|
||||
def test_init_with_both_function_types(mock_function):
|
||||
req_settings = PromptExecutionSettings(extension_data={"max_tokens": 2000, "temperature": 0.7, "top_p": 0.8})
|
||||
|
||||
prompt = "Use this input: {{$request}}"
|
||||
|
||||
prompt_template_config = PromptTemplateConfig(
|
||||
template=prompt,
|
||||
name="chat",
|
||||
template_format="semantic-kernel",
|
||||
input_variables=[
|
||||
InputVariable(name="request", description="The user input", is_required=True),
|
||||
],
|
||||
execution_settings={"default": req_settings},
|
||||
)
|
||||
|
||||
expected_plugin_name = "test_plugin"
|
||||
expected_function_name = "prompt_function"
|
||||
semantic_function = KernelFunction.from_prompt(
|
||||
prompt=prompt,
|
||||
prompt_template_config=prompt_template_config,
|
||||
plugin_name=expected_plugin_name,
|
||||
function_name=expected_function_name,
|
||||
)
|
||||
|
||||
native_function = KernelFunctionFromMethod(method=mock_function, plugin_name="MockPlugin")
|
||||
|
||||
# Add both types to the default kernel plugin
|
||||
expected_plugin_description = "A unit test plugin"
|
||||
|
||||
plugin = KernelPlugin(
|
||||
name=expected_plugin_name,
|
||||
description=expected_plugin_description,
|
||||
functions=[semantic_function, native_function],
|
||||
)
|
||||
|
||||
assert plugin.name == expected_plugin_name
|
||||
assert plugin.description == expected_plugin_description
|
||||
assert len(plugin.functions) == 2
|
||||
|
||||
for func in [semantic_function, native_function]:
|
||||
assert func.name in plugin
|
||||
assert plugin[func.name].name == func.name
|
||||
|
||||
|
||||
def test_init_with_same_function_names(mock_function):
|
||||
req_settings = PromptExecutionSettings(extension_data={"max_tokens": 2000, "temperature": 0.7, "top_p": 0.8})
|
||||
|
||||
prompt = "Use this input: {{$request}}"
|
||||
|
||||
prompt_template_config = PromptTemplateConfig(
|
||||
template=prompt,
|
||||
name="chat",
|
||||
template_format="semantic-kernel",
|
||||
input_variables=[
|
||||
InputVariable(name="request", description="The user input", is_required=True),
|
||||
],
|
||||
execution_settings={"default": req_settings},
|
||||
)
|
||||
|
||||
expected_plugin_name = "test_plugin"
|
||||
expected_function_name = "mock_function"
|
||||
semantic_function = KernelFunction.from_prompt(
|
||||
prompt=prompt,
|
||||
prompt_template_config=prompt_template_config,
|
||||
plugin_name=expected_plugin_name,
|
||||
function_name=expected_function_name,
|
||||
)
|
||||
|
||||
native_function = KernelFunctionFromMethod(method=mock_function, plugin_name="MockPlugin")
|
||||
|
||||
plugin = KernelPlugin(name=expected_plugin_name, functions=[semantic_function, native_function])
|
||||
assert len(plugin.functions) == 1
|
||||
|
||||
|
||||
# region Dict-like methods
|
||||
|
||||
|
||||
def test_set_item(mock_function):
|
||||
expected_plugin_name = "test_plugin"
|
||||
expected_plugin_description = "A unit test plugin"
|
||||
|
||||
native_function = KernelFunction.from_method(method=mock_function, plugin_name=expected_plugin_name)
|
||||
|
||||
plugin = KernelPlugin(name=expected_plugin_name, description=expected_plugin_description)
|
||||
plugin["mock_function"] = native_function
|
||||
|
||||
assert plugin.name == expected_plugin_name
|
||||
assert plugin.description == expected_plugin_description
|
||||
assert len(plugin.functions) == 1
|
||||
assert plugin["mock_function"].metadata == native_function.metadata
|
||||
|
||||
function = plugin["mock_function"]
|
||||
assert function.name == "mock_function"
|
||||
|
||||
|
||||
def test_set(mock_function):
|
||||
expected_plugin_name = "test_plugin"
|
||||
expected_plugin_description = "A unit test plugin"
|
||||
|
||||
native_function = KernelFunction.from_method(method=mock_function, plugin_name=expected_plugin_name)
|
||||
|
||||
plugin = KernelPlugin(name=expected_plugin_name, description=expected_plugin_description)
|
||||
plugin.set("mock_function", native_function)
|
||||
|
||||
assert plugin.name == expected_plugin_name
|
||||
assert plugin.description == expected_plugin_description
|
||||
assert len(plugin.functions) == 1
|
||||
assert plugin["mock_function"].metadata == native_function.metadata
|
||||
|
||||
function = plugin.get("mock_function", None)
|
||||
assert function.name == "mock_function"
|
||||
function2 = plugin.get("mock_function2", None)
|
||||
assert function2 is None
|
||||
|
||||
|
||||
def test_set_default(mock_function):
|
||||
expected_plugin_name = "test_plugin"
|
||||
expected_plugin_description = "A unit test plugin"
|
||||
|
||||
native_function = KernelFunction.from_method(method=mock_function, plugin_name=expected_plugin_name)
|
||||
native_function2 = KernelFunction.from_method(method=mock_function, plugin_name="other")
|
||||
|
||||
plugin = KernelPlugin(name=expected_plugin_name, description=expected_plugin_description)
|
||||
native_function == plugin.setdefault("mock_function", native_function)
|
||||
native_function == plugin.setdefault("mock_function", native_function2)
|
||||
|
||||
assert len(plugin.functions) == 1
|
||||
|
||||
with raises(ValueError):
|
||||
plugin.setdefault("mock_function2", None)
|
||||
|
||||
|
||||
def test_update(mock_function):
|
||||
expected_plugin_name = "test_plugin"
|
||||
expected_plugin_description = "A unit test plugin"
|
||||
|
||||
native_function = KernelFunction.from_method(method=mock_function, plugin_name=expected_plugin_name)
|
||||
native_function2 = KernelFunction.from_method(method=mock_function, plugin_name="p2")
|
||||
native_function3 = KernelFunction.from_method(method=mock_function, plugin_name="p3")
|
||||
|
||||
plugin = KernelPlugin(name=expected_plugin_name, description=expected_plugin_description)
|
||||
plugin.update(mock_function=native_function)
|
||||
assert len(plugin.functions) == 1
|
||||
|
||||
plugin.update([native_function2])
|
||||
assert len(plugin.functions) == 1
|
||||
|
||||
plugin.update({"mock_function": native_function3})
|
||||
assert len(plugin.functions) == 1
|
||||
|
||||
plugin2 = KernelPlugin(name="p2", description="p2")
|
||||
plugin2.update(plugin)
|
||||
assert len(plugin2.functions) == 1
|
||||
|
||||
plugin2.update([plugin])
|
||||
assert len(plugin2.functions) == 1
|
||||
|
||||
with raises(TypeError):
|
||||
plugin.update(1)
|
||||
|
||||
with raises(TypeError):
|
||||
plugin.update(1, 2)
|
||||
|
||||
|
||||
def test_iter():
|
||||
expected_plugin_name = "test_plugin"
|
||||
expected_plugin_description = "A unit test plugin"
|
||||
|
||||
func1 = KernelFunctionFromPrompt("test1", expected_plugin_name, None, "test prompt")
|
||||
func2 = KernelFunctionFromPrompt("test1", expected_plugin_name, None, "test prompt")
|
||||
func3 = KernelFunctionFromPrompt("test1", expected_plugin_name, None, "test prompt")
|
||||
|
||||
plugin = KernelPlugin(
|
||||
name=expected_plugin_name, description=expected_plugin_description, functions=[func1, func2, func3]
|
||||
)
|
||||
|
||||
for func in plugin:
|
||||
assert func.metadata in [func1.metadata, func2.metadata, func3.metadata]
|
||||
|
||||
|
||||
# region Properties
|
||||
|
||||
|
||||
def test_get_functions_metadata(mock_function):
|
||||
expected_plugin_name = "test_plugin"
|
||||
expected_plugin_description = "A unit test plugin"
|
||||
|
||||
native_function = KernelFunction.from_method(method=mock_function, plugin_name=expected_plugin_name)
|
||||
|
||||
plugin = KernelPlugin(name=expected_plugin_name, description=expected_plugin_description, functions=native_function)
|
||||
metadatas = plugin.get_functions_metadata()
|
||||
assert len(metadatas) == 1
|
||||
assert metadatas[0] == native_function.metadata
|
||||
|
||||
|
||||
# region Class Methods
|
||||
|
||||
|
||||
def test_from_directory():
|
||||
plugins_directory = os.path.join(os.path.dirname(__file__), "../../assets", "test_plugins")
|
||||
plugin = KernelPlugin.from_directory("TestMixedPlugin", plugins_directory)
|
||||
assert plugin is not None
|
||||
assert len(plugin.functions) == 3
|
||||
assert plugin.name == "TestMixedPlugin"
|
||||
assert plugin.get("TestFunctionYaml") is not None
|
||||
assert plugin.get("echoAsync") is not None
|
||||
assert plugin.get("TestFunction") is not None
|
||||
|
||||
|
||||
def test_from_directory_parent_directory_does_not_exist():
|
||||
# import plugins
|
||||
plugins_directory = os.path.join(os.path.dirname(__file__), "../../assets", "test_plugins_fail")
|
||||
# path to plugins directory
|
||||
with raises(PluginInitializationError, match="Plugin directory does not exist"):
|
||||
KernelPlugin.from_directory("TestPlugin", plugins_directory)
|
||||
|
||||
|
||||
def test_from_python_fail():
|
||||
with raises(PluginInitializationError, match="No class found in file"):
|
||||
KernelPlugin.from_python_file(
|
||||
"TestNativePluginNoClass",
|
||||
os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
"../../assets",
|
||||
"test_native_plugins",
|
||||
"TestNativePluginNoClass",
|
||||
"native_function.py",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_from_python_in_directory_fail():
|
||||
plugins_directory = os.path.join(os.path.dirname(__file__), "../../assets", "test_native_plugins")
|
||||
# path to plugins directory
|
||||
with raises(PluginInitializationError, match="No functions found in folder"):
|
||||
KernelPlugin.from_directory("TestNativePluginNoClass", plugins_directory)
|
||||
|
||||
|
||||
def test_from_yaml_in_directory_fail():
|
||||
plugins_directory = os.path.join(os.path.dirname(__file__), "../../assets", "test_plugins")
|
||||
# path to plugins directory
|
||||
with raises(PluginInitializationError, match="No functions found in folder"):
|
||||
KernelPlugin.from_directory("TestFunctionBadYaml", plugins_directory)
|
||||
|
||||
|
||||
def test_from_directory_other():
|
||||
plugins_directory = os.path.join(os.path.dirname(__file__), "../../assets", "test_plugins")
|
||||
# path to plugins directory
|
||||
with raises(PluginInitializationError, match="No functions found in folder"):
|
||||
KernelPlugin.from_directory("TestNoFunction", plugins_directory)
|
||||
|
||||
|
||||
def test_from_directory_with_args():
|
||||
plugins_directory = os.path.join(os.path.dirname(__file__), "../../assets", "test_native_plugins")
|
||||
# path to plugins directory
|
||||
plugin = KernelPlugin.from_directory(
|
||||
"TestNativePluginArgs",
|
||||
plugins_directory,
|
||||
class_init_arguments={"TestNativeEchoBotPlugin": {"static_input": "prefix "}},
|
||||
)
|
||||
result = plugin["echo"].method(text="test")
|
||||
assert result == "prefix test"
|
||||
|
||||
|
||||
def test_from_object_function(decorated_native_function):
|
||||
plugin = KernelPlugin.from_object("TestPlugin", {"getLightStatusFunc": decorated_native_function})
|
||||
assert plugin is not None
|
||||
assert len(plugin.functions) == 1
|
||||
assert plugin.functions.get("getLightStatus") is not None
|
||||
|
||||
|
||||
def test_from_object_class(custom_plugin_class):
|
||||
plugin = KernelPlugin.from_object("TestPlugin", custom_plugin_class())
|
||||
assert plugin is not None
|
||||
assert len(plugin.functions) == 1
|
||||
assert plugin.functions.get("getLightStatus") is not None
|
||||
|
||||
|
||||
def test_from_openapi():
|
||||
openapi_spec_file = os.path.join(
|
||||
os.path.dirname(__file__), "../../assets/test_plugins", "TestOpenAPIPlugin", "akv-openapi.yaml"
|
||||
)
|
||||
|
||||
plugin = KernelPlugin.from_openapi(
|
||||
plugin_name="TestOpenAPIPlugin",
|
||||
openapi_document_path=openapi_spec_file,
|
||||
)
|
||||
assert plugin is not None
|
||||
assert plugin.name == "TestOpenAPIPlugin"
|
||||
assert plugin.functions.get("GetSecret") is not None
|
||||
assert plugin.functions.get("SetSecret") is not None
|
||||
|
||||
|
||||
def test_custom_spec_from_openapi():
|
||||
openapi_spec_file = os.path.join(
|
||||
os.path.dirname(__file__), "../../assets/test_plugins", "TestOpenAPIPlugin", "akv-openapi.yaml"
|
||||
)
|
||||
|
||||
parser = OpenApiParser()
|
||||
openapi_spec = parser.parse(openapi_spec_file)
|
||||
|
||||
plugin = KernelPlugin.from_openapi(
|
||||
plugin_name="TestOpenAPIPlugin",
|
||||
openapi_parsed_spec=openapi_spec,
|
||||
)
|
||||
assert plugin is not None
|
||||
assert plugin.name == "TestOpenAPIPlugin"
|
||||
assert plugin.functions.get("GetSecret") is not None
|
||||
assert plugin.functions.get("SetSecret") is not None
|
||||
|
||||
|
||||
def test_from_openapi_missing_document_and_parsed_spec_throws():
|
||||
with raises(PluginInitializationError):
|
||||
KernelPlugin.from_openapi(
|
||||
plugin_name="TestOpenAPIPlugin",
|
||||
openapi_document_path=None,
|
||||
)
|
||||
|
||||
|
||||
# region Static Methods
|
||||
def test_parse_or_copy_fail():
|
||||
with raises(ValueError):
|
||||
KernelPlugin._parse_or_copy(None, "test")
|
||||
Reference in New Issue
Block a user