|
54 | 54 |
|
55 | 55 | logger = logging.getLogger(__name__) |
56 | 56 |
|
| 57 | +_MAX_INTERACTION_CHAIN_DEPTH = 10 |
| 58 | + |
57 | 59 | try: |
58 | 60 | import litellm |
59 | 61 | except ImportError: |
@@ -433,6 +435,15 @@ def _resolve_dataset( |
433 | 435 | ) -> types.EvaluationRunDataSource: |
434 | 436 | """Resolves dataset for the evaluation run.""" |
435 | 437 | 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 | + |
436 | 447 | candidate_name = _get_candidate_name(dataset, parsed_agent_info) |
437 | 448 | eval_df = dataset.eval_dataset_df |
438 | 449 | if eval_df is None and dataset.eval_cases: |
@@ -1007,6 +1018,127 @@ def _build_interaction_id_dataset( |
1007 | 1018 | return types.EvaluationDataset(eval_cases=eval_cases) |
1008 | 1019 |
|
1009 | 1020 |
|
| 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 | + |
1010 | 1142 | def _add_evaluation_run_labels( |
1011 | 1143 | labels: Optional[dict[str, str]] = None, |
1012 | 1144 | agent: Optional[str] = None, |
|
0 commit comments