chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,581 @@
|
||||
# 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}"
|
||||
Reference in New Issue
Block a user