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,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