Skip to content

Commit 71a67c4

Browse files
authored
Merge branch 'main' into main
2 parents 2df879c + 1445ad5 commit 71a67c4

File tree

19 files changed

+977
-80
lines changed

19 files changed

+977
-80
lines changed

contributing/samples/bigquery/agent.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from google.adk.tools.bigquery.config import BigQueryToolConfig
2222
from google.adk.tools.bigquery.config import WriteMode
2323
import google.auth
24+
import google.auth.transport.requests
2425

2526
# Define the desired credential type.
2627
# By default use Application Default Credentials (ADC) from the local
@@ -57,6 +58,8 @@
5758
# service account key file
5859
# https://cloud.google.com/iam/docs/service-account-creds#user-managed-keys
5960
creds, _ = google.auth.load_credentials_from_file("service_account_key.json")
61+
if not creds.valid:
62+
creds.refresh(google.auth.transport.requests.Request())
6063
credentials_config = BigQueryCredentialsConfig(credentials=creds)
6164
elif CREDENTIALS_TYPE == AuthCredentialTypes.HTTP:
6265
# Initialize the tools to use the externally provided access token. One such
@@ -73,6 +76,10 @@
7376
# Initialize the tools to use the application default credentials.
7477
# https://cloud.google.com/docs/authentication/provide-credentials-adc
7578
application_default_credentials, _ = google.auth.default()
79+
if not application_default_credentials.valid:
80+
application_default_credentials.refresh(
81+
google.auth.transport.requests.Request()
82+
)
7683
credentials_config = BigQueryCredentialsConfig(
7784
credentials=application_default_credentials
7885
)

contributing/samples/data_agent/agent.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@
5151
# Initialize the tools to use the application default credentials.
5252
# https://cloud.google.com/docs/authentication/provide-credentials-adc
5353
application_default_credentials, _ = google.auth.default()
54+
if not application_default_credentials.valid:
55+
application_default_credentials.refresh(
56+
google.auth.transport.requests.Request()
57+
)
5458
credentials_config = DataAgentCredentialsConfig(
5559
credentials=application_default_credentials
5660
)

src/google/adk/cli/cli_create.py

Lines changed: 116 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import click
2323

2424
from ..apps.app import validate_app_name
25+
from .utils import gcp_utils
2526

2627
_INIT_PY_TEMPLATE = """\
2728
from . import agent
@@ -61,18 +62,36 @@
6162
https://google.github.io/adk-docs/agents/models
6263
"""
6364

65+
_EXPRESS_TOS_MSG = """
66+
Google Cloud Express Mode Terms of Service: https://cloud.google.com/terms/google-cloud-express
67+
By continuing, you agree to the Terms of Service for Vertex AI Express Mode.
68+
Would you like to proceed? (yes/no)
69+
"""
70+
71+
_NOT_ELIGIBLE_MSG = """
72+
You are not eligible for Express Mode.
73+
Please follow these instructions to set up a full Google Cloud project:
74+
https://google.github.io/adk-docs/get-started/quickstart/#gemini---google-cloud-vertex-ai
75+
"""
76+
6477
_SUCCESS_MSG_CODE = """
6578
Agent created in {agent_folder}:
6679
- .env
6780
- __init__.py
6881
- agent.py
82+
83+
⚠️ WARNING: Secrets (like GOOGLE_API_KEY) are stored in .env.
84+
Please ensure .env is added to your .gitignore to avoid committing secrets to version control.
6985
"""
7086

7187
_SUCCESS_MSG_CONFIG = """
7288
Agent created in {agent_folder}:
7389
- .env
7490
- __init__.py
7591
- root_agent.yaml
92+
93+
⚠️ WARNING: Secrets (like GOOGLE_API_KEY) are stored in .env.
94+
Please ensure .env is added to your .gitignore to avoid committing secrets to version control.
7695
"""
7796

7897

