nace-haystack 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- haystack_integrations/common/nace/__init__.py +3 -0
- haystack_integrations/common/nace/client.py +171 -0
- haystack_integrations/common/nace/concurrency.py +49 -0
- haystack_integrations/common/nace/errors.py +138 -0
- haystack_integrations/common/nace/images.py +204 -0
- haystack_integrations/common/nace/request.py +144 -0
- haystack_integrations/common/py.typed +0 -0
- haystack_integrations/components/classifiers/nace/__init__.py +7 -0
- haystack_integrations/components/classifiers/nace/document_classifier.py +390 -0
- haystack_integrations/components/classifiers/py.typed +0 -0
- haystack_integrations/components/evaluators/nace/__init__.py +13 -0
- haystack_integrations/components/evaluators/nace/_base.py +247 -0
- haystack_integrations/components/evaluators/nace/context_relevance.py +139 -0
- haystack_integrations/components/evaluators/nace/faithfulness.py +169 -0
- haystack_integrations/components/evaluators/py.typed +0 -0
- haystack_integrations/components/routers/nace/__init__.py +7 -0
- haystack_integrations/components/routers/nace/text_router.py +316 -0
- haystack_integrations/components/routers/py.typed +0 -0
- nace_haystack-0.1.0.dist-info/METADATA +230 -0
- nace_haystack-0.1.0.dist-info/RECORD +22 -0
- nace_haystack-0.1.0.dist-info/WHEEL +4 -0
- nace_haystack-0.1.0.dist-info/licenses/LICENSE.txt +201 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026-present Nace AI
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import math
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import nace_sdk
|
|
10
|
+
from haystack import default_to_dict, logging
|
|
11
|
+
from haystack.utils import Secret
|
|
12
|
+
|
|
13
|
+
from haystack_integrations.common.nace.client import (
|
|
14
|
+
client_options,
|
|
15
|
+
is_openrouter,
|
|
16
|
+
make_async_client,
|
|
17
|
+
make_client,
|
|
18
|
+
provider_name,
|
|
19
|
+
resolve_base_url,
|
|
20
|
+
)
|
|
21
|
+
from haystack_integrations.common.nace.concurrency import map_in_threads
|
|
22
|
+
from haystack_integrations.common.nace.errors import readable_error
|
|
23
|
+
from haystack_integrations.common.nace.images import ImageInput, image_types
|
|
24
|
+
from haystack_integrations.common.nace.request import DrexResponse, build_call
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
_QUESTION_ID = "verdict"
|
|
29
|
+
|
|
30
|
+
Outcome = tuple[dict[str, Any], dict[str, Any]]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class _NaceEvaluator:
|
|
34
|
+
"""
|
|
35
|
+
Shared client lifecycle and evaluation loop for the Drex evaluators.
|
|
36
|
+
|
|
37
|
+
Each item is judged in its own request with one `noul` question; its score is Drex's probability that the
|
|
38
|
+
statement in `instructions` is true. The outputs match Haystack's built-in LLM evaluators.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
*,
|
|
44
|
+
instructions: str,
|
|
45
|
+
model: str,
|
|
46
|
+
api_key: Secret,
|
|
47
|
+
api_base_url: str | None,
|
|
48
|
+
timeout: float | None,
|
|
49
|
+
max_retries: int | None,
|
|
50
|
+
max_workers: int,
|
|
51
|
+
raise_on_failure: bool,
|
|
52
|
+
) -> None:
|
|
53
|
+
if max_workers < 1:
|
|
54
|
+
msg = f"`max_workers` must be at least 1, got {max_workers}."
|
|
55
|
+
raise ValueError(msg)
|
|
56
|
+
self.instructions = instructions
|
|
57
|
+
self.model = model
|
|
58
|
+
self.api_key = api_key
|
|
59
|
+
self.api_base_url = api_base_url
|
|
60
|
+
self.timeout = timeout
|
|
61
|
+
self.max_retries = max_retries
|
|
62
|
+
self.max_workers = max_workers
|
|
63
|
+
self.raise_on_failure = raise_on_failure
|
|
64
|
+
self._client: nace_sdk.NaceClient | None = None
|
|
65
|
+
self._async_client: nace_sdk.AsyncNaceClient | None = None
|
|
66
|
+
self._async_client_loop: asyncio.AbstractEventLoop | None = None
|
|
67
|
+
|
|
68
|
+
def _client_options(self) -> dict[str, Any]:
|
|
69
|
+
return client_options(self.api_key, self.api_base_url, self.timeout, self.max_retries)
|
|
70
|
+
|
|
71
|
+
def warm_up(self) -> None:
|
|
72
|
+
"""
|
|
73
|
+
Creates the synchronous Nace client.
|
|
74
|
+
"""
|
|
75
|
+
if self._client is None:
|
|
76
|
+
self._client = make_client(self._client_options())
|
|
77
|
+
|
|
78
|
+
async def warm_up_async(self) -> None:
|
|
79
|
+
"""
|
|
80
|
+
Creates the asynchronous Nace client for the running event loop.
|
|
81
|
+
"""
|
|
82
|
+
loop = asyncio.get_running_loop()
|
|
83
|
+
if self._async_client_loop is not None and self._async_client_loop is not loop:
|
|
84
|
+
# An async HTTP client belongs to the event loop it first ran on, so a new `asyncio.run` (a second
|
|
85
|
+
# notebook cell, a second script run) needs a new client. The old one can't be closed from this loop.
|
|
86
|
+
self._async_client = None
|
|
87
|
+
if self._async_client is None:
|
|
88
|
+
self._async_client = make_async_client(self._client_options())
|
|
89
|
+
self._async_client_loop = loop
|
|
90
|
+
|
|
91
|
+
def close(self) -> None:
|
|
92
|
+
"""
|
|
93
|
+
Releases the synchronous Nace client.
|
|
94
|
+
"""
|
|
95
|
+
if self._client is not None:
|
|
96
|
+
self._client.close()
|
|
97
|
+
self._client = None
|
|
98
|
+
|
|
99
|
+
async def close_async(self) -> None:
|
|
100
|
+
"""
|
|
101
|
+
Releases the asynchronous Nace client.
|
|
102
|
+
"""
|
|
103
|
+
if self._async_client is not None:
|
|
104
|
+
await self._async_client.aclose()
|
|
105
|
+
self._async_client = None
|
|
106
|
+
self._async_client_loop = None
|
|
107
|
+
|
|
108
|
+
def to_dict(self) -> dict[str, Any]:
|
|
109
|
+
"""
|
|
110
|
+
Serializes the component to a dictionary.
|
|
111
|
+
|
|
112
|
+
:returns:
|
|
113
|
+
Dictionary with serialized data.
|
|
114
|
+
"""
|
|
115
|
+
return default_to_dict(
|
|
116
|
+
self,
|
|
117
|
+
instructions=self.instructions,
|
|
118
|
+
model=self.model,
|
|
119
|
+
api_key=self.api_key,
|
|
120
|
+
api_base_url=self.api_base_url,
|
|
121
|
+
timeout=self.timeout,
|
|
122
|
+
max_retries=self.max_retries,
|
|
123
|
+
max_workers=self.max_workers,
|
|
124
|
+
raise_on_failure=self.raise_on_failure,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
def _check_contexts(contexts: list[list[str]]) -> None:
|
|
129
|
+
for index, context in enumerate(contexts):
|
|
130
|
+
if isinstance(context, str):
|
|
131
|
+
msg = (
|
|
132
|
+
"`contexts` must be a list of lists of strings, one list per question; "
|
|
133
|
+
f"item {index} is a single string. Wrap it in a list: [[...]]."
|
|
134
|
+
)
|
|
135
|
+
raise ValueError(msg)
|
|
136
|
+
|
|
137
|
+
@staticmethod
|
|
138
|
+
def _check_images(images: list[list[ImageInput]] | None) -> None:
|
|
139
|
+
for index, item_images in enumerate(images or ()):
|
|
140
|
+
if not isinstance(item_images, (list, tuple)):
|
|
141
|
+
msg = (
|
|
142
|
+
"`images` must hold one list of images per item, such as [[image], []]; "
|
|
143
|
+
f"item {index} is a single {type(item_images).__name__}. Wrap it in a list."
|
|
144
|
+
)
|
|
145
|
+
raise ValueError(msg)
|
|
146
|
+
|
|
147
|
+
@staticmethod
|
|
148
|
+
def _check_lengths(inputs: dict[str, list[Any] | None]) -> None:
|
|
149
|
+
lengths = {name: len(values) for name, values in inputs.items() if values is not None}
|
|
150
|
+
if len(set(lengths.values())) > 1:
|
|
151
|
+
names = ", ".join(f"`{name}` ({length})" for name, length in lengths.items())
|
|
152
|
+
msg = f"The inputs must have the same length: {names}."
|
|
153
|
+
raise ValueError(msg)
|
|
154
|
+
|
|
155
|
+
def _calls(
|
|
156
|
+
self, states: list[dict[str, Any]], images: list[list[ImageInput]] | None
|
|
157
|
+
) -> list[tuple[dict[str, Any], list[ImageInput]]]:
|
|
158
|
+
# Built for every item before any request, so a bad model, image or size fails before anything is billed
|
|
159
|
+
openrouter = is_openrouter(resolve_base_url(self.api_base_url))
|
|
160
|
+
question = {_QUESTION_ID: {"type": "noul", "instructions": self.instructions}}
|
|
161
|
+
calls = []
|
|
162
|
+
for index, state in enumerate(states):
|
|
163
|
+
item_images = list(images[index]) if images is not None else []
|
|
164
|
+
call = build_call(state, question, model=self.model, images=item_images or None, openrouter=openrouter)
|
|
165
|
+
calls.append((call, item_images))
|
|
166
|
+
return calls
|
|
167
|
+
|
|
168
|
+
@staticmethod
|
|
169
|
+
def _judged(response: DrexResponse) -> Outcome:
|
|
170
|
+
probability = response.answers[_QUESTION_ID]["noul"]
|
|
171
|
+
meta = {
|
|
172
|
+
"model": response.model,
|
|
173
|
+
"usage": response.usage,
|
|
174
|
+
"request_id": (response.model_extra or {}).get("request_id"),
|
|
175
|
+
}
|
|
176
|
+
return {"score": probability, "noul": probability, "status": "evaluated"}, meta
|
|
177
|
+
|
|
178
|
+
def _failed(self, error: nace_sdk.NaceError, item_images: list[ImageInput]) -> Outcome:
|
|
179
|
+
readable = readable_error(
|
|
180
|
+
error, provider_name(resolve_base_url(self.api_base_url)), image_types=image_types(item_images)
|
|
181
|
+
)
|
|
182
|
+
if self.raise_on_failure:
|
|
183
|
+
raise readable from error
|
|
184
|
+
meta = {"model": None, "usage": None, "request_id": getattr(readable, "request_id", None)}
|
|
185
|
+
return {"score": math.nan, "status": "error", "error": str(readable)}, meta
|
|
186
|
+
|
|
187
|
+
@staticmethod
|
|
188
|
+
def _summarize(outcomes: list[Outcome]) -> dict[str, Any]:
|
|
189
|
+
results = [result for result, _ in outcomes]
|
|
190
|
+
scores = [result["score"] for result in results]
|
|
191
|
+
valid = [score for score in scores if not math.isnan(score)]
|
|
192
|
+
skipped = len(scores) - len(valid)
|
|
193
|
+
if skipped:
|
|
194
|
+
logger.warning("{skipped} item(s) failed and were excluded from the score.", skipped=skipped)
|
|
195
|
+
return {
|
|
196
|
+
"score": sum(valid) / len(valid) if valid else math.nan,
|
|
197
|
+
"individual_scores": scores,
|
|
198
|
+
"results": results,
|
|
199
|
+
"meta": [meta for _, meta in outcomes],
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
def _evaluate(self, states: list[dict[str, Any]], images: list[list[ImageInput]] | None) -> dict[str, Any]:
|
|
203
|
+
calls = self._calls(states, images)
|
|
204
|
+
if not calls:
|
|
205
|
+
return self._summarize([])
|
|
206
|
+
self.warm_up()
|
|
207
|
+
assert self._client is not None # noqa: S101 # mypy: the client is created by warm_up above
|
|
208
|
+
client = self._client
|
|
209
|
+
|
|
210
|
+
def judge(call: dict[str, Any], item_images: list[ImageInput]) -> Outcome:
|
|
211
|
+
try:
|
|
212
|
+
return self._judged(client.system_one(**call))
|
|
213
|
+
except nace_sdk.NaceError as error:
|
|
214
|
+
return self._failed(error, item_images)
|
|
215
|
+
|
|
216
|
+
outcomes = map_in_threads(lambda pair: judge(*pair), calls, self.max_workers)
|
|
217
|
+
return self._summarize(outcomes)
|
|
218
|
+
|
|
219
|
+
async def _evaluate_async(
|
|
220
|
+
self, states: list[dict[str, Any]], images: list[list[ImageInput]] | None
|
|
221
|
+
) -> dict[str, Any]:
|
|
222
|
+
calls = self._calls(states, images)
|
|
223
|
+
if not calls:
|
|
224
|
+
return self._summarize([])
|
|
225
|
+
await self.warm_up_async()
|
|
226
|
+
assert self._async_client is not None # noqa: S101 # mypy: the client is created by warm_up_async above
|
|
227
|
+
client = self._async_client
|
|
228
|
+
semaphore = asyncio.Semaphore(self.max_workers)
|
|
229
|
+
|
|
230
|
+
async def judge(call: dict[str, Any], item_images: list[ImageInput]) -> Outcome:
|
|
231
|
+
try:
|
|
232
|
+
async with semaphore:
|
|
233
|
+
response = await client.system_one(**call)
|
|
234
|
+
except nace_sdk.NaceError as error:
|
|
235
|
+
return self._failed(error, item_images)
|
|
236
|
+
return self._judged(response)
|
|
237
|
+
|
|
238
|
+
tasks = [asyncio.create_task(judge(call, item_images)) for call, item_images in calls]
|
|
239
|
+
try:
|
|
240
|
+
outcomes = await asyncio.gather(*tasks)
|
|
241
|
+
except Exception:
|
|
242
|
+
# Only reached when `raise_on_failure` is `True`: cancel the remaining requests before re-raising
|
|
243
|
+
for task in tasks:
|
|
244
|
+
task.cancel()
|
|
245
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
246
|
+
raise
|
|
247
|
+
return self._summarize(list(outcomes))
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026-present Nace AI
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from haystack import component
|
|
8
|
+
from haystack.utils import Secret
|
|
9
|
+
|
|
10
|
+
from haystack_integrations.common.nace.client import DEFAULT_MODEL
|
|
11
|
+
from haystack_integrations.common.nace.images import ImageInput
|
|
12
|
+
|
|
13
|
+
from ._base import _NaceEvaluator
|
|
14
|
+
|
|
15
|
+
CONTEXT_RELEVANCE_INSTRUCTIONS = "Do the contexts contain information that helps answer the question?"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@component
|
|
19
|
+
class NaceContextRelevanceEvaluator(_NaceEvaluator):
|
|
20
|
+
"""
|
|
21
|
+
Judges whether the retrieved contexts help answer each question, with a Drex decision model.
|
|
22
|
+
|
|
23
|
+
A drop-in for Haystack's `ContextRelevanceEvaluator`: same inputs and outputs, but each item is one Drex request
|
|
24
|
+
that returns the probability that the contexts help answer the question.
|
|
25
|
+
|
|
26
|
+
### Usage example
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from haystack_integrations.components.evaluators.nace import NaceContextRelevanceEvaluator
|
|
30
|
+
|
|
31
|
+
evaluator = NaceContextRelevanceEvaluator()
|
|
32
|
+
result = evaluator.run(questions=["Who won the race?"], contexts=[["Ada won the 2026 city race."]])
|
|
33
|
+
print(result["score"])
|
|
34
|
+
# 0.98
|
|
35
|
+
```
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
*,
|
|
41
|
+
instructions: str = CONTEXT_RELEVANCE_INSTRUCTIONS,
|
|
42
|
+
model: str = DEFAULT_MODEL,
|
|
43
|
+
api_key: Secret = Secret.from_env_var("NACE_API_KEY"),
|
|
44
|
+
api_base_url: str | None = None,
|
|
45
|
+
timeout: float | None = None,
|
|
46
|
+
max_retries: int | None = None,
|
|
47
|
+
max_workers: int = 3,
|
|
48
|
+
raise_on_failure: bool = True,
|
|
49
|
+
) -> None:
|
|
50
|
+
"""
|
|
51
|
+
Creates a NaceContextRelevanceEvaluator.
|
|
52
|
+
|
|
53
|
+
:param instructions:
|
|
54
|
+
The yes/no question Drex answers about each item.
|
|
55
|
+
:param model:
|
|
56
|
+
The Drex model to use. Defaults to the newest Drex model; pin it in production.
|
|
57
|
+
:param api_key:
|
|
58
|
+
The Nace API key.
|
|
59
|
+
:param api_base_url:
|
|
60
|
+
Base URL of the API. If `None`, the `NACE_BASE_URL` environment variable is used, or
|
|
61
|
+
`https://console.nace.ai` if it is unset.
|
|
62
|
+
:param timeout:
|
|
63
|
+
Timeout in seconds for each request attempt. If `None`, 60 seconds are used.
|
|
64
|
+
:param max_retries:
|
|
65
|
+
Maximum number of retries after a failed request attempt. If `None`, the SDK default of 2 is used.
|
|
66
|
+
:param max_workers:
|
|
67
|
+
Maximum number of requests running at once. Must be at least 1.
|
|
68
|
+
:param raise_on_failure:
|
|
69
|
+
If `True`, the first failed request raises its error. If `False`, a failed item scores NaN, is marked
|
|
70
|
+
with `status: "error"` in `results`, and is left out of the mean `score`.
|
|
71
|
+
"""
|
|
72
|
+
# @component rebuilds the class, so zero-argument super() would point at the undecorated one
|
|
73
|
+
_NaceEvaluator.__init__(
|
|
74
|
+
self,
|
|
75
|
+
instructions=instructions,
|
|
76
|
+
model=model,
|
|
77
|
+
api_key=api_key,
|
|
78
|
+
api_base_url=api_base_url,
|
|
79
|
+
timeout=timeout,
|
|
80
|
+
max_retries=max_retries,
|
|
81
|
+
max_workers=max_workers,
|
|
82
|
+
raise_on_failure=raise_on_failure,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def _states(
|
|
86
|
+
self, questions: list[str], contexts: list[list[str]], images: list[list[ImageInput]] | None
|
|
87
|
+
) -> list[dict[str, Any]]:
|
|
88
|
+
self._check_contexts(contexts)
|
|
89
|
+
self._check_images(images)
|
|
90
|
+
self._check_lengths({"questions": questions, "contexts": contexts, "images": images})
|
|
91
|
+
return [
|
|
92
|
+
{"question": question, "contexts": list(context)}
|
|
93
|
+
for question, context in zip(questions, contexts, strict=True)
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
@component.output_types(
|
|
97
|
+
score=float, individual_scores=list[float], results=list[dict[str, Any]], meta=list[dict[str, Any]] | None
|
|
98
|
+
)
|
|
99
|
+
def run(
|
|
100
|
+
self, questions: list[str], contexts: list[list[str]], images: list[list[ImageInput]] | None = None
|
|
101
|
+
) -> dict[str, Any]:
|
|
102
|
+
"""
|
|
103
|
+
Judges whether each question's contexts help answer it.
|
|
104
|
+
|
|
105
|
+
:param questions:
|
|
106
|
+
The questions.
|
|
107
|
+
:param contexts:
|
|
108
|
+
The contexts retrieved for each question.
|
|
109
|
+
:param images:
|
|
110
|
+
Optional images for each item, which Drex Max reads as "Image 1", "Image 2" and so on.
|
|
111
|
+
:returns:
|
|
112
|
+
A dictionary with `score`, `individual_scores`, `results` and `meta`, as in Haystack's
|
|
113
|
+
`ContextRelevanceEvaluator`; failed items score NaN when `raise_on_failure` is `False`.
|
|
114
|
+
:raises ValueError:
|
|
115
|
+
If the inputs have different lengths, or images are passed to a model that reads text only.
|
|
116
|
+
"""
|
|
117
|
+
return self._evaluate(self._states(questions, contexts, images), images)
|
|
118
|
+
|
|
119
|
+
@component.output_types(
|
|
120
|
+
score=float, individual_scores=list[float], results=list[dict[str, Any]], meta=list[dict[str, Any]] | None
|
|
121
|
+
)
|
|
122
|
+
async def run_async(
|
|
123
|
+
self, questions: list[str], contexts: list[list[str]], images: list[list[ImageInput]] | None = None
|
|
124
|
+
) -> dict[str, Any]:
|
|
125
|
+
"""
|
|
126
|
+
Asynchronously judges whether each question's contexts help answer it.
|
|
127
|
+
|
|
128
|
+
This is the asynchronous version of the `run` method with the same parameters and return values.
|
|
129
|
+
|
|
130
|
+
:param questions:
|
|
131
|
+
The questions.
|
|
132
|
+
:param contexts:
|
|
133
|
+
The contexts retrieved for each question.
|
|
134
|
+
:param images:
|
|
135
|
+
Optional images for each item.
|
|
136
|
+
:returns:
|
|
137
|
+
The same dictionary as `run`.
|
|
138
|
+
"""
|
|
139
|
+
return await self._evaluate_async(self._states(questions, contexts, images), images)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026-present Nace AI
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from haystack import component
|
|
8
|
+
from haystack.utils import Secret
|
|
9
|
+
|
|
10
|
+
from haystack_integrations.common.nace.client import DEFAULT_MODEL
|
|
11
|
+
from haystack_integrations.common.nace.images import ImageInput
|
|
12
|
+
|
|
13
|
+
from ._base import _NaceEvaluator
|
|
14
|
+
|
|
15
|
+
FAITHFULNESS_INSTRUCTIONS = (
|
|
16
|
+
"Is every claim in the answer supported by the contexts? "
|
|
17
|
+
"Answer no if any claim is missing from the contexts or contradicts them."
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@component
|
|
22
|
+
class NaceFaithfulnessEvaluator(_NaceEvaluator):
|
|
23
|
+
"""
|
|
24
|
+
Judges whether each answer is supported by its contexts, with a Drex decision model.
|
|
25
|
+
|
|
26
|
+
A drop-in for Haystack's `FaithfulnessEvaluator`: same inputs and outputs, but each item is one Drex request
|
|
27
|
+
that returns the probability that every claim in the answer is supported by the contexts, instead of an LLM
|
|
28
|
+
splitting the answer into statements. Pass `images` to judge answers about images on Drex Max.
|
|
29
|
+
|
|
30
|
+
### Usage example
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from haystack_integrations.components.evaluators.nace import NaceFaithfulnessEvaluator
|
|
34
|
+
|
|
35
|
+
evaluator = NaceFaithfulnessEvaluator()
|
|
36
|
+
result = evaluator.run(
|
|
37
|
+
questions=["Who won the race?"],
|
|
38
|
+
contexts=[["Ada won the 2026 city race."]],
|
|
39
|
+
predicted_answers=["Ada won the race."],
|
|
40
|
+
)
|
|
41
|
+
print(result["individual_scores"])
|
|
42
|
+
# [0.97]
|
|
43
|
+
```
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
*,
|
|
49
|
+
instructions: str = FAITHFULNESS_INSTRUCTIONS,
|
|
50
|
+
model: str = DEFAULT_MODEL,
|
|
51
|
+
api_key: Secret = Secret.from_env_var("NACE_API_KEY"),
|
|
52
|
+
api_base_url: str | None = None,
|
|
53
|
+
timeout: float | None = None,
|
|
54
|
+
max_retries: int | None = None,
|
|
55
|
+
max_workers: int = 3,
|
|
56
|
+
raise_on_failure: bool = True,
|
|
57
|
+
) -> None:
|
|
58
|
+
"""
|
|
59
|
+
Creates a NaceFaithfulnessEvaluator.
|
|
60
|
+
|
|
61
|
+
:param instructions:
|
|
62
|
+
The yes/no question Drex answers about each item. The default is strict: an answer with any unsupported
|
|
63
|
+
claim is unfaithful.
|
|
64
|
+
:param model:
|
|
65
|
+
The Drex model to use. Defaults to the newest Drex model; pin it in production.
|
|
66
|
+
:param api_key:
|
|
67
|
+
The Nace API key.
|
|
68
|
+
:param api_base_url:
|
|
69
|
+
Base URL of the API. If `None`, the `NACE_BASE_URL` environment variable is used, or
|
|
70
|
+
`https://console.nace.ai` if it is unset.
|
|
71
|
+
:param timeout:
|
|
72
|
+
Timeout in seconds for each request attempt. If `None`, 60 seconds are used.
|
|
73
|
+
:param max_retries:
|
|
74
|
+
Maximum number of retries after a failed request attempt. If `None`, the SDK default of 2 is used.
|
|
75
|
+
:param max_workers:
|
|
76
|
+
Maximum number of requests running at once. Must be at least 1.
|
|
77
|
+
:param raise_on_failure:
|
|
78
|
+
If `True`, the first failed request raises its error. If `False`, a failed item scores NaN, is marked
|
|
79
|
+
with `status: "error"` in `results`, and is left out of the mean `score`.
|
|
80
|
+
"""
|
|
81
|
+
# @component rebuilds the class, so zero-argument super() would point at the undecorated one
|
|
82
|
+
_NaceEvaluator.__init__(
|
|
83
|
+
self,
|
|
84
|
+
instructions=instructions,
|
|
85
|
+
model=model,
|
|
86
|
+
api_key=api_key,
|
|
87
|
+
api_base_url=api_base_url,
|
|
88
|
+
timeout=timeout,
|
|
89
|
+
max_retries=max_retries,
|
|
90
|
+
max_workers=max_workers,
|
|
91
|
+
raise_on_failure=raise_on_failure,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def _states(
|
|
95
|
+
self,
|
|
96
|
+
questions: list[str],
|
|
97
|
+
contexts: list[list[str]],
|
|
98
|
+
predicted_answers: list[str],
|
|
99
|
+
images: list[list[ImageInput]] | None,
|
|
100
|
+
) -> list[dict[str, Any]]:
|
|
101
|
+
self._check_contexts(contexts)
|
|
102
|
+
self._check_images(images)
|
|
103
|
+
self._check_lengths(
|
|
104
|
+
{"questions": questions, "contexts": contexts, "predicted_answers": predicted_answers, "images": images}
|
|
105
|
+
)
|
|
106
|
+
return [
|
|
107
|
+
{"question": question, "contexts": list(context), "answer": answer}
|
|
108
|
+
for question, context, answer in zip(questions, contexts, predicted_answers, strict=True)
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
@component.output_types(
|
|
112
|
+
score=float, individual_scores=list[float], results=list[dict[str, Any]], meta=list[dict[str, Any]] | None
|
|
113
|
+
)
|
|
114
|
+
def run(
|
|
115
|
+
self,
|
|
116
|
+
questions: list[str],
|
|
117
|
+
contexts: list[list[str]],
|
|
118
|
+
predicted_answers: list[str],
|
|
119
|
+
images: list[list[ImageInput]] | None = None,
|
|
120
|
+
) -> dict[str, Any]:
|
|
121
|
+
"""
|
|
122
|
+
Judges each answer against its contexts.
|
|
123
|
+
|
|
124
|
+
:param questions:
|
|
125
|
+
The questions.
|
|
126
|
+
:param contexts:
|
|
127
|
+
The contexts for each question.
|
|
128
|
+
:param predicted_answers:
|
|
129
|
+
The answer to each question.
|
|
130
|
+
:param images:
|
|
131
|
+
Optional images for each item, which Drex Max reads as "Image 1", "Image 2" and so on.
|
|
132
|
+
:returns:
|
|
133
|
+
A dictionary with the following keys:
|
|
134
|
+
- `score`: The mean of the item scores, leaving out failed items.
|
|
135
|
+
- `individual_scores`: Drex's probability that each answer is faithful, NaN for a failed item.
|
|
136
|
+
- `results`: Per item, `score`, `noul` and `status`, or `status: "error"` and `error`.
|
|
137
|
+
- `meta`: Per item, the `model`, `usage` and `request_id`.
|
|
138
|
+
:raises ValueError:
|
|
139
|
+
If the inputs have different lengths, or images are passed to a model that reads text only.
|
|
140
|
+
"""
|
|
141
|
+
return self._evaluate(self._states(questions, contexts, predicted_answers, images), images)
|
|
142
|
+
|
|
143
|
+
@component.output_types(
|
|
144
|
+
score=float, individual_scores=list[float], results=list[dict[str, Any]], meta=list[dict[str, Any]] | None
|
|
145
|
+
)
|
|
146
|
+
async def run_async(
|
|
147
|
+
self,
|
|
148
|
+
questions: list[str],
|
|
149
|
+
contexts: list[list[str]],
|
|
150
|
+
predicted_answers: list[str],
|
|
151
|
+
images: list[list[ImageInput]] | None = None,
|
|
152
|
+
) -> dict[str, Any]:
|
|
153
|
+
"""
|
|
154
|
+
Asynchronously judges each answer against its contexts.
|
|
155
|
+
|
|
156
|
+
This is the asynchronous version of the `run` method with the same parameters and return values.
|
|
157
|
+
|
|
158
|
+
:param questions:
|
|
159
|
+
The questions.
|
|
160
|
+
:param contexts:
|
|
161
|
+
The contexts for each question.
|
|
162
|
+
:param predicted_answers:
|
|
163
|
+
The answer to each question.
|
|
164
|
+
:param images:
|
|
165
|
+
Optional images for each item.
|
|
166
|
+
:returns:
|
|
167
|
+
The same dictionary as `run`.
|
|
168
|
+
"""
|
|
169
|
+
return await self._evaluate_async(self._states(questions, contexts, predicted_answers, images), images)
|
|
File without changes
|