Skip to content

Commit 06928b2

Browse files
vertex-sdk-botcopybara-github
authored andcommitted
fix: GenAI Client(evals): Lazy-load ADK imports in _evals_common.py to avoid top-level ImportError
PiperOrigin-RevId: 901011383
1 parent 657bb26 commit 06928b2

2 files changed

Lines changed: 167 additions & 125 deletions

File tree

tests/unit/vertexai/genai/test_evals.py

Lines changed: 139 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -122,15 +122,19 @@ def mock_api_client_fixture():
122122

123123
@pytest.fixture
124124
def mock_eval_dependencies(mock_api_client_fixture):
125-
with mock.patch("google.cloud.storage.Client") as mock_storage_client, mock.patch(
126-
"google.cloud.bigquery.Client"
127-
) as mock_bq_client, mock.patch(
128-
"vertexai._genai.evals.Evals.evaluate_instances"
129-
) as mock_evaluate_instances, mock.patch(
130-
"vertexai._genai._gcs_utils.GcsUtils.upload_json_to_prefix"
131-
) as mock_upload_to_gcs, mock.patch(
132-
"vertexai._genai._evals_metric_loaders.LazyLoadedPrebuiltMetric._fetch_and_parse"
133-
) as mock_fetch_prebuilt_metric:
125+
with (
126+
mock.patch("google.cloud.storage.Client") as mock_storage_client,
127+
mock.patch("google.cloud.bigquery.Client") as mock_bq_client,
128+
mock.patch(
129+
"vertexai._genai.evals.Evals.evaluate_instances"
130+
) as mock_evaluate_instances,
131+
mock.patch(
132+
"vertexai._genai._gcs_utils.GcsUtils.upload_json_to_prefix"
133+
) as mock_upload_to_gcs,
134+
mock.patch(
135+
"vertexai._genai._evals_metric_loaders.LazyLoadedPrebuiltMetric._fetch_and_parse"
136+
) as mock_fetch_prebuilt_metric,
137+
):
134138

