Skip to content
This repository was archived by the owner on Jul 17, 2026. It is now read-only.

Commit 4625ab8

Browse files
fix: preserve evaluator result status
1 parent 580905c commit 4625ab8

5 files changed

Lines changed: 116 additions & 34 deletions

File tree

haystack/components/evaluators/context_relevance.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,17 +85,20 @@ class ContextRelevanceEvaluator(LLMEvaluator):
8585
print(result["results"])
8686
# [{
8787
# 'relevant_statements': ['Python, created by Guido van Rossum in the late 1980s.'],
88-
# 'score': 1.0
88+
# 'score': 1.0,
89+
# 'status': 'evaluated'
8990
# },
9091
# {
9192
# 'relevant_statements': ['The JVM has two primary functions: to allow Java programs to run on any device or
9293
# operating system (known as the "write once, run anywhere" principle), and to manage and
9394
# optimize program memory'],
94-
# 'score': 1.0
95+
# 'score': 1.0,
96+
# 'status': 'evaluated'
9597
# },
9698
# {
9799
# 'relevant_statements': [],
98-
# 'score': 0.0
100+
# 'score': 0.0,
101+
# 'status': 'evaluated'
99102
# }]
100103
```
101104
"""
@@ -171,7 +174,8 @@ def run(self, **inputs: Any) -> dict[str, Any]:
171174
:returns:
172175
A dictionary with the following outputs:
173176
- `score`: Mean context relevance score over all the provided input questions.
174-
- `results`: A list of dictionaries with `relevant_statements` and `score` for each input context.
177+
- `results`: A list of dictionaries with `relevant_statements`, `score`, and `status` for each input
178+
context. `status` is `evaluated` for valid results and `error` for failed evaluations.
175179
"""
176180
result = super(ContextRelevanceEvaluator, self).run(**inputs) # noqa: UP008
177181
# Post-process the raw results to calculate relevance metrics and scores
@@ -189,7 +193,8 @@ async def run_async(self, **inputs: Any) -> dict[str, Any]:
189193
:returns:
190194
A dictionary with the following outputs:
191195
- `score`: Mean context relevance score over all the provided input questions.
192-
- `results`: A list of dictionaries with `relevant_statements` and `score` for each input context.
196+
- `results`: A list of dictionaries with `relevant_statements`, `score`, and `status` for each input
197+
context. `status` is `evaluated` for valid results and `error` for failed evaluations.
193198
"""
194199
result = await super(ContextRelevanceEvaluator, self).run_async(**inputs) # noqa: UP008
195200
# Post-process the raw results to calculate relevance metrics and scores
@@ -209,8 +214,9 @@ def _postprocess_results(self, result: dict[str, Any]) -> dict[str, Any]:
209214
"""
210215
for idx, res in enumerate(result["results"]):
211216
if res is None:
212-
result["results"][idx] = {"relevant_statements": [], "score": float("nan")}
217+
result["results"][idx] = {"relevant_statements": [], "score": float("nan"), "status": "error"}
213218
continue
219+
res["status"] = "evaluated"
214220
if len(res["relevant_statements"]) > 0:
215221
res["score"] = 1
216222
else:

haystack/components/evaluators/faithfulness.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ class FaithfulnessEvaluator(LLMEvaluator):
8383
# 0.5
8484
print(result["results"])
8585
# [{'statements': ['Python is a high-level general-purpose programming language.',
86-
# 'Python was created by George Lucas.'], 'statement_scores': [1, 0], 'score': 0.5}]
86+
# 'Python was created by George Lucas.'], 'statement_scores': [1, 0], 'score': 0.5,
87+
# 'status': 'evaluated'}]
8788
```
8889
"""
8990

@@ -164,7 +165,8 @@ def run(self, **inputs: Any) -> dict[str, Any]:
164165
A dictionary with the following outputs:
165166
- `score`: Mean faithfulness score over all the provided input answers.
166167
- `individual_scores`: A list of faithfulness scores for each input answer.
167-
- `results`: A list of dictionaries with `statements` and `statement_scores` for each input answer.
168+
- `results`: A list of dictionaries with `statements`, `statement_scores`, `score`, and `status` for
169+
each input answer. `status` is `evaluated` for valid results and `error` for failed evaluations.
168170
"""
169171
result = super(FaithfulnessEvaluator, self).run(**inputs) # noqa: UP008
170172
# Post-process the raw results to calculate relevance metrics and scores
@@ -185,7 +187,8 @@ async def run_async(self, **inputs: Any) -> dict[str, Any]:
185187
A dictionary with the following outputs:
186188
- `score`: Mean faithfulness score over all the provided input answers.
187189
- `individual_scores`: A list of faithfulness scores for each input answer.
188-
- `results`: A list of dictionaries with `statements` and `statement_scores` for each input answer.
190+
- `results`: A list of dictionaries with `statements`, `statement_scores`, `score`, and `status` for
191+
each input answer. `status` is `evaluated` for valid results and `error` for failed evaluations.
189192
"""
190193
result = await super(FaithfulnessEvaluator, self).run_async(**inputs) # noqa: UP008
191194
# Post-process the raw results to calculate relevance metrics and scores
@@ -207,8 +210,14 @@ def _postprocess_results(self, result: dict[str, Any]) -> dict[str, Any]:
207210
# calculate average statement faithfulness score per query
208211
for idx, res in enumerate(result["results"]):
209212
if res is None:
210-
result["results"][idx] = {"statements": [], "statement_scores": [], "score": float("nan")}
213+
result["results"][idx] = {
214+
"statements": [],
215+
"statement_scores": [],
216+
"score": float("nan"),
217+
"status": "error",
218+
}
211219
continue
220+
res["status"] = "evaluated"
212221
if not res["statements"]:
213222
res["score"] = 0
214223
else:
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
enhancements:
3+
- |
4+
Adds a ``status`` field to each result produced by ``ContextRelevanceEvaluator`` and ``FaithfulnessEvaluator``.
5+
The field is ``evaluated`` for valid results and ``error`` for generation or parsing failures continued with
6+
``raise_on_failure=False``.

test/components/evaluators/test_context_relevance_evaluator.py

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,10 @@ def chat_generator_run(self, *args, **kwargs):
166166
results = component.run(questions=questions, contexts=contexts)
167167

168168
assert results == {
169-
"results": [{"score": 1, "relevant_statements": ["a", "b"]}, {"score": 0, "relevant_statements": []}],
169+
"results": [
170+
{"score": 1, "relevant_statements": ["a", "b"], "status": "evaluated"},
171+
{"score": 0, "relevant_statements": [], "status": "evaluated"},
172+
],
170173
"score": 0.5,
171174
"meta": None,
172175
"individual_scores": [1, 0],
@@ -195,7 +198,10 @@ def chat_generator_run(self, *args, **kwargs):
195198
]
196199
results = component.run(questions=questions, contexts=contexts)
197200
assert results == {
198-
"results": [{"score": 1, "relevant_statements": ["a", "b"]}, {"score": 0, "relevant_statements": []}],
201+
"results": [
202+
{"score": 1, "relevant_statements": ["a", "b"], "status": "evaluated"},
203+
{"score": 0, "relevant_statements": [], "status": "evaluated"},
204+
],
199205
"score": 0.5,
200206
"meta": None,
201207
"individual_scores": [1, 0],
@@ -207,13 +213,16 @@ def test_run_missing_parameters(self, monkeypatch):
207213
with pytest.raises(ValueError, match="LLM evaluator expected input parameter"):
208214
component.run()
209215

210-
def test_run_returns_nan_raise_on_failure_false(self, monkeypatch, caplog):
216+
@pytest.mark.parametrize("failure_mode", ["generation", "parsing"])
217+
def test_run_returns_nan_raise_on_failure_false(self, monkeypatch, caplog, failure_mode):
211218
monkeypatch.setenv("OPENAI_API_KEY", "test-api-key")
212219
component = ContextRelevanceEvaluator(raise_on_failure=False)
213220

214221
def chat_generator_run(self, *args, **kwargs):
215222
if "Python" in kwargs["messages"][0].text:
216-
raise Exception("OpenAI API request failed.")
223+
if failure_mode == "generation":
224+
raise Exception("OpenAI API request failed.")
225+
return {"replies": [ChatMessage.from_assistant("not valid JSON")]}
217226
return {"replies": [ChatMessage.from_assistant('{"relevant_statements": ["c", "d"], "score": 1}')]}
218227

219228
monkeypatch.setattr("haystack.components.evaluators.llm_evaluator.OpenAIChatGenerator.run", chat_generator_run)
@@ -236,12 +245,28 @@ def chat_generator_run(self, *args, **kwargs):
236245
results = component.run(questions=questions, contexts=contexts)
237246

238247
assert results["score"] == 1
239-
assert results["results"][0] == {"relevant_statements": ["c", "d"], "score": 1}
248+
assert results["results"][0] == {"relevant_statements": ["c", "d"], "score": 1, "status": "evaluated"}
240249
assert results["results"][1]["relevant_statements"] == []
241250
assert math.isnan(results["results"][1]["score"])
251+
assert results["results"][1]["status"] == "error"
242252

243253
assert "1 query(s) failed and were excluded from the score." in caplog.text
244254

255+
def test_run_all_failures_returns_nan_and_error_statuses(self, monkeypatch):
256+
monkeypatch.setenv("OPENAI_API_KEY", "test-api-key")
257+
component = ContextRelevanceEvaluator(raise_on_failure=False)
258+
259+
def chat_generator_run(self, *args, **kwargs):
260+
raise Exception("OpenAI API request failed.")
261+
262+
monkeypatch.setattr("haystack.components.evaluators.llm_evaluator.OpenAIChatGenerator.run", chat_generator_run)
263+
264+
results = component.run(questions=["q1", "q2"], contexts=[["c1"], ["c2"]])
265+
266+
assert math.isnan(results["score"])
267+
assert all(math.isnan(score) for score in results["individual_scores"])
268+
assert [result["status"] for result in results["results"]] == ["error", "error"]
269+
245270
@pytest.mark.skipif(
246271
not os.environ.get("OPENAI_API_KEY", None),
247272
reason="Export an env var called OPENAI_API_KEY containing the OpenAI API key to run this test.",
@@ -256,7 +281,7 @@ def test_live_run(self):
256281

257282
required_fields = {"results"}
258283
assert all(field in result for field in required_fields)
259-
nested_required_fields = {"score", "relevant_statements"}
284+
nested_required_fields = {"score", "relevant_statements", "status"}
260285
assert all(field in result["results"][0] for field in nested_required_fields)
261286

262287
assert "meta" in result
@@ -289,20 +314,26 @@ async def chat_generator_run_async(self, *args, **kwargs):
289314
results = await component.run_async(questions=questions, contexts=contexts)
290315

291316
assert results == {
292-
"results": [{"score": 1, "relevant_statements": ["a", "b"]}, {"score": 0, "relevant_statements": []}],
317+
"results": [
318+
{"score": 1, "relevant_statements": ["a", "b"], "status": "evaluated"},
319+
{"score": 0, "relevant_statements": [], "status": "evaluated"},
320+
],
293321
"score": 0.5,
294322
"meta": None,
295323
"individual_scores": [1, 0],
296324
}
297325

298326
@pytest.mark.asyncio
299-
async def test_run_async_returns_nan_raise_on_failure_false(self, monkeypatch, caplog):
327+
@pytest.mark.parametrize("failure_mode", ["generation", "parsing"])
328+
async def test_run_async_returns_nan_raise_on_failure_false(self, monkeypatch, caplog, failure_mode):
300329
monkeypatch.setenv("OPENAI_API_KEY", "test-api-key")
301330
component = ContextRelevanceEvaluator(raise_on_failure=False)
302331

303332
async def chat_generator_run_async(self, *args, **kwargs):
304333
if "Python" in kwargs["messages"][0].text:
305-
raise Exception("OpenAI API request failed.")
334+
if failure_mode == "generation":
335+
raise Exception("OpenAI API request failed.")
336+
return {"replies": [ChatMessage.from_assistant("not valid JSON")]}
306337
return {"replies": [ChatMessage.from_assistant('{"relevant_statements": ["c", "d"], "score": 1}')]}
307338

308339
monkeypatch.setattr(
@@ -316,9 +347,10 @@ async def chat_generator_run_async(self, *args, **kwargs):
316347
results = await component.run_async(questions=questions, contexts=contexts)
317348

318349
assert results["score"] == 1
319-
assert results["results"][0] == {"relevant_statements": ["c", "d"], "score": 1}
350+
assert results["results"][0] == {"relevant_statements": ["c", "d"], "score": 1, "status": "evaluated"}
320351
assert results["results"][1]["relevant_statements"] == []
321352
assert math.isnan(results["results"][1]["score"])
353+
assert results["results"][1]["status"] == "error"
322354

323355
assert "1 query(s) failed and were excluded from the score." in caplog.text
324356

@@ -337,7 +369,7 @@ async def test_live_run_async(self):
337369

338370
required_fields = {"results"}
339371
assert all(field in result for field in required_fields)
340-
nested_required_fields = {"score", "relevant_statements"}
372+
nested_required_fields = {"score", "relevant_statements", "status"}
341373
assert all(field in result["results"][0] for field in nested_required_fields)
342374

343375
assert "meta" in result

0 commit comments

Comments
 (0)