Skip to content
Draft
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
10 changes: 9 additions & 1 deletion python/semantic_kernel/template_engine/blocks/code_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from semantic_kernel.template_engine.blocks.block_types import BlockTypes
from semantic_kernel.template_engine.blocks.function_id_block import FunctionIdBlock
from semantic_kernel.template_engine.blocks.named_arg_block import NamedArgBlock
from semantic_kernel.template_engine.blocks.var_block import VarBlock
from semantic_kernel.template_engine.code_tokenizer import CodeTokenizer

if TYPE_CHECKING:
Expand Down Expand Up @@ -149,7 +150,14 @@ 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 NamedArgBlock, render() returns the actual value (not string representation)
# For VarBlock used as positional arg, we need to get the actual value too
if isinstance(token, VarBlock):
# Get the actual value from arguments, not the string representation
rendered_value = arguments.get(token.name, "")
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
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,20 @@ def parse_content(cls, fields: Any) -> Any:
return fields

def render(self, kernel: "Kernel", arguments: Optional["KernelArguments"] = None) -> Any:
"""Render the named argument block."""
"""Render the named argument block.

When rendering a named argument, we return the actual value from the arguments
(not the string representation) so that functions receive the correct type.
"""
if self.value:
return self.value.render()
if arguments is None:
return ""
if self.variable:
return self.variable.render(kernel, arguments)
# Return the actual value from arguments, not the string representation
# Check if the variable name exists in arguments
if self.variable.name not in arguments:
logger.warning(f"Variable `${self.variable.name}` not found in the KernelArguments")
return ""
return arguments[self.variable.name]
return None
219 changes: 219 additions & 0 deletions python/tests/unit/prompt_template/test_non_string_arguments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
# Copyright (c) Microsoft. All rights reserved.

"""Tests for non-string kernel arguments passed to functions in prompt templates."""

import pytest

from semantic_kernel import Kernel
from semantic_kernel.functions import kernel_function
from semantic_kernel.functions.kernel_arguments import KernelArguments
from semantic_kernel.prompt_template.kernel_prompt_template import KernelPromptTemplate
from semantic_kernel.prompt_template.prompt_template_config import PromptTemplateConfig


class NonStringArgumentsPlugin:
"""Plugin to test non-string argument types."""

@kernel_function
def process_int(self, value: int) -> str:
"""Process an integer argument."""
assert isinstance(value, int), f"Expected int, got {type(value)}"
return f"int:{value * 2}"

@kernel_function
def process_float(self, value: float) -> str:
"""Process a float argument."""
assert isinstance(value, (float, int)), f"Expected float, got {type(value)}"
return f"float:{value * 2.0}"

@kernel_function
def process_bool(self, value: bool) -> str:
"""Process a boolean argument."""
assert isinstance(value, bool), f"Expected bool, got {type(value)}"
return f"bool:{not value}"

@kernel_function
def process_list(self, items: list) -> str:
"""Process a list argument."""
assert isinstance(items, list), f"Expected list, got {type(items)}"
return f"list:{len(items)}"

@kernel_function
def process_dict(self, data: dict) -> str:
"""Process a dict argument."""
assert isinstance(data, dict), f"Expected dict, got {type(data)}"
return f"dict:{len(data)}"

@kernel_function
def process_none(self, value: None) -> str:
"""Process a None argument."""
assert value is None, f"Expected None, got {value}"
return "none:received"

@kernel_function
def process_multiple(self, a: int, b: str, c: list) -> str:
"""Process multiple arguments of different types."""
assert isinstance(a, int), f"Expected int for a, got {type(a)}"
assert isinstance(b, str), f"Expected str for b, got {type(b)}"
assert isinstance(c, list), f"Expected list for c, got {type(c)}"
return f"multi:{a},{b},{len(c)}"


@pytest.mark.asyncio
async def test_int_argument(kernel: Kernel):
"""Test passing integer argument to function."""
kernel.add_plugin(NonStringArgumentsPlugin(), "test")
template = "Result: {{ test.process_int value=$num }}"
arguments = KernelArguments(num=42)

result = await KernelPromptTemplate(
prompt_template_config=PromptTemplateConfig(
name="test", description="test", template=template, allow_dangerously_set_content=True
)
).render(kernel, arguments)

assert result == "Result: int:84"


@pytest.mark.asyncio
async def test_float_argument(kernel: Kernel):
"""Test passing float argument to function."""
kernel.add_plugin(NonStringArgumentsPlugin(), "test")
template = "Result: {{ test.process_float value=$num }}"
arguments = KernelArguments(num=3.14)

