Coverage for haystack/components/evaluators/faithfulness.py: 100%

50 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-21 13:53 +0000

1# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai> 

2# 

3# SPDX-License-Identifier: Apache-2.0 

4 

5import math 

6from typing import Any 

7 

8from numpy import mean as np_mean 

9 

10from haystack import component, default_from_dict, default_to_dict, logging 

11from haystack.components.evaluators.llm_evaluator import LLMEvaluator 

12from haystack.components.generators.chat.types import ChatGenerator 

13from haystack.core.serialization import component_to_dict 

14from haystack.utils import deserialize_chatgenerator_inplace 

15 

16logger = logging.getLogger(__name__) 

17 

18# Default examples to include in the prompt if the user does not provide any examples 

19_DEFAULT_EXAMPLES = [ 

20 { 

21 "inputs": { 

22 "questions": "What is the capital of Germany and when was it founded?", 

23 "contexts": ["Berlin is the capital of Germany and was founded in 1244."], 

24 "predicted_answers": "The capital of Germany, Berlin, was founded in the 13th century.", 

25 }, 

26 "outputs": { 

27 "statements": ["Berlin is the capital of Germany.", "Berlin was founded in 1244."], 

28 "statement_scores": [1, 1], 

29 }, 

30 }, 

31 { 

32 "inputs": { 

33 "questions": "What is the capital of France?", 

34 "contexts": ["Berlin is the capital of Germany."], 

35 "predicted_answers": "Paris", 

36 }, 

37 "outputs": {"statements": ["Paris is the capital of France."], "statement_scores": [0]}, 

38 }, 

39 { 

40 "inputs": { 

41 "questions": "What is the capital of Italy?", 

42 "contexts": ["Rome is the capital of Italy."], 

43 "predicted_answers": "Rome is the capital of Italy with more than 4 million inhabitants.", 

44 }, 

45 "outputs": { 

46 "statements": ["Rome is the capital of Italy.", "Rome has more than 4 million inhabitants."], 

47 "statement_scores": [1, 0], 

48 }, 

49 }, 

50] 

51 

52 

53@component 

54class FaithfulnessEvaluator(LLMEvaluator): 

55 """ 

56 Evaluator that checks if a generated answer can be inferred from the provided contexts. 

57 

58 An LLM separates the answer into multiple statements and checks whether the statement can be inferred from the 

59 context or not. The final score for the full answer is a number from 0.0 to 1.0. It represents the proportion of 

60 statements that can be inferred from the provided contexts. 

61 

62 Usage example: 

63 ```python 

64 from haystack.components.evaluators import FaithfulnessEvaluator 

65 

66 questions = ["Who created the Python language?"] 

67 contexts = [ 

68 [( 

69 "Python, created by Guido van Rossum in the late 1980s, is a high-level general-purpose programming " 

70 "language. Its design philosophy emphasizes code readability, and its language constructs aim to help " 

71 "programmers write clear, logical code for both small and large-scale software projects." 

72 )], 

73 ] 

74 predicted_answers = [ 

75 "Python is a high-level general-purpose programming language that was created by George Lucas." 

76 ] 

77 evaluator = FaithfulnessEvaluator() 

78 result = evaluator.run(questions=questions, contexts=contexts, predicted_answers=predicted_answers) 

79 

80 print(result["individual_scores"]) 

81 # [0.5] 

82 print(result["score"]) 

83 # 0.5 

84 print(result["results"]) 

85 # [{'statements': ['Python is a high-level general-purpose programming language.', 

86 # 'Python was created by George Lucas.'], 'statement_scores': [1, 0], 'score': 0.5, 

87 # 'status': 'evaluated'}] 

88 ``` 

89 """ 

90 

91 def __init__( 

92 self, 

93 examples: list[dict[str, Any]] | None = None, 

94 progress_bar: bool = True, 

95 raise_on_failure: bool = True, 

96 chat_generator: ChatGenerator | None = None, 

97 ) -> None: 

98 """ 

99 Creates an instance of FaithfulnessEvaluator. 

100 

101 If no LLM is specified using the `chat_generator` parameter, the component will use OpenAI in JSON mode. 

102 

103 :param examples: 

104 Optional few-shot examples conforming to the expected input and output format of FaithfulnessEvaluator. 

105 Default examples will be used if none are provided. 

106 Each example must be a dictionary with keys "inputs" and "outputs". 

107 "inputs" must be a dictionary with keys "questions", "contexts", and "predicted_answers". 

108 "outputs" must be a dictionary with "statements" and "statement_scores". 

109 Expected format: 

110 ```python 

111 [{ 

112 "inputs": { 

113 "questions": "What is the capital of Italy?", "contexts": ["Rome is the capital of Italy."], 

114 "predicted_answers": "Rome is the capital of Italy with more than 4 million inhabitants.", 

115 }, 

116 "outputs": { 

117 "statements": ["Rome is the capital of Italy.", "Rome has more than 4 million inhabitants."], 

118 "statement_scores": [1, 0], 

119 }, 

120 }] 

121 ``` 

122 :param progress_bar: 

123 Whether to show a progress bar during the evaluation. 

124 :param raise_on_failure: 

125 Whether to raise an exception if the API call fails. 

126 :param chat_generator: 

127 a ChatGenerator instance which represents the LLM. 

128 In order for the component to work, the LLM should be configured to return a JSON object. For example, 

129 when using the OpenAIChatGenerator, you should pass `{"response_format": {"type": "json_object"}}` in the 

130 `generation_kwargs`. 

131 """ 

