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.
Files changed (184) hide show
  1. autorag/__init__.py +82 -0
  2. autorag/chunker.py +51 -0
  3. autorag/cli.py +209 -0
  4. autorag/dashboard.py +199 -0
  5. autorag/data/__init__.py +109 -0
  6. autorag/data/chunk/__init__.py +2 -0
  7. autorag/data/chunk/base.py +128 -0
  8. autorag/data/chunk/langchain_chunk.py +76 -0
  9. autorag/data/chunk/llama_index_chunk.py +96 -0
  10. autorag/data/chunk/run.py +38 -0
  11. autorag/data/legacy/__init__.py +0 -0
  12. autorag/data/legacy/corpus/__init__.py +2 -0
  13. autorag/data/legacy/corpus/langchain.py +47 -0
  14. autorag/data/legacy/corpus/llama_index.py +93 -0
  15. autorag/data/legacy/qacreation/__init__.py +6 -0
  16. autorag/data/legacy/qacreation/base.py +239 -0
  17. autorag/data/legacy/qacreation/llama_index.py +253 -0
  18. autorag/data/legacy/qacreation/llama_index_default_prompt.txt +54 -0
  19. autorag/data/legacy/qacreation/ragas.py +75 -0
  20. autorag/data/legacy/qacreation/simple.py +99 -0
  21. autorag/data/parse/__init__.py +1 -0
  22. autorag/data/parse/base.py +79 -0
  23. autorag/data/parse/clova.py +194 -0
  24. autorag/data/parse/langchain_parse.py +87 -0
  25. autorag/data/parse/llamaparse.py +126 -0
  26. autorag/data/parse/run.py +141 -0
  27. autorag/data/parse/table_hybrid_parse.py +134 -0
  28. autorag/data/qa/__init__.py +3 -0
  29. autorag/data/qa/evolve/__init__.py +0 -0
  30. autorag/data/qa/evolve/llama_index_query_evolve.py +64 -0
  31. autorag/data/qa/evolve/openai_query_evolve.py +81 -0
  32. autorag/data/qa/evolve/prompt.py +288 -0
  33. autorag/data/qa/extract_evidence.py +1 -0
  34. autorag/data/qa/filter/__init__.py +0 -0
  35. autorag/data/qa/filter/dontknow.py +117 -0
  36. autorag/data/qa/filter/passage_dependency.py +88 -0
  37. autorag/data/qa/filter/prompt.py +73 -0
  38. autorag/data/qa/generation_gt/__init__.py +0 -0
  39. autorag/data/qa/generation_gt/base.py +16 -0
  40. autorag/data/qa/generation_gt/llama_index_gen_gt.py +41 -0
  41. autorag/data/qa/generation_gt/openai_gen_gt.py +84 -0
  42. autorag/data/qa/generation_gt/prompt.py +27 -0
  43. autorag/data/qa/query/__init__.py +0 -0
  44. autorag/data/qa/query/llama_gen_query.py +82 -0
  45. autorag/data/qa/query/openai_gen_query.py +95 -0
  46. autorag/data/qa/query/prompt.py +201 -0
  47. autorag/data/qa/sample.py +26 -0
  48. autorag/data/qa/schema.py +322 -0
  49. autorag/data/utils/__init__.py +0 -0
  50. autorag/data/utils/util.py +103 -0
  51. autorag/deploy/__init__.py +9 -0
  52. autorag/deploy/api.py +303 -0
  53. autorag/deploy/base.py +235 -0
  54. autorag/deploy/gradio.py +74 -0
  55. autorag/deploy/swagger.yml +202 -0
  56. autorag/embedding/__init__.py +0 -0
  57. autorag/embedding/base.py +144 -0
  58. autorag/embedding/vllm.py +256 -0
  59. autorag/evaluation/__init__.py +3 -0
  60. autorag/evaluation/generation.py +88 -0
  61. autorag/evaluation/metric/__init__.py +22 -0
  62. autorag/evaluation/metric/deepeval_prompt.py +322 -0
  63. autorag/evaluation/metric/g_eval_prompts/coh_detailed.txt +32 -0
  64. autorag/evaluation/metric/g_eval_prompts/con_detailed.txt +33 -0
  65. autorag/evaluation/metric/g_eval_prompts/flu_detailed.txt +26 -0
  66. autorag/evaluation/metric/g_eval_prompts/rel_detailed.txt +33 -0
  67. autorag/evaluation/metric/generation.py +504 -0
  68. autorag/evaluation/metric/retrieval.py +115 -0
  69. autorag/evaluation/metric/retrieval_contents.py +65 -0
  70. autorag/evaluation/metric/util.py +88 -0
  71. autorag/evaluation/retrieval.py +83 -0
  72. autorag/evaluation/retrieval_contents.py +65 -0
  73. autorag/evaluation/util.py +43 -0
  74. autorag/evaluator.py +559 -0
  75. autorag/node_line.py +65 -0
  76. autorag/nodes/__init__.py +0 -0
  77. autorag/nodes/generator/__init__.py +4 -0
  78. autorag/nodes/generator/base.py +103 -0
  79. autorag/nodes/generator/llama_index_llm.py +169 -0
  80. autorag/nodes/generator/openai_llm.py +329 -0
  81. autorag/nodes/generator/run.py +148 -0
  82. autorag/nodes/generator/vllm.py +147 -0
  83. autorag/nodes/generator/vllm_api.py +191 -0
  84. autorag/nodes/hybridretrieval/__init__.py +2 -0
  85. autorag/nodes/hybridretrieval/base.py +58 -0
  86. autorag/nodes/hybridretrieval/hybrid_cc.py +227 -0
  87. autorag/nodes/hybridretrieval/hybrid_rrf.py +149 -0
  88. autorag/nodes/hybridretrieval/run.py +137 -0
  89. autorag/nodes/lexicalretrieval/__init__.py +1 -0
  90. autorag/nodes/lexicalretrieval/bm25.py +381 -0
  91. autorag/nodes/lexicalretrieval/run.py +148 -0
  92. autorag/nodes/passageaugmenter/__init__.py +2 -0
  93. autorag/nodes/passageaugmenter/base.py +76 -0
  94. autorag/nodes/passageaugmenter/pass_passage_augmenter.py +43 -0
  95. autorag/nodes/passageaugmenter/prev_next_augmenter.py +155 -0
  96. autorag/nodes/passageaugmenter/run.py +131 -0
  97. autorag/nodes/passagecompressor/__init__.py +4 -0
  98. autorag/nodes/passagecompressor/base.py +78 -0
  99. autorag/nodes/passagecompressor/longllmlingua.py +115 -0
  100. autorag/nodes/passagecompressor/pass_compressor.py +16 -0
  101. autorag/nodes/passagecompressor/refine.py +54 -0
  102. autorag/nodes/passagecompressor/run.py +186 -0
  103. autorag/nodes/passagecompressor/tree_summarize.py +56 -0
  104. autorag/nodes/passagefilter/__init__.py +6 -0
  105. autorag/nodes/passagefilter/base.py +40 -0
  106. autorag/nodes/passagefilter/pass_passage_filter.py +14 -0
  107. autorag/nodes/passagefilter/percentile_cutoff.py +58 -0
  108. autorag/nodes/passagefilter/recency.py +105 -0
  109. autorag/nodes/passagefilter/run.py +138 -0
  110. autorag/nodes/passagefilter/similarity_percentile_cutoff.py +134 -0
  111. autorag/nodes/passagefilter/similarity_threshold_cutoff.py +112 -0
  112. autorag/nodes/passagefilter/threshold_cutoff.py +78 -0
  113. autorag/nodes/passagereranker/__init__.py +16 -0
  114. autorag/nodes/passagereranker/base.py +44 -0
  115. autorag/nodes/passagereranker/cohere.py +118 -0
  116. autorag/nodes/passagereranker/colbert.py +213 -0
  117. autorag/nodes/passagereranker/flag_embedding.py +112 -0
  118. autorag/nodes/passagereranker/flag_embedding_llm.py +101 -0
  119. autorag/nodes/passagereranker/flashrank.py +245 -0
  120. autorag/nodes/passagereranker/jina.py +115 -0
  121. autorag/nodes/passagereranker/koreranker.py +136 -0
  122. autorag/nodes/passagereranker/mixedbreadai.py +126 -0
  123. autorag/nodes/passagereranker/monot5.py +190 -0
  124. autorag/nodes/passagereranker/openvino.py +191 -0
  125. autorag/nodes/passagereranker/pass_reranker.py +31 -0
  126. autorag/nodes/passagereranker/rankgpt.py +170 -0
  127. autorag/nodes/passagereranker/run.py +145 -0
  128. autorag/nodes/passagereranker/sentence_transformer.py +129 -0
  129. autorag/nodes/passagereranker/tart/__init__.py +1 -0
  130. autorag/nodes/passagereranker/tart/modeling_enc_t5.py +152 -0
  131. autorag/nodes/passagereranker/tart/tart.py +139 -0
  132. autorag/nodes/passagereranker/tart/tokenization_enc_t5.py +112 -0
  133. autorag/nodes/passagereranker/time_reranker.py +72 -0
  134. autorag/nodes/passagereranker/upr.py +160 -0
  135. autorag/nodes/passagereranker/voyageai.py +109 -0
  136. autorag/nodes/promptmaker/__init__.py +12 -0
  137. autorag/nodes/promptmaker/base.py +32 -0
  138. autorag/nodes/promptmaker/chat_fstring.py +73 -0
  139. autorag/nodes/promptmaker/fstring.py +49 -0
  140. autorag/nodes/promptmaker/long_context_reorder.py +83 -0
  141. autorag/nodes/promptmaker/run.py +283 -0
  142. autorag/nodes/promptmaker/window_replacement.py +85 -0
  143. autorag/nodes/queryexpansion/__init__.py +4 -0
  144. autorag/nodes/queryexpansion/base.py +62 -0
  145. autorag/nodes/queryexpansion/hyde.py +43 -0
  146. autorag/nodes/queryexpansion/multi_query_expansion.py +57 -0
  147. autorag/nodes/queryexpansion/pass_query_expansion.py +22 -0
  148. autorag/nodes/queryexpansion/query_decompose.py +111 -0
  149. autorag/nodes/queryexpansion/run.py +308 -0
  150. autorag/nodes/retrieval/__init__.py +0 -0
  151. autorag/nodes/retrieval/base.py +127 -0
  152. autorag/nodes/retrieval/run_util.py +152 -0
  153. autorag/nodes/semanticretrieval/__init__.py +1 -0
  154. autorag/nodes/semanticretrieval/run.py +148 -0
  155. autorag/nodes/semanticretrieval/vectordb.py +339 -0
  156. autorag/nodes/util.py +16 -0
  157. autorag/parser.py +37 -0
  158. autorag/schema/__init__.py +3 -0
  159. autorag/schema/base.py +35 -0
  160. autorag/schema/metricinput.py +99 -0
  161. autorag/schema/module.py +24 -0
  162. autorag/schema/node.py +144 -0
  163. autorag/strategy.py +165 -0
  164. autorag/support.py +235 -0
  165. autorag/utils/__init__.py +8 -0
  166. autorag/utils/cast.py +45 -0
  167. autorag/utils/preprocess.py +149 -0
  168. autorag/utils/util.py +759 -0
  169. autorag/validator.py +98 -0
  170. autorag/vectordb/__init__.py +75 -0
  171. autorag/vectordb/base.py +73 -0
  172. autorag/vectordb/chroma.py +118 -0
  173. autorag/vectordb/couchbase.py +239 -0
  174. autorag/vectordb/milvus.py +169 -0
  175. autorag/vectordb/pinecone.py +121 -0
  176. autorag/vectordb/qdrant.py +155 -0
  177. autorag/vectordb/weaviate.py +184 -0
  178. autorag/web.py +81 -0
  179. autorag-0.0.0.dist-info/METADATA +780 -0
  180. autorag-0.0.0.dist-info/RECORD +184 -0
  181. autorag-0.0.0.dist-info/WHEEL +5 -0
  182. autorag-0.0.0.dist-info/entry_points.txt +2 -0
  183. autorag-0.0.0.dist-info/licenses/LICENSE +201 -0
  184. autorag-0.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,186 @@
