mirror of
https://github.com/microsoft/semantic-kernel.git
synced 2026-10-12 02:19:06 +00:00
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:
+1
-1
@@ -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
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
-19
@@ -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
|
||||
|
||||
+124
-76
@@ -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,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
+73
-34
@@ -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,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
+45
-9
@@ -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."""
|
||||
|
||||
|
||||
+10
-5
@@ -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."""
|
||||
|
||||
|
||||
+5
@@ -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."""
|
||||
|
||||
|
||||
+6
@@ -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."""
|
||||
|
||||
|
||||
+17
@@ -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."""
|
||||
|
||||
|
||||
+4
-1
@@ -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",
|
||||
),
|
||||
|
||||
+1
-4
@@ -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
|
||||
|
||||
+124
-46
@@ -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),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
+83
-15
@@ -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
|
||||
|
||||
+87
-28
@@ -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():
|
||||
|
||||
Generated
+4041
-4206
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user