chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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"
|
||||
)
|
||||
Reference in New Issue
Block a user