Skip to content

Commit fd12cb8

Browse files
jsondaicopybara-github
authored andcommitted
fix: GenAI Eval: fix a prompt template variable parsing issue when multiline json structures are present
PiperOrigin-RevId: 738555371
1 parent 96d2ecb commit fd12cb8

File tree

3 files changed

+103
-74
lines changed

3 files changed

+103
-74
lines changed

tests/unit/vertexai/test_evaluation.py

+83-54
Original file line numberDiff line numberDiff line change
@@ -2041,6 +2041,61 @@ def test_initialize_metric_column_mapping(self):
20412041
)
20422042
assert converted_metric_column_mapping == _EXPECTED_COLUMN_MAPPING
20432043

2044+
def test_upload_results(self, mock_storage_blob_from_string):
2045+
with mock.patch("json.dump") as mock_json_dump:
2046+
evaluation.utils.upload_evaluation_results(
2047+
MOCK_EVAL_RESULT,
2048+
_TEST_BUCKET,
2049+
_TEST_FILE_NAME,
2050+
"candidate_model",
2051+
"baseline_model",
2052+
"gs://test-bucket/test-dataset.csv",
2053+
[_TEST_POINTWISE_METRIC, _TEST_PAIRWISE_METRIC],
2054+
)
2055+
2056+
mock_storage_blob_from_string.assert_any_call(
2057+
uri="gs://test-bucket/test-file-name/test-file-name.csv",
2058+
client=mock.ANY,
2059+
)
2060+
mock_storage_blob_from_string.assert_any_call(
2061+
uri="gs://test-bucket/test-file-name/summary_metrics.json",
2062+
client=mock.ANY,
2063+
)
2064+
mock_json_dump.assert_called_once_with(
2065+
{
2066+
"summary_metrics": MOCK_EVAL_RESULT.summary_metrics,
2067+
"candidate_model_name": "candidate_model",
2068+
"baseline_model_name": "baseline_model",
2069+
"dataset_uri": "gs://test-bucket/test-dataset.csv",
2070+
"metric_descriptions": {
2071+
"test_pointwise_metric": {
2072+
"criteria": _CRITERIA,
2073+
"rating_rubric": _POINTWISE_RATING_RUBRIC,
2074+
},
2075+
"test_pairwise_metric": {
2076+
"criteria": _CRITERIA,
2077+
"rating_rubric": _PAIRWISE_RATING_RUBRIC,
2078+
},
2079+
},
2080+
},
2081+
mock.ANY,
2082+
)
2083+
2084+
def test_upload_results_with_default_file_name(self, mock_storage_blob_from_string):
2085+
with mock.patch.object(
2086+
aiplatform_utils, "timestamped_unique_name"
2087+
) as mock_timestamped_unique_name:
2088+
mock_timestamped_unique_name.return_value = "2025-02-10-12-00-00-12345"
2089+
evaluation.utils.upload_evaluation_results(
2090+
MOCK_EVAL_RESULT,
2091+
_TEST_BUCKET,
2092+
)
2093+
2094+
mock_storage_blob_from_string.assert_any_call(
2095+
uri="gs://test-bucket/eval_results_2025-02-10-12-00-00-12345/eval_results_2025-02-10-12-00-00-12345.csv",
2096+
client=mock.ANY,
2097+
)
2098+
20442099

20452100
class TestPromptTemplate:
20462101
def test_init(self):
@@ -2138,57 +2193,31 @@ def test_pairtwise_metric_prompt_template_with_default_values(self):
21382193
== _EXPECTED_PAIRWISE_PROMPT_TEMPLATE_WITH_DEFAULT_VALUES.strip()
21392194
)
21402195

2141-
def test_upload_results(self, mock_storage_blob_from_string):
2142-
with mock.patch("json.dump") as mock_json_dump:
2143-
evaluation.utils.upload_evaluation_results(
2144-
MOCK_EVAL_RESULT,
2145-
_TEST_BUCKET,
2146-
_TEST_FILE_NAME,
2147-
"candidate_model",
2148-
"baseline_model",
2149-
"gs://test-bucket/test-dataset.csv",
2150-
[_TEST_POINTWISE_METRIC, _TEST_PAIRWISE_METRIC],
2151-
)
2152-
2153-
mock_storage_blob_from_string.assert_any_call(
2154-
uri="gs://test-bucket/test-file-name/test-file-name.csv",
2155-
client=mock.ANY,
2156-
)
2157-
mock_storage_blob_from_string.assert_any_call(
2158-
uri="gs://test-bucket/test-file-name/summary_metrics.json",
2159-
client=mock.ANY,
2160-
)
2161-
mock_json_dump.assert_called_once_with(
2162-
{
2163-
"summary_metrics": MOCK_EVAL_RESULT.summary_metrics,
2164-
"candidate_model_name": "candidate_model",
2165-
"baseline_model_name": "baseline_model",
2166-
"dataset_uri": "gs://test-bucket/test-dataset.csv",
2167-
"metric_descriptions": {
2168-
"test_pointwise_metric": {
2169-
"criteria": _CRITERIA,
2170-
"rating_rubric": _POINTWISE_RATING_RUBRIC,
2171-
},
2172-
"test_pairwise_metric": {
2173-
"criteria": _CRITERIA,
2174-
"rating_rubric": _PAIRWISE_RATING_RUBRIC,
2175-
},
2176-
},
2177-
},
2178-
mock.ANY,
2179-
)
2180-
2181-
def test_upload_results_with_default_file_name(self, mock_storage_blob_from_string):
2182-
with mock.patch.object(
2183-
aiplatform_utils, "timestamped_unique_name"
2184-
) as mock_timestamped_unique_name:
2185-
mock_timestamped_unique_name.return_value = "2025-02-10-12-00-00-12345"
2186-
evaluation.utils.upload_evaluation_results(
2187-
MOCK_EVAL_RESULT,
2188-
_TEST_BUCKET,
2189-
)
2190-
2191-
mock_storage_blob_from_string.assert_any_call(
2192-
uri="gs://test-bucket/eval_results_2025-02-10-12-00-00-12345/eval_results_2025-02-10-12-00-00-12345.csv",
2193-
client=mock.ANY,
2194-
)
2196+
def test_complex_prompt_template_variables(self):
2197+
template_str = """Metric prompt template
2198+
instructions ...
2199+
Here are some JSON structures
2200+
{
2201+
"Function API spec": You may use default python libraries,
2202+
"example": test test
2203+
}
2204+
Output format prompt with JSON:
2205+
The answer should be a json alone which follows the json structure below:
2206+
{
2207+
"is_the_response_valid": [valid or invalid],
2208+
"reasoning":
2209+
"rewritten response":
2210+
}
2211+
Here are some actual variables:
2212+
{var_1} {var2} {_var_3} {
2213+
var_5_mutli_line
2214+
} {VAR_6} {7_var} {{var_9}}
2215+
"""
2216+
prompt_template = evaluation.PromptTemplate(template_str)
2217+
assert prompt_template.variables == {
2218+
"var_1",
2219+
"var2",
2220+
"_var_3",
2221+
"VAR_6",
2222+
"var_9",
2223+
}

