Skip to content

Commit 76b9914

Browse files
committed
Add ignore_args support for tool trajectory evaluation
1 parent 9670ce2 commit 76b9914

3 files changed

Lines changed: 107 additions & 9 deletions

File tree

src/google/adk/evaluation/eval_metrics.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,16 @@ class MatchType(Enum):
249249
),
250250
)
251251

252+
ignore_args: bool = Field(
253+
default=False,
254+
description=(
255+
"When True, tool call matching checks only the tool name and "
256+
"ignores argument values. Useful for non-deterministic "
257+
"arguments such as timestamps, generated queries, or "
258+
"other dynamic content."
259+
),
260+
)
261+
252262
@field_validator("match_type", mode="before")
253263
@classmethod
254264
def _coerce_match_type(cls, value: object) -> object:

src/google/adk/evaluation/trajectory_evaluator.py

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ def __init__(
8282
)
8383
self._threshold = criterion.threshold
8484
self._match_type = criterion.match_type
85+
self._ignore_args = criterion.ignore_args
8586
except ValidationError as e:
8687
expected_criterion_type_error = ValueError(
8788
f"`{eval_metric.metric_name}` metric expects a criterion of type"
@@ -91,9 +92,11 @@ def __init__(
9192
elif eval_metric:
9293
self._threshold = eval_metric.threshold
9394
self._match_type = ToolTrajectoryCriterion.MatchType.EXACT
95+
self._ignore_args = False
9496
else:
9597
self._threshold = threshold
9698
self._match_type = ToolTrajectoryCriterion.MatchType.EXACT
99+
self._ignore_args = False
97100

98101
@override
99102
def evaluate_invocations(
@@ -148,25 +151,42 @@ def _calculate_tool_use_accuracy(
148151
tool_use_match_status = False
149152
if self._match_type == ToolTrajectoryCriterion.MatchType.EXACT:
150153
tool_use_match_status = self._are_tool_calls_exact_match(
151-
actual_tool_uses, expected_tool_uses
154+
actual_tool_uses, expected_tool_uses, self._ignore_args
152155
)
153156
elif self._match_type == ToolTrajectoryCriterion.MatchType.IN_ORDER:
154157
tool_use_match_status = self._are_tool_calls_in_order_match(
155-
actual_tool_uses, expected_tool_uses
158+
actual_tool_uses, expected_tool_uses, self._ignore_args
156159
)
157160
elif self._match_type == ToolTrajectoryCriterion.MatchType.ANY_ORDER:
158161
tool_use_match_status = self._are_tool_calls_any_order_match(
159-
actual_tool_uses, expected_tool_uses
162+
actual_tool_uses, expected_tool_uses, self._ignore_args
160163
)
161164
else:
162165
raise ValueError(f"Unsupported match type {self._match_type}")
163166

164167
return 1.0 if tool_use_match_status else 0.0
165168

169+
def _calls_match(
170+
self,
171+
expected: genai_types.FunctionCall,
172+
actual: genai_types.FunctionCall,
173+
ignore_args: bool,
174+
) -> bool:
175+
"""Returns True if two tool calls are considered a match."""
176+
177+
if expected.name != actual.name:
178+
return False
179+
180+
if ignore_args:
181+
return True
182+
183+
return expected.args == actual.args
184+
166185
def _are_tool_calls_in_order_match(
167186
self,
168187
actual_tool_calls: list[genai_types.FunctionCall],
169188
expected_tool_calls: list[genai_types.FunctionCall],
189+
ignore_args: bool = False,
170190
) -> bool:
171191
"""Checks if expected tool calls appear in actual tool calls in order.
172192
@@ -191,10 +211,7 @@ def _are_tool_calls_in_order_match(
191211
try:
192212
current_expected = next(expected_it)
193213
for actual in actual_tool_calls:
194-
if (
195-
actual.name == current_expected.name
196-
and actual.args == current_expected.args
197-
):
214+
if self._calls_match(current_expected, actual, ignore_args):
198215
current_expected = next(expected_it)
199216
except StopIteration:
200217
return True
@@ -205,6 +222,7 @@ def _are_tool_calls_any_order_match(
205222
self,
206223
actual_tool_calls: list[genai_types.FunctionCall],
207224
expected_tool_calls: list[genai_types.FunctionCall],
225+
ignore_args: bool = False,
208226
) -> bool:
209227
"""Checks if expected tool calls appear in actual tool calls in any order.
210228
@@ -229,7 +247,7 @@ def _are_tool_calls_any_order_match(
229247
for expected in expected_tool_calls:
230248
found = False
231249
for i, actual in enumerate(actual_tool_calls_copy):
232-
if actual.name == expected.name and actual.args == expected.args:
250+
if self._calls_match(expected, actual, ignore_args):
233251
actual_tool_calls_copy.pop(i)
234252
found = True
235253
break
@@ -241,6 +259,7 @@ def _are_tool_calls_exact_match(
241259
self,
242260
actual_tool_calls: list[genai_types.FunctionCall],
243261
expected_tool_calls: list[genai_types.FunctionCall],
262+
ignore_args: bool = False,
244263
) -> bool:
245264
"""Checks if actual tool calls exactly match expected tool calls.
246265
@@ -260,7 +279,7 @@ def _are_tool_calls_exact_match(
260279
return False
261280

262281
for actual, expected in zip(actual_tool_calls, expected_tool_calls):
263-
if actual.name != expected.name or actual.args != expected.args:
282+
if not self._calls_match(expected, actual, ignore_args):
264283
return False
265284

266285
return True

tests/unittests/evaluation/test_trajectory_evaluator.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,3 +462,72 @@ def test_evaluate_invocations_no_invocations(evaluator: TrajectoryEvaluator):
462462
assert result.overall_score is None
463463
assert result.overall_eval_status == EvalStatus.NOT_EVALUATED
464464
assert not result.per_invocation_results
465+
466+
467+
def test_exact_match_ignore_args():
468+
"""Tests EXACT match when args differ but names are the same."""
469+
evaluator = TrajectoryEvaluator()
470+
471+
actual = [genai_types.FunctionCall(name="search", args={"query": "cats"})]
472+
expected = [genai_types.FunctionCall(name="search", args={"query": "dogs"})]
473+
474+
assert evaluator._are_tool_calls_exact_match(
475+
actual, expected, ignore_args=True
476+
)
477+
478+
479+
def test_in_order_match_ignore_args():
480+
"""Tests IN_ORDER match when args differ."""
481+
evaluator = TrajectoryEvaluator()
482+
483+
actual = [
484+
genai_types.FunctionCall(name="search", args={"query": "cats"}),
485+
genai_types.FunctionCall(name="lookup", args={"id": "123"}),
486+
]
487+
488+
expected = [
489+
genai_types.FunctionCall(name="search", args={"query": "dogs"}),
490+
genai_types.FunctionCall(name="lookup", args={"id": "999"}),
491+
]
492+
493+
assert evaluator._are_tool_calls_in_order_match(
494+
actual, expected, ignore_args=True
495+
)
496+
497+
498+
def test_any_order_match_ignore_args():
499+
"""Tests ANY_ORDER match when args differ and order differs."""
500+
evaluator = TrajectoryEvaluator()
501+
502+
actual = [
503+
genai_types.FunctionCall(name="lookup", args={"id": "123"}),
504+
genai_types.FunctionCall(name="search", args={"query": "cats"}),
505+
]
506+
507+
expected = [
508+
genai_types.FunctionCall(name="search", args={"query": "dogs"}),
509+
genai_types.FunctionCall(name="lookup", args={"id": "999"}),
510+
]
511+
512+
assert evaluator._are_tool_calls_any_order_match(
513+
actual, expected, ignore_args=True
514+
)
515+
516+
517+
def test_trajectory_evaluator_loads_ignore_args_from_criterion():
518+
"""Tests ignore_args configuration is propagated from criterion."""
519+
criterion = ToolTrajectoryCriterion(
520+
threshold=0.5,
521+
match_type=ToolTrajectoryCriterion.MatchType.EXACT,
522+
ignore_args=True,
523+
)
524+
525+
metric = EvalMetric(
526+
metric_name=PrebuiltMetrics.TOOL_TRAJECTORY_AVG_SCORE.value,
527+
threshold=0.5,
528+
criterion=criterion,
529+
)
530+
531+
evaluator = TrajectoryEvaluator(eval_metric=metric)
532+
533+
assert evaluator._ignore_args is True

0 commit comments

Comments
 (0)