# Copyright (c) Microsoft. All rights reserved. import pytest from openai.types.chat.chat_completion import ChatCompletion, Choice from openai.types.chat.chat_completion_message import ChatCompletionMessage 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.contents.image_content import ImageContent from semantic_kernel.contents.text_content import TextContent from semantic_kernel.contents.utils.author_role import AuthorRole from semantic_kernel.exceptions import ContentInitializationError from semantic_kernel.functions.kernel_arguments import KernelArguments 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 @pytest.fixture def mock_chat_completion_response() -> ChatCompletion: return ChatCompletion( id="test_id", choices=[ Choice(index=0, message=ChatCompletionMessage(content="test", role="assistant"), finish_reason="stop") ], created=0, model="test", object="chat.completion", ) def test_init_with_system_message_only(): system_msg = "test message" chat_history = ChatHistory(system_message=system_msg) assert len(chat_history.messages) == 1 assert chat_history.messages[0].content == system_msg def test_init_with_messages_only(): msgs = [ChatMessageContent(role=AuthorRole.USER, content=f"Message {i}") for i in range(3)] chat_history = ChatHistory(messages=msgs) assert chat_history.messages == msgs, "Chat history should contain exactly the provided messages" def test_init_with_messages_and_system_message(): system_msg = "a test system prompt" msgs = [ChatMessageContent(role=AuthorRole.USER, content=f"Message {i}") for i in range(3)] chat_history = ChatHistory(messages=msgs, system_message=system_msg) assert chat_history.messages[0].role == AuthorRole.SYSTEM, "System message should be the first in history" assert chat_history.messages[0].content == system_msg, "System message should be the first in history" assert chat_history.messages[1:] == msgs, "Remaining messages should follow the system message" def test_init_without_messages_and_system_message(chat_history: ChatHistory): assert chat_history.messages == [], "Chat history should be empty if no messages and system_message are provided" def test_add_system_message(chat_history: ChatHistory): content = "System message" chat_history.add_system_message(content) assert chat_history.messages[-1].content == content assert chat_history.messages[-1].role == AuthorRole.SYSTEM def test_add_system_message_item(chat_history: ChatHistory): content = [TextContent(text="System message")] chat_history.add_system_message(content) assert chat_history.messages[-1].content == str(content[0]) assert chat_history.messages[-1].role == AuthorRole.SYSTEM def test_add_system_message_at_init(): content = "System message" chat_history = ChatHistory(system_message=content) assert chat_history.messages[-1].content == content assert chat_history.messages[-1].role == AuthorRole.SYSTEM def test_add_user_message(chat_history: ChatHistory): content = "User message" chat_history.add_user_message(content) assert chat_history.messages[-1].content == content assert chat_history.messages[-1].role == AuthorRole.USER def test_add_user_message_list(chat_history: ChatHistory): content = [TextContent(text="User message")] chat_history.add_user_message(content) assert chat_history.messages[-1].content == content[0].text assert chat_history.messages[-1].role == AuthorRole.USER def test_add_assistant_message(chat_history: ChatHistory): content = "Assistant message" chat_history.add_assistant_message(content) assert chat_history.messages[-1].content == content assert chat_history.messages[-1].role == AuthorRole.ASSISTANT def test_add_assistant_message_list(chat_history: ChatHistory): content = [TextContent(text="Assistant message")] chat_history.add_assistant_message(content) assert chat_history.messages[-1].content == content[0].text assert chat_history.messages[-1].role == AuthorRole.ASSISTANT def test_add_tool_message_raises_without_tool_call_id(chat_history: ChatHistory): content = "Tool message" with pytest.raises(ContentInitializationError): chat_history.add_tool_message(content) def test_add_tool_message(chat_history: ChatHistory): content = "Tool message" chat_history.add_tool_message(content, tool_call_id="call_123") def test_add_tool_message_to_dict_succeeds(chat_history: ChatHistory): content = "Tool message" chat_history.add_tool_message(content, tool_call_id="call_123", function_name="test_function") assert chat_history.messages[-1].role == AuthorRole.TOOL msg = chat_history.messages[-1] assert isinstance(msg.items[0], FunctionResultContent) assert msg.items[0].function_name == "test_function" result = msg.to_dict() assert result["content"] == content assert result["role"] == AuthorRole.TOOL assert result["tool_call_id"] == "call_123" def test_add_tool_message_list(chat_history: ChatHistory): content = [FunctionResultContent(id="test", result="Tool message")] chat_history.add_tool_message(content) assert chat_history.messages[-1].items[0].result == content[0].result assert chat_history.messages[-1].role == AuthorRole.TOOL def test_add_message(chat_history: ChatHistory): content = "Test message" role = AuthorRole.USER encoding = "utf-8" chat_history.add_message(message={"role": role, "content": content}, encoding=encoding, metadata={"test": "test"}) assert chat_history.messages[-1].content == content assert chat_history.messages[-1].role == role assert chat_history.messages[-1].encoding == encoding assert chat_history.messages[-1].metadata == {"test": "test"} def test_add_message_with_image(chat_history: ChatHistory): content = "Test message" role = AuthorRole.USER encoding = "utf-8" chat_history.add_message( ChatMessageContent( role=role, items=[ TextContent(text=content), ImageContent(uri="https://test/"), ], encoding=encoding, ) ) assert chat_history.messages[-1].content == content assert chat_history.messages[-1].role == role assert chat_history.messages[-1].encoding == encoding assert str(chat_history.messages[-1].items[1].uri) == "https://test/" def test_add_message_invalid_message(chat_history: ChatHistory): content = "Test message" with pytest.raises(ContentInitializationError): chat_history.add_message(message={"content": content}) def test_add_message_invalid_type(chat_history: ChatHistory): content = "Test message" with pytest.raises(ContentInitializationError): chat_history.add_message(message=content) def test_remove_message(chat_history: ChatHistory): content = "Message to remove" role = AuthorRole.USER encoding = "utf-8" message = ChatMessageContent(role=role, content=content, encoding=encoding) chat_history.messages.append(message) assert chat_history.remove_message(message) is True assert message not in chat_history.messages def test_remove_message_invalid(chat_history: ChatHistory): content = "Message to remove" role = AuthorRole.USER encoding = "utf-8" message = ChatMessageContent(role=role, content=content, encoding=encoding) chat_history.messages.append(message) assert chat_history.remove_message("random") is False def test_len(chat_history: ChatHistory): content = "Message" chat_history.add_user_message(content) chat_history.add_system_message(content) assert len(chat_history) == 2 def test_getitem(chat_history: ChatHistory): content = "Message for index" chat_history.add_user_message(content) assert chat_history[0].content == content def test_contains(chat_history: ChatHistory): content = "Message to check" role = AuthorRole.USER encoding = "utf-8" message = ChatMessageContent(role=role, content=content, encoding=encoding) chat_history.messages.append(message) assert message in chat_history def test_iter(chat_history: ChatHistory): messages = ["Message 1", "Message 2"] for msg in messages: chat_history.add_user_message(msg) for i, message in enumerate(chat_history): assert message.content == messages[i] def test_eq(): # Create two instances of ChatHistory chat_history1 = ChatHistory() chat_history2 = ChatHistory() # Populate both instances with the same set of messages messages = [("Message 1", AuthorRole.USER), ("Message 2", AuthorRole.ASSISTANT)] for content, role in messages: chat_history1.add_message({"role": role, "content": content}) chat_history2.add_message({"role": role, "content": content}) # Assert that the two instances are considered equal assert chat_history1 == chat_history2 # Additionally, test inequality by adding an extra message to one of the histories chat_history1.add_user_message("Extra message") assert chat_history1 != chat_history2 def test_eq_invalid(chat_history: ChatHistory): # Populate both instances with the same set of messages messages = [("Message 1", AuthorRole.USER), ("Message 2", AuthorRole.ASSISTANT)] for content, role in messages: chat_history.add_message({"role": role, "content": content}) assert chat_history != "other" def test_dump(): system_msg = "a test system prompt" chat_history = ChatHistory( messages=[ChatMessageContent(role=AuthorRole.USER, content="Message")], system_message=system_msg ) dump = chat_history.model_dump(exclude_none=True) assert dump is not None assert dump["messages"][0]["role"] == AuthorRole.SYSTEM assert dump["messages"][0]["items"][0]["text"] == system_msg assert dump["messages"][1]["role"] == AuthorRole.USER assert dump["messages"][1]["items"][0]["text"] == "Message" def test_serialize(): system_msg = "a test system prompt" chat_history = ChatHistory( messages=[ ChatMessageContent( role=AuthorRole.USER, items=[ TextContent(text="Message"), ImageContent(uri="http://test.com/image.jpg"), ImageContent(data_uri="data:image/jpeg;base64,dGVzdF9kYXRh"), ], ) ], system_message=system_msg, ) json_str = chat_history.serialize() assert json_str is not None assert ( json_str == '{\n "messages": [\n {\n "metadata": {},\n "content_type": "message",\n "role": "system",\n "items": [\n {\n "metadata": {},\n "content_type": "text",\n "text": "a test system prompt"\n }\n ]\n },\n {\n "metadata": {},\n "content_type": "message",\n "role": "user",\n "items": [\n {\n "metadata": {},\n "content_type": "text",\n "text": "Message"\n },\n {\n "metadata": {},\n "content_type": "image",\n "uri": "http://test.com/image.jpg",\n "data_uri": ""\n },\n {\n "metadata": {},\n "content_type": "image",\n "data_uri": "data:image/jpeg;base64,dGVzdF9kYXRh"\n }\n ]\n }\n ]\n}' # noqa: E501 ) def test_serialize_and_deserialize_to_chat_history(mock_chat_completion_response: ChatCompletion): system_msg = "a test system prompt" msgs = [ ChatMessageContent( role=AuthorRole.USER, content=f"Message {i}", inner_content=mock_chat_completion_response, ) for i in range(3) ] chat_history = ChatHistory(messages=msgs, system_message=system_msg) json_str = chat_history.serialize() new_chat_history = ChatHistory.restore_chat_history(json_str) assert len(new_chat_history.messages) == len(chat_history.messages) for original_msg, restored_msg in zip(chat_history.messages, new_chat_history.messages): assert original_msg.role == restored_msg.role assert original_msg.content == restored_msg.content def test_deserialize_invalid_json_raises_exception(): invalid_json = "invalid json" with pytest.raises(ContentInitializationError): ChatHistory.restore_chat_history(invalid_json) def test_chat_history_to_prompt_empty(chat_history: ChatHistory): prompt = str(chat_history) assert prompt == "" def test_chat_history_to_prompt(chat_history: ChatHistory): chat_history.add_system_message("I am an AI assistant") chat_history.add_user_message("What can you do?") prompt = chat_history.to_prompt() assert ( prompt == 'I am an AI assistantWhat can you do?' # noqa: E501 ) def test_chat_history_from_rendered_prompt_empty(): rendered = "" chat_history = ChatHistory.from_rendered_prompt(rendered) assert chat_history.messages == [] def test_chat_history_from_rendered_prompt(): rendered = 'I am an AI assistantWhat can you do?' # noqa: E501 chat_history = ChatHistory.from_rendered_prompt(rendered) assert chat_history.messages[0].content == "I am an AI assistant" assert chat_history.messages[0].role == AuthorRole.SYSTEM assert chat_history.messages[1].content == "What can you do?" assert chat_history.messages[1].role == AuthorRole.USER def test_chat_history_from_rendered_prompt_multi_line(): rendered = """I am an AI assistant and I can do stuff What can you do?""" chat_history = ChatHistory.from_rendered_prompt(rendered) assert chat_history.messages[0].content == "I am an AI assistant\nand I can do \nstuff" assert chat_history.messages[0].role == AuthorRole.SYSTEM assert chat_history.messages[1].content == "What can you do?" assert chat_history.messages[1].role == AuthorRole.USER async def test_template_unsafe(chat_history: ChatHistory): chat_history.add_assistant_message("I am an AI assistant") template = "system stuff{{$chat_history}}{{$input}}" rendered = await KernelPromptTemplate( prompt_template_config=PromptTemplateConfig(name="test", description="test", template=template), allow_dangerously_set_content=True, ).render( kernel=Kernel(), arguments=KernelArguments(chat_history=chat_history, input="What can you do?"), ) assert "system stuff" in rendered assert "I am an AI assistant" in rendered assert "What can you do?" in rendered chat_history_2 = ChatHistory.from_rendered_prompt(rendered) assert chat_history_2.messages[0].content == "system stuff" assert chat_history_2.messages[0].role == AuthorRole.SYSTEM assert chat_history_2.messages[1].content == "I am an AI assistant" assert chat_history_2.messages[1].role == AuthorRole.ASSISTANT assert chat_history_2.messages[2].content == "What can you do?" assert chat_history_2.messages[2].role == AuthorRole.USER async def test_template_safe(chat_history: ChatHistory): chat_history.add_assistant_message("I am an AI assistant") template = "system stuff{{$chat_history}}{{$input}}" rendered = await KernelPromptTemplate( prompt_template_config=PromptTemplateConfig(name="test", description="test", template=template), allow_dangerously_set_content=True, ).render( kernel=Kernel(), arguments=KernelArguments(chat_history=chat_history, input="What can you do?"), ) assert "system stuff" in rendered assert "I am an AI assistant" in rendered assert "What can you do?" in rendered chat_history_2 = ChatHistory.from_rendered_prompt(rendered) assert chat_history_2.messages[0].content == "system stuff" assert chat_history_2.messages[0].role == AuthorRole.SYSTEM assert chat_history_2.messages[1].content == "I am an AI assistant" assert chat_history_2.messages[1].role == AuthorRole.ASSISTANT assert chat_history_2.messages[2].content == "What can you do?" assert chat_history_2.messages[2].role == AuthorRole.USER async def test_template_two_histories(): # ignore: E501 chat_history1 = ChatHistory() chat_history1.add_assistant_message("I am an AI assistant") chat_history2 = ChatHistory() chat_history2.add_assistant_message("I like to be added later on") template = "system prompt{{$chat_history1}}{{$input}}{{$chat_history2}}" rendered = await KernelPromptTemplate( prompt_template_config=PromptTemplateConfig(name="test", description="test", template=template), allow_dangerously_set_content=True, ).render( kernel=Kernel(), arguments=KernelArguments(chat_history1=chat_history1, chat_history2=chat_history2, input="What can you do?"), ) assert "I am an AI assistant" in rendered assert "What can you do?" in rendered assert "I like to be added later on" in rendered chat_history_out = ChatHistory.from_rendered_prompt(rendered) assert chat_history_out.messages[0].content == "system prompt" assert chat_history_out.messages[0].role == AuthorRole.SYSTEM assert chat_history_out.messages[1].content == "I am an AI assistant" assert chat_history_out.messages[1].role == AuthorRole.ASSISTANT assert chat_history_out.messages[2].content == "What can you do?" assert chat_history_out.messages[2].role == AuthorRole.USER assert chat_history_out.messages[3].content == "I like to be added later on" assert chat_history_out.messages[3].role == AuthorRole.ASSISTANT async def test_template_two_histories_one_empty(): chat_history1 = ChatHistory() chat_history2 = ChatHistory() chat_history2.add_assistant_message("I am an AI assistant") template = "system prompt{{$chat_history1}}{{$input}}{{$chat_history2}}" rendered = await KernelPromptTemplate( prompt_template_config=PromptTemplateConfig(name="test", description="test", template=template), allow_dangerously_set_content=True, ).render( kernel=Kernel(), arguments=KernelArguments(chat_history1=chat_history1, chat_history2=chat_history2, input="What can you do?"), ) chat_history_out = ChatHistory.from_rendered_prompt(rendered) assert chat_history_out.messages[0].content == "system prompt" assert chat_history_out.messages[0].role == AuthorRole.SYSTEM assert chat_history_out.messages[1].content == "What can you do?" assert chat_history_out.messages[1].role == AuthorRole.USER assert chat_history_out.messages[2].content == "I am an AI assistant" assert chat_history_out.messages[2].role == AuthorRole.ASSISTANT async def test_template_history_only(chat_history: ChatHistory): chat_history.add_assistant_message("I am an AI assistant") template = "{{$chat_history}}" rendered = await KernelPromptTemplate( prompt_template_config=PromptTemplateConfig(name="test", description="test", template=template), allow_dangerously_set_content=True, ).render(kernel=Kernel(), arguments=KernelArguments(chat_history=chat_history)) chat_history_2 = ChatHistory.from_rendered_prompt(rendered) assert chat_history_2.messages[0].content == "I am an AI assistant" assert chat_history_2.messages[0].role == AuthorRole.ASSISTANT async def test_template_without_chat_history(): template = "{{$input}}" rendered = await KernelPromptTemplate( prompt_template_config=PromptTemplateConfig(name="test", description="test", template=template) ).render(kernel=Kernel(), arguments=KernelArguments(input="What can you do?")) assert rendered == "What can you do?" chat_history = ChatHistory.from_rendered_prompt(rendered) assert chat_history.messages[0].content == "What can you do?" assert chat_history.messages[0].role == AuthorRole.USER async def test_handwritten_xml(): template = 'test content' rendered = await KernelPromptTemplate( prompt_template_config=PromptTemplateConfig(name="test", description="test", template=template) ).render(kernel=Kernel(), arguments=KernelArguments()) chat_history = ChatHistory.from_rendered_prompt(rendered) assert chat_history.messages[0].content == "test content" assert chat_history.messages[0].role == AuthorRole.USER async def test_empty_text_content_message(): template = 'test content' rendered = await KernelPromptTemplate( prompt_template_config=PromptTemplateConfig(name="test", description="test", template=template) ).render(kernel=Kernel(), arguments=KernelArguments()) chat_history = ChatHistory.from_rendered_prompt(rendered) assert chat_history.messages[0].role == AuthorRole.ASSISTANT assert chat_history.messages[1].content == "test content" assert chat_history.messages[1].role == AuthorRole.USER async def test_handwritten_xml_invalid(): template = '' rendered = await KernelPromptTemplate( prompt_template_config=PromptTemplateConfig(name="test", description="test", template=template) ).render(kernel=Kernel(), arguments=KernelArguments()) chat_history = ChatHistory.from_rendered_prompt(rendered) assert chat_history.messages[0].content == '' assert chat_history.messages[0].role == AuthorRole.USER async def test_handwritten_xml_as_arg_safe(): template = "{{$input}}" rendered = await KernelPromptTemplate( prompt_template_config=PromptTemplateConfig( name="test", description="test", template=template, ), ).render( kernel=Kernel(), arguments=KernelArguments(input='test content'), ) chat_history = ChatHistory.from_rendered_prompt(rendered) assert chat_history.messages[0].content == 'test content' assert chat_history.messages[0].role == AuthorRole.USER async def test_handwritten_xml_as_arg_unsafe_template(): template = "{{$input}}" rendered = await KernelPromptTemplate( prompt_template_config=PromptTemplateConfig(name="test", description="test", template=template), allow_dangerously_set_content=True, ).render( kernel=Kernel(), arguments=KernelArguments(input='test content'), ) chat_history = ChatHistory.from_rendered_prompt(rendered) assert chat_history.messages[0].content == "test content" assert chat_history.messages[0].role == AuthorRole.USER async def test_handwritten_xml_as_arg_unsafe_variable(): template = "{{$input}}" rendered = await KernelPromptTemplate( prompt_template_config=PromptTemplateConfig( name="test", description="test", template=template, input_variables=[InputVariable(name="input", allow_dangerously_set_content=True)], ), ).render( kernel=Kernel(), arguments=KernelArguments(input='test content'), ) chat_history = ChatHistory.from_rendered_prompt(rendered) assert chat_history.messages[0].content == "test content" assert chat_history.messages[0].role == AuthorRole.USER async def test_template_empty_history(chat_history: ChatHistory): template = "system stuff{{$chat_history}}{{$input}}" rendered = await KernelPromptTemplate( prompt_template_config=PromptTemplateConfig(name="test", description="test", template=template), allow_dangerously_set_content=True, ).render( kernel=Kernel(), arguments=KernelArguments(chat_history=chat_history, input="What can you do?"), ) chat_history_2 = ChatHistory.from_rendered_prompt(rendered) assert chat_history_2.messages[0].content == "system stuff" assert chat_history_2.messages[0].role == AuthorRole.SYSTEM assert chat_history_2.messages[1].content == "What can you do?" assert chat_history_2.messages[1].role == AuthorRole.USER def test_to_from_file(chat_history: ChatHistory, tmp_path): chat_history.add_system_message("You are an AI assistant") chat_history.add_user_message("What is the weather in Seattle?") chat_history.add_assistant_message([ FunctionCallContent(id="test1", name="WeatherPlugin-GetWeather", arguments='{{ "location": "Seattle" }}') ]) chat_history.add_tool_message([FunctionResultContent(id="test1", result="It is raining")]) chat_history.add_assistant_message("It is raining in Seattle, what else can I help you with?") file_path = tmp_path / "chat_history.json" chat_history.store_chat_history_to_file(file_path) chat_history_2 = ChatHistory.load_chat_history_from_file(file_path) assert len(chat_history_2.messages) == len(chat_history.messages) assert chat_history_2.messages[0] == chat_history.messages[0] assert chat_history_2.messages[1] == chat_history.messages[1] assert chat_history_2.messages[2] == chat_history.messages[2] assert chat_history_2.messages[3] == chat_history.messages[3] assert chat_history_2.messages[4] == chat_history.messages[4] def test_from_rendered_prompt_preserves_html_p_tag(): """HTML

