Python: Migrate to new Google GenAI SDK (#13371)

### Motivation and Context

<!-- Thank you for your contribution to the semantic-kernel repo!
Please help reviewers and future users, providing the following
information:
  1. Why is this change required?
  2. What problem does it solve?
  3. What scenario does it contribute to?
  4. If it fixes an open issue, please link to the issue here.
-->
Closes #12970, #13352

### Description

<!-- Describe your changes, the overall approach, the underlying design.
These notes will help understanding how your code works. Thanks! -->
1. Migrate Google AI connector to use the new Google AI SDK.
2. This PR also marks the VertexAI connector as deprecated in favor of
the Google AI connector. With this update, if users wants to connect to
a VertexAI project instead of a Google AI endpoint, they can do so with
the Google AI connector.
3. This PR also adds the ability for users to provide a custom client to
the Google AI connector (#13352).
4. This PR also marks `AzureTextCompletion` deprecated.

### Contribution Checklist

<!-- Before submitting this PR, please make sure: -->

- [x] The code builds clean without any errors or warnings
- [x] The PR follows the [SK Contribution
Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md)
and the [pre-submission formatting
script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts)
raises no violations
- [x] All unit tests pass, and I have added new tests where possible
- [ ] I didn't break anyone 😄
This commit is contained in:
Tao Chen
2025-11-24 21:53:57 +00:00
committed by GitHub
parent 5b30c8b703
commit d84363f477
48 changed files with 4849 additions and 4723 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ Highlights
- Flexible Agent Framework: build, orchestrate, and deploy AI agents and multi-agent systems
- Multi-Agent Systems: Model workflows and collaboration between AI specialists
- Plugin Ecosystem: Extend with Python, OpenAPI, Model Context Protocol (MCP), and more
- LLM Support: OpenAI, Azure OpenAI, Hugging Face, Mistral, Vertex AI, ONNX, Ollama, NVIDIA NIM, and others
- LLM Support: OpenAI, Azure OpenAI, Hugging Face, Mistral, Google AI, ONNX, Ollama, NVIDIA NIM, and others
- Vector DB Support: Azure AI Search, Elasticsearch, Chroma, and more
- Process Framework: Build structured business processes with workflow modeling
- Multimodal: Text, vision, audio
+2 -2
View File
@@ -24,7 +24,7 @@ classifiers = [
]
dependencies = [
# azure agents
"azure-ai-projects >= 1.0.0b12",
"azure-ai-projects ~= 1.0.0b12",
"azure-ai-agents >= 1.2.0b3",
"aiohttp ~= 3.8",
"cloudevents ~=1.0",
@@ -93,7 +93,7 @@ faiss = [
]
google = [
"google-cloud-aiplatform ~= 1.114.0",
"google-generativeai ~= 0.8"
"google-genai ~= 1.51.0"
]
hugging_face = [
"transformers[torch] ~= 4.28",
@@ -3,29 +3,21 @@
import asyncio
import os
from azure.identity import AzureCliCredential
from semantic_kernel import Kernel
from semantic_kernel.connectors.ai.open_ai import AzureTextCompletion, OpenAITextCompletion
from semantic_kernel.connectors.ai.open_ai import OpenAITextCompletion
from semantic_kernel.functions import KernelArguments
async def main():
kernel = Kernel()
useAzureOpenAI = False
model = "gpt-35-turbo-instruct" if useAzureOpenAI else "gpt-3.5-turbo-instruct"
model = "gpt-3.5-turbo-instruct"
service_id = model
# Configure AI service used by the kernel
if useAzureOpenAI:
kernel.add_service(
AzureTextCompletion(service_id=service_id, credential=AzureCliCredential()),
)
else:
kernel.add_service(
OpenAITextCompletion(service_id=service_id, ai_model_id=model),
)
kernel.add_service(
OpenAITextCompletion(service_id=service_id, ai_model_id=model),
)
# note: using plugins from the samples folder
plugins_directory = os.path.join(__file__, "../../../../../prompt_template_samples/")
@@ -11,7 +11,6 @@
| | [OpenAITextToAudio](../../../semantic_kernel/connectors/ai/open_ai/services/open_ai_text_to_audio.py) | ai_model_id, <br> api_key, <br> org_id | OPENAI_TEXT_TO_AUDIO_MODEL_ID, <br> OPENAI_API_KEY, <br> OPENAI_ORG_ID | Yes, <br> Yes, <br> No | |
| | [OpenAIAudioToText](../../../semantic_kernel/connectors/ai/open_ai/services/open_ai_audio_to_text.py) | ai_model_id, <br> api_key, <br> org_id | OPENAI_AUDIO_TO_TEXT_MODEL_ID, <br> OPENAI_API_KEY, <br> OPENAI_ORG_ID | Yes, <br> Yes, <br> No | |
| Azure OpenAI | [AzureChatCompletion](../../../semantic_kernel/connectors/ai/open_ai/services/azure_chat_completion.py) | deployment_name, <br> api_key, <br> endpoint, <br> api_version, <br> base_url | AZURE_OPENAI_CHAT_DEPLOYMENT_NAME, <br> AZURE_OPENAI_API_KEY, <br> AZURE_OPENAI_ENDPOINT, <br> AZURE_OPENAI_API_VERSION, <br> AZURE_OPENAI_BASE_URL | Yes, <br> No, <br> Yes, <br> Yes, <br> Yes | [AzureOpenAISettings](../../../semantic_kernel/connectors/ai/open_ai/settings/azure_open_ai_settings.py) |
| | [AzureTextCompletion](../../../semantic_kernel/connectors/ai/open_ai/services/azure_text_completion.py) | deployment_name, <br> api_key, <br> endpoint, <br> api_version, <br> base_url | AZURE_OPENAI_TEXT_DEPLOYMENT_NAME, <br> AZURE_OPENAI_API_KEY, <br> AZURE_OPENAI_ENDPOINT, <br> AZURE_OPENAI_API_VERSION, <br> AZURE_OPENAI_BASE_URL | Yes, <br> No, <br> Yes, <br> Yes, <br> Yes | |
| | [AzureTextEmbedding](../../../semantic_kernel/connectors/ai/open_ai/services/azure_text_embedding.py) | deployment_name, <br> api_key, <br> endpoint, <br> api_version, <br> base_url | AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME, <br> AZURE_OPENAI_API_KEY, <br> AZURE_OPENAI_ENDPOINT, <br> AZURE_OPENAI_API_VERSION, <br> AZURE_OPENAI_BASE_URL | Yes, <br> No, <br> Yes, <br> Yes, <br> Yes | |
| | [AzureTextToImage](../../../semantic_kernel/connectors/ai/open_ai/services/azure_text_to_image.py) | deployment_name, <br> api_key, <br> endpoint | AZURE_OPENAI_TEXT_TO_IMAGE_DEPLOYMENT_NAME, <br> AZURE_OPENAI_API_KEY, <br> AZURE_OPENAI_ENDPOINT | Yes, <br> No, <br> Yes | |
| | [AzureTextToAudio](../../../semantic_kernel/connectors/ai/open_ai/services/azure_text_to_audio.py) | deployment_name, <br> api_key, <br> endpoint | AZURE_OPENAI_TEXT_TO_AUDIO_DEPLOYMENT_NAME, <br> AZURE_OPENAI_API_KEY, <br> AZURE_OPENAI_ENDPOINT | Yes, <br> No, <br> Yes | |
@@ -25,9 +24,9 @@
| Google AI | [GoogleAIChatCompletion](../../../semantic_kernel/connectors/ai/google/google_ai/services/google_ai_chat_completion.py) | gemini_model_id, <br> api_key | GOOGLE_AI_GEMINI_MODEL_ID, <br> GOOGLE_AI_API_KEY | Yes, <br> Yes | [GoogleAISettings](../../../semantic_kernel/connectors/ai/google/google_ai/google_ai_settings.py) |
| | [GoogleAITextCompletion](../../../semantic_kernel/connectors/ai/google/google_ai/services/google_ai_text_completion.py) | gemini_model_id, <br> api_key | GOOGLE_AI_GEMINI_MODEL_ID, <br> GOOGLE_AI_API_KEY | Yes, <br> Yes | |
| | [GoogleAITextEmbedding](../../../semantic_kernel/connectors/ai/google/google_ai/services/google_ai_text_embedding.py) | embedding_model_id, <br> api_key | GOOGLE_AI_EMBEDDING_MODEL_ID, <br> GOOGLE_AI_API_KEY | Yes, <br> Yes | |
| Vertex AI | [VertexAIChatCompletion](../../../semantic_kernel/connectors/ai/google/vertex_ai/services/vertex_ai_chat_completion.py) | project_id, <br> region, <br> gemini_model_id | VERTEX_AI_PROJECT_ID, <br> VERTEX_AI_REGION, <br> VERTEX_AI_GEMINI_MODEL_ID | Yes, <br> No, <br> Yes | [VertexAISettings](../../../semantic_kernel/connectors/ai/google/vertex_ai/vertex_ai_settings.py) |
| | [VertexAITextCompletion](../../../semantic_kernel/connectors/ai/google/google_ai/services/google_ai_text_completion.py) | project_id, <br> region, <br> gemini_model_id | VERTEX_AI_PROJECT_ID, <br> VERTEX_AI_REGION, <br> VERTEX_AI_GEMINI_MODEL_ID | Yes, <br> No, <br> Yes | |
| | [VertexAITextEmbedding](../../../semantic_kernel/connectors/ai/google/google_ai/services/google_ai_text_embedding.py) | project_id, <br> region, <br> embedding_model_id | VERTEX_AI_PROJECT_ID, <br> VERTEX_AI_REGION, <br> VERTEX_AI_EMBEDDING_MODEL_ID | Yes, <br> No, <br> Yes | |
| Vertex AI | [GoogleAIChatCompletion](../../../semantic_kernel/connectors/ai/google/google_ai/services/google_ai_chat_completion.py) | project_id, <br> region, <br> gemini_model_id | GOOGLE_AI_CLOUD_PROJECT_ID, <br> GOOGLE_AI_CLOUD_REGION, <br> GOOGLE_AI_GEMINI_MODEL_ID, <br> GOOGLE_AI_USE_VERTEXAI | Yes, <br> No, <br> Yes, <br> Yes (must set to true) | [GoogleAISettings](../../../semantic_kernel/connectors/ai/google/google_ai/google_ai_settings.py) |
| | [GoogleAITextCompletion](../../../semantic_kernel/connectors/ai/google/google_ai/services/google_ai_text_completion.py) | project_id, <br> region, <br> gemini_model_id | GOOGLE_AI_CLOUD_PROJECT_ID, <br> GOOGLE_AI_CLOUD_REGION, <br> GOOGLE_AI_GEMINI_MODEL_ID, <br> GOOGLE_AI_USE_VERTEXAI | Yes, <br> No, <br> Yes, <br> Yes (must set to true) | |
| | [GoogleAITextEmbedding](../../../semantic_kernel/connectors/ai/google/google_ai/services/google_ai_text_embedding.py) | project_id, <br> region, <br> embedding_model_id | GOOGLE_AI_CLOUD_PROJECT_ID, <br> GOOGLE_AI_CLOUD_REGION, <br> GOOGLE_AI_EMBEDDING_MODEL_ID, <br> GOOGLE_AI_USE_VERTEXAI | Yes, <br> No, <br> Yes, <br> Yes (must set to true) | |
| HuggingFace | [HuggingFaceTextCompletion](../../../semantic_kernel/connectors/ai/hugging_face/services/hf_text_completion.py) | ai_model_id | N/A | Yes | |
| | [HuggingFaceTextEmbedding](../../../semantic_kernel/connectors/ai/hugging_face/services/hf_text_embedding.py) | ai_model_id | N/A | Yes | |
| NVIDIA NIM | [NvidiaChatCompletion](../../../semantic_kernel/connectors/ai/nvidia/services/nvidia_chat_completion.py) | ai_model_id, <br> api_key, <br> base_url | NVIDIA_CHAT_MODEL_ID, <br> NVIDIA_API_KEY, <br> NVIDIA_BASE_URL | Yes (default: meta/llama-3.1-8b-instruct), <br> Yes, <br> No | [NvidiaAISettings](../../../semantic_kernel/connectors/ai/nvidia/settings/nvidia_settings.py) |
@@ -249,10 +249,7 @@ def get_google_ai_chat_completion_service_and_request_settings() -> tuple[
Please refer to the Semantic Kernel Python documentation for more information:
https://learn.microsoft.com/en-us/python/api/semantic-kernel/semantic_kernel?view=semantic-kernel
"""
from semantic_kernel.connectors.ai.google.google_ai import (
GoogleAIChatCompletion,
GoogleAIChatPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.google import GoogleAIChatCompletion, GoogleAIChatPromptExecutionSettings
chat_service = GoogleAIChatCompletion(service_id=service_id)
request_settings = GoogleAIChatPromptExecutionSettings(service_id=service_id)
@@ -356,13 +353,10 @@ def get_vertex_ai_chat_completion_service_and_request_settings() -> tuple[
Please refer to the Semantic Kernel Python documentation for more information:
https://learn.microsoft.com/en-us/python/api/semantic-kernel/semantic_kernel?view=semantic-kernel
"""
from semantic_kernel.connectors.ai.google.vertex_ai import (
VertexAIChatCompletion,
VertexAIChatPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.google import GoogleAIChatCompletion, GoogleAIChatPromptExecutionSettings
chat_service = VertexAIChatCompletion(service_id=service_id)
request_settings = VertexAIChatPromptExecutionSettings(service_id=service_id)
chat_service = GoogleAIChatCompletion(service_id=service_id, use_vertexai=True)
request_settings = GoogleAIChatPromptExecutionSettings(service_id=service_id)
return chat_service, request_settings
@@ -16,7 +16,6 @@ class Services(str, Enum):
"""
OPENAI = "openai"
AZURE_OPENAI = "azure_openai"
BEDROCK = "bedrock"
GOOGLE_AI = "google_ai"
HUGGING_FACE = "huggingface"
@@ -36,7 +35,6 @@ def get_text_completion_service_and_request_settings(
# Use lambdas or functions to delay instantiation
text_services = {
Services.OPENAI: lambda: get_openai_text_completion_service_and_request_settings(),
Services.AZURE_OPENAI: lambda: get_azure_openai_text_completion_service_and_request_settings(),
Services.BEDROCK: lambda: get_bedrock_text_completion_service_and_request_settings(),
Services.GOOGLE_AI: lambda: get_google_ai_text_completion_service_and_request_settings(),
Services.HUGGING_FACE: lambda: get_hugging_face_text_completion_service_and_request_settings(),
@@ -75,32 +73,6 @@ def get_openai_text_completion_service_and_request_settings() -> tuple[
return text_service, request_settings
def get_azure_openai_text_completion_service_and_request_settings() -> tuple[
"TextCompletionClientBase", "PromptExecutionSettings"
]:
"""Return Azure OpenAI text completion service and request settings.
The service credentials can be read by 3 ways:
1. Via the constructor
2. Via the environment variables
3. Via an environment file
The request settings control the behavior of the service. The default settings are sufficient to get started.
However, you can adjust the settings to suit your needs.
Note: Some of the settings are NOT meant to be set by the user.
Please refer to the Semantic Kernel Python documentation for more information:
https://learn.microsoft.com/en-us/python/api/semantic-kernel/semantic_kernel?view=semantic-kernel
"""
from azure.identity import AzureCliCredential
from semantic_kernel.connectors.ai.open_ai import AzureTextCompletion, OpenAITextPromptExecutionSettings
text_service = AzureTextCompletion(credential=AzureCliCredential())
request_settings = OpenAITextPromptExecutionSettings()
return text_service, request_settings
def get_bedrock_text_completion_service_and_request_settings() -> tuple[
"TextCompletionClientBase", "PromptExecutionSettings"
]:
@@ -262,12 +234,9 @@ def get_vertex_ai_text_completion_service_and_request_settings() -> tuple[
Please refer to the Semantic Kernel Python documentation for more information:
https://learn.microsoft.com/en-us/python/api/semantic-kernel/semantic_kernel?view=semantic-kernel
"""
from semantic_kernel.connectors.ai.google.vertex_ai import (
VertexAITextCompletion,
VertexAITextPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.google import GoogleAITextCompletion, GoogleAITextPromptExecutionSettings
text_service = VertexAITextCompletion()
request_settings = VertexAITextPromptExecutionSettings()
text_service = GoogleAITextCompletion()
request_settings = GoogleAITextPromptExecutionSettings()
return text_service, request_settings
@@ -296,13 +296,10 @@ def get_vertex_ai_text_embedding_service_and_request_settings() -> tuple[
Please refer to the Semantic Kernel Python documentation for more information:
https://learn.microsoft.com/en-us/python/api/semantic-kernel/semantic_kernel?view=semantic-kernel
"""
from semantic_kernel.connectors.ai.google.vertex_ai import (
VertexAIEmbeddingPromptExecutionSettings,
VertexAITextEmbedding,
)
from semantic_kernel.connectors.ai.google import GoogleAIEmbeddingPromptExecutionSettings, GoogleAITextEmbedding
embedding_service = VertexAITextEmbedding()
embedding_service = GoogleAITextEmbedding()
# Note: not all models support specifying the dimensions or there may be constraints on the dimensions
request_settings = VertexAIEmbeddingPromptExecutionSettings(output_dimensionality=768)
request_settings = GoogleAIEmbeddingPromptExecutionSettings(output_dimensionality=768)
return embedding_service, request_settings
@@ -12,7 +12,6 @@ This sample shows how to perform text completion. This sample uses the following
# You can select from the following text embedding services:
# - Services.OPENAI
# - Services.AZURE_OPENAI
# - Services.BEDROCK
# - Services.GOOGLE_AI
# - Services.HUGGING_FACE
@@ -12,7 +12,6 @@ This sample uses the following component:
# You can select from the following text embedding services:
# - Services.OPENAI
# - Services.AZURE_OPENAI
# - Services.BEDROCK
# - Services.GOOGLE_AI
# - Services.HUGGING_FACE
@@ -135,16 +135,8 @@
"source": [
"from services import Service\n",
"\n",
"from samples.service_settings import ServiceSettings\n",
"\n",
"service_settings = ServiceSettings()\n",
"\n",
"# Select a service to use for this notebook (available services: OpenAI, AzureOpenAI, HuggingFace)\n",
"selectedService = (\n",
" Service.AzureOpenAI\n",
" if service_settings.global_llm_service is None\n",
" else Service(service_settings.global_llm_service.lower())\n",
")\n",
"selectedService = Service.OpenAI\n",
"print(f\"Using service type: {selectedService}\")"
]
},
@@ -128,16 +128,8 @@
"source": [
"from services import Service\n",
"\n",
"from samples.service_settings import ServiceSettings\n",
"\n",
"service_settings = ServiceSettings()\n",
"\n",
"# Select a service to use for this notebook (available services: OpenAI, AzureOpenAI, HuggingFace)\n",
"selectedService = (\n",
" Service.AzureOpenAI\n",
" if service_settings.global_llm_service is None\n",
" else Service(service_settings.global_llm_service.lower())\n",
")\n",
"selectedService = Service.OpenAI\n",
"print(f\"Using service type: {selectedService}\")"
]
},
@@ -24,7 +24,6 @@ All base clients inherit from the [`AIServiceClientBase`](../../services/ai_serv
| | [`OpenAITextToAudio`](./open_ai/services/open_ai_text_to_audio.py) |
| | [`OpenAIAudioToText`](./open_ai/services/open_ai_audio_to_text.py) |
| Azure OpenAI | [`AzureChatCompletion`](./open_ai/services/azure_chat_completion.py) |
| | [`AzureTextCompletion`](./open_ai/services/azure_text_completion.py) |
| | [`AzureTextEmbedding`](./open_ai/services/azure_text_embedding.py) |
| | [`AzureTextToImage`](./open_ai/services/azure_text_to_image.py) |
| | [`AzureTextToAudio`](./open_ai/services/azure_text_to_audio.py) |
@@ -38,9 +37,9 @@ All base clients inherit from the [`AIServiceClientBase`](../../services/ai_serv
| [Google AI](./google/README.md) | [`GoogleAIChatCompletion`](./google/google_ai/services/google_ai_chat_completion.py) |
| | [`GoogleAITextCompletion`](./google/google_ai/services/google_ai_text_completion.py) |
| | [`GoogleAITextEmbedding`](./google/google_ai/services/google_ai_text_embedding.py) |
| [Vertex AI](./google/README.md) | [`VertexAIChatCompletion`](./google/vertex_ai/services/vertex_ai_chat_completion.py) |
| | [`VertexAITextCompletion`](./google/vertex_ai/services/vertex_ai_text_completion.py) |
| | [`VertexAITextEmbedding`](./google/vertex_ai/services/vertex_ai_text_embedding.py) |
| [Vertex AI](./google/README.md) | [`GoogleAIChatCompletion`](./google/google_ai/services/google_ai_chat_completion.py) |
| | [`GoogleAITextCompletion`](./google/google_ai/services/google_ai_text_completion.py) |
| | [`GoogleAITextEmbedding`](./google/google_ai/services/google_ai_text_embedding.py) |
| HuggingFace | [`HuggingFaceTextCompletion`](./hugging_face/services/hf_text_completion.py) |
| | [`HuggingFaceTextEmbedding`](./hugging_face/services/hf_text_embedding.py) |
| Mistral AI | [`MistralAIChatCompletion`](./mistral_ai/services/mistral_ai_chat_completion.py) |
@@ -14,7 +14,7 @@ Once you have an API key, you can start using Gemini models in SK using the `goo
kernel = Kernel()
kernel.add_service(
GoogleAIChatCompletion(
gemini_model_id="gemini-1.5-flash",
gemini_model_id="gemini-2.5-flash",
api_key="...",
)
)
@@ -39,9 +39,11 @@ Once you have your project and your environment is set up, you can start using G
```Python
kernel = Kernel()
kernel.add_service(
VertexAIChatCompletion(
GoogleAIChatCompletion(
project_id="...",
gemini_model_id="gemini-1.5-flash",
region="...",
gemini_model_id="gemini-2.5-flash",
use_vertexai=True,
)
)
...
@@ -51,4 +53,4 @@ kernel.add_service(
## Why is there code that looks almost identical in the implementations on the two connectors
The two connectors have very similar implementations, including the utils files. However, they are fundamentally different as they depend on different packages from Google. Although the namings of many types are identical, they are different types.
The two connectors have very similar implementations, including the utils files. However, they are fundamentally different as they depend on different packages from Google. Although the namings of many types are identical, they are different types.
@@ -0,0 +1,21 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.connectors.ai.google.google_ai.google_ai_prompt_execution_settings import (
GoogleAIChatPromptExecutionSettings,
GoogleAIEmbeddingPromptExecutionSettings,
GoogleAIPromptExecutionSettings,
GoogleAITextPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.google.google_ai.services.google_ai_chat_completion import GoogleAIChatCompletion
from semantic_kernel.connectors.ai.google.google_ai.services.google_ai_text_completion import GoogleAITextCompletion
from semantic_kernel.connectors.ai.google.google_ai.services.google_ai_text_embedding import GoogleAITextEmbedding
__all__ = [
"GoogleAIChatCompletion",
"GoogleAIChatPromptExecutionSettings",
"GoogleAIEmbeddingPromptExecutionSettings",
"GoogleAIPromptExecutionSettings",
"GoogleAITextCompletion",
"GoogleAITextEmbedding",
"GoogleAITextPromptExecutionSettings",
]
@@ -1,15 +1,9 @@
# Copyright (c) Microsoft. All rights reserved.
import sys
from typing import Annotated, Any, Literal
from pydantic import Field
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
@@ -50,19 +44,6 @@ class GoogleAIChatPromptExecutionSettings(GoogleAIPromptExecutionSettings):
),
] = None
@override
def prepare_settings_dict(self, **kwargs) -> dict[str, Any]:
"""Prepare the settings as a dictionary for sending to the AI service.
This method removes the tools and tool_choice keys from the settings dictionary, as
the Google AI service mandates these two settings to be sent as separate parameters.
"""
settings_dict = super().prepare_settings_dict(**kwargs)
settings_dict.pop("tools", None)
settings_dict.pop("tool_config", None)
return settings_dict
class GoogleAIEmbeddingPromptExecutionSettings(PromptExecutionSettings):
"""Google AI Embedding Prompt Execution Settings."""
@@ -28,10 +28,19 @@ class GoogleAISettings(KernelBaseSettings):
- api_key: SecretStr - The API key for the Google AI service deployment.
This value can be found in the Google AI service deployment.
(Env var GOOGLE_AI_API_KEY)
- cloud_project_id: str - The Google Cloud project ID.
(Env var GOOGLE_AI_CLOUD_PROJECT_ID)
- cloud_region: str - The Google Cloud region.
(Env var GOOGLE_AI_CLOUD_REGION)
- use_vertexai: bool - Whether to use Vertex AI. If true, cloud_project_id and cloud_region must be provided.
(Env var GOOGLE_AI_USE_VERTEXAI)
"""
env_prefix: ClassVar[str] = "GOOGLE_AI_"
gemini_model_id: str | None = None
embedding_model_id: str | None = None
api_key: SecretStr
api_key: SecretStr | None = None
cloud_project_id: str | None = None
cloud_region: str | None = None
use_vertexai: bool = False
@@ -3,6 +3,8 @@
from abc import ABC
from typing import ClassVar
from google.genai import Client
from semantic_kernel.connectors.ai.google.google_ai.google_ai_settings import GoogleAISettings
from semantic_kernel.kernel_pydantic import KernelBaseModel
@@ -13,3 +15,5 @@ class GoogleAIBase(KernelBaseModel, ABC):
MODEL_PROVIDER_NAME: ClassVar[str] = "googleai"
service_settings: GoogleAISettings
client: Client | None = None
@@ -5,15 +5,8 @@ import sys
from collections.abc import AsyncGenerator, Callable
from typing import TYPE_CHECKING, Any, ClassVar
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
import google.generativeai as genai
from google.generativeai import GenerativeModel
from google.generativeai.protos import Candidate, Content
from google.generativeai.types import AsyncGenerateContentResponse, GenerateContentResponse, GenerationConfig
from google.genai import Client
from google.genai.types import Candidate, Content, GenerateContentConfigDict, GenerateContentResponse
from pydantic import ValidationError
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase
@@ -54,6 +47,11 @@ from semantic_kernel.utils.telemetry.model_diagnostics.decorators import (
trace_streaming_chat_completion,
)
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
if TYPE_CHECKING:
from semantic_kernel.connectors.ai.function_call_choice_configuration import FunctionCallChoiceConfiguration
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
@@ -70,7 +68,11 @@ class GoogleAIChatCompletion(GoogleAIBase, ChatCompletionClientBase):
self,
gemini_model_id: str | None = None,
api_key: str | None = None,
project_id: str | None = None,
region: str | None = None,
use_vertexai: bool | None = None,
service_id: str | None = None,
client: Client | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> None:
@@ -80,11 +82,18 @@ class GoogleAIChatCompletion(GoogleAIBase, ChatCompletionClientBase):
The following environment variables are used:
- GOOGLE_AI_GEMINI_MODEL_ID
- GOOGLE_AI_API_KEY
- GOOGLE_AI_CLOUD_PROJECT_ID
- GOOGLE_AI_CLOUD_REGION
- GOOGLE_AI_USE_VERTEXAI
Args:
gemini_model_id (str | None): The Gemini model ID. (Optional)
api_key (str | None): The API key. (Optional)
project_id (str | None): The Google Cloud project ID. (Optional)
region (str | None): The Google Cloud region. (Optional)
use_vertexai (bool | None): Whether to use Vertex AI. (Optional)
service_id (str | None): The service ID. (Optional)
client (Client | None): The Google AI client to use for break glass scenarios. (Optional)
env_file_path (str | None): The path to the .env file. (Optional)
env_file_encoding (str | None): The encoding of the .env file. (Optional)
@@ -95,18 +104,29 @@ class GoogleAIChatCompletion(GoogleAIBase, ChatCompletionClientBase):
google_ai_settings = GoogleAISettings(
gemini_model_id=gemini_model_id,
api_key=api_key,
cloud_project_id=project_id,
cloud_region=region,
use_vertexai=use_vertexai,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
except ValidationError as e:
raise ServiceInitializationError(f"Failed to validate Google AI settings: {e}") from e
if not google_ai_settings.gemini_model_id:
raise ServiceInitializationError("The Google AI Gemini model ID is required.")
if not client:
if google_ai_settings.use_vertexai and not google_ai_settings.cloud_project_id:
raise ServiceInitializationError("Project ID must be provided when use_vertexai is True.")
if not google_ai_settings.api_key:
raise ServiceInitializationError("The API key is required when use_vertexai is False.")
super().__init__(
ai_model_id=google_ai_settings.gemini_model_id,
service_id=service_id or google_ai_settings.gemini_model_id,
service_settings=google_ai_settings,
client=client,
)
# region Overriding base class methods
@@ -127,24 +147,35 @@ class GoogleAIChatCompletion(GoogleAIBase, ChatCompletionClientBase):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, GoogleAIChatPromptExecutionSettings) # nosec
genai.configure(api_key=self.service_settings.api_key.get_secret_value())
if not self.service_settings.gemini_model_id:
raise ServiceInitializationError("The Google AI Gemini model ID is required.")
model = GenerativeModel(
model_name=self.service_settings.gemini_model_id,
system_instruction=filter_system_message(chat_history),
)
collapse_function_call_results_in_chat_history(chat_history)
response: AsyncGenerateContentResponse = await model.generate_content_async(
contents=self._prepare_chat_history_for_request(chat_history),
generation_config=GenerationConfig(**settings.prepare_settings_dict()),
tools=settings.tools,
tool_config=settings.tool_config, # type: ignore
)
async def _generate_content(client: Client) -> GenerateContentResponse:
return await client.aio.models.generate_content(
model=self.service_settings.gemini_model_id, # type: ignore[arg-type]
contents=self._prepare_chat_history_for_request(chat_history), # type: ignore[arg-type]
config=GenerateContentConfigDict(
system_instruction=filter_system_message(chat_history),
**settings.prepare_settings_dict(), # type: ignore[typeddict-item]
),
)
return [self._create_chat_message_content(response, candidate) for candidate in response.candidates]
if self.client:
response: GenerateContentResponse = await _generate_content(self.client)
elif self.service_settings.use_vertexai:
with Client(
vertexai=True,
project=self.service_settings.cloud_project_id,
location=self.service_settings.cloud_region,
) as client:
response: GenerateContentResponse = await _generate_content(client) # type: ignore[no-redef]
else:
with Client(api_key=self.service_settings.api_key.get_secret_value()) as client: # type: ignore[union-attr]
response: GenerateContentResponse = await _generate_content(client) # type: ignore[no-redef]
return [self._create_chat_message_content(response, candidate) for candidate in response.candidates] # type: ignore
@override
@trace_streaming_chat_completion(GoogleAIBase.MODEL_PROVIDER_NAME)
@@ -158,29 +189,46 @@ class GoogleAIChatCompletion(GoogleAIBase, ChatCompletionClientBase):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, GoogleAIChatPromptExecutionSettings) # nosec
genai.configure(api_key=self.service_settings.api_key.get_secret_value())
if not self.service_settings.gemini_model_id:
raise ServiceInitializationError("The Google AI Gemini model ID is required.")
model = GenerativeModel(
model_name=self.service_settings.gemini_model_id,
system_instruction=filter_system_message(chat_history),
)
collapse_function_call_results_in_chat_history(chat_history)
response: AsyncGenerateContentResponse = await model.generate_content_async(
contents=self._prepare_chat_history_for_request(chat_history),
generation_config=GenerationConfig(**settings.prepare_settings_dict()),
tools=settings.tools,
tool_config=settings.tool_config, # type: ignore
stream=True,
)
async def _generate_content_stream(client: Client) -> AsyncGenerator[GenerateContentResponse, Any]:
async for chunk in await client.aio.models.generate_content_stream(
model=self.service_settings.gemini_model_id, # type: ignore[arg-type]
contents=self._prepare_chat_history_for_request(chat_history), # type: ignore[arg-type]
config=GenerateContentConfigDict(
system_instruction=filter_system_message(chat_history),
**settings.prepare_settings_dict(), # type: ignore[typeddict-item]
),
):
yield chunk
async for chunk in response:
yield [
self._create_streaming_chat_message_content(chunk, candidate, function_invoke_attempt)
for candidate in chunk.candidates
]
if self.client:
async for chunk in _generate_content_stream(self.client):
yield [
self._create_streaming_chat_message_content(chunk, candidate, function_invoke_attempt)
for candidate in chunk.candidates # type: ignore
]
elif self.service_settings.use_vertexai:
with Client(
vertexai=True,
project=self.service_settings.cloud_project_id,
location=self.service_settings.cloud_region,
) as client:
async for chunk in _generate_content_stream(client):
yield [
self._create_streaming_chat_message_content(chunk, candidate, function_invoke_attempt)
for candidate in chunk.candidates # type: ignore
]
else:
with Client(api_key=self.service_settings.api_key.get_secret_value()) as client: # type: ignore[union-attr]
async for chunk in _generate_content_stream(client):
yield [
self._create_streaming_chat_message_content(chunk, candidate, function_invoke_attempt)
for candidate in chunk.candidates # type: ignore
]
@override
def _verify_function_choice_settings(self, settings: "PromptExecutionSettings") -> None:
@@ -217,7 +265,7 @@ class GoogleAIChatCompletion(GoogleAIBase, ChatCompletionClientBase):
for message in chat_history.messages:
if message.role == AuthorRole.SYSTEM:
# Skip system messages since they are not part of the chat request.
# System message will be provided as system_instruction in the model.
# System message will be provided as system_instruction in the config.
continue
if message.role == AuthorRole.USER:
chat_request_messages.append(Content(role="user", parts=format_user_message(message)))
@@ -233,7 +281,7 @@ class GoogleAIChatCompletion(GoogleAIBase, ChatCompletionClientBase):
# region Non-streaming
def _create_chat_message_content(
self, response: AsyncGenerateContentResponse, candidate: Candidate
self, response: GenerateContentResponse, candidate: Candidate
) -> ChatMessageContent:
"""Create a chat message content object.
@@ -250,19 +298,20 @@ class GoogleAIChatCompletion(GoogleAIBase, ChatCompletionClientBase):
response_metadata.update(self._get_metadata_from_candidate(candidate))
items: list[CMC_ITEM_TYPES] = []
for idx, part in enumerate(candidate.content.parts):
if part.text:
items.append(TextContent(text=part.text, inner_content=response, metadata=response_metadata))
elif part.function_call:
items.append(
FunctionCallContent(
id=f"{part.function_call.name}_{idx!s}",
name=format_gemini_function_name_to_kernel_function_fully_qualified_name(
part.function_call.name
),
arguments={k: v for k, v in part.function_call.args.items()},
if candidate.content and candidate.content.parts:
for idx, part in enumerate(candidate.content.parts):
if part.text:
items.append(TextContent(text=part.text, inner_content=response, metadata=response_metadata))
elif part.function_call:
items.append(
FunctionCallContent(
id=f"{part.function_call.name}_{idx!s}",
name=format_gemini_function_name_to_kernel_function_fully_qualified_name(
part.function_call.name # type: ignore[arg-type]
),
arguments={k: v for k, v in part.function_call.args.items()}, # type: ignore
)
)
)
return ChatMessageContent(
ai_model_id=self.ai_model_id,
@@ -299,31 +348,32 @@ class GoogleAIChatCompletion(GoogleAIBase, ChatCompletionClientBase):
response_metadata.update(self._get_metadata_from_candidate(candidate))
items: list[STREAMING_ITEM_TYPES] = []
for idx, part in enumerate(candidate.content.parts):
if part.text:
items.append(
StreamingTextContent(
choice_index=candidate.index,
text=part.text,
inner_content=chunk,
metadata=response_metadata,
if candidate.content and candidate.content.parts:
for idx, part in enumerate(candidate.content.parts):
if part.text:
items.append(
StreamingTextContent(
choice_index=candidate.index or 0,
text=part.text,
inner_content=chunk,
metadata=response_metadata,
)
)
)
elif part.function_call:
items.append(
FunctionCallContent(
id=f"{part.function_call.name}_{idx!s}",
name=format_gemini_function_name_to_kernel_function_fully_qualified_name(
part.function_call.name
),
arguments={k: v for k, v in part.function_call.args.items()},
elif part.function_call:
items.append(
FunctionCallContent(
id=f"{part.function_call.name}_{idx!s}",
name=format_gemini_function_name_to_kernel_function_fully_qualified_name(
part.function_call.name # type: ignore[arg-type]
),
arguments={k: v for k, v in part.function_call.args.items()}, # type: ignore
)
)
)
return StreamingChatMessageContent(
ai_model_id=self.ai_model_id,
role=AuthorRole.ASSISTANT,
choice_index=candidate.index,
choice_index=candidate.index or 0,
items=items,
inner_content=chunk,
finish_reason=finish_reason,
@@ -333,9 +383,7 @@ class GoogleAIChatCompletion(GoogleAIBase, ChatCompletionClientBase):
# endregion
def _get_metadata_from_response(
self, response: AsyncGenerateContentResponse | GenerateContentResponse
) -> dict[str, Any]:
def _get_metadata_from_response(self, response: GenerateContentResponse) -> dict[str, Any]:
"""Get metadata from the response.
Args:
@@ -347,8 +395,8 @@ class GoogleAIChatCompletion(GoogleAIBase, ChatCompletionClientBase):
return {
"prompt_feedback": response.prompt_feedback,
"usage": CompletionUsage(
prompt_tokens=response.usage_metadata.prompt_token_count,
completion_tokens=response.usage_metadata.candidates_token_count,
prompt_tokens=response.usage_metadata.prompt_token_count if response.usage_metadata else None,
completion_tokens=response.usage_metadata.candidates_token_count if response.usage_metadata else None,
),
}
@@ -4,10 +4,8 @@ import sys
from collections.abc import AsyncGenerator
from typing import TYPE_CHECKING, Any
import google.generativeai as genai
from google.generativeai import GenerativeModel
from google.generativeai.protos import Candidate
from google.generativeai.types import AsyncGenerateContentResponse, GenerateContentResponse, GenerationConfig
from google.genai import Client
from google.genai.types import Candidate, GenerateContentConfigDict, GenerateContentResponse
from pydantic import ValidationError
from semantic_kernel.connectors.ai.completion_usage import CompletionUsage
@@ -41,7 +39,11 @@ class GoogleAITextCompletion(GoogleAIBase, TextCompletionClientBase):
self,
gemini_model_id: str | None = None,
api_key: str | None = None,
project_id: str | None = None,
region: str | None = None,
use_vertexai: bool | None = None,
service_id: str | None = None,
client: Client | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> None:
@@ -51,11 +53,18 @@ class GoogleAITextCompletion(GoogleAIBase, TextCompletionClientBase):
The following environment variables are used:
- GOOGLE_AI_GEMINI_MODEL_ID
- GOOGLE_AI_API_KEY
- GOOGLE_AI_CLOUD_PROJECT_ID
- GOOGLE_AI_CLOUD_REGION
- GOOGLE_AI_USE_VERTEXAI
Args:
gemini_model_id (str | None): The Gemini model ID. (Optional)
api_key (str | None): The API key. (Optional)
project_id (str | None): The Google Cloud project ID. (Optional)
region (str | None): The Google Cloud region. (Optional)
use_vertexai (bool | None): Whether to use Vertex AI. (Optional)
service_id (str | None): The service ID. (Optional)
client (Client | None): The Google AI Client to use for break glass scenarios. (Optional)
env_file_path (str | None): The path to the .env file. (Optional)
env_file_encoding (str | None): The encoding of the .env file. (Optional)
@@ -66,18 +75,29 @@ class GoogleAITextCompletion(GoogleAIBase, TextCompletionClientBase):
google_ai_settings = GoogleAISettings(
gemini_model_id=gemini_model_id,
api_key=api_key,
cloud_project_id=project_id,
cloud_region=region,
use_vertexai=use_vertexai,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
except ValidationError as e:
raise ServiceInitializationError(f"Failed to validate Google AI settings: {e}") from e
if not google_ai_settings.gemini_model_id:
raise ServiceInitializationError("The Google AI Gemini model ID is required.")
if not client:
if google_ai_settings.use_vertexai and not google_ai_settings.cloud_project_id:
raise ServiceInitializationError("Project ID must be provided when use_vertexai is True.")
if not google_ai_settings.api_key:
raise ServiceInitializationError("The API key is required when use_vertexai is False.")
super().__init__(
ai_model_id=google_ai_settings.gemini_model_id,
service_id=service_id or google_ai_settings.gemini_model_id,
service_settings=google_ai_settings,
client=client,
)
# region Overriding base class methods
@@ -98,19 +118,30 @@ class GoogleAITextCompletion(GoogleAIBase, TextCompletionClientBase):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, GoogleAITextPromptExecutionSettings) # nosec
genai.configure(api_key=self.service_settings.api_key.get_secret_value())
if not self.service_settings.gemini_model_id:
raise ServiceInitializationError("The Google AI Gemini model ID is required.")
model = GenerativeModel(
model_name=self.service_settings.gemini_model_id,
)
response: AsyncGenerateContentResponse = await model.generate_content_async(
contents=prompt,
generation_config=GenerationConfig(**settings.prepare_settings_dict()),
)
async def _generate_content(client: Client) -> GenerateContentResponse:
return await client.aio.models.generate_content(
model=self.service_settings.gemini_model_id, # type: ignore[arg-type]
contents=prompt,
config=GenerateContentConfigDict(**settings.prepare_settings_dict()), # type: ignore[typeddict-item]
)
return [self._create_text_content(response, candidate) for candidate in response.candidates]
if self.client:
response: GenerateContentResponse = await _generate_content(self.client)
elif self.service_settings.use_vertexai:
with Client(
vertexai=True,
project=self.service_settings.cloud_project_id,
location=self.service_settings.cloud_region,
) as client:
response: GenerateContentResponse = await _generate_content(client) # type: ignore[no-redef]
else:
with Client(api_key=self.service_settings.api_key.get_secret_value()) as client: # type: ignore[union-attr]
response: GenerateContentResponse = await _generate_content(client) # type: ignore[no-redef]
return [self._create_text_content(response, candidate) for candidate in response.candidates] # type: ignore
@override
@trace_streaming_text_completion(GoogleAIBase.MODEL_PROVIDER_NAME)
@@ -123,25 +154,36 @@ class GoogleAITextCompletion(GoogleAIBase, TextCompletionClientBase):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, GoogleAITextPromptExecutionSettings) # nosec
genai.configure(api_key=self.service_settings.api_key.get_secret_value())
if not self.service_settings.gemini_model_id:
raise ServiceInitializationError("The Google AI Gemini model ID is required.")
model = GenerativeModel(
model_name=self.service_settings.gemini_model_id,
)
response: AsyncGenerateContentResponse = await model.generate_content_async(
contents=prompt,
generation_config=GenerationConfig(**settings.prepare_settings_dict()),
stream=True,
)
async def _generate_content_stream(client: Client) -> AsyncGenerator[GenerateContentResponse, Any]:
async for chunk in await client.aio.models.generate_content_stream(
model=self.service_settings.gemini_model_id, # type: ignore[arg-type]
contents=prompt,
config=GenerateContentConfigDict(**settings.prepare_settings_dict()), # type: ignore[typeddict-item]
):
yield chunk
async for chunk in response:
yield [self._create_streaming_text_content(chunk, candidate) for candidate in chunk.candidates]
if self.client:
async for chunk in _generate_content_stream(self.client):
yield [self._create_streaming_text_content(chunk, candidate) for candidate in chunk.candidates] # type: ignore
elif self.service_settings.use_vertexai:
with Client(
vertexai=True,
project=self.service_settings.cloud_project_id,
location=self.service_settings.cloud_region,
) as client:
async for chunk in _generate_content_stream(client):
yield [self._create_streaming_text_content(chunk, candidate) for candidate in chunk.candidates] # type: ignore
else:
with Client(api_key=self.service_settings.api_key.get_secret_value()) as client: # type: ignore[union-attr]
async for chunk in _generate_content_stream(client):
yield [self._create_streaming_text_content(chunk, candidate) for candidate in chunk.candidates] # type: ignore
# endregion
def _create_text_content(self, response: AsyncGenerateContentResponse, candidate: Candidate) -> TextContent:
def _create_text_content(self, response: GenerateContentResponse, candidate: Candidate) -> TextContent:
"""Create a text content object.
Args:
@@ -156,7 +198,7 @@ class GoogleAITextCompletion(GoogleAIBase, TextCompletionClientBase):
return TextContent(
ai_model_id=self.ai_model_id,
text=candidate.content.parts[0].text,
text=candidate.content.parts[0].text or "" if candidate.content and candidate.content.parts else "",
inner_content=response,
metadata=response_metadata,
)
@@ -178,16 +220,13 @@ class GoogleAITextCompletion(GoogleAIBase, TextCompletionClientBase):
return StreamingTextContent(
ai_model_id=self.ai_model_id,
choice_index=candidate.index,
text=candidate.content.parts[0].text,
choice_index=candidate.index or 0,
text=candidate.content.parts[0].text or "" if candidate.content and candidate.content.parts else "",
inner_content=chunk,
metadata=response_metadata,
)
def _get_metadata_from_response(
self,
response: AsyncGenerateContentResponse | GenerateContentResponse,
) -> dict[str, Any]:
def _get_metadata_from_response(self, response: GenerateContentResponse) -> dict[str, Any]:
"""Get metadata from the response.
Args:
@@ -199,8 +238,8 @@ class GoogleAITextCompletion(GoogleAIBase, TextCompletionClientBase):
return {
"prompt_feedback": response.prompt_feedback,
"usage": CompletionUsage(
prompt_tokens=response.usage_metadata.prompt_token_count,
completion_tokens=response.usage_metadata.candidates_token_count,
prompt_tokens=response.usage_metadata.prompt_token_count if response.usage_metadata else None,
completion_tokens=response.usage_metadata.candidates_token_count if response.usage_metadata else None,
),
}
@@ -3,8 +3,8 @@
import sys
from typing import Any
import google.generativeai as genai
from google.generativeai.types.text_types import BatchEmbeddingDict
from google.genai import Client
from google.genai.types import EmbedContentConfigDict, EmbedContentResponse
from numpy import array, ndarray
from pydantic import ValidationError
@@ -30,7 +30,11 @@ class GoogleAITextEmbedding(GoogleAIBase, EmbeddingGeneratorBase):
self,
embedding_model_id: str | None = None,
api_key: str | None = None,
project_id: str | None = None,
region: str | None = None,
use_vertexai: bool | None = None,
service_id: str | None = None,
client: Client | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> None:
@@ -40,11 +44,18 @@ class GoogleAITextEmbedding(GoogleAIBase, EmbeddingGeneratorBase):
The following environment variables are used:
- GOOGLE_AI_EMBEDDING_MODEL_ID
- GOOGLE_AI_API_KEY
- GOOGLE_AI_CLOUD_PROJECT_ID
- GOOGLE_AI_CLOUD_REGION
- GOOGLE_AI_USE_VERTEXAI
Args:
embedding_model_id (str | None): The embedding model ID. (Optional)
api_key (str | None): The API key. (Optional)
project_id (str | None): The Google Cloud project ID. (Optional)
region (str | None): The Google Cloud region. (Optional)
use_vertexai (bool | None): Whether to use Vertex AI. (Optional)
service_id (str | None): The service ID. (Optional)
client (Client | None): The Google AI Client to use for break glass scenarios. (Optional)
env_file_path (str | None): The path to the .env file. (Optional)
env_file_encoding (str | None): The encoding of the .env file. (Optional)
@@ -55,18 +66,29 @@ class GoogleAITextEmbedding(GoogleAIBase, EmbeddingGeneratorBase):
google_ai_settings = GoogleAISettings(
embedding_model_id=embedding_model_id,
api_key=api_key,
cloud_project_id=project_id,
cloud_region=region,
use_vertexai=use_vertexai,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
except ValidationError as e:
raise ServiceInitializationError(f"Failed to validate Google AI settings: {e}") from e
if not google_ai_settings.embedding_model_id:
raise ServiceInitializationError("The Google AI embedding model ID is required.")
if not client:
if google_ai_settings.use_vertexai and not google_ai_settings.cloud_project_id:
raise ServiceInitializationError("Project ID must be provided when use_vertexai is True.")
if not google_ai_settings.api_key:
raise ServiceInitializationError("The API key is required when use_vertexai is False.")
super().__init__(
ai_model_id=google_ai_settings.embedding_model_id,
service_id=service_id or google_ai_settings.embedding_model_id,
service_settings=google_ai_settings,
client=client,
)
@override
@@ -92,16 +114,30 @@ class GoogleAITextEmbedding(GoogleAIBase, EmbeddingGeneratorBase):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, GoogleAIEmbeddingPromptExecutionSettings) # nosec
genai.configure(api_key=self.service_settings.api_key.get_secret_value())
if not self.service_settings.embedding_model_id:
raise ServiceInitializationError("The Google AI embedding model ID is required.")
response: BatchEmbeddingDict = await genai.embed_content_async( # type: ignore
model=self.service_settings.embedding_model_id,
content=texts,
**settings.prepare_settings_dict(),
)
return response["embedding"]
async def _embed_content(client: Client) -> EmbedContentResponse:
return await client.aio.models.embed_content(
model=self.service_settings.embedding_model_id, # type: ignore[arg-type]
contents=texts, # type: ignore[arg-type]
config=EmbedContentConfigDict(output_dimensionality=settings.output_dimensionality),
)
if self.client:
response: EmbedContentResponse = await _embed_content(self.client)
elif self.service_settings.use_vertexai:
with Client(
vertexai=True,
project=self.service_settings.cloud_project_id,
location=self.service_settings.cloud_region,
) as client:
response: EmbedContentResponse = await _embed_content(client) # type: ignore[no-redef]
else:
with Client(api_key=self.service_settings.api_key.get_secret_value()) as client: # type: ignore[union-attr]
response: EmbedContentResponse = await _embed_content(client) # type: ignore[no-redef]
return [embedding.values for embedding in response.embeddings] # type: ignore
@override
def get_prompt_execution_settings_class(
@@ -4,7 +4,7 @@ import json
import logging
from typing import TYPE_CHECKING, Any
from google.generativeai.protos import Blob, Candidate, FunctionCall, FunctionResponse, Part
from google.genai.types import FinishReason, Part
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceType
from semantic_kernel.connectors.ai.google.google_ai.google_ai_prompt_execution_settings import (
@@ -31,19 +31,22 @@ logger: logging.Logger = logging.getLogger(__name__)
def finish_reason_from_google_ai_to_semantic_kernel(
finish_reason: Candidate.FinishReason,
finish_reason: FinishReason | None,
) -> SemanticKernelFinishReason | None:
"""Convert a Google AI FinishReason to a Semantic Kernel FinishReason.
This is best effort and may not cover all cases as the enums are not identical.
"""
if finish_reason == Candidate.FinishReason.STOP:
if finish_reason is None:
return None
if finish_reason == FinishReason.STOP:
return SemanticKernelFinishReason.STOP
if finish_reason == Candidate.FinishReason.MAX_TOKENS:
if finish_reason == FinishReason.MAX_TOKENS:
return SemanticKernelFinishReason.LENGTH
if finish_reason == Candidate.FinishReason.SAFETY:
if finish_reason == FinishReason.SAFETY:
return SemanticKernelFinishReason.CONTENT_FILTER
return None
@@ -61,7 +64,7 @@ def format_user_message(message: ChatMessageContent) -> list[Part]:
parts: list[Part] = []
for item in message.items:
if isinstance(item, TextContent):
parts.append(Part(text=message.content))
parts.append(Part.from_text(text=item.text))
elif isinstance(item, ImageContent):
parts.append(_create_image_part(item))
else:
@@ -86,22 +89,19 @@ def format_assistant_message(message: ChatMessageContent) -> list[Part]:
for item in message.items:
if isinstance(item, TextContent):
if item.text:
parts.append(Part(text=item.text))
parts.append(Part.from_text(text=item.text))
elif isinstance(item, FunctionCallContent):
parts.append(
Part(
function_call=FunctionCall(
name=item.name,
# Convert the arguments to a dictionary if it is a string
args=json.loads(item.arguments) if isinstance(item.arguments, str) else item.arguments,
)
Part.from_function_call(
name=item.name, # type: ignore[arg-type]
args=json.loads(item.arguments) if isinstance(item.arguments, str) else item.arguments, # type: ignore[arg-type]
)
)
elif isinstance(item, ImageContent):
parts.append(_create_image_part(item))
else:
raise ServiceInvalidRequestError(
"Unsupported item type in Assistant message while formatting chat history for Vertex AI"
"Unsupported item type in Assistant message while formatting chat history for Google AI"
f" Inference: {type(item)}"
)
@@ -122,14 +122,11 @@ def format_tool_message(message: ChatMessageContent) -> list[Part]:
if isinstance(item, FunctionResultContent):
gemini_function_name = item.custom_fully_qualified_name(GEMINI_FUNCTION_NAME_SEPARATOR)
parts.append(
Part(
function_response=FunctionResponse(
name=gemini_function_name,
response={
"name": gemini_function_name,
"content": str(item.result),
},
)
Part.from_function_response(
name=gemini_function_name,
response={
"content": str(item.result),
},
)
)
@@ -177,7 +174,7 @@ def update_settings_from_function_choice_configuration(
def _create_image_part(image_content: ImageContent) -> Part:
if image_content.data_uri:
return Part(inline_data=Blob(mime_type=image_content.mime_type, data=image_content.data))
return Part.from_bytes(data=image_content.data, mime_type=image_content.mime_type) # type: ignore[arg-type]
# The Google AI API doesn't support images from arbitrary URIs:
# https://github.com/google-gemini/generative-ai-python/issues/357
@@ -1,5 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
import warnings
from semantic_kernel.connectors.ai.google.vertex_ai.services.vertex_ai_chat_completion import VertexAIChatCompletion
from semantic_kernel.connectors.ai.google.vertex_ai.services.vertex_ai_text_completion import VertexAITextCompletion
from semantic_kernel.connectors.ai.google.vertex_ai.services.vertex_ai_text_embedding import VertexAITextEmbedding
@@ -10,6 +12,14 @@ from semantic_kernel.connectors.ai.google.vertex_ai.vertex_ai_prompt_execution_s
VertexAITextPromptExecutionSettings,
)
# Deprecation warning for the entire Vertex AI package
warnings.warn(
"The `semantic_kernel.connectors.ai.google.vertex_ai` package is deprecated and will be removed after 01/01/2026. "
"Please use `semantic_kernel.connectors.ai.google` instead for Google AI services.",
DeprecationWarning,
stacklevel=2,
)
__all__ = [
"VertexAIChatCompletion",
"VertexAIChatPromptExecutionSettings",
@@ -0,0 +1,11 @@
# Copyright (c) Microsoft. All rights reserved.
import warnings
# Deprecation warning for Vertex AI services
warnings.warn(
"The semantic_kernel.connectors.ai.google.vertex_ai module is deprecated and will be removed after 01/01/2026. "
"Please use semantic_kernel.connectors.ai.google instead for Google AI services.",
DeprecationWarning,
stacklevel=2,
)
@@ -62,7 +62,7 @@ def format_user_message(message: ChatMessageContent) -> list[Part]:
parts: list[Part] = []
for item in message.items:
if isinstance(item, TextContent):
parts.append(Part.from_text(message.content))
parts.append(Part.from_text(item.text))
elif isinstance(item, ImageContent):
parts.append(_create_image_part(item))
else:
@@ -3,10 +3,13 @@
from abc import ABC
from typing import ClassVar
from typing_extensions import deprecated
from semantic_kernel.connectors.ai.google.vertex_ai.vertex_ai_settings import VertexAISettings
from semantic_kernel.kernel_pydantic import KernelBaseModel
@deprecated("VertexAIBase is deprecated and will be removed after 01/01/2026. Use google_ai connectors instead.")
class VertexAIBase(KernelBaseModel, ABC):
"""Vertex AI Service."""
@@ -4,13 +4,9 @@ import sys
from collections.abc import AsyncGenerator, AsyncIterable, Callable
from typing import TYPE_CHECKING, Any, ClassVar
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
import vertexai
from pydantic import ValidationError
from typing_extensions import deprecated
from vertexai.generative_models import Candidate, Content, GenerationResponse, GenerativeModel
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase
@@ -51,11 +47,20 @@ from semantic_kernel.utils.telemetry.model_diagnostics.decorators import (
trace_streaming_chat_completion,
)
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
if TYPE_CHECKING:
from semantic_kernel.connectors.ai.function_call_choice_configuration import FunctionCallChoiceConfiguration
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
@deprecated(
"VertexAIChatCompletion is deprecated and will be removed after 01/01/2026. "
"Use `semantic_kernel.connectors.ai.google.GoogleAIChatCompletion` connectors instead."
)
class VertexAIChatCompletion(VertexAIBase, ChatCompletionClientBase):
"""Google Vertex AI Chat Completion Service."""
@@ -7,6 +7,7 @@ from typing import Any
import vertexai
from pydantic import ValidationError
from typing_extensions import deprecated
from vertexai.generative_models import Candidate, GenerationResponse, GenerativeModel
from semantic_kernel.connectors.ai.completion_usage import CompletionUsage
@@ -31,6 +32,10 @@ else:
from typing_extensions import override # pragma: no cover
@deprecated(
"VertexAITextCompletion is deprecated and will be removed after 01/01/2026. "
"Use `semantic_kernel.connectors.ai.google.GoogleAITextCompletion` connectors instead."
)
class VertexAITextCompletion(VertexAIBase, TextCompletionClientBase):
"""Vertex AI Text Completion Client."""
@@ -22,7 +22,13 @@ if sys.version_info >= (3, 12):
else:
from typing_extensions import override # pragma: no cover
from typing_extensions import deprecated
@deprecated(
"VertexAITextEmbedding is deprecated and will be removed after 01/01/2026. "
"Use `semantic_kernel.connectors.ai.google.GoogleAITextEmbedding` connectors instead."
)
class VertexAITextEmbedding(VertexAIBase, EmbeddingGeneratorBase):
"""Vertex AI Text Embedding Service."""
@@ -9,11 +9,16 @@ else:
from typing_extensions import override # pragma: no cover
from pydantic import Field
from typing_extensions import deprecated
from vertexai.generative_models import Tool, ToolConfig
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
@deprecated(
"VertexAIPromptExecutionSettings is deprecated and will be removed after 01/01/2026. "
"Use google_ai connectors instead."
)
class VertexAIPromptExecutionSettings(PromptExecutionSettings):
"""Vertex AI Prompt Execution Settings."""
@@ -27,12 +32,20 @@ class VertexAIPromptExecutionSettings(PromptExecutionSettings):
top_k: int | None = None
@deprecated(
"VertexAITextPromptExecutionSettings is deprecated and will be removed after 01/01/2026. "
"Use google_ai connectors instead."
)
class VertexAITextPromptExecutionSettings(VertexAIPromptExecutionSettings):
"""Vertex AI Text Prompt Execution Settings."""
pass
@deprecated(
"VertexAIChatPromptExecutionSettings is deprecated and will be removed after 01/01/2026. "
"Use google_ai connectors instead."
)
class VertexAIChatPromptExecutionSettings(VertexAIPromptExecutionSettings):
"""Vertex AI Chat Prompt Execution Settings."""
@@ -65,6 +78,10 @@ class VertexAIChatPromptExecutionSettings(VertexAIPromptExecutionSettings):
return settings_dict
@deprecated(
"VertexAIEmbeddingPromptExecutionSettings is deprecated and will be removed after 01/01/2026. "
"Use google_ai connectors instead."
)
class VertexAIEmbeddingPromptExecutionSettings(PromptExecutionSettings):
"""Google AI Embedding Prompt Execution Settings."""
@@ -2,9 +2,12 @@
from typing import ClassVar
from typing_extensions import deprecated
from semantic_kernel.kernel_pydantic import KernelBaseSettings
@deprecated("VertexAISettings is deprecated and will be removed after 01/01/2026. Use google_ai connectors instead.")
class VertexAISettings(KernelBaseSettings):
"""Vertex AI settings.
@@ -1,6 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
import logging
import sys
from collections.abc import Mapping
from typing import Any
@@ -15,9 +16,20 @@ from semantic_kernel.connectors.ai.open_ai.services.open_ai_text_completion_base
from semantic_kernel.connectors.ai.open_ai.settings.azure_open_ai_settings import AzureOpenAISettings
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError
if sys.version_info >= (3, 13):
from warnings import deprecated
else:
from typing_extensions import deprecated
logger: logging.Logger = logging.getLogger(__name__)
@deprecated(
"The AzureTextCompletion class is deprecated and will be removed after 01/01/2026. "
"There won't be a replacement because all text completion models on Azure OpenAI "
"have retired. Please migrate to chat completion models before the deprecation date."
)
class AzureTextCompletion(AzureOpenAIConfigBase, OpenAITextCompletionBase):
"""Azure Text Completion class."""
@@ -238,7 +238,10 @@ class TestChatCompletionAgentIntegration:
"chat_completion_agent",
[
("azure", {"enable_kernel_function": True}),
("openai", {"enable_kernel_function": True}),
pytest.param(
("openai", {"enable_kernel_function": True}),
marks=pytest.mark.xfail(reason="OpenAI service raise error for downloading image from URL"),
),
],
indirect=["chat_completion_agent"],
ids=["azure-image-content-streaming", "openai-image-content-streaming"],
@@ -23,7 +23,6 @@ from semantic_kernel.connectors.ai.azure_ai_inference import (
from semantic_kernel.connectors.ai.bedrock import BedrockChatCompletion, BedrockChatPromptExecutionSettings
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase
from semantic_kernel.connectors.ai.google.google_ai import GoogleAIChatCompletion, GoogleAIChatPromptExecutionSettings
from semantic_kernel.connectors.ai.google.vertex_ai import VertexAIChatCompletion, VertexAIChatPromptExecutionSettings
from semantic_kernel.connectors.ai.mistral_ai import MistralAIChatCompletion, MistralAIChatPromptExecutionSettings
from semantic_kernel.connectors.ai.ollama import OllamaChatCompletion, OllamaChatPromptExecutionSettings
from semantic_kernel.connectors.ai.onnx import OnnxGenAIChatCompletion, OnnxGenAIPromptExecutionSettings, ONNXTemplate
@@ -62,7 +61,7 @@ ollama_setup: bool = is_service_setup_for_testing(["OLLAMA_CHAT_MODEL_ID"])
ollama_image_setup: bool = is_service_setup_for_testing(["OLLAMA_CHAT_MODEL_ID_IMAGE"])
ollama_tool_call_setup: bool = is_service_setup_for_testing(["OLLAMA_CHAT_MODEL_ID_TOOL_CALL"])
google_ai_setup: bool = is_service_setup_for_testing(["GOOGLE_AI_API_KEY", "GOOGLE_AI_GEMINI_MODEL_ID"])
vertex_ai_setup: bool = is_service_setup_for_testing(["VERTEX_AI_PROJECT_ID", "VERTEX_AI_GEMINI_MODEL_ID"])
vertex_ai_setup: bool = is_service_setup_for_testing(["GOOGLE_AI_CLOUD_PROJECT_ID", "GOOGLE_AI_GEMINI_MODEL_ID"])
onnx_setup: bool = is_service_setup_for_testing(
["ONNX_GEN_AI_CHAT_MODEL_FOLDER"], raise_if_not_set=False
) # Tests are optional for ONNX
@@ -150,7 +149,10 @@ class ChatCompletionTestBase(CompletionTestBase):
OllamaChatPromptExecutionSettings,
),
"google_ai": (GoogleAIChatCompletion() if google_ai_setup else None, GoogleAIChatPromptExecutionSettings),
"vertex_ai": (VertexAIChatCompletion() if vertex_ai_setup else None, VertexAIChatPromptExecutionSettings),
"vertex_ai": (
GoogleAIChatCompletion(use_vertexai=True) if vertex_ai_setup else None,
GoogleAIChatPromptExecutionSettings,
),
"onnx_gen_ai": (
OnnxGenAIChatCompletion(template=ONNXTemplate.PHI3V) if onnx_setup else None,
OnnxGenAIPromptExecutionSettings,
@@ -505,10 +505,6 @@ pytestmark = pytest.mark.parametrize(
{"test_type": FunctionChoiceTestTypes.AUTO},
marks=[
pytest.mark.skipif(not google_ai_setup, reason="Google AI Environment Variables not set"),
pytest.mark.xfail(
reason="Temporarily failing due to Google AI API issue on streaming only: "
"400 Function calling is not enabled for models/gemini-1.5-flash"
),
],
id="google_ai_tool_call_auto",
),
@@ -531,10 +527,6 @@ pytestmark = pytest.mark.parametrize(
{"test_type": FunctionChoiceTestTypes.NON_AUTO},
marks=[
pytest.mark.skipif(not google_ai_setup, reason="Google AI Environment Variables not set"),
pytest.mark.xfail(
reason="Temporarily failing due to Google AI API issue on streaming only: "
"400 Function calling is not enabled for models/gemini-1.5-flash"
),
],
id="google_ai_tool_call_non_auto",
),
@@ -562,7 +554,6 @@ pytestmark = pytest.mark.parametrize(
],
],
{"test_type": FunctionChoiceTestTypes.FLOW},
marks=pytest.mark.skip(reason="Skipping due to 429s from Google AI."),
id="google_ai_tool_call_flow",
),
pytest.param(
@@ -583,10 +574,6 @@ pytestmark = pytest.mark.parametrize(
{"test_type": FunctionChoiceTestTypes.AUTO},
marks=[
pytest.mark.skipif(not google_ai_setup, reason="Google AI Environment Variables not set"),
pytest.mark.xfail(
reason="Temporarily failing due to Google AI API issue on streaming only: "
"400 Function calling is not enabled for models/gemini-1.5-flash"
),
],
id="google_ai_tool_call_auto_complex_return_type",
),
@@ -46,6 +46,7 @@ pytestmark = pytest.mark.parametrize(
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Where was it made?")]),
],
{},
marks=pytest.mark.xfail(reason="OpenAI service raise error for downloading image from URL"),
id="openai_image_input_uri",
),
pytest.param(
@@ -185,10 +186,6 @@ pytestmark = pytest.mark.parametrize(
{},
marks=[
pytest.mark.skipif(not google_ai_setup, reason="Google AI Environment Variables not set"),
pytest.mark.xfail(
reason="Temporarily failing due to Google AI API issue on streaming only: "
"400 Image input modality is not enabled for models/gemini-1.5-flash"
),
],
id="google_ai_image_input_file",
),
@@ -18,6 +18,7 @@ from semantic_kernel.kernel_pydantic import KernelBaseModel
from tests.integration.completions.chat_completion_test_base import (
ChatCompletionTestBase,
anthropic_setup,
google_ai_setup,
mistral_ai_setup,
ollama_setup,
onnx_setup,
@@ -157,20 +158,20 @@ pytestmark = pytest.mark.parametrize(
# region Google AI
pytest.param(
"google_ai",
{},
{"top_p": 0.9, "temperature": 0.7},
[
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Hello")]),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="How are you today?")]),
],
{},
marks=pytest.mark.skip(reason="Skipping due to 429s from Google AI."),
marks=pytest.mark.skipif(not google_ai_setup, reason="Need Google AI setup"),
id="google_ai_text_input",
),
# endregion
# region Vertex AI
pytest.param(
"vertex_ai",
{},
{"top_p": 0.9, "temperature": 0.7},
[
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Hello")]),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="How are you today?")]),
@@ -11,38 +11,28 @@ else:
from typing_extensions import override # pragma: no cover
import pytest
from azure.identity import AzureCliCredential
from openai import AsyncAzureOpenAI
from semantic_kernel import Kernel
from semantic_kernel.connectors.ai.bedrock import BedrockTextCompletion, BedrockTextPromptExecutionSettings
from semantic_kernel.connectors.ai.google.google_ai import GoogleAITextCompletion, GoogleAITextPromptExecutionSettings
from semantic_kernel.connectors.ai.google.vertex_ai import VertexAITextCompletion, VertexAITextPromptExecutionSettings
from semantic_kernel.connectors.ai.hugging_face import HuggingFacePromptExecutionSettings, HuggingFaceTextCompletion
from semantic_kernel.connectors.ai.ollama import OllamaTextCompletion, OllamaTextPromptExecutionSettings
from semantic_kernel.connectors.ai.onnx import OnnxGenAIPromptExecutionSettings, OnnxGenAITextCompletion
from semantic_kernel.connectors.ai.open_ai import (
AzureOpenAISettings,
AzureTextCompletion,
OpenAITextCompletion,
OpenAITextPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.open_ai import OpenAITextCompletion, OpenAITextPromptExecutionSettings
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.connectors.ai.text_completion_client_base import TextCompletionClientBase
from semantic_kernel.contents import StreamingTextContent, TextContent
from semantic_kernel.utils.authentication.entra_id_authentication import get_entra_auth_token
from tests.integration.completions.completion_test_base import CompletionTestBase, ServiceType
from tests.utils import is_service_setup_for_testing, is_test_running_on_supported_platforms, retry
hugging_face_setup = util.find_spec("torch") is not None
azure_openai_setup = True
ollama_setup: bool = is_service_setup_for_testing(["OLLAMA_TEXT_MODEL_ID"]) and is_test_running_on_supported_platforms([
"Linux"
])
google_ai_setup: bool = is_service_setup_for_testing(["GOOGLE_AI_API_KEY"])
vertex_ai_setup: bool = is_service_setup_for_testing(["VERTEX_AI_PROJECT_ID"])
google_ai_setup: bool = is_service_setup_for_testing(["GOOGLE_AI_API_KEY", "GOOGLE_AI_GEMINI_MODEL_ID"])
vertex_ai_setup: bool = is_service_setup_for_testing(["GOOGLE_AI_CLOUD_PROJECT_ID", "GOOGLE_AI_GEMINI_MODEL_ID"])
onnx_setup: bool = is_service_setup_for_testing(
["ONNX_GEN_AI_TEXT_MODEL_FOLDER"], raise_if_not_set=False
) # Tests are optional for ONNX
@@ -57,13 +47,6 @@ pytestmark = pytest.mark.parametrize(
{},
id="openai_text_completion",
),
pytest.param(
"azure",
{},
["Repeat the word Hello once"],
{},
id="azure_text_completion",
),
pytest.param(
"hf_t2t",
{},
@@ -111,7 +94,7 @@ pytestmark = pytest.mark.parametrize(
{},
["Repeat the word Hello once"],
{},
marks=pytest.mark.skip(reason="Skipping due to 429s from Google AI."),
marks=pytest.mark.skipif(not google_ai_setup, reason="Need Google AI setup"),
id="google_ai_text_completion",
),
pytest.param(
@@ -190,37 +173,15 @@ class TestTextCompletion(CompletionTestBase):
@override
@pytest.fixture(scope="class")
def services(self) -> dict[str, tuple[ServiceType | None, type[PromptExecutionSettings] | None]]:
azure_openai_setup = True
credential = AzureCliCredential()
azure_openai_settings = AzureOpenAISettings()
endpoint = str(azure_openai_settings.endpoint)
deployment_name = azure_openai_settings.text_deployment_name
ad_token = get_entra_auth_token(credential, azure_openai_settings.token_endpoint)
if not ad_token:
azure_openai_setup = False
api_version = azure_openai_settings.api_version
azure_custom_client = None
if azure_openai_setup:
azure_custom_client = AzureTextCompletion(
async_client=AsyncAzureOpenAI(
azure_endpoint=endpoint,
azure_deployment=deployment_name,
azure_ad_token=ad_token,
api_version=api_version,
default_headers={"Test-User-X-ID": "test"},
),
)
"""Get the services to be tested"""
return {
"openai": (OpenAITextCompletion(), OpenAITextPromptExecutionSettings),
"azure": (
AzureTextCompletion(credential=credential) if azure_openai_setup else None,
OpenAITextPromptExecutionSettings,
),
"azure_custom_client": (azure_custom_client, OpenAITextPromptExecutionSettings),
"ollama": (OllamaTextCompletion() if ollama_setup else None, OllamaTextPromptExecutionSettings),
"google_ai": (GoogleAITextCompletion() if google_ai_setup else None, GoogleAITextPromptExecutionSettings),
"vertex_ai": (VertexAITextCompletion() if vertex_ai_setup else None, VertexAITextPromptExecutionSettings),
"vertex_ai": (
GoogleAITextCompletion(use_vertexai=True) if vertex_ai_setup else None,
GoogleAITextPromptExecutionSettings,
),
"hf_t2t": (
HuggingFaceTextCompletion(
service_id="patrickvonplaten/t5-tiny-random",
@@ -76,7 +76,10 @@ pytestmark = pytest.mark.parametrize(
"vertex_ai",
{"output_dimensionality": 10},
10,
marks=pytest.mark.skipif(not vertex_ai_setup, reason="Vertex AI environment variables not set"),
marks=(
pytest.mark.skipif(not vertex_ai_setup, reason="Vertex AI environment variables not set"),
pytest.mark.timeout(300), # Vertex AI may take longer time
),
id="vertex_ai",
),
pytest.param(
@@ -17,10 +17,6 @@ from semantic_kernel.connectors.ai.google.google_ai import (
GoogleAIEmbeddingPromptExecutionSettings,
GoogleAITextEmbedding,
)
from semantic_kernel.connectors.ai.google.vertex_ai import (
VertexAIEmbeddingPromptExecutionSettings,
VertexAITextEmbedding,
)
from semantic_kernel.connectors.ai.hugging_face import HuggingFaceTextEmbedding
from semantic_kernel.connectors.ai.mistral_ai import MistralAITextEmbedding
from semantic_kernel.connectors.ai.ollama import OllamaEmbeddingPromptExecutionSettings, OllamaTextEmbedding
@@ -47,7 +43,7 @@ mistral_ai_setup: bool = is_service_setup_for_testing(
["MISTRALAI_API_KEY", "MISTRALAI_EMBEDDING_MODEL_ID"], raise_if_not_set=False
) # We don't have a MistralAI deployment
google_ai_setup: bool = is_service_setup_for_testing(["GOOGLE_AI_API_KEY", "GOOGLE_AI_EMBEDDING_MODEL_ID"])
vertex_ai_setup: bool = is_service_setup_for_testing(["VERTEX_AI_PROJECT_ID", "VERTEX_AI_EMBEDDING_MODEL_ID"])
vertex_ai_setup: bool = is_service_setup_for_testing(["GOOGLE_AI_CLOUD_PROJECT_ID", "GOOGLE_AI_EMBEDDING_MODEL_ID"])
ollama_setup: bool = is_service_setup_for_testing(["OLLAMA_EMBEDDING_MODEL_ID"])
# When testing Bedrock, after logging into AWS CLI this has been set, so we can use it to check if the service is setup
bedrock_setup: bool = is_service_setup_for_testing(["AWS_DEFAULT_REGION"], raise_if_not_set=False)
@@ -111,8 +107,8 @@ class EmbeddingServiceTestBase:
GoogleAIEmbeddingPromptExecutionSettings,
),
"vertex_ai": (
VertexAITextEmbedding() if vertex_ai_setup else None,
VertexAIEmbeddingPromptExecutionSettings,
GoogleAITextEmbedding(use_vertexai=True) if vertex_ai_setup else None,
GoogleAIEmbeddingPromptExecutionSettings,
),
"bedrock_amazon_titan-v1": (
BedrockTextEmbedding(model_id="amazon.titan-embed-text-v1") if bedrock_setup else None,
@@ -69,7 +69,10 @@ pytestmark = pytest.mark.parametrize(
pytest.param(
"vertex_ai",
{},
marks=pytest.mark.skipif(not vertex_ai_setup, reason="Vertex AI environment variables not set"),
marks=(
pytest.mark.skipif(not vertex_ai_setup, reason="Vertex AI environment variables not set"),
pytest.mark.timeout(300), # Vertex AI may take longer time
),
id="vertex_ai",
),
pytest.param(
@@ -13,6 +13,7 @@ def service_id() -> str:
@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
@@ -1,12 +1,18 @@
# Copyright (c) Microsoft. All rights reserved.
from collections.abc import AsyncGenerator
from collections.abc import AsyncGenerator, AsyncIterator
from unittest.mock import MagicMock
import pytest
import pytest_asyncio
from google.generativeai import protos
from google.generativeai.types import AsyncGenerateContentResponse
from google.genai.types import (
Candidate,
Content,
FinishReason,
GenerateContentResponse,
GenerateContentResponseUsageMetadata,
Part,
)
@pytest.fixture()
@@ -22,6 +28,8 @@ def google_ai_unit_test_env(monkeypatch, exclude_list, override_env_param_dict):
"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)
@@ -36,74 +44,64 @@ def google_ai_unit_test_env(monkeypatch, exclude_list, override_env_param_dict):
@pytest.fixture()
def mock_google_ai_chat_completion_response() -> AsyncGenerateContentResponse:
def mock_google_ai_chat_completion_response() -> GenerateContentResponse:
"""Mock Google AI Chat Completion response."""
candidate = protos.Candidate()
candidate = Candidate()
candidate.index = 0
candidate.content = protos.Content(role="user", parts=[protos.Part(text="Test content")])
candidate.finish_reason = protos.Candidate.FinishReason.STOP
candidate.content = Content(role="user", parts=[Part.from_text(text="Test content")])
candidate.finish_reason = FinishReason.STOP
response = protos.GenerateContentResponse()
response.candidates.append(candidate)
response.usage_metadata = protos.GenerateContentResponse.UsageMetadata(
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 AsyncGenerateContentResponse(
done=True,
iterator=None,
result=response,
)
return response
@pytest.fixture()
def mock_google_ai_chat_completion_response_with_tool_call() -> AsyncGenerateContentResponse:
def mock_google_ai_chat_completion_response_with_tool_call() -> GenerateContentResponse:
"""Mock Google AI Chat Completion response."""
candidate = protos.Candidate()
candidate = Candidate()
candidate.index = 0
candidate.content = protos.Content(
candidate.content = Content(
role="user",
parts=[
protos.Part(
function_call=protos.FunctionCall(
name="test_function",
args={"test_arg": "test_value"},
)
Part.from_function_call(
name="test_function",
args={"test_arg": "test_value"},
)
],
)
candidate.finish_reason = protos.Candidate.FinishReason.STOP
candidate.finish_reason = FinishReason.STOP
response = protos.GenerateContentResponse()
response.candidates.append(candidate)
response.usage_metadata = protos.GenerateContentResponse.UsageMetadata(
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 AsyncGenerateContentResponse(
done=True,
iterator=None,
result=response,
)
return response
@pytest_asyncio.fixture
async def mock_google_ai_streaming_chat_completion_response() -> AsyncGenerateContentResponse:
async def mock_google_ai_streaming_chat_completion_response() -> AsyncIterator[GenerateContentResponse]:
"""Mock Google AI streaming Chat Completion response."""
candidate = protos.Candidate()
candidate = Candidate()
candidate.index = 0
candidate.content = protos.Content(role="user", parts=[protos.Part(text="Test content")])
candidate.finish_reason = protos.Candidate.FinishReason.STOP
candidate.content = Content(role="user", parts=[Part.from_text(text="Test content")])
candidate.finish_reason = FinishReason.STOP
response = protos.GenerateContentResponse()
response.candidates.append(candidate)
response.usage_metadata = protos.GenerateContentResponse.UsageMetadata(
response = GenerateContentResponse()
response.candidates = [candidate]
response.usage_metadata = GenerateContentResponseUsageMetadata(
prompt_token_count=0,
cached_content_token_count=0,
candidates_token_count=0,
@@ -113,32 +111,28 @@ async def mock_google_ai_streaming_chat_completion_response() -> AsyncGenerateCo
iterable = MagicMock(spec=AsyncGenerator)
iterable.__aiter__.return_value = [response]
return await AsyncGenerateContentResponse.from_aiterator(
iterator=iterable,
)
return iterable
@pytest_asyncio.fixture
async def mock_google_ai_streaming_chat_completion_response_with_tool_call() -> AsyncGenerateContentResponse:
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 = protos.Candidate()
candidate = Candidate()
candidate.index = 0
candidate.content = protos.Content(
candidate.content = Content(
role="user",
parts=[
protos.Part(
function_call=protos.FunctionCall(
name="getLightStatus",
args={"arg1": "test_value"},
)
Part.from_function_call(
name="getLightStatus",
args={"arg1": "test_value"},
)
],
)
candidate.finish_reason = protos.Candidate.FinishReason.STOP
candidate.finish_reason = FinishReason.STOP
response = protos.GenerateContentResponse()
response.candidates.append(candidate)
response.usage_metadata = protos.GenerateContentResponse.UsageMetadata(
response = GenerateContentResponse()
response.candidates = [candidate]
response.usage_metadata = GenerateContentResponseUsageMetadata(
prompt_token_count=0,
cached_content_token_count=0,
candidates_token_count=0,
@@ -148,41 +142,33 @@ async def mock_google_ai_streaming_chat_completion_response_with_tool_call() ->
iterable = MagicMock(spec=AsyncGenerator)
iterable.__aiter__.return_value = [response]
return await AsyncGenerateContentResponse.from_aiterator(
iterator=iterable,
)
return iterable
@pytest.fixture()
def mock_google_ai_text_completion_response() -> AsyncGenerateContentResponse:
def mock_google_ai_text_completion_response() -> GenerateContentResponse:
"""Mock Google AI Text Completion response."""
candidate = protos.Candidate()
candidate = Candidate()
candidate.index = 0
candidate.content = protos.Content(parts=[protos.Part(text="Test content")])
candidate.content = Content(parts=[Part.from_text(text="Test content")])
response = protos.GenerateContentResponse()
response.candidates.append(candidate)
response = GenerateContentResponse()
response.candidates = [candidate]
return AsyncGenerateContentResponse(
done=True,
iterator=None,
result=response,
)
return response
@pytest_asyncio.fixture
async def mock_google_ai_streaming_text_completion_response() -> AsyncGenerateContentResponse:
async def mock_google_ai_streaming_text_completion_response() -> AsyncIterator[GenerateContentResponse]:
"""Mock Google AI streaming Text Completion response."""
candidate = protos.Candidate()
candidate = Candidate()
candidate.index = 0
candidate.content = protos.Content(parts=[protos.Part(text="Test content")])
candidate.content = Content(parts=[Part.from_text(text="Test content")])
response = protos.GenerateContentResponse()
response.candidates.append(candidate)
response = GenerateContentResponse()
response.candidates = [candidate]
iterable = MagicMock(spec=AsyncGenerator)
iterable.__aiter__.return_value = [response]
return await AsyncGenerateContentResponse.from_aiterator(
iterator=iterable,
)
return iterable
@@ -3,9 +3,9 @@
from unittest.mock import AsyncMock, patch
import pytest
from google.generativeai import GenerativeModel
from google.generativeai.protos import Content
from google.generativeai.types import GenerationConfig
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 (
@@ -13,6 +13,7 @@ from semantic_kernel.connectors.ai.google.google_ai.google_ai_prompt_execution_s
)
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
@@ -56,17 +57,24 @@ def test_google_ai_chat_completion_init_with_model_id_in_argument(google_ai_unit
@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):
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):
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")
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
@@ -78,28 +86,30 @@ def test_prompt_execution_settings_class(google_ai_unit_test_env) -> None:
# region chat completion
@patch.object(GenerativeModel, "generate_content_async", new_callable=AsyncMock)
@patch.object(AsyncModels, "generate_content", new_callable=AsyncMock)
async def test_google_ai_chat_completion(
mock_google_ai_model_generate_content_async,
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()
settings = GoogleAIChatPromptExecutionSettings(top_k=5, temperature=0.7)
mock_google_ai_model_generate_content_async.return_value = mock_google_ai_chat_completion_response
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_async.assert_called_once_with(
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),
generation_config=GenerationConfig(**settings.prepare_settings_dict()),
tools=None,
tool_config=None,
config=GenerateContentConfigDict(
**settings.prepare_settings_dict(),
system_instruction=filter_system_message(chat_history),
),
)
assert len(responses) == 1
assert responses[0].role == "assistant"
@@ -110,6 +120,37 @@ async def test_google_ai_chat_completion(
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,
@@ -130,16 +171,16 @@ async def test_google_ai_chat_completion_with_function_choice_behavior_fail_veri
)
@patch.object(GenerativeModel, "generate_content_async", new_callable=AsyncMock)
@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_async,
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_async.return_value = mock_google_ai_chat_completion_response_with_tool_call
mock_google_ai_model_generate_content.return_value = mock_google_ai_chat_completion_response_with_tool_call
settings = GoogleAIChatPromptExecutionSettings(
function_choice_behavior=FunctionChoiceBehavior.Auto(),
@@ -157,23 +198,23 @@ async def test_google_ai_chat_completion_with_function_choice_behavior(
# 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_async.call_count == 2
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(GenerativeModel, "generate_content_async", new_callable=AsyncMock)
@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_async,
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_async.return_value = mock_google_ai_chat_completion_response
mock_google_ai_model_generate_content.return_value = mock_google_ai_chat_completion_response
settings = GoogleAIChatPromptExecutionSettings(
function_choice_behavior=FunctionChoiceBehavior.Auto(),
@@ -188,11 +229,13 @@ async def test_google_ai_chat_completion_with_function_choice_behavior_no_tool_c
kernel=kernel,
)
mock_google_ai_model_generate_content_async.assert_awaited_once_with(
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),
generation_config=GenerationConfig(**settings.prepare_settings_dict()),
tools=None,
tool_config=None,
config=GenerateContentConfigDict(
**settings.prepare_settings_dict(),
system_instruction=filter_system_message(chat_history),
),
)
assert len(responses) == 1
assert responses[0].role == "assistant"
@@ -205,9 +248,9 @@ async def test_google_ai_chat_completion_with_function_choice_behavior_no_tool_c
# region streaming chat completion
@patch.object(GenerativeModel, "generate_content_async", new_callable=AsyncMock)
@patch.object(AsyncModels, "generate_content_stream", new_callable=AsyncMock)
async def test_google_ai_streaming_chat_completion(
mock_google_ai_model_generate_content_async,
mock_google_ai_model_generate_content_stream,
google_ai_unit_test_env,
chat_history: ChatHistory,
mock_google_ai_streaming_chat_completion_response,
@@ -215,7 +258,7 @@ async def test_google_ai_streaming_chat_completion(
"""Test streaming chat completion with GoogleAIChatCompletion"""
settings = GoogleAIChatPromptExecutionSettings()
mock_google_ai_model_generate_content_async.return_value = mock_google_ai_streaming_chat_completion_response
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):
@@ -225,15 +268,51 @@ async def test_google_ai_streaming_chat_completion(
assert "usage" in messages[0].metadata
assert "prompt_feedback" in messages[0].metadata
mock_google_ai_model_generate_content_async.assert_called_once_with(
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),
generation_config=GenerationConfig(**settings.prepare_settings_dict()),
tools=None,
tool_config=None,
stream=True,
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,
@@ -256,9 +335,9 @@ async def test_google_ai_streaming_chat_completion_with_function_choice_behavior
pass
@patch.object(GenerativeModel, "generate_content_async", new_callable=AsyncMock)
@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_async,
mock_google_ai_model_generate_content_stream,
google_ai_unit_test_env,
kernel: Kernel,
chat_history: ChatHistory,
@@ -266,7 +345,7 @@ async def test_google_ai_streaming_chat_completion_with_function_choice_behavior
decorated_native_function,
) -> None:
"""Test streaming chat completion of GoogleAIChatCompletion with function choice behavior"""
mock_google_ai_model_generate_content_async.return_value = (
mock_google_ai_model_generate_content_stream.return_value = (
mock_google_ai_streaming_chat_completion_response_with_tool_call
)
@@ -302,16 +381,16 @@ async def test_google_ai_streaming_chat_completion_with_function_choice_behavior
assert all_messages[1].finish_reason is None
@patch.object(GenerativeModel, "generate_content_async", new_callable=AsyncMock)
@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_async,
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_async.return_value = mock_google_ai_streaming_chat_completion_response
mock_google_ai_model_generate_content_stream.return_value = mock_google_ai_streaming_chat_completion_response
settings = GoogleAIChatPromptExecutionSettings(
function_choice_behavior=FunctionChoiceBehavior.Auto(),
@@ -327,16 +406,15 @@ async def test_google_ai_streaming_chat_completion_with_function_choice_behavior
):
assert len(messages) == 1
assert messages[0].role == "assistant"
assert (
messages[0].content == mock_google_ai_streaming_chat_completion_response.candidates[0].content.parts[0].text
)
assert messages[0].content == "Test content"
mock_google_ai_model_generate_content_async.assert_awaited_once_with(
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),
generation_config=GenerationConfig(**settings.prepare_settings_dict()),
tools=None,
tool_config=None,
stream=True,
config=GenerateContentConfigDict(
**settings.prepare_settings_dict(),
system_instruction=filter_system_message(chat_history),
),
)
@@ -3,8 +3,9 @@
from unittest.mock import AsyncMock, patch
import pytest
from google.generativeai import GenerativeModel
from google.generativeai.types import GenerationConfig
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,
@@ -48,17 +49,24 @@ def test_google_ai_text_completion_init_with_model_id_in_argument(google_ai_unit
@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):
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):
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")
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
@@ -70,9 +78,9 @@ def test_prompt_execution_settings_class(google_ai_unit_test_env) -> None:
# region text completion
@patch.object(GenerativeModel, "generate_content_async", new_callable=AsyncMock)
@patch.object(AsyncModels, "generate_content", new_callable=AsyncMock)
async def test_google_ai_text_completion(
mock_google_model_generate_content_async,
mock_google_model_generate_content,
google_ai_unit_test_env,
prompt: str,
mock_google_ai_text_completion_response,
@@ -80,14 +88,15 @@ async def test_google_ai_text_completion(
"""Test text completion with GoogleAITextCompletion"""
settings = GoogleAITextPromptExecutionSettings()
mock_google_model_generate_content_async.return_value = mock_google_ai_text_completion_response
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_async.assert_called_once_with(
mock_google_model_generate_content.assert_called_once_with(
model=google_ai_text_completion.service_settings.gemini_model_id,
contents=prompt,
generation_config=GenerationConfig(**settings.prepare_settings_dict()),
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
@@ -96,15 +105,47 @@ async def test_google_ai_text_completion(
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(GenerativeModel, "generate_content_async", new_callable=AsyncMock)
@patch.object(AsyncModels, "generate_content_stream", new_callable=AsyncMock)
async def test_google_ai_streaming_text_completion(
mock_google_model_generate_content_async,
mock_google_model_generate_content_stream,
google_ai_unit_test_env,
prompt: str,
mock_google_ai_streaming_text_completion_response,
@@ -112,7 +153,7 @@ async def test_google_ai_streaming_text_completion(
"""Test streaming text completion with GoogleAITextCompletion"""
settings = GoogleAITextPromptExecutionSettings()
mock_google_model_generate_content_async.return_value = mock_google_ai_streaming_text_completion_response
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):
@@ -120,11 +161,38 @@ async def test_google_ai_streaming_text_completion(
assert "usage" in chunks[0].metadata
assert "prompt_feedback" in chunks[0].metadata
mock_google_model_generate_content_async.assert_called_once_with(
mock_google_model_generate_content_stream.assert_called_once_with(
model=google_ai_text_completion.service_settings.gemini_model_id,
contents=prompt,
generation_config=GenerationConfig(**settings.prepare_settings_dict()),
stream=True,
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
@@ -1,8 +1,11 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import patch
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 (
@@ -46,15 +49,22 @@ def test_google_ai_text_embedding_init_with_model_id_in_argument(google_ai_unit_
@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):
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):
GoogleAITextEmbedding(env_file_path="fake_env_file_path.env")
with pytest.raises(ServiceInitializationError, match="The API key is required when use_vertexai is False."):
GoogleAITextEmbedding(use_vertexai=True, 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_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")
def test_prompt_execution_settings_class(google_ai_unit_test_env) -> None:
@@ -65,12 +75,14 @@ def test_prompt_execution_settings_class(google_ai_unit_test_env) -> None:
# endregion init
@patch("google.generativeai.embed_content_async")
async def test_embedding(mock_embedding_client, google_ai_unit_test_env, prompt):
@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_embedding_client.return_value = {"embedding": [[0.1, 0.2, 0.3]]}
mock_google_model_embed_content.return_value = EmbedContentResponse(
embeddings=[ContentEmbedding(values=[0.1, 0.2, 0.3])]
)
settings = GoogleAIEmbeddingPromptExecutionSettings()
google_ai_text_embedding = GoogleAITextEmbedding()
@@ -81,15 +93,21 @@ async def test_embedding(mock_embedding_client, google_ai_unit_test_env, prompt)
assert len(response) == 1
assert response.all() == array([0.1, 0.2, 0.3]).all()
mock_embedding_client.assert_called_once_with(model=model_id, content=[prompt])
mock_google_model_embed_content.assert_called_once_with(
model=model_id,
contents=[prompt],
config=EmbedContentConfigDict(output_dimensionality=settings.output_dimensionality),
)
@patch("google.generativeai.embed_content_async")
async def test_embedding_with_settings(mock_embedding_client, google_ai_unit_test_env, prompt):
@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_embedding_client.return_value = {"embedding": [[0.1, 0.2, 0.3]]}
mock_google_model_embed_content.return_value = EmbedContentResponse(
embeddings=[ContentEmbedding(values=[0.1, 0.2, 0.3])]
)
settings = GoogleAIEmbeddingPromptExecutionSettings()
settings.output_dimensionality = 3
@@ -101,37 +119,41 @@ async def test_embedding_with_settings(mock_embedding_client, google_ai_unit_tes
assert len(response) == 1
assert response.all() == array([0.1, 0.2, 0.3]).all()
mock_embedding_client.assert_called_once_with(
mock_google_model_embed_content.assert_called_once_with(
model=model_id,
content=[prompt],
**settings.prepare_settings_dict(),
contents=[prompt],
config=EmbedContentConfigDict(**settings.prepare_settings_dict()),
)
@patch("google.generativeai.embed_content_async")
async def test_embedding_without_settings(mock_embedding_client, google_ai_unit_test_env, prompt):
@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_embedding_client.return_value = {"embedding": [[0.1, 0.2, 0.3]]}
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_embedding_client.assert_called_once_with(
mock_google_model_embed_content.assert_called_once_with(
model=model_id,
content=[prompt],
contents=[prompt],
config=EmbedContentConfigDict(output_dimensionality=None),
)
@patch("google.generativeai.embed_content_async")
async def test_embedding_list_input(mock_embedding_client, google_ai_unit_test_env, prompt):
@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_embedding_client.return_value = {"embedding": [[0.1, 0.2, 0.3], [0.1, 0.2, 0.3]]}
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()
@@ -142,15 +164,21 @@ async def test_embedding_list_input(mock_embedding_client, google_ai_unit_test_e
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(model=model_id, content=[prompt, prompt])
mock_google_model_embed_content.assert_called_once_with(
model=model_id,
contents=[prompt, prompt],
config=EmbedContentConfigDict(output_dimensionality=settings.output_dimensionality),
)
@patch("google.generativeai.embed_content_async")
async def test_raw_embedding(mock_embedding_client, google_ai_unit_test_env, prompt):
@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_embedding_client.return_value = {"embedding": [[0.1, 0.2, 0.3]]}
mock_google_model_embed_content.return_value = EmbedContentResponse(
embeddings=[ContentEmbedding(values=[0.1, 0.2, 0.3])]
)
settings = GoogleAIEmbeddingPromptExecutionSettings()
google_ai_text_embedding = GoogleAITextEmbedding()
@@ -161,4 +189,35 @@ async def test_raw_embedding(mock_embedding_client, google_ai_unit_test_env, pro
assert len(response) == 1
assert response[0] == [0.1, 0.2, 0.3]
mock_embedding_client.assert_called_once_with(model=model_id, content=[prompt])
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()
@@ -1,7 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from google.generativeai.protos import Candidate, Part
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,
@@ -14,16 +14,20 @@ from semantic_kernel.contents.function_result_content import FunctionResultConte
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.contents.utils.finish_reason import FinishReason as SemanticKernelFinishReason
from semantic_kernel.exceptions.service_exceptions import ServiceInvalidRequestError
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(Candidate.FinishReason.STOP) == FinishReason.STOP
assert finish_reason_from_google_ai_to_semantic_kernel(Candidate.FinishReason.MAX_TOKENS) == FinishReason.LENGTH
assert finish_reason_from_google_ai_to_semantic_kernel(Candidate.FinishReason.SAFETY) == FinishReason.CONTENT_FILTER
assert finish_reason_from_google_ai_to_semantic_kernel(Candidate.FinishReason.OTHER) is None
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():
+4041 -4206
View File
File diff suppressed because it is too large Load Diff