Skip to content

Commit 4007f3c

Browse files
jsondaicopybara-github
authored andcommitted
feat: GenAI Client(evals) - generate_conversation_scenarios support for Gemini Agents API agents
PiperOrigin-RevId: 945262437
1 parent 852c546 commit 4007f3c

4 files changed

Lines changed: 179 additions & 14 deletions

File tree

agentplatform/_genai/_evals_common.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -988,6 +988,33 @@ def _agent_data_response_text(agent_data: types.evals.AgentData) -> Optional[str
988988
return "".join(text_parts) or None
989989

990990

991+
def _agent_resource_to_agent_info(
992+
agent: str, api_client: BaseApiClient
993+
) -> "types.evals.AgentInfo":
994+
"""Builds an `AgentInfo` from a Gemini Agents API agent resource name.
995+
996+
Fetches the agent through the SDK's `api_client` (so replay recording is
997+
preserved) via `_fetch_agent_config_dict` and derives a single-agent
998+
`AgentInfo`: the agent's short name is the agents-map key and
999+
`root_agent_id`.
1000+
1001+
Args:
1002+
agent: The Gemini Agents API agent resource name
1003+
(`projects/{p}/locations/{l}/agents/{name}`).
1004+
api_client: The API client used to fetch the agent.
1005+
1006+
Returns:
1007+
An `AgentInfo` describing the fetched agent.
1008+
"""
1009+
agent_config = _fetch_agent_config_dict(api_client, agent)
1010+
short_name = agent_config.agent_id
1011+
return types.evals.AgentInfo( # pytype: disable=missing-parameter
1012+
name=short_name,
1013+
agents={short_name: agent_config},
1014+
root_agent_id=short_name,
1015+
)
1016+
1017+
9911018
_INTERACTION_TERMINAL_STATES = frozenset(
9921019
["completed", "failed", "cancelled", "incomplete", "budget_exceeded"]
9931020
)

agentplatform/_genai/evals.py

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2956,28 +2956,54 @@ def create_evaluation_set(
29562956
def generate_conversation_scenarios(
29572957
self,
29582958
*,
2959-
agent_info: evals_types.AgentInfoOrDict,
2959+
agent_info: Optional[evals_types.AgentInfoOrDict] = None,
2960+
agent: Optional[str] = None,
29602961
config: evals_types.UserScenarioGenerationConfigOrDict,
29612962
allow_cross_region_model: Optional[bool] = None,
29622963
) -> types.EvaluationDataset:
29632964
"""Generates an evaluation dataset with user scenarios,
29642965
which helps to generate conversations between a simulated user
29652966
and the agent under test.
29662967
2968+
Exactly one of `agent_info` or `agent` must be provided. When `agent` is
2969+
a Gemini Agents API agent resource name, the agent is fetched and an
2970+
`AgentInfo` is derived from it.
2971+
29672972
Args:
2968-
agent_info: The agent info to generate user scenarios for.
2973+
agent_info: The agent info to generate user scenarios for. Mutually
2974+
exclusive with `agent`.
2975+
agent: A Gemini Agents API agent resource name
2976+
(`projects/{p}/locations/{l}/agents/{name}`). When provided, the
2977+
agent is fetched and its configuration is used to build the agent
2978+
info. Mutually exclusive with `agent_info`.
29692979
config: Configuration for generating user scenarios.
29702980
allow_cross_region_model: Opt-in flag to authorize cross-region
29712981
routing for model inference.
29722982
29732983
Returns:
29742984
An EvaluationDataset containing the generated user scenarios.
29752985
"""
2976-
parsed_agent_info = (
2977-
evals_types.AgentInfo.model_validate(agent_info)
2978-
if isinstance(agent_info, dict)
2979-
else agent_info
2980-
)
2986+
if agent is not None and agent_info is not None:
2987+
raise ValueError(
2988+
"Only one of `agent` or `agent_info` may be provided, not both."
2989+
)
2990+
if agent is None and agent_info is None:
2991+
raise ValueError("One of `agent` or `agent_info` must be provided.")
2992+
if agent is not None:
2993+
if not _evals_common._is_gemini_agent_resource(agent):
2994+
raise ValueError(
2995+
"`agent` must be a Gemini Agents API agent resource name of the"
2996+
" form projects/{project}/locations/{location}/agents/{agent}."
2997+
)
2998+
parsed_agent_info = _evals_common._agent_resource_to_agent_info(
2999+
agent, self._api_client
3000+
)
3001+
else:
3002+
parsed_agent_info = (
3003+
evals_types.AgentInfo.model_validate(agent_info)
3004+
if isinstance(agent_info, dict)
3005+
else agent_info
3006+
)
29813007
response = self._generate_user_scenarios(
29823008
agents=parsed_agent_info.agents,
29833009
root_agent_id=parsed_agent_info.root_agent_id,
@@ -4769,28 +4795,54 @@ async def create_evaluation_set(
47694795
async def generate_conversation_scenarios(
47704796
self,
47714797
*,
4772-
agent_info: evals_types.AgentInfoOrDict,
4798+
agent_info: Optional[evals_types.AgentInfoOrDict] = None,
4799+
agent: Optional[str] = None,
47734800
config: evals_types.UserScenarioGenerationConfigOrDict,
47744801
allow_cross_region_model: Optional[bool] = None,
47754802
) -> types.EvaluationDataset:
47764803
"""Generates an evaluation dataset with user scenarios,
47774804
which helps to generate conversations between a simulated user
47784805
and the agent under test.
47794806
4807+
Exactly one of `agent_info` or `agent` must be provided. When `agent` is
4808+
a Gemini Agents API agent resource name, the agent is fetched and an
4809+
`AgentInfo` is derived from it.
4810+
47804811
Args:
4781-
agent_info: The agent info to generate user scenarios for.
4812+
agent_info: The agent info to generate user scenarios for. Mutually
4813+
exclusive with `agent`.
4814+
agent: A Gemini Agents API agent resource name
4815+
(`projects/{p}/locations/{l}/agents/{name}`). When provided, the
4816+
agent is fetched and its configuration is used to build the agent
4817+
info. Mutually exclusive with `agent_info`.
47824818
config: Configuration for generating user scenarios.
47834819
allow_cross_region_model: Opt-in flag to authorize cross-region
47844820
routing for model inference.
47854821
47864822
Returns:
47874823
An EvaluationDataset containing the generated user scenarios.
47884824
"""
4789-
parsed_agent_info = (
4790-
evals_types.AgentInfo.model_validate(agent_info)
4791-
if isinstance(agent_info, dict)
4792-
else agent_info
4793-
)
4825+
if agent is not None and agent_info is not None:
4826+
raise ValueError(
4827+
"Only one of `agent` or `agent_info` may be provided, not both."
4828+
)
4829+
if agent is None and agent_info is None:
4830+
raise ValueError("One of `agent` or `agent_info` must be provided.")
4831+
if agent is not None:
4832+
if not _evals_common._is_gemini_agent_resource(agent):
4833+
raise ValueError(
4834+
"`agent` must be a Gemini Agents API agent resource name of the"
4835+
" form projects/{project}/locations/{location}/agents/{agent}."
4836+
)
4837+
parsed_agent_info = _evals_common._agent_resource_to_agent_info(
4838+
agent, self._api_client
4839+
)
4840+
else:
4841+
parsed_agent_info = (
4842+
evals_types.AgentInfo.model_validate(agent_info)
4843+
if isinstance(agent_info, dict)
4844+
else agent_info
4845+
)
47944846
response = await self._generate_user_scenarios(
47954847
agents=parsed_agent_info.agents,
47964848
root_agent_id=parsed_agent_info.root_agent_id,

tests/unit/agentplatform/genai/replays/test_generate_conversation_scenarios.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,30 @@ async def test_gen_conversation_scenarios_async(client):
103103
assert eval_dataset.eval_cases[1].user_scenario.conversation_plan
104104

105105

106+
def test_scenarios_from_gemini_agent(client):
107+
"""Tests generate_conversation_scenarios() against a Gemini Agents API agent.
108+
109+
Fetches the agent via google.genai agents.get, derives an AgentInfo from it,
110+
and generates user scenarios from the derived config.
111+
"""
112+
eval_dataset = client.evals.generate_conversation_scenarios(
113+
agent=("projects/model-evaluation-dev/locations/global/agents/test-agent-eval"),
114+
config=types.evals.UserScenarioGenerationConfig(
115+
count=2,
116+
generation_instruction=(
117+
"Generate scenarios where the user tries to book a flight but"
118+
" changes their mind about the destination."
119+
),
120+
environment_context="Today is Monday. Flights to Paris are available.",
121+
model_name="gemini-2.5-flash",
122+
),
123+
)
124+
assert isinstance(eval_dataset, types.EvaluationDataset)
125+
assert len(eval_dataset.eval_cases) == 2
126+
assert eval_dataset.eval_cases[0].user_scenario.starting_prompt
127+
assert eval_dataset.eval_cases[0].user_scenario.conversation_plan
128+
129+
106130
pytestmark = pytest_helper.setup(
107131
file=__file__,
108132
globals_for_file=globals(),

tests/unit/agentplatform/genai/test_evals.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9133,6 +9133,68 @@ async def test_async_generate_conversation_scenarios(self):
91339133
request_body = call_args[0][2] # Third positional arg is the request dict
91349134
assert request_body.get("allowCrossRegionModel") is True
91359135

9136+
@mock.patch.object(_evals_common, "_fetch_agent_config_dict")
9137+
def test_generate_conversation_scenarios_from_gemini_agent(
9138+
self, mock_fetch_agent_config
9139+
):
9140+
mock_fetch_agent_config.return_value = (
9141+
agentplatform_genai_types.evals.AgentConfig(
9142+
agent_id="test-agent",
9143+
instruction="You are a helpful travel assistant.",
9144+
description="An agent that books flights.",
9145+
tools=[genai_types.Tool(google_search=genai_types.GoogleSearch())],
9146+
)
9147+
)
9148+
9149+
evals_module = evals.Evals(api_client_=self.mock_api_client)
9150+
9151+
with mock.patch.object(
9152+
evals_module, "_generate_user_scenarios"
9153+
) as mock_generate_user_scenarios:
9154+
mock_generate_user_scenarios.return_value = self.mock_response
9155+
evals_module.generate_conversation_scenarios(
9156+
agent=_TEST_GEMINI_AGENT,
9157+
config={"count": 2},
9158+
)
9159+
9160+
mock_fetch_agent_config.assert_called_once_with(
9161+
self.mock_api_client, _TEST_GEMINI_AGENT
9162+
)
9163+
call_kwargs = mock_generate_user_scenarios.call_args.kwargs
9164+
assert call_kwargs["root_agent_id"] == "test-agent"
9165+
agents = call_kwargs["agents"]
9166+
assert "test-agent" in agents
9167+
derived_config = agents["test-agent"]
9168+
assert derived_config.instruction == "You are a helpful travel assistant."
9169+
assert derived_config.description == "An agent that books flights."
9170+
assert derived_config.tools is not None
9171+
assert derived_config.tools[0].google_search is not None
9172+
9173+
def test_generate_conversation_scenarios_agent_and_agent_info_raises(self):
9174+
evals_module = evals.Evals(api_client_=self.mock_api_client)
9175+
with pytest.raises(ValueError, match="not both"):
9176+
evals_module.generate_conversation_scenarios(
9177+
agent=_TEST_GEMINI_AGENT,
9178+
agent_info=agentplatform_genai_types.evals.AgentInfo(
9179+
agents={"agent_1": {}},
9180+
root_agent_id="agent_1",
9181+
),
9182+
config={"count": 2},
9183+
)
9184+
9185+
def test_generate_conversation_scenarios_no_agent_raises(self):
9186+
evals_module = evals.Evals(api_client_=self.mock_api_client)
9187+
with pytest.raises(ValueError, match="must be provided"):
9188+
evals_module.generate_conversation_scenarios(config={"count": 2})
9189+
9190+
def test_generate_conversation_scenarios_non_gemini_agent_raises(self):
9191+
evals_module = evals.Evals(api_client_=self.mock_api_client)
9192+
with pytest.raises(ValueError, match="Gemini Agents API"):
9193+
evals_module.generate_conversation_scenarios(
9194+
agent=_TEST_AGENT_ENGINE,
9195+
config={"count": 2},
9196+
)
9197+
91369198

91379199
class TestTransformDataframe:
91389200
"""Unit tests for the _transform_dataframe function."""

0 commit comments

Comments
 (0)