Skip to content

Commit 4dc78e7

Browse files
google-genai-botcopybara-github
authored andcommitted
feat: add Spanner first-party tools
These tools support basic operations to interact with Spanner table metadata and query results. PiperOrigin-RevId: 786578728
1 parent c323de5 commit 4dc78e7

21 files changed

+1648
-263
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)