chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:21:23 +08:00
commit b957a53def
5423 changed files with 863745 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
# Copyright (c) Microsoft. All rights reserved.
from pytest import fixture
from semantic_kernel.contents.function_call_content import FunctionCallContent
@fixture(scope="module")
def function_call():
return FunctionCallContent(id="test", name="Test-Function", arguments='{"input": "world"}')
@@ -0,0 +1,143 @@
# Copyright (c) Microsoft. All rights reserved.
from xml.etree.ElementTree import Element
import pytest
from semantic_kernel.contents.annotation_content import AnnotationContent, CitationType
test_cases = [
pytest.param(AnnotationContent(file_id="12345"), id="file_id"),
pytest.param(AnnotationContent(quote="This is a quote."), id="quote"),
pytest.param(AnnotationContent(start_index=5, end_index=20), id="indices"),
pytest.param(
AnnotationContent(file_id="12345", quote="This is a quote.", start_index=5, end_index=20), id="all_fields"
),
pytest.param(
AnnotationContent(
file_id="abc",
type=CitationType.URL_CITATION.value,
url="http://example.com",
quote="q",
start_index=0,
end_index=2,
),
id="citation_type_and_url",
),
]
def test_create_empty():
annotation = AnnotationContent()
assert annotation.file_id is None
assert annotation.quote is None
assert annotation.start_index is None
assert annotation.end_index is None
def test_create_file_id():
annotation = AnnotationContent(file_id="12345")
assert annotation.file_id == "12345"
def test_create_quote():
annotation = AnnotationContent(quote="This is a quote.")
assert annotation.quote == "This is a quote."
def test_create_indices():
annotation = AnnotationContent(start_index=5, end_index=20)
assert annotation.start_index == 5
assert annotation.end_index == 20
def test_create_all_fields():
annotation = AnnotationContent(file_id="12345", quote="This is a quote.", start_index=5, end_index=20)
assert annotation.file_id == "12345"
assert annotation.quote == "This is a quote."
assert annotation.start_index == 5
assert annotation.end_index == 20
def test_update_file_id():
annotation = AnnotationContent()
annotation.file_id = "12345"
assert annotation.file_id == "12345"
def test_update_quote():
annotation = AnnotationContent()
annotation.quote = "This is a quote."
assert annotation.quote == "This is a quote."
def test_update_indices():
annotation = AnnotationContent()
annotation.start_index = 5
annotation.end_index = 20
assert annotation.start_index == 5
assert annotation.end_index == 20
def test_to_str():
annotation = AnnotationContent(file_id="12345", quote="This is a quote.", start_index=5, end_index=20)
assert (
str(annotation)
== "AnnotationContent(type=None, file_id=12345, url=None, quote=This is a quote., start_index=5, end_index=20)"
)
def test_to_element():
annotation = AnnotationContent(file_id="12345", quote="This is a quote.", start_index=5, end_index=20)
element = annotation.to_element()
assert element.tag == "annotation"
assert element.get("file_id") == "12345"
assert element.get("quote") == "This is a quote."
assert element.get("start_index") == "5"
assert element.get("end_index") == "20"
def test_from_element():
element = Element("AnnotationContent")
element.set("file_id", "12345")
element.set("quote", "This is a quote.")
element.set("start_index", "5")
element.set("end_index", "20")
annotation = AnnotationContent.from_element(element)
assert annotation.file_id == "12345"
assert annotation.quote == "This is a quote."
assert annotation.start_index == 5
assert annotation.end_index == 20
def test_to_dict():
annotation = AnnotationContent(file_id="12345", quote="This is a quote.", start_index=5, end_index=20)
expected_text = (
f"type={annotation.citation_type}, {annotation.file_id or annotation.url} {annotation.quote} "
f"(Start Index={annotation.start_index}->End Index={annotation.end_index})"
)
assert annotation.to_dict() == {
"type": "text",
"text": expected_text,
}
@pytest.mark.parametrize("annotation", test_cases)
def test_element_roundtrip(annotation):
element = annotation.to_element()
new_annotation = AnnotationContent.from_element(element)
assert new_annotation == annotation
@pytest.mark.parametrize("annotation", test_cases)
def test_to_dict_call(annotation):
ctype = annotation.citation_type.value if annotation.citation_type else None
expected_text = (
f"type={ctype}, {annotation.file_id or annotation.url} {annotation.quote} "
f"(Start Index={annotation.start_index}->End Index={annotation.end_index})"
)
expected_dict = {
"type": "text",
"text": expected_text,
}
assert annotation.to_dict() == expected_dict
@@ -0,0 +1,271 @@
# Copyright (c) Microsoft. All rights reserved.
import tempfile
from pathlib import Path
import pytest
from numpy import array
from semantic_kernel.contents.binary_content import BinaryContent
from semantic_kernel.exceptions.content_exceptions import ContentInitializationError
test_cases = [
pytest.param(BinaryContent(uri="http://test_uri"), id="uri"),
pytest.param(BinaryContent(data=b"test_data", mime_type="image/jpeg", data_format="base64"), id="data"),
pytest.param(BinaryContent(data="test_data", mime_type="image/jpeg"), id="data_str"),
pytest.param(BinaryContent(uri="http://test_uri", data=b"test_data", mime_type="image/jpeg"), id="both"),
pytest.param(BinaryContent(data_uri="data:image/jpeg;base64,dGVzdF9kYXRh"), id="data_uri"),
pytest.param(BinaryContent(data_uri="data:image/jpeg;base64,dGVzdF9kYXRh"), id="data_uri_with_params"),
pytest.param(
BinaryContent(data_uri="data:image/jpeg;foo=bar;base64,dGVzdF9kYXRh", metadata={"bar": "baz"}),
id="data_uri_with_params_and_metadata",
),
pytest.param(
BinaryContent(data=array([1, 2, 3]), mime_type="application/json", data_format="base64"), id="data_array"
),
]
def test_create_empty():
binary = BinaryContent()
assert binary.uri is None
assert binary.data == b""
assert binary.mime_type == "text/plain"
assert binary.metadata == {}
def test_create_uri():
binary = BinaryContent(uri="http://test_uri")
assert str(binary.uri) == "http://test_uri/"
def test_create_data():
binary = BinaryContent(data=b"test_data", mime_type="application/json")
assert binary.mime_type == "application/json"
assert binary.data == b"test_data"
def test_create_data_uri():
binary = BinaryContent(data_uri="data:application/json;base64,dGVzdF9kYXRh")
assert binary.mime_type == "application/json"
assert binary.data.decode() == "test_data"
def test_create_data_uri_with_params():
binary = BinaryContent(data_uri="data:image/jpeg;foo=bar;base64,dGVzdF9kYXRh")
assert binary.metadata == {"foo": "bar"}
def test_create_data_uri_with_params_and_metadata():
binary = BinaryContent(data_uri="data:image/jpeg;foo=bar;base64,dGVzdF9kYXRh", metadata={"bar": "baz"})
assert binary.metadata == {"foo": "bar", "bar": "baz"}
def test_update_data():
binary = BinaryContent()
binary.data = b"test_data"
binary.mime_type = "application/json"
assert binary.mime_type == "application/json"
assert binary.data == b"test_data"
def test_update_data_str():
binary = BinaryContent()
binary.data = "test_data"
binary.mime_type = "application/json"
assert binary.mime_type == "application/json"
assert binary.data == b"test_data"
def test_update_existing_data():
binary = BinaryContent(data_uri="data:image/jpeg;foo=bar;base64,dGVzdF9kYXRh", metadata={"bar": "baz"})
assert binary._data_uri is not None
binary._data_uri.data_format = None
binary.data = "test_data"
binary.data = b"test_data"
assert binary.data == b"test_data"
def test_update_data_uri():
binary = BinaryContent()
binary.data_uri = "data:image/jpeg;foo=bar;base64,dGVzdF9kYXRh"
assert binary.mime_type == "image/jpeg"
assert binary.data.decode() == "test_data"
assert binary.metadata == {"foo": "bar"}
def test_to_str_uri():
binary = BinaryContent(uri="http://test_uri")
assert str(binary) == "http://test_uri/"
def test_to_str_data():
binary = BinaryContent(data=b"test_data", mime_type="image/jpeg", data_format="base64")
assert str(binary) == "data:image/jpeg;base64,dGVzdF9kYXRh"
@pytest.mark.parametrize("binary", test_cases)
def test_element_roundtrip(binary):
element = binary.to_element()
new_image = BinaryContent.from_element(element)
assert new_image == binary
@pytest.mark.parametrize("binary", test_cases)
def test_to_dict(binary):
assert binary.to_dict() == {"type": "binary", "binary": {"uri": str(binary)}}
def test_can_read_with_data():
"""Test can_read property returns True when data is available."""
binary = BinaryContent(data=b"test_data", mime_type="application/pdf")
assert binary.can_read is True
def test_can_read_without_data():
"""Test can_read property returns False when no data is available."""
binary = BinaryContent(uri="http://example.com/file.pdf")
assert binary.can_read is False
def test_can_read_empty():
"""Test can_read property returns False for empty BinaryContent."""
binary = BinaryContent()
assert binary.can_read is False
def test_from_file_success():
"""Test from_file class method successfully creates BinaryContent from a file."""
test_data = b"This is test file content"
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
temp_file.write(test_data)
temp_file_path = temp_file.name
try:
binary = BinaryContent.from_file(temp_file_path, mime_type="application/pdf")
assert binary.data == test_data
assert binary.mime_type == "application/pdf"
assert binary.uri == temp_file_path
assert binary.can_read is True
# Verify data_string works (should be base64 encoded)
assert binary.data_string == "VGhpcyBpcyB0ZXN0IGZpbGUgY29udGVudA=="
finally:
Path(temp_file_path).unlink()
def test_from_file_with_path_object():
"""Test from_file class method works with Path objects."""
test_data = b"Path object test content"
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
temp_file.write(test_data)
temp_path = Path(temp_file.name)
try:
binary = BinaryContent.from_file(temp_path, mime_type="text/plain")
assert binary.data == test_data
assert binary.mime_type == "text/plain"
assert binary.uri == str(temp_path)
# Verify data_string works (should be base64 encoded)
assert binary.data_string == "UGF0aCBvYmplY3QgdGVzdCBjb250ZW50"
finally:
temp_path.unlink()
def test_from_file_binary_data():
"""Test from_file handles binary data correctly without encoding errors."""
# Test with actual binary PDF-like data
test_data = b"%PDF-1.4\n%\xf6\xe4\xfc\xdf\n1 0 obj\n<<\n/Type /Catalog"
with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as temp_file:
temp_file.write(test_data)
temp_file_path = temp_file.name
try:
binary = BinaryContent.from_file(temp_file_path, mime_type="application/pdf")
assert binary.data == test_data
assert binary.mime_type == "application/pdf"
assert binary.can_read is True
# Should not raise Unicode decode error
data_string = binary.data_string
assert isinstance(data_string, str)
assert len(data_string) > 0
finally:
Path(temp_file_path).unlink()
def test_from_file_nonexistent():
"""Test from_file raises FileNotFoundError for nonexistent files."""
with pytest.raises(FileNotFoundError, match="File not found"):
BinaryContent.from_file("/nonexistent/file.pdf")
def test_from_file_directory():
"""Test from_file raises ContentInitializationError for directories."""
with (
tempfile.TemporaryDirectory() as temp_dir,
pytest.raises(ContentInitializationError, match="Path is not a file"),
):
BinaryContent.from_file(temp_dir)
def test_write_to_file_default_no_overwrite():
"""Test write_to_file does not overwrite existing files by default."""
test_data = b"new data"
binary = BinaryContent(data=test_data, mime_type="text/plain")
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
temp_file.write(b"old data")
temp_file_path = temp_file.name
try:
with pytest.raises(FileExistsError, match="File already exists and overwrite is disabled"):
binary.write_to_file(temp_file_path)
# Verify original file was not modified
assert Path(temp_file_path).read_bytes() == b"old data"
finally:
Path(temp_file_path).unlink()
def test_write_to_file_overwrite_true():
"""Test write_to_file with overwrite=True overwrites existing files."""
test_data = b"new data"
binary = BinaryContent(data=test_data, mime_type="text/plain")
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
temp_file.write(b"old data")
temp_file_path = temp_file.name
try:
binary.write_to_file(temp_file_path, overwrite=True)
assert Path(temp_file_path).read_bytes() == test_data
finally:
Path(temp_file_path).unlink()
def test_write_to_file_overwrite_false_existing_file():
"""Test write_to_file with overwrite=False raises FileExistsError for existing files."""
test_data = b"new data"
binary = BinaryContent(data=test_data, mime_type="text/plain")
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
temp_file.write(b"old data")
temp_file_path = temp_file.name
try:
with pytest.raises(FileExistsError, match="File already exists and overwrite is disabled"):
binary.write_to_file(temp_file_path, overwrite=False)
# Verify original file was not modified
assert Path(temp_file_path).read_bytes() == b"old data"
finally:
Path(temp_file_path).unlink()
def test_write_to_file_overwrite_false_new_file():
"""Test write_to_file with overwrite=False succeeds for new files."""
test_data = b"test data"
binary = BinaryContent(data=test_data, mime_type="text/plain")
with tempfile.TemporaryDirectory() as temp_dir:
temp_file_path = Path(temp_dir) / "new_file.txt"
binary.write_to_file(temp_file_path, overwrite=False)
assert temp_file_path.read_bytes() == test_data
@@ -0,0 +1,672 @@
# 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 == "<chat_history />"
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
== '<chat_history><message role="system"><text>I am an AI assistant</text></message><message role="user"><text>What can you do?</text></message></chat_history>' # 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 = '<message role="system"><text>I am an AI assistant</text></message><message role="user"><text>What can you do?</text></message>' # 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 = """<message role="system">I am an AI assistant
and I can do
stuff</message>
<message role="user">What can you do?</message>"""
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 = '<message role="user">test content</message>'
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 = '<message role="assistant"><text></text></message><message role="user">test content</message>'
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 = '<message role="user"test content</message>'
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 == '<message role="user"test content</message>'
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='<message role="user">test content</message>'),
)
chat_history = ChatHistory.from_rendered_prompt(rendered)
assert chat_history.messages[0].content == '<message role="user">test content</message>'
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='<message role="user">test content</message>'),
)
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='<message role="user">test content</message>'),
)
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 <p> 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 - "<p>What is your name?</p>"'
)
chat_history = ChatHistory.from_rendered_prompt(rendered)
assert len(chat_history.messages) == 1
assert chat_history.messages[0].role == AuthorRole.USER
assert "<p>What is your name?</p>" 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 = "<p>First paragraph</p><div>A div</div>"
chat_history = ChatHistory.from_rendered_prompt(rendered)
assert len(chat_history.messages) == 1
assert "<p>First paragraph</p>" in chat_history.messages[0].content
assert "<div>A div</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 <b>world</b> today"
chat_history = ChatHistory.from_rendered_prompt(rendered)
assert len(chat_history.messages) == 1
assert "Hello" in chat_history.messages[0].content
assert "<b>world</b>" 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 = '<message role="user">Tell me about <b>bold</b> text</message>'
chat_history = ChatHistory.from_rendered_prompt(rendered)
# The <b> tag inside a <message> 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()
@@ -0,0 +1,232 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
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.history_reducer.chat_history_reducer_utils import (
SUMMARY_METADATA_KEY,
contains_function_call_or_result,
extract_range,
get_call_result_pairs,
locate_safe_reduction_index,
locate_summarization_boundary,
)
from semantic_kernel.contents.utils.author_role import AuthorRole
@pytest.fixture
def chat_messages_with_pairs():
msgs = []
# 1) Summary message at index 0 (system)
msg_summary = ChatMessageContent(role=AuthorRole.SYSTEM, content="Summary so far.")
msg_summary.metadata[SUMMARY_METADATA_KEY] = True
msgs.append(msg_summary)
# 2) Normal user message
msgs.append(ChatMessageContent(role=AuthorRole.USER, content="User says hello."))
# 3) Function call (call ID = "call1")
msg_func_call_1 = ChatMessageContent(role=AuthorRole.ASSISTANT, content="Function call #1")
func_call_content_1 = FunctionCallContent(id="call1", function_name="funcA", arguments={"param": "valA"})
msg_func_call_1.items.append(func_call_content_1)
msgs.append(msg_func_call_1)
# 4) Function result for call1
msg_func_result_1 = ChatMessageContent(role=AuthorRole.ASSISTANT, content="Result for call #1")
func_result_content_1 = FunctionResultContent(id="call1", content="Function #1 result text")
msg_func_result_1.items.append(func_result_content_1)
msgs.append(msg_func_result_1)
# 5) Another user message
msgs.append(ChatMessageContent(role=AuthorRole.USER, content="Another user message."))
# 6) Another function call (call ID = "call2")
msg_func_call_2 = ChatMessageContent(role=AuthorRole.ASSISTANT, content="Function call #2")
func_call_content_2 = FunctionCallContent(id="call2", function_name="funcB", arguments={"param": "valB"})
msg_func_call_2.items.append(func_call_content_2)
msgs.append(msg_func_call_2)
# 7) Another user message (no result yet for "call2")
msgs.append(ChatMessageContent(role=AuthorRole.USER, content="Wait, function result not yet?"))
# 8) Unrelated function result (call ID = "callX" doesn't match any prior call)
msg_func_result_x = ChatMessageContent(role=AuthorRole.ASSISTANT, content="Result for unknown call")
func_result_content_x = FunctionResultContent(id="callX", content="No matching call.")
msg_func_result_x.items.append(func_result_content_x)
msgs.append(msg_func_result_x)
# 9) Function result for call2
msg_func_result_2 = ChatMessageContent(role=AuthorRole.ASSISTANT, content="Result for call #2")
func_result_content_2 = FunctionResultContent(id="call2", content="Function #2 result text")
msg_func_result_2.items.append(func_result_content_2)
msgs.append(msg_func_result_2)
return msgs
def test_get_call_result_pairs_fixture_has_pairs(chat_messages_with_pairs):
"""
Since 'chat_messages_with_pairs' includes function calls with IDs,
we expect pairs. Specifically:
- (2,3) for call1
- (5,8) for call2
"""
pairs = get_call_result_pairs(chat_messages_with_pairs)
assert (2, 3) in pairs, "Expected pair for (call1) in indexes (2,3)."
assert (5, 8) in pairs, "Expected pair for (call2) in indexes (5,8)."
assert len(pairs) == 2, "Fixture should produce exactly two matched call->result pairs."
@pytest.mark.parametrize(
"message_items,expected",
[
([], False),
([FunctionCallContent(function_name="funcA", arguments={})], True),
([FunctionResultContent(id="test", content="Result")], True),
],
)
def test_contains_function_call_or_result(message_items, expected):
msg = ChatMessageContent(role=AuthorRole.USER, content="Test")
msg.items.extend(message_items)
assert contains_function_call_or_result(msg) == expected
def test_extract_range_preserve_pairs(chat_messages_with_pairs):
"""
Tests that extract_range with preserve_pairs=True keeps or skips
call/result pairs together. We'll slice from index=2 to index=9
in the updated fixture.
"""
extracted = extract_range(
chat_messages_with_pairs,
start=2,
end=9, # exclusive of index=9
preserve_pairs=True,
)
# Indices in range(2..9) => 2,3,4,5,6,7,8
# The code should preserve both pairs if they're fully in the slice.
# Pairs are (2,3) and (5,8). They are indeed fully inside [2..9).
# So we expect to keep them plus indices 4,6,7. That totals 7 messages.
assert len(extracted) == 7
# Instead of asserting exact positional equality, just check we
# have the same set of messages from 2..9 (no duplicates or omissions).
expected_slice = chat_messages_with_pairs[2:9] # indexes 2..8
assert set(extracted) == set(expected_slice), "Expected messages 2..8 to be returned."
def test_extract_range_preserve_pairs_call_outside_slice(chat_messages_with_pairs):
"""
If a function call is outside the start/end range but the result is inside,
we do NOT have to preserve that pair since it's partially out of range.
We'll pick start=4, end=9 => indices 4..8.
"""
extracted = extract_range(chat_messages_with_pairs, start=4, end=9, preserve_pairs=True)
# Indices in range(4..9) => 4,5,6,7,8
# Pairs: (2,3) is outside, (5,8) is fully inside. So (5,8) is kept together.
# The final set of messages is [4,5,6,7,8] => 5 total.
assert len(extracted) == 5
expected_slice = chat_messages_with_pairs[4:9] # indexes 4..8
assert set(extracted) == set(expected_slice), "Expected messages 4..8 to be returned."
# (2,3) do not appear, and that's correct since they're outside this slice.
def test_locate_summarization_boundary_empty():
# Edge case: empty history => boundary = 0
empty_history = []
assert locate_summarization_boundary(empty_history) == 0
def test_locate_safe_reduction_index_multiple_calls(chat_messages_with_pairs):
"""
If we set a small target_count, the code will attempt to find a safe
reduction index that doesn't orphan a function call/result pair.
"""
total_count = len(chat_messages_with_pairs) # 9
target_count = 4
idx = locate_safe_reduction_index(
chat_messages_with_pairs,
target_count=target_count,
threshold_count=0,
offset_count=0,
)
# We expect a valid index because total_count (9) > target_count (4).
assert idx is not None and 0 < idx < total_count
# Verify that from idx onward, we haven't split a matched call->result pair.
pairs = get_call_result_pairs(chat_messages_with_pairs)
for call_i, result_i in pairs:
if call_i >= idx:
# If the call is in the reduced set, the result must be in the reduced set:
assert result_i >= idx
if result_i >= idx:
# If the result is in the reduced set, the call must be in the reduced set:
assert call_i >= idx
def test_locate_safe_reduction_index_high_offset(chat_messages_with_pairs):
"""
If offset_count is large, we might not be able to reduce. Then the function
can return None if no valid reduction can be found after skipping the offset.
"""
target_count = 3
threshold_count = 0
offset_count = 5
idx = locate_safe_reduction_index(
chat_messages_with_pairs,
target_count=target_count,
threshold_count=threshold_count,
offset_count=offset_count,
)
# Possibly None if we cannot reduce after skipping the first 5 messages.
if idx is not None:
# Then it must be >= offset_count
assert idx >= offset_count
else:
# It's fine if it returns None, meaning no valid safe reduction was found.
pass
def test_locate_safe_reduction_index_tool_role_without_function_result_content():
"""Regression test: TOOL role messages without FunctionResultContent in items
must still be recognized as part of a tool call/result pair.
This prevents orphaning tool results when the TOOL message only contains
text content (no FunctionResultContent item).
"""
msgs = [
ChatMessageContent(role=AuthorRole.USER, content="Hello"),
]
# Assistant with tool call
msg_call = ChatMessageContent(role=AuthorRole.ASSISTANT, content="")
msg_call.items.append(FunctionCallContent(id="call1", function_name="myTool", arguments={"x": 1}))
msgs.append(msg_call)
# Tool result as role=TOOL but with plain text content only
msgs.append(ChatMessageContent(role=AuthorRole.TOOL, content="Tool result here"))
msgs.append(ChatMessageContent(role=AuthorRole.USER, content="Thanks"))
msgs.append(ChatMessageContent(role=AuthorRole.ASSISTANT, content="You are welcome"))
idx = locate_safe_reduction_index(msgs, target_count=3, threshold_count=0)
assert idx is not None
# The tool call (index 1) must be included if tool result (index 2) is included
kept_indices = list(range(idx, len(msgs)))
has_tool_role = any(msgs[i].role == AuthorRole.TOOL for i in kept_indices)
has_tool_call = any(any(isinstance(it, FunctionCallContent) for it in msgs[i].items) for i in kept_indices)
if has_tool_role:
assert has_tool_call, (
f"Tool result at index 2 was kept but tool call at index 1 was dropped. "
f"Kept indices: {kept_indices}, reduction index: {idx}"
)
@@ -0,0 +1,314 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import AsyncMock, MagicMock
import pytest
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.history_reducer.chat_history_reducer_utils import SUMMARY_METADATA_KEY
from semantic_kernel.contents.history_reducer.chat_history_summarization_reducer import (
ChatHistorySummarizationReducer,
)
from semantic_kernel.contents.utils.author_role import AuthorRole
from semantic_kernel.exceptions.content_exceptions import ChatHistoryReducerException
@pytest.fixture
def mock_service():
"""Returns a mock ChatCompletionClientBase with required methods."""
service = MagicMock(spec=ChatCompletionClientBase)
# Mock the get_prompt_execution_settings_class to return a placeholder
service.get_prompt_execution_settings_class.return_value = MagicMock(return_value=MagicMock(service_id="foo"))
# Mock the async call get_chat_message_content
service.get_chat_message_content = AsyncMock()
return service
@pytest.fixture
def chat_messages():
"""Returns a list of ChatMessageContent objects with default roles."""
msgs = []
# Existing summary
summary_msg = ChatMessageContent(role=AuthorRole.SYSTEM, content="Prior summary.")
summary_msg.metadata[SUMMARY_METADATA_KEY] = True
msgs.append(summary_msg)
# Normal user messages
msgs.append(ChatMessageContent(role=AuthorRole.USER, content="Hello!"))
msgs.append(ChatMessageContent(role=AuthorRole.ASSISTANT, content="Hi there."))
msgs.append(ChatMessageContent(role=AuthorRole.USER, content="What can you do?"))
msgs.append(ChatMessageContent(role=AuthorRole.ASSISTANT, content="I can help with tasks."))
msgs.append(ChatMessageContent(role=AuthorRole.USER, content="Ok, let's do something."))
return msgs
def test_summarization_reducer_init(mock_service):
reducer = ChatHistorySummarizationReducer(
service=mock_service,
target_count=10,
threshold_count=5,
summarization_instructions="Custom instructions",
use_single_summary=False,
fail_on_error=False,
)
assert reducer.service == mock_service
assert reducer.target_count == 10
assert reducer.threshold_count == 5
assert reducer.summarization_instructions == "Custom instructions"
assert reducer.use_single_summary is False
assert reducer.fail_on_error is False
def test_summarization_reducer_defaults(mock_service):
reducer = ChatHistorySummarizationReducer(service=mock_service, target_count=5)
# Check default property values
assert reducer.threshold_count == 0
assert reducer.summarization_instructions in reducer.summarization_instructions
assert reducer.use_single_summary is True
assert reducer.fail_on_error is True
def test_summarization_reducer_eq_and_hash(mock_service):
r1 = ChatHistorySummarizationReducer(service=mock_service, target_count=5, threshold_count=2)
r2 = ChatHistorySummarizationReducer(service=mock_service, target_count=5, threshold_count=2)
r3 = ChatHistorySummarizationReducer(service=mock_service, target_count=6, threshold_count=2)
assert r1 == r2
assert r1 != r3
# Test hash
assert hash(r1) == hash(r2)
assert hash(r1) != hash(r3)
async def test_summarization_reducer_reduce_no_need(chat_messages, mock_service):
reducer = ChatHistorySummarizationReducer(service=mock_service, target_count=10, threshold_count=0)
# If len(history) <= target_count => None
result = await reducer.reduce()
assert result is None
mock_service.get_chat_message_content.assert_not_awaited()
async def test_summarization_reducer_reduce_needed(mock_service):
messages = [
# A summary message (as in the original test)
ChatMessageContent(role=AuthorRole.SYSTEM, content="Existing summary", metadata={SUMMARY_METADATA_KEY: True}),
# Enough additional messages so total is > 4
ChatMessageContent(role=AuthorRole.USER, content="User says hello"),
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Assistant responds"),
ChatMessageContent(role=AuthorRole.USER, content="User says more"),
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Assistant responds again"),
ChatMessageContent(role=AuthorRole.USER, content="User says more"),
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Assistant responds again"),
]
reducer = ChatHistorySummarizationReducer(service=mock_service, target_count=3, threshold_count=1)
reducer.messages = messages # Set the chat history
# Mock that the service will return a single summary message
summary_content = ChatMessageContent(role=AuthorRole.ASSISTANT, content="This is a summary.")
mock_service.get_chat_message_content.return_value = summary_content
mock_service.get_prompt_execution_settings_from_settings.return_value = PromptExecutionSettings()
result = await reducer.reduce()
assert result is not None, "We expect a shortened list with a new summary inserted."
assert len(result) <= 5, "The resulting list should be shortened to around target_count + threshold_count."
assert any(msg.metadata.get(SUMMARY_METADATA_KEY) for msg in result), (
"We expect to see a newly inserted summary message."
)
async def test_summarization_reducer_reduce_needed_auto(mock_service):
# Mock that the service will return a single summary message
summary_content = ChatMessageContent(role=AuthorRole.ASSISTANT, content="This is a summary.")
mock_service.get_chat_message_content.return_value = summary_content
mock_service.get_prompt_execution_settings_from_settings.return_value = PromptExecutionSettings()
messages = [
# A summary message (as in the original test)
ChatMessageContent(role=AuthorRole.SYSTEM, content="Existing summary", metadata={SUMMARY_METADATA_KEY: True}),
# Enough additional messages so total is > 4
ChatMessageContent(role=AuthorRole.USER, content="User says hello"),
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Assistant responds"),
ChatMessageContent(role=AuthorRole.USER, content="User says more"),
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Assistant responds again"),
ChatMessageContent(role=AuthorRole.USER, content="User says more"),
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Assistant responds again"),
]
reducer = ChatHistorySummarizationReducer(auto_reduce=True, service=mock_service, target_count=3, threshold_count=1)
for msg in messages:
await reducer.add_message_async(msg)
assert len(reducer.messages) <= 5, (
"We should auto-reduce after each message, we have one summary, and then 4 other messages."
)
async def test_summarization_reducer_reduce_no_messages_to_summarize(mock_service):
# If we do use_single_summary=False, the older_range_start is insertion_point
# In that scenario, if insertion_point == older_range_end => no messages to summarize => return None
reducer = ChatHistorySummarizationReducer(service=mock_service, target_count=1, use_single_summary=False)
# Provide just one message flagged as summary => insertion_point=0, so older_range_start=0, older_range_end=0
only_summary = [
ChatMessageContent(role=AuthorRole.SYSTEM, content="Only summary.", metadata={SUMMARY_METADATA_KEY: True})
]
reducer.add_message(only_summary[0])
result = await reducer.reduce()
assert result is None
mock_service.get_chat_message_content.assert_not_awaited()
async def test_summarization_reducer_reduce_summarizer_returns_none(mock_service):
# If the summarizer yields no messages, we return None
reducer = ChatHistorySummarizationReducer(service=mock_service, target_count=3)
# Provide enough messages that summarization would normally occur
messages = [
ChatMessageContent(role=AuthorRole.SYSTEM, content="Existing summary", metadata={SUMMARY_METADATA_KEY: True}),
ChatMessageContent(role=AuthorRole.USER, content="User asks something"),
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Assistant replies"),
ChatMessageContent(role=AuthorRole.USER, content="Another user query"),
]
reducer.messages = messages
# Summarizer returns None
mock_service.get_chat_message_content.return_value = None
result = await reducer.reduce()
assert result is None, "If the summarizer yields no message, we return None."
async def test_summarization_reducer_reduce_summarization_fails(mock_service):
# If summarization fails, we raise if fail_on_error=True
reducer = ChatHistorySummarizationReducer(service=mock_service, target_count=3, fail_on_error=True)
# Enough messages to trigger summarization
messages = [
ChatMessageContent(role=AuthorRole.USER, content="Msg1"),
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Msg2"),
ChatMessageContent(role=AuthorRole.USER, content="Msg3"),
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Msg4"),
]
reducer.messages = messages
mock_service.get_chat_message_content.side_effect = Exception("Summarizer error")
with pytest.raises(ChatHistoryReducerException, match="failed"):
await reducer.reduce()
async def test_summarization_reducer_reduce_summarization_fails_no_raise(chat_messages, mock_service):
# If summarization fails, but fail_on_error=False, we just log and return None
reducer = ChatHistorySummarizationReducer(service=mock_service, target_count=3, fail_on_error=False)
mock_service.get_chat_message_content.side_effect = Exception("Summarizer error")
reducer.messages = chat_messages
result = await reducer.reduce()
assert result is None
async def test_summarization_reducer_private_summarize(mock_service):
"""Directly test the _summarize method for coverage."""
reducer = ChatHistorySummarizationReducer(service=mock_service, target_count=5)
chat_messages = [
ChatMessageContent(role=AuthorRole.USER, content="Message1"),
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Message2"),
]
summary_content = ChatMessageContent(role=AuthorRole.ASSISTANT, content="Mock Summary")
mock_service.get_chat_message_content.return_value = summary_content
mock_service.get_prompt_execution_settings_from_settings.return_value = PromptExecutionSettings()
actual_summary = await reducer._summarize(chat_messages)
assert actual_summary is not None, "We should get a summary message back."
assert actual_summary.content == "Mock Summary", "We expect the mock summary content."
async def test_summarization_preserves_system_message(mock_service):
"""Verify that the summarization reducer preserves system messages."""
messages = [
ChatMessageContent(role=AuthorRole.SYSTEM, content="Important system prompt"),
ChatMessageContent(role=AuthorRole.USER, content="User says hello"),
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Assistant responds"),
ChatMessageContent(role=AuthorRole.USER, content="User says more"),
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Assistant responds again"),
ChatMessageContent(role=AuthorRole.USER, content="User says even more"),
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Assistant responds yet again"),
]
reducer = ChatHistorySummarizationReducer(service=mock_service, target_count=3, threshold_count=0)
reducer.messages = messages
summary_content = ChatMessageContent(role=AuthorRole.ASSISTANT, content="Summary of conversation.")
mock_service.get_chat_message_content.return_value = summary_content
mock_service.get_prompt_execution_settings_from_settings.return_value = PromptExecutionSettings()
result = await reducer.reduce()
assert result is not None
# System message must be preserved
roles = [msg.role for msg in result.messages]
assert AuthorRole.SYSTEM in roles, "System message was lost during summarization"
# System message should be first
assert result.messages[0].role == AuthorRole.SYSTEM
assert result.messages[0].content == "Important system prompt"
async def test_summarization_does_not_preserve_summary_system_message(mock_service):
"""A prior summary with SYSTEM role should NOT be treated as the system prompt to preserve."""
summary_sys = ChatMessageContent(
role=AuthorRole.SYSTEM, content="Previous summary", metadata={SUMMARY_METADATA_KEY: True}
)
messages = [
summary_sys,
ChatMessageContent(role=AuthorRole.USER, content="User says hello"),
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Assistant responds"),
ChatMessageContent(role=AuthorRole.USER, content="User says more"),
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Assistant responds again"),
ChatMessageContent(role=AuthorRole.USER, content="User says even more"),
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Assistant responds yet again"),
]
reducer = ChatHistorySummarizationReducer(service=mock_service, target_count=3, threshold_count=0)
reducer.messages = messages
summary_content = ChatMessageContent(role=AuthorRole.ASSISTANT, content="New summary.")
mock_service.get_chat_message_content.return_value = summary_content
mock_service.get_prompt_execution_settings_from_settings.return_value = PromptExecutionSettings()
result = await reducer.reduce()
assert result is not None
# The old summary-system message should NOT be preserved as a "system prompt"
# It should be replaced by the new summary
for msg in result.messages:
if msg.role == AuthorRole.SYSTEM:
assert msg.metadata.get(SUMMARY_METADATA_KEY) is not True or msg is not summary_sys
async def test_summarization_preserves_developer_message(mock_service):
"""Verify that developer messages are preserved during summarization."""
messages = [
ChatMessageContent(role=AuthorRole.DEVELOPER, content="Developer instructions"),
ChatMessageContent(role=AuthorRole.USER, content="User says hello"),
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Assistant responds"),
ChatMessageContent(role=AuthorRole.USER, content="User says more"),
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Assistant responds again"),
ChatMessageContent(role=AuthorRole.USER, content="User says even more"),
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Assistant responds yet again"),
]
reducer = ChatHistorySummarizationReducer(service=mock_service, target_count=3, threshold_count=0)
reducer.messages = messages
summary_content = ChatMessageContent(role=AuthorRole.ASSISTANT, content="Summary of conversation.")
mock_service.get_chat_message_content.return_value = summary_content
mock_service.get_prompt_execution_settings_from_settings.return_value = PromptExecutionSettings()
result = await reducer.reduce()
assert result is not None
# Developer message must be preserved
assert result.messages[0].role == AuthorRole.DEVELOPER
assert result.messages[0].content == "Developer instructions"
@@ -0,0 +1,306 @@
# Copyright (c) Microsoft. All rights reserved.
import logging
import pytest
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.history_reducer.chat_history_reducer_utils import locate_safe_reduction_index
from semantic_kernel.contents.history_reducer.chat_history_truncation_reducer import ChatHistoryTruncationReducer
from semantic_kernel.contents.utils.author_role import AuthorRole
@pytest.fixture
def chat_messages():
msgs = []
msgs.append(ChatMessageContent(role=AuthorRole.SYSTEM, content="System message."))
msgs.append(ChatMessageContent(role=AuthorRole.USER, content="User message 1"))
msgs.append(ChatMessageContent(role=AuthorRole.ASSISTANT, content="Assistant message 1"))
msgs.append(ChatMessageContent(role=AuthorRole.USER, content="User message 2"))
msgs.append(ChatMessageContent(role=AuthorRole.ASSISTANT, content="Assistant message 2"))
return msgs
@pytest.fixture
def chat_messages_with_tools():
return [
ChatMessageContent(role=AuthorRole.SYSTEM, content="System message."),
ChatMessageContent(role=AuthorRole.USER, content="User message 1"),
ChatMessageContent(
role=AuthorRole.ASSISTANT,
items=[FunctionCallContent(id="123", function_name="search", plugin_name="plugin", arguments={"q": "x"})],
),
ChatMessageContent(
role=AuthorRole.TOOL,
items=[FunctionResultContent(id="123", function_name="search", plugin_name="plugin", result="RESULT")],
),
ChatMessageContent(role=AuthorRole.USER, content="User message 2"),
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Assistant message 2"),
]
def test_truncation_reducer_init():
reducer = ChatHistoryTruncationReducer(target_count=5, threshold_count=2)
assert reducer.target_count == 5
assert reducer.threshold_count == 2
def test_truncation_reducer_defaults():
reducer = ChatHistoryTruncationReducer(target_count=5)
assert reducer.threshold_count == 0
def test_truncation_reducer_eq_and_hash():
r1 = ChatHistoryTruncationReducer(target_count=5, threshold_count=2)
r2 = ChatHistoryTruncationReducer(target_count=5, threshold_count=2)
r3 = ChatHistoryTruncationReducer(target_count=5, threshold_count=1)
assert r1 == r2
assert r1 != r3
assert hash(r1) == hash(r2)
assert hash(r1) != hash(r3)
async def test_truncation_reducer_no_need(chat_messages):
# If total <= target + threshold => returns None
reducer = ChatHistoryTruncationReducer(target_count=5, threshold_count=0)
result = await reducer.reduce()
assert result is None
async def test_truncation_reducer_no_truncation_index_found():
# If the safe reduction index < 0, returns None
# We'll craft a scenario where the number of messages is big,
# but the function can't find a safe index to cut
msgs = [ChatMessageContent(role=AuthorRole.USER, content="Msg")] * 10
# Suppose threshold_count is huge, so effectively we can't reduce
reducer = ChatHistoryTruncationReducer(target_count=3, threshold_count=10)
reducer.messages = msgs
result = await reducer.reduce()
assert result is None
async def test_truncation_reducer_truncation(chat_messages):
# Force a smaller target so we do need to reduce
reducer = ChatHistoryTruncationReducer(target_count=2)
reducer.messages = chat_messages
result = await reducer.reduce()
# We expect 2 messages: system message is preserved + 1 conversation message
# (target_count=2 includes the system message, matching .NET SDK behavior)
assert result is not None
assert len(result) == 2
# System message should be first, followed by the last conversation message
assert result[0] == chat_messages[0] # System message preserved
assert result[0].role == AuthorRole.SYSTEM
assert result[1] == chat_messages[-1]
async def test_truncation_reducer_truncation_with_tools(chat_messages_with_tools):
# Force a smaller target so we do need to reduce
reducer = ChatHistoryTruncationReducer(target_count=3, threshold_count=0)
reducer.messages = chat_messages_with_tools
result = await reducer.reduce()
# We expect 3 messages: system message + last 2 conversation messages
# (target_count=3 includes the system message, matching .NET SDK behavior)
assert result is not None
assert len(result) == 3
# System message preserved, followed by last user-assistant pair
assert result[0] == chat_messages_with_tools[0] # System message
assert result[0].role == AuthorRole.SYSTEM
assert result[1] == chat_messages_with_tools[-2] # User message 2
assert result[2] == chat_messages_with_tools[-1] # Assistant message 2
async def test_truncation_preserves_system_message():
"""Verify that the system message is preserved after truncation (issue #12612)."""
reducer = ChatHistoryTruncationReducer(
target_count=2,
system_message="a system message",
)
reducer.add_message(ChatMessageContent(role=AuthorRole.USER, content="user prompt 1"))
reducer.add_message(ChatMessageContent(role=AuthorRole.ASSISTANT, content="response 1"))
reducer.add_message(ChatMessageContent(role=AuthorRole.USER, content="user prompt 2"))
reducer.add_message(ChatMessageContent(role=AuthorRole.ASSISTANT, content="response 2"))
reducer.add_message(ChatMessageContent(role=AuthorRole.USER, content="user prompt 3"))
reducer.add_message(ChatMessageContent(role=AuthorRole.ASSISTANT, content="response 3"))
reducer.add_message(ChatMessageContent(role=AuthorRole.USER, content="user prompt 4"))
reducer.add_message(ChatMessageContent(role=AuthorRole.ASSISTANT, content="response 4"))
result = await reducer.reduce()
assert result is not None
# System message must be present after reduction
roles = [msg.role for msg in result.messages]
assert AuthorRole.SYSTEM in roles, "System message was deleted by the history reducer"
assert result.messages[0].role == AuthorRole.SYSTEM
assert result.messages[0].content == "a system message"
async def test_truncation_preserves_developer_message():
"""Verify that developer messages are preserved after truncation."""
msgs = [
ChatMessageContent(role=AuthorRole.DEVELOPER, content="Developer instructions."),
ChatMessageContent(role=AuthorRole.USER, content="User message 1"),
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Assistant message 1"),
ChatMessageContent(role=AuthorRole.USER, content="User message 2"),
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Assistant message 2"),
]
reducer = ChatHistoryTruncationReducer(target_count=2)
reducer.messages = msgs
result = await reducer.reduce()
assert result is not None
assert len(result) == 2
# Developer message should be first
assert result[0].role == AuthorRole.DEVELOPER
assert result[0].content == "Developer instructions."
async def test_truncation_target_count_1_with_system_message():
"""With target_count=1 and a system message, reduce to just the system message."""
reducer = ChatHistoryTruncationReducer(target_count=1, system_message="System prompt")
reducer.add_message(ChatMessageContent(role=AuthorRole.USER, content="Hello"))
reducer.add_message(ChatMessageContent(role=AuthorRole.ASSISTANT, content="Hi"))
reducer.add_message(ChatMessageContent(role=AuthorRole.USER, content="How are you?"))
reducer.add_message(ChatMessageContent(role=AuthorRole.ASSISTANT, content="Good"))
result = await reducer.reduce()
# Must reduce — history has 5 messages but target is 1
assert result is not None
# Should contain only the system message
assert len(result.messages) == 1
assert result.messages[0].role == AuthorRole.SYSTEM
assert result.messages[0].content == "System prompt"
async def test_truncation_without_system_message():
"""Verify truncation works correctly when there is no system message."""
msgs = [
ChatMessageContent(role=AuthorRole.USER, content="User message 1"),
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Assistant message 1"),
ChatMessageContent(role=AuthorRole.USER, content="User message 2"),
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Assistant message 2"),
]
reducer = ChatHistoryTruncationReducer(target_count=2)
reducer.messages = msgs
result = await reducer.reduce()
assert result is not None
assert len(result) == 2
assert result[0] == msgs[-2]
assert result[1] == msgs[-1]
# --- Direct tests for locate_safe_reduction_index ---
def test_locate_safe_reduction_index_with_system_message():
"""Test locate_safe_reduction_index adjusts target_count for system message."""
history = [
ChatMessageContent(role=AuthorRole.SYSTEM, content="System"),
ChatMessageContent(role=AuthorRole.USER, content="User 1"),
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Asst 1"),
ChatMessageContent(role=AuthorRole.USER, content="User 2"),
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Asst 2"),
]
# target_count=3 with system message → adjusted to 2, so truncation_index = 5 - 2 = 3
index = locate_safe_reduction_index(history, target_count=3, has_system_message=True)
assert index is not None
assert index == 3 # Keep last 2 messages, system message re-added by caller
def test_locate_safe_reduction_index_without_system_message():
"""Test locate_safe_reduction_index without system message flag."""
history = [
ChatMessageContent(role=AuthorRole.USER, content="User 1"),
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Asst 1"),
ChatMessageContent(role=AuthorRole.USER, content="User 2"),
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Asst 2"),
]
# target_count=2, no system message → truncation_index = 4 - 2 = 2
index = locate_safe_reduction_index(history, target_count=2)
assert index is not None
assert index == 2
def test_locate_safe_reduction_index_target_zero_returns_len(caplog):
"""When target_count=1 + has_system_message → adjusted to 0 → returns len(history)."""
history = [
ChatMessageContent(role=AuthorRole.SYSTEM, content="System"),
ChatMessageContent(role=AuthorRole.USER, content="User 1"),
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Asst 1"),
]
with caplog.at_level(logging.WARNING):
index = locate_safe_reduction_index(history, target_count=1, has_system_message=True)
assert index == len(history)
assert "target_count after accounting for system message is 0" in caplog.text
def test_locate_safe_reduction_index_no_reduction_needed():
"""When total <= target + threshold, returns None (no reduction needed)."""
history = [
ChatMessageContent(role=AuthorRole.USER, content="User 1"),
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Asst 1"),
]
index = locate_safe_reduction_index(history, target_count=2, threshold_count=0)
assert index is None
# --- Multiple system/developer messages ---
async def test_truncation_multiple_system_messages():
"""Only the first system/developer message is preserved; others may be lost."""
msgs = [
ChatMessageContent(role=AuthorRole.SYSTEM, content="System prompt 1"),
ChatMessageContent(role=AuthorRole.DEVELOPER, content="Developer instructions"),
ChatMessageContent(role=AuthorRole.USER, content="User 1"),
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Asst 1"),
ChatMessageContent(role=AuthorRole.USER, content="User 2"),
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Asst 2"),
]
reducer = ChatHistoryTruncationReducer(target_count=2)
reducer.messages = msgs
result = await reducer.reduce()
assert result is not None
# First system message is preserved
assert result[0].role == AuthorRole.SYSTEM
assert result[0].content == "System prompt 1"
# --- System message in retained tail (not truncated away) ---
async def test_truncation_system_message_in_retained_tail():
"""When system message falls within the retained portion, it is not duplicated."""
msgs = [
ChatMessageContent(role=AuthorRole.USER, content="User 1"),
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Asst 1"),
ChatMessageContent(role=AuthorRole.USER, content="User 2"),
ChatMessageContent(role=AuthorRole.SYSTEM, content="Late system msg"),
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Asst 2"),
]
# target_count=3, system at index 3 is within the naive tail (indices 2..4).
# No target_count adjustment needed — system message stays in its natural position.
reducer = ChatHistoryTruncationReducer(target_count=3)
reducer.messages = msgs
result = await reducer.reduce()
assert result is not None
# Should have exactly target_count messages, with system message not duplicated
assert len(result.messages) == 3
system_msgs = [m for m in result.messages if m.role == AuthorRole.SYSTEM]
assert len(system_msgs) == 1
# --- Warning log test ---
async def test_truncation_target_1_logs_warning(caplog):
"""Verify a warning is logged when target_count=1 with a system message."""
reducer = ChatHistoryTruncationReducer(target_count=1, system_message="System")
reducer.add_message(ChatMessageContent(role=AuthorRole.USER, content="Hello"))
reducer.add_message(ChatMessageContent(role=AuthorRole.ASSISTANT, content="Hi"))
with caplog.at_level(logging.WARNING):
result = await reducer.reduce()
assert result is not None
assert "target_count after accounting for system message is 0" in caplog.text
@@ -0,0 +1,417 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from defusedxml.ElementTree import XML
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.file_reference_content import FileReferenceContent
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.contents.utils.finish_reason import FinishReason
def test_cmc():
message = ChatMessageContent(role=AuthorRole.USER, content="Hello, world!")
assert message.role == AuthorRole.USER
assert message.content == "Hello, world!"
assert len(message.items) == 1
def test_cmc_str():
message = ChatMessageContent(role="user", content="Hello, world!")
assert message.role == AuthorRole.USER
assert str(message) == "Hello, world!"
def test_cmc_full():
message = ChatMessageContent(
role=AuthorRole.USER,
name="username",
content="Hello, world!",
inner_content="Hello, world!",
encoding="utf-8",
ai_model_id="1234",
metadata={"test": "test"},
finish_reason="stop",
)
assert message.role == AuthorRole.USER
assert message.name == "username"
assert message.content == "Hello, world!"
assert message.finish_reason == FinishReason.STOP
assert len(message.items) == 1
def test_cmc_items():
message = ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Hello, world!")])
assert message.role == AuthorRole.USER
assert message.content == "Hello, world!"
assert len(message.items) == 1
def test_cmc_items_and_content():
message = ChatMessageContent(role=AuthorRole.USER, content="text", items=[TextContent(text="Hello, world!")])
assert message.role == AuthorRole.USER
assert message.content == "Hello, world!"
assert message.items[0].text == "Hello, world!"
assert message.items[1].text == "text"
assert len(message.items) == 2
def test_cmc_multiple_items():
message = ChatMessageContent(
role=AuthorRole.SYSTEM,
items=[TextContent(text="Hello, world!"), TextContent(text="Hello, world!"), ImageContent(uri="http://test/")],
)
assert message.role == AuthorRole.SYSTEM
assert message.content == "Hello, world!"
assert len(message.items) == 3
def test_cmc_content_set():
message = ChatMessageContent(role=AuthorRole.USER, content="Hello, world!")
assert message.role == AuthorRole.USER
assert message.content == "Hello, world!"
message.content = "Hello, world to you too!"
assert len(message.items) == 1
assert message.items[0].text == "Hello, world to you too!"
message.content = ""
assert message.items[0].text == "Hello, world to you too!"
def test_cmc_content_set_empty():
message = ChatMessageContent(role=AuthorRole.USER, content="Hello, world!")
assert message.role == AuthorRole.USER
assert message.content == "Hello, world!"
message.items.pop()
message.content = "Hello, world to you too!"
assert len(message.items) == 1
assert message.items[0].text == "Hello, world to you too!"
def test_cmc_to_element():
message = ChatMessageContent(
role=AuthorRole.USER, items=[TextContent(text="Hello, world!", encoding="utf8")], name=None
)
element = message.to_element()
assert element.tag == "message"
assert element.attrib == {"role": "user"}
assert element.get("name") is None
for child in element:
assert child.tag == "text"
assert child.text == "Hello, world!"
@pytest.mark.parametrize(
"message",
[
ChatMessageContent(
role=AuthorRole.USER,
items=[
TextContent(text="test"),
],
),
ChatMessageContent(
role=AuthorRole.USER,
items=[ImageContent(uri="http://test/")],
),
ChatMessageContent(
role=AuthorRole.USER,
items=[ImageContent(data=b"test_data", mime_type="image/jpeg")],
),
ChatMessageContent(
role=AuthorRole.USER, items=[FunctionCallContent(id="test", name="func_name", arguments="args")]
),
ChatMessageContent(
role=AuthorRole.USER,
items=[FunctionResultContent(id="test", name="func_name", result="result")],
),
ChatMessageContent(
role=AuthorRole.USER,
items=[
TextContent(text="Hello, world!"),
FunctionCallContent(id="test", name="func_name", arguments="args"),
FunctionResultContent(id="test", name="func_name", result="result"),
ImageContent(uri="http://test/"),
],
),
],
ids=["text", "image_uri", "image_data", "function_call", "function_result", "all"],
)
def test_cmc_to_from_element(message):
element = message.to_element()
new_message = ChatMessageContent.from_element(element)
assert message == new_message
def test_cmc_to_prompt():
message = ChatMessageContent(role=AuthorRole.USER, content="Hello, world!")
prompt = message.to_prompt()
assert prompt == '<message role="user"><text>Hello, world!</text></message>'
def test_cmc_from_element():
element = ChatMessageContent(role=AuthorRole.USER, content="Hello, world!").to_element()
message = ChatMessageContent.from_element(element)
assert message.role == AuthorRole.USER
assert message.content == "Hello, world!"
assert len(message.items) == 1
def test_cmc_from_element_content():
xml_content = '<message role="user">Hello, world!</message>'
element = XML(text=xml_content)
message = ChatMessageContent.from_element(element)
assert message.role == AuthorRole.USER
assert message.content == "Hello, world!"
assert len(message.items) == 1
@pytest.mark.parametrize(
"xml_content, user, text_content, length",
[
('<message role="user">Hello, world!</message>', "user", "Hello, world!", 1),
('<message role="user"><text>Hello, world!</text></message>', "user", "Hello, world!", 1),
(
'<message role="user"><text>Hello, world!</text><text>Hello, world!</text></message>',
"user",
"Hello, world!Hello, world!",
1,
),
(
'<message role="assistant"><function_call id="test" name="func_name">args</function_call></message>',
"assistant",
"",
1,
),
(
'<message role="tool"><function_result id="test" name="func_name">function result</function_result></message>', # noqa: E501
"tool",
"",
1,
),
(
'<message role="user"><text>Hello, world!</text><function_call id="test" name="func_name">args</function_call></message>', # noqa: E501
"user",
"Hello, world!",
2,
),
(
'<message role="user"><random>some random code sample</random>in between text<text>test</text></message>',
"user",
"<random>some random code sample</random>in between texttest",
1, # TODO: review this case
),
('<message role="user" choice_index="0">Hello, world!</message>', "user", "Hello, world!", 1),
('<message role="user"><image>data:image/jpeg;base64,dGVzdF9kYXRh</image></message>', "user", "", 1),
],
ids=[
"no_tag",
"text_tag",
"double_text_tag",
"function_call",
"function_result",
"combined",
"unknown_tag",
"streaming",
"image",
],
)
def test_cmc_from_element_content_parse(xml_content, user, text_content, length):
element = XML(text=xml_content)
message = ChatMessageContent.from_element(element)
assert message.role.value == user
assert str(message) == text_content
assert len(message.items) == length
def test_cmc_serialize():
message = ChatMessageContent(role=AuthorRole.USER, content="Hello, world!")
dumped = message.model_dump()
assert dumped["role"] == AuthorRole.USER
assert dumped["items"][0]["text"] == "Hello, world!"
def test_cmc_to_dict():
message = ChatMessageContent(role=AuthorRole.USER, content="Hello, world!")
assert message.to_dict() == {
"role": "user",
"content": "Hello, world!",
}
def test_cmc_to_dict_keys():
message = ChatMessageContent(role=AuthorRole.USER, content="Hello, world!")
assert message.to_dict(role_key="author", content_key="text") == {
"author": "user",
"text": "Hello, world!",
}
@pytest.mark.parametrize(
"input_args, expected_dict",
[
({"role": "user", "content": "Hello, world!"}, {"role": "user", "content": "Hello, world!"}),
(
{"role": "user", "content": "Hello, world!", "name": "username"},
{"role": "user", "content": "Hello, world!", "name": "username"},
),
({"role": "user", "items": [TextContent(text="Hello, world!")]}, {"role": "user", "content": "Hello, world!"}),
(
{"role": "assistant", "items": [FunctionCallContent(id="test", name="func_name", arguments="args")]},
{
"role": "assistant",
"tool_calls": [
{"id": "test", "type": "function", "function": {"name": "func_name", "arguments": "args"}}
],
},
),
(
{"role": "tool", "items": [FunctionResultContent(id="test", name="func_name", result="result")]},
{"role": "tool", "tool_call_id": "test", "content": "result"},
),
(
{
"role": "user",
"items": [
TextContent(text="Hello, "),
TextContent(text="world!"),
],
},
{
"role": "user",
"content": [{"type": "text", "text": "Hello, "}, {"type": "text", "text": "world!"}],
},
),
(
{
"role": "user",
"items": [
{"content_type": "text", "text": "Hello, "},
{"content_type": "text", "text": "world!"},
],
},
{
"role": "user",
"content": [{"type": "text", "text": "Hello, "}, {"type": "text", "text": "world!"}],
},
),
(
{
"role": "user",
"items": [
{"content_type": "annotation", "file_id": "test"},
],
},
{
"role": "user",
"content": [{"type": "text", "text": "type=None, test None (Start Index=None->End Index=None)"}],
},
),
(
{
"role": "user",
"items": [
{"content_type": "file_reference", "file_id": "test"},
],
},
{
"role": "user",
"content": [{"file_id": "test"}],
},
),
(
{
"role": "user",
"items": [
{"content_type": "function_call", "name": "test-test"},
],
},
{
"role": "user",
"content": [{"id": None, "type": "function", "function": {"name": "test-test", "arguments": None}}],
},
),
(
{
"role": "user",
"items": [
{"content_type": "function_call", "name": "test-test"},
{"content_type": "function_result", "name": "test-test", "result": "test", "id": "test"},
],
},
{
"role": "user",
"content": [
{"id": None, "type": "function", "function": {"name": "test-test", "arguments": None}},
{"tool_call_id": "test", "content": "test"},
],
},
),
(
{
"role": "user",
"items": [
{"content_type": "image", "uri": "http://test"},
],
},
{
"role": "user",
"content": [{"image_url": {"url": "http://test/"}, "type": "image_url"}],
},
),
],
ids=[
"user_content",
"user_with_name",
"user_item",
"function_call",
"function_result",
"multiple_items",
"multiple_items_serialize",
"annotations_serialize",
"file_reference_serialize",
"function_call_serialize",
"function_result_serialize",
"image_serialize",
],
)
def test_cmc_to_dict_items(input_args, expected_dict):
message = ChatMessageContent(**input_args)
assert message.to_dict() == expected_dict
def test_cmc_with_unhashable_types_can_hash():
user_messages = [
ChatMessageContent(
role=AuthorRole.USER,
items=[
TextContent(text="Describe this image."),
ImageContent(
uri="https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/New_york_times_square-terabass.jpg/1200px-New_york_times_square-terabass.jpg"
),
],
),
ChatMessageContent(
role=AuthorRole.USER,
items=[
TextContent(text="What is the main color in this image?"),
ImageContent(uri="https://upload.wikimedia.org/wikipedia/commons/5/56/White_shark.jpg"),
],
),
ChatMessageContent(
role=AuthorRole.USER,
items=[
TextContent(text="Is there an animal in this image?"),
FileReferenceContent(file_id="test_file_id"),
],
),
ChatMessageContent(
role=AuthorRole.USER,
),
]
for message in user_messages:
assert hash(message) is not None
+312
View File
@@ -0,0 +1,312 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Any
import numpy as np
import pytest
from semantic_kernel.contents.utils.data_uri import DataUri
from semantic_kernel.exceptions.content_exceptions import ContentInitializationError
@pytest.mark.parametrize(
"uri, data_bytes, data_str, mime_type, parameters, data_format",
[
pytest.param(
"data:image/jpeg;base64,dGVzdF9kYXRh",
b"test_data",
"dGVzdF9kYXRh",
"image/jpeg",
{},
"base64",
id="basic_image",
),
pytest.param(
"data:text/plain;,test_data",
b"test_data",
"test_data",
"text/plain",
{},
None,
id="basic_text",
),
pytest.param(
"data:application/octet-stream;base64,AQIDBA==",
b"\x01\x02\x03\x04",
"AQIDBA==",
"application/octet-stream",
{},
"base64",
id="basic_binary",
),
pytest.param(
"data:text/plain;base64,U29t\r\nZQ==\t",
b"Some",
"U29tZQ==",
"text/plain",
{},
"base64",
id="strip_whitespace",
),
pytest.param(
"data:application/octet-stream;utf8,01-02-03-04",
b"01-02-03-04",
"01-02-03-04",
"application/octet-stream",
{},
"utf8",
id="utf8",
),
pytest.param(
"data:text/plain;key=value;base64,U29t\r\nZQ==\t",
b"Some",
"U29tZQ==",
"text/plain",
{"key": "value"},
"base64",
id="with_params",
),
],
)
def test_data_uri_from_data_uri_str(
uri: str,
data_bytes: bytes | None,
data_str: str | None,
mime_type: str | None,
parameters: dict[str, str],
data_format: str | None,
):
data_uri = DataUri.from_data_uri(uri)
assert data_uri.data_bytes == data_bytes
assert data_uri.mime_type == mime_type
assert data_uri.parameters == parameters
assert data_uri.data_format == data_format
assert data_uri._data_str() == data_str
@pytest.mark.parametrize(
"uri, exception",
[
pytest.param("", ContentInitializationError, id="empty"),
pytest.param("data", ContentInitializationError, id="missing_colon"),
pytest.param("data:", ContentInitializationError, id="missing_comma"),
pytest.param("data:something,", ContentInitializationError, id="mime_type_without_subtype"),
pytest.param("data:something;else,data", ContentInitializationError, id="mime_type_without_subtype2"),
pytest.param("data:image/jpeg;base64,dGVzdF9kYXRh;foo=bar", ContentInitializationError, id="wrong_order"),
pytest.param("data:text/plain;test_data", ContentInitializationError, id="missing_comma"),
pytest.param(
"data:text/plain;base64,something!",
ContentInitializationError,
id="invalid_char",
),
pytest.param(
"data:text/plain;base64,U29",
ContentInitializationError,
id="missing_padding",
),
],
)
def test_data_uri_from_data_uri_fail(uri: str, exception: type[Exception]):
with pytest.raises(exception):
DataUri.from_data_uri(uri)
def test_data_uri_to_string_with_extra_metadata():
uri = DataUri.from_data_uri("data:image/jpeg;base64,dGVzdF9kYXRh")
assert uri.to_string(metadata={"foo": "bar"}) == "data:image/jpeg;foo=bar;base64,dGVzdF9kYXRh"
def test_default_mime_type():
uri = DataUri.from_data_uri("data:;base64,dGVzdF9kYXRh", default_mime_type="image/jpeg")
assert uri.mime_type == "image/jpeg"
@pytest.mark.parametrize(
"fields, uri",
[
pytest.param(
{
"data_bytes": b"test_data",
"mime_type": "image/jpeg",
"data_format": "base64",
},
"data:image/jpeg;base64,dGVzdF9kYXRh",
id="basic_image",
),
pytest.param(
{"data_str": "test_data", "mime_type": "text/plain"}, "data:text/plain;,test_data", id="basic_text"
),
pytest.param(
{
"data_bytes": b"\x01\x02\x03\x04",
"mime_type": "application/octet-stream",
"data_format": "base64",
},
"data:application/octet-stream;base64,AQIDBA==",
id="basic_binary",
),
pytest.param(
{
"data_str": "test_data/r/t",
"mime_type": "image/jpeg",
},
"data:image/jpeg;,test_data/r/t",
id="whitespace_not_base64",
),
pytest.param(
{
"data_bytes": b"test_data",
"mime_type": "image/jpeg",
"data_format": "base64",
"parameters": None,
},
"data:image/jpeg;base64,dGVzdF9kYXRh",
id="param_none",
),
pytest.param(
{
"data_bytes": b"test_data",
"mime_type": "image/jpeg",
"data_format": "base64",
"parameters": [],
},
"data:image/jpeg;base64,dGVzdF9kYXRh",
id="param_empty_list",
),
pytest.param(
{
"data_bytes": b"test_data",
"mime_type": "image/jpeg",
"data_format": "base64",
"parameters": [""],
},
"data:image/jpeg;base64,dGVzdF9kYXRh",
id="param_empty_list",
),
pytest.param(
{
"data_bytes": b"test_data",
"mime_type": "image/jpeg",
"data_format": "base64",
"parameters": {},
},
"data:image/jpeg;base64,dGVzdF9kYXRh",
id="param_empty_dict",
),
pytest.param(
{
"data_bytes": b"test_data",
"mime_type": "image/jpeg",
"data_format": "base64",
"parameters": {"foo": "bar"},
},
"data:image/jpeg;base64,dGVzdF9kYXRh",
id="param_dict",
),
],
)
def test_data_uri_from_fields(fields: dict[str, Any], uri: str):
data_uri = DataUri(**fields)
assert data_uri.to_string() == uri
@pytest.mark.parametrize(
"fields",
[
pytest.param(
{
"data_str": "test_data/r/t",
"mime_type": "image/jpeg",
"data_format": "base64",
},
id="whitespace",
),
pytest.param(
{
"data_str": "test_data/r/t",
"mime_type": "image/jpeg",
"data_format": "base64",
"parameters": ["foo"],
},
id="invalid_params",
),
],
)
def test_data_uri_from_fields_fail(fields: dict[str, Any]):
with pytest.raises(ContentInitializationError):
DataUri(**fields)
def test_eq():
data_uri1 = DataUri.from_data_uri("data:image/jpeg;base64,dGVzdF9kYXRh")
data_uri2 = DataUri.from_data_uri("data:image/jpeg;base64,dGVzdF9kYXRh")
assert data_uri1 == data_uri2
assert data_uri1 != "data:image/jpeg;base64,dGVzdF9kYXRh"
assert data_uri1 != DataUri.from_data_uri("data:image/jpeg;base64,dGVzdF9kYXRi")
def test_array():
arr = np.array([[1, 2], [3, 4]], dtype=np.uint8)
data_uri = DataUri(data_array=arr, mime_type="application/octet-stream", data_format="base64")
encoded = data_uri.to_string()
assert data_uri.data_array is not None
assert "data:application/octet-stream;base64," in encoded
assert data_uri.data_array.tobytes() == b"\x01\x02\x03\x04"
@pytest.mark.parametrize(
"data_bytes, data_str, data_array, data_format, expected_output",
[
pytest.param(
b"test_data",
None,
None,
"base64",
"dGVzdF9kYXRh",
id="bytes_base64",
),
pytest.param(
b"test_data",
None,
None,
"plain",
"test_data",
id="bytes_non_base64",
),
pytest.param(
None,
"dGVzdF9kYXRh",
None,
"base64",
"dGVzdF9kYXRh",
id="string_base64",
),
pytest.param(
None,
"plain_data",
None,
None,
"plain_data",
id="string_non_base64",
),
pytest.param(
None,
None,
np.array([1, 2, 3], dtype=np.uint8),
"base64",
"AQID",
id="array_base64",
),
pytest.param(
None,
None,
np.array([1, 2, 3], dtype=np.uint8),
"plain",
"\1\2\3",
id="array_non_base64",
),
],
)
def test__data_str(data_bytes, data_str, data_array, data_format, expected_output):
data_uri = DataUri(data_bytes=data_bytes, data_str=data_str, data_array=data_array, data_format=data_format)
assert data_uri._data_str() == expected_output
@@ -0,0 +1,76 @@
# Copyright (c) Microsoft. All rights reserved.
from xml.etree.ElementTree import Element
import pytest
from semantic_kernel.contents.file_reference_content import FileReferenceContent
def test_create_empty():
file_reference = FileReferenceContent()
assert file_reference.file_id is None
def test_create_file_id():
file_reference = FileReferenceContent(file_id="12345")
assert file_reference.file_id == "12345"
def test_update_file_id():
file_reference = FileReferenceContent()
file_reference.file_id = "12345"
assert file_reference.file_id == "12345"
def test_to_str():
file_reference = FileReferenceContent(file_id="12345")
assert str(file_reference) == "FileReferenceContent(file_id=12345)"
def test_to_element():
file_reference = FileReferenceContent(file_id="12345")
element = file_reference.to_element()
assert element.tag == "file_reference"
assert element.get("file_id") == "12345"
def test_from_element():
element = Element("FileReferenceContent")
element.set("file_id", "12345")
file_reference = FileReferenceContent.from_element(element)
assert file_reference.file_id == "12345"
def test_to_dict_simple():
file_reference = FileReferenceContent(file_id="12345")
assert file_reference.to_dict() == {
"file_id": "12345",
}
@pytest.mark.parametrize(
"file_reference",
[
pytest.param(FileReferenceContent(file_id="12345"), id="file_id"),
pytest.param(FileReferenceContent(), id="empty"),
],
)
def test_element_roundtrip(file_reference):
element = file_reference.to_element()
new_file_reference = FileReferenceContent.from_element(element)
assert new_file_reference == file_reference
@pytest.mark.parametrize(
"file_reference",
[
pytest.param(FileReferenceContent(file_id="12345"), id="file_id"),
pytest.param(FileReferenceContent(), id="empty"),
],
)
def test_to_dict(file_reference):
expected_dict = {
"file_id": file_reference.file_id,
}
assert file_reference.to_dict() == expected_dict
@@ -0,0 +1,212 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from semantic_kernel.contents.function_call_content import FunctionCallContent
from semantic_kernel.exceptions.content_exceptions import (
ContentAdditionException,
FunctionCallInvalidArgumentsException,
FunctionCallInvalidNameException,
)
from semantic_kernel.functions.kernel_arguments import KernelArguments
def test_init_from_names():
# Test initializing function call from names
fc = FunctionCallContent(function_name="Function", plugin_name="Test", arguments="""{"input": "world"}""")
assert fc.name == "Test-Function"
assert fc.function_name == "Function"
assert fc.plugin_name == "Test"
assert fc.arguments == """{"input": "world"}"""
assert str(fc) == 'Test-Function({"input": "world"})'
def test_init_dict_args():
# Test initializing function call with the args already as a dictionary
fc = FunctionCallContent(function_name="Function", plugin_name="Test", arguments={"input": "world"})
assert fc.name == "Test-Function"
assert fc.function_name == "Function"
assert fc.plugin_name == "Test"
assert fc.arguments == {"input": "world"}
assert str(fc) == 'Test-Function({"input": "world"})'
def test_init_with_metadata():
# Test initializing function call from names
fc = FunctionCallContent(function_name="Function", plugin_name="Test", metadata={"test": "test"})
assert fc.name == "Test-Function"
assert fc.function_name == "Function"
assert fc.plugin_name == "Test"
assert fc.metadata == {"test": "test"}
def test_function_call(function_call: FunctionCallContent):
assert function_call.name == "Test-Function"
assert function_call.arguments == """{"input": "world"}"""
assert function_call.function_name == "Function"
assert function_call.plugin_name == "Test"
def test_add(function_call: FunctionCallContent):
# Test adding two function calls
fc2 = FunctionCallContent(id="test", name="Test-Function", arguments="""{"input2": "world2"}""")
fc3 = function_call + fc2
assert fc3.name == "Test-Function"
assert fc3.arguments == """{"input": "world"}{"input2": "world2"}"""
def test_add_empty():
# Test adding two function calls
fc1 = FunctionCallContent(id="test1", name="Test-Function", arguments=None)
fc2 = FunctionCallContent(id="test1", name="Test-Function", arguments="")
fc3 = fc1 + fc2
assert fc3.name == "Test-Function"
assert fc3.arguments == "{}"
fc1 = FunctionCallContent(id="test1", name="Test-Function", arguments="""{"input2": "world2"}""")
fc2 = FunctionCallContent(id="test1", name="Test-Function", arguments="")
fc3 = fc1 + fc2
assert fc3.name == "Test-Function"
assert fc3.arguments == """{"input2": "world2"}"""
fc1 = FunctionCallContent(id="test1", name="Test-Function", arguments="{}")
fc2 = FunctionCallContent(id="test1", name="Test-Function", arguments="""{"input2": "world2"}""")
fc3 = fc1 + fc2
assert fc3.name == "Test-Function"
assert fc3.arguments == """{"input2": "world2"}"""
def test_add_none(function_call: FunctionCallContent):
# Test adding two function calls with one being None
fc2 = None
fc3 = function_call + fc2
assert fc3.name == "Test-Function"
assert fc3.arguments == """{"input": "world"}"""
def test_add_dict_args():
# Test adding two function calls
fc1 = FunctionCallContent(id="test1", name="Test-Function", arguments={"input1": "world"})
fc2 = FunctionCallContent(id="test1", name="Test-Function", arguments={"input2": "world2"})
fc3 = fc1 + fc2
assert fc3.name == "Test-Function"
assert fc3.arguments == {"input1": "world", "input2": "world2"}
def test_add_one_dict_args_fail():
# Test adding two function calls
fc1 = FunctionCallContent(id="test1", name="Test-Function", arguments="""{"input1": "world"}""")
fc2 = FunctionCallContent(id="test1", name="Test-Function", arguments={"input2": "world2"})
with pytest.raises(ContentAdditionException):
fc1 + fc2
def test_add_fail_id():
# Test adding two function calls
fc1 = FunctionCallContent(id="test1", name="Test-Function", arguments="""{"input2": "world2"}""")
fc2 = FunctionCallContent(id="test2", name="Test-Function", arguments="""{"input2": "world2"}""")
with pytest.raises(ContentAdditionException):
fc1 + fc2
def test_add_fail_index():
# Test adding two function calls
fc1 = FunctionCallContent(id="test", index=0, name="Test-Function", arguments="""{"input2": "world2"}""")
fc2 = FunctionCallContent(id="test", index=1, name="Test-Function", arguments="""{"input2": "world2"}""")
with pytest.raises(ContentAdditionException):
fc1 + fc2
def test_parse_arguments(function_call: FunctionCallContent):
# Test parsing arguments to dictionary
assert function_call.parse_arguments() == {"input": "world"}
def test_parse_arguments_dict():
# Test parsing arguments to dictionary
fc = FunctionCallContent(id="test", name="Test-Function", arguments={"input": "world"})
assert fc.parse_arguments() == {"input": "world"}
def test_parse_arguments_none():
# Test parsing arguments to dictionary
fc = FunctionCallContent(id="test", name="Test-Function")
assert fc.parse_arguments() is None
def test_parse_arguments_single_quotes():
# Test parsing arguments that are single quoted
fc = FunctionCallContent(id="test", name="Test-Function", arguments="{'input': 'world'}")
assert fc.parse_arguments() == {"input": "world"}
fc = FunctionCallContent(id="test", name="Test-Function", arguments="{'input': 'Let\\'s go'}")
assert fc.parse_arguments() == {"input": "Let's go"}
# Note this the below test is not a valid json string
fc = FunctionCallContent(id="test", name="Test-Function", arguments="{'input': 'Let's go'}")
with pytest.raises(FunctionCallInvalidArgumentsException):
fc.parse_arguments()
def test_parse_arguments_fail():
# Test parsing arguments to dictionary
fc = FunctionCallContent(id=None, name="Test-Function", arguments="""{"input": "world}""")
with pytest.raises(FunctionCallInvalidArgumentsException):
fc.parse_arguments()
def test_to_kernel_arguments(function_call: FunctionCallContent):
# Test parsing arguments to variables
arguments = KernelArguments()
assert isinstance(function_call.to_kernel_arguments(), KernelArguments)
arguments.update(function_call.to_kernel_arguments())
assert arguments["input"] == "world"
def test_to_kernel_arguments_none():
# Test parsing arguments to variables
fc = FunctionCallContent(id="test", name="Test-Function")
assert fc.to_kernel_arguments() == KernelArguments()
def test_split_name(function_call: FunctionCallContent):
# Test splitting the name into plugin and function name
assert function_call.split_name() == ["Test", "Function"]
def test_split_name_name_only():
# Test splitting the name into plugin and function name
fc = FunctionCallContent(id="test", name="Function")
assert fc.split_name() == ["", "Function"]
def test_split_name_dict(function_call: FunctionCallContent):
# Test splitting the name into plugin and function name
assert function_call.split_name_dict() == {"plugin_name": "Test", "function_name": "Function"}
def test_split_name_none():
fc = FunctionCallContent(id="1234")
with pytest.raises(FunctionCallInvalidNameException):
fc.split_name()
def test_fc_dump(function_call: FunctionCallContent):
# Test dumping the function call to dictionary
dumped = function_call.model_dump(exclude_none=True)
assert dumped == {
"content_type": "function_call",
"id": "test",
"name": "Test-Function",
"function_name": "Function",
"plugin_name": "Test",
"arguments": '{"input": "world"}',
"metadata": {},
}
def test_fc_dump_json(function_call: FunctionCallContent):
# Test dumping the function call to dictionary
dumped = function_call.model_dump_json(exclude_none=True)
assert (
dumped
== """{"metadata":{},"content_type":"function_call","id":"test","name":"Test-Function","function_name":"Function","plugin_name":"Test","arguments":"{\\"input\\": \\"world\\"}"}""" # noqa: E501
)
@@ -0,0 +1,219 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import Mock
import pytest
from pydantic import BaseModel, ConfigDict, Field
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.functions.function_result import FunctionResult
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata
from semantic_kernel.kernel_pydantic import KernelBaseModel
class CustomResultClass:
"""Custom class for testing."""
def __init__(self, result):
self.result = result
def __str__(self) -> str:
return self.result
class CustomObjectWithList:
"""Custom class for testing."""
def __init__(self, items):
self.items = items
def __str__(self):
return f"CustomObjectWithList({self.items})"
class AccountBalanceFrozen(KernelBaseModel):
# Make the model frozen so it's hashable
balance: int = Field(default=..., alias="account_balance")
model_config = ConfigDict(frozen=True)
class AccountBalanceNonFrozen(KernelBaseModel):
# This model is not frozen and thus not hashable by default
balance: int = Field(default=..., alias="account_balance")
def test_init():
frc = FunctionResultContent(id="test", name="test-function", result="test-result", metadata={"test": "test"})
assert frc.name == "test-function"
assert frc.function_name == "function"
assert frc.plugin_name == "test"
assert frc.metadata == {"test": "test"}
assert frc.result == "test-result"
assert str(frc) == "test-result"
assert frc.split_name() == ["test", "function"]
assert frc.to_dict() == {
"tool_call_id": "test",
"content": "test-result",
}
def test_init_from_names():
frc = FunctionResultContent(id="test", function_name="Function", plugin_name="Test", result="test-result")
assert frc.name == "Test-Function"
assert frc.function_name == "Function"
assert frc.plugin_name == "Test"
assert frc.result == "test-result"
assert str(frc) == "test-result"
@pytest.mark.parametrize(
"result",
[
"Hello world!",
123,
{"test": "test"},
FunctionResult(function=Mock(spec=KernelFunctionMetadata), value="Hello world!"),
TextContent(text="Hello world!"),
ChatMessageContent(role="user", content="Hello world!"),
ChatMessageContent(role="user", items=[ImageContent(uri="https://example.com")]),
ChatMessageContent(role="user", items=[FunctionResultContent(id="test", name="test", result="Hello world!")]),
[1, 2, 3],
[{"key": "value"}, {"another": "item"}],
{"a", "b"},
CustomResultClass("test"),
CustomObjectWithList(["one", "two", "three"]),
],
ids=[
"str",
"int",
"dict",
"FunctionResult",
"TextContent",
"ChatMessageContent",
"ChatMessageContent-ImageContent",
"ChatMessageContent-FunctionResultContent",
"list",
"list_of_dicts",
"set",
"CustomResultClass",
"CustomObjectWithList",
],
)
def test_from_fcc_and_result(result: any):
fcc = FunctionCallContent(
id="test", name="test-function", arguments='{"input": "world"}', metadata={"test": "test"}
)
frc = FunctionResultContent.from_function_call_content_and_result(fcc, result, {"test2": "test2"})
assert frc.name == "test-function"
assert frc.function_name == "function"
assert frc.plugin_name == "test"
assert frc.result is not None
assert frc.metadata == {"test": "test", "test2": "test2"}
def test_to_cmc():
frc = FunctionResultContent(id="test", name="test-function", result="test-result")
cmc = frc.to_chat_message_content()
assert cmc.role.value == "tool"
assert cmc.items[0].result == "test-result"
def test_serialize():
class CustomResultClass:
def __init__(self, result):
self.result = result
def __str__(self) -> str:
return self.result
custom_result = CustomResultClass(result="test")
frc = FunctionResultContent(id="test", name="test-function", result=custom_result)
assert (
frc.model_dump_json(exclude_none=True)
== """{"metadata":{},"content_type":"function_result","id":"test","result":"test","name":"test-function","function_name":"function","plugin_name":"test"}""" # noqa: E501
)
def test_hash_with_frozen_account_balance():
balance = AccountBalanceFrozen(account_balance=100)
content = FunctionResultContent(
id="test_id",
result=balance,
function_name="TestFunction",
)
_ = hash(content)
assert True, "Hashing FunctionResultContent with frozen model should not raise errors."
def test_hash_with_dict_result():
balance_dict = {"account_balance": 100}
content = FunctionResultContent(
id="test_id",
result=balance_dict,
function_name="TestFunction",
)
_ = hash(content)
assert True, "Hashing FunctionResultContent with dict result should not raise errors."
def test_hash_with_nested_dict_result():
nested_dict = {"account_balance": 100, "details": {"currency": "USD", "last_updated": "2025-01-28"}}
content = FunctionResultContent(
id="test_id_nested",
result=nested_dict,
function_name="TestFunctionNested",
)
_ = hash(content)
assert True, "Hashing FunctionResultContent with nested dict result should not raise errors."
def test_hash_with_list_result():
balance_list = [100, 200, 300]
content = FunctionResultContent(
id="test_id_list",
result=balance_list,
function_name="TestFunctionList",
)
_ = hash(content)
assert True, "Hashing FunctionResultContent with list result should not raise errors."
def test_hash_with_set_result():
balance_set = {100, 200, 300}
content = FunctionResultContent(
id="test_id_set",
result=balance_set,
function_name="TestFunctionSet",
)
_ = hash(content)
assert True, "Hashing FunctionResultContent with set result should not raise errors."
def test_hash_with_custom_object_result():
class CustomObject(BaseModel):
field1: str
field2: int
custom_obj = CustomObject(field1="value1", field2=42)
content = FunctionResultContent(
id="test_id_custom",
result=custom_obj,
function_name="TestFunctionCustom",
)
_ = hash(content)
assert True, "Hashing FunctionResultContent with custom object result should not raise errors."
def test_unhashable_non_frozen_model_raises_type_error():
balance = AccountBalanceNonFrozen(account_balance=100)
content = FunctionResultContent(
id="test_id_unhashable",
result=balance,
function_name="TestFunctionUnhashable",
)
_ = hash(content)
@@ -0,0 +1,196 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Any
from pydantic import BaseModel
from semantic_kernel.contents.function_call_content import FunctionCallContent
from semantic_kernel.contents.function_result_content import FunctionResultContent
class SimpleModel(BaseModel):
field1: str
field2: int
class NestedModel(BaseModel):
name: str
values: list[int]
class ModelContainer(BaseModel):
container_name: str
nested_model: NestedModel
def test_hash_with_nested_structures():
"""
Deeply nested dictionaries and lists, but with no cyclical references.
Ensures multiple levels of nested transformations work.
"""
data = {
"level1": {
"list1": [1, 2, 3],
"dict1": {"keyA": "valA", "keyB": "valB"},
},
"level2": [
{"sub_dict1": {"x": 99}},
{"sub_dict2": {"y": 100}},
],
}
content = FunctionResultContent(
id="test_nested_structures",
result=data,
function_name="TestNestedStructures",
)
_ = hash(content)
assert True, "Hashing deeply nested structures succeeded."
def test_hash_with_repeated_references():
"""
Multiple references to the same object, but no cycle.
Ensures repeated objects are handled consistently and do not cause duplication.
"""
shared_dict = {"common_key": "common_value"}
data = {
"ref1": shared_dict,
"ref2": shared_dict, # same object, repeated reference
}
content = FunctionResultContent(
id="test_repeated_references",
result=data,
function_name="TestRepeatedRefs",
)
_ = hash(content)
assert True, "Hashing repeated references (no cycles) succeeded."
def test_hash_with_simple_pydantic_model():
"""
Hash a Pydantic model that doesn't reference itself or another model.
"""
model_instance = SimpleModel(field1="hello", field2=42)
content = FunctionResultContent(
id="test_simple_model",
result=model_instance,
function_name="TestSimpleModel",
)
_ = hash(content)
assert True, "Hashing a simple Pydantic model succeeded."
def test_hash_with_nested_pydantic_models():
"""
Hash a Pydantic model containing another Pydantic model, no cycles.
"""
nested = NestedModel(name="MyNestedModel", values=[1, 2, 3])
container = ModelContainer(container_name="TopLevel", nested_model=nested)
content = FunctionResultContent(
id="test_nested_models",
result=container,
function_name="TestNestedModels",
)
_ = hash(content)
assert True, "Hashing nested Pydantic models succeeded."
def test_hash_with_triple_cycle():
"""
Three dictionaries referencing each other to form a cycle.
This ensures that multi-node cycles are also handled.
"""
dict_a: dict[str, Any] = {"a_key": 1}
dict_b: dict[str, Any] = {"b_key": 2}
dict_c: dict[str, Any] = {"c_key": 3}
dict_a["ref_to_b"] = dict_b
dict_b["ref_to_c"] = dict_c
dict_c["ref_to_a"] = dict_a
content = FunctionResultContent(
id="test_triple_cycle",
result=dict_a,
function_name="TestTripleCycle",
)
_ = hash(content)
assert True, "Hashing triple cyclical references succeeded."
def test_hash_with_cyclical_references():
"""
The original cyclical references test for thorough coverage.
"""
class CyclicalModel(BaseModel):
name: str
partner: "CyclicalModel" = None # type: ignore
CyclicalModel.model_rebuild()
model_a = CyclicalModel(name="ModelA")
model_b = CyclicalModel(name="ModelB")
model_a.partner = model_b
model_b.partner = model_a
dict_x = {"x_key": 42}
dict_y = {"y_key": 99, "ref_to_x": dict_x}
dict_x["ref_to_y"] = dict_y # type: ignore
giant_data_structure = {
"models": [model_a, model_b],
"nested": {"cyclical_dict_x": dict_x, "cyclical_dict_y": dict_y},
}
content = FunctionResultContent(
id="test_id_cyclical",
result=giant_data_structure,
function_name="TestFunctionCyclical",
)
_ = hash(content)
def test_hash_with_large_structure():
"""
Tests performance or at least correctness when dealing with
a large structure, ensuring we don't crash or exceed recursion.
"""
large_list = list(range(1000))
large_dict = {f"key_{i}": i for i in range(1000)}
combined = {
"big_list": large_list,
"big_dict": large_dict,
"nested": [
{"inner_list": large_list},
{"inner_dict": large_dict},
],
}
content = FunctionResultContent(
id="test_large_structure",
result=combined,
function_name="TestLargeStructure",
)
_ = hash(content)
def test_hash_function_call_content():
call_content = FunctionCallContent(
inner_content=None,
ai_model_id=None,
metadata={},
id="call_LAbz",
index=None,
name="menu-get_specials",
function_name="get_specials",
plugin_name="menu",
arguments="{}",
)
content = FunctionResultContent(
id="test_function_call_content", result=call_content, function_name="TestFunctionCallContent"
)
_ = hash(content)
@@ -0,0 +1,60 @@
# Copyright (c) Microsoft. All rights reserved.
import os
import pytest
from semantic_kernel.contents.image_content import ImageContent
test_cases = [
pytest.param(ImageContent(uri="http://test_uri"), id="uri"),
pytest.param(ImageContent(data=b"test_data", mime_type="image/jpeg", data_format="base64"), id="data"),
pytest.param(ImageContent(uri="http://test_uri", data=b"test_data", mime_type="image/jpeg"), id="both"),
pytest.param(
ImageContent.from_image_path(
image_path=os.path.join(os.path.dirname(__file__), "../../", "assets/sample_image.jpg")
),
id="image_file",
),
]
def test_create_uri():
image = ImageContent(uri="http://test_uri")
assert str(image.uri) == "http://test_uri/"
def test_create_file_from_path():
image_path = os.path.join(os.path.dirname(__file__), "../../", "assets/sample_image.jpg")
image = ImageContent.from_image_path(image_path=image_path)
assert image.mime_type == "image/jpeg"
assert image.data_uri.startswith("data:image/jpeg;")
assert image.data is not None
def test_create_data():
image = ImageContent(data=b"test_data", mime_type="image/jpeg")
assert image.mime_type == "image/jpeg"
assert image.data == b"test_data"
def test_to_str_uri():
image = ImageContent(uri="http://test_uri")
assert str(image) == "http://test_uri/"
def test_to_str_data():
image = ImageContent(data=b"test_data", mime_type="image/jpeg", data_format="base64")
assert str(image) == "data:image/jpeg;base64,dGVzdF9kYXRh"
@pytest.mark.parametrize("image", test_cases)
def test_element_roundtrip(image):
element = image.to_element()
new_image = ImageContent.from_element(element)
assert new_image == image
@pytest.mark.parametrize("image", test_cases)
def test_to_dict(image):
assert image.to_dict() == {"type": "image_url", "image_url": {"url": str(image)}}
@@ -0,0 +1,146 @@
# Copyright (c) Microsoft. All rights reserved.
from xml.etree.ElementTree import Element
import pytest
from semantic_kernel.contents.annotation_content import CitationType
from semantic_kernel.contents.streaming_annotation_content import StreamingAnnotationContent
test_cases = [
pytest.param(StreamingAnnotationContent(file_id="12345"), id="file_id"),
pytest.param(StreamingAnnotationContent(quote="This is a quote."), id="quote"),
pytest.param(StreamingAnnotationContent(start_index=5, end_index=20), id="indices"),
pytest.param(
StreamingAnnotationContent(file_id="12345", quote="This is a quote.", start_index=5, end_index=20),
id="all_fields",
),
pytest.param(
StreamingAnnotationContent(
file_id="abc",
citation_type=CitationType.URL_CITATION.value,
url="http://example.com",
quote="q",
title="TITLE",
start_index=0,
end_index=2,
),
id="citation_type_and_url",
),
]
def test_create_empty():
annotation = StreamingAnnotationContent()
assert annotation.file_id is None
assert annotation.quote is None
assert annotation.start_index is None
assert annotation.end_index is None
def test_create_file_id():
annotation = StreamingAnnotationContent(file_id="12345")
assert annotation.file_id == "12345"
def test_create_quote():
annotation = StreamingAnnotationContent(quote="This is a quote.")
assert annotation.quote == "This is a quote."
def test_create_indices():
annotation = StreamingAnnotationContent(start_index=5, end_index=20)
assert annotation.start_index == 5
assert annotation.end_index == 20
def test_create_all_fields():
annotation = StreamingAnnotationContent(file_id="12345", quote="This is a quote.", start_index=5, end_index=20)
assert annotation.file_id == "12345"
assert annotation.quote == "This is a quote."
assert annotation.start_index == 5
assert annotation.end_index == 20
def test_update_file_id():
annotation = StreamingAnnotationContent()
annotation.file_id = "12345"
assert annotation.file_id == "12345"
def test_update_quote():
annotation = StreamingAnnotationContent()
annotation.quote = "This is a quote."
assert annotation.quote == "This is a quote."
def test_update_indices():
annotation = StreamingAnnotationContent()
annotation.start_index = 5
annotation.end_index = 20
assert annotation.start_index == 5
assert annotation.end_index == 20
def test_to_str():
annotation = StreamingAnnotationContent(file_id="12345", quote="This is a quote.", start_index=5, end_index=20)
assert (
str(annotation)
== "StreamingAnnotationContent(type=None, file_id=12345, url=None, quote=This is a quote., title=None, start_index=5, end_index=20)" # noqa: E501
)
def test_to_element():
annotation = StreamingAnnotationContent(file_id="12345", quote="This is a quote.", start_index=5, end_index=20)
element = annotation.to_element()
assert element.tag == "streaming_annotation"
assert element.get("file_id") == "12345"
assert element.get("quote") == "This is a quote."
assert element.get("start_index") == "5"
assert element.get("end_index") == "20"
def test_from_element():
element = Element("StreamingAnnotationContent")
element.set("file_id", "12345")
element.set("quote", "This is a quote.")
element.set("start_index", "5")
element.set("end_index", "20")
annotation = StreamingAnnotationContent.from_element(element)
assert annotation.file_id == "12345"
assert annotation.quote == "This is a quote."
assert annotation.start_index == 5
assert annotation.end_index == 20
def test_to_dict():
annotation = StreamingAnnotationContent(file_id="12345", quote="This is a quote.", start_index=5, end_index=20)
expected_text = (
f"type={annotation.citation_type}, {annotation.file_id or annotation.url}, quote={annotation.quote}, title={annotation.title} " # noqa: E501
f"(Start Index={annotation.start_index}->End Index={annotation.end_index})"
)
assert annotation.to_dict() == {
"type": "text",
"text": expected_text,
}
@pytest.mark.parametrize("annotation", test_cases)
def test_element_roundtrip(annotation):
element = annotation.to_element()
new_annotation = StreamingAnnotationContent.from_element(element)
assert new_annotation == annotation
@pytest.mark.parametrize("annotation", test_cases)
def test_to_dict_call(annotation):
ctype = annotation.citation_type.value if annotation.citation_type else None
expected_text = (
f"type={ctype}, {annotation.file_id or annotation.url}, quote={annotation.quote}, title={annotation.title} " # noqa: E501
f"(Start Index={annotation.start_index}->End Index={annotation.end_index})"
)
expected_dict = {
"type": "text",
"text": expected_text,
}
assert annotation.to_dict() == expected_dict
@@ -0,0 +1,452 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from defusedxml.ElementTree import XML
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.file_reference_content import FileReferenceContent
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.streaming_chat_message_content import StreamingChatMessageContent
from semantic_kernel.contents.streaming_text_content import StreamingTextContent
from semantic_kernel.contents.text_content import TextContent
from semantic_kernel.contents.utils.author_role import AuthorRole
from semantic_kernel.contents.utils.finish_reason import FinishReason
from semantic_kernel.exceptions.content_exceptions import ContentAdditionException
def test_scmc():
message = StreamingChatMessageContent(choice_index=0, role=AuthorRole.USER, content="Hello, world!")
assert message.role == AuthorRole.USER
assert message.content == "Hello, world!"
assert len(message.items) == 1
def test_scmc_str():
message = StreamingChatMessageContent(choice_index=0, role="user", content="Hello, world!")
assert str(message) == "Hello, world!"
def test_scmc_full():
message = StreamingChatMessageContent(
choice_index=0,
role=AuthorRole.USER,
name="username",
content="Hello, world!",
inner_content="Hello, world!",
encoding="utf-8",
ai_model_id="1234",
metadata={"test": "test"},
finish_reason="stop",
)
assert message.role == AuthorRole.USER
assert message.name == "username"
assert message.content == "Hello, world!"
assert message.finish_reason == FinishReason.STOP
assert len(message.items) == 1
def test_scmc_items():
message = StreamingChatMessageContent(
choice_index=0, role=AuthorRole.USER, items=[TextContent(text="Hello, world!")]
)
assert message.role == AuthorRole.USER
assert message.content == "Hello, world!"
assert len(message.items) == 1
def test_scmc_items_and_content():
message = StreamingChatMessageContent(
choice_index=0, role=AuthorRole.USER, content="text", items=[TextContent(text="Hello, world!")]
)
assert message.role == AuthorRole.USER
assert message.content == "Hello, world!"
assert message.items[0].text == "Hello, world!"
assert message.items[1].text == "text"
assert len(message.items) == 2
def test_scmc_multiple_items():
message = StreamingChatMessageContent(
choice_index=0,
role=AuthorRole.SYSTEM,
items=[
TextContent(text="Hello, world!"),
TextContent(text="Hello, world!"),
],
)
assert message.role == AuthorRole.SYSTEM
assert message.content == "Hello, world!"
assert len(message.items) == 2
def test_scmc_content_set():
message = StreamingChatMessageContent(choice_index=0, role=AuthorRole.USER, content="Hello, world!")
assert message.role == AuthorRole.USER
assert message.content == "Hello, world!"
message.content = "Hello, world to you too!"
assert len(message.items) == 1
assert message.items[0].text == "Hello, world to you too!"
message.content = ""
assert message.items[0].text == "Hello, world to you too!"
def test_scmc_content_set_empty():
message = StreamingChatMessageContent(choice_index=0, role=AuthorRole.USER, content="Hello, world!")
assert message.role == AuthorRole.USER
assert message.content == "Hello, world!"
message.items.pop()
message.content = "Hello, world to you too!"
assert len(message.items) == 1
assert message.items[0].text == "Hello, world to you too!"
def test_scmc_to_element():
message = StreamingChatMessageContent(choice_index=0, role=AuthorRole.USER, content="Hello, world!", name=None)
element = message.to_element()
assert element.tag == "message"
assert element.attrib == {"role": "user", "choice_index": "0"}
assert element.get("name") is None
for child in element:
assert child.tag == "text"
assert child.text == "Hello, world!"
def test_scmc_to_prompt():
message = StreamingChatMessageContent(choice_index=0, role=AuthorRole.USER, content="Hello, world!")
prompt = message.to_prompt()
assert "<text>Hello, world!</text>" in prompt
assert 'choice_index="0"' in prompt
assert 'role="user"' in prompt
def test_scmc_from_element():
element = StreamingChatMessageContent(choice_index=0, role=AuthorRole.USER, content="Hello, world!").to_element()
message = StreamingChatMessageContent.from_element(element)
assert message.role == AuthorRole.USER
assert message.content == "Hello, world!"
assert len(message.items) == 1
def test_scmc_from_element_content():
xml_content = '<message role="user" choice_index="0">Hello, world!</message>'
element = XML(text=xml_content)
message = StreamingChatMessageContent.from_element(element)
assert message.role == AuthorRole.USER
assert message.content == "Hello, world!"
assert len(message.items) == 1
def test_scmc_from_element_content_missing_choice_index():
xml_content = '<message role="user">Hello, world!</message>'
element = XML(text=xml_content)
with pytest.raises(TypeError):
StreamingChatMessageContent.from_element(element)
@pytest.mark.parametrize(
"xml_content, user, text_content, length",
[
('<message role="user" choice_index="0">Hello, world!</message>', "user", "Hello, world!", 1),
('<message role="user" choice_index="0"><text>Hello, world!</text></message>', "user", "Hello, world!", 1),
(
'<message role="user" choice_index="0"><text>Hello, world!</text><text>Hello, world!</text></message>',
"user",
"Hello, world!Hello, world!",
1,
),
(
'<message role="assistant" choice_index="0"><function_call id="test" name="func_name">args</function_call></message>', # noqa: E501
"assistant",
"",
1,
),
(
'<message role="tool" choice_index="0"><function_result id="test" name="func_name">function result</function_result></message>', # noqa: E501
"tool",
"",
1,
),
(
'<message role="user" choice_index="0"><text>Hello, world!</text><function_call id="test" name="func_name">args</function_call></message>', # noqa: E501
"user",
"Hello, world!",
2,
),
(
'<message role="user" choice_index="0"><random>some random code sample</random>in between text<text>test</text></message>', # noqa: E501
"user",
"<random>some random code sample</random>in between texttest",
1, # TODO: review this case
),
],
ids=["no_tag", "text_tag", "double_text_tag", "function_call", "function_result", "combined", "unknown_tag"],
)
def test_scmc_from_element_content_parse(xml_content, user, text_content, length):
element = XML(text=xml_content)
message = StreamingChatMessageContent.from_element(element)
assert message.role.value == user
assert str(message) == text_content
assert len(message.items) == length
def test_scmc_serialize():
message = StreamingChatMessageContent(choice_index=0, role=AuthorRole.USER, content="Hello, world!")
dumped = message.model_dump()
assert dumped["role"] == AuthorRole.USER
assert dumped["items"][0]["text"] == "Hello, world!"
def test_scmc_to_dict():
message = StreamingChatMessageContent(choice_index=0, role=AuthorRole.USER, content="Hello, world!")
assert message.to_dict() == {
"role": "user",
"content": "Hello, world!",
}
def test_scmc_to_dict_keys():
message = StreamingChatMessageContent(choice_index=0, role=AuthorRole.USER, content="Hello, world!")
assert message.to_dict(role_key="author", content_key="text") == {
"author": "user",
"text": "Hello, world!",
}
@pytest.mark.parametrize(
"input_args, expected_dict",
[
({"role": "user", "content": "Hello, world!"}, {"role": "user", "content": "Hello, world!"}),
(
{"role": "user", "content": "Hello, world!", "name": "username"},
{"role": "user", "content": "Hello, world!", "name": "username"},
),
({"role": "user", "items": [TextContent(text="Hello, world!")]}, {"role": "user", "content": "Hello, world!"}),
(
{"role": "assistant", "items": [FunctionCallContent(id="test", name="func_name", arguments="args")]},
{
"role": "assistant",
"tool_calls": [
{"id": "test", "type": "function", "function": {"name": "func_name", "arguments": "args"}}
],
},
),
(
{"role": "tool", "items": [FunctionResultContent(id="test", name="func_name", result="result")]},
{"role": "tool", "tool_call_id": "test", "content": "result"},
),
(
{
"role": "user",
"items": [
TextContent(text="Hello, "),
TextContent(text="world!"),
],
},
{
"role": "user",
"content": [{"type": "text", "text": "Hello, "}, {"type": "text", "text": "world!"}],
},
),
],
ids=["user_content", "user_with_name", "user_item", "function_call", "function_result", "multiple_items"],
)
def test_scmc_to_dict_items(input_args, expected_dict):
message = StreamingChatMessageContent(choice_index=0, **input_args)
assert message.to_dict() == expected_dict
def test_scmc_add():
message1 = StreamingChatMessageContent(
choice_index=0, role=AuthorRole.USER, content="Hello, ", inner_content="source1"
)
message2 = StreamingChatMessageContent(
choice_index=0, role=AuthorRole.USER, content="world!", inner_content="source2"
)
combined = message1 + message2
assert combined.role == AuthorRole.USER
assert combined.content == "Hello, world!"
assert len(combined.items) == 1
assert len(combined.inner_content) == 2
# Make sure the original inner content is preserved
assert message1.inner_content == "source1"
assert message2.inner_content == "source2"
def test_scmc_add_three():
message1 = StreamingChatMessageContent(
choice_index=0, role=AuthorRole.USER, content="Hello, ", inner_content="source1"
)
message2 = StreamingChatMessageContent(
choice_index=0, role=AuthorRole.USER, content="world", inner_content="source2"
)
message3 = StreamingChatMessageContent(choice_index=0, role=AuthorRole.USER, content="!", inner_content="source3")
combined = message1 + message2 + message3
assert combined.role == AuthorRole.USER
assert combined.content == "Hello, world!"
assert len(combined.items) == 1
assert len(combined.inner_content) == 3
@pytest.mark.parametrize(
"message1, message2",
[
(
StreamingChatMessageContent(
choice_index=0,
role=AuthorRole.USER,
items=[StreamingTextContent(choice_index=0, text="Hello, ")],
inner_content="source1",
),
StreamingChatMessageContent(
choice_index=0,
role=AuthorRole.USER,
items=[FunctionResultContent(id="test", name="test", result="test")],
inner_content="source2",
),
),
(
StreamingChatMessageContent(
choice_index=0,
role=AuthorRole.TOOL,
items=[FunctionCallContent(id="test1", name="test")],
inner_content="source1",
),
StreamingChatMessageContent(
choice_index=0,
role=AuthorRole.TOOL,
items=[FunctionCallContent(id="test2", name="test")],
inner_content="source2",
),
),
(
StreamingChatMessageContent(
choice_index=0, role=AuthorRole.USER, items=[StreamingTextContent(text="Hello, ", choice_index=0)]
),
StreamingChatMessageContent(
choice_index=0, role=AuthorRole.USER, items=[StreamingTextContent(text="world!", choice_index=1)]
),
),
(
StreamingChatMessageContent(
choice_index=0,
role=AuthorRole.USER,
items=[StreamingTextContent(text="Hello, ", choice_index=0, ai_model_id="0")],
),
StreamingChatMessageContent(
choice_index=0,
role=AuthorRole.USER,
items=[StreamingTextContent(text="world!", choice_index=0, ai_model_id="1")],
),
),
(
StreamingChatMessageContent(
choice_index=0,
role=AuthorRole.USER,
items=[StreamingTextContent(text="Hello, ", encoding="utf-8", choice_index=0)],
),
StreamingChatMessageContent(
choice_index=0,
role=AuthorRole.USER,
items=[StreamingTextContent(text="world!", encoding="utf-16", choice_index=0)],
),
),
],
ids=[
"different_types",
"different_fccs",
"different_text_content_choice_index",
"different_text_content_models",
"different_text_content_encoding",
],
)
def test_scmc_add_different_items_same_type(message1, message2):
combined = message1 + message2
assert len(combined.items) == 2
# Make sure the original items are preserved
assert len(message1.items) == 1
assert len(message2.items) == 1
@pytest.mark.parametrize(
"message1, message2",
[
(
StreamingChatMessageContent(choice_index=0, role=AuthorRole.USER, content="Hello, "),
StreamingChatMessageContent(choice_index=0, role=AuthorRole.ASSISTANT, content="world!"),
),
(
StreamingChatMessageContent(choice_index=0, role=AuthorRole.USER, content="Hello, "),
StreamingChatMessageContent(choice_index=1, role=AuthorRole.USER, content="world!"),
),
(
StreamingChatMessageContent(choice_index=0, role=AuthorRole.USER, content="Hello, ", ai_model_id="1234"),
StreamingChatMessageContent(choice_index=0, role=AuthorRole.USER, content="world!", ai_model_id="5678"),
),
(
StreamingChatMessageContent(choice_index=0, role=AuthorRole.USER, content="Hello, ", encoding="utf-8"),
StreamingChatMessageContent(choice_index=0, role=AuthorRole.USER, content="world!", encoding="utf-16"),
),
(
StreamingChatMessageContent(choice_index=0, role=AuthorRole.USER, content="Hello, "),
ChatMessageContent(role=AuthorRole.USER, content="world!"),
),
],
ids=[
"different_roles",
"different_index",
"different_model",
"different_encoding",
"different_type",
],
)
def test_smsc_add_exception(message1, message2):
with pytest.raises(ContentAdditionException):
message1 + message2
def test_scmc_bytes():
message = StreamingChatMessageContent(choice_index=0, role=AuthorRole.USER, content="Hello, world!")
assert bytes(message) == b"Hello, world!"
assert bytes(message.items[0]) == b"Hello, world!"
def test_scmc_with_unhashable_types_can_hash():
user_messages = [
StreamingChatMessageContent(
role=AuthorRole.USER,
items=[
StreamingTextContent(text="Describe this image.", choice_index=0),
ImageContent(
uri="https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/New_york_times_square-terabass.jpg/1200px-New_york_times_square-terabass.jpg"
),
],
choice_index=0,
),
StreamingChatMessageContent(
role=AuthorRole.USER,
items=[
StreamingTextContent(text="What is the main color in this image?", choice_index=0),
ImageContent(uri="https://upload.wikimedia.org/wikipedia/commons/5/56/White_shark.jpg"),
],
choice_index=0,
),
StreamingChatMessageContent(
role=AuthorRole.USER,
items=[
StreamingTextContent(text="Is there an animal in this image?", choice_index=0),
FileReferenceContent(file_id="test_file_id"),
],
choice_index=0,
),
StreamingChatMessageContent(
role=AuthorRole.USER,
choice_index=0,
),
]
for message in user_messages:
assert hash(message) is not None
@@ -0,0 +1,76 @@
# Copyright (c) Microsoft. All rights reserved.
from xml.etree.ElementTree import Element
import pytest
from semantic_kernel.contents.streaming_file_reference_content import StreamingFileReferenceContent
def test_create_empty():
file_reference = StreamingFileReferenceContent()
assert file_reference.file_id is None
def test_create_file_id():
file_reference = StreamingFileReferenceContent(file_id="12345")
assert file_reference.file_id == "12345"
def test_update_file_id():
file_reference = StreamingFileReferenceContent()
file_reference.file_id = "12345"
assert file_reference.file_id == "12345"
def test_to_str():
file_reference = StreamingFileReferenceContent(file_id="12345")
assert str(file_reference) == "StreamingFileReferenceContent(file_id=12345)"
def test_to_element():
file_reference = StreamingFileReferenceContent(file_id="12345")
element = file_reference.to_element()
assert element.tag == "streaming_file_reference"
assert element.get("file_id") == "12345"
def test_from_element():
element = Element("StreamingFileReferenceContent")
element.set("file_id", "12345")
file_reference = StreamingFileReferenceContent.from_element(element)
assert file_reference.file_id == "12345"
def test_to_dict_simple():
file_reference = StreamingFileReferenceContent(file_id="12345")
assert file_reference.to_dict() == {
"file_id": "12345",
}
@pytest.mark.parametrize(
"file_reference",
[
pytest.param(StreamingFileReferenceContent(file_id="12345"), id="file_id"),
pytest.param(StreamingFileReferenceContent(), id="empty"),
],
)
def test_element_roundtrip(file_reference):
element = file_reference.to_element()
new_file_reference = StreamingFileReferenceContent.from_element(element)
assert new_file_reference == file_reference
@pytest.mark.parametrize(
"file_reference",
[
pytest.param(StreamingFileReferenceContent(file_id="12345"), id="file_id"),
pytest.param(StreamingFileReferenceContent(), id="empty"),
],
)
def test_to_dict(file_reference):
expected_dict = {
"file_id": file_reference.file_id,
}
assert file_reference.to_dict() == expected_dict