Skip to content

Commit 135224c

Browse files
jsondaicopybara-github
authored andcommitted
feat: GenAI Client(evals) - evaluate() agent parameter for interaction-id datasets
PiperOrigin-RevId: 947203638
1 parent 86c1a35 commit 135224c

5 files changed

Lines changed: 238 additions & 1 deletion

File tree

agentplatform/_genai/_evals_common.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -923,6 +923,90 @@ def _fetch_agent_config_dict(
923923
)
924924

925925

926+
def _get_resolved_location(api_client: Any) -> Optional[str]:
927+
"""Returns the location configured on the API client."""
928+
return getattr(api_client, "location", None)
929+
930+
931+
def _normalize_interaction_resource(
932+
interaction: str, agent: str, location: Optional[str]
933+
) -> str:
934+
"""Normalizes an interaction id into a full resource name.
935+
936+
A bare interaction id is expanded to
937+
`projects/{project}/locations/{location}/interactions/{id}` using the
938+
project and location parsed from the agent resource name. Fully-qualified
939+
interaction resource names are returned unchanged.
940+
"""
941+
if interaction.startswith("projects/"):
942+
return interaction
943+
parts = agent.split("/")
944+
project = parts[1]
945+
agent_location = parts[3] if len(parts) > 3 else (location or "global")
946+
return f"projects/{project}/locations/{agent_location}/interactions/{interaction}"
947+
948+
949+
def _build_interaction_id_dataset(
950+
loaded_data: list[dict[str, Any]],
951+
agent: Optional[str],
952+
location: Optional[str],
953+
) -> Optional[types.EvaluationDataset]:
954+
"""Builds an EvaluationDataset from rows that carry an `interaction_id`.
955+
956+
When the dataset contains an `interaction_id` column, each row is turned
957+
into an EvalCase whose `interactions_data_source` references the interaction
958+
and the Gemini agent. The backend resolves the interaction trace and agent
959+
config; no client-side prompt/response is required. Returns None if the
960+
data does not contain interaction ids.
961+
"""
962+
has_interaction_id = bool(loaded_data) and any(
963+
_evals_constant.INTERACTION_ID in row for row in loaded_data
964+
)
965+
if not has_interaction_id:
966+
if agent:
967+
raise ValueError(
968+
"An `agent` was provided but the dataset does not contain an"
969+
" `interaction_id` column. The `agent` argument is only used to"
970+
" resolve an `interaction_id` dataset column (so the backend can"
971+
" fetch the interaction trace and Agent config). To evaluate"
972+
" with an agent, provide a dataset with an `interaction_id`"
973+
" column; otherwise omit `agent`."
974+
)
975+
return None
976+
977+
if not agent:
978+
raise ValueError(
979+
"An `agent` resource name is required when the dataset contains an"
980+
" `interaction_id` column, so the backend can resolve the Agent"
981+
" config for each interaction."
982+
)
983+
if not _is_gemini_agent_resource(agent):
984+
raise ValueError(
985+
"`agent` must be a Gemini Agents API resource name of the form"
986+
" projects/{project}/locations/{location}/agents/{agent} when"
987+
f" evaluating interaction ids. Got: {agent}"
988+
)
989+
990+
gemini_agent_config = types.GeminiAgentConfig(gemini_agent=agent)
991+
eval_cases = []
992+
for i, row in enumerate(loaded_data):
993+
interaction = row.get(_evals_constant.INTERACTION_ID)
994+
if not interaction:
995+
raise ValueError(f"Missing `interaction_id` value for row {i}.")
996+
eval_cases.append(
997+
types.EvalCase(
998+
eval_case_id=f"eval_case_{i}",
999+
interactions_data_source=types.InteractionsDataSource(
1000+
interaction=_normalize_interaction_resource(
1001+
str(interaction), agent, location
1002+
),
1003+
gemini_agent_config=gemini_agent_config,
1004+
),
1005+
)
1006+
)
1007+
return types.EvaluationDataset(eval_cases=eval_cases)
1008+
1009+
9261010
def _add_evaluation_run_labels(
9271011
labels: Optional[dict[str, str]] = None,
9281012
agent: Optional[str] = None,
@@ -1939,6 +2023,8 @@ def _resolve_dataset_inputs(
19392023
dataset_schema: Optional[Literal["GEMINI", "FLATTEN", "OPENAI"]],
19402024
loader: "_evals_utils.EvalDatasetLoader",
19412025
agent_info: Optional[types.evals.AgentInfo] = None,
2026+
agent: Optional[str] = None,
2027+
api_client: Any = None,
19422028
) -> tuple[types.EvaluationDataset, int]:
19432029
"""Loads and processes single or multiple datasets for evaluation.
19442030
@@ -1988,6 +2074,21 @@ def _resolve_dataset_inputs(
19882074
ds_source_for_loader = _get_dataset_source(ds_item)
19892075
current_loaded_data = loader.load(ds_source_for_loader)
19902076

2077+
interaction_dataset = _build_interaction_id_dataset(
2078+
current_loaded_data, agent, _get_resolved_location(api_client)
2079+
)
2080+
if interaction_dataset is not None:
2081+
if dataset_schema:
2082+
raise ValueError(
2083+
"`dataset_schema` is not supported for datasets with an"
2084+
" `interaction_id` column. The interaction trace and agent"
2085+
" config are resolved by the backend, so no client-side"
2086+
" schema conversion is applied. Omit `dataset_schema` when"
2087+
" evaluating an interaction_id dataset."
2088+
)
2089+
parsed_evaluation_datasets.append(interaction_dataset)
2090+
continue
2091+
19912092
if dataset_schema:
19922093
current_schema = _evals_data_converters.EvalDatasetSchema(dataset_schema)
19932094
else:
@@ -2145,6 +2246,7 @@ def _execute_evaluation( # type: ignore[no-untyped-def]
21452246
api_client: Any,
21462247
dataset: Union[types.EvaluationDataset, list[types.EvaluationDataset]],
21472248
metrics: list[types.Metric],
2249+
agent: Optional[str] = None,
21482250
dataset_schema: Optional[Literal["GEMINI", "FLATTEN", "OPENAI"]] = None,
21492251
dest: Optional[str] = None,
21502252
location: Optional[str] = None,
@@ -2225,6 +2327,8 @@ def _execute_evaluation( # type: ignore[no-untyped-def]
22252327
dataset_schema=dataset_schema,
22262328
loader=loader,
22272329
agent_info=validated_agent_info,
2330+
agent=agent,
2331+
api_client=api_client,
22282332
)
22292333

22302334
resolved_metrics = _resolve_metrics(metrics, api_client)

agentplatform/_genai/_evals_constant.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
PARTS = "parts"
5858
USER_AUTHOR = "user"
5959
AGENT_DATA = "agent_data"
60+
INTERACTION_ID = "interaction_id"
6061
STARTING_PROMPT = "starting_prompt"
6162
CONVERSATION_PLAN = "conversation_plan"
6263
HISTORY = "history"
@@ -74,5 +75,6 @@
7475
STARTING_PROMPT,
7576
CONVERSATION_PLAN,
7677
AGENT_DATA,
78+
INTERACTION_ID,
7779
}
7880
)

agentplatform/_genai/evals.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2214,6 +2214,7 @@ def evaluate(
22142214
list[types.EvaluationDatasetOrDict],
22152215
],
22162216
metrics: Optional[list[types.MetricOrDict]] = None,
2217+
agent: Optional[str] = None,
22172218
location: Optional[str] = None,
22182219
config: Optional[types.EvaluateMethodConfigOrDict] = None,
22192220
**kwargs: Any,
@@ -2222,8 +2223,15 @@ def evaluate(
22222223
22232224
Args:
22242225
dataset: The dataset(s) to evaluate. Can be a pandas DataFrame, a single
2225-
`types.EvaluationDataset` or a list of `types.EvaluationDataset`.
2226+
`types.EvaluationDataset` or a list of `types.EvaluationDataset`. To
2227+
evaluate existing interactions, provide a dataset with an
2228+
`interaction_id` column; each interaction is resolved by the backend
2229+
using `agent` to populate the agent data for evaluation.
22262230
metrics: The list of metrics to use for evaluation.
2231+
agent: Optional Gemini Agents API agent resource name
2232+
(`projects/{project}/locations/{location}/agents/{agent}`). Required
2233+
when the dataset contains an `interaction_id` column: the backend uses
2234+
it to resolve the Agent config for each referenced interaction.
22272235
location: The location to use for the evaluation service. If not specified,
22282236
the location configured in the client will be used. If specified,
22292237
this will override the location set in `agentplatform.Client` only for
@@ -2274,6 +2282,7 @@ def evaluate(
22742282
api_client=self._api_client,
22752283
dataset=dataset,
22762284
metrics=metrics,
2285+
agent=agent,
22772286
dataset_schema=config.dataset_schema,
22782287
dest=config.dest,
22792288
location=location,

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,33 @@ def test_evaluation_single_turn_agent_data(client):
582582
assert len(evaluation_result.eval_case_results) == 1
583583

584584

585+
def test_evaluation_with_interaction_id(client):
586+
"""Tests evaluate() an interaction_id dataset with the `agent` parameter."""
587+
client._api_client._http_options.api_version = "v1beta1"
588+
eval_dataset = types.EvaluationDataset(
589+
eval_dataset_df=pd.DataFrame(
590+
{"interaction_id": ["ChA5YTc2MWEzZmIxNWQyY2Y2EAgaATAqBG1haW4"]}
591+
)
592+
)
593+
594+
evaluation_result = client.evals.evaluate(
595+
dataset=eval_dataset,
596+
agent=("projects/model-evaluation-dev/locations/global/agents/test-agent-eval"),
597+
metrics=[types.RubricMetric.MULTI_TURN_TASK_SUCCESS],
598+
)
599+
600+
assert isinstance(evaluation_result, types.EvaluationResult)
601+
assert evaluation_result.summary_metrics is not None
602+
assert len(evaluation_result.summary_metrics) > 0
603+
for summary in evaluation_result.summary_metrics:
604+
assert isinstance(summary, types.AggregatedMetricResult)
605+
assert summary.metric_name is not None
606+
assert summary.mean_score is not None
607+
608+
assert evaluation_result.eval_case_results is not None
609+
assert len(evaluation_result.eval_case_results) == 1
610+
611+
585612
pytestmark = pytest_helper.setup(
586613
file=__file__,
587614
globals_for_file=globals(),

tests/unit/agentplatform/genai/test_evals.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9905,6 +9905,101 @@ def test_evaluate_instances_sends_interactions_data_source(self):
99059905
assert data_source["gemini_agent_config"]["gemini_agent"] == _TEST_GEMINI_AGENT
99069906

99079907

9908+
class TestEvaluateInteractionIdDataset:
9909+
"""CUJ1 via evaluate(): interaction_id column + agent -> data source."""
9910+
9911+
def test_build_interaction_id_dataset_from_column(self):
9912+
loaded = [
9913+
{"interaction_id": "abc123"},
9914+
{"interaction_id": ("projects/p/locations/global/interactions/def456")},
9915+
]
9916+
dataset = _evals_common._build_interaction_id_dataset(
9917+
loaded, _TEST_GEMINI_AGENT, "global"
9918+
)
9919+
9920+
assert dataset is not None
9921+
assert len(dataset.eval_cases) == 2
9922+
ds0 = dataset.eval_cases[0].interactions_data_source
9923+
assert ds0.gemini_agent_config.gemini_agent == _TEST_GEMINI_AGENT
9924+
assert ds0.interaction == (
9925+
"projects/test-project/locations/us-central1/interactions/abc123"
9926+
)
9927+
assert (
9928+
dataset.eval_cases[1].interactions_data_source.interaction
9929+
== "projects/p/locations/global/interactions/def456"
9930+
)
9931+
assert dataset.eval_cases[0].agent_data is None
9932+
9933+
def test_build_interaction_id_dataset_requires_agent(self):
9934+
with pytest.raises(ValueError, match="agent.*required"):
9935+
_evals_common._build_interaction_id_dataset(
9936+
[{"interaction_id": "abc123"}], None, "global"
9937+
)
9938+
9939+
def test_build_interaction_id_dataset_rejects_non_gemini_agent(self):
9940+
with pytest.raises(ValueError, match="Gemini Agents API resource name"):
9941+
_evals_common._build_interaction_id_dataset(
9942+
[{"interaction_id": "abc123"}],
9943+
"projects/p/locations/us-central1/reasoningEngines/123",
9944+
"global",
9945+
)
9946+
9947+
def test_build_interaction_id_dataset_none_without_column_and_no_agent(self):
9948+
assert (
9949+
_evals_common._build_interaction_id_dataset(
9950+
[{"prompt": "hi", "response": "yo"}], None, "global"
9951+
)
9952+
is None
9953+
)
9954+
9955+
def test_build_interaction_id_dataset_agent_without_column_raises(self):
9956+
with pytest.raises(ValueError, match="interaction_id"):
9957+
_evals_common._build_interaction_id_dataset(
9958+
[{"prompt": "hi", "response": "yo"}], _TEST_GEMINI_AGENT, "global"
9959+
)
9960+
9961+
@mock.patch.object(_evals_utils, "EvalDatasetLoader")
9962+
def test_evaluate_agent_without_interaction_id_column_raises(
9963+
self, mock_eval_dataset_loader
9964+
):
9965+
agentplatform.init(project=_TEST_PROJECT, location=_TEST_LOCATION)
9966+
client = agentplatform.Client(project=_TEST_PROJECT, location=_TEST_LOCATION)
9967+
mock_df = pd.DataFrame([{"prompt": "p1", "response": "r1"}])
9968+
mock_eval_dataset_loader.return_value.load.return_value = mock_df.to_dict(
9969+
orient="records"
9970+
)
9971+
dataset = agentplatform_genai_types.EvaluationDataset(eval_dataset_df=mock_df)
9972+
9973+
with pytest.raises(ValueError, match="interaction_id"):
9974+
client.evals.evaluate(
9975+
dataset=dataset,
9976+
metrics=[agentplatform_genai_types.Metric(name="exact_match")],
9977+
agent=_TEST_GEMINI_AGENT,
9978+
)
9979+
9980+
@mock.patch.object(_evals_utils, "EvalDatasetLoader")
9981+
def test_evaluate_interaction_id_with_dataset_schema_raises(
9982+
self, mock_eval_dataset_loader
9983+
):
9984+
agentplatform.init(project=_TEST_PROJECT, location=_TEST_LOCATION)
9985+
client = agentplatform.Client(project=_TEST_PROJECT, location=_TEST_LOCATION)
9986+
mock_df = pd.DataFrame([{"interaction_id": "i1"}])
9987+
mock_eval_dataset_loader.return_value.load.return_value = mock_df.to_dict(
9988+
orient="records"
9989+
)
9990+
dataset = agentplatform_genai_types.EvaluationDataset(eval_dataset_df=mock_df)
9991+
9992+
with pytest.raises(ValueError, match="dataset_schema"):
9993+
client.evals.evaluate(
9994+
dataset=dataset,
9995+
metrics=[agentplatform_genai_types.Metric(name="exact_match")],
9996+
agent=_TEST_GEMINI_AGENT,
9997+
config=agentplatform_genai_types.EvaluateMethodConfig(
9998+
dataset_schema="GEMINI"
9999+
),
10000+
)
10001+
10002+
990810003
class TestCreateEvaluationRunGeminiAgent:
990910004
"""CUJ2: scrape a Gemini agent via create_evaluation_run."""
991010005

0 commit comments

Comments
 (0)