aidial-rag-eval 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.
- aidial_rag_eval/__init__.py +7 -0
- aidial_rag_eval/dataframe/__init__.py +11 -0
- aidial_rag_eval/dataframe/match_facts.py +40 -0
- aidial_rag_eval/dataframe/merge.py +64 -0
- aidial_rag_eval/dataframe/metrics.py +301 -0
- aidial_rag_eval/dataset.py +99 -0
- aidial_rag_eval/evaluate.py +107 -0
- aidial_rag_eval/facts/__init__.py +0 -0
- aidial_rag_eval/facts/citation.py +48 -0
- aidial_rag_eval/facts/exact.py +19 -0
- aidial_rag_eval/facts/match_str_facts.py +71 -0
- aidial_rag_eval/generation/__init__.py +0 -0
- aidial_rag_eval/generation/inference.py +344 -0
- aidial_rag_eval/generation/metric_binds.py +173 -0
- aidial_rag_eval/generation/models/__init__.py +0 -0
- aidial_rag_eval/generation/models/converters/__init__.py +0 -0
- aidial_rag_eval/generation/models/converters/base_converter.py +18 -0
- aidial_rag_eval/generation/models/converters/decontextualization_template.py +47 -0
- aidial_rag_eval/generation/models/converters/llm_decontextualization_converter.py +207 -0
- aidial_rag_eval/generation/models/inference_scorers/__init__.py +0 -0
- aidial_rag_eval/generation/models/inference_scorers/base_inference_scorer.py +19 -0
- aidial_rag_eval/generation/models/inference_scorers/inference_template.py +77 -0
- aidial_rag_eval/generation/models/inference_scorers/llm_inference_scorer.py +115 -0
- aidial_rag_eval/generation/models/lambdas.py +31 -0
- aidial_rag_eval/generation/models/refusal_detectors/__init__.py +0 -0
- aidial_rag_eval/generation/models/refusal_detectors/base_refusal_detector.py +17 -0
- aidial_rag_eval/generation/models/refusal_detectors/llm_refusal_detector.py +112 -0
- aidial_rag_eval/generation/models/refusal_detectors/refusal_template.py +47 -0
- aidial_rag_eval/generation/refusal.py +102 -0
- aidial_rag_eval/generation/types.py +52 -0
- aidial_rag_eval/generation/utils/__init__.py +0 -0
- aidial_rag_eval/generation/utils/progress_bar.py +24 -0
- aidial_rag_eval/generation/utils/segmented_text.py +169 -0
- aidial_rag_eval/metric_binds.py +15 -0
- aidial_rag_eval/metrics.py +28 -0
- aidial_rag_eval/retrieval/__init__.py +0 -0
- aidial_rag_eval/retrieval/metrics.py +66 -0
- aidial_rag_eval/retrieval/types.py +24 -0
- aidial_rag_eval/types.py +62 -0
- aidial_rag_eval/utils.py +9 -0
- aidial_rag_eval-0.1.0.dist-info/LICENSE +201 -0
- aidial_rag_eval-0.1.0.dist-info/METADATA +184 -0
- aidial_rag_eval-0.1.0.dist-info/RECORD +44 -0
- aidial_rag_eval-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from aidial_rag_eval.dataframe.metrics import (
|
|
2
|
+
calculate_generation_metrics,
|
|
3
|
+
create_rag_eval_metrics_report,
|
|
4
|
+
create_retrieval_metrics_report,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"create_rag_eval_metrics_report",
|
|
9
|
+
"create_retrieval_metrics_report",
|
|
10
|
+
"calculate_generation_metrics",
|
|
11
|
+
]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
from aidial_rag_eval.dataframe.merge import merge_ground_truth_and_answers
|
|
4
|
+
from aidial_rag_eval.facts.citation import CitationMatcher
|
|
5
|
+
from aidial_rag_eval.retrieval.types import Matcher
|
|
6
|
+
|
|
7
|
+
DEFAULT_MATCHER = CitationMatcher
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def match_facts(row: pd.Series, matcher: Matcher) -> pd.Series:
|
|
11
|
+
result_row = matcher.match_facts(row.facts, row.context)
|
|
12
|
+
return pd.Series(result_row._asdict())
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def apply_matcher_to_merged_dataframe(
|
|
16
|
+
df_merged: pd.DataFrame,
|
|
17
|
+
matcher: Matcher = DEFAULT_MATCHER,
|
|
18
|
+
) -> pd.DataFrame:
|
|
19
|
+
matched_results = df_merged.apply(
|
|
20
|
+
match_facts,
|
|
21
|
+
matcher=matcher,
|
|
22
|
+
axis=1,
|
|
23
|
+
result_type="expand",
|
|
24
|
+
)
|
|
25
|
+
assert isinstance(matched_results, pd.DataFrame)
|
|
26
|
+
return matched_results
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def match_facts_dataframe(
|
|
30
|
+
ground_truth: pd.DataFrame,
|
|
31
|
+
answers: pd.DataFrame,
|
|
32
|
+
matcher: Matcher = DEFAULT_MATCHER,
|
|
33
|
+
) -> pd.DataFrame:
|
|
34
|
+
df_merged = merge_ground_truth_and_answers(ground_truth, answers)
|
|
35
|
+
return pd.merge(
|
|
36
|
+
df_merged,
|
|
37
|
+
apply_matcher_to_merged_dataframe(df_merged, matcher),
|
|
38
|
+
left_index=True,
|
|
39
|
+
right_index=True,
|
|
40
|
+
)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
from aidial_rag_eval.types import (
|
|
4
|
+
ANSWERS_COLUMNS,
|
|
5
|
+
GROUND_TRUTH_COLUMNS,
|
|
6
|
+
MERGED_KEY_COLUMNS,
|
|
7
|
+
AnswerColumns,
|
|
8
|
+
GroundTruthColumns,
|
|
9
|
+
MergedColumns,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def merge_ground_truth_and_answers(
|
|
14
|
+
ground_truth: pd.DataFrame, answers: pd.DataFrame
|
|
15
|
+
) -> pd.DataFrame:
|
|
16
|
+
"""
|
|
17
|
+
Merges ground_truth and collected answers dataframes.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
-----------
|
|
21
|
+
ground_truth : pd.DataFrame
|
|
22
|
+
contains the ground truth answers
|
|
23
|
+
The structure of the ground truth
|
|
24
|
+
is described in `aidial_rag_eval.types.GroundTruthAnswers`.
|
|
25
|
+
|
|
26
|
+
answers : pd.DataFrame
|
|
27
|
+
contains the collected answers
|
|
28
|
+
The structure of the collected answers
|
|
29
|
+
is described in `aidial_rag_eval.types.CollectedAnswers`.
|
|
30
|
+
|
|
31
|
+
Returns
|
|
32
|
+
------------
|
|
33
|
+
pd.DataFrame
|
|
34
|
+
Returns merged ground_truth dataframe and answers dataframe.
|
|
35
|
+
The structure of the df_merged
|
|
36
|
+
is described in `aidial_rag_eval.types.MergedColumns`.
|
|
37
|
+
"""
|
|
38
|
+
ground_truth_copy = ground_truth[
|
|
39
|
+
ground_truth.columns.intersection(GROUND_TRUTH_COLUMNS)
|
|
40
|
+
].copy()
|
|
41
|
+
assert isinstance(ground_truth_copy, pd.DataFrame)
|
|
42
|
+
answers_copy = answers[answers.columns.intersection(ANSWERS_COLUMNS)].copy()
|
|
43
|
+
assert isinstance(answers_copy, pd.DataFrame)
|
|
44
|
+
ground_truth_copy = ground_truth_copy.rename(
|
|
45
|
+
columns={
|
|
46
|
+
GroundTruthColumns.ANSWER.value: MergedColumns.GROUND_TRUTH_ANSWER.value
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
ground_truth_copy[GroundTruthColumns.DOCUMENTS] = ground_truth_copy[
|
|
50
|
+
GroundTruthColumns.DOCUMENTS
|
|
51
|
+
].apply(frozenset)
|
|
52
|
+
answers_copy[AnswerColumns.DOCUMENTS] = answers_copy[AnswerColumns.DOCUMENTS].apply(
|
|
53
|
+
frozenset
|
|
54
|
+
)
|
|
55
|
+
data = pd.merge(
|
|
56
|
+
ground_truth_copy,
|
|
57
|
+
answers_copy,
|
|
58
|
+
on=MERGED_KEY_COLUMNS,
|
|
59
|
+
)
|
|
60
|
+
data[MergedColumns.DOCUMENTS] = answers_copy.loc[
|
|
61
|
+
data.index, MergedColumns.DOCUMENTS
|
|
62
|
+
]
|
|
63
|
+
data[MergedColumns.DOCUMENTS] = data[MergedColumns.DOCUMENTS].apply(list)
|
|
64
|
+
return data
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from langchain_core.language_models import BaseChatModel
|
|
5
|
+
|
|
6
|
+
from aidial_rag_eval.dataframe.match_facts import (
|
|
7
|
+
DEFAULT_MATCHER,
|
|
8
|
+
apply_matcher_to_merged_dataframe,
|
|
9
|
+
)
|
|
10
|
+
from aidial_rag_eval.dataframe.merge import merge_ground_truth_and_answers
|
|
11
|
+
from aidial_rag_eval.generation.metric_binds import metric_binds_dict
|
|
12
|
+
from aidial_rag_eval.generation.types import MetricBind, inference_column
|
|
13
|
+
from aidial_rag_eval.retrieval.metrics import (
|
|
14
|
+
calculate_metrics as calculate_metrics_by_row,
|
|
15
|
+
)
|
|
16
|
+
from aidial_rag_eval.retrieval.types import Matcher
|
|
17
|
+
from aidial_rag_eval.types import MergedColumns
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def apply_metrics_to_matched_results(match_result_data: pd.DataFrame) -> pd.DataFrame:
|
|
21
|
+
retrieval_metrics = match_result_data.apply(
|
|
22
|
+
calculate_metrics_by_row,
|
|
23
|
+
axis=1,
|
|
24
|
+
result_type="expand",
|
|
25
|
+
)
|
|
26
|
+
assert isinstance(retrieval_metrics, pd.DataFrame)
|
|
27
|
+
return retrieval_metrics
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def calculate_metrics(match_result_data: pd.DataFrame) -> pd.DataFrame:
|
|
31
|
+
return pd.merge(
|
|
32
|
+
match_result_data,
|
|
33
|
+
apply_metrics_to_matched_results(match_result_data),
|
|
34
|
+
left_index=True,
|
|
35
|
+
right_index=True,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def calculate_retrieval_metrics(
|
|
40
|
+
df_merged: pd.DataFrame,
|
|
41
|
+
matcher: Matcher = DEFAULT_MATCHER,
|
|
42
|
+
) -> pd.DataFrame:
|
|
43
|
+
"""
|
|
44
|
+
Calculates RAG evaluation retrieval metrics from df_merged dataframe.
|
|
45
|
+
|
|
46
|
+
Parameters
|
|
47
|
+
-----------
|
|
48
|
+
df_merged : pd.DataFrame
|
|
49
|
+
merged ground truth answers and collected answers dataframes
|
|
50
|
+
The structure of the df_merged
|
|
51
|
+
is described in `aidial_rag_eval.types.MergedColumns`.
|
|
52
|
+
|
|
53
|
+
matcher : Matcher, default=DEFAULT_MATCHER
|
|
54
|
+
An object responsible for matching facts
|
|
55
|
+
from the ground truth to the context in the answers.
|
|
56
|
+
|
|
57
|
+
Returns
|
|
58
|
+
------------
|
|
59
|
+
pd.DataFrame
|
|
60
|
+
Returns retrieval metrics dataframe.
|
|
61
|
+
"""
|
|
62
|
+
matched_result = apply_matcher_to_merged_dataframe(df_merged, matcher)
|
|
63
|
+
matched_result_with_df = pd.merge(
|
|
64
|
+
df_merged, matched_result, left_index=True, right_index=True
|
|
65
|
+
)
|
|
66
|
+
retrieval_metrics = apply_metrics_to_matched_results(matched_result_with_df)
|
|
67
|
+
return pd.merge(
|
|
68
|
+
matched_result, retrieval_metrics, left_index=True, right_index=True
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def create_retrieval_metrics_report(
|
|
73
|
+
ground_truth: pd.DataFrame,
|
|
74
|
+
answers: pd.DataFrame,
|
|
75
|
+
matcher: Matcher = DEFAULT_MATCHER,
|
|
76
|
+
) -> pd.DataFrame:
|
|
77
|
+
"""
|
|
78
|
+
Calculates RAG evaluation retrieval metrics from input dataframes.
|
|
79
|
+
|
|
80
|
+
Parameters
|
|
81
|
+
-----------
|
|
82
|
+
ground_truth : pd.DataFrame
|
|
83
|
+
contains the ground truth answers
|
|
84
|
+
The structure of the ground truth
|
|
85
|
+
is described in `aidial_rag_eval.types.GroundTruthAnswers`.
|
|
86
|
+
|
|
87
|
+
answers : pd.DataFrame
|
|
88
|
+
contains the collected answers
|
|
89
|
+
The structure of the collected answers
|
|
90
|
+
is described in `aidial_rag_eval.types.CollectedAnswers`.
|
|
91
|
+
|
|
92
|
+
matcher : Matcher, default=DEFAULT_MATCHER
|
|
93
|
+
An object responsible for matching facts
|
|
94
|
+
from the ground truth to the context in the answers.
|
|
95
|
+
|
|
96
|
+
Returns
|
|
97
|
+
------------
|
|
98
|
+
pd.DataFrame
|
|
99
|
+
Returns merged ground_truth dataframe,
|
|
100
|
+
answers dataframe with retrieval metrics dataframe.
|
|
101
|
+
"""
|
|
102
|
+
df_merged = merge_ground_truth_and_answers(ground_truth, answers)
|
|
103
|
+
return pd.merge(
|
|
104
|
+
df_merged,
|
|
105
|
+
calculate_retrieval_metrics(df_merged, matcher),
|
|
106
|
+
left_index=True,
|
|
107
|
+
right_index=True,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def calculate_generation_metrics(
|
|
112
|
+
df_merged: pd.DataFrame,
|
|
113
|
+
llm: BaseChatModel,
|
|
114
|
+
metric_binds: List[MetricBind],
|
|
115
|
+
max_concurrency: int = 8,
|
|
116
|
+
batch_size: int = 6,
|
|
117
|
+
show_progress_bar: bool = True,
|
|
118
|
+
) -> pd.DataFrame:
|
|
119
|
+
"""
|
|
120
|
+
Calculates RAG evaluation generation metrics from df_merged dataframe.
|
|
121
|
+
|
|
122
|
+
Parameters
|
|
123
|
+
-----------
|
|
124
|
+
df_merged : pd.DataFrame
|
|
125
|
+
merged ground truth answers and collected answers dataframes
|
|
126
|
+
The structure of the df_merged
|
|
127
|
+
is described in `aidial_rag_eval.types.MergedColumns`.
|
|
128
|
+
|
|
129
|
+
llm : BaseChatModel, optional, default=None
|
|
130
|
+
A Langchain chat model used for calculating generation metrics.
|
|
131
|
+
|
|
132
|
+
metric_binds : List[MetricBind], optional, default=None
|
|
133
|
+
A list of string constants from `aidial_rag_eval.metric_binds`
|
|
134
|
+
specifying the generation metrics.
|
|
135
|
+
|
|
136
|
+
max_concurrency : int, default=8
|
|
137
|
+
The maximum number of concurrent requests to the LLM.
|
|
138
|
+
|
|
139
|
+
batch_size : int, default=6
|
|
140
|
+
The maximum number objects are processed in a single prompt for simple tasks.
|
|
141
|
+
|
|
142
|
+
show_progress_bar : bool, default=True
|
|
143
|
+
To display a progress bar during LLM requests.
|
|
144
|
+
|
|
145
|
+
Returns
|
|
146
|
+
------------
|
|
147
|
+
pd.DataFrame
|
|
148
|
+
Returns generation metrics dataframe.
|
|
149
|
+
"""
|
|
150
|
+
df_merged_copy = df_merged.copy()
|
|
151
|
+
df_merged_copy[MergedColumns.JOINED_CONTEXT] = df_merged_copy[
|
|
152
|
+
MergedColumns.CONTEXT
|
|
153
|
+
].apply(lambda x: "\n".join(x))
|
|
154
|
+
metric_results = dict()
|
|
155
|
+
for metric_bind in metric_binds:
|
|
156
|
+
metric_results.update(
|
|
157
|
+
metric_binds_dict[metric_bind](
|
|
158
|
+
df_merged=df_merged_copy,
|
|
159
|
+
llm=llm,
|
|
160
|
+
max_concurrency=max_concurrency,
|
|
161
|
+
batch_size=batch_size,
|
|
162
|
+
show_progress_bar=show_progress_bar,
|
|
163
|
+
).to_dict(orient="series")
|
|
164
|
+
)
|
|
165
|
+
df_metrics = pd.DataFrame(data=metric_results)
|
|
166
|
+
nli_columns = [
|
|
167
|
+
column for column in metric_results.keys() if column.endswith(inference_column)
|
|
168
|
+
]
|
|
169
|
+
if nli_columns:
|
|
170
|
+
sub_df_nli = df_metrics[nli_columns]
|
|
171
|
+
df_metrics["mean_" + inference_column] = sub_df_nli.mean(1)
|
|
172
|
+
df_metrics["median_" + inference_column] = sub_df_nli.median(
|
|
173
|
+
1
|
|
174
|
+
) # pyright: ignore # noqa
|
|
175
|
+
|
|
176
|
+
return df_metrics
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def create_generation_metrics_report(
|
|
180
|
+
ground_truth: pd.DataFrame,
|
|
181
|
+
answers: pd.DataFrame,
|
|
182
|
+
llm: BaseChatModel,
|
|
183
|
+
metric_binds: List[MetricBind],
|
|
184
|
+
max_concurrency: int = 8,
|
|
185
|
+
batch_size: int = 6,
|
|
186
|
+
show_progress_bar: bool = True,
|
|
187
|
+
) -> pd.DataFrame:
|
|
188
|
+
"""
|
|
189
|
+
Calculates RAG evaluation generation metrics from input dataframes.
|
|
190
|
+
|
|
191
|
+
Parameters
|
|
192
|
+
-----------
|
|
193
|
+
ground_truth : pd.DataFrame
|
|
194
|
+
contains the ground truth answers
|
|
195
|
+
The structure of the ground truth
|
|
196
|
+
is described in `aidial_rag_eval.types.GroundTruthAnswers`.
|
|
197
|
+
|
|
198
|
+
answers : pd.DataFrame
|
|
199
|
+
contains the collected answers
|
|
200
|
+
The structure of the collected answers
|
|
201
|
+
is described in `aidial_rag_eval.types.CollectedAnswers`.
|
|
202
|
+
|
|
203
|
+
llm : BaseChatModel, optional, default=None
|
|
204
|
+
A Langchain chat model used for calculating generation metrics.
|
|
205
|
+
|
|
206
|
+
metric_binds : List[MetricBind], optional, default=None
|
|
207
|
+
A list of string constants from `aidial_rag_eval.metric_binds`
|
|
208
|
+
specifying the generation metrics.
|
|
209
|
+
|
|
210
|
+
max_concurrency : int, default=8
|
|
211
|
+
The maximum number of concurrent requests to the LLM.
|
|
212
|
+
|
|
213
|
+
batch_size : int, default=6
|
|
214
|
+
The maximum number objects are processed in a single prompt for simple tasks.
|
|
215
|
+
|
|
216
|
+
show_progress_bar : bool, default=True
|
|
217
|
+
To display a progress bar during LLM requests.
|
|
218
|
+
|
|
219
|
+
Returns
|
|
220
|
+
------------
|
|
221
|
+
pd.DataFrame
|
|
222
|
+
Returns merged ground_truth dataframe,
|
|
223
|
+
answers dataframe with generation metrics dataframe.
|
|
224
|
+
"""
|
|
225
|
+
df_merged = merge_ground_truth_and_answers(ground_truth, answers)
|
|
226
|
+
df_metrics = calculate_generation_metrics(
|
|
227
|
+
df_merged,
|
|
228
|
+
llm,
|
|
229
|
+
metric_binds,
|
|
230
|
+
max_concurrency,
|
|
231
|
+
batch_size,
|
|
232
|
+
show_progress_bar,
|
|
233
|
+
)
|
|
234
|
+
return pd.merge(df_merged, df_metrics, left_index=True, right_index=True)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def create_rag_eval_metrics_report(
|
|
238
|
+
ground_truth: pd.DataFrame,
|
|
239
|
+
answers: pd.DataFrame,
|
|
240
|
+
matcher: Matcher = DEFAULT_MATCHER,
|
|
241
|
+
llm: Optional[BaseChatModel] = None,
|
|
242
|
+
metric_binds: Optional[List[MetricBind]] = None,
|
|
243
|
+
max_concurrency: int = 8,
|
|
244
|
+
batch_size: int = 6,
|
|
245
|
+
show_progress_bar: bool = True,
|
|
246
|
+
) -> pd.DataFrame:
|
|
247
|
+
"""
|
|
248
|
+
Calculates RAG evaluation metrics from input dataframes.
|
|
249
|
+
|
|
250
|
+
Parameters
|
|
251
|
+
-----------
|
|
252
|
+
ground_truth : pd.DataFrame
|
|
253
|
+
contains the ground truth answers
|
|
254
|
+
The structure of the ground truth
|
|
255
|
+
is described in `aidial_rag_eval.types.GroundTruthAnswers`.
|
|
256
|
+
|
|
257
|
+
answers : pd.DataFrame
|
|
258
|
+
contains the collected answers
|
|
259
|
+
The structure of the collected answers
|
|
260
|
+
is described in `aidial_rag_eval.types.CollectedAnswers`.
|
|
261
|
+
|
|
262
|
+
matcher : Matcher, default=DEFAULT_MATCHER
|
|
263
|
+
An object responsible for matching facts
|
|
264
|
+
from the ground truth to the context in the answers.
|
|
265
|
+
|
|
266
|
+
llm : BaseChatModel, optional, default=None
|
|
267
|
+
A Langchain chat model used for calculating generation metrics.
|
|
268
|
+
|
|
269
|
+
metric_binds : List[MetricBind], optional, default=None
|
|
270
|
+
A list of string constants from `aidial_rag_eval.metric_binds`
|
|
271
|
+
specifying the generation metrics.
|
|
272
|
+
|
|
273
|
+
max_concurrency : int, default=8
|
|
274
|
+
The maximum number of concurrent requests to the LLM.
|
|
275
|
+
|
|
276
|
+
batch_size : int, default=6
|
|
277
|
+
The maximum number objects are processed in a single prompt for simple tasks.
|
|
278
|
+
|
|
279
|
+
show_progress_bar : bool, default=True
|
|
280
|
+
To display a progress bar during LLM requests.
|
|
281
|
+
|
|
282
|
+
Returns
|
|
283
|
+
------------
|
|
284
|
+
pd.DataFrame
|
|
285
|
+
Returns merged ground_truth dataframe, answers dataframe with metrics dataframe.
|
|
286
|
+
"""
|
|
287
|
+
df_merged = merge_ground_truth_and_answers(ground_truth, answers)
|
|
288
|
+
retrieval_metrics = calculate_retrieval_metrics(df_merged, matcher)
|
|
289
|
+
if not metric_binds:
|
|
290
|
+
return pd.merge(df_merged, retrieval_metrics, left_index=True, right_index=True)
|
|
291
|
+
if llm is None:
|
|
292
|
+
raise ValueError("Argument 'llm' is required.")
|
|
293
|
+
generation_metrics = calculate_generation_metrics(
|
|
294
|
+
df_merged,
|
|
295
|
+
llm,
|
|
296
|
+
metric_binds,
|
|
297
|
+
max_concurrency,
|
|
298
|
+
batch_size,
|
|
299
|
+
show_progress_bar,
|
|
300
|
+
)
|
|
301
|
+
return pd.concat([df_merged, retrieval_metrics, generation_metrics], axis=1)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Optional, Union
|
|
3
|
+
|
|
4
|
+
import fsspec
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from pydantic import Field, TypeAdapter
|
|
7
|
+
from pydantic.dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class DatasetMetadata:
|
|
12
|
+
created_at: Optional[datetime]
|
|
13
|
+
sources: list["Dataset"]
|
|
14
|
+
tools: dict[str, str]
|
|
15
|
+
file_info: dict = Field(default_factory=dict)
|
|
16
|
+
metrics: dict = Field(default_factory=dict)
|
|
17
|
+
statistics: dict = Field(default_factory=dict)
|
|
18
|
+
|
|
19
|
+
@staticmethod
|
|
20
|
+
def read_metadata(metadata_path: str) -> "DatasetMetadata":
|
|
21
|
+
fs, _ = fsspec.url_to_fs(metadata_path)
|
|
22
|
+
return TypeAdapter(DatasetMetadata).validate_json(fs.read_bytes(metadata_path))
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def create_missing_metadata(data_path: str) -> "DatasetMetadata":
|
|
26
|
+
fs, _ = fsspec.url_to_fs(data_path)
|
|
27
|
+
file_info = fs.info(data_path)
|
|
28
|
+
return DatasetMetadata(None, [], {}, file_info, {}, {})
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class Dataset:
|
|
33
|
+
metadata_path: str
|
|
34
|
+
data_path: str
|
|
35
|
+
metadata: DatasetMetadata
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def handle_path(path: str) -> tuple[str, str]:
|
|
39
|
+
if path.endswith(".parquet.metadata.json"):
|
|
40
|
+
return path, path[: -len(".metadata.json")]
|
|
41
|
+
if path.endswith(".parquet"):
|
|
42
|
+
return path + ".metadata.json", path
|
|
43
|
+
raise ValueError(f"Invalid path: {path}")
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def from_metadata(path: str):
|
|
47
|
+
metadata_path, data_path = Dataset.handle_path(path)
|
|
48
|
+
|
|
49
|
+
fs, _ = fsspec.url_to_fs(data_path)
|
|
50
|
+
if fs.exists(metadata_path):
|
|
51
|
+
metadata = DatasetMetadata.read_metadata(metadata_path)
|
|
52
|
+
else:
|
|
53
|
+
metadata = DatasetMetadata.create_missing_metadata(data_path)
|
|
54
|
+
|
|
55
|
+
return Dataset(metadata_path, data_path, metadata)
|
|
56
|
+
|
|
57
|
+
def read_dataframe(self, *args, **kwargs) -> pd.DataFrame:
|
|
58
|
+
return pd.read_parquet(self.data_path, *args, **kwargs)
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def write_dataframe(
|
|
62
|
+
df: pd.DataFrame,
|
|
63
|
+
path: str,
|
|
64
|
+
sources: Optional[list["Dataset"]] = None,
|
|
65
|
+
tools: Optional[dict[str, str]] = None,
|
|
66
|
+
metrics: Optional[dict[str, float]] = None,
|
|
67
|
+
statistics: Optional[dict[str, float]] = None,
|
|
68
|
+
*args,
|
|
69
|
+
**kwargs,
|
|
70
|
+
):
|
|
71
|
+
sources = sources or []
|
|
72
|
+
tools = tools or {}
|
|
73
|
+
metrics = metrics or {}
|
|
74
|
+
statistics = statistics or {}
|
|
75
|
+
metadata_path, data_path = Dataset.handle_path(path)
|
|
76
|
+
|
|
77
|
+
fs, _ = fsspec.url_to_fs(data_path)
|
|
78
|
+
df.to_parquet(data_path, *args, **kwargs)
|
|
79
|
+
file_info: dict = fs.info(data_path)
|
|
80
|
+
|
|
81
|
+
metadata = DatasetMetadata(
|
|
82
|
+
datetime.now(),
|
|
83
|
+
sources,
|
|
84
|
+
tools,
|
|
85
|
+
file_info,
|
|
86
|
+
metrics,
|
|
87
|
+
statistics,
|
|
88
|
+
)
|
|
89
|
+
fs.write_bytes(
|
|
90
|
+
metadata_path, TypeAdapter(DatasetMetadata).dump_json(metadata, indent=2)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return Dataset(metadata_path, data_path, metadata)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def source_dataset(dataset: Union[str, Dataset]):
|
|
97
|
+
if isinstance(dataset, str):
|
|
98
|
+
return Dataset.from_metadata(dataset)
|
|
99
|
+
return dataset
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from typing import Any, List, Optional, Union
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from langchain_core.language_models import BaseChatModel
|
|
5
|
+
|
|
6
|
+
from aidial_rag_eval.dataframe import create_rag_eval_metrics_report
|
|
7
|
+
from aidial_rag_eval.dataframe.match_facts import DEFAULT_MATCHER
|
|
8
|
+
from aidial_rag_eval.dataset import Dataset, source_dataset
|
|
9
|
+
from aidial_rag_eval.generation.types import MetricBind
|
|
10
|
+
from aidial_rag_eval.retrieval.types import Matcher
|
|
11
|
+
from aidial_rag_eval.utils import get_tools_versions
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def evaluate(
|
|
15
|
+
ground_truth: Union[str, Dataset],
|
|
16
|
+
answers: Union[str, Dataset],
|
|
17
|
+
dest: str,
|
|
18
|
+
matcher: Matcher = DEFAULT_MATCHER,
|
|
19
|
+
llm: Optional[BaseChatModel] = None,
|
|
20
|
+
fs: Any = None,
|
|
21
|
+
metric_binds: Optional[List[MetricBind]] = None,
|
|
22
|
+
max_concurrency: int = 8,
|
|
23
|
+
batch_size: int = 6,
|
|
24
|
+
show_progress_bar: bool = True,
|
|
25
|
+
) -> Dataset:
|
|
26
|
+
"""
|
|
27
|
+
Calculates RAG evaluation metrics from input
|
|
28
|
+
Parquet datasets and writes the results to the specified destination.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
-----------
|
|
32
|
+
ground_truth : Union[str, Dataset]
|
|
33
|
+
Path to the Parquet file containing ground truth answers or
|
|
34
|
+
a Dataset object specifying the Parquet file with ground truth answers.
|
|
35
|
+
The structure of the ground truth Parquet file
|
|
36
|
+
is described in `aidial_rag_eval.types.GroundTruthAnswers`.
|
|
37
|
+
|
|
38
|
+
answers : Union[str, Dataset]
|
|
39
|
+
Path to the Parquet file containing collected answers or
|
|
40
|
+
a Dataset object specifying the Parquet file with collected answers.
|
|
41
|
+
The structure of the collected answers Parquet file
|
|
42
|
+
is described in `aidial_rag_eval.types.CollectedAnswers`.
|
|
43
|
+
|
|
44
|
+
dest : str
|
|
45
|
+
Path for the output file in Parquet format.
|
|
46
|
+
|
|
47
|
+
matcher : Matcher, default=DEFAULT_MATCHER
|
|
48
|
+
An object responsible for matching facts
|
|
49
|
+
from the ground truth to the context in the answers.
|
|
50
|
+
|
|
51
|
+
llm : BaseChatModel, optional, default=None
|
|
52
|
+
A Langchain chat model used for calculating generation metrics.
|
|
53
|
+
|
|
54
|
+
fs : Any, optional, default=None
|
|
55
|
+
File system to be used.
|
|
56
|
+
|
|
57
|
+
metric_binds : List[MetricBind], optional, default=None
|
|
58
|
+
A list of string constants from `aidial_rag_eval.metric_binds`
|
|
59
|
+
specifying the generation metrics.
|
|
60
|
+
|
|
61
|
+
max_concurrency : int, default=8
|
|
62
|
+
The maximum number of concurrent requests to the LLM.
|
|
63
|
+
|
|
64
|
+
batch_size : int, default=6
|
|
65
|
+
The maximum number objects are processed in a single prompt for simple tasks.
|
|
66
|
+
|
|
67
|
+
show_progress_bar : bool, default=True
|
|
68
|
+
To display a progress bar during LLM requests.
|
|
69
|
+
|
|
70
|
+
Returns
|
|
71
|
+
------------
|
|
72
|
+
Dataset
|
|
73
|
+
Returns global statistics for each metric.
|
|
74
|
+
"""
|
|
75
|
+
ground_truth_dataset = source_dataset(ground_truth)
|
|
76
|
+
answers_dataset = source_dataset(answers)
|
|
77
|
+
|
|
78
|
+
ground_truth_df = ground_truth_dataset.read_dataframe(filesystem=fs)
|
|
79
|
+
answers_df = answers_dataset.read_dataframe(filesystem=fs)
|
|
80
|
+
|
|
81
|
+
df_final = create_rag_eval_metrics_report(
|
|
82
|
+
ground_truth_df,
|
|
83
|
+
answers_df,
|
|
84
|
+
matcher,
|
|
85
|
+
llm,
|
|
86
|
+
metric_binds=metric_binds,
|
|
87
|
+
max_concurrency=max_concurrency,
|
|
88
|
+
batch_size=batch_size,
|
|
89
|
+
show_progress_bar=show_progress_bar,
|
|
90
|
+
)
|
|
91
|
+
aggregated_metrics = df_final.mean(numeric_only=True)
|
|
92
|
+
assert isinstance(aggregated_metrics, pd.Series)
|
|
93
|
+
|
|
94
|
+
metrics = Dataset.write_dataframe(
|
|
95
|
+
df_final,
|
|
96
|
+
dest,
|
|
97
|
+
sources=[ground_truth_dataset, answers_dataset],
|
|
98
|
+
tools=get_tools_versions(),
|
|
99
|
+
metrics=aggregated_metrics.to_dict(),
|
|
100
|
+
statistics={
|
|
101
|
+
"Ground truth size": len(ground_truth_df),
|
|
102
|
+
"Answers size": len(answers_df),
|
|
103
|
+
"Evaluation data size": len(df_final),
|
|
104
|
+
},
|
|
105
|
+
filesystem=fs,
|
|
106
|
+
)
|
|
107
|
+
return metrics
|
|
File without changes
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from typing import Optional, Tuple
|
|
2
|
+
|
|
3
|
+
from aidial_rag_eval.facts.match_str_facts import match_str_facts
|
|
4
|
+
from aidial_rag_eval.retrieval.types import ContextChunk, FactMatchResult
|
|
5
|
+
from aidial_rag_eval.types import Context, Facts
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CitationMatcher:
|
|
9
|
+
Fact = str
|
|
10
|
+
|
|
11
|
+
@staticmethod
|
|
12
|
+
def _canonize(text: str) -> tuple[str, list[int]]:
|
|
13
|
+
text = text.replace("\t", " ")
|
|
14
|
+
text = "".join([c.lower() if c.isalnum() else " " for c in text])
|
|
15
|
+
|
|
16
|
+
new_text = ""
|
|
17
|
+
num_skipped_chars_for_pos = []
|
|
18
|
+
|
|
19
|
+
# skip duplicated spaces
|
|
20
|
+
for i, c in enumerate(text):
|
|
21
|
+
if c == " " and new_text[-1:] == " ":
|
|
22
|
+
continue
|
|
23
|
+
new_text += c
|
|
24
|
+
num_skipped_chars_for_pos.append(i - len(new_text) + 1)
|
|
25
|
+
|
|
26
|
+
num_skipped_chars_for_pos.append(len(text) - len(new_text))
|
|
27
|
+
|
|
28
|
+
return new_text, num_skipped_chars_for_pos
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def _match(fact: Fact, context_chunk: ContextChunk) -> Optional[Tuple[int, int]]:
|
|
32
|
+
canonized_fact, _ = CitationMatcher._canonize(fact)
|
|
33
|
+
canonized_context_chunk, skipped_chars = CitationMatcher._canonize(
|
|
34
|
+
context_chunk
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
start = canonized_context_chunk.find(canonized_fact)
|
|
38
|
+
if start == -1:
|
|
39
|
+
return None
|
|
40
|
+
end = start + len(canonized_fact)
|
|
41
|
+
|
|
42
|
+
start = start + skipped_chars[start]
|
|
43
|
+
end = end + skipped_chars[end]
|
|
44
|
+
return start, end
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def match_facts(facts: Facts[Fact], context: Context) -> FactMatchResult:
|
|
48
|
+
return match_str_facts(facts, context, CitationMatcher._match)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from typing import Optional, Tuple
|
|
2
|
+
|
|
3
|
+
from aidial_rag_eval.facts.match_str_facts import match_str_facts
|
|
4
|
+
from aidial_rag_eval.retrieval.types import ContextChunk, FactMatchResult
|
|
5
|
+
from aidial_rag_eval.types import Context, Facts
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ExactStringMatcher:
|
|
9
|
+
Fact = str
|
|
10
|
+
|
|
11
|
+
@staticmethod
|
|
12
|
+
def _match(fact: Fact, context_chunk: ContextChunk) -> Optional[Tuple[int, int]]:
|
|
13
|
+
if fact == context_chunk:
|
|
14
|
+
return (0, len(context_chunk))
|
|
15
|
+
return None
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def match_facts(facts: Facts[Fact], context: Context) -> FactMatchResult:
|
|
19
|
+
return match_str_facts(facts, context, ExactStringMatcher._match)
|