vertexai/evaluation/prompt_template.py

+11-10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# -*- coding: utf-8 -*-
22

3-
# Copyright 2024 Google LLC
3+
# Copyright 2025 Google LLC
44
#
55
# Licensed under the Apache License, Version 2.0 (the "License");
66
# you may not use this file except in compliance with the License.
@@ -14,18 +14,23 @@
1414
# See the License for the specific language governing permissions and
1515
# limitations under the License.
1616
#
17-
import string
17+
"""Prompt template for creating prompts with variables."""
18+
19+
import re
1820
from typing import Set
1921

22+
_VARIABLE_NAME_REGEX = r"\{([_a-zA-Z][_a-zA-Z0-9]*)\}"
23+
2024

2125
class PromptTemplate:
2226
"""A prompt template for creating prompts with variables.
2327
2428
The `PromptTemplate` class allows users to define a template string with
2529
variables represented in curly braces `{variable}`. The variable
26-
names cannot contain spaces. These variables can be replaced with specific
27-
values using the `assemble` method, providing flexibility in generating
28-
dynamic prompts.
30+
names cannot contain spaces and must start with a letter or underscore,
31+
followed by letters, digits, or underscore. These variables can be
32+
replaced with specific values using the `assemble` method, providing
33+
flexibility in generating dynamic prompts.
2934
3035
Usage:
3136
@@ -49,11 +54,7 @@ def __init__(self, template: str):
4954

5055
def _get_variables(self) -> Set[str]:
5156
"""Extracts and return a set of variable names from the template."""
52-
return set(
53-
field_name
54-
for _, field_name, _, _ in string.Formatter().parse(self.template)
55-
if field_name is not None
56-
)
57+
return set(re.findall(_VARIABLE_NAME_REGEX, self.template))
5758

5859
def assemble(self, **kwargs) -> "PromptTemplate":
5960
"""Replaces only the provided variables in the template with specific values.

vertexai/preview/evaluation/prompt_template.py

+9-10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# -*- coding: utf-8 -*-
22

3-
# Copyright 2024 Google LLC
3+
# Copyright 2025 Google LLC
44
#
55
# Licensed under the Apache License, Version 2.0 (the "License");
66
# you may not use this file except in compliance with the License.
@@ -16,18 +16,21 @@
1616
#
1717
"""Prompt template for creating prompts with variables."""
1818

19-
import string
19+
import re
2020
from typing import Set
2121

22+
_VARIABLE_NAME_REGEX = r"\{([_a-zA-Z][_a-zA-Z0-9]*)\}"
23+
2224

2325
class PromptTemplate:
2426
"""A prompt template for creating prompts with variables.
2527
2628
The `PromptTemplate` class allows users to define a template string with
2729
variables represented in curly braces `{variable}`. The variable
28-
names cannot contain spaces. These variables can be replaced with specific
29-
values using the `assemble` method, providing flexibility in generating
30-
dynamic prompts.
30+
names cannot contain spaces and must start with a letter or underscore,
31+
followed by letters, digits, or underscore. These variables can be
32+
replaced with specific values using the `assemble` method, providing
33+
flexibility in generating dynamic prompts.
3134
3235
Usage:
3336
@@ -51,11 +54,7 @@ def __init__(self, template: str):
5154

5255
def _get_variables(self) -> Set[str]:
5356
"""Extracts and return a set of variable names from the template."""
54-
return set(
55-
field_name
56-
for _, field_name, _, _ in string.Formatter().parse(self.template)
57-
if field_name is not None
58-
)
57+
return set(re.findall(_VARIABLE_NAME_REGEX, self.template))
5958

6059
def assemble(self, **kwargs) -> "PromptTemplate":
6160
"""Replaces only the provided variables in the template with specific values.

0 commit comments

Comments
 (0)