@@ -938,6 +938,176 @@ def _get_resolved_location(api_client: Any) -> Optional[str]:
938938 return getattr (api_client , "location" , None )
939939
940940
941+ class _InteractionsRestClient :
942+ """Minimal Interactions API client issued through the SDK api_client.
943+
944+ Calls go through `api_client.request()` (rather than the google.genai
945+ `_gaos` client) so that the `ReplayApiClient` records and replays them.
946+ Requests and responses are plain dicts.
947+ """
948+
949+ def __init__ (self , api_client : BaseApiClient ):
950+ self ._api_client = api_client
951+
952+ def create (self , request_dict : dict [str , Any ]) -> dict [str , Any ]:
953+ response = self ._api_client .request ("post" , "interactions" , request_dict )
954+ return json .loads (response .body ) if response .body else {}
955+
956+ def get (self , interaction_id : str ) -> dict [str , Any ]:
957+ response = self ._api_client .request ("get" , f"interactions/{ interaction_id } " , {})
958+ return json .loads (response .body ) if response .body else {}
959+
960+
961+ def _get_interactions_client (api_client : BaseApiClient ) -> _InteractionsRestClient :
962+ """Returns an Interactions API client bound to `api_client`.
963+
964+ The client issues calls through the SDK's existing `api_client` (a
965+ `BaseApiClient`, or a `ReplayApiClient` in tests) so that replay recording
966+ captures the interaction calls.
967+
968+ Args:
969+ api_client: The API client used to issue interaction calls.
970+
971+ Returns:
972+ An `_InteractionsRestClient`.
973+ """
974+ return _InteractionsRestClient (api_client )
975+
976+
977+ def _agent_data_response_text (agent_data : types .evals .AgentData ) -> Optional [str ]:
978+ """Concatenates the text of all model-role events in an AgentData."""
979+ text_parts : list [str ] = []
980+ for turn in agent_data .turns or []:
981+ for event in turn .events or []:
982+ content = event .content
983+ if not content or content .role != _evals_constant .MODEL_AUTHOR :
984+ continue
985+ for part in content .parts or []:
986+ if part .text :
987+ text_parts .append (part .text )
988+ return "" .join (text_parts ) or None
989+
990+
991+ _INTERACTION_TERMINAL_STATES = frozenset (
992+ ["completed" , "failed" , "cancelled" , "incomplete" , "budget_exceeded" ]
993+ )
994+
995+
996+ def _await_interaction (
997+ interactions_client : "_InteractionsRestClient" ,
998+ interaction : dict [str , Any ],
999+ poll_interval_seconds : float = 2.0 ,
1000+ timeout_seconds : float = 600.0 ,
1001+ ) -> dict [str , Any ]:
1002+ """Polls a background interaction until it reaches a terminal state.
1003+
1004+ Gemini agent interactions must run in the background (`background=True`), so
1005+ `create` returns before the model output is ready. This polls
1006+ `interactions.get` until the interaction reaches a terminal state and then
1007+ returns the resolved interaction.
1008+
1009+ Args:
1010+ interactions_client: The interactions client used to poll.
1011+ interaction: The interaction returned by `create`.
1012+ poll_interval_seconds: Delay between poll attempts.
1013+ timeout_seconds: Maximum time to wait before raising.
1014+
1015+ Returns:
1016+ The resolved interaction once it reaches a terminal state.
1017+
1018+ Raises:
1019+ TimeoutError: If the interaction does not complete within the timeout.
1020+ """
1021+ if interaction .get ("status" ) in _INTERACTION_TERMINAL_STATES :
1022+ return interaction
1023+ interaction_id = interaction .get ("id" )
1024+ deadline = time .monotonic () + timeout_seconds
1025+ while time .monotonic () < deadline :
1026+ time .sleep (poll_interval_seconds )
1027+ interaction = interactions_client .get (interaction_id )
1028+ if interaction .get ("status" ) in _INTERACTION_TERMINAL_STATES :
1029+ return interaction
1030+ raise TimeoutError (
1031+ f"Interaction { interaction_id } did not complete within"
1032+ f" { timeout_seconds } seconds."
1033+ )
1034+
1035+
1036+ def _run_gemini_agent_inference (
1037+ * ,
1038+ api_client : BaseApiClient ,
1039+ gemini_agent : str ,
1040+ prompt_dataset : pd .DataFrame ,
1041+ ) -> pd .DataFrame :
1042+ """Runs inference against a Gemini Agents API agent via the Interactions API.
1043+
1044+ For each prompt row, creates an interaction against `gemini_agent` and
1045+ collects the interaction id, response text, and agent data.
1046+
1047+ Args:
1048+ api_client: The API client used to issue interaction calls.
1049+ gemini_agent: The Gemini Agents API agent resource name.
1050+ prompt_dataset: The prompt DataFrame. The prompt is read from the
1051+ `request` column if present, otherwise from the `prompt` column.
1052+
1053+ Returns:
1054+ A DataFrame with columns prompt, response, interaction_id, agent_data.
1055+ """
1056+ prompt_column = (
1057+ "request" if "request" in prompt_dataset .columns else _evals_constant .PROMPT
1058+ )
1059+ if prompt_column not in prompt_dataset .columns :
1060+ raise ValueError (
1061+ "The eval dataset provided for Gemini agent inference must contain a"
1062+ f" '{ _evals_constant .PROMPT } ' or 'request' column."
1063+ )
1064+
1065+ interactions_client = _get_interactions_client (api_client )
1066+
1067+ agent_short_id = gemini_agent .split ("/" )[- 1 ]
1068+ prompts : list [str ] = []
1069+ responses : list [Optional [str ]] = []
1070+ interaction_ids : list [Optional [str ]] = []
1071+ agent_data : list [dict [str , Any ]] = []
1072+ for prompt in tqdm (
1073+ prompt_dataset [prompt_column ].tolist (), desc = "Gemini Agent Inference"
1074+ ):
1075+ prompts .append (prompt )
1076+ try :
1077+ interaction = interactions_client .create (
1078+ {
1079+ "agent" : agent_short_id ,
1080+ "input" : [{"type" : "text" , "text" : prompt }],
1081+ "store" : True ,
1082+ "background" : True ,
1083+ }
1084+ )
1085+ interaction = _await_interaction (interactions_client , interaction )
1086+ agent_data_obj = _interaction_dict_to_agent_data (interaction )
1087+ _merge_text_parts_in_agent_data (agent_data_obj )
1088+ responses .append (_agent_data_response_text (agent_data_obj ))
1089+ interaction_ids .append (interaction .get ("id" ))
1090+ agent_data .append (agent_data_obj .model_dump (mode = "json" , exclude_none = True ))
1091+ except Exception as e : # pylint: disable=broad-exception-caught
1092+ logger .warning (
1093+ "Gemini agent inference failed for a prompt (recording an empty"
1094+ " row and continuing): %s" ,
1095+ e ,
1096+ )
1097+ responses .append (None )
1098+ interaction_ids .append (None )
1099+ agent_data .append ({})
1100+
1101+ return pd .DataFrame (
1102+ {
1103+ _evals_constant .PROMPT : prompts ,
1104+ _evals_constant .RESPONSE : responses ,
1105+ _evals_constant .INTERACTION_ID : interaction_ids ,
1106+ _evals_constant .AGENT_DATA : agent_data ,
1107+ }
1108+ )
1109+
1110+
9411111def _normalize_interaction_resource (
9421112 interaction : str , agent : str , location : Optional [str ]
9431113) -> str :
@@ -1999,6 +2169,7 @@ def _execute_inference(
19992169 model : Optional [Union [Callable [[Any ], Any ], str ]] = None ,
20002170 agent_engine : Optional [Union [str , types .AgentEngine ]] = None ,
20012171 agent : Optional ["LlmAgent" ] = None , # type: ignore # noqa: F821
2172+ gemini_agent : Optional [str ] = None ,
20022173 dest : Optional [str ] = None ,
20032174 config : Optional [genai_types .GenerateContentConfig ] = None ,
20042175 prompt_template : Optional [Union [str , types .PromptTemplateOrDict ]] = None ,
@@ -2017,6 +2188,8 @@ def _execute_inference(
20172188 agent_engine: The agent engine to use for inference. Can be a resource
20182189 name string or an `AgentEngine` instance.
20192190 agent: The local agent to use for inference. Can be an ADK agent instance.
2191+ gemini_agent: The Gemini Agents API agent resource name to run inference
2192+ against via the Interactions API.
20202193 dest: The destination to save the inference results. Can be a string
20212194 representing a file path or a GCS URI.
20222195 config: The generation configuration for the model.
@@ -2034,9 +2207,10 @@ def _execute_inference(
20342207 if location :
20352208 api_client = _get_api_client_with_location (api_client , location )
20362209
2037- if sum (x is not None for x in [model , agent_engine , agent ]) != 1 :
2210+ if sum (x is not None for x in [model , agent_engine , agent , gemini_agent ]) != 1 :
20382211 raise ValueError (
2039- "Exactly one of model, agent_engine, or agent must be provided."
2212+ "Exactly one of model, agent_engine, agent, or gemini_agent must be"
2213+ " provided."
20402214 )
20412215
20422216 prompt_dataset = _load_dataframe (api_client , src )
@@ -2049,7 +2223,24 @@ def _execute_inference(
20492223
20502224 _apply_prompt_template (prompt_dataset , prompt_template )
20512225
2052- if model :
2226+ if gemini_agent :
2227+ start_time = time .time ()
2228+ logger .debug ("Starting Gemini Agent inference process ..." )
2229+ results_df = _run_gemini_agent_inference (
2230+ api_client = api_client ,
2231+ gemini_agent = gemini_agent ,
2232+ prompt_dataset = prompt_dataset ,
2233+ )
2234+ end_time = time .time ()
2235+ logger .info (
2236+ "Gemini Agent inference completed in %.2f seconds." ,
2237+ end_time - start_time ,
2238+ )
2239+ return types .EvaluationDataset (
2240+ eval_dataset_df = results_df ,
2241+ candidate_name = gemini_agent .split ("/" )[- 1 ],
2242+ )
2243+ elif model :
20532244 start_time = time .time ()
20542245 logger .debug ("Starting inference process ..." )
20552246 results_df = _run_inference_internal (
0 commit comments