Skip to content

Commit 0a00203

Browse files
jsondaicopybara-github
authored andcommitted
feat: GenAI Client(evals) - evaluate() agent parameter for interaction-id datasets
PiperOrigin-RevId: 940049391
1 parent f6ecd67 commit 0a00203

5 files changed

Lines changed: 208 additions & 1 deletion

File tree

agentplatform/_genai/_evals_common.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,90 @@ def _is_gemini_agent_resource(agent: str) -> bool:
575575
)
576576

577577

578+
def _get_resolved_location(api_client: Any) -> Optional[str]:
579+
"""Returns the location configured on the API client."""
580+
return getattr(api_client, "location", None)
581+
582+
583+
def _normalize_interaction_resource(
584+
interaction: str, agent: str, location: Optional[str]
585+
) -> str:
586+
"""Normalizes an interaction id into a full resource name.
587+
588+
A bare interaction id is expanded to
589+
`projects/{project}/locations/{location}/interactions/{id}` using the
590+
project and location parsed from the agent resource name. Fully-qualified
591+
interaction resource names are returned unchanged.
592+
"""
593+
if interaction.startswith("projects/"):
594+
return interaction
595+
parts = agent.split("/")
596+
project = parts[1]
597+
agent_location = parts[3] if len(parts) > 3 else (location or "global")
598+
return f"projects/{project}/locations/{agent_location}/interactions/{interaction}"
599+
600+
601+
def _build_interaction_id_dataset(
602+
loaded_data: list[dict[str, Any]],
603+
agent: Optional[str],
604+
location: Optional[str],
605+
) -> Optional[types.EvaluationDataset]:
606+
"""Builds an EvaluationDataset from rows that carry an `interaction_id`.
607+
608+
When the dataset contains an `interaction_id` column, each row is turned
609+
into an EvalCase whose `interactions_data_source` references the interaction
610+
and the Gemini agent. The backend resolves the interaction trace and agent
611+
config; no client-side prompt/response is required. Returns None if the
612+
data does not contain interaction ids.
613+
"""
614+
has_interaction_id = bool(loaded_data) and any(
615+
_evals_constant.INTERACTION_ID in row for row in loaded_data
616+
)
617+
if not has_interaction_id:
618+
if agent:
619+
raise ValueError(
620+
"An `agent` was provided but the dataset does not contain an"
621+
" `interaction_id` column. The `agent` argument is only used to"
622+
" resolve an `interaction_id` dataset column (so the backend can"
623+
" fetch the interaction trace and Agent config). To evaluate"
624+
" with an agent, provide a dataset with an `interaction_id`"
625+
" column; otherwise omit `agent`."
626+
)
627+
return None
628+
629+
if not agent:
630+
raise ValueError(
631+
"An `agent` resource name is required when the dataset contains an"
632+
" `interaction_id` column, so the backend can resolve the Agent"
633+
" config for each interaction."
634+
)
635+
if not _is_gemini_agent_resource(agent):
636+
raise ValueError(
637+
"`agent` must be a Gemini Agents API resource name of the form"
638+
" projects/{project}/locations/{location}/agents/{agent} when"
639+
" evaluating interaction ids. Got: %s" % agent
640+
)
641+
642+
gemini_agent_config = types.GeminiAgentConfig(gemini_agent=agent)
643+
eval_cases = []
644+
for i, row in enumerate(loaded_data):
645+
interaction = row.get(_evals_constant.INTERACTION_ID)
646+
if not interaction:
647+
raise ValueError("Missing `interaction_id` value for row %d." % i)
648+
eval_cases.append(
649+
types.EvalCase(
650+
eval_case_id="eval_case_%s" % i,
651+
interactions_data_source=types.InteractionsDataSource(
652+
interaction=_normalize_interaction_resource(
653+
str(interaction), agent, location
654+
),
655+
gemini_agent_config=gemini_agent_config,
656+
),
657+
)
658+
)
659+
return types.EvaluationDataset(eval_cases=eval_cases)
660+
661+
578662
def _add_evaluation_run_labels(
579663
labels: Optional[dict[str, str]] = None,
580664
agent: Optional[str] = None,
@@ -1591,6 +1675,8 @@ def _resolve_dataset_inputs(
15911675
dataset_schema: Optional[Literal["GEMINI", "FLATTEN", "OPENAI"]],
15921676
loader: "_evals_utils.EvalDatasetLoader",
15931677
agent_info: Optional[types.evals.AgentInfo] = None,
1678+
agent: Optional[str] = None,
1679+
api_client: Any = None,
15941680
) -> tuple[types.EvaluationDataset, int]:
15951681
"""Loads and processes single or multiple datasets for evaluation.
15961682
@@ -1640,6 +1726,13 @@ def _resolve_dataset_inputs(
16401726
ds_source_for_loader = _get_dataset_source(ds_item)
16411727
current_loaded_data = loader.load(ds_source_for_loader)
16421728

1729+
interaction_dataset = _build_interaction_id_dataset(
1730+
current_loaded_data, agent, _get_resolved_location(api_client)
1731+
)
1732+
if interaction_dataset is not None:
1733+
parsed_evaluation_datasets.append(interaction_dataset)
1734+
continue
1735+
16431736
if dataset_schema:
16441737
current_schema = _evals_data_converters.EvalDatasetSchema(dataset_schema)
16451738
else:
@@ -1797,6 +1890,7 @@ def _execute_evaluation( # type: ignore[no-untyped-def]
17971890
api_client: Any,
17981891
dataset: Union[types.EvaluationDataset, list[types.EvaluationDataset]],
17991892
metrics: list[types.Metric],
1893+
agent: Optional[str] = None,
18001894
dataset_schema: Optional[Literal["GEMINI", "FLATTEN", "OPENAI"]] = None,
18011895
dest: Optional[str] = None,
18021896
location: Optional[str] = None,
@@ -1877,6 +1971,8 @@ def _execute_evaluation( # type: ignore[no-untyped-def]
18771971
dataset_schema=dataset_schema,
18781972
loader=loader,
18791973
agent_info=validated_agent_info,
1974+
agent=agent,
1975+
api_client=api_client,
18801976
)
18811977

18821978
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: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9905,6 +9905,79 @@ 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+
99089981
class TestCreateEvaluationRunGeminiAgent:
99099982
"""CUJ2: scrape a Gemini agent via create_evaluation_run."""
99109983

0 commit comments

Comments
 (0)