Skip to content

Commit 718cdb8

Browse files
vertex-sdk-botcopybara-github
authored andcommitted
feat: Resolve Interactions API data for agent trace display in eval results.
PiperOrigin-RevId: 944658614
1 parent 135224c commit 718cdb8

3 files changed

Lines changed: 500 additions & 1 deletion

File tree

agentplatform/_genai/_evals_common.py

Lines changed: 158 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@
4848
from . import _evals_metric_loaders
4949
from . import _evals_utils
5050
from . import _gcs_utils
51+
52+
_MAX_INTERACTION_CHAIN_DEPTH = 10
5153
from . import evals
5254
from . import types
5355
from . import _transformers as t
@@ -720,7 +722,9 @@ def _interaction_dict_to_agent_data(
720722
Returns:
721723
An AgentData object with one or more ConversationTurns.
722724
"""
723-
typed_interaction = interaction_types.Interaction.model_validate(interaction)
725+
typed_interaction = interaction_types.Interaction.model_validate(
726+
interaction
727+
)
724728
all_events = _interaction_steps_to_events(typed_interaction.steps or [])
725729

726730
# Group events into turns. Each UserInputStep starts a new turn.
@@ -1007,6 +1011,154 @@ def _build_interaction_id_dataset(
10071011
return types.EvaluationDataset(eval_cases=eval_cases)
10081012

10091013

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

2504+
# Resolve interactions_data_source to agent_data for display.
2505+
# This fetches Interaction trace data client-side so that show() can
2506+
# render the System Topology and Conversation Trace sections.
2507+
dataset_list = _resolve_interactions_for_display(api_client, dataset_list)
2508+
23522509
evaluation_result.evaluation_dataset = dataset_list
23532510
evaluation_result.agent_info = validated_agent_info
23542511

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,57 @@ def test_evaluation_with_interaction_id(client):
609609
assert len(evaluation_result.eval_case_results) == 1
610610

611611

612+
def test_evaluation_with_interaction_resolution(client):
613+
"""Tests evaluate() resolves interaction trace + agent config for display."""
614+
interaction_id = "ChAxMDE5YTgzOGU3Mjg4NDE4EAgaATAqBG1haW4"
615+
agent = (
616+
"projects/model-evaluation-dev/locations/global/agents/"
617+
"async-scrape-test-fe111cc6"
618+
)
619+
eval_dataset = types.EvaluationDataset(
620+
eval_cases=[
621+
types.EvalCase(
622+
interactions_data_source=types.InteractionsDataSource(
623+
interaction=(
624+
"projects/model-evaluation-dev/locations/global"
625+
f"/interactions/{interaction_id}"
626+
),
627+
gemini_agent_config=types.GeminiAgentConfig(
628+
gemini_agent=agent,
629+
),
630+
),
631+
)
632+
]
633+
)
634+
635+
evaluation_result = client.evals.evaluate(
636+
dataset=eval_dataset,
637+
metrics=[types.RubricMetric.MULTI_TURN_TASK_SUCCESS],
638+
)
639+
640+
assert isinstance(evaluation_result, types.EvaluationResult)
641+
assert evaluation_result.summary_metrics is not None
642+
assert len(evaluation_result.summary_metrics) > 0
643+
644+
assert evaluation_result.eval_case_results is not None
645+
assert len(evaluation_result.eval_case_results) == 1
646+
647+
# Verify the interaction was resolved for display: agent_data should
648+
# be populated with conversation trace and agent topology.
649+
eval_dataset_list = evaluation_result.evaluation_dataset
650+
assert eval_dataset_list is not None
651+
assert len(eval_dataset_list) > 0
652+
resolved_case = eval_dataset_list[0].eval_cases[0]
653+
assert resolved_case.agent_data is not None
654+
# Conversation trace: at least one turn with events.
655+
assert resolved_case.agent_data.turns is not None
656+
assert len(resolved_case.agent_data.turns) > 0
657+
assert len(resolved_case.agent_data.turns[0].events) > 0
658+
# Agent topology: agents map populated with agent config.
659+
assert resolved_case.agent_data.agents is not None
660+
assert len(resolved_case.agent_data.agents) > 0
661+
662+
612663
pytestmark = pytest_helper.setup(
613664
file=__file__,
614665
globals_for_file=globals(),

0 commit comments

Comments
 (0)