Skip to content

Commit 7af8b2b

Browse files
vertex-sdk-botcopybara-github
authored andcommitted
feat: Support creating evaluation runs from pre-existing Interactions API data
PiperOrigin-RevId: 944601748
1 parent f158517 commit 7af8b2b

3 files changed

Lines changed: 269 additions & 0 deletions

File tree

agentplatform/_genai/_evals_common.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,15 @@ def _resolve_dataset(
434434
) -> types.EvaluationRunDataSource:
435435
"""Resolves dataset for the evaluation run."""
436436
if isinstance(dataset, types.EvaluationDataset):
437+
# Resolve EvalCases with interactions_data_source by fetching
438+
# each interaction and converting it to agent_data, then flowing
439+
# through the normal DataFrame/GCS pipeline.
440+
if dataset.eval_cases and _has_interactions_data_source(dataset.eval_cases):
441+
resolved_cases = _resolve_interactions_to_eval_cases(
442+
api_client, dataset.eval_cases
443+
)
444+
dataset = types.EvaluationDataset(eval_cases=resolved_cases)
445+
437446
candidate_name = _get_candidate_name(dataset, parsed_agent_info)
438447
eval_df = dataset.eval_dataset_df
439448
if eval_df is None and dataset.eval_cases:

tests/unit/agentplatform/genai/replays/test_create_evaluation_run.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -709,6 +709,42 @@ def test_create_eval_run_with_metric_resource_name(mock_uuid4, client):
709709
# == INPUT_DF_WITH_CONTEXT_AND_HISTORY.iloc[i]["response"]
710710
# )
711711
# assert evaluation_run.error is None
712+
@mock.patch("uuid.uuid4")
713+
def test_create_eval_run_with_interactions_data_source(mock_uuid4, client):
714+
"""Tests create_evaluation_run() with EvalCases using interactions_data_source.
715+
716+
The SDK resolves each interaction client-side (GET interactions/{id} and
717+
GET agents/{id}) before uploading the EvalCases to the evaluation pipeline.
718+
"""
719+
mock_uuid4.return_value = uuid.UUID("a1b2c3d4-e5f6-7890-abcd-ef1234567890")
720+
client._api_client._http_options.api_version = "v1beta1"
721+
interaction_id = "ChA2YzllYzk0MjY1NjZjODM5EAgaATAqBG1haW4"
722+
gemini_agent = (
723+
"projects/model-evaluation-dev/locations/global/agents/test-agent-eval"
724+
)
725+
eval_case = types.EvalCase(
726+
interactions_data_source=types.InteractionsDataSource(
727+
interaction=(
728+
f"projects/model-evaluation-dev/locations/global"
729+
f"/interactions/{interaction_id}"
730+
),
731+
gemini_agent_config=types.GeminiAgentConfig(
732+
gemini_agent=gemini_agent,
733+
),
734+
)
735+
)
736+
evaluation_run = client.evals.create_evaluation_run(
737+
name="test_interactions_data_source",
738+
display_name="test_interactions_data_source",
739+
dataset=types.EvaluationDataset(eval_cases=[eval_case]),
740+
dest=GCS_DEST,
741+
metrics=[GENERAL_QUALITY_METRIC],
742+
)
743+
assert isinstance(evaluation_run, types.EvaluationRun)
744+
assert evaluation_run.state == types.EvaluationRunState.PENDING
745+
assert evaluation_run.error is None
746+
747+
712748
def test_create_eval_run_with_red_teaming_config(client):
713749
"""Tests that create_evaluation_run() with red_teaming_config sends analysisConfigs."""
714750
evaluation_run = client.evals.create_evaluation_run(

tests/unit/agentplatform/genai/test_evals.py

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9563,6 +9563,230 @@ def test_resolve_dataset_preserves_conversation_history(
95639563
assert "conversation_history" in ptd_values
95649564

95659565

9566+
class TestResolveDatasetWithInteractions:
9567+
"""Tests for resolving interactions_data_source in _resolve_dataset."""
9568+
9569+
def setup_method(self):
9570+
self.mock_api_client = mock.Mock()
9571+
self.mock_api_client.project = "test-project"
9572+
self.mock_api_client.location = "us-central1"
9573+
9574+
def test_has_interactions_data_source_true(self):
9575+
cases = [
9576+
agentplatform_genai_types.EvalCase(
9577+
interactions_data_source=agentplatform_genai_types.InteractionsDataSource(
9578+
gemini_agent_config=agentplatform_genai_types.GeminiAgentConfig(
9579+
gemini_agent="projects/p/locations/l/agents/a"
9580+
),
9581+
interaction="projects/p/locations/l/interactions/i1",
9582+
)
9583+
)
9584+
]
9585+
assert _evals_common._has_interactions_data_source(cases)
9586+
9587+
def test_has_interactions_data_source_false(self):
9588+
cases = [
9589+
agentplatform_genai_types.EvalCase(
9590+
prompt=genai_types.Content(
9591+
parts=[genai_types.Part(text="test")]
9592+
),
9593+
)
9594+
]
9595+
assert not _evals_common._has_interactions_data_source(cases)
9596+
9597+
def test_resolve_rejects_mixed_cases(self):
9598+
"""Mixing interaction-based and prompt-based cases raises ValueError."""
9599+
cases = [
9600+
agentplatform_genai_types.EvalCase(
9601+
interactions_data_source=agentplatform_genai_types.InteractionsDataSource(
9602+
interaction="projects/p/locations/l/interactions/i1",
9603+
)
9604+
),
9605+
agentplatform_genai_types.EvalCase(
9606+
prompt=genai_types.Content(
9607+
parts=[genai_types.Part(text="test")]
9608+
),
9609+
),
9610+
]
9611+
with pytest.raises(ValueError, match="interactions_data_source"):
9612+
_evals_common._resolve_interactions_to_eval_cases(
9613+
self.mock_api_client, cases
9614+
)
9615+
9616+
def test_resolve_rejects_missing_interaction(self):
9617+
"""EvalCase with interactions_data_source but no interaction raises."""
9618+
cases = [
9619+
agentplatform_genai_types.EvalCase(
9620+
interactions_data_source=agentplatform_genai_types.InteractionsDataSource(
9621+
gemini_agent_config=agentplatform_genai_types.GeminiAgentConfig(
9622+
gemini_agent="projects/p/locations/l/agents/a"
9623+
),
9624+
)
9625+
),
9626+
]
9627+
with pytest.raises(ValueError, match="interaction is required"):
9628+
_evals_common._resolve_interactions_to_eval_cases(
9629+
self.mock_api_client, cases
9630+
)
9631+
9632+
def test_interaction_dict_to_agent_data_text_conversation(self):
9633+
"""Converts user_input + model_output steps to agent_data."""
9634+
interaction_dict = {
9635+
"status": "completed",
9636+
"steps": [
9637+
{
9638+
"type": "user_input",
9639+
"content": [{"type": "text", "text": "Hello agent"}],
9640+
},
9641+
{
9642+
"type": "model_output",
9643+
"content": [
9644+
{"type": "text", "text": "Hello! How can I help?"}
9645+
],
9646+
},
9647+
]
9648+
}
9649+
9650+
result = _evals_common._interaction_dict_to_agent_data(
9651+
interaction_dict
9652+
)
9653+
9654+
assert len(result.turns) == 1
9655+
events = result.turns[0].events
9656+
assert len(events) == 2
9657+
assert events[0].author == "user"
9658+
assert events[0].content.parts[0].text == "Hello agent"
9659+
assert events[1].author == "agent"
9660+
assert events[1].content.parts[0].text == "Hello! How can I help?"
9661+
9662+
def test_interaction_dict_to_agent_data_with_tool_calls(self):
9663+
"""Converts function_call + function_result steps."""
9664+
interaction_dict = {
9665+
"status": "completed",
9666+
"steps": [
9667+
{
9668+
"type": "function_call",
9669+
"name": "get_weather",
9670+
"arguments": {"city": "NYC"},
9671+
"id": "call_1",
9672+
},
9673+
{
9674+
"type": "function_result",
9675+
"name": "get_weather",
9676+
"call_id": "call_1",
9677+
"result": {"temp": "72F"},
9678+
},
9679+
]
9680+
}
9681+
9682+
result = _evals_common._interaction_dict_to_agent_data(
9683+
interaction_dict
9684+
)
9685+
9686+
events = result.turns[0].events
9687+
assert len(events) == 2
9688+
fc_event = events[0]
9689+
assert fc_event.author == "agent"
9690+
assert fc_event.content.parts[0].function_call.name == "get_weather"
9691+
fr_event = events[1]
9692+
assert fr_event.content.parts[0].function_response.id == "call_1"
9693+
9694+
def test_resolve_interactions_to_eval_cases_happy_path(self):
9695+
"""Fetches an interaction and populates agent_data on the EvalCase."""
9696+
interaction_body = json.dumps({
9697+
"status": "completed",
9698+
"steps": [
9699+
{
9700+
"type": "user_input",
9701+
"content": [{"type": "text", "text": "What is the weather?"}],
9702+
},
9703+
{
9704+
"type": "model_output",
9705+
"content": [{"type": "text", "text": "It is sunny."}],
9706+
},
9707+
]
9708+
})
9709+
agent_body = json.dumps({
9710+
"system_instruction": "You are helpful.",
9711+
"base_agent": "gemini-2.0-flash",
9712+
})
9713+
9714+
def mock_request(method, path, *args, **kwargs):
9715+
resp = mock.MagicMock()
9716+
if path.startswith("interactions/"):
9717+
resp.body = interaction_body
9718+
elif path.startswith("agents/"):
9719+
resp.body = agent_body
9720+
else:
9721+
resp.body = None
9722+
return resp
9723+
9724+
self.mock_api_client.request.side_effect = mock_request
9725+
9726+
original_case = agentplatform_genai_types.EvalCase(
9727+
interactions_data_source=agentplatform_genai_types.InteractionsDataSource(
9728+
gemini_agent_config=agentplatform_genai_types.GeminiAgentConfig(
9729+
gemini_agent="projects/p/locations/l/agents/my-agent",
9730+
),
9731+
interaction="projects/p/locations/l/interactions/i1",
9732+
),
9733+
)
9734+
9735+
resolved = _evals_common._resolve_interactions_to_eval_cases(
9736+
self.mock_api_client, [original_case]
9737+
)
9738+
9739+
assert len(resolved) == 1
9740+
result_case = resolved[0]
9741+
9742+
# interactions_data_source should be cleared after resolution.
9743+
assert result_case.interactions_data_source is None
9744+
9745+
# agent_data should be populated with the interaction content.
9746+
agent_data = result_case.agent_data
9747+
assert agent_data is not None
9748+
assert len(agent_data.turns) == 1
9749+
events = agent_data.turns[0].events
9750+
assert len(events) == 2
9751+
assert events[0].author == "user"
9752+
assert events[0].content.parts[0].text == "What is the weather?"
9753+
assert events[1].author == "agent"
9754+
assert events[1].content.parts[0].text == "It is sunny."
9755+
9756+
# agents map should be populated with config from the Agent API.
9757+
assert "my-agent" in agent_data.agents
9758+
agent_config = agent_data.agents["my-agent"]
9759+
assert agent_config.agent_id == "my-agent"
9760+
assert agent_config.instruction == "You are helpful."
9761+
assert agent_config.agent_type == "gemini-2.0-flash"
9762+
9763+
def test_resolve_preserves_original_eval_case_fields(self):
9764+
"""Fields other than agent_data are preserved on the resolved EvalCase."""
9765+
interaction_body = json.dumps({"status": "completed", "steps": []})
9766+
9767+
def mock_request(method, path, *args, **kwargs):
9768+
resp = mock.MagicMock()
9769+
resp.body = interaction_body
9770+
return resp
9771+
9772+
self.mock_api_client.request.side_effect = mock_request
9773+
9774+
original_case = agentplatform_genai_types.EvalCase(
9775+
interactions_data_source=agentplatform_genai_types.InteractionsDataSource(
9776+
interaction="projects/p/locations/l/interactions/i1",
9777+
),
9778+
)
9779+
9780+
resolved = _evals_common._resolve_interactions_to_eval_cases(
9781+
self.mock_api_client, [original_case]
9782+
)
9783+
9784+
assert len(resolved) == 1
9785+
# agent_data is populated and interactions_data_source cleared.
9786+
assert resolved[0].agent_data is not None
9787+
assert resolved[0].interactions_data_source is None
9788+
9789+
95669790
class TestRateLimiter:
95679791
"""Tests for the RateLimiter class in _evals_utils."""
95689792

0 commit comments

Comments
 (0)