Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 116 additions & 5 deletions src/google/adk/cli/cli_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import click

from ..apps.app import validate_app_name
from .utils import gcp_utils

_INIT_PY_TEMPLATE = """\
from . import agent
Expand Down Expand Up @@ -61,18 +62,36 @@
https://google.github.io/adk-docs/agents/models
"""

_EXPRESS_TOS_MSG = """
Google Cloud Express Mode Terms of Service: https://cloud.google.com/terms/google-cloud-express
By continuing, you agree to the Terms of Service for Vertex AI Express Mode.
Would you like to proceed? (yes/no)
"""

_NOT_ELIGIBLE_MSG = """
You are not eligible for Express Mode.
Please follow these instructions to set up a full Google Cloud project:
https://google.github.io/adk-docs/get-started/quickstart/#gemini---google-cloud-vertex-ai
"""

_SUCCESS_MSG_CODE = """
Agent created in {agent_folder}:
- .env
- __init__.py
- agent.py

⚠️ WARNING: Secrets (like GOOGLE_API_KEY) are stored in .env.
Please ensure .env is added to your .gitignore to avoid committing secrets to version control.
"""

_SUCCESS_MSG_CONFIG = """
Agent created in {agent_folder}:
- .env
- __init__.py
- root_agent.yaml

⚠️ WARNING: Secrets (like GOOGLE_API_KEY) are stored in .env.
Please ensure .env is added to your .gitignore to avoid committing secrets to version control.
"""


