Skip to content

Commit f33a6ed

Browse files
committed
Python: preserve already-typed native function arguments
1 parent 2550183 commit f33a6ed

2 files changed

Lines changed: 116 additions & 1 deletion

File tree

‎python/semantic_kernel/functions/kernel_function_from_method.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,13 @@ def _parse_parameter(self, value: Any, param_type: Any) -> Any:
141141
raise FunctionExecutionException(f"Expected a list for {param_type}, but got {type(value)}")
142142
else:
143143
try:
144+
# Keep custom instances intact while retaining built-in constructor coercion.
145+
if (
146+
isinstance(param_type, type)
147+
and param_type.__module__ != "builtins"
148+
and isinstance(value, param_type)
149+
):
150+
return value
144151
if isinstance(value, dict) and hasattr(param_type, "__init__"):
145152
return param_type(**value)
146153
return param_type(value)

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

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import logging
33
from collections.abc import AsyncGenerator, Iterable
44
from contextlib import nullcontext
5+
from dataclasses import dataclass
56
from importlib import import_module
67
from typing import Annotated, Any
78
from unittest.mock import Mock, patch
@@ -10,7 +11,7 @@
1011
from opentelemetry.trace import StatusCode
1112

1213
from semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion import OpenAIChatCompletion
13-
from semantic_kernel.exceptions import FunctionExecutionException, FunctionInitializationError
14+
from semantic_kernel.exceptions import FunctionExecutionException, FunctionInitializationError, KernelInvokeException
1415
from semantic_kernel.filters.functions.function_invocation_context import FunctionInvocationContext
1516
from semantic_kernel.filters.kernel_filters_extension import _rebuild_function_invocation_context
1617
from semantic_kernel.functions.function_result import FunctionResult
@@ -39,6 +40,30 @@ def __init__(self, id: str, name: str):
3940
self.name = name
4041

4142

43+
@dataclass
44+
class CustomTypeDataclass:
45+
id: str
46+
name: str
47+
48+
49+
class DerivedCustomType(CustomTypeNonPydantic):
50+
pass
51+
52+
53+
@dataclass
54+
class DerivedDataclass(CustomTypeDataclass):
55+
pass
56+
57+
58+
@dataclass
59+
class SingleFieldDataclass:
60+
name: str
61+
62+
63+
class RevalidatedCustomType(CustomType):
64+
model_config = {"revalidate_instances": "always", "validate_assignment": False}
65+
66+
4267
@pytest.fixture
4368
def get_custom_type_function_pydantic():
4469
@kernel_function
@@ -585,6 +610,89 @@ def func_default(base: str, input: str = "test"):
585610
assert str(res) == "test"
586611

587612

613+
@pytest.mark.parametrize(
614+
("param_type", "value"),
615+
[
616+
(CustomTypeNonPydantic, CustomTypeNonPydantic("1", "Ada")),
617+
(CustomTypeDataclass, CustomTypeDataclass("1", "Ada")),
618+
(SingleFieldDataclass, SingleFieldDataclass("Ada")),
619+
(CustomTypeNonPydantic, DerivedCustomType("1", "Ada")),
620+
(CustomTypeDataclass, DerivedDataclass("1", "Ada")),
621+
],
622+
)
623+
async def test_invoke_preserves_typed_instance(kernel, param_type, value):
624+
@kernel_function
625+
def identity(param: param_type) -> Any:
626+
return param
627+
628+
func = kernel.add_function(plugin_name="test", function=identity)
629+
for _ in range(2):
630+
result = await kernel.invoke(func, arguments=KernelArguments(param=value))
631+
assert result.value is value
632+
633+
634+
@pytest.mark.parametrize("input_kind", ["dicts", "instances", "mixed"])
635+
async def test_invoke_list_of_typed_instances(kernel, input_kind):
636+
@kernel_function
637+
def identity(param: list[CustomTypeDataclass]) -> Any:
638+
return param
639+
640+
values = [CustomTypeDataclass("1", "Ada"), CustomTypeDataclass("2", "Grace")]
641+
arguments = [
642+
{"id": value.id, "name": value.name}
643+
if input_kind == "dicts" or (input_kind == "mixed" and index == 1)
644+
else value
645+
for index, value in enumerate(values)
646+
]
647+
func = kernel.add_function(plugin_name="test", function=identity)
648+
result = await kernel.invoke(func, arguments=KernelArguments(param=arguments))
649+
assert result.value == values
650+
if input_kind != "dicts":
651+
assert result.value[0] is values[0]
652+
653+
654+
async def test_invoke_typed_instance_returned_by_function(kernel):
655+
value = CustomTypeDataclass("1", "Ada")
656+
657+
@kernel_function
658+
def produce() -> CustomTypeDataclass:
659+
return value
660+
661+
@kernel_function
662+
def consume(param: CustomTypeDataclass) -> str:
663+
assert param is value
664+
return param.name
665+
666+
producer = kernel.add_function(plugin_name="test", function=produce)
667+
consumer = kernel.add_function(plugin_name="test", function=consume)
668+
produced = await kernel.invoke(producer)
669+
result = await kernel.invoke(consumer, arguments=KernelArguments(param=produced.value))
670+
assert result.value == "Ada"
671+
672+
673+
async def test_invoke_pydantic_instance_still_revalidated(kernel):
674+
@kernel_function
675+
def identity(param: RevalidatedCustomType) -> Any:
676+
return param
677+
678+
func = kernel.add_function(plugin_name="test", function=identity)
679+
value = RevalidatedCustomType(id="1", name="Ada")
680+
result = await kernel.invoke(func, arguments=KernelArguments(param=value))
681+
assert result.value == value
682+
assert result.value is not value
683+
684+
value.id = 123
685+
with pytest.raises(KernelInvokeException):
686+
await kernel.invoke(func, arguments=KernelArguments(param=value))
687+
688+
689+
@pytest.mark.parametrize("value", [True, "7", 7])
690+
def test_parse_integer_still_coerces_values(get_custom_type_function_nonpydantic, value):
691+
result = get_custom_type_function_nonpydantic._parse_parameter(value, int)
692+
assert type(result) is int
693+
assert result == int(value)
694+
695+
588696
def test_parse_list_of_objects(get_custom_type_function_pydantic):
589697
func = get_custom_type_function_pydantic
590698

0 commit comments

Comments
 (0)