Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion python/semantic_kernel/template_engine/blocks/code_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,22 @@ def _enrich_function_arguments(
)
for index, token in enumerate(self.tokens[1:], start=1):
logger.debug(f"Parsing variable/value: `{self.tokens[1].content}`")
rendered_value = token.render(kernel, arguments) # type: ignore

# For VarBlock, get the raw value to preserve the original type
# For other blocks (ValBlock, NamedArgBlock), render to string as usual
from semantic_kernel.template_engine.blocks.var_block import VarBlock

if isinstance(token, VarBlock):
rendered_value = token.get_value(arguments)
elif isinstance(token, NamedArgBlock):
# NamedArgBlock may contain a VarBlock, so check for that
if token.variable:
rendered_value = token.variable.get_value(arguments)
else:
rendered_value = token.render(kernel, arguments)
else:
rendered_value = token.render(kernel, arguments) # type: ignore

if not isinstance(token, NamedArgBlock) and index == 1:
arguments[function_metadata.parameters[0].name] = rendered_value
continue
Expand Down
15 changes: 15 additions & 0 deletions python/semantic_kernel/template_engine/blocks/var_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,18 @@ def render(self, _: "Kernel", arguments: Optional["KernelArguments"] = None) ->
raise VarBlockRenderError(
f"Block {self.name} failed to be parsed to a string, type is {type(value)}"
) from e

def get_value(self, arguments: "KernelArguments | None" = None) -> Any:
"""Get the raw value of the variable from arguments without converting to string.

This is used when passing arguments to functions to preserve their original types.

Args:
arguments: The KernelArguments to get the value from.

Returns:
The raw value from the arguments, or None if not found.
"""
if arguments is None:
return None
return arguments.get(self.name, None)
96 changes: 96 additions & 0 deletions python/tests/unit/template_engine/blocks/test_code_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -483,3 +483,99 @@ def test_edge_cases(case, result):
def test_no_tokens():
with raises(CodeBlockTokenError):
CodeBlock(content="", tokens=[])


class TestNonStringArguments:
"""Test that non-string KernelArguments are preserved when passed to functions in templates."""

async def test_function_receives_int_type(self, kernel: Kernel):
"""Test that an integer argument is passed as int, not converted to string."""
received_value = None
received_type = None

@kernel_function(name="check_type")
def check_type(value: int):
nonlocal received_value, received_type
received_value = value
received_type = type(value)
return f"Received {type(value).__name__}: {value}"

function = KernelFunctionFromMethod(method=check_type, plugin_name="test")
kernel.add_plugin(KernelPlugin(name="test", functions=[function]))

code_block = CodeBlock(content="test.check_type value=$my_int")
arguments = KernelArguments(my_int=42)

await code_block.render_code(kernel, arguments)

assert received_value == 42
assert isinstance(received_value, int), f"Expected int but got {received_type}"

async def test_function_receives_list_type(self, kernel: Kernel):
"""Test that a list argument is passed as list, not converted to string."""
received_value = None
received_type = None

@kernel_function(name="check_type")
def check_type(items: list):
nonlocal received_value, received_type
received_value = items
received_type = type(items)
return f"Received {len(items)} items"

function = KernelFunctionFromMethod(method=check_type, plugin_name="test")
kernel.add_plugin(KernelPlugin(name="test", functions=[function]))

code_block = CodeBlock(content="test.check_type items=$my_list")
arguments = KernelArguments(my_list=[1, 2, 3])

await code_block.render_code(kernel, arguments)

assert received_value == [1, 2, 3]
assert isinstance(received_value, list), f"Expected list but got {received_type}"

async def test_function_receives_dict_type(self, kernel: Kernel):
"""Test that a dict argument is passed as dict, not converted to string."""
received_value = None
received_type = None

@kernel_function(name="check_type")
def check_type(data: dict):
nonlocal received_value, received_type
received_value = data
received_type = type(data)
return f"Received dict with keys: {list(data.keys())}"

function = KernelFunctionFromMethod(method=check_type, plugin_name="test")
kernel.add_plugin(KernelPlugin(name="test", functions=[function]))

code_block = CodeBlock(content="test.check_type data=$my_dict")
arguments = KernelArguments(my_dict={"key": "value", "num": 123})

await code_block.render_code(kernel, arguments)

assert received_value == {"key": "value", "num": 123}
assert isinstance(received_value, dict), f"Expected dict but got {received_type}"

async def test_named_arg_with_non_string_type(self, kernel: Kernel):
"""Test that named arguments with non-string types are preserved."""
received_count = None
received_type = None

@kernel_function(name="process")
def process(text: str, count: int):
nonlocal received_count, received_type
received_count = count
received_type = type(count)
return f"{text} x {count}"

function = KernelFunctionFromMethod(method=process, plugin_name="test")
kernel.add_plugin(KernelPlugin(name="test", functions=[function]))

code_block = CodeBlock(content="test.process 'hello' count=$repetitions")
arguments = KernelArguments(repetitions=5)

await code_block.render_code(kernel, arguments)

assert received_count == 5
assert isinstance(received_count, int), f"Expected int but got {received_type}"
Loading