Files
microsoft--semantic-kernel/python/tests/unit/template_engine/blocks/test_code_block.py
T
wehub-resource-sync b957a53def
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:21:23 +08:00

582 lines
19 KiB
Python

# Copyright (c) Microsoft. All rights reserved.
from pytest import mark, raises
from semantic_kernel.exceptions import (
CodeBlockRenderException,
CodeBlockSyntaxError,
CodeBlockTokenError,
FunctionIdBlockSyntaxError,
NamedArgBlockSyntaxError,
ValBlockSyntaxError,
VarBlockSyntaxError,
)
from semantic_kernel.functions.kernel_arguments import KernelArguments
from semantic_kernel.functions.kernel_function_decorator import kernel_function
from semantic_kernel.functions.kernel_function_from_method import KernelFunctionFromMethod
from semantic_kernel.functions.kernel_plugin import KernelPlugin
from semantic_kernel.kernel import Kernel
from semantic_kernel.template_engine.blocks.block_types import BlockTypes
from semantic_kernel.template_engine.blocks.code_block import CodeBlock
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.val_block import ValBlock
from semantic_kernel.template_engine.blocks.var_block import VarBlock
def test_init():
target = CodeBlock(
content="plugin.function 'value' arg1=$arg1",
)
assert len(target.tokens) == 3
assert target.tokens[0] == FunctionIdBlock(content="plugin.function")
assert target.tokens[1] == ValBlock(content="'value'")
assert target.tokens[2] == NamedArgBlock(content="arg1=$arg1")
assert target.type == BlockTypes.CODE
class TestCodeBlockRendering:
async def test_it_throws_if_a_plugins_are_empty(self, kernel: Kernel):
target = CodeBlock(
content="functionName",
)
assert target.tokens[0].type == BlockTypes.FUNCTION_ID
with raises(CodeBlockRenderException, match="Function `functionName` not found"):
await target.render_code(kernel, KernelArguments())
async def test_it_throws_if_a_function_doesnt_exist(self, kernel: Kernel):
target = CodeBlock(
content="functionName",
)
assert target.tokens[0].type == BlockTypes.FUNCTION_ID
kernel.add_plugin(KernelPlugin(name="test", functions=[]))
with raises(CodeBlockRenderException, match="Function `functionName` not found"):
await target.render_code(kernel, KernelArguments())
async def test_it_throws_if_a_function_call_throws(self, kernel: Kernel):
@kernel_function(name="funcName")
def invoke():
raise Exception("function exception")
function = KernelFunctionFromMethod(
method=invoke,
plugin_name="pluginName",
)
kernel.add_function(plugin_name="test", function=function)
target = CodeBlock(
content="test.funcName",
)
with raises(CodeBlockRenderException, match="test.funcName"):
await target.render_code(kernel, KernelArguments())
async def test_it_renders_code_block_consisting_of_just_a_var_block1(self, kernel: Kernel):
code_block = CodeBlock(
content="$var",
)
result = await code_block.render_code(kernel, KernelArguments(var="foo"))
assert result == "foo"
async def test_it_renders_code_block_consisting_of_just_a_val_block1(self, kernel: Kernel):
code_block = CodeBlock(
content="'ciao'",
)
result = await code_block.render_code(kernel, KernelArguments())
assert result == "ciao"
async def test_it_invokes_function_cloning_all_variables(self, kernel: Kernel):
# Set up initial context variables
arguments = KernelArguments(input="zero", var1="uno", var2="due")
# Create a FunctionIdBlock with the function name
func_id = FunctionIdBlock(content="test.funcName")
# Set up a canary dictionary to track changes in the context variables
canary = {"input": "", "var1": "", "var2": ""}
# Define the function to be invoked, which modifies the canary
# and context variables
@kernel_function(name="funcName")
def invoke(arguments: KernelArguments):
nonlocal canary
canary["input"] = arguments["input"]
canary["var1"] = arguments["var1"]
canary["var2"] = arguments["var2"]
arguments["input"] = "overridden"
arguments["var1"] = "overridden"
arguments["var2"] = "overridden"
# Create an KernelFunction with the invoke function as its delegate
function = KernelFunctionFromMethod(
method=invoke,
plugin_name="pluginName",
)
kernel.add_plugin(KernelPlugin(name="test", functions=[function]))
# Create a CodeBlock with the FunctionIdBlock and render it with the context
code_block = CodeBlock(
tokens=[func_id],
content="",
)
await code_block.render_code(kernel, arguments)
# Check that the canary values match the original context variables
assert canary["input"] == "zero"
assert canary["var1"] == "uno"
assert canary["var2"] == "due"
# Check that the original context variables were not modified
assert arguments["input"] == "zero"
assert arguments["var1"] == "uno"
assert arguments["var2"] == "due"
async def test_it_invokes_function_with_custom_variable(self, kernel: Kernel):
# Define custom variable name and value
VAR_NAME = "varName"
VAR_VALUE = "varValue"
# Set up initial context variables
arguments = KernelArguments()
arguments[VAR_NAME] = VAR_VALUE
# Create a FunctionIdBlock with the function name and a
# VarBlock with the custom variable
func_id = FunctionIdBlock(content="test.funcName")
var_block = VarBlock(content=f"${VAR_NAME}")
# Set up a canary variable to track changes in the context input
canary = ""
# Define the function to be invoked, which modifies the canary variable
@kernel_function(name="funcName")
def invoke(arguments: "KernelArguments"):
nonlocal canary
canary = arguments["varName"]
return arguments["varName"]
# Create an KernelFunction with the invoke function as its delegate
function = KernelFunctionFromMethod(
method=invoke,
plugin_name="pluginName",
)
kernel.add_plugin(KernelPlugin(name="test", functions=[function]))
# Create a CodeBlock with the FunctionIdBlock and VarBlock,
# and render it with the context
code_block = CodeBlock(
tokens=[func_id, var_block],
content="",
)
result = await code_block.render_code(kernel, arguments)
# Check that the result matches the custom variable value
assert result == VAR_VALUE
# Check that the canary value matches the custom variable value
assert canary == VAR_VALUE
async def test_it_invokes_function_with_custom_value(self, kernel: Kernel):
# Define a value to be used in the test
VALUE = "value"
# Create a FunctionIdBlock with the function name and a ValBlock with the value
func_id = FunctionIdBlock(content="test.funcName")
val_block = ValBlock(content=f"'{VALUE}'")
# Set up a canary variable to track changes in the context input
canary = ""
# Define the function to be invoked, which modifies the canary variable
@kernel_function(name="funcName")
def invoke(arguments):
nonlocal canary
canary = arguments["input"]
return arguments["input"]
# Create an KernelFunction with the invoke function as its delegate
function = KernelFunctionFromMethod(
method=invoke,
plugin_name="pluginName",
)
kernel.add_plugin(KernelPlugin(name="test", functions=[function]))
# Create a CodeBlock with the FunctionIdBlock and ValBlock,
# and render it with the context
code_block = CodeBlock(
tokens=[func_id, val_block],
content="",
)
result = await code_block.render_code(kernel, KernelArguments(input="value"))
# Check that the result matches the value
assert str(result) == VALUE
# Check that the canary value matches the value
assert canary == VALUE
async def test_it_invokes_function_with_multiple_arguments(self, kernel: Kernel):
# Define a value to be used in the test
VALUE = "value"
code_block = CodeBlock(
content=" ",
tokens=[
FunctionIdBlock(content="test.funcName", plugin_name="test", function_name="funcName", validated=True),
ValBlock(content=f'"{VALUE}"'),
NamedArgBlock(content="arg1=$arg1"),
NamedArgBlock(content='arg2="arg2"'),
],
)
# Set up a canary variable to track changes in the context input
canary = ""
# Define the function to be invoked, which modifies the canary variable
@kernel_function(name="funcName")
def invoke(input, arg1, arg2):
nonlocal canary
canary = f"{input} {arg1} {arg2}"
return input
# Create an KernelFunction with the invoke function as its delegate
function = KernelFunctionFromMethod(
method=invoke,
plugin_name="pluginName",
)
kernel.add_plugin(KernelPlugin(name="test", functions=[function]))
# Create a CodeBlock with the FunctionIdBlock and ValBlock,
# and render it with the context
result = await code_block.render_code(kernel, KernelArguments(arg1="arg1"))
# Check that the result matches the value
assert str(result) == VALUE
# Check that the canary value matches the value
assert canary == f"{VALUE} arg1 arg2"
async def test_it_invokes_function_with_only_named_arguments(self, kernel: Kernel):
code_block = CodeBlock(
content=" ",
tokens=[
FunctionIdBlock(content="test.funcName", plugin_name="test", function_name="funcName"),
NamedArgBlock(content="arg1=$arg1"),
NamedArgBlock(content='arg2="arg2"'),
],
)
# Set up a canary variable to track changes in the context input
canary = ""
# Define the function to be invoked, which modifies the canary variable
@kernel_function(name="funcName")
def invoke(arg1, arg2):
nonlocal canary
canary = f"{arg1} {arg2}"
return arg1
# Create an KernelFunction with the invoke function as its delegate
function = KernelFunctionFromMethod(
method=invoke,
plugin_name="pluginName",
)
kernel.add_plugin(KernelPlugin(name="test", functions=[function]))
# Create a CodeBlock with the FunctionIdBlock and ValBlock,
# and render it with the context
result = await code_block.render_code(kernel, KernelArguments(arg1="arg1"))
# Check that the result matches the value
assert str(result) == "arg1"
# Check that the canary value matches the value
assert canary == "arg1 arg2"
async def test_it_fails_on_function_without_args(self, kernel: Kernel):
code_block = CodeBlock(
content=" ",
tokens=[
FunctionIdBlock(content="test.funcName", plugin_name="test", function_name="funcName"),
NamedArgBlock(content="arg1=$arg1"),
NamedArgBlock(content='arg2="arg2"'),
],
)
@kernel_function(name="funcName")
def invoke():
return "function without args"
# Create an KernelFunction with the invoke function as its delegate
function = KernelFunctionFromMethod(
method=invoke,
plugin_name="test",
)
kernel.add_plugin(KernelPlugin(name="test", functions=[function]))
# Create a CodeBlock with the FunctionIdBlock and ValBlock,
# and render it with the context
with raises(
CodeBlockRenderException,
match="Function test.funcName does not take any arguments \
but it is being called in the template with 2 arguments.",
):
await code_block.render_code(kernel, KernelArguments(arg1="arg1"))
@mark.parametrize(
"token2",
[
"",
"arg2=$arg!2",
"arg2='va\"l'",
],
ids=[
"empty",
"invalid_named_arg",
"invalid_named_arg_val",
],
)
@mark.parametrize(
"token1",
[
"",
"$var!",
"\"val'",
"arg1=$arg!1",
"arg1='va\"l'",
],
ids=[
"empty",
"invalid_var",
"invalid_val",
"invalid_named_arg",
"invalid_named_arg_val",
],
)
@mark.parametrize(
"token0",
[
"plugin.func.test",
"$var!",
'"va"l"',
],
ids=[
"invalid_func",
"invalid_var",
"invalid_val",
],
)
def test_block_validation(token0, token1, token2):
with raises((
FunctionIdBlockSyntaxError,
VarBlockSyntaxError,
ValBlockSyntaxError,
NamedArgBlockSyntaxError,
CodeBlockSyntaxError,
)):
CodeBlock(
content=f"{token0} {token1} {token2}",
)
@mark.parametrize(
"token2, token2valid",
[
("", True),
("plugin.func", False),
("$var", False),
('"val"', False),
("arg1=$arg1", True),
("arg1='val'", True),
],
ids=[
"empty",
"func_invalid",
"invalid_var",
"invalid_val",
"valid_named_arg",
"valid_named_arg_val",
],
)
@mark.parametrize(
"token1, token1valid",
[
("", True),
("plugin.func", False),
("$var", True),
('"val"', True),
("arg1=$arg1", True),
("arg1='val'", True),
],
ids=[
"empty",
"func_invalid",
"var",
"val",
"valid_named_arg",
"valid_named_arg_val",
],
)
@mark.parametrize(
"token0, token0valid",
[
("func", True),
("plugin.func", True),
("$var", True),
('"val"', True),
("arg1=$arg1", False),
("arg1='val'", False),
],
ids=[
"single_name_func",
"FQN_func",
"var",
"val",
"invalid_named_arg",
"invalid_named_arg_val",
],
)
def test_positional_validation(token0, token0valid, token1, token1valid, token2, token2valid):
if not token1 and not token2valid:
mark.skipif(f"{token0} {token1} {token2}", reason="Not applicable")
return
valid = token0valid and token1valid and token2valid
if token0 in ["$var", '"val"']:
valid = True
content = f"{token0} {token1} {token2}"
if valid:
target = CodeBlock(
content=content,
)
assert target.content == content.strip()
else:
with raises(CodeBlockTokenError):
CodeBlock(
content=content,
)
@mark.parametrize(
"case, result",
[
(r"{$a", False),
],
)
def test_edge_cases(case, result):
if result:
target = CodeBlock(
content=case,
)
assert target.content == case
else:
with raises(FunctionIdBlockSyntaxError):
CodeBlock(
content=case,
)
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}"