@@ -928,6 +928,176 @@ 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+ _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+
9311101def _normalize_interaction_resource (
9321102 interaction : str , agent : str , location : Optional [str ]
9331103) -> str :
@@ -1838,6 +2008,7 @@ def _execute_inference(
18382008 model : Optional [Union [Callable [[Any ], Any ], str ]] = None ,
18392009 agent_engine : Optional [Union [str , types .AgentEngine ]] = None ,
18402010 agent : Optional ["LlmAgent" ] = None , # type: ignore # noqa: F821
2011+ gemini_agent : Optional [str ] = None ,
18412012 dest : Optional [str ] = None ,
18422013 config : Optional [genai_types .GenerateContentConfig ] = None ,
18432014 prompt_template : Optional [Union [str , types .PromptTemplateOrDict ]] = None ,
@@ -1856,6 +2027,8 @@ def _execute_inference(
18562027 agent_engine: The agent engine to use for inference. Can be a resource
18572028 name string or an `AgentEngine` instance.
18582029 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.
18592032 dest: The destination to save the inference results. Can be a string
18602033 representing a file path or a GCS URI.
18612034 config: The generation configuration for the model.
@@ -1873,9 +2046,10 @@ def _execute_inference(
18732046 if location :
18742047 api_client = _get_api_client_with_location (api_client , location )
18752048
1876- 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 :
18772050 raise ValueError (
1878- "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."
18792053 )
18802054
18812055 prompt_dataset = _load_dataframe (api_client , src )
@@ -1888,7 +2062,24 @@ def _execute_inference(
18882062
18892063 _apply_prompt_template (prompt_dataset , prompt_template )
18902064
1891- 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 :
18922083 start_time = time .time ()
18932084 logger .debug ("Starting inference process ..." )
18942085 results_df = _run_inference_internal (
0 commit comments