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

Commit 95b8f0a

Browse files
fix: preserve evaluator result status
1 parent ecf627b commit 95b8f0a

7 files changed

Lines changed: 116 additions & 70 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:

haystack/components/evaluators/llm_evaluator.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ def validate_init_parameters(
198198
)
199199
raise ValueError(msg)
200200

201-
@component.output_types(results=list[dict[str, Any]], evaluation_statuses=list[str])
201+
@component.output_types(results=list[dict[str, Any]])
202202
def run(self, **inputs: Any) -> dict[str, Any]:
203203
"""
204204
Run the LLM evaluator.
@@ -226,7 +226,6 @@ def run(self, **inputs: Any) -> dict[str, Any]:
226226
list_of_input_names_to_values = [dict(zip(input_names, v, strict=True)) for v in values]
227227

228228
results: list[dict[str, Any] | None] = []
229-
evaluation_statuses: list[str] = []
230229
metadata = []
231230
errors = 0
232231
for input_names_to_values in tqdm(list_of_input_names_to_values, disable=not self.progress_bar):
@@ -239,7 +238,6 @@ def run(self, **inputs: Any) -> dict[str, Any]:
239238
raise ValueError(f"Error while generating response for prompt: {prompt}. Error: {e}") from e
240239
logger.warning("Error while generating response for prompt: {prompt}. Error: {e}", prompt=prompt, e=e)
241240
results.append(None)
242-
evaluation_statuses.append("error")
243241
errors += 1
244242
continue
245243

@@ -248,11 +246,9 @@ def run(self, **inputs: Any) -> dict[str, Any]:
248246
)
249247
if parsed_result is None:
250248
results.append(None)
251-
evaluation_statuses.append("error")
252249
errors += 1
253250
else:
254251
results.append(parsed_result)
255-
evaluation_statuses.append("evaluated")
256252

257253
if result["replies"][0].meta:
258254
metadata.append(result["replies"][0].meta)
@@ -264,7 +260,7 @@ def run(self, **inputs: Any) -> dict[str, Any]:
264260
len=len(list_of_input_names_to_values),
265261
)
266262

267-
return {"results": results, "meta": metadata or None, "evaluation_statuses": evaluation_statuses}
263+
return {"results": results, "meta": metadata or None}
268264

269265
@component.output_types(results=list[dict[str, Any]])
270266
async def run_async(self, **inputs: Any) -> dict[str, Any]:
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
---
22
enhancements:
33
- |
4-
Adds an ``evaluation_statuses`` output to ``LLMEvaluator`` results so each evaluated row can report whether it was successfully evaluated or failed during generation/parsing when ``raise_on_failure=False`` is used.
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 & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -166,11 +166,13 @@ 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],
173-
"evaluation_statuses": ["evaluated", "evaluated"],
174176
}
175177

176178
def test_run_no_statements_extracted(self, monkeypatch):
@@ -196,11 +198,13 @@ def chat_generator_run(self, *args, **kwargs):
196198
]
197199
results = component.run(questions=questions, contexts=contexts)
198200
assert results == {
199-
"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+
],
200205
"score": 0.5,
201206
"meta": None,
202207
"individual_scores": [1, 0],
203-
"evaluation_statuses": ["evaluated", "evaluated"],
204208
}
205209

206210
def test_run_missing_parameters(self, monkeypatch):
@@ -209,13 +213,16 @@ def test_run_missing_parameters(self, monkeypatch):
209213
with pytest.raises(ValueError, match="LLM evaluator expected input parameter"):
210214
component.run()
211215

212-
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):
213218
monkeypatch.setenv("OPENAI_API_KEY", "test-api-key")
214219
component = ContextRelevanceEvaluator(raise_on_failure=False)
215220

216221
def chat_generator_run(self, *args, **kwargs):
217222
if "Python" in kwargs["messages"][0].text:
218-
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")]}
219226
return {"replies": [ChatMessage.from_assistant('{"relevant_statements": ["c", "d"], "score": 1}')]}
220227

221228
monkeypatch.setattr("haystack.components.evaluators.llm_evaluator.OpenAIChatGenerator.run", chat_generator_run)
@@ -238,13 +245,28 @@ def chat_generator_run(self, *args, **kwargs):
238245
results = component.run(questions=questions, contexts=contexts)
239246

240247
assert results["score"] == 1
241-
assert results["results"][0] == {"relevant_statements": ["c", "d"], "score": 1}
248+
assert results["results"][0] == {"relevant_statements": ["c", "d"], "score": 1, "status": "evaluated"}
242249
assert results["results"][1]["relevant_statements"] == []
243250
assert math.isnan(results["results"][1]["score"])
244-
assert results["evaluation_statuses"] == ["evaluated", "error"]
251+
assert results["results"][1]["status"] == "error"
245252

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

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+
248270
@pytest.mark.skipif(
249271
not os.environ.get("OPENAI_API_KEY", None),
250272
reason="Export an env var called OPENAI_API_KEY containing the OpenAI API key to run this test.",
@@ -259,7 +281,7 @@ def test_live_run(self):
259281

260282
required_fields = {"results"}
261283
assert all(field in result for field in required_fields)
262-
nested_required_fields = {"score", "relevant_statements"}
284+
nested_required_fields = {"score", "relevant_statements", "status"}
263285
assert all(field in result["results"][0] for field in nested_required_fields)
264286

265287
assert "meta" in result
@@ -292,20 +314,26 @@ async def chat_generator_run_async(self, *args, **kwargs):
292314
results = await component.run_async(questions=questions, contexts=contexts)
293315

294316
assert results == {
295-
"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+
],
296321
"score": 0.5,
297322
"meta": None,
298323
"individual_scores": [1, 0],
299324
}
300325

301326
@pytest.mark.asyncio
302-
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):
303329
monkeypatch.setenv("OPENAI_API_KEY", "test-api-key")
304330
component = ContextRelevanceEvaluator(raise_on_failure=False)
305331

306332
async def chat_generator_run_async(self, *args, **kwargs):
307333
if "Python" in kwargs["messages"][0].text:
308-
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")]}
309337
return {"replies": [ChatMessage.from_assistant('{"relevant_statements": ["c", "d"], "score": 1}')]}
310338

311339
monkeypatch.setattr(
@@ -319,9 +347,10 @@ async def chat_generator_run_async(self, *args, **kwargs):
319347
results = await component.run_async(questions=questions, contexts=contexts)
320348

321349
assert results["score"] == 1
322-
assert results["results"][0] == {"relevant_statements": ["c", "d"], "score": 1}
350+
assert results["results"][0] == {"relevant_statements": ["c", "d"], "score": 1, "status": "evaluated"}
323351
assert results["results"][1]["relevant_statements"] == []
324352
assert math.isnan(results["results"][1]["score"])
353+
assert results["results"][1]["status"] == "error"
325354

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

@@ -340,7 +369,7 @@ async def test_live_run_async(self):
340369

341370
required_fields = {"results"}
342371
assert all(field in result for field in required_fields)
343-
nested_required_fields = {"score", "relevant_statements"}
372+
nested_required_fields = {"score", "relevant_statements", "status"}
344373
assert all(field in result["results"][0] for field in nested_required_fields)
345374

346375
assert "meta" in result

0 commit comments

Comments
 (0)