@@ -923,6 +923,260 @@ def _fetch_agent_config_dict(
923923 )
924924
925925
926+ def _get_resolved_location (api_client : Any ) -> Optional [str ]:
927+ """Returns the location configured on the API client."""
928+ return getattr (api_client , "location" , None )
929+
930+
931+ class _InteractionsRestClient :
932+ """Minimal Interactions API client issued through the SDK api_client.
933+
934+ Calls go through `api_client.request()` (rather than the google.genai
935+ `_gaos` client) so that the `ReplayApiClient` records and replays them.
936+ Requests and responses are plain dicts.
937+ """
938+
939+ def __init__ (self , api_client : BaseApiClient ):
940+ self ._api_client = api_client
941+
942+ def create (self , request_dict : dict [str , Any ]) -> dict [str , Any ]:
943+ response = self ._api_client .request ("post" , "interactions" , request_dict )
944+ return json .loads (response .body ) if response .body else {}
945+
946+ def get (self , interaction_id : str ) -> dict [str , Any ]:
947+ response = self ._api_client .request ("get" , f"interactions/{ interaction_id } " , {})
948+ return json .loads (response .body ) if response .body else {}
949+
950+
951+ def _get_interactions_client (api_client : BaseApiClient ) -> _InteractionsRestClient :
952+ """Returns an Interactions API client bound to `api_client`.
953+
954+ The client issues calls through the SDK's existing `api_client` (a
955+ `BaseApiClient`, or a `ReplayApiClient` in tests) so that replay recording
956+ captures the interaction calls.
957+
958+ Args:
959+ api_client: The API client used to issue interaction calls.
960+
961+ Returns:
962+ An `_InteractionsRestClient`.
963+ """
964+ return _InteractionsRestClient (api_client )
965+
966+
967+ def _agent_data_response_text (agent_data : types .evals .AgentData ) -> Optional [str ]:
968+ """Concatenates the text of all model-role events in an AgentData."""
969+ text_parts : list [str ] = []
970+ for turn in agent_data .turns or []:
971+ for event in turn .events or []:
972+ content = event .content
973+ if not content or content .role != _evals_constant .MODEL_AUTHOR :
974+ continue
975+ for part in content .parts or []:
976+ if part .text :
977+ text_parts .append (part .text )
978+ return "" .join (text_parts ) or None
979+
980+
981+ _INTERACTION_TERMINAL_STATES = frozenset (
982+ ["completed" , "failed" , "cancelled" , "incomplete" , "budget_exceeded" ]
983+ )
984+
985+
986+ def _await_interaction (
987+ interactions_client : "_InteractionsRestClient" ,
988+ interaction : dict [str , Any ],
989+ poll_interval_seconds : float = 2.0 ,
990+ timeout_seconds : float = 600.0 ,
991+ ) -> dict [str , Any ]:
992+ """Polls a background interaction until it reaches a terminal state.
993+
994+ Gemini agent interactions must run in the background (`background=True`), so
995+ `create` returns before the model output is ready. This polls
996+ `interactions.get` until the interaction reaches a terminal state and then
997+ returns the resolved interaction.
998+
999+ Args:
1000+ interactions_client: The interactions client used to poll.
1001+ interaction: The interaction returned by `create`.
1002+ poll_interval_seconds: Delay between poll attempts.
1003+ timeout_seconds: Maximum time to wait before raising.
1004+
1005+ Returns:
1006+ The resolved interaction once it reaches a terminal state.
1007+
1008+ Raises:
1009+ TimeoutError: If the interaction does not complete within the timeout.
1010+ """
1011+ if interaction .get ("status" ) in _INTERACTION_TERMINAL_STATES :
1012+ return interaction
1013+ interaction_id = interaction .get ("id" )
1014+ deadline = time .monotonic () + timeout_seconds
1015+ while time .monotonic () < deadline :
1016+ time .sleep (poll_interval_seconds )
1017+ interaction = interactions_client .get (interaction_id )
1018+ if interaction .get ("status" ) in _INTERACTION_TERMINAL_STATES :
1019+ return interaction
1020+ raise TimeoutError (
1021+ f"Interaction { interaction_id } did not complete within"
1022+ f" { timeout_seconds } seconds."
1023+ )
1024+
1025+
1026+ def _run_gemini_agent_inference (
1027+ * ,
1028+ api_client : BaseApiClient ,
1029+ gemini_agent : str ,
1030+ prompt_dataset : pd .DataFrame ,
1031+ ) -> pd .DataFrame :
1032+ """Runs inference against a Gemini Agents API agent via the Interactions API.
1033+
1034+ For each prompt row, creates an interaction against `gemini_agent` and
1035+ collects the interaction id, response text, and agent data.
1036+
1037+ Args:
1038+ api_client: The API client used to issue interaction calls.
1039+ gemini_agent: The Gemini Agents API agent resource name.
1040+ prompt_dataset: The prompt DataFrame. The prompt is read from the
1041+ `request` column if present, otherwise from the `prompt` column.
1042+
1043+ Returns:
1044+ A DataFrame with columns prompt, response, interaction_id, agent_data.
1045+ """
1046+ prompt_column = (
1047+ "request" if "request" in prompt_dataset .columns else _evals_constant .PROMPT
1048+ )
1049+ if prompt_column not in prompt_dataset .columns :
1050+ raise ValueError (
1051+ "The eval dataset provided for Gemini agent inference must contain a"
1052+ f" '{ _evals_constant .PROMPT } ' or 'request' column."
1053+ )
1054+
1055+ interactions_client = _get_interactions_client (api_client )
1056+
1057+ agent_short_id = gemini_agent .split ("/" )[- 1 ]
1058+ prompts : list [str ] = []
1059+ responses : list [Optional [str ]] = []
1060+ interaction_ids : list [Optional [str ]] = []
1061+ agent_data : list [dict [str , Any ]] = []
1062+ for prompt in tqdm (
1063+ prompt_dataset [prompt_column ].tolist (), desc = "Gemini Agent Inference"
1064+ ):
1065+ prompts .append (prompt )
1066+ try :
1067+ interaction = interactions_client .create (
1068+ {
1069+ "agent" : agent_short_id ,
1070+ "input" : [{"type" : "text" , "text" : prompt }],
1071+ "store" : True ,
1072+ "background" : True ,
1073+ }
1074+ )
1075+ interaction = _await_interaction (interactions_client , interaction )
1076+ agent_data_obj = _interaction_dict_to_agent_data (interaction )
1077+ _merge_text_parts_in_agent_data (agent_data_obj )
1078+ responses .append (_agent_data_response_text (agent_data_obj ))
1079+ interaction_ids .append (interaction .get ("id" ))
1080+ agent_data .append (agent_data_obj .model_dump (mode = "json" , exclude_none = True ))
1081+ except Exception as e : # pylint: disable=broad-exception-caught
1082+ logger .warning (
1083+ "Gemini agent inference failed for a prompt (recording an empty"
1084+ " row and continuing): %s" ,
1085+ e ,
1086+ )
1087+ responses .append (None )
1088+ interaction_ids .append (None )
1089+ agent_data .append ({})
1090+
1091+ return pd .DataFrame (
1092+ {
1093+ _evals_constant .PROMPT : prompts ,
1094+ _evals_constant .RESPONSE : responses ,
1095+ _evals_constant .INTERACTION_ID : interaction_ids ,
1096+ _evals_constant .AGENT_DATA : agent_data ,
1097+ }
1098+ )
1099+
1100+
1101+ def _normalize_interaction_resource (
1102+ interaction : str , agent : str , location : Optional [str ]
1103+ ) -> str :
1104+ """Normalizes an interaction id into a full resource name.
1105+
1106+ A bare interaction id is expanded to
1107+ `projects/{project}/locations/{location}/interactions/{id}` using the
1108+ project and location parsed from the agent resource name. Fully-qualified
1109+ interaction resource names are returned unchanged.
1110+ """
1111+ if interaction .startswith ("projects/" ):
1112+ return interaction
1113+ parts = agent .split ("/" )
1114+ project = parts [1 ]
1115+ agent_location = parts [3 ] if len (parts ) > 3 else (location or "global" )
1116+ return f"projects/{ project } /locations/{ agent_location } /interactions/{ interaction } "
1117+
1118+
1119+ def _build_interaction_id_dataset (
1120+ loaded_data : list [dict [str , Any ]],
1121+ agent : Optional [str ],
1122+ location : Optional [str ],
1123+ ) -> Optional [types .EvaluationDataset ]:
1124+ """Builds an EvaluationDataset from rows that carry an `interaction_id`.
1125+
1126+ When the dataset contains an `interaction_id` column, each row is turned
1127+ into an EvalCase whose `interactions_data_source` references the interaction
1128+ and the Gemini agent. The backend resolves the interaction trace and agent
1129+ config; no client-side prompt/response is required. Returns None if the
1130+ data does not contain interaction ids.
1131+ """
1132+ has_interaction_id = bool (loaded_data ) and any (
1133+ _evals_constant .INTERACTION_ID in row for row in loaded_data
1134+ )
1135+ if not has_interaction_id :
1136+ if agent :
1137+ raise ValueError (
1138+ "An `agent` was provided but the dataset does not contain an"
1139+ " `interaction_id` column. The `agent` argument is only used to"
1140+ " resolve an `interaction_id` dataset column (so the backend can"
1141+ " fetch the interaction trace and Agent config). To evaluate"
1142+ " with an agent, provide a dataset with an `interaction_id`"
1143+ " column; otherwise omit `agent`."
1144+ )
1145+ return None
1146+
1147+ if not agent :
1148+ raise ValueError (
1149+ "An `agent` resource name is required when the dataset contains an"
1150+ " `interaction_id` column, so the backend can resolve the Agent"
1151+ " config for each interaction."
1152+ )
1153+ if not _is_gemini_agent_resource (agent ):
1154+ raise ValueError (
1155+ "`agent` must be a Gemini Agents API resource name of the form"
1156+ " projects/{project}/locations/{location}/agents/{agent} when"
1157+ f" evaluating interaction ids. Got: { agent } "
1158+ )
1159+
1160+ gemini_agent_config = types .GeminiAgentConfig (gemini_agent = agent )
1161+ eval_cases = []
1162+ for i , row in enumerate (loaded_data ):
1163+ interaction = row .get (_evals_constant .INTERACTION_ID )
1164+ if not interaction :
1165+ raise ValueError (f"Missing `interaction_id` value for row { i } ." )
1166+ eval_cases .append (
1167+ types .EvalCase (
1168+ eval_case_id = f"eval_case_{ i } " ,
1169+ interactions_data_source = types .InteractionsDataSource (
1170+ interaction = _normalize_interaction_resource (
1171+ str (interaction ), agent , location
1172+ ),
1173+ gemini_agent_config = gemini_agent_config ,
1174+ ),
1175+ )
1176+ )
1177+ return types .EvaluationDataset (eval_cases = eval_cases )
1178+
1179+
9261180def _add_evaluation_run_labels (
9271181 labels : Optional [dict [str , str ]] = None ,
9281182 agent : Optional [str ] = None ,
@@ -1754,6 +2008,7 @@ def _execute_inference(
17542008 model : Optional [Union [Callable [[Any ], Any ], str ]] = None ,
17552009 agent_engine : Optional [Union [str , types .AgentEngine ]] = None ,
17562010 agent : Optional ["LlmAgent" ] = None , # type: ignore # noqa: F821
2011+ gemini_agent : Optional [str ] = None ,
17572012 dest : Optional [str ] = None ,
17582013 config : Optional [genai_types .GenerateContentConfig ] = None ,
17592014 prompt_template : Optional [Union [str , types .PromptTemplateOrDict ]] = None ,
@@ -1772,6 +2027,8 @@ def _execute_inference(
17722027 agent_engine: The agent engine to use for inference. Can be a resource
17732028 name string or an `AgentEngine` instance.
17742029 agent: The local agent to use for inference. Can be an ADK agent instance.
2030+ gemini_agent: The Gemini Agents API agent resource name to run inference
2031+ against via the Interactions API.
17752032 dest: The destination to save the inference results. Can be a string
17762033 representing a file path or a GCS URI.
17772034 config: The generation configuration for the model.
@@ -1789,9 +2046,10 @@ def _execute_inference(
17892046 if location :
17902047 api_client = _get_api_client_with_location (api_client , location )
17912048
1792- if sum (x is not None for x in [model , agent_engine , agent ]) != 1 :
2049+ if sum (x is not None for x in [model , agent_engine , agent , gemini_agent ]) != 1 :
17932050 raise ValueError (
1794- "Exactly one of model, agent_engine, or agent must be provided."
2051+ "Exactly one of model, agent_engine, agent, or gemini_agent must be"
2052+ " provided."
17952053 )
17962054
17972055 prompt_dataset = _load_dataframe (api_client , src )
@@ -1804,7 +2062,24 @@ def _execute_inference(
18042062
18052063 _apply_prompt_template (prompt_dataset , prompt_template )
18062064
1807- if model :
2065+ if gemini_agent :
2066+ start_time = time .time ()
2067+ logger .debug ("Starting Gemini Agent inference process ..." )
2068+ results_df = _run_gemini_agent_inference (
2069+ api_client = api_client ,
2070+ gemini_agent = gemini_agent ,
2071+ prompt_dataset = prompt_dataset ,
2072+ )
2073+ end_time = time .time ()
2074+ logger .info (
2075+ "Gemini Agent inference completed in %.2f seconds." ,
2076+ end_time - start_time ,
2077+ )
2078+ return types .EvaluationDataset (
2079+ eval_dataset_df = results_df ,
2080+ candidate_name = gemini_agent .split ("/" )[- 1 ],
2081+ )
2082+ elif model :
18082083 start_time = time .time ()
18092084 logger .debug ("Starting inference process ..." )
18102085 results_df = _run_inference_internal (
@@ -1939,6 +2214,8 @@ def _resolve_dataset_inputs(
19392214 dataset_schema : Optional [Literal ["GEMINI" , "FLATTEN" , "OPENAI" ]],
19402215 loader : "_evals_utils.EvalDatasetLoader" ,
19412216 agent_info : Optional [types .evals .AgentInfo ] = None ,
2217+ agent : Optional [str ] = None ,
2218+ api_client : Any = None ,
19422219) -> tuple [types .EvaluationDataset , int ]:
19432220 """Loads and processes single or multiple datasets for evaluation.
19442221
@@ -1988,6 +2265,21 @@ def _resolve_dataset_inputs(
19882265 ds_source_for_loader = _get_dataset_source (ds_item )
19892266 current_loaded_data = loader .load (ds_source_for_loader )
19902267
2268+ interaction_dataset = _build_interaction_id_dataset (
2269+ current_loaded_data , agent , _get_resolved_location (api_client )
2270+ )
2271+ if interaction_dataset is not None :
2272+ if dataset_schema :
2273+ raise ValueError (
2274+ "`dataset_schema` is not supported for datasets with an"
2275+ " `interaction_id` column. The interaction trace and agent"
2276+ " config are resolved by the backend, so no client-side"
2277+ " schema conversion is applied. Omit `dataset_schema` when"
2278+ " evaluating an interaction_id dataset."
2279+ )
2280+ parsed_evaluation_datasets .append (interaction_dataset )
2281+ continue
2282+
19912283 if dataset_schema :
19922284 current_schema = _evals_data_converters .EvalDatasetSchema (dataset_schema )
19932285 else :
@@ -2145,6 +2437,7 @@ def _execute_evaluation( # type: ignore[no-untyped-def]
21452437 api_client : Any ,
21462438 dataset : Union [types .EvaluationDataset , list [types .EvaluationDataset ]],
21472439 metrics : list [types .Metric ],
2440+ agent : Optional [str ] = None ,
21482441 dataset_schema : Optional [Literal ["GEMINI" , "FLATTEN" , "OPENAI" ]] = None ,
21492442 dest : Optional [str ] = None ,
21502443 location : Optional [str ] = None ,
@@ -2225,6 +2518,8 @@ def _execute_evaluation( # type: ignore[no-untyped-def]
22252518 dataset_schema = dataset_schema ,
22262519 loader = loader ,
22272520 agent_info = validated_agent_info ,
2521+ agent = agent ,
2522+ api_client = api_client ,
22282523 )
22292524
22302525 resolved_metrics = _resolve_metrics (metrics , api_client )
0 commit comments