# Copyright (c) Microsoft. All rights reserved. import os import tempfile from copy import deepcopy from unittest.mock import patch import pytest from semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion import OpenAIChatCompletion from semantic_kernel.connectors.ai.open_ai.services.open_ai_text_completion import OpenAITextCompletion from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings from semantic_kernel.const import METADATA_EXCEPTION_KEY from semantic_kernel.contents import AuthorRole from semantic_kernel.contents.chat_message_content import ChatMessageContent from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent from semantic_kernel.contents.text_content import TextContent from semantic_kernel.exceptions import FunctionInitializationError from semantic_kernel.filters.functions.function_invocation_context import FunctionInvocationContext from semantic_kernel.filters.kernel_filters_extension import _rebuild_function_invocation_context from semantic_kernel.filters.prompts.prompt_render_context import PromptRenderContext from semantic_kernel.functions.kernel_arguments import KernelArguments from semantic_kernel.functions.kernel_function_from_prompt import KernelFunctionFromPrompt from semantic_kernel.kernel import Kernel from semantic_kernel.prompt_template.input_variable import InputVariable from semantic_kernel.prompt_template.kernel_prompt_template import KernelPromptTemplate from semantic_kernel.prompt_template.prompt_template_config import PromptTemplateConfig def test_init_minimal_prompt(): function = KernelFunctionFromPrompt( function_name="test", plugin_name="test", prompt="test", ) assert function.name == "test" assert function.plugin_name == "test" assert function.description is None assert function.prompt_template.prompt_template_config.template == "test" def test_init_minimal_prompt_template(): function = KernelFunctionFromPrompt( function_name="test", plugin_name="test", prompt_template=KernelPromptTemplate(prompt_template_config=PromptTemplateConfig(template="test")), ) assert function.name == "test" assert function.plugin_name == "test" assert function.description is None assert function.prompt_template.prompt_template_config.template == "test" def test_init_minimal_prompt_template_config(): function = KernelFunctionFromPrompt( function_name="test", plugin_name="test", prompt_template_config=PromptTemplateConfig(template="test") ) assert function.name == "test" assert function.plugin_name == "test" assert function.description is None assert function.prompt_template.prompt_template_config.template == "test" def test_init_no_prompt(): with pytest.raises(FunctionInitializationError): KernelFunctionFromPrompt( function_name="test", plugin_name="test", ) def test_init_invalid_name(): with pytest.raises(FunctionInitializationError): KernelFunctionFromPrompt(function_name="test func", plugin_name="test", prompt="test") def test_init_prompt_execution_settings_none(): function = KernelFunctionFromPrompt( function_name="test", plugin_name="test", prompt="test", prompt_execution_settings=None, ) assert function.name == "test" assert function.plugin_name == "test" assert function.description is None assert function.prompt_template.prompt_template_config.template == "test" def test_init_prompt_execution_settings_none_with_prompt_template(): function = KernelFunctionFromPrompt( function_name="test", plugin_name="test", prompt_template=KernelPromptTemplate( prompt_template_config=PromptTemplateConfig(template="test", execution_settings={}) ), prompt_execution_settings=None, ) assert function.name == "test" assert function.plugin_name == "test" assert function.description is None assert function.prompt_template.prompt_template_config.template == "test" def test_init_prompt_execution_settings(): function = KernelFunctionFromPrompt( function_name="test", plugin_name="test", prompt="test", prompt_execution_settings=PromptExecutionSettings(service_id="test"), ) assert function.name == "test" assert function.plugin_name == "test" assert function.description is None assert function.prompt_template.prompt_template_config.template == "test" def test_init_prompt_execution_settings_list(): function = KernelFunctionFromPrompt( function_name="test", plugin_name="test", prompt="test", prompt_execution_settings=[PromptExecutionSettings(service_id="test")], ) assert function.name == "test" assert function.plugin_name == "test" assert function.description is None assert function.prompt_template.prompt_template_config.template == "test" def test_init_prompt_execution_settings_dict(): function = KernelFunctionFromPrompt( function_name="test", plugin_name="test", prompt="test", prompt_execution_settings={"test": PromptExecutionSettings(service_id="test")}, ) assert function.name == "test" assert function.plugin_name == "test" assert function.description is None assert function.prompt_template.prompt_template_config.template == "test" async def test_invoke_chat_stream(openai_unit_test_env): kernel = Kernel() kernel.add_service(OpenAIChatCompletion(service_id="test", ai_model_id="test")) function = KernelFunctionFromPrompt( function_name="test", plugin_name="test", prompt="test", prompt_execution_settings=PromptExecutionSettings(service_id="test"), ) # This part remains unchanged - for synchronous mocking example with patch( "semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion.OpenAIChatCompletion.get_chat_message_contents" ) as mock: mock.return_value = [ChatMessageContent(role=AuthorRole.ASSISTANT, content="test", metadata={})] result = await function.invoke(kernel=kernel) assert str(result) == "test" with patch( "semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion.OpenAIChatCompletion.get_streaming_chat_message_contents" ) as mock: mock.return_value = [ StreamingChatMessageContent(choice_index=0, role=AuthorRole.ASSISTANT, content="test", metadata={}) ] async for result in function.invoke_stream(kernel=kernel): assert str(result) == "test" async def test_invoke_exception(openai_unit_test_env): kernel = Kernel() kernel.add_service(OpenAIChatCompletion(service_id="test", ai_model_id="test")) function = KernelFunctionFromPrompt( function_name="test", plugin_name="test", prompt="test", prompt_execution_settings=PromptExecutionSettings(service_id="test"), ) with patch( "semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion.OpenAIChatCompletion.get_chat_message_contents", side_effect=Exception, ) as mock: mock.return_value = [ChatMessageContent(role=AuthorRole.ASSISTANT, content="test", metadata={})] with pytest.raises(Exception, match="test"): await function.invoke(kernel=kernel) with patch( "semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion.OpenAIChatCompletion.get_streaming_chat_message_contents", side_effect=Exception, ) as mock: mock.return_value = [ StreamingChatMessageContent(choice_index=0, role=AuthorRole.ASSISTANT, content="test", metadata={}) ] with pytest.raises(Exception): async for result in function.invoke_stream(kernel=kernel): assert isinstance(result.metadata[METADATA_EXCEPTION_KEY], Exception) async def test_invoke_text(openai_unit_test_env): kernel = Kernel() kernel.add_service(OpenAITextCompletion(service_id="test", ai_model_id="test")) function = KernelFunctionFromPrompt( function_name="test", plugin_name="test", prompt="test", prompt_execution_settings=PromptExecutionSettings(service_id="test"), ) with patch( "semantic_kernel.connectors.ai.open_ai.services.open_ai_text_completion.OpenAITextCompletion.get_text_contents", ) as mock: mock.return_value = [TextContent(text="test", metadata={})] result = await function.invoke(kernel=kernel) assert str(result) == "test" with patch( "semantic_kernel.connectors.ai.open_ai.services.open_ai_text_completion.OpenAITextCompletion.get_streaming_text_contents", ) as mock: mock.return_value = [TextContent(text="test", metadata={})] async for result in function.invoke_stream(kernel=kernel): assert str(result) == "test" async def test_invoke_exception_text(openai_unit_test_env): kernel = Kernel() kernel.add_service(OpenAITextCompletion(service_id="test", ai_model_id="test")) function = KernelFunctionFromPrompt( function_name="test", plugin_name="test", prompt="test", prompt_execution_settings=PromptExecutionSettings(service_id="test"), ) with patch( "semantic_kernel.connectors.ai.open_ai.services.open_ai_text_completion.OpenAITextCompletion.get_text_contents", side_effect=Exception, ) as mock: mock.return_value = [TextContent(text="test", metadata={})] with pytest.raises(Exception, match="test"): await function.invoke(kernel=kernel) with patch( "semantic_kernel.connectors.ai.open_ai.services.open_ai_text_completion.OpenAITextCompletion.get_streaming_text_contents", side_effect=Exception, ) as mock: mock.return_value = [] with pytest.raises(Exception): async for result in function.invoke_stream(kernel=kernel): assert isinstance(result.metadata[METADATA_EXCEPTION_KEY], Exception) async def test_invoke_defaults(openai_unit_test_env): kernel = Kernel() kernel.add_service(OpenAIChatCompletion(service_id="test", ai_model_id="test")) function = KernelFunctionFromPrompt( function_name="test", plugin_name="test", prompt_template_config=PromptTemplateConfig( template="{{$input}}", input_variables=[InputVariable(name="input", type="str", default="test", is_required=False)], ), prompt_execution_settings=PromptExecutionSettings(service_id="test"), ) with patch( "semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion.OpenAIChatCompletion.get_chat_message_contents" ) as mock: mock.return_value = [ChatMessageContent(role=AuthorRole.ASSISTANT, content="test", metadata={})] result = await function.invoke(kernel=kernel) assert str(result) == "test" def test_create_with_multiple_settings(): function = KernelFunctionFromPrompt( function_name="test", plugin_name="test", prompt_template_config=PromptTemplateConfig( template="test", execution_settings=[ PromptExecutionSettings(service_id="test", temperature=0.0), PromptExecutionSettings(service_id="test2", temperature=1.0), ], ), ) assert ( function.prompt_template.prompt_template_config.execution_settings["test"].extension_data["temperature"] == 0.0 ) assert ( function.prompt_template.prompt_template_config.execution_settings["test2"].extension_data["temperature"] == 1.0 ) async def test_create_with_multiple_settings_one_service_registered(openai_unit_test_env): kernel = Kernel() kernel.add_service(OpenAIChatCompletion(service_id="test2", ai_model_id="test")) function = KernelFunctionFromPrompt( function_name="test", plugin_name="test", prompt_template_config=PromptTemplateConfig( template="test", execution_settings=[ PromptExecutionSettings(service_id="test", temperature=0.0), PromptExecutionSettings(service_id="test2", temperature=1.0), ], ), ) with patch( "semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion.OpenAIChatCompletion.get_chat_message_contents" ) as mock: mock.return_value = [ChatMessageContent(role=AuthorRole.ASSISTANT, content="test", metadata={})] result = await function.invoke(kernel=kernel) assert str(result) == "test" def test_from_yaml_fail(): with pytest.raises(FunctionInitializationError): KernelFunctionFromPrompt.from_yaml("template_format: something_else") def test_from_directory_prompt_only(): with pytest.raises(FunctionInitializationError): KernelFunctionFromPrompt.from_directory( path=os.path.join( os.path.dirname(__file__), "../../assets", "test_plugins", "TestPlugin", "TestFunctionPromptOnly", ), plugin_name="test", ) def test_from_directory_config_only(): with pytest.raises(FunctionInitializationError): KernelFunctionFromPrompt.from_directory( path=os.path.join( os.path.dirname(__file__), "../../assets", "test_plugins", "TestPlugin", "TestFunctionConfigOnly", ), plugin_name="test", ) async def test_prompt_render(kernel: Kernel, openai_unit_test_env): kernel.add_service(OpenAIChatCompletion(service_id="default", ai_model_id="test")) function = KernelFunctionFromPrompt( function_name="test", plugin_name="test", prompt="test", template_format="semantic-kernel", ) _rebuild_function_invocation_context() context = FunctionInvocationContext(function=function, kernel=kernel, arguments=KernelArguments()) prompt_render_result = await function._render_prompt(context) assert prompt_render_result.rendered_prompt == "test" async def test_prompt_render_with_filter(kernel: Kernel, openai_unit_test_env): kernel.add_service(OpenAIChatCompletion(service_id="default", ai_model_id="test")) @kernel.filter("prompt_rendering") async def prompt_rendering_filter(context: PromptRenderContext, next): await next(context) context.rendered_prompt = f"preface {context.rendered_prompt or ''}" function = KernelFunctionFromPrompt( function_name="test", plugin_name="test", prompt="test", template_format="semantic-kernel", ) _rebuild_function_invocation_context() context = FunctionInvocationContext(function=function, kernel=kernel, arguments=KernelArguments()) prompt_render_result = await function._render_prompt(context) assert prompt_render_result.rendered_prompt == "preface test" @pytest.mark.parametrize( ("mode"), [ ("python"), ("json"), ], ) def test_function_model_dump(mode: str): function = KernelFunctionFromPrompt( function_name="test", plugin_name="test", prompt="test", template_format="semantic-kernel", prompt_template_config=PromptTemplateConfig( template="test", input_variables=[InputVariable(name="input", type="str", default="test", is_required=False)], ), ) model_dump = function.model_dump(mode=mode) assert isinstance(model_dump, dict) assert "metadata" in model_dump assert len(model_dump["metadata"]["parameters"]) == 1 def test_function_model_dump_json(): function = KernelFunctionFromPrompt( function_name="test", plugin_name="test", prompt="test", template_format="semantic-kernel", ) model_dump_json = function.model_dump_json() assert isinstance(model_dump_json, str) assert "test" in model_dump_json def test_from_directory_utf8_encoding_default(): """Test loading plugin with default UTF-8 encoding.""" with tempfile.TemporaryDirectory() as temp_dir: prompt_path = os.path.join(temp_dir, "skprompt.txt") config_path = os.path.join(temp_dir, "config.json") # UTF-8 content with international characters prompt_content = """Hello! I can help with questions in multiple languages: English: Hello world! Spanish: ¡Hola mundo! Chinese: 你好世界! Japanese: こんにちは世界! Question: {{$input}} """ config_content = """{ "schema": 1, "description": "A multilingual assistant function", "input_variables": [ { "name": "input", "description": "User's question", "required": true } ] }""" # Write files with UTF-8 encoding with open(prompt_path, "w", encoding="utf-8") as f: f.write(prompt_content) with open(config_path, "w", encoding="utf-8") as f: f.write(config_content) # Test default behavior (should use UTF-8) function = KernelFunctionFromPrompt.from_directory(temp_dir) assert function.name == os.path.basename(temp_dir) assert function.description == "A multilingual assistant function" assert "你好世界" in function.prompt_template.prompt_template_config.template assert "こんにちは世界" in function.prompt_template.prompt_template_config.template def test_from_directory_explicit_utf8_encoding(): """Test loading plugin with explicit UTF-8 encoding.""" with tempfile.TemporaryDirectory() as temp_dir: prompt_path = os.path.join(temp_dir, "skprompt.txt") config_path = os.path.join(temp_dir, "config.json") prompt_content = "Hello with UTF-8 characters: ñáéíóú {{$input}}" config_content = '{"schema": 1, "description": "Test with UTF-8 characters"}' with open(prompt_path, "w", encoding="utf-8") as f: f.write(prompt_content) with open(config_path, "w", encoding="utf-8") as f: f.write(config_content) # Test explicit UTF-8 encoding function = KernelFunctionFromPrompt.from_directory(temp_dir, encoding="utf-8") assert function.description == "Test with UTF-8 characters" assert "ñáéíóú" in function.prompt_template.prompt_template_config.template def test_from_directory_latin1_encoding(): """Test loading plugin with Latin-1 encoding.""" with tempfile.TemporaryDirectory() as temp_dir: prompt_path = os.path.join(temp_dir, "skprompt.txt") config_path = os.path.join(temp_dir, "config.json") # Content with Latin-1 characters (Western European) prompt_content = """Assistant for Western European languages: French: café, naïve, résumé German: Müller, Größe, weiß Spanish: niño, señora, años Question: {{$input}} """ config_content = """{ "schema": 1, "description": "Western European language assistant", "input_variables": [ { "name": "input", "description": "User's question", "required": true } ] }""" # Write files with Latin-1 encoding with open(prompt_path, "w", encoding="latin-1") as f: f.write(prompt_content) with open(config_path, "w", encoding="latin-1") as f: f.write(config_content) # Load with Latin-1 encoding function = KernelFunctionFromPrompt.from_directory(temp_dir, encoding="latin-1") assert function.description == "Western European language assistant" assert "café" in function.prompt_template.prompt_template_config.template assert "Müller" in function.prompt_template.prompt_template_config.template assert "niño" in function.prompt_template.prompt_template_config.template def test_from_directory_cp1252_encoding(): """Test loading plugin with Windows-1252 encoding.""" with tempfile.TemporaryDirectory() as temp_dir: prompt_path = os.path.join(temp_dir, "skprompt.txt") config_path = os.path.join(temp_dir, "config.json") # Content with Windows-1252 specific characters prompt_content = """Windows text processing assistant: Smart quotes: "Hello" and 'world' Em dash: Yes—absolutely! Ellipsis: Wait… Question: {{$input}} """ config_content = """{ "schema": 1, "description": "Windows text processing assistant", "input_variables": [ { "name": "input", "description": "User's question about text processing", "required": true } ] }""" # Write files with Windows-1252 encoding with open(prompt_path, "w", encoding="cp1252") as f: f.write(prompt_content) with open(config_path, "w", encoding="cp1252") as f: f.write(config_content) # Load with Windows-1252 encoding function = KernelFunctionFromPrompt.from_directory(temp_dir, encoding="cp1252") assert function.description == "Windows text processing assistant" assert '"Hello"' in function.prompt_template.prompt_template_config.template assert "Yes—absolutely" in function.prompt_template.prompt_template_config.template assert "Wait…" in function.prompt_template.prompt_template_config.template def test_from_directory_with_plugin_name_and_encoding(): """Test loading plugin with both plugin name and encoding specified.""" with tempfile.TemporaryDirectory() as temp_dir: prompt_path = os.path.join(temp_dir, "skprompt.txt") config_path = os.path.join(temp_dir, "config.json") prompt_content = "Simple assistant: {{$input}}" config_content = '{"schema": 1, "description": "Simple assistant"}' with open(prompt_path, "w", encoding="utf-8") as f: f.write(prompt_content) with open(config_path, "w", encoding="utf-8") as f: f.write(config_content) # Load with both plugin name and encoding specified function = KernelFunctionFromPrompt.from_directory( path=temp_dir, plugin_name="MyCustomPlugin", encoding="utf-8" ) assert function.metadata.plugin_name == "MyCustomPlugin" assert function.description == "Simple assistant" assert function.prompt_template.prompt_template_config.template == "Simple assistant: {{$input}}" def test_from_directory_encoding_error_handling(): """Test that incorrect encoding raises appropriate error.""" with tempfile.TemporaryDirectory() as temp_dir: prompt_path = os.path.join(temp_dir, "skprompt.txt") config_path = os.path.join(temp_dir, "config.json") # Write UTF-8 content prompt_content = "Hello with UTF-8: 你好世界 {{$input}}" config_content = '{"schema": 1, "description": "UTF-8 content"}' with open(prompt_path, "w", encoding="utf-8") as f: f.write(prompt_content) with open(config_path, "w", encoding="utf-8") as f: f.write(config_content) # Try to read with ASCII encoding - should fail with pytest.raises(UnicodeDecodeError): KernelFunctionFromPrompt.from_directory(temp_dir, encoding="ascii") def test_from_directory_backward_compatibility(): """Test that existing code without encoding parameter still works.""" with tempfile.TemporaryDirectory() as temp_dir: prompt_path = os.path.join(temp_dir, "skprompt.txt") config_path = os.path.join(temp_dir, "config.json") prompt_content = "Basic ASCII content: {{$input}}" config_content = '{"schema": 1, "description": "Basic function"}' with open(prompt_path, "w", encoding="utf-8") as f: f.write(prompt_content) with open(config_path, "w", encoding="utf-8") as f: f.write(config_content) # Test that old calling style still works function = KernelFunctionFromPrompt.from_directory(temp_dir) assert function.description == "Basic function" assert function.prompt_template.prompt_template_config.template == "Basic ASCII content: {{$input}}" def test_kernel_function_from_prompt_deepcopy(): """Test deepcopying a KernelFunctionFromPrompt.""" function = KernelFunctionFromPrompt( function_name="test_function", plugin_name="test_plugin", prompt="Hello, world!", description="A test function.", ) copied_function = deepcopy(function) assert copied_function is not function assert copied_function.name == function.name assert copied_function.plugin_name == function.plugin_name assert copied_function.description == function.description assert copied_function.prompt_template.prompt_template_config.template == ( function.prompt_template.prompt_template_config.template ) assert copied_function.prompt_template is not function.prompt_template