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,85 @@
1
+ import logging
2
+ import os
3
+ from typing import List, Dict
4
+
5
+ import pandas as pd
6
+
7
+ from autorag.nodes.promptmaker.base import BasePromptMaker
8
+ from autorag.utils import result_to_dataframe, fetch_contents
9
+
10
+ logger = logging.getLogger("AutoRAG")
11
+
12
+
13
+ class WindowReplacement(BasePromptMaker):
14
+ def __init__(self, project_dir: str, *args, **kwargs):
15
+ super().__init__(project_dir, *args, **kwargs)
16
+ # load corpus
17
+ data_dir = os.path.join(project_dir, "data")
18
+ self.corpus_data = pd.read_parquet(
19
+ os.path.join(data_dir, "corpus.parquet"), engine="pyarrow"
20
+ )
21
+
22
+ @result_to_dataframe(["prompts"])
23
+ def pure(self, previous_result: pd.DataFrame, *args, **kwargs):
24
+ query, retrieved_contents, prompt = self.cast_to_run(
25
+ previous_result, *args, **kwargs
26
+ )
27
+ retrieved_ids = previous_result["retrieved_ids"].tolist()
28
+ # get metadata from corpus
29
+ retrieved_metadata = fetch_contents(
30
+ self.corpus_data, retrieved_ids, column_name="metadata"
31
+ )
32
+ return self._pure(prompt, query, retrieved_contents, retrieved_metadata)
33
+
34
+ def _pure(
35
+ self,
36
+ prompt: str,
37
+ queries: List[str],
38
+ retrieved_contents: List[List[str]],
39
+ retrieved_metadata: List[List[Dict]],
40
+ ) -> List[str]:
41
+ """
42
+ Replace retrieved_contents with a window to create a Prompt
43
+ (only available for corpus chunked with Sentence window method)
44
+ You must type a prompt or prompt list at a config YAML file like this:
45
+
46
+ .. Code:: yaml
47
+ nodes:
48
+ - node_type: prompt_maker
49
+ modules:
50
+ - module_type: window_replacement
51
+ prompt: [Answer this question: {query} \n\n {retrieved_contents},
52
+ Read the passages carefully and answer this question: {query} \n\n Passages: {retrieved_contents}]
53
+
54
+ :param prompt: A prompt string.
55
+ :param queries: List of query strings.
56
+ :param retrieved_contents: List of retrieved contents.
57
+ :param retrieved_metadata: List of retrieved metadata.
58
+ :return: Prompts that are made by window_replacement.
59
+ """
60
+
61
+ def window_replacement_row(
62
+ _prompt: str,
63
+ _query: str,
64
+ _retrieved_contents,
65
+ _retrieved_metadata: List[Dict],
66
+ ) -> str:
67
+ window_list = []
68
+ for content, metadata in zip(_retrieved_contents, _retrieved_metadata):
69
+ if "window" in metadata:
70
+ window_list.append(metadata["window"])
71
+ else:
72
+ window_list.append(content)
73
+ logger.info(
74
+ "Only available for corpus chunked with Sentence window method."
75
+ "window_replacement will not proceed."
76
+ )
77
+ contents_str = "\n\n".join(window_list)
78
+ return _prompt.format(query=_query, retrieved_contents=contents_str)
79
+
80
+ return list(
81
+ map(
82
+ lambda x: window_replacement_row(prompt, x[0], x[1], x[2]),
83
+ zip(queries, retrieved_contents, retrieved_metadata),
84
+ )
85
+ )
@@ -0,0 +1,4 @@
1
+ from .hyde import HyDE
2
+ from .multi_query_expansion import MultiQueryExpansion
3
+ from .pass_query_expansion import PassQueryExpansion
4
+ from .query_decompose import QueryDecompose
@@ -0,0 +1,62 @@
1
+ import abc
2
+ import logging
3
+ from pathlib import Path
4
+ from typing import List, Union
5
+
6
+ import pandas as pd
7
+
8
+ from autorag.nodes.util import make_generator_callable_param
9
+ from autorag.schema import BaseModule
10
+ from autorag.utils import validate_qa_dataset
11
+
12
+ logger = logging.getLogger("AutoRAG")
13
+
14
+
15
+ class BaseQueryExpansion(BaseModule, metaclass=abc.ABCMeta):
16
+ def __init__(self, project_dir: Union[str, Path], *args, **kwargs):
17
+ logger.info(
18
+ f"Initialize query expansion node - {self.__class__.__name__} module..."
19
+ )
20
+ # set generator module for query expansion
21
+ generator_class, generator_param = make_generator_callable_param(kwargs)
22
+ self.generator = generator_class(project_dir, **generator_param)
23
+
24
+ def __del__(self):
25
+ del self.generator
26
+ logger.info(
27
+ f"Delete query expansion node - {self.__class__.__name__} module..."
28
+ )
29
+
30
+ def cast_to_run(self, previous_result: pd.DataFrame, *args, **kwargs):
31
+ logger.info(
32
+ f"Running query expansion node - {self.__class__.__name__} module..."
33
+ )
34
+ validate_qa_dataset(previous_result)
35
+
36
+ # find queries columns
37
+ assert "query" in previous_result.columns, (
38
+ "previous_result must have query column."
39
+ )
40
+ queries = previous_result["query"].tolist()
41
+ return queries
42
+
43
+ @staticmethod
44
+ def _check_expanded_query(queries: List[str], expanded_queries: List[List[str]]):
45
+ return list(
46
+ map(
47
+ lambda query, expanded_query_list: check_expanded_query(
48
+ query, expanded_query_list
49
+ ),
50
+ queries,
51
+ expanded_queries,
52
+ )
53
+ )
54
+
55
+
56
+ def check_expanded_query(query: str, expanded_query_list: List[str]):
57
+ # check if the expanded query is the same as the original query
58
+ expanded_query_list = list(map(lambda x: x.strip(), expanded_query_list))
59
+ return [
60
+ expanded_query if expanded_query else query
61
+ for expanded_query in expanded_query_list
62
+ ]
@@ -0,0 +1,43 @@
1
+ from typing import List
2
+
3
+ import pandas as pd
4
+
5
+ from autorag.nodes.queryexpansion.base import BaseQueryExpansion
6
+ from autorag.utils import result_to_dataframe
7
+
8
+ hyde_prompt = "Please write a passage to answer the question"
9
+
10
+
11
+ class HyDE(BaseQueryExpansion):
12
+ @result_to_dataframe(["queries"])
13
+ def pure(self, previous_result: pd.DataFrame, *args, **kwargs):
14
+ queries = self.cast_to_run(previous_result, *args, **kwargs)
15
+
16
+ # pop prompt from kwargs
17
+ prompt = kwargs.pop("prompt", hyde_prompt)
18
+ kwargs.pop("generator_module_type", None)
19
+
20
+ expanded_queries = self._pure(queries, prompt, **kwargs)
21
+ return self._check_expanded_query(queries, expanded_queries)
22
+
23
+ def _pure(self, queries: List[str], prompt: str = hyde_prompt, **generator_params):
24
+ """
25
+ HyDE, which inspired by "Precise Zero-shot Dense Retrieval without Relevance Labels" (https://arxiv.org/pdf/2212.10496.pdf)
26
+ LLM model creates a hypothetical passage.
27
+ And then, retrieve passages using hypothetical passage as a query.
28
+ :param queries: List[str], queries to retrieve.
29
+ :param prompt: Prompt to use when generating hypothetical passage
30
+ :return: List[List[str]], List of hyde results.
31
+ """
32
+ full_prompts = list(
33
+ map(
34
+ lambda x: (prompt if not bool(prompt) else hyde_prompt)
35
+ + f"\nQuestion: {x}\nPassage:",
36
+ queries,
37
+ )
38
+ )
39
+ input_df = pd.DataFrame({"prompts": full_prompts})
40
+ result_df = self.generator.pure(previous_result=input_df, **generator_params)
41
+ answers = result_df["generated_texts"].tolist()
42
+ results = list(map(lambda x: [x], answers))
43
+ return results
@@ -0,0 +1,57 @@
1
+ from typing import List
2
+
3
+ import pandas as pd
4
+
5
+ from autorag.nodes.queryexpansion.base import BaseQueryExpansion
6
+ from autorag.utils import result_to_dataframe
7
+
8
+ multi_query_expansion_prompt = """You are an AI language model assistant.
9
+ Your task is to generate 3 different versions of the given user
10
+ question to retrieve relevant documents from a vector database.
11
+ By generating multiple perspectives on the user question,
12
+ your goal is to help the user overcome some of the limitations
13
+ of distance-based similarity search. Provide these alternative
14
+ questions separated by newlines. Original question: {query}"""
15
+
16
+
17
+ class MultiQueryExpansion(BaseQueryExpansion):
18
+ @result_to_dataframe(["queries"])
19
+ def pure(self, previous_result: pd.DataFrame, *args, **kwargs):
20
+ queries = self.cast_to_run(previous_result, *args, **kwargs)
21
+
22
+ # pop prompt from kwargs
23
+ prompt = kwargs.pop("prompt", multi_query_expansion_prompt)
24
+ kwargs.pop("generator_module_type", None)
25
+
26
+ expanded_queries = self._pure(queries, prompt, **kwargs)
27
+ return self._check_expanded_query(queries, expanded_queries)
28
+
29
+ def _pure(
30
+ self, queries, prompt: str = multi_query_expansion_prompt, **kwargs
31
+ ) -> List[List[str]]:
32
+ """
33
+ Expand a list of queries using a multi-query expansion approach.
34
+ LLM model generate 3 different versions queries for each input query.
35
+
36
+ :param queries: List[str], queries to decompose.
37
+ :param prompt: str, prompt to use for multi-query expansion.
38
+ default prompt comes from langchain MultiQueryRetriever default query prompt.
39
+ :return: List[List[str]], list of expansion query.
40
+ """
41
+ full_prompts = list(map(lambda x: prompt.format(query=x), queries))
42
+ input_df = pd.DataFrame({"prompts": full_prompts})
43
+ result_df = self.generator.pure(previous_result=input_df, **kwargs)
44
+ answers = result_df["generated_texts"].tolist()
45
+ results = list(
46
+ map(lambda x: get_multi_query_expansion(x[0], x[1]), zip(queries, answers))
47
+ )
48
+ return results
49
+
50
+
51
+ def get_multi_query_expansion(query: str, answer: str) -> List[str]:
52
+ try:
53
+ queries = answer.split("\n")
54
+ queries.insert(0, query)
55
+ return queries
56
+ except:
57
+ return [query]
@@ -0,0 +1,22 @@
1
+ import pandas as pd
2
+
3
+ from autorag.nodes.queryexpansion.base import BaseQueryExpansion
4
+ from autorag.utils import result_to_dataframe
5
+
6
+
7
+ class PassQueryExpansion(BaseQueryExpansion):
8
+ @result_to_dataframe(["queries"])
9
+ def pure(self, previous_result: pd.DataFrame, *args, **kwargs):
10
+ """
11
+ Do not perform query expansion.
12
+ Return with the same queries.
13
+ The dimension will be 2-d list, and the column name will be 'queries'.
14
+ """
15
+ assert "query" in previous_result.columns, (
16
+ "previous_result must have query column."
17
+ )
18
+ queries = previous_result["query"].tolist()
19
+ return list(map(lambda x: [x], queries))
20
+
21
+ def _pure(self, *args, **kwargs):
22
+ pass
@@ -0,0 +1,111 @@
1
+ from typing import List
2
+
3
+ import pandas as pd
4
+
5
+ from autorag.nodes.queryexpansion.base import BaseQueryExpansion
6
+ from autorag.utils import result_to_dataframe
7
+
8
+ decompose_prompt = """Decompose a question in self-contained sub-questions. Use \"The question needs no decomposition\" when no decomposition is needed.
9
+
10
+ Example 1:
11
+
12
+ Question: Is Hamlet more common on IMDB than Comedy of Errors?
13
+ Decompositions:
14
+ 1: How many listings of Hamlet are there on IMDB?
15
+ 2: How many listing of Comedy of Errors is there on IMDB?
16
+
17
+ Example 2:
18
+
19
+ Question: Are birds important to badminton?
20
+
21
+ Decompositions:
22
+ The question needs no decomposition
23
+
24
+ Example 3:
25
+
26
+ Question: Is it legal for a licensed child driving Mercedes-Benz to be employed in US?
27
+
28
+ Decompositions:
29
+ 1: What is the minimum driving age in the US?
30
+ 2: What is the minimum age for someone to be employed in the US?
31
+
32
+ Example 4:
33
+
34
+ Question: Are all cucumbers the same texture?
35
+
36
+ Decompositions:
37
+ The question needs no decomposition
38
+
39
+ Example 5:
40
+
41
+ Question: Hydrogen's atomic number squared exceeds number of Spice Girls?
42
+
43
+ Decompositions:
44
+ 1: What is the atomic number of hydrogen?
45
+ 2: How many Spice Girls are there?
46
+
47
+ Example 6:
48
+
49
+ Question: {question}
50
+
51
+ Decompositions:
52
+ """
53
+
54
+
55
+ class QueryDecompose(BaseQueryExpansion):
56
+ @result_to_dataframe(["queries"])
57
+ def pure(self, previous_result: pd.DataFrame, *args, **kwargs):
58
+ queries = self.cast_to_run(previous_result, *args, **kwargs)
59
+
60
+ # pop prompt from kwargs
61
+ prompt = kwargs.pop("prompt", decompose_prompt)
62
+ kwargs.pop("generator_module_type", None)
63
+
64
+ expanded_queries = self._pure(queries, prompt, **kwargs)
65
+ return self._check_expanded_query(queries, expanded_queries)
66
+
67
+ def _pure(
68
+ self, queries: List[str], prompt: str = decompose_prompt, *args, **kwargs
69
+ ) -> List[List[str]]:
70
+ """
71
+ decompose query to little piece of questions.
72
+ :param queries: List[str], queries to decompose.
73
+ :param prompt: str, prompt to use for query decomposition.
74
+ default prompt comes from Visconde's StrategyQA few-shot prompt.
75
+ :return: List[List[str]], list of decomposed query. Return input query if query is not decomposable.
76
+ """
77
+ full_prompts = []
78
+ for query in queries:
79
+ if bool(prompt):
80
+ full_prompt = f"prompt: {prompt}\n\n question: {query}"
81
+ else:
82
+ full_prompt = decompose_prompt.format(question=query)
83
+ full_prompts.append(full_prompt)
84
+ input_df = pd.DataFrame({"prompts": full_prompts})
85
+ result_df = self.generator.pure(previous_result=input_df, *args, **kwargs)
86
+ answers = result_df["generated_texts"].tolist()
87
+ results = list(
88
+ map(lambda x: get_query_decompose(x[0], x[1]), zip(queries, answers))
89
+ )
90
+ return results
91
+
92
+
93
+ def get_query_decompose(query: str, answer: str) -> List[str]:
94
+ """
95
+ decompose query to little piece of questions.
96
+ :param query: str, query to decompose.
97
+ :param answer: str, answer from query_decompose function.
98
+ :return: List[str], list of a decomposed query. Return input query if query is not decomposable.
99
+ """
100
+ if answer.lower() == "the question needs no decomposition":
101
+ return [query]
102
+ try:
103
+ lines = [line.strip() for line in answer.splitlines() if line.strip()]
104
+ if lines[0].startswith("Decompositions:"):
105
+ lines.pop(0)
106
+ questions = [line.split(":", 1)[1].strip() for line in lines if ":" in line]
107
+ if not questions:
108
+ return [query]
109
+ return questions
110
+ except:
111
+ return [query]
@@ -0,0 +1,308 @@
1
+ import logging
2
+ import os
3
+ import pathlib
4
+ from copy import deepcopy
5
+ from typing import List, Dict, Optional, Union
6
+
7
+ import pandas as pd
8
+
9
+ from autorag.evaluation import evaluate_retrieval
10
+ from autorag.schema.metricinput import MetricInput
11
+ from autorag.strategy import measure_speed, filter_by_threshold, select_best
12
+ from autorag.support import get_support_modules
13
+ from autorag.utils.cast import cast_retrieve_infos
14
+ from autorag.utils.util import make_combinations, explode
15
+
16
+ logger = logging.getLogger("AutoRAG")
17
+
18
+
19
+ def run_query_expansion_node(
20
+ modules: List,
21
+ module_params: List[Dict],
22
+ previous_result: pd.DataFrame,
23
+ node_line_dir: str,
24
+ strategies: Dict,
25
+ ) -> pd.DataFrame:
26
+ """
27
+ Run evaluation and select the best module among query expansion node results.
28
+ Initially, retrieval is run using expanded_queries, the result of the query_expansion module.
29
+ The retrieval module is run as a combination of the retrieval_modules in strategies.
30
+ If there are multiple retrieval_modules, run them all and choose the best result.
31
+ If there are no retrieval_modules, run them with the default of bm25.
32
+ In this way, the best result is selected for each module, and then the best result is selected.
33
+
34
+ :param modules: Query expansion modules to run.
35
+ :param module_params: Query expansion module parameters.
36
+ :param previous_result: Previous result dataframe.
37
+ In this case, it would be qa data.
38
+ :param node_line_dir: This node line's directory.
39
+ :param strategies: Strategies for query expansion node.
40
+ :return: The best result dataframe.
41
+ """
42
+ if not os.path.exists(node_line_dir):
43
+ os.makedirs(node_line_dir)
44
+ node_dir = os.path.join(node_line_dir, "query_expansion")
45
+ if not os.path.exists(node_dir):
46
+ os.makedirs(node_dir)
47
+ project_dir = pathlib.PurePath(node_line_dir).parent.parent
48
+
49
+ # run query expansion
50
+ results, execution_times = zip(
51
+ *map(
52
+ lambda task: measure_speed(
53
+ task[0].run_evaluator,
54
+ project_dir=project_dir,
55
+ previous_result=previous_result,
56
+ **task[1],
57
+ ),
58
+ zip(modules, module_params),
59
+ )
60
+ )
61
+ average_times = list(map(lambda x: x / len(results[0]), execution_times))
62
+
63
+ # save results to folder
64
+ pseudo_module_params = deepcopy(module_params)
65
+ for i, module_param in enumerate(pseudo_module_params):
66
+ if "prompt" in module_params:
67
+ module_param["prompt"] = str(i)
68
+ filepaths = list(
69
+ map(lambda x: os.path.join(node_dir, f"{x}.parquet"), range(len(modules)))
70
+ )
71
+ list(
72
+ map(lambda x: x[0].to_parquet(x[1], index=False), zip(results, filepaths))
73
+ ) # execute save to parquet
74
+ filenames = list(map(lambda x: os.path.basename(x), filepaths))
75
+
76
+ # make summary file
77
+ summary_df = pd.DataFrame(
78
+ {
79
+ "filename": filenames,
80
+ "module_name": list(map(lambda module: module.__name__, modules)),
81
+ "module_params": module_params,
82
+ "execution_time": average_times,
83
+ }
84
+ )
85
+
86
+ # Run evaluation when there are more than one module.
87
+ if len(modules) > 1:
88
+ # pop general keys from strategies (e.g. metrics, speed_threshold)
89
+ general_key = ["metrics", "speed_threshold", "strategy"]
90
+ general_strategy = dict(
91
+ filter(lambda x: x[0] in general_key, strategies.items())
92
+ )
93
+ extra_strategy = dict(
94
+ filter(lambda x: x[0] not in general_key, strategies.items())
95
+ )
96
+
97
+ # first, filter by threshold if it is enabled.
98
+ if general_strategy.get("speed_threshold") is not None:
99
+ results, filenames = filter_by_threshold(
100
+ results, average_times, general_strategy["speed_threshold"], filenames
101
+ )
102
+
103
+ # check metrics in strategy
104
+ if general_strategy.get("metrics") is None:
105
+ raise ValueError(
106
+ "You must at least one metrics for query expansion evaluation."
107
+ )
108
+
109
+ if extra_strategy.get("top_k") is None:
110
+ extra_strategy["top_k"] = 10 # default value
111
+
112
+ # get retrieval modules from strategy
113
+ retrieval_callables, retrieval_params = make_retrieval_callable_params(
114
+ extra_strategy
115
+ )
116
+
117
+ # get retrieval_gt
118
+ retrieval_gt = pd.read_parquet(
119
+ os.path.join(project_dir, "data", "qa.parquet"), engine="pyarrow"
120
+ )["retrieval_gt"].tolist()
121
+
122
+ # make rows to metric_inputs
123
+ metric_inputs = [
124
+ MetricInput(retrieval_gt=ret_gt, query=query, generation_gt=gen_gt)
125
+ for ret_gt, query, gen_gt in zip(
126
+ retrieval_gt,
127
+ previous_result["query"].tolist(),
128
+ previous_result["generation_gt"].tolist(),
129
+ )
130
+ ]
131
+
132
+ # run evaluation
133
+ evaluation_results = list(
134
+ map(
135
+ lambda result: evaluate_one_query_expansion_node(
136
+ retrieval_callables,
137
+ retrieval_params,
138
+ [
139
+ setattr(metric_input, "queries", queries) or metric_input
140
+ for metric_input, queries in zip(
141
+ metric_inputs, result["queries"].to_list()
142
+ )
143
+ ],
144
+ general_strategy["metrics"],
145
+ project_dir,
146
+ previous_result,
147
+ general_strategy.get("strategy", "mean"),
148
+ ),
149
+ results,
150
+ )
151
+ )
152
+
153
+ evaluation_df = pd.DataFrame(
154
+ {
155
+ "filename": filenames,
156
+ **{
157
+ f"query_expansion_{metric_name}": list(
158
+ map(lambda x: x[metric_name].mean(), evaluation_results)
159
+ )
160
+ for metric_name in general_strategy["metrics"]
161
+ },
162
+ }
163
+ )
164
+ summary_df = pd.merge(
165
+ on="filename", left=summary_df, right=evaluation_df, how="left"
166
+ )
167
+
168
+ best_result, best_filename = select_best(
169
+ evaluation_results,
170
+ general_strategy["metrics"],
171
+ filenames,
172
+ strategies.get("strategy", "mean"),
173
+ )
174
+ # change metric name columns to query_expansion_metric_name
175
+ best_result = best_result.rename(
176
+ columns={
177
+ metric_name: f"query_expansion_{metric_name}"
178
+ for metric_name in strategies["metrics"]
179
+ }
180
+ )
181
+ best_result = best_result.drop(
182
+ columns=["retrieved_contents", "retrieved_ids", "retrieve_scores"]
183
+ )
184
+ else:
185
+ best_result, best_filename = results[0], filenames[0]
186
+ best_result = pd.concat([previous_result, best_result], axis=1)
187
+
188
+ # add 'is_best' column at summary file
189
+ summary_df["is_best"] = summary_df["filename"] == best_filename
190
+
191
+ # save files
192
+ summary_df.to_csv(os.path.join(node_dir, "summary.csv"), index=False)
193
+ best_result.to_parquet(
194
+ os.path.join(node_dir, f"best_{os.path.splitext(best_filename)[0]}.parquet"),
195
+ index=False,
196
+ )
197
+
198
+ return best_result
199
+
200
+
201
+ def evaluate_one_query_expansion_node(
202
+ retrieval_funcs: List,
203
+ retrieval_params: List[Dict],
204
+ metric_inputs: List[MetricInput],
205
+ metrics: List[str],
206
+ project_dir,
207
+ previous_result: pd.DataFrame,
208
+ strategy_name: str,
209
+ ) -> pd.DataFrame:
210
+ previous_result["queries"] = [
211
+ metric_input.queries for metric_input in metric_inputs
212
+ ]
213
+ retrieval_results = list(
214
+ map(
215
+ lambda x: x[0].run_evaluator(
216
+ project_dir=project_dir, previous_result=previous_result, **x[1]
217
+ ),
218
+ zip(retrieval_funcs, retrieval_params),
219
+ )
220
+ )
221
+ # Cast each retrieval results
222
+ retrieval_result_dicts = list(map(cast_retrieve_infos, retrieval_results))
223
+ evaluation_results = list(
224
+ map(
225
+ lambda x: evaluate_retrieval_node(
226
+ x,
227
+ metric_inputs,
228
+ metrics,
229
+ ),
230
+ retrieval_result_dicts,
231
+ )
232
+ )
233
+ best_result, _ = select_best(
234
+ evaluation_results, metrics, strategy_name=strategy_name
235
+ )
236
+ best_result = pd.concat([previous_result, best_result], axis=1)
237
+ return best_result
238
+
239
+
240
+ def make_retrieval_callable_params(strategy_dict: Dict):
241
+ """
242
+ strategy_dict looks like this:
243
+
244
+ .. Code:: json
245
+
246
+ {
247
+ "metrics": ["retrieval_f1", "retrieval_recall"],
248
+ "top_k": 50,
249
+ "retrieval_modules": [
250
+ {"module_type": "bm25"},
251
+ {"module_type": "vectordb", "embedding_model": ["openai", "huggingface"]}
252
+ ]
253
+ }
254
+
255
+ """
256
+ node_dict = deepcopy(strategy_dict)
257
+ retrieval_module_list: Optional[List[Dict]] = node_dict.pop(
258
+ "retrieval_modules", None
259
+ )
260
+ if retrieval_module_list is None:
261
+ retrieval_module_list = [
262
+ {
263
+ "module_type": "bm25",
264
+ }
265
+ ]
266
+ node_params = node_dict
267
+ modules = list(
268
+ map(
269
+ lambda module_dict: get_support_modules(module_dict.pop("module_type")),
270
+ retrieval_module_list,
271
+ )
272
+ )
273
+ param_combinations = list(
274
+ map(
275
+ lambda module_dict: make_combinations({**module_dict, **node_params}),
276
+ retrieval_module_list,
277
+ )
278
+ )
279
+ return explode(modules, param_combinations)
280
+
281
+
282
+ def evaluate_retrieval_node(
283
+ result_dict: Dict,
284
+ metric_inputs: List[MetricInput],
285
+ metrics: Union[List[str], List[Dict]],
286
+ ) -> pd.DataFrame:
287
+ """
288
+ Evaluate retrieval node from retrieval node result dataframe.
289
+
290
+ :param result_df: The result dataframe from a retrieval node.
291
+ :param metric_inputs: List of metric input schema for AutoRAG.
292
+ :param metrics: Metric list from input strategies.
293
+ :return: Return result_df with metrics columns.
294
+ The columns will be 'retrieved_contents', 'retrieved_ids', 'retrieve_scores', and metric names.
295
+ """
296
+
297
+ @evaluate_retrieval(
298
+ metric_inputs=metric_inputs,
299
+ metrics=metrics,
300
+ )
301
+ def evaluate_this_module(_dict: Dict):
302
+ return (
303
+ _dict["retrieved_contents"],
304
+ _dict["retrieved_ids"],
305
+ _dict["retrieve_scores"],
306
+ )
307
+
308
+ return evaluate_this_module(result_dict)
File without changes