Skip to content

Commit 3ab3912

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 da663c0 commit 3ab3912

File tree

4 files changed

+285
-1
lines changed

4 files changed

+285
-1
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" not 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: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1317,6 +1317,9 @@ 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(keep_alive_probe.model_dump_json())
13201323
if agent and agent_engine:
13211324
raise ValueError("Please specify only one of `agent` or `agent_engine`.")
13221325
elif agent_engine:
@@ -1357,6 +1360,7 @@ def create(
13571360
image_spec=config.image_spec,
13581361
agent_config_source=agent_config_source,
13591362
container_spec=config.container_spec,
1363+
keep_alive_probe=keep_alive_probe,
13601364
)
13611365
operation = self._create(config=api_config)
13621366
reasoning_engine_id = _agent_engines_utils._get_reasoning_engine_id(
@@ -1663,6 +1667,7 @@ def _create_config(
16631667
types.ReasoningEngineSpecSourceCodeSpecAgentConfigSourceDict
16641668
] = None,
16651669
container_spec: Optional[types.ReasoningEngineSpecContainerSpecDict] = None,
1670+
keep_alive_probe: Optional[dict[str, Any]] = None,
16661671
) -> types.UpdateAgentEngineConfigDict:
16671672
import sys
16681673

@@ -1795,11 +1800,12 @@ def _create_config(
17951800
or max_instances is not None
17961801
or resource_limits is not None
17971802
or container_concurrency is not None
1803+
or keep_alive_probe is not None
17981804
)
17991805
if agent_engine_spec is None and is_deployment_spec_updated:
18001806
raise ValueError(
18011807
"To update `env_vars`, `psc_interface_config`, `min_instances`, "
1802-
"`max_instances`, `resource_limits`, or `container_concurrency`, "
1808+
"`max_instances`, `resource_limits`, `container_concurrency`, or `keep_alive_probe`, "
18031809
"you must also provide the `agent` variable or the source code "
18041810
"options (`source_packages`, `developer_connect_source` or "
18051811
"`agent_config_source`)."
@@ -1817,6 +1823,7 @@ def _create_config(
18171823
max_instances=max_instances,
18181824
resource_limits=resource_limits,
18191825
container_concurrency=container_concurrency,
1826+
keep_alive_probe=keep_alive_probe,
18201827
)
18211828
update_masks.extend(deployment_update_masks)
18221829
agent_engine_spec["deployment_spec"] = deployment_spec
@@ -1879,6 +1886,7 @@ def _generate_deployment_spec_or_raise(
18791886
max_instances: Optional[int] = None,
18801887
resource_limits: Optional[dict[str, str]] = None,
18811888
container_concurrency: Optional[int] = None,
1889+
keep_alive_probe: Optional[dict[str, Any]] = None,
18821890
) -> Tuple[dict[str, Any], Sequence[str]]:
18831891
deployment_spec: dict[str, Any] = {}
18841892
update_masks = []
@@ -1926,6 +1934,9 @@ def _generate_deployment_spec_or_raise(
19261934
if container_concurrency:
19271935
deployment_spec["container_concurrency"] = container_concurrency
19281936
update_masks.append("spec.deployment_spec.container_concurrency")
1937+
if keep_alive_probe:
1938+
deployment_spec["keep_alive_probe"] = keep_alive_probe
1939+
update_masks.append("spec.deployment_spec.keep_alive_probe")
19291940
return deployment_spec, update_masks
19301941

19311942
def _update_deployment_spec_with_env_vars_dict_or_raise(
@@ -2067,6 +2078,9 @@ def update(
20672078
agent_config_source = config.agent_config_source
20682079
if agent_config_source is not None:
20692080
agent_config_source = json.loads(agent_config_source.model_dump_json())
2081+
keep_alive_probe = config.keep_alive_probe
2082+
if keep_alive_probe is not None:
2083+
keep_alive_probe = json.loads(keep_alive_probe.model_dump_json())
20702084
if agent and agent_engine:
20712085
raise ValueError("Please specify only one of `agent` or `agent_engine`.")
20722086
elif agent_engine:
@@ -2113,6 +2127,7 @@ def update(
21132127
image_spec=image_spec,
21142128
agent_config_source=agent_config_source,
21152129
container_spec=container_spec,
2130+
keep_alive_probe=keep_alive_probe,
21162131
)
21172132
operation = self._update(name=name, config=api_config)
21182133
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
@@ -1645,6 +1651,12 @@
16451651
"AgentEngineOperation",
16461652
"AgentEngineOperationDict",
16471653
"AgentEngineOperationOrDict",
1654+
"KeepAliveProbeHttpGet",
1655+
"KeepAliveProbeHttpGetDict",
1656+
"KeepAliveProbeHttpGetOrDict",
1657+
"KeepAliveProbe",
1658+
"KeepAliveProbeDict",
1659+
"KeepAliveProbeOrDict",
16481660
"CreateAgentEngineConfig",
16491661
"CreateAgentEngineConfigDict",
16501662
"CreateAgentEngineConfigOrDict",

0 commit comments

Comments
 (0)