diff --git a/python/semantic_kernel/functions/kernel_function_decorator.py b/python/semantic_kernel/functions/kernel_function_decorator.py index 4d103a1a9093..2e597099b9e8 100644 --- a/python/semantic_kernel/functions/kernel_function_decorator.py +++ b/python/semantic_kernel/functions/kernel_function_decorator.py @@ -71,7 +71,9 @@ def decorator(func: Callable[..., object]) -> Callable[..., object]: setattr(func, "__kernel_function_parameters__", annotations) return_annotation = ( - _parse_parameter("return", func_sig.return_annotation, None) if func_sig.return_annotation else {} + _parse_parameter("return", func_sig.return_annotation, Parameter.empty) + if func_sig.return_annotation + else {} ) setattr(func, "__kernel_function_return_type__", return_annotation.get("type_", "None")) setattr(func, "__kernel_function_return_type_object__", return_annotation.get("type_object", None)) @@ -120,7 +122,8 @@ def _process_signature(func_sig: Signature) -> list[dict[str, Any]]: if arg.name == "self": continue annotation = arg.annotation - default = arg.default if arg.default != arg.empty else None + # Parameter.empty means "no default"; a default of None is still a default. + default = arg.default parsed_annotation = _parse_parameter(arg.name, annotation, default) if get_origin(annotation) is Annotated or get_origin(annotation) in {Union, types.UnionType}: underlying_type = _get_underlying_type(annotation) @@ -137,7 +140,7 @@ def _parse_parameter(name: str, param: Any, default: Any) -> dict[str, Any]: logger.debug(f"Parsing param: {name}") logger.debug(f"Parsing annotation: {param}") ret: dict[str, Any] = {"name": name} - if default is not None: + if default is not Parameter.empty: ret["default_value"] = default ret["is_required"] = False else: diff --git a/python/tests/unit/functions/test_kernel_function_decorators.py b/python/tests/unit/functions/test_kernel_function_decorators.py index 803e0d2ffc7a..048ea903200e 100644 --- a/python/tests/unit/functions/test_kernel_function_decorators.py +++ b/python/tests/unit/functions/test_kernel_function_decorators.py @@ -90,6 +90,14 @@ def func_input_object_union(self, input: InputObject | str): def func_no_typing(self, input): return input + @kernel_function + def func_input_none_default(self, input=None): + return input + + @kernel_function + def func_input_annotated_none_default(self, input: str = None): + return input + def test_func_name_as_name(): decorator_test = MiscClass() @@ -245,6 +253,24 @@ def test_kernel_function_no_typing(): assert my_func.__kernel_function_parameters__[0]["name"] == "input" +def test_kernel_function_param_none_default(): + decorator_test = MiscClass() + my_func = getattr(decorator_test, "func_input_none_default") + assert my_func.__kernel_function_parameters__[0]["type_"] == "Any" + assert not 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_annotated_none_default(): + decorator_test = MiscClass() + my_func = getattr(decorator_test, "func_input_annotated_none_default") + 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].get("default_value") is None + assert my_func.__kernel_function_parameters__[0]["name"] == "input" + + @pytest.mark.parametrize( ("name", "annotation", "description", "type_", "is_required"), [ diff --git a/python/tests/unit/functions/test_kernel_function_from_method.py b/python/tests/unit/functions/test_kernel_function_from_method.py index 377fae16066f..d56d23de91d6 100644 --- a/python/tests/unit/functions/test_kernel_function_from_method.py +++ b/python/tests/unit/functions/test_kernel_function_from_method.py @@ -461,6 +461,20 @@ def test_list_func() -> list[str]: assert str(result) == "test1,test2" +async def test_function_invoke_with_none_default_argument(kernel: Kernel): + @kernel_function(name="greet") + def greet(name: str, punctuation=None) -> str: + return f"Hello {name}{punctuation if punctuation else '!'}" + + func = KernelFunction.from_method(greet, "test") + + assert func.parameters[1].name == "punctuation" + assert func.parameters[1].is_required is False + + result = await kernel.invoke(function=func, arguments=KernelArguments(name="Ada")) + assert result.value == "Hello Ada!" + + 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)