AutoRAG 0.0.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.
- autorag/__init__.py +82 -0
- autorag/chunker.py +51 -0
- autorag/cli.py +209 -0
- autorag/dashboard.py +199 -0
- autorag/data/__init__.py +109 -0
- autorag/data/chunk/__init__.py +2 -0
- autorag/data/chunk/base.py +128 -0
- autorag/data/chunk/langchain_chunk.py +76 -0
- autorag/data/chunk/llama_index_chunk.py +96 -0
- autorag/data/chunk/run.py +38 -0
- autorag/data/legacy/__init__.py +0 -0
- autorag/data/legacy/corpus/__init__.py +2 -0
- autorag/data/legacy/corpus/langchain.py +47 -0
- autorag/data/legacy/corpus/llama_index.py +93 -0
- autorag/data/legacy/qacreation/__init__.py +6 -0
- autorag/data/legacy/qacreation/base.py +239 -0
- autorag/data/legacy/qacreation/llama_index.py +253 -0
- autorag/data/legacy/qacreation/llama_index_default_prompt.txt +54 -0
- autorag/data/legacy/qacreation/ragas.py +75 -0
- autorag/data/legacy/qacreation/simple.py +99 -0
- autorag/data/parse/__init__.py +1 -0
- autorag/data/parse/base.py +79 -0
- autorag/data/parse/clova.py +194 -0
- autorag/data/parse/langchain_parse.py +87 -0
- autorag/data/parse/llamaparse.py +126 -0
- autorag/data/parse/run.py +141 -0
- autorag/data/parse/table_hybrid_parse.py +134 -0
- autorag/data/qa/__init__.py +3 -0
- autorag/data/qa/evolve/__init__.py +0 -0
- autorag/data/qa/evolve/llama_index_query_evolve.py +64 -0
- autorag/data/qa/evolve/openai_query_evolve.py +81 -0
- autorag/data/qa/evolve/prompt.py +288 -0
- autorag/data/qa/extract_evidence.py +1 -0
- autorag/data/qa/filter/__init__.py +0 -0
- autorag/data/qa/filter/dontknow.py +117 -0
- autorag/data/qa/filter/passage_dependency.py +88 -0
- autorag/data/qa/filter/prompt.py +73 -0
- autorag/data/qa/generation_gt/__init__.py +0 -0
- autorag/data/qa/generation_gt/base.py +16 -0
- autorag/data/qa/generation_gt/llama_index_gen_gt.py +41 -0
- autorag/data/qa/generation_gt/openai_gen_gt.py +84 -0
- autorag/data/qa/generation_gt/prompt.py +27 -0
- autorag/data/qa/query/__init__.py +0 -0
- autorag/data/qa/query/llama_gen_query.py +82 -0
- autorag/data/qa/query/openai_gen_query.py +95 -0
- autorag/data/qa/query/prompt.py +201 -0
- autorag/data/qa/sample.py +26 -0
- autorag/data/qa/schema.py +322 -0
- autorag/data/utils/__init__.py +0 -0
- autorag/data/utils/util.py +103 -0
- autorag/deploy/__init__.py +9 -0
- autorag/deploy/api.py +303 -0
- autorag/deploy/base.py +235 -0
- autorag/deploy/gradio.py +74 -0
- autorag/deploy/swagger.yml +202 -0
- autorag/embedding/__init__.py +0 -0
- autorag/embedding/base.py +144 -0
- autorag/embedding/vllm.py +256 -0
- autorag/evaluation/__init__.py +3 -0
- autorag/evaluation/generation.py +88 -0
- autorag/evaluation/metric/__init__.py +22 -0
- autorag/evaluation/metric/deepeval_prompt.py +322 -0
- autorag/evaluation/metric/g_eval_prompts/coh_detailed.txt +32 -0
- autorag/evaluation/metric/g_eval_prompts/con_detailed.txt +33 -0
- autorag/evaluation/metric/g_eval_prompts/flu_detailed.txt +26 -0
- autorag/evaluation/metric/g_eval_prompts/rel_detailed.txt +33 -0
- autorag/evaluation/metric/generation.py +504 -0
- autorag/evaluation/metric/retrieval.py +115 -0
- autorag/evaluation/metric/retrieval_contents.py +65 -0
- autorag/evaluation/metric/util.py +88 -0
- autorag/evaluation/retrieval.py +83 -0
- autorag/evaluation/retrieval_contents.py +65 -0
- autorag/evaluation/util.py +43 -0
- autorag/evaluator.py +559 -0
- autorag/node_line.py +65 -0
- autorag/nodes/__init__.py +0 -0
- autorag/nodes/generator/__init__.py +4 -0
- autorag/nodes/generator/base.py +103 -0
- autorag/nodes/generator/llama_index_llm.py +169 -0
- autorag/nodes/generator/openai_llm.py +329 -0
- autorag/nodes/generator/run.py +148 -0
- autorag/nodes/generator/vllm.py +147 -0
- autorag/nodes/generator/vllm_api.py +191 -0
- autorag/nodes/hybridretrieval/__init__.py +2 -0
- autorag/nodes/hybridretrieval/base.py +58 -0
- autorag/nodes/hybridretrieval/hybrid_cc.py +227 -0
- autorag/nodes/hybridretrieval/hybrid_rrf.py +149 -0
- autorag/nodes/hybridretrieval/run.py +137 -0
- autorag/nodes/lexicalretrieval/__init__.py +1 -0
- autorag/nodes/lexicalretrieval/bm25.py +381 -0
- autorag/nodes/lexicalretrieval/run.py +148 -0
- autorag/nodes/passageaugmenter/__init__.py +2 -0
- autorag/nodes/passageaugmenter/base.py +76 -0
- autorag/nodes/passageaugmenter/pass_passage_augmenter.py +43 -0
- autorag/nodes/passageaugmenter/prev_next_augmenter.py +155 -0
- autorag/nodes/passageaugmenter/run.py +131 -0
- autorag/nodes/passagecompressor/__init__.py +4 -0
- autorag/nodes/passagecompressor/base.py +78 -0
- autorag/nodes/passagecompressor/longllmlingua.py +115 -0
- autorag/nodes/passagecompressor/pass_compressor.py +16 -0
- autorag/nodes/passagecompressor/refine.py +54 -0
- autorag/nodes/passagecompressor/run.py +186 -0
- autorag/nodes/passagecompressor/tree_summarize.py +56 -0
- autorag/nodes/passagefilter/__init__.py +6 -0
- autorag/nodes/passagefilter/base.py +40 -0
- autorag/nodes/passagefilter/pass_passage_filter.py +14 -0
- autorag/nodes/passagefilter/percentile_cutoff.py +58 -0
- autorag/nodes/passagefilter/recency.py +105 -0
- autorag/nodes/passagefilter/run.py +138 -0
- autorag/nodes/passagefilter/similarity_percentile_cutoff.py +134 -0
- autorag/nodes/passagefilter/similarity_threshold_cutoff.py +112 -0
- autorag/nodes/passagefilter/threshold_cutoff.py +78 -0
- autorag/nodes/passagereranker/__init__.py +16 -0
- autorag/nodes/passagereranker/base.py +44 -0
- autorag/nodes/passagereranker/cohere.py +118 -0
- autorag/nodes/passagereranker/colbert.py +213 -0
- autorag/nodes/passagereranker/flag_embedding.py +112 -0
- autorag/nodes/passagereranker/flag_embedding_llm.py +101 -0
- autorag/nodes/passagereranker/flashrank.py +245 -0
- autorag/nodes/passagereranker/jina.py +115 -0
- autorag/nodes/passagereranker/koreranker.py +136 -0
- autorag/nodes/passagereranker/mixedbreadai.py +126 -0
- autorag/nodes/passagereranker/monot5.py +190 -0
- autorag/nodes/passagereranker/openvino.py +191 -0
- autorag/nodes/passagereranker/pass_reranker.py +31 -0
- autorag/nodes/passagereranker/rankgpt.py +170 -0
- autorag/nodes/passagereranker/run.py +145 -0
- autorag/nodes/passagereranker/sentence_transformer.py +129 -0
- autorag/nodes/passagereranker/tart/__init__.py +1 -0
- autorag/nodes/passagereranker/tart/modeling_enc_t5.py +152 -0
- autorag/nodes/passagereranker/tart/tart.py +139 -0
- autorag/nodes/passagereranker/tart/tokenization_enc_t5.py +112 -0
- autorag/nodes/passagereranker/time_reranker.py +72 -0
- autorag/nodes/passagereranker/upr.py +160 -0
- autorag/nodes/passagereranker/voyageai.py +109 -0
- autorag/nodes/promptmaker/__init__.py +12 -0
- autorag/nodes/promptmaker/base.py +32 -0
- autorag/nodes/promptmaker/chat_fstring.py +73 -0
- autorag/nodes/promptmaker/fstring.py +49 -0
- autorag/nodes/promptmaker/long_context_reorder.py +83 -0
- autorag/nodes/promptmaker/run.py +283 -0
- autorag/nodes/promptmaker/window_replacement.py +85 -0
- autorag/nodes/queryexpansion/__init__.py +4 -0
- autorag/nodes/queryexpansion/base.py +62 -0
- autorag/nodes/queryexpansion/hyde.py +43 -0
- autorag/nodes/queryexpansion/multi_query_expansion.py +57 -0
- autorag/nodes/queryexpansion/pass_query_expansion.py +22 -0
- autorag/nodes/queryexpansion/query_decompose.py +111 -0
- autorag/nodes/queryexpansion/run.py +308 -0
- autorag/nodes/retrieval/__init__.py +0 -0
- autorag/nodes/retrieval/base.py +127 -0
- autorag/nodes/retrieval/run_util.py +152 -0
- autorag/nodes/semanticretrieval/__init__.py +1 -0
- autorag/nodes/semanticretrieval/run.py +148 -0
- autorag/nodes/semanticretrieval/vectordb.py +339 -0
- autorag/nodes/util.py +16 -0
- autorag/parser.py +37 -0
- autorag/schema/__init__.py +3 -0
- autorag/schema/base.py +35 -0
- autorag/schema/metricinput.py +99 -0
- autorag/schema/module.py +24 -0
- autorag/schema/node.py +144 -0
- autorag/strategy.py +165 -0
- autorag/support.py +235 -0
- autorag/utils/__init__.py +8 -0
- autorag/utils/cast.py +45 -0
- autorag/utils/preprocess.py +149 -0
- autorag/utils/util.py +759 -0
- autorag/validator.py +98 -0
- autorag/vectordb/__init__.py +75 -0
- autorag/vectordb/base.py +73 -0
- autorag/vectordb/chroma.py +118 -0
- autorag/vectordb/couchbase.py +239 -0
- autorag/vectordb/milvus.py +169 -0
- autorag/vectordb/pinecone.py +121 -0
- autorag/vectordb/qdrant.py +155 -0
- autorag/vectordb/weaviate.py +184 -0
- autorag/web.py +81 -0
- autorag-0.0.0.dist-info/METADATA +780 -0
- autorag-0.0.0.dist-info/RECORD +184 -0
- autorag-0.0.0.dist-info/WHEEL +5 -0
- autorag-0.0.0.dist-info/entry_points.txt +2 -0
- autorag-0.0.0.dist-info/licenses/LICENSE +201 -0
- autorag-0.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import pickle
|
|
4
|
+
import re
|
|
5
|
+
from typing import List, Dict, Tuple, Callable, Union, Iterable, Optional
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
from llama_index.core.indices.keyword_table.utils import simple_extract_keywords
|
|
10
|
+
from nltk import PorterStemmer
|
|
11
|
+
from rank_bm25 import BM25Okapi
|
|
12
|
+
from transformers import AutoTokenizer, PreTrainedTokenizerBase
|
|
13
|
+
|
|
14
|
+
from autorag.nodes.retrieval.base import (
|
|
15
|
+
evenly_distribute_passages,
|
|
16
|
+
BaseRetrieval,
|
|
17
|
+
get_bm25_pkl_name,
|
|
18
|
+
)
|
|
19
|
+
from autorag.utils import validate_corpus_dataset, fetch_contents
|
|
20
|
+
from autorag.utils.util import (
|
|
21
|
+
get_event_loop,
|
|
22
|
+
normalize_string,
|
|
23
|
+
result_to_dataframe,
|
|
24
|
+
pop_params,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def tokenize_ko_kiwi(texts: List[str]) -> List[List[str]]:
|
|
29
|
+
try:
|
|
30
|
+
from kiwipiepy import Kiwi, Token
|
|
31
|
+
except ImportError:
|
|
32
|
+
raise ImportError(
|
|
33
|
+
"You need to install kiwipiepy to use 'ko_kiwi' tokenizer. "
|
|
34
|
+
"Please install kiwipiepy by running 'pip install kiwipiepy'. "
|
|
35
|
+
"Or install Korean version of AutoRAG by running 'pip install AutoRAG[ko]'."
|
|
36
|
+
)
|
|
37
|
+
texts = list(map(lambda x: x.strip().lower(), texts))
|
|
38
|
+
kiwi = Kiwi()
|
|
39
|
+
tokenized_list: Iterable[List[Token]] = kiwi.tokenize(texts)
|
|
40
|
+
|
|
41
|
+
def extract_form_safe(x):
|
|
42
|
+
try:
|
|
43
|
+
return x.form
|
|
44
|
+
except UnicodeDecodeError:
|
|
45
|
+
return " "
|
|
46
|
+
|
|
47
|
+
return [
|
|
48
|
+
list(map(lambda x: extract_form_safe(x), token_list))
|
|
49
|
+
for token_list in tokenized_list
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def tokenize_ko_kkma(texts: List[str]) -> List[List[str]]:
|
|
54
|
+
try:
|
|
55
|
+
from konlpy.tag import Kkma
|
|
56
|
+
except ImportError:
|
|
57
|
+
raise ImportError(
|
|
58
|
+
"You need to install konlpy to use 'ko_kkma' tokenizer. "
|
|
59
|
+
"Please install konlpy by running 'pip install konlpy'. "
|
|
60
|
+
"Or install Korean version of AutoRAG by running 'pip install AutoRAG[ko]'."
|
|
61
|
+
)
|
|
62
|
+
tokenizer = Kkma()
|
|
63
|
+
tokenized_list: List[List[str]] = list(map(lambda x: tokenizer.morphs(x), texts))
|
|
64
|
+
return tokenized_list
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def tokenize_ko_okt(texts: List[str]) -> List[List[str]]:
|
|
68
|
+
try:
|
|
69
|
+
from konlpy.tag import Okt
|
|
70
|
+
except ImportError:
|
|
71
|
+
raise ImportError(
|
|
72
|
+
"You need to install konlpy to use 'ko_kkma' tokenizer. "
|
|
73
|
+
"Please install konlpy by running 'pip install konlpy'. "
|
|
74
|
+
"Or install Korean version of AutoRAG by running 'pip install AutoRAG[ko]'."
|
|
75
|
+
)
|
|
76
|
+
tokenizer = Okt()
|
|
77
|
+
tokenized_list: List[List[str]] = list(map(lambda x: tokenizer.morphs(x), texts))
|
|
78
|
+
return tokenized_list
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def tokenize_porter_stemmer(texts: List[str]) -> List[List[str]]:
|
|
82
|
+
def tokenize_remove_stopword(text: str, stemmer) -> List[str]:
|
|
83
|
+
text = text.lower()
|
|
84
|
+
words = list(simple_extract_keywords(text))
|
|
85
|
+
return [stemmer.stem(word) for word in words]
|
|
86
|
+
|
|
87
|
+
stemmer = PorterStemmer()
|
|
88
|
+
tokenized_list: List[List[str]] = list(
|
|
89
|
+
map(lambda x: tokenize_remove_stopword(x, stemmer), texts)
|
|
90
|
+
)
|
|
91
|
+
return tokenized_list
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def tokenize_space(texts: List[str]) -> List[List[str]]:
|
|
95
|
+
def tokenize_space_text(text: str) -> List[str]:
|
|
96
|
+
text = normalize_string(text)
|
|
97
|
+
return re.split(r"\s+", text.strip())
|
|
98
|
+
|
|
99
|
+
return list(map(tokenize_space_text, texts))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def load_bm25_corpus(bm25_path: str) -> Dict:
|
|
103
|
+
if bm25_path is None:
|
|
104
|
+
return {}
|
|
105
|
+
with open(bm25_path, "rb") as f:
|
|
106
|
+
bm25_corpus = pickle.load(f)
|
|
107
|
+
return bm25_corpus
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def tokenize_ja_sudachipy(texts: List[str]) -> List[List[str]]:
|
|
111
|
+
try:
|
|
112
|
+
from sudachipy import dictionary, tokenizer
|
|
113
|
+
except ImportError:
|
|
114
|
+
raise ImportError(
|
|
115
|
+
"You need to install SudachiPy to use 'sudachipy' tokenizer. "
|
|
116
|
+
"Please install SudachiPy by running 'pip install sudachipy'."
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Initialize SudachiPy with the default tokenizer
|
|
120
|
+
tokenizer_obj = dictionary.Dictionary(dict="core").create()
|
|
121
|
+
|
|
122
|
+
# Choose the tokenizer mode: NORMAL, SEARCH, A
|
|
123
|
+
mode = tokenizer.Tokenizer.SplitMode.A
|
|
124
|
+
|
|
125
|
+
# Tokenize the input texts
|
|
126
|
+
tokenized_list = []
|
|
127
|
+
for text in texts:
|
|
128
|
+
tokens = tokenizer_obj.tokenize(text, mode)
|
|
129
|
+
tokenized_list.append([token.surface() for token in tokens])
|
|
130
|
+
|
|
131
|
+
return tokenized_list
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
BM25_TOKENIZER = {
|
|
135
|
+
"porter_stemmer": tokenize_porter_stemmer,
|
|
136
|
+
"ko_kiwi": tokenize_ko_kiwi,
|
|
137
|
+
"space": tokenize_space,
|
|
138
|
+
"ko_kkma": tokenize_ko_kkma,
|
|
139
|
+
"ko_okt": tokenize_ko_okt,
|
|
140
|
+
"sudachipy": tokenize_ja_sudachipy,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class BM25(BaseRetrieval):
|
|
145
|
+
def __init__(self, project_dir: str, *args, **kwargs):
|
|
146
|
+
"""
|
|
147
|
+
Initialize BM25 module.
|
|
148
|
+
(Retrieval)
|
|
149
|
+
|
|
150
|
+
:param project_dir: The project directory path.
|
|
151
|
+
:param bm25_tokenizer: The tokenizer name that is used to the BM25.
|
|
152
|
+
It supports 'porter_stemmer', 'ko_kiwi', and huggingface `AutoTokenizer`.
|
|
153
|
+
You can pass huggingface tokenizer name.
|
|
154
|
+
Default is porter_stemmer.
|
|
155
|
+
:param kwargs: The optional arguments.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
super().__init__(project_dir)
|
|
159
|
+
# check if bm25_path and file exist
|
|
160
|
+
bm25_tokenizer = kwargs.get("bm25_tokenizer", None)
|
|
161
|
+
if bm25_tokenizer is None:
|
|
162
|
+
bm25_tokenizer = "porter_stemmer"
|
|
163
|
+
bm25_path = os.path.join(self.resources_dir, get_bm25_pkl_name(bm25_tokenizer))
|
|
164
|
+
|
|
165
|
+
assert bm25_path is not None, (
|
|
166
|
+
"bm25_path must be specified for using bm25 retrieval."
|
|
167
|
+
)
|
|
168
|
+
assert os.path.exists(bm25_path), (
|
|
169
|
+
f"bm25_path {bm25_path} does not exist. Please ingest first."
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
self.bm25_corpus = load_bm25_corpus(bm25_path)
|
|
173
|
+
assert "tokens" and "passage_id" in list(self.bm25_corpus.keys()), (
|
|
174
|
+
"bm25_corpus must contain tokens and passage_id. Please check you ingested bm25 corpus correctly."
|
|
175
|
+
)
|
|
176
|
+
self.tokenizer = select_bm25_tokenizer(bm25_tokenizer)
|
|
177
|
+
assert self.bm25_corpus["tokenizer_name"] == bm25_tokenizer, (
|
|
178
|
+
f"The bm25 corpus tokenizer is {self.bm25_corpus['tokenizer_name']}, but your input is {bm25_tokenizer}. "
|
|
179
|
+
f"You need to ingest again. Delete bm25 pkl file and re-ingest it."
|
|
180
|
+
)
|
|
181
|
+
self.bm25_instance = BM25Okapi(self.bm25_corpus["tokens"])
|
|
182
|
+
|
|
183
|
+
@result_to_dataframe(
|
|
184
|
+
[
|
|
185
|
+
"retrieved_contents_lexical",
|
|
186
|
+
"retrieved_ids_lexical",
|
|
187
|
+
"retrieve_scores_lexical",
|
|
188
|
+
]
|
|
189
|
+
)
|
|
190
|
+
def pure(self, previous_result: pd.DataFrame, *args, **kwargs):
|
|
191
|
+
queries = self.cast_to_run(previous_result)
|
|
192
|
+
pure_params = pop_params(self._pure, kwargs)
|
|
193
|
+
ids, scores = self._pure(queries, *args, **pure_params)
|
|
194
|
+
contents = fetch_contents(self.corpus_df, ids)
|
|
195
|
+
return contents, ids, scores
|
|
196
|
+
|
|
197
|
+
def _pure(
|
|
198
|
+
self,
|
|
199
|
+
queries: List[List[str]],
|
|
200
|
+
top_k: int,
|
|
201
|
+
ids: Optional[List[List[str]]] = None,
|
|
202
|
+
) -> Tuple[List[List[str]], List[List[float]]]:
|
|
203
|
+
"""
|
|
204
|
+
BM25 retrieval function.
|
|
205
|
+
You have to load a pickle file that is already ingested.
|
|
206
|
+
|
|
207
|
+
:param queries: 2-d list of query strings.
|
|
208
|
+
Each element of the list is a query strings of each row.
|
|
209
|
+
:param top_k: The number of passages to be retrieved.
|
|
210
|
+
:param ids: The optional list of ids that you want to retrieve.
|
|
211
|
+
You don't need to specify this in the general use cases.
|
|
212
|
+
Default is None.
|
|
213
|
+
:return: The 2-d list contains a list of passage ids that retrieved from bm25 and 2-d list of its scores.
|
|
214
|
+
It will be a length of queries. And each element has a length of top_k.
|
|
215
|
+
"""
|
|
216
|
+
if ids is not None:
|
|
217
|
+
score_result = list(
|
|
218
|
+
map(
|
|
219
|
+
lambda query_list, id_list: get_bm25_scores(
|
|
220
|
+
query_list,
|
|
221
|
+
id_list,
|
|
222
|
+
self.tokenizer,
|
|
223
|
+
self.bm25_instance,
|
|
224
|
+
self.bm25_corpus,
|
|
225
|
+
),
|
|
226
|
+
queries,
|
|
227
|
+
ids,
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
return ids, score_result
|
|
231
|
+
|
|
232
|
+
# run async bm25_pure function
|
|
233
|
+
tasks = [
|
|
234
|
+
bm25_pure(
|
|
235
|
+
input_queries,
|
|
236
|
+
top_k,
|
|
237
|
+
self.tokenizer,
|
|
238
|
+
self.bm25_instance,
|
|
239
|
+
self.bm25_corpus,
|
|
240
|
+
)
|
|
241
|
+
for input_queries in queries
|
|
242
|
+
]
|
|
243
|
+
loop = get_event_loop()
|
|
244
|
+
results = loop.run_until_complete(asyncio.gather(*tasks))
|
|
245
|
+
id_result = list(map(lambda x: x[0], results))
|
|
246
|
+
score_result = list(map(lambda x: x[1], results))
|
|
247
|
+
return id_result, score_result
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
async def bm25_pure(
|
|
251
|
+
queries: List[str], top_k: int, tokenizer, bm25_api: BM25Okapi, bm25_corpus: Dict
|
|
252
|
+
) -> Tuple[List[str], List[float]]:
|
|
253
|
+
"""
|
|
254
|
+
Async BM25 retrieval function.
|
|
255
|
+
Its usage is for async retrieval of bm25 row by row.
|
|
256
|
+
|
|
257
|
+
:param queries: A list of query strings.
|
|
258
|
+
:param top_k: The number of passages to be retrieved.
|
|
259
|
+
:param tokenizer: A tokenizer that will be used to tokenize queries.
|
|
260
|
+
:param bm25_api: A bm25 api instance that will be used to retrieve passages.
|
|
261
|
+
:param bm25_corpus: A dictionary containing the bm25 corpus, which is doc_id from corpus and tokenized corpus.
|
|
262
|
+
Its data structure looks like this:
|
|
263
|
+
|
|
264
|
+
.. Code:: python
|
|
265
|
+
|
|
266
|
+
{
|
|
267
|
+
"tokens": [], # 2d list of tokens
|
|
268
|
+
"passage_id": [], # 2d list of passage_id. Type must be str.
|
|
269
|
+
}
|
|
270
|
+
:return: The tuple contains a list of passage ids that retrieved from bm25 and its scores.
|
|
271
|
+
"""
|
|
272
|
+
# I don't make queries operation to async, because queries length might be small, so it will occur overhead.
|
|
273
|
+
tokenized_queries = tokenize(queries, tokenizer)
|
|
274
|
+
id_result = []
|
|
275
|
+
score_result = []
|
|
276
|
+
for query in tokenized_queries:
|
|
277
|
+
scores = bm25_api.get_scores(query)
|
|
278
|
+
sorted_scores = sorted(scores, reverse=True)
|
|
279
|
+
top_n_index = np.argsort(scores)[::-1][:top_k]
|
|
280
|
+
ids = [bm25_corpus["passage_id"][i] for i in top_n_index]
|
|
281
|
+
id_result.append(ids)
|
|
282
|
+
score_result.append(sorted_scores[:top_k])
|
|
283
|
+
|
|
284
|
+
# make a total result to top_k
|
|
285
|
+
id_result, score_result = evenly_distribute_passages(id_result, score_result, top_k)
|
|
286
|
+
# sort id_result and score_result by score
|
|
287
|
+
result = [
|
|
288
|
+
(_id, score)
|
|
289
|
+
for score, _id in sorted(
|
|
290
|
+
zip(score_result, id_result), key=lambda pair: pair[0], reverse=True
|
|
291
|
+
)
|
|
292
|
+
]
|
|
293
|
+
id_result, score_result = zip(*result)
|
|
294
|
+
return list(id_result), list(score_result)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def get_bm25_scores(
|
|
298
|
+
queries: List[str],
|
|
299
|
+
ids: List[str],
|
|
300
|
+
tokenizer,
|
|
301
|
+
bm25_api: BM25Okapi,
|
|
302
|
+
bm25_corpus: Dict,
|
|
303
|
+
) -> List[float]:
|
|
304
|
+
if len(ids) == 0 or not bool(ids):
|
|
305
|
+
return []
|
|
306
|
+
tokenized_queries = tokenize(queries, tokenizer)
|
|
307
|
+
result_dict = {id_: [] for id_ in ids}
|
|
308
|
+
for query in tokenized_queries:
|
|
309
|
+
scores = bm25_api.get_scores(query)
|
|
310
|
+
for i, id_ in enumerate(ids):
|
|
311
|
+
result_dict[id_].append(scores[bm25_corpus["passage_id"].index(id_)])
|
|
312
|
+
result_df = pd.DataFrame(result_dict)
|
|
313
|
+
return result_df.max(axis=0).tolist()
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def tokenize(queries: List[str], tokenizer) -> List[List[int]]:
|
|
317
|
+
if isinstance(tokenizer, PreTrainedTokenizerBase):
|
|
318
|
+
tokenized_queries = tokenizer(queries).input_ids
|
|
319
|
+
else:
|
|
320
|
+
tokenized_queries = tokenizer(queries)
|
|
321
|
+
return tokenized_queries
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def bm25_ingest(
|
|
325
|
+
corpus_path: str, corpus_data: pd.DataFrame, bm25_tokenizer: str = "porter_stemmer"
|
|
326
|
+
):
|
|
327
|
+
if not corpus_path.endswith(".pkl"):
|
|
328
|
+
raise ValueError(f"Corpus path {corpus_path} is not a pickle file.")
|
|
329
|
+
validate_corpus_dataset(corpus_data)
|
|
330
|
+
ids = corpus_data["doc_id"].tolist()
|
|
331
|
+
|
|
332
|
+
# Initialize bm25_corpus
|
|
333
|
+
bm25_corpus = pd.DataFrame()
|
|
334
|
+
|
|
335
|
+
# Load the BM25 corpus if it exists and get the passage ids
|
|
336
|
+
if os.path.exists(corpus_path) and os.path.getsize(corpus_path) > 0:
|
|
337
|
+
with open(corpus_path, "rb") as r:
|
|
338
|
+
corpus = pickle.load(r)
|
|
339
|
+
bm25_corpus = pd.DataFrame.from_dict(corpus)
|
|
340
|
+
duplicated_passage_rows = bm25_corpus[bm25_corpus["passage_id"].isin(ids)]
|
|
341
|
+
new_passage = corpus_data[
|
|
342
|
+
~corpus_data["doc_id"].isin(duplicated_passage_rows["passage_id"])
|
|
343
|
+
]
|
|
344
|
+
else:
|
|
345
|
+
new_passage = corpus_data
|
|
346
|
+
|
|
347
|
+
if not new_passage.empty:
|
|
348
|
+
tokenizer = select_bm25_tokenizer(bm25_tokenizer)
|
|
349
|
+
if isinstance(tokenizer, PreTrainedTokenizerBase):
|
|
350
|
+
tokenized_corpus = tokenizer(new_passage["contents"].tolist()).input_ids
|
|
351
|
+
else:
|
|
352
|
+
tokenized_corpus = tokenizer(new_passage["contents"].tolist())
|
|
353
|
+
new_bm25_corpus = pd.DataFrame(
|
|
354
|
+
{
|
|
355
|
+
"tokens": tokenized_corpus,
|
|
356
|
+
"passage_id": new_passage["doc_id"].tolist(),
|
|
357
|
+
}
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
if not bm25_corpus.empty:
|
|
361
|
+
bm25_corpus_updated = pd.concat(
|
|
362
|
+
[bm25_corpus, new_bm25_corpus], ignore_index=True
|
|
363
|
+
)
|
|
364
|
+
bm25_dict = bm25_corpus_updated.to_dict("list")
|
|
365
|
+
else:
|
|
366
|
+
bm25_dict = new_bm25_corpus.to_dict("list")
|
|
367
|
+
|
|
368
|
+
# add tokenizer name to bm25_dict
|
|
369
|
+
bm25_dict["tokenizer_name"] = bm25_tokenizer
|
|
370
|
+
|
|
371
|
+
with open(corpus_path, "wb") as w:
|
|
372
|
+
pickle.dump(bm25_dict, w)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def select_bm25_tokenizer(
|
|
376
|
+
bm25_tokenizer: str,
|
|
377
|
+
) -> Callable[[str], List[Union[int, str]]]:
|
|
378
|
+
if bm25_tokenizer in list(BM25_TOKENIZER.keys()):
|
|
379
|
+
return BM25_TOKENIZER[bm25_tokenizer]
|
|
380
|
+
|
|
381
|
+
return AutoTokenizer.from_pretrained(bm25_tokenizer, use_fast=False)
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pathlib
|
|
3
|
+
from typing import List, Dict, Union
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
from autorag.evaluation import evaluate_retrieval
|
|
8
|
+
from autorag.evaluation.retrieval import RETRIEVAL_METRIC_FUNC_DICT
|
|
9
|
+
from autorag.nodes.retrieval.run_util import save_and_summary, find_best
|
|
10
|
+
from autorag.schema.metricinput import MetricInput
|
|
11
|
+
from autorag.strategy import measure_speed
|
|
12
|
+
from autorag.utils.util import apply_recursive, to_list
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def run_lexical_retrieval_node(
|
|
16
|
+
modules: List,
|
|
17
|
+
module_params: List[Dict],
|
|
18
|
+
previous_result: pd.DataFrame,
|
|
19
|
+
node_line_dir: str,
|
|
20
|
+
strategies: Dict,
|
|
21
|
+
) -> pd.DataFrame:
|
|
22
|
+
"""
|
|
23
|
+
Run the semantic retrieval node.
|
|
24
|
+
|
|
25
|
+
:param modules: Retrieval modules to run.
|
|
26
|
+
:param module_params: Retrieval module parameters.
|
|
27
|
+
:param previous_result: Previous result dataframe.
|
|
28
|
+
Could be query expansion's best result or qa data.
|
|
29
|
+
:param node_line_dir: This node line's directory.
|
|
30
|
+
:param strategies: Strategies for retrieval node.
|
|
31
|
+
:return: The best result dataframe.
|
|
32
|
+
It contains previous result columns and retrieval node's result columns.
|
|
33
|
+
"""
|
|
34
|
+
if not os.path.exists(node_line_dir):
|
|
35
|
+
os.makedirs(node_line_dir)
|
|
36
|
+
project_dir = pathlib.PurePath(node_line_dir).parent.parent
|
|
37
|
+
qa_df = pd.read_parquet(
|
|
38
|
+
os.path.join(project_dir, "data", "qa.parquet"), engine="pyarrow"
|
|
39
|
+
)
|
|
40
|
+
retrieval_gt = qa_df["retrieval_gt"].tolist()
|
|
41
|
+
retrieval_gt = apply_recursive(lambda x: str(x), to_list(retrieval_gt))
|
|
42
|
+
# make rows to metric_inputs
|
|
43
|
+
metric_inputs = [
|
|
44
|
+
MetricInput(retrieval_gt=ret_gt, query=query, generation_gt=gen_gt)
|
|
45
|
+
for ret_gt, query, gen_gt in zip(
|
|
46
|
+
retrieval_gt, qa_df["query"].tolist(), qa_df["generation_gt"].tolist()
|
|
47
|
+
)
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
save_dir = os.path.join(node_line_dir, "lexical_retrieval")
|
|
51
|
+
if not os.path.exists(save_dir):
|
|
52
|
+
os.makedirs(save_dir)
|
|
53
|
+
|
|
54
|
+
# Run the modules
|
|
55
|
+
lexical_results, execution_times = zip(
|
|
56
|
+
*map(
|
|
57
|
+
lambda task: measure_speed(
|
|
58
|
+
task[0].run_evaluator,
|
|
59
|
+
project_dir=project_dir,
|
|
60
|
+
previous_result=previous_result,
|
|
61
|
+
**task[1],
|
|
62
|
+
),
|
|
63
|
+
zip(modules, module_params),
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
lexical_times = list(map(lambda x: x / len(lexical_results[0]), execution_times))
|
|
67
|
+
|
|
68
|
+
# run metrics
|
|
69
|
+
if strategies.get("metrics") is None:
|
|
70
|
+
raise ValueError("You must at least one metrics for retrieval evaluation.")
|
|
71
|
+
lexical_results = list(
|
|
72
|
+
map(
|
|
73
|
+
lambda x: evaluate_lexical_retrieval_node(
|
|
74
|
+
x,
|
|
75
|
+
metric_inputs,
|
|
76
|
+
strategies.get("metrics"),
|
|
77
|
+
),
|
|
78
|
+
lexical_results,
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
lexical_summary_df = save_and_summary(
|
|
83
|
+
modules,
|
|
84
|
+
module_params,
|
|
85
|
+
lexical_results,
|
|
86
|
+
lexical_times,
|
|
87
|
+
0,
|
|
88
|
+
save_dir,
|
|
89
|
+
strategies,
|
|
90
|
+
)
|
|
91
|
+
lexical_selected_result, lexical_selected_filename = find_best(
|
|
92
|
+
lexical_results,
|
|
93
|
+
lexical_times,
|
|
94
|
+
lexical_summary_df["filename"].tolist(),
|
|
95
|
+
strategies,
|
|
96
|
+
)
|
|
97
|
+
lexical_summary_df["is_best"] = (
|
|
98
|
+
lexical_summary_df["filename"] == lexical_selected_filename
|
|
99
|
+
)
|
|
100
|
+
previous_result.drop(
|
|
101
|
+
columns=list(RETRIEVAL_METRIC_FUNC_DICT.keys()), inplace=True, errors="ignore"
|
|
102
|
+
)
|
|
103
|
+
lexical_selected_result.rename(
|
|
104
|
+
columns={
|
|
105
|
+
"retrieved_contents": "retrieved_contents_lexical",
|
|
106
|
+
"retrieved_ids": "retrieved_ids_lexical",
|
|
107
|
+
"retrieve_scores": "retrieve_scores_lexical",
|
|
108
|
+
},
|
|
109
|
+
inplace=True,
|
|
110
|
+
)
|
|
111
|
+
best_result = pd.concat([previous_result, lexical_selected_result], axis=1)
|
|
112
|
+
best_result.to_parquet(
|
|
113
|
+
os.path.join(
|
|
114
|
+
save_dir, f"best_{os.path.splitext(lexical_selected_filename)[0]}.parquet"
|
|
115
|
+
),
|
|
116
|
+
index=False,
|
|
117
|
+
)
|
|
118
|
+
lexical_summary_df.to_csv(os.path.join(save_dir, "summary.csv"), index=False)
|
|
119
|
+
return best_result
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def evaluate_lexical_retrieval_node(
|
|
123
|
+
result_df: pd.DataFrame,
|
|
124
|
+
metric_inputs: List[MetricInput],
|
|
125
|
+
metrics: Union[List[str], List[Dict]],
|
|
126
|
+
) -> pd.DataFrame:
|
|
127
|
+
"""
|
|
128
|
+
Evaluate retrieval node from retrieval node result dataframe.
|
|
129
|
+
|
|
130
|
+
:param result_df: The result dataframe from a retrieval node.
|
|
131
|
+
:param metric_inputs: List of metric input schema for AutoRAG.
|
|
132
|
+
:param metrics: Metric list from input strategies.
|
|
133
|
+
:return: Return result_df with metrics columns.
|
|
134
|
+
The columns will be 'retrieved_contents_lexical', 'retrieved_ids_lexical', 'retrieve_scores_lexical', and metric names.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
@evaluate_retrieval(
|
|
138
|
+
metric_inputs=metric_inputs,
|
|
139
|
+
metrics=metrics,
|
|
140
|
+
)
|
|
141
|
+
def evaluate_this_module(df: pd.DataFrame):
|
|
142
|
+
return (
|
|
143
|
+
df["retrieved_contents_lexical"].tolist(),
|
|
144
|
+
df["retrieved_ids_lexical"].tolist(),
|
|
145
|
+
df["retrieve_scores_lexical"].tolist(),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return evaluate_this_module(result_df)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
from autorag.schema import BaseModule
|
|
8
|
+
from autorag.utils import (
|
|
9
|
+
validate_qa_dataset,
|
|
10
|
+
sort_by_scores,
|
|
11
|
+
validate_corpus_dataset,
|
|
12
|
+
cast_corpus_dataset,
|
|
13
|
+
)
|
|
14
|
+
from autorag.utils.cast import cast_retrieved_ids
|
|
15
|
+
from autorag.utils.util import select_top_k
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger("AutoRAG")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BasePassageAugmenter(BaseModule, metaclass=abc.ABCMeta):
|
|
21
|
+
def __init__(self, project_dir: str, *args, **kwargs):
|
|
22
|
+
logger.info(
|
|
23
|
+
f"Initialize passage augmenter node - {self.__class__.__name__} module..."
|
|
24
|
+
)
|
|
25
|
+
data_dir = os.path.join(project_dir, "data")
|
|
26
|
+
corpus_df = pd.read_parquet(
|
|
27
|
+
os.path.join(data_dir, "corpus.parquet"), engine="pyarrow"
|
|
28
|
+
)
|
|
29
|
+
validate_corpus_dataset(corpus_df)
|
|
30
|
+
corpus_df = cast_corpus_dataset(corpus_df)
|
|
31
|
+
self.corpus_df = corpus_df
|
|
32
|
+
|
|
33
|
+
def __del__(self):
|
|
34
|
+
logger.info(
|
|
35
|
+
f"Initialize passage augmenter node - {self.__class__.__name__} module..."
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def cast_to_run(self, previous_result: pd.DataFrame, *args, **kwargs):
|
|
39
|
+
logger.info(
|
|
40
|
+
f"Running passage augmenter node - {self.__class__.__name__} module..."
|
|
41
|
+
)
|
|
42
|
+
validate_qa_dataset(previous_result)
|
|
43
|
+
|
|
44
|
+
# find ids columns
|
|
45
|
+
return cast_retrieved_ids(previous_result)
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def sort_by_scores(
|
|
49
|
+
augmented_contents,
|
|
50
|
+
augmented_ids,
|
|
51
|
+
augmented_scores,
|
|
52
|
+
top_k: int,
|
|
53
|
+
reverse: bool = True,
|
|
54
|
+
):
|
|
55
|
+
# sort by scores
|
|
56
|
+
df = pd.DataFrame(
|
|
57
|
+
{
|
|
58
|
+
"contents": augmented_contents,
|
|
59
|
+
"ids": augmented_ids,
|
|
60
|
+
"scores": augmented_scores,
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
df[["contents", "ids", "scores"]] = df.apply(
|
|
64
|
+
lambda row: sort_by_scores(row, reverse=reverse),
|
|
65
|
+
axis=1,
|
|
66
|
+
result_type="expand",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# select by top_k
|
|
70
|
+
results = select_top_k(df, ["contents", "ids", "scores"], top_k)
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
results["contents"].tolist(),
|
|
74
|
+
results["ids"].tolist(),
|
|
75
|
+
results["scores"].tolist(),
|
|
76
|
+
)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
from autorag.nodes.passageaugmenter.base import BasePassageAugmenter
|
|
6
|
+
from autorag.utils import result_to_dataframe
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PassPassageAugmenter(BasePassageAugmenter):
|
|
10
|
+
@result_to_dataframe(["retrieved_contents", "retrieved_ids", "retrieve_scores"])
|
|
11
|
+
def pure(self, previous_result: pd.DataFrame, *args, **kwargs):
|
|
12
|
+
"""
|
|
13
|
+
Run the passage augmenter node - PassPassageAugmenter module.
|
|
14
|
+
|
|
15
|
+
:param previous_result: The previous result Dataframe.
|
|
16
|
+
:param top_k: You must input the top_k value to get the top k results.
|
|
17
|
+
:param kwargs: Not affected.
|
|
18
|
+
:return: DataFrame with retrieved_contents, retrieved_ids, and retrieve_scores columns
|
|
19
|
+
"""
|
|
20
|
+
top_k = kwargs.pop("top_k")
|
|
21
|
+
|
|
22
|
+
ids = self.cast_to_run(previous_result)
|
|
23
|
+
contents = previous_result["retrieved_contents"].tolist()
|
|
24
|
+
scores = previous_result["retrieve_scores"].tolist()
|
|
25
|
+
|
|
26
|
+
augmented_ids, augmented_contents, augmented_scores = self._pure(
|
|
27
|
+
ids, contents, scores
|
|
28
|
+
)
|
|
29
|
+
return self.sort_by_scores(
|
|
30
|
+
augmented_contents, augmented_ids, augmented_scores, top_k
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def _pure(
|
|
34
|
+
self,
|
|
35
|
+
ids_list: List[List[str]],
|
|
36
|
+
contents_list: List[List[str]],
|
|
37
|
+
scores_list: List[List[float]],
|
|
38
|
+
):
|
|
39
|
+
"""
|
|
40
|
+
Do not perform augmentation.
|
|
41
|
+
Return given passages, scores, and ids as is.
|
|
42
|
+
"""
|
|
43
|
+
return ids_list, contents_list, scores_list
|