Repository: google/adk-python

Issue:
adk deploy cloud_run fixes the Python version at 3.11, but some sample agents depend on Python 3.12.
**Describe the bug**
The data science agent from adk-samples depends on Python 3.12, but `adk deploy cloud_run` uses a hard-coded container template that uses Python 3.11. This leads to errors when the user runs the data science agent in a Cloud Run deployment. The specific error generated is a syntax error that results from changes in f-string parsing between 3.11 and 3.12.

**To Reproduce**
Steps to reproduce the behavior:
1. Install current versions of `adk-python` and `adk-samples`.
2. Run `adk deploy cloud_run` for the `data-science` agent.
3. Navigate to the web interface for the deployed agent.
4. Type anything in the web interface.
5. Get an error: `SyntaxError: unterminated string literal (detected at line 48)`

**Expected behavior**
Data science agent responds correctly.

Sample correct interaction:
User: Hello, what can you do for me?
Agent: Hello! I'm a data science and data analytics multi-agent system. I can help you with:

Querying databases: I can retrieve data from the forecasting_sticker_sales.test and forecasting_sticker_sales.train tables based on your questions.
Data analysis: I can perform various data science tasks and Python analysis on the data.
BigQuery ML: If you specifically ask for it, I can route your request to a BigQuery ML agent for tasks like model training, evaluation, or prediction.
Just let me know what you'd like to do with your data!


Discussion:
A short term workaround is to search for python:3.11-slim in your adk installation, and replace it with python:3.12-slim or python:3.13-slim.

A longer term solution is allowing a user to set this in the cli. Working on that. But thought you might like the workaround earlier. 



Patch:
diff --git a/src/google/adk/cli/cli_deploy.py b/src/google/adk/cli/cli_deploy.py
index 4888b2958b..e7848f5233 100644
--- a/src/google/adk/cli/cli_deploy.py
+++ b/src/google/adk/cli/cli_deploy.py
@@ -21,8 +21,10 @@
 import click
 from packaging.version import parse
 
+BASE_BUILD_IMAGE = 'python:3.11-slim'
+
 _DOCKERFILE_TEMPLATE = """
-FROM python:3.11-slim
+FROM {build_image}
 WORKDIR /app
 
 # Create a non-root user
@@ -130,6 +132,7 @@ def to_cloud_run(
     session_service_uri: Optional[str] = None,
     artifact_service_uri: Optional[str] = None,
     memory_service_uri: Optional[str] = None,
+    build_image: Optional[str] = BASE_BUILD_IMAGE,
     a2a: bool = False,
 ):
   """Deploys an agent to Google Cloud Run.
