Skip to content

Commit 6dbe851

Browse files
yeesiancopybara-github
authored andcommitted
chore: Add back unit tests for CLI utility to deploy to AgentEngine
Co-authored-by: Yeesian Ng <ysian@google.com> PiperOrigin-RevId: 856749290
1 parent 6ad18cc commit 6dbe851

4 files changed

Lines changed: 149 additions & 25 deletions

File tree

src/google/adk/cli/cli_deploy.py

Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,44 @@
2020
import subprocess
2121
from typing import Final
2222
from typing import Optional
23+
import warnings
2324

2425
import click
2526
from packaging.version import parse
2627

2728
_IS_WINDOWS = os.name == 'nt'
2829
_GCLOUD_CMD = 'gcloud.cmd' if _IS_WINDOWS else 'gcloud'
2930
_LOCAL_STORAGE_FLAG_MIN_VERSION: Final[str] = '1.21.0'
31+
_AGENT_ENGINE_REQUIREMENT: Final[str] = (
32+
'google-cloud-aiplatform[adk,agent_engines]'
33+
)
34+
35+
36+
def _ensure_agent_engine_dependency(requirements_txt_path: str) -> None:
37+
"""Ensures staged requirements include Agent Engine dependencies."""
38+
if not os.path.exists(requirements_txt_path):
39+
raise FileNotFoundError(
40+
f'requirements.txt not found at: {requirements_txt_path}'
41+
)
42+
43+
requirements = ''
44+
with open(requirements_txt_path, 'r', encoding='utf-8') as f:
45+
requirements = f.read()
46+
47+
for line in requirements.splitlines():
48+
stripped = line.strip()
49+
if (
50+
stripped
51+
and not stripped.startswith('#')
52+
and stripped.startswith('google-cloud-aiplatform')
53+
):
54+
return
55+
56+
with open(requirements_txt_path, 'a', encoding='utf-8') as f:
57+
if requirements and not requirements.endswith('\n'):
58+
f.write('\n')
59+
f.write(_AGENT_ENGINE_REQUIREMENT + '\n')
60+
3061