@@ -187,10 +206,10 @@ def _generate_files(
187206

188207
with open(dotenv_file_path, "w", encoding="utf-8") as f:
189208
lines = []
190-
if google_api_key:
191-
lines.append("GOOGLE_GENAI_USE_VERTEXAI=0")
192-
elif google_cloud_project and google_cloud_region:
209+
if google_cloud_project and google_cloud_region:
193210
lines.append("GOOGLE_GENAI_USE_VERTEXAI=1")
211+
elif google_api_key:
212+
lines.append("GOOGLE_GENAI_USE_VERTEXAI=0")
194213
if google_api_key:
195214
lines.append(f"GOOGLE_API_KEY={google_api_key}")
196215
if google_cloud_project:
@@ -247,18 +266,110 @@ def _prompt_to_choose_backend(
247266
A tuple of (google_api_key, google_cloud_project, google_cloud_region).
248267
"""
249268
backend_choice = click.prompt(
250-
"1. Google AI\n2. Vertex AI\nChoose a backend",
251-
type=click.Choice(["1", "2"]),
269+
"1. Google AI\n2. Vertex AI\n3. Login with Google\nChoose a backend",
270+
type=click.Choice(["1", "2", "3"]),
252271
)
253272
if backend_choice == "1":
254273
google_api_key = _prompt_for_google_api_key(google_api_key)
255274
elif backend_choice == "2":
256275
click.secho(_GOOGLE_CLOUD_SETUP_MSG, fg="green")
257276
google_cloud_project = _prompt_for_google_cloud(google_cloud_project)
258277
google_cloud_region = _prompt_for_google_cloud_region(google_cloud_region)
278+
elif backend_choice == "3":
279+
google_api_key, google_cloud_project, google_cloud_region = (
280+
_handle_login_with_google()
281+
)
259282
return google_api_key, google_cloud_project, google_cloud_region
260283

261284

285+
def _handle_login_with_google() -> (
286+
Tuple[Optional[str], Optional[str], Optional[str]]
287+
):
288+
"""Handles the "Login with Google" flow."""
289+
if not gcp_utils.check_adc():
290+
click.secho(
291+
"No Application Default Credentials found. "
292+
"Opening browser for login...",
293+
fg="yellow",
294+
)
295+
try:
296+
gcp_utils.login_adc()
297+
except RuntimeError as e:
298+
click.secho(str(e), fg="red")
299+
raise click.Abort()
300+
301+
# Check for existing Express project
302+
express_project = gcp_utils.retrieve_express_project()
303+
if express_project:
304+
api_key = express_project.get("api_key")
305+
project_id = express_project.get("project_id")
306+
region = express_project.get("region", "us-central1")
307+
if project_id:
308+
click.secho(f"Using existing Express project: {project_id}", fg="green")
309+
return api_key, project_id, region
310+
311+
# Check for existing full GCP projects
312+
projects = gcp_utils.list_gcp_projects(limit=20)
313+
if projects:
314+
click.secho("Recently created Google Cloud projects found:", fg="green")
315+
click.echo("0. Enter project ID manually")
316+
for i, (p_id, p_name) in enumerate(projects, 1):
317+
click.echo(f"{i}. {p_name} ({p_id})")
318+
319+
project_index = click.prompt(
320+
"Select a project",
321+
type=click.IntRange(0, len(projects)),
322+
)
323+
if project_index == 0:
324+
selected_project_id = _prompt_for_google_cloud(None)
325+
else:
326+
selected_project_id = projects[project_index - 1][0]
327+
region = _prompt_for_google_cloud_region(None)
328+
return None, selected_project_id, region
329+
else:
330+
if click.confirm(
331+
"No projects found automatically. Would you like to enter one"
332+
" manually?",
333+
default=False,
334+
):
335+
selected_project_id = _prompt_for_google_cloud(None)
336+
region = _prompt_for_google_cloud_region(None)
337+
return None, selected_project_id, region
338+
339+
# Check Express eligibility
340+
if gcp_utils.check_express_eligibility():
341+
click.secho(_EXPRESS_TOS_MSG, fg="yellow")
342+
if click.confirm("Do you accept the Terms of Service?", default=False):
343+
selected_region = click.prompt(
344+
"""\
345+
Choose a region for Express Mode:
346+
1. us-central1
347+
2. europe-west1
348+
3. asia-southeast1
349+
Choose region""",
350+
type=click.Choice(["1", "2", "3"]),
351+
default="1",
352+
)
353+
region_map = {
354+
"1": "us-central1",
355+
"2": "europe-west1",
356+
"3": "asia-southeast1",
357+
}
358+
region = region_map[selected_region]
359+
express_info = gcp_utils.sign_up_express(location=region)
360+
api_key = express_info.get("api_key")
361+
project_id = express_info.get("project_id")
362+
region = express_info.get("region", region)
363+
click.secho(
364+
f"Express Mode project created: {project_id}",
365+
fg="green",
366+
)
367+
return api_key, project_id, region
368+
369+
click.secho(_NOT_ELIGIBLE_MSG, fg="red")
370+
raise click.Abort()
371+
372+
262373
def _prompt_to_choose_type() -> str:
263374
"""Prompts user to choose type of agent to create."""
264375
type_choice = click.prompt(
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
# Copyright 2026 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+
"""Utilities for GCP authentication and Vertex AI Express Mode."""
16+
17+
from __future__ import annotations
18+
19+
import subprocess
20+
from typing import Any
21+
from typing import Dict
22+
from typing import List
23+
from typing import Optional
24+
from typing import Tuple
25+
26+
import google.auth
27+
import google.auth.exceptions
28+
from google.auth.transport.requests import AuthorizedSession
29+
from google.auth.transport.requests import Request
30+
from google.cloud import resourcemanager_v3
31+
import requests
32+
33+
_VERTEX_AI_ENDPOINT = "https://{location}-aiplatform.googleapis.com/v1beta1"
34+
35+
36+
def check_adc() -> bool:
37+
"""Checks if Application Default Credentials exist."""
38+
try:
39+
google.auth.default()
40+
return True
41+
except google.auth.exceptions.DefaultCredentialsError:
42+
return False
43+
44+
45+
def login_adc() -> None:
46+
"""Prompts user to login via gcloud ADC."""
47+
try:
48+
subprocess.run(
49+
["gcloud", "auth", "application-default", "login"], check=True
50+
)
51+
except (subprocess.CalledProcessError, FileNotFoundError):
52+
raise RuntimeError(
53+
"gcloud is not installed or failed to run. "
54+
"Please install gcloud to login to Application Default Credentials."
55+
)
56+
57+
58+
def get_access_token() -> str:
59+
"""Gets the ADC access token."""
60+
try:
61+
credentials, _ = google.auth.default()
62+
if not credentials.valid:
63+
credentials.refresh(Request())
64+
return credentials.token or ""
65+
except google.auth.exceptions.DefaultCredentialsError:
66+
raise RuntimeError("Application Default Credentials not found.")
67+
68+
69+
def _call_vertex_express_api(
70+
method: str,
71+
action: str,
72+
location: str = "us-central1",
73+
data: Optional[Dict[str, Any]] = None,
74+
params: Optional[Dict[str, Any]] = None,
75+
) -> Dict[str, Any]:
76+
"""Calls a Vertex AI Express API."""
77+
credentials, _ = google.auth.default()
78+
session = AuthorizedSession(credentials)
79+
url = f"{_VERTEX_AI_ENDPOINT.format(location=location)}/vertexExpress{action}"
80+
headers = {
81+
"Content-Type": "application/json",
82+
}
83+
84+
if method == "GET":
85+
response = session.get(url, headers=headers, params=params)
86+
elif method == "POST":
87+
response = session.post(url, headers=headers, json=data, params=params)
88+
else:
89+
raise ValueError(f"Unsupported method: {method}")
90+
91+
response.raise_for_status()
92+
return response.json()
93+
94+
95+
def retrieve_express_project(
96+
location: str = "us-central1",
97+
) -> Optional[Dict[str, Any]]:
98+
"""Retrieves existing Express project info."""
99+
try:
100+
response = _call_vertex_express_api(
101+
"GET",
102+
":retrieveExpressProject",
103+
location=location,
104+
params={"get_default_api_key": True},
105+
)
106+
project = response.get("expressProject")
107+
if not project:
108+
return None
109+
110+
return {
111+
"project_id": project.get("projectId"),
112+
"api_key": project.get("defaultApiKey"),
113+
"region": project.get("region", location),
114+
}
115+
except requests.exceptions.HTTPError as e:
116+
if e.response.status_code == 404:
117+
return None
118+
raise
119+
120+
121+
def check_express_eligibility(
122+
location: str = "us-central1",
123+
) -> bool:
124+
"""Checks if user is eligible for Express Mode."""
125+
try:
126+
result = _call_vertex_express_api(
127+
"GET", "/Eligibility:check", location=location
128+
)
129+
return result.get("eligibility") == "IN_SCOPE"
130+
except (requests.exceptions.HTTPError, KeyError) as e:
131+
return False
132+
133+
134+
def sign_up_express(
135+
location: str = "us-central1",
136+
) -> Dict[str, Any]:
137+
"""Signs up for Express Mode."""
138+
project = _call_vertex_express_api(
139+
"POST",
140+
":signUp",
141+
location=location,
142+
data={"region": location, "tos_accepted": True},
143+
)
144+
return {
145+
"project_id": project.get("projectId"),
146+
"api_key": project.get("defaultApiKey"),
147+
"region": project.get("region", location),
148+
}
149+
150+
151+
def list_gcp_projects(limit: int = 20) -> List[Tuple[str, str]]:
152+
"""Lists GCP projects available to the user.
153+
154+
Args:
155+
limit: The maximum number of projects to return.
156+
157+
Returns:
158+
A list of (project_id, name) tuples.
159+
"""
160+
try:
161+
client = resourcemanager_v3.ProjectsClient()
162+
search_results = client.search_projects()
163+
164+
projects = []
165+
for project in search_results:
166+
if len(projects) >= limit:
167+
break
168+
projects.append(
169+
(project.project_id, project.display_name or project.project_id)
170+
)
171+
return projects
172+
except Exception:
173+
return []

0 commit comments

Comments
 (0)