Skip to content

Commit edfa259

Browse files
fix: use output for python faithfulness statements (#201)
## Summary - Fix a bug in Python `Faithfulness` where statements were extracted from `expected` instead of `output`, causing the scorer to evaluate the wrong text. - Update both sync and async `Faithfulness` paths in `py/autoevals/ragas.py` to route statement extraction through `output`. - Add a regression test in `py/autoevals/test_ragas.py` that uses mismatched `output`/`expected` and input-driven mocks so the final score depends on correct routing. ## Bug `Faithfulness` should score whether claims in the **generated answer** are supported by context. In Python, it incorrectly passed `expected` to statement extraction, so claims were taken from ground truth rather than model output. ## Fix - In `Faithfulness._run_eval_async(...)`: change `answer=expected` -> `answer=output`. - In `Faithfulness._run_eval_sync(...)`: change `answer=expected` -> `answer=output`. - Align sync required-field validation with async by requiring `output` as well. ## Test Added `test_faithfulness_extracts_statements_from_output`: - Uses different `output` and `expected`. - Mocks `extract_statements` to derive statements from passed `answer`. - Mocks `extract_faithfulness` to derive verdicts from context containment. - Ensures score behavior reflects correct routing (would fail under old bug). ## Validation - `uv run --extra dev --extra scipy pytest py/autoevals/test_ragas.py -k faithfulness_extracts_statements_from_output` passed. - Pre-commit hooks pass on commit. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent e541f54 commit edfa259

2 files changed

Lines changed: 49 additions & 5 deletions

File tree

py/autoevals/ragas.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -983,7 +983,7 @@ async def _run_eval_async(self, output, expected=None, input=None, context=None,
983983

984984
statements = (
985985
await aextract_statements(
986-
client=self.client, question=input, answer=expected, model=self.model, **self.extra_args
986+
client=self.client, question=input, answer=output, model=self.model, **self.extra_args
987987
)
988988
)["statements"]
989989

@@ -1003,12 +1003,10 @@ async def _run_eval_async(self, output, expected=None, input=None, context=None,
10031003
)
10041004

10051005
def _run_eval_sync(self, output, expected=None, input=None, context=None, **kwargs):
1006-
check_required("Faithfulness", input=input, context=context)
1006+
check_required("Faithfulness", input=input, output=output, context=context)
10071007

10081008
statements = (
1009-
extract_statements(
1010-
client=self.client, question=input, answer=expected, model=self.model, **self.extra_args
1011-
)
1009+
extract_statements(client=self.client, question=input, answer=output, model=self.model, **self.extra_args)
10121010
)["statements"]
10131011

10141012
faithfulness = (

py/autoevals/test_ragas.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from httpx import Response
77
from openai import OpenAI
88

9+
import autoevals.ragas as ragas_module
910
from autoevals import init
1011
from autoevals.ragas import *
1112

@@ -125,6 +126,51 @@ def test_context_relevancy_score_normal_case():
125126
assert result.score >= 0.0
126127

127128

129+
def test_faithfulness_extracts_statements_from_output(monkeypatch):
130+
"""Regression test for Faithfulness answer routing.
131+
132+
This verifies that Faithfulness extracts statements from ``output`` (the model
133+
answer being evaluated), not from ``expected`` (ground truth). The test uses
134+
mismatched ``output``/``expected`` values and mocked helpers that derive
135+
statements/verdicts from their inputs so the final score depends on which
136+
field was routed into statement extraction.
137+
"""
138+
captured_answer = None
139+
140+
def fake_extract_statements(question, answer, client=None, **extra_args):
141+
nonlocal captured_answer
142+
captured_answer = answer
143+
statement = answer.strip().rstrip(".")
144+
return {"statements": [statement]}
145+
146+
def fake_extract_faithfulness(context, statements, client=None, **extra_args):
147+
faithfulness = []
148+
for statement in statements:
149+
verdict = int(statement in context)
150+
faithfulness.append(
151+
{
152+
"statement": statement,
153+
"verdict": verdict,
154+
"reason": "Supported by context" if verdict else "Not found in context",
155+
}
156+
)
157+
return {"faithfulness": faithfulness}
158+
159+
monkeypatch.setattr(ragas_module, "extract_statements", fake_extract_statements)
160+
monkeypatch.setattr(ragas_module, "extract_faithfulness", fake_extract_faithfulness)
161+
162+
scorer = Faithfulness()
163+
score = scorer.eval(
164+
input="What is the capital of France?",
165+
output="Paris is the capital of France.",
166+
expected="Lyon is the capital of France.",
167+
context="Paris is the capital of France.",
168+
)
169+
170+
assert score.score == 1
171+
assert captured_answer == "Paris is the capital of France."
172+
173+
128174
@respx.mock
129175
def test_answer_correctness_uses_custom_embedding_model():
130176
"""Test that AnswerCorrectness passes embedding_model parameter through to embeddings API."""

0 commit comments

Comments
 (0)