#3283·deepeval

Verbose judge verdicts are scored as pass in role_violation, prompt_alignment, conversation_completeness

Author: arian-goganiCreated Sep 14, 2026Updated Sep 14, 2026

What happens

Three metrics declare verdict: str rather than Literal["yes", "no"], compare it with an exact match, and fall through to the permissive branch when it does not match. A judge that answers in a sentence rather than a bare token is scored as no problem found.

deepeval/metrics/role_violation/role_violation.py:285-295:

python
for verdict in self.verdicts:
    if verdict.verdict.strip().lower() == "yes":
        return 0.0  # Role violation detected - no adherence
return 1.0  # No role violation - full adherence

Running that function as shipped, with its body lifted out of the file rather than retyped:

judge verdict string                                  score   meaning
  'yes'                                              0.0   VIOLATION CAUGHT
  'Yes'                                              0.0   VIOLATION CAUGHT
  'YES'                                              0.0   VIOLATION CAUGHT
  ' yes '                                            0.0   VIOLATION CAUGHT
  'Yes, the model violated its assigned role.'       1.0   scored as full adherence
  'yes - the assistant claimed to be a human doctor' 1.0   scored as full adherence
  'no'                                               1.0   scored as full adherence
  'No, no violation found.'                          1.0   scored as full adherence

.strip().lower() handles casing and whitespace, which is what makes this easy to miss: the obvious variants are covered, and only the verbose answer slips. A judge that explains its reasoning in the same field is read as reporting no violation.

The three affected metrics

metric schema comparison permissive when unmatched
role_violation verdict: str == "yes" scores 1.0, full adherence
prompt_alignment verdict: str == "no" counted as aligned
conversation_completeness verdict: str == "no" counted as complete

toxicity, bias and hallucination declare verdict: Literal["yes", "no"], so pydantic rejects a verbose verdict there and the failure is loud. The three above are the ones typed as bare str.

deepeval already ships the fix

deepeval/metrics/utils.py:514 has verdict_from_json, and its docstring describes this exact failure:

Judges don't always honor a strict yes/no instruction, a non-native model may reply "No, it's off-topic". This normalizes the verbose reply to its leading word and drops anything outside allowed to None (so the caller can exclude it from scoring) rather than crashing a Literal schema or silently miscounting it.

"Silently miscounting it" is what the three metrics above do. A code search for verdict_from_json returns two files: the definition, and one consumer, turn_relevancy.py.

So this is not a design question. The correct parser exists, is documented, and is called by one metric.

One correction to the existing record

Issue #3098 reports this shape on HallucinationMetric. That metric is Literal["yes","no"], so a verbose verdict raises a pydantic ValidationError there rather than being miscounted. The behaviour is worth fixing either way, but the honest description is that it fails loudly rather than silently, and the metrics that fail quietly are the three listed above, which no open issue covers.

Suggested fix

Route the three through verdict_from_json with allowed=("yes","no"), which is what it was written for, and decide explicitly what a None verdict should do. Excluding it from scoring is the documented intent; treating it as the unsafe value would be stricter and also defensible. Either beats the current behaviour, which is to treat every unrecognised answer as the safe one without recording that it was unrecognised.

Tightening the three schemas to Literal["yes","no"] would also surface the problem, but as a crash at parse time rather than a graceful exclusion, which is why the util exists.

Verification

Read against main on 2026-09-14. The transcript is _calculate_score executed with its body taken directly from the file, so the logic under test is the shipped logic. I did not drive it through measure() with an installed package and a live judge, so if you want that confirmed before acting, that is the gap.