2626import tempfile
2727from unittest import mock
2828
29+ from google.api_core import exceptions as api_exceptions
2930import google.auth.credentials
3031from google.cloud import aiplatform
3132import agentplatform
@@ -3390,9 +3391,7 @@ def test_run_inference_with_runtime_and_session_inputs_dict(
33903391 ]
33913392
33923393 mock_runtime.stream_query.return_value = iter(stream_query_return_value)
3393- mock_agentplatform_client.return_value.runtimes.get.return_value = (
3394- mock_runtime
3395- )
3394+ mock_agentplatform_client.return_value.runtimes.get.return_value = mock_runtime
33963395
33973396 inference_result = self.client.evals.run_inference(
33983397 agent="projects/test-project/locations/us-central1/reasoningEngines/123",
@@ -3499,9 +3498,7 @@ def test_run_inference_with_runtime_and_session_inputs_literal_string(
34993498 ]
35003499
35013500 mock_runtime.stream_query.return_value = iter(stream_query_return_value)
3502- mock_agentplatform_client.return_value.runtimes.get.return_value = (
3503- mock_runtime
3504- )
3501+ mock_agentplatform_client.return_value.runtimes.get.return_value = mock_runtime
35053502
35063503 inference_result = self.client.evals.run_inference(
35073504 agent="projects/test-project/locations/us-central1/reasoningEngines/123",
@@ -3591,9 +3588,7 @@ def test_run_inference_with_runtime_with_response_column_raises_error(
35913588 )
35923589
35933590 mock_runtime = mock.Mock()
3594- mock_agentplatform_client.return_value.runtimes.get.return_value = (
3595- mock_runtime
3596- )
3591+ mock_agentplatform_client.return_value.runtimes.get.return_value = mock_runtime
35973592
35983593 with pytest.raises(ValueError) as excinfo:
35993594 self.client.evals.run_inference(
@@ -3644,9 +3639,7 @@ def test_run_inference_with_runtime_falls_back_to_managed_sessions_api(
36443639 "projects/test-project/locations/us-central1"
36453640 "/reasoningEngines/123/sessions/managed-session-1"
36463641 )
3647- mock_runtime.api_client.sessions.create.return_value = (
3648- mock_session_operation
3649- )
3642+ mock_runtime.api_client.sessions.create.return_value = mock_session_operation
36503643
36513644 stream_query_return_value = [
36523645 {
@@ -3663,9 +3656,7 @@ def test_run_inference_with_runtime_falls_back_to_managed_sessions_api(
36633656 },
36643657 ]
36653658 mock_runtime.stream_query.return_value = iter(stream_query_return_value)
3666- mock_agentplatform_client.return_value.runtimes.get.return_value = (
3667- mock_runtime
3668- )
3659+ mock_agentplatform_client.return_value.runtimes.get.return_value = mock_runtime
36693660
36703661 inference_result = self.client.evals.run_inference(
36713662 agent="projects/test-project/locations/us-central1/reasoningEngines/123",
@@ -4392,9 +4383,7 @@ def test_run_inference_non_gemini_agent_routes_to_runtime(
43924383 }
43934384 ]
43944385 )
4395- mock_agentplatform_client.return_value.runtimes.get.return_value = (
4396- mock_runtime
4397- )
4386+ mock_agentplatform_client.return_value.runtimes.get.return_value = mock_runtime
43984387
43994388 self.client.evals.run_inference(
44004389 src=mock_df,
@@ -11141,6 +11130,113 @@ def test_call_with_retry_no_retry_on_non_retryable(self, mock_sleep):
1114111130 assert mock_sleep.call_count == 0
1114211131
1114311132
11133+ class TestInferenceRetry:
11134+ @pytest.mark.parametrize(
11135+ "error, expected",
11136+ [
11137+ (genai_errors.ClientError(code=429, response_json={}), True),
11138+ (genai_errors.ServerError(code=503, response_json={}), True),
11139+ (genai_errors.ClientError(code=400, response_json={}), False),
11140+ (genai_errors.ClientError(code=403, response_json={}), False),
11141+ (genai_errors.ClientError(code=404, response_json={}), False),
11142+ (api_exceptions.ResourceExhausted("quota"), True),
11143+ (api_exceptions.ServiceUnavailable("unavailable"), True),
11144+ (api_exceptions.InvalidArgument("bad request"), False),
11145+ (ValueError("bad value"), False),
11146+ ],
11147+ )
11148+ def test_is_retryable_error(self, error, expected):
11149+ assert _evals_common._is_retryable_error(error) is expected
11150+
11151+ @mock.patch("time.sleep", return_value=None)
11152+ @mock.patch.object(_evals_common, "Models")
11153+ def test_generate_content_retries_429_with_backoff(self, mock_models, mock_sleep):
11154+ response = genai_types.GenerateContentResponse(
11155+ candidates=[
11156+ genai_types.Candidate(
11157+ content=genai_types.Content(parts=[genai_types.Part(text="ok")]),
11158+ finish_reason=genai_types.FinishReason.STOP,
11159+ )
11160+ ]
11161+ )
11162+ error = genai_errors.ClientError(code=429, response_json={})
11163+ mock_models.return_value.generate_content.side_effect = [error, error, response]
11164+
11165+ result = _evals_common._generate_content_with_retry(
11166+ api_client=mock.Mock(), model="gemini-pro", contents="prompt"
11167+ )
11168+
11169+ assert result is response
11170+ assert mock_models.return_value.generate_content.call_count == 3
11171+ backoffs = [call.args[0] for call in mock_sleep.call_args_list]
11172+ assert len(backoffs) == 2
11173+ assert 1 <= backoffs[0] <= 2
11174+ assert 2 <= backoffs[1] <= 3
11175+
11176+ @mock.patch("time.sleep", return_value=None)
11177+ @mock.patch.object(_evals_common, "Models")
11178+ def test_generate_content_does_not_retry_400(self, mock_models, mock_sleep):
11179+ mock_models.return_value.generate_content.side_effect = (
11180+ genai_errors.ClientError(code=400, response_json={})
11181+ )
11182+
11183+ result = _evals_common._generate_content_with_retry(
11184+ api_client=mock.Mock(), model="gemini-pro", contents="prompt"
11185+ )
11186+
11187+ assert result["error"].startswith("Failed on attempt 1/3: 400")
11188+ assert mock_models.return_value.generate_content.call_count == 1
11189+ mock_sleep.assert_not_called()
11190+
11191+ @mock.patch("time.sleep", return_value=None)
11192+ def test_agent_engine_run_retries_429_and_fails_fast_on_400(self, mock_sleep):
11193+ runtime = mock.Mock()
11194+ runtime.stream_query.side_effect = [
11195+ genai_errors.ClientError(code=429, response_json={}),
11196+ genai_errors.ClientError(code=400, response_json={}),
11197+ ]
11198+
11199+ with mock.patch.object(
11200+ _evals_common, "_create_runtime_session", return_value="session-id"
11201+ ):
11202+ result = _evals_common._execute_agent_run_with_retry(
11203+ row=pd.Series({"prompt": "prompt"}), contents="prompt", runtime=runtime
11204+ )
11205+
11206+ assert result["error"].startswith("Failed on attempt 2/3: 400")
11207+ assert runtime.stream_query.call_count == 2
11208+ mock_sleep.assert_called_once()
11209+ assert 1 <= mock_sleep.call_args.args[0] <= 2
11210+
11211+ @mock.patch("asyncio.sleep", new_callable=mock.AsyncMock)
11212+ def test_local_agent_run_retries_429_and_fails_fast_on_400(self, mock_sleep):
11213+ mock_runners = mock.MagicMock()
11214+ mock_runners.Runner.return_value.run_async.side_effect = [
11215+ genai_errors.ClientError(code=429, response_json={}),
11216+ genai_errors.ClientError(code=400, response_json={}),
11217+ ]
11218+ mock_sessions = mock.MagicMock()
11219+ mock_sessions.InMemorySessionService.return_value.create_session = (
11220+ mock.AsyncMock()
11221+ )
11222+
11223+ with mock.patch.dict(
11224+ sys.modules,
11225+ {"google.adk.runners": mock_runners, "google.adk.sessions": mock_sessions},
11226+ ):
11227+ result = _evals_common._execute_local_agent_run_with_retry(
11228+ row=pd.Series({"prompt": "prompt"}),
11229+ contents="prompt",
11230+ agent=mock.Mock(),
11231+ api_client=mock.Mock(),
11232+ )
11233+
11234+ assert result["error"].startswith("Failed on attempt 2/3: 400")
11235+ assert mock_runners.Runner.return_value.run_async.call_count == 2
11236+ mock_sleep.assert_awaited_once()
11237+ assert 1 <= mock_sleep.await_args.args[0] <= 2
11238+
11239+
1114411240class TestComputationMetricRetry:
1114511241 """Tests for retry behavior in ComputationMetricHandler."""
1114611242
0 commit comments