Skip to content

Commit 2d63940

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

2 files changed

Lines changed: 226 additions & 0 deletions

File tree

agentplatform/_genai/_evals_common.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,96 @@ def _extract_response_from_completed_trace(
420420
return event_dicts
421421

422422

423+
def _has_interactions_data_source(
424+
eval_cases: list[types.EvalCase],
425+
) -> bool:
426+
"""Returns True if any EvalCase has interactions_data_source set."""
427+
return any(
428+
getattr(case, "interactions_data_source", None) is not None
429+
for case in eval_cases
430+
)
431+
432+
433+
def _resolve_interactions_to_eval_cases(
434+
api_client: BaseApiClient,
435+
eval_cases: list[types.EvalCase],
436+
) -> list[types.EvalCase]:
437+
"""Resolves EvalCases with interactions_data_source to agent_data.
438+
439+
For each EvalCase that has interactions_data_source set, fetches the
440+
Interaction via the SDK's interactions.get() API, converts the steps
441+
to AgentData, and returns a new EvalCase with agent_data populated.
442+
443+
Args:
444+
api_client: The API client (must have an interactions module).
445+
eval_cases: EvalCases with interactions_data_source set.
446+
447+
Returns:
448+
New list of EvalCases with agent_data populated from resolved
449+
interactions.
450+
451+
Raises:
452+
ValueError: If eval_cases have missing interaction references.
453+
"""
454+
# Validate all cases up front before making any API calls.
455+
for case in eval_cases:
456+
ids = case.interactions_data_source
457+
if ids is None:
458+
raise ValueError(
459+
"All eval_cases must have interactions_data_source set when"
460+
" using interaction resolution. Found a case without it. Do"
461+
" not mix interaction-based and prompt-based eval cases."
462+
)
463+
if not ids.interaction:
464+
raise ValueError(
465+
"interactions_data_source.interaction is required. Each"
466+
" EvalCase must reference an existing Interaction resource."
467+
)
468+
469+
resolved_cases = []
470+
471+
for case in eval_cases:
472+
ids = case.interactions_data_source
473+
474+
# Extract the interaction ID from the resource name.
475+
# Format: projects/{p}/locations/{l}/interactions/{id}
476+
parts = ids.interaction.split("/")
477+
if len(parts) >= 6 and parts[4] == "interactions":
478+
interaction_id = parts[5]
479+
else:
480+
interaction_id = ids.interaction
481+
482+
logger.info("Fetching interaction: %s", ids.interaction)
483+
path = f"interactions/{interaction_id}"
484+
response = api_client.request("get", path, {}, None)
485+
interaction_dict = (
486+
{} if not response.body else json.loads(response.body)
487+
)
488+
489+
agent_data = _interaction_dict_to_agent_data(interaction_dict)
490+
491+
# Best-effort: fetch the agent config (instruction, tools,
492+
# description) from the Agent API so the display can render
493+
# the System Topology section.
494+
gemini_cfg = getattr(ids, "gemini_agent_config", None)
495+
agent_name = (
496+
getattr(gemini_cfg, "gemini_agent", None)
497+
if gemini_cfg else None
498+
)
499+
agent_config = _fetch_agent_config_dict(
500+
api_client, agent_name or ""
501+
)
502+
agent_data.agents = {agent_config.agent_id: agent_config}
503+
504+
# Merge consecutive text events and parts so multi-paragraph
505+
# responses render as a single block in the trace display.
506+
_merge_text_parts_in_agent_data(agent_data)
507+
508+
resolved_cases.append(types.EvalCase(agent_data=agent_data))
509+
510+
return resolved_cases
511+
512+
423513
def _resolve_dataset(
424514
api_client: BaseApiClient,
425515
dataset: Union[types.EvaluationRunDataSource, types.EvaluationDataset],
@@ -428,6 +518,15 @@ def _resolve_dataset(
428518
) -> types.EvaluationRunDataSource:
429519
"""Resolves dataset for the evaluation run."""
430520
if isinstance(dataset, types.EvaluationDataset):
521+
# Resolve EvalCases with interactions_data_source by fetching
522+
# each interaction and converting it to agent_data, then flowing
523+
# through the normal DataFrame/GCS pipeline.
524+
if dataset.eval_cases and _has_interactions_data_source(dataset.eval_cases):
525+
resolved_cases = _resolve_interactions_to_eval_cases(
526+
api_client, dataset.eval_cases
527+
)
528+
dataset = types.EvaluationDataset(eval_cases=resolved_cases)
529+
431530
candidate_name = _get_candidate_name(dataset, parsed_agent_info)
432531
eval_df = dataset.eval_dataset_df
433532
if eval_df is None and dataset.eval_cases:

tests/unit/agentplatform/genai/test_evals.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9563,6 +9563,133 @@ 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+
"steps": [
9636+
{
9637+
"type": "user_input",
9638+
"content": [{"type": "text", "text": "Hello agent"}],
9639+
},
9640+
{
9641+
"type": "model_output",
9642+
"content": [
9643+
{"type": "text", "text": "Hello! How can I help?"}
9644+
],
9645+
},
9646+
]
9647+
}
9648+
9649+
result = _evals_common._interaction_dict_to_agent_data(
9650+
interaction_dict
9651+
)
9652+
9653+
assert len(result.turns) == 1
9654+
events = result.turns[0].events
9655+
assert len(events) == 2
9656+
assert events[0].author == "user"
9657+
assert events[0].content.parts[0].text == "Hello agent"
9658+
assert events[1].author == "agent"
9659+
assert events[1].content.parts[0].text == "Hello! How can I help?"
9660+
9661+
def test_interaction_dict_to_agent_data_with_tool_calls(self):
9662+
"""Converts function_call + function_result steps."""
9663+
interaction_dict = {
9664+
"steps": [
9665+
{
9666+
"type": "function_call",
9667+
"name": "get_weather",
9668+
"arguments": {"city": "NYC"},
9669+
"id": "call_1",
9670+
},
9671+
{
9672+
"type": "function_result",
9673+
"name": "get_weather",
9674+
"callId": "call_1",
9675+
"result": {"temp": "72F"},
9676+
},
9677+
]
9678+
}
9679+
9680+
result = _evals_common._interaction_dict_to_agent_data(
9681+
interaction_dict
9682+
)
9683+
9684+
events = result.turns[0].events
9685+
assert len(events) == 2
9686+
fc_event = events[0]
9687+
assert fc_event.author == "agent"
9688+
assert fc_event.content.parts[0].function_call.name == "get_weather"
9689+
fr_event = events[1]
9690+
assert fr_event.content.parts[0].function_response.id == "call_1"
9691+
9692+
95669693
class TestRateLimiter:
95679694
"""Tests for the RateLimiter class in _evals_utils."""
95689695

0 commit comments

Comments
 (0)