diff --git a/python/semantic_kernel/functions/kernel_function_decorator.py b/python/semantic_kernel/functions/kernel_function_decorator.py index 4d103a1a9093..debff126821e 100644 --- a/python/semantic_kernel/functions/kernel_function_decorator.py +++ b/python/semantic_kernel/functions/kernel_function_decorator.py @@ -71,7 +71,9 @@ def decorator(func: Callable[..., object]) -> Callable[..., object]: setattr(func, "__kernel_function_parameters__", annotations) return_annotation = ( - _parse_parameter("return", func_sig.return_annotation, None) if func_sig.return_annotation else {} + _parse_parameter("return", func_sig.return_annotation, Parameter.empty) + if func_sig.return_annotation + else {} ) setattr(func, "__kernel_function_return_type__", return_annotation.get("type_", "None")) setattr(func, "__kernel_function_return_type_object__", return_annotation.get("type_object", None)) @@ -120,7 +122,8 @@ def _process_signature(func_sig: Signature) -> list[dict[str, Any]]: if arg.name == "self": continue annotation = arg.annotation - default = arg.default if arg.default != arg.empty else None + # Parameter.empty means "no default"; a default of None is still a default. + default = arg.default 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) @@ -137,7 +140,7 @@ def _parse_parameter(name: str, param: Any, default: Any) -> dict[str, Any]: logger.debug(f"Parsing param: {name}") logger.debug(f"Parsing annotation: {param}") ret: dict[str, Any] = {"name": name} - if default is not None: + if default is not Parameter.empty: ret["default_value"] = default ret["is_required"] = False else: @@ -151,13 +154,14 @@ def _parse_parameter(name: str, param: Any, default: Any) -> dict[str, Any]: if isinstance(meta, str): ret["description"] = meta elif isinstance(meta, dict): + meta = meta.copy() # only override from the metadata if it is not already set if "description" not in ret and (description := meta.pop("description", None)): ret["description"] = description ret.update(meta) else: logger.debug(f"Unknown metadata type: {meta}") - if hasattr(param, "__origin__"): + if hasattr(param, "__origin__") and param.__origin__ not in (Union, types.UnionType): ret.update(_parse_parameter(name, param.__origin__, default)) if hasattr(param, "__args__"): args = [] diff --git a/python/tests/unit/functions/test_kernel_function_decorators.py b/python/tests/unit/functions/test_kernel_function_decorators.py index 803e0d2ffc7a..aa55a4e35682 100644 --- a/python/tests/unit/functions/test_kernel_function_decorators.py +++ b/python/tests/unit/functions/test_kernel_function_decorators.py @@ -280,3 +280,37 @@ def test_annotation_parsing(name, annotation, description, type_, is_required): assert description == annotation_dict.get("description") assert type_ == annotation_dict["type_"] assert is_required == annotation_dict["is_required"] + + +def test_annotation_description_precedence_across_metadata_entries(): + """A later dict metadata entry overrides an earlier string description.""" + meta = {"description": "later dictionary"} + + @kernel_function + def func(arg: Annotated[int, "earlier string", meta]) -> str: + return "ok" + + assert func.__kernel_function_parameters__[0]["description"] == "later dictionary" + # The caller-owned dict is untouched by decoration. + assert meta == {"description": "later dictionary"} + + +def test_shared_annotation_metadata_dict_not_mutated(): + """Decorating with one shared Annotated metadata dict keeps it usable twice.""" + shared_meta = {"description": "shared description"} + + @kernel_function + def func_one(arg: Annotated[int, shared_meta]) -> str: + return "one" + + @kernel_function + def func_two(arg: Annotated[int, shared_meta]) -> str: + return "two" + + assert shared_meta == {"description": "shared description"} + descriptions = [ + param["description"] + for func in (func_one, func_two) + for param in func.__kernel_function_parameters__ + ] + assert descriptions == ["shared description", "shared description"]