Skip to content

Commit 62f0f42

Browse files
fix(functions): allow arbitrary classes in union argument validation
TypeAdapter rejected unions that include ordinary Python classes before validating the selected branch, so a valid str or custom instance passed to a parameter annotated Custom | str raised FunctionExecutionException. Configure the adapter with arbitrary_types_allowed and add regressions for the valid primitive, the valid custom instance, and the still-rejected invalid argument. Signed-off-by: Adil Alperen Ciftci <adilalperenciftci@users.noreply.github.com>
1 parent c02e504 commit 62f0f42

2 files changed

Lines changed: 53 additions & 2 deletions

File tree

‎python/semantic_kernel/functions/kernel_function_from_method.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from inspect import isasyncgen, isasyncgenfunction, isawaitable, iscoroutinefunction, isgenerator, isgeneratorfunction
77
from typing import Any, get_origin
88

9-
from pydantic import Field, TypeAdapter, ValidationError
9+
from pydantic import ConfigDict, Field, TypeAdapter, ValidationError
1010

1111
from semantic_kernel.exceptions import FunctionExecutionException, FunctionInitializationError
1212
from semantic_kernel.filters.functions.function_invocation_context import FunctionInvocationContext
@@ -142,7 +142,8 @@ def _parse_parameter(self, value: Any, param_type: Any) -> Any:
142142
else:
143143
if get_origin(param_type) is not None:
144144
try:
145-
return TypeAdapter(param_type).validate_python(value)
145+
adapter = TypeAdapter(param_type, config=ConfigDict(arbitrary_types_allowed=True))
146+
return adapter.validate_python(value)
146147
except Exception as exc:
147148
raise FunctionExecutionException(
148149
f"Parameter is expected to be parsed to {param_type} but is not."

‎python/tests/unit/functions/test_kernel_function_from_method.py‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -918,3 +918,53 @@ def union_function(value: int | str) -> int | str:
918918
result = await function.invoke(kernel=kernel, arguments=KernelArguments(value=value))
919919

920920
assert result.value == value
921+
922+
923+
class CustomArgument:
924+
"""Plain Python class used to verify arbitrary types survive union validation."""
925+
926+
def __init__(self, label: str) -> None:
927+
self.label = label
928+
929+
930+
async def test_function_invoke_accepts_valid_primitive_in_custom_union(kernel: Kernel):
931+
@kernel_function
932+
def custom_union_function(value: CustomArgument | str) -> str:
933+
return value if isinstance(value, str) else value.label
934+
935+
function = KernelFunction.from_method(custom_union_function, "test")
936+
result = await function.invoke(kernel=kernel, arguments=KernelArguments(value="plain"))
937+
938+
assert result.value == "plain"
939+
940+
941+
async def test_function_invoke_accepts_custom_instance_in_custom_union(kernel: Kernel):
942+
@kernel_function
943+
def custom_union_function(value: CustomArgument | str) -> str:
944+
return value if isinstance(value, str) else value.label
945+
946+
function = KernelFunction.from_method(custom_union_function, "test")
947+
instance = CustomArgument("instance")
948+
result = await function.invoke(kernel=kernel, arguments=KernelArguments(value=instance))
949+
950+
assert result.value == "instance"
951+
952+
953+
async def test_function_invoke_rejects_invalid_custom_union_argument(kernel: Kernel):
954+
invoked = False
955+
956+
@kernel_function
957+
def custom_union_function(value: CustomArgument | str) -> str:
958+
nonlocal invoked
959+
invoked = True
960+
return value if isinstance(value, str) else value.label
961+
962+
function = KernelFunction.from_method(custom_union_function, "test")
963+
964+
with pytest.raises(
965+
FunctionExecutionException,
966+
match=r"Parameter value is expected to be parsed to .* but is not\.",
967+
):
968+
await function.invoke(kernel=kernel, arguments=KernelArguments(value={"unexpected": "value"}))
969+
970+
assert not invoked

0 commit comments

Comments
 (0)