Skip to content

Commit 5d2aca0

Browse files
GWealecopybara-github
authored andcommitted
fix: scope custom metrics per registry and trust only the config path
Co-authored-by: George Weale <gweale@google.com> PiperOrigin-RevId: 956688159
1 parent 76c64ef commit 5d2aca0

6 files changed

Lines changed: 318 additions & 30 deletions

File tree

src/google/adk/errors/not_found_error.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
class NotFoundError(Exception):
1919
"""Represents an error that occurs when an entity is not found."""
2020

21-
def __init__(self, message="The requested item was not found."):
21+
def __init__(
22+
self, message: str = "The requested item was not found."
23+
) -> None:
2224
"""Initializes the NotFoundError exception.
2325
2426
Args:

src/google/adk/evaluation/eval_config.py

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -269,27 +269,31 @@ def get_eval_metrics_from_config(eval_config: EvalConfig) -> list[EvalMetric]:
269269
custom_function_path = config.code_config.name
270270

271271
if isinstance(criterion, float):
272-
eval_metric_list.append(
273-
EvalMetric(
274-
metric_name=metric_name,
275-
threshold=criterion,
276-
criterion=BaseCriterion(threshold=criterion),
277-
custom_function_path=custom_function_path,
278-
)
272+
eval_metric = EvalMetric(
273+
metric_name=metric_name,
274+
threshold=criterion,
275+
criterion=BaseCriterion(threshold=criterion),
276+
custom_function_path=custom_function_path,
279277
)
280278
elif isinstance(criterion, BaseCriterion):
281-
eval_metric_list.append(
282-
EvalMetric(
283-
metric_name=metric_name,
284-
threshold=criterion.threshold,
285-
criterion=criterion,
286-
custom_function_path=custom_function_path,
287-
)
279+
eval_metric = EvalMetric(
280+
metric_name=metric_name,
281+
threshold=criterion.threshold,
282+
criterion=criterion,
283+
custom_function_path=custom_function_path,
288284
)
289285
else:
290286
raise ValueError(
291287
f"Unexpected criterion type. {type(criterion).__name__} not"
292288
" supported."
293289
)
294290

291+
# The config is written by the developer running the eval, so the path it
292+
# declares is the one honoured when the metric runs. It travels with the
293+
# metric rather than in a registry keyed by metric name, so two apps in
294+
# one process can declare the same metric name and each still gets its
295+
# own function.
296+
eval_metric._config_custom_function_path = custom_function_path # pylint: disable=protected-access
297+
eval_metric_list.append(eval_metric)
298+
295299
return eval_metric_list

src/google/adk/evaluation/eval_metrics.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from pydantic import ConfigDict
2626
from pydantic import Field
2727
from pydantic import field_validator
28+
from pydantic import PrivateAttr
2829
from pydantic import SerializeAsAny
2930
from pydantic.json_schema import SkipJsonSchema
3031
from typing_extensions import TypeAlias
@@ -300,6 +301,11 @@ class EvalMetric(EvalBaseModel):
300301
description="""Path to custom function, if this is a custom metric.""",
301302
)
302303

304+
# The path declared for this metric in the eval config it was built from.
305+
# Private, so that a metric parsed from an inbound payload cannot carry one:
306+
# the public field above is settable by whoever built that payload.
307+
_config_custom_function_path: Optional[str] = PrivateAttr(default=None)
308+
303309

304310
class EvalMetricResultDetails(EvalBaseModel):
305311
rubric_scores: Optional[list[RubricScore]] = Field(

src/google/adk/evaluation/metric_evaluator_registry.py

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,15 @@
5959
class MetricEvaluatorRegistry:
6060
"""A registry for metric Evaluators."""
6161

62-
_registry: dict[str, tuple[type[Evaluator], MetricInfo]] = {}
62+
def __init__(self) -> None:
63+
# Each registry instance owns its mappings, so a custom metric registered
64+
# for one app is not resolvable from another app's registry. The standard
65+
# metrics are seeded into every instance, as they are the same everywhere.
66+
self._registry: dict[str, tuple[type[Evaluator], MetricInfo]] = {}
67+
# Module path of the custom function backing a metric, keyed by metric
68+
# name. Only ever written from an eval config.
69+
self._custom_function_paths: dict[str, str] = {}
70+
_register_standard_metrics(self)
6371

6472
def get_evaluator(self, eval_metric: EvalMetric) -> Evaluator:
6573
"""Returns an Evaluator for the given metric.
@@ -75,14 +83,34 @@ def get_evaluator(self, eval_metric: EvalMetric) -> Evaluator:
7583
if eval_metric.metric_name not in self._registry:
7684
raise NotFoundError(f"{eval_metric.metric_name} not found in registry.")
7785

