gemini/agents/genai-experience-concierge/scripts/cli/langgraph_demo.py (260 lines of code) (raw):
# Copyright 2025 Google. This software is provided as-is, without warranty or
# representation for any use or purpose. Your use of it is subject to your
# agreement with Google.
"""Tools for deploying the end-to-end Concierge demo."""
# pylint: disable=too-many-arguments,too-many-positional-arguments
from typing import Any
import uuid
import click
from scripts.langgraph_demo import (
backend,
cloudbuild,
dataset,
defaults,
frontend,
terraform,
)
import yaml
@click.option(
"--project-id",
required=True,
help="Target project ID to create the dataset in.",
)
@click.option(
"--location",
required=False,
help="Multi-region location for the BigQuery dataset (US, EU, etc).",
default="US",
)
def create_dataset(project_id: str, location: str = "US") -> dataset.GeneratedDataset:
"""Create a mock Cymbal Retail dataset."""
return dataset.create(project=project_id, location=location)
# pylint: disable=too-many-locals
@click.option(
"--seed-project",
required=True,
help="Seed project ID used when creating the demo project.",
)
@click.option(
"--project-id",
required=True,
help="Target project ID to create for the demo.",
)
@click.option(
"--billing-account",
required=True,
help="GCP billing account ID to use for project creation.",
)
@click.option(
"--support-email",
required=True,
help="Support email to display on the demo OAuth screen.",
)
@click.option(
"--demo-users",
required=True,
help=(
"Member(s) to grant access to the hosted Gen AI Concierge demo."
" Each entry should include the kind of member (e.g. user:*, group:*, etc)."
),
multiple=True,
)
@click.option(
"--state-bucket",
required=True,
help="GCS state backend bucket for managing terraform state.",
)
@click.option(
"--region",
required=False,
help="Default region for creating resources (default=us-central1).",
default="us-central1",
)
@click.option(
"--random-project-suffix/--no-random-project-suffix",
required=False,
help="Indicate if a random suffix should be added to the project ID (default=False)",
default=False,
)
@click.option(
"--state-bucket-prefix",
required=False,
help="Prefix to insert to GCS path when storing terraform state.",
default=None,
)
@click.option(
"--org-id",
required=False,
help="GCP organization to create the project in.",
default=None,
)
@click.option(
"--folder-id",
required=False,
help="GCP folder to create the project in.",
default=None,
)
@click.option(
"--auto-approve/--no-auto-approve",
required=False,
help="Indicate if the terraform should auto-apply changes (Default=False).",
default=False,
)
def deploy(
seed_project: str,
project_id: str,
region: str,
random_project_suffix: bool,
billing_account: str,
support_email: str,
demo_users: tuple[str],
state_bucket: str,
state_bucket_prefix: str | None = None,
org_id: str | None = None,
folder_id: str | None = None,
auto_approve: bool = False,
) -> dict[str, Any]:
"""Deploy the end-to-end Concierge demo."""
# only use default source dirs. Maybe enable user-provided in future?
terraform_dir = str(defaults.TERRAFORM_DIR)
backend_dir = str(defaults.BACKEND_DIR)
frontend_dir = str(defaults.FRONTEND_DIR)
cymbal_product_path = str(defaults.PRODUCT_GCS_DATASET_PATH)
cymbal_store_path = str(defaults.STORE_GCS_DATASET_PATH)
cymbal_inventory_path = str(defaults.INVENTORY_GCS_DATASET_PATH)
log_section("Initializing terraform module...")
terraform.init(
terraform_dir=terraform_dir,
state_bucket=state_bucket,
state_bucket_prefix=state_bucket_prefix,
)
log_section("Applying terraform...")
terraform.apply(
terraform_dir=terraform_dir,
seed_project=seed_project,
project_id=project_id,
region=region,
random_project_suffix=random_project_suffix,
billing_account=billing_account,
support_email=support_email,
demo_users=demo_users,
org_id=org_id,
folder_id=folder_id,
auto_approve=auto_approve,
)
tf_outputs = terraform.outputs(terraform_dir=terraform_dir)
real_project_id = tf_outputs["project-id"]["value"]
cymbal_dataset_id = tf_outputs["cymbal-retail-dataset-id"]["value"]
cymbal_dataset_location = tf_outputs["cymbal-retail-dataset-location"]["value"]
cymbal_connection_id = tf_outputs["cymbal-retail-connection-id"]["value"]
build_service_account = tf_outputs["cloud-build-service-account"]["value"]
build_service_account_id = (
f"projects/{project_id}/serviceAccounts/{build_service_account}"
)
backend_service_account = tf_outputs["cloud-run-service-account"]["value"]
artifact_registry_repository = tf_outputs["artifact-registry-repo"]["value"]
artifact_registry_location = tf_outputs["artifact-registry-location"]["value"]
network_id = str(tf_outputs["vpc-id"]["value"])
network_name = network_id.rsplit("/", maxsplit=1)[-1]
subnetwork_id = str(tf_outputs["subnet-id"]["value"])
subnetwork_name = subnetwork_id.rsplit("/", maxsplit=1)[-1]
alloydb_secret_name = tf_outputs["concierge-alloydb-connection-secret-name"][
"value"
]
frontend_service_account = tf_outputs["app-engine-service-account"]["value"]
log_section("Creating mock Cymbal Retail dataset...")
generated_dataset = dataset.create(
project=project_id,
location=cymbal_dataset_location,
dataset_id=cymbal_dataset_id,
connection_id=cymbal_connection_id,
product_path=cymbal_product_path,
store_path=cymbal_store_path,
inventory_path=cymbal_inventory_path,
)
tag_id = uuid.uuid1()
log_section("Building backend agent server...")
backend_image_url = (
f"{artifact_registry_location}-docker.pkg.dev"
f"/{project_id}/{artifact_registry_repository}/"
f"backend:{tag_id.hex}"
)
cloudbuild.build(
project=project_id,
service_account=build_service_account_id,
image_url=backend_image_url,
source_dir=backend_dir,
)
backend_service = "concierge"
log_section(f"Deploying backend agent server ({backend_service})...")
backend.deploy(
service=backend_service,
project=project_id,
region=region,
network=network_id,
subnetwork=subnetwork_id,
service_account=backend_service_account,
image_url=backend_image_url,
alloydb_secret_name=alloydb_secret_name,
cymbal_dataset_location=cymbal_dataset_location,
cymbal_products_table_uri=generated_dataset["products_table_uri"],
cymbal_stores_table_uri=generated_dataset["stores_table_uri"],
cymbal_inventory_table_uri=generated_dataset["inventory_table_uri"],
cymbal_embedding_model_uri=generated_dataset["embedding_model_uri"],
)
backend_service_description = backend.describe(
project=project_id,
region=region,
service=backend_service,
)
backend_host = backend_service_description["status"]["url"]
log_section("Adding frontend SA as an invoker of the backend service...")
frontend_sa_member = f"serviceAccount:{frontend_service_account}"
backend.add_invoker(
service=backend_service,
project=project_id,
region=region,
invoker=frontend_sa_member,
)
log_section("Building frontend demo server...")
frontend_image_url = (
f"{artifact_registry_location}-docker.pkg.dev"
f"/{project_id}/{artifact_registry_repository}/"
f"frontend:{tag_id.hex}"
)
cloudbuild.build(
project=project_id,
service_account=build_service_account_id,
image_url=frontend_image_url,
source_dir=frontend_dir,
)
log_section("Deploying frontend demo...")
frontend.deploy(
project=project_id,
network=network_name,
subnetwork=subnetwork_name,
service_account=frontend_service_account,
concierge_host=backend_host,
frontend_image_url=frontend_image_url,
)
outputs = {
"project_id": real_project_id,
"region": region,
"network": network_name,
"subnetwork": subnetwork_name,
"terraform": {
"state_bucket": state_bucket,
"state_bucket_prefix": state_bucket_prefix,
},
"cymbal_bigquery_dataset": generated_dataset,
"build_service_account": build_service_account,
"backend": {
"service": backend_service,
"host": backend_host,
"image_url": backend_image_url,
"service_account": backend_service_account,
},
"frontend": {
"service": "default",
"host": f"{project_id}.uc.r.appspot.com",
"image_url": frontend_image_url,
"service_account": frontend_service_account,
},
}
output_str = yaml.safe_dump(outputs)
click.echo(f"Displaying the key generated resources:\n\n{output_str}")
log_section("🚀 End-to-end deployment is complete! 🚀")
return outputs
# pylint: enable=too-many-locals
def log_section(message: str) -> None:
"""Log section with spacing and bold styling."""
click.echo("\n\n" + click.style(message, bold=True) + "\n\n")