mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Add GeminiChatClient (#4847)
* Add agent-framework-gemini package * Add AGENTS.md documentation * Add LICENSE file * Add README.md for agent-framework-gemini package * Add Google Gemini API keys to .env.example * Add Google Gemini chat client implementation * Add tests for GeminiChatClient * Add Google Gemini agent examples * Fix client inheritence order * Update Gemini agent examples * Update documentation * Update AGENTS.md * Add tests for JSON string handling in GeminiChatClient * Add final response assembly test in GeminiChatClient * Add tests for handling empty candidates in GeminiChatClient * Improve Pydantic response handling in GeminiChatClient * Add tests for function result resolution and callable tool normalization * Add test for function result resolution when call_id is generated * Refactor GeminiChatClient to correct inheritance order Also updates constructor parameter order for environment file handling * Enhance documentation and clarify Gemini-specific fields * Update ThinkingConfig with new attributes and type * Add tests for GoogleSearch and GoogleMaps configs * Suppress valid-type mypy error on GeminiChatOptionsT * Move service_url method near overrides * Order _prepare_config kwargs by base then Gemini-specific * Use FunctionCallingConfigMode for clarity and type safety * Fix code_execution doc * Add agent-framework-gemini to project dependencies * Remove package from core dependencies Initial release will be done without agent-framework-gemini in core[all]. * Move integration tests into one file * Remove __init__.py file from gemini tests directory * Introduce RawGeminiChatClient as lightweight chat client Updated GeminiChatClient to inherit from RawGeminiChatClient, maintaining full functionality with added features. * Updated variable names from `model_id` to `model` Across the codebase, including environment variables and client initialization. Adjusted related tests and sample scripts to reflect this change, ensuring consistency in the usage of the Gemini model identifier. * Update AGENTS.md * Update Gemini package to alpha status * Fix docstrings in Gemini tests * Change 'model_id' to 'model' in response handling * Fix model property change in response handling * Add built-in tool factory methods to Gemini client Replaces boolean tool options (code_execution, google_search_grounding, google_maps_grounding) with static factory methods that return types.Tool objects: get_code_interpreter_tool, get_web_search_tool, get_mcp_tool, get_file_search_tool, and get_maps_grounding_tool. Simplifies _prepare_tools to a single translation boundary between FunctionTool (framework) and FunctionDeclaration (Gemini API), with types.Tool objects passed through unchanged. * Surface code execution parts _parse_parts now maps executable_code and code_execution_result parts to text Content objects so callers can see the code run and its output. Unknown part types log at debug level rather than being silently dropped. * Update Gemini client documentation * Unify Gemini model name Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com> * Update Agent Framework core version Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com> * Add Python 3.14 in classifiers * Replace kwargs with parameters in tool factories * Refactor chat options handling in Gemini client * Add tests for handling unknown and consumed keys * Update Gemini documentation Now reflects new options and built-in tool factory methods * Change build system to flit Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com> * Fix build system in pyproject.toml * Fix type checking for generate_content_stream --------- Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
64c68ca857
commit
485af07b8c
@@ -38,6 +38,9 @@ COPILOTSTUDIOAGENT__AGENTAPPID=""
|
||||
# Anthropic
|
||||
ANTHROPIC_API_KEY=""
|
||||
ANTHROPIC_MODEL=""
|
||||
# Google Gemini
|
||||
GEMINI_API_KEY=""
|
||||
GEMINI_MODEL=""
|
||||
# Ollama
|
||||
OLLAMA_ENDPOINT=""
|
||||
OLLAMA_MODEL=""
|
||||
|
||||
@@ -31,6 +31,7 @@ Status is grouped into these buckets:
|
||||
| `agent-framework-durabletask` | `python/packages/durabletask` | `beta` |
|
||||
| `agent-framework-foundry` | `python/packages/foundry` | `released` |
|
||||
| `agent-framework-foundry-local` | `python/packages/foundry_local` | `beta` |
|
||||
| `agent-framework-gemini` | `python/packages/gemini` | `alpha` |
|
||||
| `agent-framework-github-copilot` | `python/packages/github_copilot` | `beta` |
|
||||
| `agent-framework-lab` | `python/packages/lab` | `beta` |
|
||||
| `agent-framework-mem0` | `python/packages/mem0` | `beta` |
|
||||
|
||||
@@ -34,14 +34,13 @@ all = [
|
||||
"mcp>=1.24.0,<2",
|
||||
"agent-framework-a2a",
|
||||
"agent-framework-ag-ui",
|
||||
"agent-framework-anthropic",
|
||||
"agent-framework-azure-ai-search",
|
||||
"agent-framework-azure-cosmos",
|
||||
"agent-framework-anthropic",
|
||||
"agent-framework-openai",
|
||||
"agent-framework-claude",
|
||||
"agent-framework-azurefunctions",
|
||||
"agent-framework-bedrock",
|
||||
"agent-framework-chatkit",
|
||||
"agent-framework-claude",
|
||||
"agent-framework-copilotstudio",
|
||||
"agent-framework-declarative",
|
||||
"agent-framework-devui",
|
||||
@@ -52,6 +51,7 @@ all = [
|
||||
"agent-framework-lab",
|
||||
"agent-framework-mem0",
|
||||
"agent-framework-ollama",
|
||||
"agent-framework-openai",
|
||||
"agent-framework-orchestrations",
|
||||
"agent-framework-purview",
|
||||
"agent-framework-redis",
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# Gemini Package (agent-framework-gemini)
|
||||
|
||||
Integration with Google's Gemini API via the `google-genai` SDK.
|
||||
|
||||
## Core Classes
|
||||
|
||||
- **`RawGeminiChatClient`** - Lightweight chat client without any layers, for custom pipeline composition
|
||||
- **`GeminiChatClient`** - Full-featured chat client with function invocation, middleware, and telemetry
|
||||
- **`GeminiChatOptions`** - Options TypedDict for Gemini-specific parameters
|
||||
- **`GeminiSettings`** - Settings loaded from environment variables
|
||||
- **`ThinkingConfig`** - Configuration for extended thinking
|
||||
|
||||
## Gemini-specific Options
|
||||
|
||||
- **`thinking_config`** - Enable extended thinking via `ThinkingConfig`
|
||||
- **`response_schema`** - Raw JSON schema dict for structured output (alternative to `response_format`)
|
||||
- **`top_k`** - Top-K sampling parameter
|
||||
|
||||
## Built-in Tool Factory Methods
|
||||
|
||||
- **`get_web_search_tool()`** - Google Search grounding for up-to-date web answers
|
||||
- **`get_code_interpreter_tool()`** - Sandboxed code execution
|
||||
- **`get_maps_grounding_tool()`** - Google Maps grounding for location and mapping
|
||||
- **`get_file_search_tool()`** - Retrieval from Gemini file search stores
|
||||
- **`get_mcp_tool()`** - Model Context Protocol server integration
|
||||
|
||||
## Usage
|
||||
|
||||
```python
|
||||
from agent_framework import Content, Message
|
||||
from agent_framework_gemini import GeminiChatClient
|
||||
|
||||
client = GeminiChatClient(model="gemini-2.5-flash")
|
||||
response = await client.get_response([Message(role="user", contents=[Content.from_text("Hello")])])
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE
|
||||
@@ -0,0 +1,30 @@
|
||||
# Get Started with Microsoft Agent Framework Gemini
|
||||
|
||||
Install the provider package:
|
||||
|
||||
```bash
|
||||
pip install agent-framework-gemini --pre
|
||||
```
|
||||
|
||||
## Gemini Integration
|
||||
|
||||
The Gemini integration enables Microsoft Agent Framework applications to call Google Gemini models with familiar chat abstractions, including streaming, tool/function calling, and structured output.
|
||||
|
||||
## Authentication
|
||||
|
||||
Obtain an API key from [Google AI Studio](https://aistudio.google.com/apikey) and set it via environment variable:
|
||||
|
||||
```bash
|
||||
export GEMINI_API_KEY="your-api-key"
|
||||
export GEMINI_MODEL="gemini-2.5-flash"
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
See the [Google Gemini samples](samples/) for runnable end-to-end scripts covering:
|
||||
|
||||
- Basic agent with tool calling and streaming
|
||||
- Extended thinking with `ThinkingConfig`
|
||||
- Google Search grounding
|
||||
- Google Maps grounding
|
||||
- Built-in code execution
|
||||
@@ -0,0 +1,19 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import importlib.metadata
|
||||
|
||||
from ._chat_client import GeminiChatClient, GeminiChatOptions, GeminiSettings, RawGeminiChatClient, ThinkingConfig
|
||||
|
||||
try:
|
||||
__version__ = importlib.metadata.version(__name__)
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
__version__ = "0.0.0"
|
||||
|
||||
__all__ = [
|
||||
"GeminiChatClient",
|
||||
"GeminiChatOptions",
|
||||
"GeminiSettings",
|
||||
"RawGeminiChatClient",
|
||||
"ThinkingConfig",
|
||||
"__version__",
|
||||
]
|
||||
@@ -0,0 +1,939 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
from collections.abc import AsyncIterable, Awaitable, Mapping, Sequence
|
||||
from typing import Any, ClassVar, Generic, cast
|
||||
from uuid import uuid4
|
||||
|
||||
from agent_framework import (
|
||||
AGENT_FRAMEWORK_USER_AGENT,
|
||||
BaseChatClient,
|
||||
ChatAndFunctionMiddlewareTypes,
|
||||
ChatMiddlewareLayer,
|
||||
ChatOptions,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
Content,
|
||||
FinishReasonLiteral,
|
||||
FunctionInvocationConfiguration,
|
||||
FunctionInvocationLayer,
|
||||
FunctionTool,
|
||||
Message,
|
||||
ResponseStream,
|
||||
UsageDetails,
|
||||
validate_tool_mode,
|
||||
)
|
||||
from agent_framework._settings import SecretString, load_settings
|
||||
from agent_framework.observability import ChatTelemetryLayer
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
from pydantic import BaseModel
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
from typing import TypeVar # type: ignore # pragma: no cover
|
||||
else:
|
||||
from typing_extensions import TypeVar # type: ignore # pragma: no cover
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from typing import override # type: ignore # pragma: no cover
|
||||
else:
|
||||
from typing_extensions import override # type: ignore # pragma: no cover
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import TypedDict # type: ignore # pragma: no cover
|
||||
else:
|
||||
from typing_extensions import TypedDict # type: ignore # pragma: no cover
|
||||
|
||||
logger = logging.getLogger("agent_framework.gemini")
|
||||
|
||||
__all__ = [
|
||||
"GeminiChatClient",
|
||||
"GeminiChatOptions",
|
||||
"GeminiSettings",
|
||||
"RawGeminiChatClient",
|
||||
"ThinkingConfig",
|
||||
]
|
||||
|
||||
ResponseModelT = TypeVar("ResponseModelT", bound=BaseModel | None, default=None)
|
||||
|
||||
|
||||
# region Options & Settings
|
||||
|
||||
|
||||
class ThinkingConfig(TypedDict, total=False):
|
||||
"""Extended thinking configuration for Gemini models.
|
||||
|
||||
Attributes:
|
||||
include_thoughts: Whether to include thought summaries in the response. Thought summaries
|
||||
are condensed representations of the model's internal reasoning and appear as response
|
||||
parts where ``part.thought`` is ``True``. Note: the framework currently excludes
|
||||
thought parts from ``ChatResponse.contents`` and does not surface them as output.
|
||||
thinking_budget: Token budget for Gemini 2.5 models. Set to ``0`` to disable
|
||||
thinking or ``-1`` to enable a dynamic budget.
|
||||
thinking_level: Thinking level for Gemini 2.5 models and later. One of
|
||||
``ThinkingLevel.THINKING_LEVEL_UNSPECIFIED`` (default), ``ThinkingLevel.MINIMAL``,
|
||||
``ThinkingLevel.LOW``, ``ThinkingLevel.MEDIUM``, or ``ThinkingLevel.HIGH``.
|
||||
"""
|
||||
|
||||
include_thoughts: bool
|
||||
thinking_budget: int
|
||||
thinking_level: types.ThinkingLevel
|
||||
|
||||
|
||||
class GeminiChatOptions(ChatOptions[ResponseModelT], Generic[ResponseModelT], total=False):
|
||||
"""Google Gemini API-specific chat options.
|
||||
|
||||
Extends ``ChatOptions`` with Gemini-specific fields. Standard options are mapped to their
|
||||
``GenerateContentConfig`` equivalents; Gemini-specific fields are declared below.
|
||||
|
||||
Only text output is supported for now. Other modalities may be added later.
|
||||
|
||||
See: https://ai.google.dev/api/generate-content#generationconfig
|
||||
|
||||
Inherited fields from ``ChatOptions``:
|
||||
model: Model to use for this call (e.g. ``"gemini-2.5-flash"``).
|
||||
temperature: Controls randomness. Higher values produce more varied output.
|
||||
max_tokens: Maximum number of tokens to generate (``maxOutputTokens``).
|
||||
top_p: Nucleus sampling cutoff. Only tokens within the top-p probability mass are considered.
|
||||
stop: One or more sequences that stop generation when encountered (``stopSequences``).
|
||||
seed: Fixed seed for reproducible outputs.
|
||||
frequency_penalty: Reduces repetition by penalising tokens that appear frequently.
|
||||
presence_penalty: Reduces repetition by penalising tokens that have already appeared.
|
||||
tools: Function tools the model may call. Accepts ``FunctionTool`` instances, plain callables,
|
||||
or ``types.Tool`` objects returned by ``get_code_interpreter_tool``, ``get_web_search_tool``,
|
||||
``get_mcp_tool``, ``get_file_search_tool``, or ``get_maps_grounding_tool``.
|
||||
tool_choice: How the model picks a tool. One of ``'auto'``, ``'none'``, or ``'required'``.
|
||||
response_format: Pydantic model type for structured JSON output. The response text is
|
||||
parsed into the model and exposed via ``ChatResponse.value``.
|
||||
instructions: Extra system-level instructions prepended to the system message.
|
||||
|
||||
Not supported, and passing these raises a type error:
|
||||
- ``logit_bias``
|
||||
- ``allow_multiple_tool_calls``
|
||||
- ``store``
|
||||
- ``user``
|
||||
- ``metadata``
|
||||
- ``conversation_id``
|
||||
"""
|
||||
|
||||
# Gemini's GenerationConfig options
|
||||
response_schema: dict[str, Any]
|
||||
"""Raw JSON schema dict for structured output (alternative to ``response_format``).
|
||||
Sets ``response_mime_type`` to ``'application/json'`` and passes the schema directly."""
|
||||
|
||||
top_k: int
|
||||
"""Top-K sampling: limits token selection to the K most probable tokens."""
|
||||
|
||||
thinking_config: ThinkingConfig
|
||||
"""Extended thinking configuration. See ``ThinkingConfig`` for available fields."""
|
||||
|
||||
# Unsupported base options. Override with None to indicate not supported
|
||||
logit_bias: None # type: ignore[misc]
|
||||
"""Not supported in the Gemini API."""
|
||||
|
||||
allow_multiple_tool_calls: None # type: ignore[misc]
|
||||
"""Not supported. Gemini handles parallel tool calls automatically."""
|
||||
|
||||
store: None # type: ignore[misc]
|
||||
"""Not supported in the Gemini API."""
|
||||
|
||||
user: None # type: ignore[misc]
|
||||
"""Not supported in the Gemini API."""
|
||||
|
||||
metadata: None # type: ignore[misc]
|
||||
"""Not supported in the Gemini API."""
|
||||
|
||||
conversation_id: None # type: ignore[misc]
|
||||
"""Not supported in the Gemini API."""
|
||||
|
||||
|
||||
GeminiChatOptionsT = TypeVar("GeminiChatOptionsT", bound=TypedDict, default="GeminiChatOptions", covariant=True) # type: ignore[valid-type]
|
||||
|
||||
|
||||
class GeminiSettings(TypedDict, total=False):
|
||||
"""Gemini configuration settings loaded from environment or .env files."""
|
||||
|
||||
api_key: SecretString | None
|
||||
model: str | None
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
_GEMINI_SERVICE_URL = "https://generativelanguage.googleapis.com"
|
||||
|
||||
# Keys mapping to a different GenerateContentConfig field name
|
||||
_OPTION_TRANSLATIONS: dict[str, str] = {
|
||||
"max_tokens": "max_output_tokens",
|
||||
"stop": "stop_sequences",
|
||||
}
|
||||
|
||||
# Keys handled with dedicated logic, not via the generic passthrough
|
||||
_OPTION_EXPLICIT_KEYS: frozenset[str] = frozenset({
|
||||
"tools",
|
||||
"tool_choice",
|
||||
"response_format",
|
||||
"response_schema",
|
||||
"thinking_config",
|
||||
})
|
||||
|
||||
# Keys consumed upstream and not forwarded to GenerateContentConfig
|
||||
_OPTION_CONSUMED_KEYS: frozenset[str] = frozenset({
|
||||
"model",
|
||||
"instructions",
|
||||
})
|
||||
|
||||
_OPTION_EXCLUDE_KEYS: frozenset[str] = _OPTION_EXPLICIT_KEYS | _OPTION_CONSUMED_KEYS
|
||||
|
||||
_FINISH_REASON_MAP: dict[str, FinishReasonLiteral] = {
|
||||
"STOP": "stop",
|
||||
"MAX_TOKENS": "length",
|
||||
"SAFETY": "content_filter",
|
||||
"RECITATION": "content_filter",
|
||||
"LANGUAGE": "content_filter",
|
||||
"BLOCKLIST": "content_filter",
|
||||
"PROHIBITED_CONTENT": "content_filter",
|
||||
"SPII": "content_filter",
|
||||
"IMAGE_SAFETY": "content_filter",
|
||||
"IMAGE_PROHIBITED_CONTENT": "content_filter",
|
||||
"IMAGE_RECITATION": "content_filter",
|
||||
"MALFORMED_FUNCTION_CALL": "tool_calls",
|
||||
"UNEXPECTED_TOOL_CALL": "tool_calls",
|
||||
}
|
||||
|
||||
|
||||
class RawGeminiChatClient(
|
||||
BaseChatClient[GeminiChatOptionsT],
|
||||
Generic[GeminiChatOptionsT],
|
||||
):
|
||||
"""A raw Gemini chat client for the Google Gemini API without function invocation, middleware or telemetry.
|
||||
|
||||
Use this when you want full control over the request pipeline. For instance, to opt out of
|
||||
telemetry, use custom middleware, or compose your own layers. If you want the full-featured
|
||||
client with batteries included, use `GeminiChatClient` instead.
|
||||
"""
|
||||
|
||||
OTEL_PROVIDER_NAME: ClassVar[str] = "gcp.gemini" # type: ignore[reportIncompatibleVariableOverride, misc]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
api_key: str | None = None,
|
||||
model: str | None = None,
|
||||
env_file_path: str | None = None,
|
||||
env_file_encoding: str | None = None,
|
||||
client: genai.Client | None = None,
|
||||
additional_properties: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""Create a raw Gemini chat client.
|
||||
|
||||
Args:
|
||||
api_key: Google AI Studio API key. Falls back to ``GEMINI_API_KEY`` environment variable.
|
||||
model: Default model identifier. Falls back to ``GEMINI_MODEL`` environment variable.
|
||||
env_file_path: Path to a ``.env`` file for credential loading.
|
||||
env_file_encoding: Encoding for the ``.env`` file.
|
||||
client: Pre-built ``genai.Client`` instance. When provided, ``api_key`` is not required.
|
||||
additional_properties: Extra properties stored on the client instance.
|
||||
"""
|
||||
settings = load_settings(
|
||||
GeminiSettings,
|
||||
env_prefix="GEMINI_",
|
||||
api_key=api_key,
|
||||
model=model,
|
||||
env_file_path=env_file_path,
|
||||
env_file_encoding=env_file_encoding,
|
||||
)
|
||||
|
||||
if client:
|
||||
self._genai_client = client
|
||||
else:
|
||||
resolved_key = settings.get("api_key")
|
||||
if not resolved_key:
|
||||
raise ValueError(
|
||||
"Gemini API key is required. Set via api_key parameter or GEMINI_API_KEY environment variable."
|
||||
)
|
||||
self._genai_client = genai.Client(
|
||||
api_key=resolved_key.get_secret_value(),
|
||||
http_options={"headers": {"x-goog-api-client": AGENT_FRAMEWORK_USER_AGENT}},
|
||||
)
|
||||
|
||||
self.model = settings.get("model")
|
||||
|
||||
super().__init__(additional_properties=additional_properties)
|
||||
|
||||
@staticmethod
|
||||
def get_code_interpreter_tool() -> types.Tool:
|
||||
"""Create a code execution tool.
|
||||
|
||||
Pass the returned tool to the ``tools`` list of an agent or ``ChatOptions``.
|
||||
|
||||
Returns:
|
||||
A ``types.Tool`` configured for sandboxed code execution.
|
||||
"""
|
||||
return types.Tool(code_execution=types.ToolCodeExecution())
|
||||
|
||||
@staticmethod
|
||||
def get_web_search_tool(
|
||||
*,
|
||||
search_types: types.SearchTypes | None = None,
|
||||
blocking_confidence: types.PhishBlockThreshold | None = None,
|
||||
exclude_domains: list[str] | None = None,
|
||||
time_range_filter: types.Interval | None = None,
|
||||
) -> types.Tool:
|
||||
"""Create a Google Search grounding tool.
|
||||
|
||||
Pass the returned tool to the ``tools`` list of an agent or ``ChatOptions``.
|
||||
|
||||
Args:
|
||||
search_types: Controls which search types are enabled (web search, image search).
|
||||
blocking_confidence: Block sites at or above this phishing confidence level.
|
||||
Not supported in Gemini API.
|
||||
exclude_domains: List of domains to exclude from search results. Not supported in Gemini API.
|
||||
time_range_filter: Restrict results to a specific time range. Not supported in Vertex AI.
|
||||
|
||||
Returns:
|
||||
A ``types.Tool`` configured for Google Search grounding.
|
||||
"""
|
||||
return types.Tool(
|
||||
google_search=types.GoogleSearch(
|
||||
search_types=search_types,
|
||||
blocking_confidence=blocking_confidence,
|
||||
exclude_domains=exclude_domains,
|
||||
time_range_filter=time_range_filter,
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_mcp_tool(url: str, *, name: str | None = None, **kwargs: Any) -> types.Tool:
|
||||
"""Create an MCP (Model Context Protocol) server tool.
|
||||
|
||||
Pass the returned tool to the ``tools`` list of an agent or ``ChatOptions``.
|
||||
|
||||
Args:
|
||||
url: The URL of the MCP server's streamable HTTP endpoint.
|
||||
name: Optional display name for the MCP server.
|
||||
**kwargs: Additional kwargs passed to ``StreamableHttpTransport``. Supported fields
|
||||
include ``headers``, ``timeout``, ``sse_read_timeout``, and ``terminate_on_close``.
|
||||
|
||||
Returns:
|
||||
A ``types.Tool`` configured for the given MCP server.
|
||||
"""
|
||||
return types.Tool(
|
||||
mcp_servers=[
|
||||
types.McpServer(
|
||||
name=name,
|
||||
streamable_http_transport=types.StreamableHttpTransport(url=url, **kwargs),
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_file_search_tool(
|
||||
*,
|
||||
file_search_store_names: list[str] | None = None,
|
||||
top_k: int | None = None,
|
||||
metadata_filter: str | None = None,
|
||||
) -> types.Tool:
|
||||
"""Create a file search tool backed by a Gemini file search store.
|
||||
|
||||
Pass the returned tool to the ``tools`` list of an agent or ``ChatOptions``.
|
||||
|
||||
Args:
|
||||
file_search_store_names: Resource names of the file search stores to query.
|
||||
Example: ``["fileSearchStores/my-file-search-store-123"]``.
|
||||
top_k: Maximum number of retrieval chunks to return.
|
||||
metadata_filter: CEL expression to filter retrieval results by metadata.
|
||||
See https://google.aip.dev/160 for syntax.
|
||||
|
||||
Returns:
|
||||
A ``types.Tool`` configured for file search retrieval.
|
||||
"""
|
||||
return types.Tool(
|
||||
file_search=types.FileSearch(
|
||||
file_search_store_names=file_search_store_names,
|
||||
top_k=top_k,
|
||||
metadata_filter=metadata_filter,
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_maps_grounding_tool(
|
||||
*,
|
||||
enable_widget: bool | None = None,
|
||||
auth_config: types.AuthConfig | None = None,
|
||||
) -> types.Tool:
|
||||
"""Create a Google Maps grounding tool.
|
||||
|
||||
Pass the returned tool to the ``tools`` list of an agent or ``ChatOptions``.
|
||||
|
||||
Args:
|
||||
enable_widget: Return a widget context token in ``GroundingMetadata`` so callers
|
||||
can render a Google Maps widget with geospatial context.
|
||||
auth_config: Authentication config to access the Maps API. Only API key is
|
||||
supported. Not supported in Gemini API.
|
||||
|
||||
Returns:
|
||||
A ``types.Tool`` configured for Google Maps grounding.
|
||||
"""
|
||||
return types.Tool(google_maps=types.GoogleMaps(enable_widget=enable_widget, auth_config=auth_config))
|
||||
|
||||
@override
|
||||
def _inner_get_response(
|
||||
self,
|
||||
*,
|
||||
messages: Sequence[Message],
|
||||
options: Mapping[str, Any],
|
||||
stream: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> Awaitable[ChatResponse] | ResponseStream[ChatResponseUpdate, ChatResponse]:
|
||||
if stream:
|
||||
|
||||
async def _stream() -> AsyncIterable[ChatResponseUpdate]:
|
||||
validated = await self._validate_options(options)
|
||||
model, contents, config = self._prepare_request(messages, validated)
|
||||
async for chunk in await self._genai_client.aio.models.generate_content_stream( # pyright: ignore[reportUnknownMemberType]
|
||||
model=model,
|
||||
contents=contents, # type: ignore[arg-type]
|
||||
config=config,
|
||||
):
|
||||
yield self._process_chunk(chunk)
|
||||
|
||||
return self._build_response_stream(_stream(), response_format=options.get("response_format"))
|
||||
|
||||
async def _get_response() -> ChatResponse:
|
||||
validated = await self._validate_options(options)
|
||||
model, contents, config = self._prepare_request(messages, validated)
|
||||
raw = await self._genai_client.aio.models.generate_content(model=model, contents=contents, config=config) # type: ignore[arg-type]
|
||||
return self._process_generate_response(raw, response_format=validated.get("response_format"))
|
||||
|
||||
return _get_response()
|
||||
|
||||
@override
|
||||
def service_url(self) -> str:
|
||||
"""Return the base URL of the Gemini API service.
|
||||
|
||||
Returns:
|
||||
The Gemini API base URL.
|
||||
"""
|
||||
return _GEMINI_SERVICE_URL
|
||||
|
||||
# region Request preparation
|
||||
|
||||
def _prepare_request(
|
||||
self,
|
||||
messages: Sequence[Message],
|
||||
options: Mapping[str, Any],
|
||||
) -> tuple[str, list[types.Content], types.GenerateContentConfig]:
|
||||
"""Resolve the model ID, convert messages to Gemini contents, and build the generation config.
|
||||
|
||||
Call this after awaiting ``_validate_options`` so that tools and other options are
|
||||
fully normalized before the request is assembled.
|
||||
|
||||
Args:
|
||||
messages: The conversation history as framework Message objects.
|
||||
options: Validated and normalized chat options.
|
||||
|
||||
Returns:
|
||||
A tuple of the resolved model, the Gemini contents list, and the generation config.
|
||||
|
||||
Raises:
|
||||
ValueError: If no model is set on the options or the client instance.
|
||||
"""
|
||||
model = options.get("model") or self.model
|
||||
if not model:
|
||||
raise ValueError("Gemini model is required. Set via model parameter or GEMINI_MODEL environment variable.")
|
||||
|
||||
system_instruction, contents = self._prepare_gemini_messages(messages)
|
||||
if call_instructions := options.get("instructions"):
|
||||
system_instruction = (
|
||||
f"{call_instructions}\n{system_instruction}" if system_instruction else call_instructions
|
||||
)
|
||||
|
||||
return model, contents, self._prepare_config(options, system_instruction)
|
||||
|
||||
def _prepare_gemini_messages(self, messages: Sequence[Message]) -> tuple[str | None, list[types.Content]]:
|
||||
"""Convert framework messages to Gemini contents and extract system instruction.
|
||||
|
||||
Args:
|
||||
messages: The full conversation history as framework Message objects.
|
||||
|
||||
Returns:
|
||||
A tuple of (system_instruction_text, contents_list). System messages are extracted
|
||||
into the instruction string; tool results are grouped into user-role content blocks.
|
||||
"""
|
||||
system_parts: list[str] = []
|
||||
contents: list[types.Content] = []
|
||||
# Maps call_id to function name so function_result parts can include the required name field.
|
||||
call_id_to_name: dict[str, str] = {}
|
||||
# Accumulated functionResponse parts from consecutive tool messages.
|
||||
pending_tool_parts: list[types.Part] = []
|
||||
|
||||
def flush_pending_tool_parts() -> None:
|
||||
if pending_tool_parts:
|
||||
contents.append(types.Content(role="user", parts=list(pending_tool_parts)))
|
||||
pending_tool_parts.clear()
|
||||
|
||||
for message in messages:
|
||||
if message.role == "system":
|
||||
if message.text:
|
||||
system_parts.append(message.text)
|
||||
continue
|
||||
|
||||
if message.role == "tool":
|
||||
for content in message.contents:
|
||||
part = self._convert_function_result(content, call_id_to_name)
|
||||
if part is not None:
|
||||
pending_tool_parts.append(part)
|
||||
continue
|
||||
|
||||
# Non-tool message — flush any accumulated tool parts first.
|
||||
flush_pending_tool_parts()
|
||||
|
||||
parts = self._convert_message_contents(message.contents, call_id_to_name)
|
||||
if not parts:
|
||||
continue
|
||||
|
||||
role = "model" if message.role == "assistant" else "user"
|
||||
contents.append(types.Content(role=role, parts=parts))
|
||||
|
||||
flush_pending_tool_parts()
|
||||
|
||||
system_instruction = "\n".join(system_parts) if system_parts else None
|
||||
return system_instruction, contents
|
||||
|
||||
def _convert_message_contents(
|
||||
self,
|
||||
message_contents: Sequence[Content],
|
||||
call_id_to_name: dict[str, str],
|
||||
) -> list[types.Part]:
|
||||
"""Convert framework Content objects to Gemini Part objects, tracking function call IDs.
|
||||
|
||||
Args:
|
||||
message_contents: The content items of a single framework message.
|
||||
call_id_to_name: Mutable mapping updated with any function call ID-to-name pairs found.
|
||||
|
||||
Returns:
|
||||
A list of Gemini Part objects representing the message contents.
|
||||
"""
|
||||
parts: list[types.Part] = []
|
||||
for content in message_contents:
|
||||
match content.type:
|
||||
case "text":
|
||||
parts.append(types.Part(text=content.text or ""))
|
||||
case "function_call":
|
||||
call_id = content.call_id or self._generate_tool_call_id()
|
||||
if content.name:
|
||||
call_id_to_name[call_id] = content.name
|
||||
parts.append(
|
||||
types.Part(
|
||||
function_call=types.FunctionCall(
|
||||
id=call_id,
|
||||
name=content.name or "",
|
||||
args=content.parse_arguments() or {},
|
||||
)
|
||||
)
|
||||
)
|
||||
case _:
|
||||
logger.debug("Skipping unsupported content type for Gemini: %s", content.type)
|
||||
return parts
|
||||
|
||||
def _convert_function_result(
|
||||
self,
|
||||
content: Content,
|
||||
call_id_to_name: dict[str, str],
|
||||
) -> types.Part | None:
|
||||
"""Convert a function_result Content to a Gemini FunctionResponse Part.
|
||||
|
||||
Args:
|
||||
content: The framework Content object, expected to be of type ``function_result``.
|
||||
call_id_to_name: Mapping of call IDs to function names, used to resolve the required name field.
|
||||
|
||||
Returns:
|
||||
A Gemini Part containing a FunctionResponse, or None if the content type is not
|
||||
``function_result`` or the call ID cannot be resolved.
|
||||
"""
|
||||
if content.type != "function_result":
|
||||
return None
|
||||
|
||||
name = call_id_to_name.get(content.call_id or "")
|
||||
if not name:
|
||||
logger.warning(
|
||||
"Skipping function_result: no matching function_call found for call_id=%r",
|
||||
content.call_id,
|
||||
)
|
||||
return None
|
||||
|
||||
response = self._coerce_to_dict(content.result)
|
||||
return types.Part(
|
||||
function_response=types.FunctionResponse(
|
||||
id=content.call_id,
|
||||
name=name,
|
||||
response=response,
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _coerce_to_dict(value: Any) -> dict[str, Any]:
|
||||
"""Ensure a tool result value is a dict as required by Gemini's FunctionResponse.
|
||||
|
||||
Args:
|
||||
value: The raw tool result. May be a dict, JSON string, plain string, None, or any other value.
|
||||
|
||||
Returns:
|
||||
A dict representation of the value. JSON strings are parsed; all other non-dict values
|
||||
are wrapped as ``{"result": <str(value)>}``.
|
||||
"""
|
||||
if isinstance(value, dict):
|
||||
return cast(dict[str, Any], value)
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
parsed = json.loads(value)
|
||||
if isinstance(parsed, dict):
|
||||
return cast(dict[str, Any], parsed)
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
pass
|
||||
return {"result": value}
|
||||
if value is None:
|
||||
return {"result": ""}
|
||||
return {"result": str(value)}
|
||||
|
||||
def _prepare_config(
|
||||
self,
|
||||
options: Mapping[str, Any],
|
||||
system_instruction: str | None,
|
||||
) -> types.GenerateContentConfig:
|
||||
"""Build a ``types.GenerateContentConfig`` from the resolved chat options.
|
||||
|
||||
Note: ``_OPTION_TRANSLATIONS`` keys are renamed, ``_OPTION_EXCLUDE_KEYS`` are skipped, and all
|
||||
remaining keys are forwarded as-is, allowing new Gemini parameters to be adopted without
|
||||
framework changes.
|
||||
|
||||
Args:
|
||||
options: Resolved chat options mapping, typically a ``GeminiChatOptions`` dict.
|
||||
system_instruction: Combined system instruction text, or None if absent.
|
||||
|
||||
Returns:
|
||||
A fully populated ``GenerateContentConfig`` ready to pass to the Gemini API.
|
||||
"""
|
||||
kwargs: dict[str, Any] = {}
|
||||
|
||||
if system_instruction:
|
||||
kwargs["system_instruction"] = system_instruction
|
||||
|
||||
for key, value in options.items():
|
||||
if key in _OPTION_EXCLUDE_KEYS or value is None:
|
||||
continue
|
||||
kwargs[_OPTION_TRANSLATIONS.get(key, key)] = value
|
||||
|
||||
if options.get("response_format") or options.get("response_schema"):
|
||||
kwargs["response_mime_type"] = "application/json"
|
||||
if schema := options.get("response_schema"):
|
||||
kwargs["response_schema"] = schema
|
||||
if tools := self._prepare_tools(options):
|
||||
kwargs["tools"] = tools
|
||||
if tool_config := self._prepare_tool_config(options.get("tool_choice")):
|
||||
kwargs["tool_config"] = tool_config
|
||||
if thinking_config := options.get("thinking_config"):
|
||||
thinking_config_kwargs = {k: v for k, v in thinking_config.items() if v is not None}
|
||||
if thinking_config_kwargs:
|
||||
kwargs["thinking_config"] = types.ThinkingConfig(**thinking_config_kwargs)
|
||||
|
||||
return types.GenerateContentConfig(**kwargs)
|
||||
|
||||
def _prepare_tools(self, options: Mapping[str, Any]) -> list[types.Tool] | None:
|
||||
"""Translate the framework tool list into Gemini API tool objects.
|
||||
|
||||
The Gemini API does not accept framework ``FunctionTool`` objects directly.
|
||||
This method acts as the translation boundary between the two type systems.
|
||||
It handles two kinds of entries in ``options["tools"]``:
|
||||
|
||||
- ``FunctionTool``: a framework abstraction for a callable with a name,
|
||||
description, and JSON schema. Translated to ``types.FunctionDeclaration``
|
||||
(Gemini's equivalent) and grouped into a single ``types.Tool``, which is
|
||||
how the Gemini API expects function declarations to be passed.
|
||||
- ``types.Tool``: already in Gemini's native format (e.g. built-in tools
|
||||
such as search or code execution). Passed through unchanged. Use the
|
||||
``get_*_tool`` factory methods on this class to produce these.
|
||||
|
||||
Args:
|
||||
options: Resolved chat options whose ``tools`` entry may contain
|
||||
``FunctionTool`` instances, plain callables, or ``types.Tool`` objects.
|
||||
|
||||
Returns:
|
||||
A non-empty list of ``types.Tool`` objects ready for the Gemini API,
|
||||
or ``None`` if no tools are configured.
|
||||
"""
|
||||
tools_option: list[Any] = options.get("tools") or []
|
||||
|
||||
result: list[types.Tool] = []
|
||||
|
||||
# Translate framework FunctionTool objects to Gemini API FunctionDeclaration objects
|
||||
declarations = [
|
||||
types.FunctionDeclaration(
|
||||
name=tool.name,
|
||||
description=tool.description or "",
|
||||
parameters=tool.parameters(), # type: ignore[arg-type]
|
||||
)
|
||||
for tool in tools_option
|
||||
if isinstance(tool, FunctionTool)
|
||||
]
|
||||
if declarations:
|
||||
result.append(types.Tool(function_declarations=declarations))
|
||||
|
||||
# Objects of type types.Tool are already in Gemini's native format
|
||||
result.extend(tool for tool in tools_option if isinstance(tool, types.Tool))
|
||||
|
||||
return result or None
|
||||
|
||||
def _prepare_tool_config(self, tool_choice: Any) -> types.ToolConfig | None:
|
||||
"""Build a Gemini ``ToolConfig`` from the framework ``tool_choice`` value.
|
||||
|
||||
Args:
|
||||
tool_choice: Raw ``tool_choice`` value from options (string, dict, or None).
|
||||
|
||||
Returns:
|
||||
A ``types.ToolConfig`` with the appropriate ``FunctionCallingConfig``, or None
|
||||
if no ``tool_choice`` is set or the mode is unsupported.
|
||||
"""
|
||||
tool_mode = validate_tool_mode(tool_choice)
|
||||
if not tool_mode:
|
||||
return None
|
||||
|
||||
match tool_mode.get("mode"):
|
||||
case "auto":
|
||||
function_calling_mode, allowed_names = types.FunctionCallingConfigMode.AUTO, None
|
||||
case "none":
|
||||
function_calling_mode, allowed_names = types.FunctionCallingConfigMode.NONE, None
|
||||
case "required":
|
||||
function_calling_mode = types.FunctionCallingConfigMode.ANY
|
||||
name = tool_mode.get("required_function_name")
|
||||
allowed_names = [name] if name else None
|
||||
case unknown_mode:
|
||||
logger.warning("Unsupported tool_choice mode for Gemini: %s", unknown_mode)
|
||||
return None
|
||||
|
||||
function_calling_kwargs: dict[str, Any] = {"mode": function_calling_mode}
|
||||
if allowed_names:
|
||||
function_calling_kwargs["allowed_function_names"] = allowed_names
|
||||
|
||||
return types.ToolConfig(function_calling_config=types.FunctionCallingConfig(**function_calling_kwargs))
|
||||
|
||||
# endregion
|
||||
|
||||
# region Response parsing
|
||||
|
||||
def _process_generate_response(
|
||||
self,
|
||||
response: types.GenerateContentResponse,
|
||||
*,
|
||||
response_format: type[BaseModel] | None = None,
|
||||
) -> ChatResponse:
|
||||
"""Convert a Gemini generate_content response to a framework ChatResponse.
|
||||
|
||||
Args:
|
||||
response: The raw ``GenerateContentResponse`` from the Gemini API.
|
||||
response_format: Optional Pydantic model type for structured output parsing.
|
||||
When provided, the response text is parsed into the given model and
|
||||
made available via ``ChatResponse.value``.
|
||||
|
||||
Returns:
|
||||
A ``ChatResponse`` with parsed messages, usage details, finish reason, and model ID.
|
||||
"""
|
||||
candidate = response.candidates[0] if response.candidates else None
|
||||
parts: list[types.Part] = (candidate.content.parts or []) if candidate and candidate.content else []
|
||||
contents = self._parse_parts(parts)
|
||||
return ChatResponse(
|
||||
response_id=None,
|
||||
messages=[Message(role="assistant", contents=contents, raw_representation=candidate)],
|
||||
usage_details=self._parse_usage(response.usage_metadata),
|
||||
model=response.model_version or self.model,
|
||||
finish_reason=self._map_finish_reason(
|
||||
candidate.finish_reason.name if candidate and candidate.finish_reason else None
|
||||
),
|
||||
response_format=response_format,
|
||||
raw_representation=response,
|
||||
)
|
||||
|
||||
def _process_chunk(self, chunk: types.GenerateContentResponse) -> ChatResponseUpdate:
|
||||
"""Convert a single streaming chunk to a framework ChatResponseUpdate.
|
||||
|
||||
Usage details are attached only to the final chunk, identified by a non-None finish reason.
|
||||
|
||||
Args:
|
||||
chunk: A streaming ``GenerateContentResponse`` chunk from the Gemini API.
|
||||
|
||||
Returns:
|
||||
A ``ChatResponseUpdate`` with parsed contents, finish reason, and model ID.
|
||||
"""
|
||||
candidate = chunk.candidates[0] if chunk.candidates else None
|
||||
parts: list[types.Part] = (candidate.content.parts or []) if candidate and candidate.content else []
|
||||
contents = self._parse_parts(parts)
|
||||
|
||||
finish_reason = self._map_finish_reason(
|
||||
candidate.finish_reason.name if candidate and candidate.finish_reason else None
|
||||
)
|
||||
|
||||
# Attach usage to the final chunk only (when finish_reason is set).
|
||||
if finish_reason and (usage := self._parse_usage(chunk.usage_metadata)):
|
||||
contents.append(Content.from_usage(usage_details=usage))
|
||||
|
||||
return ChatResponseUpdate(
|
||||
contents=contents,
|
||||
model=chunk.model_version,
|
||||
finish_reason=finish_reason,
|
||||
raw_representation=chunk,
|
||||
)
|
||||
|
||||
def _parse_parts(self, parts: Sequence[types.Part]) -> list[Content]:
|
||||
"""Convert Gemini response parts to framework Content objects, skipping thought/reasoning parts.
|
||||
|
||||
Args:
|
||||
parts: Sequence of ``types.Part`` objects from a Gemini response candidate.
|
||||
|
||||
Returns:
|
||||
A list of framework ``Content`` objects (text, function_call, or function_result).
|
||||
"""
|
||||
contents: list[Content] = []
|
||||
for part in parts:
|
||||
if part.thought:
|
||||
continue
|
||||
if part.text is not None:
|
||||
contents.append(Content.from_text(text=part.text, raw_representation=part))
|
||||
elif part.function_call is not None:
|
||||
function_call = part.function_call
|
||||
if function_call.id:
|
||||
call_id = function_call.id
|
||||
else:
|
||||
call_id = self._generate_tool_call_id()
|
||||
logger.debug("function_call missing id; generated fallback call_id=%r", call_id)
|
||||
contents.append(
|
||||
Content.from_function_call(
|
||||
call_id=call_id,
|
||||
name=function_call.name or "",
|
||||
arguments=function_call.args or {},
|
||||
raw_representation=part,
|
||||
)
|
||||
)
|
||||
elif part.function_response is not None:
|
||||
function_response = part.function_response
|
||||
contents.append(
|
||||
Content.from_function_result(
|
||||
call_id=function_response.id or self._generate_tool_call_id(),
|
||||
result=function_response.response,
|
||||
raw_representation=part,
|
||||
)
|
||||
)
|
||||
elif part.executable_code is not None:
|
||||
if part.executable_code.code:
|
||||
contents.append(Content.from_text(text=part.executable_code.code, raw_representation=part))
|
||||
elif part.code_execution_result is not None:
|
||||
if part.code_execution_result.output:
|
||||
contents.append(Content.from_text(text=part.code_execution_result.output, raw_representation=part))
|
||||
else:
|
||||
logger.debug("Skipping unsupported response part from Gemini")
|
||||
return contents
|
||||
|
||||
def _parse_usage(self, usage: types.GenerateContentResponseUsageMetadata | None) -> UsageDetails | None:
|
||||
"""Extract token usage counts from Gemini usage metadata.
|
||||
|
||||
Args:
|
||||
usage: The ``GenerateContentResponseUsageMetadata`` from the API response, or None.
|
||||
|
||||
Returns:
|
||||
A ``UsageDetails`` dict with available token counts, or None if no usage data is present.
|
||||
"""
|
||||
if not usage:
|
||||
return None
|
||||
details: UsageDetails = {}
|
||||
if (v := usage.prompt_token_count) is not None:
|
||||
details["input_token_count"] = v
|
||||
if (v := usage.candidates_token_count) is not None:
|
||||
details["output_token_count"] = v
|
||||
if (v := usage.total_token_count) is not None:
|
||||
details["total_token_count"] = v
|
||||
return details or None
|
||||
|
||||
def _map_finish_reason(self, reason: str | None) -> FinishReasonLiteral | None:
|
||||
"""Map a Gemini finish reason string to the framework's FinishReasonLiteral.
|
||||
|
||||
Args:
|
||||
reason: The finish reason name from the Gemini API (e.g. ``"STOP"``), or None.
|
||||
|
||||
Returns:
|
||||
The corresponding ``FinishReasonLiteral``, or None if the reason is absent or unmapped.
|
||||
"""
|
||||
if not reason:
|
||||
return None
|
||||
return _FINISH_REASON_MAP.get(reason)
|
||||
|
||||
# endregion
|
||||
|
||||
@staticmethod
|
||||
def _generate_tool_call_id() -> str:
|
||||
"""Generate a unique fallback ID for tool calls that lack one.
|
||||
|
||||
Returns:
|
||||
A unique string in the format ``tool-call-<uuid_hex>``.
|
||||
"""
|
||||
return f"tool-call-{uuid4().hex}"
|
||||
|
||||
|
||||
class GeminiChatClient(
|
||||
FunctionInvocationLayer[GeminiChatOptionsT],
|
||||
ChatMiddlewareLayer[GeminiChatOptionsT],
|
||||
ChatTelemetryLayer[GeminiChatOptionsT],
|
||||
RawGeminiChatClient[GeminiChatOptionsT],
|
||||
Generic[GeminiChatOptionsT],
|
||||
):
|
||||
"""Gemini chat client for the Google Gemini API with function invocation, middleware, and telemetry.
|
||||
|
||||
This is the recommended client for most use cases. It builds on ``RawGeminiChatClient``
|
||||
and adds:
|
||||
|
||||
- **Function invocation**: automatically calls ``FunctionTool`` implementations and feeds
|
||||
results back to the model until it produces a final text response.
|
||||
- **Middleware**: a composable chain for cross-cutting concerns (logging, retries, etc.).
|
||||
- **Telemetry**: OpenTelemetry traces and metrics emitted for every request.
|
||||
|
||||
Use ``RawGeminiChatClient`` instead when you need full control over the request pipeline
|
||||
and want to opt out of one or more of these layers.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
api_key: str | None = None,
|
||||
model: str | None = None,
|
||||
env_file_path: str | None = None,
|
||||
env_file_encoding: str | None = None,
|
||||
client: genai.Client | None = None,
|
||||
additional_properties: dict[str, Any] | None = None,
|
||||
middleware: Sequence[ChatAndFunctionMiddlewareTypes] | None = None,
|
||||
function_invocation_configuration: FunctionInvocationConfiguration | None = None,
|
||||
) -> None:
|
||||
"""Create a Gemini chat client.
|
||||
|
||||
Args:
|
||||
api_key: The Google AI Studio API key. Falls back to ``GEMINI_API_KEY`` environment variable.
|
||||
model: Default model identifier. Falls back to ``GEMINI_MODEL`` environment variable.
|
||||
env_file_path: Path to a ``.env`` file for credential loading.
|
||||
env_file_encoding: Encoding for the ``.env`` file.
|
||||
client: Pre-built ``genai.Client`` instance. When provided, ``api_key`` is not required.
|
||||
additional_properties: Extra properties stored on the client instance.
|
||||
middleware: Optional middleware chain applied to every call.
|
||||
function_invocation_configuration: Optional configuration for the function invocation loop.
|
||||
"""
|
||||
super().__init__(
|
||||
api_key=api_key,
|
||||
model=model,
|
||||
env_file_path=env_file_path,
|
||||
env_file_encoding=env_file_encoding,
|
||||
client=client,
|
||||
additional_properties=additional_properties,
|
||||
middleware=middleware,
|
||||
function_invocation_configuration=function_invocation_configuration,
|
||||
)
|
||||
@@ -0,0 +1,105 @@
|
||||
[project]
|
||||
name = "agent-framework-gemini"
|
||||
description = "Google Gemini integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0a260410"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
urls.release_notes = "https://github.com/microsoft/agent-framework/releases?q=tag%3Apython-1&expanded=true"
|
||||
urls.issues = "https://github.com/microsoft/agent-framework/issues"
|
||||
classifiers = [
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Intended Audience :: Developers",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Programming Language :: Python :: 3.14",
|
||||
"Framework :: Pydantic :: 2",
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.0.0,<2.0",
|
||||
"google-genai>=1.0.0,<2.0.0",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
prerelease = "if-necessary-or-explicit"
|
||||
environments = [
|
||||
"sys_platform == 'darwin'",
|
||||
"sys_platform == 'linux'",
|
||||
"sys_platform == 'win32'"
|
||||
]
|
||||
|
||||
[tool.uv-dynamic-versioning]
|
||||
fallback-version = "0.0.0"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = 'tests'
|
||||
addopts = "-ra -q -r fEX"
|
||||
asyncio_mode = "auto"
|
||||
asyncio_default_fixture_loop_scope = "function"
|
||||
filterwarnings = []
|
||||
markers = [
|
||||
"integration: marks tests as integration tests that require external services",
|
||||
"flaky: marks tests as flaky and eligible for automatic retry",
|
||||
]
|
||||
timeout = 120
|
||||
|
||||
[tool.ruff]
|
||||
extend = "../../pyproject.toml"
|
||||
|
||||
[tool.ruff.lint.extend-per-file-ignores]
|
||||
"samples/**" = ["S", "T201"]
|
||||
|
||||
[tool.coverage.run]
|
||||
omit = [
|
||||
"**/__init__.py"
|
||||
]
|
||||
|
||||
[tool.pyright]
|
||||
extends = "../../pyproject.toml"
|
||||
include = ["agent_framework_gemini"]
|
||||
exclude = ['tests']
|
||||
|
||||
[tool.mypy]
|
||||
plugins = ['pydantic.mypy']
|
||||
strict = true
|
||||
python_version = "3.10"
|
||||
ignore_missing_imports = true
|
||||
disallow_untyped_defs = true
|
||||
no_implicit_optional = true
|
||||
check_untyped_defs = true
|
||||
warn_return_any = true
|
||||
show_error_codes = true
|
||||
warn_unused_ignores = false
|
||||
disallow_incomplete_defs = true
|
||||
disallow_untyped_decorators = true
|
||||
|
||||
[tool.bandit]
|
||||
targets = ["agent_framework_gemini"]
|
||||
exclude_dirs = ["tests"]
|
||||
|
||||
[tool.poe]
|
||||
executor.type = "uv"
|
||||
include = "../../shared_tasks.toml"
|
||||
|
||||
[tool.poe.tasks.mypy]
|
||||
help = "Run MyPy for this package."
|
||||
cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_gemini"
|
||||
|
||||
[tool.poe.tasks.test]
|
||||
help = "Run the default unit test suite for this package."
|
||||
cmd = 'pytest -m "not integration" --cov=agent_framework_gemini --cov-report=term-missing:skip-covered tests'
|
||||
|
||||
[tool.flit.module]
|
||||
name = "agent_framework_gemini"
|
||||
|
||||
[build-system]
|
||||
requires = ["flit-core >= 3.11,<4.0"]
|
||||
build-backend = "flit_core.buildapi"
|
||||
@@ -0,0 +1,18 @@
|
||||
# Google Gemini Examples
|
||||
|
||||
This folder contains examples demonstrating how to use Google Gemini models with the Agent Framework.
|
||||
|
||||
## Examples
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| [`gemini_basic.py`](gemini_basic.py) | Basic agent with a weather tool, demonstrating both streaming and non-streaming responses. |
|
||||
| [`gemini_advanced.py`](gemini_advanced.py) | Extended thinking via `ThinkingConfig` for reasoning-heavy questions (Gemini 2.5+). |
|
||||
| [`gemini_with_google_search.py`](gemini_with_google_search.py) | Google Search grounding for up-to-date answers. |
|
||||
| [`gemini_with_google_maps.py`](gemini_with_google_maps.py) | Google Maps grounding for location and mapping information. |
|
||||
| [`gemini_with_code_execution.py`](gemini_with_code_execution.py) | Built-in code execution tool for computing precise answers in a sandboxed environment. |
|
||||
|
||||
## Environment Variables
|
||||
|
||||
- `GEMINI_API_KEY`: Your Google AI Studio API key (get one from [Google AI Studio](https://aistudio.google.com/apikey))
|
||||
- `GEMINI_MODEL`: The Gemini model to use (e.g., `gemini-2.5-flash`, `gemini-2.5-pro`)
|
||||
@@ -0,0 +1,47 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Shows how to enable extended thinking with ThinkingConfig.
|
||||
|
||||
Allows the model to reason through complex problems before responding.
|
||||
|
||||
Requires the following environment variables to be set:
|
||||
- GEMINI_API_KEY
|
||||
- GEMINI_MODEL
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import Agent
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from agent_framework_gemini import GeminiChatClient, GeminiChatOptions, ThinkingConfig
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Example of extended thinking with a Python version comparison question."""
|
||||
print("=== Extended thinking ===")
|
||||
|
||||
options: GeminiChatOptions = {
|
||||
"thinking_config": ThinkingConfig(thinking_budget=2048),
|
||||
}
|
||||
|
||||
agent = Agent(
|
||||
client=GeminiChatClient(),
|
||||
name="PythonAgent",
|
||||
instructions="You are a helpful Python expert.",
|
||||
default_options=options,
|
||||
)
|
||||
|
||||
query = "What new language features were introduced in Python between 3.10 and 3.14?"
|
||||
print(f"User: {query}")
|
||||
print("Agent: ", end="", flush=True)
|
||||
async for chunk in agent.run(query, stream=True):
|
||||
if chunk.text:
|
||||
print(chunk.text, end="", flush=True)
|
||||
print("\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,78 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Shows how to use GeminiChatClient with an agent and a custom tool.
|
||||
|
||||
Covers both non-streaming and streaming responses.
|
||||
|
||||
Requires the following environment variables to be set:
|
||||
- GEMINI_API_KEY
|
||||
- GEMINI_MODEL
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from random import randint
|
||||
from typing import Annotated
|
||||
|
||||
from agent_framework import Agent, tool
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from agent_framework_gemini import GeminiChatClient
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production
|
||||
@tool(approval_mode="never_require")
|
||||
def get_weather(
|
||||
location: Annotated[str, "The location to get the weather for."],
|
||||
) -> str:
|
||||
"""Get the weather for a given location."""
|
||||
conditions = ["sunny", "cloudy", "rainy", "stormy"]
|
||||
return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C."
|
||||
|
||||
|
||||
async def non_streaming_example() -> None:
|
||||
"""Runs the agent and waits for the complete response before printing it."""
|
||||
print("=== Non-streaming ===")
|
||||
|
||||
agent = Agent(
|
||||
client=GeminiChatClient(),
|
||||
name="WeatherAgent",
|
||||
instructions="You are a helpful weather agent.",
|
||||
tools=[get_weather],
|
||||
)
|
||||
|
||||
query = "What's the weather like in Karlsruhe, Germany?"
|
||||
print(f"User: {query}")
|
||||
result = await agent.run(query)
|
||||
print(f"Result: {result}\n")
|
||||
|
||||
|
||||
async def streaming_example() -> None:
|
||||
"""Runs the agent and prints each chunk as it is received."""
|
||||
print("=== Streaming ===")
|
||||
|
||||
agent = Agent(
|
||||
client=GeminiChatClient(),
|
||||
name="WeatherAgent",
|
||||
instructions="You are a helpful weather agent.",
|
||||
tools=[get_weather],
|
||||
)
|
||||
|
||||
query = "What's the weather like in Portland and in Paris?"
|
||||
print(f"User: {query}")
|
||||
print("Agent: ", end="", flush=True)
|
||||
async for chunk in agent.run(query, stream=True):
|
||||
if chunk.text:
|
||||
print(chunk.text, end="", flush=True)
|
||||
print("\n")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Run non-streaming and streaming examples."""
|
||||
await non_streaming_example()
|
||||
await streaming_example()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,43 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Shows how to enable Gemini's built-in code execution tool.
|
||||
|
||||
Allows the model to write and run code in a sandboxed environment to answer questions.
|
||||
|
||||
Requires the following environment variables to be set:
|
||||
- GEMINI_API_KEY
|
||||
- GEMINI_MODEL
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import Agent
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from agent_framework_gemini import GeminiChatClient
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Run the code execution example."""
|
||||
print("=== Code execution ===")
|
||||
|
||||
agent = Agent(
|
||||
client=GeminiChatClient(),
|
||||
name="CodeAgent",
|
||||
instructions="You are a helpful assistant. Use code execution to compute precise answers.",
|
||||
tools=[GeminiChatClient.get_code_interpreter_tool()],
|
||||
)
|
||||
|
||||
query = "What are the first 20 prime numbers? Compute them in code."
|
||||
print(f"User: {query}")
|
||||
print("Agent: ", end="", flush=True)
|
||||
async for chunk in agent.run(query, stream=True):
|
||||
if chunk.text:
|
||||
print(chunk.text, end="", flush=True)
|
||||
print("\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,43 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Shows how to enable Google Maps grounding.
|
||||
|
||||
Allows Gemini to retrieve location and mapping information before responding.
|
||||
|
||||
Requires the following environment variables to be set:
|
||||
- GEMINI_API_KEY
|
||||
- GEMINI_MODEL
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import Agent
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from agent_framework_gemini import GeminiChatClient
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Run the Google Maps grounding example."""
|
||||
print("=== Google Maps grounding ===")
|
||||
|
||||
agent = Agent(
|
||||
client=GeminiChatClient(),
|
||||
name="MapsAgent",
|
||||
instructions="You are a helpful travel assistant. Use Google Maps to provide accurate location information.",
|
||||
tools=[GeminiChatClient.get_maps_grounding_tool()],
|
||||
)
|
||||
|
||||
query = "What are some highly rated restaurants in the city center of Karlsruhe, Germany?"
|
||||
print(f"User: {query}")
|
||||
print("Agent: ", end="", flush=True)
|
||||
async for chunk in agent.run(query, stream=True):
|
||||
if chunk.text:
|
||||
print(chunk.text, end="", flush=True)
|
||||
print("\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,43 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Shows how to enable Google Search grounding.
|
||||
|
||||
Allows Gemini to retrieve up-to-date information from the web before responding.
|
||||
|
||||
Requires the following environment variables to be set:
|
||||
- GEMINI_API_KEY
|
||||
- GEMINI_MODEL
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import Agent
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from agent_framework_gemini import GeminiChatClient
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Run the Google Search grounding example."""
|
||||
print("=== Google Search grounding ===")
|
||||
|
||||
agent = Agent(
|
||||
client=GeminiChatClient(),
|
||||
name="SearchAgent",
|
||||
instructions="You are a helpful assistant. Use Google Search to provide accurate, up-to-date answers.",
|
||||
tools=[GeminiChatClient.get_web_search_tool()],
|
||||
)
|
||||
|
||||
query = "What is the latest stable release of the .NET SDK?"
|
||||
print(f"User: {query}")
|
||||
print("Agent: ", end="", flush=True)
|
||||
async for chunk in agent.run(query, stream=True):
|
||||
if chunk.text:
|
||||
print(chunk.text, end="", flush=True)
|
||||
print("\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
File diff suppressed because it is too large
Load Diff
@@ -73,21 +73,22 @@ agent-framework-anthropic = { workspace = true }
|
||||
agent-framework-azurefunctions = { workspace = true }
|
||||
agent-framework-bedrock = { workspace = true }
|
||||
agent-framework-chatkit = { workspace = true }
|
||||
agent-framework-claude = { workspace = true }
|
||||
agent-framework-copilotstudio = { workspace = true }
|
||||
agent-framework-declarative = { workspace = true }
|
||||
agent-framework-devui = { workspace = true }
|
||||
agent-framework-durabletask = { workspace = true }
|
||||
agent-framework-foundry = { workspace = true }
|
||||
agent-framework-foundry-local = { workspace = true }
|
||||
agent-framework-gemini = { workspace = true }
|
||||
agent-framework-github-copilot = { workspace = true }
|
||||
agent-framework-lab = { workspace = true }
|
||||
agent-framework-mem0 = { workspace = true }
|
||||
agent-framework-ollama = { workspace = true }
|
||||
agent-framework-openai = { workspace = true }
|
||||
agent-framework-orchestrations = { workspace = true }
|
||||
agent-framework-purview = { workspace = true }
|
||||
agent-framework-redis = { workspace = true }
|
||||
agent-framework-github-copilot = { workspace = true }
|
||||
agent-framework-claude = { workspace = true }
|
||||
agent-framework-orchestrations = { workspace = true }
|
||||
litellm = { url = "https://files.pythonhosted.org/packages/57/77/0c6eca2cb049793ddf8ce9cdcd5123a35666c4962514788c4fc90edf1d3b/litellm-1.82.1-py3-none-any.whl" }
|
||||
|
||||
[tool.ruff]
|
||||
|
||||
Generated
+42
@@ -43,6 +43,7 @@ members = [
|
||||
"agent-framework-durabletask",
|
||||
"agent-framework-foundry",
|
||||
"agent-framework-foundry-local",
|
||||
"agent-framework-gemini",
|
||||
"agent-framework-github-copilot",
|
||||
"agent-framework-lab",
|
||||
"agent-framework-mem0",
|
||||
@@ -514,6 +515,21 @@ requires-dist = [
|
||||
{ name = "foundry-local-sdk", specifier = ">=0.5.1,<0.5.2" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-gemini"
|
||||
version = "1.0.0a260410"
|
||||
source = { editable = "packages/gemini" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "google-genai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "agent-framework-core", editable = "packages/core" },
|
||||
{ name = "google-genai", specifier = ">=1.0.0,<2.0.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-github-copilot"
|
||||
version = "1.0.0b260409"
|
||||
@@ -2334,6 +2350,32 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/eb/c6c2478d8a8d633460be40e2a8a6f8f429171997a35a96f81d3b680dec83/google_auth-2.49.1-py3-none-any.whl", hash = "sha256:195ebe3dca18eddd1b3db5edc5189b76c13e96f29e73043b923ebcf3f1a860f7", size = 240737, upload-time = "2026-03-12T19:30:53.159Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
requests = [
|
||||
{ name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "google-genai"
|
||||
version = "1.68.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "distro", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "google-auth", extra = ["requests"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "httpx", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "sniffio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "tenacity", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "websockets", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9c/2c/f059982dbcb658cc535c81bbcbe7e2c040d675f4b563b03cdb01018a4bc3/google_genai-1.68.0.tar.gz", hash = "sha256:ac30c0b8bc630f9372993a97e4a11dae0e36f2e10d7c55eacdca95a9fa14ca96", size = 511285, upload-time = "2026-03-18T01:03:18.243Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/84/de/7d3ee9c94b74c3578ea4f88d45e8de9405902f857932334d81e89bce3dfa/google_genai-1.68.0-py3-none-any.whl", hash = "sha256:a1bc9919c0e2ea2907d1e319b65471d3d6d58c54822039a249fe1323e4178d15", size = 750912, upload-time = "2026-03-18T01:03:15.983Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "googleapis-common-protos"
|
||||
version = "1.73.1"
|
||||
|
||||
Reference in New Issue
Block a user