Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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__

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
35 changes: 35 additions & 0 deletions python/tests/unit/functions/test_kernel_function_from_method.py
Original file line number Diff line number Diff line change
Expand Up @@ -681,3 +681,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
Loading