132 self.instructions = ( 

133 "Your task is to judge the faithfulness or groundedness of statements based " 

134 "on context information. First, please extract statements from a provided " 

135 "predicted answer to a question. Second, calculate a faithfulness score for each " 

136 "statement made in the predicted answer. The score is 1 if the statement can be " 

137 "inferred from the provided context or 0 if it cannot be inferred." 

138 ) 

139 self.inputs = [("questions", list[str]), ("contexts", list[list[str]]), ("predicted_answers", list[str])] 

140 self.outputs = ["statements", "statement_scores"] 

141 self.examples = examples or _DEFAULT_EXAMPLES 

142 

143 super(FaithfulnessEvaluator, self).__init__( # noqa: UP008 

144 instructions=self.instructions, 

145 inputs=self.inputs, 

146 outputs=self.outputs, 

147 examples=self.examples, 

148 chat_generator=chat_generator, 

149 raise_on_failure=raise_on_failure, 

150 progress_bar=progress_bar, 

151 ) 

152 

153 @component.output_types( 

154 individual_scores=list[float], score=float, results=list[dict[str, Any]], meta=list[dict[str, Any]] | None 

155 ) 

156 def run(self, **inputs: Any) -> dict[str, Any]: 

157 """ 

158 Run the LLM evaluator. 

159 

160 :param questions: 

161 A list of questions. 

162 :param contexts: 

163 A nested list of contexts that correspond to the questions. 

164 :param predicted_answers: 

165 A list of predicted answers. 

166 :returns: 

167 A dictionary with the following outputs: 

168 - `score`: Mean faithfulness score over all the provided input answers. 

169 - `individual_scores`: A list of faithfulness scores for each input answer. 

170 - `results`: A list of dictionaries with `statements`, `statement_scores`, `score`, and `status` for 

171 each input answer. `status` is `evaluated` for valid results and `error` for failed evaluations. 

172 """ 

173 result = super(FaithfulnessEvaluator, self).run(**inputs) # noqa: UP008 

174 # Post-process the raw results to calculate relevance metrics and scores 

175 return self._postprocess_results(result) 

176 

177 @component.output_types( 

178 individual_scores=list[float], score=float, results=list[dict[str, Any]], meta=list[dict[str, Any]] | None 

179 ) 

180 async def run_async(self, **inputs: Any) -> dict[str, Any]: 

181 """ 

182 Run the LLM evaluator asynchronously. 

183 

184 :param questions: 

185 A list of questions. 

186 :param contexts: 

187 A nested list of contexts that correspond to the questions. 

188 :param predicted_answers: 

189 A list of predicted answers. 

190 :returns: 

191 A dictionary with the following outputs: 

192 - `score`: Mean faithfulness score over all the provided input answers. 

193 - `individual_scores`: A list of faithfulness scores for each input answer. 

194 - `results`: A list of dictionaries with `statements`, `statement_scores`, `score`, and `status` for 

195 each input answer. `status` is `evaluated` for valid results and `error` for failed evaluations. 

196 """ 

197 result = await super(FaithfulnessEvaluator, self).run_async(**inputs) # noqa: UP008 

198 # Post-process the raw results to calculate relevance metrics and scores 

199 return self._postprocess_results(result) 

200 

201 def _postprocess_results(self, result: dict[str, Any]) -> dict[str, Any]: 

202 """ 

203 Post-processes raw LLM evaluator outputs to compute faithfulness scores. 

204 

205 Calculates statement-level score averages, computes the overall mean faithfulness 

206 score across successful queries, and updates the result payload. 

207 

208 :param result: 

209 The raw evaluation dictionary from the base LLM evaluator. 

210 :returns: 

211 The updated dictionary containing final scores and tracking metrics. 

212 """ 

213 

214 # calculate average statement faithfulness score per query 

215 for idx, res in enumerate(result["results"]): 

216 if res is None: 

217 result["results"][idx] = { 

218 "statements": [], 

219 "statement_scores": [], 

220 "score": float("nan"), 

221 "status": "error", 

222 } 

223 continue 

224 res["status"] = "evaluated" 

225 if not res["statements"]: 

226 res["score"] = 0 

227 else: 

228 res["score"] = np_mean(res["statement_scores"]) 

229 

230 # calculate average answer faithfulness score over all queries 

231 scores = [res["score"] for res in result["results"]] 

232 valid_scores = [s for s in scores if not math.isnan(s)] 

233 skipped = len(scores) - len(valid_scores) 

234 if skipped: 

235 logger.warning("{skipped} query(s) failed and were excluded from the score.", skipped=skipped) 

236 result["score"] = np_mean(valid_scores) if valid_scores else float("nan") 

237 result["individual_scores"] = scores 

238 

239 return result 

240 

241 def to_dict(self) -> dict[str, Any]: 

242 """ 

243 Serialize this component to a dictionary. 

244 

245 :returns: 

246 A dictionary with serialized data. 

247 """ 

248 return default_to_dict( 

249 self, 

250 chat_generator=component_to_dict(obj=self._chat_generator, name="chat_generator"), 

251 examples=self.examples, 

252 progress_bar=self.progress_bar, 

253 raise_on_failure=self.raise_on_failure, 

254 ) 

255 

256 @classmethod 

257 def from_dict(cls, data: dict[str, Any]) -> "FaithfulnessEvaluator": 

258 """ 

259 Deserialize this component from a dictionary. 

260 

261 :param data: 

262 The dictionary representation of this component. 

263 :returns: 

264 The deserialized component instance. 

265 """ 

266 if data["init_parameters"].get("chat_generator"): 

267 deserialize_chatgenerator_inplace(data["init_parameters"], key="chat_generator") 

268 return default_from_dict(cls, data)