78-
evaluator_type = self._registry[eval_metric.metric_name][0]
86+
evaluator_type, _ = self._registry[eval_metric.metric_name]
7987
if issubclass(evaluator_type, _CustomMetricEvaluator):
88+
custom_function_path = self._custom_function_path(eval_metric)
89+
if custom_function_path is None:
90+
raise NotFoundError(
91+
f"No custom function registered for {eval_metric.metric_name}."
92+
)
8093
return evaluator_type(
8194
eval_metric=eval_metric,
82-
custom_function_path=eval_metric.custom_function_path,
95+
custom_function_path=custom_function_path,
8396
)
8497
return evaluator_type(eval_metric=eval_metric)
8598

99+
def _custom_function_path(self, eval_metric: EvalMetric) -> Optional[str]:
100+
"""Returns the module path to import for a custom metric, if known.
101+
102+
Both sources are eval config entries: one recorded when the metric was
103+
registered from a config, the other carried on a metric built from a
104+
config. The `custom_function_path` field on the incoming metric is not
105+
consulted, as it can be set by whoever built the request.
106+
107+
Args:
108+
eval_metric: The metric whose custom function is being resolved.
109+
"""
110+
if path := self._custom_function_paths.get(eval_metric.metric_name):
111+
return path
112+
return eval_metric._config_custom_function_path # pylint: disable=protected-access
113+
86114
def register_evaluator(
87115
self,
88116
metric_info: MetricInfo,
@@ -92,6 +120,25 @@ def register_evaluator(
92120
93121
If a mapping already exist, then it is updated.
94122
"""
123+
self._register(metric_info, evaluator, custom_function_path=None)
124+
125+
def _register(
126+
self,
127+
metric_info: MetricInfo,
128+
evaluator: type[Evaluator],
129+
custom_function_path: Optional[str],
130+
) -> None:
131+
"""Registers an evaluator, along with the function path it may need.
132+
133+
A path already recorded for the metric is kept when this registration does
134+
not carry one, so re-registering an evaluator does not drop it.
135+
136+
Args:
137+
metric_info: Info for the metric the evaluator is registered against.
138+
evaluator: The evaluator class to register.
139+
custom_function_path: Module path of the function backing a custom
140+
metric, taken from an eval config, or None.
141+
"""
95142
metric_name = metric_info.metric_name
96143
if metric_name in self._registry:
97144
logger.info(
@@ -102,6 +149,8 @@ def register_evaluator(
102149
)
103150

104151
self._registry[str(metric_name)] = (evaluator, metric_info)
152+
if custom_function_path is not None:
153+
self._custom_function_paths[str(metric_name)] = custom_function_path
105154

106155
def get_registered_metrics(
107156
self,
@@ -113,10 +162,10 @@ def get_registered_metrics(
113162
]
114163

115164

116-
def _get_default_metric_evaluator_registry() -> MetricEvaluatorRegistry:
117-
"""Returns an instance of MetricEvaluatorRegistry with standard metrics already registered in it."""
118-
metric_evaluator_registry = MetricEvaluatorRegistry()
119-
165+
def _register_standard_metrics(
166+
metric_evaluator_registry: MetricEvaluatorRegistry,
167+
) -> None:
168+
"""Registers the metrics that ship with ADK into the given registry."""
120169
metric_evaluator_registry.register_evaluator(
121170
metric_info=TrajectoryEvaluatorMetricInfoProvider().get_metric_info(),
122171
evaluator=TrajectoryEvaluator,
@@ -175,7 +224,10 @@ def _get_default_metric_evaluator_registry() -> MetricEvaluatorRegistry:
175224
evaluator=RubricBasedMultiTurnTrajectoryEvaluator,
176225
)
177226

178-
return metric_evaluator_registry
227+
228+
def _get_default_metric_evaluator_registry() -> MetricEvaluatorRegistry:
229+
"""Returns an instance of MetricEvaluatorRegistry with standard metrics already registered in it."""
230+
return MetricEvaluatorRegistry()
179231

180232

181233
DEFAULT_METRIC_EVALUATOR_REGISTRY = _get_default_metric_evaluator_registry()
@@ -223,7 +275,7 @@ def register_custom_metrics_from_config(
223275
metric_info = _get_default_metric_info(
224276
metric_name=metric_name, description=config.description
225277
)
226-
metric_evaluator_registry.register_evaluator(
227-
metric_info, _CustomMetricEvaluator
278+
metric_evaluator_registry._register( # pylint: disable=protected-access
279+
metric_info, _CustomMetricEvaluator, config.code_config.name
228280
)
229281
return metric_evaluator_registry

0 commit comments

Comments
 (0)