chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user