1
+ import os.path
2
+ import pathlib
3
+ from typing import List, Dict
4
+
5
+ import pandas as pd
6
+
7
+ from autorag.evaluation.metric import (
8
+ retrieval_token_recall,
9
+ retrieval_token_precision,
10
+ retrieval_token_f1,
11
+ )
12
+ from autorag.schema.metricinput import MetricInput
13
+ from autorag.strategy import measure_speed, filter_by_threshold, select_best
14
+ from autorag.utils.util import fetch_contents
15
+
16
+
17
+ def run_passage_compressor_node(
18
+ modules: List,
19
+ module_params: List[Dict],
20
+ previous_result: pd.DataFrame,
21
+ node_line_dir: str,
22
+ strategies: Dict,
23
+ ) -> pd.DataFrame:
24
+ """
25
+ Run evaluation and select the best module among passage compressor modules.
26
+
27
+ :param modules: Passage compressor modules to run.
28
+ :param module_params: Passage compressor module parameters.
29
+ :param previous_result: Previous result dataframe.
30
+ Could be retrieval, reranker modules result.
31
+ It means it must contain 'query', 'retrieved_contents', 'retrieved_ids', 'retrieve_scores' columns.
32
+ :param node_line_dir: This node line's directory.
33
+ :param strategies: Strategies for passage compressor node.
34
+ In this node, we use
35
+ You can skip evaluation when you use only one module and a module parameter.
36
+ :return: The best result dataframe with previous result columns.
37
+ This node will replace 'retrieved_contents' to compressed passages, so its length will be one.
38
+ """
39
+ if not os.path.exists(node_line_dir):
40
+ os.makedirs(node_line_dir)
41
+ project_dir = pathlib.PurePath(node_line_dir).parent.parent
42
+ data_dir = os.path.join(project_dir, "data")
43
+ save_dir = os.path.join(node_line_dir, "passage_compressor")
44
+ if not os.path.exists(save_dir):
45
+ os.makedirs(save_dir)
46
+
47
+ # make retrieval contents gt
48
+ qa_data = pd.read_parquet(os.path.join(data_dir, "qa.parquet"), engine="pyarrow")
49
+ corpus_data = pd.read_parquet(
50
+ os.path.join(data_dir, "corpus.parquet"), engine="pyarrow"
51
+ )
52
+ # check qa_data have retrieval_gt
53
+ assert all(len(x[0]) > 0 for x in qa_data["retrieval_gt"].tolist()), (
54
+ "Can't use passage compressor if you don't have retrieval gt values in QA dataset."
55
+ )
56
+
57
+ # run modules
58
+ results, execution_times = zip(
59
+ *map(
60
+ lambda task: measure_speed(
61
+ task[0].run_evaluator,
62
+ project_dir=project_dir,
63
+ previous_result=previous_result,
64
+ **task[1],
65
+ ),
66
+ zip(modules, module_params),
67
+ )
68
+ )
69
+ results = list(results)
70
+ average_times = list(map(lambda x: x / len(results[0]), execution_times))
71
+
72
+ retrieval_gt_contents = list(
73
+ map(lambda x: fetch_contents(corpus_data, x), qa_data["retrieval_gt"].tolist())
74
+ )
75
+
76
+ metric_inputs = [
77
+ MetricInput(retrieval_gt_contents=ret_cont_gt)
78
+ for ret_cont_gt in retrieval_gt_contents
79
+ ]
80
+
81
+ # run metrics before filtering
82
+ if strategies.get("metrics") is None:
83
+ raise ValueError(
84
+ "You must at least one metrics for retrieval contents evaluation."
85
+ "It can be 'retrieval_token_f1', 'retrieval_token_precision', 'retrieval_token_recall'."
86
+ )
87
+ results = list(
88
+ map(
89
+ lambda x: evaluate_passage_compressor_node(
90
+ x, metric_inputs, strategies.get("metrics")
91
+ ),
92
+ results,
93
+ )
94
+ )
95
+
96
+ # save results to folder
97
+ filepaths = list(
98
+ map(lambda x: os.path.join(save_dir, f"{x}.parquet"), range(len(modules)))
99
+ )
100
+ list(
101
+ map(lambda x: x[0].to_parquet(x[1], index=False), zip(results, filepaths))
102
+ ) # execute save to parquet
103
+ filenames = list(map(lambda x: os.path.basename(x), filepaths))
104
+
105
+ # make summary file
106
+ summary_df = pd.DataFrame(
107
+ {
108
+ "filename": filenames,
109
+ "module_name": list(map(lambda module: module.__name__, modules)),
110
+ "module_params": module_params,
111
+ "execution_time": average_times,
112
+ **{
113
+ f"passage_compressor_{metric}": list(
114
+ map(lambda result: result[metric].mean(), results)
115
+ )
116
+ for metric in strategies.get("metrics")
117
+ },
118
+ }
119
+ )
120
+
121
+ # filter by strategies
122
+ if strategies.get("speed_threshold") is not None:
123
+ results, filenames = filter_by_threshold(
124
+ results, average_times, strategies["speed_threshold"], filenames
125
+ )
126
+ selected_result, selected_filename = select_best(
127
+ results,
128
+ strategies.get("metrics"),
129
+ filenames,
130
+ strategies.get("strategy", "mean"),
131
+ )
132
+ new_retrieved_contents = selected_result["retrieved_contents"]
133
+ previous_result["retrieved_contents"] = new_retrieved_contents
134
+ selected_result = selected_result.drop(columns=["retrieved_contents"])
135
+ best_result = pd.concat([previous_result, selected_result], axis=1)
136
+
137
+ # add 'is_best' column to summary file
138
+ summary_df["is_best"] = summary_df["filename"] == selected_filename
139
+
140
+ # add prefix 'passage_compressor' to best_result columns
141
+ best_result = best_result.rename(
142
+ columns={
143
+ metric_name: f"passage_compressor_{metric_name}"
144
+ for metric_name in strategies.get("metrics")
145
+ }
146
+ )
147
+
148
+ # save the result files
149
+ best_result.to_parquet(
150
+ os.path.join(
151
+ save_dir, f"best_{os.path.splitext(selected_filename)[0]}.parquet"
152
+ ),
153
+ index=False,
154
+ )
155
+ summary_df.to_csv(os.path.join(save_dir, "summary.csv"), index=False)
156
+ return best_result
157
+
158
+
159
+ def evaluate_passage_compressor_node(
160
+ result_df: pd.DataFrame, metric_inputs: List[MetricInput], metrics: List[str]
161
+ ):
162
+ metric_funcs = {
163
+ retrieval_token_recall.__name__: retrieval_token_recall,
164
+ retrieval_token_precision.__name__: retrieval_token_precision,
165
+ retrieval_token_f1.__name__: retrieval_token_f1,
166
+ }
167
+ for metric_input, generated_text in zip(
168
+ metric_inputs, result_df["retrieved_contents"].tolist()
169
+ ):
170
+ metric_input.retrieved_contents = generated_text
171
+ metrics = list(filter(lambda x: x in metric_funcs.keys(), metrics))
172
+ if len(metrics) <= 0:
173
+ raise ValueError(f"metrics must be one of {metric_funcs.keys()}")
174
+ metrics_scores = dict(
175
+ map(
176
+ lambda metric: (
177
+ metric,
178
+ metric_funcs[metric](
179
+ metric_inputs=metric_inputs,
180
+ ),
181
+ ),
182
+ metrics,
183
+ )
184
+ )
185
+ result_df = pd.concat([result_df, pd.DataFrame(metrics_scores)], axis=1)
186
+ return result_df
@@ -0,0 +1,56 @@
1
+ from typing import List, Optional
2
+
3
+ from llama_index.core import PromptTemplate
4
+ from llama_index.core.prompts import PromptType
5
+ from llama_index.core.prompts.utils import is_chat_model
6
+ from llama_index.core.response_synthesizers import TreeSummarize as ts
7
+
8
+ from autorag.nodes.passagecompressor.base import LlamaIndexCompressor
9
+ from autorag.utils.util import get_event_loop, process_batch
10
+
11
+
12
+ class TreeSummarize(LlamaIndexCompressor):
13
+ def _pure(
14
+ self,
15
+ queries: List[str],
16
+ contents: List[List[str]],
17
+ prompt: Optional[str] = None,
18
+ chat_prompt: Optional[str] = None,
19
+ batch: int = 16,
20
+ ) -> List[str]:
21
+ """
22
+ Recursively merge retrieved texts and summarizes them in a bottom-up fashion.
23
+ This function is a wrapper for llama_index.response_synthesizers.TreeSummarize.
24
+ For more information, visit https://docs.llamaindex.ai/en/latest/examples/response_synthesizers/tree_summarize.html.
25
+
26
+ :param queries: The queries for retrieved passages.
27
+ :param contents: The contents of retrieved passages.
28
+ :param prompt: The prompt template for summarization.
29
+ If you want to use chat prompt, you should pass chat_prompt instead.
30
+ At prompt, you must specify where to put 'context_str' and 'query_str'.
31
+ Default is None. When it is None, it will use llama index default prompt.
32
+ :param chat_prompt: The chat prompt template for summarization.
33
+ If you want to use normal prompt, you should pass prompt instead.
34
+ At prompt, you must specify where to put 'context_str' and 'query_str'.
35
+ Default is None. When it is None, it will use llama index default chat prompt.
36
+ :param batch: The batch size for llm.
37
+ Set low if you face some errors.
38
+ Default is 16.
39
+ :return: The list of compressed texts.
40
+ """
41
+ if prompt is not None and not is_chat_model(self.llm):
42
+ summary_template = PromptTemplate(prompt, prompt_type=PromptType.SUMMARY)
43
+ elif chat_prompt is not None and is_chat_model(self.llm):
44
+ summary_template = PromptTemplate(
45
+ chat_prompt, prompt_type=PromptType.SUMMARY
46
+ )
47
+ else:
48
+ summary_template = None
49
+ summarizer = ts(llm=self.llm, summary_template=summary_template, use_async=True)
50
+ tasks = [
51
+ summarizer.aget_response(query, content)
52
+ for query, content in zip(queries, contents)
53
+ ]
54
+ loop = get_event_loop()
55
+ results = loop.run_until_complete(process_batch(tasks, batch_size=batch))
56
+ return results
@@ -0,0 +1,6 @@
1
+ from .pass_passage_filter import PassPassageFilter
2
+ from .percentile_cutoff import PercentileCutoff
3
+ from .recency import RecencyFilter
4
+ from .similarity_percentile_cutoff import SimilarityPercentileCutoff
5
+ from .similarity_threshold_cutoff import SimilarityThresholdCutoff
6
+ from .threshold_cutoff import ThresholdCutoff
@@ -0,0 +1,40 @@
1
+ import abc
2
+ import logging
3
+ from pathlib import Path
4
+ from typing import Union
5
+
6
+ import pandas as pd
7
+
8
+ from autorag.schema.base import BaseModule
9
+ from autorag.utils import validate_qa_dataset
10
+ from autorag.utils.cast import cast_retrieve_infos
11
+
12
+ logger = logging.getLogger("AutoRAG")
13
+
14
+
15
+ class BasePassageFilter(BaseModule, metaclass=abc.ABCMeta):
16
+ def __init__(self, project_dir: Union[str, Path], *args, **kwargs):
17
+ logger.info(f"Initialize passage filter node - {self.__class__.__name__}")
18
+
19
+ def __del__(self):
20
+ logger.info(f"Prompt maker node - {self.__class__.__name__} module is deleted.")
21
+
22
+ def cast_to_run(self, previous_result: pd.DataFrame, *args, **kwargs):
23
+ logger.info(
24
+ f"Running passage filter node - {self.__class__.__name__} module..."
25
+ )
26
+ validate_qa_dataset(previous_result)
27
+
28
+ # find queries columns
29
+ assert "query" in previous_result.columns, (
30
+ "previous_result must have query column."
31
+ )
32
+ queries = previous_result["query"].tolist()
33
+
34
+ retrieve_infos = cast_retrieve_infos(previous_result)
35
+ return (
36
+ queries,
37
+ retrieve_infos["retrieved_contents"],
38
+ retrieve_infos["retrieve_scores"],
39
+ retrieve_infos["retrieved_ids"],
40
+ )
@@ -0,0 +1,14 @@
1
+ import pandas as pd
2
+
3
+ from autorag.nodes.passagefilter.base import BasePassageFilter
4
+ from autorag.utils import result_to_dataframe
5
+
6
+
7
+ class PassPassageFilter(BasePassageFilter):
8
+ @result_to_dataframe(["retrieved_contents", "retrieved_ids", "retrieve_scores"])
9
+ def pure(self, previous_result: pd.DataFrame, *args, **kwargs):
10
+ _, contents, scores, ids = self.cast_to_run(previous_result)
11
+ return contents, ids, scores
12
+
13
+ def _pure(self, *args, **kwargs):
14
+ pass
@@ -0,0 +1,58 @@
1
+ from typing import List, Tuple
2
+
3
+ import pandas as pd
4
+
5
+ from autorag.nodes.passagefilter.base import BasePassageFilter
6
+ from autorag.utils.util import sort_by_scores, select_top_k, result_to_dataframe
7
+
8
+
9
+ class PercentileCutoff(BasePassageFilter):
10
+ @result_to_dataframe(["retrieved_contents", "retrieved_ids", "retrieve_scores"])
11
+ def pure(self, previous_result: pd.DataFrame, *args, **kwargs):
12
+ queries, contents, scores, ids = self.cast_to_run(previous_result)
13
+ return self._pure(queries, contents, scores, ids, *args, **kwargs)
14
+
15
+ def _pure(
16
+ self,
17
+ queries: List[str],
18
+ contents_list: List[List[str]],
19
+ scores_list: List[List[float]],
20
+ ids_list: List[List[str]],
21
+ percentile: float,
22
+ reverse: bool = False,
23
+ ) -> Tuple[List[List[str]], List[List[str]], List[List[float]]]:
24
+ """
25
+ Filter out the contents that are below the content's length times percentile.
26
+ If This is a filter and does not override scores.
27
+ If the value of content's length times percentile is less than 1, keep the only one highest similarity content.
28
+
29
+ :param queries: The list of queries to use for filtering
30
+ :param contents_list: The list of lists of contents to filter
31
+ :param scores_list: The list of lists of scores retrieved
32
+ :param ids_list: The list of lists of ids retrieved
33
+ :param percentile: The percentile to cut off
34
+ :param reverse: If True, the lower the score, the better
35
+ Default is False.
36
+ :return: Tuple of lists containing the filtered contents, ids, and scores
37
+ """
38
+ num_top_k = max(1, int(len(scores_list[0]) * percentile))
39
+
40
+ df = pd.DataFrame(
41
+ {
42
+ "contents": contents_list,
43
+ "ids": ids_list,
44
+ "scores": scores_list,
45
+ }
46
+ )
47
+
48
+ reverse = not reverse
49
+ df[["contents", "ids", "scores"]] = df.apply(
50
+ sort_by_scores, axis=1, result_type="expand", reverse=reverse
51
+ )
52
+ results = select_top_k(df, ["contents", "ids", "scores"], num_top_k)
53
+
54
+ return (
55
+ results["contents"].tolist(),
56
+ results["ids"].tolist(),
57
+ results["scores"].tolist(),
58
+ )
@@ -0,0 +1,105 @@
1
+ import logging
2
+ import os
3
+ from datetime import datetime, date
4
+ from pathlib import Path
5
+ from typing import List, Tuple, Union
6
+
7
+ import pandas as pd
8
+
9
+ from autorag.nodes.passagefilter.base import BasePassageFilter
10
+ from autorag.utils import fetch_contents, result_to_dataframe
11
+
12
+ logger = logging.getLogger("AutoRAG")
13
+
14
+
15
+ class RecencyFilter(BasePassageFilter):
16
+ def __init__(self, project_dir: Union[str, Path], *args, **kwargs):
17
+ super().__init__(project_dir, *args, **kwargs)
18
+ self.corpus_df = pd.read_parquet(
19
+ os.path.join(project_dir, "data", "corpus.parquet"), engine="pyarrow"
20
+ )
21
+
22
+ @result_to_dataframe(["retrieved_contents", "retrieved_ids", "retrieve_scores"])
23
+ def pure(self, previous_result: pd.DataFrame, *args, **kwargs):
24
+ _, contents, scores, ids = self.cast_to_run(previous_result, *args, **kwargs)
25
+ metadatas = fetch_contents(self.corpus_df, ids, column_name="metadata")
26
+ times = [
27
+ [time["last_modified_datetime"] for time in time_list]
28
+ for time_list in metadatas
29
+ ]
30
+ return self._pure(contents, scores, ids, times, *args, **kwargs)
31
+
32
+ def _pure(
33
+ self,
34
+ contents_list: List[List[str]],
35
+ scores_list: List[List[float]],
36
+ ids_list: List[List[str]],
37
+ time_list: List[List[datetime]],
38
+ threshold_datetime: Union[datetime, date],
39
+ ) -> Tuple[List[List[str]], List[List[str]], List[List[float]]]:
40
+ """
41
+ Filter out the contents that are below the threshold datetime.
42
+ If all contents are filtered, keep the only one recency content.
43
+ If the threshold date format is incorrect, return the original contents.
44
+
45
+ :param contents_list: The list of lists of contents to filter
46
+ :param scores_list: The list of lists of scores retrieved
47
+ :param ids_list: The list of lists of ids retrieved
48
+ :param time_list: The list of lists of datetime retrieved
49
+ :param threshold_datetime: The threshold to cut off.
50
+ In recency filter, you have to use the datetime.datetime object or datetime.date object.
51
+ All you need to do is to set the date at your YAML file.
52
+ For example, you can write "2010-09-09 3:45:06" or "2010-09-09" in the YAML file.
53
+ :return: Tuple of lists containing the filtered contents, ids, and scores
54
+ """
55
+ if not (
56
+ isinstance(threshold_datetime, datetime)
57
+ or isinstance(threshold_datetime, date)
58
+ ):
59
+ raise ValueError(
60
+ f"Threshold should be a datetime object, but got {type(threshold_datetime)}"
61
+ )
62
+
63
+ if not isinstance(threshold_datetime, datetime):
64
+ threshold_datetime = datetime.combine(
65
+ threshold_datetime, datetime.min.time()
66
+ )
67
+
68
+ time_list = [
69
+ list(
70
+ map(
71
+ lambda t: datetime.combine(t, datetime.min.time())
72
+ if not isinstance(t, datetime)
73
+ else t,
74
+ time,
75
+ )
76
+ )
77
+ for time in time_list
78
+ ]
79
+
80
+ def sort_row(contents, scores, ids, time, _datetime_threshold):
81
+ combined = list(zip(contents, scores, ids, time))
82
+ combined_filtered = [
83
+ item for item in combined if item[3] >= _datetime_threshold
84
+ ]
85
+
86
+ if combined_filtered:
87
+ remain_contents, remain_scores, remain_ids, _ = zip(*combined_filtered)
88
+ else:
89
+ combined.sort(key=lambda x: x[3], reverse=True)
90
+ remain_contents, remain_scores, remain_ids, _ = zip(*combined[:1])
91
+
92
+ return list(remain_contents), list(remain_ids), list(remain_scores)
93
+
94
+ remain_contents_list, remain_ids_list, remain_scores_list = zip(
95
+ *map(
96
+ sort_row,
97
+ contents_list,
98
+ scores_list,
99
+ ids_list,
100
+ time_list,
101
+ [threshold_datetime] * len(contents_list),
102
+ )
103
+ )
104
+
105
+ return remain_contents_list, remain_ids_list, remain_scores_list
@@ -0,0 +1,138 @@
1
+ import os
2
+ import pathlib
3
+ from typing import List, Dict
4
+
5
+ import pandas as pd
6
+
7
+ from autorag.nodes.retrieval.run_util import evaluate_retrieval_node
8
+ from autorag.schema.metricinput import MetricInput
9
+ from autorag.strategy import measure_speed, filter_by_threshold, select_best
10
+ from autorag.utils.util import to_list, apply_recursive
11
+
12
+
13
+ def run_passage_filter_node(
14
+ modules: List,
15
+ module_params: List[Dict],
16
+ previous_result: pd.DataFrame,
17
+ node_line_dir: str,
18
+ strategies: Dict,
19
+ ) -> pd.DataFrame:
20
+ """
21
+ Run evaluation and select the best module among passage filter node results.
22
+
23
+ :param modules: Passage filter modules to run.
24
+ :param module_params: Passage filter module parameters.
25
+ :param previous_result: Previous result dataframe.
26
+ Could be retrieval, reranker, passage filter modules result.
27
+ It means it must contain 'query', 'retrieved_contents', 'retrieved_ids', 'retrieve_scores' columns.
28
+ :param node_line_dir: This node line's directory.
29
+ :param strategies: Strategies for passage filter node.
30
+ In this node, we use 'retrieval_f1', 'retrieval_recall' and 'retrieval_precision'.
31
+ You can skip evaluation when you use only one module and a module parameter.
32
+ :return: The best result dataframe with previous 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
+
43
+ # make rows to metric_inputs
44
+ metric_inputs = [
45
+ MetricInput(retrieval_gt=ret_gt, query=query, generation_gt=gen_gt)
46
+ for ret_gt, query, gen_gt in zip(
47
+ retrieval_gt, qa_df["query"].tolist(), qa_df["generation_gt"].tolist()
48
+ )
49
+ ]
50
+
51
+ results, execution_times = zip(
52
+ *map(
53
+ lambda task: measure_speed(
54
+ task[0].run_evaluator,
55
+ project_dir=project_dir,
56
+ previous_result=previous_result,
57
+ **task[1],
58
+ ),
59
+ zip(modules, module_params),
60
+ )
61
+ )
62
+ average_times = list(map(lambda x: x / len(results[0]), execution_times))
63
+
64
+ # run metrics before filtering
65
+ if strategies.get("metrics") is None:
66
+ raise ValueError("You must at least one metrics for passage_filter evaluation.")
67
+ results = list(
68
+ map(
69
+ lambda x: evaluate_retrieval_node(
70
+ x,
71
+ metric_inputs,
72
+ strategies.get("metrics"),
73
+ ),
74
+ results,
75
+ )
76
+ )
77
+
78
+ # save results to folder
79
+ save_dir = os.path.join(node_line_dir, "passage_filter") # node name
80
+ if not os.path.exists(save_dir):
81
+ os.makedirs(save_dir)
82
+ filepaths = list(
83
+ map(lambda x: os.path.join(save_dir, f"{x}.parquet"), range(len(modules)))
84
+ )
85
+ list(
86
+ map(lambda x: x[0].to_parquet(x[1], index=False), zip(results, filepaths))
87
+ ) # execute save to parquet
88
+ filenames = list(map(lambda x: os.path.basename(x), filepaths))
89
+
90
+ summary_df = pd.DataFrame(
91
+ {
92
+ "filename": filenames,
93
+ "module_name": list(map(lambda module: module.__name__, modules)),
94
+ "module_params": module_params,
95
+ "execution_time": average_times,
96
+ **{
97
+ f"passage_filter_{metric}": list(
98
+ map(lambda result: result[metric].mean(), results)
99
+ )
100
+ for metric in strategies.get("metrics")
101
+ },
102
+ }
103
+ )
104
+
105
+ # filter by strategies
106
+ if strategies.get("speed_threshold") is not None:
107
+ results, filenames = filter_by_threshold(
108
+ results, average_times, strategies["speed_threshold"], filenames
109
+ )
110
+ selected_result, selected_filename = select_best(
111
+ results,
112
+ strategies.get("metrics"),
113
+ filenames,
114
+ strategies.get("strategy", "mean"),
115
+ )
116
+ selected_result = selected_result.rename(
117
+ columns={
118
+ metric_name: f"passage_filter_{metric_name}"
119
+ for metric_name in strategies["metrics"]
120
+ }
121
+ )
122
+ previous_result = previous_result.drop(
123
+ columns=["retrieved_contents", "retrieved_ids", "retrieve_scores"]
124
+ )
125
+ best_result = pd.concat([previous_result, selected_result], axis=1)
126
+
127
+ # add 'is_best' column to summary file
128
+ summary_df["is_best"] = summary_df["filename"] == selected_filename
129
+
130
+ # save files
131
+ summary_df.to_csv(os.path.join(save_dir, "summary.csv"), index=False)
132
+ best_result.to_parquet(
133
+ os.path.join(
134
+ save_dir, f"best_{os.path.splitext(selected_filename)[0]}.parquet"
135
+ ),
136
+ index=False,
137
+ )
138
+ return best_result