chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run

This commit is contained in:
wehub-resource-sync
2026-07-13 13:21:23 +08:00
commit b957a53def
5423 changed files with 863745 additions and 0 deletions
@@ -0,0 +1,56 @@
# Google - Gemini
Gemini models are Google's large language models. Semantic Kernel provides two connectors to access these models from Google Cloud.
## Google AI
You can access the Gemini API from Google AI Studio. This mode of access is for quick prototyping as it relies on API keys.
Follow [these instructions](https://cloud.google.com/docs/authentication/api-keys) to create an API key.
Once you have an API key, you can start using Gemini models in SK using the `google_ai` connector. Example:
```Python
kernel = Kernel()
kernel.add_service(
GoogleAIChatCompletion(
gemini_model_id="gemini-2.5-flash",
api_key="...",
)
)
...
```
> Alternatively, you can use an .env file to store the model id and api key.
## Vertex AI
Google also offers access to Gemini through its Vertex AI platform. Vertex AI provides a more complete solution to build your enterprise AI applications end-to-end. You can read more about it [here](https://cloud.google.com/vertex-ai/generative-ai/docs/migrate/migrate-google-ai).
This mode of access requires a Google Cloud service account. Follow these [instructions](https://cloud.google.com/vertex-ai/generative-ai/docs/migrate/migrate-google-ai) to create a Google Cloud project if you don't have one already. Remember the `project id` as it is required to access the models.
Follow the steps below to set up your environment to use the Vertex AI API:
- [Install the gcloud CLI](https://cloud.google.com/sdk/docs/install)
- [Initialize the gcloud CLI](https://cloud.google.com/sdk/docs/initializing)
Once you have your project and your environment is set up, you can start using Gemini models in SK using the `vertex_ai` connector. Example:
```Python
kernel = Kernel()
kernel.add_service(
GoogleAIChatCompletion(
project_id="...",
region="...",
gemini_model_id="gemini-2.5-flash",
use_vertexai=True,
)
)
...
```
> Alternatively, you can use an .env file to store the model id and project id.
## 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.
@@ -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",
]
@@ -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",
]
@@ -0,0 +1,51 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Annotated, Any, Literal
from pydantic import Field
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
class GoogleAIPromptExecutionSettings(PromptExecutionSettings):
"""Google AI Prompt Execution Settings."""
stop_sequences: Annotated[list[str] | None, Field(max_length=5)] = None
response_mime_type: Literal["text/plain", "application/json"] | None = None
response_schema: Any | None = None
candidate_count: Annotated[int | None, Field(ge=1)] = None
max_output_tokens: Annotated[int | None, Field(ge=1)] = None
temperature: Annotated[float | None, Field(ge=0.0, le=2.0)] = None
top_p: float | None = None
top_k: int | None = None
class GoogleAITextPromptExecutionSettings(GoogleAIPromptExecutionSettings):
"""Google AI Text Prompt Execution Settings."""
pass
class GoogleAIChatPromptExecutionSettings(GoogleAIPromptExecutionSettings):
"""Google AI Chat Prompt Execution Settings."""
tools: Annotated[
list[dict[str, Any]] | None,
Field(
description="Do not set this manually. It is set by the service based "
"on the function choice configuration.",
),
] = None
tool_config: Annotated[
dict[str, Any] | None,
Field(
description="Do not set this manually. It is set by the service based "
"on the function choice configuration.",
),
] = None
class GoogleAIEmbeddingPromptExecutionSettings(PromptExecutionSettings):
"""Google AI Embedding Prompt Execution Settings."""
output_dimensionality: Annotated[int | None, Field(le=768)] = None
@@ -0,0 +1,46 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import ClassVar
from pydantic import SecretStr
from semantic_kernel.kernel_pydantic import KernelBaseSettings
class GoogleAISettings(KernelBaseSettings):
"""Google AI settings.
The settings are first loaded from environment variables with
the prefix 'GOOGLE_AI_'.
If the environment variables are not found, the settings can
be loaded from a .env file with the encoding 'utf-8'.
If the settings are not found in the .env file, the settings
are ignored; however, validation will fail alerting that the
settings are missing.
Required settings for prefix 'GOOGLE_AI_' are:
- gemini_model_id: str - The Gemini model ID for the Google AI service, i.e. gemini-1.5-pro
This value can be found in the Google AI service deployment.
(Env var GOOGLE_AI_GEMINI_MODEL_ID)
- embedding_model_id: str - The embedding model ID for the Google AI service, i.e. text-embedding-004
This value can be found in the Google AI service deployment.
(Env var GOOGLE_AI_EMBEDDING_MODEL_ID)
- 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 | None = None
cloud_project_id: str | None = None
cloud_region: str | None = None
use_vertexai: bool = False
@@ -0,0 +1,19 @@
# Copyright (c) Microsoft. All rights reserved.
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
class GoogleAIBase(KernelBaseModel, ABC):
"""Google AI Service."""
MODEL_PROVIDER_NAME: ClassVar[str] = "googleai"
service_settings: GoogleAISettings
client: Client | None = None
@@ -0,0 +1,429 @@
# Copyright (c) Microsoft. All rights reserved.
import logging
import sys
from collections.abc import AsyncGenerator, Callable
from typing import TYPE_CHECKING, Any, ClassVar
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
from semantic_kernel.connectors.ai.completion_usage import CompletionUsage
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceType
from semantic_kernel.connectors.ai.google.google_ai.google_ai_prompt_execution_settings import (
GoogleAIChatPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.google.google_ai.google_ai_settings import GoogleAISettings
from semantic_kernel.connectors.ai.google.google_ai.services.google_ai_base import GoogleAIBase
from semantic_kernel.connectors.ai.google.google_ai.services.utils import (
finish_reason_from_google_ai_to_semantic_kernel,
format_assistant_message,
format_tool_message,
format_user_message,
update_settings_from_function_choice_configuration,
)
from semantic_kernel.connectors.ai.google.shared_utils import (
collapse_function_call_results_in_chat_history,
filter_system_message,
format_gemini_function_name_to_kernel_function_fully_qualified_name,
)
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.contents.chat_message_content import CMC_ITEM_TYPES, ChatMessageContent
from semantic_kernel.contents.function_call_content import FunctionCallContent
from semantic_kernel.contents.streaming_chat_message_content import STREAMING_CMC_ITEM_TYPES as STREAMING_ITEM_TYPES
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
from semantic_kernel.contents.streaming_text_content import StreamingTextContent
from semantic_kernel.contents.text_content import TextContent
from semantic_kernel.contents.utils.author_role import AuthorRole
from semantic_kernel.contents.utils.finish_reason import FinishReason
from semantic_kernel.exceptions.service_exceptions import (
ServiceInitializationError,
ServiceInvalidExecutionSettingsError,
)
from semantic_kernel.utils.telemetry.model_diagnostics.decorators import (
trace_chat_completion,
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
logger: logging.Logger = logging.getLogger(__name__)
class GoogleAIChatCompletion(GoogleAIBase, ChatCompletionClientBase):
"""Google AI Chat Completion Client."""
SUPPORTS_FUNCTION_CALLING: ClassVar[bool] = True
def __init__(
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:
"""Initialize the Google AI Chat Completion Client.
If no arguments are provided, the service will attempt to load the settings from the environment.
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)
Raises:
ServiceInitializationError: If an error occurs during initialization.
"""
try:
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 google_ai_settings.use_vertexai and not google_ai_settings.cloud_region:
raise ServiceInitializationError("Region must be provided when use_vertexai is True.")
if not google_ai_settings.use_vertexai and 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
# Override from AIServiceClientBase
@override
def get_prompt_execution_settings_class(self) -> type["PromptExecutionSettings"]:
return GoogleAIChatPromptExecutionSettings
@override
@trace_chat_completion(GoogleAIBase.MODEL_PROVIDER_NAME)
async def _inner_get_chat_message_contents(
self,
chat_history: "ChatHistory",
settings: "PromptExecutionSettings",
) -> list["ChatMessageContent"]:
if not isinstance(settings, GoogleAIChatPromptExecutionSettings):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, GoogleAIChatPromptExecutionSettings) # nosec
if not self.service_settings.gemini_model_id:
raise ServiceInitializationError("The Google AI Gemini model ID is required.")
collapse_function_call_results_in_chat_history(chat_history)
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]
),
)
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)
async def _inner_get_streaming_chat_message_contents(
self,
chat_history: "ChatHistory",
settings: "PromptExecutionSettings",
function_invoke_attempt: int = 0,
) -> AsyncGenerator[list["StreamingChatMessageContent"], Any]:
if not isinstance(settings, GoogleAIChatPromptExecutionSettings):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, GoogleAIChatPromptExecutionSettings) # nosec
if not self.service_settings.gemini_model_id:
raise ServiceInitializationError("The Google AI Gemini model ID is required.")
collapse_function_call_results_in_chat_history(chat_history)
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
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:
if not isinstance(settings, GoogleAIChatPromptExecutionSettings):
raise ServiceInvalidExecutionSettingsError("The settings must be an GoogleAIChatPromptExecutionSettings.")
if settings.candidate_count is not None and settings.candidate_count > 1:
raise ServiceInvalidExecutionSettingsError(
"Auto-invocation of tool calls may only be used with a "
"GoogleAIChatPromptExecutionSettings.candidate_count of 1."
)
@override
def _update_function_choice_settings_callback(
self,
) -> Callable[["FunctionCallChoiceConfiguration", "PromptExecutionSettings", FunctionChoiceType], None]:
return update_settings_from_function_choice_configuration
@override
def _reset_function_choice_settings(self, settings: "PromptExecutionSettings") -> None:
if hasattr(settings, "tool_config"):
settings.tool_config = None
if hasattr(settings, "tools"):
settings.tools = None
@override
def _prepare_chat_history_for_request(
self,
chat_history: ChatHistory,
role_key: str = "role",
content_key: str = "content",
) -> list[Content]:
chat_request_messages: list[Content] = []
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 config.
continue
if message.role == AuthorRole.USER:
chat_request_messages.append(Content(role="user", parts=format_user_message(message)))
elif message.role == AuthorRole.ASSISTANT:
chat_request_messages.append(Content(role="model", parts=format_assistant_message(message)))
elif message.role == AuthorRole.TOOL:
chat_request_messages.append(Content(role="function", parts=format_tool_message(message)))
return chat_request_messages
# endregion
# region Non-streaming
def _create_chat_message_content(
self, response: GenerateContentResponse, candidate: Candidate
) -> ChatMessageContent:
"""Create a chat message content object.
Args:
response: The response from the service.
candidate: The candidate from the response.
Returns:
A chat message content object.
"""
# Best effort conversion of finish reason. The raw value will be available in metadata.
finish_reason: FinishReason | None = finish_reason_from_google_ai_to_semantic_kernel(candidate.finish_reason)
response_metadata = self._get_metadata_from_response(response)
response_metadata.update(self._get_metadata_from_candidate(candidate))
items: list[CMC_ITEM_TYPES] = []
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:
fc_metadata: dict[str, Any] = {}
thought_sig = getattr(part, "thought_signature", None)
if thought_sig:
fc_metadata["thought_signature"] = thought_sig
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
metadata=fc_metadata if fc_metadata else None,
)
)
return ChatMessageContent(
ai_model_id=self.ai_model_id,
role=AuthorRole.ASSISTANT,
items=items,
inner_content=response,
finish_reason=finish_reason,
metadata=response_metadata,
)
# endregion
# region Streaming
def _create_streaming_chat_message_content(
self,
chunk: GenerateContentResponse,
candidate: Candidate,
function_invoke_attempt: int = 0,
) -> StreamingChatMessageContent:
"""Create a streaming chat message content object.
Args:
chunk: The response from the service.
candidate: The candidate from the response.
function_invoke_attempt: The function invoke attempt.
Returns:
A streaming chat message content object.
"""
# Best effort conversion of finish reason. The raw value will be available in metadata.
finish_reason: FinishReason | None = finish_reason_from_google_ai_to_semantic_kernel(candidate.finish_reason)
response_metadata = self._get_metadata_from_response(chunk)
response_metadata.update(self._get_metadata_from_candidate(candidate))
items: list[STREAMING_ITEM_TYPES] = []
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:
fc_metadata: dict[str, Any] = {}
thought_sig = getattr(part, "thought_signature", None)
if thought_sig:
fc_metadata["thought_signature"] = thought_sig
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
metadata=fc_metadata if fc_metadata else None,
)
)
return StreamingChatMessageContent(
ai_model_id=self.ai_model_id,
role=AuthorRole.ASSISTANT,
choice_index=candidate.index or 0,
items=items,
inner_content=chunk,
finish_reason=finish_reason,
metadata=response_metadata,
function_invoke_attempt=function_invoke_attempt,
)
# endregion
def _get_metadata_from_response(self, response: GenerateContentResponse) -> dict[str, Any]:
"""Get metadata from the response.
Args:
response: The response from the service.
Returns:
A dictionary containing metadata.
"""
return {
"prompt_feedback": response.prompt_feedback,
"usage": CompletionUsage(
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,
),
}
def _get_metadata_from_candidate(self, candidate: Candidate) -> dict[str, Any]:
"""Get metadata from the candidate.
Args:
candidate: The candidate from the response.
Returns:
A dictionary containing metadata.
"""
return {
"index": candidate.index,
"finish_reason": candidate.finish_reason,
"safety_ratings": candidate.safety_ratings,
"token_count": candidate.token_count,
}
@@ -0,0 +1,262 @@
# Copyright (c) Microsoft. All rights reserved.
import sys
from collections.abc import AsyncGenerator
from typing import TYPE_CHECKING, Any
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
from semantic_kernel.connectors.ai.google.google_ai.google_ai_prompt_execution_settings import (
GoogleAITextPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.google.google_ai.google_ai_settings import GoogleAISettings
from semantic_kernel.connectors.ai.google.google_ai.services.google_ai_base import GoogleAIBase
from semantic_kernel.connectors.ai.text_completion_client_base import TextCompletionClientBase
from semantic_kernel.contents import TextContent
from semantic_kernel.contents.streaming_text_content import StreamingTextContent
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError
from semantic_kernel.utils.telemetry.model_diagnostics.decorators import (
trace_streaming_text_completion,
trace_text_completion,
)
if TYPE_CHECKING:
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
class GoogleAITextCompletion(GoogleAIBase, TextCompletionClientBase):
"""Google AI Text Completion Client."""
def __init__(
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:
"""Initialize the Google AI Text Completion Client.
If no arguments are provided, the service will attempt to load the settings from the environment.
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)
Raises:
ServiceInitializationError: If an error occurs during initialization.
"""
try:
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 google_ai_settings.use_vertexai and not google_ai_settings.cloud_region:
raise ServiceInitializationError("Region must be provided when use_vertexai is True.")
if not google_ai_settings.use_vertexai and 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
# Override from AIServiceClientBase
@override
def get_prompt_execution_settings_class(self) -> type["PromptExecutionSettings"]:
return GoogleAITextPromptExecutionSettings
@override
@trace_text_completion(GoogleAIBase.MODEL_PROVIDER_NAME)
async def _inner_get_text_contents(
self,
prompt: str,
settings: "PromptExecutionSettings",
) -> list[TextContent]:
if not isinstance(settings, GoogleAITextPromptExecutionSettings):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, GoogleAITextPromptExecutionSettings) # nosec
if not self.service_settings.gemini_model_id:
raise ServiceInitializationError("The Google AI Gemini model ID is required.")
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]
)
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)
async def _inner_get_streaming_text_contents(
self,
prompt: str,
settings: "PromptExecutionSettings",
) -> AsyncGenerator[list[StreamingTextContent], Any]:
if not isinstance(settings, GoogleAITextPromptExecutionSettings):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, GoogleAITextPromptExecutionSettings) # nosec
if not self.service_settings.gemini_model_id:
raise ServiceInitializationError("The Google AI Gemini model ID is required.")
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
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: GenerateContentResponse, candidate: Candidate) -> TextContent:
"""Create a text content object.
Args:
response: The response from the service.
candidate: The candidate from the response.
Returns:
A text content object.
"""
response_metadata = self._get_metadata_from_response(response)
response_metadata.update(self._get_metadata_from_candidate(candidate))
return TextContent(
ai_model_id=self.ai_model_id,
text=candidate.content.parts[0].text or "" if candidate.content and candidate.content.parts else "",
inner_content=response,
metadata=response_metadata,
)
def _create_streaming_text_content(
self, chunk: GenerateContentResponse, candidate: Candidate
) -> StreamingTextContent:
"""Create a streaming text content object.
Args:
chunk: The response from the service.
candidate: The candidate from the response.
Returns:
A streaming text content object.
"""
response_metadata = self._get_metadata_from_response(chunk)
response_metadata.update(self._get_metadata_from_candidate(candidate))
return StreamingTextContent(
ai_model_id=self.ai_model_id,
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: GenerateContentResponse) -> dict[str, Any]:
"""Get metadata from the response.
Args:
response: The response from the service.
Returns:
A dictionary containing metadata.
"""
return {
"prompt_feedback": response.prompt_feedback,
"usage": CompletionUsage(
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,
),
}
def _get_metadata_from_candidate(self, candidate: Candidate) -> dict[str, Any]:
"""Get metadata from the candidate.
Args:
candidate: The candidate from the response.
Returns:
A dictionary containing metadata.
"""
return {
"index": candidate.index,
"finish_reason": candidate.finish_reason,
"safety_ratings": candidate.safety_ratings,
"token_count": candidate.token_count,
}
@@ -0,0 +1,149 @@
# Copyright (c) Microsoft. All rights reserved.
import sys
from typing import Any
from google.genai import Client
from google.genai.types import EmbedContentConfigDict, EmbedContentResponse
from numpy import array, ndarray
from pydantic import ValidationError
from semantic_kernel.connectors.ai.embedding_generator_base import EmbeddingGeneratorBase
from semantic_kernel.connectors.ai.google.google_ai.google_ai_prompt_execution_settings import (
GoogleAIEmbeddingPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.google.google_ai.google_ai_settings import GoogleAISettings
from semantic_kernel.connectors.ai.google.google_ai.services.google_ai_base import GoogleAIBase
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
class GoogleAITextEmbedding(GoogleAIBase, EmbeddingGeneratorBase):
"""Google AI Text Embedding Service."""
def __init__(
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:
"""Initialize the Google AI Text Embedding service.
If no arguments are provided, the service will attempt to load the settings from the environment.
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)
Raises:
ServiceInitializationError: If an error occurs during initialization.
"""
try:
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 google_ai_settings.use_vertexai and not google_ai_settings.cloud_region:
raise ServiceInitializationError("Region must be provided when use_vertexai is True.")
if not google_ai_settings.use_vertexai and 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
async def generate_embeddings(
self,
texts: list[str],
settings: "PromptExecutionSettings | None" = None,
**kwargs: Any,
) -> ndarray:
raw_embeddings = await self.generate_raw_embeddings(texts, settings, **kwargs)
return array(raw_embeddings)
@override
async def generate_raw_embeddings(
self,
texts: list[str],
settings: "PromptExecutionSettings | None" = None,
**kwargs: Any,
) -> list[list[float]]:
if not settings:
settings = GoogleAIEmbeddingPromptExecutionSettings()
else:
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, GoogleAIEmbeddingPromptExecutionSettings) # nosec
if not self.service_settings.embedding_model_id:
raise ServiceInitializationError("The Google AI embedding model ID is required.")
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(
self,
) -> type["PromptExecutionSettings"]:
"""Get the request settings class."""
return GoogleAIEmbeddingPromptExecutionSettings
@@ -0,0 +1,203 @@
# Copyright (c) Microsoft. All rights reserved.
import json
import logging
from typing import TYPE_CHECKING, Any
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 (
GoogleAIChatPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.google.shared_utils import (
FUNCTION_CHOICE_TYPE_TO_GOOGLE_FUNCTION_CALLING_MODE,
GEMINI_FUNCTION_NAME_SEPARATOR,
sanitize_schema_for_google_ai,
)
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.function_call_content import FunctionCallContent
from semantic_kernel.contents.function_result_content import FunctionResultContent
from semantic_kernel.contents.image_content import ImageContent
from semantic_kernel.contents.text_content import TextContent
from semantic_kernel.contents.utils.finish_reason import FinishReason as SemanticKernelFinishReason
from semantic_kernel.exceptions.service_exceptions import ServiceInvalidRequestError
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata
if TYPE_CHECKING:
from semantic_kernel.connectors.ai.function_call_choice_configuration import FunctionCallChoiceConfiguration
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
logger: logging.Logger = logging.getLogger(__name__)
def finish_reason_from_google_ai_to_semantic_kernel(
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 is None:
return None
if finish_reason == FinishReason.STOP:
return SemanticKernelFinishReason.STOP
if finish_reason == FinishReason.MAX_TOKENS:
return SemanticKernelFinishReason.LENGTH
if finish_reason == FinishReason.SAFETY:
return SemanticKernelFinishReason.CONTENT_FILTER
return None
def format_user_message(message: ChatMessageContent) -> list[Part]:
"""Format a user message to the expected object for the client.
Args:
message: The user message.
Returns:
The formatted user message as a list of parts.
"""
parts: list[Part] = []
for item in message.items:
if isinstance(item, TextContent):
parts.append(Part.from_text(text=item.text))
elif isinstance(item, ImageContent):
parts.append(_create_image_part(item))
else:
raise ServiceInvalidRequestError(
"Unsupported item type in User message while formatting chat history for Google AI"
f" Inference: {type(item)}"
)
return parts
def format_assistant_message(message: ChatMessageContent) -> list[Part]:
"""Format an assistant message to the expected object for the client.
Args:
message: The assistant message.
Returns:
The formatted assistant message as a list of parts.
"""
parts: list[Part] = []
for item in message.items:
if isinstance(item, TextContent):
if item.text:
parts.append(Part.from_text(text=item.text))
elif isinstance(item, FunctionCallContent):
thought_signature = item.metadata.get("thought_signature") if item.metadata else None
if thought_signature:
parts.append(
Part(
function_call={
"name": item.name, # type: ignore[arg-type]
"args": json.loads(item.arguments) if isinstance(item.arguments, str) else item.arguments,
},
thought_signature=thought_signature,
)
)
else:
parts.append(
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 Google AI"
f" Inference: {type(item)}"
)
return parts
def format_tool_message(message: ChatMessageContent) -> list[Part]:
"""Format a tool message to the expected object for the client.
Args:
message: The tool message.
Returns:
The formatted tool message.
"""
parts: list[Part] = []
for item in message.items:
if isinstance(item, FunctionResultContent):
gemini_function_name = item.custom_fully_qualified_name(GEMINI_FUNCTION_NAME_SEPARATOR)
parts.append(
Part.from_function_response(
name=gemini_function_name,
response={
"content": str(item.result),
},
)
)
return parts
def kernel_function_metadata_to_google_ai_function_call_format(metadata: KernelFunctionMetadata) -> dict[str, Any]:
"""Convert the kernel function metadata to function calling format."""
parameters: dict[str, Any] | None = None
if metadata.parameters:
properties = {}
for param in metadata.parameters:
if param.name is None:
continue
prop_schema = sanitize_schema_for_google_ai(param.schema_data) if param.schema_data else param.schema_data
properties[param.name] = prop_schema
parameters = {
"type": "object",
"properties": properties,
"required": [p.name for p in metadata.parameters if p.is_required and p.name is not None],
}
return {
"name": metadata.custom_fully_qualified_name(GEMINI_FUNCTION_NAME_SEPARATOR),
"description": metadata.description or "",
"parameters": parameters,
}
def update_settings_from_function_choice_configuration(
function_choice_configuration: "FunctionCallChoiceConfiguration",
settings: "PromptExecutionSettings",
type: FunctionChoiceType,
) -> None:
"""Update the settings from a FunctionChoiceConfiguration."""
assert isinstance(settings, GoogleAIChatPromptExecutionSettings) # nosec
if function_choice_configuration.available_functions:
settings.tool_config = {
"function_calling_config": {
"mode": FUNCTION_CHOICE_TYPE_TO_GOOGLE_FUNCTION_CALLING_MODE[type],
}
}
settings.tools = [
{
"function_declarations": [
kernel_function_metadata_to_google_ai_function_call_format(f)
for f in function_choice_configuration.available_functions
]
}
]
def _create_image_part(image_content: ImageContent) -> Part:
if image_content.data_uri:
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
raise ServiceInvalidRequestError(
"ImageContent without data_uri in User message while formatting chat history for Google AI"
)
@@ -0,0 +1,135 @@
# Copyright (c) Microsoft. All rights reserved.
import logging
from copy import deepcopy
from typing import Any
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceType
from semantic_kernel.const import DEFAULT_FULLY_QUALIFIED_NAME_SEPARATOR
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.contents.utils.author_role import AuthorRole
from semantic_kernel.exceptions.service_exceptions import ServiceInvalidRequestError
logger: logging.Logger = logging.getLogger(__name__)
def filter_system_message(chat_history: ChatHistory) -> str | None:
"""Filter the first system message from the chat history.
If there are multiple system messages, raise an error.
If there are no system messages, return None.
"""
if len([message for message in chat_history if message.role == AuthorRole.SYSTEM]) > 1:
raise ServiceInvalidRequestError(
"Multiple system messages in chat history. Only one system message is expected."
)
for message in chat_history:
if message.role == AuthorRole.SYSTEM:
return message.content
return None
FUNCTION_CHOICE_TYPE_TO_GOOGLE_FUNCTION_CALLING_MODE = {
FunctionChoiceType.AUTO: "AUTO",
FunctionChoiceType.NONE: "NONE",
FunctionChoiceType.REQUIRED: "ANY",
}
# The separator used in the fully qualified name of the function instead of the default "-" separator.
# This is required since Gemini doesn't work well with "-" in the function name.
# https://ai.google.dev/gemini-api/docs/function-calling#function_declarations
# Using double underscore to avoid situations where the function name already contains a single underscore.
# For example, we may incorrect split a function name with a single score when the function doesn't have a plugin name.
GEMINI_FUNCTION_NAME_SEPARATOR = "__"
def format_gemini_function_name_to_kernel_function_fully_qualified_name(gemini_function_name: str) -> str:
"""Format the Gemini function name to the kernel function fully qualified name."""
if GEMINI_FUNCTION_NAME_SEPARATOR in gemini_function_name:
plugin_name, function_name = gemini_function_name.split(GEMINI_FUNCTION_NAME_SEPARATOR, 1)
return f"{plugin_name}{DEFAULT_FULLY_QUALIFIED_NAME_SEPARATOR}{function_name}"
return gemini_function_name
def sanitize_schema_for_google_ai(schema: dict[str, Any] | None) -> dict[str, Any] | None:
"""Sanitize a JSON schema dict so it is compatible with Google AI / Vertex AI.
The Google AI protobuf ``Schema`` does not support ``anyOf``, ``oneOf``, or
``allOf``. It also does not accept ``type`` as an array (e.g.
``["string", "null"]``). This helper recursively rewrites those constructs
into the subset that Google AI understands, using ``nullable`` where
appropriate.
"""
if schema is None:
return None
schema = deepcopy(schema)
return _sanitize_node(schema)
def _sanitize_node(node: dict[str, Any]) -> dict[str, Any]:
"""Recursively sanitize a single schema node."""
# --- handle ``type`` given as a list (e.g. ["string", "null"]) ---
type_val = node.get("type")
if isinstance(type_val, list):
non_null = [t for t in type_val if t != "null"]
if len(type_val) != len(non_null):
node["nullable"] = True
node["type"] = non_null[0] if non_null else "string"
# --- handle ``anyOf`` / ``oneOf`` / ``allOf`` ---
for key in ("anyOf", "oneOf", "allOf"):
variants = node.get(key)
if not variants:
continue
non_null = [v for v in variants if v.get("type") != "null"]
has_null = len(variants) != len(non_null)
chosen = _sanitize_node(non_null[0]) if non_null else {"type": "string"}
# Preserve description from the outer node
desc = node.get("description")
node.clear()
node.update(chosen)
if has_null:
node["nullable"] = True
if desc and "description" not in node:
node["description"] = desc
break # only process the first matching key
# --- recurse into nested structures ---
props = node.get("properties")
if isinstance(props, dict):
for prop_name, prop_schema in props.items():
if isinstance(prop_schema, dict):
props[prop_name] = _sanitize_node(prop_schema)
items = node.get("items")
if isinstance(items, dict):
node["items"] = _sanitize_node(items)
return node
def collapse_function_call_results_in_chat_history(chat_history: ChatHistory):
"""The Gemini API expects the results of parallel function calls to be contained in a single message to be returned.
This helper method collapses the results of parallel function calls in the chat history into a single Tool message.
Since this method in an internal method that is supposed to be called only by the Google AI and Vertex AI
connectors, it is safe to assume that the chat history contains a correct sequence of messages, i.e. there won't be
cases where the assistant wants to call 2 functions in parallel but there are more than 2 function results following
the assistant message.
"""
if not chat_history.messages:
return
current_idx = 1
while current_idx < len(chat_history):
previous_message = chat_history[current_idx - 1]
current_message = chat_history[current_idx]
if previous_message.role == AuthorRole.TOOL and current_message.role == AuthorRole.TOOL:
previous_message.items.extend(current_message.items)
chat_history.remove_message(current_message)
else:
current_idx += 1
@@ -0,0 +1,31 @@
# 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
from semantic_kernel.connectors.ai.google.vertex_ai.vertex_ai_prompt_execution_settings import (
VertexAIChatPromptExecutionSettings,
VertexAIEmbeddingPromptExecutionSettings,
VertexAIPromptExecutionSettings,
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",
"VertexAIEmbeddingPromptExecutionSettings",
"VertexAIPromptExecutionSettings",
"VertexAITextCompletion",
"VertexAITextEmbedding",
"VertexAITextPromptExecutionSettings",
]
@@ -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,
)
@@ -0,0 +1,191 @@
# Copyright (c) Microsoft. All rights reserved.
import json
import logging
from typing import TYPE_CHECKING, Any
from google.cloud.aiplatform_v1beta1.types.content import Candidate
from vertexai.generative_models import FunctionDeclaration, Part, Tool, ToolConfig
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceType
from semantic_kernel.connectors.ai.google.shared_utils import (
FUNCTION_CHOICE_TYPE_TO_GOOGLE_FUNCTION_CALLING_MODE,
GEMINI_FUNCTION_NAME_SEPARATOR,
sanitize_schema_for_google_ai,
)
from semantic_kernel.connectors.ai.google.vertex_ai.vertex_ai_prompt_execution_settings import (
VertexAIChatPromptExecutionSettings,
)
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.function_call_content import FunctionCallContent
from semantic_kernel.contents.function_result_content import FunctionResultContent
from semantic_kernel.contents.image_content import ImageContent
from semantic_kernel.contents.text_content import TextContent
from semantic_kernel.contents.utils.finish_reason import FinishReason as SemanticKernelFinishReason
from semantic_kernel.exceptions.service_exceptions import ServiceInvalidRequestError
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata
if TYPE_CHECKING:
from semantic_kernel.connectors.ai.function_call_choice_configuration import FunctionCallChoiceConfiguration
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
logger: logging.Logger = logging.getLogger(__name__)
def finish_reason_from_vertex_ai_to_semantic_kernel(
finish_reason: Candidate.FinishReason,
) -> SemanticKernelFinishReason | None:
"""Convert a Vertex 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:
return SemanticKernelFinishReason.STOP
if finish_reason == Candidate.FinishReason.MAX_TOKENS:
return SemanticKernelFinishReason.LENGTH
if finish_reason == Candidate.FinishReason.SAFETY:
return SemanticKernelFinishReason.CONTENT_FILTER
return None
def format_user_message(message: ChatMessageContent) -> list[Part]:
"""Format a user message to the expected object for the client.
Args:
message: The user message.
Returns:
The formatted user message as a list of parts.
"""
parts: list[Part] = []
for item in message.items:
if isinstance(item, TextContent):
parts.append(Part.from_text(item.text))
elif isinstance(item, ImageContent):
parts.append(_create_image_part(item))
else:
raise ServiceInvalidRequestError(
"Unsupported item type in User message while formatting chat history for Vertex AI"
f" Inference: {type(item)}"
)
return parts
def format_assistant_message(message: ChatMessageContent) -> list[Part]:
"""Format an assistant message to the expected object for the client.
Args:
message: The assistant message.
Returns:
The formatted assistant message as a list of parts.
"""
parts: list[Part] = []
for item in message.items:
if isinstance(item, TextContent):
if item.text:
parts.append(Part.from_text(item.text))
elif isinstance(item, FunctionCallContent):
part_dict: dict[str, Any] = {
"function_call": {
"name": item.name, # type: ignore[arg-type]
"args": json.loads(item.arguments) if isinstance(item.arguments, str) else item.arguments,
}
}
thought_signature = item.metadata.get("thought_signature") if item.metadata else None
if thought_signature:
part_dict["thought_signature"] = thought_signature
parts.append(Part.from_dict(part_dict))
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"
f" Inference: {type(item)}"
)
return parts
def format_tool_message(message: ChatMessageContent) -> list[Part]:
"""Format a tool message to the expected object for the client.
Args:
message: The tool message.
Returns:
The formatted tool message.
"""
parts: list[Part] = []
for item in message.items:
if isinstance(item, FunctionResultContent):
gemini_function_name = item.custom_fully_qualified_name(GEMINI_FUNCTION_NAME_SEPARATOR)
parts.append(
Part.from_function_response(
gemini_function_name,
{
"content": str(item.result),
},
)
)
return parts
def kernel_function_metadata_to_vertex_ai_function_call_format(metadata: KernelFunctionMetadata) -> FunctionDeclaration:
"""Convert the kernel function metadata to function calling format."""
properties: dict[str, Any] = {}
if metadata.parameters:
for param in metadata.parameters:
if param.name is None:
continue
prop_schema = sanitize_schema_for_google_ai(param.schema_data) if param.schema_data else param.schema_data
properties[param.name] = prop_schema
return FunctionDeclaration(
name=metadata.custom_fully_qualified_name(GEMINI_FUNCTION_NAME_SEPARATOR),
description=metadata.description or "",
parameters={
"type": "object",
"properties": properties,
"required": [p.name for p in metadata.parameters if p.is_required and p.name is not None],
},
)
def update_settings_from_function_choice_configuration(
function_choice_configuration: "FunctionCallChoiceConfiguration",
settings: "PromptExecutionSettings",
type: FunctionChoiceType,
) -> None:
"""Update the settings from a FunctionChoiceConfiguration."""
assert isinstance(settings, VertexAIChatPromptExecutionSettings) # nosec
if function_choice_configuration.available_functions:
settings.tool_config = ToolConfig(
function_calling_config=ToolConfig.FunctionCallingConfig(
mode=FUNCTION_CHOICE_TYPE_TO_GOOGLE_FUNCTION_CALLING_MODE[type],
),
)
settings.tools = [
Tool(
function_declarations=[
kernel_function_metadata_to_vertex_ai_function_call_format(f)
for f in function_choice_configuration.available_functions
]
)
]
def _create_image_part(image_content: ImageContent) -> Part:
if image_content.data_uri:
return Part.from_data(image_content.data, 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
raise ServiceInvalidRequestError(
"ImageContent without data_uri in User message while formatting chat history for Google AI"
)
@@ -0,0 +1,18 @@
# Copyright (c) Microsoft. All rights reserved.
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."""
MODEL_PROVIDER_NAME: ClassVar[str] = "vertexai"
service_settings: VertexAISettings
@@ -0,0 +1,375 @@
# Copyright (c) Microsoft. All rights reserved.
import sys
from collections.abc import AsyncGenerator, AsyncIterable, Callable
from typing import TYPE_CHECKING, Any, ClassVar
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
from semantic_kernel.connectors.ai.completion_usage import CompletionUsage
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceType
from semantic_kernel.connectors.ai.google.shared_utils import (
collapse_function_call_results_in_chat_history,
filter_system_message,
format_gemini_function_name_to_kernel_function_fully_qualified_name,
)
from semantic_kernel.connectors.ai.google.vertex_ai.services.utils import (
finish_reason_from_vertex_ai_to_semantic_kernel,
format_assistant_message,
format_tool_message,
format_user_message,
update_settings_from_function_choice_configuration,
)
from semantic_kernel.connectors.ai.google.vertex_ai.services.vertex_ai_base import VertexAIBase
from semantic_kernel.connectors.ai.google.vertex_ai.vertex_ai_prompt_execution_settings import (
VertexAIChatPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.google.vertex_ai.vertex_ai_settings import VertexAISettings
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.contents.chat_message_content import CMC_ITEM_TYPES, ChatMessageContent
from semantic_kernel.contents.function_call_content import FunctionCallContent
from semantic_kernel.contents.streaming_chat_message_content import STREAMING_CMC_ITEM_TYPES as STREAMING_ITEM_TYPES
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
from semantic_kernel.contents.streaming_text_content import StreamingTextContent
from semantic_kernel.contents.text_content import TextContent
from semantic_kernel.contents.utils.author_role import AuthorRole
from semantic_kernel.contents.utils.finish_reason import FinishReason
from semantic_kernel.exceptions.service_exceptions import (
ServiceInitializationError,
ServiceInvalidExecutionSettingsError,
)
from semantic_kernel.utils.telemetry.model_diagnostics.decorators import (
trace_chat_completion,
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."""
SUPPORTS_FUNCTION_CALLING: ClassVar[bool] = True
def __init__(
self,
project_id: str | None = None,
region: str | None = None,
gemini_model_id: str | None = None,
service_id: str | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> None:
"""Initialize the Google Vertex AI Chat Completion Service.
If no arguments are provided, the service will attempt to load the settings from the environment.
The following environment variables are used:
- VERTEX_AI_GEMINI_MODEL_ID
- VERTEX_AI_PROJECT_ID
- VERTEX_AI_REGION
Args:
project_id (str): The Google Cloud project ID.
region (str): The Google Cloud region.
gemini_model_id (str): The Gemini model ID.
service_id (str): The Vertex AI service ID.
env_file_path (str): The path to the environment file.
env_file_encoding (str): The encoding of the environment file.
"""
try:
vertex_ai_settings = VertexAISettings(
project_id=project_id,
region=region,
gemini_model_id=gemini_model_id,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
except ValidationError as e:
raise ServiceInitializationError(f"Failed to validate Vertex AI settings: {e}") from e
if not vertex_ai_settings.gemini_model_id:
raise ServiceInitializationError("The Vertex AI Gemini model ID is required.")
super().__init__(
ai_model_id=vertex_ai_settings.gemini_model_id,
service_id=service_id or vertex_ai_settings.gemini_model_id,
service_settings=vertex_ai_settings,
)
# region Overriding base class methods
# Override from AIServiceClientBase
@override
def get_prompt_execution_settings_class(self) -> type["PromptExecutionSettings"]:
return VertexAIChatPromptExecutionSettings
@override
@trace_chat_completion(VertexAIBase.MODEL_PROVIDER_NAME)
async def _inner_get_chat_message_contents(
self,
chat_history: "ChatHistory",
settings: "PromptExecutionSettings",
) -> list["ChatMessageContent"]:
if not isinstance(settings, VertexAIChatPromptExecutionSettings):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, VertexAIChatPromptExecutionSettings) # nosec
vertexai.init(project=self.service_settings.project_id, location=self.service_settings.region)
assert self.service_settings.gemini_model_id is not None # nosec
model = GenerativeModel(
self.service_settings.gemini_model_id,
system_instruction=filter_system_message(chat_history),
)
collapse_function_call_results_in_chat_history(chat_history)
response: GenerationResponse = await model.generate_content_async(
contents=self._prepare_chat_history_for_request(chat_history),
generation_config=settings.prepare_settings_dict(),
tools=settings.tools,
tool_config=settings.tool_config,
)
return [self._create_chat_message_content(response, candidate) for candidate in response.candidates]
@override
@trace_streaming_chat_completion(VertexAIBase.MODEL_PROVIDER_NAME)
async def _inner_get_streaming_chat_message_contents(
self,
chat_history: "ChatHistory",
settings: "PromptExecutionSettings",
function_invoke_attempt: int = 0,
) -> AsyncGenerator[list["StreamingChatMessageContent"], Any]:
if not isinstance(settings, VertexAIChatPromptExecutionSettings):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, VertexAIChatPromptExecutionSettings) # nosec
vertexai.init(project=self.service_settings.project_id, location=self.service_settings.region)
assert self.service_settings.gemini_model_id is not None # nosec
model = GenerativeModel(
self.service_settings.gemini_model_id,
system_instruction=filter_system_message(chat_history),
)
collapse_function_call_results_in_chat_history(chat_history)
response: AsyncIterable[GenerationResponse] = await model.generate_content_async(
contents=self._prepare_chat_history_for_request(chat_history),
generation_config=settings.prepare_settings_dict(),
tools=settings.tools,
tool_config=settings.tool_config,
stream=True,
)
async for chunk in response:
yield [
self._create_streaming_chat_message_content(chunk, candidate, function_invoke_attempt)
for candidate in chunk.candidates
]
@override
def _verify_function_choice_settings(self, settings: "PromptExecutionSettings") -> None:
if not isinstance(settings, VertexAIChatPromptExecutionSettings):
raise ServiceInvalidExecutionSettingsError("The settings must be an VertexAIChatPromptExecutionSettings.")
if settings.candidate_count is not None and settings.candidate_count > 1:
raise ServiceInvalidExecutionSettingsError(
"Auto-invocation of tool calls may only be used with a "
"VertexAIChatPromptExecutionSettings.candidate_count of 1."
)
@override
def _update_function_choice_settings_callback(
self,
) -> Callable[["FunctionCallChoiceConfiguration", "PromptExecutionSettings", FunctionChoiceType], None]:
return update_settings_from_function_choice_configuration
@override
def _reset_function_choice_settings(self, settings: "PromptExecutionSettings") -> None:
if hasattr(settings, "tool_config"):
settings.tool_config = None
if hasattr(settings, "tools"):
settings.tools = None
@override
def _prepare_chat_history_for_request(
self,
chat_history: ChatHistory,
role_key: str = "role",
content_key: str = "content",
) -> list[Content]:
chat_request_messages: list[Content] = []
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.
continue
if message.role == AuthorRole.USER:
chat_request_messages.append(Content(role="user", parts=format_user_message(message)))
elif message.role == AuthorRole.ASSISTANT:
chat_request_messages.append(Content(role="model", parts=format_assistant_message(message)))
elif message.role == AuthorRole.TOOL:
chat_request_messages.append(Content(role="function", parts=format_tool_message(message)))
return chat_request_messages
# endregion
# region Non-streaming
def _create_chat_message_content(self, response: GenerationResponse, candidate: Candidate) -> ChatMessageContent:
"""Create a chat message content object.
Args:
response: The response from the service.
candidate: The candidate from the response.
Returns:
A chat message content object.
"""
# Best effort conversion of finish reason. The raw value will be available in metadata.
finish_reason: FinishReason | None = finish_reason_from_vertex_ai_to_semantic_kernel(candidate.finish_reason)
response_metadata = self._get_metadata_from_response(response)
response_metadata.update(self._get_metadata_from_candidate(candidate))
items: list[CMC_ITEM_TYPES] = []
for idx, part in enumerate(candidate.content.parts):
part_dict = part.to_dict()
if "text" in part_dict:
items.append(TextContent(text=part.text, inner_content=response, metadata=response_metadata))
elif "function_call" in part_dict:
fc_metadata: dict[str, Any] = {}
thought_sig = part_dict.get("thought_signature")
if thought_sig:
fc_metadata["thought_signature"] = thought_sig
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()},
metadata=fc_metadata if fc_metadata else None,
)
)
return ChatMessageContent(
ai_model_id=self.ai_model_id,
role=AuthorRole.ASSISTANT,
items=items,
inner_content=response,
finish_reason=finish_reason,
metadata=response_metadata,
)
# endregion
# region Streaming
def _create_streaming_chat_message_content(
self,
chunk: GenerationResponse,
candidate: Candidate,
function_invoke_attempt: int,
) -> StreamingChatMessageContent:
"""Create a streaming chat message content object.
Args:
chunk: The response from the service.
candidate: The candidate from the response.
function_invoke_attempt: The function invoke attempt.
Returns:
A streaming chat message content object.
"""
# Best effort conversion of finish reason. The raw value will be available in metadata.
finish_reason: FinishReason | None = finish_reason_from_vertex_ai_to_semantic_kernel(candidate.finish_reason)
response_metadata = self._get_metadata_from_response(chunk)
response_metadata.update(self._get_metadata_from_candidate(candidate))
items: list[STREAMING_ITEM_TYPES] = []
for idx, part in enumerate(candidate.content.parts):
part_dict = part.to_dict()
if "text" in part_dict:
items.append(
StreamingTextContent(
choice_index=candidate.index,
text=part.text,
inner_content=chunk,
metadata=response_metadata,
)
)
elif "function_call" in part_dict:
fc_metadata_s: dict[str, Any] = {}
thought_sig_s = part_dict.get("thought_signature")
if thought_sig_s:
fc_metadata_s["thought_signature"] = thought_sig_s
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()},
metadata=fc_metadata_s if fc_metadata_s else None,
)
)
return StreamingChatMessageContent(
ai_model_id=self.ai_model_id,
role=AuthorRole.ASSISTANT,
choice_index=candidate.index,
items=items,
inner_content=chunk,
finish_reason=finish_reason,
metadata=response_metadata,
function_invoke_attempt=function_invoke_attempt,
)
# endregion
def _get_metadata_from_response(self, response: GenerationResponse) -> dict[str, Any]:
"""Get metadata from the response.
Args:
response: The response from the service.
Returns:
A dictionary containing metadata.
"""
return {
"prompt_feedback": response.prompt_feedback,
"usage": CompletionUsage(
prompt_tokens=response.usage_metadata.prompt_token_count,
completion_tokens=response.usage_metadata.candidates_token_count,
),
}
def _get_metadata_from_candidate(self, candidate: Candidate) -> dict[str, Any]:
"""Get metadata from the candidate.
Args:
candidate: The candidate from the response.
Returns:
A dictionary containing metadata.
"""
return {
"index": candidate.index,
"finish_reason": candidate.finish_reason,
"safety_ratings": candidate.safety_ratings,
}
@@ -0,0 +1,211 @@
# Copyright (c) Microsoft. All rights reserved.
import sys
from collections.abc import AsyncGenerator, AsyncIterable
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
from semantic_kernel.connectors.ai.google.vertex_ai.services.vertex_ai_base import VertexAIBase
from semantic_kernel.connectors.ai.google.vertex_ai.vertex_ai_prompt_execution_settings import (
VertexAITextPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.google.vertex_ai.vertex_ai_settings import VertexAISettings
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.connectors.ai.text_completion_client_base import TextCompletionClientBase
from semantic_kernel.contents.streaming_text_content import StreamingTextContent
from semantic_kernel.contents.text_content import TextContent
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError
from semantic_kernel.utils.telemetry.model_diagnostics.decorators import (
trace_streaming_text_completion,
trace_text_completion,
)
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
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."""
def __init__(
self,
project_id: str | None = None,
region: str | None = None,
gemini_model_id: str | None = None,
service_id: str | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> None:
"""Initialize the Google Vertex AI Text Completion Service.
If no arguments are provided, the service will attempt to load the settings from the environment.
The following environment variables are used:
- VERTEX_AI_GEMINI_MODEL_ID
- VERTEX_AI_PROJECT_ID
Args:
project_id (str): The Google Cloud project ID.
region (str): The Google Cloud region.
gemini_model_id (str): The Gemini model ID.
service_id (str): The Vertex AI service ID.
env_file_path (str): The path to the environment file.
env_file_encoding (str): The encoding of the environment file.
"""
try:
vertex_ai_settings = VertexAISettings(
project_id=project_id,
region=region,
gemini_model_id=gemini_model_id,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
except ValidationError as e:
raise ServiceInitializationError(f"Failed to validate Vertex AI settings: {e}") from e
if not vertex_ai_settings.gemini_model_id:
raise ServiceInitializationError("The Vertex AI Gemini model ID is required.")
super().__init__(
ai_model_id=vertex_ai_settings.gemini_model_id,
service_id=service_id or vertex_ai_settings.gemini_model_id,
service_settings=vertex_ai_settings,
)
# region Overriding base class methods
# Override from AIServiceClientBase
@override
def get_prompt_execution_settings_class(self) -> type["PromptExecutionSettings"]:
return VertexAITextPromptExecutionSettings
@override
@trace_text_completion(VertexAIBase.MODEL_PROVIDER_NAME)
async def _inner_get_text_contents(
self,
prompt: str,
settings: "PromptExecutionSettings",
) -> list[TextContent]:
if not isinstance(settings, VertexAITextPromptExecutionSettings):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, VertexAITextPromptExecutionSettings) # nosec
vertexai.init(project=self.service_settings.project_id, location=self.service_settings.region)
assert self.service_settings.gemini_model_id is not None # nosec
model = GenerativeModel(self.service_settings.gemini_model_id)
response: GenerationResponse = await model.generate_content_async(
contents=prompt,
generation_config=settings.prepare_settings_dict(),
)
return [self._create_text_content(response, candidate) for candidate in response.candidates]
@override
@trace_streaming_text_completion(VertexAIBase.MODEL_PROVIDER_NAME)
async def _inner_get_streaming_text_contents(
self,
prompt: str,
settings: "PromptExecutionSettings",
) -> AsyncGenerator[list[StreamingTextContent], Any]:
if not isinstance(settings, VertexAITextPromptExecutionSettings):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, VertexAITextPromptExecutionSettings) # nosec
vertexai.init(project=self.service_settings.project_id, location=self.service_settings.region)
assert self.service_settings.gemini_model_id is not None # nosec
model = GenerativeModel(self.service_settings.gemini_model_id)
response: AsyncIterable[GenerationResponse] = await model.generate_content_async(
contents=prompt,
generation_config=settings.prepare_settings_dict(),
stream=True,
)
async for chunk in response:
yield [self._create_streaming_text_content(chunk, candidate) for candidate in chunk.candidates]
# endregion
def _create_text_content(self, response: GenerationResponse, candidate: Candidate) -> TextContent:
"""Create a text content object.
Args:
response: The response from the service.
candidate: The candidate from the response.
Returns:
A text content object.
"""
response_metadata = self._get_metadata_from_response(response)
response_metadata.update(self._get_metadata_from_candidate(candidate))
return TextContent(
ai_model_id=self.ai_model_id,
text=candidate.content.parts[0].text,
inner_content=response,
metadata=response_metadata,
)
def _create_streaming_text_content(self, chunk: GenerationResponse, candidate: Candidate) -> StreamingTextContent:
"""Create a streaming text content object.
Args:
chunk: The response from the service.
candidate: The candidate from the response.
Returns:
A streaming text content object.
"""
response_metadata = self._get_metadata_from_response(chunk)
response_metadata.update(self._get_metadata_from_candidate(candidate))
return StreamingTextContent(
ai_model_id=self.ai_model_id,
choice_index=candidate.index,
text=candidate.content.parts[0].text,
inner_content=chunk,
metadata=response_metadata,
)
def _get_metadata_from_response(self, response: GenerationResponse) -> dict[str, Any]:
"""Get metadata from the response.
Args:
response: The response from the service.
Returns:
A dictionary containing metadata.
"""
return {
"prompt_feedback": response.prompt_feedback,
"usage": CompletionUsage(
prompt_tokens=response.usage_metadata.prompt_token_count,
completion_tokens=response.usage_metadata.candidates_token_count,
),
}
def _get_metadata_from_candidate(self, candidate: Candidate) -> dict[str, Any]:
"""Get metadata from the candidate.
Args:
candidate: The candidate from the response.
Returns:
A dictionary containing metadata.
"""
return {
"index": candidate.index,
"finish_reason": candidate.finish_reason,
"safety_ratings": candidate.safety_ratings,
}
@@ -0,0 +1,116 @@
# Copyright (c) Microsoft. All rights reserved.
import sys
from typing import Any
import vertexai
from numpy import array, ndarray
from pydantic import ValidationError
from vertexai.language_models import TextEmbedding, TextEmbeddingModel
from semantic_kernel.connectors.ai.embedding_generator_base import EmbeddingGeneratorBase
from semantic_kernel.connectors.ai.google.vertex_ai.services.vertex_ai_base import VertexAIBase
from semantic_kernel.connectors.ai.google.vertex_ai.vertex_ai_prompt_execution_settings import (
VertexAIEmbeddingPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.google.vertex_ai.vertex_ai_settings import VertexAISettings
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
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."""
def __init__(
self,
project_id: str | None = None,
region: str | None = None,
embedding_model_id: str | None = None,
service_id: str | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> None:
"""Initialize the Google Vertex AI Chat Completion Service.
If no arguments are provided, the service will attempt to load the settings from the environment.
The following environment variables are used:
- VERTEX_AI_EMBEDDING_MODEL_ID
- VERTEX_AI_PROJECT_ID
Args:
project_id (str): The Google Cloud project ID.
region (str): The Google Cloud region.
embedding_model_id (str): The Gemini model ID.
service_id (str): The Vertex AI service ID.
env_file_path (str): The path to the environment file.
env_file_encoding (str): The encoding of the environment file.
"""
try:
vertex_ai_settings = VertexAISettings(
project_id=project_id,
region=region,
embedding_model_id=embedding_model_id,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
except ValidationError as e:
raise ServiceInitializationError(f"Failed to validate Vertex AI settings: {e}") from e
if not vertex_ai_settings.embedding_model_id:
raise ServiceInitializationError("The Vertex AI embedding model ID is required.")
super().__init__(
ai_model_id=vertex_ai_settings.embedding_model_id,
service_id=service_id or vertex_ai_settings.embedding_model_id,
service_settings=vertex_ai_settings,
)
@override
async def generate_embeddings(
self,
texts: list[str],
settings: "PromptExecutionSettings | None" = None,
**kwargs: Any,
) -> ndarray:
raw_embeddings = await self.generate_raw_embeddings(texts, settings, **kwargs)
return array(raw_embeddings)
@override
async def generate_raw_embeddings(
self,
texts: list[str],
settings: "PromptExecutionSettings | None" = None,
**kwargs: Any,
) -> list[list[float]]:
if not settings:
settings = VertexAIEmbeddingPromptExecutionSettings()
else:
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, VertexAIEmbeddingPromptExecutionSettings) # nosec
vertexai.init(project=self.service_settings.project_id, location=self.service_settings.region)
assert self.service_settings.embedding_model_id is not None # nosec
model = TextEmbeddingModel.from_pretrained(self.service_settings.embedding_model_id)
response: list[TextEmbedding] = await model.get_embeddings_async(
texts, # type: ignore[arg-type]
**settings.prepare_settings_dict(),
)
return [text_embedding.values for text_embedding in response]
@override
def get_prompt_execution_settings_class(
self,
) -> type["PromptExecutionSettings"]:
"""Get the request settings class."""
return VertexAIEmbeddingPromptExecutionSettings
@@ -0,0 +1,89 @@
# Copyright (c) Microsoft. All rights reserved.
import sys
from typing import Annotated, Any, Literal
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
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."""
stop_sequences: Annotated[list[str] | None, Field(max_length=5)] = None
response_mime_type: Literal["text/plain", "application/json"] | None = None
response_schema: Any | None = None
candidate_count: Annotated[int | None, Field(ge=1)] = None
max_output_tokens: Annotated[int | None, Field(ge=1)] = None
temperature: Annotated[float | None, Field(ge=0.0, le=2.0)] = None
top_p: float | None = None
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."""
tools: Annotated[
list[Tool] | None,
Field(
description="Do not set this manually. It is set by the service based "
"on the function choice configuration.",
),
] = None
tool_config: Annotated[
ToolConfig | None,
Field(
description="Do not set this manually. It is set by the service based "
"on the function choice configuration.",
),
] = 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_config keys from the settings dictionary, as
the Vertex 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
@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."""
auto_truncate: bool | None = None
output_dimensionality: int | None = None
@@ -0,0 +1,40 @@
# Copyright (c) Microsoft. All rights reserved.
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.
The settings are first loaded from environment variables with
the prefix 'VERTEX_AI_'.
If the environment variables are not found, the settings can
be loaded from a .env file with the encoding 'utf-8'.
If the settings are not found in the .env file, the settings
are ignored; however, validation will fail alerting that the
settings are missing.
Required settings for prefix 'VERTEX_AI_' are:
- gemini_model_id: str - The Gemini model ID for the Vertex AI service, i.e. gemini-1.5-pro
This value can be found in the Vertex AI service deployment.
(Env var VERTEX_AI_GEMINI_MODEL_ID)
- embedding_model_id: str - The embedding model ID for the Vertex AI service, i.e. text-embedding-004
This value can be found in the Vertex AI service deployment.
(Env var VERTEX_AI_EMBEDDING_MODEL_ID)
- project_id: str - The Google Cloud project ID.
(Env var VERTEX_AI_PROJECT_ID)
- region: str - The Google Cloud region.
(Env var VERTEX_AI_REGION)
"""
env_prefix: ClassVar[str] = "VERTEX_AI_"
gemini_model_id: str | None = None
embedding_model_id: str | None = None
project_id: str
region: str | None = None