3232import agentplatform
3333from google .genai import types as genai_types
3434from google .genai ._api_client import BaseApiClient
35+ from google .genai ._gaos .google_genai import GeminiNextGenInteractions
3536from google .genai .models import Models
3637import pandas as pd
3738from tqdm import tqdm
@@ -575,6 +576,269 @@ def _is_gemini_agent_resource(agent: str) -> bool:
575576 )
576577
577578
579+ def _get_resolved_location (api_client : Any ) -> Optional [str ]:
580+ """Returns the location configured on the API client."""
581+ return getattr (api_client , "location" , None )
582+
583+
584+ def _get_interactions_client (api_client : BaseApiClient ) -> Any :
585+ """Returns a google.genai interactions client bound to `api_client`.
586+
587+ The interactions client is constructed around the SDK's existing
588+ `api_client` (a `BaseApiClient`, or a `ReplayApiClient` in tests) so that
589+ replay recording captures the interaction calls. This mirrors how
590+ `google.genai.client.Client.interactions` is built internally.
591+
592+ Args:
593+ api_client: The API client to bind the interactions client to.
594+
595+ Returns:
596+ A google.genai interactions client.
597+ """
598+ return GeminiNextGenInteractions (api_client )
599+
600+
601+ def _interaction_to_agent_data (
602+ gemini_agent : str , prompt : str , interaction : Any
603+ ) -> dict [str , Any ]:
604+ """Maps a Gemini Agents interaction into the canonical AgentData schema.
605+
606+ Populates the AgentConfig for the agent plus a single ConversationTurn with
607+ the user input event and the final model output event. The interaction's
608+ full step trajectory is not expanded event-by-event; the response text is
609+ used as the model output content.
610+
611+ Args:
612+ gemini_agent: The Gemini Agents API agent resource name.
613+ prompt: The user prompt for the interaction.
614+ interaction: The interaction returned by the Interactions API.
615+
616+ Returns:
617+ A dict representation of the AgentData for the interaction.
618+ """
619+ agent_id = gemini_agent .split ("/" )[- 1 ]
620+ agents = {
621+ agent_id : types .evals .AgentConfig ( # pytype: disable=missing-parameter
622+ agent_id = agent_id ,
623+ )
624+ }
625+ events = [
626+ types .evals .AgentEvent (
627+ author = _evals_constant .USER_AUTHOR ,
628+ content = genai_types .Content (
629+ role = _evals_constant .USER_AUTHOR ,
630+ parts = [genai_types .Part (text = prompt )],
631+ ),
632+ )
633+ ]
634+ output_text = getattr (interaction , "output_text" , None )
635+ if output_text :
636+ events .append (
637+ types .evals .AgentEvent (
638+ author = agent_id ,
639+ content = genai_types .Content (
640+ role = _evals_constant .MODEL_AUTHOR ,
641+ parts = [genai_types .Part (text = output_text )],
642+ ),
643+ )
644+ )
645+ turns = [
646+ types .evals .ConversationTurn (
647+ turn_index = 0 ,
648+ turn_id = getattr (interaction , "id" , None ) or "turn_0" ,
649+ events = events ,
650+ )
651+ ]
652+ return types .evals .AgentData (
653+ agents = agents ,
654+ turns = turns ,
655+ ).model_dump (mode = "json" , exclude_none = True )
656+
657+
658+ _INTERACTION_TERMINAL_STATES = frozenset (
659+ ["completed" , "failed" , "cancelled" , "incomplete" , "budget_exceeded" ]
660+ )
661+
662+
663+ def _await_interaction (
664+ interactions_client : Any ,
665+ interaction : Any ,
666+ poll_interval_seconds : float = 2.0 ,
667+ timeout_seconds : float = 600.0 ,
668+ ) -> Any :
669+ """Polls a background interaction until it reaches a terminal state.
670+
671+ Gemini agent interactions must run in the background (`background=True`), so
672+ `create` returns before the model output is ready. This polls
673+ `interactions.get` until the interaction reaches a terminal state and then
674+ returns the resolved interaction.
675+
676+ Args:
677+ interactions_client: The interactions client used to poll.
678+ interaction: The interaction returned by `create`.
679+ poll_interval_seconds: Delay between poll attempts.
680+ timeout_seconds: Maximum time to wait before raising.
681+
682+ Returns:
683+ The resolved interaction once it reaches a terminal state.
684+
685+ Raises:
686+ TimeoutError: If the interaction does not complete within the timeout.
687+ """
688+ if getattr (interaction , "status" , None ) in _INTERACTION_TERMINAL_STATES :
689+ return interaction
690+ interaction_id = getattr (interaction , "id" , None )
691+ deadline = time .monotonic () + timeout_seconds
692+ while time .monotonic () < deadline :
693+ time .sleep (poll_interval_seconds )
694+ interaction = interactions_client .get (interaction_id )
695+ if getattr (interaction , "status" , None ) in _INTERACTION_TERMINAL_STATES :
696+ return interaction
697+ raise TimeoutError (
698+ f"Interaction { interaction_id } did not complete within"
699+ f" { timeout_seconds } seconds."
700+ )
701+
702+
703+ def _run_gemini_agent_inference (
704+ * ,
705+ api_client : BaseApiClient ,
706+ gemini_agent : str ,
707+ prompt_dataset : pd .DataFrame ,
708+ ) -> pd .DataFrame :
709+ """Runs inference against a Gemini Agents API agent via the Interactions API.
710+
711+ For each prompt row, creates an interaction against `gemini_agent` and
712+ collects the interaction id, response text, and agent data.
713+
714+ Args:
715+ api_client: The API client used to issue interaction calls.
716+ gemini_agent: The Gemini Agents API agent resource name.
717+ prompt_dataset: The prompt DataFrame. The prompt is read from the
718+ `request` column if present, otherwise from the `prompt` column.
719+
720+ Returns:
721+ A DataFrame with columns prompt, response, interaction_id, agent_data.
722+ """
723+ prompt_column = (
724+ "request" if "request" in prompt_dataset .columns else _evals_constant .PROMPT
725+ )
726+ if prompt_column not in prompt_dataset .columns :
727+ raise ValueError (
728+ "The eval dataset provided for Gemini agent inference must contain a"
729+ f" '{ _evals_constant .PROMPT } ' or 'request' column."
730+ )
731+
732+ interactions_client = _get_interactions_client (api_client )
733+
734+ prompts : list [str ] = []
735+ responses : list [Optional [str ]] = []
736+ interaction_ids : list [Optional [str ]] = []
737+ agent_data : list [dict [str , Any ]] = []
738+ for prompt in tqdm (
739+ prompt_dataset [prompt_column ].tolist (), desc = "Gemini Agent Inference"
740+ ):
741+ interaction = interactions_client .create (
742+ agent = gemini_agent ,
743+ input = prompt ,
744+ store = True ,
745+ background = True ,
746+ )
747+ interaction = _await_interaction (interactions_client , interaction )
748+ prompts .append (prompt )
749+ responses .append (getattr (interaction , "output_text" , None ))
750+ interaction_ids .append (getattr (interaction , "id" , None ))
751+ agent_data .append (_interaction_to_agent_data (gemini_agent , prompt , interaction ))
752+
753+ return pd .DataFrame (
754+ {
755+ _evals_constant .PROMPT : prompts ,
756+ _evals_constant .RESPONSE : responses ,
757+ _evals_constant .INTERACTION_ID : interaction_ids ,
758+ _evals_constant .AGENT_DATA : agent_data ,
759+ }
760+ )
761+
762+
763+ def _normalize_interaction_resource (
764+ interaction : str , agent : str , location : Optional [str ]
765+ ) -> str :
766+ """Normalizes an interaction id into a full resource name.
767+
768+ A bare interaction id is expanded to
769+ `projects/{project}/locations/{location}/interactions/{id}` using the
770+ project and location parsed from the agent resource name. Fully-qualified
771+ interaction resource names are returned unchanged.
772+ """
773+ if interaction .startswith ("projects/" ):
774+ return interaction
775+ parts = agent .split ("/" )
776+ project = parts [1 ]
777+ agent_location = parts [3 ] if len (parts ) > 3 else (location or "global" )
778+ return f"projects/{ project } /locations/{ agent_location } /interactions/{ interaction } "
779+
780+
781+ def _build_interaction_id_dataset (
782+ loaded_data : list [dict [str , Any ]],
783+ agent : Optional [str ],
784+ location : Optional [str ],
785+ ) -> Optional [types .EvaluationDataset ]:
786+ """Builds an EvaluationDataset from rows that carry an `interaction_id`.
787+
788+ When the dataset contains an `interaction_id` column, each row is turned
789+ into an EvalCase whose `interactions_data_source` references the interaction
790+ and the Gemini agent. The backend resolves the interaction trace and agent
791+ config; no client-side prompt/response is required. Returns None if the
792+ data does not contain interaction ids.
793+ """
794+ has_interaction_id = bool (loaded_data ) and any (
795+ _evals_constant .INTERACTION_ID in row for row in loaded_data
796+ )
797+ if not has_interaction_id :
798+ if agent :
799+ raise ValueError (
800+ "An `agent` was provided but the dataset does not contain an"
801+ " `interaction_id` column. The `agent` argument is only used to"
802+ " resolve an `interaction_id` dataset column (so the backend can"
803+ " fetch the interaction trace and Agent config). To evaluate"
804+ " with an agent, provide a dataset with an `interaction_id`"
805+ " column; otherwise omit `agent`."
806+ )
807+ return None
808+
809+ if not agent :
810+ raise ValueError (
811+ "An `agent` resource name is required when the dataset contains an"
812+ " `interaction_id` column, so the backend can resolve the Agent"
813+ " config for each interaction."
814+ )
815+ if not _is_gemini_agent_resource (agent ):
816+ raise ValueError (
817+ "`agent` must be a Gemini Agents API resource name of the form"
818+ " projects/{project}/locations/{location}/agents/{agent} when"
819+ " evaluating interaction ids. Got: %s" % agent
820+ )
821+
822+ gemini_agent_config = types .GeminiAgentConfig (gemini_agent = agent )
823+ eval_cases = []
824+ for i , row in enumerate (loaded_data ):
825+ interaction = row .get (_evals_constant .INTERACTION_ID )
826+ if not interaction :
827+ raise ValueError ("Missing `interaction_id` value for row %d." % i )
828+ eval_cases .append (
829+ types .EvalCase (
830+ eval_case_id = "eval_case_%s" % i ,
831+ interactions_data_source = types .InteractionsDataSource (
832+ interaction = _normalize_interaction_resource (
833+ str (interaction ), agent , location
834+ ),
835+ gemini_agent_config = gemini_agent_config ,
836+ ),
837+ )
838+ )
839+ return types .EvaluationDataset (eval_cases = eval_cases )
840+
841+
578842def _add_evaluation_run_labels (
579843 labels : Optional [dict [str , str ]] = None ,
580844 agent : Optional [str ] = None ,
@@ -1406,6 +1670,7 @@ def _execute_inference(
14061670 model : Optional [Union [Callable [[Any ], Any ], str ]] = None ,
14071671 agent_engine : Optional [Union [str , types .AgentEngine ]] = None ,
14081672 agent : Optional ["LlmAgent" ] = None , # type: ignore # noqa: F821
1673+ gemini_agent : Optional [str ] = None ,
14091674 dest : Optional [str ] = None ,
14101675 config : Optional [genai_types .GenerateContentConfig ] = None ,
14111676 prompt_template : Optional [Union [str , types .PromptTemplateOrDict ]] = None ,
@@ -1424,6 +1689,8 @@ def _execute_inference(
14241689 agent_engine: The agent engine to use for inference. Can be a resource
14251690 name string or an `AgentEngine` instance.
14261691 agent: The local agent to use for inference. Can be an ADK agent instance.
1692+ gemini_agent: The Gemini Agents API agent resource name to run inference
1693+ against via the Interactions API.
14271694 dest: The destination to save the inference results. Can be a string
14281695 representing a file path or a GCS URI.
14291696 config: The generation configuration for the model.
@@ -1441,9 +1708,10 @@ def _execute_inference(
14411708 if location :
14421709 api_client = _get_api_client_with_location (api_client , location )
14431710
1444- if sum (x is not None for x in [model , agent_engine , agent ]) != 1 :
1711+ if sum (x is not None for x in [model , agent_engine , agent , gemini_agent ]) != 1 :
14451712 raise ValueError (
1446- "Exactly one of model, agent_engine, or agent must be provided."
1713+ "Exactly one of model, agent_engine, agent, or gemini_agent must be"
1714+ " provided."
14471715 )
14481716
14491717 prompt_dataset = _load_dataframe (api_client , src )
@@ -1456,7 +1724,24 @@ def _execute_inference(
14561724
14571725 _apply_prompt_template (prompt_dataset , prompt_template )
14581726
1459- if model :
1727+ if gemini_agent :
1728+ start_time = time .time ()
1729+ logger .debug ("Starting Gemini Agent inference process ..." )
1730+ results_df = _run_gemini_agent_inference (
1731+ api_client = api_client ,
1732+ gemini_agent = gemini_agent ,
1733+ prompt_dataset = prompt_dataset ,
1734+ )
1735+ end_time = time .time ()
1736+ logger .info (
1737+ "Gemini Agent inference completed in %.2f seconds." ,
1738+ end_time - start_time ,
1739+ )
1740+ return types .EvaluationDataset (
1741+ eval_dataset_df = results_df ,
1742+ candidate_name = gemini_agent .split ("/" )[- 1 ],
1743+ )
1744+ elif model :
14601745 start_time = time .time ()
14611746 logger .debug ("Starting inference process ..." )
14621747 results_df = _run_inference_internal (
@@ -1591,6 +1876,8 @@ def _resolve_dataset_inputs(
15911876 dataset_schema : Optional [Literal ["GEMINI" , "FLATTEN" , "OPENAI" ]],
15921877 loader : "_evals_utils.EvalDatasetLoader" ,
15931878 agent_info : Optional [types .evals .AgentInfo ] = None ,
1879+ agent : Optional [str ] = None ,
1880+ api_client : Any = None ,
15941881) -> tuple [types .EvaluationDataset , int ]:
15951882 """Loads and processes single or multiple datasets for evaluation.
15961883
@@ -1640,6 +1927,13 @@ def _resolve_dataset_inputs(
16401927 ds_source_for_loader = _get_dataset_source (ds_item )
16411928 current_loaded_data = loader .load (ds_source_for_loader )
16421929
1930+ interaction_dataset = _build_interaction_id_dataset (
1931+ current_loaded_data , agent , _get_resolved_location (api_client )
1932+ )
1933+ if interaction_dataset is not None :
1934+ parsed_evaluation_datasets .append (interaction_dataset )
1935+ continue
1936+
16431937 if dataset_schema :
16441938 current_schema = _evals_data_converters .EvalDatasetSchema (dataset_schema )
16451939 else :
@@ -1797,6 +2091,7 @@ def _execute_evaluation( # type: ignore[no-untyped-def]
17972091 api_client : Any ,
17982092 dataset : Union [types .EvaluationDataset , list [types .EvaluationDataset ]],
17992093 metrics : list [types .Metric ],
2094+ agent : Optional [str ] = None ,
18002095 dataset_schema : Optional [Literal ["GEMINI" , "FLATTEN" , "OPENAI" ]] = None ,
18012096 dest : Optional [str ] = None ,
18022097 location : Optional [str ] = None ,
@@ -1877,6 +2172,8 @@ def _execute_evaluation( # type: ignore[no-untyped-def]
18772172 dataset_schema = dataset_schema ,
18782173 loader = loader ,
18792174 agent_info = validated_agent_info ,
2175+ agent = agent ,
2176+ api_client = api_client ,
18802177 )
18812178
18822179 resolved_metrics = _resolve_metrics (metrics , api_client )
0 commit comments