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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:21:23 +08:00
commit b957a53def
5423 changed files with 863745 additions and 0 deletions
@@ -0,0 +1,400 @@
# Copyright (c) Microsoft. All rights reserved.
from collections.abc import AsyncGenerator
from unittest.mock import AsyncMock, MagicMock
import pytest
from anthropic import AsyncAnthropic
from anthropic.lib.streaming import TextEvent
from anthropic.lib.streaming._types import InputJsonEvent
from anthropic.types import (
ContentBlockStopEvent,
InputJSONDelta,
Message,
MessageDeltaUsage,
MessageStopEvent,
RawContentBlockDeltaEvent,
RawContentBlockStartEvent,
RawMessageDeltaEvent,
RawMessageStartEvent,
TextBlock,
TextDelta,
ToolUseBlock,
Usage,
)
from anthropic.types.raw_message_delta_event import Delta
from semantic_kernel.connectors.ai.anthropic.prompt_execution_settings.anthropic_prompt_execution_settings import (
AnthropicChatPromptExecutionSettings,
)
from semantic_kernel.contents.chat_message_content import (
ChatMessageContent,
FunctionCallContent,
FunctionResultContent,
TextContent,
)
from semantic_kernel.contents.const import ContentTypes
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent, StreamingTextContent
from semantic_kernel.contents.utils.author_role import AuthorRole
from semantic_kernel.contents.utils.finish_reason import FinishReason
@pytest.fixture
def mock_tool_calls_message() -> ChatMessageContent:
return ChatMessageContent(
ai_model_id="claude-3-opus-20240229",
metadata={},
content_type="message",
role=AuthorRole.ASSISTANT,
name=None,
items=[
TextContent(
inner_content=None,
ai_model_id=None,
metadata={},
content_type="text",
text="<thinking></thinking>",
encoding=None,
),
FunctionCallContent(
inner_content=None,
ai_model_id=None,
metadata={},
content_type=ContentTypes.FUNCTION_CALL_CONTENT,
id="test_function_call_content",
index=1,
name="math-Add",
function_name="Add",
plugin_name="math",
arguments={"input": 3, "amount": 3},
),
],
encoding=None,
finish_reason=FinishReason.TOOL_CALLS,
)
@pytest.fixture
def mock_parallel_tool_calls_message() -> ChatMessageContent:
return ChatMessageContent(
ai_model_id="claude-3-opus-20240229",
metadata={},
content_type="message",
role=AuthorRole.ASSISTANT,
name=None,
items=[
TextContent(
inner_content=None,
ai_model_id=None,
metadata={},
content_type="text",
text="<thinking></thinking>",
encoding=None,
),
FunctionCallContent(
inner_content=None,
ai_model_id=None,
metadata={},
content_type=ContentTypes.FUNCTION_CALL_CONTENT,
id="test_function_call_content_1",
index=1,
name="math-Add",
function_name="Add",
plugin_name="math",
arguments={"input": 3, "amount": 3},
),
FunctionCallContent(
inner_content=None,
ai_model_id=None,
metadata={},
content_type=ContentTypes.FUNCTION_CALL_CONTENT,
id="test_function_call_content_2",
index=1,
name="math-Subtract",
function_name="Subtract",
plugin_name="math",
arguments={"input": 6, "amount": 3},
),
],
encoding=None,
finish_reason=FinishReason.TOOL_CALLS,
)
@pytest.fixture
def mock_streaming_tool_calls_message() -> list:
stream_events = [
RawMessageStartEvent(
message=Message(
id="test_message_id",
content=[],
model="claude-3-opus-20240229",
role="assistant",
stop_reason=None,
stop_sequence=None,
type="message",
usage=Usage(input_tokens=1720, output_tokens=2),
),
type="message_start",
),
RawContentBlockStartEvent(content_block=TextBlock(text="", type="text"), index=0, type="content_block_start"),
RawContentBlockDeltaEvent(
delta=TextDelta(text="<thinking>", type="text_delta"), index=0, type="content_block_delta"
),
TextEvent(type="text", text="<thinking>", snapshot="<thinking>"),
RawContentBlockDeltaEvent(
delta=TextDelta(text="</thinking>", type="text_delta"), index=0, type="content_block_delta"
),
TextEvent(type="text", text="</thinking>", snapshot="<thinking></thinking>"),
ContentBlockStopEvent(
index=0, type="content_block_stop", content_block=TextBlock(text="<thinking></thinking>", type="text")
),
RawContentBlockStartEvent(
content_block=ToolUseBlock(id="test_tool_use_message_id", input={}, name="math-Add", type="tool_use"),
index=1,
type="content_block_start",
),
RawContentBlockDeltaEvent(
delta=InputJSONDelta(partial_json='{"input": 3, "amount": 3}', type="input_json_delta"),
index=1,
type="content_block_delta",
),
InputJsonEvent(type="input_json", partial_json='{"input": 3, "amount": 3}', snapshot={"input": 3, "amount": 3}),
ContentBlockStopEvent(
index=1,
type="content_block_stop",
content_block=ToolUseBlock(
id="test_tool_use_block_id", input={"input": 3, "amount": 3}, name="math-Add", type="tool_use"
),
),
RawMessageDeltaEvent(
delta=Delta(stop_reason="tool_use", stop_sequence=None),
type="message_delta",
usage=MessageDeltaUsage(output_tokens=159),
),
MessageStopEvent(
type="message_stop",
message=Message(
id="test_message_id",
content=[
TextBlock(text="<thinking></thinking>", type="text"),
ToolUseBlock(
id="test_tool_use_block_id", input={"input": 3, "amount": 3}, name="math-Add", type="tool_use"
),
],
model="claude-3-opus-20240229",
role="assistant",
stop_reason="tool_use",
stop_sequence=None,
type="message",
usage=Usage(input_tokens=100, output_tokens=100),
),
),
]
async def async_generator():
for event in stream_events:
yield event
stream_mock = AsyncMock()
stream_mock.__aenter__.return_value = async_generator()
return stream_mock
@pytest.fixture
def mock_tool_call_result_message() -> ChatMessageContent:
return ChatMessageContent(
inner_content=None,
ai_model_id=None,
metadata={},
content_type="message",
role=AuthorRole.TOOL,
name=None,
items=[
FunctionResultContent(
id="test_function_call_content",
result=6,
)
],
encoding=None,
finish_reason=FinishReason.TOOL_CALLS,
)
@pytest.fixture
def mock_parallel_tool_call_result_message() -> ChatMessageContent:
return ChatMessageContent(
inner_content=None,
ai_model_id=None,
metadata={},
content_type="message",
role=AuthorRole.TOOL,
name=None,
items=[
FunctionResultContent(
id="test_function_call_content_1",
result=6,
),
FunctionResultContent(
id="test_function_call_content_2",
result=3,
),
],
encoding=None,
finish_reason=FinishReason.TOOL_CALLS,
)
@pytest.fixture
def mock_streaming_chat_message_content() -> StreamingChatMessageContent:
return StreamingChatMessageContent(
choice_index=0,
ai_model_id="claude-3-opus-20240229",
metadata={},
role=AuthorRole.ASSISTANT,
name=None,
items=[
StreamingTextContent(
inner_content=None,
ai_model_id=None,
metadata={},
content_type="text",
text="<thinking></thinking>",
encoding=None,
choice_index=0,
),
FunctionCallContent(
inner_content=None,
ai_model_id=None,
metadata={},
content_type=ContentTypes.FUNCTION_CALL_CONTENT,
id="tool_id",
index=0,
name="math-Add",
function_name="Add",
plugin_name="math",
arguments='{"input": 3, "amount": 3}',
),
],
encoding=None,
finish_reason=FinishReason.TOOL_CALLS,
)
@pytest.fixture
def mock_settings() -> AnthropicChatPromptExecutionSettings:
return AnthropicChatPromptExecutionSettings()
@pytest.fixture
def mock_chat_message_response() -> Message:
return Message(
id="test_message_id",
content=[TextBlock(text="Hello, how are you?", type="text")],
model="claude-3-opus-20240229",
role="assistant",
stop_reason="end_turn",
stop_sequence=None,
type="message",
usage=Usage(input_tokens=10, output_tokens=10),
)
@pytest.fixture
def mock_streaming_message_response() -> AsyncGenerator:
raw_message_start_event = RawMessageStartEvent(
message=Message(
id="test_message_id",
content=[],
model="claude-3-opus-20240229",
role="assistant",
stop_reason=None,
stop_sequence=None,
type="message",
usage=Usage(input_tokens=41, output_tokens=3),
),
type="message_start",
)
raw_content_block_start_event = RawContentBlockStartEvent(
content_block=TextBlock(text="", type="text"),
index=0,
type="content_block_start",
)
raw_content_block_delta_event = RawContentBlockDeltaEvent(
delta=TextDelta(text="Hello! It", type="text_delta"),
index=0,
type="content_block_delta",
)
text_event = TextEvent(
type="text",
text="Hello! It",
snapshot="Hello! It",
)
content_block_stop_event = ContentBlockStopEvent(
index=0,
type="content_block_stop",
content_block=TextBlock(text="Hello! It's nice to meet you.", type="text"),
)
raw_message_delta_event = RawMessageDeltaEvent(
delta=Delta(stop_reason="end_turn", stop_sequence=None),
type="message_delta",
usage=MessageDeltaUsage(output_tokens=84),
)
message_stop_event = MessageStopEvent(
type="message_stop",
message=Message(
id="test_message_stop_id",
content=[TextBlock(text="Hello! It's nice to meet you.", type="text")],
model="claude-3-opus-20240229",
role="assistant",
stop_reason="end_turn",
stop_sequence=None,
type="message",
usage=Usage(input_tokens=41, output_tokens=84),
),
)
# Combine all mock events into a list
stream_events = [
raw_message_start_event,
raw_content_block_start_event,
raw_content_block_delta_event,
text_event,
content_block_stop_event,
raw_message_delta_event,
message_stop_event,
]
async def async_generator():
for event in stream_events:
yield event
# Create an AsyncMock for the stream
stream_mock = AsyncMock()
stream_mock.__aenter__.return_value = async_generator()
return stream_mock
@pytest.fixture
def mock_anthropic_client_completion(mock_chat_message_response: Message) -> AsyncAnthropic:
client = MagicMock(spec=AsyncAnthropic)
messages_mock = MagicMock()
messages_mock.create = AsyncMock(return_value=mock_chat_message_response)
client.messages = messages_mock
return client
@pytest.fixture
def mock_anthropic_client_completion_stream(mock_streaming_message_response: AsyncGenerator) -> AsyncAnthropic:
client = MagicMock(spec=AsyncAnthropic)
messages_mock = MagicMock()
messages_mock.stream.return_value = mock_streaming_message_response
client.messages = messages_mock
return client
@@ -0,0 +1,549 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from anthropic import AsyncAnthropic
from anthropic.types import Message
from semantic_kernel.connectors.ai.anthropic.prompt_execution_settings.anthropic_prompt_execution_settings import (
AnthropicChatPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.anthropic.services.anthropic_chat_completion import AnthropicChatCompletion
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.open_ai_prompt_execution_settings import (
OpenAIChatPromptExecutionSettings,
)
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.contents.chat_message_content import ChatMessageContent, FunctionCallContent, TextContent
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
from semantic_kernel.contents.utils.author_role import AuthorRole
from semantic_kernel.exceptions import (
ServiceInitializationError,
ServiceInvalidExecutionSettingsError,
ServiceResponseException,
)
from semantic_kernel.exceptions.service_exceptions import ServiceInvalidRequestError
from semantic_kernel.functions.kernel_arguments import KernelArguments
from semantic_kernel.functions.kernel_function_decorator import kernel_function
from semantic_kernel.kernel import Kernel
async def test_complete_chat_contents(
kernel: Kernel,
mock_settings: AnthropicChatPromptExecutionSettings,
mock_chat_message_response: Message,
):
client = MagicMock(spec=AsyncAnthropic)
messages_mock = MagicMock()
messages_mock.create = AsyncMock(return_value=mock_chat_message_response)
client.messages = messages_mock
chat_history = ChatHistory()
chat_history.add_user_message("test_user_message")
arguments = KernelArguments()
chat_completion_base = AnthropicChatCompletion(
ai_model_id="test_model_id", service_id="test", api_key="", async_client=client
)
content: list[ChatMessageContent] = await chat_completion_base.get_chat_message_contents(
chat_history=chat_history, settings=mock_settings, kernel=kernel, arguments=arguments
)
assert len(content) > 0
assert content[0].content != ""
assert content[0].role == AuthorRole.ASSISTANT
mock_message_text_content = ChatMessageContent(role=AuthorRole.ASSISTANT, items=[TextContent(text="test")])
mock_message_function_call = ChatMessageContent(
role=AuthorRole.ASSISTANT,
items=[
FunctionCallContent(
name="test",
arguments={"key": "test"},
)
],
)
@pytest.mark.parametrize(
"function_choice_behavior,model_responses,expected_result",
[
pytest.param(
FunctionChoiceBehavior.Auto(),
[[mock_message_function_call], [mock_message_text_content]],
TextContent,
id="auto",
),
pytest.param(
FunctionChoiceBehavior.Auto(auto_invoke=False),
[[mock_message_function_call]],
FunctionCallContent,
id="auto_none_invoke",
),
pytest.param(
FunctionChoiceBehavior.Required(auto_invoke=False),
[[mock_message_function_call]],
FunctionCallContent,
id="required_none_invoke",
),
],
)
async def test_complete_chat_contents_function_call_behavior_tool_call(
kernel: Kernel,
mock_settings: AnthropicChatPromptExecutionSettings,
function_choice_behavior: FunctionChoiceBehavior,
model_responses,
expected_result,
):
kernel.add_function("test", kernel_function(lambda key: "test", name="test"))
mock_settings.function_choice_behavior = function_choice_behavior
arguments = KernelArguments()
chat_completion_base = AnthropicChatCompletion(ai_model_id="test_model_id", service_id="test", api_key="")
with (
patch.object(chat_completion_base, "_inner_get_chat_message_contents", side_effect=model_responses),
):
response: list[ChatMessageContent] = await chat_completion_base.get_chat_message_contents(
chat_history=ChatHistory(system_message="Test"), settings=mock_settings, kernel=kernel, arguments=arguments
)
assert all(isinstance(content, expected_result) for content in response[0].items)
async def test_complete_chat_contents_function_call_behavior_without_kernel(
mock_settings: AnthropicChatPromptExecutionSettings,
mock_anthropic_client_completion: AsyncAnthropic,
):
chat_history = MagicMock()
chat_completion_base = AnthropicChatCompletion(
ai_model_id="test_model_id", service_id="test", api_key="", async_client=mock_anthropic_client_completion
)
mock_settings.function_choice_behavior = FunctionChoiceBehavior.Auto()
with pytest.raises(ServiceInvalidExecutionSettingsError):
await chat_completion_base.get_chat_message_contents(chat_history=chat_history, settings=mock_settings)
async def test_complete_chat_stream_contents(
kernel: Kernel,
mock_settings: AnthropicChatPromptExecutionSettings,
mock_streaming_message_response,
):
client = MagicMock(spec=AsyncAnthropic)
messages_mock = MagicMock()
messages_mock.stream.return_value = mock_streaming_message_response
client.messages = messages_mock
chat_history = ChatHistory()
chat_history.add_user_message("test_user_message")
arguments = KernelArguments()
chat_completion_base = AnthropicChatCompletion(
ai_model_id="test_model_id",
service_id="test",
api_key="",
async_client=client,
)
async for content in chat_completion_base.get_streaming_chat_message_contents(
chat_history, mock_settings, kernel=kernel, arguments=arguments
):
assert content is not None
mock_message_function_call = StreamingChatMessageContent(
role=AuthorRole.ASSISTANT, items=[FunctionCallContent(name="test")], choice_index="0"
)
mock_message_text_content = StreamingChatMessageContent(
role=AuthorRole.ASSISTANT, items=[TextContent(text="test")], choice_index="0"
)
@pytest.mark.parametrize(
"function_choice_behavior,model_responses,expected_result",
[
pytest.param(
FunctionChoiceBehavior.Auto(),
[[mock_message_function_call], [mock_message_text_content]],
TextContent,
id="auto",
),
pytest.param(
FunctionChoiceBehavior.Auto(auto_invoke=False),
[[mock_message_function_call]],
FunctionCallContent,
id="auto_none_invoke",
),
pytest.param(
FunctionChoiceBehavior.Required(auto_invoke=False),
[[mock_message_function_call]],
FunctionCallContent,
id="required_none_invoke",
),
pytest.param(FunctionChoiceBehavior.NoneInvoke(), [[mock_message_text_content]], TextContent, id="none"),
],
)
async def test_complete_chat_contents_streaming_function_call_behavior_tool_call(
kernel: Kernel,
mock_settings: AnthropicChatPromptExecutionSettings,
function_choice_behavior: FunctionChoiceBehavior,
model_responses,
expected_result,
):
mock_settings.function_choice_behavior = function_choice_behavior
# Mock sequence of model responses
generator_mocks = []
for mock_message in model_responses:
generator_mock = MagicMock()
generator_mock.__aiter__.return_value = [mock_message]
generator_mocks.append(generator_mock)
arguments = KernelArguments()
chat_completion_base = AnthropicChatCompletion(ai_model_id="test_model_id", service_id="test", api_key="")
with patch.object(chat_completion_base, "_inner_get_streaming_chat_message_contents", side_effect=generator_mocks):
messages = []
async for chunk in chat_completion_base.get_streaming_chat_message_contents(
chat_history=ChatHistory(system_message="Test"), settings=mock_settings, kernel=kernel, arguments=arguments
):
messages.append(chunk)
response = messages[-1]
assert all(isinstance(content, expected_result) for content in response[0].items)
async def test_anthropic_sdk_exception(kernel: Kernel, mock_settings: AnthropicChatPromptExecutionSettings):
client = MagicMock(spec=AsyncAnthropic)
messages_mock = MagicMock()
messages_mock.create.side_effect = Exception("Test Exception")
client.messages = messages_mock
chat_history = MagicMock()
arguments = KernelArguments()
chat_completion_base = AnthropicChatCompletion(
ai_model_id="test_model_id", service_id="test", api_key="", async_client=client
)
with pytest.raises(ServiceResponseException):
await chat_completion_base.get_chat_message_contents(
chat_history=chat_history, settings=mock_settings, kernel=kernel, arguments=arguments
)
async def test_anthropic_sdk_exception_streaming(kernel: Kernel, mock_settings: AnthropicChatPromptExecutionSettings):
client = MagicMock(spec=AsyncAnthropic)
messages_mock = MagicMock()
messages_mock.stream.side_effect = Exception("Test Exception")
client.messages = messages_mock
chat_history = MagicMock()
arguments = KernelArguments()
chat_completion_base = AnthropicChatCompletion(
ai_model_id="test_model_id", service_id="test", api_key="", async_client=client
)
with pytest.raises(ServiceResponseException):
async for content in chat_completion_base.get_streaming_chat_message_contents(
chat_history, mock_settings, kernel=kernel, arguments=arguments
):
assert content is not None
def test_anthropic_chat_completion_init(anthropic_unit_test_env) -> None:
# Test successful initialization
anthropic_chat_completion = AnthropicChatCompletion()
assert anthropic_chat_completion.ai_model_id == anthropic_unit_test_env["ANTHROPIC_CHAT_MODEL_ID"]
assert isinstance(anthropic_chat_completion, ChatCompletionClientBase)
@pytest.mark.parametrize("exclude_list", [["ANTHROPIC_API_KEY"]], indirect=True)
def test_anthropic_chat_completion_init_with_empty_api_key(anthropic_unit_test_env) -> None:
ai_model_id = "test_model_id"
with pytest.raises(ServiceInitializationError):
AnthropicChatCompletion(
ai_model_id=ai_model_id,
env_file_path="test.env",
)
@pytest.mark.parametrize("exclude_list", [["ANTHROPIC_CHAT_MODEL_ID"]], indirect=True)
def test_anthropic_chat_completion_init_with_empty_model_id(anthropic_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError):
AnthropicChatCompletion(
env_file_path="test.env",
)
def test_prompt_execution_settings_class(anthropic_unit_test_env):
anthropic_chat_completion = AnthropicChatCompletion()
prompt_execution_settings = anthropic_chat_completion.get_prompt_execution_settings_class()
assert prompt_execution_settings == AnthropicChatPromptExecutionSettings
async def test_with_different_execution_settings(kernel: Kernel, mock_anthropic_client_completion: MagicMock):
chat_history = MagicMock()
settings = OpenAIChatPromptExecutionSettings(temperature=0.2)
arguments = KernelArguments()
chat_completion_base = AnthropicChatCompletion(
ai_model_id="test_model_id", service_id="test", api_key="", async_client=mock_anthropic_client_completion
)
await chat_completion_base.get_chat_message_contents(
chat_history=chat_history, settings=settings, kernel=kernel, arguments=arguments
)
assert mock_anthropic_client_completion.messages.create.call_args.kwargs["temperature"] == 0.2
async def test_with_different_execution_settings_stream(
kernel: Kernel, mock_anthropic_client_completion_stream: MagicMock
):
chat_history = MagicMock()
settings = OpenAIChatPromptExecutionSettings(temperature=0.2, seed=2)
arguments = KernelArguments()
chat_completion_base = AnthropicChatCompletion(
ai_model_id="test_model_id",
service_id="test",
api_key="",
async_client=mock_anthropic_client_completion_stream,
)
async for chunk in chat_completion_base.get_streaming_chat_message_contents(
chat_history, settings, kernel=kernel, arguments=arguments
):
assert chunk is not None
assert mock_anthropic_client_completion_stream.messages.stream.call_args.kwargs["temperature"] == 0.2
async def test_prepare_chat_history_for_request_with_system_message(mock_anthropic_client_completion_stream: MagicMock):
chat_history = ChatHistory()
chat_history.add_system_message("System message")
chat_history.add_user_message("User message")
chat_history.add_assistant_message("Assistant message")
chat_history.add_system_message("Another system message")
chat_completion_base = AnthropicChatCompletion(
ai_model_id="test_model_id",
service_id="test",
api_key="",
async_client=mock_anthropic_client_completion_stream,
)
remaining_messages, system_message_content = chat_completion_base._prepare_chat_history_for_request(
chat_history, role_key="role", content_key="content"
)
assert system_message_content == "System message"
assert remaining_messages == [
{"role": AuthorRole.USER, "content": "User message"},
{"role": AuthorRole.ASSISTANT, "content": [{"type": "text", "text": "Assistant message"}]},
]
assert not any(msg["role"] == AuthorRole.SYSTEM for msg in remaining_messages)
async def test_prepare_chat_history_for_request_with_tool_message(
mock_anthropic_client_completion_stream: MagicMock,
mock_tool_calls_message: ChatMessageContent,
mock_tool_call_result_message: ChatMessageContent,
):
chat_history = ChatHistory()
chat_history.add_user_message("What is 3+3?")
chat_history.add_message(mock_tool_calls_message)
chat_history.add_message(mock_tool_call_result_message)
chat_completion_client = AnthropicChatCompletion(
ai_model_id="test_model_id",
service_id="test",
api_key="",
async_client=mock_anthropic_client_completion_stream,
)
remaining_messages, system_message_content = chat_completion_client._prepare_chat_history_for_request(
chat_history, role_key="role", content_key="content"
)
assert system_message_content is None
assert remaining_messages == [
{"role": AuthorRole.USER, "content": "What is 3+3?"},
{
"role": AuthorRole.ASSISTANT,
"content": [
{"type": "text", "text": mock_tool_calls_message.items[0].text},
{
"type": "tool_use",
"id": mock_tool_calls_message.items[1].id,
"name": mock_tool_calls_message.items[1].name,
"input": mock_tool_calls_message.items[1].arguments,
},
],
},
{
"role": AuthorRole.USER,
"content": [
{
"type": "tool_result",
"tool_use_id": mock_tool_call_result_message.items[0].id,
"content": str(mock_tool_call_result_message.items[0].result),
}
],
},
]
async def test_prepare_chat_history_for_request_with_parallel_tool_message(
mock_anthropic_client_completion_stream: MagicMock,
mock_parallel_tool_calls_message: ChatMessageContent,
mock_parallel_tool_call_result_message: ChatMessageContent,
):
chat_history = ChatHistory()
chat_history.add_user_message("What is 3+3?")
chat_history.add_message(mock_parallel_tool_calls_message)
chat_history.add_message(mock_parallel_tool_call_result_message)
chat_completion_client = AnthropicChatCompletion(
ai_model_id="test_model_id",
service_id="test",
api_key="",
async_client=mock_anthropic_client_completion_stream,
)
remaining_messages, system_message_content = chat_completion_client._prepare_chat_history_for_request(
chat_history, role_key="role", content_key="content"
)
assert system_message_content is None
assert remaining_messages == [
{"role": AuthorRole.USER, "content": "What is 3+3?"},
{
"role": AuthorRole.ASSISTANT,
"content": [
{"type": "text", "text": mock_parallel_tool_calls_message.items[0].text},
*[
{
"type": "tool_use",
"id": function_call_content.id,
"name": function_call_content.name,
"input": function_call_content.arguments,
}
for function_call_content in mock_parallel_tool_calls_message.items[1:]
],
],
},
{
"role": AuthorRole.USER,
"content": [
{
"type": "tool_result",
"tool_use_id": function_result_content.id,
"content": str(function_result_content.result),
}
for function_result_content in mock_parallel_tool_call_result_message.items
],
},
]
async def test_prepare_chat_history_for_request_with_tool_message_right_after_user_message(
mock_anthropic_client_completion_stream: MagicMock,
mock_tool_call_result_message: ChatMessageContent,
):
chat_history = ChatHistory()
chat_history.add_user_message("What is 3+3?")
chat_history.add_message(mock_tool_call_result_message)
chat_completion_client = AnthropicChatCompletion(
ai_model_id="test_model_id",
service_id="test",
api_key="",
async_client=mock_anthropic_client_completion_stream,
)
with pytest.raises(ServiceInvalidRequestError, match="Tool message found after a user or system message."):
chat_completion_client._prepare_chat_history_for_request(chat_history, role_key="role", content_key="content")
async def test_prepare_chat_history_for_request_with_tool_message_as_the_first_message(
mock_anthropic_client_completion_stream: MagicMock,
mock_tool_call_result_message: ChatMessageContent,
):
chat_history = ChatHistory()
chat_history.add_message(mock_tool_call_result_message)
chat_completion_client = AnthropicChatCompletion(
ai_model_id="test_model_id",
service_id="test",
api_key="",
async_client=mock_anthropic_client_completion_stream,
)
with pytest.raises(ServiceInvalidRequestError, match="Tool message found without a preceding message."):
chat_completion_client._prepare_chat_history_for_request(chat_history, role_key="role", content_key="content")
async def test_send_chat_stream_request_tool_calls(
mock_streaming_tool_calls_message: MagicMock,
mock_streaming_chat_message_content: StreamingChatMessageContent,
):
chat_history = ChatHistory()
chat_history.add_user_message("What is 3+3?")
chat_history.add_message(mock_streaming_chat_message_content)
settings = AnthropicChatPromptExecutionSettings(
temperature=0.2,
max_tokens=100,
top_p=1.0,
frequency_penalty=0.0,
presence_penalty=0.0,
chat_history=chat_history,
)
client = MagicMock(spec=AsyncAnthropic)
messages_mock = MagicMock()
messages_mock.stream.return_value = mock_streaming_tool_calls_message
client.messages = messages_mock
chat_completion = AnthropicChatCompletion(
ai_model_id="test_model_id",
service_id="test",
api_key="",
async_client=client,
)
response = chat_completion._send_chat_stream_request(settings)
async for message in response:
assert message is not None
def test_client_base_url(mock_anthropic_client_completion: MagicMock):
chat_completion_base = AnthropicChatCompletion(
ai_model_id="test_model_id", service_id="test", api_key="", async_client=mock_anthropic_client_completion
)
assert chat_completion_base.service_url() is not None
def test_chat_completion_reset_settings(
mock_anthropic_client_completion: MagicMock,
):
chat_completion = AnthropicChatCompletion(
ai_model_id="test_model_id", service_id="test", api_key="", async_client=mock_anthropic_client_completion
)
settings = AnthropicChatPromptExecutionSettings(tools=[{"name": "test"}], tool_choice={"type": "any"})
chat_completion._reset_function_choice_settings(settings)
assert settings.tools is None
assert settings.tool_choice is None
@@ -0,0 +1,129 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from semantic_kernel.connectors.ai.anthropic.prompt_execution_settings.anthropic_prompt_execution_settings import (
AnthropicChatPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.exceptions import ServiceInvalidExecutionSettingsError
def test_default_anthropic_chat_prompt_execution_settings():
settings = AnthropicChatPromptExecutionSettings()
assert settings.temperature is None
assert settings.top_p is None
assert settings.max_tokens == 1024
assert settings.messages is None
def test_custom_anthropic_chat_prompt_execution_settings():
settings = AnthropicChatPromptExecutionSettings(
temperature=0.5,
top_p=0.5,
max_tokens=128,
messages=[{"role": "system", "content": "Hello"}],
)
assert settings.temperature == 0.5
assert settings.top_p == 0.5
assert settings.max_tokens == 128
assert settings.messages == [{"role": "system", "content": "Hello"}]
def test_anthropic_chat_prompt_execution_settings_from_default_completion_config():
settings = PromptExecutionSettings(service_id="test_service")
chat_settings = AnthropicChatPromptExecutionSettings.from_prompt_execution_settings(settings)
assert chat_settings.service_id == "test_service"
assert chat_settings.temperature is None
assert chat_settings.top_p is None
assert chat_settings.max_tokens == 1024
def test_anthropic_chat_prompt_execution_settings_from_openai_prompt_execution_settings():
chat_settings = AnthropicChatPromptExecutionSettings(service_id="test_service", temperature=1.0)
new_settings = AnthropicChatPromptExecutionSettings(service_id="test_2", temperature=0.0)
chat_settings.update_from_prompt_execution_settings(new_settings)
assert chat_settings.service_id == "test_2"
assert chat_settings.temperature == 0.0
def test_anthropic_chat_prompt_execution_settings_from_custom_completion_config():
settings = PromptExecutionSettings(
service_id="test_service",
extension_data={
"temperature": 0.5,
"top_p": 0.5,
"max_tokens": 128,
"messages": [{"role": "system", "content": "Hello"}],
},
)
chat_settings = AnthropicChatPromptExecutionSettings.from_prompt_execution_settings(settings)
assert chat_settings.temperature == 0.5
assert chat_settings.top_p == 0.5
assert chat_settings.max_tokens == 128
def test_openai_chat_prompt_execution_settings_from_custom_completion_config_with_none():
settings = PromptExecutionSettings(
service_id="test_service",
extension_data={
"temperature": 0.5,
"top_p": 0.5,
"max_tokens": 128,
"messages": [{"role": "system", "content": "Hello"}],
},
)
chat_settings = AnthropicChatPromptExecutionSettings.from_prompt_execution_settings(settings)
assert chat_settings.temperature == 0.5
assert chat_settings.top_p == 0.5
assert chat_settings.max_tokens == 128
def test_openai_chat_prompt_execution_settings_from_custom_completion_config_with_functions():
settings = PromptExecutionSettings(
service_id="test_service",
extension_data={
"temperature": 0.5,
"top_p": 0.5,
"max_tokens": 128,
"tools": [{}],
"messages": [{"role": "system", "content": "Hello"}],
},
)
chat_settings = AnthropicChatPromptExecutionSettings.from_prompt_execution_settings(settings)
assert chat_settings.temperature == 0.5
assert chat_settings.top_p == 0.5
assert chat_settings.max_tokens == 128
def test_create_options():
settings = AnthropicChatPromptExecutionSettings(
service_id="test_service",
extension_data={
"temperature": 0.5,
"top_p": 0.5,
"max_tokens": 128,
"tools": [{}],
"messages": [{"role": "system", "content": "Hello"}],
},
)
options = settings.prepare_settings_dict()
assert options["temperature"] == 0.5
assert options["top_p"] == 0.5
assert options["max_tokens"] == 128
def test_tool_choice_none():
with pytest.raises(ServiceInvalidExecutionSettingsError, match="Tool choice 'none' is not supported by Anthropic."):
AnthropicChatPromptExecutionSettings(
service_id="test_service",
extension_data={
"temperature": 0.5,
"top_p": 0.5,
"max_tokens": 128,
"tool_choice": {"type": "none"},
"messages": [{"role": "system", "content": "Hello"}],
},
function_choice_behavior=FunctionChoiceBehavior.NoneInvoke(),
)
@@ -0,0 +1,278 @@
# Copyright (c) Microsoft. All rights reserved.
import datetime
from collections.abc import AsyncGenerator, AsyncIterator
from unittest.mock import MagicMock
import pytest
from azure.ai.inference.aio import ChatCompletionsClient, EmbeddingsClient
from azure.ai.inference.models import (
ChatChoice,
ChatCompletions,
ChatCompletionsToolCall,
ChatResponseMessage,
CompletionsUsage,
FunctionCall,
StreamingChatChoiceUpdate,
StreamingChatCompletionsUpdate,
StreamingChatResponseToolCallUpdate,
)
from azure.core.credentials import AzureKeyCredential
from semantic_kernel.connectors.ai.azure_ai_inference import (
AzureAIInferenceChatCompletion,
AzureAIInferenceTextEmbedding,
)
@pytest.fixture()
def model_id() -> str:
return "test_model_id"
@pytest.fixture()
def service_id() -> str:
return "test_service_id"
@pytest.fixture()
def azure_ai_inference_unit_test_env(monkeypatch, exclude_list, override_env_param_dict):
"""Fixture to set environment variables for Azure AI Inference Unit Tests."""
if exclude_list is None:
exclude_list = []
if override_env_param_dict is None:
override_env_param_dict = {}
env_vars = {
"AZURE_AI_INFERENCE_API_KEY": "test-api-key",
"AZURE_AI_INFERENCE_ENDPOINT": "https://test-endpoint.com",
}
env_vars.update(override_env_param_dict)
for key, value in env_vars.items():
if key not in exclude_list:
monkeypatch.setenv(key, value)
else:
monkeypatch.delenv(key, raising=False)
return env_vars
@pytest.fixture()
def model_diagnostics_test_env(monkeypatch, exclude_list, override_env_param_dict):
"""Fixture to set environment variables for Azure AI Inference Unit Tests."""
if exclude_list is None:
exclude_list = []
if override_env_param_dict is None:
override_env_param_dict = {}
env_vars = {
"SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS": "true",
"SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE": "true",
}
env_vars.update(override_env_param_dict)
for key, value in env_vars.items():
if key not in exclude_list:
monkeypatch.setenv(key, value)
else:
monkeypatch.delenv(key, raising=False)
return env_vars
@pytest.fixture()
def disabled_model_diagnostics_test_env(monkeypatch):
"""Fixture to disable diagnostics for tests that use mocking.
This is needed because AIInferenceInstrumentor's instrument/uninstrument
cycle interferes with class-level mocking of ChatCompletionsClient.complete.
"""
monkeypatch.setenv("SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS", "false")
monkeypatch.setenv("SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE", "false")
@pytest.fixture(scope="function")
def azure_ai_inference_client(azure_ai_inference_unit_test_env, request) -> ChatCompletionsClient | EmbeddingsClient:
"""Fixture to create Azure AI Inference client for unit tests."""
endpoint = azure_ai_inference_unit_test_env["AZURE_AI_INFERENCE_ENDPOINT"]
api_key = azure_ai_inference_unit_test_env["AZURE_AI_INFERENCE_API_KEY"]
credential = AzureKeyCredential(api_key)
if request.param == AzureAIInferenceChatCompletion.__name__:
return ChatCompletionsClient(endpoint=endpoint, credential=credential)
if request.param == AzureAIInferenceTextEmbedding.__name__:
return EmbeddingsClient(endpoint=endpoint, credential=credential)
raise ValueError(f"Service {request.param} not supported.")
@pytest.fixture(scope="function")
def azure_ai_inference_service(azure_ai_inference_unit_test_env, model_id, request):
"""Fixture to create Azure AI Inference service for unit tests.
This is required because the Azure AI Inference services require a client to be created,
and the client will be talking to the endpoint at creation time.
"""
endpoint = azure_ai_inference_unit_test_env["AZURE_AI_INFERENCE_ENDPOINT"]
api_key = azure_ai_inference_unit_test_env["AZURE_AI_INFERENCE_API_KEY"]
if request.param == AzureAIInferenceChatCompletion.__name__:
return AzureAIInferenceChatCompletion(model_id, api_key=api_key, endpoint=endpoint)
if request.param == AzureAIInferenceTextEmbedding.__name__:
return AzureAIInferenceTextEmbedding(model_id, api_key=api_key, endpoint=endpoint)
raise ValueError(f"Service {request.param} not supported.")
@pytest.fixture()
def mock_azure_ai_inference_chat_completion_response(model_id) -> ChatCompletions:
return ChatCompletions(
id="test_id",
created=datetime.datetime.now(),
model=model_id,
usage=CompletionsUsage(
completion_tokens=0,
prompt_tokens=0,
total_tokens=0,
),
choices=[
ChatChoice(
index=0,
finish_reason="stop",
message=ChatResponseMessage(
role="assistant",
content="Hello",
),
)
],
)
@pytest.fixture()
def mock_azure_ai_inference_chat_completion_response_with_tool_call(model_id) -> ChatCompletions:
return ChatCompletions(
id="test_id",
created=datetime.datetime.now(),
model=model_id,
usage=CompletionsUsage(
completion_tokens=0,
prompt_tokens=0,
total_tokens=0,
),
choices=[
ChatChoice(
index=0,
finish_reason="tool_calls",
message=ChatResponseMessage(
role="assistant",
tool_calls=[
ChatCompletionsToolCall(
id="test_id",
function=FunctionCall(
name="getLightStatus",
arguments='{"arg1": "test_value"}',
),
),
],
),
)
],
)
@pytest.fixture()
def mock_azure_ai_inference_streaming_chat_completion_response(model_id) -> AsyncIterator:
streaming_chat_response = MagicMock(spec=AsyncGenerator)
streaming_chat_response.__aiter__.return_value = [
StreamingChatCompletionsUpdate(
id="test_id",
created=datetime.datetime.now(),
model=model_id,
usage=CompletionsUsage(
completion_tokens=0,
prompt_tokens=0,
total_tokens=0,
),
choices=[
# Empty choice
],
),
StreamingChatCompletionsUpdate(
id="test_id",
created=datetime.datetime.now(),
model=model_id,
usage=CompletionsUsage(
completion_tokens=0,
prompt_tokens=0,
total_tokens=0,
),
choices=[
StreamingChatChoiceUpdate(
index=0,
finish_reason="stop",
delta=ChatResponseMessage(
role="assistant",
content="Hello",
),
)
],
),
]
return streaming_chat_response
@pytest.fixture()
def mock_azure_ai_inference_streaming_chat_completion_response_with_tool_call(model_id) -> AsyncIterator:
streaming_chat_response = MagicMock(spec=AsyncGenerator)
streaming_chat_response.__aiter__.return_value = [
StreamingChatCompletionsUpdate(
id="test_id",
created=datetime.datetime.now(),
model=model_id,
usage=CompletionsUsage(
completion_tokens=0,
prompt_tokens=0,
total_tokens=0,
),
choices=[
# Empty choice
],
),
StreamingChatCompletionsUpdate(
id="test_id",
created=datetime.datetime.now(),
model=model_id,
usage=CompletionsUsage(
completion_tokens=0,
prompt_tokens=0,
total_tokens=0,
),
choices=[
StreamingChatChoiceUpdate(
index=0,
finish_reason="tool_calls",
delta=ChatResponseMessage(
role="assistant",
tool_calls=[
StreamingChatResponseToolCallUpdate(
id="test_id",
function=FunctionCall(
name="getLightStatus",
arguments='{"arg1": "test_value"}',
),
),
],
),
)
],
),
]
return streaming_chat_response
@@ -0,0 +1,710 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import AsyncMock, patch
import pytest
from azure.ai.inference.aio import ChatCompletionsClient
from azure.ai.inference.models import JsonSchemaFormat, UserMessage
from azure.core.credentials import AzureKeyCredential
from pydantic import BaseModel, Field
from semantic_kernel.connectors.ai.azure_ai_inference import (
AzureAIInferenceChatCompletion,
AzureAIInferenceChatPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.azure_ai_inference.azure_ai_inference_settings import AzureAIInferenceSettings
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.contents.utils.finish_reason import FinishReason
from semantic_kernel.exceptions.service_exceptions import (
ServiceInitializationError,
ServiceInvalidExecutionSettingsError,
)
from semantic_kernel.functions.kernel_arguments import KernelArguments
from semantic_kernel.kernel import Kernel
from semantic_kernel.utils.telemetry.user_agent import SEMANTIC_KERNEL_USER_AGENT
# region init
def test_azure_ai_inference_chat_completion_init(azure_ai_inference_unit_test_env, model_id) -> None:
"""Test initialization of AzureAIInferenceChatCompletion"""
azure_ai_inference = AzureAIInferenceChatCompletion(model_id, instruction_role="developer")
assert azure_ai_inference.ai_model_id == model_id
assert azure_ai_inference.service_id == model_id
assert isinstance(azure_ai_inference.client, ChatCompletionsClient)
assert azure_ai_inference.instruction_role == "developer"
@patch("azure.ai.inference.aio.ChatCompletionsClient.__init__", return_value=None)
def test_azure_ai_inference_chat_completion_client_init(
mock_client, azure_ai_inference_unit_test_env, model_id
) -> None:
"""Test initialization of the Azure AI Inference client"""
endpoint = azure_ai_inference_unit_test_env["AZURE_AI_INFERENCE_ENDPOINT"]
api_key = azure_ai_inference_unit_test_env["AZURE_AI_INFERENCE_API_KEY"]
settings = AzureAIInferenceSettings(endpoint=endpoint, api_key=api_key)
_ = AzureAIInferenceChatCompletion(model_id)
assert mock_client.call_count == 1
assert isinstance(mock_client.call_args.kwargs["endpoint"], str)
assert mock_client.call_args.kwargs["endpoint"] == str(settings.endpoint)
assert isinstance(mock_client.call_args.kwargs["credential"], AzureKeyCredential)
assert mock_client.call_args.kwargs["credential"].key == settings.api_key.get_secret_value()
assert mock_client.call_args.kwargs["user_agent"] == SEMANTIC_KERNEL_USER_AGENT
def test_azure_ai_inference_chat_completion_init_with_service_id(
azure_ai_inference_unit_test_env, model_id, service_id
) -> None:
"""Test initialization of AzureAIInferenceChatCompletion with service_id"""
azure_ai_inference = AzureAIInferenceChatCompletion(model_id, service_id=service_id)
assert azure_ai_inference.ai_model_id == model_id
assert azure_ai_inference.service_id == service_id
assert isinstance(azure_ai_inference.client, ChatCompletionsClient)
def test_azure_ai_inference_chat_completion_init_with_api_version(azure_ai_inference_unit_test_env, model_id) -> None:
"""Test initialization of AzureAIInferenceChatCompletion with api_version"""
azure_ai_inference = AzureAIInferenceChatCompletion(model_id, api_version="2024-02-15-test")
assert azure_ai_inference.ai_model_id == model_id
assert isinstance(azure_ai_inference.client, ChatCompletionsClient)
assert azure_ai_inference.client._config.api_version == "2024-02-15-test"
@pytest.mark.parametrize(
"azure_ai_inference_client",
[AzureAIInferenceChatCompletion.__name__],
indirect=True,
)
def test_azure_ai_inference_chat_completion_init_with_custom_client(azure_ai_inference_client, model_id) -> None:
"""Test initialization of AzureAIInferenceChatCompletion with custom client"""
client = azure_ai_inference_client
azure_ai_inference = AzureAIInferenceChatCompletion(model_id, client=client)
assert azure_ai_inference.ai_model_id == model_id
assert azure_ai_inference.service_id == model_id
assert azure_ai_inference.client == client
@pytest.mark.parametrize("exclude_list", [["AZURE_AI_INFERENCE_ENDPOINT"]], indirect=True)
def test_azure_ai_inference_chat_completion_init_with_empty_endpoint(
azure_ai_inference_unit_test_env, model_id
) -> None:
"""Test initialization of AzureAIInferenceChatCompletion with empty endpoint"""
with pytest.raises(ServiceInitializationError):
AzureAIInferenceChatCompletion(model_id, env_file_path="fake_path")
def test_prompt_execution_settings_class(azure_ai_inference_unit_test_env, model_id) -> None:
azure_ai_inference = AzureAIInferenceChatCompletion(model_id)
assert azure_ai_inference.get_prompt_execution_settings_class() == AzureAIInferenceChatPromptExecutionSettings
# endregion init
# region chat completion
@pytest.mark.parametrize(
"azure_ai_inference_service",
[AzureAIInferenceChatCompletion.__name__],
indirect=True,
)
@patch.object(ChatCompletionsClient, "complete", new_callable=AsyncMock)
async def test_azure_ai_inference_chat_completion(
mock_complete,
azure_ai_inference_service,
model_id,
chat_history: ChatHistory,
mock_azure_ai_inference_chat_completion_response,
) -> None:
"""Test completion of AzureAIInferenceChatCompletion"""
user_message_content: str = "Hello"
chat_history.add_user_message(user_message_content)
settings = AzureAIInferenceChatPromptExecutionSettings()
mock_complete.return_value = mock_azure_ai_inference_chat_completion_response
responses = await azure_ai_inference_service.get_chat_message_contents(chat_history=chat_history, settings=settings)
mock_complete.assert_awaited_once_with(
messages=[UserMessage(content=user_message_content)],
model=model_id,
model_extras=None,
**settings.prepare_settings_dict(),
)
assert len(responses) == 1
assert responses[0].role == "assistant"
assert responses[0].content == "Hello"
@pytest.mark.parametrize(
"azure_ai_inference_service",
[AzureAIInferenceChatCompletion.__name__],
indirect=True,
)
@patch.object(ChatCompletionsClient, "complete", new_callable=AsyncMock)
async def test_azure_ai_inference_chat_completion_with_standard_parameters(
mock_complete,
azure_ai_inference_service,
model_id,
chat_history: ChatHistory,
mock_azure_ai_inference_chat_completion_response,
) -> None:
"""Test completion of AzureAIInferenceChatCompletion with standard OpenAI parameters"""
user_message_content: str = "Hello"
chat_history.add_user_message(user_message_content)
settings = AzureAIInferenceChatPromptExecutionSettings(
frequency_penalty=0.5,
max_tokens=100,
presence_penalty=0.5,
seed=123,
stop="stop",
temperature=0.5,
top_p=0.5,
)
mock_complete.return_value = mock_azure_ai_inference_chat_completion_response
responses = await azure_ai_inference_service.get_chat_message_contents(chat_history=chat_history, settings=settings)
mock_complete.assert_awaited_once_with(
messages=[UserMessage(content=user_message_content)],
model=model_id,
model_extras=None,
frequency_penalty=settings.frequency_penalty,
max_tokens=settings.max_tokens,
presence_penalty=settings.presence_penalty,
seed=settings.seed,
stop=settings.stop,
temperature=settings.temperature,
top_p=settings.top_p,
)
assert len(responses) == 1
assert responses[0].role == "assistant"
assert responses[0].content == "Hello"
@pytest.mark.parametrize(
"azure_ai_inference_service",
[AzureAIInferenceChatCompletion.__name__],
indirect=True,
)
@patch.object(ChatCompletionsClient, "complete", new_callable=AsyncMock)
async def test_azure_ai_inference_chat_completion_with_extra_parameters(
mock_complete,
azure_ai_inference_service,
model_id,
chat_history: ChatHistory,
mock_azure_ai_inference_chat_completion_response,
) -> None:
"""Test completion of AzureAIInferenceChatCompletion with extra parameters"""
user_message_content: str = "Hello"
chat_history.add_user_message(user_message_content)
extra_parameters = {"test_key": "test_value"}
settings = AzureAIInferenceChatPromptExecutionSettings(extra_parameters=extra_parameters)
mock_complete.return_value = mock_azure_ai_inference_chat_completion_response
responses = await azure_ai_inference_service.get_chat_message_contents(chat_history=chat_history, settings=settings)
mock_complete.assert_awaited_once_with(
messages=[UserMessage(content=user_message_content)],
model=model_id,
model_extras=settings.extra_parameters,
)
assert len(responses) == 1
assert responses[0].role == "assistant"
assert responses[0].content == "Hello"
@pytest.mark.parametrize(
"azure_ai_inference_service",
[AzureAIInferenceChatCompletion.__name__],
indirect=True,
)
async def test_azure_ai_inference_chat_completion_with_function_choice_behavior_fail_verification(
azure_ai_inference_service,
kernel,
chat_history: ChatHistory,
) -> None:
"""Test completion of AzureAIInferenceChatCompletion with function choice behavior expect verification failure"""
# Missing kernel
with pytest.raises(ServiceInvalidExecutionSettingsError):
settings = AzureAIInferenceChatPromptExecutionSettings(
function_choice_behavior=FunctionChoiceBehavior.Auto(),
)
await azure_ai_inference_service.get_chat_message_contents(
chat_history=chat_history,
settings=settings,
arguments=KernelArguments(),
)
# More than 1 responses
with pytest.raises(ServiceInvalidExecutionSettingsError):
settings = AzureAIInferenceChatPromptExecutionSettings(
function_choice_behavior=FunctionChoiceBehavior.Auto(),
extra_parameters={"n": 2},
)
await azure_ai_inference_service.get_chat_message_contents(
chat_history=chat_history,
settings=settings,
kernel=kernel,
arguments=KernelArguments(),
)
@pytest.mark.parametrize(
"azure_ai_inference_service",
[AzureAIInferenceChatCompletion.__name__],
indirect=True,
)
@patch.object(ChatCompletionsClient, "complete", new_callable=AsyncMock)
async def test_azure_ai_inference_chat_completion_with_function_choice_behavior(
mock_complete,
azure_ai_inference_service,
kernel: Kernel,
chat_history: ChatHistory,
mock_azure_ai_inference_chat_completion_response_with_tool_call,
mock_azure_ai_inference_chat_completion_response,
decorated_native_function,
disabled_model_diagnostics_test_env,
) -> None:
"""Test completion of AzureAIInferenceChatCompletion with function choice behavior"""
user_message_content: str = "Hello"
chat_history.add_user_message(user_message_content)
settings = AzureAIInferenceChatPromptExecutionSettings(
function_choice_behavior=FunctionChoiceBehavior.Auto(),
)
settings.function_choice_behavior.maximum_auto_invoke_attempts = 1
# First call returns tool call, second call returns final response
mock_complete.side_effect = [
mock_azure_ai_inference_chat_completion_response_with_tool_call,
mock_azure_ai_inference_chat_completion_response,
]
kernel.add_function(plugin_name="TestPlugin", function=decorated_native_function)
responses = await azure_ai_inference_service.get_chat_message_contents(
chat_history=chat_history,
settings=settings,
kernel=kernel,
arguments=KernelArguments(),
)
# Completion should be called twice:
# One for the tool call and one for the last completion
# after the maximum_auto_invoke_attempts is reached
assert mock_complete.call_count == 2
assert len(responses) == 1
assert responses[0].role == "assistant"
assert responses[0].content == "Hello"
@pytest.mark.parametrize(
"azure_ai_inference_service",
[AzureAIInferenceChatCompletion.__name__],
indirect=True,
)
@patch.object(ChatCompletionsClient, "complete", new_callable=AsyncMock)
async def test_azure_ai_inference_chat_completion_with_function_choice_behavior_no_tool_call(
mock_complete,
azure_ai_inference_service,
model_id,
kernel,
chat_history: ChatHistory,
mock_azure_ai_inference_chat_completion_response,
) -> None:
"""Test completion of AzureAIInferenceChatCompletion with function choice behavior but no tool call"""
user_message_content: str = "Hello"
chat_history.add_user_message(user_message_content)
settings = AzureAIInferenceChatPromptExecutionSettings(
function_choice_behavior=FunctionChoiceBehavior.Auto(),
)
mock_complete.return_value = mock_azure_ai_inference_chat_completion_response
responses = await azure_ai_inference_service.get_chat_message_contents(
chat_history=chat_history,
settings=settings,
kernel=kernel,
arguments=KernelArguments(),
)
mock_complete.assert_awaited_once_with(
messages=[UserMessage(content=user_message_content)],
model=model_id,
model_extras=None,
**settings.prepare_settings_dict(),
)
assert len(responses) == 1
assert responses[0].role == "assistant"
assert responses[0].content == "Hello"
class MockResponseModel(BaseModel):
a: int = Field(..., description="The a field")
b: str = Field(..., description="The b field")
@pytest.mark.parametrize(
"azure_ai_inference_service",
[AzureAIInferenceChatCompletion.__name__],
indirect=True,
)
@patch.object(ChatCompletionsClient, "complete", new_callable=AsyncMock)
async def test_azure_ai_inference_response_format_json_schema(
mock_complete,
azure_ai_inference_service,
model_id,
chat_history: ChatHistory,
mock_azure_ai_inference_chat_completion_response,
):
chat_history.add_user_message("Return an object with fields a and b.")
settings = AzureAIInferenceChatPromptExecutionSettings(
response_format=MockResponseModel,
structured_json_response=True,
)
mock_complete.return_value = mock_azure_ai_inference_chat_completion_response
_ = await azure_ai_inference_service.get_chat_message_contents(chat_history=chat_history, settings=settings)
assert mock_complete.call_count == 1
kwargs = mock_complete.call_args.kwargs
assert "response_format" in kwargs
response_format = kwargs["response_format"]
assert isinstance(response_format, JsonSchemaFormat)
assert response_format.name == "MockResponseModel"
assert response_format.strict is True
schema = response_format.schema
assert schema["title"] == "MockResponseModel"
assert "properties" in schema
assert "a" in schema["properties"]
assert schema["properties"]["a"]["type"] == "integer"
assert "b" in schema["properties"]
assert schema["properties"]["b"]["type"] == "string"
assert kwargs["messages"][0].content == "Return an object with fields a and b."
assert kwargs["model"] == model_id
# endregion chat completion
# region streaming chat completion
@pytest.mark.parametrize(
"azure_ai_inference_service",
[AzureAIInferenceChatCompletion.__name__],
indirect=True,
)
@patch.object(ChatCompletionsClient, "complete", new_callable=AsyncMock)
async def test_azure_ai_inference_streaming_chat_completion(
mock_complete,
azure_ai_inference_service,
model_id,
chat_history: ChatHistory,
mock_azure_ai_inference_streaming_chat_completion_response,
) -> None:
"""Test streaming completion of AzureAIInferenceChatCompletion"""
user_message_content: str = "Hello"
chat_history.add_user_message(user_message_content)
settings = AzureAIInferenceChatPromptExecutionSettings()
mock_complete.return_value = mock_azure_ai_inference_streaming_chat_completion_response
async for messages in azure_ai_inference_service.get_streaming_chat_message_contents(chat_history, settings):
assert len(messages) == 1
assert messages[0].role == "assistant"
assert messages[0].content == "Hello"
mock_complete.assert_awaited_once_with(
stream=True,
messages=[UserMessage(content=user_message_content)],
model=model_id,
model_extras=None,
**settings.prepare_settings_dict(),
)
@pytest.mark.parametrize(
"azure_ai_inference_service",
[AzureAIInferenceChatCompletion.__name__],
indirect=True,
)
@patch.object(ChatCompletionsClient, "complete", new_callable=AsyncMock)
async def test_azure_ai_inference_chat_streaming_completion_with_standard_parameters(
mock_complete,
azure_ai_inference_service,
model_id,
chat_history: ChatHistory,
mock_azure_ai_inference_streaming_chat_completion_response,
) -> None:
"""Test streaming completion of AzureAIInferenceChatCompletion with standard OpenAI parameters"""
user_message_content: str = "Hello"
chat_history.add_user_message(user_message_content)
settings = AzureAIInferenceChatPromptExecutionSettings(
frequency_penalty=0.5,
max_tokens=100,
presence_penalty=0.5,
seed=123,
stop="stop",
temperature=0.5,
top_p=0.5,
)
mock_complete.return_value = mock_azure_ai_inference_streaming_chat_completion_response
async for messages in azure_ai_inference_service.get_streaming_chat_message_contents(chat_history, settings):
assert len(messages) == 1
assert messages[0].role == "assistant"
assert messages[0].content == "Hello"
mock_complete.assert_awaited_once_with(
stream=True,
messages=[UserMessage(content=user_message_content)],
model=model_id,
model_extras=None,
frequency_penalty=settings.frequency_penalty,
max_tokens=settings.max_tokens,
presence_penalty=settings.presence_penalty,
seed=settings.seed,
stop=settings.stop,
temperature=settings.temperature,
top_p=settings.top_p,
)
@pytest.mark.parametrize(
"azure_ai_inference_service",
[AzureAIInferenceChatCompletion.__name__],
indirect=True,
)
@patch.object(ChatCompletionsClient, "complete", new_callable=AsyncMock)
async def test_azure_ai_inference_streaming_chat_completion_with_extra_parameters(
mock_complete,
azure_ai_inference_service,
model_id,
chat_history: ChatHistory,
mock_azure_ai_inference_streaming_chat_completion_response,
) -> None:
"""Test streaming completion of AzureAIInferenceChatCompletion with extra parameters"""
user_message_content: str = "Hello"
chat_history.add_user_message(user_message_content)
extra_parameters = {"test_key": "test_value"}
settings = AzureAIInferenceChatPromptExecutionSettings(extra_parameters=extra_parameters)
mock_complete.return_value = mock_azure_ai_inference_streaming_chat_completion_response
async for messages in azure_ai_inference_service.get_streaming_chat_message_contents(chat_history, settings):
assert len(messages) == 1
assert messages[0].role == "assistant"
assert messages[0].content == "Hello"
mock_complete.assert_awaited_once_with(
stream=True,
messages=[UserMessage(content=user_message_content)],
model=model_id,
model_extras=settings.extra_parameters,
)
@pytest.mark.parametrize(
"azure_ai_inference_service",
[AzureAIInferenceChatCompletion.__name__],
indirect=True,
)
async def test_azure_ai_inference_streaming_chat_completion_with_function_choice_behavior_fail_verification(
azure_ai_inference_service,
kernel,
chat_history: ChatHistory,
) -> None:
"""Test completion of AzureAIInferenceChatCompletion with function choice behavior expect verification failure"""
# Missing kernel
with pytest.raises(ServiceInvalidExecutionSettingsError):
settings = AzureAIInferenceChatPromptExecutionSettings(
function_choice_behavior=FunctionChoiceBehavior.Auto(),
)
async for _ in azure_ai_inference_service.get_streaming_chat_message_contents(
chat_history,
settings,
arguments=KernelArguments(),
):
pass
# More than 1 responses
with pytest.raises(ServiceInvalidExecutionSettingsError):
settings = AzureAIInferenceChatPromptExecutionSettings(
function_choice_behavior=FunctionChoiceBehavior.Auto(),
extra_parameters={"n": 2},
)
async for _ in azure_ai_inference_service.get_streaming_chat_message_contents(
chat_history,
settings,
kernel=kernel,
arguments=KernelArguments(),
):
pass
@pytest.mark.parametrize(
"azure_ai_inference_service",
[AzureAIInferenceChatCompletion.__name__],
indirect=True,
)
@patch.object(ChatCompletionsClient, "complete", new_callable=AsyncMock)
async def test_azure_ai_inference_streaming_chat_completion_with_function_choice_behavior(
mock_complete,
azure_ai_inference_service,
kernel: Kernel,
chat_history: ChatHistory,
mock_azure_ai_inference_streaming_chat_completion_response_with_tool_call,
decorated_native_function,
) -> None:
"""Test streaming completion of AzureAIInferenceChatCompletion with function choice behavior."""
user_message_content: str = "Hello"
chat_history.add_user_message(user_message_content)
settings = AzureAIInferenceChatPromptExecutionSettings(
function_choice_behavior=FunctionChoiceBehavior.Auto(),
)
settings.function_choice_behavior.maximum_auto_invoke_attempts = 1
mock_complete.return_value = mock_azure_ai_inference_streaming_chat_completion_response_with_tool_call
kernel.add_function(plugin_name="TestPlugin", function=decorated_native_function)
all_messages = []
async for messages in azure_ai_inference_service.get_streaming_chat_message_contents(
chat_history,
settings,
kernel=kernel,
arguments=KernelArguments(),
):
all_messages.extend(messages)
# Assert the number of total messages
assert len(all_messages) == 2, f"Expected 2 messages, got {len(all_messages)}"
# Validate the first message
assert all_messages[0].role == "assistant", f"Unexpected role for first message: {all_messages[0].role}"
assert all_messages[0].content == "", f"Unexpected content for first message: {all_messages[0].content}"
assert all_messages[0].finish_reason == FinishReason.TOOL_CALLS, (
f"Unexpected finish reason for first message: {all_messages[0].finish_reason}"
)
# Validate the second message
assert all_messages[1].role == "tool", f"Unexpected role for second message: {all_messages[1].role}"
assert all_messages[1].content == "", f"Unexpected content for second message: {all_messages[1].content}"
assert all_messages[1].finish_reason is None
@pytest.mark.parametrize(
"azure_ai_inference_service",
[AzureAIInferenceChatCompletion.__name__],
indirect=True,
)
@patch.object(ChatCompletionsClient, "complete", new_callable=AsyncMock)
async def test_azure_ai_inference_streaming_chat_completion_with_function_choice_behavior_no_tool_call(
mock_complete,
azure_ai_inference_service,
model_id,
kernel,
chat_history: ChatHistory,
mock_azure_ai_inference_streaming_chat_completion_response,
) -> None:
"""Test streaming completion of AzureAIInferenceChatCompletion with function choice behavior but no tool call"""
user_message_content: str = "Hello"
chat_history.add_user_message(user_message_content)
settings = AzureAIInferenceChatPromptExecutionSettings(
function_choice_behavior=FunctionChoiceBehavior.Auto(),
)
mock_complete.return_value = mock_azure_ai_inference_streaming_chat_completion_response
async for messages in azure_ai_inference_service.get_streaming_chat_message_contents(
chat_history,
settings,
kernel=kernel,
arguments=KernelArguments(),
):
assert len(messages) == 1
assert messages[0].role == "assistant"
assert messages[0].content == "Hello"
mock_complete.assert_awaited_once_with(
stream=True,
messages=[UserMessage(content=user_message_content)],
model=model_id,
model_extras=None,
**settings.prepare_settings_dict(),
)
class MockStreamingResponseModel(BaseModel):
foo: float = Field(..., description="Foo value")
bar: bool = Field(..., description="Bar value")
@pytest.mark.parametrize(
"azure_ai_inference_service",
[AzureAIInferenceChatCompletion.__name__],
indirect=True,
)
@patch.object(ChatCompletionsClient, "complete", new_callable=AsyncMock)
async def test_azure_ai_inference_streaming_response_format_json_schema(
mock_complete,
azure_ai_inference_service,
model_id,
chat_history: ChatHistory,
mock_azure_ai_inference_streaming_chat_completion_response,
):
chat_history.add_user_message("Stream a response with foo and bar.")
settings = AzureAIInferenceChatPromptExecutionSettings(
response_format=MockStreamingResponseModel,
structured_json_response=True,
)
mock_complete.return_value = mock_azure_ai_inference_streaming_chat_completion_response
messages = []
async for chunk in azure_ai_inference_service.get_streaming_chat_message_contents(chat_history, settings):
messages.extend(chunk)
assert mock_complete.call_count == 1
kwargs = mock_complete.call_args.kwargs
assert "response_format" in kwargs
response_format = kwargs["response_format"]
assert isinstance(response_format, JsonSchemaFormat)
assert response_format.name == "MockStreamingResponseModel"
assert response_format.strict is True
schema = response_format.schema
assert schema["title"] == "MockStreamingResponseModel"
assert "foo" in schema["properties"]
assert schema["properties"]["foo"]["type"] == "number"
assert "bar" in schema["properties"]
assert schema["properties"]["bar"]["type"] == "boolean"
assert kwargs["stream"] is True
assert kwargs["model"] == model_id
# endregion streaming chat completion
@@ -0,0 +1,163 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import AsyncMock, patch
import pytest
from azure.ai.inference.aio import EmbeddingsClient
from azure.core.credentials import AzureKeyCredential
from semantic_kernel.connectors.ai.azure_ai_inference import (
AzureAIInferenceEmbeddingPromptExecutionSettings,
AzureAIInferenceTextEmbedding,
)
from semantic_kernel.connectors.ai.azure_ai_inference.azure_ai_inference_settings import AzureAIInferenceSettings
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError
from semantic_kernel.utils.telemetry.user_agent import SEMANTIC_KERNEL_USER_AGENT
def test_azure_ai_inference_text_embedding_init(azure_ai_inference_unit_test_env, model_id) -> None:
"""Test initialization of AzureAIInferenceTextEmbedding"""
azure_ai_inference = AzureAIInferenceTextEmbedding(model_id)
assert azure_ai_inference.ai_model_id == model_id
assert azure_ai_inference.service_id == model_id
assert isinstance(azure_ai_inference.client, EmbeddingsClient)
@patch("azure.ai.inference.aio.EmbeddingsClient.__init__", return_value=None)
def test_azure_ai_inference_text_embedding_client_init(mock_client, azure_ai_inference_unit_test_env, model_id) -> None:
"""Test initialization of the Azure AI Inference client"""
endpoint = azure_ai_inference_unit_test_env["AZURE_AI_INFERENCE_ENDPOINT"]
api_key = azure_ai_inference_unit_test_env["AZURE_AI_INFERENCE_API_KEY"]
settings = AzureAIInferenceSettings(endpoint=endpoint, api_key=api_key)
_ = AzureAIInferenceTextEmbedding(model_id)
assert mock_client.call_count == 1
assert isinstance(mock_client.call_args.kwargs["endpoint"], str)
assert mock_client.call_args.kwargs["endpoint"] == str(settings.endpoint)
assert isinstance(mock_client.call_args.kwargs["credential"], AzureKeyCredential)
assert mock_client.call_args.kwargs["credential"].key == settings.api_key.get_secret_value()
assert mock_client.call_args.kwargs["user_agent"] == SEMANTIC_KERNEL_USER_AGENT
def test_azure_ai_inference_text_embedding_init_with_service_id(
azure_ai_inference_unit_test_env, model_id, service_id
) -> None:
"""Test initialization of AzureAIInferenceTextEmbedding"""
azure_ai_inference = AzureAIInferenceTextEmbedding(model_id, service_id=service_id)
assert azure_ai_inference.ai_model_id == model_id
assert azure_ai_inference.service_id == service_id
assert isinstance(azure_ai_inference.client, EmbeddingsClient)
def test_azure_ai_inference_text_embedding_init_with_api_version(azure_ai_inference_unit_test_env, model_id) -> None:
"""Test initialization of AzureAIInferenceTextEmbedding with api_version"""
azure_ai_inference = AzureAIInferenceTextEmbedding(model_id, api_version="2024-02-15-test")
assert azure_ai_inference.ai_model_id == model_id
assert isinstance(azure_ai_inference.client, EmbeddingsClient)
assert azure_ai_inference.client._config.api_version == "2024-02-15-test"
@pytest.mark.parametrize(
"azure_ai_inference_client",
[AzureAIInferenceTextEmbedding.__name__],
indirect=True,
)
def test_azure_ai_inference_chat_completion_init_with_custom_client(azure_ai_inference_client, model_id) -> None:
"""Test initialization of AzureAIInferenceTextEmbedding with custom client"""
client = azure_ai_inference_client
azure_ai_inference = AzureAIInferenceTextEmbedding(model_id, client=client)
assert azure_ai_inference.ai_model_id == model_id
assert azure_ai_inference.service_id == model_id
assert azure_ai_inference.client == client
@pytest.mark.parametrize("exclude_list", [["AZURE_AI_INFERENCE_ENDPOINT"]], indirect=True)
def test_azure_ai_inference_text_embedding_init_with_empty_endpoint(azure_ai_inference_unit_test_env, model_id) -> None:
"""Test initialization of AzureAIInferenceTextEmbedding with empty endpoint"""
with pytest.raises(ServiceInitializationError):
AzureAIInferenceTextEmbedding(model_id, env_file_path="fake_path")
@pytest.mark.parametrize(
"azure_ai_inference_service",
[AzureAIInferenceTextEmbedding.__name__],
indirect=True,
)
@patch.object(EmbeddingsClient, "embed", new_callable=AsyncMock)
async def test_azure_ai_inference_text_embedding(
mock_embed,
azure_ai_inference_service,
model_id,
) -> None:
"""Test text embedding generation of AzureAIInferenceTextEmbedding without settings"""
texts = ["hello", "world"]
await azure_ai_inference_service.generate_embeddings(texts)
mock_embed.assert_awaited_once_with(
input=texts,
model=model_id,
model_extras=None,
dimensions=None,
encoding_format=None,
input_type=None,
)
@pytest.mark.parametrize(
"azure_ai_inference_service",
[AzureAIInferenceTextEmbedding.__name__],
indirect=True,
)
@patch.object(EmbeddingsClient, "embed", new_callable=AsyncMock)
async def test_azure_ai_inference_text_embedding_with_standard_settings(
mock_embed,
azure_ai_inference_service,
model_id,
) -> None:
"""Test text embedding generation of AzureAIInferenceTextEmbedding with standard settings"""
texts = ["hello", "world"]
settings = AzureAIInferenceEmbeddingPromptExecutionSettings(
dimensions=1024, encoding_format="float", input_type="text"
)
await azure_ai_inference_service.generate_embeddings(texts, settings)
mock_embed.assert_awaited_once_with(
input=texts,
model=model_id,
model_extras=None,
dimensions=settings.dimensions,
encoding_format=settings.encoding_format,
input_type=settings.input_type,
)
@pytest.mark.parametrize(
"azure_ai_inference_service",
[AzureAIInferenceTextEmbedding.__name__],
indirect=True,
)
@patch.object(EmbeddingsClient, "embed", new_callable=AsyncMock)
async def test_azure_ai_inference_text_embedding_with_extra_parameters(
mock_embed,
azure_ai_inference_service,
model_id,
) -> None:
"""Test text embedding generation of AzureAIInferenceTextEmbedding with extra parameters"""
texts = ["hello", "world"]
extra_parameters = {"test_key": "test_value"}
settings = AzureAIInferenceEmbeddingPromptExecutionSettings(extra_parameters=extra_parameters)
await azure_ai_inference_service.generate_embeddings(texts, settings)
mock_embed.assert_awaited_once_with(
input=texts,
model=model_id,
model_extras=extra_parameters,
dimensions=settings.dimensions,
encoding_format=settings.encoding_format,
input_type=settings.input_type,
)
@@ -0,0 +1,239 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import AsyncMock, patch
import pytest
from azure.ai.inference.aio import ChatCompletionsClient
from semantic_kernel.connectors.ai.azure_ai_inference.azure_ai_inference_prompt_execution_settings import (
AzureAIInferenceChatPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.azure_ai_inference.services.azure_ai_inference_chat_completion import (
AzureAIInferenceChatCompletion,
)
from semantic_kernel.contents.chat_history import ChatHistory
@pytest.mark.parametrize(
"azure_ai_inference_service",
[AzureAIInferenceChatCompletion.__name__],
indirect=True,
)
@patch.object(ChatCompletionsClient, "complete", new_callable=AsyncMock)
@patch("azure.ai.inference.tracing.AIInferenceInstrumentor.uninstrument")
@patch("azure.ai.inference.tracing.AIInferenceInstrumentor.instrument")
async def test_azure_ai_inference_chat_completion_instrumentation(
mock_instrument,
mock_uninstrument,
mock_complete,
azure_ai_inference_service,
chat_history: ChatHistory,
mock_azure_ai_inference_chat_completion_response,
model_diagnostics_test_env,
) -> None:
"""Test completion of AzureAIInferenceChatCompletion"""
settings = AzureAIInferenceChatPromptExecutionSettings()
mock_complete.return_value = mock_azure_ai_inference_chat_completion_response
await azure_ai_inference_service.get_chat_message_contents(chat_history=chat_history, settings=settings)
mock_instrument.assert_called_once_with(enable_content_recording=True)
mock_uninstrument.assert_called_once()
@pytest.mark.parametrize(
"azure_ai_inference_service",
[
AzureAIInferenceChatCompletion.__name__,
],
indirect=True,
)
@pytest.mark.parametrize(
"override_env_param_dict",
[
{
"SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS": "False",
"SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE": "False",
},
],
indirect=True,
)
@patch.object(ChatCompletionsClient, "complete", new_callable=AsyncMock)
@patch("azure.ai.inference.tracing.AIInferenceInstrumentor.uninstrument")
@patch("azure.ai.inference.tracing.AIInferenceInstrumentor.instrument")
async def test_azure_ai_inference_chat_completion_not_instrumentation(
mock_instrument,
mock_uninstrument,
mock_complete,
azure_ai_inference_service,
chat_history: ChatHistory,
mock_azure_ai_inference_chat_completion_response,
model_diagnostics_test_env,
) -> None:
"""Test completion of AzureAIInferenceChatCompletion"""
settings = AzureAIInferenceChatPromptExecutionSettings()
mock_complete.return_value = mock_azure_ai_inference_chat_completion_response
await azure_ai_inference_service.get_chat_message_contents(chat_history=chat_history, settings=settings)
mock_instrument.assert_not_called()
mock_uninstrument.assert_not_called()
@pytest.mark.parametrize(
"azure_ai_inference_service",
[
AzureAIInferenceChatCompletion.__name__,
],
indirect=True,
)
@pytest.mark.parametrize(
"override_env_param_dict",
[
{
"SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS": "True",
"SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE": "False",
},
],
indirect=True,
)
@patch.object(ChatCompletionsClient, "complete", new_callable=AsyncMock)
@patch("azure.ai.inference.tracing.AIInferenceInstrumentor.uninstrument")
@patch("azure.ai.inference.tracing.AIInferenceInstrumentor.instrument")
async def test_azure_ai_inference_chat_completion_instrumentation_without_sensitive(
mock_instrument,
mock_uninstrument,
mock_complete,
azure_ai_inference_service,
chat_history: ChatHistory,
mock_azure_ai_inference_chat_completion_response,
model_diagnostics_test_env,
) -> None:
"""Test completion of AzureAIInferenceChatCompletion"""
settings = AzureAIInferenceChatPromptExecutionSettings()
mock_complete.return_value = mock_azure_ai_inference_chat_completion_response
await azure_ai_inference_service.get_chat_message_contents(chat_history=chat_history, settings=settings)
mock_instrument.assert_called_once_with(enable_content_recording=False)
mock_uninstrument.assert_called_once()
@pytest.mark.parametrize(
"azure_ai_inference_service",
[AzureAIInferenceChatCompletion.__name__],
indirect=True,
)
@patch.object(ChatCompletionsClient, "complete", new_callable=AsyncMock)
@patch("azure.ai.inference.tracing.AIInferenceInstrumentor.uninstrument")
@patch("azure.ai.inference.tracing.AIInferenceInstrumentor.instrument")
async def test_azure_ai_inference_streaming_chat_completion_instrumentation(
mock_instrument,
mock_uninstrument,
mock_complete,
azure_ai_inference_service,
chat_history: ChatHistory,
mock_azure_ai_inference_streaming_chat_completion_response,
model_diagnostics_test_env,
) -> None:
"""Test completion of AzureAIInferenceChatCompletion"""
settings = AzureAIInferenceChatPromptExecutionSettings()
mock_complete.return_value = mock_azure_ai_inference_streaming_chat_completion_response
async for _ in azure_ai_inference_service.get_streaming_chat_message_contents(
chat_history=chat_history, settings=settings
):
pass
mock_instrument.assert_called_once_with(enable_content_recording=True)
mock_uninstrument.assert_called_once()
@pytest.mark.parametrize(
"azure_ai_inference_service",
[
AzureAIInferenceChatCompletion.__name__,
],
indirect=True,
)
@pytest.mark.parametrize(
"override_env_param_dict",
[
{
"SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS": "False",
"SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE": "False",
},
],
indirect=True,
)
@patch.object(ChatCompletionsClient, "complete", new_callable=AsyncMock)
@patch("azure.ai.inference.tracing.AIInferenceInstrumentor.uninstrument")
@patch("azure.ai.inference.tracing.AIInferenceInstrumentor.instrument")
async def test_azure_ai_inference_streaming_chat_completion_not_instrumentation(
mock_instrument,
mock_uninstrument,
mock_complete,
azure_ai_inference_service,
chat_history: ChatHistory,
mock_azure_ai_inference_streaming_chat_completion_response,
model_diagnostics_test_env,
) -> None:
"""Test completion of AzureAIInferenceChatCompletion"""
settings = AzureAIInferenceChatPromptExecutionSettings()
mock_complete.return_value = mock_azure_ai_inference_streaming_chat_completion_response
async for _ in azure_ai_inference_service.get_streaming_chat_message_contents(
chat_history=chat_history, settings=settings
):
pass
mock_instrument.assert_not_called()
mock_uninstrument.assert_not_called()
@pytest.mark.parametrize(
"azure_ai_inference_service",
[
AzureAIInferenceChatCompletion.__name__,
],
indirect=True,
)
@pytest.mark.parametrize(
"override_env_param_dict",
[
{
"SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS": "True",
"SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE": "False",
},
],
indirect=True,
)
@patch.object(ChatCompletionsClient, "complete", new_callable=AsyncMock)
@patch("azure.ai.inference.tracing.AIInferenceInstrumentor.uninstrument")
@patch("azure.ai.inference.tracing.AIInferenceInstrumentor.instrument")
async def test_azure_ai_inference_streaming_chat_completion_instrumentation_without_sensitive(
mock_instrument,
mock_uninstrument,
mock_complete,
azure_ai_inference_service,
chat_history: ChatHistory,
mock_azure_ai_inference_streaming_chat_completion_response,
model_diagnostics_test_env,
) -> None:
"""Test completion of AzureAIInferenceChatCompletion"""
settings = AzureAIInferenceChatPromptExecutionSettings()
mock_complete.return_value = mock_azure_ai_inference_streaming_chat_completion_response
async for _ in azure_ai_inference_service.get_streaming_chat_message_contents(
chat_history=chat_history, settings=settings
):
pass
mock_instrument.assert_called_once_with(enable_content_recording=False)
mock_uninstrument.assert_called_once()
@@ -0,0 +1,169 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from azure.ai.inference.models import (
AssistantMessage,
ImageContentItem,
SystemMessage,
TextContentItem,
ToolMessage,
UserMessage,
)
from semantic_kernel.connectors.ai.azure_ai_inference.services.utils import MESSAGE_CONVERTERS
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
def test_message_convertors_contain_all_author_roles() -> None:
"""Test that all AuthorRoles are present in the MESSAGE_CONVERTERS dict."""
for role in AuthorRole:
assert role in MESSAGE_CONVERTERS
def test_format_system_message() -> None:
"""Test that a system message is formatted correctly."""
message = ChatMessageContent(role=AuthorRole.SYSTEM, content="test content")
system_message = MESSAGE_CONVERTERS[message.role](message)
assert isinstance(system_message, SystemMessage)
assert system_message.content == message.content
def test_format_user_message_with_no_image() -> None:
"""Test that a user message with no image items is formatted correctly."""
message = ChatMessageContent(role=AuthorRole.USER, content="test content")
user_message = MESSAGE_CONVERTERS[message.role](message)
assert isinstance(user_message, UserMessage)
assert user_message.content == message.content
def test_format_user_message_with_image() -> None:
"""Test that a user message with image items is formatted correctly"""
message = ChatMessageContent(
role=AuthorRole.USER,
items=[
TextContent(text="test text"),
ImageContent(uri="https://test.com/image.jpg"),
],
)
user_message = MESSAGE_CONVERTERS[message.role](message)
assert isinstance(user_message, UserMessage)
assert len(user_message.content) == 2
assert isinstance(user_message.content[0], TextContentItem)
assert isinstance(user_message.content[1], ImageContentItem)
def test_format_user_message_with_unsupported_items() -> None:
"""Test that a user message with unsupported items is formatted correctly"""
message = ChatMessageContent(
role=AuthorRole.USER,
items=[
TextContent(text="test text"),
ImageContent(), # ImageContent without uri or data_uri is unsupported
FunctionCallContent(id="test function"), # FunctionCallContent unsupported
],
)
user_message = MESSAGE_CONVERTERS[message.role](message)
assert isinstance(user_message, UserMessage)
assert len(user_message.content) == 1
assert isinstance(user_message.content[0], TextContentItem)
def test_format_assistant_message() -> None:
"""Test that an assistant message is formatted correctly."""
message = ChatMessageContent(role=AuthorRole.ASSISTANT, content="test content")
assistant_message = MESSAGE_CONVERTERS[message.role](message)
assert isinstance(assistant_message, AssistantMessage)
assert assistant_message.content == message.content
def test_format_assistant_message_with_tool_call() -> None:
"""Test that an assistant message with a tool call is formatted correctly."""
function_call_content = FunctionCallContent(id="test function")
message = ChatMessageContent(
role=AuthorRole.ASSISTANT,
items=[function_call_content],
)
assistant_message = MESSAGE_CONVERTERS[message.role](message)
assert isinstance(assistant_message, AssistantMessage)
assert assistant_message.content == message.content
assert len(assistant_message.tool_calls) == 1
assert assistant_message.tool_calls[0].id == function_call_content.id
def test_format_assistant_message_with_unsupported_items() -> None:
"""Test that an assistant message with unsupported items is formatted correctly."""
text_content = TextContent(text="test text")
message = ChatMessageContent(
role=AuthorRole.ASSISTANT,
items=[
text_content,
ImageContent(), # ImageContent is unsupported
],
)
assistant_message = MESSAGE_CONVERTERS[message.role](message)
assert isinstance(assistant_message, AssistantMessage)
assert assistant_message.content == message.content
def test_format_tool_message() -> None:
"""Test that a tool message is formatted correctly."""
function_result_content = FunctionResultContent(id="test function", result="test result")
message = ChatMessageContent(
role=AuthorRole.TOOL,
items=[function_result_content],
)
tool_message = MESSAGE_CONVERTERS[message.role](message)
assert isinstance(tool_message, ToolMessage)
assert tool_message.content == function_result_content.result
assert tool_message.tool_call_id == function_result_content.id
def test_format_tool_message_item_not_found_as_the_first_item() -> None:
"""Test that formatting a tool message where the function result item is not the first item."""
function_result_content = FunctionResultContent(id="test function", result="test result")
message = ChatMessageContent(
role=AuthorRole.TOOL,
items=[
TextContent(text="test text"),
function_result_content,
],
)
with pytest.raises(ValueError):
MESSAGE_CONVERTERS[message.role](message)
def test_format_tool_message_with_more_than_one_items() -> None:
"""Test that a tool message with more than one item is formatted correctly."""
function_result_content = FunctionResultContent(id="test function", result="test result")
message = ChatMessageContent(
role=AuthorRole.TOOL,
items=[
function_result_content,
TextContent(text="test text"),
],
)
tool_message = MESSAGE_CONVERTERS[message.role](message)
assert isinstance(tool_message, ToolMessage)
assert tool_message.content == function_result_content.result
assert tool_message.tool_call_id == function_result_content.id
@@ -0,0 +1,143 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.connectors.ai.azure_ai_inference import (
AzureAIInferenceChatPromptExecutionSettings,
AzureAIInferenceEmbeddingPromptExecutionSettings,
AzureAIInferencePromptExecutionSettings,
)
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
def test_default_azure_ai_inference_prompt_execution_settings():
settings = AzureAIInferencePromptExecutionSettings()
assert settings.frequency_penalty is None
assert settings.max_tokens is None
assert settings.presence_penalty is None
assert settings.seed is None
assert settings.stop is None
assert settings.temperature is None
assert settings.top_p is None
assert settings.extra_parameters is None
def test_custom_azure_ai_inference_prompt_execution_settings():
settings = AzureAIInferencePromptExecutionSettings(
frequency_penalty=0.5,
max_tokens=128,
presence_penalty=0.5,
seed=1,
stop="world",
temperature=0.5,
top_p=0.5,
extra_parameters={"key": "value"},
)
assert settings.frequency_penalty == 0.5
assert settings.max_tokens == 128
assert settings.presence_penalty == 0.5
assert settings.seed == 1
assert settings.stop == "world"
assert settings.temperature == 0.5
assert settings.top_p == 0.5
assert settings.extra_parameters == {"key": "value"}
def test_azure_ai_inference_prompt_execution_settings_from_default_completion_config():
settings = PromptExecutionSettings(service_id="test_service")
chat_settings = AzureAIInferenceChatPromptExecutionSettings.from_prompt_execution_settings(settings)
assert chat_settings.service_id == "test_service"
assert chat_settings.frequency_penalty is None
assert chat_settings.max_tokens is None
assert chat_settings.presence_penalty is None
assert chat_settings.seed is None
assert chat_settings.stop is None
assert chat_settings.temperature is None
assert chat_settings.top_p is None
assert chat_settings.extra_parameters is None
def test_azure_ai_inference_prompt_execution_settings_from_openai_prompt_execution_settings():
chat_settings = AzureAIInferenceChatPromptExecutionSettings(service_id="test_service", temperature=1.0)
new_settings = AzureAIInferencePromptExecutionSettings(service_id="test_2", temperature=0.0)
chat_settings.update_from_prompt_execution_settings(new_settings)
assert chat_settings.service_id == "test_2"
assert chat_settings.temperature == 0.0
def test_azure_ai_inference_prompt_execution_settings_from_custom_completion_config():
settings = PromptExecutionSettings(
service_id="test_service",
extension_data={
"frequency_penalty": 0.5,
"max_tokens": 128,
"presence_penalty": 0.5,
"seed": 1,
"stop": "world",
"temperature": 0.5,
"top_p": 0.5,
"extra_parameters": {"key": "value"},
},
)
chat_settings = AzureAIInferenceChatPromptExecutionSettings.from_prompt_execution_settings(settings)
assert chat_settings.service_id == "test_service"
assert chat_settings.frequency_penalty == 0.5
assert chat_settings.max_tokens == 128
assert chat_settings.presence_penalty == 0.5
assert chat_settings.seed == 1
assert chat_settings.stop == "world"
assert chat_settings.temperature == 0.5
assert chat_settings.top_p == 0.5
assert chat_settings.extra_parameters == {"key": "value"}
def test_azure_ai_inference_chat_prompt_execution_settings_from_custom_completion_config_with_functions():
settings = PromptExecutionSettings(
service_id="test_service",
extension_data={
"tools": [{"function": {}}],
},
)
chat_settings = AzureAIInferenceChatPromptExecutionSettings.from_prompt_execution_settings(settings)
assert chat_settings.tools == [{"function": {}}]
def test_create_options():
settings = AzureAIInferenceChatPromptExecutionSettings(
service_id="test_service",
extension_data={
"frequency_penalty": 0.5,
"max_tokens": 128,
"presence_penalty": 0.5,
"seed": 1,
"stop": "world",
"temperature": 0.5,
"top_p": 0.5,
"extra_parameters": {"key": "value"},
},
)
options = settings.prepare_settings_dict()
assert options["frequency_penalty"] == 0.5
assert options["max_tokens"] == 128
assert options["presence_penalty"] == 0.5
assert options["seed"] == 1
assert options["stop"] == "world"
assert options["temperature"] == 0.5
assert options["top_p"] == 0.5
assert options["extra_parameters"] == {"key": "value"}
assert "tools" not in options
assert "tool_config" not in options
def test_default_azure_ai_inference_embedding_prompt_execution_settings():
settings = AzureAIInferenceEmbeddingPromptExecutionSettings()
assert settings.dimensions is None
assert settings.encoding_format is None
assert settings.input_type is None
assert settings.extra_parameters is None
@@ -0,0 +1,320 @@
# Copyright (c) Microsoft. All rights reserved.
import json
from unittest.mock import Mock
import pytest
from semantic_kernel.connectors.ai.bedrock.services.model_provider.bedrock_model_provider import BedrockModelProvider
from semantic_kernel.contents.chat_history import ChatHistory
@pytest.fixture()
def model_id(request) -> str:
if hasattr(request, "param"):
return request.param
return "test_model_id"
@pytest.fixture()
def service_id() -> str:
return "test_service_id"
@pytest.fixture()
def chat_history() -> ChatHistory:
chat_history = ChatHistory(system_message="You are a helpful assistant.")
chat_history.add_user_message("Hello!")
chat_history.add_assistant_message("Hi! How can I help you today?")
chat_history.add_system_message("Be polite and respectful.")
chat_history.add_user_message("I need help with a task.")
return chat_history
@pytest.fixture()
def bedrock_unit_test_env(monkeypatch, exclude_list, override_env_param_dict):
"""Fixture to set environment variables for Amazon Bedrock AI connector unit tests."""
if exclude_list is None:
exclude_list = []
if override_env_param_dict is None:
override_env_param_dict = {}
env_vars = {
"BEDROCK_TEXT_MODEL_ID": "env_test_text_model_id",
"BEDROCK_CHAT_MODEL_ID": "env_test_chat_model_id",
"BEDROCK_EMBEDDING_MODEL_ID": "env_test_embedding_model_id",
"BEDROCK_MODEL_PROVIDER": "amazon",
}
env_vars.update(override_env_param_dict)
for key, value in env_vars.items():
if key not in exclude_list:
monkeypatch.setenv(key, value)
else:
monkeypatch.delenv(key, raising=False)
return env_vars
class MockBedrockClient(Mock):
def __init__(self, *args, **kwargs):
pass
def get_foundation_model(self, *args, **kwargs):
return {
"modelDetails": {
"responseStreamingSupported": True,
"inputModalities": ["TEXT"],
"outputModalities": ["TEXT", "EMBEDDING"],
}
}
class MockBedrockRuntimeClient(Mock):
def __init__(self, *args, **kwargs):
pass
def converse(self, *args, **kwargs):
pass
def converse_stream(self, *args, **kwargs):
pass
def invoke_model(self, *args, **kwargs):
pass
def invoke_model_with_response_stream(self, *args, **kwargs):
pass
# region mock chat completion responses
@pytest.fixture()
def mock_bedrock_chat_completion_response():
# https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-call.html#conversation-inference-call-response
return {
"output": {
"message": {
"role": "assistant",
"content": [
{
"text": "Hi! How can I help you today?",
}
],
}
},
"stopReason": "end_turn",
"usage": {
"inputTokens": 125,
"outputTokens": 60,
"totalTokens": 185,
},
}
@pytest.fixture()
def mock_bedrock_streaming_chat_completion_response():
# https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-call.html#conversation-inference-call-response
events = [
{"messageStart": {"role": "assistant"}},
{"contentBlockStart": {"contentBlockIndex": 0, "start": {}}},
{"contentBlockDelta": {"contentBlockIndex": 0, "delta": {"text": "Hi! "}}},
{"contentBlockDelta": {"contentBlockIndex": 0, "delta": {"text": "How can "}}},
{"contentBlockDelta": {"contentBlockIndex": 0, "delta": {"text": "I help you today?"}}},
{"contentBlockStop": {"contentBlockIndex": 0}},
{"messageStop": {"stopReason": "end_turn"}},
{
"metadata": {
"metrics": {"latencyMs": 1000},
"usage": {"inputTokens": 125, "outputTokens": 60, "totalTokens": 185},
}
},
]
def event_stream(events):
yield from events
return {"stream": event_stream(events)}
@pytest.fixture()
def mock_bedrock_streaming_chat_completion_invalid_response():
events = [
{"unknown": {}},
]
def event_stream(events):
yield from events
return {"stream": event_stream(events)}
# endregion
# region mock text completion responses
@pytest.fixture()
def output_text():
return "Hi! How can I help you today?"
@pytest.fixture()
def model_provider():
return BedrockModelProvider.AMAZON
@pytest.fixture()
def mock_bedrock_text_completion_response(
model_id: str,
output_text: str,
request,
):
# Check if model_provider fixture is requested by the test
model_provider = None
if "model_provider" in request.fixturenames:
model_provider = request.getfixturevalue("model_provider")
else:
model_provider = BedrockModelProvider.to_model_provider(model_id)
match model_provider:
case BedrockModelProvider.AMAZON:
body = {
"inputTextTokenCount": 10,
"results": [
{
"tokenCount": 10,
"outputText": output_text,
"completionReason": "FINISHED ",
}
],
}
case BedrockModelProvider.ANTHROPIC:
body = {
"completion": output_text,
"stop_reason": "stop_sequence",
"stop": "",
}
case BedrockModelProvider.COHERE:
body = {
"generations": [
{
"text": output_text,
}
],
}
case BedrockModelProvider.AI21LABS:
body = {
"completions": [
{
"data": {
"text": output_text,
}
}
],
}
case BedrockModelProvider.META:
body = {
"generation": output_text,
"prompt_token_count": 10,
"generation_token_count": 10,
}
case BedrockModelProvider.MISTRALAI:
body = {"outputs": [{"text": output_text}]}
mock = Mock()
mock.read.return_value = json.dumps(body)
return {"body": mock}
@pytest.fixture()
def mock_bedrock_streaming_text_completion_response(
model_id: str,
output_text: str,
request,
):
# Check if model_provider fixture is requested by the test
model_provider = None
if "model_provider" in request.fixturenames:
model_provider = request.getfixturevalue("model_provider")
else:
model_provider = BedrockModelProvider.to_model_provider(model_id)
match model_provider:
case BedrockModelProvider.AMAZON:
chunks = [
{
"chunk": {
"bytes": json.dumps({
"inputTextTokenCount": 10,
"totalOutputTextTokenCount": 10,
"outputText": chunk,
}).encode(),
}
}
for chunk in [output_text[i : i + 3] for i in range(0, len(output_text), 3)]
]
def event_stream(events):
yield from events
return {"body": event_stream(chunks)}
# endregion
# region mock text embedding responses
@pytest.fixture()
def mock_bedrock_text_embedding_response(
model_id: str,
request,
):
# Check if model_provider fixture is requested by the test
model_provider = None
if "model_provider" in request.fixturenames:
model_provider = request.getfixturevalue("model_provider")
else:
model_provider = BedrockModelProvider.to_model_provider(model_id)
match model_provider:
case BedrockModelProvider.AMAZON:
body = {
"embedding": [0.1, 0.2, 0.3],
}
case BedrockModelProvider.COHERE:
body = {
"embeddings": [[0.1, 0.2, 0.3]],
}
mock = Mock()
mock.read.return_value = json.dumps(body)
return {"body": mock}
@pytest.fixture()
def mock_bedrock_text_embedding_invalid_response(model_id: str):
model_provider = BedrockModelProvider.to_model_provider(model_id)
match model_provider:
case BedrockModelProvider.AMAZON:
body = {"embedding": 0.1}
case BedrockModelProvider.COHERE:
body = {"embeddings": 0.1}
mock = Mock()
mock.read.return_value = json.dumps(body)
return {"body": mock}
# endregion
@@ -0,0 +1,371 @@
# Copyright (c) Microsoft. All rights reserved.
from functools import reduce
from unittest.mock import Mock, patch
import boto3
import pytest
from semantic_kernel.connectors.ai.bedrock.bedrock_prompt_execution_settings import BedrockChatPromptExecutionSettings
from semantic_kernel.connectors.ai.bedrock.services.bedrock_chat_completion import BedrockChatCompletion
from semantic_kernel.connectors.ai.bedrock.services.model_provider.bedrock_model_provider import BedrockModelProvider
from semantic_kernel.connectors.ai.completion_usage import CompletionUsage
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
from semantic_kernel.contents.text_content import TextContent
from semantic_kernel.contents.utils.author_role import AuthorRole
from semantic_kernel.contents.utils.finish_reason import FinishReason
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError, ServiceInvalidResponseError
from tests.unit.connectors.ai.bedrock.conftest import MockBedrockClient, MockBedrockRuntimeClient
# region init
@patch.object(boto3, "client", return_value=Mock())
def test_bedrock_chat_completion_init(mock_client, bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Chat Completion service"""
bedrock_chat_completion = BedrockChatCompletion()
assert bedrock_chat_completion.ai_model_id == bedrock_unit_test_env["BEDROCK_CHAT_MODEL_ID"]
assert bedrock_chat_completion.service_id == bedrock_unit_test_env["BEDROCK_CHAT_MODEL_ID"]
assert bedrock_chat_completion.bedrock_model_provider == BedrockModelProvider(
bedrock_unit_test_env["BEDROCK_MODEL_PROVIDER"]
)
assert bedrock_chat_completion.bedrock_client is not None
assert bedrock_chat_completion.bedrock_runtime_client is not None
@patch.object(boto3, "client", return_value=Mock())
def test_bedrock_chat_completion_init_model_id_override(mock_client, bedrock_unit_test_env, model_id) -> None:
"""Test initialization of Amazon Bedrock Chat Completion service"""
bedrock_chat_completion = BedrockChatCompletion(model_id=model_id)
assert bedrock_chat_completion.ai_model_id == model_id
assert bedrock_chat_completion.service_id == model_id
assert bedrock_chat_completion.bedrock_client is not None
assert bedrock_chat_completion.bedrock_runtime_client is not None
@patch.object(boto3, "client", return_value=Mock())
def test_bedrock_chat_completion_init_custom_service_id(mock_client, bedrock_unit_test_env, service_id) -> None:
"""Test initialization of Amazon Bedrock Chat Completion service"""
bedrock_chat_completion = BedrockChatCompletion(service_id=service_id)
assert bedrock_chat_completion.service_id == service_id
assert bedrock_chat_completion.bedrock_client is not None
assert bedrock_chat_completion.bedrock_runtime_client is not None
def test_bedrock_chat_completion_init_custom_clients(bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Chat Completion service"""
bedrock_chat_completion = BedrockChatCompletion(
runtime_client=MockBedrockRuntimeClient(),
client=MockBedrockClient(),
)
assert isinstance(bedrock_chat_completion.bedrock_client, MockBedrockClient)
assert isinstance(bedrock_chat_completion.bedrock_runtime_client, MockBedrockRuntimeClient)
@patch.object(boto3, "client", return_value=Mock())
def test_bedrock_chat_completion_init_custom_client(mock_client, bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Chat Completion service"""
bedrock_chat_completion = BedrockChatCompletion(
client=MockBedrockClient(),
)
assert isinstance(bedrock_chat_completion.bedrock_client, MockBedrockClient)
assert bedrock_chat_completion.bedrock_runtime_client is not None
@patch.object(boto3, "client", return_value=Mock())
def test_bedrock_chat_completion_init_custom_runtime_client(mock_client, bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Chat Completion service"""
bedrock_chat_completion = BedrockChatCompletion(
runtime_client=MockBedrockRuntimeClient(),
)
assert bedrock_chat_completion.bedrock_client is not None
assert isinstance(bedrock_chat_completion.bedrock_runtime_client, MockBedrockRuntimeClient)
@patch.object(boto3, "client", return_value=Mock())
def test_bedrock_chat_completion_init_custom_bedrock_model_provider(mock_client, bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Chat Completion service"""
bedrock_chat_completion = BedrockChatCompletion(
model_provider=BedrockModelProvider.AMAZON,
)
assert bedrock_chat_completion.bedrock_model_provider == BedrockModelProvider.AMAZON
@pytest.mark.parametrize("exclude_list", [["BEDROCK_CHAT_MODEL_ID"]], indirect=True)
def test_bedrock_chat_completion_client_init_with_empty_model_id(bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Chat Completion service with empty model id"""
with pytest.raises(ServiceInitializationError, match="The Amazon Bedrock Chat Model ID is missing."):
BedrockChatCompletion(env_file_path="fake_env_file_path.env")
def test_bedrock_chat_completion_client_init_invalid_settings(bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Chat Completion service with invalid settings"""
with pytest.raises(
ServiceInitializationError, match="Failed to initialize the Amazon Bedrock Chat Completion Service."
):
BedrockChatCompletion(model_id=123) # Model ID must be a string
def test_bedrock_chat_completion_client_init_invalid_model_provider(bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Chat Completion service with invalid settings"""
with pytest.raises(
ServiceInitializationError, match="Failed to initialize the Amazon Bedrock Chat Completion Service."
):
BedrockChatCompletion(model_provider="invalid_provider")
@patch.object(boto3, "client", return_value=Mock())
def test_prompt_execution_settings_class(mock_client, bedrock_unit_test_env) -> None:
"""Test getting prompt execution settings class"""
bedrock_completion_client = BedrockChatCompletion()
assert bedrock_completion_client.get_prompt_execution_settings_class() == BedrockChatPromptExecutionSettings
# endregion
# region private methods
@patch.object(boto3, "client", return_value=Mock())
def test_prepare_chat_history_for_request(mock_client, bedrock_unit_test_env, chat_history) -> None:
"""Test preparing chat history for request"""
bedrock_chat_completion = BedrockChatCompletion()
parsed_chat_history = bedrock_chat_completion._prepare_chat_history_for_request(chat_history)
assert isinstance(parsed_chat_history, list)
assert len(parsed_chat_history) == len(chat_history) - 2 # Exclude system message
assert all([item["role"] in ["user", "assistant"] for item in parsed_chat_history])
@patch.object(boto3, "client", return_value=Mock())
def test_prepare_system_message_for_request(mock_client, bedrock_unit_test_env, chat_history) -> None:
"""Test preparing system message for request"""
bedrock_chat_completion = BedrockChatCompletion()
parsed_system_message = bedrock_chat_completion._prepare_system_messages_for_request(chat_history)
assert isinstance(parsed_system_message, list)
assert len(parsed_system_message) == 2
@pytest.mark.parametrize(
"model_id",
[
"amazon.titan",
"anthropic.claude",
"cohere.command",
"ai21.jamba",
"meta.llama",
"mistral.ai",
],
)
@patch.object(boto3, "client", return_value=Mock())
def test_prepare_settings_for_request(mock_client, model_id, chat_history) -> None:
"""Test preparing settings for request"""
bedrock_chat_completion = BedrockChatCompletion(model_id=model_id)
settings = BedrockChatPromptExecutionSettings()
parsed_settings = bedrock_chat_completion._prepare_settings_for_request(chat_history, settings)
assert isinstance(parsed_settings, dict)
assert parsed_settings["modelId"] == bedrock_chat_completion.ai_model_id
assert parsed_settings["messages"] == bedrock_chat_completion._prepare_chat_history_for_request(chat_history)
assert parsed_settings["system"] == bedrock_chat_completion._prepare_system_messages_for_request(chat_history)
assert isinstance(parsed_settings["inferenceConfig"], dict)
assert all([parsed_settings["inferenceConfig"].values()])
@pytest.mark.parametrize(
"model_id",
[
"arn:aws:bedrock:us-east-1:972143716085:application-inference-profile/123456",
],
)
@patch.object(boto3, "client", return_value=Mock())
def test_prepare_settings_for_request_with_application_inference_profile(mock_client, model_id, chat_history) -> None:
"""Test preparing settings for request"""
# Without a valid model provider, it should raise an error
bedrock_chat_completion = BedrockChatCompletion(model_id=model_id)
settings = BedrockChatPromptExecutionSettings()
with pytest.raises(
ValueError,
match=f"Model ID {model_id} does not contain a valid model provider name.",
):
bedrock_chat_completion._prepare_settings_for_request(chat_history, settings)
# With a valid model provider, it should not raise an error
bedrock_chat_completion = BedrockChatCompletion(model_id=model_id, model_provider=BedrockModelProvider.AMAZON)
parsed_settings = bedrock_chat_completion._prepare_settings_for_request(chat_history, settings)
assert isinstance(parsed_settings, dict)
assert parsed_settings["modelId"] == bedrock_chat_completion.ai_model_id
assert parsed_settings["messages"] == bedrock_chat_completion._prepare_chat_history_for_request(chat_history)
assert parsed_settings["system"] == bedrock_chat_completion._prepare_system_messages_for_request(chat_history)
assert isinstance(parsed_settings["inferenceConfig"], dict)
assert all([parsed_settings["inferenceConfig"].values()])
# endregion
# region chat completion
@pytest.mark.parametrize(
# These are fake model ids with the supported prefixes
"model_id",
[
"amazon.titan",
"anthropic.claude",
"cohere.command",
"ai21.jamba",
"meta.llama",
"mistral.ai",
],
)
async def test_bedrock_chat_completion(
model_id,
chat_history: ChatHistory,
mock_bedrock_chat_completion_response,
) -> None:
"""Test Amazon Bedrock Chat Completion complete method"""
with patch.object(
MockBedrockRuntimeClient, "converse", return_value=mock_bedrock_chat_completion_response
) as mock_converse:
# Setup
bedrock_chat_completion = BedrockChatCompletion(
model_id=model_id,
runtime_client=MockBedrockRuntimeClient(),
client=MockBedrockClient(),
)
# Act
settings = BedrockChatPromptExecutionSettings()
response = await bedrock_chat_completion.get_chat_message_contents(chat_history=chat_history, settings=settings)
# Assert
mock_converse.assert_called_once_with(
**(bedrock_chat_completion._prepare_settings_for_request(chat_history, settings))
)
assert isinstance(response, list)
assert len(response) == 1
assert isinstance(response[0], ChatMessageContent)
assert response[0].ai_model_id == model_id
assert response[0].role == AuthorRole.ASSISTANT
assert len(response[0].items) == 1
assert isinstance(response[0].items[0], TextContent)
assert response[0].finish_reason == FinishReason.STOP
assert response[0].metadata["usage"] == CompletionUsage(
prompt_tokens=mock_bedrock_chat_completion_response["usage"]["inputTokens"],
completion_tokens=mock_bedrock_chat_completion_response["usage"]["outputTokens"],
)
assert (
response[0].items[0].text
== mock_bedrock_chat_completion_response["output"]["message"]["content"][0]["text"]
)
assert response[0].inner_content == mock_bedrock_chat_completion_response
@pytest.mark.parametrize(
# These are fake model ids with the supported prefixes
"model_id",
[
"amazon.titan",
"anthropic.claude",
"cohere.command",
"ai21.jamba",
"meta.llama",
"mistral.ai",
],
)
async def test_bedrock_streaming_chat_completion(
model_id,
chat_history: ChatHistory,
mock_bedrock_streaming_chat_completion_response,
) -> None:
"""Test Amazon Bedrock Streaming Chat Completion complete method"""
with patch.object(
MockBedrockRuntimeClient, "converse_stream", return_value=mock_bedrock_streaming_chat_completion_response
) as mock_converse_stream:
# Setup
bedrock_chat_completion = BedrockChatCompletion(
model_id=model_id,
runtime_client=MockBedrockRuntimeClient(),
client=MockBedrockClient(),
)
# Act
settings = BedrockChatPromptExecutionSettings()
chunks: list[StreamingChatMessageContent] = []
async for streaming_messages in bedrock_chat_completion.get_streaming_chat_message_contents(
chat_history=chat_history, settings=settings
):
chunks.extend(streaming_messages)
response = reduce(lambda p, r: p + r, chunks)
# Assert
mock_converse_stream.assert_called_once_with(
**(bedrock_chat_completion._prepare_settings_for_request(chat_history, settings))
)
assert isinstance(response, StreamingChatMessageContent)
assert response.ai_model_id == model_id
assert response.role == AuthorRole.ASSISTANT
assert len(response.items) == 1
assert isinstance(response.inner_content, list)
assert len(response.inner_content) == 7
assert response.finish_reason == FinishReason.STOP
@pytest.mark.parametrize(
# These are fake model ids with the supported prefixes
"model_id",
[
"amazon.titan",
"anthropic.claude",
"cohere.command",
"ai21.jamba",
"meta.llama",
"mistral.ai",
],
)
async def test_bedrock_streaming_chat_completion_invalid_event(
model_id,
chat_history: ChatHistory,
mock_bedrock_streaming_chat_completion_invalid_response,
) -> None:
"""Test Amazon Bedrock Streaming Chat Completion complete method"""
with patch.object(
MockBedrockRuntimeClient,
"converse_stream",
return_value=mock_bedrock_streaming_chat_completion_invalid_response,
):
# Setup
bedrock_chat_completion = BedrockChatCompletion(
model_id=model_id,
runtime_client=MockBedrockRuntimeClient(),
client=MockBedrockClient(),
)
# Act
settings = BedrockChatPromptExecutionSettings()
with pytest.raises(ServiceInvalidResponseError):
async for chunk in bedrock_chat_completion.get_streaming_chat_message_contents(
chat_history=chat_history, settings=settings
):
pass
# endregion
@@ -0,0 +1,443 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import MagicMock
import pytest
from semantic_kernel.connectors.ai.bedrock.bedrock_prompt_execution_settings import BedrockChatPromptExecutionSettings
from semantic_kernel.connectors.ai.bedrock.services.model_provider.bedrock_model_provider import (
BedrockModelProvider,
)
from semantic_kernel.connectors.ai.bedrock.services.model_provider.utils import (
MESSAGE_CONVERTERS,
finish_reason_from_bedrock_to_semantic_kernel,
remove_none_recursively,
update_settings_from_function_choice_configuration,
)
from semantic_kernel.connectors.ai.function_call_choice_configuration import FunctionCallChoiceConfiguration
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
from semantic_kernel.connectors.ai.function_choice_type import FunctionChoiceType
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.contents.utils.finish_reason import FinishReason
from semantic_kernel.exceptions.service_exceptions import ServiceInvalidRequestError
from semantic_kernel.kernel import Kernel
def test_remove_none_recursively():
data = {
"a": 1,
"b": None,
"c": {
"d": 2,
"e": None,
"f": {
"g": 3,
"h": None,
},
},
}
expected = {
"a": 1,
"c": {
"d": 2,
"f": {
"g": 3,
},
},
}
assert remove_none_recursively(data) == expected
def test_remove_recursively_max_depth():
data = {
"a": {"b": None},
}
assert remove_none_recursively(data, max_depth=1) == data
def test_update_settings_from_function_choice_configuration_auto(kernel: Kernel, custom_plugin_class) -> None:
kernel.add_plugin(plugin=custom_plugin_class(), plugin_name="custom_plugin")
settings = BedrockChatPromptExecutionSettings()
auto_function_choice_behavior = FunctionChoiceBehavior.Auto()
auto_function_choice_behavior.configure(
kernel,
update_settings_from_function_choice_configuration,
settings,
)
assert "auto" in settings.tool_choice
assert len(settings.tools) == 1
def test_update_settings_from_function_choice_configuration_auto_without_plugin(kernel: Kernel) -> None:
settings = BedrockChatPromptExecutionSettings()
auto_function_choice_behavior = FunctionChoiceBehavior.Auto()
auto_function_choice_behavior.configure(
kernel,
update_settings_from_function_choice_configuration,
settings,
)
assert settings.tool_choice is None
assert settings.tools is None
def test_update_settings_from_function_choice_configuration_none(kernel: Kernel) -> None:
settings = BedrockChatPromptExecutionSettings()
auto_function_choice_behavior = FunctionChoiceBehavior.NoneInvoke()
auto_function_choice_behavior.configure(
kernel,
update_settings_from_function_choice_configuration,
settings,
)
assert settings.tool_choice is None
assert settings.tools is None
def test_update_settings_from_function_choice_configuration_required_with_one_function(
kernel: Kernel,
custom_plugin_class,
) -> None:
kernel.add_plugin(plugin=custom_plugin_class(), plugin_name="custom_plugin")
settings = BedrockChatPromptExecutionSettings()
auto_function_choice_behavior = FunctionChoiceBehavior.Required()
auto_function_choice_behavior.configure(
kernel,
update_settings_from_function_choice_configuration,
settings,
)
assert "tool" in settings.tool_choice
assert len(settings.tools) == 1
def test_update_settings_from_function_choice_configuration_required_with_more_than_one_functions(
kernel: Kernel,
custom_plugin_class,
experimental_plugin_class,
) -> None:
kernel.add_plugin(plugin=custom_plugin_class(), plugin_name="custom_plugin")
kernel.add_plugin(plugin=experimental_plugin_class(), plugin_name="experimental_plugin")
settings = BedrockChatPromptExecutionSettings()
auto_function_choice_behavior = FunctionChoiceBehavior.Required()
auto_function_choice_behavior.configure(
kernel,
update_settings_from_function_choice_configuration,
settings,
)
assert "any" in settings.tool_choice
assert len(settings.tools) == 2
def test_inference_profile_with_bedrock_model() -> None:
"""Test the BedrockModelProvider class returns the correct model for a given inference profile."""
us_amazon_inference_profile = "us.amazon.nova-lite-v1:0"
assert BedrockModelProvider.to_model_provider(us_amazon_inference_profile) == BedrockModelProvider.AMAZON
us_anthropic_inference_profile = "us.anthropic.claude-3-sonnet-20240229-v1:0"
assert BedrockModelProvider.to_model_provider(us_anthropic_inference_profile) == BedrockModelProvider.ANTHROPIC
eu_meta_inference_profile = "eu.meta.llama3-2-3b-instruct-v1:0"
assert BedrockModelProvider.to_model_provider(eu_meta_inference_profile) == BedrockModelProvider.META
unknown_inference_profile = "unknown"
with pytest.raises(ValueError, match="Model ID unknown does not contain a valid model provider name."):
BedrockModelProvider.to_model_provider(unknown_inference_profile)
def test_remove_none_recursively_empty_dict() -> None:
"""Test that an empty dict returns an empty dict."""
assert remove_none_recursively({}) == {}
def test_remove_none_recursively_no_none() -> None:
"""Test that a dict with no None values remains the same."""
original = {"a": 1, "b": 2}
result = remove_none_recursively(original)
assert result == {"a": 1, "b": 2}
def test_remove_none_recursively_with_none() -> None:
"""Test that dict values of None are removed."""
original = {"a": 1, "b": None, "c": {"d": None, "e": 3}}
result = remove_none_recursively(original)
# 'b' should be removed and 'd' inside nested dict should be removed
assert result == {"a": 1, "c": {"e": 3}}
def test_remove_none_recursively_max_depth() -> None:
"""Test that the function respects max_depth."""
original = {"a": {"b": {"c": None}}}
# If max_depth=1, it won't go deep enough to remove 'c'.
result = remove_none_recursively(original, max_depth=1)
assert result == {"a": {"b": {"c": None}}}
# If max_depth=3, it should remove 'c'.
result = remove_none_recursively(original, max_depth=3)
assert result == {"a": {"b": {}}}
def test_format_system_message() -> None:
"""Test that system message is formatted correctly."""
content = ChatMessageContent(role=AuthorRole.SYSTEM, content="System message")
formatted = MESSAGE_CONVERTERS[AuthorRole.SYSTEM](content)
assert formatted == {"text": "System message"}
def test_format_user_message_text_only() -> None:
"""Test user message with only text content."""
text_item = TextContent(text="Hello!")
user_message = ChatMessageContent(role=AuthorRole.USER, items=[text_item])
formatted = MESSAGE_CONVERTERS[AuthorRole.USER](user_message)
assert formatted["role"] == "user"
assert len(formatted["content"]) == 1
assert formatted["content"][0] == {"text": "Hello!"}
def test_format_user_message_image_only() -> None:
"""Test user message with only image content."""
img_item = ImageContent(data=b"abc", mime_type="image/png")
user_message = ChatMessageContent(role=AuthorRole.USER, items=[img_item])
formatted = MESSAGE_CONVERTERS[AuthorRole.USER](user_message)
assert formatted["role"] == "user"
assert len(formatted["content"]) == 1
image_section = formatted["content"][0].get("image")
assert image_section["format"] == "png"
assert image_section["source"]["bytes"] == b"abc"
def test_format_user_message_unsupported_content() -> None:
"""Test user message raises error with unsupported content type."""
# We can simulate an unsupported content type by using FunctionCallContent.
func_call_item = FunctionCallContent(id="123", function_name="test_function", arguments="{}")
user_message = ChatMessageContent(role=AuthorRole.USER, items=[func_call_item])
with pytest.raises(ServiceInvalidRequestError) as exc:
MESSAGE_CONVERTERS[AuthorRole.USER](user_message)
assert "Only text and image content are supported in a user message." in str(exc.value)
def test_format_assistant_message_text_content() -> None:
"""Test assistant message with text content."""
text_item = TextContent(text="Assistant response")
assistant_message = ChatMessageContent(role=AuthorRole.ASSISTANT, items=[text_item])
formatted = MESSAGE_CONVERTERS[AuthorRole.ASSISTANT](assistant_message)
assert formatted["role"] == "assistant"
assert formatted["content"] == [{"text": "Assistant response"}]
def test_format_assistant_message_function_call_content() -> None:
"""Test assistant message with function call content."""
func_item = FunctionCallContent(
id="fc1", plugin_name="plugin", function_name="function", arguments='{"param": "value"}'
)
assistant_message = ChatMessageContent(role=AuthorRole.ASSISTANT, items=[func_item])
formatted = MESSAGE_CONVERTERS[AuthorRole.ASSISTANT](assistant_message)
assert len(formatted["content"]) == 1
tool_use = formatted["content"][0].get("toolUse")
assert tool_use
assert tool_use["toolUseId"] == "fc1"
assert tool_use["name"] == "plugin-function"
assert tool_use["input"] == {"param": "value"}
def test_format_assistant_message_image_content_raises() -> None:
"""Test assistant message with image raises error."""
img_item = ImageContent(data=b"abc", mime_type="image/jpeg")
assistant_message = ChatMessageContent(role=AuthorRole.ASSISTANT, items=[img_item])
with pytest.raises(ServiceInvalidRequestError) as exc:
MESSAGE_CONVERTERS[AuthorRole.ASSISTANT](assistant_message)
assert "Image content is not supported in an assistant message." in str(exc.value)
def test_format_assistant_message_unsupported_type() -> None:
"""Test assistant message with unsupported item content type."""
func_res_item = FunctionResultContent(id="res1", function_name="some_function", result="some_result")
assistant_message = ChatMessageContent(role=AuthorRole.ASSISTANT, items=[func_res_item])
with pytest.raises(ServiceInvalidRequestError) as exc:
MESSAGE_CONVERTERS[AuthorRole.ASSISTANT](assistant_message)
assert "Unsupported content type in an assistant message:" in str(exc.value)
def test_format_tool_message_text() -> None:
"""Test tool message with text content."""
text_item = TextContent(text="Some text")
tool_message = ChatMessageContent(role=AuthorRole.TOOL, items=[text_item])
formatted = MESSAGE_CONVERTERS[AuthorRole.TOOL](tool_message)
assert formatted["role"] == "user" # note that for a tool message, role set to 'user'
assert formatted["content"] == [{"text": "Some text"}]
def test_format_tool_message_function_result() -> None:
"""Test tool message with function result content."""
func_result_item = FunctionResultContent(id="res_id", function_name="test_function", result="some result")
tool_message = ChatMessageContent(role=AuthorRole.TOOL, items=[func_result_item])
formatted = MESSAGE_CONVERTERS[AuthorRole.TOOL](tool_message)
assert formatted["role"] == "user"
content = formatted["content"][0]
assert content.get("toolResult")
assert content["toolResult"]["toolUseId"] == "res_id"
assert content["toolResult"]["content"] == [{"text": "some result"}]
def test_format_tool_message_image_raises() -> None:
"""Test tool message with image content raises an error."""
img_item = ImageContent(data=b"xyz", mime_type="image/jpeg")
tool_message = ChatMessageContent(role=AuthorRole.TOOL, items=[img_item])
with pytest.raises(ServiceInvalidRequestError) as exc:
MESSAGE_CONVERTERS[AuthorRole.TOOL](tool_message)
assert "Image content is not supported in a tool message." in str(exc.value)
def test_finish_reason_from_bedrock_to_semantic_kernel_stop() -> None:
"""Test that 'stop_sequence' maps to FinishReason.STOP"""
reason = finish_reason_from_bedrock_to_semantic_kernel("stop_sequence")
assert reason == FinishReason.STOP
reason = finish_reason_from_bedrock_to_semantic_kernel("end_turn")
assert reason == FinishReason.STOP
def test_finish_reason_from_bedrock_to_semantic_kernel_length() -> None:
"""Test that 'max_tokens' maps to FinishReason.LENGTH"""
reason = finish_reason_from_bedrock_to_semantic_kernel("max_tokens")
assert reason == FinishReason.LENGTH
def test_finish_reason_from_bedrock_to_semantic_kernel_content_filtered() -> None:
"""Test that 'content_filtered' maps to FinishReason.CONTENT_FILTER"""
reason = finish_reason_from_bedrock_to_semantic_kernel("content_filtered")
assert reason == FinishReason.CONTENT_FILTER
def test_finish_reason_from_bedrock_to_semantic_kernel_tool_use() -> None:
"""Test that 'tool_use' maps to FinishReason.TOOL_CALLS"""
reason = finish_reason_from_bedrock_to_semantic_kernel("tool_use")
assert reason == FinishReason.TOOL_CALLS
def test_finish_reason_from_bedrock_to_semantic_kernel_unknown() -> None:
"""Test that unknown finish reason returns None"""
reason = finish_reason_from_bedrock_to_semantic_kernel("something_unknown")
assert reason is None
@pytest.fixture
def mock_bedrock_settings() -> BedrockChatPromptExecutionSettings:
"""Helper fixture for BedrockChatPromptExecutionSettings."""
return BedrockChatPromptExecutionSettings()
@pytest.fixture
def mock_function_choice_config() -> FunctionCallChoiceConfiguration:
"""Helper fixture for a sample FunctionCallChoiceConfiguration."""
# We'll create mock kernel functions with metadata
mock_func_1 = MagicMock()
mock_func_1.fully_qualified_name = "plugin-function1"
mock_func_1.description = "Function 1 description"
param1 = MagicMock()
param1.name = "param1"
param1.schema_data = {"type": "string"}
param1.is_required = True
param2 = MagicMock()
param2.name = "param2"
param2.schema_data = {"type": "integer"}
param2.is_required = False
mock_func_1.parameters = [
param1,
param2,
]
mock_func_2 = MagicMock()
mock_func_2.fully_qualified_name = "plugin-function2"
mock_func_2.description = "Function 2 description"
mock_func_2.parameters = []
config = FunctionCallChoiceConfiguration()
config.available_functions = [mock_func_1, mock_func_2]
return config
def test_update_settings_from_function_choice_configuration_none_type(
mock_function_choice_config, mock_bedrock_settings
) -> None:
"""Test that if the FunctionChoiceType is NONE it doesn't modify settings."""
update_settings_from_function_choice_configuration(
mock_function_choice_config, mock_bedrock_settings, FunctionChoiceType.NONE
)
assert mock_bedrock_settings.tool_choice is None
assert mock_bedrock_settings.tools is None
def test_update_settings_from_function_choice_configuration_auto_two_tools(
mock_function_choice_config, mock_bedrock_settings
) -> None:
"""Test that AUTO sets tool_choice to {"auto": {}} and sets tools list"""
update_settings_from_function_choice_configuration(
mock_function_choice_config, mock_bedrock_settings, FunctionChoiceType.AUTO
)
assert mock_bedrock_settings.tool_choice == {"auto": {}}
assert len(mock_bedrock_settings.tools) == 2
# Validate structure of first tool
tool_spec_1 = mock_bedrock_settings.tools[0].get("toolSpec")
assert tool_spec_1["name"] == "plugin-function1"
assert tool_spec_1["description"] == "Function 1 description"
def test_update_settings_from_function_choice_configuration_required_many(
mock_function_choice_config, mock_bedrock_settings
) -> None:
"""Test that REQUIRED with more than one function sets tool_choice to {"any": {}}."""
update_settings_from_function_choice_configuration(
mock_function_choice_config, mock_bedrock_settings, FunctionChoiceType.REQUIRED
)
assert mock_bedrock_settings.tool_choice == {"any": {}}
assert len(mock_bedrock_settings.tools) == 2
def test_update_settings_from_function_choice_configuration_required_one(mock_bedrock_settings) -> None:
"""Test that REQUIRED with a single function picks "tool" with that function name."""
single_func = MagicMock()
single_func.fully_qualified_name = "plugin-function"
single_func.description = "Only function"
single_func.parameters = []
config = FunctionCallChoiceConfiguration()
config.available_functions = [single_func]
update_settings_from_function_choice_configuration(config, mock_bedrock_settings, FunctionChoiceType.REQUIRED)
assert mock_bedrock_settings.tool_choice == {"tool": {"name": "plugin-function"}}
assert len(mock_bedrock_settings.tools) == 1
assert mock_bedrock_settings.tools[0]["toolSpec"]["name"] == "plugin-function"
@@ -0,0 +1,324 @@
# Copyright (c) Microsoft. All rights reserved.
import json
from functools import reduce
from unittest.mock import Mock, patch
import boto3
import pytest
from semantic_kernel.connectors.ai.bedrock.bedrock_prompt_execution_settings import BedrockTextPromptExecutionSettings
from semantic_kernel.connectors.ai.bedrock.services.bedrock_text_completion import BedrockTextCompletion
from semantic_kernel.connectors.ai.bedrock.services.model_provider.bedrock_model_provider import (
BedrockModelProvider,
get_text_completion_request_body,
)
from semantic_kernel.contents.streaming_text_content import StreamingTextContent
from semantic_kernel.contents.text_content import TextContent
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError
from tests.unit.connectors.ai.bedrock.conftest import MockBedrockClient, MockBedrockRuntimeClient
# region init
@patch.object(boto3, "client", return_value=Mock())
def test_bedrock_text_completion_init(mock_client, bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Text Completion service"""
bedrock_text_completion = BedrockTextCompletion()
assert bedrock_text_completion.ai_model_id == bedrock_unit_test_env["BEDROCK_TEXT_MODEL_ID"]
assert bedrock_text_completion.service_id == bedrock_unit_test_env["BEDROCK_TEXT_MODEL_ID"]
assert bedrock_text_completion.bedrock_model_provider == BedrockModelProvider(
bedrock_unit_test_env["BEDROCK_MODEL_PROVIDER"]
)
assert bedrock_text_completion.bedrock_client is not None
assert bedrock_text_completion.bedrock_runtime_client is not None
@patch.object(boto3, "client", return_value=Mock())
def test_bedrock_text_completion_init_model_id_override(mock_client, bedrock_unit_test_env, model_id) -> None:
"""Test initialization of Amazon Bedrock Text Completion service"""
bedrock_text_completion = BedrockTextCompletion(model_id=model_id)
assert bedrock_text_completion.ai_model_id == model_id
assert bedrock_text_completion.service_id == model_id
assert bedrock_text_completion.bedrock_client is not None
assert bedrock_text_completion.bedrock_runtime_client is not None
@patch.object(boto3, "client", return_value=Mock())
def test_bedrock_text_completion_init_custom_service_id(mock_client, bedrock_unit_test_env, service_id) -> None:
"""Test initialization of Amazon Bedrock Text Completion service"""
bedrock_text_completion = BedrockTextCompletion(service_id=service_id)
assert bedrock_text_completion.service_id == service_id
assert bedrock_text_completion.bedrock_client is not None
assert bedrock_text_completion.bedrock_runtime_client is not None
def test_bedrock_text_completion_init_custom_clients(bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Text Completion service"""
bedrock_text_completion = BedrockTextCompletion(
runtime_client=MockBedrockRuntimeClient(),
client=MockBedrockClient(),
)
assert isinstance(bedrock_text_completion.bedrock_client, MockBedrockClient)
assert isinstance(bedrock_text_completion.bedrock_runtime_client, MockBedrockRuntimeClient)
@patch.object(boto3, "client", return_value=Mock())
def test_bedrock_text_completion_init_custom_client(mock_client, bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Text Completion service"""
bedrock_text_completion = BedrockTextCompletion(
client=MockBedrockClient(),
)
assert isinstance(bedrock_text_completion.bedrock_client, MockBedrockClient)
assert bedrock_text_completion.bedrock_runtime_client is not None
@patch.object(boto3, "client", return_value=Mock())
def test_bedrock_text_completion_init_custom_runtime_client(mock_client, bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Text Completion service"""
bedrock_text_completion = BedrockTextCompletion(
runtime_client=MockBedrockRuntimeClient(),
)
assert bedrock_text_completion.bedrock_client is not None
assert isinstance(bedrock_text_completion.bedrock_runtime_client, MockBedrockRuntimeClient)
@patch.object(boto3, "client", return_value=Mock())
def test_bedrock_text_completion_init_custom_bedrock_model_provider(mock_client, bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Text Completion service"""
bedrock_text_completion = BedrockTextCompletion(
model_provider=BedrockModelProvider.AMAZON,
)
assert bedrock_text_completion.bedrock_model_provider == BedrockModelProvider.AMAZON
@pytest.mark.parametrize("exclude_list", [["BEDROCK_TEXT_MODEL_ID"]], indirect=True)
def test_bedrock_text_completion_client_init_with_empty_model_id(bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Text Completion service with empty model id"""
with pytest.raises(ServiceInitializationError, match="The Amazon Bedrock Text Model ID is missing."):
BedrockTextCompletion(env_file_path="fake_env_file_path.env")
def test_bedrock_text_completion_client_init_invalid_settings(bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Text Completion service with invalid settings"""
with pytest.raises(
ServiceInitializationError, match="Failed to initialize the Amazon Bedrock Text Completion Service."
):
BedrockTextCompletion(model_id=123) # Model ID must be a string
def test_bedrock_text_completion_client_init_invalid_model_provider(bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Text Completion service with invalid settings"""
with pytest.raises(
ServiceInitializationError, match="Failed to initialize the Amazon Bedrock Text Completion Service."
):
BedrockTextCompletion(model_provider="invalid_provider")
@patch.object(boto3, "client", return_value=Mock())
def test_prompt_execution_settings_class(mock_client, bedrock_unit_test_env) -> None:
"""Test getting prompt execution settings class"""
bedrock_completion_client = BedrockTextCompletion()
assert bedrock_completion_client.get_prompt_execution_settings_class() == BedrockTextPromptExecutionSettings
# endregion
# region text completion
@pytest.mark.parametrize(
# These are fake model ids with the supported prefixes
"model_id",
[
"amazon.titan",
"anthropic.claude",
"cohere.command",
"ai21.jamba",
"meta.llama",
"mistral.ai",
],
indirect=True,
)
async def test_bedrock_text_completion(
model_id,
mock_bedrock_text_completion_response,
output_text,
) -> None:
"""Test Amazon Bedrock Text Completion complete method"""
with patch.object(
MockBedrockRuntimeClient, "invoke_model", return_value=mock_bedrock_text_completion_response
) as mock_model_invoke:
# Setup
bedrock_text_completion = BedrockTextCompletion(
model_id=model_id,
runtime_client=MockBedrockRuntimeClient(),
client=MockBedrockClient(),
)
# Act
settings = BedrockTextPromptExecutionSettings()
response = await bedrock_text_completion.get_text_contents("Hello!", settings=settings)
# Assert
mock_model_invoke.assert_called_once_with(
body=json.dumps(get_text_completion_request_body(model_id, "Hello!", settings)),
modelId=model_id,
accept="application/json",
contentType="application/json",
)
assert isinstance(response, list)
assert len(response) == 1
assert isinstance(response[0], TextContent)
assert response[0].ai_model_id == model_id
assert response[0].text == output_text
@pytest.mark.parametrize(
# These are fake model ids with the supported prefixes
"model_id",
[
"arn:aws:bedrock:us-east-1:972143716085:application-inference-profile/123456",
],
indirect=True,
)
async def test_bedrock_text_completion_with_application_inference_profile(
model_id,
mock_bedrock_text_completion_response,
output_text,
model_provider,
) -> None:
"""Test Amazon Bedrock Text Completion complete method"""
with patch.object(
MockBedrockRuntimeClient,
"invoke_model",
return_value=mock_bedrock_text_completion_response,
) as mock_model_invoke:
# Setup
bedrock_text_completion = BedrockTextCompletion(
model_id=model_id,
runtime_client=MockBedrockRuntimeClient(),
client=MockBedrockClient(),
model_provider=model_provider,
)
# Act
settings = BedrockTextPromptExecutionSettings()
await bedrock_text_completion.get_text_contents("Hello!", settings=settings)
# Assert
mock_model_invoke.assert_called_once_with(
body=json.dumps(get_text_completion_request_body(model_id, "Hello!", settings, model_provider)),
modelId=model_id,
accept="application/json",
contentType="application/json",
)
@pytest.mark.parametrize(
# These are fake model ids with the supported prefixes
"model_id",
[
"amazon.titan",
],
indirect=True,
)
async def test_bedrock_streaming_text_completion(
model_id,
mock_bedrock_streaming_text_completion_response,
output_text,
) -> None:
"""Test Amazon Bedrock Text Completion complete method"""
with patch.object(
MockBedrockRuntimeClient,
"invoke_model_with_response_stream",
return_value=mock_bedrock_streaming_text_completion_response,
) as mock_invoke_model_with_response_stream:
# Setup
bedrock_text_completion = BedrockTextCompletion(
model_id=model_id,
runtime_client=MockBedrockRuntimeClient(),
client=MockBedrockClient(),
)
# Act
settings = BedrockTextPromptExecutionSettings()
chunks: list[StreamingTextContent] = []
async for streaming_responses in bedrock_text_completion.get_streaming_text_contents(
"Hello!", settings=settings
):
chunks.extend(streaming_responses)
response = reduce(lambda p, r: p + r, chunks)
# Assert
mock_invoke_model_with_response_stream.assert_called_once_with(
body=json.dumps(get_text_completion_request_body(model_id, "Hello!", settings)),
modelId=model_id,
accept="application/json",
contentType="application/json",
)
assert isinstance(response, StreamingTextContent)
assert response.ai_model_id == model_id
assert response.text == output_text
assert response.choice_index == 0
assert isinstance(response.inner_content, list)
@pytest.mark.parametrize(
# These are fake model ids with the supported prefixes
"model_id",
[
"arn:aws:bedrock:us-east-1:972143716085:application-inference-profile/123456",
],
indirect=True,
)
async def test_bedrock_streaming_text_completion_with_application_inference_profile(
model_id,
mock_bedrock_streaming_text_completion_response,
output_text,
model_provider,
) -> None:
"""Test Amazon Bedrock Chat Completion complete method"""
with patch.object(
MockBedrockRuntimeClient,
"invoke_model_with_response_stream",
return_value=mock_bedrock_streaming_text_completion_response,
) as mock_invoke_model_with_response_stream:
# Setup
bedrock_text_completion = BedrockTextCompletion(
model_id=model_id,
runtime_client=MockBedrockRuntimeClient(),
client=MockBedrockClient(),
model_provider=model_provider,
)
# Act
settings = BedrockTextPromptExecutionSettings()
chunks: list[StreamingTextContent] = []
async for streaming_responses in bedrock_text_completion.get_streaming_text_contents(
"Hello!", settings=settings
):
chunks.extend(streaming_responses)
# Assert
mock_invoke_model_with_response_stream.assert_called_once_with(
body=json.dumps(get_text_completion_request_body(model_id, "Hello!", settings, model_provider)),
modelId=model_id,
accept="application/json",
contentType="application/json",
)
# endregion
@@ -0,0 +1,235 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import ANY, Mock, patch
import boto3
import pytest
from semantic_kernel.connectors.ai.bedrock.bedrock_prompt_execution_settings import (
BedrockEmbeddingPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.bedrock.services.bedrock_text_embedding import BedrockTextEmbedding
from semantic_kernel.connectors.ai.bedrock.services.model_provider.bedrock_model_provider import BedrockModelProvider
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError, ServiceInvalidResponseError
from tests.unit.connectors.ai.bedrock.conftest import MockBedrockClient, MockBedrockRuntimeClient
# region init
@patch.object(boto3, "client", return_value=Mock())
def test_bedrock_text_embedding_init(mock_client, bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Text Embedding service"""
bedrock_text_embedding = BedrockTextEmbedding()
assert bedrock_text_embedding.ai_model_id == bedrock_unit_test_env["BEDROCK_EMBEDDING_MODEL_ID"]
assert bedrock_text_embedding.service_id == bedrock_unit_test_env["BEDROCK_EMBEDDING_MODEL_ID"]
assert bedrock_text_embedding.bedrock_model_provider == BedrockModelProvider(
bedrock_unit_test_env["BEDROCK_MODEL_PROVIDER"]
)
assert bedrock_text_embedding.bedrock_client is not None
assert bedrock_text_embedding.bedrock_runtime_client is not None
@patch.object(boto3, "client", return_value=Mock())
def test_bedrock_text_embedding_init_model_id_override(mock_client, bedrock_unit_test_env, model_id) -> None:
"""Test initialization of Amazon Bedrock Text Embedding service"""
bedrock_text_embedding = BedrockTextEmbedding(model_id=model_id)
assert bedrock_text_embedding.ai_model_id == model_id
assert bedrock_text_embedding.service_id == model_id
assert bedrock_text_embedding.bedrock_client is not None
assert bedrock_text_embedding.bedrock_runtime_client is not None
@patch.object(boto3, "client", return_value=Mock())
def test_bedrock_text_embedding_init_custom_service_id(mock_client, bedrock_unit_test_env, service_id) -> None:
"""Test initialization of Amazon Bedrock Text Embedding service"""
bedrock_text_embedding = BedrockTextEmbedding(service_id=service_id)
assert bedrock_text_embedding.service_id == service_id
assert bedrock_text_embedding.bedrock_client is not None
assert bedrock_text_embedding.bedrock_runtime_client is not None
def test_bedrock_text_embedding_init_custom_clients(bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Text Embedding service"""
bedrock_text_embedding = BedrockTextEmbedding(
runtime_client=MockBedrockRuntimeClient(),
client=MockBedrockClient(),
)
assert isinstance(bedrock_text_embedding.bedrock_client, MockBedrockClient)
assert isinstance(bedrock_text_embedding.bedrock_runtime_client, MockBedrockRuntimeClient)
@patch.object(boto3, "client", return_value=Mock())
def test_bedrock_text_embedding_init_custom_client(mock_client, bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Text Embedding service"""
bedrock_text_embedding = BedrockTextEmbedding(
client=MockBedrockClient(),
)
assert isinstance(bedrock_text_embedding.bedrock_client, MockBedrockClient)
assert bedrock_text_embedding.bedrock_runtime_client is not None
@patch.object(boto3, "client", return_value=Mock())
def test_bedrock_text_embedding_init_custom_runtime_client(mock_client, bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Text Embedding service"""
bedrock_text_embedding = BedrockTextEmbedding(
runtime_client=MockBedrockRuntimeClient(),
)
assert bedrock_text_embedding.bedrock_client is not None
assert isinstance(bedrock_text_embedding.bedrock_runtime_client, MockBedrockRuntimeClient)
@patch.object(boto3, "client", return_value=Mock())
def test_bedrock_text_embedding_init_custom_bedrock_model_provider(mock_client, bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Text Embedding service"""
bedrock_text_embedding = BedrockTextEmbedding(
model_provider=BedrockModelProvider.AMAZON,
)
assert bedrock_text_embedding.bedrock_model_provider == BedrockModelProvider.AMAZON
@pytest.mark.parametrize("exclude_list", [["BEDROCK_EMBEDDING_MODEL_ID"]], indirect=True)
def test_bedrock_text_embedding_client_init_with_empty_model_id(bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Text Embedding service with empty model id"""
with pytest.raises(ServiceInitializationError, match="The Amazon Bedrock Text Embedding Model ID is missing."):
BedrockTextEmbedding(env_file_path="fake_env_file_path.env")
def test_bedrock_text_embedding_client_init_invalid_settings(bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Text Embedding service with invalid settings"""
with pytest.raises(
ServiceInitializationError, match="Failed to initialize the Amazon Bedrock Text Embedding Service."
):
BedrockTextEmbedding(model_id=123) # Model ID must be a string
def test_bedrock_text_embedding_client_init_invalid_model_provider(bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Text Embedding service with invalid settings"""
with pytest.raises(
ServiceInitializationError, match="Failed to initialize the Amazon Bedrock Text Embedding Service."
):
BedrockTextEmbedding(model_provider="invalid_provider")
@patch.object(boto3, "client", return_value=Mock())
def test_prompt_execution_settings_class(mock_client, bedrock_unit_test_env) -> None:
"""Test getting prompt execution settings class"""
bedrock_completion_client = BedrockTextEmbedding()
assert bedrock_completion_client.get_prompt_execution_settings_class() == BedrockEmbeddingPromptExecutionSettings
# endregion
@pytest.mark.parametrize(
# These are fake model ids with the supported prefixes
"model_id",
[
"amazon.titan",
"cohere.command",
],
indirect=True,
)
async def test_bedrock_text_embedding(model_id, mock_bedrock_text_embedding_response) -> None:
"""Test Bedrock text embedding generation"""
with patch.object(
MockBedrockRuntimeClient, "invoke_model", return_value=mock_bedrock_text_embedding_response
) as mock_model_invoke:
# Setup
bedrock_text_embedding = BedrockTextEmbedding(
model_id=model_id,
runtime_client=MockBedrockRuntimeClient(),
client=MockBedrockClient(),
)
# Act
settings = BedrockEmbeddingPromptExecutionSettings()
response = await bedrock_text_embedding.generate_embeddings(["hello", "world"], settings)
# Assert
mock_model_invoke.assert_called_with(
body=ANY,
modelId=model_id,
accept="application/json",
contentType="application/json",
)
assert mock_model_invoke.call_count == 2
assert len(response) == 2
@pytest.mark.parametrize(
# These are fake model ids with the supported prefixes
"model_id",
[
"arn:aws:bedrock:us-east-1:972143716085:application-inference-profile/123456",
],
indirect=True,
)
async def test_bedrock_text_embedding_with_application_inference_profile(
model_id,
mock_bedrock_text_embedding_response,
model_provider,
) -> None:
"""Test Bedrock text embedding generation"""
with patch.object(
MockBedrockRuntimeClient, "invoke_model", return_value=mock_bedrock_text_embedding_response
) as mock_model_invoke:
# Setup
bedrock_text_embedding = BedrockTextEmbedding(
model_id=model_id,
runtime_client=MockBedrockRuntimeClient(),
client=MockBedrockClient(),
model_provider=BedrockModelProvider.AMAZON,
)
# Act
settings = BedrockEmbeddingPromptExecutionSettings()
response = await bedrock_text_embedding.generate_embeddings(["hello", "world"], settings)
# Assert
mock_model_invoke.assert_called_with(
body=ANY,
modelId=model_id,
accept="application/json",
contentType="application/json",
)
assert mock_model_invoke.call_count == 2
assert len(response) == 2
@pytest.mark.parametrize(
# These are fake model ids with the supported prefixes
"model_id",
[
"amazon.titan",
"cohere.command",
],
indirect=True,
)
async def test_bedrock_text_embedding_with_invalid_response(
model_id, mock_bedrock_text_embedding_invalid_response
) -> None:
"""Test Bedrock text embedding generation with invalid response"""
with patch.object(
MockBedrockRuntimeClient, "invoke_model", return_value=mock_bedrock_text_embedding_invalid_response
):
# Setup
bedrock_text_embedding = BedrockTextEmbedding(
model_id=model_id,
runtime_client=MockBedrockRuntimeClient(),
client=MockBedrockClient(),
)
with pytest.raises(ServiceInvalidResponseError):
await bedrock_text_embedding.generate_embeddings(["hello", "world"])
@@ -0,0 +1,104 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.connectors.ai.bedrock import BedrockChatPromptExecutionSettings, BedrockPromptExecutionSettings
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
def test_default_bedrock_prompt_execution_settings():
settings = BedrockPromptExecutionSettings()
assert settings.temperature is None
assert settings.top_p is None
assert settings.top_k is None
assert settings.max_tokens is None
assert settings.stop == []
def test_custom_bedrock_prompt_execution_settings():
settings = BedrockPromptExecutionSettings(
temperature=0.5,
top_p=0.5,
top_k=10,
max_tokens=128,
stop=["world"],
)
assert settings.temperature == 0.5
assert settings.top_p == 0.5
assert settings.top_k == 10
assert settings.max_tokens == 128
assert settings.stop == ["world"]
def test_bedrock_prompt_execution_settings_from_default_completion_config():
settings = PromptExecutionSettings(service_id="test_service")
chat_settings = BedrockChatPromptExecutionSettings.from_prompt_execution_settings(settings)
assert chat_settings.service_id == "test_service"
assert chat_settings.temperature is None
assert chat_settings.top_p is None
assert chat_settings.top_k is None
assert chat_settings.max_tokens is None
assert chat_settings.stop == []
def test_bedrock_prompt_execution_settings_from_openai_prompt_execution_settings():
chat_settings = BedrockChatPromptExecutionSettings(service_id="test_service", temperature=1.0)
new_settings = BedrockPromptExecutionSettings(service_id="test_2", temperature=0.0)
chat_settings.update_from_prompt_execution_settings(new_settings)
assert chat_settings.service_id == "test_2"
assert chat_settings.temperature == 0.0
def test_bedrock_prompt_execution_settings_from_custom_completion_config():
settings = PromptExecutionSettings(
service_id="test_service",
extension_data={
"temperature": 0.5,
"top_p": 0.5,
"top_k": 10,
"max_tokens": 128,
"stop": ["world"],
},
)
chat_settings = BedrockChatPromptExecutionSettings.from_prompt_execution_settings(settings)
assert chat_settings.temperature == 0.5
assert chat_settings.top_p == 0.5
assert chat_settings.top_k == 10
assert chat_settings.max_tokens == 128
assert chat_settings.stop == ["world"]
def test_bedrock_chat_prompt_execution_settings_from_custom_completion_config_with_functions():
settings = PromptExecutionSettings(
service_id="test_service",
extension_data={
"tools": [{"function": {}}],
},
)
chat_settings = BedrockChatPromptExecutionSettings.from_prompt_execution_settings(settings)
assert chat_settings.tools == [{"function": {}}]
def test_create_options():
settings = BedrockPromptExecutionSettings(
service_id="test_service",
extension_data={
"temperature": 0.5,
"top_p": 0.5,
"top_k": 10,
"max_tokens": 128,
"stop": ["world"],
},
)
options = settings.prepare_settings_dict()
assert options["temperature"] == 0.5
assert options["top_p"] == 0.5
assert options["top_k"] == 10
assert options["max_tokens"] == 128
assert options["stop"] == ["world"]
@@ -0,0 +1,23 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from semantic_kernel.contents.chat_history import ChatHistory
@pytest.fixture()
def service_id() -> str:
return "test_service_id"
@pytest.fixture()
def chat_history() -> ChatHistory:
chat_history = ChatHistory()
chat_history.add_system_message("system_prompt")
chat_history.add_user_message("test_prompt")
return chat_history
@pytest.fixture()
def prompt() -> str:
return "test_prompt"
@@ -0,0 +1,174 @@
# Copyright (c) Microsoft. All rights reserved.
from collections.abc import AsyncGenerator, AsyncIterator
from unittest.mock import MagicMock
import pytest
import pytest_asyncio
from google.genai.types import (
Candidate,
Content,
FinishReason,
GenerateContentResponse,
GenerateContentResponseUsageMetadata,
Part,
)
@pytest.fixture()
def google_ai_unit_test_env(monkeypatch, exclude_list, override_env_param_dict):
"""Fixture to set environment variables for Google AI Unit Tests."""
if exclude_list is None:
exclude_list = []
if override_env_param_dict is None:
override_env_param_dict = {}
env_vars = {
"GOOGLE_AI_GEMINI_MODEL_ID": "test-gemini-model-id",
"GOOGLE_AI_EMBEDDING_MODEL_ID": "test-embedding-model-id",
"GOOGLE_AI_API_KEY": "test-api-key",
"GOOGLE_AI_CLOUD_PROJECT_ID": "test-project-id",
"GOOGLE_AI_CLOUD_REGION": "test-region",
}
env_vars.update(override_env_param_dict)
for key, value in env_vars.items():
if key not in exclude_list:
monkeypatch.setenv(key, value)
else:
monkeypatch.delenv(key, raising=False)
return env_vars
@pytest.fixture()
def mock_google_ai_chat_completion_response() -> GenerateContentResponse:
"""Mock Google AI Chat Completion response."""
candidate = Candidate()
candidate.index = 0
candidate.content = Content(role="user", parts=[Part.from_text(text="Test content")])
candidate.finish_reason = FinishReason.STOP
response = GenerateContentResponse()
response.candidates = [candidate]
response.usage_metadata = GenerateContentResponseUsageMetadata(
prompt_token_count=0,
cached_content_token_count=0,
candidates_token_count=0,
total_token_count=0,
)
return response
@pytest.fixture()
def mock_google_ai_chat_completion_response_with_tool_call() -> GenerateContentResponse:
"""Mock Google AI Chat Completion response."""
candidate = Candidate()
candidate.index = 0
candidate.content = Content(
role="user",
parts=[
Part.from_function_call(
name="test_function",
args={"test_arg": "test_value"},
)
],
)
candidate.finish_reason = FinishReason.STOP
response = GenerateContentResponse()
response.candidates = [candidate]
response.usage_metadata = GenerateContentResponseUsageMetadata(
prompt_token_count=0,
cached_content_token_count=0,
candidates_token_count=0,
total_token_count=0,
)
return response
@pytest_asyncio.fixture
async def mock_google_ai_streaming_chat_completion_response() -> AsyncIterator[GenerateContentResponse]:
"""Mock Google AI streaming Chat Completion response."""
candidate = Candidate()
candidate.index = 0
candidate.content = Content(role="user", parts=[Part.from_text(text="Test content")])
candidate.finish_reason = FinishReason.STOP
response = GenerateContentResponse()
response.candidates = [candidate]
response.usage_metadata = GenerateContentResponseUsageMetadata(
prompt_token_count=0,
cached_content_token_count=0,
candidates_token_count=0,
total_token_count=0,
)
iterable = MagicMock(spec=AsyncGenerator)
iterable.__aiter__.return_value = [response]
return iterable
@pytest_asyncio.fixture
async def mock_google_ai_streaming_chat_completion_response_with_tool_call() -> AsyncIterator[GenerateContentResponse]:
"""Mock Google AI streaming Chat Completion response with tool call."""
candidate = Candidate()
candidate.index = 0
candidate.content = Content(
role="user",
parts=[
Part.from_function_call(
name="getLightStatus",
args={"arg1": "test_value"},
)
],
)
candidate.finish_reason = FinishReason.STOP
response = GenerateContentResponse()
response.candidates = [candidate]
response.usage_metadata = GenerateContentResponseUsageMetadata(
prompt_token_count=0,
cached_content_token_count=0,
candidates_token_count=0,
total_token_count=0,
)
iterable = MagicMock(spec=AsyncGenerator)
iterable.__aiter__.return_value = [response]
return iterable
@pytest.fixture()
def mock_google_ai_text_completion_response() -> GenerateContentResponse:
"""Mock Google AI Text Completion response."""
candidate = Candidate()
candidate.index = 0
candidate.content = Content(parts=[Part.from_text(text="Test content")])
response = GenerateContentResponse()
response.candidates = [candidate]
return response
@pytest_asyncio.fixture
async def mock_google_ai_streaming_text_completion_response() -> AsyncIterator[GenerateContentResponse]:
"""Mock Google AI streaming Text Completion response."""
candidate = Candidate()
candidate.index = 0
candidate.content = Content(parts=[Part.from_text(text="Test content")])
response = GenerateContentResponse()
response.candidates = [candidate]
iterable = MagicMock(spec=AsyncGenerator)
iterable.__aiter__.return_value = [response]
return iterable
@@ -0,0 +1,650 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import AsyncMock, patch
import pytest
from google.genai import Client
from google.genai.models import AsyncModels
from google.genai.types import Content, GenerateContentConfigDict
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
from semantic_kernel.connectors.ai.google.google_ai.google_ai_prompt_execution_settings import (
GoogleAIChatPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.google.google_ai.google_ai_settings import GoogleAISettings
from semantic_kernel.connectors.ai.google.google_ai.services.google_ai_chat_completion import GoogleAIChatCompletion
from semantic_kernel.connectors.ai.google.shared_utils import filter_system_message
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.utils.finish_reason import FinishReason
from semantic_kernel.exceptions.service_exceptions import (
ServiceInitializationError,
ServiceInvalidExecutionSettingsError,
)
from semantic_kernel.kernel import Kernel
# region init
def test_google_ai_chat_completion_init(google_ai_unit_test_env) -> None:
"""Test initialization of GoogleAIChatCompletion"""
model_id = google_ai_unit_test_env["GOOGLE_AI_GEMINI_MODEL_ID"]
api_key = google_ai_unit_test_env["GOOGLE_AI_API_KEY"]
google_ai_chat_completion = GoogleAIChatCompletion()
assert google_ai_chat_completion.ai_model_id == model_id
assert google_ai_chat_completion.service_id == model_id
assert isinstance(google_ai_chat_completion.service_settings, GoogleAISettings)
assert google_ai_chat_completion.service_settings.gemini_model_id == model_id
assert google_ai_chat_completion.service_settings.api_key.get_secret_value() == api_key
def test_google_ai_chat_completion_init_with_service_id(google_ai_unit_test_env, service_id) -> None:
"""Test initialization of GoogleAIChatCompletion with a service_id that is not the model_id"""
google_ai_chat_completion = GoogleAIChatCompletion(service_id=service_id)
assert google_ai_chat_completion.service_id == service_id
def test_google_ai_chat_completion_init_with_model_id_in_argument(google_ai_unit_test_env) -> None:
"""Test initialization of GoogleAIChatCompletion with model_id in argument"""
google_ai_chat_completion = GoogleAIChatCompletion(gemini_model_id="custom_model_id")
assert google_ai_chat_completion.ai_model_id == "custom_model_id"
assert google_ai_chat_completion.service_id == "custom_model_id"
@pytest.mark.parametrize("exclude_list", [["GOOGLE_AI_GEMINI_MODEL_ID"]], indirect=True)
def test_google_ai_chat_completion_init_with_empty_model_id(google_ai_unit_test_env) -> None:
"""Test initialization of GoogleAIChatCompletion with an empty model_id"""
with pytest.raises(ServiceInitializationError, match="The Google AI Gemini model ID is required."):
GoogleAIChatCompletion(env_file_path="fake_env_file_path.env")
@pytest.mark.parametrize("exclude_list", [["GOOGLE_AI_API_KEY"]], indirect=True)
def test_google_ai_chat_completion_init_with_empty_api_key(google_ai_unit_test_env) -> None:
"""Test initialization of GoogleAIChatCompletion with an empty api_key"""
with pytest.raises(ServiceInitializationError, match="API key is required when use_vertexai is False."):
GoogleAIChatCompletion(env_file_path="fake_env_file_path.env")
@pytest.mark.parametrize("exclude_list", [["GOOGLE_AI_CLOUD_PROJECT_ID"]], indirect=True)
def test_google_ai_chat_completion_init_with_use_vertexai_missing_project_id(google_ai_unit_test_env) -> None:
"""Test initialization of GoogleAIChatCompletion with use_vertexai true but missing project_id"""
with pytest.raises(ServiceInitializationError, match="Project ID must be provided when use_vertexai is True."):
GoogleAIChatCompletion(use_vertexai=True, env_file_path="fake_env_file_path.env")
@pytest.mark.parametrize("exclude_list", [["GOOGLE_AI_CLOUD_REGION"]], indirect=True)
def test_google_ai_chat_completion_init_with_use_vertexai_missing_region(google_ai_unit_test_env) -> None:
"""Test initialization of GoogleAIChatCompletion with use_vertexai true but missing region"""
with pytest.raises(ServiceInitializationError, match="Region must be provided when use_vertexai is True."):
GoogleAIChatCompletion(use_vertexai=True, env_file_path="fake_env_file_path.env")
@pytest.mark.parametrize("exclude_list", [["GOOGLE_AI_API_KEY"]], indirect=True)
def test_google_ai_chat_completion_init_with_use_vertexai_no_api_key(google_ai_unit_test_env) -> None:
"""Test initialization of GoogleAIChatCompletion succeeds with use_vertexai=True and no api_key"""
chat_completion = GoogleAIChatCompletion(use_vertexai=True)
assert chat_completion.service_settings.use_vertexai is True
def test_prompt_execution_settings_class(google_ai_unit_test_env) -> None:
google_ai_chat_completion = GoogleAIChatCompletion()
assert google_ai_chat_completion.get_prompt_execution_settings_class() == GoogleAIChatPromptExecutionSettings
# endregion init
# region chat completion
@patch.object(AsyncModels, "generate_content", new_callable=AsyncMock)
async def test_google_ai_chat_completion(
mock_google_ai_model_generate_content,
google_ai_unit_test_env,
chat_history: ChatHistory,
mock_google_ai_chat_completion_response,
) -> None:
"""Test chat completion with GoogleAIChatCompletion"""
settings = GoogleAIChatPromptExecutionSettings(top_k=5, temperature=0.7)
mock_google_ai_model_generate_content.return_value = mock_google_ai_chat_completion_response
google_ai_chat_completion = GoogleAIChatCompletion()
responses: list[ChatMessageContent] = await google_ai_chat_completion.get_chat_message_contents(
chat_history, settings
)
mock_google_ai_model_generate_content.assert_called_once_with(
model=google_ai_chat_completion.service_settings.gemini_model_id,
contents=google_ai_chat_completion._prepare_chat_history_for_request(chat_history),
config=GenerateContentConfigDict(
**settings.prepare_settings_dict(),
system_instruction=filter_system_message(chat_history),
),
)
assert len(responses) == 1
assert responses[0].role == "assistant"
assert responses[0].content == mock_google_ai_chat_completion_response.candidates[0].content.parts[0].text
assert responses[0].finish_reason == FinishReason.STOP
assert "usage" in responses[0].metadata
assert "prompt_feedback" in responses[0].metadata
assert responses[0].inner_content == mock_google_ai_chat_completion_response
async def test_google_ai_chat_completion_with_custom_client(
chat_history: ChatHistory,
google_ai_unit_test_env,
mock_google_ai_chat_completion_response,
) -> None:
"""Test chat completion with GoogleAIChatCompletion using a custom client"""
# Create a custom client with a fake API key for testing
custom_client = Client(api_key="fake-api-key-for-testing")
# Mock the custom client's generate_content method
mock_generate_content = AsyncMock(return_value=mock_google_ai_chat_completion_response)
custom_client.aio.models.generate_content = mock_generate_content
google_ai_chat_completion = GoogleAIChatCompletion(client=custom_client)
responses: list[ChatMessageContent] = await google_ai_chat_completion.get_chat_message_contents(
chat_history,
GoogleAIChatPromptExecutionSettings(),
)
custom_client.close()
# Verify that the custom client was used and returned the expected response
assert len(responses) == 1
assert responses[0].role == "assistant"
assert responses[0].content == mock_google_ai_chat_completion_response.candidates[0].content.parts[0].text
assert responses[0].finish_reason == FinishReason.STOP
# Verify that the custom client's method was called
mock_generate_content.assert_called_once()
async def test_google_ai_chat_completion_with_function_choice_behavior_fail_verification(
chat_history: ChatHistory,
google_ai_unit_test_env,
) -> None:
"""Test completion of GoogleAIChatCompletion with function choice behavior expect verification failure"""
# Missing kernel
with pytest.raises(ServiceInvalidExecutionSettingsError):
settings = GoogleAIChatPromptExecutionSettings(
function_choice_behavior=FunctionChoiceBehavior.Auto(),
)
google_ai_chat_completion = GoogleAIChatCompletion()
await google_ai_chat_completion.get_chat_message_contents(
chat_history=chat_history,
settings=settings,
)
@patch.object(AsyncModels, "generate_content", new_callable=AsyncMock)
async def test_google_ai_chat_completion_with_function_choice_behavior(
mock_google_ai_model_generate_content,
google_ai_unit_test_env,
kernel,
chat_history: ChatHistory,
mock_google_ai_chat_completion_response_with_tool_call,
) -> None:
"""Test completion of GoogleAIChatCompletion with function choice behavior"""
mock_google_ai_model_generate_content.return_value = mock_google_ai_chat_completion_response_with_tool_call
settings = GoogleAIChatPromptExecutionSettings(
function_choice_behavior=FunctionChoiceBehavior.Auto(),
)
settings.function_choice_behavior.maximum_auto_invoke_attempts = 1
google_ai_chat_completion = GoogleAIChatCompletion()
responses = await google_ai_chat_completion.get_chat_message_contents(
chat_history=chat_history,
settings=settings,
kernel=kernel,
)
# The function should be called twice:
# One for the tool call and one for the last completion
# after the maximum_auto_invoke_attempts is reached
assert mock_google_ai_model_generate_content.call_count == 2
assert len(responses) == 1
assert responses[0].role == "assistant"
# Google doesn't return STOP as the finish reason for tool calls
assert responses[0].finish_reason == FinishReason.STOP
@patch.object(AsyncModels, "generate_content", new_callable=AsyncMock)
async def test_google_ai_chat_completion_with_function_choice_behavior_no_tool_call(
mock_google_ai_model_generate_content,
google_ai_unit_test_env,
kernel,
chat_history: ChatHistory,
mock_google_ai_chat_completion_response,
) -> None:
"""Test completion of GoogleAIChatCompletion with function choice behavior but no tool call returned"""
mock_google_ai_model_generate_content.return_value = mock_google_ai_chat_completion_response
settings = GoogleAIChatPromptExecutionSettings(
function_choice_behavior=FunctionChoiceBehavior.Auto(),
)
settings.function_choice_behavior.maximum_auto_invoke_attempts = 1
google_ai_chat_completion = GoogleAIChatCompletion()
responses = await google_ai_chat_completion.get_chat_message_contents(
chat_history=chat_history,
settings=settings,
kernel=kernel,
)
mock_google_ai_model_generate_content.assert_awaited_once_with(
model=google_ai_chat_completion.service_settings.gemini_model_id,
contents=google_ai_chat_completion._prepare_chat_history_for_request(chat_history),
config=GenerateContentConfigDict(
**settings.prepare_settings_dict(),
system_instruction=filter_system_message(chat_history),
),
)
assert len(responses) == 1
assert responses[0].role == "assistant"
assert responses[0].content == mock_google_ai_chat_completion_response.candidates[0].content.parts[0].text
# endregion chat completion
# region streaming chat completion
@patch.object(AsyncModels, "generate_content_stream", new_callable=AsyncMock)
async def test_google_ai_streaming_chat_completion(
mock_google_ai_model_generate_content_stream,
google_ai_unit_test_env,
chat_history: ChatHistory,
mock_google_ai_streaming_chat_completion_response,
) -> None:
"""Test streaming chat completion with GoogleAIChatCompletion"""
settings = GoogleAIChatPromptExecutionSettings()
mock_google_ai_model_generate_content_stream.return_value = mock_google_ai_streaming_chat_completion_response
google_ai_chat_completion = GoogleAIChatCompletion()
async for messages in google_ai_chat_completion.get_streaming_chat_message_contents(chat_history, settings):
assert len(messages) == 1
assert messages[0].role == "assistant"
assert messages[0].finish_reason == FinishReason.STOP
assert "usage" in messages[0].metadata
assert "prompt_feedback" in messages[0].metadata
mock_google_ai_model_generate_content_stream.assert_called_once_with(
model=google_ai_chat_completion.service_settings.gemini_model_id,
contents=google_ai_chat_completion._prepare_chat_history_for_request(chat_history),
config=GenerateContentConfigDict(
**settings.prepare_settings_dict(),
system_instruction=filter_system_message(chat_history),
),
)
async def test_google_ai_streaming_chat_completion_with_custom_client(
chat_history: ChatHistory,
google_ai_unit_test_env,
mock_google_ai_streaming_chat_completion_response,
) -> None:
"""Test streaming chat completion with GoogleAIChatCompletion using a custom client"""
# Create a custom client with a fake API key for testing
custom_client = Client(api_key="fake-api-key-for-testing")
# Mock the custom client's generate_content_stream method
mock_generate_content_stream = AsyncMock(return_value=mock_google_ai_streaming_chat_completion_response)
custom_client.aio.models.generate_content_stream = mock_generate_content_stream
google_ai_chat_completion = GoogleAIChatCompletion(client=custom_client)
all_messages = []
async for messages in google_ai_chat_completion.get_streaming_chat_message_contents(
chat_history, GoogleAIChatPromptExecutionSettings()
):
all_messages.extend(messages)
assert len(messages) == 1
assert messages[0].role == "assistant"
assert messages[0].finish_reason == FinishReason.STOP
assert "usage" in messages[0].metadata
assert "prompt_feedback" in messages[0].metadata
custom_client.close()
# Verify that the custom client was used and returned the expected response
assert len(all_messages) > 0
# Verify that the custom client's method was called
mock_generate_content_stream.assert_called_once()
async def test_google_ai_streaming_chat_completion_with_function_choice_behavior_fail_verification(
chat_history: ChatHistory,
google_ai_unit_test_env,
) -> None:
"""Test streaming chat completion of GoogleAIChatCompletion with function choice
behavior expect verification failure"""
# Missing kernel
with pytest.raises(ServiceInvalidExecutionSettingsError):
settings = GoogleAIChatPromptExecutionSettings(
function_choice_behavior=FunctionChoiceBehavior.Auto(),
)
google_ai_chat_completion = GoogleAIChatCompletion()
async for _ in google_ai_chat_completion.get_streaming_chat_message_contents(
chat_history=chat_history,
settings=settings,
):
pass
@patch.object(AsyncModels, "generate_content_stream", new_callable=AsyncMock)
async def test_google_ai_streaming_chat_completion_with_function_choice_behavior(
mock_google_ai_model_generate_content_stream,
google_ai_unit_test_env,
kernel: Kernel,
chat_history: ChatHistory,
mock_google_ai_streaming_chat_completion_response_with_tool_call,
decorated_native_function,
) -> None:
"""Test streaming chat completion of GoogleAIChatCompletion with function choice behavior"""
mock_google_ai_model_generate_content_stream.return_value = (
mock_google_ai_streaming_chat_completion_response_with_tool_call
)
settings = GoogleAIChatPromptExecutionSettings(
function_choice_behavior=FunctionChoiceBehavior.Auto(),
)
settings.function_choice_behavior.maximum_auto_invoke_attempts = 1
google_ai_chat_completion = GoogleAIChatCompletion()
kernel.add_function(plugin_name="TestPlugin", function=decorated_native_function)
all_messages = []
async for messages in google_ai_chat_completion.get_streaming_chat_message_contents(
chat_history,
settings,
kernel=kernel,
):
all_messages.extend(messages)
assert len(all_messages) == 2, f"Expected 2 messages, got {len(all_messages)}"
# Validate the first message
assert all_messages[0].role == "assistant", f"Unexpected role for first message: {all_messages[0].role}"
assert all_messages[0].content == "", f"Unexpected content for first message: {all_messages[0].content}"
assert all_messages[0].finish_reason == FinishReason.STOP, (
f"Unexpected finish reason for first message: {all_messages[0].finish_reason}"
)
# Validate the second message
assert all_messages[1].role == "tool", f"Unexpected role for second message: {all_messages[1].role}"
assert all_messages[1].content == "", f"Unexpected content for second message: {all_messages[1].content}"
assert all_messages[1].finish_reason is None
@patch.object(AsyncModels, "generate_content_stream", new_callable=AsyncMock)
async def test_google_ai_streaming_chat_completion_with_function_choice_behavior_no_tool_call(
mock_google_ai_model_generate_content_stream,
google_ai_unit_test_env,
kernel,
chat_history: ChatHistory,
mock_google_ai_streaming_chat_completion_response,
) -> None:
"""Test completion of GoogleAIChatCompletion with function choice behavior but no tool call returned"""
mock_google_ai_model_generate_content_stream.return_value = mock_google_ai_streaming_chat_completion_response
settings = GoogleAIChatPromptExecutionSettings(
function_choice_behavior=FunctionChoiceBehavior.Auto(),
)
settings.function_choice_behavior.maximum_auto_invoke_attempts = 1
google_ai_chat_completion = GoogleAIChatCompletion()
async for messages in google_ai_chat_completion.get_streaming_chat_message_contents(
chat_history=chat_history,
settings=settings,
kernel=kernel,
):
assert len(messages) == 1
assert messages[0].role == "assistant"
assert messages[0].content == "Test content"
mock_google_ai_model_generate_content_stream.assert_awaited_once_with(
model=google_ai_chat_completion.service_settings.gemini_model_id,
contents=google_ai_chat_completion._prepare_chat_history_for_request(chat_history),
config=GenerateContentConfigDict(
**settings.prepare_settings_dict(),
system_instruction=filter_system_message(chat_history),
),
)
# endregion streaming chat completion
def test_google_ai_chat_completion_parse_chat_history_correctly(google_ai_unit_test_env) -> None:
"""Test _prepare_chat_history_for_request method"""
google_ai_chat_completion = GoogleAIChatCompletion()
chat_history = ChatHistory()
chat_history.add_system_message("test_system_message")
chat_history.add_user_message("test_user_message")
chat_history.add_assistant_message("test_assistant_message")
parsed_chat_history = google_ai_chat_completion._prepare_chat_history_for_request(chat_history)
assert isinstance(parsed_chat_history, list)
# System message should be ignored
assert len(parsed_chat_history) == 2
assert all(isinstance(message, Content) for message in parsed_chat_history)
assert parsed_chat_history[0].role == "user"
assert parsed_chat_history[0].parts[0].text == "test_user_message"
assert parsed_chat_history[1].role == "model"
assert parsed_chat_history[1].parts[0].text == "test_assistant_message"
# region deserialization (Part → FunctionCallContent round-trip)
def test_create_chat_message_content_with_thought_signature(google_ai_unit_test_env) -> None:
"""Test that thought_signature from a Part is deserialized into FunctionCallContent.metadata."""
from google.genai.types import (
Candidate,
Content,
GenerateContentResponse,
GenerateContentResponseUsageMetadata,
Part,
)
from google.genai.types import (
FinishReason as GFinishReason,
)
from semantic_kernel.contents.function_call_content import FunctionCallContent
thought_sig_value = b"test-thought-sig-bytes"
part = Part.from_function_call(name="test_function", args={"key": "value"})
part.thought_signature = thought_sig_value
candidate = Candidate()
candidate.index = 0
candidate.content = Content(role="user", parts=[part])
candidate.finish_reason = GFinishReason.STOP
response = GenerateContentResponse()
response.candidates = [candidate]
response.usage_metadata = GenerateContentResponseUsageMetadata(
prompt_token_count=0, cached_content_token_count=0, candidates_token_count=0, total_token_count=0
)
completion = GoogleAIChatCompletion()
result = completion._create_chat_message_content(response, candidate)
fc_items = [item for item in result.items if isinstance(item, FunctionCallContent)]
assert len(fc_items) == 1
assert fc_items[0].metadata is not None
assert fc_items[0].metadata["thought_signature"] == thought_sig_value
def test_create_chat_message_content_without_thought_signature(google_ai_unit_test_env) -> None:
"""Test that FunctionCallContent works when Part has no thought_signature."""
from google.genai.types import (
Candidate,
Content,
GenerateContentResponse,
GenerateContentResponseUsageMetadata,
Part,
)
from google.genai.types import (
FinishReason as GFinishReason,
)
from semantic_kernel.contents.function_call_content import FunctionCallContent
part = Part.from_function_call(name="test_function", args={"key": "value"})
candidate = Candidate()
candidate.index = 0
candidate.content = Content(role="user", parts=[part])
candidate.finish_reason = GFinishReason.STOP
response = GenerateContentResponse()
response.candidates = [candidate]
response.usage_metadata = GenerateContentResponseUsageMetadata(
prompt_token_count=0, cached_content_token_count=0, candidates_token_count=0, total_token_count=0
)
completion = GoogleAIChatCompletion()
result = completion._create_chat_message_content(response, candidate)
fc_items = [item for item in result.items if isinstance(item, FunctionCallContent)]
assert len(fc_items) == 1
assert "thought_signature" not in fc_items[0].metadata
def test_create_streaming_chat_message_content_with_thought_signature(google_ai_unit_test_env) -> None:
"""Test that thought_signature from a Part is deserialized in streaming path."""
from google.genai.types import (
Candidate,
Content,
GenerateContentResponse,
GenerateContentResponseUsageMetadata,
Part,
)
from google.genai.types import (
FinishReason as GFinishReason,
)
from semantic_kernel.contents.function_call_content import FunctionCallContent
thought_sig_value = b"streaming-thought-sig"
part = Part.from_function_call(name="stream_func", args={"a": "b"})
part.thought_signature = thought_sig_value
candidate = Candidate()
candidate.index = 0
candidate.content = Content(role="user", parts=[part])
candidate.finish_reason = GFinishReason.STOP
chunk = GenerateContentResponse()
chunk.candidates = [candidate]
chunk.usage_metadata = GenerateContentResponseUsageMetadata(
prompt_token_count=0, cached_content_token_count=0, candidates_token_count=0, total_token_count=0
)
completion = GoogleAIChatCompletion()
result = completion._create_streaming_chat_message_content(chunk, candidate)
fc_items = [item for item in result.items if isinstance(item, FunctionCallContent)]
assert len(fc_items) == 1
assert fc_items[0].metadata is not None
assert fc_items[0].metadata["thought_signature"] == thought_sig_value
def test_create_streaming_chat_message_content_without_thought_signature(google_ai_unit_test_env) -> None:
"""Test that streaming FunctionCallContent works when Part lacks thought_signature."""
from google.genai.types import (
Candidate,
Content,
GenerateContentResponse,
GenerateContentResponseUsageMetadata,
Part,
)
from google.genai.types import (
FinishReason as GFinishReason,
)
from semantic_kernel.contents.function_call_content import FunctionCallContent
part = Part.from_function_call(name="stream_func", args={"a": "b"})
candidate = Candidate()
candidate.index = 0
candidate.content = Content(role="user", parts=[part])
candidate.finish_reason = GFinishReason.STOP
chunk = GenerateContentResponse()
chunk.candidates = [candidate]
chunk.usage_metadata = GenerateContentResponseUsageMetadata(
prompt_token_count=0, cached_content_token_count=0, candidates_token_count=0, total_token_count=0
)
completion = GoogleAIChatCompletion()
result = completion._create_streaming_chat_message_content(chunk, candidate)
fc_items = [item for item in result.items if isinstance(item, FunctionCallContent)]
assert len(fc_items) == 1
assert "thought_signature" not in fc_items[0].metadata
def test_create_chat_message_content_getattr_guard_on_missing_attribute(google_ai_unit_test_env) -> None:
"""Test that getattr guard handles SDK versions where thought_signature doesn't exist on Part."""
from unittest.mock import MagicMock
from google.genai.types import (
GenerateContentResponse,
GenerateContentResponseUsageMetadata,
)
from semantic_kernel.contents.function_call_content import FunctionCallContent
# Create a mock Part that lacks 'thought_signature' attribute entirely
mock_part = MagicMock()
mock_part.text = None
mock_part.function_call.name = "test_func"
mock_part.function_call.args = {"x": "y"}
del mock_part.thought_signature # simulate older SDK without the field
# Use a fully-mocked candidate to avoid Content pydantic validation
mock_candidate = MagicMock()
mock_candidate.index = 0
mock_candidate.content.parts = [mock_part]
mock_candidate.finish_reason = 1 # STOP
response = GenerateContentResponse()
response.candidates = [mock_candidate]
response.usage_metadata = GenerateContentResponseUsageMetadata(
prompt_token_count=0, cached_content_token_count=0, candidates_token_count=0, total_token_count=0
)
completion = GoogleAIChatCompletion()
result = completion._create_chat_message_content(response, mock_candidate)
fc_items = [item for item in result.items if isinstance(item, FunctionCallContent)]
assert len(fc_items) == 1
assert "thought_signature" not in fc_items[0].metadata
# endregion deserialization
@@ -0,0 +1,212 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import AsyncMock, patch
import pytest
from google.genai import Client
from google.genai.models import AsyncModels
from google.genai.types import GenerateContentConfigDict
from semantic_kernel.connectors.ai.google.google_ai.google_ai_prompt_execution_settings import (
GoogleAITextPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.google.google_ai.google_ai_settings import GoogleAISettings
from semantic_kernel.connectors.ai.google.google_ai.services.google_ai_text_completion import GoogleAITextCompletion
from semantic_kernel.contents.text_content import TextContent
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError
# region init
def test_google_ai_text_completion_init(google_ai_unit_test_env) -> None:
"""Test initialization of GoogleAITextCompletion"""
model_id = google_ai_unit_test_env["GOOGLE_AI_GEMINI_MODEL_ID"]
api_key = google_ai_unit_test_env["GOOGLE_AI_API_KEY"]
google_ai_text_completion = GoogleAITextCompletion()
assert google_ai_text_completion.ai_model_id == model_id
assert google_ai_text_completion.service_id == model_id
assert isinstance(google_ai_text_completion.service_settings, GoogleAISettings)
assert google_ai_text_completion.service_settings.gemini_model_id == model_id
assert google_ai_text_completion.service_settings.api_key.get_secret_value() == api_key
def test_google_ai_text_completion_init_with_service_id(google_ai_unit_test_env, service_id) -> None:
"""Test initialization of GoogleAITextCompletion with a service_id that is not the model_id"""
google_ai_text_completion = GoogleAITextCompletion(service_id=service_id)
assert google_ai_text_completion.service_id == service_id
def test_google_ai_text_completion_init_with_model_id_in_argument(google_ai_unit_test_env) -> None:
"""Test initialization of GoogleAIChatCompletion with model_id in argument"""
google_ai_text_completion = GoogleAITextCompletion(gemini_model_id="custom_model_id")
assert google_ai_text_completion.ai_model_id == "custom_model_id"
assert google_ai_text_completion.service_id == "custom_model_id"
@pytest.mark.parametrize("exclude_list", [["GOOGLE_AI_GEMINI_MODEL_ID"]], indirect=True)
def test_google_ai_text_completion_init_with_empty_model_id(google_ai_unit_test_env) -> None:
"""Test initialization of GoogleAITextCompletion with an empty model_id"""
with pytest.raises(ServiceInitializationError, match="The Google AI Gemini model ID is required."):
GoogleAITextCompletion(env_file_path="fake_env_file_path.env")
@pytest.mark.parametrize("exclude_list", [["GOOGLE_AI_API_KEY"]], indirect=True)
def test_google_ai_text_completion_init_with_empty_api_key(google_ai_unit_test_env) -> None:
"""Test initialization of GoogleAITextCompletion with an empty api_key"""
with pytest.raises(ServiceInitializationError, match="The API key is required when use_vertexai is False."):
GoogleAITextCompletion(env_file_path="fake_env_file_path.env")
@pytest.mark.parametrize("exclude_list", ["GOOGLE_AI_CLOUD_PROJECT_ID"], indirect=True)
def test_google_ai_text_completion_init_with_use_vertexai_missing_project_id(google_ai_unit_test_env) -> None:
"""Test initialization of GoogleAITextCompletion with use_vertexai true but missing project_id"""
with pytest.raises(ServiceInitializationError, match="Project ID must be provided when use_vertexai is True."):
GoogleAITextCompletion(use_vertexai=True, env_file_path="fake_env_file_path.env")
@pytest.mark.parametrize("exclude_list", [["GOOGLE_AI_CLOUD_REGION"]], indirect=True)
def test_google_ai_text_completion_init_with_use_vertexai_missing_region(google_ai_unit_test_env) -> None:
"""Test initialization of GoogleAITextCompletion with use_vertexai true but missing region"""
with pytest.raises(ServiceInitializationError, match="Region must be provided when use_vertexai is True."):
GoogleAITextCompletion(use_vertexai=True, env_file_path="fake_env_file_path.env")
@pytest.mark.parametrize("exclude_list", [["GOOGLE_AI_API_KEY"]], indirect=True)
def test_google_ai_text_completion_init_with_use_vertexai_no_api_key(google_ai_unit_test_env) -> None:
"""Test initialization of GoogleAITextCompletion succeeds with use_vertexai=True and no api_key"""
text_completion = GoogleAITextCompletion(use_vertexai=True)
assert text_completion.service_settings.use_vertexai is True
def test_prompt_execution_settings_class(google_ai_unit_test_env) -> None:
google_ai_text_completion = GoogleAITextCompletion()
assert google_ai_text_completion.get_prompt_execution_settings_class() == GoogleAITextPromptExecutionSettings
# endregion init
# region text completion
@patch.object(AsyncModels, "generate_content", new_callable=AsyncMock)
async def test_google_ai_text_completion(
mock_google_model_generate_content,
google_ai_unit_test_env,
prompt: str,
mock_google_ai_text_completion_response,
) -> None:
"""Test text completion with GoogleAITextCompletion"""
settings = GoogleAITextPromptExecutionSettings()
mock_google_model_generate_content.return_value = mock_google_ai_text_completion_response
google_ai_text_completion = GoogleAITextCompletion()
responses: list[TextContent] = await google_ai_text_completion.get_text_contents(prompt, settings)
mock_google_model_generate_content.assert_called_once_with(
model=google_ai_text_completion.service_settings.gemini_model_id,
contents=prompt,
config=GenerateContentConfigDict(**settings.prepare_settings_dict()),
)
assert len(responses) == 1
assert responses[0].text == mock_google_ai_text_completion_response.candidates[0].content.parts[0].text
assert "usage" in responses[0].metadata
assert "prompt_feedback" in responses[0].metadata
assert responses[0].inner_content == mock_google_ai_text_completion_response
async def test_google_ai_text_completion_with_custom_client(
prompt: str,
google_ai_unit_test_env,
mock_google_ai_text_completion_response,
) -> None:
"""Test text completion with GoogleAITextCompletion using a custom client"""
# Create a custom client with a fake API key for testing
custom_client = Client(api_key="fake-api-key-for-testing")
# Mock the custom client's generate_content method
mock_generate_content = AsyncMock(return_value=mock_google_ai_text_completion_response)
custom_client.aio.models.generate_content = mock_generate_content
google_ai_text_completion = GoogleAITextCompletion(client=custom_client)
responses: list[TextContent] = await google_ai_text_completion.get_text_contents(
prompt,
GoogleAITextPromptExecutionSettings(),
)
custom_client.close()
# Verify that the custom client was used and returned the expected response
assert len(responses) == 1
assert responses[0].text == mock_google_ai_text_completion_response.candidates[0].content.parts[0].text
assert "usage" in responses[0].metadata
assert "prompt_feedback" in responses[0].metadata
assert responses[0].inner_content == mock_google_ai_text_completion_response
# Verify that the custom client's method was called
mock_generate_content.assert_called_once()
# endregion text completion
# region streaming text completion
@patch.object(AsyncModels, "generate_content_stream", new_callable=AsyncMock)
async def test_google_ai_streaming_text_completion(
mock_google_model_generate_content_stream,
google_ai_unit_test_env,
prompt: str,
mock_google_ai_streaming_text_completion_response,
) -> None:
"""Test streaming text completion with GoogleAITextCompletion"""
settings = GoogleAITextPromptExecutionSettings()
mock_google_model_generate_content_stream.return_value = mock_google_ai_streaming_text_completion_response
google_ai_text_completion = GoogleAITextCompletion()
async for chunks in google_ai_text_completion.get_streaming_text_contents(prompt, settings):
assert len(chunks) == 1
assert "usage" in chunks[0].metadata
assert "prompt_feedback" in chunks[0].metadata
mock_google_model_generate_content_stream.assert_called_once_with(
model=google_ai_text_completion.service_settings.gemini_model_id,
contents=prompt,
config=GenerateContentConfigDict(**settings.prepare_settings_dict()),
)
async def test_google_ai_streaming_text_completion_with_custom_client(
prompt: str,
google_ai_unit_test_env,
mock_google_ai_streaming_text_completion_response,
) -> None:
"""Test streaming text completion with GoogleAITextCompletion using a custom client"""
# Create a custom client with a fake API key for testing
custom_client = Client(api_key="fake-api-key-for-testing")
# Mock the custom client's generate_content_stream method
mock_generate_content_stream = AsyncMock(return_value=mock_google_ai_streaming_text_completion_response)
custom_client.aio.models.generate_content_stream = mock_generate_content_stream
google_ai_text_completion = GoogleAITextCompletion(client=custom_client)
async for chunks in google_ai_text_completion.get_streaming_text_contents(
prompt, GoogleAITextPromptExecutionSettings()
):
assert len(chunks) == 1
assert "usage" in chunks[0].metadata
assert "prompt_feedback" in chunks[0].metadata
custom_client.close()
# Verify that the custom client's method was called
mock_generate_content_stream.assert_called_once()
# endregion streaming text completion
@@ -0,0 +1,237 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import AsyncMock, patch
import pytest
from google.genai import Client
from google.genai.models import AsyncModels
from google.genai.types import ContentEmbedding, EmbedContentConfigDict, EmbedContentResponse
from numpy import array, ndarray
from semantic_kernel.connectors.ai.google.google_ai.google_ai_prompt_execution_settings import (
GoogleAIEmbeddingPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.google.google_ai.google_ai_settings import GoogleAISettings
from semantic_kernel.connectors.ai.google.google_ai.services.google_ai_text_embedding import GoogleAITextEmbedding
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError
# region init
def test_google_ai_text_embedding_init(google_ai_unit_test_env) -> None:
"""Test initialization of GoogleAITextEmbedding"""
model_id = google_ai_unit_test_env["GOOGLE_AI_EMBEDDING_MODEL_ID"]
api_key = google_ai_unit_test_env["GOOGLE_AI_API_KEY"]
google_ai_text_embedding = GoogleAITextEmbedding()
assert google_ai_text_embedding.ai_model_id == model_id
assert google_ai_text_embedding.service_id == model_id
assert isinstance(google_ai_text_embedding.service_settings, GoogleAISettings)
assert google_ai_text_embedding.service_settings.embedding_model_id == model_id
assert google_ai_text_embedding.service_settings.api_key.get_secret_value() == api_key
def test_google_ai_text_embedding_init_with_service_id(google_ai_unit_test_env, service_id) -> None:
"""Test initialization of GoogleAITextEmbedding with a service_id that is not the model_id"""
google_ai_text_embedding = GoogleAITextEmbedding(service_id=service_id)
assert google_ai_text_embedding.service_id == service_id
def test_google_ai_text_embedding_init_with_model_id_in_argument(google_ai_unit_test_env) -> None:
"""Test initialization of GoogleAITextEmbedding with model_id in argument"""
google_ai_chat_completion = GoogleAITextEmbedding(embedding_model_id="custom_model_id")
assert google_ai_chat_completion.ai_model_id == "custom_model_id"
assert google_ai_chat_completion.service_id == "custom_model_id"
@pytest.mark.parametrize("exclude_list", [["GOOGLE_AI_EMBEDDING_MODEL_ID"]], indirect=True)
def test_google_ai_text_embedding_init_with_empty_model_id(google_ai_unit_test_env) -> None:
"""Test initialization of GoogleAITextEmbedding with an empty model_id"""
with pytest.raises(ServiceInitializationError, match="The Google AI embedding model ID is required."):
GoogleAITextEmbedding(env_file_path="fake_env_file_path.env")
@pytest.mark.parametrize("exclude_list", [["GOOGLE_AI_API_KEY"]], indirect=True)
def test_google_ai_text_embedding_init_with_empty_api_key(google_ai_unit_test_env) -> None:
"""Test initialization of GoogleAITextEmbedding with an empty api_key"""
with pytest.raises(ServiceInitializationError, match="The API key is required when use_vertexai is False."):
GoogleAITextEmbedding(env_file_path="fake_env_file_path.env")
@pytest.mark.parametrize("exclude_list", [["GOOGLE_AI_API_KEY"]], indirect=True)
def test_google_ai_text_embedding_init_with_use_vertexai_no_api_key(google_ai_unit_test_env) -> None:
"""Test initialization of GoogleAITextEmbedding succeeds with use_vertexai=True and no api_key"""
embedding = GoogleAITextEmbedding(use_vertexai=True)
assert embedding.service_settings.use_vertexai is True
@pytest.mark.parametrize("exclude_list", [["GOOGLE_AI_CLOUD_PROJECT_ID"]], indirect=True)
def test_google_ai_text_embedding_init_with_use_vertexai_missing_project_id(google_ai_unit_test_env) -> None:
"""Test initialization of GoogleAITextEmbedding with use_vertexai true but missing project ID"""
with pytest.raises(ServiceInitializationError, match="Project ID must be provided when use_vertexai is True."):
GoogleAITextEmbedding(use_vertexai=True, env_file_path="fake_env_file_path.env")
@pytest.mark.parametrize("exclude_list", [["GOOGLE_AI_CLOUD_REGION"]], indirect=True)
def test_google_ai_text_embedding_init_with_use_vertexai_missing_region(google_ai_unit_test_env) -> None:
"""Test initialization of GoogleAITextEmbedding with use_vertexai true but missing region"""
with pytest.raises(ServiceInitializationError, match="Region must be provided when use_vertexai is True."):
GoogleAITextEmbedding(use_vertexai=True, env_file_path="fake_env_file_path.env")
def test_prompt_execution_settings_class(google_ai_unit_test_env) -> None:
google_ai_text_embedding = GoogleAITextEmbedding()
assert google_ai_text_embedding.get_prompt_execution_settings_class() == GoogleAIEmbeddingPromptExecutionSettings
# endregion init
@patch.object(AsyncModels, "embed_content", new_callable=AsyncMock)
async def test_embedding(mock_google_model_embed_content, google_ai_unit_test_env, prompt):
"""Test that the service initializes and generates embeddings correctly."""
model_id = google_ai_unit_test_env["GOOGLE_AI_EMBEDDING_MODEL_ID"]
mock_google_model_embed_content.return_value = EmbedContentResponse(
embeddings=[ContentEmbedding(values=[0.1, 0.2, 0.3])]
)
settings = GoogleAIEmbeddingPromptExecutionSettings()
google_ai_text_embedding = GoogleAITextEmbedding()
response: ndarray = await google_ai_text_embedding.generate_embeddings(
[prompt],
settings=settings,
)
assert len(response) == 1
assert response.all() == array([0.1, 0.2, 0.3]).all()
mock_google_model_embed_content.assert_called_once_with(
model=model_id,
contents=[prompt],
config=EmbedContentConfigDict(output_dimensionality=settings.output_dimensionality),
)
@patch.object(AsyncModels, "embed_content", new_callable=AsyncMock)
async def test_embedding_with_settings(mock_google_model_embed_content, google_ai_unit_test_env, prompt):
"""Test that the service initializes and generates embeddings correctly."""
model_id = google_ai_unit_test_env["GOOGLE_AI_EMBEDDING_MODEL_ID"]
mock_google_model_embed_content.return_value = EmbedContentResponse(
embeddings=[ContentEmbedding(values=[0.1, 0.2, 0.3])]
)
settings = GoogleAIEmbeddingPromptExecutionSettings()
settings.output_dimensionality = 3
google_ai_text_embedding = GoogleAITextEmbedding()
response: ndarray = await google_ai_text_embedding.generate_embeddings(
[prompt],
settings=settings,
)
assert len(response) == 1
assert response.all() == array([0.1, 0.2, 0.3]).all()
mock_google_model_embed_content.assert_called_once_with(
model=model_id,
contents=[prompt],
config=EmbedContentConfigDict(**settings.prepare_settings_dict()),
)
@patch.object(AsyncModels, "embed_content", new_callable=AsyncMock)
async def test_embedding_without_settings(mock_google_model_embed_content, google_ai_unit_test_env, prompt):
"""Test that the service initializes and generates embeddings correctly without settings."""
model_id = google_ai_unit_test_env["GOOGLE_AI_EMBEDDING_MODEL_ID"]
mock_google_model_embed_content.return_value = EmbedContentResponse(
embeddings=[ContentEmbedding(values=[0.1, 0.2, 0.3])]
)
google_ai_text_embedding = GoogleAITextEmbedding()
response: ndarray = await google_ai_text_embedding.generate_embeddings([prompt])
assert len(response) == 1
assert response.all() == array([0.1, 0.2, 0.3]).all()
mock_google_model_embed_content.assert_called_once_with(
model=model_id,
contents=[prompt],
config=EmbedContentConfigDict(output_dimensionality=None),
)
@patch.object(AsyncModels, "embed_content", new_callable=AsyncMock)
async def test_embedding_list_input(mock_google_model_embed_content, google_ai_unit_test_env, prompt):
"""Test that the service initializes and generates embeddings correctly with a list of prompts."""
model_id = google_ai_unit_test_env["GOOGLE_AI_EMBEDDING_MODEL_ID"]
mock_google_model_embed_content.return_value = EmbedContentResponse(
embeddings=[ContentEmbedding(values=[0.1, 0.2, 0.3]), ContentEmbedding(values=[0.1, 0.2, 0.3])]
)
settings = GoogleAIEmbeddingPromptExecutionSettings()
google_ai_text_embedding = GoogleAITextEmbedding()
response: ndarray = await google_ai_text_embedding.generate_embeddings(
[prompt, prompt],
settings=settings,
)
assert len(response) == 2
assert response.all() == array([[0.1, 0.2, 0.3], [0.1, 0.2, 0.3]]).all()
mock_google_model_embed_content.assert_called_once_with(
model=model_id,
contents=[prompt, prompt],
config=EmbedContentConfigDict(output_dimensionality=settings.output_dimensionality),
)
@patch.object(AsyncModels, "embed_content", new_callable=AsyncMock)
async def test_raw_embedding(mock_google_model_embed_content, google_ai_unit_test_env, prompt):
"""Test that the service initializes and generates embeddings correctly."""
model_id = google_ai_unit_test_env["GOOGLE_AI_EMBEDDING_MODEL_ID"]
mock_google_model_embed_content.return_value = EmbedContentResponse(
embeddings=[ContentEmbedding(values=[0.1, 0.2, 0.3])]
)
settings = GoogleAIEmbeddingPromptExecutionSettings()
google_ai_text_embedding = GoogleAITextEmbedding()
response: list[list[float]] = await google_ai_text_embedding.generate_raw_embeddings(
[prompt],
settings=settings,
)
assert len(response) == 1
assert response[0] == [0.1, 0.2, 0.3]
mock_google_model_embed_content.assert_called_once_with(
model=model_id,
contents=[prompt],
config=EmbedContentConfigDict(output_dimensionality=settings.output_dimensionality),
)
async def test_embedding_with_custom_client(google_ai_unit_test_env, prompt) -> None:
"""Test embedding with GoogleAITextEmbedding using a custom client"""
# Create a custom client with a fake API key for testing
custom_client = Client(api_key="fake-api-key-for-testing")
# Mock the custom client's embed_content method
mock_embed_content = AsyncMock(
return_value=EmbedContentResponse(embeddings=[ContentEmbedding(values=[0.1, 0.2, 0.3])])
)
custom_client.aio.models.embed_content = mock_embed_content
google_ai_text_embedding = GoogleAITextEmbedding(client=custom_client)
response: list[list[float]] = await google_ai_text_embedding.generate_embeddings(
[prompt],
GoogleAIEmbeddingPromptExecutionSettings(),
)
custom_client.close()
# Verify that the custom client was used and returned the expected response
assert len(response) == 1
assert response.all() == array([0.1, 0.2, 0.3]).all()
# Verify that the custom client's method was called
mock_embed_content.assert_called_once()
@@ -0,0 +1,201 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from google.genai.types import FinishReason, Part
from semantic_kernel.connectors.ai.google.google_ai.services.utils import (
finish_reason_from_google_ai_to_semantic_kernel,
format_assistant_message,
format_user_message,
kernel_function_metadata_to_google_ai_function_call_format,
)
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.contents.utils.finish_reason import FinishReason as SemanticKernelFinishReason
from semantic_kernel.exceptions.service_exceptions import ServiceInvalidRequestError
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata
from semantic_kernel.functions.kernel_parameter_metadata import KernelParameterMetadata
def test_finish_reason_from_google_ai_to_semantic_kernel():
"""Test finish_reason_from_google_ai_to_semantic_kernel."""
assert finish_reason_from_google_ai_to_semantic_kernel(FinishReason.STOP) == SemanticKernelFinishReason.STOP
assert finish_reason_from_google_ai_to_semantic_kernel(FinishReason.MAX_TOKENS) == SemanticKernelFinishReason.LENGTH
assert (
finish_reason_from_google_ai_to_semantic_kernel(FinishReason.SAFETY)
== SemanticKernelFinishReason.CONTENT_FILTER
)
assert finish_reason_from_google_ai_to_semantic_kernel(FinishReason.OTHER) is None
assert finish_reason_from_google_ai_to_semantic_kernel(None) is None
def test_format_user_message():
"""Test format_user_message."""
user_message = ChatMessageContent(role=AuthorRole.USER, content="User message")
formatted_user_message = format_user_message(user_message)
assert len(formatted_user_message) == 1
assert isinstance(formatted_user_message[0], Part)
assert formatted_user_message[0].text == "User message"
# Test with an image content
image_content = ImageContent(data="image data", mime_type="image/png")
user_message = ChatMessageContent(
role=AuthorRole.USER,
items=[
TextContent(text="Text content"),
image_content,
],
)
formatted_user_message = format_user_message(user_message)
assert len(formatted_user_message) == 2
assert isinstance(formatted_user_message[0], Part)
assert formatted_user_message[0].text == "Text content"
assert isinstance(formatted_user_message[1], Part)
assert formatted_user_message[1].inline_data.mime_type == "image/png"
assert formatted_user_message[1].inline_data.data == image_content.data
def test_format_user_message_throws_with_unsupported_items() -> None:
"""Test format_user_message with unsupported items."""
# Test with unsupported items, any item other than TextContent and ImageContent should raise an error
user_message = ChatMessageContent(
role=AuthorRole.USER,
items=[
FunctionCallContent(),
],
)
with pytest.raises(ServiceInvalidRequestError):
format_user_message(user_message)
# Test with an ImageContent that has no data_uri
user_message = ChatMessageContent(
role=AuthorRole.USER,
items=[
ImageContent(data_uri=""),
],
)
with pytest.raises(ServiceInvalidRequestError):
format_user_message(user_message)
def test_format_assistant_message() -> None:
assistant_message = ChatMessageContent(
role=AuthorRole.ASSISTANT,
items=[
TextContent(text="test"),
FunctionCallContent(name="test_function", arguments={}),
ImageContent(data="image data", mime_type="image/png"),
],
)
formatted_assistant_message = format_assistant_message(assistant_message)
assert isinstance(formatted_assistant_message, list)
assert len(formatted_assistant_message) == 3
assert isinstance(formatted_assistant_message[0], Part)
assert formatted_assistant_message[0].text == "test"
assert isinstance(formatted_assistant_message[1], Part)
assert formatted_assistant_message[1].function_call.name == "test_function"
assert formatted_assistant_message[1].function_call.args == {}
assert isinstance(formatted_assistant_message[2], Part)
assert formatted_assistant_message[2].inline_data
def test_format_assistant_message_with_unsupported_items() -> None:
assistant_message = ChatMessageContent(
role=AuthorRole.ASSISTANT,
items=[
FunctionResultContent(id="test_id", function_name="test_function"),
],
)
with pytest.raises(ServiceInvalidRequestError):
format_assistant_message(assistant_message)
def test_format_assistant_message_with_thought_signature() -> None:
"""Test that thought_signature is preserved in function call parts."""
import base64
thought_sig = base64.b64encode(b"test_thought_signature_data")
assistant_message = ChatMessageContent(
role=AuthorRole.ASSISTANT,
items=[
FunctionCallContent(
name="test_function",
arguments={"arg1": "value1"},
metadata={"thought_signature": thought_sig},
),
],
)
formatted = format_assistant_message(assistant_message)
assert len(formatted) == 1
assert isinstance(formatted[0], Part)
assert formatted[0].function_call.name == "test_function"
assert formatted[0].function_call.args == {"arg1": "value1"}
assert formatted[0].thought_signature == thought_sig
def test_format_assistant_message_without_thought_signature() -> None:
"""Test that function calls without thought_signature still work."""
assistant_message = ChatMessageContent(
role=AuthorRole.ASSISTANT,
items=[
FunctionCallContent(
name="test_function",
arguments={"arg1": "value1"},
),
],
)
formatted = format_assistant_message(assistant_message)
assert len(formatted) == 1
assert isinstance(formatted[0], Part)
assert formatted[0].function_call.name == "test_function"
assert formatted[0].function_call.args == {"arg1": "value1"}
assert not getattr(formatted[0], "thought_signature", None)
def test_google_ai_function_call_format_sanitizes_anyof_schema() -> None:
"""Integration test: anyOf in param schema_data is sanitized in the output dict."""
metadata = KernelFunctionMetadata(
name="test_func",
description="A test function",
is_prompt=False,
parameters=[
KernelParameterMetadata(
name="messages",
description="The user messages",
is_required=True,
schema_data={
"anyOf": [
{"type": "string"},
{"type": "array", "items": {"type": "string"}},
],
"description": "The user messages",
},
),
],
)
result = kernel_function_metadata_to_google_ai_function_call_format(metadata)
param_schema = result["parameters"]["properties"]["messages"]
assert "anyOf" not in param_schema
assert param_schema["type"] == "string"
def test_google_ai_function_call_format_empty_parameters() -> None:
"""Integration test: metadata with no parameters produces parameters=None."""
metadata = KernelFunctionMetadata(
name="no_params_func",
description="No parameters",
is_prompt=False,
parameters=[],
)
result = kernel_function_metadata_to_google_ai_function_call_format(metadata)
assert result["parameters"] is None
@@ -0,0 +1,133 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.connectors.ai.google.google_ai import (
GoogleAIChatPromptExecutionSettings,
GoogleAIEmbeddingPromptExecutionSettings,
GoogleAIPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
def test_default_google_ai_prompt_execution_settings():
settings = GoogleAIPromptExecutionSettings()
assert settings.stop_sequences is None
assert settings.response_mime_type is None
assert settings.response_schema is None
assert settings.candidate_count is None
assert settings.max_output_tokens is None
assert settings.temperature is None
assert settings.top_p is None
assert settings.top_k is None
def test_custom_google_ai_prompt_execution_settings():
settings = GoogleAIPromptExecutionSettings(
stop_sequences=["world"],
response_mime_type="text/plain",
candidate_count=1,
max_output_tokens=128,
temperature=0.5,
top_p=0.5,
top_k=10,
)
assert settings.stop_sequences == ["world"]
assert settings.response_mime_type == "text/plain"
assert settings.candidate_count == 1
assert settings.max_output_tokens == 128
assert settings.temperature == 0.5
assert settings.top_p == 0.5
assert settings.top_k == 10
def test_google_ai_prompt_execution_settings_from_default_completion_config():
settings = PromptExecutionSettings(service_id="test_service")
chat_settings = GoogleAIChatPromptExecutionSettings.from_prompt_execution_settings(settings)
assert chat_settings.service_id == "test_service"
assert chat_settings.stop_sequences is None
assert chat_settings.response_mime_type is None
assert chat_settings.response_schema is None
assert chat_settings.candidate_count is None
assert chat_settings.max_output_tokens is None
assert chat_settings.temperature is None
assert chat_settings.top_p is None
assert chat_settings.top_k is None
def test_google_ai_prompt_execution_settings_from_openai_prompt_execution_settings():
chat_settings = GoogleAIChatPromptExecutionSettings(service_id="test_service", temperature=1.0)
new_settings = GoogleAIPromptExecutionSettings(service_id="test_2", temperature=0.0)
chat_settings.update_from_prompt_execution_settings(new_settings)
assert chat_settings.service_id == "test_2"
assert chat_settings.temperature == 0.0
def test_google_ai_prompt_execution_settings_from_custom_completion_config():
settings = PromptExecutionSettings(
service_id="test_service",
extension_data={
"stop_sequences": ["world"],
"response_mime_type": "text/plain",
"candidate_count": 1,
"max_output_tokens": 128,
"temperature": 0.5,
"top_p": 0.5,
"top_k": 10,
},
)
chat_settings = GoogleAIChatPromptExecutionSettings.from_prompt_execution_settings(settings)
assert chat_settings.stop_sequences == ["world"]
assert chat_settings.response_mime_type == "text/plain"
assert chat_settings.candidate_count == 1
assert chat_settings.max_output_tokens == 128
assert chat_settings.temperature == 0.5
assert chat_settings.top_p == 0.5
assert chat_settings.top_k == 10
def test_google_ai_chat_prompt_execution_settings_from_custom_completion_config_with_functions():
settings = PromptExecutionSettings(
service_id="test_service",
extension_data={
"tools": [{"function": {}}],
},
)
chat_settings = GoogleAIChatPromptExecutionSettings.from_prompt_execution_settings(settings)
assert chat_settings.tools == [{"function": {}}]
def test_create_options():
settings = GoogleAIChatPromptExecutionSettings(
service_id="test_service",
extension_data={
"stop_sequences": ["world"],
"response_mime_type": "text/plain",
"candidate_count": 1,
"max_output_tokens": 128,
"temperature": 0.5,
"top_p": 0.5,
"top_k": 10,
},
)
options = settings.prepare_settings_dict()
assert options["stop_sequences"] == ["world"]
assert options["response_mime_type"] == "text/plain"
assert options["candidate_count"] == 1
assert options["max_output_tokens"] == 128
assert options["temperature"] == 0.5
assert options["top_p"] == 0.5
assert options["top_k"] == 10
assert "tools" not in options
assert "tool_config" not in options
def test_default_google_ai_embedding_prompt_execution_settings():
settings = GoogleAIEmbeddingPromptExecutionSettings()
assert settings.output_dimensionality is None
@@ -0,0 +1,268 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceType
from semantic_kernel.connectors.ai.google.shared_utils import (
FUNCTION_CHOICE_TYPE_TO_GOOGLE_FUNCTION_CALLING_MODE,
GEMINI_FUNCTION_NAME_SEPARATOR,
collapse_function_call_results_in_chat_history,
filter_system_message,
format_gemini_function_name_to_kernel_function_fully_qualified_name,
sanitize_schema_for_google_ai,
)
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.utils.author_role import AuthorRole
from semantic_kernel.exceptions.service_exceptions import ServiceInvalidRequestError
def test_first_system_message():
"""Test filter_system_message."""
# Test with a single system message
chat_history = ChatHistory()
chat_history.add_system_message("System message")
chat_history.add_user_message("User message")
assert filter_system_message(chat_history) == "System message"
# Test with no system message
chat_history = ChatHistory()
chat_history.add_user_message("User message")
assert filter_system_message(chat_history) is None
# Test with multiple system messages
chat_history = ChatHistory()
chat_history.add_system_message("System message 1")
chat_history.add_system_message("System message 2")
with pytest.raises(ServiceInvalidRequestError):
filter_system_message(chat_history)
def test_function_choice_type_to_google_function_calling_mode_contain_all_types() -> None:
assert FunctionChoiceType.AUTO in FUNCTION_CHOICE_TYPE_TO_GOOGLE_FUNCTION_CALLING_MODE
assert FunctionChoiceType.NONE in FUNCTION_CHOICE_TYPE_TO_GOOGLE_FUNCTION_CALLING_MODE
assert FunctionChoiceType.REQUIRED in FUNCTION_CHOICE_TYPE_TO_GOOGLE_FUNCTION_CALLING_MODE
def test_format_gemini_function_name_to_kernel_function_fully_qualified_name() -> None:
# Contains the separator
gemini_function_name = f"plugin{GEMINI_FUNCTION_NAME_SEPARATOR}function"
assert (
format_gemini_function_name_to_kernel_function_fully_qualified_name(gemini_function_name) == "plugin-function"
)
# Doesn't contain the separator
gemini_function_name = "function"
assert format_gemini_function_name_to_kernel_function_fully_qualified_name(gemini_function_name) == "function"
def test_collapse_function_call_results_in_chat_history() -> None:
chat_history = ChatHistory()
chat_history.extend([
ChatMessageContent(
AuthorRole.ASSISTANT,
items=[
FunctionCallContent(id="function1", name="function1"),
FunctionCallContent(id="function2", name="function2"),
],
),
# The following two messages should be collapsed into a single message
ChatMessageContent(
AuthorRole.TOOL,
items=[FunctionResultContent(id="function1", name="function1", result="result1")],
),
ChatMessageContent(
AuthorRole.TOOL,
items=[FunctionResultContent(id="function2", name="function2", result="result2")],
),
ChatMessageContent(AuthorRole.ASSISTANT, content="Assistant message"),
ChatMessageContent(AuthorRole.USER, content="User message"),
ChatMessageContent(
AuthorRole.ASSISTANT,
items=[FunctionCallContent(id="function3", name="function3")],
),
ChatMessageContent(
AuthorRole.TOOL,
items=[FunctionResultContent(id="function3", name="function3", result="result3")],
),
ChatMessageContent(AuthorRole.ASSISTANT, content="Assistant message"),
])
assert len(chat_history.messages) == 8
collapse_function_call_results_in_chat_history(chat_history)
assert len(chat_history.messages) == 7
assert len(chat_history.messages[1].items) == 2
# --- sanitize_schema_for_google_ai tests ---
def test_sanitize_schema_none():
"""Test that None input returns None."""
assert sanitize_schema_for_google_ai(None) is None
def test_sanitize_schema_simple_passthrough():
"""Test that a simple schema passes through unchanged."""
schema = {"type": "string", "description": "A name"}
result = sanitize_schema_for_google_ai(schema)
assert result == {"type": "string", "description": "A name"}
def test_sanitize_schema_type_as_list_with_null():
"""type: ["string", "null"] should become type: "string" + nullable: true."""
schema = {"type": ["string", "null"], "description": "Optional field"}
result = sanitize_schema_for_google_ai(schema)
assert result == {"type": "string", "nullable": True, "description": "Optional field"}
def test_sanitize_schema_type_as_list_without_null():
"""type: ["string", "integer"] should pick the first type."""
schema = {"type": ["string", "integer"]}
result = sanitize_schema_for_google_ai(schema)
assert result == {"type": "string"}
def test_sanitize_schema_anyof_with_null():
"""AnyOf with null variant should become the non-null type + nullable."""
schema = {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Optional param",
}
result = sanitize_schema_for_google_ai(schema)
assert result == {"type": "string", "nullable": True, "description": "Optional param"}
def test_sanitize_schema_anyof_without_null():
"""AnyOf without null should pick the first variant."""
schema = {
"anyOf": [
{"type": "string"},
{"type": "array", "items": {"type": "string"}},
],
}
result = sanitize_schema_for_google_ai(schema)
assert result == {"type": "string"}
def test_sanitize_schema_oneof():
"""OneOf should be handled the same as anyOf."""
schema = {
"oneOf": [{"type": "integer"}, {"type": "null"}],
}
result = sanitize_schema_for_google_ai(schema)
assert result == {"type": "integer", "nullable": True}
def test_sanitize_schema_nested_properties():
"""AnyOf inside nested properties should be sanitized recursively."""
schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"value": {"anyOf": [{"type": "number"}, {"type": "null"}]},
},
}
result = sanitize_schema_for_google_ai(schema)
assert result == {
"type": "object",
"properties": {
"name": {"type": "string"},
"value": {"type": "number", "nullable": True},
},
}
def test_sanitize_schema_nested_items():
"""AnyOf inside array items should be sanitized recursively."""
schema = {
"type": "array",
"items": {"anyOf": [{"type": "string"}, {"type": "integer"}]},
}
result = sanitize_schema_for_google_ai(schema)
assert result == {
"type": "array",
"items": {"type": "string"},
}
def test_sanitize_schema_does_not_mutate_original():
"""The original schema dict should not be modified."""
schema = {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "test",
}
original = {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "test"}
sanitize_schema_for_google_ai(schema)
assert schema == original
def test_sanitize_schema_agent_messages_param():
"""Reproducer for issue #12442: str | list[str] parameter schema."""
schema = {
"anyOf": [
{"type": "string"},
{"type": "array", "items": {"type": "string"}},
],
"description": "The user messages for the agent.",
}
result = sanitize_schema_for_google_ai(schema)
assert "anyOf" not in result
assert result["type"] == "string"
assert result["description"] == "The user messages for the agent."
def test_sanitize_schema_allof():
"""AllOf should be handled like anyOf/oneOf, picking the first variant."""
schema = {
"allOf": [
{"type": "object", "properties": {"name": {"type": "string"}}},
{"type": "object", "properties": {"age": {"type": "integer"}}},
],
}
result = sanitize_schema_for_google_ai(schema)
assert "allOf" not in result
assert result["type"] == "object"
assert "name" in result["properties"]
def test_sanitize_schema_allof_with_null():
"""AllOf with a null variant should produce nullable: true."""
schema = {
"allOf": [{"type": "string"}, {"type": "null"}],
}
result = sanitize_schema_for_google_ai(schema)
assert "allOf" not in result
assert result["type"] == "string"
assert result["nullable"] is True
def test_sanitize_schema_all_null_type_list():
"""type: ["null"] should fall back to type: "string" + nullable: true."""
schema = {"type": ["null"]}
result = sanitize_schema_for_google_ai(schema)
assert result == {"type": "string", "nullable": True}
def test_sanitize_schema_all_null_anyof():
"""AnyOf where all variants are null should fall back to type: "string"."""
schema = {"anyOf": [{"type": "null"}]}
result = sanitize_schema_for_google_ai(schema)
assert result == {"type": "string", "nullable": True}
def test_sanitize_schema_chosen_variant_keeps_own_description():
"""When the chosen anyOf variant has its own description, do not overwrite it."""
schema = {
"anyOf": [
{"type": "string", "description": "inner desc"},
{"type": "null"},
],
"description": "outer desc",
}
result = sanitize_schema_for_google_ai(schema)
assert result["description"] == "inner desc"
assert result["nullable"] is True
@@ -0,0 +1,189 @@
# Copyright (c) Microsoft. All rights reserved.
from collections.abc import AsyncGenerator, AsyncIterable
from unittest.mock import MagicMock
import pytest
from google.cloud.aiplatform_v1beta1.types.content import Candidate, Content, Part
from google.cloud.aiplatform_v1beta1.types.prediction_service import GenerateContentResponse
from google.cloud.aiplatform_v1beta1.types.tool import FunctionCall
from vertexai.generative_models import GenerationResponse
from vertexai.language_models import TextEmbedding
@pytest.fixture()
def vertex_ai_unit_test_env(monkeypatch, exclude_list, override_env_param_dict):
"""Fixture to set environment variables for Vertex AI Unit Tests."""
if exclude_list is None:
exclude_list = []
if override_env_param_dict is None:
override_env_param_dict = {}
env_vars = {
"VERTEX_AI_GEMINI_MODEL_ID": "test-gemini-model-id",
"VERTEX_AI_EMBEDDING_MODEL_ID": "test-embedding-model-id",
"VERTEX_AI_PROJECT_ID": "test-project-id",
}
env_vars.update(override_env_param_dict)
for key, value in env_vars.items():
if key not in exclude_list:
monkeypatch.setenv(key, value)
else:
monkeypatch.delenv(key, raising=False)
return env_vars
@pytest.fixture()
def mock_vertex_ai_chat_completion_response() -> GenerationResponse:
"""Mock Vertex AI Chat Completion response."""
candidate = Candidate()
candidate.index = 0
candidate.content = Content(role="user", parts=[Part(text="Test content")])
candidate.finish_reason = Candidate.FinishReason.STOP
response = GenerateContentResponse()
response.candidates.append(candidate)
response.usage_metadata = GenerateContentResponse.UsageMetadata(
prompt_token_count=0,
candidates_token_count=0,
total_token_count=0,
)
return GenerationResponse._from_gapic(response)
@pytest.fixture()
def mock_vertex_ai_chat_completion_response_with_tool_call() -> GenerationResponse:
"""Mock Vertex AI Chat Completion response."""
candidate = Candidate()
candidate.index = 0
candidate.content = Content(
role="user",
parts=[
Part(
function_call=FunctionCall(
name="test_function",
args={"test_arg": "test_value"},
)
)
],
)
candidate.finish_reason = Candidate.FinishReason.STOP
response = GenerateContentResponse()
response.candidates.append(candidate)
response.usage_metadata = GenerateContentResponse.UsageMetadata(
prompt_token_count=0,
candidates_token_count=0,
total_token_count=0,
)
return GenerationResponse._from_gapic(response)
@pytest.fixture()
def mock_vertex_ai_streaming_chat_completion_response() -> AsyncIterable[GenerationResponse]:
"""Mock Vertex AI streaming Chat Completion response."""
candidate = Candidate()
candidate.index = 0
candidate.content = Content(role="user", parts=[Part(text="Test content")])
candidate.finish_reason = Candidate.FinishReason.STOP
response = GenerateContentResponse()
response.candidates.append(candidate)
response.usage_metadata = GenerateContentResponse.UsageMetadata(
prompt_token_count=0,
candidates_token_count=0,
total_token_count=0,
)
iterable = MagicMock(spec=AsyncGenerator)
iterable.__aiter__.return_value = [GenerationResponse._from_gapic(response)]
return iterable
@pytest.fixture()
def mock_vertex_ai_streaming_chat_completion_response_with_tool_call() -> AsyncIterable[GenerationResponse]:
"""Mock Vertex AI streaming Chat Completion response."""
candidate = Candidate()
candidate.index = 0
candidate.content = Content(
role="user",
parts=[
Part(
function_call=FunctionCall(
name="getLightStatus",
args={"arg1": "test_value"},
)
)
],
)
candidate.finish_reason = Candidate.FinishReason.STOP
response = GenerateContentResponse()
response.candidates.append(candidate)
response.usage_metadata = GenerateContentResponse.UsageMetadata(
prompt_token_count=0,
candidates_token_count=0,
total_token_count=0,
)
iterable = MagicMock(spec=AsyncGenerator)
iterable.__aiter__.return_value = [GenerationResponse._from_gapic(response)]
return iterable
@pytest.fixture()
def mock_vertex_ai_text_completion_response() -> GenerationResponse:
"""Mock Vertex AI Text Completion response."""
candidate = Candidate()
candidate.index = 0
candidate.content = Content(parts=[Part(text="Test content")])
response = GenerateContentResponse()
response.candidates.append(candidate)
response.usage_metadata = GenerateContentResponse.UsageMetadata(
prompt_token_count=0,
candidates_token_count=0,
total_token_count=0,
)
return GenerationResponse._from_gapic(response)
@pytest.fixture()
def mock_vertex_ai_streaming_text_completion_response() -> AsyncIterable[GenerationResponse]:
"""Mock Vertex AI streaming Text Completion response."""
candidate = Candidate()
candidate.index = 0
candidate.content = Content(parts=[Part(text="Test content")])
response = GenerateContentResponse()
response.candidates.append(candidate)
response.usage_metadata = GenerateContentResponse.UsageMetadata(
prompt_token_count=0,
candidates_token_count=0,
total_token_count=0,
)
iterable = MagicMock(spec=AsyncGenerator)
iterable.__aiter__.return_value = [GenerationResponse._from_gapic(response)]
return iterable
class MockTextEmbeddingModel:
async def get_embeddings_async(
self,
texts: list[str],
*,
auto_truncate: bool = True,
output_dimensionality: int | None = None,
) -> list[TextEmbedding]:
pass
@@ -0,0 +1,545 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import AsyncMock, patch
import pytest
from vertexai.generative_models import Content, GenerativeModel
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
from semantic_kernel.connectors.ai.google.vertex_ai.services.vertex_ai_chat_completion import VertexAIChatCompletion
from semantic_kernel.connectors.ai.google.vertex_ai.vertex_ai_prompt_execution_settings import (
VertexAIChatPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.google.vertex_ai.vertex_ai_settings import VertexAISettings
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.utils.finish_reason import FinishReason
from semantic_kernel.exceptions.service_exceptions import (
ServiceInitializationError,
ServiceInvalidExecutionSettingsError,
)
from semantic_kernel.kernel import Kernel
# region init
def test_vertex_ai_chat_completion_init(vertex_ai_unit_test_env) -> None:
"""Test initialization of VertexAIChatCompletion"""
model_id = vertex_ai_unit_test_env["VERTEX_AI_GEMINI_MODEL_ID"]
project_id = vertex_ai_unit_test_env["VERTEX_AI_PROJECT_ID"]
vertex_ai_chat_completion = VertexAIChatCompletion()
assert vertex_ai_chat_completion.ai_model_id == model_id
assert vertex_ai_chat_completion.service_id == model_id
assert isinstance(vertex_ai_chat_completion.service_settings, VertexAISettings)
assert vertex_ai_chat_completion.service_settings.gemini_model_id == model_id
assert vertex_ai_chat_completion.service_settings.project_id == project_id
def test_vertex_ai_chat_completion_init_with_service_id(vertex_ai_unit_test_env, service_id) -> None:
"""Test initialization of VertexAIChatCompletion with a service id that is not the model id"""
vertex_ai_chat_completion = VertexAIChatCompletion(service_id=service_id)
assert vertex_ai_chat_completion.service_id == service_id
def test_vertex_ai_chat_completion_init_with_model_id_in_argument(vertex_ai_unit_test_env) -> None:
"""Test initialization of VertexAIChatCompletion with model id in argument"""
vertex_ai_chat_completion = VertexAIChatCompletion(gemini_model_id="custom_model_id")
assert vertex_ai_chat_completion.ai_model_id == "custom_model_id"
assert vertex_ai_chat_completion.service_id == "custom_model_id"
@pytest.mark.parametrize("exclude_list", [["VERTEX_AI_GEMINI_MODEL_ID"]], indirect=True)
def test_vertex_ai_chat_completion_init_with_empty_model_id(vertex_ai_unit_test_env) -> None:
"""Test initialization of VertexAIChatCompletion with an empty model id"""
with pytest.raises(ServiceInitializationError):
VertexAIChatCompletion(env_file_path="fake_env_file_path.env")
@pytest.mark.parametrize("exclude_list", [["VERTEX_AI_PROJECT_ID"]], indirect=True)
def test_vertex_ai_chat_completion_init_with_empty_project_id(vertex_ai_unit_test_env) -> None:
"""Test initialization of VertexAIChatCompletion with an empty project id"""
with pytest.raises(ServiceInitializationError):
VertexAIChatCompletion(env_file_path="fake_env_file_path.env")
def test_prompt_execution_settings_class(vertex_ai_unit_test_env) -> None:
vertex_ai_chat_completion = VertexAIChatCompletion()
assert vertex_ai_chat_completion.get_prompt_execution_settings_class() == VertexAIChatPromptExecutionSettings
# endregion init
# region chat completion
@patch.object(GenerativeModel, "generate_content_async", new_callable=AsyncMock)
async def test_vertex_ai_chat_completion(
mock_vertex_ai_model_generate_content_async,
vertex_ai_unit_test_env,
chat_history: ChatHistory,
mock_vertex_ai_chat_completion_response,
) -> None:
"""Test chat completion with VertexAIChatCompletion"""
settings = VertexAIChatPromptExecutionSettings()
mock_vertex_ai_model_generate_content_async.return_value = mock_vertex_ai_chat_completion_response
vertex_ai_chat_completion = VertexAIChatCompletion()
responses: list[ChatMessageContent] = await vertex_ai_chat_completion.get_chat_message_contents(
chat_history, settings
)
# Verify the call was made once
mock_vertex_ai_model_generate_content_async.assert_called_once()
# Get the actual call arguments
call_args = mock_vertex_ai_model_generate_content_async.call_args
# Verify the contents
contents = call_args.kwargs["contents"]
assert len(contents) == 1
assert contents[0].role == "user"
assert len(contents[0].parts) == 1
assert contents[0].parts[0].text == "test_prompt"
# Verify other arguments
assert call_args.kwargs["generation_config"] == settings.prepare_settings_dict()
assert call_args.kwargs["tools"] is None
assert call_args.kwargs["tool_config"] is None
assert len(responses) == 1
assert responses[0].role == "assistant"
assert responses[0].content == mock_vertex_ai_chat_completion_response.candidates[0].content.parts[0].text
assert responses[0].finish_reason == FinishReason.STOP
assert "usage" in responses[0].metadata
assert "prompt_feedback" in responses[0].metadata
assert responses[0].inner_content == mock_vertex_ai_chat_completion_response
async def test_vertex_ai_chat_completion_with_function_choice_behavior_fail_verification(
chat_history: ChatHistory,
vertex_ai_unit_test_env,
) -> None:
"""Test completion of VertexAIChatCompletion with function choice behavior expect verification failure"""
# Missing kernel
with pytest.raises(ServiceInvalidExecutionSettingsError):
settings = VertexAIChatPromptExecutionSettings(
function_choice_behavior=FunctionChoiceBehavior.Auto(),
)
vertex_ai_chat_completion = VertexAIChatCompletion()
await vertex_ai_chat_completion.get_chat_message_contents(
chat_history=chat_history,
settings=settings,
)
@patch.object(GenerativeModel, "generate_content_async", new_callable=AsyncMock)
async def test_vertex_ai_chat_completion_with_function_choice_behavior(
mock_vertex_ai_model_generate_content_async,
vertex_ai_unit_test_env,
kernel,
chat_history: ChatHistory,
mock_vertex_ai_chat_completion_response_with_tool_call,
) -> None:
"""Test completion of VertexAIChatCompletion with function choice behavior"""
mock_vertex_ai_model_generate_content_async.return_value = mock_vertex_ai_chat_completion_response_with_tool_call
settings = VertexAIChatPromptExecutionSettings(
function_choice_behavior=FunctionChoiceBehavior.Auto(),
)
settings.function_choice_behavior.maximum_auto_invoke_attempts = 1
vertex_ai_chat_completion = VertexAIChatCompletion()
responses = await vertex_ai_chat_completion.get_chat_message_contents(
chat_history=chat_history,
settings=settings,
kernel=kernel,
)
# The function should be called twice:
# One for the tool call and one for the last completion
# after the maximum_auto_invoke_attempts is reached
assert mock_vertex_ai_model_generate_content_async.call_count == 2
assert len(responses) == 1
assert responses[0].role == "assistant"
# Google doesn't return STOP as the finish reason for tool calls
assert responses[0].finish_reason == FinishReason.STOP
@patch.object(GenerativeModel, "generate_content_async", new_callable=AsyncMock)
async def test_vertex_ai_chat_completion_with_function_choice_behavior_no_tool_call(
mock_vertex_ai_model_generate_content_async,
vertex_ai_unit_test_env,
kernel,
chat_history: ChatHistory,
mock_vertex_ai_chat_completion_response,
) -> None:
"""Test completion of VertexAIChatCompletion with function choice behavior but no tool call returned"""
mock_vertex_ai_model_generate_content_async.return_value = mock_vertex_ai_chat_completion_response
settings = VertexAIChatPromptExecutionSettings(
function_choice_behavior=FunctionChoiceBehavior.Auto(),
)
settings.function_choice_behavior.maximum_auto_invoke_attempts = 1
vertex_ai_chat_completion = VertexAIChatCompletion()
responses = await vertex_ai_chat_completion.get_chat_message_contents(
chat_history=chat_history,
settings=settings,
kernel=kernel,
)
# Verify the call was made once
mock_vertex_ai_model_generate_content_async.assert_awaited_once()
# Get the actual call arguments
call_args = mock_vertex_ai_model_generate_content_async.await_args
# Verify the contents
contents = call_args.kwargs["contents"]
assert len(contents) == 1
assert contents[0].role == "user"
assert len(contents[0].parts) == 1
assert contents[0].parts[0].text == "test_prompt"
# Verify other arguments
assert call_args.kwargs["generation_config"] == settings.prepare_settings_dict()
assert call_args.kwargs["tools"] is None
assert call_args.kwargs["tool_config"] is None
assert len(responses) == 1
assert responses[0].role == "assistant"
assert responses[0].content == mock_vertex_ai_chat_completion_response.candidates[0].content.parts[0].text
# endregion chat completion
# region streaming chat completion
@patch.object(GenerativeModel, "generate_content_async", new_callable=AsyncMock)
async def test_vertex_ai_streaming_chat_completion(
mock_vertex_ai_model_generate_content_async,
vertex_ai_unit_test_env,
chat_history: ChatHistory,
mock_vertex_ai_streaming_chat_completion_response,
) -> None:
"""Test chat completion with VertexAIChatCompletion"""
settings = VertexAIChatPromptExecutionSettings()
mock_vertex_ai_model_generate_content_async.return_value = mock_vertex_ai_streaming_chat_completion_response
vertex_ai_chat_completion = VertexAIChatCompletion()
async for messages in vertex_ai_chat_completion.get_streaming_chat_message_contents(chat_history, settings):
assert len(messages) == 1
assert messages[0].role == "assistant"
assert messages[0].finish_reason == FinishReason.STOP
assert "usage" in messages[0].metadata
assert "prompt_feedback" in messages[0].metadata
# Verify the call was made once
mock_vertex_ai_model_generate_content_async.assert_called_once()
# Get the actual call arguments
call_args = mock_vertex_ai_model_generate_content_async.call_args
# Verify the contents
contents = call_args.kwargs["contents"]
assert len(contents) == 1
assert contents[0].role == "user"
assert len(contents[0].parts) == 1
assert contents[0].parts[0].text == "test_prompt"
# Verify other arguments
assert call_args.kwargs["generation_config"] == settings.prepare_settings_dict()
assert call_args.kwargs["tools"] is None
assert call_args.kwargs["tool_config"] is None
assert call_args.kwargs["stream"] is True
async def test_vertex_ai_streaming_chat_completion_with_function_choice_behavior_fail_verification(
chat_history: ChatHistory,
vertex_ai_unit_test_env,
) -> None:
"""Test streaming chat completion of VertexAIChatCompletion with function choice
behavior expect verification failure"""
# Missing kernel
with pytest.raises(ServiceInvalidExecutionSettingsError):
settings = VertexAIChatPromptExecutionSettings(
function_choice_behavior=FunctionChoiceBehavior.Auto(),
)
vertex_ai_chat_completion = VertexAIChatCompletion()
async for _ in vertex_ai_chat_completion.get_streaming_chat_message_contents(
chat_history=chat_history,
settings=settings,
):
pass
@patch.object(GenerativeModel, "generate_content_async", new_callable=AsyncMock)
async def test_vertex_ai_streaming_chat_completion_with_function_choice_behavior(
mock_vertex_ai_model_generate_content_async,
vertex_ai_unit_test_env,
kernel: Kernel,
chat_history: ChatHistory,
mock_vertex_ai_streaming_chat_completion_response_with_tool_call,
decorated_native_function,
) -> None:
"""Test streaming chat completion of VertexAIChatCompletion with function choice behavior"""
mock_vertex_ai_model_generate_content_async.return_value = (
mock_vertex_ai_streaming_chat_completion_response_with_tool_call
)
settings = VertexAIChatPromptExecutionSettings(
function_choice_behavior=FunctionChoiceBehavior.Auto(),
)
settings.function_choice_behavior.maximum_auto_invoke_attempts = 1
vertex_ai_chat_completion = VertexAIChatCompletion()
kernel.add_function(plugin_name="TestPlugin", function=decorated_native_function)
all_messages = []
async for messages in vertex_ai_chat_completion.get_streaming_chat_message_contents(
chat_history,
settings,
kernel=kernel,
):
all_messages.extend(messages)
assert len(all_messages) == 2, f"Expected 2 messages, got {len(all_messages)}"
# Validate the first message
assert all_messages[0].role == "assistant", f"Unexpected role for first message: {all_messages[0].role}"
assert all_messages[0].content == "", f"Unexpected content for first message: {all_messages[0].content}"
assert all_messages[0].finish_reason == FinishReason.STOP, (
f"Unexpected finish reason for first message: {all_messages[0].finish_reason}"
)
# Validate the second message
assert all_messages[1].role == "tool", f"Unexpected role for second message: {all_messages[1].role}"
assert all_messages[1].content == "", f"Unexpected content for second message: {all_messages[1].content}"
assert all_messages[1].finish_reason is None
@patch.object(GenerativeModel, "generate_content_async", new_callable=AsyncMock)
async def test_vertex_ai_streaming_chat_completion_with_function_choice_behavior_no_tool_call(
mock_vertex_ai_model_generate_content_async,
vertex_ai_unit_test_env,
kernel,
chat_history: ChatHistory,
mock_vertex_ai_streaming_chat_completion_response,
) -> None:
"""Test completion of VertexAIChatCompletion with function choice behavior but no tool call returned"""
mock_vertex_ai_model_generate_content_async.return_value = mock_vertex_ai_streaming_chat_completion_response
settings = VertexAIChatPromptExecutionSettings(
function_choice_behavior=FunctionChoiceBehavior.Auto(),
)
settings.function_choice_behavior.maximum_auto_invoke_attempts = 1
vertex_ai_chat_completion = VertexAIChatCompletion()
async for messages in vertex_ai_chat_completion.get_streaming_chat_message_contents(
chat_history=chat_history,
settings=settings,
kernel=kernel,
):
assert len(messages) == 1
assert messages[0].role == "assistant"
# Verify the call was made once
mock_vertex_ai_model_generate_content_async.assert_awaited_once()
# Get the actual call arguments
call_args = mock_vertex_ai_model_generate_content_async.await_args
# Verify the contents
contents = call_args.kwargs["contents"]
assert len(contents) == 1
assert contents[0].role == "user"
assert len(contents[0].parts) == 1
assert contents[0].parts[0].text == "test_prompt"
# Verify other arguments
assert call_args.kwargs["generation_config"] == settings.prepare_settings_dict()
assert call_args.kwargs["tools"] is None
assert call_args.kwargs["tool_config"] is None
assert call_args.kwargs["stream"] is True
# endregion streaming chat completion
def test_vertex_ai_chat_completion_parse_chat_history_correctly(vertex_ai_unit_test_env) -> None:
"""Test _prepare_chat_history_for_request method"""
vertex_ai_chat_completion = VertexAIChatCompletion()
chat_history = ChatHistory()
chat_history.add_system_message("test_system_message")
chat_history.add_user_message("test_user_message")
chat_history.add_assistant_message("test_assistant_message")
parsed_chat_history = vertex_ai_chat_completion._prepare_chat_history_for_request(chat_history)
assert isinstance(parsed_chat_history, list)
# System message should be ignored
assert len(parsed_chat_history) == 2
assert all(isinstance(message, Content) for message in parsed_chat_history)
assert parsed_chat_history[0].role == "user"
assert parsed_chat_history[0].parts[0].text == "test_user_message"
assert parsed_chat_history[1].role == "model"
assert parsed_chat_history[1].parts[0].text == "test_assistant_message"
# region thought_signature deserialization tests
def test_create_chat_message_content_with_thought_signature(vertex_ai_unit_test_env) -> None:
"""Test that thought_signature from a Part dict is deserialized into FunctionCallContent.metadata."""
from unittest.mock import MagicMock
from google.cloud.aiplatform_v1beta1.types.content import Candidate as GapicCandidate
from semantic_kernel.contents.function_call_content import FunctionCallContent
thought_sig_value = "vertex-test-thought-sig"
# Create a mock Part whose to_dict() returns thought_signature
mock_part = MagicMock()
mock_part.text = None
mock_part.to_dict.return_value = {
"function_call": {"name": "test_function", "args": {"key": "value"}},
"thought_signature": thought_sig_value,
}
mock_part.function_call.name = "test_function"
mock_part.function_call.args = {"key": "value"}
# Build a mock candidate with the mock part
mock_candidate = MagicMock()
mock_candidate.index = 0
mock_candidate.content.parts = [mock_part]
mock_candidate.finish_reason = GapicCandidate.FinishReason.STOP
# Build a mock response
mock_response = MagicMock()
completion = VertexAIChatCompletion()
result = completion._create_chat_message_content(mock_response, mock_candidate)
fc_items = [item for item in result.items if isinstance(item, FunctionCallContent)]
assert len(fc_items) == 1
assert fc_items[0].metadata is not None
assert fc_items[0].metadata["thought_signature"] == thought_sig_value
def test_create_chat_message_content_without_thought_signature(vertex_ai_unit_test_env) -> None:
"""Test that FunctionCallContent works when Part dict has no thought_signature."""
from unittest.mock import MagicMock
from google.cloud.aiplatform_v1beta1.types.content import Candidate as GapicCandidate
from semantic_kernel.contents.function_call_content import FunctionCallContent
mock_part = MagicMock()
mock_part.text = None
mock_part.to_dict.return_value = {
"function_call": {"name": "test_function", "args": {"key": "value"}},
}
mock_part.function_call.name = "test_function"
mock_part.function_call.args = {"key": "value"}
mock_candidate = MagicMock()
mock_candidate.index = 0
mock_candidate.content.parts = [mock_part]
mock_candidate.finish_reason = GapicCandidate.FinishReason.STOP
mock_response = MagicMock()
completion = VertexAIChatCompletion()
result = completion._create_chat_message_content(mock_response, mock_candidate)
fc_items = [item for item in result.items if isinstance(item, FunctionCallContent)]
assert len(fc_items) == 1
assert "thought_signature" not in fc_items[0].metadata
def test_create_streaming_chat_message_content_with_thought_signature(vertex_ai_unit_test_env) -> None:
"""Test that thought_signature from a Part dict is deserialized in streaming path."""
from unittest.mock import MagicMock
from google.cloud.aiplatform_v1beta1.types.content import Candidate as GapicCandidate
from semantic_kernel.contents.function_call_content import FunctionCallContent
thought_sig_value = "vertex-streaming-thought-sig"
mock_part = MagicMock()
mock_part.text = None
mock_part.to_dict.return_value = {
"function_call": {"name": "stream_func", "args": {"a": "b"}},
"thought_signature": thought_sig_value,
}
mock_part.function_call.name = "stream_func"
mock_part.function_call.args = {"a": "b"}
mock_candidate = MagicMock()
mock_candidate.index = 0
mock_candidate.content.parts = [mock_part]
mock_candidate.finish_reason = GapicCandidate.FinishReason.STOP
mock_chunk = MagicMock()
completion = VertexAIChatCompletion()
result = completion._create_streaming_chat_message_content(mock_chunk, mock_candidate, function_invoke_attempt=0)
fc_items = [item for item in result.items if isinstance(item, FunctionCallContent)]
assert len(fc_items) == 1
assert fc_items[0].metadata is not None
assert fc_items[0].metadata["thought_signature"] == thought_sig_value
def test_create_streaming_chat_message_content_without_thought_signature(vertex_ai_unit_test_env) -> None:
"""Test that streaming FunctionCallContent works when Part dict lacks thought_signature."""
from unittest.mock import MagicMock
from google.cloud.aiplatform_v1beta1.types.content import Candidate as GapicCandidate
from semantic_kernel.contents.function_call_content import FunctionCallContent
mock_part = MagicMock()
mock_part.text = None
mock_part.to_dict.return_value = {
"function_call": {"name": "stream_func", "args": {"a": "b"}},
}
mock_part.function_call.name = "stream_func"
mock_part.function_call.args = {"a": "b"}
mock_candidate = MagicMock()
mock_candidate.index = 0
mock_candidate.content.parts = [mock_part]
mock_candidate.finish_reason = GapicCandidate.FinishReason.STOP
mock_chunk = MagicMock()
completion = VertexAIChatCompletion()
result = completion._create_streaming_chat_message_content(mock_chunk, mock_candidate, function_invoke_attempt=0)
fc_items = [item for item in result.items if isinstance(item, FunctionCallContent)]
assert len(fc_items) == 1
assert "thought_signature" not in fc_items[0].metadata
# endregion thought_signature deserialization tests
@@ -0,0 +1,130 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import AsyncMock, patch
import pytest
from vertexai.generative_models import GenerativeModel
from semantic_kernel.connectors.ai.google.vertex_ai.services.vertex_ai_text_completion import VertexAITextCompletion
from semantic_kernel.connectors.ai.google.vertex_ai.vertex_ai_prompt_execution_settings import (
VertexAITextPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.google.vertex_ai.vertex_ai_settings import VertexAISettings
from semantic_kernel.contents.text_content import TextContent
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError
# region init
def test_vertex_ai_text_completion_init(vertex_ai_unit_test_env) -> None:
"""Test initialization of VertexAITextCompletion"""
model_id = vertex_ai_unit_test_env["VERTEX_AI_GEMINI_MODEL_ID"]
project_id = vertex_ai_unit_test_env["VERTEX_AI_PROJECT_ID"]
vertex_ai_text_completion = VertexAITextCompletion()
assert vertex_ai_text_completion.ai_model_id == model_id
assert vertex_ai_text_completion.service_id == model_id
assert isinstance(vertex_ai_text_completion.service_settings, VertexAISettings)
assert vertex_ai_text_completion.service_settings.gemini_model_id == model_id
assert vertex_ai_text_completion.service_settings.project_id == project_id
def test_vertex_ai_text_completion_init_with_service_id(vertex_ai_unit_test_env, service_id) -> None:
"""Test initialization of VertexAITextCompletion with a service id that is not the model id"""
vertex_ai_text_completion = VertexAITextCompletion(service_id=service_id)
assert vertex_ai_text_completion.service_id == service_id
def test_vertex_ai_text_completion_init_with_model_id_in_argument(vertex_ai_unit_test_env) -> None:
"""Test initialization of VertexAIChatCompletion with model id in argument"""
vertex_ai_text_completion = VertexAITextCompletion(gemini_model_id="custom_model_id")
assert vertex_ai_text_completion.ai_model_id == "custom_model_id"
assert vertex_ai_text_completion.service_id == "custom_model_id"
@pytest.mark.parametrize("exclude_list", [["VERTEX_AI_GEMINI_MODEL_ID"]], indirect=True)
def test_vertex_ai_text_completion_init_with_empty_model_id(vertex_ai_unit_test_env) -> None:
"""Test initialization of VertexAITextCompletion with an empty model id"""
with pytest.raises(ServiceInitializationError):
VertexAITextCompletion(env_file_path="fake_env_file_path.env")
@pytest.mark.parametrize("exclude_list", [["VERTEX_AI_PROJECT_ID"]], indirect=True)
def test_vertex_ai_text_completion_init_with_empty_project_id(vertex_ai_unit_test_env) -> None:
"""Test initialization of VertexAITextCompletion with an empty project id"""
with pytest.raises(ServiceInitializationError):
VertexAITextCompletion(env_file_path="fake_env_file_path.env")
def test_prompt_execution_settings_class(vertex_ai_unit_test_env) -> None:
vertex_ai_text_completion = VertexAITextCompletion()
assert vertex_ai_text_completion.get_prompt_execution_settings_class() == VertexAITextPromptExecutionSettings
# endregion init
# region text completion
@patch.object(GenerativeModel, "generate_content_async", new_callable=AsyncMock)
async def test_vertex_ai_text_completion(
mock_vertex_ai_model_generate_content_async,
vertex_ai_unit_test_env,
prompt: str,
mock_vertex_ai_text_completion_response,
) -> None:
"""Test text completion with VertexAITextCompletion"""
settings = VertexAITextPromptExecutionSettings()
mock_vertex_ai_model_generate_content_async.return_value = mock_vertex_ai_text_completion_response
vertex_ai_text_completion = VertexAITextCompletion()
responses: list[TextContent] = await vertex_ai_text_completion.get_text_contents(prompt, settings)
mock_vertex_ai_model_generate_content_async.assert_called_once_with(
contents=prompt,
generation_config=settings.prepare_settings_dict(),
)
assert len(responses) == 1
assert responses[0].text == mock_vertex_ai_text_completion_response.candidates[0].content.parts[0].text
assert "usage" in responses[0].metadata
assert "prompt_feedback" in responses[0].metadata
assert responses[0].inner_content == mock_vertex_ai_text_completion_response
# endregion text completion
# region streaming text completion
@patch.object(GenerativeModel, "generate_content_async", new_callable=AsyncMock)
async def test_vertex_ai_streaming_text_completion(
mock_vertex_ai_model_generate_content_async,
vertex_ai_unit_test_env,
prompt: str,
mock_vertex_ai_streaming_text_completion_response,
) -> None:
"""Test streaming text completion with VertexAITextCompletion"""
settings = VertexAITextPromptExecutionSettings()
mock_vertex_ai_model_generate_content_async.return_value = mock_vertex_ai_streaming_text_completion_response
vertex_ai_text_completion = VertexAITextCompletion()
async for chunks in vertex_ai_text_completion.get_streaming_text_contents(prompt, settings):
assert len(chunks) == 1
assert "usage" in chunks[0].metadata
assert "prompt_feedback" in chunks[0].metadata
mock_vertex_ai_model_generate_content_async.assert_called_once_with(
contents=prompt,
generation_config=settings.prepare_settings_dict(),
stream=True,
)
# endregion streaming text completion
@@ -0,0 +1,160 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import AsyncMock, patch
import pytest
from numpy import array, ndarray
from vertexai.language_models import TextEmbedding, TextEmbeddingModel
from semantic_kernel.connectors.ai.google.vertex_ai.services.vertex_ai_text_embedding import VertexAITextEmbedding
from semantic_kernel.connectors.ai.google.vertex_ai.vertex_ai_prompt_execution_settings import (
VertexAIEmbeddingPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.google.vertex_ai.vertex_ai_settings import VertexAISettings
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError
from tests.unit.connectors.ai.google.vertex_ai.conftest import MockTextEmbeddingModel
# region init
def test_vertex_ai_text_embedding_init(vertex_ai_unit_test_env) -> None:
"""Test initialization of VertexAITextEmbedding"""
model_id = vertex_ai_unit_test_env["VERTEX_AI_EMBEDDING_MODEL_ID"]
project_id = vertex_ai_unit_test_env["VERTEX_AI_PROJECT_ID"]
vertex_ai_text_embedding = VertexAITextEmbedding()
assert vertex_ai_text_embedding.ai_model_id == model_id
assert vertex_ai_text_embedding.service_id == model_id
assert isinstance(vertex_ai_text_embedding.service_settings, VertexAISettings)
assert vertex_ai_text_embedding.service_settings.embedding_model_id == model_id
assert vertex_ai_text_embedding.service_settings.project_id == project_id
def test_vertex_ai_text_embedding_init_with_service_id(vertex_ai_unit_test_env, service_id) -> None:
"""Test initialization of VertexAITextEmbedding with a service id that is not the model id"""
vertex_ai_text_embedding = VertexAITextEmbedding(service_id=service_id)
assert vertex_ai_text_embedding.service_id == service_id
def test_vertex_ai_text_embedding_init_with_model_id_in_argument(vertex_ai_unit_test_env) -> None:
"""Test initialization of VertexAITextEmbedding with model id in argument"""
vertex_ai_chat_completion = VertexAITextEmbedding(embedding_model_id="custom_model_id")
assert vertex_ai_chat_completion.ai_model_id == "custom_model_id"
assert vertex_ai_chat_completion.service_id == "custom_model_id"
@pytest.mark.parametrize("exclude_list", [["VERTEX_AI_EMBEDDING_MODEL_ID"]], indirect=True)
def test_vertex_ai_text_embedding_init_with_empty_model_id(vertex_ai_unit_test_env) -> None:
"""Test initialization of VertexAITextEmbedding with an empty model id"""
with pytest.raises(ServiceInitializationError):
VertexAITextEmbedding(env_file_path="fake_env_file_path.env")
@pytest.mark.parametrize("exclude_list", [["VERTEX_AI_PROJECT_ID"]], indirect=True)
def test_vertex_ai_text_embedding_init_with_empty_project_id(vertex_ai_unit_test_env) -> None:
"""Test initialization of VertexAITextEmbedding with an empty project id"""
with pytest.raises(ServiceInitializationError):
VertexAITextEmbedding(env_file_path="fake_env_file_path.env")
def test_prompt_execution_settings_class(vertex_ai_unit_test_env) -> None:
vertex_ai_text_embedding = VertexAITextEmbedding()
assert vertex_ai_text_embedding.get_prompt_execution_settings_class() == VertexAIEmbeddingPromptExecutionSettings
# endregion init
@patch.object(TextEmbeddingModel, "from_pretrained")
@patch.object(MockTextEmbeddingModel, "get_embeddings_async", new_callable=AsyncMock)
async def test_embedding(mock_embedding_client, mock_from_pretrained, vertex_ai_unit_test_env, prompt):
"""Test that the service initializes and generates embeddings correctly."""
mock_from_pretrained.return_value = MockTextEmbeddingModel()
mock_embedding_client.return_value = [TextEmbedding(values=[0.1, 0.2, 0.3])]
settings = VertexAIEmbeddingPromptExecutionSettings()
vertex_ai_text_embedding = VertexAITextEmbedding()
response: ndarray = await vertex_ai_text_embedding.generate_embeddings(
[prompt],
settings=settings,
)
assert len(response) == 1
assert response.all() == array([0.1, 0.2, 0.3]).all()
mock_embedding_client.assert_called_once_with([prompt])
@patch.object(TextEmbeddingModel, "from_pretrained")
@patch.object(MockTextEmbeddingModel, "get_embeddings_async", new_callable=AsyncMock)
async def test_embedding_with_settings(mock_embedding_client, mock_from_pretrained, vertex_ai_unit_test_env, prompt):
"""Test that the service initializes and generates embeddings correctly."""
mock_from_pretrained.return_value = MockTextEmbeddingModel()
mock_embedding_client.return_value = [TextEmbedding(values=[0.1, 0.2, 0.3])]
settings = VertexAIEmbeddingPromptExecutionSettings()
settings.output_dimensionality = 3
settings.auto_truncate = True
vertex_ai_text_embedding = VertexAITextEmbedding()
response: ndarray = await vertex_ai_text_embedding.generate_embeddings(
[prompt],
settings=settings,
)
assert len(response) == 1
assert response.all() == array([0.1, 0.2, 0.3]).all()
mock_embedding_client.assert_called_once_with(
[prompt],
**settings.prepare_settings_dict(),
)
@patch.object(TextEmbeddingModel, "from_pretrained")
@patch.object(MockTextEmbeddingModel, "get_embeddings_async", new_callable=AsyncMock)
async def test_embedding_without_settings(mock_embedding_client, mock_from_pretrained, vertex_ai_unit_test_env, prompt):
"""Test that the service initializes and generates embeddings correctly without settings."""
mock_from_pretrained.return_value = MockTextEmbeddingModel()
mock_embedding_client.return_value = [TextEmbedding(values=[0.1, 0.2, 0.3])]
vertex_ai_text_embedding = VertexAITextEmbedding()
response: ndarray = await vertex_ai_text_embedding.generate_embeddings([prompt])
assert len(response) == 1
assert response.all() == array([0.1, 0.2, 0.3]).all()
mock_embedding_client.assert_called_once_with([prompt])
@patch.object(TextEmbeddingModel, "from_pretrained")
@patch.object(MockTextEmbeddingModel, "get_embeddings_async", new_callable=AsyncMock)
async def test_embedding_list_input(mock_embedding_client, mock_from_pretrained, vertex_ai_unit_test_env, prompt):
"""Test that the service initializes and generates embeddings correctly with a list of prompts."""
mock_from_pretrained.return_value = MockTextEmbeddingModel()
mock_embedding_client.return_value = [TextEmbedding(values=[0.1, 0.2, 0.3]), TextEmbedding(values=[0.1, 0.2, 0.3])]
vertex_ai_text_embedding = VertexAITextEmbedding()
response: ndarray = await vertex_ai_text_embedding.generate_embeddings([prompt, prompt])
assert len(response) == 2
assert response.all() == array([[0.1, 0.2, 0.3], [0.1, 0.2, 0.3]]).all()
mock_embedding_client.assert_called_once_with([prompt, prompt])
@patch.object(TextEmbeddingModel, "from_pretrained")
@patch.object(MockTextEmbeddingModel, "get_embeddings_async", new_callable=AsyncMock)
async def test_raw_embedding(mock_embedding_client, mock_from_pretrained, vertex_ai_unit_test_env, prompt):
"""Test that the service initializes and generates embeddings correctly."""
mock_from_pretrained.return_value = MockTextEmbeddingModel()
mock_embedding_client.return_value = [TextEmbedding(values=[0.1, 0.2, 0.3])]
settings = VertexAIEmbeddingPromptExecutionSettings()
vertex_ai_text_embedding = VertexAITextEmbedding()
response: ndarray = await vertex_ai_text_embedding.generate_raw_embeddings([prompt], settings)
assert len(response) == 1
assert response[0] == [0.1, 0.2, 0.3]
mock_embedding_client.assert_called_once_with([prompt])
@@ -0,0 +1,199 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from google.cloud.aiplatform_v1beta1.types.content import Candidate
from vertexai.generative_models import FunctionDeclaration, Part
from semantic_kernel.connectors.ai.google.vertex_ai.services.utils import (
finish_reason_from_vertex_ai_to_semantic_kernel,
format_assistant_message,
format_user_message,
kernel_function_metadata_to_vertex_ai_function_call_format,
)
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.contents.utils.finish_reason import FinishReason
from semantic_kernel.exceptions.service_exceptions import ServiceInvalidRequestError
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata
from semantic_kernel.functions.kernel_parameter_metadata import KernelParameterMetadata
def test_finish_reason_from_vertex_ai_to_semantic_kernel():
"""Test finish_reason_from_vertex_ai_to_semantic_kernel."""
assert finish_reason_from_vertex_ai_to_semantic_kernel(Candidate.FinishReason.STOP) == FinishReason.STOP
assert finish_reason_from_vertex_ai_to_semantic_kernel(Candidate.FinishReason.MAX_TOKENS) == FinishReason.LENGTH
assert finish_reason_from_vertex_ai_to_semantic_kernel(Candidate.FinishReason.SAFETY) == FinishReason.CONTENT_FILTER
assert finish_reason_from_vertex_ai_to_semantic_kernel(Candidate.FinishReason.OTHER) is None
def test_format_user_message():
"""Test format_user_message."""
user_message = ChatMessageContent(role=AuthorRole.USER, content="User message")
formatted_user_message = format_user_message(user_message)
assert len(formatted_user_message) == 1
assert isinstance(formatted_user_message[0], Part)
assert formatted_user_message[0].text == "User message"
# Test with an image content
image_content = ImageContent(data="image data", mime_type="image/png")
user_message = ChatMessageContent(
role=AuthorRole.USER,
items=[
TextContent(text="Text content"),
image_content,
],
)
formatted_user_message = format_user_message(user_message)
assert len(formatted_user_message) == 2
assert isinstance(formatted_user_message[0], Part)
assert formatted_user_message[0].text == "Text content"
assert isinstance(formatted_user_message[1], Part)
assert formatted_user_message[1].inline_data.mime_type == "image/png"
assert formatted_user_message[1].inline_data.data == image_content.data
def test_format_user_message_throws_with_unsupported_items() -> None:
"""Test format_user_message with unsupported items."""
# Test with unsupported items, any item other than TextContent and ImageContent should raise an error
user_message = ChatMessageContent(
role=AuthorRole.USER,
items=[
FunctionCallContent(),
],
)
with pytest.raises(ServiceInvalidRequestError):
format_user_message(user_message)
# Test with an ImageContent that has no data_uri
user_message = ChatMessageContent(
role=AuthorRole.USER,
items=[
ImageContent(data_uri=""),
],
)
with pytest.raises(ServiceInvalidRequestError):
format_user_message(user_message)
def test_format_assistant_message() -> None:
assistant_message = ChatMessageContent(
role=AuthorRole.ASSISTANT,
items=[
TextContent(text="test"),
FunctionCallContent(name="test_function", arguments={}),
ImageContent(data="image data", mime_type="image/png"),
],
)
formatted_assistant_message = format_assistant_message(assistant_message)
assert isinstance(formatted_assistant_message, list)
assert len(formatted_assistant_message) == 3
assert isinstance(formatted_assistant_message[0], Part)
assert formatted_assistant_message[0].text == "test"
assert isinstance(formatted_assistant_message[1], Part)
assert formatted_assistant_message[1].function_call.name == "test_function"
assert formatted_assistant_message[1].function_call.args == {}
assert isinstance(formatted_assistant_message[2], Part)
assert formatted_assistant_message[2].inline_data
def test_format_assistant_message_with_unsupported_items() -> None:
assistant_message = ChatMessageContent(
role=AuthorRole.ASSISTANT,
items=[
FunctionResultContent(id="test_id", function_name="test_function"),
],
)
with pytest.raises(ServiceInvalidRequestError):
format_assistant_message(assistant_message)
def test_format_assistant_message_with_thought_signature() -> None:
"""Test that thought_signature is preserved in function call parts for Vertex AI."""
import base64
thought_sig = base64.b64encode(b"test_thought_signature_data").decode("utf-8")
assistant_message = ChatMessageContent(
role=AuthorRole.ASSISTANT,
items=[
FunctionCallContent(
name="test_function",
arguments={"arg1": "value1"},
metadata={"thought_signature": thought_sig},
),
],
)
formatted = format_assistant_message(assistant_message)
assert len(formatted) == 1
assert isinstance(formatted[0], Part)
assert formatted[0].function_call.name == "test_function"
assert formatted[0].function_call.args == {"arg1": "value1"}
part_dict = formatted[0].to_dict()
assert "thought_signature" in part_dict
assert part_dict["thought_signature"] == thought_sig
def test_format_assistant_message_without_thought_signature() -> None:
"""Test that function calls without thought_signature still work for Vertex AI."""
assistant_message = ChatMessageContent(
role=AuthorRole.ASSISTANT,
items=[
FunctionCallContent(
name="test_function",
arguments={"arg1": "value1"},
),
],
)
formatted = format_assistant_message(assistant_message)
assert len(formatted) == 1
assert isinstance(formatted[0], Part)
assert formatted[0].function_call.name == "test_function"
assert formatted[0].function_call.args == {"arg1": "value1"}
part_dict = formatted[0].to_dict()
assert "thought_signature" not in part_dict
def test_vertex_ai_function_call_format_sanitizes_anyof_schema() -> None:
"""Integration test: anyOf in param schema_data is sanitized in the FunctionDeclaration."""
metadata = KernelFunctionMetadata(
name="test_func",
description="A test function",
is_prompt=False,
parameters=[
KernelParameterMetadata(
name="messages",
description="The user messages",
is_required=True,
schema_data={
"anyOf": [
{"type": "string"},
{"type": "array", "items": {"type": "string"}},
],
"description": "The user messages",
},
),
],
)
result = kernel_function_metadata_to_vertex_ai_function_call_format(metadata)
assert isinstance(result, FunctionDeclaration)
def test_vertex_ai_function_call_format_empty_parameters() -> None:
"""Integration test: metadata with no parameters produces empty properties, no crash."""
metadata = KernelFunctionMetadata(
name="no_params_func",
description="No parameters",
is_prompt=False,
parameters=[],
)
result = kernel_function_metadata_to_vertex_ai_function_call_format(metadata)
assert isinstance(result, FunctionDeclaration)
@@ -0,0 +1,136 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.connectors.ai.google.vertex_ai import (
VertexAIChatPromptExecutionSettings,
VertexAIPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.google.vertex_ai.vertex_ai_prompt_execution_settings import (
VertexAIEmbeddingPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
def test_default_vertex_ai_prompt_execution_settings():
settings = VertexAIPromptExecutionSettings()
assert settings.stop_sequences is None
assert settings.response_mime_type is None
assert settings.response_schema is None
assert settings.candidate_count is None
assert settings.max_output_tokens is None
assert settings.temperature is None
assert settings.top_p is None
assert settings.top_k is None
def test_custom_vertex_ai_prompt_execution_settings():
settings = VertexAIPromptExecutionSettings(
stop_sequences=["world"],
response_mime_type="text/plain",
candidate_count=1,
max_output_tokens=128,
temperature=0.5,
top_p=0.5,
top_k=10,
)
assert settings.stop_sequences == ["world"]
assert settings.response_mime_type == "text/plain"
assert settings.candidate_count == 1
assert settings.max_output_tokens == 128
assert settings.temperature == 0.5
assert settings.top_p == 0.5
assert settings.top_k == 10
def test_vertex_ai_prompt_execution_settings_from_default_completion_config():
settings = PromptExecutionSettings(service_id="test_service")
chat_settings = VertexAIChatPromptExecutionSettings.from_prompt_execution_settings(settings)
assert chat_settings.service_id == "test_service"
assert chat_settings.stop_sequences is None
assert chat_settings.response_mime_type is None
assert chat_settings.response_schema is None
assert chat_settings.candidate_count is None
assert chat_settings.max_output_tokens is None
assert chat_settings.temperature is None
assert chat_settings.top_p is None
assert chat_settings.top_k is None
def test_vertex_ai_prompt_execution_settings_from_openai_prompt_execution_settings():
chat_settings = VertexAIChatPromptExecutionSettings(service_id="test_service", temperature=1.0)
new_settings = VertexAIPromptExecutionSettings(service_id="test_2", temperature=0.0)
chat_settings.update_from_prompt_execution_settings(new_settings)
assert chat_settings.service_id == "test_2"
assert chat_settings.temperature == 0.0
def test_vertex_ai_prompt_execution_settings_from_custom_completion_config():
settings = PromptExecutionSettings(
service_id="test_service",
extension_data={
"stop_sequences": ["world"],
"response_mime_type": "text/plain",
"candidate_count": 1,
"max_output_tokens": 128,
"temperature": 0.5,
"top_p": 0.5,
"top_k": 10,
},
)
chat_settings = VertexAIChatPromptExecutionSettings.from_prompt_execution_settings(settings)
assert chat_settings.stop_sequences == ["world"]
assert chat_settings.response_mime_type == "text/plain"
assert chat_settings.candidate_count == 1
assert chat_settings.max_output_tokens == 128
assert chat_settings.temperature == 0.5
assert chat_settings.top_p == 0.5
assert chat_settings.top_k == 10
def test_vertex_ai_chat_prompt_execution_settings_from_custom_completion_config_with_functions():
settings = PromptExecutionSettings(
service_id="test_service",
extension_data={
"tools": [],
},
)
chat_settings = VertexAIChatPromptExecutionSettings.from_prompt_execution_settings(settings)
assert chat_settings.tools == []
def test_create_options():
settings = VertexAIChatPromptExecutionSettings(
service_id="test_service",
extension_data={
"stop_sequences": ["world"],
"response_mime_type": "text/plain",
"candidate_count": 1,
"max_output_tokens": 128,
"temperature": 0.5,
"top_p": 0.5,
"top_k": 10,
},
)
options = settings.prepare_settings_dict()
assert options["stop_sequences"] == ["world"]
assert options["response_mime_type"] == "text/plain"
assert options["candidate_count"] == 1
assert options["max_output_tokens"] == 128
assert options["temperature"] == 0.5
assert options["top_p"] == 0.5
assert options["top_k"] == 10
assert "tools" not in options
assert "tool_config" not in options
def test_default_vertex_ai_embedding_prompt_execution_settings():
settings = VertexAIEmbeddingPromptExecutionSettings()
assert settings.output_dimensionality is None
assert settings.auto_truncate is None
@@ -0,0 +1,219 @@
# Copyright (c) Microsoft. All rights reserved.
from threading import Thread
from unittest.mock import MagicMock, Mock, patch
import pytest
from transformers import AutoTokenizer, TextIteratorStreamer
from semantic_kernel.connectors.ai.hugging_face.services.hf_text_completion import HuggingFaceTextCompletion
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.exceptions import KernelInvokeException, ServiceResponseException
from semantic_kernel.functions.kernel_arguments import KernelArguments
from semantic_kernel.kernel import Kernel
from semantic_kernel.prompt_template.prompt_template_config import PromptTemplateConfig
@pytest.mark.parametrize(
("model_name", "task", "input_str"),
[
(
"patrickvonplaten/t5-tiny-random",
"text2text-generation",
"translate English to Dutch: Hello, how are you?",
),
(
"Falconsai/text_summarization",
"summarization",
"""
Summarize: Whales are fully aquatic, open-ocean animals:
they can feed, mate, give birth, suckle and raise their young at sea.
Whales range in size from the 2.6 metres (8.5 ft) and 135 kilograms (298 lb)
dwarf sperm whale to the 29.9 metres (98 ft) and 190 tonnes (210 short tons) blue whale,
which is the largest known animal that has ever lived. The sperm whale is the largest
toothed predator on Earth. Several whale species exhibit sexual dimorphism,
in that the females are larger than males.
""",
),
("HuggingFaceM4/tiny-random-LlamaForCausalLM", "text-generation", "Hello, I like sleeping and "),
],
ids=["text2text-generation", "summarization", "text-generation"],
)
async def test_text_completion(model_name, task, input_str):
kernel = Kernel()
ret = {"summary_text": "test"} if task == "summarization" else {"generated_text": "test"}
mock_pipeline = Mock(return_value=ret)
# Configure LLM service
with patch("semantic_kernel.connectors.ai.hugging_face.services.hf_text_completion.pipeline") as patched_pipeline:
patched_pipeline.return_value = mock_pipeline
service = HuggingFaceTextCompletion(service_id=model_name, ai_model_id=model_name, task=task)
kernel.add_service(
service=service,
)
exec_settings = PromptExecutionSettings(service_id=model_name, extension_data={"max_new_tokens": 25})
# Define semantic function using SK prompt template language
prompt = "{{$input}}"
prompt_template_config = PromptTemplateConfig(template=prompt, execution_settings=exec_settings)
kernel.add_function(
prompt_template_config=prompt_template_config,
function_name="TestFunction",
plugin_name="TestPlugin",
prompt_execution_settings=exec_settings,
)
arguments = KernelArguments(input=input_str)
await kernel.invoke(function_name="TestFunction", plugin_name="TestPlugin", arguments=arguments)
assert mock_pipeline.call_args.args[0] == input_str
async def test_text_completion_throws():
kernel = Kernel()
model_name = "patrickvonplaten/t5-tiny-random"
task = "text2text-generation"
input_str = "translate English to Dutch: Hello, how are you?"
with patch("semantic_kernel.connectors.ai.hugging_face.services.hf_text_completion.pipeline") as patched_pipeline:
mock_generator = Mock()
mock_generator.side_effect = Exception("Test exception")
patched_pipeline.return_value = mock_generator
service = HuggingFaceTextCompletion(service_id=model_name, ai_model_id=model_name, task=task)
kernel.add_service(service=service)
exec_settings = PromptExecutionSettings(service_id=model_name, extension_data={"max_new_tokens": 25})
prompt = "{{$input}}"
prompt_template_config = PromptTemplateConfig(template=prompt, execution_settings=exec_settings)
kernel.add_function(
prompt_template_config=prompt_template_config,
function_name="TestFunction",
plugin_name="TestPlugin",
prompt_execution_settings=exec_settings,
)
arguments = KernelArguments(input=input_str)
with pytest.raises(
KernelInvokeException, match="Error occurred while invoking function: 'TestPlugin-TestFunction'"
):
await kernel.invoke(function_name="TestFunction", plugin_name="TestPlugin", arguments=arguments)
@pytest.mark.parametrize(
("model_name", "task", "input_str"),
[
(
"patrickvonplaten/t5-tiny-random",
"text2text-generation",
"translate English to Dutch: Hello, how are you?",
),
("HuggingFaceM4/tiny-random-LlamaForCausalLM", "text-generation", "Hello, I like sleeping and "),
],
ids=["text2text-generation", "text-generation"],
)
async def test_text_completion_streaming(model_name, task, input_str):
ret = {"summary_text": "test"} if task == "summarization" else {"generated_text": "test"}
mock_pipeline = Mock(return_value=ret)
mock_streamer = MagicMock(spec=TextIteratorStreamer)
mock_streamer.__iter__.return_value = iter(["mocked_text"])
with (
patch(
"semantic_kernel.connectors.ai.hugging_face.services.hf_text_completion.pipeline",
return_value=mock_pipeline,
),
patch(
"semantic_kernel.connectors.ai.hugging_face.services.hf_text_completion.Thread",
side_effect=Mock(spec=Thread),
),
patch(
"semantic_kernel.connectors.ai.hugging_face.services.hf_text_completion.AutoTokenizer",
side_effect=Mock(spec=AutoTokenizer),
),
patch(
"semantic_kernel.connectors.ai.hugging_face.services.hf_text_completion.TextIteratorStreamer",
return_value=mock_streamer,
) as mock_stream,
):
mock_stream.return_value = mock_streamer
service = HuggingFaceTextCompletion(service_id=model_name, ai_model_id=model_name, task=task)
prompt = "test prompt"
exec_settings = PromptExecutionSettings(service_id=model_name, extension_data={"max_new_tokens": 25})
result = []
async for content in service.get_streaming_text_contents(prompt, exec_settings):
result.append(content)
assert len(result) == 1
assert result[0][0].inner_content == "mocked_text"
@pytest.mark.parametrize(
("model_name", "task", "input_str"),
[
(
"patrickvonplaten/t5-tiny-random",
"text2text-generation",
"translate English to Dutch: Hello, how are you?",
),
("HuggingFaceM4/tiny-random-LlamaForCausalLM", "text-generation", "Hello, I like sleeping and "),
],
ids=["text2text-generation", "text-generation"],
)
async def test_text_completion_streaming_throws(model_name, task, input_str):
ret = {"summary_text": "test"} if task == "summarization" else {"generated_text": "test"}
mock_pipeline = Mock(return_value=ret)
mock_streamer = MagicMock(spec=TextIteratorStreamer)
mock_streamer.__iter__.return_value = Exception()
with (
patch(
"semantic_kernel.connectors.ai.hugging_face.services.hf_text_completion.pipeline",
return_value=mock_pipeline,
),
patch(
"semantic_kernel.connectors.ai.hugging_face.services.hf_text_completion.Thread",
side_effect=Exception(),
),
patch(
"semantic_kernel.connectors.ai.hugging_face.services.hf_text_completion.TextIteratorStreamer",
return_value=mock_streamer,
) as mock_stream,
):
mock_stream.return_value = mock_streamer
service = HuggingFaceTextCompletion(service_id=model_name, ai_model_id=model_name, task=task)
prompt = "test prompt"
exec_settings = PromptExecutionSettings(service_id=model_name, extension_data={"max_new_tokens": 25})
with pytest.raises(ServiceResponseException, match=("Hugging Face completion failed")):
async for _ in service.get_streaming_text_contents(prompt, exec_settings):
pass
def test_hugging_face_text_completion_init():
with (
patch("semantic_kernel.connectors.ai.hugging_face.services.hf_text_completion.pipeline") as patched_pipeline,
patch(
"semantic_kernel.connectors.ai.hugging_face.services.hf_text_completion.torch.cuda.is_available"
) as mock_torch_cuda_is_available,
):
patched_pipeline.return_value = patched_pipeline
mock_torch_cuda_is_available.return_value = False
ai_model_id = "test-model"
task = "summarization"
device = -1
service = HuggingFaceTextCompletion(service_id="test", ai_model_id=ai_model_id, task=task, device=device)
assert service is not None
@@ -0,0 +1,56 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import patch
import pytest
from numpy import array, ndarray
from semantic_kernel.connectors.ai.hugging_face.services.hf_text_embedding import HuggingFaceTextEmbedding
from semantic_kernel.exceptions import ServiceResponseException
def test_huggingface_text_embedding_initialization():
model_name = "sentence-transformers/all-MiniLM-L6-v2"
device = -1
with patch("sentence_transformers.SentenceTransformer") as mock_transformer:
mock_instance = mock_transformer.return_value
service = HuggingFaceTextEmbedding(service_id="test", ai_model_id=model_name, device=device)
assert service.ai_model_id == model_name
assert service.device == "cpu"
assert service.generator == mock_instance
mock_transformer.assert_called_once_with(model_name_or_path=model_name, device="cpu")
async def test_generate_embeddings_success():
model_name = "sentence-transformers/all-MiniLM-L6-v2"
device = -1
texts = ["Hello world!", "How are you?"]
mock_embeddings = array([[0.1, 0.2], [0.3, 0.4]])
with patch("sentence_transformers.SentenceTransformer") as mock_transformer:
mock_instance = mock_transformer.return_value
mock_instance.encode.return_value = mock_embeddings
service = HuggingFaceTextEmbedding(service_id="test", ai_model_id=model_name, device=device)
embeddings = await service.generate_embeddings(texts)
assert isinstance(embeddings, ndarray)
assert embeddings.shape == (2, 2)
assert (embeddings == mock_embeddings).all()
async def test_generate_embeddings_throws():
model_name = "sentence-transformers/all-MiniLM-L6-v2"
device = -1
texts = ["Hello world!", "How are you?"]
with patch("sentence_transformers.SentenceTransformer") as mock_transformer:
mock_instance = mock_transformer.return_value
mock_instance.encode.side_effect = Exception("Test exception")
service = HuggingFaceTextEmbedding(service_id="test", ai_model_id=model_name, device=device)
with pytest.raises(ServiceResponseException, match="Hugging Face embeddings failed"):
await service.generate_embeddings(texts)
@@ -0,0 +1,616 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from mistralai import CompletionEvent, Mistral
from mistralai.models import (
AssistantMessage,
ChatCompletionChoice,
ChatCompletionResponse,
CompletionChunk,
CompletionResponseStreamChoice,
DeltaMessage,
UsageInfo,
)
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase
from semantic_kernel.connectors.ai.function_call_choice_configuration import FunctionCallChoiceConfiguration
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior, FunctionChoiceType
from semantic_kernel.connectors.ai.mistral_ai.prompt_execution_settings.mistral_ai_prompt_execution_settings import (
MistralAIChatPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.mistral_ai.services.mistral_ai_chat_completion import MistralAIChatCompletion
from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.open_ai_prompt_execution_settings import (
OpenAIChatPromptExecutionSettings,
)
from semantic_kernel.contents import FunctionCallContent, TextContent
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
from semantic_kernel.contents.utils.author_role import AuthorRole
from semantic_kernel.exceptions import (
ServiceInitializationError,
ServiceInvalidExecutionSettingsError,
ServiceResponseException,
)
from semantic_kernel.functions.kernel_arguments import KernelArguments
from semantic_kernel.functions.kernel_function import KernelFunction
from semantic_kernel.functions.kernel_function_decorator import kernel_function
from semantic_kernel.kernel import Kernel
@pytest.fixture
def mock_settings() -> MistralAIChatPromptExecutionSettings:
return MistralAIChatPromptExecutionSettings()
@pytest.fixture
def mock_mistral_ai_client_completion() -> Mistral:
client = MagicMock(spec=Mistral)
client.chat = MagicMock()
# Use proper ChatCompletionResponse type so isinstance checks pass
chat_completion_response = ChatCompletionResponse(
id="test_id",
object="object",
created=12345,
usage=UsageInfo(prompt_tokens=10, completion_tokens=5, total_tokens=15),
model="test_model_id",
choices=[
ChatCompletionChoice(
index=0,
message=AssistantMessage(role="assistant", content="Test"),
finish_reason="stop",
)
],
)
client.chat.complete_async = AsyncMock(return_value=chat_completion_response)
return client
@pytest.fixture
def mock_mistral_ai_client_completion_stream() -> Mistral:
client = MagicMock(spec=Mistral)
client.chat = MagicMock()
# Use proper CompletionEvent/CompletionChunk types so isinstance checks pass
mock_chunk = CompletionEvent(
data=CompletionChunk(
id="test_chunk",
created=12345,
model="test_model_id",
choices=[
CompletionResponseStreamChoice(
index=0,
delta=DeltaMessage(role="assistant", content="Test"),
finish_reason="stop",
)
],
)
)
async def mock_stream_async(**kwargs):
yield mock_chunk
client.chat.stream_async = AsyncMock(return_value=mock_stream_async())
return client
async def test_complete_chat_contents(
kernel: Kernel,
mock_settings: MistralAIChatPromptExecutionSettings,
mock_mistral_ai_client_completion: Mistral,
):
chat_history = MagicMock()
arguments = KernelArguments()
chat_completion_base = MistralAIChatCompletion(
ai_model_id="test_model_id", service_id="test", api_key="", async_client=mock_mistral_ai_client_completion
)
content: list[ChatMessageContent] = await chat_completion_base.get_chat_message_contents(
chat_history=chat_history, settings=mock_settings, kernel=kernel, arguments=arguments
)
assert content is not None
mock_message_text_content = ChatMessageContent(role=AuthorRole.ASSISTANT, items=[TextContent(text="test")])
mock_message_function_call = ChatMessageContent(
role=AuthorRole.ASSISTANT,
items=[
FunctionCallContent(
name="test",
arguments={"key": "test"},
)
],
)
@pytest.mark.parametrize(
"function_choice_behavior,model_responses,expected_result",
[
pytest.param(
FunctionChoiceBehavior.Auto(),
[[mock_message_function_call], [mock_message_text_content]],
TextContent,
id="auto",
),
pytest.param(
FunctionChoiceBehavior.Auto(auto_invoke=False),
[[mock_message_function_call]],
FunctionCallContent,
id="auto_none_invoke",
),
pytest.param(
FunctionChoiceBehavior.Required(auto_invoke=False),
[[mock_message_function_call]],
FunctionCallContent,
id="required_none_invoke",
),
pytest.param(FunctionChoiceBehavior.NoneInvoke(), [[mock_message_text_content]], TextContent, id="none"),
],
)
async def test_complete_chat_contents_function_call_behavior_tool_call(
kernel: Kernel,
mock_settings: MistralAIChatPromptExecutionSettings,
function_choice_behavior: FunctionChoiceBehavior,
model_responses,
expected_result,
):
kernel.add_function("test", kernel_function(lambda key: "test", name="test"))
mock_settings.function_choice_behavior = function_choice_behavior
arguments = KernelArguments()
chat_completion_base = MistralAIChatCompletion(ai_model_id="test_model_id", service_id="test", api_key="")
with (
patch.object(chat_completion_base, "_inner_get_chat_message_contents", side_effect=model_responses),
):
response: list[ChatMessageContent] = await chat_completion_base.get_chat_message_contents(
chat_history=ChatHistory(system_message="Test"), settings=mock_settings, kernel=kernel, arguments=arguments
)
assert all(isinstance(content, expected_result) for content in response[0].items)
async def test_complete_chat_contents_function_call_behavior_without_kernel(
mock_settings: MistralAIChatPromptExecutionSettings,
mock_mistral_ai_client_completion: Mistral,
):
chat_history = MagicMock()
chat_completion_base = MistralAIChatCompletion(
ai_model_id="test_model_id", service_id="test", api_key="", async_client=mock_mistral_ai_client_completion
)
mock_settings.function_choice_behavior = FunctionChoiceBehavior.Auto()
with pytest.raises(ServiceInvalidExecutionSettingsError):
await chat_completion_base.get_chat_message_contents(chat_history=chat_history, settings=mock_settings)
async def test_complete_chat_stream_contents(
kernel: Kernel,
mock_settings: MistralAIChatPromptExecutionSettings,
mock_mistral_ai_client_completion_stream: Mistral,
):
chat_history = MagicMock()
arguments = KernelArguments()
chat_completion_base = MistralAIChatCompletion(
ai_model_id="test_model_id",
service_id="test",
api_key="",
async_client=mock_mistral_ai_client_completion_stream,
)
async for content in chat_completion_base.get_streaming_chat_message_contents(
chat_history, mock_settings, kernel=kernel, arguments=arguments
):
assert content is not None
mock_message_function_call = StreamingChatMessageContent(
role=AuthorRole.ASSISTANT, items=[FunctionCallContent(name="test")], choice_index="0"
)
mock_message_text_content = StreamingChatMessageContent(
role=AuthorRole.ASSISTANT, items=[TextContent(text="test")], choice_index="0"
)
@pytest.mark.parametrize(
"function_choice_behavior,model_responses,expected_result",
[
pytest.param(
FunctionChoiceBehavior.Auto(),
[[mock_message_function_call], [mock_message_text_content]],
TextContent,
id="auto",
),
pytest.param(
FunctionChoiceBehavior.Auto(auto_invoke=False),
[[mock_message_function_call]],
FunctionCallContent,
id="auto_none_invoke",
),
pytest.param(
FunctionChoiceBehavior.Required(auto_invoke=False),
[[mock_message_function_call]],
FunctionCallContent,
id="required_none_invoke",
),
pytest.param(FunctionChoiceBehavior.NoneInvoke(), [[mock_message_text_content]], TextContent, id="none"),
],
)
async def test_complete_chat_contents_streaming_function_call_behavior_tool_call(
kernel: Kernel,
mock_settings: MistralAIChatPromptExecutionSettings,
function_choice_behavior: FunctionChoiceBehavior,
model_responses,
expected_result,
):
mock_settings.function_choice_behavior = function_choice_behavior
# Mock sequence of model responses
generator_mocks = []
for mock_message in model_responses:
generator_mock = MagicMock()
generator_mock.__aiter__.return_value = [mock_message]
generator_mocks.append(generator_mock)
arguments = KernelArguments()
chat_completion_base = MistralAIChatCompletion(ai_model_id="test_model_id", service_id="test", api_key="")
with patch.object(chat_completion_base, "_inner_get_streaming_chat_message_contents", side_effect=generator_mocks):
messages = []
async for chunk in chat_completion_base.get_streaming_chat_message_contents(
chat_history=ChatHistory(system_message="Test"), settings=mock_settings, kernel=kernel, arguments=arguments
):
messages.append(chunk)
response = messages[-1]
assert all(isinstance(content, expected_result) for content in response[0].items)
async def test_mistral_ai_sdk_exception(kernel: Kernel, mock_settings: MistralAIChatPromptExecutionSettings):
chat_history = MagicMock()
arguments = KernelArguments()
client = MagicMock(spec=Mistral)
client.chat = MagicMock()
client.chat.complete_async.side_effect = Exception("Test Exception")
chat_completion_base = MistralAIChatCompletion(
ai_model_id="test_model_id", service_id="test", api_key="", async_client=client
)
with pytest.raises(ServiceResponseException):
await chat_completion_base.get_chat_message_contents(
chat_history=chat_history, settings=mock_settings, kernel=kernel, arguments=arguments
)
async def test_mistral_ai_sdk_exception_streaming(kernel: Kernel, mock_settings: MistralAIChatPromptExecutionSettings):
chat_history = MagicMock()
arguments = KernelArguments()
client = MagicMock(spec=Mistral)
client.chat = MagicMock()
client.chat.chat_stream.side_effect = Exception("Test Exception")
chat_completion_base = MistralAIChatCompletion(
ai_model_id="test_model_id", service_id="test", api_key="", async_client=client
)
with pytest.raises(ServiceResponseException):
async for content in chat_completion_base.get_streaming_chat_message_contents(
chat_history, mock_settings, kernel=kernel, arguments=arguments
):
assert content is not None
def test_mistral_ai_chat_completion_init(mistralai_unit_test_env) -> None:
# Test successful initialization
mistral_ai_chat_completion = MistralAIChatCompletion()
assert mistral_ai_chat_completion.ai_model_id == mistralai_unit_test_env["MISTRALAI_CHAT_MODEL_ID"]
api_key = mistralai_unit_test_env["MISTRALAI_API_KEY"]
assert mistral_ai_chat_completion.async_client.sdk_configuration.security.api_key == api_key
assert isinstance(mistral_ai_chat_completion, ChatCompletionClientBase)
@pytest.mark.parametrize("exclude_list", [["MISTRALAI_API_KEY", "MISTRALAI_CHAT_MODEL_ID"]], indirect=True)
def test_mistral_ai_chat_completion_init_constructor(mistralai_unit_test_env) -> None:
# Test successful initialization
mistral_ai_chat_completion = MistralAIChatCompletion(
api_key="overwrite_api_key",
ai_model_id="overwrite_model_id",
env_file_path="test.env",
)
assert mistral_ai_chat_completion.ai_model_id == "overwrite_model_id"
assert mistral_ai_chat_completion.async_client.sdk_configuration.security.api_key == "overwrite_api_key"
assert isinstance(mistral_ai_chat_completion, ChatCompletionClientBase)
@pytest.mark.parametrize("exclude_list", [["MISTRALAI_API_KEY", "MISTRALAI_CHAT_MODEL_ID"]], indirect=True)
def test_mistral_ai_chat_completion_init_constructor_missing_model(mistralai_unit_test_env) -> None:
# Test successful initialization
with pytest.raises(ServiceInitializationError):
MistralAIChatCompletion(api_key="overwrite_api_key", env_file_path="test.env")
@pytest.mark.parametrize("exclude_list", [["MISTRALAI_API_KEY", "MISTRALAI_CHAT_MODEL_ID"]], indirect=True)
def test_mistral_ai_chat_completion_init_constructor_missing_api_key(mistralai_unit_test_env) -> None:
# Test successful initialization
with pytest.raises(ServiceInitializationError):
MistralAIChatCompletion(ai_model_id="overwrite_model_id", env_file_path="test.env")
def test_mistral_ai_chat_completion_init_hybrid(mistralai_unit_test_env) -> None:
mistral_ai_chat_completion = MistralAIChatCompletion(
ai_model_id="overwrite_model_id",
env_file_path="test.env",
)
assert mistral_ai_chat_completion.ai_model_id == "overwrite_model_id"
assert mistral_ai_chat_completion.async_client.sdk_configuration.security.api_key == "test_api_key"
@pytest.mark.parametrize("exclude_list", [["MISTRALAI_CHAT_MODEL_ID"]], indirect=True)
def test_mistral_ai_chat_completion_init_with_empty_model_id(mistralai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError):
MistralAIChatCompletion(
env_file_path="test.env",
)
def test_prompt_execution_settings_class(mistralai_unit_test_env):
mistral_ai_chat_completion = MistralAIChatCompletion()
prompt_execution_settings = mistral_ai_chat_completion.get_prompt_execution_settings_class()
assert prompt_execution_settings == MistralAIChatPromptExecutionSettings
async def test_with_different_execution_settings(kernel: Kernel, mock_mistral_ai_client_completion: MagicMock):
chat_history = MagicMock()
settings = OpenAIChatPromptExecutionSettings(temperature=0.2, seed=2)
arguments = KernelArguments()
chat_completion_base = MistralAIChatCompletion(
ai_model_id="test_model_id", service_id="test", api_key="", async_client=mock_mistral_ai_client_completion
)
await chat_completion_base.get_chat_message_contents(
chat_history=chat_history, settings=settings, kernel=kernel, arguments=arguments
)
assert mock_mistral_ai_client_completion.chat.complete_async.call_args.kwargs["temperature"] == 0.2
assert mock_mistral_ai_client_completion.chat.complete_async.call_args.kwargs["seed"] == 2
async def test_with_different_execution_settings_stream(
kernel: Kernel, mock_mistral_ai_client_completion_stream: MagicMock
):
chat_history = MagicMock()
settings = OpenAIChatPromptExecutionSettings(temperature=0.2, seed=2)
arguments = KernelArguments()
chat_completion_base = MistralAIChatCompletion(
ai_model_id="test_model_id",
service_id="test",
api_key="",
async_client=mock_mistral_ai_client_completion_stream,
)
async for chunk in chat_completion_base.get_streaming_chat_message_contents(
chat_history, settings, kernel=kernel, arguments=arguments
):
continue
assert mock_mistral_ai_client_completion_stream.chat.stream_async.call_args.kwargs["temperature"] == 0.2
assert mock_mistral_ai_client_completion_stream.chat.stream_async.call_args.kwargs["seed"] == 2
async def test_mistral_ai_chat_completion_get_chat_message_contents_success():
"""Test get_chat_message_contents with a successful ChatCompletionResponse."""
# Mock the response from the Mistral chat complete_async.
mock_response = ChatCompletionResponse(
id="some_id",
object="object",
created=12345,
usage=UsageInfo(prompt_tokens=10, completion_tokens=20, total_tokens=30),
model="test-model",
choices=[
ChatCompletionChoice(
index=0,
message=AssistantMessage(role="assistant", content="Hello!"),
finish_reason="stop",
)
],
)
async_mock_client = MagicMock(spec=Mistral)
async_mock_client.chat = MagicMock()
async_mock_client.chat.complete_async = AsyncMock(return_value=mock_response)
chat_completion = MistralAIChatCompletion(
ai_model_id="test-model",
api_key="test_key",
async_client=async_mock_client,
)
# We create a ChatHistory.
chat_history = ChatHistory()
settings = MistralAIChatPromptExecutionSettings()
results = await chat_completion.get_chat_message_contents(chat_history, settings)
# We should have exactly one ChatMessageContent.
assert len(results) == 1
assert results[0].role.value == "assistant"
assert results[0].finish_reason is not None
assert results[0].finish_reason.value == "stop"
assert "Hello!" in results[0].content
async_mock_client.chat.complete_async.assert_awaited_once()
async def test_mistral_ai_chat_completion_get_chat_message_contents_failure():
"""Test get_chat_message_contents should raise ServiceResponseException if Mistral call fails."""
async_mock_client = MagicMock(spec=Mistral)
async_mock_client.chat = MagicMock()
async_mock_client.chat.complete_async = AsyncMock(side_effect=Exception("API error"))
chat_completion = MistralAIChatCompletion(
ai_model_id="test-model",
api_key="test_key",
async_client=async_mock_client,
)
chat_history = ChatHistory()
settings = MistralAIChatPromptExecutionSettings()
with pytest.raises(ServiceResponseException) as exc:
await chat_completion.get_chat_message_contents(chat_history, settings)
assert "service failed to complete the prompt" in str(exc.value)
async def test_mistral_ai_chat_completion_get_streaming_chat_message_contents_success():
"""Test get_streaming_chat_message_contents when streaming successfully."""
# We'll yield multiple chunks to simulate streaming.
mock_chunk1 = CompletionEvent(
data=CompletionChunk(
id="chunk1",
created=1,
model="test-model",
choices=[
CompletionResponseStreamChoice(
index=0,
delta=DeltaMessage(role="assistant", content="Hello "),
finish_reason=None,
)
],
)
)
mock_chunk2 = CompletionEvent(
data=CompletionChunk(
id="chunk1",
created=1,
model="test-model",
choices=[
CompletionResponseStreamChoice(
index=0,
delta=DeltaMessage(content="World!"),
finish_reason="stop",
)
],
)
)
async def mock_stream_async(**kwargs):
yield mock_chunk1
yield mock_chunk2
async_mock_client = MagicMock(spec=Mistral)
async_mock_client.chat = MagicMock()
async_mock_client.chat.stream_async = AsyncMock(return_value=mock_stream_async())
chat_completion = MistralAIChatCompletion(
ai_model_id="test-model",
api_key="test_key",
async_client=async_mock_client,
)
chat_history = ChatHistory()
settings = MistralAIChatPromptExecutionSettings()
collected_chunks = []
async for chunk_list in chat_completion.get_streaming_chat_message_contents(chat_history, settings):
collected_chunks.append(chunk_list)
# We expect two sets of chunk_list yields.
assert len(collected_chunks) == 2
assert len(collected_chunks[0]) == 1
assert len(collected_chunks[1]) == 1
# First chunk contains "Hello ", second chunk "World!".
assert collected_chunks[0][0].items[0].text == "Hello "
assert collected_chunks[1][0].items[0].text == "World!"
async def test_mistral_ai_chat_completion_get_streaming_chat_message_contents_failure():
"""Test get_streaming_chat_message_contents raising a ServiceResponseException on failure."""
async_mock_client = MagicMock(spec=Mistral)
async_mock_client.chat = MagicMock()
async_mock_client.chat.stream_async = AsyncMock(side_effect=Exception("Streaming error"))
chat_completion = MistralAIChatCompletion(
ai_model_id="test-model",
api_key="test_key",
async_client=async_mock_client,
)
chat_history = ChatHistory()
settings = MistralAIChatPromptExecutionSettings()
with pytest.raises(ServiceResponseException) as exc:
async for _ in chat_completion.get_streaming_chat_message_contents(chat_history, settings):
pass
assert "service failed to complete the prompt" in str(exc.value)
def test_mistral_ai_chat_completion_update_settings_from_function_call_configuration_mistral():
"""Test update_settings_from_function_call_configuration_mistral sets tools etc."""
chat_completion = MistralAIChatCompletion(
ai_model_id="test-model",
api_key="test_key",
)
# Create a mock settings object.
settings = MistralAIChatPromptExecutionSettings()
# Create a function choice config with some available functions.
config = FunctionCallChoiceConfiguration()
mock_func = MagicMock(
spec=KernelFunction,
)
mock_func.name = "my_func"
mock_func.description = "some desc"
mock_func.fully_qualified_name = "mod.my_func"
mock_func.parameters = []
config.available_functions = [mock_func]
# Call the update_settings_from_function_call_configuration_mistral with type=ANY.
chat_completion.update_settings_from_function_call_configuration_mistral(
function_choice_configuration=config,
settings=settings,
type=FunctionChoiceType.AUTO,
)
assert settings.tool_choice == FunctionChoiceType.AUTO.value
assert settings.tools is not None
assert len(settings.tools) == 1
assert settings.tools[0]["function"]["name"] == "mod.my_func"
def test_mistral_ai_chat_completion_reset_function_choice_settings():
"""Test that _reset_function_choice_settings resets specific attributes."""
chat_completion = MistralAIChatCompletion(
ai_model_id="test-model",
api_key="test_key",
)
settings = MistralAIChatPromptExecutionSettings(tool_choice="any", tools=[{"name": "func1"}])
chat_completion._reset_function_choice_settings(settings)
assert settings.tool_choice is None
assert settings.tools is None
def test_mistral_ai_chat_completion_service_url():
"""Test that service_url attempts to use _endpoint from the async_client."""
async_mock_client = MagicMock(spec=Mistral)
async_mock_client._endpoint = "mistral"
chat_completion = MistralAIChatCompletion(
ai_model_id="test-model",
api_key="test_key",
async_client=async_mock_client,
)
url = chat_completion.service_url()
assert url == "mistral"
@@ -0,0 +1,114 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import AsyncMock, MagicMock
import pytest
from mistralai import Mistral
from mistralai.models import EmbeddingResponse
from semantic_kernel.connectors.ai.mistral_ai.services.mistral_ai_text_embedding import MistralAITextEmbedding
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError, ServiceResponseException
def test_embedding_with_env_variables(mistralai_unit_test_env):
text_embedding = MistralAITextEmbedding()
assert text_embedding.ai_model_id == "test_embedding_model_id"
assert text_embedding.async_client.sdk_configuration.security.api_key == "test_api_key"
@pytest.mark.parametrize("exclude_list", [["MISTRALAI_API_KEY", "MISTRALAI_EMBEDDING_MODEL_ID"]], indirect=True)
def test_embedding_with_constructor(mistralai_unit_test_env):
text_embedding = MistralAITextEmbedding(
api_key="overwrite-api-key",
ai_model_id="overwrite-model",
)
assert text_embedding.ai_model_id == "overwrite-model"
assert text_embedding.async_client.sdk_configuration.security.api_key == "overwrite-api-key"
def test_embedding_with_client(mistralai_unit_test_env):
client = MagicMock(spec=Mistral)
text_embedding = MistralAITextEmbedding(async_client=client)
assert text_embedding.async_client == client
assert text_embedding.ai_model_id == "test_embedding_model_id"
def test_embedding_with_api_key(mistralai_unit_test_env):
text_embedding = MistralAITextEmbedding(api_key="overwrite-api-key")
assert text_embedding.async_client.sdk_configuration.security.api_key == "overwrite-api-key"
assert text_embedding.ai_model_id == "test_embedding_model_id"
def test_embedding_with_model(mistralai_unit_test_env):
text_embedding = MistralAITextEmbedding(ai_model_id="overwrite-model")
assert text_embedding.ai_model_id == "overwrite-model"
assert text_embedding.async_client.sdk_configuration.security.api_key == "test_api_key"
@pytest.mark.parametrize("exclude_list", [["MISTRALAI_EMBEDDING_MODEL_ID"]], indirect=True)
def test_embedding_with_model_without_env(mistralai_unit_test_env):
text_embedding = MistralAITextEmbedding(ai_model_id="overwrite-model")
assert text_embedding.ai_model_id == "overwrite-model"
assert text_embedding.async_client.sdk_configuration.security.api_key == "test_api_key"
@pytest.mark.parametrize("exclude_list", [["MISTRALAI_EMBEDDING_MODEL_ID"]], indirect=True)
def test_embedding_missing_model(mistralai_unit_test_env):
with pytest.raises(ServiceInitializationError):
MistralAITextEmbedding(
env_file_path="test.env",
)
@pytest.mark.parametrize("exclude_list", [["MISTRALAI_API_KEY"]], indirect=True)
def test_embedding_missing_api_key(mistralai_unit_test_env):
with pytest.raises(ServiceInitializationError):
MistralAITextEmbedding(
env_file_path="test.env",
)
@pytest.mark.parametrize("exclude_list", [["MISTRALAI_API_KEY", "MISTRALAI_EMBEDDING_MODEL_ID"]], indirect=True)
def test_embedding_missing_api_key_constructor(mistralai_unit_test_env):
with pytest.raises(ServiceInitializationError):
MistralAITextEmbedding(
env_file_path="test.env",
)
@pytest.mark.parametrize("exclude_list", [["MISTRALAI_API_KEY", "MISTRALAI_EMBEDDING_MODEL_ID"]], indirect=True)
def test_embedding_missing_model_constructor(mistralai_unit_test_env):
with pytest.raises(ServiceInitializationError):
MistralAITextEmbedding(
api_key="test_api_key",
env_file_path="test.env",
)
async def test_embedding_generate_raw_embedding(mistralai_unit_test_env):
mock_client = AsyncMock(spec=Mistral)
mock_client.embeddings = AsyncMock()
mock_embedding_response = MagicMock(spec=EmbeddingResponse, data=[MagicMock(embedding=[1, 2, 3, 4, 5])])
mock_client.embeddings.create_async.return_value = mock_embedding_response
text_embedding = MistralAITextEmbedding(async_client=mock_client)
embedding = await text_embedding.generate_raw_embeddings(["test"])
assert embedding == [[1, 2, 3, 4, 5]]
async def test_embedding_generate_embedding(mistralai_unit_test_env):
mock_client = AsyncMock(spec=Mistral)
mock_client.embeddings = AsyncMock()
mock_embedding_response = MagicMock(spec=EmbeddingResponse, data=[MagicMock(embedding=[1, 2, 3, 4, 5])])
mock_client.embeddings.create_async.return_value = mock_embedding_response
text_embedding = MistralAITextEmbedding(async_client=mock_client)
embedding = await text_embedding.generate_embeddings(["test"])
assert embedding.tolist() == [[1, 2, 3, 4, 5]]
async def test_embedding_generate_embedding_exception(mistralai_unit_test_env):
mock_client = AsyncMock(spec=Mistral)
mock_client.embeddings = AsyncMock()
mock_client.embeddings.create_async.side_effect = Exception("Test Exception")
text_embedding = MistralAITextEmbedding(async_client=mock_client)
with pytest.raises(ServiceResponseException):
await text_embedding.generate_embeddings(["test"])
@@ -0,0 +1,125 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.connectors.ai.mistral_ai.prompt_execution_settings.mistral_ai_prompt_execution_settings import (
MistralAIChatPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
def test_default_mistralai_chat_prompt_execution_settings():
settings = MistralAIChatPromptExecutionSettings()
assert settings.temperature is None
assert settings.top_p is None
assert settings.max_tokens is None
assert settings.messages is None
def test_custom_mistralai_chat_prompt_execution_settings():
settings = MistralAIChatPromptExecutionSettings(
temperature=0.5,
top_p=0.5,
max_tokens=128,
messages=[{"role": "system", "content": "Hello"}],
)
assert settings.temperature == 0.5
assert settings.top_p == 0.5
assert settings.max_tokens == 128
assert settings.messages == [{"role": "system", "content": "Hello"}]
def test_mistralai_chat_prompt_execution_settings_from_default_completion_config():
settings = PromptExecutionSettings(service_id="test_service")
chat_settings = MistralAIChatPromptExecutionSettings.from_prompt_execution_settings(settings)
assert chat_settings.service_id == "test_service"
assert chat_settings.temperature is None
assert chat_settings.top_p is None
assert chat_settings.max_tokens is None
def test_mistral_chat_prompt_execution_settings_from_openai_prompt_execution_settings():
chat_settings = MistralAIChatPromptExecutionSettings(service_id="test_service", temperature=1.0)
new_settings = MistralAIChatPromptExecutionSettings(service_id="test_2", temperature=0.0)
chat_settings.update_from_prompt_execution_settings(new_settings)
assert chat_settings.service_id == "test_2"
assert chat_settings.temperature == 0.0
def test_mistral_chat_prompt_execution_settings_from_custom_completion_config():
settings = PromptExecutionSettings(
service_id="test_service",
extension_data={
"temperature": 0.5,
"top_p": 0.5,
"max_tokens": 128,
"messages": [{"role": "system", "content": "Hello"}],
},
)
chat_settings = MistralAIChatPromptExecutionSettings.from_prompt_execution_settings(settings)
assert chat_settings.temperature == 0.5
assert chat_settings.top_p == 0.5
assert chat_settings.max_tokens == 128
def test_openai_chat_prompt_execution_settings_from_custom_completion_config_with_none():
settings = PromptExecutionSettings(
service_id="test_service",
extension_data={
"temperature": 0.5,
"top_p": 0.5,
"max_tokens": 128,
"messages": [{"role": "system", "content": "Hello"}],
},
)
chat_settings = MistralAIChatPromptExecutionSettings.from_prompt_execution_settings(settings)
assert chat_settings.temperature == 0.5
assert chat_settings.top_p == 0.5
assert chat_settings.max_tokens == 128
def test_openai_chat_prompt_execution_settings_from_custom_completion_config_with_functions():
settings = PromptExecutionSettings(
service_id="test_service",
extension_data={
"temperature": 0.5,
"top_p": 0.5,
"max_tokens": 128,
"tools": [{}],
"messages": [{"role": "system", "content": "Hello"}],
},
)
chat_settings = MistralAIChatPromptExecutionSettings.from_prompt_execution_settings(settings)
assert chat_settings.temperature == 0.5
assert chat_settings.top_p == 0.5
assert chat_settings.max_tokens == 128
def test_create_options():
settings = MistralAIChatPromptExecutionSettings(
service_id="test_service",
extension_data={
"temperature": 0.5,
"top_p": 0.5,
"max_tokens": 128,
"tools": [{}],
"messages": [{"role": "system", "content": "Hello"}],
},
)
options = settings.prepare_settings_dict()
assert options["temperature"] == 0.5
assert options["top_p"] == 0.5
assert options["max_tokens"] == 128
def test_create_options_with_function_choice_behavior():
settings = MistralAIChatPromptExecutionSettings(
service_id="test_service",
function_choice_behavior="auto",
extension_data={
"temperature": 0.5,
"top_p": 0.5,
"max_tokens": 128,
"tools": [{}],
"messages": [{"role": "system", "content": "Hello"}],
},
)
assert settings.function_choice_behavior
@@ -0,0 +1,101 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from pydantic import BaseModel, ValidationError
from semantic_kernel.connectors.ai.nvidia.prompt_execution_settings.nvidia_prompt_execution_settings import (
NvidiaChatPromptExecutionSettings,
NvidiaEmbeddingPromptExecutionSettings,
NvidiaPromptExecutionSettings,
)
class TestNvidiaPromptExecutionSettings:
"""Test cases for NvidiaPromptExecutionSettings."""
def test_init_with_defaults(self):
"""Test initialization with default values."""
settings = NvidiaPromptExecutionSettings()
assert settings.format is None
assert settings.options is None
def test_init_with_values(self):
"""Test initialization with specific values."""
settings = NvidiaPromptExecutionSettings(
format="json",
options={"key": "value"},
)
assert settings.format == "json"
assert settings.options == {"key": "value"}
def test_validation_format_values(self):
"""Test format validation values."""
# Valid values
settings = NvidiaPromptExecutionSettings(format="json")
assert settings.format == "json"
class TestNvidiaChatPromptExecutionSettings:
"""Test cases for NvidiaChatPromptExecutionSettings."""
def test_init_with_defaults(self):
"""Test initialization with default values."""
settings = NvidiaChatPromptExecutionSettings()
assert settings.messages is None
assert settings.response_format is None
def test_response_format_with_pydantic_model(self):
"""Test response_format with Pydantic model."""
class TestModel(BaseModel):
name: str
value: int
settings = NvidiaChatPromptExecutionSettings(response_format=TestModel)
assert settings.response_format == TestModel
def test_response_format_with_dict(self):
"""Test response_format with dictionary."""
settings = NvidiaChatPromptExecutionSettings(response_format={"type": "json_object"})
assert settings.response_format == {"type": "json_object"}
class TestNvidiaEmbeddingPromptExecutionSettings:
"""Test cases for NvidiaEmbeddingPromptExecutionSettings."""
def test_init_with_defaults(self):
"""Test initialization with default values."""
settings = NvidiaEmbeddingPromptExecutionSettings()
assert settings.input is None
assert settings.encoding_format == "float"
assert settings.input_type == "query"
assert settings.truncate == "NONE"
def test_init_with_values(self):
"""Test initialization with specific values."""
settings = NvidiaEmbeddingPromptExecutionSettings(
input=["hello", "world"],
encoding_format="base64",
input_type="passage",
truncate="START",
)
assert settings.input == ["hello", "world"]
assert settings.encoding_format == "base64"
assert settings.input_type == "passage"
assert settings.truncate == "START"
def test_validation_encoding_format(self):
"""Test encoding_format validation."""
# Valid values
settings = NvidiaEmbeddingPromptExecutionSettings(encoding_format="float")
assert settings.encoding_format == "float"
settings = NvidiaEmbeddingPromptExecutionSettings(encoding_format="base64")
assert settings.encoding_format == "base64"
# Invalid values
with pytest.raises(ValidationError):
NvidiaEmbeddingPromptExecutionSettings(encoding_format="invalid")
@@ -0,0 +1,151 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import AsyncMock, patch
import pytest
from openai.resources.chat.completions import AsyncCompletions
from openai.types.chat import ChatCompletion, ChatCompletionMessage
from openai.types.chat.chat_completion import Choice
from openai.types.completion_usage import CompletionUsage
from pydantic import BaseModel
from semantic_kernel.connectors.ai.nvidia import NvidiaChatCompletion
from semantic_kernel.connectors.ai.nvidia.prompt_execution_settings.nvidia_prompt_execution_settings import (
NvidiaChatPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.nvidia.services.nvidia_chat_completion import DEFAULT_NVIDIA_CHAT_MODEL
from semantic_kernel.contents import ChatHistory
from semantic_kernel.exceptions import ServiceInitializationError, ServiceResponseException
@pytest.fixture
def nvidia_unit_test_env(monkeypatch, exclude_list, override_env_param_dict):
"""Fixture to set environment variables for NvidiaChatCompletion."""
if exclude_list is None:
exclude_list = []
if override_env_param_dict is None:
override_env_param_dict = {}
env_vars = {"NVIDIA_API_KEY": "test_api_key", "NVIDIA_CHAT_MODEL_ID": "meta/llama-3.1-8b-instruct"}
env_vars.update(override_env_param_dict)
for key, value in env_vars.items():
if key not in exclude_list:
monkeypatch.setenv(key, value)
else:
monkeypatch.delenv(key, raising=False)
return env_vars
def _create_mock_chat_completion(content: str = "Hello!") -> ChatCompletion:
"""Helper function to create a mock ChatCompletion response."""
message = ChatCompletionMessage(role="assistant", content=content)
choice = Choice(
finish_reason="stop",
index=0,
message=message,
)
usage = CompletionUsage(completion_tokens=20, prompt_tokens=10, total_tokens=30)
return ChatCompletion(
id="test-id",
choices=[choice],
created=1234567890,
model="meta/llama-3.1-8b-instruct",
object="chat.completion",
usage=usage,
)
class TestNvidiaChatCompletion:
"""Test cases for NvidiaChatCompletion."""
def test_init_with_defaults(self, nvidia_unit_test_env):
"""Test initialization with default values."""
service = NvidiaChatCompletion()
assert service.ai_model_id == nvidia_unit_test_env["NVIDIA_CHAT_MODEL_ID"]
def test_get_prompt_execution_settings_class(self, nvidia_unit_test_env):
"""Test getting the prompt execution settings class."""
service = NvidiaChatCompletion()
from semantic_kernel.connectors.ai.nvidia.prompt_execution_settings.nvidia_prompt_execution_settings import (
NvidiaChatPromptExecutionSettings,
)
assert service.get_prompt_execution_settings_class() == NvidiaChatPromptExecutionSettings
@pytest.mark.parametrize("exclude_list", [["NVIDIA_API_KEY"]], indirect=True)
def test_init_with_empty_api_key(self, nvidia_unit_test_env):
"""Test initialization fails with empty API key."""
with pytest.raises(ServiceInitializationError):
NvidiaChatCompletion()
@pytest.mark.parametrize("exclude_list", [["NVIDIA_CHAT_MODEL_ID"]], indirect=True)
def test_init_with_empty_model_id(self, nvidia_unit_test_env):
"""Test initialization with empty model ID uses default."""
service = NvidiaChatCompletion()
assert service.ai_model_id == DEFAULT_NVIDIA_CHAT_MODEL
def test_init_with_custom_model_id(self, nvidia_unit_test_env):
"""Test initialization with custom model ID."""
custom_model = "custom/nvidia-model"
service = NvidiaChatCompletion(ai_model_id=custom_model)
assert service.ai_model_id == custom_model
@pytest.mark.asyncio
@patch.object(AsyncCompletions, "create", new_callable=AsyncMock)
async def test_get_chat_message_contents(self, mock_create, nvidia_unit_test_env):
"""Test basic chat completion."""
mock_create.return_value = _create_mock_chat_completion("Hello!")
service = NvidiaChatCompletion()
chat_history = ChatHistory()
chat_history.add_user_message("Hello")
settings = NvidiaChatPromptExecutionSettings()
result = await service.get_chat_message_contents(chat_history, settings)
assert len(result) == 1
assert result[0].content == "Hello!"
@pytest.mark.asyncio
@patch.object(AsyncCompletions, "create", new_callable=AsyncMock)
async def test_structured_output_with_pydantic_model(self, mock_create, nvidia_unit_test_env):
"""Test structured output with Pydantic model."""
# Define test model
class TestModel(BaseModel):
name: str
value: int
mock_create.return_value = _create_mock_chat_completion('{"name": "test", "value": 42}')
service = NvidiaChatCompletion()
chat_history = ChatHistory()
chat_history.add_user_message("Give me structured data")
settings = NvidiaChatPromptExecutionSettings()
settings.response_format = TestModel
await service.get_chat_message_contents(chat_history, settings)
# Verify nvext was passed
call_args = mock_create.call_args[1]
assert "extra_body" in call_args
assert "nvext" in call_args["extra_body"]
assert "guided_json" in call_args["extra_body"]["nvext"]
@pytest.mark.asyncio
@patch.object(AsyncCompletions, "create", new_callable=AsyncMock)
async def test_error_handling(self, mock_create, nvidia_unit_test_env):
"""Test error handling."""
mock_create.side_effect = Exception("API Error")
service = NvidiaChatCompletion()
chat_history = ChatHistory()
chat_history.add_user_message("Hello")
settings = NvidiaChatPromptExecutionSettings()
with pytest.raises(ServiceResponseException):
await service.get_chat_message_contents(chat_history, settings)
@@ -0,0 +1,152 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import AsyncMock, MagicMock
import pytest
from openai import AsyncOpenAI
from semantic_kernel.connectors.ai.nvidia.prompt_execution_settings.nvidia_prompt_execution_settings import (
NvidiaChatPromptExecutionSettings,
NvidiaEmbeddingPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.nvidia.services.nvidia_handler import NvidiaHandler
from semantic_kernel.connectors.ai.nvidia.services.nvidia_model_types import NvidiaModelTypes
@pytest.fixture
def mock_openai_client():
"""Create a mock OpenAI client."""
return AsyncMock(spec=AsyncOpenAI)
@pytest.fixture
def nvidia_handler(mock_openai_client):
"""Create a NvidiaHandler instance with mocked client."""
return NvidiaHandler(
client=mock_openai_client,
ai_model_type=NvidiaModelTypes.CHAT,
ai_model_id="test-model",
api_key="test-key",
)
class TestNvidiaHandler:
"""Test cases for NvidiaHandler."""
def test_init(self, mock_openai_client):
"""Test initialization."""
handler = NvidiaHandler(
client=mock_openai_client,
ai_model_type=NvidiaModelTypes.CHAT,
)
assert handler.client == mock_openai_client
assert handler.ai_model_type == NvidiaModelTypes.CHAT
assert handler.MODEL_PROVIDER_NAME == "nvidia"
@pytest.mark.asyncio
async def test_send_chat_completion_request(self, nvidia_handler, mock_openai_client):
"""Test sending chat completion request."""
# Mock the response
mock_response = MagicMock()
mock_response.choices = [
MagicMock(
message=MagicMock(role="assistant", content="Hello!"),
finish_reason="stop",
)
]
mock_response.usage = MagicMock(prompt_tokens=10, completion_tokens=20, total_tokens=30)
mock_openai_client.chat.completions.create = AsyncMock(return_value=mock_response)
# Create settings
settings = NvidiaChatPromptExecutionSettings(
messages=[{"role": "user", "content": "Hello"}],
model="test-model",
)
# Test the method
result = await nvidia_handler._send_chat_completion_request(settings)
assert result == mock_response
# Verify usage was stored
assert nvidia_handler.prompt_tokens == 10
assert nvidia_handler.completion_tokens == 20
assert nvidia_handler.total_tokens == 30
@pytest.mark.asyncio
async def test_send_chat_completion_request_with_nvext(self, nvidia_handler, mock_openai_client):
"""Test sending chat completion request with nvext parameter."""
# Mock the response
mock_response = MagicMock()
mock_response.choices = [
MagicMock(
message=MagicMock(role="assistant", content='{"result": "success"}'),
finish_reason="stop",
)
]
mock_response.usage = MagicMock(prompt_tokens=10, completion_tokens=20, total_tokens=30)
mock_openai_client.chat.completions.create = AsyncMock(return_value=mock_response)
# Create settings with nvext
settings = NvidiaChatPromptExecutionSettings(
messages=[{"role": "user", "content": "Give me JSON"}],
model="test-model",
extra_body={"nvext": {"guided_json": {"type": "object"}}},
)
# Test the method
result = await nvidia_handler._send_chat_completion_request(settings)
assert result == mock_response
# Verify the client was called with nvext in extra_body
call_args = mock_openai_client.chat.completions.create.call_args[1]
assert "extra_body" in call_args
assert "nvext" in call_args["extra_body"]
assert call_args["extra_body"]["nvext"] == {"guided_json": {"type": "object"}}
@pytest.mark.asyncio
async def test_send_embedding_request(self, mock_openai_client):
"""Test sending embedding request."""
handler = NvidiaHandler(
client=mock_openai_client,
ai_model_type=NvidiaModelTypes.EMBEDDING,
ai_model_id="test-model",
)
# Mock the response
mock_response = MagicMock()
mock_response.data = [
MagicMock(embedding=[0.1, 0.2, 0.3]),
MagicMock(embedding=[0.4, 0.5, 0.6]),
]
mock_response.usage = MagicMock(prompt_tokens=10, total_tokens=10)
mock_openai_client.embeddings.create = AsyncMock(return_value=mock_response)
# Create settings
settings = NvidiaEmbeddingPromptExecutionSettings(
input=["hello", "world"],
model="test-model",
)
# Test the method
result = await handler._send_embedding_request(settings)
assert result == [[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]]
@pytest.mark.asyncio
async def test_send_request_unsupported_model_type(self, mock_openai_client):
"""Test send_request with unsupported model type."""
# Create a handler with invalid model type by bypassing validation
handler = NvidiaHandler(
client=mock_openai_client,
ai_model_type=NvidiaModelTypes.CHAT,
)
# Manually set the attribute to bypass Pydantic validation
object.__setattr__(handler, "ai_model_type", "UNSUPPORTED")
settings = NvidiaChatPromptExecutionSettings(
messages=[{"role": "user", "content": "Hello"}],
model="test-model",
)
with pytest.raises(NotImplementedError, match="Model type UNSUPPORTED is not supported"):
await handler._send_request(settings)
@@ -0,0 +1,134 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import AsyncMock, patch
import pytest
from openai import AsyncClient
from openai.resources.embeddings import AsyncEmbeddings
from semantic_kernel.connectors.ai.nvidia.prompt_execution_settings.nvidia_prompt_execution_settings import (
NvidiaEmbeddingPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.nvidia.services.nvidia_text_embedding import NvidiaTextEmbedding
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError, ServiceResponseException
@pytest.fixture
def nvidia_unit_test_env(monkeypatch, exclude_list, override_env_param_dict):
"""Fixture to set environment variables for NvidiaTextEmbedding."""
if exclude_list is None:
exclude_list = []
if override_env_param_dict is None:
override_env_param_dict = {}
env_vars = {"NVIDIA_API_KEY": "test_api_key", "NVIDIA_EMBEDDING_MODEL_ID": "test_embedding_model_id"}
env_vars.update(override_env_param_dict)
for key, value in env_vars.items():
if key not in exclude_list:
monkeypatch.setenv(key, value)
else:
monkeypatch.delenv(key, raising=False)
return env_vars
def test_init(nvidia_unit_test_env):
nvidia_text_embedding = NvidiaTextEmbedding()
assert nvidia_text_embedding.client is not None
assert isinstance(nvidia_text_embedding.client, AsyncClient)
assert nvidia_text_embedding.ai_model_id == nvidia_unit_test_env["NVIDIA_EMBEDDING_MODEL_ID"]
assert nvidia_text_embedding.get_prompt_execution_settings_class() == NvidiaEmbeddingPromptExecutionSettings
def test_init_validation_fail() -> None:
with pytest.raises(ServiceInitializationError):
NvidiaTextEmbedding(api_key="34523", ai_model_id={"test": "dict"})
def test_init_to_from_dict(nvidia_unit_test_env):
default_headers = {"X-Unit-Test": "test-guid"}
settings = {
"ai_model_id": nvidia_unit_test_env["NVIDIA_EMBEDDING_MODEL_ID"],
"api_key": nvidia_unit_test_env["NVIDIA_API_KEY"],
"default_headers": default_headers,
}
text_embedding = NvidiaTextEmbedding.from_dict(settings)
dumped_settings = text_embedding.to_dict()
assert dumped_settings["ai_model_id"] == settings["ai_model_id"]
assert dumped_settings["api_key"] == settings["api_key"]
@patch.object(AsyncEmbeddings, "create", new_callable=AsyncMock)
async def test_embedding_calls_with_parameters(mock_create, nvidia_unit_test_env) -> None:
ai_model_id = "NV-Embed-QA"
texts = ["hello world", "goodbye world"]
embedding_dimensions = 1536
nvidia_text_embedding = NvidiaTextEmbedding(
ai_model_id=ai_model_id,
)
await nvidia_text_embedding.generate_embeddings(texts, dimensions=embedding_dimensions)
mock_create.assert_awaited_once_with(
input=texts,
model=ai_model_id,
dimensions=embedding_dimensions,
encoding_format="float",
extra_body={"input_type": "query", "truncate": "NONE"},
)
@patch.object(AsyncEmbeddings, "create", new_callable=AsyncMock)
async def test_embedding_calls_with_settings(mock_create, nvidia_unit_test_env) -> None:
ai_model_id = "test_model_id"
texts = ["hello world", "goodbye world"]
settings = NvidiaEmbeddingPromptExecutionSettings(service_id="default")
nvidia_text_embedding = NvidiaTextEmbedding(service_id="default", ai_model_id=ai_model_id)
await nvidia_text_embedding.generate_embeddings(texts, settings=settings)
mock_create.assert_awaited_once_with(
input=texts,
model=ai_model_id,
encoding_format="float",
extra_body={"input_type": "query", "truncate": "NONE"},
)
@patch.object(AsyncEmbeddings, "create", new_callable=AsyncMock, side_effect=Exception)
async def test_embedding_fail(mock_create, nvidia_unit_test_env) -> None:
ai_model_id = "test_model_id"
texts = ["hello world", "goodbye world"]
nvidia_text_embedding = NvidiaTextEmbedding(
ai_model_id=ai_model_id,
)
with pytest.raises(ServiceResponseException):
await nvidia_text_embedding.generate_embeddings(texts)
@patch.object(AsyncEmbeddings, "create", new_callable=AsyncMock)
async def test_embedding_pes(mock_create, nvidia_unit_test_env) -> None:
ai_model_id = "test_model_id"
texts = ["hello world", "goodbye world"]
pes = PromptExecutionSettings(service_id="x", ai_model_id=ai_model_id)
nvidia_text_embedding = NvidiaTextEmbedding(ai_model_id=ai_model_id)
await nvidia_text_embedding.generate_raw_embeddings(texts, pes)
mock_create.assert_awaited_once_with(
input=texts,
model=ai_model_id,
encoding_format="float",
extra_body={"input_type": "query", "truncate": "NONE"},
)
@@ -0,0 +1,56 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.connectors.ai.nvidia.settings.nvidia_settings import NvidiaSettings
class TestNvidiaSettings:
"""Test cases for NvidiaSettings."""
def test_init_with_defaults(self):
"""Test initialization with default values."""
settings = NvidiaSettings()
assert settings.api_key is None
assert settings.base_url == "https://integrate.api.nvidia.com/v1"
assert settings.embedding_model_id is None
assert settings.chat_model_id is None
def test_init_with_values(self):
"""Test initialization with specific values."""
settings = NvidiaSettings(
api_key="test-api-key",
base_url="https://custom.nvidia.com/v1",
embedding_model_id="test-embedding-model",
chat_model_id="test-chat-model",
)
assert settings.api_key.get_secret_value() == "test-api-key"
assert settings.base_url == "https://custom.nvidia.com/v1"
assert settings.embedding_model_id == "test-embedding-model"
assert settings.chat_model_id == "test-chat-model"
def test_env_prefix(self):
"""Test environment variable prefix."""
assert NvidiaSettings.env_prefix == "NVIDIA_"
def test_api_key_secret_str(self):
"""Test that api_key is properly handled as SecretStr."""
settings = NvidiaSettings(api_key="secret-key")
# Should be SecretStr type
assert hasattr(settings.api_key, "get_secret_value")
assert settings.api_key.get_secret_value() == "secret-key"
# Should not expose the secret in string representation
str_repr = str(settings)
assert "secret-key" not in str_repr
def test_environment_variables(self, monkeypatch):
"""Test that environment variables override defaults."""
monkeypatch.setenv("NVIDIA_API_KEY", "env-key")
monkeypatch.setenv("NVIDIA_CHAT_MODEL_ID", "env-chat")
settings = NvidiaSettings()
assert settings.api_key.get_secret_value() == "env-key"
assert settings.chat_model_id == "env-chat"
@@ -0,0 +1,84 @@
# Copyright (c) Microsoft. All rights reserved.
from collections.abc import AsyncGenerator, AsyncIterator
from unittest.mock import MagicMock
import pytest
from ollama import AsyncClient
from semantic_kernel.contents.chat_history import ChatHistory
@pytest.fixture()
def model_id() -> str:
return "test_model_id"
@pytest.fixture()
def service_id() -> str:
return "test_service_id"
@pytest.fixture()
def host() -> str:
return "http://localhost:5000"
@pytest.fixture()
def custom_client() -> AsyncClient:
return AsyncClient("http://localhost:5001")
@pytest.fixture()
def chat_history() -> ChatHistory:
chat_history = ChatHistory()
chat_history.add_user_message("test_prompt")
return chat_history
@pytest.fixture()
def prompt() -> str:
return "test_prompt"
@pytest.fixture()
def default_options() -> dict:
return {"test": "test"}
@pytest.fixture()
def ollama_unit_test_env(monkeypatch, host, exclude_list):
"""Fixture to set environment variables for OllamaSettings."""
if exclude_list is None:
exclude_list = []
env_vars = {
"OLLAMA_CHAT_MODEL_ID": "test_chat_model_id",
"OLLAMA_TEXT_MODEL_ID": "test_text_model_id",
"OLLAMA_EMBEDDING_MODEL_ID": "test_embedding_model_id",
"OLLAMA_HOST": host,
}
for key, value in env_vars.items():
if key not in exclude_list:
monkeypatch.setenv(key, value)
else:
monkeypatch.delenv(key, raising=False)
return env_vars
@pytest.fixture()
def mock_streaming_text_response() -> AsyncIterator:
streaming_text_response = MagicMock(spec=AsyncGenerator)
streaming_text_response.__aiter__.return_value = [{"response": "test_response"}]
return streaming_text_response
@pytest.fixture()
def mock_streaming_chat_response() -> AsyncIterator:
streaming_chat_response = MagicMock(spec=AsyncGenerator)
streaming_chat_response.__aiter__.return_value = [{"message": {"content": "test_response"}}]
return streaming_chat_response
@@ -0,0 +1,453 @@
# Copyright (c) Microsoft. All rights reserved.
from collections.abc import AsyncGenerator
from unittest.mock import AsyncMock, MagicMock, patch
import httpx
import pytest
from ollama import AsyncClient
import semantic_kernel.connectors.ai.ollama.services.ollama_chat_completion as occ_module
from semantic_kernel.connectors.ai.completion_usage import CompletionUsage
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
from semantic_kernel.connectors.ai.ollama.ollama_prompt_execution_settings import OllamaChatPromptExecutionSettings
from semantic_kernel.connectors.ai.ollama.services.ollama_chat_completion import OllamaChatCompletion
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.streaming_chat_message_content import StreamingChatMessageContent
from semantic_kernel.contents.streaming_text_content import StreamingTextContent
from semantic_kernel.exceptions.service_exceptions import (
ServiceInitializationError,
ServiceInvalidExecutionSettingsError,
ServiceInvalidResponseError,
)
def test_settings(model_id):
"""Test that the settings class is correct."""
ollama = OllamaChatCompletion(ai_model_id=model_id)
settings = ollama.get_prompt_execution_settings_class()
assert settings == OllamaChatPromptExecutionSettings
def test_init_empty_service_id(model_id):
"""Test that the service initializes correctly with an empty service id."""
ollama = OllamaChatCompletion(ai_model_id=model_id)
assert ollama.service_id == model_id
def test_init_empty_string_ai_model_id():
"""Test that the service initializes with a error if there is no ai_model_id."""
with pytest.raises(ServiceInitializationError):
_ = OllamaChatCompletion(ai_model_id="")
def test_custom_client(model_id, custom_client):
"""Test that the service initializes correctly with a custom client."""
ollama = OllamaChatCompletion(ai_model_id=model_id, client=custom_client)
assert ollama.client == custom_client
def test_invalid_ollama_settings():
"""Test that the service initializes incorrectly with invalid settings."""
with pytest.raises(ServiceInitializationError):
_ = OllamaChatCompletion(ai_model_id=123)
@pytest.mark.parametrize("exclude_list", [["OLLAMA_CHAT_MODEL_ID"]], indirect=True)
def test_init_empty_model_id_in_env(ollama_unit_test_env):
"""Test that the service initializes incorrectly with an empty model id."""
with pytest.raises(ServiceInitializationError):
_ = OllamaChatCompletion(env_file_path="fake_env_file_path.env")
def test_function_choice_settings(ollama_unit_test_env):
"""Test that REQUIRED and NONE function choice settings are unsupported."""
ollama = OllamaChatCompletion()
with pytest.raises(ServiceInvalidExecutionSettingsError):
ollama._verify_function_choice_settings(
OllamaChatPromptExecutionSettings(function_choice_behavior=FunctionChoiceBehavior.Required())
)
with pytest.raises(ServiceInvalidExecutionSettingsError):
ollama._verify_function_choice_settings(
OllamaChatPromptExecutionSettings(function_choice_behavior=FunctionChoiceBehavior.NoneInvoke())
)
def test_service_url(ollama_unit_test_env):
"""Test that the service URL is correct."""
ollama = OllamaChatCompletion()
assert ollama.service_url() == ollama_unit_test_env["OLLAMA_HOST"]
@patch("ollama.AsyncClient.__init__", return_value=None) # mock_client
@patch("ollama.AsyncClient.chat") # mock_chat_client
async def test_custom_host(
mock_chat_client,
mock_client,
model_id,
service_id,
host,
chat_history,
prompt,
default_options,
):
"""Test that the service initializes and generates content correctly with a custom host."""
mock_chat_client.return_value = {"message": {"content": "test_response"}}
ollama = OllamaChatCompletion(ai_model_id=model_id, host=host)
chat_responses = await ollama.get_chat_message_contents(
chat_history,
OllamaChatPromptExecutionSettings(service_id=service_id, options=default_options),
)
# Check that the client was initialized once with the correct host
assert mock_client.call_count == 1
mock_client.assert_called_with(host=host)
# Check that the chat client was called once and the responses are correct
assert mock_chat_client.call_count == 1
assert len(chat_responses) == 1
assert chat_responses[0].content == "test_response"
@patch("ollama.AsyncClient.__init__", return_value=None) # mock_client
@patch("ollama.AsyncClient.chat") # mock_chat_client
async def test_custom_host_streaming(
mock_chat_client,
mock_client,
mock_streaming_chat_response,
model_id,
service_id,
host,
chat_history,
prompt,
default_options,
):
"""Test that the service initializes and generates streaming content correctly with a custom host."""
mock_chat_client.return_value = mock_streaming_chat_response
ollama = OllamaChatCompletion(ai_model_id=model_id, host=host)
async for messages in ollama.get_streaming_chat_message_contents(
chat_history,
OllamaChatPromptExecutionSettings(service_id=service_id, options=default_options),
):
assert len(messages) == 1
assert messages[0].role == "assistant"
assert messages[0].content == "test_response"
# Check that the client was initialized once with the correct host
assert mock_client.call_count == 1
mock_client.assert_called_with(host=host)
# Check that the chat client was called once
assert mock_chat_client.call_count == 1
@patch("ollama.AsyncClient.chat")
async def test_chat_completion(mock_chat_client, model_id, service_id, chat_history, default_options):
"""Test that the chat completion service completes correctly."""
mock_chat_client.return_value = {"message": {"content": "test_response"}}
ollama = OllamaChatCompletion(ai_model_id=model_id)
response = await ollama.get_chat_message_contents(
chat_history=chat_history,
settings=OllamaChatPromptExecutionSettings(service_id=service_id, options=default_options),
)
assert len(response) == 1
assert response[0].content == "test_response"
mock_chat_client.assert_called_once_with(
model=model_id,
messages=ollama._prepare_chat_history_for_request(chat_history),
options=default_options,
stream=False,
)
@patch("ollama.AsyncClient.chat")
async def test_chat_completion_wrong_return_type(
mock_chat_client,
mock_streaming_chat_response,
model_id,
service_id,
chat_history,
default_options,
):
"""Test that the chat completion service fails when the return type is incorrect."""
mock_chat_client.return_value = mock_streaming_chat_response # should not be a streaming response
ollama = OllamaChatCompletion(ai_model_id=model_id)
with pytest.raises(ServiceInvalidResponseError):
await ollama.get_chat_message_contents(
chat_history,
OllamaChatPromptExecutionSettings(service_id=service_id, options=default_options),
)
@patch("ollama.AsyncClient.chat")
async def test_streaming_chat_completion(
mock_chat_client,
mock_streaming_chat_response,
model_id,
service_id,
chat_history,
default_options,
):
"""Test that the streaming chat completion service completes correctly."""
mock_chat_client.return_value = mock_streaming_chat_response
ollama = OllamaChatCompletion(ai_model_id=model_id)
response = ollama.get_streaming_chat_message_contents(
chat_history,
OllamaChatPromptExecutionSettings(service_id=service_id, options=default_options),
)
responses = []
async for line in response:
if line:
assert line[0].content == "test_response"
responses.append(line[0].content)
assert len(responses) == 1
mock_chat_client.assert_called_once_with(
model=model_id,
messages=ollama._prepare_chat_history_for_request(chat_history),
options=default_options,
stream=True,
)
@patch("ollama.AsyncClient.chat")
async def test_streaming_chat_completion_wrong_return_type(
mock_chat_client,
model_id,
service_id,
chat_history,
default_options,
):
"""Test that the chat completion streaming service fails when the return type is incorrect."""
mock_chat_client.return_value = {"message": {"content": "test_response"}} # should not be a non-streaming response
ollama = OllamaChatCompletion(ai_model_id=model_id)
with pytest.raises(ServiceInvalidResponseError):
async for _ in ollama.get_streaming_chat_message_contents(
chat_history,
OllamaChatPromptExecutionSettings(service_id=service_id, options=default_options),
):
pass
@pytest.fixture
async def setup_ollama_chat_completion():
async_client_mock = AsyncMock(spec=AsyncClient)
async_client_mock.chat = AsyncMock()
ollama_chat_completion = OllamaChatCompletion(
service_id="test_service", ai_model_id="test_model_id", client=async_client_mock
)
return ollama_chat_completion, async_client_mock
async def test_service_url_new(setup_ollama_chat_completion):
ollama_chat_completion, async_client_mock = setup_ollama_chat_completion
# Mock the client's internal structure
async_client_mock._client = AsyncMock(spec=httpx.AsyncClient)
async_client_mock._client.base_url = "http://mocked_base_url"
service_url = ollama_chat_completion.service_url()
assert service_url == "http://mocked_base_url"
async def test_prepare_chat_history_for_request(setup_ollama_chat_completion):
ollama_chat_completion, _ = setup_ollama_chat_completion
chat_history = MagicMock(spec=ChatHistory)
chat_history.messages = []
prepared_history = ollama_chat_completion._prepare_chat_history_for_request(chat_history)
assert prepared_history == []
async def test_service_url_with_httpx_client(model_id: str) -> None:
"""
Test that service_url returns the base_url of the underlying httpx.AsyncClient.
"""
# Initialize an AsyncClient and manually set its _client attribute to an httpx.AsyncClient
client = AsyncClient(host="unused")
base = httpx.AsyncClient(base_url="http://example.com:8000")
client._client = base # simulate underlying httpx client
ollama = OllamaChatCompletion(ai_model_id=model_id, client=client)
# service_url should reflect the base_url of the httpx client
assert ollama.service_url() == "http://example.com:8000"
@patch("ollama.AsyncClient.chat", new_callable=AsyncMock)
async def test_chat_response_branch(
mock_chat: AsyncMock,
model_id: str,
service_id: str,
default_options: dict,
chat_history,
monkeypatch,
) -> None:
"""
Test get_chat_message_contents when AsyncClient.chat returns a ChatResponse instance.
"""
class DummyFunction:
def __init__(self, name, arguments):
self.name = name
self.arguments = arguments
class DummyToolCall:
def __init__(self, function):
self.function = function
class DummyMessage:
def __init__(self, content: str, tool_calls=None) -> None:
self.content = content
self.tool_calls = tool_calls or []
class DummyChatResponse:
def __init__(
self,
content: str,
model: str,
prompt_eval_count: int,
eval_count: int,
tool_calls=None,
) -> None:
function_calls = [
DummyToolCall(DummyFunction(tc["function"]["name"], tc["function"]["arguments"])) for tc in tool_calls
]
self.message = DummyMessage(content, function_calls)
self.model = model
self.prompt_eval_count = prompt_eval_count
self.eval_count = eval_count
# Monkeypatch the ChatResponse type in the module so isinstance works
monkeypatch.setattr(occ_module, "ChatResponse", DummyChatResponse)
# Prepare a dummy ChatResponse return value
dummy_resp = DummyChatResponse(
content="resp_text",
model="mdl",
prompt_eval_count=2,
eval_count=3,
tool_calls=[{"function": {"name": "fn", "arguments": {"x": 1}}}],
)
mock_chat.return_value = dummy_resp
ollama = OllamaChatCompletion(ai_model_id=model_id)
settings = OllamaChatPromptExecutionSettings(service_id=service_id, options=default_options)
results = await ollama.get_chat_message_contents(chat_history, settings)
# Only one response expected
assert len(results) == 1
msg = results[0]
# Assert it's a ChatMessageContent
assert isinstance(msg, ChatMessageContent)
# The content property should return the response text
assert msg.content == "resp_text"
# The second item should be a FunctionCallContent
func_item = msg.items[1]
assert isinstance(func_item, FunctionCallContent)
# Validate function call details
assert func_item.name == "fn"
assert func_item.arguments == {"x": 1}
# Check metadata
assert "model" in msg.metadata and msg.metadata["model"] == "mdl"
# Access usage directly, key should exist
usage = msg.metadata["usage"]
assert isinstance(usage, CompletionUsage)
assert usage.prompt_tokens == 2 and usage.completion_tokens == 3
@patch("ollama.AsyncClient.chat", new_callable=AsyncMock)
async def test_streaming_chat_response_branch(
mock_chat: AsyncMock,
model_id: str,
service_id: str,
default_options: dict,
chat_history,
monkeypatch,
) -> None:
"""
Test get_streaming_chat_message_contents when AsyncClient.chat yields ChatResponse instances.
"""
class DummyFunction:
def __init__(self, name, arguments):
self.name = name
self.arguments = arguments
class DummyToolCall:
def __init__(self, function):
self.function = function
class DummyMessage:
def __init__(self, content: str, tool_calls=None) -> None:
self.content = content
self.tool_calls = tool_calls or []
class DummyChatResponse:
def __init__(
self,
content: str,
model: str,
prompt_eval_count: int,
eval_count: int,
tool_calls=None,
) -> None:
function_calls = [
DummyToolCall(DummyFunction(tc["function"]["name"], tc["function"]["arguments"])) for tc in tool_calls
]
self.message = DummyMessage(content, function_calls)
self.model = model
self.prompt_eval_count = prompt_eval_count
self.eval_count = eval_count
# Monkeypatch ChatResponse type
monkeypatch.setattr(occ_module, "ChatResponse", DummyChatResponse)
# Prepare an async generator yielding DummyChatResponse
async def fake_stream() -> AsyncGenerator[DummyChatResponse, None]:
yield DummyChatResponse(
content="stream_text",
model="m2",
prompt_eval_count=1,
eval_count=1,
tool_calls=[{"function": {"name": "f2", "arguments": {}}}],
)
mock_chat.return_value = fake_stream()
ollama = OllamaChatCompletion(ai_model_id=model_id)
settings = OllamaChatPromptExecutionSettings(service_id=service_id, options=default_options)
collected = []
# Iterate over streamed batches
async for batch in ollama.get_streaming_chat_message_contents(chat_history, settings):
# We expect a list with a single StreamingChatMessageContent
assert len(batch) == 1
sc = batch[0]
assert isinstance(sc, StreamingChatMessageContent)
# First item should be text content
text_item = sc.items[0]
assert isinstance(text_item, StreamingTextContent)
assert text_item.text == "stream_text"
# Next item should be a FunctionCallContent
func_item = sc.items[1]
assert isinstance(func_item, FunctionCallContent)
assert func_item.name == "f2"
collected.append(sc)
# Only one batch should be collected
assert len(collected) == 1
@@ -0,0 +1,178 @@
# Copyright (c) Microsoft. All rights reserved.
from collections.abc import AsyncGenerator
from unittest.mock import MagicMock, patch
import pytest
from semantic_kernel.connectors.ai.ollama.ollama_prompt_execution_settings import OllamaTextPromptExecutionSettings
from semantic_kernel.connectors.ai.ollama.services.ollama_text_completion import OllamaTextCompletion
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError, ServiceInvalidResponseError
def test_settings(model_id):
"""Test that the settings class is correct"""
ollama = OllamaTextCompletion(ai_model_id=model_id)
settings = ollama.get_prompt_execution_settings_class()
assert settings == OllamaTextPromptExecutionSettings
def test_init_empty_service_id(model_id):
"""Test that the service initializes correctly with an empty service_id"""
ollama = OllamaTextCompletion(ai_model_id=model_id)
assert ollama.service_id == model_id
def test_custom_client(model_id, custom_client):
"""Test that the service initializes correctly with a custom client."""
ollama = OllamaTextCompletion(ai_model_id=model_id, client=custom_client)
assert ollama.client == custom_client
def test_invalid_ollama_settings():
"""Test that the service initializes incorrectly with invalid settings."""
with pytest.raises(ServiceInitializationError):
_ = OllamaTextCompletion(ai_model_id=123)
def test_service_url(ollama_unit_test_env):
"""Test that the service URL is correct."""
ollama = OllamaTextCompletion()
assert ollama.service_url() == ollama_unit_test_env["OLLAMA_HOST"]
@pytest.mark.parametrize("exclude_list", [["OLLAMA_TEXT_MODEL_ID"]], indirect=True)
def test_init_empty_model_id(ollama_unit_test_env):
"""Test that the service initializes incorrectly with an empty model_id"""
with pytest.raises(ServiceInitializationError):
_ = OllamaTextCompletion(env_file_path="fake_env_file_path.env")
@patch("ollama.AsyncClient.__init__", return_value=None) # mock_client
@patch("ollama.AsyncClient.generate") # mock_completion_client
async def test_custom_host(
mock_completion_client, mock_client, model_id, service_id, host, chat_history, default_options
):
"""Test that the service initializes and generates content correctly with a custom host."""
mock_completion_client.return_value = {"response": "test_response"}
ollama = OllamaTextCompletion(ai_model_id=model_id, host=host)
_ = await ollama.get_text_contents(
chat_history,
OllamaTextPromptExecutionSettings(service_id=service_id, options=default_options),
)
mock_client.assert_called_once_with(host=host)
@patch("ollama.AsyncClient.__init__", return_value=None) # mock_client
@patch("ollama.AsyncClient.generate") # mock_completion_client
async def test_custom_host_streaming(
mock_completion_client, mock_client, model_id, service_id, host, chat_history, default_options
):
"""Test that the service initializes and generates streaming content correctly with a custom host."""
# Create a proper async iterator mock for streaming
streaming_response = MagicMock(spec=AsyncGenerator)
streaming_response.__aiter__.return_value = [{"response": "test_response"}]
mock_completion_client.return_value = streaming_response
ollama = OllamaTextCompletion(ai_model_id=model_id, host=host)
async for _ in ollama.get_streaming_text_contents(
chat_history,
OllamaTextPromptExecutionSettings(service_id=service_id, options=default_options),
):
pass
mock_client.assert_called_once_with(host=host)
@patch("ollama.AsyncClient.generate")
async def test_completion(mock_completion_client, model_id, service_id, prompt, default_options):
"""Test that the service generates content correctly."""
mock_completion_client.return_value = {"response": "test_response"}
ollama = OllamaTextCompletion(ai_model_id=model_id)
response = await ollama.get_text_contents(
prompt=prompt,
settings=OllamaTextPromptExecutionSettings(service_id=service_id, options=default_options),
)
assert response[0].text == "test_response"
mock_completion_client.assert_called_once_with(
model=model_id,
prompt=prompt,
options=default_options,
stream=False,
)
@patch("ollama.AsyncClient.generate")
async def test_completion_wrong_return_type(
mock_completion_client,
mock_streaming_text_response,
model_id,
service_id,
chat_history,
default_options,
):
"""Test that the completion service fails when the return type is incorrect."""
mock_completion_client.return_value = mock_streaming_text_response # should not be a streaming response
ollama = OllamaTextCompletion(ai_model_id=model_id)
with pytest.raises(ServiceInvalidResponseError):
await ollama.get_text_contents(
chat_history,
OllamaTextPromptExecutionSettings(service_id=service_id, options=default_options),
)
@patch("ollama.AsyncClient.generate")
async def test_streaming_completion(
mock_completion_client,
mock_streaming_text_response,
model_id,
service_id,
prompt,
default_options,
):
"""Test that the service generates streaming content correctly."""
mock_completion_client.return_value = mock_streaming_text_response
ollama = OllamaTextCompletion(ai_model_id=model_id)
response = ollama.get_streaming_text_contents(
prompt,
OllamaTextPromptExecutionSettings(service_id=service_id, options=default_options),
)
responses = []
async for line in response:
assert line[0].text == "test_response"
responses.append(line)
assert len(responses) == 1
mock_completion_client.assert_called_once_with(
model=model_id,
prompt=prompt,
options=default_options,
stream=True,
)
@patch("ollama.AsyncClient.generate")
async def test_streaming_completion_wrong_return_type(
mock_completion_client,
model_id,
service_id,
chat_history,
default_options,
):
"""Test that the streaming completion service fails when the return type is incorrect."""
mock_completion_client.return_value = {"response": "test_response"} # should not be a non-streaming response
ollama = OllamaTextCompletion(ai_model_id=model_id)
with pytest.raises(ServiceInvalidResponseError):
async for _ in ollama.get_streaming_text_contents(
chat_history,
OllamaTextPromptExecutionSettings(service_id=service_id, options=default_options),
):
pass
@@ -0,0 +1,122 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import patch
import numpy
import pytest
from numpy import array
from semantic_kernel.connectors.ai.ollama.ollama_prompt_execution_settings import OllamaEmbeddingPromptExecutionSettings
from semantic_kernel.connectors.ai.ollama.services.ollama_text_embedding import OllamaTextEmbedding
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError
def test_init_empty_service_id(model_id):
"""Test that the service initializes correctly with an empty service id."""
ollama = OllamaTextEmbedding(ai_model_id=model_id)
assert ollama.service_id == model_id
def test_custom_client(model_id, custom_client):
"""Test that the service initializes correctly with a custom client."""
ollama = OllamaTextEmbedding(ai_model_id=model_id, client=custom_client)
assert ollama.client == custom_client
def test_invalid_ollama_settings():
"""Test that the service initializes incorrectly with invalid settings."""
with pytest.raises(ServiceInitializationError):
_ = OllamaTextEmbedding(ai_model_id=123)
@pytest.mark.parametrize("exclude_list", [["OLLAMA_EMBEDDING_MODEL_ID"]], indirect=True)
def test_init_empty_model_id(ollama_unit_test_env):
"""Test that the service initializes incorrectly with an empty model id."""
with pytest.raises(ServiceInitializationError):
_ = OllamaTextEmbedding(env_file_path="fake_env_file_path.env")
@patch("ollama.AsyncClient.__init__", return_value=None) # mock_client
@patch("ollama.AsyncClient.embeddings") # mock_embedding_client
async def test_custom_host(mock_embedding_client, mock_client, model_id, host, prompt):
"""Test that the service initializes and generates embeddings correctly with a custom host."""
mock_embedding_client.return_value = {"embedding": [0.1, 0.2, 0.3]}
ollama = OllamaTextEmbedding(ai_model_id=model_id, host=host)
_ = await ollama.generate_embeddings(
[prompt],
)
mock_client.assert_called_once_with(host=host)
@patch("ollama.AsyncClient.embeddings")
async def test_embedding(mock_embedding_client, model_id, prompt):
"""Test that the service initializes and generates embeddings correctly."""
mock_embedding_client.return_value = {"embedding": [0.1, 0.2, 0.3]}
settings = OllamaEmbeddingPromptExecutionSettings()
settings.options = {"test_key": "test_value"}
ollama = OllamaTextEmbedding(ai_model_id=model_id)
response = await ollama.generate_embeddings(
[prompt],
settings=settings,
)
assert response.all() == array([0.1, 0.2, 0.3]).all()
mock_embedding_client.assert_called_once_with(model=model_id, prompt=prompt, options=settings.options)
@patch("ollama.AsyncClient.embeddings")
async def test_embedding_list_input(mock_embedding_client, model_id, prompt):
"""Test that the service initializes and generates embeddings correctly with a list of prompts."""
mock_embedding_client.return_value = {"embedding": [0.1, 0.2, 0.3]}
settings = OllamaEmbeddingPromptExecutionSettings()
settings.options = {"test_key": "test_value"}
ollama = OllamaTextEmbedding(ai_model_id=model_id)
responses = await ollama.generate_embeddings(
[prompt, prompt],
settings=settings,
)
assert len(responses) == 2
assert type(responses) is numpy.ndarray
assert all(type(response) is numpy.ndarray for response in responses)
assert mock_embedding_client.call_count == 2
mock_embedding_client.assert_called_with(model=model_id, prompt=prompt, options=settings.options)
@patch("ollama.AsyncClient.embeddings")
async def test_raw_embedding(mock_embedding_client, model_id, prompt):
"""Test that the service initializes and generates embeddings correctly."""
mock_embedding_client.return_value = {"embedding": [0.1, 0.2, 0.3]}
settings = OllamaEmbeddingPromptExecutionSettings()
settings.options = {"test_key": "test_value"}
ollama = OllamaTextEmbedding(ai_model_id=model_id)
response = await ollama.generate_raw_embeddings(
[prompt],
settings=settings,
)
assert response == [[0.1, 0.2, 0.3]]
mock_embedding_client.assert_called_once_with(model=model_id, prompt=prompt, options=settings.options)
@patch("ollama.AsyncClient.embeddings")
async def test_raw_embedding_list_input(mock_embedding_client, model_id, prompt):
"""Test that the service initializes and generates embeddings correctly with a list of prompts."""
mock_embedding_client.return_value = {"embedding": [0.1, 0.2, 0.3]}
settings = OllamaEmbeddingPromptExecutionSettings()
settings.options = {"test_key": "test_value"}
ollama = OllamaTextEmbedding(ai_model_id=model_id)
responses = await ollama.generate_raw_embeddings(
[prompt, prompt],
settings=settings,
)
assert responses == [[0.1, 0.2, 0.3], [0.1, 0.2, 0.3]]
assert mock_embedding_client.call_count == 2
mock_embedding_client.assert_called_with(model=model_id, prompt=prompt, options=settings.options)
@@ -0,0 +1,240 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import MagicMock, patch
import pytest
from semantic_kernel.connectors.ai.function_call_choice_configuration import FunctionCallChoiceConfiguration
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceType
# The code under test
from semantic_kernel.connectors.ai.ollama.services.utils import (
MESSAGE_CONVERTERS,
update_settings_from_function_choice_configuration,
)
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
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.utils.author_role import AuthorRole
@pytest.fixture
def mock_chat_message_content() -> ChatMessageContent:
"""Fixture to create a basic ChatMessageContent object with role=USER and simple text content."""
return ChatMessageContent(
role=AuthorRole.USER,
content="Hello, I am a user message.", # The text content
)
@pytest.fixture
def mock_system_message_content() -> ChatMessageContent:
"""Fixture to create a ChatMessageContent object with role=SYSTEM."""
return ChatMessageContent(role=AuthorRole.SYSTEM, content="This is a system message.")
@pytest.fixture
def mock_assistant_message_content() -> ChatMessageContent:
"""Fixture to create a ChatMessageContent object with role=ASSISTANT."""
return ChatMessageContent(role=AuthorRole.ASSISTANT, content="This is an assistant message.")
@pytest.fixture
def mock_tool_message_content() -> ChatMessageContent:
"""Fixture to create a ChatMessageContent object with role=TOOL."""
return ChatMessageContent(role=AuthorRole.TOOL, content="This is a tool message.")
def test_message_converters_system(mock_system_message_content: ChatMessageContent) -> None:
"""Test that passing a system message returns the correct dictionary structure for 'system' role."""
# Act
converter = MESSAGE_CONVERTERS[AuthorRole.SYSTEM]
result = converter(mock_system_message_content)
# Assert
assert result["role"] == "system", "Expected role to be 'system' on the returned message."
assert result["content"] == mock_system_message_content.content, (
"Expected content to match the system message content."
)
def test_message_converters_user_no_images(mock_chat_message_content: ChatMessageContent) -> None:
"""Test that passing a user message without images returns correct dictionary structure for 'user' role."""
# Act
converter = MESSAGE_CONVERTERS[AuthorRole.USER]
result = converter(mock_chat_message_content)
# Assert
assert result["role"] == "user", "Expected role to be 'user' on the returned message."
assert result["content"] == mock_chat_message_content.content, "Expected content to match the user message content."
# Ensure that no 'images' field is added
assert "images" not in result, "No images should be present if no ImageContent is added."
def test_message_converters_user_with_images() -> None:
"""Test user message with multiple images, verifying the 'images' field is populated."""
# Arrange
img1 = ImageContent(data="some_base64_data")
img2 = ImageContent(data="other_base64_data")
content = ChatMessageContent(role=AuthorRole.USER, items=[img1, img2], content="User with images")
# Act
converter = MESSAGE_CONVERTERS[AuthorRole.USER]
result = converter(content)
# Assert
assert result["role"] == "user"
assert result["content"] == content.content
assert "images" in result, "Images field expected when ImageContent is present."
assert len(result["images"]) == 2, "Two images should be in the 'images' field."
assert result["images"] == [b"some_base64_data", b"other_base64_data"], (
"Image data should match the content from ImageContent."
)
def test_message_converters_user_with_image_missing_data() -> None:
"""Test user message with image content that has missing data, expecting ValueError."""
# Arrange
bad_image = ImageContent(data="") # empty data for image
content = ChatMessageContent(role=AuthorRole.USER, items=[bad_image])
# Act & Assert
converter = MESSAGE_CONVERTERS[AuthorRole.USER]
with pytest.raises(ValueError) as exc_info:
converter(content)
assert "Image item must contain data encoded as base64." in str(exc_info.value), (
"Should raise ValueError for missing base64 data in image."
)
def test_message_converters_assistant_basic(mock_assistant_message_content: ChatMessageContent) -> None:
"""Test assistant message without images or tool calls."""
# Act
converter = MESSAGE_CONVERTERS[AuthorRole.ASSISTANT]
result = converter(mock_assistant_message_content)
# Assert
assert result["role"] == "assistant", "Assistant role expected."
assert result["content"] == mock_assistant_message_content.content
assert "images" not in result, "No images included, so should not have an 'images' field."
assert "tool_calls" not in result, "No FunctionCallContent, so 'tool_calls' field shouldn't be present."
def test_message_converters_assistant_with_image() -> None:
"""Test assistant message containing images. Verify 'images' field is added."""
# Arrange
img = ImageContent(data="assistant_base64_data")
content = ChatMessageContent(role=AuthorRole.ASSISTANT, items=[img], content="Assistant image message")
# Act
converter = MESSAGE_CONVERTERS[AuthorRole.ASSISTANT]
result = converter(content)
# Assert
assert result["role"] == "assistant"
assert result["content"] == content.content
assert "images" in result, "Images should be included for assistant messages with ImageContent."
assert result["images"] == [b"assistant_base64_data"], "Expected matching base64 data in images."
def test_message_converters_assistant_with_tool_calls() -> None:
"""Test assistant message with FunctionCallContent should populate 'tool_calls'."""
# Arrange
tool_call_1 = FunctionCallContent(function_name="foo", arguments='{"key": "value"}')
tool_call_2 = FunctionCallContent(function_name="bar", arguments='{"another": "123"}')
content = ChatMessageContent(
role=AuthorRole.ASSISTANT, items=[tool_call_1, tool_call_2], content="Assistant with tools"
)
# Act
converter = MESSAGE_CONVERTERS[AuthorRole.ASSISTANT]
result = converter(content)
# Assert
assert result["role"] == "assistant"
assert result["content"] == content.content
assert "tool_calls" in result, "tool_calls field should be present for assistant messages with FunctionCallContent."
assert len(result["tool_calls"]) == 2, "Expected two tool calls in the result."
assert result["tool_calls"][0]["function"]["name"] == "foo", "First tool call function name mismatched."
assert result["tool_calls"][0]["function"]["arguments"] == {"key": "value"}, "Expected arguments to be JSON loaded."
assert result["tool_calls"][1]["function"]["name"] == "bar", "Second tool call function name mismatched."
assert result["tool_calls"][1]["function"]["arguments"] == {"another": "123"}, (
"Expected arguments to be JSON loaded."
)
def test_message_converters_tool_with_result() -> None:
"""Test tool message with a FunctionResultContent, verifying the message content is set."""
# Arrange
fr_content = FunctionResultContent(id="some_id", result="some result", function_name="test_func")
tool_message = ChatMessageContent(role=AuthorRole.TOOL, items=[fr_content])
# Act
converter = MESSAGE_CONVERTERS[AuthorRole.TOOL]
result = converter(tool_message)
# Assert
assert result["role"] == "tool", "Expected role to be 'tool' for a tool message."
# The code takes the first FunctionResultContent's result as the content
assert result["content"] == fr_content.result, "Expected content to match the function result."
def test_message_converters_tool_missing_function_result_content(mock_tool_message_content: ChatMessageContent) -> None:
"""Test that if no FunctionResultContent is present, ValueError is raised."""
# Arrange
mock_tool_message_content.items = [] # no FunctionResultContent in items
converter = MESSAGE_CONVERTERS[AuthorRole.TOOL]
# Act & Assert
with pytest.raises(ValueError) as exc_info:
converter(mock_tool_message_content)
assert "Tool message must have a function result content item." in str(exc_info.value)
@pytest.mark.parametrize("choice_type", [FunctionChoiceType.AUTO, FunctionChoiceType.NONE, FunctionChoiceType.REQUIRED])
def test_update_settings_from_function_choice_configuration(choice_type: FunctionChoiceType) -> None:
"""Test that update_settings_from_function_choice_configuration updates the settings with the correct tools."""
# Arrange
# We'll create a mock configuration with some available functions.
mock_config = FunctionCallChoiceConfiguration()
mock_config.available_functions = [MagicMock() for _ in range(2)]
# We also patch the kernel_function_metadata_to_function_call_format function.
# The function returns a dict object describing each function.
mock_tool_description = {"type": "function", "function": {"name": "mocked_function"}}
with patch(
"semantic_kernel.connectors.ai.ollama.services.utils.kernel_function_metadata_to_function_call_format",
return_value=mock_tool_description,
):
settings = PromptExecutionSettings()
# Act
update_settings_from_function_choice_configuration(
function_choice_configuration=mock_config,
settings=settings,
type=choice_type,
)
# Assert
# After the call, either settings.tools or settings.extension_data["tools"] should be set.
# The code tries settings.tools first and if it fails, it sets extension_data["tools"].
# We'll check both possibilities.
possible_tools = getattr(settings, "tools", None)
if possible_tools is not None:
# If settings.tools exists, ensure it got updated
assert len(possible_tools) == 2, "Should have exactly two tools set in the settings.tools attribute."
assert possible_tools[0]["function"]["name"] == "mocked_function", (
"Expected mocked function name in settings.tools."
)
else:
# Otherwise check for extension_data
assert "tools" in settings.extension_data, "Expected 'tools' in extension_data if settings.tools not present."
assert len(settings.extension_data["tools"]) == 2, "Should have exactly two tools in extension_data."
assert settings.extension_data["tools"][0]["function"]["name"] == "mocked_function", (
"Expected mocked function name in extension_data."
)
@@ -0,0 +1,95 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.connectors.ai.ollama import OllamaPromptExecutionSettings
from semantic_kernel.connectors.ai.ollama.ollama_prompt_execution_settings import (
OllamaChatPromptExecutionSettings,
OllamaTextPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
def test_default_ollama_prompt_execution_settings():
settings = OllamaPromptExecutionSettings()
assert settings.format is None
assert settings.options is None
def test_custom_ollama_prompt_execution_settings():
settings = OllamaPromptExecutionSettings(
format="json",
options={
"key": "value",
},
)
assert settings.format == "json"
assert settings.options == {"key": "value"}
def test_ollama_prompt_execution_settings_from_default_completion_config():
settings = PromptExecutionSettings(service_id="test_service")
chat_settings = OllamaChatPromptExecutionSettings.from_prompt_execution_settings(settings)
assert chat_settings.service_id == "test_service"
assert chat_settings.format is None
assert chat_settings.options is None
def test_ollama_prompt_execution_settings_from_openai_prompt_execution_settings():
chat_settings = OllamaChatPromptExecutionSettings(service_id="test_service", options={"temperature": 0.5})
new_settings = OllamaPromptExecutionSettings(service_id="test_2", options={"temperature": 0.0})
chat_settings.update_from_prompt_execution_settings(new_settings)
assert chat_settings.service_id == "test_2"
assert chat_settings.options["temperature"] == 0.0
def test_ollama_prompt_execution_settings_from_custom_completion_config():
settings = PromptExecutionSettings(
service_id="test_service",
extension_data={
"format": "json",
"options": {
"key": "value",
},
},
)
chat_settings = OllamaChatPromptExecutionSettings.from_prompt_execution_settings(settings)
assert chat_settings.service_id == "test_service"
assert chat_settings.format == "json"
assert chat_settings.options == {"key": "value"}
def test_ollama_chat_prompt_execution_settings_from_custom_completion_config_with_functions():
settings = PromptExecutionSettings(
service_id="test_service",
extension_data={
"tools": [{"function": {}}],
},
)
chat_settings = OllamaChatPromptExecutionSettings.from_prompt_execution_settings(settings)
assert chat_settings.tools == [{"function": {}}]
def test_create_options():
settings = OllamaChatPromptExecutionSettings(
service_id="test_service",
extension_data={
"format": "json",
},
)
options = settings.prepare_settings_dict()
assert options["format"] == "json"
def test_default_ollama_text_prompt_execution_settings():
settings = OllamaTextPromptExecutionSettings()
assert settings.system is None
assert settings.template is None
assert settings.context is None
assert settings.raw is None
@@ -0,0 +1,29 @@
# Copyright (c) Microsoft. All rights reserved.
import json
class MockResponse:
def __init__(self, response, status=200):
self._response = response
self.status = status
async def text(self):
return self._response
async def json(self):
return self._response
def raise_for_status(self):
pass
@property
async def content(self):
yield json.dumps(self._response).encode("utf-8")
yield json.dumps({"done": True}).encode("utf-8")
async def __aexit__(self, exc_type, exc, tb):
pass
async def __aenter__(self):
return self
@@ -0,0 +1,32 @@
# Copyright (c) Microsoft. All rights reserved.
from pytest import fixture
@fixture()
def onnx_unit_test_env(monkeypatch, exclude_list, override_env_param_dict):
"""Fixture to set environment variables for OnnxGenAISettings."""
if exclude_list is None:
exclude_list = []
if override_env_param_dict is None:
override_env_param_dict = {}
env_vars = {
"ONNX_GEN_AI_CHAT_MODEL_FOLDER": "test",
"ONNX_GEN_AI_TEXT_MODEL_FOLDER": "test",
}
env_vars.update(override_env_param_dict)
for key, value in env_vars.items():
if key not in exclude_list:
monkeypatch.setenv(key, value)
else:
monkeypatch.delenv(key, raising=False)
return env_vars
gen_ai_config = {"model": {"test": "test"}}
gen_ai_config_vision = {"model": {"vision": "test"}}
@@ -0,0 +1,187 @@
# Copyright (c) Microsoft. All rights reserved.
import json
import os
from unittest.mock import MagicMock, mock_open, patch
import pytest
from semantic_kernel.connectors.ai.onnx import OnnxGenAIChatCompletion, OnnxGenAIPromptExecutionSettings, ONNXTemplate
from semantic_kernel.contents import AuthorRole, ChatHistory, ChatMessageContent, ImageContent
from semantic_kernel.exceptions import ServiceInitializationError, ServiceInvalidExecutionSettingsError
from semantic_kernel.kernel import Kernel
from tests.unit.connectors.ai.onnx.conftest import gen_ai_config, gen_ai_config_vision
@patch("builtins.open", new_callable=mock_open, read_data=json.dumps(gen_ai_config))
@patch("onnxruntime_genai.Model")
@patch("onnxruntime_genai.Tokenizer")
def test_onnx_chat_completion_with_valid_env_variable(gen_ai_config, model, tokenizer, onnx_unit_test_env):
service = OnnxGenAIChatCompletion(template=ONNXTemplate.PHI3, env_file_path="test.env")
assert not service.enable_multi_modality
@patch("builtins.open", new_callable=mock_open, read_data=json.dumps(gen_ai_config_vision))
@patch("onnxruntime_genai.Model")
@patch("onnxruntime_genai.Tokenizer")
def test_onnx_chat_completion_with_vision_valid_env_variable(
gen_ai_vision_config, model, tokenizer, onnx_unit_test_env
):
service = OnnxGenAIChatCompletion(template=ONNXTemplate.PHI3, env_file_path="test.env")
assert service.enable_multi_modality
@patch("builtins.open", new_callable=mock_open, read_data=json.dumps(gen_ai_config))
@patch("onnxruntime_genai.Model")
@patch("onnxruntime_genai.Tokenizer")
def test_onnx_chat_completion_with_valid_parameter(gen_ai_config, model, tokenizer):
assert OnnxGenAIChatCompletion(ai_model_path="/valid_path", template=ONNXTemplate.PHI3)
@patch("builtins.open", new_callable=mock_open, read_data=json.dumps(gen_ai_config))
@patch("onnxruntime_genai.Model")
@patch("onnxruntime_genai.Tokenizer")
def test_onnx_chat_completion_with_str_template(gen_ai_config, model, tokenizer):
assert OnnxGenAIChatCompletion(ai_model_path="/valid_path", template="phi3")
def test_onnx_chat_completion_with_invalid_model():
with pytest.raises(ServiceInitializationError):
OnnxGenAIChatCompletion(
ai_model_path="/invalid_path",
template=ONNXTemplate.PHI3,
)
@patch("builtins.open", new_callable=mock_open, read_data=json.dumps(gen_ai_config_vision))
def test_onnx_chat_completion_with_multimodality_without_prompt_template(gen_ai_config_vision):
with pytest.raises(ServiceInitializationError):
OnnxGenAIChatCompletion()
def test_onnx_chat_completion_with_invalid_env_variable(onnx_unit_test_env):
with pytest.raises(ServiceInitializationError):
OnnxGenAIChatCompletion(
template=ONNXTemplate.PHI3,
)
@pytest.mark.parametrize("exclude_list", [["ONNX_GEN_AI_CHAT_MODEL_FOLDER"]], indirect=True)
def test_onnx_chat_completion_with_missing_ai_path(onnx_unit_test_env):
with pytest.raises(ServiceInitializationError):
OnnxGenAIChatCompletion(template=ONNXTemplate.PHI3, env_file_path="test.env")
@patch("builtins.open", new_callable=mock_open, read_data=json.dumps(gen_ai_config))
@patch("onnxruntime_genai.Model")
@patch("onnxruntime_genai.Tokenizer")
async def test_onnx_chat_completion(gen_ai_config, model, tokenizer):
generator_mock = MagicMock()
generator_mock.__aiter__.return_value = [["H"], ["e"], ["l"], ["l"], ["o"]]
chat_completion = OnnxGenAIChatCompletion(template=ONNXTemplate.PHI3, ai_model_path="test")
history = ChatHistory()
history.add_system_message("test")
history.add_user_message("test")
with patch.object(chat_completion, "_generate_next_token_async", return_value=generator_mock):
completed_text: ChatMessageContent = await chat_completion.get_chat_message_content(
prompt="test", chat_history=history, settings=OnnxGenAIPromptExecutionSettings(), kernel=Kernel()
)
assert str(completed_text) == "Hello"
@patch("builtins.open", new_callable=mock_open, read_data=json.dumps(gen_ai_config))
@patch("onnxruntime_genai.Model")
@patch("onnxruntime_genai.Tokenizer")
async def test_onnx_chat_completion_streaming(gen_ai_config, model, tokenizer):
generator_mock = MagicMock()
generator_mock.__aiter__.return_value = [["H"], ["e"], ["l"], ["l"], ["o"]]
chat_completion = OnnxGenAIChatCompletion(template=ONNXTemplate.PHI3, ai_model_path="test")
history = ChatHistory()
history.add_system_message("test")
history.add_user_message("test")
completed_text: str = ""
with patch.object(chat_completion, "_generate_next_token_async", return_value=generator_mock):
async for chunk in chat_completion.get_streaming_chat_message_content(
prompt="test", chat_history=history, settings=OnnxGenAIPromptExecutionSettings(), kernel=Kernel()
):
completed_text += str(chunk)
assert completed_text == "Hello"
@patch("onnxruntime_genai.Model")
def test_onnx_chat_get_image_history(model):
builtin_open = open # save the unpatched version
def patch_open(*args, **kwargs):
if "genai_config.json" in str(args[0]):
# mocked open for path "genai_config.json"
return mock_open(read_data=json.dumps(gen_ai_config_vision))(*args, **kwargs)
# unpatched version for every other path
return builtin_open(*args, **kwargs)
with patch("builtins.open", patch_open):
chat_completion = OnnxGenAIChatCompletion(
template=ONNXTemplate.PHI3,
ai_model_path="test",
)
image_content = ImageContent.from_image_path(
image_path=os.path.join(os.path.dirname(__file__), "../../../../../", "assets/sample_image.jpg")
)
history = ChatHistory()
history.add_system_message("test")
history.add_user_message("test")
history.add_message(
ChatMessageContent(
role=AuthorRole.USER,
items=[image_content],
),
)
last_image = chat_completion._get_images_from_history(history)
assert last_image == [image_content]
@patch("onnxruntime_genai.Model")
@patch("onnxruntime_genai.Tokenizer")
async def test_onnx_chat_get_image_history_with_not_multimodal(model, tokenizer):
builtin_open = open # save the unpatched version
def patch_open(*args, **kwargs):
if "genai_config.json" in str(args[0]):
# mocked open for path "genai_config.json"
return mock_open(read_data=json.dumps(gen_ai_config))(*args, **kwargs)
# unpatched version for every other path
return builtin_open(*args, **kwargs)
with patch("builtins.open", patch_open):
chat_completion = OnnxGenAIChatCompletion(
template=ONNXTemplate.PHI3,
ai_model_path="test",
)
image_content = ImageContent.from_image_path(
image_path=os.path.join(os.path.dirname(__file__), "../../../../../", "assets/sample_image.jpg")
)
history = ChatHistory()
history.add_system_message("test")
history.add_user_message("test")
history.add_message(
ChatMessageContent(
role=AuthorRole.USER,
items=[image_content],
),
)
with pytest.raises(ServiceInvalidExecutionSettingsError):
_ = await chat_completion._get_images_from_history(history)
@@ -0,0 +1,74 @@
# Copyright (c) Microsoft. All rights reserved.
import json
from unittest.mock import MagicMock, mock_open, patch
import pytest
from semantic_kernel.connectors.ai.onnx import OnnxGenAIPromptExecutionSettings, OnnxGenAITextCompletion # noqa: E402
from semantic_kernel.contents import TextContent
from semantic_kernel.exceptions import ServiceInitializationError
from tests.unit.connectors.ai.onnx.conftest import gen_ai_config
@patch("builtins.open", new_callable=mock_open, read_data=json.dumps(gen_ai_config))
@patch("onnxruntime_genai.Model")
@patch("onnxruntime_genai.Tokenizer")
def test_onnx_chat_completion_with_valid_env_variable(gen_ai_config, model, tokenizer, onnx_unit_test_env):
assert OnnxGenAITextCompletion(env_file_path="test.env")
@patch("builtins.open", new_callable=mock_open, read_data=json.dumps(gen_ai_config))
@patch("onnxruntime_genai.Model")
@patch("onnxruntime_genai.Tokenizer")
def test_onnx_chat_completion_with_valid_parameter(gen_ai_config, model, tokenizer):
assert OnnxGenAITextCompletion(ai_model_path="/valid_path")
def test_onnx_chat_completion_with_invalid_model():
with pytest.raises(ServiceInitializationError):
OnnxGenAITextCompletion(ai_model_path="/invalid_path")
def test_onnx_chat_completion_with_invalid_env_variable(onnx_unit_test_env):
with pytest.raises(ServiceInitializationError):
OnnxGenAITextCompletion()
@pytest.mark.parametrize("exclude_list", [["ONNX_GEN_AI_TEXT_MODEL_FOLDER"]], indirect=True)
def test_onnx_chat_completion_with_missing_ai_path(onnx_unit_test_env):
with pytest.raises(ServiceInitializationError):
OnnxGenAITextCompletion(env_file_path="test.env")
@patch("builtins.open", new_callable=mock_open, read_data=json.dumps(gen_ai_config))
@patch("onnxruntime_genai.Model")
@patch("onnxruntime_genai.Tokenizer")
async def test_onnx_text_completion(gen_ai_config, model, tokenizer):
generator_mock = MagicMock()
generator_mock.__aiter__.return_value = [["H"], ["e"], ["l"], ["l"], ["o"]]
text_completion = OnnxGenAITextCompletion(ai_model_path="test")
with patch.object(text_completion, "_generate_next_token_async", return_value=generator_mock):
completed_text: TextContent = await text_completion.get_text_content(
prompt="test", settings=OnnxGenAIPromptExecutionSettings()
)
assert completed_text.text == "Hello"
@patch("builtins.open", new_callable=mock_open, read_data=json.dumps(gen_ai_config))
@patch("onnxruntime_genai.Model")
@patch("onnxruntime_genai.Tokenizer")
async def test_onnx_text_completion_streaming(gen_ai_config, model, tokenizer):
generator_mock = MagicMock()
generator_mock.__aiter__.return_value = [["H"], ["e"], ["l"], ["l"], ["o"]]
text_completion = OnnxGenAITextCompletion(ai_model_path="test")
completed_text: str = ""
with patch.object(text_completion, "_generate_next_token_async", return_value=generator_mock):
async for chunk in text_completion.get_streaming_text_content(
prompt="test", settings=OnnxGenAIPromptExecutionSettings()
):
completed_text += chunk.text
assert completed_text == "Hello"
@@ -0,0 +1,132 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.connectors.ai.onnx.utils import (
gemma_template,
llama_template,
phi3_template,
phi3v_template,
phi4_template,
phi4mm_template,
)
from semantic_kernel.contents import AudioContent, AuthorRole, ChatHistory, ImageContent, TextContent
def test_phi3v_template_with_text_and_image():
history = ChatHistory(
messages=[
{"role": AuthorRole.SYSTEM, "content": "System message"},
{
"role": AuthorRole.USER,
"items": [TextContent(text="User text message"), ImageContent(url="http://example.com/image.png")],
},
{"role": AuthorRole.ASSISTANT, "content": "Assistant message"},
]
)
expected_output = (
"<|system|>\nSystem message<|end|>\n"
"<|user|>\nUser text message<|end|>\n"
"<|image_1|>\n"
"<|assistant|>\nAssistant message<|end|>\n"
"<|assistant|>\n"
)
assert phi3v_template(history) == expected_output
def test_phi4mm_template_with_text_and_image():
history = ChatHistory(
messages=[
{"role": AuthorRole.SYSTEM, "content": "System message"},
{
"role": AuthorRole.USER,
"items": [
TextContent(text="User text message"),
ImageContent(url="http://example.com/image.png"),
AudioContent(url="http://example.com/audio.mp3"),
],
},
{"role": AuthorRole.ASSISTANT, "content": "Assistant message"},
]
)
expected_output = (
"<|system|>\nSystem message<|end|>\n"
"<|user|>\nUser text message<|end|>\n"
"<|image_1|>\n"
"<|audio_1|>\n"
"<|assistant|>\nAssistant message<|end|>\n"
"<|assistant|>\n"
)
assert phi4mm_template(history) == expected_output
def test_phi3_template_with_only_text():
history = ChatHistory(messages=[{"role": AuthorRole.USER, "items": [TextContent(text="User text message")]}])
expected_output = "<|user|>\nUser text message<|end|>\n<|assistant|>\n"
assert phi3_template(history) == expected_output
def test_phi4_template_with_only_text():
history = ChatHistory(messages=[{"role": AuthorRole.USER, "items": [TextContent(text="User text message")]}])
expected_output = "<|user|>\nUser text message<|end|>\n<|assistant|>\n"
assert phi4_template(history) == expected_output
def test_gemma_template_with_user_and_assistant_messages():
history = ChatHistory(
messages=[
{"role": AuthorRole.USER, "content": "User text message"},
{"role": AuthorRole.ASSISTANT, "content": "Assistant message"},
]
)
expected_output = (
"<bos>"
"<start_of_turn>user\nUser text message<end_of_turn>\n"
"<start_of_turn>model\nAssistant message<end_of_turn>\n"
"<start_of_turn>model\n"
)
assert gemma_template(history) == expected_output
def test_gemma_template_with_only_user_message():
history = ChatHistory(messages=[{"role": AuthorRole.USER, "content": "User text message"}])
expected_output = "<bos><start_of_turn>user\nUser text message<end_of_turn>\n<start_of_turn>model\n"
assert gemma_template(history) == expected_output
def test_llama_template_with_user_and_assistant_messages():
history = ChatHistory(
messages=[
{"role": AuthorRole.USER, "content": "User text message"},
{"role": AuthorRole.ASSISTANT, "content": "Assistant message"},
]
)
expected_output = (
"<|start_header_id|>user<|end_header_id|>\n\nUser text message<|eot_id|>"
"<|start_header_id|>assistant<|end_header_id|>\n\nAssistant message<|eot_id|>"
"<|start_header_id|>assistant<|end_header_id|>"
)
assert llama_template(history) == expected_output
def test_llama_template_with_only_user_message():
history = ChatHistory(messages=[{"role": AuthorRole.USER, "content": "User text message"}])
expected_output = (
"<|start_header_id|>user<|end_header_id|>\n\nUser text message<|eot_id|>"
"<|start_header_id|>assistant<|end_header_id|>"
)
assert llama_template(history) == expected_output
@@ -0,0 +1,84 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from pydantic import ValidationError
from semantic_kernel.connectors.ai.onnx.onnx_gen_ai_prompt_execution_settings import (
OnnxGenAIPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
def test_default_onnx_chat_prompt_execution_settings():
settings = OnnxGenAIPromptExecutionSettings()
assert settings.temperature is None
assert settings.top_p is None
def test_custom_onnx_chat_prompt_execution_settings():
settings = OnnxGenAIPromptExecutionSettings(
temperature=0.5,
top_p=0.5,
max_length=128,
)
assert settings.temperature == 0.5
assert settings.top_p == 0.5
assert settings.max_length == 128
def test_onnx_chat_prompt_execution_settings_from_default_completion_config():
settings = PromptExecutionSettings(service_id="test_service")
chat_settings = OnnxGenAIPromptExecutionSettings.from_prompt_execution_settings(settings)
assert chat_settings.service_id == "test_service"
assert chat_settings.temperature is None
assert chat_settings.top_p is None
def test_onnx_chat_prompt_execution_settings_from_onnx_prompt_execution_settings():
chat_settings = OnnxGenAIPromptExecutionSettings(service_id="test_service", temperature=1.0)
new_settings = OnnxGenAIPromptExecutionSettings(service_id="test_2", temperature=0.0)
chat_settings.update_from_prompt_execution_settings(new_settings)
assert chat_settings.service_id == "test_2"
assert chat_settings.temperature == 0.0
def test_onnx_chat_prompt_execution_settings_from_custom_completion_config():
settings = PromptExecutionSettings(
service_id="test_service",
extension_data={
"temperature": 0.5,
"top_p": 0.5,
"max_length": 128,
},
)
chat_settings = OnnxGenAIPromptExecutionSettings.from_prompt_execution_settings(settings)
assert chat_settings.temperature == 0.5
assert chat_settings.top_p == 0.5
assert chat_settings.max_length == 128
def test_create_options():
settings = OnnxGenAIPromptExecutionSettings(
service_id="test_service",
extension_data={
"temperature": 0.5,
"top_p": 0.5,
"max_length": 128,
},
)
options = settings.prepare_settings_dict()
assert options["temperature"] == 0.5
assert options["top_p"] == 0.5
assert options["max_length"] == 128
def test_create_options_with_wrong_parameter():
with pytest.raises(ValidationError):
OnnxGenAIPromptExecutionSettings(
service_id="test_service",
function_choice_behavior="auto",
extension_data={
"temperature": 10.0,
"top_p": 0.5,
"max_length": 128,
},
)
@@ -0,0 +1,99 @@
# Copyright (c) Microsoft. All rights reserved.
import os
from unittest.mock import AsyncMock, patch
import pytest
from openai import AsyncAzureOpenAI
from openai.resources.audio.transcriptions import AsyncTranscriptions
from openai.types.audio import Transcription
from semantic_kernel.connectors.ai.open_ai import AzureAudioToText
from semantic_kernel.contents import AudioContent
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError, ServiceInvalidRequestError
def test_azure_audio_to_text_init(azure_openai_unit_test_env) -> None:
azure_audio_to_text = AzureAudioToText()
assert azure_audio_to_text.client is not None
assert isinstance(azure_audio_to_text.client, AsyncAzureOpenAI)
assert azure_audio_to_text.ai_model_id == azure_openai_unit_test_env["AZURE_OPENAI_AUDIO_TO_TEXT_DEPLOYMENT_NAME"]
@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_AUDIO_TO_TEXT_DEPLOYMENT_NAME"]], indirect=True)
def test_azure_audio_to_text_init_with_empty_deployment_name(azure_openai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError, match="The Azure OpenAI audio to text deployment name is required."):
AzureAudioToText(env_file_path="test.env")
@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_API_KEY"]], indirect=True)
def test_azure_audio_to_text_init_with_empty_api_key(azure_openai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError):
AzureAudioToText(env_file_path="test.env")
@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_BASE_URL"]], indirect=True)
def test_azure_audio_to_text_init_with_empty_endpoint_and_base_url(azure_openai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError, match="Please provide an endpoint or a base_url"):
AzureAudioToText(env_file_path="test.env")
@pytest.mark.parametrize("override_env_param_dict", [{"AZURE_OPENAI_ENDPOINT": "http://test.com"}], indirect=True)
def test_azure_audio_to_text_init_with_invalid_http_endpoint(azure_openai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError, match="Invalid settings: "):
AzureAudioToText()
@pytest.mark.parametrize(
"override_env_param_dict",
[{"AZURE_OPENAI_BASE_URL": "https://test_audio_to_text_deployment.test-base-url.com"}],
indirect=True,
)
def test_azure_audio_to_text_init_with_from_dict(azure_openai_unit_test_env) -> None:
default_headers = {"test_header": "test_value"}
settings = {
"deployment_name": azure_openai_unit_test_env["AZURE_OPENAI_AUDIO_TO_TEXT_DEPLOYMENT_NAME"],
"endpoint": azure_openai_unit_test_env["AZURE_OPENAI_ENDPOINT"],
"api_key": azure_openai_unit_test_env["AZURE_OPENAI_API_KEY"],
"api_version": azure_openai_unit_test_env["AZURE_OPENAI_API_VERSION"],
"default_headers": default_headers,
}
azure_audio_to_text = AzureAudioToText.from_dict(settings=settings)
assert azure_audio_to_text.client is not None
assert isinstance(azure_audio_to_text.client, AsyncAzureOpenAI)
assert azure_audio_to_text.ai_model_id == azure_openai_unit_test_env["AZURE_OPENAI_AUDIO_TO_TEXT_DEPLOYMENT_NAME"]
assert settings["deployment_name"] in str(azure_audio_to_text.client.base_url)
assert azure_audio_to_text.client.api_key == azure_openai_unit_test_env["AZURE_OPENAI_API_KEY"]
# Assert that the default header we added is present in the client's default headers
for key, value in default_headers.items():
assert key in azure_audio_to_text.client.default_headers
assert azure_audio_to_text.client.default_headers[key] == value
async def test_azure_audio_to_text_get_text_contents(azure_openai_unit_test_env) -> None:
audio_content = AudioContent.from_audio_file(
os.path.join(os.path.dirname(__file__), "../../../../../", "assets/sample_audio.mp3")
)
with patch.object(AsyncTranscriptions, "create", new_callable=AsyncMock) as mock_transcription_create:
mock_transcription_create.return_value = Transcription(text="This is a test audio file.")
openai_audio_to_text = AzureAudioToText()
text_contents = await openai_audio_to_text.get_text_contents(audio_content)
assert len(text_contents) == 1
assert text_contents[0].text == "This is a test audio file."
assert text_contents[0].ai_model_id == azure_openai_unit_test_env["AZURE_OPENAI_AUDIO_TO_TEXT_DEPLOYMENT_NAME"]
async def test_azure_audio_to_text_get_text_contents_invalid_audio_content(azure_openai_unit_test_env):
audio_content = AudioContent()
openai_audio_to_text = AzureAudioToText()
with pytest.raises(ServiceInvalidRequestError, match="Audio content uri must be a string to a local file."):
await openai_audio_to_text.get_text_contents(audio_content)
@@ -0,0 +1,960 @@
# Copyright (c) Microsoft. All rights reserved.
import json
import os
from copy import deepcopy
from unittest.mock import AsyncMock, MagicMock, patch
import openai
import pytest
from httpx import Request, Response
from openai import AsyncAzureOpenAI, AsyncStream
from openai.resources.chat.completions import AsyncCompletions as AsyncChatCompletions
from openai.types.chat import ChatCompletion, ChatCompletionChunk
from openai.types.chat.chat_completion import Choice
from openai.types.chat.chat_completion_chunk import Choice as ChunkChoice
from openai.types.chat.chat_completion_chunk import ChoiceDelta as ChunkChoiceDelta
from openai.types.chat.chat_completion_message import ChatCompletionMessage
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion
from semantic_kernel.connectors.ai.open_ai.exceptions.content_filter_ai_exception import (
ContentFilterAIException,
ContentFilterResultSeverity,
)
from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.azure_chat_prompt_execution_settings import (
AzureChatPromptExecutionSettings,
)
from semantic_kernel.const import USER_AGENT
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.contents.function_call_content import FunctionCallContent
from semantic_kernel.contents.function_result_content import FunctionResultContent
from semantic_kernel.contents.text_content import TextContent
from semantic_kernel.exceptions import ServiceInitializationError, ServiceInvalidExecutionSettingsError
from semantic_kernel.exceptions.service_exceptions import ServiceResponseException
from semantic_kernel.functions.kernel_arguments import KernelArguments
from semantic_kernel.functions.kernel_function_decorator import kernel_function
from semantic_kernel.kernel import Kernel
# region Service Setup
def test_init(azure_openai_unit_test_env) -> None:
# Test successful initialization
azure_chat_completion = AzureChatCompletion(service_id="test_service_id")
assert azure_chat_completion.client is not None
assert isinstance(azure_chat_completion.client, AsyncAzureOpenAI)
assert azure_chat_completion.ai_model_id == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"]
assert isinstance(azure_chat_completion, ChatCompletionClientBase)
assert azure_chat_completion.get_prompt_execution_settings_class() == AzureChatPromptExecutionSettings
def test_init_client(azure_openai_unit_test_env) -> None:
# Test successful initialization with client
client = MagicMock(spec=AsyncAzureOpenAI)
azure_chat_completion = AzureChatCompletion(async_client=client)
assert azure_chat_completion.client is not None
assert isinstance(azure_chat_completion.client, AsyncAzureOpenAI)
def test_init_base_url(azure_openai_unit_test_env) -> None:
# Custom header for testing
default_headers = {"X-Unit-Test": "test-guid"}
azure_chat_completion = AzureChatCompletion(
default_headers=default_headers,
)
assert azure_chat_completion.client is not None
assert isinstance(azure_chat_completion.client, AsyncAzureOpenAI)
assert azure_chat_completion.ai_model_id == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"]
assert isinstance(azure_chat_completion, ChatCompletionClientBase)
for key, value in default_headers.items():
assert key in azure_chat_completion.client.default_headers
assert azure_chat_completion.client.default_headers[key] == value
@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_BASE_URL"]], indirect=True)
def test_init_endpoint(azure_openai_unit_test_env) -> None:
azure_chat_completion = AzureChatCompletion()
assert azure_chat_completion.client is not None
assert isinstance(azure_chat_completion.client, AsyncAzureOpenAI)
assert azure_chat_completion.ai_model_id == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"]
assert isinstance(azure_chat_completion, ChatCompletionClientBase)
@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"]], indirect=True)
def test_init_with_empty_deployment_name(azure_openai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError):
AzureChatCompletion(
env_file_path="test.env",
)
@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_BASE_URL"]], indirect=True)
def test_init_with_empty_endpoint_and_base_url(azure_openai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError):
AzureChatCompletion(
env_file_path="test.env",
)
@pytest.mark.parametrize("override_env_param_dict", [{"AZURE_OPENAI_ENDPOINT": "http://test.com"}], indirect=True)
def test_init_with_invalid_endpoint(azure_openai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError):
AzureChatCompletion()
@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_BASE_URL"]], indirect=True)
def test_serialize(azure_openai_unit_test_env) -> None:
default_headers = {"X-Test": "test"}
settings = {
"deployment_name": azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"],
"endpoint": azure_openai_unit_test_env["AZURE_OPENAI_ENDPOINT"],
"api_key": azure_openai_unit_test_env["AZURE_OPENAI_API_KEY"],
"api_version": azure_openai_unit_test_env["AZURE_OPENAI_API_VERSION"],
"default_headers": default_headers,
}
azure_chat_completion = AzureChatCompletion.from_dict(settings)
dumped_settings = azure_chat_completion.to_dict()
assert dumped_settings["ai_model_id"] == settings["deployment_name"]
assert settings["endpoint"] in str(dumped_settings["base_url"])
assert settings["deployment_name"] in str(dumped_settings["base_url"])
assert settings["api_key"] == dumped_settings["api_key"]
assert settings["api_version"] == dumped_settings["api_version"]
# Assert that the default header we added is present in the dumped_settings default headers
for key, value in default_headers.items():
assert key in dumped_settings["default_headers"]
assert dumped_settings["default_headers"][key] == value
# Assert that the 'User-agent' header is not present in the dumped_settings default headers
assert USER_AGENT not in dumped_settings["default_headers"]
# endregion
# region CMC
@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",
)
@pytest.fixture
def mock_streaming_chat_completion_response() -> AsyncStream[ChatCompletionChunk]:
content = ChatCompletionChunk(
id="test_id",
choices=[ChunkChoice(index=0, delta=ChunkChoiceDelta(content="test", role="assistant"), finish_reason="stop")],
created=0,
model="test",
object="chat.completion.chunk",
)
stream = MagicMock(spec=AsyncStream)
stream.__aiter__.return_value = [content]
return stream
@patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock)
async def test_cmc(
mock_create,
kernel: Kernel,
azure_openai_unit_test_env,
chat_history: ChatHistory,
mock_chat_completion_response: ChatCompletion,
) -> None:
mock_create.return_value = mock_chat_completion_response
chat_history.add_user_message("hello world")
complete_prompt_execution_settings = AzureChatPromptExecutionSettings(service_id="test_service_id")
azure_chat_completion = AzureChatCompletion()
await azure_chat_completion.get_chat_message_contents(
chat_history=chat_history, settings=complete_prompt_execution_settings, kernel=kernel
)
mock_create.assert_awaited_once_with(
model=azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"],
stream=False,
messages=azure_chat_completion._prepare_chat_history_for_request(chat_history),
)
@patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock)
async def test_cmc_with_developer_instruction_role_propagates(
mock_create,
kernel: Kernel,
azure_openai_unit_test_env,
chat_history: ChatHistory,
mock_chat_completion_response: ChatCompletion,
) -> None:
mock_create.return_value = mock_chat_completion_response
chat_history.add_user_message("hello world")
complete_prompt_execution_settings = AzureChatPromptExecutionSettings(service_id="test_service_id")
azure_chat_completion = AzureChatCompletion(instruction_role="developer")
await azure_chat_completion.get_chat_message_contents(
chat_history=chat_history, settings=complete_prompt_execution_settings, kernel=kernel
)
mock_create.assert_awaited_once_with(
model=azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"],
stream=False,
messages=azure_chat_completion._prepare_chat_history_for_request(chat_history),
)
assert azure_chat_completion.instruction_role == "developer"
@patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock)
async def test_cmc_with_logit_bias(
mock_create,
kernel: Kernel,
azure_openai_unit_test_env,
chat_history: ChatHistory,
mock_chat_completion_response: ChatCompletion,
) -> None:
mock_create.return_value = mock_chat_completion_response
prompt = "hello world"
chat_history.add_user_message(prompt)
complete_prompt_execution_settings = AzureChatPromptExecutionSettings()
token_bias = {"1": -100}
complete_prompt_execution_settings.logit_bias = token_bias
azure_chat_completion = AzureChatCompletion()
await azure_chat_completion.get_chat_message_contents(
chat_history=chat_history, settings=complete_prompt_execution_settings, kernel=kernel
)
mock_create.assert_awaited_once_with(
model=azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"],
messages=azure_chat_completion._prepare_chat_history_for_request(chat_history),
stream=False,
logit_bias=token_bias,
)
@patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock)
async def test_cmc_with_stop(
mock_create,
azure_openai_unit_test_env,
chat_history: ChatHistory,
mock_chat_completion_response: ChatCompletion,
) -> None:
mock_create.return_value = mock_chat_completion_response
complete_prompt_execution_settings = AzureChatPromptExecutionSettings()
stop = ["!"]
complete_prompt_execution_settings.stop = stop
azure_chat_completion = AzureChatCompletion()
await azure_chat_completion.get_chat_message_contents(
chat_history=chat_history, settings=complete_prompt_execution_settings
)
mock_create.assert_awaited_once_with(
model=azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"],
messages=azure_chat_completion._prepare_chat_history_for_request(chat_history),
stream=False,
stop=stop,
)
@patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock)
async def test_azure_on_your_data(
mock_create,
kernel: Kernel,
azure_openai_unit_test_env,
chat_history: ChatHistory,
mock_chat_completion_response: ChatCompletion,
) -> None:
mock_chat_completion_response.choices = [
Choice(
index=0,
message=ChatCompletionMessage(
content="test",
role="assistant",
context={
"citations": {
"content": "test content",
"title": "test title",
"url": "test url",
"filepath": "test filepath",
"chunk_id": "test chunk_id",
},
"intent": "query used",
},
),
finish_reason="stop",
)
]
mock_create.return_value = mock_chat_completion_response
prompt = "hello world"
messages_in = chat_history
messages_in.add_user_message(prompt)
messages_out = ChatHistory()
messages_out.add_user_message(prompt)
expected_data_settings = {
"data_sources": [
{
"type": "AzureCognitiveSearch",
"parameters": {
"indexName": "test_index",
"endpoint": "https://test-endpoint-search.com",
"key": "test_key",
},
}
]
}
complete_prompt_execution_settings = AzureChatPromptExecutionSettings(extra_body=expected_data_settings)
azure_chat_completion = AzureChatCompletion()
content = await azure_chat_completion.get_chat_message_contents(
chat_history=messages_in, settings=complete_prompt_execution_settings, kernel=kernel
)
assert isinstance(content[0].items[0], FunctionCallContent)
assert isinstance(content[0].items[1], FunctionResultContent)
assert isinstance(content[0].items[2], TextContent)
assert content[0].items[2].text == "test"
mock_create.assert_awaited_once_with(
model=azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"],
messages=azure_chat_completion._prepare_chat_history_for_request(messages_out),
stream=False,
extra_body=expected_data_settings,
)
@patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock)
async def test_azure_on_your_data_string(
mock_create,
kernel: Kernel,
azure_openai_unit_test_env,
chat_history: ChatHistory,
mock_chat_completion_response: ChatCompletion,
) -> None:
mock_chat_completion_response.choices = [
Choice(
index=0,
message=ChatCompletionMessage(
content="test",
role="assistant",
context=json.dumps({
"citations": {
"content": "test content",
"title": "test title",
"url": "test url",
"filepath": "test filepath",
"chunk_id": "test chunk_id",
},
"intent": "query used",
}),
),
finish_reason="stop",
)
]
mock_create.return_value = mock_chat_completion_response
prompt = "hello world"
messages_in = chat_history
messages_in.add_user_message(prompt)
messages_out = ChatHistory()
messages_out.add_user_message(prompt)
expected_data_settings = {
"data_sources": [
{
"type": "AzureCognitiveSearch",
"parameters": {
"indexName": "test_index",
"endpoint": "https://test-endpoint-search.com",
"key": "test_key",
},
}
]
}
complete_prompt_execution_settings = AzureChatPromptExecutionSettings(extra_body=expected_data_settings)
azure_chat_completion = AzureChatCompletion()
content = await azure_chat_completion.get_chat_message_contents(
chat_history=messages_in, settings=complete_prompt_execution_settings, kernel=kernel
)
assert isinstance(content[0].items[0], FunctionCallContent)
assert isinstance(content[0].items[1], FunctionResultContent)
assert isinstance(content[0].items[2], TextContent)
assert content[0].items[2].text == "test"
mock_create.assert_awaited_once_with(
model=azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"],
messages=azure_chat_completion._prepare_chat_history_for_request(messages_out),
stream=False,
extra_body=expected_data_settings,
)
@patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock)
async def test_azure_on_your_data_fail(
mock_create,
kernel: Kernel,
azure_openai_unit_test_env,
chat_history: ChatHistory,
mock_chat_completion_response: ChatCompletion,
) -> None:
mock_chat_completion_response.choices = [
Choice(
index=0,
message=ChatCompletionMessage(
content="test",
role="assistant",
context="not a dictionary",
),
finish_reason="stop",
)
]
mock_create.return_value = mock_chat_completion_response
prompt = "hello world"
messages_in = chat_history
messages_in.add_user_message(prompt)
messages_out = ChatHistory()
messages_out.add_user_message(prompt)
expected_data_settings = {
"data_sources": [
{
"type": "AzureCognitiveSearch",
"parameters": {
"indexName": "test_index",
"endpoint": "https://test-endpoint-search.com",
"key": "test_key",
},
}
]
}
complete_prompt_execution_settings = AzureChatPromptExecutionSettings(extra_body=expected_data_settings)
azure_chat_completion = AzureChatCompletion()
content = await azure_chat_completion.get_chat_message_contents(
chat_history=messages_in, settings=complete_prompt_execution_settings, kernel=kernel
)
assert isinstance(content[0].items[0], TextContent)
assert content[0].items[0].text == "test"
mock_create.assert_awaited_once_with(
model=azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"],
messages=azure_chat_completion._prepare_chat_history_for_request(messages_out),
stream=False,
extra_body=expected_data_settings,
)
@patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock)
async def test_azure_on_your_data_split_messages(
mock_create,
kernel: Kernel,
azure_openai_unit_test_env,
chat_history: ChatHistory,
mock_chat_completion_response: ChatCompletion,
) -> None:
mock_chat_completion_response.choices = [
Choice(
index=0,
message=ChatCompletionMessage(
content="test",
role="assistant",
context={
"citations": {
"content": "test content",
"title": "test title",
"url": "test url",
"filepath": "test filepath",
"chunk_id": "test chunk_id",
},
"intent": "query used",
},
),
finish_reason="stop",
)
]
mock_create.return_value = mock_chat_completion_response
prompt = "hello world"
messages_in = chat_history
messages_in.add_user_message(prompt)
messages_out = ChatHistory()
messages_out.add_user_message(prompt)
complete_prompt_execution_settings = AzureChatPromptExecutionSettings()
azure_chat_completion = AzureChatCompletion()
content = await azure_chat_completion.get_chat_message_contents(
chat_history=messages_in, settings=complete_prompt_execution_settings, kernel=kernel
)
messages = azure_chat_completion.split_message(content[0])
assert len(messages) == 3
assert isinstance(messages[0].items[0], FunctionCallContent)
assert isinstance(messages[1].items[0], FunctionResultContent)
assert isinstance(messages[2].items[0], TextContent)
assert messages[2].items[0].text == "test"
message = azure_chat_completion.split_message(messages[0])
assert message == [messages[0]]
@patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock)
async def test_cmc_function_calling(
mock_create,
kernel: Kernel,
azure_openai_unit_test_env,
chat_history: ChatHistory,
mock_chat_completion_response: ChatCompletion,
) -> None:
mock_chat_completion_response.choices = [
Choice(
index=0,
message=ChatCompletionMessage(
content=None,
role="assistant",
function_call={"name": "test-function", "arguments": '{"key": "value"}'},
),
finish_reason="stop",
)
]
mock_create.return_value = mock_chat_completion_response
prompt = "hello world"
chat_history.add_user_message(prompt)
azure_chat_completion = AzureChatCompletion()
functions = [{"name": "test-function", "description": "test-description"}]
complete_prompt_execution_settings = AzureChatPromptExecutionSettings(
function_call="test-function",
functions=functions,
)
content = await azure_chat_completion.get_chat_message_contents(
chat_history=chat_history,
settings=complete_prompt_execution_settings,
kernel=kernel,
)
assert isinstance(content[0].items[0], FunctionCallContent)
mock_create.assert_awaited_once_with(
model=azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"],
messages=azure_chat_completion._prepare_chat_history_for_request(chat_history),
stream=False,
functions=functions,
function_call=complete_prompt_execution_settings.function_call,
)
@patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock)
async def test_cmc_tool_calling(
mock_create,
kernel: Kernel,
azure_openai_unit_test_env,
chat_history: ChatHistory,
mock_chat_completion_response: ChatCompletion,
) -> None:
mock_chat_completion_response.choices = [
Choice(
index=0,
message=ChatCompletionMessage(
content=None,
role="assistant",
tool_calls=[
{
"id": "test id",
"function": {"name": "test-tool", "arguments": '{"key": "value"}'},
"type": "function",
}
],
),
finish_reason="stop",
)
]
mock_create.return_value = mock_chat_completion_response
prompt = "hello world"
chat_history.add_user_message(prompt)
azure_chat_completion = AzureChatCompletion()
complete_prompt_execution_settings = AzureChatPromptExecutionSettings()
content = await azure_chat_completion.get_chat_message_contents(
chat_history=chat_history,
settings=complete_prompt_execution_settings,
kernel=kernel,
)
assert isinstance(content[0].items[0], FunctionCallContent)
assert content[0].items[0].id == "test id"
mock_create.assert_awaited_once_with(
model=azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"],
messages=azure_chat_completion._prepare_chat_history_for_request(chat_history),
stream=False,
)
@patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock)
async def test_cmc_tool_calling_parallel_tool_calls(
mock_create,
kernel: Kernel,
azure_openai_unit_test_env,
chat_history: ChatHistory,
mock_chat_completion_response: ChatCompletion,
) -> None:
mock_chat_completion_response.choices = [
Choice(
index=0,
message=ChatCompletionMessage(
content=None,
role="assistant",
tool_calls=[
{
"id": "test id",
"function": {"name": "test-tool", "arguments": '{"key": "value"}'},
"type": "function",
}
],
),
finish_reason="stop",
)
]
mock_create.return_value = mock_chat_completion_response
prompt = "hello world"
chat_history.add_user_message(prompt)
class MockPlugin:
@kernel_function(name="test_tool")
def test_tool(self, key: str):
return "test"
kernel.add_plugin(MockPlugin(), plugin_name="test_tool")
orig_chat_history = deepcopy(chat_history)
complete_prompt_execution_settings = AzureChatPromptExecutionSettings(
service_id="test_service_id", function_choice_behavior=FunctionChoiceBehavior.Auto()
)
with patch(
"semantic_kernel.kernel.Kernel.invoke_function_call",
new_callable=AsyncMock,
) as mock_process_function_call:
azure_chat_completion = AzureChatCompletion()
await azure_chat_completion.get_chat_message_contents(
chat_history=chat_history,
settings=complete_prompt_execution_settings,
kernel=kernel,
arguments=KernelArguments(),
)
mock_create.assert_awaited_once_with(
model=azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"],
stream=False,
messages=azure_chat_completion._prepare_chat_history_for_request(orig_chat_history),
tools=[
{
"type": "function",
"function": {
"name": "test_tool-test_tool",
"description": "",
"parameters": {
"type": "object",
"properties": {"key": {"type": "string"}},
"required": ["key"],
},
},
}
],
tool_choice="auto",
)
mock_process_function_call.assert_awaited()
@patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock)
async def test_cmc_tool_calling_parallel_tool_calls_disabled(
mock_create,
kernel: Kernel,
azure_openai_unit_test_env,
chat_history: ChatHistory,
mock_chat_completion_response: ChatCompletion,
) -> None:
mock_chat_completion_response.choices = [
Choice(
index=0,
message=ChatCompletionMessage(
content=None,
role="assistant",
tool_calls=[
{
"id": "test id",
"function": {"name": "test-tool", "arguments": '{"key": "value"}'},
"type": "function",
}
],
),
finish_reason="stop",
)
]
mock_create.return_value = mock_chat_completion_response
prompt = "hello world"
chat_history.add_user_message(prompt)
class MockPlugin:
@kernel_function(name="test_tool")
def test_tool(self, key: str):
return "test"
kernel.add_plugin(MockPlugin(), plugin_name="test_tool")
orig_chat_history = deepcopy(chat_history)
complete_prompt_execution_settings = AzureChatPromptExecutionSettings(
service_id="test_service_id",
function_choice_behavior=FunctionChoiceBehavior.Auto(),
parallel_tool_calls=False,
)
with patch(
"semantic_kernel.kernel.Kernel.invoke_function_call",
new_callable=AsyncMock,
) as mock_process_function_call:
azure_chat_completion = AzureChatCompletion()
await azure_chat_completion.get_chat_message_contents(
chat_history=chat_history,
settings=complete_prompt_execution_settings,
kernel=kernel,
arguments=KernelArguments(),
)
mock_create.assert_awaited_once_with(
model=azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"],
stream=False,
messages=azure_chat_completion._prepare_chat_history_for_request(orig_chat_history),
parallel_tool_calls=False,
tools=[
{
"type": "function",
"function": {
"name": "test_tool-test_tool",
"description": "",
"parameters": {
"type": "object",
"properties": {"key": {"type": "string"}},
"required": ["key"],
},
},
}
],
tool_choice="auto",
)
mock_process_function_call.assert_awaited()
CONTENT_FILTERED_ERROR_MESSAGE = (
"The response was filtered due to the prompt triggering Azure OpenAI's content management policy. Please "
"modify your prompt and retry. To learn more about our content filtering policies please read our "
"documentation: https://go.microsoft.com/fwlink/?linkid=2198766"
)
CONTENT_FILTERED_ERROR_FULL_MESSAGE = (
"Error code: 400 - {'error': {'message': \"%s\", 'type': null, 'param': 'prompt', 'code': 'content_filter', "
"'status': 400, 'innererror': {'code': 'ResponsibleAIPolicyViolation', 'content_filter_result': {'hate': "
"{'filtered': True, 'severity': 'high'}, 'self_harm': {'filtered': False, 'severity': 'safe'}, 'sexual': "
"{'filtered': False, 'severity': 'safe'}, 'violence': {'filtered': False, 'severity': 'safe'}}}}}"
) % CONTENT_FILTERED_ERROR_MESSAGE
@patch.object(AsyncChatCompletions, "create")
async def test_content_filtering_raises_correct_exception(
mock_create, kernel: Kernel, azure_openai_unit_test_env, chat_history: ChatHistory
) -> None:
prompt = "some prompt that would trigger the content filtering"
chat_history.add_user_message(prompt)
complete_prompt_execution_settings = AzureChatPromptExecutionSettings()
test_endpoint = os.getenv("AZURE_OPENAI_ENDPOINT")
mock_create.side_effect = openai.BadRequestError(
CONTENT_FILTERED_ERROR_FULL_MESSAGE,
response=Response(400, request=Request("POST", test_endpoint)),
body={
"message": CONTENT_FILTERED_ERROR_MESSAGE,
"type": None,
"param": "prompt",
"code": "content_filter",
"status": 400,
"innererror": {
"code": "ResponsibleAIPolicyViolation",
"content_filter_result": {
"hate": {"filtered": True, "severity": "high"},
"self_harm": {"filtered": False, "severity": "safe"},
"sexual": {"filtered": False, "severity": "safe"},
"violence": {"filtered": False, "severity": "safe"},
},
},
},
)
azure_chat_completion = AzureChatCompletion()
with pytest.raises(ContentFilterAIException, match="service encountered a content error") as exc_info:
await azure_chat_completion.get_chat_message_contents(
chat_history=chat_history, settings=complete_prompt_execution_settings, kernel=kernel
)
content_filter_exc = exc_info.value
assert content_filter_exc.param == "prompt"
assert content_filter_exc.content_filter_result["hate"].filtered
assert content_filter_exc.content_filter_result["hate"].severity == ContentFilterResultSeverity.HIGH
@patch.object(AsyncChatCompletions, "create")
async def test_content_filtering_without_response_code_raises_with_default_code(
mock_create, kernel: Kernel, azure_openai_unit_test_env, chat_history: ChatHistory
) -> None:
prompt = "some prompt that would trigger the content filtering"
chat_history.add_user_message(prompt)
complete_prompt_execution_settings = AzureChatPromptExecutionSettings()
test_endpoint = os.getenv("AZURE_OPENAI_ENDPOINT")
mock_create.side_effect = openai.BadRequestError(
CONTENT_FILTERED_ERROR_FULL_MESSAGE,
response=Response(400, request=Request("POST", test_endpoint)),
body={
"message": CONTENT_FILTERED_ERROR_MESSAGE,
"type": None,
"param": "prompt",
"code": "content_filter",
"status": 400,
"innererror": {
"content_filter_result": {
"hate": {"filtered": True, "severity": "high"},
"self_harm": {"filtered": False, "severity": "safe"},
"sexual": {"filtered": False, "severity": "safe"},
"violence": {"filtered": False, "severity": "safe"},
},
},
},
)
azure_chat_completion = AzureChatCompletion()
with pytest.raises(ContentFilterAIException, match="service encountered a content error"):
await azure_chat_completion.get_chat_message_contents(
chat_history=chat_history, settings=complete_prompt_execution_settings, kernel=kernel
)
@patch.object(AsyncChatCompletions, "create")
async def test_bad_request_non_content_filter(
mock_create, kernel: Kernel, azure_openai_unit_test_env, chat_history: ChatHistory
) -> None:
prompt = "some prompt that would trigger the content filtering"
chat_history.add_user_message(prompt)
complete_prompt_execution_settings = AzureChatPromptExecutionSettings()
test_endpoint = os.getenv("AZURE_OPENAI_ENDPOINT")
mock_create.side_effect = openai.BadRequestError(
"The request was bad.", response=Response(400, request=Request("POST", test_endpoint)), body={}
)
azure_chat_completion = AzureChatCompletion()
with pytest.raises(ServiceResponseException, match="service failed to complete the prompt"):
await azure_chat_completion.get_chat_message_contents(
chat_history=chat_history, settings=complete_prompt_execution_settings, kernel=kernel
)
@patch.object(AsyncChatCompletions, "create")
async def test_no_kernel_provided_throws_error(
mock_create, azure_openai_unit_test_env, chat_history: ChatHistory
) -> None:
prompt = "some prompt that would trigger the content filtering"
chat_history.add_user_message(prompt)
complete_prompt_execution_settings = AzureChatPromptExecutionSettings(
function_choice_behavior=FunctionChoiceBehavior.Auto()
)
test_endpoint = os.getenv("AZURE_OPENAI_ENDPOINT")
mock_create.side_effect = openai.BadRequestError(
"The request was bad.", response=Response(400, request=Request("POST", test_endpoint)), body={}
)
azure_chat_completion = AzureChatCompletion()
with pytest.raises(
ServiceInvalidExecutionSettingsError,
match="The kernel is required for function calls.",
):
await azure_chat_completion.get_chat_message_contents(
chat_history=chat_history, settings=complete_prompt_execution_settings
)
@patch.object(AsyncChatCompletions, "create")
async def test_auto_invoke_false_no_kernel_provided_throws_error(
mock_create, azure_openai_unit_test_env, chat_history: ChatHistory
) -> None:
prompt = "some prompt that would trigger the content filtering"
chat_history.add_user_message(prompt)
complete_prompt_execution_settings = AzureChatPromptExecutionSettings(
function_choice_behavior=FunctionChoiceBehavior.Auto(auto_invoke=False)
)
test_endpoint = os.getenv("AZURE_OPENAI_ENDPOINT")
mock_create.side_effect = openai.BadRequestError(
"The request was bad.", response=Response(400, request=Request("POST", test_endpoint)), body={}
)
azure_chat_completion = AzureChatCompletion()
with pytest.raises(
ServiceInvalidExecutionSettingsError,
match="The kernel is required for function calls.",
):
await azure_chat_completion.get_chat_message_contents(
chat_history=chat_history, settings=complete_prompt_execution_settings
)
@patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock)
async def test_cmc_streaming(
mock_create,
kernel: Kernel,
azure_openai_unit_test_env,
chat_history: ChatHistory,
mock_streaming_chat_completion_response: AsyncStream[ChatCompletionChunk],
) -> None:
mock_create.return_value = mock_streaming_chat_completion_response
chat_history.add_user_message("hello world")
complete_prompt_execution_settings = AzureChatPromptExecutionSettings(service_id="test_service_id")
azure_chat_completion = AzureChatCompletion()
async for msg in azure_chat_completion.get_streaming_chat_message_contents(
chat_history=chat_history, settings=complete_prompt_execution_settings, kernel=kernel
):
assert msg is not None
mock_create.assert_awaited_once_with(
model=azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"],
stream=True,
messages=azure_chat_completion._prepare_chat_history_for_request(chat_history),
# NOTE: The `stream_options={"include_usage": True}` is explicitly enforced in
# `OpenAIChatCompletionBase._inner_get_streaming_chat_message_contents`.
# To ensure consistency, we align the arguments here accordingly.
stream_options={"include_usage": True},
)
@@ -0,0 +1,111 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import Mock, patch
import pytest
from openai import AsyncAzureOpenAI
from openai.types import Completion
from semantic_kernel.connectors.ai.open_ai.services.azure_text_completion import AzureTextCompletion
from semantic_kernel.connectors.ai.text_completion_client_base import TextCompletionClientBase
from semantic_kernel.exceptions import ServiceInitializationError
@pytest.fixture
def mock_text_completion_response() -> Mock:
mock_response = Mock(spec=Completion)
mock_response.id = "test_id"
mock_response.created = "time"
mock_response.usage = None
mock_response.choices = []
return mock_response
def test_init(azure_openai_unit_test_env) -> None:
# Test successful initialization
azure_text_completion = AzureTextCompletion()
assert azure_text_completion.client is not None
assert isinstance(azure_text_completion.client, AsyncAzureOpenAI)
assert azure_text_completion.ai_model_id == azure_openai_unit_test_env["AZURE_OPENAI_TEXT_DEPLOYMENT_NAME"]
assert isinstance(azure_text_completion, TextCompletionClientBase)
def test_init_with_custom_header(azure_openai_unit_test_env) -> None:
# Custom header for testing
default_headers = {"X-Unit-Test": "test-guid"}
# Test successful initialization
azure_text_completion = AzureTextCompletion(
default_headers=default_headers,
)
assert azure_text_completion.client is not None
assert isinstance(azure_text_completion.client, AsyncAzureOpenAI)
assert azure_text_completion.ai_model_id == azure_openai_unit_test_env["AZURE_OPENAI_TEXT_DEPLOYMENT_NAME"]
assert isinstance(azure_text_completion, TextCompletionClientBase)
for key, value in default_headers.items():
assert key in azure_text_completion.client.default_headers
assert azure_text_completion.client.default_headers[key] == value
def test_azure_text_embedding_generates_no_token_with_api_key_in_env(azure_openai_unit_test_env) -> None:
with (
patch(
"semantic_kernel.utils.authentication.entra_id_authentication.get_entra_auth_token",
) as mock_get_token,
):
azure_text_completion = AzureTextCompletion()
assert azure_text_completion.client is not None
# API key is provided in env var, so the ad_token should be None
assert mock_get_token.call_count == 0
@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_TEXT_DEPLOYMENT_NAME"]], indirect=True)
def test_init_with_empty_deployment_name(monkeypatch, azure_openai_unit_test_env) -> None:
monkeypatch.delenv("AZURE_OPENAI_TEXT_DEPLOYMENT_NAME", raising=False)
with pytest.raises(ServiceInitializationError):
AzureTextCompletion(
env_file_path="test.env",
)
@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_BASE_URL"]], indirect=True)
def test_init_with_empty_endpoint_and_base_url(azure_openai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError):
AzureTextCompletion(
env_file_path="test.env",
)
@pytest.mark.parametrize("override_env_param_dict", [{"AZURE_OPENAI_ENDPOINT": "http://test.com"}], indirect=True)
def test_init_with_invalid_endpoint(azure_openai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError):
AzureTextCompletion()
@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_BASE_URL"]], indirect=True)
def test_serialize(azure_openai_unit_test_env) -> None:
default_headers = {"X-Test": "test"}
settings = {
"deployment_name": azure_openai_unit_test_env["AZURE_OPENAI_TEXT_DEPLOYMENT_NAME"],
"endpoint": azure_openai_unit_test_env["AZURE_OPENAI_ENDPOINT"],
"api_key": azure_openai_unit_test_env["AZURE_OPENAI_API_KEY"],
"api_version": azure_openai_unit_test_env["AZURE_OPENAI_API_VERSION"],
"default_headers": default_headers,
}
azure_text_completion = AzureTextCompletion.from_dict(settings)
dumped_settings = azure_text_completion.to_dict()
assert dumped_settings["ai_model_id"] == settings["deployment_name"]
assert settings["endpoint"] in str(dumped_settings["base_url"])
assert settings["deployment_name"] in str(dumped_settings["base_url"])
assert settings["api_key"] == dumped_settings["api_key"]
assert settings["api_version"] == dumped_settings["api_version"]
# Assert that the default header we added is present in the dumped_settings default headers
for key, value in default_headers.items():
assert key in dumped_settings["default_headers"]
assert dumped_settings["default_headers"][key] == value
@@ -0,0 +1,126 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import AsyncMock, call, patch
import pytest
from openai import AsyncAzureOpenAI
from openai.resources.embeddings import AsyncEmbeddings
from semantic_kernel.connectors.ai.embedding_generator_base import EmbeddingGeneratorBase
from semantic_kernel.connectors.ai.open_ai.services.azure_text_embedding import AzureTextEmbedding
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError
def test_azure_text_embedding_init(azure_openai_unit_test_env) -> None:
# Test successful initialization
azure_text_embedding = AzureTextEmbedding()
assert azure_text_embedding.client is not None
assert isinstance(azure_text_embedding.client, AsyncAzureOpenAI)
assert azure_text_embedding.ai_model_id == azure_openai_unit_test_env["AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME"]
assert isinstance(azure_text_embedding, EmbeddingGeneratorBase)
@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME"]], indirect=True)
def test_azure_text_embedding_init_with_empty_deployment_name(azure_openai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError):
AzureTextEmbedding(
env_file_path="test.env",
)
@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_BASE_URL"]], indirect=True)
def test_azure_text_embedding_init_with_empty_endpoint_and_base_url(azure_openai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError):
AzureTextEmbedding(
env_file_path="test.env",
)
@pytest.mark.parametrize("override_env_param_dict", [{"AZURE_OPENAI_ENDPOINT": "http://test.com"}], indirect=True)
def test_azure_text_embedding_init_with_invalid_endpoint(azure_openai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError):
AzureTextEmbedding()
@pytest.mark.parametrize(
"override_env_param_dict",
[{"AZURE_OPENAI_BASE_URL": "https://test_embedding_deployment.test-base-url.com"}],
indirect=True,
)
def test_azure_text_embedding_init_with_from_dict(azure_openai_unit_test_env) -> None:
default_headers = {"test_header": "test_value"}
settings = {
"deployment_name": azure_openai_unit_test_env["AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME"],
"endpoint": azure_openai_unit_test_env["AZURE_OPENAI_ENDPOINT"],
"api_key": azure_openai_unit_test_env["AZURE_OPENAI_API_KEY"],
"api_version": azure_openai_unit_test_env["AZURE_OPENAI_API_VERSION"],
"default_headers": default_headers,
}
azure_text_embedding = AzureTextEmbedding.from_dict(settings=settings)
assert azure_text_embedding.client is not None
assert isinstance(azure_text_embedding.client, AsyncAzureOpenAI)
assert azure_text_embedding.ai_model_id == azure_openai_unit_test_env["AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME"]
assert isinstance(azure_text_embedding, EmbeddingGeneratorBase)
assert settings["deployment_name"] in str(azure_text_embedding.client.base_url)
assert azure_text_embedding.client.api_key == azure_openai_unit_test_env["AZURE_OPENAI_API_KEY"]
# Assert that the default header we added is present in the client's default headers
for key, value in default_headers.items():
assert key in azure_text_embedding.client.default_headers
assert azure_text_embedding.client.default_headers[key] == value
def test_azure_text_embedding_generates_no_token_with_api_key_in_env(azure_openai_unit_test_env) -> None:
with (
patch(
"semantic_kernel.utils.authentication.entra_id_authentication.get_entra_auth_token",
) as mock_get_token,
):
azure_text_embedding = AzureTextEmbedding()
assert azure_text_embedding.client is not None
# API key is provided in env var, so the ad_token should be None
assert mock_get_token.call_count == 0
@patch.object(AsyncEmbeddings, "create", new_callable=AsyncMock)
async def test_azure_text_embedding_calls_with_parameters(mock_create, azure_openai_unit_test_env) -> None:
texts = ["hello world", "goodbye world"]
embedding_dimensions = 1536
azure_text_embedding = AzureTextEmbedding()
await azure_text_embedding.generate_embeddings(texts, dimensions=embedding_dimensions)
mock_create.assert_awaited_once_with(
input=texts,
model=azure_openai_unit_test_env["AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME"],
dimensions=embedding_dimensions,
)
@patch.object(AsyncEmbeddings, "create", new_callable=AsyncMock)
async def test_azure_text_embedding_calls_with_batches(mock_create, azure_openai_unit_test_env) -> None:
texts = [i for i in range(0, 5)]
azure_text_embedding = AzureTextEmbedding()
await azure_text_embedding.generate_embeddings(texts, batch_size=3)
mock_create.assert_has_awaits(
[
call(
model=azure_openai_unit_test_env["AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME"],
input=texts[0:3],
),
call(
model=azure_openai_unit_test_env["AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME"],
input=texts[3:5],
),
],
any_order=False,
)
@@ -0,0 +1,82 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import patch
import httpx
import pytest
from openai import AsyncAzureOpenAI, _legacy_response
from openai.resources.audio.speech import AsyncSpeech
from semantic_kernel.connectors.ai.open_ai import AzureTextToAudio
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError
def test_azure_text_to_audio_init(azure_openai_unit_test_env) -> None:
azure_text_to_audio = AzureTextToAudio()
assert azure_text_to_audio.client is not None
assert isinstance(azure_text_to_audio.client, AsyncAzureOpenAI)
assert azure_text_to_audio.ai_model_id == azure_openai_unit_test_env["AZURE_OPENAI_TEXT_TO_AUDIO_DEPLOYMENT_NAME"]
@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_TEXT_TO_AUDIO_DEPLOYMENT_NAME"]], indirect=True)
def test_azure_text_to_audio_init_with_empty_deployment_name(azure_openai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError, match="The Azure OpenAI text to audio deployment name is required."):
AzureTextToAudio(env_file_path="test.env")
@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_API_KEY"]], indirect=True)
def test_azure_text_to_audio_init_with_empty_api_key(azure_openai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError):
AzureTextToAudio(env_file_path="test.env")
@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_BASE_URL"]], indirect=True)
def test_azure_text_to_audio_init_with_empty_endpoint_and_base_url(azure_openai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError, match="Please provide an endpoint or a base_url"):
AzureTextToAudio(env_file_path="test.env")
@pytest.mark.parametrize("override_env_param_dict", [{"AZURE_OPENAI_ENDPOINT": "http://test.com"}], indirect=True)
def test_azure_text_to_audio_init_with_invalid_http_endpoint(azure_openai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError, match="Invalid settings: "):
AzureTextToAudio()
@pytest.mark.parametrize(
"override_env_param_dict",
[{"AZURE_OPENAI_BASE_URL": "https://test_text_to_audio_deployment.test-base-url.com"}],
indirect=True,
)
def test_azure_text_to_audio_init_with_from_dict(azure_openai_unit_test_env) -> None:
default_headers = {"test_header": "test_value"}
settings = {
"deployment_name": azure_openai_unit_test_env["AZURE_OPENAI_TEXT_TO_AUDIO_DEPLOYMENT_NAME"],
"endpoint": azure_openai_unit_test_env["AZURE_OPENAI_ENDPOINT"],
"api_key": azure_openai_unit_test_env["AZURE_OPENAI_API_KEY"],
"api_version": azure_openai_unit_test_env["AZURE_OPENAI_API_VERSION"],
"default_headers": default_headers,
}
azure_text_to_audio = AzureTextToAudio.from_dict(settings=settings)
assert azure_text_to_audio.client is not None
assert isinstance(azure_text_to_audio.client, AsyncAzureOpenAI)
assert azure_text_to_audio.ai_model_id == azure_openai_unit_test_env["AZURE_OPENAI_TEXT_TO_AUDIO_DEPLOYMENT_NAME"]
assert settings["deployment_name"] in str(azure_text_to_audio.client.base_url)
assert azure_text_to_audio.client.api_key == azure_openai_unit_test_env["AZURE_OPENAI_API_KEY"]
# Assert that the default header we added is present in the client's default headers
for key, value in default_headers.items():
assert key in azure_text_to_audio.client.default_headers
assert azure_text_to_audio.client.default_headers[key] == value
@patch.object(AsyncSpeech, "create", return_value=_legacy_response.HttpxBinaryResponseContent(httpx.Response(200)))
async def test_azure_text_to_audio_get_audio_contents(mock_speech_create, azure_openai_unit_test_env) -> None:
openai_audio_to_text = AzureTextToAudio()
audio_contents = await openai_audio_to_text.get_audio_contents("Hello World!")
assert len(audio_contents) == 1
assert audio_contents[0].ai_model_id == azure_openai_unit_test_env["AZURE_OPENAI_TEXT_TO_AUDIO_DEPLOYMENT_NAME"]
@@ -0,0 +1,99 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import AsyncMock, patch
import pytest
from openai import AsyncAzureOpenAI
from openai.resources.images import AsyncImages
from openai.types.image import Image
from openai.types.images_response import ImagesResponse
from semantic_kernel.connectors.ai.open_ai.services.azure_text_to_image import AzureTextToImage
from semantic_kernel.connectors.ai.text_to_image_client_base import TextToImageClientBase
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError
def test_azure_text_to_image_init(azure_openai_unit_test_env) -> None:
# Test successful initialization
azure_text_to_image = AzureTextToImage()
assert azure_text_to_image.client is not None
assert isinstance(azure_text_to_image.client, AsyncAzureOpenAI)
assert azure_text_to_image.ai_model_id == azure_openai_unit_test_env["AZURE_OPENAI_TEXT_TO_IMAGE_DEPLOYMENT_NAME"]
assert isinstance(azure_text_to_image, TextToImageClientBase)
@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_TEXT_TO_IMAGE_DEPLOYMENT_NAME"]], indirect=True)
def test_azure_text_to_image_init_with_empty_deployment_name(azure_openai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError):
AzureTextToImage(env_file_path="test.env")
@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_API_KEY"]], indirect=True)
def test_azure_text_to_image_init_with_empty_api_key(azure_openai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError):
AzureTextToImage(env_file_path="test.env")
@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_BASE_URL"]], indirect=True)
def test_azure_text_to_image_init_with_empty_endpoint_and_base_url(azure_openai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError):
AzureTextToImage(env_file_path="test.env")
@pytest.mark.parametrize("override_env_param_dict", [{"AZURE_OPENAI_ENDPOINT": "http://test.com"}], indirect=True)
def test_azure_text_to_image_init_with_invalid_endpoint(azure_openai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError):
AzureTextToImage()
@pytest.mark.parametrize(
"override_env_param_dict",
[{"AZURE_OPENAI_BASE_URL": "https://test_text_to_image_deployment.test-base-url.com"}],
indirect=True,
)
def test_azure_text_to_image_init_with_from_dict(azure_openai_unit_test_env) -> None:
default_headers = {"test_header": "test_value"}
settings = {
"deployment_name": azure_openai_unit_test_env["AZURE_OPENAI_TEXT_TO_IMAGE_DEPLOYMENT_NAME"],
"endpoint": azure_openai_unit_test_env["AZURE_OPENAI_ENDPOINT"],
"api_key": azure_openai_unit_test_env["AZURE_OPENAI_API_KEY"],
"api_version": azure_openai_unit_test_env["AZURE_OPENAI_API_VERSION"],
"default_headers": default_headers,
}
azure_text_to_image = AzureTextToImage.from_dict(settings=settings)
assert azure_text_to_image.client is not None
assert isinstance(azure_text_to_image.client, AsyncAzureOpenAI)
assert azure_text_to_image.ai_model_id == azure_openai_unit_test_env["AZURE_OPENAI_TEXT_TO_IMAGE_DEPLOYMENT_NAME"]
assert isinstance(azure_text_to_image, TextToImageClientBase)
assert settings["deployment_name"] in str(azure_text_to_image.client.base_url)
assert azure_text_to_image.client.api_key == azure_openai_unit_test_env["AZURE_OPENAI_API_KEY"]
# Assert that the default header we added is present in the client's default headers
for key, value in default_headers.items():
assert key in azure_text_to_image.client.default_headers
assert azure_text_to_image.client.default_headers[key] == value
@patch.object(AsyncImages, "generate", new_callable=AsyncMock)
async def test_azure_text_to_image_calls_with_parameters(mock_generate, azure_openai_unit_test_env) -> None:
mock_response = ImagesResponse(created=1, data=[Image(url="abc")], usage=None)
mock_generate.return_value = mock_response
prompt = "A painting of a vase with flowers"
width = 512
azure_text_to_image = AzureTextToImage(
deployment_name=azure_openai_unit_test_env["AZURE_OPENAI_TEXT_TO_IMAGE_DEPLOYMENT_NAME"]
)
await azure_text_to_image.generate_image(prompt, width=width, height=width)
mock_generate.assert_awaited_once_with(
prompt=prompt,
model=azure_openai_unit_test_env["AZURE_OPENAI_TEXT_TO_IMAGE_DEPLOYMENT_NAME"],
size=f"{width}x{width}",
n=1,
)
@@ -0,0 +1,87 @@
# Copyright (c) Microsoft. All rights reserved.
import os
from unittest.mock import AsyncMock, patch
import pytest
from openai import AsyncClient
from openai.resources.audio.transcriptions import AsyncTranscriptions
from openai.types.audio import Transcription
from semantic_kernel.connectors.ai.open_ai import OpenAIAudioToTextExecutionSettings
from semantic_kernel.connectors.ai.open_ai.services.open_ai_audio_to_text import OpenAIAudioToText
from semantic_kernel.contents import AudioContent
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError, ServiceInvalidRequestError
def test_init(openai_unit_test_env):
openai_audio_to_text = OpenAIAudioToText()
assert openai_audio_to_text.client is not None
assert isinstance(openai_audio_to_text.client, AsyncClient)
assert openai_audio_to_text.ai_model_id == openai_unit_test_env["OPENAI_AUDIO_TO_TEXT_MODEL_ID"]
def test_init_validation_fail() -> None:
with pytest.raises(ServiceInitializationError, match="Failed to create OpenAI settings."):
OpenAIAudioToText(api_key="34523", ai_model_id={"test": "dict"})
@pytest.mark.parametrize("exclude_list", [["OPENAI_AUDIO_TO_TEXT_MODEL_ID"]], indirect=True)
def test_init_audio_to_text_model_not_provided(openai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError, match="The OpenAI audio to text model ID is required."):
OpenAIAudioToText(
env_file_path="test.env",
)
@pytest.mark.parametrize("exclude_list", [["OPENAI_API_KEY"]], indirect=True)
def test_init_with_empty_api_key(openai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError):
OpenAIAudioToText(
env_file_path="test.env",
)
def test_init_to_from_dict(openai_unit_test_env):
default_headers = {"X-Unit-Test": "test-guid"}
settings = {
"ai_model_id": openai_unit_test_env["OPENAI_AUDIO_TO_TEXT_MODEL_ID"],
"api_key": openai_unit_test_env["OPENAI_API_KEY"],
"default_headers": default_headers,
}
audio_to_text = OpenAIAudioToText.from_dict(settings)
dumped_settings = audio_to_text.to_dict()
assert dumped_settings["ai_model_id"] == settings["ai_model_id"]
assert dumped_settings["api_key"] == settings["api_key"]
def test_prompt_execution_settings_class(openai_unit_test_env) -> None:
openai_audio_to_text = OpenAIAudioToText()
assert openai_audio_to_text.get_prompt_execution_settings_class() == OpenAIAudioToTextExecutionSettings
async def test_get_text_contents(openai_unit_test_env):
audio_content = AudioContent.from_audio_file(
os.path.join(os.path.dirname(__file__), "../../../../../", "assets/sample_audio.mp3")
)
with patch.object(AsyncTranscriptions, "create", new_callable=AsyncMock) as mock_transcription_create:
mock_transcription_create.return_value = Transcription(text="This is a test audio file.")
openai_audio_to_text = OpenAIAudioToText()
text_contents = await openai_audio_to_text.get_text_contents(audio_content)
assert len(text_contents) == 1
assert text_contents[0].text == "This is a test audio file."
assert text_contents[0].ai_model_id == openai_unit_test_env["OPENAI_AUDIO_TO_TEXT_MODEL_ID"]
async def test_get_text_contents_invalid_audio_content(openai_unit_test_env):
audio_content = AudioContent()
openai_audio_to_text = OpenAIAudioToText()
with pytest.raises(ServiceInvalidRequestError, match="Audio content uri must be a string to a local file."):
await openai_audio_to_text.get_text_contents(audio_content)
@@ -0,0 +1,105 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase
from semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion import OpenAIChatCompletion
from semantic_kernel.const import USER_AGENT
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError
def test_init(openai_unit_test_env) -> None:
# Test successful initialization
open_ai_chat_completion = OpenAIChatCompletion()
assert open_ai_chat_completion.ai_model_id == openai_unit_test_env["OPENAI_CHAT_MODEL_ID"]
assert isinstance(open_ai_chat_completion, ChatCompletionClientBase)
def test_init_validation_fail() -> None:
# Test successful initialization
with pytest.raises(ServiceInitializationError):
OpenAIChatCompletion(api_key="34523", ai_model_id={"test": "dict"})
def test_init_ai_model_id_constructor(openai_unit_test_env) -> None:
# Test successful initialization
ai_model_id = "test_model_id"
open_ai_chat_completion = OpenAIChatCompletion(ai_model_id=ai_model_id)
assert open_ai_chat_completion.ai_model_id == ai_model_id
assert isinstance(open_ai_chat_completion, ChatCompletionClientBase)
def test_init_with_default_header(openai_unit_test_env) -> None:
default_headers = {"X-Unit-Test": "test-guid"}
# Test successful initialization
open_ai_chat_completion = OpenAIChatCompletion(
default_headers=default_headers,
)
assert open_ai_chat_completion.ai_model_id == openai_unit_test_env["OPENAI_CHAT_MODEL_ID"]
assert isinstance(open_ai_chat_completion, ChatCompletionClientBase)
# Assert that the default header we added is present in the client's default headers
for key, value in default_headers.items():
assert key in open_ai_chat_completion.client.default_headers
assert open_ai_chat_completion.client.default_headers[key] == value
@pytest.mark.parametrize("exclude_list", [["OPENAI_CHAT_MODEL_ID"]], indirect=True)
def test_init_with_empty_model_id(openai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError):
OpenAIChatCompletion(
env_file_path="test.env",
)
@pytest.mark.parametrize("exclude_list", [["OPENAI_API_KEY"]], indirect=True)
def test_init_with_empty_api_key(openai_unit_test_env) -> None:
ai_model_id = "test_model_id"
with pytest.raises(ServiceInitializationError):
OpenAIChatCompletion(
ai_model_id=ai_model_id,
env_file_path="test.env",
)
def test_serialize(openai_unit_test_env) -> None:
default_headers = {"X-Unit-Test": "test-guid"}
settings = {
"ai_model_id": openai_unit_test_env["OPENAI_CHAT_MODEL_ID"],
"api_key": openai_unit_test_env["OPENAI_API_KEY"],
"default_headers": default_headers,
}
open_ai_chat_completion = OpenAIChatCompletion.from_dict(settings)
dumped_settings = open_ai_chat_completion.to_dict()
assert dumped_settings["ai_model_id"] == openai_unit_test_env["OPENAI_CHAT_MODEL_ID"]
assert dumped_settings["api_key"] == openai_unit_test_env["OPENAI_API_KEY"]
# Assert that the default header we added is present in the dumped_settings default headers
for key, value in default_headers.items():
assert key in dumped_settings["default_headers"]
assert dumped_settings["default_headers"][key] == value
# Assert that the 'User-agent' header is not present in the dumped_settings default headers
assert USER_AGENT not in dumped_settings["default_headers"]
def test_serialize_with_org_id(openai_unit_test_env) -> None:
settings = {
"ai_model_id": openai_unit_test_env["OPENAI_CHAT_MODEL_ID"],
"api_key": openai_unit_test_env["OPENAI_API_KEY"],
"org_id": openai_unit_test_env["OPENAI_ORG_ID"],
}
open_ai_chat_completion = OpenAIChatCompletion.from_dict(settings)
dumped_settings = open_ai_chat_completion.to_dict()
assert dumped_settings["ai_model_id"] == openai_unit_test_env["OPENAI_CHAT_MODEL_ID"]
assert dumped_settings["api_key"] == openai_unit_test_env["OPENAI_API_KEY"]
assert dumped_settings["org_id"] == openai_unit_test_env["OPENAI_ORG_ID"]
# Assert that the 'User-agent' header is not present in the dumped_settings default headers
assert USER_AGENT not in dumped_settings["default_headers"]
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,324 @@
# Copyright (c) Microsoft. All rights reserved.
import json
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from openai import AsyncStream
from openai.resources import AsyncCompletions
from openai.types import Completion as TextCompletion
from openai.types import CompletionChoice as TextCompletionChoice
from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.open_ai_prompt_execution_settings import (
OpenAITextPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.open_ai.services.open_ai_text_completion import OpenAITextCompletion
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.connectors.ai.text_completion_client_base import TextCompletionClientBase
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError
def test_init(openai_unit_test_env) -> None:
# Test successful initialization
open_ai_text_completion = OpenAITextCompletion()
assert open_ai_text_completion.ai_model_id == openai_unit_test_env["OPENAI_TEXT_MODEL_ID"]
assert isinstance(open_ai_text_completion, TextCompletionClientBase)
def test_init_with_ai_model_id(openai_unit_test_env) -> None:
# Test successful initialization
ai_model_id = "test_model_id"
open_ai_text_completion = OpenAITextCompletion(ai_model_id=ai_model_id)
assert open_ai_text_completion.ai_model_id == ai_model_id
assert isinstance(open_ai_text_completion, TextCompletionClientBase)
def test_init_with_default_header(openai_unit_test_env) -> None:
default_headers = {"X-Unit-Test": "test-guid"}
# Test successful initialization
open_ai_text_completion = OpenAITextCompletion(
default_headers=default_headers,
)
assert open_ai_text_completion.ai_model_id == openai_unit_test_env["OPENAI_TEXT_MODEL_ID"]
assert isinstance(open_ai_text_completion, TextCompletionClientBase)
for key, value in default_headers.items():
assert key in open_ai_text_completion.client.default_headers
assert open_ai_text_completion.client.default_headers[key] == value
def test_init_validation_fail() -> None:
with pytest.raises(ServiceInitializationError):
OpenAITextCompletion(api_key="34523", ai_model_id={"test": "dict"})
@pytest.mark.parametrize("exclude_list", [["OPENAI_API_KEY"]], indirect=True)
def test_init_with_empty_api_key(openai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError):
OpenAITextCompletion(
env_file_path="test.env",
)
@pytest.mark.parametrize("exclude_list", [["OPENAI_TEXT_MODEL_ID"]], indirect=True)
def test_init_with_empty_model(openai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError):
OpenAITextCompletion(
env_file_path="test.env",
)
def test_serialize(openai_unit_test_env) -> None:
default_headers = {"X-Unit-Test": "test-guid"}
settings = {
"ai_model_id": openai_unit_test_env["OPENAI_TEXT_MODEL_ID"],
"api_key": openai_unit_test_env["OPENAI_API_KEY"],
"default_headers": default_headers,
}
open_ai_text_completion = OpenAITextCompletion.from_dict(settings)
dumped_settings = open_ai_text_completion.to_dict()
assert dumped_settings["ai_model_id"] == openai_unit_test_env["OPENAI_TEXT_MODEL_ID"]
assert dumped_settings["api_key"] == openai_unit_test_env["OPENAI_API_KEY"]
# Assert that the default header we added is present in the dumped_settings default headers
for key, value in default_headers.items():
assert key in dumped_settings["default_headers"]
assert dumped_settings["default_headers"][key] == value
def test_serialize_def_headers_string(openai_unit_test_env) -> None:
default_headers = '{"X-Unit-Test": "test-guid"}'
settings = {
"ai_model_id": openai_unit_test_env["OPENAI_TEXT_MODEL_ID"],
"api_key": openai_unit_test_env["OPENAI_API_KEY"],
"default_headers": default_headers,
}
open_ai_text_completion = OpenAITextCompletion.from_dict(settings)
dumped_settings = open_ai_text_completion.to_dict()
assert dumped_settings["ai_model_id"] == openai_unit_test_env["OPENAI_TEXT_MODEL_ID"]
assert dumped_settings["api_key"] == openai_unit_test_env["OPENAI_API_KEY"]
# Assert that the default header we added is present in the dumped_settings default headers
for key, value in json.loads(default_headers).items():
assert key in dumped_settings["default_headers"]
assert dumped_settings["default_headers"][key] == value
def test_serialize_with_org_id(openai_unit_test_env) -> None:
settings = {
"ai_model_id": openai_unit_test_env["OPENAI_TEXT_MODEL_ID"],
"api_key": openai_unit_test_env["OPENAI_API_KEY"],
"org_id": openai_unit_test_env["OPENAI_ORG_ID"],
}
open_ai_text_completion = OpenAITextCompletion.from_dict(settings)
dumped_settings = open_ai_text_completion.to_dict()
assert dumped_settings["ai_model_id"] == openai_unit_test_env["OPENAI_TEXT_MODEL_ID"]
assert dumped_settings["api_key"] == openai_unit_test_env["OPENAI_API_KEY"]
assert dumped_settings["org_id"] == openai_unit_test_env["OPENAI_ORG_ID"]
# region Get Text Contents
@pytest.fixture()
def completion_response() -> TextCompletion:
return TextCompletion(
id="test",
choices=[TextCompletionChoice(text="test", index=0, finish_reason="stop")],
created=0,
model="test",
object="text_completion",
)
@pytest.fixture()
def streaming_completion_response() -> AsyncStream[TextCompletion]:
content = TextCompletion(
id="test",
choices=[TextCompletionChoice(text="test", index=0, finish_reason="stop")],
created=0,
model="test",
object="text_completion",
)
stream = MagicMock(spec=AsyncStream)
stream.__aiter__.return_value = [content]
return stream
@patch.object(AsyncCompletions, "create", new_callable=AsyncMock)
async def test_tc(
mock_create,
openai_unit_test_env,
completion_response,
) -> None:
mock_create.return_value = completion_response
complete_prompt_execution_settings = OpenAITextPromptExecutionSettings(service_id="test_service_id")
openai_text_completion = OpenAITextCompletion()
await openai_text_completion.get_text_contents(prompt="test", settings=complete_prompt_execution_settings)
mock_create.assert_awaited_once_with(
model=openai_unit_test_env["OPENAI_TEXT_MODEL_ID"],
stream=False,
prompt="test",
echo=False,
)
@patch.object(AsyncCompletions, "create", new_callable=AsyncMock)
async def test_tc_singular(
mock_create,
openai_unit_test_env,
completion_response,
) -> None:
mock_create.return_value = completion_response
complete_prompt_execution_settings = OpenAITextPromptExecutionSettings(service_id="test_service_id")
openai_text_completion = OpenAITextCompletion()
await openai_text_completion.get_text_content(prompt="test", settings=complete_prompt_execution_settings)
mock_create.assert_awaited_once_with(
model=openai_unit_test_env["OPENAI_TEXT_MODEL_ID"],
stream=False,
prompt="test",
echo=False,
)
@patch.object(AsyncCompletions, "create", new_callable=AsyncMock)
async def test_tc_prompt_execution_settings(
mock_create,
openai_unit_test_env,
completion_response,
) -> None:
mock_create.return_value = completion_response
complete_prompt_execution_settings = PromptExecutionSettings(service_id="test_service_id")
openai_text_completion = OpenAITextCompletion()
await openai_text_completion.get_text_contents(prompt="test", settings=complete_prompt_execution_settings)
mock_create.assert_awaited_once_with(
model=openai_unit_test_env["OPENAI_TEXT_MODEL_ID"],
stream=False,
prompt="test",
echo=False,
)
# region Streaming
@patch.object(AsyncCompletions, "create", new_callable=AsyncMock)
async def test_stc(
mock_create,
openai_unit_test_env,
streaming_completion_response,
) -> None:
mock_create.return_value = streaming_completion_response
complete_prompt_execution_settings = OpenAITextPromptExecutionSettings(service_id="test_service_id")
openai_text_completion = OpenAITextCompletion()
[
text
async for text in openai_text_completion.get_streaming_text_contents(
prompt="test", settings=complete_prompt_execution_settings
)
]
mock_create.assert_awaited_once_with(
model=openai_unit_test_env["OPENAI_TEXT_MODEL_ID"],
stream=True,
prompt="test",
echo=False,
)
@patch.object(AsyncCompletions, "create", new_callable=AsyncMock)
async def test_stc_singular(
mock_create,
openai_unit_test_env,
streaming_completion_response,
) -> None:
mock_create.return_value = streaming_completion_response
complete_prompt_execution_settings = OpenAITextPromptExecutionSettings(service_id="test_service_id")
openai_text_completion = OpenAITextCompletion()
[
text
async for text in openai_text_completion.get_streaming_text_content(
prompt="test", settings=complete_prompt_execution_settings
)
]
mock_create.assert_awaited_once_with(
model=openai_unit_test_env["OPENAI_TEXT_MODEL_ID"],
stream=True,
prompt="test",
echo=False,
)
@patch.object(AsyncCompletions, "create", new_callable=AsyncMock)
async def test_stc_prompt_execution_settings(
mock_create,
openai_unit_test_env,
streaming_completion_response,
) -> None:
mock_create.return_value = streaming_completion_response
complete_prompt_execution_settings = PromptExecutionSettings(service_id="test_service_id")
openai_text_completion = OpenAITextCompletion()
[
text
async for text in openai_text_completion.get_streaming_text_contents(
prompt="test", settings=complete_prompt_execution_settings
)
]
mock_create.assert_awaited_once_with(
model=openai_unit_test_env["OPENAI_TEXT_MODEL_ID"],
stream=True,
prompt="test",
echo=False,
)
@patch.object(AsyncCompletions, "create", new_callable=AsyncMock)
async def test_stc_empty_choices(
mock_create,
openai_unit_test_env,
) -> None:
content1 = TextCompletion(
id="test",
choices=[],
created=0,
model="test",
object="text_completion",
)
content2 = TextCompletion(
id="test",
choices=[TextCompletionChoice(text="test", index=0, finish_reason="stop")],
created=0,
model="test",
object="text_completion",
)
stream = MagicMock(spec=AsyncStream)
stream.__aiter__.return_value = [content1, content2]
mock_create.return_value = stream
complete_prompt_execution_settings = OpenAITextPromptExecutionSettings(service_id="test_service_id")
openai_text_completion = OpenAITextCompletion()
results = [
text
async for text in openai_text_completion.get_streaming_text_contents(
prompt="test", settings=complete_prompt_execution_settings
)
]
assert len(results) == 1
mock_create.assert_awaited_once_with(
model=openai_unit_test_env["OPENAI_TEXT_MODEL_ID"],
stream=True,
prompt="test",
echo=False,
)
@@ -0,0 +1,126 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import AsyncMock, patch
import pytest
from openai import AsyncClient
from openai.resources.embeddings import AsyncEmbeddings
from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.open_ai_prompt_execution_settings import (
OpenAIEmbeddingPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.open_ai.services.open_ai_text_embedding import OpenAITextEmbedding
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError, ServiceResponseException
def test_init(openai_unit_test_env):
openai_text_embedding = OpenAITextEmbedding()
assert openai_text_embedding.client is not None
assert isinstance(openai_text_embedding.client, AsyncClient)
assert openai_text_embedding.ai_model_id == openai_unit_test_env["OPENAI_EMBEDDING_MODEL_ID"]
assert openai_text_embedding.get_prompt_execution_settings_class() == OpenAIEmbeddingPromptExecutionSettings
def test_init_validation_fail() -> None:
with pytest.raises(ServiceInitializationError):
OpenAITextEmbedding(api_key="34523", ai_model_id={"test": "dict"})
def test_init_to_from_dict(openai_unit_test_env):
default_headers = {"X-Unit-Test": "test-guid"}
settings = {
"ai_model_id": openai_unit_test_env["OPENAI_EMBEDDING_MODEL_ID"],
"api_key": openai_unit_test_env["OPENAI_API_KEY"],
"default_headers": default_headers,
}
text_embedding = OpenAITextEmbedding.from_dict(settings)
dumped_settings = text_embedding.to_dict()
assert dumped_settings["ai_model_id"] == settings["ai_model_id"]
assert dumped_settings["api_key"] == settings["api_key"]
@pytest.mark.parametrize("exclude_list", [["OPENAI_API_KEY"]], indirect=True)
def test_init_with_empty_api_key(openai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError):
OpenAITextEmbedding(
env_file_path="test.env",
)
@pytest.mark.parametrize("exclude_list", [["OPENAI_EMBEDDING_MODEL_ID"]], indirect=True)
def test_init_with_no_model_id(openai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError):
OpenAITextEmbedding(
env_file_path="test.env",
)
@patch.object(AsyncEmbeddings, "create", new_callable=AsyncMock)
async def test_embedding_calls_with_parameters(mock_create, openai_unit_test_env) -> None:
ai_model_id = "test_model_id"
texts = ["hello world", "goodbye world"]
embedding_dimensions = 1536
openai_text_embedding = OpenAITextEmbedding(
ai_model_id=ai_model_id,
)
await openai_text_embedding.generate_embeddings(texts, dimensions=embedding_dimensions)
mock_create.assert_awaited_once_with(
input=texts,
model=ai_model_id,
dimensions=embedding_dimensions,
)
@patch.object(AsyncEmbeddings, "create", new_callable=AsyncMock)
async def test_embedding_calls_with_settings(mock_create, openai_unit_test_env) -> None:
ai_model_id = "test_model_id"
texts = ["hello world", "goodbye world"]
settings = OpenAIEmbeddingPromptExecutionSettings(service_id="default", dimensions=1536)
openai_text_embedding = OpenAITextEmbedding(service_id="default", ai_model_id=ai_model_id)
await openai_text_embedding.generate_embeddings(texts, settings=settings, timeout=10)
mock_create.assert_awaited_once_with(
input=texts,
model=ai_model_id,
dimensions=1536,
timeout=10,
)
@patch.object(AsyncEmbeddings, "create", new_callable=AsyncMock, side_effect=Exception)
async def test_embedding_fail(mock_create, openai_unit_test_env) -> None:
ai_model_id = "test_model_id"
texts = ["hello world", "goodbye world"]
embedding_dimensions = 1536
openai_text_embedding = OpenAITextEmbedding(
ai_model_id=ai_model_id,
)
with pytest.raises(ServiceResponseException):
await openai_text_embedding.generate_embeddings(texts, dimensions=embedding_dimensions)
@patch.object(AsyncEmbeddings, "create", new_callable=AsyncMock)
async def test_embedding_pes(mock_create, openai_unit_test_env) -> None:
ai_model_id = "test_model_id"
texts = ["hello world", "goodbye world"]
embedding_dimensions = 1536
pes = PromptExecutionSettings(ai_model_id=ai_model_id, dimensions=embedding_dimensions)
openai_text_embedding = OpenAITextEmbedding(ai_model_id=ai_model_id)
await openai_text_embedding.generate_raw_embeddings(texts, pes)
mock_create.assert_awaited_once_with(
input=texts,
model=ai_model_id,
dimensions=embedding_dimensions,
)
@@ -0,0 +1,69 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import patch
import httpx
import pytest
from openai import AsyncClient, _legacy_response
from openai.resources.audio.speech import AsyncSpeech
from semantic_kernel.connectors.ai.open_ai import OpenAITextToAudio, OpenAITextToAudioExecutionSettings
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError
def test_init(openai_unit_test_env):
openai_text_to_audio = OpenAITextToAudio()
assert openai_text_to_audio.client is not None
assert isinstance(openai_text_to_audio.client, AsyncClient)
assert openai_text_to_audio.ai_model_id == openai_unit_test_env["OPENAI_TEXT_TO_AUDIO_MODEL_ID"]
def test_init_validation_fail() -> None:
with pytest.raises(ServiceInitializationError, match="Failed to create OpenAI settings."):
OpenAITextToAudio(api_key="34523", ai_model_id={"test": "dict"})
@pytest.mark.parametrize("exclude_list", [["OPENAI_TEXT_TO_AUDIO_MODEL_ID"]], indirect=True)
def test_init_text_to_audio_model_not_provided(openai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError, match="The OpenAI text to audio model ID is required."):
OpenAITextToAudio(
env_file_path="test.env",
)
@pytest.mark.parametrize("exclude_list", [["OPENAI_API_KEY"]], indirect=True)
def test_init_with_empty_api_key(openai_unit_test_env) -> None:
with pytest.raises(ServiceInitializationError):
OpenAITextToAudio(
env_file_path="test.env",
)
def test_init_to_from_dict(openai_unit_test_env):
default_headers = {"X-Unit-Test": "test-guid"}
settings = {
"ai_model_id": openai_unit_test_env["OPENAI_TEXT_TO_AUDIO_MODEL_ID"],
"api_key": openai_unit_test_env["OPENAI_API_KEY"],
"default_headers": default_headers,
}
audio_to_text = OpenAITextToAudio.from_dict(settings)
dumped_settings = audio_to_text.to_dict()
assert dumped_settings["ai_model_id"] == settings["ai_model_id"]
assert dumped_settings["api_key"] == settings["api_key"]
def test_prompt_execution_settings_class(openai_unit_test_env) -> None:
openai_text_to_audio = OpenAITextToAudio()
assert openai_text_to_audio.get_prompt_execution_settings_class() == OpenAITextToAudioExecutionSettings
@patch.object(AsyncSpeech, "create", return_value=_legacy_response.HttpxBinaryResponseContent(httpx.Response(200)))
async def test_get_text_contents(mock_speech_create, openai_unit_test_env):
openai_text_to_audio = OpenAITextToAudio()
audio_contents = await openai_text_to_audio.get_audio_contents("Hello World!")
assert len(audio_contents) == 1
assert audio_contents[0].ai_model_id == openai_unit_test_env["OPENAI_TEXT_TO_AUDIO_MODEL_ID"]
@@ -0,0 +1,375 @@
# Copyright (c) Microsoft. All rights reserved.
import os
import warnings
from unittest.mock import AsyncMock, patch
import pydantic
import pytest
from openai import AsyncClient
from openai.resources.images import AsyncImages
from openai.types.image import Image
from openai.types.images_response import ImagesResponse
from semantic_kernel.connectors.ai.open_ai import OpenAITextToImage, OpenAITextToImageExecutionSettings
from semantic_kernel.connectors.ai.open_ai.services.open_ai_text_to_image_base import OpenAITextToImageBase
from semantic_kernel.exceptions.service_exceptions import (
ServiceInitializationError,
ServiceInvalidExecutionSettingsError,
ServiceInvalidRequestError,
ServiceResponseException,
)
sample_img = os.path.join(os.path.dirname(__file__), "../../../../../assets/sample_image.jpg")
def test_init(openai_unit_test_env):
"""Test that OpenAITextToImage initializes with the correct model id and client."""
openai_text_to_image = OpenAITextToImage(ai_model_id=openai_unit_test_env["OPENAI_TEXT_TO_IMAGE_MODEL_ID"])
assert openai_text_to_image.client is not None
assert isinstance(openai_text_to_image.client, AsyncClient)
assert openai_text_to_image.ai_model_id == openai_unit_test_env["OPENAI_TEXT_TO_IMAGE_MODEL_ID"]
@pytest.mark.parametrize("exclude_list", [["OPENAI_TEXT_TO_IMAGE_MODEL_ID"]], indirect=True)
def test_init_validation_fail(openai_unit_test_env) -> None:
"""Test that initialization fails when required parameters are missing."""
with pytest.raises(ServiceInitializationError):
OpenAITextToImage(api_key="34523", ai_model_id=None, env_file_path="test.env")
def test_init_to_from_dict(openai_unit_test_env):
"""Test to_dict and from_dict methods for correct serialization and deserialization."""
default_headers = {"X-Unit-Test": "test-guid"}
settings = {
"ai_model_id": openai_unit_test_env["OPENAI_TEXT_TO_IMAGE_MODEL_ID"],
"api_key": openai_unit_test_env["OPENAI_API_KEY"],
"default_headers": default_headers,
}
text_to_image = OpenAITextToImage.from_dict(settings)
dumped_settings = text_to_image.to_dict()
assert dumped_settings["ai_model_id"] == settings["ai_model_id"]
assert dumped_settings["api_key"] == settings["api_key"]
@pytest.mark.parametrize("exclude_list", [["OPENAI_API_KEY"]], indirect=True)
def test_init_with_empty_api_key(openai_unit_test_env) -> None:
"""Test that initialization fails when API key is missing."""
with pytest.raises(ServiceInitializationError):
OpenAITextToImage(
env_file_path="test.env",
)
@pytest.mark.parametrize("exclude_list", [["OPENAI_TEXT_TO_IMAGE_MODEL_ID"]], indirect=True)
def test_init_with_no_model_id(openai_unit_test_env) -> None:
"""Test that initialization fails when model id is missing."""
with pytest.raises(ServiceInitializationError):
OpenAITextToImage(
env_file_path="test.env",
)
def test_prompt_execution_settings_class(openai_unit_test_env) -> None:
"""Test that the correct prompt execution settings class is returned."""
openai_text_to_image = OpenAITextToImage()
assert openai_text_to_image.get_prompt_execution_settings_class() == OpenAITextToImageExecutionSettings
@patch.object(AsyncImages, "generate", new_callable=AsyncMock)
async def test_generate_calls_with_parameters(mock_generate, openai_unit_test_env) -> None:
"""Test that generate_image calls the OpenAI API with correct parameters."""
mock_response = ImagesResponse(created=1, data=[Image(url="abc")], usage=None)
mock_generate.return_value = mock_response
ai_model_id = "test_model_id"
prompt = "painting of flowers in vase"
width = 512
openai_text_to_image = OpenAITextToImage(ai_model_id=ai_model_id)
with warnings.catch_warnings(record=True) as w:
await openai_text_to_image.generate_image(description=prompt, width=width, height=width)
mock_generate.assert_awaited_once_with(
prompt=prompt,
model=ai_model_id,
size=f"{width}x{width}",
n=1,
)
assert len(w) == 3
@patch.object(AsyncImages, "generate", new_callable=AsyncMock, side_effect=Exception)
async def test_generate_fail(mock_generate, openai_unit_test_env) -> None:
"""Test that generate_image raises ServiceResponseException on API failure."""
ai_model_id = "test_model_id"
width = 512
openai_text_to_image = OpenAITextToImage(ai_model_id=ai_model_id)
with pytest.raises(ServiceResponseException):
await openai_text_to_image.generate_image(description="painting of flowers in vase", width=width, height=width)
async def test_generate_invalid_image_size(openai_unit_test_env) -> None:
"""Test that invalid image size raises ServiceInvalidExecutionSettingsError."""
ai_model_id = "test_model_id"
width = 100
openai_text_to_image = OpenAITextToImage(ai_model_id=ai_model_id)
with pytest.raises(ServiceInvalidExecutionSettingsError):
await openai_text_to_image.generate_image(description="painting of flowers in vase", width=width, height=width)
async def test_generate_empty_description(openai_unit_test_env) -> None:
"""Test that empty description raises ServiceInvalidExecutionSettingsError."""
ai_model_id = "test_model_id"
width = 100
openai_text_to_image = OpenAITextToImage(ai_model_id=ai_model_id)
with pytest.raises(ServiceInvalidExecutionSettingsError):
await openai_text_to_image.generate_image(description="", width=width, height=width)
@patch.object(AsyncImages, "generate", new_callable=AsyncMock)
async def test_generate_no_result(mock_generate, openai_unit_test_env) -> None:
"""Test that no result from API raises ServiceResponseException."""
mock_generate.return_value = ImagesResponse(created=0, data=[], usage=None)
ai_model_id = "test_model_id"
width = 512
openai_text_to_image = OpenAITextToImage(ai_model_id=ai_model_id)
with pytest.raises(ServiceResponseException):
await openai_text_to_image.generate_image(description="painting of flowers in vase", width=width, height=width)
@patch.object(OpenAITextToImageBase, "_send_image_edit_request", new_callable=AsyncMock)
async def test_edit_image_with_path_success(mock_edit, openai_unit_test_env):
"""Test editing an image using a file path returns the expected URL."""
mock_edit.return_value = ImagesResponse(created=1, data=[Image(url="edited_url")], usage=None)
service = OpenAITextToImage(ai_model_id=openai_unit_test_env["OPENAI_TEXT_TO_IMAGE_MODEL_ID"])
result = await service.edit_image(
prompt="edit this image",
image_paths=[sample_img],
)
assert result == ["edited_url"]
mock_edit.assert_awaited()
@patch.object(OpenAITextToImageBase, "_send_image_edit_request", new_callable=AsyncMock)
async def test_edit_image_with_file_success(mock_edit, openai_unit_test_env):
"""Test editing an image using a file object returns the expected URL."""
mock_edit.return_value = ImagesResponse(created=1, data=[Image(url="edited_url")], usage=None)
service = OpenAITextToImage(ai_model_id=openai_unit_test_env["OPENAI_TEXT_TO_IMAGE_MODEL_ID"])
with open(sample_img, "rb") as f:
result = await service.edit_image(
prompt="edit this image",
image_files=[f],
)
assert result == ["edited_url"]
mock_edit.assert_awaited()
@patch.object(OpenAITextToImageBase, "_send_image_edit_request", new_callable=AsyncMock)
async def test_edit_image_with_mask_path_and_file(mock_edit, openai_unit_test_env):
"""Test editing an image with both mask path and mask file returns the expected URL."""
mock_edit.return_value = ImagesResponse(created=1, data=[Image(url="edited_url")], usage=None)
service = OpenAITextToImage(ai_model_id=openai_unit_test_env["OPENAI_TEXT_TO_IMAGE_MODEL_ID"])
# mask_path
result = await service.edit_image(
prompt="edit with mask",
image_paths=[sample_img],
mask_path=sample_img,
)
assert result == ["edited_url"]
# mask_file
with open(sample_img, "rb") as mf:
result2 = await service.edit_image(
prompt="edit with mask",
image_paths=[sample_img],
mask_file=mf,
)
assert result2 == ["edited_url"]
@pytest.mark.asyncio
async def test_edit_image_prompt_required(openai_unit_test_env):
"""Test that an empty prompt raises ServiceInvalidRequestError."""
service = OpenAITextToImage(ai_model_id=openai_unit_test_env["OPENAI_TEXT_TO_IMAGE_MODEL_ID"])
with pytest.raises(ServiceInvalidRequestError):
await service.edit_image(prompt="", image_paths=[sample_img])
@pytest.mark.asyncio
async def test_edit_image_both_path_and_file_error(openai_unit_test_env):
"""Test that providing both image_paths and image_files raises ServiceInvalidRequestError."""
service = OpenAITextToImage(ai_model_id=openai_unit_test_env["OPENAI_TEXT_TO_IMAGE_MODEL_ID"])
with (
open(sample_img, "rb") as f,
pytest.raises(ServiceInvalidRequestError),
):
await service.edit_image(
prompt="edit",
image_paths=[sample_img],
image_files=[f],
)
@patch.object(OpenAITextToImageBase, "_send_image_edit_request", new_callable=AsyncMock)
async def test_edit_image_no_valid_data_in_response(mock_edit, openai_unit_test_env):
"""Test that no valid data in edit response raises ServiceResponseException."""
mock_edit.return_value = ImagesResponse(created=1, data=[], usage=None)
service = OpenAITextToImage(ai_model_id=openai_unit_test_env["OPENAI_TEXT_TO_IMAGE_MODEL_ID"])
with pytest.raises(ServiceResponseException):
await service.edit_image(
prompt="edit",
image_paths=[sample_img],
)
@patch.object(OpenAITextToImageBase, "_send_request", new_callable=AsyncMock)
async def test_generate_images_with_n_parameter(mock_generate, openai_unit_test_env):
"""Test that generate_images returns correct URLs when n parameter is set."""
mock_generate.return_value = ImagesResponse(created=3, data=[Image(url=f"url_{i}") for i in range(3)], usage=None)
service = OpenAITextToImage(ai_model_id=openai_unit_test_env["OPENAI_TEXT_TO_IMAGE_MODEL_ID"])
settings = OpenAITextToImageExecutionSettings(n=3)
result = await service.generate_images("prompt", settings=settings)
assert result == [f"url_{i}" for i in range(3)]
@patch.object(OpenAITextToImageBase, "_send_request", new_callable=AsyncMock)
async def test_generate_images_with_output_compression_and_background(mock_generate, openai_unit_test_env):
"""Test that output_compression and background parameters are handled correctly."""
mock_generate.return_value = ImagesResponse(created=1, data=[Image(url="url")], usage=None)
service = OpenAITextToImage(ai_model_id=openai_unit_test_env["OPENAI_TEXT_TO_IMAGE_MODEL_ID"])
settings = OpenAITextToImageExecutionSettings(output_compression=5, background="transparent")
await service.generate_images("prompt", settings=settings)
called_settings = mock_generate.call_args[0][0]
assert called_settings.output_compression == 5
assert called_settings.background == "transparent"
@patch.object(OpenAITextToImageBase, "store_usage")
def test_store_usage_for_images_response(mock_store_usage, openai_unit_test_env):
"""Test that store_usage is called for ImagesResponse."""
service = OpenAITextToImage(ai_model_id=openai_unit_test_env["OPENAI_TEXT_TO_IMAGE_MODEL_ID"])
response = ImagesResponse(created=1, data=[Image(url="url")], usage=None)
service.store_usage(response)
mock_store_usage.assert_called()
@pytest.mark.asyncio
async def test_edit_image_invalid_n_parameter():
"""Test that invalid n parameter raises pydantic.ValidationError."""
with pytest.raises(pydantic.ValidationError):
OpenAITextToImageExecutionSettings(n=0)
with pytest.raises(pydantic.ValidationError):
OpenAITextToImageExecutionSettings(n=11)
@pytest.mark.asyncio
async def test_generate_images_empty_prompt(openai_unit_test_env):
"""Test that empty prompt raises ServiceInvalidRequestError."""
service = OpenAITextToImage(ai_model_id=openai_unit_test_env["OPENAI_TEXT_TO_IMAGE_MODEL_ID"])
with pytest.raises(ServiceInvalidRequestError):
await service.generate_images("")
@patch.object(OpenAITextToImageBase, "_send_request", new_callable=AsyncMock)
async def test_generate_images_no_result(mock_generate, openai_unit_test_env):
"""Test that empty response data raises ServiceResponseException."""
mock_generate.return_value = ImagesResponse(created=0, data=[], usage=None)
service = OpenAITextToImage(ai_model_id=openai_unit_test_env["OPENAI_TEXT_TO_IMAGE_MODEL_ID"])
with pytest.raises(ServiceResponseException):
await service.generate_images("prompt")
@patch.object(OpenAITextToImageBase, "_send_request", new_callable=AsyncMock)
async def test_generate_images_b64_json_response(mock_generate, openai_unit_test_env):
"""Test that generate_images returns b64_json when url is not present."""
mock_generate.return_value = ImagesResponse(created=1, data=[Image(b64_json="base64encodeddata")], usage=None)
service = OpenAITextToImage(ai_model_id=openai_unit_test_env["OPENAI_TEXT_TO_IMAGE_MODEL_ID"])
result = await service.generate_images("prompt")
assert result == ["base64encodeddata"]
@patch.object(OpenAITextToImageBase, "_send_request", new_callable=AsyncMock)
async def test_generate_images_mixed_url_and_b64_response(mock_generate, openai_unit_test_env):
"""Test that generate_images handles mixed url and b64_json responses."""
mock_generate.return_value = ImagesResponse(
created=2,
data=[Image(url="http://example.com/img1.png"), Image(b64_json="base64data")],
usage=None,
)
service = OpenAITextToImage(ai_model_id=openai_unit_test_env["OPENAI_TEXT_TO_IMAGE_MODEL_ID"])
result = await service.generate_images("prompt")
assert result == ["http://example.com/img1.png", "base64data"]
@patch.object(OpenAITextToImageBase, "_send_request", new_callable=AsyncMock)
async def test_generate_images_with_default_settings(mock_generate, openai_unit_test_env):
"""Test that generate_images works when no settings are provided."""
mock_generate.return_value = ImagesResponse(created=1, data=[Image(url="url")], usage=None)
service = OpenAITextToImage(ai_model_id=openai_unit_test_env["OPENAI_TEXT_TO_IMAGE_MODEL_ID"])
result = await service.generate_images("a beautiful sunset")
assert result == ["url"]
mock_generate.assert_awaited_once()
@patch.object(OpenAITextToImageBase, "_send_request", new_callable=AsyncMock)
async def test_generate_images_no_valid_image_data(mock_generate, openai_unit_test_env):
"""Test that generate_images raises error when images have neither url nor b64_json."""
mock_generate.return_value = ImagesResponse(created=1, data=[Image()], usage=None)
service = OpenAITextToImage(ai_model_id=openai_unit_test_env["OPENAI_TEXT_TO_IMAGE_MODEL_ID"])
with pytest.raises(ServiceResponseException, match="No valid image data found"):
await service.generate_images("prompt")
@pytest.mark.asyncio
async def test_edit_image_neither_path_nor_file(openai_unit_test_env):
"""Test that providing neither image_paths nor image_files raises ServiceInvalidRequestError."""
service = OpenAITextToImage(ai_model_id=openai_unit_test_env["OPENAI_TEXT_TO_IMAGE_MODEL_ID"])
with pytest.raises(ServiceInvalidRequestError):
await service.edit_image(prompt="edit this")
@patch.object(OpenAITextToImageBase, "_send_image_edit_request", new_callable=AsyncMock)
async def test_edit_image_b64_json_response(mock_edit, openai_unit_test_env):
"""Test editing an image returns b64_json when url is not present."""
mock_edit.return_value = ImagesResponse(created=1, data=[Image(b64_json="edited_b64")], usage=None)
service = OpenAITextToImage(ai_model_id=openai_unit_test_env["OPENAI_TEXT_TO_IMAGE_MODEL_ID"])
result = await service.edit_image(
prompt="edit this image",
image_paths=[sample_img],
)
assert result == ["edited_b64"]
@patch.object(OpenAITextToImageBase, "_send_image_edit_request", new_callable=AsyncMock)
async def test_edit_image_mixed_response(mock_edit, openai_unit_test_env):
"""Test editing images handles mixed b64_json and url responses."""
mock_edit.return_value = ImagesResponse(
created=2,
data=[Image(b64_json="b64data"), Image(url="http://example.com/edited.png")],
usage=None,
)
service = OpenAITextToImage(ai_model_id=openai_unit_test_env["OPENAI_TEXT_TO_IMAGE_MODEL_ID"])
result = await service.edit_image(
prompt="edit these images",
image_paths=[sample_img],
)
assert result == ["b64data", "http://example.com/edited.png"]
@patch.object(OpenAITextToImageBase, "_send_image_edit_request", new_callable=AsyncMock)
async def test_edit_image_response_no_data_attribute(mock_edit, openai_unit_test_env):
"""Test that edit_image raises error when response has no valid data."""
mock_edit.return_value = ImagesResponse(created=1, data=None, usage=None)
service = OpenAITextToImage(ai_model_id=openai_unit_test_env["OPENAI_TEXT_TO_IMAGE_MODEL_ID"])
with pytest.raises(ServiceResponseException):
await service.edit_image(
prompt="edit",
image_paths=[sample_img],
)
@@ -0,0 +1,384 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from pydantic import BaseModel
from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.azure_chat_prompt_execution_settings import (
AzureAISearchDataSource,
AzureAISearchDataSourceParameters,
AzureChatPromptExecutionSettings,
ExtraBody,
)
from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.open_ai_prompt_execution_settings import (
OpenAIChatPromptExecutionSettings,
OpenAITextPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.connectors.azure_ai_search import AzureAISearchSettings
from semantic_kernel.exceptions import ServiceInvalidExecutionSettingsError
from semantic_kernel.kernel_pydantic import KernelBaseModel
############################################
# Test classes for structured output
class ClassTest:
attribute: str
class ClassTestPydantic(KernelBaseModel):
attribute: str
############################################
def test_default_openai_chat_prompt_execution_settings():
settings = OpenAIChatPromptExecutionSettings()
assert settings.temperature is None
assert settings.top_p is None
assert settings.presence_penalty is None
assert settings.frequency_penalty is None
assert settings.max_tokens is None
assert settings.stop is None
assert settings.number_of_responses is None
assert settings.logit_bias is None
assert settings.messages is None
def test_custom_openai_chat_prompt_execution_settings():
settings = OpenAIChatPromptExecutionSettings(
temperature=0.5,
top_p=0.5,
presence_penalty=0.5,
frequency_penalty=0.5,
max_tokens=128,
stop="\n",
number_of_responses=2,
logit_bias={"1": 1},
messages=[{"role": "system", "content": "Hello"}],
)
assert settings.temperature == 0.5
assert settings.top_p == 0.5
assert settings.presence_penalty == 0.5
assert settings.frequency_penalty == 0.5
assert settings.max_tokens == 128
assert settings.stop == "\n"
assert settings.number_of_responses == 2
assert settings.logit_bias == {"1": 1}
assert settings.messages == [{"role": "system", "content": "Hello"}]
def test_openai_chat_prompt_execution_settings_from_default_completion_config():
settings = PromptExecutionSettings(service_id="test_service")
chat_settings = OpenAIChatPromptExecutionSettings.from_prompt_execution_settings(settings)
assert chat_settings.service_id == "test_service"
assert chat_settings.temperature is None
assert chat_settings.top_p is None
assert chat_settings.presence_penalty is None
assert chat_settings.frequency_penalty is None
assert chat_settings.max_tokens is None
assert chat_settings.stop is None
assert chat_settings.number_of_responses is None
assert chat_settings.logit_bias is None
def test_openai_chat_prompt_execution_settings_from_openai_prompt_execution_settings():
chat_settings = OpenAIChatPromptExecutionSettings(service_id="test_service", temperature=1.0)
new_settings = OpenAIChatPromptExecutionSettings(service_id="test_2", temperature=0.0)
chat_settings.update_from_prompt_execution_settings(new_settings)
assert chat_settings.service_id == "test_2"
assert chat_settings.temperature == 0.0
def test_openai_text_prompt_execution_settings_validation():
with pytest.raises(ServiceInvalidExecutionSettingsError, match="best_of must be greater than number_of_responses"):
OpenAITextPromptExecutionSettings(best_of=1, number_of_responses=2)
def test_openai_text_prompt_execution_settings_validation_manual():
text_oai = OpenAITextPromptExecutionSettings(best_of=1, number_of_responses=1)
with pytest.raises(ServiceInvalidExecutionSettingsError, match="best_of must be greater than number_of_responses"):
text_oai.number_of_responses = 2
def test_openai_chat_prompt_execution_settings_from_custom_completion_config():
settings = PromptExecutionSettings(
service_id="test_service",
extension_data={
"temperature": 0.5,
"top_p": 0.5,
"presence_penalty": 0.5,
"frequency_penalty": 0.5,
"max_tokens": 128,
"stop": ["\n"],
"number_of_responses": 2,
"logprobs": 1,
"logit_bias": {"1": 1},
"messages": [{"role": "system", "content": "Hello"}],
},
)
chat_settings = OpenAIChatPromptExecutionSettings.from_prompt_execution_settings(settings)
assert chat_settings.temperature == 0.5
assert chat_settings.top_p == 0.5
assert chat_settings.presence_penalty == 0.5
assert chat_settings.frequency_penalty == 0.5
assert chat_settings.max_tokens == 128
assert chat_settings.stop == ["\n"]
assert chat_settings.number_of_responses == 2
assert chat_settings.logit_bias == {"1": 1}
def test_openai_chat_prompt_execution_settings_from_custom_completion_config_with_none():
settings = PromptExecutionSettings(
service_id="test_service",
extension_data={
"temperature": 0.5,
"top_p": 0.5,
"presence_penalty": 0.5,
"frequency_penalty": 0.5,
"max_tokens": 128,
"stop": ["\n"],
"number_of_responses": 2,
"functions": None,
"logit_bias": {"1": 1},
"messages": [{"role": "system", "content": "Hello"}],
},
)
chat_settings = OpenAIChatPromptExecutionSettings.from_prompt_execution_settings(settings)
assert chat_settings.temperature == 0.5
assert chat_settings.top_p == 0.5
assert chat_settings.presence_penalty == 0.5
assert chat_settings.frequency_penalty == 0.5
assert chat_settings.max_tokens == 128
assert chat_settings.stop == ["\n"]
assert chat_settings.number_of_responses == 2
assert chat_settings.logit_bias == {"1": 1}
assert chat_settings.functions is None
def test_openai_chat_prompt_execution_settings_from_custom_completion_config_with_functions():
settings = PromptExecutionSettings(
service_id="test_service",
extension_data={
"temperature": 0.5,
"top_p": 0.5,
"presence_penalty": 0.5,
"frequency_penalty": 0.5,
"max_tokens": 128,
"stop": ["\n"],
"number_of_responses": 2,
"functions": [{}],
"function_call": "auto",
"logit_bias": {"1": 1},
"messages": [{"role": "system", "content": "Hello"}],
},
)
chat_settings = OpenAIChatPromptExecutionSettings.from_prompt_execution_settings(settings)
assert chat_settings.temperature == 0.5
assert chat_settings.top_p == 0.5
assert chat_settings.presence_penalty == 0.5
assert chat_settings.frequency_penalty == 0.5
assert chat_settings.max_tokens == 128
assert chat_settings.stop == ["\n"]
assert chat_settings.number_of_responses == 2
assert chat_settings.logit_bias == {"1": 1}
assert chat_settings.functions == [{}]
def test_create_options():
settings = OpenAIChatPromptExecutionSettings(
temperature=0.5,
top_p=0.5,
presence_penalty=0.5,
frequency_penalty=0.5,
max_tokens=128,
stop=["\n"],
number_of_responses=2,
logit_bias={"1": 1},
messages=[{"role": "system", "content": "Hello"}],
function_call="auto",
)
options = settings.prepare_settings_dict()
assert options["temperature"] == 0.5
assert options["top_p"] == 0.5
assert options["presence_penalty"] == 0.5
assert options["frequency_penalty"] == 0.5
assert options["max_tokens"] == 128
assert options["stop"] == ["\n"]
assert options["n"] == 2
assert options["logit_bias"] == {"1": 1}
assert not options["stream"]
def test_create_options_azure_data():
az_source = AzureAISearchDataSource(
parameters={
"indexName": "test-index",
"endpoint": "test-endpoint",
"authentication": {"type": "api_key", "key": "test-key"},
}
)
extra = ExtraBody(data_sources=[az_source])
assert extra["data_sources"] is not None
assert extra.data_sources is not None
settings = AzureChatPromptExecutionSettings(extra_body=extra)
options = settings.prepare_settings_dict()
assert options["extra_body"] == extra.model_dump(exclude_none=True, by_alias=True)
assert options["extra_body"]["data_sources"][0]["type"] == "azure_search"
def test_create_options_azure_data_from_azure_ai_settings(azure_ai_search_unit_test_env):
az_source = AzureAISearchDataSource.from_azure_ai_search_settings(AzureAISearchSettings())
extra = ExtraBody(data_sources=[az_source])
assert extra["data_sources"] is not None
settings = AzureChatPromptExecutionSettings(extra_body=extra)
options = settings.prepare_settings_dict()
assert options["extra_body"] == extra.model_dump(exclude_none=True, by_alias=True)
assert options["extra_body"]["data_sources"][0]["type"] == "azure_search"
def test_azure_open_ai_chat_prompt_execution_settings_with_cosmosdb_data_sources():
input_dict = {
"messages": [{"role": "system", "content": "Hello"}],
"extra_body": {
"dataSources": [
{
"type": "AzureCosmosDB",
"parameters": {
"authentication": {
"type": "ConnectionString",
"connectionString": "mongodb+srv://onyourdatatest:{password}$@{cluster-name}.mongocluster.cosmos.azure.com/?tls=true&authMechanism=SCRAM-SHA-256&retrywrites=false&maxIdleTimeMS=120000",
},
"databaseName": "vectordb",
"containerName": "azuredocs",
"indexName": "azuredocindex",
"embeddingDependency": {
"type": "DeploymentName",
"deploymentName": "{embedding deployment name}",
},
"fieldsMapping": {"vectorFields": ["contentvector"]},
},
}
]
},
}
settings = AzureChatPromptExecutionSettings.model_validate(input_dict, strict=True, from_attributes=True)
assert settings.extra_body["dataSources"][0]["type"] == "AzureCosmosDB"
def test_azure_open_ai_chat_prompt_execution_settings_with_aisearch_data_sources():
input_dict = {
"messages": [{"role": "system", "content": "Hello"}],
"extra_body": {
"dataSources": [
{
"type": "AzureCognitiveSearch",
"parameters": {
"authentication": {
"type": "APIKey",
"key": "****",
},
"endpoint": "https://****.search.windows.net/",
"indexName": "azuredocindex",
"queryType": "vector",
"embeddingDependency": {
"type": "DeploymentName",
"deploymentName": "{embedding deployment name}",
},
"fieldsMapping": {"vectorFields": ["contentvector"]},
},
}
]
},
}
settings = AzureChatPromptExecutionSettings.model_validate(input_dict, strict=True, from_attributes=True)
assert settings.extra_body["dataSources"][0]["type"] == "AzureCognitiveSearch"
@pytest.mark.parametrize(
"authentication",
[
{"type": "APIKey", "key": "test_key"},
{"type": "api_key", "key": "test_key"},
pytest.param({"type": "api_key"}, marks=pytest.mark.xfail),
{"type": "SystemAssignedManagedIdentity"},
{"type": "system_assigned_managed_identity"},
{"type": "UserAssignedManagedIdentity", "managed_identity_resource_id": "test_id"},
{"type": "user_assigned_managed_identity", "managed_identity_resource_id": "test_id"},
pytest.param({"type": "user_assigned_managed_identity"}, marks=pytest.mark.xfail),
{"type": "AccessToken", "access_token": "test_token"},
{"type": "access_token", "access_token": "test_token"},
pytest.param({"type": "access_token"}, marks=pytest.mark.xfail),
pytest.param({"type": "invalid"}, marks=pytest.mark.xfail),
],
ids=[
"APIKey",
"api_key",
"api_key_no_key",
"SystemAssignedManagedIdentity",
"system_assigned_managed_identity",
"UserAssignedManagedIdentity",
"user_assigned_managed_identity",
"user_assigned_managed_identity_no_id",
"AccessToken",
"access_token",
"access_token_no_token",
"invalid",
],
)
def test_aisearch_data_source_parameters(authentication) -> None:
AzureAISearchDataSourceParameters(index_name="test_index", authentication=authentication)
def test_azure_open_ai_chat_prompt_execution_settings_with_response_format_json():
response_format = {"type": "json_object"}
settings = AzureChatPromptExecutionSettings(response_format=response_format)
options = settings.prepare_settings_dict()
assert options["response_format"] == response_format
def test_openai_chat_prompt_execution_settings_with_json_structured_output():
settings = OpenAIChatPromptExecutionSettings()
settings.response_format = {
"type": "json_schema",
"json_schema": {
"name": "math_response",
"schema": {
"type": "object",
"properties": {
"steps": {
"type": "array",
"items": {
"type": "object",
"properties": {"explanation": {"type": "string"}, "output": {"type": "string"}},
"required": ["explanation", "output"],
"additionalProperties": False,
},
},
"final_answer": {"type": "string"},
},
"required": ["steps", "final_answer"],
"additionalProperties": False,
},
"strict": True,
},
}
assert isinstance(settings.response_format, dict)
def test_openai_chat_prompt_execution_settings_with_nonpydantic_type_structured_output():
settings = OpenAIChatPromptExecutionSettings()
settings.response_format = ClassTest
assert isinstance(settings.response_format, type)
def test_openai_chat_prompt_execution_settings_with_pydantic_type_structured_output():
settings = OpenAIChatPromptExecutionSettings()
settings.response_format = ClassTestPydantic
assert issubclass(settings.response_format, BaseModel)
def test_openai_chat_prompt_execution_settings_with_invalid_structured_output():
settings = OpenAIChatPromptExecutionSettings()
with pytest.raises(ServiceInvalidExecutionSettingsError):
settings.response_format = "invalid"
@@ -0,0 +1,43 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.connectors.ai.completion_usage import CompletionUsage
def test_completion_usage() -> None:
"""Test the CompletionUsage class."""
# Create a CompletionUsage object
usage = CompletionUsage(prompt_tokens=10, completion_tokens=20)
# Check that the prompt tokens and completion tokens are set correctly
assert usage.prompt_tokens == 10
assert usage.completion_tokens == 20
# Create another CompletionUsage object
other_usage = CompletionUsage(prompt_tokens=5, completion_tokens=15)
# Add the two CompletionUsage objects together
total_usage = usage + other_usage
# Check that the total prompt tokens and completion tokens are correct
assert total_usage.prompt_tokens == 15
assert total_usage.completion_tokens == 35
def test_completion_usage_empty() -> None:
"""Test the CompletionUsage class with empty values."""
# Create a CompletionUsage object with empty values
usage = CompletionUsage()
# Check that the prompt tokens and completion tokens are None
assert usage.prompt_tokens is None
assert usage.completion_tokens is None
# Create another CompletionUsage object with empty values
other_usage = CompletionUsage(prompt_tokens=5, completion_tokens=None)
# Add the two CompletionUsage objects together
total_usage = usage + other_usage
# Check that the total prompt tokens and completion tokens are None
assert total_usage.prompt_tokens == 5
assert total_usage.completion_tokens == 0
@@ -0,0 +1,232 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import TYPE_CHECKING
from unittest.mock import Mock
import pytest
if TYPE_CHECKING:
from semantic_kernel.kernel import Kernel
from semantic_kernel.connectors.ai.function_calling_utils import _combine_filter_dicts
from semantic_kernel.connectors.ai.function_choice_behavior import (
DEFAULT_MAX_AUTO_INVOKE_ATTEMPTS,
FunctionChoiceBehavior,
FunctionChoiceType,
)
from semantic_kernel.exceptions import ServiceInitializationError
@pytest.fixture
def function_choice_behavior():
return FunctionChoiceBehavior()
@pytest.fixture
def update_settings_callback():
mock = Mock()
mock.return_value = None
return mock
def test_function_choice_behavior_auto():
behavior = FunctionChoiceBehavior.Auto(auto_invoke=True)
assert behavior.type_ == FunctionChoiceType.AUTO
assert behavior.maximum_auto_invoke_attempts == DEFAULT_MAX_AUTO_INVOKE_ATTEMPTS
def test_function_choice_behavior_none_invoke():
behavior = FunctionChoiceBehavior.NoneInvoke()
assert behavior.type_ == FunctionChoiceType.NONE
assert behavior.maximum_auto_invoke_attempts == 0
def test_function_choice_behavior_required():
expected_filters = {"included_functions": ["plugin1-func1"]}
behavior = FunctionChoiceBehavior.Required(auto_invoke=True, filters=expected_filters)
assert behavior.type_ == FunctionChoiceType.REQUIRED
assert behavior.maximum_auto_invoke_attempts == 1
assert behavior.filters == expected_filters
@pytest.mark.parametrize(("type", "max_auto_invoke_attempts"), [("auto", 5), ("none", 0), ("required", 1)])
def test_auto_function_choice_behavior_from_dict(type: str, max_auto_invoke_attempts: int):
data = {
"type": type,
"filters": {"included_functions": ["plugin1-func1", "plugin2-func2"]},
"maximum_auto_invoke_attempts": max_auto_invoke_attempts,
}
behavior = FunctionChoiceBehavior.from_dict(data)
assert behavior.type_ == FunctionChoiceType(type)
assert behavior.filters == {"included_functions": ["plugin1-func1", "plugin2-func2"]}
assert behavior.maximum_auto_invoke_attempts == max_auto_invoke_attempts
@pytest.mark.parametrize(("type", "max_auto_invoke_attempts"), [("auto", 5), ("none", 0), ("required", 1)])
def test_auto_function_choice_behavior_from_dict_with_same_filters_and_functions(
type: str, max_auto_invoke_attempts: int
):
data = {
"type": type,
"filters": {"included_functions": ["plugin1-func1", "plugin2-func2"]},
"functions": ["plugin1-func1", "plugin2-func2"],
"maximum_auto_invoke_attempts": max_auto_invoke_attempts,
}
behavior = FunctionChoiceBehavior.from_dict(data)
assert behavior.type_ == FunctionChoiceType(type)
assert behavior.filters == {"included_functions": ["plugin1-func1", "plugin2-func2"]}
assert behavior.maximum_auto_invoke_attempts == max_auto_invoke_attempts
@pytest.mark.parametrize(("type", "max_auto_invoke_attempts"), [("auto", 5), ("none", 0), ("required", 1)])
def test_auto_function_choice_behavior_from_dict_with_different_filters_and_functions(
type: str, max_auto_invoke_attempts: int
):
data = {
"type": type,
"filters": {"included_functions": ["plugin1-func1", "plugin2-func2"]},
"functions": ["plugin3-func3"],
"maximum_auto_invoke_attempts": max_auto_invoke_attempts,
}
behavior = FunctionChoiceBehavior.from_dict(data)
assert behavior.type_ == FunctionChoiceType(type)
assert behavior.filters == {"included_functions": ["plugin1-func1", "plugin2-func2", "plugin3-func3"]}
assert behavior.maximum_auto_invoke_attempts == max_auto_invoke_attempts
def test_function_choice_behavior_get_set(function_choice_behavior: FunctionChoiceBehavior):
function_choice_behavior.enable_kernel_functions = False
assert function_choice_behavior.enable_kernel_functions is False
function_choice_behavior.maximum_auto_invoke_attempts = 10
assert function_choice_behavior.maximum_auto_invoke_attempts == 10
assert function_choice_behavior.auto_invoke_kernel_functions is True
function_choice_behavior.auto_invoke_kernel_functions = False
assert function_choice_behavior.auto_invoke_kernel_functions is False
assert function_choice_behavior.maximum_auto_invoke_attempts == 0
function_choice_behavior.auto_invoke_kernel_functions = True
assert function_choice_behavior.auto_invoke_kernel_functions is True
assert function_choice_behavior.maximum_auto_invoke_attempts == 5
def test_auto_invoke_kernel_functions():
fcb = FunctionChoiceBehavior.Auto(auto_invoke=True)
assert fcb is not None
assert fcb.enable_kernel_functions is True
assert fcb.maximum_auto_invoke_attempts == 5
assert fcb.auto_invoke_kernel_functions is True
def test_none_invoke_kernel_functions():
fcb = FunctionChoiceBehavior.NoneInvoke()
assert fcb is not None
assert fcb.enable_kernel_functions is True
assert fcb.maximum_auto_invoke_attempts == 0
assert fcb.auto_invoke_kernel_functions is False
def test_enable_functions():
fcb = FunctionChoiceBehavior.Auto(auto_invoke=True, filters={"excluded_plugins": ["test"]})
assert fcb is not None
assert fcb.enable_kernel_functions is True
assert fcb.maximum_auto_invoke_attempts == 5
assert fcb.auto_invoke_kernel_functions is True
assert fcb.filters == {"excluded_plugins": ["test"]}
def test_required_function():
fcb = FunctionChoiceBehavior.Required(auto_invoke=True, filters={"included_functions": ["test"]})
assert fcb is not None
assert fcb.enable_kernel_functions is True
assert fcb.maximum_auto_invoke_attempts == 1
assert fcb.auto_invoke_kernel_functions is True
def test_configure_auto_invoke_kernel_functions(update_settings_callback, kernel: "Kernel"):
fcb = FunctionChoiceBehavior.Auto(auto_invoke=True)
fcb.configure(kernel, update_settings_callback, None)
assert update_settings_callback.called
def test_configure_auto_invoke_kernel_functions_skip(update_settings_callback, kernel: "Kernel"):
fcb = FunctionChoiceBehavior.Auto(auto_invoke=True)
fcb.enable_kernel_functions = False
fcb.configure(kernel, update_settings_callback, None)
assert not update_settings_callback.called
def test_configure_none_invoke_kernel_functions(update_settings_callback, kernel: "Kernel"):
fcb = FunctionChoiceBehavior.NoneInvoke()
fcb.configure(kernel, update_settings_callback, None)
assert update_settings_callback.called
def test_configure_none_invoke_kernel_functions_skip(update_settings_callback, kernel: "Kernel"):
fcb = FunctionChoiceBehavior.NoneInvoke()
fcb.enable_kernel_functions = False
fcb.configure(kernel, update_settings_callback, None)
assert not update_settings_callback.called
def test_configure_enable_functions(update_settings_callback, kernel: "Kernel"):
fcb = FunctionChoiceBehavior.Auto(auto_invoke=True, filters={"excluded_plugins": ["test"]})
fcb.configure(kernel, update_settings_callback, None)
assert update_settings_callback.called
def test_configure_enable_functions_skip(update_settings_callback, kernel: "Kernel"):
fcb = FunctionChoiceBehavior.Auto(auto_invoke=True, filters={"excluded_plugins": ["test"]})
fcb.enable_kernel_functions = False
fcb.configure(kernel, update_settings_callback, None)
assert not update_settings_callback.called
def test_configure_required_function(update_settings_callback, kernel: "Kernel"):
fcb = FunctionChoiceBehavior.Required(auto_invoke=True, filters={"included_functions": ["plugin1-func1"]})
fcb.configure(kernel, update_settings_callback, None)
assert update_settings_callback.called
def test_configure_required_function_max_invoke_updated(update_settings_callback, kernel: "Kernel"):
fcb = FunctionChoiceBehavior.Required(auto_invoke=True, filters={"included_functions": ["plugin1-func1"]})
fcb.maximum_auto_invoke_attempts = 10
fcb.configure(kernel, update_settings_callback, None)
assert update_settings_callback.called
assert fcb.maximum_auto_invoke_attempts == 10
def test_configure_required_function_skip(update_settings_callback, kernel: "Kernel"):
fcb = FunctionChoiceBehavior.Required(auto_invoke=True, filters={"included_functions": ["test"]})
fcb.enable_kernel_functions = False
fcb.configure(kernel, update_settings_callback, None)
assert not update_settings_callback.called
def test_service_initialization_error():
dict1 = {"filter1": ["a", "b", "c"]}
dict2 = {"filter1": "not_a_list"} # This should trigger the error
with pytest.raises(ServiceInitializationError, match="Values for filter key 'filter1' are not lists."):
_combine_filter_dicts(dict1, dict2)
def test_from_string_auto():
auto = FunctionChoiceBehavior.from_string("auto")
assert auto == FunctionChoiceBehavior.Auto()
def test_from_string_none():
none = FunctionChoiceBehavior.from_string("none")
assert none == FunctionChoiceBehavior.NoneInvoke()
def test_from_string_required():
required = FunctionChoiceBehavior.from_string("required")
assert required == FunctionChoiceBehavior.Required()
def test_from_string_invalid():
with pytest.raises(
ServiceInitializationError,
match="The specified type `invalid` is not supported. Allowed types are: `auto`, `none`, `required`.",
):
FunctionChoiceBehavior.from_string("invalid")
@@ -0,0 +1,16 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.connectors.ai import PromptExecutionSettings
def test_init():
settings = PromptExecutionSettings()
assert settings.service_id is None
assert settings.extension_data == {}
def test_init_with_data():
ext_data = {"test": "test"}
settings = PromptExecutionSettings(service_id="test", extension_data=ext_data)
assert settings.service_id == "test"
assert settings.extension_data["test"] == "test"