Skip to content

Commit fde5c65

Browse files
jsondaicopybara-github
authored andcommitted
chore: add metric handlers for GenAI evals module
PiperOrigin-RevId: 769901087
1 parent 5b1b83c commit fde5c65

5 files changed

Lines changed: 1275 additions & 62 deletions

File tree

tests/unit/vertexai/genai/test_evals.py

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import vertexai
2525
from google.cloud.aiplatform import initializer as aiplatform_initializer
2626
from vertexai import _genai
27+
from vertexai._genai import _evals_metric_handlers
2728
from vertexai._genai import _evals_data_converters
2829
from vertexai._genai import evals
2930
from vertexai._genai import types as vertexai_genai_types
@@ -1946,6 +1947,185 @@ def test_merge_with_invalid_eval_case_type(self):
19461947
schemas=[_evals_data_converters._EvalDatasetSchema.FLATTEN],
19471948
)
19481949

1950+
1951+
@pytest.mark.usefixtures("google_auth_mock")
1952+
class TestLLMMetricHandlerPayload:
1953+
def setup_method(self):
1954+
importlib.reload(aiplatform_initializer)
1955+
importlib.reload(aiplatform)
1956+
importlib.reload(vertexai)
1957+
importlib.reload(genai_types)
1958+
importlib.reload(vertexai_genai_types)
1959+
importlib.reload(_evals_data_converters)
1960+
importlib.reload(_evals_metric_handlers)
1961+
1962+
vertexai.init(project=_TEST_PROJECT, location=_TEST_LOCATION)
1963+
self.mock_api_client = mock.Mock(spec=client.Client)
1964+
self.mock_evals_module = evals.Evals(api_client_=self.mock_api_client)
1965+
1966+
def test_build_request_payload_basic_filtering_and_fields(self):
1967+
metric = vertexai_genai_types.LLMMetric(
1968+
name="test_quality",
1969+
prompt_template="Eval: {prompt} with {response}. Context: "
1970+
"{custom_context}. Ref: {reference}",
1971+
)
1972+
handler = _evals_metric_handlers.LLMMetricHandler(
1973+
module=self.mock_evals_module, metric=metric
1974+
)
1975+
1976+
eval_case = vertexai_genai_types.EvalCase(
1977+
prompt=genai_types.Content(
1978+
parts=[genai_types.Part(text="User prompt text")]
1979+
),
1980+
responses=[
1981+
vertexai_genai_types.ResponseCandidate(
1982+
response=genai_types.Content(
1983+
parts=[genai_types.Part(text="Model response text")]
1984+
)
1985+
)
1986+
],
1987+
reference=vertexai_genai_types.ResponseCandidate(
1988+
response=genai_types.Content(
1989+
parts=[genai_types.Part(text="Ground truth text")]
1990+
)
1991+
),
1992+
custom_context="Custom context value.", # pylint: disable=unexpected-keyword-arg
1993+
extra_field_not_in_template="This should be excluded.", # pylint: disable=unexpected-keyword-arg
1994+
eval_case_id="case-123",
1995+
)
1996+
1997+
payload = handler._build_request_payload(eval_case=eval_case, response_index=0)
1998+
1999+
expected_json_instance_dict = {
2000+
"prompt": "User prompt text",
2001+
"response": "Model response text",
2002+
"custom_context": "Custom context value.",
2003+
"reference": "Ground truth text",
2004+
}
2005+
2006+
actual_json_instance_str = payload["pointwise_metric_input"]["instance"][
2007+
"json_instance"
2008+
]
2009+
actual_json_instance_dict = json.loads(actual_json_instance_str)
2010+
2011+
assert actual_json_instance_dict == expected_json_instance_dict
2012+
assert "extra_field_not_in_template" not in actual_json_instance_dict
2013+
assert "eval_case_id" not in actual_json_instance_dict
2014+
2015+
assert (
2016+
"custom_output_format_config"
2017+
not in payload["pointwise_metric_input"]["metric_spec"]
2018+
)
2019+
assert (
2020+
"system_instruction" not in payload["pointwise_metric_input"]["metric_spec"]
2021+
)
2022+
assert "autorater_config" not in payload
2023+
2024+
def test_build_request_payload_various_field_types(self):
2025+
metric = vertexai_genai_types.LLMMetric(
2026+
name="complex_eval",
2027+
prompt_template=(
2028+
"P: {prompt}, R: {response}, Hist: {conversation_history}, "
2029+
"SysInstruct: {system_instruction}, "
2030+
"DictField: {dict_field}, ListField: {list_field}, "
2031+
"IntField: {int_field}, BoolField: {bool_field}"
2032+
),
2033+
)
2034+
handler = _evals_metric_handlers.LLMMetricHandler(
2035+
module=self.mock_evals_module, metric=metric
2036+
)
2037+
2038+
eval_case = vertexai_genai_types.EvalCase(
2039+
prompt=genai_types.Content(parts=[genai_types.Part(text="The Prompt")]),
2040+
responses=[
2041+
vertexai_genai_types.ResponseCandidate(
2042+
response=genai_types.Content(
2043+
parts=[genai_types.Part(text="The Response")]
2044+
)
2045+
)
2046+
],
2047+
conversation_history=[
2048+
vertexai_genai_types.Message(
2049+
content=genai_types.Content(
2050+
parts=[genai_types.Part(text="Turn 1 user")], role="user"
2051+
)
2052+
),
2053+
vertexai_genai_types.Message(
2054+
content=genai_types.Content(
2055+
parts=[genai_types.Part(text="Turn 1 model")], role="model"
2056+
)
2057+
),
2058+
],
2059+
system_instruction=genai_types.Content(
2060+
parts=[genai_types.Part(text="System instructions here.")]
2061+
),
2062+
dict_field={ # pylint: disable=unexpected-keyword-arg
2063+
"key1": "val1",
2064+
"key2": [1, 2],
2065+
},
2066+
list_field=["a", "b", {"c": 3}], # pylint: disable=unexpected-keyword-arg
2067+
int_field=42, # pylint: disable=unexpected-keyword-arg
2068+
bool_field=True, # pylint: disable=unexpected-keyword-arg
2069+
)
2070+
2071+
payload = handler._build_request_payload(eval_case=eval_case, response_index=0)
2072+
actual_json_instance_dict = json.loads(
2073+
payload["pointwise_metric_input"]["instance"]["json_instance"]
2074+
)
2075+
2076+
expected_json_instance_dict = {
2077+
"prompt": "The Prompt",
2078+
"response": "The Response",
2079+
"conversation_history": "user: Turn 1 user\nmodel: Turn 1 model",
2080+
"system_instruction": "System instructions here.",
2081+
"dict_field": json.dumps({"key1": "val1", "key2": [1, 2]}),
2082+
"list_field": json.dumps(["a", "b", {"c": 3}]),
2083+
"int_field": "42",
2084+
"bool_field": "True",
2085+
}
2086+
assert actual_json_instance_dict == expected_json_instance_dict
2087+
2088+
def test_build_request_payload_optional_metric_configs_set(self):
2089+
metric = vertexai_genai_types.LLMMetric(
2090+
name="configured_metric",
2091+
prompt_template="P: {prompt}, R: {response}",
2092+
return_raw_output=True,
2093+
judge_model_system_instruction="Be a fair judge.",
2094+
judge_model="gemini-pro",
2095+
judge_model_sampling_count=10,
2096+
)
2097+
handler = _evals_metric_handlers.LLMMetricHandler(
2098+
module=self.mock_evals_module, metric=metric
2099+
)
2100+
eval_case = vertexai_genai_types.EvalCase(
2101+
prompt=genai_types.Content(parts=[genai_types.Part(text="p")]),
2102+
responses=[
2103+
vertexai_genai_types.ResponseCandidate(
2104+
response=genai_types.Content(parts=[genai_types.Part(text="r")])
2105+
)
2106+
],
2107+
)
2108+
2109+
payload = handler._build_request_payload(eval_case=eval_case, response_index=0)
2110+
2111+
expected_json_instance = {"prompt": "p", "response": "r"}
2112+
actual_json_instance = json.loads(
2113+
payload["pointwise_metric_input"]["instance"]["json_instance"]
2114+
)
2115+
assert actual_json_instance == expected_json_instance
2116+
2117+
metric_spec_payload = payload["pointwise_metric_input"]["metric_spec"]
2118+
assert (
2119+
metric_spec_payload["metric_prompt_template"]
2120+
== "P: {prompt}, R: {response}"
2121+
)
2122+
assert metric_spec_payload["custom_output_format_config"]["return_raw_output"]
2123+
assert metric_spec_payload["system_instruction"] == "Be a fair judge."
2124+
2125+
autorater_config_payload = payload["autorater_config"]
2126+
assert autorater_config_payload["autorater_model"] == "gemini-pro"
2127+
assert autorater_config_payload["sampling_count"] == 10
2128+
19492129
def test_merge_with_invalid_prompt_type(self):
19502130
raw_dataset_1 = [
19512131
{

vertexai/_genai/_evals_common.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131

3232
logger = logging.getLogger(__name__)
3333

34-
_MAX_WORKERS = 100
34+
MAX_WORKERS = 100
3535

3636

3737
def _generate_content_with_retry(
@@ -182,9 +182,7 @@ def _run_gemini_inference(
182182
)
183183

184184
with tqdm(total=len(prompt_dataset), desc="Gemini Inference") as pbar:
185-
with concurrent.futures.ThreadPoolExecutor(
186-
max_workers=_MAX_WORKERS
187-
) as executor:
185+
with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
188186
for index, row in prompt_dataset.iterrows():
189187
request_dict = {}
190188
contents_input = row[primary_prompt_column]
@@ -281,9 +279,7 @@ def _run_custom_inference(
281279
raise ValueError("Dataset must contain either 'prompt' or 'request'.")
282280

283281
with tqdm(total=len(prompt_dataset), desc="Custom Inference") as pbar:
284-
with concurrent.futures.ThreadPoolExecutor(
285-
max_workers=_MAX_WORKERS
286-
) as executor:
282+
with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
287283
for index, row in prompt_dataset.iterrows():
288284
contents_input = row[primary_prompt_column]
289285

0 commit comments

Comments
 (0)