From e9f67ef06f6f16fce302bee28d5678c583ec6417 Mon Sep 17 00:00:00 2001 From: Sohum Trivedi Date: Fri, 17 Jul 2026 10:25:14 -0700 Subject: [PATCH] Python: fix bool parameter parsing so string "false" is not coerced to True When a kernel function declares a bool parameter and the incoming argument is a string (e.g. an LLM tool call that quotes booleans, or values coming through the template engine), _parse_parameter fell through to the generic param_type(value) fallback. bool("false") is True for any non-empty string, so enabled="false" silently ran the function with enabled=True. Parse bool-typed string arguments explicitly: "true"/"1" -> True, "false"/"0" -> False (case-insensitive, whitespace-stripped), and raise FunctionExecutionException for unparseable strings. Non-string values keep the existing behavior. This matches the .NET SDK, which parses "false" as false. --- .../functions/kernel_function_from_method.py | 8 ++++ .../test_kernel_function_from_method.py | 43 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/python/semantic_kernel/functions/kernel_function_from_method.py b/python/semantic_kernel/functions/kernel_function_from_method.py index d9dcf869b95c..09ae55dc1e07 100644 --- a/python/semantic_kernel/functions/kernel_function_from_method.py +++ b/python/semantic_kernel/functions/kernel_function_from_method.py @@ -139,6 +139,14 @@ def _parse_parameter(self, value: Any, param_type: Any) -> Any: item_type = param_type.__args__[0] return [self._parse_parameter(item, item_type) for item in value] raise FunctionExecutionException(f"Expected a list for {param_type}, but got {type(value)}") + elif param_type is bool and isinstance(value, str): + # bool("false") is True for any non-empty string, so parse the string explicitly. + lowered = value.strip().lower() + if lowered in ("true", "1"): + return True + if lowered in ("false", "0"): + return False + raise FunctionExecutionException(f"Cannot parse string '{value}' as bool.") else: try: if isinstance(value, dict) and hasattr(param_type, "__init__"): diff --git a/python/tests/unit/functions/test_kernel_function_from_method.py b/python/tests/unit/functions/test_kernel_function_from_method.py index 3b868625d87c..8b7885349e2b 100644 --- a/python/tests/unit/functions/test_kernel_function_from_method.py +++ b/python/tests/unit/functions/test_kernel_function_from_method.py @@ -520,6 +520,49 @@ def test_parse_invalid_list_raises_exception(get_custom_type_function_pydantic): func._parse_parameter(value, param_type) +@pytest.mark.parametrize( + ("value", "expected"), + [ + ("true", True), + ("True", True), + ("1", True), + ("false", False), + ("False", False), + ("FALSE", False), + ("0", False), + (" false ", False), + (True, True), + (False, False), + (1, True), + (0, False), + ], +) +def test_parse_bool_parameter(get_custom_type_function_pydantic, value, expected): + func = get_custom_type_function_pydantic + result = func._parse_parameter(value, bool) + assert result is expected + + +def test_parse_invalid_bool_string_raises_exception(get_custom_type_function_pydantic): + func = get_custom_type_function_pydantic + with pytest.raises(FunctionExecutionException, match=r"Cannot parse string 'yes' as bool."): + func._parse_parameter("yes", bool) + + +async def test_bool_parameter_from_string_argument(kernel: Kernel): + @kernel_function + def func_bool(enabled: bool) -> str: + return f"enabled={enabled}" + + func = kernel.add_function(plugin_name="test", function_name="func_bool", function=func_bool) + + res = await kernel.invoke(func, KernelArguments(enabled="false")) + assert str(res) == "enabled=False" + + res = await kernel.invoke(func, KernelArguments(enabled="true")) + assert str(res) == "enabled=True" + + def test_parse_dict_with_init_non_pydantic(get_custom_type_function_nonpydantic): func = get_custom_type_function_nonpydantic value = {"id": "3", "name": "Alice"}