@@ -163,6 +166,7 @@ def to_cloud_run(
     session_service_uri: The URI of the session service.
     artifact_service_uri: The URI of the artifact service.
     memory_service_uri: The URI of the memory service.
+    build_image: The image to use for building the Dockerfile.
   """
   app_name = app_name or os.path.basename(agent_folder)
 
@@ -211,6 +215,7 @@ def to_cloud_run(
         adk_version=adk_version,
         host_option=host_option,
         a2a_option=a2a_option,
+        build_image=build_image,
     )
     dockerfile_path = os.path.join(temp_folder, 'Dockerfile')
     os.makedirs(temp_folder, exist_ok=True)
@@ -474,6 +479,7 @@ def to_gke(
     session_service_uri: Optional[str] = None,
     artifact_service_uri: Optional[str] = None,
     memory_service_uri: Optional[str] = None,
+    build_image: Optional[str] = BASE_BUILD_IMAGE,
     a2a: bool = False,
 ):
   """Deploys an agent to Google Kubernetes Engine(GKE).
@@ -495,6 +501,7 @@ def to_gke(
     session_service_uri: The URI of the session service.
     artifact_service_uri: The URI of the artifact service.
     memory_service_uri: The URI of the memory service.
+    build_image: The image to use for building the Dockerfile.
   """
   click.secho(
       '\n🚀 Starting ADK Agent Deployment to GKE...', fg='cyan', bold=True
@@ -556,6 +563,7 @@ def to_gke(
         adk_version=adk_version,
         host_option=host_option,
         a2a_option='--a2a' if a2a else '',
+        build_image=build_image,
     )
     dockerfile_path = os.path.join(temp_folder, 'Dockerfile')
     os.makedirs(temp_folder, exist_ok=True)
diff --git a/src/google/adk/cli/cli_tools_click.py b/src/google/adk/cli/cli_tools_click.py
index 3000edf788..faead35015 100644
--- a/src/google/adk/cli/cli_tools_click.py
+++ b/src/google/adk/cli/cli_tools_click.py
@@ -44,6 +44,8 @@
     case_sensitive=False,
 )
 
+BASE_BUILD_IMAGE = "python:3.11-slim"
+
 
 class HelpfulCommand(click.Command):
   """Command that shows full help on error instead of just the error message.
@@ -968,6 +970,13 @@ def cli_api_server(
     help="Optional. Any additional origins to allow for CORS.",
     multiple=True,
 )
+@click.option(
+    "--build_image",
+    type=str,
+    default=BASE_BUILD_IMAGE,
+    show_default=True,
+    help="Optional. The docker build version used in Cloud Run deployment. ",
+)
 # TODO: Add eval_storage_uri option back when evals are supported in Cloud Run.
 @adk_services_options()
 @deprecated_adk_services_options()
@@ -991,6 +1000,7 @@ def cli_deploy_cloud_run(
     session_db_url: Optional[str] = None,  # Deprecated
     artifact_storage_uri: Optional[str] = None,  # Deprecated
     a2a: bool = False,
+    build_image: Optional[str] = BASE_BUILD_IMAGE,
 ):
   """Deploys an agent to Cloud Run.
 
@@ -1029,6 +1039,7 @@ def cli_deploy_cloud_run(
         artifact_service_uri=artifact_service_uri,
         memory_service_uri=memory_service_uri,
         a2a=a2a,
+        build_image=build_image,
     )
   except Exception as e:
     click.secho(f"Deploy failed: {e}", fg="red", err=True)
@@ -1281,6 +1292,13 @@ def cli_deploy_agent_engine(
         " version in the dev environment)"
     ),
 )
+@click.option(
+    "--build_image",
+    type=str,
+    default=BASE_BUILD_IMAGE,
+    show_default=True,
+    help="Optional. The docker build version used in GKE deployment. ",
+)
 @adk_services_options()
 @click.argument(
     "agent",
@@ -1304,6 +1322,7 @@ def cli_deploy_gke(
     session_service_uri: Optional[str] = None,
     artifact_service_uri: Optional[str] = None,
     memory_service_uri: Optional[str] = None,
+    build_image: Optional[str] = BASE_BUILD_IMAGE,
 ):
   """Deploys an agent to GKE.
 
@@ -1330,6 +1349,7 @@ def cli_deploy_gke(
         session_service_uri=session_service_uri,
         artifact_service_uri=artifact_service_uri,
         memory_service_uri=memory_service_uri,
+        build_image=build_image,
     )
   except Exception as e:
     click.secho(f"Deploy failed: {e}", fg="red", err=True)


Test patch:
diff --git a/tests/unittests/cli/utils/test_cli_tools_click.py b/tests/unittests/cli/utils/test_cli_tools_click.py
index b57097ab01..57206121c2 100644
--- a/tests/unittests/cli/utils/test_cli_tools_click.py
+++ b/tests/unittests/cli/utils/test_cli_tools_click.py
@@ -168,11 +168,43 @@ def test_cli_deploy_cloud_run_success(
           "proj",
           "--region",
           "asia-northeast1",
+          "--build_image",
+          "my-custom-image",
           str(agent_dir),
       ],
   )
   assert result.exit_code == 0
   assert rec.calls, "cli_deploy.to_cloud_run must be invoked"
+  called_kwargs = rec.calls[0][1]
+  assert called_kwargs.get("build_image") == "my-custom-image"
+
+
+def test_cli_deploy_cloud_run_default_build_image(
+    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+) -> None:
+  """Test that the default build_image is used."""
+  rec = _Recorder()
+  monkeypatch.setattr(cli_tools_click.cli_deploy, "to_cloud_run", rec)
+
+  agent_dir = tmp_path / "agent2"
+  agent_dir.mkdir()
+  runner = CliRunner()
+  result = runner.invoke(
+      cli_tools_click.main,
+      [
+          "deploy",
+          "cloud_run",
+          "--project",
+          "proj",
+          "--region",
+          "asia-northeast1",
+          str(agent_dir),
+      ],
+  )
+  assert result.exit_code == 0
+  assert rec.calls, "cli_deploy.to_cloud_run must be invoked"
+  called_kwargs = rec.calls[0][1]
+  assert called_kwargs.get("build_image") == cli_tools_click.BASE_BUILD_IMAGE
 
 
 def test_cli_deploy_cloud_run_failure(
@@ -251,6 +283,8 @@ def test_cli_deploy_gke_success(
           "us-central1",
           "--cluster_name",
           "my-cluster",
+          "--build_image",
+          "my-gke-image",
           str(agent_dir),
       ],
   )
@@ -260,6 +294,37 @@ def test_cli_deploy_gke_success(
   assert called_kwargs.get("project") == "test-proj"
   assert called_kwargs.get("region") == "us-central1"
   assert called_kwargs.get("cluster_name") == "my-cluster"
+  assert called_kwargs.get("build_image") == "my-gke-image"
+
+
+def test_cli_deploy_gke_default_build_image(
+    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+) -> None:
+  """Test that the default build_image is used for gke."""
+  rec = _Recorder()
+  monkeypatch.setattr(cli_tools_click.cli_deploy, "to_gke", rec)
+
+  agent_dir = tmp_path / "agent_gke"
+  agent_dir.mkdir()
+  runner = CliRunner()
+  result = runner.invoke(
+      cli_tools_click.main,
+      [
+          "deploy",
+          "gke",
+          "--project",
+          "test-proj",
+          "--region",
+          "us-central1",
+          "--cluster_name",
+          "my-cluster",
+          str(agent_dir),
+      ],
+  )
+  assert result.exit_code == 0
+  assert rec.calls, "cli_deploy.to_gke must be invoked"
+  called_kwargs = rec.calls[0][1]
+  assert called_kwargs.get("build_image") == cli_tools_click.BASE_BUILD_IMAGE
 
 
 # cli eval