@@ -928,6 +928,217 @@ def _get_resolved_location(api_client: Any) -> Optional[str]:
928928 return getattr (api_client , "location" , None )
929929
930930
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+ def _agent_resource_to_agent_info (
982+ agent : str , api_client : BaseApiClient
983+ ) -> "types.evals.AgentInfo" :
984+ """Builds an `AgentInfo` from a Gemini Agents API agent resource name.
985+
986+ Fetches the agent through the SDK's `api_client` (so replay recording is
987+ preserved) via `_fetch_agent_config_dict` and derives a single-agent
988+ `AgentInfo`: the agent's short name is the agents-map key and
989+ `root_agent_id`.
990+
991+ Args:
992+ agent: The Gemini Agents API agent resource name
993+ (`projects/{p}/locations/{l}/agents/{name}`).
994+ api_client: The API client used to fetch the agent.
995+
996+ Returns:
997+ An `AgentInfo` describing the fetched agent.
998+ """
999+ agent_config = _fetch_agent_config_dict (api_client , agent )
1000+ short_name = agent_config .agent_id
1001+ return types .evals .AgentInfo ( # pytype: disable=missing-parameter
1002+ name = short_name ,
1003+ agents = {short_name : agent_config },
1004+ root_agent_id = short_name ,
1005+ )
1006+
1007+
1008+ _INTERACTION_TERMINAL_STATES = frozenset (
1009+ ["completed" , "failed" , "cancelled" , "incomplete" , "budget_exceeded" ]
1010+ )
1011+
1012+ _INITIAL_POLL_INTERVAL_SECONDS = 2.0
1013+ _MAX_POLL_INTERVAL_SECONDS = 30.0
1014+ _POLL_BACKOFF_MULTIPLIER = 2.0
1015+
1016+
1017+ def _await_interaction (
1018+ interactions_client : "_InteractionsRestClient" ,
1019+ interaction : dict [str , Any ],
1020+ initial_poll_interval_seconds : float = _INITIAL_POLL_INTERVAL_SECONDS ,
1021+ max_poll_interval_seconds : float = _MAX_POLL_INTERVAL_SECONDS ,
1022+ poll_backoff_multiplier : float = _POLL_BACKOFF_MULTIPLIER ,
1023+ timeout_seconds : float = 600.0 ,
1024+ ) -> dict [str , Any ]:
1025+ """Polls a background interaction until it reaches a terminal state.
1026+
1027+ Gemini agent interactions must run in the background (`background=True`), so
1028+ `create` returns before the model output is ready. This polls
1029+ `interactions.get` until the interaction reaches a terminal state and then
1030+ returns the resolved interaction. The delay between polls grows
1031+ exponentially (capped at `max_poll_interval_seconds`) to avoid hitting rate
1032+ limits when evaluating large datasets.
1033+
1034+ Args:
1035+ interactions_client: The interactions client used to poll.
1036+ interaction: The interaction returned by `create`.
1037+ initial_poll_interval_seconds: Delay before the first poll.
1038+ max_poll_interval_seconds: Upper bound for the poll interval.
1039+ poll_backoff_multiplier: Factor the interval grows by after each poll.
1040+ timeout_seconds: Maximum time to wait before raising.
1041+
1042+ Returns:
1043+ The resolved interaction once it reaches a terminal state.
1044+
1045+ Raises:
1046+ TimeoutError: If the interaction does not complete within the timeout.
1047+ """
1048+ if interaction .get ("status" ) in _INTERACTION_TERMINAL_STATES :
1049+ return interaction
1050+ interaction_id = interaction .get ("id" )
1051+ deadline = time .monotonic () + timeout_seconds
1052+ poll_interval = initial_poll_interval_seconds
1053+ while time .monotonic () < deadline :
1054+ time .sleep (poll_interval )
1055+ interaction = interactions_client .get (interaction_id )
1056+ if interaction .get ("status" ) in _INTERACTION_TERMINAL_STATES :
1057+ return interaction
1058+ poll_interval = min (
1059+ poll_interval * poll_backoff_multiplier , max_poll_interval_seconds
1060+ )
1061+ raise TimeoutError (
1062+ f"Interaction { interaction_id } did not complete within"
1063+ f" { timeout_seconds } seconds."
1064+ )
1065+
1066+
1067+ def _run_gemini_agent_inference (
1068+ * ,
1069+ api_client : BaseApiClient ,
1070+ gemini_agent : str ,
1071+ prompt_dataset : pd .DataFrame ,
1072+ ) -> pd .DataFrame :
1073+ """Runs inference against a Gemini Agents API agent via the Interactions API.
1074+
1075+ For each prompt row, creates an interaction against `gemini_agent` and
1076+ collects the interaction id, response text, and agent data.
1077+
1078+ Args:
1079+ api_client: The API client used to issue interaction calls.
1080+ gemini_agent: The Gemini Agents API agent resource name.
1081+ prompt_dataset: The prompt DataFrame. The prompt is read from the
1082+ `request` column if present, otherwise from the `prompt` column.
1083+
1084+ Returns:
1085+ A DataFrame with columns prompt, response, interaction_id, agent_data.
1086+ """
1087+ prompt_column = (
1088+ "request" if "request" in prompt_dataset .columns else _evals_constant .PROMPT
1089+ )
1090+ if prompt_column not in prompt_dataset .columns :
1091+ raise ValueError (
1092+ "The eval dataset provided for Gemini agent inference must contain a"
1093+ f" '{ _evals_constant .PROMPT } ' or 'request' column."
1094+ )
1095+
1096+ interactions_client = _get_interactions_client (api_client )
1097+
1098+ agent_short_id = gemini_agent .split ("/" )[- 1 ]
1099+ prompts : list [str ] = []
1100+ responses : list [Optional [str ]] = []
1101+ interaction_ids : list [Optional [str ]] = []
1102+ agent_data : list [dict [str , Any ]] = []
1103+ for prompt in tqdm (
1104+ prompt_dataset [prompt_column ].tolist (), desc = "Gemini Agent Inference"
1105+ ):
1106+ prompts .append (prompt )
1107+ try :
1108+ interaction = interactions_client .create (
1109+ {
1110+ "agent" : agent_short_id ,
1111+ "input" : [{"type" : "text" , "text" : prompt }],
1112+ "store" : True ,
1113+ "background" : True ,
1114+ }
1115+ )
1116+ interaction = _await_interaction (interactions_client , interaction )
1117+ agent_data_obj = _interaction_dict_to_agent_data (interaction )
1118+ _merge_text_parts_in_agent_data (agent_data_obj )
1119+ responses .append (_agent_data_response_text (agent_data_obj ))
1120+ interaction_ids .append (interaction .get ("id" ))
1121+ agent_data .append (agent_data_obj .model_dump (mode = "json" , exclude_none = True ))
1122+ except Exception as e : # pylint: disable=broad-exception-caught
1123+ logger .warning (
1124+ "Gemini agent inference failed for a prompt (recording an empty"
1125+ " row and continuing): %s" ,
1126+ e ,
1127+ )
1128+ responses .append (None )
1129+ interaction_ids .append (None )
1130+ agent_data .append ({})
1131+
1132+ return pd .DataFrame (
1133+ {
1134+ _evals_constant .PROMPT : prompts ,
1135+ _evals_constant .RESPONSE : responses ,
1136+ _evals_constant .INTERACTION_ID : interaction_ids ,
1137+ _evals_constant .AGENT_DATA : agent_data ,
1138+ }
1139+ )
1140+
1141+
9311142def _normalize_interaction_resource (
9321143 interaction : str , agent : str , location : Optional [str ]
9331144) -> str :
@@ -1838,6 +2049,7 @@ def _execute_inference(
18382049 model : Optional [Union [Callable [[Any ], Any ], str ]] = None ,
18392050 agent_engine : Optional [Union [str , types .AgentEngine ]] = None ,
18402051 agent : Optional ["LlmAgent" ] = None , # type: ignore # noqa: F821
2052+ gemini_agent : Optional [str ] = None ,
18412053 dest : Optional [str ] = None ,
18422054 config : Optional [genai_types .GenerateContentConfig ] = None ,
18432055 prompt_template : Optional [Union [str , types .PromptTemplateOrDict ]] = None ,
@@ -1856,6 +2068,8 @@ def _execute_inference(
18562068 agent_engine: The agent engine to use for inference. Can be a resource
18572069 name string or an `AgentEngine` instance.
18582070 agent: The local agent to use for inference. Can be an ADK agent instance.
2071+ gemini_agent: The Gemini Agents API agent resource name to run inference
2072+ against via the Interactions API.
18592073 dest: The destination to save the inference results. Can be a string
18602074 representing a file path or a GCS URI.
18612075 config: The generation configuration for the model.
@@ -1873,9 +2087,10 @@ def _execute_inference(
18732087 if location :
18742088 api_client = _get_api_client_with_location (api_client , location )
18752089
1876- if sum (x is not None for x in [model , agent_engine , agent ]) != 1 :
2090+ if sum (x is not None for x in [model , agent_engine , agent , gemini_agent ]) != 1 :
18772091 raise ValueError (
1878- "Exactly one of model, agent_engine, or agent must be provided."
2092+ "Exactly one of model, agent_engine, agent, or gemini_agent must be"
2093+ " provided."
18792094 )
18802095
18812096 prompt_dataset = _load_dataframe (api_client , src )
@@ -1888,7 +2103,24 @@ def _execute_inference(
18882103
18892104 _apply_prompt_template (prompt_dataset , prompt_template )
18902105
1891- if model :
2106+ if gemini_agent :
2107+ start_time = time .time ()
2108+ logger .debug ("Starting Gemini Agent inference process ..." )
2109+ results_df = _run_gemini_agent_inference (
2110+ api_client = api_client ,
2111+ gemini_agent = gemini_agent ,
2112+ prompt_dataset = prompt_dataset ,
2113+ )
2114+ end_time = time .time ()
2115+ logger .info (
2116+ "Gemini Agent inference completed in %.2f seconds." ,
2117+ end_time - start_time ,
2118+ )
2119+ return types .EvaluationDataset (
2120+ eval_dataset_df = results_df ,
2121+ candidate_name = gemini_agent .split ("/" )[- 1 ],
2122+ )
2123+ elif model :
18922124 start_time = time .time ()
18932125 logger .debug ("Starting inference process ..." )
18942126 results_df = _run_inference_internal (
0 commit comments