From e309288e1424fa02b6f566e1e3645d87c330cbb8 Mon Sep 17 00:00:00 2001 From: fei <204683769+feiiiiii5@users.noreply.github.com> Date: Mon, 26 Oct 2026 15:03:59 +0800 Subject: [PATCH] fix(prompt-template): pass positional helper arguments to kernel functions The helper computed the positional arguments from the template but only merged kwargs into KernelArguments, so positional values were dropped and defaults won. Map them to the function's template-facing parameters, skip kernel-injected parameters, and reject excess arguments. --- .../utils/template_function_helpers.py | 25 ++++++++- .../prompt_template/test_template_helper.py | 53 +++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/python/semantic_kernel/prompt_template/utils/template_function_helpers.py b/python/semantic_kernel/prompt_template/utils/template_function_helpers.py index 3e7b7d3675ce..2cdc08087506 100644 --- a/python/semantic_kernel/prompt_template/utils/template_function_helpers.py +++ b/python/semantic_kernel/prompt_template/utils/template_function_helpers.py @@ -8,6 +8,7 @@ import nest_asyncio +from semantic_kernel.exceptions import FunctionExecutionException from semantic_kernel.functions.kernel_arguments import KernelArguments from semantic_kernel.prompt_template.const import ( HANDLEBARS_TEMPLATE_FORMAT_NAME, @@ -22,6 +23,25 @@ logger: logging.Logger = logging.getLogger(__name__) +# Parameters that the kernel injects when invoking a function. They cannot be supplied +# positionally from a template because they are not part of the template-facing signature. +_INJECTED_FUNCTION_PARAMETER_NAMES = frozenset({"kernel", "service", "execution_settings", "arguments"}) + + +def _map_positional_arguments(function: "KernelFunction", positional_args: tuple[Any, ...]) -> dict[str, Any]: + """Map positional template arguments to the function's template-facing parameters.""" + parameter_names = [ + parameter.name + for parameter in function.parameters + if parameter.name is not None and parameter.name not in _INJECTED_FUNCTION_PARAMETER_NAMES + ] + if len(positional_args) > len(parameter_names): + raise FunctionExecutionException( + f"Function '{function.fully_qualified_name}' accepts at most {len(parameter_names)} positional " + f"argument(s), but {len(positional_args)} were provided." + ) + return dict(zip(parameter_names, positional_args)) + def create_template_helper_from_function( function: "KernelFunction", @@ -86,7 +106,6 @@ def func(*args, **kwargs): if base_arguments and base_arguments.execution_settings: arguments.execution_settings = base_arguments.execution_settings # pragma: no cover arguments.update(base_arguments) - arguments.update(kwargs) if len(args) > 0 and template_format == HANDLEBARS_TEMPLATE_FORMAT_NAME: this = args[0] @@ -100,6 +119,9 @@ def func(*args, **kwargs): else: logger.debug("Jinja2 context or Handlebars context without `this`") + arguments.update(_map_positional_arguments(function, actual_args)) + arguments.update(kwargs) + logger.debug( "Invoking function %s with positional argument count %d and keyword argument count %d.", function.metadata.fully_qualified_name, @@ -131,6 +153,7 @@ async def func(*args, **kwargs): if base_arguments and base_arguments.execution_settings: arguments.execution_settings = base_arguments.execution_settings # pragma: no cover arguments.update(base_arguments) + arguments.update(_map_positional_arguments(function, args)) arguments.update(kwargs) logger.debug( "Invoking function %s with positional argument count %d and keyword argument count %d.", diff --git a/python/tests/unit/prompt_template/test_template_helper.py b/python/tests/unit/prompt_template/test_template_helper.py index 723ac2f6008a..beb6f52c36aa 100644 --- a/python/tests/unit/prompt_template/test_template_helper.py +++ b/python/tests/unit/prompt_template/test_template_helper.py @@ -4,6 +4,7 @@ import pytest +from semantic_kernel.exceptions import FunctionExecutionException from semantic_kernel.functions import kernel_function from semantic_kernel.functions.kernel_function_from_method import KernelFunctionFromMethod from semantic_kernel.kernel import Kernel @@ -95,3 +96,55 @@ async def test_create_helpers_fail(kernel: Kernel, template_format: str, enable_ assert int(str(res)) == 2 else: assert int(str(result(x=1))) == 2 + + +@pytest.mark.parametrize( + ("template_format", "prepend_context"), + [ + pytest.param(JINJA2_TEMPLATE_FORMAT_NAME, False, id="jinja2"), + pytest.param(HANDLEBARS_TEMPLATE_FORMAT_NAME, True, id="handlebars"), + ], +) +def test_create_sync_helpers_with_positional_arguments(kernel: Kernel, template_format: str, prepend_context: bool): + function = KernelFunctionFromMethod(kernel_function(lambda a=1, b=2: a + b, name="test"), plugin_name="test") + helper = create_template_helper_from_function( + function, kernel, {}, template_format, allow_dangerously_set_content=True, enable_async=False + ) + + args = (None, 3, 4) if prepend_context else (3, 4) + result = helper(*args) + + assert int(str(result)) == 7 + + +async def test_create_async_helper_with_positional_arguments(kernel: Kernel): + function = KernelFunctionFromMethod(kernel_function(lambda a=1, b=2: a + b, name="test"), plugin_name="test") + helper = create_template_helper_from_function( + function, kernel, {}, JINJA2_TEMPLATE_FORMAT_NAME, allow_dangerously_set_content=True, enable_async=True + ) + + result = await helper(3, 4) + + assert int(str(result)) == 7 + + +def test_create_helper_rejects_too_many_positional_arguments(kernel: Kernel): + function = KernelFunctionFromMethod(kernel_function(lambda a=1, b=2: a + b, name="test"), plugin_name="test") + helper = create_template_helper_from_function( + function, kernel, {}, HANDLEBARS_TEMPLATE_FORMAT_NAME, allow_dangerously_set_content=True, enable_async=False + ) + + with pytest.raises(FunctionExecutionException, match="accepts at most 2 positional argument"): + helper(None, 1, 2, 3) + + +def test_create_helper_skips_kernel_injected_parameters(kernel: Kernel): + def function_with_kernel(kernel: Kernel, value: str = "default") -> str: + return value + + function = KernelFunctionFromMethod(kernel_function(function_with_kernel, name="test"), plugin_name="test") + helper = create_template_helper_from_function( + function, kernel, {}, JINJA2_TEMPLATE_FORMAT_NAME, allow_dangerously_set_content=True, enable_async=False + ) + + assert str(helper("passed")) == "passed"