Skip to content

Commit c378551

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: 901494313
1 parent f2d73fd commit c378551

2 files changed

Lines changed: 165 additions & 120 deletions

File tree

tests/unit/vertexai/genai/test_evals.py

Lines changed: 137 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -122,15 +122,21 @@ 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+
# fmt: off
126+
with (
127+
mock.patch("google.cloud.storage.Client") as mock_storage_client,
128+
mock.patch("google.cloud.bigquery.Client") as mock_bq_client,
129+
mock.patch(
130+
"vertexai._genai.evals.Evals.evaluate_instances"
131+
) as mock_evaluate_instances,
132+
mock.patch(
133+
"vertexai._genai._gcs_utils.GcsUtils.upload_json_to_prefix"
134+
) as mock_upload_to_gcs,
135+
mock.patch(
136+
"vertexai._genai._evals_metric_loaders.LazyLoadedPrebuiltMetric._fetch_and_parse"
137+
) as mock_fetch_prebuilt_metric,
138+
):
139+
# fmt: on
134140

135141
def mock_evaluate_instances_side_effect(*args, **kwargs):
136142
metric_config = kwargs.get("metric_config", {})
@@ -3386,14 +3392,8 @@ def test_run_inference_with_agent_engine_falls_back_to_managed_sessions_api(
33863392
assert inference_result.candidate_name == "agent_engine_0"
33873393

33883394
@mock.patch.object(_evals_utils, "EvalDatasetLoader")
3389-
@mock.patch("vertexai._genai._evals_common.InMemorySessionService") # fmt: skip
3390-
@mock.patch("vertexai._genai._evals_common.Runner")
3391-
@mock.patch("vertexai._genai._evals_common.LlmAgent")
33923395
def test_run_inference_with_local_agent(
33933396
self,
3394-
mock_llm_agent,
3395-
mock_runner,
3396-
mock_session_service,
33973397
mock_eval_dataset_loader,
33983398
):
33993399
mock_df = pd.DataFrame(
@@ -3421,8 +3421,15 @@ def test_run_inference_with_local_agent(
34213421
mock_agent_instance.instruction = "mock instruction"
34223422
mock_agent_instance.tools = []
34233423
mock_agent_instance.sub_agents = []
3424-
mock_llm_agent.return_value = mock_agent_instance
3424+
3425+
# Mock ADK modules for lazy imports in _execute_local_agent_run_with_retry_async
3426+
mock_session_service = mock.MagicMock()
34253427
mock_session_service.return_value.create_session = mock.AsyncMock()
3428+
mock_runner = mock.MagicMock()
3429+
mock_adk_sessions_module = mock.MagicMock()
3430+
mock_adk_sessions_module.InMemorySessionService = mock_session_service
3431+
mock_adk_runners_module = mock.MagicMock()
3432+
mock_adk_runners_module.Runner = mock_runner
34263433
mock_runner_instance = mock_runner.return_value
34273434
stream_run_return_value_1 = [
34283435
mock.Mock(
@@ -3473,10 +3480,19 @@ def run_async_side_effect(*args, **kwargs):
34733480

34743481
mock_runner_instance.run_async.side_effect = run_async_side_effect
34753482

3476-
inference_result = self.client.evals.run_inference(
3477-
agent=mock_agent_instance,
3478-
src=mock_df,
3479-
)
3483+
with mock.patch.dict(
3484+
sys.modules,
3485+
{
3486+
"google.adk": mock.MagicMock(),
3487+
"google.adk.sessions": mock_adk_sessions_module,
3488+
"google.adk.runners": mock_adk_runners_module,
3489+
"google.adk.agents": mock.MagicMock(),
3490+
},
3491+
):
3492+
inference_result = self.client.evals.run_inference(
3493+
agent=mock_agent_instance,
3494+
src=mock_df,
3495+
)
34803496

34813497
mock_eval_dataset_loader.return_value.load.assert_called_once_with(mock_df)
34823498
assert mock_session_service.call_count == 2
@@ -3602,11 +3618,13 @@ def test_run_inference_with_litellm_string_prompt_format(
36023618
mock_api_client_fixture,
36033619
):
36043620
"""Tests inference with LiteLLM using a simple prompt string."""
3621+
# fmt: off
36053622
with mock.patch(
36063623
"vertexai._genai._evals_common.litellm"
36073624
) as mock_litellm, mock.patch(
36083625
"vertexai._genai._evals_common._call_litellm_completion"
36093626
) as mock_call_litellm_completion:
3627+
# fmt: on
36103628
mock_litellm.utils.get_valid_models.return_value = ["gpt-4o"]
36113629
prompt_df = pd.DataFrame([{"prompt": "What is LiteLLM?"}])
36123630
expected_messages = [{"role": "user", "content": "What is LiteLLM?"}]
@@ -3658,11 +3676,16 @@ def test_run_inference_with_litellm_openai_request_format(
36583676
mock_api_client_fixture,
36593677
):
36603678
"""Tests inference with LiteLLM where the row contains a chat completion request body."""
3661-
with mock.patch(
3662-
"vertexai._genai._evals_common.litellm"
3663-
) as mock_litellm, mock.patch(
3664-
"vertexai._genai._evals_common._call_litellm_completion"
3665-
) as mock_call_litellm_completion:
3679+
# fmt: off
3680+
with (
3681+
mock.patch(
3682+
"vertexai._genai._evals_common.litellm"
3683+
) as mock_litellm,
3684+
mock.patch(
3685+
"vertexai._genai._evals_common._call_litellm_completion"
3686+
) as mock_call_litellm_completion,
3687+
):
3688+
# fmt: on
36663689
mock_litellm.utils.get_valid_models.return_value = ["gpt-4o"]
36673690
prompt_df = pd.DataFrame(
36683691
[
@@ -4178,21 +4201,23 @@ def test_run_agent_internal_multi_turn_with_agent(self, mock_run_agent):
41784201
]
41794202
assert "mock_agent" in agent_data["agents"]
41804203

4181-
@mock.patch("vertexai._genai._evals_common.ADK_SessionInput") # fmt: skip
4182-
@mock.patch("vertexai._genai._evals_common.EvaluationGenerator") # fmt: skip
4183-
@mock.patch("vertexai._genai._evals_common.LlmBackedUserSimulator") # fmt: skip
4184-
@mock.patch("vertexai._genai._evals_common.ConversationScenario") # fmt: skip
4185-
@mock.patch("vertexai._genai._evals_common.LlmBackedUserSimulatorConfig") # fmt: skip
41864204
@pytest.mark.asyncio
4187-
async def test_run_adk_user_simulation_with_intermediate_events(
4188-
self,
4189-
mock_config,
4190-
mock_scenario,
4191-
mock_simulator,
4192-
mock_generator,
4193-
mock_session_input,
4194-
):
4205+
async def test_run_adk_user_simulation_with_intermediate_events(self):
41954206
"""Tests that intermediate invocation events (e.g. tool calls) are parsed successfully."""
4207+
mock_scenario = mock.MagicMock()
4208+
mock_config = mock.MagicMock()
4209+
mock_simulator = mock.MagicMock()
4210+
mock_generator = mock.MagicMock()
4211+
mock_session_input = mock.MagicMock()
4212+
mock_adk_eval_scenarios = mock.MagicMock()
4213+
mock_adk_eval_scenarios.ConversationScenario = mock_scenario
4214+
mock_adk_eval_case = mock.MagicMock()
4215+
mock_adk_eval_case.SessionInput = mock_session_input
4216+
mock_adk_eval_generator = mock.MagicMock()
4217+
mock_adk_eval_generator.EvaluationGenerator = mock_generator
4218+
mock_adk_simulator_module = mock.MagicMock()
4219+
mock_adk_simulator_module.LlmBackedUserSimulator = mock_simulator
4220+
mock_adk_simulator_module.LlmBackedUserSimulatorConfig = mock_config
41964221
row = pd.Series(
41974222
{
41984223
"starting_prompt": "I want a laptop.",
@@ -4245,7 +4270,19 @@ async def test_run_adk_user_simulation_with_intermediate_events(
42454270
mock_generator._generate_inferences_from_root_agent = mock.AsyncMock(
42464271
return_value=[mock_invocation]
42474272
)
4248-
turns = await _evals_common._run_adk_user_simulation(row, mock_agent)
4273+
with mock.patch.dict(
4274+
sys.modules,
4275+
{
4276+
"google.adk": mock.MagicMock(),
4277+
"google.adk.evaluation": mock.MagicMock(),
4278+
"google.adk.evaluation.conversation_scenarios": mock_adk_eval_scenarios,
4279+
"google.adk.evaluation.eval_case": mock_adk_eval_case,
4280+
"google.adk.evaluation.evaluation_generator": mock_adk_eval_generator,
4281+
"google.adk.evaluation.simulation": mock.MagicMock(),
4282+
"google.adk.evaluation.simulation.llm_backed_user_simulator": mock_adk_simulator_module,
4283+
},
4284+
):
4285+
turns = await _evals_common._run_adk_user_simulation(row, mock_agent)
42494286

42504287
assert len(turns) == 1
42514288
turn = turns[0]
@@ -7086,20 +7123,50 @@ def test_build_request_payload_tool_use_quality_v1_with_agent_data_tool_call(
70867123
class TestRunAdkUserSimulation:
70877124
"""Unit tests for the _run_adk_user_simulation function."""
70887125

7089-
@mock.patch("vertexai._genai._evals_common.ADK_SessionInput") # fmt: skip
7090-
@mock.patch("vertexai._genai._evals_common.EvaluationGenerator") # fmt: skip
7091-
@mock.patch("vertexai._genai._evals_common.LlmBackedUserSimulator") # fmt: skip
7092-
@mock.patch("vertexai._genai._evals_common.ConversationScenario") # fmt: skip
7093-
@mock.patch("vertexai._genai._evals_common.LlmBackedUserSimulatorConfig") # fmt: skip
7126+
def _build_adk_mock_modules(self):
7127+
"""Builds mock ADK modules for lazy imports in _run_adk_user_simulation."""
7128+
mock_scenario_cls = mock.MagicMock()
7129+
mock_config_cls = mock.MagicMock()
7130+
mock_simulator_cls = mock.MagicMock()
7131+
mock_generator_cls = mock.MagicMock()
7132+
mock_session_input_cls = mock.MagicMock()
7133+
mock_modules = {
7134+
"google.adk": mock.MagicMock(),
7135+
"google.adk.evaluation": mock.MagicMock(),
7136+
"google.adk.evaluation.conversation_scenarios": mock.MagicMock(
7137+
ConversationScenario=mock_scenario_cls
7138+
),
7139+
"google.adk.evaluation.eval_case": mock.MagicMock(
7140+
SessionInput=mock_session_input_cls
7141+
),
7142+
"google.adk.evaluation.evaluation_generator": mock.MagicMock(
7143+
EvaluationGenerator=mock_generator_cls
7144+
),
7145+
"google.adk.evaluation.simulation": mock.MagicMock(),
7146+
"google.adk.evaluation.simulation.llm_backed_user_simulator": mock.MagicMock(
7147+
LlmBackedUserSimulator=mock_simulator_cls,
7148+
LlmBackedUserSimulatorConfig=mock_config_cls,
7149+
),
7150+
}
7151+
return (
7152+
mock_modules,
7153+
mock_scenario_cls,
7154+
mock_config_cls,
7155+
mock_simulator_cls,
7156+
mock_generator_cls,
7157+
mock_session_input_cls,
7158+
)
7159+
70947160
@pytest.mark.asyncio
7095-
async def test_run_adk_user_simulation_success(
7096-
self,
7097-
mock_config_cls,
7098-
mock_scenario_cls,
7099-
mock_simulator_cls,
7100-
mock_generator_cls,
7101-
mock_session_input_cls,
7102-
):
7161+
async def test_run_adk_user_simulation_success(self):
7162+
(
7163+
mock_modules,
7164+
mock_scenario_cls,
7165+
_,
7166+
_,
7167+
mock_generator_cls,
7168+
mock_session_input_cls,
7169+
) = self._build_adk_mock_modules()
71037170
row = pd.Series(
71047171
{
71057172
"starting_prompt": "start",
@@ -7119,7 +7186,8 @@ async def test_run_adk_user_simulation_success(
71197186
return_value=[mock_invocation]
71207187
)
71217188

7122-
turns = await _evals_common._run_adk_user_simulation(row, mock_agent)
7189+
with mock.patch.dict(sys.modules, mock_modules):
7190+
turns = await _evals_common._run_adk_user_simulation(row, mock_agent)
71237191

71247192
assert len(turns) == 1
71257193
turn = turns[0]
@@ -7138,40 +7206,26 @@ async def test_run_adk_user_simulation_success(
71387206
)
71397207
mock_session_input_cls.assert_called_once()
71407208

7141-
@mock.patch("vertexai._genai._evals_common.ADK_SessionInput") # fmt: skip
7142-
@mock.patch("vertexai._genai._evals_common.EvaluationGenerator") # fmt: skip
7143-
@mock.patch("vertexai._genai._evals_common.LlmBackedUserSimulator") # fmt: skip
7144-
@mock.patch("vertexai._genai._evals_common.ConversationScenario") # fmt: skip
7145-
@mock.patch("vertexai._genai._evals_common.LlmBackedUserSimulatorConfig") # fmt: skip
71467209
@pytest.mark.asyncio
7147-
async def test_run_adk_user_simulation_missing_columns(
7148-
self,
7149-
mock_config_cls,
7150-
mock_scenario_cls,
7151-
mock_simulator_cls,
7152-
mock_generator_cls,
7153-
mock_session_input_cls,
7154-
):
7210+
async def test_run_adk_user_simulation_missing_columns(self):
7211+
mock_modules, _, _, _, _, _ = self._build_adk_mock_modules()
71557212
row = pd.Series({"conversation_plan": "plan"})
71567213
mock_agent = mock.Mock()
71577214

7158-
with pytest.raises(ValueError, match="User simulation requires"):
7159-
await _evals_common._run_adk_user_simulation(row, mock_agent)
7215+
with mock.patch.dict(sys.modules, mock_modules):
7216+
with pytest.raises(ValueError, match="User simulation requires"):
7217+
await _evals_common._run_adk_user_simulation(row, mock_agent)
71607218

7161-
@mock.patch("vertexai._genai._evals_common.ADK_SessionInput") # fmt: skip
7162-
@mock.patch("vertexai._genai._evals_common.EvaluationGenerator") # fmt: skip
7163-
@mock.patch("vertexai._genai._evals_common.LlmBackedUserSimulator") # fmt: skip
7164-
@mock.patch("vertexai._genai._evals_common.ConversationScenario") # fmt: skip
7165-
@mock.patch("vertexai._genai._evals_common.LlmBackedUserSimulatorConfig") # fmt: skip
71667219
@pytest.mark.asyncio
7167-
async def test_run_adk_user_simulation_missing_session_inputs(
7168-
self,
7169-
mock_config_cls,
7170-
mock_scenario_cls,
7171-
mock_simulator_cls,
7172-
mock_generator_cls,
7173-
mock_session_input_cls,
7174-
):
7220+
async def test_run_adk_user_simulation_missing_session_inputs(self):
7221+
(
7222+
mock_modules,
7223+
mock_scenario_cls,
7224+
_,
7225+
_,
7226+
mock_generator_cls,
7227+
mock_session_input_cls,
7228+
) = self._build_adk_mock_modules()
71757229
row = pd.Series(
71767230
{
71777231
"starting_prompt": "start",
@@ -7190,7 +7244,8 @@ async def test_run_adk_user_simulation_missing_session_inputs(
71907244
return_value=[mock_invocation]
71917245
)
71927246

7193-
await _evals_common._run_adk_user_simulation(row, mock_agent)
7247+
with mock.patch.dict(sys.modules, mock_modules):
7248+
await _evals_common._run_adk_user_simulation(row, mock_agent)
71947249

71957250
mock_scenario_cls.assert_called_once_with(
71967251
starting_prompt="start",

0 commit comments

Comments
 (0)