chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user