Skip to content

Commit 8ef6a4b

Browse files
vertex-sdk-botcopybara-github
authored andcommitted
feat: add support for keep alive probe in agent engines
Keep alive probe allows reasoning engine users to configure a probe that a deployment host can use to keep the container alive, based on the probe settings. If the keep alive endpoint returns a 2xx status, the deployment host will make a best effort (up to 1 hour) to keep the container alive. Reasoning engine users with custom container specs (BYOC) have the option to configure a custom keep alive probe while the users without custom container specs (BYOC) have the option to configure an empty keep alive probe {} and the reasoning engine platform will handle the configuration and logic for keep alive probe. To opt in, users should set the keep alive probe field when creating or updating reasoning engines. PiperOrigin-RevId: 889254469
1 parent 78525d2 commit 8ef6a4b

File tree

4 files changed

+292
-4
lines changed

4 files changed

+292
-4
lines changed

tests/unit/vertexai/genai/test_agent_engines.py

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,12 @@ def register_operations(self) -> Dict[str, List[str]]:
538538
_genai_types.IdentityType.SERVICE_ACCOUNT
539539
)
540540
_TEST_AGENT_ENGINE_ENCRYPTION_SPEC = {"kms_key_name": "test-kms-key"}
541+
_TEST_AGENT_ENGINE_KEEP_ALIVE_PROBE = {
542+
"http_get": {
543+
"path": "/health",
544+
},
545+
"max_seconds": 60,
546+
}
541547
_TEST_AGENT_ENGINE_SPEC = _genai_types.ReasoningEngineSpecDict(
542548
agent_framework=_TEST_AGENT_ENGINE_FRAMEWORK,
543549
class_methods=[_TEST_AGENT_ENGINE_CLASS_METHOD_1],
@@ -1071,6 +1077,7 @@ def test_create_agent_engine_config_with_source_packages(
10711077
config["spec"]["identity_type"]
10721078
== _TEST_AGENT_ENGINE_IDENTITY_TYPE_SERVICE_ACCOUNT
10731079
)
1080+
assert "keep_alive_probe" not in config["spec"].get("deployment_spec", {})
10741081

10751082
def test_create_agent_engine_config_with_developer_connect_source(self):
10761083
with tempfile.TemporaryDirectory() as tmpdir:
@@ -1112,6 +1119,29 @@ def test_create_agent_engine_config_with_developer_connect_source(self):
11121119
config["spec"]["identity_type"]
11131120
== _TEST_AGENT_ENGINE_IDENTITY_TYPE_SERVICE_ACCOUNT
11141121
)
1122+
assert "keep_alive_probe" not in config["spec"].get("deployment_spec", {})
1123+
1124+
@mock.patch.object(
1125+
_agent_engines_utils,
1126+
"_create_base64_encoded_tarball",
1127+
return_value="test_tarball",
1128+
)
1129+
def test_create_agent_engine_config_with_empty_keep_alive_probe(
1130+
self, mock_create_base64_encoded_tarball
1131+
):
1132+
with tempfile.TemporaryDirectory() as tmpdir:
1133+
test_file_path = os.path.join(tmpdir, "test_file.txt")
1134+
with open(test_file_path, "w") as f:
1135+
f.write("test content")
1136+
config = self.client.agent_engines._create_config(
1137+
mode="create",
1138+
source_packages=[test_file_path],
1139+
class_methods=_TEST_AGENT_ENGINE_CLASS_METHODS,
1140+
entrypoint_module="main",
1141+
entrypoint_object="app",
1142+
keep_alive_probe={},
1143+
)
1144+
assert "keep_alive_probe" in config["spec"].get("deployment_spec", {})
11151145

11161146
def test_create_agent_engine_config_with_agent_config_source_and_requirements_file(
11171147
self,
@@ -1321,6 +1351,33 @@ def test_create_agent_engine_config_with_container_spec(self):
13211351
config["spec"]["identity_type"]
13221352
== _TEST_AGENT_ENGINE_IDENTITY_TYPE_SERVICE_ACCOUNT
13231353
)
1354+
assert "keep_alive_probe" not in config["spec"].get("deployment_spec", {})
1355+
1356+
def test_create_agent_engine_config_with_container_spec_and_keep_alive_probe(
1357+
self,
1358+
):
1359+
container_spec = {"image_uri": "gcr.io/test-project/test-image"}
1360+
config = self.client.agent_engines._create_config(
1361+
mode="create",
1362+
display_name=_TEST_AGENT_ENGINE_DISPLAY_NAME,
1363+
description=_TEST_AGENT_ENGINE_DESCRIPTION,
1364+
container_spec=container_spec,
1365+
class_methods=_TEST_AGENT_ENGINE_CLASS_METHODS,
1366+
identity_type=_TEST_AGENT_ENGINE_IDENTITY_TYPE_SERVICE_ACCOUNT,
1367+
keep_alive_probe=_TEST_AGENT_ENGINE_KEEP_ALIVE_PROBE,
1368+
)
1369+
assert config["display_name"] == _TEST_AGENT_ENGINE_DISPLAY_NAME
1370+
assert config["description"] == _TEST_AGENT_ENGINE_DESCRIPTION
1371+
assert config["spec"]["container_spec"] == container_spec
1372+
assert config["spec"]["class_methods"] == _TEST_AGENT_ENGINE_CLASS_METHODS
1373+
assert (
1374+
config["spec"]["identity_type"]
1375+
== _TEST_AGENT_ENGINE_IDENTITY_TYPE_SERVICE_ACCOUNT
1376+
)
1377+
assert (
1378+
config["spec"]["deployment_spec"]["keep_alive_probe"]
1379+
== _TEST_AGENT_ENGINE_KEEP_ALIVE_PROBE
1380+
)
13241381

