Skip to content

Commit ce64d7d

Browse files
jsondaicopybara-github
authored andcommitted
feat: GenAI Client(evals) - add agent parameter to evaluate() for managed agent id and interaction-id datasets
PiperOrigin-RevId: 940049391
1 parent a44c520 commit ce64d7d

5 files changed

Lines changed: 176 additions & 1 deletion

File tree

agentplatform/_genai/_evals_common.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,80 @@ 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+
if not loaded_data or not any(
615+
_evals_constant.INTERACTION_ID in row for row in loaded_data
616+
):
617+
return None
618+
619+
if not agent:
620+
raise ValueError(
621+
"An `agent` resource name is required when the dataset contains an"
622+
" `interaction_id` column, so the backend can resolve the Agent"
623+
" config for each interaction."
624+
)
625+
if not _is_gemini_agent_resource(agent):
626+
raise ValueError(
627+
"`agent` must be a Gemini Agents API resource name of the form"
628+
" projects/{project}/locations/{location}/agents/{agent} when"
629+
" evaluating interaction ids. Got: %s" % agent
630+
)
631+
632+
gemini_agent_config = types.GeminiAgentConfig(gemini_agent=agent)
633+
eval_cases = []
634+
for i, row in enumerate(loaded_data):
635+
interaction = row.get(_evals_constant.INTERACTION_ID)
636+
if not interaction:
637+
raise ValueError("Missing `interaction_id` value for row %d." % i)
638+
eval_cases.append(
639+
types.EvalCase(
640+
eval_case_id="eval_case_%s" % i,
641+
interactions_data_source=types.InteractionsDataSource(
642+
interaction=_normalize_interaction_resource(
643+
str(interaction), agent, location
644+
),
645+
gemini_agent_config=gemini_agent_config,
646+
),
647+
)
648+
)
649+
return types.EvaluationDataset(eval_cases=eval_cases)
650+
651+
578652
def _add_evaluation_run_labels(
579653
labels: Optional[dict[str, str]] = None,
580654
agent: Optional[str] = None,
@@ -1591,6 +1665,8 @@ def _resolve_dataset_inputs(
15911665
dataset_schema: Optional[Literal["GEMINI", "FLATTEN", "OPENAI"]],
15921666
loader: "_evals_utils.EvalDatasetLoader",
15931667
agent_info: Optional[types.evals.AgentInfo] = None,
1668+
agent: Optional[str] = None,
1669+
api_client: Any = None,
15941670
) -> tuple[types.EvaluationDataset, int]:
15951671
"""Loads and processes single or multiple datasets for evaluation.
15961672
@@ -1640,6 +1716,13 @@ def _resolve_dataset_inputs(
16401716
ds_source_for_loader = _get_dataset_source(ds_item)
16411717
current_loaded_data = loader.load(ds_source_for_loader)
16421718

1719+
interaction_dataset = _build_interaction_id_dataset(
1720+
current_loaded_data, agent, _get_resolved_location(api_client)
1721+
)
1722+
if interaction_dataset is not None:
1723+
parsed_evaluation_datasets.append(interaction_dataset)
1724+
continue
1725+
16431726
if dataset_schema:
16441727
current_schema = _evals_data_converters.EvalDatasetSchema(dataset_schema)
16451728
else:
@@ -1797,6 +1880,7 @@ def _execute_evaluation( # type: ignore[no-untyped-def]
17971880
api_client: Any,
17981881
dataset: Union[types.EvaluationDataset, list[types.EvaluationDataset]],
17991882
metrics: list[types.Metric],
1883+
agent: Optional[str] = None,
18001884
dataset_schema: Optional[Literal["GEMINI", "FLATTEN", "OPENAI"]] = None,
18011885
dest: Optional[str] = None,
18021886
location: Optional[str] = None,
@@ -1877,6 +1961,8 @@ def _execute_evaluation( # type: ignore[no-untyped-def]
18771961
dataset_schema=dataset_schema,
18781962
loader=loader,
18791963
agent_info=validated_agent_info,
1964+
agent=agent,
1965+
api_client=api_client,
18801966
)
18811967

18821968
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: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,36 @@ 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=(
597+
"projects/model-evaluation-dev/locations/global/agents/"
598+
"test-agent-eval"
599+
),
600+
metrics=[types.RubricMetric.MULTI_TURN_TASK_SUCCESS],
601+
)
602+
603+
assert isinstance(evaluation_result, types.EvaluationResult)
604+
assert evaluation_result.summary_metrics is not None
605+
assert len(evaluation_result.summary_metrics) > 0
606+
for summary in evaluation_result.summary_metrics:
607+
assert isinstance(summary, types.AggregatedMetricResult)
608+
assert summary.metric_name is not None
609+
assert summary.mean_score is not None
610+
611+
assert evaluation_result.eval_case_results is not None
612+
assert len(evaluation_result.eval_case_results) == 1
613+
614+
585615
pytestmark = pytest_helper.setup(
586616
file=__file__,
587617
globals_for_file=globals(),

tests/unit/agentplatform/genai/test_evals.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9905,6 +9905,54 @@ 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(self):
9948+
assert (
9949+
_evals_common._build_interaction_id_dataset(
9950+
[{"prompt": "hi", "response": "yo"}], _TEST_GEMINI_AGENT, "global"
9951+
)
9952+
is None
9953+
)
9954+
9955+
99089956
class TestCreateEvaluationRunGeminiAgent:
99099957
"""CUJ2: scrape a Gemini agent via create_evaluation_run."""
99109958

0 commit comments

Comments
 (0)