Skip to content

Commit 9296d4d

Browse files
shawn-yang-googlecopybara-github
authored andcommitted
feat: **Allow installation scripts in AgentEngine.**
PiperOrigin-RevId: 777791889
1 parent 5b59030 commit 9296d4d

File tree

4 files changed

+289
-9
lines changed

4 files changed

+289
-9
lines changed

tests/unit/vertex_langchain/test_agent_engines.py

Lines changed: 158 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,10 @@ def register_operations(self) -> Dict[str, List[str]]:
584584
"pydantic": ["pydantic"],
585585
}
586586

587+
_TEST_BUILD_OPTIONS_INSTALLATION = _agent_engines._BUILD_OPTIONS_INSTALLATION
588+
_TEST_INSTALLATION_SUBDIR = _utils._INSTALLATION_SUBDIR
589+
_TEST_INSTALLATION_SCRIPT_PATH = f"{_TEST_INSTALLATION_SUBDIR}/install_package.sh"
590+
587591

588592
def _create_empty_fake_package(package_name: str) -> str:
589593
"""Creates a temporary directory structure representing an empty fake Python package.
@@ -1146,6 +1150,47 @@ def test_create_agent_engine_with_env_vars_list(
11461150
retry=_TEST_RETRY,
11471151
)
11481152

1153+
def test_create_agent_engine_with_build_options(
1154+
self,
1155+
create_agent_engine_mock,
1156+
cloud_storage_create_bucket_mock,
1157+
tarfile_open_mock,
1158+
cloudpickle_dump_mock,
1159+
cloudpickle_load_mock,
1160+
importlib_metadata_version_mock,
1161+
get_agent_engine_mock,
1162+
get_gca_resource_mock,
1163+
):
1164+
1165+
with mock.patch("os.path.exists", return_value=True):
1166+
agent_engines.create(
1167+
self.test_agent,
1168+
display_name=_TEST_AGENT_ENGINE_DISPLAY_NAME,
1169+
extra_packages=[
1170+
_TEST_INSTALLATION_SCRIPT_PATH,
1171+
],
1172+
build_options={
1173+
_TEST_BUILD_OPTIONS_INSTALLATION: [_TEST_INSTALLATION_SCRIPT_PATH]
1174+
},
1175+
)
1176+
test_spec = types.ReasoningEngineSpec(
1177+
package_spec=_TEST_AGENT_ENGINE_PACKAGE_SPEC,
1178+
agent_framework=_agent_engines._DEFAULT_AGENT_FRAMEWORK,
1179+
)
1180+
test_spec.class_methods.append(_TEST_AGENT_ENGINE_QUERY_SCHEMA)
1181+
create_agent_engine_mock.assert_called_with(
1182+
parent=_TEST_PARENT,
1183+
reasoning_engine=types.ReasoningEngine(
1184+
display_name=_TEST_AGENT_ENGINE_DISPLAY_NAME,
1185+
spec=test_spec,
1186+
),
1187+
)
1188+
1189+
get_agent_engine_mock.assert_called_with(
1190+
name=_TEST_AGENT_ENGINE_RESOURCE_NAME,
1191+
retry=_TEST_RETRY,
1192+
)
1193+
11491194
@pytest.mark.parametrize(
11501195
"test_case_name, test_engine_instance, expected_framework",
11511196
[
@@ -2788,8 +2833,8 @@ def test_update_agent_engine_with_no_updates(
27882833
ValueError,
27892834
match=(
27902835
"At least one of `agent_engine`, `requirements`, "
2791-
"`extra_packages`, `display_name`, `description`, or `env_vars` "
2792-
"must be specified."
2836+
"`extra_packages`, `display_name`, `description`, "
2837+
"`env_vars`, or `build_options` must be specified."
27932838
),
27942839
):
27952840
test_agent_engine = _generate_agent_engine_to_update()
@@ -3228,3 +3273,114 @@ def test_scan_with_explicit_ignore_modules(self):
32283273
"cloudpickle": "3.0.0",
32293274
"pydantic": "1.11.1",
32303275
}
3276+
3277+
3278+
class TestValidateInstallationScripts:
3279+
# pytest does not allow absl.testing.parameterized.named_parameters.
3280+
@pytest.mark.parametrize(
3281+
"name, script_paths, extra_packages",
3282+
[
3283+
(
3284+
"valid_script_in_subdir_and_extra_packages",
3285+
[f"{_utils._INSTALLATION_SUBDIR}/script.sh"],
3286+
[f"{_utils._INSTALLATION_SUBDIR}/script.sh"],
3287+
),
3288+
(
3289+
"multiple_valid_scripts",
3290+
[
3291+
f"{_utils._INSTALLATION_SUBDIR}/script1.sh",
3292+
f"{_utils._INSTALLATION_SUBDIR}/script2.sh",
3293+
],
3294+
[
3295+
f"{_utils._INSTALLATION_SUBDIR}/script1.sh",
3296+
f"{_utils._INSTALLATION_SUBDIR}/script2.sh",
3297+
],
3298+
),
3299+
("empty_script_paths_and_extra_packages", [], []),
3300+
],
3301+
)
3302+
def test_validate_installation_scripts(self, name, script_paths, extra_packages):
3303+
_utils.validate_installation_scripts_or_raise(
3304+
script_paths=script_paths, extra_packages=extra_packages
3305+
)
3306+
3307+
@pytest.mark.parametrize(
3308+
"name, script_paths, extra_packages, error_message",
3309+
[
3310+
(
3311+
"script_not_in_subdir",
3312+
["script.sh"],
3313+
["script.sh"],
3314+
(
3315+
f"Required installation script 'script.sh' is not under"
3316+
f" '{_utils._INSTALLATION_SUBDIR}'"
3317+
),
3318+
),
3319+
(
3320+
"script_not_in_extra_packages",
3321+
[f"{_utils._INSTALLATION_SUBDIR}/script.sh"],
3322+
[],
3323+
(
3324+
"User-defined installation script "
3325+
f"'{_utils._INSTALLATION_SUBDIR}/script.sh'"
3326+
" does not exist in `extra_packages`"
3327+
),
3328+
),
3329+
(
3330+
"extra_package_in_subdir_but_not_script",
3331+
[],
3332+
[f"{_utils._INSTALLATION_SUBDIR}/script.sh"],
3333+
(
3334+
f"Extra package '{_utils._INSTALLATION_SUBDIR}/script.sh' "
3335+
"is in the installation scripts subdirectory, but is not "
3336+
"specified as an installation script."
3337+
),
3338+
),
3339+
(
3340+
"one_valid_one_invalid_script_not_in_subdir",
3341+
[f"{_utils._INSTALLATION_SUBDIR}/script1.sh", "script2.sh"],
3342+
[f"{_utils._INSTALLATION_SUBDIR}/script1.sh", "script2.sh"],
3343+
(
3344+
f"Required installation script 'script2.sh' is not under"
3345+
f" '{_utils._INSTALLATION_SUBDIR}'"
3346+
),
3347+
),
3348+
(
3349+
"one_valid_one_invalid_script_not_in_extra_packages",
3350+
[
3351+
f"{_utils._INSTALLATION_SUBDIR}/script1.sh",
3352+
f"{_utils._INSTALLATION_SUBDIR}/script2.sh",
3353+
],
3354+
[f"{_utils._INSTALLATION_SUBDIR}/script1.sh"],
3355+
(
3356+
"User-defined installation script "
3357+
f"'{_utils._INSTALLATION_SUBDIR}/script2.sh' "
3358+
"does not exist in `extra_packages`"
3359+
),
3360+
),
3361+
(
3362+
"one_valid_one_invalid_extra_package_in_subdir",
3363+
[f"{_utils._INSTALLATION_SUBDIR}/script1.sh"],
3364+
[
3365+
f"{_utils._INSTALLATION_SUBDIR}/script1.sh",
3366+
f"{_utils._INSTALLATION_SUBDIR}/script2.sh",
3367+
],
3368+
(
3369+
f"Extra package '{_utils._INSTALLATION_SUBDIR}/script2.sh' "
3370+
"is in the installation scripts subdirectory, but is not "
3371+
"specified as an installation script."
3372+
),
3373+
),
3374+
],
3375+
)
3376+
def test_validate_installation_scripts_raises_error(
3377+
self,
3378+
name,
3379+
script_paths,
3380+
extra_packages,
3381+
error_message,
3382+
):
3383+
with pytest.raises(ValueError, match=error_message):
3384+
_utils.validate_installation_scripts_or_raise(
3385+
script_paths=script_paths, extra_packages=extra_packages
3386+
)

vertexai/agent_engines/__init__.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ def create(
7070
env_vars: Optional[
7171
Union[Sequence[str], Dict[str, Union[str, aip_types.SecretRef]]]
7272
] = None,
73+
build_options: Optional[Dict[str, Sequence[str]]] = None,
7374
) -> AgentEngine:
7475
"""Creates a new Agent Engine.
7576
@@ -86,6 +87,9 @@ def create(
8687
|-- user_code/
8788
| |-- utils.py
8889
| |-- ...
90+
|-- installation_scripts/
91+
| |-- install_package.sh
92+
| |-- ...
8993
|-- ...
9094
9195
To build an Agent Engine with the above files, run:
@@ -105,6 +109,12 @@ def create(
105109
"./user_src_dir/user_code", # a directory
106110
...
107111
],
112+
build_options={
113+
"installation": [
114+
"./user_src_dir/installation_scripts/install_package.sh",
115+
...
116+
],
117+
},
108118
)
109119
110120
Args:
@@ -131,6 +141,9 @@ def create(
131141
a valid key to `os.environ`. If it is a dictionary, the keys are
132142
the environment variable names, and the values are the
133143
corresponding values.
144+
build_options (Dict[str, Sequence[str]]):
145+
Optional. The build options for the Agent Engine. This includes
146+
options such as installation scripts.
134147
135148
Returns:
136149
AgentEngine: The Agent Engine that was created.
@@ -153,6 +166,7 @@ def create(
153166
gcs_dir_name=gcs_dir_name,
154167
extra_packages=extra_packages,
155168
env_vars=env_vars,
169+
build_options=build_options,
156170
)
157171

158172

@@ -237,6 +251,7 @@ def update(
237251
env_vars: Optional[
238252
Union[Sequence[str], Dict[str, Union[str, aip_types.SecretRef]]]
239253
] = None,
254+
build_options: Optional[Dict[str, Sequence[str]]] = None,
240255
) -> "AgentEngine":
241256
"""Updates an existing Agent Engine.
242257
@@ -280,6 +295,9 @@ def update(
280295
a valid key to `os.environ`. If it is a dictionary, the keys are
281296
the environment variable names, and the values are the
282297
corresponding values.
298+
build_options (Dict[str, Sequence[str]]):
299+
Optional. The build options for the Agent Engine. This includes
300+
options such as installation scripts.
283301
284302
Returns:
285303
AgentEngine: The Agent Engine that was updated.
@@ -290,8 +308,8 @@ def update(
290308
FileNotFoundError: If `extra_packages` includes a file or directory
291309
that does not exist.
292310
ValueError: if none of `display_name`, `description`,
293-
`requirements`, `extra_packages`, or `agent_engine` were
294-
specified.
311+
`requirements`, `extra_packages`, `agent_engine`, or `build_options`
312+
were specified.
295313
IOError: If requirements is a string that corresponds to a
296314
nonexistent file.
297315
"""
@@ -304,6 +322,7 @@ def update(
304322
gcs_dir_name=gcs_dir_name,
305323
extra_packages=extra_packages,
306324
env_vars=env_vars,
325+
build_options=build_options,
307326
)
308327

309328

0 commit comments

Comments
 (0)