Skip to content

Python: fix LocalEvaluator reporting zero-check items as passed - #7399

Open
CTWalk wants to merge 1 commit into
microsoft:mainfrom
CTWalk:maf-zero-check-eval
Open

Python: fix LocalEvaluator reporting zero-check items as passed#7399
CTWalk wants to merge 1 commit into
microsoft:mainfrom
CTWalk:maf-zero-check-eval

Conversation

@CTWalk

@CTWalk CTWalk commented Jul 29, 2026

Copy link
Copy Markdown

Motivation & Context

LocalEvaluator() with no checks reports every item as passed. The item
carries zero EvalScoreResult evidence, yet the run reports
{"passed": 1, "failed": 0, "errored": 0}, all_passed is True, and
raise_for_status() does not raise.

That combination is a silent false pass on exactly the surfaces a caller wires
into a quality gate. An empty check sequence is realistic when checks come from
configuration, optional plugins, or a filter that matches nothing.

.NET in this repository already treats this case as a failure:
AgentEvaluationResults.ItemPassed ends with return result.Metrics.Count > 0;
and LocalEvaluator_WithZeroChecks_ItemsHaveZeroMetricsAndFailAsync asserts
Passed == 0, Failed == 1 for an item with no metrics. This change brings
Python in line with that contract.

Description & Review Guide

  • What are the major changes?

    Two files. In LocalEvaluator.evaluate, item_passed is initialized from
    bool(check_results) instead of an unconditional True, so an item with no
    evaluated checks fails closed instead of inheriting vacuous truth from a loop
    that never runs. One regression test is added beside the existing local
    evaluator tests, and the LocalEvaluator class and evaluate() docstrings
    now state the zero-check case.

    No API, signature, dependency, or serialization change.

  • What is the impact of these changes?

    A caller running LocalEvaluator() with zero checks now gets
    passed=0, failed=1, all_passed is False, and a raising
    raise_for_status(). Behavior for any evaluator with at least one check is
    unchanged, because the initializer is equivalent to True whenever
    check_results is non-empty.

    This is an observable behavior change for the zero-check path. I have left
    "not a breaking change" checked, on the grounds that the affected callers are
    receiving a verdict with no evidence behind it and the evals surface is
    marked @experimental(feature_id=ExperimentalFeature.EVALS). If you would
    rather treat it as breaking, say so and I will add the label and title
    prefix.

  • What do you want reviewers to focus on?

    Whether fail-closed is the semantics you want on the Python side, or whether
    you would prefer an explicit error at construction/evaluation time for an
    evaluator with no checks. I chose to mirror the existing .NET guard rather
    than introduce a new exception type, but the alternative is reasonable and
    would be a larger change.

    Opened as a draft so the direction can be settled against real code. Say the
    word and I will mark it ready.

Related Issue

Fixes #7397

Small triage note: issue automation inferred .NET from the parity context
and added it alongside python; the label-prefix workflow then changed the
title to .NET: Python: [Bug]: .... I restored the Python-only title. This fix
changes only Python — .NET appears solely as the precedent being matched.
Could a maintainer remove the stale .NET label when convenient?

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.

Copilot AI review requested due to automatic review settings July 29, 2026 16:44
@CTWalk
CTWalk temporarily deployed to github-app-auth July 29, 2026 16:44 — with GitHub Actions Inactive
@CTWalk
CTWalk temporarily deployed to github-app-auth July 29, 2026 16:44 — with GitHub Actions Inactive
@CTWalk
CTWalk temporarily deployed to github-app-auth July 29, 2026 16:44 — with GitHub Actions Inactive
@agent-framework-automation agent-framework-automation Bot added the python Usage: [Issues, PRs], Target: Python label Jul 29, 2026
@CTWalk
CTWalk temporarily deployed to github-app-auth July 29, 2026 16:45 — with GitHub Actions Inactive

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes Python LocalEvaluator behavior so an item evaluated with zero checks fails closed (rather than being reported as passed with no score evidence), aligning Python semantics with the existing .NET contract and preventing silent false passes in quality gates.

Changes:

  • Initialize item_passed from bool(check_results) so zero-check evaluations are counted as failed.
  • Add a regression test asserting result counts, all_passed, empty scores, and raise_for_status() behavior for the zero-check case.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
python/packages/core/agent_framework/_evaluation.py Changes pass/fail aggregation to fail closed when no checks were evaluated.
python/packages/core/tests/core/test_local_eval.py Adds a regression test covering zero-check LocalEvaluator behavior.

Comment on lines 1575 to 1578
for item_idx, item in enumerate(items):
check_results = await asyncio.gather(*[_run_check(fn, item) for fn in self._checks])
item_passed = True
item_passed = bool(check_results)
item_scores: list[EvalScoreResult] = []

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — done. Both the LocalEvaluator class docstring and this evaluate() docstring now state that an item with no checks to evaluate fails.

@CTWalk

CTWalk commented Jul 29, 2026

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

@CTWalk
CTWalk force-pushed the maf-zero-check-eval branch from 1e56035 to 651339f Compare July 30, 2026 16:16
@CTWalk
CTWalk temporarily deployed to github-app-auth July 30, 2026 16:16 — with GitHub Actions Inactive
@CTWalk
CTWalk temporarily deployed to github-app-auth July 30, 2026 16:17 — with GitHub Actions Inactive
LocalEvaluator.evaluate initialized item_passed to True and only ever
cleared it inside the loop over check results. With no checks configured
the loop never runs, so an item with zero scores was recorded as passed:
result_counts reported one pass, all_passed was True, and
raise_for_status() did not raise.

Initialize item_passed from bool(check_results) so an item with no
evaluated checks fails closed. This matches the .NET contract in this
repository, where AgentEvaluationResults.ItemPassed ends with
'return result.Metrics.Count > 0' and is pinned by
LocalEvaluator_WithZeroChecks_ItemsHaveZeroMetricsAndFailAsync.

Add a focused regression covering the counts, all_passed, the empty
score list, and raise_for_status(). Update the LocalEvaluator class and
evaluate() docstrings, which previously described the pass rule without
the zero-check case.

Fixes microsoft#7397
@CTWalk
CTWalk force-pushed the maf-zero-check-eval branch from 651339f to a9c0a00 Compare July 30, 2026 16:29
@CTWalk
CTWalk temporarily deployed to github-app-auth July 30, 2026 16:29 — with GitHub Actions Inactive
@CTWalk
CTWalk marked this pull request as ready for review July 30, 2026 16:33
@CTWalk
CTWalk temporarily deployed to github-app-auth July 30, 2026 16:33 — with GitHub Actions Inactive
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: [Bug]: LocalEvaluator with zero checks reports items as passed

2 participants