result = await KernelPromptTemplate(
prompt_template_config=PromptTemplateConfig(
name="test", description="test", template=template, allow_dangerously_set_content=True
)
).render(kernel, arguments)

assert result == "Result: float:6.28"


@pytest.mark.asyncio
async def test_bool_argument(kernel: Kernel):
"""Test passing boolean argument to function."""
kernel.add_plugin(NonStringArgumentsPlugin(), "test")
template = "Result: {{ test.process_bool value=$flag }}"
arguments = KernelArguments(flag=True)

result = await KernelPromptTemplate(
prompt_template_config=PromptTemplateConfig(
name="test", description="test", template=template, allow_dangerously_set_content=True
)
).render(kernel, arguments)

assert result == "Result: bool:False"


@pytest.mark.asyncio
async def test_list_argument(kernel: Kernel):
"""Test passing list argument to function."""
kernel.add_plugin(NonStringArgumentsPlugin(), "test")
template = "Result: {{ test.process_list items=$my_list }}"
arguments = KernelArguments(my_list=[1, 2, 3, 4, 5])

result = await KernelPromptTemplate(
prompt_template_config=PromptTemplateConfig(
name="test", description="test", template=template, allow_dangerously_set_content=True
)
).render(kernel, arguments)

assert result == "Result: list:5"


@pytest.mark.asyncio
async def test_dict_argument(kernel: Kernel):
"""Test passing dict argument to function."""
kernel.add_plugin(NonStringArgumentsPlugin(), "test")
template = "Result: {{ test.process_dict data=$my_dict }}"
arguments = KernelArguments(my_dict={"a": 1, "b": 2, "c": 3})

result = await KernelPromptTemplate(
prompt_template_config=PromptTemplateConfig(
name="test", description="test", template=template, allow_dangerously_set_content=True
)
).render(kernel, arguments)

assert result == "Result: dict:3"


@pytest.mark.asyncio
async def test_none_argument(kernel: Kernel):
"""Test passing None argument to function."""
kernel.add_plugin(NonStringArgumentsPlugin(), "test")
template = "Result: {{ test.process_none value=$none_val }}"
arguments = KernelArguments(none_val=None)

result = await KernelPromptTemplate(
prompt_template_config=PromptTemplateConfig(
name="test", description="test", template=template, allow_dangerously_set_content=True
)
).render(kernel, arguments)

assert result == "Result: none:received"


@pytest.mark.asyncio
async def test_multiple_typed_arguments(kernel: Kernel):
"""Test passing multiple arguments of different types to function."""
kernel.add_plugin(NonStringArgumentsPlugin(), "test")
template = "Result: {{ test.process_multiple a=$num b=$text c=$items }}"
arguments = KernelArguments(num=10, text="hello", items=[1, 2, 3])

result = await KernelPromptTemplate(
prompt_template_config=PromptTemplateConfig(
name="test", description="test", template=template, allow_dangerously_set_content=True
)
).render(kernel, arguments)

assert result == "Result: multi:10,hello,3"


@pytest.mark.asyncio
async def test_string_argument_still_works(kernel: Kernel):
"""Test that string arguments still work correctly."""
kernel.add_plugin(NonStringArgumentsPlugin(), "test")
template = "Result: {{ test.process_multiple a=$num b=$text c=$items }}"
arguments = KernelArguments(num=5, text="world", items=[])

result = await KernelPromptTemplate(
prompt_template_config=PromptTemplateConfig(
name="test", description="test", template=template, allow_dangerously_set_content=True
)
).render(kernel, arguments)

assert result == "Result: multi:5,world,0"


@pytest.mark.asyncio
async def test_positional_list_argument(kernel: Kernel):
"""Test passing list as positional (first) argument to function."""
kernel.add_plugin(NonStringArgumentsPlugin(), "test")
template = "Result: {{ test.process_list $my_list }}"
arguments = KernelArguments(my_list=[1, 2, 3, 4, 5])

result = await KernelPromptTemplate(
prompt_template_config=PromptTemplateConfig(
name="test", description="test", template=template, allow_dangerously_set_content=True
)
).render(kernel, arguments)

assert result == "Result: list:5"


@pytest.mark.asyncio
async def test_positional_int_argument(kernel: Kernel):
"""Test passing int as positional (first) argument to function."""
kernel.add_plugin(NonStringArgumentsPlugin(), "test")
template = "Result: {{ test.process_int $num }}"
arguments = KernelArguments(num=42)

result = await KernelPromptTemplate(
prompt_template_config=PromptTemplateConfig(
name="test", description="test", template=template, allow_dangerously_set_content=True
)
).render(kernel, arguments)

assert result == "Result: int:84"