tags in prompts should be preserved as text, not treated as template tags. Regression test for https://github.com/microsoft/semantic-kernel/issues/13632 """ rendered = ( 'Translate following message from English language into the Spanish language - "

What is your name?

"' ) chat_history = ChatHistory.from_rendered_prompt(rendered) assert len(chat_history.messages) == 1 assert chat_history.messages[0].role == AuthorRole.USER assert "

What is your name?

" in chat_history.messages[0].content def test_from_rendered_prompt_preserves_multiple_html_tags(): """Multiple HTML tags in prompts should be preserved as text.""" rendered = "

First paragraph

A div
" chat_history = ChatHistory.from_rendered_prompt(rendered) assert len(chat_history.messages) == 1 assert "

First paragraph

" in chat_history.messages[0].content assert "
A div
" in chat_history.messages[0].content def test_from_rendered_prompt_preserves_html_with_text_around(): """HTML tags surrounded by plain text should preserve all content.""" rendered = "Hello world today" chat_history = ChatHistory.from_rendered_prompt(rendered) assert len(chat_history.messages) == 1 assert "Hello" in chat_history.messages[0].content assert "world" in chat_history.messages[0].content assert "today" in chat_history.messages[0].content def test_from_rendered_prompt_sk_tags_still_work_with_html(): """SK template tags should still be parsed correctly even when HTML tags are present.""" rendered = 'Tell me about bold text' chat_history = ChatHistory.from_rendered_prompt(rendered) # The tag inside a is handled by ChatMessageContent.from_element assert len(chat_history.messages) == 1 assert chat_history.messages[0].role == AuthorRole.USER def test_chat_history_serialize(chat_history: ChatHistory): class CustomResultClass: def __init__(self, result): self.result = result def __str__(self) -> str: return self.result custom_result = CustomResultClass(result="CustomResultTestValue") chat_history.add_system_message("You are an AI assistant") chat_history.add_user_message("What is the weather in Seattle?") chat_history.add_assistant_message([ FunctionCallContent(id="test1", name="WeatherPlugin-GetWeather", arguments='{{ "location": "Seattle" }}') ]) chat_history.add_tool_message([FunctionResultContent(id="test1", result=custom_result)]) assert "CustomResultTestValue" in chat_history.serialize()