Skip to content

Commit f8dd88f

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 86c1a35 commit f8dd88f

3 files changed

Lines changed: 466 additions & 0 deletions

File tree

agentplatform/_genai/_evals_common.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -923,6 +923,125 @@ def _fetch_agent_config_dict(
923923
)
924924

925925

926+
def _resolve_interactions_for_display(
927+
api_client: BaseApiClient,
928+
dataset_list: list[types.EvaluationDataset],
929+
) -> list[types.EvaluationDataset]:
930+
"""Resolves interactions_data_source on EvalCases for display.
931+
932+
For each EvalCase that has ``interactions_data_source`` set but no
933+
``agent_data``, fetches the Interaction and Agent config from the
934+
API, converts to AgentData, and populates it on the EvalCase so
935+
that ``show()`` can render the System Topology and Conversation
936+
Trace sections.
937+
938+
Args:
939+
api_client: The API client used to fetch interactions and agents.
940+
dataset_list: The original evaluation datasets.
941+
942+
Returns:
943+
A list of EvaluationDatasets with agent_data populated from
944+
resolved interactions.
945+
"""
946+
resolved_datasets = []
947+
for dataset in dataset_list:
948+
if not dataset.eval_cases:
949+
resolved_datasets.append(dataset)
950+
continue
951+
952+
resolved_cases = []
953+
any_resolved = False
954+
for case in dataset.eval_cases:
955+
ids = case.interactions_data_source
956+
if ids and not case.agent_data:
957+
interaction_name = ids.interaction
958+
gemini_cfg = ids.gemini_agent_config
959+
agent_name = gemini_cfg.gemini_agent if gemini_cfg else None
960+
961+
if interaction_name:
962+
try:
963+
# Extract the interaction short ID from the resource
964+
# name; handles both full resource names and bare IDs.
965+
interaction_id = interaction_name.split("/")[-1]
966+
967+
logger.info(
968+
"Fetching interaction chain for %s for display.",
969+
interaction_name,
970+
)
971+
current_interaction_id = interaction_id
972+
interactions = []
973+
seen_ids = set()
974+
for _ in range(10): # max depth
975+
if current_interaction_id in seen_ids:
976+
break
977+
seen_ids.add(current_interaction_id)
978+
path = f"interactions/{current_interaction_id}"
979+
response = api_client.request("get", path, {}, None)
980+
if not response.body:
981+
if not interactions:
982+
logger.warning(
983+
"Empty response fetching interaction %s.",
984+
interaction_name,
985+
)
986+
break
987+
interaction_dict = json.loads(response.body)
988+
interactions.append(interaction_dict)
989+
prev_id_str = interaction_dict.get("previousInteractionId") or interaction_dict.get("previous_interaction_id")
990+
if not prev_id_str:
991+
break
992+
current_interaction_id = prev_id_str.split("/")[-1]
993+
994+
if not interactions:
995+
resolved_cases.append(case)
996+
continue
997+
998+
interactions.reverse() # chronological order
999+
all_steps = []
1000+
for i_dict in interactions:
1001+
all_steps.extend(i_dict.get("steps", []))
1002+
1003+
combined_interaction = interactions[-1].copy()
1004+
combined_interaction["steps"] = all_steps
1005+
logger.warning(f"DEBUG: I fetched {len(interactions)} interactions. all_steps length: {len(all_steps)}")
1006+
for i, idct in enumerate(interactions):
1007+
logger.warning(f"DEBUG int {i}: {idct.get('id', 'no_id')} has {len(idct.get('steps', []))} steps")
1008+
agent_data = _interaction_dict_to_agent_data(combined_interaction)
1009+
1010+
# Best-effort: fetch agent config (instruction,
1011+
# tools, description) from the Agent API.
1012+
agent_config = _fetch_agent_config_dict(
1013+
api_client, agent_name or ""
1014+
)
1015+
agent_data.agents = {agent_config.agent_id: agent_config}
1016+
1017+
_merge_text_parts_in_agent_data(agent_data)
1018+
1019+
# Preserve all original EvalCase fields; only
1020+
# update agent_data.
1021+
resolved_cases.append(
1022+
case.model_copy(update={"agent_data": agent_data})
1023+
)
1024+
any_resolved = True
1025+
continue
1026+
except Exception as e: # pylint: disable=broad-exception-caught
1027+
logger.warning(
1028+
"Failed to resolve interaction %s for display: %s",
1029+
interaction_name,
1030+
e,
1031+
)
1032+
1033+
resolved_cases.append(case)
1034+
1035+
if any_resolved:
1036+
resolved_datasets.append(
1037+
dataset.model_copy(update={"eval_cases": resolved_cases})
1038+
)
1039+
else:
1040+
resolved_datasets.append(dataset)
1041+
1042+
return resolved_datasets
1043+
1044+
9261045
def _add_evaluation_run_labels(
9271046
labels: Optional[dict[str, str]] = None,
9281047
agent: Optional[str] = None,
@@ -2245,6 +2364,11 @@ def _execute_evaluation( # type: ignore[no-untyped-def]
22452364
t2 = time.perf_counter()
22462365
logger.info("Evaluation took: %f seconds", t2 - t1)
22472366

2367+
# Resolve interactions_data_source to agent_data for display.
2368+
# This fetches Interaction trace data client-side so that show() can
2369+
# render the System Topology and Conversation Trace sections.
2370+
dataset_list = _resolve_interactions_for_display(api_client, dataset_list)
2371+
22482372
evaluation_result.evaluation_dataset = dataset_list
22492373
evaluation_result.agent_info = validated_agent_info
22502374

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,57 @@ def test_evaluation_single_turn_agent_data(client):
582582
assert len(evaluation_result.eval_case_results) == 1
583583

584584

585+
def test_evaluation_with_interaction_resolution(client):
586+
"""Tests evaluate() resolves interaction trace + agent config for display."""
587+
interaction_id = "ChAxMDE5YTgzOGU3Mjg4NDE4EAgaATAqBG1haW4"
588+
agent = (
589+
"projects/model-evaluation-dev/locations/global/agents/"
590+
"async-scrape-test-fe111cc6"
591+
)
592+
eval_dataset = types.EvaluationDataset(
593+
eval_cases=[
594+
types.EvalCase(
595+
interactions_data_source=types.InteractionsDataSource(
596+
interaction=(
597+
"projects/model-evaluation-dev/locations/global"
598+
f"/interactions/{interaction_id}"
599+
),
600+
gemini_agent_config=types.GeminiAgentConfig(
601+
gemini_agent=agent,
602+
),
603+
),
604+
)
605+
]
606+
)
607+
608+
evaluation_result = client.evals.evaluate(
609+
dataset=eval_dataset,
610+
metrics=[types.RubricMetric.MULTI_TURN_TASK_SUCCESS],
611+
)
612+
613+
assert isinstance(evaluation_result, types.EvaluationResult)
614+
assert evaluation_result.summary_metrics is not None
615+
assert len(evaluation_result.summary_metrics) > 0
616+
617+
assert evaluation_result.eval_case_results is not None
618+
assert len(evaluation_result.eval_case_results) == 1
619+
620+
# Verify the interaction was resolved for display: agent_data should
621+
# be populated with conversation trace and agent topology.
622+
eval_dataset_list = evaluation_result.evaluation_dataset
623+
assert eval_dataset_list is not None
624+
assert len(eval_dataset_list) > 0
625+
resolved_case = eval_dataset_list[0].eval_cases[0]
626+
assert resolved_case.agent_data is not None
627+
# Conversation trace: at least one turn with events.
628+
assert resolved_case.agent_data.turns is not None
629+
assert len(resolved_case.agent_data.turns) > 0
630+
assert len(resolved_case.agent_data.turns[0].events) > 0
631+
# Agent topology: agents map populated with agent config.
632+
assert resolved_case.agent_data.agents is not None
633+
assert len(resolved_case.agent_data.agents) > 0
634+
635+
585636
pytestmark = pytest_helper.setup(
586637
file=__file__,
587638
globals_for_file=globals(),

0 commit comments

Comments
 (0)