3162
_DOCKERFILE_TEMPLATE: Final[str] = """
3263
FROM python:3.11-slim
@@ -656,7 +687,7 @@ def to_agent_engine(
656687
agent_folder: str,
657688
temp_folder: Optional[str] = None,
658689
adk_app: str,
659-
staging_bucket: str,
690+
staging_bucket: Optional[str] = None,
660691
trace_to_cloud: Optional[bool] = None,
661692
api_key: Optional[str] = None,
662693
adk_app_object: Optional[str] = None,
@@ -699,7 +730,8 @@ def to_agent_engine(
699730
files. It will be replaced with the generated files if it already exists.
700731
adk_app (str): The name of the file (without .py) containing the AdkApp
701732
instance.
702-
staging_bucket (str): The GCS bucket for staging the deployment artifacts.
733+
staging_bucket (str): Deprecated. This argument is no longer required or
734+
used.
703735
trace_to_cloud (bool): Whether to enable Cloud Trace.
704736
api_key (str): Optional. The API key to use for Express Mode.
705737
If not provided, the API key from the GOOGLE_API_KEY environment variable
@@ -729,26 +761,41 @@ def to_agent_engine(
729761
app_name = os.path.basename(agent_folder)
730762
display_name = display_name or app_name
731763
parent_folder = os.path.dirname(agent_folder)
732-
if parent_folder != os.getcwd():
733-
click.echo(f'Please deploy from the project dir: {parent_folder}')
734-
return
735-
tmp_app_name = app_name + '_tmp' + datetime.now().strftime('%Y%m%d_%H%M%S')
736-
temp_folder = temp_folder or tmp_app_name
737-
agent_src_path = os.path.join(parent_folder, temp_folder)
738-
click.echo(f'Staging all files in: {agent_src_path}')
739764
adk_app_object = adk_app_object or 'root_agent'
740765
if adk_app_object not in ['root_agent', 'app']:
741766
click.echo(
742767
f'Invalid adk_app_object: {adk_app_object}. Please use "root_agent"'
743768
' or "app".'
744769
)
745770
return
771+
if staging_bucket:
772+
warnings.warn(
773+
'WARNING: `staging_bucket` is deprecated and will be removed in a'
774+
' future release. Please drop it from the list of arguments.',
775+
DeprecationWarning,
776+
stacklevel=2,
777+
)
778+
779+
original_cwd = os.getcwd()
780+
did_change_cwd = False
781+
if parent_folder != original_cwd:
782+
click.echo(
783+
'Agent Engine deployment uses relative paths; temporarily switching '
784+
f'working directory to: {parent_folder}'
785+
)
786+
os.chdir(parent_folder)
787+
did_change_cwd = True
788+
tmp_app_name = app_name + '_tmp' + datetime.now().strftime('%Y%m%d_%H%M%S')
789+
temp_folder = temp_folder or tmp_app_name
790+
agent_src_path = os.path.join(parent_folder, temp_folder)
791+
click.echo(f'Staging all files in: {agent_src_path}')
746792
# remove agent_src_path if it exists
747793
if os.path.exists(agent_src_path):
748794
click.echo('Removing existing files')
749795
shutil.rmtree(agent_src_path)
750796

751797
try:
798+
click.echo(f'Staging all files in: {agent_src_path}')
752799
ignore_patterns = None
753800
ae_ignore_path = os.path.join(agent_folder, '.ae_ignore')
754801
if os.path.exists(ae_ignore_path):
@@ -757,15 +804,18 @@ def to_agent_engine(
757804
patterns = [pattern.strip() for pattern in f.readlines()]
758805
ignore_patterns = shutil.ignore_patterns(*patterns)
759806
click.echo('Copying agent source code...')
760-
shutil.copytree(agent_folder, agent_src_path, ignore=ignore_patterns)
807+
shutil.copytree(
808+
agent_folder,
809+
agent_src_path,
810+
ignore=ignore_patterns,
811+
dirs_exist_ok=True,
812+
)
761813
click.echo('Copying agent source code complete.')
762814

763815
project = _resolve_project(project)
764816

765817
click.echo('Resolving files and dependencies...')
766818
agent_config = {}
767-
if staging_bucket:
768-
agent_config['staging_bucket'] = staging_bucket
769819
if not agent_engine_config_file:
770820
# Attempt to read the agent engine config from .agent_engine_config.json in the dir (if any).
771821
agent_engine_config_file = os.path.join(
@@ -808,8 +858,9 @@ def to_agent_engine(
808858
if not os.path.exists(requirements_txt_path):
809859
click.echo(f'Creating {requirements_txt_path}...')
810860
with open(requirements_txt_path, 'w', encoding='utf-8') as f:
811-
f.write('google-cloud-aiplatform[adk,agent_engines]')
861+
f.write(_AGENT_ENGINE_REQUIREMENT + '\n')
812862
click.echo(f'Created {requirements_txt_path}')
863+
_ensure_agent_engine_dependency(requirements_txt_path)
813864
agent_config['requirements_file'] = f'{temp_folder}/requirements.txt'
814865

815866
env_vars = {}
@@ -940,7 +991,9 @@ def to_agent_engine(
940991
click.secho(f'✅ Updated agent engine: {agent_engine_id}', fg='green')
941992
finally:
942993
click.echo(f'Cleaning up the temp folder: {temp_folder}')
943-
shutil.rmtree(temp_folder)
994+
shutil.rmtree(agent_src_path)
995+
if did_change_cwd:
996+
os.chdir(original_cwd)
944997

945998

946999
def to_gke(

src/google/adk/cli/cli_tools_click.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1031,6 +1031,19 @@ def wrapper(*args, **kwargs):
10311031
return decorator
10321032

10331033

1034+
def _deprecate_staging_bucket(ctx, param, value):
1035+
if value:
1036+
click.echo(
1037+
click.style(
1038+
f"WARNING: --{param} is deprecated and will be removed. Please"
1039+
" leave it unspecified.",
1040+
fg="yellow",
1041+
),
1042+
err=True,
1043+
)
1044+
return value
1045+
1046+
10341047
def deprecated_adk_services_options():
10351048
"""Deprecated ADK services options."""
10361049

@@ -1689,10 +1702,8 @@ def cli_migrate_session(
16891702
"--staging_bucket",
16901703
type=str,
16911704
default=None,
1692-
help=(
1693-
"Optional. GCS bucket for staging the deployment artifacts. It will be"
1694-
" ignored if api_key is set."
1695-
),
1705+
help="Deprecated. This argument is no longer required or used.",
1706+
callback=_deprecate_staging_bucket,
16961707
)
16971708
@click.option(
16981709
"--agent_engine_id",
@@ -1827,16 +1838,14 @@ def cli_deploy_agent_engine(
18271838
18281839
# With Google Cloud Project and Region
18291840
adk deploy agent_engine --project=[project] --region=[region]
1830-
--staging_bucket=[staging_bucket] --display_name=[app_name]
1831-
my_agent
1841+
--display_name=[app_name] my_agent
18321842
"""
18331843
logging.getLogger("vertexai_genai.agentengines").setLevel(logging.INFO)
18341844
try:
18351845
cli_deploy.to_agent_engine(
18361846
agent_folder=agent,
18371847
project=project,
18381848
region=region,
1839-
staging_bucket=staging_bucket,
18401849
agent_engine_id=agent_engine_id,
18411850
trace_to_cloud=trace_to_cloud,
18421851
api_key=api_key,

tests/unittests/cli/utils/test_cli_deploy.py

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
from typing import Any
2727
from typing import Callable
2828
from typing import Dict
29-
from typing import Generator
3029
from typing import List
3130
from typing import Tuple
3231
from unittest import mock
@@ -227,6 +226,72 @@ def test_get_service_option_by_adk_version(
227226
assert actual.rstrip() == expected.rstrip()
228227

229228

229+
@pytest.mark.parametrize("include_requirements", [True, False])
230+
def test_to_agent_engine_happy_path(
231+
monkeypatch: pytest.MonkeyPatch,
232+
agent_dir: Callable[[bool, bool], Path],
233+
include_requirements: bool,
234+
) -> None:
235+
"""Tests the happy path for the `to_agent_engine` function."""
236+
rmtree_recorder = _Recorder()
237+
monkeypatch.setattr(shutil, "rmtree", rmtree_recorder)
238+
create_recorder = _Recorder()
239+
240+
fake_vertexai = types.ModuleType("vertexai")
241+
242+
class _FakeAgentEngines:
243+
244+
def create(self, *, config: Dict[str, Any]) -> Any:
245+
create_recorder(config=config)
246+
return types.SimpleNamespace(
247+
api_resource=types.SimpleNamespace(
248+
name="projects/p/locations/l/reasoningEngines/e"
249+
)
250+
)
251+
252+
def update(self, *, name: str, config: Dict[str, Any]) -> None:
253+
del name
254+
del config
255+
256+
class _FakeVertexClient:
257+
258+
def __init__(self, *args: Any, **kwargs: Any) -> None:
259+
del args
260+
del kwargs
261+
self.agent_engines = _FakeAgentEngines()
262+
263+
fake_vertexai.Client = _FakeVertexClient
264+
monkeypatch.setitem(sys.modules, "vertexai", fake_vertexai)
265+
src_dir = agent_dir(include_requirements, False)
266+
tmp_dir = src_dir.parent / "tmp"
267+
cli_deploy.to_agent_engine(
268+
agent_folder=str(src_dir),
269+
temp_folder="tmp",
270+
adk_app="my_adk_app",
271+
trace_to_cloud=True,
272+
project="my-gcp-project",
273+
region="us-central1",
274+
display_name="My Test Agent",
275+
description="A test agent.",
276+
)
277+
agent_file = tmp_dir / "agent.py"
278+
assert agent_file.is_file()
279+
init_file = tmp_dir / "__init__.py"
280+
assert init_file.is_file()
281+
adk_app_file = tmp_dir / "my_adk_app.py"
282+
assert adk_app_file.is_file()
283+
content = adk_app_file.read_text()
284+
assert "from .agent import root_agent" in content
285+
assert "adk_app = AdkApp(" in content
286+
assert "agent=root_agent" in content
287+
assert "enable_tracing=True" in content
288+
reqs_path = tmp_dir / "requirements.txt"
289+
assert reqs_path.is_file()
290+
assert "google-cloud-aiplatform[adk,agent_engines]" in reqs_path.read_text()
291+
assert len(create_recorder.calls) == 1
292+
assert str(rmtree_recorder.get_last_call_args()[0]) == str(tmp_dir)
293+
294+
230295
@pytest.mark.parametrize("include_requirements", [True, False])
231296
def test_to_gke_happy_path(
232297
monkeypatch: pytest.MonkeyPatch,

tests/unittests/cli/utils/test_cli_tools_click.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -400,8 +400,6 @@ def test_cli_deploy_agent_engine_success(
400400
"test-proj",
401401
"--region",
402402
"us-central1",
403-
"--staging_bucket",
404-
"gs://mybucket",
405403
str(agent_dir),
406404
],
407405
)
@@ -410,7 +408,6 @@ def test_cli_deploy_agent_engine_success(
410408
called_kwargs = rec.calls[0][1]
411409
assert called_kwargs.get("project") == "test-proj"
412410
assert called_kwargs.get("region") == "us-central1"
413-
assert called_kwargs.get("staging_bucket") == "gs://mybucket"
414411

415412

416413
# cli deploy gke

0 commit comments

Comments
 (0)