135139
def mock_evaluate_instances_side_effect(*args, **kwargs):
136140
metric_config = kwargs.get("metric_config", {})
@@ -3306,14 +3310,8 @@ def test_run_inference_with_agent_engine_falls_back_to_managed_sessions_api(
33063310
assert inference_result.candidate_name == "agent_engine_0"
33073311

33083312
@mock.patch.object(_evals_utils, "EvalDatasetLoader")
3309-
@mock.patch("vertexai._genai._evals_common.InMemorySessionService") # fmt: skip
3310-
@mock.patch("vertexai._genai._evals_common.Runner")
3311-
@mock.patch("vertexai._genai._evals_common.LlmAgent")
33123313
def test_run_inference_with_local_agent(
33133314
self,
3314-
mock_llm_agent,
3315-
mock_runner,
3316-
mock_session_service,
33173315
mock_eval_dataset_loader,
33183316
):
33193317
mock_df = pd.DataFrame(
@@ -3341,8 +3339,15 @@ def test_run_inference_with_local_agent(
33413339
mock_agent_instance.instruction = "mock instruction"
33423340
mock_agent_instance.tools = []
33433341
mock_agent_instance.sub_agents = []
3344-
mock_llm_agent.return_value = mock_agent_instance
3342+
3343+
# Mock ADK modules for lazy imports in _execute_local_agent_run_with_retry_async
3344+
mock_session_service = mock.MagicMock()
33453345
mock_session_service.return_value.create_session = mock.AsyncMock()
3346+
mock_runner = mock.MagicMock()
3347+
mock_adk_sessions_module = mock.MagicMock()
3348+
mock_adk_sessions_module.InMemorySessionService = mock_session_service
3349+
mock_adk_runners_module = mock.MagicMock()
3350+
mock_adk_runners_module.Runner = mock_runner
33463351
mock_runner_instance = mock_runner.return_value
33473352
stream_run_return_value_1 = [
33483353
mock.Mock(
@@ -3393,10 +3398,19 @@ def run_async_side_effect(*args, **kwargs):
33933398

33943399
mock_runner_instance.run_async.side_effect = run_async_side_effect
33953400

3396-
inference_result = self.client.evals.run_inference(
3397-
agent=mock_agent_instance,
3398-
src=mock_df,
3399-
)
3401+
with mock.patch.dict(
3402+
sys.modules,
3403+
{
3404+
"google.adk": mock.MagicMock(),
3405+
"google.adk.sessions": mock_adk_sessions_module,
3406+
"google.adk.runners": mock_adk_runners_module,
3407+
"google.adk.agents": mock.MagicMock(),
3408+
},
3409+
):
3410+
inference_result = self.client.evals.run_inference(
3411+
agent=mock_agent_instance,
3412+
src=mock_df,
3413+
)
34003414

34013415
mock_eval_dataset_loader.return_value.load.assert_called_once_with(mock_df)
34023416
assert mock_session_service.call_count == 2
@@ -3522,11 +3536,14 @@ def test_run_inference_with_litellm_string_prompt_format(
35223536
mock_api_client_fixture,
35233537
):
35243538
"""Tests inference with LiteLLM using a simple prompt string."""
3525-
with mock.patch(
3526-
"vertexai._genai._evals_common.litellm"
3527-
) as mock_litellm, mock.patch(
3528-
"vertexai._genai._evals_common._call_litellm_completion"
3529-
) as mock_call_litellm_completion:
3539+
with (
3540+
mock.patch(
3541+
"vertexai._genai._evals_common.litellm"
3542+
) as mock_litellm,
3543+
mock.patch(
3544+
"vertexai._genai._evals_common._call_litellm_completion"
3545+
) as mock_call_litellm_completion,
3546+
):
35303547
mock_litellm.utils.get_valid_models.return_value = ["gpt-4o"]
35313548
prompt_df = pd.DataFrame([{"prompt": "What is LiteLLM?"}])
35323549
expected_messages = [{"role": "user", "content": "What is LiteLLM?"}]
@@ -3578,11 +3595,14 @@ def test_run_inference_with_litellm_openai_request_format(
35783595
mock_api_client_fixture,
35793596
):
35803597
"""Tests inference with LiteLLM where the row contains a chat completion request body."""
3581-
with mock.patch(
3582-
"vertexai._genai._evals_common.litellm"
3583-
) as mock_litellm, mock.patch(
3584-
"vertexai._genai._evals_common._call_litellm_completion"
3585-
) as mock_call_litellm_completion:
3598+
with (
3599+
mock.patch(
3600+
"vertexai._genai._evals_common.litellm"
3601+
) as mock_litellm,
3602+
mock.patch(
3603+
"vertexai._genai._evals_common._call_litellm_completion"
3604+
) as mock_call_litellm_completion,
3605+
):
35863606
mock_litellm.utils.get_valid_models.return_value = ["gpt-4o"]
35873607
prompt_df = pd.DataFrame(
35883608
[
@@ -4098,21 +4118,23 @@ def test_run_agent_internal_multi_turn_with_agent(self, mock_run_agent):
40984118
]
40994119
assert "mock_agent" in agent_data["agents"]
41004120

4101-
@mock.patch("vertexai._genai._evals_common.ADK_SessionInput") # fmt: skip
4102-
@mock.patch("vertexai._genai._evals_common.EvaluationGenerator") # fmt: skip
4103-
@mock.patch("vertexai._genai._evals_common.LlmBackedUserSimulator") # fmt: skip
4104-
@mock.patch("vertexai._genai._evals_common.ConversationScenario") # fmt: skip
4105-
@mock.patch("vertexai._genai._evals_common.LlmBackedUserSimulatorConfig") # fmt: skip
41064121
@pytest.mark.asyncio
4107-
async def test_run_adk_user_simulation_with_intermediate_events(
4108-
self,
4109-
mock_config,
4110-
mock_scenario,
4111-
mock_simulator,
4112-
mock_generator,
4113-
mock_session_input,
4114-
):
4122+
async def test_run_adk_user_simulation_with_intermediate_events(self):
41154123
"""Tests that intermediate invocation events (e.g. tool calls) are parsed successfully."""
4124+
mock_scenario = mock.MagicMock()
4125+
mock_config = mock.MagicMock()
4126+
mock_simulator = mock.MagicMock()
4127+
mock_generator = mock.MagicMock()
4128+
mock_session_input = mock.MagicMock()
4129+
mock_adk_eval_scenarios = mock.MagicMock()
4130+
mock_adk_eval_scenarios.ConversationScenario = mock_scenario
4131+
mock_adk_eval_case = mock.MagicMock()
4132+
mock_adk_eval_case.SessionInput = mock_session_input
4133+
mock_adk_eval_generator = mock.MagicMock()
4134+
mock_adk_eval_generator.EvaluationGenerator = mock_generator
4135+
mock_adk_simulator_module = mock.MagicMock()
4136+
mock_adk_simulator_module.LlmBackedUserSimulator = mock_simulator
4137+
mock_adk_simulator_module.LlmBackedUserSimulatorConfig = mock_config
41164138
row = pd.Series(
41174139
{
41184140
"starting_prompt": "I want a laptop.",
@@ -4165,7 +4187,19 @@ async def test_run_adk_user_simulation_with_intermediate_events(
41654187
mock_generator._generate_inferences_from_root_agent = mock.AsyncMock(
41664188
return_value=[mock_invocation]
41674189
)
4168-
turns = await _evals_common._run_adk_user_simulation(row, mock_agent)
4190+
with mock.patch.dict(
4191+
sys.modules,
4192+
{
4193+
"google.adk": mock.MagicMock(),
4194+
"google.adk.evaluation": mock.MagicMock(),
4195+
"google.adk.evaluation.conversation_scenarios": mock_adk_eval_scenarios,
4196+
"google.adk.evaluation.eval_case": mock_adk_eval_case,
4197+
"google.adk.evaluation.evaluation_generator": mock_adk_eval_generator,
4198+
"google.adk.evaluation.simulation": mock.MagicMock(),
4199+
"google.adk.evaluation.simulation.llm_backed_user_simulator": mock_adk_simulator_module,
4200+
},
4201+
):
4202+
turns = await _evals_common._run_adk_user_simulation(row, mock_agent)
41694203

41704204
assert len(turns) == 1
41714205
turn = turns[0]
@@ -7006,20 +7040,50 @@ def test_build_request_payload_tool_use_quality_v1_with_agent_data_tool_call(
70067040
class TestRunAdkUserSimulation:
70077041
"""Unit tests for the _run_adk_user_simulation function."""
70087042

7009-
@mock.patch("vertexai._genai._evals_common.ADK_SessionInput") # fmt: skip
7010-
@mock.patch("vertexai._genai._evals_common.EvaluationGenerator") # fmt: skip
7011-
@mock.patch("vertexai._genai._evals_common.LlmBackedUserSimulator") # fmt: skip
7012-
@mock.patch("vertexai._genai._evals_common.ConversationScenario") # fmt: skip
7013-
@mock.patch("vertexai._genai._evals_common.LlmBackedUserSimulatorConfig") # fmt: skip
7043+
def _build_adk_mock_modules(self):
7044+
"""Builds mock ADK modules for lazy imports in _run_adk_user_simulation."""
7045+
mock_scenario_cls = mock.MagicMock()
7046+
mock_config_cls = mock.MagicMock()
7047+
mock_simulator_cls = mock.MagicMock()
7048+
mock_generator_cls = mock.MagicMock()
7049+
mock_session_input_cls = mock.MagicMock()
7050+
mock_modules = {
7051+
"google.adk": mock.MagicMock(),
7052+
"google.adk.evaluation": mock.MagicMock(),
7053+
"google.adk.evaluation.conversation_scenarios": mock.MagicMock(
7054+
ConversationScenario=mock_scenario_cls
7055+
),
7056+
"google.adk.evaluation.eval_case": mock.MagicMock(
7057+
SessionInput=mock_session_input_cls
7058+
),
7059+
"google.adk.evaluation.evaluation_generator": mock.MagicMock(
7060+
EvaluationGenerator=mock_generator_cls
7061+
),
7062+
"google.adk.evaluation.simulation": mock.MagicMock(),
7063+
"google.adk.evaluation.simulation.llm_backed_user_simulator": mock.MagicMock(
7064+
LlmBackedUserSimulator=mock_simulator_cls,
7065+
LlmBackedUserSimulatorConfig=mock_config_cls,
7066+
),
7067+
}
7068+
return (
7069+
mock_modules,
7070+
mock_scenario_cls,
7071+
mock_config_cls,
7072+
mock_simulator_cls,
7073+
mock_generator_cls,
7074+
mock_session_input_cls,
7075+
)
7076+
70147077
@pytest.mark.asyncio
7015-
async def test_run_adk_user_simulation_success(
7016-
self,
7017-
mock_config_cls,
7018-
mock_scenario_cls,
7019-
mock_simulator_cls,
7020-
mock_generator_cls,
7021-
mock_session_input_cls,
7022-
):
7078+
async def test_run_adk_user_simulation_success(self):
7079+
(
7080+
mock_modules,
7081+
mock_scenario_cls,
7082+
_,
7083+
_,
7084+
mock_generator_cls,
7085+
mock_session_input_cls,
7086+
) = self._build_adk_mock_modules()
70237087
row = pd.Series(
70247088
{
70257089
"starting_prompt": "start",
@@ -7039,7 +7103,8 @@ async def test_run_adk_user_simulation_success(
70397103
return_value=[mock_invocation]
70407104
)
70417105

7042-
turns = await _evals_common._run_adk_user_simulation(row, mock_agent)
7106+
with mock.patch.dict(sys.modules, mock_modules):
7107+
turns = await _evals_common._run_adk_user_simulation(row, mock_agent)
70437108

70447109
assert len(turns) == 1
70457110
turn = turns[0]
@@ -7058,40 +7123,26 @@ async def test_run_adk_user_simulation_success(
70587123
)
70597124
mock_session_input_cls.assert_called_once()
70607125

7061-
@mock.patch("vertexai._genai._evals_common.ADK_SessionInput") # fmt: skip
7062-
@mock.patch("vertexai._genai._evals_common.EvaluationGenerator") # fmt: skip
7063-
@mock.patch("vertexai._genai._evals_common.LlmBackedUserSimulator") # fmt: skip
7064-
@mock.patch("vertexai._genai._evals_common.ConversationScenario") # fmt: skip
7065-
@mock.patch("vertexai._genai._evals_common.LlmBackedUserSimulatorConfig") # fmt: skip
70667126
@pytest.mark.asyncio
7067-
async def test_run_adk_user_simulation_missing_columns(
7068-
self,
7069-
mock_config_cls,
7070-
mock_scenario_cls,
7071-
mock_simulator_cls,
7072-
mock_generator_cls,
7073-
mock_session_input_cls,
7074-
):
7127+
async def test_run_adk_user_simulation_missing_columns(self):
7128+
mock_modules, _, _, _, _, _ = self._build_adk_mock_modules()
70757129
row = pd.Series({"conversation_plan": "plan"})
70767130
mock_agent = mock.Mock()
70777131

7078-
with pytest.raises(ValueError, match="User simulation requires"):
7079-
await _evals_common._run_adk_user_simulation(row, mock_agent)
7132+
with mock.patch.dict(sys.modules, mock_modules):
7133+
with pytest.raises(ValueError, match="User simulation requires"):
7134+
await _evals_common._run_adk_user_simulation(row, mock_agent)
70807135

7081-
@mock.patch("vertexai._genai._evals_common.ADK_SessionInput") # fmt: skip
7082-
@mock.patch("vertexai._genai._evals_common.EvaluationGenerator") # fmt: skip
7083-
@mock.patch("vertexai._genai._evals_common.LlmBackedUserSimulator") # fmt: skip
7084-
@mock.patch("vertexai._genai._evals_common.ConversationScenario") # fmt: skip
7085-
@mock.patch("vertexai._genai._evals_common.LlmBackedUserSimulatorConfig") # fmt: skip
70867136
@pytest.mark.asyncio
7087-
async def test_run_adk_user_simulation_missing_session_inputs(
7088-
self,
7089-
mock_config_cls,
7090-
mock_scenario_cls,
7091-
mock_simulator_cls,
7092-
mock_generator_cls,
7093-
mock_session_input_cls,
7094-
):
7137+
async def test_run_adk_user_simulation_missing_session_inputs(self):
7138+
(
7139+
mock_modules,
7140+
mock_scenario_cls,
7141+
_,
7142+
_,
7143+
mock_generator_cls,
7144+
mock_session_input_cls,
7145+
) = self._build_adk_mock_modules()
70957146
row = pd.Series(
70967147
{
70977148
"starting_prompt": "start",
@@ -7110,7 +7161,8 @@ async def test_run_adk_user_simulation_missing_session_inputs(
71107161
return_value=[mock_invocation]
71117162
)
71127163

7113-
await _evals_common._run_adk_user_simulation(row, mock_agent)
7164+
with mock.patch.dict(sys.modules, mock_modules):
7165+
await _evals_common._run_adk_user_simulation(row, mock_agent)
71147166

71157167
mock_scenario_cls.assert_called_once_with(
71167168
starting_prompt="start",

0 commit comments

Comments
 (0)