# 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}"