Skip to content

Commit 163e7ba

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 163e7ba

3 files changed

Lines changed: 392 additions & 0 deletions

File tree

agentplatform/_genai/_evals_common.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@
5454

5555
logger = logging.getLogger(__name__)
5656

57+
_MAX_INTERACTION_CHAIN_DEPTH = 10
58+
5759
try:
5860
import litellm
5961
except ImportError:
@@ -433,6 +435,15 @@ def _resolve_dataset(
433435
) -> types.EvaluationRunDataSource:
434436
"""Resolves dataset for the evaluation run."""
435437
if isinstance(dataset, types.EvaluationDataset):
438+
# Resolve EvalCases with interactions_data_source by fetching
439+
# each interaction and converting it to agent_data, then flowing
440+
# through the normal DataFrame/GCS pipeline.
441+
if dataset.eval_cases and _has_interactions_data_source(dataset.eval_cases):
442+
resolved_cases = _resolve_interactions_to_eval_cases(
443+
api_client, dataset.eval_cases
444+
)
445+
dataset = types.EvaluationDataset(eval_cases=resolved_cases)
446+
436447
candidate_name = _get_candidate_name(dataset, parsed_agent_info)
437448
eval_df = dataset.eval_dataset_df
438449
if eval_df is None and dataset.eval_cases:
@@ -1007,6 +1018,127 @@ def _build_interaction_id_dataset(
10071018
return types.EvaluationDataset(eval_cases=eval_cases)
10081019

10091020

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