Expand Down Expand Up @@ -187,10 +206,10 @@ def _generate_files(

with open(dotenv_file_path, "w", encoding="utf-8") as f:
lines = []
if google_api_key:
lines.append("GOOGLE_GENAI_USE_VERTEXAI=0")
elif google_cloud_project and google_cloud_region:
if google_cloud_project and google_cloud_region:
lines.append("GOOGLE_GENAI_USE_VERTEXAI=1")
elif google_api_key:
lines.append("GOOGLE_GENAI_USE_VERTEXAI=0")
if google_api_key:
lines.append(f"GOOGLE_API_KEY={google_api_key}")
if google_cloud_project:
Expand Down Expand Up @@ -247,18 +266,110 @@ def _prompt_to_choose_backend(
A tuple of (google_api_key, google_cloud_project, google_cloud_region).
"""
backend_choice = click.prompt(
"1. Google AI\n2. Vertex AI\nChoose a backend",
type=click.Choice(["1", "2"]),
"1. Google AI\n2. Vertex AI\n3. Login with Google\nChoose a backend",
type=click.Choice(["1", "2", "3"]),
)
if backend_choice == "1":
google_api_key = _prompt_for_google_api_key(google_api_key)
elif backend_choice == "2":
click.secho(_GOOGLE_CLOUD_SETUP_MSG, fg="green")
google_cloud_project = _prompt_for_google_cloud(google_cloud_project)
google_cloud_region = _prompt_for_google_cloud_region(google_cloud_region)
elif backend_choice == "3":
google_api_key, google_cloud_project, google_cloud_region = (
_handle_login_with_google()
)
return google_api_key, google_cloud_project, google_cloud_region


def _handle_login_with_google() -> (
Tuple[Optional[str], Optional[str], Optional[str]]
):
"""Handles the "Login with Google" flow."""
if not gcp_utils.check_adc():
click.secho(
"No Application Default Credentials found. "
"Opening browser for login...",
fg="yellow",
)
try:
gcp_utils.login_adc()
except RuntimeError as e:
click.secho(str(e), fg="red")
raise click.Abort()

# Check for existing Express project
express_project = gcp_utils.retrieve_express_project()
if express_project:
api_key = express_project.get("api_key")
project_id = express_project.get("project_id")
region = express_project.get("region", "us-central1")
if project_id:
click.secho(f"Using existing Express project: {project_id}", fg="green")
return api_key, project_id, region

# Check for existing full GCP projects
projects = gcp_utils.list_gcp_projects(limit=20)
if projects:
click.secho("Recently created Google Cloud projects found:", fg="green")
click.echo("0. Enter project ID manually")
for i, (p_id, p_name) in enumerate(projects, 1):
click.echo(f"{i}. {p_name} ({p_id})")

project_index = click.prompt(
"Select a project",
type=click.IntRange(0, len(projects)),
)
if project_index == 0:
selected_project_id = _prompt_for_google_cloud(None)
else:
selected_project_id = projects[project_index - 1][0]
region = _prompt_for_google_cloud_region(None)
return None, selected_project_id, region
else:
if click.confirm(
"No projects found automatically. Would you like to enter one"
" manually?",
default=False,
):
selected_project_id = _prompt_for_google_cloud(None)
region = _prompt_for_google_cloud_region(None)
return None, selected_project_id, region

# Check Express eligibility
if gcp_utils.check_express_eligibility():
click.secho(_EXPRESS_TOS_MSG, fg="yellow")
if click.confirm("Do you accept the Terms of Service?", default=False):
selected_region = click.prompt(
"""\
Choose a region for Express Mode:
1. us-central1
2. europe-west1
3. asia-southeast1
Choose region""",
type=click.Choice(["1", "2", "3"]),
default="1",
)
region_map = {
"1": "us-central1",
"2": "europe-west1",
"3": "asia-southeast1",
}
region = region_map[selected_region]
express_info = gcp_utils.sign_up_express(location=region)
api_key = express_info.get("api_key")
project_id = express_info.get("project_id")
region = express_info.get("region", region)
click.secho(
f"Express Mode project created: {project_id}",
fg="green",
)
return api_key, project_id, region

click.secho(_NOT_ELIGIBLE_MSG, fg="red")
raise click.Abort()


def _prompt_to_choose_type() -> str:
"""Prompts user to choose type of agent to create."""
type_choice = click.prompt(
Expand Down
176 changes: 176 additions & 0 deletions src/google/adk/cli/utils/gcp_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Utilities for GCP authentication and Vertex AI Express Mode."""

from __future__ import annotations

import subprocess
from typing import Any
from typing import Dict
from typing import List
from typing import Optional
from typing import Tuple

import google.auth
import google.auth.exceptions
from google.auth.transport.requests import AuthorizedSession
from google.auth.transport.requests import Request
from google.cloud import resourcemanager_v3
import requests

# TODO: Update to production endpoint before making this public.
_STAGING_ENDPOINT = (
"https://{location}-staging-aiplatform.sandbox.googleapis.com/v1beta1"
)


def check_adc() -> bool:
"""Checks if Application Default Credentials exist."""
try:
google.auth.default()
return True
except google.auth.exceptions.DefaultCredentialsError:
return False


def login_adc() -> None:
"""Prompts user to login via gcloud ADC."""
try:
subprocess.run(
["gcloud", "auth", "application-default", "login"], check=True
)
except (subprocess.CalledProcessError, FileNotFoundError):
raise RuntimeError(
"gcloud is not installed or failed to run. "
"Please install gcloud to login to Application Default Credentials."
)


def get_access_token() -> str:
"""Gets the ADC access token."""
try:
credentials, _ = google.auth.default()
if not credentials.valid:
credentials.refresh(Request())
return credentials.token or ""
except google.auth.exceptions.DefaultCredentialsError:
raise RuntimeError("Application Default Credentials not found.")


def _call_vertex_express_api(
method: str,
action: str,
location: str = "us-central1",
data: Optional[Dict[str, Any]] = None,
params: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""Calls a Vertex AI Express API."""
credentials, _ = google.auth.default()
session = AuthorizedSession(credentials)
url = f"{_STAGING_ENDPOINT.format(location=location)}/vertexExpress{action}"
headers = {
"Content-Type": "application/json",
}

if method == "GET":
response = session.get(url, headers=headers, params=params)
elif method == "POST":
response = session.post(url, headers=headers, json=data, params=params)
else:
raise ValueError(f"Unsupported method: {method}")

response.raise_for_status()
return response.json()


def retrieve_express_project(
location: str = "us-central1",
) -> Optional[Dict[str, Any]]:
"""Retrieves existing Express project info."""
try:
response = _call_vertex_express_api(
"GET",
":retrieveExpressProject",
location=location,
params={"get_default_api_key": True},
)
project = response.get("expressProject")
if not project:
return None

return {
"project_id": project.get("projectId"),
"api_key": project.get("defaultApiKey"),
"region": project.get("region", location),
}
except requests.exceptions.HTTPError as e:
if e.response.status_code == 404:
return None
raise


def check_express_eligibility(
location: str = "us-central1",
) -> bool:
"""Checks if user is eligible for Express Mode."""
try:
result = _call_vertex_express_api(
"GET", "/Eligibility:check", location=location
)
return result.get("eligibility") == "IN_SCOPE"
except (requests.exceptions.HTTPError, KeyError) as e:
return False


def sign_up_express(
location: str = "us-central1",
) -> Dict[str, Any]:
"""Signs up for Express Mode."""
project = _call_vertex_express_api(
"POST",
":signUp",
location=location,
data={"region": location, "tos_accepted": True},
)
return {
"project_id": project.get("projectId"),
"api_key": project.get("defaultApiKey"),
"region": project.get("region", location),
}


def list_gcp_projects(limit: int = 20) -> List[Tuple[str, str]]:
"""Lists GCP projects available to the user.

Args:
limit: The maximum number of projects to return.

Returns:
A list of (project_id, name) tuples.
"""
try:
client = resourcemanager_v3.ProjectsClient()
search_results = client.search_projects()

projects = []
for project in search_results:
if len(projects) >= limit:
break
projects.append(
(project.project_id, project.display_name or project.project_id)
)
return projects
except Exception:
return []
Loading
Loading