|
| 1 | +# Custom evaluation metric |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +This sample shows how to write and register your own evaluation metric when the |
| 6 | +built-in criteria can't express the rule you care about. |
| 7 | + |
| 8 | +`temperature_safety.py` defines `temperature_safety_score`, a metric that |
| 9 | +inspects the agent's *actual* tool calls and fails if any `set_temperature` call |
| 10 | +requests a value outside the safe range of 18-30 Celsius. This is a |
| 11 | +safety/business rule the built-in criteria (`tool_trajectory_avg_score`, |
| 12 | +`response_match_score`, …) can't express, because it checks the *values* passed |
| 13 | +to a specific tool rather than comparing against a reference trajectory. |
| 14 | + |
| 15 | +## Sample Inputs |
| 16 | + |
| 17 | +The eval set (`home_automation.evalset.json`) contains one single-turn case: |
| 18 | + |
| 19 | +- `Set the Bedroom to 21 degrees.` |
| 20 | + |
| 21 | +## How To |
| 22 | + |
| 23 | +### The metric function |
| 24 | + |
| 25 | +A custom metric is any callable with this signature that returns an |
| 26 | +`EvaluationResult`: |
| 27 | + |
| 28 | +```python |
| 29 | +def temperature_safety_score( |
| 30 | + eval_metric: EvalMetric, |
| 31 | + actual_invocations: list[Invocation], |
| 32 | + expected_invocations: Optional[list[Invocation]], |
| 33 | + conversation_scenario: Optional[ConversationScenario], |
| 34 | +) -> EvaluationResult: |
| 35 | +``` |
| 36 | + |
| 37 | +The function may be sync or async. Inside it: |
| 38 | + |
| 39 | +- Read the agent's actual tool calls for each invocation with |
| 40 | + `get_all_tool_calls(invocation.intermediate_data)`. This returns |
| 41 | + `google.genai.types.FunctionCall` objects, each with a `.name` and `.args`, so |
| 42 | + you can inspect exactly what the agent called and with which arguments. |
| 43 | +- Return an `EvaluationResult`. Set `overall_eval_status` (PASSED/FAILED) and a |
| 44 | + matching `per_invocation_results` entry for every invocation. `adk eval` |
| 45 | + derives pass/fail from the status, not from `overall_score` alone. A metric |
| 46 | + that sets only a score leaves the status at `NOT_EVALUATED`, and the case is |
| 47 | + reported as not passed even with a perfect score. (When the status is not |
| 48 | + `NOT_EVALUATED`, `adk eval` also requires one `per_invocation_results` entry |
| 49 | + per invocation.) |
| 50 | + |
| 51 | +### Registering the metric |
| 52 | + |
| 53 | +The metric is wired in via `custom_metrics` in `eval_config.json`: |
| 54 | + |
| 55 | +```json |
| 56 | +{ |
| 57 | + "criteria": { |
| 58 | + "temperature_safety_score": 1.0 |
| 59 | + }, |
| 60 | + "custom_metrics": { |
| 61 | + "temperature_safety_score": { |
| 62 | + "code_config": {"name": "temperature_safety.temperature_safety_score"}, |
| 63 | + "description": "Fails if any set_temperature call is outside 18-30 Celsius." |
| 64 | + } |
| 65 | + } |
| 66 | +} |
| 67 | +``` |
| 68 | + |
| 69 | +The metric name appears in both `criteria` (with its threshold) and |
| 70 | +`custom_metrics`. The `code_config.name` is a dotted path: everything before the |
| 71 | +last dot is the *module* (`temperature_safety`) and the last segment is the |
| 72 | +*function* (`temperature_safety_score`). |
| 73 | + |
| 74 | +### Running the sample |
| 75 | + |
| 76 | +`adk eval` resolves `code_config.name` by calling |
| 77 | +`importlib.import_module("temperature_safety")`, which searches `sys.path`. The |
| 78 | +metric module lives in this sample folder, which is not on `sys.path` by default, |
| 79 | +so put the folder on `PYTHONPATH` when you run the eval: |
| 80 | + |
| 81 | +```bash |
| 82 | +PYTHONPATH=contributing/samples/evaluation/custom_metric \ |
| 83 | +adk eval contributing/samples/evaluation/home_automation_agent \ |
| 84 | + contributing/samples/evaluation/custom_metric/home_automation.evalset.json \ |
| 85 | + --config_file_path contributing/samples/evaluation/custom_metric/eval_config.json \ |
| 86 | + --print_detailed_results |
| 87 | +``` |
| 88 | + |
| 89 | +Run it from the workspace root. Without the `PYTHONPATH` prefix you'll get |
| 90 | +`ImportError: Could not import custom metric function ...`. |
| 91 | + |
| 92 | +The shipped case sets the Bedroom to a valid 21 Celsius, so the metric scores |
| 93 | +1.0 (PASSED): |
| 94 | + |
| 95 | +``` |
| 96 | +custom_metric: |
| 97 | + Tests passed: 1 |
| 98 | + Tests failed: 0 |
| 99 | +... |
| 100 | +Metric: temperature_safety_score, Status: PASSED, Score: 1.0, Threshold: 1.0 |
| 101 | +``` |
| 102 | + |
| 103 | +An unsafe value (for example, `set_temperature(location="Bedroom", temperature=45)`) would score 0.0 (FAILED). We keep the agent well-behaved |
| 104 | +and demonstrate the passing path rather than forcing an unsafe call from live |
| 105 | +inference; the FAIL branch is exactly the `18 <= temperature <= 30` check in |
| 106 | +`temperature_safety.py`. |
| 107 | + |
| 108 | +## Related Guides |
| 109 | + |
| 110 | +- Evaluation overview: https://adk.dev/evaluate/ |
| 111 | +- Evaluation criteria reference: https://adk.dev/evaluate/criteria/ |
0 commit comments