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 @@ -124,6 +124,8 @@ def _process_signature(func_sig: Signature) -> list[dict[str, Any]]:
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)
if underlying_type is None:
underlying_type = annotation
else:
underlying_type = annotation
parsed_annotation["type_object"] = underlying_type
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 @@ -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
Loading