Skip to content

Commit 311a69f

Browse files
vertex-sdk-botcopybara-github
authored andcommitted
feat: Support creating evaluation runs from pre-existing Interactions API data
PiperOrigin-RevId: 944601748
1 parent 135224c commit 311a69f

3 files changed

Lines changed: 391 additions & 0 deletions

File tree

agentplatform/_genai/_evals_common.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464

6565
MAX_WORKERS = 100
6666
AGENT_MAX_WORKERS = 20
67+
_MAX_INTERACTION_CHAIN_DEPTH = 10
6768
CONTENT = _evals_constant.CONTENT
6869
PARTS = _evals_constant.PARTS
6970
USER_AUTHOR = _evals_constant.USER_AUTHOR
@@ -433,6 +434,15 @@ def _resolve_dataset(
433434
) -> types.EvaluationRunDataSource:
434435
"""Resolves dataset for the evaluation run."""
435436
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+
436446
candidate_name = _get_candidate_name(dataset, parsed_agent_info)
437447
eval_df = dataset.eval_dataset_df
438448
if eval_df is None and dataset.eval_cases:
@@ -1007,6 +1017,127 @@ def _build_interaction_id_dataset(
10071017
return types.EvaluationDataset(eval_cases=eval_cases)
10081018

10091019

1020+
def _has_interactions_data_source(
1021+
eval_cases: list[types.EvalCase],
1022+
) -> bool:
1023+
"""Returns True if any EvalCase has interactions_data_source set."""
1024+
return any(case.interactions_data_source is not None for case in eval_cases)
1025+
1026+
1027+
def _resolve_interactions_to_eval_cases(
1028+
api_client: BaseApiClient,
1029+
eval_cases: list[types.EvalCase],
1030+
) -> list[types.EvalCase]:
1031+
"""Resolves EvalCases with interactions_data_source to agent_data.
1032+
1033+
For each EvalCase that has interactions_data_source set, fetches the
1034+
Interaction via the SDK's interactions.get() API, converts the steps
1035+
to AgentData, and returns a new EvalCase with agent_data populated.
1036+
1037+
Args:
1038+
api_client: The API client (must have an interactions module).
1039+
eval_cases: EvalCases with interactions_data_source set.
1040+
1041+
Returns:
1042+
New list of EvalCases with agent_data populated from resolved
1043+
interactions.
1044+
1045+
Raises:
1046+
ValueError: If eval_cases have missing interaction references.
1047+
"""
1048+
# Validate all cases up front before making any API calls.
1049+
for case in eval_cases:
1050+
ids = case.interactions_data_source
1051+
if ids is None:
1052+
raise ValueError(
1053+
"All eval_cases must have interactions_data_source set when"
1054+
" using interaction resolution. Found a case without it. Do"
1055+
" not mix interaction-based and prompt-based eval cases."
1056+
)
1057+
if not ids.interaction:
1058+
raise ValueError(
1059+
"interactions_data_source.interaction is required. Each"
1060+
" EvalCase must reference an existing Interaction resource."
1061+
)
1062+
1063+
resolved_cases = []
1064+
1065+
for case in eval_cases:
1066+
ids = case.interactions_data_source
1067+
1068+
# Extract the interaction short ID from the resource name.
1069+
# Handles both full resource names (projects/.../interactions/{id})
1070+
# and bare IDs by always taking the last path component.
1071+
interaction_id = ids.interaction.split("/")[-1]
1072+
1073+
logger.info("Fetching interaction: %s", ids.interaction)
1074+
1075+
current_interaction_id = interaction_id
1076+
interactions = []
1077+
seen_ids = set()
1078+
for _ in range(_MAX_INTERACTION_CHAIN_DEPTH):
1079+
if current_interaction_id in seen_ids:
1080+
break
1081+
seen_ids.add(current_interaction_id)
1082+
path = f"interactions/{current_interaction_id}"
1083+
response = api_client.request("get", path, {}, None)
1084+
if not response.body:
1085+
if not interactions:
1086+
logger.warning(
1087+
"Empty response fetching interaction %s.",
1088+
ids.interaction,
1089+
)
1090+
break
1091+
interaction_dict = json.loads(response.body)
1092+
try:
1093+
typed_interaction = interaction_types.Interaction.model_validate(interaction_dict)
1094+
except Exception as e:
1095+
logger.warning("Failed to validate interaction model: %s", e)
1096+
break
1097+
1098+
interactions.append(typed_interaction)
1099+
if not typed_interaction.previous_interaction_id:
1100+
break
1101+
current_interaction_id = typed_interaction.previous_interaction_id.split("/")[-1]
1102+
1103+
if not interactions:
1104+
agent_data = types.evals.AgentData(turns=[]) # Fallback
1105+
else:
1106+
interactions.reverse() # chronological order
1107+
all_steps = []
1108+
for i_typed in interactions:
1109+
all_steps.extend(i_typed.steps or [])
1110+
1111+
combined_interaction = interactions[-1].model_dump()
1112+
combined_interaction["steps"] = all_steps
1113+
agent_data = _interaction_dict_to_agent_data(combined_interaction)
1114+
1115+
# Best-effort: fetch the agent config (instruction, tools,
1116+
# description) from the Agent API so the display can render
1117+
# the System Topology section.
1118+
gemini_cfg = ids.gemini_agent_config
1119+
agent_name = gemini_cfg.gemini_agent if gemini_cfg else None
1120+
agent_config = _fetch_agent_config_dict(api_client, agent_name or "")
1121+
agent_data.agents = {agent_config.agent_id: agent_config}
1122+
1123+
# Merge consecutive text events and parts so multi-paragraph
1124+
# responses render as a single block in the trace display.
1125+
_merge_text_parts_in_agent_data(agent_data)
1126+
1127+
# Preserve all original EvalCase fields; only update agent_data
1128+
# and clear the now-resolved interactions_data_source.
1129+
resolved_cases.append(
1130+
case.model_copy(
1131+
update={
1132+
"agent_data": agent_data,
1133+
"interactions_data_source": None,
1134+
}
1135+
)
1136+
)
1137+
1138+
return resolved_cases
1139+
1140+
10101141
def _add_evaluation_run_labels(
10111142
labels: Optional[dict[str, str]] = None,
10121143
agent: Optional[str] = None,

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(

0 commit comments

Comments
 (0)