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