chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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",
|
||||
]
|
||||
+51
@@ -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
|
||||
+429
@@ -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,
|
||||
}
|
||||
+262
@@ -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,
|
||||
}
|
||||
+149
@@ -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
|
||||
+375
@@ -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,
|
||||
}
|
||||
+211
@@ -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,
|
||||
}
|
||||
+116
@@ -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
|
||||
+89
@@ -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
|
||||
Reference in New Issue
Block a user