├── .gitignore
├── LICENSE
├── README-zh.md
├── README.md
├── assets
├── ExperimentalResults.png
├── LevelRAG-ch.png
└── LevelRAG.png
├── scripts
├── build_dense_wiki.sh
├── build_elastic_wiki.sh
├── run_highlevel.sh
├── run_highlevel_gui.sh
├── run_simple.sh
└── run_simple_gui.sh
└── searchers
├── __init__.py
├── dense_searcher.py
├── high_level_searcher.py
├── hybrid_searcher.py
├── keyword_searcher.py
├── prompts
├── bm25_refine_emphasize_prompt.json
├── bm25_refine_extend_prompt.json
├── bm25_refine_filter_prompt.json
├── bm25_rewrite_prompt.json
├── decompose_with_context_prompt.json
├── decompose_without_context_prompt.json
├── dense_rewrite_prompt.json
├── lucene_rewrite_prompt.json
├── rewrite_by_answer_with_context_prompt.json
├── rewrite_by_answer_without_context_prompt.json
├── summarize_by_answer_prompt.json
├── verify_prompt.json
└── web_rewrite_prompt.json
├── searcher.py
└── web_searcher.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .idea
11 | .Python
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | applications/DeepSpeed-Chat/data
17 | eggs/
18 | .eggs/
19 | lib/
20 | lib64/
21 | parts/
22 | sdist/
23 | var/
24 | wheels/
25 | pip-wheel-metadata/
26 | share/python-wheels/
27 | *.egg-info/
28 | .installed.cfg
29 | *.egg
30 | MANIFEST
31 |
32 | # PyInstaller
33 | # Usually these files are written by a python script from a template
34 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
35 | *.manifest
36 | *.spec
37 |
38 | # Installer logs
39 | pip-log.txt
40 | pip-delete-this-directory.txt
41 |
42 | # Unit test / coverage reports
43 | htmlcov/
44 | .tox/
45 | .nox/
46 | .coverage
47 | .coverage.*
48 | .cache
49 | nosetests.xml
50 | coverage.xml
51 | *.cover
52 | *.py,cover
53 | .hypothesis/
54 | .pytest_cache/
55 |
56 | # Translations
57 | *.mo
58 | *.pot
59 |
60 | # Django stuff:
61 | *.log
62 | local_settings.py
63 | db.sqlite3
64 | db.sqlite3-journal
65 |
66 | # Flask stuff:
67 | instance/
68 | .webassets-cache
69 |
70 | # Scrapy stuff:
71 | .scrapy
72 |
73 | # Sphinx documentation
74 | docs/_build/
75 |
76 | # PyBuilder
77 | target/
78 |
79 | # Jupyter Notebook
80 | .ipynb_checkpoints
81 |
82 | # IPython
83 | profile_default/
84 | ipython_config.py
85 |
86 | # pyenv
87 | .python-version
88 |
89 | # pipenv
90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
93 | # install all needed dependencies.
94 | #Pipfile.lock
95 |
96 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
97 | __pypackages__/
98 |
99 | # Celery stuff
100 | celerybeat-schedule
101 | celerybeat.pid
102 |
103 | # SageMath parsed files
104 | *.sage.py
105 |
106 | # Environments
107 | .env
108 | .venv
109 | env/
110 | venv/
111 | ENV/
112 | env.bak/
113 | venv.bak/
114 |
115 | # Spyder project settings
116 | .spyderproject
117 | .spyproject
118 |
119 | # Rope project settings
120 | .ropeproject
121 |
122 | # mkdocs documentation
123 | /site
124 |
125 | # mypy
126 | .mypy_cache/
127 | .dmypy.json
128 | dmypy.json
129 |
130 | # Pyre type checker
131 | .pyre/
132 |
133 |
134 | # vscode
135 | .vscode
136 |
137 |
138 | # third party models
139 | yala/model/third_party_models
140 |
141 | # aim
142 | .aim
143 |
144 | # test files
145 | _test*.py
146 | _test*.ipynb
147 |
148 | # experimental configs
149 | experimental_configs/
150 |
151 | # hydra logs
152 | outputs/
153 |
154 | # pytest configs
155 | tests/configs/
156 |
157 | # cibuildwheel
158 | wheelhouse/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 zhangzhuocheng
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README-zh.md:
--------------------------------------------------------------------------------
1 | # LevelRAG: Enhancing Retrieval-Augmented Generation with Multi-hop Logic Planning over Rewriting Augmented Searchers
2 |
3 | 
4 | [](https://github.com/psf/black)
5 | [](https://pycqa.github.io/isort/)
6 | [](LICENSE)
7 | [](https://arxiv.org/abs/2502.18139)
8 |
9 | 本项目为论文 [**LevelRAG: Enhancing Retrieval-Augmented Generation with Multi-hop Logic Planning over Rewriting Augmented Searchers**](https://arxiv.org/abs/2502.18139) 的源码。
10 |
11 | ## 概览
12 | LevelRAG 是一种两阶段的检索增强生成(RAG)框架,结合了多跳逻辑规划和混合检索,以提高检索过程的完整性和准确性。其中第一阶段采用一个高级搜索器,将用户查询分解为原子查询。第二阶段利用多个低级搜索器,为每个子查询检索最相关的文档,然后将相关信息汇总到高级搜索器中生成最终答案。在每个低级搜索器中,采用大型语言模型(LLMs)对原子查询进行适应性优化,以更好地适应低级搜索器中内置的检索器。
13 |
14 |
15 |
16 |
17 |
18 |
19 | ## 运行 LevelRAG
20 |
21 | ### 环境准备
22 | 本项目是基于 [FlexRAG](https://github.com/ictnlp/FlexRAG) 实现的。请参照以下命令安装 FlexRAG:
23 |
24 | ```bash
25 | pip install flexrag==0.2.0
26 | ```
27 |
28 | 下载本项目源码:
29 |
30 | ```bash
31 | git clone https://github.com/ictnlp/LevelRAG
32 | ```
33 |
34 | ### 准备检索器
35 |
36 | > [!TIP]
37 | > 如果您希望以更简单的方式运行LevelRAG,请查看[运行 Simple LevelRAG](#运行-simple-levelrag)一节。
38 |
39 | 在运行 LevelRAG 前,需要构建检索器。LevelRAG 使用了三种不同的检索器,分别是 `DenseRetriever`、`ElasticRetriever`和`WebRetriever` 。除了 `WebRetriever` 不需要构建索引外,`DenseRetriever` 和 `ElasticRetriever` 都需要先构建索引。在我们的实验中,我们使用了 [Atlas](https://github.com/facebookresearch/atlas) 提供的维基百科语料库。您可以通过以下命令下载语料库:
40 |
41 |
42 | ```bash
43 | wget https://dl.fbaipublicfiles.com/atlas/corpora/wiki/enwiki-dec2021/text-list-100-sec.jsonl
44 | wget https://dl.fbaipublicfiles.com/atlas/corpora/wiki/enwiki-dec2021/infobox.jsonl
45 | ```
46 |
47 | 下载完语料库后,您可以运行以下命令构建 `DenseRetriever`:
48 |
49 | ```bash
50 | DENSE_PATH=wikipedia
51 |
52 | python -m flexrag.entrypoints.prepare_index \
53 | retriever_type=dense \
54 | corpus_path=[text-list-100-sec.jsonl,infobox.jsonl] \
55 | saving_fields=[text] \
56 | dense_config.database_path=$DENSE_PATH \
57 | dense_config.passage_encoder_config.encoder_type=hf \
58 | dense_config.passage_encoder_config.hf_config.model_path=facebook/contriever-msmarco \
59 | dense_config.passage_encoder_config.hf_config.device_id=[0] \
60 | dense_config.encode_fields=[text] \
61 | dense_config.index_type=faiss \
62 | dense_config.batch_size=1024 \
63 | dense_config.log_interval=100000
64 | ```
65 |
66 | 类似的,您可以运行以下命令构建 `ElasticRetriever`:
67 |
68 | ```bash
69 | python -m flexrag.entrypoints.prepare_index \
70 | retriever_type=elastic \
71 | corpus_path=[text-list-100-sec.jsonl,infobox.jsonl] \
72 | saving_fields=[text] \
73 | elastic_config.host='http://127.0.0.1:9200/' \
74 | elastic_config.index_name=wikipedia \
75 | elastic_config.batch_size=512 \
76 | elastic_config.log_interval=100 \
77 | reinit=True
78 | ```
79 |
80 | > **Notice:**
81 | > 在构建 `ElasticRetriever` 前,您需要安装 elasticsearch 。您可以参考[这里](https://www.elastic.co/guide/en/elasticsearch/reference/current/install-elasticsearch.html)的指令来安装 elasticsearch。
82 |
83 | `WebRetriever` 不需要构建索引,但需要您事先准备 Bing Search API 的密钥。您可以访问 [Bing Search API](https://www.microsoft.com/en-us/bing/apis) 来获取密钥。
84 |
85 |
86 | ### 准备生成器
87 | LevelRAG 使用 `Qwen2-7B-Instruct` 作为生成器,您可以通过以下命令使用 `vllm` 来部署生成器:
88 |
89 | ```bash
90 | python -m vllm.entrypoints.openai.api_server \
91 | --model Qwen2-7B-Instruct \
92 | --gpu-memory-utilization 0.95 \
93 | --tensor-parallel-size 4 \
94 | --port 8000 \
95 | --host 0.0.0.0 \
96 | --trust-remote-code
97 | ```
98 |
99 | 该命令将使用 4 个 GPU 来部署 `Qwen2-7B-Instruct` 模型,您可以根据您的 GPU 数量和显存大小来调整 `--tensor-parallel-size` 和 `--gpu-memory-utilization` 参数。
100 |
101 |
102 | ### 启动 LevelRAG 评估
103 | 在完成检索器的准备后,您可以通过运行 `scripts` 文件夹中的 `run_highlevel.sh` 脚本来运行 LevelRAG 评估脚本。注意替换脚本中的变量:
104 | - LEVELRAG_PATH:您在下载本项目源码时的路径
105 | - DENSE_PATH: 您在构建 `DenseRetriever` 时保存的路径
106 | - BING_KEY:您在准备 `WebRetriever` 时获取的 Bing Search API 密钥
107 |
108 |
109 | ### 启动 LevelRAG 图形界面
110 | 您也可以通过运行 `scripts` 文件夹中的 `run_highlevel_gui.sh` 脚本来启动 LevelRAG 的图形界面,在图形界面中您可以输入查询并查看 LevelRAG 的输出。
111 |
112 | ## 运行 Simple LevelRAG
113 | 如果您不希望使用 `WebRetriever` 及 `ElasticRetriever`。您可以仅使用 `DenseRetriever` 来运行 LevelRAG。得益于良好的多跳问题分解及子查询适应性优化, LevelRAG 在仅使用单一检索器时也能取得不错的效果,且运行速度更快。您可以通过运行 `scripts` 文件夹中的 `run_simple.sh` 脚本来运行 Simple LevelRAG 的评估脚本,或运行 `run_simple_gui.sh` 脚本来启动 Simple LevelRAG 的图形界面。
114 |
115 | 该脚本使用了 FlexRAG 项目提供的 `DenseRetriever` 检索器,因此您**无需构建索引**,直接运行脚本即可。
116 |
117 | > [!NOTE]
118 | > 请确认您已经将 `run_simple.sh` 或 `run_simple_gui.sh` 脚本中的 `API_KEY` 替换为您的 OpenAI API 密钥。
119 |
120 | > [!TIP]
121 | > 您也可以通过替换 `run_simple.sh` 或 `run_simple_gui.sh` 脚本中的 `MODEL_NAME` 来使用其它大模型作为生成器。
122 |
123 | ## 实验结果
124 | 我们在多个单跳及多跳知识密集型问答数据集上进行了实验。实验结果显示,相较于对比方法LevelRAG实现了非常显著的性能提升,实验结果请参考下表。
125 |
126 |
127 |
128 |
129 |
130 | ## 许可
131 | 本项目使用 MIT 许可证。有关更多信息,请参阅 [LICENSE](LICENSE) 文件。
132 |
133 |
134 | ## 引用
135 | 如果您觉得我们的工作对您有所帮助,请考虑引用我们的论文或 Star 本项目。
136 |
137 | ```bibtex
138 | @misc{zhang2025levelragenhancingretrievalaugmentedgeneration,
139 | title={LevelRAG: Enhancing Retrieval-Augmented Generation with Multi-hop Logic Planning over Rewriting Augmented Searchers},
140 | author={Zhuocheng Zhang and Yang Feng and Min Zhang},
141 | year={2025},
142 | eprint={2502.18139},
143 | archivePrefix={arXiv},
144 | primaryClass={cs.CL},
145 | url={https://arxiv.org/abs/2502.18139},
146 | }
147 | ```
148 |
149 | 如果您对本项目有任何问题,欢迎在 GitHub 上创建 issue 或通过邮件联系我们(zhangzhuocheng20z@ict.acn.cn)。
150 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LevelRAG: Enhancing Retrieval-Augmented Generation with Multi-hop Logic Planning over Rewriting Augmented Searchers
2 |
3 |
4 | 
5 | [](https://github.com/psf/black)
6 | [](https://pycqa.github.io/isort/)
7 | [](LICENSE)
8 | [](https://arxiv.org/abs/2502.18139)
9 |
10 |
11 |
12 | \[ [English](README.md) | [中文](README-zh.md) \]
13 |
14 |
15 |
16 | Source code for paper [**LevelRAG: Enhancing Retrieval-Augmented Generation with Multi-hop Logic Planning over Rewriting Augmented Searchers**](https://arxiv.org/abs/2502.18139).
17 |
18 | ## Overview
19 | **LevelRAG** is a two-stage retrieval-augmented generation (RAG) framework that incorporates multi-hop logic planning and hybrid retrieval to enhance both completeness and accuracy of the retrieval process. The first stage involves a high-level searcher that decomposing the user query into atomic sub-queries. The second stage utilizes multiple low-level searchers to retrieve the most relevant documents for each sub-query, which are then used to generate the final answer. In each low-level searcher, large language models (LLMs) are employed to refine the atomic queries to better fit the corresponding retriever.
20 |
21 |
22 |
23 |
24 |
25 |
26 | ## Running LevelRAG
27 |
28 | ### Prepare the Environment
29 | Our code is based on [FlexRAG](https://github.com/ictnlp/FlexRAG) project. Please follow the instruction to install FlexRAG:
30 | ```bash
31 | pip install flexrag==0.2.0
32 | ```
33 |
34 | Download the source code of this project:
35 | ```bash
36 | git clone https://github.com/ictnlp/LevelRAG
37 | ```
38 |
39 | ### Prepare the Retriever
40 |
41 | > [!TIP]
42 | > If you want to run LevelRAG in a simpler way, please refer to the [Running the Simple LevelRAG](#running-the-simple-levelrag) section.
43 |
44 | Before running the LevelRAG, preparing the retriever is necessary. LevelRAG employs three kind of retrievers in total, naming `DenseRetriever`, `ElasticRetriever`, and `WebRetriever`, respectively. Except for the `WebRetriever`, which does not require index construction, both the `DenseRetriever` and the `ElasticRetriever` need to prepare the index first. In our experiments, we use the wikipedia corpus provided by [Atlas](https://github.com/facebookresearch/atlas). You can download the corpus by running the following command:
45 |
46 | ```bash
47 | wget https://dl.fbaipublicfiles.com/atlas/corpora/wiki/enwiki-dec2021/text-list-100-sec.jsonl
48 | wget https://dl.fbaipublicfiles.com/atlas/corpora/wiki/enwiki-dec2021/infobox.jsonl
49 | ```
50 |
51 | After downloading the corpus, you can run the following command to build the `DenseRetriever`:
52 | ```bash
53 | python -m flexrag.entrypoints.prepare_index \
54 | retriever_type=dense \
55 | file_paths=[text-list-100-sec.jsonl,infobox.jsonl] \
56 | saving_fields=[title,section,text] \
57 | id_field=id \
58 | dense_config.database_path=wikipedia \
59 | dense_config.passage_encoder_config.encoder_type=hf \
60 | dense_config.passage_encoder_config.hf_config.model_path=facebook/contriever-msmarco \
61 | dense_config.passage_encoder_config.hf_config.device_id=[0] \
62 | dense_config.encode_fields=[text] \
63 | dense_config.index_type=faiss \
64 | dense_config.batch_size=1024 \
65 | dense_config.log_interval=100000
66 | ```
67 |
68 | Similarly, you can run the following command to build the `ElasticRetriever`:
69 |
70 | ```bash
71 | python -m flexrag.entrypoints.prepare_index \
72 | retriever_type=elastic \
73 | file_paths=[text-list-100-sec.jsonl,infobox.jsonl] \
74 | saving_fields=[title,section,text] \
75 | id_field=id \
76 | elastic_config.host='http://127.0.0.1:9200/' \
77 | elastic_config.index_name=wikipedia \
78 | elastic_config.batch_size=512 \
79 | elastic_config.log_interval=100000 \
80 | reinit=True
81 | ```
82 |
83 | > **Notice:**
84 | > Before building the `ElasticRetriever`, you need to setup the elasticsearch server. You can follow the instruction [here](https://www.elastic.co/guide/en/elasticsearch/reference/current/install-elasticsearch.html) to install the elasticsearch server.
85 |
86 | Using the `WebRetriever` does not require index construction. However, you need to prepare the Bing Search API_KEY in advance. You can visit [Bing Search API](https://www.microsoft.com/en-us/bing/apis) to get the API_KEY.
87 |
88 | ### Prepare the Genereator
89 | LevelRAG uses the `Qwen2-7B-Instruct` as the generator. You can deploy the generator using `vllm` by running the following command:
90 |
91 | ```bash
92 | python -m vllm.entrypoints.openai.api_server \
93 | --model Qwen2-7B-Instruct \
94 | --gpu-memory-utilization 0.95 \
95 | --tensor-parallel-size 4 \
96 | --port 8000 \
97 | --host 0.0.0.0 \
98 | --trust-remote-code
99 | ```
100 |
101 | This command will deploy the `Qwen2-7B-Instruct` using 4 GPUs. You can adjust the `--tensor-parallel-size` and the `--gpu-memory-utilization` according to your own GPU configuration.
102 |
103 |
104 | ### Run the LevelRAG Evaluation Script
105 | After preparing the retriever, you can run the LevelRAG by running the scripts in the `scripts` folder. Before running the scripts, make sure you have substituted the placeholder variables in the scripts with the correct values.
106 |
107 | - LEVELRAG_PATH: The path to the LevelRAG repository.
108 | - DENSE_PATH: The path to the `DenseRetriever`.
109 | - BING_KEY: The Bing Search API_KEY.
110 |
111 | ### Run the LevelRAG GUI Demo
112 | We also provide a GUI demo for LevelRAG. You can run the GUI demo by running the `run_highlevel_gui.sh` script in the `scripts` folder. In the GUI, you can input the query and view the output of LevelRAG.
113 |
114 | ## Running the Simple LevelRAG
115 | If you think building the retriever is too complicated, you can run the simple version of LevelRAG by running the
116 | `run_simple.sh` script in the `scripts` folder. The simple version of LevelRAG only uses the `DenseRetriever` and does not require the `WebRetriever` and the `ElasticRetriever`. Thanks to the good multi-hop problem decomposition and sub-query adaptivity optimization, LevelRAG can achieve good performance even with a single retriever, and the running speed is faster. You can also run the `run_simple_gui.sh` script to start the GUI application of the simple version of LevelRAG.
117 |
118 | > [!NOTE]
119 | > Please make sure you have change the `API_KEY` in the `run_simple.sh` script to your own OpenAI API_KEY.
120 |
121 | ## Experimental Results
122 | We conducted experiments on multiple single-hop and multi-hop knowledge-intensive question answering datasets. The experimental results show that, compared to the baseline method, LevelRAG achieves a significant performance improvement. Please refer to the table below for the experimental results.
123 |
124 |
125 |
126 |
127 |
128 | ## License
129 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
130 |
131 | ## Citation
132 | If you find our work useful, please consider citing our paper:
133 |
134 | ```bibtex
135 | @misc{zhang2025levelragenhancingretrievalaugmentedgeneration,
136 | title={LevelRAG: Enhancing Retrieval-Augmented Generation with Multi-hop Logic Planning over Rewriting Augmented Searchers},
137 | author={Zhuocheng Zhang and Yang Feng and Min Zhang},
138 | year={2025},
139 | eprint={2502.18139},
140 | archivePrefix={arXiv},
141 | primaryClass={cs.CL},
142 | url={https://arxiv.org/abs/2502.18139},
143 | }
144 | ```
145 |
146 | If you have any questions, feel free to create an issue on GitHub or contact us via email (zhangzhuocheng20z@ict.acn.cn).
--------------------------------------------------------------------------------
/assets/ExperimentalResults.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ictnlp/LevelRAG/4ad16faaae78d8c08631a69b45f870a18933591d/assets/ExperimentalResults.png
--------------------------------------------------------------------------------
/assets/LevelRAG-ch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ictnlp/LevelRAG/4ad16faaae78d8c08631a69b45f870a18933591d/assets/LevelRAG-ch.png
--------------------------------------------------------------------------------
/assets/LevelRAG.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ictnlp/LevelRAG/4ad16faaae78d8c08631a69b45f870a18933591d/assets/LevelRAG.png
--------------------------------------------------------------------------------
/scripts/build_dense_wiki.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -euo pipefail
4 |
5 | WIKI_FILE=text-list-100-sec.jsonl
6 | WIKI_INFOBOX=infobox.jsonl
7 | DENSE_PATH=
8 | ENCODER_PATH=facebook/contriever-msmarco
9 |
10 | python -m flexrag.entrypoints.prepare_index \
11 | retriever_type=dense \
12 | file_paths=[$WIKI_FILE,$WIKI_INFOBOX] \
13 | saving_fields=[title,section,text] \
14 | id_field=id \
15 | dense_config.database_path=$DENSE_PATH \
16 | dense_config.passage_encoder_config.encoder_type=hf \
17 | dense_config.passage_encoder_config.hf_config.model_path=$ENCODER_PATH \
18 | dense_config.passage_encoder_config.hf_config.device_id=[0] \
19 | dense_config.encode_fields=[text] \
20 | dense_config.index_type=faiss \
21 | dense_config.batch_size=1024 \
22 | dense_config.log_interval=100000
23 |
24 |
--------------------------------------------------------------------------------
/scripts/build_elastic_wiki.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -euo pipefail
4 |
5 | WIKI_FILE=text-list-100-sec.jsonl
6 | WIKI_INFOBOX=infobox.jsonl
7 | ELASTIC_HOST=http://127.0.0.1:9200/
8 |
9 | python -m flexrag.entrypoints.prepare_index \
10 | retriever_type=elastic \
11 | file_paths=[$WIKI_FILE,$WIKI_INFOBOX] \
12 | saving_fields=[title,section,text] \
13 | id_field=id \
14 | elastic_config.host=$ELASTIC_HOST \
15 | elastic_config.index_name=wiki \
16 | elastic_config.batch_size=512 \
17 | elastic_config.log_interval=100 \
18 | reinit=True
19 |
--------------------------------------------------------------------------------
/scripts/run_highlevel.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | LEVELRAG_PATH=""
4 | MODEL_NAME="Qwen2-7B-Instruct"
5 | BASE_URL="http://127.0.0.1:8000/v1"
6 | ELASTIC_HOST=http://127.0.0.1:9200/
7 | DENSE_PATH=
8 | ENCODER_PATH=facebook/contriever-msmarco
9 | BING_KEY=""
10 |
11 |
12 | python -m flexrag.entrypoints.run_assistant \
13 | user_module=$LEVELRAG_PATH/searchers \
14 | name=nq \
15 | split=test \
16 | assistant_type=highlevel \
17 | highlevel_config.searchers=[keyword,web,dense] \
18 | highlevel_config.decompose=True \
19 | highlevel_config.summarize_for_decompose=True \
20 | highlevel_config.summarize_for_answer=True \
21 | highlevel_config.keyword_config.rewrite_query=adaptive \
22 | highlevel_config.keyword_config.feedback_depth=3 \
23 | highlevel_config.keyword_config.response_type=short \
24 | highlevel_config.keyword_config.generator_type=openai \
25 | highlevel_config.keyword_config.openai_config.model_name=$MODEL_NAME \
26 | highlevel_config.keyword_config.openai_config.base_url=$BASE_URL \
27 | highlevel_config.keyword_config.gen_cfg.do_sample=False \
28 | highlevel_config.keyword_config.host=$ELASTIC_HOST \
29 | highlevel_config.keyword_config.index_name=wiki_2021 \
30 | highlevel_config.dense_config.rewrite_query=adaptive \
31 | highlevel_config.dense_config.response_type=short \
32 | highlevel_config.dense_config.generator_type=openai \
33 | highlevel_config.dense_config.openai_config.model_name=$MODEL_NAME \
34 | highlevel_config.dense_config.openai_config.base_url=$BASE_URL \
35 | highlevel_config.dense_config.gen_cfg.do_sample=False \
36 | highlevel_config.dense_config.database_path=$DENSE_PATH \
37 | highlevel_config.dense_config.index_type=faiss \
38 | highlevel_config.dense_config.query_encoder_config.encoder_type=hf \
39 | highlevel_config.dense_config.query_encoder_config.hf_config.model_path=$ENCODER_PATH \
40 | highlevel_config.dense_config.query_encoder_config.hf_config.device_id=[0] \
41 | highlevel_config.web_config.search_engine_type=bing \
42 | highlevel_config.web_config.bing_config.subscription_key=$BING_KEY \
43 | highlevel_config.web_config.web_reader_type=snippet \
44 | highlevel_config.web_config.rewrite_query=False \
45 | highlevel_config.web_config.response_type=short \
46 | highlevel_config.web_config.generator_type=openai \
47 | highlevel_config.web_config.openai_config.model_name=$MODEL_NAME \
48 | highlevel_config.web_config.openai_config.base_url=$BASE_URL \
49 | highlevel_config.web_config.gen_cfg.do_sample=False \
50 | highlevel_config.response_type=short \
51 | highlevel_config.generator_type=openai \
52 | highlevel_config.openai_config.model_name=$MODEL_NAME \
53 | highlevel_config.openai_config.base_url=$BASE_URL \
54 | highlevel_config.gen_cfg.do_sample=False \
55 | eval_config.metrics_type=[retrieval_success_rate,generation_f1,generation_em] \
56 | eval_config.retrieval_success_rate_config.eval_field=text \
57 | eval_config.response_preprocess.processor_type=[simplify_answer] \
58 | log_interval=10
59 |
--------------------------------------------------------------------------------
/scripts/run_highlevel_gui.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | LEVELRAG_PATH=""
4 | MODEL_NAME="Qwen2-7B-Instruct"
5 | BASE_URL="http://127.0.0.1:8000/v1"
6 | ELASTIC_HOST=http://127.0.0.1:9200/
7 | DENSE_PATH=
8 | ENCODER_PATH=facebook/contriever-msmarco
9 | BING_KEY=""
10 |
11 |
12 | python -m flexrag.entrypoints.run_interactive \
13 | user_module=$LEVELRAG_PATH/searchers \
14 | assistant_type=highlevel \
15 | highlevel_config.searchers=[keyword,web,dense] \
16 | highlevel_config.decompose=True \
17 | highlevel_config.summarize_for_decompose=True \
18 | highlevel_config.summarize_for_answer=True \
19 | highlevel_config.keyword_config.rewrite_query=adaptive \
20 | highlevel_config.keyword_config.feedback_depth=3 \
21 | highlevel_config.keyword_config.response_type=short \
22 | highlevel_config.keyword_config.generator_type=openai \
23 | highlevel_config.keyword_config.openai_config.model_name=$MODEL_NAME \
24 | highlevel_config.keyword_config.openai_config.base_url=$BASE_URL \
25 | highlevel_config.keyword_config.gen_cfg.do_sample=False \
26 | highlevel_config.keyword_config.host=$ELASTIC_HOST \
27 | highlevel_config.keyword_config.index_name=wiki_2021 \
28 | highlevel_config.dense_config.rewrite_query=adaptive \
29 | highlevel_config.dense_config.response_type=short \
30 | highlevel_config.dense_config.generator_type=openai \
31 | highlevel_config.dense_config.openai_config.model_name=$MODEL_NAME \
32 | highlevel_config.dense_config.openai_config.base_url=$BASE_URL \
33 | highlevel_config.dense_config.gen_cfg.do_sample=False \
34 | highlevel_config.dense_config.database_path=$DENSE_PATH \
35 | highlevel_config.dense_config.index_type=faiss \
36 | highlevel_config.dense_config.query_encoder_config.encoder_type=hf \
37 | highlevel_config.dense_config.query_encoder_config.hf_config.model_path=$ENCODER_PATH \
38 | highlevel_config.dense_config.query_encoder_config.hf_config.device_id=[0] \
39 | highlevel_config.web_config.search_engine_type=bing \
40 | highlevel_config.web_config.bing_config.subscription_key=$BING_KEY \
41 | highlevel_config.web_config.web_reader_type=snippet \
42 | highlevel_config.web_config.rewrite_query=False \
43 | highlevel_config.web_config.response_type=short \
44 | highlevel_config.web_config.generator_type=openai \
45 | highlevel_config.web_config.openai_config.model_name=$MODEL_NAME \
46 | highlevel_config.web_config.openai_config.base_url=$BASE_URL \
47 | highlevel_config.web_config.gen_cfg.do_sample=False \
48 | highlevel_config.response_type=short \
49 | highlevel_config.generator_type=openai \
50 | highlevel_config.openai_config.model_name=$MODEL_NAME \
51 | highlevel_config.openai_config.base_url=$BASE_URL \
52 | highlevel_config.gen_cfg.do_sample=False
53 |
--------------------------------------------------------------------------------
/scripts/run_simple.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | LEVELRAG_PATH=""
4 | MODEL_NAME="gpt-4o"
5 | API_KEY=""
6 |
7 |
8 | python -m flexrag.entrypoints.run_assistant \
9 | user_module=$LEVELRAG_PATH/searchers \
10 | name=nq \
11 | split=test \
12 | assistant_type=highlevel \
13 | highlevel_config.searchers=[dense] \
14 | highlevel_config.decompose=True \
15 | highlevel_config.summarize_for_decompose=True \
16 | highlevel_config.summarize_for_answer=True \
17 | highlevel_config.dense_config.rewrite_query=adaptive \
18 | highlevel_config.dense_config.response_type=short \
19 | highlevel_config.dense_config.generator_type=openai \
20 | highlevel_config.dense_config.openai_config.model_name=$MODEL_NAME \
21 | highlevel_config.dense_config.openai_config.api_key=$API_KEY \
22 | highlevel_config.dense_config.gen_cfg.do_sample=False \
23 | highlevel_config.dense_config.hf_repo='FlexRAG/wiki2021_atlas_contriever' \
24 | highlevel_config.response_type=short \
25 | highlevel_config.generator_type=openai \
26 | highlevel_config.openai_config.model_name=$MODEL_NAME \
27 | highlevel_config.openai_config.api_key=$api_key \
28 | highlevel_config.gen_cfg.do_sample=False \
29 | eval_config.metrics_type=[retrieval_success_rate,generation_f1,generation_em] \
30 | eval_config.retrieval_success_rate_config.eval_field=text \
31 | eval_config.response_preprocess.processor_type=[simplify_answer] \
32 | log_interval=10
33 |
--------------------------------------------------------------------------------
/scripts/run_simple_gui.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | LEVELRAG_PATH=""
4 | MODEL_NAME="gpt-4o"
5 | API_KEY=""
6 |
7 |
8 | python -m flexrag.entrypoints.run_interactive \
9 | user_module=$LEVELRAG_PATH/searchers \
10 | assistant_type=highlevel \
11 | highlevel_config.searchers=[dense] \
12 | highlevel_config.decompose=True \
13 | highlevel_config.summarize_for_decompose=True \
14 | highlevel_config.summarize_for_answer=True \
15 | highlevel_config.dense_config.rewrite_query=adaptive \
16 | highlevel_config.dense_config.response_type=short \
17 | highlevel_config.dense_config.generator_type=openai \
18 | highlevel_config.dense_config.openai_config.model_name=$MODEL_NAME \
19 | highlevel_config.dense_config.openai_config.api_key=$API_KEY \
20 | highlevel_config.dense_config.gen_cfg.do_sample=False \
21 | highlevel_config.dense_config.hf_repo='FlexRAG/wiki2021_atlas_contriever' \
22 | highlevel_config.response_type=short \
23 | highlevel_config.generator_type=openai \
24 | highlevel_config.openai_config.model_name=$MODEL_NAME \
25 | highlevel_config.openai_config.api_key=$API_KEY \
26 | highlevel_config.gen_cfg.do_sample=False
27 |
--------------------------------------------------------------------------------
/searchers/__init__.py:
--------------------------------------------------------------------------------
1 | from .keyword_searcher import KeywordSearcher, KeywordSearcherConfig
2 | from .dense_searcher import DenseSearcher, DenseSearcherConfig
3 | from .high_level_searcher import HighLevalSearcher, HighLevelSearcherConfig
4 | from .hybrid_searcher import HybridSearcher, HybridSearcherConfig
5 | from .searcher import BaseSearcher, BaseSearcherConfig
6 | from .web_searcher import WebSearcher, WebSearcherConfig
7 |
8 |
9 | __all__ = [
10 | "BaseSearcher",
11 | "BaseSearcherConfig",
12 | "KeywordSearcher",
13 | "KeywordSearcherConfig",
14 | "WebSearcher",
15 | "WebSearcherConfig",
16 | "DenseSearcher",
17 | "DenseSearcherConfig",
18 | "HybridSearcher",
19 | "HybridSearcherConfig",
20 | "HighLevalSearcher",
21 | "HighLevelSearcherConfig",
22 | "Searchers",
23 | "SearcherConfig",
24 | "load_searcher",
25 | ]
26 |
--------------------------------------------------------------------------------
/searchers/dense_searcher.py:
--------------------------------------------------------------------------------
1 | import os
2 | from copy import deepcopy
3 | from dataclasses import dataclass
4 | from typing import Optional
5 |
6 | from flexrag.assistant import ASSISTANTS
7 | from flexrag.common_dataclass import RetrievedContext
8 | from flexrag.prompt import ChatTurn, ChatPrompt
9 | from flexrag.retriever import DenseRetriever, DenseRetrieverConfig, LocalRetriever
10 | from flexrag.utils import Choices, LOGGER_MANAGER
11 |
12 | from .searcher import BaseSearcher, BaseSearcherConfig
13 |
14 |
15 | logger = LOGGER_MANAGER.getLogger("levelrag.dense_searcher")
16 |
17 |
18 | @dataclass
19 | class DenseSearcherConfig(BaseSearcherConfig, DenseRetrieverConfig):
20 | rewrite_query: Choices(["never", "pseudo", "adaptive"]) = "never" # type: ignore
21 | max_rewrite_depth: int = 3
22 | hf_repo: Optional[str] = None
23 |
24 |
25 | @ASSISTANTS("dense", config_class=DenseSearcherConfig)
26 | class DenseSearcher(BaseSearcher):
27 | def __init__(self, cfg: DenseSearcherConfig) -> None:
28 | super().__init__(cfg)
29 | # setup Dense Searcher
30 | self.rewrite = cfg.rewrite_query
31 | self.rewrite_depth = cfg.max_rewrite_depth
32 |
33 | # load Dense Retrieve
34 | if cfg.hf_repo is not None:
35 | self.retriever = LocalRetriever.load_from_hub(cfg.hf_repo)
36 | else:
37 | self.retriever = DenseRetriever(cfg)
38 |
39 | # load prompts
40 | self.rewrite_with_ctx_prompt = ChatPrompt.from_json(
41 | os.path.join(
42 | os.path.dirname(__file__),
43 | "prompts",
44 | "rewrite_by_answer_with_context_prompt.json",
45 | )
46 | )
47 | self.rewrite_wo_ctx_prompt = ChatPrompt.from_json(
48 | os.path.join(
49 | os.path.dirname(__file__),
50 | "prompts",
51 | "rewrite_by_answer_without_context_prompt.json",
52 | )
53 | )
54 | self.verify_prompt = ChatPrompt.from_json(
55 | os.path.join(
56 | os.path.dirname(__file__),
57 | "prompts",
58 | "verify_prompt.json",
59 | )
60 | )
61 | return
62 |
63 | def search(
64 | self, question: str
65 | ) -> tuple[list[RetrievedContext], list[dict[str, object]]]:
66 | # rewrite the query
67 | if self.rewrite == "pseudo":
68 | query_to_search = self.rewrite_query(question)
69 | else:
70 | query_to_search = question
71 |
72 | # begin adaptive search
73 | ctxs = []
74 | search_history = []
75 | verification = False
76 | rewrite_depth = 0
77 | while (not verification) and (rewrite_depth < self.rewrite_depth):
78 | rewrite_depth += 1
79 |
80 | # search
81 | ctxs = self.retriever.search(query=[query_to_search])[0]
82 | search_history.append(
83 | {
84 | "query": query_to_search,
85 | "ctxs": ctxs,
86 | }
87 | )
88 |
89 | # verify the contexts
90 | if self.rewrite == "adaptive":
91 | verification = self.verify_contexts(ctxs, question)
92 | else:
93 | verification = True
94 |
95 | # adaptive rewrite
96 | if (not verification) and (rewrite_depth < self.rewrite_depth):
97 | if rewrite_depth == 1:
98 | query_to_search = self.rewrite_query(question)
99 | else:
100 | query_to_search = self.rewrite_query(question, ctxs)
101 | return ctxs, search_history
102 |
103 | def rewrite_query(
104 | self, question: str, contexts: list[RetrievedContext] = []
105 | ) -> str:
106 | # Rewrite the query to be more informative
107 | if len(contexts) == 0:
108 | prompt = deepcopy(self.rewrite_wo_ctx_prompt)
109 | user_prompt = f"Question: {question}"
110 | else:
111 | prompt = deepcopy(self.rewrite_with_ctx_prompt)
112 | user_prompt = ""
113 | for n, ctx in enumerate(contexts):
114 | user_prompt += f"Context {n}: {ctx.data['text']}\n\n"
115 | user_prompt += f"Question: {question}"
116 | prompt.update(ChatTurn(role="user", content=user_prompt))
117 | query = self.agent.chat([prompt], generation_config=self.gen_cfg)[0][0]
118 | return f"{question} {query}"
119 |
120 | def verify_contexts(
121 | self,
122 | contexts: list[RetrievedContext],
123 | question: str,
124 | ) -> bool:
125 | prompt = deepcopy(self.verify_prompt)
126 | user_prompt = ""
127 | for n, ctx in enumerate(contexts):
128 | user_prompt += f"Context {n}: {ctx.data['text']}\n\n"
129 | user_prompt += f"Question: {question}"
130 | prompt.update(ChatTurn(role="user", content=user_prompt))
131 | response = self.agent.chat([prompt], generation_config=self.gen_cfg)[0][0]
132 | return "yes" in response.lower()
133 |
--------------------------------------------------------------------------------
/searchers/high_level_searcher.py:
--------------------------------------------------------------------------------
1 | import os
2 | import re
3 | from copy import deepcopy
4 | from dataclasses import dataclass
5 |
6 | from flexrag.assistant import ASSISTANTS
7 | from flexrag.common_dataclass import RetrievedContext
8 | from flexrag.prompt import ChatPrompt, ChatTurn
9 |
10 | from .hybrid_searcher import HybridSearcher, HybridSearcherConfig
11 |
12 |
13 | @dataclass
14 | class HighLevelSearcherConfig(HybridSearcherConfig):
15 | decompose: bool = False
16 | max_decompose_times: int = 100
17 | summarize_for_decompose: bool = False
18 | summarize_for_answer: bool = False
19 |
20 |
21 | @ASSISTANTS("highlevel", config_class=HighLevelSearcherConfig)
22 | class HighLevalSearcher(HybridSearcher):
23 | def __init__(self, cfg: HighLevelSearcherConfig) -> None:
24 | super().__init__(cfg)
25 | # set basic args
26 | if not cfg.decompose:
27 | self.max_decompose_times = 0
28 | else:
29 | self.max_decompose_times = cfg.max_decompose_times
30 | self.summarize_for_decompose = cfg.summarize_for_decompose
31 | self.summarize_for_answer = cfg.summarize_for_answer
32 |
33 | # load prompt
34 | self.decompose_prompt_w_ctx = ChatPrompt.from_json(
35 | os.path.join(
36 | os.path.dirname(__file__),
37 | "prompts",
38 | "decompose_with_context_prompt.json",
39 | )
40 | )
41 | self.decompose_prompt_wo_ctx = ChatPrompt.from_json(
42 | os.path.join(
43 | os.path.dirname(__file__),
44 | "prompts",
45 | "decompose_without_context_prompt.json",
46 | )
47 | )
48 | self.summarize_prompt = ChatPrompt.from_json(
49 | os.path.join(
50 | os.path.dirname(__file__),
51 | "prompts",
52 | "summarize_by_answer_prompt.json",
53 | )
54 | )
55 | return
56 |
57 | def decompose_question(
58 | self,
59 | question: str,
60 | search_history: list[dict[str, str | RetrievedContext]] = [],
61 | ) -> list[str]:
62 | # form prompt
63 | if len(search_history) > 0:
64 | prompt = deepcopy(self.decompose_prompt_w_ctx)
65 | ctx_str = self.compose_contexts(search_history)
66 | prompt.update(
67 | ChatTurn(role="user", content=f"Question: {question}\n\n{ctx_str}")
68 | )
69 | else:
70 | prompt = deepcopy(self.decompose_prompt_wo_ctx)
71 | prompt.update(ChatTurn(role="user", content=f"Question: {question}"))
72 |
73 | # get response
74 | response = self.agent.chat([prompt], generation_config=self.gen_cfg)[0][0]
75 | if "No additional information is required" in response:
76 | return []
77 | split_pattern = r"\[\d+\] ([^\[]+)"
78 | decompsed = re.findall(split_pattern, response)
79 | # If the question is not decomposed, fallback to original question
80 | if (len(decompsed) == 0) and (len(search_history) == 0):
81 | decompsed = [question]
82 |
83 | # deduplicate questions
84 | searched = set()
85 | for s in search_history:
86 | searched.add(s["question"])
87 | decompsed = [i for i in decompsed if i not in searched]
88 | return decompsed
89 |
90 | def compose_contexts(
91 | self, search_history: list[dict[str, str | list[RetrievedContext]]]
92 | ) -> str:
93 | if self.summarize_for_decompose:
94 | summed_text = self.summarize_history(search_history)
95 | ctx_text = ""
96 | for n, text in enumerate(summed_text):
97 | ctx_text += f"Context {n + 1}: {text}\n\n"
98 | ctx_text = ctx_text[:-2]
99 | else:
100 | ctx_text = ""
101 | n = 1
102 | for item in search_history:
103 | for ctx in item["contexts"]:
104 | ctx_text += f"Context {n}: {ctx.data['text']}\n\n"
105 | n += 1
106 | ctx_text = ctx_text[:-2]
107 | return ctx_text
108 |
109 | def summarize_history(
110 | self, search_history: list[dict[str, str | list[RetrievedContext]]]
111 | ) -> list[str]:
112 | summed_text = []
113 | for item in search_history:
114 | prompt = deepcopy(self.summarize_prompt)
115 | q = item["question"]
116 | usr_prompt = ""
117 | for n, ctx in enumerate(item["contexts"]):
118 | usr_prompt += f"Context {n + 1}: {ctx.data['text']}\n\n"
119 | usr_prompt += f"Question: {q}"
120 | prompt.update(ChatTurn(role="user", content=usr_prompt))
121 | ans = self.agent.chat([prompt], generation_config=self.gen_cfg)[0][0]
122 | summed_text.append(ans)
123 | return summed_text
124 |
125 | def search(
126 | self, question: str
127 | ) -> tuple[list[RetrievedContext], list[dict[str, object]]]:
128 | contexts = []
129 | search_history = []
130 | decompose_times = self.max_decompose_times
131 | if decompose_times > 0:
132 | decomposed_questions = self.decompose_question(question)
133 | decompose_times -= 1
134 | else:
135 | decomposed_questions = [question]
136 |
137 | # search the decomposed_questions
138 | while len(decomposed_questions) > 0:
139 | q = decomposed_questions.pop(0)
140 | ctxs, _ = super().search(q)
141 | search_history.append({"question": q, "contexts": ctxs})
142 | contexts.extend(ctxs)
143 | if (len(decomposed_questions) == 0) and (decompose_times > 0):
144 | decomposed_questions = self.decompose_question(question, search_history)
145 | decompose_times -= 1
146 |
147 | # post process
148 | if self.summarize_for_answer:
149 | summed_text = self.summarize_history(search_history)
150 | contexts = [
151 | RetrievedContext(
152 | retriever="highlevel_searcher",
153 | query=j["question"],
154 | data={"text": i},
155 | )
156 | for i, j in zip(summed_text, search_history)
157 | ]
158 | return contexts, search_history
159 |
--------------------------------------------------------------------------------
/searchers/hybrid_searcher.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass, field
2 |
3 | from flexrag.assistant import ASSISTANTS
4 | from flexrag.common_dataclass import RetrievedContext
5 | from flexrag.utils import Choices
6 |
7 | from .keyword_searcher import KeywordSearcher, KeywordSearcherConfig
8 | from .dense_searcher import DenseSearcher, DenseSearcherConfig
9 | from .searcher import BaseSearcher, BaseSearcherConfig
10 | from .web_searcher import WebSearcher, WebSearcherConfig
11 |
12 |
13 | @dataclass
14 | class HybridSearcherConfig(BaseSearcherConfig):
15 | searchers: list[Choices(["keyword", "web", "dense"])] = field(default_factory=list) # type: ignore
16 | keyword_config: KeywordSearcherConfig = field(default_factory=KeywordSearcherConfig) # fmt: skip
17 | web_config: WebSearcherConfig = field(default_factory=WebSearcherConfig)
18 | dense_config: DenseSearcherConfig = field(default_factory=DenseSearcherConfig) # fmt: skip
19 |
20 |
21 | @ASSISTANTS("hybrid", config_class=HybridSearcherConfig)
22 | class HybridSearcher(BaseSearcher):
23 | def __init__(self, cfg: HybridSearcherConfig) -> None:
24 | super().__init__(cfg)
25 | # load searchers
26 | self.searchers = self.load_searchers(
27 | searchers=cfg.searchers,
28 | bm25_cfg=cfg.keyword_config,
29 | web_cfg=cfg.web_config,
30 | dense_cfg=cfg.dense_config,
31 | )
32 | return
33 |
34 | def load_searchers(
35 | self,
36 | searchers: list[str],
37 | bm25_cfg: KeywordSearcherConfig,
38 | web_cfg: WebSearcherConfig,
39 | dense_cfg: DenseSearcherConfig,
40 | ) -> dict[str, BaseSearcher]:
41 | searcher_list = {}
42 | for searcher in searchers:
43 | match searcher:
44 | case "keyword":
45 | searcher_list[searcher] = KeywordSearcher(bm25_cfg)
46 | case "web":
47 | searcher_list[searcher] = WebSearcher(web_cfg)
48 | case "dense":
49 | searcher_list[searcher] = DenseSearcher(dense_cfg)
50 | case _:
51 | raise ValueError(f"Searcher {searcher} not supported")
52 | return searcher_list
53 |
54 | def search(
55 | self, question: str
56 | ) -> tuple[list[RetrievedContext], list[dict[str, object]]]:
57 | # search the question using sub-searchers
58 | contexts = []
59 | search_history = []
60 | for name, searcher in self.searchers.items():
61 | ctxs = searcher.search(question)[0]
62 | contexts.extend(ctxs)
63 | search_history.append(
64 | {
65 | "searcher": name,
66 | "context": ctxs,
67 | }
68 | )
69 | return contexts, search_history
70 |
--------------------------------------------------------------------------------
/searchers/keyword_searcher.py:
--------------------------------------------------------------------------------
1 | import os
2 | import re
3 | from copy import deepcopy
4 | from dataclasses import dataclass
5 |
6 | from flexrag.assistant import ASSISTANTS
7 | from flexrag.common_dataclass import RetrievedContext
8 | from flexrag.prompt import ChatTurn, ChatPrompt
9 | from flexrag.retriever import ElasticRetriever, ElasticRetrieverConfig
10 | from flexrag.utils import Choices, LOGGER_MANAGER
11 |
12 | from .searcher import BaseSearcher, BaseSearcherConfig
13 |
14 |
15 | logger = LOGGER_MANAGER.getLogger("levelrag.keyword_searcher")
16 |
17 |
18 | @dataclass
19 | class KeywordSearcherConfig(BaseSearcherConfig, ElasticRetrieverConfig):
20 | rewrite_query: Choices(["always", "never", "adaptive"]) = "never" # type: ignore
21 | feedback_depth: int = 1
22 |
23 |
24 | @ASSISTANTS("keyword", config_class=KeywordSearcherConfig)
25 | class KeywordSearcher(BaseSearcher):
26 | def __init__(self, cfg: KeywordSearcherConfig) -> None:
27 | super().__init__(cfg)
28 | # setup Keyword Searcher
29 | self.rewrite = cfg.rewrite_query
30 | self.feedback_depth = cfg.feedback_depth
31 |
32 | # load ElasticSearch Retriever
33 | self.retriever = ElasticRetriever(cfg)
34 |
35 | # load prompts
36 | self.rewrite_prompt = ChatPrompt.from_json(
37 | os.path.join(
38 | os.path.dirname(__file__),
39 | "prompts",
40 | "bm25_rewrite_prompt.json",
41 | )
42 | )
43 | self.verify_prompt = ChatPrompt.from_json(
44 | os.path.join(
45 | os.path.dirname(__file__),
46 | "prompts",
47 | "verify_prompt.json",
48 | )
49 | )
50 | self.refine_prompts = {
51 | "extend": ChatPrompt.from_json(
52 | os.path.join(
53 | os.path.dirname(__file__),
54 | "prompts",
55 | "bm25_refine_extend_prompt.json",
56 | )
57 | ),
58 | "filter": ChatPrompt.from_json(
59 | os.path.join(
60 | os.path.dirname(__file__),
61 | "prompts",
62 | "bm25_refine_filter_prompt.json",
63 | )
64 | ),
65 | "emphasize": ChatPrompt.from_json(
66 | os.path.join(
67 | os.path.dirname(__file__),
68 | "prompts",
69 | "bm25_refine_emphasize_prompt.json",
70 | )
71 | ),
72 | }
73 | return
74 |
75 | def search(
76 | self, question: str
77 | ) -> tuple[list[RetrievedContext], list[dict[str, object]]]:
78 | retrieval_history = []
79 |
80 | # rewrite the query
81 | match self.rewrite:
82 | case "always":
83 | query_to_search = self.rewrite_query(question)
84 | case "never":
85 | query_to_search = question
86 | case "adaptive":
87 | ctxs = self.retriever.search(query=[question])[0]
88 | verification = self.verify_contexts(ctxs, question)
89 | retrieval_history.append(
90 | {
91 | "query": question,
92 | "ctxs": ctxs,
93 | }
94 | )
95 | if verification:
96 | return ctxs, retrieval_history
97 | query_to_search = self.rewrite_query(question)
98 |
99 | # begin BFS search
100 | search_stack = [(query_to_search, 1)]
101 | total_depth = self.feedback_depth + 1
102 | while len(search_stack) > 0:
103 | # search
104 | query_to_search, depth = search_stack.pop(0)
105 | ctxs = self.retriever.search(query=[query_to_search])[0]
106 | retrieval_history.append(
107 | {
108 | "query": query_to_search,
109 | "ctxs": ctxs,
110 | }
111 | )
112 |
113 | # verify contexts
114 | if total_depth > 1:
115 | verification = self.verify_contexts(ctxs, question)
116 | else:
117 | verification = True
118 | if verification:
119 | break
120 |
121 | # if depth is already at the maximum, stop expanding
122 | if depth >= total_depth:
123 | continue
124 |
125 | # expand the search stack
126 | refined = self.refine_query(
127 | contexts=ctxs,
128 | base_query=question,
129 | current_query=query_to_search,
130 | )
131 | search_stack.extend([(rq, depth + 1) for rq in refined])
132 | return ctxs, retrieval_history
133 |
134 | def refine_query(
135 | self,
136 | contexts: list[RetrievedContext],
137 | base_query: str,
138 | current_query: str,
139 | ) -> list[str]:
140 | refined_queries = []
141 | for prompt_type in self.refine_prompts:
142 | # prepare prompt
143 | prompt = deepcopy(self.refine_prompts[prompt_type])
144 | ctx_str = ""
145 | for n, ctx in enumerate(contexts):
146 | ctx_str += f"Context {n}: {ctx.data['text']}\n\n"
147 | prompt.history[-1].content = (
148 | f"{ctx_str}{prompt.history[-1].content}\n\n"
149 | f"Current query: {current_query}\n\n"
150 | f"The information you are looking for: {base_query}"
151 | )
152 | response = self.agent.chat([prompt], generation_config=self.gen_cfg)[0][0]
153 |
154 | # prepare re patterns
155 | response_ = re.escape(response)
156 | pattern = f'("{response_}"(\^\d)?)|({response_})'
157 |
158 | # append refined query
159 | if prompt_type == "extend":
160 | refined_queries.append(f"{current_query} {response}")
161 | elif prompt_type == "filter":
162 | if re.search(pattern, current_query):
163 | refined_queries.append(re.sub(pattern, "", current_query))
164 | else:
165 | refined_queries.append(f'{current_query} -"{response}"')
166 | elif prompt_type == "emphasize":
167 | if re.search(pattern, current_query):
168 | try:
169 | current_weight = re.search(pattern, current_query).group(2)
170 | current_weight = int(current_weight[1:])
171 | except:
172 | current_weight = 1
173 | repl = re.escape(f'"{response}"^{current_weight + 1}')
174 | new_query = re.sub(pattern, repl, current_query)
175 | refined_queries.append(new_query)
176 | else:
177 | refined_queries.append(f'"{response}" {current_query}')
178 | return refined_queries
179 |
180 | def rewrite_query(self, info: str) -> str:
181 | # Rewrite the query to be more informative
182 | prompt = deepcopy(self.rewrite_prompt)
183 | prompt.update(ChatTurn(role="user", content=info))
184 | query = self.agent.chat([prompt], generation_config=self.gen_cfg)[0][0]
185 | return query
186 |
187 | def verify_contexts(
188 | self,
189 | contexts: list[RetrievedContext],
190 | question: str,
191 | ) -> bool:
192 | prompt = deepcopy(self.verify_prompt)
193 | user_prompt = ""
194 | for n, ctx in enumerate(contexts):
195 | user_prompt += f"Context {n}: {ctx.data['text']}\n\n"
196 | user_prompt += f"Topic: {question}"
197 | prompt.update(ChatTurn(role="user", content=user_prompt))
198 | response = self.agent.chat([prompt], generation_config=self.gen_cfg)[0][0]
199 | return "yes" in response.lower()
200 |
--------------------------------------------------------------------------------
/searchers/prompts/bm25_refine_emphasize_prompt.json:
--------------------------------------------------------------------------------
1 | {
2 | "system": {
3 | "role": "system",
4 | "content": "You are an AI assistant tasked with helping users refine their search queries. When given a context retrieved from an initial query, your goal is to suggest the keywords that should be emphasized to help obtain more relevant and specific information.\n\nFollow these guidelines:\n1. Analyze the Given Context: Carefully read the provided context to understand what information has already been retrieved.\n2. Identify Missing Information: Determine what specific details or aspects might be missing or could be more relevant to the user's needs.\n3. Indicate Important Keywords: Provide keywords or phrases that should be emphasized to refine the search and yield more precise results."
5 | },
6 | "history": [
7 | {
8 | "role": "user",
9 | "content": "The above context was retrieved using the given query. However, the information may not fully address user needs. To refine the search and obtain more relevant results, please suggest **one** keyword or phrase that should be emphasized to narrow down the search. Please only reply your keyword and do not output any other words."
10 | }
11 | ],
12 | "demonstrations": []
13 | }
--------------------------------------------------------------------------------
/searchers/prompts/bm25_refine_extend_prompt.json:
--------------------------------------------------------------------------------
1 | {
2 | "system": {
3 | "role": "system",
4 | "content": "You are an AI assistant tasked with helping users refine their search queries. When given a context retrieved from an initial query, your goal is to suggest additional keywords or phrases that could help obtain more relevant and specific information.\n\nFollow these guidelines:\n1. Analyze the Given Context: Carefully read the provided context to understand what information has already been retrieved.\n2. Identify Missing Information: Determine what specific details or aspects might be missing or could be more relevant to the user's needs.\n3. Suggest Additional Keywords: Provide additional keywords or phrases that could help refine the search and yield more precise results. Ensure these keywords are directly related to the topic and can help narrow down the search to more relevant information."
5 | },
6 | "history": [
7 | {
8 | "role": "user",
9 | "content": "The above context was retrieved using the given query. However, the information may not fully address user needs. To refine the search and obtain more relevant results, please suggest **one** additional keyword or phrases that could help narrow down the search. Please only reply your keyword and do not output any other words."
10 | }
11 | ],
12 | "demonstrations": []
13 | }
--------------------------------------------------------------------------------
/searchers/prompts/bm25_refine_filter_prompt.json:
--------------------------------------------------------------------------------
1 | {
2 | "system": {
3 | "role": "system",
4 | "content": "The following context is retrieved using the given query. However, the context may not contain the information you are looking for. Please provide one keyword to filter out irrelevant information.\n\nFollow these guidelines:\n1. Analyze the Given Context: Carefully read the provided context to understand what information has already been retrieved.\n2. Identify Missing Information: Determine what specific details or aspects might be missing or could be more relevant to the user's needs.\n 3. Indicate Filter Keyword: Provide a keyword that can help filter out irrelevant information and narrow down the search to more relevant results."
5 | },
6 | "history": [
7 | {
8 | "role": "user",
9 | "content": "The above context was retrieved using the given query. However, the information may not fully address user needs. To refine the search and obtain more relevant results, please suggest **one** filter word that could help narrow down the search. Please only reply your filter word and do not output any other words."
10 | }
11 | ],
12 | "demonstrations": []
13 | }
--------------------------------------------------------------------------------
/searchers/prompts/bm25_rewrite_prompt.json:
--------------------------------------------------------------------------------
1 | {
2 | "system": {
3 | "role": "system",
4 | "content": "Suggestions for Writing Queries for BM25 Search Engine\n1. Use Descriptive Keywords: Ensure your query includes all relevant keywords that describe what you are searching for.\n2. Incorporate Rare Terms: If you know any specific or rare terms related to your search, include them.\n3. Avoid Stop Words: Common words like \"the\", \"is\", and \"and\" may dilute the effectiveness of the query.\n4. Synonyms and Related Terms: Use synonyms and related terms to cover variations in how different documents might reference the same concept.\n5. Entity Searches: When searching for specific named entity, enclose them in double quotes.\nPlease optimize the following query for the BM25 Search Engine.\nPlease only reply your query and do not output any other words."
5 | },
6 | "history": [],
7 | "demonstrations": [
8 | [
9 | {
10 | "role": "user",
11 | "content": "What is John Mayne's occupation?"
12 | },
13 | {
14 | "role": "assistant",
15 | "content": "\"John Mayne\" occupation job career"
16 | }
17 | ],
18 | [
19 | {
20 | "role": "user",
21 | "content": "how many oar athletes are in the olympics"
22 | },
23 | {
24 | "role": "assistant",
25 | "content": "\"oar athletes\" olympics number count participants"
26 | }
27 | ],
28 | [
29 | {
30 | "role": "user",
31 | "content": "who introduced the system of civil services in india"
32 | },
33 | {
34 | "role": "assistant",
35 | "content": "india civil services introduced foundation"
36 | }
37 | ],
38 | [
39 | {
40 | "role": "user",
41 | "content": "Which leader did Hitler meet in the Brenner Pass in WWII?"
42 | },
43 | {
44 | "role": "assistant",
45 | "content": "Hitler \"Brenner Pass\" WWII leader meeting"
46 | }
47 | ],
48 | [
49 | {
50 | "role": "user",
51 | "content": "Which country does the airline Garuda come from?"
52 | },
53 | {
54 | "role": "assistant",
55 | "content": "Garuda airline country origin"
56 | }
57 | ]
58 | ]
59 | }
--------------------------------------------------------------------------------
/searchers/prompts/decompose_with_context_prompt.json:
--------------------------------------------------------------------------------
1 | {
2 | "system": {
3 | "role": "system",
4 | "content": "Please first indicate the additional knowledge needed to answer the following question. If the question can be answered without external knowledge, answer \"No additional information is required\".\n\n"
5 | },
6 | "history": [],
7 | "demonstrations": [
8 | [
9 | {
10 | "role": "user",
11 | "content": "Question: Which magazine was started first Arthur's Magazine or First for Women?\n\nContext 1: Arthur's Magazine was started in the 19th century.\n\nContext 2: First for Women was started in 1989."
12 | },
13 | {
14 | "role": "assistant",
15 | "content": "No additional information is required."
16 | }
17 | ],
18 | [
19 | {
20 | "role": "user",
21 | "content": "Question: What nationality was Henry Valentine Miller's wife?\n\nContext 1: Henry Valentine Miller's wife is June Miller."
22 | },
23 | {
24 | "role": "assistant",
25 | "content": "[1] What nationality was June Miller?"
26 | }
27 | ],
28 | [
29 | {
30 | "role": "user",
31 | "content": "Question: Are director of film Move (1970 Film) and director of film Méditerranée (1963 Film) from the same country?\n\nContext 1: Move (1970 Film) was directed by Stuart Rosenberg.\n\nContext 2: Méditerranée (1963 Film) was directed by Jean-Daniel Pollet."
32 | },
33 | {
34 | "role": "assistant",
35 | "content": "[1] What nationality was Stuart Rosenberg?\n[2] What nationality was Jean-Daniel Pollet?"
36 | }
37 | ],
38 | [
39 | {
40 | "role": "user",
41 | "content": "Question: Who is Rhescuporis I (Odrysian)'s paternal grandfather?\n\nContext 1: Rhescuporis I (Odrysian)'s father is Cotys III."
42 | },
43 | {
44 | "role": "assistant",
45 | "content": "[1] Who is Cotys III's father?"
46 | }
47 | ],
48 | [
49 | {
50 | "role": "user",
51 | "content": "Question: Which film came out first, The Love Route or Engal Aasan?\n\nContext 1: The Love Route came out in 1915.\n\nContext 2: Engal Aasan came out in 2009."
52 | },
53 | {
54 | "role": "assistant",
55 | "content": "No additional information is required."
56 | }
57 | ]
58 | ],
59 | "demonstrations_source": [
60 | "hotpotqa-train",
61 | "hotpotqa-train",
62 | "hotpotqa-train"
63 | ]
64 | }
--------------------------------------------------------------------------------
/searchers/prompts/decompose_without_context_prompt.json:
--------------------------------------------------------------------------------
1 | {
2 | "system": {
3 | "role": "system",
4 | "content": "Please identify the external knowledge necessary to answer the following question. If multiple concurrent pieces of external knowledge are needed, list them all. If the required external knowledge is interdependent, list only the initial piece of external knowledge needed."
5 | },
6 | "history": [],
7 | "demonstrations": [
8 | [
9 | {
10 | "role": "user",
11 | "content": "Question: Which magazine was started first Arthur's Magazine or First for Women?"
12 | },
13 | {
14 | "role": "assistant",
15 | "content": "[1] When Arthur's Magazine was founded.\n[2] When First for Women was founded."
16 | }
17 | ],
18 | [
19 | {
20 | "role": "user",
21 | "content": "Question: What nationality was Henry Valentine Miller's wife?"
22 | },
23 | {
24 | "role": "assistant",
25 | "content": "[1] Who was Henry Valentine Miller's wife?"
26 | }
27 | ],
28 | [
29 | {
30 | "role": "user",
31 | "content": "Question: Are director of film Move (1970 Film) and director of film Méditerranée (1963 Film) from the same country?"
32 | },
33 | {
34 | "role": "assistant",
35 | "content": "[1] Who direct the film Move (1970 Film)?\n[2] Who direct the film Méditerranée (1963 Film)?"
36 | }
37 | ],
38 | [
39 | {
40 | "role": "user",
41 | "content": "Question: Who is Rhescuporis I (Odrysian)'s paternal grandfather?"
42 | },
43 | {
44 | "role": "assistant",
45 | "content": "[1] Who is Rhescuporis I (Odrysian)'s father?"
46 | }
47 | ],
48 | [
49 | {
50 | "role": "user",
51 | "content": "Question: Which film came out first, The Love Route or Engal Aasan?"
52 | },
53 | {
54 | "role": "assistant",
55 | "content": "[1] When did The Love Route come out?\n[2] When did Engal Aasan come out?"
56 | }
57 | ]
58 | ],
59 | "demonstrations_source": [
60 | "hotpotqa-train",
61 | "hotpotqa-train",
62 | "2wikimultihopqa-train",
63 | "2wikimultihopqa-train"
64 | ]
65 | }
--------------------------------------------------------------------------------
/searchers/prompts/dense_rewrite_prompt.json:
--------------------------------------------------------------------------------
1 | {
2 | "system": {
3 | "role": "system",
4 | "content": "Suggestions for Writing Queries for Dense Retrieval Search Engine\n1. Use Natural Language: Dense retrieval models are designed to understand and process natural language. Formulate queries in complete sentences or phrases as you would ask a question in a conversation.\n2. Incorporate Context: Provide context to your query to help the model understand the specific aspect of the topic you are interested in. Contextual information improves the accuracy of the retrieval.\n3. Ask Specific Questions: Dense retrieval models perform well with specific queries. Instead of a single keyword, use detailed questions or statements to convey your information need.\n4. Avoid Overly Technical Language: While dense retrieval can handle a variety of terms, overly technical or jargon-heavy language might not be necessary. Aim for clarity and simplicity.\nPlease optimize the following query for the Dense Retrieval Search Engine.\nPlease only reply your query and do not output any other words."
5 | },
6 | "history": [],
7 | "demonstrations": [
8 | [
9 | {
10 | "role": "user",
11 | "content": "What is John Mayne's occupation?"
12 | },
13 | {
14 | "role": "assistant",
15 | "content": "What is John Mayne's occupation?"
16 | }
17 | ],
18 | [
19 | {
20 | "role": "user",
21 | "content": "The year my pet monster come out"
22 | },
23 | {
24 | "role": "assistant",
25 | "content": "When did My Pet Monster come out?"
26 | }
27 | ]
28 | ]
29 | }
--------------------------------------------------------------------------------
/searchers/prompts/lucene_rewrite_prompt.json:
--------------------------------------------------------------------------------
1 | {
2 | "system": {
3 | "role": "system",
4 | "content": "Suggestions for Writing Queries for BM25 Search Engine\n1. Use Descriptive Keywords: Ensure your query includes all relevant keywords that describe what you are searching for.\n2. Incorporate Rare Terms: If you know any specific or rare terms related to your search, include them.\n3. Avoid Stop Words: Common words like \"the\", \"is\", and \"and\" may dilute the effectiveness of the query.\n4. Synonyms and Related Terms: Use synonyms and related terms to cover variations in how different documents might reference the same concept.\n5. Phrase Searches: When searching for specific named entity, enclose them in double quotes.\n6. Use Boolean Operators: Use \"+\" for terms that must contains in the documents, \"-\" for terms that must not contains in the documents, and \"OR\" for terms that are optional.\nPlease optimize the following query for the BM25 Search Engine.\nPlease only reply your query and do not output any other words."
5 | },
6 | "history": [],
7 | "demonstrations": [
8 | [
9 | {
10 | "role": "user",
11 | "content": "What is John Mayne's occupation?"
12 | },
13 | {
14 | "role": "assistant",
15 | "content": "+\"John Mayne\" (occupation OR job OR career OR profession)"
16 | }
17 | ],
18 | [
19 | {
20 | "role": "user",
21 | "content": "what year did my pet monster come out"
22 | },
23 | {
24 | "role": "assistant",
25 | "content": "+\"my pet monster\" (year OR release OR debut OR came)"
26 | }
27 | ],
28 | [
29 | {
30 | "role": "user",
31 | "content": "Which prince is Queen Elizabeth II's youngest son?"
32 | },
33 | {
34 | "role": "assistant",
35 | "content": "+\"Queen Elizabeth II\" +youngest +(prince OR son OR child)"
36 | }
37 | ]
38 | ]
39 | }
--------------------------------------------------------------------------------
/searchers/prompts/rewrite_by_answer_with_context_prompt.json:
--------------------------------------------------------------------------------
1 | {
2 | "system": {
3 | "role": "system",
4 | "content": "Please answer the following question using the provided contexts if relevant. If the provided contexts are not relevant, base your answer on your own knowledge."
5 | },
6 | "history": [],
7 | "demonstrations": []
8 | }
--------------------------------------------------------------------------------
/searchers/prompts/rewrite_by_answer_without_context_prompt.json:
--------------------------------------------------------------------------------
1 | {
2 | "system": {
3 | "role": "system",
4 | "content": "Please write a passage to answer the question."
5 | },
6 | "history": [],
7 | "demonstrations": []
8 | }
--------------------------------------------------------------------------------
/searchers/prompts/summarize_by_answer_prompt.json:
--------------------------------------------------------------------------------
1 | {
2 | "system": {
3 | "role": "system",
4 | "content": "Answer the following question in a single sentence using the provided contexts if relevant; if not, answer directly without additional words."
5 | },
6 | "history": [],
7 | "demonstrations": []
8 | }
--------------------------------------------------------------------------------
/searchers/prompts/verify_prompt.json:
--------------------------------------------------------------------------------
1 | {
2 | "system": {
3 | "role": "system",
4 | "content": "Your task is to verify whether any of the following contexts contains enough information to answer the following question. Please only reply 'yes' or 'no' and do not output any other words."
5 | },
6 | "history": [],
7 | "demonstrations": []
8 | }
--------------------------------------------------------------------------------
/searchers/prompts/web_rewrite_prompt.json:
--------------------------------------------------------------------------------
1 | {
2 | "system": {
3 | "role": "system",
4 | "content": "Please optimize the following query for the Web search engine.\nSuggestions for Writing Queries for Web Search Engines:\n1. Use Specific Keywords: Identify and use the most relevant and specific keywords related to your search topic. This helps narrow down the results to the most pertinent web pages.\n2. Phrase Searches: Enclose exact phrases in quotation marks to search for those exact words in that exact order. This is useful for finding specific quotes, names, or titles.\n3. Ask Questions: Formulate your query as a question to get direct answers. For example, \"How to cook pasta?\" is likely to return step-by-step instructions.\n4. Synonyms and Variants: Include synonyms or different variations of a word to broaden your search. For instance, \"smartphone\" and \"mobile phone\" can yield different results.\nPlease only reply your query and do not output any other words."
5 | },
6 | "history": [],
7 | "demonstrations": [
8 | [
9 | {
10 | "role": "user",
11 | "content": "What is John Mayne's occupation?"
12 | },
13 | {
14 | "role": "assistant",
15 | "content": "What is \"John Mayne\"'s occupation?"
16 | }
17 | ],
18 | [
19 | {
20 | "role": "user",
21 | "content": "The year my pet monster come out"
22 | },
23 | {
24 | "role": "assistant",
25 | "content": "When did \"My Pet Monster\" come out?"
26 | }
27 | ]
28 | ]
29 | }
--------------------------------------------------------------------------------
/searchers/searcher.py:
--------------------------------------------------------------------------------
1 | from abc import abstractmethod
2 | from copy import deepcopy
3 | from dataclasses import dataclass, field
4 |
5 | from flexrag.assistant import PREDEFINED_PROMPTS, AssistantBase
6 | from flexrag.common_dataclass import RetrievedContext
7 | from flexrag.models import GENERATORS, GenerationConfig, GeneratorConfig
8 | from flexrag.prompt import ChatPrompt, ChatTurn
9 | from flexrag.utils import LOGGER_MANAGER, Choices
10 |
11 | logger = LOGGER_MANAGER.getLogger("levelrag.searcher")
12 |
13 |
14 | @dataclass
15 | class BaseSearcherConfig(GeneratorConfig):
16 | gen_cfg: GenerationConfig = field(default_factory=GenerationConfig)
17 | response_type: Choices(["short", "long", "original"]) = "short" # type: ignore
18 |
19 |
20 | class BaseSearcher(AssistantBase):
21 | def __init__(self, cfg: BaseSearcherConfig) -> None:
22 | self.agent = GENERATORS.load(cfg)
23 | self.gen_cfg = cfg.gen_cfg
24 | if self.gen_cfg.sample_num > 1:
25 | logger.warning("Sample num > 1 is not supported for Searcher")
26 | self.gen_cfg.sample_num = 1
27 |
28 | # load assistant prompt
29 | match cfg.response_type:
30 | case "short":
31 | self.prompt_with_ctx = PREDEFINED_PROMPTS["shortform_with_context"]
32 | self.prompt_wo_ctx = PREDEFINED_PROMPTS["shortform_without_context"]
33 | case "long":
34 | self.prompt_with_ctx = PREDEFINED_PROMPTS["longform_with_context"]
35 | self.prompt_wo_ctx = PREDEFINED_PROMPTS["longform_without_context"]
36 | case "original":
37 | self.prompt_with_ctx = ChatPrompt()
38 | self.prompt_wo_ctx = ChatPrompt()
39 | case _:
40 | raise ValueError(f"Invalid response type: {cfg.response_type}")
41 | return
42 |
43 | @abstractmethod
44 | def search(
45 | self, question: str
46 | ) -> tuple[list[RetrievedContext], list[dict[str, object]]]:
47 | return
48 |
49 | def answer(self, question: str) -> tuple[str, list[RetrievedContext], dict]:
50 | ctxs, history = self.search(question)
51 | response, prompt = self.answer_with_contexts(question, ctxs)
52 | return response, ctxs, {"prompt": prompt, "search_histories": history}
53 |
54 | def answer_with_contexts(
55 | self, question: str, contexts: list[RetrievedContext] = []
56 | ) -> tuple[str, ChatPrompt]:
57 | """Answer question with given contexts
58 |
59 | Args:
60 | question (str): The question to answer.
61 | contexts (list): The contexts searched by the searcher.
62 |
63 | Returns:
64 | response (str): response to the question
65 | prompt (ChatPrompt): prompt used.
66 | """
67 | # prepare system prompt
68 | if len(contexts) > 0:
69 | prompt = deepcopy(self.prompt_with_ctx)
70 | else:
71 | prompt = deepcopy(self.prompt_wo_ctx)
72 |
73 | # prepare user prompt
74 | usr_prompt = ""
75 | for n, context in enumerate(contexts):
76 | ctx = context.data.get("text")
77 | usr_prompt += f"Context {n + 1}: {ctx}\n\n"
78 | usr_prompt += f"Question: {question}"
79 |
80 | # generate response
81 | prompt.update(ChatTurn(role="user", content=usr_prompt))
82 | response = self.agent.chat([prompt], generation_config=self.gen_cfg)[0][0]
83 | return response, prompt
84 |
--------------------------------------------------------------------------------
/searchers/web_searcher.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | from copy import deepcopy
4 | from dataclasses import dataclass
5 |
6 | from flexrag.assistant import ASSISTANTS
7 | from flexrag.common_dataclass import RetrievedContext
8 | from flexrag.prompt import ChatPrompt, ChatTurn
9 | from flexrag.retriever import SimpleWebRetriever, SimpleWebRetrieverConfig
10 |
11 | from .searcher import BaseSearcher, BaseSearcherConfig
12 |
13 | logger = logging.getLogger("WebSearcher")
14 |
15 |
16 | @dataclass
17 | class WebSearcherConfig(BaseSearcherConfig, SimpleWebRetrieverConfig):
18 | rewrite_query: bool = False
19 |
20 |
21 | @ASSISTANTS("web", config_class=WebSearcherConfig)
22 | class WebSearcher(BaseSearcher):
23 | def __init__(self, cfg: WebSearcherConfig) -> None:
24 | super().__init__(cfg)
25 | # setup Web Searcher
26 | self.rewrite = cfg.rewrite_query
27 |
28 | # load Web Retrieve
29 | self.retriever = SimpleWebRetriever(cfg)
30 |
31 | # load prompt
32 | self.rewrite_prompt = ChatPrompt.from_json(
33 | os.path.join(
34 | os.path.dirname(__file__),
35 | "prompts",
36 | "web_rewrite_prompt.json",
37 | )
38 | )
39 | return
40 |
41 | def search(
42 | self, question: str
43 | ) -> tuple[list[RetrievedContext], list[dict[str, object]]]:
44 | # initialize search stack
45 | if self.rewrite:
46 | query_to_search = self.rewrite_query(question)
47 | else:
48 | query_to_search = question
49 | ctxs = self.retriever.search(query=[query_to_search])[0]
50 | for ctx in ctxs:
51 | ctx.data["text"] = ctx.data["snippet"]
52 | return ctxs, []
53 |
54 | def rewrite_query(self, info: str) -> str:
55 | # Rewrite the query to be more informative
56 | user_prompt = f"Query: {info}"
57 | prompt = deepcopy(self.rewrite_prompt)
58 | prompt.update(ChatTurn(role="user", content=user_prompt))
59 | query = self.agent.chat([prompt], generation_config=self.gen_cfg)[0][0]
60 | return query
61 |
--------------------------------------------------------------------------------