diff --git a/python/semantic_kernel/functions/kernel_function_decorator.py b/python/semantic_kernel/functions/kernel_function_decorator.py index 4d103a1a9093..834341dc6ef5 100644 --- a/python/semantic_kernel/functions/kernel_function_decorator.py +++ b/python/semantic_kernel/functions/kernel_function_decorator.py @@ -98,14 +98,14 @@ def _get_non_none_type(args: tuple) -> Any: def _get_underlying_type(annotation: Any) -> Any: """Get the underlying type of the annotation.""" if isinstance(annotation, types.UnionType): - return _get_non_none_type(annotation.__args__) + return _get_non_none_type(annotation.__args__) or annotation if hasattr(annotation, "__origin__"): if annotation.__origin__ is Union: - return _get_non_none_type(get_args(annotation)) + return _get_non_none_type(get_args(annotation)) or annotation if isinstance(annotation.__origin__, types.UnionType): - return _get_non_none_type(annotation.__origin__.__args__) + return _get_non_none_type(annotation.__origin__.__args__) or annotation.__origin__ return annotation.__origin__ diff --git a/python/semantic_kernel/functions/kernel_function_from_method.py b/python/semantic_kernel/functions/kernel_function_from_method.py index d9dcf869b95c..dc4125aefe43 100644 --- a/python/semantic_kernel/functions/kernel_function_from_method.py +++ b/python/semantic_kernel/functions/kernel_function_from_method.py @@ -4,9 +4,9 @@ import logging from collections.abc import Callable from inspect import isasyncgen, isasyncgenfunction, isawaitable, iscoroutinefunction, isgenerator, isgeneratorfunction -from typing import Any +from typing import Any, get_origin -from pydantic import Field, ValidationError +from pydantic import Field, TypeAdapter, ValidationError from semantic_kernel.exceptions import FunctionExecutionException, FunctionInitializationError from semantic_kernel.filters.functions.function_invocation_context import FunctionInvocationContext @@ -140,6 +140,13 @@ def _parse_parameter(self, value: Any, param_type: Any) -> Any: return [self._parse_parameter(item, item_type) for item in value] raise FunctionExecutionException(f"Expected a list for {param_type}, but got {type(value)}") else: + if get_origin(param_type) is not None: + try: + return TypeAdapter(param_type).validate_python(value) + except Exception as exc: + raise FunctionExecutionException( + f"Parameter is expected to be parsed to {param_type} but is not." + ) from exc try: if isinstance(value, dict) and hasattr(param_type, "__init__"): return param_type(**value) @@ -171,7 +178,6 @@ def gather_function_parameters(self, context: FunctionInvocationContext) -> dict value: Any = context.arguments[param.name] if ( param.type_ - and "," not in param.type_ and param.type_object and param.type_object is not inspect._empty and param.type_object is not Any 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 3b868625d87c..f3756c341339 100644 --- a/python/tests/unit/functions/test_kernel_function_from_method.py +++ b/python/tests/unit/functions/test_kernel_function_from_method.py @@ -566,3 +566,38 @@ def test_function_model_dump_json(get_custom_type_function_pydantic): model_dump = func.model_dump_json() assert isinstance(model_dump, str) assert "metadata" in model_dump + + +async def test_function_invoke_rejects_invalid_union_argument(kernel: Kernel): + invoked = False + + @kernel_function + def union_function(value: int | str) -> int | str: + nonlocal invoked + invoked = True + return value + + function = KernelFunction.from_method(union_function, "test") + + with pytest.raises( + FunctionExecutionException, + match=r"Parameter value is expected to be parsed to .* but is not\.", + ): + await function.invoke( + kernel=kernel, + arguments=KernelArguments(value={"unexpected": "value"}), + ) + + assert not invoked + + +@pytest.mark.parametrize("value", [42, "valid"]) +async def test_function_invoke_accepts_valid_union_argument(kernel: Kernel, value: int | str): + @kernel_function + def union_function(value: int | str) -> int | str: + return value + + function = KernelFunction.from_method(union_function, "test") + result = await function.invoke(kernel=kernel, arguments=KernelArguments(value=value)) + + assert result.value == value