# Copyright (c) Microsoft. All rights reserved. import pytest from pytest import mark from semantic_kernel.contents import AuthorRole from semantic_kernel.contents.chat_history import ChatHistory from semantic_kernel.contents.chat_message_content import ChatMessageContent from semantic_kernel.contents.function_call_content import FunctionCallContent from semantic_kernel.contents.function_result_content import FunctionResultContent from semantic_kernel.exceptions import HandlebarsTemplateRenderException, HandlebarsTemplateSyntaxError from semantic_kernel.functions.kernel_arguments import KernelArguments from semantic_kernel.kernel import Kernel from semantic_kernel.prompt_template.handlebars_prompt_template import HandlebarsPromptTemplate from semantic_kernel.prompt_template.prompt_template_config import PromptTemplateConfig def create_handlebars_prompt_template( template: str, allow_dangerously_set_content: bool = False ) -> HandlebarsPromptTemplate: return HandlebarsPromptTemplate( prompt_template_config=PromptTemplateConfig( name="test", description="test", template=template, template_format="handlebars", ), allow_dangerously_set_content=allow_dangerously_set_content, ) def test_init(): template = HandlebarsPromptTemplate( prompt_template_config=PromptTemplateConfig( name="test", description="test", template="{{input}}", template_format="handlebars" ) ) assert template.prompt_template_config.template == "{{input}}" def test_init_fail(): with pytest.raises(HandlebarsTemplateSyntaxError): HandlebarsPromptTemplate( prompt_template_config=PromptTemplateConfig( name="test", description="test", template="{{(input)}}", template_format="handlebars" ) ) def test_init_template_validation_fail(): with pytest.raises(ValueError): HandlebarsPromptTemplate( prompt_template_config=PromptTemplateConfig( name="test", description="test", template="{{(input)}}", template_format="semantic-kernel" ) ) def test_config_without_prompt(): config = PromptTemplateConfig(name="test", description="test", template_format="handlebars") template = HandlebarsPromptTemplate(prompt_template_config=config) assert template._template_compiler is None async def test_render_without_prompt(kernel: Kernel): config = PromptTemplateConfig(name="test", description="test", template_format="handlebars") template = HandlebarsPromptTemplate(prompt_template_config=config) rendered = await template.render(kernel, None) assert rendered == "" async def test_it_renders_variables(kernel: Kernel): template = "Foo {{#if bar}}{{bar}}{{else}}No Bar{{/if}}" target = create_handlebars_prompt_template(template, allow_dangerously_set_content=True) rendered = await target.render(kernel, KernelArguments(bar="Bar")) assert rendered == "Foo Bar" rendered = await target.render(kernel, KernelArguments()) assert rendered == "Foo No Bar" async def test_it_renders_nested_variables(kernel: Kernel): template = "{{foo.bar}}" target = create_handlebars_prompt_template(template, allow_dangerously_set_content=True) rendered = await target.render(kernel, KernelArguments(foo={"bar": "Foo Bar"})) assert rendered == "Foo Bar" async def test_it_renders_with_comments(kernel: Kernel): template = "{{! This comment will not show up in the output}}{{bar}}" target = create_handlebars_prompt_template(template) rendered = await target.render(kernel, KernelArguments(bar="Bar")) assert rendered == "Bar" async def test_it_renders_fail(kernel: Kernel): template = "{{ plug-func 'test1'}}" target = create_handlebars_prompt_template(template) with pytest.raises(HandlebarsTemplateRenderException): await target.render(kernel, KernelArguments()) async def test_it_renders_list(kernel: Kernel): template = "List: {{#each items}}{{this}}{{/each}}" target = create_handlebars_prompt_template(template, allow_dangerously_set_content=True) rendered = await target.render(kernel, KernelArguments(items=["item1", "item2", "item3"])) assert rendered == "List: item1item2item3" async def test_it_renders_kernel_functions_arg_from_template(kernel: Kernel, decorated_native_function): kernel.add_function(plugin_name="plug", function=decorated_native_function) template = "Function: {{plug-getLightStatus arg1='test'}}" target = create_handlebars_prompt_template(template) rendered = await target.render(kernel) assert rendered == "Function: test" async def test_it_renders_kernel_functions_arg_from_arguments(kernel: Kernel, decorated_native_function): kernel.add_function(plugin_name="plug", function=decorated_native_function) template = "Function: {{plug-getLightStatus}}" target = create_handlebars_prompt_template(template) rendered = await target.render(kernel, KernelArguments(arg1="test")) assert rendered == "Function: test" @mark.parametrize( "function, input, expected", [ ("array", "'test1' 'test2' 'test3'", "['test1', 'test2', 'test3']"), ("range", "5", "[0, 1, 2, 3, 4]"), ("range", "0 5", "[0, 1, 2, 3, 4]"), ("range", "0 '5'", "[0, 1, 2, 3, 4]"), ("range", "0 5 1", "[0, 1, 2, 3, 4]"), ("range", "0 5 2", "[0, 2, 4]"), ("range", "0 5 1 1", "[]"), ("range", "'a' 5", "[0, 1, 2, 3, 4]"), ("concat", "'test1' 'test2' 'test3'", "test1test2test3"), ("or", "true false", "true"), ("add", "1 2", "3.0"), ("add", "1 2 3", "6.0"), ("subtract", "1 2 3", "-4.0"), ("equals", "1 2", "false"), ("equals", "1 1", "true"), ("equals", "'test1' 'test2'", "false"), ("equals", "'test1' 'test1'", "true"), ("less_than", "1 2", "true"), ("lessThan", "1 2", "true"), ("less_than", "2 1", "false"), ("less_than", "1 1", "false"), ("greater_than", "2 1", "true"), ("greaterThan", "2 1", "true"), ("greater_than", "1 2", "false"), ("greater_than", "2 2", "false"), ("less_than_or_equal", "1 2", "true"), ("lessThanOrEqual", "1 2", "true"), ("less_than_or_equal", "2 1", "false"), ("less_than_or_equal", "1 1", "true"), ("greater_than_or_equal", "1 2", "false"), ("greaterThanOrEqual", "1 2", "false"), ("greater_than_or_equal", "2 1", "true"), ("greater_than_or_equal", "1 1", "true"), ("camel_case", "'test_string'", "TestString"), ("camelCase", "'test_string'", "TestString"), ("snake_case", "'TestString'", "test_string"), ("snakeCase", "'TestString'", "test_string"), ], ) async def test_helpers(function, input, expected, kernel: Kernel): template = f"{{{{ {function} {input} }}}}" target = create_handlebars_prompt_template(template) rendered = await target.render(kernel, None) assert rendered == expected async def test_helpers_set_get(kernel: Kernel): template = """{{set name="arg" value="test"}}{{get 'arg'}} {{arg}}""" target = create_handlebars_prompt_template(template) rendered = await target.render(kernel, None) assert rendered == "test test" async def test_helpers_set_get_args(kernel: Kernel): template = """{{set "arg" "test"}}{{get 'arg'}} {{arg}}""" target = create_handlebars_prompt_template(template) rendered = await target.render(kernel, None) assert rendered == "test test" async def test_helpers_empty_get(kernel: Kernel): template = """{{get}}""" target = create_handlebars_prompt_template(template) rendered = await target.render(kernel, None) assert rendered == "" async def test_helpers_set_get_from_kernel_arguments(kernel: Kernel): template = """{{set name="arg" value=(get 'arg1') }}{{get 'arg'}} {{arg}} {{arg1}}""" target = create_handlebars_prompt_template(template) rendered = await target.render(kernel, KernelArguments(arg1="test")) assert rendered == "test test test" async def test_helpers_array_from_args(kernel: Kernel): template = """{{array arg1 arg2 arg3}}""" target = create_handlebars_prompt_template(template) rendered = await target.render(kernel, KernelArguments(arg1="test1", arg2="test2", arg3="test3")) assert rendered == "['test1', 'test2', 'test3']" async def test_helpers_double_open_close(kernel: Kernel): template = "{{double_open}}{{double_close}}" target = create_handlebars_prompt_template(template) rendered = await target.render(kernel, None) assert rendered == "{{}}" async def test_helpers_json(kernel: Kernel): template = "{{json input_json}}" target = create_handlebars_prompt_template(template, allow_dangerously_set_content=True) rendered = await target.render(kernel, KernelArguments(input_json={"key": "value"})) assert rendered == '{"key": "value"}' async def test_helpers_json_empty(kernel: Kernel): template = "{{json}}" target = create_handlebars_prompt_template(template) rendered = await target.render(kernel, None) assert rendered == "" async def test_helpers_message(kernel: Kernel): template = """ {{#each chat_history}} {{#message role=role}} {{~content~}} {{/message}} {{/each}} """ target = create_handlebars_prompt_template(template, allow_dangerously_set_content=True) chat_history = ChatHistory() chat_history.add_user_message("User message") chat_history.add_assistant_message("Assistant message") rendered = await target.render(kernel, KernelArguments(chat_history=chat_history)) assert "User message" in rendered assert "Assistant message" in rendered async def test_helpers_message_escapes_xml_metacharacters(kernel: Kernel): template = """ {{#each chat_history}} {{#message role=role}} {{~content~}} {{/message}} {{/each}} """ target = create_handlebars_prompt_template(template, allow_dangerously_set_content=True) chat_history = ChatHistory() chat_history.add_user_message('What does a < b & a > c & "d" mean?') rendered = await target.render(kernel, KernelArguments(chat_history=chat_history)) assert "<" in rendered assert "&" in rendered # ElementTree does not escape > in text content (valid XML); verify round-trip works regardless. assert "a > c" in rendered or "a > c" in rendered assert '"d"' in rendered assert ChatHistory.from_rendered_prompt(rendered) == chat_history async def test_helpers_message_uses_text_element(kernel: Kernel): """Verify handlebars {{#message}} wraps content in like the Jinja2 path.""" template = """ {{#each chat_history}} {{#message role=role}} {{~content~}} {{/message}} {{/each}} """ target = create_handlebars_prompt_template(template, allow_dangerously_set_content=True) chat_history = ChatHistory() chat_history.add_user_message("User message") chat_history.add_assistant_message("Assistant message") rendered = await target.render(kernel, KernelArguments(chat_history=chat_history)) assert 'User message' in rendered assert 'Assistant message' in rendered chat_history2 = ChatHistory.from_rendered_prompt(rendered) assert chat_history2 == chat_history async def test_helpers_message_empty_content(kernel: Kernel): """Empty message content should produce , not self-closing.""" template = """ {{#each chat_history}} {{#message role=role}} {{~content~}} {{/message}} {{/each}} """ target = create_handlebars_prompt_template(template, allow_dangerously_set_content=True) chat_history = ChatHistory() chat_history.add_user_message("") rendered = await target.render(kernel, KernelArguments(chat_history=chat_history)) assert "" not in rendered assert ChatHistory.from_rendered_prompt(rendered) is not None async def test_helpers_message_fallback_empty_content(kernel: Kernel): """Fallback path (non-ChatMessageContent context) with empty block content. Should produce , not self-closing. """ template = '{{#message role="user"}}{{/message}}' target = create_handlebars_prompt_template(template, allow_dangerously_set_content=True) rendered = await target.render(kernel, KernelArguments()) assert '' in rendered assert "/>" not in rendered assert ChatHistory.from_rendered_prompt(rendered) is not None async def test_helpers_message_fallback_with_content(kernel: Kernel): """Fallback path wraps block content in a child element.""" template = '{{#message role="user"}}Hello world{{/message}}' target = create_handlebars_prompt_template(template, allow_dangerously_set_content=True) rendered = await target.render(kernel, KernelArguments()) assert 'Hello world' in rendered chat_history = ChatHistory.from_rendered_prompt(rendered) assert chat_history is not None assert len(chat_history) == 1 assert chat_history[0].content == "Hello world" async def test_helpers_message_escapes_greater_than(kernel: Kernel): """ElementTree does not escape > in text; verify round-trip still works.""" template = """ {{#each chat_history}} {{#message role=role}} {{~content~}} {{/message}} {{/each}} """ target = create_handlebars_prompt_template(template, allow_dangerously_set_content=True) chat_history = ChatHistory() chat_history.add_user_message("Is a > b true?") rendered = await target.render(kernel, KernelArguments(chat_history=chat_history)) assert "a > b" in rendered or "a > b" in rendered assert ChatHistory.from_rendered_prompt(rendered) == chat_history async def test_helpers_message_to_prompt(kernel: Kernel): template = """{{#each chat_history}}{{message_to_prompt}} {{/each}}""" target = create_handlebars_prompt_template(template, allow_dangerously_set_content=True) chat_history = ChatHistory() chat_history.add_user_message("User message") chat_history.add_message( ChatMessageContent(role=AuthorRole.ASSISTANT, items=[FunctionCallContent(id="1", name="plug-test")]) ) chat_history.add_message( ChatMessageContent( role=AuthorRole.TOOL, items=[FunctionResultContent(id="1", name="plug-test", result="Tool message")] ) ) rendered = await target.render(kernel, KernelArguments(chat_history=chat_history)) assert "text" in rendered assert "User message" in rendered assert "function_call" in rendered assert "plug-test" in rendered assert "function_result" in rendered assert "Tool message" in rendered async def test_helpers_message_to_prompt_other(kernel: Kernel): template = """{{#each other_list}}{{message_to_prompt}} {{/each}}""" target = create_handlebars_prompt_template(template, allow_dangerously_set_content=True) other_list = ["test1", "test2"] rendered = await target.render(kernel, KernelArguments(other_list=other_list)) assert rendered.strip() == """test1 test2""" async def test_helpers_messageToPrompt_other(kernel: Kernel): template = """{{#each other_list}}{{messageToPrompt}} {{/each}}""" target = create_handlebars_prompt_template(template, allow_dangerously_set_content=True) other_list = ["test1", "test2"] rendered = await target.render(kernel, KernelArguments(other_list=other_list)) assert rendered.strip() == """test1 test2""" async def test_helpers_unless(kernel: Kernel): template = """{{#unless test}}test2{{/unless}}""" target = create_handlebars_prompt_template(template) rendered = await target.render(kernel, KernelArguments(test2="test2")) assert rendered.strip() == """test2""" async def test_helpers_with(kernel: Kernel): template = """{{#with test}}{{test1}}{{/with}}""" target = create_handlebars_prompt_template(template, allow_dangerously_set_content=True) rendered = await target.render(kernel, KernelArguments(test={"test1": "test2"})) assert rendered.strip() == """test2""" async def test_helpers_lookup(kernel: Kernel): template = """{{lookup test 'test1'}}""" target = create_handlebars_prompt_template(template, allow_dangerously_set_content=True) rendered = await target.render(kernel, KernelArguments(test={"test1": "test2"})) assert rendered.strip() == """test2""" async def test_helpers_chat_history_messages(kernel: Kernel): template = """{{messages chat_history}}""" target = create_handlebars_prompt_template(template, allow_dangerously_set_content=True) chat_history = ChatHistory() chat_history.add_user_message("User message") chat_history.add_assistant_message("Assistant message") rendered = await target.render(kernel, KernelArguments(chat_history=chat_history)) assert ( rendered.strip() == """User messageAssistant message""" # noqa E501 ) async def test_helpers_chat_history_not_chat_history(kernel: Kernel): template = """{{messages chat_history}}""" target = create_handlebars_prompt_template(template, allow_dangerously_set_content=True) chat_history = "this is not a chathistory object" rendered = await target.render(kernel, KernelArguments(chat_history=chat_history)) assert rendered.strip() == "" async def test_complex_type_encoding_throws_exception(): unsafe_input = "This is the newer system message" template = """This is the system message {{unsafe_input}}""" from semantic_kernel.prompt_template.input_variable import InputVariable target = HandlebarsPromptTemplate( prompt_template_config=PromptTemplateConfig( name="test", description="test", template=template, template_format="handlebars", input_variables=[InputVariable(name="unsafe_input", allow_dangerously_set_content=False)], ) ) argument_value = {"prompt": unsafe_input} with pytest.raises(NotImplementedError) as exc_info: await target.render(Kernel(), KernelArguments(unsafe_input=argument_value)) assert "Argument 'unsafe_input'" in str(exc_info.value) async def test_safe_types_are_allowed(): template = """Number: {{number}}, Boolean: {{flag}}""" target = create_handlebars_prompt_template(template) result = await target.render(Kernel(), KernelArguments(number=42, flag=True)) assert "42" in result assert "true" in result