Skip to content

Commit 1fc8d20

Browse files
google-genai-botcopybara-github
authored andcommitted
feat: add Spanner first-party toolset (breaking change to BigQueryTool, consolidating into generic GoogleTool)
Spanner toolset support basic operations to interact with Spanner table metadata and query results. Consolidate BigQueryTool into generic GoogleTool, so that BigQueryToolset and SpannerToolset can share. PiperOrigin-RevId: 794259782
1 parent 10e3dfa commit 1fc8d20

25 files changed

+1716
-320
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ dependencies = [
3434
"google-api-python-client>=2.157.0", # Google API client discovery
3535
"google-cloud-aiplatform[agent_engines]>=1.95.1", # For VertexAI integrations, e.g. example store.
3636
"google-cloud-secret-manager>=2.22.0", # Fetching secrets in RestAPI Tool
37+
"google-cloud-spanner>=3.56.0", # For Spanner database
3738
"google-cloud-speech>=2.30.0", # For Audio Transcription
3839
"google-cloud-storage>=2.18.0, <3.0.0", # For GCS Artifact service
3940
"google-genai>=1.21.1", # Google GenAI SDK
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import annotations
16+
17+
import json
18+
from typing import List
19+
from typing import Optional
20+
21+
from fastapi.openapi.models import OAuth2
22+
from fastapi.openapi.models import OAuthFlowAuthorizationCode
23+
from fastapi.openapi.models import OAuthFlows
24+
import google.auth.credentials
25+
from google.auth.exceptions import RefreshError
26+
from google.auth.transport.requests import Request
27+
import google.oauth2.credentials
28+
from pydantic import BaseModel
29+
from pydantic import model_validator
30+
31+
from ..auth.auth_credential import AuthCredential
32+
from ..auth.auth_credential import AuthCredentialTypes
33+
from ..auth.auth_credential import OAuth2Auth
34+
from ..auth.auth_tool import AuthConfig
35+
from ..utils.feature_decorator import experimental
36+
from .tool_context import ToolContext
37+
38+
39+
@experimental
40+
class BaseGoogleCredentialsConfig(BaseModel):
41+
"""Base Google Credentials Configuration for Google API tools (Experimental).
42+
43+
Please do not use this in production, as it may be deprecated later.
44+
"""
45+
46+
# Configure the model to allow arbitrary types like Credentials
47+
model_config = {"arbitrary_types_allowed": True}
48+
49+
credentials: Optional[google.auth.credentials.Credentials] = None
50+
"""The existing auth credentials to use. If set, this credential will be used
51+
for every end user, end users don't need to be involved in the oauthflow. This
52+
field is mutually exclusive with client_id, client_secret and scopes.
53+
Don't set this field unless you are sure this credential has the permission to
54+
access every end user's data.
55+
56+
Example usage 1: When the agent is deployed in Google Cloud environment and
57+
the service account (used as application default credentials) has access to
58+
all the required Google Cloud resource. Setting this credential to allow user
59+
to access the Google Cloud resource without end users going through oauth
60+
flow.
61+
62+
To get application default credential, use: `google.auth.default(...)`. See
63+
more details in
64+
https://cloud.google.com/docs/authentication/application-default-credentials.
65+
66+
Example usage 2: When the agent wants to access the user's Google Cloud
67+
resources using the service account key credentials.
68+
69+
To load service account key credentials, use:
70+
`google.auth.load_credentials_from_file(...)`. See more details in
71+
https://cloud.google.com/iam/docs/service-account-creds#user-managed-keys.
72+
73+
When the deployed environment cannot provide a pre-existing credential,
74+
consider setting below client_id, client_secret and scope for end users to go
75+
through oauth flow, so that agent can access the user data.
76+
"""
77+
client_id: Optional[str] = None
78+
"""the oauth client ID to use."""
79+
client_secret: Optional[str] = None
80+
"""the oauth client secret to use."""
81+
scopes: Optional[List[str]] = None
82+
"""the scopes to use."""
83+
84+
_token_cache_key: Optional[str] = None
85+
"""The key to cache the token in the tool context."""
86+
87+
@model_validator(mode="after")
88+
def __post_init__(self) -> BaseGoogleCredentialsConfig:
89+
"""Validate that either credentials or client ID/secret are provided."""
90+
if not self.credentials and (not self.client_id or not self.client_secret):
91+
raise ValueError(
92+
"Must provide either credentials or client_id and client_secret pair."
93+
)
94+
if self.credentials and (
95+
self.client_id or self.client_secret or self.scopes
96+
):
97+
raise ValueError(
98+
"Cannot provide both existing credentials and"
99+
" client_id/client_secret/scopes."
100+
)
101+
102+
if self.credentials and isinstance(
103+
self.credentials, google.oauth2.credentials.Credentials
104+
):
105+
self.client_id = self.credentials.client_id
106+
self.client_secret = self.credentials.client_secret
107+
self.scopes = self.credentials.scopes
108+
109+
return self
110+
111+
112+
class GoogleCredentialsManager:
113+
"""Manages Google API credentials with automatic refresh and OAuth flow handling.
114+
115+
This class centralizes credential management so multiple tools can share
116+
the same authenticated session without duplicating OAuth logic.
117+
"""
118+
119+
def __init__(
120+
self,
121+
credentials_config: BaseGoogleCredentialsConfig,
122+
):
123+
"""Initialize the credential manager.
124+
125+
Args:
126+
credentials_config: Credentials containing client id and client secrete
127+
or default credentials
128+
"""
129+
self.credentials_config = credentials_config
130+
131+
async def get_valid_credentials(
132+
self, tool_context: ToolContext
133+
) -> Optional[google.auth.credentials.Credentials]:
134+
"""Get valid credentials, handling refresh and OAuth flow as needed.
135+
136+
Args:
137+
tool_context: The tool context for OAuth flow and state management
138+
139+
Returns:
140+
Valid Credentials object, or None if OAuth flow is needed
141+
"""
142+
# First, try to get credentials from the tool context
143+
creds_json = (
144+
tool_context.state.get(self.credentials_config._token_cache_key, None)
145+
if self.credentials_config._token_cache_key
146+
else None
147+
)
148+
creds = (
149+
google.oauth2.credentials.Credentials.from_authorized_user_info(
150+
json.loads(creds_json), self.credentials_config.scopes
151+
)
152+
if creds_json
153+
else None
154+
)
155+
156+
# If credentails are empty use the default credential
157+
if not creds:
158+
creds = self.credentials_config.credentials
159+
160+
# If non-oauth credentials are provided then use them as is. This helps
161+
# in flows such as service account keys
162+
if creds and not isinstance(creds, google.oauth2.credentials.Credentials):
163+
return creds
164+
165+
# Check if we have valid credentials
166+
if creds and creds.valid:
167+
return creds
168+
169+
# Try to refresh expired credentials
170+
if creds and creds.expired and creds.refresh_token:
171+
try:
172+
creds.refresh(Request())
173+
if creds.valid:
174+
# Cache the refreshed credentials if token cache key is set
175+
if self.credentials_config._token_cache_key:
176+
tool_context.state[self.credentials_config._token_cache_key] = (
177+
creds.to_json()
178+
)
179+
return creds
180+
except RefreshError:
181+
# Refresh failed, need to re-authenticate
182+
pass
183+
184+
# Need to perform OAuth flow
185+
return await self._perform_oauth_flow(tool_context)
186+
187+
async def _perform_oauth_flow(
188+
self, tool_context: ToolContext
189+
) -> Optional[google.oauth2.credentials.Credentials]:
190+
"""Perform OAuth flow to get new credentials.
191+
192+
Args:
193+
tool_context: The tool context for OAuth flow
194+
195+
Returns:
196+
New Credentials object, or None if flow is in progress
197+
"""
198+
199+
# Create OAuth configuration
200+
auth_scheme = OAuth2(
201+
flows=OAuthFlows(
202+
authorizationCode=OAuthFlowAuthorizationCode(
203+
authorizationUrl="https://accounts.google.com/o/oauth2/auth",
204+
tokenUrl="https://oauth2.googleapis.com/token",
205+
scopes={
206+
scope: f"Access to {scope}"
207+
for scope in self.credentials_config.scopes
208+
},
209+
)
210+
)
211+
)
212+
213+
auth_credential = AuthCredential(
214+
auth_type=AuthCredentialTypes.OAUTH2,
215+
oauth2=OAuth2Auth(
216+
client_id=self.credentials_config.client_id,
217+
client_secret=self.credentials_config.client_secret,
218+
),
219+
)
220+
221+
# Check if OAuth response is available
222+
auth_response = tool_context.get_auth_response(
223+
AuthConfig(auth_scheme=auth_scheme, raw_auth_credential=auth_credential)
224+
)
225+
226+
if auth_response:
227+
# OAuth flow completed, create credentials
228+
creds = google.oauth2.credentials.Credentials(
229+
token=auth_response.oauth2.access_token,
230+
refresh_token=auth_response.oauth2.refresh_token,
231+
token_uri=auth_scheme.flows.authorizationCode.tokenUrl,
232+
client_id=self.credentials_config.client_id,
233+
client_secret=self.credentials_config.client_secret,
234+
scopes=list(self.credentials_config.scopes),
235+
)
236+
237+
# Cache the new credentials if token cache key is set
238+
if self.credentials_config._token_cache_key:
239+
tool_context.state[self.credentials_config._token_cache_key] = (
240+
creds.to_json()
241+
)
242+
243+
return creds
244+
else:
245+
# Request OAuth flow
246+
tool_context.request_credential(
247+
AuthConfig(
248+
auth_scheme=auth_scheme,
249+
raw_auth_credential=auth_credential,
250+
)
251+
)
252+
return None

src/google/adk/tools/bigquery/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,9 @@
2828
"""
2929

3030
from .bigquery_credentials import BigQueryCredentialsConfig
31-
from .bigquery_tool import BigQueryTool
3231
from .bigquery_toolset import BigQueryToolset
3332

3433
__all__ = [
35-
"BigQueryTool",
3634
"BigQueryToolset",
3735
"BigQueryCredentialsConfig",
3836
]

0 commit comments

Comments
 (0)