13251382
def test_create_agent_engine_config_with_container_spec_and_others_raises(self):
13261383
container_spec = {"image_uri": "gcr.io/test-project/test-image"}
@@ -2116,6 +2173,7 @@ def test_create_agent_engine_with_env_vars_dict(
21162173
image_spec=None,
21172174
agent_config_source=None,
21182175
container_spec=None,
2176+
keep_alive_probe=None,
21192177
)
21202178
request_mock.assert_called_with(
21212179
"post",
@@ -2220,6 +2278,7 @@ def test_create_agent_engine_with_custom_service_account(
22202278
image_spec=None,
22212279
agent_config_source=None,
22222280
container_spec=None,
2281+
keep_alive_probe=None,
22232282
)
22242283
request_mock.assert_called_with(
22252284
"post",
@@ -2323,6 +2382,7 @@ def test_create_agent_engine_with_experimental_mode(
23232382
image_spec=None,
23242383
agent_config_source=None,
23252384
container_spec=None,
2385+
keep_alive_probe=None,
23262386
)
23272387
request_mock.assert_called_with(
23282388
"post",
@@ -2495,6 +2555,7 @@ def test_create_agent_engine_with_class_methods(
24952555
image_spec=None,
24962556
agent_config_source=None,
24972557
container_spec=None,
2558+
keep_alive_probe=None,
24982559
)
24992560
request_mock.assert_called_with(
25002561
"post",
@@ -2593,6 +2654,7 @@ def test_create_agent_engine_with_agent_framework(
25932654
image_spec=None,
25942655
agent_config_source=None,
25952656
container_spec=None,
2657+
keep_alive_probe=None,
25962658
)
25972659
request_mock.assert_called_with(
25982660
"post",
@@ -2795,6 +2857,109 @@ def test_update_agent_engine_env_vars(
27952857
None,
27962858
)
27972859

2860+
@mock.patch.object(_agent_engines_utils, "_prepare")
2861+
@mock.patch.object(_agent_engines_utils, "_await_operation")
2862+
def test_update_agent_engine_with_empty_keep_alive_probe(
2863+
self, mock_await_operation, mock_prepare
2864+
):
2865+
mock_await_operation.return_value = _genai_types.AgentEngineOperation(
2866+
response=_genai_types.ReasoningEngine(
2867+
name=_TEST_AGENT_ENGINE_RESOURCE_NAME,
2868+
spec=_TEST_AGENT_ENGINE_SPEC,
2869+
)
2870+
)
2871+
with mock.patch.object(
2872+
self.client.agent_engines._api_client, "request"
2873+
) as request_mock:
2874+
request_mock.return_value = genai_types.HttpResponse(body="")
2875+
self.client.agent_engines.update(
2876+
name=_TEST_AGENT_ENGINE_RESOURCE_NAME,
2877+
agent=self.test_agent,
2878+
config=_genai_types.AgentEngineConfig(
2879+
staging_bucket=_TEST_STAGING_BUCKET,
2880+
keep_alive_probe={},
2881+
),
2882+
)
2883+
update_mask = ",".join(
2884+
[
2885+
"spec.package_spec.pickle_object_gcs_uri",
2886+
"spec.package_spec.requirements_gcs_uri",
2887+
"spec.class_methods",
2888+
"spec.deployment_spec.keep_alive_probe",
2889+
"spec.agent_framework",
2890+
]
2891+
)
2892+
query_params = {"updateMask": update_mask}
2893+
request_mock.assert_called_with(
2894+
"patch",
2895+
f"{_TEST_AGENT_ENGINE_RESOURCE_NAME}?{urlencode(query_params)}",
2896+
{
2897+
"_url": {"name": _TEST_AGENT_ENGINE_RESOURCE_NAME},
2898+
"spec": {
2899+
"agent_framework": _TEST_AGENT_ENGINE_FRAMEWORK,
2900+
"class_methods": mock.ANY,
2901+
"package_spec": {
2902+
"python_version": _TEST_PYTHON_VERSION,
2903+
"pickle_object_gcs_uri": _TEST_AGENT_ENGINE_GCS_URI,
2904+
"requirements_gcs_uri": _TEST_AGENT_ENGINE_REQUIREMENTS_GCS_URI,
2905+
},
2906+
"deployment_spec": {"keep_alive_probe": {}},
2907+
},
2908+
"_query": {"updateMask": update_mask},
2909+
},
2910+
None,
2911+
)
2912+
2913+
@mock.patch.object(_agent_engines_utils, "_await_operation")
2914+
def test_update_agent_engine_with_container_spec_and_keep_alive_probe(
2915+
self, mock_await_operation
2916+
):
2917+
mock_await_operation.return_value = _genai_types.AgentEngineOperation(
2918+
response=_genai_types.ReasoningEngine(
2919+
name=_TEST_AGENT_ENGINE_RESOURCE_NAME,
2920+
spec=_TEST_AGENT_ENGINE_SPEC,
2921+
)
2922+
)
2923+
container_spec = {"image_uri": "gcr.io/test-project/test-image"}
2924+
with mock.patch.object(
2925+
self.client.agent_engines._api_client, "request"
2926+
) as request_mock:
2927+
request_mock.return_value = genai_types.HttpResponse(body="")
2928+
self.client.agent_engines.update(
2929+
name=_TEST_AGENT_ENGINE_RESOURCE_NAME,
2930+
config=_genai_types.AgentEngineConfig(
2931+
container_spec=container_spec,
2932+
keep_alive_probe=_TEST_AGENT_ENGINE_KEEP_ALIVE_PROBE,
2933+
class_methods=_TEST_AGENT_ENGINE_CLASS_METHODS,
2934+
),
2935+
)
2936+
update_mask = ",".join(
2937+
[
2938+
"spec.class_methods",
2939+
"spec.container_spec",
2940+
"spec.deployment_spec.keep_alive_probe",
2941+
"spec.agent_framework",
2942+
]
2943+
)
2944+
query_params = {"updateMask": update_mask}
2945+
request_mock.assert_called_with(
2946+
"patch",
2947+
f"{_TEST_AGENT_ENGINE_RESOURCE_NAME}?{urlencode(query_params)}",
2948+
{
2949+
"_url": {"name": _TEST_AGENT_ENGINE_RESOURCE_NAME},
2950+
"spec": {
2951+
"agent_framework": "custom",
2952+
"container_spec": container_spec,
2953+
"deployment_spec": {
2954+
"keep_alive_probe": _TEST_AGENT_ENGINE_KEEP_ALIVE_PROBE,
2955+
},
2956+
"class_methods": mock.ANY,
2957+
},
2958+
"_query": {"updateMask": update_mask},
2959+
},
2960+
None,
2961+
)
2962+
27982963
@mock.patch.object(_agent_engines_utils, "_await_operation")
27992964
def test_update_agent_engine_display_name(self, mock_await_operation):
28002965
mock_await_operation.return_value = _genai_types.AgentEngineOperation(

vertexai/_genai/agent_engines.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1317,6 +1317,11 @@ def create(
13171317
agent_config_source = config.agent_config_source
13181318
if agent_config_source is not None:
13191319
agent_config_source = json.loads(agent_config_source.model_dump_json())
1320+
keep_alive_probe = config.keep_alive_probe
1321+
if keep_alive_probe is not None:
1322+
keep_alive_probe = json.loads(
1323+
keep_alive_probe.model_dump_json(exclude_none=True)
1324+
)
13201325
if agent and agent_engine:
13211326
raise ValueError("Please specify only one of `agent` or `agent_engine`.")
13221327
elif agent_engine:
@@ -1357,6 +1362,7 @@ def create(
13571362
image_spec=config.image_spec,
13581363
agent_config_source=agent_config_source,
13591364
container_spec=config.container_spec,
1365+
keep_alive_probe=keep_alive_probe,
13601366
)
13611367
operation = self._create(config=api_config)
13621368
reasoning_engine_id = _agent_engines_utils._get_reasoning_engine_id(
@@ -1665,6 +1671,7 @@ def _create_config(
16651671
types.ReasoningEngineSpecSourceCodeSpecAgentConfigSourceDict
16661672
] = None,
16671673
container_spec: Optional[types.ReasoningEngineSpecContainerSpecDict] = None,
1674+
keep_alive_probe: Optional[dict[str, Any]] = None,
16681675
) -> types.UpdateAgentEngineConfigDict:
16691676
import sys
16701677

@@ -1794,14 +1801,15 @@ def _create_config(
17941801
or max_instances is not None
17951802
or resource_limits is not None
17961803
or container_concurrency is not None
1804+
or keep_alive_probe is not None
17971805
)
17981806
if agent_engine_spec is None and is_deployment_spec_updated:
17991807
raise ValueError(
18001808
"To update `env_vars`, `psc_interface_config`, `min_instances`, "
1801-
"`max_instances`, `resource_limits`, or `container_concurrency`, "
1802-
"you must also provide the `agent` variable or the source code "
1803-
"options (`source_packages`, `developer_connect_source` or "
1804-
"`agent_config_source`)."
1809+
"`max_instances`, `resource_limits`, `container_concurrency`, or "
1810+
"`keep_alive_probe`, you must also provide the `agent` variable or "
1811+
"the source code options (`source_packages`, "
1812+
"`developer_connect_source` or `agent_config_source`)."
18051813
)
18061814

18071815
if agent_engine_spec is not None:
@@ -1816,6 +1824,7 @@ def _create_config(
18161824
max_instances=max_instances,
18171825
resource_limits=resource_limits,
18181826
container_concurrency=container_concurrency,
1827+
keep_alive_probe=keep_alive_probe,
18191828
)
18201829
update_masks.extend(deployment_update_masks)
18211830
agent_engine_spec["deployment_spec"] = deployment_spec
@@ -1878,6 +1887,7 @@ def _generate_deployment_spec_or_raise(
18781887
max_instances: Optional[int] = None,
18791888
resource_limits: Optional[dict[str, str]] = None,
18801889
container_concurrency: Optional[int] = None,
1890+
keep_alive_probe: Optional[dict[str, Any]] = None,
18811891
) -> Tuple[dict[str, Any], Sequence[str]]:
18821892
deployment_spec: dict[str, Any] = {}
18831893
update_masks = []
@@ -1925,6 +1935,9 @@ def _generate_deployment_spec_or_raise(
19251935
if container_concurrency:
19261936
deployment_spec["container_concurrency"] = container_concurrency
19271937
update_masks.append("spec.deployment_spec.container_concurrency")
1938+
if keep_alive_probe is not None:
1939+
deployment_spec["keep_alive_probe"] = keep_alive_probe
1940+
update_masks.append("spec.deployment_spec.keep_alive_probe")
19281941
return deployment_spec, update_masks
19291942

19301943
def _update_deployment_spec_with_env_vars_dict_or_raise(
@@ -2066,6 +2079,11 @@ def update(
20662079
agent_config_source = config.agent_config_source
20672080
if agent_config_source is not None:
20682081
agent_config_source = json.loads(agent_config_source.model_dump_json())
2082+
keep_alive_probe = config.keep_alive_probe
2083+
if keep_alive_probe is not None:
2084+
keep_alive_probe = json.loads(
2085+
keep_alive_probe.model_dump_json(exclude_none=True)
2086+
)
20692087
if agent and agent_engine:
20702088
raise ValueError("Please specify only one of `agent` or `agent_engine`.")
20712089
elif agent_engine:
@@ -2112,6 +2130,7 @@ def update(
21122130
image_spec=image_spec,
21132131
agent_config_source=agent_config_source,
21142132
container_spec=container_spec,
2133+
keep_alive_probe=keep_alive_probe,
21152134
)
21162135
operation = self._update(name=name, config=api_config)
21172136
reasoning_engine_id = _agent_engines_utils._get_reasoning_engine_id(

vertexai/_genai/types/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,12 @@
522522
from .common import IntermediateExtractedMemoryDict
523523
from .common import IntermediateExtractedMemoryOrDict
524524
from .common import JobState
525+
from .common import KeepAliveProbe
526+
from .common import KeepAliveProbeDict
527+
from .common import KeepAliveProbeHttpGet
528+
from .common import KeepAliveProbeHttpGetDict
529+
from .common import KeepAliveProbeHttpGetOrDict
530+
from .common import KeepAliveProbeOrDict
525531
from .common import Language
526532
from .common import ListAgentEngineConfig
527533
from .common import ListAgentEngineConfigDict
@@ -1603,6 +1609,12 @@
16031609
"SecretEnvVar",
16041610
"SecretEnvVarDict",
16051611
"SecretEnvVarOrDict",
1612+
"KeepAliveProbeHttpGet",
1613+
"KeepAliveProbeHttpGetDict",
1614+
"KeepAliveProbeHttpGetOrDict",
1615+
"KeepAliveProbe",
1616+
"KeepAliveProbeDict",
1617+
"KeepAliveProbeOrDict",
16061618
"ReasoningEngineSpecDeploymentSpec",
16071619
"ReasoningEngineSpecDeploymentSpecDict",
16081620
"ReasoningEngineSpecDeploymentSpecOrDict",

0 commit comments

Comments
 (0)