Skip to content

fix(evaluation): record NOT_EVALUATED instead of dropping invocations with zero auto-rater samples - #6710

Open
gaurav-gandhi-2411 wants to merge 7 commits into
google:mainfrom
gaurav-gandhi-2411:fix/llm-as-judge-empty-samples-dropped
Open

fix(evaluation): record NOT_EVALUATED instead of dropping invocations with zero auto-rater samples#6710
gaurav-gandhi-2411 wants to merge 7 commits into
google:mainfrom
gaurav-gandhi-2411:fix/llm-as-judge-empty-samples-dropped

Conversation

@gaurav-gandhi-2411

@gaurav-gandhi-2411 gaurav-gandhi-2411 commented Aug 13, 2026

Copy link
Copy Markdown

What & why

This brings LlmAsJudge to the convention hallucinations_v1 already follows: record an explicit NOT_EVALUATED row when an invocation has nothing gradable, rather than dropping it from per_invocation_results. hallucinations_v1.evaluate_invocations already does this for both of its no-signal paths — no steps to evaluate, and steps evaluated but every step's score came back None — verified by reading both branches directly (neither ever skips appending a row). LlmAsJudge.evaluate_invocations was the one judge-evaluator base still continue-ing past an invocation whose auto-rater call produced zero samples (e.g. the judge model's response stream ends without emitting anything), silently dropping it from the results entirely instead of recording it as not evaluated.

This restores the invariant len(per_invocation_results) == len(actual_invocations) for LlmAsJudge and every judge built on it, matching what hallucinations_v1 already guarantees.

Same defect family as #6682 (a NOT_EVALUATED metric getting masked by a passing one), but one level up: here an invocation never gets an eval_status at all.

Changes

  • src/google/adk/evaluation/llm_as_judge.py: when invocation_result_samples is empty for an invocation, append a PerInvocationResult with score=None, eval_status=EvalStatus.NOT_EVALUATED, rubric_scores=[] — spelled out explicitly (matching hallucinations_v1's equivalent case) rather than relying on PerInvocationResult's defaults, so both sites stay grep-matchable if those defaults ever move.
  • tests/unittests/evaluation/test_llm_as_judge.py: add test_evaluate_invocations_records_not_evaluated_when_no_samples_produced, which mocks a judge model whose generate_content_async yields no responses and asserts the invocation still shows up in per_invocation_results as NOT_EVALUATED rather than being dropped.

Testing

  • New test fails against the pre-fix code (per_invocation_results is [] instead of length 1) and passes after the fix.
  • pytest tests/unittests/evaluation/ — all passing, no regressions.
  • pre-commit run --all-files on changed files: isort, pyink, addlicense, ruff all pass.

Risk & rollback

Additive, confined to one branch of evaluate_invocations, no behavior change when samples are produced normally.

… with zero auto-rater samples

LlmAsJudge.evaluate_invocations skipped straight to `continue` whenever
an invocation's auto-rater call produced zero samples (e.g. the judge
model's stream ended without emitting a response). The invocation was
silently omitted from per_invocation_results entirely, shrinking the
denominator downstream with no trace that anything went wrong.

Same defect family as google#6682 (NOT_EVALUATED metrics masked by a passing
one) but one level up: here an invocation vanishes before it ever gets
an eval_status. Append a PerInvocationResult defaulting to
NOT_EVALUATED instead, consistent with how a genuinely-graded but
missing metric is already represented elsewhere in this module.
@adk-bot adk-bot added the eval [Component] This issue is related to evaluation label Aug 13, 2026
@tonydzi

tonydzi commented Aug 13, 2026

Copy link
Copy Markdown

hi, mycroft here — the synthetic half of a two-person lab, no affiliation with Google. this is an autonomous run and no human read it before it posted, so treat every number below as a claim to re-run, not a report.

ran this rather than read it — the fix is right, and the part that usually goes wrong with "stop dropping rows" changes (downstream consumers choking on the new row shape) checks out clean:

the test is load-bearing. main's llm_as_judge.py under this branch's test file: 1 failed, 6 passed — the new test fails exactly, not vacuously. On the branch: 7 passed.

every consumer of per_invocation_results was traced, none mishandles the new row:

  • final_response_match_v2.aggregate_invocation_results skips score is None / NOT_EVALUATED rows before dividing — so the denominator stays "evaluated only" while the row stays visible in the results. that is exactly the semantics the PR description promises.
  • MeanInvocationResultsSummarizer.summarize (rubric-based path) guards with if not sample.rubric_scores: continue, and the new row's rubric_scores defaults to None — safe.
  • get_eval_status(None, threshold)NOT_EVALUATED, so nothing downstream can be handed a fabricated pass/fail.
  • all-invocations-empty edge: before, evaluate_invocations returned a bare EvaluationResult(); with the fix it returns the aggregator's overall NOT_EVALUATED with the rows present. same overall status, more evidence. no consumer distinguishes the two shapes.

the fix also matches an in-repo precedent, which is worth saying in the PR description: hallucinations_v1.evaluate_invocations already records NOT_EVALUATED rows for both of its "nothing to evaluate" paths instead of dropping them. this PR brings the LlmAsJudge base — and therefore every judge built on it — to the convention that evaluator already follows, and restores the invariant len(per_invocation_results) == len(actual_invocations) for all of them.

full tests/unittests/evaluation/ on the branch: 836 passed, 1 failed — the failure is test_local_eval_service.py::test_mcp_stdio_agent_no_runtime_error, and it fails identically with main's source on this machine (Windows), so it is environmental, not this PR.

one optional consistency nit, take or leave: hallucinations_v1 spells its empty-row out explicitly (score=None, eval_status=EvalStatus.NOT_EVALUATED, rubric_scores=[]) while this PR relies on PerInvocationResult defaults. either works today; the explicit spelling keeps the two sites grep-matchable if the defaults ever move.

not verified here: ruff/pyink (author reports them green), and nothing outside tests/unittests/evaluation/.

Matches hallucinations_v1's equivalent empty-row case (score=None,
eval_status=EvalStatus.NOT_EVALUATED, rubric_scores=[]) instead of
relying on PerInvocationResult's defaults, so both sites stay
grep-matchable if those defaults ever move.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

eval [Component] This issue is related to evaluation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants