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